Skip to content

Commit 1bdb36f

Browse files
committed
Add operators
1 parent 98b94ab commit 1bdb36f

File tree

2 files changed

+163
-18
lines changed

2 files changed

+163
-18
lines changed

petab/v2/core.py

Lines changed: 117 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
Field,
1313
ValidationInfo,
1414
field_validator,
15+
model_validator,
1516
)
1617

1718
from ..v1.lint import is_valid_identifier
@@ -199,19 +200,26 @@ def to_tsv(self, file_path: str | Path) -> None:
199200
df = self.to_dataframe()
200201
df.to_csv(file_path, sep="\t", index=False)
201202

203+
def __add__(self, other: Observable) -> ObservablesTable:
204+
"""Add an observable to the table."""
205+
if not isinstance(other, Observable):
206+
raise TypeError("Can only add Observable to ObservablesTable")
207+
return ObservablesTable(observables=self.observables + [other])
208+
209+
def __iadd__(self, other: Observable) -> ObservablesTable:
210+
"""Add an observable to the table in place."""
211+
if not isinstance(other, Observable):
212+
raise TypeError("Can only add Observable to ObservablesTable")
213+
self.observables.append(other)
214+
return self
215+
202216

203217
class OperationType(str, Enum):
204218
"""Operation types for model changes in the PEtab conditions table."""
205219

206220
# TODO update names
207221
SET_CURRENT_VALUE = "setCurrentValue"
208-
SET_RATE = "setRate"
209-
SET_ASSIGNMENT = "setAssignment"
210-
ADD_TO_RATE = "addToRate"
211-
ADD_TO_ASSIGNMENT = "addToAssignment"
212222
NO_CHANGE = "noChange"
213-
CONSTANT = "constant"
214-
INITIAL = "initial"
215223
...
216224

217225

@@ -222,23 +230,24 @@ class Change(BaseModel):
222230
row of the PEtab conditions table.
223231
"""
224232

225-
target_id: str = Field(alias=C.TARGET_ID)
233+
target_id: str | None = Field(alias=C.TARGET_ID, default=None)
226234
operation_type: OperationType = Field(alias=C.VALUE_TYPE)
227-
target_value: sp.Basic = Field(alias=C.TARGET_VALUE)
235+
target_value: sp.Basic | None = Field(alias=C.TARGET_VALUE, default=None)
228236

229237
class Config:
230238
populate_by_name = True
231239
arbitrary_types_allowed = True
232240
use_enum_values = True
233241

234-
@field_validator("target_id")
242+
@model_validator(mode="before")
235243
@classmethod
236-
def validate_id(cls, v):
237-
if not v:
238-
raise ValueError("ID must not be empty.")
239-
if not is_valid_identifier(v):
240-
raise ValueError(f"Invalid ID: {v}")
241-
return v
244+
def validate_id(cls, data: dict):
245+
if data.get("operation_type") != OperationType.NO_CHANGE:
246+
target_id = data.get("target_id")
247+
248+
if not is_valid_identifier(target_id):
249+
raise ValueError(f"Invalid ID: {target_id}")
250+
return data
242251

243252
@field_validator("target_value", mode="before")
244253
@classmethod
@@ -274,11 +283,24 @@ def validate_id(cls, v):
274283
raise ValueError(f"Invalid ID: {v}")
275284
return v
276285

286+
def __add__(self, other: Change) -> ChangeSet:
287+
"""Add a change to the set."""
288+
if not isinstance(other, Change):
289+
raise TypeError("Can only add Change to ChangeSet")
290+
return ChangeSet(id=self.id, changes=self.changes + [other])
291+
292+
def __iadd__(self, other: Change) -> ChangeSet:
293+
"""Add a change to the set in place."""
294+
if not isinstance(other, Change):
295+
raise TypeError("Can only add Change to ChangeSet")
296+
self.changes.append(other)
297+
return self
298+
277299

278300
class ConditionsTable(BaseModel):
279301
"""PEtab conditions table."""
280302

281-
conditions: list[ChangeSet]
303+
conditions: list[ChangeSet] = []
282304

283305
def __getitem__(self, condition_id: str) -> ChangeSet:
284306
"""Get a condition by ID."""
@@ -316,6 +338,19 @@ def to_tsv(self, file_path: str | Path) -> None:
316338
df = self.to_dataframe()
317339
df.to_csv(file_path, sep="\t", index=False)
318340

341+
def __add__(self, other: ChangeSet) -> ConditionsTable:
342+
"""Add a condition to the table."""
343+
if not isinstance(other, ChangeSet):
344+
raise TypeError("Can only add ChangeSet to ConditionsTable")
345+
return ConditionsTable(conditions=self.conditions + [other])
346+
347+
def __iadd__(self, other: ChangeSet) -> ConditionsTable:
348+
"""Add a condition to the table in place."""
349+
if not isinstance(other, ChangeSet):
350+
raise TypeError("Can only add ChangeSet to ConditionsTable")
351+
self.conditions.append(other)
352+
return self
353+
319354

320355
class ExperimentPeriod(BaseModel):
321356
"""A period of a timecourse defined by a start time and a set changes.
@@ -348,7 +383,7 @@ class Experiment(BaseModel):
348383
"""
349384

350385
id: str = Field(alias=C.EXPERIMENT_ID)
351-
periods: list[ExperimentPeriod]
386+
periods: list[ExperimentPeriod] = []
352387

353388
class Config:
354389
populate_by_name = True
@@ -363,6 +398,19 @@ def validate_id(cls, v):
363398
raise ValueError(f"Invalid ID: {v}")
364399
return v
365400

401+
def __add__(self, other: ExperimentPeriod) -> Experiment:
402+
"""Add a period to the experiment."""
403+
if not isinstance(other, ExperimentPeriod):
404+
raise TypeError("Can only add ExperimentPeriod to Experiment")
405+
return Experiment(id=self.id, periods=self.periods + [other])
406+
407+
def __iadd__(self, other: ExperimentPeriod) -> Experiment:
408+
"""Add a period to the experiment in place."""
409+
if not isinstance(other, ExperimentPeriod):
410+
raise TypeError("Can only add ExperimentPeriod to Experiment")
411+
self.periods.append(other)
412+
return self
413+
366414

367415
class ExperimentsTable(BaseModel):
368416
"""PEtab experiments table."""
@@ -398,6 +446,19 @@ def to_tsv(self, file_path: str | Path) -> None:
398446
df = self.to_dataframe()
399447
df.to_csv(file_path, sep="\t", index=False)
400448

449+
def __add__(self, other: Experiment) -> ExperimentsTable:
450+
"""Add an experiment to the table."""
451+
if not isinstance(other, Experiment):
452+
raise TypeError("Can only add Experiment to ExperimentsTable")
453+
return ExperimentsTable(experiments=self.experiments + [other])
454+
455+
def __iadd__(self, other: Experiment) -> ExperimentsTable:
456+
"""Add an experiment to the table in place."""
457+
if not isinstance(other, Experiment):
458+
raise TypeError("Can only add Experiment to ExperimentsTable")
459+
self.experiments.append(other)
460+
return self
461+
401462

402463
class Measurement(BaseModel):
403464
"""A measurement.
@@ -492,6 +553,19 @@ def to_tsv(self, file_path: str | Path) -> None:
492553
df = self.to_dataframe()
493554
df.to_csv(file_path, sep="\t", index=False)
494555

556+
def __add__(self, other: Measurement) -> MeasurementTable:
557+
"""Add a measurement to the table."""
558+
if not isinstance(other, Measurement):
559+
raise TypeError("Can only add Measurement to MeasurementTable")
560+
return MeasurementTable(measurements=self.measurements + [other])
561+
562+
def __iadd__(self, other: Measurement) -> MeasurementTable:
563+
"""Add a measurement to the table in place."""
564+
if not isinstance(other, Measurement):
565+
raise TypeError("Can only add Measurement to MeasurementTable")
566+
self.measurements.append(other)
567+
return self
568+
495569

496570
class Mapping(BaseModel):
497571
"""Mapping PEtab entities to model entities."""
@@ -542,6 +616,19 @@ def to_tsv(self, file_path: str | Path) -> None:
542616
df = self.to_dataframe()
543617
df.to_csv(file_path, sep="\t", index=False)
544618

619+
def __add__(self, other: Mapping) -> MappingTable:
620+
"""Add a mapping to the table."""
621+
if not isinstance(other, Mapping):
622+
raise TypeError("Can only add Mapping to MappingTable")
623+
return MappingTable(mappings=self.mappings + [other])
624+
625+
def __iadd__(self, other: Mapping) -> MappingTable:
626+
"""Add a mapping to the table in place."""
627+
if not isinstance(other, Mapping):
628+
raise TypeError("Can only add Mapping to MappingTable")
629+
self.mappings.append(other)
630+
return self
631+
545632

546633
class Parameter(BaseModel):
547634
"""Parameter definition."""
@@ -606,3 +693,16 @@ def from_tsv(cls, file_path: str | Path) -> ParameterTable:
606693
def to_tsv(self, file_path: str | Path) -> None:
607694
df = self.to_dataframe()
608695
df.to_csv(file_path, sep="\t", index=False)
696+
697+
def __add__(self, other: Parameter) -> ParameterTable:
698+
"""Add a parameter to the table."""
699+
if not isinstance(other, Parameter):
700+
raise TypeError("Can only add Parameter to ParameterTable")
701+
return ParameterTable(parameters=self.parameters + [other])
702+
703+
def __iadd__(self, other: Parameter) -> ParameterTable:
704+
"""Add a parameter to the table in place."""
705+
if not isinstance(other, Parameter):
706+
raise TypeError("Can only add Parameter to ParameterTable")
707+
self.parameters.append(other)
708+
return self

tests/v2/test_core.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
import tempfile
22
from pathlib import Path
33

4-
from petab.v2.core import ConditionsTable, ObservablesTable
4+
from petab.v2.core import (
5+
Change,
6+
ChangeSet,
7+
ConditionsTable,
8+
Experiment,
9+
ExperimentPeriod,
10+
ObservablesTable,
11+
OperationType,
12+
)
513
from petab.v2.petab1to2 import petab1to2
614

715
example_dir_fujita = Path(__file__).parents[2] / "doc/example/example_Fujita"
@@ -30,3 +38,40 @@ def test_conditions_table():
3038
conditions.to_tsv(tmp_file)
3139
conditions2 = ConditionsTable.from_tsv(tmp_file)
3240
assert conditions == conditions2
41+
42+
43+
def test_experiment_add_periods():
44+
"""Test operators for Experiment"""
45+
exp = Experiment(id="exp1")
46+
assert exp.periods == []
47+
48+
p1 = ExperimentPeriod(start=0, condition_id="p1")
49+
p2 = ExperimentPeriod(start=1, condition_id="p2")
50+
p3 = ExperimentPeriod(start=2, condition_id="p3")
51+
exp += p1
52+
exp += p2
53+
54+
assert exp.periods == [p1, p2]
55+
56+
exp2 = exp + p3
57+
assert exp2.periods == [p1, p2, p3]
58+
assert exp.periods == [p1, p2]
59+
60+
61+
def test_conditions_table_add_changeset():
62+
conditions_table = ConditionsTable()
63+
assert conditions_table.conditions == []
64+
65+
c1 = ChangeSet(
66+
id="condition1",
67+
changes=[Change(operation_type=OperationType.NO_CHANGE)],
68+
)
69+
c2 = ChangeSet(
70+
id="condition2",
71+
changes=[Change(operation_type=OperationType.NO_CHANGE)],
72+
)
73+
74+
conditions_table += c1
75+
conditions_table += c2
76+
77+
assert conditions_table.conditions == [c1, c2]

0 commit comments

Comments
 (0)