22
33from __future__ import annotations
44
5+ from collections .abc import Sequence
56from enum import Enum
67from pathlib import Path
8+ from typing import Annotated
79
810import numpy as np
911import pandas as pd
1012import sympy as sp
1113from pydantic import (
14+ AfterValidator ,
1215 BaseModel ,
1316 ConfigDict ,
1417 Field ,
2932 "Change" ,
3033 "Condition" ,
3134 "ConditionsTable" ,
32- "OperationType" ,
3335 "ExperimentPeriod" ,
3436 "Experiment" ,
3537 "ExperimentsTable" ,
4345]
4446
4547
48+ def is_finite_or_neg_inf (v : float , info : ValidationInfo ) -> float :
49+ if not np .isfinite (v ) and v != - np .inf :
50+ raise ValueError (
51+ f"{ info .field_name } value must be finite or -inf but got { v } "
52+ )
53+ return v
54+
55+
4656class ObservableTransformation (str , Enum ):
4757 """Observable transformation types.
4858
@@ -248,16 +258,6 @@ def __iadd__(self, other: Observable) -> ObservablesTable:
248258 return self
249259
250260
251- # TODO remove?!
252- class OperationType (str , Enum ):
253- """Operation types for model changes in the PEtab conditions table."""
254-
255- # TODO update names
256- SET_CURRENT_VALUE = "setCurrentValue"
257- NO_CHANGE = "noChange"
258- ...
259-
260-
261261class Change (BaseModel ):
262262 """A change to the model or model state.
263263
@@ -266,17 +266,13 @@ class Change(BaseModel):
266266
267267 >>> Change(
268268 ... target_id="k1",
269- ... operation_type=OperationType.SET_CURRENT_VALUE,
270269 ... target_value="10",
271270 ... ) # doctest: +NORMALIZE_WHITESPACE
272- Change(target_id='k1', operation_type='setCurrentValue',
273- target_value=10.0000000000000)
271+ Change(target_id='k1', target_value=10.0000000000000)
274272 """
275273
276274 #: The ID of the target entity to change.
277275 target_id : str | None = Field (alias = C .TARGET_ID , default = None )
278- # TODO: remove?!
279- operation_type : OperationType = Field (alias = C .OPERATION_TYPE )
280276 #: The value to set the target entity to.
281277 target_value : sp .Basic | None = Field (alias = C .TARGET_VALUE , default = None )
282278
@@ -290,14 +286,11 @@ class Change(BaseModel):
290286 @model_validator (mode = "before" )
291287 @classmethod
292288 def _validate_id (cls , data : dict ):
293- if (
294- data .get ("operation_type" , data .get (C .OPERATION_TYPE ))
295- != C .OT_NO_CHANGE
296- ):
297- target_id = data .get ("target_id" , data .get (C .TARGET_ID ))
298-
299- if not is_valid_identifier (target_id ):
300- raise ValueError (f"Invalid ID: { target_id } " )
289+ target_id = data .get ("target_id" , data .get (C .TARGET_ID ))
290+
291+ if not is_valid_identifier (target_id ):
292+ raise ValueError (f"Invalid ID: { target_id } " )
293+
301294 return data
302295
303296 @field_validator ("target_value" , mode = "before" )
@@ -323,13 +316,12 @@ class Condition(BaseModel):
323316 ... changes=[
324317 ... Change(
325318 ... target_id="k1",
326- ... operation_type=OperationType.SET_CURRENT_VALUE,
327319 ... target_value="10",
328320 ... )
329321 ... ],
330322 ... ) # doctest: +NORMALIZE_WHITESPACE
331- Condition(id='condition1', changes=[Change(target_id='k1',
332- operation_type='setCurrentValue ', target_value=10.0000000000000)])
323+ Condition(id='condition1',
324+ changes=[Change(target_id='k1 ', target_value=10.0000000000000)])
333325 """
334326
335327 #: The condition ID.
@@ -352,13 +344,13 @@ def _validate_id(cls, v):
352344 def __add__ (self , other : Change ) -> Condition :
353345 """Add a change to the set."""
354346 if not isinstance (other , Change ):
355- raise TypeError ("Can only add Change to ChangeSet " )
347+ raise TypeError ("Can only add Change to Condition " )
356348 return Condition (id = self .id , changes = self .changes + [other ])
357349
358350 def __iadd__ (self , other : Change ) -> Condition :
359351 """Add a change to the set in place."""
360352 if not isinstance (other , Change ):
361- raise TypeError ("Can only add Change to ChangeSet " )
353+ raise TypeError ("Can only add Change to Condition " )
362354 self .changes .append (other )
363355 return self
364356
@@ -379,11 +371,11 @@ def __getitem__(self, condition_id: str) -> Condition:
379371 @classmethod
380372 def from_df (cls , df : pd .DataFrame ) -> ConditionsTable :
381373 """Create a ConditionsTable from a DataFrame."""
382- if df is None :
374+ if df is None or df . empty :
383375 return cls (conditions = [])
384376
385377 conditions = []
386- for condition_id , sub_df in df .groupby (C .CONDITION_ID ):
378+ for condition_id , sub_df in df .reset_index (). groupby (C .CONDITION_ID ):
387379 changes = [Change (** row .to_dict ()) for _ , row in sub_df .iterrows ()]
388380 conditions .append (Condition (id = condition_id , changes = changes ))
389381
@@ -422,13 +414,13 @@ def to_tsv(self, file_path: str | Path) -> None:
422414 def __add__ (self , other : Condition ) -> ConditionsTable :
423415 """Add a condition to the table."""
424416 if not isinstance (other , Condition ):
425- raise TypeError ("Can only add ChangeSet to ConditionsTable" )
417+ raise TypeError ("Can only add Conditions to ConditionsTable" )
426418 return ConditionsTable (conditions = self .conditions + [other ])
427419
428420 def __iadd__ (self , other : Condition ) -> ConditionsTable :
429421 """Add a condition to the table in place."""
430422 if not isinstance (other , Condition ):
431- raise TypeError ("Can only add ChangeSet to ConditionsTable" )
423+ raise TypeError ("Can only add Conditions to ConditionsTable" )
432424 self .conditions .append (other )
433425 return self
434426
@@ -441,21 +433,20 @@ class ExperimentPeriod(BaseModel):
441433 """
442434
443435 #: The start time of the period in time units as defined in the model.
444- # TODO: Only finite times and -inf are allowed as start time
445- time : float = Field ( alias = C .TIME )
446- # TODO: decide if optional
436+ time : Annotated [ float , AfterValidator ( is_finite_or_neg_inf )] = Field (
437+ alias = C .TIME
438+ )
447439 #: The ID of the condition to be applied at the start time.
448- condition_id : str = Field (alias = C .CONDITION_ID )
440+ condition_id : str | None = Field (alias = C .CONDITION_ID , default = None )
449441
450442 #: :meta private:
451443 model_config = ConfigDict (populate_by_name = True )
452444
453445 @field_validator ("condition_id" , mode = "before" )
454446 @classmethod
455447 def _validate_id (cls , condition_id ):
456- # TODO to be decided if optional
457- if pd .isna (condition_id ):
458- return ""
448+ if pd .isna (condition_id ) or not condition_id :
449+ return None
459450 # if not condition_id:
460451 # raise ValueError("ID must not be empty.")
461452 if not is_valid_identifier (condition_id ):
@@ -633,12 +624,17 @@ def _validate_id(cls, v, info: ValidationInfo):
633624 )
634625 @classmethod
635626 def _sympify_list (cls , v ):
627+ if v is None :
628+ return []
629+
636630 if isinstance (v , float ) and np .isnan (v ):
637631 return []
632+
638633 if isinstance (v , str ):
639634 v = v .split (C .PARAMETER_SEPARATOR )
640- else :
635+ elif not isinstance ( v , Sequence ) :
641636 v = [v ]
637+
642638 return [sympify_petab (x ) for x in v ]
643639
644640
0 commit comments