From 6fffafb6108387d274d6cbf886b977589dbd660d Mon Sep 17 00:00:00 2001 From: Daniel Weindl Date: Thu, 12 Dec 2024 10:40:43 +0100 Subject: [PATCH 1/4] Fix Prior.from_par_dict for missing priorParameters columns (#341) Previously, missing `*PriorParameters` would have resulted in a KeyError. --- petab/v1/priors.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/petab/v1/priors.py b/petab/v1/priors.py index f0f37f75..e1263946 100644 --- a/petab/v1/priors.py +++ b/petab/v1/priors.py @@ -224,10 +224,8 @@ def from_par_dict( dist_type = C.PARAMETER_SCALE_UNIFORM pscale = d.get(C.PARAMETER_SCALE, C.LIN) - if ( - pd.isna(d[f"{type_}PriorParameters"]) - and dist_type == C.PARAMETER_SCALE_UNIFORM - ): + params = d.get(f"{type_}PriorParameters", None) + if pd.isna(params) and dist_type == C.PARAMETER_SCALE_UNIFORM: params = ( scale(d[C.LOWER_BOUND], pscale), scale(d[C.UPPER_BOUND], pscale), @@ -236,7 +234,7 @@ def from_par_dict( params = tuple( map( float, - d[f"{type_}PriorParameters"].split(C.PARAMETER_SEPARATOR), + params.split(C.PARAMETER_SEPARATOR), ) ) return Prior( From 6d70b2079a99ea792d8ba070d4df81d5e2f93df0 Mon Sep 17 00:00:00 2001 From: Daniel Weindl Date: Mon, 16 Dec 2024 12:58:19 +0100 Subject: [PATCH 2/4] Fix petablint v2 warning (#342) Don't show `Support for PEtab2.0 and all of petab.v2 is experimental` warning when validating PEtab v1 problems. --- petab/petablint.py | 5 +++-- petab/versions.py | 27 ++++++++++++++++----------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/petab/petablint.py b/petab/petablint.py index f8228d42..43796c42 100755 --- a/petab/petablint.py +++ b/petab/petablint.py @@ -12,9 +12,8 @@ import petab.v1 as petab from petab.v1.C import FORMAT_VERSION -from petab.v2.lint import lint_problem +from petab.v1.yaml import validate from petab.versions import get_major_version -from petab.yaml import validate logger = logging.getLogger(__name__) @@ -178,6 +177,8 @@ def main(): ret = petab.lint.lint_problem(problem) sys.exit(ret) case 2: + from petab.v2.lint import lint_problem + validation_issues = lint_problem(args.yaml_file_name) if validation_issues: validation_issues.log(logger=logger) diff --git a/petab/versions.py b/petab/versions.py index 2b263aff..93f6a60a 100644 --- a/petab/versions.py +++ b/petab/versions.py @@ -3,10 +3,10 @@ from pathlib import Path +import petab from petab.v1 import Problem as V1Problem from petab.v1.C import FORMAT_VERSION from petab.v1.yaml import load_yaml -from petab.v2 import Problem as V2Problem __all__ = [ "get_major_version", @@ -14,22 +14,27 @@ def get_major_version( - problem: str | dict | Path | V1Problem | V2Problem, + problem: str | dict | Path | V1Problem | petab.v2.Problem, ) -> int: """Get the major version number of the given problem.""" - if isinstance(problem, V1Problem): - return 1 - - if isinstance(problem, V2Problem): - return 2 + version = None if isinstance(problem, str | Path): yaml_config = load_yaml(problem) version = yaml_config.get(FORMAT_VERSION) elif isinstance(problem, dict): version = problem.get(FORMAT_VERSION) - else: - raise ValueError(f"Unsupported argument type: {type(problem)}") - version = str(version) - return int(version.split(".")[0]) + if version is not None: + version = str(version) + return int(version.split(".")[0]) + + if isinstance(problem, V1Problem): + return 1 + + from . import v2 + + if isinstance(problem, v2.Problem): + return 2 + + raise ValueError(f"Unsupported argument type: {type(problem)}") From 1d3fda13a3e7ec213cf0d97ba3d8663d25fdcdcf Mon Sep 17 00:00:00 2001 From: Daniel Weindl Date: Mon, 16 Dec 2024 13:17:52 +0100 Subject: [PATCH 3/4] Doc: fix deprecated petablint invocation (#343) Closes https://github.com/PEtab-dev/PEtab/issues/592. --- doc/example/example_petablint.ipynb | 79 ++++------------------------- 1 file changed, 10 insertions(+), 69 deletions(-) diff --git a/doc/example/example_petablint.ipynb b/doc/example/example_petablint.ipynb index ed20b0d9..6925a433 100644 --- a/doc/example/example_petablint.ipynb +++ b/doc/example/example_petablint.ipynb @@ -16,75 +16,26 @@ }, { "cell_type": "code", - "execution_count": 1, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "usage: petablint [-h] [-v] [-s SBML_FILE_NAME] [-m MEASUREMENT_FILE_NAME]\r\n", - " [-c CONDITION_FILE_NAME] [-p PARAMETER_FILE_NAME]\r\n", - " [-y YAML_FILE_NAME | -n MODEL_NAME] [-d DIRECTORY]\r\n", - "\r\n", - "Check if a set of files adheres to the PEtab format.\r\n", - "\r\n", - "optional arguments:\r\n", - " -h, --help show this help message and exit\r\n", - " -v, --verbose More verbose output\r\n", - " -s SBML_FILE_NAME, --sbml SBML_FILE_NAME\r\n", - " SBML model filename\r\n", - " -m MEASUREMENT_FILE_NAME, --measurements MEASUREMENT_FILE_NAME\r\n", - " Measurement table\r\n", - " -c CONDITION_FILE_NAME, --conditions CONDITION_FILE_NAME\r\n", - " Conditions table\r\n", - " -p PARAMETER_FILE_NAME, --parameters PARAMETER_FILE_NAME\r\n", - " Parameter table\r\n", - " -y YAML_FILE_NAME, --yaml YAML_FILE_NAME\r\n", - " PEtab YAML problem filename\r\n", - " -n MODEL_NAME, --model-name MODEL_NAME\r\n", - " Model name where all files are in the working\r\n", - " directory and follow PEtab naming convention.\r\n", - " Specifying -[smcp] will override defaults\r\n", - " -d DIRECTORY, --directory DIRECTORY\r\n" - ] - } - ], "source": [ "!petablint -h" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", "metadata": {}, - "source": [ - "Let's look at an example: In the example_Fujita folder, we have a PEtab configuration file `Fujita.yaml` telling which files belong to the Fujita model:" - ] + "source": "Let's look at an example: In the `example_Fujita/` directory, we have a PEtab problem configuration file `Fujita.yaml` telling which files belong to the \"Fujita\" problem:" }, { "cell_type": "code", - "execution_count": 2, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "parameter_file: Fujita_parameters.tsv\r\n", - "petab_version: 0.0.0a17\r\n", - "problems:\r\n", - "- condition_files:\r\n", - " - Fujita_experimentalCondition.tsv\r\n", - " measurement_files:\r\n", - " - Fujita_measurementData.tsv\r\n", - " sbml_files:\r\n", - " - Fujita_model.xml\r\n" - ] - } - ], "source": [ "!cat example_Fujita/Fujita.yaml" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -95,20 +46,10 @@ }, { "cell_type": "code", - "execution_count": 3, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[0m" - ] - } - ], - "source": [ - "!petablint -y example_Fujita/Fujita.yaml" - ] + "source": "!petablint example_Fujita/Fujita.yaml", + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", From 6a433e0606e56c36f8c82de8bd4a4f6b25fc65e2 Mon Sep 17 00:00:00 2001 From: Daniel Weindl Date: Wed, 18 Dec 2024 19:24:05 +0100 Subject: [PATCH 4/4] Support for petab v2 experiments (#332) Add basic support for PEtab version 2 experiments (see also https://github.com/PEtab-dev/PEtab/issues/586, and https://github.com/PEtab-dev/PEtab/pull/581). Follow-up to #334. Partially supersedes #263, which was started before petab.v1/petab.v2 were introduced and before https://github.com/PEtab-dev/PEtab/issues/586. * updates the required fields in the measurement table * updates some validation functions to not expect the old `simulationConditionId`s (but does not do full validation yet) * extends PEtab v1 up-conversion to create a new experiment table. --------- Co-authored-by: Dilan Pathirana <59329744+dilpath@users.noreply.github.com> --- petab/v1/calculate.py | 7 +++ petab/v1/problem.py | 8 +-- petab/v2/C.py | 17 ++---- petab/v2/__init__.py | 5 +- petab/v2/lint.py | 129 ++++++++++++++++++++++++++------------- petab/v2/petab1to2.py | 102 ++++++++++++++++++++++++++++--- petab/v2/problem.py | 27 +++----- tests/v2/test_problem.py | 4 +- 8 files changed, 207 insertions(+), 92 deletions(-) diff --git a/petab/v1/calculate.py b/petab/v1/calculate.py index 3cc86f73..32930807 100644 --- a/petab/v1/calculate.py +++ b/petab/v1/calculate.py @@ -97,6 +97,9 @@ def calculate_residuals_for_table( Calculate residuals for a single measurement table. For the arguments, see `calculate_residuals`. """ + # below, we rely on a unique index + measurement_df = measurement_df.reset_index(drop=True) + # create residual df as copy of measurement df, change column residual_df = measurement_df.copy(deep=True).rename( columns={MEASUREMENT: RESIDUAL} @@ -120,6 +123,10 @@ def calculate_residuals_for_table( for col in compared_cols ] mask = reduce(lambda x, y: x & y, masks) + if mask.sum() == 0: + raise ValueError( + f"Could not find simulation for measurement {row}." + ) simulation = simulation_df.loc[mask][SIMULATION].iloc[0] if scale: # apply scaling diff --git a/petab/v1/problem.py b/petab/v1/problem.py index ea300258..6be96c68 100644 --- a/petab/v1/problem.py +++ b/petab/v1/problem.py @@ -1149,8 +1149,8 @@ def add_measurement( sim_cond_id: str, time: float, measurement: float, - observable_parameters: Sequence[str] = None, - noise_parameters: Sequence[str] = None, + observable_parameters: Sequence[str | float] = None, + noise_parameters: Sequence[str | float] = None, preeq_cond_id: str = None, ): """Add a measurement to the problem. @@ -1172,11 +1172,11 @@ def add_measurement( } if observable_parameters is not None: record[OBSERVABLE_PARAMETERS] = [ - PARAMETER_SEPARATOR.join(observable_parameters) + PARAMETER_SEPARATOR.join(map(str, observable_parameters)) ] if noise_parameters is not None: record[NOISE_PARAMETERS] = [ - PARAMETER_SEPARATOR.join(noise_parameters) + PARAMETER_SEPARATOR.join(map(str, noise_parameters)) ] if preeq_cond_id is not None: record[PREEQUILIBRATION_CONDITION_ID] = [preeq_cond_id] diff --git a/petab/v2/C.py b/petab/v2/C.py index cb095c68..1ab6f795 100644 --- a/petab/v2/C.py +++ b/petab/v2/C.py @@ -13,14 +13,6 @@ #: Experiment ID column in the measurement table EXPERIMENT_ID = "experimentId" -# TODO: remove -#: Preequilibration condition ID column in the measurement table -PREEQUILIBRATION_CONDITION_ID = "preequilibrationConditionId" - -# TODO: remove -#: Simulation condition ID column in the measurement table -SIMULATION_CONDITION_ID = "simulationConditionId" - #: Measurement value column in the measurement table MEASUREMENT = "measurement" @@ -30,6 +22,9 @@ #: Time value that indicates steady-state measurements TIME_STEADY_STATE = _math.inf +#: Time value that indicates pre-equilibration in the experiments table +TIME_PREEQUILIBRATION = -_math.inf + #: Observable parameters column in the measurement table OBSERVABLE_PARAMETERS = "observableParameters" @@ -45,17 +40,13 @@ #: Mandatory columns of measurement table MEASUREMENT_DF_REQUIRED_COLS = [ OBSERVABLE_ID, - # TODO: add - # EXPERIMENT_ID, - SIMULATION_CONDITION_ID, + EXPERIMENT_ID, MEASUREMENT, TIME, ] #: Optional columns of measurement table MEASUREMENT_DF_OPTIONAL_COLS = [ - # TODO: remove - PREEQUILIBRATION_CONDITION_ID, OBSERVABLE_PARAMETERS, NOISE_PARAMETERS, DATASET_ID, diff --git a/petab/v2/__init__.py b/petab/v2/__init__.py index 0525d66c..adeb0e84 100644 --- a/petab/v2/__init__.py +++ b/petab/v2/__init__.py @@ -27,7 +27,10 @@ # import after v1 from ..version import __version__ # noqa: F401, E402 -from . import models # noqa: F401, E402 +from . import ( # noqa: F401, E402 + C, # noqa: F401, E402 + models, # noqa: F401, E402 +) from .conditions import * # noqa: F403, F401, E402 from .experiments import ( # noqa: F401, E402 get_experiment_df, diff --git a/petab/v2/lint.py b/petab/v2/lint.py index 76f3cdb6..2473c74d 100644 --- a/petab/v2/lint.py +++ b/petab/v2/lint.py @@ -15,6 +15,9 @@ from .. import v2 from ..v1.lint import ( _check_df, + assert_measured_observables_defined, + assert_measurements_not_null, + assert_measurements_numeric, assert_model_parameters_in_condition_or_parameter_table, assert_no_leading_trailing_whitespace, assert_parameter_bounds_are_numeric, @@ -23,13 +26,16 @@ assert_parameter_prior_parameters_are_valid, assert_parameter_prior_type_is_valid, assert_parameter_scale_is_valid, + assert_unique_observable_ids, assert_unique_parameter_ids, check_ids, - check_measurement_df, check_observable_df, check_parameter_bounds, ) -from ..v1.measurements import split_parameter_replacement_list +from ..v1.measurements import ( + assert_overrides_match_parameter_count, + split_parameter_replacement_list, +) from ..v1.observables import get_output_parameters, get_placeholders from ..v1.visualize.lint import validate_visualization_df from ..v2.C import * @@ -102,6 +108,23 @@ class ValidationError(ValidationIssue): level: ValidationIssueSeverity = field( default=ValidationIssueSeverity.ERROR, init=False ) + task: str | None = None + + def __post_init__(self): + if self.task is None: + self.task = self._get_task_name() + + def _get_task_name(self): + """Get the name of the ValidationTask that raised this error.""" + import inspect + + # walk up the stack until we find the ValidationTask.run method + for frame_info in inspect.stack(): + frame = frame_info.frame + if "self" in frame.f_locals: + task = frame.f_locals["self"] + if isinstance(task, ValidationTask): + return task.__class__.__name__ class ValidationResultList(list[ValidationIssue]): @@ -237,8 +260,51 @@ def run(self, problem: Problem) -> ValidationIssue | None: if problem.measurement_df is None: return + df = problem.measurement_df try: - check_measurement_df(problem.measurement_df, problem.observable_df) + _check_df(df, MEASUREMENT_DF_REQUIRED_COLS, "measurement") + + for column_name in MEASUREMENT_DF_REQUIRED_COLS: + if not np.issubdtype(df[column_name].dtype, np.number): + assert_no_leading_trailing_whitespace( + df[column_name].values, column_name + ) + + for column_name in MEASUREMENT_DF_OPTIONAL_COLS: + if column_name in df and not np.issubdtype( + df[column_name].dtype, np.number + ): + assert_no_leading_trailing_whitespace( + df[column_name].values, column_name + ) + + if problem.observable_df is not None: + assert_measured_observables_defined(df, problem.observable_df) + assert_overrides_match_parameter_count( + df, problem.observable_df + ) + + if OBSERVABLE_TRANSFORMATION in problem.observable_df: + # Check for positivity of measurements in case of + # log-transformation + assert_unique_observable_ids(problem.observable_df) + # If the above is not checked, in the following loop + # trafo may become a pandas Series + for measurement, obs_id in zip( + df[MEASUREMENT], df[OBSERVABLE_ID], strict=True + ): + trafo = problem.observable_df.loc[ + obs_id, OBSERVABLE_TRANSFORMATION + ] + if measurement <= 0.0 and trafo in [LOG, LOG10]: + raise ValueError( + "Measurements with observable " + f"transformation {trafo} must be " + f"positive, but {measurement} <= 0." + ) + + assert_measurements_not_null(df) + assert_measurements_numeric(df) except AssertionError as e: return ValidationError(str(e)) @@ -247,46 +313,20 @@ def run(self, problem: Problem) -> ValidationIssue | None: # condition table should be an error if the measurement table refers # to conditions - # check that measured experiments/conditions exist - # TODO: fully switch to experiment table and remove this: - if SIMULATION_CONDITION_ID in problem.measurement_df: - if problem.condition_df is None: - return - used_conditions = set( - problem.measurement_df[SIMULATION_CONDITION_ID].dropna().values - ) - if PREEQUILIBRATION_CONDITION_ID in problem.measurement_df: - used_conditions |= set( - problem.measurement_df[PREEQUILIBRATION_CONDITION_ID] - .dropna() - .values - ) - available_conditions = set( - problem.condition_df[CONDITION_ID].unique() - ) - if missing_conditions := (used_conditions - available_conditions): - return ValidationError( - "Measurement table references conditions that " - "are not specified in the condition table: " - + str(missing_conditions) - ) - elif EXPERIMENT_ID in problem.measurement_df: - if problem.experiment_df is None: - return - used_experiments = set( - problem.measurement_df[EXPERIMENT_ID].values - ) - available_experiments = set( - problem.condition_df[CONDITION_ID].unique() + # check that measured experiments + if problem.experiment_df is None: + return + + used_experiments = set(problem.measurement_df[EXPERIMENT_ID].values) + available_experiments = set( + problem.experiment_df[EXPERIMENT_ID].unique() + ) + if missing_experiments := (used_experiments - available_experiments): + raise AssertionError( + "Measurement table references experiments that " + "are not specified in the experiments table: " + + str(missing_experiments) ) - if missing_experiments := ( - used_experiments - available_experiments - ): - raise AssertionError( - "Measurement table references experiments that " - "are not specified in the experiments table: " - + str(missing_experiments) - ) class CheckConditionTable(ValidationTask): @@ -486,7 +526,7 @@ def run(self, problem: Problem) -> ValidationIssue | None: ) required_conditions = problem.experiment_df[CONDITION_ID].unique() - existing_conditions = problem.condition_df.index + existing_conditions = problem.condition_df[CONDITION_ID].unique() missing_conditions = set(required_conditions) - set( existing_conditions @@ -771,7 +811,8 @@ def append_overrides(overrides): ) # parameters that are overridden via the condition table are not allowed - parameter_ids -= set(problem.condition_df[TARGET_ID].unique()) + if problem.condition_df is not None: + parameter_ids -= set(problem.condition_df[TARGET_ID].unique()) return parameter_ids diff --git a/petab/v2/petab1to2.py b/petab/v2/petab1to2.py index d5d06229..78304328 100644 --- a/petab/v2/petab1to2.py +++ b/petab/v2/petab1to2.py @@ -4,8 +4,8 @@ from itertools import chain from pathlib import Path from urllib.parse import urlparse +from uuid import uuid4 -import numpy as np import pandas as pd from pandas.io.common import get_handle, is_url @@ -98,10 +98,81 @@ def petab1to2(yaml_config: Path | str, output_dir: Path | str = None): condition_df = v1v2_condition_df(condition_df, petab_problem.model) v2.write_condition_df(condition_df, get_dest_path(condition_file)) + # records for the experiment table to be created + experiments = [] + + def create_experiment_id(sim_cond_id: str, preeq_cond_id: str) -> str: + if not sim_cond_id and not preeq_cond_id: + return "" + if preeq_cond_id: + preeq_cond_id = f"{preeq_cond_id}_" + exp_id = f"experiment__{preeq_cond_id}__{sim_cond_id}" + if exp_id in experiments: # noqa: B023 + i = 1 + while f"{exp_id}_{i}" in experiments: # noqa: B023 + i += 1 + exp_id = f"{exp_id}_{i}" + return exp_id + + measured_experiments = ( + petab_problem.get_simulation_conditions_from_measurement_df() + ) + for ( + _, + row, + ) in measured_experiments.iterrows(): + # generate a new experiment for each simulation / pre-eq condition + # combination + sim_cond_id = row[v1.C.SIMULATION_CONDITION_ID] + preeq_cond_id = row.get(v1.C.PREEQUILIBRATION_CONDITION_ID, "") + exp_id = create_experiment_id(sim_cond_id, preeq_cond_id) + if preeq_cond_id: + experiments.append( + { + v2.C.EXPERIMENT_ID: exp_id, + v2.C.CONDITION_ID: preeq_cond_id, + v2.C.TIME: v2.C.TIME_PREEQUILIBRATION, + } + ) + experiments.append( + { + v2.C.EXPERIMENT_ID: exp_id, + v2.C.CONDITION_ID: sim_cond_id, + v2.C.TIME: 0, + } + ) + if experiments: + exp_table_path = output_dir / "experiments.tsv" + if exp_table_path.exists(): + raise ValueError( + f"Experiment table file {exp_table_path} already exists." + ) + problem_config[v2.C.EXPERIMENT_FILES] = [exp_table_path.name] + v2.write_experiment_df( + v2.get_experiment_df(pd.DataFrame(experiments)), exp_table_path + ) + for measurement_file in problem_config.get(v2.C.MEASUREMENT_FILES, []): measurement_df = v1.get_measurement_df( get_src_path(measurement_file) ) + # if there is already an experiment ID column, we rename it + if v2.C.EXPERIMENT_ID in measurement_df.columns: + measurement_df.rename( + columns={v2.C.EXPERIMENT_ID: f"experiment_id_{uuid4()}"}, + inplace=True, + ) + # add pre-eq condition id if not present or convert to string + # for simplicity + if v1.C.PREEQUILIBRATION_CONDITION_ID in measurement_df.columns: + measurement_df[ + v1.C.PREEQUILIBRATION_CONDITION_ID + ] = measurement_df[v1.C.PREEQUILIBRATION_CONDITION_ID].astype( + str + ) + else: + measurement_df[v1.C.PREEQUILIBRATION_CONDITION_ID] = "" + if ( petab_problem.condition_df is not None and len( @@ -110,20 +181,33 @@ def petab1to2(yaml_config: Path | str, output_dir: Path | str = None): ) == 0 ): - # can't have "empty" conditions with no overrides in v2 - # TODO: this needs to be done condition wise - measurement_df[v2.C.SIMULATION_CONDITION_ID] = np.nan + # we can't have "empty" conditions with no overrides in v2, + # therefore, we drop the respective condition ID completely + # TODO: or can we? + # TODO: this needs to be checked condition-wise, not globally + measurement_df[v1.C.SIMULATION_CONDITION_ID] = "" if ( v1.C.PREEQUILIBRATION_CONDITION_ID in measurement_df.columns ): - measurement_df[v2.C.PREEQUILIBRATION_CONDITION_ID] = np.nan + measurement_df[v1.C.PREEQUILIBRATION_CONDITION_ID] = "" + # condition IDs to experiment IDs + measurement_df.insert( + 0, + v2.C.EXPERIMENT_ID, + measurement_df.apply( + lambda row: create_experiment_id( + row[v1.C.SIMULATION_CONDITION_ID], + row.get(v1.C.PREEQUILIBRATION_CONDITION_ID, ""), + ), + axis=1, + ), + ) + del measurement_df[v1.C.SIMULATION_CONDITION_ID] + del measurement_df[v1.C.PREEQUILIBRATION_CONDITION_ID] v2.write_measurement_df( measurement_df, get_dest_path(measurement_file) ) - # TODO: Measurements: preequilibration to experiments/timecourses once - # finalized - ... # validate updated Problem validation_issues = v2.lint_problem(new_yaml_file) @@ -189,7 +273,7 @@ def v1v2_condition_df( """Convert condition table from petab v1 to v2.""" condition_df = condition_df.copy().reset_index() with suppress(KeyError): - # TODO: are condition names still supported in v2? + # conditionName was dropped in PEtab v2 condition_df.drop(columns=[v2.C.CONDITION_NAME], inplace=True) condition_df = condition_df.melt( diff --git a/petab/v2/problem.py b/petab/v2/problem.py index 1df2c677..f8dad754 100644 --- a/petab/v2/problem.py +++ b/petab/v2/problem.py @@ -5,7 +5,6 @@ import os import tempfile import traceback -import warnings from collections.abc import Sequence from math import nan from numbers import Number @@ -92,12 +91,6 @@ def __init__( ValidationTask ] = default_validation_tasks.copy() self.config = config - if self.experiment_df is not None: - warnings.warn( - "The experiment table is not yet supported and " - "will be ignored.", - stacklevel=2, - ) def __str__(self): model = f"with model ({self.model})" if self.model else "without model" @@ -908,47 +901,43 @@ def add_parameter( def add_measurement( self, obs_id: str, - sim_cond_id: str, + experiment_id: str, time: float, measurement: float, - observable_parameters: Sequence[str] = None, - noise_parameters: Sequence[str] = None, - preeq_cond_id: str = None, + observable_parameters: Sequence[str | float] = None, + noise_parameters: Sequence[str | float] = None, ): """Add a measurement to the problem. Arguments: obs_id: The observable ID - sim_cond_id: The simulation condition ID + experiment_id: The experiment ID time: The measurement time measurement: The measurement value observable_parameters: The observable parameters noise_parameters: The noise parameters - preeq_cond_id: The pre-equilibration condition ID """ record = { OBSERVABLE_ID: [obs_id], - SIMULATION_CONDITION_ID: [sim_cond_id], + EXPERIMENT_ID: [experiment_id], TIME: [time], MEASUREMENT: [measurement], } if observable_parameters is not None: record[OBSERVABLE_PARAMETERS] = [ - PARAMETER_SEPARATOR.join(observable_parameters) + PARAMETER_SEPARATOR.join(map(str, observable_parameters)) ] if noise_parameters is not None: record[NOISE_PARAMETERS] = [ - PARAMETER_SEPARATOR.join(noise_parameters) + PARAMETER_SEPARATOR.join(map(str, noise_parameters)) ] - if preeq_cond_id is not None: - record[PREEQUILIBRATION_CONDITION_ID] = [preeq_cond_id] tmp_df = pd.DataFrame(record) self.measurement_df = ( pd.concat([self.measurement_df, tmp_df]) if self.measurement_df is not None else tmp_df - ) + ).reset_index(drop=True) def add_mapping(self, petab_id: str, model_id: str): """Add a mapping table entry to the problem. diff --git a/tests/v2/test_problem.py b/tests/v2/test_problem.py index ba210af0..dadc3a7c 100644 --- a/tests/v2/test_problem.py +++ b/tests/v2/test_problem.py @@ -30,7 +30,7 @@ def test_load_remote(): """Test loading remote files""" yaml_url = ( "https://raw.githubusercontent.com/PEtab-dev/petab_test_suite" - "/update_v2/petabtests/cases/v2.0.0/sbml/0001/_0001.yaml" + "/update_v2/petabtests/cases/v2.0.0/sbml/0010/_0010.yaml" ) petab_problem = Problem.from_yaml(yaml_url) @@ -83,7 +83,7 @@ def test_problem_from_yaml_multiple_files(): problem.experiment_df, Path(tmpdir, f"experiments{i}.tsv") ) - problem.add_measurement(f"observable{i}", f"condition{i}", 1, 1) + problem.add_measurement(f"observable{i}", f"experiment{i}", 1, 1) petab.write_measurement_df( problem.measurement_df, Path(tmpdir, f"measurements{i}.tsv") )