LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
mapqn_qr_bounds_rsrd.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_QR_BOUNDS_RSRD_H
6#define LINE_API_MAPQN_MAPQN_QR_BOUNDS_RSRD_H
7
8/**
9 * @file
10 * @ingroup api_mapqn
11 * Quadratic-reduction bound on the utilization of one queue of a closed MAP
12 * queueing network under RS-RD blocking (repetitive service, random
13 * destination).
14 *
15 * Templated port of matlab/lib/qrf/qrf_rsrd.m (ground truth), whose own origin
16 * is the AMPL model qrboundsrsrd_skel.mod.
17 *
18 * WHAT RS-RD MEANS HERE. A job completing at i and routed to a FULL destination
19 * is not held: service is repeated at i and a fresh destination drawn. Nothing
20 * is blocked in the BAS sense, so unlike mapqn_qr_bounds_bas there is no
21 * blocking-configuration index and the model is far narrower. What the blocking
22 * costs instead is EFFECTIVE service: Ueff(i,k,n) subtracts the mass whose
23 * chosen destination was full, and pb(i) accumulates the difference.
24 *
25 * ITS HALF-INDEX IS NOT THE FAMILY'S. Populations run 0..F(i), not 0..N, so the
26 * pairwise half-index is built on F(i)+1 and this model cannot share a layout
27 * with any other mapqn bound. Every population loop below is capped by F, and
28 * that is deliberate, not an optimization.
29 *
30 * THE ONE TRAP, and it is invisible in the output. THM1 is AGGREGATED over
31 * nj >= 1: ONE row per (j,kj), not one per (j,kj,nj). The per-nj form is
32 * strictly stronger, solves cleanly, and over-tightens the polytope -- on the
33 * paper's M = 5, N = 20 instance it returns U1min = 0.92508 against the
34 * published 0.87058. THM1c is the separate nj = 0 row and is NOT part of that
35 * aggregation. The reference carries this warning in its own comment; it is
36 * repeated here because a porter reading only the loop nest would not see it.
37 *
38 * LOAD DEPENDENCE IS LIVE. Unlike qrf_bas, q here carries alpha(i,n), so the
39 * rate depends on the population at the departing queue. The reference indexes
40 * it as q{i,j}(k,h,n+1), a 1-based population axis whose first slot is
41 * population 0; rsrd_rate below takes the population directly.
42 *
43 * ARITHMETIC. Assembly is +, -, * on the model data and lp::simplex_solve uses
44 * Bland's rule with no tolerance, so at T = line::Rational the returned bound
45 * is the EXACT optimum of the exact polytope.
46 *
47 * COST. B^2 + 2 sum_i K(i) F(i) + M columns with B = sum_i (F(i)+1) K(i). No
48 * MR factor, so it is much smaller than the BAS model at equal size, but the
49 * tableau is still dense: see the ceiling recorded in _kb/03-api-layer.md.
50 */
51
52#include <cstddef>
53#include <string>
54#include <vector>
55
57#include "line/num/number.h"
58#include "line/util/error.h"
59#include "line/util/matrix.h"
60#include "line/util/lp_highs.h"
61#include "line/util/simplex.h"
62
63namespace line {
64namespace mapqn {
65
66/** Parameters of the RS-RD bound, mirroring the reference's `params`. */
67template <class T>
69 int M = 0; ///< number of queues
70 int N = 0; ///< total population
71 std::vector<int> F; ///< (M) capacity of each queue
72 std::vector<int> K; ///< (M) number of phases of each queue
73 std::vector<Matrix<T>> mu; ///< mu[i] is K(i) x K(i), completion rates
74 std::vector<Matrix<T>> v; ///< v[i] is K(i) x K(i), background rates
75 Matrix<T> r; ///< (M x M) routing probabilities
76 std::vector<std::vector<T> > alpha; ///< optional (M) x (N+1) load scalings; empty means 1
77
78 void validate() const {
79 if (M <= 0) throw InputError("qrf_rsrd: M must be positive");
80 if (N < 1) throw InputError("qrf_rsrd: N must be at least 1");
81 if (static_cast<int>(F.size()) != M) throw InputError("qrf_rsrd: F has the wrong length");
82 if (static_cast<int>(K.size()) != M) throw InputError("qrf_rsrd: K has the wrong length");
83 if (static_cast<int>(mu.size()) != M || static_cast<int>(v.size()) != M)
84 throw InputError("qrf_rsrd: mu and v must have one entry per queue");
85 for (int i = 0; i < M; ++i) {
86 if (K[i] <= 0) throw InputError("qrf_rsrd: every queue needs at least one phase");
87 if (F[i] < 1 || F[i] > N) throw InputError("qrf_rsrd: F(i) must lie in 1..N");
88 const std::size_t k = static_cast<std::size_t>(K[i]);
89 if (mu[i].rows() != k || mu[i].cols() != k)
90 throw InputError("qrf_rsrd: mu{i} must be K(i) x K(i)");
91 if (v[i].rows() != k || v[i].cols() != k)
92 throw InputError("qrf_rsrd: v{i} must be K(i) x K(i)");
93 }
94 if (r.rows() != static_cast<std::size_t>(M) || r.cols() != static_cast<std::size_t>(M))
95 throw InputError("qrf_rsrd: r must be M x M");
96 if (!alpha.empty() && static_cast<int>(alpha.size()) != M)
97 throw InputError("qrf_rsrd: alpha must have M rows when given");
98 }
99};
100
101/** Result of an RS-RD bound solve. */
102template <class T>
104 bool ok = false;
105 std::string status;
106 T objective = T(); ///< the bound on the utilization of the target queue
107 std::vector<T> U; ///< (M) utilization of each queue
108 std::vector<T> Ueff; ///< (M) effective utilization of each queue
109 std::vector<T> pb; ///< (M) blocking probability of each queue
110 std::vector<T> x;
111 std::size_t num_vars = 0;
112 std::size_t num_rows = 0;
113 std::size_t iterations = 0;
114};
115
116/**
117 * Variable layout: p2(j,nj,kj,i,ni,hi), then U(i,k,n) and Ueff(i,k,n) for
118 * n >= 1, then pb(i).
119 *
120 * The half-index runs over (queue, population 0..F, phase), so its stride is
121 * F(i)+1 rather than the N+1 the rest of the mapqn family uses.
122 */
124 int M = 0, N = 0;
125 std::vector<int> K, F, base, cumU;
126 std::size_t B = 0, off_U = 0, off_Ueff = 0, off_pb = 0, total = 0;
127
129 QrRsrdIndex(int m, int n, const std::vector<int>& k, const std::vector<int>& f)
130 : M(m), N(n), K(k), F(f) {
131 base.assign(static_cast<std::size_t>(M) + 1, 0);
132 cumU.assign(static_cast<std::size_t>(M) + 1, 0);
133 for (int i = 0; i < M; ++i) {
134 base[i + 1] = base[i] + (F[i] + 1) * K[i];
135 cumU[i + 1] = cumU[i] + K[i] * F[i];
136 }
137 B = static_cast<std::size_t>(base[M]);
138 off_U = B * B;
139 off_Ueff = off_U + static_cast<std::size_t>(cumU[M]);
140 off_pb = off_Ueff + static_cast<std::size_t>(cumU[M]);
141 total = off_pb + static_cast<std::size_t>(M);
142 }
143
144 std::size_t half(int i, int ni, int h) const {
145 return static_cast<std::size_t>(base[i]) +
146 static_cast<std::size_t>(ni) * static_cast<std::size_t>(K[i]) +
147 static_cast<std::size_t>(h);
148 }
149 std::size_t p2(int j, int nj, int kj, int i, int ni, int hi) const {
150 return half(j, nj, kj) * B + half(i, ni, hi);
151 }
152 /** n is 1-based here: U is only defined for a busy queue. */
153 std::size_t U(int i, int k, int n) const {
154 return off_U + static_cast<std::size_t>(cumU[i]) +
155 static_cast<std::size_t>(k) * static_cast<std::size_t>(F[i]) +
156 static_cast<std::size_t>(n - 1);
157 }
158 std::size_t Ueff(int i, int k, int n) const {
159 return off_Ueff + static_cast<std::size_t>(cumU[i]) +
160 static_cast<std::size_t>(k) * static_cast<std::size_t>(F[i]) +
161 static_cast<std::size_t>(n - 1);
162 }
163 std::size_t pb(int i) const { return off_pb + static_cast<std::size_t>(i); }
164 std::size_t num_vars() const { return total; }
165};
166
167namespace detail {
168
169/** q(i,j,k,h,n): the load-dependent rate at population n of queue i. */
170template <class T>
171T rsrd_rate(const QrRsrdParams<T>& p, int i, int j, int k, int h, int n) {
172 const std::size_t ki = static_cast<std::size_t>(k), hi = static_cast<std::size_t>(h);
173 const std::size_t ii = static_cast<std::size_t>(i), ji = static_cast<std::size_t>(j);
175 if (!p.alpha.empty() && n >= 0 &&
176 static_cast<std::size_t>(n) < p.alpha[static_cast<std::size_t>(i)].size())
177 a = p.alpha[static_cast<std::size_t>(i)][static_cast<std::size_t>(n)];
178 if (j != i) return T(a * p.r(ii, ji) * p.mu[i](ki, hi));
179 return T(a * (p.v[i](ki, hi) + p.r(ii, ii) * p.mu[i](ki, hi)));
180}
181
182/** Bounds: every variable lies in [0,1]. */
183template <class T>
184void rsrd_bounds(const QrRsrdParams<T>& p, const QrRsrdIndex& x, lp::LpModel<T>& m) {
185 const T one = num_traits<T>::from_int(1);
186 for (std::size_t j = 0; j < x.num_vars(); ++j) m.set_bounds(j, T(), one);
187}
188
189/** ONE: the diagonal marginal of each queue normalizes to one. */
190template <class T>
191void rsrd_one(const QrRsrdParams<T>& p, const QrRsrdIndex& x, lp::LpModel<T>& m) {
192 for (int j = 0; j < p.M; ++j) {
193 for (int nj = 0; nj <= p.F[j]; ++nj)
194 for (int kj = 0; kj < p.K[j]; ++kj) m.row_add_int(x.p2(j, nj, kj, j, nj, kj), 1);
195 m.emit_eq_int(1);
196 }
197}
198
199/**
200 * ZERO1/2/3/6/7: the states that carry no mass, pinned as ub = 0.
201 *
202 * ZERO6 and ZERO7 are the capacity-aware members: a pair (or a single queue)
203 * holding so few jobs that the REMAINING capacity of the network cannot absorb
204 * the rest of the population is impossible.
205 */
206template <class T>
207void rsrd_zero(const QrRsrdParams<T>& p, const QrRsrdIndex& x, lp::LpModel<T>& m) {
208 int totF = 0;
209 for (int i = 0; i < p.M; ++i) totF += p.F[i];
210 for (int j = 0; j < p.M; ++j) {
211 for (int nj = 0; nj <= p.F[j]; ++nj) {
212 for (int kj = 0; kj < p.K[j]; ++kj) {
213 for (int i = 0; i < p.M; ++i) {
214 for (int ni = 0; ni <= p.F[i]; ++ni) {
215 for (int hi = 0; hi < p.K[i]; ++hi) {
216 bool z = false;
217 if (i == j && nj == ni && hi != kj) z = true; // ZERO1
218 if (i == j && nj != ni) z = true; // ZERO2
219 if (i != j && nj + ni > p.N) z = true; // ZERO3
220 if (i != j && p.N - nj - ni > totF - p.F[i] - p.F[j])
221 z = true; // ZERO6
222 if (z) m.fix(x.p2(j, nj, kj, i, ni, hi), T());
223 }
224 }
225 }
226 }
227 }
228 for (int nj = 0; nj <= p.F[j]; ++nj) // ZERO7
229 for (int kj = 0; kj < p.K[j]; ++kj)
230 if (p.N - nj > totF - p.F[j]) m.fix(x.p2(j, nj, kj, j, nj, kj), T());
231 }
232}
233
234/** SYMMETRY: p2 is symmetric under swapping its halves. */
235template <class T>
236void rsrd_symmetry(const QrRsrdParams<T>& p, const QrRsrdIndex& x, lp::LpModel<T>& m) {
237 for (int j = 0; j < p.M; ++j)
238 for (int nj = 0; nj <= p.F[j]; ++nj)
239 for (int kj = 0; kj < p.K[j]; ++kj)
240 for (int i = j + 1; i < p.M; ++i)
241 for (int ni = 0; ni <= p.F[i]; ++ni)
242 for (int hi = 0; hi < p.K[i]; ++hi) {
243 const std::size_t a = x.p2(j, nj, kj, i, ni, hi);
244 const std::size_t b = x.p2(i, ni, hi, j, nj, kj);
245 if (a == b) continue;
246 m.row_add_int(a, 1);
247 m.row_add_int(b, -1);
248 m.emit_eq_int(0);
249 }
250}
251
252/** MARGINALS: the pairwise law agrees with its own marginal at every level. */
253template <class T>
254void rsrd_marginals(const QrRsrdParams<T>& p, const QrRsrdIndex& x, lp::LpModel<T>& m) {
255 for (int j = 0; j < p.M; ++j)
256 for (int kj = 0; kj < p.K[j]; ++kj)
257 for (int nj = 0; nj <= p.F[j]; ++nj)
258 for (int i = 0; i < p.M; ++i) {
259 if (i == j) continue;
260 m.row_add_int(x.p2(j, nj, kj, j, nj, kj), 1);
261 for (int ni = 0; ni <= p.F[i]; ++ni)
262 for (int hi = 0; hi < p.K[i]; ++hi)
263 m.row_add_int(x.p2(j, nj, kj, i, ni, hi), -1);
264 m.emit_eq_int(0);
265 }
266}
267
268/** UCLASSIC: U(i,k,n) is the diagonal of p2, the classical utilization. */
269template <class T>
270void rsrd_uclassic(const QrRsrdParams<T>& p, const QrRsrdIndex& x, lp::LpModel<T>& m) {
271 for (int i = 0; i < p.M; ++i)
272 for (int ki = 0; ki < p.K[i]; ++ki)
273 for (int ni = 1; ni <= p.F[i]; ++ni) {
274 m.row_add_int(x.U(i, ki, ni), 1);
275 m.row_add_int(x.p2(i, ni, ki, i, ni, ki), -1);
276 m.emit_eq_int(0);
277 }
278}
279
280/**
281 * UEFFS: the effective utilization removes the mass whose chosen destination is
282 * full, weighted by the routing probability of choosing it.
283 */
284template <class T>
285void rsrd_ueffs(const QrRsrdParams<T>& p, const QrRsrdIndex& x, lp::LpModel<T>& m) {
286 for (int i = 0; i < p.M; ++i)
287 for (int ki = 0; ki < p.K[i]; ++ki)
288 for (int ni = 1; ni <= p.F[i]; ++ni) {
289 m.row_add_int(x.Ueff(i, ki, ni), 1);
290 m.row_add_int(x.p2(i, ni, ki, i, ni, ki), -1);
291 for (int j = 0; j < p.M; ++j) {
292 if (j == i) continue;
293 if (!(p.r(static_cast<std::size_t>(i), static_cast<std::size_t>(j)) > T()))
294 continue;
295 for (int hj = 0; hj < p.K[j]; ++hj)
296 m.row_add(x.p2(i, ni, ki, j, p.F[j], hj),
297 p.r(static_cast<std::size_t>(i), static_cast<std::size_t>(j)));
298 }
299 m.emit_eq_int(0);
300 }
301}
302
303/** PBLOCK: pb(i) is the total mass lost to full destinations. */
304template <class T>
305void rsrd_pblock(const QrRsrdParams<T>& p, const QrRsrdIndex& x, lp::LpModel<T>& m) {
306 for (int i = 0; i < p.M; ++i) {
307 m.row_add_int(x.pb(i), 1);
308 for (int ki = 0; ki < p.K[i]; ++ki)
309 for (int ni = 1; ni <= p.F[i]; ++ni) {
310 m.row_add_int(x.U(i, ki, ni), -1);
311 m.row_add_int(x.Ueff(i, ki, ni), 1);
312 }
313 m.emit_eq_int(0);
314 }
315}
316
317/** PBB: pb(i) cannot exceed the probability that some destination is full. */
318template <class T>
319void rsrd_pbb(const QrRsrdParams<T>& p, const QrRsrdIndex& x, lp::LpModel<T>& m) {
320 for (int i = 0; i < p.M; ++i) {
321 m.row_add_int(x.pb(i), 1);
322 for (int j = 0; j < p.M; ++j) {
323 if (j == i) continue;
324 if (!(p.r(static_cast<std::size_t>(i), static_cast<std::size_t>(j)) > T())) continue;
325 for (int hj = 0; hj < p.K[j]; ++hj)
326 m.row_add_int(x.p2(j, p.F[j], hj, j, p.F[j], hj), -1);
327 }
328 m.emit_le_int(0);
329 }
330}
331
332/**
333 * THM2: phase balance on the EFFECTIVE utilizations for the routing-away terms
334 * and on the classical diagonal for the self-routing ones. A repeated service
335 * does not change the phase balance, which is why the two levels appear in one
336 * row.
337 */
338template <class T>
339void rsrd_thm2(const QrRsrdParams<T>& p, const QrRsrdIndex& x, lp::LpModel<T>& m) {
340 for (int i = 0; i < p.M; ++i) {
341 for (int ki = 0; ki < p.K[i]; ++ki) {
342 for (int ni = 1; ni <= p.F[i]; ++ni) {
343 for (int j = 0; j < p.M; ++j) {
344 if (j == i) continue;
345 for (int hi = 0; hi < p.K[i]; ++hi) {
346 if (hi == ki) continue;
347 m.row_add(x.Ueff(i, ki, ni), rsrd_rate(p, i, j, ki, hi, ni));
348 }
349 }
350 for (int hi = 0; hi < p.K[i]; ++hi) {
351 if (hi == ki) continue;
352 m.row_add(x.p2(i, ni, ki, i, ni, ki), rsrd_rate(p, i, i, ki, hi, ni));
353 }
354 }
355 for (int ni = 1; ni <= p.F[i]; ++ni) {
356 for (int j = 0; j < p.M; ++j) {
357 if (j == i) continue;
358 for (int hi = 0; hi < p.K[i]; ++hi) {
359 if (hi == ki) continue;
360 m.row_add(x.Ueff(i, hi, ni), T(-rsrd_rate(p, i, j, hi, ki, ni)));
361 }
362 }
363 for (int hi = 0; hi < p.K[i]; ++hi) {
364 if (hi == ki) continue;
365 m.row_add(x.p2(i, ni, hi, i, ni, hi), T(-rsrd_rate(p, i, i, hi, ki, ni)));
366 }
367 }
368 m.emit_eq_int(0);
369 }
370 }
371}
372
373/**
374 * THM1: the queue-length theorem, AGGREGATED over nj >= 1.
375 *
376 * ONE row per (j,kj). Emitting it per-nj is strictly stronger and silently
377 * over-tightens the polytope; see the header.
378 */
379template <class T>
380void rsrd_thm1(const QrRsrdParams<T>& p, const QrRsrdIndex& x, lp::LpModel<T>& m) {
381 for (int j = 0; j < p.M; ++j) {
382 for (int kj = 0; kj < p.K[j]; ++kj) {
383 for (int nj = 1; nj <= p.F[j]; ++nj) {
384 m.row_add_int(x.p2(j, nj, kj, j, nj, kj), -p.N);
385 for (int i = 0; i < p.M; ++i)
386 for (int ni = 1; ni <= p.F[i]; ++ni)
387 for (int hi = 0; hi < p.K[i]; ++hi)
388 m.row_add_int(x.p2(j, nj, kj, i, ni, hi), ni);
389 }
390 m.emit_eq_int(0);
391 }
392 }
393}
394
395/** THM1c: the same theorem at nj = 0, where only i != j can hold jobs. */
396template <class T>
397void rsrd_thm1c(const QrRsrdParams<T>& p, const QrRsrdIndex& x, lp::LpModel<T>& m) {
398 for (int j = 0; j < p.M; ++j) {
399 for (int kj = 0; kj < p.K[j]; ++kj) {
400 m.row_add_int(x.p2(j, 0, kj, j, 0, kj), -p.N);
401 for (int i = 0; i < p.M; ++i) {
402 if (i == j) continue;
403 for (int ni = 1; ni <= p.F[i]; ++ni)
404 for (int hi = 0; hi < p.K[i]; ++hi)
405 m.row_add_int(x.p2(j, 0, kj, i, ni, hi), ni);
406 }
407 m.emit_eq_int(0);
408 }
409 }
410}
411
412/** THM3a: level-crossing balance between ni and ni+1, for 1 <= ni <= F(i)-1. */
413template <class T>
414void rsrd_thm3a(const QrRsrdParams<T>& p, const QrRsrdIndex& x, lp::LpModel<T>& m) {
415 for (int i = 0; i < p.M; ++i) {
416 for (int ni = 1; ni <= p.F[i] - 1; ++ni) {
417 for (int j = 0; j < p.M; ++j) {
418 if (j == i) continue;
419 for (int kj = 0; kj < p.K[j]; ++kj)
420 for (int hj = 0; hj < p.K[j]; ++hj)
421 for (int ui = 0; ui < p.K[i]; ++ui)
422 for (int nj = 1; nj <= p.F[j]; ++nj)
423 m.row_add(x.p2(j, nj, kj, i, ni, ui),
424 rsrd_rate(p, j, i, kj, hj, nj));
425 }
426 for (int j = 0; j < p.M; ++j) {
427 if (j == i) continue;
428 for (int ki = 0; ki < p.K[i]; ++ki)
429 for (int hi = 0; hi < p.K[i]; ++hi)
430 for (int uj = 0; uj < p.K[j]; ++uj)
431 for (int nj = 0; nj <= p.F[j] - 1; ++nj)
432 m.row_add(x.p2(i, ni + 1, ki, j, nj, uj),
433 T(-rsrd_rate(p, i, j, ki, hi, ni + 1)));
434 }
435 m.emit_eq_int(0);
436 }
437 }
438}
439
440/** THM3b: the same balance at ni = 0, per arrival phase. */
441template <class T>
442void rsrd_thm3b(const QrRsrdParams<T>& p, const QrRsrdIndex& x, lp::LpModel<T>& m) {
443 for (int i = 0; i < p.M; ++i) {
444 for (int ui = 0; ui < p.K[i]; ++ui) {
445 for (int j = 0; j < p.M; ++j) {
446 if (j == i) continue;
447 for (int kj = 0; kj < p.K[j]; ++kj)
448 for (int hj = 0; hj < p.K[j]; ++hj)
449 for (int nj = 1; nj <= p.F[j]; ++nj)
450 m.row_add(x.p2(j, nj, kj, i, 0, ui), rsrd_rate(p, j, i, kj, hj, nj));
451 }
452 for (int j = 0; j < p.M; ++j) {
453 if (j == i) continue;
454 for (int ki = 0; ki < p.K[i]; ++ki)
455 for (int nj = 0; nj <= p.F[j] - 1; ++nj)
456 for (int hj = 0; hj < p.K[j]; ++hj)
457 m.row_add(x.p2(i, 1, ki, j, nj, hj),
458 T(-rsrd_rate(p, i, j, ki, ui, 1)));
459 }
460 m.emit_eq_int(0);
461 }
462 }
463}
464
465/** QBAL: queue balance, six terms, three on each side. */
466template <class T>
467void rsrd_qbal(const QrRsrdParams<T>& p, const QrRsrdIndex& x, lp::LpModel<T>& m) {
468 for (int i = 0; i < p.M; ++i) {
469 for (int ki = 0; ki < p.K[i]; ++ki) {
470 for (int hi = 0; hi < p.K[i]; ++hi) { // LHS 1
471 if (hi == ki) continue;
472 for (int j = 0; j < p.M; ++j) {
473 if (j == i) continue;
474 for (int ni = 1; ni <= p.F[i]; ++ni)
475 for (int uj = 0; uj < p.K[j]; ++uj)
476 for (int nj = 0; nj <= p.F[j] - 1; ++nj)
477 m.row_add(x.p2(i, ni, ki, j, nj, uj),
478 T(rsrd_rate(p, i, j, ki, hi, ni) *
479 num_traits<T>::from_int(ni)));
480 }
481 }
482 for (int hi = 0; hi < p.K[i]; ++hi) { // LHS 2
483 if (hi == ki) continue;
484 for (int ni = 1; ni <= p.F[i]; ++ni)
485 m.row_add(x.p2(i, ni, ki, i, ni, ki),
486 T(rsrd_rate(p, i, i, ki, hi, ni) * num_traits<T>::from_int(ni)));
487 }
488 for (int j = 0; j < p.M; ++j) { // LHS 3
489 if (j == i) continue;
490 for (int hi = 0; hi < p.K[i]; ++hi)
491 for (int ni = 1; ni <= p.F[i]; ++ni)
492 for (int uj = 0; uj < p.K[j]; ++uj) {
493 const int cap =
494 (p.F[j] - 1) < (p.N - ni) ? (p.F[j] - 1) : (p.N - ni);
495 for (int nj = 0; nj <= cap; ++nj)
496 m.row_add(x.p2(i, ni, hi, j, nj, uj),
497 rsrd_rate(p, i, j, hi, ki, ni));
498 }
499 }
500 for (int j = 0; j < p.M; ++j) { // RHS 1
501 if (j == i) continue;
502 for (int hj = 0; hj < p.K[j]; ++hj)
503 for (int ni = 0; ni <= p.F[i] - 1; ++ni)
504 for (int uj = 0; uj < p.K[j]; ++uj)
505 for (int nj = 1; nj <= p.F[j]; ++nj)
506 m.row_add(x.p2(i, ni, ki, j, nj, hj),
507 T(-rsrd_rate(p, j, i, hj, uj, nj)));
508 }
509 for (int hi = 0; hi < p.K[i]; ++hi) { // RHS 2
510 if (hi == ki) continue;
511 for (int ni = 1; ni <= p.F[i]; ++ni)
512 m.row_add(x.p2(i, ni, hi, i, ni, hi),
513 T(-(rsrd_rate(p, i, i, hi, ki, ni) * num_traits<T>::from_int(ni))));
514 }
515 for (int hi = 0; hi < p.K[i]; ++hi) { // RHS 3
516 if (hi == ki) continue;
517 for (int j = 0; j < p.M; ++j) {
518 if (j == i) continue;
519 for (int ni = 1; ni <= p.F[i]; ++ni)
520 for (int uj = 0; uj < p.K[j]; ++uj)
521 for (int nj = 0; nj <= p.F[j] - 1; ++nj)
522 m.row_add(x.p2(i, ni, hi, j, nj, uj),
523 T(-(rsrd_rate(p, i, j, hi, ki, ni) *
524 num_traits<T>::from_int(ni))));
525 }
526 }
527 m.emit_eq_int(0);
528 }
529 }
530}
531
532/**
533 * THM4: the QMIN inequality. The reference accumulates
534 * (sum nt p2 - N sum p2) and emits the NEGATED row, so the constraint is
535 * N P(j at (nj,kj), i nonempty) <= sum_t E[n_t ...].
536 */
537template <class T>
538void rsrd_thm4(const QrRsrdParams<T>& p, const QrRsrdIndex& x, lp::LpModel<T>& m) {
539 for (int j = 0; j < p.M; ++j)
540 for (int kj = 0; kj < p.K[j]; ++kj)
541 for (int i = 0; i < p.M; ++i) {
542 for (int t = 0; t < p.M; ++t)
543 for (int ht = 0; ht < p.K[t]; ++ht)
544 for (int nj = 0; nj <= p.F[j]; ++nj)
545 for (int nt = 1; nt <= p.F[t]; ++nt)
546 m.row_add_int(x.p2(j, nj, kj, t, nt, ht), -nt);
547 for (int hi = 0; hi < p.K[i]; ++hi)
548 for (int nj = 0; nj <= p.F[j]; ++nj)
549 for (int ni = 1; ni <= p.F[i]; ++ni)
550 m.row_add_int(x.p2(j, nj, kj, i, ni, hi), p.N);
551 m.emit_le_int(0);
552 }
553}
554
555} // namespace detail
556
557/**
558 * Bound the utilization of one queue over the RS-RD polytope.
559 *
560 * @param p network parameters, all 0-based
561 * @param objective_queue queue index, 0..M-1
562 * @param sense Max for an upper bound, Min for a lower bound
563 */
564template <class T>
566 MapqnSense sense = MapqnSense::Min) {
567 p.validate();
568 if (objective_queue < 0 || objective_queue >= p.M)
569 throw InputError("qrf_rsrd: objective_queue out of range");
570
571 const QrRsrdIndex x(p.M, p.N, p.K, p.F);
573 detail::rsrd_bounds(p, x, m);
574
575 detail::rsrd_one(p, x, m);
576 detail::rsrd_zero(p, x, m);
577 detail::rsrd_symmetry(p, x, m);
578 detail::rsrd_marginals(p, x, m);
579 detail::rsrd_uclassic(p, x, m);
580 detail::rsrd_ueffs(p, x, m);
581 detail::rsrd_pblock(p, x, m);
582 detail::rsrd_pbb(p, x, m);
583 detail::rsrd_thm2(p, x, m);
584 detail::rsrd_thm1(p, x, m);
585 detail::rsrd_thm1c(p, x, m);
586 detail::rsrd_thm3a(p, x, m);
587 detail::rsrd_thm3b(p, x, m);
588 detail::rsrd_qbal(p, x, m);
589 detail::rsrd_thm4(p, x, m);
590
591 const T one = num_traits<T>::from_int(1);
592 for (int ki = 0; ki < p.K[objective_queue]; ++ki)
593 for (int ni = 1; ni <= p.F[objective_queue]; ++ni)
594 m.set_cost(x.p2(objective_queue, ni, ki, objective_queue, ni, ki), one);
595 m.set_maximize(sense == MapqnSense::Max);
596
597 // lp_solve, not simplex_solve: this model outgrows the dense tableau
598 // quickly, so a double instantiation hands wide models to HiGHS while
599 // Rational stays on the exact path at compile time.
600 const lp::LpSolution<T> sol = lp::lp_solve(m);
601
602 QrRsrdResult<T> out;
604 out.ok = sol.ok();
605 out.objective = sol.objective;
606 out.x = sol.x;
607 out.num_vars = m.num_vars();
608 out.num_rows = m.num_rows();
609 out.iterations = sol.iterations;
610 if (!out.ok) return out;
611
612 out.U.assign(static_cast<std::size_t>(p.M), T());
613 out.Ueff.assign(static_cast<std::size_t>(p.M), T());
614 out.pb.assign(static_cast<std::size_t>(p.M), T());
615 for (int i = 0; i < p.M; ++i) {
616 for (int ki = 0; ki < p.K[i]; ++ki) {
617 for (int ni = 1; ni <= p.F[i]; ++ni) {
618 out.U[static_cast<std::size_t>(i)] += sol.x[x.U(i, ki, ni)];
619 out.Ueff[static_cast<std::size_t>(i)] += sol.x[x.Ueff(i, ki, ni)];
620 }
621 }
622 out.pb[static_cast<std::size_t>(i)] = sol.x[x.pb(i)];
623 }
624 return out;
625}
626
627} // namespace mapqn
628} // namespace line
629
630#endif // LINE_API_MAPQN_MAPQN_QR_BOUNDS_RSRD_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.
A sparse LP backend for line::lp::LpModel, on HiGHS (MIT).
Model parameters and variable indexing shared by the mapqn QR bounds.
Dense matrix and non-owning view.
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
MapqnSense
Which direction the bound is taken in.
QrRsrdResult< T > mapqn_qr_bounds_rsrd(const QrRsrdParams< T > &p, int objective_queue, MapqnSense sense=MapqnSense::Min)
Bound the utilization of one queue over the RS-RD polytope.
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: p2(j,nj,kj,i,ni,hi), then U(i,k,n) and Ueff(i,k,n) for n >= 1, then pb(i).
std::size_t U(int i, int k, int n) const
n is 1-based here: U is only defined for a busy queue.
QrRsrdIndex(int m, int n, const std::vector< int > &k, const std::vector< int > &f)
std::size_t half(int i, int ni, int h) const
std::size_t pb(int i) const
std::size_t p2(int j, int nj, int kj, int i, int ni, int hi) const
std::size_t Ueff(int i, int k, int n) const
Parameters of the RS-RD bound, mirroring the reference's params.
std::vector< int > K
(M) number of phases of each queue
std::vector< Matrix< T > > mu
mu[i] is K(i) x K(i), completion rates
std::vector< Matrix< T > > v
v[i] is K(i) x K(i), background rates
std::vector< int > F
(M) capacity of each queue
Matrix< T > r
(M x M) routing probabilities
std::vector< std::vector< T > > alpha
optional (M) x (N+1) load scalings; empty means 1
Result of an RS-RD bound solve.
T objective
the bound on the utilization of the target queue
std::vector< T > pb
(M) blocking probability of each queue
std::vector< T > Ueff
(M) effective utilization of each queue
std::vector< T > U
(M) utilization of each queue