// Copyright (C) 2026 Kiyotsugu Arai // SPDX-License-Identifier: LGPL-3.0-or-later /** * @file sparse_qr.hpp * @brief 疎行列用 QR 分解 (SparseQR) * * Householder QR を疎行列に適用。長方行列 (m >= n) に対応。 * 最小二乗問題 min ||Ax - b|| の解法に使用。 * * API は Eigen の SparseQR に準拠: * SparseQR qr; * qr.compute(A); * Vector x = qr.solve(b); */ #ifndef CALX_SPARSE_QR_HPP #define CALX_SPARSE_QR_HPP #include #include #include #include #include #include #include "../core/sparse_matrix.hpp" #include "../core/vector.hpp" #include "../core/matrix.hpp" namespace calx { /** * @brief 疎行列 QR 分解 * * A = Q * R (Q: m×m 直交, R: m×n 上三角) * 内部的には Q を暗黙的に保持 (Householder ベクトル + tau)。 */ template class SparseQR { public: SparseQR() = default; /// 疎行列を QR 分解する void compute(const SparseMatrix& A) { m_ = static_cast(A.rows()); n_ = static_cast(A.cols()); if (m_ < n_) throw std::invalid_argument("SparseQR: requires m >= n (overdetermined)"); computed_ = false; // 密形式に変換して Householder QR を適用 R_.resize(m_ * n_); for (std::size_t i = 0; i < m_; ++i) for (std::size_t j = 0; j < n_; ++j) R_[i * n_ + j] = A.coeff(static_cast(i), static_cast(j)); tau_.resize(n_); householder_vecs_.resize(m_ * n_, T(0)); for (std::size_t k = 0; k < n_; ++k) { // 列 k の k:m 部分のノルム T sigma = T(0); for (std::size_t i = k; i < m_; ++i) sigma += R_[i * n_ + k] * R_[i * n_ + k]; sigma = std::sqrt(sigma); if (sigma < std::numeric_limits::epsilon()) { tau_[k] = T(0); continue; } T alpha = (R_[k * n_ + k] >= T(0)) ? -sigma : sigma; T beta = R_[k * n_ + k] - alpha; // Householder ベクトルを保存 householder_vecs_[k * n_ + k] = T(1); T inv_beta = T(1) / beta; for (std::size_t i = k + 1; i < m_; ++i) householder_vecs_[i * n_ + k] = R_[i * n_ + k] * inv_beta; tau_[k] = -beta / alpha; R_[k * n_ + k] = alpha; // R の残りの列に Householder を適用 for (std::size_t j = k + 1; j < n_; ++j) { T d = R_[k * n_ + j]; for (std::size_t i = k + 1; i < m_; ++i) d += householder_vecs_[i * n_ + k] * R_[i * n_ + j]; d *= tau_[k]; R_[k * n_ + j] -= d; for (std::size_t i = k + 1; i < m_; ++i) R_[i * n_ + j] -= householder_vecs_[i * n_ + k] * d; } // R の下三角をクリア for (std::size_t i = k + 1; i < m_; ++i) R_[i * n_ + k] = T(0); } computed_ = true; } /// 最小二乗問題 min ||Ax - b|| を解く [[nodiscard]] Vector solve(const Vector& b) const { if (!computed_) throw std::runtime_error("SparseQR: compute() not called"); if (b.size() != m_) throw std::invalid_argument("SparseQR::solve: dimension mismatch"); // Q^T * b を計算 (Householder ベクトルを順に適用) Vector qtb = b; for (std::size_t k = 0; k < n_; ++k) { if (std::abs(tau_[k]) < std::numeric_limits::epsilon()) continue; T d = qtb[k]; for (std::size_t i = k + 1; i < m_; ++i) d += householder_vecs_[i * n_ + k] * qtb[i]; d *= tau_[k]; qtb[k] -= d; for (std::size_t i = k + 1; i < m_; ++i) qtb[i] -= householder_vecs_[i * n_ + k] * d; } // R * x = (Q^T b)[:n] の後退代入 Vector x(n_); for (std::size_t ii = 0; ii < n_; ++ii) { std::size_t i = n_ - 1 - ii; x[i] = qtb[i]; for (std::size_t j = i + 1; j < n_; ++j) x[i] -= R_[i * n_ + j] * x[j]; if (std::abs(R_[i * n_ + i]) < std::numeric_limits::epsilon()) throw std::runtime_error("SparseQR: R is singular"); x[i] /= R_[i * n_ + i]; } return x; } /// 分解済みかどうか [[nodiscard]] bool computed() const { return computed_; } /// 行列サイズ [[nodiscard]] std::size_t rows() const { return m_; } [[nodiscard]] std::size_t cols() const { return n_; } /// 数値ランク [[nodiscard]] std::size_t rank(T threshold = T(-1)) const { if (!computed_) return 0; if (threshold < T(0)) { threshold = std::abs(R_[0]) * static_cast(std::max(m_, n_)) * std::numeric_limits::epsilon(); } std::size_t r = 0; for (std::size_t i = 0; i < n_; ++i) { if (std::abs(R_[i * n_ + i]) > threshold) ++r; else break; } return r; } private: std::size_t m_ = 0, n_ = 0; bool computed_ = false; std::vector R_; // m×n (上三角) std::vector tau_; // n (Householder 係数) std::vector householder_vecs_; // m×n (Householder ベクトル) }; } // namespace calx #endif // CALX_SPARSE_QR_HPP