4 % @brief Choudhury-Leung-Whitt normalization constant by numerical inversion
5 % of
the generating function, extended to limited load-dependent (LLD)
6 % stations via
the per-center transforms of Bertozzi and McKenna
7 % (SIAM Review 35(2):239-268, 1993).
13 % @brief Computes
the normalization constant g(K) of a multichain closed
14 % product-form network with limited load-dependent (LLD) stations and
15 % (optionally) infinite-server delay by numerically inverting its
16 % p-dimensional generating function.
18 % The generating function
is (Bertozzi-McKenna eqs. 2.17/2.23)
20 % G(z) = exp( sum_j rho_{j0} z_j ) prod_i F_i( sum_j rho_{ji} z_j )
22 % where F_i
is the transform of
the station
factor of queue i (eq. 2.16),
24 % F_i(x) = sum_{n>=0} x^n / prod_{k=1}^n S_i(k),
26 % and S_i(k) = mu(i,k)
is the load-dependent rate scaling with k jobs at
27 % queue i. For an LLD queue, S_i(k) = c_i constant
for k >= l_i, and F_i
28 %
is the rational function (eq. 2.19)
30 % F_i(x) = [ c_i + sum_{n=1}^{l_i-1} (c_i - S_i(n))
31 % / prod_{k=1}^n S_i(k) * x^n ] / (c_i - x),
33 % analytic except
for a simple pole at x = c_i. Multiserver (c_i = number
34 % of servers) and load-independent (F_i = 1/(1-x)) queues are special
35 % cases. Since g(K) depends on S_i(k) only
for k <= sum(K), any general
36 % load-dependent input
is truncated to LLD at l_i <= sum(K) without loss
39 % g(K)
is the coefficient of prod_j z_j^{K_j}, recovered by p nested
40 % one-dimensional lattice-Poisson inversions (CLW, JACM 42(5):935-970,
41 % 1995, eq. 2.3) with a restrictive static scaling adapted from CLW eqs.
42 % 5.41-5.46: each queue
is normalized by its pole c_i (unit-pole form,
43 % simple pole) and log-domain recovery (eq. 7.1)
is applied.
45 % @fn pfqn_clw_lld(L, N, Z, mu, options)
46 % @param L (q
' x p) single-server relative traffic intensities, L(i,j)=rho_{ji}.
47 % @param N (1 x p) closed-chain population vector K.
48 % @param Z (1 x p) aggregate infinite-server relative intensities rho_{j0}
49 % (think-time term). Default: zeros(1,p).
50 % @param mu (q' x n) load-dependent rate scalings, mu(i,k) = S_i(k);
if
51 % fewer than sum(N) columns are given
the last
column is
52 % extended (LLD assumption). Default: ones (all queues
54 % @param options
struct with optional fields:
55 % .l (1 x p) inner lattice parameters l_j (roundoff control).
56 % .gamma (1 x p) aliasing parameters gamma_j (aliasing ~ 10^-gamma_j).
57 % Defaults follow CLW: l_1=1,g_1=11; l_2=l_3=2,g=13; l_j>=4=3,g=15.
58 % @return G Normalization constant g(K). Inf if it overflows double range.
59 % @return lG Natural logarithm of g(K) (always finite).
61 % Scope: cost
is prod_j 2 l_j K_j contour points, each requiring O(sum_i l_i)
62 % work, so
the routine
is practical for moderate populations and few chains.
63 % The numerator polynomials are evaluated in double precision; extreme LLD
64 % cutoffs (l_i > ~170 with large c_i) may overflow.
67function [G, lG] = pfqn_clw_lld(L, N, Z, mu, options)
69if nargin < 3 || isempty(Z)
75if nargin < 4 || isempty(mu)
76 mu = ones(qd, max(Ntot, 1));
84 G = 0; lG = -Inf; return
90% extend/truncate mu to sum(N) columns (LLD extension of last column)
92 mu = [mu, repmat(mu(:, end), 1, Ntot - size(mu, 2))];
97 line_error(mfilename, 'Load-dependent rates mu(i,k) must be positive.');
100% default lattice/aliasing parameters (CLW Section 2.2, page 962)
101if isfield(options, 'l') && ~isempty(options.l)
106 if p >= 2, l(2) = 2; end
107 if p >= 3, l(3) = 2; end
109if isfield(options, 'gamma') && ~isempty(options.gamma)
110 gam = options.gamma(:).';
112 gam = 15 * ones(1, p);
114 if p >= 2, gam(2) = 13; end
115 if p >= 3, gam(3) = 13; end
118% drop zero-population chains: the coefficient of z_j^0 equals the pgf
119% restricted to z_j = 0, so chain j is removed exactly
128% pole c_i and LLD cutoff l_i of each queue: S_i(k) = c_i for k >= l_i
130numc = cell(qd, 1); % numerator coefficients [a_0 ... a_{l_i-1}]
132 last = find(mu(i, :) ~= cpole(i), 1, 'last');
134 li = 1; % load-independent up to a constant rate c_i
141 cp = cumprod(mu(i, 1:li-1)); % prod_{k=1}^n S_i(k)
142 a(2:li) = (cpole(i) - mu(i, 1:li-1)) ./ cp;
147% contour radii r_j = 10^{-gamma_j/(2 l_j K_j)} (CLW eq. 2.7)
148r = 10 .^ (-gam ./ (2 * l .* N));
150% restrictive static scaling (CLW eqs. 5.41-5.46) on the unit-pole form:
151% each queue is normalized by its pole, rhotilde_{ji} = rho_{ji}/c_i, so the
152% denominator factor of F_i behaves as a simple pole at 1 (m_i = 1). The
153% outer contour variables are evaluated at |z_k| = r_k (most restrictive
154% point); the constraint sum_k alpha_k rhotilde_{ki} r_k < 1 keeps every
155% contour argument strictly inside the disc of analyticity of F_i.
156Lt = L ./ cpole; % q' x p, unit-pole intensities
159used = zeros(qd, 1); % sum_{k<j} alpha_k rhotilde_{ki} r_k, per queue
160etaMat = double(L ~= 0); % eta_{ki} = 1 iff rho_{ki} ~= 0 (eq. 5.46)
162 Kj = N(j); lj = l(j);
163 denom = 1 - used; % 1 - sum_{k<j} rhobar_{ki}|z_k|
164 denom(denom <= 0) = eps;
165 e = Lt(:, j) ./ denom; % effective intensity per queue
166 posq = find(Lt(:, j) > 0);
169 [es, ord] = sort(e(posq), 'descend');
170 qs = posq(ord); % original queue indices, sorted
171 cumrho = cumsum(es) ./ (1:numel(es))'; % rhobar_n (eq. 5.44)
174 % N_{ij} = n - 1 + sum_{k>j} K_k eta_{k,qi} (eq. 5.43, m_i = 1)
175 Nn = n - 1 + sum(N(j+1:p) .* etaMat(qi, j+1:p));
180 an = (prod((Kj + ll) ./ (Kj + 2 * lj * Kj + ll)))^(1 / (2 * lj * Kj));
182 aj = min(aj, an / cumrho(n));
186 aj = min(aj, Kj / Z(j)); % IS/Poisson term K_j/rho_{j0}
189 aj = 1; % chain with no demand anywhere
192 alpha0(j) = exp(-aj * Z(j));
193 used = used + aj * Lt(:, j) * r(j); % deflate for subsequent chains
196% context for the recursion (local functions, not nested, to avoid MATLAB
197% nested-function workspace sharing across recursive calls)
202ctx.arho0 = alpha .* Z; % 1 x p : alpha_j rho_{j0}
203ctx.rhoS = L .* alpha; % q' x p : alpha_j rho_{ji}
205ctx.cpole = cpole; % q' x 1 : F_i pole locations
206ctx.numc = numc; % q' x 1 cell : F_i numerator coefficients
207ctx.chunk = 2e6; % vectorization chunk for the innermost sum
209% run the nested inversion on the scaled generating function -> gbar(K)
210gbar = clw_lld_invert(1, zeros(1, 0), ctx);
212% recovery g(K) = prod alpha0_j^{-1} prod alpha_j^{-K_j} gbar(K) (eq. 7.1)
213lG = log(gbar) + sum(ctx.arho0) - sum(N .* log(alpha));
221% ---- one-dimensional lattice-Poisson inversion (CLW eq. 2.3), scaled ----
222% Extracts
the coefficient of w_j^{K_j} from g^{(j)}, recursing on inner chains.
223function val = clw_lld_invert(j, wfixed, ctx)
224Kj = ctx.N(j); lj = ctx.l(j); rj = ctx.r(j);
227 ph = exp(-1i * pi * k1 / lj);
230 theta = pi * (k1 + lj * kk) / (lj * Kj);
231 wj = rj * exp(1i * theta); % 1 x 2Kj contour points
235 for a = 1:ctx.chunk:nk
236 b = min(a + ctx.chunk - 1, nk);
237 W = [repmat(wfixed, b - a + 1, 1), wj(a:b).
'];
238 fv = clw_lld_gbar(W, ctx);
239 inner = inner + sum(signs(a:b).' .* fv);
244 inner = inner + signs(t) * clw_lld_invert(j + 1, [wfixed, wj(t)], ctx);
247 acc = acc + ph * inner;
249val = acc / (2 * lj * Kj * rj^Kj);
255% ---- scaled generating function Gbar evaluated at rows of W (n x p) ----
256function g = clw_lld_gbar(W, ctx)
257% Gbar(w) = exp( sum_j alpha_j rho_{j0} (w_j - 1) )
258% * prod_i F_i( sum_j alpha_j rho_{ji} w_j )
259% with F_i(x) = N_i(x)/(c_i - x) (Bertozzi-McKenna eq. 2.19); F_i(0) = 1.
260% Note exp(log a + log b) = a*b
for the principal complex log, so branch
261% choices in
the per-queue logs are immaterial.
262expo = (W - 1) * ctx.arho0.
'; % n x 1
263X = W * ctx.rhoS.'; % n x q
' contour arguments
264logF = zeros(size(W, 1), 1);
268 num = a(end) * ones(size(xi)); % Horner on N_i(x)
269 for k = numel(a)-1:-1:1
270 num = num .* xi + a(k);
272 logF = logF + log(num) - log(ctx.cpole(i) - xi);