2
2
3
3
from __future__ import annotations
4
4
5
+ from collections .abc import Sequence
5
6
from enum import Enum
6
7
from pathlib import Path
8
+ from typing import Annotated
7
9
8
10
import numpy as np
9
11
import pandas as pd
10
12
import sympy as sp
11
13
from pydantic import (
14
+ AfterValidator ,
12
15
BaseModel ,
16
+ BeforeValidator ,
13
17
ConfigDict ,
14
18
Field ,
15
19
ValidationInfo ,
29
33
"Change" ,
30
34
"Condition" ,
31
35
"ConditionsTable" ,
32
- "OperationType" ,
33
36
"ExperimentPeriod" ,
34
37
"Experiment" ,
35
38
"ExperimentsTable" ,
43
46
]
44
47
45
48
49
+ def is_finite_or_neg_inf (v : float , info : ValidationInfo ) -> float :
50
+ if not np .isfinite (v ) and v != - np .inf :
51
+ raise ValueError (
52
+ f"{ info .field_name } value must be finite or -inf but got { v } "
53
+ )
54
+ return v
55
+
56
+
57
+ def _convert_nan_to_none (v ):
58
+ if isinstance (v , float ) and np .isnan (v ):
59
+ return None
60
+ return v
61
+
62
+
46
63
class ObservableTransformation (str , Enum ):
47
64
"""Observable transformation types.
48
65
@@ -248,16 +265,6 @@ def __iadd__(self, other: Observable) -> ObservablesTable:
248
265
return self
249
266
250
267
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
-
261
268
class Change (BaseModel ):
262
269
"""A change to the model or model state.
263
270
@@ -266,17 +273,13 @@ class Change(BaseModel):
266
273
267
274
>>> Change(
268
275
... target_id="k1",
269
- ... operation_type=OperationType.SET_CURRENT_VALUE,
270
276
... target_value="10",
271
277
... ) # doctest: +NORMALIZE_WHITESPACE
272
- Change(target_id='k1', operation_type='setCurrentValue',
273
- target_value=10.0000000000000)
278
+ Change(target_id='k1', target_value=10.0000000000000)
274
279
"""
275
280
276
281
#: The ID of the target entity to change.
277
282
target_id : str | None = Field (alias = C .TARGET_ID , default = None )
278
- # TODO: remove?!
279
- operation_type : OperationType = Field (alias = C .OPERATION_TYPE )
280
283
#: The value to set the target entity to.
281
284
target_value : sp .Basic | None = Field (alias = C .TARGET_VALUE , default = None )
282
285
@@ -290,14 +293,11 @@ class Change(BaseModel):
290
293
@model_validator (mode = "before" )
291
294
@classmethod
292
295
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 } " )
296
+ target_id = data .get ("target_id" , data .get (C .TARGET_ID ))
297
+
298
+ if not is_valid_identifier (target_id ):
299
+ raise ValueError (f"Invalid ID: { target_id } " )
300
+
301
301
return data
302
302
303
303
@field_validator ("target_value" , mode = "before" )
@@ -323,13 +323,12 @@ class Condition(BaseModel):
323
323
... changes=[
324
324
... Change(
325
325
... target_id="k1",
326
- ... operation_type=OperationType.SET_CURRENT_VALUE,
327
326
... target_value="10",
328
327
... )
329
328
... ],
330
329
... ) # doctest: +NORMALIZE_WHITESPACE
331
- Condition(id='condition1', changes=[Change(target_id='k1',
332
- operation_type='setCurrentValue ', target_value=10.0000000000000)])
330
+ Condition(id='condition1',
331
+ changes=[Change(target_id='k1 ', target_value=10.0000000000000)])
333
332
"""
334
333
335
334
#: The condition ID.
@@ -352,13 +351,13 @@ def _validate_id(cls, v):
352
351
def __add__ (self , other : Change ) -> Condition :
353
352
"""Add a change to the set."""
354
353
if not isinstance (other , Change ):
355
- raise TypeError ("Can only add Change to ChangeSet " )
354
+ raise TypeError ("Can only add Change to Condition " )
356
355
return Condition (id = self .id , changes = self .changes + [other ])
357
356
358
357
def __iadd__ (self , other : Change ) -> Condition :
359
358
"""Add a change to the set in place."""
360
359
if not isinstance (other , Change ):
361
- raise TypeError ("Can only add Change to ChangeSet " )
360
+ raise TypeError ("Can only add Change to Condition " )
362
361
self .changes .append (other )
363
362
return self
364
363
@@ -379,11 +378,11 @@ def __getitem__(self, condition_id: str) -> Condition:
379
378
@classmethod
380
379
def from_df (cls , df : pd .DataFrame ) -> ConditionsTable :
381
380
"""Create a ConditionsTable from a DataFrame."""
382
- if df is None :
381
+ if df is None or df . empty :
383
382
return cls (conditions = [])
384
383
385
384
conditions = []
386
- for condition_id , sub_df in df .groupby (C .CONDITION_ID ):
385
+ for condition_id , sub_df in df .reset_index (). groupby (C .CONDITION_ID ):
387
386
changes = [Change (** row .to_dict ()) for _ , row in sub_df .iterrows ()]
388
387
conditions .append (Condition (id = condition_id , changes = changes ))
389
388
@@ -422,13 +421,13 @@ def to_tsv(self, file_path: str | Path) -> None:
422
421
def __add__ (self , other : Condition ) -> ConditionsTable :
423
422
"""Add a condition to the table."""
424
423
if not isinstance (other , Condition ):
425
- raise TypeError ("Can only add ChangeSet to ConditionsTable" )
424
+ raise TypeError ("Can only add Conditions to ConditionsTable" )
426
425
return ConditionsTable (conditions = self .conditions + [other ])
427
426
428
427
def __iadd__ (self , other : Condition ) -> ConditionsTable :
429
428
"""Add a condition to the table in place."""
430
429
if not isinstance (other , Condition ):
431
- raise TypeError ("Can only add ChangeSet to ConditionsTable" )
430
+ raise TypeError ("Can only add Conditions to ConditionsTable" )
432
431
self .conditions .append (other )
433
432
return self
434
433
@@ -441,21 +440,20 @@ class ExperimentPeriod(BaseModel):
441
440
"""
442
441
443
442
#: 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
443
+ time : Annotated [ float , AfterValidator ( is_finite_or_neg_inf )] = Field (
444
+ alias = C .TIME
445
+ )
447
446
#: The ID of the condition to be applied at the start time.
448
- condition_id : str = Field (alias = C .CONDITION_ID )
447
+ condition_id : str | None = Field (alias = C .CONDITION_ID , default = None )
449
448
450
449
#: :meta private:
451
450
model_config = ConfigDict (populate_by_name = True )
452
451
453
452
@field_validator ("condition_id" , mode = "before" )
454
453
@classmethod
455
454
def _validate_id (cls , condition_id ):
456
- # TODO to be decided if optional
457
- if pd .isna (condition_id ):
458
- return ""
455
+ if pd .isna (condition_id ) or not condition_id :
456
+ return None
459
457
# if not condition_id:
460
458
# raise ValueError("ID must not be empty.")
461
459
if not is_valid_identifier (condition_id ):
@@ -633,12 +631,17 @@ def _validate_id(cls, v, info: ValidationInfo):
633
631
)
634
632
@classmethod
635
633
def _sympify_list (cls , v ):
634
+ if v is None :
635
+ return []
636
+
636
637
if isinstance (v , float ) and np .isnan (v ):
637
638
return []
639
+
638
640
if isinstance (v , str ):
639
641
v = v .split (C .PARAMETER_SEPARATOR )
640
- else :
642
+ elif not isinstance ( v , Sequence ) :
641
643
v = [v ]
644
+
642
645
return [sympify_petab (x ) for x in v ]
643
646
644
647
@@ -710,7 +713,13 @@ class Mapping(BaseModel):
710
713
#: PEtab entity ID.
711
714
petab_id : str = Field (alias = C .PETAB_ENTITY_ID )
712
715
#: Model entity ID.
713
- model_id : str = Field (alias = C .MODEL_ENTITY_ID )
716
+ model_id : Annotated [str | None , BeforeValidator (_convert_nan_to_none )] = (
717
+ Field (alias = C .MODEL_ENTITY_ID , default = None )
718
+ )
719
+ #: Arbitrary name
720
+ name : Annotated [str | None , BeforeValidator (_convert_nan_to_none )] = Field (
721
+ alias = C .NAME , default = None
722
+ )
714
723
715
724
#: :meta private:
716
725
model_config = ConfigDict (populate_by_name = True )
0 commit comments