33from __future__ import annotations
44
55import math
6- import re
76import shutil
87import warnings
98from contextlib import suppress
1615from pandas .io .common import get_handle , is_url
1716
1817from .. import v1 , v2
19- from ..v1 .math import sympify_petab
2018from ..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
2221from ..versions import get_major_version
2322from .models import MODEL_TYPE_SBML
2423
2524__all__ = ["petab1to2" ]
2625
2726
2827def 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