Skip to content

Commit cb78310

Browse files
committed
conditions
1 parent 421b0c5 commit cb78310

File tree

4 files changed

+212
-9
lines changed

4 files changed

+212
-9
lines changed

petab/v2/core.py

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,29 @@
2020

2121

2222
class ObservableTransformation(str, Enum):
23+
"""Observable transformation types.
24+
25+
Observable transformations as used in the PEtab observables table.
26+
"""
27+
2328
LIN = C.LIN
2429
LOG = C.LOG
2530
LOG10 = C.LOG10
2631

2732

2833
class NoiseDistribution(str, Enum):
34+
"""Noise distribution types.
35+
36+
Noise distributions as used in the PEtab observables table.
37+
"""
38+
2939
NORMAL = C.NORMAL
3040
LAPLACE = C.LAPLACE
3141

3242

3343
class Observable(BaseModel):
44+
"""Observable definition."""
45+
3446
id: str = Field(alias=C.OBSERVABLE_ID)
3547
name: str | None = Field(alias=C.OBSERVABLE_NAME, default=None)
3648
formula: sp.Basic | None = Field(alias=C.OBSERVABLE_FORMULA, default=None)
@@ -82,6 +94,8 @@ class Config:
8294

8395

8496
class ObservablesTable(BaseModel):
97+
"""PEtab observables table."""
98+
8599
observables: list[Observable]
86100

87101
@classmethod
@@ -107,3 +121,169 @@ def from_tsv(cls, file_path: str | Path) -> ObservablesTable:
107121
def to_tsv(self, file_path: str | Path) -> None:
108122
df = self.to_dataframe()
109123
df.to_csv(file_path, sep="\t", index=False)
124+
125+
126+
class OperationType(str, Enum):
127+
"""Operation types for model changes in the PEtab conditions table."""
128+
129+
# TODO update names
130+
SET_CURRENT_VALUE = "setCurrentValue"
131+
SET_RATE = "setRate"
132+
SET_ASSIGNMENT = "setAssignment"
133+
CONSTANT = "constant"
134+
INITIAL = "initial"
135+
...
136+
137+
138+
class Change(BaseModel):
139+
"""A change to the model or model state.
140+
141+
A change to the model or model state, corresponding to an individual
142+
row of the PEtab conditions table.
143+
"""
144+
145+
target_id: str = Field(alias=C.TARGET_ID)
146+
operation_type: OperationType = Field(alias=C.VALUE_TYPE)
147+
target_value: sp.Basic = Field(alias=C.TARGET_VALUE)
148+
149+
class Config:
150+
populate_by_name = True
151+
arbitrary_types_allowed = True
152+
use_enum_values = True
153+
154+
@field_validator("target_id")
155+
@classmethod
156+
def validate_id(cls, v):
157+
if not v:
158+
raise ValueError("ID must not be empty.")
159+
if not is_valid_identifier(v):
160+
raise ValueError(f"Invalid ID: {v}")
161+
return v
162+
163+
@field_validator("target_value", mode="before")
164+
@classmethod
165+
def sympify(cls, v):
166+
if v is None or isinstance(v, sp.Basic):
167+
return v
168+
if isinstance(v, float) and np.isnan(v):
169+
return None
170+
171+
return sympify_petab(v)
172+
173+
174+
class ChangeSet(BaseModel):
175+
"""A set of changes to the model or model state.
176+
177+
A set of simultaneously occuring changes to the model or model state,
178+
corresponding to a perturbation of the underlying system. This corresponds
179+
to all rows of the PEtab conditions table with the same condition ID.
180+
"""
181+
182+
id: str = Field(alias=C.CONDITION_ID)
183+
changes: list[Change]
184+
185+
class Config:
186+
populate_by_name = True
187+
188+
@field_validator("id")
189+
@classmethod
190+
def validate_id(cls, v):
191+
if not v:
192+
raise ValueError("ID must not be empty.")
193+
if not is_valid_identifier(v):
194+
raise ValueError(f"Invalid ID: {v}")
195+
return v
196+
197+
198+
class ConditionsTable(BaseModel):
199+
"""PEtab conditions table."""
200+
201+
conditions: list[ChangeSet]
202+
203+
@classmethod
204+
def from_dataframe(cls, df: pd.DataFrame) -> ConditionsTable:
205+
if df is None:
206+
return cls(conditions=[])
207+
208+
conditions = []
209+
for condition_id, sub_df in df.groupby(C.CONDITION_ID):
210+
changes = [Change(**row.to_dict()) for _, row in sub_df.iterrows()]
211+
conditions.append(ChangeSet(id=condition_id, changes=changes))
212+
213+
return cls(conditions=conditions)
214+
215+
def to_dataframe(self) -> pd.DataFrame:
216+
records = [
217+
{C.CONDITION_ID: condition.id, **change.model_dump()}
218+
for condition in self.conditions
219+
for change in condition.changes
220+
]
221+
return pd.DataFrame(records)
222+
223+
@classmethod
224+
def from_tsv(cls, file_path: str | Path) -> ConditionsTable:
225+
df = pd.read_csv(file_path, sep="\t")
226+
return cls.from_dataframe(df)
227+
228+
def to_tsv(self, file_path: str | Path) -> None:
229+
df = self.to_dataframe()
230+
df.to_csv(file_path, sep="\t", index=False)
231+
232+
233+
class ExperimentPeriod(BaseModel):
234+
"""A period of a timecourse defined by a start time and a set changes.
235+
236+
This corresponds to a row of the PEtab experiments table.
237+
"""
238+
239+
start: float = Field(alias=C.TIME)
240+
conditions: list[ChangeSet]
241+
242+
class Config:
243+
populate_by_name = True
244+
245+
246+
class Experiment(BaseModel):
247+
"""An experiment or a timecourse defined by an ID and a set of different
248+
periods.
249+
250+
Corresponds to a group of rows of the PEtab experiments table with the same
251+
experiment ID.
252+
"""
253+
254+
id: str = Field(alias=C.EXPERIMENT_ID)
255+
periods: list[ExperimentPeriod]
256+
257+
class Config:
258+
populate_by_name = True
259+
arbitrary_types_allowed = True
260+
261+
262+
class ExperimentsTable(BaseModel):
263+
"""PEtab experiments table."""
264+
265+
experiments: list[Experiment]
266+
267+
@classmethod
268+
def from_dataframe(cls, df: pd.DataFrame) -> ExperimentsTable:
269+
if df is None:
270+
return cls(experiments=[])
271+
272+
experiments = [
273+
Experiment(**row.to_dict())
274+
for _, row in df.reset_index().iterrows()
275+
]
276+
277+
return cls(experiments=experiments)
278+
279+
def to_dataframe(self) -> pd.DataFrame:
280+
return pd.DataFrame(self.model_dump()["experiments"])
281+
282+
@classmethod
283+
def from_tsv(cls, file_path: str | Path) -> ExperimentsTable:
284+
df = pd.read_csv(file_path, sep="\t")
285+
return cls.from_dataframe(df)
286+
287+
def to_tsv(self, file_path: str | Path) -> None:
288+
df = self.to_dataframe()
289+
df.to_csv(file_path, sep="\t", index=False)

petab/v2/petab1to2.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ def v1v2_condition_df(
280280
id_vars=[v1.C.CONDITION_ID],
281281
var_name=v2.C.TARGET_ID,
282282
value_name=v2.C.TARGET_VALUE,
283-
)
283+
).dropna(subset=[v2.C.TARGET_VALUE])
284284

285285
if condition_df.empty:
286286
# This happens if there weren't any condition-specific changes

petab/v2/problem.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,11 +93,22 @@ def __init__(
9393
] = default_validation_tasks.copy()
9494
self.config = config
9595

96-
from .core import Observable, ObservablesTable
96+
from .core import (
97+
ChangeSet,
98+
ConditionsTable,
99+
Observable,
100+
ObservablesTable,
101+
)
102+
103+
self.observables_table: ObservablesTable = (
104+
ObservablesTable.from_dataframe(self.observable_df)
105+
)
106+
self.observables: list[Observable] = self.observables_table.observables
97107

98-
self.observables: list[Observable] = ObservablesTable.from_dataframe(
99-
self.observable_df
108+
self.conditions_table: ConditionsTable = (
109+
ConditionsTable.from_dataframe(self.condition_df)
100110
)
111+
self.conditions: list[ChangeSet] = self.conditions_table.conditions
101112

102113
def __str__(self):
103114
model = f"with model ({self.model})" if self.model else "without model"

tests/v2/test_core.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import tempfile
22
from pathlib import Path
33

4-
from petab.v2.core import ObservablesTable
4+
from petab.v2.core import ConditionsTable, ObservablesTable
5+
from petab.v2.petab1to2 import petab1to2
6+
7+
example_dir_fujita = Path(__file__).parents[2] / "doc/example/example_Fujita"
58

69

710
def test_observables_table():
8-
file = (
9-
Path(__file__).parents[2]
10-
/ "doc/example/example_Fujita/Fujita_observables.tsv"
11-
)
11+
file = example_dir_fujita / "Fujita_observables.tsv"
1212

1313
# read-write-read round trip
1414
observables = ObservablesTable.from_tsv(file)
@@ -18,3 +18,15 @@ def test_observables_table():
1818
observables.to_tsv(tmp_file)
1919
observables2 = ObservablesTable.from_tsv(tmp_file)
2020
assert observables == observables2
21+
22+
23+
def test_conditions_table():
24+
with tempfile.TemporaryDirectory() as tmp_dir:
25+
petab1to2(example_dir_fujita / "Fujita.yaml", tmp_dir)
26+
file = Path(tmp_dir, "Fujita_experimentalCondition.tsv")
27+
# read-write-read round trip
28+
conditions = ConditionsTable.from_tsv(file)
29+
tmp_file = Path(tmp_dir) / "conditions.tsv"
30+
conditions.to_tsv(tmp_file)
31+
conditions2 = ConditionsTable.from_tsv(tmp_file)
32+
assert conditions == conditions2

0 commit comments

Comments
 (0)