fix(gaussian): treat non-positive precision as improper in mu()/sigma()
EP message cancellation can leave a Gaussian's precision (pi) a tiny negative value — round-off of exactly zero. mu()/sigma() only special-cased pi == 0, so sigma() computed 1/sqrt(pi) = NaN for pi < 0. That NaN flowed through the moment-space Sub in the game diff-chain and poisoned every skill in the slice once it grew past ~75 competitors, making converge() return all-NaN on real-scale histories (regression vs 0.1.0, which stored sigma directly). Guard pi <= 0.0 in both accessors (improper Gaussian: mu 0, sigma infinite), matching the existing pi == 0 handling. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+31
-2
@@ -53,7 +53,11 @@ impl Gaussian {
|
||||
|
||||
#[inline]
|
||||
pub fn mu(&self) -> f64 {
|
||||
if self.pi == 0.0 {
|
||||
// A non-positive precision is an improper (uninformative) Gaussian — its mean is
|
||||
// undefined. Treat it like `pi == 0` and return 0. EP message cancellation can land
|
||||
// `pi` on a tiny negative value (round-off of exactly zero); without this guard
|
||||
// `tau / pi` would yield a spurious finite mean.
|
||||
if self.pi <= 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
self.tau / self.pi
|
||||
@@ -62,7 +66,10 @@ impl Gaussian {
|
||||
|
||||
#[inline]
|
||||
pub fn sigma(&self) -> f64 {
|
||||
if self.pi == 0.0 {
|
||||
// A non-positive precision is improper → infinite standard deviation. Guarding
|
||||
// `pi <= 0.0` (not just `== 0.0`) keeps `1.0 / pi.sqrt()` from returning NaN when EP
|
||||
// cancellation produces a tiny negative precision (round-off of exactly zero).
|
||||
if self.pi <= 0.0 {
|
||||
f64::INFINITY
|
||||
} else if self.pi.is_infinite() {
|
||||
0.0
|
||||
@@ -174,6 +181,28 @@ impl ops::Div<Gaussian> for Gaussian {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn non_positive_precision_is_improper_not_nan() {
|
||||
// EP message cancellation can leave `pi` a tiny negative (round-off of exactly zero).
|
||||
// Such a Gaussian is improper/uninformative: mu() must be 0 and sigma() infinite, not
|
||||
// NaN. A NaN here propagates through the moment-space `Sub` in the game chain and
|
||||
// poisons every skill in the slice.
|
||||
let tiny_neg = Gaussian::from_natural(-5.55e-17, -8.88e-16);
|
||||
assert_eq!(tiny_neg.mu(), 0.0);
|
||||
assert!(tiny_neg.sigma().is_infinite());
|
||||
|
||||
// A frankly-negative precision is treated the same way.
|
||||
let neg = Gaussian::from_natural(-1.0, 2.0);
|
||||
assert_eq!(neg.mu(), 0.0);
|
||||
assert!(neg.sigma().is_infinite());
|
||||
|
||||
// Subtracting such a message must not produce NaN (the original failure path).
|
||||
let proper = Gaussian::from_ms(9.75, 1.256);
|
||||
let diff = proper - tiny_neg;
|
||||
assert!(diff.pi().is_finite() && !diff.pi().is_nan());
|
||||
assert!(diff.tau().is_finite() && !diff.tau().is_nan());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add() {
|
||||
let n = Gaussian::from_ms(25.0, 25.0 / 3.0);
|
||||
|
||||
Reference in New Issue
Block a user