quality() returns 0 then NaN past ~150 groups: a determinant ratio in linear space #59

Closed
opened 2026-09-09 14:51:42 +00:00 by logaritmisk · 0 comments
Owner

src/lib.rs:720:

let s_arg = ata.determinant() / middle.determinant();
libm::exp(e_arg) * s_arg.sqrt()

Both determinants are products of k-1 diagonal entries in linear space. The implementation matches the closed form (beta / sqrt(beta^2 + sigma^2))^(k-1) to ~3e-15 until one of them over- or underflows, then fails hard.

Measured

Crate defaults (sigma = 25/3, beta = 25/6), single-member groups, reproduced independently:

groups= 2   quality = 4.472135954999579e-1
groups= 50  quality = 7.502999089839427e-18
groups=150  quality = 8.447625976290425e-53   <- correct
groups=200  quality = 0e0
groups=250  quality = NaN
groups=300  quality = NaN

Across parameter sets:

sigma, beta last correct k first wrong k returned true value
25/3, 25/6 150 200 0 2.83e-70
25/3, 25/6 250 NaN 9.51e-88
50, 25/6 60 100 0 1.03e-107
1e-3, 1e-3 52 60 NaN 1.32e-9

The last row is the sharp one: at 60 groups with a small beta the true answer is 1.3e-9, entirely representable, and the function returns NaN. det(ata) = beta^(2(k-1)) * k underflows to 0 at k ~ 52 when beta = 1e-3, det(middle) underflows too, and the result is 0/0.

Mechanism confirmed by an independent mpmath reconstruction of the same matrices; predicted and observed transitions agree exactly:

n sigma, beta log10 abs det(ata) log10 abs det(middle)
150 25/3, 25/6 186.87 291.02 both finite, correct
200 25/3, 25/6 248.98 388.07 finite/inf = 0.0
250 25/3, 25/6 311.05 485.10 inf/inf = NaN
100 8, 100 398.00 398.27 NaN

Reachability

quality() directly, and History::predict_quality. Neither caps the group count — unlike predict_outcome, which has MAX_PREDICTED_TEAMS = 6. So a 200-team quality query is a supported call that silently returns 0.

Fix

s_arg only ever feeds .sqrt() and is then multiplied by exp(e_arg), so the whole thing is a drop-in log-space rearrangement:

exp(e_arg + 0.5 * (logdet(ata) - logdet(middle)))

accumulating ln|diagonal| during the LU rather than multiplying. src/matrix.rs:86-89 already has the diagonal in hand, so this is local to matrix.rs plus one line in lib.rs.

The LU itself is fine — verified against the analytic value to ~3e-15 across k = 2..150, partial pivoting is textbook, and the sign = 0.0 singular path is stable under later negation. The defect is only the linear-space ratio.

Secondary: an undocumented panic

quality(&[&[Gaussian::from_ms(0.0, 0.0)], &[Gaussian::from_ms(1.0, 0.0)]], 0.0)
  -> panic "cannot invert a singular matrix"   (src/matrix.rs:180)

quality(..., 1.0) -> NaN

quality()'s documented panics are only "fewer than two groups" and "an empty group". A zero-sigma rating is degenerate input, but the panic escapes a #[must_use] pub fn with a message from a private module. Worth either documenting or converting.

Found by a floating-point audit, 2026-09-09. Reproduced independently.

`src/lib.rs:720`: ```rust let s_arg = ata.determinant() / middle.determinant(); libm::exp(e_arg) * s_arg.sqrt() ``` Both determinants are products of `k-1` diagonal entries in linear space. The implementation matches the closed form `(beta / sqrt(beta^2 + sigma^2))^(k-1)` to ~3e-15 until one of them over- or underflows, then fails hard. ## Measured Crate defaults (`sigma = 25/3`, `beta = 25/6`), single-member groups, reproduced independently: ``` groups= 2 quality = 4.472135954999579e-1 groups= 50 quality = 7.502999089839427e-18 groups=150 quality = 8.447625976290425e-53 <- correct groups=200 quality = 0e0 groups=250 quality = NaN groups=300 quality = NaN ``` Across parameter sets: | sigma, beta | last correct k | first wrong k | returned | true value | |---|---|---|---|---| | 25/3, 25/6 | 150 | 200 | `0` | 2.83e-70 | | 25/3, 25/6 | | 250 | `NaN` | 9.51e-88 | | 50, 25/6 | 60 | 100 | `0` | 1.03e-107 | | **1e-3, 1e-3** | **52** | **60** | **`NaN`** | **1.32e-9** | The last row is the sharp one: at 60 groups with a small beta the true answer is **1.3e-9**, entirely representable, and the function returns `NaN`. `det(ata) = beta^(2(k-1)) * k` underflows to 0 at `k ~ 52` when `beta = 1e-3`, `det(middle)` underflows too, and the result is `0/0`. Mechanism confirmed by an independent mpmath reconstruction of the same matrices; predicted and observed transitions agree exactly: | n | sigma, beta | log10 abs det(ata) | log10 abs det(middle) | | |---|---|---|---|---| | 150 | 25/3, 25/6 | 186.87 | 291.02 | both finite, correct | | 200 | 25/3, 25/6 | 248.98 | **388.07** | finite/inf = `0.0` | | 250 | 25/3, 25/6 | **311.05** | **485.10** | inf/inf = `NaN` | | 100 | 8, 100 | **398.00** | **398.27** | `NaN` | ## Reachability `quality()` directly, and `History::predict_quality`. Neither caps the group count — unlike `predict_outcome`, which has `MAX_PREDICTED_TEAMS = 6`. So a 200-team quality query is a supported call that silently returns `0`. ## Fix `s_arg` only ever feeds `.sqrt()` and is then multiplied by `exp(e_arg)`, so the whole thing is a drop-in log-space rearrangement: ``` exp(e_arg + 0.5 * (logdet(ata) - logdet(middle))) ``` accumulating `ln|diagonal|` during the LU rather than multiplying. `src/matrix.rs:86-89` already has the diagonal in hand, so this is local to `matrix.rs` plus one line in `lib.rs`. The LU itself is fine — verified against the analytic value to ~3e-15 across `k = 2..150`, partial pivoting is textbook, and the `sign = 0.0` singular path is stable under later negation. The defect is only the linear-space ratio. ## Secondary: an undocumented panic ``` quality(&[&[Gaussian::from_ms(0.0, 0.0)], &[Gaussian::from_ms(1.0, 0.0)]], 0.0) -> panic "cannot invert a singular matrix" (src/matrix.rs:180) quality(..., 1.0) -> NaN ``` `quality()`'s documented panics are only "fewer than two groups" and "an empty group". A zero-sigma rating is degenerate input, but the panic escapes a `#[must_use] pub fn` with a message from a private module. Worth either documenting or converting. Found by a floating-point audit, 2026-09-09. Reproduced independently.
logaritmisk added the bugnumerics labels 2026-09-09 14:53:54 +00:00
Sign in to join this conversation.