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% see _kb/03-api-layer.md (pfqn/ family: scaling, log-domain switches, dispatch gates)
151Lt = L ./ cpole; % q' x p, unit-pole intensities
152alpha = ones(1, p);
153alpha0 = ones(1, p);
154used = zeros(qd, 1); % sum_{k<j} alpha_k rhotilde_{ki} r_k, per queue
155etaMat = double(L ~= 0); % eta_{ki} = 1 iff rho_{ki} ~= 0 (eq. 5.46)
156for j = 1:p
157 Kj = N(j); lj = l(j);
158 denom = 1 - used; % 1 - sum_{k<j} rhobar_{ki}|z_k|
159 denom(denom <= 0) = eps;
160 e = Lt(:, j) ./ denom; % effective intensity per queue
161 posq = find(Lt(:, j) > 0);
162 aj = Inf;
163 if ~isempty(posq)
164 [es, ord] = sort(e(posq), 'descend');
165 qs = posq(ord); % original queue indices, sorted
166 cumrho = cumsum(es) ./ (1:numel(es))'; % rhobar_n (eq. 5.44)
167 for n = 1:numel(es)
168 qi = qs(n);
169 % N_{ij} = n - 1 + sum_{k>j} K_k eta_{k,qi} (eq. 5.43, m_i = 1)
170 Nn = n - 1 + sum(N(j+1:p) .* etaMat(qi, j+1:p));
171 if Nn <= 0
172 an = 1;
173 else
174 ll = (1:Nn)';
175 an = (prod((Kj + ll) ./ (Kj + 2 * lj * Kj + ll)))^(1 / (2 * lj * Kj));
176 end
177 aj = min(aj, an / cumrho(n));
178 end
179 end
180 if Z(j) > 0
181 aj = min(aj, Kj / Z(j)); % IS/Poisson term K_j/rho_{j0}
182 end
183 if ~isfinite(aj)
184 aj = 1; % chain with no demand anywhere
185 end
186 alpha(j) = aj;
187 alpha0(j) = exp(-aj * Z(j));
188 used = used + aj * Lt(:, j) * r(j); % deflate for subsequent chains
189end
190
191% context for the recursion (local functions, not nested, to avoid MATLAB
192% nested-function workspace sharing across recursive calls)
193ctx.N = N;
194ctx.l = l;
195ctx.r = r;
196ctx.p = p;
197ctx.arho0 = alpha .* Z; % 1 x p : alpha_j rho_{j0}
198ctx.rhoS = L .* alpha; % q' x p : alpha_j rho_{ji}
199ctx.q = qd;
200ctx.cpole = cpole; % q' x 1 : F_i pole locations
201ctx.numc = numc; % q' x 1 cell : F_i numerator coefficients
202ctx.chunk = 2e6; % vectorization chunk for the innermost sum
203
204% run the nested inversion on the scaled generating function -> gbar(K)
205gbar = clw_lld_invert(1, zeros(1, 0), ctx);
206
207% recovery g(K) = prod alpha0_j^{-1} prod alpha_j^{-K_j} gbar(K) (eq. 7.1)
208lG = log(gbar) + sum(ctx.arho0) - sum(N .* log(alpha));
209if lG > 709
210 G = Inf;
211else
212 G = exp(lG);
213end
214end
215
216% ---- one-dimensional lattice-Poisson inversion (CLW eq. 2.3), scaled ----
217% Extracts the coefficient of w_j^{K_j} from g^{(j)}, recursing on inner chains.
218function val = clw_lld_invert(j, wfixed, ctx)
219Kj = ctx.N(j); lj = ctx.l(j); rj = ctx.r(j);
220acc = 0;
221for k1 = 0:lj-1
222 ph = exp(-1i * pi * k1 / lj);
223 kk = (-Kj):(Kj-1);
224 signs = (-1) .^ kk;
225 theta = pi * (k1 + lj * kk) / (lj * Kj);
226 wj = rj * exp(1i * theta); % 1 x 2Kj contour points
227 if j == ctx.p
228 inner = 0;
229 nk = numel(wj);
230 for a = 1:ctx.chunk:nk
231 b = min(a + ctx.chunk - 1, nk);
232 W = [repmat(wfixed, b - a + 1, 1), wj(a:b).'];
233 fv = clw_lld_gbar(W, ctx);
234 inner = inner + sum(signs(a:b).' .* fv);
235 end
236 else
237 inner = 0;
238 for t = 1:numel(wj)
239 inner = inner + signs(t) * clw_lld_invert(j + 1, [wfixed, wj(t)], ctx);
240 end
241 end
242 acc = acc + ph * inner;
243end
244val = acc / (2 * lj * Kj * rj^Kj);
245if j == 1
246 val = real(val);
247end
248end
249
250% ---- scaled generating function Gbar evaluated at rows of W (n x p) ----
251function g = clw_lld_gbar(W, ctx)
252% Gbar(w) = exp( sum_j alpha_j rho_{j0} (w_j - 1) )
253% * prod_i F_i( sum_j alpha_j rho_{ji} w_j )
254% with F_i(x) = N_i(x)/(c_i - x) (Bertozzi-McKenna eq. 2.19); F_i(0) = 1.
255% Note exp(log a + log b) = a*b for the principal complex log, so branch
256% choices in the per-queue logs are immaterial.
257expo = (W - 1) * ctx.arho0.'; % n x 1
258X = W * ctx.rhoS.'; % n x q' contour arguments
259logF = zeros(size(W, 1), 1);
260for i = 1:ctx.q
261 xi = X(:, i);
262 a = ctx.numc{i};
263 num = a(end) * ones(size(xi)); % Horner on N_i(x)
264 for k = numel(a)-1:-1:1
265 num = num .* xi + a(k);
266 end
267 logF = logF + log(num) - log(ctx.cpole(i) - xi);
268end
269g = exp(expo + logF);
270end