LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_qlen_joint_moments.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_QLEN_JOINT_MOMENTS_H
6#define LINE_API_PFQN_QLEN_JOINT_MOMENTS_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Joint moments of the queue-length vector of a closed product-form network,
12 * obtained from normalizing constants.
13 *
14 * Templated port of matlab/src/api/pfqn/pfqn_qlen_joint_moments.m,
15 * cross-checked against jar/src/main/java/jline/api/pfqn/nc/
16 * Pfqn_qlen_joint_moments.java.
17 *
18 * The coordinates are (station, class) pairs. Two pairs sharing a class give the
19 * cross-station covariance of that class; two pairs sharing a station give the
20 * cross-class covariance at that station, which is what a class-oriented method
21 * of moments (pfqn_comomrm and its relatives) is positioned to deliver. Two
22 * exact routes reach the joint survival array, and both end in the same
23 * conversion, the tail edge of the house of moments (api/moment) followed by the
24 * joint central-moment and cumulant conversions:
25 *
26 * - SINGLE CLASS (R = 1), route 'tail'. The survival probabilities are ratios
27 * of normalizing constants of the network itself,
28 *
29 * P(n_i >= k_i for all i) = (prod_i L_i^k_i) G(N - sum_i k_i) / G(N)
30 *
31 * which holds because a load-independent single-class station has the
32 * geometric occupancy L_i^n. Only N+1 constants of the ORIGINAL model are
33 * needed, which is why any normalizing-constant algorithm serves it.
34 *
35 * - MULTICLASS, route 'pmf'. The geometric factorization fails, since a
36 * multiclass load-independent station carries the multinomial occupancy
37 * f_i(n_i) = |n_i|! prod_r L_ir^n_ir / n_ir!. What holds instead is the joint
38 * law of the selected stations in terms of the COMPLEMENTARY network, the
39 * model with those stations deleted and the think times kept,
40 *
41 * P(n_i = m_i, i in S) = prod_i f_i(m_i) G_(S^c)(N - sum_i m_i) / G(N).
42 *
43 * The survival array is the reverse cumulative sum of that array, exactly,
44 * since the box covers the support.
45 *
46 * Neither the factorial nor the raw moments have a one-constant closed form; the
47 * survival array is the queue-length functional that does. The
48 * normalizing-constant algorithm is INJECTED rather than called at a fixed site:
49 * the whole set of populations is known before any evaluation, so it is emitted
50 * in one batch and an algorithm that produces several constants in one pass
51 * serves it without recomputation.
52 *
53 * THE DEFAULT METHOD IS EXACT, NOT ADAPTIVE. The point of this routine is an
54 * exact moment array; an approximate normalizing constant would silently make
55 * every moment approximate. The reference builds its options from
56 * SolverNC.defaultOptions and overrides only the method, which this port
57 * reproduces by defaulting NcMethod to Exact.
58 *
59 * Reference: M. Reiser and S. S. Lavenberg, "Mean-value analysis of closed
60 * multichain queuing networks", JACM 27(2):313-322, 1980.
61 *
62 * Arithmetic: TRANSCENDENTAL. The survival array is assembled in the log domain
63 * so that no ratio of normalizing constants overflows.
64 */
65
66#include <algorithm>
67#include <cmath>
68#include <cstddef>
69#include <functional>
70#include <limits>
71#include <string>
72#include <vector>
73
78#include "line/num/number.h"
79#include "line/util/error.h"
80#include "line/util/matrix.h"
81
82namespace line {
83namespace pfqn {
84
85/** Which survival-array identity is used. */
86enum class QlenJointRoute {
87 Auto, ///< tail for a single class, pmf otherwise
88 Tail, ///< the geometric single-class identity
89 Pmf ///< the complementary-network joint law
90};
91
92/** Everything pfqn_qlen_joint_moments reports. */
93template <class T>
101 std::vector<T> mean; ///< (P) mean of each coordinate
102 Matrix<T> cov; ///< (P x P) covariance of the coordinates
103 // info
104 std::string route;
105 std::size_t points = 0; ///< distinct populations requested
106 std::size_t served = 0; ///< how many the injected source supplied
107 std::size_t evals = 0; ///< how many needed a pfqn_nc call
108 bool exact = true;
109 std::vector<std::pair<std::size_t, std::size_t>> pairs; ///< 0-based (station, class)
110 std::vector<std::size_t> dims;
111};
112
113/**
114 * Injected source of log G. Invoked ONCE per network as lGsrc(Lsub, pops), pops
115 * being a list of populations; it must return one value per population and may
116 * return NaN where it cannot serve, which is then filled in by pfqn_nc. The
117 * 'pmf' route queries the COMPLEMENTARY network, so a source must answer for
118 * whichever demand matrix it is handed.
119 */
120template <class T>
122 std::function<std::vector<double>(const Matrix<T>&, const std::vector<std::vector<int>>&)>;
123
124namespace detail {
125
126/** Advance a 0-based multi-index, first dimension fastest. */
127inline bool qlen_odometer(std::vector<std::size_t>& a, const std::vector<std::size_t>& dims) {
128 for (std::size_t l = 0; l < dims.size(); ++l) {
129 if (++a[l] < dims[l]) return true;
130 a[l] = 0;
131 }
132 return false;
133}
134
135/**
136 * MomentTensor drops trailing singleton dimensions, so a d-length multi-index
137 * has to be truncated to the tensor order; the dropped entries are always zero
138 * because their extent is one.
139 */
140template <class T>
141std::vector<std::size_t> qlen_trunc(const moment::MomentTensor<T>& A,
142 const std::vector<std::size_t>& ord) {
143 std::vector<std::size_t> out(ord.begin(), ord.begin() + std::min(ord.size(), A.order()));
144 out.resize(A.order(), 0);
145 return out;
146}
147
148/** Index of a population vector in the deduplicated request list. */
149inline std::size_t qlen_findrow(const std::vector<std::vector<int>>& rows,
150 const std::vector<int>& key) {
151 for (std::size_t p = 0; p < rows.size(); ++p)
152 if (rows[p] == key) return p;
153 throw NumericError("pfqn_qlen_joint_moments: a requested population was not evaluated");
154}
155
156/** Log normalizing constant of a pure-delay network, prod_r Z_r^n_r / n_r!. */
157template <class T>
158double qlen_delay_lg(const std::vector<T>& Z, const std::vector<int>& n) {
159 using std::log;
160 double lg = 0.0;
161 for (std::size_t r = 0; r < n.size(); ++r) {
162 if (n[r] == 0) continue;
163 if (Z[r] <= num_traits<T>::from_int(0)) return -std::numeric_limits<double>::infinity();
164 lg += static_cast<double>(n[r]) * num_traits<T>::log_as_double(Z[r]) -
166 num_lgamma<T>(num_traits<T>::from_int(static_cast<long>(n[r]) + 1)));
167 }
168 return lg;
169}
170
171/** Evaluate log G at a batch of populations, honouring the injected source. */
172template <class T>
173std::vector<double> qlen_batch_lg(const Matrix<T>& Lsub, const std::vector<std::vector<int>>& pops,
174 const Matrix<T>& Z, const QlenJointLgSource<T>& lGsrc,
175 NcMethod method, const NcOptions& nopt, std::size_t& served,
176 std::size_t& evals) {
177 const std::size_t P = pops.size();
178 std::vector<double> lg(P, std::numeric_limits<double>::quiet_NaN());
179 served = 0;
180 if (lGsrc) {
181 lg = lGsrc(Lsub, pops);
182 if (lg.size() != P)
183 throw InputError(
184 "pfqn_qlen_joint_moments: the lGsrc source must return one value per requested "
185 "population");
186 for (std::size_t p = 0; p < P; ++p)
187 if (std::isfinite(lg[p])) ++served;
188 }
189 evals = 0;
190 for (std::size_t p = 0; p < P; ++p) {
191 if (std::isfinite(lg[p])) continue;
192 const std::vector<T> lambda(pops[p].size(), num_traits<T>::from_int(0));
193 lg[p] = pfqn_nc(lambda, Lsub, pops[p], Z, method, num_traits<T>::from_int(0), nopt).lG;
194 ++evals;
195 }
196 return lg;
197}
198
199} // namespace detail
200
201/**
202 * @brief Joint moments of the queue-length vector of a closed product-form
203 * network, obtained from normalizing constants.
204 *
205 * @param L (M x R) demands of the QUEUEING stations; delay stations belong
206 * in Z, their marginals following a different law
207 * @param N (R) population vector
208 * @param Z (R) think times, zeros for none
209 * @param pairs 0-based (station, class) coordinates, one per dimension of the
210 * returned arrays; empty for every class of every station
211 * @param route which survival identity to use
212 * @param lGsrc injected source of log G; empty to call pfqn_nc throughout
213 * @param method the normalizing-constant algorithm handed to pfqn_nc
214 * @param nopt sample count and seed the estimators read
215 */
216template <class T>
218 const Matrix<T>& L, const std::vector<int>& N, const std::vector<T>& Z,
219 const std::vector<std::pair<std::size_t, std::size_t>>& pairs, QlenJointRoute route,
220 const QlenJointLgSource<T>& lGsrc, NcMethod method, const NcOptions& nopt) {
222 "pfqn_qlen_joint_moments assembles the survival array in the log domain and "
223 "needs transcendental arithmetic");
224 using std::exp;
225 using std::log;
226 const std::size_t M = L.rows();
227 const std::size_t R = L.cols();
228 const T zero = num_traits<T>::from_int(0);
229 if (N.size() != R)
230 throw InputError(
231 "pfqn_qlen_joint_moments: the population vector N must have one entry per class");
232 std::vector<T> Zv = Z;
233 if (Zv.empty()) Zv.assign(R, zero);
234 if (Zv.size() != R)
235 throw InputError(
236 "pfqn_qlen_joint_moments: the think time vector Z must have one entry per class");
237 Matrix<T> Zm(1, R, zero);
238 for (std::size_t r = 0; r < R; ++r) Zm(0, r) = Zv[r];
239
240 std::vector<std::pair<std::size_t, std::size_t>> pr = pairs;
241 if (pr.empty()) {
242 for (std::size_t i = 0; i < M; ++i)
243 for (std::size_t r = 0; r < R; ++r) pr.push_back(std::make_pair(i, r));
244 }
245 for (std::size_t j = 0; j < pr.size(); ++j)
246 if (pr[j].first >= M || pr[j].second >= R)
247 throw InputError("pfqn_qlen_joint_moments: a (station,class) pair is out of range");
248 {
249 std::vector<std::pair<std::size_t, std::size_t>> s = pr;
250 std::sort(s.begin(), s.end());
251 if (std::unique(s.begin(), s.end()) != s.end())
252 throw InputError("pfqn_qlen_joint_moments: the (station,class) pairs must be distinct");
253 }
254
255 if (route == QlenJointRoute::Auto) route = (R == 1) ? QlenJointRoute::Tail : QlenJointRoute::Pmf;
256 if (route == QlenJointRoute::Tail && R > 1)
257 throw InputError(
258 "pfqn_qlen_joint_moments: the tail route needs the geometric occupancy of a "
259 "single-class load-independent station; with several classes the multinomial factor "
260 "breaks the survival identity, so use the pmf route");
261
262 const std::size_t d = pr.size();
263 std::vector<std::size_t> dims(d);
264 for (std::size_t j = 0; j < d; ++j)
265 dims[j] = static_cast<std::size_t>(N[pr[j].second]) + 1;
266
268 res.pairs = pr;
269 res.dims = dims;
270 res.route = (route == QlenJointRoute::Tail) ? "tail" : "pmf";
271
272 moment::MomentTensor<T> tail(dims);
273 std::size_t served = 0, evals = 0, points = 0;
274
275 if (route == QlenJointRoute::Tail) {
276 // the whole population set is known up front: N minus the total order
277 std::vector<std::vector<int>> need;
278 std::vector<std::size_t> a(d, 0);
279 do {
280 long s = 0;
281 for (std::size_t j = 0; j < d; ++j) s += static_cast<long>(a[j]);
282 if (static_cast<long>(N[0]) - s >= 0) {
283 std::vector<int> row(1, static_cast<int>(static_cast<long>(N[0]) - s));
284 if (std::find(need.begin(), need.end(), row) == need.end()) need.push_back(row);
285 }
286 } while (detail::qlen_odometer(a, dims));
287 if (std::find(need.begin(), need.end(), N) == need.end()) need.push_back(N);
288 std::sort(need.begin(), need.end());
289
290 std::size_t sv = 0, ev = 0;
291 const std::vector<double> lg =
292 detail::qlen_batch_lg(L, need, Zm, lGsrc, method, nopt, sv, ev);
293 served = sv;
294 evals = ev;
295 points = need.size();
296 const double lgN = lg[detail::qlen_findrow(need, N)];
297
298 std::fill(a.begin(), a.end(), static_cast<std::size_t>(0));
299 std::size_t ia = 0;
300 do {
301 long s = 0;
302 for (std::size_t j = 0; j < d; ++j) s += static_cast<long>(a[j]);
303 if (static_cast<long>(N[0]) - s >= 0) {
304 double acc = 0.0;
305 bool ok = true;
306 for (std::size_t j = 0; j < d; ++j) {
307 if (a[j] == 0) continue;
308 const T Lij = L(pr[j].first, pr[j].second);
309 if (Lij <= zero) {
310 ok = false;
311 break;
312 }
313 acc += static_cast<double>(a[j]) * num_traits<T>::log_as_double(Lij);
314 }
315 if (ok) {
316 std::vector<int> key(1, static_cast<int>(static_cast<long>(N[0]) - s));
317 tail.data[ia] = num_traits<T>::from_double(
318 std::exp(acc + lg[detail::qlen_findrow(need, key)] - lgN));
319 }
320 }
321 ++ia;
322 } while (detail::qlen_odometer(a, dims));
323 } else {
324 // the joint law of the selected stations needs every class of those
325 // stations, so the internal box runs over (station,class) and the
326 // requested pairs are marginalized out of it afterwards
327 std::vector<std::size_t> stations;
328 for (std::size_t j = 0; j < d; ++j) stations.push_back(pr[j].first);
329 std::sort(stations.begin(), stations.end());
330 stations.erase(std::unique(stations.begin(), stations.end()), stations.end());
331 std::vector<std::pair<std::size_t, std::size_t>> coords;
332 for (std::size_t si = 0; si < stations.size(); ++si)
333 for (std::size_t r = 0; r < R; ++r) coords.push_back(std::make_pair(stations[si], r));
334 const std::size_t dc = coords.size();
335 std::vector<std::size_t> cdims(dc);
336 for (std::size_t j = 0; j < dc; ++j)
337 cdims[j] = static_cast<std::size_t>(N[coords[j].second]) + 1;
338
339 std::vector<std::size_t> keep;
340 for (std::size_t i = 0; i < M; ++i)
341 if (std::find(stations.begin(), stations.end(), i) == stations.end()) keep.push_back(i);
342 Matrix<T> Lsub(keep.size(), R, zero);
343 for (std::size_t k = 0; k < keep.size(); ++k)
344 for (std::size_t r = 0; r < R; ++r) Lsub(k, r) = L(keep[k], r);
345
346 std::vector<std::vector<int>> need;
347 std::vector<std::size_t> a(dc, 0);
348 do {
349 std::vector<int> n = N;
350 for (std::size_t j = 0; j < dc; ++j) n[coords[j].second] -= static_cast<int>(a[j]);
351 bool ok = true;
352 for (std::size_t r = 0; r < R; ++r)
353 if (n[r] < 0) ok = false;
354 if (ok && std::find(need.begin(), need.end(), n) == need.end()) need.push_back(n);
355 } while (detail::qlen_odometer(a, cdims));
356 std::sort(need.begin(), need.end());
357
358 std::vector<double> lgc(need.size(), 0.0);
359 if (keep.empty()) {
360 for (std::size_t p = 0; p < need.size(); ++p) lgc[p] = detail::qlen_delay_lg(Zv, need[p]);
361 served = need.size();
362 evals = 0;
363 } else {
364 std::size_t sv = 0, ev = 0;
365 lgc = detail::qlen_batch_lg(Lsub, need, Zm, lGsrc, method, nopt, sv, ev);
366 served = sv;
367 evals = ev;
368 }
369 points = need.size();
370
371 std::size_t sv0 = 0, ev0 = 0;
372 const QlenJointLgSource<T> none;
373 const std::vector<std::vector<int>> onlyN(1, N);
374 const std::vector<double> lgNv =
375 detail::qlen_batch_lg(L, onlyN, Zm, none, method, nopt, sv0, ev0);
376 const double lgN = lgNv[0];
377 evals += ev0;
378
379 moment::MomentTensor<T> marg(dims);
380 std::fill(a.begin(), a.end(), static_cast<std::size_t>(0));
381 do {
382 std::vector<int> n = N;
383 for (std::size_t j = 0; j < dc; ++j) n[coords[j].second] -= static_cast<int>(a[j]);
384 bool inbox = true;
385 for (std::size_t r = 0; r < R; ++r)
386 if (n[r] < 0) inbox = false;
387 if (!inbox) continue;
388 const double gc = lgc[detail::qlen_findrow(need, n)];
389 if (!std::isfinite(gc)) continue;
390 double acc = gc - lgN;
391 bool ok = true;
392 for (std::size_t si = 0; si < stations.size() && ok; ++si) {
393 const std::size_t i = stations[si];
394 long tot = 0;
395 for (std::size_t j = 0; j < dc; ++j)
396 if (coords[j].first == i) tot += static_cast<long>(a[j]);
397 acc += num_traits<T>::to_double(
398 detail::num_lgamma<T>(num_traits<T>::from_int(tot + 1)));
399 for (std::size_t j = 0; j < dc; ++j) {
400 if (coords[j].first != i || a[j] == 0) continue;
401 const T Lir = L(i, coords[j].second);
402 if (Lir <= zero) {
403 ok = false;
404 break;
405 }
406 acc += static_cast<double>(a[j]) * num_traits<T>::log_as_double(Lir) -
407 num_traits<T>::to_double(detail::num_lgamma<T>(
408 num_traits<T>::from_int(static_cast<long>(a[j]) + 1)));
409 }
410 }
411 if (!ok) continue;
412 std::vector<std::size_t> sub(d, 0);
413 for (std::size_t j = 0; j < d; ++j)
414 for (std::size_t jc = 0; jc < dc; ++jc)
415 if (coords[jc] == pr[j]) sub[j] = a[jc];
416 marg.at(detail::qlen_trunc(marg, sub)) += num_traits<T>::from_double(std::exp(acc));
417 } while (detail::qlen_odometer(a, cdims));
418
419 // the survival array is the reverse cumulative sum along every mode
420 tail = marg;
421 for (std::size_t mode = 0; mode < tail.order(); ++mode) {
422 const std::size_t n = tail.sz[mode];
423 std::size_t stride = 1;
424 for (std::size_t l = 0; l < mode; ++l) stride *= tail.sz[l];
425 const std::size_t outer = tail.numel() / (n * stride);
426 for (std::size_t o = 0; o < outer; ++o)
427 for (std::size_t s = 0; s < stride; ++s) {
428 const std::size_t base = o * n * stride + s;
429 for (std::size_t k = n - 1; k-- > 0;)
430 tail.data[base + k * stride] += tail.data[base + (k + 1) * stride];
431 }
432 }
433 }
434
435 res.tail = tail;
436 res.binomial = moment::moment_joint_binomial_from_tail(tail);
437 res.factorial = moment::moment_joint_factorial_from_binomial(res.binomial);
438 res.raw = moment::moment_joint_raw_from_factorial(res.factorial);
439 res.central = moment::moment_joint_central_from_raw(res.raw);
440 res.cumulant = moment::moment_joint_cumulant_from_raw(res.raw);
441
442 res.mean.assign(d, zero);
443 res.cov = Matrix<T>(d, d, zero);
444 for (std::size_t j = 0; j < d; ++j) {
445 std::vector<std::size_t> e(d, 0);
446 e[j] = 1;
447 res.mean[j] = res.raw.at(detail::qlen_trunc(res.raw, e));
448 for (std::size_t l = 0; l < d; ++l) {
449 std::vector<std::size_t> aa(d, 0);
450 ++aa[j];
451 ++aa[l];
452 res.cov(j, l) = res.cumulant.at(detail::qlen_trunc(res.cumulant, aa));
453 }
454 }
455 res.points = points;
456 res.served = served;
457 res.evals = evals;
458 return res;
459}
460
461/** MATLAB defaults: every coordinate, the automatic route, no injected source. */
462template <class T>
464 const std::vector<T>& Z) {
465 return pfqn_qlen_joint_moments(L, N, Z, std::vector<std::pair<std::size_t, std::size_t>>(),
467 NcOptions());
468}
469
470/** MATLAB defaults with an explicit coordinate list. */
471template <class T>
473 const Matrix<T>& L, const std::vector<int>& N, const std::vector<T>& Z,
474 const std::vector<std::pair<std::size_t, std::size_t>>& pairs) {
477}
478
479} // namespace pfqn
480} // namespace line
481
482#endif // LINE_API_PFQN_QLEN_JOINT_MOMENTS_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.
Joint moment conversions on the house of moments.
Joint moment arrays and the mode products used by every joint conversion.
MomentTensor< T > moment_joint_central_from_raw(const MomentTensor< T > &m)
Joint central moments from joint raw moments, reading the means off m.
MomentTensor< T > moment_joint_factorial_from_binomial(const MomentTensor< T > &b)
Joint factorial moments from joint binomial moments.
MomentTensor< T > moment_joint_raw_from_factorial(const MomentTensor< T > &f)
Joint raw moments from joint factorial moments.
MomentTensor< T > moment_joint_cumulant_from_raw(const MomentTensor< T > &m)
Joint cumulants from joint raw moments.
MomentTensor< T > moment_joint_binomial_from_tail(const MomentTensor< T > &t)
Joint binomial moments from joint tail moments.
QlenJointMomentsResult< T > pfqn_qlen_joint_moments(const Matrix< T > &L, const std::vector< int > &N, const std::vector< T > &Z, const std::vector< std::pair< std::size_t, std::size_t > > &pairs, QlenJointRoute route, const QlenJointLgSource< T > &lGsrc, NcMethod method, const NcOptions &nopt)
Joint moments of the queue-length vector of a closed product-form network, obtained from normalizing ...
std::function< std::vector< double >(const Matrix< T > &, const std::vector< std::vector< int > > &)> QlenJointLgSource
Injected source of log G.
QlenJointRoute
Which survival-array identity is used.
@ Auto
tail for a single class, pmf otherwise
@ Pmf
the complementary-network joint law
@ Tail
the geometric single-class identity
NcMethod
The methods this port dispatches, one per compute_norm_const case.
Definition pfqn_nc.h:101
NcDispatchResult< T > pfqn_nc(const std::vector< T > &lambda, const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &Z, NcMethod method, const T &atol, const NcOptions &nopt)
Normalizing constant of a product-form queueing network: the dispatcher.
Definition pfqn_nc.h:276
Number-type abstraction for the templated API port.
Shared scalar machinery for the integration / asymptotic members of the pfqn family (pfqn_le,...
Normalizing constant of a product-form queueing network: the dispatcher.
Joint moment array of size (n_1+1)x...x(n_d+1), stored column major.
std::size_t order() const
The options fields compute_norm_const reads beyond the method itself.
Definition pfqn_nc.h:215
Everything pfqn_qlen_joint_moments reports.
std::size_t evals
how many needed a pfqn_nc call
Matrix< T > cov
(P x P) covariance of the coordinates
std::vector< std::pair< std::size_t, std::size_t > > pairs
0-based (station, class)
std::size_t points
distinct populations requested
std::size_t served
how many the injected source supplied
std::vector< T > mean
(P) mean of each coordinate