From d78c9c02c032b58bcf4c1cdbd4bd8054565325bd Mon Sep 17 00:00:00 2001 From: Daniel Weindl Date: Mon, 16 Dec 2024 16:57:23 +0100 Subject: [PATCH 1/5] observables --- petab/v2/core.py | 109 ++++++++++++++++++++++++++++++++++++++++++ petab/v2/problem.py | 6 +++ tests/v2/test_core.py | 20 ++++++++ 3 files changed, 135 insertions(+) create mode 100644 petab/v2/core.py create mode 100644 tests/v2/test_core.py diff --git a/petab/v2/core.py b/petab/v2/core.py new file mode 100644 index 00000000..cdf22f21 --- /dev/null +++ b/petab/v2/core.py @@ -0,0 +1,109 @@ +"""Types around the PEtab object model.""" +from __future__ import annotations + +from enum import Enum +from pathlib import Path + +import numpy as np +import pandas as pd +import sympy as sp +from pydantic import ( + BaseModel, + Field, + ValidationInfo, + field_validator, +) + +from ..v1.lint import is_valid_identifier +from ..v1.math import sympify_petab +from . import C + + +class ObservableTransformation(str, Enum): + LIN = C.LIN + LOG = C.LOG + LOG10 = C.LOG10 + + +class NoiseDistribution(str, Enum): + NORMAL = C.NORMAL + LAPLACE = C.LAPLACE + + +class Observable(BaseModel): + id: str = Field(alias=C.OBSERVABLE_ID) + name: str | None = Field(alias=C.OBSERVABLE_NAME, default=None) + formula: sp.Basic | None = Field(alias=C.OBSERVABLE_FORMULA, default=None) + transformation: ObservableTransformation = Field( + alias=C.OBSERVABLE_TRANSFORMATION, default=ObservableTransformation.LIN + ) + noise_formula: sp.Basic | None = Field(alias=C.NOISE_FORMULA, default=None) + noise_distribution: NoiseDistribution = Field( + alias=C.NOISE_DISTRIBUTION, default=NoiseDistribution.NORMAL + ) + + @field_validator("id") + @classmethod + def validate_id(cls, v): + if not v: + raise ValueError("ID must not be empty.") + if not is_valid_identifier(v): + raise ValueError(f"Invalid ID: {v}") + return v + + @field_validator( + "name", + "formula", + "noise_formula", + "noise_formula", + "noise_distribution", + "transformation", + mode="before", + ) + @classmethod + def convert_nan_to_none(cls, v, info: ValidationInfo): + if isinstance(v, float) and np.isnan(v): + return cls.model_fields[info.field_name].default + return v + + @field_validator("formula", "noise_formula", mode="before") + @classmethod + def sympify(cls, v): + if v is None or isinstance(v, sp.Basic): + return v + if isinstance(v, float) and np.isnan(v): + return None + + return sympify_petab(v) + + class Config: + populate_by_name = True + arbitrary_types_allowed = True + + +class ObservablesTable(BaseModel): + observables: list[Observable] + + @classmethod + def from_dataframe(cls, df: pd.DataFrame) -> ObservablesTable: + if df is None: + return cls(observables=[]) + + observables = [ + Observable(**row.to_dict()) + for _, row in df.reset_index().iterrows() + ] + + return cls(observables=observables) + + def to_dataframe(self) -> pd.DataFrame: + return pd.DataFrame(self.model_dump()["observables"]) + + @classmethod + def from_tsv(cls, file_path: str | Path) -> ObservablesTable: + df = pd.read_csv(file_path, sep="\t") + return cls.from_dataframe(df) + + def to_tsv(self, file_path: str | Path) -> None: + df = self.to_dataframe() + df.to_csv(file_path, sep="\t", index=False) diff --git a/petab/v2/problem.py b/petab/v2/problem.py index bcf38768..c4fb241b 100644 --- a/petab/v2/problem.py +++ b/petab/v2/problem.py @@ -93,6 +93,12 @@ def __init__( ] = default_validation_tasks.copy() self.config = config + from .core import Observable, ObservablesTable + + self.observables: list[Observable] = ObservablesTable.from_dataframe( + self.observable_df + ) + def __str__(self): model = f"with model ({self.model})" if self.model else "without model" diff --git a/tests/v2/test_core.py b/tests/v2/test_core.py new file mode 100644 index 00000000..f5987f16 --- /dev/null +++ b/tests/v2/test_core.py @@ -0,0 +1,20 @@ +import tempfile +from pathlib import Path + +from petab.v2.core import ObservablesTable + + +def test_observables_table(): + file = ( + Path(__file__).parents[2] + / "doc/example/example_Fujita/Fujita_observables.tsv" + ) + + # read-write-read round trip + observables = ObservablesTable.from_tsv(file) + + with tempfile.TemporaryDirectory() as tmp_dir: + tmp_file = Path(tmp_dir) / "observables.tsv" + observables.to_tsv(tmp_file) + observables2 = ObservablesTable.from_tsv(tmp_file) + assert observables == observables2 From 40a0d7c741f7da808beef5e56479eec89c532231 Mon Sep 17 00:00:00 2001 From: Daniel Weindl Date: Mon, 16 Dec 2024 21:26:48 +0100 Subject: [PATCH 2/5] conditions --- petab/v2/core.py | 180 ++++++++++++++++++++++++++++++++++++++++++ petab/v2/petab1to2.py | 2 +- petab/v2/problem.py | 17 +++- tests/v2/test_core.py | 22 ++++-- 4 files changed, 212 insertions(+), 9 deletions(-) diff --git a/petab/v2/core.py b/petab/v2/core.py index cdf22f21..2839e643 100644 --- a/petab/v2/core.py +++ b/petab/v2/core.py @@ -20,17 +20,29 @@ class ObservableTransformation(str, Enum): + """Observable transformation types. + + Observable transformations as used in the PEtab observables table. + """ + LIN = C.LIN LOG = C.LOG LOG10 = C.LOG10 class NoiseDistribution(str, Enum): + """Noise distribution types. + + Noise distributions as used in the PEtab observables table. + """ + NORMAL = C.NORMAL LAPLACE = C.LAPLACE class Observable(BaseModel): + """Observable definition.""" + id: str = Field(alias=C.OBSERVABLE_ID) name: str | None = Field(alias=C.OBSERVABLE_NAME, default=None) formula: sp.Basic | None = Field(alias=C.OBSERVABLE_FORMULA, default=None) @@ -82,6 +94,8 @@ class Config: class ObservablesTable(BaseModel): + """PEtab observables table.""" + observables: list[Observable] @classmethod @@ -107,3 +121,169 @@ def from_tsv(cls, file_path: str | Path) -> ObservablesTable: def to_tsv(self, file_path: str | Path) -> None: df = self.to_dataframe() df.to_csv(file_path, sep="\t", index=False) + + +class OperationType(str, Enum): + """Operation types for model changes in the PEtab conditions table.""" + + # TODO update names + SET_CURRENT_VALUE = "setCurrentValue" + SET_RATE = "setRate" + SET_ASSIGNMENT = "setAssignment" + CONSTANT = "constant" + INITIAL = "initial" + ... + + +class Change(BaseModel): + """A change to the model or model state. + + A change to the model or model state, corresponding to an individual + row of the PEtab conditions table. + """ + + target_id: str = Field(alias=C.TARGET_ID) + operation_type: OperationType = Field(alias=C.VALUE_TYPE) + target_value: sp.Basic = Field(alias=C.TARGET_VALUE) + + class Config: + populate_by_name = True + arbitrary_types_allowed = True + use_enum_values = True + + @field_validator("target_id") + @classmethod + def validate_id(cls, v): + if not v: + raise ValueError("ID must not be empty.") + if not is_valid_identifier(v): + raise ValueError(f"Invalid ID: {v}") + return v + + @field_validator("target_value", mode="before") + @classmethod + def sympify(cls, v): + if v is None or isinstance(v, sp.Basic): + return v + if isinstance(v, float) and np.isnan(v): + return None + + return sympify_petab(v) + + +class ChangeSet(BaseModel): + """A set of changes to the model or model state. + + A set of simultaneously occuring changes to the model or model state, + corresponding to a perturbation of the underlying system. This corresponds + to all rows of the PEtab conditions table with the same condition ID. + """ + + id: str = Field(alias=C.CONDITION_ID) + changes: list[Change] + + class Config: + populate_by_name = True + + @field_validator("id") + @classmethod + def validate_id(cls, v): + if not v: + raise ValueError("ID must not be empty.") + if not is_valid_identifier(v): + raise ValueError(f"Invalid ID: {v}") + return v + + +class ConditionsTable(BaseModel): + """PEtab conditions table.""" + + conditions: list[ChangeSet] + + @classmethod + def from_dataframe(cls, df: pd.DataFrame) -> ConditionsTable: + if df is None: + return cls(conditions=[]) + + conditions = [] + for condition_id, sub_df in df.groupby(C.CONDITION_ID): + changes = [Change(**row.to_dict()) for _, row in sub_df.iterrows()] + conditions.append(ChangeSet(id=condition_id, changes=changes)) + + return cls(conditions=conditions) + + def to_dataframe(self) -> pd.DataFrame: + records = [ + {C.CONDITION_ID: condition.id, **change.model_dump()} + for condition in self.conditions + for change in condition.changes + ] + return pd.DataFrame(records) + + @classmethod + def from_tsv(cls, file_path: str | Path) -> ConditionsTable: + df = pd.read_csv(file_path, sep="\t") + return cls.from_dataframe(df) + + def to_tsv(self, file_path: str | Path) -> None: + df = self.to_dataframe() + df.to_csv(file_path, sep="\t", index=False) + + +class ExperimentPeriod(BaseModel): + """A period of a timecourse defined by a start time and a set changes. + + This corresponds to a row of the PEtab experiments table. + """ + + start: float = Field(alias=C.TIME) + conditions: list[ChangeSet] + + class Config: + populate_by_name = True + + +class Experiment(BaseModel): + """An experiment or a timecourse defined by an ID and a set of different + periods. + + Corresponds to a group of rows of the PEtab experiments table with the same + experiment ID. + """ + + id: str = Field(alias=C.EXPERIMENT_ID) + periods: list[ExperimentPeriod] + + class Config: + populate_by_name = True + arbitrary_types_allowed = True + + +class ExperimentsTable(BaseModel): + """PEtab experiments table.""" + + experiments: list[Experiment] + + @classmethod + def from_dataframe(cls, df: pd.DataFrame) -> ExperimentsTable: + if df is None: + return cls(experiments=[]) + + experiments = [ + Experiment(**row.to_dict()) + for _, row in df.reset_index().iterrows() + ] + + return cls(experiments=experiments) + + def to_dataframe(self) -> pd.DataFrame: + return pd.DataFrame(self.model_dump()["experiments"]) + + @classmethod + def from_tsv(cls, file_path: str | Path) -> ExperimentsTable: + df = pd.read_csv(file_path, sep="\t") + return cls.from_dataframe(df) + + def to_tsv(self, file_path: str | Path) -> None: + df = self.to_dataframe() + df.to_csv(file_path, sep="\t", index=False) diff --git a/petab/v2/petab1to2.py b/petab/v2/petab1to2.py index a5690882..25bde2b7 100644 --- a/petab/v2/petab1to2.py +++ b/petab/v2/petab1to2.py @@ -294,7 +294,7 @@ def v1v2_condition_df( id_vars=[v1.C.CONDITION_ID], var_name=v2.C.TARGET_ID, value_name=v2.C.TARGET_VALUE, - ) + ).dropna(subset=[v2.C.TARGET_VALUE]) if condition_df.empty: # This happens if there weren't any condition-specific changes diff --git a/petab/v2/problem.py b/petab/v2/problem.py index c4fb241b..ae90a93b 100644 --- a/petab/v2/problem.py +++ b/petab/v2/problem.py @@ -93,11 +93,22 @@ def __init__( ] = default_validation_tasks.copy() self.config = config - from .core import Observable, ObservablesTable + from .core import ( + ChangeSet, + ConditionsTable, + Observable, + ObservablesTable, + ) + + self.observables_table: ObservablesTable = ( + ObservablesTable.from_dataframe(self.observable_df) + ) + self.observables: list[Observable] = self.observables_table.observables - self.observables: list[Observable] = ObservablesTable.from_dataframe( - self.observable_df + self.conditions_table: ConditionsTable = ( + ConditionsTable.from_dataframe(self.condition_df) ) + self.conditions: list[ChangeSet] = self.conditions_table.conditions def __str__(self): model = f"with model ({self.model})" if self.model else "without model" diff --git a/tests/v2/test_core.py b/tests/v2/test_core.py index f5987f16..76933a1c 100644 --- a/tests/v2/test_core.py +++ b/tests/v2/test_core.py @@ -1,14 +1,14 @@ import tempfile from pathlib import Path -from petab.v2.core import ObservablesTable +from petab.v2.core import ConditionsTable, ObservablesTable +from petab.v2.petab1to2 import petab1to2 + +example_dir_fujita = Path(__file__).parents[2] / "doc/example/example_Fujita" def test_observables_table(): - file = ( - Path(__file__).parents[2] - / "doc/example/example_Fujita/Fujita_observables.tsv" - ) + file = example_dir_fujita / "Fujita_observables.tsv" # read-write-read round trip observables = ObservablesTable.from_tsv(file) @@ -18,3 +18,15 @@ def test_observables_table(): observables.to_tsv(tmp_file) observables2 = ObservablesTable.from_tsv(tmp_file) assert observables == observables2 + + +def test_conditions_table(): + with tempfile.TemporaryDirectory() as tmp_dir: + petab1to2(example_dir_fujita / "Fujita.yaml", tmp_dir) + file = Path(tmp_dir, "Fujita_experimentalCondition.tsv") + # read-write-read round trip + conditions = ConditionsTable.from_tsv(file) + tmp_file = Path(tmp_dir) / "conditions.tsv" + conditions.to_tsv(tmp_file) + conditions2 = ConditionsTable.from_tsv(tmp_file) + assert conditions == conditions2 From 8be0c04daf308026fe8449c033393f22f5f501f9 Mon Sep 17 00:00:00 2001 From: Daniel Weindl Date: Thu, 19 Dec 2024 14:39:03 +0100 Subject: [PATCH 3/5] getitem --- petab/v2/core.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/petab/v2/core.py b/petab/v2/core.py index 2839e643..fbdb3fcd 100644 --- a/petab/v2/core.py +++ b/petab/v2/core.py @@ -98,6 +98,13 @@ class ObservablesTable(BaseModel): observables: list[Observable] + def __getitem__(self, observable_id: str) -> Observable: + """Get an observable by ID.""" + for observable in self.observables: + if observable.id == observable_id: + return observable + raise KeyError(f"Observable ID {observable_id} not found") + @classmethod def from_dataframe(cls, df: pd.DataFrame) -> ObservablesTable: if df is None: @@ -200,6 +207,13 @@ class ConditionsTable(BaseModel): conditions: list[ChangeSet] + def __getitem__(self, condition_id: str) -> ChangeSet: + """Get a condition by ID.""" + for condition in self.conditions: + if condition.id == condition_id: + return condition + raise KeyError(f"Condition ID {condition_id} not found") + @classmethod def from_dataframe(cls, df: pd.DataFrame) -> ConditionsTable: if df is None: From 427088b817f4df4849f0db2f1d3d2eb801e5e481 Mon Sep 17 00:00:00 2001 From: Daniel Weindl Date: Thu, 19 Dec 2024 22:51:30 +0100 Subject: [PATCH 4/5] Measurements --- petab/v2/core.py | 120 ++++++++++++++++++++++++++++++++++++++++++-- petab/v2/problem.py | 20 ++++++++ 2 files changed, 135 insertions(+), 5 deletions(-) diff --git a/petab/v2/core.py b/petab/v2/core.py index fbdb3fcd..a8297324 100644 --- a/petab/v2/core.py +++ b/petab/v2/core.py @@ -279,14 +279,27 @@ class ExperimentsTable(BaseModel): experiments: list[Experiment] @classmethod - def from_dataframe(cls, df: pd.DataFrame) -> ExperimentsTable: + def from_dataframe( + cls, df: pd.DataFrame, conditions: ConditionsTable = None + ) -> ExperimentsTable: if df is None: return cls(experiments=[]) - experiments = [ - Experiment(**row.to_dict()) - for _, row in df.reset_index().iterrows() - ] + if conditions is None: + conditions = {} + + experiments = [] + for experiment_id, cur_exp_df in df.groupby(C.EXPERIMENT_ID): + periods = [] + for time, cur_period_df in cur_exp_df.groupby(C.TIME): + period_conditions = [ + conditions[row[C.CONDITION_ID]] + for _, row in cur_period_df.iterrows() + ] + periods.append( + ExperimentPeriod(start=time, conditions=period_conditions) + ) + experiments.append(Experiment(id=experiment_id, periods=periods)) return cls(experiments=experiments) @@ -301,3 +314,100 @@ def from_tsv(cls, file_path: str | Path) -> ExperimentsTable: def to_tsv(self, file_path: str | Path) -> None: df = self.to_dataframe() df.to_csv(file_path, sep="\t", index=False) + + +class Measurement(BaseModel): + """A measurement. + + A measurement of an observable at a specific time point in a specific + experiment. + """ + + # TODO: ID vs object + observable_id: str = Field(alias=C.OBSERVABLE_ID) + experiment_id: str | None = Field(alias=C.EXPERIMENT_ID, default=None) + time: float = Field(alias=C.TIME) + measurement: float = Field(alias=C.MEASUREMENT) + observable_parameters: list[sp.Basic] = Field( + alias=C.OBSERVABLE_PARAMETERS, default_factory=list + ) + noise_parameters: list[sp.Basic] = Field( + alias=C.NOISE_PARAMETERS, default_factory=list + ) + + class Config: + populate_by_name = True + arbitrary_types_allowed = True + + @field_validator( + "experiment_id", + "observable_parameters", + "noise_parameters", + mode="before", + ) + @classmethod + def convert_nan_to_none(cls, v, info: ValidationInfo): + if isinstance(v, float) and np.isnan(v): + return cls.model_fields[info.field_name].default + return v + + @field_validator("observable_id", "experiment_id") + @classmethod + def validate_id(cls, v, info: ValidationInfo): + if not v: + if info.field_name == "experiment_id": + return None + raise ValueError("ID must not be empty.") + if not is_valid_identifier(v): + raise ValueError(f"Invalid ID: {v}") + return v + + @field_validator( + "observable_parameters", "noise_parameters", mode="before" + ) + @classmethod + def sympify_list(cls, v): + if isinstance(v, float) and np.isnan(v): + return [] + if isinstance(v, str): + v = v.split(C.PARAMETER_SEPARATOR) + else: + v = [v] + return [sympify_petab(x) for x in v] + + +class MeasurementTable(BaseModel): + """PEtab measurement table.""" + + measurements: list[Measurement] + + @classmethod + def from_dataframe( + cls, + df: pd.DataFrame, + observables_table: ObservablesTable, + experiments_table: ExperimentsTable, + ) -> MeasurementTable: + if df is None: + return cls(measurements=[]) + + measurements = [ + Measurement( + **row.to_dict(), + ) + for _, row in df.reset_index().iterrows() + ] + + return cls(measurements=measurements) + + def to_dataframe(self) -> pd.DataFrame: + return pd.DataFrame(self.model_dump()["measurements"]) + + @classmethod + def from_tsv(cls, file_path: str | Path) -> MeasurementTable: + df = pd.read_csv(file_path, sep="\t") + return cls.from_dataframe(df) + + def to_tsv(self, file_path: str | Path) -> None: + df = self.to_dataframe() + df.to_csv(file_path, sep="\t", index=False) diff --git a/petab/v2/problem.py b/petab/v2/problem.py index ae90a93b..bf59cd20 100644 --- a/petab/v2/problem.py +++ b/petab/v2/problem.py @@ -96,6 +96,9 @@ def __init__( from .core import ( ChangeSet, ConditionsTable, + Experiment, + ExperimentsTable, + MeasurementTable, Observable, ObservablesTable, ) @@ -110,6 +113,23 @@ def __init__( ) self.conditions: list[ChangeSet] = self.conditions_table.conditions + self.experiments_table: ExperimentsTable = ( + ExperimentsTable.from_dataframe( + self.experiment_df, self.conditions_table + ) + ) + self.experiments: list[Experiment] = self.experiments_table.experiments + + self.measurement_table: MeasurementTable = ( + MeasurementTable.from_dataframe( + self.measurement_df, + observables_table=self.observables_table, + experiments_table=self.experiments_table, + ) + ) + + # TODO: measurements, parameters, visualization, mapping + def __str__(self): model = f"with model ({self.model})" if self.model else "without model" From fca15e47c32064a8344d1be1be4c43705c137abe Mon Sep 17 00:00:00 2001 From: Daniel Weindl Date: Thu, 19 Dec 2024 23:03:34 +0100 Subject: [PATCH 5/5] mapping,parameters --- petab/v2/core.py | 126 ++++++++++++++++++++++++++++++++++++++++++++ petab/v2/problem.py | 10 +++- 2 files changed, 135 insertions(+), 1 deletion(-) diff --git a/petab/v2/core.py b/petab/v2/core.py index a8297324..93dda5e9 100644 --- a/petab/v2/core.py +++ b/petab/v2/core.py @@ -30,6 +30,17 @@ class ObservableTransformation(str, Enum): LOG10 = C.LOG10 +class ParameterScale(str, Enum): + """Parameter scales. + + Parameter scales as used in the PEtab parameters table. + """ + + LIN = C.LIN + LOG = C.LOG + LOG10 = C.LOG10 + + class NoiseDistribution(str, Enum): """Noise distribution types. @@ -411,3 +422,118 @@ def from_tsv(cls, file_path: str | Path) -> MeasurementTable: def to_tsv(self, file_path: str | Path) -> None: df = self.to_dataframe() df.to_csv(file_path, sep="\t", index=False) + + +class Mapping(BaseModel): + """Mapping PEtab entities to model entities.""" + + petab_id: str = Field(alias=C.PETAB_ENTITY_ID) + model_id: str = Field(alias=C.MODEL_ENTITY_ID) + + class Config: + populate_by_name = True + + @field_validator( + "petab_id", + ) + @classmethod + def validate_id(cls, v): + if not v: + raise ValueError("ID must not be empty.") + if not is_valid_identifier(v): + raise ValueError(f"Invalid ID: {v}") + return v + + +class MappingTable(BaseModel): + """PEtab mapping table.""" + + mappings: list[Mapping] + + @classmethod + def from_dataframe(cls, df: pd.DataFrame) -> MappingTable: + if df is None: + return cls(mappings=[]) + + mappings = [ + Mapping(**row.to_dict()) for _, row in df.reset_index().iterrows() + ] + + return cls(mappings=mappings) + + def to_dataframe(self) -> pd.DataFrame: + return pd.DataFrame(self.model_dump()["mappings"]) + + @classmethod + def from_tsv(cls, file_path: str | Path) -> MappingTable: + df = pd.read_csv(file_path, sep="\t") + return cls.from_dataframe(df) + + def to_tsv(self, file_path: str | Path) -> None: + df = self.to_dataframe() + df.to_csv(file_path, sep="\t", index=False) + + +class Parameter(BaseModel): + """Parameter definition.""" + + id: str = Field(alias=C.PARAMETER_ID) + lb: float | None = Field(alias=C.LOWER_BOUND, default=None) + ub: float | None = Field(alias=C.UPPER_BOUND, default=None) + nominal_value: float | None = Field(alias=C.NOMINAL_VALUE, default=None) + scale: ParameterScale = Field( + alias=C.PARAMETER_SCALE, default=ParameterScale.LIN + ) + estimate: bool = Field(alias=C.ESTIMATE, default=True) + # TODO priors + + class Config: + populate_by_name = True + arbitrary_types_allowed = True + use_enum_values = True + + @field_validator("id") + @classmethod + def validate_id(cls, v): + if not v: + raise ValueError("ID must not be empty.") + if not is_valid_identifier(v): + raise ValueError(f"Invalid ID: {v}") + return v + + @field_validator("lb", "ub", "nominal_value") + @classmethod + def convert_nan_to_none(cls, v): + if isinstance(v, float) and np.isnan(v): + return None + return v + + +class ParameterTable(BaseModel): + """PEtab parameter table.""" + + parameters: list[Parameter] + + @classmethod + def from_dataframe(cls, df: pd.DataFrame) -> ParameterTable: + if df is None: + return cls(parameters=[]) + + parameters = [ + Parameter(**row.to_dict()) + for _, row in df.reset_index().iterrows() + ] + + return cls(parameters=parameters) + + def to_dataframe(self) -> pd.DataFrame: + return pd.DataFrame(self.model_dump()["parameters"]) + + @classmethod + def from_tsv(cls, file_path: str | Path) -> ParameterTable: + df = pd.read_csv(file_path, sep="\t") + return cls.from_dataframe(df) + + def to_tsv(self, file_path: str | Path) -> None: + df = self.to_dataframe() + df.to_csv(file_path, sep="\t", index=False) diff --git a/petab/v2/problem.py b/petab/v2/problem.py index bf59cd20..626aeef3 100644 --- a/petab/v2/problem.py +++ b/petab/v2/problem.py @@ -98,9 +98,11 @@ def __init__( ConditionsTable, Experiment, ExperimentsTable, + MappingTable, MeasurementTable, Observable, ObservablesTable, + ParameterTable, ) self.observables_table: ObservablesTable = ( @@ -128,7 +130,13 @@ def __init__( ) ) - # TODO: measurements, parameters, visualization, mapping + self.mapping_table: MappingTable = MappingTable.from_dataframe( + self.mapping_df + ) + self.parameter_table: ParameterTable = ParameterTable.from_dataframe( + self.parameter_df + ) + # TODO: visualization table def __str__(self): model = f"with model ({self.model})" if self.model else "without model"