LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
mapqn_qr_bounds_bas.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_BAS_H
6#define LINE_API_MAPQN_MAPQN_QR_BOUNDS_BAS_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 with a FINITE-CAPACITY station under blocking-after-service.
13 *
14 * Templated port of matlab/lib/qrf/qrf_bas.m (ground truth), whose own origin is
15 * the AMPL model qrboundsbas_skel.mod.
16 *
17 * WHAT BAS MEANS HERE. Station f has capacity F(f). A job completing at station
18 * j and routed to a full f cannot move, so j is BLOCKED: it holds the completed
19 * job and serves nothing until f frees a slot. The set of currently blocked
20 * stations, and the order in which they blocked, is the blocking configuration
21 * m in 0..MR-1. Every probability variable carries it, which is what separates
22 * this model from mapqn_bnd_qr_ld: the state is (pairwise occupancy, blocking
23 * configuration), not occupancy alone.
24 *
25 * THE BLOCKING TABLES ARE SUPPLIED, NOT DERIVED. BB, MM, ZZ, ZM and MM1 are
26 * caller data here exactly as they are in the reference, where
27 * example_bas_small.m writes them out literally. Deriving them from (M, f)
28 * would be a different routine and is deliberately not attempted.
29 *
30 * FAMILY-BY-FAMILY TRAPS, all verified against the reference and recorded in
31 * _kb/03-api-layer.md. Read them before editing any family below:
32 * - THM30 and THM3 do NOT share a loop nest. THM30's RHS sums over hj with a
33 * coefficient independent of hj (a pure multiplicity); THM3's sums over hi
34 * with a coefficient that depends on it. Copying one into the other scales
35 * the row by K and still solves.
36 * - THM3f is pinned to configuration 0, not looped over m: below capacity
37 * there is no blocking, so only the unblocked configuration contributes.
38 * - THM3L couples TWO configurations through mp = MM1(m,j), and its RHS
39 * variable is the diagonal in mp, not in m.
40 * - THM4 is accumulated as (sum nt p2 - N sum p2) and then NEGATED, giving
41 * N P(j at (nj,kj), i nonempty) <= sum_t E[n_t ...].
42 * - SYMMETRY skips pairs both of whose members are already pinned to zero, so
43 * it must run AFTER the ZERO pass. The emission order below is load bearing.
44 *
45 * UPPER BOUNDS ARE INFINITE, unlike every other bound in this family. The
46 * reference initializes ub = inf and only the ZERO families pin anything; the
47 * variables are bounded above through ONE instead. lp::LpModel's default is
48 * exactly lb = 0 with a free upper bound, so nothing is set here beyond the
49 * ZERO pins.
50 *
51 * COST. MR * B^2 + sum_i K(i) columns with B = (N+1) sum_i K(i), so the model
52 * is MR times the load-dependent one. The tableau here is dense, which confines
53 * the port to small instances; the reference's own paper instance is ~6e4
54 * columns and needs a sparse revised simplex.
55 */
56
57#include <cstddef>
58#include <string>
59#include <vector>
60
62#include "line/num/number.h"
63#include "line/util/error.h"
64#include "line/util/matrix.h"
65#include "line/util/lp_highs.h"
66#include "line/util/simplex.h"
67
68namespace line {
69namespace mapqn {
70
71/**
72 * Parameters of the BAS bound, mirroring the reference's `params`.
73 *
74 * Queues, phases and blocking configurations are 0-based here; the reference is
75 * 1-based in all three, so `f`, the entries of `MM` and the entries of `MM1`
76 * all drop by one relative to a MATLAB script. `MM1` keeps the reference's
77 * "absent" marker as a negative entry rather than 0.
78 */
79template <class T>
81 int M = 0; ///< number of queues
82 int N = 0; ///< total population
83 int f = 0; ///< index of the finite-capacity queue, 0-based
84 std::vector<int> F; ///< (M) capacity of each queue
85 std::vector<int> K; ///< (M) number of phases of each queue
86 std::vector<Matrix<T>> mu; ///< mu[i] is K(i) x K(i), completion rates
87 std::vector<Matrix<T>> v; ///< v[i] is K(i) x K(i), background rates
88 Matrix<T> r; ///< (M x M) routing probabilities
89 int MR = 0; ///< number of blocking configurations
90 std::vector<std::vector<int> > BB; ///< (MR x M) 1 if queue i is blocked in m
91 std::vector<std::vector<int> > MM; ///< (MR x 2) blocking order, 0-based queue indices
92 std::vector<int> ZZ; ///< (MR) number of blocked queues in m
93 int ZM = 0; ///< maximum blocking depth
94 std::vector<std::vector<int> > MM1; ///< (MR x M) extended order; negative means absent
95
96 void validate() const {
97 if (M <= 0) throw InputError("qrf_bas: M must be positive");
98 if (N < 1) throw InputError("qrf_bas: N must be at least 1");
99 if (f < 0 || f >= M) throw InputError("qrf_bas: f out of range");
100 if (static_cast<int>(F.size()) != M) throw InputError("qrf_bas: F has the wrong length");
101 if (static_cast<int>(K.size()) != M) throw InputError("qrf_bas: K has the wrong length");
102 if (static_cast<int>(mu.size()) != M || static_cast<int>(v.size()) != M)
103 throw InputError("qrf_bas: mu and v must have one entry per queue");
104 for (int i = 0; i < M; ++i) {
105 if (K[i] <= 0) throw InputError("qrf_bas: every queue needs at least one phase");
106 if (F[i] < 0 || F[i] > N) throw InputError("qrf_bas: F(i) must lie in 0..N");
107 const std::size_t k = static_cast<std::size_t>(K[i]);
108 if (mu[i].rows() != k || mu[i].cols() != k)
109 throw InputError("qrf_bas: mu{i} must be K(i) x K(i)");
110 if (v[i].rows() != k || v[i].cols() != k)
111 throw InputError("qrf_bas: v{i} must be K(i) x K(i)");
112 }
113 if (r.rows() != static_cast<std::size_t>(M) || r.cols() != static_cast<std::size_t>(M))
114 throw InputError("qrf_bas: r must be M x M");
115 if (MR < 1) throw InputError("qrf_bas: MR must be at least 1");
116 if (static_cast<int>(BB.size()) != MR || static_cast<int>(MM.size()) != MR ||
117 static_cast<int>(ZZ.size()) != MR || static_cast<int>(MM1.size()) != MR)
118 throw InputError("qrf_bas: the blocking tables must have MR rows");
119 for (int m = 0; m < MR; ++m) {
120 if (static_cast<int>(BB[m].size()) != M || static_cast<int>(MM1[m].size()) != M)
121 throw InputError("qrf_bas: BB and MM1 must have M columns");
122 if (MM[m].size() < 2) throw InputError("qrf_bas: MM must have two columns");
123 }
124 // ZM IS max(ZZ), and a larger one couples THM3I to a depth that has no
125 // configuration, emptying the polytope instead of failing cleanly. Zero
126 // is legal and means no configuration blocks anything, which is what
127 // `qrf_bas.m` runs when its `for z = 0:(ZM-1)` is empty.
128 int zmax = 0;
129 for (std::size_t m = 0; m < ZZ.size(); ++m) zmax = std::max(zmax, ZZ[m]);
130 if (ZM != zmax)
131 throw InputError("qrf_bas: ZM must equal max(ZZ) (got ZM = " + std::to_string(ZM) +
132 ", max(ZZ) = " + std::to_string(zmax) + ")");
133 }
134};
135
136/** Result of a BAS bound solve. */
137template <class T>
139 bool ok = false; ///< the LP reached an optimal vertex
140 std::string status; ///< textual LP status
141 T objective = T(); ///< the bound on the utilization of the target queue
142 std::vector<T> U; ///< (M) utilization of each queue at the optimal vertex
143 /**
144 * (M) P(n_i >= 1) over EVERY configuration, blocked ones included. This is
145 * what `U` used to hold; at a BAS station it counts a blocked server as
146 * busy, so it is occupancy and NOT utilization. Kept because it is the
147 * quantity the QRF papers report.
148 */
149 std::vector<T> occupancy;
150 Matrix<T> e; ///< M x max(K) effective per-phase utilizations
151 std::vector<T> x; ///< full solution vector, indexed by QrBasIndex
152 std::size_t num_vars = 0;
153 std::size_t num_rows = 0;
154 std::size_t iterations = 0;
155};
156
157/**
158 * Variable layout: p2(j,nj,kj,i,ni,hi,m) then e(i,ki).
159 *
160 * The pairwise half-index is the one the rest of the mapqn family uses, so the
161 * blocking configuration is the FASTEST varying subscript here. The reference
162 * nests it differently (j, nj, kj, i, m, ni, hi); order does not affect the
163 * polytope, only how the two assemblies diff.
164 */
166 int M = 0, N = 0, MR = 0;
167 std::vector<int> K;
168 std::vector<int> cumK;
169 std::size_t sumK = 0, block = 0, off_e = 0, total = 0;
170
172 QrBasIndex(int m, int n, const std::vector<int>& k, int mr) : M(m), N(n), MR(mr), K(k) {
173 cumK.assign(static_cast<std::size_t>(M) + 1, 0);
174 for (int i = 0; i < M; ++i) cumK[i + 1] = cumK[i] + K[i];
175 sumK = static_cast<std::size_t>(cumK[M]);
176 block = static_cast<std::size_t>(N + 1) * sumK;
177 off_e = block * block * static_cast<std::size_t>(MR);
178 total = off_e + sumK;
179 }
180
181 std::size_t half(int i, int ni, int h) const {
182 return static_cast<std::size_t>(N + 1) * static_cast<std::size_t>(cumK[i]) +
183 static_cast<std::size_t>(ni) * static_cast<std::size_t>(K[i]) +
184 static_cast<std::size_t>(h);
185 }
186 std::size_t p2(int j, int nj, int kj, int i, int ni, int hi, int m) const {
187 return (half(j, nj, kj) * block + half(i, ni, hi)) * static_cast<std::size_t>(MR) +
188 static_cast<std::size_t>(m);
189 }
190 std::size_t e(int i, int ki) const {
191 return off_e + static_cast<std::size_t>(cumK[i]) + static_cast<std::size_t>(ki);
192 }
193 std::size_t num_vars() const { return total; }
194};
195
196namespace detail {
197
198/** q(i,j,k,h), the same non-load-dependent rate the rest of the family uses. */
199template <class T>
200T bas_rate(const QrBasParams<T>& p, int i, int j, int k, int h) {
201 const std::size_t ki = static_cast<std::size_t>(k), hi = static_cast<std::size_t>(h);
202 const std::size_t ii = static_cast<std::size_t>(i), ji = static_cast<std::size_t>(j);
203 if (j != i) return T(p.r(ii, ji) * p.mu[i](ki, hi));
204 return T(p.v[i](ki, hi) + p.r(ii, ii) * p.mu[i](ki, hi));
205}
206
207/**
208 * ZERO1..ZERO8: the states that carry no mass, pinned as ub = 0.
209 *
210 * Returns the indicator, because SYMMETRY skips pairs both of whose members are
211 * pinned and therefore depends on this pass having run.
212 */
213template <class T>
214std::vector<char> bas_zero(const QrBasParams<T>& p, const QrBasIndex& x, lp::LpModel<T>& m) {
215 std::vector<char> zero(x.num_vars(), 0);
216 for (int j = 0; j < p.M; ++j) {
217 for (int nj = 0; nj <= p.N; ++nj) {
218 for (int kj = 0; kj < p.K[j]; ++kj) {
219 for (int i = 0; i < p.M; ++i) {
220 for (int ni = 0; ni <= p.N; ++ni) {
221 for (int hi = 0; hi < p.K[i]; ++hi) {
222 for (int mm = 0; mm < p.MR; ++mm) {
223 bool z = false;
224 if (i == j && nj == ni && hi != kj) z = true; // ZERO1
225 if (i == j && nj != ni) z = true; // ZERO2
226 if (i != j && nj + ni > p.N) z = true; // ZERO3
227 if (nj > p.F[j]) z = true; // ZERO6
228 if (mm >= 1 && p.BB[mm][j] == 1 && nj == 0) z = true; // ZERO5
229 if (mm >= 1 && p.BB[mm][j] == 1 && i != j && i != p.f &&
230 ni + nj + p.F[p.f] > p.N)
231 z = true; // ZERO7
232 if (j == p.f && nj >= 1 && nj <= p.F[p.f] - 1 && mm >= 1)
233 z = true; // ZERO8
234 if (z) {
235 const std::size_t idx = x.p2(j, nj, kj, i, ni, hi, mm);
236 m.fix(idx, T());
237 zero[idx] = 1;
238 }
239 }
240 }
241 }
242 }
243 }
244 }
245 }
246 // ZERO4: for m >= 1 and j != f, the finite queue below capacity carries no
247 // mass in a blocking configuration.
248 for (int j = 0; j < p.M; ++j) {
249 if (j == p.f) continue;
250 for (int nj = 0; nj <= p.N; ++nj) {
251 for (int kj = 0; kj < p.K[j]; ++kj) {
252 for (int mm = 1; mm < p.MR; ++mm) {
253 for (int nf = 0; nf <= p.F[p.f] - 1; ++nf) {
254 for (int hf = 0; hf < p.K[p.f]; ++hf) {
255 const std::size_t idx = x.p2(j, nj, kj, p.f, nf, hf, mm);
256 m.fix(idx, T());
257 zero[idx] = 1;
258 }
259 }
260 }
261 }
262 }
263 }
264 return zero;
265}
266
267/** ONE: the diagonal marginal of each queue normalizes to one. */
268template <class T>
269void bas_one(const QrBasParams<T>& p, const QrBasIndex& x, lp::LpModel<T>& m) {
270 for (int j = 0; j < p.M; ++j) {
271 for (int nj = 0; nj <= p.N; ++nj)
272 for (int kj = 0; kj < p.K[j]; ++kj)
273 for (int mm = 0; mm < p.MR; ++mm) m.row_add_int(x.p2(j, nj, kj, j, nj, kj, mm), 1);
274 m.emit_eq_int(1);
275 }
276}
277
278/** SYMMETRY: p2 is symmetric under swapping its halves, within a configuration. */
279template <class T>
280void bas_symmetry(const QrBasParams<T>& p, const QrBasIndex& x, lp::LpModel<T>& m,
281 const std::vector<char>& zero) {
282 for (int j = 0; j < p.M; ++j) {
283 const int njmax = p.N < p.F[j] ? p.N : p.F[j];
284 for (int nj = 0; nj <= njmax; ++nj) {
285 for (int kj = 0; kj < p.K[j]; ++kj) {
286 for (int i = j + 1; i < p.M; ++i) {
287 const int nimax = p.N < p.F[i] ? p.N : p.F[i];
288 for (int ni = 0; ni <= nimax; ++ni) {
289 if (nj + ni > p.N) continue;
290 for (int hi = 0; hi < p.K[i]; ++hi) {
291 for (int mm = 0; mm < p.MR; ++mm) {
292 const std::size_t a = x.p2(j, nj, kj, i, ni, hi, mm);
293 const std::size_t b = x.p2(i, ni, hi, j, nj, kj, mm);
294 if (zero[a] && zero[b]) continue;
295 if (a == b) continue;
296 m.row_add_int(a, 1);
297 m.row_add_int(b, -1);
298 m.emit_eq_int(0);
299 }
300 }
301 }
302 }
303 }
304 }
305 }
306}
307
308/** MARGINALS: the pairwise law agrees with its own marginal at every level. */
309template <class T>
310void bas_marginals(const QrBasParams<T>& p, const QrBasIndex& x, lp::LpModel<T>& m) {
311 for (int j = 0; j < p.M; ++j) {
312 for (int kj = 0; kj < p.K[j]; ++kj) {
313 const int njmax = p.N < p.F[j] ? p.N : p.F[j];
314 for (int nj = 0; nj <= njmax; ++nj) {
315 for (int i = 0; i < p.M; ++i) {
316 if (i == j) continue;
317 for (int mm = 0; mm < p.MR; ++mm) {
318 m.row_add_int(x.p2(j, nj, kj, j, nj, kj, mm), 1);
319 const int cap = (p.N - nj) < p.F[i] ? (p.N - nj) : p.F[i];
320 for (int ni = 0; ni <= cap; ++ni)
321 for (int hi = 0; hi < p.K[i]; ++hi)
322 m.row_add_int(x.p2(j, nj, kj, i, ni, hi, mm), -1);
323 m.emit_eq_int(0);
324 }
325 }
326 }
327 }
328 }
329}
330
331/**
332 * UEFF: e(i,ki) is the probability that queue i is busy in phase ki AND NOT
333 * blocked. It is the effective utilization, which is what the objective and
334 * THM1 read, and it is why a blocked station contributes nothing to throughput.
335 */
336template <class T>
337void bas_ueff(const QrBasParams<T>& p, const QrBasIndex& x, lp::LpModel<T>& m) {
338 for (int i = 0; i < p.M; ++i) {
339 for (int ki = 0; ki < p.K[i]; ++ki) {
340 m.row_add_int(x.e(i, ki), -1);
341 for (int j = 0; j < p.M; ++j) {
342 const int njmax = p.N < p.F[j] ? p.N : p.F[j];
343 for (int nj = 0; nj <= njmax; ++nj) {
344 for (int kj = 0; kj < p.K[j]; ++kj) {
345 for (int mm = 0; mm < p.MR; ++mm) {
346 if (p.BB[mm][i] != 0) continue;
347 const int nimax = p.N < p.F[i] ? p.N : p.F[i];
348 for (int ni = 1; ni <= nimax; ++ni)
349 m.row_add_int(x.p2(j, nj, kj, i, ni, ki, mm), 1);
350 }
351 }
352 }
353 }
354 m.emit_eq_int(0);
355 }
356 }
357}
358
359/** THM1: phase balance on the effective utilizations. */
360template <class T>
361void bas_thm1(const QrBasParams<T>& p, const QrBasIndex& x, lp::LpModel<T>& m) {
362 for (int i = 0; i < p.M; ++i) {
363 for (int ki = 0; ki < p.K[i]; ++ki) {
364 for (int j = 0; j < p.M; ++j)
365 for (int hi = 0; hi < p.K[i]; ++hi)
366 if (j != i || hi != ki) m.row_add(x.e(i, ki), bas_rate(p, i, j, ki, hi));
367 for (int j = 0; j < p.M; ++j)
368 for (int hi = 0; hi < p.K[i]; ++hi)
369 if (j != i || hi != ki)
370 m.row_add(x.e(i, hi), T(-bas_rate(p, i, j, hi, ki)));
371 m.emit_eq_int(0);
372 }
373 }
374}
375
376/** THM2: the queue-length theorem conditioned on (j,nj,kj,m). */
377template <class T>
378void bas_thm2(const QrBasParams<T>& p, const QrBasIndex& x, lp::LpModel<T>& m) {
379 for (int j = 0; j < p.M; ++j) {
380 for (int kj = 0; kj < p.K[j]; ++kj) {
381 for (int nj = 0; nj <= p.F[j]; ++nj) {
382 for (int mm = 0; mm < p.MR; ++mm) {
383 m.row_add_int(x.p2(j, nj, kj, j, nj, kj, mm), -p.N);
384 for (int i = 0; i < p.M; ++i)
385 for (int ni = 1; ni <= p.F[i]; ++ni)
386 for (int ki = 0; ki < p.K[i]; ++ki)
387 m.row_add_int(x.p2(j, nj, kj, i, ni, ki, mm), ni);
388 m.emit_eq_int(0);
389 }
390 }
391 }
392 }
393}
394
395/** COR1: the second moment of the population. */
396template <class T>
397void bas_cor1(const QrBasParams<T>& p, const QrBasIndex& x, lp::LpModel<T>& m) {
398 for (int mm = 0; mm < p.MR; ++mm)
399 for (int i = 0; i < p.M; ++i)
400 for (int j = 0; j < p.M; ++j)
401 for (int nj = 1; nj <= p.F[j]; ++nj)
402 for (int ni = 1; ni <= p.F[i]; ++ni)
403 for (int ki = 0; ki < p.K[i]; ++ki)
404 for (int kj = 0; kj < p.K[j]; ++kj)
405 m.row_add_int(x.p2(j, nj, kj, i, ni, ki, mm), ni * nj);
406 m.emit_eq_int(p.N * p.N);
407}
408
409/**
410 * THM30: level-crossing balance at an empty station i != f, per arrival phase.
411 *
412 * NOTE the RHS multiplicity: the first two RHS blocks sum over the OTHER
413 * queue's phase (hj) while the coefficient q(i,j,ki,ui) does not depend on it.
414 * That is deliberate and differs from THM3 below, which sums over i's own phase
415 * with a coefficient that does depend on it.
416 */
417template <class T>
418void bas_thm30(const QrBasParams<T>& p, const QrBasIndex& x, lp::LpModel<T>& m) {
419 const int f = p.f;
420 for (int i = 0; i < p.M; ++i) {
421 if (i == f) continue;
422 for (int ui = 0; ui < p.K[i]; ++ui) {
423 for (int j = 0; j < p.M; ++j) {
424 if (j == i || j == f) continue;
425 for (int nj = 1; nj <= p.F[j]; ++nj)
426 for (int kj = 0; kj < p.K[j]; ++kj)
427 for (int hj = 0; hj < p.K[j]; ++hj)
428 for (int mm = 0; mm < p.MR; ++mm)
429 if (p.BB[mm][j] == 0)
430 m.row_add(x.p2(j, nj, kj, i, 0, ui, mm),
431 bas_rate(p, j, i, kj, hj));
432 }
433 for (int nj = 1; nj <= p.F[f]; ++nj)
434 for (int kj = 0; kj < p.K[f]; ++kj)
435 for (int hj = 0; hj < p.K[f]; ++hj)
436 for (int mm = 0; mm < p.MR; ++mm)
437 if (p.MM[mm][0] != i)
438 m.row_add(x.p2(f, nj, kj, i, 0, ui, mm),
439 bas_rate(p, f, i, kj, hj));
440
441 for (int j = 0; j < p.M; ++j) {
442 if (j == i || j == f) continue;
443 for (int nj = 0; nj <= p.F[j]; ++nj)
444 for (int ki = 0; ki < p.K[i]; ++ki)
445 for (int hj = 0; hj < p.K[j]; ++hj)
446 for (int mm = 0; mm < p.MR; ++mm)
447 if (p.BB[mm][i] == 0)
448 m.row_add(x.p2(j, nj, hj, i, 1, ki, mm),
449 T(-bas_rate(p, i, j, ki, ui)));
450 }
451 for (int nj = 0; nj <= p.F[f] - 1; ++nj)
452 for (int ki = 0; ki < p.K[i]; ++ki)
453 for (int hj = 0; hj < p.K[f]; ++hj)
454 for (int mm = 0; mm < p.MR; ++mm)
455 if (p.BB[mm][i] == 0)
456 m.row_add(x.p2(f, nj, hj, i, 1, ki, mm),
457 T(-bas_rate(p, i, f, ki, ui)));
458 for (int mm = 0; mm < p.MR; ++mm) {
459 if (!(p.BB[mm][i] == 1 && p.MM[mm][0] == i)) continue;
460 for (int kf = 0; kf < p.K[f]; ++kf)
461 for (int pf = 0; pf < p.K[f]; ++pf)
462 for (int w = 0; w < p.M; ++w)
463 if (w != f && w != i)
464 m.row_add(x.p2(f, p.F[f], kf, i, 1, ui, mm),
465 T(-bas_rate(p, f, w, kf, pf)));
466 }
467 m.emit_eq_int(0);
468 }
469 }
470}
471
472/**
473 * THM3: level-crossing balance between ni and ni+1 at a station i != f.
474 *
475 * NOTE the RHS multiplicity differs from THM30: here the sum runs over i's own
476 * phase hi and the coefficient q(i,j,ki,hi) depends on it.
477 */
478template <class T>
479void bas_thm3(const QrBasParams<T>& p, const QrBasIndex& x, lp::LpModel<T>& m) {
480 const int f = p.f;
481 for (int i = 0; i < p.M; ++i) {
482 if (i == f) continue;
483 for (int ni = 1; ni <= p.F[i] - 1; ++ni) {
484 for (int j = 0; j < p.M; ++j) {
485 if (j == i || j == f) continue;
486 for (int nj = 1; nj <= p.F[j]; ++nj)
487 for (int kj = 0; kj < p.K[j]; ++kj)
488 for (int hj = 0; hj < p.K[j]; ++hj)
489 for (int ui = 0; ui < p.K[i]; ++ui)
490 for (int mm = 0; mm < p.MR; ++mm)
491 if (p.BB[mm][j] == 0)
492 m.row_add(x.p2(j, nj, kj, i, ni, ui, mm),
493 bas_rate(p, j, i, kj, hj));
494 }
495 for (int nj = 1; nj <= p.F[f]; ++nj)
496 for (int kj = 0; kj < p.K[f]; ++kj)
497 for (int hj = 0; hj < p.K[f]; ++hj)
498 for (int ui = 0; ui < p.K[i]; ++ui)
499 for (int mm = 0; mm < p.MR; ++mm)
500 if (p.MM[mm][0] != i)
501 m.row_add(x.p2(f, nj, kj, i, ni, ui, mm),
502 bas_rate(p, f, i, kj, hj));
503
504 for (int j = 0; j < p.M; ++j) {
505 if (j == i || j == f) continue;
506 for (int nj = 0; nj <= p.F[j]; ++nj)
507 for (int ki = 0; ki < p.K[i]; ++ki)
508 for (int hi = 0; hi < p.K[i]; ++hi)
509 for (int uj = 0; uj < p.K[j]; ++uj)
510 for (int mm = 0; mm < p.MR; ++mm)
511 if (p.BB[mm][i] == 0)
512 m.row_add(x.p2(j, nj, uj, i, ni + 1, ki, mm),
513 T(-bas_rate(p, i, j, ki, hi)));
514 }
515 for (int nj = 0; nj <= p.F[f] - 1; ++nj)
516 for (int ki = 0; ki < p.K[i]; ++ki)
517 for (int hi = 0; hi < p.K[i]; ++hi)
518 for (int uj = 0; uj < p.K[f]; ++uj)
519 for (int mm = 0; mm < p.MR; ++mm)
520 if (p.BB[mm][i] == 0)
521 m.row_add(x.p2(f, nj, uj, i, ni + 1, ki, mm),
522 T(-bas_rate(p, i, f, ki, hi)));
523 for (int mm = 0; mm < p.MR; ++mm) {
524 if (!(p.BB[mm][i] == 1 && p.MM[mm][0] == i)) continue;
525 for (int ki = 0; ki < p.K[i]; ++ki)
526 for (int kf = 0; kf < p.K[f]; ++kf)
527 for (int pf = 0; pf < p.K[f]; ++pf)
528 for (int w = 0; w < p.M; ++w)
529 if (w != f && w != i)
530 m.row_add(x.p2(f, p.F[f], kf, i, ni + 1, ki, mm),
531 T(-bas_rate(p, f, w, kf, pf)));
532 }
533 m.emit_eq_int(0);
534 }
535 }
536}
537
538/**
539 * THM3f: level-crossing balance at the finite queue itself.
540 *
541 * The RHS is pinned to configuration 0: with f below capacity nothing is
542 * blocked, so only the unblocked configuration can supply the departure.
543 */
544template <class T>
545void bas_thm3f(const QrBasParams<T>& p, const QrBasIndex& x, lp::LpModel<T>& m) {
546 const int f = p.f;
547 for (int ni = 0; ni <= p.F[f] - 1; ++ni) {
548 for (int j = 0; j < p.M; ++j) {
549 if (j == f) continue;
550 for (int nj = 1; nj <= p.F[j]; ++nj)
551 for (int kj = 0; kj < p.K[j]; ++kj)
552 for (int hj = 0; hj < p.K[j]; ++hj)
553 for (int uf = 0; uf < p.K[f]; ++uf)
554 for (int mm = 0; mm < p.MR; ++mm)
555 if (p.BB[mm][j] == 0)
556 m.row_add(x.p2(j, nj, kj, f, ni, uf, mm),
557 bas_rate(p, j, f, kj, hj));
558 }
559 for (int j = 0; j < p.M; ++j) {
560 if (j == f) continue;
561 for (int nj = 0; nj <= p.F[j]; ++nj)
562 for (int kf = 0; kf < p.K[f]; ++kf)
563 for (int hf = 0; hf < p.K[f]; ++hf)
564 for (int uj = 0; uj < p.K[j]; ++uj)
565 m.row_add(x.p2(j, nj, uj, f, ni + 1, kf, 0),
566 T(-bas_rate(p, f, j, kf, hf)));
567 }
568 m.emit_eq_int(0);
569 }
570}
571
572/** THM3I: balance across blocking depth z, at the finite queue's capacity. */
573template <class T>
574void bas_thm3i(const QrBasParams<T>& p, const QrBasIndex& x, lp::LpModel<T>& m) {
575 const int f = p.f;
576 for (int z = 0; z <= p.ZM - 1; ++z) {
577 for (int j = 0; j < p.M; ++j) {
578 if (j == f) continue;
579 for (int nj = 1; nj <= p.F[j]; ++nj)
580 for (int kj = 0; kj < p.K[j]; ++kj)
581 for (int hj = 0; hj < p.K[j]; ++hj)
582 for (int uf = 0; uf < p.K[f]; ++uf)
583 for (int mm = 0; mm < p.MR; ++mm)
584 if (p.BB[mm][j] == 0 && p.ZZ[mm] == z)
585 m.row_add(x.p2(j, nj, kj, f, p.F[f], uf, mm),
586 bas_rate(p, j, f, kj, hj));
587 }
588 for (int j = 0; j < p.M; ++j) {
589 if (j == f) continue;
590 for (int nj = 0; nj <= p.F[j]; ++nj)
591 for (int kf = 0; kf < p.K[f]; ++kf)
592 for (int hf = 0; hf < p.K[f]; ++hf)
593 for (int uj = 0; uj < p.K[j]; ++uj)
594 for (int mm = 0; mm < p.MR; ++mm)
595 if (p.ZZ[mm] == z + 1)
596 m.row_add(x.p2(j, nj, uj, f, p.F[f], kf, mm),
597 T(-bas_rate(p, f, j, kf, hf)));
598 }
599 m.emit_eq_int(0);
600 }
601}
602
603/**
604 * THM3L: the maximum-blocking-depth closure.
605 *
606 * The only family that couples two blocking configurations: mp = MM1(m,j) is a
607 * second configuration index and the RHS variable is the DIAGONAL in mp, not
608 * in m. Fires only for the m at depth ZM - 1.
609 */
610template <class T>
611void bas_thm3l(const QrBasParams<T>& p, const QrBasIndex& x, lp::LpModel<T>& m) {
612 const int f = p.f;
613 for (int mm = 0; mm < p.MR; ++mm) {
614 if (p.ZZ[mm] != p.ZM - 1) continue;
615 for (int j = 0; j < p.M; ++j) {
616 if (j == f || p.BB[mm][j] != 0 || p.MM1[mm][j] < 0) continue;
617 for (int nj = 1; nj <= p.F[j]; ++nj)
618 for (int kj = 0; kj < p.K[j]; ++kj)
619 for (int hj = 0; hj < p.K[j]; ++hj)
620 for (int uf = 0; uf < p.K[f]; ++uf)
621 m.row_add(x.p2(j, nj, kj, f, p.F[f], uf, mm),
622 bas_rate(p, j, f, kj, hj));
623 }
624 for (int j = 0; j < p.M; ++j) {
625 if (j == f || p.BB[mm][j] != 0 || p.MM1[mm][j] < 0) continue;
626 const int mp = p.MM1[mm][j];
627 for (int kf = 0; kf < p.K[f]; ++kf)
628 for (int uf = 0; uf < p.K[f]; ++uf)
629 for (int w = 0; w < p.M; ++w)
630 if (w != f)
631 m.row_add(x.p2(f, p.F[f], kf, f, p.F[f], kf, mp),
632 T(-bas_rate(p, f, w, kf, uf)));
633 }
634 m.emit_eq_int(0);
635 }
636}
637
638/**
639 * THM4: the QMIN inequality.
640 *
641 * The reference accumulates (sum nt p2 - N sum p2) and emits the NEGATED row,
642 * so the constraint is N P(j at (nj,kj), i nonempty) <= sum_t E[n_t ...].
643 * Emitted directly in that sense here.
644 */
645template <class T>
646void bas_thm4(const QrBasParams<T>& p, const QrBasIndex& x, lp::LpModel<T>& m) {
647 for (int j = 0; j < p.M; ++j) {
648 for (int kj = 0; kj < p.K[j]; ++kj) {
649 for (int i = 0; i < p.M; ++i) {
650 for (int mm = 0; mm < p.MR; ++mm) {
651 for (int t = 0; t < p.M; ++t)
652 for (int ht = 0; ht < p.K[t]; ++ht)
653 for (int nj = 0; nj <= p.F[j]; ++nj)
654 for (int nt = 1; nt <= p.F[t]; ++nt)
655 m.row_add_int(x.p2(j, nj, kj, t, nt, ht, mm), -nt);
656 for (int hi = 0; hi < p.K[i]; ++hi)
657 for (int nj = 0; nj <= p.F[j]; ++nj)
658 for (int ni = 1; ni <= p.F[i]; ++ni)
659 m.row_add_int(x.p2(j, nj, kj, i, ni, hi, mm), p.N);
660 m.emit_le_int(0);
661 }
662 }
663 }
664 }
665}
666
667} // namespace detail
668
669/**
670 * Bound the utilization of one queue over the BAS polytope.
671 *
672 * The utilization is sum over m, k and n >= 1 of p2(i,n,k,i,n,k,m), i.e. the
673 * probability that queue i holds at least one job, in ANY blocking
674 * configuration. That is the raw utilization; the per-phase EFFECTIVE
675 * utilization, which excludes the blocked configurations, is returned in `e`.
676 *
677 * @param p network and blocking parameters, all 0-based
678 * @param objective_queue queue index, 0..M-1
679 * @param sense Max for an upper bound, Min for a lower bound
680 */
681template <class T>
683 MapqnSense sense = MapqnSense::Min) {
684 p.validate();
685 if (objective_queue < 0 || objective_queue >= p.M)
686 throw InputError("qrf_bas: objective_queue out of range");
687
688 const QrBasIndex x(p.M, p.N, p.K, p.MR);
690 // No upper bounds are set: the reference leaves ub = inf and bounds the
691 // variables through ONE. LpModel's default is exactly lb = 0, ub free.
692
693 // Families in the reference's emission order. ZERO must precede SYMMETRY,
694 // which skips pairs both of whose members it pinned.
695 const std::vector<char> zero = detail::bas_zero(p, x, m);
696 detail::bas_one(p, x, m);
697 detail::bas_symmetry(p, x, m, zero);
698 detail::bas_marginals(p, x, m);
699 detail::bas_ueff(p, x, m);
700 detail::bas_thm1(p, x, m);
701 detail::bas_thm2(p, x, m);
702 detail::bas_cor1(p, x, m);
703 detail::bas_thm30(p, x, m);
704 detail::bas_thm3(p, x, m);
705 detail::bas_thm3f(p, x, m);
706 detail::bas_thm3i(p, x, m);
707 detail::bas_thm3l(p, x, m);
708 detail::bas_thm4(p, x, m);
709
710 // UTILIZATION of the target queue, not occupancy. The cost used to sum the
711 // diagonal p2 over ALL configurations, i.e. P(n_i >= 1) with the BLOCKED
712 // ones included; a blocked BAS server holds a job it has already finished
713 // and does no work, so that is occupancy. On cqn_bas_blocking it gave
714 // U = 1 against an exact utilization of 0.590164, and the error propagated
715 // into the derived throughput through U = X*V*s.
716 //
717 // e is already the right quantity -- see bas_ueff, which restricts to the
718 // configurations where i is NOT blocked -- and optimising it rather than
719 // reading it out afterwards is what keeps the answer a BOUND.
720 //
721 // The 1/M matches THIS port's UEFF, which emits one row per (i,ki) with j
722 // summed INSIDE, leaving e scaled by M. The python and JAR ports emit one
723 // row per (j,i,ki) instead and carry no such factor: the scale belongs to
724 // the formulation, not to the definition of e.
725 const T one = num_traits<T>::from_int(1);
726 const T inv_m = one / num_traits<T>::from_int(p.M);
727 for (int ki = 0; ki < p.K[objective_queue]; ++ki)
728 m.set_cost(x.e(objective_queue, ki), inv_m);
729 m.set_maximize(sense == MapqnSense::Max);
730
731 // lp_solve, not simplex_solve: this model outgrows the dense tableau
732 // quickly, so a double instantiation hands wide models to HiGHS while
733 // Rational stays on the exact path at compile time.
734 const lp::LpSolution<T> sol = lp::lp_solve(m);
735
736 QrBasResult<T> out;
738 out.ok = sol.ok();
739 out.objective = sol.objective;
740 out.x = sol.x;
741 out.num_vars = m.num_vars();
742 out.num_rows = m.num_rows();
743 out.iterations = sol.iterations;
744 if (!out.ok) return out;
745
746 // U(i) at the optimal vertex, read from the SAME quantity the cost vector
747 // optimises so the objective queue's entry is a genuine bound. `qrf_bas.m`
748 // reads all M out of ONE solve, which is what the analyzer needs to rebuild
749 // the table; the other stations are incidental values at that vertex.
750 const T zero_u = num_traits<T>::from_int(0);
751 out.U.assign(static_cast<std::size_t>(p.M), zero_u);
752 for (int i = 0; i < p.M; ++i)
753 for (int ki = 0; ki < p.K[i]; ++ki)
754 out.U[static_cast<std::size_t>(i)] += sol.x[x.e(i, ki)] * inv_m;
755
756 out.occupancy.assign(static_cast<std::size_t>(p.M), zero_u);
757 for (int i = 0; i < p.M; ++i)
758 for (int mm = 0; mm < p.MR; ++mm)
759 for (int ki = 0; ki < p.K[i]; ++ki)
760 for (int ni = 1; ni <= p.F[i]; ++ni)
761 out.occupancy[static_cast<std::size_t>(i)] +=
762 sol.x[x.p2(i, ni, ki, i, ni, ki, mm)];
763
764 std::size_t maxK = 0;
765 for (int i = 0; i < p.M; ++i)
766 if (static_cast<std::size_t>(p.K[i]) > maxK) maxK = static_cast<std::size_t>(p.K[i]);
767 out.e = Matrix<T>(static_cast<std::size_t>(p.M), maxK);
768 for (int i = 0; i < p.M; ++i)
769 for (int ki = 0; ki < p.K[i]; ++ki)
770 out.e(static_cast<std::size_t>(i), static_cast<std::size_t>(ki)) = sol.x[x.e(i, ki)];
771 return out;
772}
773
774} // namespace mapqn
775} // namespace line
776
777#endif // LINE_API_MAPQN_MAPQN_QR_BOUNDS_BAS_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.
QrBasResult< T > mapqn_qr_bounds_bas(const QrBasParams< T > &p, int objective_queue, MapqnSense sense=MapqnSense::Min)
Bound the utilization of one queue over the BAS 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,m) then e(i,ki).
std::size_t num_vars() const
std::size_t p2(int j, int nj, int kj, int i, int ni, int hi, int m) const
std::size_t e(int i, int ki) const
std::size_t half(int i, int ni, int h) const
QrBasIndex(int m, int n, const std::vector< int > &k, int mr)
Parameters of the BAS bound, mirroring the reference's params.
std::vector< int > F
(M) capacity of each queue
std::vector< int > K
(M) number of phases of each queue
std::vector< int > ZZ
(MR) number of blocked queues in m
std::vector< std::vector< int > > MM1
(MR x M) extended order; negative means absent
std::vector< std::vector< int > > MM
(MR x 2) blocking order, 0-based queue indices
int ZM
maximum blocking depth
int f
index of the finite-capacity queue, 0-based
std::vector< Matrix< T > > mu
mu[i] is K(i) x K(i), completion rates
Matrix< T > r
(M x M) routing probabilities
int MR
number of blocking configurations
std::vector< Matrix< T > > v
v[i] is K(i) x K(i), background rates
std::vector< std::vector< int > > BB
(MR x M) 1 if queue i is blocked in m
Result of a BAS bound solve.
std::vector< T > U
(M) utilization of each queue at the optimal vertex
std::string status
textual LP status
std::vector< T > x
full solution vector, indexed by QrBasIndex
Matrix< T > e
M x max(K) effective per-phase utilizations.
T objective
the bound on the utilization of the target queue
std::vector< T > occupancy
(M) P(n_i >= 1) over EVERY configuration, blocked ones included.
bool ok
the LP reached an optimal vertex