LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
solver_mam_basic_mmap_inner.m
1function [QN,UN,RN,TN,CN,XN,totiter] = solver_mam_basic_mmap_inner(sn, options, lambda)
2% [QN,UN,RN,TN,CN,XN,TOTITER] = SOLVER_MAM_BASIC_MMAP_INNER(SN, OPTIONS, LAMBDA)
3%
4% MAM/MMAP fork-join decomposition algorithm parameterised by per-class arrival
5% rates LAMBDA. Performs the departure-process refinement loop only;
6% population enforcement (closed networks) is the wrapper's responsibility.
7%
8% Copyright (c) 2012-2026, Imperial College London
9% All rights reserved.
10
11config = options.config;
12if ~isfield(config, 'fj_sync_q_len')
13 config.fj_sync_q_len = 2;
14end
15if ~isfield(config, 'etaqa_trunc')
16 config.etaqa_trunc = 8;
17end
18
19PH = sn.proc;
20I = sn.nnodes;
21M = sn.nstations;
22K = sn.nclasses;
23V = cellsum(sn.visits);
24S = 1./sn.rates;
25
26QN = zeros(M,K);
27UN = zeros(M,K);
28RN = zeros(M,K);
29TN = zeros(M,K);
30CN = zeros(1,K);
31XN = zeros(1,K);
32
33% Build FJ synchronization map
34fjSyncMap = sn_build_fj_sync_map(sn);
35
36% Prepare PH service distributions
37pie = {};
38D0 = {};
39for ist=1:M
40 switch sn.sched(ist)
41 case SchedStrategy.EXT
42 TN(ist,:) = sn.rates(ist,:);
43 TN(ist,isnan(TN(ist,:))) = 0;
44 case {SchedStrategy.FCFS, SchedStrategy.HOL, SchedStrategy.FCFSPRPRIO}
45 for k=1:K
46 PH{ist}{k} = map_scale(PH{ist}{k}, map_mean(PH{ist}{k})/sn.nservers(ist));
47 pie{ist}{k} = map_pie(PH{ist}{k});
48 D0{ist,k} = PH{ist}{k}{1};
49 if any(isnan(D0{ist,k}))
50 D0{ist,k} = -GlobalConstants.Immediate;
51 pie{ist}{k} = 1;
52 PH{ist}{k} = map_exponential(GlobalConstants.Immediate);
53 end
54 end
55 case SchedStrategy.INF
56 for k=1:K
57 pie{ist}{k} = map_pie(PH{ist}{k});
58 D0{ist,k} = PH{ist}{k}{1};
59 if any(isnan(D0{ist,k}))
60 D0{ist,k} = -GlobalConstants.Immediate;
61 pie{ist}{k} = 1;
62 PH{ist}{k} = map_exponential(GlobalConstants.Immediate);
63 end
64 end
65 case SchedStrategy.PS
66 for k=1:K
67 PH{ist}{k} = map_scale(PH{ist}{k}, map_mean(PH{ist}{k})/sn.nservers(ist));
68 pie{ist}{k} = map_pie(PH{ist}{k});
69 D0{ist,k} = PH{ist}{k}{1};
70 if any(isnan(D0{ist,k}))
71 D0{ist,k} = -GlobalConstants.Immediate;
72 pie{ist}{k} = 1;
73 PH{ist}{k} = map_exponential(GlobalConstants.Immediate);
74 end
75 end
76 end
77end
78
79% departure-process fixed point (FJ parametric decomposition), driven on
80% the station queue lengths by the generic DA driver
81DEP = {};
82fpopts = options;
83fpopts.config.da_miniter = 3; % legacy loop tested convergence only from the third sweep
84fpopts.config.da_norm = @(xn,xr) max(abs(xn(:)-xr(:))./(xr(:)+GlobalConstants.FineTol)); % relative difference
85[~, totiter] = da_fpi(@mmap_dec_sweep, QN, fpopts);
86if options.verbose
87 line_printf('\nMAM FJ parametric decomposition completed in %d iterations.', totiter);
88end
89
90% Join: derive QN/RN from parallel branch means
91for joinIdx = find(sn.nodetype == NodeType.Join)'
92 joinStat = sn.nodeToStation(joinIdx);
93 if isnan(joinStat)
94 continue;
95 end
96 syncGroups = unique(fjSyncMap.nodeSync(joinIdx, :));
97 syncGroups = syncGroups(syncGroups > 0);
98 for r = 1:K
99 if TN(joinStat, r) <= 0
100 continue;
101 end
102 syncDelay = 0;
103 joinArrivalRate = 0;
104 for gid = syncGroups
105 branchNodes = find(fjSyncMap.nodeSync(joinIdx, :) == gid);
106 branchRt = zeros(1, numel(branchNodes));
107 branchTput = zeros(1, numel(branchNodes));
108 used = 0;
109 for b = 1:numel(branchNodes)
110 branchStat = sn.nodeToStation(branchNodes(b));
111 if isnan(branchStat) || RN(branchStat, r) <= 0
112 continue;
113 end
114 used = used + 1;
115 branchRt(used) = RN(branchStat, r);
116 branchTput(used) = TN(branchStat, r);
117 end
118 branchRt = branchRt(1:used);
119 branchTput = branchTput(1:used);
120 if numel(branchRt) < 2
121 continue;
122 end
123 lambdai = 1 ./ branchRt;
124 maxBranchRt = 0;
125 for pow = 0:(numel(branchRt) - 1)
126 maxBranchRt = maxBranchRt + (-1)^pow * sum(1 ./ sum(nchoosek(lambdai, pow + 1), 2));
127 end
128 syncDelay = syncDelay + max(maxBranchRt - mean(branchRt), 0);
129 joinArrivalRate = joinArrivalRate + sum(branchTput);
130 end
131 RN(joinStat, r) = syncDelay;
132 QN(joinStat, r) = joinArrivalRate * syncDelay;
133 UN(joinStat, r) = 0;
134 end
135end
136
137CN = sum(RN,1);
138QN(isnan(QN)) = 0;
139RN(isnan(RN)) = 0;
140UN(isnan(UN)) = 0;
141TN(isnan(TN)) = 0;
142
143 function [xnew, xref] = mmap_dec_sweep(~, itnum)
144 % Initialize departure processes (node-indexed: DEP{ind,r})
145 if itnum == 1
146 DEP = cell(I,K);
147 for ind=1:I
148 isForkJoin = (sn.nodetype(ind) == NodeType.Fork || sn.nodetype(ind) == NodeType.Join);
149 if sn.isstation(ind) && ~isForkJoin
150 ist = sn.nodeToStation(ind);
151 for r=1:K
152 if V(ist,r) > 0 && lambda(r) > 0
153 DEP{ind,r} = map_scale(PH{ist}{r}, 1 / (lambda(r) * V(ist,r)));
154 elseif sn.isslc(r)
155 % Self-looping classes are handled separately by the
156 % closed-network wrapper (their metrics are pinned at the
157 % reference station). They must not inject a saturating
158 % arrival stream into the shared-queue decomposition, so
159 % give them a vanishing-rate departure process.
160 DEP{ind,r} = map_exponential(1/GlobalConstants.Zero);
161 else
162 DEP{ind,r} = PH{ist}{r};
163 end
164 end
165 else
166 for r=1:K
167 if lambda(r) > 0
168 DEP{ind,r} = map_exponential(1/lambda(r));
169 else
170 DEP{ind,r} = map_exponential(1/GlobalConstants.Immediate);
171 end
172 end
173 end
174 end
175 end
176
177 % Compute arrival processes with FJ synchronization
178 ARV = solver_mam_traffic_mmap(sn, DEP, config, fjSyncMap);
179
180 xref = QN;
181 for ist=1:M
182 ind = sn.stationToNode(ist);
183 switch sn.nodetype(ind)
184 case NodeType.Join
185 for k=1:K
186 TN(ist,k) = lambda(k);
187 UN(ist,k) = 0;
188 QN(ist,k) = 0;
189 RN(ist,k) = 0;
190 end
191 case NodeType.Queue
192 if ~isempty(ARV{ind}) && iscell(ARV{ind})
193 if length(ARV{ind}{1}) > config.space_max
194 if options.verbose
195 line_printf('\nArrival process at node %d is now at %d states. Compressing.', ind, length(ARV{ind}{1}));
196 end
197 ARV{ind} = mmap_compress(ARV{ind}, config);
198 end
199
200 finiteCapUsed = false;
201 switch sn.sched(ist)
202 case {SchedStrategy.FCFS, SchedStrategy.HOL, SchedStrategy.FCFSPRPRIO}
203 isFiniteCap = isfinite(sn.cap(ist));
204 if isFiniteCap
205 capK = sn.cap(ist);
206 [isMmck, muMmck] = mam_detect_mmck(sn, ist, K, ARV{ind});
207 if isMmck
208 aggrLambda_ist = sum(mmap_lambda(ARV{ind}), 'omitnan');
209 exactRes = qsys_mmck(aggrLambda_ist, muMmck, sn.nservers(ist), capK);
210 meanQ_fc = exactRes.meanQueueLength;
211 lossProb_fc = exactRes.lossProbability;
212 else
213 [meanQ_fc, lossProb_fc, ~] = mam_truncate_renorm( ...
214 {ARV{ind}{[1,3:end]}}, {pie{ist}{:}}, {D0{ist,:}}, capK);
215 end
216 lambdaInflow = mmap_lambda(ARV{ind});
217 lambdaInflow(isnan(lambdaInflow)) = 0;
218 TN_eff = lambdaInflow * (1 - lossProb_fc);
219 sumTN = sum(TN_eff);
220 S_actual = zeros(1, K);
221 for k=1:K
222 S_actual(k) = map_mean(PH{ist}{k}) * sn.nservers(ist);
223 end
224 if sumTN > 0
225 Savg_eff = sum(TN_eff .* S_actual, 'omitnan') / sumTN;
226 Wq = max(0, meanQ_fc / sumTN - Savg_eff);
227 else
228 Wq = 0;
229 end
230 for k=1:K
231 TN(ist,k) = TN_eff(k);
232 UN(ist,k) = TN(ist,k) * map_mean(PH{ist}{k});
233 if TN(ist,k) > 0
234 RN(ist,k) = Wq + S_actual(k);
235 QN(ist,k) = TN(ist,k) * RN(ist,k);
236 else
237 RN(ist,k) = 0;
238 QN(ist,k) = 0;
239 end
240 end
241 finiteCapUsed = true;
242 else
243 rho_ist_classes = mmap_lambda(ARV{ind}) .* arrayfun(@(k) map_mean(PH{ist}{k}), 1:K);
244 rho_ist_classes(isnan(rho_ist_classes)) = 0;
245 % Exclude self-looping classes: they are pinned
246 % by the closed wrapper and must not drive the
247 % FCFS station into the saturation branch.
248 rho_ist = sum(rho_ist_classes(~sn.isslc));
249 if rho_ist < 1 - GlobalConstants.FineTol
250 % MMAPPH1FCFS models service as a renewal phase-type
251 % (marginal), discarding service-time autocorrelation.
252 % For the single-class single-server M/MAP/1 case with a
253 % genuinely correlated service process, use the exact
254 % MAP/MAP/1 queue (Q_CT_MAP_MAP_1) which carries the
255 % service phase across departures.
256 useMapMap1 = (K == 1) && (sn.nservers(ist) == 1) && ...
257 (abs(map_acf(PH{ist}{1}, 1)) > GlobalConstants.CoarseTol);
258 if useMapMap1
259 Carv0 = ARV{ind}{1};
260 Carv1 = ARV{ind}{3};
261 ql = Q_CT_MAP_MAP_1(Carv0, Carv1, PH{ist}{1}{1}, PH{ist}{1}{2}, 'MaxNumComp', 100000);
262 ql = ql(:);
263 QN(ist,1) = sum((0:numel(ql)-1)' .* ql);
264 else
265 [Qret{1:K}, ~] = MMAPPH1FCFS({ARV{ind}{[1,3:end]}}, {pie{ist}{:}}, {D0{ist,:}}, 'ncMoms', 1, 'ncDistr', 2);
266 for k=1:K
267 QN(ist,k) = sum(Qret{k});
268 end
269 end
270 else
271 % Saturation: bound queue lengths so the
272 % wrapper's bisection can recognise overload
273 % without NaNs propagating from MMAPPH1FCFS.
274 for k=1:K
275 if isfinite(sn.njobs(k))
276 QN(ist,k) = sn.njobs(k);
277 else
278 QN(ist,k) = 1/GlobalConstants.FineTol;
279 end
280 end
281 end
282 TN(ist,:) = mmap_lambda(ARV{ind});
283 end
284 case SchedStrategy.PS
285 TN(ist,:) = mmap_lambda(ARV{ind});
286 for k=1:K
287 UN(ist,k) = TN(ist,k) * S(ist,k);
288 end
289 % Self-looping classes are pinned by the closed
290 % wrapper and must not count toward the PS sharing
291 % denominator (a single permanent SLC job would
292 % otherwise drive the queue to saturation).
293 Uden = min([1-GlobalConstants.FineTol, sum(UN(ist,~sn.isslc))]);
294 for k=1:K
295 QN(ist,k) = UN(ist,k)/(1-Uden);
296 end
297 end
298
299 if ~finiteCapUsed
300 for k=1:K
301 UN(ist,k) = TN(ist,k) * map_mean(PH{ist}{k});
302 QN(ist,k) = QN(ist,k) + TN(ist,k)*(map_mean(PH{ist}{k})*sn.nservers(ist)) * (sn.nservers(ist)-1)/sn.nservers(ist);
303 RN(ist,k) = QN(ist,k) ./ TN(ist,k);
304 end
305 end
306 end
307 otherwise
308 switch sn.sched(ist)
309 case SchedStrategy.INF
310 if ~isempty(ARV{ind}) && iscell(ARV{ind})
311 TN(ist,:) = mmap_lambda(ARV{ind});
312 end
313 for k=1:K
314 if TN(ist,k) > 0
315 UN(ist,k) = S(ist,k)*TN(ist,k);
316 QN(ist,k) = TN(ist,k)*S(ist,k);
317 RN(ist,k) = S(ist,k);
318 end
319 end
320 case SchedStrategy.EXT
321 % Source: TN already set above
322 end
323 end
324 end
325
326 % Update departure processes
327 for ist=1:M
328 ind = sn.stationToNode(ist);
329 switch sn.nodetype(ind)
330 case NodeType.Queue
331 if ~isempty(ARV{ind}) && iscell(ARV{ind})
332 switch sn.sched(ist)
333 case {SchedStrategy.FCFS, SchedStrategy.HOL, SchedStrategy.FCFSPRPRIO}
334 for r=1:K
335 A = mmap_hide(ARV{ind}, setdiff(1:K,r));
336 Srv = PH{ist}{r};
337 na = length(A{1});
338 ns = length(Srv{1});
339 etaqa_n = config.etaqa_trunc;
340 etaqa_sz = (etaqa_n+1)*na*ns;
341 rho = sum(UN(ist,:));
342 if etaqa_sz <= config.space_max && rho < 1-GlobalConstants.FineTol
343 try
344 DEP{ind,r} = qbd_depproc_etaqa(A, Srv, etaqa_n);
345 DEP{ind,r} = map_normalize(DEP{ind,r});
346 catch
347 DEP{ind,r} = Srv;
348 end
349 else
350 DEP{ind,r} = Srv;
351 end
352 if V(ist,r) > 0 && lambda(r) > 0
353 DEP{ind,r} = map_scale(DEP{ind,r}, 1 / (lambda(r) * V(ist,r)));
354 end
355 end
356 case SchedStrategy.PS
357 for r=1:K
358 A = mmap_hide(ARV{ind}, setdiff(1:K,r));
359 Srv = PH{ist}{r};
360 na = length(A{1});
361 ns = length(Srv{1});
362 etaqa_n = config.etaqa_trunc;
363 etaqa_sz = (etaqa_n+1)*na*ns;
364 rho = sum(UN(ist,:));
365 if V(ist,r) > 0 && lambda(r) > 0
366 if etaqa_sz <= config.space_max && rho < 1-GlobalConstants.FineTol
367 try
368 DEP{ind,r} = qbd_depproc_etaqa_ps(A, Srv, etaqa_n);
369 DEP{ind,r} = map_normalize(DEP{ind,r});
370 catch
371 DEP{ind,r} = Srv;
372 end
373 else
374 DEP{ind,r} = Srv;
375 end
376 DEP{ind,r} = map_scale(DEP{ind,r}, 1 / (lambda(r) * V(ist,r)));
377 end
378 end
379 end
380 end
381 case NodeType.Join
382 for r=1:K
383 if TN(ist,r) > 0
384 DEP{ind,r} = map_exponential(1/TN(ist,r));
385 end
386 end
387 end
388 end
389 xnew = QN;
390 end
391end
Definition Station.m:245