// Copyright (C) 2026 Kiyotsugu Arai // SPDX-License-Identifier: LGPL-3.0-or-later /** * @file sparse_matrix_algorithms.hpp * @brief Iterative solvers and preconditioners for sparse matrices * * Iterative solvers: CG, BiCG, BiCGSTAB, GMRES(m) * Preconditioners: Jacobi, SSOR, ILU(0), IC(0) * All solvers return SolverResult; preconditioners are injected as callables. */ #ifndef SANGI_SPARSE_MATRIX_ALGORITHMS_HPP #define SANGI_SPARSE_MATRIX_ALGORITHMS_HPP #include #include #include #include #include #include #include #include #include #include #include #include "../concepts/algebraic_concepts.hpp" #include "sparse_matrix.hpp" #include "vector.hpp" #include "convergence_criteria.hpp" namespace sangi { namespace sparse_algorithms { // ============================================================================ // SolverResult — return value of solvers // ============================================================================ template struct SolverResult { Vector x; ///< Solution vector T residual_norm; ///< Final residual norm std::size_t iterations; ///< Iteration count bool converged; ///< Whether convergence was achieved }; // ============================================================================ // Preconditioner type definition — callable (Vector -> Vector) // ============================================================================ template using Preconditioner = std::function(const Vector&)>; /// Identity preconditioner (no preconditioning) template Preconditioner identity_preconditioner() { return [](const Vector& r) -> Vector { return r; }; } // ============================================================================ // Helper: extract the diagonal of a sparse matrix // ============================================================================ template requires concepts::OrderedField && std::integral std::vector sparse_diagonal(const SparseMatrix& A) { const auto n = A.rows(); std::vector diag(n, T{0}); for (IndexType i = 0; i < n; ++i) { diag[i] = A.coeff(i, i); } return diag; } // ============================================================================ // Preconditioner factories // ============================================================================ /** * @brief Jacobi (diagonal) preconditioner * * M = diag(A), M^{-1} r = r[i] / A(i,i) * The lightest preconditioner at O(n); effective for diagonally dominant matrices. */ template requires concepts::OrderedField && std::integral Preconditioner jacobi_preconditioner(const SparseMatrix& A) { auto diag = sparse_diagonal(A); const auto n = A.rows(); // Zero-diagonal check for (IndexType i = 0; i < n; ++i) { if (std::abs(diag[i]) < std::numeric_limits::epsilon()) { throw std::invalid_argument( "Jacobi preconditioner: zero diagonal element at row " + std::to_string(i)); } } return [diag = std::move(diag)](const Vector& r) -> Vector { const auto m = r.size(); Vector z(m); for (std::size_t i = 0; i < m; ++i) { z[i] = r[i] / diag[i]; } return z; }; } /** * @brief SSOR (Symmetric Successive Over-Relaxation) preconditioner * * M = (D/omega + L) D^{-1} (D/omega + U) * Compute M^{-1} r via forward and backward substitution. * Reduces to symmetric Gauss-Seidel when omega = 1.0. */ template requires concepts::OrderedField && std::integral Preconditioner ssor_preconditioner( const SparseMatrix& A, T omega = T{1}) { // Convert to CSR and store auto A_csr = std::make_shared>(A); A_csr->convert_to(SparseStorageFormat::CSR); const auto n = A.rows(); auto diag = sparse_diagonal(A); return [A_csr, diag, omega, n](const Vector& r) -> Vector { Vector y(n, T{0}); Vector z(n, T{0}); // Forward substitution: (D/omega + L) y = r for (IndexType i = 0; i < n; ++i) { T sum = r[i]; for (IndexType j = 0; j < i; ++j) { T aij = A_csr->coeff(i, j); if (aij != T{0}) { sum -= aij * y[j]; } } y[i] = omega * sum / diag[i]; } // D/omega scaling: y_i *= diag[i] / omega for (IndexType i = 0; i < n; ++i) { y[i] *= diag[i] / omega; } // Backward substitution: (D/omega + U) z = y for (IndexType ii = 0; ii < n; ++ii) { IndexType i = n - 1 - ii; T sum = y[i]; for (IndexType j = i + 1; j < n; ++j) { T aij = A_csr->coeff(i, j); if (aij != T{0}) { sum -= aij * z[j]; } } z[i] = omega * sum / diag[i]; } // Correct SSOR scaling: M_SSOR = omega(2-omega) (D/omega+L) D^{-1} (D/omega+U) // Multiply the final result by (2-omega) for (IndexType i = 0; i < n; ++i) { z[i] *= (T{2} - omega); } return z; }; } /** * @brief ILU(0) (Incomplete LU with zero fill) preconditioner * * Performs LU decomposition only within the non-zero pattern of A and stores * L and U in CSR form. M^{-1} r is computed via L y = r (forward substitution) * followed by U z = y (backward substitution). */ template requires concepts::OrderedField && std::integral Preconditioner ilu_preconditioner(const SparseMatrix& A) { const auto n = A.rows(); if (A.cols() != n) { throw std::invalid_argument("ILU: matrix must be square"); } // Build the work array in CSR form // Run IKJ-variant ILU(0) using a dense vector per row // lu_vals[i][j] = the LU value at (i, j) (only within A's pattern) // First, collect each row's non-zero column indices and values struct RowEntry { IndexType col; T val; }; std::vector> rows(n); for (IndexType i = 0; i < n; ++i) { for (IndexType j = 0; j < n; ++j) { T v = A.coeff(i, j); if (v != T{0}) { rows[i].push_back({j, v}); } } // Sort by column order std::sort(rows[i].begin(), rows[i].end(), [](const RowEntry& a, const RowEntry& b) { return a.col < b.col; }); } // IKJ-variant ILU(0): update each entry of row i. // w = copy of A(i,:) -> eliminate using pivot rows k < i -> copy into L, U. std::vector> L_rows(n), U_rows(n); for (IndexType i = 0; i < n; ++i) { // Dense work array for row i (only at non-zero positions) std::vector w(n, T{0}); std::vector nz(n, false); // Non-zero pattern for (auto& e : rows[i]) { w[e.col] = e.val; nz[e.col] = true; } // k loop: for k < i, eliminate columns where nz[k] is true for (IndexType k = 0; k < i; ++k) { if (!nz[k]) continue; // Divide by U(k,k) T ukk = T{0}; for (auto& e : U_rows[k]) { if (e.col == k) { ukk = e.val; break; } } if (std::abs(ukk) < std::numeric_limits::epsilon()) { continue; // Skip zero pivot } w[k] /= ukk; // Update entries where j > k, U(k,j) != 0, and nz[j] for (auto& e : U_rows[k]) { if (e.col > k && nz[e.col]) { w[e.col] -= w[k] * e.val; } } } // Store into L, U for (auto& e : rows[i]) { IndexType j = e.col; if (j < i) { L_rows[i].push_back({j, w[j]}); } else { U_rows[i].push_back({j, w[j]}); } } // L's diagonal is 1 L_rows[i].push_back({i, T{1}}); std::sort(L_rows[i].begin(), L_rows[i].end(), [](const RowEntry& a, const RowEntry& b) { return a.col < b.col; }); } // Hold L and U row data via shared_ptr auto l_data = std::make_shared>>(std::move(L_rows)); auto u_data = std::make_shared>>(std::move(U_rows)); return [l_data, u_data, n](const Vector& r) -> Vector { // Forward substitution: L y = r Vector y(n, T{0}); for (IndexType i = 0; i < n; ++i) { T sum = r[i]; for (auto& e : (*l_data)[i]) { if (e.col < i) { sum -= e.val * y[e.col]; } } // L(i,i) = 1, so no division needed y[i] = sum; } // Backward substitution: U z = y Vector z(n, T{0}); for (IndexType ii = 0; ii < n; ++ii) { IndexType i = n - 1 - ii; T sum = y[i]; T uii = T{0}; for (auto& e : (*u_data)[i]) { if (e.col == i) { uii = e.val; } else if (e.col > i) { sum -= e.val * z[e.col]; } } if (std::abs(uii) < std::numeric_limits::epsilon()) { z[i] = T{0}; } else { z[i] = sum / uii; } } return z; }; } // ============================================================================ // Backward compatibility: incomplete_lu_decomposition (returns an (L, U) pair) // ============================================================================ template requires concepts::OrderedField && std::integral std::pair, SparseMatrix> incomplete_lu_decomposition( const SparseMatrix& A, [[maybe_unused]] int fill_level = 0) { const auto n = A.rows(); if (A.cols() != n) { throw std::invalid_argument("ILU: matrix must be square"); } struct RowEntry { IndexType col; T val; }; std::vector> rows(n); for (IndexType i = 0; i < n; ++i) { for (IndexType j = 0; j < n; ++j) { T v = A.coeff(i, j); if (v != T{0}) { rows[i].push_back({j, v}); } } std::sort(rows[i].begin(), rows[i].end(), [](const RowEntry& a, const RowEntry& b) { return a.col < b.col; }); } std::vector> L_rows(n), U_rows(n); for (IndexType i = 0; i < n; ++i) { std::vector w(n, T{0}); std::vector nz(n, false); for (auto& e : rows[i]) { w[e.col] = e.val; nz[e.col] = true; } for (IndexType k = 0; k < i; ++k) { if (!nz[k]) continue; T ukk = T{0}; for (auto& e : U_rows[k]) { if (e.col == k) { ukk = e.val; break; } } if (std::abs(ukk) < std::numeric_limits::epsilon()) continue; w[k] /= ukk; for (auto& e : U_rows[k]) { if (e.col > k && nz[e.col]) { w[e.col] -= w[k] * e.val; } } } for (auto& e : rows[i]) { if (e.col < i) L_rows[i].push_back({e.col, w[e.col]}); else U_rows[i].push_back({e.col, w[e.col]}); } L_rows[i].push_back({i, T{1}}); std::sort(L_rows[i].begin(), L_rows[i].end(), [](const RowEntry& a, const RowEntry& b) { return a.col < b.col; }); } SparseMatrix L(n, n, SparseStorageFormat::COO); SparseMatrix U(n, n, SparseStorageFormat::COO); for (IndexType i = 0; i < n; ++i) { for (auto& e : L_rows[i]) L.set_coeff(i, e.col, e.val); for (auto& e : U_rows[i]) U.set_coeff(i, e.col, e.val); } return {L, U}; } // ============================================================================ // IC(0) (Incomplete Cholesky with zero fill) preconditioner // ============================================================================ /** * @brief IC(0) (Incomplete Cholesky with zero fill) preconditioner * * Performs Cholesky decomposition only within the lower-triangular non-zero * pattern of a symmetric positive-definite matrix A. * A ~= L * L^T (no fill-in). * M^{-1} r is computed via L y = r (forward substitution) followed by * L^T z = y (backward substitution). */ template requires concepts::OrderedField && std::integral Preconditioner incomplete_cholesky_preconditioner(const SparseMatrix& A) { const auto n = A.rows(); if (A.cols() != n) { throw std::invalid_argument("IC: matrix must be square"); } // Gather the lower-triangular non-zero pattern row by row (including the diagonal) struct Entry { IndexType col; T val; }; std::vector> L_rows(n); for (IndexType i = 0; i < n; ++i) { for (IndexType j = 0; j <= i; ++j) { T v = A.coeff(i, j); if (v != T{0}) { L_rows[i].push_back({j, v}); } } // Already in column order (j loop is ascending), but sort to be safe std::sort(L_rows[i].begin(), L_rows[i].end(), [](const Entry& a, const Entry& b) { return a.col < b.col; }); // Ensure the diagonal element exists in the pattern if (L_rows[i].empty() || L_rows[i].back().col != i) { throw std::runtime_error("IC: missing diagonal element (matrix may not be SPD)"); } } // Build column->row lists for fast lookup of non-zero rows per column. // col_rows[j] = list of rows i where L_rows[i] has an entry with col == j. std::vector> col_rows(n); for (IndexType i = 0; i < n; ++i) { for (auto& e : L_rows[i]) { if (e.col < i) { // Strict lower triangle (off-diagonal) col_rows[e.col].push_back(i); } } } // IC(0) decomposition: processed row by row. // L(i,j) = (A(i,j) - Sum_{kindex maps for speed. std::vector> row_col_idx(n); // row_col_idx[i][p] = L_rows[i][p].col for (IndexType i = 0; i < n; ++i) { row_col_idx[i].reserve(L_rows[i].size()); for (auto& e : L_rows[i]) { row_col_idx[i].push_back(e.col); } } // Process row i for (IndexType i = 0; i < n; ++i) { for (std::size_t p = 0; p < L_rows[i].size(); ++p) { IndexType j = L_rows[i][p].col; T sum = T{0}; if (j < i) { // Off-diagonal: L(i,j) = (A(i,j) - Sum_{k::epsilon()) { throw std::runtime_error("IC: zero diagonal encountered (matrix may not be SPD)"); } L_rows[i][p].val = (L_rows[i][p].val - sum) / ljj; } else { // Diagonal: L(i,i) = sqrt(A(i,i) - Sum_{k>>(std::move(L_rows)); auto dim = n; return [l_data, dim](const Vector& r) -> Vector { auto n = dim; // Forward substitution: L y = r Vector y(n, T{0}); for (IndexType i = 0; i < n; ++i) { T sum = r[i]; for (auto& e : (*l_data)[i]) { if (e.col < i) { sum -= e.val * y[e.col]; } } // L(i,i) is the last element of the row y[i] = sum / (*l_data)[i].back().val; } // Backward substitution: L^T z = y // Row-based transform: start with z = y; for row i (descending) do // z[i] /= L(i,i), then for L(i,k) (k < i) do z[k] -= L(i,k) * z[i]. Vector z = y; for (IndexType ii = 0; ii < n; ++ii) { IndexType i = n - 1 - ii; z[i] /= (*l_data)[i].back().val; for (auto& e : (*l_data)[i]) { if (e.col < i) { z[e.col] -= e.val * z[i]; } } } return z; }; } // ============================================================================ // AMG (aggregation-based algebraic multigrid) preconditioner // ============================================================================ namespace detail_amg { /// One level of the AMG hierarchy. template struct AMGLevel { SparseMatrix A; ///< Operator on this level (CSR) std::vector dinv; ///< 1/diag // CSR row arrays for the Gauss-Seidel smoother (forward pre / backward post // sweeps form a symmetric, SPD smoother → SPD V-cycle, required by CG). std::vector rptr, cidx; std::vector rval; SparseMatrix P; ///< Prolongator n×nc (smoothed aggregation) SparseMatrix Pt; ///< Restriction Pᵀ (nc×n) bool coarsest = false; bool dense_solve = false; ///< coarsest solved by a dense LU std::size_t dn = 0; std::vector lu; ///< dn×dn row-major LU factors std::vector piv; }; /// Greedy strength-based aggregation. Fills agg[i] with a coarse-node id and /// returns the coarse node count nc. A node is strongly connected to j when /// |A(i,j)| ≥ theta · max_{k≠i} |A(i,k)|. template std::size_t aggregate(const SparseMatrix& A, std::vector& agg, T theta = T(0.08)) { const std::size_t n = static_cast(A.rows()); const auto& vals = A.csc_values(); const auto& rows = A.csc_row_indices(); const auto& cptr = A.csc_col_ptr(); std::vector rowmax(n, T(0)); for (std::size_t j = 0; j < n; ++j) for (std::size_t k = cptr[j]; k < cptr[j + 1]; ++k) { std::size_t i = static_cast(rows[k]); if (i != j) rowmax[i] = std::max(rowmax[i], std::abs(vals[k])); } std::vector> nbr(n); for (std::size_t j = 0; j < n; ++j) for (std::size_t k = cptr[j]; k < cptr[j + 1]; ++k) { std::size_t i = static_cast(rows[k]); if (i != j && rowmax[i] > T(0) && std::abs(vals[k]) >= theta * rowmax[i]) nbr[i].push_back(j); } const IndexType UNSET = static_cast(-1); agg.assign(n, UNSET); std::size_t nc = 0; for (std::size_t i = 0; i < n; ++i) { if (agg[i] != UNSET) continue; agg[i] = static_cast(nc); for (std::size_t j : nbr[i]) if (agg[j] == UNSET) agg[j] = static_cast(nc); ++nc; } return nc; } /// Smoothed-aggregation prolongator P (n×nc): P = (I − ω D⁻¹ A) P_tent, where the /// tentative prolongator P_tent(i, agg[i]) = 1 is the piecewise-constant aggregate /// indicator. The Jacobi smoothing of P makes the resulting V-cycle a convergent /// SPD operator (suitable for CG), unlike unsmoothed aggregation. template SparseMatrix build_prolongator( const SparseMatrix& A, const std::vector& agg, std::size_t nc, const std::vector& dinv, T omega_P) { const std::size_t n = static_cast(A.rows()); SparseMatrix Ptent(static_cast(n), static_cast(nc), SparseStorageFormat::COO); for (std::size_t i = 0; i < n; ++i) Ptent.set_coeff(static_cast(i), agg[i], T(1)); Ptent.convert_to(SparseStorageFormat::CSR); SparseMatrix S = A * Ptent; // n×nc std::map, T> acc; for (std::size_t i = 0; i < n; ++i) acc[{static_cast(i), agg[i]}] += T(1); // P_tent part { const auto& vals = S.csc_values(); const auto& rows = S.csc_row_indices(); const auto& cptr = S.csc_col_ptr(); const std::size_t ncc = static_cast(S.cols()); for (std::size_t J = 0; J < ncc; ++J) for (std::size_t k = cptr[J]; k < cptr[J + 1]; ++k) { std::size_t i = static_cast(rows[k]); acc[{static_cast(i), static_cast(J)}] += -omega_P * dinv[i] * vals[k]; } } SparseMatrix P(static_cast(n), static_cast(nc), SparseStorageFormat::COO); for (const auto& [ij, v] : acc) if (v != T(0)) P.set_coeff(ij.first, ij.second, v); P.convert_to(SparseStorageFormat::CSR); return P; } /// Small dense LU with partial pivoting (coarsest-level direct solve). template void dense_lu_factor(std::vector& lu, std::vector& piv, std::size_t n) { piv.resize(n); for (std::size_t i = 0; i < n; ++i) piv[i] = static_cast(i); const T tiny = std::numeric_limits::epsilon(); for (std::size_t k = 0; k < n; ++k) { std::size_t p = k; for (std::size_t i = k + 1; i < n; ++i) if (std::abs(lu[i * n + k]) > std::abs(lu[p * n + k])) p = i; if (p != k) { for (std::size_t j = 0; j < n; ++j) std::swap(lu[k * n + j], lu[p * n + j]); std::swap(piv[k], piv[p]); } T d = lu[k * n + k]; if (std::abs(d) < tiny) d = (d >= T(0) ? T(1) : T(-1)) * tiny; for (std::size_t i = k + 1; i < n; ++i) { T f = lu[i * n + k] / d; lu[i * n + k] = f; for (std::size_t j = k + 1; j < n; ++j) lu[i * n + j] -= f * lu[k * n + j]; } } } template Vector dense_lu_solve(const std::vector& lu, const std::vector& piv, std::size_t n, const Vector& b) { Vector y(n, T(0)); for (std::size_t i = 0; i < n; ++i) y[i] = b[static_cast(piv[i])]; for (std::size_t i = 0; i < n; ++i) for (std::size_t j = 0; j < i; ++j) y[i] -= lu[i * n + j] * y[j]; const T tiny = std::numeric_limits::epsilon(); for (std::size_t ii = n; ii-- > 0;) { for (std::size_t j = ii + 1; j < n; ++j) y[ii] -= lu[ii * n + j] * y[j]; T d = lu[ii * n + ii]; if (std::abs(d) < tiny) d = (d >= T(0) ? T(1) : T(-1)) * tiny; y[ii] /= d; } return y; } template class AMGHierarchy { public: AMGHierarchy(const SparseMatrix& A, std::size_t coarse_max = 50, std::size_t max_levels = 25, int nu = 2) : nu_(nu) { levels_.reserve(max_levels); SparseMatrix cur(A); cur.convert_to(SparseStorageFormat::CSR); while (true) { AMGLevel lv; const std::size_t n = static_cast(cur.rows()); // diagonal inverse + Gershgorin estimate of ρ(D⁻¹A) for the SA damping lv.dinv.assign(n, T(0)); std::vector rowsum(n, T(0)); { const auto& vals = cur.csc_values(); const auto& rows = cur.csc_row_indices(); const auto& cptr = cur.csc_col_ptr(); for (std::size_t j = 0; j < n; ++j) for (std::size_t k = cptr[j]; k < cptr[j + 1]; ++k) { std::size_t i = static_cast(rows[k]); rowsum[i] += std::abs(vals[k]); if (i == j) { lv.dinv[i] = (std::abs(vals[k]) > std::numeric_limits::epsilon()) ? T(1) / vals[k] : T(0); } } } T rho = T(0); for (std::size_t i = 0; i < n; ++i) rho = std::max(rho, rowsum[i] * std::abs(lv.dinv[i])); if (rho <= T(0)) rho = T(1); bool stop = (n <= coarse_max) || (levels_.size() + 1 >= max_levels); std::size_t nc = 0; std::vector agg; if (!stop) { nc = aggregate(cur, agg); if (nc == 0 || nc >= n) stop = true; // coarsening stalled } if (stop) { lv.coarsest = true; if (n <= 400) { lv.dense_solve = true; lv.dn = n; lv.lu.assign(n * n, T(0)); const auto& vals = cur.csc_values(); const auto& rows = cur.csc_row_indices(); const auto& cptr = cur.csc_col_ptr(); for (std::size_t j = 0; j < n; ++j) for (std::size_t k = cptr[j]; k < cptr[j + 1]; ++k) lv.lu[static_cast(rows[k]) * n + j] = vals[k]; dense_lu_factor(lv.lu, lv.piv, n); } lv.A = std::move(cur); levels_.push_back(std::move(lv)); break; } // CSR row arrays for Gauss-Seidel smoothing on this level. { const auto& vals = cur.csc_values(); const auto& rows = cur.csc_row_indices(); const auto& cptr = cur.csc_col_ptr(); const std::size_t nz = vals.size(); lv.rptr.assign(n + 1, 0); for (std::size_t k = 0; k < nz; ++k) lv.rptr[static_cast(rows[k]) + 1]++; for (std::size_t i = 0; i < n; ++i) lv.rptr[i + 1] += lv.rptr[i]; lv.cidx.resize(nz); lv.rval.resize(nz); std::vector pos(lv.rptr.begin(), lv.rptr.end() - 1); for (std::size_t j = 0; j < n; ++j) for (std::size_t k = cptr[j]; k < cptr[j + 1]; ++k) { std::size_t i = static_cast(rows[k]); IndexType p = pos[i]++; lv.cidx[p] = static_cast(j); lv.rval[p] = vals[k]; } } const T omega_P = (T(4) / T(3)) / rho; SparseMatrix P = build_prolongator(cur, agg, nc, lv.dinv, omega_P); SparseMatrix Pt = P.transpose(); SparseMatrix AP = cur * P; // Galerkin: Ac = Pᵀ A P SparseMatrix Ac = Pt * AP; Ac.convert_to(SparseStorageFormat::CSR); lv.P = std::move(P); lv.Pt = std::move(Pt); lv.A = std::move(cur); levels_.push_back(std::move(lv)); cur = std::move(Ac); } } Vector apply(const Vector& b) const { return vcycle(0, b); } std::size_t num_levels() const { return levels_.size(); } private: std::vector> levels_; int nu_; // One Gauss-Seidel sweep. forward=true sweeps rows ascending, false descending. // x_i ← (b_i − Σ_{j≠i} A_ij x_j) / A_ii, using the latest x. void gs_sweep(const AMGLevel& lv, Vector& x, const Vector& b, bool forward) const { const std::size_t n = lv.dinv.size(); for (std::size_t t = 0; t < n; ++t) { std::size_t i = forward ? t : (n - 1 - t); T sum = b[i]; for (IndexType k = lv.rptr[i]; k < lv.rptr[i + 1]; ++k) { std::size_t j = static_cast(lv.cidx[k]); if (j != i) sum -= lv.rval[k] * x[j]; } x[i] = sum * lv.dinv[i]; } } Vector vcycle(std::size_t l, const Vector& b) const { const AMGLevel& lv = levels_[l]; const std::size_t n = lv.dinv.size(); if (lv.coarsest) { if (lv.dense_solve) return dense_lu_solve(lv.lu, lv.piv, lv.dn, b); // Fallback (large, un-coarsenable): symmetric Gauss-Seidel iterations. Vector x(n, T(0)); for (int it = 0; it < 50; ++it) { gs_sweep(lv, x, b, true); gs_sweep(lv, x, b, false); } return x; } Vector x(n, T(0)); for (int s = 0; s < nu_; ++s) gs_sweep(lv, x, b, true); // pre-smooth (forward) Vector Ax = lv.A * x; Vector rr(n); for (std::size_t i = 0; i < n; ++i) rr[i] = b[i] - Ax[i]; Vector rc = lv.Pt * rr; // restrict (Pᵀ r) Vector ec = vcycle(l + 1, rc); // coarse-grid correction Vector corr = lv.P * ec; // prolong (P e) x += corr; // correct for (int s = 0; s < nu_; ++s) gs_sweep(lv, x, b, false); // post-smooth (backward) return x; } }; } // namespace detail_amg /** * @brief AMG (aggregation-based algebraic multigrid) preconditioner * * Builds a multilevel hierarchy by greedy strength-based aggregation with a * piecewise-constant prolongator and a Galerkin coarse operator (Ac = Pᵀ A P). * One application performs a single symmetric V-cycle (damped-Jacobi pre/post * smoothing, dense LU on the coarsest level), which yields an SPD operator * suitable as a CG/BiCGStab preconditioner. Most effective for SPD sparse * systems amenable to coarsening (e.g. discretized elliptic PDEs). The * hierarchy is constructed once and shared by the returned callable. */ template requires concepts::OrderedField && std::integral Preconditioner amg_preconditioner(const SparseMatrix& A) { if (A.rows() != A.cols()) throw std::invalid_argument("AMG: matrix must be square"); auto hier = std::make_shared>(A); return [hier](const Vector& r) -> Vector { return hier->apply(r); }; } // ============================================================================ // Backward compatibility: incomplete_cholesky_decomposition (returns L) // ============================================================================ /** * @brief Return the L matrix of the IC(0) decomposition */ template requires concepts::OrderedField && std::integral SparseMatrix incomplete_cholesky_decomposition( const SparseMatrix& A, [[maybe_unused]] int fill_level = 0) { const auto n = A.rows(); if (A.cols() != n) { throw std::invalid_argument("IC: matrix must be square"); } // Gather the lower-triangular non-zero pattern struct Entry { IndexType col; T val; }; std::vector> L_rows(n); for (IndexType i = 0; i < n; ++i) { for (IndexType j = 0; j <= i; ++j) { T v = A.coeff(i, j); if (v != T{0}) { L_rows[i].push_back({j, v}); } } std::sort(L_rows[i].begin(), L_rows[i].end(), [](const Entry& a, const Entry& b) { return a.col < b.col; }); if (L_rows[i].empty() || L_rows[i].back().col != i) { throw std::runtime_error("IC: missing diagonal element"); } } // IC(0) decomposition for (IndexType i = 0; i < n; ++i) { for (std::size_t p = 0; p < L_rows[i].size(); ++p) { IndexType j = L_rows[i][p].col; T sum = T{0}; if (j < i) { std::size_t pi = 0, pj = 0; while (pi < L_rows[i].size() && pj < L_rows[j].size()) { IndexType ci = L_rows[i][pi].col; IndexType cj = L_rows[j][pj].col; if (ci < j && cj < j) { if (ci == cj) { sum += L_rows[i][pi].val * L_rows[j][pj].val; ++pi; ++pj; } else if (ci < cj) { ++pi; } else { ++pj; } } else { break; } } T ljj = L_rows[j].back().val; if (std::abs(ljj) < std::numeric_limits::epsilon()) { throw std::runtime_error("IC: zero diagonal encountered"); } L_rows[i][p].val = (L_rows[i][p].val - sum) / ljj; } else { for (std::size_t q = 0; q < p; ++q) { sum += L_rows[i][q].val * L_rows[i][q].val; } T diag = L_rows[i][p].val - sum; if (diag <= T{0}) { throw std::runtime_error("IC: non-positive diagonal"); } L_rows[i][p].val = std::sqrt(diag); } } } SparseMatrix L(n, n, SparseStorageFormat::COO); for (IndexType i = 0; i < n; ++i) { for (auto& e : L_rows[i]) { L.set_coeff(i, e.col, e.val); } } return L; } // ============================================================================ // Forward / backward substitution for triangular matrices (sparse version) // ============================================================================ template requires concepts::OrderedField && std::integral Vector triangular_solve( const SparseMatrix& M, const Vector& b, bool lower = true) { const auto n = M.rows(); if (M.cols() != n) throw std::invalid_argument("Matrix must be square"); if (b.size() != n) throw std::invalid_argument("Incompatible vector dimensions"); Vector x(n, T{0}); if (lower) { for (IndexType i = 0; i < n; ++i) { T sum = b[i]; for (IndexType j = 0; j < i; ++j) { T v = M.coeff(i, j); if (v != T{0}) sum -= v * x[j]; } x[i] = sum / M.coeff(i, i); } } else { for (IndexType ii = 0; ii < n; ++ii) { IndexType i = n - 1 - ii; T sum = b[i]; for (IndexType j = i + 1; j < n; ++j) { T v = M.coeff(i, j); if (v != T{0}) sum -= v * x[j]; } x[i] = sum / M.coeff(i, i); } } return x; } // ============================================================================ // CG (Conjugate Gradient) — for symmetric positive-definite matrices // ============================================================================ /** * @brief Preconditioned Conjugate Gradient method * * Solve Ax = b for a symmetric positive-definite sparse matrix A. * If a preconditioner M is supplied, solve M^{-1} A x = M^{-1} b via CG. */ template requires concepts::OrderedField && std::integral SolverResult conjugate_gradient( const SparseMatrix& A, const Vector& b, const ConvergenceCriteria& criteria, Preconditioner precond = {}, std::optional> x0 = std::nullopt) { const auto n = A.rows(); if (A.cols() != n) throw std::invalid_argument("CG: matrix must be square"); if (b.size() != n) throw std::invalid_argument("CG: incompatible dimensions"); if (!precond) precond = identity_preconditioner(); Vector x = x0.value_or(Vector(n, T{0})); Vector r = b - A * x; const T init_rnorm = norm(r); if (init_rnorm <= criteria.absolute_tolerance()) { return {x, init_rnorm, 0, true}; } Vector z = precond(r); Vector p = z; T rz = dot(r, z); // Common convergence-check helper for all exits auto is_converged = [&](T rnorm) { return rnorm <= criteria.absolute_tolerance() || rnorm <= criteria.relative_tolerance() * init_rnorm; }; for (std::size_t iter = 0; iter < criteria.max_iterations(); ++iter) { Vector Ap = A * p; T pAp = dot(p, Ap); if (std::abs(pAp) < std::numeric_limits::epsilon()) { // The internal residual r has accumulated error, so judge with the true residual T rnorm = norm(b - A * x); return {x, rnorm, iter, is_converged(rnorm)}; } T alpha = rz / pAp; x += p * alpha; r -= Ap * alpha; T rnorm = norm(r); if (is_converged(rnorm)) { return {x, rnorm, iter + 1, true}; } z = precond(r); T rz_new = dot(r, z); T beta = rz_new / rz; rz = rz_new; p = z + p * beta; } // Also re-check convergence at max iterations (the internal residual may misjudge due to accumulated error) T final_rnorm = norm(b - A * x); return {x, final_rnorm, criteria.max_iterations(), is_converged(final_rnorm)}; } // ============================================================================ // BiCG (BiConjugate Gradient) — for non-symmetric matrices // ============================================================================ template requires concepts::OrderedField && std::integral SolverResult biconjugate_gradient( const SparseMatrix& A, const Vector& b, const ConvergenceCriteria& criteria, std::optional> x0 = std::nullopt) { const auto n = A.rows(); if (A.cols() != n) throw std::invalid_argument("BiCG: matrix must be square"); if (b.size() != n) throw std::invalid_argument("BiCG: incompatible dimensions"); const auto At = A.transpose(); Vector x = x0.value_or(Vector(n, T{0})); Vector r = b - A * x; Vector r_tilde = r; const T init_rnorm = norm(r); if (init_rnorm <= criteria.absolute_tolerance()) { return {x, init_rnorm, 0, true}; } Vector p = r; Vector p_tilde = r_tilde; T rho = dot(r_tilde, r); // Breakdown threshold: relative to the initial residual scale const T breakdown_tol = std::numeric_limits::epsilon() * init_rnorm * init_rnorm; // Convergence-check helper (uses the true residual) auto check_convergence = [&](std::size_t iter) -> SolverResult { T rnorm = norm(b - A * x); bool conv = (rnorm <= criteria.absolute_tolerance() || rnorm <= criteria.relative_tolerance() * init_rnorm); return {x, rnorm, iter, conv}; }; for (std::size_t iter = 0; iter < criteria.max_iterations(); ++iter) { if (std::abs(rho) < breakdown_tol) { return check_convergence(iter); } Vector Ap = A * p; Vector Atp = At * p_tilde; T denom = dot(p_tilde, Ap); if (std::abs(denom) < breakdown_tol) { return check_convergence(iter); } T alpha = rho / denom; x += p * alpha; r -= Ap * alpha; r_tilde -= Atp * alpha; T rnorm = norm(r); if (rnorm <= criteria.absolute_tolerance() || rnorm <= criteria.relative_tolerance() * init_rnorm) { return {x, rnorm, iter + 1, true}; } T rho_new = dot(r_tilde, r); T beta = rho_new / rho; rho = rho_new; p = r + p * beta; p_tilde = r_tilde + p_tilde * beta; } // At max iterations: re-check using the true residual T final_rnorm = norm(b - A * x); bool conv = (final_rnorm <= criteria.absolute_tolerance() || final_rnorm <= criteria.relative_tolerance() * init_rnorm); return {x, final_rnorm, criteria.max_iterations(), conv}; } // ============================================================================ // BiCGSTAB (Stabilized BiConjugate Gradient) — for non-symmetric matrices // ============================================================================ template requires concepts::OrderedField && std::integral SolverResult bicgstab( const SparseMatrix& A, const Vector& b, const ConvergenceCriteria& criteria, Preconditioner precond = {}, std::optional> x0 = std::nullopt) { const auto n = A.rows(); if (A.cols() != n) throw std::invalid_argument("BiCGSTAB: matrix must be square"); if (b.size() != n) throw std::invalid_argument("BiCGSTAB: incompatible dimensions"); if (!precond) precond = identity_preconditioner(); Vector x = x0.value_or(Vector(n, T{0})); Vector r = b - A * x; Vector r_hat = r; const T init_rnorm = norm(r); if (init_rnorm <= criteria.absolute_tolerance()) { return {x, init_rnorm, 0, true}; } T rho = T{1}, alpha = T{1}, omega_val = T{1}; Vector p(n, T{0}); Vector v(n, T{0}); // Breakdown threshold: relative to the initial residual scale const T eps = std::numeric_limits::epsilon(); const T breakdown_tol = eps * init_rnorm * init_rnorm; const T stag_tol = eps * init_rnorm; // Convergence-check helper using the true residual (guards against accumulated error in the internal r) auto check_true_convergence = [&](std::size_t iter) -> SolverResult { T rnorm = norm(b - A * x); bool conv = (rnorm <= criteria.absolute_tolerance() || rnorm <= criteria.relative_tolerance() * init_rnorm); return {x, rnorm, iter, conv}; }; for (std::size_t iter = 0; iter < criteria.max_iterations(); ++iter) { T rho_new = dot(r_hat, r); if (std::abs(rho_new) < breakdown_tol) { return check_true_convergence(iter); } if (iter == 0) { p = r; } else { T beta = (rho_new / rho) * (alpha / omega_val); p = r + (p - v * omega_val) * beta; } rho = rho_new; // Preconditioned: p_hat = M^{-1} p Vector p_hat = precond(p); v = A * p_hat; T denom = dot(r_hat, v); if (std::abs(denom) < breakdown_tol) { return check_true_convergence(iter); } alpha = rho / denom; Vector s = r - v * alpha; T s_norm = norm(s); if (s_norm <= criteria.absolute_tolerance() || s_norm <= criteria.relative_tolerance() * init_rnorm) { x += p_hat * alpha; return {x, s_norm, iter + 1, true}; } // Preconditioned: s_hat = M^{-1} s Vector s_hat = precond(s); Vector t = A * s_hat; T tt = dot(t, t); if (std::abs(tt) < breakdown_tol) { x += p_hat * alpha; return check_true_convergence(iter + 1); } omega_val = dot(t, s) / tt; x += p_hat * alpha + s_hat * omega_val; r = s - t * omega_val; T rnorm = norm(r); if (rnorm <= criteria.absolute_tolerance() || rnorm <= criteria.relative_tolerance() * init_rnorm) { return {x, rnorm, iter + 1, true}; } if (std::abs(omega_val) < stag_tol) { return check_true_convergence(iter + 1); } } // At max iterations: re-check with the true residual (guards against accumulated error in the internal r) T final_rnorm = norm(b - A * x); bool conv = (final_rnorm <= criteria.absolute_tolerance() || final_rnorm <= criteria.relative_tolerance() * init_rnorm); return {x, final_rnorm, criteria.max_iterations(), conv}; } // ============================================================================ // GMRES(m) (Generalized Minimum Residual) — for non-symmetric matrices // ============================================================================ /** * @brief Restarted GMRES * * Apply Arnoldi + Givens rotations successively. Outer iteration is via restarts. * @param restart maximum dimension of the Krylov subspace (default 30) */ template requires concepts::OrderedField && std::integral SolverResult gmres( const SparseMatrix& A, const Vector& b, const ConvergenceCriteria& criteria, std::size_t restart = 30, Preconditioner precond = {}, std::optional> x0 = std::nullopt) { const auto n = static_cast(A.rows()); if (static_cast(A.cols()) != n) throw std::invalid_argument("GMRES: matrix must be square"); if (b.size() != n) throw std::invalid_argument("GMRES: incompatible dimensions"); if (!precond) precond = identity_preconditioner(); if (restart == 0 || restart > n) restart = n; Vector x = x0.value_or(Vector(n, T{0})); const T b_norm = norm(b); const T threshold = criteria.absolute_tolerance() + criteria.relative_tolerance() * b_norm; std::size_t total_iter = 0; while (total_iter < criteria.max_iterations()) { // Residual Vector r = precond(b - A * x); T r_norm = norm(r); if (r_norm <= threshold) { return {x, r_norm, total_iter, true}; } // Arnoldi basis const std::size_t m = std::min(restart, criteria.max_iterations() - total_iter); std::vector> Q(m + 1); Q[0] = r / r_norm; // Upper Hessenberg matrix (m+1 x m) std::vector> H(m + 1, std::vector(m, T{0})); // Givens rotation parameters std::vector cs(m, T{0}), sn(m, T{0}); // Transformed right-hand side std::vector g(m + 1, T{0}); g[0] = r_norm; std::size_t j = 0; for (; j < m; ++j) { // Arnoldi step: w = M^{-1} A q_j Vector w = precond(A * Q[j]); // Modified Gram-Schmidt for (std::size_t i = 0; i <= j; ++i) { H[i][j] = dot(Q[i], w); w -= Q[i] * H[i][j]; } H[j + 1][j] = norm(w); // Happy breakdown if (std::abs(H[j + 1][j]) < std::numeric_limits::epsilon() * T{100}) { // Apply prior Givens rotations to column j of H for (std::size_t i = 0; i < j; ++i) { T tmp = cs[i] * H[i][j] + sn[i] * H[i + 1][j]; H[i + 1][j] = -sn[i] * H[i][j] + cs[i] * H[i + 1][j]; H[i][j] = tmp; } // The j-th Givens rotation T rr = std::sqrt(H[j][j] * H[j][j] + H[j + 1][j] * H[j + 1][j]); if (rr > T{0}) { cs[j] = H[j][j] / rr; sn[j] = H[j + 1][j] / rr; H[j][j] = rr; H[j + 1][j] = T{0}; T g_new = -sn[j] * g[j]; g[j] = cs[j] * g[j]; g[j + 1] = g_new; } ++j; break; } Q[j + 1] = w / H[j + 1][j]; // Apply prior Givens rotations to column j of H for (std::size_t i = 0; i < j; ++i) { T tmp = cs[i] * H[i][j] + sn[i] * H[i + 1][j]; H[i + 1][j] = -sn[i] * H[i][j] + cs[i] * H[i + 1][j]; H[i][j] = tmp; } // Compute the j-th Givens rotation T rr = std::sqrt(H[j][j] * H[j][j] + H[j + 1][j] * H[j + 1][j]); cs[j] = H[j][j] / rr; sn[j] = H[j + 1][j] / rr; H[j][j] = rr; H[j + 1][j] = T{0}; // Update the right-hand side T g_new = -sn[j] * g[j]; g[j] = cs[j] * g[j]; g[j + 1] = g_new; r_norm = std::abs(g[j + 1]); ++total_iter; if (r_norm <= threshold) { ++j; break; } } // Backward substitution: R y = g (R is the upper-triangular part of H) const std::size_t dim = j; std::vector y(dim, T{0}); for (std::size_t ii = 0; ii < dim; ++ii) { std::size_t idx = dim - 1 - ii; y[idx] = g[idx]; for (std::size_t k = idx + 1; k < dim; ++k) { y[idx] -= H[idx][k] * y[k]; } if (std::abs(H[idx][idx]) > std::numeric_limits::epsilon()) { y[idx] /= H[idx][idx]; } } // Update the solution: x += Q * y for (std::size_t k = 0; k < dim; ++k) { x += Q[k] * y[k]; } if (r_norm <= threshold) { return {x, r_norm, total_iter, true}; } } T final_rnorm = norm(b - A * x); return {x, final_rnorm, total_iter, false}; } // ============================================================================ // Convenience function: BiCGSTAB with ILU preconditioning (backward compatibility) // ============================================================================ template requires concepts::OrderedField && std::integral SolverResult ilu_bicgstab( const SparseMatrix& A, const Vector& b, const ConvergenceCriteria& criteria, int fill_level = 0, std::optional> x0 = std::nullopt) { (void)fill_level; // Only ILU(0) is supported auto precond = ilu_preconditioner(A); return bicgstab(A, b, criteria, precond, std::move(x0)); } // ============================================================================ // Arnoldi iteration (standalone API) // ============================================================================ template requires concepts::OrderedField && std::integral std::pair>, std::vector>> arnoldi_iteration( const SparseMatrix& A, const Vector& v, std::size_t m) { const auto n = static_cast(A.rows()); if (static_cast(A.cols()) != n) throw std::invalid_argument("Arnoldi: matrix must be square"); if (v.size() != n) throw std::invalid_argument("Arnoldi: incompatible dimensions"); m = std::min(m, n); std::vector> Q(m + 1); T v_norm = norm(v); if (v_norm < std::numeric_limits::epsilon()) { throw std::invalid_argument("Arnoldi: zero initial vector"); } Q[0] = v / v_norm; std::vector> H(m + 1, std::vector(m, T{0})); for (std::size_t j = 0; j < m; ++j) { Vector w = A * Q[j]; for (std::size_t i = 0; i <= j; ++i) { H[i][j] = dot(Q[i], w); w -= Q[i] * H[i][j]; } H[j + 1][j] = norm(w); if (std::abs(H[j + 1][j]) < std::numeric_limits::epsilon()) { Q.resize(j + 2); H.resize(j + 2); for (auto& row : H) row.resize(j + 1); break; } Q[j + 1] = w / H[j + 1][j]; } return {Q, H}; } // ============================================================================ // EigenResult — return value of eigenvalue solvers // ============================================================================ template struct EigenResult { std::vector eigenvalues; ///< Eigenvalues (requested count) std::vector> eigenvectors; ///< Corresponding eigenvectors std::size_t iterations; ///< Iteration count bool converged; ///< Whether all eigenvalues converged }; // ============================================================================ // TridiagonalEigen — eigenvalues of a tridiagonal matrix (QR method) // ============================================================================ /// @brief Compute all eigenvalues of a tridiagonal matrix using the QL method (LAPACK style) /// @param d diagonal elements (n elements) /// @param e sub-diagonal elements (n-1 elements) /// @return vector of eigenvalues (sorted ascending) template requires concepts::OrderedField std::vector tridiagonal_eigenvalues( std::vector d, std::vector e) { int n = static_cast(d.size()); if (n == 0) return {}; if (n == 1) return {d[0]}; // The inner loop of the QL method writes e[i+1] (for i = m-1..l), so it // writes to e[n-1]. The input e has n-1 elements (sub-diagonal), so extend // it to n elements with a sentinel 0 (per the LAPACK dsteqr spec). Failing // to do so causes allocator-dependent heap corruption (Exit 0xc0000374), // which becomes flaky. if (static_cast(e.size()) < n) { e.resize(n, T(0)); } const T eps = std::numeric_limits::epsilon(); for (int l = 0; l < n; ++l) { int iter = 0; while (true) { // Find smallest m >= l such that e[m] ≈ 0 int m = l; for (; m < n - 1; ++m) { T dd = std::abs(d[m]) + std::abs(d[m + 1]); if (std::abs(e[m]) <= eps * dd) break; } if (m == l) break; // eigenvalue d[l] converged if (++iter > 30 * n) break; // QL implicit shift (Wilkinson) T g = (d[l + 1] - d[l]) / (T(2) * e[l]); T r = std::hypot(g, T(1)); g = d[m] - d[l] + e[l] / (g + std::copysign(r, g)); T s = T(1); T c = T(1); T p = T(0); bool deflated = false; for (int i = m - 1; i >= l; --i) { T f = s * e[i]; T b = c * e[i]; r = std::hypot(f, g); e[i + 1] = r; if (r == T(0)) { d[i + 1] -= p; e[m] = T(0); deflated = true; break; } s = f / r; c = g / r; g = d[i + 1] - p; r = (d[i] - g) * s + T(2) * c * b; p = s * r; d[i + 1] = g + p; g = c * r - b; } if (!deflated) { d[l] -= p; e[l] = g; e[m] = T(0); } } } std::sort(d.begin(), d.end()); return d; } // ============================================================================ // Lanczos method — a few eigenvalues of a symmetric sparse matrix // ============================================================================ /// @brief Lanczos method (a few eigenvalues of a symmetric sparse matrix) /// @param A symmetric sparse matrix /// @param k number of eigenvalues to compute /// @param which "LM"/"LA" (largest), "SM"/"SA" (smallest) /// @param max_iter maximum Krylov dimension (0 = auto) /// @param tol unused (kept for compatibility) template requires concepts::OrderedField && std::integral EigenResult lanczos( const SparseMatrix& A, std::size_t k = 6, const std::string& which = "LM", std::size_t max_iter = 0, T tol = T(0)) { (void)tol; const auto n = static_cast(A.rows()); if (static_cast(A.cols()) != n) throw std::invalid_argument("lanczos: matrix must be square"); if (k == 0 || k > n) throw std::invalid_argument("lanczos: invalid k"); // Krylov dimension: min(n, max(2k+1, 20)) std::size_t m = (max_iter > 0) ? std::min(max_iter, n) : std::min(std::max(k * 2 + 1, std::size_t(20)), n); // Initial vector Vector q(n); T inv_sqrt_n = T(1) / std::sqrt(static_cast(n)); for (std::size_t i = 0; i < n; ++i) q[i] = inv_sqrt_n; std::vector alpha_vec(m); std::vector beta_vec(m); std::vector> Q(m); // Lanczos tridiagonalization (full reorthogonalization) Q[0] = q; for (std::size_t j = 0; j < m; ++j) { Vector w = A * Q[j]; alpha_vec[j] = dot(Q[j], w); w -= Q[j] * alpha_vec[j]; if (j > 0) w -= Q[j - 1] * beta_vec[j - 1]; // Full reorthogonalization for (std::size_t i = 0; i <= j; ++i) { T h = dot(Q[i], w); w -= Q[i] * h; } beta_vec[j] = norm(w); if (j + 1 < m) { if (std::abs(beta_vec[j]) < std::numeric_limits::epsilon() * T(100)) { // Invariant subspace found — fill with random-ish orthogonal vector Q[j + 1] = Vector(n); for (std::size_t idx = 0; idx < n; ++idx) Q[j + 1][idx] = T(0); Q[j + 1][(j + 1) % n] = T(1); // Reorthogonalize for (std::size_t i = 0; i <= j; ++i) { T h = dot(Q[i], Q[j + 1]); Q[j + 1] -= Q[i] * h; } T nn2 = norm(Q[j + 1]); if (nn2 > std::numeric_limits::epsilon()) Q[j + 1] = Q[j + 1] / nn2; } else { Q[j + 1] = w / beta_vec[j]; } } } // Compute eigenvalues of the tridiagonal matrix std::vector a_copy(alpha_vec.begin(), alpha_vec.begin() + static_cast(m)); std::vector b_copy(beta_vec.begin(), beta_vec.begin() + static_cast(m) - 1); auto eigs = tridiagonal_eigenvalues(a_copy, b_copy); // Select according to `which` EigenResult result; result.eigenvalues.resize(k); if (which == "LA" || which == "LM") { for (std::size_t i = 0; i < k; ++i) result.eigenvalues[i] = eigs[eigs.size() - 1 - i]; } else { for (std::size_t i = 0; i < k; ++i) result.eigenvalues[i] = eigs[i]; } result.iterations = m; result.converged = true; result.eigenvectors.resize(k); for (std::size_t i = 0; i < k; ++i) result.eigenvectors[i] = Q[0]; // placeholder return result; } // ============================================================================ // Eigenvalues via the Arnoldi method — a few eigenvalues of a non-symmetric sparse matrix // ============================================================================ /// @brief Eigenvalues of a Hessenberg matrix (QR method) /// @param H Hessenberg matrix (m x m, row-major) /// @return eigenvalues (real parts only; complex eigenvalues return their real part) template requires concepts::OrderedField std::vector hessenberg_eigenvalues( std::vector> H) { int n = static_cast(H.size()); if (n == 0) return {}; const T eps = std::numeric_limits::epsilon() * T(100); int nn = n; // active matrix size (deflated from bottom) for (int iter = 0; iter < n * 300; ++iter) { if (nn <= 1) break; // Deflation: find active block bottom while (nn > 1 && std::abs(H[nn - 1][nn - 2]) <= eps * (std::abs(H[nn - 2][nn - 2]) + std::abs(H[nn - 1][nn - 1]))) --nn; if (nn <= 1) break; // Find active block top int lo = nn - 2; while (lo > 0 && std::abs(H[lo][lo - 1]) > eps * (std::abs(H[lo - 1][lo - 1]) + std::abs(H[lo][lo]))) --lo; // Wilkinson shift from 2×2 trailing submatrix of active block T shift = H[nn - 1][nn - 1]; // QR step with Givens rotations on active block [lo..nn-1] for (int i = lo; i < nn - 1; ++i) { T a = H[i][i] - shift; T b = H[i + 1][i]; T r = std::hypot(a, b); T c = (r == T(0)) ? T(1) : a / r; T s = (r == T(0)) ? T(0) : b / r; // Apply Givens from left: G^T * H for (int j = 0; j < n; ++j) { T t1 = c * H[i][j] + s * H[i + 1][j]; T t2 = -s * H[i][j] + c * H[i + 1][j]; H[i][j] = t1; H[i + 1][j] = t2; } // Apply Givens from right: H * G int jmax = std::min(i + 2, nn - 1); for (int j = 0; j <= jmax; ++j) { T t1 = c * H[j][i] + s * H[j][i + 1]; T t2 = -s * H[j][i] + c * H[j][i + 1]; H[j][i] = t1; H[j][i + 1] = t2; } } } std::vector eigs(n); for (int i = 0; i < n; ++i) eigs[i] = H[i][i]; return eigs; } /// @brief Arnoldi method (a few eigenvalues of a non-symmetric sparse matrix) /// @param A sparse matrix (square) /// @param k number of eigenvalues to compute /// @param which "LM" (largest magnitude), "SM" (smallest magnitude), "LR" (largest real part) /// @param max_krylov Krylov dimension (0 = auto) /// @param tol unused (kept for compatibility) template requires concepts::OrderedField && std::integral EigenResult arnoldi_eigs( const SparseMatrix& A, std::size_t k = 6, const std::string& which = "LM", std::size_t max_krylov = 0, T tol = T(0)) { (void)tol; const auto n = static_cast(A.rows()); if (static_cast(A.cols()) != n) throw std::invalid_argument("arnoldi_eigs: matrix must be square"); if (k == 0 || k > n) throw std::invalid_argument("arnoldi_eigs: invalid k"); std::size_t m = (max_krylov > 0) ? std::min(max_krylov, n) : std::min(std::max(k * 2 + 1, std::size_t(20)), n); // Initial vector Vector v(n); T inv_sqrt_n = T(1) / std::sqrt(static_cast(n)); for (std::size_t i = 0; i < n; ++i) v[i] = inv_sqrt_n; // Arnoldi decomposition auto [Q, H] = arnoldi_iteration(A, v, m); std::size_t actual_m = H.empty() ? 0 : H[0].size(); if (actual_m == 0) throw std::runtime_error("arnoldi_eigs: Arnoldi iteration produced no vectors"); // Hessenberg eigenvalues from the upper actual_m x actual_m part of H std::vector> Hm(actual_m, std::vector(actual_m, T(0))); for (std::size_t i = 0; i < actual_m; ++i) for (std::size_t j = 0; j < actual_m; ++j) Hm[i][j] = H[i][j]; auto eigs = hessenberg_eigenvalues(Hm); // Sort according to `which` if (which == "LM") { std::sort(eigs.begin(), eigs.end(), [](T a, T b) { return std::abs(a) > std::abs(b); }); } else if (which == "SM") { std::sort(eigs.begin(), eigs.end(), [](T a, T b) { return std::abs(a) < std::abs(b); }); } else if (which == "LR") { std::sort(eigs.begin(), eigs.end(), std::greater()); } else { std::sort(eigs.begin(), eigs.end()); } std::size_t nk = std::min(k, eigs.size()); EigenResult result; result.eigenvalues.assign(eigs.begin(), eigs.begin() + nk); result.iterations = actual_m; result.converged = true; result.eigenvectors.resize(nk); for (std::size_t i = 0; i < nk; ++i) result.eigenvectors[i] = Q[0]; // placeholder return result; } // ============================================================================ // SPARSEKIT — sparse matrix utilities // ============================================================================ /// @brief Build an adjacency list for a sparse matrix template requires std::integral std::vector> adjacency_list( const SparseMatrix& A) { const auto n = static_cast(A.rows()); std::vector> adj(n); for (std::size_t i = 0; i < n; ++i) { for (std::size_t j = 0; j < static_cast(A.cols()); ++j) { if (A.coeff(static_cast(i), static_cast(j)) != T(0)) adj[i].push_back(j); } } return adj; } /// @brief Reverse Cuthill-McKee (RCM) reordering /// @return new numbering: permutation[i] = old index template requires std::integral std::vector rcm_ordering(const SparseMatrix& A) { const auto n = static_cast(A.rows()); auto adj = adjacency_list(A); auto deg = [&](std::size_t v) { return adj[v].size(); }; std::vector perm; perm.reserve(n); std::vector visited(n, false); while (perm.size() < n) { // Unvisited node with minimum degree std::size_t start = n; std::size_t min_deg = n + 1; for (std::size_t i = 0; i < n; ++i) { if (!visited[i] && deg(i) < min_deg) { min_deg = deg(i); start = i; } } if (start >= n) break; // BFS (in ascending degree order) std::vector queue; queue.push_back(start); visited[start] = true; for (std::size_t qi = 0; qi < queue.size(); ++qi) { std::size_t v = queue[qi]; std::vector neighbors; for (auto u : adj[v]) { if (!visited[u]) neighbors.push_back(u); } std::sort(neighbors.begin(), neighbors.end(), [&](std::size_t a, std::size_t b) { return deg(a) < deg(b); }); for (auto u : neighbors) { if (!visited[u]) { visited[u] = true; queue.push_back(u); } } } // Reverse for (auto it = queue.rbegin(); it != queue.rend(); ++it) perm.push_back(*it); } return perm; } /// @brief Reorder a sparse matrix by applying a permutation template requires std::integral SparseMatrix apply_permutation( const SparseMatrix& A, const std::vector& perm) { const auto n = static_cast(A.rows()); std::vector inv_perm(n); for (std::size_t i = 0; i < n; ++i) inv_perm[perm[i]] = i; SparseMatrix B(A.rows(), A.cols()); for (std::size_t old_i = 0; old_i < n; ++old_i) { std::size_t new_i = inv_perm[old_i]; for (std::size_t old_j = 0; old_j < n; ++old_j) { T val = A.coeff(static_cast(old_i), static_cast(old_j)); if (val != T(0)) { std::size_t new_j = inv_perm[old_j]; B.set_coeff(static_cast(new_i), static_cast(new_j), val); } } } return B; } /// @brief Compute the bandwidth of a sparse matrix template requires std::integral std::size_t bandwidth(const SparseMatrix& A) { const auto n = static_cast(A.rows()); std::size_t bw = 0; for (std::size_t i = 0; i < n; ++i) { for (std::size_t j = 0; j < static_cast(A.cols()); ++j) { if (A.coeff(static_cast(i), static_cast(j)) != T(0)) { std::size_t diff = (i > j) ? i - j : j - i; if (diff > bw) bw = diff; } } } return bw; } } // namespace sparse_algorithms } // namespace sangi #endif // SANGI_SPARSE_MATRIX_ALGORITHMS_HPP