Skip to content

Commit 4d5e790

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 9a4efb4 commit 4d5e790

File tree

2 files changed

+187
-0
lines changed

2 files changed

+187
-0
lines changed

petab/v2/problem.py

Lines changed: 132 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,133 @@ 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])

tests/v2/test_problem.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
11
import tempfile
22
from pathlib import Path
33

4+
import numpy as np
45
import pandas as pd
6+
from pandas.testing import assert_frame_equal
57

68
import petab.v2 as petab
79
from petab.v2 import Problem
810
from petab.v2.C import (
911
CONDITION_ID,
12+
ESTIMATE,
13+
LOWER_BOUND,
1014
MEASUREMENT,
1115
NOISE_FORMULA,
16+
NOMINAL_VALUE,
1217
OBSERVABLE_FORMULA,
1318
OBSERVABLE_ID,
19+
PARAMETER_ID,
1420
SIMULATION_CONDITION_ID,
1521
TIME,
22+
UPPER_BOUND,
1623
)
1724

1825

@@ -105,3 +112,51 @@ def test_problem_from_yaml_multiple_files():
105112
assert petab_problem.measurement_df.shape[0] == 2
106113
assert petab_problem.observable_df.shape[0] == 2
107114
assert petab_problem.condition_df.shape[0] == 2
115+
116+
117+
def test_modify_problem():
118+
"""Test modifying a problem via the API."""
119+
problem = Problem()
120+
problem.add_condition("condition1", parameter1=1)
121+
problem.add_condition("condition2", parameter2=2)
122+
123+
exp_condition_df = pd.DataFrame(
124+
data={
125+
CONDITION_ID: ["condition1", "condition2"],
126+
"parameter1": [1.0, np.nan],
127+
"parameter2": [np.nan, 2.0],
128+
}
129+
).set_index([CONDITION_ID])
130+
assert_frame_equal(
131+
problem.condition_df, exp_condition_df, check_dtype=False
132+
)
133+
134+
problem.add_observable("observable1", "1")
135+
problem.add_observable("observable2", "2", noise_formula=2.2)
136+
137+
exp_observable_df = pd.DataFrame(
138+
data={
139+
OBSERVABLE_ID: ["observable1", "observable2"],
140+
OBSERVABLE_FORMULA: ["1", "2"],
141+
NOISE_FORMULA: [np.nan, 2.2],
142+
}
143+
).set_index([OBSERVABLE_ID])
144+
assert_frame_equal(
145+
problem.observable_df, exp_observable_df, check_dtype=False
146+
)
147+
148+
problem.add_parameter("parameter1", 1, 0, lb=1, ub=2)
149+
problem.add_parameter("parameter2", False, 2)
150+
151+
exp_parameter_df = pd.DataFrame(
152+
data={
153+
PARAMETER_ID: ["parameter1", "parameter2"],
154+
ESTIMATE: [1, 0],
155+
NOMINAL_VALUE: [0.0, 2.0],
156+
LOWER_BOUND: [1.0, np.nan],
157+
UPPER_BOUND: [2.0, np.nan],
158+
}
159+
).set_index([PARAMETER_ID])
160+
assert_frame_equal(
161+
problem.parameter_df, exp_parameter_df, check_dtype=False
162+
)

0 commit comments

Comments
 (0)