LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
solver_ssa_analyzer.m
1function [QN,UN,RN,TN,CN,XN,runtime,method,tranSysState,tranSync,sn,QNCI,UNCI,RNCI,TNCI,ANCI,WNCI] = solver_ssa_analyzer(sn, options)
2% [QN,UN,RN,TN,CN,XN,RUNTIME] = SOLVER_SSA_ANALYZER(SN, OPTIONS)
3% Wrapper that selects the most suitable SSA performance-analysis back-end.
4%
5% If every station uses scheduling policy INF, EXT, or PS (and the network
6% has no cache nodes), the faster Next-Reaction-Method analyser
7% -> solver_ssa_analyzer_nrm
8% is invoked. Otherwise the original serial / parallel analysers are used.
9%
10% Copyright (c) 2012-2026, Imperial College London
11% All rights reserved.
12
13Tstart = tic;
14
15line_debug('SSA analyzer starting: method=%s, nstations=%d, nclasses=%d', options.method, sn.nstations, sn.nclasses);
16
17% Convert non-Markovian distributions to PH
18sn = sn_nonmarkov_toph(sn, options);
19
20% Capture initial state after conversion (state may have been expanded for MAPs)
21init_state = sn.state;
22
23% Initialize CI outputs
24M = sn.nstations;
25K = sn.nclasses;
26QNCI = [];
27UNCI = [];
28RNCI = [];
29TNCI = [];
30ANCI = [];
31WNCI = [];
32
33% Check if confidence intervals are requested
34[confintEnabled, confintLevel] = Solver.parseConfInt(options.confint);
35
36% -------------------------------------------------------------------------
37% Pick analysis back-end
38% -------------------------------------------------------------------------
39switch options.method
40 case {'default'} % "default" prefers NRM for closed QNs with INF/PS
41 % Stochastic Petri nets (Place/Transition) are simulated only by the NRM
42 % SPN path (the serial afterEvent engine does not fire transitions
43 % correctly). Route them to NRM directly, bypassing the queueing-network
44 % eligibility checks below, which are phrased in terms of station
45 % scheduling and populations that a Petri net does not have.
46 if any(sn.nodetype == NodeType.Transition)
47 line_debug('Default method: SPN detected, using NRM');
48 [XN,UN,QN,RN,TN,CN,tranSysState,tranSync,sn] = ...
49 solver_ssa_analyzer_nrm(sn, options);
50 method = 'nrm';
51 if confintEnabled && ~isempty(tranSysState) && length(tranSysState) > 1
52 [QNCI, UNCI, RNCI, TNCI, ANCI, WNCI] = ssa_compute_batch_means_ci(tranSysState, sn, confintLevel);
53 end
54 runtime = toc(Tstart);
55 return
56 end
57 % Prefer the NRM whenever it can actually run this model. isNrmEligible
58 % mirrors the scheduling list of solver_ssa_analyzer_nrm and every
59 % per-feature guard, so the default path routes to NRM for exactly the
60 % models the explicit method='nrm' path accepts (FCFS/LCFS/buffered
61 % families, POLLING, PAS, caches, open or closed).
62 nrmSupported = isNrmEligible(sn);
63
64 if nrmSupported
65 line_debug('Default method: using NRM (Next Reaction Method)\n');
66 line_debug('Using NRM method (fast path), calling solver_ssa_analyzer_nrm');
67 [XN,UN,QN,RN,TN,CN,tranSysState,tranSync,sn] = ...
68 solver_ssa_analyzer_nrm(sn, options);
69 method = 'nrm';
70 % Compute CI using batch means if enabled
71 if confintEnabled && ~isempty(tranSysState) && length(tranSysState) > 1
72 [QNCI, UNCI, RNCI, TNCI, ANCI, WNCI] = ssa_compute_batch_means_ci(tranSysState, sn, confintLevel, options);
73 end
74 runtime = toc(Tstart);
75 return
76 else
77 % otherwise fall through to serial selection
78 line_debug('Default method: using serial SSA\n');
79 line_debug('NRM not supported, falling back to serial method');
80 options.method = 'serial';
81 end
82
83 case 'nrm'
84 if ~routingNrmOK(sn)
85 % NRM routes departures via the static rt matrix (JSQ and memoryless
86 % KCHOICES are handled natively); the remaining state-dependent
87 % routing strategies need the serial engine
88 line_warning(mfilename, 'NRM does not support RL routing; falling back to the serial method.');
89 options.method = 'serial';
90 sn.method = 'default/serial';
91 elseif ~renegeNrmOK(sn)
92 % Phase-type patience would need the remaining-patience phase of
93 % each waiting job, which the reaction network does not carry
94 line_warning(mfilename, 'NRM supports only exponential (memoryless) patience for reneging; falling back to the serial method.');
95 options.method = 'serial';
96 sn.method = 'default/serial';
97 elseif ~balkNrmOK(sn)
98 % EXPECTED_WAIT / COMBINED balking depend on the mean waiting time,
99 % which is not a function of the state vector
100 line_warning(mfilename, 'NRM only supports QUEUE_LENGTH balking; falling back to the serial method.');
101 options.method = 'serial';
102 sn.method = 'default/serial';
103 elseif ~phaseNrmOK(sn)
104 % Phase-type service is expanded exactly only at the INF/PS and
105 % non-preemptive buffered families; a preemptive (LCFSPR) or polling
106 % station with phase-type service needs the serial engine.
107 line_warning(mfilename, 'NRM expands phase-type service only at INF/PS and non-preemptive buffered stations; falling back to the serial method.');
108 options.method = 'serial';
109 sn.method = 'default/serial';
110 elseif ~cacheNrmOK(sn)
111 % A cache with a retrieval (delayed-hit) system sends a miss to a
112 % queue and back, which the immediate class-switch cache model does
113 % not yet reproduce; use the serial engine.
114 line_warning(mfilename, 'NRM does not yet support the cache retrieval (delayed-hit) system; falling back to the serial method.');
115 options.method = 'serial';
116 sn.method = 'default/serial';
117 else
118 % Finite capacity regions run in the NRM under both rules (DROP
119 % censors the refused transition, WAITQ parks it in a per-region
120 % FIFO), so no region rule forces a serial fallback here.
121 line_debug('Using explicit NRM method, calling solver_ssa_analyzer_nrm');
122
123 [XN,UN,QN,RN,TN,CN,tranSysState,tranSync,sn] = ...
124 solver_ssa_analyzer_nrm(sn, options);
125 method = 'nrm';
126 sn.method = 'default/nrm';
127 % Compute CI using batch means if enabled
128 if confintEnabled && ~isempty(tranSysState) && length(tranSysState) > 1
129 [QNCI, UNCI, RNCI, TNCI, ANCI, WNCI] = ssa_compute_batch_means_ci(tranSysState, sn, confintLevel, options);
130 end
131 runtime = toc(Tstart);
132 return
133 end
134 case 'ssa' % alias for serial path below
135 line_debug('Using ssa alias, redirecting to serial method');
136 options.method = 'serial';
137 sn.method = 'default/serial';
138end
139
140% SERIAL / PARALLEL ANALYSERS (legacy paths) ------------------------------
141switch options.method
142 case {'serial'}
143 line_debug('Using serial method, calling solver_ssa_analyzer_serial');
144 [XN,UN,QN,RN,TN,CN,tranSysState,tranSync,sn] = ...
145 solver_ssa_analyzer_serial(sn, init_state, options, false);
146 method = 'serial';
147
148 case {'para','parallel'}
149 % Prefer the NRM on the same eligibility gate as the default path: an
150 % NRM-eligible model runs on the fast single-run NRM rather than
151 % replicated serial simulation.
152 if isNrmEligible(sn)
153 line_debug('Parallel method: model is NRM-eligible, using NRM');
154 [XN,UN,QN,RN,TN,CN,tranSysState,tranSync,sn] = ...
155 solver_ssa_analyzer_nrm(sn, options);
156 method = 'nrm';
157 if confintEnabled && ~isempty(tranSysState) && length(tranSysState) > 1
158 [QNCI, UNCI, RNCI, TNCI, ANCI, WNCI] = ssa_compute_batch_means_ci(tranSysState, sn, confintLevel, options);
159 end
160 runtime = toc(Tstart);
161 return
162 end
163 line_debug('Using parallel method, calling solver_ssa_analyzer_parallel');
164 try
165 [XN,UN,QN,RN,TN,CN,tranSysState,tranSync,sn] = ...
166 solver_ssa_analyzer_parallel(sn, init_state, options);
167 method = 'parallel';
168 catch ME
169 if strcmp(ME.identifier,'MATLAB:spmd:NoPCT')
170 line_printf(['Parallel Computing Toolbox unavailable – ',...
171 'falling back to serial SSA.\n']);
172 [XN,UN,QN,RN,TN,CN,tranSysState,tranSync,sn] = ...
173 solver_ssa_analyzer_serial(sn, init_state, options, true);
174 method = 'serial';
175 else
176 rethrow(ME);
177 end
178 end
179
180 otherwise
181 error('solver_ssa_analyzer:UnknownMethod', ...
182 'Unknown analysis method: %s', options.method);
183end
184
185% Compute CI using batch means if enabled
186if confintEnabled && ~isempty(tranSysState) && length(tranSysState) > 1
187 [QNCI, UNCI, RNCI, TNCI, ANCI, WNCI] = ssa_compute_batch_means_ci(tranSysState, sn, confintLevel, options);
188end
189
190runtime = toc(Tstart);
191end
192
193function [QNCI, UNCI, RNCI, TNCI, ANCI, WNCI] = ssa_compute_batch_means_ci(tranSysState, sn, confintLevel, options)
194% SSA_COMPUTE_BATCH_MEANS_CI Compute confidence intervals using batch means method
195%
196% tranSysState{1} contains the cumulative time at each sample
197% tranSysState{2:end} contain the state vectors for each stateful node
198
199M = sn.nstations;
200K = sn.nclasses;
201QNCI = zeros(M, K);
202UNCI = zeros(M, K);
203RNCI = zeros(M, K);
204TNCI = zeros(M, K);
205ANCI = zeros(M, K);
206WNCI = zeros(M, K);
207
208% Extract time and state data
209if iscell(tranSysState) && length(tranSysState) > 1
210 times = tranSysState{1};
211 nSamples = length(times);
212
213 if nSamples < 20
214 % Not enough samples for batch means
215 return;
216 end
217
218 % Number of batches (use 10-30 batches for good CI estimation)
219 numBatches = min(20, floor(nSamples / 10));
220 if numBatches < 2
221 return;
222 end
223 batchSize = floor(nSamples / numBatches);
224
225 % Discard initial transient before batch means: use
226 % options.config.warmupfrac when set (> 0), else the legacy 10% discard
227 if isfield(options,'config') && isfield(options.config,'warmupfrac') ...
228 && ~isempty(options.config.warmupfrac) && options.config.warmupfrac > 0
229 warmupfrac = options.config.warmupfrac;
230 else
231 warmupfrac = 0.1;
232 end
233 transientCutoff = max(1, floor(nSamples * warmupfrac));
234
235 % Extract queue length data from tranSysState
236 % tranSysState{2:end} contains state vectors for each stateful node
237 % We need to compute marginal queue lengths per station/class
238
239 % Compute batch means for queue lengths
240 for ist = 1:M
241 isf = sn.stationToStateful(ist);
242 if isf > 0 && (1 + isf) <= length(tranSysState)
243 stateData = tranSysState{1 + isf};
244 if isempty(stateData)
245 continue;
246 end
247
248 for k = 1:K
249 % Extract queue length for this station/class from state data
250 % The state data format depends on the scheduling strategy
251 % For simplicity, we'll use the marginal extraction
252 ind = sn.stationToNode(ist);
253
254 % Compute time-weighted batch means
255 batchMeans = zeros(1, numBatches);
256 for b = 1:numBatches
257 startIdx = transientCutoff + (b-1) * batchSize + 1;
258 endIdx = min(transientCutoff + b * batchSize, nSamples);
259 if startIdx > nSamples || startIdx >= endIdx
260 continue;
261 end
262
263 % Compute time-weighted average for this batch
264 % times contains cumulative times, compute inter-sample durations
265 if startIdx > 1
266 prevTime = times(startIdx - 1);
267 else
268 prevTime = 0;
269 end
270 batchTimes = times(startIdx:endIdx);
271
272 if length(batchTimes) >= 1
273 % Compute time duration each state was held
274 if startIdx == 1
275 dt = [batchTimes(1); diff(batchTimes)];
276 else
277 dt = [batchTimes(1) - prevTime; diff(batchTimes)];
278 end
279
280 % Extract queue lengths from state data
281 % For now, sum all columns to get total jobs at the station
282 % This works for most queue types where state represents job counts
283 qLengths = sum(stateData(startIdx:endIdx, :), 2);
284
285 totalTime = sum(dt);
286 if totalTime > 0
287 batchMeans(b) = sum(qLengths .* dt) / totalTime;
288 end
289 end
290 end
291
292 % Count valid batches (non-NaN)
293 validMask = ~isnan(batchMeans);
294 batchMeans = batchMeans(validMask);
295 nBatches = length(batchMeans);
296
297 if nBatches >= 2
298 % Compute mean and standard error
299 batchMean = mean(batchMeans);
300 batchStd = std(batchMeans);
301 stdErr = batchStd / sqrt(nBatches);
302
303 % t-critical value for confidence level
304 alpha = 1 - confintLevel;
305 tCrit = tinv(1 - alpha/2, nBatches - 1);
306
307 % Confidence interval half-width
308 QNCI(ist, k) = tCrit * stdErr;
309 end
310 end
311 end
312 end
313
314 % For utilization, response time, and throughput CIs, use relative scaling
315 % These are derived from queue length CI using Little's law relationships
316 UNCI = QNCI; % Simplified - utilization CI scales similarly
317 RNCI = QNCI; % Response time CI - would need service rate info
318 TNCI = QNCI; % Throughput CI - would need arrival rate info
319end
320end
321
322function ok = fcrNrmOK(sn)
323% Finite capacity regions are supported under both rules. DROP destroys a
324% refused job (the departure fires and the job never reaches the destination);
325% WAITQ parks it in a per-region FIFO and admits it head-of-line as capacity
326% frees. The NRM carries the FIFO explicitly (see fcrReleaseCascade), so no
327% region rule forces a fallback. Linear-constraint and memory-budget regions
328% ride the same admission test.
329ok = true;
330end
331
332function ok = routingNrmOK(sn)
333% True when every routing strategy in the model is one the NRM resolves at
334% firing time. JSQ and KCHOICES select from the candidate queue lengths;
335% RROBIN/WRROBIN walk a rotation pointer, and KCHOICES with withMemory=true
336% keeps its previously selected destination; the NRM carries both as auxiliary
337% state alongside the buffers, since no rate reads them -- they only steer the
338% destination draw, so they need not enter the reaction network. RL needs an
339% external policy and still requires the serial engine.
340ok = ~any(sn.routing(:) == RoutingStrategy.RL);
341if ~ok
342 return
343end
344end
345
346function ok = balkNrmOK(sn)
347% True when no station uses a balking strategy that the NRM cannot evaluate.
348% QUEUE_LENGTH is a pure function of the state vector, so the NRM draws it at
349% firing time; EXPECTED_WAIT and COMBINED depend on the mean waiting time and
350% need the serial engine (State.afterEventStation rejects them likewise).
351ok = true;
352if ~isfield(sn,'balkingStrategy') || isempty(sn.balkingStrategy)
353 return
354end
355bs = sn.balkingStrategy(:);
356ok = all(bs == 0 | bs == BalkingStrategy.QUEUE_LENGTH);
357end
358
359function ok = cacheNrmOK(sn)
360% True for every Cache node. The NRM models a cache access as an immediate
361% state-dependent class switch (read -> hit/miss/retrieval) at the cache node,
362% applying the same replacement logic as State.afterEventCache to the cache
363% contents carried alongside the buffers, INCLUDING the retrieval (delayed-hit)
364% system: a miss for an item not yet being fetched begins a retrieval (the job
365% is routed to the fetch queue and returns to complete the miss), and a
366% concurrent request for an item already being fetched is absorbed as a delayed
367% hit -- matching the serial engine's sample-path semantics.
368ok = true;
369end
370
371function ok = isNrmEligible(sn)
372% True when the NRM engine can run this model. The scheduling list matches
373% solver_ssa_analyzer_nrm's own validation (INF/PS family, non-preemptive
374% buffered family, LCFSPR, PAS, POLLING); the per-feature guards exclude the
375% sub-cases it cannot reproduce. The NRM simulates open and closed models alike
376% -- it only lacks Fork/Join node handling -- so the gate is a Fork/Join
377% exclusion, not the INF/PS-only sn_is_population_model. Used to prefer the NRM
378% on the default and parallel dispatch paths.
379allowedSched = [SchedStrategy.INF, SchedStrategy.EXT, SchedStrategy.PS, ...
380 SchedStrategy.LPS, SchedStrategy.DPS, SchedStrategy.GPS, ...
381 SchedStrategy.PSPRIO, SchedStrategy.DPSPRIO, SchedStrategy.GPSPRIO, ...
382 SchedStrategy.FCFS, SchedStrategy.LCFS, SchedStrategy.SIRO, ...
383 SchedStrategy.HOL, SchedStrategy.SEPT, SchedStrategy.LEPT, ...
384 SchedStrategy.LCFSPR, SchedStrategy.PAS, SchedStrategy.POLLING];
385ok = all(arrayfun(@(s) any(s == allowedSched), sn.sched)) && ...
386 cacheNrmOK(sn) && ...
387 ~sn_has_fork_join(sn) && ...
388 routingNrmOK(sn) && ...
389 fcrNrmOK(sn) && ...
390 balkNrmOK(sn) && ...
391 renegeNrmOK(sn) && ...
392 phaseNrmOK(sn);
393end
394
395function ok = renegeNrmOK(sn)
396% True when no station renegs with non-exponential patience. The NRM abandons
397% at the aggregate rate (waiting count)*mu, which is only correct when patience
398% is memoryless; phase-type patience would need each waiting job's remaining
399% phase. SOLVER_SSA rejects the same combination outright.
400ok = true;
401if ~isfield(sn,'impatienceClass') || isempty(sn.impatienceClass)
402 return
403end
404bad = (sn.impatienceClass == ImpatienceType.RENEGING) & (sn.impatienceType ~= ProcessType.EXP);
405ok = ~any(bad(:));
406end
407
408function ok = phaseNrmOK(sn)
409% True when every non-exponential service sits at a station whose rate law the
410% NRM expands exactly. Phase expansion splits the class-level share across a
411% class's phases in the ratio kir/nir, which needs only the per-phase
412% populations -- true of the INF/PS family, where every job present is in
413% service. A buffered policy instead needs the phase multiset of the jobs
414% ACTUALLY in service, which the waiting-only buffer does not record, so
415% non-exponential service there still needs the serial engine.
416ok = true;
417% INF/PS family: every job present is in service, so the class share splits
418% across a class's phases in the ratio kir/nir from the per-phase populations
419% alone. Non-preemptive buffered family (FCFS/LCFS/SIRO/HOL/SEPT/LEPT): only the
420% jobs actually in service carry a phase, tracked in the NRM's auxiliary
421% in-service multiset svcph. Preemptive LCFSPR (would need the preempted job's
422% phase remembered) and POLLING (a controller carries a single in-service job)
423% are excluded and still route to the serial engine.
424%
425% EXT (Source) is deliberately NOT in this list: a phase-type ARRIVAL process
426% is not a service law the phase expansion covers -- the NRM fires one arrival
427% per phase instead of one per renewal, inflating the arrival rate by the phase
428% count (an Erlang-2 source doubles lambda and destabilizes the queue). A
429% non-exponential arrival therefore forces the serial engine.
430exact = [SchedStrategy.INF, SchedStrategy.PS, SchedStrategy.LPS, ...
431 SchedStrategy.DPS, SchedStrategy.GPS, SchedStrategy.PSPRIO, ...
432 SchedStrategy.DPSPRIO, SchedStrategy.GPSPRIO, ...
433 SchedStrategy.FCFS, SchedStrategy.LCFS, SchedStrategy.SIRO, ...
434 SchedStrategy.HOL, SchedStrategy.SEPT, SchedStrategy.LEPT];
435for ist = 1:sn.nstations
436 for r = 1:sn.nclasses
437 if sn.procid(ist,r) == ProcessType.DISABLED || sn.procid(ist,r) == ProcessType.EXP
438 continue
439 end
440 if ~any(sn.sched(ist) == exact)
441 ok = false;
442 return
443 end
444 end
445end
446end
Definition fjtag.m:157
Definition Station.m:245