LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
solver_amva.m
1function [Q,U,R,T,C,X,lG,totiter,method,converged] = solver_amva(sn,options)
2% [Q,U,R,T,C,X,lG,ITER] = SOLVER_AMVA(SN, OPTIONS)
3%
4% Copyright (c) 2012-2026, Imperial College London
5% All rights reserved.
6if nargin < 2
7 options = SolverMVA.defaultOptions;
8end
9%% aggregate chains
10[Lchain,STchain,Vchain,alpha,Nchain,SCVchain,refstatchain] = sn_get_demands_chain(sn);
11
12%% check options
13if ~isfield(options.config,'np_priority')
14 options.config.np_priority = 'default';
15end
16if ~isfield(options.config,'multiserver')
17 options.config.multiserver = 'default';
18end
19if ~isfield(options.config,'highvar')
20 options.config.highvar = 'default';
21end
22
23% [] when the handler reports no flag (single-loop handlers, where the count is
24% sound); see _kb/06-solver-catalog.md (MVA section) for AMVA convergence
25converged = [];
26
27% Apply hard cap on iter_max for stability (Python parity)
28max_iter_cap = 10000;
29options.iter_max = min(options.iter_max, max_iter_cap);
30
31switch options.method
32 case 'amva.qli'
33 options.method = 'qli';
34 case {'amva.qd', 'amva.qdamva', 'qdamva'}
35 options.method = 'qd';
36 case 'amva.aql'
37 options.method = 'aql';
38 case 'amva.qdaql'
39 options.method = 'qdaql';
40 case 'amva.lin'
41 options.method = 'lin';
42 case 'amva.qdlin'
43 options.method = 'qdlin';
44 case 'amva.fli'
45 options.method = 'fli';
46 case 'amva.bs'
47 options.method = 'bs';
48 case 'amva.ab'
49 options.method = 'ab';
50 case 'amva.schmidt'
51 options.method = 'schmidt';
52 case 'amva.schmidt-ext'
53 options.method = 'schmidt-ext';
54 case {'default','amva'}
55 if (sum(Nchain)<=2 || any(Nchain<1))
56 options.method = 'qd'; % changing to bs degrades accuracy
57 else
58 if max(sn.nservers(isfinite(sn.nservers)))==1
59 options.method = 'egflin'; % if single server model
60 else
61 options.method = 'lin'; % lin seems way worse than aql in test_LQN_8.xml
62 end
63 end
64end
65method = options.method;
66
67%% trivial models
68if sn_has_homogeneous_scheduling(sn,SchedStrategy.INF)
69 options.config.multiserver = 'default';
70 [Q,U,R,T,C,X,lG,totiter,converged] = solver_amvald(sn,Lchain,STchain,Vchain,alpha,Nchain,SCVchain,refstatchain,options);
71 return
72end
73
74sourceIdx = sn.nodetype == NodeType.Source;
75queueIdx = sn.nodetype == NodeType.Queue;
76delayIdx = sn.nodetype == NodeType.Delay;
77
78%% run amva method
79M = sn.nstations;
80%K = sn.nclasses;
81C = sn.nchains;
82V = zeros(M,C);
83for i=sn.nodeToStation(sourceIdx)
84 for c=1:sn.nchains
85 inchain = find(sn.chains(c,:));
86 if sum(sn.rates(sn.nodeToStation(sourceIdx),inchain), 'omitnan') > 0
87 V(i,c) = 1;
88 end
89 end
90end
91Q = zeros(M,C);
92U = zeros(M,C);
93
94cond1 = sn_has_product_form_not_het_fcfs(sn);
95cond2 = ~sn_has_load_dependence(sn);
96cond3 = ~sn_has_open_classes(sn);
97if cond1 && cond2 && (cond3 || (sn_has_product_form(sn) && sn_has_open_classes(sn) && strcmpi(options.method,'lin')))
98 % we can use linearizer only if the open model is not heterfcfs as that approximation is not supported, so strict product-form is required
99 [lambda,L0,N,Z0,~,nservers,V(sn.nodeToStation(queueIdx|delayIdx),:)] = sn_get_product_form_chain_params(sn);
100 L = L0;
101 Z = Z0;
102 switch options.config.multiserver
103 case {'default','seidmann'}
104 % apply seidmann
105 L = L ./ repmat(nservers(:),1,C);
106 for j=1:size(L,1) % move component of queue j to the first delay
107 Z(1,:) = Z(1,:) + L0(j,:) .* (repmat(nservers(j),1,C) - 1)./ repmat(nservers(j),1,C);
108 end
109 case 'softmin'
110 [Q,U,R,T,C,X,lG,totiter,converged] = solver_amvald(sn,Lchain,STchain,Vchain,alpha,Nchain,SCVchain,refstatchain,options);
111 return
112 otherwise
113 %no-op
114 end
115
116 % Warm-start queue lengths from a supplied initial solution (options.init_sol),
117 % aligned to the algorithm's queueing-station rows. The AMVA fixed point starts
118 % from Q0 instead of the default N/M guess, which sharply cuts iterations when
119 % the seed is near the fixed point (e.g., the previous SolverLN outer iterate).
120 Q0 = [];
121 if isfield(options,'init_sol') && ~isempty(options.init_sol)
122 Qinit = options.init_sol;
123 stationsQ = sort(sn.nodeToStation(queueIdx));
124 stationsQ = stationsQ(stationsQ>0);
125 if size(Qinit,2) == C && size(Qinit,1) == sn.nstations && numel(stationsQ) == size(L,1)
126 Q0 = Qinit(stationsQ, :);
127 elseif isequal(size(Qinit), size(L))
128 Q0 = Qinit;
129 end
130 if ~isempty(Q0) && (any(~isfinite(Q0(:))) || any(Q0(:) < 0))
131 Q0 = []; % reject malformed seed
132 end
133 end
134
135 switch options.method
136 case 'sqni' % square root non-iterative approximation
137 if sn.nstations==2 && sum(sn.sched==SchedStrategy.INF)==1
138 [Q(sn.nodeToStation(queueIdx),:),U(sn.nodeToStation(queueIdx),:),X] = pfqn_sqni(N,L,Z);
139 else
140 [Q,U,R,T,C,X,lG,totiter,method] = deal([],[],[],[],[],[],[],0,options.method);
141 return
142 end
143 totiter=1;
144 case 'bs'
145 [X,Q(sn.nodeToStation(queueIdx),:),U(sn.nodeToStation(queueIdx),:),~,totiter] = pfqn_bs(L,N,Z,options.tol,options.iter_max,Q0,sn.sched(queueIdx));
146 case 'aql'
147 if sn_has_multi_server(sn)
148 line_error(mfilename,'AQL cannot handle multi-server stations. Try with the ''default'' or ''lin'' methods.');
149 end
150 [X,Q(sn.nodeToStation(queueIdx),:),U(sn.nodeToStation(queueIdx),:),~,totiter] = pfqn_aql(L,N,Z,options.tol,options.iter_max,Q0);
151 case 'ab'
152 % Akyildiz-Bolch AMVA for multi-server networks
153 % Use L0/Z0 (original demands) since ab handles multi-server directly
154 % without needing Seidmann transformation
155 nDelays = sum(delayIdx); % Count actual delay stations, not size(Z)
156 if nDelays > 0
157 nservers_full = [Inf*ones(nDelays,1); nservers];
158 D_full = [Z0; L0];
159 V_full = [ones(nDelays,C); V(sn.nodeToStation(queueIdx),:)];
160 sched_full = [SchedStrategy.INF*ones(nDelays,1); sn.sched(sn.nodeToStation(queueIdx))];
161 else
162 nservers_full = nservers;
163 D_full = L0;
164 V_full = V(sn.nodeToStation(queueIdx),:);
165 sched_full = sn.sched(sn.nodeToStation(queueIdx));
166 end
167 [Q_tmp,U_tmp,~,~,X,totiter] = pfqn_ab_amva(D_full,N,V_full,nservers_full,sched_full,false,'ab');
168 if nDelays > 0
169 Q(sn.nodeToStation(delayIdx),:) = Q_tmp(1:nDelays,:);
170 U(sn.nodeToStation(delayIdx),:) = Q_tmp(1:nDelays,:); % delay utilization = queue length
171 end
172 Q(sn.nodeToStation(queueIdx),:) = Q_tmp(nDelays+1:end,:);
173 U(sn.nodeToStation(queueIdx),:) = U_tmp(nDelays+1:end,:);
174 case 'schmidt'
175 % Schmidt's exact MVA for class-dependent FCFS
176 % Use L0/Z0 (original demands) since schmidt handles multi-server directly
177 % without needing Seidmann transformation
178 nDelays = sum(delayIdx); % Count actual delay stations, not size(Z)
179 if nDelays > 0
180 D_full = [Z0; L0];
181 S_full = [Inf*ones(nDelays,1); nservers];
182 sched_full = [SchedStrategy.INF*ones(nDelays,1); sn.sched(sn.nodeToStation(queueIdx))];
183 V_full = [ones(nDelays,C); V(sn.nodeToStation(queueIdx),:)];
184 else
185 D_full = L0;
186 S_full = nservers;
187 sched_full = sn.sched(sn.nodeToStation(queueIdx));
188 V_full = V(sn.nodeToStation(queueIdx),:);
189 end
190 [X_tmp,Q_tmp,~,~,~] = pfqn_schmidt(D_full,N,S_full,sched_full,V_full);
191 if nDelays > 0
192 Q(sn.nodeToStation(delayIdx),:) = Q_tmp(1:nDelays,:);
193 U(sn.nodeToStation(delayIdx),:) = Q_tmp(1:nDelays,:); % delay utilization = queue length
194 end
195 Q(sn.nodeToStation(queueIdx),:) = Q_tmp(nDelays+1:end,:);
196 X = X_tmp(1,:);
197 % Compute utilization from throughput: U = X * D / nservers
198 U(sn.nodeToStation(queueIdx),:) = repmat(X,size(L0,1),1) .* L0 ./ repmat(nservers,1,C);
199 totiter = 1;
200 case 'schmidt-ext'
201 % Extended Schmidt MVA with alpha corrections
202 % Use L0/Z0 (original demands) since schmidt-ext handles multi-server directly
203 % without needing Seidmann transformation
204 nDelays = sum(delayIdx); % Count actual delay stations, not size(Z)
205 if nDelays > 0
206 D_full = [Z0; L0];
207 S_full = [Inf*ones(nDelays,1); nservers];
208 sched_full = [SchedStrategy.INF*ones(nDelays,1); sn.sched(sn.nodeToStation(queueIdx))];
209 else
210 D_full = L0;
211 S_full = nservers;
212 sched_full = sn.sched(sn.nodeToStation(queueIdx));
213 end
214 [X_tmp,Q_tmp,~,~,~] = pfqn_schmidt_ext(D_full,N,S_full,sched_full);
215 if nDelays > 0
216 Q(sn.nodeToStation(delayIdx),:) = Q_tmp(1:nDelays,:);
217 U(sn.nodeToStation(delayIdx),:) = Q_tmp(1:nDelays,:); % delay utilization = queue length
218 end
219 Q(sn.nodeToStation(queueIdx),:) = Q_tmp(nDelays+1:end,:);
220 X = X_tmp(1,:);
221 % Compute utilization from throughput: U = X * D / nservers
222 U(sn.nodeToStation(queueIdx),:) = repmat(X,size(L0,1),1) .* L0 ./ repmat(nservers,1,C);
223 totiter = 1;
224 case {'lin','gflin','egflin'}
225 % For class-dependent models, use solver_amvald which handles them
226 if ~isempty(sn.cdscaling) || ~isempty(sn.jdscaling)
227 [Q,U,R,T,C,X,lG,totiter,converged] = solver_amvald(sn,Lchain,STchain,Vchain,alpha,Nchain,SCVchain,refstatchain,options);
228 return
229 elseif max(nservers)==1
230 % remove sources from L
231 [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);
232 else
233 switch options.config.multiserver
234 case 'conway'
235 [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);
236 case 'krzesinski'
237 [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);
238 case {'default', 'softmin', 'seidmann', 'suri'}
239 [Q,U,R,T,C,X,lG,totiter,converged] = solver_amvald(sn,Lchain,STchain,Vchain,alpha,Nchain,SCVchain,refstatchain,options);
240 return
241 end
242 end
243 otherwise
244 switch options.config.multiserver
245 case {'conway','erlang','krzesinski'}
246 options.config.multiserver = 'default';
247 end
248 [Q,U,R,T,C,X,lG,totiter,converged] = solver_amvald(sn,Lchain,STchain,Vchain,alpha,Nchain,SCVchain,refstatchain,options);
249 return
250 end
251
252 % compute performance at delay, then unapply seidmann if needed
253 for i=1:size(Z0,1)
254 % For ab/schmidt methods, Q for delays was already set correctly by the algorithm
255 % using original demands Z0. For other methods, use Seidmann-modified Z.
256 if strcmp(options.method,'ab') || startsWith(options.method,'schmidt')
257 Q(sn.nodeToStation(delayIdx),:) = repmat(X,sum(delayIdx),1) .* Z0;
258 else
259 Q(sn.nodeToStation(delayIdx),:) = repmat(X,sum(delayIdx),1) .* Z;
260 end
261 U(sn.nodeToStation(delayIdx),:) = Q(sn.nodeToStation(delayIdx),:);
262 switch options.config.multiserver
263 case {'default','seidmann'}
264 % Skip Seidmann un-apply for ab and schmidt methods as it removes queue length
265 if ~strcmp(options.method,'ab') && ~startsWith(options.method,'schmidt')
266 for j=1:size(L,1)
267 if i == 1 && nservers(j)>1
268 % un-apply seidmann from first delay and move it to
269 % the origin queue
270 jq = find(queueIdx,j);
271 Q(jq,:) = Q(jq,:) + (L0(j,:) .* (repmat(nservers(j),1,C) - 1)./ repmat(nservers(j),1,C)) .* X;
272 end
273 end
274 end
275 end
276 end
277 T = V .* repmat(X,M,1);
278 R = Q ./ T;
279 % For ab/schmidt methods, use original delay demands Z0
280 if strcmp(options.method,'ab') || startsWith(options.method,'schmidt')
281 C = N ./ X - Z0;
282 else
283 C = N ./ X - Z;
284 end
285 lG = NaN;
286 if sn_has_class_switching(sn)
287 [Q,U,R,T,C,X] = sn_deaggregate_chain_results(sn, Lchain, [], STchain, Vchain, alpha, [], [], R, T, [], X);
288 end
289else
290 switch options.config.multiserver
291 case {'conway','erlang','krzesinski'}
292 options.config.multiserver = 'default';
293 end
294 [Q,U,R,T,C,X,lG,totiter,converged] = solver_amvald(sn,Lchain,STchain,Vchain,alpha,Nchain,SCVchain,refstatchain,options);
295end
296end
297