`posterior_of`, `posterior_of_at` and `expected_variance_reduction` each
built the joint precision matrix, factorised it, asked one question and
threw it away. The factorisation is O(n^3) in the history's appearances
and depends only on the fit, so a caller asking about every pair in a
standings table, every cell in a grid, or every candidate in an
active-learning sweep paid for the same factorisation once per question.
`History::joint()` returns a `Joint` handle that pays it once. Measured
on 1976 appearances, 90 queries: 68.4s one-shot against 745ms factorise
plus 93ms of queries — 81.6x, with bit-identical answers. Per query,
Criterion at 480 appearances: 9.0ms one-shot against 48us cached, 187x.
The handle borrows the history, which is what makes it correct with no
invalidation logic: the borrow checker forbids adding events or refitting
while it is alive, so there is no window in which the factorisation could
describe a fit that no longer exists. It also makes the lifetime of the
n^2 factor explicit rather than parking it in the history forever — at
4000 appearances that is 128MB, which is not something to cache silently.
Every question the joint answers turns out to be a bilinear form,
c^T A^-1 a = (L^-1 c) . (L^-1 a)
so no caller ever needs L^-1 c itself. Replacing the general solve with a
forward substitution drops the back substitution as wasted work, halving
a query, and removes a failure mode: a variance as `c . (A^-1 c)` is a
difference of products that can round negative, where `|L^-1 c|^2` is a
sum of squares and cannot.
The one-shot calls are unchanged in cost and now delegate to the handle,
so the two paths cannot drift apart. tests/joint_handle.rs asserts they
agree bit for bit, including at pinned times, under UnknownKeys::Prior,
and across candidate matchups.
Refs #51
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011hcFjNDmHXZF8URGLku5zZ
86 lines
2.2 KiB
TOML
86 lines
2.2 KiB
TOML
[package]
|
|
name = "trueskill-tt"
|
|
version = "0.6.0"
|
|
edition = "2024"
|
|
rust-version = "1.85"
|
|
description = "TrueSkill Through Time: Bayesian skill rating that tracks how skill evolves over time, via Gaussian message passing"
|
|
repository = "https://git.aceofba.se/logaritmisk/trueskill-tt"
|
|
authors = ["Anders Olsson"]
|
|
# Publishing is restricted to the private kellnr registry; this also makes
|
|
# an accidental `cargo publish` to crates.io a hard error rather than a
|
|
# irreversible mistake. Index is declared in `.cargo/config.toml`.
|
|
publish = ["kellnr"]
|
|
readme = "README.md"
|
|
keywords = ["trueskill", "rating", "bayesian", "elo", "skill"]
|
|
categories = ["algorithms", "science", "game-development"]
|
|
license = "MIT OR Apache-2.0"
|
|
# `examples/atp.csv` is a 48 MB tennis dataset — 99% of the packaged crate,
|
|
# for a library whose source is 312 KB. `examples/atp.rs` opens it by
|
|
# relative path at runtime, so excluding the data still compiles; the
|
|
# example just needs the file fetched from the repo to run.
|
|
exclude = [
|
|
"/docs",
|
|
"/benches/*.txt",
|
|
"/temp",
|
|
"/.gitea",
|
|
"/examples/atp.csv",
|
|
]
|
|
|
|
[lib]
|
|
bench = false
|
|
|
|
[[bench]]
|
|
name = "batch"
|
|
harness = false
|
|
|
|
[[bench]]
|
|
name = "gaussian"
|
|
harness = false
|
|
|
|
[[bench]]
|
|
name = "history_converge"
|
|
harness = false
|
|
|
|
[[bench]]
|
|
name = "scored"
|
|
harness = false
|
|
|
|
[[bench]]
|
|
name = "ingest"
|
|
harness = false
|
|
|
|
[dependencies]
|
|
approx = { version = "0.5.1", optional = true }
|
|
libm = "0.2.16"
|
|
rayon = { version = "1", optional = true }
|
|
smallvec = "1"
|
|
|
|
[features]
|
|
approx = ["dep:approx"]
|
|
rayon = ["dep:rayon"]
|
|
|
|
[dev-dependencies]
|
|
criterion = "0.5"
|
|
plotters = { version = "0.3", default-features = false, features = ["svg_backend", "all_elements", "all_series"] }
|
|
plotters-backend = "0.3"
|
|
proptest = "1.11.0"
|
|
time = { version = "0.3", features = ["parsing"] }
|
|
trueskill-tt = { path = ".", features = ["approx"] }
|
|
|
|
# Debug symbols in release are for `just flame` (cargo-flamegraph), which needs
|
|
# them to symbolicate. Profile settings in a library are ignored by downstream
|
|
# consumers, so these only affect local builds — this is deliberate, not an
|
|
# oversight.
|
|
[profile.release]
|
|
debug = true
|
|
|
|
[profile.bench]
|
|
debug = true
|
|
|
|
[profile.dev]
|
|
debug = true
|
|
|
|
[[bench]]
|
|
name = "joint"
|
|
harness = false
|