From bdffef4a05fd55886ad6eee4de7b171b189fa6a8 Mon Sep 17 00:00:00 2001 From: Daniel Weindl Date: Thu, 28 Aug 2025 14:55:14 +0200 Subject: [PATCH] v2 linter: check measurement.experimentId Check for undefined experiments referenced in the measurement table. Closes #434. --- petab/v2/lint.py | 24 ++++++++++++++++++++++++ tests/v2/test_lint.py | 18 ++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/petab/v2/lint.py b/petab/v2/lint.py index 0780b340..6bfcfe50 100644 --- a/petab/v2/lint.py +++ b/petab/v2/lint.py @@ -42,6 +42,7 @@ "CheckObservablesDoNotShadowModelEntities", "CheckUnusedConditions", "CheckPriorDistribution", + "CheckUndefinedExperiments", "lint_problem", "default_validation_tasks", ] @@ -691,6 +692,28 @@ def run(self, problem: Problem) -> ValidationIssue | None: return None +class CheckUndefinedExperiments(ValidationTask): + """A task to check for experiments that are used in the measurement + table but not defined in the experiment table.""" + + def run(self, problem: Problem) -> ValidationIssue | None: + used_experiments = { + m.experiment_id + for m in problem.measurements + if m.experiment_id is not None + } + available_experiments = {e.id for e in problem.experiments} + + if undefined_experiments := used_experiments - available_experiments: + return ValidationWarning( + f"Experiments {undefined_experiments} are used in the " + "measurements table but are not defined in the experiments " + "table." + ) + + return None + + class CheckUnusedConditions(ValidationTask): """A task to check for conditions that are not used in the experiment table.""" @@ -1053,6 +1076,7 @@ def get_placeholders( CheckValidConditionTargets(), CheckExperimentTable(), CheckExperimentConditionsExist(), + CheckUndefinedExperiments(), CheckObservablesDoNotShadowModelEntities(), CheckAllParametersPresentInParameterTable(), CheckValidParameterInConditionOrParameterTable(), diff --git a/tests/v2/test_lint.py b/tests/v2/test_lint.py index 12973d86..1013670f 100644 --- a/tests/v2/test_lint.py +++ b/tests/v2/test_lint.py @@ -64,3 +64,21 @@ def test_invalid_model_id_in_measurements(): # Use a valid model ID problem.measurements[0].model_id = "model1" assert (error := check.run(problem)) is None, error + + +def test_undefined_experiment_id_in_measurements(): + """Test that measurements with an undefined experiment ID are caught.""" + problem = Problem() + problem.add_experiment("e1", 0, "c1") + problem.add_observable("obs1", "A") + problem.add_measurement("obs1", experiment_id="e1", time=0, measurement=1) + + check = CheckUndefinedExperiments() + + # Valid experiment ID + assert (error := check.run(problem)) is None, error + + # Invalid experiment ID + problem.measurements[0].experiment_id = "invalid_experiment_id" + assert (error := check.run(problem)) is not None + assert "not defined" in error.message