diff --git a/petab/schemas/petab_schema.v2.0.0.yaml b/petab/schemas/petab_schema.v2.0.0.yaml index 7f1b7443..1a285070 100644 --- a/petab/schemas/petab_schema.v2.0.0.yaml +++ b/petab/schemas/petab_schema.v2.0.0.yaml @@ -23,6 +23,14 @@ properties: - type: integer description: Version of the PEtab format + id: + type: string + description: | + Identifier of the PEtab problem. + + This is optional and has no effect on the PEtab problem itself. + pattern: "^[a-zA-Z_]\\w*$" + parameter_files: type: array description: | diff --git a/petab/v2/core.py b/petab/v2/core.py index 0f55ea8e..b331c713 100644 --- a/petab/v2/core.py +++ b/petab/v2/core.py @@ -1630,6 +1630,18 @@ def mappings(self) -> list[Mapping]: chain.from_iterable(mt.mappings for mt in self.mapping_tables) ) + @property + def id(self) -> str | None: + """The ID of the PEtab problem if set, ``None`` otherwise.""" + return self.config.id + + @id.setter + def id(self, value: str): + """Set the ID of the PEtab problem.""" + if self.config is None: + self.config = ProblemConfig(format_version="2.0.0") + self.config.id = value + def get_optimization_parameters(self) -> list[str]: """ Get the list of optimization parameter IDs from parameter table. @@ -2327,6 +2339,10 @@ class ProblemConfig(BaseModel): ) #: The PEtab format version. format_version: str = "2.0.0" + + #: The problem ID. + id: str | None = None + #: The path to the parameter file, relative to ``base_path``. # TODO https://github.com/PEtab-dev/PEtab/pull/641: # rename to parameter_files in yaml for consistency with other files? @@ -2388,6 +2404,9 @@ def to_yaml(self, filename: str | Path): data["model_files"][model_id][C.MODEL_LOCATION] = str( data["model_files"][model_id]["location"] ) + if data["id"] is None: + # The schema requires a valid id or no id field at all. + del data["id"] write_yaml(data, filename) diff --git a/tests/v2/test_core.py b/tests/v2/test_core.py index 0a9e2429..e38f31f1 100644 --- a/tests/v2/test_core.py +++ b/tests/v2/test_core.py @@ -707,3 +707,35 @@ def test_petablint_v2(tmpdir): result = subprocess.run(["petablint", str(Path(tmpdir, "problem.yaml"))]) # noqa: S603,S607 assert result.returncode == 0 + + +def test_problem_id(tmpdir): + """Test that the problem ID works as expected.""" + from jsonschema import ValidationError + + def make_yaml(id_line: str) -> str: + return f""" + format_version: 2.0.0 + {id_line} + model_files: {{}} + parameter_files: [] + observable_files: [] + condition_files: [] + measurement_files: [] + """ + + filepath = Path(tmpdir, "problem.yaml") + with open(filepath, "w") as f: + f.write(make_yaml("id: my_problem_id")) + problem = Problem.from_yaml(filepath) + assert problem.id == "my_problem_id" + + with open(filepath, "w") as f: + f.write(make_yaml("id: ")) + with pytest.raises(ValidationError): + Problem.from_yaml(filepath) + + with open(filepath, "w") as f: + f.write(make_yaml("")) + problem = Problem.from_yaml(filepath) + assert problem.id is None