Skip to content

evd: the 2 x 2 block solves of the real eigenvector back substitution follow ?LALN2 - #152

Closed
shinaoka wants to merge 1 commit into
mainfrom
fix/145-real-evd-graded
Closed

shinaoka wants to merge 1 commit into
mainfrom
fix/145-real-evd-graded

Conversation

@shinaoka

Copy link
Copy Markdown
Member

Summary

Closes #145. On strongly graded matrices D A D^-1, the real nonsymmetric EVD returned wrong eigenvectors for every complex pair (GET22 up to 2.6e9).

Stage-by-stage comparison with LAPACK 3.12.0

f64, n = 64, D = diag(10^linspace(-6, 6, n)):

stage faer LAPACK
Schur backward error (eps |A|) 8.0
eigenvectors of faer's own Schur form T (dtrevc3 on the same T) 3.4e7 2.2e-4
dgeevx on A, balanc = N / P / S / B 0.58 / 0.58 / 2.1e-10 / 2.1e-10
faer on A after this fix 0.45
  • Where it breaks: in the eigenvector back substitution of the real Schur form, not in the reduction.
  • Which columns: all 24 failing eigen-blocks are complex pairs. For each, the first wrong entry is a 2×2 block solve above the pair's eigenvalue.
  • Balancing (?GEBAL) is not the cause: without it, dgeev gives 0.58 and the fixed faer 0.45, both well under the threshold. Balancing would bring both to about 1e-10. Adopting it is a separate decision.

Cause

  • solve_cplx_shifted_upper_quasi_triangular_system solved each shifted 2×2 block through its inverse, and compared the determinant with the entry-sized clamp eps_norm.
  • A determinant is a product of two entries. So every block with entries below sqrt(eps_norm) got det = eps_norm while keeping its original numerators (adjugate).
  • scaled_into_range (LAPACK validation: the nonsymmetric EVD fixtures exceed the GET22-style residual threshold (4 dtypes) #112) shrinks the blocks of a graded matrix enough for that to happen to every one of them.

Fix

  • Both 2×2 block solves now use solve_shifted_2x2_cplx, a port of ?LALN2's method:
    • if every entry is below eps_norm, the block is taken as eps_norm * I;
    • otherwise, Gaussian elimination with complete pivoting, with the second pivot raised to eps_norm when it is smaller.
  • The two solves are the complex-shift one and the real-shift one above a real eigenvalue. The real-shift one inverted through a^2 - b c, which underflowed to inf for tiny blocks.
  • Review history: the first version raised only the determinant. The independent review (gpt-6.1-sol) gave a 4×4 Schur form where that is not a solved system (residual 4.5e14). Its counterexample is now a test, and the fix was changed to the elimination above. The second review found no BLOCKER or IMPORTANT issues.

Tests (all four dtypes; the real ones reach the changed code)

test before
lapack_nonsymmetric_evd_graded_pairs_keep_their_residual: graded n = 16, 64, 128 plus a coupled 4×4, left and right GET22 f64 5.9e8 at n = 16, 1.5e12 on the 4×4
lapack_nonsymmetric_evd_pair_solve_with_a_small_determinant determinant-floor version: 4.5e14; main: f32 4.2e6
lapack_nonsymmetric_evd_real_shift_block_below_the_square_root_of_min_positive main: NoConvergence

Upstream (upstream-reports/faer-029-real-evd-pair-clamp)

  • faer 0.24.4 has the same comparison (z < eps * norm, src/linalg/evd/mod.rs:828).
  • It does not fail on the graded matrices, because upstream doesn't scale the Schur form first. It does fail on a 4×4 with two small, close complex pairs coupled by entries of size 1e8:
residual ratio
faer 0.24.4 8.97e4
LAPACK dgeev 9.6e-14
0.24.4 with this 2×2 solve swapped in 6.6e-14
fork after this fix 6.0e-14

Coordination

#112 (primerose-faer-autopilot) has no open PR. What remains there is the c32 complex path (kinds 6/7), and this change does not touch it.

🤖 Generated with Claude Code

… follow ?LALN2 (#145)

The back substitution for a complex eigenvalue of a real Schur form solved
each 2 x 2 diagonal block through its inverse and compared the block's
determinant with the entry-sized clamp `eps_norm`. A determinant is a
product of two entries, so every block with entries below
`sqrt(eps_norm)` got the determinant `eps_norm` with its original
numerators. After `scaled_into_range` the blocks of a strongly graded
matrix are all that small, so every complex pair got a wrong eigenvector
(GET22 up to 2.6e9).

Stage comparison with LAPACK 3.12.0 on f64 n = 64: the Schur form is fine
(backward error 8.0); on that same Schur form `dtrevc3` gives 2.2e-4 and
faer 3.4e7, and every failing eigen-block is a complex pair. Balancing is
not the cause (`dgeev` without it: 0.58; fixed faer: 0.45).

Both 2 x 2 solves now use `?LALN2`'s method (`solve_shifted_2x2_cplx`):
`eps_norm * I` when every entry is below `eps_norm`, otherwise
elimination with complete pivoting and the second pivot raised to
`eps_norm`. That covers the real-shift solve too, whose inverse through
`a^2 - b c` underflowed to `inf` for tiny blocks. Raising only the
determinant was rejected in review: with the original numerators it is not
a solved system (4.5e14 on a 4 x 4 Schur form, now a test).

Upstream faer 0.24.4 has the same comparison; it is added to
upstream-reports as faer-029 with a 4 x 4 input that fails there and
passes with this solve.

Closes #145

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@shinaoka

Copy link
Copy Markdown
Member Author

Combined into #154 together with the other two nonsymmetric EVD fixes (#148, #151, #152), as the maintainer directed on 2026-10-29. The commits are carried over with their authorship; the upstream reports are renumbered faer-029 (#112), faer-030 (#144) and faer-031 (#145).

@shinaoka shinaoka closed this Oct 29, 2026
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.

real EVD: eigenvectors of strongly graded matrices have GET22 residuals up to 2.6e9

1 participant