LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
pfqn_mvaoi.m
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)
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_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.
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 < 5
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);
80for i = 1:K
81 if ~isa(mu{i}, 'function_handle')
82 line_error(mfilename, 'each mu{i} must be a function handle mu_i(n).');
83 end
84end
85
86R = numel(N);
87N = round(N(:)');
88Z = Z(:)';
89if any(~isfinite(N))
90 line_error(mfilename, 'pfqn_mvaoi requires finite (closed) populations.');
91end
92if isempty(Dli)
93 Dli = zeros(0, R);
94end
95J = size(Dli, 1);
96I = eye(R);
97
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)
104for i = 1:K
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');
108end
109
110zeroS = zeros(K, R);
111k0 = statekey(zeroS, zeros(1,R));
112Xc(k0) = zeros(1, R);
113Qlc(k0) = zeros(J, R);
114for i = 1:K
115 Dc{i}(k0) = zeros(1, R);
116 Qc{i}(k0) = zeros(1, R);
117end
118
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).
124states = {};
125[si, nmat, SS, DD] = sprod(K+2, N);
126while si >= 0
127 states{end+1} = nmat(1:K+1, :); %#ok<AGROW>
128 [si, nmat] = sprod(si, SS, DD);
129end
130sums = cellfun(@(nm) sum(nm(K+1, :)), states);
131[~, ord] = sort(sums);
132states = states(ord);
133
134for p = 1:numel(states)
135 nm = states{p};
136 S = nm(1:K, :);
137 Nn = nm(K+1, :);
138 key = statekey(S, Nn);
139 if sum(Nn) == 0
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);
145 for i = 1:K
146 Dc{i}(key) = zeros(1, R);
147 Qc{i}(key) = zeros(1, R);
148 end
149 continue
150 end
151
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
155 for i = 1:K
156 Qsub{i} = zeros(R, R);
157 for r = 1:R
158 if Nn(r) == 0
159 continue
160 end
161 if Nn(r) == 1
162 mur = mu{i}(shiftrow(S, i, I(r,:)));
163 if mur > 0
164 Dt(i,r) = (1/mur) * rho_fn(i, r, S, oner(Nn,r), mu, Xc, Rc, I, R);
165 end
166 else
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)));
170 if xs2(r) > 0
171 Dprev = Dc{i}(statekey(S, oner(Nn,r)));
172 Dt(i,r) = (xs(r)/xs2(r)) * Dprev(r);
173 end
174 end
175 end
176 for s = 1:R
177 if Nn(s) > 0
178 Ss = S; Ss(i,:) = Ss(i,:) + I(s,:);
179 Qsub{i}(s,:) = Qc{i}(statekey(Ss, oner(Nn,s)));
180 end
181 end
182 end
183
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))
186 for r = 1:R
187 if Nn(r) == 0
188 continue
189 end
190 Qsub_li = Qlc(statekey(S, oner(Nn,r))); % J x R
191 for j = 1:J
192 betaLI(j,r) = Dli(j,r) * (1 + sum(Qsub_li(j,:)));
193 end
194 end
195
196 % Population conservation A X = Nn over classes with Nn_r > 0.
197 idx = find(Nn > 0);
198 m = numel(idx);
199 A = zeros(m, m);
200 for a = 1:m
201 r = idx(a);
202 for b = 1:m
203 s = idx(b);
204 if s == r
205 val = Z(r) + sum(betaLI(:,r));
206 for i = 1:K
207 val = val + Dt(i,r) * (1 + Qsub{i}(r,r));
208 end
209 A(a,b) = val;
210 else
211 val = 0;
212 for i = 1:K
213 val = val + Dt(i,s) * Qsub{i}(s,r);
214 end
215 A(a,b) = val;
216 end
217 end
218 end
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.
228 Xk = zeros(1, R);
229 for a = 1:m
230 r = idx(a);
231 denom = A(a,a);
232 for b = 1:m
233 if b ~= a
234 s = idx(b);
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)
237 if Xnes(r) > 0
238 denom = denom + A(a,b) * (Xner(s) / Xnes(r));
239 end
240 end
241 end
242 if denom > 0
243 Xk(r) = Nn(r) / denom;
244 end
245 end
246
247 % Assemble OI-station queues Q_i and LI-queue queues Qli.
248 Qk_li = zeros(J, R);
249 for r = 1:R
250 if Nn(r) == 0
251 continue
252 end
253 Qk_li(:,r) = Xk(r) * betaLI(:,r);
254 end
255 for i = 1:K
256 U = Dt(i,:) .* Xk; % 1 x R bottom-job utilization
257 Qi = zeros(1, R);
258 for r = 1:R
259 Qi(r) = U(r) + U * Qsub{i}(:,r);
260 end
261 Qc{i}(key) = Qi;
262 Dc{i}(key) = Dt(i,:);
263 end
264 Xc(key) = Xk;
265 Qlc(key) = Qk_li;
266end
267
268keyN = statekey(zeroS, N);
269X = Xc(keyN);
270Qoi = zeros(K, R);
271for i = 1:K
272 Qoi(i,:) = Qc{i}(keyN);
273end
274Qli = Qlc(keyN);
275Qdelay = X .* Z;
276
277if nargout >= 5
278 Soi = oi_insvc_means(Z, N, mu, Xc, zeroS, K, R);
279end
280end
281
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.
289shp = N + 1;
290stride = ones(1, R);
291for d = 2:R
292 stride(d) = stride(d-1) * shp(d-1);
293end
294total = prod(shp);
295subs = zeros(total, R);
296for i = 1:total
297 li = i - 1;
298 for d = 1:R
299 subs(i, d) = mod(li, shp(d));
300 li = floor(li / shp(d));
301 end
302end
303[~, ord] = sort(sum(subs, 2)); % process populations by increasing size
304
305% Cache X^{(0)}(k) over the lattice.
306Xk = zeros(total, R);
307for i = 1:total
308 Xk(i,:) = Xc(statekey(zeroS, subs(i,:)));
309end
310
311Soi = zeros(K, R);
312for m = 1:K
313 gm = pfqn_oi_insvc(mu{m}, N); % E[sir_r | n] over the lattice
314 muv = zeros(total, 1);
315 for i = 1:total
316 if sum(subs(i,:)) > 0
317 muv(i) = mu{m}(subs(i,:));
318 end
319 end
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
323 for bb = 1:total
324 b = ord(bb);
325 k = subs(b, :);
326 if sum(k) == 0
327 continue
328 end
329 acc0 = 0;
330 for aa = 1:total
331 a = ord(aa);
332 n = subs(a, :);
333 if sum(n) == 0 || any(n > k)
334 continue
335 end
336 if muv(a) <= 0
337 continue
338 end
339 acc = 0;
340 for r = 1:R
341 if n(r) > 0
342 acc = acc + Xk(b, r) * pMv(a - stride(r), b - stride(r));
343 end
344 end
345 pMv(a, b) = acc / muv(a);
346 acc0 = acc0 + pMv(a, b);
347 end
348 pMv(1, b) = 1 - acc0; % empty-state probability by complement
349 end
350 idxN = 1 + sum(N .* stride);
351 for r = 1:R
352 Soi(m, r) = pMv(:, idxN)' * gm(:, r);
353 end
354end
355end
356
357% =========================================================================
358% Helper functions
359% =========================================================================
360
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);
366if isKey(Rc{i}, rkey)
367 rrow = Rc{i}(rkey);
368 if ~isnan(rrow(r))
369 rv = rrow(r);
370 return
371 end
372else
373 rrow = nan(1, R);
374end
375if sum(M) == 0
376 rv = 1.0;
377 rrow(r) = rv;
378 Rc{i}(rkey) = rrow;
379 return
380end
381rs = -1;
382for rss = 1:R
383 if rss ~= r && M(rss) > 0
384 rs = rss;
385 break
386 end
387end
388rxu = Xc(statekey(S, M));
389rSp = S; rSp(i,:) = rSp(i,:) + I(r,:);
390rxu2 = Xc(statekey(rSp, M));
391rratio = 0.0;
392if rxu2(rs) > 0
393 rratio = rxu(rs) / rxu2(rs);
394end
395rv = rho_fn(i, r, S, oner(M, rs), mu, Xc, Rc, I, R) * rratio;
396rrow(r) = rv;
397Rc{i}(rkey) = rrow;
398end
399
400function row = shiftrow(S, i, e)
401% Return the per-class occupancy row s_i + e for OI station i.
402row = S(i,:) + e;
403end
404
405function v = oner(v, r)
406% Decrement entry r of v by one.
407v(r) = v(r) - 1;
408end
409
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]);
413end
Definition Station.m:245