tensor4all_core/matrixlu.rs
1//! Rank-Revealing LU decomposition (rrLU) implementation.
2//!
3//! Provides [`RrLU`], a full-pivoting LU decomposition that reveals the
4//! numerical rank of a matrix. The decomposition is:
5//!
6//! ```text
7//! P_row * A * P_col = L * U
8//! ```
9//!
10//! where `P_row`, `P_col` are permutation matrices. The rank is determined
11//! by the number of pivots exceeding the tolerance thresholds in
12//! [`RrLUOptions`].
13//!
14//! # Examples
15//!
16//! ```
17//! use tensor4all_core::matrixlu::rrlu;
18//! use tensor4all_tensorbackend::from_vec2d;
19//!
20//! let m = from_vec2d(vec![
21//! vec![1.0_f64, 2.0],
22//! vec![3.0, 4.0],
23//! ]);
24//! let lu = rrlu(&m, None).unwrap();
25//! assert_eq!(lu.npivots(), 2);
26//! ```
27
28use crate::error::{MatrixCIError, Result};
29use crate::scalar::Scalar;
30use tensor4all_tensorbackend::{transpose, Matrix};
31
32/// Rank-Revealing LU decomposition.
33///
34/// Represents a matrix `A` as `P_row * A * P_col = L * U`, where `P_row`
35/// and `P_col` are permutation matrices, `L` is lower-triangular, and `U`
36/// is upper-triangular. One of `L` or `U` has unit diagonal, controlled by
37/// the `left_orthogonal` option.
38///
39/// # Examples
40///
41/// ```
42/// use tensor4all_core::matrixlu::rrlu;
43/// use tensor4all_tensorbackend::{from_vec2d, mat_mul};
44///
45/// let m = from_vec2d(vec![
46/// vec![1.0_f64, 2.0, 3.0],
47/// vec![4.0, 5.0, 6.0],
48/// vec![7.0, 8.0, 10.0],
49/// ]);
50///
51/// let lu = rrlu(&m, None).unwrap();
52/// assert_eq!(lu.npivots(), 3);
53///
54/// // Verify L * U reconstructs the permuted matrix
55/// let l = lu.left(false);
56/// let u = lu.right(false);
57/// let reconstructed = mat_mul(&l, &u).unwrap();
58///
59/// // Check reconstruction matches the permuted matrix
60/// for i in 0..3 {
61/// for j in 0..3 {
62/// let orig_row = lu.row_permutation()[i];
63/// let orig_col = lu.col_permutation()[j];
64/// assert!((reconstructed[[i, j]] - m[[orig_row, orig_col]]).abs() < 1e-10);
65/// }
66/// }
67/// ```
68#[derive(Debug, Clone)]
69pub struct RrLU<T: Scalar> {
70 /// Row permutation
71 row_permutation: Vec<usize>,
72 /// Column permutation
73 col_permutation: Vec<usize>,
74 /// Lower triangular matrix L
75 l: Matrix<T>,
76 /// Upper triangular matrix U
77 u: Matrix<T>,
78 /// Whether L is left-orthogonal (L has 1s on diagonal) or U is (U has 1s on diagonal)
79 left_orthogonal: bool,
80 /// Number of pivots
81 n_pivot: usize,
82 /// Last pivot error
83 error: f64,
84}
85
86impl<T: Scalar> RrLU<T> {
87 /// Create an empty rrLU for a matrix of given size.
88 ///
89 /// Used internally. Most users should call [`rrlu`] or [`rrlu_mut`]
90 /// instead.
91 ///
92 /// # Examples
93 ///
94 /// ```
95 /// use tensor4all_core::RrLU;
96 ///
97 /// let lu = RrLU::<f64>::new(3, 4, true);
98 /// assert_eq!(lu.nrows(), 3);
99 /// assert_eq!(lu.ncols(), 4);
100 /// assert_eq!(lu.npivots(), 0);
101 /// assert!(lu.is_left_orthogonal());
102 /// ```
103 pub fn new(nr: usize, nc: usize, left_orthogonal: bool) -> Self {
104 Self {
105 row_permutation: (0..nr).collect(),
106 col_permutation: (0..nc).collect(),
107 l: Matrix::zeros(nr, 0),
108 u: Matrix::zeros(0, nc),
109 left_orthogonal,
110 n_pivot: 0,
111 error: f64::NAN,
112 }
113 }
114
115 /// Number of rows
116 ///
117 /// # Examples
118 ///
119 /// ```
120 /// use tensor4all_core::matrixlu::rrlu;
121 /// use tensor4all_tensorbackend::from_vec2d;
122 ///
123 /// let m = from_vec2d(vec![vec![1.0_f64, 2.0], vec![3.0, 4.0], vec![5.0, 6.0]]);
124 /// let lu = rrlu(&m, None).unwrap();
125 /// assert_eq!(lu.nrows(), 3);
126 /// ```
127 pub fn nrows(&self) -> usize {
128 self.l.nrows()
129 }
130
131 /// Number of columns
132 ///
133 /// # Examples
134 ///
135 /// ```
136 /// use tensor4all_core::matrixlu::rrlu;
137 /// use tensor4all_tensorbackend::from_vec2d;
138 ///
139 /// let m = from_vec2d(vec![vec![1.0_f64, 2.0, 3.0], vec![4.0, 5.0, 6.0]]);
140 /// let lu = rrlu(&m, None).unwrap();
141 /// assert_eq!(lu.ncols(), 3);
142 /// ```
143 pub fn ncols(&self) -> usize {
144 self.u.ncols()
145 }
146
147 /// Number of pivots
148 ///
149 /// # Examples
150 ///
151 /// ```
152 /// use tensor4all_core::matrixlu::rrlu;
153 /// use tensor4all_tensorbackend::from_vec2d;
154 ///
155 /// let m = from_vec2d(vec![vec![1.0_f64, 2.0], vec![3.0, 4.0]]);
156 /// let lu = rrlu(&m, None).unwrap();
157 /// assert_eq!(lu.npivots(), 2);
158 /// ```
159 pub fn npivots(&self) -> usize {
160 self.n_pivot
161 }
162
163 /// Row permutation
164 ///
165 /// # Examples
166 ///
167 /// ```
168 /// use tensor4all_core::matrixlu::rrlu;
169 /// use tensor4all_tensorbackend::from_vec2d;
170 ///
171 /// let m = from_vec2d(vec![vec![1.0_f64, 2.0], vec![3.0, 4.0]]);
172 /// let lu = rrlu(&m, None).unwrap();
173 /// let perm = lu.row_permutation();
174 /// assert_eq!(perm.len(), 2);
175 /// // Permutation is a rearrangement of 0..nrows
176 /// let mut sorted = perm.to_vec();
177 /// sorted.sort();
178 /// assert_eq!(sorted, vec![0, 1]);
179 /// ```
180 pub fn row_permutation(&self) -> &[usize] {
181 &self.row_permutation
182 }
183
184 /// Column permutation
185 ///
186 /// # Examples
187 ///
188 /// ```
189 /// use tensor4all_core::matrixlu::rrlu;
190 /// use tensor4all_tensorbackend::from_vec2d;
191 ///
192 /// let m = from_vec2d(vec![vec![1.0_f64, 2.0], vec![3.0, 4.0]]);
193 /// let lu = rrlu(&m, None).unwrap();
194 /// let perm = lu.col_permutation();
195 /// assert_eq!(perm.len(), 2);
196 /// let mut sorted = perm.to_vec();
197 /// sorted.sort();
198 /// assert_eq!(sorted, vec![0, 1]);
199 /// ```
200 pub fn col_permutation(&self) -> &[usize] {
201 &self.col_permutation
202 }
203
204 /// Get row indices (selected pivots)
205 ///
206 /// # Examples
207 ///
208 /// ```
209 /// use tensor4all_core::{matrixlu::rrlu, RrLUOptions};
210 /// use tensor4all_tensorbackend::from_vec2d;
211 ///
212 /// let m = from_vec2d(vec![vec![1.0_f64, 2.0], vec![3.0, 4.0]]);
213 /// let lu = rrlu(&m, Some(RrLUOptions { max_bond_dim: 1, ..Default::default() })).unwrap();
214 /// let rows = lu.row_indices();
215 /// assert_eq!(rows.len(), 1);
216 /// assert!(rows[0] < 2);
217 /// ```
218 pub fn row_indices(&self) -> Vec<usize> {
219 self.row_permutation[0..self.n_pivot].to_vec()
220 }
221
222 /// Get column indices (selected pivots)
223 ///
224 /// # Examples
225 ///
226 /// ```
227 /// use tensor4all_core::{matrixlu::rrlu, RrLUOptions};
228 /// use tensor4all_tensorbackend::from_vec2d;
229 ///
230 /// let m = from_vec2d(vec![vec![1.0_f64, 2.0], vec![3.0, 4.0]]);
231 /// let lu = rrlu(&m, Some(RrLUOptions { max_bond_dim: 1, ..Default::default() })).unwrap();
232 /// let cols = lu.col_indices();
233 /// assert_eq!(cols.len(), 1);
234 /// assert!(cols[0] < 2);
235 /// ```
236 pub fn col_indices(&self) -> Vec<usize> {
237 self.col_permutation[0..self.n_pivot].to_vec()
238 }
239
240 /// Get left matrix (optionally permuted)
241 ///
242 /// # Examples
243 ///
244 /// ```
245 /// use tensor4all_core::matrixlu::rrlu;
246 /// use tensor4all_tensorbackend::{from_vec2d, mat_mul};
247 ///
248 /// let m = from_vec2d(vec![vec![1.0_f64, 2.0], vec![3.0, 4.0]]);
249 /// let lu = rrlu(&m, None).unwrap();
250 ///
251 /// // Unpermuted: L * U reconstructs the row/col-permuted matrix
252 /// let l = lu.left(false);
253 /// let u = lu.right(false);
254 /// let prod = mat_mul(&l, &u).unwrap();
255 /// for i in 0..2 {
256 /// for j in 0..2 {
257 /// let ri = lu.row_permutation()[i];
258 /// let cj = lu.col_permutation()[j];
259 /// assert!((prod[[i, j]] - m[[ri, cj]]).abs() < 1e-10);
260 /// }
261 /// }
262 /// ```
263 pub fn left(&self, permute: bool) -> Matrix<T> {
264 if permute {
265 let mut result = Matrix::zeros(self.l.nrows(), self.l.ncols());
266 for j in 0..self.l.ncols() {
267 for (new_i, &old_i) in self.row_permutation.iter().enumerate() {
268 result[[old_i, j]] = self.l[[new_i, j]];
269 }
270 }
271 result
272 } else {
273 self.l.clone()
274 }
275 }
276
277 /// Get right matrix (optionally permuted)
278 ///
279 /// # Examples
280 ///
281 /// ```
282 /// use tensor4all_core::matrixlu::rrlu;
283 /// use tensor4all_tensorbackend::{from_vec2d, mat_mul};
284 ///
285 /// let m = from_vec2d(vec![vec![1.0_f64, 2.0], vec![3.0, 4.0]]);
286 /// let lu = rrlu(&m, None).unwrap();
287 /// let l = lu.left(false);
288 /// let u = lu.right(false);
289 /// assert_eq!(u.nrows(), lu.npivots());
290 /// assert_eq!(u.ncols(), lu.ncols());
291 /// // L * U reconstructs the permuted matrix
292 /// let prod = mat_mul(&l, &u).unwrap();
293 /// assert!((prod[[0, 0]] - m[[lu.row_permutation()[0], lu.col_permutation()[0]]]).abs() < 1e-10);
294 /// ```
295 pub fn right(&self, permute: bool) -> Matrix<T> {
296 if permute {
297 let mut result = Matrix::zeros(self.u.nrows(), self.u.ncols());
298 for (new_j, &old_j) in self.col_permutation.iter().enumerate() {
299 for i in 0..self.u.nrows() {
300 result[[i, old_j]] = self.u[[i, new_j]];
301 }
302 }
303 result
304 } else {
305 self.u.clone()
306 }
307 }
308
309 pub(crate) fn left_unpermuted(&self) -> &Matrix<T> {
310 &self.l
311 }
312
313 pub(crate) fn right_unpermuted(&self) -> &Matrix<T> {
314 &self.u
315 }
316
317 /// Get diagonal elements
318 ///
319 /// # Examples
320 ///
321 /// ```
322 /// use tensor4all_core::matrixlu::rrlu;
323 /// use tensor4all_tensorbackend::from_vec2d;
324 ///
325 /// let m = from_vec2d(vec![vec![1.0_f64, 2.0], vec![3.0, 4.0]]);
326 /// let lu = rrlu(&m, None).unwrap();
327 /// let d = lu.diag();
328 /// assert_eq!(d.len(), lu.npivots());
329 /// // Diagonal elements are non-zero for full-rank matrices
330 /// for &val in &d {
331 /// assert!(val.abs() > 1e-14);
332 /// }
333 /// ```
334 pub fn diag(&self) -> Vec<T> {
335 let n = self.n_pivot;
336 if self.left_orthogonal {
337 (0..n).map(|i| self.u[[i, i]]).collect()
338 } else {
339 (0..n).map(|i| self.l[[i, i]]).collect()
340 }
341 }
342
343 /// Get pivot errors
344 ///
345 /// # Examples
346 ///
347 /// ```
348 /// use tensor4all_core::matrixlu::rrlu;
349 /// use tensor4all_tensorbackend::from_vec2d;
350 ///
351 /// let m = from_vec2d(vec![vec![1.0_f64, 2.0], vec![3.0, 4.0]]);
352 /// let lu = rrlu(&m, None).unwrap();
353 /// let errs = lu.pivot_errors();
354 /// // One entry per pivot plus the final residual
355 /// assert_eq!(errs.len(), lu.npivots() + 1);
356 /// // Errors are non-negative
357 /// for &e in &errs {
358 /// assert!(e >= 0.0);
359 /// }
360 /// ```
361 pub fn pivot_errors(&self) -> Vec<f64> {
362 let mut errors: Vec<f64> = self.diag().iter().map(|d| d.abs_val()).collect();
363 errors.push(self.error);
364 errors
365 }
366
367 /// Get last pivot error
368 ///
369 /// # Examples
370 ///
371 /// ```
372 /// use tensor4all_core::matrixlu::rrlu;
373 /// use tensor4all_tensorbackend::from_vec2d;
374 ///
375 /// let m = from_vec2d(vec![vec![1.0_f64, 2.0], vec![3.0, 4.0]]);
376 /// let lu = rrlu(&m, None).unwrap();
377 /// // Full-rank decomposition has zero residual
378 /// assert_eq!(lu.last_pivot_error(), 0.0);
379 /// ```
380 pub fn last_pivot_error(&self) -> f64 {
381 self.error
382 }
383
384 /// Transpose the decomposition
385 ///
386 /// # Examples
387 ///
388 /// ```
389 /// use tensor4all_core::matrixlu::rrlu;
390 /// use tensor4all_tensorbackend::from_vec2d;
391 ///
392 /// let m = from_vec2d(vec![vec![1.0_f64, 2.0], vec![3.0, 4.0]]);
393 /// let lu = rrlu(&m, None).unwrap();
394 /// let lu_t = lu.transpose();
395 /// assert_eq!(lu_t.nrows(), lu.ncols());
396 /// assert_eq!(lu_t.ncols(), lu.nrows());
397 /// assert_eq!(lu_t.npivots(), lu.npivots());
398 /// assert_eq!(lu_t.is_left_orthogonal(), !lu.is_left_orthogonal());
399 /// ```
400 pub fn transpose(&self) -> RrLU<T> {
401 RrLU {
402 row_permutation: self.col_permutation.clone(),
403 col_permutation: self.row_permutation.clone(),
404 l: transpose(&self.u),
405 u: transpose(&self.l),
406 left_orthogonal: !self.left_orthogonal,
407 n_pivot: self.n_pivot,
408 error: self.error,
409 }
410 }
411
412 /// Check if left-orthogonal (L has 1s on diagonal)
413 ///
414 /// # Examples
415 ///
416 /// ```
417 /// use tensor4all_core::{matrixlu::rrlu, RrLUOptions};
418 /// use tensor4all_tensorbackend::from_vec2d;
419 ///
420 /// let m = from_vec2d(vec![vec![1.0_f64, 2.0], vec![3.0, 4.0]]);
421 ///
422 /// let lu = rrlu(&m, None).unwrap();
423 /// assert!(lu.is_left_orthogonal()); // default
424 ///
425 /// let lu2 = rrlu(&m, Some(RrLUOptions {
426 /// left_orthogonal: false, ..Default::default()
427 /// })).unwrap();
428 /// assert!(!lu2.is_left_orthogonal());
429 /// ```
430 pub fn is_left_orthogonal(&self) -> bool {
431 self.left_orthogonal
432 }
433}
434
435pub(crate) fn validate_col_major_matrix_len(
436 nrows: usize,
437 ncols: usize,
438 actual_len: usize,
439) -> crate::Result<()> {
440 let expected = nrows
441 .checked_mul(ncols)
442 .ok_or_else(|| MatrixCIError::InvalidArgument {
443 message: format!("matrix shape {nrows} x {ncols} overflows usize"),
444 })?;
445 if actual_len != expected {
446 return Err(MatrixCIError::InvalidArgument {
447 message: format!(
448 "column-major matrix length mismatch: expected {expected}, got {actual_len}"
449 ),
450 });
451 }
452 Ok(())
453}
454
455/// Validate [`RrLUOptions`] before a factorization.
456///
457/// Negative or non-finite tolerances return [`MatrixCIError::InvalidArgument`].
458/// Shared by the dense, lazy, and selection-only entry points.
459pub(crate) fn validate_luci_options(options: &RrLUOptions) -> Result<()> {
460 for (name, value) in [("rel_tol", options.rel_tol), ("abs_tol", options.abs_tol)] {
461 if !value.is_finite() || value < 0.0 {
462 return Err(MatrixCIError::InvalidArgument {
463 message: format!("{name} must be finite and nonnegative"),
464 });
465 }
466 }
467 Ok(())
468}
469
470/// Validate the entries of a column-major matrix before a factorization.
471///
472/// NaN input returns [`MatrixCIError::NaNEncountered`]; infinite input
473/// magnitudes return [`MatrixCIError::InvalidArgument`].
474pub(crate) fn validate_luci_matrix<T: Scalar>(a: &Matrix<T>) -> Result<()> {
475 if a.as_col_major_slice().iter().any(|&value| value.is_nan()) {
476 return Err(MatrixCIError::NaNEncountered {
477 matrix: "input".into(),
478 });
479 }
480 if a.as_col_major_slice()
481 .iter()
482 .any(|&value| !value.abs_val().is_finite())
483 {
484 return Err(MatrixCIError::InvalidArgument {
485 message: "input matrix magnitudes must be finite".into(),
486 });
487 }
488 Ok(())
489}
490
491/// Validate a column-major matrix and its [`RrLUOptions`] before a factorization.
492///
493/// Shared by the full-factor and selection-only entry points so both reject the
494/// same inputs with the same error: negative or non-finite tolerances and
495/// infinite input magnitudes return [`MatrixCIError::InvalidArgument`]; NaN
496/// input returns [`MatrixCIError::NaNEncountered`].
497pub(crate) fn validate_owned_luci_input<T: Scalar>(
498 a: &Matrix<T>,
499 options: &RrLUOptions,
500) -> Result<()> {
501 validate_luci_options(options)?;
502 validate_luci_matrix(a)
503}
504
505#[inline]
506fn col_major_offset(nrows: usize, row: usize, col: usize) -> usize {
507 row + nrows * col
508}
509
510#[inline]
511fn col_major_get<T: Copy>(data: &[T], nrows: usize, row: usize, col: usize) -> T {
512 let offset = col_major_offset(nrows, row, col);
513 debug_assert!(row < nrows);
514 debug_assert!(offset < data.len());
515 // SAFETY: callers pass matrix-derived dimensions and prechecked ranges.
516 unsafe { *data.get_unchecked(offset) }
517}
518
519#[inline]
520fn col_major_set<T>(data: &mut [T], nrows: usize, row: usize, col: usize, value: T) {
521 let offset = col_major_offset(nrows, row, col);
522 debug_assert!(row < nrows);
523 debug_assert!(offset < data.len());
524 // SAFETY: callers pass matrix-derived dimensions and prechecked ranges.
525 unsafe {
526 *data.get_unchecked_mut(offset) = value;
527 }
528}
529
530fn submatrix_argmax_col_major<T: Scalar>(
531 data: &[T],
532 nrows: usize,
533 ncols: usize,
534 row_start: usize,
535 row_end: usize,
536 col_start: usize,
537 col_end: usize,
538) -> (usize, usize, T) {
539 debug_assert!(row_start < row_end);
540 debug_assert!(col_start < col_end);
541 debug_assert!(row_end <= nrows);
542 debug_assert!(col_end <= ncols);
543 debug_assert_eq!(data.len(), nrows * ncols);
544
545 let first = col_major_get(data, nrows, row_start, col_start);
546 let mut max_val = first.abs_sq();
547 let mut max_row = row_start;
548 let mut max_col = col_start;
549
550 for col in col_start..col_end {
551 let col_start_offset = col_major_offset(nrows, row_start, col);
552 for (offset, row) in (col_start_offset..).zip(row_start..row_end) {
553 // SAFETY: row and column loops stay within the prechecked region.
554 let value = unsafe { *data.get_unchecked(offset) };
555 let value_abs = value.abs_sq();
556 if value_abs > max_val {
557 max_val = value_abs;
558 max_row = row;
559 max_col = col;
560 }
561 }
562 }
563
564 // Preserve the normal-scale squared-magnitude fast path and its tie order.
565 // Outside the component type's normal range, compare robust magnitudes.
566 if !max_val.is_finite() || max_val < T::min_positive() {
567 let mut max_abs = first.abs_val();
568 max_row = row_start;
569 max_col = col_start;
570 for col in col_start..col_end {
571 let start = col_major_offset(nrows, row_start, col);
572 for (&value, row) in data[start..start + row_end - row_start]
573 .iter()
574 .zip(row_start..row_end)
575 {
576 let magnitude = value.abs_val();
577 if magnitude > max_abs {
578 max_abs = magnitude;
579 max_row = row;
580 max_col = col;
581 }
582 }
583 }
584 }
585
586 (
587 max_row,
588 max_col,
589 col_major_get(data, nrows, max_row, max_col),
590 )
591}
592
593fn swap_rows_col_major<T>(data: &mut [T], nrows: usize, ncols: usize, row_a: usize, row_b: usize) {
594 debug_assert!(row_a < nrows);
595 debug_assert!(row_b < nrows);
596 debug_assert_eq!(data.len(), nrows * ncols);
597 if row_a == row_b {
598 return;
599 }
600
601 let ptr = data.as_mut_ptr();
602 for col in 0..ncols {
603 let offset_a = col_major_offset(nrows, row_a, col);
604 let offset_b = col_major_offset(nrows, row_b, col);
605 // SAFETY: row_a/row_b are in range, and each column offset is within
606 // the matrix-sized backing slice.
607 unsafe {
608 std::ptr::swap(ptr.add(offset_a), ptr.add(offset_b));
609 }
610 }
611}
612
613fn swap_cols_col_major<T>(data: &mut [T], nrows: usize, ncols: usize, col_a: usize, col_b: usize) {
614 debug_assert!(col_a < ncols);
615 debug_assert!(col_b < ncols);
616 debug_assert_eq!(data.len(), nrows * ncols);
617 if col_a == col_b || nrows == 0 {
618 return;
619 }
620
621 let start_a = col_major_offset(nrows, 0, col_a);
622 let start_b = col_major_offset(nrows, 0, col_b);
623 // SAFETY: distinct columns are non-overlapping contiguous ranges of
624 // length nrows in column-major storage.
625 unsafe {
626 std::ptr::swap_nonoverlapping(
627 data.as_mut_ptr().add(start_a),
628 data.as_mut_ptr().add(start_b),
629 nrows,
630 );
631 }
632}
633
634/// Select division once per pivot, preserving ordinary-scale arithmetic.
635/// Reciprocal multiplication avoids squaring an extreme complex divisor.
636enum PivotDivisor<T> {
637 Direct(T),
638 Reciprocal(T),
639}
640
641impl<T: Scalar> PivotDivisor<T> {
642 fn new(pivot: T) -> Self {
643 let squared = pivot.abs_sq();
644 if squared >= T::min_positive() && squared.is_finite() {
645 Self::Direct(pivot)
646 } else {
647 let inverse = T::from_f64(1.0 / pivot.abs_val());
648 Self::Reciprocal((pivot * inverse).conj() * inverse)
649 }
650 }
651}
652
653/// Stop only for an exact zero or an unrepresentable reciprocal in T.
654pub(crate) fn pivot_is_undividable<T: Scalar>(magnitude: f64) -> bool {
655 magnitude == 0.0
656 || (magnitude.is_finite() && !T::from_f64(1.0 / magnitude).abs_val().is_finite())
657}
658
659fn scale_column_by<T: Scalar, F: Fn(T) -> T>(
660 data: &mut [T],
661 nrows: usize,
662 col: usize,
663 row_start: usize,
664 divide: F,
665) {
666 let start = col_major_offset(nrows, row_start, col);
667 for value in &mut data[start..nrows * (col + 1)] {
668 *value = divide(*value);
669 }
670}
671
672fn scale_column_tail<T: Scalar>(
673 data: &mut [T],
674 nrows: usize,
675 col: usize,
676 row_start: usize,
677 pivot: T,
678) {
679 if row_start >= nrows {
680 return;
681 }
682 match PivotDivisor::new(pivot) {
683 PivotDivisor::Direct(p) => scale_column_by(data, nrows, col, row_start, |v| v / p),
684 PivotDivisor::Reciprocal(p) => scale_column_by(data, nrows, col, row_start, |v| v * p),
685 }
686}
687
688fn scale_row_by<T: Scalar, F: Fn(T) -> T>(
689 data: &mut [T],
690 nrows: usize,
691 ncols: usize,
692 row: usize,
693 col_start: usize,
694 divide: F,
695) {
696 for col in col_start..ncols {
697 let value = divide(col_major_get(data, nrows, row, col));
698 col_major_set(data, nrows, row, col, value);
699 }
700}
701
702fn scale_row_tail<T: Scalar>(
703 data: &mut [T],
704 nrows: usize,
705 ncols: usize,
706 row: usize,
707 col_start: usize,
708 pivot: T,
709) {
710 match PivotDivisor::new(pivot) {
711 PivotDivisor::Direct(p) => scale_row_by(data, nrows, ncols, row, col_start, |v| v / p),
712 PivotDivisor::Reciprocal(p) => scale_row_by(data, nrows, ncols, row, col_start, |v| v * p),
713 }
714}
715
716fn update_trailing_submatrix<T: Scalar>(data: &mut [T], nrows: usize, ncols: usize, pivot: usize) {
717 let tail_row_start = pivot + 1;
718 let tail_col_start = pivot + 1;
719 if tail_row_start >= nrows || tail_col_start >= ncols {
720 return;
721 }
722
723 let tail_len = nrows - tail_row_start;
724 let pivot_col_tail_start = col_major_offset(nrows, tail_row_start, pivot);
725 for col in tail_col_start..ncols {
726 let y = col_major_get(data, nrows, pivot, col);
727 let target_start = col_major_offset(nrows, tail_row_start, col);
728 let (before_target, target_and_after) = data.split_at_mut(target_start);
729 let pivot_col_tail = &before_target[pivot_col_tail_start..pivot_col_tail_start + tail_len];
730 let target_tail = &mut target_and_after[..tail_len];
731 for (target, &x) in target_tail.iter_mut().zip(pivot_col_tail.iter()) {
732 *target = *target - x * y;
733 }
734 }
735}
736
737fn extract_l_from_factorized<T: Scalar>(
738 data: &[T],
739 nrows: usize,
740 rank: usize,
741 left_orthogonal: bool,
742) -> Result<Matrix<T>> {
743 debug_assert!(rank <= nrows);
744
745 let mut l_data = vec![T::zero(); nrows * rank];
746 for col in 0..rank {
747 let src_start = col_major_offset(nrows, col, col);
748 let src_end = col_major_offset(nrows, nrows - 1, col) + 1;
749 let dst_start = col_major_offset(nrows, col, col);
750 l_data[dst_start..dst_start + (nrows - col)].copy_from_slice(&data[src_start..src_end]);
751 }
752
753 if left_orthogonal {
754 for i in 0..rank {
755 l_data[col_major_offset(nrows, i, i)] = T::one();
756 }
757 }
758
759 if l_data.iter().any(|&value| !value.abs_val().is_finite()) {
760 return Err(MatrixCIError::NaNEncountered {
761 matrix: "L".to_string(),
762 });
763 }
764
765 Ok(Matrix::from_col_major_vec(nrows, rank, l_data))
766}
767
768fn extract_u_from_factorized<T: Scalar>(
769 data: &[T],
770 nrows: usize,
771 ncols: usize,
772 rank: usize,
773 left_orthogonal: bool,
774) -> Result<Matrix<T>> {
775 debug_assert!(rank <= nrows.min(ncols));
776 debug_assert_eq!(data.len(), nrows * ncols);
777
778 let mut u_data = vec![T::zero(); rank * ncols];
779 for col in 0..ncols {
780 let rows_to_copy = rank.min(col + 1);
781 if rows_to_copy > 0 {
782 let src_start = col_major_offset(nrows, 0, col);
783 let dst_start = col_major_offset(rank, 0, col);
784 u_data[dst_start..dst_start + rows_to_copy]
785 .copy_from_slice(&data[src_start..src_start + rows_to_copy]);
786 }
787 }
788
789 if !left_orthogonal {
790 for i in 0..rank {
791 u_data[col_major_offset(rank, i, i)] = T::one();
792 }
793 }
794
795 if u_data.iter().any(|&value| !value.abs_val().is_finite()) {
796 return Err(MatrixCIError::NaNEncountered {
797 matrix: "U".to_string(),
798 });
799 }
800
801 Ok(Matrix::from_col_major_vec(rank, ncols, u_data))
802}
803
804fn extract_lu_from_factorized<T: Scalar>(
805 data: &[T],
806 nrows: usize,
807 ncols: usize,
808 rank: usize,
809 left_orthogonal: bool,
810) -> Result<(Matrix<T>, Matrix<T>)> {
811 let left = extract_l_from_factorized(data, nrows, rank, left_orthogonal)?;
812 let right = extract_u_from_factorized(data, nrows, ncols, rank, left_orthogonal)?;
813 Ok((left, right))
814}
815
816/// Options for rank-revealing LU decomposition.
817///
818/// # Examples
819///
820/// ```
821/// use tensor4all_core::RrLUOptions;
822///
823/// // Default: rel_tol = 1e-14, no absolute tolerance, no rank limit
824/// let opts = RrLUOptions::default();
825/// assert_eq!(opts.rel_tol, 1e-14);
826/// assert_eq!(opts.abs_tol, 0.0);
827/// assert!(opts.left_orthogonal);
828///
829/// // Limit rank to 5
830/// let opts = RrLUOptions { max_bond_dim: 5, ..Default::default() };
831/// assert_eq!(opts.max_bond_dim, 5);
832/// ```
833#[derive(Debug, Clone)]
834pub struct RrLUOptions {
835 /// Maximum rank
836 pub max_bond_dim: usize,
837 /// Relative tolerance
838 pub rel_tol: f64,
839 /// Absolute tolerance
840 pub abs_tol: f64,
841 /// Left orthogonal (L has 1s on diagonal) or right orthogonal (U has 1s)
842 pub left_orthogonal: bool,
843}
844
845struct RrLUFactorization {
846 row_permutation: Vec<usize>,
847 col_permutation: Vec<usize>,
848 n_pivot: usize,
849 error: f64,
850}
851
852/// Left factor and pivot diagnostics needed by row interpolation.
853///
854/// This avoids materializing the complementary upper factor for RSI's
855/// row-only path while sharing the same pivoting implementation as [`rrlu_mut`].
856pub(crate) struct RrLULeft<T: Scalar> {
857 pub(crate) row_permutation: Vec<usize>,
858 pub(crate) l: Matrix<T>,
859 pub(crate) n_pivot: usize,
860 pub(crate) pivot_magnitudes: Vec<f64>,
861}
862
863fn factorize_mut_in_place<T: Scalar>(
864 a: &mut Matrix<T>,
865 opts: RrLUOptions,
866) -> Result<RrLUFactorization> {
867 let nr = a.nrows();
868 let nc = a.ncols();
869 let data = a.as_col_major_mut_slice();
870 validate_col_major_matrix_len(nr, nc, data.len())?;
871 if data.iter().any(|&value| !value.abs_val().is_finite()) {
872 return Err(MatrixCIError::NaNEncountered {
873 matrix: "input".to_string(),
874 });
875 }
876
877 let max_bond_dim = opts.max_bond_dim.min(nr).min(nc);
878 let mut row_permutation = (0..nr).collect::<Vec<_>>();
879 let mut col_permutation = (0..nc).collect::<Vec<_>>();
880 let mut n_pivot = 0;
881 let mut error = f64::NAN;
882 let mut max_error = 0.0f64;
883
884 while n_pivot < max_bond_dim {
885 let k = n_pivot;
886 if k >= nr || k >= nc {
887 break;
888 }
889
890 let (pivot_row, pivot_col, pivot_val) =
891 submatrix_argmax_col_major(data, nr, nc, k, nr, k, nc);
892 let pivot_abs = pivot_val.abs_val();
893 error = pivot_abs;
894 if !pivot_abs.is_finite() {
895 return Err(MatrixCIError::NaNEncountered {
896 matrix: "residual".to_string(),
897 });
898 }
899
900 // Check stopping criteria (but add at least 1 pivot)
901 if n_pivot > 0 && (pivot_abs < opts.rel_tol * max_error || pivot_abs < opts.abs_tol) {
902 break;
903 }
904
905 if pivot_is_undividable::<T>(pivot_abs) {
906 break;
907 }
908
909 max_error = max_error.max(pivot_abs);
910
911 // Swap rows and columns
912 if pivot_row != k {
913 swap_rows_col_major(data, nr, nc, k, pivot_row);
914 row_permutation.swap(k, pivot_row);
915 }
916 if pivot_col != k {
917 swap_cols_col_major(data, nr, nc, k, pivot_col);
918 col_permutation.swap(k, pivot_col);
919 }
920
921 let pivot = col_major_get(data, nr, k, k);
922
923 // Eliminate
924 if opts.left_orthogonal {
925 scale_column_tail(data, nr, k, k + 1, pivot);
926 } else {
927 scale_row_tail(data, nr, nc, k, k + 1, pivot);
928 }
929
930 update_trailing_submatrix(data, nr, nc, k);
931 n_pivot += 1;
932 }
933
934 // Preserve the full-factor API's existing diagnostic semantics.
935 if n_pivot >= nr.min(nc) {
936 error = 0.0;
937 }
938
939 Ok(RrLUFactorization {
940 row_permutation,
941 col_permutation,
942 n_pivot,
943 error,
944 })
945}
946
947/// Pivot magnitudes and the final residual diagnostic of a factorization.
948///
949/// The trailing entry is `state.error` unless the search stopped exactly at a
950/// rank cap below `min(nrows, ncols)`, in which case it is the maximum
951/// magnitude of the remaining Schur complement. A zero cap therefore reports
952/// the whole matrix. Shared with [`rrlu_mut`] so the selection-only path cannot
953/// drift from the full-factor error history.
954fn pivot_errors_from_factorization<T: Scalar>(
955 a: &Matrix<T>,
956 state: &RrLUFactorization,
957 rank_cap: usize,
958) -> Result<Vec<f64>> {
959 let nrows = a.nrows();
960 let ncols = a.ncols();
961 let data = a.as_col_major_slice();
962 let mut errors = (0..state.n_pivot)
963 .map(|i| data[col_major_offset(nrows, i, i)].abs_val())
964 .collect::<Vec<_>>();
965
966 let max_bond_dim = rank_cap.min(nrows).min(ncols);
967 let mut error = state.error;
968 if state.n_pivot < nrows.min(ncols) && state.n_pivot == max_bond_dim {
969 error = 0.0;
970 for col in state.n_pivot..ncols {
971 for row in state.n_pivot..nrows {
972 let residual = col_major_get(data, nrows, row, col).abs_val();
973 if !residual.is_finite() {
974 return Err(MatrixCIError::NaNEncountered {
975 matrix: "residual".to_string(),
976 });
977 }
978 error = error.max(residual);
979 }
980 }
981 }
982 errors.push(error);
983 Ok(errors)
984}
985
986pub(crate) fn rrlu_left_mut<T: Scalar>(
987 a: &mut Matrix<T>,
988 options: Option<RrLUOptions>,
989) -> Result<RrLULeft<T>> {
990 let mut options = options.unwrap_or_default();
991 options.left_orthogonal = true;
992 let state = factorize_mut_in_place(a, options)?;
993 let nrows = a.nrows();
994 let data = a.as_col_major_slice();
995 let mut pivot_magnitudes = (0..state.n_pivot)
996 .map(|i| data[col_major_offset(nrows, i, i)].abs_val())
997 .collect::<Vec<_>>();
998 // The row-only API measures the remaining residual even at a rank cap.
999 let residual = if state.n_pivot >= a.nrows().min(a.ncols()) {
1000 0.0
1001 } else {
1002 let (_, _, value) = submatrix_argmax_col_major(
1003 data,
1004 nrows,
1005 a.ncols(),
1006 state.n_pivot,
1007 nrows,
1008 state.n_pivot,
1009 a.ncols(),
1010 );
1011 value.abs_val()
1012 };
1013 if !residual.is_finite() {
1014 return Err(MatrixCIError::NaNEncountered {
1015 matrix: "residual".to_string(),
1016 });
1017 }
1018 pivot_magnitudes.push(residual);
1019 let l = extract_l_from_factorized(data, nrows, state.n_pivot, true)?;
1020 Ok(RrLULeft {
1021 row_permutation: state.row_permutation,
1022 l,
1023 n_pivot: state.n_pivot,
1024 pivot_magnitudes,
1025 })
1026}
1027
1028impl Default for RrLUOptions {
1029 fn default() -> Self {
1030 Self {
1031 max_bond_dim: usize::MAX,
1032 rel_tol: 1e-14,
1033 abs_tol: 0.0,
1034 left_orthogonal: true,
1035 }
1036 }
1037}
1038
1039/// Perform in-place rank-revealing LU decomposition.
1040///
1041/// The input matrix `a` is modified in place. Use [`rrlu`] for a
1042/// non-destructive version.
1043///
1044/// # Errors
1045///
1046/// Returns [`MatrixCIError::InvalidArgument`] if the matrix shape product
1047/// overflows `usize` or its backing storage length does not match the shape.
1048/// Returns [`MatrixCIError::NaNEncountered`] if input entries or intermediate
1049/// factors have non-finite magnitudes.
1050///
1051/// # Examples
1052///
1053/// ```
1054/// use tensor4all_core::{matrixlu::rrlu_mut, RrLUOptions};
1055/// use tensor4all_tensorbackend::from_vec2d;
1056///
1057/// let mut m = from_vec2d(vec![
1058/// vec![1.0_f64, 2.0],
1059/// vec![3.0, 4.0],
1060/// ]);
1061/// let lu = rrlu_mut(&mut m, Some(RrLUOptions { max_bond_dim: 1, ..Default::default() })).unwrap();
1062/// assert_eq!(lu.npivots(), 1);
1063/// ```
1064pub fn rrlu_mut<T: Scalar>(a: &mut Matrix<T>, options: Option<RrLUOptions>) -> Result<RrLU<T>> {
1065 let opts = options.unwrap_or_default();
1066 let left_orthogonal = opts.left_orthogonal;
1067 let rank_cap = opts.max_bond_dim;
1068 let state = factorize_mut_in_place(a, opts)?;
1069 let nrows = a.nrows();
1070 let ncols = a.ncols();
1071 let (l, u) = extract_lu_from_factorized(
1072 a.as_col_major_slice(),
1073 nrows,
1074 ncols,
1075 state.n_pivot,
1076 left_orthogonal,
1077 )?;
1078
1079 // A rank ceiling exits before the next pivot scan. The shared extraction
1080 // reports the remaining Schur complement, not the magnitude of the last
1081 // accepted pivot.
1082 let mut error = pivot_errors_from_factorization(a, &state, rank_cap)?;
1083 let error = error.pop().unwrap_or(0.0);
1084
1085 Ok(RrLU {
1086 row_permutation: state.row_permutation,
1087 col_permutation: state.col_permutation,
1088 l,
1089 u,
1090 left_orthogonal,
1091 n_pivot: state.n_pivot,
1092 error,
1093 })
1094}
1095
1096/// Pivot indices and error diagnostics from a rank-revealing LU selection.
1097///
1098/// This is the selection-only counterpart of [`RrLU`]: the same pivot search
1099/// and rank decision as [`rrlu`]/[`rrlu_mut`], but without assembling the `L`
1100/// and `U` factors. Use it when only the selected pivots and the pivot error
1101/// history are needed and the factors would be discarded.
1102///
1103/// # Examples
1104///
1105/// ```
1106/// use tensor4all_core::matrixlu::rrlu_pivots;
1107/// use tensor4all_tensorbackend::from_vec2d;
1108///
1109/// let a = from_vec2d(vec![
1110/// vec![1.0_f64, 2.0],
1111/// vec![2.0, 4.0],
1112/// ]);
1113/// let pivots = rrlu_pivots(a, None).unwrap();
1114/// assert_eq!(pivots.rank, 1);
1115/// assert_eq!(pivots.row_indices.len(), pivots.rank);
1116/// assert_eq!(pivots.col_indices.len(), pivots.rank);
1117/// // One accepted pivot plus the final residual.
1118/// assert_eq!(pivots.pivot_errors.len(), pivots.rank + 1);
1119/// ```
1120#[derive(Debug, Clone, PartialEq)]
1121pub struct RrLUPivots {
1122 /// Selected row indices, in pivot order.
1123 pub row_indices: Vec<usize>,
1124 /// Selected column indices, in pivot order.
1125 pub col_indices: Vec<usize>,
1126 /// Accepted pivot magnitudes followed by the final residual estimate.
1127 pub pivot_errors: Vec<f64>,
1128 /// Number of selected pivots.
1129 pub rank: usize,
1130}
1131
1132/// Select rrLU pivots without constructing the triangular factors.
1133///
1134/// Consumes the column-major matrix `a` (its storage is reused for the in-place
1135/// elimination) and returns the selected row and column indices plus the pivot
1136/// error history, using the same rank and tolerance rules as [`rrlu_mut`]. The
1137/// trailing `pivot_errors` entry is the maximum remaining residual after a rank
1138/// cap, so the sequence matches [`rrlu_mut`]'s `pivot_errors` exactly.
1139///
1140/// # Errors
1141///
1142/// Returns [`MatrixCIError::InvalidArgument`] if the matrix shape product
1143/// overflows `usize`, its backing storage length does not match the shape, a
1144/// tolerance is negative or non-finite, or an input entry is infinite. Returns
1145/// [`MatrixCIError::NaNEncountered`] if an input entry or the remaining
1146/// residual is NaN.
1147///
1148/// # Examples
1149///
1150/// ```
1151/// use tensor4all_core::{matrixlu::rrlu_pivots, RrLUOptions};
1152/// use tensor4all_tensorbackend::from_vec2d;
1153///
1154/// let a = from_vec2d(vec![
1155/// vec![1.0_f64, 0.0],
1156/// vec![0.0, 2.0],
1157/// ]);
1158/// // The first pivot is the largest-magnitude entry, at (1, 1).
1159/// let pivots = rrlu_pivots(a, Some(RrLUOptions::default())).unwrap();
1160/// assert_eq!(pivots.rank, 2);
1161/// assert_eq!(pivots.row_indices, vec![1, 0]);
1162/// assert_eq!(pivots.col_indices, vec![1, 0]);
1163/// assert_eq!(pivots.pivot_errors, vec![2.0, 1.0, 0.0]);
1164/// ```
1165pub fn rrlu_pivots<T: Scalar>(
1166 mut a: Matrix<T>,
1167 options: Option<RrLUOptions>,
1168) -> Result<RrLUPivots> {
1169 let opts = options.unwrap_or_default();
1170 let rank_cap = opts.max_bond_dim;
1171 validate_owned_luci_input(&a, &opts)?;
1172 let state = factorize_mut_in_place(&mut a, opts)?;
1173 let pivot_errors = pivot_errors_from_factorization(&a, &state, rank_cap)?;
1174 Ok(RrLUPivots {
1175 row_indices: state.row_permutation[..state.n_pivot].to_vec(),
1176 col_indices: state.col_permutation[..state.n_pivot].to_vec(),
1177 pivot_errors,
1178 rank: state.n_pivot,
1179 })
1180}
1181
1182/// Perform rank-revealing LU decomposition (non-destructive).
1183///
1184/// Clones the input matrix and calls [`rrlu_mut`].
1185///
1186/// # Errors
1187///
1188/// Returns [`MatrixCIError::InvalidArgument`] if the matrix shape product
1189/// overflows `usize` or its backing storage length does not match the shape.
1190/// Returns [`MatrixCIError::NaNEncountered`] if input entries or intermediate
1191/// factors have non-finite magnitudes.
1192///
1193/// # Examples
1194///
1195/// ```
1196/// use tensor4all_core::matrixlu::rrlu;
1197/// use tensor4all_tensorbackend::from_vec2d;
1198///
1199/// let m = from_vec2d(vec![
1200/// vec![1.0_f64, 0.0],
1201/// vec![0.0, 2.0],
1202/// ]);
1203/// let lu = rrlu(&m, None).unwrap();
1204/// assert_eq!(lu.npivots(), 2);
1205/// assert_eq!(lu.nrows(), 2);
1206/// assert_eq!(lu.ncols(), 2);
1207/// ```
1208pub fn rrlu<T: Scalar>(a: &Matrix<T>, options: Option<RrLUOptions>) -> Result<RrLU<T>> {
1209 let mut a_copy = a.clone();
1210 rrlu_mut(&mut a_copy, options)
1211}
1212
1213/// Convert L matrix to solve L * X = B given pivot matrix P
1214///
1215/// Modifies `c` in place so that the columns satisfy the triangular
1216/// system defined by `p`. The matrix `c` must have at least `p.nrows()`
1217/// columns.
1218///
1219/// # Errors
1220/// Returns [`MatrixCIError::InvalidArgument`] when `p` is not square or
1221/// `c` has too few columns, and [`MatrixCIError::SingularMatrix`] for a zero
1222/// diagonal pivot or one with an unrepresentable reciprocal; non-finite pivots
1223/// return [`MatrixCIError::NaNEncountered`].
1224///
1225/// # Examples
1226///
1227/// ```
1228/// use tensor4all_core::matrixlu::cols_to_l_matrix;
1229/// use tensor4all_tensorbackend::from_vec2d;
1230///
1231/// // Upper-triangular P (2x2)
1232/// let p = from_vec2d(vec![
1233/// vec![2.0_f64, 1.0],
1234/// vec![0.0, 3.0],
1235/// ]);
1236/// // c has 3 rows, 2 columns (ncols >= p.nrows())
1237/// let mut c = from_vec2d(vec![
1238/// vec![4.0_f64, 5.0],
1239/// vec![6.0, 9.0],
1240/// vec![8.0, 7.0],
1241/// ]);
1242/// cols_to_l_matrix(&mut c, &p, true).unwrap();
1243/// // After processing: c[:,0] was divided by p[0,0]=2
1244/// assert!((c[[0, 0]] - 2.0).abs() < 1e-10);
1245/// assert!((c[[1, 0]] - 3.0).abs() < 1e-10);
1246/// assert!((c[[2, 0]] - 4.0).abs() < 1e-10);
1247/// ```
1248pub fn cols_to_l_matrix<T: Scalar>(
1249 c: &mut Matrix<T>,
1250 p: &Matrix<T>,
1251 _left_orthogonal: bool,
1252) -> Result<()> {
1253 if p.nrows() != p.ncols() || c.ncols() < p.nrows() {
1254 return Err(MatrixCIError::InvalidArgument {
1255 message: format!(
1256 "cols_to_l_matrix requires square p and c.ncols() >= p.nrows(), got p=({}, {}), c.ncols()={}",
1257 p.nrows(),
1258 p.ncols(),
1259 c.ncols()
1260 ),
1261 });
1262 }
1263 let n = p.nrows();
1264 for k in 0..n {
1265 let magnitude = p[[k, k]].abs_val();
1266 if !magnitude.is_finite() {
1267 return Err(MatrixCIError::NaNEncountered {
1268 matrix: "pivot".to_string(),
1269 });
1270 }
1271 if pivot_is_undividable::<T>(magnitude) {
1272 return Err(MatrixCIError::SingularMatrix);
1273 }
1274 }
1275
1276 for k in 0..n {
1277 let pivot = p[[k, k]];
1278 let nrows = c.nrows();
1279 scale_column_tail(c.as_col_major_mut_slice(), nrows, k, 0, pivot);
1280
1281 // c[:, k+1:] -= c[:, k] * p[k, k+1:]
1282 for j in (k + 1)..c.ncols() {
1283 let p_kj = p[[k, j]];
1284 for i in 0..c.nrows() {
1285 let c_ik = c[[i, k]];
1286 let old = c[[i, j]];
1287 c[[i, j]] = old - c_ik * p_kj;
1288 }
1289 }
1290 }
1291 Ok(())
1292}
1293
1294/// Convert R matrix to solve X * U = B given pivot matrix P
1295///
1296/// Modifies `r` in place so that the rows satisfy the triangular
1297/// system defined by `p`. The matrix `r` must have at least `p.nrows()`
1298/// rows.
1299///
1300/// # Errors
1301/// Returns [`MatrixCIError::InvalidArgument`] when `p` is not square or
1302/// `r` has too few rows, and [`MatrixCIError::SingularMatrix`] for a zero
1303/// diagonal pivot or one with an unrepresentable reciprocal; non-finite pivots
1304/// return [`MatrixCIError::NaNEncountered`].
1305///
1306/// # Examples
1307///
1308/// ```
1309/// use tensor4all_core::matrixlu::rows_to_u_matrix;
1310/// use tensor4all_tensorbackend::from_vec2d;
1311///
1312/// // Lower-triangular P (2x2)
1313/// let p = from_vec2d(vec![
1314/// vec![2.0_f64, 0.0],
1315/// vec![1.0, 3.0],
1316/// ]);
1317/// // r has 2 rows (nrows >= p.nrows()), 3 columns
1318/// let mut r = from_vec2d(vec![
1319/// vec![4.0_f64, 6.0, 8.0],
1320/// vec![5.0, 9.0, 7.0],
1321/// ]);
1322/// rows_to_u_matrix(&mut r, &p, true).unwrap();
1323/// // After processing: r[0,:] was divided by p[0,0]=2
1324/// assert!((r[[0, 0]] - 2.0).abs() < 1e-10);
1325/// assert!((r[[0, 1]] - 3.0).abs() < 1e-10);
1326/// assert!((r[[0, 2]] - 4.0).abs() < 1e-10);
1327/// ```
1328pub fn rows_to_u_matrix<T: Scalar>(
1329 r: &mut Matrix<T>,
1330 p: &Matrix<T>,
1331 _left_orthogonal: bool,
1332) -> Result<()> {
1333 if p.nrows() != p.ncols() || r.nrows() < p.nrows() {
1334 return Err(MatrixCIError::InvalidArgument {
1335 message: format!(
1336 "rows_to_u_matrix requires square p and r.nrows() >= p.nrows(), got p=({}, {}), r.nrows()={}",
1337 p.nrows(),
1338 p.ncols(),
1339 r.nrows()
1340 ),
1341 });
1342 }
1343 let n = p.nrows();
1344 for k in 0..n {
1345 let magnitude = p[[k, k]].abs_val();
1346 if !magnitude.is_finite() {
1347 return Err(MatrixCIError::NaNEncountered {
1348 matrix: "pivot".to_string(),
1349 });
1350 }
1351 if pivot_is_undividable::<T>(magnitude) {
1352 return Err(MatrixCIError::SingularMatrix);
1353 }
1354 }
1355
1356 for k in 0..n {
1357 let pivot = p[[k, k]];
1358 let (nrows, ncols) = (r.nrows(), r.ncols());
1359 scale_row_tail(r.as_col_major_mut_slice(), nrows, ncols, k, 0, pivot);
1360
1361 // r[k+1:, :] -= p[k+1:, k] * r[k, :]
1362 for i in (k + 1)..r.nrows() {
1363 let p_ik = p[[i, k]];
1364 for j in 0..r.ncols() {
1365 let r_kj = r[[k, j]];
1366 let old = r[[i, j]];
1367 r[[i, j]] = old - p_ik * r_kj;
1368 }
1369 }
1370 }
1371 Ok(())
1372}
1373
1374#[cfg(test)]
1375mod tests;