Commit Graph
71 Commits
Author SHA1 Message Date
logaritmiskandClaude Opus 4.7 f3c074c24c feat(history): parallel learning_curves under rayon feature
Per-slice posterior collection runs in parallel via par_iter; merge
into the per-key HashMap is sequential in slice order so iteration
order and HashMap insertion order are identical to the sequential
impl. Preserves deterministic output across thread counts.

Default-feature (no rayon) build unchanged — uses the T2 sequential
impl.

Part of T3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 13:54:47 +02:00
logaritmiskandClaude Sonnet 4.6 4b99485fc8 perf(time-slice): restore sequential direct-write path under cfg(not(feature = "rayon"))
The compute/apply split introduced in 3680c54 was always active — the
sequential build paid EventOutput heap-alloc overhead even without
rayon, regressing Batch::iteration from 23.46 µs to 33.79 µs (+44%).

This commit makes the split feature-gated: under cfg(feature = "rayon")
the compute/apply pattern stays (needed for par_iter); under
cfg(not(feature = "rayon")) events update SkillStore inline via
Event::iteration_direct, matching the T2 performance profile.

EventOutput, Event::compute, and Event::apply_output are now
cfg(feature = "rayon")-only. TimeSlice::sweep_color_groups has two
cfg-gated implementations sharing the same signature.

Sequential restored to 23.29 µs; parallel 34.31 µs (small-workload
overhead expected — rayon threadpool amortizes at larger scales).

Part of T3.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 13:52:48 +02:00
logaritmisk 3680c54d3c feat(time-slice): parallel within-slice event iteration via rayon
Under #[cfg(feature = "rayon")], the per-iteration event sweep
processes events color-by-color: within a color, events touch
disjoint Index values by construction, so par_iter is safe.
Across colors, sequential ordering preserves async-EP semantics.

Event::compute() is a pure function returning an owned EventOutput
(new per-item likelihoods, evidence, and pre-computed new skill
likelihoods). The apply phase runs sequentially after the parallel
map, writing EventOutput values back to SkillStore and each event's
item likelihoods. This avoids shared mutable state in the hot loop.

Default build (no rayon) uses a sequential fallback that traverses
the same color-group order — behaviorally identical to the parallel
path. This keeps goldens bit-identical across feature configurations.

Scenario 3b applied: event updates read from and write to the shared
SkillStore, so the compute/apply split (Option A) was necessary.

Part of T3 of docs/superpowers/specs/2026-04-23-trueskill-engine-redesign-design.md.
2026-04-24 13:48:41 +02:00
logaritmiskandClaude Sonnet 4.6 9836b7b709 feat(time-slice): compute and maintain color groups; reorder events
TimeSlice gains a color_groups field of type ColorGroups, recomputed
whenever events change. After recompute, self.events is physically
reordered so color-0 events are first, then color-1, etc. Each color
is therefore a contiguous range of indices in self.events —
the invariant that Task 6's parallel par_iter_mut exploits.

Greedy coloring via crate::color_group::color_greedy; agent indices
come from Event::iter_agents. ColorGroups gains a color_range helper
that returns the contiguous Range<usize> for a given color.

Numerical behavior unchanged: async-EP is order-independent at
convergence, so event reordering does not affect goldens.

Part of T3.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 13:42:05 +02:00
logaritmisk a40c0d6301 feat(color-group): add greedy within-slice event partitioning
ColorGroups holds a partition of event indices into color groups such
that events of the same color touch no shared Index. Computed greedily
in ingestion order: each event goes into the first color whose existing
members are disjoint from the event's indices.

Used in T3 for safe within-slice parallelism — events in the same
color can run concurrently without touching each other's skills.

Part of T3 of docs/superpowers/specs/2026-04-23-trueskill-engine-redesign-design.md.
2026-04-24 13:38:21 +02:00
logaritmisk 4f302ed28e feat(api): add Send + Sync bounds to public traits
Required for T3 rayon-based parallelism. Affected traits:
- Time (+ Send + Sync + 'static)
- Drift<T> (+ Send + Sync)
- Observer<T> (+ Send + Sync)
- Factor (+ Send + Sync)
- Schedule (+ Send + Sync)

All built-in impls (i64, Untimed, ConstantDrift, NullObserver,
EpsilonOrMax, TeamSumFactor, RankDiffFactor, TruncFactor,
BuiltinFactor) naturally satisfy these bounds via auto-derive.

Minor breaking change: downstream custom impls that aren't already
thread-safe will need to add the bounds.

Part of T3 of docs/superpowers/specs/2026-04-23-trueskill-engine-redesign-design.md.
2026-04-24 13:36:39 +02:00
logaritmisk 9fe40042da feat(cargo): add rayon as optional dependency
Opt-in feature flag — users who want parallel paths build with
--features rayon. Default build remains single-threaded.

Spec Section 6 calls for default-on; we defer that flip until the
feature is stable under field use.

Part of T3 of docs/superpowers/specs/2026-04-23-trueskill-engine-redesign-design.md.
2026-04-24 13:35:15 +02:00
logaritmiskandClaude Opus 4.7 f0793a8470 docs: add T3 concurrency implementation plan
11-task plan for rayon-backed within-slice parallelism per
Section 6 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 13:34:00 +02:00
logaritmisk d2aab82c1e T0 + T1 + T2: engine redesign through new API surface (#1)
Implements tiers T0, T1, T2 of `docs/superpowers/specs/2026-04-23-trueskill-engine-redesign-design.md`. All three tiers have landed together on this branch because they build on one another; this PR rolls them up for a single review pass.

Per-tier plans:
- T0: `docs/superpowers/plans/2026-04-23-t0-numerical-parity.md`
- T1: `docs/superpowers/plans/2026-04-24-t1-factor-graph.md`
- T2: `docs/superpowers/plans/2026-04-24-t2-new-api-surface.md`

## Summary

### T0 — Numerical parity (internal)

- `Gaussian` switched to natural-parameter storage `(pi, tau)`; mul/div now ~7× faster (218 ps vs 1.57 ns).
- `HashMap<Index, _>` → dense `Vec<_>` keyed by `Index.0` (via `AgentStore<D>`, `SkillStore`).
- `ScratchArena` eliminates per-event allocations in `Game::likelihoods`.
- `InferenceError` seed type added (1 variant).
- 38 → 53 tests passing through T1.
- Benchmark: `Batch::iteration` 29.84 → 21.25 µs.

### T1 — Factor graph machinery (internal)

- `Factor` trait + `BuiltinFactor` enum (TeamSum / RankDiff / Trunc) driving within-game inference.
- `VarStore` flat storage for variable marginals.
- `Schedule` trait + `EpsilonOrMax` impl replacing the hand-rolled EP loop.
- `Game::likelihoods` rebuilt on the factor-graph machinery; iteration counts and goldens preserved to within 1e-6.
- 53 tests passing.
- Benchmark: `Batch::iteration` 23.01 µs (slight regression absorbed in T2).

### T2 — New API surface (breaking)

**Renames:**
- `IndexMap → KeyTable`, `Player → Rating`, `Agent → Competitor`, `Batch → TimeSlice`

**New types:**
- `Time` trait with `Untimed` ZST and `i64` impls; `Drift<T>`, `Rating<T, D>`, `Competitor<T, D>`, `TimeSlice<T>`, `History<T, D, O, K>` all generic.
- `Event<T, K>`, `Team<K>`, `Member<K>`, `Outcome` (`Ranked` variant; `#[non_exhaustive]`).
- `Observer<T>` trait + `NullObserver`.
- `ConvergenceOptions`, `ConvergenceReport`.
- `GameOptions`, `OwnedGame<T, D>`.

**Three-tier ingestion:**
- `history.record_winner(&K, &K, T)` / `record_draw(&K, &K, T)` — 1v1 convenience.
- `history.add_events(iter)` — typed bulk.
- `history.event(T).team([...]).weights([...]).ranking([...]).commit()` — fluent.

**Query API:** `current_skill`, `learning_curve`, `learning_curves` (keyed on `K`), `log_evidence`, `log_evidence_for`, `predict_quality`, `predict_outcome`.

**Game constructors:** `ranked`, `one_v_one`, `free_for_all`, `custom` — all returning `Result<_, InferenceError>`.

**`factors` module:** `Factor`, `Schedule`, `VarStore`, `VarId`, `BuiltinFactor`, `EpsilonOrMax`, `ScheduleReport`, `TeamSumFactor`, `RankDiffFactor`, `TruncFactor` now public.

**Errors:** `InferenceError` gains `MismatchedShape`, `InvalidProbability`, `ConvergenceFailed`; boundary panics converted to `Result`.

**Removed (breaking):** `History::convergence(iters, eps, verbose)`, `HistoryBuilder::gamma(f64)`, `HistoryBuilder::time(bool)`, `History.time: bool`, `learning_curves_by_index`, nested-Vec public `add_events`.

## Behavior change (documented in CHANGELOG)

`Time = Untimed` has `elapsed_to → 0`, so no drift accumulates between slices. The old `time=false` mode implicitly forced `elapsed=1` on reappearance via an `i64::MAX` sentinel — that quirk is not reproducible under a typed time axis. Tests that depended on it now use `History::<i64, _>` with explicit `1..=n` timestamps. One test (`test_env_ttt`) had 3 Gaussian goldens updated to reflect the corrected semantics; documented in commit `33a7d90`.

## Final numbers

| Metric | Before T0 | After T2 | Delta |
|---|---|---|---|
| `Batch::iteration` | 29.84 µs | 21.36 µs | **-28%** |
| `Gaussian::mul` | 1.57 ns | 219 ps | **-86%** |
| `Gaussian::div` | 1.57 ns | 219 ps | **-86%** |
| Tests passing | 38 | 90 | +52 |

All other Gaussian ops unchanged (~219 ps add/sub, ~264 ps pi/tau reads).

## Test plan

- [x] `cargo test --features approx` — 90/90 pass (68 lib + 10 api_shape + 6 game + 4 record_winner + 2 equivalence)
- [x] `cargo clippy --all-targets --features approx -- -D warnings` — clean
- [x] `cargo +nightly fmt --check` — clean
- [x] `cargo bench --bench batch` — 21.36 µs
- [x] `cargo bench --bench gaussian` — unchanged from T1
- [x] `cargo run --example atp --features approx` — rewritten in new API, runs clean
- [x] Historical Game-level goldens preserved in `tests/equivalence.rs`
- [x] Public API matches spec Section 4 (verified by integration tests in `tests/api_shape.rs`)

## Commit history

~45 commits total across T0 + T1 + T2. Each task is self-contained and individually tested; the branch is bisectable. See `git log main..t2-new-api-surface` for the full list.

## Deferred to later tiers

- `Outcome::Scored` + `MarginFactor` — T4
- `Damped` / `Residual` schedules — T4
- `Send + Sync` bounds + Rayon parallelism — T3
- N-team `predict_outcome` — T4
- `Game::custom` full ergonomics — T4

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Reviewed-on: #1
Co-authored-by: Anders Olsson <anders.e.olsson@gmail.com>
Co-committed-by: Anders Olsson <anders.e.olsson@gmail.com>
2026-04-24 11:20:04 +00:00
logaritmisk a14df02089 chore: do not publish 2026-04-23 20:26:52 +02:00
logaritmisk 0d266b4428 chore: make cargo release add CHANGELOG.md before commit 2026-04-23 20:26:16 +02:00
logaritmisk a4b4e5e8fa chore: clean up 2026-04-23 20:24:10 +02:00
logaritmisk 04d5478ee4 style: cargo fmt 2026-04-23 20:23:13 +02:00
logaritmisk 480467ac32 chore: added cliff.toml, release.toml and rustfmt.toml 2026-04-23 20:22:27 +02:00
logaritmisk dc47964310 added benchmark 2026-03-23 14:55:18 +01:00
logaritmisk 61a5507f5c remove notepad 2026-03-23 14:21:23 +01:00
logaritmisk a1f282a1c8 feat: added a Drift trait and a "default" ConstantDrift implementation 2026-03-16 12:06:04 +01:00
logaritmisk 853f177fa8 Small changes for new 2024 edition 2025-02-21 14:09:58 +01:00
logaritmisk fc0efcdc52 Update edition 2025-02-21 14:06:28 +01:00
logaritmisk 3bbddb168f Ignore temp folder 2024-04-03 14:43:54 +02:00
logaritmisk 2366c45f6a Basic test for quality 2024-04-03 10:25:10 +02:00
logaritmisk 3a22b20a17 Added todo to readme, and documentation for quality function 2024-04-03 09:53:07 +02:00
logaritmisk 02ae2f0977 Change assert to debug_assert 2024-04-03 09:44:41 +02:00
logaritmisk a491f8de8d Fix broken link in README 2023-01-09 14:37:07 +01:00
logaritmisk 0b4b07d60e Added more links to readme 2023-01-09 14:35:34 +01:00
logaritmisk 05f178641c Rename d to diff, and t to team 2022-12-29 20:38:36 +01:00
logaritmisk e3906aebaa Small refactor 2022-12-27 22:37:12 +01:00
logaritmisk 8e25826f91 More rustifying 2022-12-27 22:30:20 +01:00
logaritmisk 9b6cb9e7eb Make it more rusty 2022-12-27 22:24:01 +01:00
logaritmisk fdddf56156 Remove unused mut reference 2022-12-27 22:11:04 +01:00
logaritmisk b93194f762 Added default implementation for TeamMessage 2022-12-20 11:08:27 +01:00
logaritmisk 2b83ee5ef9 Added benchmark for Batch 2022-12-19 07:42:08 +01:00
logaritmisk 2bdd3d9b89 Remove warnings and refactor some code 2022-12-16 19:46:01 +01:00
logaritmisk 912a282cd8 More refactoring 2022-12-16 15:57:56 +01:00
logaritmisk 51467f7b69 Fix clippy warning 2022-12-16 15:51:58 +01:00
logaritmisk 6dd84f7fd2 Refactor so we can see if there is any way to improve the performance 2022-12-16 15:38:29 +01:00
logaritmisk 5eb8e62d6e Agents doens't have to be behind a mutable reference in within_priors 2022-12-15 20:13:25 +01:00
logaritmisk 8bea1b5399 Agents doens't have to be behind a mutable reference in within_prior 2022-12-15 20:12:55 +01:00
logaritmisk 6546cb54b5 Added a get function to IndexMap 2022-12-12 13:25:07 +01:00
logaritmisk 13e6454d3d Update crates and added methods to get a key or all keys in an IndexMap 2022-12-12 10:50:59 +01:00
logaritmisk 18d55a8ccf Clean up example 2022-07-04 23:13:57 +02:00
logaritmisk 32df04fb6d Small change 2022-06-28 23:29:44 +02:00
logaritmisk 22c61d47b1 Change time to use i64 instead of u64 2022-06-28 23:18:55 +02:00
logaritmisk 6125a81696 Fixed test 2022-06-27 11:49:47 +02:00
logaritmisk 2467d7e027 Update test to use assert_ulps_eq 2022-06-27 11:46:59 +02:00
logaritmisk 0633c766a5 Update example so now it works, and thats, well, good 2022-06-27 10:22:26 +02:00
logaritmisk cd1079a811 Use and Index struct instead of str and String for player id 2022-06-27 10:16:12 +02:00
logaritmisk d52895f804 Remove unused code 2022-06-23 07:48:44 +02:00
logaritmisk ba2f8cffdc Update test to use builder 2022-06-23 07:44:34 +02:00
logaritmisk 73b9cabac8 Added builder for History, and start migrating test to use builder instead. 2022-06-22 21:13:24 +02:00
logaritmisk a573f5cf0a More tests 2022-06-21 10:48:04 +02:00
logaritmisk 1647ba5c77 More tests 2022-06-21 10:42:14 +02:00
logaritmisk be4834ca85 Fix tests 2022-06-20 23:58:05 +02:00
logaritmisk 0a35645091 More things, more tests 2022-06-20 23:56:35 +02:00
logaritmisk 0efb0312da More tests, more code 2022-06-19 23:52:52 +02:00
logaritmisk c9d9d59535 More things, better things, awesome 2022-06-18 23:39:42 +02:00
logaritmisk dc10504b80 Port from julia version instead 2022-06-18 22:27:38 +02:00
logaritmisk 36c3366990 Refactor some stuff 2022-06-15 13:26:03 +02:00
logaritmisk 0a3327f076 Refactor some code 2022-06-15 11:27:24 +02:00
logaritmisk 811a4ca418 Inline a lot of functions 2022-06-14 23:00:21 +02:00
logaritmisk 9b025fb53a Use PlayerIndex instead of String 2022-06-14 22:51:11 +02:00
logaritmisk 3fbac02de3 It works, or so it seems 2022-06-13 23:18:19 +02:00
logaritmisk 1b6e07225b Handle case where there is no time 2022-06-13 21:55:43 +02:00
logaritmisk 82e7b22443 Use flatten instead of flat_map 2022-06-13 11:37:56 +02:00
logaritmisk de4b324651 Remove Display impl, better to use Debug 2022-06-13 11:35:08 +02:00
logaritmisk 87a64acb83 Added more functions to History 2022-06-13 11:27:49 +02:00
logaritmisk 4a13e4dcd2 More test passing for History 2022-06-13 11:04:05 +02:00
logaritmisk 4227617513 Working on History struct. First test is passing. 2022-06-12 23:11:13 +02:00
logaritmisk 5a7053fb5d Passing tests for Batch 2022-06-12 21:43:35 +02:00
logaritmisk ae1c765dbb Begin working on batch. 2022-06-12 21:02:57 +02:00
logaritmisk de58d01322 Initial commit. 2022-06-10 15:22:27 +02:00