Jumps, JumpProblem, and Aggregators
Jump Types: Constant Rate, Mass Action, Variable Rate and Regular
Exact jump process simulation algorithms tend to describe the realization of each jump event chronologically. Individual jumps are usually associated with changes to the state variable u, which in turn changes the rates at which jump events occur. These jumps can be specified as a ConstantRateJump, MassActionJump, or a VariableRateJump.
Each individual type of jump that can occur is represented through (implicitly or explicitly) specifying two pieces of information; a rate function (i.e., intensity or propensity) for the jump and an affect! function for the jump. The former gives the probability per time a particular jump can occur given the current state of the system, and hence determines the time at which jumps can happen. The latter specifies the instantaneous change in the state of the system when the jump occurs.
A specific jump type is a VariableRateJump if its rate function is dependent on values which may change between the occurrence of any two jump events of the process. Examples include jumps where the rate is an explicit function of time, or depends on a state variable that is modified via continuous dynamics such as an ODE or SDE. Such "general" VariableRateJumps can be expensive to simulate because it is necessary to consider the (possibly continuous) changes in the rate function when calculating the next jump time.
BoundedVariableRateJumps represent a special subset of VariableRateJumps where one can specify functions that calculate a time window over which the rate is bounded by a constant (presuming the state u is unchanged due to another ConstantRateJump, MassActionJump or bounded VariableRateJump). They can be simulated more efficiently using rejection-sampling based approaches that leverage this upper bound.
ConstantRateJumps are more restricted in that they assume the rate functions are constant at all times between two consecutive jumps of the system. That is, any states or parameters that a rate function depends on must not change between the times at which two consecutive jumps occur.
A MassActionJumps is a specialized representation for a collection of ConstantRateJump jumps that can each be interpreted as a standard mass action reaction. For systems comprised of many mass action reactions, using the MassActionJump type will offer improved performance compared to using multiple ConstantRateJumps. Note, only one MassActionJump should be defined per JumpProblem; it is then responsible for handling all mass action reaction type jumps. For systems with both mass action jumps and non-mass action jumps, one can create one MassActionJump to handle the mass action jumps, and create a number of ConstantRateJumps or VariableRateJumps to handle the non-mass action jumps.
Since exact methods simulate each individual jump, they may become computationally expensive to simulate processes over timescales that involve many jump occurrences. As an alternative, inexact τ-leaping methods take discrete steps through time, over which they simultaneously execute many jumps. These methods can be much faster as they do not need to simulate the realization of every individual jump event. τ-leaping methods trade accuracy for speed, and are best used when a set of jumps do not make significant changes to the processes' state and/or rates over the course of one time-step (i.e., during a leap interval). A single RegularJump is used to encode jumps for τ-leaping algorithms. While τ-leaping methods can be proven to converge in the limit that the time-step approaches zero, their accuracy can be highly dependent on the chosen time-step. As a rule of thumb, if changes to the state variable u during a time-step (i.e., leap interval) are "minimal" compared to the size of the system, an τ-leaping method can often provide reasonable solution approximations.
Currently, ConstantRateJumps, MassActionJumps, and VariableRateJumps can be coupled to standard SciML ODE/SDE solvers since they are internally handled via callbacks. For ConstantRateJumps, MassActionJumps, and bounded VariableRateJump the determination of the next jump time and type is handled by a user-selected aggregator algorithm. RegularJumps currently require their own special time integrators.
Defining a Constant Rate Jump
The constructor for a ConstantRateJump is:
ConstantRateJump(rate, affect!)rate(u, p, t)is a function which calculates the rate given the current stateu, parametersp, and timet.affect!(integrator)is the effect on the equation using the integrator interface. It encodes how the state should change due to one occurrence of the jump.
Defining a Mass Action Jump
The constructor for a MassActionJump is:
MassActionJump(reactant_stoich, net_stoich; scale_rates = true, param_idxs = nothing)reactant_stoichis a vector whosekth entry is the reactant stoichiometry of thekth reaction. The reactant stoichiometry for an individual reaction is assumed to be represented as a vector ofPairs, mapping species integer id to stoichiometric coefficient.net_stoichis assumed to have the same type asreactant_stoich; a vector whosekth entry is the net stoichiometry of thekth reaction. The net stoichiometry for an individual reaction is again represented as a vector ofPairs, mapping species id to the net change in the species when the reaction occurs.scale_ratesis an optional parameter that specifies whether the rate constants correspond to stochastic rate constants in the sense used by Gillespie, and hence need to be rescaled. The default,scale_rates = true, corresponds to rescaling the passed in rate constants. See below.param_idxsis a vector of the indices within the parameter vector,p, that correspond to the rate constant for each jump.
Notes for Mass Action Jumps
When using
MassActionJumpthe default behavior is to assume rate constants correspond to stochastic rate constants in the sense used by Gillespie (J. Comp. Phys., 1976, 22 (4)). This means that for a reaction such as $2A \overset{k}{\rightarrow} B$, the jump rate function constructed byMassActionJumpwould bek*A*(A-1)/2!. For a trimolecular reaction like $3A \overset{k}{\rightarrow} B$ the rate function would bek*A*(A-1)*(A-2)/3!. To avoid having the reaction rates rescaled (by1/2and1/6for these two examples), one can pass theMassActionJumpconstructor the optional named parameterscale_rates = false, i.e., useMassActionJump(reactant_stoich, net_stoich; scale_rates = false, param_idxs)Zero order reactions can be passed as
reactant_stoichs in one of two ways. Consider the $\varnothing \overset{k}{\rightarrow} A$ reaction with ratek=1:p = [1.0] reactant_stoich = [[0 => 1]] net_stoich = [[1 => 1]] jump = MassActionJump(reactant_stoich, net_stoich; param_idxs = [1])Alternatively, one can create an empty vector of pairs to represent the reaction:
p = [1.0] reactant_stoich = [Vector{Pair{Int, Int}}()] net_stoich = [[1 => 1]] jump = MassActionJump(reactant_stoich, net_stoich; param_idxs = [1])For performance reasons, it is recommended to order species indices in stoichiometry vectors from smallest to largest. That is
reactant_stoich = [[1 => 2, 3 => 1, 4 => 2], [2 => 2, 3 => 2]]is preferred over
reactant_stoich = [[3 => 1, 1 => 2, 4 => 2], [3 => 2, 2 => 2]]
Defining a Variable Rate Jump
The constructor for a VariableRateJump is:
VariableRateJump(rate, affect!;
lrate = nothing, urate = nothing, rateinterval = nothing,
idxs = nothing, rootfind = true, save_positions = (true, true),
interp_points = 10, abstol = 1e-12, reltol = 0)rate(u, p, t)is a function which calculates the rate given the current stateu, parametersp, and timet.affect!(integrator)is the effect on the equation using the integrator interface. It encodes how the state should change due to one occurrence of the jump.
To define a bounded VariableRateJump, which can be simulated more efficiently with bounded VariableRateJump supporting aggregators such as Coevolve, one must also specify
urate(u, p, t), a function which computes an upper bound for the rate in the intervalttot + rateinterval(u, p, t)at timetgiven stateuand parametersp.rateinterval(u, p, t), a function which computes a time intervalttot + rateinterval(u, p, t)given stateuand parameterspover which theuratebound will hold (andlratebound if provided, see below).
For increased performance, one can also specify a lower bound that should be valid over the same rateinterval
lrate(u, p, t), a function which computes a lower bound for the rate in the intervalttot + rateinterval(u, p, t)at timetgiven stateuand parametersp.lrateshould remain valid under the same conditions asurate.
Note that
- It is currently only possible to simulate
VariableRateJumps withSSAStepperwhen using systems with boundedVariableRateJumps and theCoevolveaggregator. - Any
JumpProblemwithVariableRateJumpthat does not use theCoevolveaggregator must be coupled to a continuous problem type such as anODEProblemto handle time-stepping. Continuous time-stepper will ignore the provided aggregator and treat allVariableRateJumps asContinuousCallbacks, using therate(u, p, t)function to construct theconditionfunction that triggers a callback. - When using
Coevolvewith aJumpProblemcoupled to a continuous problem such as anODEProblem, the aggregator will handle the jumps in same way that it does withSSAStepper. However, ensure that giventthe bounds will hold for the duration ofrateinterval(t)for the full coupled system's dynamics or the algorithm will not give correct samples. Numerical and analytical solutions are generally not guaranteed to satisfy the same bounds, especially in large complicated models. Consider adding some slack on the bounds and approach complex models with care. In most simple cases the bounds should be close enough. For debugging purposes one might want to add safety checks in the bound functions. - In some circumstances with complex model of many variables it can be difficult to determine good a priori bounds on the ODE variables. For some discussion on the bound setting problem see Lemaire et al.[1].
Defining a Regular Jump
The constructor for a RegularJump is:
RegularJump(rate, c, numjumps; mark_dist = nothing)rate(out, u, p, t)is the function which computes the rate for every regular jump processc(du, u, p, t, counts, mark)calculates the update givencountsnumber of jumps for each jump process in the interval.numjumpsis the number of jump processes, i.e., the number ofrateequations and the number ofcounts.mark_distis the distribution for a mark.
JumpProblem
To define a JumpProblem, one must first define the basic problem. This can be a DiscreteProblem if there is no differential equation, or an ODE/SDE/DDE/DAE if you would like to augment a differential equation with jumps. Denote this previously defined problem as prob. Then the constructor for the jump problem is:
JumpProblem(prob, aggregator, jumps::JumpSet;
save_positions = prob isa AbstractDiscreteProblem ? (false, true) :
(true, true))The aggregator is the method for simulating ConstantRateJumps, MassActionJumps, and bounded VariableRateJumps (if supported by the aggregator). They are called aggregators since they resolve all these jumps in a single discrete simulation algorithm. The possible aggregators are given below. jumps is a JumpSet which is just a collection of jumps. Instead of passing a JumpSet, one may just pass a list of jumps as trailing positional arguments. For example:
JumpProblem(prob, aggregator, jump1, jump2)and the internals will automatically build the JumpSet. save_positions determines whether to save the state of the system just before and/or after jumps occur.
Note that a JumpProblem/JumpSet can only have 1 RegularJump (since a RegularJump itself describes multiple processes together). Similarly, it can only have one MassActionJump (since it also describes multiple processes together).
Jump Aggregators for Exact Simulation
Jump aggregators are methods for simulating ConstantRateJumps, MassActionJumps, and bounded VariableRateJumps (if supported) exactly. They are called aggregators since they combine all jumps to handle within a single discrete simulation algorithm. Aggregators combine jumps in different ways and offer different trade-offs. However, all aggregators describe the realization of each and every individual jump chronologically. Since they do not skip any jumps, they are considered exact methods. Note that none of the aggregators discussed in this section can be used with RegularJumps which are used for time-step based (inexact) τ-leaping methods.
The current aggregators are (note that an italicized name indicates the aggregator requires various types of dependency graphs, see the next section):
Direct: The Gillespie Direct method SSA [2].DirectFW: the Gillespie Direct method SSA [2] withFunctionWrappers. This aggregator uses a different internal storage format for collections ofConstantRateJumps.DirectCR: The Composition-Rejection Direct method of Slepoy et al.[3]. For large networks and linear chain-type networks, it will often give better performance thanDirect. Dependency graph required.SortingDirect: The Sorting Direct Method of McCollum et al.[4]. It will usually offer performance as good asDirect, and for some systems can offer substantially better performance. Dependency graph required.RSSA: The Rejection SSA (RSSA) method of Thanh et al.[5][6]. WithRSSACR, for very large reaction networks, it often offers the best performance of all methods. Dependency graph required.RSSACR: The Rejection SSA (RSSA) with Composition-Rejection method of Thanh et al.[7]. WithRSSA, for very large reaction networks, it often offers the best performance of all methods. Dependency graph required.RDirect: A variant of Gillespie's Direct method [2] that uses rejection to sample the next reaction.FRM: The Gillespie first reaction method SSA [2].Directshould generally offer better performance and be preferred toFRM.FRMFW: The Gillespie first reaction method SSA [2] withFunctionWrappers.NRM: The Gibson-Bruck Next Reaction Method [8]. For some reaction network structures, this may offer better performance thanDirect(for example, large, linear chains of reactions). Dependency graph required.Coevolve: An improvement of the COEVOLVE algorithm of Farajtabar et al.[9]. Currently the only aggregator that also supports boundedVariableRateJumps. As opposed to COEVOLVE, this method syncs the thinning procedure with the stepper which allows it to handle dependencies on continuous dynamics. Essentially reduces toNRMin handlingConstantRateJumps andMassActionJumps. Dependency graph required.
To pass the aggregator, pass the instantiation of the type. For example:
JumpProblem(prob, Direct(), jump1, jump2)will build a problem where the jumps are simulated using Gillespie's Direct SSA method.
Jump Aggregators Requiring Dependency Graphs
Italicized constant rate jump aggregators above require the user to pass a dependency graph to JumpProblem. Coevolve, DirectCR, NRM, and SortingDirect require a jump-jump dependency graph, passed through the named parameter dep_graph. i.e.,
JumpProblem(prob, DirectCR(), jump1, jump2; dep_graph = your_dependency_graph)For systems with only MassActionJumps, or those generated from a Catalystreaction_network, this graph will be auto-generated. Otherwise, you must construct the dependency graph whenever the set of jumps include ConstantRateJumps and/or bounded VariableRateJumps.
Dependency graphs are represented as a Vector{Vector{Int}}, with the ith vector containing the indices of the jumps for which rates must be recalculated when the ith jump occurs. Internally, all MassActionJumps are ordered before ConstantRateJumps and bounded VariableRateJumps. General VariableRateJumps are not handled by aggregators, and so not included in the jump ordering for dependency graphs. Note that the relative order between ConstantRateJumps and relative order between bounded VariableRateJumps is preserved. In this way, one can precalculate the jump order to manually construct dependency graphs.
RSSA and RSSACR require two different types of dependency graphs, passed through the following JumpProblem kwargs:
vartojumps_map- AVector{Vector{Int}}mapping each variable index,i, to a set of jump indices. The jump indices correspond to jumps with rate functions that depend on the value ofu[i].jumptovars_map- AVector{Vector{Int}}mapping each jump index to a set of variable indices. The corresponding variables are those that have their value,u[i], altered when the jump occurs.
For systems generated from a Catalystreaction_network these will be auto-generated. Otherwise, you must explicitly construct and pass in these mappings.
Recommendations for exact methods
For representing and aggregating jumps
- Use a
MassActionJumpto handle all jumps that can be represented as mass action reactions with constant rate between jumps. This will generally offer the fastest performance. - Use
ConstantRateJumps for any remaining jumps with a constant rate between jumps. - Use
VariableRateJumps for any remaining jumps with variable rate between jumps. If possible, construct a boundedVariableRateJumpas described above and in the doc string. The tighter and easier to compute the bounds are, the faster the resulting simulation will be. Use theCoevolveaggregator to ensure such jumps are handled via the more efficient aggregator interface.Coevolvehandles continuous steppers so can be coupled with a continuous problem type such as anODEProblemas long as the bounds are satisfied given changes inuoverrateinterval.
For systems with only ConstantRateJumps and MassActionJumps,
- For a small number of jumps, < ~10,
Directwill often perform as well as the other aggregators. - For > ~10 jumps
SortingDirectwill often offer better performance thanDirect. - For large numbers of jumps with sparse chain like structures and similar jump rates, for example continuous time random walks,
RSSACR,DirectCRand thenNRMoften have the best performance. - For very large networks, with many updates per jump,
RSSAandRSSACRwill often substantially outperform the other methods.
For pure jump systems, time-step using SSAStepper() with a DiscreteProblem unless one has general (i.e., non-bounded) VariableRateJumps.
In general, for systems with sparse dependency graphs if Direct is slow, one of SortingDirect, RSSA or RSSACR will usually offer substantially better performance. See DiffEqBenchmarks.jl for benchmarks on several example networks.
Remaking JumpProblems
When running many simulations, it can often be convenient to update the initial condition or simulation parameters without having to create and initialize a new JumpProblem. In such situations remake can be used to change the initial condition, time span, and the parameter vector. Note, the new JumpProblem will alias internal data structures from the old problem, including core components of the SSA aggregators. As such, only the new problem generated by remake should be used for subsequent simulations.
As an example, consider the following SIR model:
rate1(u, p, t) = p[1] * u[1] * u[2]
function affect1!(integrator)
integrator.u[1] -= 1
integrator.u[2] += 1
end
jump = ConstantRateJump(rate1, affect1!)
rate2(u, p, t) = p[2] * u[2]
function affect2!(integrator)
integrator.u[2] -= 1
integrator.u[3] += 1
end
jump2 = ConstantRateJump(rate2, affect2!)
u0 = [999, 1, 0]
p = (0.1 / 1000, 0.01)
tspan = (0.0, 250.0)
dprob = DiscreteProblem(u0, tspan, p)
jprob = JumpProblem(dprob, Direct(), jump, jump2)
sol = solve(jprob, SSAStepper())We can change any of u0, p and/or tspan by either making a new DiscreteProblem
u02 = [10, 1, 0]
p2 = (0.1 / 1000, 0.0)
tspan2 = (0.0, 2500.0)
dprob2 = DiscreteProblem(u02, tspan2, p2)
jprob2 = remake(jprob, prob = dprob2)
sol2 = solve(jprob2, SSAStepper())or by directly remaking with the new parameters
jprob2 = remake(jprob, u0 = u02, p = p2, tspan = tspan2)
sol2 = solve(jprob2, SSAStepper())To avoid ambiguities, the following will give an error
jprob2 = remake(jprob, prob = dprob2, u0 = u02)as will trying to update either p or tspan while passing a new DiscreteProblem using the prob kwarg.
References
- 1V. Lemaire, M. Thieullen and N. Thomas, Exact Simulation of the Jump Times of a Class of Piecewise Deterministic Markov Processes, Journal of Scientific Computing, 75 (3), 1776-1807 (2018). doi:10.1007/s10915-017-0607-4.
- 2D. T. Gillespie, A general method for numerically simulating the stochastic time evolution of coupled chemical reactions, Journal of Computational Physics, 22 (4), 403–434 (1976). doi:10.1016/0021-9991(76)90041-3.
- 3A. Slepoy, A.P. Thompson and S.J. Plimpton, A constant-time kinetic Monte Carlo algorithm for simulation of large biochemical reaction networks, Journal of Chemical Physics, 128 (20), 205101 (2008). doi:10.1063/1.2919546.
- 4J. M. McCollum, G. D. Peterson, C. D. Cox, M. L. Simpson and N. F. Samatova, The sorting direct method for stochastic simulation of biochemical systems with varying reaction execution behavior, Computational Biology and Chemistry, 30 (1), 39049 (2006). doi:10.1016/j.compbiolchem.2005.10.007.
- 5V. H. Thanh, C. Priami and R. Zunino, Efficient rejection-based simulation of biochemical reactions with stochastic noise and delays, Journal of Chemical Physics, 141 (13), 134116 (2014). doi:10.1063/1.4896985.
- 6V. H. Thanh, R. Zunino and C. Priami, On the rejection-based algorithm for simulation and analysis of large-scale reaction networks, Journal of Chemical Physics, 142 (24), 244106 (2015). doi:10.1063/1.4922923.
- 7V. H. Thanh, R. Zunino, and C. Priami, Efficient constant-time complexity algorithm for stochastic simulation of large reaction networks, IEEE/ACM Transactions on Computational Biology and Bioinformatics, 14 (3), 657-667 (2017). doi:10.1109/TCBB.2016.2530066.
- 8M. A. Gibson and J. Bruck, Efficient exact stochastic simulation of chemical systems with many species and many channels, Journal of Physical Chemistry A, 104 (9), 1876-1889 (2000). doi:10.1021/jp993732q.
- 9M. Farajtabar, Y. Wang, M. Gomez-Rodriguez, S. Li, H. Zha, and L. Song, COEVOLVE: a joint point process model for information diffusion and network evolution, Journal of Machine Learning Research 18(1), 1305–1353 (2017). doi: 10.5555/3122009.3122050.