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,
16+ BeforeValidator,
1317 ConfigDict,
1418 Field,
1519 ValidationInfo,
2933 "Change",
3034 "Condition",
3135 "ConditionsTable",
32- "OperationType",
3336 "ExperimentPeriod",
3437 "Experiment",
3538 "ExperimentsTable",
4346]
4447
4548
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+
4663class ObservableTransformation(str, Enum):
4764 """Observable transformation types.
4865
@@ -248,16 +265,6 @@ def __iadd__(self, other: Observable) -> ObservablesTable:
248265 return self
249266
250267
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-
261268class Change(BaseModel):
262269 """A change to the model or model state.
263270
@@ -266,17 +273,13 @@ class Change(BaseModel):
266273
267274 >>> Change(
268275 ... target_id="k1",
269- ... operation_type=OperationType.SET_CURRENT_VALUE,
270276 ... target_value="10",
271277 ... ) # 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)
274279 """
275280
276281 #: The ID of the target entity to change.
277282 target_id: str | None = Field(alias=C.TARGET_ID, default=None)
278- # TODO: remove?!
279- operation_type: OperationType = Field(alias=C.OPERATION_TYPE)
280283 #: The value to set the target entity to.
281284 target_value: sp.Basic | None = Field(alias=C.TARGET_VALUE, default=None)
282285
@@ -290,14 +293,11 @@ class Change(BaseModel):
290293 @model_validator(mode="before")
291294 @classmethod
292295 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+
301301 return data
302302
303303 @field_validator("target_value", mode="before")
@@ -323,13 +323,12 @@ class Condition(BaseModel):
323323 ... changes=[
324324 ... Change(
325325 ... target_id="k1",
326- ... operation_type=OperationType.SET_CURRENT_VALUE,
327326 ... target_value="10",
328327 ... )
329328 ... ],
330329 ... ) # 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)])
333332 """
334333
335334 #: The condition ID.
@@ -352,13 +351,13 @@ def _validate_id(cls, v):
352351 def __add__(self, other: Change) -> Condition:
353352 """Add a change to the set."""
354353 if not isinstance(other, Change):
355- raise TypeError("Can only add Change to ChangeSet ")
354+ raise TypeError("Can only add Change to Condition ")
356355 return Condition(id=self.id, changes=self.changes + [other])
357356
358357 def __iadd__(self, other: Change) -> Condition:
359358 """Add a change to the set in place."""
360359 if not isinstance(other, Change):
361- raise TypeError("Can only add Change to ChangeSet ")
360+ raise TypeError("Can only add Change to Condition ")
362361 self.changes.append(other)
363362 return self
364363
@@ -379,11 +378,11 @@ def __getitem__(self, condition_id: str) -> Condition:
379378 @classmethod
380379 def from_df(cls, df: pd.DataFrame) -> ConditionsTable:
381380 """Create a ConditionsTable from a DataFrame."""
382- if df is None:
381+ if df is None or df.empty :
383382 return cls(conditions=[])
384383
385384 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):
387386 changes = [Change(**row.to_dict()) for _, row in sub_df.iterrows()]
388387 conditions.append(Condition(id=condition_id, changes=changes))
389388
@@ -422,13 +421,13 @@ def to_tsv(self, file_path: str | Path) -> None:
422421 def __add__(self, other: Condition) -> ConditionsTable:
423422 """Add a condition to the table."""
424423 if not isinstance(other, Condition):
425- raise TypeError("Can only add ChangeSet to ConditionsTable")
424+ raise TypeError("Can only add Conditions to ConditionsTable")
426425 return ConditionsTable(conditions=self.conditions + [other])
427426
428427 def __iadd__(self, other: Condition) -> ConditionsTable:
429428 """Add a condition to the table in place."""
430429 if not isinstance(other, Condition):
431- raise TypeError("Can only add ChangeSet to ConditionsTable")
430+ raise TypeError("Can only add Conditions to ConditionsTable")
432431 self.conditions.append(other)
433432 return self
434433
@@ -441,21 +440,20 @@ class ExperimentPeriod(BaseModel):
441440 """
442441
443442 #: 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+ )
447446 #: 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 )
449448
450449 #: :meta private:
451450 model_config = ConfigDict(populate_by_name=True)
452451
453452 @field_validator("condition_id", mode="before")
454453 @classmethod
455454 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
459457 # if not condition_id:
460458 # raise ValueError("ID must not be empty.")
461459 if not is_valid_identifier(condition_id):
@@ -633,12 +631,17 @@ def _validate_id(cls, v, info: ValidationInfo):
633631 )
634632 @classmethod
635633 def _sympify_list(cls, v):
634+ if v is None:
635+ return []
636+
636637 if isinstance(v, float) and np.isnan(v):
637638 return []
639+
638640 if isinstance(v, str):
639641 v = v.split(C.PARAMETER_SEPARATOR)
640- else :
642+ elif not isinstance(v, Sequence) :
641643 v = [v]
644+
642645 return [sympify_petab(x) for x in v]
643646
644647
@@ -710,7 +713,13 @@ class Mapping(BaseModel):
710713 #: PEtab entity ID.
711714 petab_id: str = Field(alias=C.PETAB_ENTITY_ID)
712715 #: 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+ )
714723
715724 #: :meta private:
716725 model_config = ConfigDict(populate_by_name=True)
0 commit comments