Skip to content

Commit 45a3371

Browse files
authored
Add SbmlModel.from_antimony (#331)
Simplify creating a PEtab SbmlModel from antimony files or strings. Replace simplesbml by antimony in tests.
1 parent 980926f commit 45a3371

10 files changed

+191
-122
lines changed

petab/v1/models/sbml_model.py

+77-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Functions for handling SBML models"""
2+
from __future__ import annotations
23

34
import itertools
45
from collections.abc import Iterable
@@ -32,8 +33,25 @@ def __init__(
3233
sbml_document: libsbml.SBMLDocument = None,
3334
model_id: str = None,
3435
):
36+
"""Constructor.
37+
38+
:param sbml_model: SBML model. Optional if `sbml_document` is given.
39+
:param sbml_reader: SBML reader. Optional.
40+
:param sbml_document: SBML document. Optional if `sbml_model` is given.
41+
:param model_id: Model ID. Defaults to the SBML model ID."""
3542
super().__init__()
3643

44+
if sbml_model is None and sbml_document is None:
45+
raise ValueError(
46+
"Either sbml_model or sbml_document must be given."
47+
)
48+
49+
if sbml_model is None:
50+
sbml_model = sbml_document.getModel()
51+
52+
if sbml_document is None:
53+
sbml_document = sbml_model.getSBMLDocument()
54+
3755
self.sbml_reader: libsbml.SBMLReader | None = sbml_reader
3856
self.sbml_document: libsbml.SBMLDocument | None = sbml_document
3957
self.sbml_model: libsbml.Model | None = sbml_model
@@ -70,7 +88,7 @@ def __setstate__(self, state):
7088
self.__dict__.update(state)
7189

7290
@staticmethod
73-
def from_file(filepath_or_buffer, model_id: str = None):
91+
def from_file(filepath_or_buffer, model_id: str = None) -> SbmlModel:
7492
sbml_reader, sbml_document, sbml_model = get_sbml_model(
7593
filepath_or_buffer
7694
)
@@ -82,7 +100,12 @@ def from_file(filepath_or_buffer, model_id: str = None):
82100
)
83101

84102
@staticmethod
85-
def from_string(sbml_string, model_id: str = None):
103+
def from_string(sbml_string, model_id: str = None) -> SbmlModel:
104+
"""Create SBML model from an SBML string.
105+
106+
:param sbml_string: SBML model as string.
107+
:param model_id: Model ID. Defaults to the SBML model ID.
108+
"""
86109
sbml_reader, sbml_document, sbml_model = load_sbml_from_string(
87110
sbml_string
88111
)
@@ -97,6 +120,18 @@ def from_string(sbml_string, model_id: str = None):
97120
model_id=model_id,
98121
)
99122

123+
@staticmethod
124+
def from_antimony(ant_model: str | Path) -> SbmlModel:
125+
"""Create SBML model from an Antimony model.
126+
127+
Requires the `antimony` package (https://github.com/sys-bio/antimony).
128+
129+
:param ant_model: Antimony model as string or path to file.
130+
Strings are interpreted as Antimony model strings.
131+
"""
132+
sbml_str = antimony2sbml(ant_model)
133+
return SbmlModel.from_string(sbml_str)
134+
100135
@property
101136
def model_id(self):
102137
return self._model_id
@@ -238,3 +273,43 @@ def sympify_sbml(sbml_obj: libsbml.ASTNode | libsbml.SBase) -> sp.Expr:
238273
)
239274

240275
return sp.sympify(formula_str, locals=_clash)
276+
277+
278+
def antimony2sbml(ant_model: str | Path) -> str:
279+
"""Convert Antimony model to SBML.
280+
281+
:param ant_model: Antimony model as string or path to file.
282+
Strings are interpreted as Antimony model strings.
283+
284+
:returns:
285+
The SBML model as string.
286+
"""
287+
import antimony as ant
288+
289+
# Unload everything / free memory
290+
ant.clearPreviousLoads()
291+
ant.freeAll()
292+
293+
try:
294+
# potentially fails because of too long file name
295+
is_file = ant_model and Path(ant_model).exists()
296+
except OSError:
297+
is_file = False
298+
299+
if is_file:
300+
status = ant.loadAntimonyFile(str(ant_model))
301+
else:
302+
status = ant.loadAntimonyString(ant_model)
303+
if status < 0:
304+
raise RuntimeError(
305+
f"Antimony model could not be loaded: {ant.getLastError()}"
306+
)
307+
308+
if (main_module_name := ant.getMainModuleName()) is None:
309+
raise AssertionError("There is no Antimony module.")
310+
311+
sbml_str = ant.getSBMLString(main_module_name)
312+
if not sbml_str:
313+
raise ValueError("Antimony model could not be converted to SBML.")
314+
315+
return sbml_str

pyproject.toml

+6-2
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,11 @@ maintainers = [
3535

3636
[project.optional-dependencies]
3737
tests = [
38+
"antimony>=2.14.0",
39+
"pysb",
3840
"pytest",
3941
"pytest-cov",
40-
"simplesbml",
4142
"scipy",
42-
"pysb",
4343
]
4444
quality = [
4545
"pre-commit",
@@ -48,6 +48,9 @@ reports = [
4848
# https://github.com/spatialaudio/nbsphinx/issues/641
4949
"Jinja2==3.0.3",
5050
]
51+
antimony = [
52+
"antimony>=2.14.0",
53+
]
5154
combine = [
5255
"python-libcombine>=0.2.6",
5356
]
@@ -61,6 +64,7 @@ doc = [
6164
# https://github.com/spatialaudio/nbsphinx/issues/687#issuecomment-1339271312
6265
"ipython>=7.21.0, !=8.7.0",
6366
"pysb",
67+
"antimony>=2.14.0"
6468
]
6569
vis = [
6670
"matplotlib>=3.6.0",

tests/v1/test_combine.py

+4-6
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44

55
import pandas as pd
66

7-
import petab
7+
import petab.v1 as petab
88
from petab.C import *
9+
from petab.v1.models.sbml_model import SbmlModel
910

1011
# import fixtures
1112
pytest_plugins = [
@@ -16,10 +17,7 @@
1617
def test_combine_archive():
1718
"""Test `create_combine_archive` and `Problem.from_combine`"""
1819
# Create test files
19-
import simplesbml
20-
21-
ss_model = simplesbml.SbmlModel()
22-
20+
model = SbmlModel.from_antimony("")
2321
# Create tables with arbitrary content
2422
measurement_df = pd.DataFrame(
2523
data={
@@ -80,7 +78,7 @@ def test_combine_archive():
8078
) as tempdir:
8179
# Write test data
8280
outdir = Path(tempdir)
83-
petab.write_sbml(ss_model.document, outdir / sbml_file_name)
81+
model.to_file(outdir / sbml_file_name)
8482
petab.write_measurement_df(
8583
measurement_df, outdir / measurement_file_name
8684
)

tests/v1/test_deprecated.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ def test_problem_with_sbml_model():
1414
"""Test that a problem can be correctly created from sbml model."""
1515
# retrieve test data
1616
(
17-
ss_model,
17+
model,
1818
condition_df,
1919
observable_df,
2020
measurement_df,
@@ -23,7 +23,7 @@ def test_problem_with_sbml_model():
2323

2424
with pytest.deprecated_call():
2525
petab_problem = petab.Problem( # noqa: F811
26-
sbml_model=ss_model.model,
26+
model=model,
2727
condition_df=condition_df,
2828
measurement_df=measurement_df,
2929
parameter_df=parameter_df,

tests/v1/test_lint.py

+18-15
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818

1919
def test_assert_measured_observables_present():
2020
# create test model
21-
2221
measurement_df = pd.DataFrame(
2322
data={
2423
OBSERVABLE_ID: ["non-existing1"],
@@ -255,15 +254,15 @@ def test_assert_no_leading_trailing_whitespace():
255254

256255

257256
def test_assert_model_parameters_in_condition_or_parameter_table():
258-
import simplesbml
259-
260257
from petab.models.sbml_model import SbmlModel
261258

262-
ss_model = simplesbml.SbmlModel()
263-
ss_model.addParameter("parameter1", 0.0)
264-
ss_model.addParameter("noiseParameter1_", 0.0)
265-
ss_model.addParameter("observableParameter1_", 0.0)
266-
sbml_model = SbmlModel(sbml_model=ss_model.model)
259+
ant_model = """
260+
parameter1 = 0.0
261+
noiseParameter1_ = 0.0
262+
observableParameter1_ = 0.0
263+
"""
264+
sbml_model = SbmlModel.from_antimony(ant_model)
265+
assert sbml_model.is_valid()
267266

268267
lint.assert_model_parameters_in_condition_or_parameter_table(
269268
sbml_model, pd.DataFrame(columns=["parameter1"]), pd.DataFrame()
@@ -284,7 +283,10 @@ def test_assert_model_parameters_in_condition_or_parameter_table():
284283
sbml_model, pd.DataFrame(), pd.DataFrame()
285284
)
286285

287-
ss_model.addAssignmentRule("parameter1", "parameter2")
286+
sbml_model = SbmlModel.from_antimony(
287+
ant_model + "\nparameter2 = 0\nparameter1 := parameter2"
288+
)
289+
assert sbml_model.is_valid()
288290
lint.assert_model_parameters_in_condition_or_parameter_table(
289291
sbml_model, pd.DataFrame(), pd.DataFrame()
290292
)
@@ -499,12 +501,11 @@ def test_assert_measurement_conditions_present_in_condition_table():
499501

500502
def test_check_condition_df():
501503
"""Check that we correctly detect errors in condition table"""
502-
import simplesbml
503504

504505
from petab.models.sbml_model import SbmlModel
505506

506-
ss_model = simplesbml.SbmlModel()
507-
model = SbmlModel(sbml_model=ss_model.model)
507+
model = SbmlModel.from_antimony("")
508+
508509
condition_df = pd.DataFrame(
509510
data={
510511
CONDITION_ID: ["condition1"],
@@ -527,7 +528,7 @@ def test_check_condition_df():
527528
lint.check_condition_df(condition_df, model, observable_df)
528529

529530
# fix by adding parameter
530-
ss_model.addParameter("p1", 1.0)
531+
model = SbmlModel.from_antimony("p1 = 1")
531532
lint.check_condition_df(condition_df, model)
532533

533534
# species missing in model
@@ -536,7 +537,7 @@ def test_check_condition_df():
536537
lint.check_condition_df(condition_df, model)
537538

538539
# fix:
539-
ss_model.addSpecies("[s1]", 1.0)
540+
model = SbmlModel.from_antimony("p1 = 1; species s1 = 1")
540541
lint.check_condition_df(condition_df, model)
541542

542543
# compartment missing in model
@@ -545,7 +546,9 @@ def test_check_condition_df():
545546
lint.check_condition_df(condition_df, model)
546547

547548
# fix:
548-
ss_model.addCompartment(comp_id="c2", vol=1.0)
549+
model = SbmlModel.from_antimony(
550+
"p1 = 1; species s1 = 1; compartment c2 = 1"
551+
)
549552
lint.check_condition_df(condition_df, model)
550553

551554

tests/v1/test_observables.py

+5-12
Original file line numberDiff line numberDiff line change
@@ -69,14 +69,11 @@ def test_write_observable_df():
6969

7070
def test_get_output_parameters():
7171
"""Test measurements.get_output_parameters."""
72-
# sbml model
73-
import simplesbml
74-
7572
from petab.models.sbml_model import SbmlModel
7673

77-
ss_model = simplesbml.SbmlModel()
78-
ss_model.addParameter("fixedParameter1", 1.0)
79-
ss_model.addParameter("observable_1", 1.0)
74+
model = SbmlModel.from_antimony(
75+
"fixedParameter1 = 1.0; observable_1 = 1.0"
76+
)
8077

8178
# observable file
8279
observable_df = pd.DataFrame(
@@ -88,9 +85,7 @@ def test_get_output_parameters():
8885
}
8986
).set_index(OBSERVABLE_ID)
9087

91-
output_parameters = petab.get_output_parameters(
92-
observable_df, SbmlModel(sbml_model=ss_model.model)
93-
)
88+
output_parameters = petab.get_output_parameters(observable_df, model)
9489

9590
assert output_parameters == ["offset", "scaling"]
9691

@@ -105,9 +100,7 @@ def test_get_output_parameters():
105100
}
106101
).set_index(OBSERVABLE_ID)
107102

108-
output_parameters = petab.get_output_parameters(
109-
observable_df, SbmlModel(sbml_model=ss_model.model)
110-
)
103+
output_parameters = petab.get_output_parameters(observable_df, model)
111104

112105
assert output_parameters == ["N", "beta"]
113106

0 commit comments

Comments
 (0)