1function [X, Qoi, Qli, Qdelay, Soi] = pfqn_mvaoi(Z, N, mu, Dli, options)
2% [X, QOI, QLI, QDELAY, SOI] = PFQN_MVAOI(Z, N, MU, DLI, OPTIONS)
4% Mean-value analysis of a closed product-form queueing network composed of an
5% aggregated infinite-server (delay) node, any number of load-independent (LI)
6% single-server product-form queues, and any number of order-independent (OI) /
7% pass-and-swap stations with empty swap graph. This
is the mean-value
8% counterpart of PFQN_OI_NC and
the marginal-distribution form PFQN_MVAOI_MARG:
9% it returns
the same exact per-
class throughput and queue-lengths but WITHOUT
10% computing any normalizing constant or joint marginal,
using only mean
11% quantities (throughputs, demands, queue-lengths) evaluated on shifted models.
12% It
is the composition-dependent generalization of
the Conditional MVA (CMVA)
13% of Casale,
"A Note on Stable Flow-Equivalent Aggregation in Closed Networks"
14% (QUESTA 2009), whose
"third form" (rate depending on
the full per-
class
15% occupancy vector)
is realized here, extended to MULTIPLE OI stations by
16% carrying one rate-shift vector s_i per OI station i.
18% Throughout, r and s index job classes; i indexes OI stations; j indexes LI
19% queues. For a single OI station and no LI queue
the analysis recurs on
the
20% shift vector s_i (
the OI occupancy already committed at
the bottom of station
21% i), Nn = N - s_i
the jobs still to distribute:
22% Q^{(S)}(Nn) = sum_r U_r^{(S)}(Nn) ( e_r + Q^{(S+e_r@i)}(Nn - e_r) ),
23% U_r^{(S)}(Nn) = D_r^{(S)}(Nn) X_r^{(S)}(Nn) (bottom-job utilization),
24% with
the class-r OI demand and throughput satisfying
25% D_r^{(S)}(Nn) = (1/mu_i(s_i+e_r)) rho_{i,r}^{(S)}(Nn-e_r), Nn_r = 1,
26% D_r^{(S)}(Nn) = [X_r^{(S)}(Nn-e_r)/X_r^{(S+e_r@i)}(Nn-e_r)] D_r^{(S)}(Nn-e_r), Nn_r >= 2,
27% rho_{i,r}^{(S)}(M) = rho_{i,r}^{(S)}(M-e_s) X_s^{(S)}(M)/X_s^{(S+e_r@i)}(M), rho(0)=1, s ~= r,
28% and X_r^{(S)}(Nn) closed by population conservation. With K OI stations
the
29% shift becomes a K x R matrix S (row i = s_i); each OI station keeps its own
30% D^i, rho^i and Q^i recursions driven by
the common throughput X^{(S)}(Nn), and
31%
the conservation identity aggregates every station
's contribution:
32% Nn_r = X_r Z_r + sum_j Q^{(j)}_r + sum_i Q^{(i)}_r,
33% where the LI queue Q^{(j)}_r = X_r D_{j,r} (1 + sum_s Q^{(j)}_s(Nn - e_r)) is
34% the standard arrival-theorem term. States (S, Nn) are processed by increasing
35% sum(Nn) so every reference lands at a strictly smaller free population.
38% Z - (1 x R) think-time demand vector of the aggregated delay node.
39% N - (1 x R) closed population vector, finite.
40% mu - cell array {mu_1,...,mu_K} of function handles; mu_i(n) returns the OI
41% total service rate of station i for per-class occupancy n (1 x R). A
42% bare function handle is accepted as the single-station shorthand.
43% Dli - (J x R) per-class demand matrix of the LI single-server queues
44% (D_{j,r} = V_{j,r}/rate_{j,r}); empty or omitted when J = 0.
45% options - solver options (optional, currently unused).
48% X - (1 x R) per-class throughput X_r = G(N-e_r)/G(N).
49% Qoi - (K x R) per-class mean queue-length at each OI station (row i).
50% Qli - (J x R) per-class mean queue-length at each LI queue (row j).
51% Qdelay - (1 x R) per-class mean queue-length at the delay node (X.*Z).
52% Soi - (K x R) per-class mean number of IN-SERVICE jobs at each OI station,
53% i.e. E[sir_r] with sir_r the count of class-r jobs receiving a
54% strictly positive rank rate (see PFQN_OI_INSVC); the utilization of
55% OI station i is Soi(i,r)/c_i. Unlike X/Qoi/Qli, which are pure
56% mean-value quantities, Soi is a distributional statistic and is
57% therefore obtained from the OI count marginal
58% pM_i(n|k) = (1/mu_i(n)) sum_r X_r(k) pM_i(n-e_r|k-e_r),
59% pM_i(0|k) = 1 - sum_{n ~= 0} pM_i(n|k),
60% which is assembled here from the zero-shift throughputs X^{(0)}(k)
61% already cached by the mean-value recursion above (no normalizing
62% constant is formed). It is only computed when requested.
64% Copyright (c) 2012-2026, Imperial College London
71 options = struct(); %#ok<NASGU>
73if isa(mu, 'function_handle
')
76if ~iscell(mu) || isempty(mu)
77 line_error(mfilename, 'mu must be a (nonempty) cell of OI rate handles.
');
81 if ~isa(mu{i}, 'function_handle
')
82 line_error(mfilename, 'each mu{i} must be a function handle mu_i(n).
');
90 line_error(mfilename, 'pfqn_mvaoi requires finite (closed) populations.
');
98% Caches keyed by the string of [S(:).' , Nn]. Reachable states only.
99Xc = containers.Map(
'KeyType',
'char',
'ValueType',
'any'); % X^{(S)}(Nn) 1xR
100Qlc = containers.Map(
'KeyType',
'char',
'ValueType',
'any'); % Qli^{(S)}(Nn) JxR
101Dc = cell(1, K); % Dc{i}: D_i^{(S)}(Nn) 1xR
102Qc = cell(1, K); % Qc{i}: Q_i^{(S)}(Nn) 1xR
103Rc = cell(1, K); % Rc{i}: rho_i^{(S)}(M) 1xR (NaN until computed)
105 Dc{i} = containers.Map(
'KeyType',
'char',
'ValueType',
'any');
106 Qc{i} = containers.Map(
'KeyType',
'char',
'ValueType',
'any');
107 Rc{i} = containers.Map(
'KeyType',
'char',
'ValueType',
'any');
111k0 = statekey(zeroS, zeros(1,R));
113Qlc(k0) = zeros(J, R);
115 Dc{i}(k0) = zeros(1, R);
116 Qc{i}(k0) = zeros(1, R);
119% Enumerate all reachable (S, Nn) with, per
class r, sum_i s_i,r + Nn_r <= N_r.
120% A slack bucket (
the last row) absorbs
the remaining N_r, so every (K+2) x R
121% matrix produced by sprod has
column sums N: rows 1..K are
the OI shift vectors
122% s_1..s_K, row K+1
is the free vector Nn, and
the last (slack) row
is dropped.
123% States are then processed by increasing sum(Nn).
125[si, nmat, SS, DD] = sprod(K+2, N);
127 states{end+1} = nmat(1:K+1, :); %#ok<AGROW>
128 [si, nmat] = sprod(si, SS, DD);
130sums = cellfun(@(nm) sum(nm(K+1, :)), states);
131[~, ord] = sort(sums);
134for p = 1:numel(states)
138 key = statekey(S, Nn);
140 % No free jobs: throughput and all queue-ahead terms vanish. This must
141 % be stored
for EVERY shift S (not only S = 0) because Qsub references
142 % (S + e_s@i, Nn - e_s) which reaches such states when Nn = e_s.
143 Xc(key) = zeros(1, R);
144 Qlc(key) = zeros(J, R);
146 Dc{i}(key) = zeros(1, R);
147 Qc{i}(key) = zeros(1, R);
152 % Per-OI-station
class demands D_i and queue-ahead vectors Qsub_i.
153 Dt = zeros(K, R); % Dt(i,r)
154 Qsub = cell(1, K); % Qsub{i}(s,r) = Q_i^{(S+e_s@i)}(Nn - e_s), comp r
156 Qsub{i} = zeros(R, R);
162 mur = mu{i}(shiftrow(S, i, I(r,:)));
164 Dt(i,r) = (1/mur) * rho_fn(i, r, S, oner(Nn,r), mu, Xc, Rc, I, R);
167 Sp = S; Sp(i,:) = Sp(i,:) + I(r,:);
168 xs = Xc(statekey(S, oner(Nn,r)));
169 xs2 = Xc(statekey(Sp, oner(Nn,r)));
171 Dprev = Dc{i}(statekey(S, oner(Nn,r)));
172 Dt(i,r) = (xs(r)/xs2(r)) * Dprev(r);
178 Ss = S; Ss(i,:) = Ss(i,:) + I(s,:);
179 Qsub{i}(s,:) = Qc{i}(statekey(Ss, oner(Nn,s)));
184 % LI-queue arrival-theorem coefficients beta_j,r and aggregate per
class.
185 betaLI = zeros(J, R); % beta_j,r = D_j,r (1 + sum_s Qli_j,s(Nn - e_r))
190 Qsub_li = Qlc(statekey(S, oner(Nn,r))); % J x R
192 betaLI(j,r) = Dli(j,r) * (1 + sum(Qsub_li(j,:)));
196 % Population conservation A X = Nn over classes with Nn_r > 0.
205 val = Z(r) + sum(betaLI(:,r));
207 val = val + Dt(i,r) * (1 + Qsub{i}(r,r));
213 val = val + Dt(i,s) * Qsub{i}(s,r);
219 % Throughput closure. The OI queue recurrence couples X_s (s~=r) via
the
220 % off-diagonal A_{r,s}, so
the population-conservation identity A X = Nn
is
221 % not a per-
class Little's-law ratio as in
the canonical CMVA. Rather than
222 % solving
the linear system, we decouple it with
the product-form throughput
223 % ratio identity (valid at fixed shift S)
224 % X_s(S,Nn)/X_r(S,Nn) = X_s(S,Nn-e_r)/X_r(S,Nn-e_s),
225 % which yields
the scalar per-
class recurrence
226 % X_r = Nn_r / ( A_{r,r} + sum_{s~=r} A_{r,s} X_s(S,Nn-e_r)/X_r(S,Nn-e_s) )
227 % from same-shift one-job-less throughputs already in
the cache.
235 Xner = Xc(statekey(S, oner(Nn,r))); % X(S, Nn-e_r)
236 Xnes = Xc(statekey(S, oner(Nn,s))); % X(S, Nn-e_s)
238 denom = denom + A(a,b) * (Xner(s) / Xnes(r));
243 Xk(r) = Nn(r) / denom;
247 % Assemble OI-station queues Q_i and LI-queue queues Qli.
253 Qk_li(:,r) = Xk(r) * betaLI(:,r);
256 U = Dt(i,:) .* Xk; % 1 x R bottom-job utilization
259 Qi(r) = U(r) + U * Qsub{i}(:,r);
262 Dc{i}(key) = Dt(i,:);
268keyN = statekey(zeroS, N);
272 Qoi(i,:) = Qc{i}(keyN);
278 Soi = oi_insvc_means(Z, N, mu, Xc, zeroS, K, R);
282% =========================================================================
283function Soi = oi_insvc_means(Z, N, mu, Xc, zeroS, K, R)
284% Mean number of in-service jobs per
class at each OI station, E[sir_r], from
285%
the OI
count marginal pM_i(n|k) built on
the cached zero-shift throughputs
286% X^{(0)}(k). Exact because in product form
287% pM_i(n|k) = Phi_i(n) G_{-i}(k-n)/G(k) and X_r(k) = G(k-e_r)/G(k), so
the
288% recursion below reproduces
the balanced-fairness identity
for Phi_i.
292 stride(d) = stride(d-1) * shp(d-1);
295subs = zeros(total, R);
299 subs(i, d) = mod(li, shp(d));
300 li = floor(li / shp(d));
303[~, ord] = sort(sum(subs, 2)); % process populations by increasing size
305% Cache X^{(0)}(k) over
the lattice.
308 Xk(i,:) = Xc(statekey(zeroS, subs(i,:)));
313 gm = pfqn_oi_insvc(mu{m}, N); % E[sir_r | n] over
the lattice
314 muv = zeros(total, 1);
316 if sum(subs(i,:)) > 0
317 muv(i) = mu{m}(subs(i,:));
320 % pMv(a,b) = pM_m(n_a | k_b), filled
for n_a <= k_b by increasing sum(k).
321 pMv = zeros(total, total);
322 pMv(1,1) = 1; % pM(0|0) = 1
333 if sum(n) == 0 || any(n > k)
342 acc = acc + Xk(b, r) * pMv(a - stride(r), b - stride(r));
345 pMv(a, b) = acc / muv(a);
346 acc0 = acc0 + pMv(a, b);
348 pMv(1, b) = 1 - acc0; % empty-state probability by complement
350 idxN = 1 + sum(N .* stride);
352 Soi(m, r) = pMv(:, idxN)
' * gm(:, r);
357% =========================================================================
359% =========================================================================
361function rv = rho_fn(i, r, S, M, mu, Xc, Rc, I, R)
362% rho_{i,r}^{(S)}(M): recursion on M (class r held fixed, M_r stays 0). NOTE all
363% local names are r-prefixed to avoid corrupting the recursion via shared
364% storage in nested calls.
365rkey = statekey(S, M);
383 if rss ~= r && M(rss) > 0
388rxu = Xc(statekey(S, M));
389rSp = S; rSp(i,:) = rSp(i,:) + I(r,:);
390rxu2 = Xc(statekey(rSp, M));
393 rratio = rxu(rs) / rxu2(rs);
395rv = rho_fn(i, r, S, oner(M, rs), mu, Xc, Rc, I, R) * rratio;
400function row = shiftrow(S, i, e)
401% Return the per-class occupancy row s_i + e for OI station i.
405function v = oner(v, r)
406% Decrement entry r of v by one.
410function key = statekey(S, Nn)
411% String key for the lattice state (S, Nn); S is K x R, Nn is 1 x R.
412key = sprintf('%d_
', [reshape(S.', 1, []), Nn]);