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}