LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
mapqn_bnd_lr_pf.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_PF_H
6#define LINE_API_MAPQN_MAPQN_BND_LR_PF_H
7
8/**
9 * @file
10 * @ingroup api_mapqn
11 * Linear-reduction (LR) bound on the utilization of one station of a closed
12 * product-form network.
13 *
14 * Templated port of matlab/lib/qrf/mapqn_bnd_lr_pf.m (ground truth), whose own
15 * origin is the AMPL model bnd_linearreduction_pf.mod.
16 *
17 * WHAT MAKES IT A BOUND. The exact stationary distribution satisfies every
18 * constraint assembled below -- normalization, the utilization and queue-length
19 * definitions, population conservation, the global flow balance of Gordon-Newell
20 * and the joint-marginal identities -- but those constraints do not pin it down.
21 * The feasible set is therefore a POLYTOPE CONTAINING the exact solution, so
22 * minimizing U_i over it cannot exceed the true utilization and maximizing it
23 * cannot fall short. Both senses are valid bounds, which is why one routine
24 * serves `lr.lower` and `lr.upper`.
25 *
26 * NOT TO BE CONFUSED WITH `qrf.mmi.linear`, whose name refers only to its
27 * explicit Aeq/beq constraint representation: its OBJECTIVE is the nonlinear
28 * MEM entropy and it needs `fmincon`. This method is an LP end to end, which is
29 * exactly why it ports and the rest of the QRF family does not.
30 *
31 * ARITHMETIC. Assembly is additions and multiplications, and the optimum is a
32 * vertex of a rational polytope, so at `Rational` the bound is EXACT and is
33 * deliberately left ungated: `lp::simplex_solve` uses Bland's rule with no
34 * tolerance under exact arithmetic. MATLAB reaches the same vertex through
35 * `linprog`'s interior-point method, which approaches it from the interior and
36 * stops a few digits short; a deviation against MATLAB is therefore expected to
37 * be MATLAB's convergence gap and not this port's error.
38 *
39 * COST. The variable count is 2M + M^2 + 2 M^2 (N+1) and the row count is
40 * O(M^2 + M N), so the LP grows quadratically in the station count and linearly
41 * in the population. It is a bound, not a cheap one.
42 */
43
44#include <cstddef>
45#include <string>
46#include <vector>
47
48#include "line/num/number.h"
49#include "line/util/error.h"
50#include "line/util/matrix.h"
51#include "line/util/simplex.h"
53
54namespace line {
55namespace mapqn {
56
57/** Product-form parameters of the LR bound, mirroring MATLAB's `params`. */
58template <class T>
59struct LrPfParams {
60 int M = 0; ///< station count
61 int N = 0; ///< closed population
62 std::vector<T> mu; ///< (M) service rates
63 Matrix<T> r; ///< (M x M) routing probabilities
64};
65
66/** Return value of mapqn_bnd_lr_pf, mirroring the MATLAB `result` struct. */
67template <class T>
68struct LrPfResult {
69 T objective = T(); ///< the bounded utilization of the objective station
70 std::vector<T> U; ///< (M) utilizations at the optimal vertex
71 std::vector<T> Q; ///< (M) queue lengths at the optimal vertex
72 std::string status; ///< simplex status name
73 bool ok = false;
74 std::size_t iterations = 0;
75 std::size_t num_vars = 0, num_rows = 0;
76};
77
78namespace detail {
79
80/**
81 * The variable layout of the reference, in its declaration order: U, Q, C, p1,
82 * p1c. The order is load bearing only in that the tests compare against MATLAB
83 * through the RESULT fields, but keeping it identical makes the two assemblies
84 * diffable line by line.
85 */
86struct LrPfIndex {
87 int M = 0, N = 0;
88 std::size_t u0 = 0, q0 = 0, c0 = 0, p1_0 = 0, p1c0 = 0, total = 0;
89
90 LrPfIndex(int M_, int N_) : M(M_), N(N_) {
91 const std::size_t m = static_cast<std::size_t>(M);
92 const std::size_t np1 = static_cast<std::size_t>(N + 1);
93 u0 = 0;
94 q0 = u0 + m;
95 c0 = q0 + m;
96 p1_0 = c0 + m * m;
97 p1c0 = p1_0 + m * m * np1;
98 total = p1c0 + m * m * np1;
99 }
100 std::size_t U(int i) const { return u0 + static_cast<std::size_t>(i); }
101 std::size_t Q(int i) const { return q0 + static_cast<std::size_t>(i); }
102 std::size_t C(int j, int i) const {
103 return c0 + static_cast<std::size_t>(j) * static_cast<std::size_t>(M) +
104 static_cast<std::size_t>(i);
105 }
106 std::size_t p1(int j, int i, int n) const {
107 return p1_0 +
108 (static_cast<std::size_t>(j) * static_cast<std::size_t>(M) +
109 static_cast<std::size_t>(i)) *
110 static_cast<std::size_t>(N + 1) +
111 static_cast<std::size_t>(n);
112 }
113 std::size_t p1c(int j, int i, int n) const {
114 return p1c0 +
115 (static_cast<std::size_t>(j) * static_cast<std::size_t>(M) +
116 static_cast<std::size_t>(i)) *
117 static_cast<std::size_t>(N + 1) +
118 static_cast<std::size_t>(n);
119 }
120};
121
122} // namespace detail
123
124/**
125 * Port of `mapqn_bnd_lr_pf`.
126 *
127 * @param p station count, population, rates and routing
128 * @param objective_queue 1-BASED station whose utilization is bounded, as in the
129 * reference
130 * @param sense Min for the `lr.lower` bound, Max for `lr.upper`
131 */
132template <class T>
133LrPfResult<T> mapqn_bnd_lr_pf(const LrPfParams<T>& p, int objective_queue, MapqnSense sense) {
134 const int M = p.M, N = p.N;
135 if (M <= 0) throw InputError("mapqn_bnd_lr_pf: the station count must be positive");
136 if (N <= 0) throw InputError("mapqn_bnd_lr_pf: the population must be positive");
137 if (static_cast<int>(p.mu.size()) != M)
138 throw InputError("mapqn_bnd_lr_pf: mu has the wrong length");
139 if (static_cast<int>(p.r.rows()) != M || static_cast<int>(p.r.cols()) != M)
140 throw InputError("mapqn_bnd_lr_pf: the routing matrix is not (M x M)");
141 if (objective_queue < 1 || objective_queue > M)
142 throw InputError("mapqn_bnd_lr_pf: the objective station is out of range");
143
144 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
145 const T Nt = num_traits<T>::from_int(N);
146
147 // q(i,j) = r(i,j) mu(i): the rate at which station i sends work to j.
148 Matrix<T> q(static_cast<std::size_t>(M), static_cast<std::size_t>(M), zero);
149 for (int i = 0; i < M; ++i)
150 for (int j = 0; j < M; ++j)
151 q(static_cast<std::size_t>(i), static_cast<std::size_t>(j)) =
152 T(p.r(static_cast<std::size_t>(i), static_cast<std::size_t>(j)) *
153 p.mu[static_cast<std::size_t>(i)]);
154
155 const detail::LrPfIndex ix(M, N);
156 lp::LpModel<T> m(ix.total);
157
158 for (int i = 0; i < M; ++i) {
159 m.set_bounds(ix.U(i), zero, one);
160 m.set_bounds(ix.Q(i), zero, Nt);
161 for (int j = 0; j < M; ++j) m.set_bounds(ix.C(j, i), zero, Nt);
162 }
163 for (int j = 0; j < M; ++j)
164 for (int i = 0; i < M; ++i)
165 for (int n = 0; n <= N; ++n) {
166 m.set_bounds(ix.p1(j, i, n), zero, one);
167 m.set_bounds(ix.p1c(j, i, n), zero, one);
168 }
169
170 // ZER1 and ZER3 are BOUNDS in the reference, not rows: a station observed
171 // from itself is never empty (the observer is there), and a station observed
172 // from a different one never holds the whole population.
173 for (int j = 0; j < M; ++j) m.set_upper(ix.p1(j, j, 0), zero);
174 for (int j = 0; j < M; ++j)
175 for (int i = 0; i < M; ++i)
176 if (j != i) m.set_upper(ix.p1(j, i, N), zero);
177
178 // CEQU: the self-conditional queue length is the queue length.
179 for (int j = 0; j < M; ++j) {
180 m.row_clear();
181 m.row_add(ix.C(j, j), one);
182 m.row_add(ix.Q(j), T(-one));
183 m.emit(lp::LpSense::EQ, zero);
184 }
185 // ONE1: each conditional marginal and its complement sum to one.
186 for (int j = 0; j < M; ++j)
187 for (int i = 0; i < M; ++i) {
188 m.row_clear();
189 for (int n = 0; n <= N; ++n) {
190 m.row_add(ix.p1(j, i, n), one);
191 m.row_add(ix.p1c(j, i, n), one);
192 }
193 m.emit(lp::LpSense::EQ, one);
194 }
195 // UTIL: the utilization is the mass of the conditional marginal, whichever
196 // station it is conditioned on -- which is what ties the M copies together.
197 for (int i = 0; i < M; ++i)
198 for (int t = 0; t < M; ++t) {
199 m.row_clear();
200 m.row_add(ix.U(i), one);
201 for (int n = 0; n <= N; ++n) m.row_add(ix.p1(i, t, n), T(-one));
202 m.emit(lp::LpSense::EQ, zero);
203 }
204 // QLEN and CLEN: first moments of the marginals.
205 for (int i = 0; i < M; ++i) {
206 m.row_clear();
207 m.row_add(ix.Q(i), one);
208 for (int n = 0; n <= N; ++n) m.row_add_int(ix.p1(i, i, n), -static_cast<long>(n));
209 m.emit(lp::LpSense::EQ, zero);
210 }
211 for (int j = 0; j < M; ++j)
212 for (int i = 0; i < M; ++i) {
213 m.row_clear();
214 m.row_add(ix.C(j, i), one);
215 for (int n = 0; n <= N; ++n) m.row_add_int(ix.p1(j, i, n), -static_cast<long>(n));
216 m.emit(lp::LpSense::EQ, zero);
217 }
218 // MPCB: the conditional queue lengths seen from j sum to N U_j.
219 for (int j = 0; j < M; ++j) {
220 m.row_clear();
221 for (int i = 0; i < M; ++i) m.row_add(ix.C(j, i), one);
222 m.row_add(ix.U(j), T(-Nt));
223 m.emit(lp::LpSense::EQ, zero);
224 }
225 // POPC: the population is conserved.
226 m.row_clear();
227 for (int i = 0; i < M; ++i) m.row_add(ix.Q(i), one);
228 m.emit(lp::LpSense::EQ, Nt);
229
230 // GFFL0 and GFFL: Gordon-Newell global flow balance at each level. The
231 // reference ACCUMULATES into the row, so the p1(i,i,.) coefficient collects
232 // -sum_{j != i} q(i,j) rather than being overwritten; row_add mirrors that.
233 for (int i = 0; i < M; ++i) {
234 m.row_clear();
235 for (int j = 0; j < M; ++j) {
236 if (j == i) continue;
237 m.row_add(ix.p1(j, i, 0), q(static_cast<std::size_t>(j), static_cast<std::size_t>(i)));
238 m.row_add(ix.p1(i, i, 1),
239 T(-q(static_cast<std::size_t>(i), static_cast<std::size_t>(j))));
240 }
241 m.emit(lp::LpSense::EQ, zero);
242 }
243 for (int i = 0; i < M; ++i)
244 for (int n = 1; n <= N - 1; ++n) {
245 m.row_clear();
246 for (int j = 0; j < M; ++j) {
247 if (j == i) continue;
248 m.row_add(ix.p1(j, i, n),
249 q(static_cast<std::size_t>(j), static_cast<std::size_t>(i)));
250 m.row_add(ix.p1(i, i, n + 1),
251 T(-q(static_cast<std::size_t>(i), static_cast<std::size_t>(j))));
252 }
253 m.emit(lp::LpSense::EQ, zero);
254 }
255 // UJNT: the joint occupancy of (i,j) is symmetric in the two orderings.
256 for (int i = 0; i < M; ++i)
257 for (int j = 0; j < M; ++j) {
258 m.row_clear();
259 for (int n = 1; n <= N; ++n) m.row_add(ix.p1(j, i, n), one);
260 for (int n = 1; n <= N; ++n) m.row_add(ix.p1(i, j, n), T(-one));
261 m.emit(lp::LpSense::EQ, zero);
262 }
263 // QBAL: flow balance of station i against every other station.
264 for (int i = 0; i < M; ++i) {
265 m.row_clear();
266 for (int j = 0; j < M; ++j) {
267 if (j == i) continue;
268 m.row_add(ix.U(i), q(static_cast<std::size_t>(i), static_cast<std::size_t>(j)));
269 for (int n = 1; n <= N; ++n)
270 m.row_add(ix.p1(i, j, n),
271 T(-q(static_cast<std::size_t>(j), static_cast<std::size_t>(i))));
272 m.row_add(ix.p1(j, i, 0),
273 T(-q(static_cast<std::size_t>(j), static_cast<std::size_t>(i))));
274 }
275 m.emit(lp::LpSense::EQ, zero);
276 }
277
278 m.set_cost(ix.U(objective_queue - 1), one);
279 m.set_maximize(sense == MapqnSense::Max);
280
281 LrPfResult<T> res;
282 res.num_vars = m.num_vars();
283 res.num_rows = m.num_rows();
286 res.iterations = s.iterations;
287 res.ok = s.ok();
288 if (!res.ok) return res;
289 res.objective = s.objective;
290 res.U.resize(static_cast<std::size_t>(M));
291 res.Q.resize(static_cast<std::size_t>(M));
292 for (int i = 0; i < M; ++i) {
293 res.U[static_cast<std::size_t>(i)] = s.x[ix.U(i)];
294 res.Q[static_cast<std::size_t>(i)] = s.x[ix.Q(i)];
295 }
296 return res;
297}
298
299} // namespace mapqn
300} // namespace line
301
302#endif // LINE_API_MAPQN_MAPQN_BND_LR_PF_H
InputError(const std::string &what)
Definition error.h:39
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
std::size_t num_rows() const
Definition simplex.h:125
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_clear()
Discard whatever the row accumulator holds.
Definition simplex.h:179
void set_upper(std::size_t j, const T &v)
Definition simplex.h:134
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.
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
MapqnSense
Which direction the bound is taken in.
LrPfResult< T > mapqn_bnd_lr_pf(const LrPfParams< T > &p, int objective_queue, MapqnSense sense)
Port of mapqn_bnd_lr_pf.
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
Product-form parameters of the LR bound, mirroring MATLAB's params.
int N
closed population
Matrix< T > r
(M x M) routing probabilities
std::vector< T > mu
(M) service rates
Return value of mapqn_bnd_lr_pf, mirroring the MATLAB result struct.
T objective
the bounded utilization of the objective station
std::vector< T > U
(M) utilizations at the optimal vertex
std::string status
simplex status name
std::vector< T > Q
(M) queue lengths at the optimal vertex