1function [QN, UN, RN, TN, CN, XN, lG, sn] = solver_ssa_nrm(sn, options)
2% SOLVER_SSA_NRM Steady‑state analysis via the Next‑Reaction Method (SSA)
4% [QN, UN, RN, TN, CN, XN, LG, SN] = SOLVER_SSA_NRM(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% • computes performance metrics directly during simulation;
9% • returns standard queueing performance measures.
12% QN – M×K matrix of mean queue lengths
13% UN – M×K matrix of utilizations
14% RN – M×K matrix of response times
15% TN – M×K matrix of throughputs
16% CN – 1×K vector of cycle times
17% XN – 1×K vector of system throughputs
18% LG – Logarithm of normalizing constant (not computed)
19% SN – (Possibly updated) network structure.
21% See also SOLVER_SSA_NRM_SPACE, NEXT_REACTION_METHOD_DIRECT.
23% ---------------------------------------------------------------------
24% Parameters & shorthands
25% ---------------------------------------------------------------------
26samples = options.samples;
33% ---------------------------------------------------------------------
34% Stoichiometry & reaction mapping (self‑loops included) ----------------
35% ---------------------------------------------------------------------
36S = zeros(0, I*R); % will transpose at the end
41% this is currently M^2*R^2, it can be lowered to M*R decoupling the
47 fromIR(k,:) = [ind, r];
48 fromIdx(k) = (ind-1)*R + r;
51 Srow = zeros(1, I*R); % build stoichiometry row
53 Srow(fromIdx(k)) = -Inf;
55 Srow(fromIdx(k)) = -1;
58 if sn.rtnodes((ind-1)*R+r, (jnd-1)*R+s) > 0
59 toIdx{k}(end+1) = (jnd-1)*R + s;
60 p = sn.rtnodes((ind-1)*R+r, (jnd-1)*R+s);
62 Srow((jnd-1)*R + s) = Srow((jnd-1)*R + s) + p;
70S = S.'; % states × reactions
72% ---------------------------------------------------------------------
73% Initial state vector --------------------------------------------------
74% ---------------------------------------------------------------------
75nvec0 = zeros(I*R,1); % initial state (aggregate state)
78 [~,nir] = State.toMarginalAggr(sn, ind, state{sn.nodeToStateful(ind)});
81 if sn.nodetype(ind) == NodeType.Source
84 line_error(mfilename,
'Infinite population error.');
87 nvec0((ind-1)*R + r,1) = nir(r);
97 ist = sn.nodeToStation(ind);
98 muir = sn.rates(ist,r);
102 mi(ind,1) = sn.nservers(ist);
106 rates(ind,r) = GlobalConstants.Immediate;
107 mi(ind,1) = GlobalConstants.MaxInt;
110 mi(isinf(mi)) = GlobalConstants.MaxInt;
113% Propensity function ---------------------------------------------------
114epstol = GlobalConstants.Zero;
116for j=1:length(fromIdx)
117 if sn.isstation(fromIR(j,1))
118 switch sn.sched(sn.nodeToStation(fromIR(j,1)))
119 case SchedStrategy.EXT
120 a{j} = @(X) rates(fromIR(j,1), fromIR(j,2));
121 case SchedStrategy.INF
122 a{j} = @(X) rates(fromIR(j,1), fromIR(j,2)) * X(fromIdx(j));
123 case SchedStrategy.PS
124 if R == 1 % single
class
125 a{j} = @(X) rates(fromIR(j,1), fromIR(j,2)) * min( mi(fromIR(j,1)), X(fromIdx(j)));
127 a{j} = @(X) rates(fromIR(j,1), fromIR(j,2)) * ( X(fromIdx(j)) ./ ...
128 (epstol+sum( X(((fromIR(j,1)-1)*R + 1):((fromIR(j,1)-1)*R + R)) ) )) * ...
129 min( mi(fromIR(j,1)), ...
130 (epstol+sum( X(((fromIR(j,1)-1)*R + 1):((fromIR(j,1)-1)*R + R)) ) ));
134 a{j} = @(X) rates(fromIR(j,1), fromIR(j,2)) * min(1, X(fromIdx(j)));
138% Propensity functions dependencies -----------------------------------
139D = cell(1,size(S,2));
141 J = find(S(:,k))'; % set of state variables affected by reaction k
146 r = mod(pos-1, R) + 1;
147 ind = ((pos-r)/R) + 1;
148 vecd(end+1:end+R) = ((ind-1)*R + 1) : (ind*R);
150 % vecd now contains all state variables affected by the firing of
151 % reaction k. We now find the propensity functions that depend
157 vecs = [vecs,find(S(vecd(j),:)<0)];
165% Having accounted
for them in D, we can now remove self-loops markings
168% ---------------------------------------------------------------------
169% Initialize performance metric matrices
170% ---------------------------------------------------------------------
171lG = 0; % Not computed in SSA
173% ---------------------------------------------------------------------
174% Run SSA/NRM with direct metric computation
175% ---------------------------------------------------------------------
176[QN, UN, RN, TN, CN, XN] = next_reaction_method_direct(S, D, a, nvec0, samples, options, sn, fromIdx, fromIR);
180% ======================================================================
181% Next-Reaction Method with direct metric computation
182% ======================================================================
183function [QN, UN, RN, TN, CN, XN] = next_reaction_method_direct(S, D, a, nvec0, samples, options, sn, fromIdx, fromIR)
185numReactions = size(S,2);
197% when a reaction fires,
this matrix helps selecting the probability that a
198% particular routing or phase
is selected as a result ------------------
199P = S;
P(
P<0)=
P(
P<0)+1';
200fromIdxCell = cell(numReactions,1);
201toIdxCell = cell(numReactions,1);
202cdfVec = cell(numReactions,1);
204 nnzP(r) = nnz(
P(:,r));
206 fromIdxCell{r} = find(S(:,r)<0);
207 toIdxCell{r} = find(
P(:,r));
208 cdfVec{r} = cumsum(
P(toIdxCell{r},r));
212% initialise Gillespie clocks ------------------------------------------
218Pk = -log(rand(1,numReactions));
219Tk = zeros(1,numReactions);
221tau = (Pk - Tk) ./ Ak;
223% Performance tracking variables
225NK = sn.njobs
'; % Jobs per class
226servers = sn.nservers;
230 [dt, kfire] = min(tau);
231 if isinf(dt), line_error(mfilename,'Deadlock. Quitting nrm method.
'); end
233 totalTime = totalTime + dt;
235 % Accumulate state-dependent metrics during this time interval
237 ind = sn.stationToNode(ist);
239 currentPop = nvec((ind-1)*R + k);
241 % Accumulate queue length (QN)
242 QN(ist, k) = QN(ist, k) + currentPop * dt;
244 % Compute throughput contribution from departures
245 irIndex = (ind-1)*R + k;
247 idx = find(fromIdx == irIndex);
249 depRate = Ak(idx(1));
251 TN(ist, k) = TN(ist, k) + depRate * dt;
253 % Compute utilization based on scheduling policy
255 case {SchedStrategy.INF, SchedStrategy.EXT}
256 UN(ist, k) = UN(ist, k) + currentPop * dt;
257 case SchedStrategy.PS
258 totalPop = sum(nvec(((ind-1)*R + 1):(ind*R)));
260 utilization = (currentPop / totalPop) * min(servers(ist), totalPop) / servers(ist);
264 UN(ist, k) = UN(ist, k) + utilization * dt;
271 % update aggregate state
273 r = 1+find(rand>=cdfVec{kfire},1);
277 nvec(fromIdxCell{kfire}) = nvec(fromIdxCell{kfire}) - 1;
278 nvec(toIdxCell{kfire}(r)) = nvec(toIdxCell{kfire}(r)) + 1;
280 nvec = nvec + S(:,kfire); % zero change for self-loops
285 % update rates for all reactions dependent on the last fired reaction
291 Pk(kfire) = Pk(kfire) - log(rand);
292 tau = (Pk - Tk) ./ Ak;
295 % do not count immediate events
297 print_progress(options, n);
299% Print newline after progress counter
300if isfield(options,'verbose
') && options.verbose
304% Normalize metrics by total time
308 QN(ist, k) = QN(ist, k) / totalTime;
309 UN(ist, k) = UN(ist, k) / totalTime;
310 TN(ist, k) = TN(ist, k) / totalTime;
315% Compute derived metrics
317 % System throughput at reference station
318 XN(1, k) = TN(sn.refstat(k), k);
323 RN(ist, k) = QN(ist, k) / TN(ist, k);
331 CN(1, k) = NK(k) / XN(1, k);
343 function print_progress(opt, samples_collected)
344 if ~isfield(opt,'verbose
') || ~opt.verbose, return; end
345 if samples_collected == 1e3
346 line_printf('\nSSA samples: %8d
', samples_collected);
347 elseif opt.verbose == 2
348 if samples_collected == 0
349 line_printf('\nSSA samples: %9d
', samples_collected);
351 line_printf('\b\b\b\b\b\b\b\b\b%9d
', samples_collected);
353 elseif mod(samples_collected,1e3)==0 || opt.verbose == 2
354 line_printf('\b\b\b\b\b\b\b\b\b%9d
', samples_collected);
357end % next_reaction_method_direct