Skip to content

Commit 78e32c0

Browse files
committed
Functions for adding conditions/observables/parameter to Problem
Will simplify writing test cases and interactively assembling petab problems. To be extended. Related to #220.
1 parent d3e4006 commit 78e32c0

File tree

4 files changed

+461
-87
lines changed

4 files changed

+461
-87
lines changed

petab/v1/problem.py

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

petab/v2/problem.py

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
import logging
55
import os
66
import tempfile
7+
from collections.abc import Sequence
78
from math import nan
9+
from numbers import Number
810
from pathlib import Path
911
from typing import TYPE_CHECKING
1012

@@ -724,3 +726,177 @@ def validate(
724726
break
725727

726728
return validation_results
729+
730+
def add_condition(self, id_: str, name: str = None, **kwargs):
731+
"""Add a simulation condition to the problem.
732+
733+
Arguments:
734+
id_: The condition id
735+
name: The condition name
736+
kwargs: Parameter, value pairs to add to the condition table.
737+
"""
738+
record = {CONDITION_ID: [id_], **kwargs}
739+
if name is not None:
740+
record[CONDITION_NAME] = name
741+
tmp_df = pd.DataFrame(record).set_index([CONDITION_ID])
742+
if self.condition_df is None:
743+
self.condition_df = tmp_df
744+
else:
745+
self.condition_df = pd.concat([self.condition_df, tmp_df])
746+
747+
def add_observable(
748+
self,
749+
id_: str,
750+
formula: str,
751+
noise_formula: str | float | int = None,
752+
noise_distribution: str = None,
753+
transform: str = None,
754+
name: str = None,
755+
**kwargs,
756+
):
757+
"""Add an observable to the problem.
758+
759+
Arguments:
760+
id_: The observable id
761+
formula: The observable formula
762+
noise_formula: The noise formula
763+
noise_distribution: The noise distribution
764+
transform: The observable transformation
765+
name: The observable name
766+
kwargs: additional columns/values to add to the observable table
767+
768+
"""
769+
record = {
770+
OBSERVABLE_ID: [id_],
771+
OBSERVABLE_FORMULA: [formula],
772+
}
773+
if name is not None:
774+
record[OBSERVABLE_NAME] = [name]
775+
if noise_formula is not None:
776+
record[NOISE_FORMULA] = [noise_formula]
777+
if noise_distribution is not None:
778+
record[NOISE_DISTRIBUTION] = [noise_distribution]
779+
if transform is not None:
780+
record[OBSERVABLE_TRANSFORMATION] = [transform]
781+
record.update(kwargs)
782+
783+
tmp_df = pd.DataFrame(record).set_index([OBSERVABLE_ID])
784+
if self.observable_df is None:
785+
self.observable_df = tmp_df
786+
else:
787+
self.observable_df = pd.concat([self.observable_df, tmp_df])
788+
789+
def add_parameter(
790+
self,
791+
id_: str,
792+
estimated: bool | str | int = True,
793+
nominal_value=None,
794+
scale: str = None,
795+
lb: Number = None,
796+
ub: Number = None,
797+
init_prior_type: str = None,
798+
init_prior_pars: str | Sequence = None,
799+
obj_prior_type: str = None,
800+
obj_prior_pars: str | Sequence = None,
801+
**kwargs,
802+
):
803+
"""Add a parameter to the problem.
804+
805+
Arguments:
806+
id_: The parameter id
807+
estimated: Whether the parameter is estimated
808+
nominal_value: The nominal value of the parameter
809+
scale: The parameter scale
810+
lb: The lower bound of the parameter
811+
ub: The upper bound of the parameter
812+
init_prior_type: The type of the initialization prior distribution
813+
init_prior_pars: The parameters of the initialization prior
814+
distribution
815+
obj_prior_type: The type of the objective prior distribution
816+
obj_prior_pars: The parameters of the objective prior distribution
817+
kwargs: additional columns/values to add to the parameter table
818+
"""
819+
record = {
820+
PARAMETER_ID: [id_],
821+
}
822+
if estimated is not None:
823+
record[ESTIMATE] = [
824+
int(estimated)
825+
if isinstance(estimated, bool | int)
826+
else estimated
827+
]
828+
if nominal_value is not None:
829+
record[NOMINAL_VALUE] = [nominal_value]
830+
if scale is not None:
831+
record[PARAMETER_SCALE] = [scale]
832+
if lb is not None:
833+
record[LOWER_BOUND] = [lb]
834+
if ub is not None:
835+
record[UPPER_BOUND] = [ub]
836+
if init_prior_type is not None:
837+
record[INITIALIZATION_PRIOR_TYPE] = [init_prior_type]
838+
if init_prior_pars is not None:
839+
if not isinstance(init_prior_pars, str):
840+
init_prior_pars = PARAMETER_SEPARATOR.join(
841+
map(str, init_prior_pars)
842+
)
843+
record[INITIALIZATION_PRIOR_PARAMETERS] = [init_prior_pars]
844+
if obj_prior_type is not None:
845+
record[OBJECTIVE_PRIOR_TYPE] = [obj_prior_type]
846+
if obj_prior_pars is not None:
847+
if not isinstance(obj_prior_pars, str):
848+
obj_prior_pars = PARAMETER_SEPARATOR.join(
849+
map(str, obj_prior_pars)
850+
)
851+
record[OBJECTIVE_PRIOR_PARAMETERS] = [obj_prior_pars]
852+
record.update(kwargs)
853+
854+
tmp_df = pd.DataFrame(record).set_index([PARAMETER_ID])
855+
if self.parameter_df is None:
856+
self.parameter_df = tmp_df
857+
else:
858+
self.parameter_df = pd.concat([self.parameter_df, tmp_df])
859+
860+
def add_measurement(
861+
self,
862+
obs_id: str,
863+
sim_cond_id: str,
864+
time: float,
865+
measurement: float,
866+
observable_parameters: Sequence[str] = None,
867+
noise_parameters: Sequence[str] = None,
868+
preeq_cond_id: str = None,
869+
):
870+
"""Add a measurement to the problem.
871+
872+
Arguments:
873+
obs_id: The observable ID
874+
sim_cond_id: The simulation condition ID
875+
time: The measurement time
876+
measurement: The measurement value
877+
observable_parameters: The observable parameters
878+
noise_parameters: The noise parameters
879+
preeq_cond_id: The pre-equilibration condition ID
880+
"""
881+
record = {
882+
OBSERVABLE_ID: [obs_id],
883+
SIMULATION_CONDITION_ID: [sim_cond_id],
884+
TIME: [time],
885+
MEASUREMENT: [measurement],
886+
}
887+
if observable_parameters is not None:
888+
record[OBSERVABLE_PARAMETERS] = [
889+
PARAMETER_SEPARATOR.join(observable_parameters)
890+
]
891+
if noise_parameters is not None:
892+
record[NOISE_PARAMETERS] = [
893+
PARAMETER_SEPARATOR.join(noise_parameters)
894+
]
895+
if preeq_cond_id is not None:
896+
record[PREEQUILIBRATION_CONDITION_ID] = [preeq_cond_id]
897+
898+
tmp_df = pd.DataFrame(record)
899+
if self.measurement_df is None:
900+
self.measurement_df = tmp_df
901+
else:
902+
self.measurement_df = pd.concat([self.measurement_df, tmp_df])

0 commit comments

Comments
 (0)