Skip to main content

la_stack/
column_basis.rs

1#![forbid(unsafe_code)]
2
3//! Provisional numerical column selection for fixed rectangular arrays.
4
5use crate::{ArithmeticOperation, LaError, Tolerance};
6
7/// Numerical column selection for an `R`-row, `C`-column input.
8///
9/// Created by [`column_basis`]. Both input dimensions remain part of the type.
10/// The selected indices are unique, strictly increasing, and less than `C`;
11/// their count is at most `min(R, C)`. Storage is stack-backed, with capacity
12/// `R`. Neither the indices nor their count certify exact independence or rank.
13#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14#[must_use]
15pub struct ColumnBasis<const R: usize, const C: usize> {
16    columns: [usize; R],
17    rank: usize,
18}
19
20impl<const R: usize, const C: usize> ColumnBasis<R, C> {
21    /// Borrow selected column indices in increasing input-column order.
22    #[must_use]
23    pub const fn columns(&self) -> &[usize] {
24        self.columns.split_at(self.rank).0
25    }
26
27    /// Number of pivots accepted under the requested absolute tolerance.
28    ///
29    /// This is a provisional numerical rank, not an exact rank certificate,
30    /// including when the requested tolerance is zero.
31    #[must_use]
32    pub const fn rank(&self) -> usize {
33        self.rank
34    }
35}
36
37/// Select columns of a rectangular array by partial-pivoted elimination.
38///
39/// Columns are visited from left to right. At each step the largest absolute
40/// entry among remaining rows is the candidate pivot; ties select the first
41/// row in the current row order. A pivot is accepted only when its magnitude
42/// is **strictly greater** than `tolerance.get()`. Rejected columns are skipped;
43/// selection ends after all columns or `R` accepted pivots. Empty and zero
44/// matrices return rank zero; tall, square, and wide shapes are supported.
45///
46/// The tolerance is absolute, in input coefficient units. There is no automatic
47/// scaling or normalization. To use a relative policy, compute a finite absolute
48/// threshold from the original input scale and pass it through
49/// [`Tolerance::try_new`]. Rescaling rows or columns can change the selection.
50/// Elimination uses partial row pivoting and fused multiply-add updates
51/// (Gaussian elimination, `REFERENCES.md` \[11, 12\]). No absolute rounding-error
52/// bound is provided. Rounding, underflow, growth, and ill-conditioning can alter
53/// the selected columns and rank even at zero tolerance. The result is provisional:
54/// exact certification of a square selected basis remains the caller's job.
55/// Failure to find a pivot never produces an exact singularity claim.
56///
57/// The input is borrowed and preserved. A local `R × C` copy and `R` indices
58/// use stack storage; no allocation or optional feature is required. Dimensions
59/// have no dispatch cap. Work is `O(R (1 + C (1 + min(R, C))))`, including
60/// validation and initialization for empty shapes.
61///
62/// # Errors
63/// All inputs are checked in row-major order before elimination, including
64/// columns beyond a full basis. NaN/infinity returns [`LaError::NonFinite`]
65/// with input origin and the original matrix cell. A non-finite update returns
66/// the same error with [`ArithmeticOperation::ColumnBasis`] and its cell in the
67/// row-permuted work array. Insufficient numerical rank is a successful partial
68/// selection, not an error. No partially computed result is returned on error.
69///
70/// # Examples
71/// ```
72/// use la_stack::prelude::*;
73/// # fn main() -> Result<(), LaError> {
74/// let rows = [[0.0, 1.0, 2.0, 0.0], [0.0, 2.0, 4.0, 1.0]];
75/// let basis: ColumnBasis<2, 4> = column_basis(&rows, Tolerance::try_new(1e-12)?)?;
76/// assert_eq!(basis.columns(), &[1, 3]);
77/// assert_eq!(basis.rank(), 2);
78/// # Ok(())
79/// # }
80/// ```
81pub const fn column_basis<const R: usize, const C: usize>(
82    rows: &[[f64; C]; R],
83    tolerance: Tolerance,
84) -> Result<ColumnBasis<R, C>, LaError> {
85    let mut row = 0;
86    while row < R {
87        let mut col = 0;
88        while col < C {
89            if !rows[row][col].is_finite() {
90                return Err(LaError::non_finite_input_matrix(row, col));
91            }
92            col += 1;
93        }
94        row += 1;
95    }
96    let mut work = *rows;
97    let mut basis = ColumnBasis {
98        columns: [0; R],
99        rank: 0,
100    };
101    let mut col = 0;
102    while col < C && basis.rank < R {
103        let pivot_row = basis.rank;
104        let mut candidate = pivot_row;
105        row = pivot_row + 1;
106        while row < R {
107            if work[row][col].abs() > work[candidate][col].abs() {
108                candidate = row;
109            }
110            row += 1;
111        }
112        if work[candidate][col].abs() > tolerance.get() {
113            work.swap(pivot_row, candidate);
114            let pivot = work[pivot_row][col];
115            row = pivot_row + 1;
116            while row < R {
117                // Partial pivoting bounds the multiplier by one. A nonzero
118                // accepted finite pivot makes division finite, even if it underflows.
119                let factor = work[row][col] / pivot;
120                work[row][col] = 0.0;
121                let mut trailing = col + 1;
122                while trailing < C {
123                    let value = (-factor).mul_add(work[pivot_row][trailing], work[row][trailing]);
124                    if !value.is_finite() {
125                        return Err(LaError::non_finite_computation_matrix(
126                            ArithmeticOperation::ColumnBasis,
127                            row,
128                            trailing,
129                        ));
130                    }
131                    work[row][trailing] = value;
132                    trailing += 1;
133                }
134                row += 1;
135            }
136            basis.columns[basis.rank] = col;
137            basis.rank += 1;
138        }
139        col += 1;
140    }
141    Ok(basis)
142}