LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_mvaoi_marg.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_MVAOI_MARG_H
6#define LINE_API_PFQN_MVAOI_MARG_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Exact marginal load-dependent MVA for a closed network of delay,
12 * load-independent and ANY number of order-independent (OI) stations.
13 *
14 * Templated port of matlab/src/api/pfqn/pfqn_mvaoi_marg.m. This is the
15 * marginal-distribution counterpart of pfqn_mvaoi (the mean-value CMVA form):
16 * the same X and Q by a completely different route, which is what makes the
17 * pair worth having.
18 *
19 * Each OI station carries its own joint COUNT-VECTOR marginal
20 *
21 * pM_i(n | k) = (1/mu_i(n)) sum_r X_r(k) pM_i(n - e_r | k - e_r), n != 0
22 * pM_i(0 | k) = 1 - sum_{n != 0} pM_i(n | k)
23 *
24 * driven by the common per-class throughput. The recursion is exact per station
25 * because in product form pM_i(n|k) = Phi_i(n) G_{-i}(k-n)/G(k) with
26 * X_r(k) = G(k-e_r)/G(k) and the balanced-fairness identity
27 * Phi_i(n) = (1/mu_i(n)) sum_r Phi_i(n - e_r).
28 *
29 * Because the OI rate is class dependent, the mean-value response-time formula
30 * is NOT exact, so X_r(k) is closed at every population level by population
31 * conservation, X_r A_r + sum_i QM_ir(k;X) = k_r with A_r the non-OI residence
32 * sum, and QM_ir read off the exact marginal. That closure is implicit in X and
33 * the reference solves it by damped substitution (factor 1/2, tolerance 1e-13,
34 * 2000 sweeps). This port keeps the same iteration; changing the damping or the
35 * sweep count changes the last digits.
36 *
37 * RATE HANDLE CONVENTION, a genuine trap. pfqn_mvaoi's handles take the
38 * per-class COUNT VECTOR n. This routine's take the MICROSTATE, the ordered
39 * list of class indices with repetition (MATLAB's repelem(1:R, n)). The
40 * reference keeps both conventions and converts between them in oi_rate; the
41 * port keeps them too rather than silently unifying, so a handle written for
42 * one routine is not accidentally accepted by the other. Microstate indices are
43 * ZERO-based here, MATLAB's are one-based; a permutation-invariant rate, which
44 * is what "order independent" means, cannot tell the difference, and any handle
45 * that could is not an OI rate.
46 *
47 * Arithmetic: no transcendental, so it is left UNGATED and instantiates at
48 * Rational. As with pfqn_momlin the exact instantiation is available rather
49 * than advisable: the closure is a fixed point reached only in the limit, so
50 * rational iterates grow without buying accuracy. Use double or Real.
51 *
52 * REFERENCE DEFECTS: none found.
53 */
54
55#include <algorithm>
56#include <cstddef>
57#include <functional>
58#include <map>
59#include <vector>
60
61#include "line/num/number.h"
62#include "line/util/error.h"
63#include "line/util/matrix.h"
64
65namespace line {
66namespace pfqn {
67
68/** Return value of pfqn_mvaoi_marg, mirroring [XN, QN]. */
69template <class T>
71 std::vector<T> X; ///< (R) per-class throughput
72 Matrix<T> Q; ///< (M x R) per-class queue length at every station
73};
74
75namespace detail {
76
77/** All integer vectors 0 <= v <= bound, first component varying fastest. */
78inline std::vector<std::vector<int>> enum_vecs(const std::vector<int>& bound) {
79 std::vector<std::vector<int>> out;
80 std::size_t total = 1;
81 for (std::size_t r = 0; r < bound.size(); ++r)
82 total *= static_cast<std::size_t>(bound[r]) + 1;
83 out.reserve(total);
84 std::vector<int> v(bound.size(), 0);
85 for (std::size_t i = 0; i < total; ++i) {
86 std::size_t li = i;
87 for (std::size_t r = 0; r < bound.size(); ++r) {
88 v[r] = static_cast<int>(li % (static_cast<std::size_t>(bound[r]) + 1));
89 li /= static_cast<std::size_t>(bound[r]) + 1;
90 }
91 out.push_back(v);
92 }
93 return out;
94}
95
96/** Microstate of a count vector: class indices with repetition, ascending. */
97inline std::vector<int> microstate(const std::vector<int>& n) {
98 std::vector<int> mi;
99 for (std::size_t r = 0; r < n.size(); ++r)
100 for (int c = 0; c < n[r]; ++c) mi.push_back(static_cast<int>(r));
101 return mi;
102}
103
104} // namespace detail
105
106/**
107 * @brief Exact marginal load-dependent MVA for a closed network of delay,
108 * load-independent and ANY number of order-independent (OI) stations.
109 *
110 * @param D (M x R) per-class demands; the rows of OI stations are ignored
111 * @param N (R) closed populations
112 * @param isDelay (M) true for infinite-server stations
113 * @param mu (M) rate handles; callable only at the OI stations, and taking
114 * the MICROSTATE (see the header note), not the count vector
115 */
116template <class T>
117MvaoiMargResult<T> pfqn_mvaoi_marg(const Matrix<T>& D, const std::vector<int>& N,
118 const std::vector<bool>& isDelay,
119 const std::vector<std::function<T(const std::vector<int>&)>>& mu) {
120 const std::size_t M = D.rows(), R = N.size();
121 if (M == 0 || R == 0) throw InputError("pfqn_mvaoi_marg: empty model");
122 if (D.cols() != R) throw InputError("pfqn_mvaoi_marg: D and N disagree on the class count");
123 if (isDelay.size() != M || mu.size() != M)
124 throw InputError("pfqn_mvaoi_marg: isDelay and mu must have one entry per station");
125 for (std::size_t r = 0; r < R; ++r)
126 if (N[r] < 0) throw InputError("pfqn_mvaoi_marg: negative population");
127
128 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
129 const T half = num_traits<T>::from_rational(1, 2);
130 const T xtol = num_traits<T>::from_double(1e-13);
131
132 std::vector<std::size_t> oi_list;
133 std::vector<bool> isOI(M, false);
134 for (std::size_t i = 0; i < M; ++i)
135 if (mu[i]) {
136 isOI[i] = true;
137 oi_list.push_back(i);
138 }
139 const std::size_t nOI = oi_list.size();
140 if (nOI == 0)
141 throw InputError(
142 "pfqn_mvaoi_marg: at least one order-independent station is required");
143
144 // OI total rate at a count vector, through the microstate as the reference does
145 std::vector<std::function<T(const std::vector<int>&)>> muM(nOI);
146 for (std::size_t o = 0; o < nOI; ++o) {
147 const std::function<T(const std::vector<int>&)> f = mu[oi_list[o]];
148 muM[o] = [f, zero](const std::vector<int>& n) -> T {
149 int tot = 0;
150 for (int x : n) tot += x;
151 if (tot == 0) return zero;
152 return f(detail::microstate(n));
153 };
154 }
155
156 std::map<std::vector<int>, std::vector<T>> X_cache;
157 std::map<std::vector<int>, Matrix<T>> Q_cache;
158 std::vector<std::map<std::vector<int>, std::map<std::vector<int>, T>>> pM(nOI);
159
160 const std::vector<int> zeroK(R, 0);
161 X_cache[zeroK] = std::vector<T>(R, zero);
162 Q_cache[zeroK] = Matrix<T>(M, R, zero);
163 for (std::size_t o = 0; o < nOI; ++o) pM[o][zeroK][zeroK] = one;
164
165 std::vector<std::vector<int>> pops = detail::enum_vecs(N);
166 std::stable_sort(pops.begin(), pops.end(),
167 [](const std::vector<int>& a, const std::vector<int>& b) {
168 int sa = 0, sb = 0;
169 for (int x : a) sa += x;
170 for (int x : b) sb += x;
171 if (sa != sb) return sa < sb;
172 return a < b;
173 });
174
175 // marginal of one OI station at population k, given the throughput Xk
176 const auto oi_marginal = [&](const std::vector<int>& k, const std::vector<T>& Xk,
177 const std::function<T(const std::vector<int>&)>& mrate,
178 const std::map<std::vector<int>, std::map<std::vector<int>, T>>& cache) {
179 std::map<std::vector<int>, T> out;
180 const std::vector<std::vector<int>> vecs = detail::enum_vecs(k);
181 T psum = zero;
182 bool haveZero = false;
183 for (std::size_t idx = 0; idx < vecs.size(); ++idx) {
184 const std::vector<int>& n = vecs[idx];
185 int tot = 0;
186 for (int x : n) tot += x;
187 if (tot == 0) {
188 haveZero = true;
189 continue;
190 }
191 const T rate = mrate(n);
192 if (!(rate > zero)) continue;
193 T acc = zero;
194 for (std::size_t r = 0; r < R; ++r) {
195 if (n[r] < 1 || k[r] < 1) continue;
196 std::vector<int> nr = n, kr = k;
197 nr[r] -= 1;
198 kr[r] -= 1;
199 auto itk = cache.find(kr);
200 if (itk == cache.end()) continue;
201 auto itn = itk->second.find(nr);
202 if (itn == itk->second.end()) continue;
203 acc += Xk[r] * itn->second;
204 }
205 const T p = acc / rate;
206 out[n] = p;
207 psum += p;
208 }
209 if (haveZero) out[std::vector<int>(R, 0)] = one - psum;
210 return out;
211 };
212
213 for (std::size_t pidx = 0; pidx < pops.size(); ++pidx) {
214 const std::vector<int>& k = pops[pidx];
215 int ktot = 0;
216 for (int x : k) ktot += x;
217 if (ktot == 0) continue;
218
219 // non-OI response times by the arrival theorem at k - e_r
220 Matrix<T> Rfix(M, R, zero);
221 std::vector<T> A(R, zero);
222 for (std::size_t r = 0; r < R; ++r) {
223 if (k[r] == 0) continue;
224 std::vector<int> kr = k;
225 kr[r] -= 1;
226 const Matrix<T>& Qkr = Q_cache.at(kr);
227 for (std::size_t i = 0; i < M; ++i) {
228 if (isOI[i]) continue;
229 if (isDelay[i]) {
230 Rfix(i, r) = D(i, r);
231 } else {
232 T s = zero;
233 for (std::size_t t = 0; t < R; ++t) s += Qkr(i, t);
234 Rfix(i, r) = D(i, r) * (one + s);
235 }
236 A[r] += Rfix(i, r);
237 }
238 }
239
240 std::vector<T> Xk(R, zero);
241 for (std::size_t r = 0; r < R; ++r)
242 if (k[r] > 0) Xk[r] = num_traits<T>::from_int(k[r]) / (A[r] + one);
243
244 std::vector<std::map<std::vector<int>, T>> marg(nOI);
245 for (int it = 0; it < 2000; ++it) {
246 std::vector<T> QMtot(R, zero);
247 for (std::size_t o = 0; o < nOI; ++o) {
248 marg[o] = oi_marginal(k, Xk, muM[o], pM[o]);
249 for (auto itm = marg[o].begin(); itm != marg[o].end(); ++itm)
250 for (std::size_t r = 0; r < R; ++r)
251 QMtot[r] += num_traits<T>::from_int(itm->first[r]) * itm->second;
252 }
253 std::vector<T> Xnew(R, zero);
254 for (std::size_t r = 0; r < R; ++r)
255 if (k[r] > 0 && A[r] > zero) {
256 const T v = (num_traits<T>::from_int(k[r]) - QMtot[r]) / A[r];
257 Xnew[r] = (v > zero) ? v : zero;
258 }
259 T mx = zero;
260 for (std::size_t r = 0; r < R; ++r) {
261 const T d = num_abs(T(Xnew[r] - Xk[r]));
262 if (d > mx) mx = d;
263 }
264 if (mx < xtol) {
265 Xk = Xnew;
266 break;
267 }
268 for (std::size_t r = 0; r < R; ++r) Xk[r] = half * Xk[r] + half * Xnew[r];
269 }
270 for (std::size_t o = 0; o < nOI; ++o) marg[o] = oi_marginal(k, Xk, muM[o], pM[o]);
271
272 Matrix<T> Qk(M, R, zero);
273 for (std::size_t o = 0; o < nOI; ++o)
274 for (auto itm = marg[o].begin(); itm != marg[o].end(); ++itm)
275 for (std::size_t r = 0; r < R; ++r)
276 Qk(oi_list[o], r) += num_traits<T>::from_int(itm->first[r]) * itm->second;
277 for (std::size_t r = 0; r < R; ++r)
278 for (std::size_t i = 0; i < M; ++i)
279 if (!isOI[i]) Qk(i, r) = Xk[r] * Rfix(i, r);
280
281 X_cache[k] = Xk;
282 Q_cache[k] = Qk;
283 for (std::size_t o = 0; o < nOI; ++o) pM[o][k] = marg[o];
284 }
285
287 res.X = X_cache.at(N);
288 res.Q = Q_cache.at(N);
289 return res;
290}
291
292} // namespace pfqn
293} // namespace line
294
295#endif // LINE_API_PFQN_MVAOI_MARG_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
The exception types the port throws.
Dense matrix and non-owning view.
MvaoiMargResult< T > pfqn_mvaoi_marg(const Matrix< T > &D, const std::vector< int > &N, const std::vector< bool > &isDelay, const std::vector< std::function< T(const std::vector< int > &)> > &mu)
Exact marginal load-dependent MVA for a closed network of delay, load-independent and ANY number of o...
T num_abs(const T &v)
Definition number.h:172
Number-type abstraction for the templated API port.
Return value of pfqn_mvaoi_marg, mirroring [XN, QN].
std::vector< T > X
(R) per-class throughput
Matrix< T > Q
(M x R) per-class queue length at every station