1function [Q,U,R,T,C,X,lG,runtime,iter,method] = solver_nc_oi_analyzer(sn, options)
2% [Q,U,R,T,C,X,LG,RUNTIME,ITER,METHOD] = SOLVER_NC_OI_NC_ANALYZER(SN, OPTIONS)
4% Exact normalizing-constant analysis of a closed queueing network that mixes
5% order-independent (OI) stations with ordinary BCMP product-
form stations.
7% - OI stations (SchedStrategy.OI / PAS with an empty swap graph), analyzed
8% by the balanced-fairness rank rate mu(supp n) (Bonald & Proutiere 2003);
9% - any BCMP product-
form station: infinite-server (delay, IS), processor
10% sharing (PS), LCFS-PR, and
class-independent-rate FCFS, single- or multi-
11% server, analyzed by the load-dependent BCMP weight table
12% W_i(n) = (sum n)!/prod(n_r!) * prod_r D_{i,r}^{n_r} / prod_{k=1}^{sum n} beta_i(k),
13% with D_{i,r} = V(i,r)/rate(i,r) the per-
class demand and beta_i(k) the
14% load-dependent capacity (min(k,c)
for a c-server queue, k
for IS).
16% The full-network normalizing-constant table G(
P) over the lattice
17% 0 <=
P <= N
is assembled by balanced-fairness convolution of all station
18% tables, with the OI stations and the aggregated delay evaluated through
19% PFQN_NCOI. The exact per-
class mean queue length at any station follows from
20% the OI functional-server (FNC) identity of PFQN_OI_FNC (Casale, QEST 2006):
21% E[
n_{i,r}] = (
sum_{0<=b<=N} Psi_{i,r}(b) G(N-b) ) / G(N) - 1,
22% Psi_{i,r} being the FNC balance function built from that station
's balance
23% table with f(n)=n_r. Per-class throughput X_r = G(N-e_r)/G(N); delay queue
24% length and response time follow from Little's law.
26% General per-class
visits at the OI stations are supported: they enter the
27% v-weighted balanced-fairness balance Phi^v(n)=(1/mu(n)) sum_r v_r Phi^v(n-e_r).
28% BCMP-station
visits are arbitrary (folded into D).
30% Copyright (c) 2012-2026, Imperial College London
39% ---- reject
class switching (OI rank rates are per raw class) --------------
41 if numel(sn.inchain{c}) > 1
42 line_error(mfilename,
'solver_nc_oi requires one class per chain (no class switching).');
45if any(isinf(sn.njobs))
46 line_error(mfilename, 'solver_nc_oi requires a closed queueing network.');
48N = round(sn.njobs(:)');
50% ---- classify stations -----------------------------------------------------
51% OI: rank-rate balanced-fairness station. INF: aggregated into the delay Z.
52% Q : ordinary BCMP product-
form station (PS / LCFS-PR / FCFS), a load-
53% dependent weight table with server count c(ist).
59 ind = sn.stationToNode(ist);
60 if sn.sched(ist) == SchedStrategy.INF
62 elseif sn.sched(ist) == SchedStrategy.PAS || sn.sched(ist) == SchedStrategy.OI
64 if ind >= 1 && ind <= numel(sn.nodeparam) && isstruct(sn.nodeparam{ind}) ...
65 && isfield(sn.nodeparam{ind},
'swapGraph')
66 sg = sn.nodeparam{ind}.swapGraph;
68 if isempty(sg) || any(sg(:) ~= 0)
69 line_error(mfilename,
'solver_nc_oi supports OI stations only (PAS with a non-empty swap graph is not order-independent).');
72 svc{ist} = sn.nodeparam{ind}.svcRateFun;
74 line_error(mfilename,
'OI station %d has no service rate function; set it via setService(@(c) ...).', ist);
76 elseif any(sn.sched(ist) == [SchedStrategy.PS, SchedStrategy.LCFSPR, SchedStrategy.FCFS, SchedStrategy.SIRO])
78 if any(sn.sched(ist) == [SchedStrategy.FCFS, SchedStrategy.SIRO])
79 % BCMP type 1 (and the order-insensitive SIRO, which shares the
80 % FCFS queue-length distribution for exponential service) require a
81 % class-independent service rate for product
form.
82 rr = sn.rates(ist, :);
83 rr = rr(isfinite(rr) & sn.njobs > 0);
84 if ~isempty(rr) && (max(rr) - min(rr)) > 1e-9 * max(rr)
85 line_error(mfilename, '
Station %d has class-dependent FCFS/SIRO rates and
is not product
form; solver_nc_oi requires class-independent rates.', ist);
89 line_error(mfilename, 'solver_nc_oi supports only INF (delay), OI, PS, LCFS-PR, SIRO and class-independent FCFS stations.');
93% ---- per-class
visits (chain == class); normalize to the reference station -
96 c = find(sn.chains(:, r)); % the chain carrying class r
97 vis = sn.
visits{c}; % (nstateful x nclasses)
99 isf = sn.stationToStateful(ist);
100 V(ist, r) = vis(isf, r);
102 vref = V(sn.refstat(r), r);
104 V(:, r) = V(:, r) / vref;
108% ---- per-
class demand and aggregated delay demand Z_r ----------------------
109% Z_r = sum over INF stations of V(i,r) * mean service time(i,r).
111ST(~isfinite(ST)) = 0;
113for ist = find(isINF(:))
'
115 Z(r) = Z(r) + V(ist, r) * ST(ist, r);
118D = zeros(M, K); % per-class demand at BCMP queues
119for ist = find(isQ(:))'
121 D(ist, r) = V(ist, r) * ST(ist, r);
125% OI-station
class visit ratios feed the v-weighted balanced-fairness balance
126% (pfqn_ncoi / oi_phi); general (non-unit)
visits are supported.
127oiList = find(isOI(:))
';
128oivis = cell(1, numel(oiList));
129for m = 1:numel(oiList)
130 oivis{m} = V(oiList(m), :);
133% ---- OI rank-rate handles on a per-class count vector ----------------------
134% svcRateFun(c) is permutation-invariant, so it is a function of the count vector
135% n; evaluate it on a canonical microstate holding n_r copies of class r. see
136% _kb/06-solver-catalog.md (NC section, OI analyzer)
137rates = cell(1, numel(oiList));
138for m = 1:numel(oiList)
139 fun = svc{oiList(m)};
140 rates{m} = @(n) fun(oi_microstate(n));
143% ---- population lattice ----------------------------------------------------
144[shp, stride, total] = oi_lattice(N);
146% ---- core normalizing-constant table (OI stations + aggregated delay) ------
147Gfull = zeros(total, 1);
150 Gfull(i) = pfqn_ncoi(Z, P, rates, oivis);
153% ---- fold the BCMP queueing stations by lattice convolution ----------------
154qList = find(isQ(:))';
156 c = sn.nservers(ist);
157 Wq = oi_ld_table(D(ist, :), c, shp, total);
158 Gfull = oi_conv(Gfull, Wq, shp, stride, total);
164% ---- per-
class throughput X_r = G(N - e_r)/G(N) ----------------------------
168 er = zeros(1, K); er(r) = 1;
169 X(r) = Gfull(1 + sum((N - er) .* stride)) / G;
173% ---- per-station per-
class mean queue length via the FNC identity ----------
175for m = 1:numel(oiList) % OI stations
177 Phi = oi_phi(rates{m}, N, oivis{m});
180 [~, Psir] = pfqn_oi_fnc(Phi, N, @(n) n(r));
181 Q(ist, r) = oi_fnc_mean(Psir, Gfull, shp, stride, total) / G - 1;
185for ist = qList % BCMP queueing stations
186 Wq = oi_ld_table(D(ist, :), sn.nservers(ist), shp, total);
189 [~, Psir] = pfqn_oi_fnc(Wq, N, @(n) n(r));
190 Q(ist, r) = oi_fnc_mean(Psir, Gfull, shp, stride, total) / G - 1;
194for ist = find(isINF(:))
' % delay: Little's law
196 Q(ist, r) = X(r) * V(ist, r) * ST(ist, r);
200% ---- throughput, utilization, response time per station --------------------
206 T(ist, r) = X(r) * V(ist, r);
209for ist = find(isINF(:))
'
210 U(ist, :) = Q(ist, :); % INF utilization convention
212for ist = qList % BCMP queue: offered-load per server
213 c = sn.nservers(ist);
214 if ~isfinite(c) || c <= 0, c = 1; end
216 U(ist, r) = X(r) * D(ist, r) / c;
219for m = 1:numel(oiList)
220 % In-service utilization U_r = E[sir_r]/c via the functional-server identity
221 % E[f(n)] = G^{+}/G - 1 (pfqn_oi_fnc, pfqn_oi_insvc); see
222 % _kb/06-solver-catalog.md (NC section, OI analyzer)
224 S = sn.nservers(ist);
225 if ~isfinite(S) || S <= 0, S = 1; end
226 Phi = oi_phi(rates{m}, N, oivis{m});
227 gins = pfqn_oi_insvc(rates{m}, N);
230 fr = @(n) gins(1 + sum(n .* stride), r);
231 [~, Psir] = pfqn_oi_fnc(Phi, N, fr);
232 U(ist, r) = (oi_fnc_mean(Psir, Gfull, shp, stride, total) / G - 1) / S;
239 R(ist, r) = Q(ist, r) / T(ist, r);
244C = zeros(1, K); % per-class system response time
251runtime = toc(Tstart);
254% ==========================================================================
255function [shp, stride, total] = oi_lattice(N)
256% Column-major lattice descriptor for populations 0 <= n <= N.
257N = round(N(:)'); R = numel(N); shp = N + 1;
259for d = 2:R, stride(d) = stride(d-1) * shp(d-1); end
263% ==========================================================================
264function c = oi_microstate(n)
265% Canonical ordered microstate holding n_r copies of
class r (n a count
266% vector). For an order-independent station the service rate
is invariant to
267% the ordering, so
this representative suffices to evaluate svcRateFun(c).
268c = repelem(1:numel(n), round(n));
271% ==========================================================================
272function n = oi_sub(i, shp)
273% Decode linear index i (1-based) to the subscript vector n (0-based counts).
274R = numel(shp); n = zeros(1, R); li = i - 1;
275for d = 1:R, n(d) = mod(li, shp(d)); li = floor(li / shp(d)); end
278% ==========================================================================
279function Phi = oi_phi(oirate, N, vis)
280% Forward v-weighted balanced-fairness fill of the OI balance function:
281% Phi(0)=1, Phi(n) = (1/mu(n))
sum_{r: n_r>0} v_r Phi(n - e_r).
282[shp, stride, total] = oi_lattice(N);
284if nargin < 3 || isempty(vis), vis = ones(1, R); end
285Phiv = zeros(total, 1);
288 if sum(n) == 0, Phiv(i) = 1;
continue, end
290 for r = 1:R,
if n(r) > 0, s = s + vis(r) * Phiv(i - stride(r)); end, end
291 Phiv(i) = s / oirate(n);
293if R == 1, Phi = Phiv;
else, Phi = reshape(Phiv, shp); end
296% ==========================================================================
297function W = oi_ld_table(Dq, c, shp, total)
298% BCMP load-dependent weight table over the lattice:
299% W(n) = (sum n)!/prod(n_r!) * prod_r D_r^{n_r} / prod_{k=1}^{sum n} beta(k),
300% beta(k) = min(k,c)
for a c-server queue (c=1 -> single server, beta==1).
301% Column-major flat vector. W(0)=1.
302Dq = Dq(:)
'; R = numel(shp); W = zeros(total, 1);
303if ~isfinite(c) || c <= 0, c = 1; end
307 logf = gammaln(tot + 1); ok = true;
310 if Dq(r) <= 0, ok = false; break, end
311 logf = logf + n(r) * log(Dq(r)) - gammaln(n(r) + 1);
314 if ~ok, continue, end
316 logf = logf - log(min(k, c));
322% ==========================================================================
323function Cv = oi_conv(Av, Bv, shp, stride, total)
324% Lattice convolution Cv(m) = sum_{0<=a<=m} Av(a) Bv(m-a) over 0..N.
326subs = zeros(total, R);
327for i = 1:total, subs(i, :) = oi_sub(i, shp); end
330 m = subs(i, :); acc = 0;
334 acc = acc + Av(j) * Bv(1 + sum((m - a) .* stride));
341% ==========================================================================
342function val = oi_fnc_mean(Psi, Gfull, shp, stride, total)
343% G^{+} = sum_{0<=b<=N} Psi(b) G(N-b): the FNC of the target station convolved
344% against the full-network normalizing-constant table, evaluated at n = N.
345N = shp - 1; Psiv = Psi(:); val = 0;
347 if Psiv(i) == 0, continue, end
349 val = val + Psiv(i) * Gfull(1 + sum((N - b) .* stride));