Skip to main content

xingo_sparse/
factor.rs

1//! # Chapter 5.3 — Sparse LU factorization of the basis: [`Lu`]
2//!
3//! ## Purpose
4//! Every simplex iteration solves two or three linear systems with the basis
5//! matrix `B` (Chapter 5.4). We factorize `B` once into sparse triangular
6//! factors and keep them across iterations, modified by the Forrest–Tomlin
7//! update (Chapter 5.5) until a fresh factorization is cheaper or safer.
8//! Simplex bases are special: most of their columns are logical or nearly
9//! triangular, so most pivots can be found *without any arithmetic* by
10//! peeling off singletons; only a small *bump* (the kernel) needs genuine
11//! Gaussian elimination, done with Markowitz pivoting under a threshold.
12//!
13//! ## Notation
14//! `B ∈ R^{m×m}` has rows `i` and positions `k` (Chapter 5.1). The
15//! factorization is a sequence of `m` *steps*; step `s` pivots on the entry
16//! `(p_s, q_s)` (row `p_s`, position `q_s`). With the permutation matrices
17//! `P` (row `s` of `PB` is row `p_s` of `B`) and `Q` (column `s` of `BQ` is
18//! column `q_s` of `B`) we compute
19//! ```text
20//!   P B Q = L U,
21//! ```
22//! `L` unit lower triangular and `U` upper triangular. We store both in the
23//! *original* indices, never permuting values:
24//!
25//! * `L` as one column eta per step with a nonzero subdiagonal: the pivot
26//!   row `p_s` and the multipliers `l_is` for rows `i` pivoted later;
27//! * `U` as its diagonal `u_{p_s q_s}` (indexed by the row `p_s`) and its
28//!   off-diagonal entries `u_ij` (row `i`, position `j`, with `step(i) <
29//!   step(j)`), held *twice*: by rows and by columns. FTRAN needs `U` by
30//!   columns, BTRAN by rows, and the Forrest–Tomlin update must delete a
31//!   column *and* a row of `U` — so both copies are dynamic
32//!   (Chapter 5.2) and are kept identical.
33//!
34//! The *active submatrix* `S` during the elimination has row counts `r_i`
35//! and column counts `c_j`. A *column singleton* is a column with `c_j = 1`,
36//! a *row singleton* a row with `r_i = 1`. The Markowitz merit of a
37//! candidate pivot is `(r_i − 1)(c_j − 1)`, an upper bound on the fill it
38//! creates. With the threshold `u` ([`LuOptions::rel_pivot_tol`] `∈ (0,
39//! 1]`), an entry is *acceptable* iff `|s_ij| ≥ u · max_k |s_kj|` and
40//! `|s_ij| > τ_abs` ([`LuOptions::abs_pivot_tol`]). Entries of magnitude
41//! at most `ε_drop` ([`LuOptions::drop_tol`]) are negligible.
42//!
43//! ## Algorithm L (LU factorization of a simplex basis)
44//! Given the columns of `B` through a [`crate::ColumnSource`] (in the
45//! simplex method: the basis heading and `A`).
46//!
47//! L1. [Load.] Validate the source (Chapter 5.1, B1). Build `S = B` in
48//!     a column file with values and a row file with the pattern only;
49//!     record `nnz(B)` and `max |b_ij|`. Bucket columns and rows by count.
50//! L2. [Empty columns.] Every active column with `c_j = 0` is *dead*: it
51//!     can never be pivoted. Remove it (its position joins the deficiency
52//!     list).
53//! L3. [Column singleton.] If some column `j` has `c_j = 1`, with entry
54//!     `(i, v)`: if `|v| > τ_abs` pivot on `(i, j)` (step L6) — the `L`
55//!     column of this step is empty and row `i` is copied to `U` — else
56//!     declare `j` dead. Go to L2.
57//! L4. [Row singleton.] Otherwise, if some row `i` with `r_i = 1` has its
58//!     only entry `(i, j)` above `τ_abs`, pivot on it (step L6) — the `U` row
59//!     is empty, the multipliers `s_kj / s_ij` form the `L` column, and no
60//!     entry of `S` is updated. Go to L2. Steps L3–L4 constitute the
61//!     *triangular phase*: no arithmetic beyond one division per `L` entry.
62//!     Singleton pivots are forced (the only entry of their row or column in
63//!     any completion of the elimination), so no threshold is applied to
64//!     them; they cannot create growth in `U`.
65//! L5. [Markowitz.] Otherwise (the *bump*), remove the empty columns as in
66//!     L2; if no active column remains, go to L7. Run Algorithm M to obtain
67//!     a pivot and a list of dead columns; remove the dead columns, pivot
68//!     (step L6) and repeat L5.
69//! L5′. [Pre-ordered bump; only with [`Lu::set_pre_order`]`(Some(PreOrder::Colamd))`,
70//!     task L1.3.] Instead of L5: compute the
71//!     COLAMD order (Chapter 5.10, Algorithm C) of the columns of the bump
72//!     on its pattern; for each column `c` in that order that is still
73//!     active (after removing the empty columns as in L2): if `γ_c =
74//!     max_i |s_ic| ≤ τ_abs` declare `c` dead; otherwise pivot (step L6) on
75//!     the acceptable entry `(i, c)` of least row count `r_i` (ties: the
76//!     larger `|s_ic|`, then the first in the column) — threshold partial
77//!     pivoting inside a fixed column order, as UMFPACK and SuperLU do.
78//!     With `pre_order = None` step L5 runs exactly as before, so the
79//!     factors are bit for bit those of the second edition.
80//! L6. [Pivot on `(p, c)`.] Append the step `(p, c)` and the diagonal
81//!     `u_pc = s_pc`. For every other `(i, s_ic)` of column `c` record
82//!     `l_ic = s_ic / s_pc`; for every other `(p, s_pj)` of row `p` record
83//!     `u_pj = s_pj`. Remove row `p` and column `c` from `S`. Then, for each
84//!     `j` of the `U` row, find the positions in column `j` of the rows of
85//!     the `L` column (one pass over column `j` against a map of those
86//!     rows) and set `s_ij ← s_ij − l_ic u_pj` for every `i` of the `L`
87//!     column, in its order, appending fill-in to both files; entries that
88//!     cancel to `|s_ij| ≤ ε_drop` are dropped, in the order of column `j`.
89//!     Only the entries just updated or filled can cancel — plus, the first
90//!     time column `j` is updated, entries of `B` that were negligible from
91//!     the start, so such a column is scanned whole once. Update the counts
92//!     of the touched rows and columns.
93//! L7. [Finish.] If any column is dead, the basis is singular: report its
94//!     positions and the rows that never received a pivot
95//!     ([`LuError::Singular`]). Otherwise build the row and column
96//!     copies of `U`, the inverse step maps, and the statistics, including
97//!     the growth factor `max |u_ij| / max |b_ij|` and `max |u_ij|` itself,
98//!     the reference of the update policy (Chapter 5.5). Record the pivot
99//!     sequence `(p_s, q_s)` as the [`SymbolicLu`] of Chapter 5.12 (into
100//!     the arrays of the previous one: `2m` copies, no allocation once
101//!     warm). Step L7 reads only the output of the elimination (`Elim`),
102//!     so Algorithm R of Chapter 5.12 shares it.
103//!
104//! ## Algorithm M (Markowitz search with threshold)
105//! Let `best` be the acceptable entry of least merit found so far (ties go
106//! to the larger ratio `|s_ij| / max_k |s_kj|`), and `e` the number of lines
107//! examined that contained an acceptable entry.
108//!
109//! M1. For `k = 1, 2, …, m`:
110//! M2. [Columns of count `k`.] For each active column `j` with `c_j = k`:
111//!     compute `γ_j = max_i |s_ij|`; if `γ_j ≤ τ_abs`, mark `j` dead and go
112//!     on. Otherwise evaluate every acceptable entry with merit
113//!     `(r_i − 1)(k − 1)`. Every unexamined candidate now lies in a row with
114//!     `r_i ≥ k` (rows of smaller count were examined whole) and a column
115//!     with `c_j ≥ k`, so its merit is at least `(k − 1)²`: stop if `best`
116//!     reaches that bound, or if `e ≥ search_limit` and `best` exists.
117//! M3. [Rows of count `k`.] The same for each active row `i` with `r_i = k`,
118//!     scanning its columns `j` (merit `(k − 1)(c_j − 1)`); the bound for
119//!     unexamined candidates is now `k (k − 1)`.
120//! M4. Return `best` (possibly none) and the dead columns.
121//!
122//! The search limit (default 4, [`LuOptions::search_limit`]) is Zlatev's
123//! restriction: the merit found after a few lines is almost always the
124//! minimum, and the full search would cost `O(nnz(S))` per pivot. If M
125//! returns no pivot and no dead column, every active column was examined
126//! and found empty, so L5 terminates.
127//!
128//! ## Invariants
129//! Checked by [`LuFactor::check_invariants`] in the tests and after every
130//! factorization in debug builds.
131//! (F1) The non-vacant slots of `(p_s)` and `(q_s)` are permutations of
132//!      `0..m`, and the inverse maps `step(p_s) = step(q_s) = s` agree with
133//!      them (right after a factorization there are no vacant slots; each
134//!      update adds one, Chapter 5.5).
135//! (F2) Every `L` multiplier `l_is` of step `s` has `step(i) > s`, where
136//!      `step` is the order of the last factorization (updates reorder `U`
137//!      but leave `L` alone).
138//! (F3) Every off-diagonal `u_ij` has `step(i) < step(j)`; the row and
139//!      column copies of `U` hold exactly the same entries; every diagonal
140//!      entry is nonzero.
141//! (F4) Right after a factorization, `L U = P B Q` up to rounding; tested
142//!      by property tests against the dense product.
143//!
144//! ### The two files of `S` (task P3.37)
145//! The column file and the row file of `S` are *cross-linked* (Chapter 5.2,
146//! Algorithm T): every entry holds a hint of the position of its twin in
147//! the other file, exact in long lines. Every deletion of L2, L6 and of the
148//! cancellations of L6, and every lookup of `s_ij` from the row side (L4,
149//! M3), is then `O(1)` in a long line instead of a linear search along it
150//! (short lines are still searched, which is cheaper than keeping their
151//! hints exact). The deletions keep the lines in the same
152//! order as the searches did, so the elimination — pivots, fill, the order
153//! of `L` and `U` — is the same, bit for bit. Invariant (S3) of Chapter 5.2
154//! is checked after the load and after the triangular phase in debug
155//! builds.
156//!
157//! ## The objects (second edition)
158//! [`Lu<T, U>`](Lu) is the factorization object of the contract: the
159//! options [`LuOptions`], the stored factors [`Factors<T>`] (the steps,
160//! `L`, `U` by rows and by columns, the spike, the work arrays and the
161//! statistics [`LuStats`]), the basis-update policy `U` (Chapter 5.5,
162//! [`crate::ForrestTomlin`] by default, which owns the row etas) and the
163//! moving averages of the density policy (Chapter 5.4). The simplex method
164//! programs against the trait [`LuFactor`]. In the first edition all of
165//! this was one struct, `Factor`; splitting it moves no arithmetic: the
166//! update reaches the factors through `&mut Factors<T>`, and the type
167//! parameter `U` is resolved at compile time.
168//!
169//! `LuOptions` merges the first edition's `FactorOptions` with the two
170//! tolerances the factorization borrowed from the solver's `Tolerances`:
171//! `markowitz` became [`LuOptions::rel_pivot_tol`] and `zero` became
172//! [`LuOptions::drop_tol`] (the solver copies them in when it creates a
173//! factor).
174//!
175//! ## Complexity
176//! The triangular phase costs `O(nnz(B))` plus one division per `L` entry.
177//! A Markowitz pivot costs `O(Σ_j c_j)` over the columns of its `U` row,
178//! plus the bounded search. (Before task P3.37 every deletion also searched
179//! its line, `O(Σ_i r_i²)` per factorization: on `hypothyroid-k1` half the
180//! time of a factorization.)
181//!
182//! ## References
183//! Markowitz, "The elimination form of the inverse and its application to
184//! linear programming", Management Sci. 3, 1957; Suhl & Suhl, "Computing
185//! sparse LU factorizations for large-scale linear programming bases", ORSA
186//! J. Comput. 2(4), 1990 (triangular phase, count lists, search);
187//! Zlatev, "On some pivotal strategies in Gaussian elimination by sparse
188//! technique", SIAM J. Numer. Anal. 17, 1980 (restricted search);
189//! Maros (2003), ch. 5 (§5.4 LU of the basis) and ch. 8 (§8.2 bump);
190//! Davis, *Direct Methods for Sparse Linear Systems* (2006), ch. 6; Duff,
191//! Erisman & Reid (1986), ch. 7 and §9.4.
192
193use crate::basis::ColumnSource;
194use crate::error::LuError;
195use crate::real::{Real, approx_eq, is_exact_zero, is_zero, le_exact, lt};
196use crate::solve::{Kind, NO_ETA, SolveStats, Workspace};
197use crate::store::{CountLists, LineStore, NIL, check_links, remove_linked};
198use crate::symbolic::SymbolicLu;
199use crate::update::{BasisUpdate, ForrestTomlin, UpdateOutcome, UpdateStats};
200use crate::{Colamd, Ordering, OrderingTarget, Pattern, Row, SparseError, SparseVec};
201
202/// Options of the factorization, of the solves and of the update policy.
203///
204/// The names follow the contract (D-0080); the defaults are those of the
205/// first edition (`FactorOptions` and the two fields of the solver's
206/// `Tolerances` it read), so a default [`Lu`] behaves exactly as the
207/// first edition's default `Factor`.
208#[derive(Clone, Copy, Debug, PartialEq)]
209pub struct LuOptions {
210    /// Absolute pivot tolerance `τ_abs`: no pivot of magnitude at or below
211    /// it is accepted, in the factorization or in an update (`1e-10`).
212    ///
213    /// Absolute, in the units of `B`: an unscaled matrix whose columns
214    /// hold only tiny entries is declared singular. The Harwell–Boeing
215    /// matrix sherman3 has 2 107 of its 5 005 columns with largest entry
216    /// `10⁻¹⁰` (inactive cells of a reservoir model) and is reported with
217    /// rank deficiency 2 107, while SuperLU, which has no absolute
218    /// threshold, factorizes it. Scale first ([`crate::Scaling`]: with
219    /// `Geometric { max_iters: 4, power_of_two: true }` it factorizes with
220    /// the defaults, residual `5·10⁻¹²`), or lower `τ_abs` (`10⁻¹²`:
221    /// residual `7·10⁻¹⁷`; task L1.5b, D9). The simplex method scales its
222    /// computational form when it is badly scaled (`ScaleMode::Auto`,
223    /// Chapter 6.9) before it factorizes.
224    pub abs_pivot_tol: f64,
225    /// Relative threshold `u ∈ (0, 1]` of Markowitz threshold pivoting
226    /// (`1e-2`; the solver's `Tolerances::markowitz`).
227    pub rel_pivot_tol: f64,
228    /// Number of lines with an acceptable entry after which the Markowitz
229    /// search stops (`4`, Zlatev's restriction; at least 1).
230    pub search_limit: usize,
231    /// Growth limit (`1e10`): the factor is flagged
232    /// ([`LuStats::growth_exceeded`]) when `max |u_ij| / max |b_ij|`
233    /// exceeds it, and an update asks for a refactorization when the
234    /// growth *added by the updates*, [`UpdateStats::update_growth`],
235    /// exceeds it (Chapter 5.5, refactorization policy).
236    pub growth_limit: f64,
237    /// Fill trigger: an update asks for a refactorization when
238    /// `nnz(L) + nnz(U) + nnz(etas)` exceeds this multiple of
239    /// `nnz(L) + nnz(U)` right after the factorization (`3.0`).
240    pub fill_factor: f64,
241    /// Number of updates after which a refactorization is requested
242    /// (`100`).
243    pub refactor_interval: usize,
244    /// Relative tolerance of the Forrest–Tomlin stability test: the new
245    /// diagonal must agree with `α_r · u_old` within this relative
246    /// distance (`1e-8`).
247    pub update_stability_tol: f64,
248    /// Largest right-hand side density (`count/m`) for which a solve may
249    /// use the hypersparse algorithms (Chapter 5.4, policy (b); `0.10`).
250    /// A negative value disables them.
251    pub hyper_rhs_density: f64,
252    /// Largest expected result density for which a solve may use the
253    /// hypersparse algorithms (Chapter 5.4, policy (c); `0.10`).
254    pub hyper_result_density: f64,
255    /// Drop tolerance `ε_drop`: magnitudes at or below it are treated as
256    /// zero by the elimination and the update (`1e-12`; the solver's
257    /// `Tolerances::zero`).
258    pub drop_tol: f64,
259}
260
261/// Column pre-orders available to the dynamic LU ([`Lu::set_pre_order`];
262/// the contract's `PreOrder`, which it placed in `LuOptions::pre_order`).
263#[derive(Clone, Copy, Debug, PartialEq, Eq)]
264pub enum PreOrder {
265    /// [`crate::Colamd`] with its defaults on the columns of the bump.
266    Colamd,
267}
268
269impl Default for LuOptions {
270    fn default() -> Self {
271        Self {
272            abs_pivot_tol: 1e-10,
273            rel_pivot_tol: 1e-2,
274            search_limit: 4,
275            growth_limit: 1e10,
276            fill_factor: 3.0,
277            refactor_interval: 100,
278            update_stability_tol: 1e-8,
279            hyper_rhs_density: 0.10,
280            hyper_result_density: 0.10,
281            drop_tol: 1e-12,
282        }
283    }
284}
285
286impl LuOptions {
287    /// `search_limit ≥ 1`, `rel_pivot_tol ∈ (0, 1]`, and the tolerances
288    /// finite and non-negative (asserted in debug builds when the options
289    /// are installed).
290    fn is_valid(&self) -> bool {
291        let finite_nonneg = |x: f64| x.is_finite() && !lt(x, 0.0);
292        self.search_limit >= 1
293            && lt(0.0, self.rel_pivot_tol)
294            && !lt(1.0, self.rel_pivot_tol)
295            && finite_nonneg(self.abs_pivot_tol)
296            && finite_nonneg(self.drop_tol)
297            && finite_nonneg(self.update_stability_tol)
298    }
299}
300
301/// Statistics of the current factor, for logs and the phase-1 benchmark.
302/// The update counters are in [`UpdateStats`].
303#[derive(Clone, Copy, Debug, Default, PartialEq)]
304pub struct LuStats {
305    /// Dimension `m` of the basis.
306    pub m: usize,
307    /// Nonzeros of `B` at the last factorization.
308    pub nnz_basis: usize,
309    /// Stored multipliers of `L` (its unit diagonal is implicit).
310    pub nnz_l: usize,
311    /// Entries of `U`, including the `m` diagonal entries.
312    pub nnz_u: usize,
313    /// Fill-in `nnz_l + nnz_u + nnz_eta − nnz_basis` (saturating at 0;
314    /// `nnz_eta` from [`UpdateStats`]).
315    pub fill_in: usize,
316    /// Pivots taken as column singletons in the triangular phase (L3).
317    pub col_singletons: usize,
318    /// Pivots taken as row singletons in the triangular phase (L4).
319    pub row_singletons: usize,
320    /// Pivots chosen by the Markowitz search (L5): the size of the bump.
321    pub markowitz_pivots: usize,
322    /// Growth factor `max_u / max_basis`.
323    pub growth: f64,
324    /// Successful factorizations performed by this object.
325    pub factorizations: usize,
326    /// FTRAN calls (Chapter 5.4), accumulated over the life of the object.
327    pub ftran: SolveStats,
328    /// BTRAN calls, accumulated over the life of the object.
329    pub btran: SolveStats,
330    /// FTRAN-for-update calls, accumulated over the life of the object.
331    pub ftran_update: SolveStats,
332    /// `max |b_ij|` at the last factorization.
333    pub max_basis: f64,
334    /// Largest `|u_ij|` ever stored since the last factorization.
335    pub max_u: f64,
336    /// `max |u_ij|` right after the last factorization (before any
337    /// update).
338    pub max_u_fresh: f64,
339    /// `growth > LuOptions::growth_limit`; a diagnostic of the
340    /// factorization itself (a fresh factor may already be flagged).
341    pub growth_exceeded: bool,
342    /// Refactorizations that reused the pivot sequence of the previous
343    /// factorization ([`LuFactor::refactorize_same_pattern`], Algorithm R
344    /// of Chapter 5.12), accumulated over the life of the object.
345    pub symbolic_reuses: usize,
346    /// Calls of [`LuFactor::refactorize_same_pattern`] that fell back to a
347    /// full factorization (no sequence, another order `m`, or a pivot
348    /// refused by the threshold test), accumulated over the life of the
349    /// object.
350    pub symbolic_fallbacks: usize,
351}
352
353/// The stored factors `P, L, U, Q` of a basis (row and column files,
354/// MA28/Suhl style), the recorded spike, the work arrays of the solves
355/// and the statistics. Opaque: the basis-update policies reach it through
356/// [`BasisUpdate`].
357#[derive(Clone, Debug)]
358pub struct Factors<T: Real> {
359    pub(crate) m: usize,
360    pub(crate) valid: bool,
361    /// `p_s`: pivot row of step `s`.
362    pub(crate) step_row: Vec<u32>,
363    /// `q_s`: pivot position of step `s`.
364    pub(crate) step_col: Vec<u32>,
365    /// `step(i)` for rows.
366    pub(crate) row_step: Vec<usize>,
367    /// `step(j)` for positions.
368    pub(crate) col_step: Vec<usize>,
369    /// `step(i)` for rows at the last factorization: the order of `L`,
370    /// which the updates do not change.
371    pub(crate) l_row_step: Vec<usize>,
372    /// `L` etas: eta `e` has pivot row `l_pivot[e]` and multipliers
373    /// `l_index/l_value[l_start[e]..l_start[e+1]]`.
374    pub(crate) l_start: Vec<usize>,
375    pub(crate) l_pivot: Vec<u32>,
376    pub(crate) l_index: Vec<u32>,
377    pub(crate) l_value: Vec<T>,
378    /// Index of the `L` eta whose pivot row is `p`, or `NO_ETA` (the
379    /// successors of `p` in the graph `G_L`, Chapter 5.4).
380    pub(crate) l_eta_of_row: Vec<u32>,
381    /// `L` by rows (the graph `G_Lᵀ`): row `i` holds the pivot rows `p`
382    /// and multipliers `l_ip` in `lt_pivot/lt_value[lt_start[i]..lt_start[i+1]]`.
383    pub(crate) lt_start: Vec<usize>,
384    pub(crate) lt_pivot: Vec<u32>,
385    pub(crate) lt_value: Vec<T>,
386    /// Diagonal of `U`, indexed by the pivot row.
387    pub(crate) diag: Vec<T>,
388    /// Off-diagonal `U` by rows: line `i` holds `(j, u_ij)`.
389    pub(crate) u_rows: LineStore<T>,
390    /// Off-diagonal `U` by columns: line `j` holds `(i, u_ij)`.
391    pub(crate) u_cols: LineStore<T>,
392    /// Spike `R L⁻¹ a_q` recorded by `ftran_for_update` (indexed by rows,
393    /// dense, zero outside `spike_idx`).
394    pub(crate) spike: Vec<T>,
395    /// Rows where `spike` may be nonzero.
396    pub(crate) spike_idx: Vec<u32>,
397    pub(crate) spike_valid: bool,
398    /// Work arrays of the solves and the update (Chapter 5.4).
399    pub(crate) ws: Workspace<T>,
400    /// `nnz(L) + nnz(U)` right after the factorization (fill trigger).
401    pub(crate) nnz_factor0: usize,
402    pub(crate) stats: LuStats,
403}
404
405impl<T: Real> Default for Factors<T> {
406    fn default() -> Self {
407        Self {
408            m: 0,
409            valid: false,
410            step_row: Vec::new(),
411            step_col: Vec::new(),
412            row_step: Vec::new(),
413            col_step: Vec::new(),
414            l_row_step: Vec::new(),
415            l_start: vec![0],
416            l_pivot: Vec::new(),
417            l_index: Vec::new(),
418            l_value: Vec::new(),
419            l_eta_of_row: Vec::new(),
420            lt_start: vec![0],
421            lt_pivot: Vec::new(),
422            lt_value: Vec::new(),
423            diag: Vec::new(),
424            u_rows: LineStore::default(),
425            u_cols: LineStore::default(),
426            spike: Vec::new(),
427            spike_idx: Vec::new(),
428            spike_valid: false,
429            ws: Workspace::default(),
430            nnz_factor0: 0,
431            stats: LuStats::default(),
432        }
433    }
434}
435
436/// Sparse LU factors of a basis with a basis-update policy `U`
437/// (Forrest–Tomlin by default): the contract's `Lu<T, U>`.
438///
439/// Usage contract (the simplex method of Chapter 6), through
440/// [`LuFactor`]:
441/// 1. [`LuFactor::factorize`] with a [`ColumnSource`] (the basis heading
442///    and `A`);
443/// 2. [`LuFactor::ftran`] / [`LuFactor::btran`] any number of times;
444/// 3. to exchange the column at position `r`: [`LuFactor::ftran_for_update`]
445///    on the entering column `a_q` (this yields `α_q = B⁻¹ a_q` *and*
446///    records the spike), then [`LuFactor::update`]`(r, α_q[r])`;
447/// 4. when the update returns [`UpdateOutcome::RefactorNeeded`] (or an
448///    error), call [`LuFactor::factorize`] with the new heading — the
449///    factor never refactorizes by itself, since only the caller holds the
450///    columns (D-0016).
451#[derive(Clone, Debug)]
452pub struct Lu<T: Real = f64, U: BasisUpdate<T> = ForrestTomlin<T>> {
453    pub(crate) opts: LuOptions,
454    pub(crate) factors: Factors<T>,
455    pub(crate) update: U,
456    /// Moving averages of the result density of FTRAN, BTRAN and FTRAN for
457    /// update (Chapter 5.4, policy (c)).
458    pub(crate) expected_density: [f64; 3],
459    /// The pivot sequence of the last successful factorization (Chapter
460    /// 5.12); `None` before it and after a failed one.
461    pub(crate) symbolic: Option<SymbolicLu>,
462    /// Column pre-order of the bump (step L5′); `None`: Algorithm M.
463    pub(crate) pre_order: Option<PreOrder>,
464    /// Relative threshold of the replay test (step R5 of Chapter 5.12);
465    /// `None`: [`LuOptions::rel_pivot_tol`].
466    pub(crate) refactor_pivot_tol: Option<f64>,
467}
468
469impl<T: Real, U: BasisUpdate<T>> Default for Lu<T, U> {
470    fn default() -> Self {
471        Self::new(LuOptions::default())
472    }
473}
474
475/// What every LU of the crate exposes; the simplex method programs
476/// against this trait (the contract's `LuFactor<T>`).
477pub trait LuFactor<T: Real> {
478    /// Algorithm L: factorizes the `m × m` basis `B` whose columns the
479    /// source gives.
480    ///
481    /// On success the factor represents `B` and all previous updates are
482    /// discarded. On [`LuError::Singular`] the factor is invalid until the
483    /// next successful call; the error names the dependent positions and
484    /// the rows without pivot, so the caller can substitute logicals and
485    /// retry. On [`LuError::Structure`] (invariant (B1) violated) nothing
486    /// was factorized.
487    fn factorize<C: ColumnSource<T>>(&mut self, cols: &C) -> Result<(), LuError>;
488    /// Algorithm R (Chapter 5.12): factorizes the `m × m` basis `B` with
489    /// the pivot sequence of the last successful factorization — no
490    /// search, no count lists — as long as every pivot passes the
491    /// threshold test; otherwise (or without a sequence of order `m`) it
492    /// falls back to [`LuFactor::factorize`]. Meant for new values on the
493    /// same pattern; the outcome and the errors are those of `factorize`,
494    /// and [`LuStats::symbolic_reuses`] / [`LuStats::symbolic_fallbacks`]
495    /// say which path ran.
496    fn refactorize_same_pattern<C: ColumnSource<T>>(&mut self, cols: &C) -> Result<(), LuError>;
497    /// The pivot sequence of the last successful factorization, if any.
498    fn symbolic(&self) -> Option<&SymbolicLu>;
499    /// `v ← B⁻¹ v`: row-indexed in, position-indexed out (D-0016).
500    fn ftran(&mut self, v: &mut SparseVec<T>);
501    /// `v ← B⁻ᵀ v`: position-indexed in, row-indexed out.
502    fn btran(&mut self, v: &mut SparseVec<T>);
503    /// [`LuFactor::ftran`] that also records the spike for the next
504    /// [`LuFactor::update`].
505    fn ftran_for_update(&mut self, v: &mut SparseVec<T>);
506    /// Replaces the column at position `r`; `alpha_r` is entry `r` of the
507    /// `α_q` returned by the last `ftran_for_update`. The factor never
508    /// refactorizes on its own.
509    fn update(&mut self, r: usize, alpha_r: T) -> Result<UpdateOutcome, LuError>;
510    /// Dimension `m` of the last basis given to `factorize`.
511    fn m(&self) -> usize;
512    /// `true` iff the factor holds a valid factorization (the last
513    /// `factorize` succeeded).
514    fn is_valid(&self) -> bool;
515    /// Statistics of the current factor.
516    fn stats(&self) -> &LuStats;
517    /// Statistics of the update policy since the last factorization.
518    fn update_stats(&self) -> &UpdateStats;
519    /// The options in force.
520    fn options(&self) -> &LuOptions;
521    /// Replaces the options. The factorization reads them at the next
522    /// `factorize`, the solves and the update at their next call (e.g. a
523    /// tighter [`LuOptions::rel_pivot_tol`] after numerical trouble,
524    /// `PLAN.md` §8).
525    fn set_options(&mut self, opts: LuOptions);
526    /// Checks invariants (F1)–(F3) of a valid factor (and of its updates).
527    fn check_invariants(&self) -> Result<(), SparseError>;
528}
529
530/// Converts a count to `f64` for ratios.
531#[inline]
532#[allow(clippy::cast_precision_loss)] // counts stay far below 2^52
533pub(crate) fn count_f64(n: usize) -> f64 {
534    n as f64
535}
536
537/// A vacant slot of the step sequence: the Forrest–Tomlin update moves a
538/// step to the end by appending it and leaving its old slot vacant
539/// (Chapter 5.5, step U5), so that no other step is renumbered.
540pub(crate) const VACANT: u32 = u32::MAX;
541
542/// Converts an index below `m ≤ u32::MAX` to `u32`.
543#[inline]
544pub(crate) fn idx32(i: usize) -> u32 {
545    u32::try_from(i).expect("index exceeds u32::MAX")
546}
547
548impl<T: Real, U: BasisUpdate<T>> Lu<T, U> {
549    /// An empty factor (no valid factorization yet).
550    pub fn new(opts: LuOptions) -> Self {
551        debug_assert!(opts.is_valid(), "{opts:?}");
552        Self {
553            opts,
554            factors: Factors::default(),
555            update: U::default(),
556            expected_density: [0.0; 3],
557            symbolic: None,
558            pre_order: None,
559            refactor_pivot_tol: None,
560        }
561    }
562
563    /// The column pre-order of the bump (step L5′; Chapter 5.12): `None`
564    /// (the default) runs Algorithm M exactly as before, bit for bit;
565    /// `Some(PreOrder::Colamd)` eliminates the bump in the COLAMD order of
566    /// its columns, choosing the row by threshold partial pivoting. Read at
567    /// the next [`LuFactor::factorize`].
568    ///
569    /// (The contract has it as a field `LuOptions::pre_order`; a field
570    /// would make `LuOptions`, and the simplex's `SimplexOptions` that
571    /// holds it, 8 bytes larger, past the 256-byte limit of clippy's
572    /// `large_types_passed_by_value` in the simplex and the MIP solver.)
573    pub fn set_pre_order(&mut self, pre_order: Option<PreOrder>) {
574        self.pre_order = pre_order;
575    }
576
577    /// The column pre-order in force ([`Lu::set_pre_order`]).
578    #[inline]
579    pub fn pre_order(&self) -> Option<PreOrder> {
580        self.pre_order
581    }
582
583    /// The relative threshold `u_R` of the replay test of
584    /// [`LuFactor::refactorize_same_pattern`] (step R5 of Chapter 5.12):
585    /// `None` (the default) uses [`LuOptions::rel_pivot_tol`], the
586    /// threshold of the Markowitz search — a replayed sequence is then one
587    /// Algorithm L would have accepted, (R2); `Some(u_R)` with
588    /// `0 < u_R ≤ u` accepts the recorded pivots down to `u_R` times the
589    /// largest entry of their column, trading a little stability for
590    /// fewer fallbacks when the values drift (task L1.5b, D8). Never read
591    /// by [`LuFactor::factorize`] (Algorithm L is unchanged). A setter, not
592    /// a field of [`LuOptions`], for the reason of [`Lu::set_pre_order`].
593    ///
594    /// # Panics
595    /// If `u_R` is not in `(0, 1]`.
596    pub fn set_refactor_pivot_tol(&mut self, tol: Option<f64>) {
597        if let Some(u) = tol {
598            assert!(
599                u.is_finite() && lt(0.0, u) && le_exact(u, 1.0),
600                "refactor pivot tolerance {u} outside (0, 1]"
601            );
602        }
603        self.refactor_pivot_tol = tol;
604    }
605
606    /// The replay threshold in force ([`Lu::set_refactor_pivot_tol`]).
607    #[inline]
608    pub fn refactor_pivot_tol(&self) -> Option<f64> {
609        self.refactor_pivot_tol
610    }
611
612    /// `true` iff a spike is recorded and [`LuFactor::update`] may be
613    /// called.
614    #[inline]
615    pub fn has_spike(&self) -> bool {
616        self.factors.valid && self.factors.spike_valid
617    }
618
619    /// The basis-update policy (its statistics and name).
620    #[inline]
621    pub fn update_policy(&self) -> &U {
622        &self.update
623    }
624
625    /// Step L7: moves the factors out of the elimination (Algorithm L or
626    /// Algorithm R of Chapter 5.12), or reports singularity; records the
627    /// pivot sequence.
628    pub(crate) fn finish(&mut self, k: Elim<T>) -> Result<(), LuError> {
629        let f = &mut self.factors;
630        let m = f.m;
631        let mut stats = LuStats {
632            m,
633            nnz_basis: k.nnz_basis,
634            max_basis: k.max_basis.to_f64(),
635            col_singletons: k.col_singletons,
636            row_singletons: k.row_singletons,
637            markowitz_pivots: k.markowitz_pivots,
638            factorizations: f.stats.factorizations,
639            ftran: f.stats.ftran,
640            btran: f.stats.btran,
641            ftran_update: f.stats.ftran_update,
642            symbolic_reuses: f.stats.symbolic_reuses,
643            symbolic_fallbacks: f.stats.symbolic_fallbacks,
644            ..LuStats::default()
645        };
646        if !k.dead.is_empty() {
647            self.symbolic = None;
648            let mut cols = k.dead;
649            cols.sort_unstable();
650            let mut pivoted = vec![false; m];
651            for &p in &k.step_row {
652                pivoted[p as usize] = true;
653            }
654            let rows: Vec<Row> = (0..m)
655                .filter(|&i| !pivoted[i])
656                .map(Row::from_usize)
657                .collect();
658            debug_assert_eq!(rows.len(), cols.len());
659            f.stats = stats;
660            self.update.reset(m, 0.0);
661            return Err(LuError::Singular {
662                rank_deficiency: cols.len(),
663                cols,
664                rows,
665            });
666        }
667
668        // Inverse step maps.
669        f.row_step = vec![NIL; m];
670        f.col_step = vec![NIL; m];
671        for (s, (&p, &q)) in k.step_row.iter().zip(&k.step_col).enumerate() {
672            f.row_step[p as usize] = s;
673            f.col_step[q as usize] = s;
674        }
675        f.l_row_step.clone_from(&f.row_step);
676        f.step_row = k.step_row;
677        f.step_col = k.step_col;
678        f.l_start = k.l_start;
679        f.l_pivot = k.l_pivot;
680        f.l_index = k.l_index;
681        f.l_value = k.l_value;
682        f.diag = k.diag;
683
684        // U by rows and by columns, with a little room for the updates.
685        let mut row_len = vec![0_usize; m];
686        let mut col_len = vec![0_usize; m];
687        for &(i, j, _) in &k.u_trip {
688            row_len[i as usize] += 1;
689            col_len[j as usize] += 1;
690        }
691        let slack = |n: &usize| n + 2;
692        let row_caps: Vec<usize> = row_len.iter().map(slack).collect();
693        let col_caps: Vec<usize> = col_len.iter().map(slack).collect();
694        f.u_rows = LineStore::new(&row_caps);
695        f.u_cols = LineStore::new(&col_caps);
696        let mut max_u = T::ZERO;
697        for &(i, j, v) in &k.u_trip {
698            f.u_rows.push(i as usize, j, v);
699            f.u_cols.push(j as usize, i, v);
700            max_u = max_u.max(v.abs());
701        }
702        for &d in &f.diag {
703            max_u = max_u.max(d.abs());
704        }
705
706        f.prepare_hypersparse();
707
708        stats.nnz_l = f.l_value.len();
709        stats.nnz_u = f.u_rows.live() + m;
710        stats.max_u = max_u.to_f64();
711        stats.max_u_fresh = stats.max_u;
712        stats.factorizations += 1;
713        f.stats = stats;
714        f.nnz_factor0 = stats.nnz_l + stats.nnz_u;
715        let update_growth = f.refresh_stats(self.opts.growth_limit, 0);
716        self.update.reset(m, update_growth);
717        f.valid = true;
718        debug_assert!(f.check_invariants().is_ok(), "{:?}", f.check_invariants());
719        // The reusable pivot sequence (Chapter 5.12), into the buffers of
720        // the previous one.
721        let sym = self.symbolic.get_or_insert_with(SymbolicLu::default);
722        sym.pivot_rows.clone_from(&f.step_row);
723        sym.pivot_cols.clone_from(&f.step_col);
724        sym.singletons = stats.col_singletons + stats.row_singletons;
725        sym.nnz_l = stats.nnz_l;
726        sym.nnz_u = stats.nnz_u;
727        Ok(())
728    }
729}
730
731impl<T: Real, U: BasisUpdate<T>> LuFactor<T> for Lu<T, U> {
732    fn factorize<C: ColumnSource<T>>(&mut self, cols: &C) -> Result<(), LuError> {
733        self.factors.valid = false;
734        self.factors.spike_valid = false;
735        cols.validate()?;
736        self.factors.m = cols.m();
737
738        // L1. Load.
739        let mut k = Kernel::load(cols, self.opts);
740        // L2–L4. Triangular phase.
741        k.triangular_phase();
742        match self.pre_order {
743            // L5. Markowitz phase on the bump.
744            None => k.markowitz_phase(),
745            // L5′. The bump in its COLAMD column order.
746            Some(PreOrder::Colamd) => k.preordered_phase(),
747        }
748        // L7. Finish.
749        self.finish(k.into_elim())
750    }
751
752    fn refactorize_same_pattern<C: ColumnSource<T>>(&mut self, cols: &C) -> Result<(), LuError> {
753        self.refactorize_impl(cols)
754    }
755
756    #[inline]
757    fn symbolic(&self) -> Option<&SymbolicLu> {
758        self.symbolic.as_ref()
759    }
760
761    #[inline]
762    fn ftran(&mut self, v: &mut SparseVec<T>) {
763        self.ftran_kind(v, Kind::Ftran);
764    }
765
766    #[inline]
767    fn btran(&mut self, v: &mut SparseVec<T>) {
768        self.btran_impl(v);
769    }
770
771    #[inline]
772    fn ftran_for_update(&mut self, v: &mut SparseVec<T>) {
773        self.ftran_kind(v, Kind::FtranUpdate);
774    }
775
776    #[inline]
777    fn update(&mut self, r: usize, alpha_r: T) -> Result<UpdateOutcome, LuError> {
778        self.update
779            .update(&mut self.factors, &self.opts, r, alpha_r)
780    }
781
782    #[inline]
783    fn m(&self) -> usize {
784        self.factors.m
785    }
786
787    #[inline]
788    fn is_valid(&self) -> bool {
789        self.factors.valid
790    }
791
792    #[inline]
793    fn stats(&self) -> &LuStats {
794        &self.factors.stats
795    }
796
797    #[inline]
798    fn update_stats(&self) -> &UpdateStats {
799        self.update.stats()
800    }
801
802    #[inline]
803    fn options(&self) -> &LuOptions {
804        &self.opts
805    }
806
807    fn set_options(&mut self, opts: LuOptions) {
808        debug_assert!(opts.is_valid(), "{opts:?}");
809        self.opts = opts;
810    }
811
812    fn check_invariants(&self) -> Result<(), SparseError> {
813        self.factors.check_invariants()
814    }
815}
816
817impl<T: Real> Factors<T> {
818    /// Step L7, continued: the structures of the hypersparse solves
819    /// (Chapter 5.4) — the map from rows to `L` etas, `L` by rows, the work
820    /// arrays and an empty spike. (The row etas are emptied by the update
821    /// policy, [`BasisUpdate::reset`].)
822    fn prepare_hypersparse(&mut self) {
823        let m = self.m;
824        self.l_eta_of_row.clear();
825        self.l_eta_of_row.resize(m, NO_ETA);
826        let mut row_len = vec![0_usize; m + 1];
827        for (e, &p) in self.l_pivot.iter().enumerate() {
828            self.l_eta_of_row[p as usize] = idx32(e);
829            for &i in &self.l_index[self.l_start[e]..self.l_start[e + 1]] {
830                row_len[i as usize + 1] += 1;
831            }
832        }
833        for i in 0..m {
834            row_len[i + 1] += row_len[i];
835        }
836        let nnz = self.l_index.len();
837        self.lt_pivot.clear();
838        self.lt_pivot.resize(nnz, 0);
839        self.lt_value.clear();
840        self.lt_value.resize(nnz, T::ZERO);
841        let mut next = row_len.clone();
842        for (e, &p) in self.l_pivot.iter().enumerate() {
843            let r = self.l_start[e]..self.l_start[e + 1];
844            for (&i, &l) in self.l_index[r.clone()].iter().zip(&self.l_value[r]) {
845                let at = next[i as usize];
846                self.lt_pivot[at] = p;
847                self.lt_value[at] = l;
848                next[i as usize] += 1;
849            }
850        }
851        self.lt_start = row_len;
852        self.ws.resize(m);
853        self.ws.queued.clear();
854        if self.spike.len() == m {
855            for &i in &self.spike_idx {
856                self.spike[i as usize] = T::ZERO;
857            }
858        } else {
859            self.spike = vec![T::ZERO; m];
860        }
861        self.spike_idx.clear();
862    }
863
864    /// Recomputes the derived statistics (fill-in, growth) with `nnz_eta`
865    /// row-eta multipliers, and returns the growth added by the updates,
866    /// `max_u / max(max_basis, max_u_fresh)` (`1` without a reference).
867    pub(crate) fn refresh_stats(&mut self, growth_limit: f64, nnz_eta: usize) -> f64 {
868        let s = &mut self.stats;
869        s.nnz_u = self.u_rows.live() + self.m;
870        s.fill_in = (s.nnz_l + s.nnz_u + nnz_eta).saturating_sub(s.nnz_basis);
871        s.growth = if lt(0.0, s.max_basis) {
872            s.max_u / s.max_basis
873        } else {
874            1.0
875        };
876        s.growth_exceeded = lt(growth_limit, s.growth);
877        let reference = s.max_basis.max(s.max_u_fresh);
878        if lt(0.0, reference) {
879            s.max_u / reference
880        } else {
881            1.0
882        }
883    }
884
885    /// Checks invariants (F1)–(F3) of a valid factor (and of its updates).
886    pub(crate) fn check_invariants(&self) -> Result<(), SparseError> {
887        let bad = |msg: String| Err(SparseError::Malformed(msg));
888        if !self.valid {
889            return bad("factor is not valid".into());
890        }
891        let m = self.m;
892        // (F1)
893        if self.step_row.len() != self.step_col.len() {
894            return bad("step arrays differ in length".into());
895        }
896        let mut steps = 0;
897        for s in 0..self.step_row.len() {
898            let (p, q) = (self.step_row[s], self.step_col[s]);
899            if p == VACANT && q == VACANT {
900                continue;
901            }
902            steps += 1;
903            let (p, q) = (p as usize, q as usize);
904            if p >= m || q >= m || self.row_step[p] != s || self.col_step[q] != s {
905                return bad(format!("step {s} inconsistent with the inverse maps"));
906            }
907        }
908        if steps != m {
909            return bad(format!("{steps} steps for m = {m}"));
910        }
911        // (F2), in the step order of the last factorization.
912        for e in 0..self.l_pivot.len() {
913            let sp = self.l_row_step[self.l_pivot[e] as usize];
914            for &i in &self.l_index[self.l_start[e]..self.l_start[e + 1]] {
915                if self.l_row_step[i as usize] <= sp {
916                    return bad(format!("L eta {e} touches row {i} pivoted earlier"));
917                }
918            }
919        }
920        // (F3)
921        let mut n_row = 0;
922        for i in 0..m {
923            if is_exact_zero(self.diag[i]) {
924                return bad(format!("zero diagonal at row {i}"));
925            }
926            let (idx, val) = self.u_rows.line(i);
927            n_row += idx.len();
928            for (&j, &v) in idx.iter().zip(val) {
929                if self.col_step[j as usize] <= self.row_step[i] {
930                    return bad(format!("U entry ({i}, {j}) below the diagonal"));
931                }
932                match self.u_cols.get(j as usize, idx32(i)) {
933                    Some(w) if approx_eq(v, w, T::ZERO) => {}
934                    _ => return bad(format!("U copies disagree at ({i}, {j})")),
935                }
936            }
937        }
938        if n_row != self.u_cols.live() {
939            return bad("U row and column copies differ in size".into());
940        }
941        Ok(())
942    }
943}
944
945/// The active submatrix and the factors under construction (Algorithm L).
946struct Kernel<T: Real> {
947    m: usize,
948    /// `τ_abs`, `u` and `ε_drop` in the scalar type.
949    abs_tol: T,
950    rel_tol: T,
951    drop_tol: T,
952    search_limit: usize,
953    /// Column file: line `j` holds `(i, s_ij)`, linked to `rows`
954    /// (Chapter 5.2, Algorithm T).
955    cols: LineStore<T>,
956    /// Row file (pattern only): line `i` holds `(j, ·)`, linked to `cols`.
957    rows: LineStore<T>,
958    col_lists: CountLists,
959    row_lists: CountLists,
960    /// Row → relative position in the column being updated, for the
961    /// cancelled entries still to remove (`NIL` elsewhere).
962    wmap: Vec<usize>,
963    /// Row → index in the `L` column of the current pivot (`NIL` elsewhere).
964    lmap: Vec<usize>,
965    /// Column `j` may still hold a negligible entry of `B` itself
966    /// (`|b_ij| ≤ ε_drop`), so its next update scans it whole.
967    tiny: Vec<bool>,
968    /// Work arrays of step L6 (kept to avoid an allocation per pivot).
969    tw: Vec<usize>,
970    cancelled: Vec<(usize, u32)>,
971    nnz_basis: usize,
972    max_basis: T,
973    step_row: Vec<u32>,
974    step_col: Vec<u32>,
975    diag: Vec<T>,
976    l_start: Vec<usize>,
977    l_pivot: Vec<u32>,
978    l_index: Vec<u32>,
979    l_value: Vec<T>,
980    /// Off-diagonal `U` entries `(i, j, u_ij)` in pivot order.
981    u_trip: Vec<(u32, u32, T)>,
982    dead: Vec<usize>,
983    col_singletons: usize,
984    row_singletons: usize,
985    markowitz_pivots: usize,
986}
987
988/// The output of an elimination — Algorithm L's kernel or Algorithm R of
989/// Chapter 5.12 — that step L7 turns into [`Factors`].
990pub(crate) struct Elim<T: Real> {
991    pub(crate) nnz_basis: usize,
992    pub(crate) max_basis: T,
993    pub(crate) col_singletons: usize,
994    pub(crate) row_singletons: usize,
995    pub(crate) markowitz_pivots: usize,
996    pub(crate) step_row: Vec<u32>,
997    pub(crate) step_col: Vec<u32>,
998    pub(crate) diag: Vec<T>,
999    pub(crate) l_start: Vec<usize>,
1000    pub(crate) l_pivot: Vec<u32>,
1001    pub(crate) l_index: Vec<u32>,
1002    pub(crate) l_value: Vec<T>,
1003    /// Off-diagonal `U` entries `(i, j, u_ij)`.
1004    pub(crate) u_trip: Vec<(u32, u32, T)>,
1005    pub(crate) dead: Vec<usize>,
1006}
1007
1008/// A candidate pivot of Algorithm M.
1009#[derive(Clone, Copy)]
1010struct Candidate<T: Real> {
1011    i: usize,
1012    j: usize,
1013    merit: usize,
1014    ratio: T,
1015}
1016
1017impl<T: Real> Candidate<T> {
1018    /// Smaller merit wins; ties go to the larger ratio `|s_ij| / γ_j`.
1019    fn beats(&self, other: Option<&Candidate<T>>) -> bool {
1020        other.is_none_or(|o| {
1021            self.merit < o.merit || (self.merit == o.merit && lt(o.ratio, self.ratio))
1022        })
1023    }
1024}
1025
1026impl<T: Real> Kernel<T> {
1027    /// Step L1.
1028    fn load<C: ColumnSource<T>>(basis: &C, opts: LuOptions) -> Self {
1029        let m = basis.m();
1030        let drop_tol = T::from_f64(opts.drop_tol);
1031        let mut col_len = vec![0_usize; m];
1032        let mut row_len = vec![0_usize; m];
1033        for (k, len) in col_len.iter_mut().enumerate() {
1034            basis.for_each_entry(k, |i, _| {
1035                *len += 1;
1036                row_len[i as usize] += 1;
1037            });
1038        }
1039        let caps = |v: &[usize]| v.iter().map(|&n| n + 4).collect::<Vec<_>>();
1040        let mut cols = LineStore::new_linked(&caps(&col_len));
1041        let mut rows = LineStore::new_pattern_linked(&caps(&row_len));
1042        let mut max_basis = T::ZERO;
1043        let mut tiny = vec![false; m];
1044        for (k, tiny_k) in tiny.iter_mut().enumerate() {
1045            basis.for_each_entry(k, |i, v| {
1046                *tiny_k |= is_zero(v, drop_tol);
1047                let (q, r) = (cols.len(k), rows.len(i as usize));
1048                cols.push_linked(k, i, v, r);
1049                rows.push_linked(i as usize, idx32(k), T::ZERO, q);
1050                max_basis = max_basis.max(v.abs());
1051            });
1052        }
1053        debug_assert_eq!(check_links(&cols, &rows, true), Ok(()));
1054        let mut col_lists = CountLists::new(m, m);
1055        let mut row_lists = CountLists::new(m, m);
1056        for j in 0..m {
1057            col_lists.insert(j, cols.len(j));
1058        }
1059        for i in 0..m {
1060            row_lists.insert(i, rows.len(i));
1061        }
1062        Self {
1063            m,
1064            abs_tol: T::from_f64(opts.abs_pivot_tol),
1065            rel_tol: T::from_f64(opts.rel_pivot_tol),
1066            drop_tol,
1067            search_limit: opts.search_limit,
1068            nnz_basis: cols.live(),
1069            cols,
1070            rows,
1071            col_lists,
1072            row_lists,
1073            wmap: vec![NIL; m],
1074            lmap: vec![NIL; m],
1075            tiny,
1076            tw: Vec::new(),
1077            cancelled: Vec::new(),
1078            max_basis,
1079            step_row: Vec::with_capacity(m),
1080            step_col: Vec::with_capacity(m),
1081            diag: vec![T::ZERO; m],
1082            l_start: vec![0],
1083            l_pivot: Vec::new(),
1084            l_index: Vec::new(),
1085            l_value: Vec::new(),
1086            u_trip: Vec::new(),
1087            dead: Vec::new(),
1088            col_singletons: 0,
1089            row_singletons: 0,
1090            markowitz_pivots: 0,
1091        }
1092    }
1093
1094    /// The output fields, moved (step L7 reads nothing else).
1095    fn into_elim(self) -> Elim<T> {
1096        Elim {
1097            nnz_basis: self.nnz_basis,
1098            max_basis: self.max_basis,
1099            col_singletons: self.col_singletons,
1100            row_singletons: self.row_singletons,
1101            markowitz_pivots: self.markowitz_pivots,
1102            step_row: self.step_row,
1103            step_col: self.step_col,
1104            diag: self.diag,
1105            l_start: self.l_start,
1106            l_pivot: self.l_pivot,
1107            l_index: self.l_index,
1108            l_value: self.l_value,
1109            u_trip: self.u_trip,
1110            dead: self.dead,
1111        }
1112    }
1113
1114    /// Step L5′: the bump in the COLAMD order of its columns, each column
1115    /// pivoted on its acceptable entry of least row count (ties: larger
1116    /// magnitude, then the first in the column).
1117    fn preordered_phase(&mut self) {
1118        self.kill_empty_columns();
1119        for c in self.bump_order() {
1120            self.kill_empty_columns();
1121            if !self.col_lists.contains(c) {
1122                continue;
1123            }
1124            let gamma = self
1125                .cols
1126                .line(c)
1127                .1
1128                .iter()
1129                .fold(T::ZERO, |g, &v| g.max(v.abs()));
1130            if !self.above_abs(gamma) {
1131                self.kill(c);
1132                continue;
1133            }
1134            let u = self.rel_tol;
1135            let mut best: Option<(usize, usize, T)> = None;
1136            let (idx, val) = self.cols.line(c);
1137            for (&i, &v) in idx.iter().zip(val) {
1138                let (i, a) = (i as usize, v.abs());
1139                if !self.above_abs(v) || lt(a, u * gamma) {
1140                    continue;
1141                }
1142                let r = self.rows.len(i);
1143                if best.is_none_or(|(_, rb, ab)| r < rb || (r == rb && lt(ab, a))) {
1144                    best = Some((i, r, a));
1145                }
1146            }
1147            let (i, _, _) = best.expect("the largest entry is acceptable");
1148            self.pivot(i, c);
1149            self.markowitz_pivots += 1;
1150        }
1151        self.kill_empty_columns();
1152        debug_assert_eq!(
1153            self.col_lists.size(),
1154            0,
1155            "every bump column is pivoted or dead"
1156        );
1157    }
1158
1159    /// Step L5′, first part: the COLAMD order (Chapter 5.10, Algorithm C)
1160    /// of the active columns, on the pattern of the active submatrix.
1161    fn bump_order(&self) -> Vec<usize> {
1162        let mut row_of = vec![NIL; self.m];
1163        let mut n_rows = 0;
1164        for (i, r) in row_of.iter_mut().enumerate() {
1165            if self.row_lists.contains(i) {
1166                *r = n_rows;
1167                n_rows += 1;
1168            }
1169        }
1170        let active: Vec<usize> = (0..self.m)
1171            .filter(|&j| self.col_lists.contains(j))
1172            .collect();
1173        let mut col_ptr = Vec::with_capacity(active.len() + 1);
1174        let mut row_idx = Vec::new();
1175        col_ptr.push(0);
1176        for &j in &active {
1177            let start = row_idx.len();
1178            row_idx.extend(
1179                self.cols
1180                    .indices(j)
1181                    .iter()
1182                    .map(|&i| idx32(row_of[i as usize])),
1183            );
1184            row_idx[start..].sort_unstable();
1185            col_ptr.push(row_idx.len());
1186        }
1187        let pattern = Pattern {
1188            n_rows,
1189            col_ptr,
1190            row_idx,
1191        };
1192        debug_assert_eq!(pattern.check_invariants(), Ok(()));
1193        let perm = Colamd::default()
1194            .order(&pattern, OrderingTarget::Columns)
1195            .expect("a valid pattern");
1196        perm.fwd.iter().map(|&k| active[k as usize]).collect()
1197    }
1198
1199    /// `|v| > τ_abs`.
1200    #[inline]
1201    fn above_abs(&self, v: T) -> bool {
1202        lt(self.abs_tol, v.abs())
1203    }
1204
1205    /// `s_ij` for the entry at position `r` of row `i` (through its link).
1206    #[inline]
1207    fn value_in_row(&self, i: usize, r: usize) -> T {
1208        let j = self.rows.index_at(i, r) as usize;
1209        let q = self
1210            .cols
1211            .find(j, idx32(i), self.rows.link(i, r))
1212            .expect("row and column files agree");
1213        self.cols.value_at(j, q)
1214    }
1215
1216    /// The position in the row file of the twin of the entry at position
1217    /// `q` of column `j` (Algorithm T: its hint, else a search).
1218    #[inline]
1219    fn row_twin(&self, j: usize, q: usize) -> (usize, usize) {
1220        let i = self.cols.index_at(j, q) as usize;
1221        let r = self
1222            .rows
1223            .find(i, idx32(j), self.cols.link(j, q))
1224            .expect("row and column files agree");
1225        (i, r)
1226    }
1227
1228    /// Removes the dead column `j` from the active submatrix.
1229    fn kill(&mut self, j: usize) {
1230        // Its entries leave the row file one by one (Algorithm T; removals
1231        // from the rows leave column j unchanged), then the column is
1232        // cleared as a whole.
1233        for q in 0..self.cols.len(j) {
1234            let (i, r) = self.row_twin(j, q);
1235            remove_linked(&mut self.rows, &mut self.cols, i, r);
1236            self.row_lists.set(i, self.rows.len(i));
1237        }
1238        self.cols.clear(j);
1239        self.col_lists.remove(j);
1240        self.dead.push(j);
1241    }
1242
1243    /// Step L2.
1244    fn kill_empty_columns(&mut self) {
1245        while let Some(j) = self.col_lists.first(0) {
1246            self.kill(j);
1247        }
1248    }
1249
1250    /// Steps L2–L4.
1251    fn triangular_phase(&mut self) {
1252        loop {
1253            self.kill_empty_columns();
1254            // L3. Column singleton.
1255            if let Some(j) = self.col_lists.first(1) {
1256                let (idx, val) = self.cols.line(j);
1257                let (i, v) = (idx[0] as usize, val[0]);
1258                if self.above_abs(v) {
1259                    self.pivot(i, j);
1260                    self.col_singletons += 1;
1261                } else {
1262                    self.kill(j);
1263                }
1264                continue;
1265            }
1266            // L4. Row singleton with a pivot above τ_abs.
1267            let mut found = None;
1268            let mut e = self.row_lists.first(1);
1269            while let Some(i) = e {
1270                let j = self.rows.index_at(i, 0) as usize;
1271                let v = self.value_in_row(i, 0);
1272                if self.above_abs(v) {
1273                    found = Some((i, j));
1274                    break;
1275                }
1276                e = self.row_lists.next(i);
1277            }
1278            let Some((i, j)) = found else {
1279                debug_assert_eq!(check_links(&self.cols, &self.rows, false), Ok(()));
1280                return;
1281            };
1282            self.pivot(i, j);
1283            self.row_singletons += 1;
1284        }
1285    }
1286
1287    /// Step L5.
1288    fn markowitz_phase(&mut self) {
1289        loop {
1290            self.kill_empty_columns();
1291            if self.col_lists.size() == 0 {
1292                return;
1293            }
1294            let (best, dead) = self.search();
1295            let killed = !dead.is_empty();
1296            for j in dead {
1297                self.kill(j);
1298            }
1299            match best {
1300                Some(c) => {
1301                    self.pivot(c.i, c.j);
1302                    self.markowitz_pivots += 1;
1303                }
1304                None if killed => {}
1305                None => {
1306                    debug_assert!(
1307                        false,
1308                        "Markowitz search found neither pivot nor dead column"
1309                    );
1310                    return;
1311                }
1312            }
1313        }
1314    }
1315
1316    /// Algorithm M.
1317    fn search(&self) -> (Option<Candidate<T>>, Vec<usize>) {
1318        let u = self.rel_tol;
1319        let limit = self.search_limit.max(1);
1320        let mut best: Option<Candidate<T>> = None;
1321        let mut dead = Vec::new();
1322        let mut examined = 0_usize;
1323        let col_max = |j: usize| {
1324            self.cols
1325                .line(j)
1326                .1
1327                .iter()
1328                .fold(T::ZERO, |g, &v| g.max(v.abs()))
1329        };
1330        let acceptable = |v: T, gamma: T| self.above_abs(v) && !lt(v.abs(), u * gamma);
1331        for k in 1..=self.m {
1332            // M2. Columns of count k.
1333            let mut e = self.col_lists.first(k);
1334            while let Some(j) = e {
1335                e = self.col_lists.next(j);
1336                let gamma = col_max(j);
1337                if !self.above_abs(gamma) {
1338                    dead.push(j);
1339                    continue;
1340                }
1341                let mut any = false;
1342                let (idx, val) = self.cols.line(j);
1343                for (&i, &v) in idx.iter().zip(val) {
1344                    if acceptable(v, gamma) {
1345                        any = true;
1346                        let i = i as usize;
1347                        let c = Candidate {
1348                            i,
1349                            j,
1350                            merit: (self.rows.len(i) - 1) * (k - 1),
1351                            ratio: v.abs() / gamma,
1352                        };
1353                        if c.beats(best.as_ref()) {
1354                            best = Some(c);
1355                        }
1356                    }
1357                }
1358                examined += usize::from(any);
1359                if let Some(b) = &best
1360                    && (b.merit <= (k - 1) * (k - 1) || examined >= limit)
1361                {
1362                    return (best, dead);
1363                }
1364            }
1365            // M3. Rows of count k.
1366            let mut e = self.row_lists.first(k);
1367            while let Some(i) = e {
1368                e = self.row_lists.next(i);
1369                let mut any = false;
1370                for (r, &j) in self.rows.indices(i).iter().enumerate() {
1371                    let j = j as usize;
1372                    let gamma = col_max(j);
1373                    let v = self.value_in_row(i, r);
1374                    if acceptable(v, gamma) {
1375                        any = true;
1376                        let c = Candidate {
1377                            i,
1378                            j,
1379                            merit: (k - 1) * (self.cols.len(j) - 1),
1380                            ratio: v.abs() / gamma,
1381                        };
1382                        if c.beats(best.as_ref()) {
1383                            best = Some(c);
1384                        }
1385                    }
1386                }
1387                examined += usize::from(any);
1388                if let Some(b) = &best
1389                    && (b.merit <= k * (k - 1) || examined >= limit)
1390                {
1391                    return (best, dead);
1392                }
1393            }
1394        }
1395        (best, dead)
1396    }
1397
1398    /// Step L6 for column `j` of the `U` row (`u_pj`): `s_ij ← s_ij −
1399    /// l_ic u_pj` for the `L` column `lcol` (rows marked in `lmap`), fill-in
1400    /// appended to both files, then the cancellations removed in the order
1401    /// of the column. `lq` and `cancelled` are work arrays.
1402    fn update_column(
1403        &mut self,
1404        j: usize,
1405        u_pj: T,
1406        lcol: &[(u32, T)],
1407        lq: &mut Vec<usize>,
1408        cancelled: &mut Vec<(usize, u32)>,
1409    ) {
1410        // The positions in column j of the rows of the L column
1411        // (one pass over the column).
1412        lq.clear();
1413        lq.resize(lcol.len(), NIL);
1414        for (q, &i) in self.cols.line(j).0.iter().enumerate() {
1415            let k = self.lmap[i as usize];
1416            if k != NIL {
1417                lq[k] = q;
1418            }
1419        }
1420        for (k, &(i, l_ic)) in lcol.iter().enumerate() {
1421            let delta = -l_ic * u_pj;
1422            let q = lq[k];
1423            if q == NIL {
1424                let (q, r) = (self.cols.len(j), self.rows.len(i as usize));
1425                self.cols.push_linked(j, i, delta, r);
1426                lq[k] = q;
1427                self.rows.push_linked(i as usize, idx32(j), T::ZERO, q);
1428            } else {
1429                self.cols.values_mut(j)[q] += delta;
1430            }
1431        }
1432        // Cancellations, in the order of the column: only the entries
1433        // just updated or filled can have become negligible, besides
1434        // negligible entries of B itself in a column never scanned.
1435        cancelled.clear();
1436        if self.tiny[j] {
1437            self.tiny[j] = false;
1438            let (idx, val) = self.cols.line(j);
1439            cancelled.extend(
1440                idx.iter()
1441                    .zip(val)
1442                    .enumerate()
1443                    .filter(|&(_, (_, &v))| is_zero(v, self.drop_tol))
1444                    .map(|(q, (&i, _))| (q, i)),
1445            );
1446        } else {
1447            for (k, &(i, _)) in lcol.iter().enumerate() {
1448                if is_zero(self.cols.value_at(j, lq[k]), self.drop_tol) {
1449                    cancelled.push((lq[k], i));
1450                }
1451            }
1452            cancelled.sort_unstable();
1453        }
1454        // They leave both files in that order; `wmap` holds the
1455        // current position of each one still to remove and follows
1456        // the entry that moves into a hole (Algorithm T).
1457        for &(q, i) in cancelled.iter() {
1458            self.wmap[i as usize] = q;
1459        }
1460        for &(_, i) in cancelled.iter() {
1461            let q = self.wmap[i as usize];
1462            let (_, t) = remove_linked(&mut self.cols, &mut self.rows, j, q);
1463            if q < self.cols.len(j) {
1464                let moved = self.cols.index_at(j, q) as usize;
1465                if self.wmap[moved] != NIL {
1466                    self.wmap[moved] = q;
1467                }
1468            }
1469            let r = self
1470                .rows
1471                .find(i as usize, idx32(j), t)
1472                .expect("row and column files agree");
1473            remove_linked(&mut self.rows, &mut self.cols, i as usize, r);
1474            self.wmap[i as usize] = NIL;
1475        }
1476    }
1477
1478    /// Step L6: pivots on `(p, c)` (row `p`, position `c`).
1479    fn pivot(&mut self, p: usize, c: usize) {
1480        let (p32, c32) = (idx32(p), idx32(c));
1481        let piv = self.cols.get(c, p32).expect("pivot entry is stored");
1482
1483        // L column: multipliers of the other rows of column c, and the
1484        // hints of their twins in the row file (`tw`, parallel).
1485        let mut tw = std::mem::take(&mut self.tw);
1486        tw.clear();
1487        let mut r_p = NIL;
1488        let lcol: Vec<(u32, T)> = {
1489            let (idx, val) = self.cols.line(c);
1490            let mut lcol = Vec::with_capacity(idx.len().saturating_sub(1));
1491            for (q, (&i, &v)) in idx.iter().zip(val).enumerate() {
1492                if i == p32 {
1493                    r_p = self.cols.link(c, q);
1494                } else {
1495                    lcol.push((i, v / piv));
1496                    tw.push(self.cols.link(c, q));
1497                }
1498            }
1499            lcol
1500        };
1501        debug_assert_ne!(r_p, NIL, "the pivot entry is in its column");
1502        // Column c leaves the row file through the links (Algorithm T), in
1503        // the order of before: the other rows first, then row p.
1504        self.cols.clear(c);
1505        self.col_lists.remove(c);
1506        for (&(i, _), &t) in lcol.iter().zip(&tw) {
1507            let i = i as usize;
1508            let r = self
1509                .rows
1510                .find(i, c32, t)
1511                .expect("row and column files agree");
1512            remove_linked(&mut self.rows, &mut self.cols, i, r);
1513        }
1514        let r_p = self
1515            .rows
1516            .find(p, c32, r_p)
1517            .expect("row and column files agree");
1518        remove_linked(&mut self.rows, &mut self.cols, p, r_p);
1519
1520        // U row: the other entries of row p, removed from their columns
1521        // through the links (which leaves row p itself unchanged); row p is
1522        // cleared afterwards.
1523        let mut urow = Vec::with_capacity(self.rows.len(p));
1524        for r in 0..self.rows.len(p) {
1525            let j = self.rows.index_at(p, r) as usize;
1526            let q = self
1527                .cols
1528                .find(j, p32, self.rows.link(p, r))
1529                .expect("row and column files agree");
1530            let v = self.cols.value_at(j, q);
1531            remove_linked(&mut self.cols, &mut self.rows, j, q);
1532            urow.push((idx32(j), v));
1533        }
1534        self.rows.clear(p);
1535        self.row_lists.remove(p);
1536
1537        // Schur complement update s_ij ← s_ij − l_ic u_pj.
1538        if !lcol.is_empty() && !urow.is_empty() {
1539            for (k, &(i, _)) in lcol.iter().enumerate() {
1540                self.lmap[i as usize] = k;
1541            }
1542            // `tw` is free again: it holds the positions in column j.
1543            let lq = &mut tw;
1544            let mut cancelled = std::mem::take(&mut self.cancelled);
1545            for &(j, u_pj) in &urow {
1546                self.update_column(j as usize, u_pj, &lcol, lq, &mut cancelled);
1547            }
1548            for &(i, _) in &lcol {
1549                self.lmap[i as usize] = NIL;
1550            }
1551            self.cancelled = cancelled;
1552        }
1553        self.tw = tw;
1554        for &(i, _) in &lcol {
1555            let i = i as usize;
1556            self.row_lists.set(i, self.rows.len(i));
1557        }
1558        for &(j, _) in &urow {
1559            let j = j as usize;
1560            self.col_lists.set(j, self.cols.len(j));
1561        }
1562
1563        // Record the step.
1564        self.step_row.push(p32);
1565        self.step_col.push(c32);
1566        self.diag[p] = piv;
1567        if !lcol.is_empty() {
1568            self.l_pivot.push(p32);
1569            for (i, l) in lcol {
1570                self.l_index.push(i);
1571                self.l_value.push(l);
1572            }
1573            self.l_start.push(self.l_index.len());
1574        }
1575        for (j, u) in urow {
1576            self.u_trip.push((p32, j, u));
1577        }
1578    }
1579}