LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
map_optim_dist.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_MAM_MAP_OPTIM_DIST_H
6#define LINE_API_MAM_MAP_OPTIM_DIST_H
7
8/**
9 * @file
10 * @ingroup api_mam
11 * Fit the D1 of a MAP by MINIMIZING a distance to a reference MAP.
12 *
13 * Port of matlab/lib/kpctoolbox/map/map_optim_dist.m and map_optim_dist_acf.m
14 * (twins in python/line_solver/api/mapdist/continuous.py). The distances
15 * themselves -- `map_dist`, `map_dist_acf` -- are already in `map_dist.h`; what
16 * is here is the OPTIMIZATION over D1 with D0 held fixed.
17 *
18 * Reference: G. Horvath, "Measuring the distance between MAPs and some
19 * applications", ASMTA 2015, LNCS 9081, pp. 95-109.
20 *
21 * WHAT IS BEING FITTED, AND WHY D0 IS FIXED. Given a reference MAP A and a
22 * chosen D0 for the approximation B, the free parameter is B's D1. The
23 * constraints are exactly the two that make (B0, B1) a MAP with the DECLARED
24 * embedded distribution alB:
25 *
26 * - `alB (-B0)^-1 B1 = alB`, i.e. alB is stationary at arrivals. This is the
27 * `kron(I, alB inv(-B0))` block;
28 * - each row of B0 + B1 sums to zero, i.e. B1's row sums are `-B0`'s. This is
29 * the `kron(ones(1,N), I)` block.
30 *
31 * Every entry is bounded below by 1e-6 rather than by 0, which is the
32 * reference's own floor: an exactly zero entry makes the fitted MAP reducible,
33 * and the distance is then defined on a different state space than the one the
34 * constraints were written for.
35 *
36 * THE LAG-1 CASE IS A CONVEX QP AND IS SOLVED AS ONE. At L = 1 the distance is
37 * a quadratic form in vec(B1),
38 *
39 * d(vB1) = vB1' H vB1 + vA1' H_AA vA1 - 2 vA1' H_AB vB1,
40 *
41 * with `H = mkron(X_BB, Z_BB)` positive semidefinite, so the reference calls
42 * `quadprog` and gets a global optimum. Every other case is a general nonlinear
43 * program over the same affine set and the reference calls `fmincon`, which is
44 * local. Both are served here by `auglag`, and the DISTINCTION IS PRESERVED IN
45 * WHAT THE RESULT PROMISES: `global` is set only on the L = 1 path.
46 *
47 * The six Lyapunov solves are the reference's; `lyap_solve` is the tree's
48 * MATLAB-compatible `lyap(A,B,C)`, solving `A X + X B + C = 0`.
49 *
50 * COLUMN-MAJOR THROUGHOUT. `vec` here is MATLAB's, stacking COLUMNS, because
51 * the Kronecker identity the quadratic form rests on (`vec(A X B) = kron(B', A)
52 * vec(X)`) holds in that convention and in no other. Getting this wrong
53 * transposes the fitted D1 without changing its row sums, so the constraints
54 * still pass and only the distance is wrong.
55 *
56 * ARITHMETIC: transcendental, inherited from the distances.
57 */
58
59#include <algorithm>
60#include <cmath>
61#include <cstddef>
62#include <vector>
63
66#include "line/util/lu.h"
67#include "line/num/number.h"
68#include "line/util/auglag.h"
69#include "line/util/error.h"
70#include "line/util/matrix.h"
71#include "line/util/sylvester.h"
72
73namespace line {
74namespace mam {
75
76/** What an optimizing distance fit returns. */
77template <class T>
79 Matrix<T> B1; ///< the fitted D1
80 T d; ///< the distance achieved
81 bool global = false; ///< true only on the convex lag-1 path
82};
83
84namespace optdistdetail {
85
86/** MATLAB's `vec`: the COLUMNS stacked. */
87template <class T>
88std::vector<T> vec(const Matrix<T>& A) {
89 std::vector<T> v(A.rows() * A.cols(), num_traits<T>::from_int(0));
90 std::size_t c = 0;
91 for (std::size_t j = 0; j < A.cols(); ++j)
92 for (std::size_t i = 0; i < A.rows(); ++i) v[c++] = A(i, j);
93 return v;
94}
95
96/** The inverse of `vec` at a known shape. */
97template <class T>
98Matrix<T> unvec(const std::vector<T>& v, std::size_t n) {
100 std::size_t c = 0;
101 for (std::size_t j = 0; j < n; ++j)
102 for (std::size_t i = 0; i < n; ++i) A(i, j) = v[c++];
103 return A;
104}
105
106/**
107 * `kron(A, B)` in MATLAB's ordering.
108 *
109 * Spelled locally rather than taken from `util/` because the name is already
110 * declared in the enclosing namespace and an unqualified call there is
111 * ambiguous; the two agree, this one just cannot be confused for the other.
112 */
113template <class T>
114Matrix<T> mkron(const Matrix<T>& A, const Matrix<T>& B) {
115 const std::size_t ar = A.rows(), ac = A.cols(), br = B.rows(), bc = B.cols();
116 Matrix<T> K(ar * br, ac * bc, num_traits<T>::from_int(0));
117 for (std::size_t i = 0; i < ar; ++i)
118 for (std::size_t j = 0; j < ac; ++j)
119 for (std::size_t k = 0; k < br; ++k)
120 for (std::size_t l = 0; l < bc; ++l)
121 K(i * br + k, j * bc + l) = A(i, j) * B(k, l);
122 return K;
123}
124
125/** The row sums of -M, as a column. */
126template <class T>
127Matrix<T> neg_row_sums(const Matrix<T>& M) {
128 Matrix<T> v(M.rows(), 1, num_traits<T>::from_int(0));
129 for (std::size_t i = 0; i < M.rows(); ++i) {
130 T s = num_traits<T>::from_int(0);
131 for (std::size_t j = 0; j < M.cols(); ++j) s += M(i, j);
132 v(i, 0) = -s;
133 }
134 return v;
135}
136
137/**
138 * The equality block: alB stationary at arrivals, then the row sums.
139 *
140 * Returned as (Aeq, beq) so both fitters state the SAME constraints; a fitter
141 * that rebuilt them would be free to drift from the other.
142 */
143template <class T>
144void build_constraints(const Matrix<T>& B0, const std::vector<T>& alB, Matrix<T>* Aeq,
145 std::vector<T>* beq) {
146 const std::size_t n = B0.rows();
147 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
148
149 Matrix<T> negB0(n, n, zero);
150 for (std::size_t i = 0; i < n; ++i)
151 for (std::size_t j = 0; j < n; ++j) negB0(i, j) = -B0(i, j);
152 // alB * inv(-B0), by solving rather than inverting.
153 Matrix<T> negB0T(n, n, zero);
154 for (std::size_t i = 0; i < n; ++i)
155 for (std::size_t j = 0; j < n; ++j) negB0T(i, j) = negB0(j, i);
156 const std::vector<T> row = solve(negB0T, alB); // (alB inv(-B0))' solves (-B0)' x = alB'
157
158 Matrix<T> rowM(1, n, zero);
159 for (std::size_t j = 0; j < n; ++j) rowM(0, j) = row[j];
160 Matrix<T> I(n, n, zero);
161 for (std::size_t i = 0; i < n; ++i) I(i, i) = one;
162 Matrix<T> ones1(1, n, one);
163
164 const Matrix<T> top = mkron(I, rowM); // (n x n^2)
165 const Matrix<T> bot = mkron(ones1, I); // (n x n^2)
166 *Aeq = Matrix<T>(2 * n, n * n, zero);
167 for (std::size_t i = 0; i < n; ++i)
168 for (std::size_t j = 0; j < n * n; ++j) {
169 (*Aeq)(i, j) = top(i, j);
170 (*Aeq)(n + i, j) = bot(i, j);
171 }
172 const Matrix<T> b = neg_row_sums(B0);
173 beq->assign(2 * n, zero);
174 for (std::size_t i = 0; i < n; ++i) {
175 (*beq)[i] = alB[i];
176 (*beq)[n + i] = b(i, 0);
177 }
178}
179
180/** Minimize `f` over {Aeq x = beq, x >= lo} from a feasible-ish start. */
181template <class T, class F>
182std::vector<T> constrained_min(F f, const Matrix<T>& Aeq, const std::vector<T>& beq,
183 const std::vector<T>& x0, const T& lo) {
184 const std::size_t m = x0.size();
185 auto h = [&Aeq, &beq, m](const std::vector<T>& x) {
186 std::vector<T> r(Aeq.rows(), num_traits<T>::from_int(0));
187 for (std::size_t i = 0; i < Aeq.rows(); ++i) {
188 T s = -beq[i];
189 for (std::size_t j = 0; j < m; ++j) s += Aeq(i, j) * x[j];
190 r[i] = s;
191 }
192 return r;
193 };
194 auto g = [](const std::vector<T>&) { return std::vector<T>(); };
195 std::vector<Bound<T>> bounds(m);
196 for (std::size_t j = 0; j < m; ++j) {
197 bounds[j].has_lo = true;
198 bounds[j].lo = lo;
199 }
200 return auglag(f, h, g, x0, bounds).x;
201}
202
203} // namespace optdistdetail
204
205/**
206 * Fit B1 minimizing the lag-L joint-density distance to `a`, with B0 fixed.
207 *
208 * @param a the reference MAP
209 * @param alA its embedded (at-arrivals) distribution
210 * @param B0 the approximation's D0, held fixed
211 * @param alB the approximation's declared embedded distribution
212 * @param L number of lags; L = 1 takes the convex quadratic path
213 */
214template <class T>
215MapOptimDist<T> map_optim_dist(const Map<T>& a, const std::vector<T>& alA, const Matrix<T>& B0,
216 const std::vector<T>& alB, unsigned L) {
217 using namespace optdistdetail;
218 static_assert(num_traits<T>::has_transcendental, "map_optim_dist needs the MAP distances");
219 const std::size_t n = B0.rows();
220 if (n == 0 || B0.cols() != n) throw InputError("map_optim_dist: B0 must be square");
221 if (alB.size() != n) throw InputError("map_optim_dist: alB has the wrong length");
222 if (alA.size() != a.D0.rows()) throw InputError("map_optim_dist: alA has the wrong length");
223 if (L == 0) throw InputError("map_optim_dist: at least one lag is required");
224
225 const T zero = num_traits<T>::from_int(0);
226 const T lo = num_traits<T>::from_double(1e-6);
227 Matrix<T> Aeq;
228 std::vector<T> beq;
229 build_constraints(B0, alB, &Aeq, &beq);
230
231 MapOptimDist<T> out;
232 out.d = zero;
233
234 if (L == 1) {
235 // The six Lyapunov solves of the reference, then the quadratic form.
236 const std::size_t na = a.D0.rows();
237 Matrix<T> A0t(na, na, zero), B0t(n, n, zero);
238 for (std::size_t i = 0; i < na; ++i)
239 for (std::size_t j = 0; j < na; ++j) A0t(i, j) = a.D0(j, i);
240 for (std::size_t i = 0; i < n; ++i)
241 for (std::size_t j = 0; j < n; ++j) B0t(i, j) = B0(j, i);
242
243 Matrix<T> alAc(na, 1, zero), alBc(n, 1, zero);
244 for (std::size_t i = 0; i < na; ++i) alAc(i, 0) = alA[i];
245 for (std::size_t i = 0; i < n; ++i) alBc(i, 0) = alB[i];
246 const Matrix<T> av = neg_row_sums(a.D0), bv = neg_row_sums(B0);
247
248 auto outer = [zero](const Matrix<T>& u, const Matrix<T>& v) {
249 Matrix<T> M(u.rows(), v.rows(), zero);
250 for (std::size_t i = 0; i < u.rows(); ++i)
251 for (std::size_t j = 0; j < v.rows(); ++j) M(i, j) = u(i, 0) * v(j, 0);
252 return M;
253 };
254
255 const Matrix<T> Z_AB = lyap_solve(A0t, B0, outer(alAc, alBc));
256 const Matrix<T> Z_AA = lyap_solve(A0t, a.D0, outer(alAc, alAc));
257 const Matrix<T> Z_BB = lyap_solve(B0t, B0, outer(alBc, alBc));
258 const Matrix<T> X_AB = lyap_solve(a.D0, B0t, outer(av, bv));
259 const Matrix<T> X_AA = lyap_solve(a.D0, A0t, outer(av, av));
260 const Matrix<T> X_BB = lyap_solve(B0, B0t, outer(bv, bv));
261
262 const Matrix<T> H = mkron(X_BB, Z_BB);
263 const Matrix<T> HAB = mkron(X_AB, Z_AB);
264 const Matrix<T> HAA = mkron(X_AA, Z_AA);
265 const std::vector<T> vA1 = vec(a.D1);
266
267 // f(x) = x' H x - 2 (vA1' HAB) x, plus the constant vA1' HAA vA1.
268 std::vector<T> lin(n * n, zero);
269 for (std::size_t j = 0; j < n * n; ++j) {
270 T s = zero;
271 for (std::size_t i = 0; i < vA1.size(); ++i) s += vA1[i] * HAB(i, j);
272 lin[j] = s;
273 }
274 T cst = zero;
275 for (std::size_t i = 0; i < vA1.size(); ++i)
276 for (std::size_t j = 0; j < vA1.size(); ++j) cst += vA1[i] * HAA(i, j) * vA1[j];
277
278 auto quad = [&H, &lin, cst, zero](const std::vector<T>& x) {
279 T v = cst;
280 for (std::size_t i = 0; i < x.size(); ++i) {
281 T row = zero;
282 for (std::size_t j = 0; j < x.size(); ++j) row += H(i, j) * x[j];
283 v += x[i] * row;
284 v -= num_traits<T>::from_int(2) * lin[i] * x[i];
285 }
286 return v;
287 };
288
289 // The start point satisfies the row sums by construction, which keeps
290 // the multiplier iteration from beginning far outside the affine set.
291 std::vector<T> x0(n * n, lo);
292 Matrix<T> seed(n, n, zero);
293 for (std::size_t i = 0; i < n; ++i)
294 for (std::size_t j = 0; j < n; ++j)
295 seed(i, j) = bv(i, 0) / num_traits<T>::from_int(static_cast<long>(n));
296 x0 = vec(seed);
297
298 const std::vector<T> sol = constrained_min(quad, Aeq, beq, x0, lo);
299 out.B1 = unvec(sol, n);
300 out.d = quad(sol);
301 out.global = true; // the quadratic form is convex, so this is the optimum
302 return out;
303 }
304
305 // Every other lag count: the distance itself, minimized over the same set.
306 auto obj = [&a, &B0, &alA, &alB, L, n](const std::vector<T>& x) {
307 Map<T> b;
308 b.D0 = B0;
309 b.D1 = unvec(x, n);
310 return map_dist(a, b, L, alA, alB);
311 };
312 Matrix<T> seed(n, n, zero);
313 const Matrix<T> bv = neg_row_sums(B0);
314 for (std::size_t i = 0; i < n; ++i)
315 for (std::size_t j = 0; j < n; ++j)
316 seed(i, j) = bv(i, 0) / num_traits<T>::from_int(static_cast<long>(n));
317 const std::vector<T> sol = constrained_min(obj, Aeq, beq, vec(seed), lo);
318 out.B1 = unvec(sol, n);
319 out.d = obj(sol);
320 out.global = false; // a general nonlinear program: this is a local optimum
321 return out;
322}
323
324/**
325 * Fit B1 minimizing the AUTOCORRELATION distance to `a`, with B0 fixed.
326 *
327 * The reference re-evaluates the distance at the returned B1 rather than
328 * trusting the optimizer's own objective value, and so does this: the two can
329 * differ when the solve stops on its own tolerance, and the number a caller
330 * reports should be the distance of the MAP it was handed.
331 */
332template <class T>
333MapOptimDist<T> map_optim_dist_acf(const Map<T>& a, const std::vector<T>& alA,
334 const Matrix<T>& B0, const std::vector<T>& alB) {
335 using namespace optdistdetail;
336 static_assert(num_traits<T>::has_transcendental, "map_optim_dist_acf needs the MAP distances");
337 const std::size_t n = B0.rows();
338 if (n == 0 || B0.cols() != n) throw InputError("map_optim_dist_acf: B0 must be square");
339 if (alB.size() != n) throw InputError("map_optim_dist_acf: alB has the wrong length");
340 if (alA.size() != a.D0.rows()) throw InputError("map_optim_dist_acf: alA has the wrong length");
341
342 const T zero = num_traits<T>::from_int(0);
343 const T lo = num_traits<T>::from_double(1e-6);
344 Matrix<T> Aeq;
345 std::vector<T> beq;
346 build_constraints(B0, alB, &Aeq, &beq);
347
348 auto obj = [&a, &B0, &alA, &alB, n](const std::vector<T>& x) {
349 Map<T> b;
350 b.D0 = B0;
351 b.D1 = unvec(x, n);
352 return map_dist_acf(a, b, alA, alB);
353 };
354 Matrix<T> seed(n, n, zero);
355 const Matrix<T> bv = neg_row_sums(B0);
356 for (std::size_t i = 0; i < n; ++i)
357 for (std::size_t j = 0; j < n; ++j)
358 seed(i, j) = bv(i, 0) / num_traits<T>::from_int(static_cast<long>(n));
359
360 MapOptimDist<T> out;
361 const std::vector<T> sol = constrained_min(obj, Aeq, beq, vec(seed), lo);
362 out.B1 = unvec(sol, n);
363 Map<T> b;
364 b.D0 = B0;
365 b.D1 = out.B1;
366 out.d = map_dist_acf(a, b, alA, alB);
367 out.global = false;
368 return out;
369}
370
371} // namespace mam
372} // namespace line
373
374#endif // LINE_API_MAM_MAP_OPTIM_DIST_H
Augmented Lagrangian method for equality- and inequality-constrained minimization,...
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.
LU factorization with partial pivoting, templated on the number type.
Analytic distances between continuous-time MAPs.
Markovian arrival process descriptors: stationary vectors, rate, moments, autocorrelation and the ind...
Dense matrix and non-owning view.
MapOptimDist< T > map_optim_dist(const Map< T > &a, const std::vector< T > &alA, const Matrix< T > &B0, const std::vector< T > &alB, unsigned L)
Fit B1 minimizing the lag-L joint-density distance to a, with B0 fixed.
T map_dist_acf(const Map< T > &a, const Map< T > &b, const std::vector< T > &alA, const std::vector< T > &alB)
Squared L2 distance between the two autocorrelation functions (map_dist_acf.m).
Definition map_dist.h:241
MapOptimDist< T > map_optim_dist_acf(const Map< T > &a, const std::vector< T > &alA, const Matrix< T > &B0, const std::vector< T > &alB)
Fit B1 minimizing the AUTOCORRELATION distance to a, with B0 fixed.
T map_dist(const Map< T > &a, const Map< T > &b, unsigned L, const std::vector< T > &alA, const std::vector< T > &alB)
Squared L2 distance between the two joint densities up to lag L (map_dist.m).
Definition map_dist.h:226
AugLagResult< T > auglag(F f, H h, G g, const std::vector< T > &x0, const std::vector< Bound< T > > &bounds, const AugLagOptions< T > &opt)
Augmented Lagrangian with a scalar objective and a simplex inner solver.
Definition auglag.h:141
Matrix< T > lyap_solve(const Matrix< T > &A, const Matrix< T > &B, const Matrix< T > &C)
MATLAB lyap(A,B,C) solves A X + X B + C = 0, i.e.
Definition sylvester.h:123
std::vector< T > solve(const Matrix< T > &A, const std::vector< T > &b)
Convenience: solve Ax = b, leaving A and b untouched.
Definition lu.h:158
Number-type abstraction for the templated API port.
What an optimizing distance fit returns.
Matrix< T > B1
the fitted D1
bool global
true only on the convex lag-1 path
T d
the distance achieved
A MAP as the pair of matrices (D0, D1).
Definition map_moment.h:53
Matrix< T > D1
Definition map_moment.h:55
Matrix< T > D0
Definition map_moment.h:54
The Sylvester equation A X + X B = C, and MATLAB's lyap(A,B,C).