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_OI_NC. 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% Requires unit per-class
visits at
the OI stations (
the rank-rate balanced-
27% fairness framework); BCMP-station
visits are arbitrary (folded into D).
29% Copyright (c) 2012-2026, Imperial College London
38% ---- reject
class switching (OI rank rates are per raw class) --------------
40 if numel(sn.inchain{c}) > 1
41 line_error(mfilename,
'solver_nc_oi requires one class per chain (no class switching).');
44if any(isinf(sn.njobs))
45 line_error(mfilename, 'solver_nc_oi requires a closed queueing network.');
47N = round(sn.njobs(:)');
49% ---- classify stations -----------------------------------------------------
50% OI: rank-rate balanced-fairness station. INF: aggregated into
the delay Z.
51% Q : ordinary BCMP product-form station (PS / LCFS-PR / FCFS), a load-
52% dependent weight table with server
count c(ist).
58 ind = sn.stationToNode(ist);
59 if sn.sched(ist) == SchedStrategy.INF
61 elseif sn.sched(ist) == SchedStrategy.PAS || sn.sched(ist) == SchedStrategy.OI
64 && isfield(sn.nodeparam{ind},
'swapGraph')
65 sg = sn.nodeparam{ind}.swapGraph;
67 if isempty(sg) || any(sg(:) ~= 0)
68 line_error(mfilename,
'solver_nc_oi supports OI stations only (PAS with a non-empty swap graph is not order-independent).');
71 svc{ist} = sn.nodeparam{ind}.svcRateFun;
73 line_error(mfilename,
'OI station %d has no service rate function; set it via setService(@(c) ...).', ist);
75 elseif any(sn.sched(ist) == [SchedStrategy.PS, SchedStrategy.LCFSPR, SchedStrategy.FCFS, SchedStrategy.SIRO])
77 if any(sn.sched(ist) == [SchedStrategy.FCFS, SchedStrategy.SIRO])
78 % BCMP type 1 (and
the order-insensitive SIRO, which shares
the
79 % FCFS queue-length distribution for exponential service) require a
80 % class-independent service rate for product form.
81 rr = sn.rates(ist, :);
82 rr = rr(isfinite(rr) & sn.njobs > 0);
83 if ~isempty(rr) && (max(rr) - min(rr)) > 1e-9 * max(rr)
84 line_error(mfilename, '
Station %d has class-dependent FCFS/SIRO rates and
is not product form; solver_nc_oi requires class-independent rates.', ist);
88 line_error(mfilename, 'solver_nc_oi supports only INF (delay), OI, PS, LCFS-PR, SIRO and class-independent FCFS stations.');
92% ---- per-class
visits (chain == class); normalize to
the reference station -
95 c = find(sn.chains(:, r)); %
the chain carrying class r
96 vis = sn.
visits{c}; % (nstateful x nclasses)
98 isf = sn.stationToStateful(ist);
99 V(ist, r) = vis(isf, r);
101 vref = V(sn.refstat(r), r);
103 V(:, r) = V(:, r) / vref;
107% ---- per-
class demand and aggregated delay demand Z_r ----------------------
108% Z_r = sum over INF stations of V(i,r) * mean service time(i,r).
110ST(~isfinite(ST)) = 0;
112for ist = find(isINF(:))
'
114 Z(r) = Z(r) + V(ist, r) * ST(ist, r);
117D = zeros(M, K); % per-class demand at BCMP queues
118for ist = find(isQ(:))'
120 D(ist, r) = V(ist, r) * ST(ist, r);
124% ---- require unit
visits at OI stations ------------------------------------
125oiList = find(isOI(:))
';
128 if N(r) > 0 && abs(V(ist, r) - 1) > 1e-9
129 line_error(mfilename, 'solver_nc_oi requires unit per-class
visits at OI station %d (got V=%g for class %d).
', ist, V(ist, r), r);
134% ---- OI rank-rate handles on a per-class count vector ----------------------
135% The OI service function svcRateFun(c) takes an ordered microstate list c. For
136% an order-independent station its value is permutation-invariant, hence a
137% function of the multiset of present jobs, i.e. of the full count vector n
138% (including class multiplicities, not merely the support). Evaluate it on a
139% canonical microstate holding n_r copies of class r (any ordering is valid).
140rates = cell(1, numel(oiList));
141for m = 1:numel(oiList)
142 fun = svc{oiList(m)};
143 rates{m} = @(n) fun(oi_microstate(n));
146% ---- population lattice ----------------------------------------------------
147[shp, stride, total] = oi_lattice(N);
149% ---- core normalizing-constant table (OI stations + aggregated delay) ------
150Gfull = zeros(total, 1);
153 Gfull(i) = pfqn_oi_nc(Z, P, rates);
156% ---- fold the BCMP queueing stations by lattice convolution ----------------
157qList = find(isQ(:))';
159 c = sn.nservers(ist);
160 Wq = oi_ld_table(D(ist, :), c, shp, total);
161 Gfull = oi_conv(Gfull, Wq, shp, stride, total);
167% ---- per-
class throughput X_r = G(N - e_r)/G(N) ----------------------------
171 er = zeros(1, K); er(r) = 1;
172 X(r) = Gfull(1 + sum((N - er) .* stride)) / G;
176% ---- per-station per-
class mean queue length via
the FNC identity ----------
178for m = 1:numel(oiList) % OI stations
180 Phi = oi_phi(rates{m}, N);
183 [~, Psir] = pfqn_oi_fnc(Phi, N, @(n) n(r));
184 Q(ist, r) = oi_fnc_mean(Psir, Gfull, shp, stride, total) / G - 1;
188for ist = qList % BCMP queueing stations
189 Wq = oi_ld_table(D(ist, :), sn.nservers(ist), shp, total);
192 [~, Psir] = pfqn_oi_fnc(Wq, N, @(n) n(r));
193 Q(ist, r) = oi_fnc_mean(Psir, Gfull, shp, stride, total) / G - 1;
197for ist = find(isINF(:))
' % delay: Little's law
199 Q(ist, r) = X(r) * V(ist, r) * ST(ist, r);
203% ---- throughput, utilization, response time per station --------------------
209 T(ist, r) = X(r) * V(ist, r);
212for ist = find(isINF(:))
'
213 U(ist, :) = Q(ist, :); % INF utilization convention
215for ist = qList % BCMP queue: offered-load per server
216 c = sn.nservers(ist);
217 if ~isfinite(c) || c <= 0, c = 1; end
219 U(ist, r) = X(r) * D(ist, r) / c;
222for m = 1:numel(oiList)
223 % In-service utilization U_r = E[sir_r]/c, with sir_r the number of class-r
224 % jobs receiving a strictly positive rank rate. E[sir_r | n] = g(n,r) is a
225 % function of the count vector (PFQN_OI_INSVC), so its mean is read off the
226 % functional-server identity E[f(n)] = G^{+}/G - 1 of PFQN_OI_FNC. This
227 % matches the exact CTMC and LDES convention; it coincides with the
228 % offered-load form T/mu(e_r)/c only when a job engages a single server.
230 S = sn.nservers(ist);
231 if ~isfinite(S) || S <= 0, S = 1; end
232 Phi = oi_phi(rates{m}, N);
233 gins = pfqn_oi_insvc(rates{m}, N);
236 fr = @(n) gins(1 + sum(n .* stride), r);
237 [~, Psir] = pfqn_oi_fnc(Phi, N, fr);
238 U(ist, r) = (oi_fnc_mean(Psir, Gfull, shp, stride, total) / G - 1) / S;
245 R(ist, r) = Q(ist, r) / T(ist, r);
250C = zeros(1, K); % per-class system response time
257runtime = toc(Tstart);
260% ==========================================================================
261function [shp, stride, total] = oi_lattice(N)
262% Column-major lattice descriptor for populations 0 <= n <= N.
263N = round(N(:)'); R = numel(N); shp = N + 1;
265for d = 2:R, stride(d) = stride(d-1) * shp(d-1); end
269% ==========================================================================
270function c = oi_microstate(n)
271% Canonical ordered microstate holding n_r copies of
class r (n a
count
272% vector). For an order-independent station
the service rate
is invariant to
273%
the ordering, so
this representative suffices to evaluate svcRateFun(c).
274c = repelem(1:numel(n), round(n));
277% ==========================================================================
278function n = oi_sub(i, shp)
279% Decode linear index i (1-based) to
the subscript vector n (0-based counts).
280R = numel(shp); n = zeros(1, R); li = i - 1;
281for d = 1:R, n(d) = mod(li, shp(d)); li = floor(li / shp(d)); end
284% ==========================================================================
285function Phi = oi_phi(oirate, N)
286% Forward balanced-fairness fill of
the OI balance function over
the lattice:
287% Phi(0)=1, Phi(n) = (1/mu(n)) sum_{r: n_r>0} Phi(n - e_r).
288[shp, stride, total] = oi_lattice(N);
289Phiv = zeros(total, 1); R = numel(shp);
292 if sum(n) == 0, Phiv(i) = 1;
continue, end
294 for r = 1:R,
if n(r) > 0, s = s + Phiv(i - stride(r)); end, end
295 Phiv(i) = s / oirate(n);
297if R == 1, Phi = Phiv;
else, Phi = reshape(Phiv, shp); end
300% ==========================================================================
301function W = oi_ld_table(Dq, c, shp, total)
302% BCMP load-dependent weight table over
the lattice:
303% W(n) = (sum n)!/prod(n_r!) * prod_r D_r^{n_r} / prod_{k=1}^{sum n} beta(k),
304% beta(k) = min(k,c)
for a c-server queue (c=1 -> single server, beta==1).
305% Column-major flat vector. W(0)=1.
306Dq = Dq(:)
'; R = numel(shp); W = zeros(total, 1);
307if ~isfinite(c) || c <= 0, c = 1; end
311 logf = gammaln(tot + 1); ok = true;
314 if Dq(r) <= 0, ok = false; break, end
315 logf = logf + n(r) * log(Dq(r)) - gammaln(n(r) + 1);
318 if ~ok, continue, end
320 logf = logf - log(min(k, c));
326% ==========================================================================
327function Cv = oi_conv(Av, Bv, shp, stride, total)
328% Lattice convolution Cv(m) = sum_{0<=a<=m} Av(a) Bv(m-a) over 0..N.
330subs = zeros(total, R);
331for i = 1:total, subs(i, :) = oi_sub(i, shp); end
334 m = subs(i, :); acc = 0;
338 acc = acc + Av(j) * Bv(1 + sum((m - a) .* stride));
345% ==========================================================================
346function val = oi_fnc_mean(Psi, Gfull, shp, stride, total)
347% G^{+} = sum_{0<=b<=N} Psi(b) G(N-b): the FNC of the target station convolved
348% against the full-network normalizing-constant table, evaluated at n = N.
349N = shp - 1; Psiv = Psi(:); val = 0;
351 if Psiv(i) == 0, continue, end
353 val = val + Psiv(i) * Gfull(1 + sum((N - b) .* stride));