Skip to content

Commit a00d04f

Browse files
committed
Update v2 condition table targetValue validation
After PEtab-dev/PEtab#645, any condition that is used as a first condition cannot refer to any symbols other then the parameters listed in the parameter table, or `time'. Closes #424.
1 parent 876c781 commit a00d04f

File tree

3 files changed

+87
-5
lines changed

3 files changed

+87
-5
lines changed

petab/v2/C.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,9 @@
285285
#: separator for multiple parameter values (bounds, observableParameters, ...)
286286
PARAMETER_SEPARATOR = ";"
287287

288+
#: The time symbol for use in any PEtab-specific mathematical expressions
289+
TIME_SYMBOL = "time"
290+
288291

289292
__all__ = [
290293
x

petab/v2/lint.py

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"CheckObservablesDoNotShadowModelEntities",
3939
"CheckUnusedConditions",
4040
"CheckPriorDistribution",
41+
"CheckInitialChangeSymbols",
4142
"lint_problem",
4243
"default_validation_tasks",
4344
]
@@ -709,6 +710,62 @@ def run(self, problem: Problem) -> ValidationIssue | None:
709710
return None
710711

711712

713+
class CheckInitialChangeSymbols(ValidationTask):
714+
"""
715+
Check that changes of any first period of any experiment only refers to
716+
allowed symbols.
717+
718+
The only allowed symbols are those that are present in the parameter table.
719+
"""
720+
721+
def run(self, problem: Problem) -> ValidationIssue | None:
722+
if not problem.experiments:
723+
return None
724+
725+
if not problem.conditions:
726+
return None
727+
728+
allowed_symbols = {p.id for p in problem.parameters}
729+
allowed_symbols.add(TIME_SYMBOL)
730+
# IDs of conditions that have already been checked
731+
valid_conditions = set()
732+
id_to_condition = {c.id: c for c in problem.conditions}
733+
734+
messages = []
735+
for experiment in problem.experiments:
736+
if not experiment.periods:
737+
continue
738+
739+
first_period = experiment.sorted_periods[0]
740+
for condition_id in first_period.condition_ids:
741+
if condition_id in valid_conditions:
742+
continue
743+
744+
# we assume that all referenced condition IDs are valid
745+
condition = id_to_condition[condition_id]
746+
747+
used_symbols = {
748+
str(sym)
749+
for change in condition.changes
750+
for sym in change.target_value.free_symbols
751+
}
752+
invalid_symbols = used_symbols - allowed_symbols
753+
if invalid_symbols:
754+
messages.append(
755+
f"Condition {condition.id} is applied at the start of "
756+
f"experiment {experiment.id}, and thus, its "
757+
f"target value expressions must only contain "
758+
f"symbols from the parameter table, or `time`. "
759+
"However, it contains additional symbols: "
760+
f"{invalid_symbols}. "
761+
)
762+
763+
if messages:
764+
return ValidationError("\n".join(messages))
765+
766+
return None
767+
768+
712769
class CheckPriorDistribution(ValidationTask):
713770
"""A task to validate the prior distribution of a PEtab problem."""
714771

@@ -1022,10 +1079,7 @@ def get_placeholders(
10221079
CheckValidParameterInConditionOrParameterTable(),
10231080
CheckUnusedExperiments(),
10241081
CheckUnusedConditions(),
1025-
# TODO: atomize checks, update to long condition table, re-enable
1026-
# TODO validate mapping table
1027-
CheckValidParameterInConditionOrParameterTable(),
1028-
CheckAllParametersPresentInParameterTable(),
1029-
CheckValidConditionTargets(),
10301082
CheckPriorDistribution(),
1083+
CheckInitialChangeSymbols(),
1084+
# TODO validate mapping table
10311085
]

tests/v2/test_lint.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,28 @@ 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_validate_initial_change_symbols():
43+
"""Test validation of symbols in target value expressions for changes
44+
applied at the start of an experiment."""
45+
problem = Problem()
46+
problem.model = SbmlModel.from_antimony("p1 = 1; p2 = 2")
47+
problem.add_experiment("e1", 0, "c1", 1, "c2")
48+
problem.add_condition("c1", p1="p2 + time")
49+
problem.add_condition("c2", p1="p2", p2="p1")
50+
problem.add_parameter("p1", nominal_value=1, estimate=False)
51+
problem.add_parameter("p2", nominal_value=2, estimate=False)
52+
53+
check = CheckInitialChangeSymbols()
54+
assert check.run(problem) is None
55+
56+
# removing `p1` from the parameter table is okay, as `c2` is never
57+
# used at the start of an experiment
58+
problem.parameter_tables[0].parameters.remove(problem["p1"])
59+
assert check.run(problem) is None
60+
61+
# removing `p2` is not okay, as it is used at the start of an experiment
62+
problem.parameter_tables[0].parameters.remove(problem["p2"])
63+
assert (error := check.run(problem)) is not None
64+
assert "contains additional symbols: {'p2'}" in error.message

0 commit comments

Comments
 (0)