LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_joint.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_JOINT_H
6#define LINE_API_PFQN_JOINT_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Joint queue-length probability of a closed product-form network.
12 *
13 * Templated port of matlab/src/api/pfqn/pfqn_joint.m, whose single entry point
14 * dispatches on the SHAPE of its first argument; the two behaviours are split
15 * into two named functions here, since C++ can tell an (M) vector from an
16 * (M x R) matrix at the call site and a shape-dispatching overload would only
17 * hide the distinction.
18 *
19 * PER-CLASS form, pfqn_joint. For a per-station, per-class occupancy n(i,r),
20 * the unnormalized weight is the product of the per-station multinomial terms
21 * and the delay term,
22 *
23 * F(n) = prod_r Z_r^{n0_r} / n0_r! * prod_i [ (sum_r n_ir)! / prod_r n_ir!
24 * * prod_r L(i,r)^{n_ir} ],
25 *
26 * with n0 = N - sum_i n(i,:) the jobs left at the delay, and the probability is
27 * F(n)/G(N).
28 *
29 * TOTAL form, pfqn_joint_total. When only the per-station TOTALS m(i) are
30 * given, the per-class split is unknown and the weight is a PERMANENT: it sums
31 * the product-form weight over every assignment of the N_r class-r jobs to the
32 * m(i) slots of station i. The reference builds that permanent by expanding
33 * every station row m(i) times and every class column N_r times and calling
34 * perm(); this port calls pfqn_perm directly on the (sum m) x R matrix with
35 * column multiplicities N, which is the same quantity without materializing
36 * the expansion. The delay contributes its own 1/n0! as in the reference.
37 *
38 * Arithmetic: EXACT-CAPABLE, both forms. The reference works in log space
39 * throughout (log/gammaln/exp) purely to keep the factorials in range; every
40 * quantity involved is a ratio of products of the inputs and of factorials, so
41 * it is formed directly here and the probability comes out as an exact
42 * rational. Note the cost of the total form: pfqn_perm is prod_r (N_r + 1)
43 * evaluations of a length-(sum m) product, so it is a small-model routine.
44 */
45
46#include <cstddef>
47#include <vector>
48
52#include "line/num/number.h"
53#include "line/util/error.h"
54#include "line/util/matrix.h"
55
56namespace line {
57namespace pfqn {
58
59/**
60 * Joint probability of a PER-CLASS occupancy matrix.
61 *
62 * @param n (M x R) per-station, per-class occupancy
63 * @param L (M x R) service demands
64 * @param N (R) populations
65 * @param Z (K x R) think times, summed over rows; may be empty
66 * @param G the normalizing constant G(N)
67 */
68template <class T>
69T pfqn_joint(const Matrix<int>& n, const Matrix<T>& L, const std::vector<int>& N,
70 const Matrix<T>& Z, const T& G) {
71 const std::size_t M = L.rows(), R = L.cols();
72 if (N.size() != R) throw InputError("pfqn_joint: L and N disagree on the class count");
73 if (n.rows() != M || n.cols() != R)
74 throw InputError("pfqn_joint: the occupancy matrix has the wrong shape");
75 const T zero = num_traits<T>::from_int(0);
76 const T one = num_traits<T>::from_int(1);
77 if (G == zero) throw NumericError("pfqn_joint: the normalizing constant is zero");
78
79 std::vector<T> Zsum(R, zero);
80 T Ztot = zero;
81 if (!Z.empty()) {
82 if (Z.cols() != R) throw InputError("pfqn_joint: Z and N disagree on the class count");
83 for (std::size_t k = 0; k < Z.rows(); ++k)
84 for (std::size_t r = 0; r < R; ++r) Zsum[r] += Z(k, r);
85 }
86 for (std::size_t r = 0; r < R; ++r) Ztot += Zsum[r];
87
88 // Jobs left at the delay.
89 std::vector<int> n0(R, 0);
90 for (std::size_t r = 0; r < R; ++r) {
91 int used = 0;
92 for (std::size_t i = 0; i < M; ++i) used += n(i, r);
93 n0[r] = N[r] - used;
94 if (n0[r] < 0) throw InputError("pfqn_joint: the occupancy exceeds the population");
95 }
96
97 T F = one;
98 if (Ztot > zero) {
99 for (std::size_t r = 0; r < R; ++r) {
100 if (n0[r] == 0) continue;
101 F *= num_pow_int(Zsum[r], static_cast<unsigned>(n0[r])) /
102 num_factorial<T>(static_cast<unsigned>(n0[r]));
103 }
104 } else {
105 for (std::size_t r = 0; r < R; ++r)
106 if (n0[r] != 0) return zero; // no delay to hold them
107 }
108 for (std::size_t i = 0; i < M; ++i) {
109 int tot = 0;
110 for (std::size_t r = 0; r < R; ++r) tot += n(i, r);
111 T term = num_factorial<T>(static_cast<unsigned>(tot));
112 for (std::size_t r = 0; r < R; ++r) {
113 if (n(i, r) == 0) continue;
114 term *= num_pow_int(L(i, r), static_cast<unsigned>(n(i, r))) /
115 num_factorial<T>(static_cast<unsigned>(n(i, r)));
116 }
117 F *= term;
118 }
119 return F / G;
120}
121
122/**
123 * Joint probability of the per-station TOTAL queue lengths.
124 *
125 * @param m (M) per-station total occupancy; the delay takes the remainder
126 * @param L (M x R) service demands
127 * @param N (R) populations
128 * @param Z (K x R) think times, summed over rows; may be empty
129 * @param G the normalizing constant G(N)
130 */
131template <class T>
132T pfqn_joint_total(const std::vector<int>& m, const Matrix<T>& L, const std::vector<int>& N,
133 const Matrix<T>& Z, const T& G) {
134 const std::size_t M = L.rows(), R = L.cols();
135 if (N.size() != R) throw InputError("pfqn_joint_total: L and N disagree on the class count");
136 if (m.size() != M) throw InputError("pfqn_joint_total: the occupancy vector has the wrong length");
137 const T zero = num_traits<T>::from_int(0);
138 if (G == zero) throw NumericError("pfqn_joint_total: the normalizing constant is zero");
139
140 std::vector<T> Zsum(R, zero);
141 T Ztot = zero;
142 if (!Z.empty()) {
143 if (Z.cols() != R) throw InputError("pfqn_joint_total: Z and N disagree on the class count");
144 for (std::size_t k = 0; k < Z.rows(); ++k)
145 for (std::size_t r = 0; r < R; ++r) Zsum[r] += Z(k, r);
146 }
147 for (std::size_t r = 0; r < R; ++r) Ztot += Zsum[r];
148
149 long Ntot = 0, mtot = 0;
150 for (int v : N) Ntot += v;
151 for (int v : m) {
152 if (v < 0) throw InputError("pfqn_joint_total: negative occupancy");
153 mtot += v;
154 }
155 const long n0 = Ntot - mtot;
156 if (n0 < 0) throw InputError("pfqn_joint_total: the occupancy exceeds the population");
157 if (n0 > 0 && !(Ztot > zero)) return zero; // no delay to hold the remainder
158
159 // The aggregated think time is ONE extra infinite-server row holding the
160 // remainder, which is exactly the shape pfqn_jointmarg takes. Delegating
161 // keeps the identity in one place; a model with SEVERAL delays needs the
162 // general form, because each of them carries its own 1/n_j!.
163 Matrix<T> Lall(M + 1, R);
164 for (std::size_t i = 0; i < M; ++i)
165 for (std::size_t r = 0; r < R; ++r) Lall(i, r) = L(i, r);
166 for (std::size_t r = 0; r < R; ++r) Lall(M, r) = Zsum[r];
167 std::vector<int> nall(m);
168 nall.push_back(static_cast<int>(n0));
169 std::vector<std::size_t> infset(1, M);
170 return pfqn_jointmarg(nall, Lall, N, infset, G);
171}
172
173/** Overload computing G with pfqn_ca first, matching the reference's default. */
174template <class T>
175T pfqn_joint(const Matrix<int>& n, const Matrix<T>& L, const std::vector<int>& N,
176 const Matrix<T>& Z) {
177 return pfqn_joint(n, L, N, Z, pfqn_ca(L, N, Z).G);
178}
179
180template <class T>
181T pfqn_joint_total(const std::vector<int>& m, const Matrix<T>& L, const std::vector<int>& N,
182 const Matrix<T>& Z) {
183 return pfqn_joint_total(m, L, N, Z, pfqn_ca(L, N, Z).G);
184}
185
186} // namespace pfqn
187} // namespace line
188
189#endif // LINE_API_PFQN_JOINT_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.
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
T pfqn_joint_total(const std::vector< int > &m, const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &Z, const T &G)
Joint probability of the per-station TOTAL queue lengths.
Definition pfqn_joint.h:132
T pfqn_jointmarg(const std::vector< int > &n, const Matrix< T > &L, const std::vector< int > &N, const std::vector< std::size_t > &infset, const T &G, const std::string &engine="exact", std::uint64_t seed=0)
Joint probability of the per-station TOTAL queue lengths.
T pfqn_joint(const Matrix< int > &n, const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &Z, const T &G)
Joint probability of a PER-CLASS occupancy matrix.
Definition pfqn_joint.h:69
T num_factorial(unsigned n)
Factorial as a value of T.
Definition number.h:184
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.
Convolution algorithm for the exact normalizing constant of a closed product-form network (Buzen 1973...
Joint probability of the per-station TOTAL queue lengths.
Permanent of a matrix with repeated columns, by Ryser's formula.