LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_ncldmx.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_NCLDMX_H
6#define LINE_API_PFQN_NCLDMX_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Normalizing constant of a MIXED open/closed network with limited load
12 * dependence.
13 *
14 * Templated port of matlab/src/api/pfqn/pfqn_ncldmx.m.
15 *
16 * The closed-conditional constant of a mixed limited-load-dependent network is
17 * the constant of a purely CLOSED load-dependent network in which every
18 * station carries the Bruell-Balbo-Afshari effective-capacity rate
19 *
20 * mu_i^eff(n) = 1 / EC_i(n),
21 *
22 * with EC from pfqn_ldmx_ec folding the open classes into the closed
23 * subnetwork. The open classes contribute the separable prefactor
24 *
25 * Gopen = prod_i E_i(0),
26 *
27 * which reduces to prod_i 1/(1 - rho_i) in the load-independent limit. The two
28 * are returned separately, as in the reference, because callers use the closed
29 * one for the closed-class ratios G(N - e_r)/G(N) and the open one only for
30 * the joint state probabilities.
31 *
32 * OPEN CLASSES are marked by a NEGATIVE population, as everywhere in this port
33 * (see pfqn_nc); MATLAB uses Inf, which has no counterpart in an exact field.
34 *
35 * Arithmetic: EXACT-CAPABLE. pfqn_ldmx_ec and pfqn_ncld are both exact, and
36 * everything this routine adds is a reciprocal and a product. Gopen is
37 * returned as a value of T, not only as its logarithm, which is what lets the
38 * mixed constant stay in the rational field end to end.
39 *
40 * MEAN MEASURES come out of the same identification, without ever enumerating
41 * the closed population lattice -- which is what makes this the
42 * normalizing-constant counterpart of pfqn_mvaldmx rather than a rename of it:
43 *
44 * - closed throughputs are the ratios X_r = G(N - e_r)/G(N);
45 * - closed queue lengths are the conditional normalizing-constant recursion
46 * of the load-dependent closed network (pfqn_mushift / pfqn_fnc), applied
47 * to the effective-capacity rates;
48 * - open queue lengths are the Bruell-Balbo-Afshari sum
49 * Q_ir = lambda_r D_ir sum_n (n+1) EC_i(n+1) P_i(n) with its SATURATED
50 * TAIL FOLDED ONTO THE CLOSED MEAN. EC_i(n) is constant for n >= b_i, the
51 * level where the rate row stops growing, so writing
52 * EC_i(n) = EC_i^inf + delta_i(n) with delta_i(n) = 0 for n >= b_i leaves
53 *
54 * Q_ir = lambda_r D_ir [ EC_i^inf (Q_i^closed + 1)
55 * + sum_{n=0}^{b_i-2} (n+1) delta_i(n+1) P_i(n) ]
56 *
57 * using sum_n P_i(n) = 1 and sum_n n P_i(n) = Q_i^closed. Only the first
58 * b_i-1 marginals survive, and b_i is the SERVER COUNT, not the population:
59 * a single-server station needs none at all and the formula collapses to
60 * the classical lambda_r D_ir (1 + Q_i^closed)/(1 - rho_i).
61 *
62 * The ratios themselves are taken in the log domain and converted back with
63 * from_double, exactly as solver_ncld's closed branch does: a mean measure is a
64 * ratio of constants, not a constant, so it leaves the exact field there.
65 */
66
67#include <cmath>
68#include <cstddef>
69#include <limits>
70#include <vector>
71
76#include "line/num/number.h"
77#include "line/util/error.h"
78#include "line/util/matrix.h"
79
80namespace line {
81namespace pfqn {
82
83template <class T>
85 T G; ///< closed-conditional normalizing constant
86 double lG; ///< its logarithm
87 T Gopen; ///< the open-class prefactor prod_i E_i(0)
88 double lGopen;///< its logarithm
89 Matrix<T> EC; ///< the effective-capacity terms, for the caller's mean measures
90 std::vector<T> XN; ///< (R) throughputs: G(N-e_r)/G(N) closed, lambda_r open
91 Matrix<T> QN; ///< (M x R) mean queue lengths
92};
93
94namespace detail {
95
96/**
97 * pfqn_ncld, with the empty-station residual network handled explicitly: a
98 * network reduced to its think times alone has G(N) = prod_r Z_r^N_r / N_r!,
99 * and no G at all when a class has jobs but neither demand nor think time.
100 */
101template <class T>
102double ncldmx_lg(const Matrix<T>& L, const std::vector<int>& N, const Matrix<T>& Z,
103 const Matrix<T>& mu, NcldMethod method, const T& atol, const NcOptions& nopt) {
104 if (L.rows() == 0) {
105 bool empty = true;
106 for (std::size_t r = 0; r < N.size(); ++r)
107 if (N[r] > 0) { empty = false; break; }
108 if (empty) return 0.0;
109 double acc = 0.0;
110 for (std::size_t r = 0; r < N.size(); ++r) {
111 if (N[r] <= 0) continue;
112 double zr = 0.0;
113 for (std::size_t i = 0; i < Z.rows(); ++i) zr += num_traits<T>::to_double(Z(i, r));
114 if (!(zr > 0.0)) return -std::numeric_limits<double>::infinity();
115 acc += N[r] * std::log(zr) - std::lgamma(static_cast<double>(N[r]) + 1.0);
116 }
117 return acc;
118 }
119 return pfqn_ncld(L, N, Z, mu, method, atol, nopt).lG;
120}
121
122/** A copy of A with row `skip` removed. */
123template <class T>
124Matrix<T> ncldmx_drop_row(const Matrix<T>& A, std::size_t skip) {
125 const std::size_t rows = A.rows() == 0 ? 0 : A.rows() - 1;
126 Matrix<T> out(rows, A.cols(), num_traits<T>::from_int(0));
127 std::size_t w = 0;
128 for (std::size_t i = 0; i < A.rows(); ++i) {
129 if (i == skip) continue;
130 for (std::size_t j = 0; j < A.cols(); ++j) out(w, j) = A(i, j);
131 ++w;
132 }
133 return out;
134}
135
136/** Non-negative integer vectors k with sum(k) == n and k <= cap. */
137inline void ncldmx_compositions(int rem, const std::vector<int>& cap, std::size_t idx,
138 std::vector<int>& cur, std::vector<std::vector<int> >& out) {
139 if (idx + 1 == cap.size()) {
140 if (rem <= cap[idx]) {
141 cur[idx] = rem;
142 out.push_back(cur);
143 }
144 return;
145 }
146 const int hi = rem < cap[idx] ? rem : cap[idx];
147 for (int v = 0; v <= hi; ++v) {
148 cur[idx] = v;
149 ncldmx_compositions(rem - v, cap, idx + 1, cur, out);
150 }
151 cur[idx] = 0;
152}
153
154/**
155 * First column of the trailing constant run of a limited load-dependence row,
156 * i.e. the level b with mu(n) = mu(b) for every n >= b. This is the level
157 * pfqn_ldmx_ec infers, and hence the one past which EC is constant. One-based.
158 */
159template <class T>
160std::size_t ncldmx_lld_level(const Matrix<T>& mu, std::size_t row) {
161 std::size_t b = mu.cols();
162 if (b == 0) return 1;
163 while (b > 1 && mu(row, b - 2) == mu(row, b - 1)) --b;
164 return b;
165}
166
167} // namespace detail
168
169/**
170 * @brief Normalizing constant of a MIXED open/closed network with limited
171 * load dependence.
172 *
173 * @param lambda (R) arrival rates; must be zero on the closed classes
174 * @param D (M x R) service demands
175 * @param N (R) populations; a NEGATIVE entry marks an open class
176 * @param Z (K x R) think times of the closed classes
177 * @param mu (M x >=Kc) load-dependent rate lattice
178 */
179template <class T>
180NcldmxResult<T> pfqn_ncldmx(const std::vector<T>& lambda, const Matrix<T>& D,
181 const std::vector<int>& N, const Matrix<T>& Z, const Matrix<T>& mu,
182 NcldMethod method, const T& atol, const NcOptions& nopt) {
183 const std::size_t M = D.rows();
184 const std::size_t R = D.cols();
185 if (N.size() != R) throw InputError("pfqn_ncldmx: D and N disagree on the class count");
186 if (lambda.size() != R)
187 throw InputError("pfqn_ncldmx: lambda and D disagree on the class count");
188 if (mu.rows() != M) throw InputError("pfqn_ncldmx: mu and D disagree on the station count");
189
190 const T zero = num_traits<T>::from_int(0);
191 const T one = num_traits<T>::from_int(1);
192
193 std::vector<std::size_t> openCl, closedCl;
194 for (std::size_t r = 0; r < R; ++r) (N[r] < 0 ? openCl : closedCl).push_back(r);
195 for (std::size_t r : closedCl)
196 if (lambda[r] != zero)
197 throw InputError("pfqn_ncldmx: an arrival rate is specified on a closed class");
198
199 std::vector<int> Nc(closedCl.size(), 0);
200 long Kc = 0;
201 for (std::size_t a = 0; a < closedCl.size(); ++a) {
202 Nc[a] = N[closedCl[a]];
203 Kc += Nc[a];
204 }
205 const std::size_t width = static_cast<std::size_t>(Kc > 0 ? Kc : 1);
206
207 // PAD the rate lattice out to the closed population, then add one extra
208 // column, matching how pfqn_mvaldmx is fed. Padding NEVER SHORTENS: the row
209 // the caller handed in is kept in full and only extended, because
210 // pfqn_ldmx_ec reads the saturation level b_i off this row -- the first
211 // column equal to the last -- and a row cut at the closed population
212 // declares a c-server station saturated at min(n,c) with n < c whenever c
213 // exceeds it. Cutting to width+1 read the M/M/3 of test_nc as an M/M/2
214 // (mean 3.4286 against the exact 1.7368) and, with no closed class at all,
215 // as an M/M/1. The reference pads with `repmat(mup(:,end),...)` and appends,
216 // never truncating; Pfqn_ncldmx.java and ncldmx.py already take the wider
217 // of the two.
218 const std::size_t padCols = (mu.cols() > width ? mu.cols() : width) + 1;
219 Matrix<T> mup(M, padCols);
220 for (std::size_t i = 0; i < M; ++i)
221 for (std::size_t k = 0; k < padCols; ++k)
222 mup(i, k) = mu.cols() == 0 ? one : mu(i, k < mu.cols() ? k : mu.cols() - 1);
223
224 std::vector<T> lambdao(R, zero);
225 for (std::size_t r : openCl) lambdao[r] = lambda[r];
226
227 const LdmxEcResult<T> ec = pfqn_ldmx_ec(lambdao, D, mup);
228
229 NcldmxResult<T> res;
230 res.EC = ec.EC;
231 res.Gopen = one;
232 for (std::size_t i = 0; i < M; ++i) res.Gopen *= ec.E(i, 0);
234
235 Matrix<T> Dc(M, closedCl.size());
236 for (std::size_t i = 0; i < M; ++i)
237 for (std::size_t a = 0; a < closedCl.size(); ++a) Dc(i, a) = D(i, closedCl[a]);
238 Matrix<T> Zc(Z.empty() ? 0 : Z.rows(), closedCl.size());
239 for (std::size_t i = 0; i < Zc.rows(); ++i)
240 for (std::size_t a = 0; a < closedCl.size(); ++a) Zc(i, a) = Z(i, closedCl[a]);
241
242 Matrix<T> muEff(M, width);
243 for (std::size_t i = 0; i < M; ++i)
244 for (std::size_t k = 0; k < width; ++k) {
245 if (ec.EC(i, k) == zero)
246 throw NumericError("pfqn_ncldmx: an effective capacity term is zero");
247 muEff(i, k) = one / ec.EC(i, k);
248 }
249
250 if (Kc == 0) {
251 res.G = one;
252 res.lG = 0.0;
253 } else {
254 const NcldResult<T> nc = pfqn_ncld(Dc, Nc, Zc, muEff, method, atol, nopt);
255 res.G = nc.G;
256 res.lG = nc.lG;
257 }
258
259 // ---- mean measures ----
260 const std::size_t C = closedCl.size();
261 res.XN.assign(R, zero);
262 res.QN = Matrix<T>(M, R, zero);
263 for (std::size_t r : openCl) res.XN[r] = lambda[r];
264
265 std::vector<double> lGr(C > 0 ? C : 1, 0.0);
266 if (Kc > 0) {
267 for (std::size_t a = 0; a < C; ++a) {
268 if (Nc[a] <= 0) continue;
269 std::vector<int> Ncr = Nc;
270 --Ncr[a];
271 lGr[a] = detail::ncldmx_lg(Dc, Ncr, Zc, muEff, method, atol, nopt);
272 res.XN[closedCl[a]] = num_traits<T>::from_double(std::exp(lGr[a] - res.lG));
273 }
274 // closed queue lengths: the conditional normalizing-constant recursion of
275 // the load-dependent closed network, on the effective-capacity rates
276 for (std::size_t i = 0; i < M; ++i) {
277 bool anyDemand = false;
278 for (std::size_t a = 0; a < C; ++a)
279 if (Dc(i, a) > zero) { anyDemand = true; break; }
280 if (!anyDemand) continue;
281 const Matrix<T> muhat = pfqn_mushift(muEff, i);
282 Matrix<T> muhatRow(1, muhat.cols(), zero);
283 for (std::size_t k = 0; k < muhat.cols(); ++k) muhatRow(0, k) = muhat(i, k);
284 const FncResult<T> fnc = pfqn_fnc(muhatRow);
285 const double cshift = num_traits<T>::to_double(fnc.c[0]);
286 const Matrix<T> Dminus = detail::ncldmx_drop_row(Dc, i);
287 const Matrix<T> muminus = detail::ncldmx_drop_row(muEff, i);
288 Matrix<T> DcPlus(M + 1, C, zero);
289 for (std::size_t i2 = 0; i2 < M; ++i2)
290 for (std::size_t a = 0; a < C; ++a) DcPlus(i2, a) = Dc(i2, a);
291 for (std::size_t a = 0; a < C; ++a) DcPlus(M, a) = Dc(i, a);
292 Matrix<T> muhatPlus(M + 1, muhat.cols(), zero);
293 for (std::size_t i2 = 0; i2 < M; ++i2)
294 for (std::size_t k = 0; k < muhat.cols(); ++k) muhatPlus(i2, k) = muhat(i2, k);
295 for (std::size_t k = 0; k < muhat.cols() && k < fnc.mu.cols(); ++k)
296 muhatPlus(M, k) = fnc.mu(0, k);
297 for (std::size_t a = 0; a < C; ++a) {
298 if (Nc[a] <= 0 || !(Dc(i, a) > zero)) continue;
299 std::vector<int> Ncr = Nc;
300 --Ncr[a];
301 const double lGhat = detail::ncldmx_lg(Dc, Ncr, Zc, muhat, method, atol, nopt);
302 const double lGhatf =
303 detail::ncldmx_lg(DcPlus, Ncr, Zc, muhatPlus, method, atol, nopt);
304 const double lGminus =
305 detail::ncldmx_lg(Dminus, Ncr, Zc, muminus, method, atol, nopt);
306 const double CQ =
307 (std::exp(lGhatf - lGhat) - 1.0) + cshift * (std::exp(lGminus - lGhat) - 1.0);
308 const double ldDemand = std::log(num_traits<T>::to_double(Dc(i, a))) + lGhat -
309 std::log(num_traits<T>::to_double(muEff(i, 0))) - lGr[a];
310 res.QN(i, closedCl[a]) = num_traits<T>::from_double(
311 std::exp(ldDemand) * num_traits<T>::to_double(res.XN[closedCl[a]]) * (1.0 + CQ));
312 }
313 }
314 }
315
316 // open queue lengths, with the saturated tail of EC folded onto the closed mean
317 if (!openCl.empty()) {
318 for (std::size_t i = 0; i < M; ++i) {
319 double Qtot = 0.0;
320 for (std::size_t a = 0; a < C; ++a)
321 Qtot += num_traits<T>::to_double(res.QN(i, closedCl[a]));
322 const std::size_t b = detail::ncldmx_lld_level(mup, i);
323 const std::size_t bcap = b < ec.EC.cols() ? b : ec.EC.cols();
324 const double ECinf = num_traits<T>::to_double(ec.EC(i, bcap - 1));
325 double acc = ECinf * (Qtot + 1.0);
326 if (b >= 2) {
327 const Matrix<T> Dminus = detail::ncldmx_drop_row(Dc, i);
328 const Matrix<T> muminus = detail::ncldmx_drop_row(muEff, i);
329 Matrix<T> Drow(1, C, zero), murow(1, muEff.cols(), zero);
330 for (std::size_t a = 0; a < C; ++a) Drow(0, a) = Dc(i, a);
331 for (std::size_t k = 0; k < muEff.cols(); ++k) murow(0, k) = muEff(i, k);
332 const Matrix<T> Zzero(1, C, zero);
333 for (std::size_t n = 0; n + 2 <= b; ++n) {
334 const double delta = num_traits<T>::to_double(ec.EC(i, n)) - ECinf;
335 if (delta == 0.0) continue;
336 double Pn = 0.0;
337 if (Kc == 0) {
338 // WITH NO CLOSED POPULATION THE MARGINAL IS DEGENERATE, not
339 // absent: P_i(0) = 1 and P_i(n) = 0 above it, so only the n = 0
340 // term survives and acc collapses to EC_i(1), the exact open
341 // load-dependent mean. Skipping the loop instead left acc at
342 // EC_i^inf, i.e. read a c-server station as if every arrival found
343 // it saturated -- an M/M/3 at lambda = 1.5 came back with mean 1
344 // against the exact 1.7368.
345 Pn = n == 0 ? 1.0 : 0.0;
346 } else {
347 std::vector<std::vector<int> > ks;
348 std::vector<int> cur(C, 0);
349 if (C > 0)
350 detail::ncldmx_compositions(static_cast<int>(n), Nc, 0, cur, ks);
351 else if (n == 0)
352 ks.push_back(std::vector<int>());
353 for (std::size_t t = 0; t < ks.size(); ++t) {
354 std::vector<int> rest(C, 0);
355 for (std::size_t a = 0; a < C; ++a) rest[a] = Nc[a] - ks[t][a];
356 const double lF = n == 0 ? 0.0
357 : detail::ncldmx_lg(Drow, ks[t], Zzero, murow,
358 method, atol, nopt);
359 const double lGbar =
360 detail::ncldmx_lg(Dminus, rest, Zc, muminus, method, atol, nopt);
361 Pn += std::exp(lF + lGbar - res.lG);
362 }
363 }
364 acc += static_cast<double>(n + 1) * delta * Pn;
365 }
366 }
367 for (std::size_t r : openCl)
368 res.QN(i, r) = num_traits<T>::from_double(
369 num_traits<T>::to_double(lambda[r]) * num_traits<T>::to_double(D(i, r)) * acc);
370 }
371 }
372
373 return res;
374}
375
376/** Overload with the exact (zero-tolerance) filters and default sampling options. */
377template <class T>
378NcldmxResult<T> pfqn_ncldmx(const std::vector<T>& lambda, const Matrix<T>& D,
379 const std::vector<int>& N, const Matrix<T>& Z, const Matrix<T>& mu) {
380 return pfqn_ncldmx(lambda, D, N, Z, mu, NcldMethod::Default, num_traits<T>::from_int(0),
381 NcOptions());
382}
383
384} // namespace pfqn
385} // namespace line
386
387#endif // LINE_API_PFQN_NCLDMX_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
bool empty() const
Definition matrix.h:92
NumericError(const std::string &what)
Definition error.h:45
The exception types the port throws.
Dense matrix and non-owning view.
NcldMethod
The load-dependent methods this port dispatches.
Definition pfqn_ncld.h:87
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...
NcldResult< T > pfqn_ncld(const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &Z, const Matrix< T > &mu, NcldMethod method, const T &atol, const NcOptions &nopt)
Normalizing constant of a LOAD-DEPENDENT closed network: the dispatcher.
Definition pfqn_ncld.h:153
Matrix< T > pfqn_mushift(const Matrix< T > &mu, const std::vector< std::size_t > &iset)
Shift the load-dependent service-rate lattice of selected stations.
FncResult< T > pfqn_fnc(const Matrix< T > &alpha)
Automatic offset search (the one-argument MATLAB branch).
Definition pfqn_fnc.h:166
NcldmxResult< T > pfqn_ncldmx(const std::vector< T > &lambda, const Matrix< T > &D, const std::vector< int > &N, const Matrix< T > &Z, const Matrix< T > &mu, NcldMethod method, const T &atol, const NcOptions &nopt)
Normalizing constant of a MIXED open/closed network with limited load dependence.
Number-type abstraction for the templated API port.
Load-dependent rates of the functional server f(n) = n + c.
Bruell-Balbo-Afshari effective-capacity terms for a MIXED open/closed network with limited load depen...
Shift the load-dependent service-rate lattice of selected stations.
Normalizing constant of a LOAD-DEPENDENT closed network: the dispatcher.
Return value of pfqn_fnc, mirroring [mu, c].
Definition pfqn_fnc.h:62
std::vector< T > c
Definition pfqn_fnc.h:64
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
The options fields compute_norm_const reads beyond the method itself.
Definition pfqn_nc.h:215
T Gopen
the open-class prefactor prod_i E_i(0)
Definition pfqn_ncldmx.h:87
T G
closed-conditional normalizing constant
Definition pfqn_ncldmx.h:85
double lGopen
its logarithm
Definition pfqn_ncldmx.h:88
Matrix< T > EC
the effective-capacity terms, for the caller's mean measures
Definition pfqn_ncldmx.h:89
std::vector< T > XN
(R) throughputs: G(N-e_r)/G(N) closed, lambda_r open
Definition pfqn_ncldmx.h:90
Matrix< T > QN
(M x R) mean queue lengths
Definition pfqn_ncldmx.h:91
double lG
its logarithm
Definition pfqn_ncldmx.h:86