1use 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
20pub trait IncrementalQrScalar:
34 BackendLinalgScalar + MatrixScalar + MatrixTriangularSolveScalar + ComplexFloat
35{
36 fn conjugate(self) -> Self;
38
39 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#[derive(Debug, Clone)]
91pub struct IncrementalQr<T> {
92 q: Matrix<T>,
93 r: Matrix<T>,
94 inverse_adjoint: Option<Matrix<T>>,
98}
99
100impl<T> IncrementalQr<T>
101where
102 T: IncrementalQrScalar,
103{
104 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 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 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 pub fn q(&self) -> Matrix<T> {
411 self.q.clone()
412 }
413
414 pub fn rank(&self) -> usize {
432 self.q.ncols()
433 }
434
435 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 pub fn r(&self) -> Matrix<T> {
506 self.r.clone()
507 }
508
509 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;