Skip to content

Commit d0443e3

Browse files
committed
Add constants + I/O for new conditions/experiments tables
* constants * read/write experiment table * add experiments table to Problem, and populate from yaml * some first validation tasks To be complemented by separate pull requests.
1 parent d3e4006 commit d0443e3

File tree

11 files changed

+338
-16
lines changed

11 files changed

+338
-16
lines changed

doc/modules.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,8 @@ API Reference
3030
petab.v1.yaml
3131
petab.v2
3232
petab.v2.C
33+
petab.v2.experiments
3334
petab.v2.lint
35+
petab.v2.models
3436
petab.v2.problem
37+
petab.v2.petab1to2

petab/schemas/petab_schema.v2.0.0.yaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ properties:
7676
description: List of PEtab condition files.
7777
$ref: "#/definitions/list_of_files"
7878

79+
experiment_files:
80+
description: List of PEtab condition files.
81+
$ref: "#/definitions/list_of_files"
82+
7983
observable_files:
8084
description: List of PEtab observable files.
8185
$ref: "#/definitions/list_of_files"
@@ -92,7 +96,6 @@ properties:
9296
- model_files
9397
- observable_files
9498
- measurement_files
95-
- condition_files
9699

97100
extensions:
98101
type: object

petab/v2/C.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,14 @@
1010
#: Observable ID column in the observable and measurement tables
1111
OBSERVABLE_ID = "observableId"
1212

13+
#: Experiment ID column in the measurement table
14+
EXPERIMENT_ID = "experimentId"
15+
16+
# TODO: remove
1317
#: Preequilibration condition ID column in the measurement table
1418
PREEQUILIBRATION_CONDITION_ID = "preequilibrationConditionId"
1519

20+
# TODO: remove
1621
#: Simulation condition ID column in the measurement table
1722
SIMULATION_CONDITION_ID = "simulationConditionId"
1823

@@ -40,13 +45,16 @@
4045
#: Mandatory columns of measurement table
4146
MEASUREMENT_DF_REQUIRED_COLS = [
4247
OBSERVABLE_ID,
48+
# TODO: add
49+
# EXPERIMENT_ID,
4350
SIMULATION_CONDITION_ID,
4451
MEASUREMENT,
4552
TIME,
4653
]
4754

4855
#: Optional columns of measurement table
4956
MEASUREMENT_DF_OPTIONAL_COLS = [
57+
# TODO: remove
5058
PREEQUILIBRATION_CONDITION_ID,
5159
OBSERVABLE_PARAMETERS,
5260
NOISE_PARAMETERS,
@@ -125,9 +133,45 @@
125133

126134
#: Condition ID column in the condition table
127135
CONDITION_ID = "conditionId"
136+
# TODO: removed?
128137
#: Condition name column in the condition table
129138
CONDITION_NAME = "conditionName"
130139

140+
#: Column in the condition table with the ID of an entity that is changed
141+
TARGET_ID = "targetId"
142+
#: Column in the condition table with the type of value that is changed
143+
VALUE_TYPE = "valueType"
144+
#: Column in the condition table with the new value of the target entity
145+
TARGET_VALUE = "targetValue"
146+
# value types:
147+
VT_CONSTANT = "constant"
148+
VT_INITIAL = "initial"
149+
VT_RATE = "rate"
150+
VT_ASSIGNMENT = "assignment"
151+
VT_RELATIVE_RATE = "relativeRate"
152+
VT_RELATIVE_ASSIGNMENT = "relativeAssignment"
153+
VALUE_TYPES = [
154+
VT_CONSTANT,
155+
VT_INITIAL,
156+
VT_RATE,
157+
VT_ASSIGNMENT,
158+
VT_RELATIVE_RATE,
159+
VT_RELATIVE_ASSIGNMENT,
160+
]
161+
162+
CONDITION_DF_COLS = [
163+
CONDITION_ID,
164+
TARGET_ID,
165+
VALUE_TYPE,
166+
TARGET_VALUE,
167+
]
168+
169+
# EXPERIMENTS
170+
EXPERIMENT_DF_REQUIRED_COLS = [
171+
EXPERIMENT_ID,
172+
TIME,
173+
CONDITION_ID,
174+
]
131175

132176
# OBSERVABLES
133177

@@ -332,6 +376,8 @@
332376
MODEL_LANGUAGE = "language"
333377
#: Condition files key in the YAML file
334378
CONDITION_FILES = "condition_files"
379+
#: Experiment files key in the YAML file
380+
EXPERIMENT_FILES = "experiment_files"
335381
#: Measurement files key in the YAML file
336382
MEASUREMENT_FILES = "measurement_files"
337383
#: Observable files key in the YAML file

petab/v2/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
from warnings import warn
66

77
from ..v1 import * # noqa: F403, F401, E402
8+
from .experiments import ( # noqa: F401
9+
get_experiment_df,
10+
write_experiment_df,
11+
)
812

913
# import after v1
1014
from .problem import Problem # noqa: F401

petab/v2/experiments.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""Functions operating on the PEtab experiments table."""
2+
from pathlib import Path
3+
4+
import pandas as pd
5+
6+
__all__ = ["get_experiment_df", "write_experiment_df"]
7+
8+
9+
def get_experiment_df(
10+
experiments_file: str | pd.DataFrame | Path | None,
11+
) -> pd.DataFrame | None:
12+
"""
13+
Read the provided observable file into a ``pandas.Dataframe``.
14+
15+
Arguments:
16+
experiments_file: Name of the file to read from or pandas.Dataframe.
17+
18+
Returns:
19+
Observable DataFrame
20+
"""
21+
if experiments_file is None:
22+
return experiments_file
23+
24+
if isinstance(experiments_file, str | Path):
25+
experiments_file = pd.read_csv(
26+
experiments_file, sep="\t", float_precision="round_trip"
27+
)
28+
29+
return experiments_file
30+
31+
32+
def write_experiment_df(df: pd.DataFrame, filename: str | Path) -> None:
33+
"""Write PEtab experiments table
34+
35+
Arguments:
36+
df: PEtab experiments table
37+
filename: Destination file name
38+
"""
39+
df = get_experiment_df(df)
40+
df.to_csv(filename, sep="\t", index=False)

petab/v2/lint.py

Lines changed: 100 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,6 @@
1010
import numpy as np
1111
import pandas as pd
1212

13-
from petab.v1 import (
14-
assert_model_parameters_in_condition_or_parameter_table,
15-
)
16-
from petab.v1.C import (
17-
ESTIMATE,
18-
MODEL_ENTITY_ID,
19-
NOISE_PARAMETERS,
20-
NOMINAL_VALUE,
21-
OBSERVABLE_PARAMETERS,
22-
PARAMETER_DF_REQUIRED_COLS,
23-
PARAMETER_ID,
24-
)
2513
from petab.v1.conditions import get_parametric_overrides
2614
from petab.v1.lint import (
2715
_check_df,
@@ -42,6 +30,10 @@
4230
get_valid_parameters_for_parameter_table,
4331
)
4432
from petab.v1.visualize.lint import validate_visualization_df
33+
from petab.v2 import (
34+
assert_model_parameters_in_condition_or_parameter_table,
35+
)
36+
from petab.v2.C import *
4537

4638
from ..v1 import (
4739
assert_measurement_conditions_present_in_condition_table,
@@ -61,10 +53,13 @@
6153
"ValidationTask",
6254
"CheckModel",
6355
"CheckTableExists",
56+
"CheckValidPetabIdColumn",
6457
"CheckMeasurementTable",
6558
"CheckConditionTable",
6659
"CheckObservableTable",
6760
"CheckParameterTable",
61+
"CheckExperimentTable",
62+
"CheckExperimentConditionsExist",
6863
"CheckAllParametersPresentInParameterTable",
6964
"CheckValidParameterInConditionOrParameterTable",
7065
"CheckVisualizationTable",
@@ -214,6 +209,35 @@ def run(self, problem: Problem) -> ValidationIssue | None:
214209
return ValidationError(f"{self.table_name} table is missing.")
215210

216211

212+
class CheckValidPetabIdColumn(ValidationTask):
213+
"""A task to check that a given column contains only valid PEtab IDs."""
214+
215+
def __init__(
216+
self, table_name: str, column_name: str, required_column: bool = True
217+
):
218+
self.table_name = table_name
219+
self.column_name = column_name
220+
self.required_column = required_column
221+
222+
def run(self, problem: Problem) -> ValidationIssue | None:
223+
df = getattr(problem, f"{self.table_name}_df")
224+
if df is None:
225+
return
226+
227+
if self.column_name not in df.columns:
228+
if self.required_column:
229+
return ValidationError(
230+
f"Column {self.column_name} is missing in "
231+
f"{self.table_name} table."
232+
)
233+
return
234+
235+
try:
236+
check_ids(df[self.column_name].values, kind=self.column_name)
237+
except ValueError as e:
238+
return ValidationError(str(e))
239+
240+
217241
class CheckMeasurementTable(ValidationTask):
218242
"""A task to validate the measurement table of a PEtab problem."""
219243

@@ -356,6 +380,66 @@ def run(self, problem: Problem) -> ValidationIssue | None:
356380
return ValidationError(str(e))
357381

358382

383+
class CheckExperimentTable(ValidationTask):
384+
"""A task to validate the experiment table of a PEtab problem."""
385+
386+
def run(self, problem: Problem) -> ValidationIssue | None:
387+
if problem.experiment_df is None:
388+
return
389+
390+
df = problem.experiment_df
391+
392+
try:
393+
_check_df(df, EXPERIMENT_DF_REQUIRED_COLS, "experiment")
394+
except AssertionError as e:
395+
return ValidationError(str(e))
396+
397+
# valid timepoints
398+
invalid = []
399+
for time in df[TIME].values:
400+
try:
401+
time = float(time)
402+
if not np.isfinite(time) and time != -np.inf:
403+
invalid.append(time)
404+
except ValueError:
405+
invalid.append(time)
406+
if invalid:
407+
return ValidationError(
408+
f"Invalid timepoints in experiment table: {invalid}"
409+
)
410+
411+
412+
class CheckExperimentConditionsExist(ValidationTask):
413+
"""A task to validate that all conditions in the experiment table exist
414+
in the condition table."""
415+
416+
def run(self, problem: Problem) -> ValidationIssue | None:
417+
if problem.experiment_df is None:
418+
return
419+
420+
if (
421+
problem.condition_df is None
422+
and problem.experiment_df is not None
423+
and not problem.experiment_df.empty
424+
):
425+
return ValidationError(
426+
"Experiment table is non-empty, "
427+
"but condition table is missing."
428+
)
429+
430+
required_conditions = problem.experiment_df[CONDITION_ID].unique()
431+
existing_conditions = problem.condition_df.index
432+
433+
missing_conditions = set(required_conditions) - set(
434+
existing_conditions
435+
)
436+
if missing_conditions:
437+
return ValidationError(
438+
f"Experiment table contains conditions that are not present "
439+
f"in the condition table: {missing_conditions}"
440+
)
441+
442+
359443
class CheckAllParametersPresentInParameterTable(ValidationTask):
360444
"""Ensure all required parameters are contained in the parameter table
361445
with no additional ones."""
@@ -558,6 +642,10 @@ def append_overrides(overrides):
558642
CheckModel(),
559643
CheckMeasurementTable(),
560644
CheckConditionTable(),
645+
CheckExperimentTable(),
646+
CheckValidPetabIdColumn("experiment", EXPERIMENT_ID),
647+
CheckValidPetabIdColumn("experiment", CONDITION_ID),
648+
CheckExperimentConditionsExist(),
561649
CheckObservableTable(),
562650
CheckObservablesDoNotShadowModelEntities(),
563651
CheckParameterTable(),

0 commit comments

Comments
 (0)