// Copyright (C) 2026 Kiyotsugu Arai // SPDX-License-Identifier: LGPL-3.0-or-later // matrix_functions.hpp // // Matrix functions: expm (matrix exponential), sqrtm (matrix square root), logm (matrix logarithm) // // Algorithms: // expm — Scaling-and-squaring + Padé [13/13] (Higham 2005) // sqrtm — Schur decomposition + Parlett recursion (Björck-Hammarling 1983) // logm — Schur decomposition + Parlett recursion #ifndef SANGI_MATRIX_FUNCTIONS_HPP #define SANGI_MATRIX_FUNCTIONS_HPP #include #include #include #include #include #include #include #include #include #include #include namespace sangi { namespace algorithms { namespace detail_matfun { // T → int conversion (for sangi::Float, demote to int via toDouble) // Used in expm's scaling parameter s = ceil(log2(norm/theta)) etc. template inline int float_to_int(const T& x) { if constexpr (std::is_floating_point_v) { return static_cast(x); } else { // sangi::Float: demote to double via .toDouble(), then to int return static_cast(x.toDouble()); } } } //===================================================================== // Helper functions (internal) //===================================================================== namespace detail_matfun { // Matrix 1-norm: max_j Σ_i |A_{ij}| (maximum absolute column sum) template T matrix_one_norm(const BaseMatrix& A) { const auto rows = A.rows(); const auto cols = A.cols(); T max_col_sum = T(0); for (std::size_t j = 0; j < cols; ++j) { T col_sum = T(0); for (std::size_t i = 0; i < rows; ++i) { col_sum += std::abs(A(i, j)); } if (col_sum > max_col_sum) { max_col_sum = col_sum; } } return max_col_sum; } // Detect the block structure of a quasi-upper-triangular matrix // block_starts[k] = starting row of the k-th block // block_sizes[k] = 1 or 2 template void detect_blocks(const BaseMatrix& T_mat, std::size_t n, std::vector& block_starts, std::vector& block_sizes) { block_starts.clear(); block_sizes.clear(); std::size_t i = 0; while (i < n) { if (i + 1 < n && T_mat(i + 1, i) != T(0)) { // 2×2 block block_starts.push_back(i); block_sizes.push_back(2); i += 2; } else { // 1×1 block block_starts.push_back(i); block_sizes.push_back(1); i += 1; } } } // Compute the eigenvalues α±iβ of a 2×2 block B // B = [[a,b],[c,d]], α = (a+d)/2, β = sqrt(|bc + ((a-d)/2)²|) // Only for the complex-conjugate-pair case (disc < 0) template void block2_eigenvalues(const BaseMatrix& T_mat, std::size_t i, T& alpha, T& beta) { T a = T_mat(i, i), b = T_mat(i, i + 1); T c = T_mat(i + 1, i), d = T_mat(i + 1, i + 1); alpha = (a + d) / T(2); T half_diff = (a - d) / T(2); T disc = half_diff * half_diff + b * c; if (disc < T(0)) { beta = std::sqrt(-disc); } else { // Real-eigenvalue case (α ± sqrt(disc)) beta = T(0); } } // Compute f(B) for a 2×2 block B // f(B) = u·I + (v/β)·(B - α·I) // func_re, func_im: f(α+iβ) = func_re + i·func_im template void apply_block2_function( const BaseMatrix& T_mat, std::size_t i, Matrix& F, T alpha, T beta, T func_re, T func_im) { if (beta == T(0)) { // Real-eigenvalue case: a 2×2 block but β=0 // Set f(α) on the diagonal elements F(i, i) = func_re; F(i + 1, i + 1) = func_re; // Approximate the upper-triangular element by f'(α) * T_ij // (in practice this case is rare) F(i, i + 1) = T(0); F(i + 1, i) = T(0); return; } T u = func_re; T v_over_beta = func_im / beta; // Set F's (i,i), (i,i+1), (i+1,i), (i+1,i+1) F(i, i) = u + v_over_beta * (T_mat(i, i) - alpha); F(i, i + 1) = v_over_beta * T_mat(i, i + 1); F(i + 1, i) = v_over_beta * T_mat(i + 1, i); F(i + 1, i + 1) = u + v_over_beta * (T_mat(i + 1, i + 1) - alpha); } // Small-block Sylvester equation solver: A·X - X·B = C // A: p×p, B: q×q, C: p×q, X: p×q (p,q ∈ {1,2}) // Writes the solution into the submatrix F[ri:ri+p, ci:ci+q] of F template void solve_sylvester_block( const BaseMatrix& F, // F matrix (source for reading A_block and B_block) const BaseMatrix& T_mat, // original quasi-upper-triangular matrix Matrix& result, // destination for writing the solution std::size_t ri, std::size_t p, // row block (start, size) std::size_t ci, std::size_t q, // column block (start, size) std::size_t n) { // Compute the RHS: C = (F_ii · T_ij - T_ij · F_jj) + Σ_k (F_ik · T_kj - T_ik · F_kj) // where F_ii = result[ri:ri+p, ri:ri+p] (diagonal block) // F_jj = result[ci:ci+q, ci:ci+q] (diagonal block) // T_ij = T_mat[ri:ri+p, ci:ci+q] (upper-triangular block) // First compute the T_ij · (F_ii - F_jj) term // Then add the cross terms from the intermediate blocks // C = F_ii · T_ij T C[2][2] = {}; for (std::size_t a = 0; a < p; ++a) { for (std::size_t b = 0; b < q; ++b) { T sum = T(0); for (std::size_t k = 0; k < p; ++k) { sum += result(ri + a, ri + k) * T_mat(ri + k, ci + b); } C[a][b] = sum; } } // C -= T_ij · F_jj for (std::size_t a = 0; a < p; ++a) { for (std::size_t b = 0; b < q; ++b) { T sum = T(0); for (std::size_t k = 0; k < q; ++k) { sum += T_mat(ri + a, ci + k) * result(ci + k, ci + b); } C[a][b] -= sum; } } // Add the cross terms from the intermediate blocks // Σ_{k: ri+p ≤ k < ci} (F[ri:,k:] · T[k:,ci:] - T[ri:,k:] · F[k:,ci:]) for (std::size_t k = ri + p; k < ci; ++k) { for (std::size_t a = 0; a < p; ++a) { for (std::size_t b = 0; b < q; ++b) { C[a][b] += result(ri + a, k) * T_mat(k, ci + b) - T_mat(ri + a, k) * result(k, ci + b); } } } // Solve the Sylvester equation A·X - X·B = C // A = T_mat[ri:ri+p, ri:ri+p] (diagonal block of T, not F_ii!) // Note: deriving from the relation TF=FT gives // T_ii · F_ij - F_ij · T_jj = RHS // so A = T_ii, B = T_jj if (p == 1 && q == 1) { // Scalar: x = c / (t_ii - t_jj) T denom = T_mat(ri, ri) - T_mat(ci, ci); if (std::abs(denom) < std::numeric_limits::epsilon() * T(100)) { result(ri, ci) = T(0); // degenerate case } else { result(ri, ci) = C[0][0] / denom; } } else if (p == 1 && q == 2) { // 1×2 Sylvester: a·X - X·B = C // [a*x0 - x0*b00 - x1*b10, a*x1 - x0*b01 - x1*b11] = [c0, c1] T a = T_mat(ri, ri); T b00 = T_mat(ci, ci), b01 = T_mat(ci, ci + 1); T b10 = T_mat(ci + 1, ci), b11 = T_mat(ci + 1, ci + 1); // M · [x0, x1]^T = [c0, c1]^T T m00 = a - b00, m01 = -b10; T m10 = -b01, m11 = a - b11; T det = m00 * m11 - m01 * m10; if (std::abs(det) < std::numeric_limits::epsilon() * T(100)) { result(ri, ci) = T(0); result(ri, ci + 1) = T(0); } else { result(ri, ci) = ( m11 * C[0][0] - m01 * C[0][1]) / det; result(ri, ci + 1) = (-m10 * C[0][0] + m00 * C[0][1]) / det; } } else if (p == 2 && q == 1) { // 2×1 Sylvester: A·X - X·b = C T b = T_mat(ci, ci); T a00 = T_mat(ri, ri), a01 = T_mat(ri, ri + 1); T a10 = T_mat(ri + 1, ri), a11 = T_mat(ri + 1, ri + 1); // [a00-b, a01; a10, a11-b] · [x0; x1] = [c0; c1] T m00 = a00 - b, m01 = a01; T m10 = a10, m11 = a11 - b; T det = m00 * m11 - m01 * m10; if (std::abs(det) < std::numeric_limits::epsilon() * T(100)) { result(ri, ci) = T(0); result(ri + 1, ci) = T(0); } else { result(ri, ci) = ( m11 * C[0][0] - m01 * C[1][0]) / det; result(ri + 1, ci) = (-m10 * C[0][0] + m00 * C[1][0]) / det; } } else { // 2×2 Sylvester: A·X - X·B = C → 4×4 linear system T a00 = T_mat(ri, ri), a01 = T_mat(ri, ri + 1); T a10 = T_mat(ri + 1, ri), a11 = T_mat(ri + 1, ri + 1); T b00 = T_mat(ci, ci), b01 = T_mat(ci, ci + 1); T b10 = T_mat(ci + 1, ci), b11 = T_mat(ci + 1, ci + 1); // vec(X) = [x00, x10, x01, x11]^T (column-major) // M · vec(X) = vec(C) where M = I⊗A - B^T⊗I // M = [[a00-b00, a01, -b10, 0 ], // [a10, a11-b00, 0, -b10 ], // [-b01, 0, a00-b11, a01 ], // [0, -b01, a10, a11-b11]] T M[4][4] = { {a00 - b00, a01, -b10, T(0)}, {a10, a11 - b00, T(0), -b10}, {-b01, T(0), a00 - b11, a01}, {T(0), -b01, a10, a11 - b11} }; T rhs[4] = {C[0][0], C[1][0], C[0][1], C[1][1]}; // 4×4 Gaussian elimination (partial pivoting) int piv[4] = {0, 1, 2, 3}; for (int col = 0; col < 4; ++col) { // Pivot selection int max_row = col; T max_val = std::abs(M[piv[col]][col]); for (int row = col + 1; row < 4; ++row) { T v = std::abs(M[piv[row]][col]); if (v > max_val) { max_val = v; max_row = row; } } std::swap(piv[col], piv[max_row]); if (max_val < std::numeric_limits::epsilon() * T(100)) { // Singular case result(ri, ci) = T(0); result(ri + 1, ci) = T(0); result(ri, ci + 1) = T(0); result(ri + 1, ci + 1) = T(0); return; } // Elimination for (int row = col + 1; row < 4; ++row) { T factor = M[piv[row]][col] / M[piv[col]][col]; for (int k = col + 1; k < 4; ++k) { M[piv[row]][k] -= factor * M[piv[col]][k]; } rhs[piv[row]] -= factor * rhs[piv[col]]; } } // Back substitution T x[4]; for (int i = 3; i >= 0; --i) { T sum = rhs[piv[i]]; for (int k = i + 1; k < 4; ++k) { sum -= M[piv[i]][k] * x[k]; } x[i] = sum / M[piv[i]][i]; } result(ri, ci) = x[0]; result(ri + 1, ci) = x[1]; result(ri, ci + 1) = x[2]; result(ri + 1, ci + 1) = x[3]; } } } // namespace detail_matfun //===================================================================== // expm — matrix exponential //===================================================================== // expm via the Schur-Parlett method for arbitrary-precision T (= sangi::Float etc.). // Based on Higham (2008) "Functions of Matrices" §10.4. // 1. Schur decomposition A = Q T Q^T (T is quasi-upper-triangular) // 2. Compute exp(T_ii) directly on the diagonal blocks (1×1 / 2×2 complex conjugate) // 3. Fill in the upper-triangular off-diagonal via Parlett iteration // 4. exp(A) = Q exp(T) Q^T // Advantage: avoids cancellation inside Padé, no precision loss at arbitrary precision. // Drawback: O(n³) cost of the Schur decomposition (large constant); Parlett iteration breaks down on repeated eigenvalues // (precision degrades when eigenvalues are closely spaced; a separate block reorder would be needed but // this is accepted as the same scope of limitation as sqrtm/logm). template Matrix expm_schur_parlett(const BaseMatrix& A_basemat) { const auto n = A_basemat.rows(); if (n == 0) return Matrix(A_basemat); if (n == 1) { Matrix result(1, 1); result(0, 0) = std::exp(A_basemat(0, 0)); return result; } // Schur decomposition A = Q T Q^T auto [T_mat, Q] = schur_decomposition(A_basemat); // Detect the block structure std::vector block_starts, block_sizes; detail_matfun::detect_blocks(T_mat, n, block_starts, block_sizes); // Initialize the result matrix F = exp(T) Matrix F(n, n, T(0)); // Step 1: apply exp directly to the diagonal blocks for (std::size_t bi = 0; bi < block_starts.size(); ++bi) { std::size_t i = block_starts[bi]; std::size_t sz = block_sizes[bi]; if (sz == 1) { // 1×1 block: exp(T_ii) — high precision via sangi::exp F(i, i) = std::exp(T_mat(i, i)); } else { // 2×2 block: complex-conjugate eigenvalues α±iβ // exp(α+iβ) = exp(α)·cos(β) + i·exp(α)·sin(β) T alpha, beta; detail_matfun::block2_eigenvalues(T_mat, i, alpha, beta); T ea = std::exp(alpha); T u = ea * std::cos(beta); // real part of exp(α+iβ) T v = ea * std::sin(beta); // imaginary part detail_matfun::apply_block2_function(T_mat, i, F, alpha, beta, u, v); } } // Step 2: compute the super-diagonal blocks via Parlett iteration for (std::size_t d = 1; d < block_starts.size(); ++d) { for (std::size_t bi = 0; bi + d < block_starts.size(); ++bi) { std::size_t bj = bi + d; std::size_t ri = block_starts[bi]; std::size_t p = block_sizes[bi]; std::size_t ci = block_starts[bj]; std::size_t q = block_sizes[bj]; detail_matfun::solve_sylvester_block(F, T_mat, F, ri, p, ci, q, n); } } // Step 3: recover exp(A) = Q·F·Q^T Matrix Qt = Q.transpose(); return Q * F * Qt; } /** * @brief Matrix exponential exp(A) * * Scaling-and-squaring method + Padé [13/13] approximation (float/double). * Based on Higham (2005) "The Scaling and Squaring Method for * the Matrix Exponential Revisited". * * For arbitrary-precision types (= sangi::Float etc.), dispatches to the Schur-Parlett method: * - avoids the V±U cancellation of Padé * - can deliver precision according to target_precision * - cost: Schur decomposition O(n³) (Hessenberg + QR iteration) * * @tparam T element type (float, double, sangi::Float) * @param A input matrix (n×n square matrix) * @return exp(A) * @throws DimensionError if A is not square */ template Matrix expm(const BaseMatrix& A_basemat) { // Arbitrary-precision types dispatch to Schur-Parlett (suited for high precision) if constexpr (!std::is_floating_point_v) { return expm_schur_parlett(A_basemat); } Matrix A(A_basemat); // 1 copy via BaseMatrix ctor const auto n = A.rows(); if (n != A.cols()) { assert(false && "DimensionError: expm: matrix must be square"); throw DimensionError("expm: matrix must be square"); } if (n == 0) { return A; } if (n == 1) { Matrix result(1, 1); result(0, 0) = std::exp(A(0, 0)); return result; } auto I = Matrix::identity(n); // θ values for the Padé approximation (Higham Table 10.2) const T theta3 = T(1.495585217958292e-02); const T theta5 = T(2.539398330063230e-01); const T theta7 = T(9.504178996162932e-01); const T theta9 = T(2.097847961257068e+00); const T theta13 = T(5.371920351148152e+00); T norm1 = detail_matfun::matrix_one_norm(A); // Precompute A² (used at all orders) Matrix A2 = A * A; // When a low-order Padé can be used (||A||₁ is sufficiently small) auto pade_3 = [&]() -> Matrix { // b₀=120, b₁=60, b₂=12, b₃=1 const T b0 = T(120), b1 = T(60), b2 = T(12), b3 = T(1); Matrix U = A * (b3 * A2 + b1 * I); Matrix V = b2 * A2 + b0 * I; return solve_multi(Matrix(V - U), Matrix(V + U)); }; auto pade_5 = [&]() -> Matrix { const T b0 = T(30240), b1 = T(15120), b2 = T(3360); const T b3 = T(420), b4 = T(30), b5 = T(1); Matrix A4 = A2 * A2; Matrix U = A * (b5 * A4 + b3 * A2 + b1 * I); Matrix V = b4 * A4 + b2 * A2 + b0 * I; return solve_multi(Matrix(V - U), Matrix(V + U)); }; auto pade_7 = [&]() -> Matrix { const T b0 = T(17297280), b1 = T(8648640), b2 = T(1995840); const T b3 = T(277200), b4 = T(25200), b5 = T(1512); const T b6 = T(56), b7 = T(1); Matrix A4 = A2 * A2; Matrix A6 = A4 * A2; Matrix U = A * (b7 * A6 + b5 * A4 + b3 * A2 + b1 * I); Matrix V = b6 * A6 + b4 * A4 + b2 * A2 + b0 * I; return solve_multi(Matrix(V - U), Matrix(V + U)); }; auto pade_9 = [&]() -> Matrix { const T b0 = T(17643225600.0), b1 = T(8821612800.0); const T b2 = T(2075673600.0), b3 = T(302702400.0); const T b4 = T(30270240.0), b5 = T(2162160.0); const T b6 = T(110880.0), b7 = T(3960.0); const T b8 = T(90.0), b9 = T(1); Matrix A4 = A2 * A2; Matrix A6 = A4 * A2; Matrix A8 = A4 * A4; Matrix U = A * (b9 * A8 + b7 * A6 + b5 * A4 + b3 * A2 + b1 * I); Matrix V = b8 * A8 + b6 * A6 + b4 * A4 + b2 * A2 + b0 * I; return solve_multi(Matrix(V - U), Matrix(V + U)); }; // Arbitrary-precision types already dispatched to Schur-Parlett at the top of the function. The following is float/double only. if (norm1 <= theta3) return pade_3(); if (norm1 <= theta5) return pade_5(); if (norm1 <= theta7) return pade_7(); if (norm1 <= theta9) return pade_9(); // Padé [13/13] with scaling (theta13 = 5.37 as the target norm) int s = std::max(0, detail_matfun::float_to_int(std::ceil(std::log2(norm1 / theta13)))); T scale = std::ldexp(T(1), -s); Matrix As = A * scale; Matrix As2 = As * As; Matrix As4 = As2 * As2; Matrix As6 = As4 * As2; // Padé [13/13] coefficients const T b0 = T(64764752532480000.0); const T b1 = T(32382376266240000.0); const T b2 = T(7771770303897600.0); const T b3 = T(1187353796428800.0); const T b4 = T(129060195264000.0); const T b5 = T(10559470521600.0); const T b6 = T(670442572800.0); const T b7 = T(33522128640.0); const T b8 = T(1323241920.0); const T b9 = T(40840800.0); const T b10 = T(960960.0); const T b11 = T(16380.0); const T b12 = T(182.0); const T b13 = T(1.0); // U = As · (As6·(b13·As6 + b11·As4 + b9·As2) + b7·As6 + b5·As4 + b3·As2 + b1·I) Matrix inner_u = b13 * As6 + b11 * As4 + b9 * As2; Matrix U = As * (As6 * inner_u + b7 * As6 + b5 * As4 + b3 * As2 + b1 * I); // V = As6·(b12·As6 + b10·As4 + b8·As2) + b6·As6 + b4·As4 + b2·As2 + b0·I Matrix inner_v = b12 * As6 + b10 * As4 + b8 * As2; Matrix V = As6 * inner_v + b6 * As6 + b4 * As4 + b2 * As2 + b0 * I; // Solve (V - U) · F = V + U Matrix F = solve_multi(Matrix(V - U), Matrix(V + U)); // Square s times for (int i = 0; i < s; ++i) { F = F * F; } return F; } //===================================================================== // sqrtm — matrix square root //===================================================================== /** * @brief Matrix square root A^{1/2} (principal value) * * Schur decomposition + Parlett recursion (Björck-Hammarling 1983). * The principal square root exists when no eigenvalue of A lies on the negative real axis. * * @tparam T element type (float, double) * @param A input matrix (n×n square matrix) * @return A^{1/2} * @throws DimensionError if A is not square * @throws MathError if a negative real eigenvalue exists */ template Matrix sqrtm(const BaseMatrix& A) { const auto n = A.rows(); if (n != A.cols()) { assert(false && "DimensionError: sqrtm: matrix must be square"); throw DimensionError("sqrtm: matrix must be square"); } if (n == 0) { return Matrix(A); } if (n == 1) { if (A(0, 0) < T(0)) { throw MathError("sqrtm: matrix has negative real eigenvalue"); } Matrix result(1, 1); result(0, 0) = std::sqrt(A(0, 0)); return result; } // Schur decomposition: A = Q·T·Qᵀ auto [T_mat, Q] = schur_decomposition(A); // Detect the block structure std::vector block_starts, block_sizes; detail_matfun::detect_blocks(T_mat, n, block_starts, block_sizes); // Initialize the result matrix F = sqrtm(T) Matrix F(n, n, T(0)); // Step 1: compute the diagonal blocks for (std::size_t bi = 0; bi < block_starts.size(); ++bi) { std::size_t i = block_starts[bi]; std::size_t sz = block_sizes[bi]; if (sz == 1) { // 1×1 block: sqrt(T_ii) if (T_mat(i, i) < T(0)) { throw MathError("sqrtm: matrix has negative real eigenvalue"); } F(i, i) = std::sqrt(T_mat(i, i)); } else { // 2×2 block: complex-conjugate eigenvalues α±iβ // √(α+iβ) = √r · (cos(θ/2) + i·sin(θ/2)) // r = |λ| = √(α²+β²), θ = arg(λ) = atan2(β, α) T alpha, beta; detail_matfun::block2_eigenvalues(T_mat, i, alpha, beta); T r = std::sqrt(alpha * alpha + beta * beta); T theta = std::atan2(beta, alpha); T sqrt_r = std::sqrt(r); T u = sqrt_r * std::cos(theta / T(2)); T v = sqrt_r * std::sin(theta / T(2)); detail_matfun::apply_block2_function(T_mat, i, F, alpha, beta, u, v); } } // Step 2: compute the super-diagonal blocks (Parlett recursion) // Process block columns moving away from the diagonal for (std::size_t d = 1; d < block_starts.size(); ++d) { for (std::size_t bi = 0; bi + d < block_starts.size(); ++bi) { std::size_t bj = bi + d; std::size_t ri = block_starts[bi]; std::size_t p = block_sizes[bi]; std::size_t ci = block_starts[bj]; std::size_t q = block_sizes[bj]; detail_matfun::solve_sylvester_block(F, T_mat, F, ri, p, ci, q, n); } } // Step 3: recover sqrtm(A) = Q·F·Qᵀ Matrix Qt = Q.transpose(); return Q * F * Qt; } //===================================================================== // logm — matrix logarithm //===================================================================== /** * @brief Matrix logarithm log(A) (principal value) * * Schur decomposition + Parlett recursion. * The principal logarithm exists when no eigenvalue of A lies on the negative real axis. * * @tparam T element type (float, double) * @param A input matrix (n×n square matrix) * @return log(A) * @throws DimensionError if A is not square * @throws MathError if a non-positive real eigenvalue exists */ template Matrix logm(const BaseMatrix& A) { const auto n = A.rows(); if (n != A.cols()) { assert(false && "DimensionError: logm: matrix must be square"); throw DimensionError("logm: matrix must be square"); } if (n == 0) { return Matrix(A); } if (n == 1) { if (A(0, 0) <= T(0)) { throw MathError("logm: matrix has non-positive real eigenvalue"); } Matrix result(1, 1); result(0, 0) = std::log(A(0, 0)); return result; } // Schur decomposition: A = Q·T·Qᵀ auto [T_mat, Q] = schur_decomposition(A); // Detect the block structure std::vector block_starts, block_sizes; detail_matfun::detect_blocks(T_mat, n, block_starts, block_sizes); // Initialize the result matrix F = logm(T) Matrix F(n, n, T(0)); // Step 1: compute the diagonal blocks for (std::size_t bi = 0; bi < block_starts.size(); ++bi) { std::size_t i = block_starts[bi]; std::size_t sz = block_sizes[bi]; if (sz == 1) { // 1×1 block: log(T_ii) if (T_mat(i, i) <= T(0)) { throw MathError("logm: matrix has non-positive real eigenvalue"); } F(i, i) = std::log(T_mat(i, i)); } else { // 2×2 block: complex-conjugate eigenvalues α±iβ T alpha, beta; detail_matfun::block2_eigenvalues(T_mat, i, alpha, beta); T r2 = alpha * alpha + beta * beta; T u = std::log(r2) / T(2); // ½·log(|λ|²) T v = std::atan2(beta, alpha); detail_matfun::apply_block2_function(T_mat, i, F, alpha, beta, u, v); } } // Step 2: compute the super-diagonal blocks (Parlett recursion) for (std::size_t d = 1; d < block_starts.size(); ++d) { for (std::size_t bi = 0; bi + d < block_starts.size(); ++bi) { std::size_t bj = bi + d; std::size_t ri = block_starts[bi]; std::size_t p = block_sizes[bi]; std::size_t ci = block_starts[bj]; std::size_t q = block_sizes[bj]; detail_matfun::solve_sylvester_block(F, T_mat, F, ri, p, ci, q, n); } } // Step 3: recover logm(A) = Q·F·Qᵀ Matrix Qt = Q.transpose(); return Q * F * Qt; } // ================================================================ // Frechet derivative (block-triangular method) // ================================================================ // // The Frechet derivative L_f(A)[E] of a matrix function f satisfies: // f(A + εE) = f(A) + ε·L_f(A)[E] + O(ε²) // // Block-triangular method: // M = [A, E; 0, A] (2n × 2n) // f(M) = [f(A), L_f(A)[E]; 0, f(A)] // → L_f(A)[E] = top-right n×n block of f(M) // // Reference: Higham (2008) "Functions of Matrices", Chapter 3 /// Frechet derivative of expm: L_exp(A)[E] /// @param A n×n matrix /// @param E n×n perturbation direction /// @return L_exp(A)[E] (n×n matrix) template [[nodiscard]] Matrix frechet_expm(const BaseMatrix& A, const BaseMatrix& E) { const auto n = A.rows(); if (n != A.cols() || n != E.rows() || n != E.cols()) throw DimensionError("frechet_expm: A and E must be square and same size"); // M = [A, E; 0, A] Matrix M(2*n, 2*n, T(0)); for (std::size_t i = 0; i < n; ++i) for (std::size_t j = 0; j < n; ++j) { M(i, j) = A(i, j); M(i, j + n) = E(i, j); M(i + n, j + n) = A(i, j); } auto fM = expm(M); // Extract top-right block Matrix L(n, n); for (std::size_t i = 0; i < n; ++i) for (std::size_t j = 0; j < n; ++j) L(i, j) = fM(i, j + n); return L; } /// Frechet derivative of logm: L_log(A)[E] template [[nodiscard]] Matrix frechet_logm(const BaseMatrix& A, const BaseMatrix& E) { const auto n = A.rows(); if (n != A.cols() || n != E.rows() || n != E.cols()) throw DimensionError("frechet_logm: A and E must be square and same size"); Matrix M(2*n, 2*n, T(0)); for (std::size_t i = 0; i < n; ++i) for (std::size_t j = 0; j < n; ++j) { M(i, j) = A(i, j); M(i, j + n) = E(i, j); M(i + n, j + n) = A(i, j); } auto fM = logm(M); Matrix L(n, n); for (std::size_t i = 0; i < n; ++i) for (std::size_t j = 0; j < n; ++j) L(i, j) = fM(i, j + n); return L; } /// Frechet derivative of sqrtm: L_sqrt(A)[E] template [[nodiscard]] Matrix frechet_sqrtm(const BaseMatrix& A, const BaseMatrix& E) { const auto n = A.rows(); if (n != A.cols() || n != E.rows() || n != E.cols()) throw DimensionError("frechet_sqrtm: A and E must be square and same size"); Matrix M(2*n, 2*n, T(0)); for (std::size_t i = 0; i < n; ++i) for (std::size_t j = 0; j < n; ++j) { M(i, j) = A(i, j); M(i, j + n) = E(i, j); M(i + n, j + n) = A(i, j); } auto fM = sqrtm(M); Matrix L(n, n); for (std::size_t i = 0; i < n; ++i) for (std::size_t j = 0; j < n; ++j) L(i, j) = fM(i, j + n); return L; } /// Generic Frechet derivative: compute L_f(A)[E] for any matrix function f /// @param f matrix function f: Matrix → Matrix /// @param A n×n matrix /// @param E n×n perturbation direction /// @return L_f(A)[E] (n×n matrix) template [[nodiscard]] Matrix frechet(MatFunc f, const BaseMatrix& A, const BaseMatrix& E) { const auto n = A.rows(); if (n != A.cols() || n != E.rows() || n != E.cols()) throw DimensionError("frechet: A and E must be square and same size"); Matrix M(2*n, 2*n, T(0)); for (std::size_t i = 0; i < n; ++i) for (std::size_t j = 0; j < n; ++j) { M(i, j) = A(i, j); M(i, j + n) = E(i, j); M(i + n, j + n) = A(i, j); } auto fM = f(M); Matrix L(n, n); for (std::size_t i = 0; i < n; ++i) for (std::size_t j = 0; j < n; ++j) L(i, j) = fM(i, j + n); return L; } //===================================================================== // Differentials of decompositions (Decomposition Differentials) //===================================================================== // ================================================================ // diff_svd_sigma — singular-value differential of the SVD // ================================================================ // // For A = U · diag(σ) · Vt, compute dσ from dA. // dσ_i = u_i^T · dA · v_i // // Note: dU, dV are omitted as they are unstable with repeated singular values. // The singular-values-only differential is stable and fast. /// Singular-value differential of the SVD: dσ_i = u_i^T · dA · v_i /// @param U left singular vector matrix (m×m or m×k) /// @param sigma singular value vector (k entries) /// @param Vt transpose of the right singular vector matrix (n×n or k×n) /// @param dA perturbation matrix (m×n) /// @return dσ vector (k entries) template [[nodiscard]] Vector diff_svd_sigma(const BaseMatrix& U, const BaseVector& sigma, const BaseMatrix& Vt, const BaseMatrix& dA) { const auto m = dA.rows(); const auto n = dA.cols(); const auto k = sigma.size(); if (U.rows() != m || Vt.cols() != n) throw DimensionError("diff_svd_sigma: dimension mismatch between U/Vt and dA"); // dA_rotated = U^T · dA · V = U^T · dA · Vt^T // dσ_i = (U^T · dA · V)_{ii} // Since V = Vt^T, dA · V = dA · Vt^T auto V = Vt.transposed(); // 0-copy view Vector dsigma(k); for (std::size_t i = 0; i < k; ++i) { // Compute u_i^T · dA · v_i as a dot product T sum = T(0); for (std::size_t r = 0; r < m; ++r) { T dA_vi_r = T(0); for (std::size_t c = 0; c < n; ++c) { dA_vi_r += dA(r, c) * V(c, i); } sum += U(r, i) * dA_vi_r; } dsigma[i] = sum; } return dsigma; } // ================================================================ // diff_symmetric_eigen — differential of the symmetric eigendecomposition // ================================================================ // // For A = V · diag(λ) · V^T, compute dλ, dV from dA. // dλ_i = v_i^T · dA · v_i // (dV)_{:,j} += v_i · (v_i^T · dA · v_j) / (λ_j - λ_i) for i ≠ j // // Note: dV is unstable when λ_i ≈ λ_j (repeated eigenvalues). // If λ_j - λ_i is at or below the machine epsilon, set that component to 0. /// Differential of the symmetric eigendecomposition /// @param eigenvalues eigenvalue vector (n entries) /// @param eigenvectors eigenvector matrix (n×n, columns are eigenvectors) /// @param dA perturbation matrix (n×n symmetric matrix) /// @return the pair {dλ, dV} template [[nodiscard]] std::pair, Matrix> diff_symmetric_eigen( const BaseVector& eigenvalues_basevec, const Matrix& eigenvectors, const Matrix& dA) { Vector eigenvalues(eigenvalues_basevec); // 1 copy via BaseVector ctor const auto n = eigenvalues.size(); if (eigenvectors.rows() != n || eigenvectors.cols() != n) throw DimensionError("diff_symmetric_eigen: eigenvectors must be n x n"); if (dA.rows() != n || dA.cols() != n) throw DimensionError("diff_symmetric_eigen: dA must be n x n"); // Compute V^T · dA · V Matrix Vt = eigenvectors.transpose(); Matrix VtdAV = Vt * dA * eigenvectors; // dλ_i = (V^T · dA · V)_{ii} Vector dlambda(n); for (std::size_t i = 0; i < n; ++i) { dlambda[i] = VtdAV(i, i); } // dV = V · Ω where Ω_{ij} = VtdAV_{ij} / (λ_j - λ_i) (i ≠ j), Ω_{ii} = 0 const T eps = std::numeric_limits::epsilon() * T(100); Matrix Omega(n, n, T(0)); for (std::size_t i = 0; i < n; ++i) { for (std::size_t j = 0; j < n; ++j) { if (i != j) { T diff = eigenvalues[j] - eigenvalues[i]; if (std::abs(diff) > eps) { Omega(i, j) = VtdAV(i, j) / diff; } // Leave as 0 for repeated eigenvalues } } } Matrix dV = eigenvectors * Omega; return { dlambda, dV }; } // ================================================================ // diff_cholesky — differential of the Cholesky decomposition // ================================================================ // // For A = L · L^T, compute dA → dL. // S = L^{-1} · dA · L^{-T} // dL = L · tril(S) where the diagonal elements are S_{ii}/2 // // Reference: Giles (2008) "An extended collection of matrix derivative results // for forward and reverse mode automatic differentiation" /// Differential of the Cholesky decomposition /// @param L Cholesky factor (lower-triangular matrix, n×n) /// @param dA perturbation matrix (n×n symmetric matrix) /// @return dL (lower-triangular matrix, n×n) template [[nodiscard]] Matrix diff_cholesky(const BaseMatrix& L, const BaseMatrix& dA) { const auto n = L.rows(); if (L.cols() != n) throw DimensionError("diff_cholesky: L must be square"); if (dA.rows() != n || dA.cols() != n) throw DimensionError("diff_cholesky: dA must be n x n"); // Since cholesky_decomposition may leave original values in the upper-triangular part, // copy and use only the lower-triangular part Matrix Lc(n, n, T(0)); for (std::size_t i = 0; i < n; ++i) for (std::size_t j = 0; j <= i; ++j) Lc(i, j) = L(i, j); // Direct method: solve the Lyapunov equation L · dL^T + dL · L^T = dA // dL is lower-triangular. Compute column by column in the order (j, j), (j+1, j), ..., (n-1, j). // // Diagonal (i = j): // dA_{jj} = 2 L_{jj} dL_{jj} + 2 Σ_{k j): // dA_{ij} = dL_{ij} L_{jj} + Σ_{k dL(n, n, T(0)); for (std::size_t j = 0; j < n; ++j) { // Diagonal element dL_{jj} T sum_diag = T(0); for (std::size_t k = 0; k < j; ++k) { sum_diag += Lc(j, k) * dL(j, k); } dL(j, j) = (dA(j, j) - T(2) * sum_diag) / (T(2) * Lc(j, j)); // Off-diagonal element dL_{ij} (i > j) for (std::size_t i = j + 1; i < n; ++i) { T sum = T(0); // Σ_{k