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