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 a single think-time vector Z; collect
the
55% load-independent (LI) queue demands and
the OI-station rate handles.
56% PFQN_MVAOI models an LI queue as a SINGLE server, so a multiserver BCMP queue
57% (c > 1) cannot go down that path. It
is instead promoted to an OI station:
58%
the c-server BCMP weight W(n) = |n|!/prod(n_r!) prod_r D_r^{n_r} /
59% prod_{k<=|n|} beta(k), beta(k) = min(k,c), satisfies
the OI balance recursion
60% Phi(n) = (1/mu(n)) sum_{r:n_r>0} Phi(n - e_r) exactly
for the permutation-
61% invariant rate mu(n) = (beta(|n|)/|n|) * sum_r n_r/D_r, since
62% W(n - e_r)/W(n) = n_r beta(|n|)/(|n| D_r). This reuses
the exact multi-OI CMVA
63% instead of approximating
the multiserver station. Reporting still classifies
64%
the station as a queue (ms_list), so its utilization keeps
the per-server
65% offered-load convention.
74 elseif isfinite(sn.nservers(i)) && sn.nservers(i) > 1
75 ms_list(end+1) = i; %#ok<AGROW>
77 li_list(end+1) = i; %#ok<AGROW>
82muCell = cell(1, numel(oi_list) + numel(ms_list));
83for o = 1:numel(oi_list)
84 node_oi = sn.stationToNode(oi_list(o));
85 svcRateFun = sn.nodeparam{node_oi}.svcRateFun;
86 muCell{o} = @(n) oi_rate(svcRateFun, n, R);
88for j = 1:numel(ms_list)
89 muCell{numel(oi_list) + j} = ms_oi_rate(D(ms_list(j), :), sn.nservers(ms_list(j)));
92[X, Qoi, Qli, ~, Soi] = pfqn_mvaoi(Z, N, muCell, Dli, options);
94% Assemble per-station mean queue-lengths.
96for o = 1:numel(oi_list)
97 QN(oi_list(o), :) = Qoi(o, :);
99for j = 1:numel(ms_list)
100 QN(ms_list(j), :) = Qoi(numel(oi_list) + j, :);
102for j = 1:numel(li_list)
103 QN(li_list(j), :) = Qli(j, :);
107 QN(i, :) = X .* D(i, :); % IS station: exact product-form share
112% Row of Soi/Qoi holding each OI station (muCell order: oi_list, then ms_list).
114for o = 1:numel(oi_list)
115 oiRow(oi_list(o)) = o;
118% Assemble outputs at
the full population.
125 TN(i,r) = XN(r) * V(i,r);
127 RN(i,r) = QN(i,r) / XN(r);
130 % In-service utilization U_r = E[sir_r]/nservers, as in
131 % solver_nc_oi_analyzer and
the exact CTMC/LDES: sir_r counts
the
132 %
class-r jobs receiving a strictly positive rank rate. Soi holds
133 % E[sir_r] from
the OI
count marginal.
135 if ~isfinite(sv) || sv <= 0
138 UN(i,r) = Soi(oiRow(i), r) / sv;
143 if ~isfinite(sv) || sv <= 0
146 UN(i,r) = XN(r) * D(i,r) / sv;
153runtime = toc(tstart);
158% =========================================================================
160% =========================================================================
162function oi_list = find_oi_stations(sn)
163%
Station indices of all OI stations: PAS/OI scheduling with an all-zero swap
164% graph and a service-rate function (mirrors nc_is_oi_model detection).
166for ist = 1:sn.nstations
167 if sn.sched(ist) ~= SchedStrategy.PAS && sn.sched(ist) ~= SchedStrategy.OI
170 ind = sn.stationToNode(ist);
171 if ind < 1 || ind > numel(sn.nodeparam) || ~isstruct(sn.nodeparam{ind})
175 if ~isfield(np,
'swapGraph') || ~isfield(np,
'svcRateFun') || isempty(np.svcRateFun)
179 if isempty(sg) || any(sg(:) ~= 0)
182 oi_list(end+1) = ist; %
#ok<AGROW>
186function h = ms_oi_rate(Dq, c)
187% OI rate function reproducing
the c-server BCMP station with per-
class demands
188% Dq: mu(n) = (min(|n|,c)/|n|) * sum_{r: n_r>0} n_r/Dq_r. For c = 1
this is the
189% familiar total completion rate of a multiclass single-server queue, and
for a
190% single
class it reduces to min(n,c)/Dq (M/M/c).
192if ~isfinite(c) || c <= 0
195h = @(n) ms_oi_rate_eval(n, Dq, c);
198function rate = ms_oi_rate_eval(n, Dq, c)
206 if n(r) > 0 && Dq(r) > 0
207 acc = acc + n(r) / Dq(r);
210rate = (min(tot, c) / tot) * acc;
213function rate = oi_rate(svcRateFun, n, R)
214% OI total service rate at count vector n via the 1-based canonical microstate.
219micro = repelem(1:R, n);
220rate = svcRateFun(micro);