Skip to content

Commit f26a924

Browse files
authored
Merge pull request #52 from biosimulators/import-copasi
Allow the COPASI Biosimulator to simulate files exported by COPASI
2 parents 298f282 + 69cf63b commit f26a924

File tree

8 files changed

+248
-9
lines changed

8 files changed

+248
-9
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ venv.bak/
120120
# Temp directory
121121
temp/
122122
tests/results/
123+
tests/out/
123124

124125
*.tmp
125126

biosimulators_copasi/__main__.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,91 @@
1010
from . import get_simulator_version
1111
from ._version import __version__
1212
from .core import exec_sedml_docs_in_combine_archive
13+
from .utils import fix_copasi_generated_combine_archive as fix_copasi_generated_combine_archive_func
14+
from biosimulators_utils.config import get_config
1315
from biosimulators_utils.simulator.cli import build_cli
16+
from biosimulators_utils.simulator.data_model import EnvironmentVariable
17+
from biosimulators_utils.simulator.environ import ENVIRONMENT_VARIABLES as DEFAULT_ENVIRONMENT_VARIABLES
18+
import cement
19+
import termcolor
20+
21+
ENVIRONMENT_VARIABLES = list(DEFAULT_ENVIRONMENT_VARIABLES.values())
22+
ENVIRONMENT_VARIABLES.append(
23+
EnvironmentVariable(
24+
name='FIX_COPASI_GENERATED_COMBINE_ARCHIVE',
25+
description=(
26+
'Whether to make COPASI-generated COMBINE archives compatible with the '
27+
'specifications of the OMEX manifest and SED-ML standards.'
28+
),
29+
options=['0', '1'],
30+
default='0',
31+
more_info_url='https://docs.biosimulators.org/Biosimulators_COPASI/source/biosimulators_copasi.html',
32+
)
33+
)
1434

1535
App = build_cli('biosimulators-copasi', __version__,
1636
'COPASI', get_simulator_version(), 'http://copasi.org',
1737
exec_sedml_docs_in_combine_archive,
38+
environment_variables=ENVIRONMENT_VARIABLES,
1839
)
1940

2041

2142
def main():
2243
with App() as app:
2344
app.run()
45+
46+
47+
class FixCopasiGeneratedCombineArchiveController(cement.Controller):
48+
""" Controller for fixing COPASI-generated COMBINE archives """
49+
50+
class Meta:
51+
label = 'base'
52+
help = 'Fix a COPASI-generated COMBINE/OMEX archive'
53+
description = (
54+
'Correct a COPASI-generated COMBINE/OMEX archive to be consistent with '
55+
'the specifications of the OMEX manifest and SED-ML formats'
56+
)
57+
arguments = [
58+
(
59+
['-i', '--in-file'],
60+
dict(
61+
type=str,
62+
required=True,
63+
help='Path to COMBINE/OMEX file to correct',
64+
),
65+
),
66+
(
67+
['-o', '--out-file'],
68+
dict(
69+
type=str,
70+
required=True,
71+
help='Path to save the corrected archive',
72+
),
73+
),
74+
]
75+
76+
@cement.ex(hide=True)
77+
def _default(self):
78+
args = self.app.pargs
79+
config = get_config()
80+
try:
81+
fix_copasi_generated_combine_archive_func(args.in_file, args.out_file)
82+
except Exception as exception:
83+
if config.DEBUG:
84+
raise
85+
raise SystemExit(termcolor.colored(str(exception), 'red')) from exception
86+
87+
88+
class FixCopasiGeneratedCombineArchiveApp(cement.App):
89+
""" Command line application for fixing COPASI-generated COMBINE/OMEX archives """
90+
class Meta:
91+
label = 'fix-copasi-generated-combine-archive'
92+
base_controller = 'base'
93+
handlers = [
94+
FixCopasiGeneratedCombineArchiveController,
95+
]
96+
97+
98+
def fix_copasi_generated_combine_archive():
99+
with FixCopasiGeneratedCombineArchiveApp() as app:
100+
app.run()

biosimulators_copasi/core.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,19 @@
2222
from kisao.data_model import AlgorithmSubstitutionPolicy, ALGORITHM_SUBSTITUTION_POLICY_LEVELS
2323
from .data_model import KISAO_ALGORITHMS_MAP, Units
2424
from .utils import (get_algorithm_id, set_algorithm_parameter_value,
25-
get_copasi_model_object_by_sbml_id, get_copasi_model_obj_sbml_ids)
25+
get_copasi_model_object_by_sbml_id, get_copasi_model_obj_sbml_ids,
26+
fix_copasi_generated_combine_archive as fix_copasi_generated_combine_archive_func)
2627
import COPASI
2728
import lxml
2829
import math
2930
import numpy
30-
31+
import os
32+
import tempfile
3133

3234
__all__ = ['exec_sedml_docs_in_combine_archive', 'exec_sed_doc', 'exec_sed_task', 'preprocess_sed_task']
3335

3436

35-
def exec_sedml_docs_in_combine_archive(archive_filename, out_dir, config=None):
37+
def exec_sedml_docs_in_combine_archive(archive_filename, out_dir, config=None, fix_copasi_generated_combine_archive=None):
3638
""" Execute the SED tasks defined in a COMBINE/OMEX archive and save the outputs
3739
3840
Args:
@@ -45,16 +47,31 @@ def exec_sedml_docs_in_combine_archive(archive_filename, out_dir, config=None):
4547
with reports at keys ``{ relative-path-to-SED-ML-file-within-archive }/{ report.id }`` within the HDF5 file
4648
4749
config (:obj:`Config`, optional): BioSimulators common configuration
50+
fix_copasi_generated_combine_archive (:obj:`bool`, optional): Whether to make COPASI-generated COMBINE archives
51+
compatible with the specifications of the OMEX manifest and SED-ML standards
4852
4953
Returns:
5054
:obj:`tuple`:
5155
5256
* :obj:`SedDocumentResults`: results
5357
* :obj:`CombineArchiveLog`: log
5458
"""
55-
return exec_sedml_docs_in_archive(exec_sed_doc, archive_filename, out_dir,
56-
apply_xml_model_changes=True,
57-
config=config)
59+
if fix_copasi_generated_combine_archive is None:
60+
fix_copasi_generated_combine_archive = os.getenv('FIX_COPASI_GENERATED_COMBINE_ARCHIVE', '0').lower() in ['1', 'true']
61+
62+
if fix_copasi_generated_combine_archive:
63+
temp_archive_file, temp_archive_filename = tempfile.mkstemp()
64+
os.close(temp_archive_file)
65+
fix_copasi_generated_combine_archive_func(archive_filename, temp_archive_filename)
66+
archive_filename = temp_archive_filename
67+
68+
result = exec_sedml_docs_in_archive(exec_sed_doc, archive_filename, out_dir,
69+
apply_xml_model_changes=True,
70+
config=config)
71+
if fix_copasi_generated_combine_archive:
72+
os.remove(temp_archive_filename)
73+
74+
return result
5875

5976

6077
def exec_sed_doc(doc, working_dir, base_out_path, rel_out_path=None,

biosimulators_copasi/utils.py

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,28 @@
77
"""
88

99
from .data_model import KISAO_ALGORITHMS_MAP, KISAO_PARAMETERS_MAP, Units
10-
from biosimulators_utils.config import Config # noqa: F401
10+
from biosimulators_utils.combine.data_model import CombineArchiveContentFormat
11+
from biosimulators_utils.combine.io import CombineArchiveReader, CombineArchiveWriter
12+
from biosimulators_utils.config import get_config, Config # noqa: F401
1113
from biosimulators_utils.data_model import ValueType
1214
from biosimulators_utils.simulator.utils import get_algorithm_substitution_policy
1315
from biosimulators_utils.utils.core import validate_str_value, parse_value
1416
from kisao.data_model import AlgorithmSubstitutionPolicy, ALGORITHM_SUBSTITUTION_POLICY_LEVELS
1517
from kisao.utils import get_preferred_substitute_algorithm_by_ids
1618
import COPASI
1719
import itertools
20+
import libsedml
21+
import lxml
22+
import os
23+
import shutil
24+
import tempfile
1825

1926
__all__ = [
2027
'get_algorithm_id',
2128
'set_algorithm_parameter_value',
2229
'get_copasi_model_object_by_sbml_id',
2330
'get_copasi_model_obj_sbml_ids',
31+
'fix_copasi_generated_combine_archive',
2432
]
2533

2634

@@ -241,3 +249,61 @@ def get_copasi_model_obj_sbml_ids(model):
241249
ids.append(object.getSBMLId())
242250

243251
return ids
252+
253+
254+
def fix_copasi_generated_combine_archive(in_filename, out_filename, config=None):
255+
""" Utility function that corrects COMBINE/OMEX archives generated by COPASI so they are compatible
256+
with other tools.
257+
258+
All currently released versions of COPASI export COMBINE archive files. However, these archives
259+
presently diverge from the specifications of the SED-ML format.
260+
261+
* Format in OMEX manifests is not a valid PURL media type URI
262+
* SED-ML files lack namespaces for SBML
263+
264+
Args:
265+
in_filename (:obj:`str`): path to a COMBINE archive to correct
266+
out_filename (:obj:`str`): path to save correctd COMBINE archive
267+
config (:obj:`Config`, optional): BioSimulators-utils configuration
268+
"""
269+
config = config or get_config()
270+
archive_dirname = tempfile.mkdtemp()
271+
try:
272+
archive = CombineArchiveReader().run(in_filename, archive_dirname, config=config)
273+
except Exception:
274+
shutil.rmtree(archive_dirname)
275+
raise
276+
277+
# correct URI for COPASI application format
278+
for content in archive.contents:
279+
if content.format == 'application/x-copasi':
280+
content.format = CombineArchiveContentFormat.CopasiML
281+
# potentially issue warning messages if needed
282+
break
283+
284+
# add SBML namespace to SED-ML file
285+
ns = None
286+
for content in archive.contents:
287+
if content.format == 'http://identifiers.org/combine.specifications/sbml':
288+
with open(os.path.join(archive_dirname, content.location), 'rb') as sbml:
289+
root = lxml.etree.parse(sbml)
290+
# get default ns
291+
ns = root.getroot().nsmap[None]
292+
break
293+
294+
if ns:
295+
for content in archive.contents:
296+
if content.format == 'http://identifiers.org/combine.specifications/sed-ml':
297+
sedml_file = os.path.join(archive_dirname, content.location)
298+
doc = libsedml.readSedMLFromFile(sedml_file)
299+
sedml_ns = doc.getSedNamespaces().getNamespaces()
300+
if not sedml_ns.hasPrefix('sbml'):
301+
sedml_ns.add(ns, 'sbml')
302+
libsedml.writeSedMLToFile(doc, sedml_file)
303+
# potentially issue warning message here, that the sedml file had no SBML prefix and it was added
304+
break
305+
306+
try:
307+
CombineArchiveWriter().run(archive, archive_dirname, out_filename)
308+
finally:
309+
shutil.rmtree(archive_dirname)

docs-src/tutorial.rst

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ New values of parameter KISAO_0000534 (list of deterministic reactions) of KISAO
2424
</algorithm>
2525
2626
27-
Command-line program
28-
--------------------
27+
Command-line program for executing COMBINE/OMEX archives
28+
--------------------------------------------------------
2929

3030
The command-line program can be used to execute COMBINE/OMEX archives that describe simulations as illustrated below.
3131

@@ -69,3 +69,31 @@ For example, the following command could be used to use the Docker image to exec
6969
ghcr.io/biosimulators/copasi:latest \
7070
-i /tmp/working-dir/modeling-study.omex \
7171
-o /tmp/working-dir
72+
73+
Command-line program for correcting COMBINE/OMEX archives created by COPASI
74+
---------------------------------------------------------------------------
75+
76+
The ``fix-copasi-generated-combine-archive`` command-line program can be used to align COMBINE/OMEX archives created by COPASI with the specifications of the OMEX manifest and SED-ML formats.
77+
78+
.. code-block:: text
79+
80+
usage: fix-copasi-generated-combine-archive [-h] [-d] [-q] -i IN_FILE -o OUT_FILE
81+
82+
Correct a COPASI-generated COMBINE/OMEX archive to be consistent with the specifications of the OMEX manifest and SED-ML formats
83+
84+
optional arguments:
85+
-h, --help show this help message and exit
86+
-d, --debug full application debug mode
87+
-q, --quiet suppress all console output
88+
-i IN_FILE, --in-file IN_FILE
89+
Path to COMBINE/OMEX file to correct
90+
-o OUT_FILE, --out-file OUT_FILE
91+
Path to save the corrected archive
92+
93+
For example, the following command could be used to correct the example COPASI-generated archive in the ``tests/fixtures`` directory:
94+
95+
.. code-block:: text
96+
97+
fix-copasi-generated-combine-archive \
98+
-i tests/fixtures/copasi-34-export.omex \
99+
-o tests/fixtures/copasi-34-export-fixed.omex

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
entry_points={
4949
'console_scripts': [
5050
'biosimulators-copasi = biosimulators_copasi.__main__:main',
51+
'fix-copasi-generated-combine-archive = biosimulators_copasi.__main__:fix_copasi_generated_combine_archive',
5152
],
5253
},
5354
)
13.9 KB
Binary file not shown.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import shutil
2+
import tempfile
3+
import unittest
4+
import os
5+
from unittest import mock
6+
7+
from biosimulators_utils.combine.io import CombineArchiveReader, CombineArchiveWriter
8+
from biosimulators_utils.config import get_config
9+
from biosimulators_utils.log.data_model import Status
10+
11+
import biosimulators_copasi
12+
import biosimulators_copasi.utils
13+
14+
15+
class FixCopasiGeneratedCombineArchiveTestCase(unittest.TestCase):
16+
def setUp(self):
17+
dirname = os.path.dirname(__file__)
18+
self.filename = os.path.join(dirname, 'fixtures', 'copasi-34-export.omex')
19+
self.assertTrue(os.path.exists(self.filename))
20+
self.archive_tmp_dir = tempfile.mkdtemp()
21+
self.out_dir = tempfile.mkdtemp()
22+
23+
def tearDown(self):
24+
shutil.rmtree(self.archive_tmp_dir)
25+
shutil.rmtree(self.out_dir)
26+
27+
@unittest.expectedFailure
28+
def test_execute_copasi_generated_archive(self):
29+
# ideally this shouldn't fail
30+
results, log = biosimulators_copasi.exec_sedml_docs_in_combine_archive(self.filename, self.out_dir)
31+
self.assertEqual(log.status, Status.SUCCEEDED)
32+
33+
def test_executed_fixed_copasi_generated_archive(self):
34+
# fix the archive
35+
corrected_filename = os.path.join(self.archive_tmp_dir, 'archive.omex')
36+
biosimulators_copasi.utils.fix_copasi_generated_combine_archive(self.filename, corrected_filename)
37+
38+
# check that the corrected archive can be executed
39+
results, log = biosimulators_copasi.exec_sedml_docs_in_combine_archive(corrected_filename, self.out_dir)
40+
self.assertEqual(log.status, Status.SUCCEEDED)
41+
42+
results, log = biosimulators_copasi.exec_sedml_docs_in_combine_archive(
43+
self.filename, self.out_dir, fix_copasi_generated_combine_archive=True)
44+
self.assertEqual(log.status, Status.SUCCEEDED)
45+
46+
with mock.patch.dict(os.environ, {'FIX_COPASI_GENERATED_COMBINE_ARCHIVE': '1'}):
47+
results, log = biosimulators_copasi.exec_sedml_docs_in_combine_archive(
48+
self.filename, self.out_dir)
49+
self.assertEqual(log.status, Status.SUCCEEDED)

0 commit comments

Comments
 (0)