xingo_sparse/basis.rs
1//! # Chapter 5.1 — Basis columns
2//!
3//! ## Purpose
4//! The simplex method never forms its basis matrix `B` explicitly: `B` is a
5//! list of `m` references into the computational form `[A | ±I]`. This
6//! chapter fixes how the factorization receives that list, so that the
7//! simplex (Chapter 6) can call [`crate::LuFactor::factorize`] with its
8//! basis heading and the constraint matrix `A`, without copying a column.
9//!
10//! The factorization reads `B` through the trait [`ColumnSource`] (the
11//! contract's name): the pair `(&[ColumnRef], &CscMatrix)` of the simplex
12//! is one source, a square [`CscMatrix`] (`B` given explicitly) another.
13//! The trait is used by static dispatch only — `factorize` is generic in
14//! the source and the visitor of the entries is a generic closure — so the
15//! load of the factorization (step L1) costs what it cost in the first
16//! edition, where it was hard-wired to the pair.
17//!
18//! ## Notation
19//! `A ∈ R^{m×n}` is a [`CscMatrix`]. The basis is `B = [b_0 | … | b_{m−1}]`,
20//! and `k ∈ 0..m` is the *position* of a basic variable (the index of `x_B`
21//! it occupies). Each column is either
22//!
23//! * structural, `b_k = a_j = A e_j` ([`ColumnRef::Structural`]), or
24//! * logical, `b_k = σ e_i` with `σ ∈ {+1, −1}` ([`ColumnRef::Logical`]) —
25//! the column of the slack of row `i`; the sign lets the simplex use either
26//! convention (`A x − s = 0` gives `σ = −1`, `A x + s = b` gives `σ = +1`).
27//!
28//! Positions and rows are different index spaces even though both range over
29//! `0..m`: FTRAN maps a vector indexed by rows to one indexed by positions,
30//! BTRAN the other way round (Chapter 5.4).
31//!
32//! ## Invariants
33//! (B1) Structural indices are `< n`, logical rows are `< m`, and `A` has
34//! exactly `m` rows; checked by [`ColumnSource::validate`], which
35//! [`crate::LuFactor::factorize`] calls first, reporting a violation as
36//! [`crate::LuError::Structure`].
37//! Repeated references (the same column twice) are *not* an input error:
38//! they make `B` singular, which the factorization diagnoses.
39//!
40//! ## References
41//! Maros (2003), §5.1 (computational form and logical variables);
42//! Koberstein (2005), §2.2.
43
44use crate::real::Real;
45use crate::{Col, CscMatrix, Row, SparseError};
46
47/// Sign `σ` of a logical column `σ e_i`.
48#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
49pub enum Sign {
50 /// `σ = +1`.
51 Plus,
52 /// `σ = −1`.
53 Minus,
54}
55
56impl Sign {
57 /// The value `σ` as a float.
58 #[inline]
59 pub fn value(self) -> f64 {
60 match self {
61 Sign::Plus => 1.0,
62 Sign::Minus => -1.0,
63 }
64 }
65
66 /// The value `σ` in the scalar type `T`.
67 #[inline]
68 pub fn of<T: Real>(self) -> T {
69 T::from_f64(self.value())
70 }
71}
72
73/// A column of the basis: a structural column of `A` or a logical `σ e_i`.
74#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
75pub enum ColumnRef {
76 /// Column `j` of `A`.
77 Structural(Col),
78 /// The logical column `σ e_i` of row `i`.
79 Logical(Row, Sign),
80}
81
82impl ColumnRef {
83 /// Calls `f(i, b_ik)` for every stored entry of the column (in increasing
84 /// row order for structural columns).
85 ///
86 /// # Panics
87 /// If a structural index is out of range for `a`.
88 #[inline]
89 pub fn for_each_entry<T: Real>(self, a: &CscMatrix<T>, mut f: impl FnMut(u32, T)) {
90 match self {
91 ColumnRef::Structural(j) => {
92 let (rows, vals) = a.col(j);
93 for (&i, &v) in rows.iter().zip(vals) {
94 f(i, v);
95 }
96 }
97 ColumnRef::Logical(i, s) => f(i.0, s.of()),
98 }
99 }
100
101 /// Checks (B1) for this column of a basis of `A`.
102 pub(crate) fn validate<T: Real>(self, a: &CscMatrix<T>) -> Result<(), SparseError> {
103 match self {
104 ColumnRef::Structural(j) if j.index() >= a.n_cols() => {
105 Err(SparseError::IndexOutOfRange {
106 what: "structural basis column",
107 index: j.index(),
108 bound: a.n_cols(),
109 })
110 }
111 ColumnRef::Logical(i, _) if i.index() >= a.n_rows() => {
112 Err(SparseError::IndexOutOfRange {
113 what: "logical basis row",
114 index: i.index(),
115 bound: a.n_rows(),
116 })
117 }
118 _ => Ok(()),
119 }
120 }
121}
122
123/// Where the factorization reads the columns of `B` from (the contract's
124/// `ColumnSource`). Static dispatch only: [`crate::LuFactor::factorize`] is
125/// generic in the source, and [`ColumnSource::for_each_entry`] takes a
126/// generic visitor.
127pub trait ColumnSource<T: Real> {
128 /// Order `m` of `B` (its number of columns).
129 fn m(&self) -> usize;
130 /// Calls `f(i, b_ik)` for every stored entry of column `k`, rows
131 /// increasing for structural columns.
132 fn for_each_entry<F: FnMut(u32, T)>(&self, k: usize, f: F);
133 /// Upper bound on the nonzeros of `B` (an allocation hint).
134 fn nnz_hint(&self) -> usize;
135 /// Checks that the source describes a square `m × m` matrix whose
136 /// references are in range (invariant (B1)).
137 fn validate(&self) -> Result<(), SparseError>;
138}
139
140/// The simplex basis: the heading `cols` over `[A | ±I]`.
141impl<T: Real> ColumnSource<T> for (&[ColumnRef], &CscMatrix<T>) {
142 #[inline]
143 fn m(&self) -> usize {
144 self.0.len()
145 }
146 #[inline]
147 fn for_each_entry<F: FnMut(u32, T)>(&self, k: usize, f: F) {
148 self.0[k].for_each_entry(self.1, f);
149 }
150 fn nnz_hint(&self) -> usize {
151 self.0
152 .iter()
153 .map(|c| match *c {
154 ColumnRef::Structural(j) => {
155 self.1.col_ptr[j.index() + 1] - self.1.col_ptr[j.index()]
156 }
157 ColumnRef::Logical(..) => 1,
158 })
159 .sum()
160 }
161 fn validate(&self) -> Result<(), SparseError> {
162 let (basis, a) = *self;
163 if a.n_rows() != basis.len() {
164 return Err(SparseError::Dimension {
165 what: "basis (must have one column per row of A)",
166 expected: a.n_rows(),
167 found: basis.len(),
168 });
169 }
170 for c in basis {
171 c.validate(a)?;
172 }
173 Ok(())
174 }
175}
176
177/// An explicit square matrix `B`.
178impl<T: Real> ColumnSource<T> for CscMatrix<T> {
179 #[inline]
180 fn m(&self) -> usize {
181 self.n_cols()
182 }
183 #[inline]
184 fn for_each_entry<F: FnMut(u32, T)>(&self, k: usize, mut f: F) {
185 let (rows, vals) = self.col(Col::from_usize(k));
186 for (&i, &v) in rows.iter().zip(vals) {
187 f(i, v);
188 }
189 }
190 fn nnz_hint(&self) -> usize {
191 self.nnz()
192 }
193 fn validate(&self) -> Result<(), SparseError> {
194 if self.n_rows() == self.n_cols() {
195 Ok(())
196 } else {
197 Err(SparseError::Dimension {
198 what: "matrix B (must be square)",
199 expected: self.n_rows(),
200 found: self.n_cols(),
201 })
202 }
203 }
204}