7aa7fb62ddf7ef8214041baa88698009753bbac7
3
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6139061740 |
fix: keep the truncated variance representable in the far tail
`v_w` returned `w` and let `trunc` form `1 - w`. `w` tends to 1 out in the tail, so that subtraction lost about log10(alpha^2) digits — and the quantity it was destroying is perfectly representable. Two separate cancellations, fixed separately. The non-tie half: `half_line_truncation` now returns `1 - w` computed symbolically rather than as `1 - v*gap`. With `alpha*gap = 1 - inv^2*b` the leading ones cancel on paper instead of in floating point. Measured against the exact truncated variance: alpha before after 1e6 8.9e-5 rel 0.0 rel (exact) 1e8 returns 0.0 0.0 rel (exact) At 1e8 the old form gave `sigma_trunc = 0`, and `from_ms(mu, 0.0)` is a point mass whose `mu()` is inf/inf = NaN. `beta(1e-8).sigma(1e-8)` with priors 1000 apart went from Err + NaN skills to a finite fit. The tie half is a different subtraction — `w = v^2 - u`, where both grow as alpha^2 while their difference stays O(1). The existing escape hatch could not cover it: it keys on `alpha * width >= HALF_LINE_WINDOW`, how many window-widths from the mean the window sits, and a NARROW window fails that however deep it is. Measured at alpha 1e6 with a 1e-6 window it kept four digits and returned `1 - w = -2.4e-4` where the truth is +2.8e-13. One step earlier it was quietly wrong instead: `1 - w = 1.0` exactly, a truncation reported as a no-op, where the truth was 5e-17. Over a narrow window the density is a truncated exponential in `s = (x - alpha)/width`, whose mean and variance are closed forms, so `v = alpha + width*m(t)` and `1 - w = width^2 * V(t)` with no large subtraction at all. Validated against high-precision quadrature: v exact to 4e-10, `1 - w` to 4e-10 across the region it is used in. The crossover is on `alpha / width` rather than on either alone, because that ratio is what says how many digits the subtraction has left — and the approximation is most accurate exactly where the subtraction is worst, since both improve as the window narrows. Defaults are bit-identical (pi 0.02398318151216503 before and after). Tests: the three reproductions from the issue, the narrow-window form against pinned quadrature values, and a continuity sweep across all three tie branches — a misplaced crossover is the real risk here, and a jump at a boundary is visible even without pinning absolute values. Closes #60 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011hcFjNDmHXZF8URGLku5zZ |
||
|
|
c65373f476 |
fix!: propagate NaN through the convergence reduction
`tuple_max` compared with a plain `>`, which is false against NaN, so a
NaN accumulator was replaced by the next finite delta. The fold runs over
`TimeSlice::posteriors()`, a HashMap, so whether a NaN survived to `step`
depended on per-process hash order.
Measured before, four competitors in one slice with one pathological
pair, same binary and input, 30 separate processes:
16 Ok converged=true, iterations=1, a = Gaussian { pi: NaN, tau: NaN }
14 Err NonFiniteResult
After: 30/30 Err. A coin flip on whether a NaN fit was reported as an
error or as a successful, converged fit — inside the guard whose entire
purpose is "NaN is never convergence".
`f64::max` would not have fixed it. It also ignores NaN by design, which
is the same defect wearing a standard-library name, and a test pins that
we do not use it.
`Gaussian::delta` had to be fixed FIRST, and that ordering is the whole
subtlety. Two identical improper messages produced `(0.0, NaN)` — not
from `mu()`, which is guarded and returns 0.0, but from `inf - inf` in
the sigma component. That NaN is reachable in ordinary healthy inference:
once a pairing is more than about nine cavity-sigma apart the truncation
is a no-op and the chain compares one identity message against another.
Propagating NaN without fixing `delta` would therefore have turned
correct fits into NonFiniteResult errors. `delta` now answers the
identical-message case in natural space before touching the accessors.
My first version of the `delta` test asserted `mu()` was NaN. It is not;
the accessor guards `pi <= 0.0`. The test caught my own wrong premise,
and the doc comment is corrected to match.
BREAKING CHANGE: a fit that produced NaN in a non-final reduction
position previously returned `Ok` with `converged: true` and a NaN
posterior; it now returns `Err(NonFiniteResult)`. That was always the
documented intent.
Closes #58
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011hcFjNDmHXZF8URGLku5zZ
|
||
|
|
a18df521eb |
test: cover non-finite results and color-group disjointness
The two gaps #26 named that were never filled. NonFiniteResult had no test at all — the name appeared in `tests/` only inside a doc comment, and it is the sub-claim in that issue's title. It turns out to be very much reachable, and from *finite* inputs: sigma at 1e300, beta at 1e300, sigma at 1e-300, score_sigma at 1e-300, and scores at 1e308 all overflow inside inference, where the boundary checks cannot see them. That matters because the failure is silent by default — NaN fails every comparison, so a naive `step < epsilon` reads a NaN step as converged, which is why the crate has `step_converged`/`step_is_finite`. Pinned from outside, including that `converge_partial` does not launder a breakdown into an `Ok`, and with a control asserting merely extreme parameters still converge so the suite cannot pass by always failing. Color-group disjointness was #26's fourth acceptance criterion and had only five hand-written cases. Now a proptest over three shapes: a dense pool where collisions force colors to multiply, a sparse one where most events are independent, and repeated members within a single event. Two of my first assertions were wrong about the code rather than the reverse. A competitor named twice *within* one event is not a collision — `color_greedy` collects each event's members into a set for that reason. And contiguity is not a property of `color_greedy`: it holds only after `recompute_color_groups` reorders events so each color occupies one range. The test now asserts what is actually promised — that the reorder is always *possible*, since the parallel sweep slices `&mut` sub-ranges from those groups and overlapping ranges would be unsound. Refs #26 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011hcFjNDmHXZF8URGLku5zZ |