@@ -30,6 +30,17 @@ class ObservableTransformation(str, Enum):
3030 LOG10 = C .LOG10
3131
3232
33+ class ParameterScale (str , Enum ):
34+ """Parameter scales.
35+
36+ Parameter scales as used in the PEtab parameters table.
37+ """
38+
39+ LIN = C .LIN
40+ LOG = C .LOG
41+ LOG10 = C .LOG10
42+
43+
3344class NoiseDistribution (str , Enum ):
3445 """Noise distribution types.
3546
@@ -411,3 +422,118 @@ def from_tsv(cls, file_path: str | Path) -> MeasurementTable:
411422 def to_tsv (self , file_path : str | Path ) -> None :
412423 df = self .to_dataframe ()
413424 df .to_csv (file_path , sep = "\t " , index = False )
425+
426+
427+ class Mapping (BaseModel ):
428+ """Mapping PEtab entities to model entities."""
429+
430+ petab_id : str = Field (alias = C .PETAB_ENTITY_ID )
431+ model_id : str = Field (alias = C .MODEL_ENTITY_ID )
432+
433+ class Config :
434+ populate_by_name = True
435+
436+ @field_validator (
437+ "petab_id" ,
438+ )
439+ @classmethod
440+ def validate_id (cls , v ):
441+ if not v :
442+ raise ValueError ("ID must not be empty." )
443+ if not is_valid_identifier (v ):
444+ raise ValueError (f"Invalid ID: { v } " )
445+ return v
446+
447+
448+ class MappingTable (BaseModel ):
449+ """PEtab mapping table."""
450+
451+ mappings : list [Mapping ]
452+
453+ @classmethod
454+ def from_dataframe (cls , df : pd .DataFrame ) -> MappingTable :
455+ if df is None :
456+ return cls (mappings = [])
457+
458+ mappings = [
459+ Mapping (** row .to_dict ()) for _ , row in df .reset_index ().iterrows ()
460+ ]
461+
462+ return cls (mappings = mappings )
463+
464+ def to_dataframe (self ) -> pd .DataFrame :
465+ return pd .DataFrame (self .model_dump ()["mappings" ])
466+
467+ @classmethod
468+ def from_tsv (cls , file_path : str | Path ) -> MappingTable :
469+ df = pd .read_csv (file_path , sep = "\t " )
470+ return cls .from_dataframe (df )
471+
472+ def to_tsv (self , file_path : str | Path ) -> None :
473+ df = self .to_dataframe ()
474+ df .to_csv (file_path , sep = "\t " , index = False )
475+
476+
477+ class Parameter (BaseModel ):
478+ """Parameter definition."""
479+
480+ id : str = Field (alias = C .PARAMETER_ID )
481+ lb : float | None = Field (alias = C .LOWER_BOUND , default = None )
482+ ub : float | None = Field (alias = C .UPPER_BOUND , default = None )
483+ nominal_value : float | None = Field (alias = C .NOMINAL_VALUE , default = None )
484+ scale : ParameterScale = Field (
485+ alias = C .PARAMETER_SCALE , default = ParameterScale .LIN
486+ )
487+ estimate : bool = Field (alias = C .ESTIMATE , default = True )
488+ # TODO priors
489+
490+ class Config :
491+ populate_by_name = True
492+ arbitrary_types_allowed = True
493+ use_enum_values = True
494+
495+ @field_validator ("id" )
496+ @classmethod
497+ def validate_id (cls , v ):
498+ if not v :
499+ raise ValueError ("ID must not be empty." )
500+ if not is_valid_identifier (v ):
501+ raise ValueError (f"Invalid ID: { v } " )
502+ return v
503+
504+ @field_validator ("lb" , "ub" , "nominal_value" )
505+ @classmethod
506+ def convert_nan_to_none (cls , v ):
507+ if isinstance (v , float ) and np .isnan (v ):
508+ return None
509+ return v
510+
511+
512+ class ParameterTable (BaseModel ):
513+ """PEtab parameter table."""
514+
515+ parameters : list [Parameter ]
516+
517+ @classmethod
518+ def from_dataframe (cls , df : pd .DataFrame ) -> ParameterTable :
519+ if df is None :
520+ return cls (parameters = [])
521+
522+ parameters = [
523+ Parameter (** row .to_dict ())
524+ for _ , row in df .reset_index ().iterrows ()
525+ ]
526+
527+ return cls (parameters = parameters )
528+
529+ def to_dataframe (self ) -> pd .DataFrame :
530+ return pd .DataFrame (self .model_dump ()["parameters" ])
531+
532+ @classmethod
533+ def from_tsv (cls , file_path : str | Path ) -> ParameterTable :
534+ df = pd .read_csv (file_path , sep = "\t " )
535+ return cls .from_dataframe (df )
536+
537+ def to_tsv (self , file_path : str | Path ) -> None :
538+ df = self .to_dataframe ()
539+ df .to_csv (file_path , sep = "\t " , index = False )
0 commit comments