Skip to content

Commit c50f4a8

Browse files
committed
lint
1 parent 2e26c8d commit c50f4a8

File tree

5 files changed

+94
-80
lines changed

5 files changed

+94
-80
lines changed

petab/v2/core.py

+24-2
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,8 @@ class ExperimentPeriod(BaseModel):
441441
"""
442442

443443
#: The start time of the period in time units as defined in the model.
444-
start: float = Field(alias=C.TIME)
444+
# TODO: Only finite times and -inf are allowed as start time
445+
time: float = Field(alias=C.TIME)
445446
# TODO: decide if optional
446447
#: The ID of the condition to be applied at the start time.
447448
condition_id: str = Field(alias=C.CONDITION_ID)
@@ -519,7 +520,7 @@ def from_df(cls, df: pd.DataFrame) -> ExperimentsTable:
519520
for experiment_id, cur_exp_df in df.groupby(C.EXPERIMENT_ID):
520521
periods = [
521522
ExperimentPeriod(
522-
start=row[C.TIME], condition_id=row[C.CONDITION_ID]
523+
time=row[C.TIME], condition_id=row[C.CONDITION_ID]
523524
)
524525
for _, row in cur_exp_df.iterrows()
525526
]
@@ -567,6 +568,13 @@ def __iadd__(self, other: Experiment) -> ExperimentsTable:
567568
self.experiments.append(other)
568569
return self
569570

571+
def __getitem__(self, item):
572+
"""Get an experiment by ID."""
573+
for experiment in self.experiments:
574+
if experiment.id == item:
575+
return experiment
576+
raise KeyError(f"Experiment ID {item} not found")
577+
570578

571579
class Measurement(BaseModel):
572580
"""A measurement.
@@ -770,6 +778,13 @@ def __iadd__(self, other: Mapping) -> MappingTable:
770778
self.mappings.append(other)
771779
return self
772780

781+
def __getitem__(self, petab_id: str) -> Mapping:
782+
"""Get a mapping by PEtab ID."""
783+
for mapping in self.mappings:
784+
if mapping.petab_id == petab_id:
785+
return mapping
786+
raise KeyError(f"PEtab ID {petab_id} not found")
787+
773788

774789
class Parameter(BaseModel):
775790
"""Parameter definition."""
@@ -862,3 +877,10 @@ def __iadd__(self, other: Parameter) -> ParameterTable:
862877
raise TypeError("Can only add Parameter to ParameterTable")
863878
self.parameters.append(other)
864879
return self
880+
881+
def __getitem__(self, item) -> Parameter:
882+
"""Get a parameter by ID."""
883+
for parameter in self.parameters:
884+
if parameter.id == item:
885+
return parameter
886+
raise KeyError(f"Parameter ID {item} not found")

petab/v2/lint.py

+30-48
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import logging
66
from abc import ABC, abstractmethod
7-
from collections import OrderedDict
7+
from collections import Counter, OrderedDict
88
from collections.abc import Set
99
from dataclasses import dataclass, field
1010
from enum import IntEnum
@@ -496,62 +496,44 @@ class CheckExperimentTable(ValidationTask):
496496
"""A task to validate the experiment table of a PEtab problem."""
497497

498498
def run(self, problem: Problem) -> ValidationIssue | None:
499-
if problem.experiment_df is None:
500-
return
501-
502-
df = problem.experiment_df
503-
504-
try:
505-
_check_df(df, EXPERIMENT_DF_REQUIRED_COLS, "experiment")
506-
except AssertionError as e:
507-
return ValidationError(str(e))
499+
messages = []
500+
for experiment in problem.experiments_table.experiments:
501+
# Check that there are no duplicate timepoints
502+
counter = Counter(period.time for period in experiment.periods)
503+
duplicates = {time for time, count in counter.items() if count > 1}
504+
if duplicates:
505+
messages.append(
506+
f"Experiment {experiment.id} contains duplicate "
507+
f"timepoints: {duplicates}"
508+
)
508509

509-
# valid timepoints
510-
invalid = []
511-
for time in df[TIME].values:
512-
try:
513-
time = float(time)
514-
if not np.isfinite(time) and time != -np.inf:
515-
invalid.append(time)
516-
except ValueError:
517-
invalid.append(time)
518-
if invalid:
519-
return ValidationError(
520-
f"Invalid timepoints in experiment table: {invalid}"
521-
)
510+
if messages:
511+
return ValidationError("\n".join(messages))
522512

523513

524514
class CheckExperimentConditionsExist(ValidationTask):
525515
"""A task to validate that all conditions in the experiment table exist
526516
in the condition table."""
527517

528518
def run(self, problem: Problem) -> ValidationIssue | None:
529-
if problem.experiment_df is None:
530-
return
531-
532-
if (
533-
problem.condition_df is None
534-
and problem.experiment_df is not None
535-
and not problem.experiment_df.empty
536-
):
537-
return ValidationError(
538-
"Experiment table is non-empty, "
539-
"but condition table is missing."
540-
)
541-
542-
required_conditions = problem.experiment_df[CONDITION_ID].unique()
543-
existing_conditions = problem.condition_df[CONDITION_ID].unique()
519+
messages = []
520+
available_conditions = {
521+
c.id
522+
for c in problem.conditions_table.conditions
523+
if not pd.isna(c.id)
524+
}
525+
for experiment in problem.experiments_table.experiments:
526+
missing_conditions = {
527+
period.condition for period in experiment.periods
528+
} - available_conditions
529+
if missing_conditions:
530+
messages.append(
531+
f"Experiment {experiment.id} requires conditions that are "
532+
f"not present in the condition table: {missing_conditions}"
533+
)
544534

545-
missing_conditions = set(required_conditions) - set(
546-
existing_conditions
547-
)
548-
# TODO NA allowed?
549-
missing_conditions = {x for x in missing_conditions if not pd.isna(x)}
550-
if missing_conditions:
551-
return ValidationError(
552-
f"Experiment table contains conditions that are not present "
553-
f"in the condition table: {missing_conditions}"
554-
)
535+
if messages:
536+
return ValidationError("\n".join(messages))
555537

556538

557539
class CheckAllParametersPresentInParameterTable(ValidationTask):

petab/v2/problem.py

+33-14
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,32 @@ def __str__(self):
153153
f"{observables}, {measurements}, {parameters}"
154154
)
155155

156+
def __getitem__(self, key):
157+
"""Get PEtab entity by ID.
158+
159+
This allows accessing PEtab entities such as conditions, experiments,
160+
observables, and parameters by their ID.
161+
162+
Accessing model entities is not currently not supported.
163+
"""
164+
for table in (
165+
self.conditions_table,
166+
self.experiments_table,
167+
self.observables_table,
168+
self.measurement_table,
169+
self.parameter_table,
170+
self.mapping_table,
171+
):
172+
if table is not None:
173+
try:
174+
return table[key]
175+
except KeyError:
176+
pass
177+
178+
raise KeyError(
179+
f"Entity with ID '{key}' not found in the PEtab problem"
180+
)
181+
156182
@staticmethod
157183
def from_yaml(
158184
yaml_config: dict | Path | str, base_path: str | Path = None
@@ -1062,20 +1088,13 @@ def add_experiment(self, id_: str, *args):
10621088
"Arguments must be pairs of timepoints and condition IDs."
10631089
)
10641090

1065-
records = []
1066-
for i in range(0, len(args), 2):
1067-
records.append(
1068-
{
1069-
EXPERIMENT_ID: id_,
1070-
TIME: args[i],
1071-
CONDITION_ID: args[i + 1],
1072-
}
1073-
)
1074-
tmp_df = pd.DataFrame(records)
1075-
self.experiment_df = (
1076-
pd.concat([self.experiment_df, tmp_df])
1077-
if self.experiment_df is not None
1078-
else tmp_df
1091+
periods = [
1092+
core.ExperimentPeriod(time=args[i], condition_id=args[i + 1])
1093+
for i in range(0, len(args), 2)
1094+
]
1095+
1096+
self.experiments_table.experiments.append(
1097+
core.Experiment(id=id_, periods=periods)
10791098
)
10801099

10811100
def __iadd__(self, other):

tests/v2/test_core.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@ def test_experiment_add_periods():
4242
exp = Experiment(id="exp1")
4343
assert exp.periods == []
4444

45-
p1 = ExperimentPeriod(start=0, condition_id="p1")
46-
p2 = ExperimentPeriod(start=1, condition_id="p2")
47-
p3 = ExperimentPeriod(start=2, condition_id="p3")
45+
p1 = ExperimentPeriod(time=0, condition_id="p1")
46+
p2 = ExperimentPeriod(time=1, condition_id="p2")
47+
p3 = ExperimentPeriod(time=2, condition_id="p3")
4848
exp += p1
4949
exp += p2
5050

tests/v2/test_lint.py

+4-13
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,14 @@
1010
def test_check_experiments():
1111
"""Test ``CheckExperimentTable``."""
1212
problem = Problem()
13-
problem.add_experiment("e1", 0, "c1", 1, "c2")
14-
problem.add_experiment("e2", "-inf", "c1", 1, "c2")
15-
assert problem.experiment_df.shape == (4, 3)
1613

1714
check = CheckExperimentTable()
1815
assert check.run(problem) is None
1916

20-
assert check.run(Problem()) is None
21-
22-
tmp_problem = deepcopy(problem)
23-
tmp_problem.experiment_df.loc[0, TIME] = "invalid"
24-
assert check.run(tmp_problem) is not None
25-
26-
tmp_problem = deepcopy(problem)
27-
tmp_problem.experiment_df.loc[0, TIME] = "inf"
28-
assert check.run(tmp_problem) is not None
17+
problem.add_experiment("e1", 0, "c1", 1, "c2")
18+
problem.add_experiment("e2", "-inf", "c1", 1, "c2")
19+
assert check.run(problem) is None
2920

3021
tmp_problem = deepcopy(problem)
31-
tmp_problem.experiment_df.drop(columns=[TIME], inplace=True)
22+
tmp_problem["e1"].periods[0].time = tmp_problem["e1"].periods[1].time
3223
assert check.run(tmp_problem) is not None

0 commit comments

Comments
 (0)