LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_qdamva.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_QDAMVA_H
6#define LINE_API_PFQN_QDAMVA_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * QD-AMVA: queue-dependent approximate mean value analysis.
12 *
13 * Port of matlab/src/api/pfqn/pfqn_qdamva.m, the queue-dependent AMVA of
14 * Casale, Perez and Wang (IFIP PERFORMANCE 2015), on a closed multiclass
15 * product-form network.
16 *
17 * A Schweitzer/Bard core in which the class-r demand at station k is scaled by
18 * the queue-dependence term g_k evaluated at the ARRIVAL-INSTANT total queue
19 * length, `g = pfqn_lldfun(1 + delta * rowsum(Q), mu)`.
20 *
21 * SETTING `mu` TO A CONSTANT ROW RECOVERS PLAIN SCHWEITZER AMVA ONLY FOR A
22 * SINGLE CLASS. `pfqn_lldfun` does skip a row of ONES, so g == 1 there, but the
23 * residence time that remains is `1 + delta * rowsum(Q)` with ONE aggregate
24 * delta = (sum(N)-1)/sum(N) applied to the whole arrival-instant queue, where
25 * Bard-Schweitzer shrinks the TAGGED class alone:
26 *
27 * 1 + sum_{s != r} Q(k,s) + (N(r)-1)/N(r) * Q(k,r)
28 *
29 * The two coincide iff K == 1. Measured over 40 random three-class instances,
30 * pfqn_qdamva(L,N,Z,ones) departs from pfqn_bs by up to 0.217 in absolute queue
31 * length, and is the LESS accurate of the two on single-server multiclass models
32 * (mean relative error on Q 0.069 against 0.056 at R = 3), the aggregate delta
33 * buying nothing once g == 1. This is the QD-AMVA closure, not a defect of the
34 * port, but do not use the function as a Schweitzer oracle for K > 1.
35 *
36 * MU IS A DIMENSIONLESS RATE MULTIPLIER, NOT A RATE. mu(k,n) is the factor by
37 * which station k serves faster when it holds n jobs. Two traps follow from
38 * `pfqn_lldfun` and are the reference's, not this port's:
39 *
40 * - it SKIPS a station whose mu row is identically ONE, so a single-server
41 * station must be a row of ones and a c-server station `min(1..smax, c)`.
42 * Until 2026-09-13 the gate was `range(...) > 0` and skipped EVERY constant
43 * row, so a uniform multiplier c != 1 silently returned g = 1 -- the station
44 * ran unscaled, against what solver_mvald and SolverCTMC give.
45 * - smax = mu.cols() must be at least ceil(sum(N)) or the interpolation
46 * clamps the population and the top of the rate curve is never reached.
47 *
48 * Delay stations are carried in Z, not as rows of L. Closed classes only: an
49 * infinite N(r) is not supported.
50 *
51 * Arithmetic: TRANSCENDENTAL-GATED, inherited whole from `pfqn_lldfun` -- the
52 * softmin it evaluates is not an element of the field generated by the inputs.
53 */
54
55#include <algorithm>
56#include <cmath>
57#include <cstddef>
58#include <vector>
59
61#include "line/util/error.h"
62#include "line/util/matrix.h"
63#include "line/num/number.h"
64
65namespace line {
66namespace pfqn {
67
68/** What `pfqn_qdamva` returns: the fixed point and how it was reached. */
69template <class T>
71 Matrix<T> Q; ///< (M x R) mean queue lengths
72 Matrix<T> X; ///< (1 x R) per-class throughputs
73 Matrix<T> U; ///< (M x R) per-class utilizations, carrying the g scaling
74 Matrix<T> R; ///< (M x R) per-class residence times, Q = X .* R
75 std::size_t iter = 0;
76};
77
78/**
79 * @brief QD-AMVA: queue-dependent approximate mean value analysis.
80 *
81 * @param L (M x R) service demand matrix.
82 * @param N (R) population vector, finite.
83 * @param Z (R) think time vector; empty means no think time.
84 * @param mu (M x smax) queue-dependent rate multipliers; empty means none.
85 * @param Q0 (M x R) initial guess; empty means the reference's demand split.
86 * @param tol convergence tolerance on the queue lengths (default 1e-6).
87 * @param maxiter maximum number of iterations (default 10000).
88 */
89template <class T>
90QdAmvaResult<T> pfqn_qdamva(const Matrix<T>& L, const std::vector<T>& N, const std::vector<T>& Z,
91 const Matrix<T>& mu, const Matrix<T>& Q0, double tol = 1e-6,
92 std::size_t maxiter = 10000) {
94 "pfqn_qdamva requires transcendental arithmetic");
95 const std::size_t M = static_cast<std::size_t>(L.rows());
96 const std::size_t K = static_cast<std::size_t>(L.cols());
97 if (N.size() != K)
98 throw InputError("pfqn_qdamva: the population vector must have one entry per class");
99 if (!Z.empty() && Z.size() != K)
100 throw InputError("pfqn_qdamva: the think-time vector must have one entry per class");
101
102 const T zero = num_traits<T>::from_int(0);
103 const T one = num_traits<T>::from_int(1);
104
105 QdAmvaResult<T> out;
106 out.Q = Matrix<T>(M, K, zero);
107 out.X = Matrix<T>(1, K, zero);
108 out.U = Matrix<T>(M, K, zero);
109 out.R = Matrix<T>(M, K, zero);
110
111 T Ntot = zero;
112 for (std::size_t r = 0; r < K; ++r) Ntot += N[r];
113 // delta is undefined on an empty population, and the reference returns the
114 // zero queue rather than dividing by it.
115 if (!(num_traits<T>::to_double(Ntot) > 0.0)) return out;
116
117 if (Q0.rows() == static_cast<int>(M) && Q0.cols() == static_cast<int>(K)) {
118 out.Q = Q0;
119 } else {
120 // Ltot = 0 for a class with no demand anywhere: L/Ltot is a NaN the
121 // iteration never recovers from. Such a column arises routinely in a
122 // layered fixed point, where a caller can start with no work at the
123 // layer station, so the column is left at zero instead.
124 for (std::size_t r = 0; r < K; ++r) {
125 T tot = zero;
126 for (std::size_t k = 0; k < M; ++k) tot += L(k, r);
127 if (!(num_traits<T>::to_double(tot) > 0.0)) continue;
128 for (std::size_t k = 0; k < M; ++k) out.Q(k, r) = T(L(k, r) / tot * N[r]);
129 }
130 }
131
132 const T delta = T((Ntot - one) / Ntot);
133 // Q*10 as the sentinel, as the reference notes, stalls on an all-zero seed:
134 // the loop would exit before its first pass. Offset instead.
135 Matrix<T> Qprev = out.Q;
136 for (std::size_t k = 0; k < M; ++k)
137 for (std::size_t r = 0; r < K; ++r)
138 Qprev(k, r) = T(out.Q(k, r) + num_traits<T>::from_double(10.0 * (1.0 + tol)));
139
140 std::vector<T> Ak(M, zero);
141 while (out.iter < maxiter) {
142 double gap = 0.0;
143 for (std::size_t k = 0; k < M; ++k)
144 for (std::size_t r = 0; r < K; ++r)
145 gap = std::max(gap, std::fabs(num_traits<T>::to_double(out.Q(k, r)) -
146 num_traits<T>::to_double(Qprev(k, r))));
147 if (out.iter > 0 && gap <= tol) break;
148 ++out.iter;
149 Qprev = out.Q;
150
151 // The arrival-instant total queue length, class independent.
152 for (std::size_t k = 0; k < M; ++k) {
153 T rowsum = zero;
154 for (std::size_t r = 0; r < K; ++r) rowsum += out.Q(k, r);
155 Ak[k] = T(one + delta * rowsum);
156 }
157 const std::vector<T> g = pfqn_lldfun(Ak, mu, std::vector<double>());
158
159 for (std::size_t r = 0; r < K; ++r) {
160 for (std::size_t k = 0; k < M; ++k) {
161 T rowsum = zero;
162 for (std::size_t s = 0; s < K; ++s) rowsum += out.Q(k, s);
163 out.R(k, r) = T(L(k, r) * g[k] * (one + delta * rowsum));
164 }
165 T denom = Z.empty() ? zero : Z[r];
166 for (std::size_t k = 0; k < M; ++k) denom += out.R(k, r);
167 out.X(0, r) = (num_traits<T>::to_double(denom) > 0.0) ? T(N[r] / denom) : zero;
168 for (std::size_t k = 0; k < M; ++k) {
169 out.Q(k, r) = T(out.X(0, r) * out.R(k, r));
170 out.U(k, r) = T(L(k, r) * g[k] * out.X(0, r));
171 }
172 }
173 }
174 return out;
175}
176
177} // namespace pfqn
178} // namespace line
179
180#endif // LINE_API_PFQN_QDAMVA_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.
QdAmvaResult< T > pfqn_qdamva(const Matrix< T > &L, const std::vector< T > &N, const std::vector< T > &Z, const Matrix< T > &mu, const Matrix< T > &Q0, double tol=1e-6, std::size_t maxiter=10000)
QD-AMVA: queue-dependent approximate mean value analysis.
Definition pfqn_qdamva.h:90
std::vector< T > pfqn_lldfun(const std::vector< T > &n, const Matrix< T > &lldscaling, const std::vector< double > &nservers)
AMVA-QD limited-load-dependence function.
Definition pfqn_lldfun.h:81
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
Number-type abstraction for the templated API port.
AMVA-QD limited-load-dependence function.
What pfqn_qdamva returns: the fixed point and how it was reached.
Definition pfqn_qdamva.h:70
Matrix< T > U
(M x R) per-class utilizations, carrying the g scaling
Definition pfqn_qdamva.h:73
Matrix< T > X
(1 x R) per-class throughputs
Definition pfqn_qdamva.h:72
Matrix< T > Q
(M x R) mean queue lengths
Definition pfqn_qdamva.h:71
Matrix< T > R
(M x R) per-class residence times, Q = X .* R
Definition pfqn_qdamva.h:74