Skip to content

Commit 99373c4

Browse files
authored
Merge pull request #320 from dweindl/release_0.5.0
Release 0.5.0
2 parents b0e0e54 + 3e36584 commit 99373c4

File tree

16 files changed

+432
-5
lines changed

16 files changed

+432
-5
lines changed

CHANGELOG.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,37 @@
11
# PEtab changelog
22

3+
## 0.5 series
4+
5+
### 0.5.0
6+
7+
**Fixes**
8+
* Circumvent `SettingWithCopyWarning`
9+
10+
by @m-philipps in https://github.com/PEtab-dev/libpetab-python/pull/306
11+
12+
* If `flatten_timepoint_specific_output_overrides` makes the visualization
13+
table invalid, remove it from `Problem`
14+
15+
by @m-philipps in https://github.com/PEtab-dev/libpetab-python/pull/316
16+
17+
**Features**
18+
19+
* Added `petab.v2.models` by @dweindl in https://github.com/PEtab-dev/libpetab-python/pull/302
20+
* Added `petab.v1.priors.priors_to_measurements(...)` for replacing
21+
`objectivePrior*` by observables/measurements
22+
23+
by @dweindl in https://github.com/PEtab-dev/libpetab-python/pull/309, https://github.com/PEtab-dev/libpetab-python/pull/315, https://github.com/PEtab-dev/libpetab-python/pull/317
24+
25+
* Make model id optional for `PySBModel`
26+
27+
by @dweindl in https://github.com/PEtab-dev/libpetab-python/pull/318
28+
29+
* Implemented `Model.__repr__`
30+
31+
by @dweindl in https://github.com/PEtab-dev/libpetab-python/pull/319
32+
33+
**Full Changelog**: https://github.com/PEtab-dev/libpetab-python/compare/v0.4.1...v0.5.0
34+
335
## 0.4 series
436

537
This series contains many changes related to the new `petab.v2` subpackage. `petab.v2` should not be considered stable; the `petab.v2` API may change rapidly until we release libpetab-python v1.0.0.

doc/modules.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ API Reference
2020
petab.v1.observables
2121
petab.v1.parameter_mapping
2222
petab.v1.parameters
23+
petab.v1.priors
2324
petab.v1.problem
2425
petab.v1.sampling
2526
petab.v1.sbml

petab/v1/core.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,20 @@ def flatten_timepoint_specific_output_overrides(
299299
petab_problem.observable_df.index.name = OBSERVABLE_ID
300300
petab_problem.measurement_df = pd.concat(new_measurement_dfs)
301301

302+
# remove visualization df if it uses observables that are not in the
303+
# flattened PEtab problem
304+
if petab_problem.visualization_df is not None:
305+
assert petab_problem.observable_df.index.name == OBSERVABLE_ID
306+
if not all(
307+
petab_problem.observable_df.index.isin(
308+
petab_problem.visualization_df[Y_VALUES]
309+
)
310+
):
311+
petab_problem.visualization_df = None
312+
logger.warning(
313+
"Removing visualization table from flattened PEtab problem."
314+
)
315+
302316

303317
def unflatten_simulation_df(
304318
simulation_df: pd.DataFrame,

petab/v1/models/model.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ class Model(abc.ABC):
1616
def __init__(self):
1717
...
1818

19+
def __repr__(self):
20+
return f"<{self.__class__.__name__} {self.model_id!r}>"
21+
1922
@staticmethod
2023
@abc.abstractmethod
2124
def from_file(filepath_or_buffer: Any, model_id: str) -> Model:

petab/v1/models/pysb_model.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import pysb
1111

12+
from .. import is_valid_identifier
1213
from . import MODEL_TYPE_PYSB
1314
from .model import Model
1415

@@ -53,14 +54,21 @@ class PySBModel(Model):
5354

5455
type_id = MODEL_TYPE_PYSB
5556

56-
def __init__(self, model: pysb.Model, model_id: str):
57+
def __init__(self, model: pysb.Model, model_id: str = None):
5758
super().__init__()
5859

5960
self.model = model
60-
self._model_id = model_id
61+
self._model_id = model_id or self.model.name
62+
63+
if not is_valid_identifier(self._model_id):
64+
raise ValueError(
65+
f"Model ID '{self._model_id}' is not a valid identifier. "
66+
"Either provide a valid identifier or change the model name "
67+
"to a valid PEtab model identifier."
68+
)
6169

6270
@staticmethod
63-
def from_file(filepath_or_buffer, model_id: str):
71+
def from_file(filepath_or_buffer, model_id: str = None):
6472
return PySBModel(
6573
model=_pysb_model_from_path(filepath_or_buffer), model_id=model_id
6674
)

petab/v1/parameters.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,7 @@ def get_priors_from_df(
458458
# get types and parameters of priors from dataframe
459459
par_to_estimate = parameter_df.loc[parameter_df[ESTIMATE] == 1]
460460

461-
if parameter_ids:
461+
if parameter_ids is not None:
462462
try:
463463
par_to_estimate = par_to_estimate.loc[parameter_ids, :]
464464
except KeyError as e:

petab/v1/priors.py

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
"""Functions related to prior handling."""
2+
import copy
3+
4+
import numpy as np
5+
import pandas as pd
6+
7+
from ..v1.C import PREEQUILIBRATION_CONDITION_ID
8+
from . import (
9+
ESTIMATE,
10+
LAPLACE,
11+
LIN,
12+
LOG,
13+
LOG10,
14+
LOG_LAPLACE,
15+
LOG_NORMAL,
16+
MEASUREMENT,
17+
NOISE_DISTRIBUTION,
18+
NOISE_FORMULA,
19+
NOISE_PARAMETERS,
20+
NORMAL,
21+
OBJECTIVE_PRIOR_PARAMETERS,
22+
OBJECTIVE_PRIOR_TYPE,
23+
OBSERVABLE_FORMULA,
24+
OBSERVABLE_ID,
25+
OBSERVABLE_TRANSFORMATION,
26+
PARAMETER_SCALE,
27+
PARAMETER_SCALE_LAPLACE,
28+
PARAMETER_SCALE_NORMAL,
29+
PARAMETER_SEPARATOR,
30+
SIMULATION_CONDITION_ID,
31+
TIME,
32+
Problem,
33+
)
34+
35+
__all__ = ["priors_to_measurements"]
36+
37+
38+
def priors_to_measurements(problem: Problem):
39+
"""Convert priors to measurements.
40+
41+
Reformulate the given problem such that the objective priors are converted
42+
to measurements. This is done by adding a new observable
43+
``prior_{parameter_id}`` for each estimated parameter that has an objective
44+
prior, and adding a corresponding measurement to the measurement table.
45+
The new measurement is the prior distribution itself. The resulting
46+
optimization problem will be equivalent to the original problem.
47+
This is meant to be used for tools that do not support priors.
48+
49+
The conversion involves the probability density function (PDF) of the
50+
prior, the parameters (e.g., location and scale) of that prior PDF, and the
51+
scale and value of the estimated parameter. Currently, `uniform` priors are
52+
not supported by this method. This method creates observables with:
53+
54+
- `observableFormula`: the parameter value on the `parameterScale`
55+
- `observableTransformation`: `log` for `logNormal`/`logLaplace`
56+
distributions, `lin` otherwise
57+
58+
and measurements with:
59+
60+
- `measurement`: the PDF location
61+
- `noiseFormula`: the PDF scale
62+
63+
Arguments
64+
---------
65+
problem:
66+
The problem to be converted.
67+
68+
Returns
69+
-------
70+
The new problem with the priors converted to measurements.
71+
"""
72+
new_problem = copy.deepcopy(problem)
73+
74+
# we only need to consider parameters that are estimated
75+
par_df_tmp = problem.parameter_df.loc[problem.parameter_df[ESTIMATE] == 1]
76+
77+
if (
78+
OBJECTIVE_PRIOR_TYPE not in par_df_tmp
79+
or par_df_tmp.get(OBJECTIVE_PRIOR_TYPE).isna().all()
80+
or OBJECTIVE_PRIOR_PARAMETERS not in par_df_tmp
81+
or par_df_tmp.get(OBJECTIVE_PRIOR_PARAMETERS).isna().all()
82+
):
83+
# nothing to do
84+
return new_problem
85+
86+
def scaled_observable_formula(parameter_id, parameter_scale):
87+
if parameter_scale == LIN:
88+
return parameter_id
89+
if parameter_scale == LOG:
90+
return f"ln({parameter_id})"
91+
if parameter_scale == LOG10:
92+
return f"log10({parameter_id})"
93+
raise ValueError(f"Unknown parameter scale {parameter_scale}.")
94+
95+
new_measurement_dicts = []
96+
new_observable_dicts = []
97+
for _, row in par_df_tmp.iterrows():
98+
prior_type = row[OBJECTIVE_PRIOR_TYPE]
99+
parameter_scale = row.get(PARAMETER_SCALE, LIN)
100+
if pd.isna(prior_type):
101+
if not pd.isna(row[OBJECTIVE_PRIOR_PARAMETERS]):
102+
raise AssertionError(
103+
"Objective prior parameters are set, but prior type is "
104+
"not specified."
105+
)
106+
continue
107+
108+
if "uniform" in prior_type.lower():
109+
# for measurements, "uniform" is not supported yet
110+
# if necessary, this could still be implemented by adding another
111+
# observable/measurement that will produce a constant objective
112+
# offset
113+
raise NotImplementedError("Uniform priors are not supported.")
114+
115+
parameter_id = row.name
116+
prior_parameters = tuple(
117+
map(
118+
float,
119+
row[OBJECTIVE_PRIOR_PARAMETERS].split(PARAMETER_SEPARATOR),
120+
)
121+
)
122+
if len(prior_parameters) != 2:
123+
raise AssertionError(
124+
"Expected two objective prior parameters for parameter "
125+
f"{parameter_id}, but got {prior_parameters}."
126+
)
127+
128+
# create new observable
129+
new_obs_id = f"prior_{parameter_id}"
130+
if new_obs_id in new_problem.observable_df.index:
131+
raise ValueError(
132+
f"Observable ID {new_obs_id}, which is to be "
133+
"created, already exists."
134+
)
135+
new_observable = {
136+
OBSERVABLE_ID: new_obs_id,
137+
OBSERVABLE_FORMULA: scaled_observable_formula(
138+
parameter_id,
139+
parameter_scale if "parameterScale" in prior_type else LIN,
140+
),
141+
NOISE_FORMULA: f"noiseParameter1_{new_obs_id}",
142+
}
143+
if prior_type in (LOG_NORMAL, LOG_LAPLACE):
144+
new_observable[OBSERVABLE_TRANSFORMATION] = LOG
145+
elif OBSERVABLE_TRANSFORMATION in new_problem.observable_df:
146+
# only set default if the column is already present
147+
new_observable[OBSERVABLE_TRANSFORMATION] = LIN
148+
149+
if prior_type in (NORMAL, PARAMETER_SCALE_NORMAL, LOG_NORMAL):
150+
new_observable[NOISE_DISTRIBUTION] = NORMAL
151+
elif prior_type in (LAPLACE, PARAMETER_SCALE_LAPLACE, LOG_LAPLACE):
152+
new_observable[NOISE_DISTRIBUTION] = LAPLACE
153+
else:
154+
raise NotImplementedError(
155+
f"Objective prior type {prior_type} is not implemented."
156+
)
157+
158+
new_observable_dicts.append(new_observable)
159+
160+
# add measurement
161+
# we could just use any condition and time point since the parameter
162+
# value is constant. however, using an existing timepoint and
163+
# (preequilibrationConditionId+)simulationConditionId will avoid
164+
# requiring extra simulations and solver stops in tools that do not
165+
# check for time dependency of the observable. we use the first
166+
# condition/timepoint from the measurement table
167+
new_measurement = {
168+
OBSERVABLE_ID: new_obs_id,
169+
TIME: problem.measurement_df[TIME].iloc[0],
170+
MEASUREMENT: prior_parameters[0],
171+
NOISE_PARAMETERS: prior_parameters[1],
172+
SIMULATION_CONDITION_ID: new_problem.measurement_df[
173+
SIMULATION_CONDITION_ID
174+
].iloc[0],
175+
}
176+
if PREEQUILIBRATION_CONDITION_ID in new_problem.measurement_df:
177+
new_measurement[
178+
PREEQUILIBRATION_CONDITION_ID
179+
] = new_problem.measurement_df[PREEQUILIBRATION_CONDITION_ID].iloc[
180+
0
181+
]
182+
new_measurement_dicts.append(new_measurement)
183+
184+
# remove prior from parameter table
185+
new_problem.parameter_df.loc[
186+
parameter_id, OBJECTIVE_PRIOR_TYPE
187+
] = np.nan
188+
new_problem.parameter_df.loc[
189+
parameter_id, OBJECTIVE_PRIOR_PARAMETERS
190+
] = np.nan
191+
192+
new_problem.observable_df = pd.concat(
193+
[
194+
new_problem.observable_df,
195+
pd.DataFrame(new_observable_dicts).set_index(OBSERVABLE_ID),
196+
]
197+
)
198+
new_problem.measurement_df = pd.concat(
199+
[
200+
new_problem.measurement_df,
201+
pd.DataFrame(new_measurement_dicts),
202+
],
203+
ignore_index=True,
204+
)
205+
return new_problem

petab/v1/visualize/plotting.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ def __init__(
6464
self.data_to_plot.sort_index(inplace=True)
6565

6666
self.conditions = conditions_
67+
if self.conditions is not None:
68+
self.conditions = self.conditions.copy()
6769
self.inf_point = (
6870
np.inf in self.conditions if self.conditions is not None else False
6971
)

petab/v2/models/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
"""Handling of different model types supported by PEtab."""
2+
from ...v1.models import * # noqa: F401, F403

petab/v2/models/model.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
"""PEtab model abstraction"""
2+
from ...v1.models.model import * # noqa: F401, F403

petab/v2/models/pysb_model.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
"""Functions for handling PySB models"""
2+
from ...v1.models.pysb_model import * # noqa: F401, F403

petab/v2/models/sbml_model.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
"""Functions for handling SBML models"""
2+
from ...v1.models.sbml_model import * # noqa: F401, F403

petab/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
"""PEtab library version"""
2-
__version__ = "0.4.1"
2+
__version__ = "0.5.0"

tests/v1/test_model_pysb.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,9 @@ def test_pattern_parsing(uses_pysb):
8787
pattern = pysb.as_complex_pattern(B(s="a") ** c1)
8888
assert pattern_from_string(str(pattern), model).matches(pattern)
8989
assert str(pattern) == str(pattern_from_string("B(s='a') ** c1", model))
90+
91+
92+
def test_pysb_model_repr(uses_pysb):
93+
model = pysb.Model(name="test")
94+
petab_model = PySBModel(model)
95+
assert repr(petab_model) == "<PySBModel 'test'>"

0 commit comments

Comments
 (0)