LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
mapqn_bnd_lr.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_H
6#define LINE_API_MAPQN_MAPQN_BND_LR_H
7
8/**
9 * @file
10 * @ingroup api_mapqn
11 * General linear-reduction (LR) bound on the utilization of one queue-phase of
12 * a closed MAP queueing network.
13 *
14 * Templated port of matlab/lib/qrf/mapqn_bnd_lr.m (ground truth), cross-checked
15 * against python/line_solver/api/mapqn/bnd_lr.py. Note the MATLAB file's own
16 * header records that it is a port OF the Python file, so the two are one
17 * lineage rather than two independent derivations; where they disagree, neither
18 * is automatically right and the AMPL model noblo_skel.mod settles it.
19 *
20 * WHAT DISTINGUISHES IT FROM THE QUADRATIC REDUCTION. This model keeps only the
21 * singly-indexed p1(j,k,i,ni,h) and p1c, never the joint p2, so it is the
22 * cheaper relaxation: the column count is linear in (N+1) sum_i K(i) rather
23 * than quadratic. Fifteen of its twenty families are literally the quadratic
24 * model's, and live in mapqn_p1_common.h. The five that are its own replace
25 * what the p2 level would otherwise supply:
26 * MPCB sum_i C(j,k,i) = N U(j,k), the aggregate of the QR model's THM2
27 * UJNT AMPL SIMMETRY projected onto p1, standing in for PI23
28 * GFFL0 AMPL THM30 on p1, the empty-station level crossing
29 * GFFL AMPL THM3 on p1, the level crossing between n_i and n_i + 1
30 * QBAL throughput balance at each station
31 *
32 * THE FAILURE MODE OF THIS FAMILY IS A VACUOUS BOUND, NOT A CRASH. GFFL and
33 * GFFL0 are the only families that read the rates into the p1 variables, and
34 * SRVB the only one that reads them into U. Drop them and no constraint
35 * distinguishes a fast station from a slow one, so U = 0 stays feasible and the
36 * routine returns the [0,1] box with every subscript internally consistent.
37 * That is exactly the 2026-07-20 defect in mapqn_bnd_lr.m, which built q and
38 * then referenced it in NO constraint at all. Each family is therefore emitted
39 * by a function named after it; do not inline them.
40 *
41 * ORACLE. At K(i) == 1 for every i the phase structure disappears and this
42 * model must reproduce mapqn_bnd_lr_pf exactly, on the same instance and in the
43 * same sense. That is the check test_mapqn_bnd_lr.cpp leads with, and it is
44 * what the MATLAB reference itself uses (it holds there to 1e-7).
45 *
46 * ARITHMETIC. Assembly is +, -, * on the model data and lp::simplex_solve uses
47 * Bland's rule with no tolerance, so at T = line::Rational the returned bound
48 * is the EXACT optimum of the exact polytope. MATLAB reaches it with linprog's
49 * 'interior-point' and lands a few digits short.
50 */
51
52#include <cstddef>
53#include <string>
54#include <vector>
55
58#include "line/num/number.h"
59#include "line/util/error.h"
60#include "line/util/matrix.h"
61#include "line/util/simplex.h"
62
63namespace line {
64namespace mapqn {
65
66/** Result of a general LR bound solve. */
67template <class T>
69 bool ok = false; ///< the LP reached an optimal vertex
70 std::string status; ///< textual LP status
71 T objective = T(); ///< the bound on U(objective_queue, objective_phase)
72 Matrix<T> U; ///< M x max(K) utilizations, 0 beyond K(i)
73 Matrix<T> IT; ///< M x max(K) idle times, 0 beyond K(i)
74 Matrix<T> Q; ///< M x max(K) mean queue lengths, 0 beyond K(i)
75 std::vector<T> x; ///< full solution vector, indexed by LrIndex
76 std::size_t num_vars = 0;
77 std::size_t num_rows = 0;
78 std::size_t iterations = 0;
79};
80
81/**
82 * Variable layout of the LR model: the shared p1-level blocks and nothing else.
83 * Defined in mapqn_p1_common.h, constructed here with the joint block switched
84 * off.
85 */
87
88namespace detail {
89
90/**
91 * MPCB: sum over i of C(j,k,i) = N U(j,k).
92 *
93 * The aggregate the QR model derives from THM2 by summing over nj >= 1. Here it
94 * is imposed directly, because there is no p2 to sum.
95 */
96template <class T>
97void lr_mpcb(const MapqnParams<T>& p, const LrIndex& x, lp::LpModel<T>& m) {
98 for (int j = 0; j < p.M; ++j) {
99 for (int k = 0; k < p.K[j]; ++k) {
100 for (int i = 0; i < p.M; ++i) m.row_add_int(x.C(j, k, i), 1);
101 m.row_add_int(x.U(j, k), -p.N);
102 m.emit_eq_int(0);
103 }
104 }
105}
106
107/**
108 * UJNT: joint-probability symmetry (AMPL SIMMETRY, projected onto p1).
109 *
110 * sum{ni>=1} p1(j,k,i,ni,h) = sum{nj>=1} p1(i,h,j,nj,k): both sides are
111 * P(n_j >= 1, phase_j = k, n_i >= 1, phase_i = h). Emitted once per unordered
112 * pair, as the reference does; the reverse pair is the same row negated.
113 */
114template <class T>
115void lr_ujnt(const MapqnParams<T>& p, const LrIndex& x, lp::LpModel<T>& m) {
116 for (int j = 0; j < p.M; ++j) {
117 for (int k = 0; k < p.K[j]; ++k) {
118 for (int i = 0; i < p.M; ++i) {
119 for (int h = 0; h < p.K[i]; ++h) {
120 if (!((j < i) || (j == i && k < h))) continue;
121 for (int ni = 1; ni <= p.N; ++ni) m.row_add_int(x.p1(j, k, i, ni, h), 1);
122 for (int nj = 1; nj <= p.N; ++nj) m.row_add_int(x.p1(i, h, j, nj, k), -1);
123 m.emit_eq_int(0);
124 }
125 }
126 }
127 }
128}
129
130/**
131 * GFFL0: level-crossing balance at an empty station, per arrival phase (AMPL
132 * THM30). The rate into {n_i = 0, phase_i = u} from a busy neighbour equals the
133 * rate out of {n_i = 1} through a completion at i.
134 */
135template <class T>
136void lr_gffl0(const MapqnParams<T>& p, const LrIndex& x, lp::LpModel<T>& m) {
137 for (int i = 0; i < p.M; ++i) {
138 for (int u = 0; u < p.K[i]; ++u) {
139 for (int j = 0; j < p.M; ++j) {
140 if (j == i) continue;
141 for (int k = 0; k < p.K[j]; ++k)
142 for (int h = 0; h < p.K[j]; ++h)
143 m.row_add(x.p1(j, k, i, 0, u), qr_rate(p, j, i, k, h));
144 for (int k = 0; k < p.K[i]; ++k)
145 m.row_add(x.p1(i, k, i, 1, k), T(-qr_rate(p, i, j, k, u)));
146 }
147 m.emit_eq_int(0);
148 }
149 }
150}
151
152/**
153 * GFFL: level-crossing balance between n_i and n_i + 1 (AMPL THM3).
154 *
155 * The arrival rate to station i while it holds ni jobs equals the completion
156 * rate at i while it holds ni + 1. With it absent, a station idle with
157 * probability one is feasible and the utilization lower bound collapses to
158 * zero.
159 */
160template <class T>
161void lr_gffl(const MapqnParams<T>& p, const LrIndex& x, lp::LpModel<T>& m) {
162 for (int i = 0; i < p.M; ++i) {
163 for (int ni = 0; ni <= p.N - 1; ++ni) {
164 for (int j = 0; j < p.M; ++j) {
165 if (j == i) continue;
166 for (int k = 0; k < p.K[j]; ++k)
167 for (int h = 0; h < p.K[j]; ++h)
168 for (int u = 0; u < p.K[i]; ++u)
169 m.row_add(x.p1(j, k, i, ni, u), qr_rate(p, j, i, k, h));
170 for (int k = 0; k < p.K[i]; ++k)
171 for (int h = 0; h < p.K[i]; ++h)
172 m.row_add(x.p1(i, k, i, ni + 1, k), T(-qr_rate(p, i, j, k, h)));
173 }
174 m.emit_eq_int(0);
175 }
176 }
177}
178
179/**
180 * QBAL: throughput balance at each station.
181 *
182 * The departure rate from i equals the arrival rate to i, with the arrival side
183 * split over whether station i is busy (the p1(i,u,j,nj,k) term, nj >= 1) or
184 * empty (the p1(j,k,i,0,u) term).
185 */
186template <class T>
187void lr_qbal(const MapqnParams<T>& p, const LrIndex& x, lp::LpModel<T>& m) {
188 for (int i = 0; i < p.M; ++i) {
189 for (int j = 0; j < p.M; ++j) {
190 if (j == i) continue;
191 for (int k = 0; k < p.K[i]; ++k)
192 for (int h = 0; h < p.K[i]; ++h)
193 m.row_add(x.U(i, k), qr_rate(p, i, j, k, h));
194 for (int k = 0; k < p.K[j]; ++k) {
195 for (int h = 0; h < p.K[j]; ++h) {
196 const T w = qr_rate(p, j, i, k, h);
197 for (int u = 0; u < p.K[i]; ++u)
198 for (int nj = 1; nj <= p.N; ++nj)
199 m.row_add(x.p1(i, u, j, nj, k), T(-w));
200 for (int u = 0; u < p.K[i]; ++u) m.row_add(x.p1(j, k, i, 0, u), T(-w));
201 }
202 }
203 }
204 m.emit_eq_int(0);
205 }
206}
207
208} // namespace detail
209
210/**
211 * Bound U(objective_queue, objective_phase) over the linear-reduction polytope.
212 *
213 * @param p network parameters; queues and phases are 0-based.
214 * alpha is IGNORED: this model has no load dependence.
215 * @param objective_queue queue index, 0..M-1
216 * @param objective_phase phase index, 0..K(objective_queue)-1
217 * @param sense Max for an upper bound, Min for a lower bound
218 */
219template <class T>
220MapqnBndLrResult<T> mapqn_bnd_lr(const MapqnParams<T>& p, int objective_queue,
221 int objective_phase, MapqnSense sense = MapqnSense::Max) {
222 p.validate();
223 detail::p1_check_objective(p, objective_queue, objective_phase);
224
225 const LrIndex x(p.M, p.N, p.K, false);
227 detail::p1_bounds(p, x, m);
228
229 // Families, in the order the reference emits them. Named one per function
230 // so the inventory is diffable against mapqn_bnd_lr.m; see the header note
231 // on why an omitted family reads as a loose bound rather than an error.
232 detail::p1_zer1(p, x, m);
233 detail::p1_zer2(p, x, m);
234 detail::p1_zer3(p, x, m);
235 detail::p1_zer4(p, x, m);
236 detail::p1_cequ(p, x, m);
237 detail::p1_one1(p, x, m);
238 detail::p1_utlb(p, x, m);
239 detail::p1_utlc(p, x, m);
240 detail::p1_qlen(p, x, m);
241 detail::p1_clen(p, x, m);
242 detail::p1_one(p, x, m);
243 detail::p1_popc(p, x, m);
244 detail::lr_mpcb(p, x, m);
245 detail::p1_srvb(p, x, m);
246 detail::lr_ujnt(p, x, m);
247 detail::lr_gffl0(p, x, m);
248 detail::lr_gffl(p, x, m);
249 detail::lr_qbal(p, x, m);
250 detail::p1_uub1(p, x, m);
251 detail::p1_qub1(p, x, m);
252
253 m.set_cost(x.U(objective_queue, objective_phase), num_traits<T>::from_int(1));
254 m.set_maximize(sense == MapqnSense::Max);
255
257
260 out.ok = sol.ok();
261 out.objective = sol.objective;
262 out.x = sol.x;
263 out.num_vars = m.num_vars();
264 out.num_rows = m.num_rows();
265 out.iterations = sol.iterations;
266 if (!out.ok) return out;
267
268 std::size_t maxK = 0;
269 for (int i = 0; i < p.M; ++i)
270 if (static_cast<std::size_t>(p.K[i]) > maxK) maxK = static_cast<std::size_t>(p.K[i]);
271 out.U = Matrix<T>(static_cast<std::size_t>(p.M), maxK);
272 out.IT = Matrix<T>(static_cast<std::size_t>(p.M), maxK);
273 out.Q = Matrix<T>(static_cast<std::size_t>(p.M), maxK);
274 for (int i = 0; i < p.M; ++i) {
275 for (int k = 0; k < p.K[i]; ++k) {
276 const std::size_t ii = static_cast<std::size_t>(i), kk = static_cast<std::size_t>(k);
277 out.U(ii, kk) = sol.x[x.U(i, k)];
278 out.IT(ii, kk) = sol.x[x.IT(i, k)];
279 out.Q(ii, kk) = sol.x[x.Q(i, k)];
280 }
281 }
282 return out;
283}
284
285} // namespace mapqn
286} // namespace line
287
288#endif // LINE_API_MAPQN_MAPQN_BND_LR_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 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.
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
MapqnBndLrResult< T > mapqn_bnd_lr(const MapqnParams< T > &p, int objective_queue, int objective_phase, MapqnSense sense=MapqnSense::Max)
Bound U(objective_queue, objective_phase) over the linear-reduction polytope.
MapqnSense
Which direction the bound is taken in.
MapqnP1Index LrIndex
Variable layout of the LR model: the shared p1-level blocks and nothing else.
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 LR bound solve.
Matrix< T > IT
M x max(K) idle times, 0 beyond K(i).
Matrix< T > U
M x max(K) utilizations, 0 beyond K(i).
std::string status
textual LP status
T objective
the bound on U(objective_queue, objective_phase)
Matrix< T > Q
M x max(K) mean queue lengths, 0 beyond K(i).
std::vector< T > x
full solution vector, indexed by LrIndex
bool ok
the LP reached an optimal vertex
Variable layout of the p1-level models.
std::size_t Q(int i, int k) 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
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