| // This file is part of Eigen, a lightweight C++ template library |
| // for linear algebra. |
| // |
| // This Source Code Form is subject to the terms of the Mozilla |
| // Public License v. 2.0. If a copy of the MPL was not distributed |
| // with this file, You can obtain one at http://mozilla.org/MPL/2.0/. |
| // SPDX-FileCopyrightText: The Eigen Authors |
| // SPDX-License-Identifier: MPL-2.0 |
| |
| #ifndef EIGEN_STRUCTURED_LOOK_AHEAD_LEVINSON_H |
| #define EIGEN_STRUCTURED_LOOK_AHEAD_LEVINSON_H |
| |
| // IWYU pragma: private |
| #include "./InternalHeaderCheck.h" |
| |
| namespace Eigen { |
| |
| template <typename Scalar_> |
| class LookAheadLevinson; |
| |
| namespace internal { |
| |
| template <typename Scalar_> |
| struct traits<LookAheadLevinson<Scalar_>> : traits<Matrix<Scalar_, Dynamic, Dynamic>> { |
| using XprKind = MatrixXpr; |
| using StorageKind = SolverStorage; |
| using StorageIndex = int; |
| using BaseTraits = traits<Matrix<Scalar_, Dynamic, Dynamic>>; |
| static constexpr int Flags = BaseTraits::Flags & RowMajorBit; |
| static constexpr int CoeffReadCost = Dynamic; |
| }; |
| |
| // Persymmetry E_n T_n E_n = T_n^T requires sum_i a_i b_i, not the conjugating dot(). |
| template <typename D1, typename D2> |
| typename D1::Scalar structured_tdot(const MatrixBase<D1>& a, const MatrixBase<D2>& b) { |
| return a.cwiseProduct(b).sum(); |
| } |
| |
| template <typename Scalar> |
| Matrix<Scalar, Dynamic, 1> structured_upshift(const Matrix<Scalar, Dynamic, 1>& v) { |
| const Index k = v.size(); |
| Matrix<Scalar, Dynamic, 1> w = Matrix<Scalar, Dynamic, 1>::Zero(k); |
| if (k > 1) w.head(k - 1) = v.tail(k - 1); |
| return w; |
| } |
| |
| } // namespace internal |
| |
| /** \ingroup StructuredMatrices_Module |
| * \class LookAheadLevinson |
| * \brief Look-ahead Levinson direct solver for general Toeplitz systems. |
| * |
| * Solves \c T*x = b for a square \ref Toeplitz matrix \c T in O(n^2) operations. |
| * This is an implementation of the look-ahead Levinson algorithm of T. F. Chan and |
| * P. C. Hansen, which extends the classical Levinson recursion to remain (weakly) |
| * numerically stable for general — including indefinite and ill-conditioned — |
| * Toeplitz matrices. When the recursion would otherwise break down at a |
| * near-singular leading principal submatrix, the algorithm "looks ahead" and takes |
| * a block step (up to \ref maxBlockSize) over it. As a by-product it produces an |
| * estimate of the matrix condition number (\ref conditionEstimate). |
| * |
| * The class derives from \c SolverBase and follows the usual decomposition style; |
| * transposed and adjoint systems reuse the factorization of \c T through the |
| * persymmetry \f$ T^T = E T E \f$ (with \c E the exchange matrix), so all three |
| * solves below cost the same: |
| * \code |
| * LookAheadLevinson<double> levinson(T); // or levinson.compute(T); |
| * VectorXd x = levinson.solve(b); // solve T * x = b |
| * VectorXd y = levinson.transpose().solve(b); // solve T^T * y = b |
| * VectorXd z = levinson.adjoint().solve(b); // solve T^H * z = b |
| * \endcode |
| * |
| * \tparam Scalar_ the scalar type, real or complex. |
| * |
| * References: |
| * - T. F. Chan and P. C. Hansen, "A look-ahead Levinson algorithm for general |
| * Toeplitz systems," IEEE Trans. Signal Process., 40(5):1079-1090, 1992. |
| * - T. F. Chan and P. C. Hansen, "A look-ahead Levinson algorithm for indefinite |
| * Toeplitz systems," SIAM J. Matrix Anal. Appl., 13(2):490-506, 1992. |
| * |
| * \sa class Toeplitz |
| */ |
| template <typename Scalar_> |
| class LookAheadLevinson : public SolverBase<LookAheadLevinson<Scalar_>> { |
| public: |
| using Base = SolverBase<LookAheadLevinson>; |
| friend class SolverBase<LookAheadLevinson>; |
| EIGEN_GENERIC_PUBLIC_INTERFACE(LookAheadLevinson) |
| using DenseVector = Matrix<Scalar, Dynamic, 1>; |
| using DenseMatrix = Matrix<Scalar, Dynamic, Dynamic>; |
| |
| /** Default constructor; call \ref compute before \ref solve. */ |
| LookAheadLevinson() : m_maxBlockSize(4), m_n(0), m_isInitialized(false), m_info(InvalidInput) {} |
| |
| /** Constructs and factorizes from the Toeplitz matrix \a T, which may have fixed |
| * or dynamic dimensions. */ |
| template <int Rows_, int Cols_> |
| explicit LookAheadLevinson(const Toeplitz<Scalar, Rows_, Cols_>& T) |
| : m_maxBlockSize(4), m_n(0), m_isInitialized(false), m_info(InvalidInput) { |
| compute(T); |
| } |
| |
| /** Sets the maximum look-ahead block size \f$ p_{\max} \f$ (default 4). The solver |
| * is numerically stable as long as \c T has no more than \c p-1 consecutive |
| * ill-conditioned leading principal submatrices. Must be called before \ref compute. */ |
| LookAheadLevinson& setMaxBlockSize(Index p) { |
| eigen_assert(p >= 1); |
| m_maxBlockSize = p; |
| return *this; |
| } |
| |
| /** \returns the maximum look-ahead block size \f$ p_{\max} \f$. \sa setMaxBlockSize */ |
| Index maxBlockSize() const { return m_maxBlockSize; } |
| |
| Index rows() const noexcept { return m_n; } |
| Index cols() const noexcept { return m_n; } |
| |
| /** Factorizes the square Toeplitz matrix \a T. \sa solve */ |
| template <int Rows_, int Cols_> |
| LookAheadLevinson& compute(const Toeplitz<Scalar, Rows_, Cols_>& T); |
| |
| #ifdef EIGEN_PARSED_BY_DOXYGEN |
| /** \returns the solution \c x of \c T*x = \a b, as a lazily evaluated expression. |
| * Supports multiple right-hand sides. The transposed and adjoint systems are |
| * solved at the same cost, and with the same factorization, through |
| * \c transpose().solve(b) and \c adjoint().solve(b). |
| * \pre \ref compute has been called. */ |
| template <typename Rhs> |
| inline const Solve<LookAheadLevinson, Rhs> solve(const MatrixBase<Rhs>& b) const; |
| #endif |
| |
| #ifndef EIGEN_PARSED_BY_DOXYGEN |
| /** \internal Evaluates the solution of \c T*x = rhs into \a dst; called through |
| * the \c Solve expression returned by \c SolverBase::solve. The copy must be |
| * coefficient-wise (noalias), never a move: when the Solve expression is |
| * evaluated inside a larger expression, \c evaluator<Solve> caches pointers |
| * into \a dst before calling this, and a move-assignment of the returned |
| * matrix (same plain type for a matrix right-hand side) would steal its buffer |
| * and leave those pointers dangling. */ |
| template <typename RhsType, typename DstType> |
| void _solve_impl(const RhsType& rhs, DstType& dst) const { |
| dst.noalias() = solveForward(rhs); |
| } |
| |
| /** \internal Persymmetry E T E = T^T (with E the exchange matrix) turns the |
| * transposed system T^T x = b into T (E x) = E b, so a transposed solve is a |
| * forward solve with row-reversed right-hand side and solution -- conjugated on |
| * the way in and out for the adjoint -- reusing the factorization of T as is. */ |
| template <bool Conjugate, typename RhsType, typename DstType> |
| void _solve_impl_transposed(const RhsType& rhs, DstType& dst) const { |
| const DenseMatrix reversed = rhs.template conjugateIf<Conjugate>().colwise().reverse(); |
| dst = solveForward(reversed).colwise().reverse().template conjugateIf<Conjugate>(); |
| } |
| #endif |
| |
| /** \returns \c Success if the algorithm could skip all ill-conditioned leading |
| * submatrices within the look-ahead range, \c NumericalIssue otherwise. */ |
| ComputationInfo info() const { |
| eigen_assert(m_isInitialized && "LookAheadLevinson is not initialized."); |
| return m_info; |
| } |
| |
| /** \returns an estimate of the 2-norm condition number of \c T, available after |
| * \ref compute (the "algorithm condition number" of Chan and Hansen). */ |
| RealScalar conditionEstimate() const { |
| eigen_assert(m_isInitialized && "LookAheadLevinson is not initialized."); |
| return m_sMin > RealScalar(0) ? m_normEst / m_sMin : NumTraits<RealScalar>::infinity(); |
| } |
| |
| private: |
| /** \internal \returns the solution of the forward system \c T*x = b by advancing |
| * x through the recorded steps of the factorization. */ |
| template <typename Rhs> |
| DenseMatrix solveForward(const Rhs& b) const { |
| const Index nrhs = b.cols(); |
| DenseMatrix x = m_luInit.solve(b.topRows(m_k0)); |
| DenseMatrix rhs, a, xn; |
| for (const Step& s : m_steps) { |
| rhs = b.middleRows(s.k, s.p) - s.WspEk * x; // (27): b_p - S_p^T E_k x_k |
| a = s.luGamma.solve(rhs); |
| xn.resize(s.k + s.p, nrhs); |
| xn.topRows(s.k) = x + s.EkYp * a; // [E_k Y_p; I_p] a_p |
| xn.bottomRows(s.p) = a; |
| x = xn; |
| } |
| return x; |
| } |
| |
| // Per accepted block step, the b-independent data needed to advance x in solve(). |
| struct Step { |
| Index k; // order before the step |
| Index p; // block size |
| DenseMatrix EkYp; // E_k Y_p (k x p) |
| DenseMatrix WspEk; // S_p^T E_k (p x k) |
| FullPivLU<DenseMatrix> luGamma; |
| }; |
| |
| // Leading p x p principal submatrix built directly from the generators. |
| static DenseMatrix leadingBlock(const DenseVector& c, const DenseVector& r, Index p) { |
| DenseMatrix B(p, p); |
| for (Index j = 0; j < p; ++j) |
| for (Index i = 0; i < p; ++i) B(i, j) = (i >= j) ? c[i - j] : r[j - i]; |
| return B; |
| } |
| |
| // Builds a (len x cols) block whose column j is the length-len generator segment |
| // v.segment(first + j, len); used to assemble the S_p and R_p shift blocks. |
| static DenseMatrix shiftBlock(const DenseVector& v, Index first, Index len, Index cols) { |
| DenseMatrix B(len, cols); |
| for (Index j = 0; j < cols; ++j) B.col(j) = v.segment(first + j, len); |
| return B; |
| } |
| |
| static RealScalar smallestSingularValue(const DenseMatrix& M) { |
| if (M.rows() == 1) return numext::abs(M(0, 0)); |
| JacobiSVD<DenseMatrix> svd(M); |
| return svd.singularValues()(svd.singularValues().size() - 1); |
| } |
| |
| Index m_maxBlockSize; |
| Index m_n; |
| Index m_k0; |
| FullPivLU<DenseMatrix> m_luInit; // LU of the leading T_{k0} |
| std::vector<Step> m_steps; |
| bool m_isInitialized; |
| ComputationInfo m_info; |
| RealScalar m_sMin; |
| RealScalar m_normEst; |
| }; |
| |
| template <typename Scalar_> |
| template <int Rows_, int Cols_> |
| LookAheadLevinson<Scalar_>& LookAheadLevinson<Scalar_>::compute(const Toeplitz<Scalar, Rows_, Cols_>& T) { |
| using internal::structured_tdot; |
| using internal::structured_upshift; |
| |
| EIGEN_STATIC_ASSERT(Rows_ == Dynamic || Cols_ == Dynamic || Rows_ == Cols_, YOU_MIXED_MATRICES_OF_DIFFERENT_SIZES) |
| eigen_assert(T.rows() == T.cols() && "LookAheadLevinson requires a square Toeplitz matrix"); |
| const Index n = T.rows(); |
| const DenseVector c = T.column(); // (rho_0, sigma_1, ..., sigma_{n-1}) |
| const DenseVector r = T.row(); // (rho_0, rho_1, ..., rho_{n-1}) |
| const Scalar rho0 = c[0]; |
| |
| m_n = n; |
| m_steps.clear(); |
| m_info = Success; |
| m_normEst = c.cwiseAbs().sum() + r.cwiseAbs().sum(); // cheap upper bound on ||T||_2 |
| |
| Index k; |
| DenseVector y, z; |
| Scalar gamma(0); |
| { |
| Index kbest = 1; |
| RealScalar best = RealScalar(-1); |
| const Index kmax = numext::mini<Index>(m_maxBlockSize, n); |
| for (Index i = 1; i <= kmax; ++i) { |
| RealScalar sv = smallestSingularValue(leadingBlock(c, r, i)); |
| if (sv > best) { |
| best = sv; |
| kbest = i; |
| } |
| } |
| k = m_k0 = kbest; |
| m_luInit.compute(leadingBlock(c, r, k)); |
| m_sMin = best; |
| } |
| |
| bool lastWasBlock = false; |
| DenseVector yPrev, zPrev; |
| Scalar gammaPrev(0); |
| Index kp = 0, pp = 0; |
| DenseMatrix Ypp, Zpp; |
| FullPivLU<DenseMatrix> luGammaPP; |
| DenseVector y_kp_1, z_kp_1; |
| |
| if (k < n) { |
| y = m_luInit.transpose().solve((-r.segment(1, k)).eval()); // T_k^T y = -r_k |
| z = m_luInit.solve((-c.segment(1, k)).eval()); // T_k z = -s_k |
| gamma = rho0 + structured_tdot(c.segment(1, k), y); |
| if (k == 1) { |
| lastWasBlock = false; |
| yPrev = DenseVector::Zero(0); |
| zPrev = DenseVector::Zero(0); |
| gammaPrev = rho0; // gamma^{(0)} |
| } else { |
| lastWasBlock = true; // treat init as a block step of size k from order 0 |
| kp = 0; |
| pp = k; |
| Ypp = DenseMatrix::Zero(0, k); |
| Zpp = DenseMatrix::Zero(0, k); |
| luGammaPP = m_luInit; |
| y_kp_1 = DenseVector::Zero(0); |
| z_kp_1 = DenseVector::Zero(0); |
| } |
| } |
| |
| while (k < n) { |
| const Index pcap = numext::mini<Index>(m_maxBlockSize, n - k); |
| const DenseVector Ek_y = y.reverse(), Ek_z = z.reverse(); |
| |
| DenseVector g_k, h_k; |
| bool gh_ready = false; |
| auto ensure_gh = [&]() { |
| if (gh_ready) return; |
| if (!lastWasBlock) { |
| // Theorem 1, eq (33) |
| g_k.resize(k); |
| h_k.resize(k); |
| g_k.head(k - 1) = zPrev.reverse(); |
| g_k[k - 1] = Scalar(1); |
| g_k /= gammaPrev; |
| h_k.head(k - 1) = yPrev.reverse(); |
| h_k[k - 1] = Scalar(1); |
| h_k /= gammaPrev; |
| } else { |
| // Theorem 2: y_{k,1}, z_{k,1} (eqs 47-49), then g_k, h_k (eq 50). |
| DenseVector rhs_y = -r.segment(kp + 2, pp); |
| DenseVector rhs_z = -c.segment(kp + 2, pp); |
| if (kp > 0) { |
| const DenseMatrix Rpp = shiftBlock(r, 1, kp, pp); |
| const DenseMatrix Spp = shiftBlock(c, 1, kp, pp); |
| rhs_y.noalias() -= Rpp.transpose() * y_kp_1.reverse(); |
| rhs_z.noalias() -= Spp.transpose() * z_kp_1.reverse(); |
| } |
| const DenseVector a_y = luGammaPP.transpose().solve(rhs_y); |
| const DenseVector a_z = luGammaPP.solve(rhs_z); |
| DenseVector yk1 = DenseVector::Zero(k), zk1 = DenseVector::Zero(k); |
| if (kp > 0) { |
| yk1.head(kp) = y_kp_1 + (Zpp * a_y).reverse(); |
| zk1.head(kp) = z_kp_1 + (Ypp * a_z).reverse(); |
| } |
| yk1.tail(pp) = a_y; |
| zk1.tail(pp) = a_z; |
| const Scalar c1 = -r[k + 1] - structured_tdot(r.segment(1, k), Ek_y); |
| const Scalar d1 = -c[k + 1] - structured_tdot(c.segment(1, k), Ek_z); |
| g_k = (yk1 - structured_upshift<Scalar>(y) + y[0] * y) / c1; |
| h_k = (zk1 - structured_upshift<Scalar>(z) + z[0] * z) / d1; |
| } |
| gh_ready = true; |
| }; |
| |
| // Build Y_p, Z_p incrementally and pick the block size p (eqs 31-32, 46, 39). |
| DenseMatrix Yp(k, pcap), Zp(k, pcap); |
| Yp.col(0) = y; |
| Zp.col(0) = z; |
| Index chosen = -1; |
| Index bestP = 1; |
| RealScalar bestPsi = RealScalar(-1); |
| DenseMatrix bestGamma; |
| |
| for (Index p = 1; p <= pcap; ++p) { |
| if (p >= 2) { |
| ensure_gh(); |
| const Index i = p - 1; |
| const Scalar ci = -r[k + i] - structured_tdot(r.segment(i, k), Ek_y); |
| const Scalar di = -c[k + i] - structured_tdot(c.segment(i, k), Ek_z); |
| Yp.col(i) = structured_upshift<Scalar>(Yp.col(i - 1)) - Yp.col(i - 1)[0] * y + ci * g_k; |
| Zp.col(i) = structured_upshift<Scalar>(Zp.col(i - 1)) - Zp.col(i - 1)[0] * z + di * h_k; |
| } |
| const DenseMatrix Sp = shiftBlock(c, 1, k, p); |
| DenseMatrix Gamma = leadingBlock(c, r, p) + Sp.transpose() * Yp.leftCols(p); // (20) |
| const RealScalar muY = Yp.leftCols(p).cwiseAbs().maxCoeff(); |
| const RealScalar muZ = Zp.leftCols(p).cwiseAbs().maxCoeff(); |
| const RealScalar denom = numext::maxi(numext::maxi(RealScalar(1), muY), numext::maxi(muZ, muY * muZ)); |
| const RealScalar psi = smallestSingularValue(Gamma) / denom; // (46) |
| if (psi > bestPsi) { |
| bestPsi = psi; |
| bestP = p; |
| bestGamma = Gamma; |
| } |
| if (psi > RealScalar(0.1) * m_sMin) { // (39) |
| chosen = p; |
| bestGamma = Gamma; |
| break; |
| } |
| } |
| |
| DenseMatrix Gamma = bestGamma; |
| Index p; |
| if (chosen > 0) { |
| p = chosen; |
| } else { |
| p = bestP; |
| m_sMin = bestPsi; // pmax too small to skip cleanly; record the degraded conditioning |
| } |
| |
| const Index K = k + p; |
| const bool finalStep = (K == n); |
| |
| const DenseMatrix Sp = shiftBlock(c, 1, k, p); |
| const DenseMatrix Rp = shiftBlock(r, 1, k, p); |
| FullPivLU<DenseMatrix> luG(Gamma); |
| |
| // Record the b-independent data for solve(): rhs = b_p - (S_p^T E_k) x_k ; x update via E_k Y_p. |
| Step step; |
| step.k = k; |
| step.p = p; |
| step.EkYp = Yp.leftCols(p).colwise().reverse(); // E_k Y_p |
| step.WspEk = Sp.transpose().rowwise().reverse(); // S_p^T E_k |
| step.luGamma = luG; |
| m_steps.push_back(step); |
| |
| if (!finalStep) { |
| const DenseVector rhs_e = -r.segment(k + 1, p) - Rp.transpose() * Ek_y; // (28) |
| const DenseVector rhs_f = -c.segment(k + 1, p) - Sp.transpose() * Ek_z; // (29) |
| const DenseVector e_p = luG.transpose().solve(rhs_e); |
| const DenseVector f_p = luG.solve(rhs_f); |
| |
| DenseVector y_new(K), z_new(K); |
| y_new.head(k) = y + (Zp.leftCols(p) * e_p).reverse(); // (25) |
| y_new.tail(p) = e_p; |
| z_new.head(k) = z + (Yp.leftCols(p) * f_p).reverse(); // (26) |
| z_new.tail(p) = f_p; |
| |
| const DenseVector y_save = y, z_save = z; |
| const Scalar gamma_save = gamma; |
| if (p == 1) |
| gamma = (Scalar(1) - e_p[0] * f_p[0]) * gamma; // (15) |
| else |
| gamma = rho0 + structured_tdot(c.segment(1, K), y_new); // (5) |
| y = y_new; |
| z = z_new; |
| |
| if (p == 1) { |
| lastWasBlock = false; |
| yPrev = y_save; |
| zPrev = z_save; |
| gammaPrev = gamma_save; |
| } else { |
| lastWasBlock = true; |
| kp = k; |
| pp = p; |
| Ypp = Yp.leftCols(p); |
| Zpp = Zp.leftCols(p); |
| luGammaPP = luG; |
| y_kp_1 = Yp.col(1); |
| z_kp_1 = Zp.col(1); |
| } |
| } |
| k = K; |
| } |
| |
| const RealScalar kappa = m_sMin > RealScalar(0) ? m_normEst / m_sMin : NumTraits<RealScalar>::infinity(); |
| if (!(kappa * NumTraits<RealScalar>::epsilon() < RealScalar(1))) m_info = NumericalIssue; |
| m_isInitialized = true; |
| return *this; |
| } |
| |
| } // namespace Eigen |
| |
| #endif // EIGEN_STRUCTURED_LOOK_AHEAD_LEVINSON_H |