LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_harel_bounds.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_HAREL_BOUNDS_H
6#define LINE_API_PFQN_HAREL_BOUNDS_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Harel-Namn-Sturm throughput bounds for a single-class closed network.
12 *
13 * Templated port of jar/src/main/java/jline/api/pfqn/Pfqn_harel_bounds.java.
14 * MATLAB has no counterpart, so the JAR is the reference.
15 *
16 * These are the SHARP bounds of the paper, distinct from the `sb` family
17 * already in solver_ba_analyzer: `sb` uses only the first three power sums in
18 * closed form, whereas this family evaluates the normalizing constant exactly
19 * at small populations and extrapolates from it. Both cite Harel1999; they are
20 * different results in it and neither subsumes the other.
21 *
22 * Write A_i = sum_j rho_j^i for the power sums of the relative utilizations.
23 * Then
24 *
25 * G(n) = h_n(rho), the complete homogeneous symmetric polynomial,
26 * TH(n) = G(n-1) / G(n), the exact throughput at population n,
27 * LB = N / (A_1 + (N-1) (A_N/A_1)^{1/(N-1)}),
28 * UB(n) = N / (A_1 + ((N-1)/(n-1)) (n/TH(n) - A_1)), 2 <= n <= N.
29 *
30 * G(n) IS the normalizing constant of the closed load-independent network at
31 * population n, which is the oracle the port is tested against: G(n) computed
32 * here must equal pfqn_ca on the same demands.
33 *
34 * TWO DELIBERATE NOTES ON THE PORT.
35 *
36 * First, the reference hardcodes G(0)..G(7) as expanded polynomials in the
37 * power sums and REFUSES n > 7 with "G(n) polynomial not available". Those
38 * expansions are the Newton-Girard recurrence
39 *
40 * n G(n) = sum_{i=1..n} A_i G(n-i)
41 *
42 * unrolled by hand. The port evaluates the recurrence instead: it agrees term
43 * for term with the reference at every n <= 7, needs no table, and is
44 * EXACT-CAPABLE where the expanded form needs pow. The n <= 7 refusal on the
45 * PUBLIC entry points is nevertheless kept, so the contract callers see is the
46 * reference's; only the internal ceiling is gone. See _kb/03-api-layer.md.
47 *
48 * Second, the reference refuses a nonzero think time rather than folding it in,
49 * because the bounds are derived for a network with no terminal population.
50 * That refusal is reproduced: silently dropping Z would return a bound that
51 * does not bound.
52 *
53 * Arithmetic: G and UB are EXACT-CAPABLE. LB needs an (N-1)-st root and is
54 * TRANSCENDENTAL for N > 2; at N <= 2 the root is trivial and LB is exact too.
55 */
56
57#include <cstddef>
58#include <string>
59#include <vector>
60
61#include "line/num/number.h"
62#include "line/util/error.h"
63
64namespace line {
65namespace pfqn {
66
67/** Return value of pfqn_harel_bounds, mirroring Ret.pfqnHarelBounds. */
68template <class T>
70 T LB; ///< throughput lower bound at population N
71 std::vector<T> UB; ///< UB[n] for n = 2..maxUB; entries 0 and 1 are unset
72 std::vector<T> TH; ///< TH[n] = exact throughput at population n, n = 1..maxUB
73 int N = 0; ///< population the bounds are stated at
74 std::size_t k = 0; ///< number of stations
75 int maxUB = 0; ///< largest n at which an upper bound was formed
76};
77
78namespace detail {
79
80/** Power sums A_i = sum_j rho_j^i, i = 1..maxPower; A[0] is unused. */
81template <class T>
82std::vector<T> harel_power_sums(const std::vector<T>& rho, int maxPower) {
83 std::vector<T> A(static_cast<std::size_t>(maxPower) + 1, num_traits<T>::from_int(0));
84 for (int i = 1; i <= maxPower; ++i) {
86 for (std::size_t j = 0; j < rho.size(); ++j) s += num_pow_int(rho[j], static_cast<unsigned>(i));
87 A[static_cast<std::size_t>(i)] = s;
88 }
89 return A;
90}
91
92/** G(0..n) by the Newton-Girard recurrence n G(n) = sum_i A_i G(n-i). */
93template <class T>
94std::vector<T> harel_G(const std::vector<T>& A, int n) {
95 if (static_cast<int>(A.size()) <= n)
96 throw InputError("pfqn_harel_bounds: too few power sums for the requested population");
97 std::vector<T> G(static_cast<std::size_t>(n) + 1, num_traits<T>::from_int(0));
99 for (int m = 1; m <= n; ++m) {
100 T acc = num_traits<T>::from_int(0);
101 for (int i = 1; i <= m; ++i)
102 acc += T(A[static_cast<std::size_t>(i)] * G[static_cast<std::size_t>(m - i)]);
103 G[static_cast<std::size_t>(m)] = T(acc / num_traits<T>::from_int(m));
104 }
105 return G;
106}
107
108/** The reference refuses a nonzero think time rather than folding it in. */
109template <class T>
110void harel_reject_thinktime(const T& Z, const std::string& who) {
111 if (Z != num_traits<T>::from_int(0))
112 throw InputError(who +
113 " is only valid for networks with zero think time; the provided think "
114 "time is nonzero");
115}
116
117/** Shared input screening of the loading vector. */
118template <class T>
119void harel_check_rho(const std::vector<T>& rho) {
120 if (rho.empty())
121 throw InputError("pfqn_harel_bounds: the loading vector must have at least one element");
122 for (std::size_t i = 0; i < rho.size(); ++i)
123 if (rho[i] <= num_traits<T>::from_int(0))
124 throw InputError("pfqn_harel_bounds: all loading factors must be positive");
125}
126
127/** LB = N / (A1 + (N-1) (A_N/A_1)^{1/(N-1)}). */
128template <class T>
129T harel_lower_bound(const std::vector<T>& A, int N) {
130 const T A1 = A[1];
131 if (N == 1) return T(num_traits<T>::from_int(1) / A1);
132 const T ratio = T(A[static_cast<std::size_t>(N)] / A1);
133 // At N == 2 the exponent is one, so the root is the ratio itself and the
134 // bound stays available in exact arithmetic.
135 if (N == 2)
136 return T(num_traits<T>::from_int(2) / T(A1 + ratio));
137 if constexpr (!num_traits<T>::has_transcendental) {
138 throw UnsupportedError(
139 "pfqn_harel_lb needs an (N-1)-st root and is unavailable in exact arithmetic for "
140 "N > 2");
141 } else {
142 using std::pow;
143 const T root = pow(ratio, T(num_traits<T>::from_int(1) / num_traits<T>::from_int(N - 1)));
144 return T(num_traits<T>::from_int(N) / T(A1 + num_traits<T>::from_int(N - 1) * root));
145 }
146}
147
148/** UB(n) = N / (A1 + ((N-1)/(n-1)) (n/TH(n) - A1)). */
149template <class T>
150T harel_upper_from_th(const T& A1, int N, int n, const T& THn) {
151 if (THn == num_traits<T>::from_int(0))
152 throw NumericError("pfqn_harel_bounds: the throughput at the extrapolation point is zero");
153 const T nOverTH = T(num_traits<T>::from_int(n) / THn);
154 const T den = T(A1 + T(num_traits<T>::from_int(N - 1) / num_traits<T>::from_int(n - 1)) *
155 T(nOverTH - A1));
156 if (den == num_traits<T>::from_int(0))
157 throw NumericError("pfqn_harel_bounds: the upper-bound denominator vanishes");
158 return T(num_traits<T>::from_int(N) / den);
159}
160
161} // namespace detail
162
163/**
164 * Lower bound alone.
165 *
166 * @param rho (k) relative utilizations, all strictly positive
167 * @param N population, at least 1
168 * @param Z think time; must be zero
169 */
170template <class T>
171T pfqn_harel_lb(const std::vector<T>& rho, int N, const T& Z) {
172 detail::harel_reject_thinktime(Z, "pfqn_harel_lb");
173 if (N < 1) throw InputError("pfqn_harel_lb: the population must be at least 1");
174 detail::harel_check_rho(rho);
175 const std::vector<T> A = detail::harel_power_sums(rho, N);
176 return detail::harel_lower_bound(A, N);
177}
178
179/** Zero think time. */
180template <class T>
181T pfqn_harel_lb(const std::vector<T>& rho, int N) {
182 return pfqn_harel_lb(rho, N, num_traits<T>::from_int(0));
183}
184
185/**
186 * Upper bound extrapolated from the exact throughput at population n.
187 *
188 * @param n extrapolation point, 2 <= n <= min(N, 7)
189 */
190template <class T>
191T pfqn_harel_ub(const std::vector<T>& rho, int N, int n, const T& Z) {
192 detail::harel_reject_thinktime(Z, "pfqn_harel_ub");
193 if (N < 1) throw InputError("pfqn_harel_ub: the population must be at least 1");
194 if (n < 2) throw InputError("pfqn_harel_ub: the extrapolation point must be at least 2");
195 if (n > N) throw InputError("pfqn_harel_ub: the extrapolation point cannot exceed N");
196 // Kept from the reference, whose hardcoded G(n) table stops at 7.
197 if (n > 7) throw InputError("pfqn_harel_ub: the extrapolation point cannot exceed 7");
198 detail::harel_check_rho(rho);
199 const std::vector<T> A = detail::harel_power_sums(rho, n);
200 const std::vector<T> G = detail::harel_G(A, n);
201 if (G[static_cast<std::size_t>(n)] == num_traits<T>::from_int(0))
202 throw NumericError("pfqn_harel_ub: the normalizing constant vanishes");
203 const T THn = T(G[static_cast<std::size_t>(n) - 1] / G[static_cast<std::size_t>(n)]);
204 return detail::harel_upper_from_th(A[1], N, n, THn);
205}
206
207/** Zero think time. */
208template <class T>
209T pfqn_harel_ub(const std::vector<T>& rho, int N, int n) {
210 return pfqn_harel_ub(rho, N, n, num_traits<T>::from_int(0));
211}
212
213/**
214 * Both bounds, plus the exact throughputs the upper bounds extrapolate from.
215 *
216 * @param maxUB largest extrapolation point; defaults to min(N, 7) when <= 0
217 */
218template <class T>
219HarelBoundsResult<T> pfqn_harel_bounds(const std::vector<T>& rho, int N, const T& Z, int maxUB) {
220 detail::harel_reject_thinktime(Z, "pfqn_harel_bounds");
221 if (N < 1) throw InputError("pfqn_harel_bounds: the population must be at least 1");
222 detail::harel_check_rho(rho);
223 const int effectiveMaxUB = maxUB > 0 ? maxUB : (N < 7 ? N : 7);
224 if (effectiveMaxUB > 7)
225 throw InputError("pfqn_harel_bounds: upper bounds are available only for n <= 7");
226 if (effectiveMaxUB > N)
227 throw InputError("pfqn_harel_bounds: the extrapolation point cannot exceed N");
228
230 res.N = N;
231 res.k = rho.size();
232 res.maxUB = effectiveMaxUB;
233
234 // The lower bound reads A up to N, the upper bounds only up to maxUB.
235 const int maxPower = N > effectiveMaxUB ? N : effectiveMaxUB;
236 const std::vector<T> A = detail::harel_power_sums(rho, maxPower);
237 res.LB = detail::harel_lower_bound(A, N);
238
239 const std::vector<T> G = detail::harel_G(A, effectiveMaxUB);
240 const T zero = num_traits<T>::from_int(0);
241 res.TH.assign(static_cast<std::size_t>(effectiveMaxUB) + 1, zero);
242 res.UB.assign(static_cast<std::size_t>(effectiveMaxUB) + 1, zero);
243 for (int n = 1; n <= effectiveMaxUB; ++n) {
244 if (G[static_cast<std::size_t>(n)] == zero)
245 throw NumericError("pfqn_harel_bounds: the normalizing constant vanishes");
246 res.TH[static_cast<std::size_t>(n)] =
247 T(G[static_cast<std::size_t>(n) - 1] / G[static_cast<std::size_t>(n)]);
248 }
249 for (int n = 2; n <= effectiveMaxUB; ++n)
250 res.UB[static_cast<std::size_t>(n)] =
251 detail::harel_upper_from_th(A[1], N, n, res.TH[static_cast<std::size_t>(n)]);
252 return res;
253}
254
255/** Zero think time, default extrapolation ceiling min(N, 7). */
256template <class T>
257HarelBoundsResult<T> pfqn_harel_bounds(const std::vector<T>& rho, int N) {
258 return pfqn_harel_bounds(rho, N, num_traits<T>::from_int(0), 0);
259}
260
261} // namespace pfqn
262} // namespace line
263
264#endif // LINE_API_PFQN_HAREL_BOUNDS_H
InputError(const std::string &what)
Definition error.h:39
NumericError(const std::string &what)
Definition error.h:45
UnsupportedError(const std::string &what)
Definition error.h:51
The exception types the port throws.
HarelBoundsResult< T > pfqn_harel_bounds(const std::vector< T > &rho, int N, const T &Z, int maxUB)
Both bounds, plus the exact throughputs the upper bounds extrapolate from.
T pfqn_harel_ub(const std::vector< T > &rho, int N, int n, const T &Z)
Upper bound extrapolated from the exact throughput at population n.
T pfqn_harel_lb(const std::vector< T > &rho, int N, const T &Z)
Lower bound alone.
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.
Return value of pfqn_harel_bounds, mirroring Ret.pfqnHarelBounds.
std::vector< T > UB
UB[n] for n = 2..maxUB; entries 0 and 1 are unset.
T LB
throughput lower bound at population N
std::vector< T > TH
TH[n] = exact throughput at population n, n = 1..maxUB.
int maxUB
largest n at which an upper bound was formed
int N
population the bounds are stated at
std::size_t k
number of stations