Skip to content

Commit 7607d30

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 To be complemented by separate pull requests.
1 parent d3e4006 commit 7607d30

File tree

10 files changed

+184
-3
lines changed

10 files changed

+184
-3
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/v1/C.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,8 @@
337337
MODEL_LANGUAGE = "language"
338338
#: Condition files key in the YAML file
339339
CONDITION_FILES = "condition_files"
340+
#: Experiment files key in the YAML file
341+
EXPERIMENT_FILES = "experiment_files"
340342
#: Measurement files key in the YAML file
341343
MEASUREMENT_FILES = "measurement_files"
342344
#: Observable files key in the YAML file

petab/v2/C.py

Lines changed: 37 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,38 @@
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+
]
131168

132169
# OBSERVABLES
133170

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/problem.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import logging
55
import os
66
import tempfile
7+
import warnings
78
from math import nan
89
from pathlib import Path
910
from typing import TYPE_CHECKING
@@ -24,6 +25,7 @@
2425
from ..v1.C import * # noqa: F403
2526
from ..v1.models.model import Model, model_factory
2627
from ..v1.yaml import get_path_prefix
28+
from . import experiments
2729

2830
if TYPE_CHECKING:
2931
from ..v2.lint import ValidationIssue, ValidationResultList, ValidationTask
@@ -38,6 +40,7 @@ class Problem:
3840
3941
- model
4042
- condition table
43+
- experiment table
4144
- measurement table
4245
- parameter table
4346
- observables table
@@ -47,6 +50,7 @@ class Problem:
4750
4851
Parameters:
4952
condition_df: PEtab condition table
53+
experiment_df: PEtab experiment table
5054
measurement_df: PEtab measurement table
5155
parameter_df: PEtab parameter table
5256
observable_df: PEtab observable table
@@ -60,6 +64,7 @@ def __init__(
6064
self,
6165
model: Model = None,
6266
condition_df: pd.DataFrame = None,
67+
experiment_df: pd.DataFrame = None,
6368
measurement_df: pd.DataFrame = None,
6469
parameter_df: pd.DataFrame = None,
6570
visualization_df: pd.DataFrame = None,
@@ -70,6 +75,7 @@ def __init__(
7075
from ..v2.lint import default_validation_tasks
7176

7277
self.condition_df: pd.DataFrame | None = condition_df
78+
self.experiment_df: pd.DataFrame | None = experiment_df
7379
self.measurement_df: pd.DataFrame | None = measurement_df
7480
self.parameter_df: pd.DataFrame | None = parameter_df
7581
self.visualization_df: pd.DataFrame | None = visualization_df
@@ -81,8 +87,22 @@ def __init__(
8187
ValidationTask
8288
] = default_validation_tasks.copy()
8389

90+
if self.experiment_df is not None:
91+
warnings.warn(
92+
"The experiment table is not yet supported and "
93+
"will be ignored.",
94+
stacklevel=2,
95+
)
96+
8497
def __str__(self):
8598
model = f"with model ({self.model})" if self.model else "without model"
99+
100+
experiments = (
101+
f"{self.experiment_df.shape[0]} experiments"
102+
if self.experiment_df is not None
103+
else "without experiments table"
104+
)
105+
86106
conditions = (
87107
f"{self.condition_df.shape[0]} conditions"
88108
if self.condition_df is not None
@@ -112,8 +132,8 @@ def __str__(self):
112132
parameters = "without parameter_df table"
113133

114134
return (
115-
f"PEtab Problem {model}, {conditions}, {observables}, "
116-
f"{measurements}, {parameters}"
135+
f"PEtab Problem {model}, {conditions}, {experiments}, "
136+
f"{observables}, {measurements}, {parameters}"
117137
)
118138

119139
@staticmethod
@@ -230,6 +250,16 @@ def get_path(filename):
230250
else None
231251
)
232252

253+
experiment_files = [
254+
get_path(f) for f in problem0.get(EXPERIMENT_FILES, [])
255+
]
256+
# If there are multiple tables, we will merge them
257+
experiment_df = (
258+
core.concat_tables(experiment_files, experiments.get_experiment_df)
259+
if experiment_files
260+
else None
261+
)
262+
233263
visualization_files = [
234264
get_path(f) for f in problem0.get(VISUALIZATION_FILES, [])
235265
]
@@ -260,6 +290,7 @@ def get_path(filename):
260290

261291
return Problem(
262292
condition_df=condition_df,
293+
experiment_df=experiment_df,
263294
measurement_df=measurement_df,
264295
parameter_df=parameter_df,
265296
observable_df=observable_df,

pytest.ini

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@ filterwarnings =
77
ignore:Support for PEtab2.0 is experimental:UserWarning
88
ignore:.*inspect.getargspec\(\) is deprecated.*:DeprecationWarning
99
ignore:.*Passing unrecognized arguments to super\(PyDevIPCompleter6\).*:DeprecationWarning
10+
# TODO: until we have proper v2 support
11+
ignore:The experiment table is not yet supported and will be ignored:UserWarning

tests/v2/test_experiments.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""Tests related to ``petab.v2.experiments``."""
2+
from tempfile import NamedTemporaryFile
3+
4+
import pandas as pd
5+
6+
from petab.v2.C import CONDITION_ID, EXPERIMENT_ID, TIME
7+
from petab.v2.experiments import get_experiment_df, write_experiment_df
8+
9+
10+
def test_experiment_df_io():
11+
# Test None
12+
assert get_experiment_df(None) is None
13+
14+
# Test DataFrame
15+
df = pd.DataFrame(
16+
{
17+
EXPERIMENT_ID: ["e1", "e2"],
18+
CONDITION_ID: ["c1", "c2"],
19+
TIME: [0, 1],
20+
}
21+
)
22+
df = get_experiment_df(df)
23+
assert EXPERIMENT_ID not in df.columns
24+
assert df.shape == (2, 2)
25+
26+
# Test writing to file and round trip
27+
with NamedTemporaryFile() as tmp:
28+
write_experiment_df(df, tmp.name)
29+
df2 = get_experiment_df(tmp.name)
30+
assert df.equals(df2)

tests/v2/test_problem.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from petab.v2 import Problem
88
from petab.v2.C import (
99
CONDITION_ID,
10+
EXPERIMENT_ID,
1011
MEASUREMENT,
1112
NOISE_FORMULA,
1213
OBSERVABLE_FORMULA,
@@ -54,6 +55,7 @@ def test_problem_from_yaml_multiple_files():
5455
measurement_files: [measurements1.tsv, measurements2.tsv]
5556
observable_files: [observables1.tsv, observables2.tsv]
5657
model_files:
58+
experiment_files: [experiments1.tsv, experiments2.tsv]
5759
"""
5860

5961
with tempfile.TemporaryDirectory() as tmpdir:
@@ -72,6 +74,17 @@ def test_problem_from_yaml_multiple_files():
7274
condition_df, Path(tmpdir, f"conditions{i}.tsv")
7375
)
7476

77+
experiment_df = pd.DataFrame(
78+
{
79+
EXPERIMENT_ID: [f"experiment{i}"],
80+
TIME: [0],
81+
CONDITION_ID: [f"condition{i}"],
82+
}
83+
).set_index([EXPERIMENT_ID])
84+
petab.write_experiment_df(
85+
experiment_df, Path(tmpdir, f"experiments{i}.tsv")
86+
)
87+
7588
measurement_df = pd.DataFrame(
7689
{
7790
SIMULATION_CONDITION_ID: [f"condition{i}"],
@@ -105,3 +118,4 @@ def test_problem_from_yaml_multiple_files():
105118
assert petab_problem.measurement_df.shape[0] == 2
106119
assert petab_problem.observable_df.shape[0] == 2
107120
assert petab_problem.condition_df.shape[0] == 2
121+
assert petab_problem.experiment_df.shape[0] == 2

0 commit comments

Comments
 (0)