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)
76bufferedSched = [SchedStrategy.FCFS, SchedStrategy.LCFS];
77buffers0 = cell(I,1); % per-node ordered buffer of waiting job
classes (FCFS/LCFS)
83 state_i = state{sn.nodeToStateful(ind)};
84 [~,nir] = State.toMarginalAggr(sn, ind, state_i);
87 if sn.nodetype(ind) == NodeType.Source
90 line_error(mfilename,
'Infinite population error.');
93 nvec0((ind-1)*R + r,1) = nir(r);
96 % Populate buffers
for buffered
nodes from the raw state vector
97 % (only stations have FCFS/LCFS scheduling; skip non-station
98 % stateful
nodes such as RROBIN dispatchers/Routers and Caches)
99 ist = sn.nodeToStation(ind);
100 if ist >= 1 && any(sn.sched(ist) == bufferedSched)
101 sumK = sum(sn.phasessz(ist,:));
102 sumNvars = sum(sn.nvars(ind,:));
103 bufCols = size(state_i,2) - sumK - sumNvars;
105 classId = state_i(1,pos);
106 if classId >= 1 && classId <= R
107 buffers0{ind}(end+1) = classId; % addLast
109 % classId == 0 means empty position, skip
120 ist = sn.nodeToStation(ind);
121 muir = sn.rates(ist,r);
125 mi(ind,1) = sn.nservers(ist);
129 rates(ind,r) = GlobalConstants.Immediate;
130 mi(ind,1) = GlobalConstants.MaxInt;
133 mi(isinf(mi)) = GlobalConstants.MaxInt;
136% Propensity function ---------------------------------------------------
137epstol = GlobalConstants.Zero;
139for j=1:length(fromIdx)
140 if sn.isstation(fromIR(j,1))
141 switch sn.sched(sn.nodeToStation(fromIR(j,1)))
142 case SchedStrategy.EXT
143 a{j} = @(X, bufs) rates(fromIR(j,1), fromIR(j,2));
144 case SchedStrategy.INF
145 a{j} = @(X, bufs) rates(fromIR(j,1), fromIR(j,2)) * X(fromIdx(j));
146 case SchedStrategy.PS
147 if R == 1 % single
class
148 a{j} = @(X, bufs) rates(fromIR(j,1), fromIR(j,2)) * min( mi(fromIR(j,1)), X(fromIdx(j)));
150 a{j} = @(X, bufs) rates(fromIR(j,1), fromIR(j,2)) * ( X(fromIdx(j)) ./ ...
151 (epstol+sum( X(((fromIR(j,1)-1)*R + 1):((fromIR(j,1)-1)*R + R)) ) )) * ...
152 min( mi(fromIR(j,1)), ...
153 (epstol+sum( X(((fromIR(j,1)-1)*R + 1):((fromIR(j,1)-1)*R + R)) ) ));
155 case {SchedStrategy.FCFS, SchedStrategy.LCFS}
156 % Invariant: numel(bufs{ind}) == max(0, total - mi(ind)).
157 % Rate
is proportional to the jobs actually being served, i.e.
158 % the
class-r population minus the
class-r jobs waiting in buffer.
159 a{j} = @(X, bufs) rates(fromIR(j,1), fromIR(j,2)) * ...
160 max(0, X(fromIdx(j)) - sum(bufs{fromIR(j,1)} == fromIR(j,2)));
163 a{j} = @(X, bufs) rates(fromIR(j,1), fromIR(j,2)) * min(1, X(fromIdx(j)));
167% Propensity functions dependencies -----------------------------------
168D = cell(1,size(S,2));
170 J = find(S(:,k))'; % set of state variables affected by reaction k
175 r = mod(pos-1, R) + 1;
176 ind = ((pos-r)/R) + 1;
177 vecd(end+1:end+R) = ((ind-1)*R + 1) : (ind*R);
179 % vecd now contains all state variables affected by the firing of
180 % reaction k. We now find the propensity functions that depend
186 vecs = [vecs,find(S(vecd(j),:)<0)];
194% Having accounted
for them in D, we can now remove self-loops markings
197% ---------------------------------------------------------------------
198% Initialize performance metric matrices
199% ---------------------------------------------------------------------
200lG = 0; % Not computed in SSA
202% ---------------------------------------------------------------------
203% Run SSA/NRM with direct metric computation
204% ---------------------------------------------------------------------
205[QN, UN, RN, TN, CN, XN] = next_reaction_method_direct(S, D, a, nvec0, buffers0, samples, options, sn, fromIdx, fromIR, mi);
209% ======================================================================
210% Next-Reaction Method with direct metric computation
211% ======================================================================
212function [QN, UN, RN, TN, CN, XN] = next_reaction_method_direct(S, D, a, nvec0, buffers0, samples, options, sn, fromIdx, fromIR, mi)
214numReactions = size(S,2);
226% when a reaction fires,
this matrix helps selecting the probability that a
227% particular routing or phase
is selected as a result ------------------
228P = S;
P(
P<0)=
P(
P<0)+1';
229fromIdxCell = cell(numReactions,1);
230toIdxCell = cell(numReactions,1);
231cdfVec = cell(numReactions,1);
233 nnzP(r) = nnz(
P(:,r));
235 fromIdxCell{r} = find(S(:,r)<0);
236 toIdxCell{r} = find(
P(:,r));
237 cdfVec{r} = cumsum(
P(toIdxCell{r},r));
241% initialise Gillespie clocks ------------------------------------------
243buffers = buffers0; % working copy of the per-node ordered buffers
245 Ak(k) = a{k}(nvec0, buffers);
248Pk = -log(rand(1,numReactions));
249Tk = zeros(1,numReactions);
251tau = (Pk - Tk) ./ Ak;
253% Performance tracking variables
255NK = sn.njobs
'; % Jobs per class
256servers = sn.nservers;
257PH = sn.proc; % service-process MAPs/PHs
261 [dt, kfire] = min(tau);
262 if isinf(dt), line_error(mfilename,'Deadlock. Quitting nrm method.
'); end
264 totalTime = totalTime + dt;
266 % Accumulate state-dependent metrics during this time interval
268 ind = sn.stationToNode(ist);
270 currentPop = nvec((ind-1)*R + k);
272 % Accumulate queue length (QN)
273 QN(ist, k) = QN(ist, k) + currentPop * dt;
275 % Compute throughput contribution from departures
276 irIndex = (ind-1)*R + k;
278 idx = find(fromIdx == irIndex);
280 depRate = Ak(idx(1));
282 TN(ist, k) = TN(ist, k) + depRate * dt;
284 % Compute utilization based on scheduling policy
286 case {SchedStrategy.INF, SchedStrategy.EXT}
287 UN(ist, k) = UN(ist, k) + currentPop * dt;
288 case SchedStrategy.PS
289 totalPop = sum(nvec(((ind-1)*R + 1):(ind*R)));
291 utilization = (currentPop / totalPop) * min(servers(ist), totalPop) / servers(ist);
295 UN(ist, k) = UN(ist, k) + utilization * dt;
296 case {SchedStrategy.FCFS, SchedStrategy.LCFS}
297 if ~isempty(PH{ist}{k})
298 waiting = sum(buffers{ind} == k);
299 inService = currentPop - waiting;
300 UN(ist, k) = UN(ist, k) + (inService / servers(ist)) * dt;
308 % update aggregate state
311 % Inverse-CDF sampling: smallest r such that cdfVec(r) > rand. The
312 % previous formulation `1+find(rand>=cdfVec,1)` was a misuse of
313 % find(...,1) that always returned 2 once rand exceeded cdfVec(1),
314 % leaving destinations beyond the second one unreachable (e.g. all
315 % traffic skipping Station3 in a 3-way RAND split).
316 r = find(cdfVec{kfire} > rand, 1);
318 r = length(cdfVec{kfire});
320 nvec(fromIdxCell{kfire}) = nvec(fromIdxCell{kfire}) - 1;
321 nvec(toIdxCell{kfire}(r)) = nvec(toIdxCell{kfire}(r)) + 1;
322 destPos = toIdxCell{kfire}(r);
324 nvec = nvec + S(:,kfire); % zero change for self-loops
325 dpos = find(S(:,kfire) > 0); % deterministic destination (single move)
331 % maintain FCFS/LCFS buffers given the source/destination of this firing
332 buffers = updateBuffers(kfire, nvec, buffers, fromIR, destPos, mi, R, sn);
336 % update rates for all reactions dependent on the last fired reaction
338 Ak(k) = a{k}(nvec, buffers);
342 Pk(kfire) = Pk(kfire) - log(rand);
343 tau = (Pk - Tk) ./ Ak;
346 % do not count immediate events
348 print_progress(options, n);
350% Print newline after progress counter
351if isfield(options,'verbose
') && options.verbose
355% Normalize metrics by total time
359 QN(ist, k) = QN(ist, k) / totalTime;
360 UN(ist, k) = UN(ist, k) / totalTime;
361 TN(ist, k) = TN(ist, k) / totalTime;
366% Compute derived metrics
368 % System throughput at reference station
369 XN(1, k) = TN(sn.refstat(k), k);
374 RN(ist, k) = QN(ist, k) / TN(ist, k);
382 CN(1, k) = NK(k) / XN(1, k);
394 function print_progress(opt, samples_collected)
395 if ~isfield(opt,'verbose
') || ~opt.verbose, return; end
396 if samples_collected == 1e3
397 line_printf('\bSSA samples: %8d
', samples_collected);
398 elseif opt.verbose == 2
399 if samples_collected == 0
400 line_printf('\bSSA samples: %9d
', samples_collected);
402 line_printf('\b\b\b\b\b\b\b\b\b%9d
', samples_collected);
404 elseif mod(samples_collected,1e3)==0 || opt.verbose == 2
405 line_printf('\b\b\b\b\b\b\b\b\b%9d
', samples_collected);
408end % next_reaction_method_direct
410% ======================================================================
411% Buffer maintenance for FCFS/LCFS nodes
412% ======================================================================
413function buffers = updateBuffers(kfire, nvec, buffers, fromIR, destPos, mi, R, sn)
414% Maintain the ordered per-node buffers when reaction KFIRE fires. A
415% departure removes the head/tail job of the source buffer; an arrival at a
416% buffered destination whose servers are all busy joins the buffer head.
417ind = fromIR(kfire,1); % source node of the firing
419% Handle departure from FCFS/LCFS source node
421 if ~isempty(buffers{ind})
422 buffers{ind}(end) = []; % pollLast
424elseif isLCFS(ind, sn)
425 if ~isempty(buffers{ind})
426 buffers{ind}(1) = []; % pollFirst
430% Handle arrival at a buffered destination node
431if ~isempty(destPos) && destPos > 0
432 jnd = floor((destPos-1)/R) + 1;
433 s = mod(destPos-1, R) + 1;
434 if isFCFS(jnd, sn) || isLCFS(jnd, sn)
435 totalAtDest = sum(nvec(((jnd-1)*R + 1):(jnd*R)));
436 if totalAtDest > mi(jnd)
437 % All servers busy - arriving job joins back of buffer
438 buffers{jnd} = [s, buffers{jnd}]; % addFirst
440 % Otherwise job went straight into service, buffer unchanged
445function tf = isFCFS(ind, sn)
448 ist = sn.nodeToStation(ind);
449 tf = (sn.sched(ist) == SchedStrategy.FCFS);
453function tf = isLCFS(ind, sn)
456 ist = sn.nodeToStation(ind);
457 tf = (sn.sched(ist) == SchedStrategy.LCFS);