LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_sib.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_PFQN_SIB_H
6#define LINE_API_PFQN_PFQN_SIB_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Successively Improving Bounds (Srinivasan 1985/1987) on the cycle time and
12 * throughput of a single-class closed product-form network.
13 *
14 * Templated port of matlab/src/api/pfqn/pfqn_sib.m, including its local
15 * functions phi_u1, sigma and betaL. The hierarchy bounds
16 * phi(K) = sum_m rho_m Q_m(K) from both sides, with level 1 the closed form of
17 * Theorem 2.1 and higher levels the S_i power-sum forms of Theorems 3.5 and
18 * 3.6, and reads the cycle time off W(N) = sum(L) (1 + phi(N-1)).
19 *
20 * REFERENCE DEFECT ABOVE LEVEL 1, and the port does NOT reproduce it.
21 * pfqn_sib.m declares phi_u1, sigma and betaL as NESTED functions, which in
22 * MATLAB share the parent's workspace rather than getting their own. phi_u1
23 * assigns `eta = (K-1)/K` internally, and the parent has already set
24 * `eta = (N-2)/(N-1)` for the Theorem 3.5 prefactor 0.5/eta. At level 1 sigma
25 * returns before it ever calls phi_u1, so eta survives and the bound is
26 * correct; from level 2 on, sigma calls phi_u1(N-3), which OVERWRITES the
27 * parent's eta, and the prefactor 0.5/eta is then evaluated with the wrong
28 * value. On L = [1/2, 1/3, 1/5], N = 5 the parent eta is 3/4 and the clobbered
29 * one is 1/2, so phi_u_n comes out 1.5x too large -- 2.4919 instead of
30 * 1.6613 -- which exceeds the Section-2 baseline 1.7798, so `min` discards it.
31 * The net effect is that MATLAB's SIB hierarchy is INERT above level 1: levels
32 * 2, 3 and 4 all return the level-0 baseline, and the "bound" reported at
33 * level 2 (X in [1.7407, 1.9335]) is LOOSER than the one at level 1
34 * ([1.7607, 1.9335]), which contradicts the monotonicity the method is for.
35 * A C++ port has no such aliasing -- the three helpers are lambdas with their
36 * own scope -- so this port computes the intended Theorem 3.5 value and its
37 * bounds do tighten with the level: level 2 gives 1.8182, level 3 gives
38 * 1.8369, both still below the exact 1.8558. Reproducing the MATLAB value here
39 * would mean deliberately reintroducing a scoping accident, so the divergence
40 * is documented and pinned in the tests instead.
41 *
42 * DELAY IS REJECTED, following the reference: the no-delay phi bounds do not
43 * bracket the with-delay congestion, so pfqn_sib.m raises
44 * pfqn_sib:delayUnsupported for Z > 0 rather than return an invalid bracket.
45 * The port throws InputError in the same case; returning a bound that is not
46 * one would be worse than refusing.
47 *
48 * ARITHMETIC. Both Theorem 3.5 and Theorem 2.1 solve a quadratic, so the
49 * bounds carry a square root and the routine is gated on
50 * num_traits<T>::has_transcendental. That is a real restriction rather than a
51 * formality: unlike the CBH and PBH families, SIB cannot be evaluated exactly.
52 */
53
54#include <algorithm>
55#include <cmath>
56#include <cstddef>
57#include <vector>
58
59#include "line/num/number.h"
60#include "line/util/error.h"
61
62namespace line {
63namespace pfqn {
64
65/** Return value of pfqn_sib, mirroring [Xlo, Xhi, Wlo, Whi]. */
66template <class T>
67struct SibBounds {
68 T Xlo;
69 T Xhi;
70 T Wlo;
71 T Whi;
72};
73
74/**
75 * @brief Successively Improving Bounds (Srinivasan 1985/1987) on the cycle
76 * time and throughput of a single-class closed product-form network.
77 *
78 * @param L (M) fixed-rate demands; delay demand is NOT accepted
79 * @param N population, at least 2
80 * @param Z think time, must be zero (see the header note)
81 * @param level bound level >= 1, default 3 at the convenience overload
82 */
83template <class T>
84SibBounds<T> pfqn_sib(const std::vector<T>& L, int N, const T& Z, int level) {
86 "pfqn_sib requires transcendental arithmetic (the bounds solve a quadratic)");
87 using std::sqrt;
88 const std::size_t M = L.size();
89 if (M == 0) throw InputError("pfqn_sib: empty demand vector");
90 if (N < 2) throw InputError("pfqn_sib: population must be at least two");
91 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
92 if (Z > zero)
93 throw InputError(
94 "pfqn_sib supports Z=0 only (delay needs the Section-3.2 demand substitution)");
95 const int lv = std::max(1, level);
96
97 T Lsum = zero;
98 for (const T& x : L) Lsum += x;
99 if (Lsum == zero) throw InputError("pfqn_sib: all demands are zero");
100 std::vector<T> rho(M);
101 T rho_u = zero;
102 for (std::size_t i = 0; i < M; ++i) {
103 rho[i] = T(L[i] / Lsum);
104 if (rho[i] > rho_u) rho_u = rho[i];
105 }
106 const int imax = lv + 3;
107 std::vector<T> S(static_cast<std::size_t>(imax) + 1, zero); // S[i], i = 1..imax
108 for (int i = 1; i <= imax; ++i) {
109 T s = zero;
110 for (std::size_t m = 0; m < M; ++m) s += num_pow_int(rho[m], static_cast<unsigned>(i));
111 S[static_cast<std::size_t>(i)] = s;
112 }
113 const T S2 = S[2];
114
115 // alpha_i, i = 0..level (eqs. 3.5-3.6).
116 std::vector<T> alpha(static_cast<std::size_t>(lv) + 1, zero);
117 alpha[0] = S2;
118 for (int i = 1; i <= lv; ++i) {
119 T acc = zero;
120 for (int j = 0; j <= i - 1; ++j)
121 acc += S[static_cast<std::size_t>(i + 1 - j)] * alpha[static_cast<std::size_t>(j)];
122 alpha[static_cast<std::size_t>(i)] = T(S[static_cast<std::size_t>(i + 2)] - acc);
123 }
124
125 // Level-1 upper bound on phi(K), eq. 3.18.
126 const auto phi_u1 = [&](int K) -> T {
127 if (K <= 0) return zero;
128 if (K == 1) return S2;
129 const T KT = num_traits<T>::from_int(K);
130 const T eta = T(T(KT - one) / KT);
131 const T T1 = T(T(KT - one) * rho_u - one);
132 const T disc = T(T1 * T1 + num_traits<T>::from_int(4) * T(KT - one) * S2);
133 return T(T(num_traits<T>::from_rational(1, 2) / eta) * T(T1 + sqrt(disc)));
134 };
135
136 // eq. (3.22c). NN plays the role of N-1.
137 const auto sigma = [&](int NN, int i) -> T {
138 T s = zero;
139 if (i <= 0) return s;
140 const T Dbar = T(one + phi_u1(NN - 2));
141 T pnum = one;
142 for (int j = 1; j <= i; ++j) {
143 pnum *= num_traits<T>::from_int(NN - 1 - (j - 1));
144 s += T(T(rho_u * S[static_cast<std::size_t>(j + 1)] - S[static_cast<std::size_t>(j + 2)]) *
145 pnum / num_pow_int(Dbar, static_cast<unsigned>(j)));
146 }
147 return s;
148 };
149
150 // eq. (3.23c). NN plays the role of N-1.
151 const auto betaL = [&](int NN, int i) -> T {
152 T b = zero;
153 if (i <= 0) return b;
154 for (int j = 1; j <= i - 1; ++j) {
155 T p = one;
156 for (int m = 2; m <= j; ++m)
157 p *= T(num_traits<T>::from_int(NN - m) / T(one + phi_u1(NN - m)));
158 b += alpha[static_cast<std::size_t>(j)] * p;
159 }
160 T p = one;
161 for (int m = 2; m <= i; ++m) p *= T(num_traits<T>::from_int(NN - m) / T(one + phi_u1(NN - m)));
162 const T Nim2 = num_traits<T>::from_int(NN - i - 2); // MATLAB Nim2 = NN-1-i-1
163 if (alpha[static_cast<std::size_t>(i) - 1] == zero)
164 throw NumericError("pfqn_sib: zero alpha coefficient in the level correction");
165 const T corr =
166 T(one + T(alpha[static_cast<std::size_t>(i)] / alpha[static_cast<std::size_t>(i) - 1]) *
167 Nim2 / T(one + Nim2 * alpha[0]));
168 b += alpha[static_cast<std::size_t>(i)] * p * corr;
169 return b;
170 };
171
172 const int NN = N - 1;
173 T phi_lo = T(num_traits<T>::from_int(N - 1) * S2);
174 const T T1s2 = T(num_traits<T>::from_int(N - 1) * rho_u - one);
175 T phi_hi = T(num_traits<T>::from_rational(1, 2) *
176 T(T1s2 + sqrt(T(T1s2 * T1s2 + num_traits<T>::from_int(4 * (N - 1)) * S2))));
177
178 if (N >= 3) {
179 const T eta = T(num_traits<T>::from_int(N - 2) / num_traits<T>::from_int(N - 1));
180 const T T1u = T(num_traits<T>::from_int(N - 2) * rho_u - one);
181 const T su = sigma(NN, lv - 1);
182 T d = T(T1u * T1u + num_traits<T>::from_int(4 * (N - 2)) * T(S2 - su));
183 if (d < zero) d = zero;
184 const T phi_u_n = T(T(num_traits<T>::from_rational(1, 2) / eta) * T(T1u + sqrt(d)));
185 if (phi_u_n < phi_hi) phi_hi = phi_u_n;
186
187 const T T1l = T(num_traits<T>::from_int(N - 2) * S2 - one);
188 const T bl = betaL(NN, lv - 1);
189 T dl = T(T1l * T1l + num_traits<T>::from_int(4 * (N - 2)) *
190 T(S2 + num_traits<T>::from_int(N - 2) * bl));
191 if (dl < zero) dl = zero;
192 // eq (3.23) divides by 2*eta, the SAME constant eq (3.22) applies as
193 // 0.5/eta above; the Greek eta on the scan was read as the level index n
194 const T phi_l_n = T(T(T1l + sqrt(dl)) / T(num_traits<T>::from_int(2) * eta));
195 if (phi_l_n > phi_lo) phi_lo = phi_l_n;
196 }
197
198 if (phi_lo < zero) phi_lo = zero;
199 if (phi_hi < phi_lo) phi_hi = phi_lo;
200
201 SibBounds<T> r;
202 r.Wlo = T(Lsum * T(one + phi_lo) + Z);
203 r.Whi = T(Lsum * T(one + phi_hi) + Z);
204 r.Xlo = T(num_traits<T>::from_int(N) / r.Whi);
205 r.Xhi = T(num_traits<T>::from_int(N) / r.Wlo);
206 return r;
207}
208
209template <class T>
210SibBounds<T> pfqn_sib(const std::vector<T>& L, int N, const T& Z) {
211 return pfqn_sib(L, N, Z, 3);
212}
213
214} // namespace pfqn
215} // namespace line
216
217#endif // LINE_API_PFQN_PFQN_SIB_H
InputError(const std::string &what)
Definition error.h:39
NumericError(const std::string &what)
Definition error.h:45
The exception types the port throws.
SibBounds< T > pfqn_sib(const std::vector< T > &L, int N, const T &Z, int level)
Successively Improving Bounds (Srinivasan 1985/1987) on the cycle time and throughput of a single-cla...
Definition pfqn_sib.h:84
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_sib, mirroring [Xlo, Xhi, Wlo, Whi].
Definition pfqn_sib.h:67