LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
map_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_DIST_H
6#define LINE_API_MAM_MAP_DIST_H
7
8/**
9 * @file
10 * @ingroup api_mam
11 * Analytic distances between continuous-time MAPs.
12 *
13 * Templated port of matlab/lib/kpctoolbox/map: map_exp_mul_int.m,
14 * map_geo_mul_sum.m, map_dist.m, map_dist_acf.m, map_dist_lag1.m and
15 * map_feastol.m. These are the objective functions the KPC fitters minimise, so
16 * they are evaluated once per iteration and must agree with the reference to
17 * full precision, not merely to the tolerance of a fitted MAP.
18 *
19 * The discrete-time twins live in dmap.h and rest on Stein equations
20 * A X B - X + C = 0; the continuous ones rest on Sylvester equations
21 * A X + X B + C = 0, MATLAB's three-argument lyap. Both are solved here through
22 * the Kronecker form, which is exact in the rational backend where a
23 * Schur-based solver could not be. The single exception is map_geo_mul_sum,
24 * which still needs a Stein solve because its recursion is on the embedded
25 * chain rather than on the generator.
26 *
27 * SIGN TRAP: MATLAB's lyap(A, B, C) solves A X + X B + C = 0, so C enters with
28 * a PLUS. Reading it as the control-theory Lyapunov equation A X + X A' = -C
29 * flips the sign of every distance and leaves the minimiser unchanged, which is
30 * why a fitter can look healthy on top of the error.
31 */
32
33#include <cstddef>
34#include <vector>
35
36#include "line/api/mam/dmap.h"
38#include "line/num/number.h"
39#include "line/util/error.h"
40#include "line/util/linalg.h"
41#include "line/util/lu.h"
42#include "line/util/matrix.h"
43
44namespace line {
45namespace mam {
46
47namespace detail {
48
49/** MATLAB lyap(A, B, C): solves A X + X B + C = 0 through the Kronecker form. */
50template <class T>
51Matrix<T> sylv_solve(const Matrix<T>& A, const Matrix<T>& B, const Matrix<T>& C) {
52 const std::size_t m = A.rows(), n = B.cols();
53 if (A.cols() != m || B.rows() != n) throw InputError("sylv_solve: A and B must be square");
54 if (C.rows() != m || C.cols() != n)
55 throw InputError("sylv_solve: the right-hand side is not conformable");
56 const std::size_t N = m * n;
57 Matrix<T> M(N, N, num_traits<T>::from_int(0));
58 // vec(A X) = kron(I_n, A) vec(X) and vec(X B) = kron(B', I_m) vec(X)
59 for (std::size_t j = 0; j < n; ++j)
60 for (std::size_t ia = 0; ia < m; ++ia)
61 for (std::size_t ja = 0; ja < m; ++ja) M(j * m + ia, j * m + ja) += A(ia, ja);
62 for (std::size_t jb = 0; jb < n; ++jb)
63 for (std::size_t ib = 0; ib < n; ++ib)
64 for (std::size_t k = 0; k < m; ++k) M(jb * m + k, ib * m + k) += B(ib, jb);
65 std::vector<T> rhs = vec_colmajor(C);
66 for (std::size_t k = 0; k < N; ++k) rhs[k] = -rhs[k];
67 const std::vector<T> x = solve(M, rhs);
68 Matrix<T> X(m, n, num_traits<T>::from_int(0));
69 for (std::size_t j = 0; j < n; ++j)
70 for (std::size_t i = 0; i < m; ++i) X(i, j) = x[j * m + i];
71 return X;
72}
73
74/** Transpose. */
75template <class T>
76Matrix<T> tr(const Matrix<T>& A) {
77 Matrix<T> B(A.cols(), A.rows());
78 for (std::size_t i = 0; i < A.rows(); ++i)
79 for (std::size_t j = 0; j < A.cols(); ++j) B(j, i) = A(i, j);
80 return B;
81}
82
83/** Row sums of -D0, the vector of total exit rates. */
84template <class T>
85Matrix<T> negrowsum(const Matrix<T>& D0) {
86 Matrix<T> v(D0.rows(), 1, num_traits<T>::from_int(0));
87 for (std::size_t i = 0; i < D0.rows(); ++i)
88 for (std::size_t j = 0; j < D0.cols(); ++j) v(i, 0) -= D0(i, j);
89 return v;
90}
91
92/** Outer product of a row-vector pair, u' * v. */
93template <class T>
94Matrix<T> outer(const std::vector<T>& u, const std::vector<T>& v) {
95 Matrix<T> M(u.size(), v.size(), num_traits<T>::from_int(0));
96 for (std::size_t i = 0; i < u.size(); ++i)
97 for (std::size_t j = 0; j < v.size(); ++j) M(i, j) = u[i] * v[j];
98 return M;
99}
100
101/**
102 * vec(A)' kron(X, Z) vec(B) without forming the Kronecker product.
103 *
104 * With the column-major vec the summand is A(i,j) X(j,l) Z(i,k) B(k,l), which
105 * collapses to the Frobenius inner product of Z with A X B'.
106 */
107template <class T>
108T quad_kron(const Matrix<T>& A, const Matrix<T>& X, const Matrix<T>& Z, const Matrix<T>& B) {
109 const Matrix<T> AXBt = matmul(matmul(A, X), tr(B));
110 if (AXBt.rows() != Z.rows() || AXBt.cols() != Z.cols())
111 throw InputError("quad_kron: the two Sylvester solutions are not conformable");
112 T s = num_traits<T>::from_int(0);
113 for (std::size_t i = 0; i < Z.rows(); ++i)
114 for (std::size_t j = 0; j < Z.cols(); ++j) s += Z(i, j) * AXBt(i, j);
115 return s;
116}
117
118/** Reciprocal 1-norm condition number, MATLAB's rcond. */
119template <class T>
120T rcond1(const Matrix<T>& M) {
121 const T zero = num_traits<T>::from_int(0);
122 T nM = zero;
123 for (std::size_t j = 0; j < M.cols(); ++j) {
124 T c = zero;
125 for (std::size_t i = 0; i < M.rows(); ++i) c += num_abs(M(i, j));
126 if (c > nM) nM = c;
127 }
128 if (nM == zero) return zero;
129 const Matrix<T> Mi = inverse(M);
130 T nI = zero;
131 for (std::size_t j = 0; j < Mi.cols(); ++j) {
132 T c = zero;
133 for (std::size_t i = 0; i < Mi.rows(); ++i) c += num_abs(Mi(i, j));
134 if (c > nI) nI = c;
135 }
136 if (nI == zero) return zero;
137 return num_traits<T>::from_int(1) / (nM * nI);
138}
139
140} // namespace detail
141
142/**
143 * Integral of the product of the two interarrival densities up to lag L.
144 *
145 * The reference recursion is Z_1 = lyap(B0', A0, alB' alA) followed by
146 * Z_i = lyap(B0', A0, B1' Z_{i-1} A1), read out as (-B0 1)' Z (-A0 1).
147 */
148template <class T>
149T map_exp_mul_int(const Map<T>& a, const Map<T>& b, unsigned L, const std::vector<T>& alA,
150 const std::vector<T>& alB) {
151 if (L == 0) throw InputError("map_exp_mul_int: the lag L must be positive");
152 const Matrix<T> B0t = detail::tr(b.D0), B1t = detail::tr(b.D1);
153 Matrix<T> Z = detail::sylv_solve(B0t, a.D0, detail::outer(alB, alA));
154 for (unsigned i = 1; i < L; ++i)
155 Z = detail::sylv_solve(B0t, a.D0, matmul(matmul(B1t, Z), a.D1));
156 const Matrix<T> ea = detail::negrowsum(a.D0), eb = detail::negrowsum(b.D0);
158 for (std::size_t i = 0; i < eb.rows(); ++i)
159 for (std::size_t j = 0; j < ea.rows(); ++j) d += eb(i, 0) * Z(i, j) * ea(j, 0);
160 return d;
161}
162
163/** map_exp_mul_int with both stationary embedded distributions taken from the MAPs. */
164template <class T>
165T map_exp_mul_int(const Map<T>& a, const Map<T>& b, unsigned L) {
166 return map_exp_mul_int(a, b, L, map_pie(a), map_pie(b));
167}
168
169/**
170 * Geometrically weighted sum of the cross moments of the two embedded chains.
171 *
172 * When the Stein operator is numerically singular the reference returns
173 * 1/rcond(M) as a large penalty rather than a distance, which is what steers the
174 * fitter away from that corner of the parameter space; the same guard is kept
175 * here, driven by the pivot growth of the Kronecker matrix.
176 */
177template <class T>
178T map_geo_mul_sum(const Map<T>& a, const Map<T>& b, const std::vector<T>& alA,
179 const std::vector<T>& alB) {
180 const std::size_t NA = a.order(), NB = b.order();
181 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
182 Matrix<T> negA0(NA, NA, zero), negB0(NB, NB, zero);
183 for (std::size_t i = 0; i < NA; ++i)
184 for (std::size_t j = 0; j < NA; ++j) negA0(i, j) = -a.D0(i, j);
185 for (std::size_t i = 0; i < NB; ++i)
186 for (std::size_t j = 0; j < NB; ++j) negB0(i, j) = -b.D0(i, j);
187 const Matrix<T> A0i = inverse(negA0), B0i = inverse(negB0);
188 Matrix<T> PAh = matmul(A0i, a.D1), PBh = matmul(B0i, b.D1);
189 for (std::size_t i = 0; i < NA; ++i)
190 for (std::size_t j = 0; j < NA; ++j) PAh(i, j) -= alA[j];
191 for (std::size_t i = 0; i < NB; ++i)
192 for (std::size_t j = 0; j < NB; ++j) PBh(i, j) -= alB[j];
193 // The Stein operator M = I - kron(PBh', PAh) is the one the reference conditions on
194 Matrix<T> M(NA * NB, NA * NB, zero);
195 for (std::size_t jb = 0; jb < NB; ++jb)
196 for (std::size_t ib = 0; ib < NB; ++ib)
197 for (std::size_t ia = 0; ia < NA; ++ia)
198 for (std::size_t ja = 0; ja < NA; ++ja)
199 M(jb * NA + ia, ib * NA + ja) = -PBh(ib, jb) * PAh(ia, ja);
200 for (std::size_t k = 0; k < NA * NB; ++k) M(k, k) += one;
201 const T rc = detail::rcond1(M);
202 if (rc == zero) throw InputError("map_geo_mul_sum: the Stein operator is singular");
203 if (rc < num_traits<T>::from_double(1e-10)) return one / rc;
204 // C = sum(A0i, 2) * (alB * B0i), an NA x NB rank-one right-hand side
205 std::vector<T> ra(NA, zero);
206 for (std::size_t i = 0; i < NA; ++i)
207 for (std::size_t j = 0; j < NA; ++j) ra[i] += A0i(i, j);
208 const std::vector<T> cb = vecmul(alB, B0i);
209 const Matrix<T> X = detail::stein_solve(PAh, PBh, detail::outer(ra, cb));
210 // d = sum(alA * A0i * X * B0i)
211 const std::vector<T> u = vecmul(vecmul(alA, A0i), X);
212 const std::vector<T> w = vecmul(u, B0i);
213 T d = zero;
214 for (std::size_t j = 0; j < NB; ++j) d += w[j];
215 return d;
216}
217
218/** map_geo_mul_sum with both embedded distributions taken from the MAPs. */
219template <class T>
220T map_geo_mul_sum(const Map<T>& a, const Map<T>& b) {
221 return map_geo_mul_sum(a, b, map_pie(a), map_pie(b));
222}
223
224/** Squared L2 distance between the two joint densities up to lag L (map_dist.m). */
225template <class T>
226T map_dist(const Map<T>& a, const Map<T>& b, unsigned L, const std::vector<T>& alA,
227 const std::vector<T>& alB) {
228 const T two = num_traits<T>::from_int(2);
229 return map_exp_mul_int(a, a, L + 1, alA, alA) - two * map_exp_mul_int(a, b, L + 1, alA, alB) +
230 map_exp_mul_int(b, b, L + 1, alB, alB);
231}
232
233/** map_dist with both embedded distributions taken from the MAPs. */
234template <class T>
235T map_dist(const Map<T>& a, const Map<T>& b, unsigned L) {
236 return map_dist(a, b, L, map_pie(a), map_pie(b));
237}
238
239/** Squared L2 distance between the two autocorrelation functions (map_dist_acf.m). */
240template <class T>
241T map_dist_acf(const Map<T>& a, const Map<T>& b, const std::vector<T>& alA,
242 const std::vector<T>& alB) {
243 const T two = num_traits<T>::from_int(2), four = num_traits<T>::from_int(4);
244 const T mA = map_moment(a, 1u), m2A = map_moment(a, 2u);
245 const T mB = map_moment(b, 1u), m2B = map_moment(b, 2u);
246 const T varA = m2A - mA * mA, varB = m2B - mB * mB;
247 if (varA == num_traits<T>::from_int(0) || varB == num_traits<T>::from_int(0))
248 throw InputError("map_dist_acf: a deterministic MAP has no autocorrelation");
249 return (map_geo_mul_sum(a, a, alA, alA) - m2A * m2A / four) / (varA * varA) -
250 two * (map_geo_mul_sum(a, b, alA, alB) - m2A * m2B / four) / (varA * varB) +
251 (map_geo_mul_sum(b, b, alB, alB) - m2B * m2B / four) / (varB * varB);
252}
253
254/** map_dist_acf with both embedded distributions taken from the MAPs. */
255template <class T>
256T map_dist_acf(const Map<T>& a, const Map<T>& b) {
257 return map_dist_acf(a, b, map_pie(a), map_pie(b));
258}
259
260/**
261 * Squared L2 distance between the two lag-one joint densities (map_dist_lag1.m).
262 *
263 * The quadratic form is vec(D1)' kron(X, Z) vec(D1) with X and Z the two
264 * Sylvester solutions; it is accumulated directly here rather than through the
265 * Kronecker product, which would be an order-four matrix in the MAP order.
266 */
267template <class T>
268T map_dist_lag1(const Map<T>& a, const Map<T>& b, const std::vector<T>& alA,
269 const std::vector<T>& alB) {
270 const std::size_t NA = a.order(), NB = b.order();
271 const Matrix<T> A0t = detail::tr(a.D0), B0t = detail::tr(b.D0);
272 const Matrix<T> ea = detail::negrowsum(a.D0), eb = detail::negrowsum(b.D0);
273 std::vector<T> av(NA), bv(NB);
274 for (std::size_t i = 0; i < NA; ++i) av[i] = ea(i, 0);
275 for (std::size_t i = 0; i < NB; ++i) bv[i] = eb(i, 0);
276 const Matrix<T> Z_AB = detail::sylv_solve(A0t, b.D0, detail::outer(alA, alB));
277 const Matrix<T> Z_AA = detail::sylv_solve(A0t, a.D0, detail::outer(alA, alA));
278 const Matrix<T> Z_BB = detail::sylv_solve(B0t, b.D0, detail::outer(alB, alB));
279 const Matrix<T> X_AB = detail::sylv_solve(a.D0, B0t, detail::outer(av, bv));
280 const Matrix<T> X_AA = detail::sylv_solve(a.D0, A0t, detail::outer(av, av));
281 const Matrix<T> X_BB = detail::sylv_solve(b.D0, B0t, detail::outer(bv, bv));
282 const T two = num_traits<T>::from_int(2);
283 return detail::quad_kron(b.D1, X_BB, Z_BB, b.D1) + detail::quad_kron(a.D1, X_AA, Z_AA, a.D1) -
284 two * detail::quad_kron(a.D1, X_AB, Z_AB, b.D1);
285}
286
287/** map_dist_lag1 with both embedded distributions taken from the MAPs. */
288template <class T>
289T map_dist_lag1(const Map<T>& a, const Map<T>& b) {
290 return map_dist_lag1(a, b, map_pie(a), map_pie(b));
291}
292
293} // namespace mam
294} // namespace line
295
296#endif // LINE_API_MAM_MAP_DIST_H
InputError(const std::string &what)
Definition error.h:39
std::size_t rows() const
Definition matrix.h:89
Discrete-time Markovian arrival processes (D-MAPs).
The exception types the port throws.
Dense linear algebra over the templated number type: products, identity, inverse, and powers.
LU factorization with partial pivoting, templated on the number type.
Markovian arrival process descriptors: stationary vectors, rate, moments, autocorrelation and the ind...
Dense matrix and non-owning view.
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
T map_dist_lag1(const Map< T > &a, const Map< T > &b, const std::vector< T > &alA, const std::vector< T > &alB)
Squared L2 distance between the two lag-one joint densities (map_dist_lag1.m).
Definition map_dist.h:268
T map_geo_mul_sum(const Map< T > &a, const Map< T > &b, const std::vector< T > &alA, const std::vector< T > &alB)
Geometrically weighted sum of the cross moments of the two embedded chains.
Definition map_dist.h:178
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
std::vector< T > map_pie(const Map< T > &m)
Phase distribution seen by an arriving job, pie = pi D1 / (pi D1 e).
Definition map_moment.h:89
T map_moment(const Map< T > &m, unsigned k)
Raw moment of order k of the inter-arrival time: k!
Definition map_moment.h:118
T map_exp_mul_int(const Map< T > &a, const Map< T > &b, unsigned L, const std::vector< T > &alA, const std::vector< T > &alB)
Integral of the product of the two interarrival densities up to lag L.
Definition map_dist.h:149
T num_abs(const T &v)
Definition number.h:172
std::vector< T > vecmul(const std::vector< T > &v, const Matrix< T > &A)
Row vector times matrix, v A.
Definition linalg.h:50
Matrix< T > inverse(const Matrix< T > &A)
Inverse by LU with one factorization and n back substitutions.
Definition linalg.h:72
Matrix< T > matmul(const Matrix< T > &A, const Matrix< T > &B)
Matrix product A B.
Definition linalg.h:36
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.
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
std::size_t order() const
Definition map_moment.h:57