LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
mapqn_qrf_noblo.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_NOBLO_H
6#define LINE_API_MAPQN_MAPQN_QRF_NOBLO_H
7
8/**
9 * @file
10 * @ingroup api_mapqn
11 * The QRF no-blocking nonlinear bounds: `qrf_noblo_mmi`, `qrf_noblo_mem` and
12 * the load-dependent `qrf_noblo_mmi_ld`.
13 *
14 * Port of python/line_solver/api/mapqn/qrf_noblo_{mmi,mem,mmi_ld}.py and the
15 * `sub_qrfcon_noblo` constraint inventory they share. `api/mapqn` has no MATLAB
16 * implementation, so Python and the JAR are the references.
17 *
18 * THE ARITY OF q SELECTS THE MODEL, exactly as the AMPL skeletons do:
19 *
20 * - a 4D q[i][j][k][h] is the population-free form of `qrboundsbas_skel.mod`,
21 * used by MMI and MEM. THM1 is stated on the aggregated e[i,k], which is
22 * exact here because no alpha makes the rate population-dependent.
23 * - a 5D q[i][j][k][h][n] is the load-dependent form of
24 * `qrboundsrsrd_skel.mod`, whose fifth index is the population of the
25 * EMITTING station. THM1 is then stated per population against the
26 * station-i marginal.
27 *
28 * THM30 AND THM3 ARE WHAT MAKE THE POLYTOPE DEPEND ON THE SERVICE RATES AT
29 * ALL. They are the marginal-balance families; without them an LP over the
30 * remaining constraints returns the vacuous [0,1] for every instance (the
31 * reference verified that against glpsol). Both are emitted in either form and
32 * differ only in the q lookup.
33 *
34 * ARITHMETIC: transcendental, inherited from the objectives.
35 */
36
37#include <algorithm>
38#include <cstddef>
39#include <string>
40#include <vector>
41
43#include "line/num/number.h"
44#include "line/util/error.h"
45#include "line/util/matrix.h"
46
47namespace line {
48namespace mapqn {
49
50/**
51 * The transition rates the constraint inventory reads.
52 *
53 * `load_dependent` selects the arity: with it false only `q4` is read, with it
54 * true only `q5`. They are kept as two members rather than one flattened array
55 * because confusing the two is the failure mode the reference documents -- a
56 * population-free q cannot distinguish the rate at which a station empties at
57 * population n from the rate at n', so the polytope stops pinning the
58 * utilization at all.
59 */
60template <class T>
61struct QrfRates {
62 bool load_dependent = false;
63 std::size_t M = 0, Kmax = 0, N = 0;
64 std::vector<T> q4; ///< [i][j][k][h]
65 std::vector<T> q5; ///< [i][j][k][h][n], n the EMITTING station's population
66
67 T at(std::size_t i, std::size_t j, std::size_t k, std::size_t h, std::size_t n) const {
69 return q5[((((i * M + j) * Kmax + k) * Kmax + h) * (N + 1)) + n];
70 return q4[((i * M + j) * Kmax + k) * Kmax + h];
71 }
72 /** The population-free lookup, for the arms that do not carry n. */
73 T at4(std::size_t i, std::size_t j, std::size_t k, std::size_t h) const {
74 return q4[((i * M + j) * Kmax + k) * Kmax + h];
75 }
76};
77
78/** build_q_from_mu_v_rt: the population-free rates. */
79template <class T>
80QrfRates<T> qrf_build_q(std::size_t M, const std::vector<int>& K, const Matrix<T>& mu,
81 const Matrix<T>& v, const Matrix<T>& rt) {
82 const T zero = num_traits<T>::from_int(0);
83 const std::size_t Kmax = static_cast<std::size_t>(*std::max_element(K.begin(), K.end()));
85 q.load_dependent = false;
86 q.M = M;
87 q.Kmax = Kmax;
88 q.q4.assign(M * M * Kmax * Kmax, zero);
89 for (std::size_t i = 0; i < M; ++i)
90 for (std::size_t j = 0; j < M; ++j)
91 for (std::size_t k = 0; k < static_cast<std::size_t>(K[i]); ++k)
92 for (std::size_t h = 0; h < static_cast<std::size_t>(K[i]); ++h) {
93 const T mv = mu(i, k * Kmax + h);
94 q.q4[((i * M + j) * Kmax + k) * Kmax + h] =
95 (j != i) ? T(rt(i, j) * mv) : T(v(i, k * Kmax + h) + rt(i, i) * mv);
96 }
97 return q;
98}
99
100/**
101 * build_q_ld: the load-dependent rates.
102 *
103 * `qrboundsrsrd_skel.mod:11` declares q with FIVE indices, the fifth being the
104 * population n of the EMITTING station, with q[...,0] = 0, and the scaling
105 * alpha[i,n] multiplying BOTH the background term v and the completion term
106 * rt[i,i] mu. Dropping alpha from the v term is as wrong as dropping the index.
107 *
108 * @param alpha (M x N), 0-based in the population, so alpha(i, n-1) is the
109 * AMPL alpha[i,n]; an empty matrix means all ones
110 */
111template <class T>
112QrfRates<T> qrf_build_q_ld(std::size_t M, const std::vector<int>& K, const Matrix<T>& mu,
113 const Matrix<T>& v, const Matrix<T>& rt, std::size_t N,
114 const Matrix<T>& alpha) {
115 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
116 const std::size_t Kmax = static_cast<std::size_t>(*std::max_element(K.begin(), K.end()));
117 const bool unit = (alpha.rows() == 0 || alpha.cols() == 0);
118 if (!unit && (alpha.rows() != M || alpha.cols() != N))
119 throw InputError("qrf_build_q_ld: alpha must be (M x N)");
120 QrfRates<T> q;
121 q.load_dependent = true;
122 q.M = M;
123 q.Kmax = Kmax;
124 q.N = N;
125 q.q5.assign(M * M * Kmax * Kmax * (N + 1), zero);
126 for (std::size_t i = 0; i < M; ++i)
127 for (std::size_t j = 0; j < M; ++j)
128 for (std::size_t k = 0; k < static_cast<std::size_t>(K[i]); ++k)
129 for (std::size_t h = 0; h < static_cast<std::size_t>(K[i]); ++h)
130 for (std::size_t n = 1; n <= N; ++n) {
131 const T a = unit ? one : alpha(i, n - 1);
132 const T mv = mu(i, k * Kmax + h);
133 const std::size_t at =
134 ((((i * M + j) * Kmax + k) * Kmax + h) * (N + 1)) + n;
135 q.q5[at] = (j != i) ? T(rt(i, j) * mv * a)
136 : T(v(i, k * Kmax + h) * a + rt(i, i) * mv * a);
137 }
138 return q;
139}
140
141/** extract_mu_v_from_maps: the completion and background rates of each MAP. */
142template <class T>
143void qrf_extract_mu_v(const std::vector<std::pair<Matrix<T>, Matrix<T>>>& MAPs, std::size_t M,
144 const std::vector<int>& K, Matrix<T>* mu, Matrix<T>* v) {
145 const T zero = num_traits<T>::from_int(0);
146 const std::size_t Kmax = static_cast<std::size_t>(*std::max_element(K.begin(), K.end()));
147 *mu = Matrix<T>(M, Kmax * Kmax, zero);
148 *v = Matrix<T>(M, Kmax * Kmax, zero);
149 for (std::size_t i = 0; i < M; ++i) {
150 const Matrix<T>& D0 = MAPs[i].first;
151 const Matrix<T>& D1 = MAPs[i].second;
152 for (std::size_t h = 0; h < static_cast<std::size_t>(K[i]); ++h)
153 for (std::size_t k = 0; k < static_cast<std::size_t>(K[i]); ++k) {
154 (*mu)(i, h * Kmax + k) = D1(h, k);
155 // The DIAGONAL of D0 is the total exit rate, not a background
156 // transition, so it is dropped here rather than carried.
157 // (from, to), the order mu and the q assembly already use.
158 (*v)(i, h * Kmax + k) = (h == k) ? zero : D0(h, k);
159 }
160 }
161}
162
163/** The two constraint blocks: g(x) <= 0 and h(x) = 0. */
164template <class T>
166 std::vector<T> ineq;
167 std::vector<T> eq;
168};
169
170/**
171 * The full no-blocking constraint inventory, `sub_qrfcon_noblo`.
172 *
173 * @param x decision vector
174 * @param q the rates, whose arity selects the model
175 * @param BB (MR x M) blocking-state matrix
176 * @param F (M) capacity per queue
177 */
178template <class T>
179QrfConstraints<T> sub_qrfcon_noblo(const std::vector<T>& x, const QrfRates<T>& q, std::size_t M,
180 std::size_t MR, const Matrix<int>& BB,
181 const std::vector<int>& F, std::size_t N,
182 const std::vector<int>& K) {
183 const T zero = num_traits<T>::from_int(0);
184 const QrfVars<T> V = sub_qrfvar(x, M, N, K, MR);
186 std::vector<T>& ceq = out.eq;
187 std::vector<T>& c = out.ineq;
188 auto Ki = [&K](std::size_t i) { return static_cast<std::size_t>(K[i]); };
189 auto Fi = [&F](std::size_t i) { return static_cast<std::size_t>(F[i]); };
190
191 // ONE: each station's diagonal marginal is a probability distribution.
192 for (std::size_t j = 0; j < M; ++j) {
193 T val = zero;
194 for (std::size_t nj = 0; nj <= N; ++nj)
195 for (std::size_t k = 0; k < Ki(j); ++k)
196 for (std::size_t m = 0; m < MR; ++m) val += V.p(j, nj, k, j, nj, k, m);
197 ceq.push_back(val - num_traits<T>::from_int(1));
198 }
199
200 // ZERO1/ZERO2/ZERO3: a pair entry that describes an impossible joint state.
201 for (std::size_t j = 0; j < M; ++j)
202 for (std::size_t k = 0; k < Ki(j); ++k)
203 for (std::size_t nj = 0; nj <= N; ++nj)
204 for (std::size_t i = 0; i < M; ++i)
205 for (std::size_t h = 0; h < Ki(i); ++h)
206 for (std::size_t ni = 0; ni <= N; ++ni)
207 for (std::size_t m = 0; m < MR; ++m) {
208 if (i == j && nj == ni && h != k)
209 ceq.push_back(V.p(j, nj, k, i, ni, h, m)); // ZERO1
210 if (i == j && nj != ni)
211 ceq.push_back(V.p(j, nj, k, i, ni, h, m)); // ZERO2
212 if (i != j && nj + ni > N)
213 ceq.push_back(V.p(j, nj, k, i, ni, h, m)); // ZERO3
214 }
215
216 // ZERO5: a blocked station holds no empty-population mass.
217 for (std::size_t j = 0; j < M; ++j)
218 for (std::size_t k = 0; k < Ki(j); ++k)
219 for (std::size_t i = 0; i < M; ++i)
220 for (std::size_t h = 0; h < Ki(i); ++h)
221 for (std::size_t ni = 0; ni <= Fi(i); ++ni)
222 for (std::size_t m = 1; m < MR; ++m)
223 if (BB(m, j) == 1) ceq.push_back(V.p(j, 0, k, i, ni, h, m));
224
225 // ZERO6: above the buffer there is no mass.
226 for (std::size_t j = 0; j < M; ++j)
227 for (std::size_t k = 0; k < Ki(j); ++k)
228 for (std::size_t nj = Fi(j) + 1; nj <= N; ++nj)
229 for (std::size_t i = 0; i < M; ++i)
230 for (std::size_t h = 0; h < Ki(i); ++h)
231 for (std::size_t ni = 0; ni <= N; ++ni)
232 for (std::size_t m = 0; m < MR; ++m)
233 ceq.push_back(V.p(j, nj, k, i, ni, h, m));
234
235 // ZERO7 is the blocking family and is empty at MR = 1, which is the whole
236 // of the no-blocking model; it is not emitted rather than emitted as 0 = 0.
237
238 // SYMMETRY: the pair tensor describes an unordered pair.
239 for (std::size_t j = 0; j < M; ++j)
240 for (std::size_t nj = 0; nj <= N; ++nj)
241 for (std::size_t k = 0; k < Ki(j); ++k)
242 for (std::size_t i = 0; i < M; ++i)
243 for (std::size_t ni = 0; ni <= N; ++ni)
244 for (std::size_t h = 0; h < Ki(i); ++h)
245 for (std::size_t m = 0; m < MR; ++m)
246 ceq.push_back(V.p(i, ni, h, j, nj, k, m) -
247 V.p(j, nj, k, i, ni, h, m));
248
249 // MARGINALS: the diagonal entry is the marginal of the pair over the other
250 // station. The inner sum runs over the POPULATION range 0..N, not over the
251 // 1-based index, which is where the reference's twin twice went wrong.
252 for (std::size_t j = 0; j < M; ++j)
253 for (std::size_t k = 0; k < Ki(j); ++k)
254 for (std::size_t nj = 0; nj <= N; ++nj)
255 for (std::size_t i = 0; i < M; ++i)
256 for (std::size_t m = 0; m < MR; ++m) {
257 if (i == j) continue;
258 T val = V.p(j, nj, k, j, nj, k, m);
259 for (std::size_t ni = 0; ni <= N; ++ni)
260 for (std::size_t h = 0; h < Ki(i); ++h)
261 val -= V.p(j, nj, k, i, ni, h, m);
262 ceq.push_back(val);
263 }
264
265 // UEFF: the effective rate variable is the busy marginal of the pair.
266 for (std::size_t j = 0; j < M; ++j)
267 for (std::size_t i = 0; i < M; ++i)
268 for (std::size_t ki = 0; ki < Ki(i); ++ki) {
269 T val = V.e[i * V.Kmax + ki];
270 for (std::size_t nj = 0; nj <= N; ++nj)
271 for (std::size_t kj = 0; kj < Ki(j); ++kj)
272 for (std::size_t m = 0; m < MR; ++m)
273 for (std::size_t ni = 1; ni <= N; ++ni)
274 if (BB(m, i) == 0) val -= V.p(j, nj, kj, i, ni, ki, m);
275 ceq.push_back(val);
276 }
277
278 // THM1: phase balance at station i. Stated on the aggregated e in the
279 // population-free form, and per population against the station-i marginal
280 // in the load-dependent one, because there the rate depends on n.
281 for (std::size_t i = 0; i < M; ++i)
282 for (std::size_t k = 0; k < Ki(i); ++k) {
283 T val = zero;
284 if (q.load_dependent) {
285 for (std::size_t ni = 1; ni <= Fi(i); ++ni)
286 for (std::size_t m = 0; m < MR; ++m)
287 for (std::size_t j = 0; j < M; ++j)
288 for (std::size_t h = 0; h < Ki(i); ++h) {
289 val += q.at(i, j, k, h, ni) * V.p(i, ni, k, i, ni, k, m);
290 val -= q.at(i, j, h, k, ni) * V.p(i, ni, h, i, ni, h, m);
291 }
292 } else {
293 for (std::size_t j = 0; j < M; ++j)
294 for (std::size_t h = 0; h < Ki(i); ++h) {
295 val += q.at4(i, j, k, h) * V.e[i * V.Kmax + k];
296 val -= q.at4(i, j, h, k) * V.e[i * V.Kmax + h];
297 }
298 }
299 ceq.push_back(val);
300 }
301
302 // THM2: the population carried by the conditional distribution is N.
303 for (std::size_t j = 0; j < M; ++j)
304 for (std::size_t k = 0; k < Ki(j); ++k)
305 for (std::size_t nj = 0; nj <= Fi(j); ++nj)
306 for (std::size_t m = 0; m < MR; ++m) {
307 T val = zero;
308 for (std::size_t i = 0; i < M; ++i)
309 for (std::size_t ni = 1; ni <= Fi(i); ++ni)
310 for (std::size_t ki = 0; ki < Ki(i); ++ki)
311 val += num_traits<T>::from_int(static_cast<long>(ni)) *
312 V.p(j, nj, k, i, ni, ki, m);
313 val -= num_traits<T>::from_int(static_cast<long>(N)) *
314 V.p(j, nj, k, j, nj, k, m);
315 ceq.push_back(val);
316 }
317
318 // COR1: the second moment of the total population is N^2.
319 {
320 T val = zero;
321 for (std::size_t m = 0; m < MR; ++m)
322 for (std::size_t i = 0; i < M; ++i)
323 for (std::size_t j = 0; j < M; ++j)
324 for (std::size_t nj = 1; nj <= Fi(j); ++nj)
325 for (std::size_t ni = 1; ni <= Fi(i); ++ni)
326 for (std::size_t ki = 0; ki < Ki(i); ++ki)
327 for (std::size_t kj = 0; kj < Ki(j); ++kj)
329 static_cast<long>(ni * nj)) *
330 V.p(j, nj, kj, i, ni, ki, m);
331 val -= num_traits<T>::from_int(static_cast<long>(N * N));
332 ceq.push_back(val);
333 }
334
335 // THM30 {i, u}: balance across the ni = 0 boundary of station i.
336 for (std::size_t i = 0; i < M; ++i)
337 for (std::size_t u = 0; u < Ki(i); ++u) {
338 T val = zero;
339 for (std::size_t j = 0; j < M; ++j) {
340 if (j == i) continue;
341 for (std::size_t nj = 1; nj <= Fi(j); ++nj)
342 for (std::size_t k = 0; k < Ki(j); ++k) {
343 T coef = zero;
344 for (std::size_t h = 0; h < Ki(j); ++h) coef += q.at(j, i, k, h, nj);
345 if (coef == zero) continue;
346 for (std::size_t m = 0; m < MR; ++m)
347 val += coef * V.p(j, nj, k, i, 0, u, m);
348 }
349 }
350 for (std::size_t j = 0; j < M; ++j) {
351 if (j == i) continue;
352 for (std::size_t nj = 0; nj <= Fi(j); ++nj)
353 for (std::size_t k = 0; k < Ki(i); ++k) {
354 // The emitting population is 1: the transition leaves
355 // station i holding exactly one job.
356 const T coef = q.at(i, j, k, u, 1);
357 if (coef == zero) continue;
358 for (std::size_t h = 0; h < Ki(j); ++h)
359 for (std::size_t m = 0; m < MR; ++m)
360 val -= coef * V.p(j, nj, h, i, 1, k, m);
361 }
362 }
363 ceq.push_back(val);
364 }
365
366 // THM3 {i, ni in 0..F[i]-1}: balance across the ni -> ni+1 boundary.
367 for (std::size_t i = 0; i < M; ++i)
368 for (std::size_t ni = 0; ni + 1 <= Fi(i); ++ni) {
369 T val = zero;
370 for (std::size_t j = 0; j < M; ++j) {
371 if (j == i) continue;
372 for (std::size_t nj = 1; nj <= Fi(j); ++nj)
373 for (std::size_t k = 0; k < Ki(j); ++k) {
374 T coef = zero;
375 for (std::size_t h = 0; h < Ki(j); ++h) coef += q.at(j, i, k, h, nj);
376 if (coef == zero) continue;
377 for (std::size_t u = 0; u < Ki(i); ++u)
378 for (std::size_t m = 0; m < MR; ++m)
379 val += coef * V.p(j, nj, k, i, ni, u, m);
380 }
381 }
382 for (std::size_t j = 0; j < M; ++j) {
383 if (j == i) continue;
384 for (std::size_t nj = 0; nj <= Fi(j); ++nj)
385 for (std::size_t k = 0; k < Ki(i); ++k) {
386 T coef = zero;
387 for (std::size_t h = 0; h < Ki(i); ++h)
388 coef += q.at(i, j, k, h, ni + 1);
389 if (coef == zero) continue;
390 for (std::size_t u = 0; u < Ki(j); ++u)
391 for (std::size_t m = 0; m < MR; ++m)
392 val -= coef * V.p(j, nj, u, i, ni + 1, k, m);
393 }
394 }
395 ceq.push_back(val);
396 }
397
398 // THM4: an inequality, stated as >= upstream and stored with the sign
399 // swapped so that every row of this block reads g(x) <= 0.
400 for (std::size_t j = 0; j < M; ++j)
401 for (std::size_t k = 0; k < Ki(j); ++k)
402 for (std::size_t i = 0; i < M; ++i)
403 for (std::size_t m = 0; m < MR; ++m) {
404 T val = zero;
405 for (std::size_t t = 0; t < M; ++t)
406 for (std::size_t h = 0; h < Ki(t); ++h)
407 for (std::size_t njx = 0; njx <= N; ++njx)
408 for (std::size_t nt = 0; nt <= N; ++nt)
409 val -= num_traits<T>::from_int(static_cast<long>(nt)) *
410 V.p(j, njx, k, t, nt, h, m);
411 for (std::size_t h = 0; h < Ki(i); ++h)
412 for (std::size_t njx = 0; njx <= N; ++njx)
413 for (std::size_t ni = 1; ni <= N; ++ni)
414 val += num_traits<T>::from_int(static_cast<long>(N)) *
415 V.p(j, njx, k, i, ni, h, m);
416 c.push_back(val);
417 }
418
419 return out;
420}
421
422namespace qrfdetail {
423
424/** Assemble, reduce, start and solve: the body every entry point shares. */
425template <class T, class Obj, class Grad>
426QrfMetrics<T> run_noblo(const QrfRates<T>& q, std::size_t M, std::size_t MR,
427 const Matrix<int>& BB, const std::vector<int>& F, std::size_t N,
428 const std::vector<int>& K, Obj objective, Grad gradient,
429 const std::string& name, const Matrix<T>* alpha = nullptr) {
430 const std::size_t n = qrf_num_vars(M, N, K, MR);
431
432 // Both blocks come from ONE residual callback each, so a family added to
433 // the inventory reaches the equalities and the inequalities together.
434 const QrfAffine<T> eq = qrf_affine_matrices<T>(
435 [&](const std::vector<T>& z) { return sub_qrfcon_noblo(z, q, M, MR, BB, F, N, K).eq; }, n);
436 const QrfAffine<T> ub = qrf_affine_matrices<T>(
437 [&](const std::vector<T>& z) { return sub_qrfcon_noblo(z, q, M, MR, BB, F, N, K).ineq; },
438 n);
439 const QrfReduced<T> red = qrf_reduce_equalities(eq.A, eq.b);
440
441 const std::vector<T> x0 = qrf_feasible_start(red.A, red.b, ub.A, ub.b, n);
442 const std::vector<T> xopt =
443 solve_qrf_nlp(objective, gradient, x0, red.A, red.b, ub.A, ub.b, name);
444 return qrf_extract_results(sub_qrfvar(xopt, M, N, K, MR), M, K, F, MR, alpha);
445}
446
447} // namespace qrfdetail
448
449/**
450 * `qrf_noblo_mmi`: the no-blocking bound under mutual-information minimization.
451 *
452 * @param M number of queues
453 * @param K phases per queue
454 * @param N total population
455 * @param mu (M x Kmax*Kmax) completion rates
456 * @param v (M x Kmax*Kmax) background rates
457 * @param rt (M x M) routing matrix
458 */
459template <class T>
460QrfMetrics<T> qrf_noblo_mmi(std::size_t M, const std::vector<int>& K, std::size_t N,
461 const Matrix<T>& mu, const Matrix<T>& v, const Matrix<T>& rt) {
462 const std::size_t MR = 1; // no blocking: one configuration, by definition
463 Matrix<int> BB(1, M, 0);
464 const std::vector<int> F(M, static_cast<int>(N));
465 const QrfRates<T> q = qrf_build_q(M, K, mu, v, rt);
466 const std::vector<long> idx = qrf_index_map(M, N, K, MR);
467 return qrfdetail::run_noblo<T>(
468 q, M, MR, BB, F, N, K,
469 [&](const std::vector<T>& x) { return mmi_objective(x, M, N, K, F, MR); },
470 [&](const std::vector<T>& x) { return mmi_gradient(x, M, N, K, F, MR, idx); },
471 "qrf_noblo_mmi");
472}
473
474/**
475 * `qrf_noblo_bethe`: the same polytope under the tree-reweighted free entropy.
476 *
477 * The polytope, the phase-1 feasible start and the NLP call are exactly those
478 * of `qrf_noblo_mmi`; the objective is the only difference. See
479 * `bethe_objective` in `mapqn_qrf_common.h` for what it is and why the uniform
480 * edge weight is lambda = 1/M.
481 *
482 * ONE SOLVE, NO RESTARTS. The objective is convex on this polytope, so there
483 * is no second local minimum for a restart to find; the single solve from the
484 * phase-1 point returns the global optimum.
485 *
486 * @param M number of queues
487 * @param K phases per queue
488 * @param N total population
489 * @param mu (M x Kmax*Kmax) completion rates
490 * @param v (M x Kmax*Kmax) background rates
491 * @param rt (M x M) routing matrix
492 */
493template <class T>
494QrfMetrics<T> qrf_noblo_bethe(std::size_t M, const std::vector<int>& K, std::size_t N,
495 const Matrix<T>& mu, const Matrix<T>& v, const Matrix<T>& rt) {
496 const std::size_t MR = 1; // no blocking: one configuration, by definition
497 Matrix<int> BB(1, M, 0);
498 const std::vector<int> F(M, static_cast<int>(N));
499 const QrfRates<T> q = qrf_build_q(M, K, mu, v, rt);
500 const std::vector<long> idx = qrf_index_map(M, N, K, MR);
501 return qrfdetail::run_noblo<T>(
502 q, M, MR, BB, F, N, K,
503 [&](const std::vector<T>& x) { return bethe_objective(x, M, N, K, F, MR); },
504 [&](const std::vector<T>& x) { return bethe_gradient(x, M, N, K, F, MR, idx); },
505 "qrf_noblo_bethe");
506}
507
508/** `qrf_noblo_mem`: the same polytope under maximum entropy. */
509template <class T>
510QrfMetrics<T> qrf_noblo_mem(const std::vector<std::pair<Matrix<T>, Matrix<T>>>& MAPs,
511 std::size_t N, const Matrix<T>& rt) {
512 const std::size_t M = MAPs.size();
513 std::vector<int> K(M, 0);
514 for (std::size_t i = 0; i < M; ++i) K[i] = static_cast<int>(MAPs[i].first.rows());
515 Matrix<T> mu, v;
516 qrf_extract_mu_v(MAPs, M, K, &mu, &v);
517
518 const std::size_t MR = 1;
519 Matrix<int> BB(1, M, 0);
520 const std::vector<int> F(M, static_cast<int>(N));
521 const QrfRates<T> q = qrf_build_q(M, K, mu, v, rt);
522 const std::vector<long> idx = qrf_index_map(M, N, K, MR);
523 return qrfdetail::run_noblo<T>(
524 q, M, MR, BB, F, N, K,
525 [&](const std::vector<T>& x) { return mem_objective(x, M, N, K, F, MR); },
526 [&](const std::vector<T>& x) { return mem_gradient(x, M, N, K, F, MR, idx); },
527 "qrf_noblo_mem");
528}
529
530/**
531 * `qrf_noblo_mmi_ld`: MMI on the LOAD-DEPENDENT polytope.
532 *
533 * The only difference from `qrf_noblo_mmi` is the arity of q, and that is the
534 * whole point: with a population-free q the balance families cannot tell the
535 * rate at which a station empties at population n from the rate at n', so the
536 * polytope stops pinning the utilization.
537 *
538 * @param alpha (M x N) load-dependent scaling, alpha(i, n-1) being the AMPL
539 * alpha[i,n]; an empty matrix means all ones
540 */
541template <class T>
542QrfMetrics<T> qrf_noblo_mmi_ld(std::size_t M, const std::vector<int>& K, std::size_t N,
543 const Matrix<T>& mu, const Matrix<T>& v, const Matrix<T>& rt,
544 const Matrix<T>& alpha) {
545 const std::size_t MR = 1;
546 Matrix<int> BB(1, M, 0);
547 const std::vector<int> F(M, static_cast<int>(N));
548 const QrfRates<T> q = qrf_build_q_ld(M, K, mu, v, rt, N, alpha);
549 const std::vector<long> idx = qrf_index_map(M, N, K, MR);
550 return qrfdetail::run_noblo<T>(
551 q, M, MR, BB, F, N, K,
552 [&](const std::vector<T>& x) { return mmi_objective(x, M, N, K, F, MR); },
553 [&](const std::vector<T>& x) { return mmi_gradient(x, M, N, K, F, MR, idx); },
554 "qrf_noblo_mmi_ld", &alpha);
555}
556
557/**
558 * `qrf_noblo_mmi_linear`: the load-dependent no-blocking bound, under MMI.
559 *
560 * The `linear` in the name is about HOW the reference builds its constraints,
561 * not about which constraints they are and not about the objective: it emits
562 * the same inventory directly as sparse matrices instead of recovering it from
563 * a residual callback, because scipy's SLSQP under-allocates its Fortran
564 * workspace when the equality block outnumbers the variables and corrupts the
565 * heap rather than refusing. This port recovers the matrices affinely for every
566 * entry point and reduces the equality block before it reaches the optimizer,
567 * so the distinction does not arise and the two spellings are one function
568 * here. It is verified, not assumed: `test_mapqn_qrf_noblo.cpp` checks this
569 * against the reference.
570 *
571 * Until 2026-08-29 the MATLAB reference called its own mem() here, with mmi()
572 * surviving only in a commented-out line, and this port mirrored that: the
573 * entry point named for mutual-information minimisation returned an entropy
574 * extremum. The objective is now MMI in all four ports. MMI is not convex, so
575 * unlike the MEM it replaces this entry point has no unique optimum and agrees
576 * with the reference only where the polytope pins the answer.
577 */
578template <class T>
579QrfMetrics<T> qrf_noblo_mmi_linear(const std::vector<std::pair<Matrix<T>, Matrix<T>>>& MAPs,
580 std::size_t N, const Matrix<T>& rt, const Matrix<T>& alpha) {
581 const std::size_t M = MAPs.size();
582 std::vector<int> K(M, 0);
583 for (std::size_t i = 0; i < M; ++i) K[i] = static_cast<int>(MAPs[i].first.rows());
584 Matrix<T> mu, v;
585 qrf_extract_mu_v(MAPs, M, K, &mu, &v);
586
587 const std::size_t MR = 1;
588 Matrix<int> BB(1, M, 0);
589 const std::vector<int> F(M, static_cast<int>(N));
590 const QrfRates<T> q = qrf_build_q_ld(M, K, mu, v, rt, N, alpha);
591 const std::vector<long> idx = qrf_index_map(M, N, K, MR);
592 return qrfdetail::run_noblo<T>(
593 q, M, MR, BB, F, N, K,
594 [&](const std::vector<T>& x) { return mmi_objective(x, M, N, K, F, MR); },
595 [&](const std::vector<T>& x) { return mmi_gradient(x, M, N, K, F, MR, idx); },
596 "qrf_noblo_mmi_linear", &alpha);
597}
598
599} // namespace mapqn
600} // namespace line
601
602#endif // LINE_API_MAPQN_MAPQN_QRF_NOBLO_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
The exception types the port throws.
Shared machinery of the QRF nonlinear bounds (qrf_noblo_*, qrf_bas_*).
Dense matrix and non-owning view.
QrfRates< T > qrf_build_q(std::size_t M, const std::vector< int > &K, const Matrix< T > &mu, const Matrix< T > &v, const Matrix< T > &rt)
build_q_from_mu_v_rt: the population-free rates.
std::vector< T > mem_gradient(const std::vector< T > &x, std::size_t M, std::size_t N, const std::vector< int > &K, const std::vector< int > &F, std::size_t MR, const std::vector< long > &idx)
Gradient of mem_objective: d/dp of p log(p') is log p' + p/p'.
QrfMetrics< T > qrf_extract_results(const QrfVars< T > &v, std::size_t M, const std::vector< int > &K, const std::vector< int > &F, std::size_t MR, const Matrix< T > *alpha=nullptr)
extract_results: the diagonal marginals of the optimal tensor, plus the alpha-weighted mean BN.
T mem_objective(const std::vector< T > &x, std::size_t M, std::size_t N, const std::vector< int > &K, const std::vector< int > &F, std::size_t MR)
Maximum-entropy objective, returned as the NEGATIVE entropy +sum p log p over the diagonal entries,...
std::size_t qrf_num_vars(std::size_t M, std::size_t N, const std::vector< int > &K, std::size_t MR)
Number of decision variables the layout actually USES.
QrfMetrics< T > qrf_noblo_mem(const std::vector< std::pair< Matrix< T >, Matrix< T > > > &MAPs, std::size_t N, const Matrix< T > &rt)
qrf_noblo_mem: the same polytope under maximum entropy.
QrfAffine< T > qrf_affine_matrices(Fn fn, std::size_t n)
Recover (A, b) from an affine residual map.
T mmi_objective(const std::vector< T > &x, std::size_t M, std::size_t N, const std::vector< int > &K, const std::vector< int > &F, std::size_t MR)
Mutual-information objective.
QrfMetrics< T > qrf_noblo_mmi_ld(std::size_t M, const std::vector< int > &K, std::size_t N, const Matrix< T > &mu, const Matrix< T > &v, const Matrix< T > &rt, const Matrix< T > &alpha)
qrf_noblo_mmi_ld: MMI on the LOAD-DEPENDENT polytope.
QrfMetrics< T > qrf_noblo_mmi(std::size_t M, const std::vector< int > &K, std::size_t N, const Matrix< T > &mu, const Matrix< T > &v, const Matrix< T > &rt)
qrf_noblo_mmi: the no-blocking bound under mutual-information minimization.
QrfRates< T > qrf_build_q_ld(std::size_t M, const std::vector< int > &K, const Matrix< T > &mu, const Matrix< T > &v, const Matrix< T > &rt, std::size_t N, const Matrix< T > &alpha)
build_q_ld: the load-dependent rates.
std::vector< T > qrf_feasible_start(const Matrix< T > &Aeq, const std::vector< T > &beq, const Matrix< T > &Aub, const std::vector< T > &bub, std::size_t n)
A point of the polytope, as the phase 1 of qrf_noblo_start.m.
QrfMetrics< T > qrf_noblo_mmi_linear(const std::vector< std::pair< Matrix< T >, Matrix< T > > > &MAPs, std::size_t N, const Matrix< T > &rt, const Matrix< T > &alpha)
qrf_noblo_mmi_linear: the load-dependent no-blocking bound, under MMI.
QrfVars< T > sub_qrfvar(const std::vector< T > &x, std::size_t M, std::size_t N, const std::vector< int > &K, std::size_t MR)
Unflatten x into the pair tensor and the effective rates.
std::vector< T > bethe_gradient(const std::vector< T > &x, std::size_t M, std::size_t N, const std::vector< int > &K, const std::vector< int > &F, std::size_t MR, const std::vector< long > &idx)
Gradient of bethe_objective.
std::vector< T > solve_qrf_nlp(Obj objective, Grad gradient, const std::vector< T > &x0, const Matrix< T > &Aeq, const std::vector< T > &beq, const Matrix< T > &Aub, const std::vector< T > &bub, const std::string &name, unsigned max_iter=200, double gap_tol=1e-10)
The same, with the polytope given as matrices rather than as an LpModel.
QrfReduced< T > qrf_reduce_equalities(const Matrix< T > &A, const std::vector< T > &b)
Drop the linearly dependent equality rows, keeping the feasible set exact.
std::vector< long > qrf_index_map(std::size_t M, std::size_t N, const std::vector< int > &K, std::size_t MR)
Flat position of every p2 entry, in the FILL ORDER of sub_qrfvar.
QrfMetrics< T > qrf_noblo_bethe(std::size_t M, const std::vector< int > &K, std::size_t N, const Matrix< T > &mu, const Matrix< T > &v, const Matrix< T > &rt)
qrf_noblo_bethe: the same polytope under the tree-reweighted free entropy.
std::vector< T > mmi_gradient(const std::vector< T > &x, std::size_t M, std::size_t N, const std::vector< int > &K, const std::vector< int > &F, std::size_t MR, const std::vector< long > &idx)
Gradient of mmi_objective.
T bethe_objective(const std::vector< T > &x, std::size_t M, std::size_t N, const std::vector< int > &K, const std::vector< int > &F, std::size_t MR)
Tree-reweighted (Bethe) free entropy at the uniform spanning-tree weight, the objective of qrf....
QrfConstraints< T > sub_qrfcon_noblo(const std::vector< T > &x, const QrfRates< T > &q, std::size_t M, std::size_t MR, const Matrix< int > &BB, const std::vector< int > &F, std::size_t N, const std::vector< int > &K)
The full no-blocking constraint inventory, sub_qrfcon_noblo.
void qrf_extract_mu_v(const std::vector< std::pair< Matrix< T >, Matrix< T > > > &MAPs, std::size_t M, const std::vector< int > &K, Matrix< T > *mu, Matrix< T > *v)
extract_mu_v_from_maps: the completion and background rates of each MAP.
Number-type abstraction for the templated API port.
The two constraint blocks: g(x) <= 0 and h(x) = 0.
The utilizations and queue lengths read off an optimal pair tensor.
The transition rates the constraint inventory reads.
T at4(std::size_t i, std::size_t j, std::size_t k, std::size_t h) const
The population-free lookup, for the arms that do not carry n.
std::vector< T > q4
[i][j][k][h]
T at(std::size_t i, std::size_t j, std::size_t k, std::size_t h, std::size_t n) const
std::vector< T > q5
[i][j][k][h][n], n the EMITTING station's population
The unflattened decision vector: the pair tensor and the effective rates.
const T & p(std::size_t j, std::size_t nj, std::size_t k, std::size_t i, std::size_t ni, std::size_t h, std::size_t m) const
std::vector< T > e
e[i*Kmax + k]