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% This
is currently M^2*R^2, it may be lowered to M*R decoupling the
22% routing from the departure rate, but then the caching
for the rates in
23% each state becomes less efficient as a given state in M*R can yield
24% multiple outgoing rates depending on the routing target. Also,
this
25% would still require a M^2*R^2 loop at the end to compute arvRates.
31 if sn.rtnodes((ind-1)*R+r, (jnd-1)*R+s) == 0,
continue; end
33 fromIdx(k) = (ind-1)*R + r;
34 toIdx(k) = (jnd-1)*R + s;
35 rateIR(k,:) = [ind, r];
36 probIR(k) = sn.rtnodes((ind-1)*R+r, (jnd-1)*R+s);
38 % build stoichiometry row
40 if fromIdx(k) ~= toIdx(k)
41 Srow(fromIdx(k)) = -1;
43 else % mark self-loops
44 Srow(fromIdx(k)) = -Inf;
51S = S.
'; % states × reactions
53% ---------------------------------------------------------------------
54% Initial state vector --------------------------------------------------
55% ---------------------------------------------------------------------
56nvec0 = zeros(I*R,1); % initial state (aggregate state)
59 [~,nir] = State.toMarginalAggr(sn, ind, state{sn.nodeToStateful(ind)});
62 if sn.nodetype(ind) == NodeType.Source
65 nir(r) = GlobalConstants.MaxInt;
68 nvec0((ind-1)*R + r,1) = nir(r);
78 ist = sn.nodeToStation(ind);
79 rir = sn.rates(ist,r);
83 mi(ind,1) = sn.nservers(ist);
87 rates(ind,r) = GlobalConstants.Immediate;
88 mi(ind,1) = GlobalConstants.MaxInt;
91 mi(isinf(mi)) = GlobalConstants.MaxInt;
94% Propensity function ---------------------------------------------------
95epstol = GlobalConstants.Zero;
97for k=1:length(fromIdx)
98 if sn.isstation(rateIR(k,1))
99 switch sn.sched(sn.nodeToStation(rateIR(k,1)))
100 case SchedStrategy.EXT
101 a{k} = @(X) rates(rateIR(k,1), rateIR(k,2)) * ...
103 case SchedStrategy.INF
104 a{k} = @(X) rates(rateIR(k,1), rateIR(k,2)) * ...
105 probIR(k) * X(fromIdx(k));
106 case SchedStrategy.PS
107 if R == 1 % single class
108 a{k} = @(X) rates(rateIR(k,1), rateIR(k,2)) * ...
110 min( mi(rateIR(k,1)), X(fromIdx(k)));
112 a{k} = @(X) rates(rateIR(k,1), rateIR(k,2)) * ...
114 ( X(fromIdx(k)) ./ ...
115 (epstol+sum( X(((rateIR(k,1)-1)*R + 1):((rateIR(k,1)-1)*R + R)) ) )) * ...
116 min( mi(rateIR(k,1)), ...
117 (epstol+sum( X(((rateIR(k,1)-1)*R + 1):((rateIR(k,1)-1)*R + R)) ) ));
121 a{k} = @(X) rates(rateIR(k,1), rateIR(k,2)) * ...
122 probIR(k) * min(1, X(fromIdx(k)));
126% Propensity functions dependencies -----------------------------------
127D = cell(1,size(S,2));
129 J = find(S(:,k))'; % set of state variables affected by reaction k
134 r = mod(pos-1, R) + 1;
135 ind = ((pos-r)/R) + 1;
136 vecd(end+1:end+R) = ((ind-1)*R + 1) : (ind*R);
138 % vecd now contains all state variables affected by the firing of
139 % reaction k. We now find the propensity functions that depend
145 vecs = [vecs,find(S(vecd(j),:)<=-1)];
152% Having accounted
for them in D, we can now remove self-loops markings
154% ---------------------------------------------------------------------
155% Run SSA/NRM -----------------------------------------------------------
156% ---------------------------------------------------------------------
157if false %snIsClosedModel(sn)
158 % mixed-radix hashing
159 reactCache = containers.Map(
'KeyType',
'uint64',
'ValueType',
'any');
161 mixedradix = [cumprod(repmat(1+njobs,1,I))];
162 mixedradix = [1,mixedradix(1:end-1)];
163 hashfun = @(v) uint64(mixedradix*v(:));
165 % buffer size unbounded so use
string
166 reactCache = containers.Map(
'KeyType',
'char',
'ValueType',
'any');
167 hashfun = @(v) mat2str(v(:)
');
169[t, X] = next_reaction_method(S, D, a, nvec0, samples, options, reactCache, hashfun);
171% ---------------------------------------------------------------------
172% Empirical state probabilities ----------------------------------------
173% ---------------------------------------------------------------------
175[SSq, ~, ic] = unique(X(:,1:end-1).',
'rows',
'stable');
176timeAccum = accumarray(ic, dt);
177pi = timeAccum / sum(timeAccum);
179% ---------------------------------------------------------------------
180% Per‑state arrival / departure rates (self‑loops counted) -------------
181% ---------------------------------------------------------------------
182numStates = size(SSq,1);
183arvRates = zeros(numStates, I*R);
184depRates = zeros(numStates, I*R);
187 a_state = reactCache(hashfun(SSq(k,:)
'));
189 for ia = 1:length(fromIdx)
190 depRates(k, fromIdx(ia)) = depRates(k, fromIdx(ia)) + a_state(ia);
191 %arvRates(k, toIdx(ia)) = arvRates(k, toIdx(ia)) + a_state(ia);
196% ======================================================================
197% Next-Reaction Method core --------------------------------------------
198% ======================================================================
199function [t, nvecout, kfire] = next_reaction_method(S, D, a, nvec0, samples, options, reactcache, hashfun)
200numReactions = size(S,2);
203% initialise Gillespie clocks -----------------------------------------
205Ak = zeros(1,size(S,2));
211reactcache(key) = Ak; % cache first state's propensities
212Pk = -log(rand(1,numReactions));
213Tk = zeros(1,numReactions);
215tau = (Pk - Tk) ./ Ak;
217% logs -----------------------------------------------------------------
218tout = zeros(samples,1);
219nvecsim = zeros(length(nvec0),samples);
220kfires = zeros(samples,1);
224 [dt, kfire] = min(tau);
225 kfires(n) = kfire; % record reaction that fired
226 if isinf(dt), line_error(mfilename,
'Deadlock. Quitting nrm method.'); end
229 % update aggregate state
230 nvec = nvec + S(:,kfire); % zero change
for self-loops
233 % update rates
for all reactions dependent on the last fired one
239 if ~isKey(reactcache,key)
240 reactcache(key) = Ak; % store rates of
new state
243 % maintain random number pool
244 n_mod = mod(n,rand_pool_size);
246 rand_pool = rand(1+min(rand_pool_size, samples-n),1);
250 Pk(kfire) = Pk(kfire) - log(rand_pool(n_mod));
251 tau = (Pk - Tk) ./ Ak;
258 %
do not count immediate events
260 print_progress(options, n);
262% Print newline after progress counter
263if isfield(options,
'verbose') && options.verbose
268nvecout = [nvec0, nvecsim];
270 function print_progress(opt, samples_collected)
271 if ~isfield(opt,
'verbose') || ~opt.verbose,
return; end
272 if samples_collected == 1e3
273 line_printf(
'\nSSA samples: %8d', samples_collected);
274 elseif opt.verbose == 2
275 if samples_collected == 0
276 line_printf(
'\nSSA samples: %9d', samples_collected);
278 line_printf(
'\b\b\b\b\b\b\b\b\b%9d', samples_collected);
280 elseif mod(samples_collected,1e3)==0 || opt.verbose == 2
281 line_printf(
'\b\b\b\b\b\b\b\b\b%9d', samples_collected);
284end % next_reaction_method