LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_ncoi.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_NCOI_H
6#define LINE_API_PFQN_NCOI_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Normalizing constant of a closed network of ORDER-INDEPENDENT (OI) /
12 * pass-and-swap stations with empty swap graph, plus one aggregated delay.
13 *
14 * Templated port of matlab/src/api/pfqn/pfqn_ncoi.m.
15 *
16 * An OI station's total service rate mu_i(n) depends on the per-class
17 * occupancy n only through which classes are present. Its balance function
18 * satisfies the balanced-fairness recursion of Bonald and Proutiere (2003),
19 *
20 * Phi_i(0) = 1, Phi_i(n) = (1/mu_i(n)) sum_{r: n_r>0} Phi_i(n - e_r),
21 *
22 * and G(N) is the convolution of the per-station balance functions with the
23 * multinomial delay factor prod_r Z_r^{n_r}/n_r!,
24 *
25 * g_0(n) = F_Z(n), g_i(n) = sum_{0<=x<=n} Phi_i(x) g_{i-1}(n-x),
26 * G(N) = g_K(N).
27 *
28 * This is a MACROSTATE routine: the balance functions and the convolution are
29 * both tabulated over the count lattice 0 <= n <= N, never over orderings.
30 * That is legitimate exactly because an OI rate is permutation-invariant, so
31 * Phi(n) -- itself the sum of the ordered-prefix weights over all orderings of
32 * the multiset n -- closes on the count vector. With a non-empty swap graph
33 * the closure fails and pfqn_pas_nc (microstate) must be used instead.
34 *
35 * Cost: O(K R L) for the balance functions and O(K prod_r (N_r+1)(N_r+2)/2)
36 * for the convolutions, with L = prod_r (N_r+1); that is the order of a
37 * load-dependent Buzen convolution.
38 *
39 * Arithmetic: EXACT-CAPABLE. The routine performs only reciprocals, additions
40 * and multiplications, plus the exact multinomial delay factor. Whether the
41 * result is exact therefore depends only on the rate callables, which are
42 * evaluated and never inspected. A nonpositive rate marks an occupancy the
43 * station cannot serve: its balance value is zero, which prunes every ordering
44 * through it, exactly as the reference does.
45 */
46
47#include <cstddef>
48#include <functional>
49#include <vector>
50
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/** An OI station's total service rate as a function of the occupancy vector. */
60template <class T>
61using OiRate = std::function<T(const std::vector<int>&)>;
62
63namespace detail {
64
65/** Column-major lattice of the box 0 <= n <= N, with its strides. */
66struct OiLattice {
67 std::vector<int> dims;
68 std::vector<int> strides;
69 std::size_t ngrid;
70 std::vector<std::vector<int>> counts; ///< counts[k] is the point at index k
71 std::vector<std::size_t> order; ///< indices sorted by total population
72};
73
74inline OiLattice oi_lattice(const std::vector<int>& N) {
75 const std::size_t R = N.size();
76 OiLattice lat;
77 lat.dims.resize(R);
78 lat.strides.resize(R);
79 std::size_t ngrid = 1;
80 int total = 0;
81 for (std::size_t r = 0; r < R; ++r) {
82 lat.dims[r] = N[r] + 1;
83 lat.strides[r] = static_cast<int>(ngrid);
84 ngrid *= static_cast<std::size_t>(lat.dims[r]);
85 total += N[r];
86 }
87 lat.ngrid = ngrid;
88 lat.counts.assign(ngrid, std::vector<int>(R, 0));
89 std::vector<std::vector<std::size_t>> byPop(static_cast<std::size_t>(total) + 1);
90 for (std::size_t k = 0; k < ngrid; ++k) {
91 std::size_t rest = k;
92 int sum = 0;
93 for (std::size_t r = 0; r < R; ++r) {
94 lat.counts[k][r] = static_cast<int>(rest % static_cast<std::size_t>(lat.dims[r]));
95 rest /= static_cast<std::size_t>(lat.dims[r]);
96 sum += lat.counts[k][r];
97 }
98 byPop[static_cast<std::size_t>(sum)].push_back(k);
99 }
100 lat.order.reserve(ngrid);
101 for (std::size_t s = 0; s < byPop.size(); ++s)
102 for (std::size_t i = 0; i < byPop[s].size(); ++i) lat.order.push_back(byPop[s][i]);
103 return lat;
104}
105
106/**
107 * v-weighted balanced-fairness recursion over the count lattice, in increasing
108 * population: Phi(n) = (1/mu(n)) sum_{r: n_r>0} v_r Phi(n - e_r). vis is the
109 * station's class visit vector, whose geometric factor prod_r v_r^{n_r} carries
110 * the OI-station visit ratio; empty means unit visits.
111 */
112template <class T>
113std::vector<T> oi_nc_balance(const OiLattice& lat, const OiRate<T>& rate, std::size_t R,
114 const std::vector<T>& vis) {
115 const T zero = num_traits<T>::from_int(0);
116 const T one = num_traits<T>::from_int(1);
117 std::vector<T> phi(lat.ngrid, zero);
118 for (std::size_t t = 0; t < lat.order.size(); ++t) {
119 const std::size_t k = lat.order[t];
120 const std::vector<int>& n = lat.counts[k];
121 int sum = 0;
122 for (std::size_t r = 0; r < R; ++r) sum += n[r];
123 if (sum == 0) {
124 phi[k] = one;
125 continue;
126 }
127 const T mun = rate(n);
128 if (!(mun > zero)) continue; // unreachable station state: zero balance
129 T acc = zero;
130 for (std::size_t r = 0; r < R; ++r)
131 if (n[r] > 0)
132 acc += (vis.empty() ? one : vis[r]) *
133 phi[k - static_cast<std::size_t>(lat.strides[r])];
134 phi[k] = acc / mun;
135 }
136 return phi;
137}
138
139} // namespace detail
140
141/**
142 * @brief Normalizing constant of a closed network of ORDER-INDEPENDENT (OI) /
143 * pass-and-swap stations with empty swap graph, plus one aggregated
144 * delay.
145 *
146 * @param Z (R) think-time demand of the aggregated delay node
147 * @param N (R) closed population, finite
148 * @param mu (K) OI rate callables, one per station; may be empty
149 * @param visits (K x R) per-station class visit ratios weighting the balance
150 * recursion; empty for unit visits
151 */
152template <class T>
153NcResult<T> pfqn_ncoi(const std::vector<T>& Z, const std::vector<int>& N,
154 const std::vector<OiRate<T>>& mu, const Matrix<T>& visits) {
155 const std::size_t R = N.size();
156 if (Z.size() != R) throw InputError("pfqn_ncoi: Z and N must have the same class count");
157 if (!visits.empty() && (visits.rows() != mu.size() || visits.cols() != R))
158 throw InputError("pfqn_ncoi: visits must be K x R");
159 for (int v : N)
160 if (v < 0) throw InputError("pfqn_ncoi: requires finite, nonnegative populations");
161 for (std::size_t i = 0; i < mu.size(); ++i)
162 if (!mu[i]) throw InputError("pfqn_ncoi: an OI rate callable is empty");
163
164 const T zero = num_traits<T>::from_int(0);
165 const T one = num_traits<T>::from_int(1);
166 if (R == 0) return {one, num_traits<T>::log_as_double(one)};
167
168 const detail::OiLattice lat = detail::oi_lattice(N);
169
170 // Delay balance function: the multinomial factor F_Z(n). A class with
171 // population but no delay demand makes the state infeasible.
172 std::vector<T> g(lat.ngrid, zero);
173 for (std::size_t k = 0; k < lat.ngrid; ++k) {
174 T f = one;
175 bool feas = true;
176 for (std::size_t r = 0; r < R; ++r) {
177 const int nr = lat.counts[k][r];
178 if (nr == 0) continue;
179 if (!(Z[r] > zero)) {
180 feas = false;
181 break;
182 }
183 f *= num_pow_int(Z[r], static_cast<unsigned>(nr)) /
184 num_factorial<T>(static_cast<unsigned>(nr));
185 }
186 if (feas) g[k] = f;
187 }
188
189 // Convolve in one OI station at a time.
190 std::vector<int> rem(R, 0), y(R, 0);
191 for (std::size_t i = 0; i < mu.size(); ++i) {
192 std::vector<T> vis;
193 if (!visits.empty()) {
194 vis.resize(R);
195 for (std::size_t r = 0; r < R; ++r) vis[r] = visits(i, r);
196 }
197 const std::vector<T> phi = detail::oi_nc_balance<T>(lat, mu[i], R, vis);
198 std::vector<T> gnext(lat.ngrid, zero);
199 for (std::size_t kx = 0; kx < lat.ngrid; ++kx) {
200 if (!(phi[kx] != zero)) continue;
201 std::size_t base = 0;
202 for (std::size_t r = 0; r < R; ++r) {
203 rem[r] = N[r] - lat.counts[kx][r];
204 y[r] = 0;
205 base += static_cast<std::size_t>(lat.counts[kx][r]) *
206 static_cast<std::size_t>(lat.strides[r]);
207 }
208 // Odometer over the sub-box 0 <= y <= rem; lin() is linear, so the
209 // target index of x + y is base + lin(y).
210 for (;;) {
211 std::size_t ylin = 0;
212 for (std::size_t r = 0; r < R; ++r)
213 ylin += static_cast<std::size_t>(y[r]) * static_cast<std::size_t>(lat.strides[r]);
214 gnext[base + ylin] += phi[kx] * g[ylin];
215 std::size_t d = 0;
216 while (d < R && y[d] == rem[d]) {
217 y[d] = 0;
218 ++d;
219 }
220 if (d == R) break;
221 y[d] += 1;
222 }
223 }
224 g.swap(gnext);
225 }
226
227 const T G = g[lat.ngrid - 1];
228 return {G, num_traits<T>::log_as_double(G)};
229}
230
231/** Overload with unit visits. */
232template <class T>
233NcResult<T> pfqn_ncoi(const std::vector<T>& Z, const std::vector<int>& N,
234 const std::vector<OiRate<T>>& mu) {
235 return pfqn_ncoi(Z, N, mu, Matrix<T>());
236}
237
238} // namespace pfqn
239} // namespace line
240
241#endif // LINE_API_PFQN_NCOI_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
The exception types the port throws.
Dense matrix and non-owning view.
NcResult< T > pfqn_ncoi(const std::vector< T > &Z, const std::vector< int > &N, const std::vector< OiRate< T > > &mu, const Matrix< T > &visits)
Normalizing constant of a closed network of ORDER-INDEPENDENT (OI) / pass-and-swap stations with empt...
Definition pfqn_ncoi.h:153
std::function< T(const std::vector< int > &)> OiRate
An OI station's total service rate as a function of the occupancy vector.
Definition pfqn_ncoi.h:61
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...
Return value of the normalizing-constant family, mirroring Ret.pfqnNc.
Definition pfqn_ca.h:44