Skip to content

Commit 107fb74

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

File tree

2 files changed

+70
-2
lines changed

2 files changed

+70
-2
lines changed

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: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,35 @@ 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"))
46+
problem.models[0]._model_id = "model1"
47+
problem.add_observable("obs1", "A")
48+
problem.add_measurement("obs1", "e1", 0, 1)
49+
50+
# Add a measurement with an invalid model ID
51+
problem.measurement_df.at[0, "model_id"] = "invalid_model_id"
52+
53+
check = CheckMeasurementModelId()
54+
55+
# Single model -> model ID is optional
56+
assert (error := check.run(problem)) is None, error
57+
58+
# Two models -> model ID must be set
59+
problem.models.append(SbmlModel.from_antimony("p2 = 2"))
60+
problem.models[1]._model_id = "model2"
61+
assert (error := check.run(problem)) is not None
62+
assert "multiple models" in error.message
63+
64+
# Set model ID to a non-existing model ID
65+
problem.measurements[0].model_id = "invalid_model_id"
66+
assert (error := check.run(problem)) is not None
67+
assert "does not match" in error.message
68+
69+
# Use a valid model ID
70+
problem.measurements[0].model_id = "model1"
71+
assert (error := check.run(problem)) is None, error

0 commit comments

Comments
 (0)