Skip to main content

xingo_sparse/
solve.rs

1//! # Chapter 5.4 — FTRAN and BTRAN, dense and hypersparse
2//!
3//! ## Purpose
4//! The simplex method needs `α_q = B⁻¹ a_q` (FTRAN, forward transformation)
5//! and `ρ_r = B⁻ᵀ e_r`, `y = B⁻ᵀ c_B` (BTRAN, backward transformation).
6//! This chapter applies the factors of Chapter 5.3 and the row etas of
7//! Chapter 5.5 to a right-hand side, in one of two ways:
8//!
9//! * the *dense* Algorithms F and B run over every elimination step,
10//!   skipping exact zeros: `O(m + nnz(L) + nnz(U) + nnz(etas))` whatever
11//!   the right-hand side;
12//! * the *hypersparse* Algorithms HF and HB first compute, by depth-first
13//!   search in the graphs of the factors, the set of entries that can
14//!   become nonzero (the *reach* of the right-hand side, Gilbert & Peierls
15//!   1988), and then perform only the operations of the reach, in a
16//!   topological order. Their cost is proportional to the number of
17//!   entries and factor nonzeros actually involved — on the large sparse LPs
18//!   (Kennington `ken-*`, `pds-*`) a handful out of `m ≈ 10⁵`, which is what
19//!   Hall & McKinnon (2005) call *hypersparsity*.
20//!
21//! The choice between the two is automatic, per call (policy below).
22//!
23//! ## Notation
24//! After `t` updates the factor represents
25//! ```text
26//!   B = Pᵀ L R₁⁻¹ ⋯ R_t⁻¹ U Qᵀ,
27//! ```
28//! where `R_e = I − e_{p} μ_eᵀ` is the row eta of update `e` (target row
29//! `p`, multipliers `μ_e` on rows pivoted after `p`). FTRAN maps a vector
30//! indexed by *rows* to one indexed by *positions*; BTRAN the reverse.
31//! For a row `p` let `q(p) = q_{step(p)}` be the position pivoted with it,
32//! and for a position `q` let `p(q) = p_{step(q)}`.
33//!
34//! The four *graphs* of the hypersparse solves have an arc `a → b` when
35//! the value at `b` is modified by an operation that reads the value at
36//! `a`:
37//!
38//! * `G_L` on rows: `p → i` for every multiplier `l_ip` of the `L` eta with
39//!   pivot row `p` (column-wise `L`, as stored);
40//! * `G_U` on rows: `p → i` for every off-diagonal `u_{i q(p)}` of column
41//!   `q(p)` of `U` (`U` by columns);
42//! * `G_Uᵀ` on positions: `q → j` for every off-diagonal `u_{p(q) j}` of row
43//!   `p(q)` of `U` (`U` by rows);
44//! * `G_Lᵀ` on rows: `i → p` for every multiplier `l_ip` (`L` by *rows*, a
45//!   transposed copy built once per factorization).
46//!
47//! The row etas are applied in creation order (FTRAN) or in reverse
48//! creation order (BTRAN); their dependencies are held by two lists per
49//! row: the etas that *read* row `k` (`k` among their `p_k`) and the etas
50//! that *target* row `p`.
51//!
52//! ## Algorithm F (dense FTRAN: solve `B x = b`)
53//! F1. [L.] For each `L` eta in pivot order (pivot row `p`, multipliers
54//!     `l_ip`): if `y_p ≠ 0`, set `y_i ← y_i − l_ip y_p`.
55//! F2. [R.] For each row eta in creation order (target `p`): set
56//!     `y_p ← y_p − Σ_k μ_k y_{p_k}`. The vector `y` now holds the *spike*
57//!     `R L⁻¹ b` — recorded by [`crate::LuFactor::ftran_for_update`].
58//! F3. [U.] For the steps `s` in decreasing order, with pivot `(p_s, q_s)`:
59//!     `x_{q_s} ← y_{p_s} / u_{p_s q_s}`; if nonzero, `y_i ← y_i − u_{i q_s}
60//!     x_{q_s}` for the entries of column `q_s` of `U` (all in rows of earlier
61//!     steps).
62//!
63//! ## Algorithm B (dense BTRAN: solve `Bᵀ y = c`)
64//! B1. [Uᵀ.] For the steps `s` in increasing order:
65//!     `w_{p_s} ← c_{q_s} / u_{p_s q_s}`;
66//!     if nonzero, `c_j ← c_j − u_{p_s j} w_{p_s}` for the entries of row
67//!     `p_s` of `U` (all in positions of later steps).
68//! B2. [Rᵀ.] For each row eta in *reverse* creation order (target `p`): if
69//!     `w_p ≠ 0`, `w_{p_k} ← w_{p_k} − μ_k w_p`.
70//! B3. [Lᵀ.] For each `L` eta in *reverse* pivot order (pivot row `p`):
71//!     `w_p ← w_p − Σ_i l_ip w_i`. Then `y = w`.
72//!
73//! Correctness: F1–F3 apply `L⁻¹`, `R_t ⋯ R_1` and `U⁻¹` (with the
74//! permutations implicit in the indexing) to `b`; B1–B3 apply the
75//! transposes in the opposite order.
76//!
77//! ## Algorithm D (reach: depth-first search, Gilbert & Peierls)
78//! Given a graph `G` and a set `S` of start nodes, produce the nodes
79//! reachable from `S` in a *topological* order (every arc `a → b` between
80//! reached nodes has `a` before `b`).
81//!
82//! D1. [Start.] Take a fresh mark (a stamp: no clearing). Empty the output
83//!     list `post`. For each `s ∈ S` not yet marked, mark it and push
84//!     `(s, 0)` on the stack; then run D2 until the stack is empty.
85//! D2. [Step.] Let `(v, k)` be the top of the stack. If `v` has a `k`-th
86//!     successor `w`, advance `k`; if `w` is unmarked, mark it and push
87//!     `(w, 0)`. Otherwise pop `v` and append it to `post` (`v` is
88//!     *finished*).
89//! D3. [Order.] A node finishes after every node reachable from it, so
90//!     `post` read backwards is a topological order.
91//!
92//! The stack is explicit (no recursion: the paths in `G_L` can be `10⁵`
93//! long). Cost: `O(|reach| + arcs out of the reach)`.
94//!
95//! ## Algorithm HF (hypersparse FTRAN)
96//! The right-hand side `b` is given with its pattern.
97//! HF1. [Load.] Copy the nonzeros of `b` into the row work array `y`;
98//!      empty `b` (it receives the result).
99//! HF2. [Reach of L.] Run Algorithm D on `G_L` from the rows of HF1.
100//! HF3. [Apply L.] For each reached row `p` in topological order: if `p`
101//!      is the pivot of an `L` eta and `y_p ≠ 0`, set `y_i ← y_i − l_ip y_p`.
102//!      The reached rows form the list `Λ` (marked).
103//! HF4. [Row etas.] Put in a min-priority queue every eta that reads a row
104//!      of `Λ`. While the queue is not empty, take the smallest eta `e`
105//!      (target `t`): `y_t ← y_t − Σ_k μ_k y_{p_k}`; if `t ∉ Λ`, add it to
106//!      `Λ` and queue the etas after `e` that read `t`. (An eta before `e`
107//!      that reads `t` was applied — or skipped — while `y_t` was still
108//!      zero, exactly as in F2.) For [`crate::LuFactor::ftran_for_update`],
109//!      record the spike `y` on `Λ`.
110//! HF5. [Reach of U.] Run Algorithm D on `G_U` from `Λ`.
111//! HF6. [Apply U.] For each reached row `p` in topological order:
112//!      `x_{q(p)} ← y_p / u_{p q(p)}`, `y_p ← 0`; if `x_{q(p)} ≠ 0`, set
113//!      `y_i ← y_i − u_{i q(p)} x_{q(p)}` along column `q(p)`. The pattern
114//!      of the result is the set of positions `q(p)` of the reach.
115//!
116//! ## Algorithm HB (hypersparse BTRAN)
117//! HB1. [Load.] Copy the nonzeros of `c` into the position work array;
118//!      empty `c`.
119//! HB2. [Reach of Uᵀ.] Run Algorithm D on `G_Uᵀ` from the positions of HB1.
120//! HB3. [Apply Uᵀ.] For each reached position `q` in topological order:
121//!      `w_{p(q)} ← c_q / u_{p(q) q}`, `c_q ← 0`; if nonzero,
122//!      `c_j ← c_j − u_{p(q) j} w_{p(q)}` along row `p(q)`. The rows `p(q)`
123//!      form the list `Λ` (marked).
124//! HB4. [Row etas, transposed.] Put in a max-priority queue every eta whose
125//!      target is in `Λ`. While the queue is not empty, take the largest eta
126//!      `e` (target `t`): if `w_t ≠ 0`, `w_{p_k} ← w_{p_k} − μ_k w_t`; for
127//!      every `p_k ∉ Λ`, add it to `Λ` and queue the etas *before* `e` that
128//!      target `p_k`. (The etas after `e` that target `p_k` already ran,
129//!      with `w_{p_k} = 0`: no-ops in B2 too.)
130//! HB5. [Reach of Lᵀ.] Run Algorithm D on `G_Lᵀ` from `Λ`.
131//! HB6. [Apply Lᵀ.] For each reached row `i` in topological order: if
132//!      `w_i ≠ 0`, set `w_p ← w_p − l_ip w_i` for every multiplier of row
133//!      `i` of `L`. The result `y = w` has the reach as pattern.
134//!
135//! The direction of the arcs is the delicate point. In FTRAN `L` and `U`
136//! are used by *columns* (scatter: a known value pushes into the rows it
137//! modifies), so the graphs are those of the stored columns. In BTRAN the
138//! dense algorithm uses `U` by rows (B1 is already a scatter, along row
139//! `p(q)`: arcs `q → j`) but `L` by columns as a *gather* (B3 sums over the
140//! column of eta `p`): a gather cannot be driven by a reach, so HB6 turns it
141//! into a scatter along the rows of `L` (`i → p`), which needs the
142//! transposed copy of `L`. The row etas are the opposite: F2 is a gather
143//! (driven by the "readers" lists) and B2 a scatter (driven by the
144//! "targets" lists).
145//!
146//! ## Choice of the algorithm (alternation policy)
147//! Following Hall & McKinnon (2005, §4) and HiGHS, the hypersparse
148//! algorithm is used for a call iff
149//! (a) the right-hand side is in sparse mode (it has a pattern);
150//! (b) its density `count/m` is at most
151//!     [`LuOptions::hyper_rhs_density`](crate::LuOptions) (`0.10`);
152//! (c) the *expected* density of the result — an exponential moving
153//!     average, with weight [`HYPER_HISTORY_WEIGHT`] for the newest
154//!     result, of the result densities of the previous calls of the same
155//!     kind (FTRAN, BTRAN, FTRAN for update; kept across factorizations,
156//!     starting at 0) — is at most
157//!     [`LuOptions::hyper_result_density`](crate::LuOptions)
158//!     (`0.10`).
159//!
160//! When the result fills in, the reach costs more than the dense sweep
161//! (the search visits every arc once, then the numerical pass visits them
162//! again), so (c) switches a kind of operation to the dense algorithms
163//! after about three dense results, and back once results are sparse again
164//! (the dense algorithms measure their result density exactly). The
165//! per-kind statistics ([`SolveStats`]) are in [`crate::LuStats`].
166//!
167//! ## Division of labour (second edition)
168//! Steps F2, B2, HF4 and HB4 — the row etas — belong to the basis-update
169//! policy (Chapter 5.5): they are the methods
170//! [`BasisUpdate::apply_ftran`], [`BasisUpdate::apply_btran`],
171//! [`BasisUpdate::apply_ftran_hyper`] and [`BasisUpdate::apply_btran_hyper`]
172//! of [`crate::ForrestTomlin`], called at exactly the place where the first
173//! edition ran them inline, with the same work arrays. Everything else is
174//! here, on the stored factors.
175//!
176//! ## Invariants
177//! (V1) On return the [`SparseVec`] is in sparse mode. After a dense solve
178//!      its pattern is the exact support of the result (in increasing
179//!      order); after a hypersparse solve it is the *symbolic reach*, a
180//!      superset of the support that may contain explicit zeros (allowed by
181//!      the invariant (V2) of [`SparseVec`]; call [`SparseVec::tidy`] to
182//!      drop zeros and tiny entries). Computing the exact support would
183//!      cost one more pass for no benefit to the simplex method.
184//! (V2) Both work arrays are zero between calls.
185//! (V3) The dense and the hypersparse algorithms perform the same
186//!      operations except for the order of independent updates (and
187//!      HB6, which sums the terms of B3 one at a time): their results agree
188//!      to rounding (property test: relative error ≤ 1e-12).
189//!
190//! ## Complexity
191//! Dense: `O(m + nnz(L) + nnz(U) + nnz(etas))`. Hypersparse:
192//! `O(r + f + e log e)` with `r` the reach sizes, `f` the factor nonzeros
193//! in the columns (or rows) of the reach and `e` the number of row etas
194//! involved.
195//!
196//! ## References
197//! J. R. Gilbert and T. Peierls, "Sparse partial pivoting in time
198//! proportional to arithmetic operations", SIAM J. Sci. Stat. Comput. 9(5),
199//! 1988 (the reach by depth-first search); J. A. J. Hall and K. I. M.
200//! McKinnon, "Hyper-sparsity in the revised simplex method and how to
201//! exploit it", Comput. Optim. Appl. 32, 2005 (hypersparse FTRAN/BTRAN,
202//! the density thresholds); T. A. Davis, *Direct Methods for Sparse Linear
203//! Systems* (2006), §3.2 (`cs_reach`, `cs_dfs`); Maros (2003), §5.4–5.5;
204//! Forrest & Tomlin (1972); Suhl & Suhl (1990), §4; Koberstein (2005),
205//! §5.3; HiGHS `HFactor` (studied, not copied).
206
207use crate::SparseVec;
208use crate::factor::{Factors, Lu, VACANT, idx32};
209use crate::real::{Real, is_exact_zero, lt};
210use crate::update::BasisUpdate;
211
212/// Weight of the newest result in the moving average of the result
213/// density of each kind of solve (policy (c) of the module documentation).
214pub const HYPER_HISTORY_WEIGHT: f64 = 0.05;
215
216/// "No `L` eta" in the map from rows to `L` etas of [`Factors`].
217pub(crate) const NO_ETA: u32 = u32::MAX;
218
219/// Counters of one kind of solve (FTRAN, BTRAN or FTRAN for update),
220/// accumulated over the life of the [`Lu`] (not reset by a
221/// factorization). The contract's mean densities are the methods
222/// [`SolveStats::mean_rhs_density`] and
223/// [`SolveStats::mean_result_density`] over exact sums (no division per
224/// call).
225#[derive(Clone, Copy, Debug, Default, PartialEq)]
226pub struct SolveStats {
227    /// Number of calls.
228    pub calls: usize,
229    /// Calls served by the hypersparse algorithm (HF or HB).
230    pub hyper_calls: usize,
231    /// Sum over the calls of the right-hand side density `count/m`.
232    pub rhs_density_sum: f64,
233    /// Sum over the calls of the result density `count/m` (pattern size:
234    /// the reach for hypersparse calls, the exact support otherwise).
235    pub result_density_sum: f64,
236}
237
238impl SolveStats {
239    /// Mean right-hand side density (0 without calls).
240    pub fn mean_rhs_density(&self) -> f64 {
241        self.mean(self.rhs_density_sum)
242    }
243
244    /// Mean result density (0 without calls).
245    pub fn mean_result_density(&self) -> f64 {
246        self.mean(self.result_density_sum)
247    }
248
249    /// Fraction of the calls served by the hypersparse algorithm.
250    pub fn hyper_fraction(&self) -> f64 {
251        #[allow(clippy::cast_precision_loss)] // counts far below 2⁵³
252        let h = self.hyper_calls as f64;
253        self.mean(h)
254    }
255
256    fn mean(&self, sum: f64) -> f64 {
257        if self.calls == 0 {
258            0.0
259        } else {
260            #[allow(clippy::cast_precision_loss)]
261            let c = self.calls as f64;
262            sum / c
263        }
264    }
265
266    fn record(&mut self, hyper: bool, rhs_density: f64, result_density: f64) {
267        self.calls += 1;
268        self.hyper_calls += usize::from(hyper);
269        self.rhs_density_sum += rhs_density;
270        self.result_density_sum += result_density;
271    }
272}
273
274/// The kinds of solve, for the statistics and the moving averages.
275#[derive(Clone, Copy, Debug, PartialEq, Eq)]
276pub(crate) enum Kind {
277    Ftran = 0,
278    Btran = 1,
279    FtranUpdate = 2,
280}
281
282/// Work arrays of the solves and of the update (invariant (V2): the value
283/// arrays are zero between calls).
284#[derive(Clone, Debug)]
285pub(crate) struct Workspace<T: Real> {
286    /// Value work array (rows in FTRAN, positions in BTRAN and in the
287    /// update's spike row).
288    pub work: Vec<T>,
289    /// Second value work array (rows, BTRAN).
290    pub work2: Vec<T>,
291    /// Node marks of Algorithm D (and of the lists `Λ`).
292    pub mark: Vec<u32>,
293    /// Current stamp: `mark[v] == stamp` means marked.
294    pub stamp: u32,
295    /// The explicit stack of Algorithm D: `(node, next arc)`.
296    pub stack: Vec<(u32, u32)>,
297    /// The list `Λ`.
298    pub list: Vec<u32>,
299    /// The postorder of Algorithm D.
300    pub post: Vec<u32>,
301    /// Row etas currently in the priority queue of HF4/HB4.
302    pub queued: Vec<bool>,
303    /// Flags by position for the update's elimination (Chapter 5.5).
304    pub flag: Vec<bool>,
305}
306
307impl<T: Real> Default for Workspace<T> {
308    fn default() -> Self {
309        Self {
310            work: Vec::new(),
311            work2: Vec::new(),
312            mark: Vec::new(),
313            stamp: 0,
314            stack: Vec::new(),
315            list: Vec::new(),
316            post: Vec::new(),
317            queued: Vec::new(),
318            flag: Vec::new(),
319        }
320    }
321}
322
323impl<T: Real> Workspace<T> {
324    /// Sizes the arrays for dimension `m` (all zero).
325    pub fn resize(&mut self, m: usize) {
326        if self.work.len() != m {
327            *self = Self {
328                work: vec![T::ZERO; m],
329                work2: vec![T::ZERO; m],
330                mark: vec![0; m],
331                flag: vec![false; m],
332                ..Self::default()
333            };
334        }
335    }
336
337    /// A fresh stamp for Algorithm D, step D1.
338    pub fn new_stamp(&mut self) -> u32 {
339        if self.stamp == u32::MAX {
340            self.mark.fill(0);
341            self.stamp = 0;
342        }
343        self.stamp += 1;
344        self.stamp
345    }
346}
347
348/// Algorithm D: the nodes reachable from `seeds` in the graph whose
349/// successors of `v` are `adj(v)` are appended to `post` in postorder
350/// (read it backwards for a topological order). Uses the current stamp:
351/// marked nodes are not entered.
352fn reach<'g>(
353    mark: &mut [u32],
354    stamp: u32,
355    stack: &mut Vec<(u32, u32)>,
356    seeds: &[u32],
357    post: &mut Vec<u32>,
358    adj: impl Fn(usize) -> &'g [u32],
359) {
360    // D1.
361    for &s in seeds {
362        if mark[s as usize] == stamp {
363            continue;
364        }
365        mark[s as usize] = stamp;
366        stack.push((s, 0));
367        // D2.
368        while let Some(top) = stack.last_mut() {
369            let (v, k) = *top;
370            let succ = adj(v as usize);
371            if let Some(&w) = succ.get(k as usize) {
372                top.1 = k + 1;
373                if mark[w as usize] != stamp {
374                    mark[w as usize] = stamp;
375                    stack.push((w, 0));
376                }
377            } else {
378                stack.pop();
379                post.push(v);
380            }
381        }
382    }
383}
384
385impl<T: Real, U: BasisUpdate<T>> Lu<T, U> {
386    /// Overwrites `rhs` (indexed by rows) with `x = B⁻¹ rhs` (indexed by
387    /// positions) — Algorithm F or HF (see the alternation policy) — and,
388    /// for [`Kind::FtranUpdate`], records the spike `R L⁻¹ a_q` (after step
389    /// F2/HF4) for the next update.
390    ///
391    /// # Panics
392    /// If the factor is not valid or `rhs.size() != m` — both are
393    /// violations of the calling contract.
394    pub(crate) fn ftran_kind(&mut self, rhs: &mut SparseVec<T>, kind: Kind) {
395        self.factors.assert_ready(rhs);
396        let rhs_density = rhs.density();
397        let hyper = self.use_hyper(rhs, kind);
398        let mut ws = std::mem::take(&mut self.factors.ws);
399        let for_update = kind == Kind::FtranUpdate;
400        if hyper {
401            self.factors
402                .ftran_hyper(&self.update, rhs, &mut ws, for_update);
403        } else {
404            self.factors
405                .ftran_dense(&self.update, rhs, &mut ws, for_update);
406        }
407        self.factors.ws = ws;
408        if for_update {
409            self.factors.spike_valid = true;
410        }
411        self.record(kind, hyper, rhs_density, rhs.density());
412    }
413
414    /// Overwrites `rhs` (indexed by positions) with `y = B⁻ᵀ rhs` (indexed
415    /// by rows), by Algorithm B or HB (see the alternation policy).
416    ///
417    /// # Panics
418    /// As [`Lu::ftran_kind`].
419    pub(crate) fn btran_impl(&mut self, rhs: &mut SparseVec<T>) {
420        self.factors.assert_ready(rhs);
421        let rhs_density = rhs.density();
422        let hyper = self.use_hyper(rhs, Kind::Btran);
423        let mut ws = std::mem::take(&mut self.factors.ws);
424        if hyper {
425            self.factors.btran_hyper(&self.update, rhs, &mut ws);
426        } else {
427            self.factors.btran_dense(&self.update, rhs, &mut ws);
428        }
429        self.factors.ws = ws;
430        self.record(Kind::Btran, hyper, rhs_density, rhs.density());
431    }
432
433    /// The alternation policy (a)–(c).
434    fn use_hyper(&self, rhs: &SparseVec<T>, kind: Kind) -> bool {
435        rhs.is_sparse()
436            && !lt(self.opts.hyper_rhs_density, rhs.density())
437            && !lt(
438                self.opts.hyper_result_density,
439                self.expected_density[kind as usize],
440            )
441    }
442
443    /// Statistics and moving average of one call.
444    fn record(&mut self, kind: Kind, hyper: bool, rhs_density: f64, result_density: f64) {
445        let ema = &mut self.expected_density[kind as usize];
446        *ema = (1.0 - HYPER_HISTORY_WEIGHT) * *ema + HYPER_HISTORY_WEIGHT * result_density;
447        let st = &mut self.factors.stats;
448        let s = match kind {
449            Kind::Ftran => &mut st.ftran,
450            Kind::Btran => &mut st.btran,
451            Kind::FtranUpdate => &mut st.ftran_update,
452        };
453        s.record(hyper, rhs_density, result_density);
454    }
455
456    /// Expected result density of each kind of solve (policy (c)), for
457    /// diagnostics: `[ftran, btran, ftran_for_update]`.
458    pub fn expected_densities(&self) -> [f64; 3] {
459        self.expected_density
460    }
461}
462
463impl<T: Real> Factors<T> {
464    fn assert_ready(&self, rhs: &SparseVec<T>) {
465        assert!(
466            self.valid,
467            "solve with a factor that holds no valid factorization"
468        );
469        assert_eq!(rhs.size(), self.m, "right-hand side has the wrong size");
470    }
471
472    /// Records the spike `y` (row-indexed) given on the rows of `rows`,
473    /// or on all rows when `rows` is `None`.
474    fn record_spike(&mut self, y: &[T], rows: Option<&[u32]>) {
475        for &i in &self.spike_idx {
476            self.spike[i as usize] = T::ZERO;
477        }
478        self.spike_idx.clear();
479        match rows {
480            Some(rows) => {
481                for &i in rows {
482                    self.spike[i as usize] = y[i as usize];
483                }
484                self.spike_idx.extend_from_slice(rows);
485            }
486            None => {
487                for (i, (&v, s)) in y.iter().zip(self.spike.iter_mut()).enumerate() {
488                    if !is_exact_zero(v) {
489                        *s = v;
490                        self.spike_idx.push(idx32(i));
491                    }
492                }
493            }
494        }
495    }
496
497    // ----------------------------------------------------------------------
498    // Dense algorithms F and B.
499
500    /// Algorithm F.
501    fn ftran_dense<U: BasisUpdate<T>>(
502        &mut self,
503        upd: &U,
504        rhs: &mut SparseVec<T>,
505        ws: &mut Workspace<T>,
506        for_update: bool,
507    ) {
508        let y = rhs.values_mut();
509        self.apply_l(y);
510        upd.apply_ftran(y);
511        if for_update {
512            self.record_spike(y, None);
513        }
514        self.solve_u(y, &mut ws.work2);
515        rhs.rebuild_pattern();
516    }
517
518    /// Algorithm B.
519    fn btran_dense<U: BasisUpdate<T>>(
520        &self,
521        upd: &U,
522        rhs: &mut SparseVec<T>,
523        ws: &mut Workspace<T>,
524    ) {
525        let c = rhs.values_mut();
526        let w = &mut ws.work2;
527        self.solve_ut(c, w);
528        upd.apply_btran(w);
529        self.apply_lt(w);
530        // c is all zero after B1; swap the result in and keep (V2).
531        c.swap_with_slice(w);
532        rhs.rebuild_pattern();
533    }
534
535    /// Step F1.
536    pub(crate) fn apply_l(&self, y: &mut [T]) {
537        for (e, &p) in self.l_pivot.iter().enumerate() {
538            let y_p = y[p as usize];
539            if is_exact_zero(y_p) {
540                continue;
541            }
542            let r = self.l_start[e]..self.l_start[e + 1];
543            for (&i, &l) in self.l_index[r.clone()].iter().zip(&self.l_value[r]) {
544                y[i as usize] -= l * y_p;
545            }
546        }
547    }
548
549    /// Step F3: on entry `y` is indexed by rows, on exit by positions; `x`
550    /// is a zero scratch array of length `m`, zero again on exit.
551    fn solve_u(&self, y: &mut [T], x: &mut [T]) {
552        for s in (0..self.step_row.len()).rev() {
553            let (p, q) = (self.step_row[s], self.step_col[s]);
554            if p == VACANT {
555                continue;
556            }
557            let (p, q) = (p as usize, q as usize);
558            let x_q = y[p] / self.diag[p];
559            x[q] = x_q;
560            if is_exact_zero(x_q) {
561                continue;
562            }
563            let (idx, val) = self.u_cols.line(q);
564            for (&i, &u) in idx.iter().zip(val) {
565                y[i as usize] -= u * x_q;
566            }
567        }
568        // Every y_p was consumed (y is now zero up to the values x took).
569        y.copy_from_slice(x);
570        x.fill(T::ZERO);
571    }
572
573    /// Step B1: `c` is indexed by positions (and left zero); the result is
574    /// written into the zero row-indexed array `w`.
575    fn solve_ut(&self, c: &mut [T], w: &mut [T]) {
576        for s in 0..self.step_row.len() {
577            let (p, q) = (self.step_row[s], self.step_col[s]);
578            if p == VACANT {
579                continue;
580            }
581            let (p, q) = (p as usize, q as usize);
582            let w_p = c[q] / self.diag[p];
583            c[q] = T::ZERO;
584            w[p] = w_p;
585            if is_exact_zero(w_p) {
586                continue;
587            }
588            let (idx, val) = self.u_rows.line(p);
589            for (&j, &u) in idx.iter().zip(val) {
590                c[j as usize] -= u * w_p;
591            }
592        }
593    }
594
595    /// Step B3.
596    fn apply_lt(&self, w: &mut [T]) {
597        for (e, &p) in self.l_pivot.iter().enumerate().rev() {
598            let r = self.l_start[e]..self.l_start[e + 1];
599            let s: T = self.l_index[r.clone()]
600                .iter()
601                .zip(&self.l_value[r])
602                .map(|(&i, &l)| l * w[i as usize])
603                .sum();
604            w[p as usize] -= s;
605        }
606    }
607
608    // ----------------------------------------------------------------------
609    // Hypersparse algorithms HF and HB.
610
611    /// The successors of row `p` in `G_L`.
612    #[inline]
613    fn l_succ(&self, p: usize) -> &[u32] {
614        match self.l_eta_of_row[p] {
615            NO_ETA => &[],
616            e => &self.l_index[self.l_start[e as usize]..self.l_start[e as usize + 1]],
617        }
618    }
619
620    /// Position pivoted with row `p`.
621    #[inline]
622    fn q_of(&self, p: usize) -> usize {
623        self.step_col[self.row_step[p]] as usize
624    }
625
626    /// Row pivoted with position `q`.
627    #[inline]
628    fn p_of(&self, q: usize) -> usize {
629        self.step_row[self.col_step[q]] as usize
630    }
631
632    /// Algorithm HF.
633    fn ftran_hyper<U: BasisUpdate<T>>(
634        &mut self,
635        upd: &U,
636        rhs: &mut SparseVec<T>,
637        ws: &mut Workspace<T>,
638        for_update: bool,
639    ) {
640        // HF1. Load.
641        let mut seeds = std::mem::take(&mut ws.list);
642        seeds.clear();
643        for &i in rhs.pattern().unwrap_or(&[]) {
644            let v = rhs.get(i);
645            if !is_exact_zero(v) {
646                ws.work[i as usize] = v;
647                seeds.push(i);
648            }
649        }
650        rhs.clear();
651        // HF2. Reach of L.
652        let stamp = ws.new_stamp();
653        ws.post.clear();
654        reach(
655            &mut ws.mark,
656            stamp,
657            &mut ws.stack,
658            &seeds,
659            &mut ws.post,
660            |p| self.l_succ(p),
661        );
662        // HF3. Apply L in topological order; Λ = the reach.
663        let y = &mut ws.work;
664        for &p in ws.post.iter().rev() {
665            let e = self.l_eta_of_row[p as usize];
666            let y_p = y[p as usize];
667            if e == NO_ETA || is_exact_zero(y_p) {
668                continue;
669            }
670            let r = self.l_start[e as usize]..self.l_start[e as usize + 1];
671            for (&i, &l) in self.l_index[r.clone()].iter().zip(&self.l_value[r]) {
672                y[i as usize] -= l * y_p;
673            }
674        }
675        let mut list = seeds;
676        list.clear();
677        list.extend(ws.post.iter().rev());
678        // HF4. Row etas, in creation order through a min-queue
679        // (Chapter 5.5: the update policy owns them).
680        upd.apply_ftran_hyper(
681            &mut ws.work,
682            &mut list,
683            &mut ws.mark,
684            ws.stamp,
685            &mut ws.queued,
686        );
687        if for_update {
688            self.record_spike(&ws.work, Some(&list));
689        }
690        // HF5. Reach of U.
691        let stamp = ws.new_stamp();
692        ws.post.clear();
693        reach(
694            &mut ws.mark,
695            stamp,
696            &mut ws.stack,
697            &list,
698            &mut ws.post,
699            |p| self.u_cols.line(self.q_of(p)).0,
700        );
701        // HF6. Apply U in topological order.
702        let y = &mut ws.work;
703        for &p in ws.post.iter().rev() {
704            let p = p as usize;
705            let q = self.q_of(p);
706            let x_q = y[p] / self.diag[p];
707            y[p] = T::ZERO;
708            rhs.set(idx32(q), x_q);
709            if is_exact_zero(x_q) {
710                continue;
711            }
712            let (idx, val) = self.u_cols.line(q);
713            for (&i, &u) in idx.iter().zip(val) {
714                y[i as usize] -= u * x_q;
715            }
716        }
717        ws.list = list;
718    }
719
720    /// Algorithm HB.
721    fn btran_hyper<U: BasisUpdate<T>>(
722        &self,
723        upd: &U,
724        rhs: &mut SparseVec<T>,
725        ws: &mut Workspace<T>,
726    ) {
727        // HB1. Load (positions).
728        let mut list = std::mem::take(&mut ws.list);
729        list.clear();
730        for &q in rhs.pattern().unwrap_or(&[]) {
731            let v = rhs.get(q);
732            if !is_exact_zero(v) {
733                ws.work[q as usize] = v;
734                list.push(q);
735            }
736        }
737        rhs.clear();
738        // HB2. Reach of Uᵀ.
739        let stamp = ws.new_stamp();
740        ws.post.clear();
741        reach(
742            &mut ws.mark,
743            stamp,
744            &mut ws.stack,
745            &list,
746            &mut ws.post,
747            |q| self.u_rows.line(self.p_of(q)).0,
748        );
749        // HB3. Apply Uᵀ in topological order; Λ = the pivot rows reached.
750        list.clear();
751        for &q in ws.post.iter().rev() {
752            let q = q as usize;
753            let p = self.p_of(q);
754            let w_p = ws.work[q] / self.diag[p];
755            ws.work[q] = T::ZERO;
756            ws.work2[p] = w_p;
757            list.push(idx32(p));
758            if is_exact_zero(w_p) {
759                continue;
760            }
761            let (idx, val) = self.u_rows.line(p);
762            for (&j, &u) in idx.iter().zip(val) {
763                ws.work[j as usize] -= u * w_p;
764            }
765        }
766        let stamp = ws.new_stamp();
767        for &p in &list {
768            ws.mark[p as usize] = stamp;
769        }
770        // HB4. Row etas, in reverse creation order through a max-queue
771        // (Chapter 5.5: the update policy owns them).
772        upd.apply_btran_hyper(
773            &mut ws.work2,
774            &mut list,
775            &mut ws.mark,
776            ws.stamp,
777            &mut ws.queued,
778        );
779        // HB5. Reach of Lᵀ.
780        let stamp = ws.new_stamp();
781        ws.post.clear();
782        reach(
783            &mut ws.mark,
784            stamp,
785            &mut ws.stack,
786            &list,
787            &mut ws.post,
788            |i| &self.lt_pivot[self.lt_start[i]..self.lt_start[i + 1]],
789        );
790        // HB6. Apply Lᵀ in topological order, then store the result.
791        let w = &mut ws.work2;
792        for &i in ws.post.iter().rev() {
793            let i = i as usize;
794            let w_i = w[i];
795            if is_exact_zero(w_i) {
796                continue;
797            }
798            let r = self.lt_start[i]..self.lt_start[i + 1];
799            for (&p, &l) in self.lt_pivot[r.clone()].iter().zip(&self.lt_value[r]) {
800                w[p as usize] -= l * w_i;
801            }
802        }
803        for &i in ws.post.iter().rev() {
804            rhs.set(i, w[i as usize]);
805            w[i as usize] = T::ZERO;
806        }
807        ws.list = list;
808    }
809}