Skip to content

Commit f1b6f0c

Browse files
committed
..
1 parent 0cf7ca3 commit f1b6f0c

File tree

2 files changed

+70
-21
lines changed

2 files changed

+70
-21
lines changed

doc/modules.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ API Reference
3232
petab.v1.yaml
3333
petab.v2
3434
petab.v2.C
35+
petab.v2.converters
3536
petab.v2.core
3637
petab.v2.experiments
3738
petab.v2.lint

petab/v2/converters.py

Lines changed: 69 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,44 +2,45 @@
22

33
from __future__ import annotations
44

5+
import warnings
56
from copy import deepcopy
67
from math import inf
78

89
import libsbml
9-
from sbmlmath import set_math
10+
from sbmlmath import sbml_math_to_sympy, set_math
1011

1112
from .core import Change, Condition, Experiment, ExperimentPeriod
1213
from .models._sbml_utils import add_sbml_parameter, check
1314
from .models.sbml_model import SbmlModel
1415
from .problem import Problem
1516

17+
__all__ = ["ExperimentsToEventsConverter"]
18+
1619

1720
class ExperimentsToEventsConverter:
1821
"""Convert PEtab experiments to SBML events.
1922
2023
For an SBML-model-based PEtab problem, this class converts the PEtab
2124
experiments to events as far as possible.
2225
23-
Currently, this assumes that there is no other event in the model
24-
that could trigger at the same time as the events created here.
25-
I.e., the events responsible for applying PEtab condition changes
26-
don't have a priority assigned that would guarantee that they are executed
27-
before any pre-existing events.
26+
If the model already contains events, PEtab events are added with a higher
27+
priority than the existing events to guarantee that PEtab condition changes
28+
are applied before any pre-existing assignments.
2829
2930
The PEtab problem must not contain any identifiers starting with
3031
``_petab``.
3132
3233
All periods and condition changes that are represented by events
3334
will be removed from the condition table.
34-
Each experiment will have at most one period with a start time of -inf
35+
Each experiment will have at most one period with a start time of ``-inf``
3536
and one period with a finite start time. The associated changes with
3637
these periods are only the steady-state pre-simulation indicator
3738
(if necessary), and the experiment indicator parameter.
3839
"""
3940

4041
#: ID of the parameter that indicates whether the model is in
41-
# the steady-state pre-simulation phase (1) or not (0).
42-
PRE_STEADY_STATE_INDICATOR = "_petab_pre_steady_state_indicator"
42+
# the steady-state pre-simulation phase (1) or not (0).
43+
PRE_SIM_INDICATOR = "_petab_pre_simulation_indicator"
4344

4445
def __init__(self, problem: Problem):
4546
"""Initialize the converter.
@@ -50,30 +51,81 @@ def __init__(self, problem: Problem):
5051
if not isinstance(problem.model, SbmlModel):
5152
raise ValueError("Only SBML models are supported.")
5253

53-
self._problem = problem
54-
self._model = problem.model.sbml_model
55-
self._presim_indicator = self.PRE_STEADY_STATE_INDICATOR
54+
self._original_problem = problem
55+
self._new_problem = deepcopy(self._original_problem)
56+
57+
self._model = self._new_problem.model.sbml_model
58+
self._presim_indicator = self.PRE_SIM_INDICATOR
59+
60+
# The maximum event priority that was found in the unprocessed model.
61+
self._max_event_priority = None
62+
# The priority that will be used for the PEtab events.
63+
self._petab_event_priority = None
64+
65+
self._preprocess()
5666

67+
def _preprocess(self):
68+
"""Check whether we can handle the given problem and store some model
69+
information."""
5770
model = self._model
5871
if model.getLevel() < 3:
72+
# try to upgrade the SBML model
5973
if not model.getSBMLDocument().setLevelAndVersion(3, 2):
6074
raise ValueError(
6175
"Cannot handle SBML models with SBML level < 3, "
6276
"because they do not support initial values for event "
6377
"triggers and automatic upconversion failed."
6478
)
6579

80+
# Collect event priorities
81+
event_priorities = {
82+
ev.getId() or str(ev): sbml_math_to_sympy(ev.getPriority())
83+
for ev in model.getListOfEvents()
84+
if ev.getPriority() and ev.getPriority().getMath() is not None
85+
}
86+
87+
# Check for non-constant event priorities and track the maximum
88+
# priority used so far.
89+
for e, priority in event_priorities.items():
90+
if priority.free_symbols:
91+
# We'd need to find the maximum priority of all events,
92+
# which is challenging/impossible to do in general.
93+
raise NotImplementedError(
94+
f"Event `{e}` has a non-constant priority: {priority}. "
95+
"This is currently not supported."
96+
)
97+
self._max_event_priority = max(
98+
self._max_event_priority or 0, float(priority)
99+
)
100+
101+
self._petab_event_priority = (
102+
self._max_event_priority + 1
103+
if self._max_event_priority is not None
104+
else None
105+
)
106+
# Check for undefined event priorities and warn
107+
for event in model.getListOfEvents():
108+
if (prio := event.getPriority()) and prio.getMath() is None:
109+
warnings.warn(
110+
f"Event `{event.getId()}` has no priority set. "
111+
"Make sure that this event cannot trigger at the time of "
112+
"PEtab condition change, otherwise the behavior is "
113+
"undefined.",
114+
stacklevel=1,
115+
)
116+
66117
def convert(self) -> Problem:
67118
"""Convert the PEtab experiments to SBML events.
68119
69120
:return: The converted PEtab problem.
70121
"""
71-
problem = deepcopy(self._problem)
72122

73123
self._add_presimulation_indicator()
74124

125+
problem = self._new_problem
75126
for experiment in problem.experiment_table.experiments:
76127
self._convert_experiment(problem, experiment)
128+
77129
self._add_indicators_to_conditions(problem)
78130

79131
validation_results = problem.validate()
@@ -99,9 +151,7 @@ def _convert_experiment(self, problem: Problem, experiment: Experiment):
99151
kept_periods = []
100152
for i_period, period in enumerate(experiment.periods):
101153
# check for non-zero initial times of the first period
102-
if (
103-
i_period == 0 or (i_period == 1 and has_presimulation)
104-
) and period.time != 0:
154+
if (i_period == int(has_presimulation)) and period.time != 0:
105155
# TODO: we could address that by offsetting all occurrences of
106156
# the SBML time in the model (except for the newly added
107157
# events triggers). Or we better just leave it to the
@@ -161,17 +211,15 @@ def _create_period_begin_event(
161211
# TODO: for now, add separate events for each experiment x period,
162212
# this could be optimized to reuse events
163213

164-
# TODO if there is already some event that could trigger
165-
# at this time, we need event priorities. This is difficult to
166-
# implement, though, since in general, we can't know the maximum
167-
# priority of the other events, unless they are static.
168-
169214
ev = self._model.createEvent()
170215
check(ev.setId(f"_petab_event_{experiment.id}_{i_period}"))
171216
check(ev.setUseValuesFromTriggerTime(True))
172217
trigger = ev.createTrigger()
173218
check(trigger.setInitialValue(False)) # may trigger at t=0
174219
check(trigger.setPersistent(True))
220+
if self._petab_event_priority is not None:
221+
priority = ev.createPriority()
222+
set_math(priority, self._petab_event_priority)
175223

176224
exp_ind_id = self.get_experiment_indicator(experiment.id)
177225

0 commit comments

Comments
 (0)