LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
expm.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_UTIL_EXPM_H
6#define LINE_UTIL_EXPM_H
7
8/**
9 * @file
10 * @ingroup line_util
11 * Matrix exponential by scaling and squaring with a diagonal Pade approximant.
12 *
13 * This is the numerical primitive behind MATLAB's expm(), which the kpctoolbox
14 * MAP counting-process descriptors (map_cdf, map_pdf, map_acfc, map_count_var,
15 * map_varcount, map_count_moment) and the LRU(m)-MAP TTL cache approximation
16 * (cache_lrum_map_levelstats) all call. It has no MATLAB source file of its
17 * own in the tree; the reference is the algorithm of
18 *
19 * N. J. Higham, "The scaling and squaring method for the matrix exponential
20 * revisited", SIAM J. Matrix Anal. Appl. 26(4):1179-1193, 2005,
21 *
22 * which is what MATLAB implements. Given A, pick a scaling s so that
23 * ||2^-s A||_1 <= theta_m, evaluate the [m/m] Pade approximant
24 *
25 * r_m(X) = D_m(X)^-1 N_m(X), N_m(X) = sum_k c_k X^k,
26 * D_m(X) = sum_k (-1)^k c_k X^k,
27 * c_0 = 1, c_k = c_{k-1} (m-k+1) / (k (2m-k+1)),
28 *
29 * and square the result s times: exp(A) = r_m(2^-s A)^(2^s).
30 *
31 * ARITHMETIC: the answer is an approximation controlled by a tolerance -- the
32 * Pade truncation error is nonzero for a general A no matter how the arithmetic
33 * is carried out -- so this is gated on num_traits<T>::has_transcendental and
34 * refuses to instantiate at exact rational arithmetic. It is nevertheless exact
35 * (to rounding) on the two cases that matter for testing: A = 0 returns the
36 * identity by a shortcut, and a nilpotent A with A^q = 0, q <= 2m+1, is
37 * reproduced exactly because the Pade error term is O(X^(2m+1)).
38 *
39 * PRECISION AWARENESS: Higham's theta table is calibrated for double unit
40 * roundoff, and using it unchanged at Real<50> would silently deliver only
41 * double accuracy. The bound on the [m/m] Pade error,
42 *
43 * |exp(x) - r_m(x)| ~ C_m |x|^(2m+1), C_m = (m!)^2 / ((2m)! (2m+1)!),
44 *
45 * is inverted here against the working precision of T, so theta_m shrinks as
46 * the precision grows (theta_13 = 5.1 at double, 0.28 at 50 digits, 0.0039 at
47 * 100 digits). At double the resulting thresholds agree with Higham's table to
48 * within a few percent.
49 */
50
51#include <cmath>
52#include <cstddef>
53#include <limits>
54#include <vector>
55
56#include "line/num/number.h"
57#include "line/util/error.h"
58#include "line/util/linalg.h"
59#include "line/util/matrix.h"
60
61namespace line {
62
63namespace detail {
64
65/** Decimal digits carried by the working type, used to size the scaling. */
66template <class T>
67inline int expm_digits10() {
68 const int d = std::numeric_limits<T>::digits10;
69 return d > 0 ? d : 15;
70}
71
72/**
73 * Largest ||X||_1 for which the [m/m] Pade approximant meets the working
74 * precision, from C_m theta^(2m+1) <= 10^-digits10. Capped at Higham's 5.37 so
75 * the double path never scales less than the published algorithm.
76 */
77inline double expm_theta(int m, int digits10) {
78 const double lgC = 2.0 * std::lgamma(m + 1.0) - std::lgamma(2.0 * m + 1.0) -
79 std::lgamma(2.0 * m + 2.0);
80 const double log10C = lgC / std::log(10.0);
81 const double t = std::pow(10.0, (-log10C - digits10) / (2.0 * m + 1.0));
82 return t < 5.37 ? t : 5.37;
83}
84
85/** ||A||_1, the maximum absolute column sum, as a double. */
86template <class T>
87double expm_norm1(const Matrix<T>& A) {
88 double best = 0.0;
89 for (std::size_t j = 0; j < A.cols(); ++j) {
90 double s = 0.0;
91 for (std::size_t i = 0; i < A.rows(); ++i)
92 s += num_traits<T>::to_double(num_abs(T(A(i, j))));
93 if (s > best) best = s;
94 }
95 return best;
96}
97
98/** Diagonal [m/m] Pade approximant of exp at the matrix X. */
99template <class T>
100Matrix<T> expm_pade(const Matrix<T>& X, unsigned m) {
101 const std::size_t n = X.rows();
102 // Pade coefficients c_k, exact ratios of integers evaluated in T.
103 std::vector<T> c(m + 1);
104 c[0] = num_traits<T>::from_int(1);
105 for (unsigned k = 1; k <= m; ++k) {
106 const T num = num_traits<T>::from_int(static_cast<long>(m - k + 1));
107 const T den = num_traits<T>::from_int(static_cast<long>(k)) *
108 num_traits<T>::from_int(static_cast<long>(2 * m - k + 1));
109 c[k] = c[k - 1] * num / den;
110 }
111
112 Matrix<T> N = eye<T>(n); // c_0 I
113 Matrix<T> D = eye<T>(n); // c_0 I
114 Matrix<T> P = eye<T>(n); // X^k
115 for (unsigned k = 1; k <= m; ++k) {
116 P = matmul(P, X);
117 const bool odd = (k % 2u) == 1u;
118 for (std::size_t i = 0; i < n; ++i)
119 for (std::size_t j = 0; j < n; ++j) {
120 const T term = c[k] * P(i, j);
121 N(i, j) += term;
122 if (odd)
123 D(i, j) -= term;
124 else
125 D(i, j) += term;
126 }
127 }
128 return matmul(inverse(D), N);
129}
130
131} // namespace detail
132
133/**
134 * Matrix exponential exp(A).
135 *
136 * @param A square matrix
137 * @return exp(A), by scaling and squaring with a Pade approximant whose degree
138 * and scaling are chosen for the working precision of T
139 */
140template <class T>
143 "expm is a tolerance-controlled approximation and requires "
144 "transcendental (inexact) arithmetic");
145 const std::size_t n = A.rows();
146 if (A.cols() != n) throw InputError("expm: matrix is not square");
147 if (n == 0) throw InputError("expm: empty matrix");
148
149 const T zero = num_traits<T>::from_int(0);
150 bool allzero = true;
151 for (std::size_t i = 0; i < n && allzero; ++i)
152 for (std::size_t j = 0; j < n; ++j)
153 if (!(A(i, j) == zero)) {
154 allzero = false;
155 break;
156 }
157 if (allzero) return eye<T>(n); // exp(0) = I, exactly
158
159 const double nrm = detail::expm_norm1(A);
160 if (!(nrm == nrm) || nrm == std::numeric_limits<double>::infinity())
161 throw NumericError("expm: matrix contains a non-finite entry");
162
163 const int d10 = detail::expm_digits10<T>();
164 const unsigned degrees[5] = {3u, 5u, 7u, 9u, 13u};
165 unsigned m = 13u;
166 int s = 0;
167 bool picked = false;
168 for (int k = 0; k < 5; ++k) {
169 if (nrm <= detail::expm_theta(static_cast<int>(degrees[k]), d10)) {
170 m = degrees[k];
171 picked = true;
172 break;
173 }
174 }
175 if (!picked) {
176 const double theta13 = detail::expm_theta(13, d10);
177 s = static_cast<int>(std::ceil(std::log2(nrm / theta13)));
178 if (s < 0) s = 0;
179 if (s > 4096) throw NumericError("expm: matrix norm is too large to scale");
180 }
181
182 Matrix<T> X = A;
183 if (s > 0) {
184 const T half = num_traits<T>::from_rational(1, 2);
185 T scale = num_traits<T>::from_int(1);
186 for (int k = 0; k < s; ++k) scale *= half; // 2^-s, exact in binary FP
187 for (std::size_t i = 0; i < n; ++i)
188 for (std::size_t j = 0; j < n; ++j) X(i, j) *= scale;
189 }
190
191 Matrix<T> E = detail::expm_pade(X, m);
192 for (int k = 0; k < s; ++k) E = matmul(E, E);
193 return E;
194}
195
196/** exp(t A), the form every MAP descriptor actually needs. */
197template <class T>
198Matrix<T> expm(const Matrix<T>& A, const T& t) {
199 Matrix<T> B = A;
200 for (std::size_t i = 0; i < B.rows(); ++i)
201 for (std::size_t j = 0; j < B.cols(); ++j) B(i, j) *= t;
202 return expm(B);
203}
204
205} // namespace line
206
207#endif // LINE_UTIL_EXPM_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
NumericError(const std::string &what)
Definition error.h:45
The exception types the port throws.
Dense linear algebra over the templated number type: products, identity, inverse, and powers.
Dense matrix and non-owning view.
T num_abs(const T &v)
Definition number.h:172
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
Matrix< T > eye(std::size_t n)
Identity of order n.
Definition linalg.h:28
Matrix< T > expm(const Matrix< T > &A)
Matrix exponential exp(A).
Definition expm.h:141
Number-type abstraction for the templated API port.