LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_divdiff_ld.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_DIVDIFF_LD_H
6#define LINE_API_PFQN_PFQN_DIVDIFF_LD_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Exact load-dependent normalizing constant by divided differences of a
12 * single-class constant.
13 *
14 * Templated port of matlab/src/api/pfqn/pfqn_divdiff_ld.m. Evaluates the
15 * multiclass load-dependent normalizing constant as the N-th order finite
16 * difference, in the class populations, of a SINGLE-CLASS load-dependent
17 * constant:
18 *
19 * G(N) = sum_{0<=n<=N} (-1)^(|N|-|n|) / (N_1! ... N_R!)
20 * * prod_r nchoosek(N_r,n_r) * Gld_n(|N|)
21 *
22 * where |N| = sum_r N_r, |n| = sum_r n_r and Gld_n(|N|) is the normalizing
23 * constant of the single-class model over the same M stations, with the same
24 * load-dependent rates alpha_i(.) = mu(i,.), total population |N| and
25 * aggregated demands rho_i(n) = sum_r n_r L(i,r). Since
26 * nchoosek(N_r,n_r)/N_r! = 1/(n_r! (N_r-n_r)!), the coefficient is evaluated
27 * in the equivalent factorial form, which needs no binomial.
28 *
29 * The identity holds for arbitrary rate functions alpha_i(.), hence it covers
30 * load-independent, infinite-server, multiserver and limited load-dependent
31 * stations alike. Cost is prod_r (N_r+1) evaluations of pfqn_gldsingle.
32 *
33 * NUMERICS. The sum alternates in sign and its terms are much larger than the
34 * result, so it is evaluated as a signed log-sum-exp, which removes the range
35 * problem but not the cancellation. The reference raises warnings past 15
36 * decimal digits lost and when the sum comes out negative; warnings become
37 * outputs on this side: lossDigits is reported and a negative sum returns NaN
38 * exactly as MATLAB does, so the routine is gated on
39 * num_traits<T>::has_transcendental like pfqn_explicit_ld.
40 */
41
42#include <cmath>
43#include <cstddef>
44#include <limits>
45#include <vector>
46
50#include "line/num/number.h"
51#include "line/util/error.h"
52#include "line/util/matrix.h"
54
55namespace line {
56namespace pfqn {
57
58/** Return value of pfqn_divdiff_ld, mirroring [lG, G, lossDigits]. */
59template <class T>
61 T lG; ///< logarithm of the normalizing constant
62 T G; ///< the normalizing constant
63 double lossDigits = 0.0; ///< decimal digits lost to cancellation in the alternating sum
64};
65
66/**
67 * @brief Exact load-dependent normalizing constant by divided differences of a
68 * single-class constant.
69 *
70 * @param L (M x R) service demands
71 * @param N (R) population vector
72 * @param Z (D x R) think times, folded in as an infinite server; empty for none
73 * @param mu (M x >= sum(N)) load-dependent rates, alpha_i(j) = mu(i,j); empty
74 * for load-independent stations (all ones)
75 */
76template <class T>
77DivdiffLdResult<T> pfqn_divdiff_ld(const Matrix<T>& L, const std::vector<int>& N,
78 const Matrix<T>& Z, const Matrix<T>& mu) {
80 "pfqn_divdiff_ld requires transcendental arithmetic: the alternating sum is "
81 "carried as a signed log-sum-exp. Use pfqn_gld for the same constant in exact "
82 "arithmetic");
83 using std::exp;
84 const T zero = num_traits<T>::from_int(0);
85 const T one = num_traits<T>::from_int(1);
86 const double dinf = std::numeric_limits<double>::infinity();
87 const double dnan = std::numeric_limits<double>::quiet_NaN();
88 const std::size_t R = N.size();
89
91 res.lossDigits = 0.0;
92 long Nt = 0;
93 for (std::size_t r = 0; r < R; ++r) Nt += N[r];
94 if (Nt < 0) {
95 res.lG = num_traits<T>::from_double(-dinf);
96 res.G = zero;
97 return res;
98 }
99 if (Nt == 0) {
100 res.lG = zero;
101 res.G = one;
102 return res;
103 }
104 const std::size_t M0 = L.empty() ? 0 : L.rows();
105 const std::size_t Ntu = static_cast<std::size_t>(Nt);
106
107 // mu defaults to ones(M, Nt); otherwise checked and trimmed to Nt columns so
108 // that every rate used downstream is positive.
109 Matrix<T> mu2(M0, Ntu, one);
110 if (!mu.empty()) {
111 if (mu.rows() != M0)
112 throw InputError(
113 "pfqn_divdiff_ld: the load-dependent rate matrix must have one row per station "
114 "of L.");
115 if (mu.cols() < Ntu)
116 throw InputError(
117 "pfqn_divdiff_ld: the load-dependent rate matrix must have at least sum(N) "
118 "columns.");
119 for (std::size_t i = 0; i < M0; ++i)
120 for (std::size_t j = 0; j < Ntu; ++j) mu2(i, j) = mu(i, j);
121 }
122
123 // Z folded in as an infinite-server station with rates 1..Nt
124 T Zsum = zero;
125 std::vector<T> Zrow(R, zero);
126 if (!Z.empty()) {
127 for (std::size_t d = 0; d < Z.rows(); ++d)
128 for (std::size_t r = 0; r < Z.cols() && r < R; ++r) {
129 Zrow[r] += Z(d, r);
130 Zsum += Z(d, r);
131 }
132 }
133 const bool withZ = !Z.empty() && Zsum > zero;
134 const std::size_t M = M0 + (withZ ? 1 : 0);
135 Matrix<T> L2(M, R, zero);
136 for (std::size_t i = 0; i < M0; ++i)
137 for (std::size_t r = 0; r < R; ++r) L2(i, r) = L(i, r);
138 Matrix<T> mu3(M, Ntu, one);
139 for (std::size_t i = 0; i < M0; ++i)
140 for (std::size_t j = 0; j < Ntu; ++j) mu3(i, j) = mu2(i, j);
141 if (withZ) {
142 for (std::size_t r = 0; r < R; ++r) L2(M0, r) = Zrow[r];
143 for (std::size_t j = 0; j < Ntu; ++j)
144 mu3(M0, j) = num_traits<T>::from_int(static_cast<long>(j) + 1);
145 }
146
147 if (M == 0) {
148 res.lG = num_traits<T>::from_double(-dinf);
149 res.G = zero;
150 return res;
151 }
152 for (std::size_t i = 0; i < M; ++i)
153 for (std::size_t r = 0; r < R; ++r)
154 if (L2(i, r) < zero)
155 throw InputError("pfqn_divdiff_ld: the demand matrix must be nonnegative.");
156 for (std::size_t i = 0; i < M; ++i)
157 for (std::size_t j = 0; j < Ntu; ++j)
158 if (!(mu3(i, j) > zero))
159 throw InputError(
160 "pfqn_divdiff_ld: the load-dependent rates must be strictly positive.");
161
162 if (R == 1) {
163 // in a single class the difference operator is the identity, so the
164 // recursion is used directly and no cancellation is incurred
165 const NcResult<T> g = pfqn_gldsingle(L2, static_cast<int>(Nt), mu3);
167 res.G = exp(res.lG);
168 return res;
169 }
170
171 // ---- alternating sum over the sub-populations 0 <= n <= N ----
172 std::vector<T> lterm;
173 std::vector<double> sterm;
174 std::vector<int> n(R, 0);
175 const T minf = num_traits<T>::from_double(-dinf);
176 while (true) {
177 long sn = 0;
178 for (std::size_t r = 0; r < R; ++r) sn += n[r];
179 if (sn > 0) {
180 // n = 0 leaves every induced demand at zero, so its single-class
181 // constant vanishes at sum(N)>0 and the term is left out of the sum
182 Matrix<T> Ln(M, 1, zero);
183 for (std::size_t i = 0; i < M; ++i)
184 for (std::size_t r = 0; r < R; ++r)
185 Ln(i, 0) += L2(i, r) * num_traits<T>::from_int(n[r]);
186 const NcResult<T> g = pfqn_gldsingle(Ln, static_cast<int>(Nt), mu3);
188 for (std::size_t r = 0; r < R; ++r) {
189 lt -= detail::num_factln<T>(num_traits<T>::from_int(n[r]));
190 lt -= detail::num_factln<T>(num_traits<T>::from_int(N[r] - n[r]));
191 }
192 lterm.push_back(lt);
193 sterm.push_back(((Nt - sn) % 2 == 0) ? 1.0 : -1.0);
194 } else {
195 lterm.push_back(minf);
196 sterm.push_back(0.0);
197 }
198 if (!next_pop(n, N)) break;
199 }
200
201 const detail::SignedLse<T> s = detail::explicit_signed_logsumexp(lterm, sterm);
202 res.lossDigits = s.lossDigits;
203 if (s.sgn == 0) {
204 res.lG = minf;
205 res.G = zero;
206 return res;
207 } else if (s.sgn < 0) {
208 // The alternating sum returned a negative value: double precision is
209 // exhausted by cancellation. MATLAB warns and returns NaN; lossDigits
210 // carries the diagnostic here.
211 res.lG = num_traits<T>::from_double(dnan);
212 res.G = num_traits<T>::from_double(dnan);
213 return res;
214 }
215 res.lG = s.lS;
216 res.G = exp(res.lG);
217 // lossDigits > 15: the reference warns that the result is unreliable in double
218 // precision; the count is returned for the caller to act on.
219 return res;
220}
221
222template <class T>
223DivdiffLdResult<T> pfqn_divdiff_ld(const Matrix<T>& L, const std::vector<int>& N,
224 const Matrix<T>& Z) {
225 return pfqn_divdiff_ld(L, N, Z, Matrix<T>());
226}
227
228template <class T>
229DivdiffLdResult<T> pfqn_divdiff_ld(const Matrix<T>& L, const std::vector<int>& N) {
230 return pfqn_divdiff_ld(L, N, Matrix<T>(), Matrix<T>());
231}
232
233} // namespace pfqn
234} // namespace line
235
236#endif // LINE_API_PFQN_PFQN_DIVDIFF_LD_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.
NcResult< T > pfqn_gldsingle(const Matrix< T > &L, int N, const Matrix< T > &mu)
Exact normalizing constant of a SINGLE-CLASS closed network whose stations are load dependent.
DivdiffLdResult< T > pfqn_divdiff_ld(const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &Z, const Matrix< T > &mu)
Exact load-dependent normalizing constant by divided differences of a single-class constant.
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
bool next_pop(std::vector< int > &n, const std::vector< int > &N)
Advance n to the next population vector in the lattice 0 <= n <= N, odometer order with the last clas...
Definition population.h:56
Number-type abstraction for the templated API port.
Shared scalar machinery for the integration / asymptotic members of the pfqn family (pfqn_le,...
Explicit closed-form normalizing constant of a multiclass closed network.
Exact normalizing constant of a SINGLE-CLASS closed network whose stations are load dependent.
Population-vector enumeration and combinatorics.
Return value of pfqn_divdiff_ld, mirroring [lG, G, lossDigits].
T G
the normalizing constant
T lG
logarithm of the normalizing constant
double lossDigits
decimal digits lost to cancellation in the alternating sum
Return value of the normalizing-constant family, mirroring Ret.pfqnNc.
Definition pfqn_ca.h:44
double lG
log of the constant, always a double and always finite
Definition pfqn_ca.h:46