33# Exact Rejection-based Stochastic Simulation Algorithm (RSSA)
44# for continuous-time Markov jump processes (exponential clocks).
55#
6- # Algorithmic core:
7- # - Maintain per-clock *true* rate a_i and a certified upper bound \bar a_i >= a_i.
8- # - Maintain Ā = sum_i \bar a_i and a Fenwick tree over { \bar a_i } for O(log N) sampling.
9- # - Draw candidate times from Exp(Ā). Select candidate clock i by Categorical(\bar a_i/Ā).
10- # - Accept with probability a_i / \bar a_i; otherwise reject and continue thinning.
11- #
12- # Exactness: standard thinning of a Poisson process with rate Ā, with acceptance a_i/\bar a_i,
13- # yields the target Markov jump process (homogeneous propensities). See Thanh et al. (2014, 2015).
14- #
15- # Notes:
16- # - This implementation targets time-homogeneous propensities (Exponential only).
17- # For time-dependent rates (tRSSA), you need piecewise-time envelopes and an integral sampler;
18- # these can be added as a thin extension without changing the public interface.
19- #
206using Random
217using Distributions: UnivariateDistribution, Exponential, rate
228
@@ -29,6 +15,20 @@ Rejection-based SSA with global Fenwick tree for candidate selection.
2915This is for exponential distributions only not time-dependent rates.
3016- `bound_factor` ≥ 1.0 controls default upper bounds: \\ bar a_i ← max(\\ bar a_i, bound_factor * a_i).
3117 Set to 1.0 for no rejections (reduces to direct-method timing with tree selection).
18+
19+ # Algorithmic core:
20+ - Maintain per-clock *true* rate a_i and a certified upper bound \b ar a_i >= a_i.
21+ - Maintain Ā = sum_i \b ar a_i and a Fenwick tree over { \b ar a_i } for O(log N) sampling.
22+ - Draw candidate times from Exp(Ā). Select candidate clock i by Categorical(\b ar a_i/Ā).
23+ - Accept with probability a_i / \b ar a_i; otherwise reject and continue thinning.
24+
25+ Exactness: standard thinning of a Poisson process with rate Ā, with acceptance a_i/\b ar a_i,
26+ yields the target Markov jump process (homogeneous propensities). See Thanh et al. (2014, 2015).
27+
28+ Notes:
29+ - This implementation targets time-homogeneous propensities (Exponential only).
30+ For time-dependent rates (tRSSA), you need piecewise-time envelopes and an integral sampler;
31+ these can be added as a thin extension without changing the public interface.
3232"""
3333mutable struct RSSA{K,T} <: SSA{K,T}
3434 idx_of:: Dict{K,Int} # key → index (stable; indices are never reused)
@@ -141,13 +141,25 @@ function _ensure_index!(s::RSSA{K,T}, key::K) where {K,T}
141141 if idx != 0
142142 return idx
143143 end
144+
145+ # Append new, disabled clock
144146 push!(s. keys_vec, key)
145147 push!(s. present, false )
146148 push!(s. a, zero(T))
147149 push!(s. abar, zero(T))
148150 push!(s. bit, zero(T))
149151 idx = length(s. keys_vec)
150152 s. idx_of[key] = idx
153+
154+ # Rebuild Fenwick tree over current bounds for enabled clocks
155+ fill!(s. bit, zero(T))
156+ for j in 1 : idx
157+ if s. present[j] && s. abar[j] > zero(T)
158+ _bit_add!(s. bit, j, s. abar[j])
159+ end
160+ end
161+ # Note: we do *not* touch s.Abar here; it is still the sum of abar over enabled clocks.
162+
151163 return idx
152164end
153165
@@ -172,20 +184,26 @@ end
172184function set_global_bound_factor!(s:: RSSA{K,T} , bf) where {K,T}
173185 s. bound_factor = convert(T, bf)
174186 s. bound_factor < one(T) && (s. bound_factor = one(T))
175- # rebuild BIT and Abar
176187 fill!(s. bit, zero(T))
177188 s. Abar = zero(T)
178189 for idx in 1 : length(s. keys_vec)
179190 if s. present[idx]
180- s. abar[idx] = max(s. bound_factor * s. a[idx], eps(T))
181- _bit_add!(s. bit, idx, s. abar[idx])
182- s. Abar += s. abar[idx]
191+ if s. a[idx] <= zero(T)
192+ s. abar[idx] = zero(T)
193+ else
194+ s. abar[idx] = s. bound_factor * s. a[idx]
195+ end
196+ if s. abar[idx] > zero(T)
197+ _bit_add!(s. bit, idx, s. abar[idx])
198+ s. Abar += s. abar[idx]
199+ end
183200 end
184201 end
185202 _invalidate!(s)
186203 return s
187204end
188205
206+
189207# ---- interface methods ----
190208
191209# No scheduled times to perturb; just drop cached sample.
@@ -214,7 +232,8 @@ function enable!(s::RSSA{K,T},
214232 s. a[idx] = λ
215233
216234 # Choose a default bound if needed
217- newabar = max(oldabar, s. bound_factor * λ)
235+ # If λ is zero, force abar to zero to avoid infinite loops in next()
236+ newabar = λ > zero(T) ? max(oldabar, s. bound_factor * λ) : zero(T)
218237 if ! old_enabled
219238 # enable
220239 s. present[idx] = true
259278
260279# After firing, nothing to remove; just invalidate cached sample.
261280function fire!(s:: RSSA{K,T} , key:: K , when:: T ) where {K,T}
262- _invalidate !(s)
281+ disable !(s, key, when )
263282 return s
264283end
265284
@@ -275,7 +294,10 @@ function next(s::RSSA{K,T}, when::T, rng::AbstractRNG) where {K,T}
275294 end
276295
277296 t = when
297+ iteration = 0
278298 while true
299+ iteration += 1
300+
279301 # candidate time from Exp(Abar)
280302 Δ = rand(rng, Exponential(inv(s. Abar)))
281303 t += Δ
@@ -286,6 +308,7 @@ function next(s::RSSA{K,T}, when::T, rng::AbstractRNG) where {K,T}
286308
287309 # in case of numerical corner cases, resample
288310 if j < 1 || j > length(s. keys_vec) || ! s. present[j] || s. abar[j] <= zero(T)
311+
289312 continue
290313 end
291314
0 commit comments