akreon-linalg
Fixed-size linear algebra and numerical methods in C++26.
Loading...
Searching...
No Matches
matrix_exp.hpp
Go to the documentation of this file.
1/*
2 * Copyright © 2025-2026 Akreon LLC
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6#pragma once
7
8#include "eig_decomp.hpp"
10
11namespace linalg {
12
21template <VectorContainer Vec>
24 const Vec& D, real_type_t<vector_element_t<Vec>> tol = 1e-10) {
25 constexpr std::size_t N = vector_dim_v<Vec>;
26
29 std::size_t k = 0;
30
31 for (std::size_t i = 0; i < N; ++i) {
32 if (taken(i)) continue;
33
34 for (std::size_t j = i; j < N; ++j) {
35 if (!taken(j) && is_same(D(j), D(i), tol)) {
36 p(k++) = j;
37 taken(j) = true;
38 }
39 }
40 }
41
42 return p;
43}
44
52template <std::size_t N>
55 for (std::size_t i = 0; i < N; ++i) p_inv(p(i)) = i;
56 return p_inv;
57}
58
67template <MatrixContainer Mat>
68constexpr auto permute_ptjp(const Mat& J,
69 const Vector<std::size_t, mat_rows_v<Mat>>& p) {
70 constexpr std::size_t N = mat_rows_v<Mat>;
71 Mat J_p;
72 for (std::size_t i = 0; i < N; ++i)
73 for (std::size_t j = 0; j < N; ++j) J_p(i, j) = J(p(i), p(j));
74 return J_p;
75}
76
85template <MatrixContainer Mat>
86constexpr auto unpermute_papt(const Mat& A,
87 const Vector<std::size_t, mat_rows_v<Mat>>& p) {
88 constexpr std::size_t N = mat_rows_v<Mat>;
89 Mat R;
90 for (std::size_t i = 0; i < N; ++i)
91 for (std::size_t j = 0; j < N; ++j) R(p(i), p(j)) = A(i, j);
92 return R;
93}
94
120template <SquareMatrix Mat>
121constexpr auto matrix_exp(const Mat& A) {
122 using T = matrix_scalar_t<Mat>;
123 using ScalarT = real_type_t<T>;
124 using Complex = std::complex<ScalarT>;
125
126 constexpr auto N = mat_rows_v<Mat>;
127 static_assert(N >= 1, "Matrix exponential requires at least 1 row");
128
129 // Recover the similarity basis first. Stop if eigen iteration did not
130 // converge or the basis cannot be inverted; no partial exponential is used.
131 const auto eig_decomp = EigenDecomposition(A);
132 if (!eig_decomp.converged) return Matrix<Complex, N, N>{};
133
134 const auto V = eig_decomp.V;
135 const auto [V_inv, event] = MatrixInverse<decltype(V)>()(V);
137 const auto D = eig_decomp.lambda;
138 // Keep the transformed operator, rather than only diag(D): its off-diagonal
139 // entries carry the generalized-eigenvector couplings of defective roots.
140 const auto J = V_inv * (as_complex_matrix(A) * V);
141
142 // Apply the same permutation to rows and columns, preserving the operator
143 // while collecting each repeated root into one contiguous coordinate run.
144 const auto p = make_group_permutation(D);
145 const auto p_inv = invert_permutation(p);
146 const auto Jp = permute_ptjp(J, p); // P^T J P
147
149 std::size_t i = 0;
150 while (i < N) {
151 // [i, j) contains one eigenvalue group. Its size bounds the longest
152 // possible Jordan chain; several shorter chains can share the group.
153 std::size_t j = i + 1;
154 while (j < N && is_same(D(p(j)), D(p(i)))) ++j;
155 std::size_t chain_len = j - i;
156 const auto eig = D(p(i));
157 const auto exp_lambda = constexpr_exp_lambda(eig);
158
159 // A simple eigenvalue needs only its scalar exponential, with no
160 // nilpotent correction.
161 if (chain_len == 1) {
162 exp_Jp(i, i) = exp_lambda;
163 i = j;
164 continue;
165 }
166
167 // For J_block = lambda*I + N, evaluate exp(lambda)*exp(N).
169 // Initialize exp(N) with its identity term.
170 for (std::size_t a = 0; a < chain_len; ++a)
171 E_blk(i + a, i + a) = Complex(1);
172 // Extract the upper-triangular nilpotent part N = J_block - lambda*I.
173 for (std::size_t a = 0; a < chain_len; ++a)
174 for (std::size_t b = a; b < chain_len; ++b)
175 N_blk(i + a, i + b) = Jp(i + a, i + b) - (a == b ? eig : Complex(0));
176
177 // At iteration m, P holds N^m and inv_factorial becomes 1/m!. The series
178 // terminates at chain_len - 1 because the strictly upper-triangular
179 // nilpotent part has N^chain_len = 0. For a diagonalizable repeated root,
180 // N is zero and this leaves just the identity term.
183 for (std::size_t m = 1; m < chain_len; ++m) {
184 inv_factorial /= Complex(ScalarT(m));
185 for (std::size_t a = 0; a < chain_len; ++a)
186 for (std::size_t b = a; b < chain_len; ++b)
187 E_blk(i + a, i + b) += (P(i + a, i + b) * inv_factorial);
188 // N_blk is zero outside this Jordan block. Keep the full block when
189 // multiplying: dropping its last column loses higher superdiagonals.
190 P = P * N_blk;
191 }
192
193 // Reattach exp(lambda) to the completed nilpotent exponential and write
194 // this block into the grouped result before advancing to the next root.
195 for (std::size_t a = 0; a < chain_len; ++a)
196 for (std::size_t b = a; b < chain_len; ++b)
197 exp_Jp(i + a, i + b) = E_blk(i + a, i + b) * exp_lambda;
198
199 i = j;
200 }
201
202 // Undo the grouping and map exp(J) back as V*exp(J)*V^-1.
203 const auto exp_J = unpermute_papt(exp_Jp, p_inv);
204 return V * (exp_J * V_inv);
205}
206
207} // namespace linalg
Field classified by scalar_traits as complex.
typename vector_traits_t< Vec >::element_type vector_element_t
Registered vector coordinate type.
typename scalar_traits_t< T >::real_type real_type_t
Associated real component type registered for T.
constexpr auto as_complex_matrix(const Mat &A)
express matrix as complex one
constexpr auto permute_ptjp(const Mat &J, const Vector< std::size_t, mat_rows_v< Mat > > &p)
Reorder matrix rows and columns by the same permutation.
constexpr bool is_same(const T &a, const T &b, real_type_t< T > tol=1e-10)
Compare complex scalars by magnitude of their difference.
constexpr std::size_t mat_rows_v
Registered compile-time row count after cv/ref normalization.
constexpr auto make_group_permutation(const Vec &D, real_type_t< vector_element_t< Vec > > tol=1e-10)
Group approximately equal eigenvalues into contiguous index runs.
constexpr auto matrix_exp(const Mat &A)
Compute exp(A) using its eigenvector and generalized-eigenvector basis.
constexpr auto invert_permutation(const Vector< std::size_t, N > &p)
Invert an index permutation.
typename mat_traits< remove_cvref_t< Mat > >::value_type matrix_scalar_t
Registered matrix scalar type after cv/ref normalization.
constexpr auto unpermute_papt(const Mat &A, const Vector< std::size_t, mat_rows_v< Mat > > &p)
Scatter matrix entries through a row-and-column permutation.
constexpr auto constexpr_exp_lambda(const T &lambda, int terms=16)
Approximate the exponential of a complex scalar.
Computes eigenvalues and eigenvectors satisfying A V = V diag(lambda).
Owning, fixed-size, row-major matrix container over a scalar field.
Definition matrix.hpp:44
Owning, fixed-size vector container.
Definition vector.hpp:36