LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_lld.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_LLD_H
6#define LINE_API_PFQN_PFQN_LLD_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Normalizing constant of a multiclass LIMITED load-dependent closed model.
12 *
13 * Templated port of matlab/src/api/pfqn/pfqn_lld.m. Same recursion, same
14 * arithmetic and the same result as the reference's pfqn_gld, but with the
15 * rate shift saturated at the LIMITED LOAD-DEPENDENCE threshold, which turns
16 * the recursion's state space finite and lets it be memoised:
17 *
18 * g(m,n,j) = g(m-1,n,0) + sum_r L(m,r)/alpha_m(j+1) * g(m,n-e_r,min(j+1,s_m-1))
19 *
20 * with j the number of shifts row m has taken. Once j >= s_m-1, where s_m is
21 * the population past which alpha_m stays constant, a further shift leaves the
22 * row unchanged over the columns the recursion can still read, so the state
23 * (m,n,j) repeats and one memo answers what the unmemoised recursion recomputes
24 * down an exponential tree. The state space is M * prod_r(N_r+1) * max_k s_k.
25 *
26 * Every shortcut of the reference's pfqn_gld cascade is kept and evaluated on
27 * the same materialised arguments: the single-station closed form, the
28 * single-class kernel (pfqn_lldsingle), and the exact fixed-rate constant
29 * (pfqn_ca, MATLAB's pfqn_nc 'exact') when no station of the block is load
30 * dependent. The third output s holds the detected per-station thresholds.
31 *
32 * ARITHMETIC. Field operations only: the closed form at m = 1 is formed as a
33 * multinomial times integer powers rather than through exp/log, so the routine
34 * is EXACT in rational arithmetic, like pfqn_gld and pfqn_lldsingle here. The
35 * threshold scan tests exact equality first and then an eps-relative tie on
36 * FINITE pairs, as the reference does.
37 */
38
39#include <cmath>
40#include <cstddef>
41#include <limits>
42#include <map>
43#include <vector>
44
48#include "line/num/number.h"
49#include "line/util/error.h"
50#include "line/util/matrix.h"
51
52namespace line {
53namespace pfqn {
54
55/** Return value of pfqn_lld, mirroring [G, lG, s]. */
56template <class T>
57struct LldResult {
58 T G; ///< normalizing constant in the requested arithmetic
59 double lG; ///< log of the constant
60 std::vector<int> s; ///< (M) per-station thresholds, alpha_i(n) = alpha_i(s_i) for n >= s_i
61};
62
63/**
64 * @brief Normalizing constant of a multiclass LIMITED load-dependent closed model.
65 *
66 * @param L (M x R) service demands
67 * @param N (R) population vector
68 * @param mu (M x >= sum(N)) load-dependent rates, alpha_i(j) = mu(i,j); empty for
69 * all ones (every station a single server)
70 */
71template <class T>
72LldResult<T> pfqn_lld(const Matrix<T>& L, const std::vector<int>& N, const Matrix<T>& mu) {
73 const std::size_t M = L.rows();
74 const std::size_t R = N.size();
75 if (!L.empty() && L.cols() != R)
76 throw InputError("pfqn_lld: demand matrix and population vector disagree on the class count");
77 const T zero = num_traits<T>::from_int(0);
78 const T one = num_traits<T>::from_int(1);
79
80 long Ntot = 0;
81 for (int v : N) {
82 if (v < 0) throw InputError("pfqn_lld: negative population");
83 Ntot += v;
84 }
85 const std::size_t Ntu = static_cast<std::size_t>(Ntot);
86
87 // As in pfqn_gld, the mu default has to precede every branch that reads mu.
88 Matrix<T> mu2 = mu.empty() ? Matrix<T>(M, Ntu, one) : mu;
89 if (!mu2.empty() && mu2.rows() != M) throw InputError("pfqn_lld: mu has the wrong station count");
90 const std::size_t ncols = mu2.empty() ? 0 : mu2.cols();
91
92 // ---- s_k: the smallest offset past which the rate row is constant ----
93 // Equality is tested first so that an infinite rate ties with itself instead
94 // of producing Inf-Inf; the tolerance is confined to FINITE pairs.
95 std::vector<int> s(M, 1);
96 if (ncols >= 1) {
97 const double eps = std::numeric_limits<double>::epsilon();
98 for (std::size_t m = 0; m < M; ++m) {
99 s[m] = static_cast<int>(ncols);
100 const T tail = mu2(m, ncols - 1);
101 const double tl = num_traits<T>::to_double(tail);
102 for (std::size_t n = ncols; n >= 2; --n) {
103 const T prev = mu2(m, n - 2);
104 const double pv = num_traits<T>::to_double(prev);
105 const bool tie = prev == tail ||
106 (std::isfinite(tl) && std::isfinite(pv) &&
107 std::fabs(pv - tl) <= eps * std::max(std::fabs(tl), 1.0));
108 if (tie)
109 s[m] = static_cast<int>(n - 1);
110 else
111 break;
112 }
113 }
114 }
115 for (std::size_t m = 0; m < M; ++m)
116 if (s[m] < 1) s[m] = 1;
117
118 // memo over (m, j, n)
119 std::map<std::vector<int>, T> memo;
120
121 // materialize(m, n, j): rows 0..m-2 are never shifted, row m-1 is shifted j
122 // times, every row truncated by one column per job already placed.
123 const auto materialize = [&](std::size_t m, const std::vector<int>& n, int j, Matrix<T>& Lm,
124 Matrix<T>& mum) {
125 long sn = 0;
126 for (int v : n) sn += v;
127 const long cols = static_cast<long>(ncols) - (Ntot - sn);
128 Lm = Matrix<T>(m, R, zero);
129 for (std::size_t i = 0; i < m; ++i)
130 for (std::size_t r = 0; r < R; ++r) Lm(i, r) = L(i, r);
131 const std::size_t cu = cols > 0 ? static_cast<std::size_t>(cols) : 0;
132 mum = Matrix<T>(m, cu, zero);
133 if (cols > 0 && m > 0) {
134 for (std::size_t i = 0; i + 1 < m; ++i)
135 for (std::size_t c = 0; c < cu; ++c) mum(i, c) = mu2(i, c);
136 for (std::size_t c = 0; c < cu; ++c)
137 mum(m - 1, c) = mu2(m - 1, static_cast<std::size_t>(j) + c);
138 }
139 };
140
141 std::function<T(std::size_t, const std::vector<int>&, int)> node;
142 std::function<T(const Matrix<T>&, const std::vector<int>&, const Matrix<T>&, std::size_t, int)>
143 gld_node;
144
145 node = [&](std::size_t m, const std::vector<int>& n, int j) -> T {
146 std::vector<int> key;
147 key.reserve(2 + R);
148 key.push_back(static_cast<int>(m));
149 key.push_back(j);
150 for (int v : n) key.push_back(v);
151 const auto it = memo.find(key);
152 if (it != memo.end()) return it->second;
153 Matrix<T> Lm, mum;
154 materialize(m, n, j, Lm, mum);
155 const T g = gld_node(Lm, n, mum, m, j);
156 memo[key] = g;
157 return g;
158 };
159
160 gld_node = [&](const Matrix<T>& Lm, const std::vector<int>& n, const Matrix<T>& mum,
161 std::size_t m, int j) -> T {
162 long sn = 0;
163 for (int v : n) sn += v;
164 if (m == 1) {
165 for (std::size_t r = 0; r < R; ++r)
166 if (n[r] > 0 && Lm(0, r) == zero) return zero;
167 // exp(factln(sum n) - sum factln(n) + sum_r n_r log L_r - sum_k log mu(1,k))
168 T g = num_multinomial<T>(n);
169 for (std::size_t r = 0; r < R; ++r)
170 if (n[r] > 0) g *= num_pow_int(Lm(0, r), static_cast<unsigned>(n[r]));
171 for (long k = 0; k < sn; ++k) g = T(g / mum(0, static_cast<std::size_t>(k)));
172 return g;
173 }
174 if (R == 1) {
175 return pfqn_lldsingle(Lm, static_cast<int>(sn), mum).G;
176 }
177 if (Lm.empty()) return zero;
178 bool isLoadDep = false;
179 std::vector<bool> isInfServer(m, false);
180 for (std::size_t ist = 0; ist < m; ++ist) {
181 bool allOne = true, allRamp = true;
182 for (long k = 0; k < sn; ++k) {
183 const T& v = mum(ist, static_cast<std::size_t>(k));
184 if (v != one) allOne = false;
185 if (v != num_traits<T>::from_int(k + 1)) allRamp = false;
186 }
187 // MATLAB: min(row)==1 & max(row)==1 is [] on an empty row, so the
188 // branch falls through to all([]) = true, the infinite-server arm.
189 if (sn > 0 && allOne) {
190 isInfServer[ist] = false;
191 } else if (allRamp) {
192 isInfServer[ist] = true;
193 } else {
194 isLoadDep = true;
195 }
196 }
197 if (!isLoadDep) {
198 std::size_t nli = 0;
199 for (std::size_t ist = 0; ist < m; ++ist)
200 if (!isInfServer[ist]) ++nli;
201 Matrix<T> Lli(nli > 0 ? nli : 1, R, zero);
202 Matrix<T> Zli(1, R, zero);
203 std::size_t q = 0;
204 for (std::size_t ist = 0; ist < m; ++ist) {
205 if (isInfServer[ist]) {
206 for (std::size_t r = 0; r < R; ++r) Zli(0, r) += Lm(ist, r);
207 } else {
208 for (std::size_t r = 0; r < R; ++r) Lli(q, r) = Lm(ist, r);
209 ++q;
210 }
211 }
212 return pfqn_ca(Lli, n, Zli).G;
213 }
214 if (m == 0) return zero;
215 if (sn == 0) return one;
216 // The recursion itself, memoised through node(). The shift saturates at
217 // s(m)-1, past which the row the child would see is the one it sees now.
218 T g = node(m - 1, n, 0);
219 for (std::size_t r = 0; r < R; ++r) {
220 if (n[r] > 0) {
221 const int jn = std::min(j + 1, s[m - 1] - 1);
222 g += T(T(Lm(m - 1, r) / mum(m - 1, 0)) * node(m, oner(n, r + 1), jn));
223 }
224 }
225 return g;
226 };
227
228 LldResult<T> res;
229 res.s = s;
230 res.G = node(M, N, 0);
232 return res;
233}
234
235/** Overload with all rates equal to one, i.e. every station a single server. */
236template <class T>
237LldResult<T> pfqn_lld(const Matrix<T>& L, const std::vector<int>& N) {
238 return pfqn_lld(L, N, Matrix<T>());
239}
240
241} // namespace pfqn
242} // namespace line
243
244#endif // LINE_API_PFQN_PFQN_LLD_H
InputError(const std::string &what)
Definition error.h:105
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
The exception types the port throws.
Dense matrix and non-owning view.
T num_multinomial(const std::vector< int > &m)
Multinomial coefficient sum(m)!
NcResult< T > pfqn_lldsingle(const Matrix< T > &L, int N, const Matrix< T > &mu)
Exact normalizing constant of a SINGLE-CLASS closed network whose stations are LIMITED load dependent...
std::vector< int > oner(const std::vector< int > &N, std::size_t r)
matlab/src/util/oner.m: decrement position r of N, with r given 1-based and r == 0 meaning "leave N a...
NcResult< T > pfqn_ca(const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &Z)
Convolution algorithm for the exact normalizing constant of a closed product-form network (Buzen 1973...
Definition pfqn_ca.h:120
LldResult< T > pfqn_lld(const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &mu)
Normalizing constant of a multiclass LIMITED load-dependent closed model.
Definition pfqn_lld.h:72
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
T num_pow_int(const T &base, unsigned e)
Integer power, valid in any field (no transcendental requirement).
Definition number.h:204
Number-type abstraction for the templated API port.
Scaffolding shared by the approximate-MVA family.
Convolution algorithm for the exact normalizing constant of a closed product-form network (Buzen 1973...
Exact normalizing constant of a SINGLE-CLASS closed network whose stations are LIMITED load dependent...
Return value of pfqn_lld, mirroring [G, lG, s].
Definition pfqn_lld.h:57
double lG
log of the constant
Definition pfqn_lld.h:59
std::vector< int > s
(M) per-station thresholds, alpha_i(n) = alpha_i(s_i) for n >= s_i
Definition pfqn_lld.h:60
T G
normalizing constant in the requested arithmetic
Definition pfqn_lld.h:58