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 % 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);
51 if confintEnabled && ~isempty(tranSysState) && length(tranSysState) > 1
52 [QNCI, UNCI, RNCI, TNCI, ANCI, WNCI] = ssa_compute_batch_means_ci(tranSysState, sn, confintLevel);
54 runtime = toc(Tstart);
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);
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);
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);
74 runtime = toc(Tstart);
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';
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';
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';
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');
123 [XN,UN,QN,RN,TN,CN,tranSysState,tranSync,sn] = ...
124 solver_ssa_analyzer_nrm(sn, options);
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);
131 runtime = toc(Tstart);
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';
140% SERIAL / PARALLEL ANALYSERS (legacy paths) ------------------------------
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);
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.
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);
157 if confintEnabled && ~isempty(tranSysState) && length(tranSysState) > 1
158 [QNCI, UNCI, RNCI, TNCI, ANCI, WNCI] = ssa_compute_batch_means_ci(tranSysState, sn, confintLevel, options);
160 runtime = toc(Tstart);
163 line_debug(
'Using parallel method, calling solver_ssa_analyzer_parallel');
165 [XN,UN,QN,RN,TN,CN,tranSysState,tranSync,sn] = ...
166 solver_ssa_analyzer_parallel(sn, init_state, options);
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);
181 error('solver_ssa_analyzer:UnknownMethod', ...
182 'Unknown analysis method: %s', options.method);
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);
190runtime = toc(Tstart);
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
196% tranSysState{1} contains
the cumulative time at each sample
197% tranSysState{2:end} contain
the state vectors
for each stateful node
208% Extract time and state data
209if iscell(tranSysState) && length(tranSysState) > 1
210 times = tranSysState{1};
211 nSamples = length(times);
214 % Not enough samples
for batch means
218 % Number of batches (use 10-30 batches
for good CI estimation)
219 numBatches = min(20, floor(nSamples / 10));
223 batchSize = floor(nSamples / numBatches);
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;
233 transientCutoff = max(1, floor(nSamples * warmupfrac));
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
239 % Compute batch means
for queue lengths
241 isf = sn.stationToStateful(ist);
242 if isf > 0 && (1 + isf) <= length(tranSysState)
243 stateData = tranSysState{1 + isf};
244 if isempty(stateData)
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);
254 % Compute time-weighted batch means
255 batchMeans = zeros(1, numBatches);
257 startIdx = transientCutoff + (b-1) * batchSize + 1;
258 endIdx = min(transientCutoff + b * batchSize, nSamples);
259 if startIdx > nSamples || startIdx >= endIdx
263 % Compute time-weighted average for this batch
264 % times contains cumulative times, compute inter-sample durations
266 prevTime = times(startIdx - 1);
270 batchTimes = times(startIdx:endIdx);
272 if length(batchTimes) >= 1
273 % Compute time duration each state was held
275 dt = [batchTimes(1); diff(batchTimes)];
277 dt = [batchTimes(1) - prevTime; diff(batchTimes)];
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);
287 batchMeans(b) = sum(qLengths .* dt) / totalTime;
292 % Count valid batches (non-NaN)
293 validMask = ~isnan(batchMeans);
294 batchMeans = batchMeans(validMask);
295 nBatches = length(batchMeans);
298 % Compute mean and standard error
299 batchMean = mean(batchMeans);
300 batchStd = std(batchMeans);
301 stdErr = batchStd / sqrt(nBatches);
303 % t-critical value for confidence level
304 alpha = 1 - confintLevel;
305 tCrit = tinv(1 - alpha/2, nBatches - 1);
307 % Confidence interval half-width
308 QNCI(ist, k) = tCrit * stdErr;
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
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.
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);
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).
352if ~isfield(sn,
'balkingStrategy') || isempty(sn.balkingStrategy)
355bs = sn.balkingStrategy(:);
356ok = all(bs == 0 | bs == BalkingStrategy.QUEUE_LENGTH);
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.
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) && ...
391 renegeNrmOK(sn) && ...
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.
401if ~isfield(sn,'impatienceClass') || isempty(sn.impatienceClass)
404bad = (sn.impatienceClass == ImpatienceType.RENEGING) & (sn.impatienceType ~= ProcessType.EXP);
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.
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.
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
440 if ~any(sn.sched(ist) == exact)