LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
pfqn_mvaoi.m
1function [X, Qoi, Qli, Qdelay, Soi] = pfqn_mvaoi(Z, N, mu, Dli, visits, options)
2% [X, QOI, QLI, QDELAY, SOI] = PFQN_MVAOI(Z, N, MU, DLI, OPTIONS)
3%
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_NCOI 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.
17%
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.
36%
37% Parameters:
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).
46%
47% Returns:
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.
63%
64% Copyright (c) 2012-2026, Imperial College London
65% All rights reserved.
66
67if nargin < 4
68 Dli = [];
69end
70if nargin < 6
71 options = struct(); %#ok<NASGU>
72end
73if isa(mu, 'function_handle')
74 mu = {mu};
75end
76if ~iscell(mu) || isempty(mu)
77 line_error(mfilename, 'mu must be a (nonempty) cell of OI rate handles.');
78end
79K = numel(mu);
80% Per-OI-station class visit ratios v_{i,r}. They enter the class-r demand base
81% case theta_{i,r}(N_r=1)=v_{i,r}/mu_i(...) (eq mvaoi-D); the N_r>=2 ratio case
82% cancels visits. Default unit visits. ms-promoted OI stations pass ones here,
83% their visits already folded into the rate handle by the analyzer.
84if nargin < 5 || isempty(visits)
85 visits = repmat({ones(1, numel(N))}, 1, K);
86end
87for i = 1:K
88 if ~isa(mu{i}, 'function_handle')
89 line_error(mfilename, 'each mu{i} must be a function handle mu_i(n).');
90 end
91end
92
93R = numel(N);
94N = round(N(:)');
95Z = Z(:)';
96if any(~isfinite(N))
97 line_error(mfilename, 'pfqn_mvaoi requires finite (closed) populations.');
98end
99if isempty(Dli)
100 Dli = zeros(0, R);
101end
102J = size(Dli, 1);
103I = eye(R);
104
105% Caches keyed by the string of [S(:).' , Nn]. Reachable states only.
106Xc = containers.Map('KeyType','char','ValueType','any'); % X^{(S)}(Nn) 1xR
107Qlc = containers.Map('KeyType','char','ValueType','any'); % Qli^{(S)}(Nn) JxR
108Dc = cell(1, K); % Dc{i}: D_i^{(S)}(Nn) 1xR
109Qc = cell(1, K); % Qc{i}: Q_i^{(S)}(Nn) 1xR
110Rc = cell(1, K); % Rc{i}: rho_i^{(S)}(M) 1xR (NaN until computed)
111for i = 1:K
112 Dc{i} = containers.Map('KeyType','char','ValueType','any');
113 Qc{i} = containers.Map('KeyType','char','ValueType','any');
114 Rc{i} = containers.Map('KeyType','char','ValueType','any');
115end
116
117zeroS = zeros(K, R);
118k0 = statekey(zeroS, zeros(1,R));
119Xc(k0) = zeros(1, R);
120Qlc(k0) = zeros(J, R);
121for i = 1:K
122 Dc{i}(k0) = zeros(1, R);
123 Qc{i}(k0) = zeros(1, R);
124end
125
126% see _kb/03-api-layer.md (pfqn/ family: scaling, log-domain switches, dispatch gates)
127states = {};
128[si, nmat, SS, DD] = sprod(K+2, N);
129while si >= 0
130 states{end+1} = nmat(1:K+1, :); %#ok<AGROW>
131 [si, nmat] = sprod(si, SS, DD);
132end
133sums = cellfun(@(nm) sum(nm(K+1, :)), states);
134[~, ord] = sort(sums);
135states = states(ord);
136
137for p = 1:numel(states)
138 nm = states{p};
139 S = nm(1:K, :);
140 Nn = nm(K+1, :);
141 key = statekey(S, Nn);
142 if sum(Nn) == 0
143 % No free jobs: throughput and all queue-ahead terms vanish. This must
144 % be stored for EVERY shift S (not only S = 0) because Qsub references
145 % (S + e_s@i, Nn - e_s) which reaches such states when Nn = e_s.
146 Xc(key) = zeros(1, R);
147 Qlc(key) = zeros(J, R);
148 for i = 1:K
149 Dc{i}(key) = zeros(1, R);
150 Qc{i}(key) = zeros(1, R);
151 end
152 continue
153 end
154
155 % Per-OI-station class demands D_i and queue-ahead vectors Qsub_i.
156 Dt = zeros(K, R); % Dt(i,r)
157 Qsub = cell(1, K); % Qsub{i}(s,r) = Q_i^{(S+e_s@i)}(Nn - e_s), comp r
158 for i = 1:K
159 Qsub{i} = zeros(R, R);
160 for r = 1:R
161 if Nn(r) == 0
162 continue
163 end
164 if Nn(r) == 1
165 mur = mu{i}(shiftrow(S, i, I(r,:)));
166 if mur > 0
167 Dt(i,r) = (visits{i}(r)/mur) * rho_fn(i, r, S, oner(Nn,r), mu, Xc, Rc, I, R);
168 end
169 else
170 Sp = S; Sp(i,:) = Sp(i,:) + I(r,:);
171 xs = Xc(statekey(S, oner(Nn,r)));
172 xs2 = Xc(statekey(Sp, oner(Nn,r)));
173 if xs2(r) > 0
174 Dprev = Dc{i}(statekey(S, oner(Nn,r)));
175 Dt(i,r) = (xs(r)/xs2(r)) * Dprev(r);
176 end
177 end
178 end
179 for s = 1:R
180 if Nn(s) > 0
181 Ss = S; Ss(i,:) = Ss(i,:) + I(s,:);
182 Qsub{i}(s,:) = Qc{i}(statekey(Ss, oner(Nn,s)));
183 end
184 end
185 end
186
187 % LI-queue arrival-theorem coefficients beta_j,r and aggregate per class.
188 betaLI = zeros(J, R); % beta_j,r = D_j,r (1 + sum_s Qli_j,s(Nn - e_r))
189 for r = 1:R
190 if Nn(r) == 0
191 continue
192 end
193 Qsub_li = Qlc(statekey(S, oner(Nn,r))); % J x R
194 for j = 1:J
195 betaLI(j,r) = Dli(j,r) * (1 + sum(Qsub_li(j,:)));
196 end
197 end
198
199 % Population conservation A X = Nn over classes with Nn_r > 0.
200 idx = find(Nn > 0);
201 m = numel(idx);
202 A = zeros(m, m);
203 for a = 1:m
204 r = idx(a);
205 for b = 1:m
206 s = idx(b);
207 if s == r
208 val = Z(r) + sum(betaLI(:,r));
209 for i = 1:K
210 val = val + Dt(i,r) * (1 + Qsub{i}(r,r));
211 end
212 A(a,b) = val;
213 else
214 val = 0;
215 for i = 1:K
216 val = val + Dt(i,s) * Qsub{i}(s,r);
217 end
218 A(a,b) = val;
219 end
220 end
221 end
222 % see _kb/03-api-layer.md (pfqn/ family: scaling, log-domain switches, dispatch gates)
223 Xk = zeros(1, R);
224 for a = 1:m
225 r = idx(a);
226 denom = A(a,a);
227 for b = 1:m
228 if b ~= a
229 s = idx(b);
230 Xner = Xc(statekey(S, oner(Nn,r))); % X(S, Nn-e_r)
231 Xnes = Xc(statekey(S, oner(Nn,s))); % X(S, Nn-e_s)
232 if Xnes(r) > 0
233 denom = denom + A(a,b) * (Xner(s) / Xnes(r));
234 end
235 end
236 end
237 if denom > 0
238 Xk(r) = Nn(r) / denom;
239 end
240 end
241
242 % Assemble OI-station queues Q_i and LI-queue queues Qli.
243 Qk_li = zeros(J, R);
244 for r = 1:R
245 if Nn(r) == 0
246 continue
247 end
248 Qk_li(:,r) = Xk(r) * betaLI(:,r);
249 end
250 for i = 1:K
251 U = Dt(i,:) .* Xk; % 1 x R bottom-job utilization
252 Qi = zeros(1, R);
253 for r = 1:R
254 Qi(r) = U(r) + U * Qsub{i}(:,r);
255 end
256 Qc{i}(key) = Qi;
257 Dc{i}(key) = Dt(i,:);
258 end
259 Xc(key) = Xk;
260 Qlc(key) = Qk_li;
261end
262
263keyN = statekey(zeroS, N);
264X = Xc(keyN);
265Qoi = zeros(K, R);
266for i = 1:K
267 Qoi(i,:) = Qc{i}(keyN);
268end
269Qli = Qlc(keyN);
270Qdelay = X .* Z;
271
272if nargout >= 5
273 Soi = oi_insvc_means(Z, N, mu, Xc, zeroS, K, R);
274end
275end
276
277% =========================================================================
278function Soi = oi_insvc_means(Z, N, mu, Xc, zeroS, K, R)
279% Mean number of in-service jobs per class at each OI station, E[sir_r], from
280% the OI count marginal pM_i(n|k) built on the cached zero-shift throughputs
281% X^{(0)}(k). Exact because in product form
282% 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
283% recursion below reproduces the balanced-fairness identity for Phi_i.
284shp = N + 1;
285stride = ones(1, R);
286for d = 2:R
287 stride(d) = stride(d-1) * shp(d-1);
288end
289total = prod(shp);
290subs = zeros(total, R);
291for i = 1:total
292 li = i - 1;
293 for d = 1:R
294 subs(i, d) = mod(li, shp(d));
295 li = floor(li / shp(d));
296 end
297end
298[~, ord] = sort(sum(subs, 2)); % process populations by increasing size
299
300% Cache X^{(0)}(k) over the lattice.
301Xk = zeros(total, R);
302for i = 1:total
303 Xk(i,:) = Xc(statekey(zeroS, subs(i,:)));
304end
305
306Soi = zeros(K, R);
307for m = 1:K
308 gm = pfqn_oi_insvc(mu{m}, N); % E[sir_r | n] over the lattice
309 muv = zeros(total, 1);
310 for i = 1:total
311 if sum(subs(i,:)) > 0
312 muv(i) = mu{m}(subs(i,:));
313 end
314 end
315 % pMv(a,b) = pM_m(n_a | k_b), filled for n_a <= k_b by increasing sum(k).
316 pMv = zeros(total, total);
317 pMv(1,1) = 1; % pM(0|0) = 1
318 for bb = 1:total
319 b = ord(bb);
320 k = subs(b, :);
321 if sum(k) == 0
322 continue
323 end
324 acc0 = 0;
325 for aa = 1:total
326 a = ord(aa);
327 n = subs(a, :);
328 if sum(n) == 0 || any(n > k)
329 continue
330 end
331 if muv(a) <= 0
332 continue
333 end
334 acc = 0;
335 for r = 1:R
336 if n(r) > 0
337 acc = acc + Xk(b, r) * pMv(a - stride(r), b - stride(r));
338 end
339 end
340 pMv(a, b) = acc / muv(a);
341 acc0 = acc0 + pMv(a, b);
342 end
343 pMv(1, b) = 1 - acc0; % empty-state probability by complement
344 end
345 idxN = 1 + sum(N .* stride);
346 for r = 1:R
347 Soi(m, r) = pMv(:, idxN)' * gm(:, r);
348 end
349end
350end
351
352% =========================================================================
353% Helper functions
354% =========================================================================
355
356function rv = rho_fn(i, r, S, M, mu, Xc, Rc, I, R)
357% rho_{i,r}^{(S)}(M): recursion on M (class r held fixed, M_r stays 0). NOTE all
358% local names are r-prefixed to avoid corrupting the recursion via shared
359% storage in nested calls.
360rkey = statekey(S, M);
361if isKey(Rc{i}, rkey)
362 rrow = Rc{i}(rkey);
363 if ~isnan(rrow(r))
364 rv = rrow(r);
365 return
366 end
367else
368 rrow = nan(1, R);
369end
370if sum(M) == 0
371 rv = 1.0;
372 rrow(r) = rv;
373 Rc{i}(rkey) = rrow;
374 return
375end
376rs = -1;
377for rss = 1:R
378 if rss ~= r && M(rss) > 0
379 rs = rss;
380 break
381 end
382end
383rxu = Xc(statekey(S, M));
384rSp = S; rSp(i,:) = rSp(i,:) + I(r,:);
385rxu2 = Xc(statekey(rSp, M));
386rratio = 0.0;
387if rxu2(rs) > 0
388 rratio = rxu(rs) / rxu2(rs);
389end
390rv = rho_fn(i, r, S, oner(M, rs), mu, Xc, Rc, I, R) * rratio;
391rrow(r) = rv;
392Rc{i}(rkey) = rrow;
393end
394
395function row = shiftrow(S, i, e)
396% Return the per-class occupancy row s_i + e for OI station i.
397row = S(i,:) + e;
398end
399
400function v = oner(v, r)
401% Decrement entry r of v by one.
402v(r) = v(r) - 1;
403end
404
405function key = statekey(S, Nn)
406% String key for the lattice state (S, Nn); S is K x R, Nn is 1 x R.
407key = sprintf('%d_', [reshape(S.', 1, []), Nn]);
408end