Skip to content

Commit 9baf981

Browse files
authored
Functions for adding conditions/observables/parameter to Problem (#328)
Add functions for adding individual conditions/observables/parameter/measurements to Problem. This will simplify writing test cases and interactively assembling petab problems. `petab.v2.Problem.add_*` will be added / updated to the new format separately. Related to #220.
1 parent d3e4006 commit 9baf981

File tree

7 files changed

+518
-96
lines changed

7 files changed

+518
-96
lines changed

petab/v1/mapping.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Functionality related to the PEtab entity mapping table"""
2+
# TODO: Move to petab.v2.mapping
23
from pathlib import Path
34

45
import pandas as pd

petab/v1/problem.py

Lines changed: 180 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33

44
import os
55
import tempfile
6-
from collections.abc import Iterable
6+
from collections.abc import Iterable, Sequence
77
from math import nan
8+
from numbers import Number
89
from pathlib import Path, PurePosixPath
910
from typing import TYPE_CHECKING
1011
from warnings import warn
@@ -1005,3 +1006,181 @@ def n_priors(self) -> int:
10051006
return 0
10061007

10071008
return self.parameter_df[OBJECTIVE_PRIOR_PARAMETERS].notna().sum()
1009+
1010+
def add_condition(self, id_: str, name: str = None, **kwargs):
1011+
"""Add a simulation condition to the problem.
1012+
1013+
Arguments:
1014+
id_: The condition id
1015+
name: The condition name
1016+
kwargs: Parameter, value pairs to add to the condition table.
1017+
"""
1018+
record = {CONDITION_ID: [id_], **kwargs}
1019+
if name is not None:
1020+
record[CONDITION_NAME] = name
1021+
tmp_df = pd.DataFrame(record).set_index([CONDITION_ID])
1022+
self.condition_df = (
1023+
pd.concat([self.condition_df, tmp_df])
1024+
if self.condition_df is not None
1025+
else tmp_df
1026+
)
1027+
1028+
def add_observable(
1029+
self,
1030+
id_: str,
1031+
formula: str | float | int,
1032+
noise_formula: str | float | int = None,
1033+
noise_distribution: str = None,
1034+
transform: str = None,
1035+
name: str = None,
1036+
**kwargs,
1037+
):
1038+
"""Add an observable to the problem.
1039+
1040+
Arguments:
1041+
id_: The observable id
1042+
formula: The observable formula
1043+
noise_formula: The noise formula
1044+
noise_distribution: The noise distribution
1045+
transform: The observable transformation
1046+
name: The observable name
1047+
kwargs: additional columns/values to add to the observable table
1048+
1049+
"""
1050+
record = {
1051+
OBSERVABLE_ID: [id_],
1052+
OBSERVABLE_FORMULA: [formula],
1053+
}
1054+
if name is not None:
1055+
record[OBSERVABLE_NAME] = [name]
1056+
if noise_formula is not None:
1057+
record[NOISE_FORMULA] = [noise_formula]
1058+
if noise_distribution is not None:
1059+
record[NOISE_DISTRIBUTION] = [noise_distribution]
1060+
if transform is not None:
1061+
record[OBSERVABLE_TRANSFORMATION] = [transform]
1062+
record.update(kwargs)
1063+
1064+
tmp_df = pd.DataFrame(record).set_index([OBSERVABLE_ID])
1065+
self.observable_df = (
1066+
pd.concat([self.observable_df, tmp_df])
1067+
if self.observable_df is not None
1068+
else tmp_df
1069+
)
1070+
1071+
def add_parameter(
1072+
self,
1073+
id_: str,
1074+
estimated: bool | str | int = True,
1075+
nominal_value=None,
1076+
scale: str = None,
1077+
lb: Number = None,
1078+
ub: Number = None,
1079+
init_prior_type: str = None,
1080+
init_prior_pars: str | Sequence = None,
1081+
obj_prior_type: str = None,
1082+
obj_prior_pars: str | Sequence = None,
1083+
**kwargs,
1084+
):
1085+
"""Add a parameter to the problem.
1086+
1087+
Arguments:
1088+
id_: The parameter id
1089+
estimated: Whether the parameter is estimated
1090+
nominal_value: The nominal value of the parameter
1091+
scale: The parameter scale
1092+
lb: The lower bound of the parameter
1093+
ub: The upper bound of the parameter
1094+
init_prior_type: The type of the initialization prior distribution
1095+
init_prior_pars: The parameters of the initialization prior
1096+
distribution
1097+
obj_prior_type: The type of the objective prior distribution
1098+
obj_prior_pars: The parameters of the objective prior distribution
1099+
kwargs: additional columns/values to add to the parameter table
1100+
"""
1101+
record = {
1102+
PARAMETER_ID: [id_],
1103+
}
1104+
if estimated is not None:
1105+
record[ESTIMATE] = [
1106+
int(estimated)
1107+
if isinstance(estimated, bool | int)
1108+
else estimated
1109+
]
1110+
if nominal_value is not None:
1111+
record[NOMINAL_VALUE] = [nominal_value]
1112+
if scale is not None:
1113+
record[PARAMETER_SCALE] = [scale]
1114+
if lb is not None:
1115+
record[LOWER_BOUND] = [lb]
1116+
if ub is not None:
1117+
record[UPPER_BOUND] = [ub]
1118+
if init_prior_type is not None:
1119+
record[INITIALIZATION_PRIOR_TYPE] = [init_prior_type]
1120+
if init_prior_pars is not None:
1121+
if not isinstance(init_prior_pars, str):
1122+
init_prior_pars = PARAMETER_SEPARATOR.join(
1123+
map(str, init_prior_pars)
1124+
)
1125+
record[INITIALIZATION_PRIOR_PARAMETERS] = [init_prior_pars]
1126+
if obj_prior_type is not None:
1127+
record[OBJECTIVE_PRIOR_TYPE] = [obj_prior_type]
1128+
if obj_prior_pars is not None:
1129+
if not isinstance(obj_prior_pars, str):
1130+
obj_prior_pars = PARAMETER_SEPARATOR.join(
1131+
map(str, obj_prior_pars)
1132+
)
1133+
record[OBJECTIVE_PRIOR_PARAMETERS] = [obj_prior_pars]
1134+
record.update(kwargs)
1135+
1136+
tmp_df = pd.DataFrame(record).set_index([PARAMETER_ID])
1137+
self.parameter_df = (
1138+
pd.concat([self.parameter_df, tmp_df])
1139+
if self.parameter_df is not None
1140+
else tmp_df
1141+
)
1142+
1143+
def add_measurement(
1144+
self,
1145+
obs_id: str,
1146+
sim_cond_id: str,
1147+
time: float,
1148+
measurement: float,
1149+
observable_parameters: Sequence[str] = None,
1150+
noise_parameters: Sequence[str] = None,
1151+
preeq_cond_id: str = None,
1152+
):
1153+
"""Add a measurement to the problem.
1154+
1155+
Arguments:
1156+
obs_id: The observable ID
1157+
sim_cond_id: The simulation condition ID
1158+
time: The measurement time
1159+
measurement: The measurement value
1160+
observable_parameters: The observable parameters
1161+
noise_parameters: The noise parameters
1162+
preeq_cond_id: The pre-equilibration condition ID
1163+
"""
1164+
record = {
1165+
OBSERVABLE_ID: [obs_id],
1166+
SIMULATION_CONDITION_ID: [sim_cond_id],
1167+
TIME: [time],
1168+
MEASUREMENT: [measurement],
1169+
}
1170+
if observable_parameters is not None:
1171+
record[OBSERVABLE_PARAMETERS] = [
1172+
PARAMETER_SEPARATOR.join(observable_parameters)
1173+
]
1174+
if noise_parameters is not None:
1175+
record[NOISE_PARAMETERS] = [
1176+
PARAMETER_SEPARATOR.join(noise_parameters)
1177+
]
1178+
if preeq_cond_id is not None:
1179+
record[PREEQUILIBRATION_CONDITION_ID] = [preeq_cond_id]
1180+
1181+
tmp_df = pd.DataFrame(record)
1182+
self.measurement_df = (
1183+
pd.concat([self.measurement_df, tmp_df])
1184+
if self.measurement_df is not None
1185+
else tmp_df
1186+
)

petab/v2/petab1to2.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import shutil
33
from itertools import chain
44
from pathlib import Path
5+
from urllib.parse import urlparse
56

67
from pandas.io.common import get_handle, is_url
78

@@ -76,7 +77,7 @@ def petab1to2(yaml_config: Path | str, output_dir: Path | str = None):
7677
# condition tables, observable tables, SBML files, parameter table:
7778
# no changes - just copy
7879
file = yaml_config[C.PARAMETER_FILE]
79-
_copy_file(get_src_path(file), get_dest_path(file))
80+
_copy_file(get_src_path(file), Path(get_dest_path(file)))
8081

8182
for problem_config in yaml_config[C.PROBLEMS]:
8283
for file in chain(
@@ -89,7 +90,7 @@ def petab1to2(yaml_config: Path | str, output_dir: Path | str = None):
8990
problem_config.get(C.MEASUREMENT_FILES, []),
9091
problem_config.get(C.VISUALIZATION_FILES, []),
9192
):
92-
_copy_file(get_src_path(file), get_dest_path(file))
93+
_copy_file(get_src_path(file), Path(get_dest_path(file)))
9394

9495
# TODO: Measurements: preequilibration to experiments/timecourses once
9596
# finalized
@@ -131,15 +132,23 @@ def _update_yaml(yaml_config: dict) -> dict:
131132
return yaml_config
132133

133134

134-
def _copy_file(src: Path | str, dest: Path | str):
135+
def _copy_file(src: Path | str, dest: Path):
135136
"""Copy file."""
136-
src = str(src)
137-
dest = str(dest)
137+
# src might be a URL - convert to Path if local
138+
src_url = urlparse(src)
139+
if not src_url.scheme:
140+
src = Path(src)
141+
elif src_url.scheme == "file" and not src_url.netloc:
142+
src = Path(src.removeprefix("file:/"))
138143

139144
if is_url(src):
140145
with get_handle(src, mode="r") as src_handle:
141146
with open(dest, "w") as dest_handle:
142147
dest_handle.write(src_handle.handle.read())
143148
return
144149

145-
shutil.copy(str(src), str(dest))
150+
try:
151+
if dest.samefile(src):
152+
return
153+
except FileNotFoundError:
154+
shutil.copy(str(src), str(dest))

0 commit comments

Comments
 (0)