Skip to main content

xingo_sparse/
update.rs

1//! # Chapter 5.5 — The Forrest–Tomlin update and the refactorization policy
2//!
3//! ## Purpose
4//! A simplex iteration replaces one basic column: `B' = B + (a_q − B e_r)
5//! e_rᵀ`, where `r` is the leaving *position*. Refactorizing at every
6//! iteration would cost far more than the iteration itself, so the factors
7//! are *updated*. Forrest and Tomlin (1972) keep `L` untouched, replace one
8//! column of `U` by the transformed entering column (the *spike*) and
9//! restore triangularity by eliminating one row, recorded as a row eta.
10//! Their update keeps `U` as sparse as a fresh factorization in practice,
11//! which the product form of the inverse does not.
12//!
13//! ## Notation
14//! As in Chapter 5.4, `B = Pᵀ L R₁⁻¹ ⋯ R_t⁻¹ U Qᵀ`. Let `s` be the spike
15//! `R_t ⋯ R_1 L⁻¹ a_q` (indexed by rows, recorded by
16//! [`crate::LuFactor::ftran_for_update`]), `α_q = B⁻¹ a_q` and `α_r` its entry
17//! at position `r`. Let `τ = step(r)`, `p = p_τ` the pivot row of that
18//! step and `u_old = u_{p r}` the diagonal entry of `U` there.
19//!
20//! ## Algorithm U (Forrest–Tomlin update, classical variant)
21//! U1. [Locate.] Find `τ = step(r)` and `p = p_τ`. Fail with
22//!     [`LuError::NoSpike`] if no spike was recorded since the factor
23//!     last changed.
24//! U2. [Spike row.] Copy row `p` of `U` (entries `u_pj`, `step(j) > τ`) into
25//!     a dense work vector `w` indexed by positions, and let `σ ← s_p`.
26//! U3. [Eliminate.] Think of column `r` of `U` as replaced by `s` and of
27//!     step `τ` as moved to the end of the pivot sequence (rows and columns
28//!     shifted cyclically). Row `p` is then the only row below the diagonal
29//!     and must be eliminated with the rows of the later steps, in step
30//!     order: for the steps `k > τ` with `j = q_k`, if `w_j` is not
31//!     negligible, set `μ_k ← w_j / u_{p_k j}`, `w_j ← 0`,
32//!     `w_l ← w_l − μ_k u_{p_k l}` for the entries of row `p_k`, and
33//!     `σ ← σ − μ_k s_{p_k}`. Since row `p_k` only has entries in later
34//!     steps, the fill lands where it is eliminated later.
35//! U4. [Test.] The new diagonal is `d = σ`. Since `det B' = α_r det B` and
36//!     the cyclic shifts of rows and columns cancel in sign, exact arithmetic
37//!     gives `d = α_r u_old`. Let `ζ = |s_p| + Σ_k |μ_k s_{p_k}|`, the size
38//!     of the terms summed into `σ` (its rounding error is `O(ε ζ)`). If
39//!     `|d| ≤ τ_abs max(1, ζ)`, the new basis is singular:
40//!     return [`LuError::Singular`] with position `r` and row `p`. If
41//!     `|d − α_r u_old| > ε_FT max(|d|, |α_r u_old|)`, the update is
42//!     numerically unreliable: return
43//!     [`UpdateOutcome::RefactorNeeded`]`(`[`RefactorReason::Unstable`]`)`.
44//!     In both cases nothing has been modified: the factor still represents
45//!     `B`.
46//! U5. [Commit.] Delete the old column `r` and the off-diagonal row `p` from
47//!     both copies of `U`; insert the spike entries `s_i` (`i ≠ p`,
48//!     `|s_i| > ε_drop`) as the new column `r`; set `u_{pr} ← d`; move
49//!     step `τ` to the end of the sequence (its slot `τ` becomes *vacant*
50//!     and the step is appended, so the step numbers grow beyond `m − 1`
51//!     until the next factorization; only their order matters); store the
52//!     row eta
53//!     `R = I − e_p μᵀ` if `μ ≠ 0`. Now `R L⁻¹ P B' Q' = U'` with `U'` upper
54//!     triangular in the new step order.
55//! U6. [Policy.] Count the update and decide whether to ask for a
56//!     refactorization (below).
57//!
58//! The variant is the *classical* one: the spike is stored as a column of
59//! `U` and the elimination of row `p` uses a dense position-indexed work
60//! vector. The steps of U3 with a nonzero `w_j` are visited through a
61//! min-priority queue of steps (task P1.4) rather than by scanning
62//! `τ+1..m`, U5 inserts the spike from its recorded pattern and moves step
63//! `τ` to the end by leaving a vacant slot (no renumbering), so the cost of
64//! an update is proportional to the entries involved, as with the linked
65//! permutation lists of Suhl's variant.
66//!
67//! ## Refactorization policy
68//! The factor never refactorizes by itself — the columns of `B` belong to
69//! the caller. After a successful update, [`crate::LuFactor::update`]
70//! returns [`UpdateOutcome::RefactorNeeded`] with the first applicable
71//! reason, the update *already applied* and the factor valid:
72//!
73//! * [`RefactorReason::UpdateLimit`] — `refactor_interval` updates (default
74//!   100) since the last factorization;
75//! * [`RefactorReason::FillGrowth`] — `nnz(L) + nnz(U) + nnz(etas)` exceeds
76//!   `fill_factor` (default 3) times its value right after the
77//!   factorization: solves have become more expensive than a refactorization
78//!   would make them;
79//! * [`RefactorReason::Growth`] — the *updates* have made `U` grow by more
80//!   than `growth_limit` (default `10¹⁰`): with `g_0 = max |u_ij|` right
81//!   after the factorization and `β = max |b_ij|`,
82//!   `max |u_ij| > growth_limit · max(β, g_0)`.
83//!
84//! The growth trigger is measured *from the fresh factorization*, not from
85//! `B`: a refactorization can only undo the growth the updates added, so a
86//! basis whose fresh factors already grow past the limit (MAROS-R7 of
87//! Netlib with the Markowitz threshold `0.01`: `max |u| / max |b| > 10¹⁰`
88//! straight out of the Markowitz phase) must not ask for one at every
89//! update — the old test,
90//! `max |u_ij| / β > growth_limit`, did exactly that and turned the solve
91//! into a loop of refactorizations (278 instead of 854 iterations per
92//! second). The absolute growth `max |u_ij| / β` stays available as a
93//! diagnostic ([`crate::LuStats::growth_exceeded`]); taking the larger
94//! of `β` and `g_0` as the reference makes the trigger identical to the
95//! old one when the fresh factor did not grow (`g_0 ≤ β`). The cure of a
96//! grown fresh factor is a tighter threshold, a decision of the caller
97//! ([`crate::LuFactor::set_options`] with a larger
98//! [`crate::LuOptions::rel_pivot_tol`]; the simplex method of Chapter 6
99//! does it).
100//!
101//! Only [`RefactorReason::Unstable`] comes with the update *not* applied.
102//! The caller should refactorize before the next solve in every case; with
103//! the first three it may postpone (the factor is exact up to rounding).
104//!
105//! ## The update as a policy (second edition)
106//! The contract makes the update algorithm a type parameter of the
107//! factorization, `Lu<T, U = ForrestTomlin<T>>`, through the trait
108//! [`BasisUpdate`] (D-0080, addendum): an update policy owns its own data
109//! — here the row etas `R_e` and the lists of the etas that read or target
110//! each row — and reaches the stored factors through `&mut`
111//! [`crate::Factors`]. Besides Algorithm U it provides the steps of the
112//! solves that apply its data (F2, B2, HF4 and HB4 of Chapter 5.4), called
113//! by the solves at the point where the first edition ran them inline.
114//! Bartels–Golub (task L1.4) will be the second implementation. The trait
115//! is implementable inside the crate only (its arguments are opaque
116//! outside), which keeps the factors' invariants in one place.
117//!
118//! ## Invariants
119//! (F1)–(F3) of Chapter 5.3 hold after every successful update (checked by
120//! the property tests through [`crate::LuFactor::check_invariants`]); FTRAN and
121//! BTRAN after `k` updates agree with a fresh factorization of the updated
122//! basis (property test, relative error ≤ 1e-9).
123//!
124//! ## References
125//! Forrest & Tomlin, "Updated triangular factors of the basis to maintain
126//! sparsity in the product form simplex method", Math. Programming 2(1),
127//! 1972; Suhl & Suhl, "A fast LU update for linear programming", Annals of
128//! OR 43, 1993; Maros (2003), §8.3 (Forrest–Tomlin) and §8.5 (stability
129//! checks); Koberstein (2005), §5.4; Huangfu & Hall (2018) for the
130//! comparison `d` versus `α_r u_old`.
131
132use std::cmp::Reverse;
133use std::collections::BinaryHeap;
134use std::fmt::Debug;
135
136use crate::error::LuError;
137use crate::factor::{Factors, LuOptions, VACANT, count_f64, idx32};
138use crate::real::{Real, is_exact_zero, is_zero, lt};
139use crate::solve::Workspace;
140use crate::{Row, SparseError};
141
142/// Why [`crate::LuFactor::update`] asks for a refactorization.
143#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
144pub enum RefactorReason {
145    /// `refactor_interval` updates since the last factorization (applied).
146    UpdateLimit,
147    /// Fill of `L + U + etas` above `fill_factor` times its initial size
148    /// (applied).
149    FillGrowth,
150    /// Growth added by the updates above `growth_limit` (applied; see
151    /// [`UpdateStats::update_growth`]).
152    Growth,
153    /// The new diagonal disagrees with `α_r · u_old`: the update was
154    /// **not** applied and the factor still represents the old basis.
155    Unstable,
156}
157
158/// Result of a successful update.
159#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
160pub enum UpdateOutcome {
161    /// The factor now represents the new basis.
162    Updated,
163    /// The caller should refactorize; see [`RefactorReason`] for whether
164    /// the update was applied.
165    RefactorNeeded(RefactorReason),
166}
167
168/// Counters of the update policy since the last factorization.
169#[derive(Clone, Copy, Debug, Default, PartialEq)]
170pub struct UpdateStats {
171    /// Updates since the last factorization.
172    pub updates: usize,
173    /// Multipliers stored in the row etas.
174    pub nnz_eta: usize,
175    /// Number of row etas (updates whose spike row needed elimination).
176    pub n_etas: usize,
177    /// Growth added by the updates: `max_u / max(max_basis, max_u_fresh)`
178    /// in the terms of [`crate::LuStats`] (at most `1` right after a
179    /// factorization). The policy compares it with
180    /// [`LuOptions::growth_limit`].
181    pub update_growth: f64,
182}
183
184/// A basis-update policy over the stored factors (the contract's
185/// `BasisUpdate<T>`; see the module documentation). Every implementation
186/// is transactional — on `Err` and on [`RefactorReason::Unstable`] the
187/// factors are exactly as before — and never modifies `L`.
188pub trait BasisUpdate<T: Real>: Default + Clone + Debug {
189    /// Replaces the column at position `r` by the entering column whose
190    /// spike the last `ftran_for_update` left in `factors`; `alpha_r` is
191    /// entry `r` of its result.
192    fn update(
193        &mut self,
194        factors: &mut Factors<T>,
195        opts: &LuOptions,
196        r: usize,
197        alpha_r: T,
198    ) -> Result<UpdateOutcome, LuError>;
199    /// Step F2 of the dense FTRAN on the row-indexed work array `y`.
200    fn apply_ftran(&self, y: &mut [T]);
201    /// Step B2 of the dense BTRAN on the row-indexed work array `w`.
202    fn apply_btran(&self, w: &mut [T]);
203    /// Step HF4 of the hypersparse FTRAN: `list` is the reach `Λ`, marked
204    /// in `mark` with `stamp`; rows the etas add are appended and marked.
205    /// `queued` is a zero work array with one flag per eta.
206    fn apply_ftran_hyper(
207        &self,
208        y: &mut [T],
209        list: &mut Vec<u32>,
210        mark: &mut [u32],
211        stamp: u32,
212        queued: &mut [bool],
213    );
214    /// Step HB4 of the hypersparse BTRAN, as [`BasisUpdate::apply_ftran_hyper`].
215    fn apply_btran_hyper(
216        &self,
217        w: &mut [T],
218        list: &mut Vec<u32>,
219        mark: &mut [u32],
220        stamp: u32,
221        queued: &mut [bool],
222    );
223    /// Forgets every update: called after each factorization of an
224    /// `m × m` basis, with the update growth of the fresh factors (`0`
225    /// after a failed factorization).
226    fn reset(&mut self, m: usize, update_growth: f64);
227    /// The counters since the last factorization.
228    fn stats(&self) -> &UpdateStats;
229    /// A short name for logs (`"forrest-tomlin"`).
230    fn name(&self) -> &'static str;
231}
232
233/// Forrest & Tomlin (1972): row etas and a permuted, modified `U`
234/// (Algorithm U).
235#[derive(Clone, Debug)]
236pub struct ForrestTomlin<T: Real> {
237    /// Row etas: eta `e` performs `y[eta_target[e]] −= Σ μ_k y[eta_index[k]]`
238    /// over its range.
239    eta_start: Vec<usize>,
240    eta_target: Vec<u32>,
241    eta_index: Vec<u32>,
242    eta_value: Vec<T>,
243    /// For each row `k`, the row etas that read it (`k` among their
244    /// indices), in creation order.
245    eta_readers: Vec<Vec<u32>>,
246    /// For each row `p`, the row etas that target it, in creation order.
247    eta_targets: Vec<Vec<u32>>,
248    /// Rows with a nonempty `eta_readers` or `eta_targets` list (possibly
249    /// repeated), to clear them at the next factorization.
250    eta_rows: Vec<u32>,
251    stats: UpdateStats,
252}
253
254impl<T: Real> Default for ForrestTomlin<T> {
255    fn default() -> Self {
256        Self {
257            eta_start: vec![0],
258            eta_target: Vec::new(),
259            eta_index: Vec::new(),
260            eta_value: Vec::new(),
261            eta_readers: Vec::new(),
262            eta_targets: Vec::new(),
263            eta_rows: Vec::new(),
264            stats: UpdateStats::default(),
265        }
266    }
267}
268
269impl<T: Real> BasisUpdate<T> for ForrestTomlin<T> {
270    /// Algorithm U. Errors: [`LuError::Singular`] if the new basis is
271    /// singular, [`LuError::NoSpike`] without a recorded spike,
272    /// [`LuError::NotFactorized`] on an invalid factor,
273    /// [`LuError::Structure`] if `r ≥ m`. On every error, and on
274    /// [`RefactorReason::Unstable`], the factor is left unchanged.
275    fn update(
276        &mut self,
277        f: &mut Factors<T>,
278        opts: &LuOptions,
279        r: usize,
280        alpha_r: T,
281    ) -> Result<UpdateOutcome, LuError> {
282        if !f.valid {
283            return Err(LuError::NotFactorized);
284        }
285        if r >= f.m {
286            return Err(SparseError::IndexOutOfRange {
287                what: "leaving basis position",
288                index: r,
289                bound: f.m,
290            }
291            .into());
292        }
293        if !f.spike_valid {
294            return Err(LuError::NoSpike);
295        }
296        // U1. Locate.
297        let tau = f.col_step[r];
298        let p = f.step_row[tau] as usize;
299        let u_old = f.diag[p];
300
301        // U2–U3. Eliminate the spike row (the factor is not modified).
302        let mut ws = std::mem::take(&mut f.ws);
303        let (eta, d, zeta) = eliminate_spike_row(f, opts, tau, p, &mut ws);
304        f.ws = ws;
305
306        // U4. Singularity and stability tests (nothing modified yet).
307        if !lt(T::from_f64(opts.abs_pivot_tol) * zeta.max(T::ONE), d.abs()) {
308            return Err(LuError::Singular {
309                rank_deficiency: 1,
310                cols: vec![r],
311                rows: vec![Row::from_usize(p)],
312            });
313        }
314        let expected = alpha_r * u_old;
315        let scale = d.abs().max(expected.abs());
316        if lt(
317            T::from_f64(opts.update_stability_tol) * scale,
318            (d - expected).abs(),
319        ) {
320            return Ok(UpdateOutcome::RefactorNeeded(RefactorReason::Unstable));
321        }
322
323        // U5. Commit.
324        self.commit(f, opts, r, tau, p, d, eta);
325        debug_assert!(f.check_invariants().is_ok(), "{:?}", f.check_invariants());
326
327        // U6. Refactorization policy.
328        Ok(self.policy(f, opts))
329    }
330
331    /// Step F2.
332    #[inline]
333    fn apply_ftran(&self, y: &mut [T]) {
334        for e in 0..self.eta_target.len() {
335            self.apply_eta(e, y);
336        }
337    }
338
339    /// Step B2.
340    #[inline]
341    fn apply_btran(&self, w: &mut [T]) {
342        for e in (0..self.eta_target.len()).rev() {
343            self.apply_eta_t(e, w);
344        }
345    }
346
347    /// Step HF4: the row etas in creation order through a min-queue.
348    fn apply_ftran_hyper(
349        &self,
350        y: &mut [T],
351        list: &mut Vec<u32>,
352        mark: &mut [u32],
353        stamp: u32,
354        queued: &mut [bool],
355    ) {
356        if self.eta_target.is_empty() {
357            return;
358        }
359        let mut heap: BinaryHeap<Reverse<u32>> = BinaryHeap::new();
360        for &i in list.iter() {
361            for &e in &self.eta_readers[i as usize] {
362                if !queued[e as usize] {
363                    queued[e as usize] = true;
364                    heap.push(Reverse(e));
365                }
366            }
367        }
368        while let Some(Reverse(e)) = heap.pop() {
369            queued[e as usize] = false;
370            self.apply_eta(e as usize, y);
371            let t = self.eta_target[e as usize] as usize;
372            if mark[t] != stamp {
373                mark[t] = stamp;
374                list.push(idx32(t));
375                let readers = &self.eta_readers[t];
376                let after = readers.partition_point(|&e2| e2 <= e);
377                for &e2 in &readers[after..] {
378                    if !queued[e2 as usize] {
379                        queued[e2 as usize] = true;
380                        heap.push(Reverse(e2));
381                    }
382                }
383            }
384        }
385    }
386
387    /// Step HB4: the row etas in reverse creation order through a
388    /// max-queue.
389    fn apply_btran_hyper(
390        &self,
391        w: &mut [T],
392        list: &mut Vec<u32>,
393        mark: &mut [u32],
394        stamp: u32,
395        queued: &mut [bool],
396    ) {
397        if self.eta_target.is_empty() {
398            return;
399        }
400        let mut heap: BinaryHeap<u32> = BinaryHeap::new();
401        for &p in list.iter() {
402            for &e in &self.eta_targets[p as usize] {
403                if !queued[e as usize] {
404                    queued[e as usize] = true;
405                    heap.push(e);
406                }
407            }
408        }
409        while let Some(e) = heap.pop() {
410            queued[e as usize] = false;
411            self.apply_eta_t(e as usize, w);
412            let r = self.eta_start[e as usize]..self.eta_start[e as usize + 1];
413            for &k in &self.eta_index[r] {
414                let k = k as usize;
415                if mark[k] == stamp {
416                    continue;
417                }
418                mark[k] = stamp;
419                list.push(idx32(k));
420                let targets = &self.eta_targets[k];
421                let before = targets.partition_point(|&e2| e2 < e);
422                for &e2 in &targets[..before] {
423                    if !queued[e2 as usize] {
424                        queued[e2 as usize] = true;
425                        heap.push(e2);
426                    }
427                }
428            }
429        }
430    }
431
432    fn reset(&mut self, m: usize, update_growth: f64) {
433        self.eta_start = vec![0];
434        self.eta_target.clear();
435        self.eta_index.clear();
436        self.eta_value.clear();
437        if self.eta_readers.len() == m {
438            for &k in &self.eta_rows {
439                self.eta_readers[k as usize].clear();
440                self.eta_targets[k as usize].clear();
441            }
442        } else {
443            self.eta_readers = vec![Vec::new(); m];
444            self.eta_targets = vec![Vec::new(); m];
445        }
446        self.eta_rows.clear();
447        self.stats = UpdateStats {
448            update_growth,
449            ..UpdateStats::default()
450        };
451    }
452
453    #[inline]
454    fn stats(&self) -> &UpdateStats {
455        &self.stats
456    }
457
458    fn name(&self) -> &'static str {
459        "forrest-tomlin"
460    }
461}
462
463impl<T: Real> ForrestTomlin<T> {
464    /// Row eta `e` in FTRAN: `y_t ← y_t − Σ_k μ_k y_{p_k}`.
465    #[inline]
466    fn apply_eta(&self, e: usize, y: &mut [T]) {
467        let r = self.eta_start[e]..self.eta_start[e + 1];
468        let s: T = self.eta_index[r.clone()]
469            .iter()
470            .zip(&self.eta_value[r])
471            .map(|(&k, &mu)| mu * y[k as usize])
472            .sum();
473        y[self.eta_target[e] as usize] -= s;
474    }
475
476    /// Row eta `e` in BTRAN: if `w_t ≠ 0`, `w_{p_k} ← w_{p_k} − μ_k w_t`.
477    #[inline]
478    fn apply_eta_t(&self, e: usize, w: &mut [T]) {
479        let w_t = w[self.eta_target[e] as usize];
480        if is_exact_zero(w_t) {
481            return;
482        }
483        let r = self.eta_start[e]..self.eta_start[e + 1];
484        for (&k, &mu) in self.eta_index[r.clone()].iter().zip(&self.eta_value[r]) {
485            w[k as usize] -= mu * w_t;
486        }
487    }
488
489    /// Step U5.
490    #[allow(clippy::too_many_arguments)] // the quantities of steps U1–U4
491    fn commit(
492        &mut self,
493        f: &mut Factors<T>,
494        opts: &LuOptions,
495        r: usize,
496        tau: usize,
497        p: usize,
498        d: T,
499        eta: Vec<(u32, T)>,
500    ) {
501        let drop_tol = T::from_f64(opts.drop_tol);
502        let (r32, p32) = (idx32(r), idx32(p));
503        // Delete the old column r and the off-diagonal row p of U.
504        let old_col: Vec<u32> = f.u_cols.line(r).0.to_vec();
505        for i in old_col {
506            f.u_rows.remove(i as usize, r32);
507        }
508        f.u_cols.clear(r);
509        let old_row: Vec<u32> = f.u_rows.line(p).0.to_vec();
510        for j in old_row {
511            f.u_cols.remove(j as usize, p32);
512        }
513        f.u_rows.clear(p);
514        // Insert the spike as the new column r.
515        let mut max_u = f.stats.max_u.max(d.abs().to_f64());
516        for &i in &f.spike_idx {
517            let (iu, s_i) = (i as usize, f.spike[i as usize]);
518            if iu == p || is_zero(s_i, drop_tol) {
519                continue;
520            }
521            f.u_cols.push(r, i, s_i);
522            f.u_rows.push(iu, r32, s_i);
523            max_u = max_u.max(s_i.abs().to_f64());
524        }
525        f.diag[p] = d;
526        // Move step τ to the end: vacate its slot and append it, so that no
527        // other step is renumbered (O(1) instead of O(m)).
528        f.step_row[tau] = VACANT;
529        f.step_col[tau] = VACANT;
530        let last = f.step_row.len();
531        f.step_row.push(p32);
532        f.step_col.push(r32);
533        f.row_step[p] = last;
534        f.col_step[r] = last;
535        // Store the row eta.
536        if !eta.is_empty() {
537            let e = idx32(self.eta_target.len());
538            self.eta_target.push(p32);
539            self.eta_targets[p].push(e);
540            self.eta_rows.push(p32);
541            for (k, mu) in eta {
542                self.eta_index.push(k);
543                self.eta_value.push(mu);
544                self.eta_readers[k as usize].push(e);
545                self.eta_rows.push(k);
546            }
547            self.eta_start.push(self.eta_index.len());
548            f.ws.queued.push(false);
549        }
550        f.spike_valid = false;
551        f.stats.max_u = max_u;
552        self.stats.updates += 1;
553        self.stats.nnz_eta = self.eta_value.len();
554        self.stats.n_etas = self.eta_target.len();
555        self.stats.update_growth = f.refresh_stats(opts.growth_limit, self.stats.nnz_eta);
556    }
557
558    /// Step U6: the refactorization policy.
559    fn policy(&self, f: &Factors<T>, opts: &LuOptions) -> UpdateOutcome {
560        let s = &f.stats;
561        let u = &self.stats;
562        let nnz_now = s.nnz_l + s.nnz_u + u.nnz_eta;
563        if u.updates >= opts.refactor_interval {
564            UpdateOutcome::RefactorNeeded(RefactorReason::UpdateLimit)
565        } else if lt(
566            opts.fill_factor * count_f64(f.nnz_factor0),
567            count_f64(nnz_now),
568        ) {
569            UpdateOutcome::RefactorNeeded(RefactorReason::FillGrowth)
570        } else if lt(opts.growth_limit, u.update_growth) {
571            UpdateOutcome::RefactorNeeded(RefactorReason::Growth)
572        } else {
573            UpdateOutcome::Updated
574        }
575    }
576}
577
578/// Steps U2–U3: returns the row eta `(p_k, μ_k)`, the new diagonal
579/// `d = σ` and the scale `ζ = |s_p| + Σ |μ_k s_{p_k}|` of the terms
580/// summed into it. The work array `ws.work` (by positions) is zero on
581/// entry and on exit.
582///
583/// The steps `k` with a nonzero `w_{q_k}` are visited in increasing
584/// order through a min-priority queue instead of scanning `τ+1..m`:
585/// the same operations in the same order as the scan, at a cost
586/// proportional to the entries of `U` involved (Chapter 5.4).
587fn eliminate_spike_row<T: Real>(
588    f: &Factors<T>,
589    opts: &LuOptions,
590    tau: usize,
591    p: usize,
592    ws: &mut Workspace<T>,
593) -> (Vec<(u32, T)>, T, T) {
594    let drop_tol = T::from_f64(opts.drop_tol);
595    let w = &mut ws.work;
596    let flag = &mut ws.flag;
597    let mut heap: BinaryHeap<Reverse<usize>> = BinaryHeap::new();
598    // U2. Row p of U into the work vector.
599    let (idx, val) = f.u_rows.line(p);
600    for (&j, &u) in idx.iter().zip(val) {
601        w[j as usize] = u;
602        flag[j as usize] = true;
603        heap.push(Reverse(f.col_step[j as usize]));
604    }
605    let mut sigma = f.spike[p];
606    let mut zeta = sigma.abs();
607
608    // U3. Eliminate row p with the rows of steps τ+1, …, m−1.
609    let mut eta: Vec<(u32, T)> = Vec::new();
610    while let Some(Reverse(k)) = heap.pop() {
611        debug_assert!(k > tau);
612        let j = f.step_col[k] as usize;
613        let w_j = w[j];
614        w[j] = T::ZERO;
615        flag[j] = false;
616        if is_zero(w_j, drop_tol) {
617            continue;
618        }
619        let p_k = f.step_row[k] as usize;
620        let mu = w_j / f.diag[p_k];
621        let (idx, val) = f.u_rows.line(p_k);
622        for (&l, &u) in idx.iter().zip(val) {
623            let l = l as usize;
624            if !flag[l] {
625                flag[l] = true;
626                heap.push(Reverse(f.col_step[l]));
627            }
628            w[l] -= mu * u;
629        }
630        sigma -= mu * f.spike[p_k];
631        zeta += (mu * f.spike[p_k]).abs();
632        eta.push((idx32(p_k), mu));
633    }
634    (eta, sigma, zeta)
635}