1function [pi, outspace, depRates, sn] = solver_ssa_nrm_space(sn, options)
2% SOLVER_SSA_NRM_SPACE Steady‑state analysis via the Next‑Reaction Method (SSA)
4% [PI, SSQ, ARVRATES, DEPRATES, SN] = SOLVER_SSA_NRM_SPACE(SN, OPTIONS)
5% runs a stochastic simulation of the queueing network described in SN
6%
for OPTIONS.samples reaction firings
using Gibson & Bruck
's
7% Next‑Reaction Method. During the run it:
8% • observes every distinct global state visited and the time spent in it;
9% • accumulates the per‑state propensities of **all** enabled reactions,
10% including *self‑loops* (service completions routed back to the same
14% PI – 1×S vector of empirical steady‑state probabilities
15% (sojourn‑time fractions) for the S unique states;
16% SSQ – S×(M·R) matrix listing those states row‑by‑row in the
17% flattened (station, class) order;
18% DEPRATES – S×(M·R) matrix of total departure rates from each queue
19% in the corresponding state;
20% SN – (Possibly updated) network structure.
22% See also NEXT_REACTION_METHOD.
24% ---------------------------------------------------------------------
25% Parameters & shorthands
26% ---------------------------------------------------------------------
27samples = options.samples;
31% ---------------------------------------------------------------------
32% Stoichiometry & reaction mapping (self‑loops included) ----------------
33% ---------------------------------------------------------------------
34S = zeros(0, I*R); % will transpose at the end
39% this is currently M^2*R^2, it can be lowered to M*R decoupling the
45 fromIR(k,:) = [ind, r];
46 fromIdx(k) = (ind-1)*R + r;
49 Srow = zeros(1, I*R); % build stoichiometry row
51 Srow(fromIdx(k)) = -Inf;
53 Srow(fromIdx(k)) = -1;
56 if sn.rtnodes((ind-1)*R+r, (jnd-1)*R+s) > 0
57 toIdx{k}(end+1) = (jnd-1)*R + s;
58 p = sn.rtnodes((ind-1)*R+r, (jnd-1)*R+s);
60 Srow((jnd-1)*R + s) = Srow((jnd-1)*R + s) + p;
68S = S.'; % states × reactions
70% ---------------------------------------------------------------------
71% Initial state vector --------------------------------------------------
72% ---------------------------------------------------------------------
73nvec0 = zeros(I*R,1); % initial state (aggregate state)
74bufferedSched = [SchedStrategy.FCFS, SchedStrategy.LCFS];
75buffers0 = cell(I,1); % per-node ordered buffer of waiting job
classes (FCFS/LCFS)
81 state_i = state{sn.nodeToStateful(ind)};
82 [~,nir] = State.toMarginalAggr(sn, ind, state_i);
85 if sn.nodetype(ind) == NodeType.Source
88 line_error(mfilename,
'Infinite population error.');
91 nvec0((ind-1)*R + r,1) = nir(r);
94 % Populate buffers
for buffered
nodes from the raw state vector
95 % (only stations have FCFS/LCFS scheduling; skip non-station
96 % stateful
nodes such as RROBIN dispatchers/Routers and Caches)
97 ist = sn.nodeToStation(ind);
98 if ist >= 1 && any(sn.sched(ist) == bufferedSched)
99 sumK = sum(sn.phasessz(ist,:));
100 sumNvars = sum(sn.nvars(ind,:));
101 bufCols = size(state_i,2) - sumK - sumNvars;
103 classId = state_i(1,pos);
104 if classId >= 1 && classId <= R
105 buffers0{ind}(end+1) = classId; % addLast
107 % classId == 0 means empty position, skip
118 ist = sn.nodeToStation(ind);
119 muir = sn.rates(ist,r);
123 mi(ind,1) = sn.nservers(ist);
127 rates(ind,r) = GlobalConstants.Immediate;
128 mi(ind,1) = GlobalConstants.MaxInt;
131 mi(isinf(mi)) = GlobalConstants.MaxInt;
134% Propensity function ---------------------------------------------------
135epstol = GlobalConstants.Zero;
137for j=1:length(fromIdx)
138 if sn.isstation(fromIR(j,1))
139 switch sn.sched(sn.nodeToStation(fromIR(j,1)))
140 case SchedStrategy.EXT
141 a{j} = @(X, bufs) rates(fromIR(j,1), fromIR(j,2));
142 case SchedStrategy.INF
143 a{j} = @(X, bufs) rates(fromIR(j,1), fromIR(j,2)) * X(fromIdx(j));
144 case SchedStrategy.PS
145 if R == 1 % single
class
146 a{j} = @(X, bufs) rates(fromIR(j,1), fromIR(j,2)) * min( mi(fromIR(j,1)), X(fromIdx(j)));
148 a{j} = @(X, bufs) rates(fromIR(j,1), fromIR(j,2)) * ( X(fromIdx(j)) ./ ...
149 (epstol+sum( X(((fromIR(j,1)-1)*R + 1):((fromIR(j,1)-1)*R + R)) ) )) * ...
150 min( mi(fromIR(j,1)), ...
151 (epstol+sum( X(((fromIR(j,1)-1)*R + 1):((fromIR(j,1)-1)*R + R)) ) ));
153 case {SchedStrategy.FCFS, SchedStrategy.LCFS}
154 % Rate proportional to the jobs actually being served, i.e. the
155 %
class-r population minus the
class-r jobs waiting in buffer.
156 a{j} = @(X, bufs) rates(fromIR(j,1), fromIR(j,2)) * ...
157 max(0, X(fromIdx(j)) - sum(bufs{fromIR(j,1)} == fromIR(j,2)));
160 a{j} = @(X, bufs) rates(fromIR(j,1), fromIR(j,2)) * min(1, X(fromIdx(j)));
164% Propensity functions dependencies -----------------------------------
165D = cell(1,size(S,2));
167 J = find(S(:,k))'; % set of state variables affected by reaction k
172 r = mod(pos-1, R) + 1;
173 ind = ((pos-r)/R) + 1;
174 vecd(end+1:end+R) = ((ind-1)*R + 1) : (ind*R);
176 % vecd now contains all state variables affected by the firing of
177 % reaction k. We now find the propensity functions that depend
183 vecs = [vecs,find(S(vecd(j),:)<0)];
191% Having accounted
for them in D, we can now remove self-loops markings
193% ---------------------------------------------------------------------
194% Run SSA/NRM -----------------------------------------------------------
195% ---------------------------------------------------------------------
196if false %snIsClosedModel(sn)
197 % mixed-radix hashing
198 reactCache = containers.Map(
'KeyType',
'uint64',
'ValueType',
'any');
200 mixedradix = [cumprod(repmat(1+njobs,1,I))];
201 mixedradix = [1,mixedradix(1:end-1)];
202 hashfun = @(v, bufs) uint64(mixedradix*v(:));
204 % buffer size unbounded so use string; the key combines the aggregate
205 % state vector with the ordered contents of every node buffer, so that
206 % FCFS/LCFS states differing only in queueing order remain distinct.
207 reactCache = containers.Map(
'KeyType',
'char',
'ValueType',
'any');
208 hashfun = @(v, bufs) [mat2str(v(:)
'), '|
', bufferHashAll(bufs)];
210[t, nvecsim, bufferStates, ~, ~] = next_reaction_method(S, D, a, nvec0, buffers0, samples, options, reactCache, hashfun, fromIR, mi, R, sn);
212% ---------------------------------------------------------------------
213% Empirical state probabilities ----------------------------------------
214% ---------------------------------------------------------------------
216% Find unique states keyed on both the aggregate state and the buffers
217numIntervals = size(nvecsim,2) - 1;
218stateKeys = cell(numIntervals,1);
219for i = 1:numIntervals
220 stateKeys{i} = hashfun(nvecsim(:,i), bufferStates{i});
222[~, ia, ic] = unique(stateKeys, 'stable
');
223outspace = nvecsim(:, ia).';
224outspaceBuffers = bufferStates(ia);
225timeAccum = accumarray(ic, dt(:));
226pi = timeAccum / sum(timeAccum);
228% ---------------------------------------------------------------------
229% Per‑state arrival / departure rates (self‑loops counted) -------------
230% ---------------------------------------------------------------------
231numStates = size(outspace,1);
232depRates = zeros(numStates, I*R);
235 a_state = reactCache(hashfun(outspace(st,:)
', outspaceBuffers{st}));
236 for j = 1:length(fromIdx)
237 depRates(st, fromIdx(j)) = depRates(st, fromIdx(j)) + a_state(j);
240end % solver_ssa_nrm_space
242% ======================================================================
243% Next-Reaction Method core --------------------------------------------
244% ======================================================================
245function [t, nvec, bufferStates, kfires, rfires] = next_reaction_method(S, D, a, nvec0, buffers0, samples, options, reactcache, hashfun, fromIR, mi, R, sn)
246numReactions = size(S,2);
248buffers = buffers0; % working copy of the per-node ordered buffers
250% when a reaction fires, this matrix helps selecting the probability that a
251% particular routing or phase is selected as a result ------------------
252P = S; P(P<0)=P(P<0)+1';
253fromIdx = cell(numReactions,1);
254toIdx = cell(numReactions,1);
255cdfVec = cell(numReactions,1);
257 nnzP(r) = nnz(
P(:,r));
259 fromIdx{r} = find(S(:,r)<0);
260 toIdx{r} = find(
P(:,r));
261 cdfVec{r} = cumsum(
P(toIdx{r},r));
265% initialise Gillespie clocks ------------------------------------------
268 Ak(k) = a{k}(nvec0, buffers);
271key = hashfun(nvec, buffers);
272reactcache(key) = Ak; % cache first state
's propensities
273Pk = -log(rand(1,numReactions));
274Tk = zeros(1,numReactions);
276tau = (Pk - Tk) ./ Ak;
278% logs -----------------------------------------------------------------
279tout = zeros(samples,1);
280nvecout = zeros(length(nvec0),samples);
281bufferStates = cell(1, samples+1);
282bufferStates{1} = buffers; % buffers in the initial state
283kfires = zeros(samples,1);
284rfires = zeros(samples,1);
287 [dt, kfire] = min(tau);
289 if isinf(dt), line_error(mfilename,'Deadlock. Quitting nrm method.
'); end
293 % update aggregate state
296 r = 1+find(rand>=cdfVec{kfire},1);
301 nvec(fromIdx{kfire}) = nvec(fromIdx{kfire}) - 1;
302 nvec(toIdx{kfire}(r)) = nvec(toIdx{kfire}(r)) + 1;
303 destPos = toIdx{kfire}(r);
305 nvec = nvec + S(:,kfire); % zero change for self-loops
306 dpos = find(S(:,kfire) > 0); % deterministic destination (single move)
312 % maintain FCFS/LCFS buffers given the source/destination of this firing
313 buffers = updateBuffers(kfire, nvec, buffers, fromIR, destPos, mi, R, sn);
317 % update rates for all reactions dependent on the last fired reaction
319 Ak(k) = a{k}(nvec, buffers);
322 key = hashfun(nvec, buffers);
323 if ~isKey(reactcache,key)
324 reactcache(key) = Ak; % store propensities of new state
327 % maintain random number pool
328 n_mod = mod(n,rand_pool_size);
330 rand_pool = rand(1+min(rand_pool_size, samples-n),1);
334 Pk(kfire) = Pk(kfire) - log(rand_pool(n_mod));
335 tau = (Pk - Tk) ./ Ak;
341 bufferStates{n+1} = buffers;
343 % do not count immediate events
345 print_progress(options, n);
347% Print newline after progress counter
348if isfield(options,'verbose
') && options.verbose
353nvec = [nvec0, nvecout];
355 function print_progress(opt, samples_collected)
356 if ~isfield(opt,'verbose
') || ~opt.verbose, return; end
357 if samples_collected == 1e3
358 line_printf('\nSSA samples: %8d
', samples_collected);
359 elseif opt.verbose == 2
360 if samples_collected == 0
361 line_printf('\nSSA samples: %9d
', samples_collected);
363 line_printf('\b\b\b\b\b\b\b\b\b%9d
', samples_collected);
365 elseif mod(samples_collected,1e3)==0 || opt.verbose == 2
366 line_printf('\b\b\b\b\b\b\b\b\b%9d
', samples_collected);
369end % next_reaction_method
371% ======================================================================
372% Buffer maintenance and hashing helpers for FCFS/LCFS nodes
373% ======================================================================
374function buffers = updateBuffers(kfire, nvec, buffers, fromIR, destPos, mi, R, sn)
375% Maintain the ordered per-node buffers when reaction KFIRE fires. A
376% departure removes the head/tail job of the source buffer; an arrival at a
377% buffered destination whose servers are all busy joins the buffer head.
378ind = fromIR(kfire,1); % source node of the firing
380% Handle departure from FCFS/LCFS source node
382 if ~isempty(buffers{ind})
383 buffers{ind}(end) = []; % pollLast
385elseif isLCFS(ind, sn)
386 if ~isempty(buffers{ind})
387 buffers{ind}(1) = []; % pollFirst
391% Handle arrival at a buffered destination node
392if ~isempty(destPos) && destPos > 0
393 jnd = floor((destPos-1)/R) + 1;
394 s = mod(destPos-1, R) + 1;
395 if isFCFS(jnd, sn) || isLCFS(jnd, sn)
396 totalAtDest = sum(nvec(((jnd-1)*R + 1):(jnd*R)));
397 if totalAtDest > mi(jnd)
398 % All servers busy - arriving job joins back of buffer
399 buffers{jnd} = [s, buffers{jnd}]; % addFirst
401 % Otherwise job went straight into service, buffer unchanged
406function s = bufferHashAll(bufs)
407% Buffer hash over all node indices (used to distinguish unique states).
408parts = cell(1, numel(bufs));
409for ind = 1:numel(bufs)
410 parts{ind} = [num2str(ind), ':
', mat2str(bufs{ind}(:)')];
412s = strjoin(parts,
'|');
415function tf = isFCFS(ind, sn)
418 ist = sn.nodeToStation(ind);
419 tf = (sn.sched(ist) == SchedStrategy.FCFS);
423function tf = isLCFS(ind, sn)
426 ist = sn.nodeToStation(ind);
427 tf = (sn.sched(ist) == SchedStrategy.LCFS);