Skip to content

Commit 7bf1691

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/petab1to2-prior-mappings
# Conflicts: # petab/v2/petab1to2.py # tests/v2/test_conversion.py
2 parents 4ae2024 + 1a83733 commit 7bf1691

2 files changed

Lines changed: 362 additions & 56 deletions

File tree

‎petab/v2/petab1to2.py‎

Lines changed: 60 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from __future__ import annotations
44

55
import math
6-
import re
76
import shutil
87
import warnings
98
from contextlib import suppress
@@ -16,17 +15,20 @@
1615
from pandas.io.common import get_handle, is_url
1716

1817
from .. import v1, v2
19-
from ..v1.math import sympify_petab
2018
from ..v1.parameters import unscale
21-
from ..v1.yaml import get_path_prefix, load_yaml, validate
19+
from ..v1.yaml import get_path_prefix, load_yaml
20+
from ..v1.yaml import validate as validate_yaml
2221
from ..versions import get_major_version
2322
from .models import MODEL_TYPE_SBML
2423

2524
__all__ = ["petab1to2"]
2625

2726

2827
def petab1to2(
29-
yaml_config: Path | str, output_dir: Path | str | None = None
28+
yaml_config: Path | str,
29+
output_dir: Path | str | None = None,
30+
*,
31+
validate: bool = True,
3032
) -> v2.Problem | None:
3133
"""Convert from PEtab 1.0 to PEtab 2.0 format.
3234
@@ -48,31 +50,45 @@ def petab1to2(
4850
:param output_dir:
4951
The output directory to save the converted PEtab problem, or ``None``,
5052
to return a :class:`petab.v2.Problem` instance.
53+
:param validate:
54+
Whether to lint the input PEtab v1 problem, and the
55+
output PEtab v2 problem.
56+
If ``True`` (default), both problems are validated.
57+
If ``False``, neither problem is validated.
58+
The ``yaml_config`` content is validated regardless.
5159
5260
:raises ValueError:
53-
If the input is invalid or does not pass linting or if the generated
54-
files do not pass linting.
61+
If the input is invalid or, if ``validate`` is ``True``, if the input
62+
does not pass linting or if the generated files do not pass linting.
5563
"""
5664
if output_dir is not None:
57-
return petab_files_1to2(yaml_config, output_dir)
65+
return petab_files_1to2(yaml_config, output_dir, validate=validate)
5866

5967
with TemporaryDirectory() as tmp_dir:
60-
petab_files_1to2(yaml_config, tmp_dir)
68+
petab_files_1to2(yaml_config, tmp_dir, validate=validate)
6169
return v2.Problem.from_yaml(Path(tmp_dir, Path(yaml_config).name))
6270

6371

64-
def petab_files_1to2(yaml_config: Path | str | dict, output_dir: Path | str):
72+
def petab_files_1to2(
73+
yaml_config: Path | str | dict,
74+
output_dir: Path | str,
75+
*,
76+
validate: bool = True,
77+
):
6578
"""Convert PEtab files from PEtab 1.0 to PEtab 2.0.
6679
6780
6881
:param yaml_config:
6982
The PEtab problem as dictionary or YAML file name.
7083
:param output_dir:
7184
The output directory to save the converted PEtab problem.
85+
:param validate:
86+
Whether to lint the PEtab v1 problem before and the generated
87+
PEtab v2 problem after the conversion. See :func:`petab1to2`.
7288
7389
:raises ValueError:
74-
If the input is invalid or does not pass linting or if the generated
75-
files do not pass linting.
90+
If the input is invalid or, if ``validate`` is ``True``, if the input
91+
does not pass linting or if the generated files do not pass linting.
7692
"""
7793
if isinstance(yaml_config, Path | str):
7894
yaml_file = str(yaml_config)
@@ -87,7 +103,7 @@ def petab_files_1to2(yaml_config: Path | str | dict, output_dir: Path | str):
87103
get_dest_path = lambda filename: f"{output_dir}/{filename}" # noqa: E731
88104

89105
# Validate the original PEtab problem
90-
validate(yaml_config, path_prefix=path_prefix)
106+
validate_yaml(yaml_config, path_prefix=path_prefix)
91107
if get_major_version(yaml_config) != 1:
92108
raise ValueError("PEtab problem is not version 1.")
93109
petab_problem = v1.Problem.from_yaml(yaml_file or yaml_config)
@@ -96,7 +112,7 @@ def petab_files_1to2(yaml_config: Path | str | dict, output_dir: Path | str):
96112
petab_problem.condition_df = petab_problem.condition_df.drop(
97113
columns=[v1.C.CONDITION_NAME], errors="ignore"
98114
)
99-
if v1.lint_problem(petab_problem):
115+
if validate and v1.lint_problem(petab_problem):
100116
raise ValueError("Provided PEtab problem does not pass linting.")
101117

102118
output_dir = Path(output_dir)
@@ -136,20 +152,23 @@ def petab_files_1to2(yaml_config: Path | str | dict, output_dir: Path | str):
136152
# records for the experiment table to be created
137153
experiments = []
138154

155+
def v2_condition_id(cond_id: str) -> str:
156+
"""Get the condition ID to use in the v2 experiment table.
157+
158+
Conditions without any changes (all-NaN rows) are not included in the
159+
v2 condition table. Periods applying such a condition get an empty
160+
condition ID ("use the model as is").
161+
"""
162+
if petab_problem.condition_df.loc[cond_id].notna().any():
163+
return cond_id
164+
return ""
165+
139166
def create_experiment_id(sim_cond_id: str, preeq_cond_id: str) -> str:
140167
if not sim_cond_id and not preeq_cond_id:
141168
return ""
142-
# check whether the conditions will exist in the v2 condition table
143-
sim_cond_exists = (
144-
petab_problem.condition_df.loc[sim_cond_id].notna().any()
145-
)
146-
preeq_cond_exists = (
147-
preeq_cond_id
148-
and petab_problem.condition_df.loc[preeq_cond_id].notna().any()
149-
)
150-
if not sim_cond_exists and not preeq_cond_exists:
151-
# if we have only all-NaN conditions, we don't create a new
152-
# experiment
169+
if not preeq_cond_id and not v2_condition_id(sim_cond_id):
170+
# without pre-equilibration and without any changes, there is
171+
# nothing to be described by an experiment
153172
return ""
154173

155174
if preeq_cond_id:
@@ -181,14 +200,14 @@ def create_experiment_id(sim_cond_id: str, preeq_cond_id: str) -> str:
181200
{
182201
v2.C.EXPERIMENT_ID: exp_id,
183202
v2.C.TIME: v2.C.TIME_PREEQUILIBRATION,
184-
v2.C.CONDITION_ID: preeq_cond_id,
203+
v2.C.CONDITION_ID: v2_condition_id(preeq_cond_id),
185204
}
186205
)
187206
experiments.append(
188207
{
189208
v2.C.EXPERIMENT_ID: exp_id,
190209
v2.C.TIME: 0,
191-
v2.C.CONDITION_ID: sim_cond_id,
210+
v2.C.CONDITION_ID: v2_condition_id(sim_cond_id),
192211
}
193212
)
194213
if experiments:
@@ -219,20 +238,6 @@ def create_experiment_id(sim_cond_id: str, preeq_cond_id: str) -> str:
219238
else:
220239
measurement_df[v1.C.PREEQUILIBRATION_CONDITION_ID] = ""
221240

222-
if (
223-
petab_problem.condition_df is not None
224-
and len(
225-
set(petab_problem.condition_df.columns) - {v1.C.CONDITION_NAME}
226-
)
227-
== 0
228-
):
229-
# we can't have "empty" conditions with no overrides in v2,
230-
# therefore, we drop the respective condition ID completely
231-
# TODO: or can we?
232-
# TODO: this needs to be checked condition-wise, not globally
233-
measurement_df[v1.C.SIMULATION_CONDITION_ID] = ""
234-
if v1.C.PREEQUILIBRATION_CONDITION_ID in measurement_df.columns:
235-
measurement_df[v1.C.PREEQUILIBRATION_CONDITION_ID] = ""
236241
# condition IDs to experiment IDs
237242
measurement_df.insert(
238243
0,
@@ -255,6 +260,9 @@ def create_experiment_id(sim_cond_id: str, preeq_cond_id: str) -> str:
255260
new_yaml_file = output_dir / Path(yaml_file).name
256261
new_yaml_config.to_yaml(new_yaml_file)
257262

263+
if not validate:
264+
return
265+
258266
# validate updated Problem
259267
validation_issues = v2.lint_problem(new_yaml_file)
260268

@@ -442,23 +450,19 @@ def extract_placeholders(row: pd.Series, type_: str) -> str:
442450
if pd.isna(formula):
443451
return ""
444452

445-
t = f"{re.escape(type_)}Parameter"
446-
o = re.escape(row[v1.C.OBSERVABLE_ID])
447-
448-
pattern = re.compile(rf"(?:^|\W)({t}\d+_{o})(?=\W|$)")
449-
450-
expr = sympify_petab(formula)
451-
# for 10+ placeholders, the current lexicographical sorting will result
452-
# in incorrect ordering of the placeholder IDs, so that they don't
453-
# align with the overrides in the measurement table, but who does
454-
# that anyway?
455-
return v2.C.PARAMETER_SEPARATOR.join(
456-
sorted(
457-
str(sym)
458-
for sym in expr.free_symbols
459-
if sym.is_Symbol and pattern.match(str(sym))
453+
# v1 overrides are positional: the i-th override replaces
454+
# `{type_}Parameter{i}_{observableId}`. This returns the placeholders
455+
# ordered by index and fails if they are not numbered 1..n.
456+
try:
457+
placeholders = v1.get_formula_placeholders(
458+
formula, row[v1.C.OBSERVABLE_ID], type_
460459
)
461-
)
460+
except AssertionError as e:
461+
raise ValueError(
462+
f"Cannot convert the {type_} placeholders of observable "
463+
f"`{row[v1.C.OBSERVABLE_ID]}': {e}"
464+
) from e
465+
return v2.C.PARAMETER_SEPARATOR.join(placeholders)
462466

463467
df[v2.C.OBSERVABLE_PLACEHOLDERS] = df.apply(
464468
extract_placeholders, args=("observable",), axis=1

0 commit comments

Comments
 (0)