LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
mapqn_bnd_lr_mva.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_MAPQN_MAPQN_BND_LR_MVA_H
6#define LINE_API_MAPQN_MAPQN_BND_LR_MVA_H
7
8/**
9 * @file
10 * @ingroup api_mapqn
11 * MVA-shaped linear-reduction bound for a closed network of M - 1 exponential
12 * queues and ONE MAP queue.
13 *
14 * Templated port of matlab/lib/qrf/mapqn_bnd_lr_mva.m (ground truth),
15 * cross-checked against python/line_solver/api/mapqn/bnd_lr_mva.py.
16 *
17 * A DIFFERENT MODEL, NOT A VARIANT. mapqn_bnd_lr relaxes onto the marginal
18 * probabilities p1(j,k,i,ni,h) and reads utilizations out of them. This one
19 * never introduces a probability variable at all: it works directly on the
20 * mean-value quantities UN(i,k), QN(i,k) and the conditional length B(j,k,i),
21 * in the manner of an MVA recursion turned into a relaxation. The consequence
22 * is a model of 2 M K + M^2 K columns instead of one quadratic in (N+1) sum_i
23 * K(i), so it is by far the cheapest bound in the family and the only one that
24 * stays small as the population grows. Nothing here is shared with
25 * mapqn_p1_common.h, and it deliberately does not use MapqnParams: the network
26 * shape is different (see LrMvaParams).
27 *
28 * NETWORK SHAPE. Queues 1..M-1 are exponential with scalar rates muM(i), and
29 * queue M is the MAP, whose K levels are the phase process. That asymmetry is
30 * carried entirely by q(i,j,k,h) below and by nothing else, which is why the
31 * families read uniformly over i even though the stations are not alike.
32 *
33 * THE FAILURE MODE OF THIS FAMILY IS A VACUOUS BOUND, NOT A CRASH. FLOW, UBAL,
34 * QBAL, MCC and MCC2 are the families that read the rates; the rest are
35 * structural. Drop the rate-bearing ones and UN = 0 stays feasible while every
36 * subscript remains internally consistent. Each family is emitted by a function
37 * named after the reference's own numbered section, so the inventory is
38 * diffable; do not inline them.
39 *
40 * ARITHMETIC. Assembly is +, -, * on the model data and lp::simplex_solve uses
41 * Bland's rule with no tolerance, so at T = line::Rational the returned bound
42 * is the EXACT optimum of the exact polytope. The MATLAB reference defaults to
43 * linprog's 'interior-point-legacy' here, which on these badly scaled instances
44 * is markedly more accurate than plain 'interior-point'.
45 */
46
47#include <cstddef>
48#include <string>
49#include <vector>
50
52#include "line/num/number.h"
53#include "line/util/error.h"
54#include "line/util/matrix.h"
55#include "line/util/simplex.h"
56
57namespace line {
58namespace mapqn {
59
60/**
61 * Parameters of the MVA-shaped LR bound, mirroring the reference's `params`.
62 *
63 * Queues are 0-based here, so the MAP queue is index M - 1 and muM carries the
64 * M - 1 exponential rates of queues 0..M-2. K is a scalar: it is the level
65 * count of the single MAP, not a per-queue vector.
66 */
67template <class T>
69 int M = 0; ///< number of queues, the last of which is the MAP
70 int N = 0; ///< closed population
71 int K = 0; ///< number of levels of the MAP queue
72 std::vector<T> muM; ///< (M-1) service rates of the exponential queues
73 Matrix<T> muMAP; ///< (K x K) completion rates of the MAP queue
74 Matrix<T> v; ///< (K x K) level-change rates of the MAP queue
75 Matrix<T> r; ///< (M x M) routing probabilities
76
77 void validate() const {
78 if (M <= 1) throw InputError("mapqn_bnd_lr_mva: M must be at least 2");
79 if (N < 1) throw InputError("mapqn_bnd_lr_mva: N must be at least 1");
80 if (K < 1) throw InputError("mapqn_bnd_lr_mva: K must be at least 1");
81 if (static_cast<int>(muM.size()) != M - 1)
82 throw InputError("mapqn_bnd_lr_mva: muM must have M-1 entries");
83 const std::size_t k = static_cast<std::size_t>(K);
84 if (muMAP.rows() != k || muMAP.cols() != k)
85 throw InputError("mapqn_bnd_lr_mva: muMAP must be K x K");
86 if (v.rows() != k || v.cols() != k)
87 throw InputError("mapqn_bnd_lr_mva: v must be K x K");
88 if (r.rows() != static_cast<std::size_t>(M) || r.cols() != static_cast<std::size_t>(M))
89 throw InputError("mapqn_bnd_lr_mva: r must be M x M");
90 }
91};
92
93/**
94 * The variable family the objective is taken over.
95 *
96 * The paper states its bounds on the AGGREGATE over levels: U_i(N) = sum_k
97 * U_i^k(N) is the utilization of station i, while U_i^k alone is its
98 * utilization while the MAP sits in phase k. Pass objective_level = -1 for
99 * that aggregate; optimizing the K terms separately and adding them is also a
100 * bound but a strictly looser one, since the phases cannot all peak at once.
101 */
102enum class MapqnObjectiveVar { UN, QN };
103
104/** Result of an MVA-shaped LR bound solve. */
105template <class T>
107 bool ok = false; ///< the LP reached an optimal vertex
108 std::string status; ///< textual LP status
109 T objective = T(); ///< the bound on UN(objective_queue, objective_level)
110 Matrix<T> UN; ///< M x K utilizations
111 Matrix<T> QN; ///< M x K queue lengths
112 std::vector<T> x; ///< full solution vector, indexed by LrMvaIndex
113 std::size_t num_vars = 0;
114 std::size_t num_rows = 0;
115 std::size_t iterations = 0;
116};
117
118/** Variable layout: UN, then QN, then B, in the reference's declaration order. */
120 int M = 0, K = 0;
121 std::size_t off_UN = 0, off_QN = 0, off_B = 0, total = 0;
122
124 LrMvaIndex(int m, int k) : M(m), K(k) {
125 const std::size_t mk = static_cast<std::size_t>(M) * static_cast<std::size_t>(K);
126 off_UN = 0;
127 off_QN = mk;
128 off_B = 2 * mk;
129 total = off_B + mk * static_cast<std::size_t>(M);
130 }
131 std::size_t UN(int i, int k) const {
132 return off_UN + static_cast<std::size_t>(i) * static_cast<std::size_t>(K) +
133 static_cast<std::size_t>(k);
134 }
135 std::size_t QN(int i, int k) const {
136 return off_QN + static_cast<std::size_t>(i) * static_cast<std::size_t>(K) +
137 static_cast<std::size_t>(k);
138 }
139 std::size_t B(int j, int k, int i) const {
140 return off_B +
141 (static_cast<std::size_t>(j) * static_cast<std::size_t>(K) +
142 static_cast<std::size_t>(k)) *
143 static_cast<std::size_t>(M) +
144 static_cast<std::size_t>(i);
145 }
146 std::size_t num_vars() const { return total; }
147};
148
149namespace detail {
150
151/**
152 * q(i,j,k,h): rate at which queue i at level k routes a job to queue j and the
153 * level becomes h.
154 *
155 * Port of the nested q of mapqn_bnd_lr_mva.m. The two branches are the whole of
156 * the network's asymmetry:
157 * i < M-1 an exponential queue, which cannot change the level, so the rate
158 * is r(i,j) mu(i) on the diagonal k == h and zero off it;
159 * i == M-1 the MAP. Routing away (j < M-1) carries muMAP(k,h). Routing to
160 * itself carries v(k,h) + r(M-1,M-1) muMAP(k,h) and is zero at
161 * k == h, since a self-loop that changes nothing is not an event.
162 */
163template <class T>
164T lr_mva_rate(const LrMvaParams<T>& p, int i, int j, int k, int h) {
165 const std::size_t ki = static_cast<std::size_t>(k), hi = static_cast<std::size_t>(h);
166 const int last = p.M - 1;
167 if (i < last) {
168 if (k != h) return T();
169 return T(p.r(static_cast<std::size_t>(i), static_cast<std::size_t>(j)) *
170 p.muM[static_cast<std::size_t>(i)]);
171 }
172 if (j < last)
173 return T(p.r(static_cast<std::size_t>(last), static_cast<std::size_t>(j)) *
174 p.muMAP(ki, hi));
175 if (k == h) return T();
176 return T(p.v(ki, hi) + p.r(static_cast<std::size_t>(last), static_cast<std::size_t>(last)) *
177 p.muMAP(ki, hi));
178}
179
180/** Variable bounds: 0 <= UN <= 1, 0 <= QN <= N, 0 <= B <= N. */
181template <class T>
182void lr_mva_bounds(const LrMvaParams<T>& p, const LrMvaIndex& x, lp::LpModel<T>& m) {
183 const T one = num_traits<T>::from_int(1);
184 const T nn = num_traits<T>::from_int(p.N);
185 for (int i = 0; i < p.M; ++i) {
186 for (int k = 0; k < p.K; ++k) {
187 m.set_bounds(x.UN(i, k), T(), one);
188 m.set_bounds(x.QN(i, k), T(), nn);
189 for (int j = 0; j < p.M; ++j) m.set_bounds(x.B(i, k, j), T(), nn);
190 }
191 }
192}
193
194/** 1. QNB: B(j,k,i) <= QN(i,k). A conditional length cannot exceed the mean. */
195template <class T>
196void lr_mva_qnb(const LrMvaParams<T>& p, const LrMvaIndex& x, lp::LpModel<T>& m) {
197 for (int i = 0; i < p.M; ++i) {
198 for (int k = 0; k < p.K; ++k) {
199 for (int j = 0; j < p.M; ++j) {
200 m.row_add_int(x.QN(i, k), -1);
201 m.row_add_int(x.B(j, k, i), 1);
202 m.emit_le_int(0);
203 }
204 }
205 }
206}
207
208/** 2. UMAX: a station is busy at one level at a time. */
209template <class T>
210void lr_mva_umax(const LrMvaParams<T>& p, const LrMvaIndex& x, lp::LpModel<T>& m) {
211 for (int i = 0; i < p.M; ++i) {
212 for (int k = 0; k < p.K; ++k) m.row_add_int(x.UN(i, k), 1);
213 m.emit_le_int(1);
214 }
215}
216
217/** 3. POPCONSTR: the mean lengths carry the whole closed population. */
218template <class T>
219void lr_mva_popconstr(const LrMvaParams<T>& p, const LrMvaIndex& x, lp::LpModel<T>& m) {
220 for (int i = 0; i < p.M; ++i)
221 for (int k = 0; k < p.K; ++k) m.row_add_int(x.QN(i, k), 1);
222 m.emit_eq_int(p.N);
223}
224
225/** 4. FLOW: rate into station i equals rate out of it. */
226template <class T>
227void lr_mva_flow(const LrMvaParams<T>& p, const LrMvaIndex& x, lp::LpModel<T>& m) {
228 for (int i = 0; i < p.M; ++i) {
229 for (int k = 0; k < p.K; ++k) {
230 for (int mm = 0; mm < p.K; ++mm) {
231 for (int w = 0; w < p.M; ++w) {
232 m.row_add(x.UN(w, k), lr_mva_rate(p, w, i, k, mm));
233 m.row_add(x.UN(i, mm), T(-lr_mva_rate(p, i, w, mm, k)));
234 }
235 }
236 }
237 m.emit_eq_int(0);
238 }
239}
240
241/** 5. UBAL: level balance of the MAP queue's utilization. */
242template <class T>
243void lr_mva_ubal(const LrMvaParams<T>& p, const LrMvaIndex& x, lp::LpModel<T>& m) {
244 const int last = p.M - 1;
245 for (int k = 0; k < p.K; ++k) {
246 for (int h = 0; h < p.K; ++h) {
247 if (h == k) continue;
248 for (int w = 0; w < p.M; ++w) {
249 m.row_add(x.UN(last, k), lr_mva_rate(p, last, w, k, h));
250 m.row_add(x.UN(last, h), T(-lr_mva_rate(p, last, w, h, k)));
251 }
252 }
253 m.emit_eq_int(0);
254 }
255}
256
257/** 6. QBAL: level balance of the MAP queue's mean length. */
258template <class T>
259void lr_mva_qbal(const LrMvaParams<T>& p, const LrMvaIndex& x, lp::LpModel<T>& m) {
260 const int last = p.M - 1;
261 for (int k = 0; k < p.K; ++k) {
262 for (int h = 0; h < p.K; ++h) {
263 if (h == k) continue;
264 for (int w = 0; w < p.M; ++w) m.row_add(x.QN(last, k), lr_mva_rate(p, last, w, k, h));
265 }
266 for (int mm = 0; mm < p.K; ++mm)
267 for (int j = 0; j < last; ++j)
268 m.row_add(x.UN(last, mm), lr_mva_rate(p, last, j, mm, k));
269 for (int j = 0; j < last; ++j)
270 m.row_add(x.UN(j, k), T(-lr_mva_rate(p, j, last, k, k)));
271 for (int h = 0; h < p.K; ++h) {
272 if (h == k) continue;
273 for (int w = 0; w < p.M; ++w)
274 m.row_add(x.QN(last, h), T(-lr_mva_rate(p, last, w, h, k)));
275 }
276 m.emit_eq_int(0);
277 }
278}
279
280/** 7. MCC: the marginal-consistency cut, in its (N+1)-weighted form. */
281template <class T>
282void lr_mva_mcc(const LrMvaParams<T>& p, const LrMvaIndex& x, lp::LpModel<T>& m) {
283 const T np1 = num_traits<T>::from_int(p.N + 1);
284 for (int i = 0; i < p.M; ++i) {
285 for (int k = 0; k < p.K; ++k) {
286 for (int mm = 0; mm < p.K; ++mm) {
287 for (int w = 0; w < p.M; ++w) {
288 if (w == i) continue;
289 m.row_add(x.QN(i, k), lr_mva_rate(p, i, w, k, mm));
290 }
291 for (int j = 0; j < p.M; ++j) {
292 if (j == i) continue;
293 const T q = lr_mva_rate(p, j, i, k, mm);
294 m.row_add(x.QN(j, k), q);
295 for (int wp = 0; wp < p.M; ++wp) {
296 if (wp == i || wp == j) continue;
297 m.row_add(x.B(j, k, wp), q);
298 }
299 m.row_add(x.UN(j, k), T(-(np1 * q)));
300 }
301 }
302 }
303 m.emit_eq_int(0);
304 }
305}
306
307/** 8. MCC2: the same cut with the conditional length taken at i itself. */
308template <class T>
309void lr_mva_mcc2(const LrMvaParams<T>& p, const LrMvaIndex& x, lp::LpModel<T>& m) {
310 for (int i = 0; i < p.M; ++i) {
311 for (int k = 0; k < p.K; ++k) {
312 for (int mm = 0; mm < p.K; ++mm) {
313 for (int w = 0; w < p.M; ++w) {
314 if (w == i) continue;
315 m.row_add(x.QN(i, k), lr_mva_rate(p, i, w, k, mm));
316 }
317 for (int j = 0; j < p.M; ++j) {
318 if (j == i) continue;
319 const T q = lr_mva_rate(p, j, i, k, mm);
320 m.row_add(x.B(j, k, i), T(-q));
321 m.row_add(x.UN(j, k), T(-q));
322 }
323 }
324 }
325 m.emit_eq_int(0);
326 }
327}
328
329/** 9. QMAX: QN(w,k) <= N UN(w,k). An idle level holds no jobs. */
330template <class T>
331void lr_mva_qmax(const LrMvaParams<T>& p, const LrMvaIndex& x, lp::LpModel<T>& m) {
332 for (int w = 0; w < p.M; ++w) {
333 for (int k = 0; k < p.K; ++k) {
334 m.row_add_int(x.QN(w, k), 1);
335 m.row_add_int(x.UN(w, k), -p.N);
336 m.emit_le_int(0);
337 }
338 }
339}
340
341/** 10. QMIN: N UN(j,k) <= sum_w QN(w,k), for every witness j. */
342template <class T>
343void lr_mva_qmin(const LrMvaParams<T>& p, const LrMvaIndex& x, lp::LpModel<T>& m) {
344 for (int k = 0; k < p.K; ++k) {
345 for (int j = 0; j < p.M; ++j) {
346 for (int w = 0; w < p.M; ++w) m.row_add_int(x.QN(w, k), -1);
347 m.row_add_int(x.UN(j, k), p.N);
348 m.emit_le_int(0);
349 }
350 }
351}
352
353} // namespace detail
354
355/**
356 * Bound UN or QN at (objective_queue, objective_level) over the MVA-shaped LR
357 * polytope.
358 *
359 * @param p network parameters; queues and levels are 0-based, and
360 * the MAP queue is index M - 1
361 * @param objective_queue queue index, 0..M-1
362 * @param objective_level level index, 0..K-1, or -1 for the SUM over levels
363 * @param sense Max for an upper bound, Min for a lower bound
364 * @param objective_var UN (default) or QN, the variable family optimized over
365 */
366template <class T>
368 int objective_level,
371 p.validate();
372 if (objective_queue < 0 || objective_queue >= p.M)
373 throw InputError("mapqn_bnd_lr_mva: objective_queue out of range");
374 if (objective_level < -1 || objective_level >= p.K)
375 throw InputError("mapqn_bnd_lr_mva: objective_level out of range");
376
377 const LrMvaIndex x(p.M, p.K);
379 detail::lr_mva_bounds(p, x, m);
380
381 // Families, in the reference's numbered order. Named one per function so
382 // the inventory is diffable against mapqn_bnd_lr_mva.m.
383 detail::lr_mva_qnb(p, x, m);
384 detail::lr_mva_umax(p, x, m);
385 detail::lr_mva_popconstr(p, x, m);
386 detail::lr_mva_flow(p, x, m);
387 detail::lr_mva_ubal(p, x, m);
388 detail::lr_mva_qbal(p, x, m);
389 detail::lr_mva_mcc(p, x, m);
390 detail::lr_mva_mcc2(p, x, m);
391 detail::lr_mva_qmax(p, x, m);
392 detail::lr_mva_qmin(p, x, m);
393
394 // One level, or their sum when objective_level is -1.
395 const int first_level = (objective_level < 0) ? 0 : objective_level;
396 const int last_level = (objective_level < 0) ? p.K - 1 : objective_level;
397 for (int k = first_level; k <= last_level; ++k) {
398 m.set_cost(objective_var == MapqnObjectiveVar::UN ? x.UN(objective_queue, k)
399 : x.QN(objective_queue, k),
401 }
402 m.set_maximize(sense == MapqnSense::Max);
403
405
408 out.ok = sol.ok();
409 out.objective = sol.objective;
410 out.x = sol.x;
411 out.num_vars = m.num_vars();
412 out.num_rows = m.num_rows();
413 out.iterations = sol.iterations;
414 if (!out.ok) return out;
415
416 out.UN = Matrix<T>(static_cast<std::size_t>(p.M), static_cast<std::size_t>(p.K));
417 out.QN = Matrix<T>(static_cast<std::size_t>(p.M), static_cast<std::size_t>(p.K));
418 for (int i = 0; i < p.M; ++i) {
419 for (int k = 0; k < p.K; ++k) {
420 const std::size_t ii = static_cast<std::size_t>(i), kk = static_cast<std::size_t>(k);
421 out.UN(ii, kk) = sol.x[x.UN(i, k)];
422 out.QN(ii, kk) = sol.x[x.QN(i, k)];
423 }
424 }
425 return out;
426}
427
428} // namespace mapqn
429} // namespace line
430
431#endif // LINE_API_MAPQN_MAPQN_BND_LR_MVA_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
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
std::size_t num_rows() const
Definition simplex.h:125
void set_cost(std::size_t j, const T &v)
Definition simplex.h:164
std::size_t num_vars() const
Definition simplex.h:124
The exception types the port throws.
Model parameters and variable indexing shared by the mapqn QR bounds.
Dense matrix and non-owning view.
const char * lp_status_name(LpStatus s)
Definition simplex.h:84
LpSolution< T > simplex_solve(const LpModel< T > &model, std::size_t max_iterations=0)
Solve the model.
Definition simplex.h:286
MapqnBndLrMvaResult< T > mapqn_bnd_lr_mva(const LrMvaParams< T > &p, int objective_queue, int objective_level, MapqnSense sense=MapqnSense::Max, MapqnObjectiveVar objective_var=MapqnObjectiveVar::UN)
Bound UN or QN at (objective_queue, objective_level) over the MVA-shaped LR polytope.
MapqnSense
Which direction the bound is taken in.
MapqnObjectiveVar
The variable family the objective is taken over.
Number-type abstraction for the templated API port.
Templated primal simplex with Bland's rule.
std::size_t iterations
Definition simplex.h:98
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
Variable layout: UN, then QN, then B, in the reference's declaration order.
std::size_t B(int j, int k, int i) const
std::size_t QN(int i, int k) const
std::size_t num_vars() const
std::size_t UN(int i, int k) const
Parameters of the MVA-shaped LR bound, mirroring the reference's params.
Matrix< T > r
(M x M) routing probabilities
Matrix< T > muMAP
(K x K) completion rates of the MAP queue
int K
number of levels of the MAP queue
int M
number of queues, the last of which is the MAP
Matrix< T > v
(K x K) level-change rates of the MAP queue
std::vector< T > muM
(M-1) service rates of the exponential queues
int N
closed population
Result of an MVA-shaped LR bound solve.
Matrix< T > QN
M x K queue lengths.
Matrix< T > UN
M x K utilizations.
std::vector< T > x
full solution vector, indexed by LrMvaIndex
T objective
the bound on UN(objective_queue, objective_level)
std::string status
textual LP status
bool ok
the LP reached an optimal vertex