Skip to content

Commit d486665

Browse files
committed
Validate modelId in measurements
Check that we have a valid model ID. Related to #392.
1 parent 876c781 commit d486665

File tree

3 files changed

+72
-4
lines changed

3 files changed

+72
-4
lines changed

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/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",
@@ -769,8 +773,39 @@ def run(self, problem: Problem) -> ValidationIssue | None:
769773
return None
770774

771775

772-
# TODO: check that Measurements model IDs match the available ones
773-
# https://github.com/PEtab-dev/libpetab-python/issues/392
776+
class CheckMeasurementModelId(ValidationTask):
777+
"""Validate model IDs of measurements."""
778+
779+
def run(self, problem: Problem) -> ValidationIssue | None:
780+
messages = []
781+
available_models = {m.model_id for m in problem.models}
782+
783+
for measurement in problem.measurements:
784+
if not measurement.model_id:
785+
if len(available_models) < 2:
786+
# If there is only one model, it is not required to specify
787+
# the model ID in the measurement table.
788+
continue
789+
790+
messages.append(
791+
f"Measurement `{measurement}' does not have a model ID, "
792+
"but there are multiple models available. "
793+
"Please specify the model ID in the measurement table."
794+
)
795+
continue
796+
797+
if measurement.model_id not in available_models:
798+
messages.append(
799+
f"Measurement `{measurement}' has model ID "
800+
f"`{measurement.model_id}' which does not match "
801+
"any of the available models: "
802+
f"{available_models}."
803+
)
804+
805+
if messages:
806+
return ValidationError("\n".join(messages))
807+
808+
return None
774809

775810

776811
def get_valid_parameters_for_parameter_table(
@@ -1011,6 +1046,7 @@ def get_placeholders(
10111046
CheckProblemConfig(),
10121047
CheckModel(),
10131048
CheckUniquePrimaryKeys(),
1049+
CheckMeasurementModelId(),
10141050
CheckMeasuredObservablesDefined(),
10151051
CheckPosLogMeasurements(),
10161052
CheckOverridesMatchPlaceholders(),

tests/v2/test_lint.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,33 @@ def test_check_incompatible_targets():
3737
problem["e1"].periods[0].condition_ids.append("c2")
3838
assert (error := check.run(problem)) is not None
3939
assert "overlapping targets {'p1'}" in error.message
40+
41+
42+
def test_invalid_model_id_in_measurements():
43+
"""Test that measurements with an invalid model ID are caught."""
44+
problem = Problem()
45+
problem.models.append(SbmlModel.from_antimony("p1 = 1", model_id="model1"))
46+
problem.add_observable("obs1", "A")
47+
problem.add_measurement("obs1", "e1", 0, 1)
48+
49+
# Add a measurement with an invalid model ID
50+
problem.measurement_df.at[0, "model_id"] = "invalid_model_id"
51+
52+
check = CheckMeasurementModelId()
53+
54+
# Single model -> model ID is optional
55+
assert (error := check.run(problem)) is None, error
56+
57+
# Two models -> model ID must be set
58+
problem.models.append(SbmlModel.from_antimony("p2 = 2", model_id="model2"))
59+
assert (error := check.run(problem)) is not None
60+
assert "multiple models" in error.message
61+
62+
# Set model ID to a non-existing model ID
63+
problem.measurements[0].model_id = "invalid_model_id"
64+
assert (error := check.run(problem)) is not None
65+
assert "does not match" in error.message
66+
67+
# Use a valid model ID
68+
problem.measurements[0].model_id = "model1"
69+
assert (error := check.run(problem)) is None, error

0 commit comments

Comments
 (0)