Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions petab/v1/models/sbml_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,16 +130,18 @@ def from_string(sbml_string, model_id: str = None) -> SbmlModel:
)

@staticmethod
def from_antimony(ant_model: str | Path) -> SbmlModel:
def from_antimony(ant_model: str | Path, **kwargs) -> SbmlModel:
"""Create SBML model from an Antimony model.

Requires the `antimony` package (https://github.com/sys-bio/antimony).

:param ant_model: Antimony model as string or path to file.
Strings are interpreted as Antimony model strings.
:param kwargs: Additional keyword arguments passed to
:meth:`SbmlModel.from_string`.
"""
sbml_str = antimony2sbml(ant_model)
return SbmlModel.from_string(sbml_str)
return SbmlModel.from_string(sbml_str, **kwargs)

def to_antimony(self) -> str:
"""Convert the SBML model to an Antimony string."""
Expand Down
40 changes: 38 additions & 2 deletions petab/v2/lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@
"ValidationTask",
"CheckModel",
"CheckProblemConfig",
"CheckMeasuredObservablesDefined",
"CheckOverridesMatchPlaceholders",
"CheckMeasuredExperimentsDefined",
"CheckMeasurementModelId",
"CheckPosLogMeasurements",
"CheckValidConditionTargets",
"CheckUniquePrimaryKeys",
Expand Down Expand Up @@ -769,8 +773,39 @@ def run(self, problem: Problem) -> ValidationIssue | None:
return None


# TODO: check that Measurements model IDs match the available ones
# https://github.com/PEtab-dev/libpetab-python/issues/392
class CheckMeasurementModelId(ValidationTask):
"""Validate model IDs of measurements."""

def run(self, problem: Problem) -> ValidationIssue | None:
messages = []
available_models = {m.model_id for m in problem.models}

for measurement in problem.measurements:
if not measurement.model_id:
if len(available_models) < 2:
# If there is only one model, it is not required to specify
# the model ID in the measurement table.
continue

messages.append(
f"Measurement `{measurement}' does not have a model ID, "
"but there are multiple models available. "
"Please specify the model ID in the measurement table."
)
continue

if measurement.model_id not in available_models:
messages.append(
f"Measurement `{measurement}' has model ID "
f"`{measurement.model_id}' which does not match "
"any of the available models: "
f"{available_models}."
)

if messages:
return ValidationError("\n".join(messages))

return None


def get_valid_parameters_for_parameter_table(
Expand Down Expand Up @@ -1011,6 +1046,7 @@ def get_placeholders(
CheckProblemConfig(),
CheckModel(),
CheckUniquePrimaryKeys(),
CheckMeasurementModelId(),
CheckMeasuredObservablesDefined(),
CheckPosLogMeasurements(),
CheckOverridesMatchPlaceholders(),
Expand Down
27 changes: 27 additions & 0 deletions tests/v2/test_lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,30 @@ def test_check_incompatible_targets():
problem["e1"].periods[0].condition_ids.append("c2")
assert (error := check.run(problem)) is not None
assert "overlapping targets {'p1'}" in error.message


def test_invalid_model_id_in_measurements():
"""Test that measurements with an invalid model ID are caught."""
problem = Problem()
problem.models.append(SbmlModel.from_antimony("p1 = 1", model_id="model1"))
problem.add_observable("obs1", "A")
problem.add_measurement("obs1", "e1", 0, 1)

check = CheckMeasurementModelId()

# Single model -> model ID is optional
assert (error := check.run(problem)) is None, error

# Two models -> model ID must be set
problem.models.append(SbmlModel.from_antimony("p2 = 2", model_id="model2"))
assert (error := check.run(problem)) is not None
assert "multiple models" in error.message

# Set model ID to a non-existing model ID
problem.measurements[0].model_id = "invalid_model_id"
assert (error := check.run(problem)) is not None
assert "does not match" in error.message

# Use a valid model ID
problem.measurements[0].model_id = "model1"
assert (error := check.run(problem)) is None, error
Loading