Skip to main content

tensor4all_tensorbackend/
incremental_qr.rs

1//! Backend-native incremental QR factorization for column-major dense matrices.
2//!
3//! The block update is independently derived from Appendix C.3 of
4//! Camaño--Epperly--Tropp,
5//! [arXiv:2504.06475](https://arxiv.org/abs/2504.06475). For an existing
6//! factorization `Y = Q R` and appended columns `Y'`, it computes two
7//! block Gram--Schmidt projection passes followed by a backend QR of the
8//! residual. The second pass limits loss of orthogonality without introducing
9//! scalar reflector kernels.
10
11use anyhow::anyhow;
12use num_complex::{Complex64, ComplexFloat};
13
14use crate::backend::{
15    qr_backend, src_error_estimate, src_error_estimate_from_inverse_adjoint, src_inverse_adjoint,
16    BackendLinalgError, BackendLinalgScalar, MatrixTriangularSolveScalar,
17};
18use crate::matrix::{mat_mul, Matrix, MatrixScalar};
19
20/// Scalar operations required by [`IncrementalQr`].
21///
22/// The implementation is provided for the two scalar types supported by the
23/// backend, `f64` and `Complex64`. The conjugation hook keeps the update
24/// correct for complex matrices while using the same column-major algorithm
25/// for real matrices.
26///
27/// # Examples
28///
29/// ```
30/// use tensor4all_tensorbackend::IncrementalQrScalar;
31/// assert_eq!(<f64 as IncrementalQrScalar>::conjugate(2.0), 2.0);
32/// ```
33pub trait IncrementalQrScalar:
34    BackendLinalgScalar + MatrixScalar + MatrixTriangularSolveScalar + ComplexFloat
35{
36    /// Return the Hermitian conjugate of one scalar.
37    fn conjugate(self) -> Self;
38
39    /// Convert a non-negative real norm into this scalar type.
40    fn from_real(value: f64) -> Self;
41}
42
43impl IncrementalQrScalar for f64 {
44    fn conjugate(self) -> Self {
45        self
46    }
47
48    fn from_real(value: f64) -> Self {
49        value
50    }
51}
52
53impl IncrementalQrScalar for Complex64 {
54    fn conjugate(self) -> Self {
55        self.conj()
56    }
57
58    fn from_real(value: f64) -> Self {
59        Self::new(value, 0.0)
60    }
61}
62
63/// Thin QR state that can append columns without refactorizing the old block.
64///
65/// The state stores an explicit thin `Q` factor and an upper-trapezoidal
66/// `R` factor. Appending a full-rank block uses two backend matrix-product
67/// projection passes, factorizes only the residual block through the configured
68/// QR backend, and updates the block-triangular `R`.
69///
70/// The matrix layout is column-major throughout. The current state must have
71/// at least as many rows as columns, and appends are accepted only while the
72/// resulting factorization remains thin.
73///
74/// # Examples
75///
76/// ```
77/// use tensor4all_tensorbackend::{IncrementalQr, Matrix, mat_mul};
78///
79/// let first = Matrix::from_col_major_vec(3, 1, vec![1.0_f64, 2.0, 3.0]);
80/// let appended = Matrix::from_col_major_vec(3, 1, vec![2.0, 0.0, 1.0]);
81/// let mut qr = IncrementalQr::new(first).unwrap();
82/// qr.append(&appended).unwrap();
83/// let reconstructed = mat_mul(&qr.q(), &qr.r()).unwrap();
84/// assert!(reconstructed
85///     .as_col_major_slice()
86///     .iter()
87///     .zip([1.0, 2.0, 3.0, 2.0, 0.0, 1.0])
88///     .all(|(actual, expected)| (actual - expected).abs() < 1.0e-12));
89/// ```
90#[derive(Debug, Clone)]
91pub struct IncrementalQr<T> {
92    q: Matrix<T>,
93    r: Matrix<T>,
94    /// `R^{-†}` for the current square full-rank QR block. `None` denotes a
95    /// rank-deficient or rectangular state for which the Appendix C estimate
96    /// is not defined.
97    inverse_adjoint: Option<Matrix<T>>,
98}
99
100impl<T> IncrementalQr<T>
101where
102    T: IncrementalQrScalar,
103{
104    /// Resume an incremental QR update from compatible thin factors.
105    ///
106    /// # Arguments
107    /// * `q` - Existing column-major `m × p` thin factor.
108    /// * `r` - Existing column-major `p × n` upper-trapezoidal factor, where
109    ///   `n >= p`.
110    ///
111    /// # Returns
112    /// An update state whose next append extends the represented factorization.
113    ///
114    /// # Errors
115    /// Returns a backend error when the factors are empty, have incompatible
116    /// dimensions, are not thin, or backend QR/multiplication fails.
117    ///
118    /// # Examples
119    ///
120    /// ```
121    /// use tensor4all_tensorbackend::{IncrementalQr, Matrix};
122    ///
123    /// let state = IncrementalQr::from_factors(
124    ///     Matrix::from_col_major_vec(2, 1, vec![1.0_f64, 0.0]),
125    ///     Matrix::from_col_major_vec(1, 1, vec![2.0]),
126    /// )
127    /// .unwrap();
128    /// assert_eq!(state.q().ncols(), 1);
129    /// assert_eq!(state.r().nrows(), 1);
130    /// ```
131    pub fn from_factors(
132        q: Matrix<T>,
133        r: Matrix<T>,
134    ) -> std::result::Result<Self, BackendLinalgError> {
135        if q.nrows() == 0 || q.ncols() == 0 {
136            return Err(anyhow!("incremental QR factors must be non-empty").into());
137        }
138        if q.nrows() < q.ncols() {
139            return Err(anyhow!(
140                "incremental QR factors must be thin, got Q {}x{}",
141                q.nrows(),
142                q.ncols()
143            )
144            .into());
145        }
146        if r.nrows() != q.ncols() || r.ncols() < q.ncols() {
147            return Err(anyhow!(
148                "incremental QR factor dimensions are incompatible: Q {}x{}, R {}x{}",
149                q.nrows(),
150                q.ncols(),
151                r.nrows(),
152                r.ncols()
153            )
154            .into());
155        }
156
157        let (q, q_r) = factorize_backend(q)?;
158        let r = mat_mul(&q_r, &r)
159            .map_err(|error| anyhow!("incremental QR factor conversion failed: {error}"))?;
160        let inverse_adjoint = try_inverse_adjoint(&r);
161        Ok(Self {
162            q,
163            r,
164            inverse_adjoint,
165        })
166    }
167
168    /// Factorize a non-empty tall-or-square matrix into thin `Q` and square `R`.
169    ///
170    /// # Arguments
171    /// * `input` - Column-major `m × n` matrix with `m >= n` and `n > 0`.
172    ///
173    /// # Returns
174    /// A state containing factors satisfying `input = Q R` up to backend
175    /// floating-point error.
176    ///
177    /// # Errors
178    /// Returns a backend error when the input dimensions are invalid because
179    /// the matrix is empty or wide, or when backend QR conversion or
180    /// factorization fails.
181    ///
182    /// # Examples
183    ///
184    /// ```
185    /// use tensor4all_tensorbackend::{IncrementalQr, Matrix};
186    ///
187    /// let qr = IncrementalQr::new(Matrix::from_col_major_vec(
188    ///     2,
189    ///     1,
190    ///     vec![1.0_f64, 2.0],
191    /// ))
192    /// .unwrap();
193    /// assert_eq!(qr.q().nrows(), 2);
194    /// assert_eq!(qr.r().ncols(), 1);
195    /// ```
196    pub fn new(input: Matrix<T>) -> std::result::Result<Self, BackendLinalgError> {
197        if input.nrows() == 0 || input.ncols() == 0 {
198            return Err(anyhow!("incremental QR requires a non-empty matrix").into());
199        }
200        if input.nrows() < input.ncols() {
201            return Err(anyhow!(
202                "incremental QR requires a tall-or-square matrix, got {}x{}",
203                input.nrows(),
204                input.ncols()
205            )
206            .into());
207        }
208
209        let (q, r) = factorize_backend(input)?;
210        let inverse_adjoint = try_inverse_adjoint(&r);
211        Ok(Self {
212            q,
213            r,
214            inverse_adjoint,
215        })
216    }
217
218    /// Append a column block using the existing QR state.
219    ///
220    /// # Arguments
221    /// * `new_columns` - Column-major `m × k` block with the same row count as
222    ///   the initial matrix and `k > 0`.
223    ///
224    /// # Returns
225    /// Updates this state in place so that `Q R` represents the original
226    /// matrix followed by `new_columns`.
227    ///
228    /// # Errors
229    /// Returns a backend error when the input dimensions are invalid because
230    /// row counts differ, the append is empty, or the resulting matrix would
231    /// be wide; when a rank or column count overflows; or when backend matrix
232    /// multiplication, conversion, or QR factorization fails.
233    ///
234    /// # Examples
235    ///
236    /// ```
237    /// use tensor4all_tensorbackend::{IncrementalQr, Matrix};
238    ///
239    /// let mut qr = IncrementalQr::new(Matrix::from_col_major_vec(
240    ///     3,
241    ///     1,
242    ///     vec![1.0_f64, 2.0, 3.0],
243    /// ))
244    /// .unwrap();
245    /// qr.append(&Matrix::from_col_major_vec(
246    ///     3,
247    ///     1,
248    ///     vec![3.0_f64, 2.0, 1.0],
249    /// ))
250    /// .unwrap();
251    /// assert_eq!(qr.r().ncols(), 2);
252    /// ```
253    pub fn append(
254        &mut self,
255        new_columns: &Matrix<T>,
256    ) -> std::result::Result<(), BackendLinalgError> {
257        if new_columns.nrows() != self.q.nrows() {
258            return Err(anyhow!(
259                "incremental QR append row count {} does not match {}",
260                new_columns.nrows(),
261                self.q.nrows()
262            )
263            .into());
264        }
265        if new_columns.ncols() == 0 {
266            return Err(anyhow!("incremental QR append requires at least one column").into());
267        }
268        let maximum_new_rank = self
269            .q
270            .ncols()
271            .checked_add(new_columns.ncols())
272            .ok_or_else(|| anyhow!("incremental QR rank overflow"))?;
273        if maximum_new_rank > self.q.nrows() {
274            return Err(anyhow!(
275                "incremental QR append would produce a wide factorization: {} rows, {} columns",
276                self.q.nrows(),
277                maximum_new_rank
278            )
279            .into());
280        }
281
282        let new_columns_norm = frobenius_norm(new_columns)?;
283        let residual_tolerance = 32.0
284            * f64::EPSILON
285            * (self.q.nrows().max(new_columns.ncols()) as f64)
286            * new_columns_norm.max(1.0);
287
288        let (projection, residual) = project_twice(&self.q, new_columns)?;
289        let (appended_q, appended_r) = factorize_backend(residual)?;
290        if diagonal_is_full_rank(&appended_r, residual_tolerance) {
291            return self.commit_full_rank_block(projection, appended_q, appended_r);
292        }
293
294        for column in 0..new_columns.ncols() {
295            let column = matrix_column(new_columns, column)?;
296            let (projection, residual) = project_twice(&self.q, &column)?;
297            let residual_norm = frobenius_norm(&residual)?;
298            if residual_norm <= residual_tolerance {
299                self.commit_dependent_column(projection)?;
300                continue;
301            }
302            let (appended_q, appended_r) = factorize_backend(residual)?;
303            self.commit_full_rank_block(projection, appended_q, appended_r)?;
304        }
305        Ok(())
306    }
307
308    fn commit_full_rank_block(
309        &mut self,
310        projection: Matrix<T>,
311        appended_q: Matrix<T>,
312        appended_r: Matrix<T>,
313    ) -> std::result::Result<(), BackendLinalgError> {
314        let old_rank = self.q.ncols();
315        let old_column_count = self.r.ncols();
316        let appended_rank = appended_q.ncols();
317        if projection.nrows() != old_rank
318            || projection.ncols() != appended_rank
319            || appended_q.nrows() != self.q.nrows()
320            || appended_r.nrows() != appended_rank
321            || appended_r.ncols() != appended_rank
322        {
323            return Err(anyhow!(
324                "incremental QR backend update returned incompatible blocks: projection {}x{}, Q' {}x{}, R'' {}x{}",
325                projection.nrows(),
326                projection.ncols(),
327                appended_q.nrows(),
328                appended_q.ncols(),
329                appended_r.nrows(),
330                appended_r.ncols()
331            )
332            .into());
333        }
334
335        let r = assemble_r(&self.r, &projection, &appended_r)?;
336        let new_rank = old_rank
337            .checked_add(appended_rank)
338            .ok_or_else(|| anyhow!("incremental QR rank overflow"))?;
339        let new_column_count = r.ncols();
340        let inverse_adjoint = if new_rank == new_column_count {
341            if old_rank == old_column_count {
342                if let Some(previous) = self.inverse_adjoint.as_ref() {
343                    Some(update_inverse_adjoint(previous, &projection, &appended_r)?)
344                } else {
345                    try_inverse_adjoint(&r)
346                }
347            } else {
348                try_inverse_adjoint(&r)
349            }
350        } else {
351            None
352        };
353
354        self.q
355            .append_columns(&appended_q)
356            .map_err(|error| anyhow!("incremental QR Q append failed: {error}"))?;
357        self.r = r;
358        self.inverse_adjoint = inverse_adjoint;
359        Ok(())
360    }
361
362    fn commit_dependent_column(
363        &mut self,
364        projection: Matrix<T>,
365    ) -> std::result::Result<(), BackendLinalgError> {
366        if projection.nrows() != self.q.ncols() || projection.ncols() != 1 {
367            return Err(anyhow!(
368                "incremental QR dependent-column projection has shape {}x{}, expected {}x1",
369                projection.nrows(),
370                projection.ncols(),
371                self.q.ncols()
372            )
373            .into());
374        }
375        let new_column_count = self
376            .r
377            .ncols()
378            .checked_add(1)
379            .ok_or_else(|| anyhow!("incremental QR column count overflow"))?;
380        let mut r = Matrix::try_zeros(self.r.nrows(), new_column_count)
381            .map_err(|error| anyhow!("incremental QR R allocation failed: {error}"))?;
382        for column in 0..self.r.ncols() {
383            for row in 0..self.r.nrows() {
384                r[[row, column]] = self.r[[row, column]];
385            }
386        }
387        for row in 0..self.r.nrows() {
388            r[[row, self.r.ncols()]] = projection[[row, 0]];
389        }
390        self.r = r;
391        self.inverse_adjoint = None;
392        Ok(())
393    }
394
395    /// Return a copy of the current thin `Q` factor.
396    ///
397    /// # Examples
398    ///
399    /// ```
400    /// use tensor4all_tensorbackend::{IncrementalQr, Matrix};
401    ///
402    /// let qr = IncrementalQr::new(Matrix::from_col_major_vec(
403    ///     2,
404    ///     1,
405    ///     vec![1.0_f64, 0.0],
406    /// ))
407    /// .unwrap();
408    /// assert_eq!(qr.q().ncols(), 1);
409    /// ```
410    pub fn q(&self) -> Matrix<T> {
411        self.q.clone()
412    }
413
414    /// Return the current thin factor width.
415    ///
416    /// This is the number of columns in both `Q` and the row count of `R`.
417    ///
418    /// # Examples
419    ///
420    /// ```
421    /// use tensor4all_tensorbackend::{IncrementalQr, Matrix};
422    ///
423    /// let qr = IncrementalQr::new(Matrix::from_col_major_vec(
424    ///     2,
425    ///     1,
426    ///     vec![1.0_f64, 0.0],
427    /// ))
428    /// .unwrap();
429    /// assert_eq!(qr.rank(), 1);
430    /// ```
431    pub fn rank(&self) -> usize {
432        self.q.ncols()
433    }
434
435    /// Return a contiguous range of columns from the current thin `Q` factor.
436    ///
437    /// # Arguments
438    /// * `start` - Zero-based column in the current `Q` factor.
439    /// * `count` - Number of columns to materialize.
440    ///
441    /// # Returns
442    /// The requested column-major `m × count` block of `Q`.
443    ///
444    /// # Errors
445    /// Returns a backend error when the requested range overflows or is out of
446    /// bounds for the current thin-factor width, or when the output shape is
447    /// invalid because its element count overflows.
448    ///
449    /// # Examples
450    ///
451    /// ```
452    /// use tensor4all_tensorbackend::{IncrementalQr, Matrix};
453    ///
454    /// let qr = IncrementalQr::new(Matrix::from_col_major_vec(
455    ///     3,
456    ///     2,
457    ///     vec![1.0_f64, 0.0, 0.0, 0.0, 1.0, 0.0],
458    /// ))
459    /// .unwrap();
460    /// let second = qr.q_columns(1, 1).unwrap();
461    /// assert_eq!(second.nrows(), 3);
462    /// assert_eq!(second.ncols(), 1);
463    /// assert!((second[[1, 0]].abs() - 1.0).abs() < 1.0e-12);
464    /// ```
465    pub fn q_columns(
466        &self,
467        start: usize,
468        count: usize,
469    ) -> std::result::Result<Matrix<T>, BackendLinalgError> {
470        let end = start
471            .checked_add(count)
472            .ok_or_else(|| anyhow!("incremental QR Q-column range overflows usize"))?;
473        if end > self.q.ncols() {
474            return Err(anyhow!(
475                "incremental QR Q-column range {start}..{end} exceeds width {}",
476                self.q.ncols()
477            )
478            .into());
479        }
480        let mut q = Matrix::try_zeros(self.q.nrows(), count)
481            .map_err(|error| anyhow!("incremental QR Q-column allocation failed: {error}"))?;
482        for column in 0..count {
483            for row in 0..self.q.nrows() {
484                q[[row, column]] = self.q[[row, start + column]];
485            }
486        }
487        Ok(q)
488    }
489
490    /// Return a copy of the current upper-trapezoidal `R` factor.
491    ///
492    /// # Examples
493    ///
494    /// ```
495    /// use tensor4all_tensorbackend::{IncrementalQr, Matrix};
496    ///
497    /// let qr = IncrementalQr::new(Matrix::from_col_major_vec(
498    ///     2,
499    ///     1,
500    ///     vec![1.0_f64, 0.0],
501    /// ))
502    /// .unwrap();
503    /// assert_eq!(qr.r().nrows(), 1);
504    /// ```
505    pub fn r(&self) -> Matrix<T> {
506        self.r.clone()
507    }
508
509    /// Compute the Appendix C SRC estimate from the current `R` factor.
510    ///
511    /// # Returns
512    /// The randomized residual and norm estimates associated with the current
513    /// sketch width.
514    ///
515    /// # Errors
516    /// Returns a backend error when the current factor is singular or contains
517    /// invalid values.
518    ///
519    /// # Examples
520    ///
521    /// ```
522    /// use tensor4all_tensorbackend::{IncrementalQr, Matrix};
523    ///
524    /// let qr = IncrementalQr::new(Matrix::from_col_major_vec(
525    ///     2,
526    ///     1,
527    ///     vec![1.0_f64, 0.0],
528    /// ))
529    /// .unwrap();
530    /// let estimate = qr.error_estimate().unwrap();
531    /// assert!(estimate.error.is_finite());
532    /// assert!(estimate.norm.is_finite());
533    /// ```
534    pub fn error_estimate(
535        &self,
536    ) -> std::result::Result<crate::SrcErrorEstimate, BackendLinalgError> {
537        if let Some(inverse_adjoint) = self.inverse_adjoint.as_ref() {
538            src_error_estimate_from_inverse_adjoint(&self.r, inverse_adjoint)
539        } else {
540            src_error_estimate(&self.r)
541        }
542    }
543}
544
545fn factorize_backend<T>(
546    input: Matrix<T>,
547) -> std::result::Result<(Matrix<T>, Matrix<T>), BackendLinalgError>
548where
549    T: IncrementalQrScalar,
550{
551    let (q, r) = qr_backend(input.into_typed_tensor())?;
552    let q = Matrix::try_from_typed_tensor(q)
553        .map_err(|error| anyhow!("incremental QR backend Q conversion failed: {error}"))?;
554    let r = Matrix::try_from_typed_tensor(r)
555        .map_err(|error| anyhow!("incremental QR backend R conversion failed: {error}"))?;
556    Ok((q, r))
557}
558
559fn project_twice<T>(
560    q: &Matrix<T>,
561    columns: &Matrix<T>,
562) -> std::result::Result<(Matrix<T>, Matrix<T>), BackendLinalgError>
563where
564    T: IncrementalQrScalar,
565{
566    let q_adjoint = matrix_adjoint(q)?;
567    let first_projection = mat_mul(&q_adjoint, columns)
568        .map_err(|error| anyhow!("incremental QR first projection failed: {error}"))?;
569    let first_reconstruction = mat_mul(q, &first_projection)
570        .map_err(|error| anyhow!("incremental QR first reconstruction failed: {error}"))?;
571    let first_residual = matrix_subtract(columns, &first_reconstruction)?;
572
573    let correction = mat_mul(&q_adjoint, &first_residual)
574        .map_err(|error| anyhow!("incremental QR reorthogonalization failed: {error}"))?;
575    let correction_reconstruction = mat_mul(q, &correction).map_err(|error| {
576        anyhow!("incremental QR reorthogonalization reconstruction failed: {error}")
577    })?;
578    let residual = matrix_subtract(&first_residual, &correction_reconstruction)?;
579    let projection = matrix_add(&first_projection, &correction)?;
580    Ok((projection, residual))
581}
582
583fn matrix_adjoint<T>(matrix: &Matrix<T>) -> std::result::Result<Matrix<T>, BackendLinalgError>
584where
585    T: IncrementalQrScalar,
586{
587    let mut adjoint = Matrix::try_zeros(matrix.ncols(), matrix.nrows())
588        .map_err(|error| anyhow!("incremental QR adjoint allocation failed: {error}"))?;
589    for column in 0..matrix.ncols() {
590        for row in 0..matrix.nrows() {
591            adjoint[[column, row]] = matrix[[row, column]].conjugate();
592        }
593    }
594    Ok(adjoint)
595}
596
597fn matrix_add<T>(
598    left: &Matrix<T>,
599    right: &Matrix<T>,
600) -> std::result::Result<Matrix<T>, BackendLinalgError>
601where
602    T: IncrementalQrScalar,
603{
604    ensure_same_shape("addition", left, right)?;
605    let values = left
606        .as_col_major_slice()
607        .iter()
608        .zip(right.as_col_major_slice())
609        .map(|(left, right)| *left + *right)
610        .collect();
611    Matrix::try_from_col_major_vec(left.nrows(), left.ncols(), values)
612        .map_err(|error| anyhow!("incremental QR addition result is invalid: {error}").into())
613}
614
615fn matrix_subtract<T>(
616    left: &Matrix<T>,
617    right: &Matrix<T>,
618) -> std::result::Result<Matrix<T>, BackendLinalgError>
619where
620    T: IncrementalQrScalar,
621{
622    ensure_same_shape("subtraction", left, right)?;
623    let values = left
624        .as_col_major_slice()
625        .iter()
626        .zip(right.as_col_major_slice())
627        .map(|(left, right)| *left - *right)
628        .collect();
629    Matrix::try_from_col_major_vec(left.nrows(), left.ncols(), values)
630        .map_err(|error| anyhow!("incremental QR subtraction result is invalid: {error}").into())
631}
632
633fn ensure_same_shape<T>(
634    operation: &str,
635    left: &Matrix<T>,
636    right: &Matrix<T>,
637) -> std::result::Result<(), BackendLinalgError> {
638    if left.nrows() != right.nrows() || left.ncols() != right.ncols() {
639        return Err(anyhow!(
640            "incremental QR {operation} shape mismatch: {}x{} and {}x{}",
641            left.nrows(),
642            left.ncols(),
643            right.nrows(),
644            right.ncols()
645        )
646        .into());
647    }
648    Ok(())
649}
650
651fn matrix_column<T>(
652    matrix: &Matrix<T>,
653    column: usize,
654) -> std::result::Result<Matrix<T>, BackendLinalgError>
655where
656    T: IncrementalQrScalar,
657{
658    if column >= matrix.ncols() {
659        return Err(anyhow!(
660            "incremental QR column {column} exceeds width {}",
661            matrix.ncols()
662        )
663        .into());
664    }
665    let start = column
666        .checked_mul(matrix.nrows())
667        .ok_or_else(|| anyhow!("incremental QR column offset overflow"))?;
668    let end = start
669        .checked_add(matrix.nrows())
670        .ok_or_else(|| anyhow!("incremental QR column range overflow"))?;
671    Matrix::try_from_col_major_vec(
672        matrix.nrows(),
673        1,
674        matrix.as_col_major_slice()[start..end].to_vec(),
675    )
676    .map_err(|error| anyhow!("incremental QR column extraction failed: {error}").into())
677}
678
679fn frobenius_norm<T>(matrix: &Matrix<T>) -> std::result::Result<f64, BackendLinalgError>
680where
681    T: IncrementalQrScalar,
682{
683    let norm = matrix
684        .as_col_major_slice()
685        .iter()
686        .map(|value| value.matrix_abs_sq())
687        .sum::<f64>()
688        .sqrt();
689    if !norm.is_finite() {
690        return Err(anyhow!("incremental QR produced a non-finite residual norm").into());
691    }
692    Ok(norm)
693}
694
695fn diagonal_is_full_rank<T>(r: &Matrix<T>, tolerance: f64) -> bool
696where
697    T: IncrementalQrScalar,
698{
699    r.nrows() == r.ncols()
700        && (0..r.ncols()).all(|diagonal| {
701            let magnitude = r[[diagonal, diagonal]].matrix_abs_sq().sqrt();
702            magnitude.is_finite() && magnitude > tolerance
703        })
704}
705
706fn assemble_r<T>(
707    old: &Matrix<T>,
708    projection: &Matrix<T>,
709    residual_r: &Matrix<T>,
710) -> std::result::Result<Matrix<T>, BackendLinalgError>
711where
712    T: IncrementalQrScalar,
713{
714    if projection.nrows() != old.nrows()
715        || residual_r.nrows() != residual_r.ncols()
716        || projection.ncols() != residual_r.ncols()
717    {
718        return Err(anyhow!(
719            "incremental QR R blocks are incompatible: R {}x{}, projection {}x{}, residual R {}x{}",
720            old.nrows(),
721            old.ncols(),
722            projection.nrows(),
723            projection.ncols(),
724            residual_r.nrows(),
725            residual_r.ncols()
726        )
727        .into());
728    }
729    let new_rows = old
730        .nrows()
731        .checked_add(residual_r.nrows())
732        .ok_or_else(|| anyhow!("incremental QR R row count overflow"))?;
733    let new_columns = old
734        .ncols()
735        .checked_add(residual_r.ncols())
736        .ok_or_else(|| anyhow!("incremental QR R column count overflow"))?;
737    let mut result = Matrix::try_zeros(new_rows, new_columns)
738        .map_err(|error| anyhow!("incremental QR R allocation failed: {error}"))?;
739
740    for column in 0..old.ncols() {
741        for row in 0..old.nrows() {
742            result[[row, column]] = old[[row, column]];
743        }
744    }
745    for column in 0..projection.ncols() {
746        let target_column = old.ncols() + column;
747        for row in 0..projection.nrows() {
748            result[[row, target_column]] = projection[[row, column]];
749        }
750        for row in 0..residual_r.nrows() {
751            result[[old.nrows() + row, target_column]] = residual_r[[row, column]];
752        }
753    }
754    Ok(result)
755}
756
757fn try_inverse_adjoint<T>(r: &Matrix<T>) -> Option<Matrix<T>>
758where
759    T: IncrementalQrScalar,
760{
761    src_inverse_adjoint(r).ok()
762}
763
764fn update_inverse_adjoint<T>(
765    previous: &Matrix<T>,
766    projection: &Matrix<T>,
767    residual_r: &Matrix<T>,
768) -> std::result::Result<Matrix<T>, BackendLinalgError>
769where
770    T: IncrementalQrScalar,
771{
772    let old_rank = previous.nrows();
773    let appended_rank = residual_r.nrows();
774    if previous.ncols() != old_rank
775        || projection.nrows() != old_rank
776        || projection.ncols() != appended_rank
777        || residual_r.ncols() != appended_rank
778    {
779        return Err(anyhow!(
780            "incremental QR inverse-adjoint blocks are incompatible: G {}x{}, projection {}x{}, R'' {}x{}",
781            previous.nrows(),
782            previous.ncols(),
783            projection.nrows(),
784            projection.ncols(),
785            residual_r.nrows(),
786            residual_r.ncols()
787        )
788        .into());
789    }
790
791    let projection_adjoint = matrix_adjoint(projection)?;
792    let residual_inverse_adjoint = src_inverse_adjoint(residual_r)?;
793    let coupling = mat_mul(&projection_adjoint, previous)
794        .map_err(|error| anyhow!("incremental QR inverse-adjoint coupling failed: {error}"))?;
795    let lower = mat_mul(&residual_inverse_adjoint, &coupling)
796        .map_err(|error| anyhow!("incremental QR inverse-adjoint update failed: {error}"))?;
797
798    let new_rank = old_rank
799        .checked_add(appended_rank)
800        .ok_or_else(|| anyhow!("incremental QR inverse-adjoint rank overflow"))?;
801    let mut updated = Matrix::try_zeros(new_rank, new_rank)
802        .map_err(|error| anyhow!("incremental QR inverse-adjoint allocation failed: {error}"))?;
803    for column in 0..old_rank {
804        for row in 0..old_rank {
805            updated[[row, column]] = previous[[row, column]];
806        }
807    }
808    for column in 0..appended_rank {
809        for row in 0..appended_rank {
810            updated[[old_rank + row, old_rank + column]] = residual_inverse_adjoint[[row, column]];
811        }
812        for row in 0..old_rank {
813            updated[[old_rank + column, row]] = -lower[[column, row]];
814        }
815    }
816    Ok(updated)
817}
818
819#[cfg(test)]
820mod tests;