Skip to main content

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}