1function [pi, SSq, depRates, sn] = solver_ssa_nrm_OM2R2(sn, options)
2% SOLVER_SSA_NRM Steady‑state analysis via Gibson & Bruck
's Next‑Reaction
3% Method (SSA) as modified in Anderson's, THE JOURNAL OF CHEMICAL PHYSICS
6% ---------------------------------------------------------------------
7% Parameters & shorthands
8% ---------------------------------------------------------------------
9samples = options.samples;
13% ---------------------------------------------------------------------
14% Stoichiometry & reaction mapping (self‑loops included) ----------------
15% ---------------------------------------------------------------------
16S = zeros(0, I*R); % will transpose at the end
21% see _kb/06-solver-catalog.md
for rationale (SSA NRM reaction mapping O(M^2*R^2))
27 if sn.rtnodes((ind-1)*R+r, (jnd-1)*R+s) == 0,
continue; end
29 fromIdx(k) = (ind-1)*R + r;
30 toIdx(k) = (jnd-1)*R + s;
31 rateIR(k,:) = [ind, r];
32 probIR(k) = sn.rtnodes((ind-1)*R+r, (jnd-1)*R+s);
34 % build stoichiometry row
36 if fromIdx(k) ~= toIdx(k)
37 Srow(fromIdx(k)) = -1;
39 else % mark self-loops
40 Srow(fromIdx(k)) = -Inf;
47S = S.
'; % states × reactions
49% ---------------------------------------------------------------------
50% Initial state vector --------------------------------------------------
51% ---------------------------------------------------------------------
52nvec0 = zeros(I*R,1); % initial state (aggregate state)
55 [~,nir] = State.toMarginalAggr(sn, ind, state{sn.nodeToStateful(ind)});
58 if sn.nodetype(ind) == NodeType.Source
61 nir(r) = GlobalConstants.MaxInt;
64 nvec0((ind-1)*R + r,1) = nir(r);
74 ist = sn.nodeToStation(ind);
75 rir = sn.rates(ist,r);
79 mi(ind,1) = sn.nservers(ist);
83 rates(ind,r) = GlobalConstants.Immediate;
84 mi(ind,1) = GlobalConstants.MaxInt;
87 mi(isinf(mi)) = GlobalConstants.MaxInt;
90% Propensity function ---------------------------------------------------
91epstol = GlobalConstants.Zero;
93for k=1:length(fromIdx)
94 if sn.isstation(rateIR(k,1))
95 switch sn.sched(sn.nodeToStation(rateIR(k,1)))
96 case SchedStrategy.EXT
97 a{k} = @(X) rates(rateIR(k,1), rateIR(k,2)) * ...
99 case SchedStrategy.INF
100 a{k} = @(X) rates(rateIR(k,1), rateIR(k,2)) * ...
101 probIR(k) * X(fromIdx(k));
102 case SchedStrategy.PS
103 if R == 1 % single class
104 a{k} = @(X) rates(rateIR(k,1), rateIR(k,2)) * ...
106 min( mi(rateIR(k,1)), X(fromIdx(k)));
108 a{k} = @(X) rates(rateIR(k,1), rateIR(k,2)) * ...
110 ( X(fromIdx(k)) ./ ...
111 (epstol+sum( X(((rateIR(k,1)-1)*R + 1):((rateIR(k,1)-1)*R + R)) ) )) * ...
112 min( mi(rateIR(k,1)), ...
113 (epstol+sum( X(((rateIR(k,1)-1)*R + 1):((rateIR(k,1)-1)*R + R)) ) ));
117 a{k} = @(X) rates(rateIR(k,1), rateIR(k,2)) * ...
118 probIR(k) * min(1, X(fromIdx(k)));
122% Propensity functions dependencies -----------------------------------
123D = cell(1,size(S,2));
125 J = find(S(:,k))'; % set of state variables affected by reaction k
130 r = mod(pos-1, R) + 1;
131 ind = ((pos-r)/R) + 1;
132 vecd(end+1:end+R) = ((ind-1)*R + 1) : (ind*R);
134 % vecd now contains all state variables affected by the firing of
135 % reaction k. We now find the propensity functions that depend
141 vecs = [vecs,find(S(vecd(j),:)<=-1)];
148% Having accounted
for them in D, we can now remove self-loops markings
150% ---------------------------------------------------------------------
151% Run SSA/NRM -----------------------------------------------------------
152% ---------------------------------------------------------------------
153if false %snIsClosedModel(sn)
154 % mixed-radix hashing
155 reactCache = containers.Map(
'KeyType',
'uint64',
'ValueType',
'any');
157 mixedradix = [cumprod(repmat(1+njobs,1,I))];
158 mixedradix = [1,mixedradix(1:end-1)];
159 hashfun = @(v) uint64(mixedradix*v(:));
161 % buffer size unbounded so use
string
162 reactCache = containers.Map(
'KeyType',
'char',
'ValueType',
'any');
163 hashfun = @(v) mat2str(v(:)
');
165[t, X] = next_reaction_method(S, D, a, nvec0, samples, options, reactCache, hashfun);
167% ---------------------------------------------------------------------
168% Empirical state probabilities ----------------------------------------
169% ---------------------------------------------------------------------
171[SSq, ~, ic] = unique(X(:,1:end-1).',
'rows',
'stable');
172timeAccum = accumarray(ic, dt);
173pi = timeAccum / sum(timeAccum);
175% ---------------------------------------------------------------------
176% Per‑state arrival / departure rates (self‑loops counted) -------------
177% ---------------------------------------------------------------------
178numStates = size(SSq,1);
179arvRates = zeros(numStates, I*R);
180depRates = zeros(numStates, I*R);
183 a_state = reactCache(hashfun(SSq(k,:)
'));
185 for ia = 1:length(fromIdx)
186 depRates(k, fromIdx(ia)) = depRates(k, fromIdx(ia)) + a_state(ia);
187 %arvRates(k, toIdx(ia)) = arvRates(k, toIdx(ia)) + a_state(ia);
192% ======================================================================
193% Next-Reaction Method core --------------------------------------------
194% ======================================================================
195function [t, nvecout, kfire] = next_reaction_method(S, D, a, nvec0, samples, options, reactcache, hashfun)
196numReactions = size(S,2);
199% initialise Gillespie clocks -----------------------------------------
201Ak = zeros(1,size(S,2));
207reactcache(key) = Ak; % cache first state's propensities
208Pk = -log(rand(1,numReactions));
209Tk = zeros(1,numReactions);
211tau = (Pk - Tk) ./ Ak;
213% logs -----------------------------------------------------------------
214tout = zeros(samples,1);
215nvecsim = zeros(length(nvec0),samples);
216kfires = zeros(samples,1);
220 [dt, kfire] = min(tau);
221 kfires(n) = kfire; % record reaction that fired
222 if isinf(dt), line_error(mfilename,
'Deadlock. Quitting nrm method.'); end
225 % update aggregate state
226 nvec = nvec + S(:,kfire); % zero change
for self-loops
229 % update rates
for all reactions dependent on the last fired one
235 if ~isKey(reactcache,key)
236 reactcache(key) = Ak; % store rates of
new state
239 % maintain random number pool
240 n_mod = mod(n,rand_pool_size);
242 rand_pool = rand(1+min(rand_pool_size, samples-n),1);
246 Pk(kfire) = Pk(kfire) - log(rand_pool(n_mod));
247 tau = (Pk - Tk) ./ Ak;
254 %
do not count immediate events
256 print_progress(options, n);
258% Print newline after progress counter
259if isfield(options,
'verbose') && options.verbose
264nvecout = [nvec0, nvecsim];
266 function print_progress(opt, samples_collected)
267 if ~isfield(opt,
'verbose') || ~opt.verbose || batchStartupOptionUsed,
return; end
268 if samples_collected == 1e3
269 line_printf(
'\nSSA samples: %8d', samples_collected);
270 elseif opt.verbose == 2
271 if samples_collected == 0
272 line_printf(
'\nSSA samples: %9d', samples_collected);
274 line_printf(
'\b\b\b\b\b\b\b\b\b%9d', samples_collected);
276 elseif mod(samples_collected,1e3)==0 || opt.verbose == 2
277 line_printf(
'\b\b\b\b\b\b\b\b\b%9d', samples_collected);
280end % next_reaction_method