Skip to main content

xingo_sparse/
lib.rs

1//! # Chapter 5 — `xingo-sparse`: sparse linear algebra for optimization
2//!
3//! *Second edition (PLAN.md trilha L, task L1.2; D-0080 and its two
4//! addenda).* The first edition of this chapter was the crate
5//! `xingo-linalg`: the LU factorization of the simplex basis with
6//! Forrest–Tomlin updates and hypersparse solves. This edition moves it,
7//! together with the compressed containers of `xingo-core` (formerly
8//! Chapter 1.2), into a crate that does not depend on the solver and can
9//! be published on its own. **The migration has no numerical effect**:
10//! instantiated with `f64`, every operation below performs the same
11//! floating-point operations in the same order as the first edition, so
12//! the solver's results are bit for bit those of before (checked on the
13//! MIPLIB anchors and the `nl9` set, D-0080 addendum 2).
14//!
15//! ## 5.A The layer inversion
16//! Before: `xingo-linalg → xingo-core` (the factorization borrowed the
17//! containers, the typed indices, the error type and the tolerances of
18//! the solver's core). Now: `xingo-core → xingo-sparse`, and
19//! `xingo-sparse` depends on nothing in the workspace. Each borrowed item
20//! received a home here:
21//!
22//! | first edition (in `xingo-core`)          | second edition (here)                         |
23//! |------------------------------------------|-----------------------------------------------|
24//! | `CscMatrix`, `CsrMatrix`, `SparseVec`     | [`CscMatrix<T>`], [`CsrMatrix<T>`], [`SparseVec<T>`] (§5.8), `T = f64` by default |
25//! | `Col`, `Row` (Chapter 1.1)                | [`Col`], [`Row`] (§5.7), the same newtypes      |
26//! | `CoreError`                               | [`SparseError`] (containers) and [`LuError`] (factorization), §5.0 |
27//! | `Tolerances::{markowitz, zero}`           | [`LuOptions::rel_pivot_tol`], [`LuOptions::drop_tol`] |
28//! | `Tolerances::{lt, approx_eq, is_zero, …}` | the comparison helpers of §5.6 (crate-private)  |
29//!
30//! `xingo-core` re-exports the containers at their old paths as the `f64`
31//! instances (`xingo_core::CscMatrix = xingo_sparse::CscMatrix<f64>`, and
32//! so on), re-exports [`Col`] and [`Row`], and converts [`SparseError`] and
33//! [`LuError`] into its `CoreError`; the solver maps its `Tolerances` into
34//! [`LuOptions`] when it creates a factor.
35//!
36//! ## 5.B Scalars and genericity
37//! Every container and the factorization are generic in the scalar
38//! `T: `[`Real`] (§5.6), with `T = f64` the default type parameter, so
39//! `CscMatrix` written without parameters is `CscMatrix<f64>`. [`Real`] is
40//! the small trait a threshold-pivoting LU needs — the field operations, a
41//! total order on the values that occur (`PartialOrd`, NaN excluded by
42//! contract), `abs`, `max`, `is_finite`, conversion from and to `f64` —
43//! and is implemented here for `f64` only; `xingo-extprec` (trilha L2)
44//! will implement it for double-double and quad-double. Intervals are not
45//! `Real` (no total order, no single rounding; D-0080, Q2).
46//!
47//! *Genericity costs nothing* (D-0080, addendum 2): the code is generic
48//! by monomorphization only — no trait objects in the kernels — and every
49//! method of [`Real`] for `f64` is an `#[inline]` call of the inherent
50//! `f64` method, so `Lu<f64>` compiles to the same machine operations as
51//! the first edition's `Factor`. Options and statistics stay in `f64`
52//! (they are thresholds and counters, not data); a threshold enters the
53//! kernels through [`Real::from_f64`], the identity for `f64`.
54//!
55//! ## 5.C Comparisons
56//! The rule of the solver — "never compare `f64` outside
57//! `xingo_core::Tolerances`" — cannot hold in a crate below `xingo-core`.
58//! Its equivalent here: **every comparison of floating-point values goes
59//! through the helpers of §5.6** (`lt`, `le`, `approx_eq`, `is_zero`,
60//! `is_exact_zero`, the same definitions as `Tolerances`, generic in `T`),
61//! and every *threshold* of the factorization is a field of [`LuOptions`]
62//! — never a bare literal in a kernel. The two constants that remain are
63//! policy, documented where they are defined: the clearing fraction of
64//! [`SparseVec::clear`] (`0.3`, Chapter 5.8.3) and the moving-average
65//! weight [`HYPER_HISTORY_WEIGHT`] (Chapter 5.4).
66//!
67//! ## 5.D Contract and implementation
68//! The approved contract (`contracts/xingo-sparse/src/lib.rs`, L1.1) is the
69//! specification of the whole v0.1. This edition realizes the part listed
70//! for L1.2; its divergences from the contract are recorded in the cycle's
71//! decisions and listed for ratification.
72//!
73//! | contract                         | here                                                    |
74//! |----------------------------------|---------------------------------------------------------|
75//! | `Real`                           | [`Real`] (adds `Sum`, needed for bit-identical sums)     |
76//! | `CscMatrix<T>`, `CsrMatrix<T>`, `SparseVec<T>` | the same layouts; first-edition methods kept |
77//! | `Triplets<T>`, `BlockPartition` | [`Triplets`] (§5.8.4; `to_csc` keeps (C3): a zero *sum* is not stored, zero *inputs* are legal; + `from_csc`, `len`, `is_empty`, `clear`, `reserve`, `check_invariants`), [`BlockPartition`] (§5.8.5; + `new`, `from_sizes`, `check_invariants` and the block accessors) (L1.7a) |
78//! | `SparseError`, `LuError`         | [`SparseError`], [`LuError`] (variants of the first edition, see §5.0) |
79//! | `ColumnRef`, `Sign`, `ColumnSource<T>` | [`ColumnRef`], [`Sign`], [`ColumnSource`] (static dispatch) |
80//! | `LuOptions`, `LuStats`, `SolveStats`, `UpdateStats` | [`LuOptions`] (+ `drop_tol`), [`LuStats`], [`SolveStats`], [`UpdateStats`] |
81//! | `Lu<T, U = ForrestTomlin<T>>`, `LuFactor<T>`, `Factors<T>` | [`Lu`], [`LuFactor`], [`Factors`] |
82//! | `BasisUpdate<T>`, `ForrestTomlin<T>` | [`BasisUpdate`], [`ForrestTomlin`]                  |
83//! | `UpdateOutcome`, `RefactorReason` | [`UpdateOutcome`], [`RefactorReason`] (unchanged)      |
84//! | `Pattern`, `Perm`, `CscMatrix::{pattern, symmetric_pattern}` | [`Pattern`] (+ `transpose`, `symmetric`, `ata`, `cholesky_nnz`), [`Perm`] (L1.3) |
85//! | `Ordering`, `OrderingTarget`, `Natural`, `Amd`, `Colamd` | the same (L1.3; `Amd::default()` has aggressive absorption on) |
86//! | `PreOrder`, `LuOptions::pre_order`, `SymbolicLu`, `LuStats::symbolic_reuses` | [`PreOrder`], `Lu::{set_pre_order, pre_order}` (not an `LuOptions` field: see [`Lu::set_pre_order`]), [`SymbolicLu`] (+ `singletons`), `LuStats::{symbolic_reuses, symbolic_fallbacks}` (L1.3) |
87//! | `LuFactor::{refactorize_same_pattern, symbolic}` | the same (L1.3); the replay threshold `Lu::{set_refactor_pivot_tol, refactor_pivot_tol}` (L1.5b, default = `rel_pivot_tol`) |
88//! | `Equilibration`, `Scaling<T>` | [`Equilibration`], [`Scaling`] (+ `check_invariants`; §5.13, L1.5) |
89//! | `Inertia`, `Regularization<T>`, `Refinement<T>`, `RefineReport<T>`, `KktForm`, `KktSolver<T>`, `KktError` | the same (§5.14, L1.5), plus `Default` for the option structs, `Regularization::{none, relative_to, is_quasi_definite}`, `KktForm::expected_inertia`, `KktError::Structure`, [`EQUILIBRATION_PASSES`]; the augmented inertia reads `{positive: m, negative: n}`; L1.5b: `Regularization::scale` with [`ShiftScale`] (shifts in the equilibrated matrix by default), `KktError::NotFinite`, [`RefineWorkspace`] and `KktSolver::solve_refined_with`, refinement by GMRES |
90//! | `FaerLdl`, `FaerSymbolic`, `FaerNumeric<T>` (feature `faer`) | the same (§5.15, L1.5), plus read-only accessors |
91//! | `DenseBlock<T>`, `DenseKernel<T>`, `NaiveDense`, `FaerDense`, `DenseError` | the same (§5.16, L1.5); `sytrf` also returns its permutation and `2×2` marks |
92//!
93//! ## 5.E What the tasks add
94//! * **L1.3** (done) — [`Perm`] and [`Pattern`] (§5.9), the orderings
95//!   [`Natural`], [`Amd`] and [`Colamd`] behind [`Ordering`] (§5.10, our
96//!   own; their engine, Algorithm Q, in §5.11), the reusable pivot
97//!   sequence [`SymbolicLu`] with [`LuFactor::refactorize_same_pattern`]
98//!   and the COLAMD pre-order of the bump [`Lu::set_pre_order`] (§5.12,
99//!   step L5′ of §5.3). With `pre_order = None` (the default) Algorithm L
100//!   is unchanged, bit for bit; the simplex uses none of it yet.
101//! * **L1.4** (after P3.9) — `BartelsGolub<T>`, the second
102//!   [`BasisUpdate`]; the trait already separates the update policy from
103//!   the stored factors ([`Factors`]), so the simplex will not change.
104//! * **L1.5** (done) — row/column scaling [`Scaling`] by the policies of
105//!   [`Equilibration`] (§5.13); the KKT interface [`KktSolver`] with the
106//!   regularization policy and the iterative refinement shared by every
107//!   backend (§5.14); its v0.1 backend [`FaerLdl`] over faer's sparse
108//!   `LDLᵀ`/`LBLᵀ` ordered by our [`Amd`] (§5.15, feature `faer`); and the
109//!   dense kernels [`DenseKernel`], [`NaiveDense`] and `FaerDense`
110//!   (§5.16). The simplex uses none of it, and faer is not in the
111//!   solver's build: the workspace depends on this crate without default
112//!   features.
113//! * **L1.5b** (done) — before 0.1.0: a default regularization that holds
114//!   at the end of an interior-point method (shifts in the equilibrated
115//!   matrix, §5.14), refinement by right-preconditioned GMRES with
116//!   reusable work vectors, non-finite factors reported, solves without
117//!   allocation and exact bump counts (§5.15), the elimination-tree
118//!   postorder before faer's supernodes (§5.9, Algorithm P), a faster
119//!   Algorithm Q (§5.11), and the replay threshold of Algorithm R (§5.12).
120//! * **L1.7a** (done) — what 0.1.0 ships besides the code: the last two
121//!   containers of the contract, the coordinate builder [`Triplets`]
122//!   (§5.8.4, Algorithm C′) and [`BlockPartition`] (§5.8.5, Algorithm B);
123//!   the crate's README, CHANGELOG and three runnable examples
124//!   (`examples/`: a basis LU with a Forrest–Tomlin update, the orderings
125//!   against the natural order, one KKT step of an interior-point method);
126//!   complete item documentation. No solver path changes (nothing uses the
127//!   two new types yet).
128//! * **L1.6** — the own `SupernodalLdl<T, K>`, after the interior-point
129//!   method has measured faer.
130//!
131//! ## Purpose (the first edition, unchanged)
132//! The revised simplex method (Chapter 6) works with the basis matrix `B`
133//! only through three operations: factorize `B`, solve with `B` or `Bᵀ`,
134//! and replace one column of `B`. This crate provides them
135//! (`PLAN.md` §5 items 1–2, tasks P1.1–P1.2):
136//!
137//! * §5.0 [`error`] — [`SparseError`], [`LuError`], [`KktError`] and
138//!   [`DenseError`];
139//! * §5.1 [`basis`] — how the basis is named: [`ColumnRef`] into `[A | ±I]`,
140//!   and the general [`ColumnSource`];
141//! * §5.2 (private) — dynamic row/column files and count lists;
142//! * §5.3 [`factor`] — [`Lu`] and [`LuFactor::factorize`], Algorithm L:
143//!   singleton (triangular) phase, then Markowitz pivoting with a relative
144//!   threshold on the bump (Suhl & Suhl 1990);
145//! * §5.4 [`solve`] — [`LuFactor::ftran`] and [`LuFactor::btran`]: the
146//!   dense Algorithms F and B and the hypersparse Algorithms HF and HB
147//!   (Gilbert–Peierls reach), chosen per call by the density policy of
148//!   Hall & McKinnon (task P1.4);
149//! * §5.5 [`update`] — [`LuFactor::update`] through the [`BasisUpdate`]
150//!   policy, the Forrest–Tomlin update [`ForrestTomlin`] (Algorithm U), and
151//!   the refactorization policy ([`UpdateOutcome`]);
152//! * §5.6 [`real`] — the scalar trait [`Real`] and the comparisons;
153//! * §5.7 [`index`] — the typed indices [`Col`] and [`Row`];
154//! * §5.8 [`compressed`], [`csc`], [`csr`], [`vector`] — the containers
155//!   (Algorithms T, C and S; Chapter 1.2 of the first edition); [`triplets`]
156//!   — the coordinate builder [`Triplets`] (Algorithm C′); [`partition`] —
157//!   [`BlockPartition`] (Algorithm B);
158//! * §5.9 [`perm`] — [`Perm`] and [`Pattern`], and the Cholesky count of an
159//!   ordering (Algorithm E);
160//! * §5.10 [`ordering`] — [`Ordering`], [`Natural`], [`Amd`] (Algorithm A),
161//!   [`Colamd`] (Algorithm C);
162//! * §5.11 (private) — approximate minimum degree on a quotient graph
163//!   (Algorithm Q), the engine of both;
164//! * §5.12 [`symbolic`] — [`SymbolicLu`] and Algorithm R
165//!   ([`LuFactor::refactorize_same_pattern`]);
166//! * §5.13 [`scaling`] — [`Equilibration`] and [`Scaling`] (Algorithms G,
167//!   R and I);
168//! * §5.14 [`kkt`] — [`KktSolver`] and its shared policy: regularization
169//!   (Algorithm K) and iterative refinement (Algorithm F);
170//! * §5.15 `faer_ldl` (feature `faer`) — [`FaerLdl`] (Algorithms A, N, S);
171//! * §5.16 [`dense`] — [`DenseKernel`], [`NaiveDense`], `FaerDense`
172//!   (Algorithms P, B, T, G, S).
173//!
174//! ## Notation
175//! `B ∈ R^{m×m}`; *rows* `i` and basis *positions* `k` both range over
176//! `0..m` but are distinct index spaces. `P B Q = L U` with `P`, `Q`
177//! permutations, `L` unit lower and `U` upper triangular; after `t`
178//! updates `B = Pᵀ L R₁⁻¹ ⋯ R_t⁻¹ U Qᵀ` with row etas `R_e` and updated
179//! `P`, `Q`, `U`. Identifiers follow the chapters: `l_ic`, `u_pj`, `mu`
180//! (`μ`), `alpha_r` (`α_r`), `tau` (`τ`), `sigma` (`σ`). Indices are `u32`
181//! in the stored arrays, pointers `usize` (PLAN.md §3.2; D-0080, Q3).
182//!
183//! ## Contract with the simplex method
184//! 1. `lu.factorize(&(&heading[..], &a))` (a [`ColumnSource`]); on
185//!    [`LuError::Singular`] replace each `cols[k]` by
186//!    `ColumnRef::Logical(rows[k], _)` and factorize again.
187//! 2. `ftran(&mut v)` maps a row-indexed `v` to the position-indexed
188//!    `B⁻¹ v`; `btran(&mut v)` maps a position-indexed `v` to the
189//!    row-indexed `B⁻ᵀ v`. Both take `&mut self`: the factor owns the work
190//!    arrays of the solves and the density statistics that choose between
191//!    the dense and the hypersparse algorithms. Give the right-hand side in
192//!    sparse mode with a correct pattern to allow the hypersparse
193//!    algorithms; the result comes back in sparse mode, its pattern a
194//!    superset of its support (Chapter 5.4, invariant (V1)).
195//! 3. To pivot position `r` out for column `a_q`:
196//!    `ftran_for_update(&mut a_q)` (giving `α_q`), then
197//!    `update(r, α_q[r])`. On [`UpdateOutcome::RefactorNeeded`] (or any
198//!    error) refactorize with the new heading; the factor never does so on
199//!    its own (D-0016).
200//!
201//! ## Safety
202//! Per D-0006 this is the one crate where `unsafe` is permitted (audited,
203//! `// SAFETY:` comments, Miri where executable). This edition, like the
204//! first, uses none; optimized kernels wait for measurements (task P1.10).
205//! The `faer` feature (L1.5) brings faer's own `unsafe` kernels in as a
206//! dependency; the code of this crate stays free of `unsafe`.
207//!
208//! ## References
209//! Suhl & Suhl (1990); Forrest & Tomlin (1972); Maros, *Computational
210//! Techniques of the Simplex Method* (2003), ch. 5 and 8; Davis, *Direct
211//! Methods for Sparse Linear Systems* (2006); Duff, Erisman & Reid (1986);
212//! Koberstein (2005), ch. 5; Gilbert & Peierls (1988); Hall & McKinnon
213//! (2005); Gustavson (1978); Amestoy, Davis & Duff (1996); Davis, Gilbert,
214//! Larimore & Ng (2004); the contract of the crate
215//! (`contracts/xingo-sparse`, D-0080).
216
217// Without `faer`, the chapter text still names FaerLdl/FaerDense.
218#![cfg_attr(not(feature = "faer"), allow(rustdoc::broken_intra_doc_links))]
219
220pub mod basis;
221pub mod compressed;
222pub mod csc;
223pub mod csr;
224pub mod dense;
225pub mod error;
226pub mod factor;
227#[cfg(feature = "faer")]
228pub mod faer_ldl;
229pub mod index;
230pub mod kkt;
231pub mod ordering;
232pub mod partition;
233pub mod perm;
234mod qgraph;
235pub mod real;
236pub mod scaling;
237pub mod solve;
238mod store;
239pub mod symbolic;
240pub mod triplets;
241pub mod update;
242pub mod vector;
243
244#[cfg(test)]
245mod tests;
246
247/// The code blocks of the crate's README, compiled and run as doc-tests
248/// (`cargo test --doc`) so that the README never drifts from the API. The
249/// README is not the root documentation — this chapter is — and its KKT
250/// snippet needs `faer`, hence the gate.
251#[cfg(all(doctest, feature = "faer"))]
252#[doc = include_str!("../README.md")]
253pub struct ReadmeDoctests;
254
255pub use basis::{ColumnRef, ColumnSource, Sign};
256pub use csc::CscMatrix;
257pub use csr::CsrMatrix;
258#[cfg(feature = "faer")]
259pub use dense::FaerDense;
260pub use dense::{DenseBlock, DenseKernel, NaiveDense};
261pub use error::{DenseError, KktError, LuError, SparseError};
262pub use factor::{Factors, Lu, LuFactor, LuOptions, LuStats, PreOrder};
263#[cfg(feature = "faer")]
264pub use faer_ldl::{FaerLdl, FaerNumeric, FaerSymbolic};
265pub use index::{Col, Row};
266pub use kkt::{
267    EQUILIBRATION_PASSES, Inertia, KktForm, KktSolver, RefineReport, RefineWorkspace, Refinement,
268    Regularization, ShiftScale,
269};
270pub use ordering::{Amd, Colamd, Natural, Ordering, OrderingTarget};
271pub use partition::BlockPartition;
272pub use perm::{Pattern, Perm};
273pub use real::Real;
274pub use scaling::{Equilibration, Scaling};
275pub use solve::{HYPER_HISTORY_WEIGHT, SolveStats};
276pub use symbolic::SymbolicLu;
277pub use triplets::Triplets;
278pub use update::{BasisUpdate, ForrestTomlin, RefactorReason, UpdateOutcome, UpdateStats};
279pub use vector::SparseVec;