LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
solver_ssa_nrm_space.m
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)
3%
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
11% queue).
12%
13% Outputs
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.
21%
22% See also NEXT_REACTION_METHOD.
23
24% ---------------------------------------------------------------------
25% Parameters & shorthands
26% ---------------------------------------------------------------------
27samples = options.samples;
28R = sn.nclasses;
29I = sn.nnodes;
30state = sn.state;
31% ---------------------------------------------------------------------
32% Stoichiometry & reaction mapping (self‑loops included) ----------------
33% ---------------------------------------------------------------------
34S = zeros(0, I*R); % will transpose at the end
35fromIdx = [];
36toIdx = [];
37fromIR = [];
38
39% see _kb/06-solver-catalog.md for rationale (SSA NRM reaction mapping O(M^2*R^2))
40k = 0;
41for ind = 1:I
42 for r = 1:R
43 k = k + 1;
44 fromIR(k,:) = [ind, r];
45 fromIdx(k) = (ind-1)*R + r;
46 probIR{k} = [];
47 toIdx{k} = [];
48 Srow = zeros(1, I*R); % build stoichiometry row
49 if sn.isslc(r)
50 Srow(fromIdx(k)) = -Inf;
51 else
52 Srow(fromIdx(k)) = -1;
53 for jnd = 1:I
54 for s = 1:R
55 if sn.rtnodes((ind-1)*R+r, (jnd-1)*R+s) > 0
56 toIdx{k}(end+1) = (jnd-1)*R + s;
57 p = sn.rtnodes((ind-1)*R+r, (jnd-1)*R+s);
58 probIR{k}(end+1) = p;
59 Srow((jnd-1)*R + s) = Srow((jnd-1)*R + s) + p;
60 end
61 end
62 end
63 end
64 S(k,:) = Srow;
65 end
66end
67S = S.'; % states × reactions
68
69% ---------------------------------------------------------------------
70% Initial state vector --------------------------------------------------
71% ---------------------------------------------------------------------
72nvec0 = zeros(I*R,1); % initial state (aggregate state)
73bufferedSched = [SchedStrategy.FCFS, SchedStrategy.LCFS];
74buffers0 = cell(I,1); % per-node ordered buffer of waiting job classes (FCFS/LCFS)
75for ind=1:I
76 buffers0{ind} = [];
77end
78for ind=1:I
79 if sn.isstateful(ind)
80 state_i = state{sn.nodeToStateful(ind)};
81 [~,nir] = State.toMarginalAggr(sn, ind, state_i);
82 for r = 1:R
83 if isinf(nir(r))
84 if sn.nodetype(ind) == NodeType.Source
85 nir(r) = 1;
86 else
87 line_error(mfilename, 'Infinite population error.');
88 end
89 end
90 nvec0((ind-1)*R + r,1) = nir(r);
91 end
92
93 % Populate buffers for buffered nodes from the raw state vector
94 % (only stations have FCFS/LCFS scheduling; skip non-station
95 % stateful nodes such as RROBIN dispatchers/Routers and Caches)
96 ist = sn.nodeToStation(ind);
97 if ist >= 1 && any(sn.sched(ist) == bufferedSched)
98 sumK = sum(sn.phasessz(ist,:));
99 sumNvars = sum(sn.nvars(ind,:));
100 bufCols = size(state_i,2) - sumK - sumNvars;
101 for pos = 1:bufCols
102 classId = state_i(1,pos);
103 if classId >= 1 && classId <= R
104 buffers0{ind}(end+1) = classId; % addLast
105 end
106 % classId == 0 means empty position, skip
107 end
108 end
109 end
110end
111
112mi = zeros(I,1);
113rates = zeros(I,R);
114for ind=1:I
115 if sn.isstation(ind)
116 for r=1:R
117 ist = sn.nodeToStation(ind);
118 muir = sn.rates(ist,r);
119 if ~isnan(muir)
120 rates(ind,r) = muir;
121 end
122 mi(ind,1) = sn.nservers(ist);
123 end
124 else
125 for r=1:R
126 rates(ind,r) = GlobalConstants.Immediate;
127 mi(ind,1) = GlobalConstants.MaxInt;
128 end
129 end
130 mi(isinf(mi)) = GlobalConstants.MaxInt;
131end
132
133% Propensity function ---------------------------------------------------
134epstol = GlobalConstants.Zero;
135a = {};
136for j=1:length(fromIdx)
137 if sn.isstation(fromIR(j,1))
138 switch sn.sched(sn.nodeToStation(fromIR(j,1)))
139 case SchedStrategy.EXT
140 a{j} = @(X, bufs) rates(fromIR(j,1), fromIR(j,2));
141 case SchedStrategy.INF
142 a{j} = @(X, bufs) rates(fromIR(j,1), fromIR(j,2)) * X(fromIdx(j));
143 case SchedStrategy.PS
144 if R == 1 % single class
145 a{j} = @(X, bufs) rates(fromIR(j,1), fromIR(j,2)) * min( mi(fromIR(j,1)), X(fromIdx(j)));
146 else
147 a{j} = @(X, bufs) rates(fromIR(j,1), fromIR(j,2)) * ( X(fromIdx(j)) ./ ...
148 (epstol+sum( X(((fromIR(j,1)-1)*R + 1):((fromIR(j,1)-1)*R + R)) ) )) * ...
149 min( mi(fromIR(j,1)), ...
150 (epstol+sum( X(((fromIR(j,1)-1)*R + 1):((fromIR(j,1)-1)*R + R)) ) ));
151 end
152 case {SchedStrategy.FCFS, SchedStrategy.LCFS}
153 % Rate proportional to the jobs actually being served, i.e. the
154 % class-r population minus the class-r jobs waiting in buffer.
155 a{j} = @(X, bufs) rates(fromIR(j,1), fromIR(j,2)) * ...
156 max(0, X(fromIdx(j)) - sum(bufs{fromIR(j,1)} == fromIR(j,2)));
157 end
158 else
159 a{j} = @(X, bufs) rates(fromIR(j,1), fromIR(j,2)) * min(1, X(fromIdx(j)));
160 end
161end
162
163% Propensity functions dependencies -----------------------------------
164D = cell(1,size(S,2));
165for k=1:size(D,2)
166 J = find(S(:,k))'; % set of state variables affected by reaction k
167 vecd = [];
168 for j=1:length(J)
169 % (ind-1)*R + r
170 pos = J(j);
171 r = mod(pos-1, R) + 1;
172 ind = ((pos-r)/R) + 1;
173 vecd(end+1:end+R) = ((ind-1)*R + 1) : (ind*R);
174 end
175 % vecd now contains all state variables affected by the firing of
176 % reaction k. We now find the propensity functions that depend
177 % on those variables
178 if ~isempty(vecd)
179 vecd = unique(vecd);
180 vecs = [];
181 for j=1:length(vecd)
182 vecs = [vecs,find(S(vecd(j),:)<0)];
183 end
184 D{k} = unique(vecs);
185 else
186 D{k} = [];
187 end
188end
189
190% Having accounted for them in D, we can now remove self-loops markings
191S(isinf(S))=0;
192% ---------------------------------------------------------------------
193% Run SSA/NRM -----------------------------------------------------------
194% ---------------------------------------------------------------------
195if false %snIsClosedModel(sn)
196 % mixed-radix hashing
197 reactCache = containers.Map('KeyType','uint64','ValueType','any');
198 njobs = sn.njobs;
199 mixedradix = [cumprod(repmat(1+njobs,1,I))];
200 mixedradix = [1,mixedradix(1:end-1)];
201 hashfun = @(v, bufs) uint64(mixedradix*v(:));
202else
203 % buffer size unbounded so use string; the key combines the aggregate
204 % state vector with the ordered contents of every node buffer, so that
205 % FCFS/LCFS states differing only in queueing order remain distinct.
206 reactCache = containers.Map('KeyType','char','ValueType','any');
207 hashfun = @(v, bufs) [mat2str(v(:)'), '|', bufferHashAll(bufs)];
208end
209[t, nvecsim, bufferStates, ~, ~] = next_reaction_method(S, D, a, nvec0, buffers0, samples, options, reactCache, hashfun, fromIR, mi, R, sn);
210
211% ---------------------------------------------------------------------
212% Empirical state probabilities ----------------------------------------
213% ---------------------------------------------------------------------
214dt = diff(t);
215% Find unique states keyed on both the aggregate state and the buffers
216numIntervals = size(nvecsim,2) - 1;
217stateKeys = cell(numIntervals,1);
218for i = 1:numIntervals
219 stateKeys{i} = hashfun(nvecsim(:,i), bufferStates{i});
220end
221[~, ia, ic] = unique(stateKeys, 'stable');
222outspace = nvecsim(:, ia).';
223outspaceBuffers = bufferStates(ia);
224timeAccum = accumarray(ic, dt(:));
225pi = timeAccum / sum(timeAccum);
226
227% ---------------------------------------------------------------------
228% Per‑state arrival / departure rates (self‑loops counted) -------------
229% ---------------------------------------------------------------------
230numStates = size(outspace,1);
231depRates = zeros(numStates, I*R);
232
233for st = 1:numStates
234 a_state = reactCache(hashfun(outspace(st,:)', outspaceBuffers{st}));
235 for j = 1:length(fromIdx)
236 depRates(st, fromIdx(j)) = depRates(st, fromIdx(j)) + a_state(j);
237 end
238end
239end % solver_ssa_nrm_space
240
241% ======================================================================
242% Next-Reaction Method core --------------------------------------------
243% ======================================================================
244function [t, nvec, bufferStates, kfires, rfires] = next_reaction_method(S, D, a, nvec0, buffers0, samples, options, reactcache, hashfun, fromIR, mi, R, sn)
245numReactions = size(S,2);
246rand_pool_size = 1e7;
247buffers = buffers0; % working copy of the per-node ordered buffers
248
249% when a reaction fires, this matrix helps selecting the probability that a
250% particular routing or phase is selected as a result ------------------
251P = S; P(P<0)=P(P<0)+1';
252fromIdx = cell(numReactions,1);
253toIdx = cell(numReactions,1);
254cdfVec = cell(numReactions,1);
255for r=1:numReactions
256 nnzP(r) = nnz(P(:,r));
257 if nnzP(r)>1
258 fromIdx{r} = find(S(:,r)<0);
259 toIdx{r} = find(P(:,r));
260 cdfVec{r} = cumsum(P(toIdx{r},r));
261 end
262end
263
264% initialise Gillespie clocks ------------------------------------------
265t = 0;
266for k=1:size(S,2)
267 Ak(k) = a{k}(nvec0, buffers);
268end
269nvec = nvec0;
270key = hashfun(nvec, buffers);
271reactcache(key) = Ak; % cache first state's propensities
272Pk = -log(rand(1,numReactions));
273Tk = zeros(1,numReactions);
274
275tau = (Pk - Tk) ./ Ak;
276
277% logs -----------------------------------------------------------------
278tout = zeros(samples,1);
279nvecout = zeros(length(nvec0),samples);
280bufferStates = cell(1, samples+1);
281bufferStates{1} = buffers; % buffers in the initial state
282kfires = zeros(samples,1);
283rfires = zeros(samples,1);
284n = 1;
285while n <= samples
286 [dt, kfire] = min(tau);
287 kfires(n) = kfire;
288 if isinf(dt), line_error(mfilename,'Deadlock. Quitting nrm method.'); end
289
290 t = t + dt;
291
292 % update aggregate state
293 destPos = [];
294 if nnzP(kfire)>1
295 r = 1+find(rand>=cdfVec{kfire},1);
296 if isempty(r)
297 r = 1;
298 end
299 rfires(n) = r;
300 nvec(fromIdx{kfire}) = nvec(fromIdx{kfire}) - 1;
301 nvec(toIdx{kfire}(r)) = nvec(toIdx{kfire}(r)) + 1;
302 destPos = toIdx{kfire}(r);
303 else
304 nvec = nvec + S(:,kfire); % zero change for self-loops
305 dpos = find(S(:,kfire) > 0); % deterministic destination (single move)
306 if ~isempty(dpos)
307 destPos = dpos(1);
308 end
309 end
310
311 % maintain FCFS/LCFS buffers given the source/destination of this firing
312 buffers = updateBuffers(kfire, nvec, buffers, fromIR, destPos, mi, R, sn);
313
314 Tk = Tk + Ak * dt;
315
316 % update rates for all reactions dependent on the last fired reaction
317 for k=D{kfire}
318 Ak(k) = a{k}(nvec, buffers);
319 end
320
321 key = hashfun(nvec, buffers);
322 if ~isKey(reactcache,key)
323 reactcache(key) = Ak; % store propensities of new state
324 end
325
326 % maintain random number pool
327 n_mod = mod(n,rand_pool_size);
328 if n_mod == 1
329 rand_pool = rand(1+min(rand_pool_size, samples-n),1);
330 end
331
332 % update clocks
333 Pk(kfire) = Pk(kfire) - log(rand_pool(n_mod));
334 tau = (Pk - Tk) ./ Ak;
335 tau(Ak==0) = inf;
336
337 % update measures
338 tout(n) = t;
339 nvecout(:,n) = nvec;
340 bufferStates{n+1} = buffers;
341
342 % do not count immediate events
343 n = n + 1;
344 print_progress(options, n);
345end
346% Print newline after progress counter
347if isfield(options,'verbose') && options.verbose
348 line_printf('\n');
349end
350
351t = [0; tout];
352nvec = [nvec0, nvecout];
353
354 function print_progress(opt, samples_collected)
355 if ~isfield(opt,'verbose') || ~opt.verbose || batchStartupOptionUsed, return; end
356 if samples_collected == 1e3
357 line_printf('\nSSA samples: %8d', samples_collected);
358 elseif opt.verbose == 2
359 if samples_collected == 0
360 line_printf('\nSSA samples: %9d', samples_collected);
361 else
362 line_printf('\b\b\b\b\b\b\b\b\b%9d', samples_collected);
363 end
364 elseif mod(samples_collected,1e3)==0 || opt.verbose == 2
365 line_printf('\b\b\b\b\b\b\b\b\b%9d', samples_collected);
366 end
367 end
368end % next_reaction_method
369
370% ======================================================================
371% Buffer maintenance and hashing helpers for FCFS/LCFS nodes
372% ======================================================================
373function buffers = updateBuffers(kfire, nvec, buffers, fromIR, destPos, mi, R, sn)
374% Maintain the ordered per-node buffers when reaction KFIRE fires. A
375% departure removes the head/tail job of the source buffer; an arrival at a
376% buffered destination whose servers are all busy joins the buffer head.
377ind = fromIR(kfire,1); % source node of the firing
378
379% Handle departure from FCFS/LCFS source node
380if isFCFS(ind, sn)
381 if ~isempty(buffers{ind})
382 buffers{ind}(end) = []; % pollLast
383 end
384elseif isLCFS(ind, sn)
385 if ~isempty(buffers{ind})
386 buffers{ind}(1) = []; % pollFirst
387 end
388end
389
390% Handle arrival at a buffered destination node
391if ~isempty(destPos) && destPos > 0
392 jnd = floor((destPos-1)/R) + 1;
393 s = mod(destPos-1, R) + 1;
394 if isFCFS(jnd, sn) || isLCFS(jnd, sn)
395 totalAtDest = sum(nvec(((jnd-1)*R + 1):(jnd*R)));
396 if totalAtDest > mi(jnd)
397 % All servers busy - arriving job joins back of buffer
398 buffers{jnd} = [s, buffers{jnd}]; % addFirst
399 end
400 % Otherwise job went straight into service, buffer unchanged
401 end
402end
403end
404
405function s = bufferHashAll(bufs)
406% Buffer hash over all node indices (used to distinguish unique states).
407parts = cell(1, numel(bufs));
408for ind = 1:numel(bufs)
409 parts{ind} = [num2str(ind), ':', mat2str(bufs{ind}(:)')];
410end
411s = strjoin(parts, '|');
412end
413
414function tf = isFCFS(ind, sn)
415tf = false;
416if sn.isstation(ind)
417 ist = sn.nodeToStation(ind);
418 tf = (sn.sched(ist) == SchedStrategy.FCFS);
419end
420end
421
422function tf = isLCFS(ind, sn)
423tf = false;
424if sn.isstation(ind)
425 ist = sn.nodeToStation(ind);
426 tf = (sn.sched(ist) == SchedStrategy.LCFS);
427end
428end
Definition fjtag.m:161