LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_conv.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_CONV_H
6#define LINE_API_PFQN_CONV_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Multichain convolution algorithm with class-dependent service rates
12 * (Sauer 1983, "Computational Algorithms for State-Dependent Queueing
13 * Networks", ACM TOCS 1(1):67-92, Section 5.2).
14 *
15 * Templated port of matlab/src/api/pfqn/pfqn_conv.m.
16 *
17 * G(N) is the multivariate discrete convolution of the M station factors and
18 * the delay factor,
19 *
20 * G_0(n) = F_Z(n) = prod_r Z_r^{n_r} / n_r!,
21 * G_m(n) = sum_{0 <= i <= n} X_m(i) G_{m-1}(n - i),
22 *
23 * where a class-dependent station builds its factor from Sauer eq. (40),
24 *
25 * X_m(n) = (|n| / n_r) (L(m,r) / beta_{m,r}(n)) X_m(n - e_r), X_m(0) = 1,
26 *
27 * for the first class r with n_r > 0. beta is the DIMENSIONLESS scaling of the
28 * service demand supplied by the caller, so beta = 1 means "no correction" and
29 * the recurrence collapses to the load-independent multinomial form. A station
30 * with no scaling callable is folded in by the classical in-place Buzen update
31 *
32 * G_m(n) = G_{m-1}(n) + sum_r L(m,r) G_m(n - e_r)
33 *
34 * in O(P R) rather than O(P^2), P = prod_r (N_r + 1), so a model with no
35 * class-dependent station reduces operation for operation to pfqn_ca on the
36 * same demands and returns the identical value.
37 *
38 * Arithmetic: EXACT-CAPABLE. Every operation is an addition, a multiplication
39 * or a division in the field of the inputs; the reference's use of log/exp
40 * inside its local Fz is a range-management device for the delay factor and is
41 * replaced here by the same detail::pff_delay that pfqn_ca uses. Whether the
42 * result is exact for a class-dependent station is a property of the supplied
43 * beta callables, which are evaluated but never inspected. Note that unlike
44 * pfqn_ca this routine applies NO power-of-two rescaling in double: the
45 * reference does not, and a class-dependent station factor is not homogeneous
46 * in the demands once beta is state dependent, so no exact exponent shift
47 * exists in general. Use T = Real<D> or T = Rational when the constant leaves
48 * the double range.
49 */
50
51#include <algorithm>
52#include <cmath>
53#include <cstddef>
54#include <limits>
55#include <string>
56#include <vector>
57
60#include "line/num/number.h"
61#include "line/util/error.h"
62#include "line/util/matrix.h"
64
65namespace line {
66namespace pfqn {
67
68/**
69 * @brief Multichain convolution algorithm with class-dependent service rates
70 * (Sauer 1983, "Computational Algorithms for State-Dependent Queueing
71 * Networks", ACM TOCS 1(1):67-92, Section 5.2).
72 *
73 * @param L (M x R) service demands
74 * @param N (R) population per class, finite
75 * @param Z (K x R) think times, summed over rows; may be empty
76 * @param cdscaling (M) class-dependence callables; an empty entry marks a
77 * load-independent station. Pass an empty vector for none.
78 */
79template <class T>
80NcResult<T> pfqn_conv(const Matrix<T>& L, const std::vector<int>& N, const Matrix<T>& Z,
81 const std::vector<CdScaling<T>>& cdscaling) {
82 const std::size_t M = L.empty() ? 0 : L.rows();
83 const std::size_t R = N.size();
84 if (!L.empty() && L.cols() != R)
85 throw InputError("pfqn_conv: L and N disagree on the class count");
86 if (!cdscaling.empty() && cdscaling.size() != M)
87 throw InputError("pfqn_conv: the scaling vector has the wrong station count");
88 for (int v : N)
89 if (v < 0) throw InputError("pfqn_conv: the convolution algorithm requires finite, "
90 "nonnegative (closed) populations");
91
92 const T zero = num_traits<T>::from_int(0);
93 const T one = num_traits<T>::from_int(1);
94
95 std::vector<T> Zsum(R, zero);
96 if (!Z.empty()) {
97 if (Z.cols() != R) throw InputError("pfqn_conv: Z and N disagree on the class count");
98 for (std::size_t k = 0; k < Z.rows(); ++k)
99 for (std::size_t r = 0; r < R; ++r) Zsum[r] += Z(k, r);
100 }
101
102 const std::vector<std::size_t> prods = plane_sizes(N);
103 const std::size_t total = population_count(N);
104
105 // ---- G_0(n) = F_Z(n) ----------------------------------------------------
106 std::vector<T> G(total, zero);
107 {
108 std::vector<int> n(R, 0);
109 bool more = true;
110 while (more) {
111 G[pop_index(n, prods)] = detail::pff_delay(Zsum, n);
112 more = next_pop(n, N);
113 }
114 }
115
116 // ---- fold in one station at a time --------------------------------------
117 for (std::size_t ist = 0; ist < M; ++ist) {
118 const bool isCd = !cdscaling.empty() && static_cast<bool>(cdscaling[ist]);
119 if (!isCd) {
120 // Load-independent: in-place Buzen update, lexicographic order
121 // guarantees n - e_r is already at its new value.
122 std::vector<int> n(R, 0);
123 bool more = true;
124 while (more) {
125 const std::size_t idx = pop_index(n, prods);
126 T acc = G[idx];
127 for (std::size_t r = 0; r < R; ++r)
128 if (n[r] >= 1) acc += L(ist, r) * G[idx - prods[r]];
129 G[idx] = acc;
130 more = next_pop(n, N);
131 }
132 continue;
133 }
134
135 // Class-dependent: build the station factor X_m, then convolve.
136 std::vector<T> Xm(total, zero);
137 Xm[0] = one;
138 {
139 std::vector<int> n(R, 0);
140 bool more = next_pop(n, N); // X_m(0) is already set
141 while (more) {
142 const std::size_t idx = pop_index(n, prods);
143 std::size_t r = 0;
144 while (r < R && n[r] == 0) ++r;
145 // next_pop only yields nonzero vectors past the origin, so r < R.
146 std::vector<T> row(R);
147 for (std::size_t s = 0; s < R; ++s)
148 row[s] = num_traits<T>::from_int(n[s]);
149 const std::vector<T> bval = cdscaling[ist](row);
150 if (bval.empty())
151 throw InputError("pfqn_conv: a class-dependence callable returned nothing");
152 const T beta = bval.size() > 1 ? bval.at(r) : bval[0];
153 if (beta > zero) {
154 int tot = 0;
155 for (int v : n) tot += v;
156 const T fac = num_traits<T>::from_int(tot) / num_traits<T>::from_int(n[r]);
157 Xm[idx] = fac * (L(ist, r) / beta) * Xm[idx - prods[r]];
158 } else {
159 Xm[idx] = zero; // a nonpositive scaling kills the state
160 }
161 more = next_pop(n, N);
162 }
163 }
164 // The recursion walks each vector along its FIRST busy class; that is the station's product-form factor
165 // only if every other class gives the same value (Kolmogorov's criterion). A joint scaling such as
166 // eta(n) = min(n_A, c) is zero whenever no class-A job is present, and class B alone was then never served
167 // (QLen 0 with Tput 2). Refuse instead, as pfqn_conv.m does.
168 {
169 std::vector<int> n(R, 0);
170 bool more = next_pop(n, N);
171 while (more) {
172 const std::size_t idx = pop_index(n, prods);
173 std::vector<T> row(R);
174 int tot = 0;
175 for (std::size_t s = 0; s < R; ++s) { row[s] = num_traits<T>::from_int(n[s]); tot += n[s]; }
176 const std::vector<T> bval = cdscaling[ist](row);
177 for (std::size_t r = 0; r < R; ++r) {
178 if (n[r] <= 0 || !(L(ist, r) > zero)) continue;
179 const T beta = bval.size() > 1 ? bval.at(r) : bval[0];
180 std::string pop = "[";
181 for (std::size_t s = 0; s < R; ++s) pop += (s ? ", " : "") + std::to_string(n[s]);
182 pop += "]";
183 if (!(beta > zero))
184 throw UnsupportedError(ErrorKind{"ClassDependence"},
185 "The class/joint dependence at station " + std::to_string(ist + 1) + " stops class " +
186 std::to_string(r + 1) + " in the population " + pop + " (zero rate): the network has no "
187 "product form, so the normalizing constant does not apply. Use SolverCTMC, SolverSSA or "
188 "SolverLDES.");
189 const T fac = num_traits<T>::from_int(tot) / num_traits<T>::from_int(n[r]);
190 const double cand = num_traits<T>::to_double(T(fac * (L(ist, r) / beta) * Xm[idx - prods[r]]));
191 const double ref = num_traits<T>::to_double(Xm[idx]);
192 if (std::fabs(cand - ref) > 1e-8 * std::max(std::fabs(ref), std::numeric_limits<double>::min()))
193 throw UnsupportedError(ErrorKind{"ClassDependence"},
194 "The class/joint dependence at station " + std::to_string(ist + 1) + " does not satisfy "
195 "Kolmogorov's criterion (population " + pop + "): the network has no product form, so the "
196 "normalizing constant does not apply. Use SolverCTMC, SolverSSA or SolverLDES.");
197 }
198 more = next_pop(n, N);
199 }
200 }
201
202 std::vector<T> Gold(G);
203 std::vector<int> n(R, 0);
204 bool more = true;
205 while (more) {
206 const std::size_t idxn = pop_index(n, prods);
207 T acc = zero;
208 // Inner sweep over 0 <= i <= n.
209 std::vector<int> i(R, 0);
210 bool more_i = true;
211 while (more_i) {
212 std::size_t idx_i = 0, idx_nmi = 0;
213 for (std::size_t r = 0; r < R; ++r) {
214 idx_i += prods[r] * static_cast<std::size_t>(i[r]);
215 idx_nmi += prods[r] * static_cast<std::size_t>(n[r] - i[r]);
216 }
217 acc += Xm[idx_i] * Gold[idx_nmi];
218 more_i = next_pop(i, n);
219 }
220 G[idxn] = acc;
221 more = next_pop(n, N);
222 }
223 }
224
225 const T Gn = G[total - 1];
226 return {Gn, num_traits<T>::log_as_double(Gn)};
227}
228
229/** Overload with no class dependence, i.e. plain multichain convolution. */
230template <class T>
231NcResult<T> pfqn_conv(const Matrix<T>& L, const std::vector<int>& N, const Matrix<T>& Z) {
232 return pfqn_conv(L, N, Z, std::vector<CdScaling<T>>());
233}
234
235template <class T>
236NcResult<T> pfqn_conv(const Matrix<T>& L, const std::vector<int>& N) {
237 return pfqn_conv(L, N, Matrix<T>(), std::vector<CdScaling<T>>());
238}
239
240} // namespace pfqn
241} // namespace line
242
243#endif // LINE_API_PFQN_CONV_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
UnsupportedError(const std::string &what)
Definition error.h:119
The exception types the port throws.
Dense matrix and non-owning view.
std::function< std::vector< T >(const std::vector< T > &)> CdScaling
A per-station class-dependence callable: the population row -> 1 or R rates.
Definition pfqn_cdfun.h:45
NcResult< T > pfqn_conv(const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &Z, const std::vector< CdScaling< T > > &cdscaling)
Multichain convolution algorithm with class-dependent service rates (Sauer 1983, "Computational Algor...
Definition pfqn_conv.h:80
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
std::size_t population_count(const std::vector< int > &N)
Number of population vectors n with 0 <= n <= N.
Definition population.h:38
std::vector< std::size_t > plane_sizes(const std::vector< int > &N)
Mixed-radix plane sizes: prods[r] = prod_{s<r} (N[s]+1).
Definition population.h:27
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
std::size_t pop_index(const std::vector< int > &n, const std::vector< std::size_t > &prods)
Index of n in the lattice, 0-based (MATLAB hashpop is 1-based).
Definition population.h:45
Number-type abstraction for the templated API port.
Convolution algorithm for the exact normalizing constant of a closed product-form network (Buzen 1973...
AMVA-QD class-dependence function.
Population-vector enumeration and combinatorics.
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
Return value of the normalizing-constant family, mirroring Ret.pfqnNc.
Definition pfqn_ca.h:44