@@ -200,7 +200,9 @@ def convert(self) -> Problem:
200200 return self ._new_problem
201201
202202 def _convert_experiment (self , experiment : Experiment ) -> None :
203- """Convert a single experiment to SBML events."""
203+ """
204+ Convert a single experiment to SBML events or initial assignments.
205+ """
204206 model = self ._model
205207 experiment .sort_periods ()
206208 has_preequilibration = experiment .has_preequilibration
@@ -215,7 +217,13 @@ def _convert_experiment(self, experiment: Experiment) -> None:
215217 "model."
216218 )
217219 add_sbml_parameter (model , id_ = exp_ind_id , constant = False , value = 0 )
218- kept_periods = []
220+ kept_periods : list [ExperimentPeriod ] = []
221+ # Collect values for initial assignments for the different experiments.
222+ # All expressions must be combined into a single initial assignment
223+ # per target.
224+ # target_id -> (experiment_indicator, target_value)
225+ period0_assignments : dict [str , list [tuple [str , sp .Basic ]]] = {}
226+
219227 for i_period , period in enumerate (experiment .sorted_periods ):
220228 if period .is_preequilibration :
221229 # pre-equilibration cannot be represented in SBML,
@@ -231,21 +239,80 @@ def _convert_experiment(self, experiment: Experiment) -> None:
231239 # or the only non-equilibration period (handled above)
232240 continue
233241
234- # TODO: If this is the first period, we may want to implement
235- # the changes as initialAssignments instead of events.
236- # That way, we don't need to worry about any event priorities
237- # and tools that don't support event priorities can still handle
242+ # Encode the period changes in the SBML model as events
243+ # that trigger at the start of the period or,
244+ # for the first period, as initial assignments.
245+ # Initial assignments are required for the first period,
246+ # because other initial assignments may depend on
247+ # the changed values.
248+ # Additionally, tools that don't support events can still handle
238249 # single-period experiments.
250+ if i_period == 0 :
251+ exp_ind = self .get_experiment_indicator (experiment .id )
252+ for change in self ._new_problem .get_changes_for_period (period ):
253+ period0_assignments .setdefault (
254+ change .target_id , []
255+ ).append ((exp_ind , change .target_value ))
256+ else :
257+ ev = self ._create_period_start_event (
258+ experiment = experiment ,
259+ i_period = i_period ,
260+ period = period ,
261+ )
262+ self ._create_event_assignments_for_period (
263+ ev ,
264+ self ._new_problem .get_changes_for_period (period ),
265+ )
239266
240- ev = self ._create_period_start_event (
241- experiment = experiment ,
242- i_period = i_period ,
243- period = period ,
244- )
245- self ._create_event_assignments_for_period (
246- ev ,
247- self ._new_problem .get_changes_for_period (period ),
248- )
267+ # Create initial assignments for the first period
268+ if period0_assignments :
269+ free_symbols_in_assignments = set ()
270+ for target_id , changes in period0_assignments .items ():
271+ # The initial value might only be changed for a subset of
272+ # experiments. We need to keep the original initial value
273+ # for all other experiments.
274+
275+ # Is there an initial assignment for this target already?
276+ # If not, fall back to the initial value of the target.
277+ if (
278+ ia := model .getInitialAssignmentBySymbol (target_id )
279+ ) is not None :
280+ default = sbml_math_to_sympy (ia .getMath ())
281+ else :
282+ # use the initial value of the target as default
283+ target = model .getElementBySId (target_id )
284+ default = self ._initial_value_from_element (target )
285+
286+ # combine all changes into a single piecewise expression
287+ if expr_cond_pairs := [
288+ (target_value , sp .Symbol (exp_ind ) > 0.5 )
289+ for exp_ind , target_value in changes
290+ if target_value != default
291+ ]:
292+ # only create the initial assignment if there is
293+ # actually something to change
294+ expr = sp .Piecewise (
295+ * expr_cond_pairs ,
296+ (default , True ),
297+ )
298+
299+ # Create a new initial assignment if necessary, otherwise
300+ # overwrite the existing one.
301+ if ia is None :
302+ ia = model .createInitialAssignment ()
303+ ia .setSymbol (target_id )
304+
305+ set_math (ia , expr )
306+ free_symbols_in_assignments |= expr .free_symbols
307+
308+ # the target value may depend on parameters that are only
309+ # introduced in the PEtab parameter table - those need
310+ # to be added to the model
311+ for sym in free_symbols_in_assignments :
312+ if model .getElementBySId (sym .name ) is None :
313+ add_sbml_parameter (
314+ model , id_ = sym .name , constant = True , value = 0
315+ )
249316
250317 if len (kept_periods ) > 2 :
251318 raise AssertionError ("Expected at most two periods to be kept." )
@@ -261,6 +328,39 @@ def _convert_experiment(self, experiment: Experiment) -> None:
261328
262329 experiment .periods = kept_periods
263330
331+ @staticmethod
332+ def _initial_value_from_element (target : libsbml .SBase ) -> sp .Basic :
333+ # use the initial value of the target as default
334+ if target is None :
335+ raise ValueError ("`target` is None." )
336+
337+ if target .getTypeCode () == libsbml .SBML_COMPARTMENT :
338+ return sp .Float (target .getSize ())
339+
340+ if target .getTypeCode () == libsbml .SBML_SPECIES :
341+ if target .getHasOnlySubstanceUnits ():
342+ # amount-based -> return amount
343+ if target .isSetInitialAmount ():
344+ return sp .Float (target .getInitialAmount ())
345+ return sp .Float (target .getInitialConcentration ()) * sp .Symbol (
346+ target .getCompartment ()
347+ )
348+ # concentration-based -> return concentration
349+ if target .isSetInitialConcentration ():
350+ return sp .Float (target .getInitialConcentration ())
351+
352+ return sp .Float (target .getInitialAmount ()) / sp .Symbol (
353+ target .getCompartment ()
354+ )
355+
356+ if target .getTypeCode () == libsbml .SBML_PARAMETER :
357+ return sp .Float (target .getValue ())
358+
359+ raise NotImplementedError (
360+ "Cannot create initial assignment for unsupported SBML "
361+ f"entity type { target .getTypeCode ()} ."
362+ )
363+
264364 def _create_period_start_event (
265365 self , experiment : Experiment , i_period : int , period : ExperimentPeriod
266366 ) -> libsbml .Event :
0 commit comments