// Copyright (C) 2026 Kiyotsugu Arai // SPDX-License-Identifier: LGPL-3.0-or-later /** * @file iterative_solvers.hpp * @brief 反復法ソルバー API 拡充 * * 追加ソルバー: * - MINRES (対称不定行列用) * - FGMRES (可変前処理対応の Flexible GMRES) * * 統一 API: * - IterativeSolver : ビルダーパターンによるソルバー構成 * - SolverLog : 収束履歴の記録 * - solve() : 行列性質に基づく自動ソルバー選択 */ #ifndef CALX_ITERATIVE_SOLVERS_HPP #define CALX_ITERATIVE_SOLVERS_HPP #include #include #include #include #include #include #include #include #include #include #include #include namespace calx { namespace sparse_algorithms { // ============================================================================ // SolverLog — 収束履歴の記録 // ============================================================================ template struct SolverLog { std::vector residual_history; ///< 各反復の残差ノルム std::vector time_history; ///< (将来用) 各反復の累積時間 void clear() { residual_history.clear(); time_history.clear(); } void record(T rnorm) { residual_history.push_back(rnorm); } /// 収束率 (最後の数反復の幾何平均) [[nodiscard]] T convergence_rate(std::size_t window = 5) const { auto n = residual_history.size(); if (n < 2) return T(1); auto w = std::min(window, n - 1); T ratio = residual_history[n - 1] / residual_history[n - 1 - w]; return std::pow(std::abs(ratio), T(1) / static_cast(w)); } }; // ============================================================================ // MINRES — 対称不定行列用 // ============================================================================ /** * @brief MINRES (Minimum Residual Method) * * 対称 (不定可) 疎行列 A に対して Ax = b を解く。 * CG が正定値を要求するのに対し、MINRES は不定行列にも使える。 * * 実装: Lanczos 三重対角化 + GMRES 式 Givens QR * Lanczos で対称行列を三重対角化し、GMRES と同じ Givens 回転手順で * Hessenberg 上三角分解を行う。対称性から Hessenberg = 三重対角となり * 各列の回転が 1 回で済む (GMRES の j 回に対して定数回)。 */ template requires concepts::OrderedField && std::integral SolverResult minres( const SparseMatrix& A, const Vector& b, const ConvergenceCriteria& criteria, Preconditioner precond = {}, std::optional> x0 = std::nullopt, SolverLog* log = nullptr) { const auto n = static_cast(A.rows()); if (static_cast(A.cols()) != n) throw std::invalid_argument("MINRES: matrix must be square"); if (b.size() != n) throw std::invalid_argument("MINRES: incompatible dimensions"); (void)precond; 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; // Lanczos + Givens QR (GMRES 方式を三重対角に特化) const std::size_t max_iter = std::min(criteria.max_iterations(), n); Vector r = b - A * x; T r_norm = norm(r); if (log) log->record(r_norm); if (r_norm <= threshold) { return {x, r_norm, 0, true}; } // Lanczos 基底 std::vector> Q(max_iter + 1); Q[0] = r / r_norm; // 三重対角行列を Hessenberg 形式で保持 (列ごとに 2 要素: H[j][j], H[j+1][j]) // ただし Givens 回転適用前の値を保持するため、前の列も必要 // → GMRES と同じ構造を使う (H は (m+1)×m だが三重対角なので帯幅 2) std::vector> H(max_iter + 1, std::vector(max_iter, T{0})); std::vector cs(max_iter, T{0}), sn(max_iter, T{0}); std::vector g(max_iter + 1, T{0}); g[0] = r_norm; Vector v_prev(n, T{0}); std::size_t j = 0; for (; j < max_iter; ++j) { // Lanczos ステップ (= 対称行列の Arnoldi ステップ) Vector w = A * Q[j]; // Modified Gram-Schmidt (対称性から j-1 と j のみ非ゼロ) if (j > 0) { H[j - 1][j] = dot(Q[j - 1], w); // = beta_j (三重対角の上側) w -= Q[j - 1] * H[j - 1][j]; } H[j][j] = dot(Q[j], w); // = alpha_j w -= Q[j] * H[j][j]; H[j + 1][j] = norm(w); // = beta_{j+1} // Happy breakdown if (std::abs(H[j + 1][j]) < std::numeric_limits::epsilon() * T{100}) { // Givens 回転を適用してから終了 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; } 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]; // 以前の Givens 回転を H 列に適用 (三重対角 → G_{j-2} が fill-in を生むため最大 2 つ) for (std::size_t i = (j >= 2 ? j - 2 : 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; } // j 番目の Givens 回転 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}; 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]); if (log) log->record(r_norm); if (r_norm <= threshold) { ++j; break; } } // 後退代入: R y = g 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]; } } // 解の更新: x += Q * y for (std::size_t k = 0; k < dim; ++k) { x += Q[k] * y[k]; } T final_rnorm = norm(b - A * x); return {x, final_rnorm, dim, final_rnorm <= threshold}; } // ============================================================================ // FGMRES — Flexible GMRES (可変前処理対応) // ============================================================================ /** * @brief Flexible GMRES (可変前処理対応) * * 通常の GMRES では各反復で同一の前処理が必要だが、 * FGMRES では反復ごとに異なる前処理を適用できる。 * 前処理付き基底ベクトルを別途保持するため、メモリは 2 倍。 * * 参考: Saad (1993), "A Flexible Inner-Outer Preconditioned GMRES Algorithm" */ template requires concepts::OrderedField && std::integral SolverResult fgmres( const SparseMatrix& A, const Vector& b, const ConvergenceCriteria& criteria, std::size_t restart = 30, Preconditioner precond = {}, std::optional> x0 = std::nullopt, SolverLog* log = nullptr) { const auto n = static_cast(A.rows()); if (static_cast(A.cols()) != n) throw std::invalid_argument("FGMRES: matrix must be square"); if (b.size() != n) throw std::invalid_argument("FGMRES: 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); if (b_norm <= criteria.absolute_tolerance()) { return {x, b_norm, 0, true}; } const T threshold = criteria.absolute_tolerance() + criteria.relative_tolerance() * b_norm; std::size_t total_iter = 0; while (total_iter < criteria.max_iterations()) { Vector r = b - A * x; T r_norm = norm(r); if (log) log->record(r_norm); if (r_norm <= threshold) { return {x, r_norm, total_iter, true}; } const std::size_t m = std::min(restart, criteria.max_iterations() - total_iter); // Arnoldi 基底 V と前処理済み基底 Z std::vector> V(m + 1); std::vector> Z(m); V[0] = r / r_norm; // 上 Hessenberg 行列 (m+1 x m) std::vector> H(m + 1, std::vector(m, T{0})); // Givens 回転パラメータ std::vector cs(m, T{0}), sn(m, T{0}); // 変換済み右辺 std::vector g(m + 1, T{0}); g[0] = r_norm; std::size_t j = 0; for (; j < m; ++j) { // FGMRES: z_j = M^{-1} v_j (前処理済み基底を保持) Z[j] = precond(V[j]); Vector w = A * Z[j]; // Modified Gram-Schmidt for (std::size_t i = 0; i <= j; ++i) { H[i][j] = dot(V[i], w); w -= V[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}) { // 以前の Givens 回転を適用 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; } 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; } V[j + 1] = w / H[j + 1][j]; // 以前の Givens 回転を 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; } // j 番目の Givens 回転 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}; 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 (log) log->record(r_norm); if (r_norm <= threshold) { ++j; break; } } // 後退代入 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]; } } // FGMRES: x += Z * y (前処理済み基底を使用) for (std::size_t k = 0; k < dim; ++k) { x += Z[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}; } // ============================================================================ // ログ付き CG / BiCGSTAB / GMRES — 既存ソルバーのラッパー // ============================================================================ /** * @brief ログ付き CG */ template requires concepts::OrderedField && std::integral SolverResult conjugate_gradient_logged( const SparseMatrix& A, const Vector& b, const ConvergenceCriteria& criteria, SolverLog& log, 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); log.record(init_rnorm); if (init_rnorm <= criteria.absolute_tolerance()) { return {x, init_rnorm, 0, true}; } Vector z = precond(r); Vector p = z; T rz = dot(r, z); 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()) { 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); log.record(rnorm); 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; } T final_rnorm = norm(b - A * x); return {x, final_rnorm, criteria.max_iterations(), is_converged(final_rnorm)}; } /** * @brief ログ付き BiCGSTAB */ template requires concepts::OrderedField && std::integral SolverResult bicgstab_logged( const SparseMatrix& A, const Vector& b, const ConvergenceCriteria& criteria, SolverLog& log, 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); log.record(init_rnorm); 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}), v(n, T{0}); const T eps = std::numeric_limits::epsilon(); const T breakdown_tol = eps * init_rnorm * init_rnorm; 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) { T rho_new = dot(r_hat, r); if (std::abs(rho_new) < breakdown_tol) { T rnorm = norm(b - A * x); return {x, rnorm, iter, is_converged(rnorm)}; } if (iter == 0) { p = r; } else { T beta = (rho_new / rho) * (alpha / omega_val); p = r + (p - v * omega_val) * beta; } rho = rho_new; Vector p_hat = precond(p); v = A * p_hat; T denom = dot(r_hat, v); if (std::abs(denom) < breakdown_tol) { T rnorm = norm(b - A * x); return {x, rnorm, iter, is_converged(rnorm)}; } alpha = rho / denom; Vector s = r - v * alpha; T s_norm = norm(s); if (is_converged(s_norm)) { x += p_hat * alpha; log.record(s_norm); return {x, s_norm, iter + 1, true}; } 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; T rnorm = norm(b - A * x); return {x, rnorm, iter + 1, is_converged(rnorm)}; } omega_val = dot(t, s) / tt; x += p_hat * alpha + s_hat * omega_val; r = s - t * omega_val; T rnorm = norm(r); log.record(rnorm); if (is_converged(rnorm)) { return {x, rnorm, iter + 1, true}; } if (std::abs(omega_val) < eps * init_rnorm) { T true_rnorm = norm(b - A * x); return {x, true_rnorm, iter + 1, is_converged(true_rnorm)}; } } T final_rnorm = norm(b - A * x); return {x, final_rnorm, criteria.max_iterations(), is_converged(final_rnorm)}; } // ============================================================================ // SolverType 列挙 // ============================================================================ enum class SolverType { CG, ///< 共役勾配法 (SPD) BiCG, ///< BiCG (非対称) BiCGSTAB, ///< BiCGSTAB (非対称, 安定) GMRES, ///< GMRES(m) (非対称) FGMRES, ///< Flexible GMRES (可変前処理) MINRES, ///< MINRES (対称不定) Auto ///< 自動選択 }; enum class PreconditionerType { None, ///< 前処理なし Jacobi, ///< Jacobi (対角) SSOR, ///< SSOR ILU0, ///< ILU(0) IC0, ///< IC(0) (SPD 用) Custom ///< カスタム }; // ============================================================================ // IterativeSolver — ビルダーパターンによる統一 API // ============================================================================ /** * @brief 反復法ソルバーの統一インタフェース * * 使用例: * @code * auto result = IterativeSolver(A, b) * .solver(SolverType::BiCGSTAB) * .preconditioner(PreconditionerType::ILU0) * .tolerance(1e-10, 1e-8) * .maxIterations(500) * .enableLog() * .solve(); * @endcode */ template requires concepts::OrderedField && std::integral class IterativeSolver { public: IterativeSolver(const SparseMatrix& A, const Vector& b) : A_(A), b_(b) {} /// ソルバーの種類を設定 IterativeSolver& solver(SolverType type) { solver_type_ = type; return *this; } /// 組み込み前処理を設定 IterativeSolver& preconditioner(PreconditionerType type) { precond_type_ = type; return *this; } /// カスタム前処理を設定 IterativeSolver& preconditioner(Preconditioner p) { precond_type_ = PreconditionerType::Custom; custom_precond_ = std::move(p); return *this; } /// SSOR の緩和係数を設定 IterativeSolver& ssorOmega(T omega) { ssor_omega_ = omega; return *this; } /// 許容誤差を設定 IterativeSolver& tolerance(T abs_tol, T rel_tol) { criteria_.set_absolute_tolerance(abs_tol); criteria_.set_relative_tolerance(rel_tol); return *this; } /// 最大反復回数を設定 IterativeSolver& maxIterations(std::size_t n) { criteria_.set_max_iterations(n); return *this; } /// GMRES リスタートパラメータを設定 IterativeSolver& restart(std::size_t m) { restart_ = m; return *this; } /// 初期推定値を設定 IterativeSolver& initialGuess(Vector x0) { x0_ = std::move(x0); return *this; } /// 収束履歴の記録を有効化 IterativeSolver& enableLog() { log_enabled_ = true; return *this; } /// 収束履歴を取得 (solve() 後に呼ぶ) [[nodiscard]] const SolverLog& log() const { return log_; } /// ソルバーを実行 [[nodiscard]] SolverResult solve() { auto precond = buildPreconditioner(); auto type = (solver_type_ == SolverType::Auto) ? autoSelect() : solver_type_; if (log_enabled_) log_.clear(); SolverLog* log_ptr = log_enabled_ ? &log_ : nullptr; switch (type) { case SolverType::CG: if (log_ptr) { return conjugate_gradient_logged(A_, b_, criteria_, *log_ptr, precond, x0_); } return conjugate_gradient(A_, b_, criteria_, precond, x0_); case SolverType::BiCG: return biconjugate_gradient(A_, b_, criteria_, x0_); case SolverType::BiCGSTAB: if (log_ptr) { return bicgstab_logged(A_, b_, criteria_, *log_ptr, precond, x0_); } return bicgstab(A_, b_, criteria_, precond, x0_); case SolverType::GMRES: return gmres(A_, b_, criteria_, restart_, precond, x0_); case SolverType::FGMRES: return fgmres(A_, b_, criteria_, restart_, precond, x0_, log_ptr); case SolverType::MINRES: return minres(A_, b_, criteria_, precond, x0_, log_ptr); default: return conjugate_gradient(A_, b_, criteria_, precond, x0_); } } private: const SparseMatrix& A_; const Vector& b_; SolverType solver_type_ = SolverType::Auto; PreconditionerType precond_type_ = PreconditionerType::None; Preconditioner custom_precond_; ConvergenceCriteria criteria_; std::size_t restart_ = 30; std::optional> x0_; T ssor_omega_ = T{1}; bool log_enabled_ = false; SolverLog log_; Preconditioner buildPreconditioner() { switch (precond_type_) { case PreconditionerType::Jacobi: return jacobi_preconditioner(A_); case PreconditionerType::SSOR: return ssor_preconditioner(A_, ssor_omega_); case PreconditionerType::ILU0: return ilu_preconditioner(A_); case PreconditionerType::IC0: return incomplete_cholesky_preconditioner(A_); case PreconditionerType::Custom: return custom_precond_; case PreconditionerType::None: default: return {}; } } /// 行列の性質から自動的にソルバーを選択 SolverType autoSelect() const { // 対称判定 (サンプリング) bool symmetric = isApproximatelySymmetric(); if (symmetric) { // SPD の可能性 → CG を試行 (不定なら MINRES にフォールバック) return SolverType::CG; } // 非対称 → BiCGSTAB (一般的に安定) return SolverType::BiCGSTAB; } /// 対称性の簡易判定 (対角近傍のサンプリング) bool isApproximatelySymmetric() const { const auto n = A_.rows(); const std::size_t samples = std::min(static_cast(n), std::size_t(50)); const T eps = std::numeric_limits::epsilon() * T(1000); for (std::size_t k = 0; k < samples; ++k) { std::size_t i = k * static_cast(n) / samples; for (std::size_t j = i + 1; j < std::min(i + 5, static_cast(n)); ++j) { T aij = A_.coeff(static_cast(i), static_cast(j)); T aji = A_.coeff(static_cast(j), static_cast(i)); T scale = std::max(std::abs(aij), std::abs(aji)); if (scale > T(0) && std::abs(aij - aji) > eps * scale) { return false; } } } return true; } }; // ============================================================================ // solve() — 自動選択の便利関数 // ============================================================================ /** * @brief 疎行列の連立一次方程式を自動的に適切なソルバーで解く * * 行列の性質を判定し、対称なら CG、非対称なら BiCGSTAB を選択。 * ILU(0) 前処理を自動適用。 */ template requires concepts::OrderedField && std::integral SolverResult solve( const SparseMatrix& A, const Vector& b, const ConvergenceCriteria& criteria = ConvergenceCriteria()) { return IterativeSolver(A, b) .preconditioner(PreconditionerType::ILU0) .solver(SolverType::Auto) .tolerance(criteria.absolute_tolerance(), criteria.relative_tolerance()) .maxIterations(criteria.max_iterations()) .solve(); } } // namespace sparse_algorithms } // namespace calx #endif // CALX_ITERATIVE_SOLVERS_HPP