Skip to content

Commit f64ea80

Browse files
committed
conditions
1 parent 38d2f21 commit f64ea80

File tree

4 files changed

+171
-9
lines changed

4 files changed

+171
-9
lines changed

petab/v2/core.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,140 @@ def from_tsv(cls, file_path: str | Path) -> ObservablesTable:
107107
def to_tsv(self, file_path: str | Path) -> None:
108108
df = self.to_dataframe()
109109
df.to_csv(file_path, sep="\t", index=False)
110+
111+
112+
class OperationType(str, Enum):
113+
# TODO update names
114+
SET_CURRENT_VALUE = "setCurrentValue"
115+
SET_RATE = "setRate"
116+
SET_ASSIGNMENT = "setAssignment"
117+
CONSTANT = "constant"
118+
INITIAL = "initial"
119+
...
120+
121+
122+
class Change(BaseModel):
123+
target_id: str = Field(alias=C.TARGET_ID)
124+
operation_type: OperationType = Field(alias=C.VALUE_TYPE)
125+
target_value: sp.Basic = Field(alias=C.TARGET_VALUE)
126+
127+
class Config:
128+
populate_by_name = True
129+
arbitrary_types_allowed = True
130+
use_enum_values = True
131+
132+
@field_validator("target_id")
133+
@classmethod
134+
def validate_id(cls, v):
135+
if not v:
136+
raise ValueError("ID must not be empty.")
137+
if not is_valid_identifier(v):
138+
raise ValueError(f"Invalid ID: {v}")
139+
return v
140+
141+
@field_validator("target_value", mode="before")
142+
@classmethod
143+
def sympify(cls, v):
144+
if v is None or isinstance(v, sp.Basic):
145+
return v
146+
if isinstance(v, float) and np.isnan(v):
147+
return None
148+
149+
return sympify_petab(v)
150+
151+
152+
class ExperimentalCondition(BaseModel):
153+
id: str = Field(alias=C.CONDITION_ID)
154+
changes: list[Change]
155+
156+
class Config:
157+
populate_by_name = True
158+
159+
@field_validator("id")
160+
@classmethod
161+
def validate_id(cls, v):
162+
if not v:
163+
raise ValueError("ID must not be empty.")
164+
if not is_valid_identifier(v):
165+
raise ValueError(f"Invalid ID: {v}")
166+
return v
167+
168+
169+
class ConditionsTable(BaseModel):
170+
conditions: list[ExperimentalCondition]
171+
172+
@classmethod
173+
def from_dataframe(cls, df: pd.DataFrame) -> ConditionsTable:
174+
if df is None:
175+
return cls(conditions=[])
176+
177+
conditions = []
178+
for condition_id, sub_df in df.groupby(C.CONDITION_ID):
179+
changes = [Change(**row.to_dict()) for _, row in sub_df.iterrows()]
180+
conditions.append(
181+
ExperimentalCondition(id=condition_id, changes=changes)
182+
)
183+
184+
return cls(conditions=conditions)
185+
186+
def to_dataframe(self) -> pd.DataFrame:
187+
records = [
188+
{C.CONDITION_ID: condition.id, **change.model_dump()}
189+
for condition in self.conditions
190+
for change in condition.changes
191+
]
192+
return pd.DataFrame(records)
193+
194+
@classmethod
195+
def from_tsv(cls, file_path: str | Path) -> ConditionsTable:
196+
df = pd.read_csv(file_path, sep="\t")
197+
return cls.from_dataframe(df)
198+
199+
def to_tsv(self, file_path: str | Path) -> None:
200+
df = self.to_dataframe()
201+
df.to_csv(file_path, sep="\t", index=False)
202+
203+
204+
class ExperimentPeriod(BaseModel):
205+
start: float = Field(alias=C.TIME)
206+
conditions: list[ExperimentalCondition]
207+
208+
class Config:
209+
populate_by_name = True
210+
211+
212+
class Experiment(BaseModel):
213+
id: str = Field(alias=C.EXPERIMENT_ID)
214+
periods: list[ExperimentPeriod]
215+
216+
class Config:
217+
populate_by_name = True
218+
arbitrary_types_allowed = True
219+
220+
221+
class ExperimentsTable(BaseModel):
222+
experiments: list[Experiment]
223+
224+
@classmethod
225+
def from_dataframe(cls, df: pd.DataFrame) -> ExperimentsTable:
226+
if df is None:
227+
return cls(experiments=[])
228+
229+
experiments = [
230+
Experiment(**row.to_dict())
231+
for _, row in df.reset_index().iterrows()
232+
]
233+
234+
return cls(experiments=experiments)
235+
236+
def to_dataframe(self) -> pd.DataFrame:
237+
return pd.DataFrame(self.model_dump()["experiments"])
238+
239+
@classmethod
240+
def from_tsv(cls, file_path: str | Path) -> ExperimentsTable:
241+
df = pd.read_csv(file_path, sep="\t")
242+
return cls.from_dataframe(df)
243+
244+
def to_tsv(self, file_path: str | Path) -> None:
245+
df = self.to_dataframe()
246+
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: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,11 +92,24 @@ def __init__(
9292
] = default_validation_tasks.copy()
9393
self.config = config
9494

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

97-
self.observables: list[Observable] = ObservablesTable.from_dataframe(
98-
self.observable_df
107+
self.conditions_table: ConditionsTable = (
108+
ConditionsTable.from_dataframe(self.condition_df)
99109
)
110+
self.conditions: list[
111+
ExperimentalCondition
112+
] = self.conditions_table.conditions
100113

101114
def __str__(self):
102115
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)