LCOV - code coverage report
Current view: top level - src/include/panoc-alm/inner/detail - limited-memory-qr.hpp (source / functions) Hit Total Coverage
Test: ecee3ec3a495b05c61f077aa7a236b7e00601437 Lines: 104 111 93.7 %
Date: 2021-11-04 22:49:09 Functions: 22 23 95.7 %
Legend: Lines: hit not hit

          Line data    Source code
       1             : #include <Eigen/Jacobi>
       2             : #include <cstddef>
       3             : #include <panoc-alm/util/ringbuffer.hpp>
       4             : #include <panoc-alm/util/vec.hpp>
       5             : #include <type_traits>
       6             : 
       7             : namespace pa {
       8             : 
       9             : /// Incremental QR factorization using modified Gram-Schmidt with
      10             : /// reorthogonalization.
      11             : ///
      12             : /// Computes A = QR while allowing efficient removal of the first
      13             : /// column of A or adding new columns at the end of A.
      14           6 : class LimitedMemoryQR {
      15             :   public:
      16             :     LimitedMemoryQR() = default;
      17             : 
      18             :     /// @param  n
      19             :     ///         The size of the vectors, the number of rows of A.
      20             :     /// @param  m
      21             :     ///         The maximum number of columns of A.
      22             :     ///
      23             :     /// The maximum dimensions of Q are n×m and the maximum dimensions of R are
      24             :     /// m×m.
      25          12 :     LimitedMemoryQR(size_t n, size_t m) : Q(n, m), R(m, m) {}
      26             : 
      27          84 :     size_t n() const { return Q.rows(); }
      28         404 :     size_t m() const { return Q.cols(); }
      29             : 
      30             :     /// Add the given column to the right.
      31             :     template <class VecV>
      32          31 :     void add_column(const VecV &v) {
      33          31 :         assert(q_idx < m());
      34             : 
      35          31 :         auto q = Q.col(q_idx);
      36          31 :         auto r = R.col(r_idx_end);
      37             : 
      38             :         // Modified Gram-Schmidt to make q orthogonal to Q
      39          31 :         q = v;
      40          75 :         for (size_t i = 0; i < q_idx; ++i) {
      41          44 :             real_t s = Q.col(i).dot(q);
      42          44 :             r(i)     = s;
      43          44 :             q -= s * Q.col(i);
      44          44 :         }
      45             : 
      46             :         // Compute the norms of orthogonalized q and original v
      47          31 :         real_t norm_q = q.norm();
      48          31 :         real_t norm_v = v.norm();
      49             : 
      50             :         // If ‖q‖ is significantly smaller than ‖v‖, perform
      51             :         // reorthogonalization
      52          31 :         real_t η = 0.7;
      53          46 :         while (norm_q < η * norm_v) {
      54          15 :             ++reorth_count;
      55          44 :             for (size_t i = 0; i < q_idx; ++i) {
      56          29 :                 real_t s = Q.col(i).dot(q);
      57          29 :                 r(i) += s;
      58          29 :                 q -= s * Q.col(i);
      59          29 :             }
      60          15 :             norm_v = norm_q;
      61          15 :             norm_q = q.norm();
      62             :         }
      63             : 
      64             :         // Normalize q such that new matrix (Q q) remains orthogonal (i.e. has
      65             :         // orthonormal columns)
      66          31 :         r(q_idx) = norm_q;
      67          31 :         q /= norm_q;
      68             : 
      69             :         // Increment indices, add a column to Q and R.
      70          31 :         ++q_idx;
      71          31 :         r_idx_end = r_succ(r_idx_end);
      72          31 :     }
      73             : 
      74             :     /// Remove the leftmost column.
      75          15 :     void remove_column() {
      76          15 :         assert(q_idx > 0);
      77             : 
      78             :         // After removing the first colomn of the upper triangular matrix R,
      79             :         // it becomes upper Hessenberg. Givens rotations are used to make it
      80             :         // triangular again.
      81          15 :         Eigen::JacobiRotation<real_t> G;
      82          15 :         size_t r = 0;                   // row index of R
      83          15 :         size_t c = r_succ(r_idx_start); // column index of R in storage
      84          44 :         while (r < q_idx - 1) {
      85             :             // Compute the Givens rotation that makes the subdiagonal element
      86             :             // of column c or R zero.
      87          29 :             G.makeGivens(R(r, c), R(r + 1, c), &R(r, c));
      88             :             // Apply it to the remaining columns of R.
      89             :             // Not the current column, because the diagonal element was updated
      90             :             // by the makeGivens function, and the subdiagonal element doesn't
      91             :             // have to be set to zero explicitly, it's implicit.
      92             :             // Also not the previous columns, because they are already
      93             :             // implicitly zero below the diagonal and this rotation wouldn't
      94             :             // have any effect there.
      95             :             // TODO: can this be sped up by applying it in two blocks instead
      96             :             //       of column by column?
      97          43 :             for (size_t cc = r_succ(c); cc != r_idx_end; cc = r_succ(cc))
      98          14 :                 R.col(cc).applyOnTheLeft(r, r + 1, G.adjoint());
      99             :             // Apply the inverse of the Givens rotation to Q.
     100          29 :             Q.block(0, 0, Q.rows(), q_idx).applyOnTheRight(r, r + 1, G);
     101             :             // Advance indices to next diagonal element of R.
     102          29 :             ++r;
     103          29 :             c = r_succ(c);
     104             :         }
     105             :         // Remove rightmost column of Q, since it corresponds to the bottom row
     106             :         // of R, which was set to zero by the Givens rotations
     107          15 :         --q_idx;
     108             :         // Remove the first column of R.
     109          15 :         r_idx_start = r_succ(r_idx_start);
     110          15 :     }
     111             : 
     112             :     /// Solve the least squares problem Ax = b.
     113             :     template <class VecB, class VecX>
     114           8 :     void solve_col(const VecB &b, VecX &x) const {
     115             :         // Iterate over the diagonal of R, starting at the bottom right,
     116             :         // this is standard back substitution
     117             :         // (recall that R is stored in a circular buffer, so R.col(i) is
     118             :         // not the mathematical i-th column)
     119           8 :         auto rev_bgn = ring_reverse_iter().begin();
     120           8 :         auto rev_end = ring_reverse_iter().end();
     121           8 :         auto fwd_end = ring_iter().end();
     122          32 :         for (auto it_d = rev_bgn; it_d != rev_end; ++it_d) {
     123             :             // Row/column index of diagonal element of R
     124         192 :             auto [rR, cR] = *it_d;
     125             :             // (r is the zero-based mathematical index, c is the index in
     126             :             // the circular buffer)
     127          24 :             x(rR) = Q.col(rR).transpose() * b; // Compute rhs Qᵀb
     128             :             // In the current row of R, iterate over the elements to the
     129             :             // right of the diagonal
     130             :             // Iterating from left to right seems to give better results
     131          48 :             for (auto it_c = it_d.forwardit; it_c != fwd_end; ++it_c) {
     132          72 :                 auto [rX2, cR2] = *it_c;
     133          24 :                 x(rR) -= R(rR, cR2) * x(rX2);
     134          24 :             }
     135          24 :             x(rR) /= R(rR, cR); // Divide by diagonal element
     136          24 :         }
     137           8 :     }
     138             : 
     139             :     /// Solve the least squares problem AX = B.
     140             :     template <class MatB, class MatX>
     141           2 :     void solve(const MatB &B, MatX &X) const {
     142           2 :         assert(B.cols() <= X.cols());
     143           2 :         assert(B.rows() >= Q.rows());
     144           2 :         assert(X.rows() >= Eigen::Index(num_columns()));
     145             :         // Each column of the right hand side is solved as an individual system
     146           6 :         for (Eigen::Index cB = 0; cB < B.cols(); ++cB) {
     147           4 :             auto b = B.col(cB);
     148           4 :             auto x = X.col(cB);
     149           4 :             solve_col(b, x);
     150           4 :         }
     151           2 :     }
     152             : 
     153             :     template <class Derived>
     154             :     using solve_ret_t = std::conditional_t<
     155             :         Eigen::internal::traits<Derived>::ColsAtCompileTime == 1, vec, mat>;
     156             : 
     157             :     /// Solve the least squares problem AX = B.
     158             :     template <class Derived>
     159           1 :     solve_ret_t<Derived> solve(const Eigen::DenseBase<Derived> &B) {
     160           1 :         solve_ret_t<Derived> X(m(), B.cols());
     161           1 :         solve(B, X);
     162           1 :         return X;
     163           1 :     }
     164             : 
     165             :     /// Get the full, raw storage for the orthogonal matrix Q.
     166             :     const mat &get_raw_Q() const { return Q; }
     167             :     /// Get the full, raw storage for the upper triangular matrix R.
     168             :     /// The columns of this matrix are permuted because it's stored as a
     169             :     /// circular buffer for efficiently appending columns to the end and
     170             :     /// popping columns from the front.
     171             :     const mat &get_raw_R() const { return R; }
     172             : 
     173             :     /// Get the full storage for the upper triangular matrix R but with the
     174             :     /// columns in the correct order.
     175             :     /// @note   Meant for tests only, creates a permuted copy.
     176          38 :     mat get_full_R() const {
     177          38 :         if (r_idx_start == 0)
     178          16 :             return R;
     179             :         // Using a permutation matrix here isn't as efficient as rotating the
     180             :         // matrix manually, but this function is only used in tests, so it
     181             :         // shouldn't matter.
     182          22 :         Eigen::PermutationMatrix<Eigen::Dynamic> P(R.cols());
     183          22 :         P.setIdentity();
     184          44 :         std::rotate(P.indices().data(), P.indices().data() + r_idx_start,
     185          22 :                     P.indices().data() + P.size());
     186          22 :         return R * P;
     187          38 :     }
     188             :     /// Get the matrix R such that Q times R is the original matrix.
     189             :     /// @note   Meant for tests only, creates a permuted copy.
     190          38 :     mat get_R() const {
     191          38 :         return get_full_R()
     192          38 :             .block(0, 0, q_idx, q_idx)
     193          38 :             .triangularView<Eigen::Upper>();
     194           0 :     }
     195             :     /// Get the matrix Q such that Q times R is the original matrix.
     196             :     /// @note   Meant for tests only, creates a copy.
     197          84 :     mat get_Q() const { return Q.block(0, 0, n(), q_idx); }
     198             : 
     199             :     /// Multiply the matrix R by a scalar.
     200           1 :     void scale_R(real_t scal) {
     201          10 :         for (auto [i, r_idx] : ring_iter())
     202           3 :             R.col(r_idx).topRows(i + 1) *= scal;
     203           1 :     }
     204             : 
     205             :     /// Get the number of MGS reorthogonalizations.
     206             :     size_t get_reorth_count() const { return reorth_count; }
     207             :     /// Reset the number of MGS reorthogonalizations.
     208             :     void clear_reorth_count() { reorth_count = 0; }
     209             : 
     210             :     /// Reset all indices, clearing the Q and R matrices.
     211           0 :     void reset() {
     212           0 :         q_idx        = 0;
     213           0 :         r_idx_start  = 0;
     214           0 :         r_idx_end    = 0;
     215           0 :         reorth_count = 0;
     216           0 :     }
     217             : 
     218             :     /// Re-allocate storage for a problem with a different size. Causes
     219             :     /// a @ref reset.
     220             :     void resize(size_t n, size_t m) {
     221             :         Q.resize(n, m);
     222             :         R.resize(m, m);
     223             :         reset();
     224             :     }
     225             : 
     226             :     /// Get the number of columns that are currently stored.
     227          20 :     size_t num_columns() const { return q_idx; }
     228             :     /// Get the head index of the circular buffer (points to the oldest
     229             :     /// element).
     230             :     size_t ring_head() const { return r_idx_start; }
     231             :     /// Get the tail index of the circular buffer (points to one past the most
     232             :     /// recent element).
     233           8 :     size_t ring_tail() const { return r_idx_end; }
     234             :     /// Get the next index in the circular buffer.
     235             :     size_t ring_next(size_t i) const { return r_succ(i); }
     236             :     /// Get the previous index in the circular buffer.
     237             :     size_t ring_prev(size_t i) const { return r_pred(i); }
     238             : 
     239             :     /// Get iterators in the circular buffer.
     240          33 :     CircularRange<size_t> ring_iter() const {
     241          33 :         return {q_idx, r_idx_start, r_idx_end, m()};
     242             :     }
     243             :     /// Get reverse iterators in the circular buffer.
     244          16 :     ReverseCircularRange<size_t> ring_reverse_iter() const {
     245          16 :         return ring_iter();
     246             :     }
     247             : 
     248             :   private:
     249             :     mat Q; ///< Storage for orthogonal factor Q.
     250             :     mat R; ///< Storage for upper triangular factor R.
     251             : 
     252           6 :     size_t q_idx       = 0; ///< Number of columns of Q being stored.
     253           6 :     size_t r_idx_start = 0; ///< Index of the first column of R.
     254           6 :     size_t r_idx_end   = 0; ///< Index of the one-past-last column of R.
     255             : 
     256           6 :     size_t reorth_count = 0; ///< Number of MGS reorthogonalizations.
     257             : 
     258             :     /// Get the next index in the circular storage for R.
     259         266 :     size_t r_succ(size_t i) const { return i + 1 < m() ? i + 1 : 0; }
     260             :     /// Get the previous index in the circular storage for R.
     261             :     size_t r_pred(size_t i) const { return i == 0 ? m() - 1 : i - 1; }
     262             : };
     263             : 
     264             : } // namespace pa

Generated by: LCOV version 1.15