1function [QN,UN,RN,TN,CN,XN,totiter] = solver_mam_basic_mmap_closed(sn, options)
2% [QN,UN,RN,TN,CN,XN,TOTITER] = SOLVER_MAM_BASIC_MMAP_CLOSED(SN, OPTIONS)
4% Closed-network wrapper around solver_mam_basic_mmap_inner. Drives a
5% per-
class bisection on the surrogate arrival rate LAMBDA so that the
6% inner solver
's queue lengths match the closed population SN.NJOBS.
7% Mirrors the outer-loop structure of solver_mna_closed.
9% Copyright (c) 2012-2026, Imperial College London
16% Per-class bisection bounds: upper = slowest non-INF station rate for that class
17nonInfStations = find(sn.nservers < Inf);
18lambda_lb = zeros(1,K);
19lambda_ub = zeros(1,K);
22 if ~isempty(nonInfStations)
23 rates_k = sn.rates(nonInfStations, k);
24 rates_k = rates_k(isfinite(rates_k) & rates_k > 0);
27 infStations = find(sn.nservers == Inf);
28 rates_inf = sn.rates(infStations, k);
29 rates_inf = rates_inf(isfinite(rates_inf) & rates_inf > 0);
33 lambda_ub(k) = max(rates_inf);
36 lambda_ub(k) = min(rates_k);
41QNc(~isfinite(QNc)) = 0; % open classes contribute 0; only closed populations gate convergence
46% Self-looping classes are pinned by the SLC clamp below; they must not
47% contribute a (saturating) surrogate arrival stream to the inner algorithm.
50inner_options = options;
51inner_options.iter_max = max(20, ceil(options.iter_max/10));
52inner_options.verbose = false;
53% Cap MMAP phase truncation at 16 (lossless, avoids O(dim^3) waste);
54% see _kb/06-solver-catalog.md for rationale
55if ~isfield(inner_options.config, 'space_max
') || inner_options.config.space_max > 16
56 inner_options.config.space_max = 16;
66% Last successful inner-algorithm outputs (for fallback if final trial diverges)
67QN_last = QN; UN_last = UN; RN_last = RN;
68TN_last = TN; CN_last = CN; XN_last = XN;
71bisect_tol = max(options.iter_tol, 1e-3);
73while max(abs(QN_chain - QNc)) > bisect_tol && it_out < options.iter_max
76 bracket_collapsed = true;
78 if ~isfinite(QNc(k)) || QNc(k) == 0 || sn.isslc(k)
81 if QN_chain(k) < QNc(k)
82 lambda_lb(k) = lambda(k);
84 lambda_ub(k) = lambda(k);
86 lambda(k) = 0.5 * (lambda_lb(k) + lambda_ub(k));
87 % Bisection can still refine class k only while its bracket is
88 % wider than the precision floor below which LAMBDA cannot move
89 % any reported metric.
90 if (lambda_ub(k) - lambda_lb(k)) > GlobalConstants.FineTol * max(1, abs(lambda_ub(k)))
91 bracket_collapsed = false;
94 % Bracket-width stagnation break; see _kb/06-solver-catalog.md for rationale
102 [QN, UN, RN, TN, CN, XN, ~] = solver_mam_basic_mmap_inner(sn, inner_options, lambda);
105 % Inner algorithm diverged (typically MMAPPH1FCFS / lyap NaN under
106 % saturation). Treat all chains as overloaded so the bisection
107 % drops lambda on its next step.
108 algorithm_ok = false;
112 % SLC clamp: all jobs at refstat for self-looping classes
116 QN(sn.refstat(k), k) = sn.njobs(k);
119 QN_chain = sum(QN, 1);
120 QN_chain(isnan(QN_chain) | isinf(QN_chain)) = 1/GlobalConstants.FineTol;
121 QN_last = QN; UN_last = UN; RN_last = RN;
122 TN_last = TN; CN_last = CN; XN_last = XN;
125 QN_chain = ones(1,K) * (1/GlobalConstants.FineTol);
129% If the last trial diverged, fall back to the most recent successful one
130if ~algorithm_ok && have_good
131 QN = QN_last; UN = UN_last; RN = RN_last;
132 TN = TN_last; CN = CN_last; XN = XN_last;
135% Final SLC pass: pin throughput/utilisation at refstat (mirrors solver_mna_closed)
140 QN(ist, k) = sn.njobs(k);
141 TN(ist, k) = sn.njobs(k) * sn.rates(ist, k);
143 RN(ist, k) = QN(ist, k) / TN(ist, k);
147 UN(ist, k) = S(ist, k) * TN(ist, k);
151% Population redistribution within chain (matches solver_mna_closed:323-328)
153 inchain = sn.inchain{c};
154 if isfinite(sn.njobs(c))
155 sumQ = sum(sum(QN(:,inchain)));
157 QN(:,inchain) = sn.njobs(c) .* QN(:,inchain) / sumQ;
162% Delay/INF utilisation = mean number of jobs (matches solver_mna_closed)
163for ist=1:sn.nstations
164 if sn.sched(ist) == SchedStrategy.INF
165 UN(ist,:) = QN(ist,:);