LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
pfqn_clw_lld.m
1%{
2%{
3 % @file pfqn_clw_lld.m
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).
8%}
9%}
10
11%{
12%{
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.
17 %
18 % The generating function is (Bertozzi-McKenna eqs. 2.17/2.23)
19 %
20 % G(z) = exp( sum_j rho_{j0} z_j ) prod_i F_i( sum_j rho_{ji} z_j )
21 %
22 % where F_i is the transform of the station factor of queue i (eq. 2.16),
23 %
24 % F_i(x) = sum_{n>=0} x^n / prod_{k=1}^n S_i(k),
25 %
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)
29 %
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),
32 %
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
37 % of exactness.
38 %
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.
44 %
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
53 % load-independent).
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).
60 %
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.
65%}
66%}
67function [G, lG] = pfqn_clw_lld(L, N, Z, mu, options)
68[qd, p] = size(L);
69if nargin < 3 || isempty(Z)
70 Z = zeros(1, p);
71end
72N = N(:).';
73Z = Z(:).';
74Ntot = sum(N);
75if nargin < 4 || isempty(mu)
76 mu = ones(qd, max(Ntot, 1));
77end
78if nargin < 5
79 options = struct();
80end
81
82% trivial populations
83if any(N < 0)
84 G = 0; lG = -Inf; return
85end
86if all(N == 0)
87 G = 1; lG = 0; return
88end
89
90% extend/truncate mu to sum(N) columns (LLD extension of last column)
91if size(mu, 2) < Ntot
92 mu = [mu, repmat(mu(:, end), 1, Ntot - size(mu, 2))];
93else
94 mu = mu(:, 1:Ntot);
95end
96if any(mu(:) <= 0)
97 line_error(mfilename, 'Load-dependent rates mu(i,k) must be positive.');
98end
99
100% default lattice/aliasing parameters (CLW Section 2.2, page 962)
101if isfield(options, 'l') && ~isempty(options.l)
102 l = options.l(:).';
103else
104 l = 3 * ones(1, p);
105 l(1) = 1;
106 if p >= 2, l(2) = 2; end
107 if p >= 3, l(3) = 2; end
108end
109if isfield(options, 'gamma') && ~isempty(options.gamma)
110 gam = options.gamma(:).';
111else
112 gam = 15 * ones(1, p);
113 gam(1) = 11;
114 if p >= 2, gam(2) = 13; end
115 if p >= 3, gam(3) = 13; end
116end
117
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
120keep = N > 0;
121L = L(:, keep);
122N = N(keep);
123Z = Z(keep);
124l = l(keep);
125gam = gam(keep);
126p = numel(N);
127
128% pole c_i and LLD cutoff l_i of each queue: S_i(k) = c_i for k >= l_i
129cpole = mu(:, end);
130numc = cell(qd, 1); % numerator coefficients [a_0 ... a_{l_i-1}]
131for i = 1:qd
132 last = find(mu(i, :) ~= cpole(i), 1, 'last');
133 if isempty(last)
134 li = 1; % load-independent up to a constant rate c_i
135 else
136 li = last + 1;
137 end
138 a = zeros(1, li);
139 a(1) = cpole(i);
140 if li > 1
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;
143 end
144 numc{i} = a;
145end
146
147% contour radii r_j = 10^{-gamma_j/(2 l_j K_j)} (CLW eq. 2.7)
148r = 10 .^ (-gam ./ (2 * l .* N));
149
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
157alpha = ones(1, p);
158alpha0 = ones(1, p);
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)
161for j = 1:p
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);
167 aj = Inf;
168 if ~isempty(posq)
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)
172 for n = 1:numel(es)
173 qi = qs(n);
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));
176 if Nn <= 0
177 an = 1;
178 else
179 ll = (1:Nn)';
180 an = (prod((Kj + ll) ./ (Kj + 2 * lj * Kj + ll)))^(1 / (2 * lj * Kj));
181 end
182 aj = min(aj, an / cumrho(n));
183 end
184 end
185 if Z(j) > 0
186 aj = min(aj, Kj / Z(j)); % IS/Poisson term K_j/rho_{j0}
187 end
188 if ~isfinite(aj)
189 aj = 1; % chain with no demand anywhere
190 end
191 alpha(j) = aj;
192 alpha0(j) = exp(-aj * Z(j));
193 used = used + aj * Lt(:, j) * r(j); % deflate for subsequent chains
194end
195
196% context for the recursion (local functions, not nested, to avoid MATLAB
197% nested-function workspace sharing across recursive calls)
198ctx.N = N;
199ctx.l = l;
200ctx.r = r;
201ctx.p = p;
202ctx.arho0 = alpha .* Z; % 1 x p : alpha_j rho_{j0}
203ctx.rhoS = L .* alpha; % q' x p : alpha_j rho_{ji}
204ctx.q = qd;
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
208
209% run the nested inversion on the scaled generating function -> gbar(K)
210gbar = clw_lld_invert(1, zeros(1, 0), ctx);
211
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));
214if lG > 709
215 G = Inf;
216else
217 G = exp(lG);
218end
219end
220
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);
225acc = 0;
226for k1 = 0:lj-1
227 ph = exp(-1i * pi * k1 / lj);
228 kk = (-Kj):(Kj-1);
229 signs = (-1) .^ kk;
230 theta = pi * (k1 + lj * kk) / (lj * Kj);
231 wj = rj * exp(1i * theta); % 1 x 2Kj contour points
232 if j == ctx.p
233 inner = 0;
234 nk = numel(wj);
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);
240 end
241 else
242 inner = 0;
243 for t = 1:numel(wj)
244 inner = inner + signs(t) * clw_lld_invert(j + 1, [wfixed, wj(t)], ctx);
245 end
246 end
247 acc = acc + ph * inner;
248end
249val = acc / (2 * lj * Kj * rj^Kj);
250if j == 1
251 val = real(val);
252end
253end
254
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);
265for i = 1:ctx.q
266 xi = X(:, i);
267 a = ctx.numc{i};
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);
271 end
272 logF = logF + log(num) - log(ctx.cpole(i) - xi);
273end
274g = exp(expo + logF);
275end
Definition Station.m:245