2
2
3
3
from __future__ import annotations
4
4
5
+ import warnings
5
6
from copy import deepcopy
6
7
from math import inf
7
8
8
9
import libsbml
9
- from sbmlmath import set_math
10
+ from sbmlmath import sbml_math_to_sympy , set_math
10
11
11
12
from .core import Change , Condition , Experiment , ExperimentPeriod
12
13
from .models ._sbml_utils import add_sbml_parameter , check
13
14
from .models .sbml_model import SbmlModel
14
15
from .problem import Problem
15
16
17
+ __all__ = ["ExperimentsToEventsConverter" ]
18
+
16
19
17
20
class ExperimentsToEventsConverter :
18
21
"""Convert PEtab experiments to SBML events.
19
22
20
23
For an SBML-model-based PEtab problem, this class converts the PEtab
21
24
experiments to events as far as possible.
22
25
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.
28
29
29
30
The PEtab problem must not contain any identifiers starting with
30
31
``_petab``.
31
32
32
33
All periods and condition changes that are represented by events
33
34
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``
35
36
and one period with a finite start time. The associated changes with
36
37
these periods are only the steady-state pre-simulation indicator
37
38
(if necessary), and the experiment indicator parameter.
38
39
"""
39
40
40
41
#: 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 "
43
44
44
45
def __init__ (self , problem : Problem ):
45
46
"""Initialize the converter.
@@ -50,30 +51,81 @@ def __init__(self, problem: Problem):
50
51
if not isinstance (problem .model , SbmlModel ):
51
52
raise ValueError ("Only SBML models are supported." )
52
53
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 ()
56
66
67
+ def _preprocess (self ):
68
+ """Check whether we can handle the given problem and store some model
69
+ information."""
57
70
model = self ._model
58
71
if model .getLevel () < 3 :
72
+ # try to upgrade the SBML model
59
73
if not model .getSBMLDocument ().setLevelAndVersion (3 , 2 ):
60
74
raise ValueError (
61
75
"Cannot handle SBML models with SBML level < 3, "
62
76
"because they do not support initial values for event "
63
77
"triggers and automatic upconversion failed."
64
78
)
65
79
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
+
66
117
def convert (self ) -> Problem :
67
118
"""Convert the PEtab experiments to SBML events.
68
119
69
120
:return: The converted PEtab problem.
70
121
"""
71
- problem = deepcopy (self ._problem )
72
122
73
123
self ._add_presimulation_indicator ()
74
124
125
+ problem = self ._new_problem
75
126
for experiment in problem .experiment_table .experiments :
76
127
self ._convert_experiment (problem , experiment )
128
+
77
129
self ._add_indicators_to_conditions (problem )
78
130
79
131
validation_results = problem .validate ()
@@ -99,9 +151,7 @@ def _convert_experiment(self, problem: Problem, experiment: Experiment):
99
151
kept_periods = []
100
152
for i_period , period in enumerate (experiment .periods ):
101
153
# 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 :
105
155
# TODO: we could address that by offsetting all occurrences of
106
156
# the SBML time in the model (except for the newly added
107
157
# events triggers). Or we better just leave it to the
@@ -161,17 +211,15 @@ def _create_period_begin_event(
161
211
# TODO: for now, add separate events for each experiment x period,
162
212
# this could be optimized to reuse events
163
213
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
-
169
214
ev = self ._model .createEvent ()
170
215
check (ev .setId (f"_petab_event_{ experiment .id } _{ i_period } " ))
171
216
check (ev .setUseValuesFromTriggerTime (True ))
172
217
trigger = ev .createTrigger ()
173
218
check (trigger .setInitialValue (False )) # may trigger at t=0
174
219
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 )
175
223
176
224
exp_ind_id = self .get_experiment_indicator (experiment .id )
177
225
0 commit comments