1classdef SolverMAM < NetworkSolver
2 % Matrix-Analytic and RCAT methods solver
4 % Implements matrix-analytic methods and RCAT
for structured Markov chain analysis.
6 % Copyright (c) 2012-2026, Imperial College London
10 function self = SolverMAM(model,varargin)
11 % SOLVERMAM Create a Matrix-Analytic Methods solver instance
13 % @brief Creates a MAM solver
for structured Markov chain analysis
14 % @param model Network model to be analyzed via matrix-analytic methods
15 % @param varargin Optional parameters (method, tolerance, etc.)
16 % @
return self SolverMAM instance configured
for MAM analysis
18 self@NetworkSolver(model, mfilename);
19 self.setOptions(Solver.parseOptions(varargin, self.defaultOptions));
23 function sn = getStruct(self)
26 % Get data structure summarizing
the model
27 sn = self.model.getStruct(
true);
30 runtime = runAnalyzer(self, options);
31 RD = getCdfRespT(self, R);
33 function [allMethods] = listValidMethods(self)
34 % allMethods = LISTVALIDMETHODS()
35 % List valid methods
for this solver
36 sn = self.model.getStruct();
37 % Note: Method order must match expected values in test files.
38 % New methods should be added at
the end to preserve index alignment.
39 %
'exact' method removed - autocat moved to line-legacy.git
40 allMethods = {
'default',
'dec.source',
'dec.mmap',
'dec.poisson',
'mna',
'inap',
'ldqbd',
'inapplus',
'dec.source.mmap',
'inapinf'};
43 function featSupported = getMethodFeatureSet(self, method) %#ok<INUSD>
44 % Every MAM method shares
the solver-level feature envelope;
the
45 % genuine per-method restrictions (
'mna',
'ldqbd') are structural
46 % and are applied in supportsModelMethod below.
48 % Defining this
is what lets NetworkSolver.supportsModelMethod name
49 %
the offending features: with no method feature set it falls back
50 % to
the coarse supports(model) and returns an empty reason, so
the
51 % gate could only report "features not supported" without saying
53 featSupported = SolverMAM.getFeatureSet();
56 function [
bool, reason] = supportsModelMethod(self, method)
57 % Method-aware gate for
the genuine per-method restrictions of
the
58 % MAM analyzer (mirrors
the inline guards in solver_mam_analyzer):
59 % 'mna' does not support mixed open/closed models, and 'ldqbd'
60 % requires a single-class model. All other methods rely on
the
61 % coarse MAM feature set and
the analyzer's topology-based routing
62 % (e.g. a Fork-Join model on 'default'/'dec.source'
is routed to
the
63 % FJ solver, not rejected).
64 sn = self.model.getStruct();
67 if ~sn_is_open_model(sn) && ~sn_is_closed_model(sn)
69 reason = 'The mna method does not support mixed open/closed models.';
72 % mna decomposes INTER-station arrival flows: each class
is
73 % assumed to flow between stations, generating an arrival
74 % process to fit at each queue. A self-looping class
is
75 % confined to a single queueing station, so it has no
76 % inter-station flow and its "arrival"
MMAP degenerates to
77 % an absorbing generator (ctmc_solve then errors with "no
78 % recurrent state" inside map_lambda). Reject rather than
79 % return
the ~1/FineTol garbage
the fit produced before.
81 for kmna = 1:sn.nclasses
82 % Only a CLOSED class
is self-looping: an open class
83 % confined to one queue has an external Poisson source
84 % that mna decomposes normally. njobs
is Inf for open.
85 if ~isfinite(sn.njobs(kmna))
88 vis = find(Vmna(:, kmna) > GlobalConstants.FineTol);
89 if isscalar(vis) && sn.sched(vis) ~= SchedStrategy.INF ...
90 && sn.sched(vis) ~= SchedStrategy.EXT
92 reason = sprintf(['The mna method does not support self-looping ' ...
93 'classes (class %d
is confined to station %d with no ' ...
94 'inter-station flow to decompose). Use
the dec.source method.'], ...
102 reason = 'The ldqbd method requires a single-class model.';
105 case {
'inap',
'inapplus',
'inapinf',
'exact'}
106 % RCAT builds one scalar birth-death chain per
107 % (station,
class) from sn.rates alone (build_rcat in
108 % solver_mam_ag):
the state
is the queue length, with no
109 % service-phase dimension. Every process
is therefore
110 % collapsed to its mean rate, so a non-exponential arrival
111 % or service process would be answered as
if it were
112 % exponential. Measured vs SolverCTMC on an open M/PH/1,
113 % INAP returns
the M/M/1 queue length
for EVERY scv
114 % (1.000000 at rho=0.5, 4.000000 at rho=0.8): 14% error at
115 % scv=0.5 and 43% at scv=4.0. Reject rather than
116 % mis-answer; use dec.source
for non-exponential models.
117 nonExp = (sn.procid ~= ProcessType.EXP) & isfinite(sn.rates) & (sn.rates > 0);
118 % A signal
class has no queue process of its own: build_rcat
119 % skips it in processMap and reads only its Source arrival
120 % rate, so its SERVICE process at a queue station
is never
121 % used. It
is a trigger, and models commonly declare it
122 % Immediate, so exempt those entries (the signal's arrival
123 % process at the Source is still required to be exponential).
124 if isfield(sn, 'issignal') && ~isempty(sn.issignal) && any(sn.issignal)
125 issource = false(size(nonExp, 1), 1);
126 for ist = 1:numel(issource)
127 issource(ist) = sn.nodetype(sn.stationToNode(ist)) == NodeType.Source;
129 nonExp(~issource, logical(sn.issignal(:))') = false;
132 [ist, r] = find(nonExp, 1);
134 reason = sprintf(['The %s method supports exponential processes only ' ...
135 '(RCAT models each station-class by its mean rate, with no service-phase ' ...
136 'dimension), but station %d class %d is %s. Use the dec.source method ' ...
137 'for non-exponential models.'], method, ist, r, ...
138 ProcessType.toText(sn.procid(ist, r)));
141 % build_rcat never reads sn.nservers: every station
is
142 % modelled as a SINGLE server, with departure rate mu rather
143 % than min(n,c)*mu. For c > 1
the isolated chain
is then
144 % driven at rho = lambda/mu instead of lambda/(c*mu), so a
145 % perfectly stable station becomes unstable in isolation and
146 % pins against
the maxStates truncation. Measured vs
147 % SolverCTMC: open M/M/2 rho=0.6 gives 94.000001 instead of
148 % 1.875000, M/M/3 gives 97.750000 instead of 2.332117 (~50x).
149 % Reject rather than mis-answer.
150 multi = isfinite(sn.nservers) & (sn.nservers > 1);
152 ist = find(multi, 1);
154 reason = sprintf(['The %s method supports single-server stations only ' ...
155 '(RCAT does not model sn.nservers, so a multiserver station is driven ' ...
156 'at rho = lambda/mu instead of lambda/(c*mu)), but station %d has %d ' ...
157 'servers. Use the dec.source method for multiserver models.'], ...
158 method, ist, sn.nservers(ist));
162 [bool, reason] = supportsModelMethod@NetworkSolver(self, method);
169 function featSupported = getFeatureSet()
170 % FEATSUPPORTED = GETFEATURESET()
172 featSupported = SolverFeatureSet;
174 featSupported.setTrue({'Sink','Source',...
175 'Fork','Join','Forker','Joiner',... % Fork-Join support (via FJ_codes)
176 'Delay','DelayStation','Queue',...
177 'APH','Coxian','Erlang','Exp','HyperExp','MMPP2','MAP','MMAP','DMAP','ME','RAP',...
178 'Det','Gamma','Lognormal','Pareto','Uniform','Weibull',...
179 'StatelessClassSwitcher','InfiniteServer',...
181 'SharedServer','Buffer','Dispatcher',...
182 'Server','JobSink','RandomSource','ServiceTunnel',...
183 'SchedStrategy_INF','SchedStrategy_PS','SchedStrategy_HOL',...
184 'SchedStrategy_FCFSPRPRIO',... % solver_mam_basic: MMAPPH1PRPR
185 'SchedStrategy_FCFS',...
186 'RoutingStrategy_PROB','RoutingStrategy_RAND',...
187 'ClosedClass','SelfLoopingClass',...
189 % Add RCAT (AG) features
190 featSupported.setTrue({'Sink', 'Source', ...
191 'Fork','Join','Forker','Joiner',... % Fork-Join support (via FJ_codes)
192 'Delay', 'DelayStation', 'Queue', ...
193 'APH', 'Coxian', 'Erlang', 'Exp', 'HyperExp', ...
194 'Det','Gamma','Lognormal','Pareto','Uniform','Weibull',...
195 'StatelessClassSwitcher', 'InfiniteServer', ...
196 'SharedServer', 'Buffer', 'Dispatcher', ...
197 'Server', 'JobSink', 'RandomSource', 'ServiceTunnel', ...
198 'SchedStrategy_INF', 'SchedStrategy_PS', ...
199 'SchedStrategy_FCFS', ...
200 'RoutingStrategy_PROB', 'RoutingStrategy_RAND', ...
203 'OpenSignal', 'ClosedSignal', ... % G-network signals (solver_mam_ag)
204 'SignalType_NEGATIVE', 'SignalType_CATASTROPHE', ...
205 'SignalBatchRemoval'}); % AG reads sn.signalremdist
206 % Add BMAP/PH/N/N retrial queue features
207 featSupported.setTrue({'Retrial', 'BMAP', 'PH'});
208 % Setup/delay-off: open stations are solved exactly by
209 % qbd_setupdelayoff; closed stations use
the per-instance
210 % cold-start race of
the isfunction branch.
211 featSupported.setTrue({'SetupDelayOff'});
214 function [bool, featSupported] = supports(model)
215 % [BOOL, FEATSUPPORTED] = SUPPORTS(MODEL)
217 featUsed = model.getUsedLangFeatures();
218 featSupported = SolverMAM.getFeatureSet();
219 bool = SolverFeatureSet.supports(featSupported, featUsed);
222 function options = defaultOptions()
223 % OPTIONS = DEFAULTOPTIONS()
224 options = SolverOptions('MAM');
227 function libs = getLibrariesUsed(sn, options)
228 % GETLIBRARIESUSED Get list of external libraries used by MAM solver
229 % Detect libraries used by MAM solver based on topology and method
232 % MAMSolver used
for matrix-analytic methods (M/G/1, GI/M/1 types)
233 % This includes
default and decomposition methods
234 if ismember(options.method, {
'default',
'dec.source',
'dec.mmap',
'dec.poisson',
'dec.source.mmap'})
235 libs{end+1} =
'MAMSolver';
238 % Q-MAM used
for specific RCAT-based methods
239 if ismember(options.method, {
'mna',
'inap',
'inapplus',
'inapinf'})
240 libs{end+1} =
'Q-MAM';
243 % SMCSolver used
for QBD (Quasi-Birth-Death) analysis
244 % Currently available but not actively used in
default paths
245 % Uncomment when QBD methods are activated:
246 %
if ismember(options.method, {
'qbd'})
247 % libs{end+1} =
'SMCSolver';
250 % BUTools usage detection (via KPCToolbox MAP functions)
251 % BUTools
is used when analyzing MAP/PH distributions
252 if ~isempty(sn) && isfield(sn,
'proc') && ~isempty(sn.proc) && any(~cellfun(@isempty, sn.proc(:)))
253 libs{end+1} =
'BUTools';
256 % Remove duplicates and maintain order
257 libs = unique(libs,
'stable');