@@ -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 )
0 commit comments