Skip to content

Commit b12d492

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 b12d492

File tree

10 files changed

+298
-16
lines changed

10 files changed

+298
-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: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"""Functions operating on the PEtab experiments table."""
2+
from pathlib import Path
3+
4+
import pandas as pd
5+
6+
from .C import EXPERIMENT_ID
7+
8+
__all__ = ["get_experiment_df", "write_experiment_df"]
9+
10+
11+
def get_experiment_df(
12+
experiments_file: str | pd.DataFrame | Path | None,
13+
) -> pd.DataFrame | None:
14+
"""
15+
Read the provided observable file into a ``pandas.Dataframe``.
16+
17+
Arguments:
18+
observable_file: Name of the file to read from or pandas.Dataframe.
19+
20+
Returns:
21+
Observable DataFrame
22+
"""
23+
if experiments_file is None:
24+
return experiments_file
25+
26+
if isinstance(experiments_file, str | Path):
27+
experiments_file = pd.read_csv(
28+
experiments_file, sep="\t", float_precision="round_trip"
29+
)
30+
31+
if not isinstance(experiments_file.index, pd.RangeIndex):
32+
experiments_file.reset_index(
33+
drop=experiments_file.index.name != EXPERIMENT_ID,
34+
inplace=True,
35+
)
36+
37+
try:
38+
experiments_file.set_index([EXPERIMENT_ID], inplace=True)
39+
except KeyError:
40+
raise KeyError(
41+
f"Experiment table missing mandatory field {EXPERIMENT_ID}."
42+
) from None
43+
44+
return experiments_file
45+
46+
47+
def write_experiment_df(df: pd.DataFrame, filename: str | Path) -> None:
48+
"""Write PEtab experiments table
49+
50+
Arguments:
51+
df: PEtab experiments table
52+
filename: Destination file name
53+
"""
54+
df = get_experiment_df(df)
55+
df.to_csv(filename, sep="\t", index=True)

petab/v2/lint.py

Lines changed: 105 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,71 @@ 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[1:], "experiment")
394+
except AssertionError as e:
395+
return ValidationError(str(e))
396+
397+
if df.index.name != EXPERIMENT_ID:
398+
return ValidationError(
399+
f"Experiment table has wrong index {df.index.name}. "
400+
f"Expected {EXPERIMENT_ID}.",
401+
)
402+
403+
# valid timepoints
404+
invalid = []
405+
for time in df[TIME].values:
406+
try:
407+
float(time)
408+
except ValueError:
409+
if time != "-inf":
410+
invalid.append(time)
411+
if invalid:
412+
return ValidationError(
413+
f"Invalid timepoints in experiment table: {invalid}"
414+
)
415+
416+
417+
class CheckExperimentConditionsExist(ValidationTask):
418+
"""A task to validate that all conditions in the experiment table exist
419+
in the condition table."""
420+
421+
def run(self, problem: Problem) -> ValidationIssue | None:
422+
if problem.experiment_df is None:
423+
return
424+
425+
if (
426+
problem.condition_df is None
427+
and problem.experiment_df is not None
428+
and not problem.experiment_df.empty
429+
):
430+
return ValidationError(
431+
"Experiment table is non-empty, "
432+
"but condition table is missing."
433+
)
434+
435+
required_conditions = problem.experiment_df[CONDITION_ID].unique()
436+
existing_conditions = problem.condition_df.index
437+
438+
missing_conditions = set(required_conditions) - set(
439+
existing_conditions
440+
)
441+
if missing_conditions:
442+
return ValidationError(
443+
f"Experiment table contains conditions that are not present "
444+
f"in the condition table: {missing_conditions}"
445+
)
446+
447+
359448
class CheckAllParametersPresentInParameterTable(ValidationTask):
360449
"""Ensure all required parameters are contained in the parameter table
361450
with no additional ones."""
@@ -558,6 +647,10 @@ def append_overrides(overrides):
558647
CheckModel(),
559648
CheckMeasurementTable(),
560649
CheckConditionTable(),
650+
CheckExperimentTable(),
651+
CheckValidPetabIdColumn("experiment", EXPERIMENT_ID),
652+
CheckValidPetabIdColumn("experiment", CONDITION_ID),
653+
CheckExperimentConditionsExist(),
561654
CheckObservableTable(),
562655
CheckObservablesDoNotShadowModelEntities(),
563656
CheckParameterTable(),

0 commit comments

Comments
 (0)