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}