LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_clw.h
Go to the documentation of this file.
1/*
2 * Copyright (c) 2012-2026, QORE Lab, Imperial College London
3 * All rights reserved.
4 */
5#ifndef LINE_API_PFQN_CLW_H
6#define LINE_API_PFQN_CLW_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Choudhury-Leung-Whitt normalization constant by numerical inversion of the
12 * generating function (JACM 42(5):935-970, 1995), and its limited
13 * load-dependent extension through the per-center transforms of Bertozzi and
14 * McKenna (SIAM Review 35(2):239-268, 1993).
15 *
16 * Templated port of matlab/src/api/pfqn/pfqn_clw.m and pfqn_clw_lld.m.
17 *
18 * The generating function of g(K) is (CLW eq. 4.5)
19 *
20 * G(z) = exp( sum_j rho_{j0} z_j ) / prod_i ( 1 - sum_j rho_{ji} z_j )^{m_i}
21 *
22 * and, with limited load-dependent stations (Bertozzi-McKenna 2.17/2.23),
23 *
24 * G(z) = exp( sum_j rho_{j0} z_j ) prod_i F_i( sum_j rho_{ji} z_j ),
25 * F_i(x) = [ c_i + sum_{n=1}^{l_i-1} (c_i - S_i(n)) / prod_{k<=n} S_i(k) x^n ]
26 * / (c_i - x),
27 *
28 * analytic except for a simple pole at x = c_i. g(K) is the coefficient of
29 * prod_j z_j^{K_j}, recovered by p NESTED one-dimensional lattice-Poisson
30 * inversions (eq. 2.3) on contours of radius r_j = 10^{-gamma_j/(2 l_j K_j)},
31 * with the restrictive static scaling of eqs. 5.41-5.46 and log-domain
32 * recovery (eq. 7.1). pfqn_clw applies both of the paper's speed-ups: dimension
33 * reduction by decomposition (Sec. 3, Sec. 5.4), which inverts the subset D
34 * minimizing |D| + max_i |S_i(D)| (eq. 3.3) and then each connected component of
35 * the remainder separately, and Euler summation of the inner sums (Sec. 2.4,
36 * eq. 2.22), which replaces 2 l_j K_j contour points by 2 l_j (n+m+1) wherever
37 * K_j > n+m, refining m until |E(m,n) - E(m,n+1)| settles. pfqn_clw_lld keeps
38 * the plain nested inversion of cost prod_j 2 l_j K_j.
39 *
40 * COMPLEX ARITHMETIC WITHOUT std::complex. The contour integrand is genuinely
41 * complex, and std::complex is specified only for float, double and long
42 * double; instantiating it on a Boost.Multiprecision number is unspecified
43 * behavior. detail::Cx<T> below is a two-field complex with the six operations
44 * this routine needs, so the Real backends get real high precision on the
45 * inversion rather than silently falling back to double.
46 *
47 * Arithmetic: TRANSCENDENTAL, double and Real only. exp, log, atan2, sqrt and
48 * a fractional power all appear; the contour radius alone is 10^{-gamma/(2 l
49 * K)}, which is not in the field of the inputs. This is the one member of the
50 * normalizing-constant family that CANNOT be instantiated exactly, and that is
51 * intrinsic to inverting a generating function numerically, not an artifact of
52 * the port. Accuracy against the exact pfqn_ca on the models tested is ~4e-10
53 * for p = 2, ~7e-7 for p = 3, matching the reference's own claim of about 1e-9
54 * and confirming that the port reproduces the reference's aliasing rather than
55 * adding error of its own.
56 *
57 * REFERENCE DEFECTS
58 *
59 * 1. pfqn_clw returns NaN when SOME chain has zero population. The contour
60 * count is 2 l_j K_j, so a chain with K_j = 0 makes the final division
61 * acc / (2 l_j K_j r_j^{K_j}) a 0/0. pfqn_clw_lld guards against exactly
62 * this by dropping the zero-population chains up front ("the coefficient of
63 * z_j^0 equals the pgf restricted to z_j = 0, so chain j is removed
64 * exactly"); pfqn_clw never received that guard. Reproduce with
65 * pfqn_clw([0.1 0.2; 0.3 0.05], [2 0], [1.0 0.5]), which returns NaN where
66 * the answer is the p = 1 constant. THIS PORT APPLIES THE GUARD to both
67 * routines, so pfqn_clw here returns the finite value; that is the only
68 * input on which the two disagree.
69 * 2. Dead code in both routines: alpha0_j = exp(-alpha_j rho_{j0}) is computed
70 * in the scaling loop and never read. The recovery (eq. 7.1) is written in
71 * terms of sum_j alpha_j rho_{j0}, i.e. -sum_j log alpha0_j, so the array
72 * is redundant rather than wrong. It is not carried here.
73 * 3. `denom(denom <= 0) = eps` in the scaling loop silently substitutes
74 * 2.2e-16 for a nonpositive deflated denominator, which turns a chain whose
75 * predecessors have already saturated a queue into an enormous effective
76 * intensity rather than reporting the saturation. Reproduced, because the
77 * scaling only has to keep the contour inside the disc of analyticity and
78 * the recovery divides the choice back out, so the result is unaffected;
79 * but it is a silent branch, not a designed one.
80 */
81
82#include <algorithm>
83#include <cmath>
84#include <cstddef>
85#include <limits>
86#include <vector>
87
89#include "line/num/number.h"
90#include "line/util/error.h"
91#include "line/util/matrix.h"
92
93namespace line {
94namespace pfqn {
95
96/** Return value of pfqn_clw and pfqn_clw_lld, mirroring [G, lG]. */
97template <class T>
98struct ClwResult {
99 T G; ///< normalization constant, +infinity when it overflows the range of T
100 T lG; ///< its natural logarithm, always finite
101};
102
103/** Optional lattice and aliasing parameters; empty means "use the CLW defaults". */
105 std::vector<int> l; ///< inner lattice parameters l_j (roundoff control)
106 std::vector<double> gamma; ///< aliasing parameters, aliasing ~ 10^-gamma_j
107 bool euler = true; ///< Euler-sum the inner sums where K_j > eulerN + eulerM
108 int eulerN = 11; ///< terms summed exactly before averaging (n in eq. 2.22)
109 int eulerM = 20; ///< starting order of the averaging (m in eq. 2.22)
110 double eulerTol = 1e-10; ///< relative tolerance on |E(m,n) - E(m,n+1)|
111 int eulerMaxM = 160; ///< largest Euler order reached by doubling
112 bool dimred = true; ///< dimension reduction by decomposition (Section 3)
113 int dimredMaxD = 4; ///< largest |D| examined when minimizing (3.3)
114 std::vector<double> beta; ///< multipliers on alpha_j, the manual tuning of page 956
115};
116
117namespace detail {
118
119/**
120 * Cx<T>, cx_add, cx_mul, cx_scale, cx_div and cx_expi come from
121 * pfqn_asympt_common.h, which introduced the same two-field complex for the
122 * Norlund-Rice integrands. Only the three operations that inversion needs and
123 * that file does not have are added here.
124 */
125template <class T>
126Cx<T> cx_sub(const Cx<T>& a, const Cx<T>& b) {
127 return Cx<T>(T(a.re - b.re), T(a.im - b.im));
128}
129
130/** Principal complex logarithm, log|z| + i arg z. */
131template <class T>
132Cx<T> cx_log(const Cx<T>& a) {
133 using std::atan2;
134 using std::log;
135 using std::sqrt;
136 const T mod = sqrt(T(a.re * a.re + a.im * a.im));
137 return Cx<T>(T(log(mod)), T(atan2(a.im, a.re)));
138}
139
140template <class T>
141Cx<T> cx_exp(const Cx<T>& a) {
142 using std::cos;
143 using std::exp;
144 using std::sin;
145 const T e = exp(a.re);
146 return Cx<T>(T(e * cos(a.im)), T(e * sin(a.im)));
147}
148
149/**
150 * x^y for positive x. std::pow, not exp(y log x): the latter costs a couple of
151 * ulps on the contour radius r_j = 10^{-gamma_j/(2 l_j K_j)}, and the inversion
152 * is an alternating sum that amplifies exactly that kind of error.
153 */
154template <class T>
155T clw_pow_real(const T& x, const T& y) {
156 using std::pow;
157 return T(pow(x, y));
158}
159
160/** The page-956 scale multipliers beta_j, in the retained chain index space. */
161template <class T>
162std::vector<T> clw_beta(const std::vector<std::size_t>& keep, const ClwOptions& opt) {
163 std::vector<T> beta(keep.size(), num_traits<T>::from_int(1));
164 if (opt.beta.empty()) return beta;
165 for (std::size_t j = 0; j < keep.size(); ++j) {
166 if (keep[j] >= opt.beta.size()) throw InputError("pfqn_clw: options.beta has the wrong length");
167 beta[j] = num_traits<T>::from_double(opt.beta[keep[j]]);
168 }
169 return beta;
170}
171
172/**
173 * Inversion order of the variables: the subset D of the interdependence graph
174 * that is inverted first, then the connected components of what is left
175 * (CLW Section 3). Indices are into the retained chains.
176 */
177struct ClwPlan {
178 std::vector<std::size_t> D;
179 std::vector<std::vector<std::size_t>> comps;
180};
181
182/** Connected components of adj with the nodes in mask removed. */
183inline std::vector<std::vector<std::size_t>> clw_components(
184 const std::vector<std::vector<char>>& adj, const std::vector<char>& mask) {
185 const std::size_t p = adj.size();
186 std::vector<long> lab(p, -1);
187 long nc = 0;
188 for (std::size_t s = 0; s < p; ++s) {
189 if (mask[s] || lab[s] >= 0) continue;
190 lab[s] = nc;
191 std::vector<std::size_t> stack(1, s);
192 while (!stack.empty()) {
193 const std::size_t v = stack.back();
194 stack.pop_back();
195 for (std::size_t u = 0; u < p; ++u)
196 if (adj[v][u] && !mask[u] && lab[u] < 0) {
197 lab[u] = nc;
198 stack.push_back(u);
199 }
200 }
201 ++nc;
202 }
203 std::vector<std::vector<std::size_t>> out(static_cast<std::size_t>(nc));
204 for (std::size_t j = 0; j < p; ++j)
205 if (lab[j] >= 0) out[static_cast<std::size_t>(lab[j])].push_back(j);
206 return out;
207}
208
209inline double clw_binom(std::size_t n, std::size_t k) {
210 double v = 1.0;
211 for (std::size_t i = 1; i <= k; ++i) v = v * static_cast<double>(n - k + i) / static_cast<double>(i);
212 return v;
213}
214
215/**
216 * The interdependence graph of the factors of (4.5) and the subset D minimizing
217 * the inversion dimension |D| + max_i |S_i(D)| (eqs. 3.1-3.3), by enumeration in
218 * increasing cardinality. A plan of dimension p is no reduction at all, and is
219 * returned as the single all-chain component so the recursion is unchanged.
220 */
221template <class T>
222ClwPlan clw_plan(const Matrix<T>& L, const ClwOptions& opt) {
223 const std::size_t qd = L.rows(), p = L.cols();
224 ClwPlan trivial;
225 trivial.comps.resize(1);
226 for (std::size_t j = 0; j < p; ++j) trivial.comps[0].push_back(j);
227 if (!opt.dimred || p <= 2) return trivial;
228 const T zero = num_traits<T>::from_int(0);
229 std::vector<std::vector<char>> adj(p, std::vector<char>(p, 0));
230 for (std::size_t i = 0; i < qd; ++i)
231 for (std::size_t a = 0; a < p; ++a) {
232 if (L(i, a) == zero) continue;
233 for (std::size_t b = 0; b < p; ++b)
234 if (b != a && L(i, b) != zero) adj[a][b] = 1; // each factor is a clique
235 }
236 const std::vector<char> none(p, 0);
237 std::vector<std::vector<std::size_t>> bestC = clw_components(adj, none);
238 std::size_t best = 0;
239 for (std::size_t c = 0; c < bestC.size(); ++c) best = std::max(best, bestC[c].size());
240 std::vector<std::size_t> bestD;
241 const std::size_t maxd = std::min<std::size_t>(
242 (opt.dimredMaxD > 0) ? static_cast<std::size_t>(opt.dimredMaxD) : 0, p - 1);
243 for (std::size_t dd = 1; dd <= maxd; ++dd) {
244 if (dd >= best) break; // dimension is at least |D|
245 if (clw_binom(p, dd) > 2e5) break; // (3.3) is solved by enumeration only
246 std::vector<std::size_t> sub(dd);
247 for (std::size_t t = 0; t < dd; ++t) sub[t] = t;
248 while (true) {
249 std::vector<char> mask(p, 0);
250 for (std::size_t t = 0; t < dd; ++t) mask[sub[t]] = 1;
251 const std::vector<std::vector<std::size_t>> cc = clw_components(adj, mask);
252 std::size_t mx = 0;
253 for (std::size_t c = 0; c < cc.size(); ++c) mx = std::max(mx, cc[c].size());
254 if (dd + mx < best) {
255 best = dd + mx;
256 bestD = sub;
257 bestC = cc;
258 }
259 std::size_t t = dd;
260 while (t-- > 0 && sub[t] == p - dd + t) {
261 }
262 if (t >= dd) break; // wrapped: enumeration exhausted
263 ++sub[t];
264 for (std::size_t u = t + 1; u < dd; ++u) sub[u] = sub[u - 1] + 1;
265 }
266 }
267 if (best >= p) return trivial;
268 ClwPlan plan;
269 plan.D = bestD;
270 plan.comps = bestC;
271 return plan;
272}
273
274/**
275 * The CLW lattice and aliasing parameters, defaulted by INVERSION DEPTH: the
276 * dimension reduction sets the order of the variables (Section 5.4), and every
277 * component restarts at depth |D|+1 because the components are inverted in
278 * parallel. depth is 1-based and indexed by retained chain; keep maps retained
279 * chains back to the caller's index space, which is where opt.l lives.
280 */
281inline void clw_defaults(std::size_t pfull, const std::vector<std::size_t>& keep,
282 const std::vector<std::size_t>& depth, const ClwOptions& opt,
283 std::vector<int>& l, std::vector<double>& gam) {
284 const std::size_t p = keep.size();
285 l.assign(p, 3);
286 gam.assign(p, 15.0);
287 for (std::size_t j = 0; j < p; ++j) {
288 if (depth[j] == 1) {
289 l[j] = 1;
290 gam[j] = 11.0;
291 } else if (depth[j] <= 3) {
292 l[j] = 2;
293 gam[j] = 13.0;
294 }
295 }
296 if (!opt.l.empty()) {
297 if (opt.l.size() != pfull) throw InputError("pfqn_clw: options.l has the wrong length");
298 for (std::size_t j = 0; j < p; ++j) l[j] = opt.l[keep[j]];
299 }
300 if (!opt.gamma.empty()) {
301 if (opt.gamma.size() != pfull)
302 throw InputError("pfqn_clw: options.gamma has the wrong length");
303 for (std::size_t j = 0; j < p; ++j) gam[j] = opt.gamma[keep[j]];
304 }
305 for (std::size_t j = 0; j < p; ++j)
306 if (l[j] < 1) throw InputError("pfqn_clw: the lattice parameters must be positive");
307}
308
309/**
310 * Positional form of the defaults, for the members of the family that invert in
311 * chain order and take no plan: pfqn_clwoi, pfqn_clwjd and the pfqn_ncld cost
312 * estimate.
313 */
314inline void clw_defaults(std::size_t p, const ClwOptions& opt, std::vector<int>& l,
315 std::vector<double>& gam) {
316 std::vector<std::size_t> keep(p, 0), depth(p, 0);
317 for (std::size_t j = 0; j < p; ++j) {
318 keep[j] = j;
319 depth[j] = j + 1;
320 }
321 clw_defaults(p, keep, depth, opt, l, gam);
322}
323
324/**
325 * The restrictive static scaling of CLW eqs. 5.41-5.46.
326 *
327 * @param Lsc intensities the constraint is phrased on (rho for pfqn_clw, the
328 * unit-pole rho/c_i for pfqn_clw_lld)
329 * @param mult per-queue multiplicity entering N_{ij}; all ones in the LLD form,
330 * where every F_i contributes a single pole
331 * @param Lraw the unscaled demands, before the chain-level scaling
332 * @param N (R) population per chain
333 * @param Z (R) think times
334 * @param l per-chain station index list
335 * @param r per-chain scaling factors
336 */
337template <class T>
338std::vector<T> clw_scaling(const Matrix<T>& Lsc, const Matrix<T>& Lraw, const std::vector<int>& N,
339 const std::vector<T>& Z, const std::vector<int>& l,
340 const std::vector<T>& r, const std::vector<long>& mult,
341 const std::vector<std::size_t>& order, const std::vector<T>& beta) {
342 const std::size_t qd = Lsc.rows(), p = Lsc.cols();
343 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
344 const T eps = num_traits<T>::from_double(std::numeric_limits<double>::epsilon());
345 std::vector<T> alpha(p, one), used(qd, zero);
346 // in inversion order, which is what the dimension reduction changes (Sec. 5.4)
347 for (std::size_t t = 0; t < p; ++t) {
348 const std::size_t j = order[t];
349 const long Kj = N[j], lj = l[j];
350 if (Kj == 0) continue; // empty chain: no lattice, and 2*lj*Kj = 0 below
351 std::vector<std::size_t> posq;
352 std::vector<T> e(qd, zero);
353 for (std::size_t i = 0; i < qd; ++i) {
354 T den = one - used[i];
355 if (!(den > zero)) den = eps;
356 e[i] = Lsc(i, j) / den;
357 if (Lsc(i, j) > zero) posq.push_back(i);
358 }
359 bool have = false;
360 T aj = zero;
361 if (!posq.empty()) {
362 std::stable_sort(posq.begin(), posq.end(),
363 [&](std::size_t a, std::size_t b) { return e[b] < e[a]; });
364 T cum = zero;
365 long cummb = 0;
366 for (std::size_t n = 0; n < posq.size(); ++n) {
367 const std::size_t qi = posq[n];
368 cum += e[qi];
369 cummb += mult[qi];
370 const T rhobar = cum / num_traits<T>::from_int(static_cast<long>(n) + 1);
371 long Nn = cummb - 1;
372 for (std::size_t u = t + 1; u < p; ++u)
373 if (Lraw(qi, order[u]) != zero) Nn += N[order[u]];
374 T an = one;
375 if (Nn > 0) {
376 // in the log domain: the product runs over N_{ij} factors below
377 // one and underflows to zero at a few hundred of them, which
378 // would silently set alpha_j = 0 and lG = NaN
379 using std::exp;
380 using std::log;
381 T lp = zero;
382 for (long ll = 1; ll <= Nn; ++ll)
383 lp += log(T(num_traits<T>::from_int(Kj + ll) /
384 num_traits<T>::from_int(Kj + 2 * lj * Kj + ll)));
385 an = exp(T(lp / num_traits<T>::from_int(2 * lj * Kj)));
386 }
387 const T cand = an / rhobar;
388 if (!have || cand < aj) {
389 aj = cand;
390 have = true;
391 }
392 }
393 }
394 if (Z[j] > zero) {
395 const T cand = num_traits<T>::from_int(Kj) / Z[j];
396 if (!have || cand < aj) {
397 aj = cand;
398 have = true;
399 }
400 }
401 if (!have) aj = one; // chain with no demand anywhere
402 alpha[j] = T(beta[j] * aj);
403 for (std::size_t i = 0; i < qd; ++i) used[i] += alpha[j] * Lsc(i, j) * r[j];
404 }
405 return alpha;
406}
407
408/** Scaling in chain order with no page-956 tuning, for the same three callers. */
409template <class T>
410std::vector<T> clw_scaling(const Matrix<T>& Lsc, const Matrix<T>& Lraw, const std::vector<int>& N,
411 const std::vector<T>& Z, const std::vector<int>& l,
412 const std::vector<T>& r, const std::vector<long>& mult) {
413 std::vector<std::size_t> order(Lsc.cols(), 0);
414 for (std::size_t j = 0; j < order.size(); ++j) order[j] = j;
415 return clw_scaling(Lsc, Lraw, N, Z, l, r, mult, order,
416 std::vector<T>(order.size(), num_traits<T>::from_int(1)));
417}
418
419/**
420 * Euler weights of eq. (2.22): E(m,n) = sum_i w_i (-1)^i a_i.
421 *
422 * E(m,n) = 2^-m sum_{k=0}^{m} C(m,k) S_{n+k} with S_t = sum_{i<=t} (-1)^i a_i,
423 * so a_i carries the mass of every partial sum that contains it.
424 */
425template <class T>
426std::vector<T> clw_euler_weights(int n, int mm) {
427 const T one = num_traits<T>::from_int(1);
428 std::vector<T> b(static_cast<std::size_t>(mm) + 1, one);
429 for (int k = 1; k <= mm; ++k)
430 b[static_cast<std::size_t>(k)] =
431 T(b[static_cast<std::size_t>(k) - 1] * num_traits<T>::from_int(mm - k + 1) /
432 num_traits<T>::from_int(k)); // C(mm,k)
433 const T scale = T(one / num_pow_int(num_traits<T>::from_int(2), static_cast<unsigned>(mm)));
434 for (int k = 0; k <= mm; ++k) b[static_cast<std::size_t>(k)] *= scale;
435 std::vector<T> tail(static_cast<std::size_t>(mm) + 1, num_traits<T>::from_int(0));
436 T acc = num_traits<T>::from_int(0);
437 for (int k = mm; k >= 0; --k) {
438 acc += b[static_cast<std::size_t>(k)];
439 tail[static_cast<std::size_t>(k)] = acc; // 2^-mm sum_{j>=k} C(mm,j)
440 }
441 std::vector<T> w(static_cast<std::size_t>(n + mm) + 1, one);
442 for (int i = n + 1; i <= n + mm; ++i)
443 w[static_cast<std::size_t>(i)] = tail[static_cast<std::size_t>(i - n)];
444 return w;
445}
446
447/**
448 * The inner sum of (2.3) over the lattice index k, with Euler summation.
449 *
450 * The sum splits at k = 0 into two nearly alternating series (Section 2.4);
451 * each is replaced by its Euler sum (eq. 2.22). The order m is doubled until the
452 * paper's own estimate |E(m,n) - E(m,n+1)| falls under the tolerance, and the
453 * exact sum is taken once n+m reaches K_j, so accuracy is not traded away.
454 * `ev(k)` returns the inverted function at the lattice point of index k.
455 */
456template <class T, class EV>
457Cx<T> clw_inner(long Kj, const ClwOptions& opt, const EV& ev) {
458 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
459 using std::sqrt;
460 int mCur = opt.eulerM;
461 while (true) {
462 const long TT = static_cast<long>(opt.eulerN) + mCur;
463 if (!opt.euler || Kj <= TT + 1) {
464 Cx<T> s(zero, zero);
465 for (long k = -Kj; k <= Kj - 1; ++k)
466 s = cx_add(s, cx_scale(ev(k), (k % 2 == 0) ? one : T(-one)));
467 return s;
468 }
469 const std::vector<T> w1 = clw_euler_weights<T>(opt.eulerN, mCur);
470 const std::vector<T> w2 = clw_euler_weights<T>(opt.eulerN + 1, mCur);
471 Cx<T> e1(zero, zero), e2(zero, zero);
472 for (long s = 0; s <= TT + 1; ++s) {
473 const Cx<T> dv = cx_sub(ev(s), ev(-(s + 1)));
474 const T sgn = (s % 2 == 0) ? one : T(-one);
475 if (s <= TT) e1 = cx_add(e1, cx_scale(dv, T(sgn * w1[static_cast<std::size_t>(s)])));
476 e2 = cx_add(e2, cx_scale(dv, T(sgn * w2[static_cast<std::size_t>(s)])));
477 }
478 const Cx<T> df = cx_sub(e1, e2);
479 const T dn = sqrt(T(df.re * df.re + df.im * df.im));
480 const T en = sqrt(T(e2.re * e2.re + e2.im * e2.im));
481 if (dn <= num_traits<T>::from_double(opt.eulerTol) * en || mCur >= opt.eulerMaxM) return e2;
482 mCur *= 2;
483 }
484}
485
486/** Everything the nested inversion needs besides the contour point itself. */
487template <class T>
488struct ClwCtx {
489 const std::vector<int>* N;
490 const std::vector<int>* l;
491 const std::vector<T>* r;
492 const ClwPlan* plan;
493 const ClwOptions* opt;
494 T pi;
495};
496
497template <class T, class F>
498Cx<T> clw_invert_d(std::size_t t, std::vector<Cx<T>>& w, const ClwCtx<T>& cx, const F& gbar);
499
500/**
501 * One lattice-Poisson inversion (CLW eq. 2.3), scaled: extracts the coefficient
502 * of w_j^{K_j} from whatever `next` evaluates at the contour points.
503 */
504template <class T, class NEXT>
505Cx<T> clw_lattice(std::size_t j, std::vector<Cx<T>>& w, const ClwCtx<T>& cx, const NEXT& next) {
506 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
507 const long Kj = (*cx.N)[j], lj = (*cx.l)[j];
508 const T rj = (*cx.r)[j];
509 if (Kj == 0) {
510 // [w_j^0] Gbar = Gbar(w_j=0): the K=0 lattice is the single point 0, and
511 // exp(-arho0_j) there cancels the +arho0_j added back into lG
512 w[j] = Cx<T>(zero, zero);
513 return next();
514 }
515 Cx<T> acc(zero, zero);
516 for (long k1 = 0; k1 < lj; ++k1) {
517 const Cx<T> ph =
518 cx_expi(T(-cx.pi * num_traits<T>::from_int(k1) / num_traits<T>::from_int(lj)));
519 const Cx<T> inner = clw_inner<T>(Kj, *cx.opt, [&](long k) {
520 const T theta = T(cx.pi * num_traits<T>::from_int(k1 + lj * k) /
521 num_traits<T>::from_int(lj * Kj));
522 w[j] = cx_scale(cx_expi(theta), rj);
523 return next();
524 });
525 acc = cx_add(acc, cx_mul(ph, inner));
526 }
527 const T den =
528 T(num_traits<T>::from_int(2 * lj * Kj) * num_pow_int(rj, static_cast<unsigned>(Kj)));
529 return cx_scale(acc, T(one / den));
530}
531
532/** Inversion of one component of the interdependence graph minus D. */
533template <class T, class F>
534Cx<T> clw_invert_c(std::size_t c, std::size_t s, std::vector<Cx<T>>& w, const ClwCtx<T>& cx,
535 const F& gbar) {
536 const std::vector<std::size_t>& vars = cx.plan->comps[c];
537 if (s >= vars.size()) return gbar(w, static_cast<long>(c));
538 Cx<T> val = clw_lattice(vars[s], w, cx, [&]() { return clw_invert_c(c, s + 1, w, cx, gbar); });
539 if (cx.plan->D.empty() && s == 0) {
540 // with D empty every component is an independent subnetwork, so its
541 // coefficient is real
542 val.im = num_traits<T>::from_int(0);
543 }
544 return val;
545}
546
547/** Outer inversion over the committed variables D (Section 3). */
548template <class T, class F>
549Cx<T> clw_invert_d(std::size_t t, std::vector<Cx<T>>& w, const ClwCtx<T>& cx, const F& gbar) {
550 if (t >= cx.plan->D.size()) {
551 // D fixed: the remaining factors have no variable in common, so the
552 // coefficient of the inner monomial is the product of the components'
553 Cx<T> val = gbar(w, -1);
554 for (std::size_t c = 0; c < cx.plan->comps.size(); ++c)
555 val = cx_mul(val, clw_invert_c(c, 0, w, cx, gbar));
556 return val;
557 }
558 Cx<T> val =
559 clw_lattice(cx.plan->D[t], w, cx, [&]() { return clw_invert_d(t + 1, w, cx, gbar); });
560 if (t == 0) val.im = num_traits<T>::from_int(0);
561 return val;
562}
563
564/**
565 * The plain nested inversion of eq. (2.3) over all p variables in order, with
566 * neither acceleration: the entry point pfqn_clwoi and pfqn_clwjd use, whose
567 * `gbar` takes the contour point alone.
568 */
569template <class T, class F>
570Cx<T> clw_invert(std::size_t j, std::vector<Cx<T>>& w, const std::vector<int>& N,
571 const std::vector<int>& l, const std::vector<T>& r, std::size_t p,
572 const F& gbar) {
573 using std::acos;
574 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
575 ClwPlan plan;
576 plan.comps.resize(1);
577 for (std::size_t u = j; u < p; ++u) plan.comps[0].push_back(u);
578 ClwOptions opt;
579 opt.euler = false;
580 opt.dimred = false;
581 ClwCtx<T> cx;
582 cx.N = &N;
583 cx.l = &l;
584 cx.r = &r;
585 cx.plan = &plan;
586 cx.opt = &opt;
587 cx.pi = T(num_traits<T>::from_int(2) * acos(zero));
588 return clw_invert_d(0, w, cx, [&](const std::vector<Cx<T>>& wv, long bucket) {
589 return (bucket < 0) ? Cx<T>(one, zero) : gbar(wv);
590 });
591}
592
593} // namespace detail
594
595/**
596 * @brief Choudhury-Leung-Whitt normalization constant by numerical inversion
597 * of the generating function (JACM 42(5):935-970, 1995), and its
598 * limited load-dependent extension through the per-center transforms of
599 * Bertozzi and McKenna (SIAM Review 35(2):239-268, 1993).
600 *
601 * @param L (q' x p) single-server relative traffic intensities, L(i,j) = rho_{ji}
602 * @param N (p) closed-chain population vector
603 * @param Z (p) aggregate infinite-server relative intensities rho_{j0}
604 * @param m (q') queue multiplicities; empty for all ones
605 * @param opt lattice and aliasing parameters
606 */
607template <class T>
608ClwResult<T> pfqn_clw(const Matrix<T>& L, const std::vector<int>& N, const std::vector<T>& Z,
609 const std::vector<long>& m, const ClwOptions& opt) {
611 "pfqn_clw requires transcendental arithmetic (contour integration of a "
612 "generating function)");
613 using std::exp;
614 using std::log;
615 const std::size_t qd = L.rows();
616 if (L.cols() != N.size()) throw InputError("pfqn_clw: L and N disagree on the chain count");
617 if (Z.size() != N.size()) throw InputError("pfqn_clw: Z and N disagree on the chain count");
618 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
619
620 ClwResult<T> res;
621 for (std::size_t j = 0; j < N.size(); ++j)
622 if (N[j] < 0) {
623 res.G = zero;
624 res.lG = T(-std::numeric_limits<T>::infinity());
625 return res;
626 }
627 bool allzero = true;
628 for (std::size_t j = 0; j < N.size(); ++j)
629 if (N[j] > 0) allzero = false;
630 if (allzero) {
631 res.G = one;
632 res.lG = zero;
633 return res;
634 }
635
636 // zero-population chain drop rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
637 std::vector<std::size_t> keep;
638 for (std::size_t j = 0; j < N.size(); ++j)
639 if (N[j] > 0) keep.push_back(j);
640 const std::size_t p = keep.size();
641 Matrix<T> Lk(qd, p, zero);
642 std::vector<int> Nk(p, 0), l(p, 1);
643 std::vector<T> Zk(p, zero);
644 std::vector<double> gam(p, 0.0);
645 for (std::size_t j = 0; j < p; ++j) {
646 for (std::size_t i = 0; i < qd; ++i) Lk(i, j) = L(i, keep[j]);
647 Nk[j] = N[keep[j]];
648 Zk[j] = Z[keep[j]];
649 }
650
651 // dimension reduction (Section 3): D is inverted first, then each connected
652 // component of the interdependence graph minus D, independently
653 const detail::ClwPlan plan = detail::clw_plan(Lk, opt);
654 std::vector<std::size_t> order, depth(p, 0);
655 for (std::size_t t = 0; t < plan.D.size(); ++t) {
656 order.push_back(plan.D[t]);
657 depth[plan.D[t]] = t + 1;
658 }
659 for (std::size_t c = 0; c < plan.comps.size(); ++c)
660 for (std::size_t s = 0; s < plan.comps[c].size(); ++s) {
661 order.push_back(plan.comps[c][s]);
662 depth[plan.comps[c][s]] = plan.D.size() + s + 1;
663 }
664 detail::clw_defaults(N.size(), keep, depth, opt, l, gam);
665 std::vector<long> mult(qd, 1);
666 if (!m.empty()) {
667 if (m.size() != qd) throw InputError("pfqn_clw: m must have one entry per queue");
668 for (std::size_t i = 0; i < qd; ++i) {
669 if (m[i] < 1) throw InputError("pfqn_clw: the queue multiplicities must be positive");
670 mult[i] = m[i];
671 }
672 }
673
674 // contour radii r_j = 10^{-gamma_j / (2 l_j K_j)} (eq. 2.7)
675 std::vector<T> r(p, one);
676 for (std::size_t j = 0; j < p; ++j)
677 r[j] = detail::clw_pow_real(
679 T(num_traits<T>::from_double(-gam[j]) /
680 num_traits<T>::from_int(2 * static_cast<long>(l[j]) * Nk[j])));
681
682 const std::vector<T> alpha =
683 detail::clw_scaling(Lk, Lk, Nk, Zk, l, r, mult, order, detail::clw_beta<T>(keep, opt));
684
685 std::vector<T> arho0(p, zero);
686 Matrix<T> rhoS(qd, p, zero);
687 for (std::size_t j = 0; j < p; ++j) {
688 arho0[j] = alpha[j] * Zk[j];
689 for (std::size_t i = 0; i < qd; ++i) rhoS(i, j) = Lk(i, j) * alpha[j];
690 }
691
692 // split the factors of (4.5) over D and the components: a queue whose chains
693 // all lie in D is constant during the component inversions, and every other
694 // queue has all of its non-D chains inside a single component
695 std::vector<char> inD(p, 0);
696 for (std::size_t t = 0; t < plan.D.size(); ++t) inD[plan.D[t]] = 1;
697 std::vector<long> compOf(p, 0);
698 for (std::size_t c = 0; c < plan.comps.size(); ++c)
699 for (std::size_t s = 0; s < plan.comps[c].size(); ++s)
700 compOf[plan.comps[c][s]] = static_cast<long>(c);
701 std::vector<long> qBucket(qd, -1);
702 for (std::size_t i = 0; i < qd; ++i)
703 for (std::size_t j = 0; j < p; ++j)
704 if (Lk(i, j) != zero && !inD[j]) {
705 qBucket[i] = compOf[j];
706 break;
707 }
708
709 // per-group normalization (Section 2.2, page 944): the scaling normalizes the
710 // whole generating function, not each group, and a decomposition multiplies
711 // the groups together. Every factor has nonnegative coefficients, so the
712 // group modulus is maximized at w = r; that constant cancels in the recovery
713 // and is left at zero on the undecomposed path, which is thus unchanged.
714 const bool decomposed = !plan.D.empty() || plan.comps.size() > 1;
715 std::vector<T> off(plan.comps.size() + 1, zero); // [0] is the D group
716 if (decomposed) {
717 for (std::size_t b = 0; b <= plan.comps.size(); ++b) {
718 const long tag = static_cast<long>(b) - 1;
719 T o = zero;
720 const std::vector<std::size_t>& ch =
721 (tag < 0) ? plan.D : plan.comps[static_cast<std::size_t>(tag)];
722 for (std::size_t t = 0; t < ch.size(); ++t) o += arho0[ch[t]] * T(r[ch[t]] - one);
723 for (std::size_t i = 0; i < qd; ++i) {
724 if (qBucket[i] != tag) continue;
725 T x = zero;
726 for (std::size_t j = 0; j < p; ++j) x += rhoS(i, j) * r[j];
727 T pole = T(one - x);
728 if (!(pole > zero)) pole = num_traits<T>::from_double(std::numeric_limits<double>::min());
729 o -= num_traits<T>::from_int(mult[i]) * log(pole);
730 }
731 off[b] = o;
732 }
733 }
734
735 const auto gbar = [&](const std::vector<detail::Cx<T>>& w, long bucket) {
736 const std::vector<std::size_t>& ch =
737 (bucket < 0) ? plan.D : plan.comps[static_cast<std::size_t>(bucket)];
738 detail::Cx<T> expo(zero, zero);
739 for (std::size_t t = 0; t < ch.size(); ++t) {
740 const std::size_t j = ch[t];
741 expo = detail::cx_add(
742 expo, detail::cx_scale(detail::Cx<T>(T(w[j].re - one), w[j].im), arho0[j]));
743 }
744 detail::Cx<T> logden(zero, zero);
745 for (std::size_t i = 0; i < qd; ++i) {
746 if (qBucket[i] != bucket) continue;
747 detail::Cx<T> a(zero, zero);
748 for (std::size_t j = 0; j < p; ++j)
749 a = detail::cx_add(a, detail::cx_scale(w[j], rhoS(i, j)));
750 const detail::Cx<T> lg = detail::cx_log(detail::Cx<T>(T(one - a.re), T(-a.im)));
751 logden = detail::cx_add(
752 logden, detail::cx_scale(lg, num_traits<T>::from_int(mult[i])));
753 }
754 const T o = off[static_cast<std::size_t>(bucket + 1)];
755 return detail::cx_exp(detail::Cx<T>(T(expo.re - logden.re - o), T(expo.im - logden.im)));
756 };
757
758 detail::ClwCtx<T> cx;
759 cx.N = &Nk;
760 cx.l = &l;
761 cx.r = &r;
762 cx.plan = &plan;
763 cx.opt = &opt;
764 {
765 using std::acos;
766 cx.pi = T(num_traits<T>::from_int(2) * acos(zero));
767 }
768 std::vector<detail::Cx<T>> w(p, detail::Cx<T>(zero, zero));
769 const detail::Cx<T> gv = detail::clw_invert_d(0, w, cx, gbar);
770 if (!(gv.re > zero))
771 throw NumericError("pfqn_clw: the inverted generating function is not positive");
772
773 T lG = log(gv.re);
774 for (std::size_t b = 0; b < off.size(); ++b) lG += off[b];
775 for (std::size_t j = 0; j < p; ++j)
776 lG += arho0[j] - num_traits<T>::from_int(Nk[j]) * log(alpha[j]);
777 res.lG = lG;
778 res.G = (lG > num_traits<T>::from_int(709)) ? T(std::numeric_limits<T>::infinity()) : T(exp(lG));
779 return res;
780}
781
782/** Overload with unit multiplicities and the CLW default parameters. */
783template <class T>
784ClwResult<T> pfqn_clw(const Matrix<T>& L, const std::vector<int>& N, const std::vector<T>& Z) {
785 return pfqn_clw(L, N, Z, std::vector<long>(), ClwOptions());
786}
787
788/** Overload with the CLW default parameters. */
789template <class T>
790ClwResult<T> pfqn_clw(const Matrix<T>& L, const std::vector<int>& N, const std::vector<T>& Z,
791 const std::vector<long>& m) {
792 return pfqn_clw(L, N, Z, m, ClwOptions());
793}
794
795/**
796 * Limited load-dependent form (matlab pfqn_clw_lld.m).
797 *
798 * @param L (q' x p) relative traffic intensities
799 * @param N (p) populations
800 * @param Z (p) infinite-server intensities
801 * @param mu (q' x n) load-dependent rate scalings S_i(k); the last column is
802 * extended when fewer than sum(N) are supplied (the LLD assumption),
803 * and empty means all queues are load independent
804 * @param opt lattice and aliasing parameters
805 */
806template <class T>
807ClwResult<T> pfqn_clw_lld(const Matrix<T>& L, const std::vector<int>& N, const std::vector<T>& Z,
808 const Matrix<T>& mu, const ClwOptions& opt) {
810 "pfqn_clw_lld requires transcendental arithmetic (contour integration of a "
811 "generating function)");
812 using std::exp;
813 using std::log;
814 const std::size_t qd = L.rows();
815 if (L.cols() != N.size()) throw InputError("pfqn_clw_lld: L and N disagree on the chain count");
816 if (Z.size() != N.size()) throw InputError("pfqn_clw_lld: Z and N disagree on the chain count");
817 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
818
819 ClwResult<T> res;
820 long Ntot = 0;
821 for (std::size_t j = 0; j < N.size(); ++j) {
822 if (N[j] < 0) {
823 res.G = zero;
824 res.lG = T(-std::numeric_limits<T>::infinity());
825 return res;
826 }
827 Ntot += N[j];
828 }
829 if (Ntot == 0) {
830 res.G = one;
831 res.lG = zero;
832 return res;
833 }
834
835 // extend or truncate mu to sum(N) columns (LLD extension of the last column)
836 Matrix<T> S(qd, static_cast<std::size_t>(Ntot), one);
837 if (!mu.empty()) {
838 if (mu.rows() != qd) throw InputError("pfqn_clw_lld: mu must have one row per queue");
839 for (std::size_t i = 0; i < qd; ++i)
840 for (long k = 0; k < Ntot; ++k) {
841 const std::size_t src = (static_cast<std::size_t>(k) < mu.cols())
842 ? static_cast<std::size_t>(k)
843 : mu.cols() - 1;
844 if (!(mu(i, src) > zero))
845 throw InputError("pfqn_clw_lld: the load-dependent rates must be positive");
846 S(i, static_cast<std::size_t>(k)) = mu(i, src);
847 }
848 }
849
850 std::vector<std::size_t> keep;
851 for (std::size_t j = 0; j < N.size(); ++j)
852 if (N[j] > 0) keep.push_back(j);
853 const std::size_t p = keep.size();
854 Matrix<T> Lk(qd, p, zero);
855 std::vector<int> Nk(p, 0), l(p, 1);
856 std::vector<T> Zk(p, zero);
857 std::vector<double> gam(p, 0.0);
858 std::vector<std::size_t> order(p, 0), depth(p, 0);
859 for (std::size_t j = 0; j < p; ++j) {
860 for (std::size_t i = 0; i < qd; ++i) Lk(i, j) = L(i, keep[j]);
861 Nk[j] = N[keep[j]];
862 Zk[j] = Z[keep[j]];
863 order[j] = j;
864 depth[j] = j + 1;
865 }
866 // the accelerations are wired for pfqn_clw only: extending them to the LLD
867 // form means porting the same change to all four codebases, so this routine
868 // keeps the plain nested inversion and its numerics
869 ClwOptions lopt = opt;
870 lopt.euler = false;
871 lopt.dimred = false;
872 detail::clw_defaults(N.size(), keep, depth, lopt, l, gam);
873 detail::ClwPlan lplan;
874 lplan.comps.assign(1, order);
875
876 // pole c_i and LLD cutoff l_i: S_i(k) = c_i for k >= l_i
877 std::vector<T> cpole(qd, one);
878 std::vector<std::vector<T>> numc(qd);
879 for (std::size_t i = 0; i < qd; ++i) {
880 cpole[i] = S(i, static_cast<std::size_t>(Ntot) - 1);
881 long last = -1;
882 for (long k = 0; k < Ntot; ++k)
883 if (S(i, static_cast<std::size_t>(k)) != cpole[i]) last = k;
884 const long li = (last < 0) ? 1 : last + 2; // MATLAB last is 1-based
885 std::vector<T> a(static_cast<std::size_t>(li), zero);
886 a[0] = cpole[i];
887 T cp = one;
888 for (long n = 1; n < li; ++n) {
889 cp *= S(i, static_cast<std::size_t>(n) - 1);
890 a[static_cast<std::size_t>(n)] = (cpole[i] - S(i, static_cast<std::size_t>(n) - 1)) / cp;
891 }
892 numc[i] = a;
893 }
894
895 std::vector<T> r(p, one);
896 for (std::size_t j = 0; j < p; ++j)
897 r[j] = detail::clw_pow_real(
899 T(num_traits<T>::from_double(-gam[j]) /
900 num_traits<T>::from_int(2 * static_cast<long>(l[j]) * Nk[j])));
901
902 // unit-pole intensities: each F_i behaves as a simple pole at 1
903 Matrix<T> Lt(qd, p, zero);
904 for (std::size_t i = 0; i < qd; ++i)
905 for (std::size_t j = 0; j < p; ++j) Lt(i, j) = Lk(i, j) / cpole[i];
906 const std::vector<long> mult(qd, 1);
907 const std::vector<T> alpha =
908 detail::clw_scaling(Lt, Lk, Nk, Zk, l, r, mult, order, detail::clw_beta<T>(keep, lopt));
909
910 std::vector<T> arho0(p, zero);
911 Matrix<T> rhoS(qd, p, zero);
912 for (std::size_t j = 0; j < p; ++j) {
913 arho0[j] = alpha[j] * Zk[j];
914 for (std::size_t i = 0; i < qd; ++i) rhoS(i, j) = Lk(i, j) * alpha[j];
915 }
916
917 const auto gbar = [&](const std::vector<detail::Cx<T>>& w, long bucket) {
918 if (bucket < 0) return detail::Cx<T>(one, zero); // no D group in the trivial plan
919 detail::Cx<T> expo(zero, zero);
920 for (std::size_t j = 0; j < p; ++j)
921 expo = detail::cx_add(
922 expo, detail::cx_scale(detail::Cx<T>(T(w[j].re - one), w[j].im), arho0[j]));
923 detail::Cx<T> logF(zero, zero);
924 for (std::size_t i = 0; i < qd; ++i) {
925 detail::Cx<T> x(zero, zero);
926 for (std::size_t j = 0; j < p; ++j)
927 x = detail::cx_add(x, detail::cx_scale(w[j], rhoS(i, j)));
928 const std::vector<T>& a = numc[i];
929 detail::Cx<T> num(a.back(), zero); // Horner on N_i(x)
930 for (std::size_t k = a.size() - 1; k-- > 0;)
931 num = detail::cx_add(detail::cx_mul(num, x), detail::Cx<T>(a[k], zero));
932 logF = detail::cx_add(logF, detail::cx_log(num));
933 logF = detail::cx_sub(
934 logF, detail::cx_log(detail::Cx<T>(T(cpole[i] - x.re), T(-x.im))));
935 }
936 return detail::cx_exp(detail::cx_add(expo, logF));
937 };
938
939 detail::ClwCtx<T> cx;
940 cx.N = &Nk;
941 cx.l = &l;
942 cx.r = &r;
943 cx.plan = &lplan;
944 cx.opt = &lopt;
945 {
946 using std::acos;
947 cx.pi = T(num_traits<T>::from_int(2) * acos(zero));
948 }
949 std::vector<detail::Cx<T>> w(p, detail::Cx<T>(zero, zero));
950 const detail::Cx<T> gv = detail::clw_invert_d(0, w, cx, gbar);
951 if (!(gv.re > zero))
952 throw NumericError("pfqn_clw_lld: the inverted generating function is not positive");
953
954 T lG = log(gv.re);
955 for (std::size_t j = 0; j < p; ++j)
956 lG += arho0[j] - num_traits<T>::from_int(Nk[j]) * log(alpha[j]);
957 res.lG = lG;
958 res.G = (lG > num_traits<T>::from_int(709)) ? T(std::numeric_limits<T>::infinity()) : T(exp(lG));
959 return res;
960}
961
962/** Overload with the CLW default parameters. */
963template <class T>
964ClwResult<T> pfqn_clw_lld(const Matrix<T>& L, const std::vector<int>& N, const std::vector<T>& Z,
965 const Matrix<T>& mu) {
966 return pfqn_clw_lld(L, N, Z, mu, ClwOptions());
967}
968
969/** Overload with all queues load independent. */
970template <class T>
971ClwResult<T> pfqn_clw_lld(const Matrix<T>& L, const std::vector<int>& N, const std::vector<T>& Z) {
972 return pfqn_clw_lld(L, N, Z, Matrix<T>(), ClwOptions());
973}
974
975} // namespace pfqn
976} // namespace line
977
978#endif // LINE_API_PFQN_CLW_H
InputError(const std::string &what)
Definition error.h:39
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
bool empty() const
Definition matrix.h:92
NumericError(const std::string &what)
Definition error.h:45
The exception types the port throws.
Dense matrix and non-owning view.
ClwResult< T > pfqn_clw(const Matrix< T > &L, const std::vector< int > &N, const std::vector< T > &Z, const std::vector< long > &m, const ClwOptions &opt)
Choudhury-Leung-Whitt normalization constant by numerical inversion of the generating function (JACM ...
Definition pfqn_clw.h:608
ClwResult< T > pfqn_clw_lld(const Matrix< T > &L, const std::vector< int > &N, const std::vector< T > &Z, const Matrix< T > &mu, const ClwOptions &opt)
Limited load-dependent form (matlab pfqn_clw_lld.m).
Definition pfqn_clw.h:807
T num_pow_int(const T &base, unsigned e)
Integer power, valid in any field (no transcendental requirement).
Definition number.h:192
Number-type abstraction for the templated API port.
Shared scalar machinery for the integration / asymptotic members of the pfqn family (pfqn_le,...
Optional lattice and aliasing parameters; empty means "use the CLW defaults".
Definition pfqn_clw.h:104
bool dimred
dimension reduction by decomposition (Section 3)
Definition pfqn_clw.h:112
std::vector< int > l
inner lattice parameters l_j (roundoff control)
Definition pfqn_clw.h:105
bool euler
Euler-sum the inner sums where K_j > eulerN + eulerM.
Definition pfqn_clw.h:107
int eulerN
terms summed exactly before averaging (n in eq. 2.22)
Definition pfqn_clw.h:108
std::vector< double > gamma
aliasing parameters, aliasing ~ 10^-gamma_j
Definition pfqn_clw.h:106
std::vector< double > beta
multipliers on alpha_j, the manual tuning of page 956
Definition pfqn_clw.h:114
int eulerM
starting order of the averaging (m in eq. 2.22)
Definition pfqn_clw.h:109
double eulerTol
relative tolerance on |E(m,n) - E(m,n+1)|
Definition pfqn_clw.h:110
int eulerMaxM
largest Euler order reached by doubling
Definition pfqn_clw.h:111
int dimredMaxD
largest |D| examined when minimizing (3.3)
Definition pfqn_clw.h:113
Return value of pfqn_clw and pfqn_clw_lld, mirroring [G, lG].
Definition pfqn_clw.h:98
T G
normalization constant, +infinity when it overflows the range of T
Definition pfqn_clw.h:99
T lG
its natural logarithm, always finite
Definition pfqn_clw.h:100