From 1d268844c7d8cd42c0a88f4e00c812f02b9c6c16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Fr=C3=B6hlich?= Date: Thu, 8 Oct 2026 13:31:37 +0100 Subject: [PATCH 1/2] petab1to2: allow skipping validation Add a keyword-only `validate` argument to `petab1to2` and `petab_files_1to2`. With `validate=False`, neither the PEtab v1 input nor the generated PEtab v2 problem is linted, so problems that fail linting for reasons unrelated to the conversion (e.g., a visualization table referencing a non-existent datasetId, or an intentionally incomplete model) can still be converted. The default behaviour is unchanged. The YAML file is still validated against the v1 schema. Closes #528 Co-Authored-By: Claude Opus 5.5 --- petab/v2/petab1to2.py | 48 +++++++++++++++++++------ tests/v2/test_conversion.py | 70 +++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 11 deletions(-) diff --git a/petab/v2/petab1to2.py b/petab/v2/petab1to2.py index 14ed8939..23ca4bd8 100644 --- a/petab/v2/petab1to2.py +++ b/petab/v2/petab1to2.py @@ -16,7 +16,8 @@ from .. import v1, v2 from ..v1.math import sympify_petab -from ..v1.yaml import get_path_prefix, load_yaml, validate +from ..v1.yaml import get_path_prefix, load_yaml +from ..v1.yaml import validate as validate_yaml from ..versions import get_major_version from .models import MODEL_TYPE_SBML @@ -24,7 +25,10 @@ def petab1to2( - yaml_config: Path | str, output_dir: Path | str | None = None + yaml_config: Path | str, + output_dir: Path | str | None = None, + *, + validate: bool = True, ) -> v2.Problem | None: """Convert from PEtab 1.0 to PEtab 2.0 format. @@ -46,20 +50,36 @@ def petab1to2( :param output_dir: The output directory to save the converted PEtab problem, or ``None``, to return a :class:`petab.v2.Problem` instance. + :param validate: + Whether to lint the PEtab v1 problem before and the generated + PEtab v2 problem after the conversion. + If ``True`` (default), the conversion fails if either does not pass + linting. + If ``False``, both lint steps are skipped. This allows converting + problems that fail linting for reasons unrelated to the conversion, + e.g., a visualization table referencing non-existent datasets or an + intentionally incomplete model. In this case, the result should be + checked separately, e.g., using ``petablint``. + The YAML file is validated against the PEtab v1 schema in either case. :raises ValueError: - If the input is invalid or does not pass linting or if the generated - files do not pass linting. + If the input is invalid or, if ``validate`` is ``True``, if the input + does not pass linting or if the generated files do not pass linting. """ if output_dir is not None: - return petab_files_1to2(yaml_config, output_dir) + return petab_files_1to2(yaml_config, output_dir, validate=validate) with TemporaryDirectory() as tmp_dir: - petab_files_1to2(yaml_config, tmp_dir) + petab_files_1to2(yaml_config, tmp_dir, validate=validate) return v2.Problem.from_yaml(Path(tmp_dir, Path(yaml_config).name)) -def petab_files_1to2(yaml_config: Path | str | dict, output_dir: Path | str): +def petab_files_1to2( + yaml_config: Path | str | dict, + output_dir: Path | str, + *, + validate: bool = True, +): """Convert PEtab files from PEtab 1.0 to PEtab 2.0. @@ -67,10 +87,13 @@ def petab_files_1to2(yaml_config: Path | str | dict, output_dir: Path | str): The PEtab problem as dictionary or YAML file name. :param output_dir: The output directory to save the converted PEtab problem. + :param validate: + Whether to lint the PEtab v1 problem before and the generated + PEtab v2 problem after the conversion. See :func:`petab1to2`. :raises ValueError: - If the input is invalid or does not pass linting or if the generated - files do not pass linting. + If the input is invalid or, if ``validate`` is ``True``, if the input + does not pass linting or if the generated files do not pass linting. """ if isinstance(yaml_config, Path | str): yaml_file = str(yaml_config) @@ -85,7 +108,7 @@ def petab_files_1to2(yaml_config: Path | str | dict, output_dir: Path | str): get_dest_path = lambda filename: f"{output_dir}/{filename}" # noqa: E731 # Validate the original PEtab problem - validate(yaml_config, path_prefix=path_prefix) + validate_yaml(yaml_config, path_prefix=path_prefix) if get_major_version(yaml_config) != 1: raise ValueError("PEtab problem is not version 1.") petab_problem = v1.Problem.from_yaml(yaml_file or yaml_config) @@ -94,7 +117,7 @@ def petab_files_1to2(yaml_config: Path | str | dict, output_dir: Path | str): petab_problem.condition_df = petab_problem.condition_df.drop( columns=[v1.C.CONDITION_NAME], errors="ignore" ) - if v1.lint_problem(petab_problem): + if validate and v1.lint_problem(petab_problem): raise ValueError("Provided PEtab problem does not pass linting.") output_dir = Path(output_dir) @@ -253,6 +276,9 @@ def create_experiment_id(sim_cond_id: str, preeq_cond_id: str) -> str: new_yaml_file = output_dir / Path(yaml_file).name new_yaml_config.to_yaml(new_yaml_file) + if not validate: + return + # validate updated Problem validation_issues = v2.lint_problem(new_yaml_file) diff --git a/tests/v2/test_conversion.py b/tests/v2/test_conversion.py index 3380c363..5ff7a46a 100644 --- a/tests/v2/test_conversion.py +++ b/tests/v2/test_conversion.py @@ -1,4 +1,5 @@ import logging +from pathlib import Path import pandas as pd import pytest @@ -41,6 +42,75 @@ def test_petab1to2_remote(): assert len(problem.measurements) +def test_petab1to2_validate(tmp_path): + """Test that `validate=False` allows converting problems that fail + linting for reasons unrelated to the conversion.""" + from petab.v1.models.sbml_model import SbmlModel + + v1_problem = v1.Problem( + model=SbmlModel.from_antimony( + "A -> B; k1 * A; A = 1; B = 0; k1 = 0.5" + ), + condition_df=v1.get_condition_df( + pd.DataFrame({v1.C.CONDITION_ID: ["c0"], "A": [1.0]}) + ), + observable_df=v1.get_observable_df( + pd.DataFrame( + { + v1.C.OBSERVABLE_ID: ["obs_B"], + v1.C.OBSERVABLE_FORMULA: ["B"], + v1.C.NOISE_FORMULA: [0.1], + } + ) + ), + measurement_df=v1.get_measurement_df( + pd.DataFrame( + { + v1.C.OBSERVABLE_ID: ["obs_B", "obs_B"], + v1.C.SIMULATION_CONDITION_ID: ["c0", "c0"], + v1.C.TIME: [1.0, 2.0], + v1.C.MEASUREMENT: [0.3, 0.5], + v1.C.DATASET_ID: ["d1", "d1"], + } + ) + ), + parameter_df=v1.get_parameter_df( + pd.DataFrame( + { + v1.C.PARAMETER_ID: ["k1"], + v1.C.PARAMETER_SCALE: [v1.C.LIN], + v1.C.LOWER_BOUND: [1e-3], + v1.C.UPPER_BOUND: [1e3], + v1.C.NOMINAL_VALUE: [0.5], + v1.C.ESTIMATE: [1], + } + ) + ), + # refers to a dataset that is not in the measurement table + visualization_df=pd.DataFrame( + { + v1.C.PLOT_ID: ["p1"], + v1.C.DATASET_ID: ["no_such_dataset"], + v1.C.X_VALUES: [v1.C.TIME], + } + ), + ) + yaml_file = v1_problem.to_files_generic(tmp_path / "v1") + + with pytest.raises(ValueError, match="does not pass linting"): + petab1to2(yaml_file) + + problem = petab1to2(yaml_file, validate=False) + assert isinstance(problem, Problem) + assert len(problem.measurements) == 2 + + output_dir = tmp_path / "v2" + output_dir.mkdir() + petab1to2(yaml_file, output_dir, validate=False) + problem = Problem.from_yaml(output_dir / Path(yaml_file).name) + assert len(problem.measurements) == 2 + + try: import benchmark_models_petab From 9663aff439816a13f96b9c2d335195d312084eeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Fr=C3=B6hlich?= Date: Thu, 8 Oct 2026 18:40:49 +0100 Subject: [PATCH 2/2] Update petab/v2/petab1to2.py Co-authored-by: Dilan Pathirana <59329744+dilpath@users.noreply.github.com> --- petab/v2/petab1to2.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/petab/v2/petab1to2.py b/petab/v2/petab1to2.py index 23ca4bd8..435473cc 100644 --- a/petab/v2/petab1to2.py +++ b/petab/v2/petab1to2.py @@ -51,16 +51,11 @@ def petab1to2( The output directory to save the converted PEtab problem, or ``None``, to return a :class:`petab.v2.Problem` instance. :param validate: - Whether to lint the PEtab v1 problem before and the generated - PEtab v2 problem after the conversion. - If ``True`` (default), the conversion fails if either does not pass - linting. - If ``False``, both lint steps are skipped. This allows converting - problems that fail linting for reasons unrelated to the conversion, - e.g., a visualization table referencing non-existent datasets or an - intentionally incomplete model. In this case, the result should be - checked separately, e.g., using ``petablint``. - The YAML file is validated against the PEtab v1 schema in either case. + Whether to lint the input PEtab v1 problem, and the + output PEtab v2 problem. + If ``True`` (default), both problems are validated. + If ``False``, neither problem is validated. + The ``yaml_config`` content is validated regardless. :raises ValueError: If the input is invalid or, if ``validate`` is ``True``, if the input