Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 77 additions & 2 deletions petab/v1/models/sbml_model.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Functions for handling SBML models"""
from __future__ import annotations

import itertools
from collections.abc import Iterable
Expand Down Expand Up @@ -32,8 +33,25 @@ def __init__(
sbml_document: libsbml.SBMLDocument = None,
model_id: str = None,
):
"""Constructor.

:param sbml_model: SBML model. Optional if `sbml_document` is given.
:param sbml_reader: SBML reader. Optional.
:param sbml_document: SBML document. Optional if `sbml_model` is given.
:param model_id: Model ID. Defaults to the SBML model ID."""
super().__init__()

if sbml_model is None and sbml_document is None:
raise ValueError(
"Either sbml_model or sbml_document must be given."
)

if sbml_model is None:
sbml_model = sbml_document.getModel()

if sbml_document is None:
sbml_document = sbml_model.getSBMLDocument()

self.sbml_reader: libsbml.SBMLReader | None = sbml_reader
self.sbml_document: libsbml.SBMLDocument | None = sbml_document
self.sbml_model: libsbml.Model | None = sbml_model
Expand Down Expand Up @@ -70,7 +88,7 @@ def __setstate__(self, state):
self.__dict__.update(state)

@staticmethod
def from_file(filepath_or_buffer, model_id: str = None):
def from_file(filepath_or_buffer, model_id: str = None) -> SbmlModel:
sbml_reader, sbml_document, sbml_model = get_sbml_model(
filepath_or_buffer
)
Expand All @@ -82,7 +100,12 @@ def from_file(filepath_or_buffer, model_id: str = None):
)

@staticmethod
def from_string(sbml_string, model_id: str = None):
def from_string(sbml_string, model_id: str = None) -> SbmlModel:
"""Create SBML model from an SBML string.

:param sbml_string: SBML model as string.
:param model_id: Model ID. Defaults to the SBML model ID.
"""
sbml_reader, sbml_document, sbml_model = load_sbml_from_string(
sbml_string
)
Expand All @@ -97,6 +120,18 @@ def from_string(sbml_string, model_id: str = None):
model_id=model_id,
)

@staticmethod
def from_antimony(ant_model: str | Path) -> SbmlModel:
"""Create SBML model from an Antimony model.

Requires the `antimony` package (https://github.com/sys-bio/antimony).

:param ant_model: Antimony model as string or path to file.
Strings are interpreted as Antimony model strings.
"""
sbml_str = antimony2sbml(ant_model)
return SbmlModel.from_string(sbml_str)

@property
def model_id(self):
return self._model_id
Expand Down Expand Up @@ -238,3 +273,43 @@ def sympify_sbml(sbml_obj: libsbml.ASTNode | libsbml.SBase) -> sp.Expr:
)

return sp.sympify(formula_str, locals=_clash)


def antimony2sbml(ant_model: str | Path) -> str:
"""Convert Antimony model to SBML.

:param ant_model: Antimony model as string or path to file.
Strings are interpreted as Antimony model strings.

:returns:
The SBML model as string.
"""
import antimony as ant

# Unload everything / free memory
ant.clearPreviousLoads()
ant.freeAll()

try:
# potentially fails because of too long file name
is_file = ant_model and Path(ant_model).exists()
except OSError:
is_file = False

if is_file:
status = ant.loadAntimonyFile(str(ant_model))
else:
status = ant.loadAntimonyString(ant_model)
if status < 0:
raise RuntimeError(
f"Antimony model could not be loaded: {ant.getLastError()}"
)

if (main_module_name := ant.getMainModuleName()) is None:
raise AssertionError("There is no Antimony module.")

sbml_str = ant.getSBMLString(main_module_name)
if not sbml_str:
raise ValueError("Antimony model could not be converted to SBML.")

return sbml_str
8 changes: 6 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ maintainers = [

[project.optional-dependencies]
tests = [
"antimony>=2.14.0",
"pysb",
"pytest",
"pytest-cov",
"simplesbml",
"scipy",
"pysb",
]
quality = [
"pre-commit",
Expand All @@ -48,6 +48,9 @@ reports = [
# https://github.com/spatialaudio/nbsphinx/issues/641
"Jinja2==3.0.3",
]
antimony = [
"antimony>=2.14.0",
]
combine = [
"python-libcombine>=0.2.6",
]
Expand All @@ -61,6 +64,7 @@ doc = [
# https://github.com/spatialaudio/nbsphinx/issues/687#issuecomment-1339271312
"ipython>=7.21.0, !=8.7.0",
"pysb",
"antimony>=2.14.0"
]
vis = [
"matplotlib>=3.6.0",
Expand Down
10 changes: 4 additions & 6 deletions tests/v1/test_combine.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@

import pandas as pd

import petab
import petab.v1 as petab
from petab.C import *
from petab.v1.models.sbml_model import SbmlModel

# import fixtures
pytest_plugins = [
Expand All @@ -16,10 +17,7 @@
def test_combine_archive():
"""Test `create_combine_archive` and `Problem.from_combine`"""
# Create test files
import simplesbml

ss_model = simplesbml.SbmlModel()

model = SbmlModel.from_antimony("")
# Create tables with arbitrary content
measurement_df = pd.DataFrame(
data={
Expand Down Expand Up @@ -80,7 +78,7 @@ def test_combine_archive():
) as tempdir:
# Write test data
outdir = Path(tempdir)
petab.write_sbml(ss_model.document, outdir / sbml_file_name)
model.to_file(outdir / sbml_file_name)
petab.write_measurement_df(
measurement_df, outdir / measurement_file_name
)
Expand Down
4 changes: 2 additions & 2 deletions tests/v1/test_deprecated.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def test_problem_with_sbml_model():
"""Test that a problem can be correctly created from sbml model."""
# retrieve test data
(
ss_model,
model,
condition_df,
observable_df,
measurement_df,
Expand All @@ -23,7 +23,7 @@ def test_problem_with_sbml_model():

with pytest.deprecated_call():
petab_problem = petab.Problem( # noqa: F811
sbml_model=ss_model.model,
model=model,
condition_df=condition_df,
measurement_df=measurement_df,
parameter_df=parameter_df,
Expand Down
33 changes: 18 additions & 15 deletions tests/v1/test_lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@

def test_assert_measured_observables_present():
# create test model

measurement_df = pd.DataFrame(
data={
OBSERVABLE_ID: ["non-existing1"],
Expand Down Expand Up @@ -255,15 +254,15 @@ def test_assert_no_leading_trailing_whitespace():


def test_assert_model_parameters_in_condition_or_parameter_table():
import simplesbml

from petab.models.sbml_model import SbmlModel

ss_model = simplesbml.SbmlModel()
ss_model.addParameter("parameter1", 0.0)
ss_model.addParameter("noiseParameter1_", 0.0)
ss_model.addParameter("observableParameter1_", 0.0)
sbml_model = SbmlModel(sbml_model=ss_model.model)
ant_model = """
parameter1 = 0.0
noiseParameter1_ = 0.0
observableParameter1_ = 0.0
"""
sbml_model = SbmlModel.from_antimony(ant_model)
assert sbml_model.is_valid()

lint.assert_model_parameters_in_condition_or_parameter_table(
sbml_model, pd.DataFrame(columns=["parameter1"]), pd.DataFrame()
Expand All @@ -284,7 +283,10 @@ def test_assert_model_parameters_in_condition_or_parameter_table():
sbml_model, pd.DataFrame(), pd.DataFrame()
)

ss_model.addAssignmentRule("parameter1", "parameter2")
sbml_model = SbmlModel.from_antimony(
ant_model + "\nparameter2 = 0\nparameter1 := parameter2"
)
assert sbml_model.is_valid()
lint.assert_model_parameters_in_condition_or_parameter_table(
sbml_model, pd.DataFrame(), pd.DataFrame()
)
Expand Down Expand Up @@ -499,12 +501,11 @@ def test_assert_measurement_conditions_present_in_condition_table():

def test_check_condition_df():
"""Check that we correctly detect errors in condition table"""
import simplesbml

from petab.models.sbml_model import SbmlModel

ss_model = simplesbml.SbmlModel()
model = SbmlModel(sbml_model=ss_model.model)
model = SbmlModel.from_antimony("")

condition_df = pd.DataFrame(
data={
CONDITION_ID: ["condition1"],
Expand All @@ -527,7 +528,7 @@ def test_check_condition_df():
lint.check_condition_df(condition_df, model, observable_df)

# fix by adding parameter
ss_model.addParameter("p1", 1.0)
model = SbmlModel.from_antimony("p1 = 1")
lint.check_condition_df(condition_df, model)

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

# fix:
ss_model.addSpecies("[s1]", 1.0)
model = SbmlModel.from_antimony("p1 = 1; species s1 = 1")
lint.check_condition_df(condition_df, model)

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

# fix:
ss_model.addCompartment(comp_id="c2", vol=1.0)
model = SbmlModel.from_antimony(
"p1 = 1; species s1 = 1; compartment c2 = 1"
)
lint.check_condition_df(condition_df, model)


Expand Down
17 changes: 5 additions & 12 deletions tests/v1/test_observables.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,11 @@ def test_write_observable_df():

def test_get_output_parameters():
"""Test measurements.get_output_parameters."""
# sbml model
import simplesbml

from petab.models.sbml_model import SbmlModel

ss_model = simplesbml.SbmlModel()
ss_model.addParameter("fixedParameter1", 1.0)
ss_model.addParameter("observable_1", 1.0)
model = SbmlModel.from_antimony(
"fixedParameter1 = 1.0; observable_1 = 1.0"
)

# observable file
observable_df = pd.DataFrame(
Expand All @@ -88,9 +85,7 @@ def test_get_output_parameters():
}
).set_index(OBSERVABLE_ID)

output_parameters = petab.get_output_parameters(
observable_df, SbmlModel(sbml_model=ss_model.model)
)
output_parameters = petab.get_output_parameters(observable_df, model)

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

Expand All @@ -105,9 +100,7 @@ def test_get_output_parameters():
}
).set_index(OBSERVABLE_ID)

output_parameters = petab.get_output_parameters(
observable_df, SbmlModel(sbml_model=ss_model.model)
)
output_parameters = petab.get_output_parameters(observable_df, model)

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

Expand Down
Loading
Loading