Skip to main content

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;