From e7af5306863e8def22547ae807c65ee8f7ac1d6a Mon Sep 17 00:00:00 2001 From: Daniel Weindl Date: Wed, 24 Sep 2025 16:30:50 +0200 Subject: [PATCH 1/2] Add `id` field to `v2.ProblemConfig` * Update schema * v2.Problem.id * v2.ProblemConfig.id Related to PEtab-dev/PEtab#646. Closes #441. --- petab/schemas/petab_schema.v2.0.0.yaml | 8 ++++++++ petab/v2/core.py | 16 ++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/petab/schemas/petab_schema.v2.0.0.yaml b/petab/schemas/petab_schema.v2.0.0.yaml index 7f1b7443..6e7f9e0d 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..ef5c724b 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: + """The ID of the PEtab problem if set, ``None`` otherwise.""" + return self.config.id if self.config else None + + @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 = "" + #: 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? From 511c6d424ea25869c53d5b060367a4948d5ddd93 Mon Sep 17 00:00:00 2001 From: Daniel Weindl Date: Wed, 1 Oct 2025 16:49:16 +0200 Subject: [PATCH 2/2] .. --- petab/schemas/petab_schema.v2.0.0.yaml | 2 +- petab/v2/core.py | 9 +++++--- tests/v2/test_core.py | 32 ++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/petab/schemas/petab_schema.v2.0.0.yaml b/petab/schemas/petab_schema.v2.0.0.yaml index 6e7f9e0d..1a285070 100644 --- a/petab/schemas/petab_schema.v2.0.0.yaml +++ b/petab/schemas/petab_schema.v2.0.0.yaml @@ -29,7 +29,7 @@ properties: Identifier of the PEtab problem. This is optional and has no effect on the PEtab problem itself. - pattern: "^[a-zA-Z_]\\w*|^$" + pattern: "^[a-zA-Z_]\\w*$" parameter_files: type: array diff --git a/petab/v2/core.py b/petab/v2/core.py index ef5c724b..b331c713 100644 --- a/petab/v2/core.py +++ b/petab/v2/core.py @@ -1631,9 +1631,9 @@ def mappings(self) -> list[Mapping]: ) @property - def id(self) -> str: + def id(self) -> str | None: """The ID of the PEtab problem if set, ``None`` otherwise.""" - return self.config.id if self.config else None + return self.config.id @id.setter def id(self, value: str): @@ -2341,7 +2341,7 @@ class ProblemConfig(BaseModel): format_version: str = "2.0.0" #: The problem ID. - id: str = "" + id: str | None = None #: The path to the parameter file, relative to ``base_path``. # TODO https://github.com/PEtab-dev/PEtab/pull/641: @@ -2404,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