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.
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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
src/lib.rs:720:Both determinants are products of
k-1diagonal 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:Across parameter sets:
0NaN0NaNThe 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)) * kunderflows to 0 atk ~ 52whenbeta = 1e-3,det(middle)underflows too, and the result is0/0.Mechanism confirmed by an independent mpmath reconstruction of the same matrices; predicted and observed transitions agree exactly:
0.0NaNNaNReachability
quality()directly, andHistory::predict_quality. Neither caps the group count — unlikepredict_outcome, which hasMAX_PREDICTED_TEAMS = 6. So a 200-team quality query is a supported call that silently returns0.Fix
s_argonly ever feeds.sqrt()and is then multiplied byexp(e_arg), so the whole thing is a drop-in log-space rearrangement:accumulating
ln|diagonal|during the LU rather than multiplying.src/matrix.rs:86-89already has the diagonal in hand, so this is local tomatrix.rsplus one line inlib.rs.The LU itself is fine — verified against the analytic value to ~3e-15 across
k = 2..150, partial pivoting is textbook, and thesign = 0.0singular path is stable under later negation. The defect is only the linear-space ratio.Secondary: an undocumented panic
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 fnwith a message from a private module. Worth either documenting or converting.Found by a floating-point audit, 2026-09-09. Reproduced independently.