LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
mapqn_qrf_bas_nlp.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_QRF_BAS_NLP_H
6#define LINE_API_MAPQN_MAPQN_QRF_BAS_NLP_H
7
8/**
9 * @file
10 * @ingroup api_mapqn
11 * `qrf_bas_mmi`, `qrf_bas_mem` and `qrf_bas_bethe`: the nonlinear bounds on the
12 * BAS-BLOCKING polytope.
13 *
14 * Port of python/line_solver/api/mapqn/qrf_bas_nlp.py. `api/mapqn` has no
15 * MATLAB implementation, so Python and the JAR are the references.
16 *
17 * THE POLYTOPE IS NOT RE-DERIVED HERE. It is exactly the one
18 * `mapqn_qr_bounds_bas` builds and that the LP token `qrf.bas` is validated on
19 * against the AMPL model; only the OBJECTIVE differs, the LP maximizing one
20 * station's utilization where these minimize mutual information, negative
21 * entropy or the tree-reweighted (Bethe) free entropy over the same feasible
22 * set. Re-transcribing the fifteen constraint
23 * families would duplicate several hundred lines whose index conventions are
24 * exactly where the reference's own twin twice went wrong -- the THM30/THM3
25 * population-to-index shift, and a MARGINALS sum whose upper limit was taken on
26 * the 1-based index rather than on the population. Sharing the builder means a
27 * family added to the LP reaches the NLP with it.
28 *
29 * The decision vector is therefore the LP's own, indexed through `QrBasIndex`,
30 * NOT the flat `sub_qrfvar` layout of the no-blocking family. The two are
31 * different orderings of different variable sets and must not be mixed: the BAS
32 * index skips a population above a station's capacity, which the no-blocking
33 * layout carries and pins to zero.
34 *
35 * CONVEXITY, AS IN THE NO-BLOCKING TWIN: MEM is convex on the polytope and has
36 * a unique optimum; MMI is NOT (its `-p_ij log p_ii` terms are not), so it
37 * reports a local optimum fixed by the phase-1 LP vertex. BETHE is the convex
38 * combination the weight lambda = 1/M is chosen to make convex on the LOCAL
39 * MARGINAL polytope; the BAS set adds the blocking families on top of the
40 * marginal ones, so the objective's convexity carries but the per-configuration
41 * marginal consistency that the argument rests on is not proved under ZERO5.
42 * Start-point independence here is therefore MEASURED, not assumed. See
43 * `mapqn_qrf_common.h` for the measurement behind all three.
44 *
45 * ARITHMETIC: transcendental.
46 */
47
48#include <cstddef>
49#include <string>
50#include <vector>
51
54#include "line/num/number.h"
55#include "line/util/error.h"
56#include "line/util/lp_highs.h"
57#include "line/util/simplex.h"
58
59namespace line {
60namespace mapqn {
61
62namespace qrfbas {
63
64/** The BAS feasible set, built by the same calls the LP bound makes. */
65template <class T>
67 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
69 // Families in the reference's emission order. ZERO must precede SYMMETRY,
70 // which skips pairs both of whose members it pinned.
71 const std::vector<char> zeroed = detail::bas_zero(p, x, m);
72 detail::bas_one(p, x, m);
73 detail::bas_symmetry(p, x, m, zeroed);
74 detail::bas_marginals(p, x, m);
75 detail::bas_ueff(p, x, m);
76 detail::bas_thm1(p, x, m);
77 detail::bas_thm2(p, x, m);
78 detail::bas_cor1(p, x, m);
79 detail::bas_thm30(p, x, m);
80 detail::bas_thm3(p, x, m);
81 detail::bas_thm3f(p, x, m);
82 detail::bas_thm3i(p, x, m);
83 detail::bas_thm3l(p, x, m);
84 detail::bas_thm4(p, x, m);
85 // THE [0,1] BOX IS NOT IMPOSED, AND MUST NOT BE. The reference's phase-1
86 // `linprog` passes `bounds=[(0,1)]*n`, but here that upper bound is
87 // IMPLIED and imposing it explicitly breaks the solve: ONE fixes each
88 // station's diagonal marginal to sum to one over nonnegative entries, so
89 // every diagonal entry is at most one; MARGINALS ties each off-diagonal
90 // entry to its own diagonal, so those are too; and UEFF writes `e` as a sum
91 // of the same entries. Adding 246 redundant upper bounds to the dense
92 // tableau instead made `lp_solve` report Optimal on a point whose largest
93 // coordinate was 7.3e9 -- an out-of-bounds answer with a success status,
94 // which then propagated into utilizations of 92 and queue lengths of 334.
95 // The free-upper formulation is the one `mapqn_qr_bounds_bas` solves and
96 // validates against MATLAB, so it is the one used here.
97 (void)zero;
98 (void)one;
99 return m;
100}
101
102/**
103 * The (ij, ii, jj) column triples of the MI objective, i != j, ni, nj >= n_from.
104 *
105 * n_from is 0 for both callers, the reference's own range: the idle cell
106 * carries the strongest correlation in a closed chain, and BETHE's entropy term
107 * spans the same range so the two blocks agree.
108 */
109inline void mmi_terms(const QrBasIndex& x, const std::vector<int>& F, std::vector<std::size_t>* ij,
110 std::vector<std::size_t>* ii, std::vector<std::size_t>* jj,
111 int n_from = 1) {
112 for (int m = 0; m < x.MR; ++m)
113 for (int i = 0; i < x.M; ++i)
114 for (int ki = 0; ki < x.K[i]; ++ki)
115 for (int j = 0; j < x.M; ++j) {
116 if (i == j) continue;
117 for (int kj = 0; kj < x.K[j]; ++kj)
118 for (int ni = n_from; ni <= F[i]; ++ni)
119 for (int nj = n_from; nj <= F[j]; ++nj) {
120 ij->push_back(x.p2(i, ni, ki, j, nj, kj, m));
121 ii->push_back(x.p2(i, ni, ki, i, ni, ki, m));
122 jj->push_back(x.p2(j, nj, kj, j, nj, kj, m));
123 }
124 }
125}
126
127/** The diagonal columns of the MEM objective, ni >= n_from (1 for MEM, 0 for BETHE). */
128inline std::vector<std::size_t> mem_terms(const QrBasIndex& x, const std::vector<int>& F,
129 int n_from = 1) {
130 std::vector<std::size_t> out;
131 for (int m = 0; m < x.MR; ++m)
132 for (int i = 0; i < x.M; ++i)
133 for (int k = 0; k < x.K[i]; ++k)
134 for (int ni = n_from; ni <= F[i]; ++ni) out.push_back(x.p2(i, ni, k, i, ni, k, m));
135 return out;
136}
137
138/** Which functional `mapqn_qrf_bas` minimizes over the BAS polytope. */
139enum class Objective { Mmi, Mem, Bethe };
140
141} // namespace qrfbas
142
143/**
144 * Solve one BAS-blocking NLP bound.
145 *
146 * @param p the same parameters the LP token `qrf.bas` takes
147 * @param obj which functional to minimize over the polytope
148 * @param max_iter Frank-Wolfe iteration cap. EVERY ITERATE IS FEASIBLE, since
149 * each step is a convex combination of two points of the
150 * polytope, so a truncated run returns a worse bound but never
151 * an invalid one. That is what makes a small cap a legitimate
152 * way to keep a test cheap: the BAS polytope has hundreds of
153 * columns and each iteration costs one LP plus a line search.
154 */
155template <class T>
157 unsigned max_iter = 200) {
158 p.validate();
159 const T tol = qrf_logtol<T>();
160 const QrBasIndex x(p.M, p.N, p.K, p.MR);
161 const bool mem = (obj == qrfbas::Objective::Mem);
162 const bool bethe = (obj == qrfbas::Objective::Bethe);
163 // lambda = 1/M: half the uniform point 2/M of the spanning-tree polytope of
164 // K_M, the largest uniform edge weight at which the tree-reweighted entropy
165 // is concave and the program therefore convex.
167 const std::string name =
168 bethe ? "qrf_bas_bethe" : (mem ? "qrf_bas_mem" : "qrf_bas_mmi");
169
170 const lp::LpModel<T> poly = qrfbas::bas_polytope(p, x);
171
172 // The MI block spans 0..F, the reference's range; MEM's spans 1..F, except
173 // under BETHE, which combines the two and needs one range across both.
174 std::vector<std::size_t> ij, ii, jj, diag;
175 if (mem || bethe) {
176 diag = qrfbas::mem_terms(x, p.F, bethe ? 0 : 1);
177 if (diag.empty())
178 throw InputError(name +
179 ": the model has no marginal variables, so the objective is empty");
180 }
181 if (!mem) {
182 qrfbas::mmi_terms(x, p.F, &ij, &ii, &jj, 0);
183 if (ij.empty())
184 throw InputError(name +
185 ": the model has no off-diagonal joint variables, so the mutual "
186 "information objective is empty");
187 }
188
189 // The MI block is scaled by lambda under BETHE and by one under MMI, and
190 // is absent under MEM; the entropy block is present under MEM and BETHE.
191 // Written as `-> T` explicitly: a deduced return type over an expression-
192 // template Rational returns a node holding references to dead temporaries.
193 const T mi_scale = bethe ? lam : num_traits<T>::from_int(1);
194 auto objective = [&](const std::vector<T>& z) -> T {
196 for (std::size_t t = 0; t < diag.size(); ++t) {
197 const T pv = z[diag[t]];
198 f += T(pv * qrf_log<T>(tol + pv));
199 }
200 for (std::size_t t = 0; t < ij.size(); ++t) {
201 const T pij = z[ij[t]], pii = z[ii[t]], pjj = z[jj[t]];
202 f += T(mi_scale * pij *
203 (qrf_log<T>(tol + pij) - qrf_log<T>(tol + pii) - qrf_log<T>(tol + pjj)));
204 }
205 return f;
206 };
207 auto gradient = [&](const std::vector<T>& z) -> std::vector<T> {
208 std::vector<T> g(z.size(), num_traits<T>::from_int(0));
209 for (std::size_t t = 0; t < diag.size(); ++t) {
210 const T pv = z[diag[t]];
211 g[diag[t]] += T(qrf_log<T>(tol + pv) + pv / (tol + pv));
212 }
213 for (std::size_t t = 0; t < ij.size(); ++t) {
214 const T pij = z[ij[t]], pii = z[ii[t]], pjj = z[jj[t]];
215 g[ij[t]] += T(mi_scale * (qrf_log<T>(tol + pij) - qrf_log<T>(tol + pii) -
216 qrf_log<T>(tol + pjj) + pij / (tol + pij)));
217 g[ii[t]] -= T(mi_scale * pij / (tol + pii));
218 g[jj[t]] -= T(mi_scale * pij / (tol + pjj));
219 }
220 return g;
221 };
222
223 const std::vector<T> x0 = qrf_feasible_start_lp(poly, name);
224 const std::vector<T> xopt = solve_qrf_nlp_lp(objective, gradient, x0, poly, name, max_iter);
225
226 // UTILIZATION, not occupancy. UN used to sum the diagonal p2 over ALL
227 // blocking configurations, i.e. P(n_i >= 1) with the BLOCKED ones included.
228 // A blocked BAS server holds a job it has already finished and does no work,
229 // so that is occupancy: on the M=2, N=3, F=[2 3] cyclic model it reported
230 // U2 = 1 where the exact utilization is 7/15, which the LP over the SAME
231 // polytope already returns. e carries the right quantity -- bas_ueff pins
232 // e(i,ki) to the mass with n_i >= 1 in the configurations where i is NOT
233 // blocked.
234 //
235 // The 1/M matches THIS port's UEFF, which emits one row per (i,ki) with j
236 // summed INSIDE, leaving e scaled by M; the python and JAR ports emit one
237 // row per (j,i,ki) and carry no such factor. QN stays on the diagonal p2
238 // over every configuration, blocked included, because a blocked job is still
239 // held at the station and counts towards its population.
240 QrfMetrics<T> out;
241 out.UN.assign(static_cast<std::size_t>(p.M), num_traits<T>::from_int(0));
242 out.QN.assign(static_cast<std::size_t>(p.M), num_traits<T>::from_int(0));
243 const T inv_m = num_traits<T>::from_int(1) / num_traits<T>::from_int(p.M);
244 for (int i = 0; i < p.M; ++i)
245 for (int ki = 0; ki < p.K[i]; ++ki)
246 out.UN[static_cast<std::size_t>(i)] += T(xopt[x.e(i, ki)] * inv_m);
247 for (int i = 0; i < p.M; ++i)
248 for (int m = 0; m < p.MR; ++m)
249 for (int ni = 1; ni <= p.F[i]; ++ni)
250 for (int ki = 0; ki < p.K[i]; ++ki)
251 out.QN[static_cast<std::size_t>(i)] +=
252 T(num_traits<T>::from_int(ni) * xopt[x.p2(i, ni, ki, i, ni, ki, m)]);
253 return out;
254}
255
256/** Minimum-mutual-information bound on the BAS-blocking polytope. */
257template <class T>
258QrfMetrics<T> mapqn_qrf_bas_mmi(const QrBasParams<T>& p, unsigned max_iter = 200) {
259 return mapqn_qrf_bas(p, qrfbas::Objective::Mmi, max_iter);
260}
261
262/** Maximum-entropy bound on the BAS-blocking polytope. */
263template <class T>
264QrfMetrics<T> mapqn_qrf_bas_mem(const QrBasParams<T>& p, unsigned max_iter = 200) {
265 return mapqn_qrf_bas(p, qrfbas::Objective::Mem, max_iter);
266}
267
268/**
269 * Tree-reweighted (Bethe) free entropy bound on the BAS-blocking polytope.
270 *
271 * `lambda*sum_{i!=j} I(n_i;n_j) - sum_i H(n_i)` at lambda = 1/M, the objective
272 * of `qrf_noblo_bethe` evaluated over the BAS decision vector: the blocking
273 * configurations and the per-station capacities enter through the ranges alone.
274 */
275template <class T>
276QrfMetrics<T> mapqn_qrf_bas_bethe(const QrBasParams<T>& p, unsigned max_iter = 200) {
277 return mapqn_qrf_bas(p, qrfbas::Objective::Bethe, max_iter);
278}
279
280} // namespace mapqn
281} // namespace line
282
283#endif // LINE_API_MAPQN_MAPQN_QRF_BAS_NLP_H
InputError(const std::string &what)
Definition error.h:39
Sparse LP in the natural form, with per-variable bounds.
Definition simplex.h:112
The exception types the port throws.
A sparse LP backend for line::lp::LpModel, on HiGHS (MIT).
Quadratic-reduction bound on the utilization of one queue of a closed MAP queueing network with a FIN...
Shared machinery of the QRF nonlinear bounds (qrf_noblo_*, qrf_bas_*).
void mmi_terms(const QrBasIndex &x, const std::vector< int > &F, std::vector< std::size_t > *ij, std::vector< std::size_t > *ii, std::vector< std::size_t > *jj, int n_from=1)
The (ij, ii, jj) column triples of the MI objective, i != j, ni, nj >= n_from.
lp::LpModel< T > bas_polytope(const QrBasParams< T > &p, const QrBasIndex &x)
The BAS feasible set, built by the same calls the LP bound makes.
std::vector< std::size_t > mem_terms(const QrBasIndex &x, const std::vector< int > &F, int n_from=1)
The diagonal columns of the MEM objective, ni >= n_from (1 for MEM, 0 for BETHE).
Objective
Which functional mapqn_qrf_bas minimizes over the BAS polytope.
std::vector< T > qrf_feasible_start_lp(const lp::LpModel< T > &polytope, const std::string &name)
A feasible point of an LpModel polytope: the MINIMUM-NORM one.
QrfMetrics< T > mapqn_qrf_bas_mmi(const QrBasParams< T > &p, unsigned max_iter=200)
Minimum-mutual-information bound on the BAS-blocking polytope.
QrfMetrics< T > mapqn_qrf_bas_bethe(const QrBasParams< T > &p, unsigned max_iter=200)
Tree-reweighted (Bethe) free entropy bound on the BAS-blocking polytope.
QrfMetrics< T > mapqn_qrf_bas_mem(const QrBasParams< T > &p, unsigned max_iter=200)
Maximum-entropy bound on the BAS-blocking polytope.
QrfMetrics< T > mapqn_qrf_bas(const QrBasParams< T > &p, qrfbas::Objective obj, unsigned max_iter=200)
Solve one BAS-blocking NLP bound.
std::vector< T > solve_qrf_nlp_lp(Obj objective, Grad gradient, const std::vector< T > &x0, const lp::LpModel< T > &polytope, const std::string &name, unsigned max_iter=200, double gap_tol=1e-10)
Minimize a convex objective over {Aeq x = beq, Aub x <= bub, 0 <= x <= 1}, starting from a feasible p...
T qrf_logtol()
The reference's LOGTOL: the shift that keeps log() off zero.
T qrf_log(const T &v)
log() in the working arithmetic.
Number-type abstraction for the templated API port.
Templated primal simplex with Bland's rule.
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
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
int MR
number of blocking configurations
The utilizations and queue lengths read off an optimal pair tensor.