LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
mapqn_bnd_qr.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_QR_H
6#define LINE_API_MAPQN_MAPQN_BND_QR_H
7
8/**
9 * @file
10 * @ingroup api_mapqn
11 * General quadratic-reduction (QR) bound on the utilization of one queue-phase
12 * of a closed MAP queueing network.
13 *
14 * Templated port of matlab/lib/qrf/mapqn_bnd_qr.m (ground truth), cross-checked
15 * against python/line_solver/api/mapqn/bnd_qr.py. It is NOT ported from
16 * jar/src/main/java/jline/api/mapqn/Mapqn_bnd_qr.java, which hardcodes
17 * GoalType.MAXIMIZE and takes no sense argument, so it cannot express the lower
18 * bound at all (see _kb/03-api-layer.md).
19 *
20 * THIS IS NOT mapqn_bnd_qr_ld AT alpha == 1. The load-dependent model relaxes
21 * onto the pairwise law p2 alone and imposes 15 families over it. This model
22 * carries the singly-indexed p1 and p1c alongside p2, plus the aggregate U, IT,
23 * Q and C, and ties the three levels together with the projection families
24 * PI21/PI22/PI23. The extra level buys the aggregate inequalities UUB1, QUB1,
25 * CUB1, CUB2 and THM4, which have no expression in a pure p2 model. The two
26 * polytopes are different relaxations of the same chain and neither contains
27 * the other; keep both.
28 *
29 * WHAT MAKES IT A BOUND. Every constraint below is satisfied by the exact
30 * stationary distribution, and none of them pins it down, so the feasible set
31 * is a polytope CONTAINING the exact solution. Optimizing U(i,k) over it
32 * therefore brackets the true utilization from whichever side is asked for.
33 * The bound is a relaxation, not an approximation: it is valid, not merely
34 * close.
35 *
36 * THE FAILURE MODE OF THIS FAMILY IS A VACUOUS BOUND, NOT A CRASH. Omit THM30
37 * and THM3 and no constraint mentions mu at all, so U = 0 and U = 1 are both
38 * feasible and the routine returns the [0,1] box while every subscript in the
39 * file stays internally consistent. That is exactly how the 2026-07-20 defect
40 * in mapqn_bnd_qr.m survived (it carried only SRVB and returned [0,1/3] where
41 * the true range is [0.1970, 0.2182]). Each family is therefore emitted by a
42 * function named after it, so the inventory is diffable against the reference
43 * by name. Do not inline them.
44 *
45 * INERT VARIABLES OMITTED. The reference registers UP(j,k,i,h), QP(j,k,i,h) and
46 * I(j,k,i) and gives them upper bounds, then references them in no constraint
47 * and in no objective (verified: UPidx, QPidx and Iidx appear in
48 * mapqn_bnd_qr.m only at their own registration and bounding). A variable with
49 * no row and no cost cannot move the optimum, so they are not allocated here.
50 * This drops 2 (sum_i K(i))^2 + M sum_i K(i) columns and changes no bound.
51 *
52 * ARITHMETIC. Assembly is +, -, * on the model data and lp::simplex_solve uses
53 * Bland's rule with no tolerance, so at T = line::Rational the returned bound
54 * is the EXACT optimum of the exact polytope. The MATLAB reference reaches it
55 * with linprog's 'interior-point' (its 'interior-point-legacy' declares this
56 * system infeasible once the balance families are present) and lands a few
57 * digits short, so a deviation against MATLAB is expected to be MATLAB's
58 * convergence gap, not this port's error.
59 *
60 * COST. (N+1) sum_i K(i) is the pairwise half-index B; the model has
61 * B^2 + 2 B sum_i K(i) + (M+3) sum_i K(i) columns, so it grows as the fourth
62 * power of the population. The tableau is dense, which confines the port to
63 * small and medium instances.
64 */
65
66#include <cstddef>
67#include <string>
68#include <vector>
69
72#include "line/num/number.h"
73#include "line/util/error.h"
74#include "line/util/matrix.h"
75#include "line/util/simplex.h"
76
77namespace line {
78namespace mapqn {
79
80/** Result of a general QR bound solve. */
81template <class T>
83 bool ok = false; ///< the LP reached an optimal vertex
84 std::string status; ///< textual LP status
85 T objective = T(); ///< the bound on U(objective_queue, objective_phase)
86 Matrix<T> U; ///< M x max(K) utilizations, 0 beyond K(i)
87 Matrix<T> IT; ///< M x max(K) idle times, 0 beyond K(i)
88 Matrix<T> Q; ///< M x max(K) mean queue lengths, 0 beyond K(i)
89 std::vector<T> x; ///< full solution vector, indexed by QrIndex
90 std::size_t num_vars = 0;
91 std::size_t num_rows = 0;
92 std::size_t iterations = 0;
93};
94
95/**
96 * Variable layout of the general QR model: the shared p1-level blocks plus the
97 * joint p2 block. Defined in mapqn_p1_common.h, which the linear reduction
98 * shares; the alias keeps the name this model is documented and tested under.
99 */
101
102namespace detail {
103
104/** ZER5: p2(j,nj,k,j,nj,h) = 0 for h != k (AMPL ZERO1). */
105template <class T>
106void qr_zer5(const MapqnParams<T>& p, const QrIndex& x, lp::LpModel<T>& m) {
107 for (int j = 0; j < p.M; ++j)
108 for (int nj = 0; nj <= p.N; ++nj)
109 for (int k = 0; k < p.K[j]; ++k)
110 for (int h = 0; h < p.K[j]; ++h)
111 if (h != k) m.fix(x.p2(j, nj, k, j, nj, h), T());
112}
113
114/** ZER6: p2(j,nj,k,j,ni,h) = 0 for ni != nj (AMPL ZERO2). */
115template <class T>
116void qr_zer6(const MapqnParams<T>& p, const QrIndex& x, lp::LpModel<T>& m) {
117 for (int j = 0; j < p.M; ++j)
118 for (int nj = 0; nj <= p.N; ++nj)
119 for (int k = 0; k < p.K[j]; ++k)
120 for (int ni = 0; ni <= p.N; ++ni)
121 if (ni != nj)
122 for (int h = 0; h < p.K[j]; ++h) m.fix(x.p2(j, nj, k, j, ni, h), T());
123}
124
125/** ZER7: p2(j,nj,k,i,ni,h) = 0 for i != j and nj + ni > N (AMPL ZERO3). */
126template <class T>
127void qr_zer7(const MapqnParams<T>& p, const QrIndex& x, lp::LpModel<T>& m) {
128 for (int j = 0; j < p.M; ++j)
129 for (int nj = 0; nj <= p.N; ++nj)
130 for (int k = 0; k < p.K[j]; ++k)
131 for (int i = 0; i < p.M; ++i)
132 if (i != j)
133 for (int ni = 0; ni <= p.N; ++ni)
134 if (nj + ni > p.N)
135 for (int h = 0; h < p.K[i]; ++h)
136 m.fix(x.p2(j, nj, k, i, ni, h), T());
137}
138
139/** PCL2: the second moment of the population, sum ni*nj*p2 = N^2. */
140template <class T>
141void qr_pcl2(const MapqnParams<T>& p, const QrIndex& x, lp::LpModel<T>& m) {
142 for (int i = 0; i < p.M; ++i)
143 for (int j = 0; j < p.M; ++j)
144 for (int ni = 1; ni <= p.N; ++ni)
145 for (int nj = 1; nj <= p.N; ++nj)
146 for (int h = 0; h < p.K[i]; ++h)
147 for (int k = 0; k < p.K[j]; ++k)
148 m.row_add_int(x.p2(i, ni, h, j, nj, k), ni * nj);
149 m.emit_eq_int(p.N * p.N);
150}
151
152/** PI21: p1 is the projection of p2 over the busy populations nj >= 1. */
153template <class T>
154void qr_pi21(const MapqnParams<T>& p, const QrIndex& x, lp::LpModel<T>& m) {
155 for (int j = 0; j < p.M; ++j) {
156 for (int k = 0; k < p.K[j]; ++k) {
157 for (int i = 0; i < p.M; ++i) {
158 for (int ni = 0; ni <= p.N; ++ni) {
159 for (int h = 0; h < p.K[i]; ++h) {
160 m.row_add_int(x.p1(j, k, i, ni, h), 1);
161 for (int nj = 1; nj <= p.N; ++nj)
162 m.row_add_int(x.p2(j, nj, k, i, ni, h), -1);
163 m.emit_eq_int(0);
164 }
165 }
166 }
167 }
168 }
169}
170
171/** PI22: p1c is the nj = 0 slice of p2. */
172template <class T>
173void qr_pi22(const MapqnParams<T>& p, const QrIndex& x, lp::LpModel<T>& m) {
174 for (int j = 0; j < p.M; ++j) {
175 for (int k = 0; k < p.K[j]; ++k) {
176 for (int i = 0; i < p.M; ++i) {
177 for (int ni = 0; ni <= p.N; ++ni) {
178 for (int h = 0; h < p.K[i]; ++h) {
179 m.row_add_int(x.p1c(j, k, i, ni, h), 1);
180 m.row_add_int(x.p2(j, 0, k, i, ni, h), -1);
181 m.emit_eq_int(0);
182 }
183 }
184 }
185 }
186 }
187}
188
189/**
190 * PI23: p2 is symmetric under swapping its two halves.
191 *
192 * Emitted only for lexicographically ordered pairs, as the reference does; the
193 * reverse pair is the same row negated and the diagonal is the trivial 0 = 0.
194 */
195template <class T>
196void qr_pi23(const MapqnParams<T>& p, const QrIndex& x, lp::LpModel<T>& m) {
197 for (int j = 0; j < p.M; ++j) {
198 for (int nj = 0; nj <= p.N; ++nj) {
199 for (int k = 0; k < p.K[j]; ++k) {
200 for (int i = 0; i < p.M; ++i) {
201 for (int ni = 0; ni <= p.N; ++ni) {
202 for (int h = 0; h < p.K[i]; ++h) {
203 const bool ordered = (j < i) || (j == i && nj < ni) ||
204 (j == i && nj == ni && k < h);
205 if (!ordered) continue;
206 const std::size_t a = x.p2(i, ni, h, j, nj, k);
207 const std::size_t b = x.p2(j, nj, k, i, ni, h);
208 if (a == b) continue;
209 m.row_add_int(a, 1);
210 m.row_add_int(b, -1);
211 m.emit_eq_int(0);
212 }
213 }
214 }
215 }
216 }
217 }
218}
219
220/**
221 * MARG: the pairwise law agrees with its own marginal at every population
222 * (AMPL MARGINALS). ONE1 imposes only the aggregate over nj, so this is a
223 * separate family.
224 */
225template <class T>
226void qr_marg(const MapqnParams<T>& p, const QrIndex& x, lp::LpModel<T>& m) {
227 for (int j = 0; j < p.M; ++j) {
228 for (int k = 0; k < p.K[j]; ++k) {
229 for (int nj = 0; nj <= p.N; ++nj) {
230 for (int i = 0; i < p.M; ++i) {
231 if (i == j) continue;
232 m.row_add_int(x.p2(j, nj, k, j, nj, k), 1);
233 for (int ni = 0; ni <= p.N - nj; ++ni)
234 for (int h = 0; h < p.K[i]; ++h)
235 m.row_add_int(x.p2(j, nj, k, i, ni, h), -1);
236 m.emit_eq_int(0);
237 }
238 }
239 }
240 }
241}
242
243/**
244 * THM2: the queue-length theorem conditioned on (j,nj,k). Summed over nj >= 1
245 * it recovers the aggregate sum_i C(j,k,i) = N U(j,k).
246 */
247template <class T>
248void qr_thm2(const MapqnParams<T>& p, const QrIndex& x, lp::LpModel<T>& m) {
249 for (int j = 0; j < p.M; ++j) {
250 for (int k = 0; k < p.K[j]; ++k) {
251 for (int nj = 0; nj <= p.N; ++nj) {
252 for (int i = 0; i < p.M; ++i)
253 for (int ni = 1; ni <= p.N; ++ni)
254 for (int h = 0; h < p.K[i]; ++h)
255 m.row_add_int(x.p2(j, nj, k, i, ni, h), ni);
256 m.row_add_int(x.p2(j, nj, k, j, nj, k), -p.N);
257 m.emit_eq_int(0);
258 }
259 }
260 }
261}
262
263/**
264 * THM30: level-crossing balance at an empty station, per arrival phase (AMPL
265 * THM30). The rate into {n_i = 0, phase_i = u} from a busy neighbour equals the
266 * rate out of {n_i = 1} through a completion at i.
267 */
268template <class T>
269void qr_thm30(const MapqnParams<T>& p, const QrIndex& x, lp::LpModel<T>& m) {
270 for (int i = 0; i < p.M; ++i) {
271 for (int u = 0; u < p.K[i]; ++u) {
272 for (int j = 0; j < p.M; ++j) {
273 if (j == i) continue;
274 for (int nj = 1; nj <= p.N; ++nj)
275 for (int k = 0; k < p.K[j]; ++k)
276 for (int h = 0; h < p.K[j]; ++h)
277 m.row_add(x.p2(j, nj, k, i, 0, u), qr_rate(p, j, i, k, h));
278 for (int nj = 0; nj <= p.N; ++nj)
279 for (int k = 0; k < p.K[i]; ++k)
280 for (int h = 0; h < p.K[j]; ++h)
281 m.row_add(x.p2(j, nj, h, i, 1, k), T(-qr_rate(p, i, j, k, u)));
282 }
283 m.emit_eq_int(0);
284 }
285 }
286}
287
288/**
289 * THM3: level-crossing balance between n_i and n_i + 1 (AMPL THM3).
290 *
291 * This family and THM30 are the only ones that mention mu. Drop them and no
292 * constraint distinguishes a fast station from a slow one, so U = 0 stays
293 * feasible and the bound collapses to the [0,1] box.
294 */
295template <class T>
296void qr_thm3(const MapqnParams<T>& p, const QrIndex& x, lp::LpModel<T>& m) {
297 for (int i = 0; i < p.M; ++i) {
298 for (int ni = 0; ni <= p.N - 1; ++ni) {
299 for (int j = 0; j < p.M; ++j) {
300 if (j == i) continue;
301 for (int nj = 1; nj <= p.N; ++nj)
302 for (int k = 0; k < p.K[j]; ++k)
303 for (int h = 0; h < p.K[j]; ++h)
304 for (int u = 0; u < p.K[i]; ++u)
305 m.row_add(x.p2(j, nj, k, i, ni, u), qr_rate(p, j, i, k, h));
306 for (int nj = 0; nj <= p.N; ++nj)
307 for (int k = 0; k < p.K[i]; ++k)
308 for (int u = 0; u < p.K[j]; ++u)
309 for (int h = 0; h < p.K[i]; ++h)
310 m.row_add(x.p2(j, nj, u, i, ni + 1, k),
311 T(-qr_rate(p, i, j, k, h)));
312 }
313 m.emit_eq_int(0);
314 }
315 }
316}
317
318/** CUB1: C(j,k,i) <= sum_h Q(i,h). A conditional length cannot exceed the mean. */
319template <class T>
320void qr_cub1(const MapqnParams<T>& p, const QrIndex& x, lp::LpModel<T>& m) {
321 for (int j = 0; j < p.M; ++j) {
322 for (int k = 0; k < p.K[j]; ++k) {
323 for (int i = 0; i < p.M; ++i) {
324 m.row_add_int(x.C(j, k, i), 1);
325 for (int h = 0; h < p.K[i]; ++h) m.row_add_int(x.Q(i, h), -1);
326 m.emit_le_int(0);
327 }
328 }
329 }
330}
331
332/** CUB2: C(j,k,i) <= N U(j,k). The conditioning event has probability U. */
333template <class T>
334void qr_cub2(const MapqnParams<T>& p, const QrIndex& x, lp::LpModel<T>& m) {
335 for (int j = 0; j < p.M; ++j) {
336 for (int k = 0; k < p.K[j]; ++k) {
337 for (int i = 0; i < p.M; ++i) {
338 m.row_add_int(x.C(j, k, i), 1);
339 m.row_add_int(x.U(j, k), -p.N);
340 m.emit_le_int(0);
341 }
342 }
343 }
344}
345
346/** THM4: the QMIN inequality, N P(j busy in k, i nonempty) <= sum_t C(j,k,t). */
347template <class T>
348void qr_thm4(const MapqnParams<T>& p, const QrIndex& x, lp::LpModel<T>& m) {
349 for (int j = 0; j < p.M; ++j) {
350 for (int k = 0; k < p.K[j]; ++k) {
351 for (int i = 0; i < p.M; ++i) {
352 for (int h = 0; h < p.K[i]; ++h)
353 for (int nj = 0; nj <= p.N; ++nj)
354 for (int ni = 1; ni <= p.N; ++ni)
355 m.row_add_int(x.p2(j, nj, k, i, ni, h), p.N);
356 for (int t = 0; t < p.M; ++t)
357 for (int h = 0; h < p.K[t]; ++h)
358 for (int nj = 0; nj <= p.N; ++nj)
359 for (int nt = 0; nt <= p.N; ++nt)
360 m.row_add_int(x.p2(j, nj, k, t, nt, h), -nt);
361 m.emit_le_int(0);
362 }
363 }
364 }
365}
366
367} // namespace detail
368
369/**
370 * Bound U(objective_queue, objective_phase) over the general QR polytope.
371 *
372 * @param p network parameters; queues and phases are 0-based.
373 * alpha is IGNORED: this model has no load dependence,
374 * use mapqn_bnd_qr_ld for that.
375 * @param objective_queue queue index, 0..M-1
376 * @param objective_phase phase index, 0..K(objective_queue)-1
377 * @param sense Max for an upper bound, Min for a lower bound
378 */
379template <class T>
380MapqnBndQrResult<T> mapqn_bnd_qr(const MapqnParams<T>& p, int objective_queue,
381 int objective_phase, MapqnSense sense = MapqnSense::Max) {
382 p.validate();
383 detail::p1_check_objective(p, objective_queue, objective_phase);
384
385 const QrIndex x(p.M, p.N, p.K, true);
387 detail::p1_bounds(p, x, m);
388
389 // Families, in the order the reference emits them. Named one per function
390 // so the inventory is diffable against mapqn_bnd_qr.m; see the header note
391 // on why an omitted family reads as a loose bound rather than an error.
392 detail::p1_zer1(p, x, m);
393 detail::p1_zer2(p, x, m);
394 detail::p1_zer3(p, x, m);
395 detail::p1_zer4(p, x, m);
396 detail::qr_zer5(p, x, m);
397 detail::qr_zer6(p, x, m);
398 detail::qr_zer7(p, x, m);
399 detail::p1_cequ(p, x, m);
400 detail::p1_one1(p, x, m);
401 detail::p1_utlb(p, x, m);
402 detail::p1_utlc(p, x, m);
403 detail::p1_qlen(p, x, m);
404 detail::p1_srvb(p, x, m);
405 detail::p1_popc(p, x, m);
406 detail::p1_one(p, x, m);
407 detail::qr_pcl2(p, x, m);
408 detail::qr_pi21(p, x, m);
409 detail::qr_pi22(p, x, m);
410 detail::qr_pi23(p, x, m);
411 detail::p1_clen(p, x, m);
412 detail::qr_marg(p, x, m);
413 detail::qr_thm2(p, x, m);
414 detail::qr_thm30(p, x, m);
415 detail::qr_thm3(p, x, m);
416 detail::p1_uub1(p, x, m);
417 detail::p1_qub1(p, x, m);
418 detail::qr_cub1(p, x, m);
419 detail::qr_cub2(p, x, m);
420 detail::qr_thm4(p, x, m);
421
422 m.set_cost(x.U(objective_queue, objective_phase), num_traits<T>::from_int(1));
423 m.set_maximize(sense == MapqnSense::Max);
424
426
429 out.ok = sol.ok();
430 out.objective = sol.objective;
431 out.x = sol.x;
432 out.num_vars = m.num_vars();
433 out.num_rows = m.num_rows();
434 out.iterations = sol.iterations;
435 if (!out.ok) return out;
436
437 std::size_t maxK = 0;
438 for (int i = 0; i < p.M; ++i)
439 if (static_cast<std::size_t>(p.K[i]) > maxK) maxK = static_cast<std::size_t>(p.K[i]);
440 out.U = Matrix<T>(static_cast<std::size_t>(p.M), maxK);
441 out.IT = Matrix<T>(static_cast<std::size_t>(p.M), maxK);
442 out.Q = Matrix<T>(static_cast<std::size_t>(p.M), maxK);
443 for (int i = 0; i < p.M; ++i) {
444 for (int k = 0; k < p.K[i]; ++k) {
445 const std::size_t ii = static_cast<std::size_t>(i), kk = static_cast<std::size_t>(k);
446 out.U(ii, kk) = sol.x[x.U(i, k)];
447 out.IT(ii, kk) = sol.x[x.IT(i, k)];
448 out.Q(ii, kk) = sol.x[x.Q(i, k)];
449 }
450 }
451 return out;
452}
453
454} // namespace mapqn
455} // namespace line
456
457#endif // LINE_API_MAPQN_MAPQN_BND_QR_H
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 emit_eq_int(long rhs)
Definition simplex.h:220
void row_add_int(std::size_t j, long v)
Definition simplex.h:197
void set_cost(std::size_t j, const T &v)
Definition simplex.h:164
std::size_t num_vars() const
Definition simplex.h:124
void fix(std::size_t j, const T &v)
Pin a variable to a value; it is substituted out of the tableau.
Definition simplex.h:156
void row_add(std::size_t j, const T &v)
row(j) += v, the accumulation the MATLAB reference performs.
Definition simplex.h:188
void emit_le_int(long rhs)
Definition simplex.h:219
The exception types the port throws.
The variable layout and the constraint families shared by the two reductions that carry singly-indexe...
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
void qr_thm4(const MapqnParams< T > &p, const P2Index &idx, lp::LpModel< T > &m)
THM4 (QMIN): for each (j,k,i), sum_{t,h,nj,nt} nt p2(j,nj,k,t,nt,h) >= N sum_{h,nj,...
MapqnP1Index QrIndex
Variable layout of the general QR model: the shared p1-level blocks plus the joint p2 block.
MapqnSense
Which direction the bound is taken in.
MapqnBndQrResult< T > mapqn_bnd_qr(const MapqnParams< T > &p, int objective_queue, int objective_phase, MapqnSense sense=MapqnSense::Max)
Bound U(objective_queue, objective_phase) over the general QR polytope.
void qr_thm2(const MapqnParams< T > &p, const P2Index &idx, lp::LpModel< T > &m)
THM2 (phase balance): for each (i,k) the total rate out of phase k at queue i equals the total rate i...
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
Result of a general QR bound solve.
Matrix< T > U
M x max(K) utilizations, 0 beyond K(i).
std::vector< T > x
full solution vector, indexed by QrIndex
bool ok
the LP reached an optimal vertex
std::string status
textual LP status
Matrix< T > Q
M x max(K) mean queue lengths, 0 beyond K(i).
Matrix< T > IT
M x max(K) idle times, 0 beyond K(i).
T objective
the bound on U(objective_queue, objective_phase)
Variable layout of the p1-level models.
std::size_t Q(int i, int k) const
std::size_t p1c(int j, int k, int i, int ni, int h) const
std::size_t IT(int i, int k) const
std::size_t C(int j, int k, int i) const
std::size_t p1(int j, int k, int i, int ni, int h) const
std::size_t U(int i, int k) const
std::size_t num_vars() const
std::size_t p2(int j, int nj, int k, int i, int ni, int h) const
Parameters of a MAP queueing network for the QR bounds.
int N
total population
std::vector< int > K
K[i] = number of phases at queue i.
int M
number of queues