Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
43 changes: 32 additions & 11 deletions petab/v2/petab1to2.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,19 @@

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

__all__ = ["petab1to2"]


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.

Expand All @@ -46,31 +50,45 @@ 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 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 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.


:param yaml_config:
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)
Expand All @@ -85,7 +103,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)
Expand All @@ -94,7 +112,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)
Expand Down Expand Up @@ -242,6 +260,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)

Expand Down
68 changes: 68 additions & 0 deletions tests/v2/test_conversion.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
from pathlib import Path

import pandas as pd
import pytest
Expand Down Expand Up @@ -170,6 +171,73 @@ def test_petab1to2_empty_conditions_no_preequilibration(tmp_path):
assert _measured_periods(problem) == [None]


def test_petab1to2_validate(tmp_path):
"""Test that `validate=False` allows converting problems that fail
linting for reasons unrelated to the conversion."""
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
Comment on lines +230 to +238


try:
import benchmark_models_petab

Expand Down
Loading