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 rate
s 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" VariableRateJump
s 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.
BoundedVariableRateJump
s represent a special subset of VariableRateJump
s 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.
ConstantRateJump
s 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 MassActionJump
s 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 ConstantRateJump
s. 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 ConstantRateJump
s or VariableRateJump
s 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, ConstantRateJump
s, MassActionJump
s, and VariableRateJump
s can be coupled to standard SciML ODE/SDE solvers since they are internally handled via callbacks. For ConstantRateJump
s, MassActionJump
s, and bounded VariableRateJump
the determination of the next jump time and type is handled by a user-selected aggregator algorithm. RegularJump
s 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_stoich
is a vector whosek
th entry is the reactant stoichiometry of thek
th reaction. The reactant stoichiometry for an individual reaction is assumed to be represented as a vector ofPair
s, mapping species integer id to stoichiometric coefficient.net_stoich
is assumed to have the same type asreactant_stoich
; a vector whosek
th entry is the net stoichiometry of thek
th reaction. The net stoichiometry for an individual reaction is again represented as a vector ofPair
s, mapping species id to the net change in the species when the reaction occurs.scale_rates
is 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_idxs
is 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
MassActionJump
the 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 byMassActionJump
would 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/2
and1/6
for these two examples), one can pass theMassActionJump
constructor 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_stoich
s 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 intervalt
tot + rateinterval(u, p, t)
at timet
given stateu
and parametersp
.rateinterval(u, p, t)
, a function which computes a time intervalt
tot + rateinterval(u, p, t)
given stateu
and parametersp
over which theurate
bound will hold (andlrate
bound 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 intervalt
tot + rateinterval(u, p, t)
at timet
given stateu
and parametersp
.lrate
should remain valid under the same conditions asurate
.
Note that
- It is currently only possible to simulate
VariableRateJump
s withSSAStepper
when using systems with boundedVariableRateJump
s and theCoevolve
aggregator. - Any
JumpProblem
withVariableRateJump
that does not use theCoevolve
aggregator must be coupled to a continuous problem type such as anODEProblem
to handle time-stepping. Continuous time-stepper will ignore the provided aggregator and treat allVariableRateJump
s asContinuousCallback
s, using therate(u, p, t)
function to construct thecondition
function that triggers a callback. - When using
Coevolve
with aJumpProblem
coupled to a continuous problem such as anODEProblem
, the aggregator will handle the jumps in same way that it does withSSAStepper
. However, ensure that givent
the 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 givencounts
number of jumps for each jump process in the interval.numjumps
is the number of jump processes, i.e., the number ofrate
equations and the number ofcounts
.mark_dist
is 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 ConstantRateJump
s, MassActionJump
s, and bounded VariableRateJump
s (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 ConstantRateJump
s, MassActionJump
s, and bounded VariableRateJump
s (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].Direct
should 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 boundedVariableRateJump
s. As opposed to COEVOLVE, this method syncs the thinning procedure with the stepper which allows it to handle dependencies on continuous dynamics. Essentially reduces toNRM
in handlingConstantRateJump
s andMassActionJump
s. 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 MassActionJump
s, 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 ConstantRateJump
s and/or bounded VariableRateJump
s.
Dependency graphs are represented as a Vector{Vector{Int}}
, with the i
th vector containing the indices of the jumps for which rates must be recalculated when the i
th jump occurs. Internally, all MassActionJump
s are ordered before ConstantRateJump
s and bounded VariableRateJump
s. General VariableRateJump
s are not handled by aggregators, and so not included in the jump ordering for dependency graphs. Note that the relative order between ConstantRateJump
s and relative order between bounded VariableRateJump
s 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
MassActionJump
to handle all jumps that can be represented as mass action reactions with constant rate between jumps. This will generally offer the fastest performance. - Use
ConstantRateJump
s for any remaining jumps with a constant rate between jumps. - Use
VariableRateJump
s for any remaining jumps with variable rate between jumps. If possible, construct a boundedVariableRateJump
as described above and in the doc string. The tighter and easier to compute the bounds are, the faster the resulting simulation will be. Use theCoevolve
aggregator to ensure such jumps are handled via the more efficient aggregator interface.Coevolve
handles continuous steppers so can be coupled with a continuous problem type such as anODEProblem
as long as the bounds are satisfied given changes inu
overrateinterval
.
For systems with only ConstantRateJump
s and MassActionJump
s,
- For a small number of jumps, < ~10,
Direct
will often perform as well as the other aggregators. - For > ~10 jumps
SortingDirect
will 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
,DirectCR
and thenNRM
often have the best performance. - For very large networks, with many updates per jump,
RSSA
andRSSACR
will 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) VariableRateJump
s.
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 JumpProblem
s
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.