LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
solver_mam_basic.m
1function [QN,UN,RN,TN,CN,XN,totiter] = solver_mam_basic(sn, options)
2% [Q,U,R,T,C,X] = SOLVER_MAM_BASIC(QN, PH, OPTIONS)
3
4% This solver uses MAM to solve queues in isolation, but simplifies the
5% traffic equations by using visits to rescale the flows into the queue
6% inputs.
7%
8% Copyright (c) 2012-2026, Imperial College London
9% All rights reserved.
10
11config = options.config;
12tol = options.tol;
13
14PH = sn.proc;
15%% generate local state spaces
16I = sn.nnodes;
17M = sn.nstations;
18K = sn.nclasses;
19C = sn.nchains;
20N = sn.njobs';
21V = cellsum(sn.visits);
22S = 1./sn.rates;
23Strue = S; % service times as declared, used to report utilization below
24Lchain = sn_get_demands_chain(sn);
25
26% SLC interference via inflated service time; see _kb/06-solver-catalog.md for rationale
27slcjobs = zeros(M,1);
28for k=1:K
29 if sn.isslc(k)
30 ist_k = sn.refstat(k);
31 if isfinite(sn.nservers(ist_k))
32 slcjobs(ist_k) = slcjobs(ist_k) + sn.njobs(k);
33 end
34 end
35end
36for ist=1:M
37 if slcjobs(ist) > 0
38 for k=1:K
39 if ~sn.isslc(k)
40 S(ist,k) = S(ist,k)*(1+slcjobs(ist));
41 end
42 end
43 % The chain demands drive the throughput fixed point and must be
44 % consistent with the inflated service times.
45 Lchain(ist,:) = Lchain(ist,:)*(1+slcjobs(ist));
46 end
47end
48
49QN = zeros(M,K);
50UN = zeros(M,K);
51RN = zeros(M,K);
52TN = zeros(M,K);
53CN = zeros(1,K);
54XN = zeros(1,K);
55
56% Track stations using exact MAP/D/c solver (skip post-processing for these)
57mapdcStations = false(M, 1);
58
59pie = {};
60D0 = {};
61
62lambda = zeros(1,C);
63chainSysArrivals = cell(1,C);
64TN_1 = TN+Inf;
65
66it = 0;
67
68% open queueing system (one node is the external world)
69% first build the joint arrival process
70for ist=1:M
71 switch sn.sched(ist)
72 case {SchedStrategy.FCFS, SchedStrategy.HOL, SchedStrategy.FCFSPRPRIO, SchedStrategy.PS}
73 for k=1:K
74 % Skip Det processes - they will be handled by qsys_mapdc
75 if sn.procid(ist, k) == ProcessType.DET
76 % For Det, we don't need MAP representation since we use exact solver
77 pie{ist}{k} = [];
78 D0{ist,k} = [];
79 continue;
80 end
81 % divide service time by nservers, surrogate delay in tandem;
82 % ME/RAP rescaled by rate alone, see _kb/06-solver-catalog.md for rationale
83 if sn.procid(ist, k) == ProcessType.ME || sn.procid(ist, k) == ProcessType.RAP
84 ratio = map_mean(PH{ist}{k}) / (S(ist,k)/sn.nservers(ist));
85 PH{ist}{k} = {PH{ist}{k}{1}*ratio, PH{ist}{k}{2}*ratio};
86 else
87 PH{ist}{k} = map_scale(PH{ist}{k}, S(ist,k)/sn.nservers(ist));
88 end
89 pie{ist}{k} = map_pie(PH{ist}{k});
90 D0{ist,k} = PH{ist}{k}{1};
91 if any(isnan(D0{ist,k}))
92 D0{ist,k} = -GlobalConstants.Immediate;
93 pie{ist}{k} = 1;
94 PH{ist}{k} = map_exponential(GlobalConstants.Immediate);
95 end
96 end
97 end
98end % i
99
100isOpen = false;
101isClosed = false;
102if any(isinf(sn.njobs))
103 isOpen = true;
104end
105if any(isfinite(sn.njobs))
106 isClosed = true;
107end
108isMixed = isOpen & isClosed;
109
110% SLC chains excluded from the throughput fixed point; see _kb/06-solver-catalog.md for rationale
111isslcchain = false(1,C);
112for c=1:C
113 isslcchain(c) = all(sn.isslc(sn.inchain{c}));
114end
115
116lambdas_inchain = cell(1,C);
117for c=1:C
118 inchain = sn.inchain{c};
119 lambdas_inchain{c} = sn.rates(sn.refstat(inchain(1)),inchain);
120 %lambdas_inchain{c} = lambdas_inchain{c}(isfinite(lambdas_inchain{c}));
121 lambda(c) = sum(lambdas_inchain{c}(isfinite(lambdas_inchain{c})));
122 if isslcchain(c)
123 lambda(c) = 0;
124 lambdas_inchain{c} = zeros(1,length(inchain));
125 end
126 ist = sn.refstat(inchain(1)); % identical for all classes in the chain
127 if isinf(sum(sn.njobs(inchain))) % if open chain
128 % ist here is the source
129 % assemble a MMAP for the arrival process from all classes
130 for k=1:K
131 if isnan(PH{ist}{k}{1})
132 PH{ist}{k} = map_exponential(Inf); % no arrivals from this class
133 end
134 end
135 inchain = sn.inchain{c};
136 k = inchain(1);
137 chainSysArrivals{c} = {PH{ist}{k}{1},PH{ist}{k}{2},PH{ist}{k}{2}};
138 for ki=2:length(inchain)
139 k = inchain(ki);
140 if isnan(PH{ist}{k}{1})
141 PH{ist}{k} = map_exponential(Inf); % no arrivals from this class
142 end
143 chainSysArrivals{c} = mmap_super_safe({chainSysArrivals{c},{PH{ist}{k}{1},PH{ist}{k}{2},PH{ist}{k}{2}}}, config.space_max, 'default');
144 end
145 TN(ist,inchain') = lambdas_inchain{c};
146 end
147end
148
149sd = isfinite(sn.nservers);
150
151isclosedchain = false(1,C);
152isopenchain = false(1,C);
153for c=1:C
154 isopenchain(c) = isinf(sum(sn.njobs(sn.inchain{c})));
155 isclosedchain(c) = ~isopenchain(c) && ~isslcchain(c);
156end
157% Mixed-network backoff differs from uniform 1/Umax; see _kb/06-solver-catalog.md for rationale
158ismixed = any(isclosedchain) && any(isopenchain);
159% Closed chains claim only the capacity open traffic leaves free; stay inside
160% the stability region (see _kb/06-solver-catalog.md for rationale)
161Ulim = 1 - GlobalConstants.CoarseTol;
162
163% Stations already flagged as carrying a non-phase-type service process, scoped
164% to this solve so the fixed-point iteration reports each station once while
165% every new model is still warned about.
166meWarned = false(1, M);
167
168while max(max(abs(TN-TN_1))) > tol && it <= options.iter_max %#ok<max>
169 it = it + 1;
170 TN_1 = TN;
171 Umax = max(sum(UN(sd,:),2));
172 if ismixed || Umax < 1
173 for c=1:C
174 inchain = sn.inchain{c};
175 if isclosedchain(c)
176 Nc = sum(sn.njobs(inchain)); % closed population
177 QNc = max(tol, sum(sum(QN(:,inchain),2,"omitnan"))); %#ok<NANSUM>
178 TNlb = Nc./sum(Lchain(:,c));
179 if it == 1
180 lambda(c) = TNlb; % lower bound
181 else
182 lambda(c) = lambda(c) * it/options.iter_max + (Nc / QNc) * lambda(c) * (options.iter_max-it)/options.iter_max; % iteration-averaged regula falsi;
183 end
184 end
185 end
186 end
187 if ismixed
188 % Back closed chains onto the busiest station's residual capacity;
189 % see _kb/06-solver-catalog.md for rationale
190 Uchain = Lchain(sd,:) .* lambda;
191 Uopen = sum(Uchain(:,~isclosedchain), 2);
192 Uclosed = sum(Uchain(:, isclosedchain), 2);
193 binding = Uclosed > tol;
194 if any(binding)
195 theta = min((Ulim - Uopen(binding)) ./ Uclosed(binding));
196 if theta < 1
197 lambda(isclosedchain) = lambda(isclosedchain) * max(0, theta);
198 end
199 end
200 elseif Umax >= 1
201 lambda = lambda * 1/Umax;
202 end
203
204 for c=1:C
205 inchain = sn.inchain{c};
206 if isslcchain(c)
207 % SLC vanishing-rate arrival surrogate; see _kb/06-solver-catalog.md for rationale
208 chainSysArrivals{c} = mmap_exponential(GlobalConstants.Zero);
209 elseif ~isinf(sum(sn.njobs(inchain)))
210 % Closed chain: Poisson surrogate at current throughput iterate;
211 % see _kb/06-solver-catalog.md for rationale
212 chainSysArrivals{c} = mmap_exponential(lambda(c));
213 end
214 for m=1:M
215 TN(m,inchain) = V(m,inchain) .* lambda(c);
216 end
217 end
218
219 for ind=1:I
220 if sn.isstation(ind)
221 ist = sn.nodeToStation(ind);
222 switch sn.nodetype(ind)
223 case NodeType.Join
224 for c=1:C
225 inchain = sn.inchain{c};
226 for k=inchain
227 fanin = nnz(sn.rtnodes(:, (ind-1)*K+k));
228 TN(ist,k) = lambda(c)*V(ist,k)/fanin;
229 UN(ist,k) = 0;
230 QN(ist,k) = 0;
231 RN(ist,k) = 0;
232 end
233 end
234 otherwise
235 switch sn.sched(ist)
236 case SchedStrategy.INF
237 for c=1:C
238 inchain = sn.inchain{c};
239 for k=inchain
240 if V(ist,k) == 0
241 % Non-visiting class NaN guard; see _kb/06-solver-catalog.md for rationale
242 TN(ist,k) = 0;
243 UN(ist,k) = 0;
244 QN(ist,k) = 0;
245 RN(ist,k) = 0;
246 continue;
247 end
248 TN(ist,k) = lambda(c)*V(ist,k);
249 % INF station: U = QLen = TN*S (no /c), single-V Little;
250 % see _kb/06-solver-catalog.md for rationale
251 UN(ist,k) = S(ist,k)*TN(ist,k);
252 QN(ist,k) = TN(ist,k).*S(ist,k);
253 RN(ist,k) = QN(ist,k)/TN(ist,k);
254 end
255 end
256 case SchedStrategy.PS
257 for c=1:C
258 inchain = sn.inchain{c};
259 for k=inchain
260 if V(ist,k) == 0
261 % Non-visiting class NaN guard; see _kb/06-solver-catalog.md for rationale
262 TN(ist,k) = 0;
263 UN(ist,k) = 0;
264 continue;
265 end
266 TN(ist,k) = lambda(c)*V(ist,k);
267 % Utilization Law: a c-server station holds TN*S/c of its capacity.
268 UN(ist,k) = S(ist,k)*TN(ist,k)/sn.nservers(ist);
269 end
270 %Nc = sum(sn.njobs(inchain)); % closed population
271 Uden = min([1-GlobalConstants.FineTol,sum(UN(ist,:))]);
272 for k=inchain
273 if V(ist,k) == 0
274 QN(ist,k) = 0;
275 RN(ist,k) = 0;
276 continue;
277 end
278 %QN(ist,k) = (UN(ist,k)-UN(ist,k)^(Nc+1))/(1-Uden); % geometric bound type approximation
279 QN(ist,k) = UN(ist,k)/(1-Uden);
280 RN(ist,k) = QN(ist,k)/TN(ist,k);
281 end
282 end
283 case {SchedStrategy.FCFS, SchedStrategy.HOL, SchedStrategy.FCFSPRPRIO}
284 chainArrivalAtNode = cell(1,C);
285 rates = cell(M,C);
286 mapdcUsed = false; % Flag for exact MAP/D/c solver
287 finiteCapUsed = false; % Flag for finite-capacity truncate-and-renormalize
288 finiteCapLossPerClass = []; % per-class loss, exact MMAP/G/1/K branch only
289 for c=1:C %for each chain
290 rates{ist,c} = V(ist,:) .* lambda(c); % visits of classes within the chain
291 inchain = find(sn.chains(c,:))';
292 markProb = rates{ist,c}(inchain) / sum(rates{ist,c}(inchain));
293 markProb(isnan(markProb)) = 0;
294 chainArrivalAtNode{c} = mmap_mark(chainSysArrivals{c}, markProb);
295 % Normalize only Markovian arrivals, not ME/RAP;
296 % see _kb/06-solver-catalog.md for rationale
297 if mam_chain_arrival_is_markovian(sn, c)
298 chainArrivalAtNode{c} = mmap_normalize(chainArrivalAtNode{c});
299 end
300 % mmap_scale targets marks 1:C; pass chain's own visit
301 % rates, skip zero-rate SLC; see _kb/06-solver-catalog.md for rationale
302 tgtrates = rates{ist,c}(inchain);
303 if any(tgtrates > 0)
304 chainArrivalAtNode{c} = mmap_scale(chainArrivalAtNode{c}, 1./tgtrates, 0); % non-iterative approximation
305 end
306 chainIsMarkovian = mam_chain_arrival_is_markovian(sn, c);
307 if c == 1
308 if chainIsMarkovian
309 aggrArrivalAtNode = mmap_super_safe({chainArrivalAtNode{c}, mmap_exponential(0,1)}, config.space_max, 'default');
310 aggrArrivalAtNode = {aggrArrivalAtNode{1} aggrArrivalAtNode{2} aggrArrivalAtNode{2}};
311 else
312 % ME/RAP: build {D0,D1,D1} shape directly (avoid
313 % mmap_super normalize); see _kb/06-solver-catalog.md for rationale
314 aggrArrivalAtNode = {chainArrivalAtNode{c}{1}, chainArrivalAtNode{c}{2}, chainArrivalAtNode{c}{2}};
315 end
316 lc = map_lambda(chainArrivalAtNode{c});
317 if lc>0
318 aggrArrivalAtNode = mmap_scale(aggrArrivalAtNode, 1/lc, 0); % non-iterative approximation
319 end
320 else
321 if ~chainIsMarkovian
322 % ME/RAP correlation only approximated under superposition;
323 % see _kb/06-solver-catalog.md for rationale
324 line_warning_always(mfilename, ...
325 'Chain %d has a matrix-exponential or rational arrival process, which the superposition of several chains at station %s cannot represent exactly. Its autocorrelation is approximated by the closest Markovian arrival process.', ...
326 c, sn.nodenames{sn.stationToNode(ist)});
327 end
328 aggrArrivalAtNode = mmap_super_safe({aggrArrivalAtNode, chainArrivalAtNode{c}}, config.space_max, 'default');
329 end
330 end
331 Qret = cell(1,K);
332 if (sn.sched(ist)==SchedStrategy.HOL && any(sn.classprio ~= sn.classprio(1))) % if priorities are not identical; sched holds numeric ids so == must be used (strcmp on numerics is always false)
333 [uK,iK] = unique(sn.classprio);
334 % BUTools convention: D1=lowest priority, DK=highest priority
335 % LINE convention: lower value = higher priority
336 % unique() returns ascending order, so we need to reverse for BUTools
337 iK = flipud(iK(:));
338 if length(uK) == length(sn.classprio) % if all priorities are different
339 [Qret{iK'}] = MMAPPH1NPPR({aggrArrivalAtNode{[1;2+iK]}}, {pie{ist}{iK}}, {D0{ist,iK}}, 'ncMoms', 1);
340 else
341 line_error(mfilename,'Solver MAM requires either identical priorities or all distinct priorities');
342 end
343 elseif (sn.sched(ist)==SchedStrategy.FCFSPRPRIO && any(sn.classprio ~= sn.classprio(1))) % FCFS preemptive resume priority
344 [uK,iK] = unique(sn.classprio);
345 % BUTools convention: D1=lowest priority, DK=highest priority
346 % LINE convention: lower value = higher priority
347 % unique() returns ascending order, so we need to reverse for BUTools
348 iK = flipud(iK(:));
349 if length(uK) == length(sn.classprio) % if all priorities are different
350 [Qret{iK'}] = MMAPPH1PRPR({aggrArrivalAtNode{[1;2+iK]}}, {pie{ist}{iK}}, {D0{ist,iK}}, 'ncMoms', 1);
351 else
352 line_error(mfilename,'Solver MAM requires either identical priorities or all distinct priorities');
353 end
354 else
355 aggrUtil = sum(mmap_lambda(aggrArrivalAtNode)./(GlobalConstants.FineTol+sn.rates(ist,1:K)*sn.nservers(ist)), 'omitnan'); %% to debug
356 aggrLambda = mmap_lambda(aggrArrivalAtNode);
357 if aggrUtil < 1-GlobalConstants.FineTol
358 if any(isinf(N))
359 % Check for MAP/D/c: single-class open model with deterministic service
360 isMapDc = (K == 1) && (sn.procid(ist, 1) == ProcessType.DET);
361 % Check for D/M/c: single-class open, exp service at this queue,
362 % deterministic source elsewhere.
363 isDMc = false;
364 dmcSourceIdx = -1;
365 if K == 1 && ~isMapDc && sn.procid(ist, 1) == ProcessType.EXP
366 for jst = 1:sn.nstations
367 if jst ~= ist && sn.procid(jst, 1) == ProcessType.DET
368 isDMc = true;
369 dmcSourceIdx = jst;
370 break;
371 end
372 end
373 end
374 % Check for PH/M/c: single-class open, exp service at this queue,
375 % source has PH inter-arrivals (Erlang/Coxian/MAP/PH/...).
376 % Covers both c=1 (PH/M/1) and c>1 (PH/M/c via matrix-geometric).
377 isPhM1 = false;
378 phM1SourceIdx = -1;
379 if K == 1 && ~isMapDc && ~isDMc && sn.procid(ist, 1) == ProcessType.EXP ...
380 && isfinite(sn.nservers(ist)) && sn.nservers(ist) >= 1
381 for jst = 1:sn.nstations
382 if jst == ist
383 continue;
384 end
385 srcProc = sn.procid(jst, 1);
386 if srcProc ~= ProcessType.EXP && srcProc ~= ProcessType.DET ...
387 && srcProc ~= ProcessType.IMMEDIATE && srcProc ~= ProcessType.DISABLED
388 % PH/M/c gate is RENEWAL, not non-exponential;
389 % see _kb/06-solver-catalog.md for rationale
390 if ~mam_srcproc_is_renewal(sn, jst)
391 continue;
392 end
393 isPhM1 = true;
394 phM1SourceIdx = jst;
395 break;
396 elseif srcProc == ProcessType.EXP && sn.nservers(ist) > 1 ...
397 && sn.nstations == 2 ...
398 && sn.nodetype(sn.stationToNode(jst)) == NodeType.Source
399 % M/M/c: the single-fast-server surrogate used by the
400 % generic path is inexact for c>1; dispatch to the exact
401 % PH/M/c matrix-geometric solver (Erlang-C result)
402 isPhM1 = true;
403 phM1SourceIdx = jst;
404 break;
405 end
406 end
407 end
408 % Finite buffer takes precedence over infinite-buffer
409 % closed forms; see _kb/06-solver-catalog.md for rationale
410 isFiniteCap = isfinite(sn.cap(ist));
411 if isFiniteCap
412 isPhM1 = false;
413 isDMc = false;
414 isMapDc = false;
415 end
416 if isPhM1
417 muQ = 1.0 / S(ist, 1);
418 try
419 phPair = sn.proc{phM1SourceIdx}{1};
420 D0_ph = phPair{1};
421 D1_ph = phPair{2};
422 pie_src = map_pie({D0_ph, D1_ph});
423 phm1Result = qsys_phmc(pie_src, D0_ph, muQ, sn.nservers(ist));
424 Qret{1} = phm1Result.meanQueueLength;
425 mapdcUsed = true;
426 mapdcStations(ist) = true;
427 line_debug(options, 'Using exact PH/M/%d solver: Q=%.4f, W=%.4f', ...
428 sn.nservers(ist), phm1Result.meanQueueLength, phm1Result.meanWaitingTime);
429 catch
430 isPhM1 = false;
431 end
432 end
433 if isDMc
434 muQ = 1.0 / S(ist, 1);
435 lamD = sn.rates(dmcSourceIdx, 1);
436 try
437 dmcResult = qsys_dmc(lamD, muQ, sn.nservers(ist));
438 Qret{1} = dmcResult.meanQueueLength;
439 mapdcUsed = true; % skip surrogate delay correction
440 mapdcStations(ist) = true;
441 line_debug(options, 'Using exact D/M/%d solver: Q=%.4f, W=%.4f', ...
442 sn.nservers(ist), dmcResult.meanQueueLength, dmcResult.meanWaitingTime);
443 catch
444 isDMc = false;
445 end
446 end
447 if isPhM1 || isDMc
448 % handled above; skip the MAP/D/c, finite-cap, and MMAPPH1FCFS branches
449 elseif isMapDc
450 % Use exact MAP/D/c solver from Q-MAM
451 D0_arr = aggrArrivalAtNode{1};
452 D1_arr = aggrArrivalAtNode{2};
453 detServiceTime = S(ist, 1); % 1/rate = service time
454 numServers = sn.nservers(ist);
455
456 mapdcResult = qsys_mapdc(D0_arr, D1_arr, detServiceTime, numServers);
457 Qret{1} = mapdcResult.meanQueueLength;
458 % Store result for later use to skip surrogate delay adjustment
459 mapdcUsed = true;
460 mapdcStations(ist) = true; % Mark for skipping post-processing
461 line_debug('Using exact MAP/D/%d solver: Q=%.4f, W=%.4f', ...
462 numServers, mapdcResult.meanQueueLength, mapdcResult.meanWaitingTime);
463 elseif isFiniteCap
464 % Finite-buffer FCFS. Detect exact M/M/c/K
465 % (Poisson arrivals + same exp service across
466 % classes); else use MMAP[K]/PH[K]/1/FCFS
467 % truncate-and-renormalize approximation.
468 mapdcUsed = false;
469 capK = sn.cap(ist);
470 finiteCapLossPerClass = [];
471 [isMmck, muMmck] = mam_detect_mmck(sn, ist, K, aggrArrivalAtNode);
472 if isMmck
473 aggrLambdaTotal = sum(aggrLambda, 'omitnan');
474 exactRes = qsys_mmck(aggrLambdaTotal, muMmck, sn.nservers(ist), capK);
475 finiteCapMeanQ = exactRes.meanQueueLength;
476 finiteCapLossProb = exactRes.lossProbability;
477 finiteCapP0 = exactRes.queueLengthDist(1);
478 line_debug('Using exact M/M/%d/%d: Q=%.4f, ploss=%.4f', ...
479 sn.nservers(ist), capK, finiteCapMeanQ, finiteCapLossProb);
480 elseif sn.nservers(ist) == 1
481 % Exact MMAP[K]/G/1/K with per-class loss ratio;
482 % see _kb/06-solver-catalog.md for rationale
483 svcMix = mam_svc_mixture({aggrArrivalAtNode{[1,3:end]}}, ...
484 {pie{ist}{:}}, {D0{ist,:}});
485 exRes = qsys_mmapg1k(aggrArrivalAtNode{1}, ...
486 aggrArrivalAtNode(3:end), svcMix, capK);
487 finiteCapMeanQ = exRes.meanQueueLength;
488 finiteCapLossProb = exRes.lossAggregate;
489 finiteCapLossPerClass = exRes.lossRatio;
490 finiteCapP0 = exRes.p0;
491 line_debug('Using exact MMAP/G/1/%d: Q=%.4f, ploss=%.4f, per-class ploss=%s', ...
492 capK, finiteCapMeanQ, finiteCapLossProb, mat2str(finiteCapLossPerClass, 4));
493 else
494 [meanQ_fc, lossProb_fc, p_norm_fc] = mam_truncate_renorm( ...
495 {aggrArrivalAtNode{[1,3:end]}}, {pie{ist}{:}}, {D0{ist,:}}, capK);
496 finiteCapMeanQ = meanQ_fc;
497 finiteCapLossProb = lossProb_fc;
498 finiteCapP0 = p_norm_fc(1);
499 line_debug('Using MMAP/PH/%d/%d truncate-renorm: Q=%.4f, ploss=%.4f', ...
500 sn.nservers(ist), capK, meanQ_fc, lossProb_fc);
501 end
502 finiteCapUsed = true;
503 for k=1:K
504 Qret{k} = NaN; % filled per class in result loop
505 end
506 mapdcStations(ist) = true; % skip surrogate delay 2nd pass
507 elseif sn.isfunction(ist)
508 % Open setup/delay-off via qbd_setupdelayoff; nodeparam
509 % is NODE-indexed; see _kb/06-solver-catalog.md for rationale
510 mapdcUsed = false;
511 alpharate = map_lambda(sn.nodeparam{sn.stationToNode(ist)}{end}.setupTime);
512 alphascv = map_scv(sn.nodeparam{sn.stationToNode(ist)}{end}.setupTime);
513 betarate = map_lambda(sn.nodeparam{sn.stationToNode(ist)}{end}.delayoffTime);
514 betascv = map_scv(sn.nodeparam{sn.stationToNode(ist)}{end}.delayoffTime);
515 mu_k = zeros(1, K);
516 lambda_k = zeros(1, K);
517 active_k = false(1, K);
518 for k=1:K
519 pie_k = pie{ist}{k};
520 if ~isempty(pie_k) && ~isnan(pie_k(1))
521 mu_k(k) = 1 / (pie_k * inv(-D0{ist,k}) * ones(size(pie_k))');
522 c = find(sn.chains(:,k), 1);
523 lambda_k(k) = rates{ist,c}(k);
524 active_k(k) = true;
525 end
526 end
527 rho_k = lambda_k ./ mu_k;
528 rho_k(~active_k) = 0;
529 rho_total = sum(rho_k);
530 if rho_total > 0
531 % Aggregate service rate, split queue by load;
532 % see _kb/06-solver-catalog.md for rationale
533 aggrLambdaTotal = sum(aggrLambda, 'omitnan');
534 aggrRate = aggrLambdaTotal / rho_total;
535 Q_total = qbd_setupdelayoff(aggrLambdaTotal, aggrRate, alpharate, alphascv, betarate, betascv);
536 for k=1:K
537 if active_k(k)
538 Qret{k} = Q_total * rho_k(k) / rho_total;
539 else
540 Qret{k} = 0;
541 end
542 end
543 else
544 for k=1:K
545 Qret{k} = 0;
546 end
547 end
548 else
549 mapdcUsed = false;
550 % MMAPPH1FCFS renewal marginal vs exact MAP/MAP/1 and
551 % RAP/RAP/1; see _kb/06-solver-catalog.md for rationale
552 isMEorRAPsvc = any(sn.procid(ist,:) == ProcessType.ME | ...
553 sn.procid(ist,:) == ProcessType.RAP);
554 useRapRap1 = false;
555 if isMEorRAPsvc
556 if (K == 1) && (sn.nservers(ist) == 1)
557 useRapRap1 = true;
558 else
559 % Multi-class/server ME falls back to MMAPPH1FCFS
560 % with a warning; see _kb/06-solver-catalog.md for rationale
561 if ~meWarned(ist)
562 meWarned(ist) = true;
563 line_warning_always(mfilename, ...
564 'Station %s has a matrix-exponential or rational service process, which the RAP/RAP/1 analysis supports only with a single class at a single server (here %d classes, %g servers). Falling back to the phase-type approximation MMAPPH1FCFS, which is not exact for this service process.', ...
565 sn.nodenames{sn.stationToNode(ist)}, K, sn.nservers(ist));
566 end
567 end
568 end
569 useMapMap1 = ~useRapRap1 && (K == 1) && (sn.nservers(ist) == 1) && ...
570 (abs(map_acf(PH{ist}{1}, 1)) > GlobalConstants.CoarseTol);
571 if useRapRap1
572 % qbd_raprap1 gives QLen only, RN via Little;
573 % see _kb/06-solver-catalog.md for rationale
574 [~, QNrap] = qbd_raprap1({aggrArrivalAtNode{1}, aggrArrivalAtNode{3}}, ...
575 {PH{ist}{1}{1}, PH{ist}{1}{2}});
576 Qret{1} = QNrap;
577 elseif useMapMap1
578 ql = Q_CT_MAP_MAP_1(aggrArrivalAtNode{1}, aggrArrivalAtNode{3}, ...
579 PH{ist}{1}{1}, PH{ist}{1}{2}, 'MaxNumComp', 100000);
580 ql = ql(:);
581 Qret{1} = sum((0:numel(ql)-1)' .* ql);
582 else
583 [Qret{1:K}] = MMAPPH1FCFS({aggrArrivalAtNode{[1,3:end]}}, {pie{ist}{:}}, {D0{ist,:}}, 'ncMoms', 1);
584 end
585 end
586 else % all closed classes
587 maxLevel = sum(N(isfinite(N)))+1;
588 D = {aggrArrivalAtNode{[1,3:end]}};
589 pdistr_k = cell(1,K);
590 if map_lambda(D)< GlobalConstants.FineTol
591 for k=1:K
592 pdistr_k = [1-GlobalConstants.FineTol, GlobalConstants.FineTol];
593 Qret{k} = GlobalConstants.FineTol / sn.rates(ist);
594 end
595 else
596 if sn.isfunction(ist)
597 alpharate = map_lambda(sn.nodeparam{sn.stationToNode(ist)}{end}.setupTime);
598 betarate = map_lambda(sn.nodeparam{sn.stationToNode(ist)}{end}.delayoffTime);
599 betascv = map_scv(sn.nodeparam{sn.stationToNode(ist)}{end}.delayoffTime);
600 lambda_k = zeros(1, K);
601 active_k = false(1, K);
602 for k=1:K
603 pie_k = pie{ist}{k};
604 if ~isnan(pie_k(1))
605 c = find(sn.chains(:,k), 1);
606 lambda_k(k) = rates{ist,c}(k);
607 active_k(k) = true;
608 end
609 end
610 if any(active_k)
611 % Closed setup/delay-off: per-instance cold-start race,
612 % p_cold = LST_delayoff(1/ZT); see _kb/06-solver-catalog.md for rationale
613 setupMean = 1/alpharate;
614 if betascv == 1.0
615 % rate taken directly, as in qbd_setupdelayoff
616 betaProc = {-betarate, betarate};
617 else
618 betaProc = APH.fitMeanAndSCV(1/betarate, betascv).getProcess;
619 end
620 Tb = betaProc{1};
621 pie_b = map_pie(betaProc);
622 infstat = isinf(sn.nservers);
623 for k=1:K
624 if active_k(k) && lambda_k(k) > 0 && isfinite(S(ist,k))
625 c = find(sn.chains(:,k), 1);
626 inchain_k = find(sn.chains(c,:));
627 % ZT = chain think demand per visit;
628 % see _kb/06-solver-catalog.md for rationale
629 Vtot = sum(V(ist, inchain_k));
630 ZT = sum(Lchain(infstat, c), 'omitnan') / max(Vtot, GlobalConstants.FineTol);
631 nu = 1 / max(ZT, GlobalConstants.FineTol);
632 pcold = pie_b * ((nu*eye(size(Tb,1)) - Tb) \ (-Tb*ones(size(Tb,1),1)));
633 % service part S/nservers, delay correction adds rest;
634 % see _kb/06-solver-catalog.md for rationale
635 Qret{k} = lambda_k(k) * (pcold*setupMean + S(ist,k)/sn.nservers(ist));
636 elseif active_k(k)
637 % Zero-load class holds no jobs (NaN guard);
638 % see _kb/06-solver-catalog.md for rationale
639 Qret{k} = 0;
640 else
641 % Inactive class holds no jobs (NaN guard);
642 % see _kb/06-solver-catalog.md for rationale
643 Qret{k} = 0;
644 end
645 end
646 else
647 for k=1:K
648 Qret{k} = NaN;
649 end
650 end
651 else
652 % Capture all K per-class distributions;
653 % see _kb/06-solver-catalog.md for rationale
654 pdistr_all = cell(1,K);
655 [pdistr_all{1:K}] = MMAPPH1FCFS(D, {pie{ist}{:}}, {D0{ist,:}}, 'ncDistr', maxLevel);
656 for k=1:K
657 pdistr = pdistr_all{k};
658 pdistr_k = abs(pdistr(1:(N(k)+1)));
659 % Truncate at N(k), complement over truncated vector;
660 % see _kb/06-solver-catalog.md for rationale
661 pdistr_k(end) = abs(1-sum(pdistr_k(1:end-1)));
662 pdistr_k = pdistr_k / sum(pdistr_k(1:(N(k)+1)));
663 Qret{k} = max(0,min(N(k),(0:N(k))*pdistr_k(1:(N(k)+1))'));
664 end
665 end
666 end
667 end
668 else
669 for k=1:K
670 Qret{k} = sn.njobs(k);
671 end
672 end
673 end
674 if finiteCapUsed
675 % Finite-cap per-class decomposition R_k = W_q + S_k;
676 % see _kb/06-solver-catalog.md for rationale
677 lambdaInflow = zeros(1, K);
678 for k=1:K
679 cidx = find(sn.chains(:,k),1);
680 lambdaInflow(k) = rates{ist,cidx}(k);
681 end
682 lambdaInflow(isnan(lambdaInflow)) = 0;
683 if ~isempty(finiteCapLossPerClass)
684 % Exact MMAP/G/1/K branch: loss differs by class
685 TN_eff = lambdaInflow .* (1 - finiteCapLossPerClass(1:K));
686 else
687 TN_eff = lambdaInflow * (1 - finiteCapLossProb);
688 end
689 sumTN = sum(TN_eff);
690 if sumTN > 0
691 Savg_eff = sum(TN_eff .* S(ist,1:K), 'omitnan') / sumTN;
692 Wq = max(0, finiteCapMeanQ / sumTN - Savg_eff);
693 else
694 Wq = 0;
695 end
696 for k=1:K
697 TN(ist,k) = TN_eff(k);
698 UN(ist,k) = TN(ist,k) * S(ist,k) / sn.nservers(ist);
699 if TN(ist,k) > 0
700 RN(ist,k) = Wq + S(ist,k);
701 QN(ist,k) = TN(ist,k) * RN(ist,k);
702 else
703 RN(ist,k) = 0;
704 QN(ist,k) = 0;
705 end
706 end
707 else
708 QN(ist,:) = cell2mat(Qret);
709 for k=1:K
710 c = find(sn.chains(:,k),1);
711 TN(ist,k) = rates{ist,c}(k);
712 UN(ist,k) = TN(ist,k) * S(ist,k) / sn.nservers(ist);
713 if sn.isfunction(ist) && ~isfinite(UN(ist,k))
714 % Unfreeze NaN-service util at setup station only;
715 % see _kb/06-solver-catalog.md for rationale
716 UN(ist,k) = 0;
717 end
718 QN(ist,k) = Qret{k};
719 if mapdcUsed
720 % For MAP/D/c, D/M/c, or PH/M/1, use exact results
721 % (no surrogate delay adjustment).
722 if isPhM1
723 RN(ist,k) = phm1Result.meanSojournTime;
724 elseif isDMc
725 RN(ist,k) = dmcResult.meanSojournTime;
726 else
727 RN(ist,k) = mapdcResult.meanSojournTime;
728 end
729 else
730 % Add surrogate-delay jobs, NaN-service guard;
731 % see _kb/06-solver-catalog.md for rationale
732 if isfinite(S(ist,k))
733 QN(ist,k) = QN(ist,k) + TN(ist,k)*S(ist,k) * (sn.nservers(ist)-1)/sn.nservers(ist);
734 end
735 RN(ist,k) = QN(ist,k) ./ TN(ist,k);
736 end
737 end
738 end
739 end
740 end
741 else % not a station
742 switch sn.nodetype(ind)
743 case NodeType.Fork
744 % line_error(mfilename,'Fork nodes not supported yet by MAM solver.');
745 end
746 end
747 end
748 %it
749 %max(max(abs(TN-TN_1)))
750end
751totiter = it + 2;
752CN = sum(RN,1);
753QN = abs(QN);
754for it=1:2 % second pass to rescale again QN based on RN correction
755 for c=1:C
756 inchain = sn.inchain{c};
757 Nc = sum(sn.njobs(inchain));
758 if isfinite(Nc)
759 QNc = sum(sum(QN(:,inchain)));
760 QN(:,inchain) = QN(:,inchain) * (Nc / QNc);
761 end
762 for ind=1:I
763 for k=inchain
764 if sn.isstation(ind)
765 ist = sn.nodeToStation(ind);
766 % Skip stations using exact MAP/D/c solver (already have exact values)
767 if mapdcStations(ist)
768 continue;
769 end
770 if V(ist,k)>0
771 if isinf(sn.nservers(ist))
772 RN(ist,k) = S(ist,k);
773 else
774 RN(ist,k) = max([S(ist,k), QN(ist,k) ./ TN(ist,k)]);
775 end
776 else
777 RN(ist,k) = 0;
778 end
779 QN(ist,k) = RN(ist,k) .* TN(ist,k);
780 end
781 end
782 end
783 Nc = sum(sn.njobs(inchain)); % closed population
784 if Nc == 0 % if closed chain
785 QN(:,c)=0;
786 UN(:,c)=0;
787 RN(:,c)=0;
788 TN(:,c)=0;
789 CN(c)=0;
790 XN(c)=0;
791 end
792 end
793end
794
795% SLC clamp: closed-form leftover-capacity U_slc = 1 - sum_j U_j, applied last;
796% see _kb/06-solver-catalog.md for rationale
797slcAll = find(sn.isslc);
798if ~isempty(slcAll)
799 % Non-SLC util from declared (uninflated) service time;
800 % see _kb/06-solver-catalog.md for rationale
801 for ist=1:M
802 if slcjobs(ist) > 0
803 for k=1:K
804 if ~sn.isslc(k)
805 UN(ist,k) = Strue(ist,k)*TN(ist,k);
806 end
807 end
808 end
809 end
810 QN(:,slcAll) = 0;
811 UN(:,slcAll) = 0;
812 RN(:,slcAll) = 0;
813 TN(:,slcAll) = 0;
814 for ist=1:M
815 slck = slcAll(sn.refstat(slcAll) == ist);
816 if isempty(slck)
817 continue;
818 end
819 if isinf(sn.nservers(ist))
820 % Delay station: no contention, every customer is always in
821 % service, so the class completes at its full aggregate rate.
822 for k=slck(:)'
823 QN(ist,k) = sn.njobs(k);
824 TN(ist,k) = sn.njobs(k)*sn.rates(ist,k);
825 RN(ist,k) = Strue(ist,k);
826 UN(ist,k) = Strue(ist,k)*TN(ist,k);
827 end
828 else
829 nsrv = sn.nservers(ist);
830 Uleft = max(0, 1 - sum(UN(ist,~sn.isslc)));
831 % Several self-looping classes at one station share the free
832 % capacity in proportion to the service rate they offer.
833 w = sn.njobs(slck(:)').*sn.rates(ist,slck(:)');
834 if sum(w) <= 0
835 continue;
836 end
837 for i=1:numel(slck)
838 k = slck(i);
839 UN(ist,k) = min(Uleft*w(i)/sum(w), min(sn.njobs(k),nsrv)/nsrv);
840 TN(ist,k) = sn.rates(ist,k)*UN(ist,k)*nsrv;
841 QN(ist,k) = sn.njobs(k);
842 if TN(ist,k) > 0
843 RN(ist,k) = QN(ist,k) ./ TN(ist,k);
844 else
845 RN(ist,k) = 0;
846 end
847 end
848 end
849 end
850 CN = sum(RN,1);
851end
852end
Definition fjtag.m:161