LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_ldmx_ec.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_LDMX_EC_H
6#define LINE_API_PFQN_LDMX_EC_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Bruell-Balbo-Afshari effective-capacity terms for a MIXED open/closed
12 * network with limited load dependence.
13 *
14 * Templated port of matlab/src/api/pfqn/pfqn_ldmx_ec.m.
15 *
16 * Station i is limited load dependent: its rate lattice mu(i,.) is arbitrary
17 * up to the saturation level b_i, the first k with mu(i,k) = mu(i,end), and
18 * constant beyond it. Writing C(i,k) = 1/mu(i,k) and Lo_i = sum_r lambda_r
19 * D(i,r) for the open load, the E-function
20 *
21 * E_i(n) = sum_{n0 >= 0} C(n+n0, n0) Lo_i^{n0} prod_{j=n+1}^{n+n0} C(i,j)
22 *
23 * has the closed form E_i(n) = 1/(1 - Lo_i C(i,b_i))^{n+1} once n >= b_i, and
24 * below saturation is assembled from three finite pieces E1 + E2 - E3, with
25 * E1 the geometric tail, E2 the exact head, and E3 the head of the geometric
26 * that E1 double counts. Eprime is the same construction one index up, and the
27 * effective capacity is the ratio
28 *
29 * EC_i(n) = C(i,n) E_i(n) / E_i(n-1), n = 1, ..., Nt.
30 *
31 * Its reciprocal is the load-dependent rate that turns the mixed model into a
32 * purely closed one, which is what pfqn_ncldmx and pfqn_mvaldmx consume.
33 *
34 * Arithmetic: EXACT-CAPABLE. Every term is a sum, product, quotient or integer
35 * power in the field of the inputs; the reference itself uses no
36 * transcendental function. The one thing the caller must respect is the
37 * stability condition Lo_i C(i,b_i) < 1: at Lo_i C(i,b_i) = 1 the geometric
38 * denominator vanishes and the routine throws, and above 1 the sum that E
39 * represents diverges even though the closed form still evaluates. That check
40 * is exact in rational arithmetic and is not a tolerance.
41 */
42
43#include <cstddef>
44#include <vector>
45
46#include "line/num/number.h"
47#include "line/util/error.h"
48#include "line/util/matrix.h"
49
50namespace line {
51namespace pfqn {
52
53template <class T>
55 Matrix<T> EC; ///< (M x Nt) effective capacity, EC(i,n) for n = 1..Nt
56 Matrix<T> E; ///< (M x Nt+1) E-function, column 1+n
57 Matrix<T> Eprime; ///< (M x Nt+1) Eprime-function, column 1+n
58 std::vector<T> Lo; ///< (M) open load per station
59};
60
61/**
62 * @brief Bruell-Balbo-Afshari effective-capacity terms for a MIXED
63 * open/closed network with limited load dependence.
64 *
65 * @param lambda (R) arrival rates, zero on the closed classes
66 * @param D (M x R) service demands
67 * @param mu (M x Nt) load-dependent rate lattice
68 */
69template <class T>
70LdmxEcResult<T> pfqn_ldmx_ec(const std::vector<T>& lambda, const Matrix<T>& D,
71 const Matrix<T>& mu) {
72 const std::size_t M = mu.rows();
73 const std::size_t Nt = mu.cols();
74 if (M == 0 || Nt == 0) throw InputError("pfqn_ldmx_ec: the rate lattice is empty");
75 if (D.rows() != M) throw InputError("pfqn_ldmx_ec: D and mu disagree on the station count");
76 if (lambda.size() != D.cols())
77 throw InputError("pfqn_ldmx_ec: lambda and D disagree on the class count");
78
79 const T zero = num_traits<T>::from_int(0);
80 const T one = num_traits<T>::from_int(1);
81
83 res.Lo.assign(M, zero);
84 for (std::size_t i = 0; i < M; ++i)
85 for (std::size_t r = 0; r < lambda.size(); ++r) res.Lo[i] += lambda[r] * D(i, r);
86
87 // b_i: the first index at which the rate reaches its saturation value.
88 std::vector<std::size_t> b(M, 1);
89 for (std::size_t i = 0; i < M; ++i) {
90 const T& last = mu(i, Nt - 1);
91 std::size_t k = 1;
92 while (k < Nt && mu(i, k - 1) != last) ++k;
93 b[i] = k;
94 }
95 std::size_t bmax = 1;
96 for (std::size_t i = 0; i < M; ++i) bmax = b[i] > bmax ? b[i] : bmax;
97
98 // C = 1/mu, extended past the lattice by the saturation rate, exactly as
99 // the reference pads mu with max(b)+1 copies of its last column.
100 const std::size_t Cw = Nt + bmax + 2;
101 Matrix<T> C(M, Cw);
102 for (std::size_t i = 0; i < M; ++i) {
103 for (std::size_t k = 0; k < Cw; ++k) {
104 const T& rate = k < Nt ? mu(i, k) : mu(i, Nt - 1);
105 if (rate == zero) throw NumericError("pfqn_ldmx_ec: a load-dependent rate is zero");
106 C(i, k) = one / rate;
107 }
108 }
109 // Cq(i,k) is C(i,k) at the reference's 1-based rate index k >= 1.
110 const auto Cq = [&](std::size_t i, std::size_t k) -> const T& { return C(i, k - 1); };
111
112 res.EC = Matrix<T>(M, Nt, zero);
113 res.E = Matrix<T>(M, Nt + 1, zero);
114 res.Eprime = Matrix<T>(M, Nt + 1, zero);
115
116 for (std::size_t i = 0; i < M; ++i) {
117 const T Cb = Cq(i, b[i]);
118 const T denom = one - res.Lo[i] * Cb;
119 if (denom == zero)
120 throw NumericError(
121 "pfqn_ldmx_ec: the station is saturated by the open classes (Lo * C(b) = 1), the "
122 "effective capacity is unbounded");
123 const T geo = one / denom;
124 // Number of head terms: n0 = 0 .. b_i - 2, empty when b_i = 1.
125 const std::size_t nhead = b[i] >= 2 ? b[i] - 1 : 0;
126
127 std::vector<T> E1(Nt + 1, zero);
128 for (std::size_t n = 0; n <= Nt; ++n) {
129 if (n >= b[i]) {
130 res.E(i, n) = num_pow_int(geo, static_cast<unsigned>(n + 1));
131 res.Eprime(i, n) = Cb * res.E(i, n);
132 continue;
133 }
134 // ---- E1: the geometric tail --------------------------------------
135 if (n == 0) {
136 E1[0] = geo;
137 for (std::size_t j = 1; j + 1 <= b[i]; ++j) E1[0] *= Cq(i, j) / Cb;
138 } else {
139 E1[n] = geo * Cb / Cq(i, n) * E1[n - 1];
140 }
141 // ---- E2 and E2prime: the exact head ------------------------------
142 T E2 = zero, E2p = zero;
143 {
144 T F2 = one; // n0 = 0
145 T F2p = Cq(i, n + 1); // n0 = 0
146 for (std::size_t n0 = 0; n0 < nhead; ++n0) {
147 if (n0 > 0) {
148 const T w = num_traits<T>::from_int(static_cast<long>(n + n0)) /
149 num_traits<T>::from_int(static_cast<long>(n0));
150 F2 = w * res.Lo[i] * Cq(i, n + n0) * F2;
151 F2p = w * res.Lo[i] * Cq(i, n + n0 + 1) * F2p;
152 }
153 E2 += F2;
154 E2p += F2p;
155 }
156 }
157 // ---- E3: the head of the geometric that E1 double counts ----------
158 T E3 = zero;
159 {
160 // F3(n, 0) obeys its own recursion in n, so it is rebuilt here
161 // from the n = 0 seed rather than carried across iterations.
162 T F3 = one;
163 for (std::size_t j = 1; j + 1 <= b[i]; ++j) F3 *= Cq(i, j) / Cb;
164 for (std::size_t k = 1; k <= n; ++k) F3 = Cb / Cq(i, k) * F3;
165 for (std::size_t n0 = 0; n0 < nhead; ++n0) {
166 if (n0 > 0) {
167 const T w = num_traits<T>::from_int(static_cast<long>(n + n0)) /
168 num_traits<T>::from_int(static_cast<long>(n0));
169 F3 = w * res.Lo[i] * Cb * F3;
170 }
171 E3 += F3;
172 }
173 }
174 res.E(i, n) = E1[n] + E2 - E3;
175 if (n + 1 < b[i])
176 res.Eprime(i, n) = Cb * E1[n] + E2p - Cb * E3;
177 else
178 res.Eprime(i, n) = Cb * res.E(i, n);
179 }
180
181 for (std::size_t n = 1; n <= Nt; ++n) {
182 if (res.E(i, n - 1) == zero) throw NumericError("pfqn_ldmx_ec: E vanishes");
183 res.EC(i, n - 1) = Cq(i, n) * res.E(i, n) / res.E(i, n - 1);
184 }
185 }
186 return res;
187}
188
189} // namespace pfqn
190} // namespace line
191
192#endif // LINE_API_PFQN_LDMX_EC_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
NumericError(const std::string &what)
Definition error.h:45
The exception types the port throws.
Dense matrix and non-owning view.
LdmxEcResult< T > pfqn_ldmx_ec(const std::vector< T > &lambda, const Matrix< T > &D, const Matrix< T > &mu)
Bruell-Balbo-Afshari effective-capacity terms for a MIXED open/closed network with limited load depen...
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.
std::vector< T > Lo
(M) open load per station
Matrix< T > Eprime
(M x Nt+1) Eprime-function, column 1+n
Matrix< T > EC
(M x Nt) effective capacity, EC(i,n) for n = 1..Nt
Matrix< T > E
(M x Nt+1) E-function, column 1+n