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.
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.
10% Copyright (c) 2012-2026, Imperial College London
15line_debug(
'SSA analyzer starting: method=%s, nstations=%d, nclasses=%d', options.method, sn.nstations, sn.nclasses);
17% Convert non-Markovian distributions to PH
18sn = sn_nonmarkov_toph(sn, options);
20% Capture initial state after conversion (state may have been expanded
for MAPs)
23% Initialize CI outputs
33% Check
if confidence intervals are requested
34[confintEnabled, confintLevel] = Solver.parseConfInt(options.confint);
36% -------------------------------------------------------------------------
37% Pick analysis back-end
38% -------------------------------------------------------------------------
40 case {
'default'} %
"default" prefers NRM
for closed QNs with INF/PS
41 % see _kb/06-solver-catalog.md
for rationale (SSA NRM dispatch)
42 if any(sn.nodetype == NodeType.Transition)
43 line_debug('Default method: SPN detected, using NRM');
44 [XN,UN,QN,RN,TN,CN,tranSysState,tranSync,sn] = ...
45 solver_ssa_analyzer_nrm(sn, options);
47 if confintEnabled && ~isempty(tranSysState) && length(tranSysState) > 1
48 [QNCI, UNCI, RNCI, TNCI, ANCI, WNCI] = ssa_compute_batch_means_ci(tranSysState, sn, confintLevel);
50 runtime = toc(Tstart);
53 % see _kb/06-solver-catalog.md for rationale (SSA NRM dispatch)
54 nrmSupported = isNrmEligible(sn);
57 line_debug('Default method: using NRM (Next Reaction Method)\n');
58 line_debug('Using NRM method (fast path), calling solver_ssa_analyzer_nrm');
59 [XN,UN,QN,RN,TN,CN,tranSysState,tranSync,sn] = ...
60 solver_ssa_analyzer_nrm(sn, options);
62 % Compute CI using batch means if enabled
63 if confintEnabled && ~isempty(tranSysState) && length(tranSysState) > 1
64 [QNCI, UNCI, RNCI, TNCI, ANCI, WNCI] = ssa_compute_batch_means_ci(tranSysState, sn, confintLevel, options);
66 runtime = toc(Tstart);
69 % otherwise fall through to serial selection
70 line_debug('Default method: using serial SSA\n');
71 line_debug('NRM not supported, falling back to serial method');
72 options.method = 'serial';
77 % NRM routes departures via the static rt matrix (JSQ and memoryless
78 % SQ are handled natively); the remaining state-dependent
79 % routing strategies need the serial engine
80 line_warning(mfilename, 'NRM does not support RL routing; falling back to the serial method.');
81 options.method = 'serial';
82 sn.method = 'default/serial';
83 elseif ~renegeNrmOK(sn)
84 % Phase-type patience would need the remaining-patience phase of
85 % each waiting job, which the reaction network does not carry
86 line_warning(mfilename, 'NRM supports only exponential (memoryless) patience for reneging; falling back to the serial method.');
87 options.method = 'serial';
88 sn.method = 'default/serial';
90 % EXPECTED_WAIT / COMBINED balking depend on the mean waiting time,
91 % which
is not a function of the state vector
92 line_warning(mfilename, 'NRM only supports QUEUE_LENGTH balking; falling back to the serial method.');
93 options.method = 'serial';
94 sn.method = 'default/serial';
95 elseif ~phaseNrmOK(sn)
96 % Phase-type service
is expanded exactly only at the INF/PS and
97 % non-preemptive buffered families; a preemptive (LCFSPR) or polling
98 % station with phase-type service needs the serial engine.
99 line_warning(mfilename, 'NRM expands phase-type service only at INF/PS and non-preemptive buffered stations; falling back to the serial method.');
100 options.method = 'serial';
101 sn.method = 'default/serial';
102 elseif ~cacheNrmOK(sn)
103 % A cache with a retrieval (delayed-hit) system sends a miss to a
104 % queue and back, which the immediate class-switch cache model does
105 % not yet reproduce; use the serial engine.
106 line_warning(mfilename, 'NRM does not yet support the cache retrieval (delayed-hit) system; falling back to the serial method.');
107 options.method = 'serial';
108 sn.method = 'default/serial';
110 % Finite capacity regions run in the NRM under both rules (DROP
111 % censors the refused transition, WAITQ parks it in a per-region
112 % FIFO), so no region rule forces a serial fallback here.
113 line_debug('Using explicit NRM method, calling solver_ssa_analyzer_nrm');
115 [XN,UN,QN,RN,TN,CN,tranSysState,tranSync,sn] = ...
116 solver_ssa_analyzer_nrm(sn, options);
118 sn.method = 'default/nrm';
119 % Compute CI using batch means if enabled
120 if confintEnabled && ~isempty(tranSysState) && length(tranSysState) > 1
121 [QNCI, UNCI, RNCI, TNCI, ANCI, WNCI] = ssa_compute_batch_means_ci(tranSysState, sn, confintLevel, options);
123 runtime = toc(Tstart);
126 case 'ssa' % alias for serial path below
127 line_debug('Using ssa alias, redirecting to serial method');
128 options.method = 'serial';
129 sn.method = 'default/serial';
132% SERIAL / PARALLEL ANALYSERS (legacy paths) ------------------------------
135 line_debug(
'Using serial method, calling solver_ssa_analyzer_serial');
136 [XN,UN,QN,RN,TN,CN,tranSysState,tranSync,sn] = ...
137 solver_ssa_analyzer_serial(sn, init_state, options,
false);
140 case {
'para',
'parallel'}
141 % Prefer the NRM on the same eligibility gate as the
default path: an
142 % NRM-eligible model runs on the fast single-run NRM rather than
143 % replicated serial simulation.
145 line_debug(
'Parallel method: model is NRM-eligible, using NRM');
146 [XN,UN,QN,RN,TN,CN,tranSysState,tranSync,sn] = ...
147 solver_ssa_analyzer_nrm(sn, options);
149 if confintEnabled && ~isempty(tranSysState) && length(tranSysState) > 1
150 [QNCI, UNCI, RNCI, TNCI, ANCI, WNCI] = ssa_compute_batch_means_ci(tranSysState, sn, confintLevel, options);
152 runtime = toc(Tstart);
155 line_debug(
'Using parallel method, calling solver_ssa_analyzer_parallel');
157 [XN,UN,QN,RN,TN,CN,tranSysState,tranSync,sn] = ...
158 solver_ssa_analyzer_parallel(sn, init_state, options);
161 if strcmp(ME.identifier,
'MATLAB:spmd:NoPCT')
162 line_printf(['Parallel Computing Toolbox unavailable – ',...
163 'falling back to serial SSA.\n']);
164 [XN,UN,QN,RN,TN,CN,tranSysState,tranSync,sn] = ...
165 solver_ssa_analyzer_serial(sn, init_state, options, true);
173 error('solver_ssa_analyzer:UnknownMethod', ...
174 'Unknown analysis method: %s', options.method);
177% Compute CI using batch means if enabled
178if confintEnabled && ~isempty(tranSysState) && length(tranSysState) > 1
179 [QNCI, UNCI, RNCI, TNCI, ANCI, WNCI] = ssa_compute_batch_means_ci(tranSysState, sn, confintLevel, options);
182runtime = toc(Tstart);
185function [QNCI, UNCI, RNCI, TNCI, ANCI, WNCI] = ssa_compute_batch_means_ci(tranSysState, sn, confintLevel, options)
186% SSA_COMPUTE_BATCH_MEANS_CI Compute confidence intervals using batch means method
188% tranSysState{1} contains the cumulative time at each sample
189% tranSysState{2:end} contain the state vectors
for each stateful node
200% Extract time and state data
201if iscell(tranSysState) && length(tranSysState) > 1
202 times = tranSysState{1};
203 nSamples = length(times);
206 % Not enough samples
for batch means
210 % Number of batches (use 10-30 batches
for good CI estimation)
211 numBatches = min(20, floor(nSamples / 10));
215 batchSize = floor(nSamples / numBatches);
217 % Discard initial transient before batch means: use
218 % options.config.warmupfrac when set (> 0),
else the legacy 10% discard
219 if isfield(options,
'config') && isfield(options.config,
'warmupfrac') ...
220 && ~isempty(options.config.warmupfrac) && options.config.warmupfrac > 0
221 warmupfrac = options.config.warmupfrac;
225 transientCutoff = max(1, floor(nSamples * warmupfrac));
227 % Extract queue length data from tranSysState
228 % tranSysState{2:end} contains state vectors
for each stateful node
229 % We need to compute marginal queue lengths per station/
class
231 % Compute batch means
for queue lengths
233 isf = sn.stationToStateful(ist);
234 if isf > 0 && (1 + isf) <= length(tranSysState)
235 stateData = tranSysState{1 + isf};
236 if isempty(stateData)
241 % Extract queue length
for this station/
class from state data
242 % The state data format depends on the scheduling strategy
243 % For simplicity, we
'll use the marginal extraction
244 ind = sn.stationToNode(ist);
246 % Compute time-weighted batch means
247 batchMeans = zeros(1, numBatches);
249 startIdx = transientCutoff + (b-1) * batchSize + 1;
250 endIdx = min(transientCutoff + b * batchSize, nSamples);
251 if startIdx > nSamples || startIdx >= endIdx
255 % Compute time-weighted average for this batch
256 % times contains cumulative times, compute inter-sample durations
258 prevTime = times(startIdx - 1);
262 batchTimes = times(startIdx:endIdx);
264 if length(batchTimes) >= 1
265 % Compute time duration each state was held
267 dt = [batchTimes(1); diff(batchTimes)];
269 dt = [batchTimes(1) - prevTime; diff(batchTimes)];
272 % Extract queue lengths from state data
273 % For now, sum all columns to get total jobs at the station
274 % This works for most queue types where state represents job counts
275 qLengths = sum(stateData(startIdx:endIdx, :), 2);
279 batchMeans(b) = sum(qLengths .* dt) / totalTime;
284 % Count valid batches (non-NaN)
285 validMask = ~isnan(batchMeans);
286 batchMeans = batchMeans(validMask);
287 nBatches = length(batchMeans);
290 % Compute mean and standard error
291 batchMean = mean(batchMeans);
292 batchStd = std(batchMeans);
293 stdErr = batchStd / sqrt(nBatches);
295 % t-critical value for confidence level
296 alpha = 1 - confintLevel;
297 tCrit = tinv(1 - alpha/2, nBatches - 1);
299 % Confidence interval half-width
300 QNCI(ist, k) = tCrit * stdErr;
306 % For utilization, response time, and throughput CIs, use relative scaling
307 % These are derived from queue length CI using Little's law relationships
308 UNCI = QNCI; % Simplified - utilization CI scales similarly
309 RNCI = QNCI; % Response time CI - would need service rate info
310 TNCI = QNCI; % Throughput CI - would need arrival rate info
314function ok = fcrNrmOK(sn)
315% Finite capacity regions are supported under both rules. DROP destroys a
316% refused job (the departure fires and the job never reaches the destination);
317% WAITQ parks it in a per-region FIFO and admits it head-of-line as capacity
318% frees. The NRM carries the FIFO explicitly (see fcrReleaseCascade), so no
319% region rule forces a fallback. Linear-constraint and memory-budget regions
320% ride the same admission test.
324function ok = routingNrmOK(sn)
325% True when every routing strategy in the model
is one the NRM resolves at
326% firing time. JSQ and SQ(d) select from the candidate queue lengths and
327% RROBIN/WRROBIN walk a rotation pointer, which the NRM carries as auxiliary
328% state alongside the buffers, since no rate reads it -- it only steers the
329% destination draw, so it need not enter the reaction network. RL needs an
330% external policy and still
requires the serial engine.
331ok = ~any(sn.routing(:) == RoutingStrategy.RL);
337function ok = balkNrmOK(sn)
338% True when no station uses a balking strategy that the NRM cannot evaluate.
339% QUEUE_LENGTH
is a pure function of the state vector, so the NRM draws it at
340% firing time; EXPECTED_WAIT and COMBINED depend on the mean waiting time and
341% need the serial engine (State.afterEventStation rejects them likewise).
343if ~isfield(sn,
'balkingStrategy') || isempty(sn.balkingStrategy)
346bs = sn.balkingStrategy(:);
347ok = all(bs == 0 | bs == BalkingStrategy.QUEUE_LENGTH);
350function ok = cacheNrmOK(sn)
351% True for every Cache node. The NRM models a cache access as an immediate
352% state-dependent class switch (read -> hit/miss/retrieval) at the cache node,
353% applying the same replacement logic as State.afterEventCache to the cache
354% contents carried alongside the buffers, INCLUDING the retrieval (delayed-hit)
355% system: a miss for an item not yet being fetched begins a retrieval (the job
356%
is routed to the fetch queue and returns to complete the miss), and a
357% concurrent request for an item already being fetched
is absorbed as a delayed
358% hit -- matching the serial engine's sample-path semantics.
362function ok = isNrmEligible(sn)
363% True when the NRM engine can run this model. The scheduling list matches
364% solver_ssa_analyzer_nrm's own validation (INF/PS family, non-preemptive
365% buffered family, LCFSPR, PAS, POLLING); the per-feature guards exclude the
366% sub-cases it cannot reproduce. The NRM simulates open and closed models alike
367% -- it only lacks Fork/Join node handling -- so the gate
is a Fork/Join
368% exclusion, not the INF/PS-only sn_is_population_model. Used to prefer the NRM
369% on the default and parallel dispatch paths.
370allowedSched = [SchedStrategy.INF, SchedStrategy.EXT, SchedStrategy.PS, ...
371 SchedStrategy.LPS, SchedStrategy.DPS, SchedStrategy.GPS, ...
372 SchedStrategy.PSPRIO, SchedStrategy.DPSPRIO, SchedStrategy.GPSPRIO, ...
373 SchedStrategy.FCFS, SchedStrategy.LCFS, SchedStrategy.SIRO, ...
374 SchedStrategy.HOL, SchedStrategy.SEPT, SchedStrategy.LEPT, ...
375 SchedStrategy.LCFSPR, SchedStrategy.PAS, SchedStrategy.POLLING];
376ok = all(arrayfun(@(s) any(s == allowedSched), sn.sched)) && ...
377 cacheNrmOK(sn) && ...
378 ~sn_has_fork_join(sn) && ...
379 routingNrmOK(sn) && ...
382 renegeNrmOK(sn) && ...
386function ok = renegeNrmOK(sn)
387% True when no station renegs with non-exponential patience. The NRM abandons
388% at the aggregate rate (waiting count)*mu, which
is only correct when patience
389%
is memoryless; phase-type patience would need each waiting job's remaining
390% phase. SOLVER_SSA rejects the same combination outright.
392if ~isfield(sn,'impatienceClass') || isempty(sn.impatienceClass)
395bad = (sn.impatienceClass == ImpatienceType.RENEGING) & (sn.impatienceType ~= ProcessType.EXP);
399function ok = phaseNrmOK(sn)
400% True when every non-exponential service sits at a station whose rate law the
401% NRM expands exactly. Phase expansion splits the class-level share across a
402% class's phases in the ratio kir/nir, which needs only the per-phase
403% populations -- true of the INF/PS family, where every job present
is in
404% service. A buffered policy instead needs the phase multiset of the jobs
405% ACTUALLY in service, which the waiting-only buffer does not record, so
406% non-exponential service there still needs the serial engine.
408% see _kb/06-solver-catalog.md for rationale (SSA NRM dispatch, EXT-source exclusion)
409exact = [SchedStrategy.INF, SchedStrategy.PS, SchedStrategy.LPS, ...
410 SchedStrategy.DPS, SchedStrategy.GPS, SchedStrategy.PSPRIO, ...
411 SchedStrategy.DPSPRIO, SchedStrategy.GPSPRIO, ...
412 SchedStrategy.FCFS, SchedStrategy.LCFS, SchedStrategy.SIRO, ...
413 SchedStrategy.HOL, SchedStrategy.SEPT, SchedStrategy.LEPT];
414for ist = 1:sn.nstations
415 for r = 1:sn.nclasses
416 if sn.procid(ist,r) == ProcessType.DISABLED || sn.procid(ist,r) == ProcessType.EXP
419 if ~any(sn.sched(ist) == exact)