Skip to content

fix(eigen3): start from the eigenvalue with a multiplicity of 1 - #11

Open
Lanfei wants to merge 1 commit into
dimforge:mainfrom
Lanfei:fix-symmetric-eigen3-repeated-eigenvalue
Open

Lanfei wants to merge 1 commit into
dimforge:mainfrom
Lanfei:fix-symmetric-eigen3-repeated-eigenvalue

Conversation

@Lanfei

@Lanfei Lanfei commented Oct 15, 2026

Copy link
Copy Markdown

Problem

SymmetricEigen3::new returns wrong eigenvectors for a rotated matrix whose smallest eigenvalue is repeated. Its eigenvalues are correct, but V · diag(λ) · Vᵀ doesn't reconstruct the input.

For example, R · diag(0.02, 0.02, 0.035) · Rᵀ with R a rotation of 0.9 rad around (1, 2, 3) is reconstructed with a ~24% relative error. A distinct or fully repeated spectrum is fine.

This is the inertia tensor of any axisymmetric body. Downstream, parry3d::MassProperties::with_inertia_matrix (used when summing the mass properties of several colliders) gets wrong principal axes. In rapier3d 0.36, adding a zero-mass collider to a rigid-body whose only mass carrier has an axisymmetric, rotated inertia changes the body's inertia tensor by ~30%. rapier3d-compat 0.20, which still used nalgebra's eigensolver, was off by ~5e-5 on the same input.

Cause

new always computes the first eigenvector from the smallest eigenvalue with eigenvector1, which (as its documentation says) requires an eigenvalue with a multiplicity of 1. When the smallest eigenvalue is repeated, mat - λI has rank 1, so the cross products eigenvector1 relies on are pure rounding noise and give an arbitrary direction. Without a rotation the noise is exactly zero and the d_max < 1e-20 fallback happens to return the right axis, which is why the diagonal tests pass.

Fix

As in Eberly's algorithm, start from the extreme eigenvalue that is the farthest from the middle one, which is the one with a multiplicity of 1 when two eigenvalues are equal:

  • if λ1 - λ0 >= λ2 - λ1: v0 = eigenvector1(λ0), v1 = eigenvector2(v0, λ1), v2 = v0 × v1 (unchanged),
  • otherwise: v2 = eigenvector1(λ2), v1 = eigenvector2(v2, λ1), v0 = v1 × v2.

Eigenvalues stay in ascending order, eigenvector columns match them, and the basis stays right-handed.

Tests

  • New eigen_3x3_repeated_eigenvalue: rotated matrices with a repeated smallest or largest eigenvalue, for SymmetricEigen3 and SymmetricEigen3A. It checks the eigenvalues, orthonormality, right-handedness and the reconstruction. It fails without the fix and passes with it.
  • eigen_3x3 hard-coded the sign of each eigenvector, which isn't defined. With this change, the first and last ones come out negated (still a valid right-handed basis), so the test now compares each eigenvector up to its sign and checks the determinant.
  • cargo test --all-features, cargo test --no-default-features --features libm, cargo clippy --all-features -- -D warnings and cargo fmt --check pass.
  • Downstream, with this branch patched into parry3d 0.31.1, MassProperties::with_inertia_matrix on the example above goes from a 2.4e-1 to a 1.1e-7 reconstruction error. In a rapier3d JS build, the rigid-body inertia error mentioned above goes from 3e-1 to 1e-7.

AI assistance (Claude) was used to investigate, write the fix and the tests, and I reviewed the results.

SymmetricEigen3::new always computed the first eigenvector from the smallest
eigenvalue with eigenvector1, which requires a multiplicity of 1. When the
smallest eigenvalue is repeated (e.g. the inertia tensor of an axisymmetric
body), its eigenvector is dominated by rounding noise, so the decomposition
of a rotated such matrix doesn't reconstruct it.

Start from the extreme eigenvalue that is the farthest from the middle one,
as in Eberly's algorithm.
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