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}