converge() returns all-NaN once a time slice exceeds ~75 competitors (regression vs 0.1.0) #3

Closed
opened 2026-06-12 20:26:21 +00:00 by logaritmisk · 0 comments
Owner

Status: fixed in v0.1.2 (commit on main). Filing for the record.

Symptom

History::converge() produces all-NaN skills once a single time slice holds more than ~75 distinct competitors (finite at ≤70, NaN at ≥80; ~300 1v1 events, even in a single time bucket). 0.1.0 was finite on identical data. This broke a downstream consumer's production ratings (hundreds of players → 100% NaN) when it upgraded 0.1.0 → 0.1.1.

Reproducer (deterministic, no real data)

N competitors, ~4 random 1v1 games each, one time slice, default config (β=1, σ=6, drift=0.1, max_iter=30). At N≥75 every current_skill() is NaN. (Added as tests/large_history_converges_finite.rs.)

Root cause

0.1.1 switched Gaussian to natural-param storage (pi, tau) with sigma() = 1/sqrt(pi) computed lazily, but mu()/sigma() only special-cased pi == 0.0, not pi < 0. EP message cancellation in the game diff-chain (TruncFactor) leaves an outgoing message precision at a tiny negative value (~-5.5e-17 — round-off of exactly zero). sigma() = 1/sqrt(negative) = NaN, which the moment-space Gaussian - Gaussian in run_chain's boundary update propagates into lhood_win/lhood_lose → every skill in the slice goes NaN. Larger slices give the EP more refinement passes, so a message reliably lands on the round-off-negative precision past the threshold. 0.1.0 stored sigma as a plain field, so it never hit 1/sqrt(pi).

Fix

Guard pi <= 0.0 (not just == 0.0) in Gaussian::mu() (→ 0) and sigma() (→ ∞) — an improper/uninformative Gaussian, matching the existing pi == 0 handling. Verified: reproducer 0 NaN at 12–250 competitors; full suite green; a downstream real-data fit (3442 votes, 669 ranked) is finite end-to-end.

Prevention

Added two regression tests: a Gaussian accessor unit test (non-positive precision → improper, not NaN) and a ≥75-competitor converge() finiteness test.

🤖 Generated with Claude Code

**Status:** fixed in v0.1.2 (commit on `main`). Filing for the record. ## Symptom `History::converge()` produces **all-NaN** skills once a single time slice holds more than ~75 distinct competitors (finite at ≤70, NaN at ≥80; ~300 1v1 events, even in a single time bucket). 0.1.0 was finite on identical data. This broke a downstream consumer's production ratings (hundreds of players → 100% NaN) when it upgraded 0.1.0 → 0.1.1. ## Reproducer (deterministic, no real data) N competitors, ~4 random 1v1 games each, one time slice, default config (β=1, σ=6, drift=0.1, max_iter=30). At N≥75 every `current_skill()` is NaN. (Added as `tests/large_history_converges_finite.rs`.) ## Root cause 0.1.1 switched `Gaussian` to natural-param storage (`pi`, `tau`) with `sigma() = 1/sqrt(pi)` computed lazily, but `mu()`/`sigma()` only special-cased `pi == 0.0`, not `pi < 0`. EP message cancellation in the game diff-chain (`TruncFactor`) leaves an outgoing message precision at a **tiny negative value (~-5.5e-17 — round-off of exactly zero)**. `sigma() = 1/sqrt(negative) = NaN`, which the moment-space `Gaussian - Gaussian` in `run_chain`'s boundary update propagates into `lhood_win`/`lhood_lose` → every skill in the slice goes NaN. Larger slices give the EP more refinement passes, so a message reliably lands on the round-off-negative precision past the threshold. 0.1.0 stored `sigma` as a plain field, so it never hit `1/sqrt(pi)`. ## Fix Guard `pi <= 0.0` (not just `== 0.0`) in `Gaussian::mu()` (→ 0) and `sigma()` (→ ∞) — an improper/uninformative Gaussian, matching the existing `pi == 0` handling. Verified: reproducer 0 NaN at 12–250 competitors; full suite green; a downstream real-data fit (3442 votes, 669 ranked) is finite end-to-end. ## Prevention Added two regression tests: a `Gaussian` accessor unit test (non-positive precision → improper, not NaN) and a ≥75-competitor `converge()` finiteness test. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: logaritmisk/trueskill-tt#3