LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_dac.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_DAC_H
6#define LINE_API_PFQN_PFQN_DAC_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Distribution Analysis by Chain (de Souza e Silva, UCLA CSD-870023, 1987):
12 * the JOINT queue-length distribution of a closed product-form network with
13 * single-server, infinite-server and queue-dependent centers.
14 *
15 * Templated port of matlab/src/api/pfqn/pfqn_dac.m together with its local
16 * functions dac_lattice, dac_compositions, dac_rank and dac_step. The
17 * recursion runs over a related network in which every chain holds one
18 * customer, a transformation that leaves the aggregate queue-length
19 * distribution unchanged. Adding one customer with demands r to a network of
20 * k customers gives
21 *
22 * c_j = sum_{n=1..k+1} (n/mu_j(n)) P_j^{k}(n-1)
23 * lambda = 1 / sum_j r_j c_j
24 * P^{k+1}(n) = lambda sum_j r_j (n_j/mu_j(n_j)) P^{k}(n - e_j)
25 *
26 * so probability mass is conserved by construction and the recursion is
27 * numerically stable, unlike a normalizing-constant route. Per-chain
28 * throughputs and queue lengths come from re-running the tail of the recursion
29 * with each chain placed last, sharing the common prefix.
30 *
31 * ARITHMETIC. Every step is an addition, a multiplication or a division of
32 * field elements, so the whole joint distribution is EXACT in rational
33 * arithmetic and the routine is deliberately left ungated. That is the point
34 * of the algorithm here: an exact joint distribution is what availability
35 * modelling needs, and it is the natural oracle for the marginal that MVA
36 * returns.
37 */
38
39#include <cstddef>
40#include <vector>
41
42#include "line/num/number.h"
43#include "line/util/error.h"
44#include "line/util/matrix.h"
45
46namespace line {
47namespace pfqn {
48
49/** Return value of pfqn_dac, mirroring [Pjoint, states, XN, QN, UN, CN, pi]. */
50template <class T>
51struct DacResult {
52 std::vector<T> Pjoint; ///< one probability per row of `states`
53 std::vector<std::vector<int>> states; ///< aggregate states, J columns
54 std::vector<T> XN; ///< (R) chain throughput
55 Matrix<T> QN; ///< (M x R) mean queue length
56 std::vector<T> UN; ///< (M) utilization, 1 - P(empty)
57 std::vector<T> CN; ///< (R) cycle time, excluding think time
58 Matrix<T> PI; ///< (M x (Nt+1)) marginal distribution
59};
60
61namespace detail {
62
63/** All J-part compositions of k, in lexicographic order (dac_compositions). */
64inline void dac_compositions(std::size_t J, int k, std::vector<std::vector<int>>& out) {
65 out.clear();
66 std::vector<int> n(J, 0);
67 if (J == 0) return;
68 n[J - 1] = k;
69 // Lexicographic order on the leading parts: enumerate by decrementing the
70 // rightmost position that can give a unit to a position on its left.
71 while (true) {
72 out.push_back(n);
73 std::size_t i = J - 1;
74 while (i > 0 && n[i] == 0) --i;
75 if (i == 0) break;
76 // Move one unit from position i to position i-1 and flush the rest
77 // rightwards, which walks the compositions in lexicographic order.
78 n[i - 1] += 1;
79 const int rest = n[i] - 1;
80 for (std::size_t d = i; d < J; ++d) n[d] = 0;
81 n[J - 1] = rest;
82 }
83}
84
85/** Lexicographic rank (1-based, as in dac_rank) of a composition. */
86inline std::size_t dac_rank(const std::vector<int>& n, int k, std::size_t J,
87 const std::vector<std::vector<double>>& C) {
88 std::size_t idx = 1;
89 int rem = k;
90 // 1-based vs 0-based binomial index rationale: see _kb/03-api-layer.md (cpp port notes: pfqn)
91 for (std::size_t j = 0; j + 1 < J; ++j) {
92 const std::size_t col = J - j - 2;
93 for (int v = 0; v < n[j]; ++v) {
94 const int s = rem - v;
95 idx += static_cast<std::size_t>(
96 C[static_cast<std::size_t>(s + static_cast<int>(J - j) - 2)][col]);
97 }
98 rem -= n[j];
99 }
100 return idx;
101}
102
103} // namespace detail
104
105/**
106 * @brief Distribution Analysis by Chain (de Souza e Silva, UCLA CSD-870023,
107 * 1987): the JOINT queue-length distribution of a closed product-form
108 * network with single-server, infinite-server and queue-dependent
109 * centers.
110 *
111 * @param L (M x R) demands
112 * @param N (R) population
113 * @param Z (R) think times; a non-zero total appends an IS center, so the
114 * states then have M+1 columns
115 * @param mu (M x Nt) load-dependent rates, empty for all ones
116 */
117template <class T>
118DacResult<T> pfqn_dac(const Matrix<T>& L, const std::vector<int>& N, const std::vector<T>& Z,
119 const Matrix<T>& mu) {
120 const std::size_t M = L.rows(), R = L.cols();
121 if (N.size() != R) throw InputError("pfqn_dac: L and N disagree on the class count");
122 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
123 int Nt = 0;
124 for (int v : N) {
125 if (v < 0) throw InputError("pfqn_dac: the population vector must be non-negative");
126 Nt += v;
127 }
128 const std::size_t mucols = static_cast<std::size_t>(Nt > 0 ? Nt : 1);
129 if (!mu.empty()) {
130 if (mu.rows() != M) throw InputError("pfqn_dac: mu must have one row per station");
131 if (Nt > 0 && mu.cols() < static_cast<std::size_t>(Nt))
132 throw InputError("pfqn_dac: mu must have at least sum(N) columns");
133 }
134
135 std::vector<T> Zv = Z;
136 if (Zv.empty()) Zv.assign(R, zero);
137 T Zsum = zero;
138 for (const T& v : Zv) Zsum += v;
139 const bool hasZ = Zsum > zero;
140 const std::size_t J = M + (hasZ ? 1 : 0);
141
142 Matrix<T> Lx(J, R), mux(J, mucols);
143 for (std::size_t i = 0; i < M; ++i) {
144 for (std::size_t r = 0; r < R; ++r) Lx(i, r) = L(i, r);
145 for (std::size_t k = 0; k < mucols; ++k)
146 mux(i, k) = mu.empty() ? one : mu(i, k);
147 }
148 if (hasZ) {
149 for (std::size_t r = 0; r < R; ++r) Lx(M, r) = Zv[r];
150 for (std::size_t k = 0; k < mucols; ++k)
151 mux(M, k) = num_traits<T>::from_int(static_cast<long>(k) + 1);
152 }
153
154 DacResult<T> res;
155 if (Nt == 0) {
156 res.states.assign(1, std::vector<int>(J, 0));
157 res.Pjoint.assign(1, one);
158 res.XN.assign(R, zero);
159 res.QN = Matrix<T>(M, R, zero);
160 res.UN.assign(M, zero);
161 res.CN.assign(R, zero);
162 res.PI = Matrix<T>(M, 1, one);
163 return res;
164 }
165
166 std::vector<std::size_t> active;
167 for (std::size_t r = 0; r < R; ++r)
168 if (N[r] > 0) active.push_back(r);
169 for (std::size_t idx = 0; idx < active.size(); ++idx) {
170 bool any = false;
171 for (std::size_t j = 0; j < J; ++j)
172 if (Lx(j, active[idx]) > zero) any = true;
173 if (!any) throw InputError("pfqn_dac: a chain has null demand at every center");
174 }
175
176 // Lattice of aggregate states, level by level, with the successor map.
177 std::vector<std::vector<std::vector<int>>> lv(static_cast<std::size_t>(Nt) + 1);
178 for (int k = 0; k <= Nt; ++k) detail::dac_compositions(J, k, lv[static_cast<std::size_t>(k)]);
179 std::vector<std::vector<double>> C(
180 static_cast<std::size_t>(Nt) + J + 1, std::vector<double>(J + 2, 0.0));
181 for (std::size_t a = 0; a + 1 <= static_cast<std::size_t>(Nt) + J; ++a)
182 for (std::size_t b = 0; b <= (a < J ? a : J); ++b)
183 C[a][b] = (b == 0) ? 1.0 : (C[a - 1][b - 1] + C[a - 1][b]);
184 std::vector<std::vector<std::vector<std::size_t>>> succ(static_cast<std::size_t>(Nt) + 1);
185 for (int k = 0; k < Nt; ++k) {
186 const std::vector<std::vector<int>>& Sv = lv[static_cast<std::size_t>(k)];
187 succ[static_cast<std::size_t>(k)].assign(Sv.size(), std::vector<std::size_t>(J, 0));
188 for (std::size_t i = 0; i < Sv.size(); ++i)
189 for (std::size_t j = 0; j < J; ++j) {
190 std::vector<int> t = Sv[i];
191 t[j] += 1;
192 succ[static_cast<std::size_t>(k)][i][j] = detail::dac_rank(t, k + 1, J, C) - 1;
193 }
194 }
195
196 // One recursion step: add one customer with demands r to a k-customer net.
197 const auto dac_step = [&](const std::vector<T>& p, const std::vector<T>& r, int k,
198 std::vector<T>& pn, T& lam, std::vector<T>& Lq) {
199 const std::vector<std::vector<int>>& Sv = lv[static_cast<std::size_t>(k)];
200 Matrix<T> marg(J, static_cast<std::size_t>(k) + 1, zero);
201 for (std::size_t i = 0; i < Sv.size(); ++i)
202 for (std::size_t j = 0; j < J; ++j)
203 marg(j, static_cast<std::size_t>(Sv[i][j])) += p[i];
204 std::vector<T> c(J, zero);
205 for (std::size_t j = 0; j < J; ++j)
206 for (int n = 1; n <= k + 1; ++n) {
207 const T mur = mux(j, static_cast<std::size_t>(n) - 1);
208 if (mur == zero) throw NumericError("pfqn_dac: zero load-dependent rate");
209 c[j] += T(num_traits<T>::from_int(n) / mur) *
210 marg(j, static_cast<std::size_t>(n) - 1);
211 }
212 T den = zero;
213 for (std::size_t j = 0; j < J; ++j) den += r[j] * c[j];
214 if (den == zero) throw NumericError("pfqn_dac: zero cycle time for the added customer");
215 lam = T(one / den);
216 Lq.assign(J, zero);
217 for (std::size_t j = 0; j < J; ++j) Lq[j] = T(lam * r[j] * c[j]);
218
219 pn.assign(lv[static_cast<std::size_t>(k) + 1].size(), zero);
220 for (std::size_t j = 0; j < J; ++j) {
221 if (!(r[j] > zero)) continue;
222 for (std::size_t i = 0; i < Sv.size(); ++i) {
223 const int nj = Sv[i][j] + 1;
224 const T mur = mux(j, static_cast<std::size_t>(nj) - 1);
225 const T w = T(lam * r[j] * T(num_traits<T>::from_int(nj) / mur) * p[i]);
226 pn[succ[static_cast<std::size_t>(k)][i][j]] += w;
227 }
228 }
229 };
230
231 // Chain order: the D distinct chains last, sharing the common prefix.
232 const std::size_t D = active.size();
233 std::vector<std::size_t> prefix;
234 for (std::size_t idx = 0; idx < D; ++idx)
235 for (int t = 0; t + 1 < N[active[idx]]; ++t) prefix.push_back(active[idx]);
236
237 std::vector<T> p(1, one);
238 int k = 0;
239 std::vector<T> pn, Lq;
240 T lam = zero;
241 const auto column = [&](std::size_t r) {
242 std::vector<T> col(J);
243 for (std::size_t j = 0; j < J; ++j) col[j] = Lx(j, r);
244 return col;
245 };
246 for (std::size_t idx = 0; idx < prefix.size(); ++idx) {
247 dac_step(p, column(prefix[idx]), k, pn, lam, Lq);
248 p = pn;
249 ++k;
250 }
251
252 std::vector<std::vector<T>> Sp(D);
253 Sp[0] = p;
254 std::vector<T> pb = p;
255 int kb = k;
256 for (std::size_t idx = 0; idx < D; ++idx) {
257 dac_step(pb, column(active[idx]), kb, pn, lam, Lq);
258 pb = pn;
259 ++kb;
260 if (idx + 1 < D) Sp[idx + 1] = pb;
261 }
262 res.Pjoint = pb;
263 res.states = lv[static_cast<std::size_t>(Nt)];
264
265 res.XN.assign(R, zero);
266 res.QN = Matrix<T>(M, R, zero);
267 {
268 const std::size_t r = active[D - 1];
269 res.XN[r] = T(num_traits<T>::from_int(N[r]) * lam);
270 for (std::size_t i = 0; i < M; ++i)
271 res.QN(i, r) = T(num_traits<T>::from_int(N[r]) * Lq[i]);
272 }
273 for (std::size_t idx = 0; idx + 1 < D; ++idx) {
274 std::vector<T> pc = Sp[idx];
275 int kc = k + static_cast<int>(idx);
276 std::vector<std::size_t> order;
277 for (std::size_t t = idx + 1; t < D; ++t) order.push_back(active[t]);
278 order.push_back(active[idx]);
279 for (std::size_t t = 0; t < order.size(); ++t) {
280 dac_step(pc, column(order[t]), kc, pn, lam, Lq);
281 pc = pn;
282 ++kc;
283 }
284 const std::size_t r = active[idx];
285 res.XN[r] = T(num_traits<T>::from_int(N[r]) * lam);
286 for (std::size_t i = 0; i < M; ++i)
287 res.QN(i, r) = T(num_traits<T>::from_int(N[r]) * Lq[i]);
288 }
289
290 res.PI = Matrix<T>(M, static_cast<std::size_t>(Nt) + 1, zero);
291 for (std::size_t i = 0; i < res.states.size(); ++i)
292 for (std::size_t j = 0; j < M; ++j)
293 res.PI(j, static_cast<std::size_t>(res.states[i][j])) += res.Pjoint[i];
294 res.UN.assign(M, zero);
295 for (std::size_t j = 0; j < M; ++j) res.UN[j] = T(one - res.PI(j, 0));
296 res.CN.assign(R, zero);
297 for (std::size_t idx = 0; idx < D; ++idx) {
298 const std::size_t r = active[idx];
299 if (res.XN[r] == zero) throw NumericError("pfqn_dac: zero chain throughput");
300 res.CN[r] = T(num_traits<T>::from_int(N[r]) / res.XN[r] - Zv[r]);
301 }
302 return res;
303}
304
305template <class T>
306DacResult<T> pfqn_dac(const Matrix<T>& L, const std::vector<int>& N, const std::vector<T>& Z) {
307 return pfqn_dac(L, N, Z, Matrix<T>());
308}
309
310} // namespace pfqn
311} // namespace line
312
313#endif // LINE_API_PFQN_PFQN_DAC_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.
DacResult< T > pfqn_dac(const Matrix< T > &L, const std::vector< int > &N, const std::vector< T > &Z, const Matrix< T > &mu)
Distribution Analysis by Chain (de Souza e Silva, UCLA CSD-870023, 1987): the JOINT queue-length dist...
Definition pfqn_dac.h:118
Number-type abstraction for the templated API port.
Return value of pfqn_dac, mirroring [Pjoint, states, XN, QN, UN, CN, pi].
Definition pfqn_dac.h:51
std::vector< T > Pjoint
one probability per row of states
Definition pfqn_dac.h:52
std::vector< T > XN
(R) chain throughput
Definition pfqn_dac.h:54
std::vector< T > UN
(M) utilization, 1 - P(empty)
Definition pfqn_dac.h:56
std::vector< std::vector< int > > states
aggregate states, J columns
Definition pfqn_dac.h:53
Matrix< T > QN
(M x R) mean queue length
Definition pfqn_dac.h:55
Matrix< T > PI
(M x (Nt+1)) marginal distribution
Definition pfqn_dac.h:58
std::vector< T > CN
(R) cycle time, excluding think time
Definition pfqn_dac.h:57