Skip to content

fix(lsmr): underflowed norms, Givens update scale, LSMR's own residual estimate - #391

Merged
schroedk merged 4 commits into
mainfrom
fix/lsmr-numerics
Sep 23, 2026
Merged

schroedk merged 4 commits into
mainfrom
fix/lsmr-numerics

Conversation

@schroedk

@schroedk schroedk commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

Stack 3/6 (fixes), based on main (#394 merged). Independent of the exit rework in #388.

  • α and β are recovered from a norm-scaled form when their squared sum underflows: ‖b‖ = 1e100 reported x = 0 converged at 0 iterations. The fused axpy kernel returns ‖y‖ itself, so no step holds a squared norm.
  • The (x, h, h̄) update divides by one Givens diagonal at a time without an absolute ε floor: ‖A‖ ≈ 1e-9 or beyond 1e±154 lost the update.
  • The residual estimate is LSMR's own ‖r_k‖, from Fong & Saunders' third rotation sequence ResidualChain (Q̃, §3.4), instead of LSQR's |φ̄|, which understates it, so a residual-tolerance stop could fire before the iterate met the tolerance.

Each fix's test fails with the fix reverted. The third fix moves residual-tolerance stops and has not been benchmarked on its own.

@schroedk
schroedk added this pull request to stack #393 September 23, 2026 07:24
Base automatically changed from refactor/demeaned-owner to main September 23, 2026 07:32
@schroedk
schroedk force-pushed the fix/lsmr-numerics branch 3 times, most recently from 61f3f1f to 8ad42ac Compare September 23, 2026 13:16
@schroedk
schroedk removed this pull request from stack #393 September 23, 2026 13:17
@schroedk
schroedk changed the base branch from main to refactor/lsmr-rotation-types September 23, 2026 13:17
@schroedk
schroedk added this pull request to stack #396 September 23, 2026 13:17
Base automatically changed from refactor/lsmr-rotation-types to main September 23, 2026 13:21
Every α and β in the stream is a norm read off an unscaled squared sum:
the square vanishes at magnitudes the norm itself represents, and the
exit read that zero as a verdict. `α₁ = √(p̃ᵀM⁻¹p̃)` on a design with
`‖b‖ = 1e100` reported `x = 0` converged at zero iterations.

`alpha_from_vp` falls back to a norm-scaled product, and the fused
kernel `axpy_with_norm` returns `‖y‖` itself through `norm_from_sq`,
which only pays the scaled pass when the sum left the normal range. The
three `underflowed_alpha` cases now solve instead of certifying `x = 0`.
`SolutionState::update` floored its Givens diagonals at an absolute
`f64::EPSILON`, on the claim that they are O(1). They scale with `‖A‖`:
at `‖A‖ = 1e-9` the floor dropped the whole update and left `x = 0`
describing a residual the recurrence reported as solved. Their product
scales with `‖A‖²` and leaves the double range near `‖A‖ ≈ 1e±154`,
where `ζ / (ρρ̄)` read as zero or NaN. Only an exactly vanished diagonal
now ends a chain, and each factor divides by one diagonal in turn.
`residual_estimate` returned `|φ̄_k|`, the residual of LSQR's iterate on
the same Krylov space. LSQR minimizes `‖r‖` there, so it understates the
residual of the `x_k` LSMR returns (0.453 against a recomputed 0.479 on
the Vandermonde fixture after two steps), and a residual-tolerance stop
could fire before the iterate met the tolerance.

The third rotation chain of Fong & Saunders §3.4, the one SciPy's `lsmr`
runs, recovers `‖r_k‖` from scalars already in hand. It takes their sign
for `ᾱ`, under which LSQR's `−c α` would flip `β̂` on every other step.
@schroedk
schroedk merged commit 77975cc into main Sep 23, 2026
5 checks passed
@schroedk
schroedk deleted the fix/lsmr-numerics branch September 26, 2026 06:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant