From 280b64bb866ab8deabfc0b0777e59d98753e3d1a Mon Sep 17 00:00:00 2001 From: gzagatti Date: Tue, 25 Oct 2022 23:27:12 +0800 Subject: [PATCH 01/72] adds working QueueMethod aggregator and ConditionalRateJump. --- src/JumpProcesses.jl | 7 +- src/aggregators/aggregators.jl | 8 +- src/aggregators/queue.jl | 167 +++++++++++++++++++++++++++++++++ src/jumps.jl | 123 ++++++++++++++++-------- src/problem.jl | 18 +++- 5 files changed, 280 insertions(+), 43 deletions(-) create mode 100644 src/aggregators/queue.jl diff --git a/src/JumpProcesses.jl b/src/JumpProcesses.jl index 600be47a0..13f7b24a3 100644 --- a/src/JumpProcesses.jl +++ b/src/JumpProcesses.jl @@ -50,6 +50,7 @@ include("aggregators/prioritytable.jl") include("aggregators/directcr.jl") include("aggregators/rssacr.jl") include("aggregators/rdirect.jl") +include("aggregators/queue.jl") # spatial: include("spatial/spatial_massaction_jump.jl") @@ -72,14 +73,14 @@ include("coupling.jl") include("SSA_stepper.jl") include("simple_regular_solve.jl") -export ConstantRateJump, VariableRateJump, RegularJump, MassActionJump, - JumpSet +export ConstantRateJump, VariableRateJump, ConditionalRateJump, RegularJump, + MassActionJump, JumpSet export JumpProblem export SplitCoupledJumpProblem -export Direct, DirectFW, SortingDirect, DirectCR +export Direct, DirectFW, SortingDirect, DirectCR, QueueMethod export BracketData, RSSA export FRM, FRMFW, NRM export RSSACR, RDirect diff --git a/src/aggregators/aggregators.jl b/src/aggregators/aggregators.jl index bc5e354fb..d04b1f791 100644 --- a/src/aggregators/aggregators.jl +++ b/src/aggregators/aggregators.jl @@ -144,8 +144,13 @@ doi: 10.1063/1.4928635 """ struct DirectCRDirect <: AbstractAggregatorAlgorithm end +""" +The Queue Method. This method handles conditional intensity rates. +""" +struct QueueMethod <: AbstractAggregatorAlgorithm end + const JUMP_AGGREGATORS = (Direct(), DirectFW(), DirectCR(), SortingDirect(), RSSA(), FRM(), - FRMFW(), NRM(), RSSACR(), RDirect()) + FRMFW(), NRM(), RSSACR(), RDirect(), QueueMethod()) # For JumpProblem construction without an aggregator struct NullAggregator <: AbstractAggregatorAlgorithm end @@ -156,6 +161,7 @@ needs_depgraph(aggregator::DirectCR) = true needs_depgraph(aggregator::SortingDirect) = true needs_depgraph(aggregator::NRM) = true needs_depgraph(aggregator::RDirect) = true +needs_depgraph(aggregator::QueueMethod) = true # true if aggregator requires a map from solution variable to dependent jumps. # It is implicitly assumed these aggregators also require the reverse map, from diff --git a/src/aggregators/queue.jl b/src/aggregators/queue.jl new file mode 100644 index 000000000..c44bb8b77 --- /dev/null +++ b/src/aggregators/queue.jl @@ -0,0 +1,167 @@ +""" +The Queue Method. This method handles conditional intensity rates. + +```jl +# simulating a Hawkes process +function rate_factory(prev_rate, t0, u, params, t) + λ0, α, β = params + λt = prev_rate(u, params, t) + + if t == t0 + if λt ≈ λ0 + rate(u, params, s) = λ0 + α*exp(-β*(s-t)) + else + rate(u, params, s) = prev_rate(u, params, t) + α*exp(-β*(s-t)) + end + elseif t > t0 + if λt ≈ λ0 + rate(u, params, s) = λ0 + else + rate = prev_rate + end + else + error("t must be equal or higher than t0") + end + + lrate = λ0 + urate = rate(u, params, t) + if urate < lrate + error("The upper bound rate should not be lower than the lower bound.") + end + L = urate == lrate ? typemax(t) : 1/(2*rate) + + return rate, lrate, urate, L +end +affect!(integrator) = integratro.u[1] += 1 +jump = ConstantRateJump(rate_factory, affect!) +``` +""" +mutable struct QueueMethodJumpAggregation{T, S, F1, F2, F3, RNG, DEPGR, PQ} <: AbstractSSAJumpAggregator + next_jump::Int # the next jump to execute + prev_jump::Int # the previous jump that was executed + next_jump_time::T # the time of the next jump + end_time::T # the time to stop a simulation + cur_rates::F1 # vector of current propensity values + sum_rate::T # sum of current propensity values + ma_jumps::S # any MassActionJumps for the system (scalar form) + rates::F2 # vector of rate functions for ConditionalRateJumps + affects!::F3 # vector of affect functions for ConditionalRateJumps + save_positions::Tuple{Bool, Bool} # tuple for whether the jumps before and/or after event + rng::RNG # random number generator + dep_gr::DEPGR + pq::PQ +end + +function QueueMethodJumpAggregation(nj::Int, njt::T, et::T, crs::F1, sr::T, + maj::S, rs::F2, affs!::F3, sps::Tuple{Bool, Bool}, + rng::RNG; dep_graph = nothing, + kwargs...) where {T, S, F1, F2, F3, RNG} + if get_num_majumps(maj) > 0 + error("Mass-action jumps are not supported with the Queue Method.") + end + + if dep_graph === nothing + if !isempty(rs) + error("To use ConstantRateJumps with Queue Method algorithm a dependency graph must be supplied.") + end + else + dg = dep_graph + # make sure each jump depends on itself + add_self_dependencies!(dg) + end + + pq = MutableBinaryMinHeap{T}() + + QueueMethodJumpAggregation{T, S, F1, F2, F3, RNG, typeof(dg), typeof(pq)}(nj, nj, njt, et, crs, sr, + maj, rs, affs!, sps, rng, dg, pq) +end + +# creating the JumpAggregation structure (tuple-based constant jumps) +function aggregate(aggregator::QueueMethod, u, p, t, end_time, conditional_jumps, + ma_jumps, save_positions, rng; kwargs...) + # rates, affects!, RateWrapper = get_jump_info_fwrappers(u, p, t, conditional_jumps) + rates, affects! = get_jump_info_fwrappers(u, p, t, conditional_jumps) + + + sum_rate = zero(typeof(t)) + # cur_rates = Vector{RateWrapper}(nothing, length(conditional_jumps)) + cur_rates = Vector{Any}(nothing, length(conditional_jumps)) + next_jump = 0 + next_jump_time = typemax(typeof(t)) + QueueMethodJumpAggregation(next_jump, next_jump_time, end_time, cur_rates, sum_rate, + ma_jumps, rates, affects!, save_positions, rng; kwargs...) +end + +# set up a new simulation and calculate the first jump / jump time +function initialize!(p::QueueMethodJumpAggregation, integrator, u, params, t) + p.end_time = integrator.sol.prob.tspan[2] + fill_rates_and_get_times!(p, u, params, t) + generate_jumps!(p, integrator, u, params, t) + nothing +end + +# execute one jump, changing the system state +function execute_jumps!(p::QueueMethodJumpAggregation, integrator, u, params, t) + # execute jump + u = update_state!(p, integrator, u) + + # update current jump rates and times + update_dependent_rates!(p, u, params, t) + + nothing +end + +# calculate the next jump / jump time +function generate_jumps!(p::QueueMethodJumpAggregation, integrator, u, params, t) + p.next_jump_time, p.next_jump = top_with_handle(p.pq) + nothing +end + +######################## SSA specific helper routines ######################## +function update_dependent_rates!(p::QueueMethodJumpAggregation, u, params, t) + @inbounds dep_rxs = p.dep_gr[p.next_jump] + @unpack cur_rates, rates = p + + @inbounds for rx in dep_rxs + @inbounds trx, cur_rates[rx] = next_time(p, rates[rx], cur_rates[rx], u, params, t) + update!(p.pq, rx, trx) + end + + nothing +end + +function next_time(p::QueueMethodJumpAggregation, rate_factory, prev_rate, u, params, t) + t0 = t + @unpack end_time, rng = p + rate = nothing + while t < end_time + rate, lrate, urate, L = rate_factory(prev_rate, t0, u, params, t) + s = randexp(rng) / urate + if s > L + t = t + L + continue + end + if urate > lrate + v = rand(rng) + if (v > lrate/urate) && (v > rate(u, params, t + s)/urate) + t = t + s + continue + end + end + t = t + s + return t, rate + end + return typemax(t), rate +end + +# reevaulate all rates, recalculate all jump times, and reinit the priority queue +function fill_rates_and_get_times!(p::QueueMethodJumpAggregation, u, params, t) + @unpack cur_rates, rates = p + pqdata = Vector{eltype(t)}(undef, length(rates)) + @inbounds for (rx, rate) in enumerate(rates) + @inbounds trx, cur_rates[rx] = next_time(p, rates[rx], cur_rates[rx], u, params,t) + pqdata[rx] = trx + end + p.pq = MutableBinaryMinHeap(pqdata) + nothing +end diff --git a/src/jumps.jl b/src/jumps.jl index 7f2a84e13..ed6065daf 100644 --- a/src/jumps.jl +++ b/src/jumps.jl @@ -91,6 +91,26 @@ function VariableRateJump(rate, affect!; save_positions, abstol, reltol) end +""" +$(TYPEDEF) + +Defines a jump process with a ... + +## Fields + +$(FIELDS) + +## Examples + +Foo ... +""" +struct ConditionalRateJump{F1, F2} <: AbstractJump + """Function `rate(u, p, t)` that returns """ + rate::F1 + """Function `affect(integrator)` that updates the state for one occurrence of the jump.""" + affect!::F2 +end + struct RegularJump{iip, R, C, MD} rate::R c::C @@ -379,11 +399,13 @@ jprob = JumpProblem(oprob, Direct(), jset) sol = solve(jprob, Tsit5()) ``` """ -struct JumpSet{T1, T2, T3, T4} <: AbstractJump +struct JumpSet{T1, T2, T3, T4, T5} <: AbstractJump """Collection of [`VariableRateJump`](@ref)s""" variable_jumps::T1 """Collection of [`ConstantRateJump`](@ref)s""" constant_jumps::T2 + """Collection of [`ConditionalRateJump`](@ref)s""" + conditional_jumps::T5 """Collection of `RegularJump`s""" regular_jump::T3 """Collection of [`MassActionJump`](@ref)s""" @@ -393,23 +415,24 @@ function JumpSet(vj, cj, rj, maj::MassActionJump{S, T, U, V}) where {S <: Number JumpSet(vj, cj, rj, check_majump_type(maj)) end -JumpSet(jump::ConstantRateJump) = JumpSet((), (jump,), nothing, nothing) -JumpSet(jump::VariableRateJump) = JumpSet((jump,), (), nothing, nothing) -JumpSet(jump::RegularJump) = JumpSet((), (), jump, nothing) -JumpSet(jump::AbstractMassActionJump) = JumpSet((), (), nothing, jump) -function JumpSet(; variable_jumps = (), constant_jumps = (), +JumpSet(jump::VariableRateJump) = JumpSet((jump,), (), (), nothing, nothing) +JumpSet(jump::ConstantRateJump) = JumpSet((), (jump,), (), nothing, nothing) +JumpSet(jump::ConditionalRateJump) = JumpSet((), (), (jump,), nothing, nothing) +JumpSet(jump::RegularJump) = JumpSet((), (), (), jump, nothing) +JumpSet(jump::AbstractMassActionJump) = JumpSet((), (), (), nothing, jump) +function JumpSet(; variable_jumps = (), constant_jumps = (), conditional_jumps = (), regular_jumps = nothing, massaction_jumps = nothing) - JumpSet(variable_jumps, constant_jumps, regular_jumps, massaction_jumps) + JumpSet(variable_jumps, constant_jumps, conditional_jumps, regular_jumps, massaction_jumps) end JumpSet(jb::Nothing) = JumpSet() # For Varargs, use recursion to make it type-stable function JumpSet(jumps::AbstractJump...) - JumpSet(split_jumps((), (), nothing, nothing, jumps...)...) + JumpSet(split_jumps((), (), (), nothing, nothing, jumps...)...) end # handle vector of mass action jumps -function JumpSet(vjs, cjs, rj, majv::Vector{T}) where {T <: MassActionJump} +function JumpSet(vjs, cjs, djs, rj, majv::Vector{T}) where {T <: MassActionJump} if isempty(majv) error("JumpSets do not accept empty mass action jump collections; use \"nothing\" instead.") end @@ -420,27 +443,31 @@ function JumpSet(vjs, cjs, rj, majv::Vector{T}) where {T <: MassActionJump} massaction_jump_combine(maj, majv[i]) end - JumpSet(vjs, cjs, rj, maj) + JumpSet(vjs, cjs, djs, rj, maj) end @inline get_num_majumps(jset::JumpSet) = get_num_majumps(jset.massaction_jump) -@inline split_jumps(vj, cj, rj, maj) = vj, cj, rj, maj -@inline function split_jumps(vj, cj, rj, maj, v::VariableRateJump, args...) - split_jumps((vj..., v), cj, rj, maj, args...) +@inline split_jumps(vj, cj, dj, rj, maj) = vj, cj, dj, rj, maj +@inline function split_jumps(vj, cj, dj, rj, maj, v::VariableRateJump, args...) + split_jumps((vj..., v), cj, dj, rj, maj, args...) +end +@inline function split_jumps(vj, cj, dj, rj, maj, c::ConstantRateJump, args...) + split_jumps(vj, (cj..., c), dj, rj, maj, args...) end -@inline function split_jumps(vj, cj, rj, maj, c::ConstantRateJump, args...) - split_jumps(vj, (cj..., c), rj, maj, args...) +@inline function split_jumps(vj, cj, dj, rj, maj, d::ConditionalRateJump, args...) + split_jumps(vj, cj, (dj..., d), rj, maj, args...) end -@inline function split_jumps(vj, cj, rj, maj, c::RegularJump, args...) - split_jumps(vj, cj, regular_jump_combine(rj, c), maj, args...) +@inline function split_jumps(vj, cj, dj, rj, maj, c::RegularJump, args...) + split_jumps(vj, cj, dj, regular_jump_combine(rj, c), maj, args...) end -@inline function split_jumps(vj, cj, rj, maj, c::MassActionJump, args...) - split_jumps(vj, cj, rj, massaction_jump_combine(maj, c), args...) +@inline function split_jumps(vj, cj, dj, rj, maj, c::MassActionJump, args...) + split_jumps(vj, cj, dj, rj, massaction_jump_combine(maj, c), args...) end -@inline function split_jumps(vj, cj, rj, maj, j::JumpSet, args...) +@inline function split_jumps(vj, cj, dj, rj, maj, j::JumpSet, args...) split_jumps((vj..., j.variable_jumps...), (cj..., j.constant_jumps...), + (dj..., j.conditional_jumps...), regular_jump_combine(rj, j.regular_jump), massaction_jump_combine(maj, j.massaction_jump), args...) end @@ -549,11 +576,11 @@ function massaction_jump_combine(maj1::MassActionJump, maj2::MassActionJump) maj2.param_mapper) end -##### helper methods for unpacking rates and affects! from constant jumps ##### -function get_jump_info_tuples(constant_jumps) - if (constant_jumps !== nothing) && !isempty(constant_jumps) - rates = ((c.rate for c in constant_jumps)...,) - affects! = ((c.affect! for c in constant_jumps)...,) +##### helper methods for unpacking rates and affects! from constant or conditional jumps ##### +function get_jump_info_tuples(jumps) + if (jumps !== nothing) && !isempty(jumps) + rates = ((c.rate for c in jumps)...,) + affects! = ((c.affect! for c in jumps)...,) else rates = () affects! = () @@ -562,18 +589,40 @@ function get_jump_info_tuples(constant_jumps) rates, affects! end -function get_jump_info_fwrappers(u, p, t, constant_jumps) - RateWrapper = FunctionWrappers.FunctionWrapper{typeof(t), - Tuple{typeof(u), typeof(p), typeof(t)}} - AffectWrapper = FunctionWrappers.FunctionWrapper{Nothing, Tuple{Any}} +function get_jump_info_fwrappers(u, p, t, jumps) + if eltype(jumps) <: ConstantRateJump + RateWrapper = FunctionWrappers.FunctionWrapper{typeof(t), + Tuple{typeof(u), typeof(p), typeof(t)}} + AffectWrapper = FunctionWrappers.FunctionWrapper{Nothing, Tuple{Any}} - if (constant_jumps !== nothing) && !isempty(constant_jumps) - rates = [RateWrapper(c.rate) for c in constant_jumps] - affects! = [AffectWrapper(x -> (c.affect!(x); nothing)) for c in constant_jumps] - else - rates = Vector{RateWrapper}() - affects! = Vector{AffectWrapper}() - end + if (jumps !== nothing) && !isempty(jumps) + rates = [RateWrapper(c.rate) for c in jumps] + affects! = [AffectWrapper(x -> (c.affect!(x); nothing)) for c in jumps] + else + rates = Vector{RateWrapper}() + affects! = Vector{AffectWrapper}() + end - rates, affects! + return rates, affects! + elseif eltype(jumps) <: ConditionalRateJump + # RateWrapper = FunctionWrappers.FunctionWrapper{ + # typeof(t), + # Tuple{typeof(u), typeof(p), typeof(t)} + # } + # RateFactoryWrapper = FunctionWrappers.FunctionWrapper{ + # Tuple{RateWrapper, typeof(t), typeof(t), typeof(t)}, + # Tuple{Union{Nothing, RateWrapper}, typeof(t), typeof(u), typeof(p), typeof(t)} + # } + AffectWrapper = FunctionWrappers.FunctionWrapper{Nothing, Tuple{Any}} + if (jumps !== nothing) & !isempty(jumps) + # rates = [RateFactoryWrapper(c.rate) for c in jumps] + rates = [c.rate for c in jumps] + affects! = [AffectWrapper(x -> (c.affect!(x); nothing)) for c in jumps] + else + rates = Vector{RateFactoryWrapper}() + affects! = Vector{AffectWrapper}() + end + + rates, affects! + end end diff --git a/src/problem.jl b/src/problem.jl index 92feb6566..4e897fece 100644 --- a/src/problem.jl +++ b/src/problem.jl @@ -64,7 +64,7 @@ mutable struct JumpProblem{iip, P, A, C, J <: Union{Nothing, AbstractJumpAggrega aggregator::A """The underlying state data associated with the chosen aggregator.""" discrete_jump_aggregation::J - """`CallBackSet` with the underlying `ConstantRate` and `VariableRate` jumps.""" + """`CallBackSet` with the underlying `ConstantRate`, `VariableRate` and `ConditionalRateJump` jumps.""" jump_callback::C """The `VariableRateJump`s.""" variable_jumps::J2 @@ -125,6 +125,9 @@ end function JumpProblem(prob, jumps::VariableRateJump; kwargs...) JumpProblem(prob, JumpSet(jumps); kwargs...) end +function JumpProblem(prob, jumps::ConditionalRateJump; kwargs...) + JumpProblem(prob, JumpSet(jumps); kwargs...) +end function JumpProblem(prob, jumps::RegularJump; kwargs...) JumpProblem(prob, JumpSet(jumps); kwargs...) end @@ -209,7 +212,18 @@ function JumpProblem(prob, aggregator::AbstractAggregatorAlgorithm, jumps::JumpS variable_jump_callback = build_variable_callback(CallbackSet(), 0, jumps.variable_jumps...; rng = rng) end - jump_cbs = CallbackSet(constant_jump_callback, variable_jump_callback) + + ## Conditional Rate Handling + if typeof(jumps.conditional_jumps) <: Tuple{} + cond = nothing + conditional_jump_callback = CallBackSet() + else + cond = aggregate(aggregator, u, prob.p, t, end_time, jumps.conditional_jumps, maj, + save_positions, rng; spatial_system = spatial_system, + hopping_constants = hopping_constants, kwargs...) + conditional_jump_callback = DiscreteCallback(cond) + end + jump_cbs = CallbackSet(constant_jump_callback, variable_jump_callback, conditional_jump_callback) solkwargs = make_kwarg(; callback) JumpProblem{iip, typeof(new_prob), typeof(aggregator), From f39abefd5af0094309951cd5d7588be1653fbd65 Mon Sep 17 00:00:00 2001 From: gzagatti Date: Wed, 26 Oct 2022 18:10:25 +0800 Subject: [PATCH 02/72] Incorporates conditional history to QueueMethod. --- src/aggregators/queue.jl | 246 ++++++++++++++++++++------------------- src/jumps.jl | 92 ++++++++------- src/problem.jl | 50 ++++---- 3 files changed, 203 insertions(+), 185 deletions(-) diff --git a/src/aggregators/queue.jl b/src/aggregators/queue.jl index c44bb8b77..59d844599 100644 --- a/src/aggregators/queue.jl +++ b/src/aggregators/queue.jl @@ -1,167 +1,173 @@ """ -The Queue Method. This method handles conditional intensity rates. - -```jl -# simulating a Hawkes process -function rate_factory(prev_rate, t0, u, params, t) - λ0, α, β = params - λt = prev_rate(u, params, t) - - if t == t0 - if λt ≈ λ0 - rate(u, params, s) = λ0 + α*exp(-β*(s-t)) - else - rate(u, params, s) = prev_rate(u, params, t) + α*exp(-β*(s-t)) - end - elseif t > t0 - if λt ≈ λ0 - rate(u, params, s) = λ0 - else - rate = prev_rate - end - else - error("t must be equal or higher than t0") - end - - lrate = λ0 - urate = rate(u, params, t) - if urate < lrate - error("The upper bound rate should not be lower than the lower bound.") - end - L = urate == lrate ? typemax(t) : 1/(2*rate) - - return rate, lrate, urate, L -end -affect!(integrator) = integratro.u[1] += 1 -jump = ConstantRateJump(rate_factory, affect!) -``` +Queue method. This method handles conditional intensity rates. """ -mutable struct QueueMethodJumpAggregation{T, S, F1, F2, F3, RNG, DEPGR, PQ} <: AbstractSSAJumpAggregator - next_jump::Int # the next jump to execute - prev_jump::Int # the previous jump that was executed - next_jump_time::T # the time of the next jump - end_time::T # the time to stop a simulation - cur_rates::F1 # vector of current propensity values - sum_rate::T # sum of current propensity values - ma_jumps::S # any MassActionJumps for the system (scalar form) - rates::F2 # vector of rate functions for ConditionalRateJumps - affects!::F3 # vector of affect functions for ConditionalRateJumps - save_positions::Tuple{Bool, Bool} # tuple for whether the jumps before and/or after event - rng::RNG # random number generator - dep_gr::DEPGR - pq::PQ +mutable struct QueueMethodJumpAggregation{T, S, F1, F2, F3, RNG, DEPGR, PQ} <: + AbstractSSAJumpAggregator + next_jump::Int # the next jump to execute + prev_jump::Int # the previous jump that was executed + next_jump_time::T # the time of the next jump + end_time::T # the time to stop a simulation + cur_rates::F1 # vector of current propensity values + sum_rate::T # sum of current propensity values + ma_jumps::S # any MassActionJumps for the system (scalar form) + rates::F2 # vector of rate functions for ConditionalRateJumps + affects!::F3 # vector of affect functions for ConditionalRateJumps + save_positions::Tuple{Bool, Bool} # tuple for whether to save the jumps before and/or after event + rng::RNG # random number generator + dep_gr::DEPGR # dependency graph + pq::PQ # priority queue of next time + h::Array{Array{T}, 1} # history of jumps end function QueueMethodJumpAggregation(nj::Int, njt::T, et::T, crs::F1, sr::T, maj::S, rs::F2, affs!::F3, sps::Tuple{Bool, Bool}, rng::RNG; dep_graph = nothing, kwargs...) where {T, S, F1, F2, F3, RNG} - if get_num_majumps(maj) > 0 - error("Mass-action jumps are not supported with the Queue Method.") - end - - if dep_graph === nothing - if !isempty(rs) - error("To use ConstantRateJumps with Queue Method algorithm a dependency graph must be supplied.") + if get_num_majumps(maj) > 0 + error("Mass-action jumps are not supported with the Queue Method.") end - else - dg = dep_graph - # make sure each jump depends on itself - add_self_dependencies!(dg) - end - pq = MutableBinaryMinHeap{T}() + if dep_graph === nothing + if !isempty(rs) + error("To use ConstantRateJumps with Queue Method algorithm a dependency graph must be supplied.") + end + else + dg = dep_graph + # make sure each jump depends on itself + add_self_dependencies!(dg) + end - QueueMethodJumpAggregation{T, S, F1, F2, F3, RNG, typeof(dg), typeof(pq)}(nj, nj, njt, et, crs, sr, - maj, rs, affs!, sps, rng, dg, pq) + pq = MutableBinaryMinHeap{T}() + h = Array{Array{T}, 1}(undef, length(rs)) + QueueMethodJumpAggregation{T, S, F1, F2, F3, RNG, typeof(dg), typeof(pq)}(nj, nj, njt, + et, + crs, sr, maj, + rs, + affs!, sps, + rng, + dg, pq, h) end # creating the JumpAggregation structure (tuple-based constant jumps) function aggregate(aggregator::QueueMethod, u, p, t, end_time, conditional_jumps, - ma_jumps, save_positions, rng; kwargs...) - # rates, affects!, RateWrapper = get_jump_info_fwrappers(u, p, t, conditional_jumps) - rates, affects! = get_jump_info_fwrappers(u, p, t, conditional_jumps) - - - sum_rate = zero(typeof(t)) - # cur_rates = Vector{RateWrapper}(nothing, length(conditional_jumps)) - cur_rates = Vector{Any}(nothing, length(conditional_jumps)) - next_jump = 0 - next_jump_time = typemax(typeof(t)) - QueueMethodJumpAggregation(next_jump, next_jump_time, end_time, cur_rates, sum_rate, - ma_jumps, rates, affects!, save_positions, rng; kwargs...) + ma_jumps, save_positions, rng; dep_graph, kwargs...) + # TODO: Fix FunctionWrapper as it unstable with more than 2 processes + # U, P, T, G = typeof(u), typeof(p), typeof(t), typeof(dep_graph) + # RateWrapper = FunctionWrappers.FunctionWrapper{T, Tuple{U, P, T}} + # ConditionalRateWrapper = FunctionWrappers.FunctionWrapper{ + # Tuple{RateWrapper, T, T, T}, + # Tuple{Int, G, + # Array{Array{T, 1}}, U, + # P, T} + # } + # AffectWrapper = FunctionWrappers.FunctionWrapper{Nothing, Tuple{Any}} + # if (conditional_jumps !== nothing) && !isempty(conditional_jumps) + # rates = [ConditionalRateWrapper(c.rate) for c in conditional_jumps] + # affects! = [AffectWrapper(x -> (c.affect!(x); nothing)) for c in conditional_jumps] + # else + # rates = Vector{ConditionalRateWrapper}() + # affects! = Vector{AffectWrapper}() + # end + # cur_rates = Vector{RateWrapper}(undef, length(conditional_jumps)) + AffectWrapper = FunctionWrappers.FunctionWrapper{Nothing, Tuple{Any}} + if (conditional_jumps !== nothing) && !isempty(conditional_jumps) + rates = [c.rate for c in conditional_jumps] + affects! = [AffectWrapper(x -> (c.affect!(x); nothing)) for c in conditional_jumps] + else + rates = Vector{Any}() + affects! = Vector{AffectWrapper}() + end + cur_rates = Array{Any}(undef, length(conditional_jumps)) + sum_rate = zero(typeof(t)) + next_jump = 0 + next_jump_time = typemax(typeof(t)) + QueueMethodJumpAggregation(next_jump, next_jump_time, end_time, cur_rates, sum_rate, + ma_jumps, rates, affects!, save_positions, rng; dep_graph, + kwargs...) end # set up a new simulation and calculate the first jump / jump time function initialize!(p::QueueMethodJumpAggregation, integrator, u, params, t) - p.end_time = integrator.sol.prob.tspan[2] - fill_rates_and_get_times!(p, u, params, t) - generate_jumps!(p, integrator, u, params, t) - nothing + p.end_time = integrator.sol.prob.tspan[2] + p.h = [eltype(p.h)(undef, 0) for _ in 1:length(p.h)] + fill_rates_and_get_times!(p, u, params, t) + generate_jumps!(p, integrator, u, params, t) + nothing end # execute one jump, changing the system state function execute_jumps!(p::QueueMethodJumpAggregation, integrator, u, params, t) # execute jump u = update_state!(p, integrator, u) - # update current jump rates and times update_dependent_rates!(p, u, params, t) - nothing end # calculate the next jump / jump time function generate_jumps!(p::QueueMethodJumpAggregation, integrator, u, params, t) - p.next_jump_time, p.next_jump = top_with_handle(p.pq) - nothing + p.next_jump_time, p.next_jump = top_with_handle(p.pq) + if p.next_jump_time < p.end_time + push!(p.h[p.next_jump], p.next_jump_time) + else + # throw the history away once simulation is over + p.h = Array{eltype(p.h)}(undef, length(p.h)) + end + nothing end ######################## SSA specific helper routines ######################## function update_dependent_rates!(p::QueueMethodJumpAggregation, u, params, t) - @inbounds dep_rxs = p.dep_gr[p.next_jump] - @unpack cur_rates, rates = p + @inbounds dep_rxs = p.dep_gr[p.next_jump] + @unpack cur_rates, rates = p - @inbounds for rx in dep_rxs - @inbounds trx, cur_rates[rx] = next_time(p, rates[rx], cur_rates[rx], u, params, t) - update!(p.pq, rx, trx) - end + @inbounds for rx in dep_rxs + @inbounds trx, cur_rates[rx] = next_time(p, rx, rates[rx], u, params, t) + update!(p.pq, rx, trx) + end - nothing + nothing end -function next_time(p::QueueMethodJumpAggregation, rate_factory, prev_rate, u, params, t) - t0 = t - @unpack end_time, rng = p - rate = nothing - while t < end_time - rate, lrate, urate, L = rate_factory(prev_rate, t0, u, params, t) - s = randexp(rng) / urate - if s > L - t = t + L - continue - end - if urate > lrate - v = rand(rng) - if (v > lrate/urate) && (v > rate(u, params, t + s)/urate) +function next_time(p::QueueMethodJumpAggregation, rx, rate, u, params, t) + @unpack end_time, rng, dep_gr, h = p + cur_rate = nothing + while t < end_time + # println("HERE rx ", rx, " rate ", rate) + # println("HERE rx ", rx, " h ", h, " dep_gr ", dep_gr, " params ", params, " t ", t, " u ", u) + cur_rate, lrate, urate, L = rate(rx, dep_gr, h, u, params, t) + # println("HERE 1") + if lrate > urate + error("The lower bound should be lower than the upper bound rate for t = $(t) and rx = $(rx), but lower bound = $(lrate) > upper bound = $(urate)") + end + # println("HERE 2") + s = randexp(rng) / urate + if s > L + t = t + L + continue + end + # println("HERE 3") + v = rand(rng) + # the first inequality is less expensive and short-circuits the evaluation + if (v > lrate / urate) && (v > cur_rate(u, params, t + s) / urate) + t = t + s + continue + end + # println("HERE 4") t = t + s - continue - end + return t, cur_rate end - t = t + s - return t, rate - end - return typemax(t), rate + return typemax(t), cur_rate end # reevaulate all rates, recalculate all jump times, and reinit the priority queue function fill_rates_and_get_times!(p::QueueMethodJumpAggregation, u, params, t) - @unpack cur_rates, rates = p - pqdata = Vector{eltype(t)}(undef, length(rates)) - @inbounds for (rx, rate) in enumerate(rates) - @inbounds trx, cur_rates[rx] = next_time(p, rates[rx], cur_rates[rx], u, params,t) - pqdata[rx] = trx - end - p.pq = MutableBinaryMinHeap(pqdata) - nothing + @unpack cur_rates, rates = p + pqdata = Vector{eltype(t)}(undef, length(rates)) + @inbounds for (rx, rate) in enumerate(rates) + @inbounds trx, cur_rates[rx] = next_time(p, rx, rate, u, params, t) + pqdata[rx] = trx + end + p.pq = MutableBinaryMinHeap(pqdata) + nothing end diff --git a/src/jumps.jl b/src/jumps.jl index ed6065daf..603488572 100644 --- a/src/jumps.jl +++ b/src/jumps.jl @@ -94,21 +94,50 @@ end """ $(TYPEDEF) -Defines a jump process with a ... +Defines a jump process with a conditional rate. More precisely, one where the rate +function depends on past events. The rate can also change according to time between two jumps. ## Fields $(FIELDS) ## Examples - -Foo ... +Suppose `u[1]` follows a Hawkes jump process. This is a type of self-exciting +process in which the realization of an event increases the likelihood of new +nearby events. A corresponding `ConditionalRateJump` for this jump process is +```julia +function rate(i, g, h, u, p, t) + λ0, α, β = p + _h = typeof(t)[] + for j in g[i] + for _t in reverse(h[i]) + if α*exp(-β*(t - _t)) ≈ 0 break end + push!(_h, _t) + end + end + rate(u, p, s) = λ0 + α * sum([exp(-β*(s - _t)) for _t in _h]) + lrate = λ0 + urate = rate(u, p, t) + L = urate == lrate ? typemax(t) : 1/(2*urate) + return rate, lrate, urate, L +end +affect!(integrator) = integrator.u[1] += 1 +crj = ConditionalRateJump(rate, affect!) +prob = DiscreteProblem(u0, tspan, p) +jprob = JumpProblem(prob, QueueMethod(), crj) +``` """ struct ConditionalRateJump{F1, F2} <: AbstractJump - """Function `rate(u, p, t)` that returns """ - rate::F1 - """Function `affect(integrator)` that updates the state for one occurrence of the jump.""" - affect!::F2 + """ + Function `rate(i, g, h, u, p, t)` that returns `rate(u, p, s)`, `lrate`, + `urate` and `L` for jump `i` with dependency graph `g`, history `h`, state + `u`, parameters `p` and time `t`. `rate(u, p, s)` is a function that computes the + rate at time `s`, `lrate` and `urate` are the lower and upper rate bounds in + interval `t` to `t + L`. + """ + rate::F1 + """Function `affect(integrator)` that updates the state for one occurrence of the jump.""" + affect!::F2 end struct RegularJump{iip, R, C, MD} @@ -422,7 +451,8 @@ JumpSet(jump::RegularJump) = JumpSet((), (), (), jump, nothing) JumpSet(jump::AbstractMassActionJump) = JumpSet((), (), (), nothing, jump) function JumpSet(; variable_jumps = (), constant_jumps = (), conditional_jumps = (), regular_jumps = nothing, massaction_jumps = nothing) - JumpSet(variable_jumps, constant_jumps, conditional_jumps, regular_jumps, massaction_jumps) + JumpSet(variable_jumps, constant_jumps, conditional_jumps, regular_jumps, + massaction_jumps) end JumpSet(jb::Nothing) = JumpSet() @@ -589,40 +619,18 @@ function get_jump_info_tuples(jumps) rates, affects! end -function get_jump_info_fwrappers(u, p, t, jumps) - if eltype(jumps) <: ConstantRateJump - RateWrapper = FunctionWrappers.FunctionWrapper{typeof(t), - Tuple{typeof(u), typeof(p), typeof(t)}} - AffectWrapper = FunctionWrappers.FunctionWrapper{Nothing, Tuple{Any}} +function get_jump_info_fwrappers(u, p, t, constant_jumps) + RateWrapper = FunctionWrappers.FunctionWrapper{typeof(t), + Tuple{typeof(u), typeof(p), typeof(t)}} + AffectWrapper = FunctionWrappers.FunctionWrapper{Nothing, Tuple{Any}} - if (jumps !== nothing) && !isempty(jumps) - rates = [RateWrapper(c.rate) for c in jumps] - affects! = [AffectWrapper(x -> (c.affect!(x); nothing)) for c in jumps] - else - rates = Vector{RateWrapper}() - affects! = Vector{AffectWrapper}() - end - - return rates, affects! - elseif eltype(jumps) <: ConditionalRateJump - # RateWrapper = FunctionWrappers.FunctionWrapper{ - # typeof(t), - # Tuple{typeof(u), typeof(p), typeof(t)} - # } - # RateFactoryWrapper = FunctionWrappers.FunctionWrapper{ - # Tuple{RateWrapper, typeof(t), typeof(t), typeof(t)}, - # Tuple{Union{Nothing, RateWrapper}, typeof(t), typeof(u), typeof(p), typeof(t)} - # } - AffectWrapper = FunctionWrappers.FunctionWrapper{Nothing, Tuple{Any}} - if (jumps !== nothing) & !isempty(jumps) - # rates = [RateFactoryWrapper(c.rate) for c in jumps] - rates = [c.rate for c in jumps] - affects! = [AffectWrapper(x -> (c.affect!(x); nothing)) for c in jumps] - else - rates = Vector{RateFactoryWrapper}() - affects! = Vector{AffectWrapper}() - end - - rates, affects! + if (constant_jumps !== nothing) && !isempty(constant_jumps) + rates = [RateWrapper(c.rate) for c in constant_jumps] + affects! = [AffectWrapper(x -> (c.affect!(x); nothing)) for c in constant_jumps] + else + rates = Vector{RateWrapper}() + affects! = Vector{AffectWrapper}() end + + rates, affects! end diff --git a/src/problem.jl b/src/problem.jl index 4e897fece..f4e8fa2bc 100644 --- a/src/problem.jl +++ b/src/problem.jl @@ -188,17 +188,31 @@ function JumpProblem(prob, aggregator::AbstractAggregatorAlgorithm, jumps::JumpS !is_spatial(aggregator) # check if need to flatten prob, maj = flatten(maj, prob, spatial_system, hopping_constants; kwargs...) end - ## Constant Rate Handling + ## Constant and conditional rate handling t, end_time, u = prob.tspan[1], prob.tspan[2], prob.u0 + # check if there are no jumps if (typeof(jumps.constant_jumps) <: Tuple{}) && (maj === nothing) && - !is_spatial(aggregator) # check if there are no jumps - disc = nothing + !is_spatial(aggregator) && (typeof(jumps.conditional_jumps) <: Tuple{}) + agg = nothing constant_jump_callback = CallbackSet() - else - disc = aggregate(aggregator, u, prob.p, t, end_time, jumps.constant_jumps, maj, - save_positions, rng; spatial_system = spatial_system, - hopping_constants = hopping_constants, kwargs...) - constant_jump_callback = DiscreteCallback(disc) + # constant and conditional jumps are exclusive + elseif (typeof(jumps.conditional_jumps) <: Tuple{}) + if typeof(aggregator) <: QueueMethod + error("`QueueMethod` aggregator is not supported with `ConstantRateJump` or `MassActionJump`.") + end + agg = aggregate(aggregator, u, prob.p, t, end_time, jumps.constant_jumps, maj, + save_positions, rng; spatial_system = spatial_system, + hopping_constants = hopping_constants, kwargs...) + constant_jump_callback = DiscreteCallback(agg) + elseif (typeof(jumps.constant_jumps) <: Tuple{}) && (maj === nothing) && + !is_spatial(aggregator) + if !(typeof(aggregator) <: QueueMethod) + error("`ConditionalRateJump` can only be used with the `QueueMethod` aggregator.") + end + agg = aggregate(aggregator, u, prob.p, t, end_time, jumps.conditional_jumps, maj, + save_positions, rng; spatial_system = spatial_system, + hopping_constants = hopping_constants, kwargs...) + constant_jump_callback = DiscreteCallback(agg) end iip = isinplace_jump(prob, jumps.regular_jump) @@ -213,24 +227,14 @@ function JumpProblem(prob, aggregator::AbstractAggregatorAlgorithm, jumps::JumpS jumps.variable_jumps...; rng = rng) end - ## Conditional Rate Handling - if typeof(jumps.conditional_jumps) <: Tuple{} - cond = nothing - conditional_jump_callback = CallBackSet() - else - cond = aggregate(aggregator, u, prob.p, t, end_time, jumps.conditional_jumps, maj, - save_positions, rng; spatial_system = spatial_system, - hopping_constants = hopping_constants, kwargs...) - conditional_jump_callback = DiscreteCallback(cond) - end - jump_cbs = CallbackSet(constant_jump_callback, variable_jump_callback, conditional_jump_callback) + jump_cbs = CallbackSet(constant_jump_callback, variable_jump_callback) solkwargs = make_kwarg(; callback) JumpProblem{iip, typeof(new_prob), typeof(aggregator), - typeof(jump_cbs), typeof(disc), + typeof(jump_cbs), typeof(agg), typeof(jumps.variable_jumps), typeof(jumps.regular_jump), - typeof(maj), typeof(rng), typeof(solkwargs)}(new_prob, aggregator, disc, + typeof(maj), typeof(rng), typeof(solkwargs)}(new_prob, aggregator, agg, jump_cbs, jumps.variable_jumps, jumps.regular_jump, maj, rng, solkwargs) @@ -285,7 +289,7 @@ function extend_problem(prob::DiffEqBase.AbstractDDEProblem, jumps; rng = DEFAUL ttype = eltype(prob.tspan) u0 = ExtendedJumpArray(prob.u0, [-randexp(rng, ttype) for i in 1:length(jumps.variable_jumps)]) - ramake(prob, f = DDEFunction{true}(jump_f), u0 = u0) + remake(prob, f = DDEFunction{true}(jump_f), u0 = u0) end # Not sure if the DAE one is correct: Should be a residual of sorts @@ -372,7 +376,7 @@ end function Base.show(io::IO, mime::MIME"text/plain", A::JumpProblem) summary(io, A) println(io) - println(io, "Number of constant rate jumps: ", + println(io, "Number of constant/conditional rate jumps: ", A.discrete_jump_aggregation === nothing ? 0 : num_constant_rate_jumps(A.discrete_jump_aggregation)) println(io, "Number of variable rate jumps: ", length(A.variable_jumps)) From 724acb3f8a915601f5813cb3c0c6e397fc1d1247 Mon Sep 17 00:00:00 2001 From: gzagatti Date: Wed, 26 Oct 2022 18:43:58 +0800 Subject: [PATCH 03/72] remove debugging comments. --- src/aggregators/queue.jl | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/aggregators/queue.jl b/src/aggregators/queue.jl index 59d844599..60d782dca 100644 --- a/src/aggregators/queue.jl +++ b/src/aggregators/queue.jl @@ -133,27 +133,21 @@ function next_time(p::QueueMethodJumpAggregation, rx, rate, u, params, t) @unpack end_time, rng, dep_gr, h = p cur_rate = nothing while t < end_time - # println("HERE rx ", rx, " rate ", rate) - # println("HERE rx ", rx, " h ", h, " dep_gr ", dep_gr, " params ", params, " t ", t, " u ", u) cur_rate, lrate, urate, L = rate(rx, dep_gr, h, u, params, t) - # println("HERE 1") if lrate > urate error("The lower bound should be lower than the upper bound rate for t = $(t) and rx = $(rx), but lower bound = $(lrate) > upper bound = $(urate)") end - # println("HERE 2") s = randexp(rng) / urate if s > L t = t + L continue end - # println("HERE 3") v = rand(rng) # the first inequality is less expensive and short-circuits the evaluation if (v > lrate / urate) && (v > cur_rate(u, params, t + s) / urate) t = t + s continue end - # println("HERE 4") t = t + s return t, cur_rate end From 28cb9240b8e8812dccc38a5df5f942eddd60b647 Mon Sep 17 00:00:00 2001 From: gzagatti Date: Thu, 27 Oct 2022 00:17:17 +0800 Subject: [PATCH 04/72] adds marked jump to QueueMethod. --- src/aggregators/queue.jl | 67 +++++++++++++++++++++++++++++----------- src/jumps.jl | 12 +++++-- 2 files changed, 59 insertions(+), 20 deletions(-) diff --git a/src/aggregators/queue.jl b/src/aggregators/queue.jl index 60d782dca..5cbf37b73 100644 --- a/src/aggregators/queue.jl +++ b/src/aggregators/queue.jl @@ -1,7 +1,7 @@ """ Queue method. This method handles conditional intensity rates. """ -mutable struct QueueMethodJumpAggregation{T, S, F1, F2, F3, RNG, DEPGR, PQ} <: +mutable struct QueueMethodJumpAggregation{T, S, F1, F2, F3, F4, RNG, DEPGR, PQ, V} <: AbstractSSAJumpAggregator next_jump::Int # the next jump to execute prev_jump::Int # the previous jump that was executed @@ -16,12 +16,13 @@ mutable struct QueueMethodJumpAggregation{T, S, F1, F2, F3, RNG, DEPGR, PQ} <: rng::RNG # random number generator dep_gr::DEPGR # dependency graph pq::PQ # priority queue of next time - h::Array{Array{T}, 1} # history of jumps + marks::F4 # vector of mark functions for ConditionalRateJumps + h::V # history of jumps end function QueueMethodJumpAggregation(nj::Int, njt::T, et::T, crs::F1, sr::T, maj::S, rs::F2, affs!::F3, sps::Tuple{Bool, Bool}, - rng::RNG; dep_graph = nothing, + rng::RNG; dep_graph = nothing, marks = nothing, kwargs...) where {T, S, F1, F2, F3, RNG} if get_num_majumps(maj) > 0 error("Mass-action jumps are not supported with the Queue Method.") @@ -38,14 +39,19 @@ function QueueMethodJumpAggregation(nj::Int, njt::T, et::T, crs::F1, sr::T, end pq = MutableBinaryMinHeap{T}() - h = Array{Array{T}, 1}(undef, length(rs)) - QueueMethodJumpAggregation{T, S, F1, F2, F3, RNG, typeof(dg), typeof(pq)}(nj, nj, njt, - et, - crs, sr, maj, - rs, - affs!, sps, - rng, - dg, pq, h) + if marks === nothing + h = Array{Array{T}, 1}(undef, length(rs)) + else + h = Array{Array{Tuple{T, T}}, 1}(undef, length(rs)) + end + QueueMethodJumpAggregation{T, S, F1, F2, F3, typeof(marks), RNG, typeof(dg), typeof(pq), + typeof(h)}(nj, nj, njt, + et, + crs, sr, maj, + rs, + affs!, sps, + rng, + dg, pq, marks, h) end # creating the JumpAggregation structure (tuple-based constant jumps) @@ -69,13 +75,25 @@ function aggregate(aggregator::QueueMethod, u, p, t, end_time, conditional_jumps # affects! = Vector{AffectWrapper}() # end # cur_rates = Vector{RateWrapper}(undef, length(conditional_jumps)) - AffectWrapper = FunctionWrappers.FunctionWrapper{Nothing, Tuple{Any}} if (conditional_jumps !== nothing) && !isempty(conditional_jumps) rates = [c.rate for c in conditional_jumps] - affects! = [AffectWrapper(x -> (c.affect!(x); nothing)) for c in conditional_jumps] + marks = [c.mark for c in conditional_jumps] + if eltype(marks) === Nothing + marks = nothing + AffectWrapper = FunctionWrappers.FunctionWrapper{Nothing, Tuple{Int, Any}} + affects! = [AffectWrapper((i, integrator) -> (c.affect!(i, integrator); nothing)) + for c in conditional_jumps] + else + AffectWrapper = FunctionWrappers.FunctionWrapper{Nothing, + Tuple{Int, Any, eltype(u)}} + affects! = [AffectWrapper((i, integrator, m) -> (c.affect!(i, integrator, m); nothing)) + for c in conditional_jumps] + end else rates = Vector{Any}() + AffectWrapper = FunctionWrappers.FunctionWrapper{Nothing, Tuple{Int, Any}} affects! = Vector{AffectWrapper}() + marks = nothing end cur_rates = Array{Any}(undef, length(conditional_jumps)) sum_rate = zero(typeof(t)) @@ -83,6 +101,7 @@ function aggregate(aggregator::QueueMethod, u, p, t, end_time, conditional_jumps next_jump_time = typemax(typeof(t)) QueueMethodJumpAggregation(next_jump, next_jump_time, end_time, cur_rates, sum_rate, ma_jumps, rates, affects!, save_positions, rng; dep_graph, + marks, kwargs...) end @@ -98,7 +117,7 @@ end # execute one jump, changing the system state function execute_jumps!(p::QueueMethodJumpAggregation, integrator, u, params, t) # execute jump - u = update_state!(p, integrator, u) + u = update_state!(p, integrator, u, params, t) # update current jump rates and times update_dependent_rates!(p, u, params, t) nothing @@ -107,13 +126,25 @@ end # calculate the next jump / jump time function generate_jumps!(p::QueueMethodJumpAggregation, integrator, u, params, t) p.next_jump_time, p.next_jump = top_with_handle(p.pq) - if p.next_jump_time < p.end_time + # if p.next_jump_time > p.end_time + # # throw the history away once simulation is over + # p.h = Array{eltype(p.h)}(undef, length(p.h)) + # end + nothing +end + +@inline function update_state!(p::QueueMethodJumpAggregation, integrator, u, params, t) + @unpack next_jump, dep_gr, marks, h = p + if marks === nothing push!(p.h[p.next_jump], p.next_jump_time) + @inbounds p.affects![next_jump](next_jump, integrator) else - # throw the history away once simulation is over - p.h = Array{eltype(p.h)}(undef, length(p.h)) + m = marks[next_jump](next_jump, dep_gr, h, params, t) + push!(p.h[p.next_jump], (p.next_jump_time, m)) + @inbounds p.affects![next_jump](next_jump, integrator, m) end - nothing + p.prev_jump = next_jump + return integrator.u end ######################## SSA specific helper routines ######################## diff --git a/src/jumps.jl b/src/jumps.jl index 603488572..ac75a7c58 100644 --- a/src/jumps.jl +++ b/src/jumps.jl @@ -127,7 +127,7 @@ prob = DiscreteProblem(u0, tspan, p) jprob = JumpProblem(prob, QueueMethod(), crj) ``` """ -struct ConditionalRateJump{F1, F2} <: AbstractJump +struct ConditionalRateJump{F1, F2, F3} <: AbstractJump """ Function `rate(i, g, h, u, p, t)` that returns `rate(u, p, s)`, `lrate`, `urate` and `L` for jump `i` with dependency graph `g`, history `h`, state @@ -136,10 +136,18 @@ struct ConditionalRateJump{F1, F2} <: AbstractJump interval `t` to `t + L`. """ rate::F1 - """Function `affect(integrator)` that updates the state for one occurrence of the jump.""" + """Function `affect!(i, integrator)` or `affect!(i, integrator, m)` that updates the state for one occurrence of the jump.""" affect!::F2 + """ + Function `mark(i, g, h, p, t)` that samples from the mark distribution for + jump `i` given dependency graph `g`, history `h`, parameters `p` and jump + time `t`. If `mark(i, g, h, p, t)` the jump is unmarked. + """ + mark::F3 end +ConditionalRateJump(rate, affect!) = ConditionalRateJump(rate, affect!, nothing) + struct RegularJump{iip, R, C, MD} rate::R c::C From e27a90b3935a2ca5a5e099f194b7499a5f349b5e Mon Sep 17 00:00:00 2001 From: gzagatti Date: Thu, 27 Oct 2022 23:46:39 +0800 Subject: [PATCH 05/72] save history. --- src/SSA_stepper.jl | 2 +- src/aggregators/queue.jl | 26 ++++++++-------- src/problem.jl | 65 +++++++++++++++++++++++++++++++++++++--- 3 files changed, 76 insertions(+), 17 deletions(-) diff --git a/src/SSA_stepper.jl b/src/SSA_stepper.jl index e5c9d080b..7effcb852 100644 --- a/src/SSA_stepper.jl +++ b/src/SSA_stepper.jl @@ -6,7 +6,7 @@ Highly efficient integrator for pure jump problems that involve only ## Notes - Only works with `JumProblem`s defined from `DiscreteProblem`s. -- Only works with collections of `ConstantRateJump`s and `MassActionJump`s. +- Only works with collections of `ConstantRateJump`s, `ConditionalRateJump`s and `MassActionJump`s. - Only supports `DiscreteCallback`s for events. ## Examples diff --git a/src/aggregators/queue.jl b/src/aggregators/queue.jl index 5cbf37b73..7765f16f4 100644 --- a/src/aggregators/queue.jl +++ b/src/aggregators/queue.jl @@ -18,11 +18,13 @@ mutable struct QueueMethodJumpAggregation{T, S, F1, F2, F3, F4, RNG, DEPGR, PQ, pq::PQ # priority queue of next time marks::F4 # vector of mark functions for ConditionalRateJumps h::V # history of jumps + save_history::Bool # whether to save event history after solving end function QueueMethodJumpAggregation(nj::Int, njt::T, et::T, crs::F1, sr::T, maj::S, rs::F2, affs!::F3, sps::Tuple{Bool, Bool}, rng::RNG; dep_graph = nothing, marks = nothing, + save_history = false, kwargs...) where {T, S, F1, F2, F3, RNG} if get_num_majumps(maj) > 0 error("Mass-action jumps are not supported with the Queue Method.") @@ -34,8 +36,6 @@ function QueueMethodJumpAggregation(nj::Int, njt::T, et::T, crs::F1, sr::T, end else dg = dep_graph - # make sure each jump depends on itself - add_self_dependencies!(dg) end pq = MutableBinaryMinHeap{T}() @@ -51,12 +51,12 @@ function QueueMethodJumpAggregation(nj::Int, njt::T, et::T, crs::F1, sr::T, rs, affs!, sps, rng, - dg, pq, marks, h) + dg, pq, marks, h, save_history) end # creating the JumpAggregation structure (tuple-based constant jumps) function aggregate(aggregator::QueueMethod, u, p, t, end_time, conditional_jumps, - ma_jumps, save_positions, rng; dep_graph, kwargs...) + ma_jumps, save_positions, rng; kwargs...) # TODO: Fix FunctionWrapper as it unstable with more than 2 processes # U, P, T, G = typeof(u), typeof(p), typeof(t), typeof(dep_graph) # RateWrapper = FunctionWrappers.FunctionWrapper{T, Tuple{U, P, T}} @@ -100,8 +100,7 @@ function aggregate(aggregator::QueueMethod, u, p, t, end_time, conditional_jumps next_jump = 0 next_jump_time = typemax(typeof(t)) QueueMethodJumpAggregation(next_jump, next_jump_time, end_time, cur_rates, sum_rate, - ma_jumps, rates, affects!, save_positions, rng; dep_graph, - marks, + ma_jumps, rates, affects!, save_positions, rng; marks, kwargs...) end @@ -126,10 +125,10 @@ end # calculate the next jump / jump time function generate_jumps!(p::QueueMethodJumpAggregation, integrator, u, params, t) p.next_jump_time, p.next_jump = top_with_handle(p.pq) - # if p.next_jump_time > p.end_time - # # throw the history away once simulation is over - # p.h = Array{eltype(p.h)}(undef, length(p.h)) - # end + if (p.next_jump_time > p.end_time) && !p.save_history + # throw the history away once simulation is over + p.h = Array{eltype(p.h)}(undef, length(p.h)) + end nothing end @@ -149,8 +148,11 @@ end ######################## SSA specific helper routines ######################## function update_dependent_rates!(p::QueueMethodJumpAggregation, u, params, t) - @inbounds dep_rxs = p.dep_gr[p.next_jump] - @unpack cur_rates, rates = p + @unpack cur_rates, rates, next_jump = p + @inbounds dep_rxs = p.dep_gr[next_jump] + + @inbounds trx, cur_rates[next_jump] = next_time(p, next_jump, rates[next_jump], u, params, t) + update!(p.pq, next_jump, trx) @inbounds for rx in dep_rxs @inbounds trx, cur_rates[rx] = next_time(p, rx, rates[rx], u, params, t) diff --git a/src/problem.jl b/src/problem.jl index f4e8fa2bc..f7d8278a2 100644 --- a/src/problem.jl +++ b/src/problem.jl @@ -169,7 +169,7 @@ function JumpProblem(prob, aggregator::AbstractAggregatorAlgorithm, jumps::JumpS (false, true) : (true, true), rng = DEFAULT_RNG, scale_rates = true, useiszero = true, spatial_system = nothing, hopping_constants = nothing, - callback = nothing, kwargs...) + callback = nothing, save_history = false, kwargs...) # initialize the MassActionJump rate constants with the user parameters if using_params(jumps.massaction_jump) @@ -210,8 +210,7 @@ function JumpProblem(prob, aggregator::AbstractAggregatorAlgorithm, jumps::JumpS error("`ConditionalRateJump` can only be used with the `QueueMethod` aggregator.") end agg = aggregate(aggregator, u, prob.p, t, end_time, jumps.conditional_jumps, maj, - save_positions, rng; spatial_system = spatial_system, - hopping_constants = hopping_constants, kwargs...) + save_positions, rng; save_history = save_history, kwargs...) constant_jump_callback = DiscreteCallback(agg) end @@ -342,7 +341,7 @@ function build_variable_callback(cb, idx, jump; rng = DEFAULT_RNG) CallbackSet(cb, new_cb) end -aggregator(jp::JumpProblem{P, A, C, J, J2}) where {P, A, C, J, J2} = A +aggregator(jp::JumpProblem{iip, P, A, C, J}) where {iip, P, A, C, J} = A @inline function extend_tstops!(tstops, jp::JumpProblem{P, A, C, J, J2}) where {P, A, C, J, J2} @@ -350,6 +349,64 @@ aggregator(jp::JumpProblem{P, A, C, J, J2}) where {P, A, C, J, J2} = A push!(tstops, jp.jump_callback.discrete_callbacks[1].condition.next_jump_time) end +function history(jp::JumpProblem{iip, P, A, C, J}) where {iip, P, A, C, + J <: QueueMethodJumpAggregation} + jp.discrete_jump_aggregation.h +end + +function conditional_rate(jp::JumpProblem{iip, P, A, C, J}, + sol::DiffEqBase.AbstractODESolution; + saveat = nothing) where {iip, P, A, C, + J <: QueueMethodJumpAggregation} + agg = jp.discrete_jump_aggregation + if !agg.save_history + error("History was not saved; set `save_history = true` when calling `JumpProblem`.") + end + if typeof(saveat) <: Number + _saveat = jp.prob.tspan[1]:saveat:jp.prob.tspan[2] + else + _saveat = sol.t + end + h = agg.h + dep_gr = agg.dep_gr + rates = agg.rates + p = jp.prob.p + _h = [eltype(h)(undef, 0) for _ in 1:length(h)] + hixs = zeros(Int, length(h)) + condrates = Array{Array{eltype(_saveat), 1}, 1}() + for t in _saveat + # get history up to t + @inbounds for i in 1:length(h) + # println("HERE2 i ", i) + hi = h[i] + ix = hixs[i] + if eltype(h) <: Tuple + while ((ix + 1) <= length(hi)) && hi[ix + 1][1] <= t + ix += 1 + end + else + while ((ix + 1) <= length(hi)) && hi[ix + 1] <= t + ix += 1 + end + end + _h[i] = ix == 0 ? [] : hi[1:ix] + end + # compute the rate at time t + u = sol(t) + condrate = Array{typeof(t), 1}() + @inbounds for i in 1:length(h) + _rate, = rates[i](i, dep_gr, _h, u, p, t) + push!(condrate, _rate(u, p, t)) + end + push!(condrates, condrate) + end + return DiffEqBase.build_solution(jp.prob, sol.alg, _saveat, condrates, dense = false, + calculate_error = false, + destats = DiffEqBase.DEStats(0), + interp = DiffEqBase.ConstantInterpolation(_saveat, + condrates)) +end + @inline function update_jumps!(du, u, p, t, idx, jump) idx += 1 du[idx] = jump.rate(u.u, p, t) From ed9c118daaefd3e907417019719e99aeac39d656 Mon Sep 17 00:00:00 2001 From: gzagatti Date: Fri, 28 Oct 2022 15:45:53 +0800 Subject: [PATCH 06/72] reorders exports. --- src/JumpProcesses.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/JumpProcesses.jl b/src/JumpProcesses.jl index 13f7b24a3..b500059f9 100644 --- a/src/JumpProcesses.jl +++ b/src/JumpProcesses.jl @@ -80,10 +80,11 @@ export JumpProblem export SplitCoupledJumpProblem -export Direct, DirectFW, SortingDirect, DirectCR, QueueMethod +export Direct, DirectFW, SortingDirect, DirectCR export BracketData, RSSA export FRM, FRMFW, NRM export RSSACR, RDirect +export QueueMethod, history, conditional_rate export get_num_majumps, needs_depgraph, needs_vartojumps_map From 7afd21b2b5c2e33ca1be33b1f5a2e57cb9dd78c1 Mon Sep 17 00:00:00 2001 From: gzagatti Date: Mon, 31 Oct 2022 21:01:28 +0800 Subject: [PATCH 07/72] use VariableRateJump instead and tweaks to next_time algorithm. --- src/JumpProcesses.jl | 2 +- src/SSA_stepper.jl | 2 +- src/aggregators/aggregators.jl | 2 +- src/aggregators/queue.jl | 289 +++++++++++++++++++++------------ src/jumps.jl | 199 ++++++++++++----------- src/problem.jl | 82 +++++----- 6 files changed, 330 insertions(+), 246 deletions(-) diff --git a/src/JumpProcesses.jl b/src/JumpProcesses.jl index b500059f9..ac98f7af8 100644 --- a/src/JumpProcesses.jl +++ b/src/JumpProcesses.jl @@ -73,7 +73,7 @@ include("coupling.jl") include("SSA_stepper.jl") include("simple_regular_solve.jl") -export ConstantRateJump, VariableRateJump, ConditionalRateJump, RegularJump, +export ConstantRateJump, VariableRateJump, RegularJump, MassActionJump, JumpSet export JumpProblem diff --git a/src/SSA_stepper.jl b/src/SSA_stepper.jl index 7effcb852..6d05d1fc7 100644 --- a/src/SSA_stepper.jl +++ b/src/SSA_stepper.jl @@ -6,7 +6,7 @@ Highly efficient integrator for pure jump problems that involve only ## Notes - Only works with `JumProblem`s defined from `DiscreteProblem`s. -- Only works with collections of `ConstantRateJump`s, `ConditionalRateJump`s and `MassActionJump`s. +- Only works with collections of `ConstantRateJump`s, `VariableRateJump`s and `MassActionJump`s. - Only supports `DiscreteCallback`s for events. ## Examples diff --git a/src/aggregators/aggregators.jl b/src/aggregators/aggregators.jl index d04b1f791..bca6a6569 100644 --- a/src/aggregators/aggregators.jl +++ b/src/aggregators/aggregators.jl @@ -145,7 +145,7 @@ doi: 10.1063/1.4928635 struct DirectCRDirect <: AbstractAggregatorAlgorithm end """ -The Queue Method. This method handles conditional intensity rates. +The Queue Method. This method handles variable intensity rates. """ struct QueueMethod <: AbstractAggregatorAlgorithm end diff --git a/src/aggregators/queue.jl b/src/aggregators/queue.jl index 7765f16f4..d9234a3dd 100644 --- a/src/aggregators/queue.jl +++ b/src/aggregators/queue.jl @@ -1,113 +1,137 @@ """ -Queue method. This method handles conditional intensity rates. +Queue method. This method handles variable intensity rates. """ -mutable struct QueueMethodJumpAggregation{T, S, F1, F2, F3, F4, RNG, DEPGR, PQ, V} <: +mutable struct QueueMethodJumpAggregation{T, S, F1, F2, F3, F4, RNG, DEPGR, PQ, H} <: AbstractSSAJumpAggregator - next_jump::Int # the next jump to execute - prev_jump::Int # the previous jump that was executed - next_jump_time::T # the time of the next jump - end_time::T # the time to stop a simulation - cur_rates::F1 # vector of current propensity values - sum_rate::T # sum of current propensity values - ma_jumps::S # any MassActionJumps for the system (scalar form) - rates::F2 # vector of rate functions for ConditionalRateJumps - affects!::F3 # vector of affect functions for ConditionalRateJumps + next_jump::Int # the next jump to execute + prev_jump::Int # the previous jump that was executed + next_jump_time::T # the time of the next jump + end_time::T # the time to stop a simulation + cur_rates::F1 # not used + sum_rate::T # not used + ma_jumps::S # not used + rates::F2 # vector of rate functions + affects!::F3 # vector of affect functions for VariableRateJumps save_positions::Tuple{Bool, Bool} # tuple for whether to save the jumps before and/or after event - rng::RNG # random number generator - dep_gr::DEPGR # dependency graph - pq::PQ # priority queue of next time - marks::F4 # vector of mark functions for ConditionalRateJumps - h::V # history of jumps - save_history::Bool # whether to save event history after solving + rng::RNG # random number generator + dep_gr::DEPGR # dependency graph + pq::PQ # priority queue of next time + marks::F4 # vector of mark functions for VariableRateJumps + h::H # history of jumps + save_history::Bool # whether to save event history after solving + lrates::F2 # vector of rate lower bound functions + urates::F2 # vector of rate upper bound functions + Ls::F2 # vector of interval length functions end function QueueMethodJumpAggregation(nj::Int, njt::T, et::T, crs::F1, sr::T, maj::S, rs::F2, affs!::F3, sps::Tuple{Bool, Bool}, - rng::RNG; dep_graph = nothing, marks = nothing, + rng::RNG; dep_graph = nothing, marks::F4, + history::H, save_history = false, - kwargs...) where {T, S, F1, F2, F3, RNG} + lrates, urates, Ls) where {T, S, F1, F2, F3, F4, RNG, H} if get_num_majumps(maj) > 0 error("Mass-action jumps are not supported with the Queue Method.") end if dep_graph === nothing - if !isempty(rs) - error("To use ConstantRateJumps with Queue Method algorithm a dependency graph must be supplied.") - end + gr = [Int[] for _ in length(rs)] + elseif length(dep_graph) != length(rs) + error("Dependency graph must have same length as the number of jumps.") else - dg = dep_graph + gr = [sort(Int[d for d in deps]) for deps in dep_graph] end - pq = MutableBinaryMinHeap{T}() - if marks === nothing - h = Array{Array{T}, 1}(undef, length(rs)) - else - h = Array{Array{Tuple{T, T}}, 1}(undef, length(rs)) - end - QueueMethodJumpAggregation{T, S, F1, F2, F3, typeof(marks), RNG, typeof(dg), typeof(pq), - typeof(h)}(nj, nj, njt, - et, - crs, sr, maj, - rs, - affs!, sps, - rng, - dg, pq, marks, h, save_history) + pq = PriorityQueue{Int, T}() + QueueMethodJumpAggregation{T, S, F1, F2, F3, F4, RNG, typeof(gr), typeof(pq), + typeof(history)}(nj, nj, njt, + et, + crs, sr, maj, + rs, + affs!, sps, + rng, + gr, pq, marks, history, save_history, + lrates, urates, Ls) end -# creating the JumpAggregation structure (tuple-based constant jumps) -function aggregate(aggregator::QueueMethod, u, p, t, end_time, conditional_jumps, - ma_jumps, save_positions, rng; kwargs...) - # TODO: Fix FunctionWrapper as it unstable with more than 2 processes - # U, P, T, G = typeof(u), typeof(p), typeof(t), typeof(dep_graph) - # RateWrapper = FunctionWrappers.FunctionWrapper{T, Tuple{U, P, T}} - # ConditionalRateWrapper = FunctionWrappers.FunctionWrapper{ - # Tuple{RateWrapper, T, T, T}, - # Tuple{Int, G, - # Array{Array{T, 1}}, U, - # P, T} - # } - # AffectWrapper = FunctionWrappers.FunctionWrapper{Nothing, Tuple{Any}} - # if (conditional_jumps !== nothing) && !isempty(conditional_jumps) - # rates = [ConditionalRateWrapper(c.rate) for c in conditional_jumps] - # affects! = [AffectWrapper(x -> (c.affect!(x); nothing)) for c in conditional_jumps] - # else - # rates = Vector{ConditionalRateWrapper}() - # affects! = Vector{AffectWrapper}() - # end - # cur_rates = Vector{RateWrapper}(undef, length(conditional_jumps)) - if (conditional_jumps !== nothing) && !isempty(conditional_jumps) - rates = [c.rate for c in conditional_jumps] - marks = [c.mark for c in conditional_jumps] +# creating the JumpAggregation structure (tuple-based variable jumps) +function aggregate(aggregator::QueueMethod, u, p, t, end_time, variable_jumps, + ma_jumps, save_positions, rng; dep_graph = nothing, save_history = false, + kwargs...) + U, P, T, H = typeof(u), typeof(p), typeof(t), typeof(t) + G = Vector{Vector{Int}} + if (variable_jumps !== nothing) && !isempty(variable_jumps) + marks = [c.mark for c in variable_jumps] if eltype(marks) === Nothing - marks = nothing - AffectWrapper = FunctionWrappers.FunctionWrapper{Nothing, Tuple{Int, Any}} - affects! = [AffectWrapper((i, integrator) -> (c.affect!(i, integrator); nothing)) - for c in conditional_jumps] + MarkWrapper = Nothing + AffectWrapper = FunctionWrappers.FunctionWrapper{Nothing, Tuple{Any}} + RateWrapper = FunctionWrappers.FunctionWrapper{T, + Tuple{U, P, T, G, + Vector{Vector{H}}}} else + MarkWrapper = FunctionWrappers.FunctionWrapper{U, + Tuple{U, P, T, G, + Vector{Vector{H}}}} AffectWrapper = FunctionWrappers.FunctionWrapper{Nothing, - Tuple{Int, Any, eltype(u)}} - affects! = [AffectWrapper((i, integrator, m) -> (c.affect!(i, integrator, m); nothing)) - for c in conditional_jumps] + Tuple{Any, U}} + H = Tuple{T, U} + RateWrapper = FunctionWrappers.FunctionWrapper{T, + Tuple{U, P, T, G, + Vector{Vector{H}}}} end else - rates = Vector{Any}() + MarkWrapper = Nothing AffectWrapper = FunctionWrappers.FunctionWrapper{Nothing, Tuple{Int, Any}} - affects! = Vector{AffectWrapper}() + RateWrapper = FunctionWrappers.FunctionWrapper{T, + Tuple{U, P, T, G, Vector{Vector{H}}}} + end + + if (variable_jumps !== nothing) && !isempty(variable_jumps) + if eltype(marks) === Nothing + marks = nothing + affects! = [AffectWrapper((integrator) -> (c.affect!(integrator); nothing)) + for c in variable_jumps] + else + marks = [convert(MarkWrapper, m) for m in marks] + affects! = [AffectWrapper((integrator, m) -> (c.affect!(integrator, m); nothing)) + for c in variable_jumps] + end + + history = [jump.history for jump in variable_jumps] + if eltype(history) === Nothing + history = [Vector{H}() for _ in variable_jumps] + else + history = [convert(H, h) for h in history] + end + + rates = [RateWrapper(c.rate) for c in variable_jumps] + lrates = [RateWrapper(c.lrate) for c in variable_jumps] + urates = [RateWrapper(c.urate) for c in variable_jumps] + Ls = [RateWrapper(c.L) for c in variable_jumps] + else marks = nothing + history = Vector{Vector{H}}() + affects! = Vector{AffectWrapper}() + rates = Vector{RateWrapper}() + lrates = Vector{RateWrapper}() + urates = Vector{RateWrapper}() + Ls = Vector{RateWrapper}() end - cur_rates = Array{Any}(undef, length(conditional_jumps)) + cur_rates = nothing sum_rate = zero(typeof(t)) next_jump = 0 next_jump_time = typemax(typeof(t)) QueueMethodJumpAggregation(next_jump, next_jump_time, end_time, cur_rates, sum_rate, - ma_jumps, rates, affects!, save_positions, rng; marks, - kwargs...) + ma_jumps, rates, affects!, save_positions, rng; + dep_graph = dep_graph, marks = marks, + history = history, save_history = save_history, + lrates = lrates, urates = urates, Ls = Ls, kwargs...) end # set up a new simulation and calculate the first jump / jump time function initialize!(p::QueueMethodJumpAggregation, integrator, u, params, t) p.end_time = integrator.sol.prob.tspan[2] - p.h = [eltype(p.h)(undef, 0) for _ in 1:length(p.h)] + reset_history!(p, integrator) fill_rates_and_get_times!(p, u, params, t) generate_jumps!(p, integrator, u, params, t) nothing @@ -124,10 +148,13 @@ end # calculate the next jump / jump time function generate_jumps!(p::QueueMethodJumpAggregation, integrator, u, params, t) - p.next_jump_time, p.next_jump = top_with_handle(p.pq) + p.next_jump, p.next_jump_time = peek(p.pq) + # println("GENERATE_JUMPS! ", peek(p.pq), " p.next_jump_time ", p.next_jump_time, + # " p.next_jump ", + # p.next_jump) if (p.next_jump_time > p.end_time) && !p.save_history # throw the history away once simulation is over - p.h = Array{eltype(p.h)}(undef, length(p.h)) + reset_history!(p, integrator) end nothing end @@ -135,12 +162,14 @@ end @inline function update_state!(p::QueueMethodJumpAggregation, integrator, u, params, t) @unpack next_jump, dep_gr, marks, h = p if marks === nothing + # println("UPDATE_STATE!", " p.next_jump ", p.next_jump, " p.next_jump_time ", + # p.next_jump_time, " integrator ", integrator) push!(p.h[p.next_jump], p.next_jump_time) - @inbounds p.affects![next_jump](next_jump, integrator) + @inbounds p.affects![next_jump](integrator) else - m = marks[next_jump](next_jump, dep_gr, h, params, t) + m = marks[next_jump](u, params, t, dep_gr, h) push!(p.h[p.next_jump], (p.next_jump_time, m)) - @inbounds p.affects![next_jump](next_jump, integrator, m) + @inbounds p.affects![next_jump](integrator, m) end p.prev_jump = next_jump return integrator.u @@ -148,53 +177,97 @@ end ######################## SSA specific helper routines ######################## function update_dependent_rates!(p::QueueMethodJumpAggregation, u, params, t) - @unpack cur_rates, rates, next_jump = p - @inbounds dep_rxs = p.dep_gr[next_jump] + @unpack next_jump, end_time, rates, pq = p + @inbounds deps = copy(p.dep_gr[next_jump]) - @inbounds trx, cur_rates[next_jump] = next_time(p, next_jump, rates[next_jump], u, params, t) - update!(p.pq, next_jump, trx) + Base.insert!(deps, searchsortedfirst(deps, next_jump), next_jump) - @inbounds for rx in dep_rxs - @inbounds trx, cur_rates[rx] = next_time(p, rx, rates[rx], u, params, t) - update!(p.pq, rx, trx) + while !isempty(deps) + j = pop!(deps) + @inbounds tj = next_time(p, j, u, params, t, end_time, deps) + pq[j] = tj end nothing end -function next_time(p::QueueMethodJumpAggregation, rx, rate, u, params, t) - @unpack end_time, rng, dep_gr, h = p - cur_rate = nothing - while t < end_time - cur_rate, lrate, urate, L = rate(rx, dep_gr, h, u, params, t) - if lrate > urate - error("The lower bound should be lower than the upper bound rate for t = $(t) and rx = $(rx), but lower bound = $(lrate) > upper bound = $(urate)") +function next_time(p::QueueMethodJumpAggregation, i, u, params, t, tstop, ignore) + @unpack pq, dep_gr, h = p + # println("NEXT_TIME i ", i) + # we only need to determine whether a jump will take place before any of the dependents + for (j, _t) in pairs(pq) + # println("NEXT_TIME pq LOOP1 j ", j, " _t ", _t) + @inbounds if j != i && !insorted(j, ignore) && insorted(j, dep_gr[i]) && _t < tstop + # println("NEXT_TIME pq LOOP2 j ", j, " tstop ", _t) + tstop = _t + break end - s = randexp(rng) / urate - if s > L - t = t + L + end + return next_time(p, i, u, params, t, tstop) +end + +function next_time(p::QueueMethodJumpAggregation, i, u, params, t, tstop) + @unpack rng, pq, dep_gr, h = p + rate, lrate, urate, L = p.rates[i], p.lrates[i], p.urates[i], p.Ls[i] + while t < tstop + # println("NEXT_TIME i ", i, " u ", u, " params ", params, " t ", t, " dep_gr ", + # dep_gr, + # " h ", h) + _urate = urate(u, params, t, dep_gr, h) + _L = L(u, params, t, dep_gr, h) + s = randexp(rng) / _urate + if s > _L + t = t + _L continue end + _lrate = lrate(u, params, t, dep_gr, h) + if _lrate > _urate + error("The lower bound should be lower than the upper bound rate for t = $(t) and i = $(i), but lower bound = $(_lrate) > upper bound = $(_urate)") + end v = rand(rng) - # the first inequality is less expensive and short-circuits the evaluation - if (v > lrate / urate) && (v > cur_rate(u, params, t + s) / urate) - t = t + s - continue + # first inequality is less expensive and short-circuits the evaluation + if (v > _lrate / _urate) + _rate = rate(u, params, t + s, dep_gr, h) + if (v > _rate / _urate) + t = t + s + continue + end end t = t + s - return t, cur_rate + # println("NEXT TIME RETURN t ", t, " rate ", rate(u, params, t, dep_gr, h)) + return t end - return typemax(t), cur_rate + return typemax(t) end # reevaulate all rates, recalculate all jump times, and reinit the priority queue function fill_rates_and_get_times!(p::QueueMethodJumpAggregation, u, params, t) - @unpack cur_rates, rates = p - pqdata = Vector{eltype(t)}(undef, length(rates)) - @inbounds for (rx, rate) in enumerate(rates) - @inbounds trx, cur_rates[rx] = next_time(p, rx, rate, u, params, t) - pqdata[rx] = trx + @unpack rates, end_time = p + pqdata = Vector{Tuple{Int, eltype(t)}}(undef, length(rates)) + @inbounds for i in 1:length(rates) + @inbounds ti = next_time(p, i, u, params, t, end_time) + pqdata[i] = (i, ti) + end + p.pq = PriorityQueue(pqdata) + nothing +end + +function reset_history!(p::QueueMethodJumpAggregation, integrator) + start_time = integrator.sol.prob.tspan[1] + @unpack h = p + @inbounds for i in 1:length(h) + hi = h[i] + ix = 0 + if eltype(h) <: Tuple + while ((ix + 1) <= length(hi)) && hi[ix + 1][1] <= start_time + ix += 1 + end + else + while ((ix + 1) <= length(hi)) && hi[ix + 1] <= start_time + ix += 1 + end + end + h[i] = ix == 0 ? eltype(h)[] : hi[1:ix] end - p.pq = MutableBinaryMinHeap(pqdata) nothing end diff --git a/src/jumps.jl b/src/jumps.jl index ac75a7c58..26c97217e 100644 --- a/src/jumps.jl +++ b/src/jumps.jl @@ -50,7 +50,34 @@ jump process is ```julia rate(u,p,t) = t*p[1]*u[1] affect!(integrator) = integrator.u[1] -= 1 -crj = VariableRateJump(rate, affect!) +vrj = VariableRateJump(rate, affect!) +``` + +Suppose `u[1]` follows a Hawkes jump process. This is a type of self-exciting +process in which the realization of an event increases the likelihood of new +nearby events. A corresponding `VariableRateJump` for this jump process is +```julia +function rate(u, p, t, g, h) + λ, α, β = p + x = zero(typeof(t)) + for _t in reverse(h[1]) + _x = α*exp(-β*(t - _t)) + if _x ≈ 0 break end + x += _x + end + return λ + x +end +lrate(u, p, t, g, h) = p[1] +urate(u, p, ,t, g, h) = rate(u, p, t, g, h) +function L(u, p, t, g, h) + _lrate = lrate(u, p, t, g, h) + _urate = urate(u, p, t, g, h) + return _urate == _lrate ? typemax(t) : 1/(2*_urate) +end +affect!(integrator) = integrator.u[1] += 1 +vrj = VariableRateJump(rate, affect!; lrate=lrate, urate=urate, L=L) +prob = DiscreteProblem(u0, tspan, p) +jprob = JumpProblem(prob, QueueMethod(), vrj) ``` ## Notes @@ -60,18 +87,46 @@ crj = VariableRateJump(rate, affect!) `VariableRateJump`s will result in all `ConstantRateJump`, `VariableRateJump` and callback `affect!` functions receiving an integrator with `integrator.u` an [`ExtendedJumpArray`](@ref). -- Must be used with `ODEProblem`s or `SDEProblem`s to be correctly simulated - (i.e. can not currently be used with `DiscreteProblem`s). +- When using the `QueueMethod` aggregator `DiscreteProblem` can be used. + Otherwise, `ODEProblem` or `SDEProblem` must be used to be correctly simulated. - Salis H., Kaznessis Y., Accurate hybrid stochastic simulation of a system of coupled chemical or biochemical reactions, Journal of Chemical Physics, 122 (5), DOI:10.1063/1.1835951 is used for calculating jump times with `VariableRateJump`s within ODE/SDE integrators. """ -struct VariableRateJump{R, F, I, T, T2} <: AbstractJump - """Function `rate(u,p,t)` that returns the jump's current rate.""" +struct VariableRateJump{R, F, F3, H, R2, R3, R4, I, T, T2} <: AbstractJump + """When planning to use the `QueueMethod` aggregator, function `lrate(u, p, + t, g, h)` that computes the rate at time `t` given state `u`, parameters + `p`, dependency graph `g` and history `h`. If using another aggregator, + function `rate(u,p,t)` that returns the jump's current rate.""" rate::R - """Function `affect(integrator)` that updates the state for one occurrence of the jump.""" + """When planning to use marks, function `affect!(integrator, m)` that updates + the state for one occurrence of the jump given `integrator` and sampled mark + `m`. Otherwise, function `affect!(integrator)` that does not depend on the + mark.""" affect!::F + """Function `mark(u, p, t, g, h)` that samples from the mark distribution for + jump, given dependency graph `g`, history `h`, parameters `p` and jump + time `t`. If `mark(u, p, t, g, h) === nothing` the jump is unmarked.""" + mark::F3 + """Array with previous jump history.""" + history::H + """When planning to use the `QueueMethod` aggregator, function `lrate(u, p, + t, g, h)` that computes the lower rate bound in interval `t` to `t + L` at + time `t` given state `u`, parameters `p`, dependency graph `g` and history + `h`. This is not required if using another aggregator.""" + lrate::R2 + """When planning to use the `QueueMethod` aggregator, function `lrate(u, p, + t, g, h)` that computes the upper rate bound in interval `t` to `t + L` at + time `t` given state `u`, parameters `p`, dependency graph `g` and history + `h`. This is not required if using another aggregator.""" + urate::R3 + """When planning to use the `QueueMethod` aggregator, function `lrate(u, p, + t, g, h)` that computes the interval length `L` starting at time `t` given + state `u`, parameters `p`, dependency graph `g` and history `h` for which + the rate is bounded between `lrate` and `urate`. This is not required if + using another aggregator.""" + L::R4 idxs::I rootfind::Bool interp_points::Int @@ -81,72 +136,33 @@ struct VariableRateJump{R, F, I, T, T2} <: AbstractJump end function VariableRateJump(rate, affect!; + mark = nothing, + history = nothing, + lrate = nothing, urate = nothing, + L = nothing, rootfind = true, idxs = nothing, - rootfind = true, - save_positions = (true, true), + save_positions = (lrate !== nothing && urate !== nothing && + L !== nothing) ? (false, true) : (true, true), interp_points = 10, abstol = 1e-12, reltol = 0) - VariableRateJump(rate, affect!, idxs, - rootfind, interp_points, - save_positions, abstol, reltol) -end - -""" -$(TYPEDEF) - -Defines a jump process with a conditional rate. More precisely, one where the rate -function depends on past events. The rate can also change according to time between two jumps. - -## Fields - -$(FIELDS) - -## Examples -Suppose `u[1]` follows a Hawkes jump process. This is a type of self-exciting -process in which the realization of an event increases the likelihood of new -nearby events. A corresponding `ConditionalRateJump` for this jump process is -```julia -function rate(i, g, h, u, p, t) - λ0, α, β = p - _h = typeof(t)[] - for j in g[i] - for _t in reverse(h[i]) - if α*exp(-β*(t - _t)) ≈ 0 break end - push!(_h, _t) + if !((lrate === nothing && urate === nothing && L === nothing) || + (lrate !== nothing && urate !== nothing && L !== nothing)) + error("Either `lrate`, `urate` and `L` must be nothing, or all of them must be defined.") end - end - rate(u, p, s) = λ0 + α * sum([exp(-β*(s - _t)) for _t in _h]) - lrate = λ0 - urate = rate(u, p, t) - L = urate == lrate ? typemax(t) : 1/(2*urate) - return rate, lrate, urate, L -end -affect!(integrator) = integrator.u[1] += 1 -crj = ConditionalRateJump(rate, affect!) -prob = DiscreteProblem(u0, tspan, p) -jprob = JumpProblem(prob, QueueMethod(), crj) -``` -""" -struct ConditionalRateJump{F1, F2, F3} <: AbstractJump - """ - Function `rate(i, g, h, u, p, t)` that returns `rate(u, p, s)`, `lrate`, - `urate` and `L` for jump `i` with dependency graph `g`, history `h`, state - `u`, parameters `p` and time `t`. `rate(u, p, s)` is a function that computes the - rate at time `s`, `lrate` and `urate` are the lower and upper rate bounds in - interval `t` to `t + L`. - """ - rate::F1 - """Function `affect!(i, integrator)` or `affect!(i, integrator, m)` that updates the state for one occurrence of the jump.""" - affect!::F2 - """ - Function `mark(i, g, h, p, t)` that samples from the mark distribution for - jump `i` given dependency graph `g`, history `h`, parameters `p` and jump - time `t`. If `mark(i, g, h, p, t)` the jump is unmarked. - """ - mark::F3 + + VariableRateJump(rate, affect!, mark, history, lrate, urate, L, idxs, rootfind, + interp_points, save_positions, abstol, reltol) end -ConditionalRateJump(rate, affect!) = ConditionalRateJump(rate, affect!, nothing) +function VariableRateJump(jump::ConstantRateJump) + rate = (u, p, t, g, h) -> jump.rate(u, p, t) + L = (u, p, t, g, h) -> typemax(t) + VariableRateJump(rate, jump.affect!; mark = nothing, history = nothing, lrate = rate, + urate = rate, L = L, idx = nothing, rootfind = true, + save_positions = (false, true), + interp_points = 10, + abstol = 1e-12, reltol = 0) +end struct RegularJump{iip, R, C, MD} rate::R @@ -436,14 +452,12 @@ jprob = JumpProblem(oprob, Direct(), jset) sol = solve(jprob, Tsit5()) ``` """ -struct JumpSet{T1, T2, T3, T4, T5} <: AbstractJump +struct JumpSet{T1, T2, T3, T4} <: AbstractJump """Collection of [`VariableRateJump`](@ref)s""" variable_jumps::T1 """Collection of [`ConstantRateJump`](@ref)s""" constant_jumps::T2 - """Collection of [`ConditionalRateJump`](@ref)s""" - conditional_jumps::T5 - """Collection of `RegularJump`s""" + """Collection of [`RegularRateJump`](@ref)s""" regular_jump::T3 """Collection of [`MassActionJump`](@ref)s""" massaction_jump::T4 @@ -452,25 +466,24 @@ function JumpSet(vj, cj, rj, maj::MassActionJump{S, T, U, V}) where {S <: Number JumpSet(vj, cj, rj, check_majump_type(maj)) end -JumpSet(jump::VariableRateJump) = JumpSet((jump,), (), (), nothing, nothing) -JumpSet(jump::ConstantRateJump) = JumpSet((), (jump,), (), nothing, nothing) -JumpSet(jump::ConditionalRateJump) = JumpSet((), (), (jump,), nothing, nothing) -JumpSet(jump::RegularJump) = JumpSet((), (), (), jump, nothing) -JumpSet(jump::AbstractMassActionJump) = JumpSet((), (), (), nothing, jump) -function JumpSet(; variable_jumps = (), constant_jumps = (), conditional_jumps = (), +JumpSet(jump::VariableRateJump) = JumpSet((jump,), (), nothing, nothing) +JumpSet(jump::ConstantRateJump) = JumpSet((), (jump,), nothing, nothing) +JumpSet(jump::RegularJump) = JumpSet((), (), jump, nothing) +JumpSet(jump::AbstractMassActionJump) = JumpSet((), (), nothing, jump) +function JumpSet(; variable_jumps = (), constant_jumps = (), regular_jumps = nothing, massaction_jumps = nothing) - JumpSet(variable_jumps, constant_jumps, conditional_jumps, regular_jumps, + JumpSet(variable_jumps, constant_jumps, regular_jumps, massaction_jumps) end JumpSet(jb::Nothing) = JumpSet() # For Varargs, use recursion to make it type-stable function JumpSet(jumps::AbstractJump...) - JumpSet(split_jumps((), (), (), nothing, nothing, jumps...)...) + JumpSet(split_jumps((), (), nothing, nothing, jumps...)...) end # handle vector of mass action jumps -function JumpSet(vjs, cjs, djs, rj, majv::Vector{T}) where {T <: MassActionJump} +function JumpSet(vjs, cjs, rj, majv::Vector{T}) where {T <: MassActionJump} if isempty(majv) error("JumpSets do not accept empty mass action jump collections; use \"nothing\" instead.") end @@ -481,31 +494,27 @@ function JumpSet(vjs, cjs, djs, rj, majv::Vector{T}) where {T <: MassActionJump} massaction_jump_combine(maj, majv[i]) end - JumpSet(vjs, cjs, djs, rj, maj) + JumpSet(vjs, cjs, rj, maj) end @inline get_num_majumps(jset::JumpSet) = get_num_majumps(jset.massaction_jump) -@inline split_jumps(vj, cj, dj, rj, maj) = vj, cj, dj, rj, maj -@inline function split_jumps(vj, cj, dj, rj, maj, v::VariableRateJump, args...) - split_jumps((vj..., v), cj, dj, rj, maj, args...) -end -@inline function split_jumps(vj, cj, dj, rj, maj, c::ConstantRateJump, args...) - split_jumps(vj, (cj..., c), dj, rj, maj, args...) +@inline split_jumps(vj, cj, rj, maj) = vj, cj, rj, maj +@inline function split_jumps(vj, cj, rj, maj, v::VariableRateJump, args...) + split_jumps((vj..., v), cj, rj, maj, args...) end -@inline function split_jumps(vj, cj, dj, rj, maj, d::ConditionalRateJump, args...) - split_jumps(vj, cj, (dj..., d), rj, maj, args...) +@inline function split_jumps(vj, cj, rj, maj, c::ConstantRateJump, args...) + split_jumps(vj, (cj..., c), rj, maj, args...) end -@inline function split_jumps(vj, cj, dj, rj, maj, c::RegularJump, args...) - split_jumps(vj, cj, dj, regular_jump_combine(rj, c), maj, args...) +@inline function split_jumps(vj, cj, rj, maj, c::RegularJump, args...) + split_jumps(vj, cj, regular_jump_combine(rj, c), maj, args...) end -@inline function split_jumps(vj, cj, dj, rj, maj, c::MassActionJump, args...) - split_jumps(vj, cj, dj, rj, massaction_jump_combine(maj, c), args...) +@inline function split_jumps(vj, cj, rj, maj, c::MassActionJump, args...) + split_jumps(vj, cj, rj, massaction_jump_combine(maj, c), args...) end -@inline function split_jumps(vj, cj, dj, rj, maj, j::JumpSet, args...) +@inline function split_jumps(vj, cj, rj, maj, j::JumpSet, args...) split_jumps((vj..., j.variable_jumps...), (cj..., j.constant_jumps...), - (dj..., j.conditional_jumps...), regular_jump_combine(rj, j.regular_jump), massaction_jump_combine(maj, j.massaction_jump), args...) end @@ -614,7 +623,7 @@ function massaction_jump_combine(maj1::MassActionJump, maj2::MassActionJump) maj2.param_mapper) end -##### helper methods for unpacking rates and affects! from constant or conditional jumps ##### +##### helper methods for unpacking rates and affects! from constant jumps ##### function get_jump_info_tuples(jumps) if (jumps !== nothing) && !isempty(jumps) rates = ((c.rate for c in jumps)...,) diff --git a/src/problem.jl b/src/problem.jl index f7d8278a2..07d9a6739 100644 --- a/src/problem.jl +++ b/src/problem.jl @@ -64,7 +64,7 @@ mutable struct JumpProblem{iip, P, A, C, J <: Union{Nothing, AbstractJumpAggrega aggregator::A """The underlying state data associated with the chosen aggregator.""" discrete_jump_aggregation::J - """`CallBackSet` with the underlying `ConstantRate`, `VariableRate` and `ConditionalRateJump` jumps.""" + """`CallBackSet` with the underlying `ConstantRate` and `VariableRate` jumps.""" jump_callback::C """The `VariableRateJump`s.""" variable_jumps::J2 @@ -125,9 +125,6 @@ end function JumpProblem(prob, jumps::VariableRateJump; kwargs...) JumpProblem(prob, JumpSet(jumps); kwargs...) end -function JumpProblem(prob, jumps::ConditionalRateJump; kwargs...) - JumpProblem(prob, JumpSet(jumps); kwargs...) -end function JumpProblem(prob, jumps::RegularJump; kwargs...) JumpProblem(prob, JumpSet(jumps); kwargs...) end @@ -169,7 +166,7 @@ function JumpProblem(prob, aggregator::AbstractAggregatorAlgorithm, jumps::JumpS (false, true) : (true, true), rng = DEFAULT_RNG, scale_rates = true, useiszero = true, spatial_system = nothing, hopping_constants = nothing, - callback = nothing, save_history = false, kwargs...) + callback = nothing, kwargs...) # initialize the MassActionJump rate constants with the user parameters if using_params(jumps.massaction_jump) @@ -188,53 +185,58 @@ function JumpProblem(prob, aggregator::AbstractAggregatorAlgorithm, jumps::JumpS !is_spatial(aggregator) # check if need to flatten prob, maj = flatten(maj, prob, spatial_system, hopping_constants; kwargs...) end - ## Constant and conditional rate handling + ## Constant rate handling t, end_time, u = prob.tspan[1], prob.tspan[2], prob.u0 # check if there are no jumps if (typeof(jumps.constant_jumps) <: Tuple{}) && (maj === nothing) && - !is_spatial(aggregator) && (typeof(jumps.conditional_jumps) <: Tuple{}) - agg = nothing + !is_spatial(aggregator) && !(typeof(aggregator) <: QueueMethod) + disc_agg = nothing constant_jump_callback = CallbackSet() - # constant and conditional jumps are exclusive - elseif (typeof(jumps.conditional_jumps) <: Tuple{}) - if typeof(aggregator) <: QueueMethod - error("`QueueMethod` aggregator is not supported with `ConstantRateJump` or `MassActionJump`.") - end - agg = aggregate(aggregator, u, prob.p, t, end_time, jumps.constant_jumps, maj, - save_positions, rng; spatial_system = spatial_system, - hopping_constants = hopping_constants, kwargs...) - constant_jump_callback = DiscreteCallback(agg) - elseif (typeof(jumps.constant_jumps) <: Tuple{}) && (maj === nothing) && - !is_spatial(aggregator) - if !(typeof(aggregator) <: QueueMethod) - error("`ConditionalRateJump` can only be used with the `QueueMethod` aggregator.") - end - agg = aggregate(aggregator, u, prob.p, t, end_time, jumps.conditional_jumps, maj, - save_positions, rng; save_history = save_history, kwargs...) - constant_jump_callback = DiscreteCallback(agg) + elseif !(typeof(aggregator) <: QueueMethod) + disc_agg = aggregate(aggregator, u, prob.p, t, end_time, jumps.constant_jumps, maj, + save_positions, rng; spatial_system = spatial_system, + hopping_constants = hopping_constants, kwargs...) + constant_jump_callback = DiscreteCallback(disc_agg) end iip = isinplace_jump(prob, jumps.regular_jump) - ## Variable Rate Handling + ## variable Rate Handling + variable_jumps = nothing if typeof(jumps.variable_jumps) <: Tuple{} new_prob = prob variable_jump_callback = CallbackSet() + cont_agg = JumpSet().variable_jumps + elseif typeof(aggregator) <: QueueMethod + if (typeof(jumps.constant_jumps) <: Tuple{}) + variable_jumps = jumps.variable_jumps + else + variable_jumps = [jump for jump in jumps.variable_jumps] + append!(variable_jumps, + [VariableRateJump(jump) for jump in jumps.constant_jumps]) + end + new_prob = prob + disc_agg = aggregate(aggregator, u, prob.p, t, end_time, variable_jumps, maj, + save_positions, rng; kwargs...) + constant_jump_callback = DiscreteCallback(disc_agg) + variable_jump_callback = CallbackSet() + cont_agg = JumpSet().variable_jumps else new_prob = extend_problem(prob, jumps; rng = rng) variable_jump_callback = build_variable_callback(CallbackSet(), 0, jumps.variable_jumps...; rng = rng) + cont_agg = jumps.variable_jumps end jump_cbs = CallbackSet(constant_jump_callback, variable_jump_callback) solkwargs = make_kwarg(; callback) JumpProblem{iip, typeof(new_prob), typeof(aggregator), - typeof(jump_cbs), typeof(agg), - typeof(jumps.variable_jumps), + typeof(jump_cbs), typeof(disc_agg), + typeof(cont_agg), typeof(jumps.regular_jump), - typeof(maj), typeof(rng), typeof(solkwargs)}(new_prob, aggregator, agg, - jump_cbs, jumps.variable_jumps, + typeof(maj), typeof(rng), typeof(solkwargs)}(new_prob, aggregator, disc_agg, + jump_cbs, cont_agg, jumps.regular_jump, maj, rng, solkwargs) end @@ -381,13 +383,13 @@ function conditional_rate(jp::JumpProblem{iip, P, A, C, J}, hi = h[i] ix = hixs[i] if eltype(h) <: Tuple - while ((ix + 1) <= length(hi)) && hi[ix + 1][1] <= t - ix += 1 - end + while ((ix + 1) <= length(hi)) && hi[ix + 1][1] <= t + ix += 1 + end else - while ((ix + 1) <= length(hi)) && hi[ix + 1] <= t - ix += 1 - end + while ((ix + 1) <= length(hi)) && hi[ix + 1] <= t + ix += 1 + end end _h[i] = ix == 0 ? [] : hi[1:ix] end @@ -395,8 +397,8 @@ function conditional_rate(jp::JumpProblem{iip, P, A, C, J}, u = sol(t) condrate = Array{typeof(t), 1}() @inbounds for i in 1:length(h) - _rate, = rates[i](i, dep_gr, _h, u, p, t) - push!(condrate, _rate(u, p, t)) + _rate = rates[i](u, p, t, dep_gr, _h) + push!(condrate, _rate) end push!(condrates, condrate) end @@ -433,10 +435,10 @@ end function Base.show(io::IO, mime::MIME"text/plain", A::JumpProblem) summary(io, A) println(io) - println(io, "Number of constant/conditional rate jumps: ", + println(io, "Number of jumps with discrete aggregation: ", A.discrete_jump_aggregation === nothing ? 0 : num_constant_rate_jumps(A.discrete_jump_aggregation)) - println(io, "Number of variable rate jumps: ", length(A.variable_jumps)) + println(io, "Number of jumps with continuous aggregation: ", length(A.variable_jumps)) nmajs = (A.massaction_jump !== nothing) ? get_num_majumps(A.massaction_jump) : 0 println(io, "Number of mass action jumps: ", nmajs) if A.regular_jump !== nothing From 94dd52e332b65074ab1dedc6033805992989fa32 Mon Sep 17 00:00:00 2001 From: gzagatti Date: Mon, 31 Oct 2022 23:45:36 +0800 Subject: [PATCH 08/72] improve initialization performance by removing FunctionWrapper. --- src/aggregators/queue.jl | 116 +++++++++++++++++++++++++-------------- 1 file changed, 76 insertions(+), 40 deletions(-) diff --git a/src/aggregators/queue.jl b/src/aggregators/queue.jl index d9234a3dd..505752066 100644 --- a/src/aggregators/queue.jl +++ b/src/aggregators/queue.jl @@ -58,64 +58,100 @@ end function aggregate(aggregator::QueueMethod, u, p, t, end_time, variable_jumps, ma_jumps, save_positions, rng; dep_graph = nothing, save_history = false, kwargs...) - U, P, T, H = typeof(u), typeof(p), typeof(t), typeof(t) - G = Vector{Vector{Int}} - if (variable_jumps !== nothing) && !isempty(variable_jumps) - marks = [c.mark for c in variable_jumps] - if eltype(marks) === Nothing - MarkWrapper = Nothing - AffectWrapper = FunctionWrappers.FunctionWrapper{Nothing, Tuple{Any}} - RateWrapper = FunctionWrappers.FunctionWrapper{T, - Tuple{U, P, T, G, - Vector{Vector{H}}}} - else - MarkWrapper = FunctionWrappers.FunctionWrapper{U, - Tuple{U, P, T, G, - Vector{Vector{H}}}} - AffectWrapper = FunctionWrappers.FunctionWrapper{Nothing, - Tuple{Any, U}} - H = Tuple{T, U} - RateWrapper = FunctionWrappers.FunctionWrapper{T, - Tuple{U, P, T, G, - Vector{Vector{H}}}} - end - else - MarkWrapper = Nothing - AffectWrapper = FunctionWrappers.FunctionWrapper{Nothing, Tuple{Int, Any}} - RateWrapper = FunctionWrappers.FunctionWrapper{T, - Tuple{U, P, T, G, Vector{Vector{H}}}} - end + # TODO: FunctionWrapper slows down big problems + # U, P, T, H = typeof(u), typeof(p), typeof(t), typeof(t) + # G = Vector{Vector{Int}} + # if (variable_jumps !== nothing) && !isempty(variable_jumps) + # marks = [c.mark for c in variable_jumps] + # if eltype(marks) === Nothing + # MarkWrapper = Nothing + # AffectWrapper = FunctionWrappers.FunctionWrapper{Nothing, Tuple{Any}} + # RateWrapper = FunctionWrappers.FunctionWrapper{T, + # Tuple{U, P, T, G, + # Vector{Vector{H}}}} + # else + # MarkWrapper = FunctionWrappers.FunctionWrapper{U, + # Tuple{U, P, T, G, + # Vector{Vector{H}}}} + # AffectWrapper = FunctionWrappers.FunctionWrapper{Nothing, + # Tuple{Any, U}} + # H = Tuple{T, U} + # RateWrapper = FunctionWrappers.FunctionWrapper{T, + # Tuple{U, P, T, G, + # Vector{Vector{H}}}} + # end + # else + # MarkWrapper = Nothing + # AffectWrapper = FunctionWrappers.FunctionWrapper{Nothing, Tuple{Int, Any}} + # RateWrapper = FunctionWrappers.FunctionWrapper{T, + # Tuple{U, P, T, G, Vector{Vector{H}}}} + # end + # if (variable_jumps !== nothing) && !isempty(variable_jumps) + # if eltype(marks) === Nothing + # marks = nothing + # affects! = [AffectWrapper((integrator) -> (c.affect!(integrator); nothing)) + # for c in variable_jumps] + # else + # marks = [convert(MarkWrapper, m) for m in marks] + # affects! = [AffectWrapper((integrator, m) -> (c.affect!(integrator, m); nothing)) + # for c in variable_jumps] + # end + + # history = [jump.history for jump in variable_jumps] + # if eltype(history) === Nothing + # history = [Vector{H}() for _ in variable_jumps] + # else + # history = [convert(H, h) for h in history] + # end + + # rates = [RateWrapper(c.rate) for c in variable_jumps] + # lrates = [RateWrapper(c.lrate) for c in variable_jumps] + # urates = [RateWrapper(c.urate) for c in variable_jumps] + # Ls = [RateWrapper(c.L) for c in variable_jumps] + # else + # marks = nothing + # history = Vector{Vector{H}}() + # affects! = Vector{AffectWrapper}() + # rates = Vector{RateWrapper}() + # lrates = Vector{RateWrapper}() + # urates = Vector{RateWrapper}() + # Ls = Vector{RateWrapper}() + # end + U, T, H = typeof(u), typeof(t), typeof(t) if (variable_jumps !== nothing) && !isempty(variable_jumps) + marks = [c.mark for c in variable_jumps] if eltype(marks) === Nothing marks = nothing + AffectWrapper = FunctionWrappers.FunctionWrapper{Nothing, Tuple{Any}} affects! = [AffectWrapper((integrator) -> (c.affect!(integrator); nothing)) for c in variable_jumps] else - marks = [convert(MarkWrapper, m) for m in marks] + AffectWrapper = FunctionWrappers.FunctionWrapper{Nothing, + Tuple{Any, eltype(u)}} affects! = [AffectWrapper((integrator, m) -> (c.affect!(integrator, m); nothing)) for c in variable_jumps] + H = Tuple{T, eltype(u)} end - history = [jump.history for jump in variable_jumps] if eltype(history) === Nothing history = [Vector{H}() for _ in variable_jumps] else - history = [convert(H, h) for h in history] + history = [convert(Vector{H}, h) for h in history] end - - rates = [RateWrapper(c.rate) for c in variable_jumps] - lrates = [RateWrapper(c.lrate) for c in variable_jumps] - urates = [RateWrapper(c.urate) for c in variable_jumps] - Ls = [RateWrapper(c.L) for c in variable_jumps] + rates = Any[c.rate for c in variable_jumps] + lrates = Any[c.lrate for c in variable_jumps] + urates = Any[c.urate for c in variable_jumps] + Ls = Any[c.L for c in variable_jumps] else marks = nothing - history = Vector{Vector{H}}() + AffectWrapper = FunctionWrappers.FunctionWrapper{Nothing, Tuple{Any}} affects! = Vector{AffectWrapper}() - rates = Vector{RateWrapper}() - lrates = Vector{RateWrapper}() - urates = Vector{RateWrapper}() - Ls = Vector{RateWrapper}() + history = Vector{Vector{H}}() + rates = Vector{Any}() + lrates = Vector{Any}() + urates = Vector{Any}() + Ls = Vector{Any}() end cur_rates = nothing sum_rate = zero(typeof(t)) From dc9376dbff6f7233a8be592762b5ee270dd18c01 Mon Sep 17 00:00:00 2001 From: gzagatti Date: Mon, 31 Oct 2022 23:46:12 +0800 Subject: [PATCH 09/72] fix bugs. --- src/aggregators/queue.jl | 2 +- src/problem.jl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/aggregators/queue.jl b/src/aggregators/queue.jl index 505752066..059de535d 100644 --- a/src/aggregators/queue.jl +++ b/src/aggregators/queue.jl @@ -294,7 +294,7 @@ function reset_history!(p::QueueMethodJumpAggregation, integrator) @inbounds for i in 1:length(h) hi = h[i] ix = 0 - if eltype(h) <: Tuple + if eltype(hi) <: Tuple while ((ix + 1) <= length(hi)) && hi[ix + 1][1] <= start_time ix += 1 end diff --git a/src/problem.jl b/src/problem.jl index 07d9a6739..2e72479c0 100644 --- a/src/problem.jl +++ b/src/problem.jl @@ -382,7 +382,7 @@ function conditional_rate(jp::JumpProblem{iip, P, A, C, J}, # println("HERE2 i ", i) hi = h[i] ix = hixs[i] - if eltype(h) <: Tuple + if eltype(hi) <: Tuple while ((ix + 1) <= length(hi)) && hi[ix + 1][1] <= t ix += 1 end From fa5c0ade3df05e9663005a4133f6b9422edd2584 Mon Sep 17 00:00:00 2001 From: gzagatti Date: Wed, 2 Nov 2022 00:49:04 +0800 Subject: [PATCH 10/72] needs jumptovars_map and vartojumps_map. --- src/aggregators/queue.jl | 102 ++++++++++++++++++++++++++------------- 1 file changed, 69 insertions(+), 33 deletions(-) diff --git a/src/aggregators/queue.jl b/src/aggregators/queue.jl index 059de535d..b55e3a8b2 100644 --- a/src/aggregators/queue.jl +++ b/src/aggregators/queue.jl @@ -1,7 +1,7 @@ """ Queue method. This method handles variable intensity rates. """ -mutable struct QueueMethodJumpAggregation{T, S, F1, F2, F3, F4, RNG, DEPGR, PQ, H} <: +mutable struct QueueMethodJumpAggregation{T, S, F1, F2, F3, F4, RNG, VJMAP, JVMAP, PQ, H} <: AbstractSSAJumpAggregator next_jump::Int # the next jump to execute prev_jump::Int # the previous jump that was executed @@ -14,7 +14,8 @@ mutable struct QueueMethodJumpAggregation{T, S, F1, F2, F3, F4, RNG, DEPGR, PQ, affects!::F3 # vector of affect functions for VariableRateJumps save_positions::Tuple{Bool, Bool} # tuple for whether to save the jumps before and/or after event rng::RNG # random number generator - dep_gr::DEPGR # dependency graph + vartojumps_map::VJMAP # map from variable to dependent jumps + jumptovars_map::JVMAP # map from jumps to dependent variables pq::PQ # priority queue of next time marks::F4 # vector of mark functions for VariableRateJumps h::H # history of jumps @@ -26,7 +27,9 @@ end function QueueMethodJumpAggregation(nj::Int, njt::T, et::T, crs::F1, sr::T, maj::S, rs::F2, affs!::F3, sps::Tuple{Bool, Bool}, - rng::RNG; dep_graph = nothing, marks::F4, + rng::RNG; vartojumps_map = nothing, + jumptovars_map = nothing, + marks::F4, history::H, save_history = false, lrates, urates, Ls) where {T, S, F1, F2, F3, F4, RNG, H} @@ -34,29 +37,62 @@ function QueueMethodJumpAggregation(nj::Int, njt::T, et::T, crs::F1, sr::T, error("Mass-action jumps are not supported with the Queue Method.") end - if dep_graph === nothing - gr = [Int[] for _ in length(rs)] - elseif length(dep_graph) != length(rs) - error("Dependency graph must have same length as the number of jumps.") + if vartojumps_map === nothing + vjmap = [Int[] for _ in length(rs)] + if jumptovars_map !== nothing + for (jump, vars) in enumerate(jumptovars_map) + for var in vars + push!(vjmap[var], jump) + end + end + end else - gr = [sort(Int[d for d in deps]) for deps in dep_graph] + vjmap = vartojumps_map end + if length(vjmap) != length(rs) + error("Map from variable to dependent jumps must have same length as the number of jumps.") + end + + vjmap = [OrderedSet{Int}(push!(jumps, var)) for (var, jumps) in enumerate(vjmap)] + + if jumptovars_map === nothing + jvmap = [Int[] for _ in length(rs)] + if vartojumps_map !== nothing + for (var, jumps) in enumerate(vartojumps_map) + for jump in jumps + push!(jvmap[jump], var) + end + end + end + else + jvmap = jumptovars_map + end + + if length(jvmap) != length(rs) + error("Map from jump to dependent variables must have same length as the number of jumps.") + end + + jvmap = [OrderedSet{Int}(push!(vars, jump)) for (jump, vars) in enumerate(jvmap)] + pq = PriorityQueue{Int, T}() - QueueMethodJumpAggregation{T, S, F1, F2, F3, F4, RNG, typeof(gr), typeof(pq), + QueueMethodJumpAggregation{T, S, F1, F2, F3, F4, RNG, typeof(vjmap), typeof(jvmap), + typeof(pq), typeof(history)}(nj, nj, njt, et, crs, sr, maj, rs, affs!, sps, rng, - gr, pq, marks, history, save_history, + vjmap, jvmap, pq, marks, history, + save_history, lrates, urates, Ls) end # creating the JumpAggregation structure (tuple-based variable jumps) function aggregate(aggregator::QueueMethod, u, p, t, end_time, variable_jumps, - ma_jumps, save_positions, rng; dep_graph = nothing, save_history = false, + ma_jumps, save_positions, rng; vartojumps_map = nothing, + jumptovars_map = nothing, save_history = false, kwargs...) # TODO: FunctionWrapper slows down big problems # U, P, T, H = typeof(u), typeof(p), typeof(t), typeof(t) @@ -159,7 +195,8 @@ function aggregate(aggregator::QueueMethod, u, p, t, end_time, variable_jumps, next_jump_time = typemax(typeof(t)) QueueMethodJumpAggregation(next_jump, next_jump_time, end_time, cur_rates, sum_rate, ma_jumps, rates, affects!, save_positions, rng; - dep_graph = dep_graph, marks = marks, + vartojumps_map = vartojumps_map, + jumptovars_map = jumptovars_map, marks = marks, history = history, save_history = save_history, lrates = lrates, urates = urates, Ls = Ls, kwargs...) end @@ -196,14 +233,14 @@ function generate_jumps!(p::QueueMethodJumpAggregation, integrator, u, params, t end @inline function update_state!(p::QueueMethodJumpAggregation, integrator, u, params, t) - @unpack next_jump, dep_gr, marks, h = p + @unpack next_jump, jumptovars_map, marks, h = p if marks === nothing # println("UPDATE_STATE!", " p.next_jump ", p.next_jump, " p.next_jump_time ", # p.next_jump_time, " integrator ", integrator) push!(p.h[p.next_jump], p.next_jump_time) @inbounds p.affects![next_jump](integrator) else - m = marks[next_jump](u, params, t, dep_gr, h) + m = marks[next_jump](u, params, t, jumptovars_map, h) push!(p.h[p.next_jump], (p.next_jump_time, m)) @inbounds p.affects![next_jump](integrator, m) end @@ -214,26 +251,25 @@ end ######################## SSA specific helper routines ######################## function update_dependent_rates!(p::QueueMethodJumpAggregation, u, params, t) @unpack next_jump, end_time, rates, pq = p - @inbounds deps = copy(p.dep_gr[next_jump]) - - Base.insert!(deps, searchsortedfirst(deps, next_jump), next_jump) - - while !isempty(deps) - j = pop!(deps) - @inbounds tj = next_time(p, j, u, params, t, end_time, deps) - pq[j] = tj + @inbounds vars = collect(p.jumptovars_map[next_jump]) + shuffle!(vars) + while !isempty(vars) + var = pop!(vars) + tvar = next_time(p, var, u, params, t, end_time, vars) + pq[var] = tvar end - nothing end function next_time(p::QueueMethodJumpAggregation, i, u, params, t, tstop, ignore) - @unpack pq, dep_gr, h = p + @unpack pq = p + jumps = p.vartojumps_map[i] + # println("NEXT_TIME i ", i) # we only need to determine whether a jump will take place before any of the dependents for (j, _t) in pairs(pq) # println("NEXT_TIME pq LOOP1 j ", j, " _t ", _t) - @inbounds if j != i && !insorted(j, ignore) && insorted(j, dep_gr[i]) && _t < tstop + @inbounds if i != j && !(j in ignore) && (j in jumps) && _t < tstop # println("NEXT_TIME pq LOOP2 j ", j, " tstop ", _t) tstop = _t break @@ -243,34 +279,34 @@ function next_time(p::QueueMethodJumpAggregation, i, u, params, t, tstop, ignore end function next_time(p::QueueMethodJumpAggregation, i, u, params, t, tstop) - @unpack rng, pq, dep_gr, h = p + @unpack rng, pq, jumptovars_map, h = p rate, lrate, urate, L = p.rates[i], p.lrates[i], p.urates[i], p.Ls[i] while t < tstop - # println("NEXT_TIME i ", i, " u ", u, " params ", params, " t ", t, " dep_gr ", - # dep_gr, + # println("NEXT_TIME i ", i, " u ", u, " params ", params, " t ", t, " jumptovars_map ", + # jumptovars_map, # " h ", h) - _urate = urate(u, params, t, dep_gr, h) - _L = L(u, params, t, dep_gr, h) + _urate = urate(u, params, t, jumptovars_map, h) + _L = L(u, params, t, jumptovars_map, h) s = randexp(rng) / _urate if s > _L t = t + _L continue end - _lrate = lrate(u, params, t, dep_gr, h) + _lrate = lrate(u, params, t, jumptovars_map, h) if _lrate > _urate error("The lower bound should be lower than the upper bound rate for t = $(t) and i = $(i), but lower bound = $(_lrate) > upper bound = $(_urate)") end v = rand(rng) # first inequality is less expensive and short-circuits the evaluation if (v > _lrate / _urate) - _rate = rate(u, params, t + s, dep_gr, h) + _rate = rate(u, params, t + s, jumptovars_map, h) if (v > _rate / _urate) t = t + s continue end end t = t + s - # println("NEXT TIME RETURN t ", t, " rate ", rate(u, params, t, dep_gr, h)) + # println("NEXT TIME RETURN t ", t, " rate ", rate(u, params, t, jumptovars_map, h)) return t end return typemax(t) From de6f6a2e2299504f68734697ef790de647d3345d Mon Sep 17 00:00:00 2001 From: gzagatti Date: Wed, 2 Nov 2022 01:07:12 +0800 Subject: [PATCH 11/72] remove mark and history from the aggregator. --- src/aggregators/queue.jl | 130 ++++++++++++++------------------------- src/jumps.jl | 45 +++++--------- 2 files changed, 62 insertions(+), 113 deletions(-) diff --git a/src/aggregators/queue.jl b/src/aggregators/queue.jl index b55e3a8b2..36c3f155e 100644 --- a/src/aggregators/queue.jl +++ b/src/aggregators/queue.jl @@ -1,7 +1,7 @@ """ Queue method. This method handles variable intensity rates. """ -mutable struct QueueMethodJumpAggregation{T, S, F1, F2, F3, F4, RNG, VJMAP, JVMAP, PQ, H} <: +mutable struct QueueMethodJumpAggregation{T, S, F1, F2, F3, RNG, VJMAP, JVMAP, PQ} <: AbstractSSAJumpAggregator next_jump::Int # the next jump to execute prev_jump::Int # the previous jump that was executed @@ -17,9 +17,6 @@ mutable struct QueueMethodJumpAggregation{T, S, F1, F2, F3, F4, RNG, VJMAP, JVMA vartojumps_map::VJMAP # map from variable to dependent jumps jumptovars_map::JVMAP # map from jumps to dependent variables pq::PQ # priority queue of next time - marks::F4 # vector of mark functions for VariableRateJumps - h::H # history of jumps - save_history::Bool # whether to save event history after solving lrates::F2 # vector of rate lower bound functions urates::F2 # vector of rate upper bound functions Ls::F2 # vector of interval length functions @@ -29,10 +26,7 @@ function QueueMethodJumpAggregation(nj::Int, njt::T, et::T, crs::F1, sr::T, maj::S, rs::F2, affs!::F3, sps::Tuple{Bool, Bool}, rng::RNG; vartojumps_map = nothing, jumptovars_map = nothing, - marks::F4, - history::H, - save_history = false, - lrates, urates, Ls) where {T, S, F1, F2, F3, F4, RNG, H} + lrates, urates, Ls) where {T, S, F1, F2, F3, RNG} if get_num_majumps(maj) > 0 error("Mass-action jumps are not supported with the Queue Method.") end @@ -76,23 +70,22 @@ function QueueMethodJumpAggregation(nj::Int, njt::T, et::T, crs::F1, sr::T, jvmap = [OrderedSet{Int}(push!(vars, jump)) for (jump, vars) in enumerate(jvmap)] pq = PriorityQueue{Int, T}() - QueueMethodJumpAggregation{T, S, F1, F2, F3, F4, RNG, typeof(vjmap), typeof(jvmap), - typeof(pq), - typeof(history)}(nj, nj, njt, - et, - crs, sr, maj, - rs, - affs!, sps, - rng, - vjmap, jvmap, pq, marks, history, - save_history, - lrates, urates, Ls) + QueueMethodJumpAggregation{T, S, F1, F2, F3, RNG, typeof(vjmap), typeof(jvmap), + typeof(pq) + }(nj, nj, njt, + et, + crs, sr, maj, + rs, + affs!, sps, + rng, + vjmap, jvmap, pq, + lrates, urates, Ls) end # creating the JumpAggregation structure (tuple-based variable jumps) function aggregate(aggregator::QueueMethod, u, p, t, end_time, variable_jumps, ma_jumps, save_positions, rng; vartojumps_map = nothing, - jumptovars_map = nothing, save_history = false, + jumptovars_map = nothing, kwargs...) # TODO: FunctionWrapper slows down big problems # U, P, T, H = typeof(u), typeof(p), typeof(t), typeof(t) @@ -154,36 +147,18 @@ function aggregate(aggregator::QueueMethod, u, p, t, end_time, variable_jumps, # urates = Vector{RateWrapper}() # Ls = Vector{RateWrapper}() # end - U, T, H = typeof(u), typeof(t), typeof(t) + U, T = typeof(u), typeof(t), typeof(t) if (variable_jumps !== nothing) && !isempty(variable_jumps) - marks = [c.mark for c in variable_jumps] - if eltype(marks) === Nothing - marks = nothing - AffectWrapper = FunctionWrappers.FunctionWrapper{Nothing, Tuple{Any}} - affects! = [AffectWrapper((integrator) -> (c.affect!(integrator); nothing)) - for c in variable_jumps] - else - AffectWrapper = FunctionWrappers.FunctionWrapper{Nothing, - Tuple{Any, eltype(u)}} - affects! = [AffectWrapper((integrator, m) -> (c.affect!(integrator, m); nothing)) - for c in variable_jumps] - H = Tuple{T, eltype(u)} - end - history = [jump.history for jump in variable_jumps] - if eltype(history) === Nothing - history = [Vector{H}() for _ in variable_jumps] - else - history = [convert(Vector{H}, h) for h in history] - end + AffectWrapper = FunctionWrappers.FunctionWrapper{Nothing, Tuple{Any}} + affects! = [AffectWrapper((integrator) -> (c.affect!(integrator); nothing)) + for c in variable_jumps] rates = Any[c.rate for c in variable_jumps] lrates = Any[c.lrate for c in variable_jumps] urates = Any[c.urate for c in variable_jumps] Ls = Any[c.L for c in variable_jumps] else - marks = nothing AffectWrapper = FunctionWrappers.FunctionWrapper{Nothing, Tuple{Any}} affects! = Vector{AffectWrapper}() - history = Vector{Vector{H}}() rates = Vector{Any}() lrates = Vector{Any}() urates = Vector{Any}() @@ -196,15 +171,13 @@ function aggregate(aggregator::QueueMethod, u, p, t, end_time, variable_jumps, QueueMethodJumpAggregation(next_jump, next_jump_time, end_time, cur_rates, sum_rate, ma_jumps, rates, affects!, save_positions, rng; vartojumps_map = vartojumps_map, - jumptovars_map = jumptovars_map, marks = marks, - history = history, save_history = save_history, + jumptovars_map = jumptovars_map, lrates = lrates, urates = urates, Ls = Ls, kwargs...) end # set up a new simulation and calculate the first jump / jump time function initialize!(p::QueueMethodJumpAggregation, integrator, u, params, t) p.end_time = integrator.sol.prob.tspan[2] - reset_history!(p, integrator) fill_rates_and_get_times!(p, u, params, t) generate_jumps!(p, integrator, u, params, t) nothing @@ -225,25 +198,14 @@ function generate_jumps!(p::QueueMethodJumpAggregation, integrator, u, params, t # println("GENERATE_JUMPS! ", peek(p.pq), " p.next_jump_time ", p.next_jump_time, # " p.next_jump ", # p.next_jump) - if (p.next_jump_time > p.end_time) && !p.save_history - # throw the history away once simulation is over - reset_history!(p, integrator) - end nothing end @inline function update_state!(p::QueueMethodJumpAggregation, integrator, u, params, t) - @unpack next_jump, jumptovars_map, marks, h = p - if marks === nothing - # println("UPDATE_STATE!", " p.next_jump ", p.next_jump, " p.next_jump_time ", - # p.next_jump_time, " integrator ", integrator) - push!(p.h[p.next_jump], p.next_jump_time) - @inbounds p.affects![next_jump](integrator) - else - m = marks[next_jump](u, params, t, jumptovars_map, h) - push!(p.h[p.next_jump], (p.next_jump_time, m)) - @inbounds p.affects![next_jump](integrator, m) - end + @unpack next_jump = p + # println("UPDATE_STATE!", " p.next_jump ", p.next_jump, " p.next_jump_time ", + # p.next_jump_time, " integrator ", integrator) + @inbounds p.affects![next_jump](integrator) p.prev_jump = next_jump return integrator.u end @@ -279,27 +241,27 @@ function next_time(p::QueueMethodJumpAggregation, i, u, params, t, tstop, ignore end function next_time(p::QueueMethodJumpAggregation, i, u, params, t, tstop) - @unpack rng, pq, jumptovars_map, h = p + @unpack rng, pq = p rate, lrate, urate, L = p.rates[i], p.lrates[i], p.urates[i], p.Ls[i] while t < tstop # println("NEXT_TIME i ", i, " u ", u, " params ", params, " t ", t, " jumptovars_map ", # jumptovars_map, # " h ", h) - _urate = urate(u, params, t, jumptovars_map, h) - _L = L(u, params, t, jumptovars_map, h) + _urate = urate(u, params, t) + _L = L(u, params, t) s = randexp(rng) / _urate if s > _L t = t + _L continue end - _lrate = lrate(u, params, t, jumptovars_map, h) + _lrate = lrate(u, params, t) if _lrate > _urate error("The lower bound should be lower than the upper bound rate for t = $(t) and i = $(i), but lower bound = $(_lrate) > upper bound = $(_urate)") end v = rand(rng) # first inequality is less expensive and short-circuits the evaluation if (v > _lrate / _urate) - _rate = rate(u, params, t + s, jumptovars_map, h) + _rate = rate(u, params, t + s) if (v > _rate / _urate) t = t + s continue @@ -324,22 +286,22 @@ function fill_rates_and_get_times!(p::QueueMethodJumpAggregation, u, params, t) nothing end -function reset_history!(p::QueueMethodJumpAggregation, integrator) - start_time = integrator.sol.prob.tspan[1] - @unpack h = p - @inbounds for i in 1:length(h) - hi = h[i] - ix = 0 - if eltype(hi) <: Tuple - while ((ix + 1) <= length(hi)) && hi[ix + 1][1] <= start_time - ix += 1 - end - else - while ((ix + 1) <= length(hi)) && hi[ix + 1] <= start_time - ix += 1 - end - end - h[i] = ix == 0 ? eltype(h)[] : hi[1:ix] - end - nothing -end +# function reset_history!(p::QueueMethodJumpAggregation, integrator) +# start_time = integrator.sol.prob.tspan[1] +# @unpack h = p +# @inbounds for i in 1:length(h) +# hi = h[i] +# ix = 0 +# if eltype(hi) <: Tuple +# while ((ix + 1) <= length(hi)) && hi[ix + 1][1] <= start_time +# ix += 1 +# end +# else +# while ((ix + 1) <= length(hi)) && hi[ix + 1] <= start_time +# ix += 1 +# end +# end +# h[i] = ix == 0 ? eltype(h)[] : hi[1:ix] +# end +# nothing +# end diff --git a/src/jumps.jl b/src/jumps.jl index 26c97217e..890a07fab 100644 --- a/src/jumps.jl +++ b/src/jumps.jl @@ -94,38 +94,27 @@ jprob = JumpProblem(prob, QueueMethod(), vrj) (5), DOI:10.1063/1.1835951 is used for calculating jump times with `VariableRateJump`s within ODE/SDE integrators. """ -struct VariableRateJump{R, F, F3, H, R2, R3, R4, I, T, T2} <: AbstractJump - """When planning to use the `QueueMethod` aggregator, function `lrate(u, p, - t, g, h)` that computes the rate at time `t` given state `u`, parameters - `p`, dependency graph `g` and history `h`. If using another aggregator, - function `rate(u,p,t)` that returns the jump's current rate.""" +struct VariableRateJump{R, F, R2, R3, R4, I, T, T2} <: AbstractJump + """Function `rate(u,p,t)` that returns the jump's current rate given state + `u`, parameters `p` and time `t`.""" rate::R - """When planning to use marks, function `affect!(integrator, m)` that updates - the state for one occurrence of the jump given `integrator` and sampled mark - `m`. Otherwise, function `affect!(integrator)` that does not depend on the - mark.""" + """Function `affect!(integrator)` that updates the state for one occurrence + of the jump given `integrator`.""" affect!::F - """Function `mark(u, p, t, g, h)` that samples from the mark distribution for - jump, given dependency graph `g`, history `h`, parameters `p` and jump - time `t`. If `mark(u, p, t, g, h) === nothing` the jump is unmarked.""" - mark::F3 - """Array with previous jump history.""" - history::H """When planning to use the `QueueMethod` aggregator, function `lrate(u, p, - t, g, h)` that computes the lower rate bound in interval `t` to `t + L` at - time `t` given state `u`, parameters `p`, dependency graph `g` and history - `h`. This is not required if using another aggregator.""" + t)` that computes the lower rate bound in interval `t` to `t + L` at time + `t` given state `u`, parameters `p`. This is not required if using another + aggregator.""" lrate::R2 """When planning to use the `QueueMethod` aggregator, function `lrate(u, p, - t, g, h)` that computes the upper rate bound in interval `t` to `t + L` at - time `t` given state `u`, parameters `p`, dependency graph `g` and history - `h`. This is not required if using another aggregator.""" + t)` that computes the upper rate bound in interval `t` to `t + L` at time + `t` given state `u`, parameters `p`. This is not required if using another + aggregator.""" urate::R3 """When planning to use the `QueueMethod` aggregator, function `lrate(u, p, - t, g, h)` that computes the interval length `L` starting at time `t` given - state `u`, parameters `p`, dependency graph `g` and history `h` for which - the rate is bounded between `lrate` and `urate`. This is not required if - using another aggregator.""" + t)` that computes the interval length `L` starting at time `t` given state + `u`, parameters `p` for which the rate is bounded between `lrate` and + `urate`. This is not required if using another aggregator.""" L::R4 idxs::I rootfind::Bool @@ -136,8 +125,6 @@ struct VariableRateJump{R, F, F3, H, R2, R3, R4, I, T, T2} <: AbstractJump end function VariableRateJump(rate, affect!; - mark = nothing, - history = nothing, lrate = nothing, urate = nothing, L = nothing, rootfind = true, idxs = nothing, @@ -150,14 +137,14 @@ function VariableRateJump(rate, affect!; error("Either `lrate`, `urate` and `L` must be nothing, or all of them must be defined.") end - VariableRateJump(rate, affect!, mark, history, lrate, urate, L, idxs, rootfind, + VariableRateJump(rate, affect!, lrate, urate, L, idxs, rootfind, interp_points, save_positions, abstol, reltol) end function VariableRateJump(jump::ConstantRateJump) rate = (u, p, t, g, h) -> jump.rate(u, p, t) L = (u, p, t, g, h) -> typemax(t) - VariableRateJump(rate, jump.affect!; mark = nothing, history = nothing, lrate = rate, + VariableRateJump(rate, jump.affect!; lrate = rate, urate = rate, L = L, idx = nothing, rootfind = true, save_positions = (false, true), interp_points = 10, From a56eff4bb6aa8857f7a21fb51697b2fb4c04571d Mon Sep 17 00:00:00 2001 From: gzagatti Date: Wed, 2 Nov 2022 17:37:43 +0800 Subject: [PATCH 12/72] adds utilities for dealing with history. --- src/utils.jl | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 src/utils.jl diff --git a/src/utils.jl b/src/utils.jl new file mode 100644 index 000000000..5eef440d2 --- /dev/null +++ b/src/utils.jl @@ -0,0 +1,65 @@ +""" +Removes all entries from the history later than `start_time`. If +`start_time`remove all entries. +""" +function reset_history!(h; start_time = nothing) + if start_time === nothing + start_time = -Inf + end + @inbounds for i in 1:length(h) + hi = h[i] + ix = 0 + if eltype(hi) <: Tuple + while ((ix + 1) <= length(hi)) && hi[ix + 1][1] <= start_time + ix += 1 + end + else + while ((ix + 1) <= length(hi)) && hi[ix + 1] <= start_time + ix += 1 + end + end + h[i] = ix == 0 ? eltype(h)[] : hi[1:ix] + end + nothing +end + +function conditional_rate(rate_closures, h, sol; saveat = nothing) + if eltype(h[1]) <: Tuple + h = [_h[1] for _h in h] + end + if typeof(saveat) <: Number + _saveat = sol.t[1]:saveat:sol.t[end] + else + _saveat = sol.t + end + p = sol.prob.p + _h = [eltype(h)(undef, 0) for _ in 1:length(h)] + hixs = zeros(Int, length(h)) + condrates = Array{Array{eltype(_saveat), 1}, 1}() + for t in _saveat + # get history up to t + @inbounds for i in 1:length(h) + # println("HERE2 i ", i) + hi = h[i] + ix = hixs[i] + while ((ix + 1) <= length(hi)) && hi[ix + 1] <= t + ix += 1 + end + _h[i] = ix == 0 ? [] : hi[1:ix] + end + # compute the rate at time t + u = sol(t) + condrate = Array{typeof(t), 1}() + @inbounds for i in 1:length(h) + rate = rate_closures[i](_h) + _rate = rate(u, p, t) + push!(condrate, _rate) + end + push!(condrates, condrate) + end + return DiffEqBase.build_solution(sol.prob, sol.alg, _saveat, condrates, dense = false, + calculate_error = false, + destats = DiffEqBase.DEStats(0), + interp = DiffEqBase.ConstantInterpolation(_saveat, + condrates)) +end From 2683f85ef19785e3594814d529b237a139b1229e Mon Sep 17 00:00:00 2001 From: gzagatti Date: Wed, 2 Nov 2022 17:38:27 +0800 Subject: [PATCH 13/72] fix bugs. --- src/JumpProcesses.jl | 6 ++++- src/aggregators/queue.jl | 36 ++++++------------------- src/problem.jl | 58 ---------------------------------------- 3 files changed, 13 insertions(+), 87 deletions(-) diff --git a/src/JumpProcesses.jl b/src/JumpProcesses.jl index ac98f7af8..632eeae6d 100644 --- a/src/JumpProcesses.jl +++ b/src/JumpProcesses.jl @@ -72,6 +72,7 @@ include("coupled_array.jl") include("coupling.jl") include("SSA_stepper.jl") include("simple_regular_solve.jl") +include("utils.jl") export ConstantRateJump, VariableRateJump, RegularJump, MassActionJump, JumpSet @@ -84,7 +85,7 @@ export Direct, DirectFW, SortingDirect, DirectCR export BracketData, RSSA export FRM, FRMFW, NRM export RSSACR, RDirect -export QueueMethod, history, conditional_rate +export QueueMethod export get_num_majumps, needs_depgraph, needs_vartojumps_map @@ -100,4 +101,7 @@ export SpatialMassActionJump export outdegree, num_sites, neighbors export NSM, DirectCRDirect +# utilities to deal with conditional rates +export reset_history!, conditional_rate + end # module diff --git a/src/aggregators/queue.jl b/src/aggregators/queue.jl index 36c3f155e..a7105722e 100644 --- a/src/aggregators/queue.jl +++ b/src/aggregators/queue.jl @@ -32,43 +32,43 @@ function QueueMethodJumpAggregation(nj::Int, njt::T, et::T, crs::F1, sr::T, end if vartojumps_map === nothing - vjmap = [Int[] for _ in length(rs)] + vjmap = [OrderedSet{Int}() for _ in 1:length(rs)] if jumptovars_map !== nothing for (jump, vars) in enumerate(jumptovars_map) + push!(vjmap[jump], jump) for var in vars push!(vjmap[var], jump) end end end else - vjmap = vartojumps_map + vjmap = [OrderedSet{Int}(append!([], jumps, [var])) + for (var, jumps) in enumerate(vartojumps_map)] end if length(vjmap) != length(rs) error("Map from variable to dependent jumps must have same length as the number of jumps.") end - vjmap = [OrderedSet{Int}(push!(jumps, var)) for (var, jumps) in enumerate(vjmap)] - if jumptovars_map === nothing - jvmap = [Int[] for _ in length(rs)] + jvmap = [OrderedSet{Int}() for _ in 1:length(rs)] if vartojumps_map !== nothing for (var, jumps) in enumerate(vartojumps_map) + push!(jvmap[var], var) for jump in jumps push!(jvmap[jump], var) end end end else - jvmap = jumptovars_map + jvmap = [OrderedSet{Int}(append!([], vars, [jump])) + for (jump, vars) in enumerate(jumptovars_map)] end if length(jvmap) != length(rs) error("Map from jump to dependent variables must have same length as the number of jumps.") end - jvmap = [OrderedSet{Int}(push!(vars, jump)) for (jump, vars) in enumerate(jvmap)] - pq = PriorityQueue{Int, T}() QueueMethodJumpAggregation{T, S, F1, F2, F3, RNG, typeof(vjmap), typeof(jvmap), typeof(pq) @@ -285,23 +285,3 @@ function fill_rates_and_get_times!(p::QueueMethodJumpAggregation, u, params, t) p.pq = PriorityQueue(pqdata) nothing end - -# function reset_history!(p::QueueMethodJumpAggregation, integrator) -# start_time = integrator.sol.prob.tspan[1] -# @unpack h = p -# @inbounds for i in 1:length(h) -# hi = h[i] -# ix = 0 -# if eltype(hi) <: Tuple -# while ((ix + 1) <= length(hi)) && hi[ix + 1][1] <= start_time -# ix += 1 -# end -# else -# while ((ix + 1) <= length(hi)) && hi[ix + 1] <= start_time -# ix += 1 -# end -# end -# h[i] = ix == 0 ? eltype(h)[] : hi[1:ix] -# end -# nothing -# end diff --git a/src/problem.jl b/src/problem.jl index 2e72479c0..c1fab987b 100644 --- a/src/problem.jl +++ b/src/problem.jl @@ -351,64 +351,6 @@ aggregator(jp::JumpProblem{iip, P, A, C, J}) where {iip, P, A, C, J} = A push!(tstops, jp.jump_callback.discrete_callbacks[1].condition.next_jump_time) end -function history(jp::JumpProblem{iip, P, A, C, J}) where {iip, P, A, C, - J <: QueueMethodJumpAggregation} - jp.discrete_jump_aggregation.h -end - -function conditional_rate(jp::JumpProblem{iip, P, A, C, J}, - sol::DiffEqBase.AbstractODESolution; - saveat = nothing) where {iip, P, A, C, - J <: QueueMethodJumpAggregation} - agg = jp.discrete_jump_aggregation - if !agg.save_history - error("History was not saved; set `save_history = true` when calling `JumpProblem`.") - end - if typeof(saveat) <: Number - _saveat = jp.prob.tspan[1]:saveat:jp.prob.tspan[2] - else - _saveat = sol.t - end - h = agg.h - dep_gr = agg.dep_gr - rates = agg.rates - p = jp.prob.p - _h = [eltype(h)(undef, 0) for _ in 1:length(h)] - hixs = zeros(Int, length(h)) - condrates = Array{Array{eltype(_saveat), 1}, 1}() - for t in _saveat - # get history up to t - @inbounds for i in 1:length(h) - # println("HERE2 i ", i) - hi = h[i] - ix = hixs[i] - if eltype(hi) <: Tuple - while ((ix + 1) <= length(hi)) && hi[ix + 1][1] <= t - ix += 1 - end - else - while ((ix + 1) <= length(hi)) && hi[ix + 1] <= t - ix += 1 - end - end - _h[i] = ix == 0 ? [] : hi[1:ix] - end - # compute the rate at time t - u = sol(t) - condrate = Array{typeof(t), 1}() - @inbounds for i in 1:length(h) - _rate = rates[i](u, p, t, dep_gr, _h) - push!(condrate, _rate) - end - push!(condrates, condrate) - end - return DiffEqBase.build_solution(jp.prob, sol.alg, _saveat, condrates, dense = false, - calculate_error = false, - destats = DiffEqBase.DEStats(0), - interp = DiffEqBase.ConstantInterpolation(_saveat, - condrates)) -end - @inline function update_jumps!(du, u, p, t, idx, jump) idx += 1 du[idx] = jump.rate(u.u, p, t) From c6249d9784a4262e15caa6e05c1ef36c842e02d2 Mon Sep 17 00:00:00 2001 From: gzagatti Date: Wed, 2 Nov 2022 18:01:10 +0800 Subject: [PATCH 14/72] adds option for indices when computing conditional rate. --- src/utils.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils.jl b/src/utils.jl index 5eef440d2..1f123d0bd 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -23,7 +23,7 @@ function reset_history!(h; start_time = nothing) nothing end -function conditional_rate(rate_closures, h, sol; saveat = nothing) +function conditional_rate(rate_closures, h, sol; saveat = nothing, ixs=1:length(h)) if eltype(h[1]) <: Tuple h = [_h[1] for _h in h] end @@ -50,7 +50,7 @@ function conditional_rate(rate_closures, h, sol; saveat = nothing) # compute the rate at time t u = sol(t) condrate = Array{typeof(t), 1}() - @inbounds for i in 1:length(h) + @inbounds for i in ixs rate = rate_closures[i](_h) _rate = rate(u, p, t) push!(condrate, _rate) From ccbfee565d45a06582a32279a2d6581874e21caf Mon Sep 17 00:00:00 2001 From: gzagatti Date: Wed, 2 Nov 2022 18:01:32 +0800 Subject: [PATCH 15/72] use Vector instead of Tuple in JumpSet. --- src/jumps.jl | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/jumps.jl b/src/jumps.jl index 890a07fab..6aa246589 100644 --- a/src/jumps.jl +++ b/src/jumps.jl @@ -453,11 +453,17 @@ function JumpSet(vj, cj, rj, maj::MassActionJump{S, T, U, V}) where {S <: Number JumpSet(vj, cj, rj, check_majump_type(maj)) end -JumpSet(jump::VariableRateJump) = JumpSet((jump,), (), nothing, nothing) -JumpSet(jump::ConstantRateJump) = JumpSet((), (jump,), nothing, nothing) -JumpSet(jump::RegularJump) = JumpSet((), (), jump, nothing) -JumpSet(jump::AbstractMassActionJump) = JumpSet((), (), nothing, jump) -function JumpSet(; variable_jumps = (), constant_jumps = (), +function JumpSet(jump::VariableRateJump) + JumpSet(VariableRateJump[jump], ConstantRateJump[], nothing, nothing) +end +function JumpSet(jump::ConstantRateJump) + JumpSet(VariableRateJump[], ConstantRateJump[jump], nothing, nothing) +end +JumpSet(jump::RegularJump) = JumpSet(VariableRateJump[], ConstantRateJump[], jump, nothing) +function JumpSet(jump::AbstractMassActionJump) + JumpSet(VariableRateJump[], ConstantRateJump[], nothing, jump) +end +function JumpSet(; variable_jumps = VariableRateJump[], constant_jumps = ConstantRateJump[], regular_jumps = nothing, massaction_jumps = nothing) JumpSet(variable_jumps, constant_jumps, regular_jumps, massaction_jumps) @@ -466,7 +472,8 @@ JumpSet(jb::Nothing) = JumpSet() # For Varargs, use recursion to make it type-stable function JumpSet(jumps::AbstractJump...) - JumpSet(split_jumps((), (), nothing, nothing, jumps...)...) + JumpSet(split_jumps(VariableRateJump[], ConstantRateJump[], nothing, nothing, + jumps...)...) end # handle vector of mass action jumps @@ -488,10 +495,10 @@ end @inline split_jumps(vj, cj, rj, maj) = vj, cj, rj, maj @inline function split_jumps(vj, cj, rj, maj, v::VariableRateJump, args...) - split_jumps((vj..., v), cj, rj, maj, args...) + split_jumps(push!(vj, v), cj, rj, maj, args...) end @inline function split_jumps(vj, cj, rj, maj, c::ConstantRateJump, args...) - split_jumps(vj, (cj..., c), rj, maj, args...) + split_jumps(vj, push!(cj, c), rj, maj, args...) end @inline function split_jumps(vj, cj, rj, maj, c::RegularJump, args...) split_jumps(vj, cj, regular_jump_combine(rj, c), maj, args...) @@ -500,8 +507,8 @@ end split_jumps(vj, cj, rj, massaction_jump_combine(maj, c), args...) end @inline function split_jumps(vj, cj, rj, maj, j::JumpSet, args...) - split_jumps((vj..., j.variable_jumps...), - (cj..., j.constant_jumps...), + split_jumps(append!(vj, j.variable_jumps), + append!(cj, j.constant_jumps), regular_jump_combine(rj, j.regular_jump), massaction_jump_combine(maj, j.massaction_jump), args...) end From 98b06fd6411b19ffd22d06b7208af477225a5344 Mon Sep 17 00:00:00 2001 From: gzagatti Date: Wed, 2 Nov 2022 18:11:12 +0800 Subject: [PATCH 16/72] adds docstring to function. --- src/utils.jl | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/utils.jl b/src/utils.jl index 1f123d0bd..5c3aa4c08 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -23,7 +23,16 @@ function reset_history!(h; start_time = nothing) nothing end -function conditional_rate(rate_closures, h, sol; saveat = nothing, ixs=1:length(h)) +""" +Computes conditional rate, given a vector of `rate_closures`, the history of +the process `h`, the solution `sol`. Optionally, it is possible to provide save +points with `saveat` and the indices of the targeted variables with `ixs`. + +The vector `rate_closures` contains functions `closure(_h)` that returns a +function `rate(u, p, t)` which computes the conditional rate given any history +`_h`. +""" +function conditional_rate(rate_closures, h, sol; saveat = nothing, ixs = 1:length(h)) if eltype(h[1]) <: Tuple h = [_h[1] for _h in h] end @@ -37,9 +46,7 @@ function conditional_rate(rate_closures, h, sol; saveat = nothing, ixs=1:length( hixs = zeros(Int, length(h)) condrates = Array{Array{eltype(_saveat), 1}, 1}() for t in _saveat - # get history up to t @inbounds for i in 1:length(h) - # println("HERE2 i ", i) hi = h[i] ix = hixs[i] while ((ix + 1) <= length(hi)) && hi[ix + 1] <= t @@ -47,7 +54,6 @@ function conditional_rate(rate_closures, h, sol; saveat = nothing, ixs=1:length( end _h[i] = ix == 0 ? [] : hi[1:ix] end - # compute the rate at time t u = sol(t) condrate = Array{typeof(t), 1}() @inbounds for i in ixs From 210827809a4a6063a5f14f4ec0ba1abca7cf566e Mon Sep 17 00:00:00 2001 From: gzagatti Date: Thu, 3 Nov 2022 00:50:15 +0800 Subject: [PATCH 17/72] modifies QueueMethod requirement. --- src/aggregators/aggregators.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/aggregators/aggregators.jl b/src/aggregators/aggregators.jl index bca6a6569..a352f137e 100644 --- a/src/aggregators/aggregators.jl +++ b/src/aggregators/aggregators.jl @@ -161,7 +161,6 @@ needs_depgraph(aggregator::DirectCR) = true needs_depgraph(aggregator::SortingDirect) = true needs_depgraph(aggregator::NRM) = true needs_depgraph(aggregator::RDirect) = true -needs_depgraph(aggregator::QueueMethod) = true # true if aggregator requires a map from solution variable to dependent jumps. # It is implicitly assumed these aggregators also require the reverse map, from @@ -169,6 +168,7 @@ needs_depgraph(aggregator::QueueMethod) = true needs_vartojumps_map(aggregator::AbstractAggregatorAlgorithm) = false needs_vartojumps_map(aggregator::RSSA) = true needs_vartojumps_map(aggregator::RSSACR) = true +needs_vartojumps_map(aggregator::QueueMethod) = true is_spatial(aggregator::AbstractAggregatorAlgorithm) = false is_spatial(aggregator::NSM) = true From ba74592062b4a13ac4da1a89daca2808015e4859 Mon Sep 17 00:00:00 2001 From: gzagatti Date: Sat, 5 Nov 2022 00:34:36 +0800 Subject: [PATCH 18/72] improve performance and simplify QueueMethod. --- src/aggregators/queue.jl | 55 +++++++++++++++------------------------- 1 file changed, 20 insertions(+), 35 deletions(-) diff --git a/src/aggregators/queue.jl b/src/aggregators/queue.jl index a7105722e..38cc93136 100644 --- a/src/aggregators/queue.jl +++ b/src/aggregators/queue.jl @@ -1,14 +1,14 @@ """ Queue method. This method handles variable intensity rates. """ -mutable struct QueueMethodJumpAggregation{T, S, F1, F2, F3, RNG, VJMAP, JVMAP, PQ} <: +mutable struct QueueMethodJumpAggregation{T, S, F2, F3, RNG, VJMAP, JVMAP, PQ} <: AbstractSSAJumpAggregator next_jump::Int # the next jump to execute prev_jump::Int # the previous jump that was executed next_jump_time::T # the time of the next jump end_time::T # the time to stop a simulation - cur_rates::F1 # not used - sum_rate::T # not used + cur_rates::Nothing # not used + sum_rate::Nothing # not used ma_jumps::S # not used rates::F2 # vector of rate functions affects!::F3 # vector of affect functions for VariableRateJumps @@ -22,7 +22,7 @@ mutable struct QueueMethodJumpAggregation{T, S, F1, F2, F3, RNG, VJMAP, JVMAP, P Ls::F2 # vector of interval length functions end -function QueueMethodJumpAggregation(nj::Int, njt::T, et::T, crs::F1, sr::T, +function QueueMethodJumpAggregation(nj::Int, njt::T, et::T, crs::Nothing, sr::Nothing, maj::S, rs::F2, affs!::F3, sps::Tuple{Bool, Bool}, rng::RNG; vartojumps_map = nothing, jumptovars_map = nothing, @@ -70,7 +70,7 @@ function QueueMethodJumpAggregation(nj::Int, njt::T, et::T, crs::F1, sr::T, end pq = PriorityQueue{Int, T}() - QueueMethodJumpAggregation{T, S, F1, F2, F3, RNG, typeof(vjmap), typeof(jvmap), + QueueMethodJumpAggregation{T, S, F2, F3, RNG, typeof(vjmap), typeof(jvmap), typeof(pq) }(nj, nj, njt, et, @@ -165,7 +165,7 @@ function aggregate(aggregator::QueueMethod, u, p, t, end_time, variable_jumps, Ls = Vector{Any}() end cur_rates = nothing - sum_rate = zero(typeof(t)) + sum_rate = nothing next_jump = 0 next_jump_time = typemax(typeof(t)) QueueMethodJumpAggregation(next_jump, next_jump_time, end_time, cur_rates, sum_rate, @@ -195,16 +195,11 @@ end # calculate the next jump / jump time function generate_jumps!(p::QueueMethodJumpAggregation, integrator, u, params, t) p.next_jump, p.next_jump_time = peek(p.pq) - # println("GENERATE_JUMPS! ", peek(p.pq), " p.next_jump_time ", p.next_jump_time, - # " p.next_jump ", - # p.next_jump) nothing end @inline function update_state!(p::QueueMethodJumpAggregation, integrator, u, params, t) @unpack next_jump = p - # println("UPDATE_STATE!", " p.next_jump ", p.next_jump, " p.next_jump_time ", - # p.next_jump_time, " integrator ", integrator) @inbounds p.affects![next_jump](integrator) p.prev_jump = next_jump return integrator.u @@ -212,41 +207,32 @@ end ######################## SSA specific helper routines ######################## function update_dependent_rates!(p::QueueMethodJumpAggregation, u, params, t) - @unpack next_jump, end_time, rates, pq = p + @unpack next_jump, rates, pq = p @inbounds vars = collect(p.jumptovars_map[next_jump]) shuffle!(vars) while !isempty(vars) var = pop!(vars) - tvar = next_time(p, var, u, params, t, end_time, vars) + tvar = next_time(p, var, u, params, t) pq[var] = tvar end nothing end -function next_time(p::QueueMethodJumpAggregation, i, u, params, t, tstop, ignore) - @unpack pq = p - jumps = p.vartojumps_map[i] - - # println("NEXT_TIME i ", i) - # we only need to determine whether a jump will take place before any of the dependents - for (j, _t) in pairs(pq) - # println("NEXT_TIME pq LOOP1 j ", j, " _t ", _t) - @inbounds if i != j && !(j in ignore) && (j in jumps) && _t < tstop - # println("NEXT_TIME pq LOOP2 j ", j, " tstop ", _t) - tstop = _t +function next_time(p::QueueMethodJumpAggregation, i, u, params, t) + @unpack end_time, rng, pq = p + rate, lrate, urate, L = p.rates[i], p.lrates[i], p.urates[i], p.Ls[i] + vartojumps_map = p.vartojumps_map[i] + tstop = end_time + for j in vartojumps_map + if j == i + continue + end + if pq[j] < end_time + tstop = pq[j] break end end - return next_time(p, i, u, params, t, tstop) -end - -function next_time(p::QueueMethodJumpAggregation, i, u, params, t, tstop) - @unpack rng, pq = p - rate, lrate, urate, L = p.rates[i], p.lrates[i], p.urates[i], p.Ls[i] while t < tstop - # println("NEXT_TIME i ", i, " u ", u, " params ", params, " t ", t, " jumptovars_map ", - # jumptovars_map, - # " h ", h) _urate = urate(u, params, t) _L = L(u, params, t) s = randexp(rng) / _urate @@ -268,7 +254,6 @@ function next_time(p::QueueMethodJumpAggregation, i, u, params, t, tstop) end end t = t + s - # println("NEXT TIME RETURN t ", t, " rate ", rate(u, params, t, jumptovars_map, h)) return t end return typemax(t) @@ -279,7 +264,7 @@ function fill_rates_and_get_times!(p::QueueMethodJumpAggregation, u, params, t) @unpack rates, end_time = p pqdata = Vector{Tuple{Int, eltype(t)}}(undef, length(rates)) @inbounds for i in 1:length(rates) - @inbounds ti = next_time(p, i, u, params, t, end_time) + @inbounds ti = next_time(p, i, u, params, t) pqdata[i] = (i, ti) end p.pq = PriorityQueue(pqdata) From 6f19944d96031833a80a94a4770469f81a661edb Mon Sep 17 00:00:00 2001 From: gzagatti Date: Sat, 5 Nov 2022 00:35:20 +0800 Subject: [PATCH 19/72] fix bugs. --- src/problem.jl | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/problem.jl b/src/problem.jl index c1fab987b..ff5aac472 100644 --- a/src/problem.jl +++ b/src/problem.jl @@ -185,10 +185,11 @@ function JumpProblem(prob, aggregator::AbstractAggregatorAlgorithm, jumps::JumpS !is_spatial(aggregator) # check if need to flatten prob, maj = flatten(maj, prob, spatial_system, hopping_constants; kwargs...) end - ## Constant rate handling + + ## Constant and variable rate handling t, end_time, u = prob.tspan[1], prob.tspan[2], prob.u0 # check if there are no jumps - if (typeof(jumps.constant_jumps) <: Tuple{}) && (maj === nothing) && + if (length(jumps.constant_jumps) == 0) && (maj === nothing) && !is_spatial(aggregator) && !(typeof(aggregator) <: QueueMethod) disc_agg = nothing constant_jump_callback = CallbackSet() @@ -199,19 +200,20 @@ function JumpProblem(prob, aggregator::AbstractAggregatorAlgorithm, jumps::JumpS constant_jump_callback = DiscreteCallback(disc_agg) end - iip = isinplace_jump(prob, jumps.regular_jump) - - ## variable Rate Handling - variable_jumps = nothing - if typeof(jumps.variable_jumps) <: Tuple{} + if length(jumps.variable_jumps) == 0 new_prob = prob variable_jump_callback = CallbackSet() cont_agg = JumpSet().variable_jumps + if typeof(aggregator) <: QueueMethod + disc_agg = nothing + constant_jump_callback = CallbackSet() + end elseif typeof(aggregator) <: QueueMethod - if (typeof(jumps.constant_jumps) <: Tuple{}) + variable_jumps = VariableRateJump[] + if (length(jumps.constant_jumps) == 0) variable_jumps = jumps.variable_jumps else - variable_jumps = [jump for jump in jumps.variable_jumps] + variable_jumps = append!(variable_jumps, jumps.variable_jumps) append!(variable_jumps, [VariableRateJump(jump) for jump in jumps.constant_jumps]) end @@ -223,6 +225,8 @@ function JumpProblem(prob, aggregator::AbstractAggregatorAlgorithm, jumps::JumpS cont_agg = JumpSet().variable_jumps else new_prob = extend_problem(prob, jumps; rng = rng) + disc_agg = nothing + constant_jump_callback = CallbackSet() variable_jump_callback = build_variable_callback(CallbackSet(), 0, jumps.variable_jumps...; rng = rng) cont_agg = jumps.variable_jumps @@ -230,6 +234,8 @@ function JumpProblem(prob, aggregator::AbstractAggregatorAlgorithm, jumps::JumpS jump_cbs = CallbackSet(constant_jump_callback, variable_jump_callback) + iip = isinplace_jump(prob, jumps.regular_jump) + solkwargs = make_kwarg(; callback) JumpProblem{iip, typeof(new_prob), typeof(aggregator), typeof(jump_cbs), typeof(disc_agg), From 42568f98d0519ae998f7ec8a9e7d08acf5cc96b1 Mon Sep 17 00:00:00 2001 From: gzagatti Date: Fri, 25 Nov 2022 15:53:10 +0800 Subject: [PATCH 20/72] adds support for MA jumps; pass all unit tests. --- src/aggregators/queue.jl | 160 +++++++++++++++++++++++---------------- src/problem.jl | 54 +++++++------ 2 files changed, 128 insertions(+), 86 deletions(-) diff --git a/src/aggregators/queue.jl b/src/aggregators/queue.jl index 38cc93136..0c904d2b9 100644 --- a/src/aggregators/queue.jl +++ b/src/aggregators/queue.jl @@ -1,7 +1,7 @@ """ Queue method. This method handles variable intensity rates. """ -mutable struct QueueMethodJumpAggregation{T, S, F2, F3, RNG, VJMAP, JVMAP, PQ} <: +mutable struct QueueMethodJumpAggregation{T, S, F1, F2, RNG, INVDEPGR, DEPGR, PQ} <: AbstractSSAJumpAggregator next_jump::Int # the next jump to execute prev_jump::Int # the previous jump that was executed @@ -10,67 +10,64 @@ mutable struct QueueMethodJumpAggregation{T, S, F2, F3, RNG, VJMAP, JVMAP, PQ} < cur_rates::Nothing # not used sum_rate::Nothing # not used ma_jumps::S # not used - rates::F2 # vector of rate functions - affects!::F3 # vector of affect functions for VariableRateJumps + rates::F1 # vector of rate functions + affects!::F2 # vector of affect functions for VariableRateJumps save_positions::Tuple{Bool, Bool} # tuple for whether to save the jumps before and/or after event rng::RNG # random number generator - vartojumps_map::VJMAP # map from variable to dependent jumps - jumptovars_map::JVMAP # map from jumps to dependent variables + dep_gr::DEPGR # map from jumps to jumps depending on it + inv_dep_gr::INVDEPGR # map from jumsp to jumps it depends on pq::PQ # priority queue of next time - lrates::F2 # vector of rate lower bound functions - urates::F2 # vector of rate upper bound functions - Ls::F2 # vector of interval length functions + lrates::F1 # vector of rate lower bound functions + urates::F1 # vector of rate upper bound functions + Ls::F1 # vector of interval length functions end function QueueMethodJumpAggregation(nj::Int, njt::T, et::T, crs::Nothing, sr::Nothing, - maj::S, rs::F2, affs!::F3, sps::Tuple{Bool, Bool}, - rng::RNG; vartojumps_map = nothing, - jumptovars_map = nothing, - lrates, urates, Ls) where {T, S, F1, F2, F3, RNG} - if get_num_majumps(maj) > 0 - error("Mass-action jumps are not supported with the Queue Method.") + maj::S, rs::F1, affs!::F2, sps::Tuple{Bool, Bool}, + rng::RNG; u::U, inv_dep_gr = nothing, + dep_gr = nothing, + lrates, urates, Ls) where {T, S, F1, F2, RNG, U} + if inv_dep_gr === nothing && dep_gr === nothing + if (get_num_majumps(maj) == 0) || !isempty(rs) + error("To use VariableRateJumps with the Queue Method algorithm a dependency graph between jumps and/or its inverse must be supplied.") + else + dg = make_dependency_graph(length(u), maj) + idg = dg + end end - if vartojumps_map === nothing - vjmap = [OrderedSet{Int}() for _ in 1:length(rs)] - if jumptovars_map !== nothing - for (jump, vars) in enumerate(jumptovars_map) - push!(vjmap[jump], jump) - for var in vars - push!(vjmap[var], jump) - end - end - end - else - vjmap = [OrderedSet{Int}(append!([], jumps, [var])) - for (var, jumps) in enumerate(vartojumps_map)] + num_jumps = get_num_majumps(maj) + length(rs) + + if dep_gr !== nothing + # using a Set to ensure that edges are not duplicate + dg = [Set{Int}(append!([], jumps, [var])) + for (var, jumps) in enumerate(dep_gr)] end - if length(vjmap) != length(rs) - error("Map from variable to dependent jumps must have same length as the number of jumps.") + if inv_dep_gr !== nothing + # using a Set to ensure that edges are not duplicate + idg = [Set{Int}(append!([], vars, [jump])) + for (jump, vars) in enumerate(inv_dep_gr)] end - if jumptovars_map === nothing - jvmap = [OrderedSet{Int}() for _ in 1:length(rs)] - if vartojumps_map !== nothing - for (var, jumps) in enumerate(vartojumps_map) - push!(jvmap[var], var) - for jump in jumps - push!(jvmap[jump], var) - end - end - end - else - jvmap = [OrderedSet{Int}(append!([], vars, [jump])) - for (jump, vars) in enumerate(jumptovars_map)] + if dep_gr === nothing + dg = idg end - if length(jvmap) != length(rs) - error("Map from jump to dependent variables must have same length as the number of jumps.") + if inv_dep_gr === nothing + idg = dg + end + + if length(dg) != num_jumps + error("Number of nodes in the dependency graph must be the same as the number of jumps.") + end + + if length(idg) != num_jumps + error("Number of nodes in the inverse dependency graph must be the same as the number of jumps.") end pq = PriorityQueue{Int, T}() - QueueMethodJumpAggregation{T, S, F2, F3, RNG, typeof(vjmap), typeof(jvmap), + QueueMethodJumpAggregation{T, S, F1, F2, RNG, typeof(dg), typeof(idg), typeof(pq) }(nj, nj, njt, et, @@ -78,16 +75,17 @@ function QueueMethodJumpAggregation(nj::Int, njt::T, et::T, crs::Nothing, sr::No rs, affs!, sps, rng, - vjmap, jvmap, pq, + dg, idg, pq, lrates, urates, Ls) end # creating the JumpAggregation structure (tuple-based variable jumps) function aggregate(aggregator::QueueMethod, u, p, t, end_time, variable_jumps, - ma_jumps, save_positions, rng; vartojumps_map = nothing, - jumptovars_map = nothing, + ma_jumps, save_positions, rng; + dep_gr = nothing, inv_dep_gr = nothing, kwargs...) # TODO: FunctionWrapper slows down big problems + # keeping the problematic implementation here for future reference # U, P, T, H = typeof(u), typeof(p), typeof(t), typeof(t) # G = Vector{Vector{Int}} # if (variable_jumps !== nothing) && !isempty(variable_jumps) @@ -147,7 +145,6 @@ function aggregate(aggregator::QueueMethod, u, p, t, end_time, variable_jumps, # urates = Vector{RateWrapper}() # Ls = Vector{RateWrapper}() # end - U, T = typeof(u), typeof(t), typeof(t) if (variable_jumps !== nothing) && !isempty(variable_jumps) AffectWrapper = FunctionWrappers.FunctionWrapper{Nothing, Tuple{Any}} affects! = [AffectWrapper((integrator) -> (c.affect!(integrator); nothing)) @@ -170,8 +167,9 @@ function aggregate(aggregator::QueueMethod, u, p, t, end_time, variable_jumps, next_jump_time = typemax(typeof(t)) QueueMethodJumpAggregation(next_jump, next_jump_time, end_time, cur_rates, sum_rate, ma_jumps, rates, affects!, save_positions, rng; - vartojumps_map = vartojumps_map, - jumptovars_map = jumptovars_map, + u = u, + dep_gr = dep_gr, + inv_dep_gr = inv_dep_gr, lrates = lrates, urates = urates, Ls = Ls, kwargs...) end @@ -199,8 +197,18 @@ function generate_jumps!(p::QueueMethodJumpAggregation, integrator, u, params, t end @inline function update_state!(p::QueueMethodJumpAggregation, integrator, u, params, t) - @unpack next_jump = p - @inbounds p.affects![next_jump](integrator) + @unpack ma_jumps, next_jump = p + num_majumps = get_num_majumps(ma_jumps) + if next_jump <= num_majumps + if u isa SVector + integrator.u = executerx(u, next_jump, ma_jumps) + else + @inbounds executerx!(u, next_jump, ma_jumps) + end + else + idx = next_jump - num_majumps + @inbounds p.affects![next_jump](integrator) + end p.prev_jump = next_jump return integrator.u end @@ -208,22 +216,41 @@ end ######################## SSA specific helper routines ######################## function update_dependent_rates!(p::QueueMethodJumpAggregation, u, params, t) @unpack next_jump, rates, pq = p - @inbounds vars = collect(p.jumptovars_map[next_jump]) + @inbounds vars = collect(p.dep_gr[next_jump]) shuffle!(vars) + for i in vars + pq[i] = typemax(t) + end while !isempty(vars) - var = pop!(vars) - tvar = next_time(p, var, u, params, t) - pq[var] = tvar + i = pop!(vars) + ti = next_time(p, i, u, params, t) + pq[i] = ti end nothing end +function get_rates(p::QueueMethodJumpAggregation, i, u, t) + ma_jumps = p.ma_jumps + num_majumps = get_num_majumps(ma_jumps) + if i <= num_majumps + _rate = evalrxrate(u, i, ma_jumps) + rate(u, p, t) = _rate + lrate = rate + urate = rate + L(u, p, t) = typemax(t) + else + idx = i - num_majumps + rate, lrate, urate, L = p.rates[idx], p.lrates[idx], p.urates[idx], p.Ls[idx] + end + return rate, lrate, urate, L +end + function next_time(p::QueueMethodJumpAggregation, i, u, params, t) @unpack end_time, rng, pq = p - rate, lrate, urate, L = p.rates[i], p.lrates[i], p.urates[i], p.Ls[i] - vartojumps_map = p.vartojumps_map[i] + rate, lrate, urate, L = get_rates(p, i, u, t) + inv_dep_gr = p.inv_dep_gr[i] tstop = end_time - for j in vartojumps_map + for j in inv_dep_gr if j == i continue end @@ -262,11 +289,16 @@ end # reevaulate all rates, recalculate all jump times, and reinit the priority queue function fill_rates_and_get_times!(p::QueueMethodJumpAggregation, u, params, t) @unpack rates, end_time = p - pqdata = Vector{Tuple{Int, eltype(t)}}(undef, length(rates)) - @inbounds for i in 1:length(rates) + num_jumps = get_num_majumps(p.ma_jumps) + length(rates) + pqdata = Vector{Tuple{Int, eltype(t)}}(undef, num_jumps) + @inbounds for i in 1:num_jumps + pqdata[i] = (i, typemax(t)) + end + pq = PriorityQueue(pqdata) + p.pq = pq + @inbounds for i in shuffle(1:num_jumps) @inbounds ti = next_time(p, i, u, params, t) - pqdata[i] = (i, ti) + pq[i] = ti end - p.pq = PriorityQueue(pqdata) nothing end diff --git a/src/problem.jl b/src/problem.jl index ff5aac472..09bb6bb76 100644 --- a/src/problem.jl +++ b/src/problem.jl @@ -188,27 +188,17 @@ function JumpProblem(prob, aggregator::AbstractAggregatorAlgorithm, jumps::JumpS ## Constant and variable rate handling t, end_time, u = prob.tspan[1], prob.tspan[2], prob.u0 - # check if there are no jumps - if (length(jumps.constant_jumps) == 0) && (maj === nothing) && - !is_spatial(aggregator) && !(typeof(aggregator) <: QueueMethod) - disc_agg = nothing - constant_jump_callback = CallbackSet() - elseif !(typeof(aggregator) <: QueueMethod) - disc_agg = aggregate(aggregator, u, prob.p, t, end_time, jumps.constant_jumps, maj, - save_positions, rng; spatial_system = spatial_system, - hopping_constants = hopping_constants, kwargs...) - constant_jump_callback = DiscreteCallback(disc_agg) - end - if length(jumps.variable_jumps) == 0 + if length(jumps.variable_jumps) == 0 && (length(jumps.constant_jumps) == 0) && + (maj === nothing) && !is_spatial(aggregator) + # check if there are no jumps new_prob = prob variable_jump_callback = CallbackSet() cont_agg = JumpSet().variable_jumps - if typeof(aggregator) <: QueueMethod - disc_agg = nothing - constant_jump_callback = CallbackSet() - end + disc_agg = nothing + constant_jump_callback = CallbackSet() elseif typeof(aggregator) <: QueueMethod + # QueueMethod handles all types of jumps together variable_jumps = VariableRateJump[] if (length(jumps.constant_jumps) == 0) variable_jumps = jumps.variable_jumps @@ -224,12 +214,32 @@ function JumpProblem(prob, aggregator::AbstractAggregatorAlgorithm, jumps::JumpS variable_jump_callback = CallbackSet() cont_agg = JumpSet().variable_jumps else - new_prob = extend_problem(prob, jumps; rng = rng) - disc_agg = nothing - constant_jump_callback = CallbackSet() - variable_jump_callback = build_variable_callback(CallbackSet(), 0, - jumps.variable_jumps...; rng = rng) - cont_agg = jumps.variable_jumps + # the fallback is to handle each jump type separately + if (length(jumps.constant_jumps) == 0) && (maj === nothing) && + !is_spatial(aggregator) + disc_agg = nothing + constant_jump_callback = CallbackSet() + else + disc_agg = aggregate(aggregator, u, prob.p, t, end_time, jumps.constant_jumps, + maj, + save_positions, rng; spatial_system = spatial_system, + hopping_constants = hopping_constants, kwargs...) + constant_jump_callback = DiscreteCallback(disc_agg) + end + + if length(jumps.variable_jumps) > 0 && !is_spatial(aggregator) + new_prob = extend_problem(prob, jumps; rng = rng) + disc_agg = nothing + constant_jump_callback = CallbackSet() + variable_jump_callback = build_variable_callback(CallbackSet(), 0, + jumps.variable_jumps...; + rng = rng) + cont_agg = jumps.variable_jumps + else + new_prob = prob + variable_jump_callback = CallbackSet() + cont_agg = JumpSet().variable_jumps + end end jump_cbs = CallbackSet(constant_jump_callback, variable_jump_callback) From c91b0214aba71144998db4421160622c8f9cbc05 Mon Sep 17 00:00:00 2001 From: gzagatti Date: Mon, 28 Nov 2022 17:27:31 +0800 Subject: [PATCH 21/72] improves aggregator speed by using FunctionWrappers and avoid allocations. --- src/aggregators/queue.jl | 146 ++++++++++++--------------------------- 1 file changed, 45 insertions(+), 101 deletions(-) diff --git a/src/aggregators/queue.jl b/src/aggregators/queue.jl index 0c904d2b9..52adbaf5c 100644 --- a/src/aggregators/queue.jl +++ b/src/aggregators/queue.jl @@ -1,7 +1,7 @@ """ Queue method. This method handles variable intensity rates. """ -mutable struct QueueMethodJumpAggregation{T, S, F1, F2, RNG, INVDEPGR, DEPGR, PQ} <: +mutable struct QueueMethodJumpAggregation{T, S, F1, F2, RNG, GR, PQ} <: AbstractSSAJumpAggregator next_jump::Int # the next jump to execute prev_jump::Int # the previous jump that was executed @@ -14,8 +14,8 @@ mutable struct QueueMethodJumpAggregation{T, S, F1, F2, RNG, INVDEPGR, DEPGR, PQ affects!::F2 # vector of affect functions for VariableRateJumps save_positions::Tuple{Bool, Bool} # tuple for whether to save the jumps before and/or after event rng::RNG # random number generator - dep_gr::DEPGR # map from jumps to jumps depending on it - inv_dep_gr::INVDEPGR # map from jumsp to jumps it depends on + dep_gr::GR # map from jumps to jumps depending on it + inv_dep_gr::GR # map from jumsp to jumps it depends on pq::PQ # priority queue of next time lrates::F1 # vector of rate lower bound functions urates::F1 # vector of rate upper bound functions @@ -42,12 +42,14 @@ function QueueMethodJumpAggregation(nj::Int, njt::T, et::T, crs::Nothing, sr::No # using a Set to ensure that edges are not duplicate dg = [Set{Int}(append!([], jumps, [var])) for (var, jumps) in enumerate(dep_gr)] + dg = [sort!(collect(i)) for i in dg] end if inv_dep_gr !== nothing # using a Set to ensure that edges are not duplicate idg = [Set{Int}(append!([], vars, [jump])) for (jump, vars) in enumerate(inv_dep_gr)] + idg = [sort!(collect(i)) for i in idg] end if dep_gr === nothing @@ -67,7 +69,7 @@ function QueueMethodJumpAggregation(nj::Int, njt::T, et::T, crs::Nothing, sr::No end pq = PriorityQueue{Int, T}() - QueueMethodJumpAggregation{T, S, F1, F2, RNG, typeof(dg), typeof(idg), + QueueMethodJumpAggregation{T, S, F1, F2, RNG, typeof(dg), typeof(pq) }(nj, nj, njt, et, @@ -84,82 +86,22 @@ function aggregate(aggregator::QueueMethod, u, p, t, end_time, variable_jumps, ma_jumps, save_positions, rng; dep_gr = nothing, inv_dep_gr = nothing, kwargs...) - # TODO: FunctionWrapper slows down big problems - # keeping the problematic implementation here for future reference - # U, P, T, H = typeof(u), typeof(p), typeof(t), typeof(t) - # G = Vector{Vector{Int}} - # if (variable_jumps !== nothing) && !isempty(variable_jumps) - # marks = [c.mark for c in variable_jumps] - # if eltype(marks) === Nothing - # MarkWrapper = Nothing - # AffectWrapper = FunctionWrappers.FunctionWrapper{Nothing, Tuple{Any}} - # RateWrapper = FunctionWrappers.FunctionWrapper{T, - # Tuple{U, P, T, G, - # Vector{Vector{H}}}} - # else - # MarkWrapper = FunctionWrappers.FunctionWrapper{U, - # Tuple{U, P, T, G, - # Vector{Vector{H}}}} - # AffectWrapper = FunctionWrappers.FunctionWrapper{Nothing, - # Tuple{Any, U}} - # H = Tuple{T, U} - # RateWrapper = FunctionWrappers.FunctionWrapper{T, - # Tuple{U, P, T, G, - # Vector{Vector{H}}}} - # end - # else - # MarkWrapper = Nothing - # AffectWrapper = FunctionWrappers.FunctionWrapper{Nothing, Tuple{Int, Any}} - # RateWrapper = FunctionWrappers.FunctionWrapper{T, - # Tuple{U, P, T, G, Vector{Vector{H}}}} - # end - - # if (variable_jumps !== nothing) && !isempty(variable_jumps) - # if eltype(marks) === Nothing - # marks = nothing - # affects! = [AffectWrapper((integrator) -> (c.affect!(integrator); nothing)) - # for c in variable_jumps] - # else - # marks = [convert(MarkWrapper, m) for m in marks] - # affects! = [AffectWrapper((integrator, m) -> (c.affect!(integrator, m); nothing)) - # for c in variable_jumps] - # end - - # history = [jump.history for jump in variable_jumps] - # if eltype(history) === Nothing - # history = [Vector{H}() for _ in variable_jumps] - # else - # history = [convert(H, h) for h in history] - # end - - # rates = [RateWrapper(c.rate) for c in variable_jumps] - # lrates = [RateWrapper(c.lrate) for c in variable_jumps] - # urates = [RateWrapper(c.urate) for c in variable_jumps] - # Ls = [RateWrapper(c.L) for c in variable_jumps] - # else - # marks = nothing - # history = Vector{Vector{H}}() - # affects! = Vector{AffectWrapper}() - # rates = Vector{RateWrapper}() - # lrates = Vector{RateWrapper}() - # urates = Vector{RateWrapper}() - # Ls = Vector{RateWrapper}() - # end + AffectWrapper = FunctionWrappers.FunctionWrapper{Nothing, Tuple{Any}} + RateWrapper = FunctionWrappers.FunctionWrapper{typeof(t), + Tuple{typeof(u), typeof(p), typeof(t)}} if (variable_jumps !== nothing) && !isempty(variable_jumps) - AffectWrapper = FunctionWrappers.FunctionWrapper{Nothing, Tuple{Any}} affects! = [AffectWrapper((integrator) -> (c.affect!(integrator); nothing)) for c in variable_jumps] - rates = Any[c.rate for c in variable_jumps] - lrates = Any[c.lrate for c in variable_jumps] - urates = Any[c.urate for c in variable_jumps] - Ls = Any[c.L for c in variable_jumps] + rates = [RateWrapper(c.rate) for c in variable_jumps] + lrates = Any[RateWrapper(c.lrate) for c in variable_jumps] + urates = Any[RateWrapper(c.urate) for c in variable_jumps] + Ls = Any[RateWrapper(c.L) for c in variable_jumps] else - AffectWrapper = FunctionWrappers.FunctionWrapper{Nothing, Tuple{Any}} affects! = Vector{AffectWrapper}() - rates = Vector{Any}() - lrates = Vector{Any}() - urates = Vector{Any}() - Ls = Vector{Any}() + rates = Vector{RateWrapper}() + lrates = Vector{RateWrapper}() + urates = Vector{RateWrapper}() + Ls = Vector{RateWrapper}() end cur_rates = nothing sum_rate = nothing @@ -216,20 +158,15 @@ end ######################## SSA specific helper routines ######################## function update_dependent_rates!(p::QueueMethodJumpAggregation, u, params, t) @unpack next_jump, rates, pq = p - @inbounds vars = collect(p.dep_gr[next_jump]) - shuffle!(vars) - for i in vars - pq[i] = typemax(t) - end - while !isempty(vars) - i = pop!(vars) - ti = next_time(p, i, u, params, t) + @inbounds deps = p.dep_gr[next_jump] + for (ix, i) in enumerate(deps) + ti = next_time(p, u, params, t, i, @inbounds view(deps, ix:length(deps))) pq[i] = ti end nothing end -function get_rates(p::QueueMethodJumpAggregation, i, u, t) +function get_rates(p::QueueMethodJumpAggregation, i, u) ma_jumps = p.ma_jumps num_majumps = get_num_majumps(ma_jumps) if i <= num_majumps @@ -245,20 +182,25 @@ function get_rates(p::QueueMethodJumpAggregation, i, u, t) return rate, lrate, urate, L end -function next_time(p::QueueMethodJumpAggregation, i, u, params, t) - @unpack end_time, rng, pq = p - rate, lrate, urate, L = get_rates(p, i, u, t) - inv_dep_gr = p.inv_dep_gr[i] +function next_time(p::QueueMethodJumpAggregation, u, params, t, i, not_updated) + @unpack end_time, pq = p tstop = end_time + inv_dep_gr = p.inv_dep_gr[i] for j in inv_dep_gr if j == i continue end - if pq[j] < end_time - tstop = pq[j] + if (j < i || !(j in not_updated)) && @inbounds pq[j] < end_time + @inbounds tstop = pq[j] break end end + return next_time(p, u, params, t, i, tstop) +end + +function next_time(p::QueueMethodJumpAggregation{T}, u, params, t, i, tstop::T) where {T} + @unpack rng, pq = p + rate, lrate, urate, L = get_rates(p, i, u) while t < tstop _urate = urate(u, params, t) _L = L(u, params, t) @@ -270,15 +212,17 @@ function next_time(p::QueueMethodJumpAggregation, i, u, params, t) _lrate = lrate(u, params, t) if _lrate > _urate error("The lower bound should be lower than the upper bound rate for t = $(t) and i = $(i), but lower bound = $(_lrate) > upper bound = $(_urate)") - end - v = rand(rng) - # first inequality is less expensive and short-circuits the evaluation - if (v > _lrate / _urate) - _rate = rate(u, params, t + s) - if (v > _rate / _urate) - t = t + s - continue - end + else _lrate < _urate + # when the lower and upper bound are the same, then v < 1 = _lrate / _urate = _rate / _urate + v = rand(rng) + # first inequality is less expensive and short-circuits the evaluation + if (v > _lrate / _urate) + _rate = rate(u, params, t + s) + if (v > _rate / _urate) + t = t + s + continue + end + end end t = t + s return t @@ -288,7 +232,7 @@ end # reevaulate all rates, recalculate all jump times, and reinit the priority queue function fill_rates_and_get_times!(p::QueueMethodJumpAggregation, u, params, t) - @unpack rates, end_time = p + @unpack rates = p num_jumps = get_num_majumps(p.ma_jumps) + length(rates) pqdata = Vector{Tuple{Int, eltype(t)}}(undef, num_jumps) @inbounds for i in 1:num_jumps @@ -297,7 +241,7 @@ function fill_rates_and_get_times!(p::QueueMethodJumpAggregation, u, params, t) pq = PriorityQueue(pqdata) p.pq = pq @inbounds for i in shuffle(1:num_jumps) - @inbounds ti = next_time(p, i, u, params, t) + @inbounds ti = next_time(p, u, params, t, i, i:num_jumps) pq[i] = ti end nothing From db541d926883972c95430eb4cbaba6d9f7282655 Mon Sep 17 00:00:00 2001 From: gzagatti Date: Mon, 28 Nov 2022 23:11:13 +0800 Subject: [PATCH 22/72] use heap instead of priority queue for improved performance. --- src/aggregators/queue.jl | 61 +++++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/src/aggregators/queue.jl b/src/aggregators/queue.jl index 52adbaf5c..7bb0ec162 100644 --- a/src/aggregators/queue.jl +++ b/src/aggregators/queue.jl @@ -15,7 +15,8 @@ mutable struct QueueMethodJumpAggregation{T, S, F1, F2, RNG, GR, PQ} <: save_positions::Tuple{Bool, Bool} # tuple for whether to save the jumps before and/or after event rng::RNG # random number generator dep_gr::GR # map from jumps to jumps depending on it - inv_dep_gr::GR # map from jumsp to jumps it depends on + inv_dep_gr::GR # map from jumps to jumps it depends on + jump_times::Vector{T} # map from jumps to candidate times pq::PQ # priority queue of next time lrates::F1 # vector of rate lower bound functions urates::F1 # vector of rate upper bound functions @@ -68,7 +69,8 @@ function QueueMethodJumpAggregation(nj::Int, njt::T, et::T, crs::Nothing, sr::No error("Number of nodes in the inverse dependency graph must be the same as the number of jumps.") end - pq = PriorityQueue{Int, T}() + jts = Vector{T}() + pq = MutableBinaryMinHeap{T}() QueueMethodJumpAggregation{T, S, F1, F2, RNG, typeof(dg), typeof(pq) }(nj, nj, njt, @@ -77,7 +79,7 @@ function QueueMethodJumpAggregation(nj::Int, njt::T, et::T, crs::Nothing, sr::No rs, affs!, sps, rng, - dg, idg, pq, + dg, idg, jts, pq, lrates, urates, Ls) end @@ -134,7 +136,7 @@ end # calculate the next jump / jump time function generate_jumps!(p::QueueMethodJumpAggregation, integrator, u, params, t) - p.next_jump, p.next_jump_time = peek(p.pq) + p.next_jump_time, p.next_jump = top_with_handle(p.pq) nothing end @@ -157,11 +159,12 @@ end ######################## SSA specific helper routines ######################## function update_dependent_rates!(p::QueueMethodJumpAggregation, u, params, t) - @unpack next_jump, rates, pq = p + @unpack next_jump, rates, jump_times, pq = p @inbounds deps = p.dep_gr[next_jump] for (ix, i) in enumerate(deps) ti = next_time(p, u, params, t, i, @inbounds view(deps, ix:length(deps))) - pq[i] = ti + jump_times[i] = ti + update!(pq, i, ti) end nothing end @@ -183,23 +186,22 @@ function get_rates(p::QueueMethodJumpAggregation, i, u) end function next_time(p::QueueMethodJumpAggregation, u, params, t, i, not_updated) - @unpack end_time, pq = p + @unpack end_time, jump_times = p tstop = end_time inv_dep_gr = p.inv_dep_gr[i] for j in inv_dep_gr if j == i continue end - if (j < i || !(j in not_updated)) && @inbounds pq[j] < end_time - @inbounds tstop = pq[j] - break + if (j < i || !(j in not_updated)) && @inbounds jump_times[j] < tstop + @inbounds tstop = jump_times[j] end end return next_time(p, u, params, t, i, tstop) end function next_time(p::QueueMethodJumpAggregation{T}, u, params, t, i, tstop::T) where {T} - @unpack rng, pq = p + @unpack rng = p rate, lrate, urate, L = get_rates(p, i, u) while t < tstop _urate = urate(u, params, t) @@ -212,17 +214,18 @@ function next_time(p::QueueMethodJumpAggregation{T}, u, params, t, i, tstop::T) _lrate = lrate(u, params, t) if _lrate > _urate error("The lower bound should be lower than the upper bound rate for t = $(t) and i = $(i), but lower bound = $(_lrate) > upper bound = $(_urate)") - else _lrate < _urate - # when the lower and upper bound are the same, then v < 1 = _lrate / _urate = _rate / _urate - v = rand(rng) - # first inequality is less expensive and short-circuits the evaluation - if (v > _lrate / _urate) - _rate = rate(u, params, t + s) - if (v > _rate / _urate) - t = t + s - continue - end - end + else + _lrate < _urate + # when the lower and upper bound are the same, then v < 1 = _lrate / _urate = _rate / _urate + v = rand(rng) + # first inequality is less expensive and short-circuits the evaluation + if (v > _lrate / _urate) + _rate = rate(u, params, t + s) + if (v > _rate / _urate) + t = t + s + continue + end + end end t = t + s return t @@ -234,15 +237,15 @@ end function fill_rates_and_get_times!(p::QueueMethodJumpAggregation, u, params, t) @unpack rates = p num_jumps = get_num_majumps(p.ma_jumps) + length(rates) - pqdata = Vector{Tuple{Int, eltype(t)}}(undef, num_jumps) + jump_times = Vector{eltype(t)}(undef, num_jumps) @inbounds for i in 1:num_jumps - pqdata[i] = (i, typemax(t)) + jump_times[i] = typemax(t) end - pq = PriorityQueue(pqdata) - p.pq = pq - @inbounds for i in shuffle(1:num_jumps) - @inbounds ti = next_time(p, u, params, t, i, i:num_jumps) - pq[i] = ti + p.jump_times = jump_times + @inbounds for i in 1:num_jumps + ti = next_time(p, u, params, t, i, Int[]) + jump_times[i] = ti end + p.pq = MutableBinaryMinHeap(copy(jump_times)) nothing end From 7c973b714ad718e3f2e401cd96b23daad5b8077c Mon Sep 17 00:00:00 2001 From: gzagatti Date: Tue, 29 Nov 2022 23:28:20 +0800 Subject: [PATCH 23/72] fix API. --- src/aggregators/queue.jl | 24 ++++++++++++------------ src/jumps.jl | 7 +++---- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/aggregators/queue.jl b/src/aggregators/queue.jl index 7bb0ec162..0fc13ed53 100644 --- a/src/aggregators/queue.jl +++ b/src/aggregators/queue.jl @@ -25,10 +25,10 @@ end function QueueMethodJumpAggregation(nj::Int, njt::T, et::T, crs::Nothing, sr::Nothing, maj::S, rs::F1, affs!::F2, sps::Tuple{Bool, Bool}, - rng::RNG; u::U, inv_dep_gr = nothing, - dep_gr = nothing, + rng::RNG; u::U, inv_dep_graph = nothing, + dep_graph = nothing, lrates, urates, Ls) where {T, S, F1, F2, RNG, U} - if inv_dep_gr === nothing && dep_gr === nothing + if inv_dep_graph === nothing && dep_graph === nothing if (get_num_majumps(maj) == 0) || !isempty(rs) error("To use VariableRateJumps with the Queue Method algorithm a dependency graph between jumps and/or its inverse must be supplied.") else @@ -39,25 +39,25 @@ function QueueMethodJumpAggregation(nj::Int, njt::T, et::T, crs::Nothing, sr::No num_jumps = get_num_majumps(maj) + length(rs) - if dep_gr !== nothing + if dep_graph !== nothing # using a Set to ensure that edges are not duplicate dg = [Set{Int}(append!([], jumps, [var])) - for (var, jumps) in enumerate(dep_gr)] + for (var, jumps) in enumerate(dep_graph)] dg = [sort!(collect(i)) for i in dg] end - if inv_dep_gr !== nothing + if inv_dep_graph !== nothing # using a Set to ensure that edges are not duplicate idg = [Set{Int}(append!([], vars, [jump])) - for (jump, vars) in enumerate(inv_dep_gr)] + for (jump, vars) in enumerate(inv_dep_graph)] idg = [sort!(collect(i)) for i in idg] end - if dep_gr === nothing + if dep_graph === nothing dg = idg end - if inv_dep_gr === nothing + if inv_dep_graph === nothing idg = dg end @@ -86,7 +86,7 @@ end # creating the JumpAggregation structure (tuple-based variable jumps) function aggregate(aggregator::QueueMethod, u, p, t, end_time, variable_jumps, ma_jumps, save_positions, rng; - dep_gr = nothing, inv_dep_gr = nothing, + dep_graph = nothing, inv_dep_graph = nothing, kwargs...) AffectWrapper = FunctionWrappers.FunctionWrapper{Nothing, Tuple{Any}} RateWrapper = FunctionWrappers.FunctionWrapper{typeof(t), @@ -112,8 +112,8 @@ function aggregate(aggregator::QueueMethod, u, p, t, end_time, variable_jumps, QueueMethodJumpAggregation(next_jump, next_jump_time, end_time, cur_rates, sum_rate, ma_jumps, rates, affects!, save_positions, rng; u = u, - dep_gr = dep_gr, - inv_dep_gr = inv_dep_gr, + dep_graph = dep_graph, + inv_dep_graph = inv_dep_graph, lrates = lrates, urates = urates, Ls = Ls, kwargs...) end diff --git a/src/jumps.jl b/src/jumps.jl index 6aa246589..b352b2bc1 100644 --- a/src/jumps.jl +++ b/src/jumps.jl @@ -142,10 +142,9 @@ function VariableRateJump(rate, affect!; end function VariableRateJump(jump::ConstantRateJump) - rate = (u, p, t, g, h) -> jump.rate(u, p, t) - L = (u, p, t, g, h) -> typemax(t) - VariableRateJump(rate, jump.affect!; lrate = rate, - urate = rate, L = L, idx = nothing, rootfind = true, + L = (u, p, t) -> typemax(t) + VariableRateJump(jump.rate, jump.affect!; lrate = jump.rate, + urate = jump.rate, L = L, idxs = nothing, rootfind = true, save_positions = (false, true), interp_points = 10, abstol = 1e-12, reltol = 0) From 9cb377025fbb66eab66a09c178b726a09f7bc69a Mon Sep 17 00:00:00 2001 From: gzagatti Date: Wed, 30 Nov 2022 10:13:30 +0800 Subject: [PATCH 24/72] leave from next_time loop as soon as possible. --- src/aggregators/queue.jl | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/aggregators/queue.jl b/src/aggregators/queue.jl index 0fc13ed53..3423a255b 100644 --- a/src/aggregators/queue.jl +++ b/src/aggregators/queue.jl @@ -211,6 +211,10 @@ function next_time(p::QueueMethodJumpAggregation{T}, u, params, t, i, tstop::T) t = t + _L continue end + _t = t + s + if _t > tstop + break + end _lrate = lrate(u, params, t) if _lrate > _urate error("The lower bound should be lower than the upper bound rate for t = $(t) and i = $(i), but lower bound = $(_lrate) > upper bound = $(_urate)") @@ -220,15 +224,14 @@ function next_time(p::QueueMethodJumpAggregation{T}, u, params, t, i, tstop::T) v = rand(rng) # first inequality is less expensive and short-circuits the evaluation if (v > _lrate / _urate) - _rate = rate(u, params, t + s) + _rate = rate(u, params, _t) if (v > _rate / _urate) - t = t + s + t = _t continue end end end - t = t + s - return t + return _t end return typemax(t) end From a9484a1fdd4b8ad12835a21e942d9baca01c48d1 Mon Sep 17 00:00:00 2001 From: gzagatti Date: Thu, 1 Dec 2022 12:01:33 +0800 Subject: [PATCH 25/72] fix QueueMethod requirement. --- src/aggregators/aggregators.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/aggregators/aggregators.jl b/src/aggregators/aggregators.jl index a352f137e..bca6a6569 100644 --- a/src/aggregators/aggregators.jl +++ b/src/aggregators/aggregators.jl @@ -161,6 +161,7 @@ needs_depgraph(aggregator::DirectCR) = true needs_depgraph(aggregator::SortingDirect) = true needs_depgraph(aggregator::NRM) = true needs_depgraph(aggregator::RDirect) = true +needs_depgraph(aggregator::QueueMethod) = true # true if aggregator requires a map from solution variable to dependent jumps. # It is implicitly assumed these aggregators also require the reverse map, from @@ -168,7 +169,6 @@ needs_depgraph(aggregator::RDirect) = true needs_vartojumps_map(aggregator::AbstractAggregatorAlgorithm) = false needs_vartojumps_map(aggregator::RSSA) = true needs_vartojumps_map(aggregator::RSSACR) = true -needs_vartojumps_map(aggregator::QueueMethod) = true is_spatial(aggregator::AbstractAggregatorAlgorithm) = false is_spatial(aggregator::NSM) = true From e0ba0833b85ce553f2e39a5995b60330c0100821 Mon Sep 17 00:00:00 2001 From: gzagatti Date: Thu, 1 Dec 2022 12:13:54 +0800 Subject: [PATCH 26/72] remove logic for pausing next time as premature optimization. --- src/aggregators/queue.jl | 91 ++++++++++------------------------------ 1 file changed, 23 insertions(+), 68 deletions(-) diff --git a/src/aggregators/queue.jl b/src/aggregators/queue.jl index 3423a255b..60b058d51 100644 --- a/src/aggregators/queue.jl +++ b/src/aggregators/queue.jl @@ -15,8 +15,6 @@ mutable struct QueueMethodJumpAggregation{T, S, F1, F2, RNG, GR, PQ} <: save_positions::Tuple{Bool, Bool} # tuple for whether to save the jumps before and/or after event rng::RNG # random number generator dep_gr::GR # map from jumps to jumps depending on it - inv_dep_gr::GR # map from jumps to jumps it depends on - jump_times::Vector{T} # map from jumps to candidate times pq::PQ # priority queue of next time lrates::F1 # vector of rate lower bound functions urates::F1 # vector of rate upper bound functions @@ -25,51 +23,28 @@ end function QueueMethodJumpAggregation(nj::Int, njt::T, et::T, crs::Nothing, sr::Nothing, maj::S, rs::F1, affs!::F2, sps::Tuple{Bool, Bool}, - rng::RNG; u::U, inv_dep_graph = nothing, + rng::RNG; u::U, dep_graph = nothing, lrates, urates, Ls) where {T, S, F1, F2, RNG, U} - if inv_dep_graph === nothing && dep_graph === nothing + if dep_graph === nothing if (get_num_majumps(maj) == 0) || !isempty(rs) - error("To use VariableRateJumps with the Queue Method algorithm a dependency graph between jumps and/or its inverse must be supplied.") + error("To use VariableRateJumps with the Queue Method algorithm a dependency graph between jumps must be supplied.") else dg = make_dependency_graph(length(u), maj) - idg = dg end end num_jumps = get_num_majumps(maj) + length(rs) - if dep_graph !== nothing - # using a Set to ensure that edges are not duplicate - dg = [Set{Int}(append!([], jumps, [var])) - for (var, jumps) in enumerate(dep_graph)] - dg = [sort!(collect(i)) for i in dg] - end - - if inv_dep_graph !== nothing - # using a Set to ensure that edges are not duplicate - idg = [Set{Int}(append!([], vars, [jump])) - for (jump, vars) in enumerate(inv_dep_graph)] - idg = [sort!(collect(i)) for i in idg] - end - - if dep_graph === nothing - dg = idg - end - - if inv_dep_graph === nothing - idg = dg - end + # using a Set to ensure that edges are not duplicate + dg = [Set{Int}(append!([], jumps, [var])) + for (var, jumps) in enumerate(dep_graph)] + dg = [sort!(collect(i)) for i in dg] if length(dg) != num_jumps error("Number of nodes in the dependency graph must be the same as the number of jumps.") end - if length(idg) != num_jumps - error("Number of nodes in the inverse dependency graph must be the same as the number of jumps.") - end - - jts = Vector{T}() pq = MutableBinaryMinHeap{T}() QueueMethodJumpAggregation{T, S, F1, F2, RNG, typeof(dg), typeof(pq) @@ -79,14 +54,14 @@ function QueueMethodJumpAggregation(nj::Int, njt::T, et::T, crs::Nothing, sr::No rs, affs!, sps, rng, - dg, idg, jts, pq, + dg, pq, lrates, urates, Ls) end # creating the JumpAggregation structure (tuple-based variable jumps) function aggregate(aggregator::QueueMethod, u, p, t, end_time, variable_jumps, ma_jumps, save_positions, rng; - dep_graph = nothing, inv_dep_graph = nothing, + dep_graph = nothing, kwargs...) AffectWrapper = FunctionWrappers.FunctionWrapper{Nothing, Tuple{Any}} RateWrapper = FunctionWrappers.FunctionWrapper{typeof(t), @@ -108,13 +83,12 @@ function aggregate(aggregator::QueueMethod, u, p, t, end_time, variable_jumps, cur_rates = nothing sum_rate = nothing next_jump = 0 - next_jump_time = typemax(typeof(t)) + next_jump_time = typemax(t) QueueMethodJumpAggregation(next_jump, next_jump_time, end_time, cur_rates, sum_rate, ma_jumps, rates, affects!, save_positions, rng; u = u, dep_graph = dep_graph, - inv_dep_graph = inv_dep_graph, - lrates = lrates, urates = urates, Ls = Ls, kwargs...) + lrates = lrates, urates = urates, Ls = Ls) end # set up a new simulation and calculate the first jump / jump time @@ -151,7 +125,7 @@ end end else idx = next_jump - num_majumps - @inbounds p.affects![next_jump](integrator) + @inbounds p.affects![idx](integrator) end p.prev_jump = next_jump return integrator.u @@ -159,11 +133,10 @@ end ######################## SSA specific helper routines ######################## function update_dependent_rates!(p::QueueMethodJumpAggregation, u, params, t) - @unpack next_jump, rates, jump_times, pq = p - @inbounds deps = p.dep_gr[next_jump] + @inbounds deps = p.dep_gr[p.next_jump] + @unpack end_time, pq = p for (ix, i) in enumerate(deps) - ti = next_time(p, u, params, t, i, @inbounds view(deps, ix:length(deps))) - jump_times[i] = ti + ti = next_time(p, u, params, t, i, end_time) update!(pq, i, ti) end nothing @@ -185,26 +158,14 @@ function get_rates(p::QueueMethodJumpAggregation, i, u) return rate, lrate, urate, L end -function next_time(p::QueueMethodJumpAggregation, u, params, t, i, not_updated) - @unpack end_time, jump_times = p - tstop = end_time - inv_dep_gr = p.inv_dep_gr[i] - for j in inv_dep_gr - if j == i - continue - end - if (j < i || !(j in not_updated)) && @inbounds jump_times[j] < tstop - @inbounds tstop = jump_times[j] - end - end - return next_time(p, u, params, t, i, tstop) -end - function next_time(p::QueueMethodJumpAggregation{T}, u, params, t, i, tstop::T) where {T} @unpack rng = p rate, lrate, urate, L = get_rates(p, i, u) while t < tstop _urate = urate(u, params, t) + if _urate == zero(t) + return typemax(t) + end _L = L(u, params, t) s = randexp(rng) / _urate if s > _L @@ -218,8 +179,7 @@ function next_time(p::QueueMethodJumpAggregation{T}, u, params, t, i, tstop::T) _lrate = lrate(u, params, t) if _lrate > _urate error("The lower bound should be lower than the upper bound rate for t = $(t) and i = $(i), but lower bound = $(_lrate) > upper bound = $(_urate)") - else - _lrate < _urate + elseif _lrate < _urate # when the lower and upper bound are the same, then v < 1 = _lrate / _urate = _rate / _urate v = rand(rng) # first inequality is less expensive and short-circuits the evaluation @@ -238,17 +198,12 @@ end # reevaulate all rates, recalculate all jump times, and reinit the priority queue function fill_rates_and_get_times!(p::QueueMethodJumpAggregation, u, params, t) - @unpack rates = p - num_jumps = get_num_majumps(p.ma_jumps) + length(rates) + @unpack end_time = p + num_jumps = get_num_majumps(p.ma_jumps) + length(p.rates) jump_times = Vector{eltype(t)}(undef, num_jumps) @inbounds for i in 1:num_jumps - jump_times[i] = typemax(t) - end - p.jump_times = jump_times - @inbounds for i in 1:num_jumps - ti = next_time(p, u, params, t, i, Int[]) - jump_times[i] = ti + jump_times[i] = next_time(p, u, params, t, i, end_time) end - p.pq = MutableBinaryMinHeap(copy(jump_times)) + p.pq = MutableBinaryMinHeap(jump_times) nothing end From a18e04fc5381455ab730e54ae01857eeec8ac57e Mon Sep 17 00:00:00 2001 From: gzagatti Date: Thu, 1 Dec 2022 16:33:50 +0800 Subject: [PATCH 27/72] fix zero rate. --- src/aggregators/queue.jl | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/aggregators/queue.jl b/src/aggregators/queue.jl index 60b058d51..5f9cd587f 100644 --- a/src/aggregators/queue.jl +++ b/src/aggregators/queue.jl @@ -163,17 +163,14 @@ function next_time(p::QueueMethodJumpAggregation{T}, u, params, t, i, tstop::T) rate, lrate, urate, L = get_rates(p, i, u) while t < tstop _urate = urate(u, params, t) - if _urate == zero(t) - return typemax(t) - end _L = L(u, params, t) - s = randexp(rng) / _urate + s = _urate == zero(t) ? typemax(t) : randexp(rng) / _urate + _t = t + s if s > _L t = t + _L continue end - _t = t + s - if _t > tstop + if _t >= tstop break end _lrate = lrate(u, params, t) From 08366d952733fafffd7dc978961c38ed32b268d9 Mon Sep 17 00:00:00 2001 From: gzagatti Date: Mon, 5 Dec 2022 14:57:24 +0800 Subject: [PATCH 28/72] move utilities out of the module. --- src/JumpProcesses.jl | 4 --- src/utils.jl | 71 -------------------------------------------- 2 files changed, 75 deletions(-) delete mode 100644 src/utils.jl diff --git a/src/JumpProcesses.jl b/src/JumpProcesses.jl index 632eeae6d..4001b3127 100644 --- a/src/JumpProcesses.jl +++ b/src/JumpProcesses.jl @@ -72,7 +72,6 @@ include("coupled_array.jl") include("coupling.jl") include("SSA_stepper.jl") include("simple_regular_solve.jl") -include("utils.jl") export ConstantRateJump, VariableRateJump, RegularJump, MassActionJump, JumpSet @@ -101,7 +100,4 @@ export SpatialMassActionJump export outdegree, num_sites, neighbors export NSM, DirectCRDirect -# utilities to deal with conditional rates -export reset_history!, conditional_rate - end # module diff --git a/src/utils.jl b/src/utils.jl deleted file mode 100644 index 5c3aa4c08..000000000 --- a/src/utils.jl +++ /dev/null @@ -1,71 +0,0 @@ -""" -Removes all entries from the history later than `start_time`. If -`start_time`remove all entries. -""" -function reset_history!(h; start_time = nothing) - if start_time === nothing - start_time = -Inf - end - @inbounds for i in 1:length(h) - hi = h[i] - ix = 0 - if eltype(hi) <: Tuple - while ((ix + 1) <= length(hi)) && hi[ix + 1][1] <= start_time - ix += 1 - end - else - while ((ix + 1) <= length(hi)) && hi[ix + 1] <= start_time - ix += 1 - end - end - h[i] = ix == 0 ? eltype(h)[] : hi[1:ix] - end - nothing -end - -""" -Computes conditional rate, given a vector of `rate_closures`, the history of -the process `h`, the solution `sol`. Optionally, it is possible to provide save -points with `saveat` and the indices of the targeted variables with `ixs`. - -The vector `rate_closures` contains functions `closure(_h)` that returns a -function `rate(u, p, t)` which computes the conditional rate given any history -`_h`. -""" -function conditional_rate(rate_closures, h, sol; saveat = nothing, ixs = 1:length(h)) - if eltype(h[1]) <: Tuple - h = [_h[1] for _h in h] - end - if typeof(saveat) <: Number - _saveat = sol.t[1]:saveat:sol.t[end] - else - _saveat = sol.t - end - p = sol.prob.p - _h = [eltype(h)(undef, 0) for _ in 1:length(h)] - hixs = zeros(Int, length(h)) - condrates = Array{Array{eltype(_saveat), 1}, 1}() - for t in _saveat - @inbounds for i in 1:length(h) - hi = h[i] - ix = hixs[i] - while ((ix + 1) <= length(hi)) && hi[ix + 1] <= t - ix += 1 - end - _h[i] = ix == 0 ? [] : hi[1:ix] - end - u = sol(t) - condrate = Array{typeof(t), 1}() - @inbounds for i in ixs - rate = rate_closures[i](_h) - _rate = rate(u, p, t) - push!(condrate, _rate) - end - push!(condrates, condrate) - end - return DiffEqBase.build_solution(sol.prob, sol.alg, _saveat, condrates, dense = false, - calculate_error = false, - destats = DiffEqBase.DEStats(0), - interp = DiffEqBase.ConstantInterpolation(_saveat, - condrates)) -end From 2b1cdd88394e1cd69549bc752c9acbc8c102054e Mon Sep 17 00:00:00 2001 From: gzagatti Date: Wed, 7 Dec 2022 14:05:15 +0800 Subject: [PATCH 29/72] adds documentation for VariableRateJump. --- docs/src/api.md | 8 +- docs/src/faq.md | 56 +++-- docs/src/index.md | 11 +- docs/src/jump_solve.md | 43 ++-- docs/src/jump_types.md | 220 ++++++++++++------ .../tutorials/discrete_stochastic_example.md | 138 +++++++++-- docs/src/tutorials/jump_diffusion.md | 5 + docs/src/tutorials/simple_poisson_process.md | 50 +++- src/jumps.jl | 53 ++--- 9 files changed, 393 insertions(+), 191 deletions(-) diff --git a/docs/src/api.md b/docs/src/api.md index 7bebbfe82..84db2d6ea 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -13,14 +13,15 @@ reset_aggregated_jumps! ## Types of Jumps ```@docs ConstantRateJump -MassActionJump VariableRateJump +MassActionJump JumpSet ``` ## Aggregators Aggregators are the underlying algorithms used for sampling -[`MassActionJump`](@ref)s and [`ConstantRateJump`](@ref)s. +[`MassActionJump`](@ref)s, [`ConstantRateJump`](@ref)s and +[`VariableRateJump`](@ref)s. ```@docs Direct DirectCR @@ -30,10 +31,11 @@ RDirect RSSA RSSACR SortingDirect +QueueMethod ``` # Private API Functions ```@docs ExtendedJumpArray SSAIntegrator -``` \ No newline at end of file +``` diff --git a/docs/src/faq.md b/docs/src/faq.md index ba569e51e..f956925b9 100644 --- a/docs/src/faq.md +++ b/docs/src/faq.md @@ -1,16 +1,19 @@ # FAQ ## My simulation is really slow and/or using a lot of memory, what can I do? -To reduce memory use, use `save_positions=(false,false)` in the `JumpProblem` -constructor as described [earlier](@ref save_positions_docs) to turn off saving -the system state before and after every jump. Combined with use of `saveat` in -the call to `solve` this can dramatically reduce memory usage. - -While `Direct` is often fastest for systems with 10 or less `ConstantRateJump`s -or `MassActionJump`s, if your system has many jumps or one jump occurs most -frequently, other stochastic simulation algorithms may be faster. See [Constant -Rate Jump Aggregators](@ref) and the subsequent sections there for guidance on -choosing different SSAs (called aggregators in JumpProcesses). +Exact methods simulate each and every jump. To reduce memory use, use +`save_positions=(false,false)` in the `JumpProblem` constructor as described +[earlier](@ref save_positions_docs) to turn off saving the system state before +and after every jump. You often do not need to save all jump poisitons when you +only want to track cumulative counts. Combined with use of `saveat` in the call +to `solve` this can dramatically reduce memory usage. + +While `Direct` is often fastest for systems with 10 or less `ConstantRateJump`s, +`VariableRateJump`s, and/or or `MassActionJump`s, if your system has many jumps +or one jump occurs most frequently, other stochastic simulation algorithms may +be faster. See [Jump Aggregators for Exact Simulation](@ref) and the subsequent +sections there for guidance on choosing different SSAs (called aggregators in +JumpProcesses). ## When running many consecutive simulations, for example within an `EnsembleProblem` or loop, how can I update `JumpProblem`s? @@ -22,8 +25,8 @@ internal aggregators for each new parameter value or initial condition. ## How can I define collections of many different jumps and pass them to `JumpProblem`? We can use `JumpSet`s to collect jumps together, and then pass them into -`JumpProblem`s directly. For example, using the `MassActionJump` and -`ConstantRateJump` defined earlier we can write +`JumpProblem`s directly. For example, using the `ConstantRateJump`, +`VariableRateJump` and `MassActionJump` defined earlier we can write ```julia jset = JumpSet(mass_act_jump, birth_jump) @@ -66,16 +69,19 @@ default. On versions below 1.7 it uses `Xoroshiro128Star`. ## What are these aggregators and aggregations in JumpProcesses? JumpProcesses provides a variety of methods for sampling the time the next -`ConstantRateJump` or `MassActionJump` occurs, and which jump type happens at -that time. These methods are examples of stochastic simulation algorithms -(SSAs), also known as Gillespie methods, Doob's method, or Kinetic Monte Carlo -methods. In the JumpProcesses terminology we call such methods "aggregators", and -the cache structures that hold their basic data "aggregations". See [Constant -Rate Jump Aggregators](@ref) for a list of the available SSA aggregators. +`ConstantRateJump`, `VariableRateJump` or `MassActionJump` occurs, and which +jump type happens at that time. These methods are examples of stochastic +simulation algorithms (SSAs), also known as Gillespie methods, Doob's method, or +Kinetic Monte Carlo methods. These are catch-all terms for jump (or point) +processes simulation methods most commonly used in the biochemistry literature. +In the JumpProcesses terminology we call such methods "aggregators", and the +cache structures that hold their basic data "aggregations". See [Jump +Aggregators for Exact Simulation](@ref) for a list of the available SSA +aggregators. ## How should jumps be ordered in dependency graphs? Internally, JumpProcesses SSAs (aggregators) order all `MassActionJump`s first, -then all `ConstantRateJumps`. i.e. in the example +then all `ConstantRateJumps` and/or `VariableRateJumps`. i.e. in the example ```julia using JumpProcesses @@ -99,15 +105,15 @@ The four jumps would be ordered by the first jump in `maj`, the second jump in `maj`, `cj1`, and finally `cj2`. Any user-generated dependency graphs should then follow this ordering when assigning an integer id to each jump. -See also [Constant Rate Jump Aggregators Requiring Dependency Graphs](@ref) for +See also [Jump Aggregators Requiring Dependency Graphs](@ref) for more on dependency graphs needed for the various SSAs. -## How do I use callbacks with `ConstantRateJump` or `MassActionJump` systems? +## How do I use callbacks with `ConstantRateJump`, `VariableRateJump` or `MassActionJump` systems? -Callbacks can be used with `ConstantRateJump`s and `MassActionJump`s. When -solving a pure jump system with `SSAStepper`, only discrete callbacks can be -used (otherwise a different time stepper is needed). When using an ODE or SDE -time stepper any callback should work. +Callbacks can be used with `ConstantRateJump`s, `VariableRateJump`s and +`MassActionJump`s. When solving a pure jump system with `SSAStepper`, only +discrete callbacks can be used (otherwise a different time stepper is needed). +When using an ODE or SDE time stepper any callback should work. *Note, when modifying `u` or `p` within a callback, you must call [`reset_aggregated_jumps!`](@ref) after making updates.* This ensures that the diff --git a/docs/src/index.md b/docs/src/index.md index aa881a268..721a78384 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -1,9 +1,10 @@ # JumpProcesses.jl: Stochastic Simulation Algorithms for Jump Processes, Jump-ODEs, and Jump-Diffusions JumpProcesses.jl, formerly DiffEqJump.jl, provides methods for simulating jump -processes, known as stochastic simulation algorithms (SSAs), Doob's method, -Gillespie methods, or Kinetic Monte Carlo methods across different fields of -science. It also enables the incorporation of jump processes into hybrid -jump-ODE and jump-SDE models, including jump diffusions. +(or point) processes. Across different fields of science such methods are also +known as stochastic simulation algorithms (SSAs), Doob's method, Gillespie +methods, or Kinetic Monte Carlo methods . It also enables the incorporation of +jump processes into hybrid jump-ODE and jump-SDE models, including jump +diffusions. JumpProcesses is a component package in the [SciML](https://sciml.ai/) ecosystem, and one of the core solver libraries included in @@ -105,4 +106,4 @@ link = "https://github.com/SciML/"*name*".jl/tree/gh-pages/v"*version*"/assets/P ``` ```@raw html ">project file. -``` \ No newline at end of file +``` diff --git a/docs/src/jump_solve.md b/docs/src/jump_solve.md index eedf163ea..3c9f55932 100644 --- a/docs/src/jump_solve.md +++ b/docs/src/jump_solve.md @@ -6,19 +6,32 @@ solve(prob::JumpProblem,alg;kwargs) ## Recommended Methods -A `JumpProblem(prob,aggregator,jumps...)` comes in two forms. The first major -form is if it does not have a `RegularJump`. In this case, it can be solved with -any integrator on `prob`. However, in the case of a pure `JumpProblem` (a -`JumpProblem` over a `DiscreteProblem`), there are special algorithms -available. The `SSAStepper()` is an efficient streamlined algorithm for running -the `aggregator` version of the SSA for pure `ConstantRateJump` and/or -`MassActionJump` problems. However, it is not compatible with event handling. If -events are necessary, then `FunctionMap` does well. - -If there is a `RegularJump`, then specific methods must be used. The current +Because `JumpProblem`s can be solved with two classes of methods, exact and +inexact, they come in two forms. Exact algorithms tend to describe the +realization of each jump chronologically. Alternatively, inexact methods tend to +take small leaps through time so they are guaranteed to terminate in finite +time. These methods can be much faster as they only simulate the total number of +points in each leap interval and thus do not need to simulate the realization of +every single jump. Jumps for exact methods can be defined with +`ConstantRateJump`, `VariableRateJump` and/or `MassActionJump` On the other +hand, jumps for inexact methods are defined with `RegularJump`. + +There are special algorithms available for a pure exact `JumpProblem` (a +`JumpProblem` over a `DiscreteProblem`). The `SSAStepper()` is an efficient +streamlined integrator for running simulation algorithms of such problems. This +integrator is named after the term Stochastic Simulation Algorithm (SSA) which +is a catch-all term in biochemistry to denote algorithms for simulating jump +processes. In turn, we denote aggregators algorithms for simulating jump +processes that can use the `SSAStepper()` integrator. These algorithms can solve +problems initialized with `ConstantRateJump`, `VariableRateJump` and/or +`MassActionJump`. Although `SSAStepper()` is usually faster, it is not +compatible with event handling. If events are necessary, then `FunctionMap` does +well. + +If there is a `RegularJump`, then inexact methods must be used. The current recommended method is `TauLeaping` if you need adaptivity, events, etc. If you -just need the most barebones fixed time step leaping method, then `SimpleTauLeaping` -can have performance benefits. +just need the most barebones fixed time step leaping method, then +`SimpleTauLeaping` can have performance benefits. ## Special Methods for Pure Jump Problems @@ -28,9 +41,9 @@ algorithms are optimized for pure jump problems. ### JumpProcesses.jl -- `SSAStepper`: a stepping algorithm for pure `ConstantRateJump` and/or - `MassActionJump` `JumpProblem`s. Supports handling of `DiscreteCallback` - and saving controls like `saveat`. +- `SSAStepper`: a stepping integrator for pure `ConstantRateJump`, + `VariableRateJump` and/or `MassActionJump` `JumpProblem`s. Supports handling + of `DiscreteCallback` and saving controls like `saveat`. ## RegularJump Compatible Methods diff --git a/docs/src/jump_types.md b/docs/src/jump_types.md index 6cc8a2004..bf6dc566e 100644 --- a/docs/src/jump_types.md +++ b/docs/src/jump_types.md @@ -2,21 +2,27 @@ ### Mathematical Specification of an problem with jumps -Jumps are defined as a Poisson process which changes states at some `rate`. When -there are multiple possible jumps, the process is a compound Poisson process. On its -own, a jump equation is a continuous-time Markov Chain where the time to the -next jump is exponentially distributed as calculated by the rate. This type of -process, known in biology as "Gillespie discrete stochastic simulations" and -modeled by the Chemical Master Equation (CME), is the same thing as adding jumps -to a `DiscreteProblem`. However, any differential equation can be extended by jumps -as well. For example, we have an ODE with jumps, denoted by +Jumps (or point) processes are stochastic processes with discrete changes driven +by some `rate`. The homogeneous Poisson process is the canonical point process +with a constant rate of change. Processes involving multiple jumps are known as +compound jump (or point) processes. + +A compound Poisson process is a continuous-time Markov Chain where the time to +the next jump is exponentially distributed as calculated by the rate. This type +of process is known in biology as "Gillespie discrete stochastic simulation", +modeled by the Chemical Master Equation (CME). Alternatively, in the statistics +literature the composition of Poisson processes is described by the +superposition theorem. + +Any differential equation can be extended by jumps. For example, we have an ODE +with jumps, denoted by ```math \frac{du}{dt} = f(u,p,t) + \sum_{i}c_i(u,p,t)p_i(t) ``` -where ``p_i`` is a Poisson counter of rate ``\lambda_i(u,p,t)``. -Extending a stochastic differential equation to have jumps is commonly known as a Jump +where ``p_i`` is a Poisson counter of rate ``\lambda_i(u,p,t)``. Extending +a stochastic differential equation to have jumps is commonly known as a Jump Diffusion, and is denoted by ```math @@ -25,19 +31,34 @@ du = f(u,p,t)dt + \sum_{j}g_j(u,t)dW_j(t) + \sum_{i}c_i(u,p,t)dp_i(t) ## Types of Jumps: Regular, Variable, Constant Rate and Mass Action -A `RegularJump` is a set of jumps that do not make structural changes to the -underlying equation. These kinds of jumps only change values of the dependent -variable (`u`) and thus can be treated in an inexact manner. Other jumps, such -as those which change the size of `u`, require exact handling which is also -known as time-adaptive jumping. These can only be specified as a -`ConstantRateJump`, `MassActionJump`, or a `VariableRateJump`. -We denote a jump as variable rate if its rate function is dependent on values -which may change between constant rate jumps. For example, if there are multiple -jumps whose rates only change when one of them occur, than that set of jumps is -a constant rate jump. If a jump's rate depends on the differential equation, -time, or by some value which changes outside of any constant rate jump, then it -is denoted as variable. +Exact algorithms tend to describe the realization of each jump chronologically. +In more complex cases, such jumps are conditioned on the history of past events. +Such jumps are usually associated with changes to the state variable `u` which +in turn changes the `rate` of event occurence. These jumps can be specified as +a `ConstantRateJump`, `MassActionJump`, or a `VariableRateJump`. Since exact +methods simulate each and every point, they might face termination issues when +the `rate` of event occurrence explodes. + +Alternatively, inexact methods tend to take small leaps through time so they are +guaranteed to terminate in finite time. These methods can be much faster as they +only simulate the total number of points in each leap interval and thus do not +need to simulate the realization of every single jump. Since inexact methods +trade accuracy for speed, they should be used when a set of jumps do not make +significant changes to the system during the leap interval. A `RegularJump` is +used for inexact algorithms. Note that inexact methods are not always +inaccurate. In the case of homogeneous Poisson processes, they produce accurate +results. However, they can produce less accurate results for more complex +problems, thus it is important to have a good understanding of the problem. As +a rule of thumb, if changes to the state variable `u` during a leap are minimal +compared to size of the system, an inexact method should provide reasonable +solutions. + +We denote a jump as variable if its rate function is dependent on values +which may change between any jump in the system. For instance, when the rate is +a function of time. Variable jumps can be more expensive to simulate because it +is necessary to take into account the dynamics of the rate function when +simulating the next jump time. A `MassActionJump` is a specialized representation for a collection of constant rate jumps that can each be interpreted as a standard mass action reaction. For @@ -46,42 +67,57 @@ will offer improved performance. 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` to handle the non-mass action jumps. +create a number of `ConstantRateJumps` or `VariableRateJump` to handle the +non-mass action jumps. -`RegularJump`s are optimized for regular jumping algorithms like tau-leaping and -hybrid algorithms. `ConstantRateJump`s and `MassActionJump`s are optimized for -SSA algorithms. `ConstantRateJump`s, `MassActionJump`s and `VariableRateJump`s -can be added to standard DiffEq algorithms since they are simply callbacks, -while `RegularJump`s require special algorithms. +`RegularJump`s are optimized for inexact jumping algorithms like tau-leaping and +hybrid algorithms. `ConstantRateJump`, `VariableRateJump`, `MassActionJump` are +optimized for exact methods (also known in the biochemistry literature as SSA +algorithms). `ConstantRateJump`s, `VariableRateJump`s and `MassActionJump`s can +be added to standard DiffEq algorithms since they are simply callbacks, while +`RegularJump`s require special algorithms. -#### Defining a Regular Jump +#### Defining a Constant Rate Jump -The constructor for a `RegularJump` is: +The constructor for a `ConstantRateJump` is: ```julia -RegularJump(rate,c,numjumps;mark_dist = nothing) +ConstantRateJump(rate,affect!) ``` -- `rate(out,u,p,t)` is the function which computes the rate for every regular - jump process -- `c(du,u,p,t,counts,mark)` is calculates the update given `counts` number of - jumps for each jump process in the interval. -- `numjumps` is the number of jump processes, i.e. the number of `rate` equations - and the number of `counts` -- `mark_dist` is the distribution for the mark. +- `rate(u,p,t)` is a function which calculates the rate given the time and the state. +- `affect!(integrator)` is the effect on the equation, using the integrator interface. -#### Defining a Constant Rate Jump +#### Defining a Variable Rate Jump -The constructor for a `ConstantRateJump` is: +The constructor for a `VariableRateJump` is: ```julia -ConstantRateJump(rate,affect!) +VariableRateJump(rate,affect!; + lrate=nothing, urate=nothing, L=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 time and the state. - `affect!(integrator)` is the effect on the equation, using the integrator interface. - - +- When planning to use the `QueueMethod` aggregator, the arguments `lrate`, + `urate` and `L` are required. They consist of three functions: `lrate(u, p, + t)` computes the lower bound of the intensity rate in the interval `t` to `t + + L` given state `u` and parameters `p`; `urate(u, p, t)` computes the upper + bound of the intensity rate; and `L(u, p, t)` computes the interval length + for which the rate is bounded between `lrate` and `urate`. +- It is only possible to solve a `VariableRateJump` with `SSAStepper` when using + the `QueueMethod` aggregator. +- When using a different aggregator than `QueueMethod`, there is no need to + define `lrate`, `urate` and `L`. Note that in this case, the problem can only + be solved with continuous integration. Internally, `VariableRateJump` is + transformed into a `ContinuousCallback`. The `rate(u, p, t)` is used to + construct the `condition` function that triggers the callback. + #### Defining a Mass Action Jump The constructor for a `MassActionJump` is: @@ -142,23 +178,21 @@ MassActionJump(reactant_stoich, net_stoich; scale_rates = true, param_idxs=nothi reactant_stoich = [[3 => 1, 1 => 2, 4 => 2], [3 => 2, 2 => 2]] ``` +#### Defining a Regular Jump -#### Defining a Variable Rate Jump - -The constructor for a `VariableRateJump` is: +The constructor for a `RegularJump` is: ```julia -VariableRateJump(rate,affect!; - idxs = nothing, - rootfind=true, - save_positions=(true,true), - interp_points=10, - abstol=1e-12,reltol=0) +RegularJump(rate,c,numjumps;mark_dist = nothing) ``` -Note that this is the same as defining a `ContinuousCallback`, except that instead -of the `condition` function, you provide a `rate(u,p,t)` function for the `rate` at -a given time and state. +- `rate(out,u,p,t)` is the function which computes the rate for every regular + jump process +- `c(du,u,p,t,counts,mark)` calculates the update given `counts` number of + jumps for each jump process in the interval. +- `numjumps` is the number of jump processes, i.e. the number of `rate` equations + and the number of `counts` +- `mark_dist` is the distribution for the mark. ## Defining a Jump Problem @@ -172,27 +206,36 @@ JumpProblem(prob,aggregator::Direct,jumps::JumpSet; save_positions = typeof(prob) <: AbstractDiscreteProblem ? (false,true) : (true,true)) ``` -The aggregator is the method for aggregating the constant jumps. These are defined -below. `jumps` is a `JumpSet` which is just a gathering of jumps. Instead of -passing a `JumpSet`, one may just pass a list of jumps themselves. For example: +The aggregator is the method for simulating jumps. They are called aggregators +since they combine all `jumps` in a single discrete simulation algorithm. +Aggregators are defined below. `jumps` is a `JumpSet` which is just a gathering +of jumps. Instead of passing a `JumpSet`, one may just pass a list of jumps +themselves. For example: ```julia JumpProblem(prob,aggregator,jump1,jump2) ``` -and the internals will automatically build the `JumpSet`. `save_positions` is the -`save_positions` argument built by the aggregation of the constant rate jumps. +and the internals will automatically build the `JumpSet`. `save_positions` +determines whether to save the state of the system just before and/or after +events 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). -## Constant Rate Jump Aggregators +## Jump Aggregators for Exact Simulation + +Jump aggregators are methods for simulating jumps exactly. They are called +aggregators since they combine all `jumps` in a single discrete simulation +algorithm. Aggregators combine `jump` in different ways and offer different +trade-offs. However, all aggregators describe the realization of each and every +jump chronologically. Since they do not skip any jump, they are considered exact +methods. Note that none of the aggregators discussed in this section can be used +with `RegularJumps` which are used for inexact methods. -Constant rate jump aggregators are the methods by which constant rate -jumps, including `MassActionJump`s, are lumped together. This is required in all -algorithms for both speed and accuracy. The current methods are: +The current aggregators are: - `Direct`: the Gillespie Direct method SSA. - `RDirect`: A variant of Gillespie's Direct method that uses rejection to @@ -218,6 +261,11 @@ algorithms for both speed and accuracy. The current methods are: - *`SortingDirect`*: The Sorting Direct Method of McCollum et al. It will usually offer performance as good as `Direct`, and for some systems can offer substantially better performance. (Requires dependency graph, see below.) +- *`QueueMethod`*: The queueing method. This is a modification of Ogata's + algorihm for simulating any compound point process that evolves through time. + This is the only aggregator that handles `VariableRateJump`. If rates do not + change between jump events (i.e. `ConsantRateJump` or `MassActionJump`) this + aggregator is very similar to `NRM`. (Requires dependency graph, see below.) To pass the aggregator, pass the instantiation of the type. For example: @@ -228,21 +276,27 @@ JumpProblem(prob,Direct(),jump1,jump2) will build a problem where the constant rate jumps are solved using Gillespie's Direct SSA method. -## Constant Rate Jump Aggregators Requiring Dependency Graphs +## Jump Aggregators Requiring Dependency Graphs Italicized constant rate jump aggregators require the user to pass a dependency -graph to `JumpProblem`. `DirectCR`, `NRM` and `SortingDirect` require a -jump-jump dependency graph, passed through the named parameter `dep_graph`. i.e. +graph to `JumpProblem`. `DirectCR`, `NRM`, `SortingDirect` and `QueueMethod` +require a jump-jump dependency graph, passed through the named parameter +`dep_graph`. i.e. ```julia JumpProblem(prob,DirectCR(),jump1,jump2; dep_graph=your_dependency_graph) ``` For systems with only `MassActionJump`s, or those generated from a [Catalyst](https://docs.sciml.ai/Catalyst/stable/) `reaction_network`, this graph -will be auto-generated. Otherwise you must construct the dependency graph -manually. 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 (with the latter internally ordered in the -same order they were passed in). +will be auto-generated. Otherwise, you must construct the dependency graph +whenever using `ConstantRateJump`s and/or `VariableRateJump`s. This is also the +case when combining `MassActionJump` with `ConstantRateJump`s and/or +`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 `VariableRateJump`s (with the latter internally ordered +in the same order they were passed in). Thus, keep that in mind when combining +`MassActionJump`s with other types of jumps. `RSSA` and `RSSACR` require two different types of dependency graphs, passed through the following `JumpProblem` kwargs: @@ -257,11 +311,17 @@ For systems generated from a [Catalyst](https://docs.sciml.ai/Catalyst/stable/) `reaction_network` these will be auto-generated. Otherwise you must explicitly construct and pass in these mappings. -## Recommendations for Constant Rate Jumps -For representing and aggregating constant rate jumps +## Recommendations for exact methods +For representing and aggregating jumps - Use a `MassActionJump` to handle all jumps that can be represented as mass - action reactions. This will generally offer the fastest performance. -- Use `ConstantRateJump`s for any remaining jumps. + action reactions with constant rate between jumps. This will generally offer + the fastest performance. +- Use `ConstantRateJump`s for any remaining jumps with constant rate between + jumps. +- Use `VariableRateJump`s for any remaining jumps with variable rate between + jumps. You will need to define the lower and upper rate boundaries as well as + the interval for which the boundaries apply. The tighter the boundaries and + the easier to compute, the faster the resulting algorithm will be. - 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 than `Direct`. @@ -270,6 +330,12 @@ For representing and aggregating constant rate jumps `NRM` often have the best performance. - For very large networks, with many updates per jump, `RSSA` and `RSSACR` will often substantially outperform the other methods. +- For systems with `VariableRateJump`, only the `QueueMethod` aggregator is + supported. +- The `SSAStepper()` can be used with `VariableRateJump`s that modify the state + of differential equations. However, it is not possible to use `SSAStepper()` + to solve `VariableRateJump` that are combined with differential equations that + modify the rate of the jumps. In general, for systems with sparse dependency graphs if `Direct` is slow, one of `SortingDirect`, `RSSA` or `RSSACR` will usually offer substantially better diff --git a/docs/src/tutorials/discrete_stochastic_example.md b/docs/src/tutorials/discrete_stochastic_example.md index ad9634b2a..50afaeec3 100644 --- a/docs/src/tutorials/discrete_stochastic_example.md +++ b/docs/src/tutorials/discrete_stochastic_example.md @@ -1,9 +1,9 @@ # [Continuous-Time Jump Processes and Gillespie Methods](@id ssa_tutorial) In this tutorial we will describe how to define and simulate continuous-time -jump processes, also known in biological fields as stochastic chemical kinetics -(i.e. Gillespie) models. It is not necessary to have read the [first -tutorial](@ref poisson_proc_tutorial). We will illustrate +jump (or point) processes, also known in biological fields as stochastic +chemical kinetics (i.e. Gillespie) models. It is not necessary to have read the +[first tutorial](@ref poisson_proc_tutorial). We will illustrate - The different types of jumps that can be represented in JumpProcesses and their use cases. - How to speed up pure-jump simulations with only [`ConstantRateJump`](@ref)s @@ -136,8 +136,8 @@ is then given by the rate constant multiplied by the number of possible pairs of susceptible and infected people. This formulation is known as the [law of mass action](https://en.wikipedia.org/wiki/Law_of_mass_action). Similarly, we have that each individual infected person is assumed to recover with probability per -time ``\nu``, so that the probability per time *some* infected person becomes -recovered is ``\nu`` times the number of infected people, i.e. ``\nu I(t)``. +time ``\nu``, so that the probability per time *some* infected person recovers +is ``\nu`` times the number of infected people, i.e. ``\nu I(t)``. Rate functions give the probability per time for each of the two types of jumps to occur, and hence determine when the state of our system changes. To fully @@ -253,14 +253,18 @@ In general | Jump Type | Performance | Generality | |:----------: | :----------: |:------------:| | [`MassActionJump`](@ref MassActionJumpSect) | Fastest | Restrictive rates/affects | -| [`ConstantRateJump`](@ref ConstantRateJumpSect) | Somewhat Slower | Much more general | +| [`ConstantRateJump`](@ref ConstantRateJumpSect) | Somewhat Slower | Rates must +be constant between jumps | +| [`VariableRateJump` with `QueueMethod` aggregator](@ref VariableRateJumpQueueMethodSect) | Somewhat Slower | Rates can be a function of time, but not of ODE variables | | [`VariableRateJump`](@ref VariableRateJumpSect) | Slowest | Completely general | It is recommended to try to encode jumps using the most performant option that -supports the desired generality of the underlying `rate` and `affect` functions. -Below we describe the different jump types, and show how the SIR model can be -formulated using first `ConstantRateJump`s and then `MassActionJump`s -(`VariableRateJump`s are considered later). +supports the desired generality of the underlying `rate` and `affect!` +functions. Below we describe the different jump types, and show how the SIR +model can be formulated using first `ConstantRateJump`s, `VariableRateJump`s +with `QueueMethod` aggregator and then `MassActionJump`s. Completely general +models that use `VariableRateJump`s with over an `ODEProblem` are considered +later. ## [Defining the Jumps Directly: `ConstantRateJump`](@id ConstantRateJumpSect) The constructor for a `ConstantRateJump` is: @@ -369,23 +373,109 @@ these rates only change when `u[1]` or `u[2]` is changed, and `u[1]` and `u[2]` only change when one of the jumps occur, this setup is valid. However, a rate of `t*p[1]*u[1]*u[2]` would not be valid because the rate would change during the interval, as would `p[2]*u[1]*u[4]` when `u[4]` is the solution to a continuous -problem such as an ODE or SDE or can be changed via a `VariableRateJump`. Thus -one must be careful to follow this rule when choosing rates. +problem such as an ODE or SDE. Thus one must be careful to follow this rule when +choosing rates. In summary, if a particular jump process has a rate function that depends explicitly or implicitly on a continuously changing quantity, you need to use a [`VariableRateJump`](@ref). +## [Defining the Jumps Directly: `VariableRateJump`](@id VariableRateJumpQueueMethodSect) + +Now, let's assume that the infection rate is decreasing over time. That is, once +individuals gets infected they reaches peak infectivity. The force of infection +then decreases exponentially to a basal level. In this case, we must keep track +of the time of infection events. Let the history `H(t)` contain the timestamp of +all active infections. Then, the rate of infection becomes +```math +\beta S(t) I(t) + \alpha S(t) \sum_{t_i \in H(t)} exp(-\gamma (t - t_i)) +``` +Where ``\beta`` is the basal rate of infection, ``\alpha`` is the spike in the +rate of infection and ``\gamma`` is the rate which the spike decreases. Here we +choose parameters such that infectivity reaches a basal rate close to ``0`` +after spiking. The spike is equal to the rate ``\beta`` chosen in the previous +section and ``gamma`` is the same as the recovery rate. In other words, we are +modelling a situation in which individuals gradually recover. + +```@example tut2 +β1 = 0.001 / 1000.0 +α = 0.1 / 1000.0 +γ = 0.001 +p1 = (β1, ν, α, γ) +``` + +We define a vector `H` to hold the timestamp of active infections. Then, we +define infection as a `VariableRateJump`. To use the `QueueMethod` aggregator, +we need to specify the lower- and upper-bounds of the rate which should be valid +from the time they are computed `t` until `t + L(u, p, t)`. + +```@example tut2 +H = zeros(Float64, 10) +rate3(u, p, t) = p[1]*u[1]*u[2] + p[3]*u[1]*sum([exp(-p[4]*(t - _t)) for _t in H]) +lrate = rate1 # β*S*I +urate = rate3 +L(u, p, t) = 1 / (2*urate(u, p, t)) +function affect3!(integrator) + integrator.u[1] -= 1 # S -> S - 1 + integrator.u[2] += 1 # I -> I + 1 + push!(I, integrator.t) + nothing +end +jump3 = VariableRateJump(rate3, affect3; lrate=lrate, urate=urate, L=L) +``` + +Next, we redefine the recovery jump's `affect!` such that a random infection is +removed from `H` for every recovery. + +```@example tut2 +function affect4!(integrator) + integrator.u[2] -= 1 + integrator.u[3] += 1 + length(I) > 0 && deleteat!(H, rand(1:length(I))) + nothing +end +jump4 = ConstantRateJump(rate2, affect4!) +``` + +With the jumps defined, we can build +a [`DiscreteProblem`](https://docs.sciml.ai/DiffEqDocs/stable/types/discrete_types/). +`VariableRateJump`s over a `DiscreteProblem` can only be solved with the +`QueueMethod` aggregator. We need to specify a depedency graph to use this +aggregator. In this case, both processes mutually affect each other. + +```@example tut2 +jump_prob = JumpProblem(prob, QueueMethod(), jump3, jump4; dep_graph=[[1,2], [1,2]]) +``` + +We now have a problem that can be solved with `SSAStepper` to handle +time-stepping the `QueueMethod` aggregator from jump to jump: + +```@example tut2 +sol = solve(jump_pro, SSAStepper()) +plot(sol, label=["S(t)", "I(t)", "R(t)"]) +``` + +Surprinsingly, we see that even with an exponential decrease in infectivity we +reach similar results as with a constant infection rate. + +Note that `VariableRateJump` over `DiscreteProblem` can be quite general, but it +is not possible to handle rates that change according to an ODE variable. A rate +such as `p[2]*u[1]*u[4]` when `u[4]` is the solution of a continuous problem +such as an ODE or SDE can only solved using a continuous integrator as discussed +[below](@ref VariableRateJumpSect) + ## SSAStepper Any common interface algorithm can be used to perform the time-stepping since it is implemented over the callback interface. This allows for hybrid systems that mix ODEs, SDEs and jumps. In many cases we may have a pure jump system that only -involves `ConstantRateJump`s and/or `MassActionJump`s (see below). When that's -the case, a substantial performance benefit may be gained by using +involves `ConstantRateJump`s, `VariableRateJump`s whose rates do not depend on +a continuous variable, and/or `MassActionJump`s (see below). When that's the +case, a substantial performance benefit may be gained by using [`SSAStepper`](@ref). Note, `SSAStepper` is a more limited time-stepper which only supports discrete events, and does not allow simultaneous coupled ODEs or -SDEs or `VariableRateJump`s. It is, however, very efficient for pure jump -problems involving only `ConstantRateJump`s and `MassActionJump`s. +SDEs or `VariableRateJump`s whose rate depend on a continuous variable. It is, +however, very efficient for pure jump problems involving only +`ConstantRateJump`s, `VariableRateJump`s and `MassActionJump`s. ## [Reducing Memory Use: Controlling Saving Behavior](@id save_positions_docs) @@ -506,7 +596,7 @@ For chemical reaction systems Catalyst.jl automatically groups reactions into their optimal jump representation. -## Defining the Jumps Directly: Mixing `ConstantRateJump` and `MassActionJump` +## Defining the Jumps Directly: Mixing `ConstantRateJump`/`VariableRateJump` and `MassActionJump` Suppose we now want to add in to the SIR model another jump that can not be represented as a mass action reaction. We can create a new `ConstantRateJump` and simulate a hybrid system using both the `MassActionJump` for the two @@ -527,6 +617,9 @@ sol = solve(jump_prob, SSAStepper()) plot(sol; label=["S(t)" "I(t)" "R(t)"]) ``` +Note that, we can also combine `MassActionJump` with `VariableRateJump` when +using the `QueueMethod` aggregator in the same manner. + ## Adding Jumps to a Differential Equation If we instead used some form of differential equation instead of a `DiscreteProblem`, we would couple the jumps/reactions to the differential @@ -544,15 +637,20 @@ prob = ODEProblem(f, u₀, tspan, p) Notice we gave the 4th component a starting value of 100.0, and used floating point numbers for the initial condition since some solution components now evolve continuously. The same steps as above will allow us to solve this hybrid -equation when using `ConstantRateJumps` (or `MassActionJump`s). For example, we -can solve it using the `Tsit5()` method via: +equation when using `ConstantRateJump`s, `VariableRateJump`s or +`MassActionJump`s. For example, we can solve it using the `Tsit5()` method via: ```@example tut2 jump_prob = JumpProblem(prob, Direct(), jump, jump2) sol = solve(jump_prob, Tsit5()) plot(sol; label=["S(t)" "I(t)" "R(t)" "u₄(t)"]) ``` -## [Adding a VariableRateJump](@id VariableRateJumpSect) +Note that when using `VariableRateJump`s with the `QueueMethod` aggregator, the +ODE problem should not modify the rates of any jump. However, the opposite where +the jumps modify the ODE variables is allowed. In these cases, you should +observe an improved performance when using the `QueueMethod`. + +## [Adding a VariableRateJump that Depends on a Continuous Variable](@id VariableRateJumpSect) Now let's consider adding a reaction whose rate changes continuously with the differential equation. To continue our example, let there be a new reaction with rate depending on `u[4]` of the form ``u_4 \to u_4 + \textrm{I}``, with a diff --git a/docs/src/tutorials/jump_diffusion.md b/docs/src/tutorials/jump_diffusion.md index 3f0371098..c5ecf6238 100644 --- a/docs/src/tutorials/jump_diffusion.md +++ b/docs/src/tutorials/jump_diffusion.md @@ -120,6 +120,11 @@ plot(sol) In this way we have solve a mixed jump-ODE, i.e. a piecewise deterministic Markov process. +Note that in this case, the rate of the `VariableRateJump`s depend on a variable +that is driven by an `ODEProblem`, thus we cannot use the `QueueMethod` to solve +the jump problem. + + ## Jump Diffusion Now we will finally solve the jump diffusion problem. The steps are the same as before, except we now start with a `SDEProblem` instead of an `ODEProblem`. diff --git a/docs/src/tutorials/simple_poisson_process.md b/docs/src/tutorials/simple_poisson_process.md index 85d078429..924383500 100644 --- a/docs/src/tutorials/simple_poisson_process.md +++ b/docs/src/tutorials/simple_poisson_process.md @@ -188,25 +188,51 @@ D(t) &= Y_d \left(\int_0^t \mu N(s^-) \, ds \right). \end{align*} ``` -We'll then re-encode the first jump as a +The birth rate is cyclical, bounded between a lower-bound of ``λ`` and an +upper-bound of ``2 λ``. We'll then re-encode the first jump as a `VariableRateJump` ```@example tut1 rate1(u,p,t) = p.λ * (sin(pi*t/2) + 1) +lrate1(u,p,t) = p.λ +urate1(u, p, t) = 2 * p.λ +L1(u, p, t) = typemax(t) affect1!(integrator) = (integrator.u[1] += 1) -vrj = VariableRateJump(rate1, affect1!) +vrj1 = VariableRateJump(rate1, affect1!; lrate=lrate1, urate=urate1, L=L1) ``` -Because this new jump can modify the value of `u[1]` between death events, and -the death transition rate depends on this value, we must also update our death -jump process to also be a `VariableRateJump` + +Since births modify the population size `u[1]` and deaths `u[2]` occur at a rate +proportional to the population size. We must represent this relation in +a dependency graph. Note that the indices in the graph correspond to the order +in which the jumps appear when the problem is constructed. The graph below +indicates that births (event 1) modify deaths (event 2), but deaths do not +modify births. +```@example tut1 +dep_graph = [[2], []] +``` + +We can then construct the corresponding problem, passing both jumps to +`JumpProblem` as well as the dependency graph. Since we are dealing with +a `VariableRateJump` we must use the `QueueMethod` aggregator. +```@example tut1 +jprob = JumpProblem(dprob, QueueMethod(), vrj1, deathcrj; dep_graph=dep_graph) +sol = solve(jprob, SSAStepper()) +plot(sol, label=["N(t)" "D(t)"], xlabel="t", legend=:topleft) +``` + +In a scenario, where we did not know the bounds of the time-dependent rate. We +would have to use a continuous problem type to properly handle the jump times. +Under this assumption we would define the `VariableRateJump` as following: +```@example tut1 +vrj2 = VariableRateJump(rate1, affect1!) +``` + +Since the death rate now depends on a `VariableRateJump` without bounds, we need +to redefine the death jump process as a `VariableRateJump` ```@example tut1 deathvrj = VariableRateJump(deathrate, deathaffect!) ``` -Note, if the death rate only depended on values that were unchanged by a -variable rate jump, then it could have remained a `ConstantRateJump`. This would -have been the case if, for example, it depended on `u[2]` instead of `u[1]`. -To simulate our jump process we now need to use a continuous problem type to -properly handle determining the jump times. We do this by constructing an +To simulate our jump process we now need to construct an ordinary differential equation problem, `ODEProblem`, but setting the ODE derivative to preserve the state (i.e. to zero). We are essentially defining a combined ODE-jump process, i.e. a [piecewise deterministic Markov @@ -234,7 +260,7 @@ function f!(du, u, p, t) end u₀ = [0.0, 0.0] oprob = ODEProblem(f!, u₀, tspan, p) -jprob = JumpProblem(oprob, Direct(), vrj, deathvrj) +jprob = JumpProblem(oprob, Direct(), vrj2, deathvrj) ``` We simulate our jump process, using the `Tsit5` ODE solver as the time stepper in place of `SSAStepper` @@ -283,4 +309,4 @@ dprob = DiscreteProblem(u₀, tspan, p) jprob = JumpProblem(dprob, Direct(), crj) sol = solve(jprob, SSAStepper()) plot(sol, label=["N(t)" "G(t)"], xlabel="t") -``` \ No newline at end of file +``` diff --git a/src/jumps.jl b/src/jumps.jl index b352b2bc1..d19e5e0ef 100644 --- a/src/jumps.jl +++ b/src/jumps.jl @@ -53,42 +53,27 @@ affect!(integrator) = integrator.u[1] -= 1 vrj = VariableRateJump(rate, affect!) ``` -Suppose `u[1]` follows a Hawkes jump process. This is a type of self-exciting -process in which the realization of an event increases the likelihood of new -nearby events. A corresponding `VariableRateJump` for this jump process is +In case we want to use the `QueueMethod` aggregator, we need to pass the rate +boundaries and interval for which the rates apply. The `QueueMethod` aggregator +allow us to perform discrete steps with `SSAStepper()`. ```julia -function rate(u, p, t, g, h) - λ, α, β = p - x = zero(typeof(t)) - for _t in reverse(h[1]) - _x = α*exp(-β*(t - _t)) - if _x ≈ 0 break end - x += _x - end - return λ + x -end -lrate(u, p, t, g, h) = p[1] -urate(u, p, ,t, g, h) = rate(u, p, t, g, h) -function L(u, p, t, g, h) - _lrate = lrate(u, p, t, g, h) - _urate = urate(u, p, t, g, h) - return _urate == _lrate ? typemax(t) : 1/(2*_urate) -end -affect!(integrator) = integrator.u[1] += 1 +L(u,p,t) = (1/p[1])*2 +rate(u,p,t) = t*p[1]*u[1] +lrate(u, p, t) = rate(u, p, t) +urate(u,p,t) = rate(u, p, t + L(u,p,t)) +affect!(integrator) = integrator.u[1] -= 1 vrj = VariableRateJump(rate, affect!; lrate=lrate, urate=urate, L=L) -prob = DiscreteProblem(u0, tspan, p) -jprob = JumpProblem(prob, QueueMethod(), vrj) ``` ## Notes -- **`VariableRateJump`s result in `integrator`s storing an effective state type - that wraps the main state vector.** See [`ExtendedJumpArray`](@ref) for - details on using this object. Note that the presence of *any* - `VariableRateJump`s will result in all `ConstantRateJump`, `VariableRateJump` - and callback `affect!` functions receiving an integrator with `integrator.u` - an [`ExtendedJumpArray`](@ref). -- When using the `QueueMethod` aggregator `DiscreteProblem` can be used. +- When using the `QueueMethod` aggregator, `DiscreteProblem` can be used. Otherwise, `ODEProblem` or `SDEProblem` must be used to be correctly simulated. +- **When not using the `QueueMethod` aggregator, `VariableRateJump`s result in + `integrator`s storing an effective state type that wraps the main state + vector.** See [`ExtendedJumpArray`](@ref) for details on using this object. Note + that the presence of *any* `VariableRateJump`s will result in all + `ConstantRateJump`, `VariableRateJump` and callback `affect!` functions + receiving an integrator with `integrator.u` an [`ExtendedJumpArray`](@ref). - Salis H., Kaznessis Y., Accurate hybrid stochastic simulation of a system of coupled chemical or biochemical reactions, Journal of Chemical Physics, 122 (5), DOI:10.1063/1.1835951 is used for calculating jump times with @@ -102,16 +87,16 @@ struct VariableRateJump{R, F, R2, R3, R4, I, T, T2} <: AbstractJump of the jump given `integrator`.""" affect!::F """When planning to use the `QueueMethod` aggregator, function `lrate(u, p, - t)` that computes the lower rate bound in interval `t` to `t + L` at time + t)` that computes the lower bound of the rate in interval `t` to `t + L` at time `t` given state `u`, parameters `p`. This is not required if using another aggregator.""" lrate::R2 - """When planning to use the `QueueMethod` aggregator, function `lrate(u, p, - t)` that computes the upper rate bound in interval `t` to `t + L` at time + """When planning to use the `QueueMethod` aggregator, function `urate(u, p, + t)` that computes the upper bound of the rate in interval `t` to `t + L` at time `t` given state `u`, parameters `p`. This is not required if using another aggregator.""" urate::R3 - """When planning to use the `QueueMethod` aggregator, function `lrate(u, p, + """When planning to use the `QueueMethod` aggregator, function `L(u, p, t)` that computes the interval length `L` starting at time `t` given state `u`, parameters `p` for which the rate is bounded between `lrate` and `urate`. This is not required if using another aggregator.""" From d6ac52d78a450ad85385904a60b0265a8f55b21b Mon Sep 17 00:00:00 2001 From: gzagatti Date: Wed, 7 Dec 2022 14:05:40 +0800 Subject: [PATCH 30/72] fix if statement. --- src/aggregators/queue.jl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/aggregators/queue.jl b/src/aggregators/queue.jl index 5f9cd587f..e72fe3526 100644 --- a/src/aggregators/queue.jl +++ b/src/aggregators/queue.jl @@ -32,15 +32,15 @@ function QueueMethodJumpAggregation(nj::Int, njt::T, et::T, crs::Nothing, sr::No else dg = make_dependency_graph(length(u), maj) end + else + # using a Set to ensure that edges are not duplicate + dg = [Set{Int}(append!([], jumps, [var])) + for (var, jumps) in enumerate(dep_graph)] + dg = [sort!(collect(i)) for i in dg] end num_jumps = get_num_majumps(maj) + length(rs) - # using a Set to ensure that edges are not duplicate - dg = [Set{Int}(append!([], jumps, [var])) - for (var, jumps) in enumerate(dep_graph)] - dg = [sort!(collect(i)) for i in dg] - if length(dg) != num_jumps error("Number of nodes in the dependency graph must be the same as the number of jumps.") end From ceb293b4e2c744978890727ca110a0d57dcd110c Mon Sep 17 00:00:00 2001 From: gzagatti Date: Wed, 7 Dec 2022 14:06:11 +0800 Subject: [PATCH 31/72] adds test for VariableRateJump. --- test/hawkes_test.jl | 122 ++++++++++++++++++++++++++++++++++++++++++++ test/runtests.jl | 1 + 2 files changed, 123 insertions(+) create mode 100644 test/hawkes_test.jl diff --git a/test/hawkes_test.jl b/test/hawkes_test.jl new file mode 100644 index 000000000..61c4574e6 --- /dev/null +++ b/test/hawkes_test.jl @@ -0,0 +1,122 @@ +using JumpProcesses, OrdinaryDiffEq, Statistics +using Test +using StableRNGs +rng = StableRNG(12345) + + +function reset_history!(h; start_time = nothing) + @inbounds for i = 1:length(h) + h[i] = eltype(h)[] + end + nothing +end + +function empirical_rate(sol) + return (sol(sol.t[end]) - sol(sol.t[1])) / (sol.t[end] - sol.t[1]) +end + +function hawkes_rate(i::Int, g, h) + function rate(u, p, t) + λ, α, β = p + x = zero(typeof(t)) + for j in g[i] + for _t in reverse(h[j]) + λij = α*exp(-β*(t - _t)) + if λij ≈ 0 break end + x += λij + end + end + return λ + x + end + return rate +end + +function hawkes_jump(i::Int, g, h) + rate = hawkes_rate(i, g, h) + lrate(u, p, t) = p[1] + urate = rate + function L(u, p, t) + _lrate = lrate(u, p, t) + _urate = urate(u, p, t) + return _urate == _lrate ? typemax(t) : 1/(2*_urate) + end + function affect!(integrator) + push!(h[i], integrator.t) + integrator.u[i] += 1 + end + return VariableRateJump(rate, affect!; lrate=lrate, urate=urate, L=L) +end + +function hawkes_jump(u, g, h) + return [hawkes_jump(i, g, h) for i in 1:length(u)] +end + +function hawkes_problem(p, agg::QueueMethod; u=[0.], tspan=(0., 50.), save_positions=(false, true), + g = [[1]], h = [[]]) + dprob = DiscreteProblem(u, tspan, p) + jumps = hawkes_jump(u, g, h) + jprob = JumpProblem(dprob, agg, jumps...; + dep_graph=g, save_positions=save_positions, rng=rng) + return jprob +end + +function f!(du, u, p, t) + du .= 0 + nothing +end + +function hawkes_problem(p, agg; u=[0.], tspan=(0., 50.), save_positions=(false, true), + g = [[1]], h = [[]]) + oprob = ODEProblem(f!, u, tspan, p) + jumps = hawkes_jump(u, g, h) + jprob = JumpProblem(oprob, agg, jumps...; save_positions=save_positions, rng=rng) + return jprob +end + +function expected_stats_hawkes_problem(p, tspan) + T = tspan[end] - tspan[1] + λ, α, β = p + γ = β - α + κ = β / γ + Eλ = λ * κ + # Equation 21 + # J. Da Fonseca and R. Zaatour, + # “Hawkes Process: Fast Calibration, Application to Trade Clustering and Diffusive Limit.” + # Rochester, NY, Aug. 04, 2013. doi: 10.2139/ssrn.2294112. + Varλ = (Eλ*(T*κ^2 + (1-κ^2)*(1-exp(-T*γ))/γ))/(T^2) + return Eλ, Varλ +end + +u0 = [0.] +p = (0.5, 0.5, 2.0) +tspan = (0., 200.) +g = [[1]] +h = [Float64[]] + +Eλ, Varλ = expected_stats_hawkes_problem(p, tspan) + +algs = (Direct(), QueueMethod()) +Nsims = 250 + +for alg in algs + jump_prob = hawkes_problem(p, alg; u=u0, tspan=tspan, g=g, h=h) + if typeof(alg) <: QueueMethod + stepper = SSAStepper() + else + stepper = Tsit5() + end + sols = Vector{ODESolution}(undef, Nsims) + for n in 1:Nsims + reset_history!(h) + sols[n] = solve(jump_prob, stepper) + end + if typeof(alg) <: QueueMethod + λs = permutedims(mapreduce((sol) -> empirical_rate(sol), hcat, sols)) + else + cols = length(sols[1].u[1].u) + λs = permutedims(mapreduce((sol) -> empirical_rate(sol), hcat, sols))[:, 1:cols] + end + @test isapprox(mean(λs), Eλ; atol=0.01) + @test isapprox(var(λs), Varλ; atol=0.001) +end + diff --git a/test/runtests.jl b/test/runtests.jl index 1c89f66f1..1b9a7b210 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -21,6 +21,7 @@ using JumpProcesses, DiffEqBase, SafeTestsets @time @safetestset "A + B <--> C" begin include("reversible_binding.jl") end @time @safetestset "Remake tests" begin include("remake_test.jl") end @time @safetestset "Long time accuracy test" begin include("longtimes_test.jl") end + @time @safetestset "Hawkes process" begin include("hawkes_test.jl") end @time @safetestset "Reaction rates" begin include("spatial/reaction_rates.jl") end @time @safetestset "Hop rates" begin include("spatial/hop_rates.jl") end @time @safetestset "Topology" begin include("spatial/topology.jl") end From c7a832c410c7df89730b8dcf355bc3ab8287ebde Mon Sep 17 00:00:00 2001 From: gzagatti Date: Wed, 7 Dec 2022 14:08:54 +0800 Subject: [PATCH 32/72] linting. --- test/hawkes_test.jl | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/test/hawkes_test.jl b/test/hawkes_test.jl index 61c4574e6..0d51cff4d 100644 --- a/test/hawkes_test.jl +++ b/test/hawkes_test.jl @@ -3,9 +3,8 @@ using Test using StableRNGs rng = StableRNG(12345) - function reset_history!(h; start_time = nothing) - @inbounds for i = 1:length(h) + @inbounds for i in 1:length(h) h[i] = eltype(h)[] end nothing @@ -21,8 +20,10 @@ function hawkes_rate(i::Int, g, h) x = zero(typeof(t)) for j in g[i] for _t in reverse(h[j]) - λij = α*exp(-β*(t - _t)) - if λij ≈ 0 break end + λij = α * exp(-β * (t - _t)) + if λij ≈ 0 + break + end x += λij end end @@ -38,25 +39,26 @@ function hawkes_jump(i::Int, g, h) function L(u, p, t) _lrate = lrate(u, p, t) _urate = urate(u, p, t) - return _urate == _lrate ? typemax(t) : 1/(2*_urate) + return _urate == _lrate ? typemax(t) : 1 / (2 * _urate) end function affect!(integrator) push!(h[i], integrator.t) integrator.u[i] += 1 end - return VariableRateJump(rate, affect!; lrate=lrate, urate=urate, L=L) + return VariableRateJump(rate, affect!; lrate = lrate, urate = urate, L = L) end function hawkes_jump(u, g, h) return [hawkes_jump(i, g, h) for i in 1:length(u)] end -function hawkes_problem(p, agg::QueueMethod; u=[0.], tspan=(0., 50.), save_positions=(false, true), - g = [[1]], h = [[]]) +function hawkes_problem(p, agg::QueueMethod; u = [0.0], tspan = (0.0, 50.0), + save_positions = (false, true), + g = [[1]], h = [[]]) dprob = DiscreteProblem(u, tspan, p) jumps = hawkes_jump(u, g, h) jprob = JumpProblem(dprob, agg, jumps...; - dep_graph=g, save_positions=save_positions, rng=rng) + dep_graph = g, save_positions = save_positions, rng = rng) return jprob end @@ -65,11 +67,12 @@ function f!(du, u, p, t) nothing end -function hawkes_problem(p, agg; u=[0.], tspan=(0., 50.), save_positions=(false, true), - g = [[1]], h = [[]]) +function hawkes_problem(p, agg; u = [0.0], tspan = (0.0, 50.0), + save_positions = (false, true), + g = [[1]], h = [[]]) oprob = ODEProblem(f!, u, tspan, p) jumps = hawkes_jump(u, g, h) - jprob = JumpProblem(oprob, agg, jumps...; save_positions=save_positions, rng=rng) + jprob = JumpProblem(oprob, agg, jumps...; save_positions = save_positions, rng = rng) return jprob end @@ -83,13 +86,13 @@ function expected_stats_hawkes_problem(p, tspan) # J. Da Fonseca and R. Zaatour, # “Hawkes Process: Fast Calibration, Application to Trade Clustering and Diffusive Limit.” # Rochester, NY, Aug. 04, 2013. doi: 10.2139/ssrn.2294112. - Varλ = (Eλ*(T*κ^2 + (1-κ^2)*(1-exp(-T*γ))/γ))/(T^2) + Varλ = (Eλ * (T * κ^2 + (1 - κ^2) * (1 - exp(-T * γ)) / γ)) / (T^2) return Eλ, Varλ end -u0 = [0.] +u0 = [0.0] p = (0.5, 0.5, 2.0) -tspan = (0., 200.) +tspan = (0.0, 200.0) g = [[1]] h = [Float64[]] @@ -99,7 +102,7 @@ algs = (Direct(), QueueMethod()) Nsims = 250 for alg in algs - jump_prob = hawkes_problem(p, alg; u=u0, tspan=tspan, g=g, h=h) + jump_prob = hawkes_problem(p, alg; u = u0, tspan = tspan, g = g, h = h) if typeof(alg) <: QueueMethod stepper = SSAStepper() else @@ -116,7 +119,6 @@ for alg in algs cols = length(sols[1].u[1].u) λs = permutedims(mapreduce((sol) -> empirical_rate(sol), hcat, sols))[:, 1:cols] end - @test isapprox(mean(λs), Eλ; atol=0.01) - @test isapprox(var(λs), Varλ; atol=0.001) + @test isapprox(mean(λs), Eλ; atol = 0.01) + @test isapprox(var(λs), Varλ; atol = 0.001) end - From 757436704a28d3b6266d2ce7c10e75d382f2df4b Mon Sep 17 00:00:00 2001 From: gzagatti Date: Wed, 7 Dec 2022 16:02:45 +0800 Subject: [PATCH 33/72] fix documentation. --- docs/src/jump_types.md | 97 +++++++++---------- .../tutorials/discrete_stochastic_example.md | 25 ++--- 2 files changed, 62 insertions(+), 60 deletions(-) diff --git a/docs/src/jump_types.md b/docs/src/jump_types.md index bf6dc566e..9ef06ad5e 100644 --- a/docs/src/jump_types.md +++ b/docs/src/jump_types.md @@ -5,7 +5,7 @@ Jumps (or point) processes are stochastic processes with discrete changes driven by some `rate`. The homogeneous Poisson process is the canonical point process with a constant rate of change. Processes involving multiple jumps are known as -compound jump (or point) processes. +compound jump (or point) processes. A compound Poisson process is a continuous-time Markov Chain where the time to the next jump is exponentially distributed as calculated by the rate. This type @@ -32,32 +32,32 @@ du = f(u,p,t)dt + \sum_{j}g_j(u,t)dW_j(t) + \sum_{i}c_i(u,p,t)dp_i(t) ## Types of Jumps: Regular, Variable, Constant Rate and Mass Action -Exact algorithms tend to describe the realization of each jump chronologically. -In more complex cases, such jumps are conditioned on the history of past events. -Such jumps are usually associated with changes to the state variable `u` which -in turn changes the `rate` of event occurence. These jumps can be specified as -a `ConstantRateJump`, `MassActionJump`, or a `VariableRateJump`. Since exact -methods simulate each and every point, they might face termination issues when +Exact algorithms tend to describe the realization of each jump chronologically. +In more complex cases, such jumps are conditioned on the history of past events. +Such jumps are usually associated with changes to the state variable `u` which +in turn changes the `rate` of event occurence. These jumps can be specified as +a `ConstantRateJump`, `MassActionJump`, or a `VariableRateJump`. Since exact +methods simulate each and every point, they might face termination issues when the `rate` of event occurrence explodes. -Alternatively, inexact methods tend to take small leaps through time so they are -guaranteed to terminate in finite time. These methods can be much faster as they -only simulate the total number of points in each leap interval and thus do not -need to simulate the realization of every single jump. Since inexact methods -trade accuracy for speed, they should be used when a set of jumps do not make -significant changes to the system during the leap interval. A `RegularJump` is -used for inexact algorithms. Note that inexact methods are not always -inaccurate. In the case of homogeneous Poisson processes, they produce accurate -results. However, they can produce less accurate results for more complex -problems, thus it is important to have a good understanding of the problem. As -a rule of thumb, if changes to the state variable `u` during a leap are minimal -compared to size of the system, an inexact method should provide reasonable +Alternatively, inexact methods tend to take small leaps through time so they are +guaranteed to terminate in finite time. These methods can be much faster as they +only simulate the total number of points in each leap interval and thus do not +need to simulate the realization of every single jump. Since inexact methods +trade accuracy for speed, they should be used when a set of jumps do not make +significant changes to the system during the leap interval. A `RegularJump` is +used for inexact algorithms. Note that inexact methods are not always +inaccurate. In the case of homogeneous Poisson processes, they produce accurate +results. However, they can produce less accurate results for more complex +problems, thus it is important to have a good understanding of the problem. As +a rule of thumb, if changes to the state variable `u` during a leap are minimal +compared to size of the system, an inexact method should provide reasonable solutions. We denote a jump as variable if its rate function is dependent on values -which may change between any jump in the system. For instance, when the rate is -a function of time. Variable jumps can be more expensive to simulate because it -is necessary to take into account the dynamics of the rate function when +which may change between any jump in the system. For instance, when the rate is +a function of time. Variable jumps can be more expensive to simulate because it +is necessary to take into account the dynamics of the rate function when simulating the next jump time. A `MassActionJump` is a specialized representation for a collection of constant @@ -67,7 +67,7 @@ will offer improved performance. 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 `VariableRateJump` to handle the +create a number of `ConstantRateJumps` or `VariableRateJump` to handle the non-mass action jumps. `RegularJump`s are optimized for inexact jumping algorithms like tau-leaping and @@ -93,8 +93,8 @@ ConstantRateJump(rate,affect!) The constructor for a `VariableRateJump` is: ```julia -VariableRateJump(rate,affect!; - lrate=nothing, urate=nothing, L=nothing +VariableRateJump(rate,affect!; + lrate=nothing, urate=nothing, L=nothing, idxs=nothing, rootfind=true, save_positions=(true,true), @@ -105,19 +105,19 @@ VariableRateJump(rate,affect!; - `rate(u,p,t)` is a function which calculates the rate given the time and the state. - `affect!(integrator)` is the effect on the equation, using the integrator interface. - When planning to use the `QueueMethod` aggregator, the arguments `lrate`, - `urate` and `L` are required. They consist of three functions: `lrate(u, p, - t)` computes the lower bound of the intensity rate in the interval `t` to `t - + L` given state `u` and parameters `p`; `urate(u, p, t)` computes the upper + `urate` and `L` are required. They consist of three functions: `lrate(u, p, t)` + computes the lower bound of the intensity rate in the interval `t` to `t + L` + given state `u` and parameters `p`; `urate(u, p, t)` computes the upper bound of the intensity rate; and `L(u, p, t)` computes the interval length - for which the rate is bounded between `lrate` and `urate`. -- It is only possible to solve a `VariableRateJump` with `SSAStepper` when using + for which the rate is bounded between `lrate` and `urate`. +- It is only possible to solve a `VariableRateJump` with `SSAStepper` when using the `QueueMethod` aggregator. -- When using a different aggregator than `QueueMethod`, there is no need to - define `lrate`, `urate` and `L`. Note that in this case, the problem can only - be solved with continuous integration. Internally, `VariableRateJump` is - transformed into a `ContinuousCallback`. The `rate(u, p, t)` is used to +- When using a different aggregator than `QueueMethod`, there is no need to + define `lrate`, `urate` and `L`. Note that in this case, the problem can only + be solved with continuous integration. Internally, `VariableRateJump` is + transformed into a `ContinuousCallback`. The `rate(u, p, t)` is used to construct the `condition` function that triggers the callback. - + #### Defining a Mass Action Jump The constructor for a `MassActionJump` is: @@ -286,16 +286,16 @@ JumpProblem(prob,DirectCR(),jump1,jump2; dep_graph=your_dependency_graph) ``` For systems with only `MassActionJump`s, or those generated from a [Catalyst](https://docs.sciml.ai/Catalyst/stable/) `reaction_network`, this graph -will be auto-generated. Otherwise, you must construct the dependency graph -whenever using `ConstantRateJump`s and/or `VariableRateJump`s. This is also the -case when combining `MassActionJump` with `ConstantRateJump`s and/or -`VariableRateJump`s. +will be auto-generated. Otherwise, you must construct the dependency graph +whenever using `ConstantRateJump`s and/or `VariableRateJump`s. This is also the +case when combining `MassActionJump` with `ConstantRateJump`s and/or +`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 `VariableRateJump`s (with the latter internally ordered -in the same order they were passed in). Thus, keep that in mind when combining +in the same order they were passed in). Thus, keep that in mind when combining `MassActionJump`s with other types of jumps. `RSSA` and `RSSACR` require two different types of dependency graphs, passed @@ -314,14 +314,14 @@ 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 + action reactions with constant rate between jumps. This will generally offer the fastest performance. -- Use `ConstantRateJump`s for any remaining jumps with constant rate between +- Use `ConstantRateJump`s for any remaining jumps with constant rate between jumps. -- Use `VariableRateJump`s for any remaining jumps with variable rate between - jumps. You will need to define the lower and upper rate boundaries as well as - the interval for which the boundaries apply. The tighter the boundaries and - the easier to compute, the faster the resulting algorithm will be. +- Use `VariableRateJump`s for any remaining jumps with variable rate between + jumps. You will need to define the lower and upper rate boundaries as well as + the interval for which the boundaries apply. The tighter the boundaries and + the easier to compute, the faster the resulting algorithm will be. - 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 than `Direct`. @@ -330,12 +330,11 @@ For representing and aggregating jumps `NRM` often have the best performance. - For very large networks, with many updates per jump, `RSSA` and `RSSACR` will often substantially outperform the other methods. -- For systems with `VariableRateJump`, only the `QueueMethod` aggregator is +- For systems with `VariableRateJump`, only the `QueueMethod` aggregator is supported. - The `SSAStepper()` can be used with `VariableRateJump`s that modify the state of differential equations. However, it is not possible to use `SSAStepper()` - to solve `VariableRateJump` that are combined with differential equations that - modify the rate of the jumps. + to solve `VariableRateJump`s whose rate depends on a continuous variable. In general, for systems with sparse dependency graphs if `Direct` is slow, one of `SortingDirect`, `RSSA` or `RSSACR` will usually offer substantially better diff --git a/docs/src/tutorials/discrete_stochastic_example.md b/docs/src/tutorials/discrete_stochastic_example.md index 50afaeec3..ec8fb4196 100644 --- a/docs/src/tutorials/discrete_stochastic_example.md +++ b/docs/src/tutorials/discrete_stochastic_example.md @@ -418,23 +418,24 @@ L(u, p, t) = 1 / (2*urate(u, p, t)) function affect3!(integrator) integrator.u[1] -= 1 # S -> S - 1 integrator.u[2] += 1 # I -> I + 1 - push!(I, integrator.t) + push!(H, integrator.t) nothing end -jump3 = VariableRateJump(rate3, affect3; lrate=lrate, urate=urate, L=L) +jump3 = VariableRateJump(rate3, affect3!; lrate=lrate, urate=urate, L=L) ``` Next, we redefine the recovery jump's `affect!` such that a random infection is removed from `H` for every recovery. ```@example tut2 +rate4(u, p, t) = p[2] * u[2] # ν*I function affect4!(integrator) integrator.u[2] -= 1 integrator.u[3] += 1 - length(I) > 0 && deleteat!(H, rand(1:length(I))) + length(H) > 0 && deleteat!(H, rand(1:length(H))) nothing end -jump4 = ConstantRateJump(rate2, affect4!) +jump4 = ConstantRateJump(rate4, affect4!) ``` With the jumps defined, we can build @@ -444,6 +445,7 @@ a [`DiscreteProblem`](https://docs.sciml.ai/DiffEqDocs/stable/types/discrete_typ aggregator. In this case, both processes mutually affect each other. ```@example tut2 +prob = DiscreteProblem(u₀, tspan, p1) jump_prob = JumpProblem(prob, QueueMethod(), jump3, jump4; dep_graph=[[1,2], [1,2]]) ``` @@ -451,7 +453,7 @@ We now have a problem that can be solved with `SSAStepper` to handle time-stepping the `QueueMethod` aggregator from jump to jump: ```@example tut2 -sol = solve(jump_pro, SSAStepper()) +sol = solve(jump_prob, SSAStepper()) plot(sol, label=["S(t)", "I(t)", "R(t)"]) ``` @@ -491,6 +493,7 @@ for other this is a tuple `(bool1, bool2)` which sets whether to save before or after a jump. If we do not want to save at every jump, we would thus pass: ```@example tut2 +prob = DiscreteProblem(u₀, tspan, p) jump_prob = JumpProblem(prob, Direct(), jump, jump2; save_positions = (false, false)) ``` Now the saving controls associated with the integrator should specified, see the @@ -656,21 +659,21 @@ differential equation. To continue our example, let there be a new reaction with rate depending on `u[4]` of the form ``u_4 \to u_4 + \textrm{I}``, with a rate constant of `1e-2`: ```@example tut2 -rate3(u, p, t) = 1e-2 * u[4] -function affect3!(integrator) +rate5(u, p, t) = 1e-2 * u[4] +function affect5!(integrator) integrator.u[2] += 1 # I -> I + 1 nothing end -jump3 = VariableRateJump(rate3, affect3!) +jump5 = VariableRateJump(rate5, affect5!) ``` -Notice, since `rate3` depends on a variable that evolves continuously, and hence +Notice, since `rate5` depends on a variable that evolves continuously, and hence is not constant between jumps, *we must use a `VariableRateJump`*. Solving the equation is exactly the same: ```@example tut2 u₀ = [999.0, 10.0, 0.0, 1.0] prob = ODEProblem(f, u₀, tspan, p) -jump_prob = JumpProblem(prob, Direct(), jump, jump2, jump3) +jump_prob = JumpProblem(prob, Direct(), jump, jump2, jump5) sol = solve(jump_prob, Tsit5()) plot(sol; label=["S(t)" "I(t)" "R(t)" "u₄(t)"]) ``` @@ -686,7 +689,7 @@ function g(du, u, p, t) du[4] = 0.1u[4] end prob = SDEProblem(f, g, [999.0, 1.0, 0.0, 1.0], (0.0, 250.0), p) -jump_prob = JumpProblem(prob, Direct(), jump, jump2, jump3) +jump_prob = JumpProblem(prob, Direct(), jump, jump2, jump5) sol = solve(jump_prob, SRIW1()) plot(sol; label=["S(t)" "I(t)" "R(t)" "u₄(t)"]) ``` From efdb3a3b9eec49302a44f9d5c370e66cd18b9245 Mon Sep 17 00:00:00 2001 From: gzagatti Date: Wed, 7 Dec 2022 16:03:11 +0800 Subject: [PATCH 34/72] adds source to documentation. --- src/aggregators/aggregators.jl | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/aggregators/aggregators.jl b/src/aggregators/aggregators.jl index bca6a6569..42fa7722b 100644 --- a/src/aggregators/aggregators.jl +++ b/src/aggregators/aggregators.jl @@ -145,7 +145,14 @@ doi: 10.1063/1.4928635 struct DirectCRDirect <: AbstractAggregatorAlgorithm end """ -The Queue Method. This method handles variable intensity rates. +The Queue Method. This method handles variable intensity rates with +user-defined bounds and interdependent processes. It reduces to NRM when rates +are constant. + +COEVOLVE: a joint point process model for information diffusion and network +evolution, M. Farajtabar, Y. Wang, M. Gomez-Rodriguez, S. Li, H. Zha, and L. +Song, Journal of Machine Learning Research 18(1), 1305–1353 (2017). doi: +10.5555/3122009.3122050. """ struct QueueMethod <: AbstractAggregatorAlgorithm end From 82aff42251296e5d214852de153129a9ea90b550 Mon Sep 17 00:00:00 2001 From: gzagatti Date: Wed, 7 Dec 2022 16:03:23 +0800 Subject: [PATCH 35/72] fix problem initialization. --- src/problem.jl | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/problem.jl b/src/problem.jl index 09bb6bb76..758341633 100644 --- a/src/problem.jl +++ b/src/problem.jl @@ -229,8 +229,6 @@ function JumpProblem(prob, aggregator::AbstractAggregatorAlgorithm, jumps::JumpS if length(jumps.variable_jumps) > 0 && !is_spatial(aggregator) new_prob = extend_problem(prob, jumps; rng = rng) - disc_agg = nothing - constant_jump_callback = CallbackSet() variable_jump_callback = build_variable_callback(CallbackSet(), 0, jumps.variable_jumps...; rng = rng) From bd67a7f6b20fb0545c6156cd1924be9372bae2c4 Mon Sep 17 00:00:00 2001 From: gzagatti Date: Thu, 8 Dec 2022 10:38:06 +0800 Subject: [PATCH 36/72] renames QueueMethod to Coevolve; fix documentation references. --- docs/src/api.md | 2 +- docs/src/jump_types.md | 89 ++++++++++----- .../tutorials/discrete_stochastic_example.md | 20 ++-- docs/src/tutorials/jump_diffusion.md | 2 +- docs/src/tutorials/simple_poisson_process.md | 4 +- src/JumpProcesses.jl | 2 +- src/aggregators/aggregators.jl | 105 +++++++++--------- src/aggregators/queue.jl | 26 ++--- src/jumps.jl | 14 +-- src/problem.jl | 4 +- test/hawkes_test.jl | 8 +- 11 files changed, 156 insertions(+), 120 deletions(-) diff --git a/docs/src/api.md b/docs/src/api.md index 84db2d6ea..03361680f 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -31,7 +31,7 @@ RDirect RSSA RSSACR SortingDirect -QueueMethod +Coevolve ``` # Private API Functions diff --git a/docs/src/jump_types.md b/docs/src/jump_types.md index 9ef06ad5e..1a049de11 100644 --- a/docs/src/jump_types.md +++ b/docs/src/jump_types.md @@ -104,15 +104,15 @@ VariableRateJump(rate,affect!; - `rate(u,p,t)` is a function which calculates the rate given the time and the state. - `affect!(integrator)` is the effect on the equation, using the integrator interface. -- When planning to use the `QueueMethod` aggregator, the arguments `lrate`, +- When planning to use the `Coevolve` aggregator, the arguments `lrate`, `urate` and `L` are required. They consist of three functions: `lrate(u, p, t)` computes the lower bound of the intensity rate in the interval `t` to `t + L` given state `u` and parameters `p`; `urate(u, p, t)` computes the upper bound of the intensity rate; and `L(u, p, t)` computes the interval length for which the rate is bounded between `lrate` and `urate`. - It is only possible to solve a `VariableRateJump` with `SSAStepper` when using - the `QueueMethod` aggregator. -- When using a different aggregator than `QueueMethod`, there is no need to + the `Coevolve` aggregator. +- When using a different aggregator than `Coevolve`, there is no need to define `lrate`, `urate` and `L`. Note that in this case, the problem can only be solved with continuous integration. Internally, `VariableRateJump` is transformed into a `ContinuousCallback`. The `rate(u, p, t)` is used to @@ -237,34 +237,34 @@ with `RegularJumps` which are used for inexact methods. The current aggregators are: -- `Direct`: the Gillespie Direct method SSA. -- `RDirect`: A variant of Gillespie's Direct method that uses rejection to - sample the next reaction. -- *`DirectCR`*: The Composition-Rejection Direct method of Slepoy et al. For - large networks and linear chain-type networks it will often give better - performance than `Direct`. (Requires dependency graph, see below.) -- `DirectFW`: the Gillespie Direct method SSA with `FunctionWrappers`. This +- `Direct`: The Gillespie Direct method SSA [1]. +- `DirectFW`: the Gillespie Direct method SSA [1] with `FunctionWrappers`. This aggregator uses a different internal storage format for collections of `ConstantRateJumps`. -- `FRM`: the Gillespie first reaction method SSA. `Direct` should generally - offer better performance and be preferred to `FRM`. -- `FRMFW`: the Gillespie first reaction method SSA with `FunctionWrappers`. -- *`NRM`*: The Gibson-Bruck Next Reaction Method. For some reaction network - structures this may offer better performance than `Direct` (for example, - large, linear chains of reactions). (Requires dependency graph, see below.) -- *`RSSA`*: The Rejection SSA (RSSA) method of Thanh et al. With `RSSACR`, for +- *`DirectCR`*: The Composition-Rejection Direct method of Slepoy et al [2]. For + large networks and linear chain-type networks it will often give better + performance than `Direct`. (Requires dependency graph, see below.) +- *`SortingDirect`*: The Sorting Direct Method of McCollum et al [3]. It will + usually offer performance as good as `Direct`, and for some systems can offer + substantially better performance. (Requires dependency graph, see below.) +- *`RSSA`*: The Rejection SSA (RSSA) method of Thanh et al [4,5]. With `RSSACR`, for very large reaction networks it often offers the best performance of all methods. (Requires dependency graph, see below.) - *`RSSACR`*: The Rejection SSA (RSSA) with Composition-Rejection method of - Thanh et al. With `RSSA`, for very large reaction networks it often offers the + Thanh et al [6]. With `RSSA`, for very large reaction networks it often offers the best performance of all methods. (Requires dependency graph, see below.) -- *`SortingDirect`*: The Sorting Direct Method of McCollum et al. It will - usually offer performance as good as `Direct`, and for some systems can offer - substantially better performance. (Requires dependency graph, see below.) -- *`QueueMethod`*: The queueing method. This is a modification of Ogata's - algorihm for simulating any compound point process that evolves through time. - This is the only aggregator that handles `VariableRateJump`. If rates do not - change between jump events (i.e. `ConsantRateJump` or `MassActionJump`) this +- `RDirect`: A variant of Gillespie's Direct method [1] that uses rejection to + sample the next reaction. +- `FRM`: The Gillespie first reaction method SSA [1]. `Direct` should generally + offer better performance and be preferred to `FRM`. +- `FRMFW`: The Gillespie first reaction method SSA [1] with `FunctionWrappers`. +- *`NRM`*: The Gibson-Bruck Next Reaction Method [7]. For some reaction network + structures this may offer better performance than `Direct` (for example, + large, linear chains of reactions). (Requires dependency graph, see below.) +- *`Coevolve`*: An adaptation of the COEVOLVE algorithm of Farajtabar et al [8]. + for simulating any compound point process that evolves through time. This is + the only aggregator that handles `VariableRateJump`. If rates do not change + between jump events (i.e. `ConsantRateJump` or `MassActionJump`) this aggregator is very similar to `NRM`. (Requires dependency graph, see below.) To pass the aggregator, pass the instantiation of the type. For example: @@ -276,9 +276,44 @@ JumpProblem(prob,Direct(),jump1,jump2) will build a problem where the constant rate jumps are solved using Gillespie's Direct SSA method. +[1] Daniel 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. + +[2] A. 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. + +[3] J. 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. + +[4] V. 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. + +[5] V. 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. + +[6] V. 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. + +[7] M. 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. + +[8] M. 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. + ## Jump Aggregators Requiring Dependency Graphs Italicized constant rate jump aggregators require the user to pass a dependency -graph to `JumpProblem`. `DirectCR`, `NRM`, `SortingDirect` and `QueueMethod` +graph to `JumpProblem`. `DirectCR`, `NRM`, `SortingDirect` and `Coevolve` require a jump-jump dependency graph, passed through the named parameter `dep_graph`. i.e. ```julia @@ -330,7 +365,7 @@ For representing and aggregating jumps `NRM` often have the best performance. - For very large networks, with many updates per jump, `RSSA` and `RSSACR` will often substantially outperform the other methods. -- For systems with `VariableRateJump`, only the `QueueMethod` aggregator is +- For systems with `VariableRateJump`, only the `Coevolve` aggregator is supported. - The `SSAStepper()` can be used with `VariableRateJump`s that modify the state of differential equations. However, it is not possible to use `SSAStepper()` diff --git a/docs/src/tutorials/discrete_stochastic_example.md b/docs/src/tutorials/discrete_stochastic_example.md index ec8fb4196..61f451072 100644 --- a/docs/src/tutorials/discrete_stochastic_example.md +++ b/docs/src/tutorials/discrete_stochastic_example.md @@ -255,14 +255,14 @@ In general | [`MassActionJump`](@ref MassActionJumpSect) | Fastest | Restrictive rates/affects | | [`ConstantRateJump`](@ref ConstantRateJumpSect) | Somewhat Slower | Rates must be constant between jumps | -| [`VariableRateJump` with `QueueMethod` aggregator](@ref VariableRateJumpQueueMethodSect) | Somewhat Slower | Rates can be a function of time, but not of ODE variables | +| [`VariableRateJump` with `Coevolve` aggregator](@ref VariableRateJumpCoevolveSect) | Somewhat Slower | Rates can be a function of time, but not of ODE variables | | [`VariableRateJump`](@ref VariableRateJumpSect) | Slowest | Completely general | It is recommended to try to encode jumps using the most performant option that supports the desired generality of the underlying `rate` and `affect!` functions. Below we describe the different jump types, and show how the SIR model can be formulated using first `ConstantRateJump`s, `VariableRateJump`s -with `QueueMethod` aggregator and then `MassActionJump`s. Completely general +with `Coevolve` aggregator and then `MassActionJump`s. Completely general models that use `VariableRateJump`s with over an `ODEProblem` are considered later. @@ -380,7 +380,7 @@ In summary, if a particular jump process has a rate function that depends explicitly or implicitly on a continuously changing quantity, you need to use a [`VariableRateJump`](@ref). -## [Defining the Jumps Directly: `VariableRateJump`](@id VariableRateJumpQueueMethodSect) +## [Defining the Jumps Directly: `VariableRateJump`](@id VariableRateJumpCoevolveSect) Now, let's assume that the infection rate is decreasing over time. That is, once individuals gets infected they reaches peak infectivity. The force of infection @@ -405,7 +405,7 @@ p1 = (β1, ν, α, γ) ``` We define a vector `H` to hold the timestamp of active infections. Then, we -define infection as a `VariableRateJump`. To use the `QueueMethod` aggregator, +define infection as a `VariableRateJump`. To use the `Coevolve` aggregator, we need to specify the lower- and upper-bounds of the rate which should be valid from the time they are computed `t` until `t + L(u, p, t)`. @@ -441,16 +441,16 @@ jump4 = ConstantRateJump(rate4, affect4!) With the jumps defined, we can build a [`DiscreteProblem`](https://docs.sciml.ai/DiffEqDocs/stable/types/discrete_types/). `VariableRateJump`s over a `DiscreteProblem` can only be solved with the -`QueueMethod` aggregator. We need to specify a depedency graph to use this +`Coevolve` aggregator. We need to specify a depedency graph to use this aggregator. In this case, both processes mutually affect each other. ```@example tut2 prob = DiscreteProblem(u₀, tspan, p1) -jump_prob = JumpProblem(prob, QueueMethod(), jump3, jump4; dep_graph=[[1,2], [1,2]]) +jump_prob = JumpProblem(prob, Coevolve(), jump3, jump4; dep_graph=[[1,2], [1,2]]) ``` We now have a problem that can be solved with `SSAStepper` to handle -time-stepping the `QueueMethod` aggregator from jump to jump: +time-stepping the `Coevolve` aggregator from jump to jump: ```@example tut2 sol = solve(jump_prob, SSAStepper()) @@ -621,7 +621,7 @@ plot(sol; label=["S(t)" "I(t)" "R(t)"]) ``` Note that, we can also combine `MassActionJump` with `VariableRateJump` when -using the `QueueMethod` aggregator in the same manner. +using the `Coevolve` aggregator in the same manner. ## Adding Jumps to a Differential Equation If we instead used some form of differential equation instead of a @@ -648,10 +648,10 @@ sol = solve(jump_prob, Tsit5()) plot(sol; label=["S(t)" "I(t)" "R(t)" "u₄(t)"]) ``` -Note that when using `VariableRateJump`s with the `QueueMethod` aggregator, the +Note that when using `VariableRateJump`s with the `Coevolve` aggregator, the ODE problem should not modify the rates of any jump. However, the opposite where the jumps modify the ODE variables is allowed. In these cases, you should -observe an improved performance when using the `QueueMethod`. +observe an improved performance when using the `Coevolve`. ## [Adding a VariableRateJump that Depends on a Continuous Variable](@id VariableRateJumpSect) Now let's consider adding a reaction whose rate changes continuously with the diff --git a/docs/src/tutorials/jump_diffusion.md b/docs/src/tutorials/jump_diffusion.md index c5ecf6238..ff459720c 100644 --- a/docs/src/tutorials/jump_diffusion.md +++ b/docs/src/tutorials/jump_diffusion.md @@ -121,7 +121,7 @@ In this way we have solve a mixed jump-ODE, i.e. a piecewise deterministic Markov process. Note that in this case, the rate of the `VariableRateJump`s depend on a variable -that is driven by an `ODEProblem`, thus we cannot use the `QueueMethod` to solve +that is driven by an `ODEProblem`, thus we cannot use the `Coevolve` to solve the jump problem. diff --git a/docs/src/tutorials/simple_poisson_process.md b/docs/src/tutorials/simple_poisson_process.md index 924383500..22b460d32 100644 --- a/docs/src/tutorials/simple_poisson_process.md +++ b/docs/src/tutorials/simple_poisson_process.md @@ -212,9 +212,9 @@ dep_graph = [[2], []] We can then construct the corresponding problem, passing both jumps to `JumpProblem` as well as the dependency graph. Since we are dealing with -a `VariableRateJump` we must use the `QueueMethod` aggregator. +a `VariableRateJump` we must use the `Coevolve` aggregator. ```@example tut1 -jprob = JumpProblem(dprob, QueueMethod(), vrj1, deathcrj; dep_graph=dep_graph) +jprob = JumpProblem(dprob, Coevolve(), vrj1, deathcrj; dep_graph=dep_graph) sol = solve(jprob, SSAStepper()) plot(sol, label=["N(t)" "D(t)"], xlabel="t", legend=:topleft) ``` diff --git a/src/JumpProcesses.jl b/src/JumpProcesses.jl index 4001b3127..039348f67 100644 --- a/src/JumpProcesses.jl +++ b/src/JumpProcesses.jl @@ -84,7 +84,7 @@ export Direct, DirectFW, SortingDirect, DirectCR export BracketData, RSSA export FRM, FRMFW, NRM export RSSACR, RDirect -export QueueMethod +export Coevolve export get_num_majumps, needs_depgraph, needs_vartojumps_map diff --git a/src/aggregators/aggregators.jl b/src/aggregators/aggregators.jl index 42fa7722b..0fc117024 100644 --- a/src/aggregators/aggregators.jl +++ b/src/aggregators/aggregators.jl @@ -8,9 +8,9 @@ tuples. Fastest for a small (total) number of `ConstantRateJump`s or `MassActionJump`s (~10). For larger numbers of possible jumps use other methods. -Gillespie, Daniel T. (1976). A General Method for Numerically Simulating the -Stochastic Time Evolution of Coupled Chemical Reactions. Journal of -Computational Physics. 22 (4): 403–434. doi:10.1016/0021-9991(76)90041-3. +Daniel 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. """ struct Direct <: AbstractAggregatorAlgorithm end @@ -21,9 +21,9 @@ numbers of `ConstantRateJump`s. However, for such large numbers of jump different classes of aggregators are usually much more performant (i.e. `SortingDirect`, `DirectCR`, `RSSA` or `RSSACR`). -Gillespie, Daniel T. (1976). A General Method for Numerically Simulating the -Stochastic Time Evolution of Coupled Chemical Reactions. Journal of -Computational Physics. 22 (4): 403–434. doi:10.1016/0021-9991(76)90041-3. +Daniel 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. """ struct DirectFW <: AbstractAggregatorAlgorithm end @@ -33,13 +33,13 @@ for systems with large numbers of jumps with special structure (for example a linear chain of reactions, or jumps corresponding to particles hopping on a grid or graph). -- A. 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 +A. 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. -- S. Mauch and M. Stalzer, Efficient formulations for exact stochastic - simulation of chemical systems, ACM Transactions on Computational Biology and - Bioinformatics, 8 (1), 27-35 (2010). doi:10.1109/TCBB.2009.47 +S. Mauch and M. Stalzer, Efficient formulations for exact stochastic +simulation of chemical systems, ACM Transactions on Computational Biology and +Bioinformatics, 8 (1), 27-35 (2010). doi:10.1109/TCBB.2009.47. """ struct DirectCR <: AbstractAggregatorAlgorithm end @@ -49,9 +49,9 @@ sized systems (tens of jumps), or systems where a few jumps occur much more frequently than others. J. 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 +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. """ struct SortingDirect <: AbstractAggregatorAlgorithm end @@ -59,13 +59,13 @@ struct SortingDirect <: AbstractAggregatorAlgorithm end The Rejection SSA method. One of the best methods for systems with hundreds to many thousands of jumps (along with `RSSACR`) and sparse dependency graphs. -- V. 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 +V. 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 -- V. 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 +V. 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. """ struct RSSA <: AbstractAggregatorAlgorithm end @@ -73,15 +73,15 @@ struct RSSA <: AbstractAggregatorAlgorithm end The Rejection SSA Composition-Rejection method. Often the best performer for systems with tens of thousands of jumps and sparse depedency graphs. -V. 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, Vol. 14, No. 3, -657-667 (2017). +V. 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. """ struct RSSACR <: AbstractAggregatorAlgorithm end """ -A rejection-based direct method. +A rejection-based direct method. """ struct RDirect <: AbstractAggregatorAlgorithm end @@ -91,9 +91,9 @@ struct RDirect <: AbstractAggregatorAlgorithm end Gillespie's First Reaction Method. Should not be used for practical applications due to slow performance relative to all other methods. -Gillespie, Daniel T. (1976). A General Method for Numerically Simulating the -Stochastic Time Evolution of Coupled Chemical Reactions. Journal of -Computational Physics. 22 (4): 403–434. doi:10.1016/0021-9991(76)90041-3. +Daniel 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. """ struct FRM <: AbstractAggregatorAlgorithm end @@ -102,9 +102,9 @@ Gillespie's First Reaction Method with `FunctionWrappers` for handling `ConstantRateJump`s. Should not be used for practical applications due to slow performance relative to all other methods. -Gillespie, Daniel T. (1976). A General Method for Numerically Simulating the -Stochastic Time Evolution of Coupled Chemical Reactions. Journal of -Computational Physics. 22 (4): 403–434. doi:10.1016/0021-9991(76)90041-3. +Daniel 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. """ struct FRMFW <: AbstractAggregatorAlgorithm end @@ -115,10 +115,23 @@ one of `DirectCR`, `RSSA`, or `RSSACR` for such systems. M. 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 +104 (9), 1876-1889 (2000). doi:10.1021/jp993732q. """ struct NRM <: AbstractAggregatorAlgorithm end +""" +An adaptaton of the COEVOLVE algorithm for simulating any compound jump process +that evolves through time. This method handles variable intensity rates with +user-defined bounds and inter-dependent processes. It reduces to NRM when rates +are constant. + +M. 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. +""" +struct Coevolve <: AbstractAggregatorAlgorithm end + # spatial methods """ @@ -128,8 +141,8 @@ determine where on the grid/graph the next jump occurs, and then the `Direct` method to determine which jump at the given location occurs. Elf, Johan and Ehrenberg, M, Spontaneous separation of bi-stable biochemical -systems into spatial domains of opposite phases,Systems Biology, 2004 vol. 1(2) -pp. 230-236. doi:10.1049/sb:20045021 +systems into spatial domains of opposite phases,Systems Biology, 1(2), 230-236 +(2004). doi:10.1049/sb:20045021. """ struct NSM <: AbstractAggregatorAlgorithm end @@ -138,26 +151,14 @@ The Direct Composition-Rejection Direct method. Uses the `DirectCR` method to determine where on the grid/graph a jump occurs, and the `Direct` method to determine which jump occurs at the sampled location. -Constant-complexity stochastic simulation algorithm with optimal binning, Kevin -R. Sanft and Hans G. Othmer, Journal of Chemical Physics 143, 074108 (2015); -doi: 10.1063/1.4928635 +Kevin R. Sanft and Hans G. Othmer, Constant-complexity stochastic simulation +algorithm with optimal binning, Journal of Chemical Physics 143, 074108 +(2015). doi: 10.1063/1.4928635. """ struct DirectCRDirect <: AbstractAggregatorAlgorithm end -""" -The Queue Method. This method handles variable intensity rates with -user-defined bounds and interdependent processes. It reduces to NRM when rates -are constant. - -COEVOLVE: a joint point process model for information diffusion and network -evolution, M. Farajtabar, Y. Wang, M. Gomez-Rodriguez, S. Li, H. Zha, and L. -Song, Journal of Machine Learning Research 18(1), 1305–1353 (2017). doi: -10.5555/3122009.3122050. -""" -struct QueueMethod <: AbstractAggregatorAlgorithm end - const JUMP_AGGREGATORS = (Direct(), DirectFW(), DirectCR(), SortingDirect(), RSSA(), FRM(), - FRMFW(), NRM(), RSSACR(), RDirect(), QueueMethod()) + FRMFW(), NRM(), RSSACR(), RDirect(), Coevolve()) # For JumpProblem construction without an aggregator struct NullAggregator <: AbstractAggregatorAlgorithm end @@ -168,7 +169,7 @@ needs_depgraph(aggregator::DirectCR) = true needs_depgraph(aggregator::SortingDirect) = true needs_depgraph(aggregator::NRM) = true needs_depgraph(aggregator::RDirect) = true -needs_depgraph(aggregator::QueueMethod) = true +needs_depgraph(aggregator::Coevolve) = true # true if aggregator requires a map from solution variable to dependent jumps. # It is implicitly assumed these aggregators also require the reverse map, from diff --git a/src/aggregators/queue.jl b/src/aggregators/queue.jl index e72fe3526..45b8abaa0 100644 --- a/src/aggregators/queue.jl +++ b/src/aggregators/queue.jl @@ -1,7 +1,7 @@ """ Queue method. This method handles variable intensity rates. """ -mutable struct QueueMethodJumpAggregation{T, S, F1, F2, RNG, GR, PQ} <: +mutable struct CoevolveJumpAggregation{T, S, F1, F2, RNG, GR, PQ} <: AbstractSSAJumpAggregator next_jump::Int # the next jump to execute prev_jump::Int # the previous jump that was executed @@ -21,7 +21,7 @@ mutable struct QueueMethodJumpAggregation{T, S, F1, F2, RNG, GR, PQ} <: Ls::F1 # vector of interval length functions end -function QueueMethodJumpAggregation(nj::Int, njt::T, et::T, crs::Nothing, sr::Nothing, +function CoevolveJumpAggregation(nj::Int, njt::T, et::T, crs::Nothing, sr::Nothing, maj::S, rs::F1, affs!::F2, sps::Tuple{Bool, Bool}, rng::RNG; u::U, dep_graph = nothing, @@ -46,7 +46,7 @@ function QueueMethodJumpAggregation(nj::Int, njt::T, et::T, crs::Nothing, sr::No end pq = MutableBinaryMinHeap{T}() - QueueMethodJumpAggregation{T, S, F1, F2, RNG, typeof(dg), + CoevolveJumpAggregation{T, S, F1, F2, RNG, typeof(dg), typeof(pq) }(nj, nj, njt, et, @@ -59,7 +59,7 @@ function QueueMethodJumpAggregation(nj::Int, njt::T, et::T, crs::Nothing, sr::No end # creating the JumpAggregation structure (tuple-based variable jumps) -function aggregate(aggregator::QueueMethod, u, p, t, end_time, variable_jumps, +function aggregate(aggregator::Coevolve, u, p, t, end_time, variable_jumps, ma_jumps, save_positions, rng; dep_graph = nothing, kwargs...) @@ -84,7 +84,7 @@ function aggregate(aggregator::QueueMethod, u, p, t, end_time, variable_jumps, sum_rate = nothing next_jump = 0 next_jump_time = typemax(t) - QueueMethodJumpAggregation(next_jump, next_jump_time, end_time, cur_rates, sum_rate, + CoevolveJumpAggregation(next_jump, next_jump_time, end_time, cur_rates, sum_rate, ma_jumps, rates, affects!, save_positions, rng; u = u, dep_graph = dep_graph, @@ -92,7 +92,7 @@ function aggregate(aggregator::QueueMethod, u, p, t, end_time, variable_jumps, end # set up a new simulation and calculate the first jump / jump time -function initialize!(p::QueueMethodJumpAggregation, integrator, u, params, t) +function initialize!(p::CoevolveJumpAggregation, integrator, u, params, t) p.end_time = integrator.sol.prob.tspan[2] fill_rates_and_get_times!(p, u, params, t) generate_jumps!(p, integrator, u, params, t) @@ -100,7 +100,7 @@ function initialize!(p::QueueMethodJumpAggregation, integrator, u, params, t) end # execute one jump, changing the system state -function execute_jumps!(p::QueueMethodJumpAggregation, integrator, u, params, t) +function execute_jumps!(p::CoevolveJumpAggregation, integrator, u, params, t) # execute jump u = update_state!(p, integrator, u, params, t) # update current jump rates and times @@ -109,12 +109,12 @@ function execute_jumps!(p::QueueMethodJumpAggregation, integrator, u, params, t) end # calculate the next jump / jump time -function generate_jumps!(p::QueueMethodJumpAggregation, integrator, u, params, t) +function generate_jumps!(p::CoevolveJumpAggregation, integrator, u, params, t) p.next_jump_time, p.next_jump = top_with_handle(p.pq) nothing end -@inline function update_state!(p::QueueMethodJumpAggregation, integrator, u, params, t) +@inline function update_state!(p::CoevolveJumpAggregation, integrator, u, params, t) @unpack ma_jumps, next_jump = p num_majumps = get_num_majumps(ma_jumps) if next_jump <= num_majumps @@ -132,7 +132,7 @@ end end ######################## SSA specific helper routines ######################## -function update_dependent_rates!(p::QueueMethodJumpAggregation, u, params, t) +function update_dependent_rates!(p::CoevolveJumpAggregation, u, params, t) @inbounds deps = p.dep_gr[p.next_jump] @unpack end_time, pq = p for (ix, i) in enumerate(deps) @@ -142,7 +142,7 @@ function update_dependent_rates!(p::QueueMethodJumpAggregation, u, params, t) nothing end -function get_rates(p::QueueMethodJumpAggregation, i, u) +function get_rates(p::CoevolveJumpAggregation, i, u) ma_jumps = p.ma_jumps num_majumps = get_num_majumps(ma_jumps) if i <= num_majumps @@ -158,7 +158,7 @@ function get_rates(p::QueueMethodJumpAggregation, i, u) return rate, lrate, urate, L end -function next_time(p::QueueMethodJumpAggregation{T}, u, params, t, i, tstop::T) where {T} +function next_time(p::CoevolveJumpAggregation{T}, u, params, t, i, tstop::T) where {T} @unpack rng = p rate, lrate, urate, L = get_rates(p, i, u) while t < tstop @@ -194,7 +194,7 @@ function next_time(p::QueueMethodJumpAggregation{T}, u, params, t, i, tstop::T) end # reevaulate all rates, recalculate all jump times, and reinit the priority queue -function fill_rates_and_get_times!(p::QueueMethodJumpAggregation, u, params, t) +function fill_rates_and_get_times!(p::CoevolveJumpAggregation, u, params, t) @unpack end_time = p num_jumps = get_num_majumps(p.ma_jumps) + length(p.rates) jump_times = Vector{eltype(t)}(undef, num_jumps) diff --git a/src/jumps.jl b/src/jumps.jl index d19e5e0ef..1a7677cb9 100644 --- a/src/jumps.jl +++ b/src/jumps.jl @@ -53,8 +53,8 @@ affect!(integrator) = integrator.u[1] -= 1 vrj = VariableRateJump(rate, affect!) ``` -In case we want to use the `QueueMethod` aggregator, we need to pass the rate -boundaries and interval for which the rates apply. The `QueueMethod` aggregator +In case we want to use the `Coevolve` aggregator, we need to pass the rate +boundaries and interval for which the rates apply. The `Coevolve` aggregator allow us to perform discrete steps with `SSAStepper()`. ```julia L(u,p,t) = (1/p[1])*2 @@ -66,9 +66,9 @@ vrj = VariableRateJump(rate, affect!; lrate=lrate, urate=urate, L=L) ``` ## Notes -- When using the `QueueMethod` aggregator, `DiscreteProblem` can be used. +- When using the `Coevolve` aggregator, `DiscreteProblem` can be used. Otherwise, `ODEProblem` or `SDEProblem` must be used to be correctly simulated. -- **When not using the `QueueMethod` aggregator, `VariableRateJump`s result in +- **When not using the `Coevolve` aggregator, `VariableRateJump`s result in `integrator`s storing an effective state type that wraps the main state vector.** See [`ExtendedJumpArray`](@ref) for details on using this object. Note that the presence of *any* `VariableRateJump`s will result in all @@ -86,17 +86,17 @@ struct VariableRateJump{R, F, R2, R3, R4, I, T, T2} <: AbstractJump """Function `affect!(integrator)` that updates the state for one occurrence of the jump given `integrator`.""" affect!::F - """When planning to use the `QueueMethod` aggregator, function `lrate(u, p, + """When planning to use the `Coevolve` aggregator, function `lrate(u, p, t)` that computes the lower bound of the rate in interval `t` to `t + L` at time `t` given state `u`, parameters `p`. This is not required if using another aggregator.""" lrate::R2 - """When planning to use the `QueueMethod` aggregator, function `urate(u, p, + """When planning to use the `Coevolve` aggregator, function `urate(u, p, t)` that computes the upper bound of the rate in interval `t` to `t + L` at time `t` given state `u`, parameters `p`. This is not required if using another aggregator.""" urate::R3 - """When planning to use the `QueueMethod` aggregator, function `L(u, p, + """When planning to use the `Coevolve` aggregator, function `L(u, p, t)` that computes the interval length `L` starting at time `t` given state `u`, parameters `p` for which the rate is bounded between `lrate` and `urate`. This is not required if using another aggregator.""" diff --git a/src/problem.jl b/src/problem.jl index 758341633..62a6f857d 100644 --- a/src/problem.jl +++ b/src/problem.jl @@ -197,8 +197,8 @@ function JumpProblem(prob, aggregator::AbstractAggregatorAlgorithm, jumps::JumpS cont_agg = JumpSet().variable_jumps disc_agg = nothing constant_jump_callback = CallbackSet() - elseif typeof(aggregator) <: QueueMethod - # QueueMethod handles all types of jumps together + elseif typeof(aggregator) <: Coevolve + # Coevolve handles all types of jumps together variable_jumps = VariableRateJump[] if (length(jumps.constant_jumps) == 0) variable_jumps = jumps.variable_jumps diff --git a/test/hawkes_test.jl b/test/hawkes_test.jl index 0d51cff4d..121a4b9f7 100644 --- a/test/hawkes_test.jl +++ b/test/hawkes_test.jl @@ -52,7 +52,7 @@ function hawkes_jump(u, g, h) return [hawkes_jump(i, g, h) for i in 1:length(u)] end -function hawkes_problem(p, agg::QueueMethod; u = [0.0], tspan = (0.0, 50.0), +function hawkes_problem(p, agg::Coevolve; u = [0.0], tspan = (0.0, 50.0), save_positions = (false, true), g = [[1]], h = [[]]) dprob = DiscreteProblem(u, tspan, p) @@ -98,12 +98,12 @@ h = [Float64[]] Eλ, Varλ = expected_stats_hawkes_problem(p, tspan) -algs = (Direct(), QueueMethod()) +algs = (Direct(), Coevolve()) Nsims = 250 for alg in algs jump_prob = hawkes_problem(p, alg; u = u0, tspan = tspan, g = g, h = h) - if typeof(alg) <: QueueMethod + if typeof(alg) <: Coevolve stepper = SSAStepper() else stepper = Tsit5() @@ -113,7 +113,7 @@ for alg in algs reset_history!(h) sols[n] = solve(jump_prob, stepper) end - if typeof(alg) <: QueueMethod + if typeof(alg) <: Coevolve λs = permutedims(mapreduce((sol) -> empirical_rate(sol), hcat, sols)) else cols = length(sols[1].u[1].u) From 6cb6881bcd12fb9737ee60785c725ddbe1588771 Mon Sep 17 00:00:00 2001 From: gzagatti Date: Thu, 8 Dec 2022 14:53:26 +0800 Subject: [PATCH 37/72] linting and file rename. --- src/JumpProcesses.jl | 2 +- src/aggregators/{queue.jl => coevolve.jl} | 34 +++++++++++------------ 2 files changed, 18 insertions(+), 18 deletions(-) rename src/aggregators/{queue.jl => coevolve.jl} (88%) diff --git a/src/JumpProcesses.jl b/src/JumpProcesses.jl index 039348f67..0794d48f2 100644 --- a/src/JumpProcesses.jl +++ b/src/JumpProcesses.jl @@ -50,7 +50,7 @@ include("aggregators/prioritytable.jl") include("aggregators/directcr.jl") include("aggregators/rssacr.jl") include("aggregators/rdirect.jl") -include("aggregators/queue.jl") +include("aggregators/coevolve.jl") # spatial: include("spatial/spatial_massaction_jump.jl") diff --git a/src/aggregators/queue.jl b/src/aggregators/coevolve.jl similarity index 88% rename from src/aggregators/queue.jl rename to src/aggregators/coevolve.jl index 45b8abaa0..955dfd963 100644 --- a/src/aggregators/queue.jl +++ b/src/aggregators/coevolve.jl @@ -22,10 +22,10 @@ mutable struct CoevolveJumpAggregation{T, S, F1, F2, RNG, GR, PQ} <: end function CoevolveJumpAggregation(nj::Int, njt::T, et::T, crs::Nothing, sr::Nothing, - maj::S, rs::F1, affs!::F2, sps::Tuple{Bool, Bool}, - rng::RNG; u::U, - dep_graph = nothing, - lrates, urates, Ls) where {T, S, F1, F2, RNG, U} + maj::S, rs::F1, affs!::F2, sps::Tuple{Bool, Bool}, + rng::RNG; u::U, + dep_graph = nothing, + lrates, urates, Ls) where {T, S, F1, F2, RNG, U} if dep_graph === nothing if (get_num_majumps(maj) == 0) || !isempty(rs) error("To use VariableRateJumps with the Queue Method algorithm a dependency graph between jumps must be supplied.") @@ -47,15 +47,15 @@ function CoevolveJumpAggregation(nj::Int, njt::T, et::T, crs::Nothing, sr::Nothi pq = MutableBinaryMinHeap{T}() CoevolveJumpAggregation{T, S, F1, F2, RNG, typeof(dg), - typeof(pq) - }(nj, nj, njt, - et, - crs, sr, maj, - rs, - affs!, sps, - rng, - dg, pq, - lrates, urates, Ls) + typeof(pq) + }(nj, nj, njt, + et, + crs, sr, maj, + rs, + affs!, sps, + rng, + dg, pq, + lrates, urates, Ls) end # creating the JumpAggregation structure (tuple-based variable jumps) @@ -85,10 +85,10 @@ function aggregate(aggregator::Coevolve, u, p, t, end_time, variable_jumps, next_jump = 0 next_jump_time = typemax(t) CoevolveJumpAggregation(next_jump, next_jump_time, end_time, cur_rates, sum_rate, - ma_jumps, rates, affects!, save_positions, rng; - u = u, - dep_graph = dep_graph, - lrates = lrates, urates = urates, Ls = Ls) + ma_jumps, rates, affects!, save_positions, rng; + u = u, + dep_graph = dep_graph, + lrates = lrates, urates = urates, Ls = Ls) end # set up a new simulation and calculate the first jump / jump time From 77b4cb249c6f9815966ff453c118509c9372235c Mon Sep 17 00:00:00 2001 From: gzagatti Date: Sun, 11 Dec 2022 13:24:47 +0800 Subject: [PATCH 38/72] adds default for lrate; fix VariableRateJump API. --- src/jumps.jl | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/jumps.jl b/src/jumps.jl index 1a7677cb9..1e88c9865 100644 --- a/src/jumps.jl +++ b/src/jumps.jl @@ -113,13 +113,15 @@ function VariableRateJump(rate, affect!; lrate = nothing, urate = nothing, L = nothing, rootfind = true, idxs = nothing, - save_positions = (lrate !== nothing && urate !== nothing && - L !== nothing) ? (false, true) : (true, true), + save_positions = (false, true), interp_points = 10, abstol = 1e-12, reltol = 0) - if !((lrate === nothing && urate === nothing && L === nothing) || - (lrate !== nothing && urate !== nothing && L !== nothing)) - error("Either `lrate`, `urate` and `L` must be nothing, or all of them must be defined.") + if !(urate !== nothing && L !== nothing) && !(urate === nothing && L === nothing) + error("Either `urate` and `L` must be nothing, or both of them must be defined.") + end + + if (urate !== nothing && lrate === nothing) + lrate = (u, p, t) -> zero(typeof(t)) end VariableRateJump(rate, affect!, lrate, urate, L, idxs, rootfind, From 41b8f2a0c2497525fbcb39c27a72419d518a7efd Mon Sep 17 00:00:00 2001 From: gzagatti Date: Sun, 11 Dec 2022 13:25:34 +0800 Subject: [PATCH 39/72] ensure correct type inference. --- src/aggregators/coevolve.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/aggregators/coevolve.jl b/src/aggregators/coevolve.jl index 955dfd963..5726a62bc 100644 --- a/src/aggregators/coevolve.jl +++ b/src/aggregators/coevolve.jl @@ -34,9 +34,9 @@ function CoevolveJumpAggregation(nj::Int, njt::T, et::T, crs::Nothing, sr::Nothi end else # using a Set to ensure that edges are not duplicate - dg = [Set{Int}(append!([], jumps, [var])) - for (var, jumps) in enumerate(dep_graph)] - dg = [sort!(collect(i)) for i in dg] + dgsets = [Set{Int}(append!(Int[], jumps, [var])) + for (var, jumps) in enumerate(dep_graph)] + dg = [sort!(collect(i)) for i in dgsets] end num_jumps = get_num_majumps(maj) + length(rs) From f0dc9a747056babeec2418f48b73c12b51b55490 Mon Sep 17 00:00:00 2001 From: gzagatti Date: Sun, 11 Dec 2022 13:26:15 +0800 Subject: [PATCH 40/72] removes unncessary update_state! from Coevolve. --- src/aggregators/coevolve.jl | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/aggregators/coevolve.jl b/src/aggregators/coevolve.jl index 5726a62bc..7669ecf5d 100644 --- a/src/aggregators/coevolve.jl +++ b/src/aggregators/coevolve.jl @@ -114,23 +114,6 @@ function generate_jumps!(p::CoevolveJumpAggregation, integrator, u, params, t) nothing end -@inline function update_state!(p::CoevolveJumpAggregation, integrator, u, params, t) - @unpack ma_jumps, next_jump = p - num_majumps = get_num_majumps(ma_jumps) - if next_jump <= num_majumps - if u isa SVector - integrator.u = executerx(u, next_jump, ma_jumps) - else - @inbounds executerx!(u, next_jump, ma_jumps) - end - else - idx = next_jump - num_majumps - @inbounds p.affects![idx](integrator) - end - p.prev_jump = next_jump - return integrator.u -end - ######################## SSA specific helper routines ######################## function update_dependent_rates!(p::CoevolveJumpAggregation, u, params, t) @inbounds deps = p.dep_gr[p.next_jump] From a8cf1c6a623ec585746cf309ebe46543d16fd4bf Mon Sep 17 00:00:00 2001 From: gzagatti Date: Sun, 11 Dec 2022 13:27:05 +0800 Subject: [PATCH 41/72] corrects eltype to typeof. --- src/aggregators/coevolve.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/aggregators/coevolve.jl b/src/aggregators/coevolve.jl index 7669ecf5d..d6225fbc8 100644 --- a/src/aggregators/coevolve.jl +++ b/src/aggregators/coevolve.jl @@ -180,7 +180,7 @@ end function fill_rates_and_get_times!(p::CoevolveJumpAggregation, u, params, t) @unpack end_time = p num_jumps = get_num_majumps(p.ma_jumps) + length(p.rates) - jump_times = Vector{eltype(t)}(undef, num_jumps) + jump_times = Vector{typeof(t)}(undef, num_jumps) @inbounds for i in 1:num_jumps jump_times[i] = next_time(p, u, params, t, i, end_time) end From c1b7589a2a21345ec1f7bc1041d0aea05e5c22f6 Mon Sep 17 00:00:00 2001 From: gzagatti Date: Sun, 11 Dec 2022 13:27:27 +0800 Subject: [PATCH 42/72] inlines get_rate in Coevolve. --- src/aggregators/coevolve.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/aggregators/coevolve.jl b/src/aggregators/coevolve.jl index d6225fbc8..46f9ae0a3 100644 --- a/src/aggregators/coevolve.jl +++ b/src/aggregators/coevolve.jl @@ -125,7 +125,7 @@ function update_dependent_rates!(p::CoevolveJumpAggregation, u, params, t) nothing end -function get_rates(p::CoevolveJumpAggregation, i, u) +@inline function get_rates(p::CoevolveJumpAggregation, i, u) ma_jumps = p.ma_jumps num_majumps = get_num_majumps(ma_jumps) if i <= num_majumps From 138b07f89407cdcfa1d18679541c0220af4881b1 Mon Sep 17 00:00:00 2001 From: gzagatti Date: Sun, 11 Dec 2022 13:27:50 +0800 Subject: [PATCH 43/72] adds Coevolve to more tests. --- test/bimolerx_test.jl | 2 +- test/degenerate_rx_cases.jl | 2 +- test/geneexpr_test.jl | 2 +- test/linearreaction_test.jl | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/bimolerx_test.jl b/test/bimolerx_test.jl index 5673fcf28..e91c04eab 100644 --- a/test/bimolerx_test.jl +++ b/test/bimolerx_test.jl @@ -15,7 +15,7 @@ doprintmeans = false # SSAs to test SSAalgs = (RDirect(), RSSACR(), Direct(), DirectFW(), FRM(), FRMFW(), SortingDirect(), - NRM(), RSSA(), DirectCR()) + NRM(), RSSA(), DirectCR(), Coevolve()) Nsims = 32000 tf = 0.01 diff --git a/test/degenerate_rx_cases.jl b/test/degenerate_rx_cases.jl index 4fede6f1f..b81bb2b34 100644 --- a/test/degenerate_rx_cases.jl +++ b/test/degenerate_rx_cases.jl @@ -13,7 +13,7 @@ doprint = false doplot = false methods = (RDirect(), RSSACR(), Direct(), DirectFW(), FRM(), FRMFW(), SortingDirect(), - NRM(), RSSA(), DirectCR()) + NRM(), RSSA(), DirectCR(), Coevolve()) # one reaction case, mass action jump, vector of data rate = [2.0] diff --git a/test/geneexpr_test.jl b/test/geneexpr_test.jl index 88b5dfda3..24e1e4140 100644 --- a/test/geneexpr_test.jl +++ b/test/geneexpr_test.jl @@ -13,7 +13,7 @@ doprintmeans = false # SSAs to test SSAalgs = (RDirect(), RSSACR(), Direct(), DirectFW(), FRM(), FRMFW(), SortingDirect(), - NRM(), RSSA(), DirectCR()) + NRM(), RSSA(), DirectCR(), Coevolve()) # numerical parameters Nsims = 8000 diff --git a/test/linearreaction_test.jl b/test/linearreaction_test.jl index 787add34b..d169b5713 100644 --- a/test/linearreaction_test.jl +++ b/test/linearreaction_test.jl @@ -309,7 +309,7 @@ for method in SSAalgs end # for dependency graph methods just test with mass action jumps -SSAalgs = [RDirect(), NRM(), SortingDirect(), DirectCR()] +SSAalgs = [RDirect(), NRM(), SortingDirect(), DirectCR(), Coevolve()] jump_prob_gens = [A_to_B_ma] for method in SSAalgs for jump_prob_gen in jump_prob_gens From b87e00e39981b53af4f07082f8be7e1ce36053ce Mon Sep 17 00:00:00 2001 From: gzagatti Date: Sun, 11 Dec 2022 21:43:52 +0800 Subject: [PATCH 44/72] fix update_state! call. --- src/aggregators/coevolve.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/aggregators/coevolve.jl b/src/aggregators/coevolve.jl index 46f9ae0a3..12f109e7c 100644 --- a/src/aggregators/coevolve.jl +++ b/src/aggregators/coevolve.jl @@ -102,7 +102,7 @@ end # execute one jump, changing the system state function execute_jumps!(p::CoevolveJumpAggregation, integrator, u, params, t) # execute jump - u = update_state!(p, integrator, u, params, t) + u = update_state!(p, integrator, u) # update current jump rates and times update_dependent_rates!(p, u, params, t) nothing From 27707e6a534a94988cf808853943976ca53480d1 Mon Sep 17 00:00:00 2001 From: gzagatti Date: Sun, 11 Dec 2022 21:45:40 +0800 Subject: [PATCH 45/72] adds supports_variablerates trait. --- src/aggregators/aggregators.jl | 4 ++++ src/problem.jl | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/aggregators/aggregators.jl b/src/aggregators/aggregators.jl index 0fc117024..0ec2e28ae 100644 --- a/src/aggregators/aggregators.jl +++ b/src/aggregators/aggregators.jl @@ -178,6 +178,10 @@ needs_vartojumps_map(aggregator::AbstractAggregatorAlgorithm) = false needs_vartojumps_map(aggregator::RSSA) = true needs_vartojumps_map(aggregator::RSSACR) = true +# true if aggregator supports variable rates +supports_variablerates(aggregator::AbstractAggregatorAlgorithm) = false +supports_variablerates(aggregator::Coevolve) = true + is_spatial(aggregator::AbstractAggregatorAlgorithm) = false is_spatial(aggregator::NSM) = true is_spatial(aggregator::DirectCRDirect) = true diff --git a/src/problem.jl b/src/problem.jl index 62a6f857d..35188d993 100644 --- a/src/problem.jl +++ b/src/problem.jl @@ -197,7 +197,7 @@ function JumpProblem(prob, aggregator::AbstractAggregatorAlgorithm, jumps::JumpS cont_agg = JumpSet().variable_jumps disc_agg = nothing constant_jump_callback = CallbackSet() - elseif typeof(aggregator) <: Coevolve + elseif supports_variablerates(aggregator) # Coevolve handles all types of jumps together variable_jumps = VariableRateJump[] if (length(jumps.constant_jumps) == 0) From 0b59e52d81bf2988631ac8a950a7f891c644d1be Mon Sep 17 00:00:00 2001 From: gzagatti Date: Mon, 12 Dec 2022 09:18:54 +0800 Subject: [PATCH 46/72] re-use random number. --- src/aggregators/coevolve.jl | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/src/aggregators/coevolve.jl b/src/aggregators/coevolve.jl index 12f109e7c..1063208a9 100644 --- a/src/aggregators/coevolve.jl +++ b/src/aggregators/coevolve.jl @@ -7,7 +7,7 @@ mutable struct CoevolveJumpAggregation{T, S, F1, F2, RNG, GR, PQ} <: prev_jump::Int # the previous jump that was executed next_jump_time::T # the time of the next jump end_time::T # the time to stop a simulation - cur_rates::Nothing # not used + cur_rates::Vector{T} # not used sum_rate::Nothing # not used ma_jumps::S # not used rates::F1 # vector of rate functions @@ -21,7 +21,7 @@ mutable struct CoevolveJumpAggregation{T, S, F1, F2, RNG, GR, PQ} <: Ls::F1 # vector of interval length functions end -function CoevolveJumpAggregation(nj::Int, njt::T, et::T, crs::Nothing, sr::Nothing, +function CoevolveJumpAggregation(nj::Int, njt::T, et::T, crs::Vector{T}, sr::Nothing, maj::S, rs::F1, affs!::F2, sps::Tuple{Bool, Bool}, rng::RNG; u::U, dep_graph = nothing, @@ -80,7 +80,8 @@ function aggregate(aggregator::Coevolve, u, p, t, end_time, variable_jumps, urates = Vector{RateWrapper}() Ls = Vector{RateWrapper}() end - cur_rates = nothing + num_jumps = get_num_majumps(ma_jumps) + length(rates) + cur_rates = Vector{typeof(t)}(undef, num_jumps) sum_rate = nothing next_jump = 0 next_jump_time = typemax(t) @@ -117,10 +118,11 @@ end ######################## SSA specific helper routines ######################## function update_dependent_rates!(p::CoevolveJumpAggregation, u, params, t) @inbounds deps = p.dep_gr[p.next_jump] - @unpack end_time, pq = p + @unpack cur_rates, end_time, pq = p for (ix, i) in enumerate(deps) - ti = next_time(p, u, params, t, i, end_time) + ti, last_urate_i = next_time(p, u, params, t, i, end_time) update!(pq, i, ti) + cur_rates[i] = last_urate_i end nothing end @@ -144,13 +146,21 @@ end function next_time(p::CoevolveJumpAggregation{T}, u, params, t, i, tstop::T) where {T} @unpack rng = p rate, lrate, urate, L = get_rates(p, i, u) + _urate = urate(u, params, t) + _last_urate = p.cur_rates[i] + if i != p.next_jump && _last_urate > zero(t) + s = _urate == zero(t) ? typemax(t) : _last_urate / _urate * (p.pq[i] - t) + else + s = _urate == zero(t) ? typemax(t) : randexp(rng) / _urate + end + _t = t + s while t < tstop - _urate = urate(u, params, t) _L = L(u, params, t) - s = _urate == zero(t) ? typemax(t) : randexp(rng) / _urate - _t = t + s if s > _L t = t + _L + _urate = urate(u, params, t) + s = _urate == zero(t) ? typemax(t) : randexp(rng) / _urate + _t = t + s continue end if _t >= tstop @@ -167,22 +177,26 @@ function next_time(p::CoevolveJumpAggregation{T}, u, params, t, i, tstop::T) whe _rate = rate(u, params, _t) if (v > _rate / _urate) t = _t + _urate = urate(u, params, t) + s = _urate == zero(t) ? typemax(t) : randexp(rng) / _urate + _t = t + s continue end end end - return _t + break end - return typemax(t) + return _t, _urate end # reevaulate all rates, recalculate all jump times, and reinit the priority queue function fill_rates_and_get_times!(p::CoevolveJumpAggregation, u, params, t) @unpack end_time = p num_jumps = get_num_majumps(p.ma_jumps) + length(p.rates) + p.cur_rates = zeros(typeof(t), num_jumps) jump_times = Vector{typeof(t)}(undef, num_jumps) @inbounds for i in 1:num_jumps - jump_times[i] = next_time(p, u, params, t, i, end_time) + jump_times[i], p.cur_rates[i] = next_time(p, u, params, t, i, end_time) end p.pq = MutableBinaryMinHeap(jump_times) nothing From a4abd479217fbc8e57fe1dbb7929e4ede0c33914 Mon Sep 17 00:00:00 2001 From: gzagatti Date: Mon, 12 Dec 2022 12:50:35 +0800 Subject: [PATCH 47/72] revert JumpSet changes. --- src/jumps.jl | 30 +++++++++++------------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/src/jumps.jl b/src/jumps.jl index 1e88c9865..29fc2a7eb 100644 --- a/src/jumps.jl +++ b/src/jumps.jl @@ -439,27 +439,19 @@ function JumpSet(vj, cj, rj, maj::MassActionJump{S, T, U, V}) where {S <: Number JumpSet(vj, cj, rj, check_majump_type(maj)) end -function JumpSet(jump::VariableRateJump) - JumpSet(VariableRateJump[jump], ConstantRateJump[], nothing, nothing) -end -function JumpSet(jump::ConstantRateJump) - JumpSet(VariableRateJump[], ConstantRateJump[jump], nothing, nothing) -end -JumpSet(jump::RegularJump) = JumpSet(VariableRateJump[], ConstantRateJump[], jump, nothing) -function JumpSet(jump::AbstractMassActionJump) - JumpSet(VariableRateJump[], ConstantRateJump[], nothing, jump) -end -function JumpSet(; variable_jumps = VariableRateJump[], constant_jumps = ConstantRateJump[], +JumpSet(jump::ConstantRateJump) = JumpSet((), (jump,), nothing, nothing) +JumpSet(jump::VariableRateJump) = JumpSet((jump,), (), nothing, nothing) +JumpSet(jump::RegularJump) = JumpSet((), (), jump, nothing) +JumpSet(jump::AbstractMassActionJump) = JumpSet((), (), nothing, jump) +function JumpSet(; variable_jumps = (), constant_jumps = (), regular_jumps = nothing, massaction_jumps = nothing) - JumpSet(variable_jumps, constant_jumps, regular_jumps, - massaction_jumps) + JumpSet(variable_jumps, constant_jumps, regular_jumps, massaction_jumps) end JumpSet(jb::Nothing) = JumpSet() # For Varargs, use recursion to make it type-stable function JumpSet(jumps::AbstractJump...) - JumpSet(split_jumps(VariableRateJump[], ConstantRateJump[], nothing, nothing, - jumps...)...) + JumpSet(split_jumps((), (), nothing, nothing, jumps...)...) end # handle vector of mass action jumps @@ -481,10 +473,10 @@ end @inline split_jumps(vj, cj, rj, maj) = vj, cj, rj, maj @inline function split_jumps(vj, cj, rj, maj, v::VariableRateJump, args...) - split_jumps(push!(vj, v), cj, rj, maj, args...) + split_jumps((vj..., v), cj, rj, maj, args...) end @inline function split_jumps(vj, cj, rj, maj, c::ConstantRateJump, args...) - split_jumps(vj, push!(cj, c), rj, maj, args...) + split_jumps(vj, (cj..., c), rj, maj, args...) end @inline function split_jumps(vj, cj, rj, maj, c::RegularJump, args...) split_jumps(vj, cj, regular_jump_combine(rj, c), maj, args...) @@ -493,8 +485,8 @@ end split_jumps(vj, cj, rj, massaction_jump_combine(maj, c), args...) end @inline function split_jumps(vj, cj, rj, maj, j::JumpSet, args...) - split_jumps(append!(vj, j.variable_jumps), - append!(cj, j.constant_jumps), + split_jumps((vj..., j.variable_jumps...), + (cj..., j.constant_jumps...), regular_jump_combine(rj, j.regular_jump), massaction_jump_combine(maj, j.massaction_jump), args...) end From 54ed717b81fe688906561b67d8eb9dcd932042bc Mon Sep 17 00:00:00 2001 From: gzagatti Date: Mon, 12 Dec 2022 12:52:14 +0800 Subject: [PATCH 48/72] avoid creating anonymous function when not needed. --- src/aggregators/coevolve.jl | 69 +++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/src/aggregators/coevolve.jl b/src/aggregators/coevolve.jl index 1063208a9..fbc3300e2 100644 --- a/src/aggregators/coevolve.jl +++ b/src/aggregators/coevolve.jl @@ -9,7 +9,7 @@ mutable struct CoevolveJumpAggregation{T, S, F1, F2, RNG, GR, PQ} <: end_time::T # the time to stop a simulation cur_rates::Vector{T} # not used sum_rate::Nothing # not used - ma_jumps::S # not used + ma_jumps::S # MassActionJumps rates::F1 # vector of rate functions affects!::F2 # vector of affect functions for VariableRateJumps save_positions::Tuple{Bool, Bool} # tuple for whether to save the jumps before and/or after event @@ -127,26 +127,27 @@ function update_dependent_rates!(p::CoevolveJumpAggregation, u, params, t) nothing end -@inline function get_rates(p::CoevolveJumpAggregation, i, u) - ma_jumps = p.ma_jumps - num_majumps = get_num_majumps(ma_jumps) - if i <= num_majumps - _rate = evalrxrate(u, i, ma_jumps) - rate(u, p, t) = _rate - lrate = rate - urate = rate - L(u, p, t) = typemax(t) - else - idx = i - num_majumps - rate, lrate, urate, L = p.rates[idx], p.lrates[idx], p.urates[idx], p.Ls[idx] - end - return rate, lrate, urate, L +@inline function get_ma_urate(p::CoevolveJumpAggregation, i, u, params, t) + return evalrxrate(u, i, p.ma_jumps), typemax(t) +end + +@inline function get_urate(p::CoevolveJumpAggregation, idx, u, params, t) + @inbounds return p.urates[idx](u, params, t), p.Ls[idx](u, params, t) +end + +@inline function get_lrate(p::CoevolveJumpAggregation, idx, u, params, t) + @inbounds return p.lrates[idx](u, params, t) +end + +@inline function get_rate(p::CoevolveJumpAggregation, idx, u, params, t) + @inbounds return p.rates[idx](u, params, t) end function next_time(p::CoevolveJumpAggregation{T}, u, params, t, i, tstop::T) where {T} @unpack rng = p - rate, lrate, urate, L = get_rates(p, i, u) - _urate = urate(u, params, t) + idx = i - get_num_majumps(p.ma_jumps) + _urate, _L = idx > 0 ? get_urate(p, idx, u, params, t) : + get_ma_urate(p, i, u, params, t) _last_urate = p.cur_rates[i] if i != p.next_jump && _last_urate > zero(t) s = _urate == zero(t) ? typemax(t) : _last_urate / _urate * (p.pq[i] - t) @@ -155,10 +156,9 @@ function next_time(p::CoevolveJumpAggregation{T}, u, params, t, i, tstop::T) whe end _t = t + s while t < tstop - _L = L(u, params, t) if s > _L t = t + _L - _urate = urate(u, params, t) + _urate, _L = get_urate(p, idx, u, params, t) s = _urate == zero(t) ? typemax(t) : randexp(rng) / _urate _t = t + s continue @@ -166,21 +166,22 @@ function next_time(p::CoevolveJumpAggregation{T}, u, params, t, i, tstop::T) whe if _t >= tstop break end - _lrate = lrate(u, params, t) - if _lrate > _urate - error("The lower bound should be lower than the upper bound rate for t = $(t) and i = $(i), but lower bound = $(_lrate) > upper bound = $(_urate)") - elseif _lrate < _urate - # when the lower and upper bound are the same, then v < 1 = _lrate / _urate = _rate / _urate - v = rand(rng) - # first inequality is less expensive and short-circuits the evaluation - if (v > _lrate / _urate) - _rate = rate(u, params, _t) - if (v > _rate / _urate) - t = _t - _urate = urate(u, params, t) - s = _urate == zero(t) ? typemax(t) : randexp(rng) / _urate - _t = t + s - continue + if idx > 0 && @inbounds p.urates[idx] !== p.lrates[idx] + _lrate = get_lrate(p, idx, u, params, t) + if _lrate > _urate + error("The lower bound should be lower than the upper bound rate for t = $(t) and i = $(i), but lower bound = $(_lrate) > upper bound = $(_urate)") + elseif _lrate < _urate + # when the lower and upper bound are the same, then v < 1 = _lrate / _urate = _rate / _urate + v = rand(rng) + # first inequality is less expensive and short-circuits the evaluation + if (v > _lrate / _urate) + if (v > get_rate(p, idx, u, params, _t) / _urate) + t = _t + _urate, _L = get_urate(p, idx, u, params, t) + s = _urate == zero(t) ? typemax(t) : randexp(rng) / _urate + _t = t + s + continue + end end end end From 0ca14937388b20b16e85751533742caed0343f1b Mon Sep 17 00:00:00 2001 From: gzagatti Date: Mon, 12 Dec 2022 13:36:46 +0800 Subject: [PATCH 49/72] remove the VariableRateJump initializer from a ConstantRateJump. --- src/jumps.jl | 9 --------- src/problem.jl | 6 ++++-- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/jumps.jl b/src/jumps.jl index 29fc2a7eb..5c7208ef3 100644 --- a/src/jumps.jl +++ b/src/jumps.jl @@ -128,15 +128,6 @@ function VariableRateJump(rate, affect!; interp_points, save_positions, abstol, reltol) end -function VariableRateJump(jump::ConstantRateJump) - L = (u, p, t) -> typemax(t) - VariableRateJump(jump.rate, jump.affect!; lrate = jump.rate, - urate = jump.rate, L = L, idxs = nothing, rootfind = true, - save_positions = (false, true), - interp_points = 10, - abstol = 1e-12, reltol = 0) -end - struct RegularJump{iip, R, C, MD} rate::R c::C diff --git a/src/problem.jl b/src/problem.jl index 35188d993..407fa6982 100644 --- a/src/problem.jl +++ b/src/problem.jl @@ -203,9 +203,11 @@ function JumpProblem(prob, aggregator::AbstractAggregatorAlgorithm, jumps::JumpS if (length(jumps.constant_jumps) == 0) variable_jumps = jumps.variable_jumps else - variable_jumps = append!(variable_jumps, jumps.variable_jumps) append!(variable_jumps, - [VariableRateJump(jump) for jump in jumps.constant_jumps]) + [VariableRateJump(jump.rate, jump.affect!; lrate = jump.rate, + urate = jump.rate, L = (u, p, t) -> typemax(t)) + for jump in jumps.constant_jumps]) + append!(variable_jumps, jumps.variable_jumps) end new_prob = prob disc_agg = aggregate(aggregator, u, prob.p, t, end_time, variable_jumps, maj, From 4d89b5dbdfeae6445a893cfddd09ba385ef15f58 Mon Sep 17 00:00:00 2001 From: gzagatti Date: Mon, 12 Dec 2022 13:37:13 +0800 Subject: [PATCH 50/72] fix comments. --- src/aggregators/coevolve.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/aggregators/coevolve.jl b/src/aggregators/coevolve.jl index fbc3300e2..43a659cbe 100644 --- a/src/aggregators/coevolve.jl +++ b/src/aggregators/coevolve.jl @@ -7,7 +7,7 @@ mutable struct CoevolveJumpAggregation{T, S, F1, F2, RNG, GR, PQ} <: prev_jump::Int # the previous jump that was executed next_jump_time::T # the time of the next jump end_time::T # the time to stop a simulation - cur_rates::Vector{T} # not used + cur_rates::Vector{T} # the last computed upper bound for each rate sum_rate::Nothing # not used ma_jumps::S # MassActionJumps rates::F1 # vector of rate functions @@ -171,7 +171,7 @@ function next_time(p::CoevolveJumpAggregation{T}, u, params, t, i, tstop::T) whe if _lrate > _urate error("The lower bound should be lower than the upper bound rate for t = $(t) and i = $(i), but lower bound = $(_lrate) > upper bound = $(_urate)") elseif _lrate < _urate - # when the lower and upper bound are the same, then v < 1 = _lrate / _urate = _rate / _urate + # when the lower and upper bound are the same, then v < 1 = _lrate / _urate = _urate / _urate v = rand(rng) # first inequality is less expensive and short-circuits the evaluation if (v > _lrate / _urate) From f51fc44d17835613926a1fbda7163a50a2d63bff Mon Sep 17 00:00:00 2001 From: gzagatti Date: Mon, 12 Dec 2022 15:42:56 +0800 Subject: [PATCH 51/72] treat all jump types differently in Coevolve. --- src/aggregators/coevolve.jl | 126 ++++++++++++++++++++---------------- src/problem.jl | 16 +---- 2 files changed, 75 insertions(+), 67 deletions(-) diff --git a/src/aggregators/coevolve.jl b/src/aggregators/coevolve.jl index 43a659cbe..9bf245910 100644 --- a/src/aggregators/coevolve.jl +++ b/src/aggregators/coevolve.jl @@ -39,7 +39,7 @@ function CoevolveJumpAggregation(nj::Int, njt::T, et::T, crs::Vector{T}, sr::Not dg = [sort!(collect(i)) for i in dgsets] end - num_jumps = get_num_majumps(maj) + length(rs) + num_jumps = get_num_majumps(maj) + length(urates) if length(dg) != num_jumps error("Number of nodes in the dependency graph must be the same as the number of jumps.") @@ -59,28 +59,37 @@ function CoevolveJumpAggregation(nj::Int, njt::T, et::T, crs::Vector{T}, sr::Not end # creating the JumpAggregation structure (tuple-based variable jumps) -function aggregate(aggregator::Coevolve, u, p, t, end_time, variable_jumps, +function aggregate(aggregator::Coevolve, u, p, t, end_time, constant_jumps, ma_jumps, save_positions, rng; - dep_graph = nothing, + dep_graph = nothing, variable_jumps = nothing, kwargs...) AffectWrapper = FunctionWrappers.FunctionWrapper{Nothing, Tuple{Any}} RateWrapper = FunctionWrappers.FunctionWrapper{typeof(t), Tuple{typeof(u), typeof(p), typeof(t)}} + affects! = Vector{AffectWrapper}() + rates = Vector{RateWrapper}() + lrates = Vector{RateWrapper}() + urates = Vector{RateWrapper}() + Ls = Vector{RateWrapper}() + + if (constant_jumps !== nothing) && !isempty(constant_jumps) + append!(affects!, + [AffectWrapper((integrator) -> (j.affect!(integrator); nothing)) + for j in constant_jumps]) + append!(urates, [RateWrapper(j.rate) for j in constant_jumps]) + end + if (variable_jumps !== nothing) && !isempty(variable_jumps) - affects! = [AffectWrapper((integrator) -> (c.affect!(integrator); nothing)) - for c in variable_jumps] - rates = [RateWrapper(c.rate) for c in variable_jumps] - lrates = Any[RateWrapper(c.lrate) for c in variable_jumps] - urates = Any[RateWrapper(c.urate) for c in variable_jumps] - Ls = Any[RateWrapper(c.L) for c in variable_jumps] - else - affects! = Vector{AffectWrapper}() - rates = Vector{RateWrapper}() - lrates = Vector{RateWrapper}() - urates = Vector{RateWrapper}() - Ls = Vector{RateWrapper}() + append!(affects!, + [AffectWrapper((integrator) -> (j.affect!(integrator); nothing)) + for j in variable_jumps]) + append!(rates, [RateWrapper(j.rate) for j in variable_jumps]) + append!(lrates, [RateWrapper(j.lrate) for j in variable_jumps]) + append!(urates, [RateWrapper(j.urate) for j in variable_jumps]) + append!(Ls, [RateWrapper(j.L) for j in variable_jumps]) end - num_jumps = get_num_majumps(ma_jumps) + length(rates) + + num_jumps = get_num_majumps(ma_jumps) + length(urates) cur_rates = Vector{typeof(t)}(undef, num_jumps) sum_rate = nothing next_jump = 0 @@ -128,72 +137,81 @@ function update_dependent_rates!(p::CoevolveJumpAggregation, u, params, t) end @inline function get_ma_urate(p::CoevolveJumpAggregation, i, u, params, t) - return evalrxrate(u, i, p.ma_jumps), typemax(t) + return evalrxrate(u, i, p.ma_jumps) +end + +@inline function get_urate(p::CoevolveJumpAggregation, uidx, u, params, t) + @inbounds return p.urates[uidx](u, params, t) end -@inline function get_urate(p::CoevolveJumpAggregation, idx, u, params, t) - @inbounds return p.urates[idx](u, params, t), p.Ls[idx](u, params, t) +@inline function get_L(p::CoevolveJumpAggregation, lidx, u, params, t) + @inbounds return p.Ls[lidx](u, params, t) end -@inline function get_lrate(p::CoevolveJumpAggregation, idx, u, params, t) - @inbounds return p.lrates[idx](u, params, t) +@inline function get_lrate(p::CoevolveJumpAggregation, lidx, u, params, t) + @inbounds return p.lrates[lidx](u, params, t) end -@inline function get_rate(p::CoevolveJumpAggregation, idx, u, params, t) - @inbounds return p.rates[idx](u, params, t) +@inline function get_rate(p::CoevolveJumpAggregation, lidx, u, params, t) + @inbounds return p.rates[lidx](u, params, t) end function next_time(p::CoevolveJumpAggregation{T}, u, params, t, i, tstop::T) where {T} @unpack rng = p - idx = i - get_num_majumps(p.ma_jumps) - _urate, _L = idx > 0 ? get_urate(p, idx, u, params, t) : - get_ma_urate(p, i, u, params, t) - _last_urate = p.cur_rates[i] - if i != p.next_jump && _last_urate > zero(t) - s = _urate == zero(t) ? typemax(t) : _last_urate / _urate * (p.pq[i] - t) + num_majumps = get_num_majumps(p.ma_jumps) + num_cjumps = length(p.urates) - length(p.rates) + uidx = i - num_majumps + lidx = i - num_majumps - num_cjumps + urate = uidx > 0 ? get_urate(p, uidx, u, params, t) : + get_ma_urate(p, i, u, params, t) + last_urate = p.cur_rates[i] + if i != p.next_jump && last_urate > zero(t) + s = urate == zero(t) ? typemax(t) : last_urate / urate * (p.pq[i] - t) else - s = _urate == zero(t) ? typemax(t) : randexp(rng) / _urate + s = urate == zero(t) ? typemax(t) : randexp(rng) / urate end _t = t + s - while t < tstop - if s > _L - t = t + _L - _urate, _L = get_urate(p, idx, u, params, t) - s = _urate == zero(t) ? typemax(t) : randexp(rng) / _urate - _t = t + s - continue - end - if _t >= tstop - break - end - if idx > 0 && @inbounds p.urates[idx] !== p.lrates[idx] - _lrate = get_lrate(p, idx, u, params, t) - if _lrate > _urate - error("The lower bound should be lower than the upper bound rate for t = $(t) and i = $(i), but lower bound = $(_lrate) > upper bound = $(_urate)") - elseif _lrate < _urate - # when the lower and upper bound are the same, then v < 1 = _lrate / _urate = _urate / _urate + if lidx > 0 + while t < tstop + L = get_L(p, lidx, u, params, t) + if s > L + t = t + L + urate = get_urate(p, uidx, u, params, t) + s = urate == zero(t) ? typemax(t) : randexp(rng) / urate + _t = t + s + continue + end + if _t >= tstop + break + end + lrate = p.urates[uidx] === p.lrates[lidx] ? urate : + get_lrate(p, lidx, u, params, t) + if lrate > urate + error("The lower bound should be lower than the upper bound rate for t = $(t) and i = $(i), but lower bound = $(lrate) > upper bound = $(urate)") + elseif lrate < urate + # when the lower and upper bound are the same, then v < 1 = lrate / urate = urate / urate v = rand(rng) # first inequality is less expensive and short-circuits the evaluation - if (v > _lrate / _urate) - if (v > get_rate(p, idx, u, params, _t) / _urate) + if (v > lrate / urate) + if (v > get_rate(p, lidx, u, params, _t) / urate) t = _t - _urate, _L = get_urate(p, idx, u, params, t) - s = _urate == zero(t) ? typemax(t) : randexp(rng) / _urate + urate = get_urate(p, uidx, u, params, t) + s = urate == zero(t) ? typemax(t) : randexp(rng) / urate _t = t + s continue end end end + break end - break end - return _t, _urate + return _t, urate end # reevaulate all rates, recalculate all jump times, and reinit the priority queue function fill_rates_and_get_times!(p::CoevolveJumpAggregation, u, params, t) @unpack end_time = p - num_jumps = get_num_majumps(p.ma_jumps) + length(p.rates) + num_jumps = get_num_majumps(p.ma_jumps) + length(p.urates) p.cur_rates = zeros(typeof(t), num_jumps) jump_times = Vector{typeof(t)}(undef, num_jumps) @inbounds for i in 1:num_jumps diff --git a/src/problem.jl b/src/problem.jl index 407fa6982..efa80b2c5 100644 --- a/src/problem.jl +++ b/src/problem.jl @@ -198,20 +198,10 @@ function JumpProblem(prob, aggregator::AbstractAggregatorAlgorithm, jumps::JumpS disc_agg = nothing constant_jump_callback = CallbackSet() elseif supports_variablerates(aggregator) - # Coevolve handles all types of jumps together - variable_jumps = VariableRateJump[] - if (length(jumps.constant_jumps) == 0) - variable_jumps = jumps.variable_jumps - else - append!(variable_jumps, - [VariableRateJump(jump.rate, jump.affect!; lrate = jump.rate, - urate = jump.rate, L = (u, p, t) -> typemax(t)) - for jump in jumps.constant_jumps]) - append!(variable_jumps, jumps.variable_jumps) - end new_prob = prob - disc_agg = aggregate(aggregator, u, prob.p, t, end_time, variable_jumps, maj, - save_positions, rng; kwargs...) + disc_agg = aggregate(aggregator, u, prob.p, t, end_time, jumps.constant_jumps, maj, + save_positions, rng; variable_jumps = jumps.variable_jumps, + kwargs...) constant_jump_callback = DiscreteCallback(disc_agg) variable_jump_callback = CallbackSet() cont_agg = JumpSet().variable_jumps From c55691c4ca45fe629d3625ed0e0b992dedad5c25 Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Tue, 13 Dec 2022 22:02:04 -0500 Subject: [PATCH 52/72] fix local doc builds and add some tutorial updates --- docs/make.jl | 6 +- docs/src/index.md | 18 +- .../tutorials/discrete_stochastic_example.md | 389 ++++++++++-------- src/jumps.jl | 6 +- 4 files changed, 228 insertions(+), 191 deletions(-) diff --git a/docs/make.jl b/docs/make.jl index 71aad1bc8..0502a9600 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,7 +1,9 @@ using Documenter, JumpProcesses -cp("./docs/Manifest.toml", "./docs/src/assets/Manifest.toml", force = true) -cp("./docs/Project.toml", "./docs/src/assets/Project.toml", force = true) +docpath = Base.source_dir() +assetpath = joinpath(docpath, "src", "assets") +cp(joinpath(docpath, "Manifest.toml"), joinpath(assetpath, "Manifest.toml"), force = true) +cp(joinpath(docpath, "Project.toml"), joinpath(assetpath, "Project.toml"), force = true) include("pages.jl") diff --git a/docs/src/index.md b/docs/src/index.md index 721a78384..fbbbdf973 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -79,20 +79,21 @@ versioninfo() # hide ``` ```@example using Pkg # hide -Pkg.status(;mode = PKGMODE_MANIFEST) # hide +Pkg.status(; mode = PKGMODE_MANIFEST) # hide ``` ```@raw html ``` ```@raw html -You can also download the +You can also download the manifest file and the @@ -100,9 +101,10 @@ link = "https://github.com/SciML/"*name*".jl/tree/gh-pages/v"*version*"/assets/M ``` ```@eval using TOML -version = TOML.parse(read("../../Project.toml",String))["version"] -name = TOML.parse(read("../../Project.toml",String))["name"] -link = "https://github.com/SciML/"*name*".jl/tree/gh-pages/v"*version*"/assets/Project.toml" +projtoml = joinpath("..", "..", "Project.toml") +version = TOML.parse(read(projtoml, String))["version"] +name = TOML.parse(read(projtoml, String))["name"] +link = "https://github.com/SciML/" * name * ".jl/tree/gh-pages/v" * version * "/assets/Project.toml" ``` ```@raw html ">project file. diff --git a/docs/src/tutorials/discrete_stochastic_example.md b/docs/src/tutorials/discrete_stochastic_example.md index 61f451072..6b9a34436 100644 --- a/docs/src/tutorials/discrete_stochastic_example.md +++ b/docs/src/tutorials/discrete_stochastic_example.md @@ -6,13 +6,15 @@ chemical kinetics (i.e. Gillespie) models. It is not necessary to have read the [first tutorial](@ref poisson_proc_tutorial). We will illustrate - The different types of jumps that can be represented in JumpProcesses and their use cases. -- How to speed up pure-jump simulations with only [`ConstantRateJump`](@ref)s - and [`MassActionJump`](@ref)s by using the [`SSAStepper`](@ref) time stepper. +- How to speed up pure-jump simulations with only [`ConstantRateJump`](@ref)s, + [`MassActionJump`](@ref)s, and bounded `VariableRateJump`s by using the + [`SSAStepper`](@ref) time stepper. - How to define and use [`MassActionJump`](@ref)s, a more specialized type of [`ConstantRateJump`](@ref) that offers improved computational performance. +- How to define and use bounded [`VariableRateJump`](@ref)s in pure-jump simulations. - How to use saving controls to reduce memory use per simulation. -- How to use [`VariableRateJump`](@ref)s and when they should be preferred over - `ConstantRateJump`s and `MassActionJump`s. +- How to use general [`VariableRateJump`](@ref)s and when they should be + preferred over the other jump types. - How to create hybrid problems mixing the various jump types with ODEs or SDEs. - How to use `RegularJump`s to enable faster, but approximate, time stepping via τ-leaping methods. @@ -202,7 +204,7 @@ jump_prob = JumpProblem(sir_model, prob, Direct()) Here `Direct()` indicates that we will determine the random times and types of reactions using [Gillespie's Direct stochastic simulation algorithm (SSA)](https://doi.org/10.1016/0021-9991(76)90041-3), also known as Doob's -method or Kinetic Monte Carlo. See [Constant Rate Jump Aggregators](@ref) for +method or Kinetic Monte Carlo. See [Jump Aggregators for Exact Simulation](@ref) for other supported SSAs. We now have a problem that can be evolved in time using the JumpProcesses solvers. @@ -253,18 +255,25 @@ In general | Jump Type | Performance | Generality | |:----------: | :----------: |:------------:| | [`MassActionJump`](@ref MassActionJumpSect) | Fastest | Restrictive rates/affects | -| [`ConstantRateJump`](@ref ConstantRateJumpSect) | Somewhat Slower | Rates must -be constant between jumps | -| [`VariableRateJump` with `Coevolve` aggregator](@ref VariableRateJumpCoevolveSect) | Somewhat Slower | Rates can be a function of time, but not of ODE variables | +| [`ConstantRateJump`](@ref ConstantRateJumpSect) | Somewhat Slower than `MassActionJump`| Rate function must be constant between jumps | +| [`VariableRateJump` with rate bounds](@ref VariableRateJumpWithBnds) | Somewhat Slower than `ConstantRateJump` | Rate functions can explicitly depend on time, but require an upper bound that is guaranteed constant between jumps over some time interval | | [`VariableRateJump`](@ref VariableRateJumpSect) | Slowest | Completely general | It is recommended to try to encode jumps using the most performant option that supports the desired generality of the underlying `rate` and `affect!` functions. Below we describe the different jump types, and show how the SIR -model can be formulated using first `ConstantRateJump`s, `VariableRateJump`s -with `Coevolve` aggregator and then `MassActionJump`s. Completely general -models that use `VariableRateJump`s with over an `ODEProblem` are considered -later. +model can be formulated using first `ConstantRateJump`s, then more performant +`MassActionJump`s, and finally with `VariableRateJump`s using rate bounds. We +conclude by presenting several completely general models that use +`VariableRateJump`s without rate bounds, and which require an ODE solver to +handle time-stepping. + +We note, in the remainder we will refer to *bounded* `VariableRateJump`s as +those for which we can specify functions calculating a time window over which +the rate is bounded by a constant (as long as the state is unchanged), see [the +section on bounded `VariableRateJump`s for details](@ref VariableRateJumpWithBnds). +`VariableRateJump`s or *general* `VariableRateJump`s will refer to those for +which such functions are not available. ## [Defining the Jumps Directly: `ConstantRateJump`](@id ConstantRateJumpSect) The constructor for a `ConstantRateJump` is: @@ -326,7 +335,7 @@ jump_prob = JumpProblem(prob, Direct(), jump, jump2) Here [`Direct()`](@ref) indicates that we will determine the random times and types of jumps that occur using [Gillespie's Direct stochastic simulation algorithm (SSA)](https://doi.org/10.1016/0021-9991(76)90041-3), also known as -Doob's method or Kinetic Monte Carlo. See [Constant Rate Jump Aggregators](@ref) +Doob's method or Kinetic Monte Carlo. See [Jump Aggregators for Exact Simulation](@ref) for other supported SSAs. We now have a problem that can be evolved in time using the JumpProcesses solvers. @@ -347,20 +356,112 @@ plot(sol, label=["S(t)" "I(t)" "R(t)"]) Note, in systems with more than a few jumps (more than ~10), it can be advantageous to use more sophisticated SSAs than `Direct`. For such systems it is recommended to use [`SortingDirect`](@ref), [`RSSA`](@ref) or -[`RSSACR`](@ref), see the list of JumpProcesses SSAs at [Constant Rate Jump -Aggregators](@ref). +[`RSSACR`](@ref), see the list of JumpProcesses SSAs at [Jump Aggregators for +Exact Simulation](@ref). +## SSAStepper +Any common interface algorithm can be used to perform the time-stepping since it +is implemented over the callback interface. This allows for hybrid systems that +mix ODEs, SDEs and jumps. In many cases we may have a pure jump system that only +involves `ConstantRateJump`s, `MassActionJump`s, and/or bounded +`VariableRateJump`s (see below). In those cases a substantial performance +benefit may be gained by using [`SSAStepper`](@ref). Note, `SSAStepper` is a +more limited time-stepper which only supports discrete events, and does not +allow simultaneous coupled ODEs/SDEs, or general `VariableRateJump`s. It is, +however, very efficient for pure jump problems involving only +`ConstantRateJump`s, `MassActionJump`s, and bounded `VariableRateJump`s. + +## [Defining the Jumps Directly: `MassActionJump`](@id MassActionJumpSect) +For `ConstantRateJump`s that can be represented as mass action reactions a +further specialization of the jump type is possible that offers improved +computational performance; [`MassActionJump`](@ref). Suppose the system has +``N`` chemical species ``\{S_1,\dots,S_N\}``. A general mass action reaction has +the form + +```math +R_1 S_1 + R_2 S_2 + \dots + R_N S_N \overset{k}{\rightarrow} P_1 S_1 + P_2 S_2 + \dots + P_N S_N +``` +where the non-negative integers ``(R_1,\dots,R_N)`` denote the *reactant +stoichiometry* of the reaction, and the non-negative integers +``(P_1,\dots,P_N)`` the *product stoichiometry*. The *net stoichiometry* is the +net change in each chemical species from the reaction occurring one time, given +by ``\mathbf{\nu} = (P_1-R_1,\dots,P_N-R_N)``. As such, the `affect!` function associated with +a `MassActionJump` simply changes the state, ``\mathbf{u}(t) = (u_1(t),\dots,u_N(t))``, +by updating +```math +\mathbf{u}(t) \to \mathbf{u}(t) + \mathbf{\nu}. +``` +The default rate function, `ρ = rate(u,p,t)`, is based on stochastic chemical +kinetics and given by +```math +ρ(\mathbf{u}(t)) = k \prod_{i=1}^N \begin{pmatrix} u_i \\ R_i \end{pmatrix} += k \prod_{i=1}^N \frac{u_i!}{R_i! (u_i - R_i)!} += k \prod_{i=1}^N \frac{u_i (u_i - 1) \cdots (u_i - R_i + 1)}{R_i!} +``` +where ``k`` denotes the rate constant of the reaction (in units of per time). + +As an example, consider again the SIR model. The species are `u = (S,I,R)`. The +first reaction has rate `β`, reactant stoichiometry `(1, 1, 0)`, product +stoichiometry `(0, 2, 0)`, and net stoichiometry `(-1, 1, 0)`. The second reaction +has rate `ν`, reactant stoichiometry `(0, 1, 0)`, product stoichiometry `(0, 0, 1)`, +and net stoichiometry `(0, -1, 1)`. + +We can manually encode this system as a mass action jump by specifying the +indexes of the rate constants in `p`, the reactant stoichiometry, and the net +stoichiometry. We note that the first two determine the rate function, with the +latter determining the affect function. +```@example tut2 +rateidxs = [1, 2] # i.e. [β, ν] +reactant_stoich = +[ + [1 => 1, 2 => 1], # 1*S and 1*I + [2 => 1] # 1*I +] +net_stoich = +[ + [1 => -1, 2 => 1], # -1*S and 1*I + [2 => -1, 3 => 1] # -1*I and 1*R +] +mass_act_jump = MassActionJump(reactant_stoich, net_stoich; param_idxs=rateidxs) +``` +Notice, one typically should define *one* `MassActionJump` that encodes each +possible jump that can be represented via a mass action reaction. This is in +contrast to `ConstantRateJump`s or `VariableRateJump`s where separate instances +are created for each distinct jump type. + +Just like for `ConstantRateJumps`, to then simulate the system we create +a `JumpProblem` and call `solve`: +```@example tut2 +jump_prob = JumpProblem(prob, Direct(), mass_act_jump) +sol = solve(jump_prob, SSAStepper()) +plot(sol; label=["S(t)" "I(t)" "R(t)"]) +``` + +For more details about MassActionJumps see [Defining a Mass Action Jump](@ref). +We note that one could include the factors of ``1 / R_i!`` directly in the rate +constant passed into a [`MassActionJump`](@ref), so that the desired rate +function that gets evaluated is +```math +\hat{k} \prod_{i=1}^N u_i (u_i - 1) \cdots (u_i - R_i + 1) +``` +with ``\hat{k} = k / \prod_{i=1}^{N} R_i!`` the renormalized rate constant. +Passing the keyword argument `scale_rates = false` will disable +`MassActionJump`s internally rescaling the rate constant by ``(\prod_{i=1}^{N} +R_i!)^{-1}``. -### *Caution about Constant Rate Jumps* -`ConstantRateJump`s are quite general, but they do have one restriction. They -assume that the rate functions are constant at all times between two consecutive -jumps of the system. i.e. any species/states or parameters that the rate -function depends on must not change between the times at which two consecutive -jumps occur. Such conditions are violated if one has a time dependent parameter -like ``\beta(t)`` or if some of the solution components, say `u[2]`, may also -evolve through a coupled ODE, SDE, or a [`VariableRateJump`](@ref) (see below -for examples). For problems where the rate function may change between -consecutive jumps, [`VariableRateJump`](@ref)s must be used. +For chemical reaction systems Catalyst.jl automatically groups reactions +into their optimal jump representation. + +### *Caution about ConstantRateJumps and MassActionJumps* +`ConstantRateJump`s and `MassActionJump`s are restricted in that they assume the +rate functions are constant at all times between two consecutive jumps of the +system. That is, any species/states or parameters that a rate function depends +on must not change between the times at which two consecutive jumps occur. Such +conditions are violated if one has a time dependent parameter like ``\beta(t)`` +or if some of the solution components, say `u[2]`, may also evolve through a +coupled ODE, SDE, or a general [`VariableRateJump`](@ref) (see below for +examples). For problems where the rate function may change between consecutive +jumps, bounded or general [`VariableRateJump`](@ref)s must be used. Thus in the examples above, ```julia @@ -371,47 +472,51 @@ both must be constant other than changes due to some other `ConstantRateJump` or `MassActionJump` (the same restriction applies to `MassActionJump`s). Since these rates only change when `u[1]` or `u[2]` is changed, and `u[1]` and `u[2]` only change when one of the jumps occur, this setup is valid. However, a rate of -`t*p[1]*u[1]*u[2]` would not be valid because the rate would change during the -interval, as would `p[2]*u[1]*u[4]` when `u[4]` is the solution to a continuous -problem such as an ODE or SDE. Thus one must be careful to follow this rule when -choosing rates. +`t*p[1]*u[1]*u[2]` would not be valid because the rate would change in between +jumps, as would `p[2]*u[1]*u[4]` when `u[4]` is the solution to a continuous +problem such as an ODE/SDE, or can be changed by a general `VariableRateJump`. +Thus one must be careful to follow this rule when choosing rates. In summary, if a particular jump process has a rate function that depends explicitly or implicitly on a continuously changing quantity, you need to use a [`VariableRateJump`](@ref). -## [Defining the Jumps Directly: `VariableRateJump`](@id VariableRateJumpCoevolveSect) +## [Defining the Jumps Directly using a bounded `VariableRateJump`](@id VariableRateJumpWithBnds) -Now, let's assume that the infection rate is decreasing over time. That is, once -individuals gets infected they reaches peak infectivity. The force of infection -then decreases exponentially to a basal level. In this case, we must keep track -of the time of infection events. Let the history `H(t)` contain the timestamp of -all active infections. Then, the rate of infection becomes +Assume that the infection rate is now decreasing over time. That is, when +individuals get infected they immediately reach peak infectivity. The force of +infection then decreases exponentially to a basal level. In this case, we must +keep track of the time of infection events. Let the history ``H(t)`` contain the +timestamps of all ``I(t)`` active infections. The rate of infection is then ```math -\beta S(t) I(t) + \alpha S(t) \sum_{t_i \in H(t)} exp(-\gamma (t - t_i)) +\beta S(t) I(t) + \alpha S(t) \sum_{t_i \in H(t)} \exp(-\gamma (t - t_i)) ``` -Where ``\beta`` is the basal rate of infection, ``\alpha`` is the spike in the -rate of infection and ``\gamma`` is the rate which the spike decreases. Here we -choose parameters such that infectivity reaches a basal rate close to ``0`` -after spiking. The spike is equal to the rate ``\beta`` chosen in the previous -section and ``gamma`` is the same as the recovery rate. In other words, we are -modelling a situation in which individuals gradually recover. +where ``\beta`` is the basal rate of infection, ``\alpha`` is the spike in the +rate of infection, and ``\gamma`` is the rate at which the spike decreases. Here +we choose parameters such that infectivity rate due to a single infected +individual returns to the basal rate after spiking to ``\beta + \alpha``. In +other words, we are modelling a situation in infected individuals gradually +become less infectious prior to recovering. Our parameters are then ```@example tut2 β1 = 0.001 / 1000.0 α = 0.1 / 1000.0 -γ = 0.001 +γ = 0.05 p1 = (β1, ν, α, γ) ``` -We define a vector `H` to hold the timestamp of active infections. Then, we -define infection as a `VariableRateJump`. To use the `Coevolve` aggregator, -we need to specify the lower- and upper-bounds of the rate which should be valid -from the time they are computed `t` until `t + L(u, p, t)`. +We define a vector `H` to hold the timestamp of active infections. Then, we +define an infection reaction as a bounded `VariableRateJump`, requiring us to +again provide `rate` and `affect` functions, but also give functions that +calculate an upper-bound on the rate (`urate(u,p,t)`), an optional lower-bound +on the rate (`lrate(u,p,t)`), and a time window over which the bounds are valid +as long as any states these three rates depend on are unchanged (`L(u,p,t)`). +The lower- and upper-bounds of the rate should be valid from the time they are +computed `t` until `t + L(u, p, t)`: ```@example tut2 H = zeros(Float64, 10) -rate3(u, p, t) = p[1]*u[1]*u[2] + p[3]*u[1]*sum([exp(-p[4]*(t - _t)) for _t in H]) +rate3(u, p, t) = p[1]*u[1]*u[2] + p[3]*u[1]*sum(exp(-p[4]*(t - _t)) for _t in H) lrate = rate1 # β*S*I urate = rate3 L(u, p, t) = 1 / (2*urate(u, p, t)) @@ -423,8 +528,12 @@ function affect3!(integrator) end jump3 = VariableRateJump(rate3, affect3!; lrate=lrate, urate=urate, L=L) ``` +Note that here we set the lower bound rate to be the normal SIR infection rate, +and set the upper bound rate equal to the new rate of infection (`rate3`). As +long as `u[1]` and `u[2]` are unchanged by another jump, for any `s` in `[t,t + +L(u,p,t)]` we have that `lrate(u,p,t) <= rate3(u,p,s) <= urate(u,p,t)`. -Next, we redefine the recovery jump's `affect!` such that a random infection is +Next, we redefine the recovery jump's `affect!` such that a random infection is removed from `H` for every recovery. ```@example tut2 @@ -438,15 +547,25 @@ end jump4 = ConstantRateJump(rate4, affect4!) ``` -With the jumps defined, we can build -a [`DiscreteProblem`](https://docs.sciml.ai/DiffEqDocs/stable/types/discrete_types/). -`VariableRateJump`s over a `DiscreteProblem` can only be solved with the -`Coevolve` aggregator. We need to specify a depedency graph to use this -aggregator. In this case, both processes mutually affect each other. +With the jumps defined, we can build a +[`DiscreteProblem`](https://docs.sciml.ai/DiffEqDocs/stable/types/discrete_types/). +Bounded `VariableRateJump`s over a `DiscreteProblem` can currently only be +simulated with the `Coevolve` aggregator. The aggregator requires a dependency +graph to indicate when a given jump occurs which other jumps in the system +should have their rate recalculated (i.e. their rate depends on states modified +by one occurrence of the first jump). In this case, both processes mutually +affect each other so we have + +```@example tut2 +dep_graph = [[1,2], [1,2]] +``` +Here `dep_graph[2] = [1,2]` indicates that when the second jump occurs, both the +first and second jumps need to have their rates recalculated. We can then +construct our `JumpProblem` as before, specifying the `Coevolve` aggregator: ```@example tut2 prob = DiscreteProblem(u₀, tspan, p1) -jump_prob = JumpProblem(prob, Coevolve(), jump3, jump4; dep_graph=[[1,2], [1,2]]) +jump_prob = JumpProblem(prob, Coevolve(), jump3, jump4; dep_graph) ``` We now have a problem that can be solved with `SSAStepper` to handle @@ -454,31 +573,19 @@ time-stepping the `Coevolve` aggregator from jump to jump: ```@example tut2 sol = solve(jump_prob, SSAStepper()) -plot(sol, label=["S(t)", "I(t)", "R(t)"]) +plot(sol, label=["S(t)" "I(t)" "R(t)"]) ``` -Surprinsingly, we see that even with an exponential decrease in infectivity we -reach similar results as with a constant infection rate. +We see that the time-dependent infection rate leads to a lower peak of the +infection throughout the population. -Note that `VariableRateJump` over `DiscreteProblem` can be quite general, but it -is not possible to handle rates that change according to an ODE variable. A rate -such as `p[2]*u[1]*u[4]` when `u[4]` is the solution of a continuous problem -such as an ODE or SDE can only solved using a continuous integrator as discussed +Note that bounded `VariableRateJump`s over `DiscreteProblem`s can be quite +general, but it is not possible to handle rates that change according to an +ODE/SDE modified variable. A rate such as `p[2]*u[1]*u[4]` when `u[4]` is the +solution of a continuous problem such as an ODE or SDE can only be handled using +a general `VariableRateJump` within a continuous integrator as discussed [below](@ref VariableRateJumpSect) -## SSAStepper -Any common interface algorithm can be used to perform the time-stepping since it -is implemented over the callback interface. This allows for hybrid systems that -mix ODEs, SDEs and jumps. In many cases we may have a pure jump system that only -involves `ConstantRateJump`s, `VariableRateJump`s whose rates do not depend on -a continuous variable, and/or `MassActionJump`s (see below). When that's the -case, a substantial performance benefit may be gained by using -[`SSAStepper`](@ref). Note, `SSAStepper` is a more limited time-stepper which -only supports discrete events, and does not allow simultaneous coupled ODEs or -SDEs or `VariableRateJump`s whose rate depend on a continuous variable. It is, -however, very efficient for pure jump problems involving only -`ConstantRateJump`s, `VariableRateJump`s and `MassActionJump`s. - ## [Reducing Memory Use: Controlling Saving Behavior](@id save_positions_docs) Note that jumps act via DifferentialEquations.jl's [callback @@ -516,95 +623,12 @@ will no longer be exact for a pure jump process, as the solution values at jump times have not been stored. i.e for `t` a time we did not save at `sol(t)` will no longer give the exact value of the solution at `t`.* - -## [Defining the Jumps Directly: `MassActionJump`](@id MassActionJumpSect) -For `ConstantRateJump`s that can be represented as mass action reactions a -further specialization of the jump type is possible that offers improved -computational performance; [`MassActionJump`](@ref). Suppose the system has -``N`` chemical species ``\{S_1,\dots,S_N\}``. A general mass action reaction has -the form - -```math -R_1 S_1 + R_2 S_2 + \dots + R_N S_N \overset{k}{\rightarrow} P_1 S_1 + P_2 S_2 + \dots + P_N S_N -``` -where the non-negative integers ``(R_1,\dots,R_N)`` denote the *reactant -stoichiometry* of the reaction, and the non-negative integers -``(P_1,\dots,P_N)`` the *product stoichiometry*. The *net stoichiometry* is the -net change in each chemical species from the reaction occurring one time, given -by ``\mathbf{\nu} = (P_1-R_1,\dots,P_N-R_N)``. As such, the `affect!` function associated with -a `MassActionJump` simply changes the state, ``\mathbf{u}(t) = (u_1(t),\dots,u_N(t))``, -by updating -```math -\mathbf{u}(t) \to \mathbf{u}(t) + \mathbf{\nu}. -``` -The default rate function, `ρ = rate(u,p,t)`, is based on stochastic chemical -kinetics and given by -```math -ρ(\mathbf{u}(t)) = k \prod_{i=1}^N \begin{pmatrix} u_i \\ R_i \end{pmatrix} -= k \prod_{i=1}^N \frac{u_i!}{R_i! (u_i - R_i)!} -= k \prod_{i=1}^N \frac{u_i (u_i - 1) \cdots (u_i - R_i + 1)}{R_i!} -``` -where ``k`` denotes the rate constant of the reaction (in units of per time). - -As an example, consider again the SIR model. The species are `u = (S,I,R)`. The -first reaction has rate `β`, reactant stoichiometry `(1, 1, 0)`, product -stoichiometry `(0, 2, 0)`, and net stoichiometry `(-1, 1, 0)`. The second reaction -has rate `ν`, reactant stoichiometry `(0, 1, 0)`, product stoichiometry `(0, 0, 1)`, -and net stoichiometry `(0, -1, 1)`. - -We can manually encode this system as a mass action jump by specifying the -indexes of the rate constants in `p`, the reactant stoichiometry, and the net -stoichiometry. We note that the first two determine the rate function, with the -latter determining the affect function. -```@example tut2 -rateidxs = [1, 2] # i.e. [β, ν] -reactant_stoich = -[ - [1 => 1, 2 => 1], # 1*S and 1*I - [2 => 1] # 1*I -] -net_stoich = -[ - [1 => -1, 2 => 1], # -1*S and 1*I - [2 => -1, 3 => 1] # -1*I and 1*R -] -mass_act_jump = MassActionJump(reactant_stoich, net_stoich; param_idxs=rateidxs) -``` -Notice, one typically should define *one* `MassActionJump` that encodes each -possible jump that can be represented via a mass action reaction. This is in -contrast to `ConstantRateJump`s or `VariableRateJump`s where separate instances -are created for each distinct jump type. - -Just like for `ConstantRateJumps`, to then simulate the system we create -a `JumpProblem` and call `solve`: -```@example tut2 -jump_prob = JumpProblem(prob, Direct(), mass_act_jump) -sol = solve(jump_prob, SSAStepper()) -plot(sol; label=["S(t)" "I(t)" "R(t)"]) -``` - -For more details about MassActionJumps see [Defining a Mass Action Jump](@ref). -We note that one could include the factors of ``1 / R_i!`` directly in the rate -constant passed into a [`MassActionJump`](@ref), so that the desired rate -function that gets evaluated is -```math -\hat{k} \prod_{i=1}^N u_i (u_i - 1) \cdots (u_i - R_i + 1) -``` -with ``\hat{k} = k / \prod_{i=1}^{N} R_i!`` the renormalized rate constant. -Passing the keyword argument `scale_rates = false` will disable -`MassActionJump`s internally rescaling the rate constant by `\prod_{i=1}^{N} -R_i!`. - -For chemical reaction systems Catalyst.jl automatically groups reactions -into their optimal jump representation. - - ## Defining the Jumps Directly: Mixing `ConstantRateJump`/`VariableRateJump` and `MassActionJump` -Suppose we now want to add in to the SIR model another jump that can not be -represented as a mass action reaction. We can create a new `ConstantRateJump` -and simulate a hybrid system using both the `MassActionJump` for the two -previous reactions, and the new `ConstantRateJump`. Let's suppose we want to let -susceptible people be born with the following jump rate: +Suppose we now want to add in to the original SIR model another jump that can +not be represented as a mass action reaction. We can create a new +`ConstantRateJump` and simulate a hybrid system using both the `MassActionJump` +for the two original reactions, and the new `ConstantRateJump`. Let's suppose we +want to let susceptible people be born with the following jump rate: ```@example tut2 birth_rate(u,p,t) = 10.0 * u[1] / (200.0 + u[1]) + 10.0 function birth_affect!(integrator) @@ -620,14 +644,14 @@ sol = solve(jump_prob, SSAStepper()) plot(sol; label=["S(t)" "I(t)" "R(t)"]) ``` -Note that, we can also combine `MassActionJump` with `VariableRateJump` when -using the `Coevolve` aggregator in the same manner. +Note that we can combine `MassActionJump`s, `ConstantRateJump`s and bounded +`VariableRateJump`s using the `Coevolve` aggregator. ## Adding Jumps to a Differential Equation -If we instead used some form of differential equation instead of a -`DiscreteProblem`, we would couple the jumps/reactions to the differential -equation. Let's define an ODE problem, where the continuous part only acts on -some new 4th component: +If we instead used some form of differential equation via an `ODEProblem` +instead of a `DiscreteProblem`, we can couple the jumps/reactions to the +differential equation. Let's define an ODE problem, where the continuous part +only acts on some new 4th component: ```@example tut2 using OrdinaryDiffEq function f(du, u, p, t) @@ -640,20 +664,24 @@ prob = ODEProblem(f, u₀, tspan, p) Notice we gave the 4th component a starting value of 100.0, and used floating point numbers for the initial condition since some solution components now evolve continuously. The same steps as above will allow us to solve this hybrid -equation when using `ConstantRateJump`s, `VariableRateJump`s or -`MassActionJump`s. For example, we can solve it using the `Tsit5()` method via: +equation when using `ConstantRateJump`s, `MassActionJump`s, or +`VariableRateJump`s. For example, we can solve it using the `Tsit5()` method +via: ```@example tut2 jump_prob = JumpProblem(prob, Direct(), jump, jump2) sol = solve(jump_prob, Tsit5()) plot(sol; label=["S(t)" "I(t)" "R(t)" "u₄(t)"]) ``` -Note that when using `VariableRateJump`s with the `Coevolve` aggregator, the -ODE problem should not modify the rates of any jump. However, the opposite where -the jumps modify the ODE variables is allowed. In these cases, you should -observe an improved performance when using the `Coevolve`. +Note, when using `ConstantRateJump`s, `MassActionJump`s, and bounded +`VariableRateJump`s, the ODE derivative function `f(du, u, p, t)` should not +modify any states in `du` that the corresponding jump rate functions depend on. +However, the opposite where jumps modify the ODE variables is allowed. If one +needs to change a component of `u` in the ODE for which a rate function is +dependent, then one must use a general `VariableRateJump` as described in the +next section. -## [Adding a VariableRateJump that Depends on a Continuous Variable](@id VariableRateJumpSect) +## [Adding a general VariableRateJump that Depends on a Continuously Evolving Variable](@id VariableRateJumpSect) Now let's consider adding a reaction whose rate changes continuously with the differential equation. To continue our example, let there be a new reaction with rate depending on `u[4]` of the form ``u_4 \to u_4 + \textrm{I}``, with a @@ -667,7 +695,8 @@ end jump5 = VariableRateJump(rate5, affect5!) ``` Notice, since `rate5` depends on a variable that evolves continuously, and hence -is not constant between jumps, *we must use a `VariableRateJump`*. +is not constant between jumps, *we must use a general `VariableRateJump` without +upper/lower bounds*. Solving the equation is exactly the same: ```@example tut2 @@ -677,8 +706,8 @@ jump_prob = JumpProblem(prob, Direct(), jump, jump2, jump5) sol = solve(jump_prob, Tsit5()) plot(sol; label=["S(t)" "I(t)" "R(t)" "u₄(t)"]) ``` -*Note that `VariableRateJump`s require using a continuous problem, like an -ODE/SDE/DDE/DAE problem, and using floating point initial conditions.* +*Note that general `VariableRateJump`s require using a continuous problem, like +an ODE/SDE/DDE/DAE problem, and using floating point initial conditions.* Lastly, we are not restricted to ODEs. For example, we can solve the same jump problem except with multiplicative noise on `u[4]` by using an `SDEProblem` @@ -694,7 +723,7 @@ sol = solve(jump_prob, SRIW1()) plot(sol; label=["S(t)" "I(t)" "R(t)" "u₄(t)"]) ``` -For more details about `VariableRateJump`s see [Defining a Variable Rate +For more details about general `VariableRateJump`s see [Defining a Variable Rate Jump](@ref). diff --git a/src/jumps.jl b/src/jumps.jl index 5c7208ef3..d67deb7d4 100644 --- a/src/jumps.jl +++ b/src/jumps.jl @@ -128,6 +128,10 @@ function VariableRateJump(rate, affect!; interp_points, save_positions, abstol, reltol) end +""" +$(TYPEDEF) + +""" struct RegularJump{iip, R, C, MD} rate::R c::C @@ -421,7 +425,7 @@ struct JumpSet{T1, T2, T3, T4} <: AbstractJump variable_jumps::T1 """Collection of [`ConstantRateJump`](@ref)s""" constant_jumps::T2 - """Collection of [`RegularRateJump`](@ref)s""" + """Collection of [`RegularJump`](@ref)s""" regular_jump::T3 """Collection of [`MassActionJump`](@ref)s""" massaction_jump::T4 From bf6771a17c139ab9822f7a7cfd7b46e6f225397e Mon Sep 17 00:00:00 2001 From: gzagatti Date: Wed, 14 Dec 2022 12:18:08 +0800 Subject: [PATCH 53/72] fix error message. --- src/aggregators/coevolve.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/aggregators/coevolve.jl b/src/aggregators/coevolve.jl index 9bf245910..f1f091308 100644 --- a/src/aggregators/coevolve.jl +++ b/src/aggregators/coevolve.jl @@ -28,7 +28,7 @@ function CoevolveJumpAggregation(nj::Int, njt::T, et::T, crs::Vector{T}, sr::Not lrates, urates, Ls) where {T, S, F1, F2, RNG, U} if dep_graph === nothing if (get_num_majumps(maj) == 0) || !isempty(rs) - error("To use VariableRateJumps with the Queue Method algorithm a dependency graph between jumps must be supplied.") + error("To use Coevolve a dependency graph between jumps must be supplied.") else dg = make_dependency_graph(length(u), maj) end From ca46720f7caf65b2220a5f9acc1f19dfa42a04b1 Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Wed, 14 Dec 2022 12:22:54 -0500 Subject: [PATCH 54/72] change L to rateinterval --- src/SSA_stepper.jl | 9 +-- src/jumps.jl | 133 ++++++++++++++++++++++++++++++--------------- 2 files changed, 95 insertions(+), 47 deletions(-) diff --git a/src/SSA_stepper.jl b/src/SSA_stepper.jl index 28f3c99ae..dfbb945b2 100644 --- a/src/SSA_stepper.jl +++ b/src/SSA_stepper.jl @@ -1,12 +1,13 @@ """ $(TYPEDEF) -Highly efficient integrator for pure jump problems that involve only -`ConstantRateJump`s and/or `MassActionJump`s. +Highly efficient integrator for pure jump problems that involve only `ConstantRateJump`s, +`MassActionJump`s, and/or `VariableRateJump`s *with rate bounds*. ## Notes -- Only works with `JumProblem`s defined from `DiscreteProblem`s. -- Only works with collections of `ConstantRateJump`s, `VariableRateJump`s and `MassActionJump`s. +- Only works with `JumpProblem`s defined from `DiscreteProblem`s. +- Only works with collections of `ConstantRateJump`s, `MassActionJump`s, and + `VariableRateJump`s with rate bounds. - Only supports `DiscreteCallback`s for events. ## Examples diff --git a/src/jumps.jl b/src/jumps.jl index d67deb7d4..3ef34093b 100644 --- a/src/jumps.jl +++ b/src/jumps.jl @@ -33,51 +33,79 @@ end """ $(TYPEDEF) -Defines a jump process with a rate (i.e. hazard, intensity, or propensity) that -may explicitly depend on time. More precisely, one where the rate function is -allowed to change *between* the occurrence of jumps. For detailed examples and -usage information see the +Defines a jump process with a rate (i.e. hazard, intensity, or propensity) that may +explicitly depend on time. More precisely, one where the rate function is allowed to change +*between* the occurrence of jumps. For detailed examples and usage information see the - [Tutorial](https://docs.sciml.ai/JumpProcesses/stable/tutorials/discrete_stochastic_example/) +Note that two types of `VariableRateJump`s are currently supported, with different +performance charactertistics. +- A general `VariableRateJump` or `VariableRateJump` will refer to one in which only `rate` + and `affect` functions are specified. + + * These are the most general in what they can represent, but require the use of an + `ODEProblem` or `SDEProblem` whose underlying timestepper handles their evolution in + time (via the callback interface). + * This is the least performant jump type in simulations. + +- Bounded `VariableRateJump`s require passing the keyword arguments `urate` and + `rateinterval`, corresponding to functions `urate(u, p, t)` and `rateinterval(u, p, t)`, + see below. These must calculate a time window over which the rate function is bounded by a + constant (as long as any components of the state on which the upper bound function depends + do not change). One can also optionally provide a lower bound function, `lrate(u, p, t)` + via the `lrate` keyword argument, that can lead to increased performance. + + * Bounded `VariableRateJump`s can currently be used in the `Coevolve` aggregator, and + can therefore be efficiently simulated in pure-jump `DiscreteProblem`s using the + `SSAStepper` time-stepper. + * These can be substantially more performant than general `VariableRateJump`s without + the rate bound functions. + +The additional user provided functions leveraged by bounded `VariableRateJumps`, `urate(u, +p, t)`, `rateinterval(u, p, t)`, and the optional `lrate(u, p, t)` require that +- For `s` in `[t, t + rateinterval(u, p, t)]`, we should have that `lrate(u, p, t) <= + rate(u, p, s) <= urate(u, p, t)` provided any components of `u` on which these functions + depend remain unchanged. + ## Fields $(FIELDS) ## Examples -Suppose `u[1]` gives the amount of particles and `t*p[1]` the probability per -time each particle can decay away. A corresponding `VariableRateJump` for this -jump process is +Suppose `u[1]` gives the amount of particles and `t*p[1]` the probability per time each +particle can decay away. A corresponding `VariableRateJump` for this jump process is ```julia rate(u,p,t) = t*p[1]*u[1] affect!(integrator) = integrator.u[1] -= 1 vrj = VariableRateJump(rate, affect!) ``` -In case we want to use the `Coevolve` aggregator, we need to pass the rate -boundaries and interval for which the rates apply. The `Coevolve` aggregator -allow us to perform discrete steps with `SSAStepper()`. +In case we want to use the `Coevolve` aggregator, we need to pass the rate boundaries and +interval for which the rates apply. The `Coevolve` aggregator allow us to perform discrete +steps with `SSAStepper()`. ```julia -L(u,p,t) = (1/p[1])*2 -rate(u,p,t) = t*p[1]*u[1] +rateinterval(u,p,t) = (1 / p[1]) * 2 +rate(u,p,t) = t * p[1] * u[1] lrate(u, p, t) = rate(u, p, t) -urate(u,p,t) = rate(u, p, t + L(u,p,t)) +urate(u,p,t) = rate(u, p, t + rateinterval(u,p,t)) affect!(integrator) = integrator.u[1] -= 1 -vrj = VariableRateJump(rate, affect!; lrate=lrate, urate=urate, L=L) +vrj = VariableRateJump(rate, affect!; lrate = lrate, urate = urate, + rateinterval = rateinterval) ``` ## Notes -- When using the `Coevolve` aggregator, `DiscreteProblem` can be used. - Otherwise, `ODEProblem` or `SDEProblem` must be used to be correctly simulated. -- **When not using the `Coevolve` aggregator, `VariableRateJump`s result in - `integrator`s storing an effective state type that wraps the main state - vector.** See [`ExtendedJumpArray`](@ref) for details on using this object. Note - that the presence of *any* `VariableRateJump`s will result in all - `ConstantRateJump`, `VariableRateJump` and callback `affect!` functions - receiving an integrator with `integrator.u` an [`ExtendedJumpArray`](@ref). -- Salis H., Kaznessis Y., Accurate hybrid stochastic simulation of a system of - coupled chemical or biochemical reactions, Journal of Chemical Physics, 122 - (5), DOI:10.1063/1.1835951 is used for calculating jump times with - `VariableRateJump`s within ODE/SDE integrators. +- When using the `Coevolve` aggregator, `DiscreteProblem` can be used. Otherwise, + `ODEProblem` or `SDEProblem` must be used to be correctly simulated. +- **When not using the `Coevolve` aggregator, `VariableRateJump`s result in `integrator`s + storing an effective state type that wraps the main state vector.** See + [`ExtendedJumpArray`](@ref) for details on using this object. Note that the presence of + *any* `VariableRateJump`s will result in all `ConstantRateJump`, `VariableRateJump` and + callback `affect!` functions receiving an integrator with `integrator.u` an + [`ExtendedJumpArray`](@ref). +- Salis H., Kaznessis Y., Accurate hybrid stochastic simulation of a system of coupled + chemical or biochemical reactions, Journal of Chemical Physics, 122 (5), + DOI:10.1063/1.1835951 is used for calculating jump times with `VariableRateJump`s within + ODE/SDE integrators. """ struct VariableRateJump{R, F, R2, R3, R4, I, T, T2} <: AbstractJump """Function `rate(u,p,t)` that returns the jump's current rate given state @@ -86,21 +114,29 @@ struct VariableRateJump{R, F, R2, R3, R4, I, T, T2} <: AbstractJump """Function `affect!(integrator)` that updates the state for one occurrence of the jump given `integrator`.""" affect!::F - """When planning to use the `Coevolve` aggregator, function `lrate(u, p, - t)` that computes the lower bound of the rate in interval `t` to `t + L` at time - `t` given state `u`, parameters `p`. This is not required if using another - aggregator.""" + """Optional function `lrate(u, p, t)` that computes a lower bound on the rate in the + interval `t` to `t + rateinterval(u, p, t)` at time `t` given state `u` and parameters + `p`. The bound should hold over this time interval as long as components of `u` for + which the rate functions are dependent do not change. When using aggregators that + support bounded `VariableRateJump`s, currently only `Coevolve`, providing a lower-bound + can lead to improved performance. + """ lrate::R2 - """When planning to use the `Coevolve` aggregator, function `urate(u, p, - t)` that computes the upper bound of the rate in interval `t` to `t + L` at time - `t` given state `u`, parameters `p`. This is not required if using another - aggregator.""" + """Optional function `urate(u, p, t)` for general `VariableRateJump`s, but is required + to define a bounded `VariableRateJump`, which can be used with supporting aggregators, + currently only `Coevolve`, and offers improved computational performance. Computes an + upper bound for the rate in the interval `t` to `t + rateinterval(u, p, t)` at time `t` + given state `u` and parameters `p`. The bound should hold over this time interval as + long as components of `u` for which the rate functions are dependent do not change. """ urate::R3 - """When planning to use the `Coevolve` aggregator, function `L(u, p, - t)` that computes the interval length `L` starting at time `t` given state - `u`, parameters `p` for which the rate is bounded between `lrate` and - `urate`. This is not required if using another aggregator.""" - L::R4 + """Optional function `rateinterval(u, p, t)` for general `VariableRateJump`s, but is + required to define a bounded `VariableRateJump`, which can be used with supporting + aggregators, currently only `Coevolve`, and offers improved computational performance. + Computes the time interval from time `t` over which the `urate` and `lrate` bounds will + hold, `t` to `t + rateinterval(u, p, t)`, given state `u` and parameters `p`. The bound + should hold over this time interval as long as components of `u` for which the rate + functions are dependent do not change. """ + rateinterval::R4 idxs::I rootfind::Bool interp_points::Int @@ -109,22 +145,33 @@ struct VariableRateJump{R, F, R2, R3, R4, I, T, T2} <: AbstractJump reltol::T2 end +""" +``` +function VariableRateJump(rate, affect!; lrate = nothing, urate = nothing, + rateinterval = nothing, rootfind = true, + idxs = nothing, + save_positions = (false, true), + interp_points = 10, + abstol = 1e-12, reltol = 0) +``` +""" function VariableRateJump(rate, affect!; lrate = nothing, urate = nothing, - L = nothing, rootfind = true, + rateinterval = nothing, rootfind = true, idxs = nothing, save_positions = (false, true), interp_points = 10, abstol = 1e-12, reltol = 0) - if !(urate !== nothing && L !== nothing) && !(urate === nothing && L === nothing) - error("Either `urate` and `L` must be nothing, or both of them must be defined.") + if !(urate !== nothing && rateinterval !== nothing) && + !(urate === nothing && rateinterval === nothing) + error("`urate` and `rateinterval` must both be `nothing`, or must both be defined.") end if (urate !== nothing && lrate === nothing) lrate = (u, p, t) -> zero(typeof(t)) end - VariableRateJump(rate, affect!, lrate, urate, L, idxs, rootfind, + VariableRateJump(rate, affect!, lrate, urate, rateinterval, idxs, rootfind, interp_points, save_positions, abstol, reltol) end From e2718641d346417bc2bed7f93c9616301d118652 Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Wed, 14 Dec 2022 12:55:02 -0500 Subject: [PATCH 55/72] L -> rateinterval --- .../tutorials/discrete_stochastic_example.md | 11 ++--- src/aggregators/coevolve.jl | 41 +++++++------------ test/hawkes_test.jl | 11 +++-- 3 files changed, 26 insertions(+), 37 deletions(-) diff --git a/docs/src/tutorials/discrete_stochastic_example.md b/docs/src/tutorials/discrete_stochastic_example.md index 6b9a34436..9e6140a8b 100644 --- a/docs/src/tutorials/discrete_stochastic_example.md +++ b/docs/src/tutorials/discrete_stochastic_example.md @@ -510,28 +510,29 @@ define an infection reaction as a bounded `VariableRateJump`, requiring us to again provide `rate` and `affect` functions, but also give functions that calculate an upper-bound on the rate (`urate(u,p,t)`), an optional lower-bound on the rate (`lrate(u,p,t)`), and a time window over which the bounds are valid -as long as any states these three rates depend on are unchanged (`L(u,p,t)`). +as long as any states these three rates depend on are unchanged +(`rateinterval(u,p,t)`). The lower- and upper-bounds of the rate should be valid from the time they are -computed `t` until `t + L(u, p, t)`: +computed `t` until `t + rateinterval(u, p, t)`: ```@example tut2 H = zeros(Float64, 10) rate3(u, p, t) = p[1]*u[1]*u[2] + p[3]*u[1]*sum(exp(-p[4]*(t - _t)) for _t in H) lrate = rate1 # β*S*I urate = rate3 -L(u, p, t) = 1 / (2*urate(u, p, t)) +rateinterval(u, p, t) = 1 / (2*urate(u, p, t)) function affect3!(integrator) integrator.u[1] -= 1 # S -> S - 1 integrator.u[2] += 1 # I -> I + 1 push!(H, integrator.t) nothing end -jump3 = VariableRateJump(rate3, affect3!; lrate=lrate, urate=urate, L=L) +jump3 = VariableRateJump(rate3, affect3!; lrate, urate, rateinterval) ``` Note that here we set the lower bound rate to be the normal SIR infection rate, and set the upper bound rate equal to the new rate of infection (`rate3`). As long as `u[1]` and `u[2]` are unchanged by another jump, for any `s` in `[t,t + -L(u,p,t)]` we have that `lrate(u,p,t) <= rate3(u,p,s) <= urate(u,p,t)`. +rateinterval(u,p,t)]` we have that `lrate(u,p,t) <= rate3(u,p,s) <= urate(u,p,t)`. Next, we redefine the recovery jump's `affect!` such that a random infection is removed from `H` for every recovery. diff --git a/src/aggregators/coevolve.jl b/src/aggregators/coevolve.jl index f1f091308..557592923 100644 --- a/src/aggregators/coevolve.jl +++ b/src/aggregators/coevolve.jl @@ -18,14 +18,13 @@ mutable struct CoevolveJumpAggregation{T, S, F1, F2, RNG, GR, PQ} <: pq::PQ # priority queue of next time lrates::F1 # vector of rate lower bound functions urates::F1 # vector of rate upper bound functions - Ls::F1 # vector of interval length functions + rateintervals::F1 # vector of interval length functions end function CoevolveJumpAggregation(nj::Int, njt::T, et::T, crs::Vector{T}, sr::Nothing, maj::S, rs::F1, affs!::F2, sps::Tuple{Bool, Bool}, - rng::RNG; u::U, - dep_graph = nothing, - lrates, urates, Ls) where {T, S, F1, F2, RNG, U} + rng::RNG; u::U, dep_graph = nothing, lrates, urates, + rateintervals) where {T, S, F1, F2, RNG, U} if dep_graph === nothing if (get_num_majumps(maj) == 0) || !isempty(rs) error("To use Coevolve a dependency graph between jumps must be supplied.") @@ -47,22 +46,14 @@ function CoevolveJumpAggregation(nj::Int, njt::T, et::T, crs::Vector{T}, sr::Not pq = MutableBinaryMinHeap{T}() CoevolveJumpAggregation{T, S, F1, F2, RNG, typeof(dg), - typeof(pq) - }(nj, nj, njt, - et, - crs, sr, maj, - rs, - affs!, sps, - rng, - dg, pq, - lrates, urates, Ls) + typeof(pq)}(nj, nj, njt, et, crs, sr, maj, rs, affs!, sps, rng, + dg, pq, lrates, urates, rateintervals) end # creating the JumpAggregation structure (tuple-based variable jumps) function aggregate(aggregator::Coevolve, u, p, t, end_time, constant_jumps, - ma_jumps, save_positions, rng; - dep_graph = nothing, variable_jumps = nothing, - kwargs...) + ma_jumps, save_positions, rng; dep_graph = nothing, + variable_jumps = nothing, kwargs...) AffectWrapper = FunctionWrappers.FunctionWrapper{Nothing, Tuple{Any}} RateWrapper = FunctionWrappers.FunctionWrapper{typeof(t), Tuple{typeof(u), typeof(p), typeof(t)}} @@ -70,7 +61,7 @@ function aggregate(aggregator::Coevolve, u, p, t, end_time, constant_jumps, rates = Vector{RateWrapper}() lrates = Vector{RateWrapper}() urates = Vector{RateWrapper}() - Ls = Vector{RateWrapper}() + rateintervals = Vector{RateWrapper}() if (constant_jumps !== nothing) && !isempty(constant_jumps) append!(affects!, @@ -86,7 +77,7 @@ function aggregate(aggregator::Coevolve, u, p, t, end_time, constant_jumps, append!(rates, [RateWrapper(j.rate) for j in variable_jumps]) append!(lrates, [RateWrapper(j.lrate) for j in variable_jumps]) append!(urates, [RateWrapper(j.urate) for j in variable_jumps]) - append!(Ls, [RateWrapper(j.L) for j in variable_jumps]) + append!(rateintervals, [RateWrapper(j.rateinterval) for j in variable_jumps]) end num_jumps = get_num_majumps(ma_jumps) + length(urates) @@ -96,9 +87,7 @@ function aggregate(aggregator::Coevolve, u, p, t, end_time, constant_jumps, next_jump_time = typemax(t) CoevolveJumpAggregation(next_jump, next_jump_time, end_time, cur_rates, sum_rate, ma_jumps, rates, affects!, save_positions, rng; - u = u, - dep_graph = dep_graph, - lrates = lrates, urates = urates, Ls = Ls) + u, dep_graph, lrates, urates, rateintervals) end # set up a new simulation and calculate the first jump / jump time @@ -144,8 +133,8 @@ end @inbounds return p.urates[uidx](u, params, t) end -@inline function get_L(p::CoevolveJumpAggregation, lidx, u, params, t) - @inbounds return p.Ls[lidx](u, params, t) +@inline function get_rateinterval(p::CoevolveJumpAggregation, lidx, u, params, t) + @inbounds return p.rateintervals[lidx](u, params, t) end @inline function get_lrate(p::CoevolveJumpAggregation, lidx, u, params, t) @@ -173,9 +162,9 @@ function next_time(p::CoevolveJumpAggregation{T}, u, params, t, i, tstop::T) whe _t = t + s if lidx > 0 while t < tstop - L = get_L(p, lidx, u, params, t) - if s > L - t = t + L + rateinterval = get_rateinterval(p, lidx, u, params, t) + if s > rateinterval + t = t + rateinterval urate = get_urate(p, uidx, u, params, t) s = urate == zero(t) ? typemax(t) : randexp(rng) / urate _t = t + s diff --git a/test/hawkes_test.jl b/test/hawkes_test.jl index 121a4b9f7..547e20316 100644 --- a/test/hawkes_test.jl +++ b/test/hawkes_test.jl @@ -36,7 +36,7 @@ function hawkes_jump(i::Int, g, h) rate = hawkes_rate(i, g, h) lrate(u, p, t) = p[1] urate = rate - function L(u, p, t) + function rateinterval(u, p, t) _lrate = lrate(u, p, t) _urate = urate(u, p, t) return _urate == _lrate ? typemax(t) : 1 / (2 * _urate) @@ -45,7 +45,7 @@ function hawkes_jump(i::Int, g, h) push!(h[i], integrator.t) integrator.u[i] += 1 end - return VariableRateJump(rate, affect!; lrate = lrate, urate = urate, L = L) + return VariableRateJump(rate, affect!; lrate, urate, rateinterval) end function hawkes_jump(u, g, h) @@ -57,8 +57,7 @@ function hawkes_problem(p, agg::Coevolve; u = [0.0], tspan = (0.0, 50.0), g = [[1]], h = [[]]) dprob = DiscreteProblem(u, tspan, p) jumps = hawkes_jump(u, g, h) - jprob = JumpProblem(dprob, agg, jumps...; - dep_graph = g, save_positions = save_positions, rng = rng) + jprob = JumpProblem(dprob, agg, jumps...; dep_graph = g, save_positions, rng) return jprob end @@ -72,7 +71,7 @@ function hawkes_problem(p, agg; u = [0.0], tspan = (0.0, 50.0), g = [[1]], h = [[]]) oprob = ODEProblem(f!, u, tspan, p) jumps = hawkes_jump(u, g, h) - jprob = JumpProblem(oprob, agg, jumps...; save_positions = save_positions, rng = rng) + jprob = JumpProblem(oprob, agg, jumps...; save_positions, rng) return jprob end @@ -102,7 +101,7 @@ algs = (Direct(), Coevolve()) Nsims = 250 for alg in algs - jump_prob = hawkes_problem(p, alg; u = u0, tspan = tspan, g = g, h = h) + jump_prob = hawkes_problem(p, alg; u = u0, tspan, g, h) if typeof(alg) <: Coevolve stepper = SSAStepper() else From 8c393e287f9d772644fe08fcdfa99cbe3bb7e5ba Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Wed, 14 Dec 2022 13:01:06 -0500 Subject: [PATCH 56/72] add global nullrate function --- src/jumps.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/jumps.jl b/src/jumps.jl index 3ef34093b..ccb3fcc6c 100644 --- a/src/jumps.jl +++ b/src/jumps.jl @@ -145,6 +145,8 @@ struct VariableRateJump{R, F, R2, R3, R4, I, T, T2} <: AbstractJump reltol::T2 end +nullrate(u, p, t::T) where {T <: Number} = zero(T) + """ ``` function VariableRateJump(rate, affect!; lrate = nothing, urate = nothing, @@ -168,7 +170,7 @@ function VariableRateJump(rate, affect!; end if (urate !== nothing && lrate === nothing) - lrate = (u, p, t) -> zero(typeof(t)) + lrate = nullrate end VariableRateJump(rate, affect!, lrate, urate, rateinterval, idxs, rootfind, From a6f8889a6ddce7fe44f31201954e42bb7d3870f1 Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Wed, 14 Dec 2022 13:15:49 -0500 Subject: [PATCH 57/72] format --- src/jumps.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jumps.jl b/src/jumps.jl index ccb3fcc6c..dc05b29fe 100644 --- a/src/jumps.jl +++ b/src/jumps.jl @@ -165,7 +165,7 @@ function VariableRateJump(rate, affect!; interp_points = 10, abstol = 1e-12, reltol = 0) if !(urate !== nothing && rateinterval !== nothing) && - !(urate === nothing && rateinterval === nothing) + !(urate === nothing && rateinterval === nothing) error("`urate` and `rateinterval` must both be `nothing`, or must both be defined.") end From b0259a06b52cbf99ffcb6ec4a6ce604bdf4c19b6 Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Thu, 22 Dec 2022 14:06:11 -0500 Subject: [PATCH 58/72] more doc updates --- docs/src/api.md | 6 +- docs/src/faq.md | 56 +++++++++-------- docs/src/jump_solve.md | 62 ++++++++++--------- docs/src/jump_types.md | 19 +++--- .../tutorials/discrete_stochastic_example.md | 15 ++--- docs/src/tutorials/jump_diffusion.md | 7 ++- 6 files changed, 88 insertions(+), 77 deletions(-) diff --git a/docs/src/api.md b/docs/src/api.md index 03361680f..2b5706c58 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -13,16 +13,17 @@ reset_aggregated_jumps! ## Types of Jumps ```@docs ConstantRateJump -VariableRateJump MassActionJump +VariableRateJump JumpSet ``` ## Aggregators Aggregators are the underlying algorithms used for sampling -[`MassActionJump`](@ref)s, [`ConstantRateJump`](@ref)s and +[`ConstantRateJump`](@ref)s, [`MassActionJump`](@ref)s, and [`VariableRateJump`](@ref)s. ```@docs +Coevolve Direct DirectCR FRM @@ -31,7 +32,6 @@ RDirect RSSA RSSACR SortingDirect -Coevolve ``` # Private API Functions diff --git a/docs/src/faq.md b/docs/src/faq.md index f956925b9..368e2d9c5 100644 --- a/docs/src/faq.md +++ b/docs/src/faq.md @@ -1,19 +1,20 @@ # FAQ ## My simulation is really slow and/or using a lot of memory, what can I do? -Exact methods simulate each and every jump. To reduce memory use, use -`save_positions=(false,false)` in the `JumpProblem` constructor as described -[earlier](@ref save_positions_docs) to turn off saving the system state before -and after every jump. You often do not need to save all jump poisitons when you -only want to track cumulative counts. Combined with use of `saveat` in the call -to `solve` this can dramatically reduce memory usage. - -While `Direct` is often fastest for systems with 10 or less `ConstantRateJump`s, -`VariableRateJump`s, and/or or `MassActionJump`s, if your system has many jumps -or one jump occurs most frequently, other stochastic simulation algorithms may -be faster. See [Jump Aggregators for Exact Simulation](@ref) and the subsequent -sections there for guidance on choosing different SSAs (called aggregators in -JumpProcesses). +Exact methods simulate every jump, and by default save the state before and +after each jump. To reduce memory use, use `save_positions = (false, false)` in +the `JumpProblem` constructor as described [earlier](@ref save_positions_docs) +to turn off saving the system state before and after every jump. Combined with +use of `saveat` in the call to `solve`, to specify the specific times at which +to save the state, this can dramatically reduce memory usage. + +While `Direct` is often fastest for systems with 10 or less `ConstantRateJump`s +and/or `MassActionJump`s, if your system has many jumps or one jump occurs most +frequently, other stochastic simulation algorithms may be faster. See [Jump +Aggregators for Exact Simulation](@ref) and the subsequent sections there for +guidance on choosing different SSAs (called aggregators in JumpProcesses). For +systems with bounded `VariableRateJump`s using `Coevolve` with `SSAStepper` +instead of an ODE/SDE time stepper can give a significant performance boost. ## When running many consecutive simulations, for example within an `EnsembleProblem` or loop, how can I update `JumpProblem`s? @@ -25,8 +26,9 @@ internal aggregators for each new parameter value or initial condition. ## How can I define collections of many different jumps and pass them to `JumpProblem`? We can use `JumpSet`s to collect jumps together, and then pass them into -`JumpProblem`s directly. For example, using the `ConstantRateJump`, -`VariableRateJump` and `MassActionJump` defined earlier we can write +`JumpProblem`s directly. For example, using a `MassActionJump` and +`ConstantRateJump` defined in the [second tutorial](@ref ssa_tutorial), we can +write ```julia jset = JumpSet(mass_act_jump, birth_jump) @@ -45,8 +47,8 @@ vj1 = VariableRateJump(rate3, affect3!) vj2 = VariableRateJump(rate4, affect4!) vjtuple = (vj1, vj2) -jset = JumpSet(; constant_jumps=cjvec, variable_jumps=vjtuple, - massaction_jumps=mass_act_jump) +jset = JumpSet(; constant_jumps = cjvec, variable_jumps = vjtuple, + massaction_jumps = mass_act_jump) ``` ## How can I set the random number generator used in the jump process sampling algorithms (SSAs)? @@ -69,15 +71,15 @@ default. On versions below 1.7 it uses `Xoroshiro128Star`. ## What are these aggregators and aggregations in JumpProcesses? JumpProcesses provides a variety of methods for sampling the time the next -`ConstantRateJump`, `VariableRateJump` or `MassActionJump` occurs, and which +`ConstantRateJump`, `MassActionJump`, or `VariableRateJump` occurs, and which jump type happens at that time. These methods are examples of stochastic simulation algorithms (SSAs), also known as Gillespie methods, Doob's method, or -Kinetic Monte Carlo methods. These are catch-all terms for jump (or point) -processes simulation methods most commonly used in the biochemistry literature. -In the JumpProcesses terminology we call such methods "aggregators", and the -cache structures that hold their basic data "aggregations". See [Jump -Aggregators for Exact Simulation](@ref) for a list of the available SSA -aggregators. +Kinetic Monte Carlo methods. These are all names for jump (or point) processes +simulation methods used across the biology, chemistry, engineering, mathematics, +and physics literature. In the JumpProcesses terminology we call such methods +"aggregators", and the cache structures that hold their basic data +"aggregations". See [Jump Aggregators for Exact Simulation](@ref) for a list of +the available SSA aggregators. ## How should jumps be ordered in dependency graphs? Internally, JumpProcesses SSAs (aggregators) order all `MassActionJump`s first, @@ -108,10 +110,10 @@ then follow this ordering when assigning an integer id to each jump. See also [Jump Aggregators Requiring Dependency Graphs](@ref) for more on dependency graphs needed for the various SSAs. -## How do I use callbacks with `ConstantRateJump`, `VariableRateJump` or `MassActionJump` systems? +## How do I use callbacks with jump simulations? -Callbacks can be used with `ConstantRateJump`s, `VariableRateJump`s and -`MassActionJump`s. When solving a pure jump system with `SSAStepper`, only +Callbacks can be used with `ConstantRateJump`s, `MassActionJump`s, and +`VariableRateJump`s. When solving a pure jump system with `SSAStepper`, only discrete callbacks can be used (otherwise a different time stepper is needed). When using an ODE or SDE time stepper any callback should work. diff --git a/docs/src/jump_solve.md b/docs/src/jump_solve.md index 3c9f55932..9c6f04615 100644 --- a/docs/src/jump_solve.md +++ b/docs/src/jump_solve.md @@ -6,31 +6,36 @@ solve(prob::JumpProblem,alg;kwargs) ## Recommended Methods -Because `JumpProblem`s can be solved with two classes of methods, exact and -inexact, they come in two forms. Exact algorithms tend to describe the -realization of each jump chronologically. Alternatively, inexact methods tend to -take small leaps through time so they are guaranteed to terminate in finite -time. These methods can be much faster as they only simulate the total number of -points in each leap interval and thus do not need to simulate the realization of -every single jump. Jumps for exact methods can be defined with -`ConstantRateJump`, `VariableRateJump` and/or `MassActionJump` On the other -hand, jumps for inexact methods are defined with `RegularJump`. - -There are special algorithms available for a pure exact `JumpProblem` (a -`JumpProblem` over a `DiscreteProblem`). The `SSAStepper()` is an efficient -streamlined integrator for running simulation algorithms of such problems. This -integrator is named after the term Stochastic Simulation Algorithm (SSA) which -is a catch-all term in biochemistry to denote algorithms for simulating jump -processes. In turn, we denote aggregators algorithms for simulating jump -processes that can use the `SSAStepper()` integrator. These algorithms can solve -problems initialized with `ConstantRateJump`, `VariableRateJump` and/or -`MassActionJump`. Although `SSAStepper()` is usually faster, it is not -compatible with event handling. If events are necessary, then `FunctionMap` does -well. - -If there is a `RegularJump`, then inexact methods must be used. The current -recommended method is `TauLeaping` if you need adaptivity, events, etc. If you -just need the most barebones fixed time step leaping method, then +`JumpProblem`s can be solved with two classes of methods, exact and inexact. +Exact algorithms currently sample realizations of the jump processes in +chronological order, executing individual jumps sequentially at randomly sampled +times. In contrast, inexact (τ-leaping) methods are time-step based, executing +multiple occurrences of jumps during each time-step. These methods can be much +faster as they only simulate the total number of jumps over each leap interval, +and thus do not need to simulate the realization of every single jump. Jumps for +use with exact simulation methods can be defined as `ConstantRateJump`s, +`MassActionJump`s, and/or `VariableRateJump`. Jumps for use with inexact +τ-leaping methods should be defined as `RegularJump`s. + +There are special algorithms available for efficiently simulating an exact, pure +`JumpProblem` (i.e. a `JumpProblem` over a `DiscreteProblem`). `SSAStepper()` +is an efficient streamlined integrator for time stepping such problems from +individual jump to jump. This integrator is named after Stochastic Simulation +Algorithms (SSAs), commonly used naming in chemistry and biology applications +for the class of exact jump process simulation algorithms. In turn, we denote by +"aggregators" the algorithms that `SSAStepper` calls to calculate the next jump +time and to execute a jump (i.e. change the system state appropriately). All +JumpProcesses aggregators can be used with `ConstantRateJump`s and +`MassActionJump`s, with a subset of aggregators also working with bounded + `VariableRateJump`s (see [the first tutorial](@ref poisson_proc_tutorial) for +the definition of bounded `VariableRateJump`s). Although `SSAStepper()` is +usually faster, it only supports discrete events (`DiscreteCallback`s), for pure +jump problems requiring continuous events (`ContinuousCallback`s) the less +performant `FunctionMap` time-stepper can be used. + +If there is a `RegularJump`, then inexact τ-leaping methods must be used. The +current recommended method is `TauLeaping` if one needs adaptivity, events, etc. +If ones only needs the most barebones fixed time-step leaping method, then `SimpleTauLeaping` can have performance benefits. ## Special Methods for Pure Jump Problems @@ -41,9 +46,10 @@ algorithms are optimized for pure jump problems. ### JumpProcesses.jl -- `SSAStepper`: a stepping integrator for pure `ConstantRateJump`, - `VariableRateJump` and/or `MassActionJump` `JumpProblem`s. Supports handling - of `DiscreteCallback` and saving controls like `saveat`. +- `SSAStepper`: a stepping integrator for `JumpProblem`s defined over + `DiscreteProblem`s involving `ConstantRateJump`s, `MassActionJump`s, and/or + bounded `VariableRateJump`s . Supports handling of `DiscreteCallback`s and + saving controls like `saveat`. ## RegularJump Compatible Methods diff --git a/docs/src/jump_types.md b/docs/src/jump_types.md index 1a049de11..c471c59ea 100644 --- a/docs/src/jump_types.md +++ b/docs/src/jump_types.md @@ -2,17 +2,18 @@ ### Mathematical Specification of an problem with jumps -Jumps (or point) processes are stochastic processes with discrete changes driven -by some `rate`. The homogeneous Poisson process is the canonical point process -with a constant rate of change. Processes involving multiple jumps are known as -compound jump (or point) processes. +Jumps (or point) processes are stochastic processes with discrete state changes +driven by a `rate` function. The homogeneous Poisson process is the canonical +point process with a constant rate of change. Processes involving multiple jumps +are known as compound jump (or point) processes. A compound Poisson process is a continuous-time Markov Chain where the time to -the next jump is exponentially distributed as calculated by the rate. This type -of process is known in biology as "Gillespie discrete stochastic simulation", -modeled by the Chemical Master Equation (CME). Alternatively, in the statistics -literature the composition of Poisson processes is described by the -superposition theorem. +the next jump is exponentially distributed as determined by the rate. Simulation +algorithms for these types of processes are known in biology and chemistry as +Gillespie methods or Stochastic Simulation Algorithms (SSA), with the time +evolution that the probability these processes are an a given state at a given +time satisfying the Chemical Master Equation (CME). In the statistics literature, +the composition of Poisson processes is described by the superposition theorem. Any differential equation can be extended by jumps. For example, we have an ODE with jumps, denoted by diff --git a/docs/src/tutorials/discrete_stochastic_example.md b/docs/src/tutorials/discrete_stochastic_example.md index 9e6140a8b..b97631aed 100644 --- a/docs/src/tutorials/discrete_stochastic_example.md +++ b/docs/src/tutorials/discrete_stochastic_example.md @@ -511,9 +511,8 @@ again provide `rate` and `affect` functions, but also give functions that calculate an upper-bound on the rate (`urate(u,p,t)`), an optional lower-bound on the rate (`lrate(u,p,t)`), and a time window over which the bounds are valid as long as any states these three rates depend on are unchanged -(`rateinterval(u,p,t)`). -The lower- and upper-bounds of the rate should be valid from the time they are -computed `t` until `t + rateinterval(u, p, t)`: +(`rateinterval(u,p,t)`). The lower- and upper-bounds of the rate should be valid +from the time they are computed `t` until `t + rateinterval(u, p, t)`: ```@example tut2 H = zeros(Float64, 10) @@ -531,8 +530,9 @@ jump3 = VariableRateJump(rate3, affect3!; lrate, urate, rateinterval) ``` Note that here we set the lower bound rate to be the normal SIR infection rate, and set the upper bound rate equal to the new rate of infection (`rate3`). As -long as `u[1]` and `u[2]` are unchanged by another jump, for any `s` in `[t,t + -rateinterval(u,p,t)]` we have that `lrate(u,p,t) <= rate3(u,p,s) <= urate(u,p,t)`. +required for bounded `VariableRateJump`s, we have for any `s` in `[t,t + +rateinterval(u,p,t)]` the bound `lrate(u,p,t) <= rate3(u,p,s) <= urate(u,p,t)` +will hold provided the dependent states `u[1]` and `u[2]` have not changed. Next, we redefine the recovery jump's `affect!` such that a random infection is removed from `H` for every recovery. @@ -554,8 +554,9 @@ Bounded `VariableRateJump`s over a `DiscreteProblem` can currently only be simulated with the `Coevolve` aggregator. The aggregator requires a dependency graph to indicate when a given jump occurs which other jumps in the system should have their rate recalculated (i.e. their rate depends on states modified -by one occurrence of the first jump). In this case, both processes mutually -affect each other so we have +by one occurrence of the first jump). This ensures that rates, rate bounds, and +rate intervals are recalculated when invalidated due to changes in `u`. For the +current example, both processes mutually affect each other so we have ```@example tut2 dep_graph = [[1,2], [1,2]] diff --git a/docs/src/tutorials/jump_diffusion.md b/docs/src/tutorials/jump_diffusion.md index ff459720c..462ce72e0 100644 --- a/docs/src/tutorials/jump_diffusion.md +++ b/docs/src/tutorials/jump_diffusion.md @@ -120,9 +120,10 @@ plot(sol) In this way we have solve a mixed jump-ODE, i.e. a piecewise deterministic Markov process. -Note that in this case, the rate of the `VariableRateJump`s depend on a variable -that is driven by an `ODEProblem`, thus we cannot use the `Coevolve` to solve -the jump problem. +Note that in this case, the rate of the `VariableRateJump`s depends on a +variable that is driven by an `ODEProblem`, and thus they would not satisfy the +conditions to be represented as bounded `VariableRateJump`s (and hence can not +be simulated with the `Coevolve` aggregator). ## Jump Diffusion From 199564fac404b0341db79548049fcee78b339f16 Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Thu, 22 Dec 2022 14:13:19 -0500 Subject: [PATCH 59/72] fix typos --- docs/src/tutorials/jump_diffusion.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/tutorials/jump_diffusion.md b/docs/src/tutorials/jump_diffusion.md index 462ce72e0..de889168f 100644 --- a/docs/src/tutorials/jump_diffusion.md +++ b/docs/src/tutorials/jump_diffusion.md @@ -120,7 +120,7 @@ plot(sol) In this way we have solve a mixed jump-ODE, i.e. a piecewise deterministic Markov process. -Note that in this case, the rate of the `VariableRateJump`s depends on a +Note that in this case, the rates of the `VariableRateJump`s depend on a variable that is driven by an `ODEProblem`, and thus they would not satisfy the conditions to be represented as bounded `VariableRateJump`s (and hence can not be simulated with the `Coevolve` aggregator). From 5dda9b83e75afce8f6a2cf6b5b70421443628966 Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Wed, 28 Dec 2022 15:13:03 -0500 Subject: [PATCH 60/72] more doc updates --- docs/src/jump_types.md | 384 ++++++++++++++++++++++++----------------- src/jumps.jl | 55 +++--- 2 files changed, 255 insertions(+), 184 deletions(-) diff --git a/docs/src/jump_types.md b/docs/src/jump_types.md index c471c59ea..4d3234127 100644 --- a/docs/src/jump_types.md +++ b/docs/src/jump_types.md @@ -1,6 +1,6 @@ # [Jump Problems](@id jump_problem_type) -### Mathematical Specification of an problem with jumps +## Mathematical Specification of a problem with jumps Jumps (or point) processes are stochastic processes with discrete state changes driven by a `rate` function. The homogeneous Poisson process is the canonical @@ -11,7 +11,7 @@ A compound Poisson process is a continuous-time Markov Chain where the time to the next jump is exponentially distributed as determined by the rate. Simulation algorithms for these types of processes are known in biology and chemistry as Gillespie methods or Stochastic Simulation Algorithms (SSA), with the time -evolution that the probability these processes are an a given state at a given +evolution that the probability these processes are in a given state at a given time satisfying the Chemical Master Equation (CME). In the statistics literature, the composition of Poisson processes is described by the superposition theorem. @@ -27,108 +27,103 @@ a stochastic differential equation to have jumps is commonly known as a Jump Diffusion, and is denoted by ```math -du = f(u,p,t)dt + \sum_{j}g_j(u,t)dW_j(t) + \sum_{i}c_i(u,p,t)dp_i(t) +du(t) = f(u,p,t)dt + \sum_{j}g_j(u,t)dW_j(t) + \sum_{i}c_i(u,p,t)dp_i(t) ``` -## Types of Jumps: Regular, Variable, Constant Rate and Mass Action - - -Exact algorithms tend to describe the realization of each jump chronologically. -In more complex cases, such jumps are conditioned on the history of past events. -Such jumps are usually associated with changes to the state variable `u` which -in turn changes the `rate` of event occurence. These jumps can be specified as -a `ConstantRateJump`, `MassActionJump`, or a `VariableRateJump`. Since exact -methods simulate each and every point, they might face termination issues when -the `rate` of event occurrence explodes. - -Alternatively, inexact methods tend to take small leaps through time so they are -guaranteed to terminate in finite time. These methods can be much faster as they -only simulate the total number of points in each leap interval and thus do not -need to simulate the realization of every single jump. Since inexact methods -trade accuracy for speed, they should be used when a set of jumps do not make -significant changes to the system during the leap interval. A `RegularJump` is -used for inexact algorithms. Note that inexact methods are not always -inaccurate. In the case of homogeneous Poisson processes, they produce accurate -results. However, they can produce less accurate results for more complex -problems, thus it is important to have a good understanding of the problem. As -a rule of thumb, if changes to the state variable `u` during a leap are minimal -compared to size of the system, an inexact method should provide reasonable -solutions. - -We denote a jump as variable if its rate function is dependent on values -which may change between any jump in the system. For instance, when the rate is -a function of time. Variable jumps can be more expensive to simulate because it -is necessary to take into account the dynamics of the rate function when -simulating the next jump time. - -A `MassActionJump` is a specialized representation for a collection of constant -rate 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. Note, only one `MassActionJump` should be -defined per `JumpProblem`; it is then responsible for handling all mass action +## Types of Jumps: 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`](@ref), +[`MassActionJump`](@ref), or a [`VariableRateJump`](@ref). + +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 later specifies the instantaneous change in the state of the system +when the jump occurs. + +A specific jump type is a [`VariableRateJump`](@ref) 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 take into account the (possibly +continuous) changes in the rate function when calculating the next jump time. + +*Bounded* [`VariableRateJump`](@ref)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`](@ref)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`](@ref)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 comnpared to using +multiple `ConstantRateJump`s. Note, only one `MassActionJump` should be defined +per [`JumpProblem`](@ref); 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 `VariableRateJump` to handle the +create a number of `ConstantRateJump`s or `VariableRateJump`s to handle the non-mass action jumps. -`RegularJump`s are optimized for inexact jumping algorithms like tau-leaping and -hybrid algorithms. `ConstantRateJump`, `VariableRateJump`, `MassActionJump` are -optimized for exact methods (also known in the biochemistry literature as SSA -algorithms). `ConstantRateJump`s, `VariableRateJump`s and `MassActionJump`s can -be added to standard DiffEq algorithms since they are simply callbacks, while -`RegularJump`s require special algorithms. +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`](@ref) 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 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: +The constructor for a [`ConstantRateJump`](@ref) is: ```julia -ConstantRateJump(rate,affect!) +ConstantRateJump(rate, affect!) ``` -- `rate(u,p,t)` is a function which calculates the rate given the time and the state. -- `affect!(integrator)` is the effect on the equation, using the integrator interface. - -#### Defining a Variable Rate Jump - -The constructor for a `VariableRateJump` is: - -```julia -VariableRateJump(rate,affect!; - lrate=nothing, urate=nothing, L=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 time and the state. -- `affect!(integrator)` is the effect on the equation, using the integrator interface. -- When planning to use the `Coevolve` aggregator, the arguments `lrate`, - `urate` and `L` are required. They consist of three functions: `lrate(u, p, t)` - computes the lower bound of the intensity rate in the interval `t` to `t + L` - given state `u` and parameters `p`; `urate(u, p, t)` computes the upper - bound of the intensity rate; and `L(u, p, t)` computes the interval length - for which the rate is bounded between `lrate` and `urate`. -- It is only possible to solve a `VariableRateJump` with `SSAStepper` when using - the `Coevolve` aggregator. -- When using a different aggregator than `Coevolve`, there is no need to - define `lrate`, `urate` and `L`. Note that in this case, the problem can only - be solved with continuous integration. Internally, `VariableRateJump` is - transformed into a `ContinuousCallback`. The `rate(u, p, t)` is used to - construct the `condition` function that triggers the callback. +- `rate(u, p, t)` is a function which calculates the rate given the current + state `u`, parameters `p`, and time `t`. +- `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: +The constructor for a [`MassActionJump`](@ref) is: ```julia MassActionJump(reactant_stoich, net_stoich; scale_rates = true, param_idxs=nothing) ``` - `reactant_stoich` is a vector whose `k`th entry is the reactant stoichiometry of the `k`th reaction. The reactant stoichiometry for an individual reaction - is assumed to be represented as a vector of `Pair`s, mapping species id to - stoichiometric coefficient. + is assumed to be represented as a vector of `Pair`s, mapping species integer + id to stoichiometric coefficient. - `net_stoich` is assumed to have the same type as `reactant_stoich`; a vector whose `k`th entry is the net stoichiometry of the `k`th reaction. The net stoichiometry for an individual reaction is again represented as a vector @@ -136,7 +131,7 @@ MassActionJump(reactant_stoich, net_stoich; scale_rates = true, param_idxs=nothi 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`, + 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. @@ -150,12 +145,13 @@ MassActionJump(reactant_stoich, net_stoich; scale_rates = true, param_idxs=nothi ``3A \overset{k}{\rightarrow} B`` the rate function would be `k*A*(A-1)*(A-2)/3!`. To *avoid* having the reaction rates rescaled (by `1/2` and `1/6` for these two examples), one can pass the `MassActionJump` - constructor the optional named parameter `scale_rates=false`, i.e. use + constructor the optional named parameter `scale_rates = false`, i.e. use ```julia MassActionJump(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 rate `k=1`: + Consider the ``\varnothing \overset{k}{\rightarrow} A`` reaction with rate + `k=1`: ```julia p = [1.] reactant_stoich = [[0 => 1]] @@ -179,47 +175,105 @@ MassActionJump(reactant_stoich, net_stoich; scale_rates = true, param_idxs=nothi reactant_stoich = [[3 => 1, 1 => 2, 4 => 2], [3 => 2, 2 => 2]] ``` +#### Defining a Variable Rate Jump + +The constructor for a [`VariableRateJump`](@ref) is: + +```julia +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 + state `u`, parameters `p`, and time `t`. +- `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 + interval `t` to `t + rateinterval(u, p, t)` at time `t` given state `u` and + parameters `p`. +- `rateinterval(u, p, t)`, a function which computes a time interval `t` to `t + + rateinterval(u, p, t)` given state `u` and parameters `p` over which the + `urate` bound will hold (and `lrate` bound if provided, see below). + +Note that it is ok if the `urate` bound would be violated within the +`rateinterval` due to a change in `u` arising from another `ConstantRateJump`, +`MassActionJump` or *bounded* `VariableRateJump` being executed, as the chosen +aggregator will then handle recalculating the rate bound and interval. *However, +if the bound could be violated within the time interval due to a change in `u` +arising from continuous dynamics such as a coupled ODE, SDE, or a general +`VariableRateJump`, bounds should not be given.* This ensures the jump is +classified as a general `VariableRateJump` and properly handled. + +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 + interval `t` to `t + rateinterval(u, p, t)` at time `t` given state `u` and + parameters `p`. `lrate` should remain valid under the same conditions as + `urate`. + +Note that +- It is currently only possible to simulate `VariableRateJump`s with + `SSAStepper` when using systems with only bounded `VariableRateJump`s and the + `Coevolve` aggregator. +- When choosing a different aggregator than `Coevolve`, `SSAStepper` can not + currently be used, and the `JumpProblem` must be coupled to a continuous + problem type such as an `ODEProblem` to handle time-stepping. The continuous + time-stepper treats *all* `VariableRateJump`s as `ContinuousCallback`s, using + the `rate(u, p, t)` function to construct the `condition` function that + triggers a callback. + + #### Defining a Regular Jump -The constructor for a `RegularJump` is: +The constructor for a [`RegularJump`](@ref) is: ```julia -RegularJump(rate,c,numjumps;mark_dist = nothing) +RegularJump(rate, c, numjumps; mark_dist = nothing) ``` -- `rate(out,u,p,t)` is the function which computes the rate for every regular +- `rate(out, u, p, t)` is the function which computes the rate for every regular jump process -- `c(du,u,p,t,counts,mark)` calculates the update given `counts` number of +- `c(du, u, p, t, counts, mark)` calculates the update given `counts` number of jumps for each jump process in the interval. -- `numjumps` is the number of jump processes, i.e. the number of `rate` equations - and the number of `counts` -- `mark_dist` is the distribution for the mark. +- `numjumps` is the number of jump processes, i.e. the number of `rate` + equations and the number of `counts`. +- `mark_dist` is the distribution for a mark. ## Defining a Jump Problem -To define a `JumpProblem`, you must first define the basic problem. This can be +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: +previously defined problem as `prob`. Then the constructor for the jump problem +is: ```julia -JumpProblem(prob,aggregator::Direct,jumps::JumpSet; +JumpProblem(prob, aggregator, jumps::JumpSet; save_positions = typeof(prob) <: AbstractDiscreteProblem ? (false,true) : (true,true)) ``` -The aggregator is the method for simulating jumps. They are called aggregators -since they combine all `jumps` in a single discrete simulation algorithm. -Aggregators are defined below. `jumps` is a `JumpSet` which is just a gathering -of jumps. Instead of passing a `JumpSet`, one may just pass a list of jumps -themselves. For example: +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`](@ref) 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: ```julia -JumpProblem(prob,aggregator,jump1,jump2) +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 -events occur. +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 @@ -228,15 +282,18 @@ together). ## Jump Aggregators for Exact Simulation -Jump aggregators are methods for simulating jumps exactly. They are called -aggregators since they combine all `jumps` in a single discrete simulation -algorithm. Aggregators combine `jump` in different ways and offer different -trade-offs. However, all aggregators describe the realization of each and every -jump chronologically. Since they do not skip any jump, they are considered exact -methods. Note that none of the aggregators discussed in this section can be used -with `RegularJumps` which are used for inexact methods. +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: +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 [1]. - `DirectFW`: the Gillespie Direct method SSA [1] with `FunctionWrappers`. This @@ -244,16 +301,16 @@ The current aggregators are: `ConstantRateJumps`. - *`DirectCR`*: The Composition-Rejection Direct method of Slepoy et al [2]. For large networks and linear chain-type networks it will often give better - performance than `Direct`. (Requires dependency graph, see below.) + performance than `Direct`. - *`SortingDirect`*: The Sorting Direct Method of McCollum et al [3]. It will usually offer performance as good as `Direct`, and for some systems can offer - substantially better performance. (Requires dependency graph, see below.) -- *`RSSA`*: The Rejection SSA (RSSA) method of Thanh et al [4,5]. With `RSSACR`, for - very large reaction networks it often offers the best performance of all - methods. (Requires dependency graph, see below.) + substantially better performance. +- *`RSSA`*: The Rejection SSA (RSSA) method of Thanh et al [4,5]. With `RSSACR`, + for very large reaction networks it often offers the best performance of all + methods. - *`RSSACR`*: The Rejection SSA (RSSA) with Composition-Rejection method of - Thanh et al [6]. With `RSSA`, for very large reaction networks it often offers the - best performance of all methods. (Requires dependency graph, see below.) + Thanh et al [6]. With `RSSA`, for very large reaction networks it often offers + the best performance of all methods. - `RDirect`: A variant of Gillespie's Direct method [1] that uses rejection to sample the next reaction. - `FRM`: The Gillespie first reaction method SSA [1]. `Direct` should generally @@ -261,21 +318,20 @@ The current aggregators are: - `FRMFW`: The Gillespie first reaction method SSA [1] with `FunctionWrappers`. - *`NRM`*: The Gibson-Bruck Next Reaction Method [7]. For some reaction network structures this may offer better performance than `Direct` (for example, - large, linear chains of reactions). (Requires dependency graph, see below.) + large, linear chains of reactions). - *`Coevolve`*: An adaptation of the COEVOLVE algorithm of Farajtabar et al [8]. - for simulating any compound point process that evolves through time. This is - the only aggregator that handles `VariableRateJump`. If rates do not change - between jump events (i.e. `ConsantRateJump` or `MassActionJump`) this - aggregator is very similar to `NRM`. (Requires dependency graph, see below.) + Currently the only aggregator that also supports *bounded* + `VariableRateJump`s. Essentially reduces to `NRM` in handling + `ConstantRateJump`s and `MassActionJump`s. To pass the aggregator, pass the instantiation of the type. For example: ```julia -JumpProblem(prob,Direct(),jump1,jump2) +JumpProblem(prob, Direct(), jump1, jump2) ``` -will build a problem where the constant rate jumps are solved using Gillespie's -Direct SSA method. +will build a problem where the jumps are simulated using Gillespie's Direct SSA +method. [1] Daniel T. Gillespie, A general method for numerically simulating the stochastic time evolution of coupled chemical reactions, Journal of Computational Physics, @@ -313,26 +369,27 @@ evolution, Journal of Machine Learning Research 18(1), 1305–1353 (2017). doi: 10.5555/3122009.3122050. ## Jump Aggregators Requiring Dependency Graphs -Italicized constant rate jump aggregators require the user to pass a dependency -graph to `JumpProblem`. `DirectCR`, `NRM`, `SortingDirect` and `Coevolve` -require a jump-jump dependency graph, passed through the named parameter -`dep_graph`. i.e. +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. ```julia -JumpProblem(prob,DirectCR(),jump1,jump2; dep_graph=your_dependency_graph) +JumpProblem(prob, DirectCR(), jump1, jump2; dep_graph = your_dependency_graph) ``` For systems with only `MassActionJump`s, or those generated from a -[Catalyst](https://docs.sciml.ai/Catalyst/stable/) `reaction_network`, this graph -will be auto-generated. Otherwise, you must construct the dependency graph -whenever using `ConstantRateJump`s and/or `VariableRateJump`s. This is also the -case when combining `MassActionJump` with `ConstantRateJump`s and/or +[Catalyst](https://docs.sciml.ai/Catalyst/stable/) `reaction_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 `VariableRateJump`s (with the latter internally ordered -in the same order they were passed in). Thus, keep that in mind when combining -`MassActionJump`s with other types of jumps. +`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: @@ -352,25 +409,28 @@ 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 constant rate between +- 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. You will need to define the lower and upper rate boundaries as well as - the interval for which the boundaries apply. The tighter the boundaries and - the easier to compute, the faster the resulting algorithm will be. + jumps. If possible, construct a bounded [`VariableRateJump`](@ref) 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 the `Coevolve` + aggregator to ensure such jumps are handled via the more efficient aggregator + interface. + +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 than `Direct`. +- For > ~10 jumps `SortingDirect` will often offer better performance than + `Direct`. - For large numbers of jumps with sparse chain like structures and similar jump rates, for example continuous time random walks, `RSSACR`, `DirectCR` and then `NRM` often have the best performance. - For very large networks, with many updates per jump, `RSSA` and `RSSACR` will often substantially outperform the other methods. -- For systems with `VariableRateJump`, only the `Coevolve` aggregator is - supported. -- The `SSAStepper()` can be used with `VariableRateJump`s that modify the state - of differential equations. However, it is not possible to use `SSAStepper()` - to solve `VariableRateJump`s whose rate depends on a continuous variable. + +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 @@ -389,44 +449,44 @@ components of the SSA aggregators. As such, only the new problem generated by As an example, consider the following SIR model: ```julia -rate1(u,p,t) = (0.1/1000.0)*u[1]*u[2] +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!) +jump = ConstantRateJump(rate1, affect1!) -rate2(u,p,t) = 0.01u[2] +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) +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 `tspan` by either making a new +We can change any of `u0`, `p` and/or `tspan` by either making a new `DiscreteProblem` ```julia -u02 = [10,1,0] +u02 = [10, 1, 0] p2 = (.1/1000, 0.0) -tspan2 = (0.0,2500.0) +tspan2 = (0.0, 2500.0) dprob2 = DiscreteProblem(u02, tspan2, p2) -jprob2 = remake(jprob, prob=dprob2) +jprob2 = remake(jprob, prob = dprob2) sol2 = solve(jprob2, SSAStepper()) ``` or by directly remaking with the new parameters ```julia -jprob2 = remake(jprob, u0=u02, p=p2, tspan=tspan2) +jprob2 = remake(jprob, u0 = u02, p = p2, tspan = tspan2) sol2 = solve(jprob2, SSAStepper()) ``` To avoid ambiguities, the following will give an error ```julia -jprob2 = remake(jprob, prob=dprob2, u0=u02) +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. diff --git a/src/jumps.jl b/src/jumps.jl index dc05b29fe..947c9a8b1 100644 --- a/src/jumps.jl +++ b/src/jumps.jl @@ -51,9 +51,16 @@ performance charactertistics. - Bounded `VariableRateJump`s require passing the keyword arguments `urate` and `rateinterval`, corresponding to functions `urate(u, p, t)` and `rateinterval(u, p, t)`, see below. These must calculate a time window over which the rate function is bounded by a - constant (as long as any components of the state on which the upper bound function depends - do not change). One can also optionally provide a lower bound function, `lrate(u, p, t)` - via the `lrate` keyword argument, that can lead to increased performance. + constant. Note that it is ok if the rate bound would be violated within the time interval + due to a change in `u` arising from another `ConstantRateJump`, `MassActionJump` or + *bounded* `VariableRateJump being executed, as the chosen aggregator will then handle + recalculating the rate bound and interval. *However, if the bound could be violated within + the time interval due to a change in `u` arising from continuous dynamics such as a + coupled ODE, SDE, or a general `VariableRateJump`, bounds should not be given.* This + ensures the jump is classified as a general `VariableRateJump` and properly handled. One + can also optionally provide a lower bound function, `lrate(u, p, t)`, via the `lrate` + keyword argument. This can lead to increased performance. The validity of the lower bound + should hold under the same conditions and rate interval as `urate`. * Bounded `VariableRateJump`s can currently be used in the `Coevolve` aggregator, and can therefore be efficiently simulated in pure-jump `DiscreteProblem`s using the @@ -61,11 +68,15 @@ performance charactertistics. * These can be substantially more performant than general `VariableRateJump`s without the rate bound functions. -The additional user provided functions leveraged by bounded `VariableRateJumps`, `urate(u, -p, t)`, `rateinterval(u, p, t)`, and the optional `lrate(u, p, t)` require that -- For `s` in `[t, t + rateinterval(u, p, t)]`, we should have that `lrate(u, p, t) <= - rate(u, p, s) <= urate(u, p, t)` provided any components of `u` on which these functions - depend remain unchanged. +Reemphasizing, the additional user provided functions leveraged by bounded +`VariableRateJumps`, `urate(u, p, t)`, `rateinterval(u, p, t)`, and the optional `lrate(u, +p, t)` require that +- For `s` in `[t, t + rateinterval(u, p, t)]`, we have that `lrate(u, p, t) <= rate(u, p, s) + <= urate(u, p, t)`. +- It is ok if these bounds would be violated during the time window due to another + `ConstantRateJump`, `MassActionJump` or bounded `VariableRateJump` occurring, however, + they must remaing valid if `u` changes for any other reason (for example, due to + continuous dynamics like ODEs, SDEs, or general `VariableRateJump`s). ## Fields @@ -116,26 +127,27 @@ struct VariableRateJump{R, F, R2, R3, R4, I, T, T2} <: AbstractJump affect!::F """Optional function `lrate(u, p, t)` that computes a lower bound on the rate in the interval `t` to `t + rateinterval(u, p, t)` at time `t` given state `u` and parameters - `p`. The bound should hold over this time interval as long as components of `u` for - which the rate functions are dependent do not change. When using aggregators that - support bounded `VariableRateJump`s, currently only `Coevolve`, providing a lower-bound - can lead to improved performance. + `p`. This bound must rigorously hold during the time interval as long as another + `ConstantRateJump`, `MassActionJump`, or *bounded* `VariableRateJump` has not been + sampled. When using aggregators that support bounded `VariableRateJump`s, currently only + `Coevolve`, providing a lower-bound can lead to improved performance. """ lrate::R2 """Optional function `urate(u, p, t)` for general `VariableRateJump`s, but is required to define a bounded `VariableRateJump`, which can be used with supporting aggregators, currently only `Coevolve`, and offers improved computational performance. Computes an upper bound for the rate in the interval `t` to `t + rateinterval(u, p, t)` at time `t` - given state `u` and parameters `p`. The bound should hold over this time interval as - long as components of `u` for which the rate functions are dependent do not change. """ + given state `u` and parameters `p`. This bound must rigorously hold during the time + interval as long as another `ConstantRateJump`, `MassActionJump`, or *bounded* + `VariableRateJump` has not been sampled. """ urate::R3 """Optional function `rateinterval(u, p, t)` for general `VariableRateJump`s, but is required to define a bounded `VariableRateJump`, which can be used with supporting aggregators, currently only `Coevolve`, and offers improved computational performance. Computes the time interval from time `t` over which the `urate` and `lrate` bounds will - hold, `t` to `t + rateinterval(u, p, t)`, given state `u` and parameters `p`. The bound - should hold over this time interval as long as components of `u` for which the rate - functions are dependent do not change. """ + hold, `t` to `t + rateinterval(u, p, t)`, given state `u` and parameters `p`. This bound + must rigorously hold during the time interval as long as another `ConstantRateJump`, + `MassActionJump`, or *bounded* `VariableRateJump` has not been sampled. """ rateinterval::R4 idxs::I rootfind::Bool @@ -218,8 +230,7 @@ action form, offering improved performance within jump algorithms compared to - [Tutorial](https://docs.sciml.ai/JumpProcesses/stable/tutorials/discrete_stochastic_example/) ### Constructors -- `MassActionJump(reactant_stoich, net_stoich; scale_rates=true, - param_idxs=nothing)` +- `MassActionJump(reactant_stoich, net_stoich; scale_rates = true, param_idxs = nothing)` Here `reactant_stoich` denotes the reactant stoichiometry for each reaction and `net_stoich` the net stoichiometry for each reaction. @@ -229,12 +240,12 @@ Here `reactant_stoich` denotes the reactant stoichiometry for each reaction and $(FIELDS) ## Keyword Arguments -- `scale_rates=true`, whether to rescale the reaction rate constants according +- `scale_rates = true`, whether to rescale the reaction rate constants according to the stoichiometry. -- `nocopy=false`, whether the `MassActionJump` can alias the `scaled_rates` and +- `nocopy = false`, whether the `MassActionJump` can alias the `scaled_rates` and `reactant_stoch` from the input. Note, if `scale_rates=true` this will potentially modify both of these. -- `param_idxs=nothing`, indexes in the parameter vector, `JumpProblem.prob.p`, +- `param_idxs = nothing`, indexes in the parameter vector, `JumpProblem.prob.p`, that correspond to each reaction's rate. See the tutorial and main docs for details. From e75c34f5c9502040ce13b3a444ca42f3f285fdf5 Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Wed, 28 Dec 2022 16:34:09 -0500 Subject: [PATCH 61/72] finish tutorial updates --- docs/src/tutorials/simple_poisson_process.md | 140 ++++++++++++------- 1 file changed, 88 insertions(+), 52 deletions(-) diff --git a/docs/src/tutorials/simple_poisson_process.md b/docs/src/tutorials/simple_poisson_process.md index 22b460d32..7f6bbee9a 100644 --- a/docs/src/tutorials/simple_poisson_process.md +++ b/docs/src/tutorials/simple_poisson_process.md @@ -6,14 +6,15 @@ primarily in chemical or population process models, where several types of jumps may occur, can skip directly to the [second tutorial](@ref ssa_tutorial) for a tutorial covering similar material but focused on the SIR model. -JumpProcesses allows the simulation of jump processes where the transition rate, i.e. -intensity or propensity, can be a function of the current solution, current +JumpProcesses allows the simulation of jump processes where the transition rate, +i.e. intensity or propensity, can be a function of the current solution, current parameters, and current time. Throughout this tutorial these are denoted by `u`, `p` and `t`. Likewise, when a jump occurs any DifferentialEquations.jl-compatible change to the current system state, as encoded by a [DifferentialEquations.jl integrator](https://docs.sciml.ai/DiffEqDocs/stable/basics/integrator/), is -allowed. This includes changes to the current state or to parameter values. +allowed. This includes changes to the current state or to parameter values (for +example via a callback). This tutorial requires several packages, which can be added if not already installed via @@ -29,7 +30,7 @@ default(; lw = 2) ``` ## `ConstantRateJump`s -Our first example will be to simulate a simple Poission counting process, +Our first example will be to simulate a simple Poisson counting process, ``N(t)``, with a constant transition rate of λ. We can interpret this as a birth process where new individuals are created at the constant rate λ. ``N(t)`` then gives the current population size. In terms of a unit Poisson counting process, @@ -61,7 +62,7 @@ sol = solve(jprob, SSAStepper()) plot(sol, label="N(t)", xlabel="t", legend=:bottomright) ``` -We can define and simulate our jump process using JumpProcesses. We first load our +We can define and simulate our jump process as follows. We first load our packages ```@example tut1 using JumpProcesses, Plots @@ -104,29 +105,28 @@ tspan = (0.0, 10.0) ``` Finally, we construct the associated SciML problem types and generate one realization of the process. We first create a `DiscreteProblem` to encode that -we are simulating a process that evolves in discrete time steps. Note, this -currently requires that the process has constant transition rates *between* -jumps +we are simulating a process that evolves in discrete time steps. ```@example tut1 dprob = DiscreteProblem(u₀, tspan, p) ``` We next create a [`JumpProblem`](@ref) that wraps the discrete problem, and -specifies which algorithm to use for determining next jump times (and in the -case of multiple possible jumps the next jump type). Here we use the classical -`Direct` method, proposed by Gillespie in the chemical reaction context, but -going back to earlier work by Doob and others (and also known as Kinetic Monte -Carlo in the physics literature) +specifies which algorithm, called an aggregator in JumpProcesses, to use for +determining next jump times (and in the case of multiple possible jumps the next +jump type). Here we use the classical `Direct` method, proposed by Gillespie in +the chemical reaction context, but going back to earlier work by Doob and others +(and also known as Kinetic Monte Carlo in the physics literature) ```@example tut1 # a jump problem, specifying we will use the Direct method to sample # jump times and events, and that our jump is encoded by crj jprob = JumpProblem(dprob, Direct(), crj) ``` -We are finally ready to simulate one realization of our jump process +We are finally ready to simulate one realization of our jump process, selecting +`SSAStepper` to handle time-stepping our system from jump to jump ```@example tut1 # now we simulate the jump process in time, using the SSAStepper time-stepper sol = solve(jprob, SSAStepper()) -plot(sol, label="N(t)", xlabel="t", legend=:bottomright) +plot(sol, labels = "N(t)", xlabel = "t", legend = :bottomright) ``` ### More general `ConstantRateJump`s @@ -157,11 +157,11 @@ second `ConstantRateJump`. We then construct the corresponding problems, passing both jumps to `JumpProblem`, and can solve as before ```@example tut1 p = (λ = 2.0, μ = 1.5) -u₀ = [0,0] # (N(0), D(0)) +u₀ = [0, 0] # (N(0), D(0)) dprob = DiscreteProblem(u₀, tspan, p) jprob = JumpProblem(dprob, Direct(), crj, deathcrj) sol = solve(jprob, SSAStepper()) -plot(sol, label=["N(t)" "D(t)"], xlabel="t", legend=:topleft) +plot(sol, labels = ["N(t)" "D(t)"], xlabel = "t", legend = :topleft) ``` In the next tutorial we will also introduce [`MassActionJump`](@ref)s, which are @@ -175,12 +175,13 @@ by adding or subtracting a constant vector from `u`. ## `VariableRateJump`s for processes that are not constant between jumps So far we have assumed that our jump processes have transition rates that are constant in between jumps. In many applications this may be a limiting -assumption. To support such models JumpProcesses has the [`VariableRateJump`](@ref) -type, which represents jump processes that have an arbitrary time dependence in -the calculation of the transition rate, including transition rates that depend -on states which can change in between `ConstantRateJump`s. Let's consider the -previous example, but now let the birth rate be time dependent, ``b(t) = \lambda -\left(\sin(\pi t / 2) + 1\right)``, so that our model becomes +assumption. To support such models JumpProcesses has the +[`VariableRateJump`](@ref) type, which represents jump processes that have an +arbitrary time dependence in the calculation of the transition rate, including +transition rates that depend on states which can change in between two jumps +occurring. Let's consider the previous example, but now let the birth rate be +time dependent, ``b(t) = \lambda \left(\sin(\pi t / 2) + 1\right)``, so that our +model becomes ```math \begin{align*} N(t) &= Y_b\left(\int_0^t \left( \lambda \sin\left(\tfrac{\pi s}{2}\right) + 1 \right) \, d s\right) - Y_d \left(\int_0^t \mu N(s^-) \, ds \right), \\ @@ -188,54 +189,85 @@ D(t) &= Y_d \left(\int_0^t \mu N(s^-) \, ds \right). \end{align*} ``` + The birth rate is cyclical, bounded between a lower-bound of ``λ`` and an -upper-bound of ``2 λ``. We'll then re-encode the first jump as a -`VariableRateJump` +upper-bound of ``2 λ``. We'll then re-encode the first (birth) jump as a +`VariableRateJump`. Two types of `VariableRateJump`s are supported, general and +bounded. The latter are generally more performant, but are also more restrictive +in when they can be used. They also require specifying additional information +beyond just `rate` and `affect!` functions. + +Let's see how to build a bounded `VariableRateJump` encoding our new birth +process. We first specify the rate and affect functions, just like for a +`ConstantRateJump`, ```@example tut1 rate1(u,p,t) = p.λ * (sin(pi*t/2) + 1) -lrate1(u,p,t) = p.λ -urate1(u, p, t) = 2 * p.λ -L1(u, p, t) = typemax(t) affect1!(integrator) = (integrator.u[1] += 1) -vrj1 = VariableRateJump(rate1, affect1!; lrate=lrate1, urate=urate1, L=L1) +``` +We next provide functions that determine a time interval over which the rate is +bounded from above given `u`, `p` and `t`. From these we can construct the new +bounded `VariableRateJump`: +```@example tut1 +# We require that rate1(u,p,s) <= urate(u,p,s) +# for t <= s <= t + rateinterval(u,p,t) +rateinterval(u, p, t) = typemax(t) +urate(u, p, t) = 2 * p.λ + +# Optionally, we can give a lower bound over the same interval. +# This may boost computational performance. +lrate(u, p, t) = p.λ + +# now we construct the bounded VariableRateJump +vrj1 = VariableRateJump(rate1, affect1!; lrate, urate, rateinterval) ``` -Since births modify the population size `u[1]` and deaths `u[2]` occur at a rate -proportional to the population size. We must represent this relation in -a dependency graph. Note that the indices in the graph correspond to the order -in which the jumps appear when the problem is constructed. The graph below -indicates that births (event 1) modify deaths (event 2), but deaths do not -modify births. +Finally, to efficiently simulate the new jump process we must also specify a +dependency graph. This indicates when a given jump occurs, which jumps in the +system need to have their rates and/or rate bounds recalculated (for example, +due to depending on changed components in `u`). We also assume the convention +that a given jump depends on itself. Since the first (birth) jump modifies the +population size `u[1]`, and the second (death) jump occurs at a rate +proportional to `u[1]`, when the first jump occurs we need to recalculate both +of the rates. In contrast, death does not change `u[1]`, and so the dependencies +of the second (death) jump are only itself. Note that the indices in the graph +correspond to the order in which the jumps appear when the problem is +constructed. The graph below encodes the dependents of the birth and death jumps +respectively ```@example tut1 -dep_graph = [[2], []] +dep_graph = [[1,2], [2]] ``` We can then construct the corresponding problem, passing both jumps to -`JumpProblem` as well as the dependency graph. Since we are dealing with -a `VariableRateJump` we must use the `Coevolve` aggregator. +`JumpProblem` as well as the dependency graph. We must use an aggregator that +supports bounded `VariableRateJump`s, in this case we choose the `Coevolve` +aggregator. ```@example tut1 -jprob = JumpProblem(dprob, Coevolve(), vrj1, deathcrj; dep_graph=dep_graph) +jprob = JumpProblem(dprob, Coevolve(), vrj1, deathcrj; dep_graph) sol = solve(jprob, SSAStepper()) -plot(sol, label=["N(t)" "D(t)"], xlabel="t", legend=:topleft) +plot(sol, labels = ["N(t)" "D(t)"], xlabel = "t", legend = :topleft) ``` -In a scenario, where we did not know the bounds of the time-dependent rate. We -would have to use a continuous problem type to properly handle the jump times. -Under this assumption we would define the `VariableRateJump` as following: +If we did not know the upper rate bound or rate interval functions for the +time-dependent rate, we would have to use a continuous problem type and general +`VariableRateJump` to correctly handle calculating the jump times. Under this +assumption we would define a general `VariableRateJump` as following: ```@example tut1 vrj2 = VariableRateJump(rate1, affect1!) ``` -Since the death rate now depends on a `VariableRateJump` without bounds, we need -to redefine the death jump process as a `VariableRateJump` +Since the death rate now depends on a variable, `u[2]`, modified by a general +`VariableRateJump` (i.e. one that is not bounded), we also need to redefine the +death jump process as a general `VariableRateJump` ```@example tut1 deathvrj = VariableRateJump(deathrate, deathaffect!) ``` -To simulate our jump process we now need to construct an -ordinary differential equation problem, `ODEProblem`, but setting the ODE -derivative to preserve the state (i.e. to zero). We are essentially defining a -combined ODE-jump process, i.e. a [piecewise deterministic Markov +To simulate our jump process we now need to construct a continuous problem type +to couple the jumps to, for example an ordinary differential equation (ODE) or +stochastic differential equation (SDE). Let's use an ODE, encoded via an +`ODEProblem`. We simply set the ODE derivative to zero to preserve the state. We +are essentially defining a combined ODE-jump process, i.e. a [piecewise +deterministic Markov process](https://en.wikipedia.org/wiki/Piecewise-deterministic_Markov_process), but one where the ODE is trivial and does not change the state. To use this problem type and the ODE solvers we first load `OrdinaryDiffEq.jl` or @@ -251,7 +283,7 @@ using OrdinaryDiffEq # or using DifferentialEquations ``` We can then construct our ODE problem with a trivial ODE derivative component. -Note, to work with the ODE solver time stepper we must change our initial +Note, to work with the ODE solver time stepper we must also change our initial condition to be floating point valued ```@example tut1 function f!(du, u, p, t) @@ -262,13 +294,17 @@ u₀ = [0.0, 0.0] oprob = ODEProblem(f!, u₀, tspan, p) jprob = JumpProblem(oprob, Direct(), vrj2, deathvrj) ``` -We simulate our jump process, using the `Tsit5` ODE solver as the time stepper in -place of `SSAStepper` +We can now simulate our jump process, using the `Tsit5` ODE solver as the time +stepper in place of `SSAStepper` ```@example tut1 sol = solve(jprob, Tsit5()) plot(sol, label=["N(t)" "D(t)"], xlabel="t", legend=:topleft) ``` +For more details on when bounded vs. general `VariableRateJump`s can be used, +see the [next tutorial](@ref ssa_tutorial) and the [Jump Problems](@ref +jump_problem_type) documentation page. + ## Having a Random Jump Distribution Suppose we want to simulate a compound Poisson process, ``G(t)``, where ```math From 6ceef8dcc45753fe7909dc7451cc66ce7262f4ce Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Thu, 29 Dec 2022 13:56:38 -0500 Subject: [PATCH 62/72] tutorial bug fix --- docs/src/api.md | 1 + docs/src/tutorials/simple_poisson_process.md | 20 +++++++++++--------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/docs/src/api.md b/docs/src/api.md index 2b5706c58..1a62677cf 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -15,6 +15,7 @@ reset_aggregated_jumps! ConstantRateJump MassActionJump VariableRateJump +RegularJump JumpSet ``` diff --git a/docs/src/tutorials/simple_poisson_process.md b/docs/src/tutorials/simple_poisson_process.md index 7f6bbee9a..0f96bb490 100644 --- a/docs/src/tutorials/simple_poisson_process.md +++ b/docs/src/tutorials/simple_poisson_process.md @@ -225,16 +225,18 @@ Finally, to efficiently simulate the new jump process we must also specify a dependency graph. This indicates when a given jump occurs, which jumps in the system need to have their rates and/or rate bounds recalculated (for example, due to depending on changed components in `u`). We also assume the convention -that a given jump depends on itself. Since the first (birth) jump modifies the -population size `u[1]`, and the second (death) jump occurs at a rate -proportional to `u[1]`, when the first jump occurs we need to recalculate both -of the rates. In contrast, death does not change `u[1]`, and so the dependencies -of the second (death) jump are only itself. Note that the indices in the graph -correspond to the order in which the jumps appear when the problem is -constructed. The graph below encodes the dependents of the birth and death jumps -respectively +that a given jump depends on itself. Internally, JumpProcesses preserves the +relative ordering of jumps of each distinct type, but always reorders all +`ConstantRateJump`s to appear before any `VariableRateJump`s. As such, the +`ConstantRateJump` representing the death process will have internal index 1, +and our new bounded `VariableRateJump` for birth will have internal index 2. +Since birth modifies the population size `u[1]`, and death occurs at a rate +proportional to `u[1]`, when birth occurs we need to recalculate both of rates. +In contrast, death does not change `u[1]`, and so when death occurs we only need +to recalculate the death rate. The graph below encodes the dependents of the +death (`dep_graph[1]`) and birth (`dep_graph[2]`) jumps respectively ```@example tut1 -dep_graph = [[1,2], [2]] +dep_graph = [[1], [1,2]] ``` We can then construct the corresponding problem, passing both jumps to From bba4aafbc467f7277609ff817f8444d362429ace Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Thu, 29 Dec 2022 15:44:09 -0500 Subject: [PATCH 63/72] doc tweaks --- docs/src/jump_types.md | 10 +++++----- src/jumps.jl | 22 ++++++++++------------ 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/docs/src/jump_types.md b/docs/src/jump_types.md index 4d3234127..1d5ebcd41 100644 --- a/docs/src/jump_types.md +++ b/docs/src/jump_types.md @@ -223,11 +223,11 @@ Note that `SSAStepper` when using systems with only bounded `VariableRateJump`s and the `Coevolve` aggregator. - When choosing a different aggregator than `Coevolve`, `SSAStepper` can not - currently be used, and the `JumpProblem` must be coupled to a continuous - problem type such as an `ODEProblem` to handle time-stepping. The continuous - time-stepper treats *all* `VariableRateJump`s as `ContinuousCallback`s, using - the `rate(u, p, t)` function to construct the `condition` function that - triggers a callback. + currently be used, and the `JumpProblem` must be coupled to a continuous + problem type such as an `ODEProblem` to handle time-stepping. The continuous + time-stepper treats *all* `VariableRateJump`s as `ContinuousCallback`s, using + the `rate(u, p, t)` function to construct the `condition` function that + triggers a callback. #### Defining a Regular Jump diff --git a/src/jumps.jl b/src/jumps.jl index 947c9a8b1..fe3527a33 100644 --- a/src/jumps.jl +++ b/src/jumps.jl @@ -53,7 +53,7 @@ performance charactertistics. see below. These must calculate a time window over which the rate function is bounded by a constant. Note that it is ok if the rate bound would be violated within the time interval due to a change in `u` arising from another `ConstantRateJump`, `MassActionJump` or - *bounded* `VariableRateJump being executed, as the chosen aggregator will then handle + *bounded* `VariableRateJump` being executed, as the chosen aggregator will then handle recalculating the rate bound and interval. *However, if the bound could be violated within the time interval due to a change in `u` arising from continuous dynamics such as a coupled ODE, SDE, or a general `VariableRateJump`, bounds should not be given.* This @@ -91,9 +91,8 @@ affect!(integrator) = integrator.u[1] -= 1 vrj = VariableRateJump(rate, affect!) ``` -In case we want to use the `Coevolve` aggregator, we need to pass the rate boundaries and -interval for which the rates apply. The `Coevolve` aggregator allow us to perform discrete -steps with `SSAStepper()`. +To define a bounded `VariableRateJump` that can be used with supporting aggregators such as +`Coevolve`, we must define bounds and a rate interval: ```julia rateinterval(u,p,t) = (1 / p[1]) * 2 rate(u,p,t) = t * p[1] * u[1] @@ -105,14 +104,13 @@ vrj = VariableRateJump(rate, affect!; lrate = lrate, urate = urate, ``` ## Notes -- When using the `Coevolve` aggregator, `DiscreteProblem` can be used. Otherwise, - `ODEProblem` or `SDEProblem` must be used to be correctly simulated. -- **When not using the `Coevolve` aggregator, `VariableRateJump`s result in `integrator`s - storing an effective state type that wraps the main state vector.** See - [`ExtendedJumpArray`](@ref) for details on using this object. Note that the presence of - *any* `VariableRateJump`s will result in all `ConstantRateJump`, `VariableRateJump` and - callback `affect!` functions receiving an integrator with `integrator.u` an - [`ExtendedJumpArray`](@ref). +- When using an aggregator that supports bounded `VariableRateJump`s, `DiscreteProblem` can + be used. Otherwise, `ODEProblem` or `SDEProblem` must be used. +- **When not using aggregators that support bounded `VariableRateJump`s, or when there are + general `VariableRateJump`s, `integrator`s store an effective state type that wraps the + main state vector.** See [`ExtendedJumpArray`](@ref) for details on using this object. In + this case all `ConstantRateJump`, `VariableRateJump` and callback `affect!` functions + receive an integrator with `integrator.u` an [`ExtendedJumpArray`](@ref). - Salis H., Kaznessis Y., Accurate hybrid stochastic simulation of a system of coupled chemical or biochemical reactions, Journal of Chemical Physics, 122 (5), DOI:10.1063/1.1835951 is used for calculating jump times with `VariableRateJump`s within From 72ac0a1f85b2f0428efc198b643acb5d9700ccea Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Fri, 30 Dec 2022 11:18:50 -0500 Subject: [PATCH 64/72] simplify coevolve --- src/aggregators/coevolve.jl | 35 +++++++++++++++-------------------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/src/aggregators/coevolve.jl b/src/aggregators/coevolve.jl index 557592923..ebe36695d 100644 --- a/src/aggregators/coevolve.jl +++ b/src/aggregators/coevolve.jl @@ -150,9 +150,8 @@ function next_time(p::CoevolveJumpAggregation{T}, u, params, t, i, tstop::T) whe num_majumps = get_num_majumps(p.ma_jumps) num_cjumps = length(p.urates) - length(p.rates) uidx = i - num_majumps - lidx = i - num_majumps - num_cjumps - urate = uidx > 0 ? get_urate(p, uidx, u, params, t) : - get_ma_urate(p, i, u, params, t) + lidx = uidx - num_cjumps + urate = uidx > 0 ? get_urate(p, uidx, u, params, t) : get_ma_urate(p, i, u, params, t) last_urate = p.cur_rates[i] if i != p.next_jump && last_urate > zero(t) s = urate == zero(t) ? typemax(t) : last_urate / urate * (p.pq[i] - t) @@ -170,26 +169,22 @@ function next_time(p::CoevolveJumpAggregation{T}, u, params, t, i, tstop::T) whe _t = t + s continue end - if _t >= tstop - break - end - lrate = p.urates[uidx] === p.lrates[lidx] ? urate : - get_lrate(p, lidx, u, params, t) - if lrate > urate - error("The lower bound should be lower than the upper bound rate for t = $(t) and i = $(i), but lower bound = $(lrate) > upper bound = $(urate)") - elseif lrate < urate + (_t >= tstop) && break + + lrate = get_lrate(p, lidx, u, params, t) + if lrate < urate # when the lower and upper bound are the same, then v < 1 = lrate / urate = urate / urate - v = rand(rng) + v = rand(rng) * urate # first inequality is less expensive and short-circuits the evaluation - if (v > lrate / urate) - if (v > get_rate(p, lidx, u, params, _t) / urate) - t = _t - urate = get_urate(p, uidx, u, params, t) - s = urate == zero(t) ? typemax(t) : randexp(rng) / urate - _t = t + s - continue - end + if (v > lrate) && (v > get_rate(p, lidx, u, params, _t)) + t = _t + urate = get_urate(p, uidx, u, params, t) + s = urate == zero(t) ? typemax(t) : randexp(rng) / urate + _t = t + s + continue end + elseif lrate > urate + error("The lower bound should be lower than the upper bound rate for t = $(t) and i = $(i), but lower bound = $(lrate) > upper bound = $(urate)") end break end From cd7ddf50e420add3aeb5c2f6245cc9bf4a23d340 Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Fri, 30 Dec 2022 11:20:19 -0500 Subject: [PATCH 65/72] add inbounds --- src/aggregators/coevolve.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/aggregators/coevolve.jl b/src/aggregators/coevolve.jl index ebe36695d..d859240bf 100644 --- a/src/aggregators/coevolve.jl +++ b/src/aggregators/coevolve.jl @@ -120,7 +120,7 @@ function update_dependent_rates!(p::CoevolveJumpAggregation, u, params, t) for (ix, i) in enumerate(deps) ti, last_urate_i = next_time(p, u, params, t, i, end_time) update!(pq, i, ti) - cur_rates[i] = last_urate_i + @inbounds cur_rates[i] = last_urate_i end nothing end From 176ca8154fd3c31db890032b0e6cdff4c09a193d Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Sat, 31 Dec 2022 16:21:31 -0500 Subject: [PATCH 66/72] make lrate optional --- src/aggregators/coevolve.jl | 60 +++++++++++++++++++++---------------- src/jumps.jl | 8 +++-- test/hawkes_test.jl | 38 ++++++++++++++--------- 3 files changed, 65 insertions(+), 41 deletions(-) diff --git a/src/aggregators/coevolve.jl b/src/aggregators/coevolve.jl index d859240bf..a2ff8c466 100644 --- a/src/aggregators/coevolve.jl +++ b/src/aggregators/coevolve.jl @@ -19,12 +19,13 @@ mutable struct CoevolveJumpAggregation{T, S, F1, F2, RNG, GR, PQ} <: lrates::F1 # vector of rate lower bound functions urates::F1 # vector of rate upper bound functions rateintervals::F1 # vector of interval length functions + haslratevec::Vector{Bool} # vector of whether an lrate was provided for this vrj end function CoevolveJumpAggregation(nj::Int, njt::T, et::T, crs::Vector{T}, sr::Nothing, maj::S, rs::F1, affs!::F2, sps::Tuple{Bool, Bool}, rng::RNG; u::U, dep_graph = nothing, lrates, urates, - rateintervals) where {T, S, F1, F2, RNG, U} + rateintervals, haslratevec) where {T, S, F1, F2, RNG, U} if dep_graph === nothing if (get_num_majumps(maj) == 0) || !isempty(rs) error("To use Coevolve a dependency graph between jumps must be supplied.") @@ -47,7 +48,7 @@ function CoevolveJumpAggregation(nj::Int, njt::T, et::T, crs::Vector{T}, sr::Not pq = MutableBinaryMinHeap{T}() CoevolveJumpAggregation{T, S, F1, F2, RNG, typeof(dg), typeof(pq)}(nj, nj, njt, et, crs, sr, maj, rs, affs!, sps, rng, - dg, pq, lrates, urates, rateintervals) + dg, pq, lrates, urates, rateintervals, haslratevec) end # creating the JumpAggregation structure (tuple-based variable jumps) @@ -57,37 +58,46 @@ function aggregate(aggregator::Coevolve, u, p, t, end_time, constant_jumps, AffectWrapper = FunctionWrappers.FunctionWrapper{Nothing, Tuple{Any}} RateWrapper = FunctionWrappers.FunctionWrapper{typeof(t), Tuple{typeof(u), typeof(p), typeof(t)}} - affects! = Vector{AffectWrapper}() - rates = Vector{RateWrapper}() - lrates = Vector{RateWrapper}() - urates = Vector{RateWrapper}() - rateintervals = Vector{RateWrapper}() - - if (constant_jumps !== nothing) && !isempty(constant_jumps) - append!(affects!, - [AffectWrapper((integrator) -> (j.affect!(integrator); nothing)) - for j in constant_jumps]) - append!(urates, [RateWrapper(j.rate) for j in constant_jumps]) + + ncrjs = (constant_jumps === nothing) ? 0 : length(constant_jumps) + nvrjs = (variable_jumps === nothing) ? 0 : length(variable_jumps) + nrjs = ncrjs + nvrjs + affects! = Vector{AffectWrapper}(undef, nrjs) + rates = Vector{RateWrapper}(undef, nvrjs) + lrates = similar(rates) + urates = similar(rates) + rateintervals = similar(rates) + haslratevec = zeros(Bool, nvrjs) + + idx = 1 + if constant_jumps !== nothing + for crj in constant_jumps + affects![idx] = AffectWrapper(integ -> (crj.affect!(integ); nothing)) + urates[idx] = RateWrapper(crj.rate) + idx += 1 + end end - if (variable_jumps !== nothing) && !isempty(variable_jumps) - append!(affects!, - [AffectWrapper((integrator) -> (j.affect!(integrator); nothing)) - for j in variable_jumps]) - append!(rates, [RateWrapper(j.rate) for j in variable_jumps]) - append!(lrates, [RateWrapper(j.lrate) for j in variable_jumps]) - append!(urates, [RateWrapper(j.urate) for j in variable_jumps]) - append!(rateintervals, [RateWrapper(j.rateinterval) for j in variable_jumps]) + if variable_jumps !== nothing + for (i, vrj) in enumerate(variable_jumps) + affects![idx] = AffectWrapper(integ -> (vrj.affect!(integ); nothing)) + urates[idx] = RateWrapper(vrj.urate) + idx += 1 + rates[i] = RateWrapper(vrj.rate) + rateintervals[i] = RateWrapper(vrj.rateinterval) + haslratevec[i] = haslrate(vrj) + lrates[i] = haslratevec[i] ? RateWrapper(vrj.lrate) : RateWrapper(nullrate) + end end - num_jumps = get_num_majumps(ma_jumps) + length(urates) + num_jumps = get_num_majumps(ma_jumps) + nrjs cur_rates = Vector{typeof(t)}(undef, num_jumps) sum_rate = nothing next_jump = 0 next_jump_time = typemax(t) CoevolveJumpAggregation(next_jump, next_jump_time, end_time, cur_rates, sum_rate, ma_jumps, rates, affects!, save_positions, rng; - u, dep_graph, lrates, urates, rateintervals) + u, dep_graph, lrates, urates, rateintervals, haslratevec) end # set up a new simulation and calculate the first jump / jump time @@ -146,7 +156,7 @@ end end function next_time(p::CoevolveJumpAggregation{T}, u, params, t, i, tstop::T) where {T} - @unpack rng = p + @unpack rng, haslratevec = p num_majumps = get_num_majumps(p.ma_jumps) num_cjumps = length(p.urates) - length(p.rates) uidx = i - num_majumps @@ -171,7 +181,7 @@ function next_time(p::CoevolveJumpAggregation{T}, u, params, t, i, tstop::T) whe end (_t >= tstop) && break - lrate = get_lrate(p, lidx, u, params, t) + lrate = haslratevec[lidx] ? get_lrate(p, lidx, u, params, t) : zero(t) if lrate < urate # when the lower and upper bound are the same, then v < 1 = lrate / urate = urate / urate v = rand(rng) * urate diff --git a/src/jumps.jl b/src/jumps.jl index fe3527a33..56f7c3cb1 100644 --- a/src/jumps.jl +++ b/src/jumps.jl @@ -155,6 +155,10 @@ struct VariableRateJump{R, F, R2, R3, R4, I, T, T2} <: AbstractJump reltol::T2 end +isbounded(::VariableRateJump) = true +isbounded(::VariableRateJump{R,F,R2,Nothing}) where {R,F,R2} = false +haslrate(::VariableRateJump) = true +haslrate(::VariableRateJump{R, F, Nothing}) where {R,F} = false nullrate(u, p, t::T) where {T <: Number} = zero(T) """ @@ -179,8 +183,8 @@ function VariableRateJump(rate, affect!; error("`urate` and `rateinterval` must both be `nothing`, or must both be defined.") end - if (urate !== nothing && lrate === nothing) - lrate = nullrate + if lrate !== nothing + (urate !== nothing) || error("If a lower bound rate, `lrate`, is given than an upper bound rate, `urate`, and rate interval, `rateinterval`, must also be provided.") end VariableRateJump(rate, affect!, lrate, urate, rateinterval, idxs, rootfind, diff --git a/test/hawkes_test.jl b/test/hawkes_test.jl index 547e20316..32fbb3ca8 100644 --- a/test/hawkes_test.jl +++ b/test/hawkes_test.jl @@ -32,14 +32,22 @@ function hawkes_rate(i::Int, g, h) return rate end -function hawkes_jump(i::Int, g, h) +function hawkes_jump(i::Int, g, h; uselrate = true) rate = hawkes_rate(i, g, h) - lrate(u, p, t) = p[1] urate = rate - function rateinterval(u, p, t) - _lrate = lrate(u, p, t) - _urate = urate(u, p, t) - return _urate == _lrate ? typemax(t) : 1 / (2 * _urate) + if uselrate + lrate(u, p, t) = p[1] + rateinterval = (u, p, t) -> begin + _lrate = lrate(u, p, t) + _urate = urate(u, p, t) + return _urate == _lrate ? typemax(t) : 1 / (2 * _urate) + end + else + lrate = nothing + rateinterval = (u, p, t) -> begin + _urate = urate(u, p, t) + return 1 / (2 * _urate) + end end function affect!(integrator) push!(h[i], integrator.t) @@ -48,15 +56,15 @@ function hawkes_jump(i::Int, g, h) return VariableRateJump(rate, affect!; lrate, urate, rateinterval) end -function hawkes_jump(u, g, h) - return [hawkes_jump(i, g, h) for i in 1:length(u)] +function hawkes_jump(u, g, h; uselrate = true) + return [hawkes_jump(i, g, h; uselrate) for i in 1:length(u)] end function hawkes_problem(p, agg::Coevolve; u = [0.0], tspan = (0.0, 50.0), save_positions = (false, true), - g = [[1]], h = [[]]) + g = [[1]], h = [[]], uselrate = true) dprob = DiscreteProblem(u, tspan, p) - jumps = hawkes_jump(u, g, h) + jumps = hawkes_jump(u, g, h; uselrate) jprob = JumpProblem(dprob, agg, jumps...; dep_graph = g, save_positions, rng) return jprob end @@ -68,7 +76,7 @@ end function hawkes_problem(p, agg; u = [0.0], tspan = (0.0, 50.0), save_positions = (false, true), - g = [[1]], h = [[]]) + g = [[1]], h = [[]], kwargs...) oprob = ODEProblem(f!, u, tspan, p) jumps = hawkes_jump(u, g, h) jprob = JumpProblem(oprob, agg, jumps...; save_positions, rng) @@ -97,11 +105,13 @@ h = [Float64[]] Eλ, Varλ = expected_stats_hawkes_problem(p, tspan) -algs = (Direct(), Coevolve()) +algs = (Direct(), Coevolve(), Coevolve()) +uselrate = zeros(Bool, length(algs)) +uselrate[3] = true Nsims = 250 -for alg in algs - jump_prob = hawkes_problem(p, alg; u = u0, tspan, g, h) +for (i, alg) in enumerate(algs) + jump_prob = hawkes_problem(p, alg; u = u0, tspan, g, h, uselrate = uselrate[i]) if typeof(alg) <: Coevolve stepper = SSAStepper() else From cf0f050d72376629425c8953c2d9d41dbd64b36d Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Sat, 31 Dec 2022 17:13:34 -0500 Subject: [PATCH 67/72] fix initialization bug --- HISTORY.md | 9 +++++++++ src/aggregators/coevolve.jl | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 HISTORY.md diff --git a/HISTORY.md b/HISTORY.md new file mode 100644 index 000000000..d7e1e2c7f --- /dev/null +++ b/HISTORY.md @@ -0,0 +1,9 @@ +# Breaking updates and feature summaries across releases + +## JumpProcesses unreleased (master branch) +- Support for "bounded" `VariableRateJump`s that can be used with the `Coevolve` + aggregator for faster simulation of jump processes with time-dependent rates. + In particular, if all `VariableRateJump`s in a pure-jump system are bounded one + can use `Coevolve` with `SSAStepper` for better performance. See the + documentation, particularly the first and second tutorials, for details on + defining and using bounded `VariableRateJump`s. \ No newline at end of file diff --git a/src/aggregators/coevolve.jl b/src/aggregators/coevolve.jl index a2ff8c466..b8cd5761d 100644 --- a/src/aggregators/coevolve.jl +++ b/src/aggregators/coevolve.jl @@ -65,8 +65,8 @@ function aggregate(aggregator::Coevolve, u, p, t, end_time, constant_jumps, affects! = Vector{AffectWrapper}(undef, nrjs) rates = Vector{RateWrapper}(undef, nvrjs) lrates = similar(rates) - urates = similar(rates) rateintervals = similar(rates) + urates = Vector{RateWrapper}(undef, nrjs) haslratevec = zeros(Bool, nvrjs) idx = 1 From d5cacb30572a008eca62f231d8ef878e17781ba7 Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Sun, 1 Jan 2023 17:29:34 -0500 Subject: [PATCH 68/72] rework JumpProblem --- src/jumps.jl | 21 +++++++++++ src/problem.jl | 95 ++++++++++++++++++++++++-------------------------- 2 files changed, 67 insertions(+), 49 deletions(-) diff --git a/src/jumps.jl b/src/jumps.jl index 56f7c3cb1..dec0a7196 100644 --- a/src/jumps.jl +++ b/src/jumps.jl @@ -527,6 +527,27 @@ function JumpSet(vjs, cjs, rj, majv::Vector{T}) where {T <: MassActionJump} end @inline get_num_majumps(jset::JumpSet) = get_num_majumps(jset.massaction_jump) +@inline num_majumps(jset::JumpSet) = get_num_majumps(jset) + +@inline function num_crjs(jset::JumpSet) + (jset.constant_jumps !== nothing) ? length(jset.constant_jumps) : 0 +end + +@inline function num_vrjs(jset::JumpSet) + (jset.variable_jumps !== nothing) ? length(jset.variable_jumps) : 0 +end + +@inline function num_bndvrjs(jset::JumpSet) + (jset.variable_jumps !== nothing) ? count(isbounded, jset.variable_jumps) : 0 +end + +@inline function num_continvrjs(jset::JumpSet) + (jset.variable_jumps !== nothing) ? count(!isbounded, jset.variable_jumps) : 0 +end + +num_jumps(jset::JumpSet) = num_majumps(jset) + num_crjs(jset) + num_vrjs(jset) +num_discretejumps(jset::JumpSet) = num_majumps(jset) + num_crjs(jset) + num_bndvrjs(jset) +num_cdiscretejumps(jset::JumpSet) = num_majumps(jset) + num_crjs(jset) @inline split_jumps(vj, cj, rj, maj) = vj, cj, rj, maj @inline function split_jumps(vj, cj, rj, maj, v::VariableRateJump, args...) diff --git a/src/problem.jl b/src/problem.jl index efa80b2c5..91a8a447d 100644 --- a/src/problem.jl +++ b/src/problem.jl @@ -50,6 +50,9 @@ $(FIELDS) the jump occurs. - `spatial_system`, for spatial problems the underlying spatial structure. - `hopping_constants`, for spatial problems the spatial transition rate coefficients. +- `use_vrj_bounds = true`, set to false to disable handling bounded `VariableRateJump`s + with a supporting aggregator (such as `Coevolve`). They will then be handled via the + continuous integration interace, and treated like general `VariableRateJump`s. Please see the [tutorial page](https://docs.sciml.ai/JumpProcesses/stable/tutorials/discrete_stochastic_example/) in the @@ -166,7 +169,7 @@ function JumpProblem(prob, aggregator::AbstractAggregatorAlgorithm, jumps::JumpS (false, true) : (true, true), rng = DEFAULT_RNG, scale_rates = true, useiszero = true, spatial_system = nothing, hopping_constants = nothing, - callback = nothing, kwargs...) + callback = nothing, use_vrj_bounds = true, kwargs...) # initialize the MassActionJump rate constants with the user parameters if using_params(jumps.massaction_jump) @@ -182,61 +185,55 @@ function JumpProblem(prob, aggregator::AbstractAggregatorAlgorithm, jumps::JumpS ## Spatial jumps handling if spatial_system !== nothing && hopping_constants !== nothing && - !is_spatial(aggregator) # check if need to flatten + !is_spatial(aggregator) prob, maj = flatten(maj, prob, spatial_system, hopping_constants; kwargs...) end - ## Constant and variable rate handling + if is_spatial(aggregator) + (num_crjs(jumps) == num_vrjs(jumps) == 0) || error("Spatial aggregators only support MassActionJumps currently.") + kwargs = merge((; hopping_constants, spatial_system), kwargs) + end + + ndiscjumps = get_num_majumps(maj) + num_crjs(jumps) + + # separate bounded variable rate jumps *if* the aggregator can use them + if use_vrj_bounds && supports_variablerates(aggregator) && (num_bndvrjs(jumps) > 0) + bvrjs = filter(isbounded, jumps.variable_jumps) + cvrjs = filter(!isbounded, jumps.variable_jumps) + kwargs = merge((; variable_jumps = bvrjs), kwargs) + ndiscjumps += length(bvrjs) + else + bvrjs = nothing + cvrjs = jumps.variable_jumps + end + t, end_time, u = prob.tspan[1], prob.tspan[2], prob.u0 - if length(jumps.variable_jumps) == 0 && (length(jumps.constant_jumps) == 0) && - (maj === nothing) && !is_spatial(aggregator) - # check if there are no jumps - new_prob = prob - variable_jump_callback = CallbackSet() - cont_agg = JumpSet().variable_jumps + # handle majs, crjs, and bounded vrjs + if (ndiscjumps == 0) && !is_spatial(aggregator) disc_agg = nothing constant_jump_callback = CallbackSet() - elseif supports_variablerates(aggregator) - new_prob = prob + else disc_agg = aggregate(aggregator, u, prob.p, t, end_time, jumps.constant_jumps, maj, - save_positions, rng; variable_jumps = jumps.variable_jumps, - kwargs...) + save_positions, rng; kwargs...) constant_jump_callback = DiscreteCallback(disc_agg) + end + + # handle any remaining vrjs + if length(cvrjs) > 0 + new_prob = extend_problem(prob, cvrjs; rng) + variable_jump_callback = build_variable_callback(CallbackSet(), 0, cvrjs...; rng) + cont_agg = cvrjs + else + new_prob = prob variable_jump_callback = CallbackSet() cont_agg = JumpSet().variable_jumps - else - # the fallback is to handle each jump type separately - if (length(jumps.constant_jumps) == 0) && (maj === nothing) && - !is_spatial(aggregator) - disc_agg = nothing - constant_jump_callback = CallbackSet() - else - disc_agg = aggregate(aggregator, u, prob.p, t, end_time, jumps.constant_jumps, - maj, - save_positions, rng; spatial_system = spatial_system, - hopping_constants = hopping_constants, kwargs...) - constant_jump_callback = DiscreteCallback(disc_agg) - end - - if length(jumps.variable_jumps) > 0 && !is_spatial(aggregator) - new_prob = extend_problem(prob, jumps; rng = rng) - variable_jump_callback = build_variable_callback(CallbackSet(), 0, - jumps.variable_jumps...; - rng = rng) - cont_agg = jumps.variable_jumps - else - new_prob = prob - variable_jump_callback = CallbackSet() - cont_agg = JumpSet().variable_jumps - end end jump_cbs = CallbackSet(constant_jump_callback, variable_jump_callback) - iip = isinplace_jump(prob, jumps.regular_jump) - solkwargs = make_kwarg(; callback) + JumpProblem{iip, typeof(new_prob), typeof(aggregator), typeof(jump_cbs), typeof(disc_agg), typeof(cont_agg), @@ -248,7 +245,7 @@ function JumpProblem(prob, aggregator::AbstractAggregatorAlgorithm, jumps::JumpS end function extend_problem(prob::DiffEqBase.AbstractDiscreteProblem, jumps; rng = DEFAULT_RNG) - error("VariableRateJumps require a continuous problem, like an ODE/SDE/DDE/DAE problem.") + error("General `VariableRateJump`s require a continuous problem, like an ODE/SDE/DDE/DAE problem. To use a `DiscreteProblem` bounded `VariableRateJump`s must be used. See the JumpProcesses docs.") end function extend_problem(prob::DiffEqBase.AbstractODEProblem, jumps; rng = DEFAULT_RNG) @@ -257,19 +254,19 @@ function extend_problem(prob::DiffEqBase.AbstractODEProblem, jumps; rng = DEFAUL jump_f = let _f = _f function jump_f(du::ExtendedJumpArray, u::ExtendedJumpArray, p, t) _f(du.u, u.u, p, t) - update_jumps!(du, u, p, t, length(u.u), jumps.variable_jumps...) + update_jumps!(du, u, p, t, length(u.u), jumps...) end end ttype = eltype(prob.tspan) u0 = ExtendedJumpArray(prob.u0, - [-randexp(rng, ttype) for i in 1:length(jumps.variable_jumps)]) + [-randexp(rng, ttype) for i in 1:length(jumps)]) remake(prob, f = ODEFunction{true}(jump_f), u0 = u0) end function extend_problem(prob::DiffEqBase.AbstractSDEProblem, jumps; rng = DEFAULT_RNG) function jump_f(du, u, p, t) prob.f(du.u, u.u, p, t) - update_jumps!(du, u, p, t, length(u.u), jumps.variable_jumps...) + update_jumps!(du, u, p, t, length(u.u), jumps...) end if prob.noise_rate_prototype === nothing @@ -284,18 +281,18 @@ function extend_problem(prob::DiffEqBase.AbstractSDEProblem, jumps; rng = DEFAUL ttype = eltype(prob.tspan) u0 = ExtendedJumpArray(prob.u0, - [-randexp(rng, ttype) for i in 1:length(jumps.variable_jumps)]) + [-randexp(rng, ttype) for i in 1:length(jumps)]) remake(prob, f = SDEFunction{true}(jump_f, jump_g), g = jump_g, u0 = u0) end function extend_problem(prob::DiffEqBase.AbstractDDEProblem, jumps; rng = DEFAULT_RNG) jump_f = function (du, u, h, p, t) prob.f(du.u, u.u, h, p, t) - update_jumps!(du, u, p, t, length(u.u), jumps.variable_jumps...) + update_jumps!(du, u, p, t, length(u.u), jumps...) end ttype = eltype(prob.tspan) u0 = ExtendedJumpArray(prob.u0, - [-randexp(rng, ttype) for i in 1:length(jumps.variable_jumps)]) + [-randexp(rng, ttype) for i in 1:length(jumps)]) remake(prob, f = DDEFunction{true}(jump_f), u0 = u0) end @@ -303,11 +300,11 @@ end function extend_problem(prob::DiffEqBase.AbstractDAEProblem, jumps; rng = DEFAULT_RNG) jump_f = function (out, du, u, p, t) prob.f(out.u, du.u, u.u, t) - update_jumps!(du, u, t, length(u.u), jumps.variable_jumps...) + update_jumps!(du, u, t, length(u.u), jumps...) end ttype = eltype(prob.tspan) u0 = ExtendedJumpArray(prob.u0, - [-randexp(rng, ttype) for i in 1:length(jumps.variable_jumps)]) + [-randexp(rng, ttype) for i in 1:length(jumps)]) remake(prob, f = DAEFunction{true}(jump_f), u0 = u0) end From 9b41d17188f34be76220f826742e71b70a8d5fb0 Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Sun, 1 Jan 2023 17:34:29 -0500 Subject: [PATCH 69/72] format --- src/aggregators/coevolve.jl | 2 +- src/jumps.jl | 7 ++++--- src/problem.jl | 5 ++++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/aggregators/coevolve.jl b/src/aggregators/coevolve.jl index b8cd5761d..84cf3cac1 100644 --- a/src/aggregators/coevolve.jl +++ b/src/aggregators/coevolve.jl @@ -61,7 +61,7 @@ function aggregate(aggregator::Coevolve, u, p, t, end_time, constant_jumps, ncrjs = (constant_jumps === nothing) ? 0 : length(constant_jumps) nvrjs = (variable_jumps === nothing) ? 0 : length(variable_jumps) - nrjs = ncrjs + nvrjs + nrjs = ncrjs + nvrjs affects! = Vector{AffectWrapper}(undef, nrjs) rates = Vector{RateWrapper}(undef, nvrjs) lrates = similar(rates) diff --git a/src/jumps.jl b/src/jumps.jl index dec0a7196..c51790f5c 100644 --- a/src/jumps.jl +++ b/src/jumps.jl @@ -156,9 +156,9 @@ struct VariableRateJump{R, F, R2, R3, R4, I, T, T2} <: AbstractJump end isbounded(::VariableRateJump) = true -isbounded(::VariableRateJump{R,F,R2,Nothing}) where {R,F,R2} = false +isbounded(::VariableRateJump{R, F, R2, Nothing}) where {R, F, R2} = false haslrate(::VariableRateJump) = true -haslrate(::VariableRateJump{R, F, Nothing}) where {R,F} = false +haslrate(::VariableRateJump{R, F, Nothing}) where {R, F} = false nullrate(u, p, t::T) where {T <: Number} = zero(T) """ @@ -184,7 +184,8 @@ function VariableRateJump(rate, affect!; end if lrate !== nothing - (urate !== nothing) || error("If a lower bound rate, `lrate`, is given than an upper bound rate, `urate`, and rate interval, `rateinterval`, must also be provided.") + (urate !== nothing) || + error("If a lower bound rate, `lrate`, is given than an upper bound rate, `urate`, and rate interval, `rateinterval`, must also be provided.") end VariableRateJump(rate, affect!, lrate, urate, rateinterval, idxs, rootfind, diff --git a/src/problem.jl b/src/problem.jl index 91a8a447d..c1c03435d 100644 --- a/src/problem.jl +++ b/src/problem.jl @@ -186,11 +186,14 @@ function JumpProblem(prob, aggregator::AbstractAggregatorAlgorithm, jumps::JumpS ## Spatial jumps handling if spatial_system !== nothing && hopping_constants !== nothing && !is_spatial(aggregator) + (num_crjs(jumps) == num_vrjs(jumps) == 0) || + error("Spatial aggregators only support MassActionJumps currently.") prob, maj = flatten(maj, prob, spatial_system, hopping_constants; kwargs...) end if is_spatial(aggregator) - (num_crjs(jumps) == num_vrjs(jumps) == 0) || error("Spatial aggregators only support MassActionJumps currently.") + (num_crjs(jumps) == num_vrjs(jumps) == 0) || + error("Spatial aggregators only support MassActionJumps currently.") kwargs = merge((; hopping_constants, spatial_system), kwargs) end From 1103cd0a37f12634b712ef72c1fe387bef611121 Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Sun, 1 Jan 2023 19:26:24 -0500 Subject: [PATCH 70/72] add tests --- test/hawkes_test.jl | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/test/hawkes_test.jl b/test/hawkes_test.jl index 32fbb3ca8..0de428e36 100644 --- a/test/hawkes_test.jl +++ b/test/hawkes_test.jl @@ -131,3 +131,37 @@ for (i, alg) in enumerate(algs) @test isapprox(mean(λs), Eλ; atol = 0.01) @test isapprox(var(λs), Varλ; atol = 0.001) end + +# test stepping Coevolve with continuous integrator and bounded jumps +let + oprob = ODEProblem(f!, u0, tspan, p) + jumps = hawkes_jump(u0, g, h) + jprob = JumpProblem(oprob, Coevolve(), jumps...; dep_graph = g, rng) + @test ((jprob.variable_jumps === nothing) || isempty(jprob.variable_jumps)) + sols = Vector{ODESolution}(undef, Nsims) + for n in 1:Nsims + reset_history!(h) + sols[n] = solve(jprob, Tsit5()) + end + λs = permutedims(mapreduce((sol) -> empirical_rate(sol), hcat, sols)) + @test isapprox(mean(λs), Eλ; atol = 0.01) + @test isapprox(var(λs), Varλ; atol = 0.001) +end + +# test disabling bounded jumps and using continuous integrator +let + oprob = ODEProblem(f!, u0, tspan, p) + jumps = hawkes_jump(u0, g, h) + jprob = JumpProblem(oprob, Coevolve(), jumps...; dep_graph = g, rng, + use_vrj_bounds = false) + @test length(jprob.variable_jumps) == 1 + sols = Vector{ODESolution}(undef, Nsims) + for n in 1:Nsims + reset_history!(h) + sols[n] = solve(jprob, Tsit5()) + end + cols = length(sols[1].u[1].u) + λs = permutedims(mapreduce((sol) -> empirical_rate(sol), hcat, sols))[:, 1:cols] + @test isapprox(mean(λs), Eλ; atol = 0.01) + @test isapprox(var(λs), Varλ; atol = 0.001) +end From e4556d71bb9e68a159fee95675701aba8f5cba90 Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Mon, 2 Jan 2023 23:25:14 -0500 Subject: [PATCH 71/72] Update docs/src/jump_types.md Co-authored-by: Guilherme Zagatti --- docs/src/jump_types.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/jump_types.md b/docs/src/jump_types.md index 1d5ebcd41..1590e47ee 100644 --- a/docs/src/jump_types.md +++ b/docs/src/jump_types.md @@ -69,7 +69,7 @@ change between the times at which two consecutive jumps occur. A [`MassActionJump`](@ref)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 comnpared to using +`MassActionJump` type will offer improved performance compared to using multiple `ConstantRateJump`s. Note, only one `MassActionJump` should be defined per [`JumpProblem`](@ref); it is then responsible for handling all mass action reaction type jumps. For systems with both mass action jumps and non-mass action From b0d72b52d4d98e4a32b42f266a45064aa0ffb371 Mon Sep 17 00:00:00 2001 From: Sam Isaacson Date: Mon, 2 Jan 2023 23:36:35 -0500 Subject: [PATCH 72/72] Apply suggestions from code review Co-authored-by: Guilherme Zagatti --- docs/src/tutorials/discrete_stochastic_example.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/tutorials/discrete_stochastic_example.md b/docs/src/tutorials/discrete_stochastic_example.md index b97631aed..52453744c 100644 --- a/docs/src/tutorials/discrete_stochastic_example.md +++ b/docs/src/tutorials/discrete_stochastic_example.md @@ -489,12 +489,12 @@ infection then decreases exponentially to a basal level. In this case, we must keep track of the time of infection events. Let the history ``H(t)`` contain the timestamps of all ``I(t)`` active infections. The rate of infection is then ```math -\beta S(t) I(t) + \alpha S(t) \sum_{t_i \in H(t)} \exp(-\gamma (t - t_i)) +\beta_1 S(t) I(t) + \alpha S(t) \sum_{t_i \in H(t)} \exp(-\gamma (t - t_i)) ``` -where ``\beta`` is the basal rate of infection, ``\alpha`` is the spike in the +where ``\beta_1`` is the basal rate of infection, ``\alpha`` is the spike in the rate of infection, and ``\gamma`` is the rate at which the spike decreases. Here we choose parameters such that infectivity rate due to a single infected -individual returns to the basal rate after spiking to ``\beta + \alpha``. In +individual returns to the basal rate after spiking to ``\beta_1 + \alpha``. In other words, we are modelling a situation in infected individuals gradually become less infectious prior to recovering. Our parameters are then