1function [Q,U,R,T,C,X,lG,totiter,method] = solver_amva(sn,options)
2% [Q,U,R,T,C,X,lG,ITER] = SOLVER_AMVA(SN, OPTIONS)
4% Copyright (c) 2012-2026, Imperial College London
7 options = SolverMVA.defaultOptions;
10[Lchain,STchain,Vchain,alpha,Nchain,SCVchain,refstatchain] = sn_get_demands_chain(sn);
13if ~isfield(options.config,
'np_priority')
14 options.config.np_priority = 'default';
16if ~isfield(options.config,'multiserver')
17 options.config.multiserver = 'default';
19if ~isfield(options.config,'highvar')
20 options.config.highvar = 'default';
23% Apply hard cap on iter_max for stability (Python parity)
25options.iter_max = min(options.iter_max, max_iter_cap);
29 options.method = 'qli';
30 case {
'amva.qd',
'amva.qdamva',
'qdamva'}
31 options.method =
'qd';
33 options.method =
'aql';
35 options.method =
'qdaql';
37 options.method =
'lin';
39 options.method =
'qdlin';
41 options.method =
'fli';
43 options.method =
'bs';
45 options.method =
'ab';
47 options.method =
'schmidt';
48 case 'amva.schmidt-ext'
49 options.method =
'schmidt-ext';
50 case {
'default',
'amva'}
51 if (sum(Nchain)<=2 || any(Nchain<1))
52 options.method =
'qd'; % changing to bs degrades accuracy
54 if max(sn.nservers(isfinite(sn.nservers)))==1
55 options.method =
'egflin'; %
if single server model
57 options.method =
'lin'; % lin seems way worse than aql in test_LQN_8.xml
61method = options.method;
64if sn_has_homogeneous_scheduling(sn,SchedStrategy.INF)
65 options.config.multiserver = 'default';
66 [Q,U,R,T,C,X,lG,totiter] = solver_amvald(sn,Lchain,STchain,Vchain,alpha,Nchain,SCVchain,refstatchain,options);
70sourceIdx = sn.nodetype == NodeType.Source;
71queueIdx = sn.nodetype == NodeType.Queue;
72delayIdx = sn.nodetype == NodeType.Delay;
79for i=sn.nodeToStation(sourceIdx)
81 inchain = find(sn.chains(c,:));
82 if sum(sn.rates(sn.nodeToStation(sourceIdx),inchain), 'omitnan') > 0
90cond1 = sn_has_product_form_not_het_fcfs(sn);
91cond2 = ~sn_has_load_dependence(sn);
92cond3 = ~sn_has_open_classes(sn);
93if cond1 && cond2 && (cond3 || (sn_has_product_form(sn) && sn_has_open_classes(sn) && strcmpi(options.method,'lin')))
94 % we can use linearizer only if
the open model
is not heterfcfs as that approximation
is not supported, so strict product-form
is required
95 [lambda,L0,N,Z0,~,nservers,V(sn.nodeToStation(queueIdx|delayIdx),:)] = sn_get_product_form_chain_params(sn);
98 switch options.config.multiserver
99 case {
'default',
'seidmann'}
101 L = L ./ repmat(nservers(:),1,C);
102 for j=1:size(L,1) % move component of queue j to
the first delay
103 Z(1,:) = Z(1,:) + L0(j,:) .* (repmat(nservers(j),1,C) - 1)./ repmat(nservers(j),1,C);
106 [Q,U,R,T,C,X,lG,totiter] = solver_amvald(sn,Lchain,STchain,Vchain,alpha,Nchain,SCVchain,refstatchain,options);
112 % Warm-start queue lengths from a supplied initial solution (options.init_sol),
113 % aligned to
the algorithm
's queueing-station rows. The AMVA fixed point starts
114 % from Q0 instead of the default N/M guess, which sharply cuts iterations when
115 % the seed is near the fixed point (e.g., the previous SolverLN outer iterate).
117 if isfield(options,'init_sol
') && ~isempty(options.init_sol)
118 Qinit = options.init_sol;
119 stationsQ = sort(sn.nodeToStation(queueIdx));
120 stationsQ = stationsQ(stationsQ>0);
121 if size(Qinit,2) == C && size(Qinit,1) == sn.nstations && numel(stationsQ) == size(L,1)
122 Q0 = Qinit(stationsQ, :);
123 elseif isequal(size(Qinit), size(L))
126 if ~isempty(Q0) && (any(~isfinite(Q0(:))) || any(Q0(:) < 0))
127 Q0 = []; % reject malformed seed
131 switch options.method
132 case 'sqni
' % square root non-iterative approximation
133 if sn.nstations==2 && sum(sn.sched==SchedStrategy.INF)==1
134 [Q(sn.nodeToStation(queueIdx),:),U(sn.nodeToStation(queueIdx),:),X] = pfqn_sqni(N,L,Z);
136 [Q,U,R,T,C,X,lG,totiter,method] = deal([],[],[],[],[],[],[],0,options.method);
141 [X,Q(sn.nodeToStation(queueIdx),:),U(sn.nodeToStation(queueIdx),:),~,totiter] = pfqn_bs(L,N,Z,options.tol,options.iter_max,Q0,sn.sched(queueIdx));
143 if sn_has_multi_server(sn)
144 line_error(mfilename,'AQL cannot handle multi-server stations. Try with
the ''default'' or
''lin
'' methods.
');
146 [X,Q(sn.nodeToStation(queueIdx),:),U(sn.nodeToStation(queueIdx),:),~,totiter] = pfqn_aql(L,N,Z,options.tol,options.iter_max,Q0);
148 % Akyildiz-Bolch AMVA for multi-server networks
149 % Use L0/Z0 (original demands) since ab handles multi-server directly
150 % without needing Seidmann transformation
151 nDelays = sum(delayIdx); % Count actual delay stations, not size(Z)
153 nservers_full = [Inf*ones(nDelays,1); nservers];
155 V_full = [ones(nDelays,C); V(sn.nodeToStation(queueIdx),:)];
156 sched_full = [SchedStrategy.INF*ones(nDelays,1); sn.sched(sn.nodeToStation(queueIdx))];
158 nservers_full = nservers;
160 V_full = V(sn.nodeToStation(queueIdx),:);
161 sched_full = sn.sched(sn.nodeToStation(queueIdx));
163 [Q_tmp,U_tmp,~,~,X,totiter] = pfqn_ab_amva(D_full,N,V_full,nservers_full,sched_full,false,'ab
');
165 Q(sn.nodeToStation(delayIdx),:) = Q_tmp(1:nDelays,:);
166 U(sn.nodeToStation(delayIdx),:) = Q_tmp(1:nDelays,:); % delay utilization = queue length
168 Q(sn.nodeToStation(queueIdx),:) = Q_tmp(nDelays+1:end,:);
169 U(sn.nodeToStation(queueIdx),:) = U_tmp(nDelays+1:end,:);
171 % Schmidt's exact MVA
for class-dependent FCFS
172 % Use L0/Z0 (original demands) since schmidt handles multi-server directly
173 % without needing Seidmann transformation
174 nDelays = sum(delayIdx); % Count actual delay stations, not size(Z)
177 S_full = [Inf*ones(nDelays,1); nservers];
178 sched_full = [SchedStrategy.INF*ones(nDelays,1); sn.sched(sn.nodeToStation(queueIdx))];
179 V_full = [ones(nDelays,C); V(sn.nodeToStation(queueIdx),:)];
183 sched_full = sn.sched(sn.nodeToStation(queueIdx));
184 V_full = V(sn.nodeToStation(queueIdx),:);
186 [X_tmp,Q_tmp,~,~,~] = pfqn_schmidt(D_full,N,S_full,sched_full,V_full);
188 Q(sn.nodeToStation(delayIdx),:) = Q_tmp(1:nDelays,:);
189 U(sn.nodeToStation(delayIdx),:) = Q_tmp(1:nDelays,:); % delay utilization = queue length
191 Q(sn.nodeToStation(queueIdx),:) = Q_tmp(nDelays+1:end,:);
193 % Compute utilization from throughput: U = X * D / nservers
194 U(sn.nodeToStation(queueIdx),:) = repmat(X,size(L0,1),1) .* L0 ./ repmat(nservers,1,C);
197 % Extended Schmidt MVA with alpha corrections
198 % Use L0/Z0 (original demands) since schmidt-ext handles multi-server directly
199 % without needing Seidmann transformation
200 nDelays = sum(delayIdx); % Count actual delay stations, not size(Z)
203 S_full = [Inf*ones(nDelays,1); nservers];
204 sched_full = [SchedStrategy.INF*ones(nDelays,1); sn.sched(sn.nodeToStation(queueIdx))];
208 sched_full = sn.sched(sn.nodeToStation(queueIdx));
210 [X_tmp,Q_tmp,~,~,~] = pfqn_schmidt_ext(D_full,N,S_full,sched_full);
212 Q(sn.nodeToStation(delayIdx),:) = Q_tmp(1:nDelays,:);
213 U(sn.nodeToStation(delayIdx),:) = Q_tmp(1:nDelays,:); % delay utilization = queue length
215 Q(sn.nodeToStation(queueIdx),:) = Q_tmp(nDelays+1:end,:);
217 % Compute utilization from throughput: U = X * D / nservers
218 U(sn.nodeToStation(queueIdx),:) = repmat(X,size(L0,1),1) .* L0 ./ repmat(nservers,1,C);
220 case {
'lin',
'gflin',
'egflin'}
221 % For
class-dependent models, use solver_amvald which handles them
222 if ~isempty(sn.cdscaling)
223 [Q,U,R,T,C,X,lG,totiter] = solver_amvald(sn,Lchain,STchain,Vchain,alpha,Nchain,SCVchain,refstatchain,options);
225 elseif max(nservers)==1
226 % remove sources from L
227 [Q(sn.nodeToStation(queueIdx),:),U(sn.nodeToStation(queueIdx),:),~,~,X,totiter] = pfqn_linearizermx(lambda,L,N,Z,nservers,sn.sched(sn.nodeToStation(queueIdx)),options.tol,options.iter_max, options.method, Q0);
229 switch options.config.multiserver
231 [Q(sn.nodeToStation(queueIdx),:),U(sn.nodeToStation(queueIdx),:),~,~,X,totiter] = pfqn_conwayms(L,N,Z,nservers,sn.sched(queueIdx),options.tol,options.iter_max,Q0);
233 [Q(sn.nodeToStation(queueIdx),:),U(sn.nodeToStation(queueIdx),:),~,~,X,totiter] = pfqn_linearizermx(lambda,L,N,Z,nservers,sn.sched(sn.nodeToStation(queueIdx)),options.tol,options.iter_max, options.method, Q0);
234 case {
'default',
'softmin',
'seidmann',
'suri'}
235 [Q,U,R,T,C,X,lG,totiter] = solver_amvald(sn,Lchain,STchain,Vchain,alpha,Nchain,SCVchain,refstatchain,options);
240 switch options.config.multiserver
241 case {
'conway',
'erlang',
'krzesinski'}
242 options.config.multiserver =
'default';
244 [Q,U,R,T,C,X,lG,totiter] = solver_amvald(sn,Lchain,STchain,Vchain,alpha,Nchain,SCVchain,refstatchain,options);
248 % compute performance at delay, then unapply seidmann
if needed
250 % For ab/schmidt methods, Q
for delays was already set correctly by
the algorithm
251 %
using original demands Z0. For other methods, use Seidmann-modified Z.
252 if strcmp(options.method,
'ab') || startsWith(options.method,
'schmidt')
253 Q(sn.nodeToStation(delayIdx),:) = repmat(X,sum(delayIdx),1) .* Z0;
255 Q(sn.nodeToStation(delayIdx),:) = repmat(X,sum(delayIdx),1) .* Z;
257 U(sn.nodeToStation(delayIdx),:) = Q(sn.nodeToStation(delayIdx),:);
258 switch options.config.multiserver
259 case {
'default',
'seidmann'}
260 % Skip Seidmann un-apply
for ab and schmidt methods as it removes queue length
261 if ~strcmp(options.method,
'ab') && ~startsWith(options.method,
'schmidt')
263 if i == 1 && nservers(j)>1
264 % un-apply seidmann from first delay and move it to
266 jq = find(queueIdx,j);
267 Q(jq,:) = Q(jq,:) + (L0(j,:) .* (repmat(nservers(j),1,C) - 1)./ repmat(nservers(j),1,C)) .* X;
273 T = V .* repmat(X,M,1);
275 % For ab/schmidt methods, use original delay demands Z0
276 if strcmp(options.method,'ab') || startsWith(options.method,'schmidt')
282 if sn_has_class_switching(sn)
283 [Q,U,R,T,C,X] = sn_deaggregate_chain_results(sn, Lchain, [], STchain, Vchain, alpha, [], [], R, T, [], X);
286 switch options.config.multiserver
287 case {
'conway',
'erlang',
'krzesinski'}
288 options.config.multiserver =
'default';
290 [Q,U,R,T,C,X,lG,totiter] = solver_amvald(sn,Lchain,STchain,Vchain,alpha,Nchain,SCVchain,refstatchain,options);