Adds soft Gaussian-observation evidence on the per-pair diff variable,
enabling continuous score margins as a richer alternative to ranks.
Public API:
- `Outcome::Scored([scores])` (non-breaking enum extension under
`#[non_exhaustive]`).
- `Game::scored(teams, outcome, options)` constructor parallel to
`Game::ranked`.
- `EventBuilder::scores([...])` fluent helper.
- `HistoryBuilder::score_sigma(σ)` knob (default 1.0, validated > 0).
- `GameOptions::score_sigma`.
- `EventKind` re-exported from `lib.rs` (annotated `#[non_exhaustive]`).
- New `InferenceError::InvalidParameter { name, value }` variant.
Internals:
- `MarginFactor` (`factor/margin.rs`): Gaussian observation factor that
closes in one EP step; cavity-cached log-evidence mirrors `TruncFactor`.
- `BuiltinFactor::Margin` dispatch arm.
- `DiffFactor` enum in `game.rs` lets `Game::likelihoods` and the new
`likelihoods_scored` share the per-pair link abstraction.
- Per-event `EventKind { Ranked, Scored { score_sigma } }` routed through
`TimeSlice::add_events`, `iteration_direct`, and `log_evidence`.
Tests: 88 lib + 27 integration (4 new in `tests/scored.rs`); existing
goldens byte-identical. Bench: `benches/scored.rs` baseline ~960µs for
60 events × 20-player pool with default convergence.
Plan: docs/superpowers/plans/2026-04-27-t4-margin-factor.md
Spec item marked Done.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
48 lines
1.4 KiB
Rust
48 lines
1.4 KiB
Rust
use criterion::{Criterion, criterion_group, criterion_main};
|
|
use trueskill_tt::{
|
|
BETA, Competitor, EventKind, GAMMA, KeyTable, MU, P_DRAW, Rating, SIGMA, TimeSlice,
|
|
drift::ConstantDrift, gaussian::Gaussian, storage::CompetitorStore,
|
|
};
|
|
|
|
fn criterion_benchmark(criterion: &mut Criterion) {
|
|
let mut index_map = KeyTable::new();
|
|
|
|
let a = index_map.get_or_create("a");
|
|
let b = index_map.get_or_create("b");
|
|
let c = index_map.get_or_create("c");
|
|
|
|
let mut agents: CompetitorStore<i64, ConstantDrift> = CompetitorStore::new();
|
|
|
|
for agent in [a, b, c] {
|
|
agents.insert(
|
|
agent,
|
|
Competitor {
|
|
rating: Rating::new(Gaussian::from_ms(MU, SIGMA), BETA, ConstantDrift(GAMMA)),
|
|
..Default::default()
|
|
},
|
|
);
|
|
}
|
|
|
|
let mut composition = Vec::new();
|
|
let mut results = Vec::new();
|
|
let mut weights = Vec::new();
|
|
|
|
for _ in 0..100 {
|
|
composition.push(vec![vec![a], vec![b]]);
|
|
results.push(vec![1.0, 0.0]);
|
|
weights.push(vec![vec![1.0], vec![1.0]]);
|
|
}
|
|
|
|
let kinds = vec![EventKind::Ranked; composition.len()];
|
|
|
|
let mut time_slice = TimeSlice::new(1, P_DRAW);
|
|
time_slice.add_events(composition, results, weights, kinds, &agents);
|
|
|
|
criterion.bench_function("Batch::iteration", |b| {
|
|
b.iter(|| time_slice.iteration(0, &agents))
|
|
});
|
|
}
|
|
|
|
criterion_group!(benches, criterion_benchmark);
|
|
criterion_main!(benches);
|