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)
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
8% Copyright (c) 2012-2026, Imperial College London
11config = options.config;
15%% generate local state spaces
21V = cellsum(sn.visits);
23Strue = S; % service times as declared, used to report utilization below
24Lchain = sn_get_demands_chain(sn);
26% SLC interference via inflated service time; see _kb/06-solver-catalog.md for rationale
30 ist_k = sn.refstat(k);
31 if isfinite(sn.nservers(ist_k))
32 slcjobs(ist_k) = slcjobs(ist_k) + sn.njobs(k);
40 S(ist,k) = S(ist,k)*(1+slcjobs(ist));
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));
56% Track stations using exact MAP/D/c solver (skip post-processing for these)
57mapdcStations = false(M, 1);
63chainSysArrivals = cell(1,C);
68% open queueing system (one node is the external world)
69% first build the joint arrival process
72 case {SchedStrategy.FCFS, SchedStrategy.HOL, SchedStrategy.FCFSPRPRIO, SchedStrategy.PS}
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
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};
87 PH{ist}{k} = map_scale(PH{ist}{k}, S(ist,k)/sn.nservers(ist));
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;
94 PH{ist}{k} = map_exponential(GlobalConstants.Immediate);
102if any(isinf(sn.njobs))
105if any(isfinite(sn.njobs))
108isMixed = isOpen & isClosed;
110% SLC chains excluded from the throughput fixed point; see _kb/06-solver-catalog.md for rationale
111isslcchain = false(1,C);
113 isslcchain(c) = all(sn.isslc(sn.inchain{c}));
116lambdas_inchain = cell(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})));
124 lambdas_inchain{c} = zeros(1,length(inchain));
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
131 if isnan(PH{ist}{k}{1})
132 PH{ist}{k} = map_exponential(Inf); % no arrivals from
this class
135 inchain = sn.inchain{c};
138 for ki=2:length(inchain)
140 if isnan(PH{ist}{k}{1})
141 PH{ist}{k} = map_exponential(Inf); % no arrivals from
this class
145 TN(ist,inchain
') = lambdas_inchain{c};
149sd = isfinite(sn.nservers);
151isclosedchain = false(1,C);
152isopenchain = false(1,C);
154 isopenchain(c) = isinf(sum(sn.njobs(sn.inchain{c})));
155 isclosedchain(c) = ~isopenchain(c) && ~isslcchain(c);
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;
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);
168while max(max(abs(TN-TN_1))) > tol && it <= options.iter_max %#ok<max>
171 Umax = max(sum(UN(sd,:),2));
172 if ismixed || Umax < 1
174 inchain = sn.inchain{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));
180 lambda(c) = TNlb; % lower bound
182 lambda(c) = lambda(c) * it/options.iter_max + (Nc / QNc) * lambda(c) * (options.iter_max-it)/options.iter_max; % iteration-averaged regula falsi;
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);
197 lambda(isclosedchain) = lambda(isclosedchain) * max(0, theta);
201 lambda = lambda * 1/Umax;
205 inchain = sn.inchain{c};
207 % SLC vanishing-rate arrival surrogate; see _kb/06-solver-catalog.md
for rationale
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
215 TN(m,inchain) = V(m,inchain) .* lambda(c);
221 ist = sn.nodeToStation(ind);
222 switch sn.nodetype(ind)
225 inchain = sn.inchain{c};
227 fanin = nnz(sn.rtnodes(:, (ind-1)*K+k));
228 TN(ist,k) = lambda(c)*V(ist,k)/fanin;
236 case SchedStrategy.INF
238 inchain = sn.inchain{c};
241 % Non-visiting
class NaN guard; see _kb/06-solver-catalog.md
for rationale
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);
256 case SchedStrategy.PS
258 inchain = sn.inchain{c};
261 % Non-visiting
class NaN guard; see _kb/06-solver-catalog.md
for rationale
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);
270 %Nc = sum(sn.njobs(inchain)); % closed population
271 Uden = min([1-GlobalConstants.FineTol,sum(UN(ist,:))]);
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);
283 case {SchedStrategy.FCFS, SchedStrategy.HOL, SchedStrategy.FCFSPRPRIO}
284 chainArrivalAtNode = cell(1,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});
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);
304 chainArrivalAtNode{c} = mmap_scale(chainArrivalAtNode{c}, 1./tgtrates, 0); % non-iterative approximation
306 chainIsMarkovian = mam_chain_arrival_is_markovian(sn, c);
309 aggrArrivalAtNode = mmap_super_safe({chainArrivalAtNode{c}, mmap_exponential(0,1)}, config.space_max,
'default');
310 aggrArrivalAtNode = {aggrArrivalAtNode{1} aggrArrivalAtNode{2} aggrArrivalAtNode{2}};
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}};
316 lc = map_lambda(chainArrivalAtNode{c});
318 aggrArrivalAtNode = mmap_scale(aggrArrivalAtNode, 1/lc, 0); % non-iterative approximation
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)});
328 aggrArrivalAtNode = mmap_super_safe({aggrArrivalAtNode, chainArrivalAtNode{c}}, config.space_max,
'default');
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
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);
341 line_error(mfilename,'Solver MAM
requires either identical priorities or all distinct priorities
');
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
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);
352 line_error(mfilename,
'Solver MAM requires either identical priorities or all distinct priorities');
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
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.
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
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).
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
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)
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)
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));
417 muQ = 1.0 / S(ist, 1);
419 phPair = sn.proc{phM1SourceIdx}{1};
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;
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);
434 muQ = 1.0 / S(ist, 1);
435 lamD = sn.rates(dmcSourceIdx, 1);
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);
448 % handled above; skip the MAP/D/c, finite-cap, and MMAPPH1FCFS branches
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);
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
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);
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.
470 finiteCapLossPerClass = [];
471 [isMmck, muMmck] = mam_detect_mmck(sn, ist, K, aggrArrivalAtNode);
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));
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);
502 finiteCapUsed =
true;
504 Qret{k} = NaN; % filled per
class in result loop
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
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);
516 lambda_k = zeros(1, K);
517 active_k =
false(1, 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);
527 rho_k = lambda_k ./ mu_k;
528 rho_k(~active_k) = 0;
529 rho_total = sum(rho_k);
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);
538 Qret{k} = Q_total * rho_k(k) / rho_total;
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);
556 if (K == 1) && (sn.nservers(ist) == 1)
559 % Multi-class/server ME falls back to MMAPPH1FCFS
560 % with a warning; see _kb/06-solver-catalog.md for rationale
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));
569 useMapMap1 = ~useRapRap1 && (K == 1) && (sn.nservers(ist) == 1) && ...
570 (abs(map_acf(PH{ist}{1}, 1)) > GlobalConstants.CoarseTol);
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}});
578 ql = Q_CT_MAP_MAP_1(aggrArrivalAtNode{1}, aggrArrivalAtNode{3}, ...
579 PH{ist}{1}{1}, PH{ist}{1}{2}, 'MaxNumComp
', 100000);
581 Qret{1} = sum((0:numel(ql)-1)' .* ql);
583 [Qret{1:K}] = MMAPPH1FCFS({aggrArrivalAtNode{[1,3:end]}}, {pie{ist}{:}}, {D0{ist,:}},
'ncMoms', 1);
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
592 pdistr_k = [1-GlobalConstants.FineTol, GlobalConstants.FineTol];
593 Qret{k} = GlobalConstants.FineTol / sn.rates(ist);
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);
605 c = find(sn.chains(:,k), 1);
606 lambda_k(k) = rates{ist,c}(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;
615 % rate taken directly, as in qbd_setupdelayoff
616 betaProc = {-betarate, betarate};
618 betaProc = APH.fitMeanAndSCV(1/betarate, betascv).getProcess;
621 pie_b = map_pie(betaProc);
622 infstat = isinf(sn.nservers);
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));
637 % Zero-load
class holds no jobs (NaN guard);
638 % see _kb/06-solver-catalog.md
for rationale
641 % Inactive
class holds no jobs (NaN guard);
642 % see _kb/06-solver-catalog.md
for rationale
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);
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))
'));
670 Qret{k} = sn.njobs(k);
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);
679 cidx = find(sn.chains(:,k),1);
680 lambdaInflow(k) = rates{ist,cidx}(k);
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));
687 TN_eff = lambdaInflow * (1 - finiteCapLossProb);
691 Savg_eff = sum(TN_eff .* S(ist,1:K), 'omitnan
') / sumTN;
692 Wq = max(0, finiteCapMeanQ / sumTN - Savg_eff);
697 TN(ist,k) = TN_eff(k);
698 UN(ist,k) = TN(ist,k) * S(ist,k) / sn.nservers(ist);
700 RN(ist,k) = Wq + S(ist,k);
701 QN(ist,k) = TN(ist,k) * RN(ist,k);
708 QN(ist,:) = cell2mat(Qret);
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
720 % For MAP/D/c, D/M/c, or PH/M/1, use exact results
721 % (no surrogate delay adjustment).
723 RN(ist,k) = phm1Result.meanSojournTime;
725 RN(ist,k) = dmcResult.meanSojournTime;
727 RN(ist,k) = mapdcResult.meanSojournTime;
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);
735 RN(ist,k) = QN(ist,k) ./ TN(ist,k);
742 switch sn.nodetype(ind)
744 % line_error(mfilename,'Fork
nodes not supported yet by MAM solver.
');
749 %max(max(abs(TN-TN_1)))
754for it=1:2 % second pass to rescale again QN based on RN correction
756 inchain = sn.inchain{c};
757 Nc = sum(sn.njobs(inchain));
759 QNc = sum(sum(QN(:,inchain)));
760 QN(:,inchain) = QN(:,inchain) * (Nc / QNc);
765 ist = sn.nodeToStation(ind);
766 % Skip stations using exact MAP/D/c solver (already have exact values)
767 if mapdcStations(ist)
771 if isinf(sn.nservers(ist))
772 RN(ist,k) = S(ist,k);
774 RN(ist,k) = max([S(ist,k), QN(ist,k) ./ TN(ist,k)]);
779 QN(ist,k) = RN(ist,k) .* TN(ist,k);
783 Nc = sum(sn.njobs(inchain)); % closed population
784 if Nc == 0 % if closed chain
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);
799 % Non-SLC util from declared (uninflated) service time;
800 % see _kb/06-solver-catalog.md for rationale
805 UN(ist,k) = Strue(ist,k)*TN(ist,k);
815 slck = slcAll(sn.refstat(slcAll) == ist);
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.
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);
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(:)');
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);
843 RN(ist,k) = QN(ist,k) ./ TN(ist,k);