LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
npfqn_bnd_bgt.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_NPFQN_NPFQN_BND_BGT_H
6#define LINE_API_NPFQN_NPFQN_BND_BGT_H
7
8/**
9 * @file
10 * @ingroup api_npfqn
11 * Piecewise-linear Lyapunov UPPER bound on the steady-state queue lengths of a
12 * multitype (deterministic-routing) multiclass Markovian queueing network,
13 * valid for EVERY work-conserving Markovian policy.
14 *
15 * Templated port of matlab/src/api/npfqn/npfqn_bnd_bgt.m, cross-checked against
16 * jar/src/main/java/jline/api/npfqn/Npfqn_bnd_bgt.java.
17 *
18 * MODEL. J single-server stations; I customer types; type i arrives as a
19 * Poisson stream of rate lambda(i) and passes through stages k = 0..Ji-1, stage
20 * k being served at station sigma[i][k] at exponential rate mu[i][k]. Class
21 * (i,k) is the buffer of type i at stage k; N = sum_i Ji is the class count.
22 *
23 * METHOD. Solve the Down-Meyn global-stability linear program GLP[dm], eq.
24 * (25)-(28) of the reference, in the piecewise-linear Lyapunov function
25 * Phi(x) = max_j L^j'x:
26 *
27 * L^j(i,1) lambda_i + mu(i,k) (L^j(i,k+1) - L^j(i,k)) + V_j <= -gamma
28 * for (i,k) in station j
29 * mu(i,k) (L^j(i,k+1) - L^j(i,k)) <= V_j for (i,k) not in j
30 * (1/(J-1)) sum_{j' != j} L^j'(i,k) >= L^j(i,k) for (i,k) not in j
31 * L, V, gamma >= 0
32 *
33 * with L^j(i,Ji+1) = 0. A feasible solution with gamma > 0 certifies that EVERY
34 * work-conserving policy is stable, and a smoothed Phi is then a Lyapunov
35 * function with drift gamma/4 and an explicit exception parameter, giving the
36 * reference's Theorem 4 bound
37 *
38 * E[L^j'Q] <= 16 N J^2 (J-1) (Lmax+gamma)^3/gamma^2
39 * + 8 (Lmax + gamma/2)^2/gamma =: U
40 *
41 * for every j, whence E[Q(i,k)] <= U / max_j L^j(i,k).
42 *
43 * THE RATES ARE RESCALED so that sum_i lambda_i + sum_{i,k} mu(i,k) = 1, the
44 * uniformization the reference imposes before Theorem 4. Queue lengths are
45 * counts and are unaffected by the time scale.
46 *
47 * NORMALIZATION, WHICH THE REFERENCE LEAVES OPEN. GLP[dm] is homogeneous and so
48 * is the bound, so this routine fixes L^j(i,k) <= 1 and MAXIMIZES gamma, then
49 * breaks ties among gamma-optimal solutions by maximizing sum L: a degenerate
50 * optimum can otherwise zero some L^j(i,k) and report an infinite bound for a
51 * class for no reason.
52 *
53 * THE BOUND IS LOOSE, and knowingly so: the exception parameter carries
54 * (Lmax+gamma)^3/gamma^2 and dominates as soon as J > 1. What is sharp is the
55 * STABILITY CERTIFICATE gamma > 0 and the geometric tail RATE.
56 *
57 * ARITHMETIC. Rational-clean: the LP data and the bound are polynomial in the
58 * rates and the dense simplex is exact.
59 *
60 * Reference: D. Bertsimas, D. Gamarnik, J. N. Tsitsiklis (2001). Performance of
61 * multiclass Markovian queueing networks via piecewise linear Lyapunov
62 * functions. Annals of Applied Probability 11(4), 1384-1428, Section 5.1
63 * (GLP[dm] of Down and Meyn 1997, and Theorem 4).
64 */
65
66#include <cstddef>
67#include <string>
68#include <vector>
69
70#include "line/num/number.h"
71#include "line/util/error.h"
72#include "line/util/lp_highs.h"
73#include "line/util/simplex.h"
74
75namespace line {
76namespace npfqn {
77
78template <class T>
79struct BndBgt {
80 std::vector<std::vector<T> > Qub; ///< per type and stage, the bound on E[Q(i,k)]
81 std::vector<bool> finite; ///< false where max_j L is 0 and Qub is meaningless
82 T gamma = T(); ///< the drift certificate, strictly positive on success
83 T Lmax = T(); ///< max over j and (i,k) of L
84 std::vector<std::vector<T> > L; ///< the Lyapunov coefficients, [J][N]
85 std::vector<T> V; ///< the per-station slack
86 T B = T(); ///< exception parameter of the smoothed function
87 T U = T(); ///< the Theorem 4 bound on E[L^j'Q]
88 T tailRatio = T(); ///< geometric decay ratio of the tail bound
89 T tailStep = T(); ///< step of the tail bound, 2(Lmax+gamma/2)
90 std::vector<T> rho; ///< per-class nominal load
91 std::vector<T> rhoStation; ///< per-station nominal load
92 T scale = T(); ///< the uniformization divisor
93 std::vector<std::size_t> classType, classStage, classStation;
94};
95
96/**
97 * @brief Piecewise-linear Lyapunov UPPER bound on the steady-state queue
98 * lengths of a multitype (deterministic-routing) multiclass Markovian
99 * queueing network, valid for EVERY work-conserving Markovian policy.
100 *
101 * @param lambda Poisson arrival rate of each type
102 * @param mu mu[i][k] = service rate of stage k of type i
103 * @param sigma sigma[i][k] = zero-based station of stage k of type i
104 * @param J number of stations
105 */
106template <class T>
107BndBgt<T> npfqn_bnd_bgt(const std::vector<T>& lambda, const std::vector<std::vector<T> >& mu,
108 const std::vector<std::vector<std::size_t> >& sigma, std::size_t J) {
109 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
110 const T two = num_traits<T>::from_int(2), four = num_traits<T>::from_int(4);
111 const std::size_t I = lambda.size();
112 if (mu.size() != I || sigma.size() != I)
113 throw InputError("npfqn_bnd_bgt: mu and sigma must have one entry per type");
114
115 // ---- flatten (i,k) into a class index ----
116 BndBgt<T> out;
117 std::vector<T> muc;
118 std::vector<std::size_t> firstOf(I, 0), nextOf;
119 for (std::size_t i = 0; i < I; ++i) {
120 if (mu[i].size() != sigma[i].size())
121 throw InputError("npfqn_bnd_bgt: mu and sigma disagree on the stage count of a type");
122 if (mu[i].empty()) throw InputError("npfqn_bnd_bgt: a type has no stage");
123 if (!(lambda[i] > zero))
124 throw InputError("npfqn_bnd_bgt: every type needs a strictly positive arrival rate");
125 firstOf[i] = muc.size();
126 for (std::size_t k = 0; k < mu[i].size(); ++k) {
127 out.classType.push_back(i);
128 out.classStage.push_back(k);
129 out.classStation.push_back(sigma[i][k]);
130 muc.push_back(mu[i][k]);
131 }
132 }
133 const std::size_t N = muc.size();
134 for (std::size_t c = 0; c < N; ++c)
135 if (!(muc[c] > zero))
136 throw InputError("npfqn_bnd_bgt: every stage needs a strictly positive service rate");
137 nextOf.assign(N, N); // N means "leaves the network"
138 for (std::size_t c = 0; c + 1 < N; ++c)
139 if (out.classType[c + 1] == out.classType[c]) nextOf[c] = c + 1;
140
141 // ---- loads ----
142 out.rho.assign(N, zero);
143 out.rhoStation.assign(J, zero);
144 for (std::size_t c = 0; c < N; ++c) {
145 out.rho[c] = T(lambda[out.classType[c]] / muc[c]);
146 out.rhoStation[out.classStation[c]] = T(out.rhoStation[out.classStation[c]] + out.rho[c]);
147 }
148 for (std::size_t j = 0; j < J; ++j)
149 if (!(out.rhoStation[j] < one))
150 throw UnsupportedError("npfqn_bnd_bgt: station " + std::to_string(j + 1) +
151 " is saturated: the load condition of the reference fails");
152
153 // ---- uniformization ----
154 T scale = zero;
155 for (std::size_t i = 0; i < I; ++i) scale = T(scale + lambda[i]);
156 for (std::size_t c = 0; c < N; ++c) scale = T(scale + muc[c]);
157 out.scale = scale;
158 std::vector<T> lam(I, zero), mus(N, zero);
159 for (std::size_t i = 0; i < I; ++i) lam[i] = T(lambda[i] / scale);
160 for (std::size_t c = 0; c < N; ++c) mus[c] = T(muc[c] / scale);
161
162 // ---- LP layout: L(j,c) -> j*N + c ; V(j) -> J*N + j ; gamma -> J*N+J ----
163 const std::size_t oV = J * N, ig = J * N + J, nv = J * N + J + 1;
164 lp::LpModel<T> m(nv);
165 m.set_maximize(true);
166 for (std::size_t v = 0; v < J * N; ++v) m.set_bounds(v, zero, one);
167
168 for (std::size_t j = 0; j < J; ++j) {
169 for (std::size_t c = 0; c < N; ++c) {
170 m.row_clear();
171 if (out.classStation[c] == j) {
172 m.row_add(j * N + firstOf[out.classType[c]], lam[out.classType[c]]);
173 m.row_add(j * N + c, T(-mus[c]));
174 if (nextOf[c] < N) m.row_add(j * N + nextOf[c], mus[c]);
175 m.row_add(oV + j, one);
176 m.row_add(ig, one);
177 m.emit(lp::LpSense::LE, zero);
178 } else {
179 m.row_add(j * N + c, T(-mus[c]));
180 if (nextOf[c] < N) m.row_add(j * N + nextOf[c], mus[c]);
181 m.row_add(oV + j, T(-one));
182 m.emit(lp::LpSense::LE, zero);
183 if (J > 1) {
184 m.row_clear();
185 m.row_add(j * N + c, one);
186 const T w = T(-(one / num_traits<T>::from_int(static_cast<int>(J - 1))));
187 for (std::size_t jp = 0; jp < J; ++jp)
188 if (jp != j) m.row_add(jp * N + c, w);
189 m.emit(lp::LpSense::LE, zero);
190 }
191 }
192 }
193 }
194
195 m.set_cost(ig, one);
197 if (!s.ok())
198 throw UnsupportedError(std::string("npfqn_bnd_bgt: GLP[dm] did not solve to optimality (") +
199 lp::lp_status_name(s.status) + ")");
200 T gamma = s.objective;
201 if (!(gamma > zero))
202 throw UnsupportedError(
203 "npfqn_bnd_bgt: GLP[dm] has no solution with gamma > 0: this network is not certified "
204 "globally stable, so no finite piecewise-linear Lyapunov bound exists");
205
206 // Tie-break among gamma-optimal solutions: maximize sum L, so a degenerate
207 // vertex does not report an infinite bound for a class it zeroed arbitrarily.
208 {
209 lp::LpModel<T> m2 = m;
210 m2.row_clear();
211 m2.row_add(ig, one);
212 m2.emit(lp::LpSense::GE, gamma);
213 m2.set_cost(ig, zero);
214 for (std::size_t v = 0; v < J * N; ++v) m2.set_cost(v, one);
215 const lp::LpSolution<T> s2 = lp::lp_solve(m2);
216 if (s2.ok()) {
217 s = s2;
218 gamma = s.x[ig];
219 }
220 }
221
222 out.gamma = gamma;
223 out.L.assign(J, std::vector<T>(N, zero));
224 out.Lmax = zero;
225 for (std::size_t j = 0; j < J; ++j)
226 for (std::size_t c = 0; c < N; ++c) {
227 out.L[j][c] = s.x[j * N + c];
228 if (out.L[j][c] > out.Lmax) out.Lmax = out.L[j][c];
229 }
230 out.V.assign(J, zero);
231 for (std::size_t j = 0; j < J; ++j) out.V[j] = s.x[oV + j];
232
233 const T Lg = T(out.Lmax + gamma);
234 const T Lh = T(out.Lmax + gamma / two);
235 out.B = T(num_traits<T>::from_int(16) * num_traits<T>::from_int(static_cast<int>(N)) *
236 num_traits<T>::from_int(static_cast<int>(J * J)) *
237 num_traits<T>::from_int(static_cast<int>(J - 1)) * Lg * Lg * Lg / (gamma * gamma));
238 out.U = T(out.B + num_traits<T>::from_int(8) * Lh * Lh / gamma);
239 out.tailStep = T(two * Lh);
240 out.tailRatio = T(Lh / (out.Lmax + num_traits<T>::from_int(3) * gamma / four));
241
242 out.Qub.assign(I, std::vector<T>());
243 out.finite.assign(N, true);
244 for (std::size_t i = 0; i < I; ++i) out.Qub[i].assign(mu[i].size(), zero);
245 for (std::size_t c = 0; c < N; ++c) {
246 T best = zero;
247 for (std::size_t j = 0; j < J; ++j)
248 if (out.L[j][c] > best) best = out.L[j][c];
249 if (best > zero) {
250 out.Qub[out.classType[c]][out.classStage[c]] = T(out.U / best);
251 } else {
252 out.finite[c] = false;
253 out.Qub[out.classType[c]][out.classStage[c]] = zero;
254 }
255 }
256 return out;
257}
258
259} // namespace npfqn
260} // namespace line
261
262#endif // LINE_API_NPFQN_NPFQN_BND_BGT_H
InputError(const std::string &what)
Definition error.h:39
UnsupportedError(const std::string &what)
Definition error.h:51
Sparse LP in the natural form, with per-variable bounds.
Definition simplex.h:112
void set_maximize(bool m)
true to maximize c'x (the default), false to minimize.
Definition simplex.h:174
void emit(LpSense sense, const T &rhs)
Emit the accumulated row with the given relation and right-hand side.
Definition simplex.h:200
void set_cost(std::size_t j, const T &v)
Definition simplex.h:164
void row_clear()
Discard whatever the row accumulator holds.
Definition simplex.h:179
void set_bounds(std::size_t j, const T &lo, const T &hi)
Definition simplex.h:139
void row_add(std::size_t j, const T &v)
row(j) += v, the accumulation the MATLAB reference performs.
Definition simplex.h:188
The exception types the port throws.
A sparse LP backend for line::lp::LpModel, on HiGHS (MIT).
LpSolution< T > lp_solve(const LpModel< T > &model, std::size_t dense_max_cols=512)
Solve, choosing the backend by arithmetic and size.
Definition lp_highs.h:176
const char * lp_status_name(LpStatus s)
Definition simplex.h:84
BndBgt< T > npfqn_bnd_bgt(const std::vector< T > &lambda, const std::vector< std::vector< T > > &mu, const std::vector< std::vector< std::size_t > > &sigma, std::size_t J)
Piecewise-linear Lyapunov UPPER bound on the steady-state queue lengths of a multitype (deterministic...
Number-type abstraction for the templated API port.
Templated primal simplex with Bland's rule.
T objective
c'x, in the sense requested (max or min)
Definition simplex.h:97
bool ok() const
Definition simplex.h:99
std::vector< T > x
primal solution in the ORIGINAL variable space
Definition simplex.h:96
std::vector< T > rho
per-class nominal load
std::vector< T > rhoStation
per-station nominal load
std::vector< std::vector< T > > L
the Lyapunov coefficients, [J][N]
std::vector< bool > finite
false where max_j L is 0 and Qub is meaningless
std::vector< std::size_t > classType
std::vector< std::size_t > classStage
T tailRatio
geometric decay ratio of the tail bound
T tailStep
step of the tail bound, 2(Lmax+gamma/2)
T U
the Theorem 4 bound on E[L^j'Q]
T gamma
the drift certificate, strictly positive on success
T Lmax
max over j and (i,k) of L
T B
exception parameter of the smoothed function
std::vector< std::size_t > classStation
std::vector< std::vector< T > > Qub
per type and stage, the bound on E[Q(i,k)]
std::vector< T > V
the per-station slack
T scale
the uniformization divisor