Skip to content

Commit 48a4334

Browse files
authored
Merge branch 'main' into validate_initial_changes
2 parents a00d04f + 8795dbf commit 48a4334

File tree

9 files changed

+83
-9
lines changed

9 files changed

+83
-9
lines changed

doc/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"numpy": ("https://numpy.org/devdocs/", None),
5353
"sympy": ("https://docs.sympy.org/latest/", None),
5454
"python": ("https://docs.python.org/3", None),
55+
"pydantic": ("https://docs.pydantic.dev/latest/", None),
5556
}
5657

5758
# Add any paths that contain templates here, relative to this directory.

petab/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def __getattr__(name):
3636
return getattr(importlib.import_module("petab.v1"), name)
3737

3838

39-
def v1getattr(name, module):
39+
def _v1getattr(name, module):
4040
if name not in ("__path__", "__all__"):
4141
warn(
4242
f"Accessing `petab.{name}` is deprecated and will be removed in "
@@ -67,7 +67,7 @@ def v1getattr(name, module):
6767
real_module = importlib.import_module(
6868
f"petab.v1.{'.'.join(v1_object_parts)}"
6969
)
70-
real_module.__getattr__ = partial(v1getattr, module=real_module)
70+
real_module.__getattr__ = partial(_v1getattr, module=real_module)
7171
sys.modules[module_name] = real_module
7272
except ModuleNotFoundError:
7373
pass

petab/v1/models/sbml_model.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,16 +130,18 @@ def from_string(sbml_string, model_id: str = None) -> SbmlModel:
130130
)
131131

132132
@staticmethod
133-
def from_antimony(ant_model: str | Path) -> SbmlModel:
133+
def from_antimony(ant_model: str | Path, **kwargs) -> SbmlModel:
134134
"""Create SBML model from an Antimony model.
135135
136136
Requires the `antimony` package (https://github.com/sys-bio/antimony).
137137
138138
:param ant_model: Antimony model as string or path to file.
139139
Strings are interpreted as Antimony model strings.
140+
:param kwargs: Additional keyword arguments passed to
141+
:meth:`SbmlModel.from_string`.
140142
"""
141143
sbml_str = antimony2sbml(ant_model)
142-
return SbmlModel.from_string(sbml_str)
144+
return SbmlModel.from_string(sbml_str, **kwargs)
143145

144146
def to_antimony(self) -> str:
145147
"""Convert the SBML model to an Antimony string."""

petab/v1/sbml.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ def write_sbml(sbml_doc: libsbml.SBMLDocument, filename: Path | str) -> None:
169169
sbml_doc: SBML document containing the SBML model
170170
filename: Destination file name
171171
"""
172+
Path(filename).parent.mkdir(parents=True, exist_ok=True)
172173
sbml_writer = libsbml.SBMLWriter()
173174
ret = sbml_writer.writeSBMLToFile(sbml_doc, str(filename))
174175
if not ret:

petab/v2/core.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2035,9 +2035,10 @@ def add_parameter(
20352035
def add_measurement(
20362036
self,
20372037
obs_id: str,
2038-
experiment_id: str,
2038+
*,
20392039
time: float,
20402040
measurement: float,
2041+
experiment_id: str | None = None,
20412042
observable_parameters: Sequence[str | float] | str | float = None,
20422043
noise_parameters: Sequence[str | float] | str | float = None,
20432044
):

petab/v2/lint.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@
2727
"ValidationTask",
2828
"CheckModel",
2929
"CheckProblemConfig",
30+
"CheckMeasuredObservablesDefined",
31+
"CheckOverridesMatchPlaceholders",
32+
"CheckMeasuredExperimentsDefined",
33+
"CheckMeasurementModelId",
3034
"CheckPosLogMeasurements",
3135
"CheckValidConditionTargets",
3236
"CheckUniquePrimaryKeys",
@@ -826,8 +830,39 @@ def run(self, problem: Problem) -> ValidationIssue | None:
826830
return None
827831

828832

829-
# TODO: check that Measurements model IDs match the available ones
830-
# https://github.com/PEtab-dev/libpetab-python/issues/392
833+
class CheckMeasurementModelId(ValidationTask):
834+
"""Validate model IDs of measurements."""
835+
836+
def run(self, problem: Problem) -> ValidationIssue | None:
837+
messages = []
838+
available_models = {m.model_id for m in problem.models}
839+
840+
for measurement in problem.measurements:
841+
if not measurement.model_id:
842+
if len(available_models) < 2:
843+
# If there is only one model, it is not required to specify
844+
# the model ID in the measurement table.
845+
continue
846+
847+
messages.append(
848+
f"Measurement `{measurement}' does not have a model ID, "
849+
"but there are multiple models available. "
850+
"Please specify the model ID in the measurement table."
851+
)
852+
continue
853+
854+
if measurement.model_id not in available_models:
855+
messages.append(
856+
f"Measurement `{measurement}' has model ID "
857+
f"`{measurement.model_id}' which does not match "
858+
"any of the available models: "
859+
f"{available_models}."
860+
)
861+
862+
if messages:
863+
return ValidationError("\n".join(messages))
864+
865+
return None
831866

832867

833868
def get_valid_parameters_for_parameter_table(
@@ -1068,6 +1103,7 @@ def get_placeholders(
10681103
CheckProblemConfig(),
10691104
CheckModel(),
10701105
CheckUniquePrimaryKeys(),
1106+
CheckMeasurementModelId(),
10711107
CheckMeasuredObservablesDefined(),
10721108
CheckPosLogMeasurements(),
10731109
CheckOverridesMatchPlaceholders(),

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ doc = [
6565
# https://github.com/spatialaudio/nbsphinx/issues/687#issuecomment-1339271312
6666
"ipython>=7.21.0, !=8.7.0",
6767
"pysb",
68-
"antimony>=2.14.0"
68+
"antimony>=2.14.0",
69+
"sbmlmath>=0.4.0",
6970
]
7071
vis = [
7172
"matplotlib>=3.6.0",

tests/v2/test_core.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,12 @@ def test_problem_from_yaml_multiple_files():
389389
problem.experiment_df, Path(tmpdir, f"experiments{i}.tsv")
390390
)
391391

392-
problem.add_measurement(f"observable{i}", f"experiment{i}", 1, 1)
392+
problem.add_measurement(
393+
f"observable{i}",
394+
experiment_id=f"experiment{i}",
395+
time=1,
396+
measurement=1,
397+
)
393398
petab.write_measurement_df(
394399
problem.measurement_df, Path(tmpdir, f"measurements{i}.tsv")
395400
)

tests/v2/test_lint.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,30 @@ def test_validate_initial_change_symbols():
6262
problem.parameter_tables[0].parameters.remove(problem["p2"])
6363
assert (error := check.run(problem)) is not None
6464
assert "contains additional symbols: {'p2'}" in error.message
65+
66+
67+
def test_invalid_model_id_in_measurements():
68+
"""Test that measurements with an invalid model ID are caught."""
69+
problem = Problem()
70+
problem.models.append(SbmlModel.from_antimony("p1 = 1", model_id="model1"))
71+
problem.add_observable("obs1", "A")
72+
problem.add_measurement("obs1", experiment_id="e1", time=0, measurement=1)
73+
74+
check = CheckMeasurementModelId()
75+
76+
# Single model -> model ID is optional
77+
assert (error := check.run(problem)) is None, error
78+
79+
# Two models -> model ID must be set
80+
problem.models.append(SbmlModel.from_antimony("p2 = 2", model_id="model2"))
81+
assert (error := check.run(problem)) is not None
82+
assert "multiple models" in error.message
83+
84+
# Set model ID to a non-existing model ID
85+
problem.measurements[0].model_id = "invalid_model_id"
86+
assert (error := check.run(problem)) is not None
87+
assert "does not match" in error.message
88+
89+
# Use a valid model ID
90+
problem.measurements[0].model_id = "model1"
91+
assert (error := check.run(problem)) is None, error

0 commit comments

Comments
 (0)