Skip to content

Commit 942e277

Browse files
committed
Measurements
1 parent 653cafd commit 942e277

File tree

2 files changed

+135
-5
lines changed

2 files changed

+135
-5
lines changed

petab/v2/core.py

Lines changed: 115 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -279,14 +279,27 @@ class ExperimentsTable(BaseModel):
279279
experiments: list[Experiment]
280280

281281
@classmethod
282-
def from_dataframe(cls, df: pd.DataFrame) -> ExperimentsTable:
282+
def from_dataframe(
283+
cls, df: pd.DataFrame, conditions: ConditionsTable = None
284+
) -> ExperimentsTable:
283285
if df is None:
284286
return cls(experiments=[])
285287

286-
experiments = [
287-
Experiment(**row.to_dict())
288-
for _, row in df.reset_index().iterrows()
289-
]
288+
if conditions is None:
289+
conditions = {}
290+
291+
experiments = []
292+
for experiment_id, cur_exp_df in df.groupby(C.EXPERIMENT_ID):
293+
periods = []
294+
for time, cur_period_df in cur_exp_df.groupby(C.TIME):
295+
period_conditions = [
296+
conditions[row[C.CONDITION_ID]]
297+
for _, row in cur_period_df.iterrows()
298+
]
299+
periods.append(
300+
ExperimentPeriod(start=time, conditions=period_conditions)
301+
)
302+
experiments.append(Experiment(id=experiment_id, periods=periods))
290303

291304
return cls(experiments=experiments)
292305

@@ -301,3 +314,100 @@ def from_tsv(cls, file_path: str | Path) -> ExperimentsTable:
301314
def to_tsv(self, file_path: str | Path) -> None:
302315
df = self.to_dataframe()
303316
df.to_csv(file_path, sep="\t", index=False)
317+
318+
319+
class Measurement(BaseModel):
320+
"""A measurement.
321+
322+
A measurement of an observable at a specific time point in a specific
323+
experiment.
324+
"""
325+
326+
# TODO: ID vs object
327+
observable_id: str = Field(alias=C.OBSERVABLE_ID)
328+
experiment_id: str | None = Field(alias=C.EXPERIMENT_ID, default=None)
329+
time: float = Field(alias=C.TIME)
330+
measurement: float = Field(alias=C.MEASUREMENT)
331+
observable_parameters: list[sp.Basic] = Field(
332+
alias=C.OBSERVABLE_PARAMETERS, default_factory=list
333+
)
334+
noise_parameters: list[sp.Basic] = Field(
335+
alias=C.NOISE_PARAMETERS, default_factory=list
336+
)
337+
338+
class Config:
339+
populate_by_name = True
340+
arbitrary_types_allowed = True
341+
342+
@field_validator(
343+
"experiment_id",
344+
"observable_parameters",
345+
"noise_parameters",
346+
mode="before",
347+
)
348+
@classmethod
349+
def convert_nan_to_none(cls, v, info: ValidationInfo):
350+
if isinstance(v, float) and np.isnan(v):
351+
return cls.model_fields[info.field_name].default
352+
return v
353+
354+
@field_validator("observable_id", "experiment_id")
355+
@classmethod
356+
def validate_id(cls, v, info: ValidationInfo):
357+
if not v:
358+
if info.field_name == "experiment_id":
359+
return None
360+
raise ValueError("ID must not be empty.")
361+
if not is_valid_identifier(v):
362+
raise ValueError(f"Invalid ID: {v}")
363+
return v
364+
365+
@field_validator(
366+
"observable_parameters", "noise_parameters", mode="before"
367+
)
368+
@classmethod
369+
def sympify_list(cls, v):
370+
if isinstance(v, float) and np.isnan(v):
371+
return []
372+
if isinstance(v, str):
373+
v = v.split(C.PARAMETER_SEPARATOR)
374+
else:
375+
v = [v]
376+
return [sympify_petab(x) for x in v]
377+
378+
379+
class MeasurementTable(BaseModel):
380+
"""PEtab measurement table."""
381+
382+
measurements: list[Measurement]
383+
384+
@classmethod
385+
def from_dataframe(
386+
cls,
387+
df: pd.DataFrame,
388+
observables_table: ObservablesTable,
389+
experiments_table: ExperimentsTable,
390+
) -> MeasurementTable:
391+
if df is None:
392+
return cls(measurements=[])
393+
394+
measurements = [
395+
Measurement(
396+
**row.to_dict(),
397+
)
398+
for _, row in df.reset_index().iterrows()
399+
]
400+
401+
return cls(measurements=measurements)
402+
403+
def to_dataframe(self) -> pd.DataFrame:
404+
return pd.DataFrame(self.model_dump()["measurements"])
405+
406+
@classmethod
407+
def from_tsv(cls, file_path: str | Path) -> MeasurementTable:
408+
df = pd.read_csv(file_path, sep="\t")
409+
return cls.from_dataframe(df)
410+
411+
def to_tsv(self, file_path: str | Path) -> None:
412+
df = self.to_dataframe()
413+
df.to_csv(file_path, sep="\t", index=False)

petab/v2/problem.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ def __init__(
9696
from .core import (
9797
ChangeSet,
9898
ConditionsTable,
99+
Experiment,
100+
ExperimentsTable,
101+
MeasurementTable,
99102
Observable,
100103
ObservablesTable,
101104
)
@@ -110,6 +113,23 @@ def __init__(
110113
)
111114
self.conditions: list[ChangeSet] = self.conditions_table.conditions
112115

116+
self.experiments_table: ExperimentsTable = (
117+
ExperimentsTable.from_dataframe(
118+
self.experiment_df, self.conditions_table
119+
)
120+
)
121+
self.experiments: list[Experiment] = self.experiments_table.experiments
122+
123+
self.measurement_table: MeasurementTable = (
124+
MeasurementTable.from_dataframe(
125+
self.measurement_df,
126+
observables_table=self.observables_table,
127+
experiments_table=self.experiments_table,
128+
)
129+
)
130+
131+
# TODO: measurements, parameters, visualization, mapping
132+
113133
def __str__(self):
114134
model = f"with model ({self.model})" if self.model else "without model"
115135

0 commit comments

Comments
 (0)