1function [QN,UN,RN,TN,CN,XN,runtime] = solver_ba_qrf_analyzer(sn, options)
2% SOLVER_BA_QRF_ANALYZER Adapter
for QRF library functions within SolverBA
4% [QN,UN,RN,TN,CN,XN,RUNTIME] = SOLVER_BA_QRF_ANALYZER(SN, OPTIONS)
6% Bridges the LINE sn
struct to QRF (Quadratic Reduction Framework) library
7% functions
for approximating performance metrics of single-
class closed
8% queueing networks with PH service.
10% Copyright (c) 2012-2026, Imperial College London
21% QRF only supports single-
class closed networks
23 line_error(mfilename,
'QRF methods only support single-class networks (found %d classes).', K);
25if any(isinf(sn.njobs))
26 line_error(mfilename, 'QRF methods only support closed networks.');
29% Extract MAPs as cell array: MAPs{i} = {D0, D1}
31K_phases = zeros(M, 1);
35 K_phases(i) = size(MAPs{i}{1}, 1);
38 MAPs{i} = {-1, 1}; % fallback exponential rate 1
42% Build routing matrix (M x M) from sn.rt (MK x MK)
46 rt(i,j) = sn.rt(i, j); % K=1, so indexing
is direct
50% Extract mu and v arrays from MAPs
52mu = zeros(M, Kmax, Kmax);
53v = zeros(M, Kmax, Kmax);
59 % Both mu and v are indexed (from phase, to phase), matching
60 % qrf_noblo_*
's q{i,j}(k,h) = v{i}(k,h) + r(i,i)*mu{i}(k,h).
61 % mu{i}(k,h) is the completion rate from phase k leaving to phase
62 % h, i.e. D1(k,h); v{i}(k,h) is the background phase change k -> h,
63 % i.e. D0(k,h) off the diagonal. Writing v as D0(h,k) transposes
64 % it, which is invisible for a reversible D0 but reverses the phase
65 % order of an Erlang (upper bidiagonal D0) and silently changes the
67 mu(i, h, k) = D1(h, k);
71 v(i, h, k) = D0(h, k);
77% Dispatch based on method
81 [UN_qrf, QN_qrf] = qrf_noblo_mmi(M, MR, K_phases(:)', N, mu, v, rt);
84 [UN_qrf, QN_qrf] = qrf_noblo_mem(MAPs, N, rt);
87 if isfield(options.config,
'qrf_alpha') && ~isempty(options.config.qrf_alpha)
88 alpha = options.config.qrf_alpha;
92 [UN_qrf, QN_qrf] = qrf_noblo_mmi_ld(MAPs, N, rt, alpha);
95 if isfield(options.config, 'qrf_alpha') && ~isempty(options.config.qrf_alpha)
96 alpha = options.config.qrf_alpha;
100 [UN_qrf, QN_qrf] = qrf_noblo_mmi_linear(MAPs, N, rt, alpha);
103 % isfield-guarded so a missing field reaches the explicit line_error
104 % below instead of throwing an opaque "Unrecognized field name".
105 if isfield(options.config, 'qrf_params')
106 qp = options.config.qrf_params;
111 line_error(mfilename, 'qrf.bas.mmi requires options.config.qrf_params with fields: f, MR, BB, F.');
117 [UN_qrf, QN_qrf] = qrf_bas_mmi_simple(f, M, MR, BB, K_phases(:)', F, N, mu, v, rt);
120 if isfield(options.config, 'qrf_params')
121 qp = options.config.qrf_params;
126 line_error(mfilename, 'qrf.bas.mem requires options.config.qrf_params with fields: f, MR, MM, MM1, ZZ, ZM, BB, F.');
136 [UN_qrf, QN_qrf] = qrf_bas_mem(f, M, MR, MM, MM1, ZZ, ZM, BB, K_phases(:)', F, N, mu, v, rt);
139 params = sn_to_qrf_params(sn, MAPs, K_phases, N, mu, v, rt, options);
140 params.verbose = options.verbose > 0;
141 [result] = qrf_bas(params);
142 % qrf_bas returns utilization bounds; derive QN via visit ratios
143 [UN_qrf, QN_qrf] = derive_qn_from_bounds(result.U(:)', M, N, S, PH, sn);
146 params = sn_to_qrf_params(sn, MAPs, K_phases, N, mu, v, rt, options);
147 params.verbose = options.verbose > 0;
148 [result] = qrf_rsrd(params);
149 [UN_qrf, QN_qrf] = derive_qn_from_bounds(result.U(:)', M, N, S, PH, sn);
152 line_error(mfilename, 'Unknown QRF method: %s', options.method);
155% Normalize QN to population constraint
157 QN_qrf = QN_qrf / sum(QN_qrf) * N;
160% Map 1D QRF results to M x K matrices (K=1)
170% Derive all metrics from the QRF utilization using Little's law and visit
173% UN_qrf(i)
is sum_{ni>=1, ki} p2(i,ni,ki,i,ni,ki), i.e.
P(n_i >= 1)
174% marginalised over phase. That IS the utilization and it
is bounded by 1
175% through the ONE constraint. An earlier comment here held that it
"is the sum
176% of phase effective utilizations, which can exceed 1 for phase-type service"; that
177%
is false, and what exceeds 1
is the sum ACROSS stations, which
is expected.
178% Adjudicated against glpsol on the paper
's AMPL model at K=2 (2-station
179% closed, Exp + Erlang-2): every UN_qrf entry lands inside the glpsol
180% [min,max] range for that station's utilization.
182% Throughput therefore comes from a finite-server station via
183% U_i = X * V_i * stime_i, which
is exact
for a single server. The previous
184% inversion X = QN(refstat) / (V*stime_ref) instead assumed R == stime at the
185% reference station, which holds only
for an infinite server; with a FCFS
186% reference station it returned X above the bottleneck capacity and hence
187% UN > 1. UN_qrf(i)/(V_i*stime_i)
is consistent across stations, so any
188% finite-server station determines X.
190% Compute visit ratios
191if isfield(sn,
'visits') && ~isempty(sn.visits)
194 [
visits] = sn_refresh_visits(sn, sn.chains, sn.rt, sn.rtnodes);
198refstat = sn.refstat(1);
200% Per-station mean service times
203 if ~isempty(PH{i}{1})
204 stimes(i) = map_mean(PH{i}{1});
208% Find system throughput XN from the QRF utilization at a finite-server
209% station: U_i = XN * V(i,1) * stime_i / S(i), exact
for a single server.
212 if ~isinf(S(i)) && stimes(i) > 0 && V(i, 1) > 0 && UN_qrf(i) > 0
213 XN(1) = UN_qrf(i) * S(i) / (V(i, 1) * stimes(i));
218 % No finite-server station carries load: fall back to the reference
219 % station, where QN = XN * V * stime holds exactly for a delay.
220 if stimes(refstat) > 0 && V(refstat, 1) > 0
221 XN(1) = QN(refstat, 1) / (V(refstat, 1) * stimes(refstat));
225% Derive per-station metrics from XN and visit ratios
227 if ~isempty(PH{i}{1})
229 TN(i, 1) = XN(1) * V(i, 1);
231 % Delay (infinite server): UN = QN by LINE convention
234 % Finite server: the QRF utilization directly
236 UN(i, 1) = UN_qrf(i);
239 % Response time via Little's law
241 RN(i, 1) = QN(i, 1) / TN(i, 1);
259runtime = toc(Tstart);
262function params = sn_to_qrf_params(sn, MAPs, K_phases, N, mu, v, rt, options)
263% Build params
struct for qrf_bas / qrf_rsrd from sn struct
268params.K = K_phases(:);
271% Convert mu/v from 3D arrays to cell arrays expected by qrf_bas/qrf_rsrd
272% Each mu{i}, v{i} must be a [Ki x Ki] matrix (even
for Ki=1)
273params.mu = cell(M, 1);
274params.v = cell(M, 1);
277 params.mu{i} = reshape(mu(i, 1:Ki, 1:Ki), Ki, Ki);
278 params.v{i} = reshape(v(i, 1:Ki, 1:Ki), Ki, Ki);
282params.F = zeros(M, 1);
284 if isfield(sn,
'cap') && ~isempty(sn.cap)
285 params.F(i) = sn.cap(i);
286 if isinf(params.F(i))
294% Blocking parameters from options or auto-infer. The field
is read through
295% isfield because the else-branch below
is a documented "not provided" fallback:
296% reading options.config.qrf_params directly throws "Unrecognized field name"
297% whenever config carries no such field, which
is the common case, and that
298% makes the fallback unreachable.
299if isfield(options.config, 'qrf_params')
300 qp = options.config.qrf_params;
305 % Validate required fields for BAS blocking configuration
306 if strcmp(options.method, 'qrf.bas')
307 required_bas = {
'f',
'MR',
'BB',
'MM',
'MM1',
'ZZ',
'ZM'};
308 for idx = 1:length(required_bas)
309 if ~isfield(qp, required_bas{idx})
310 line_error(mfilename,
'qrf_params must contain field ''%s'' for qrf.bas method.', required_bas{idx});
314 if isfield(qp,
'f'), params.f = qp.f; end
315 if isfield(qp,
'MR'), params.MR = qp.MR; end
316 if isfield(qp,
'BB'), params.BB = qp.BB; end
317 if isfield(qp,
'MM'), params.MM = qp.MM; end
318 if isfield(qp,
'ZZ'), params.ZZ = qp.ZZ; end
319 if isfield(qp,
'ZM'), params.ZM = qp.ZM; end
320 if isfield(qp,
'MM1'), params.MM1 = qp.MM1; end
322 % No blocking configuration supplied. This used to fall back to
323 % no-blocking defaults (MR=1) with a warning, which
is NOT a faithful
324 % substitute: measured against SolverCTMC on sanity_CQN_rm_{fcfs,ps}_1class,
325 % the defaults put qrf.bas and qrf.rsrd 4.16667 from exact where the
326 % pre-migration goldens -- produced with real blocking parameters -- sit at
327 % 0.133333, i.e. 31x closer. Returning a number that wrong
is worse than
328 % refusing, so the caller
is told what
is missing and how to supply it.
329 line_error(mfilename, [
'The ''%s'' method requires blocking parameters, which were not ' ...
330 'supplied. Set options.config.qrf_params to a struct with fields f, MR, BB, MM, ' ...
331 'MM1, ZZ, ZM (BB is per-station buffer capacity, MR the blocking multiplier). ' ...
332 'There is no meaningful default: assuming no blocking (MR=1) yields a bound ' ...
333 'roughly 31x farther from exact than a correctly parameterised one.'], options.method);
336% Load-dependent alpha
337if isfield(options.config,
'qrf_alpha') && ~isempty(options.config.qrf_alpha)
338 alpha_mat = options.config.qrf_alpha;
339 params.alpha = cell(M, 1);
341 params.alpha{i} = alpha_mat(i, :)
';
346function [UN_qrf, QN_qrf] = derive_qn_from_bounds(U_bounds, M, N, S, PH, sn)
347% Derive QN from utilization bounds using visit ratios and Little's law.
348% For bounds methods (qrf.bas, qrf.rsrd), only utilization
is returned.
349% We compute system throughput from the utilization at queue stations,
350% then derive QN at all stations via Little
's law.
353% Compute visit ratios
354if isfield(sn, 'visits') && ~isempty(sn.visits)
355 V = cellsum(sn.visits);
357 [visits] = sn_refresh_visits(sn, sn.chains, sn.rt, sn.rtnodes);
361% Find XN from the first finite-server station with nonzero utilization
364 if ~isinf(S(i)) && ~isempty(PH{i}{1}) && U_bounds(i) > 0 && V(i,1) > 0
365 stime = map_mean(PH{i}{1});
367 % UN = XN * V(i) * stime / S(i), so XN = UN * S(i) / (V(i) * stime)
368 XN_est = U_bounds(i) * S(i) / (V(i, 1) * stime);
374% Derive QN at each station: QN(i) = XN * V(i) * RN(i)
375% For single-server queues: RN(i) >= stime(i) (at least one service time)
376% Use Little's law: QN(i) = TN(i) * RN(i) where TN(i) = XN * V(i)
379 if ~isempty(PH{i}{1})
380 stime = map_mean(PH{i}{1});
381 TN_i = XN_est * V(i, 1);
383 % Delay: QN = TN * stime
384 QN_qrf(i) = TN_i * stime;
385 UN_qrf(i) = QN_qrf(i); % LINE convention
for INF server
387 % Queue: QN = TN * stime / (1 - U)
for M/G/1-like estimate
389 QN_qrf(i) = TN_i * stime / (1 - U_bounds(i));
391 QN_qrf(i) = N; % saturated
397% Rescale to population constraint
399 QN_qrf = QN_qrf / sum(QN_qrf) * N;