22
33from __future__ import annotations
44
5+ import warnings
56from copy import deepcopy
67from math import inf
78
89import libsbml
9- from sbmlmath import set_math
10+ from sbmlmath import sbml_math_to_sympy , set_math
1011
1112from .core import Change , Condition , Experiment , ExperimentPeriod
1213from .models ._sbml_utils import add_sbml_parameter , check
1314from .models .sbml_model import SbmlModel
1415from .problem import Problem
1516
17+ __all__ = ["ExperimentsToEventsConverter" ]
18+
1619
1720class 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