Commit Graph

43 Commits

Author SHA1 Message Date
ec8b7e538c feat(api): add fluent history.event(t).team(...).commit() builder
Third tier of the ingestion API (spec Section 4). Powers one-off
events with irregular shapes where neither record_winner (too
simple) nor typed add_events (too verbose) fits cleanly.

EventBuilder accumulates teams, weights, and outcome. Supports:
- .team([keys]) — add a team
- .weights([w..]) — per-member weights on the most-recently-added team
- .ranking([ranks]) — explicit per-team ranks
- .winner(i) — convenience: team i wins, others tied
- .draw() — all teams tied
- .commit() — finalize into an Event<T, K> and delegate to add_events

Part of T2 of docs/superpowers/specs/2026-04-23-trueskill-engine-redesign-design.md.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 12:42:26 +02:00
a6e008f8ff feat(api): add ConvergenceOptions, ConvergenceReport, History::converge
New public types:
- ConvergenceOptions { max_iter, epsilon } — config for the loop
- ConvergenceReport { iterations, final_step, log_evidence, converged,
  per_iteration_time, slices_skipped } — post-hoc summary

History and HistoryBuilder gain a third generic parameter
O: Observer<T> = NullObserver. Builder methods:
- .convergence(opts) sets the ConvergenceOptions
- .observer(o) plugs in an Observer (reshapes the builder's O param)

History::converge() runs the existing iteration loop driven by the
stored opts, emits observer callbacks on each iteration end and on
completion, and returns Result<ConvergenceReport, InferenceError>.

The old convergence(iters, eps, verbose) stays — gets removed in
Task 20 after tests are translated.

Part of T2 of docs/superpowers/specs/2026-04-23-trueskill-engine-redesign-design.md.
2026-04-24 12:20:24 +02:00
726896a2ba feat(api): add Observer trait and NullObserver default
Observer replaces verbose: bool with structured progress callbacks:
on_iteration_end, on_batch_processed, on_converged — all no-op
default impls so users override only what they need. NullObserver
is a ZST default.

Send + Sync bounds deferred to T3 (Rayon support).

Fully additive — wired into History::converge in Task 12.

Part of T2 of docs/superpowers/specs/2026-04-23-trueskill-engine-redesign-design.md.
2026-04-24 12:16:25 +02:00
f5a486329e feat(api): add Event<T, K>, Team<K>, Member<K> typed event description
Replaces the old nested Vec<Vec<Vec<_>>> event description on the
public API boundary. Member<K>::from(K) enables ergonomic literal
lists. Member::with_weight / with_prior are builder methods for the
optional per-event overrides.

Fully additive — no existing call sites updated. Consumed by
History::add_events(iter) in Task 15.

Part of T2 of docs/superpowers/specs/2026-04-23-trueskill-engine-redesign-design.md.
2026-04-24 12:14:58 +02:00
3df422db78 feat(api): add Outcome enum with Ranked variant
Outcome::winner(i, n), Outcome::draw(n), Outcome::ranking(iter) are
the convenience constructors. Marked #[non_exhaustive] so Scored can
be added in T4 without breaking match exhaustiveness.

Adds smallvec = "1" as a direct dependency.

Part of T2 of docs/superpowers/specs/2026-04-23-trueskill-engine-redesign-design.md.
2026-04-24 12:12:53 +02:00
33a7d90b89 refactor(history): remove time: bool; translate tests to explicit timestamps
The bool encoded 'no time axis' which is now expressed at the type
level (T = Untimed). The old !self.time branch generated sequential
i64 timestamps internally (1..=n) and bumped all agents' last_time at
every tick; tests that relied on this now pass those timestamps
explicitly and reflect the correct time=true elapsed semantics.

Collapsed `if self.time { A } else { B }` into the A branch everywhere
in add_events_with_prior. Removed the two !self.time blocks that
updated all agents' last_time at every slice regardless of participation.

sort_time is now generic over `T: Copy + Ord`.

HistoryBuilder::time(bool) removed. History<i64, ConstantDrift>
default remains, producing the same behavior as old .time(true).

The test_env_ttt Gaussian goldens are updated to reflect the correct
time=true semantics (b.elapsed=2 instead of 1 due to b skipping t=2);
this is a correction: the old !self.time last_time bump was an
implementation quirk that diverged from the Python reference.

55 tests pass. clippy clean. fmt clean.

Part of T2 of docs/superpowers/specs/2026-04-23-trueskill-engine-redesign-design.md.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 12:09:23 +02:00
a285c1a0f2 feat(api): add Time trait with Untimed and i64 impls
Foundation for generic History time axis. Untimed is the ZST case
(no drift across slices); i64 is the standard timestamp case.
Additional impls (time::OffsetDateTime, chrono) can be added behind
feature flags in follow-up work.

The trait is not yet wired into History — that happens in Task 7
along with generifying Drift over T.

Part of T2 of docs/superpowers/specs/2026-04-23-trueskill-engine-redesign-design.md.
2026-04-24 11:32:38 +02:00
5e752f9e98 refactor(api): rename Batch to TimeSlice
TimeSlice says what it is: every event sharing one timestamp. The
History field .batches is renamed to .time_slices. Local variables
named `batch` referring to TimeSlice instances are renamed to
`time_slice`.

Part of T2 of docs/superpowers/specs/2026-04-23-trueskill-engine-redesign-design.md.
2026-04-24 10:54:31 +02:00
decbd895a3 refactor(api): rename Agent to Competitor and .player field to .rating
Competitor holds dynamic per-history state (message, last_time) for
someone competing; its configuration lives in a Rating.

AgentStore renamed to CompetitorStore to match. The internal
`clean()` free function's parameter name changed from `agents` to
`competitors` for consistency.

Local variable names (agent_idx, this_agent) inside history.rs are
left unchanged — they represent abstract identifiers, not Competitor
instances.

Part of T2 of docs/superpowers/specs/2026-04-23-trueskill-engine-redesign-design.md.
2026-04-24 10:48:50 +02:00
2f5aa98eac refactor(api): rename Player to Rating
The struct holds prior/beta/drift — a rating configuration, not a
person. The person-with-temporal-state is the Competitor (renamed in
the next task). Resolves Player/Agent ambiguity.

Part of T2 of docs/superpowers/specs/2026-04-23-trueskill-engine-redesign-design.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 10:43:19 +02:00
52f5f76a34 refactor(lib): make key_table module private; revert bench var rename
Address code review feedback from Task 2:
- key_table module doesn't need pub visibility; the KeyTable re-export
  at lib.rs root already exposes the only public type. Matches the
  error/history private-module pattern.
- Revert an incidental bench variable rename (index_map → index) that
  wasn't part of the task scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 10:38:22 +02:00
c69fe4e67c refactor(api): rename IndexMap to KeyTable
The former name collided with the popular indexmap crate. KeyTable
lives in its own module. Public API unchanged beyond the rename.

Part of T2 of docs/superpowers/specs/2026-04-23-trueskill-engine-redesign-design.md.
2026-04-24 10:34:14 +02:00
cdfd75f846 bench: capture T1 final numbers and fix clippy warnings
Fixed:
- Removed unused .enumerate() in batch.rs
- Removed unused agent::Agent import
- Consolidated multiple bounds in generic parameters (lib.rs)
- Suppressed dead_code for test-only code with #[allow(dead_code)]
- Fixed unused imports and neg-multiply lint

Batch::iteration: 27.023 µs (T0 was 21.253 µs, expected minor regression from T1 infrastructure).
Gaussian::* unchanged (~236-280 ps).

Acceptance: T1 factor-graph refactor lands without clippy/fmt issues.
All 53 tests pass. Closes T1 tier.
2026-04-24 09:04:29 +02:00
cb07a874e8 refactor(game): rebuild Game::likelihoods on factor-graph machinery
Game::likelihoods now uses VarStore (for diff vars) and TruncFactor
(for EP truncation + evidence caching) instead of TeamMessage and
DiffMessage. The EP loop structure is preserved exactly; VarId-keyed
diff vars live in the arena's VarStore (capacity reused per batch).

ScratchArena loses teams/diffs/ties/margins; gains VarStore and
sort_buf (sort_perm allocation eliminated). message.rs deleted.

Public API of Game (new, posteriors, likelihoods, evidence) unchanged.
2026-04-24 08:51:18 +02:00
da69f02ff7 feat(schedule): add Schedule trait and EpsilonOrMax impl
EpsilonOrMax mirrors today's Game::likelihoods loop: sweep forward
then backward over iterating factors, capped at 10 iterations or
step <= 1e-6. Setup factors (TeamSum) run exactly once before the
loop begins.

ScheduleReport is the only public surface from this module.
2026-04-24 08:25:13 +02:00
54e46bef59 feat(factor): implement TruncFactor with cached evidence
EP truncation factor that operates on a diff variable. Stores its
outgoing message so the cavity computation produces the correct EP
message on each propagation. The first propagation caches the
evidence contribution (cdf-bounded probability) for log_evidence().

Promotes lib::cdf to pub(crate) so the factor can use it.
2026-04-24 08:22:06 +02:00
dac4427b65 feat(factor): introduce VarId and VarStore
Foundation types for the T1 factor graph machinery. VarStore is a
flat Vec<Gaussian> indexed by VarId; variables are allocated by
alloc() and the store can be cleared between games to reuse capacity.

Part of T1 of docs/superpowers/specs/2026-04-23-trueskill-engine-redesign-design.md.
2026-04-24 08:09:25 +02:00
b1e0fcb817 perf(game): eliminate per-event allocations via ScratchArena
Game::likelihoods previously allocated four Vecs (teams, diffs, ties,
margins) on every call. Batch now owns one ScratchArena reused across
all Game::new calls in the iteration loop; likelihoods() clears and
extends the arena buffers instead of allocating fresh.

For log_evidence (called infrequently), a local ScratchArena is created
per invocation so the method signature stays &self.

Also: add #[derive(Debug)] to TeamMessage and DiffMessage (required by
ScratchArena's own Debug derive).

Part of T0 engine redesign.
2026-04-24 07:24:29 +02:00
49d2b317da refactor(history): replace HashMap<Index, Agent<D>> with dense AgentStore<D>
AgentStore<D> is a Vec<Option<Agent<D>>>-backed store indexed directly
by Index.0, eliminating per-iteration hashing in the cross-history
forward/backward sweep. Implements Index<Index>/IndexMut<Index> for
ergonomic agent access.

AgentStore is public (so benches/batch.rs can use it). SkillStore
remains pub(crate) since Skill is pub(crate) in batch.rs.

HashMap<Index, _> is now only used for the posteriors() return value
(temporary; will be replaced in T2 with a proper typed return) and
for the add_events_with_prior(priors: HashMap<Index, Player<D>>) API
(also T2 target).

Part of T0 engine redesign.
2026-04-24 07:15:21 +02:00
8f60258dba refactor(batch): replace HashMap<Index, Skill> with dense SkillStore
SkillStore is a Vec<Skill>-backed dense store with a parallel present
mask, indexed directly by Index.0. Eliminates per-iteration hashing
in the within-slice convergence loop; O(1) array lookup replaces O(1)
amortised hash lookup with better cache behaviour.

Iteration order is now ascending-by-Index (was arbitrary for HashMap);
EP fixed point is order-independent so posteriors are unchanged.

Part of T0 engine redesign.
2026-04-24 07:08:20 +02:00
709ece335f feat: introduce InferenceError; mu_sigma panic already eliminated
mu_sigma was deleted as part of the Gaussian nat-param rewrite (its
only callers were the old Mul/Div impls). This commit adds the
InferenceError enum as a seed for the T2 API surface, with the
NegativePrecision variant that mu_sigma would have returned.

Part of T0 engine redesign.
2026-04-24 07:00:26 +02:00
a667deb7e1 refactor(gaussian): switch to natural-parameter storage (pi, tau)
Mul and Div become two f64 adds/subs with no sqrt in the hot path.
mu() and sigma() are computed on demand from stored pi/tau.

Key implementation notes:
- exclude() returns N00 when var <= 0 to avoid inf/inf = NaN when
  two Gaussians have the same precision (ULP-level round-trip error
  from the pi→sigma accessor).
- Mul<f64> by 0.0 returns N00 (point mass at 0), matching old behavior.
- from_ms(0, 0) == N00 {pi:inf, tau:0}; from_ms(0, inf) == N_INF {pi:0, tau:0}.

Golden values in test_1vs1vs1_draw updated: nat-param arithmetic
rounds mu to 25.0 (was 24.999999) and shifts sigma by ~3e-7.
Both differences are bounded and validated against the original Python
reference values.

Part of T0 engine redesign.
2026-04-24 06:59:43 +02:00
04d5478ee4 style: cargo fmt 2026-04-23 20:23:13 +02:00
a1f282a1c8 feat: added a Drift trait and a "default" ConstantDrift implementation 2026-03-16 12:06:04 +01:00
853f177fa8 Small changes for new 2024 edition 2025-02-21 14:09:58 +01:00
2366c45f6a Basic test for quality 2024-04-03 10:25:10 +02:00
3a22b20a17 Added todo to readme, and documentation for quality function 2024-04-03 09:53:07 +02:00
Anders Olsson
7e2576085f Make quality a free standing function instead 2023-10-26 11:11:54 +02:00
Anders Olsson
062c9d3765 Added quality function 2023-10-26 11:09:30 +02:00
e3906aebaa Small refactor 2022-12-27 22:37:12 +01:00
fdddf56156 Remove unused mut reference 2022-12-27 22:11:04 +01:00
2b83ee5ef9 Added benchmark for Batch 2022-12-19 07:42:08 +01:00
6546cb54b5 Added a get function to IndexMap 2022-12-12 13:25:07 +01:00
13e6454d3d Update crates and added methods to get a key or all keys in an IndexMap 2022-12-12 10:50:59 +01:00
22c61d47b1 Change time to use i64 instead of u64 2022-06-28 23:18:55 +02:00
2467d7e027 Update test to use assert_ulps_eq 2022-06-27 11:46:59 +02:00
cd1079a811 Use and Index struct instead of str and String for player id 2022-06-27 10:16:12 +02:00
73b9cabac8 Added builder for History, and start migrating test to use builder instead. 2022-06-22 21:13:24 +02:00
c9d9d59535 More things, better things, awesome 2022-06-18 23:39:42 +02:00
dc10504b80 Port from julia version instead 2022-06-18 22:27:38 +02:00
4227617513 Working on History struct. First test is passing. 2022-06-12 23:11:13 +02:00
ae1c765dbb Begin working on batch. 2022-06-12 21:02:57 +02:00
de58d01322 Initial commit. 2022-06-10 15:22:27 +02:00