LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
solver_ssa_nrm.m
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)
3%
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.
10%
11% Outputs
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.
20%
21% See also SOLVER_SSA_NRM_SPACE, NEXT_REACTION_METHOD_DIRECT.
22
23% ---------------------------------------------------------------------
24% Parameters & shorthands
25% ---------------------------------------------------------------------
26samples = options.samples;
27R = sn.nclasses;
28I = sn.nnodes;
29M = sn.nstations;
30K = sn.nclasses;
31state = sn.state;
32
33% ---------------------------------------------------------------------
34% Stoichiometry & reaction mapping (self‑loops included) ----------------
35% ---------------------------------------------------------------------
36S = zeros(0, I*R); % will transpose at the end
37fromIdx = [];
38toIdx = [];
39fromIR = [];
40
41% this is currently M^2*R^2, it can be lowered to M*R decoupling the
42% routing
43k = 0;
44for ind = 1:I
45 for r = 1:R
46 k = k + 1;
47 fromIR(k,:) = [ind, r];
48 fromIdx(k) = (ind-1)*R + r;
49 probIR{k} = [];
50 toIdx{k} = [];
51 Srow = zeros(1, I*R); % build stoichiometry row
52 if sn.isslc(r)
53 Srow(fromIdx(k)) = -Inf;
54 else
55 Srow(fromIdx(k)) = -1;
56 for jnd = 1:I
57 for s = 1:R
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);
61 probIR{k}(end+1) = p;
62 Srow((jnd-1)*R + s) = Srow((jnd-1)*R + s) + p;
63 end
64 end
65 end
66 end
67 S(k,:) = Srow;
68 end
69end
70S = S.'; % states × reactions
71
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)
78for ind=1:I
79 buffers0{ind} = [];
80end
81for ind=1:I
82 if sn.isstateful(ind)
83 state_i = state{sn.nodeToStateful(ind)};
84 [~,nir] = State.toMarginalAggr(sn, ind, state_i);
85 for r = 1:R
86 if isinf(nir(r))
87 if sn.nodetype(ind) == NodeType.Source
88 nir(r) = 1;
89 else
90 line_error(mfilename, 'Infinite population error.');
91 end
92 end
93 nvec0((ind-1)*R + r,1) = nir(r);
94 end
95
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;
104 for pos = 1:bufCols
105 classId = state_i(1,pos);
106 if classId >= 1 && classId <= R
107 buffers0{ind}(end+1) = classId; % addLast
108 end
109 % classId == 0 means empty position, skip
110 end
111 end
112 end
113end
114
115mi = zeros(I,1);
116rates = zeros(I,R);
117for ind=1:I
118 if sn.isstation(ind)
119 for r=1:R
120 ist = sn.nodeToStation(ind);
121 muir = sn.rates(ist,r);
122 if ~isnan(muir)
123 rates(ind,r) = muir;
124 end
125 mi(ind,1) = sn.nservers(ist);
126 end
127 else
128 for r=1:R
129 rates(ind,r) = GlobalConstants.Immediate;
130 mi(ind,1) = GlobalConstants.MaxInt;
131 end
132 end
133 mi(isinf(mi)) = GlobalConstants.MaxInt;
134end
135
136% Propensity function ---------------------------------------------------
137epstol = GlobalConstants.Zero;
138a = {};
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)));
149 else
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)) ) ));
154 end
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)));
161 end
162 else
163 a{j} = @(X, bufs) rates(fromIR(j,1), fromIR(j,2)) * min(1, X(fromIdx(j)));
164 end
165end
166
167% Propensity functions dependencies -----------------------------------
168D = cell(1,size(S,2));
169for k=1:size(D,2)
170 J = find(S(:,k))'; % set of state variables affected by reaction k
171 vecd = [];
172 for j=1:length(J)
173 % (ind-1)*R + r
174 pos = J(j);
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);
178 end
179 % vecd now contains all state variables affected by the firing of
180 % reaction k. We now find the propensity functions that depend
181 % on those variables
182 if ~isempty(vecd)
183 vecd = unique(vecd);
184 vecs = [];
185 for j=1:length(vecd)
186 vecs = [vecs,find(S(vecd(j),:)<0)];
187 end
188 D{k} = unique(vecs);
189 else
190 D{k} = [];
191 end
192end
193
194% Having accounted for them in D, we can now remove self-loops markings
195S(isinf(S))=0;
196
197% ---------------------------------------------------------------------
198% Initialize performance metric matrices
199% ---------------------------------------------------------------------
200lG = 0; % Not computed in SSA
201
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);
206
207end % solver_ssa_nrm
208
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)
213
214numReactions = size(S,2);
215R = sn.nclasses;
216I = sn.nnodes;
217M = sn.nstations;
218K = sn.nclasses;
219QN = zeros(M, K);
220UN = zeros(M, K);
221RN = zeros(M, K);
222TN = zeros(M, K);
223CN = zeros(1, K);
224XN = zeros(1, K);
225
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);
232for r=1:numReactions
233 nnzP(r) = nnz(P(:,r));
234 if nnzP(r)>1
235 fromIdxCell{r} = find(S(:,r)<0);
236 toIdxCell{r} = find(P(:,r));
237 cdfVec{r} = cumsum(P(toIdxCell{r},r));
238 end
239end
240
241% initialise Gillespie clocks ------------------------------------------
242t = 0;
243buffers = buffers0; % working copy of the per-node ordered buffers
244for k=1:size(S,2)
245 Ak(k) = a{k}(nvec0, buffers);
246end
247nvec = nvec0;
248Pk = -log(rand(1,numReactions));
249Tk = zeros(1,numReactions);
250
251tau = (Pk - Tk) ./ Ak;
252
253% Performance tracking variables
254totalTime = 0;
255NK = sn.njobs'; % Jobs per class
256servers = sn.nservers;
257PH = sn.proc; % service-process MAPs/PHs
258
259n = 1;
260while n <= samples
261 [dt, kfire] = min(tau);
262 if isinf(dt), line_error(mfilename,'Deadlock. Quitting nrm method.'); end
263
264 totalTime = totalTime + dt;
265
266 % Accumulate state-dependent metrics during this time interval
267 for ist = 1:M
268 ind = sn.stationToNode(ist);
269 for k = 1:K
270 currentPop = nvec((ind-1)*R + k);
271
272 % Accumulate queue length (QN)
273 QN(ist, k) = QN(ist, k) + currentPop * dt;
274
275 % Compute throughput contribution from departures
276 irIndex = (ind-1)*R + k;
277 depRate = 0;
278 idx = find(fromIdx == irIndex);
279 if ~isempty(idx)
280 depRate = Ak(idx(1));
281 end
282 TN(ist, k) = TN(ist, k) + depRate * dt;
283
284 % Compute utilization based on scheduling policy
285 switch sn.sched(ist)
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)));
290 if totalPop > 0
291 utilization = (currentPop / totalPop) * min(servers(ist), totalPop) / servers(ist);
292 else
293 utilization = 0;
294 end
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;
301 end
302 end
303 end
304 end
305
306 t = t + dt;
307
308 % update aggregate state
309 destPos = [];
310 if nnzP(kfire)>1
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);
317 if isempty(r)
318 r = length(cdfVec{kfire});
319 end
320 nvec(fromIdxCell{kfire}) = nvec(fromIdxCell{kfire}) - 1;
321 nvec(toIdxCell{kfire}(r)) = nvec(toIdxCell{kfire}(r)) + 1;
322 destPos = toIdxCell{kfire}(r);
323 else
324 nvec = nvec + S(:,kfire); % zero change for self-loops
325 dpos = find(S(:,kfire) > 0); % deterministic destination (single move)
326 if ~isempty(dpos)
327 destPos = dpos(1);
328 end
329 end
330
331 % maintain FCFS/LCFS buffers given the source/destination of this firing
332 buffers = updateBuffers(kfire, nvec, buffers, fromIR, destPos, mi, R, sn);
333
334 Tk = Tk + Ak * dt;
335
336 % update rates for all reactions dependent on the last fired reaction
337 for k=D{kfire}
338 Ak(k) = a{k}(nvec, buffers);
339 end
340
341 % update clocks
342 Pk(kfire) = Pk(kfire) - log(rand);
343 tau = (Pk - Tk) ./ Ak;
344 tau(Ak==0) = inf;
345
346 % do not count immediate events
347 n = n + 1;
348 print_progress(options, n);
349end % while
350% Print newline after progress counter
351if isfield(options,'verbose') && options.verbose
352 line_printf('\n');
353end
354
355% Normalize metrics by total time
356if totalTime > 0
357 for ist = 1:M
358 for k = 1:K
359 QN(ist, k) = QN(ist, k) / totalTime;
360 UN(ist, k) = UN(ist, k) / totalTime;
361 TN(ist, k) = TN(ist, k) / totalTime;
362 end
363 end
364end
365
366% Compute derived metrics
367for k = 1:K
368 % System throughput at reference station
369 XN(1, k) = TN(sn.refstat(k), k);
370
371 % Response times
372 for ist = 1:M
373 if TN(ist, k) > 0
374 RN(ist, k) = QN(ist, k) / TN(ist, k);
375 else
376 RN(ist, k) = 0;
377 end
378 end
379
380 % Cycle times
381 if XN(1, k) > 0
382 CN(1, k) = NK(k) / XN(1, k);
383 end
384end
385
386% Handle NaN values
387QN(isnan(QN)) = 0;
388UN(isnan(UN)) = 0;
389RN(isnan(RN)) = 0;
390XN(isnan(XN)) = 0;
391TN(isnan(TN)) = 0;
392CN(isnan(CN)) = 0;
393
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);
401 else
402 line_printf('\b\b\b\b\b\b\b\b\b%9d', samples_collected);
403 end
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);
406 end
407 end
408end % next_reaction_method_direct
409
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
418
419% Handle departure from FCFS/LCFS source node
420if isFCFS(ind, sn)
421 if ~isempty(buffers{ind})
422 buffers{ind}(end) = []; % pollLast
423 end
424elseif isLCFS(ind, sn)
425 if ~isempty(buffers{ind})
426 buffers{ind}(1) = []; % pollFirst
427 end
428end
429
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
439 end
440 % Otherwise job went straight into service, buffer unchanged
441 end
442end
443end
444
445function tf = isFCFS(ind, sn)
446tf = false;
447if sn.isstation(ind)
448 ist = sn.nodeToStation(ind);
449 tf = (sn.sched(ist) == SchedStrategy.FCFS);
450end
451end
452
453function tf = isLCFS(ind, sn)
454tf = false;
455if sn.isstation(ind)
456 ist = sn.nodeToStation(ind);
457 tf = (sn.sched(ist) == SchedStrategy.LCFS);
458end
459end
Definition mmt.m:124