1function [QN,UN,RN,TN,CN,XN,lG,runtime,iter,actualmethod] = solver_mva_oi_analyzer(sn, options)
2% SOLVER_MVA_OI_ANALYZER Exact mean-value MVA
for order-independent networks
4% An order-independent (OI) station
is a
class-dependent load-dependent server
5% whose total service rate mu(n)
is a permutation-invariant function of the
6% per-
class count vector n. A closed network of infinite-server (delay) and
7% load-independent (single-server, product-
form) stations plus ANY number of OI
8% stations
is product-
form. This analyzer aggregates the delay stations into a
9% single think-time vector Z, collects the load-independent (LI) queue demands
10% and the OI-station rate handles, and calls PFQN_MVAOI, the mean-value
11% Conditional-MVA (CMVA) that carries one rate-shift vector per OI station and
12% returns exact per-
class throughput and queue-lengths WITHOUT any normalizing
13% constant or joint marginal. The marginal-distribution counterpart
is
17% Reiser, Lavenberg (1980). Mean-Value Analysis of Closed Multichain Queuing
18% Networks. JACM 27(2). Load-dependent extension: Bruell, Balbo, Afshari
19% (1984). OI stations / CMVA: Casale (2009); Casale, Comte, Dorsman (2026).
21% Copyright (c) 2012-2026, Imperial College London
26oi_list = find_oi_stations(sn);
28 error(
'solver_mva_oi_analyzer:NoOIStation', ...
29 'OI solver requires at least one order-independent station');
34N = round(sn.njobs(:)
');
38isDelay = (sn.sched(:)' == SchedStrategy.INF);
40% Per-
class service demand D_ir = V_ir/rate_ir at non-OI stations.
42for c = 1:numel(sn.visits)
48 if isfinite(sn.rates(i,r)) && sn.rates(i,r) > 0
49 D(i,r) = V(i,r) / sn.rates(i,r);
54% Aggregate the delay stations into Z; collect the LI queue demands and OI-station
55% rate handles. A multiserver BCMP queue (c>1) cannot take the single-server LI
56% path, so it
is promoted to an OI station via the c-server BCMP weight; see
57% _kb/06-solver-catalog.md (NC section, OI analyzer) for the W(n)/mu(n) identity.
66 elseif isfinite(sn.nservers(i)) && sn.nservers(i) > 1
67 ms_list(end+1) = i; %
#ok<AGROW>
69 li_list(end+1) = i; %#ok<AGROW>
74muCell = cell(1, numel(oi_list) + numel(ms_list));
75% Per-muCell-station visit vectors. Genuine OI stations carry their
class visit
76% ratios V(oi,:) (the rate handle has no
visits); ms-promoted stations pass
77% ones, as their
visits are already folded into D by ms_oi_rate.
78oivis = cell(1, numel(oi_list) + numel(ms_list));
79for o = 1:numel(oi_list)
80 node_oi = sn.stationToNode(oi_list(o));
81 svcRateFun = sn.nodeparam{node_oi}.svcRateFun;
82 muCell{o} = @(n) oi_rate(svcRateFun, n, R);
83 oivis{o} = V(oi_list(o), :);
85for j = 1:numel(ms_list)
86 muCell{numel(oi_list) + j} = ms_oi_rate(D(ms_list(j), :), sn.nservers(ms_list(j)));
87 oivis{numel(oi_list) + j} = ones(1, R);
90[X, Qoi, Qli, ~, Soi] = pfqn_mvaoi(Z, N, muCell, Dli, oivis, options);
92% Assemble per-station mean queue-lengths.
94for o = 1:numel(oi_list)
95 QN(oi_list(o), :) = Qoi(o, :);
97for j = 1:numel(ms_list)
98 QN(ms_list(j), :) = Qoi(numel(oi_list) + j, :);
100for j = 1:numel(li_list)
101 QN(li_list(j), :) = Qli(j, :);
105 QN(i, :) = X .* D(i, :); % IS station: exact product-
form share
110% Row of Soi/Qoi holding each OI station (muCell order: oi_list, then ms_list).
112for o = 1:numel(oi_list)
113 oiRow(oi_list(o)) = o;
116% Assemble outputs at the full population.
123 TN(i,r) = XN(r) * V(i,r);
125 RN(i,r) = QN(i,r) / XN(r);
128 % In-service utilization U_r = E[sir_r]/nservers; see
129 % _kb/06-solver-catalog.md (NC section, OI analyzer). Soi holds E[sir_r].
131 if ~isfinite(sv) || sv <= 0
134 UN(i,r) = Soi(oiRow(i), r) / sv;
139 if ~isfinite(sv) || sv <= 0
142 UN(i,r) = XN(r) * D(i,r) / sv;
149runtime = toc(tstart);
154% =========================================================================
156% =========================================================================
158function oi_list = find_oi_stations(sn)
159%
Station indices of all OI stations: PAS/OI scheduling with an all-zero swap
160% graph and a service-rate function (mirrors nc_is_oi_model detection).
162for ist = 1:sn.nstations
163 if sn.sched(ist) ~= SchedStrategy.PAS && sn.sched(ist) ~= SchedStrategy.OI
166 ind = sn.stationToNode(ist);
167 if ind < 1 || ind > numel(sn.nodeparam) || ~isstruct(sn.nodeparam{ind})
170 np = sn.nodeparam{ind};
171 if ~isfield(np,
'swapGraph') || ~isfield(np,
'svcRateFun') || isempty(np.svcRateFun)
175 if isempty(sg) || any(sg(:) ~= 0)
178 oi_list(end+1) = ist; %
#ok<AGROW>
182function h = ms_oi_rate(Dq, c)
183% OI rate function reproducing the c-server BCMP station with per-
class demands
184% Dq: mu(n) = (min(|n|,c)/|n|) *
sum_{r: n_r>0} n_r/Dq_r. For c = 1
this is the
185% familiar total completion rate of a multiclass single-server queue, and
for a
186% single
class it reduces to min(n,c)/Dq (M/M/c).
188if ~isfinite(c) || c <= 0
191h = @(n) ms_oi_rate_eval(n, Dq, c);
194function rate = ms_oi_rate_eval(n, Dq, c)
202 if n(r) > 0 && Dq(r) > 0
203 acc = acc + n(r) / Dq(r);
206rate = (min(tot, c) / tot) * acc;
209function rate = oi_rate(svcRateFun, n, R)
210% OI total service rate at count vector n via the 1-based canonical microstate.
215micro = repelem(1:R, n);
216rate = svcRateFun(micro);