1function out = mvaDispatch(self, sn, options)
2% OUT = MVADISPATCH(SN, OPTIONS)
4% One inner solve of the MVA analyzer dispatch: pick the analyzer that fits
5% SN and
return its metrics. This
is the solve callback that
6% @NetworkSolver/fjFixedPoint.m drives on each pass of the fork-join (MMT/HT)
7% fixed point; on a model without forks it runs exactly once. The body
is the
8% dispatch that used to sit
inline in runAnalyzer.
10% OUT has fields QN, UN, RN, TN, CN, XN, lG, runtime, lastiter, method and
11% actualmethod (
'' when the analyzer did not report one).
13% Copyright (c) 2012-2026, Imperial College London
16 line_debug(options,
'Product-form check: hasProductForm=%d (exact method requested)', self.model.hasProductFormSolution);
17 % OI/PAS are exactly solvable via solver_mva_oi_analyzer; exempt from the product-
form guard (see _kb/06-solver-catalog.md, MVA OI dispatch)
18 hasOIorPAS = any(sn.sched == SchedStrategy.OI | sn.sched == SchedStrategy.PAS);
19 if strcmp(options.method,
'exact') && ~self.model.hasProductFormSolution && ~hasOIorPAS
20 line_error(mfilename,
'The exact method requires the model to have a product-form solution. This model does not have one.\nYou can use Network.hasProductFormSolution() to check before running the solver.\n Run the ''mva'' method to obtain an approximation based on the exact MVA algorithm.\n');
22 if strcmp(options.method,
'mva') && ~self.model.hasProductFormSolution
23 line_warning(mfilename,
'The exact method requires the model to have a product-form solution. This model does not have one.\nYou can use Network.hasProductFormSolution() to check before running the solver.\nSolverMVA will return an approximation generated by an exact MVA algorithm.');
26 method = options.method;
28 % Check
for size-based policies (SRPT, PSJF, FB, LRPT, SETF) in multiclass open systems
29 queueIdx = find(sn.nodetype == NodeType.Queue);
30 isSizeBasedPolicy =
false;
32 schedType = sn.sched(sn.nodeToStation(queueIdx(1)));
33 isSizeBasedPolicy = ismember(schedType, [SchedStrategy.SRPT, SchedStrategy.PSJF, SchedStrategy.FB, SchedStrategy.LRPT, SchedStrategy.SETF]);
36 % Check
for order-independent (OI) queues. An OI station
is a PAS/OI
37 % queue with an all-zero swap graph and a service-rate function;
38 % detect it the same way the NC-oi path does (nc_is_oi_model),
39 % not via a nodeparam flag (which
is never populated).
41 if ~any(isinf(sn.njobs))
42 for ist = 1:sn.nstations
43 if sn.sched(ist) ~= SchedStrategy.PAS && sn.sched(ist) ~= SchedStrategy.OI
46 ind = sn.stationToNode(ist);
47 if ind < 1 || ind > numel(sn.nodeparam) || ~isstruct(sn.nodeparam{ind}) ...
48 || ~isfield(sn.nodeparam{ind},
'swapGraph') ...
49 || ~isfield(sn.nodeparam{ind},
'svcRateFun') || isempty(sn.nodeparam{ind}.svcRateFun)
52 sg = sn.nodeparam{ind}.swapGraph;
53 if isempty(sg) || any(sg(:) ~= 0)
61 ci_cache = find(sn.nodetype == NodeType.Cache, 1);
62 hasRetrieval = ~isempty(ci_cache) && isfield(sn.nodeparam{ci_cache},
'retrievalSystemCapacity') ...
63 && sn.nodeparam{ci_cache}.retrievalSystemCapacity > 0;
64 hasOIStation = any(sn.sched == SchedStrategy.OI | sn.sched == SchedStrategy.PAS);
65 if ~isempty(noi_idx) && nc_is_oi_model(sn) && any(strcmpi(options.method,{
'default',
'exact'}))
66 % Order-independent queueing network
67 line_debug(options,
'MVA: order-independent closed network, routing to solver_mva_oi_analyzer');
68 [QN,UN,RN,TN,CN,XN,lG,runtime,lastiter,actualmethod] = solver_mva_oi_analyzer(sn, options);
70 % MVA supports OI/PAS stations only via the exact path above;
71 % see _kb/06-solver-catalog.md (MVA section)
for why AMVA cannot.
72 line_error(mfilename, sprintf([
'SolverMVA supports order-independent (OI) and pass-and-swap (PAS) stations only\n' ...
73 'through its exact order-independent analyzer, which requires method ''default'' or ''exact''\n' ...
74 '(got ''%s''), an empty/zero swap graph at every OI/PAS station, a closed model, and every\n' ...
75 'other station to be product-form (INF, PS, LCFS-PR, SIRO, or class-independent-rate FCFS).\n' ...
76 'Use SolverCTMC or SolverLDES for this model.'], options.method));
77 elseif hasRetrieval % delayed-hit (retrieval-system) cache
78 if any(sn.nodetype == NodeType.Source)
79 line_debug(options, 'Open delayed-hit retrieval cache, routing to mva_retrieval_analyzer');
80 [QN,UN,RN,TN,CN,XN,lG,hitprob,missprob,delayedprob,hitproblist,itemprob,latency,runtime,actualmethod] = solver_mva_retrieval_analyzer(sn, options);
82 line_debug(options, 'Closed integrated delayed-hit retrieval cache, routing to mva_cacheqn_retrieval_analyzer');
83 [QN,UN,RN,TN,CN,XN,lG,hitprob,missprob,delayedprob,hitproblist,itemprob,latency,runtime,actualmethod] = solver_mva_cacheqn_retrieval_analyzer(sn, options);
87 if sn.nodetype(ind) == NodeType.Cache
88 self.model.
nodes{ind}.setResultHitProb(hitprob);
89 self.model.nodes{ind}.setResultMissProb(missprob);
90 self.model.nodes{ind}.setResultDelayedHitProb(delayedprob);
91 self.model.nodes{ind}.setResultHitProbList(hitproblist);
92 self.model.nodes{ind}.setResultItemProb(itemprob);
93 self.model.nodes{ind}.setResultResidT(latency);
96 self.model.refreshStruct(
true);
97 elseif sn.nclosedjobs == 0 && length(sn.nodetype)==3 && all(sort(sn.nodetype)
' == sort([NodeType.Source,NodeType.Queue,NodeType.Sink])) && isSizeBasedPolicy
98 % Multiclass open system with size-based scheduling (SRPT, PSJF, FB, LRPT, SETF)
99 line_debug(options, 'Size-based scheduling detected (%s), routing to qsys_sizebased_analyzer
', char(schedType));
100 [QN,UN,RN,TN,CN,XN,lG,runtime,lastiter,actualmethod] = solver_mva_qsys_sizebased_analyzer(sn, options, schedType);
101 elseif sn.nclasses==1 && sn.nclosedjobs == 0 && length(sn.nodetype)==3 && all(sort(sn.nodetype)' == sort([NodeType.Source,NodeType.Queue,NodeType.Sink])) %
is an open queueing system
102 line_debug(options,
'Single-class open queueing system (Source-Queue-Sink), routing to qsys_analyzer');
103 [QN,UN,RN,TN,CN,XN,lG,runtime,lastiter,actualmethod] = solver_mva_qsys_analyzer(sn, options);
104 elseif sn.nclasses>1 && sn.nclosedjobs == 0 && length(sn.nodetype)==3 && all(sort(sn.nodetype)
' == sort([NodeType.Source,NodeType.Queue,NodeType.Sink])) && sn.sched(find(sn.nodetype==NodeType.Queue)) == SchedStrategy.POLLING % is an open polling system
105 line_debug(options, 'Multiclass open polling system, routing to polling_analyzer
');
106 [QN,UN,RN,TN,CN,XN,lG,runtime,lastiter,actualmethod] = solver_mva_polling_analyzer(sn, options);
107 elseif sn.nclasses>1 && sn.nclosedjobs == 0 && length(sn.nodetype)==3 && all(sort(sn.nodetype)' == sort([NodeType.Source,NodeType.Queue,NodeType.Sink])) && sn.sched(find(sn.nodetype==NodeType.Queue)) == SchedStrategy.HOL && sn.nservers(sn.nodeToStation(sn.nodetype==NodeType.Queue)) == 1 && all(abs(sn.scv(sn.nodeToStation(sn.nodetype==NodeType.Source),:) - 1) < 1e-6 | ~isfinite(sn.scv(sn.nodeToStation(sn.nodetype==NodeType.Source),:)))
108 % Multiclass open HOL (non-preemptive priority) M/G/1: exact Cobham
109 % formula; the AMVA path applies the preemptive shadow-server formula.
110 line_debug(options,
'Multiclass open HOL priority queue, routing to qsys_prio_analyzer');
111 [QN,UN,RN,TN,CN,XN,lG,runtime,lastiter,actualmethod] = solver_mva_qsys_prio_analyzer(sn, options);
112 elseif sn.nclasses>1 && sn.nclasses<=3 && sn.nclosedjobs == 0 && length(sn.nodetype)==3 && all(sort(sn.nodetype)
' == sort([NodeType.Source,NodeType.Queue,NodeType.Sink])) && sn.sched(find(sn.nodetype==NodeType.Queue)) == SchedStrategy.DPS && sn.nservers(sn.nodeToStation(sn.nodetype==NodeType.Queue)) == 1 && all(abs(sn.scv(sn.nodeToStation(sn.nodetype==NodeType.Source),:) - 1) < 1e-6 | ~isfinite(sn.scv(sn.nodeToStation(sn.nodetype==NodeType.Source),:))) && all(abs(sn.scv(sn.nodeToStation(sn.nodetype==NodeType.Queue),:) - 1) < 1e-6 | ~isfinite(sn.scv(sn.nodeToStation(sn.nodetype==NodeType.Queue),:)))
113 % Single open M/M/1-DPS queue: numerically exact DPS via the
114 % truncated multiclass CTMC (qsys_mm1_dps). The AMVA-DPS cross-term
115 % correction violates the equal-rate conservation law.
116 line_debug(options, 'Multiclass open M/M/1-DPS queue, routing to exact qsys_mm1_dps
');
118 src_dps = sn.nodeToStation(sn.nodetype==NodeType.Source);
119 q_dps = sn.nodeToStation(sn.nodetype==NodeType.Queue);
120 lam_dps = sn.rates(src_dps,:);
121 mu_dps = sn.rates(q_dps,:);
122 w_dps = sn.schedparam(q_dps,:); w_dps(~(w_dps>0)) = 1;
123 Tdps = qsys_mm1_dps(lam_dps, mu_dps, w_dps);
124 QN = zeros(sn.nstations, sn.nclasses); UN = QN; RN = QN; TN = QN; CN = QN;
125 XN = zeros(1, sn.nclasses);
126 for rdps = 1:sn.nclasses
127 RN(q_dps,rdps) = Tdps(rdps); CN(q_dps,rdps) = Tdps(rdps);
128 XN(rdps) = lam_dps(rdps);
129 UN(q_dps,rdps) = lam_dps(rdps)/mu_dps(rdps);
130 TN(q_dps,rdps) = lam_dps(rdps); TN(src_dps,rdps) = lam_dps(rdps);
131 QN(q_dps,rdps) = lam_dps(rdps)*Tdps(rdps);
133 lG = 0; lastiter = 0; runtime = toc(T0dps);
134 actualmethod = 'mm1.dps
';
135 elseif sn.nclosedjobs == 0 && length(sn.nodetype)==3 && all(sort(sn.nodetype)' == sort([NodeType.Source,NodeType.Cache,NodeType.Sink])) %
is a non-rentrant cache
136 line_debug(options,
'Non-reentrant cache (Source-Cache-Sink), routing to cache_analyzer');
137 % random initialization
138 for ind = 1:sn.nnodes
139 if sn.nodetype(ind) == NodeType.Cache
140 prob = self.model.nodes{ind}.server.hitClass;
142 self.model.nodes{ind}.setResultHitProb(prob);
143 self.model.nodes{ind}.setResultMissProb(1-prob);
146 self.model.refreshChains();
148 [QN,UN,RN,TN,CN,XN,lG,runtime,lastiter,actualmethod,hitproblist,itemprob] = solver_mva_cache_analyzer(sn, options);
150 for ind = 1:sn.nnodes
151 if sn.nodetype(ind) == NodeType.Cache
152 self.model.nodes{ind}.setResultHitProbList(hitproblist);
153 self.model.nodes{ind}.setResultItemProb(itemprob);
154 hitClass = self.model.nodes{ind}.getHitClass;
155 missClass = self.model.nodes{ind}.getMissClass;
156 hitprob = zeros(1,length(hitClass));
157 for k=1:length(self.model.nodes{ind}.getHitClass)
158 chain_k = sn.chains(:,k)>0;
159 inchain = sn.chains(chain_k,:)>0;
163 hitprob(k) = XN(h) / sum(XN(inchain),"omitnan"); %
#ok<NANSUM>
166 self.model.nodes{ind}.setResultHitProb(hitprob);
167 self.model.nodes{ind}.setResultMissProb(1-hitprob);
170 %self.model.refreshChains();
171 self.model.refreshStruct(
true);
172 else % queueing network
173 if any(sn.nodetype == NodeType.Cache) %
if integrated caching-queueing
174 line_debug(options,
'Integrated caching-queueing network, routing to cacheqn_analyzer');
175 [QN,UN,RN,TN,CN,XN,lG,hitprob,missprob,runtime,lastiter] = solver_mva_cacheqn_analyzer(self, options);
176 for ind = 1:sn.nnodes
177 if sn.nodetype(ind) == NodeType.Cache
178 self.model.nodes{ind}.setResultHitProb(hitprob(ind,:));
179 self.model.nodes{ind}.setResultMissProb(missprob(ind,:));
182 %self.model.refreshChains();
183 self.model.refreshStruct(
true);
184 else % ordinary queueing network
186 case {
'aba.upper',
'aba.lower',
'bjb.upper',
'bjb.lower',
'pb.upper',
'pb.lower',
'gb.upper',
'gb.lower',
'sb.upper',
'sb.lower',
'mwba.upper',
'mwba.lower'}
187 line_error(mfilename, [
'Bound methods have moved to SolverBA. ' ...
188 'Use SolverBA(model,''method'',''%s'') (or LINE with that method) instead of SolverMVA.'], method);
189 case {
'marie',
'amva.marie'}
190 line_debug(options,
'Using Marie aggregation-decomposition method');
191 [QN,UN,RN,TN,CN,XN,lG,runtime,lastiter,actualmethod] = solver_mva_marie_analyzer(sn, options);
193 if ~isempty(sn.lldscaling) || ~isempty(sn.cdscaling) || ~isempty(sn.jdscaling)
194 line_debug(options, 'Load-dependent scaling detected (lldscaling=%d, cdscaling=%d), routing to mvald_analyzer', ~isempty(sn.lldscaling), ~isempty(sn.cdscaling));
195 [QN,UN,RN,TN,CN,XN,lG,runtime,lastiter,actualmethod] = solver_mvald_analyzer(sn, options);
198 if self.model.hasFork && strcmp(subopts.method,'default')
199 % force AMVA for a forked default model; see
200 % _kb/06-solver-catalog.md (MVA section) for why
201 subopts.method = 'amva';
203 line_debug(options, 'Standard queueing network, routing to mva_analyzer (method=%s)', subopts.method);
204 [QN,UN,RN,TN,CN,XN,lG,runtime,lastiter,actualmethod] = solver_mva_analyzer(sn, subopts);
210out = struct('QN', QN, 'UN', UN, 'RN', RN, 'TN', TN, 'CN', CN, 'XN', XN, ...
211 'lG', lG, 'runtime', runtime, 'lastiter', lastiter, 'method', method, ...
213if exist('actualmethod','var') && ~isempty(actualmethod)
214 out.actualmethod = actualmethod;