diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..fea410f --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +* text=auto eol=lf + +*.bat text eol=crlf +*.cmd text eol=crlf +*.ps1 text eol=crlf diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..12de968 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,32 @@ +## What this delivers + +Launches Universal Sports Engine 0.3.0: a deterministic, high-throughput, +multi-league simulation platform with exact replay, bounded recursive calibration, +online expert weighting, drift-aware shadow monitoring, and content-addressed +champion governance. + +## Why it matters + +- Unifies MLB, NBA, WNBA, NCAA MBB, NFL, NCAA Football, and NHL behind a neutral kernel. +- Preserves sport-specific rules and mechanics in isolated league packs. +- Separates fast analytical distributions from interpretable event simulation. +- Makes every autonomous promotion, rejection, weight update, and rollback auditable. +- Fails closed when integrity, calibration, drift, or reproducibility gates fail. + +## Verified evidence + +- [ ] Ruff passes. +- [ ] Strict MyPy passes. +- [ ] All 67 integration and invariant tests pass. +- [ ] Release evidence gates pass across all seven leagues. +- [ ] The 49-control negative and mechanism suite reports zero failures. +- [ ] The 14,000-game stress audit reports zero failures. +- [ ] The autonomy audit verifies champion, ledger, shadow-update, and rollback integrity. +- [ ] A clean wheel rebuild reproduces the expected package contents. + +## Evidence boundary + +This release verifies mechanics, determinism, replay integrity, autonomy governance, +and measured local throughput. It does not claim real-world predictive supremacy. +That requires licensed point-in-time game, roster, injury, event, and tracking data +plus held-out and prospective validation. diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml new file mode 100644 index 0000000..55961fc --- /dev/null +++ b/.github/workflows/quality.yml @@ -0,0 +1,54 @@ +name: Quality + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +concurrency: + group: quality-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + verify: + name: Python 3.12 verification + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Check out source + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: pip + + - name: Install project + run: python -m pip install --upgrade pip && python -m pip install -e ".[dev]" + + - name: Lint + run: python -m ruff check . + + - name: Type-check + run: python -m mypy src scripts + + - name: Test + run: python -m pytest -q + + - name: Build wheel + run: python -m pip wheel . --no-deps --wheel-dir dist + + - name: Verify release gates + run: >- + python scripts/build_release_evidence.py + --games 500 + --benchmark-games 100 + --seed 20260718 + --workers 2 + --output "${{ runner.temp }}/release-evidence" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..077d4a5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +.venv/ +.repro-venv/ +.repro-v3-venv/ +__pycache__/ +.pytest_cache/ +.ruff_cache/ +.mypy_cache/ +*.egg-info/ +build/ +dist/ +reports/archive/ +reports/COPY_PROOF.json +reports/HANDOFF_PROOF.json +reports/COMPREHENSIVE_AUDIT_V2.md +reports/PERFORMANCE_AUDIT_V2.json +reports/PERFORMANCE_AUDIT_V3.json +reports/RECOVERY_AND_COMPLETION_REPORT.md +reports/STRESS_AUDIT_V2.json diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d813013 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,10 @@ +# Universal Sports Engine Rules + +- Keep the simulation truth independent from rendering. +- Preserve deterministic replay: identical versions, inputs, and seed must produce identical events. +- Keep sport semantics in league packs; the core API must remain domain-neutral. +- Fail explicitly on invalid state, unknown league IDs, corrupted event chains, and invalid rules. +- Do not claim real-world calibration from synthetic fixtures or reference anchors. +- Do not add wagering, sportsbook, brokerage, or live-capital behavior. +- Keep changes typed, functional, minimal, and covered by integration or replay tests. +- Never weaken a quality gate because a candidate or league pack fails it. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..ac83dce --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,64 @@ +# Architecture + +## Decision + +The kernel uses immutable event drafts and a content-addressed event log as the +source of truth, pure reducers for replay, fixed phase ordering, and label-addressed random draws. This +combines the strongest parts of the two architectures recovered from the stalled +Kimi build: + +1. Event-sourced state plus hierarchical domain state. +2. Explicit `PERCEIVE -> DECIDE -> ARBITRATE -> RESOLVE -> APPLY -> PUBLISH` phases. +3. Independent random namespaces and common-random-number counterfactuals. +4. Game-granularity parallelism; a single game timeline remains deterministic. +5. A result-envelope hash that commits to league, final score, seed, event chain, + and evidence metadata. + +## Layers + +```text +CLI / SDK / domain-neutral bridge + | +Champion governance, recursive calibration, adaptive weights, drift quarantine + | +F0/F1 routing, batch, historical evaluation, counterfactuals + | +League packs and sport-family resolvers + | +Domain-neutral event log, state, rules, RNG, replay +``` + +League packs own scoring, timing, possession/inning/drive/shift semantics, and +reference anchors. The kernel contains no player, ball, puck, inning, possession, +or scoreboard type. + +## Fidelity + +- F0: analytical/batch score distribution (implemented; no event path). +- F1: event-level paths (implemented for every league). +- F2: spatial-agent contracts and escalation interface (API complete; no tracking calibration). +- F3: rendering/physics adapter boundary (not implemented without tracking/physics evidence). + +The fidelity router selects the lowest evidence-supported fidelity and rejects +unsupported F2/F3 requests. It never uses visual +plausibility as a substitute for held-out calibration. + +## Integrity and performance + +League packs accumulate immutable drafts locally and content-address them once in +linear time. External replay import recomputes the event chain and result envelope. +The RNG maps semantic labels directly to BLAKE2b uniform draws, so unrelated model +changes do not perturb other random namespaces. + +## Recursive model governance + +The adaptive layer is outside the neutral kernel. Profiles, expert mixtures, +online calibration state, champion bundles, and decision ledgers are immutable +and content-addressed. Search may mutate only bounded analytical parameters. +Promotion is decided on chronological calibration forecasts made before each +outcome is revealed. Test data, rule packs, evaluators, and quality gates are not +mutable search inputs. + +Online shadow adaptation changes expert weights and interval scale, while two +loss EWMAs monitor drift. Drifted checkpoints remain auditable but are rejected +by adaptive simulation until a healthy successor or rollback is selected. diff --git a/AUTONOMOUS_IMPROVEMENT.md b/AUTONOMOUS_IMPROVEMENT.md new file mode 100644 index 0000000..5254d8b --- /dev/null +++ b/AUTONOMOUS_IMPROVEMENT.md @@ -0,0 +1,80 @@ +# Autonomous recursive improvement + +Version 0.3 adds a bounded model-improvement system around the deterministic +simulation kernel. It changes analytical model parameters and ensemble weights; +it cannot edit source code, official rule packs, historical outcomes, split +boundaries, evaluators, promotion gates, or live-action interfaces. + +## Evidence loop + +```text +point-in-time train block + -> deterministic population search + -> diverse bounded expert pool + -> chronological calibration predictions + -> outcome revealed once + -> expert weights and interval scale updated + -> challenger compared with incumbent + -> promote or retain + -> content-addressed checkpoint and chained ledger + -> shadow outcomes update weights and drift state + -> healthy, quarantine, recalibrate, or roll back +``` + +Training outcomes select bounded score-model parameters. Calibration outcomes +are consumed prequentially: the ensemble predicts first, then receives the +outcome exactly once. Test outcomes are excluded from tuning and promotion. +Shadow outcomes may update an already selected champion but cannot rewrite its +lineage or erase prior decisions. + +## Adjustable state + +Each expert contains league-specific home and away score locations, total and +margin dispersion, and strength-response exponents. Recursive search uses +deterministic log-space mutations, retains a diverse elite set, and reduces the +mutation radius by generation. + +The selected ensemble applies multiplicative expert weighting using joint score +likelihood and home-win Brier loss. A separate online calibration state adjusts +the 90% interval radius. Short- and long-horizon loss estimates produce a drift +score. A drift alert blocks adaptive simulation until a new governed checkpoint +or explicit rollback is selected. + +## Promotion gates + +A challenger must satisfy every preregistered requirement in +`AUTONOMY_CONFIG.json`: + +- minimum composite-loss improvement; +- no excessive Brier deterioration; +- no excessive interval-coverage deterioration; +- finite scores and normalized non-negative weights; +- healthy calibration state; +- sufficient non-overlapping train and calibration evidence; +- valid profile, ensemble, adaptive-state, bundle, and ledger hashes. + +Synthetic fixtures can validate learning mechanics but can never certify +real-world accuracy. Real promotion claims require licensed point-in-time data +and an untouched test audit after the champion is frozen. + +## Commands + +```powershell +.\.venv\Scripts\sports-engine.exe auto-improve ` + --csv data\point-in-time\historical.csv ` + --config AUTONOMY_CONFIG.json ` + --output reports\improvement-run-001 + +.\.venv\Scripts\sports-engine.exe simulate-adaptive ` + --bundle reports\improvement-run-001\champion_bundle.json ` + --league nba --seed 42 --home-strength 1.0 --away-strength 1.0 + +.\.venv\Scripts\sports-engine.exe adapt-online ` + --csv data\point-in-time\shadow.csv ` + --config AUTONOMY_CONFIG.json ` + --bundle reports\improvement-run-001\champion_bundle.json ` + --league nba --output reports\nba-shadow-checkpoint-001.json +``` + +Output paths are fail-closed: existing improvement artifacts and adaptive +checkpoints are not overwritten. diff --git a/AUTONOMY_CONFIG.json b/AUTONOMY_CONFIG.json new file mode 100644 index 0000000..65b84e3 --- /dev/null +++ b/AUTONOMY_CONFIG.json @@ -0,0 +1,22 @@ +{ + "brier_weight": 2.0, + "coverage_weight": 2.0, + "drift_threshold": 0.12, + "elite_count": 6, + "experts_per_ensemble": 6, + "generations": 8, + "interval_learning_rate": 0.04, + "long_decay": 0.03, + "maximum_brier_deterioration": 0.01, + "maximum_coverage_error_deterioration": 0.03, + "minimum_calibration_games": 50, + "minimum_drift_observations": 40, + "minimum_relative_improvement": 0.01, + "minimum_train_games": 100, + "mutation_scale": 0.12, + "online_learning_rate": 0.25, + "population_size": 32, + "seed": 20260718, + "short_decay": 0.20, + "target_coverage": 0.90 +} diff --git a/DATA_RIGHTS_AND_PROVENANCE.md b/DATA_RIGHTS_AND_PROVENANCE.md new file mode 100644 index 0000000..7e45ca7 --- /dev/null +++ b/DATA_RIGHTS_AND_PROVENANCE.md @@ -0,0 +1,22 @@ +# Data Rights and Provenance + +## Local build inputs + +No licensed event feed, tracking feed, private injury feed, sportsbook feed, or +proprietary roster database is present in this repository. + +The shipped engine uses: + +- deterministic synthetic events generated by the versioned league packs; +- transparent league-level reference anchors for broad output sanity checks; +- official rule-source links as provenance pointers for the minimal rule manifests. + +The reference anchors are approximate research fixtures. They are not an ingested, +point-in-time dataset and are labeled `provisional_reference_anchor` everywhere. + +## Prohibited promotion + +These inputs cannot support a claim of held-out player, team, game, season, spatial, +or counterfactual accuracy. Such claims require separately licensed data manifests, +earliest-available timestamps, revisions, train/calibration/test boundaries, and +independent reproduction. diff --git a/DOMAIN_NEUTRAL_ADAPTATION_READINESS.md b/DOMAIN_NEUTRAL_ADAPTATION_READINESS.md new file mode 100644 index 0000000..83c1dfd --- /dev/null +++ b/DOMAIN_NEUTRAL_ADAPTATION_READINESS.md @@ -0,0 +1,12 @@ +# Domain-neutral adaptation readiness + +No external project was inspected, imported, or modified during this build. + +The reusable boundary consists only of immutable state, action, observation, +scenario, counterfactual, uncertainty, fidelity, and evidence contracts. A future +consumer must implement its own domain pack and validation program. Sports rule +packs, reference anchors, league identities, and generated scores must not cross +that boundary. + +Compatibility is demonstrated by the included non-sports inventory-control fixture. +There is no live-action, capital, brokerage, sportsbook, or wagering adapter. diff --git a/DOMAIN_NEUTRAL_KERNEL_SPEC.md b/DOMAIN_NEUTRAL_KERNEL_SPEC.md new file mode 100644 index 0000000..01d604b --- /dev/null +++ b/DOMAIN_NEUTRAL_KERNEL_SPEC.md @@ -0,0 +1,22 @@ +# Domain-neutral kernel specification + +The kernel owns only immutable events, world-state payloads, phase ordering, +versioned rules, deterministic random draws, replay verification, fidelity routing, +and evidence metadata. It contains no sports entities or timing assumptions. + +## Invariants + +- Sequence numbers are contiguous and zero-based. +- Every event hash commits to its body and prior event hash. +- Identical root seed and semantic labels produce the same draw. +- An unavailable fidelity raises instead of silently returning a lower-fidelity answer. +- A league pack must verify its event stream before publishing a result. +- Portable replay import verifies the complete chain before returning a result. + +## Extension points + +- League pack: complete-game resolver and rule manifest. +- Learned transition: replaces a league transition function without changing evidence contracts. +- Spatial adapter: produces `SpatialState` values only after league-specific validation. +- Physics adapter: remains independent of rendering and uses the same event sink. +- Policy adapter: consumes observations and permitted actions, never omniscient state by default. diff --git a/KNOWN_LIMITATIONS.md b/KNOWN_LIMITATIONS.md new file mode 100644 index 0000000..9a25c40 --- /dev/null +++ b/KNOWN_LIMITATIONS.md @@ -0,0 +1,22 @@ +# Known Limitations + +- The stalled remote Kimi repository was not exported; this is an independent local reconstruction. +- F1 event paths are calibrated only to broad reference anchors, not licensed held-out data. +- Player identities, rosters, coaching policies, officials, injuries, fatigue, venues, and weather are contract surfaces rather than calibrated models. +- F2 spatial trajectories and F3 physics/rendering are intentionally not fabricated without tracking evidence. +- Rule manifests cover complete-game flow but are not a complete executable transcription of every official rulebook edge case. +- Generic F1 games resolve a winner; competition-specific regular-season tie and + postseason context is not yet a first-class scenario input. +- The analytical F0 model matches provisional aggregate moments but does not emit + sport-valid event sequences or prove game-level predictive skill. +- Historical evaluation is implemented, but no licensed point-in-time test manifest + was supplied, so CRPS, Brier, and coverage results on real games remain unavailable. +- Recursive tuning currently adjusts F0 analytical score distributions and ensemble + weights. It does not fabricate player, tracking, tactical, F2, or F3 calibration. +- The bundled autonomy audit uses labeled synthetic fixtures solely to prove search, + weighting, gating, checkpoint, and replay mechanics. Its promotions are not + real-world champions. +- Windows multi-process calls require an importable script or CLI entrypoint; an + interactive/stdin caller receives an explicit error and should use F0 or one worker. +- Counterfactual pairing reduces simulation noise but does not by itself establish causality. +- No live feed, wagering, sportsbook, trading, or capital interface exists. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..db493a3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 ObtuseAI + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/PRIOR_ART_REGISTRY.md b/PRIOR_ART_REGISTRY.md new file mode 100644 index 0000000..3ec1c1c --- /dev/null +++ b/PRIOR_ART_REGISTRY.md @@ -0,0 +1,35 @@ +# Prior-art registry + +The architecture was compared against public research interfaces rather than +claiming novelty from naming alone. + +| System | Relevant capability | Retained lesson | This engine's actual distinction | +|---|---|---|---| +| Google Research Football | Physics-based multi-agent sports environment | Keep simulation separate from policy learning | Seven league packs share one event/evidence kernel; F2/F3 are not claimed without calibration | +| OpenSpiel | Extensive-form, simultaneous-action, imperfect-information games | Preserve legal actions, histories, observations, and replaceable policies | Sports event logs are content-addressed and coupled to versioned rule manifests | +| PettingZoo Parallel API | Standard simultaneous multi-agent stepping and conformance tests | Keep an adapter boundary for parallel agent research | The core remains dependency-free and uses explicit phase ordering for deterministic game truth | +| EnvPool and Pgx research | Vectorized, accelerator-oriented environment execution | Treat throughput as measured adapter work | Current release parallelizes complete games with deterministic seed namespaces; no GPU speed claim is made | +| Population Based Training | Joint model and hyperparameter population evolution | Preserve champion/challenger lineages and bounded exploit/explore cycles | Search is deterministic, parameter-bounded, data-split aware, and cannot mutate evaluator code | +| Adaptive Conformal Inference | Coverage adaptation under distribution shift | Update interval calibration prospectively and monitor distribution change | A content-addressed state couples interval scaling with expert weights and fail-closed drift quarantine | +| Prediction with expert advice | Sequential distribution aggregation and performance-based weights | Score before outcome reveal, then update expert weights | Joint score likelihood and Brier loss adjust an auditable league expert pool without erasing prior weights | +| Optuna | Search/pruning framework design | Declare the search space and prune weak configurations | This dependency-free engine uses a small deterministic search surface tied directly to immutable simulator profiles | + +Primary references: + +- Google Research Football paper: https://arxiv.org/abs/1907.11180 +- OpenSpiel paper: https://arxiv.org/abs/1908.09453 +- PettingZoo Parallel API: https://pettingzoo.farama.org/api/parallel/ +- EnvPool paper: https://arxiv.org/abs/2206.10558 +- Pgx paper: https://arxiv.org/abs/2303.17503 +- Population Based Training: https://arxiv.org/abs/1711.09846 +- Adaptive Conformal Inference: https://arxiv.org/abs/2106.00170 +- Online aggregation of probabilistic forecasts: https://proceedings.mlr.press/v128/korotin20a/korotin20a.pdf +- Optuna: https://arxiv.org/abs/1907.10902 + +The defensible contribution through version 0.3.0 is the combination of portable +hash-chained sports event paths, label-addressed randomness, seven isolated league +packs, common-random-number counterfactuals, fail-closed evidence envelopes, and +content-addressed recursive model governance. +It is an engineering synthesis, not a claim to have invented event simulation, +multi-agent environments, digital twins, population search, conformal adaptation, +or expert weighting. diff --git a/QUALITY_GATES.yaml b/QUALITY_GATES.yaml new file mode 100644 index 0000000..366adcd --- /dev/null +++ b/QUALITY_GATES.yaml @@ -0,0 +1,61 @@ +version: 3 +live_wagering: prohibited +engineering: + strict_mypy_required: true + ruff_required: true + integration_suite_required: true +kernel: + exact_replay_required: true + event_hash_chain_required: true + result_envelope_hash_required: true + monotonic_clock_required: true + stable_phase_order_required: true + invalid_state_fails_closed: true + deterministic_label_addressed_rng_required: true +leagues: + required: [mlb, nba, wnba, ncaambb, nfl, ncaaf, nhl] + complete_game_flow_required: true + event_log_required: true + rule_source_version_required: true + maximum_rule_coverage_claim: complete_game_flow_subset +accuracy: + synthetic_data_may_certify_real_world_calibration: false + reference_anchor_status: provisional_reference_anchor + mean_score_error_within_domain_tolerance: true + total_score_sd_error_within_domain_tolerance: true + home_advantage_material_reversal_z_minimum: -1.96 + paired_strength_monotonicity_required: true + point_in_time_feature_cutoff_required: true + chronological_split_separation_required: true + proper_distribution_scores_required_for_historical_evaluation: true + held_out_point_in_time_data_required_for_certification: true +autonomy: + configuration_file_required: AUTONOMY_CONFIG.json + bounded_parameter_search_required: true + deterministic_candidate_generation_required: true + train_calibration_test_shadow_separation_required: true + prequential_calibration_required: true + test_data_may_select_or_tune_candidate: false + evaluator_or_rule_mutation_allowed: false + complete_failed_trial_ledger_required: true + profile_hash_required: true + ensemble_hash_required: true + adaptive_state_hash_required: true + champion_bundle_hash_required: true + rollback_hash_required: true + drift_quarantine_required: true + existing_checkpoint_overwrite_allowed: false + synthetic_autonomy_audit_may_certify_real_world_accuracy: false +performance: + reference_report: reports/PERFORMANCE_AUDIT_V3_FINAL.json + minimum_f0_games_per_second: 75000 + minimum_single_worker_f1_games_per_second: 250 + minimum_best_four_worker_speedup_over_single_worker: 1.20 +stress: + minimum_randomized_games: 14000 + result_integrity_failures_allowed: 0 + negative_controls_required: true +domain_neutral_bridge: + sport_specific_core_types_allowed: false + evidence_metadata_required: true + live_action_adapter_allowed: false diff --git a/README.md b/README.md new file mode 100644 index 0000000..a48d8f7 --- /dev/null +++ b/README.md @@ -0,0 +1,176 @@ +
+ +# Universal Sports Engine + +### Deterministic simulation. Recursive calibration. Auditable evolution. + +A high-throughput, multi-league sports research engine with exact replay, +self-tuning analytical ensembles, drift-aware online calibration, and +content-addressed champion governance. + +[![Version](https://img.shields.io/badge/version-0.3.0-7c3aed?style=for-the-badge)](./STATUS.md) +[![Python](https://img.shields.io/badge/python-3.12%2B-3776ab?style=for-the-badge&logo=python&logoColor=white)](./pyproject.toml) +[![Leagues](https://img.shields.io/badge/leagues-7-0f766e?style=for-the-badge)](#seven-leagues-one-kernel) +[![Tests](https://img.shields.io/badge/tests-67%20passing-16a34a?style=for-the-badge)](./reports/RELEASE_EVIDENCE.md) +[![License](https://img.shields.io/badge/license-MIT-111827?style=for-the-badge)](./LICENSE) + +**Research release:** mechanics, integrity, autonomy, and performance are verified. +Real-world predictive supremacy is not claimed without licensed point-in-time data. + +
+ +--- + +## Why it is different + +Universal Sports Engine treats every simulation and model improvement as evidence, +not theater. + +- **Exact by construction** — immutable event streams, deterministic random namespaces, + SHA-256 event chains, and complete result-envelope verification. +- **Fast at two fidelities** — analytical F0 distributions for massive Monte Carlo and + event-level F1 paths for interpretable game flow. +- **Autonomously adaptive** — bounded population search, multiplicative expert weighting, + self-calibrating intervals, champion/challenger gates, and prospective shadow updates. +- **Fail-closed** — drift alerts quarantine adaptive simulation; failed challengers remain + in the ledger; existing checkpoints are never silently overwritten. +- **Portable** — dependency-free runtime, clean Python SDK and CLI, deterministic JSON + replay, and a domain-neutral stochastic-control boundary. + +## Verified release snapshot + +| Dimension | Version 0.3 evidence | +|---|---:| +| Supported leagues | 7 | +| Tests | 67 passed | +| Negative and mechanism controls | 49 passed, 0 failed | +| Randomized stress | 14,000 games / 2,003,304 events / 0 failures | +| F0 throughput | 111K–127K games/second | +| F1 single-worker throughput | 347–1,011 games/second | +| F1 four-worker scaling | 1.42×–1.82× | +| Recursive autonomy arena | 4 promotions / 3 guarded retentions | +| Calibration and shadow weight changes | 7 / 4 | +| Shadow drift alerts | 0 | + +Machine-readable results live in +[release evidence](./reports/RELEASE_EVIDENCE.md), +[performance evidence](./reports/PERFORMANCE_AUDIT_V3_FINAL.json), +[stress evidence](./reports/STRESS_AUDIT_V3.json), and the +[autonomy audit](./reports/AUTONOMY_AUDIT_V3.json). + +## Architecture + +```mermaid +flowchart TD + CLI["CLI / Python SDK"] --> GOV["Champion governance"] + GOV --> CAL["Recursive calibration + online weights"] + CAL --> F0["F0 analytical distributions"] + GOV --> F1["F1 event simulation"] + F0 --> PACKS["Seven isolated league packs"] + F1 --> PACKS + PACKS --> CORE["Deterministic event / rule / RNG kernel"] + CORE --> PROOF["Replay + hashes + evidence bundles"] + PROOF --> RED["Stress, controls, drift, rollback"] +``` + +The neutral kernel contains no player, inning, possession, ball, puck, or scoreboard +type. Sport semantics remain inside league packs; model governance remains outside +simulation truth. + +## Seven leagues, one kernel + +| Family | League packs | Primary F1 unit | +|---|---|---| +| Bat and ball | MLB | Pitch / plate appearance / inning | +| Court invasion | NBA, WNBA, NCAA MBB | Possession / period / game | +| Gridiron | NFL, NCAA Football | Play / drive / game | +| Ice invasion | NHL | Event / shift / period | + +Each pack owns its rules, scoring, clocks, stochastic transitions, and provisional +reference anchors. One sport is never treated as a rescaled copy of another. + +## Recursive improvement engine + +```mermaid +flowchart LR + T["Point-in-time train"] --> S["Bounded population search"] + S --> C["Chronological calibration"] + C --> W["Expert reweighting + interval adaptation"] + W --> G{"All gates pass?"} + G -- Yes --> P["Promote + freeze checkpoint"] + G -- No --> R["Retain incumbent + record failure"] + P --> H["Prospective shadow outcomes"] + R --> H + H --> D{"Drift?"} + D -- Healthy --> W + D -- Alert --> Q["Quarantine / recalibrate / rollback"] +``` + +Profiles, ensembles, adaptive states, champion bundles, and promotion ledgers all +carry independent content hashes. Promoted bundles embed the complete incumbent +model and state, so rollback is self-contained rather than merely documented. + +Read the full [autonomous improvement protocol](./AUTONOMOUS_IMPROVEMENT.md). + +## Quick start + +```powershell +python -m venv .venv +.\.venv\Scripts\python.exe -m pip install -e ".[dev]" + +.\.venv\Scripts\sports-engine.exe list-leagues +.\.venv\Scripts\sports-engine.exe simulate --league nba --seed 42 +.\.venv\Scripts\sports-engine.exe simulate-fast ` + --league nfl --seed 42 --home-strength 1.0 --away-strength 1.0 +``` + +### Run recursive calibration + +```powershell +.\.venv\Scripts\sports-engine.exe auto-improve ` + --csv data\point-in-time\historical.csv ` + --config AUTONOMY_CONFIG.json ` + --output reports\improvement-run-001 + +.\.venv\Scripts\sports-engine.exe verify-autonomy ` + --bundle reports\improvement-run-001\champion_bundle.json ` + --ledger reports\improvement-run-001\improvement_ledger.jsonl +``` + +### Simulate an adaptive champion + +```powershell +.\.venv\Scripts\sports-engine.exe simulate-adaptive ` + --bundle reports\improvement-run-001\champion_bundle.json ` + --league nba --seed 42 --home-strength 1.0 --away-strength 1.0 +``` + +## Evidence and documentation + +| Document | Purpose | +|---|---| +| [Architecture](./ARCHITECTURE.md) | Kernel, fidelity, and governance boundaries | +| [Autonomous improvement](./AUTONOMOUS_IMPROVEMENT.md) | Search, weighting, drift, promotion, and rollback | +| [Quality gates](./QUALITY_GATES.yaml) | Machine-readable non-negotiable release rules | +| [Release evidence](./reports/RELEASE_EVIDENCE.md) | Current validation and reproducibility snapshot | +| [Upgrade audit](./reports/AUTONOMOUS_RECURSIVE_UPGRADE_V3.md) | Version 0.3 implementation and results | +| [Known limitations](./KNOWN_LIMITATIONS.md) | Exact boundaries of demonstrated capability | +| [Prior art](./PRIOR_ART_REGISTRY.md) | Primary references and defensible distinctions | + +## Evidence boundary + +The bundled autonomy arena is deliberately synthetic. It proves that the engine can +discover, reject, promote, reweight, checkpoint, reload, monitor, and roll back models; +it does **not** turn synthetic outcomes into a claim of real-world accuracy. + +Licensed point-in-time game, roster, injury, event, and tracking data are required +before held-out predictive certification. F2 spatial agents and F3 physics remain +blocked rather than fabricated. + +--- + +
+ +**Build measurable worlds. Preserve every decision. Believe only what survives.** + +
diff --git a/RESEARCH_CONSTITUTION.md b/RESEARCH_CONSTITUTION.md new file mode 100644 index 0000000..5dff31d --- /dev/null +++ b/RESEARCH_CONSTITUTION.md @@ -0,0 +1,12 @@ +# Research Constitution + +1. The engine produces research simulations, not wagers or live actions. +2. Identical versioned inputs and seeds must reproduce exactly. +3. Every meaningful transition must be present in a verifiable event stream. +4. Hard rules are declarative and versioned; inferred behavior remains replaceable. +5. Sport-specific concepts stay outside the domain-neutral kernel. +6. Synthetic fixtures test mechanics; they never certify real-world predictive accuracy. +7. Calibration claims require legally usable point-in-time data and held-out evaluation. +8. Negative controls, adversarial tests, and failures remain part of the evidence. +9. Complexity must add measured fidelity, performance, or explanatory value. +10. Unresolved gaps remain explicit and fail closed. diff --git a/STATUS.md b/STATUS.md new file mode 100644 index 0000000..7799431 --- /dev/null +++ b/STATUS.md @@ -0,0 +1,53 @@ +# Status + +## Executive determination + +**VERSION 0.3 ADAPTIVE AUDITED PROVISIONAL MULTI-LEAGUE ENGINE COMPLETE** + +Engineering scope is complete for deterministic F0/F1 simulation, portable replay, +counterfactual branching, batch execution, seasons, tournaments, seven league +packs, validation, negative controls, fidelity gating, CLI, and the domain-neutral +future-adaptation bridge. + +The audit added strict typing, full result-envelope integrity, temporal and phase +invariants, confidence-aware reference gates, distributional historical evaluation, +current rule-source manifests, randomized stress testing, and a distinct analytical +F0 path. + +Version 0.3 adds bounded recursive analytical calibration, deterministic population +search, online expert weighting, adaptive interval coverage, drift quarantine, +content-addressed champion checkpoints, rollback lineage, and chained promotion +ledgers. The seven-league synthetic autonomy arena was exactly reproducible, adjusted +all seven expert pools, promoted four challengers, and correctly retained three +incumbents under preregistered guardrails. + +Certification is intentionally blocked at held-out real-world calibration because +the Kimi remote workspace was not exported and no licensed point-in-time event or +tracking dataset exists in the local handoff. + +## Recovery note + +The prior remote run stopped during Stage 5 expansion. Its recovery agents remained +pending for over an hour, and the Kimi memory quota expired before the repository +was exported. This local build reconstructs the captured architecture and completes +the executable scope without claiming to possess the lost remote artifacts. + +## Verified release evidence + +- Static analysis: strict `mypy` and `ruff` passed. +- Integration/replay suite: 62 tests passed. +- Reference-anchor run: 500 games per league, 3,500 total; all seven remained + `provisional_reference_anchor`. +- Negative and mechanism controls: 49 executed, zero failures. +- Randomized stress: 14,000 games and 2,003,304 verified events, zero failures. +- Performance: F0 exceeded 137,000 games/second in every league; single-worker F1 + ranged from 372 to 1,204 games/second with full result-envelope hashing. +- At two workers, F1 throughput improved 2.18x to 4.99x versus the version 0.1 + evidence benchmark, depending on league. +- Clean wheel reproduction: package installed in a new virtual environment and + independently ran league discovery and an NHL simulation. +- Kimi desktop process: stopped. The separate Kimi CLI process was preserved. +- Recursive autonomy audit: seven leagues, eight generations each, 224 candidates + per generation wave, four promotions, three guarded retentions, seven weight + adjustments, exact bundle reload reproduction, four further prospective-shadow + weight changes, zero drift alerts, and exact shadow-checkpoint reproduction. diff --git a/TECHNOLOGY_BAKEOFF.md b/TECHNOLOGY_BAKEOFF.md new file mode 100644 index 0000000..00a898a --- /dev/null +++ b/TECHNOLOGY_BAKEOFF.md @@ -0,0 +1,31 @@ +# Technology Bake-off + +## Compared architectures + +1. Event-sourced immutable kernel with explicit phase ordering and pure reducers. +2. Coroutine/process-oriented discrete-event simulation with serialized continuation state. + +The event-sourced design won because exact replay, branching, failure recovery, and +cross-platform pickling are direct properties rather than special-case machinery. +The process-oriented design's strongest ideas were retained: phase frontiers, +single-timeline determinism, game-level parallelism, and replaceable resolvers. + +## Stack decision + +The core uses Python 3.12+ and the standard library. This minimizes supply-chain and +installation risk while preserving multiprocessing and ML interoperability. +Columnar/GPU accelerators remain optional adapter work once a licensed dataset and a +measured bottleneck justify them. Rendering is separate from simulation truth. + +## Version 0.2 performance decision + +Profiling identified tuple-copy event construction, repeated `random.Random` +initialization, and redundant result-time chain verification as the binding costs. +Version 0.2 replaced them with linear draft finalization, direct digest-to-uniform +draws, and one trusted construction pass followed by verification at external +boundaries. + +A binary event encoding prototype was rejected after profiling showed that many +small `hash.update` and `struct.pack` calls were slower in CPython than one canonical +JSON serialization. Rejection evidence was preserved instead of retaining an +architecturally fashionable regression. diff --git a/data/contracts/HISTORICAL_GAMES.md b/data/contracts/HISTORICAL_GAMES.md new file mode 100644 index 0000000..2f9c360 --- /dev/null +++ b/data/contracts/HISTORICAL_GAMES.md @@ -0,0 +1,24 @@ +# Historical game evaluation contract + +The `evaluate-historical` command accepts a UTF-8 CSV with these exact columns: + +| Column | Meaning | +|---|---| +| `game_id` | Stable unique identifier | +| `league` | One supported lowercase league ID | +| `decision_time` | Offset-aware ISO-8601 forecast timestamp | +| `features_available_at` | Latest feature timestamp; must not exceed decision time | +| `outcome_time` | Offset-aware ISO-8601 completion timestamp | +| `home_score`, `away_score` | Non-negative final scores | +| `home_strength`, `away_strength` | Frozen multipliers within `[0.25, 2.0]` | +| `split` | `train`, `calibration`, `test`, or prospective `shadow` | + +For each league, outcomes in an earlier split must precede decisions in the next +split. Reporting accepts only test-split games and returns score MAE, home-win Brier +score, score CRPS, and 50%/90% interval coverage. A successful run is historical +evaluation evidence, not automatic predictive certification. + +Autonomous search may read `train` and perform prequential promotion checks on +`calibration`. It may not tune on `test`. An already frozen champion may update +weights, interval scale, and drift state from `shadow` records only after each +outcome is chronologically available. diff --git a/dummy_bridge/DOMAIN_NEUTRAL_PROTOCOL.md b/dummy_bridge/DOMAIN_NEUTRAL_PROTOCOL.md new file mode 100644 index 0000000..fab3719 --- /dev/null +++ b/dummy_bridge/DOMAIN_NEUTRAL_PROTOCOL.md @@ -0,0 +1,20 @@ +# Domain-Neutral Dummy Bridge + +The bridge exposes immutable `State`, `Action`, `Observation`, `PolicyDecision`, +`Scenario`, `Counterfactual`, and `Evidence` records. No record imports league, +player, possession, ball, puck, score, or wagering semantics. + +Required adapter operations: + +```text +initialize(scenario, seed) -> state +observe(state, observer) -> observation +enumerate_actions(observation, constraints) -> actions +transition(state, actions, seed_labels) -> state, events +score(state, objective) -> distribution +branch(state, intervention, shared_seed) -> paired evidence +``` + +The included generic inventory-control fixture is the compatibility proof that the +contracts are reusable outside sports. A future Dummy adapter must provide its own +domain pack and must not import sports rules or reference anchors. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3a9d857 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,38 @@ +[build-system] +requires = ["setuptools>=77"] +build-backend = "setuptools.build_meta" + +[project] +name = "universal-sports-engine" +version = "0.3.0" +description = "Deterministic multi-league sports simulation and counterfactual research engine" +readme = "README.md" +requires-python = ">=3.12" +license = "MIT" +authors = [{ name = "Universal Sports Engine Research Program" }] +dependencies = [] + +[project.optional-dependencies] +dev = ["mypy>=1.16", "pytest>=9.0", "ruff>=0.12"] + +[project.scripts] +sports-engine = "universal_sports_engine.cli:main" + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.pytest.ini_options] +addopts = "-ra --strict-markers" +testpaths = ["tests"] + +[tool.ruff] +line-length = 100 +target-version = "py312" + +[tool.ruff.lint] +select = ["E", "F", "I", "UP", "B", "SIM", "RUF"] + +[tool.mypy] +python_version = "3.12" +strict = true +files = ["src", "scripts"] diff --git a/reports/AUTONOMOUS_RECURSIVE_UPGRADE_V3.md b/reports/AUTONOMOUS_RECURSIVE_UPGRADE_V3.md new file mode 100644 index 0000000..8e5f8d5 --- /dev/null +++ b/reports/AUTONOMOUS_RECURSIVE_UPGRADE_V3.md @@ -0,0 +1,105 @@ +# Autonomous recursive upgrade audit — version 0.3 + +## Determination + +**ENGINEERING PASS — REAL-WORLD CALIBRATION STILL BLOCKED BY MISSING DATA.** + +The isolated Universal Sports Engine now contains a functioning governed +recursive-improvement system. It can search bounded analytical models, tune +expert mixtures, adapt interval coverage, detect drift, promote challengers, +retain incumbents, reproduce checkpoints, verify decision ledgers, and roll +back a promoted league without changing source code or protected evaluators. + +No Dummy workspace or integration was inspected or modified. + +## Implemented autonomy stack + +- Immutable content-addressed league profiles. +- Deterministic log-space population mutation. +- Eight-generation default champion/challenger search. +- Train-only parameter selection. +- Chronological prequential calibration evaluation. +- Joint total/margin likelihood scoring. +- Home-win Brier guardrail. +- Adaptive 90% interval scaling. +- Multiplicative online expert weighting. +- Short/long loss EWMA drift detection. +- Fail-closed adaptive simulation during drift alerts. +- Fully embedded incumbent ensembles and states for self-contained rollback. +- Hash-verified profile, ensemble, state, bundle, and decision-ledger envelopes. +- Atomic, no-overwrite checkpoint publication. +- Prospective shadow-only weight and interval updates. +- CLI workflows for improve, simulate, adapt, verify, and roll back. + +## Seven-league autonomy arena + +The arena uses synthetic fixtures solely to test learning mechanics. It is not +real-world evidence. + +| Measure | Result | +|---|---:| +| Supported leagues | 7 | +| Search generations per league | 8 | +| Population per generation | 32 | +| Training games per league | 100 | +| Calibration games per league | 50 | +| Shadow games per league | 40 | +| Challengers promoted | 4 | +| Incumbents retained by guardrails | 3 | +| Calibration expert pools reweighted | 7 | +| Shadow expert pools reweighted | 4 | +| Shadow drift alerts | 0 | +| Ledger records verified | 7 | +| Deterministic repeat | PASS | +| Champion bundle reload | PASS | +| Shadow bundle reload | PASS | +| Embedded rollback smoke test | PASS | + +MLB was retained despite better aggregate composite loss because its Brier +guardrail deteriorated beyond the configured limit. NCAAF and NFL were retained +because their relative improvements did not reach the preregistered threshold. +This demonstrates that the loop can reject as well as promote. + +## Base-engine regression evidence + +- Strict MyPy: PASS. +- Ruff: PASS. +- Test suite: 67 passed. +- Randomized stress: 14,000 games, 2,003,304 events, zero integrity failures. +- F0 throughput after hot-loop optimization: 111,365 to 127,129 games/second + for six leagues and 121,071 for NHL in the final benchmark. +- Single-worker F1: 347 to 1,011 games/second. +- Four-worker F1 speedup: 1.42x to 1.82x. + +Exact machine-readable evidence: + +- `reports/AUTONOMY_AUDIT_V3.json` +- `reports/autonomy_v3_fixture/champion_bundle.json` +- `reports/autonomy_v3_fixture/shadow_adapted_bundle.json` +- `reports/autonomy_v3_fixture/improvement_ledger.jsonl` +- `reports/PERFORMANCE_AUDIT_V3_FINAL.json` +- `reports/STRESS_AUDIT_V3.json` + +## Research basis + +The implementation adapts established ideas while making no unsupported novelty +claim: + +- Population Based Training: https://arxiv.org/abs/1711.09846 +- Adaptive Conformal Inference: https://arxiv.org/abs/2106.00170 +- Online probabilistic expert aggregation: + https://proceedings.mlr.press/v128/korotin20a/korotin20a.pdf +- Optuna search-system design: https://arxiv.org/abs/1907.10902 + +Its defensible distinction is the evidence architecture: deterministic sports +simulation, point-in-time splits, bounded recursive search, online weighting, +adaptive coverage, drift quarantine, embedded rollback, and complete +content-addressed lineage in one dependency-free package. + +## Certification boundary + +This release proves autonomous learning mechanics, integrity, reproducibility, +and performance. It cannot prove superior real-world prediction without licensed +point-in-time game, player, roster, injury, event, and tracking data. Current +recursive tuning applies to F0 analytical score models; F1 event calibration, +F2 spatial agents, and F3 physics remain blocked rather than fabricated. diff --git a/reports/AUTONOMY_AUDIT_V3.json b/reports/AUTONOMY_AUDIT_V3.json new file mode 100644 index 0000000..c6fad96 --- /dev/null +++ b/reports/AUTONOMY_AUDIT_V3.json @@ -0,0 +1,191 @@ +{ + "bundle_hash": "56c5551d36093708f6da5ad0165f968a12c69c7ede7216959f7a4d532a40d754", + "bundle_reproduced": true, + "deterministic_recursive_search": true, + "evidence_status": "synthetic_autonomy_audit_not_real_world_certification", + "leagues": [ + { + "challenger_score": { + "composite_loss": 5.628048038203239, + "games": 50, + "home_win_brier": 0.21438362127823524, + "interval_coverage": 0.88, + "interval_coverage_error": 0.020000000000000018, + "mean_joint_negative_log_likelihood": 5.159280795646769 + }, + "decision": "retained_incumbent:brier_guardrail_failed", + "generations": 8, + "incumbent_score": { + "composite_loss": 5.874629433875539, + "games": 50, + "home_win_brier": 0.20077420341310603, + "interval_coverage": 0.96, + "interval_coverage_error": 0.05999999999999994, + "mean_joint_negative_log_likelihood": 5.353081027049327 + }, + "league": "mlb", + "promoted": false, + "relative_improvement": 0.04197394890142503, + "selected_ensemble_hash": "bb0b6a4184ebbf8489abaf3dc3df077d280c14d888340b59c2871215f6df8360", + "selected_state_hash": "a23ac9f2ccc36db8368dcd341b0025f12d6d2b188c95ca9013b1dc61b461e78a" + }, + { + "challenger_score": { + "composite_loss": 8.248030969834542, + "games": 50, + "home_win_brier": 0.0698963567147646, + "interval_coverage": 0.94, + "interval_coverage_error": 0.039999999999999925, + "mean_joint_negative_log_likelihood": 8.028238256405013 + }, + "decision": "promoted", + "generations": 8, + "incumbent_score": { + "composite_loss": 9.310371209830079, + "games": 50, + "home_win_brier": 0.0654562378544873, + "interval_coverage": 0.83, + "interval_coverage_error": 0.07000000000000006, + "mean_joint_negative_log_likelihood": 9.039458734121103 + }, + "league": "nba", + "promoted": true, + "relative_improvement": 0.11410288763501682, + "selected_ensemble_hash": "60e7da6020d67de678c0d044f300847fde90bf16161917510b08c83bfd5dbe96", + "selected_state_hash": "fe671acbd9f09da2f1984ffa82fa65ab28b4110cb59498739477866e598f119d" + }, + { + "challenger_score": { + "composite_loss": 8.761251921087043, + "games": 50, + "home_win_brier": 0.2309784924355083, + "interval_coverage": 0.87, + "interval_coverage_error": 0.030000000000000027, + "mean_joint_negative_log_likelihood": 8.239294936216025 + }, + "decision": "retained_incumbent:minimum_relative_improvement_not_met", + "generations": 8, + "incumbent_score": { + "composite_loss": 8.809151979845048, + "games": 50, + "home_win_brier": 0.22737367673106743, + "interval_coverage": 0.92, + "interval_coverage_error": 0.020000000000000018, + "mean_joint_negative_log_likelihood": 8.314404626382911 + }, + "league": "ncaaf", + "promoted": false, + "relative_improvement": 0.005437533472869828, + "selected_ensemble_hash": "110e0a2b1d912b65c9c8e2421251514d29e036503be2191a6a19c5418d16b42a", + "selected_state_hash": "6490ed8bb46c72d47e410317f52bba03128f9076dbe23088cbd0748735b8b91b" + }, + { + "challenger_score": { + "composite_loss": 8.216060900552336, + "games": 50, + "home_win_brier": 0.16681685856160933, + "interval_coverage": 0.92, + "interval_coverage_error": 0.020000000000000018, + "mean_joint_negative_log_likelihood": 7.842427183429115 + }, + "decision": "promoted", + "generations": 8, + "incumbent_score": { + "composite_loss": 8.578503263410775, + "games": 50, + "home_win_brier": 0.16468304335835116, + "interval_coverage": 0.9, + "interval_coverage_error": 0.0, + "mean_joint_negative_log_likelihood": 8.249137176694074 + }, + "league": "ncaambb", + "promoted": true, + "relative_improvement": 0.04225006993986202, + "selected_ensemble_hash": "3a26b1869b41e662ecdfe044a54229c6e5998280f88a6ad5003326302350654e", + "selected_state_hash": "924515d3adfcde4094160bbe9a1c1741be83320aad2c0d1e322f724c309fcb9d" + }, + { + "challenger_score": { + "composite_loss": 8.35269018511674, + "games": 50, + "home_win_brier": 0.23858136537417907, + "interval_coverage": 0.89, + "interval_coverage_error": 0.010000000000000009, + "mean_joint_negative_log_likelihood": 7.855527454368383 + }, + "decision": "retained_incumbent:minimum_relative_improvement_not_met", + "generations": 8, + "incumbent_score": { + "composite_loss": 8.427984368402127, + "games": 50, + "home_win_brier": 0.23551834611431333, + "interval_coverage": 0.94, + "interval_coverage_error": 0.039999999999999925, + "mean_joint_negative_log_likelihood": 7.8769476761735 + }, + "league": "nfl", + "promoted": false, + "relative_improvement": 0.008933830438470725, + "selected_ensemble_hash": "52cc748b0f03d805bc32644c746152d71b8878f6cb3e9e2e6df533e3cb3718e2", + "selected_state_hash": "c56bdf6245fc5a57c735db00e6d448c6670f5a96632121cbfa3e9d7e30dd0e91" + }, + { + "challenger_score": { + "composite_loss": 4.7671157212113355, + "games": 50, + "home_win_brier": 0.2228624325270619, + "interval_coverage": 0.9, + "interval_coverage_error": 0.0, + "mean_joint_negative_log_likelihood": 4.321390856157212 + }, + "decision": "promoted", + "generations": 8, + "incumbent_score": { + "composite_loss": 4.831941943380519, + "games": 50, + "home_win_brier": 0.2199413705553162, + "interval_coverage": 0.91, + "interval_coverage_error": 0.010000000000000009, + "mean_joint_negative_log_likelihood": 4.372059202269887 + }, + "league": "nhl", + "promoted": true, + "relative_improvement": 0.013416183995751795, + "selected_ensemble_hash": "b4268f4251d62981ee852625c817f48947e9e12aecc39da1b77d4ff869c86289", + "selected_state_hash": "9e103f41a8cf8181c03bde20e2b260ff7daa3ca261c7c760e94c10dca68cc368" + }, + { + "challenger_score": { + "composite_loss": 8.379522034337166, + "games": 50, + "home_win_brier": 0.16069882492885665, + "interval_coverage": 0.9, + "interval_coverage_error": 0.0, + "mean_joint_negative_log_likelihood": 8.058124384479452 + }, + "decision": "promoted", + "generations": 8, + "incumbent_score": { + "composite_loss": 9.188926654018786, + "games": 50, + "home_win_brier": 0.16577671914460065, + "interval_coverage": 0.87, + "interval_coverage_error": 0.030000000000000027, + "mean_joint_negative_log_likelihood": 8.797373215729584 + }, + "league": "wnba", + "promoted": true, + "relative_improvement": 0.08808478401855853, + "selected_ensemble_hash": "369bd1fab70c4fe9c443214b61e5bb1297947388fae8402061d599b6dc5a975d", + "selected_state_hash": "b5389b73b296d8ff3307fa1679ea4534b3e0cdb82aba2cf0f5a58625c2587f88" + } + ], + "promotions": 4, + "shadow_bundle_hash": "7f1903911221d44a29bbed34a9e13e47e25f0bf8c1ccf3ba880dbc2c2d03722a", + "shadow_bundle_reproduced": true, + "shadow_drift_alerts": 0, + "shadow_weight_adjustments": 4, + "total_leagues": 7, + "verified_ledger_records": 7, + "weight_adjustments": 7 +} diff --git a/reports/NEGATIVE_CONTROL_REPORT.json b/reports/NEGATIVE_CONTROL_REPORT.json new file mode 100644 index 0000000..38395ca --- /dev/null +++ b/reports/NEGATIVE_CONTROL_REPORT.json @@ -0,0 +1,247 @@ +[ + { + "control": "mlb.deterministic_replay", + "description": "same seed and version", + "passed": true + }, + { + "control": "mlb.finite_scores", + "description": "scores must be finite", + "passed": true + }, + { + "control": "mlb.event_integrity", + "description": "SHA-256 chain verification", + "passed": true + }, + { + "control": "mlb.result_integrity", + "description": "complete result envelope hash verification", + "passed": true + }, + { + "control": "mlb.seed_sensitivity", + "description": "different seed changes the path", + "passed": true + }, + { + "control": "mlb.strength_monotonicity", + "description": "paired stronger home input must increase mean home score", + "passed": true + }, + { + "control": "mlb.home_advantage_direction", + "description": "home advantage must not be statistically materially reversed", + "passed": true + }, + { + "control": "nba.deterministic_replay", + "description": "same seed and version", + "passed": true + }, + { + "control": "nba.finite_scores", + "description": "scores must be finite", + "passed": true + }, + { + "control": "nba.event_integrity", + "description": "SHA-256 chain verification", + "passed": true + }, + { + "control": "nba.result_integrity", + "description": "complete result envelope hash verification", + "passed": true + }, + { + "control": "nba.seed_sensitivity", + "description": "different seed changes the path", + "passed": true + }, + { + "control": "nba.strength_monotonicity", + "description": "paired stronger home input must increase mean home score", + "passed": true + }, + { + "control": "nba.home_advantage_direction", + "description": "home advantage must not be statistically materially reversed", + "passed": true + }, + { + "control": "wnba.deterministic_replay", + "description": "same seed and version", + "passed": true + }, + { + "control": "wnba.finite_scores", + "description": "scores must be finite", + "passed": true + }, + { + "control": "wnba.event_integrity", + "description": "SHA-256 chain verification", + "passed": true + }, + { + "control": "wnba.result_integrity", + "description": "complete result envelope hash verification", + "passed": true + }, + { + "control": "wnba.seed_sensitivity", + "description": "different seed changes the path", + "passed": true + }, + { + "control": "wnba.strength_monotonicity", + "description": "paired stronger home input must increase mean home score", + "passed": true + }, + { + "control": "wnba.home_advantage_direction", + "description": "home advantage must not be statistically materially reversed", + "passed": true + }, + { + "control": "ncaambb.deterministic_replay", + "description": "same seed and version", + "passed": true + }, + { + "control": "ncaambb.finite_scores", + "description": "scores must be finite", + "passed": true + }, + { + "control": "ncaambb.event_integrity", + "description": "SHA-256 chain verification", + "passed": true + }, + { + "control": "ncaambb.result_integrity", + "description": "complete result envelope hash verification", + "passed": true + }, + { + "control": "ncaambb.seed_sensitivity", + "description": "different seed changes the path", + "passed": true + }, + { + "control": "ncaambb.strength_monotonicity", + "description": "paired stronger home input must increase mean home score", + "passed": true + }, + { + "control": "ncaambb.home_advantage_direction", + "description": "home advantage must not be statistically materially reversed", + "passed": true + }, + { + "control": "nfl.deterministic_replay", + "description": "same seed and version", + "passed": true + }, + { + "control": "nfl.finite_scores", + "description": "scores must be finite", + "passed": true + }, + { + "control": "nfl.event_integrity", + "description": "SHA-256 chain verification", + "passed": true + }, + { + "control": "nfl.result_integrity", + "description": "complete result envelope hash verification", + "passed": true + }, + { + "control": "nfl.seed_sensitivity", + "description": "different seed changes the path", + "passed": true + }, + { + "control": "nfl.strength_monotonicity", + "description": "paired stronger home input must increase mean home score", + "passed": true + }, + { + "control": "nfl.home_advantage_direction", + "description": "home advantage must not be statistically materially reversed", + "passed": true + }, + { + "control": "ncaaf.deterministic_replay", + "description": "same seed and version", + "passed": true + }, + { + "control": "ncaaf.finite_scores", + "description": "scores must be finite", + "passed": true + }, + { + "control": "ncaaf.event_integrity", + "description": "SHA-256 chain verification", + "passed": true + }, + { + "control": "ncaaf.result_integrity", + "description": "complete result envelope hash verification", + "passed": true + }, + { + "control": "ncaaf.seed_sensitivity", + "description": "different seed changes the path", + "passed": true + }, + { + "control": "ncaaf.strength_monotonicity", + "description": "paired stronger home input must increase mean home score", + "passed": true + }, + { + "control": "ncaaf.home_advantage_direction", + "description": "home advantage must not be statistically materially reversed", + "passed": true + }, + { + "control": "nhl.deterministic_replay", + "description": "same seed and version", + "passed": true + }, + { + "control": "nhl.finite_scores", + "description": "scores must be finite", + "passed": true + }, + { + "control": "nhl.event_integrity", + "description": "SHA-256 chain verification", + "passed": true + }, + { + "control": "nhl.result_integrity", + "description": "complete result envelope hash verification", + "passed": true + }, + { + "control": "nhl.seed_sensitivity", + "description": "different seed changes the path", + "passed": true + }, + { + "control": "nhl.strength_monotonicity", + "description": "paired stronger home input must increase mean home score", + "passed": true + }, + { + "control": "nhl.home_advantage_direction", + "description": "home advantage must not be statistically materially reversed", + "passed": true + } +] diff --git a/reports/PERFORMANCE_AUDIT_V3_FINAL.json b/reports/PERFORMANCE_AUDIT_V3_FINAL.json new file mode 100644 index 0000000..a351cdc --- /dev/null +++ b/reports/PERFORMANCE_AUDIT_V3_FINAL.json @@ -0,0 +1,261 @@ +[ + { + "elapsed_seconds": 0.7956835000077263, + "events": 0, + "fidelity": "F0_ANALYTICAL", + "games": 100000, + "games_per_second": 125678.11196163924, + "league": "mlb", + "results": 100000, + "workers": 1 + }, + { + "elapsed_seconds": 1.3158665999944787, + "events": 86681, + "fidelity": "F1_EVENT", + "games": 1000, + "games_per_second": 759.9554544542706, + "league": "mlb", + "workers": 1 + }, + { + "elapsed_seconds": 1.1424124999903142, + "events": 86681, + "fidelity": "F1_EVENT", + "games": 1000, + "games_per_second": 875.3405621948975, + "league": "mlb", + "workers": 2 + }, + { + "elapsed_seconds": 0.9006621000007726, + "events": 86681, + "fidelity": "F1_EVENT", + "games": 1000, + "games_per_second": 1110.294304600074, + "league": "mlb", + "workers": 4 + }, + { + "elapsed_seconds": 0.8979504000162706, + "events": 0, + "fidelity": "F0_ANALYTICAL", + "games": 100000, + "games_per_second": 111364.72571111727, + "league": "nba", + "results": 100000, + "workers": 1 + }, + { + "elapsed_seconds": 2.880733900004998, + "events": 200358, + "fidelity": "F1_EVENT", + "games": 1000, + "games_per_second": 347.1337633782367, + "league": "nba", + "workers": 1 + }, + { + "elapsed_seconds": 2.5140926000021864, + "events": 200358, + "fidelity": "F1_EVENT", + "games": 1000, + "games_per_second": 397.7578232397368, + "league": "nba", + "workers": 2 + }, + { + "elapsed_seconds": 1.5892845000198577, + "events": 200358, + "fidelity": "F1_EVENT", + "games": 1000, + "games_per_second": 629.2139638859533, + "league": "nba", + "workers": 4 + }, + { + "elapsed_seconds": 0.8157341000041924, + "events": 0, + "fidelity": "F0_ANALYTICAL", + "games": 100000, + "games_per_second": 122588.96618332622, + "league": "wnba", + "results": 100000, + "workers": 1 + }, + { + "elapsed_seconds": 2.316987900005188, + "events": 161766, + "fidelity": "F1_EVENT", + "games": 1000, + "games_per_second": 431.5948305115279, + "league": "wnba", + "workers": 1 + }, + { + "elapsed_seconds": 1.8946968000091147, + "events": 161766, + "fidelity": "F1_EVENT", + "games": 1000, + "games_per_second": 527.7889317146625, + "league": "wnba", + "workers": 2 + }, + { + "elapsed_seconds": 1.4412796000251547, + "events": 161766, + "fidelity": "F1_EVENT", + "games": 1000, + "games_per_second": 693.8279012500745, + "league": "wnba", + "workers": 4 + }, + { + "elapsed_seconds": 0.7866054000041913, + "events": 0, + "fidelity": "F0_ANALYTICAL", + "games": 100000, + "games_per_second": 127128.54501058238, + "league": "ncaambb", + "results": 100000, + "workers": 1 + }, + { + "elapsed_seconds": 2.022876900009578, + "events": 139472, + "fidelity": "F1_EVENT", + "games": 1000, + "games_per_second": 494.3454542366197, + "league": "ncaambb", + "workers": 1 + }, + { + "elapsed_seconds": 1.6672028000175487, + "events": 139472, + "fidelity": "F1_EVENT", + "games": 1000, + "games_per_second": 599.8070540605343, + "league": "ncaambb", + "workers": 2 + }, + { + "elapsed_seconds": 1.1097719999961555, + "events": 139472, + "fidelity": "F1_EVENT", + "games": 1000, + "games_per_second": 901.085988836864, + "league": "ncaambb", + "workers": 4 + }, + { + "elapsed_seconds": 0.8237592999939807, + "events": 0, + "fidelity": "F0_ANALYTICAL", + "games": 100000, + "games_per_second": 121394.68410339126, + "league": "nfl", + "results": 100000, + "workers": 1 + }, + { + "elapsed_seconds": 2.0862898000050336, + "events": 165521, + "fidelity": "F1_EVENT", + "games": 1000, + "games_per_second": 479.3197953599674, + "league": "nfl", + "workers": 1 + }, + { + "elapsed_seconds": 1.8517403000150807, + "events": 165521, + "fidelity": "F1_EVENT", + "games": 1000, + "games_per_second": 540.032530475173, + "league": "nfl", + "workers": 2 + }, + { + "elapsed_seconds": 1.2846881999867037, + "events": 165521, + "fidelity": "F1_EVENT", + "games": 1000, + "games_per_second": 778.3989920747695, + "league": "nfl", + "workers": 4 + }, + { + "elapsed_seconds": 0.8141856000002008, + "events": 0, + "fidelity": "F0_ANALYTICAL", + "games": 100000, + "games_per_second": 122822.11820004595, + "league": "ncaaf", + "results": 100000, + "workers": 1 + }, + { + "elapsed_seconds": 2.30761300001177, + "events": 181042, + "fidelity": "F1_EVENT", + "games": 1000, + "games_per_second": 433.3482260651589, + "league": "ncaaf", + "workers": 1 + }, + { + "elapsed_seconds": 1.9399241000064649, + "events": 181042, + "fidelity": "F1_EVENT", + "games": 1000, + "games_per_second": 515.4840851746043, + "league": "ncaaf", + "workers": 2 + }, + { + "elapsed_seconds": 1.3367247999995016, + "events": 181042, + "fidelity": "F1_EVENT", + "games": 1000, + "games_per_second": 748.097140114684, + "league": "ncaaf", + "workers": 4 + }, + { + "elapsed_seconds": 0.8259643000201322, + "events": 0, + "fidelity": "F0_ANALYTICAL", + "games": 100000, + "games_per_second": 121070.60801243175, + "league": "nhl", + "results": 100000, + "workers": 1 + }, + { + "elapsed_seconds": 0.9892554999969434, + "events": 65803, + "fidelity": "F1_EVENT", + "games": 1000, + "games_per_second": 1010.8611981465757, + "league": "nhl", + "workers": 1 + }, + { + "elapsed_seconds": 0.8409421000105795, + "events": 65803, + "fidelity": "F1_EVENT", + "games": 1000, + "games_per_second": 1189.142510509843, + "league": "nhl", + "workers": 2 + }, + { + "elapsed_seconds": 0.6960250999836717, + "events": 65803, + "fidelity": "F1_EVENT", + "games": 1000, + "games_per_second": 1436.729796128702, + "league": "nhl", + "workers": 4 + } +] diff --git a/reports/PERFORMANCE_BENCHMARK.json b/reports/PERFORMANCE_BENCHMARK.json new file mode 100644 index 0000000..b6517b8 --- /dev/null +++ b/reports/PERFORMANCE_BENCHMARK.json @@ -0,0 +1,135 @@ +[ + { + "elapsed_seconds": 0.12223709997488186, + "events": 0, + "fidelity": "F0_ANALYTICAL", + "games": 10000, + "games_per_second": 81808.22354305584, + "league": "mlb", + "results": 10000, + "workers": 1 + }, + { + "elapsed_seconds": 0.3034494000021368, + "events": 8830, + "fidelity": "F1_EVENT", + "games": 100, + "games_per_second": 329.5442337315408, + "league": "mlb", + "workers": 2 + }, + { + "elapsed_seconds": 0.08154599997214973, + "events": 0, + "fidelity": "F0_ANALYTICAL", + "games": 10000, + "games_per_second": 122630.17196938272, + "league": "nba", + "results": 10000, + "workers": 1 + }, + { + "elapsed_seconds": 0.41247360000852495, + "events": 19954, + "fidelity": "F1_EVENT", + "games": 100, + "games_per_second": 242.43975856378012, + "league": "nba", + "workers": 2 + }, + { + "elapsed_seconds": 0.13489250000566244, + "events": 0, + "fidelity": "F0_ANALYTICAL", + "games": 10000, + "games_per_second": 74133.10598869638, + "league": "wnba", + "results": 10000, + "workers": 1 + }, + { + "elapsed_seconds": 0.3936280999914743, + "events": 16098, + "fidelity": "F1_EVENT", + "games": 100, + "games_per_second": 254.0469036691383, + "league": "wnba", + "workers": 2 + }, + { + "elapsed_seconds": 0.15269150002859533, + "events": 0, + "fidelity": "F0_ANALYTICAL", + "games": 10000, + "games_per_second": 65491.530295578, + "league": "ncaambb", + "results": 10000, + "workers": 1 + }, + { + "elapsed_seconds": 0.34768939996138215, + "events": 13938, + "fidelity": "F1_EVENT", + "games": 100, + "games_per_second": 287.6130247603378, + "league": "ncaambb", + "workers": 2 + }, + { + "elapsed_seconds": 0.13669269997626543, + "events": 0, + "fidelity": "F0_ANALYTICAL", + "games": 10000, + "games_per_second": 73156.79624249389, + "league": "nfl", + "results": 10000, + "workers": 1 + }, + { + "elapsed_seconds": 0.37054779997561127, + "events": 16529, + "fidelity": "F1_EVENT", + "games": 100, + "games_per_second": 269.8707157526824, + "league": "nfl", + "workers": 2 + }, + { + "elapsed_seconds": 0.09823079995112494, + "events": 0, + "fidelity": "F0_ANALYTICAL", + "games": 10000, + "games_per_second": 101801.06448258116, + "league": "ncaaf", + "results": 10000, + "workers": 1 + }, + { + "elapsed_seconds": 0.4321784999920055, + "events": 17952, + "fidelity": "F1_EVENT", + "games": 100, + "games_per_second": 231.3858741280508, + "league": "ncaaf", + "workers": 2 + }, + { + "elapsed_seconds": 0.14026539999758825, + "events": 0, + "fidelity": "F0_ANALYTICAL", + "games": 10000, + "games_per_second": 71293.41947602147, + "league": "nhl", + "results": 10000, + "workers": 1 + }, + { + "elapsed_seconds": 0.2912550999899395, + "events": 6583, + "fidelity": "F1_EVENT", + "games": 100, + "games_per_second": 343.341627334437, + "league": "nhl", + "workers": 2 + } +] diff --git a/reports/RELEASE_EVIDENCE.md b/reports/RELEASE_EVIDENCE.md new file mode 100644 index 0000000..24ca359 --- /dev/null +++ b/reports/RELEASE_EVIDENCE.md @@ -0,0 +1,63 @@ +# Release Evidence + +## Determination + +**PROVISIONAL MULTI-LEAGUE F0/F1 ENGINE COMPLETE.** + +This bundle verifies mechanics, determinism, replay integrity, league coverage, +reference-anchor sanity, headless execution, and the content-addressed autonomy +surface. It does not certify real-world predictive accuracy. No licensed point-in-time +event or tracking dataset was available. + +## Reference-anchor validation + +| League | Games | Home mean | Away mean | Mean error | Total SD error | Home z | Status | +|---|---:|---:|---:|---:|---:|---:|---| +| mlb | 500 | 4.942 | 4.426 | 0.086 | 0.021 | 2.58 | provisional_reference_anchor | +| nba | 500 | 116.606 | 115.722 | 0.024 | 0.003 | 1.18 | provisional_reference_anchor | +| wnba | 500 | 84.146 | 83.096 | 0.039 | 0.000 | 1.68 | provisional_reference_anchor | +| ncaambb | 500 | 69.156 | 67.136 | 0.078 | 0.003 | 3.47 | provisional_reference_anchor | +| nfl | 500 | 25.882 | 24.500 | 0.124 | 0.022 | 2.26 | provisional_reference_anchor | +| ncaaf | 500 | 33.456 | 29.822 | 0.154 | 0.057 | 5.46 | provisional_reference_anchor | +| nhl | 500 | 3.204 | 3.018 | 0.023 | 0.006 | 1.77 | provisional_reference_anchor | + +## Negative controls + +- Controls executed: 49 +- Failures: 0 + +## Headless benchmark + +| Fidelity | League | Games | Events | Seconds | Games/second | +|---|---|---:|---:|---:|---:| +| F0_ANALYTICAL | mlb | 10000 | 0 | 0.122 | 81808.22 | +| F1_EVENT | mlb | 100 | 8830 | 0.303 | 329.54 | +| F0_ANALYTICAL | nba | 10000 | 0 | 0.082 | 122630.17 | +| F1_EVENT | nba | 100 | 19954 | 0.412 | 242.44 | +| F0_ANALYTICAL | wnba | 10000 | 0 | 0.135 | 74133.11 | +| F1_EVENT | wnba | 100 | 16098 | 0.394 | 254.05 | +| F0_ANALYTICAL | ncaambb | 10000 | 0 | 0.153 | 65491.53 | +| F1_EVENT | ncaambb | 100 | 13938 | 0.348 | 287.61 | +| F0_ANALYTICAL | nfl | 10000 | 0 | 0.137 | 73156.80 | +| F1_EVENT | nfl | 100 | 16529 | 0.371 | 269.87 | +| F0_ANALYTICAL | ncaaf | 10000 | 0 | 0.098 | 101801.06 | +| F1_EVENT | ncaaf | 100 | 17952 | 0.432 | 231.39 | +| F0_ANALYTICAL | nhl | 10000 | 0 | 0.140 | 71293.42 | +| F1_EVENT | nhl | 100 | 6583 | 0.291 | 343.34 | + +## Reproducibility + +- Source manifest SHA-256: `7de72acb29259942070b5d8c0bd1532e8c6a58a5a5ecce6cf8ef76634175e272` +- Every event stream is invariant-checked and SHA-256 chained during construction. +- External replay imports reverify both the event chain and complete result envelope. +- Same input and seed equality is exercised for every league. +- Cross-worker batch equality is exercised by the integration suite. +- Recursive profiles, ensembles, adaptive states, champion bundles, and promotion + ledgers have independent content hashes. +- Autonomous search cannot mutate rules, evaluators, split boundaries, or source code. + +## Certification boundary + +`provisional_reference_anchor` is the strongest supported status. F2 spatial +calibration, F3 physics/rendering, player/team models, exhaustive rulebooks, and +held-out predictive validity remain blocked pending licensed data and scoped evidence. diff --git a/reports/RELEASE_MANIFEST.json b/reports/RELEASE_MANIFEST.json new file mode 100644 index 0000000..6720e5d --- /dev/null +++ b/reports/RELEASE_MANIFEST.json @@ -0,0 +1,268 @@ +{ + "engine_version": "0.3.0", + "evidence_status": "provisional_reference_anchor", + "league_ids": [ + "mlb", + "nba", + "wnba", + "ncaambb", + "nfl", + "ncaaf", + "nhl" + ], + "source_files": [ + [ + ".github/PULL_REQUEST_TEMPLATE.md", + "5b47fed04e57c5d858113040675e621d72265c881fd9680cfec5433c4dd99dd4" + ], + [ + ".github/workflows/quality.yml", + "5bdb93402626a6ddc4dd0ddee2801c2f2c3f6169333226b10fddb6dbf37a9b6a" + ], + [ + "data/contracts/HISTORICAL_GAMES.md", + "253328097b260e361eefd368720b4d039ab36a2877eeb027b7acbd04f580c646" + ], + [ + "dummy_bridge/DOMAIN_NEUTRAL_PROTOCOL.md", + "f612003bdf09839f6f675bb362c5cbae89c8610cfecc863b5a6749aa3a8b8f2f" + ], + [ + "scripts/audit_autonomy.py", + "df7884929493a1faab7fe6b387e82beba44a0228f99addaa1399d92cfab2e61b" + ], + [ + "scripts/audit_performance.py", + "a6b24c29cff64161db093c35631b16f8f85f93792752afff1b4f5926fc3c6850" + ], + [ + "scripts/audit_stress.py", + "728fbce5fb0179bd5461ccc947c801d0d89a3b1faf992ff1486b6bb0e685d3c2" + ], + [ + "scripts/build_release_evidence.py", + "810c664d62d72735a95e5f45a25520e95aa2efd2b7f065fc332f81d2f0ab94e8" + ], + [ + "src/universal_sports_engine/__init__.py", + "3913d4e82adc237821c9311d23987845b8cabc094c992bb60c369fa1e3b0fa77" + ], + [ + "src/universal_sports_engine/accuracy.py", + "e7a299d519ef8ad45402a0dc10ca47d1571e0cb6a5adfb2f2a898d899ebb8213" + ], + [ + "src/universal_sports_engine/adaptive.py", + "294d84426f0db394cb7326a33c7899389a3c54794aee8c2888ce0b4e6efe5e27" + ], + [ + "src/universal_sports_engine/analytical.py", + "f4dc550bfb01a399cf37f48f83489eb1f3e20fb27bfa50d3d370851c75f04d74" + ], + [ + "src/universal_sports_engine/autotune.py", + "d0b4b0fd5c95d8a87cb44750bfccba721ec94ad0352692ebcc17d859572eabe1" + ], + [ + "src/universal_sports_engine/cli.py", + "cb8ef9cb378397a2cc4dd661131ab8a345e7ae65b238b5253e0a2ab393e40038" + ], + [ + "src/universal_sports_engine/core/__init__.py", + "72bad1d11e7bbc22f13745be6bc0ea3cb85b799eaadc778d6ce53f42882de99f" + ], + [ + "src/universal_sports_engine/core/event_log.py", + "f5f53d302611e688b47f174bd250e7cd1cefaba92b7b6c529e94c2c43439661e" + ], + [ + "src/universal_sports_engine/core/fidelity.py", + "10e801d8a3239fc8c74012cde6cbd8f70c87f5d07aba1d4817314f11ca103032" + ], + [ + "src/universal_sports_engine/core/replay_io.py", + "09837a230d5e9cf45e0e2b50cbc5f3fa4d349a1f940f9987dc05ea083f610c84" + ], + [ + "src/universal_sports_engine/core/result_integrity.py", + "7834d5ca621d0f6a5e12062ef645e9bd133127c6417c0f2a4cfc2f66d7b1f536" + ], + [ + "src/universal_sports_engine/core/rng.py", + "77ef1313cb4e8e8ccffe4eca4f4fd6e79dd99f4558dbc0f559526dc7fa3c1c6c" + ], + [ + "src/universal_sports_engine/core/rules.py", + "969692c3ba895b11532252962d02cc3c44eeaac99569f8300ff5f60ea0d66db1" + ], + [ + "src/universal_sports_engine/core/types.py", + "599b884a78761ff20ebfddd997dfba5b221fc0130192c6472d3b2e72bbe16716" + ], + [ + "src/universal_sports_engine/dummy_bridge/__init__.py", + "013d0c67eb3017dd3da60bb689563d1e769da1c04183ce5b4f2faa6101a3752d" + ], + [ + "src/universal_sports_engine/dummy_bridge/contracts.py", + "18b7c319e33dcfeaface05a3edf90758c415bfc79b3a3e8d10cd85620cd230be" + ], + [ + "src/universal_sports_engine/league_packs/__init__.py", + "a285351dc558ac2bfa53458e108b21e038bfe7eae17462becf3db72b6e28fe68" + ], + [ + "src/universal_sports_engine/league_packs/baseball.py", + "3001fadb8ab6d7019fbd4bc0ec497d1f4ce707b77f37c56cbf7c6465e1611590" + ], + [ + "src/universal_sports_engine/league_packs/basketball.py", + "9ed6c6f6fe78fc07760a9d4aeea2809d60a4b60490ee642d0c326bccacbbe020" + ], + [ + "src/universal_sports_engine/league_packs/common.py", + "1da6b20287e6b3941c75779fd239dfb4d4fbcfdc72285e2e2545c0bf3e32d53d" + ], + [ + "src/universal_sports_engine/league_packs/football.py", + "5dc3f6722207809170486c7e048bd6b26b1c8e2ded356552cc79c65796dd362c" + ], + [ + "src/universal_sports_engine/league_packs/hockey.py", + "3099637237c10155ee9c2c16bddf2ffef6896945714e5021432f1abf1de1306b" + ], + [ + "src/universal_sports_engine/league_packs/registry.py", + "1929f573e3d534f692377c3135456557c9a2dc58920d70c6bde079dd8ec8ae17" + ], + [ + "src/universal_sports_engine/league_packs/rule_packs.py", + "c6942d5094495273f34150f17488edf25d834b52b7a83aa86caf7bba60864826" + ], + [ + "src/universal_sports_engine/reference.py", + "e6db7aa5a442dc13297cb95278a7b5436176d71ba77152440e192835956ae77e" + ], + [ + "src/universal_sports_engine/season.py", + "39c154a839933501bffd6cd89f4017c97df1d7f5acae02236455bec3b9390bf6" + ], + [ + "src/universal_sports_engine/simulation.py", + "7b4809acd99aa9c8737114500b2985504bb9d12feb588b2e570e431d45097b0e" + ], + [ + "src/universal_sports_engine/validation.py", + "7a29d749f200d8c59090e2a22e337a0fd78f2ec0f1a4a022320d194ec4538992" + ], + [ + "tests/test_accuracy.py", + "ab9d80d164d0de9bdd7642516ffbae0cd6e49e2345425601b7fc6e43ddb9ab62" + ], + [ + "tests/test_analytical.py", + "2c5445b8cace24a8bfeda4f298d2a873f7a5f904ab0b44b5f2cf12446153b2c6" + ], + [ + "tests/test_cli.py", + "a12f40db8abac0d741c77722f31dddbb8dcced1be3e225856cd215075e91ae46" + ], + [ + "tests/test_core.py", + "e25832b7db56eed1aab79f0a306aff8fbf8d108f952246e8523ba2c238eba036" + ], + [ + "tests/test_dummy_bridge.py", + "203b903956ef780f47c1cc901bc946235fe8b46b2604f2bd2afc232a829e5cc4" + ], + [ + "tests/test_leagues.py", + "4e06ae778e8cfbcec390acccf00651c4e7c38b1d3dfc21e93c6bd15b018f4687" + ], + [ + "tests/test_recursive_improvement.py", + "7a067a82c06dba39a902ac92d37cf1e0336cb28359e7b6ef0acb6091370d9f32" + ], + [ + "tests/test_season.py", + "8820c1031c1045dd820ca4447edd95687b3a6a010b035fd03af216203114a068" + ], + [ + "tests/test_validation.py", + "c88db08d10885e5f3de1f7af4f811fa99be75b67cd76224f0a9342a27523b7ac" + ], + [ + ".gitattributes", + "1d66749d1ffae745779df1a44bd2404240ff260608514a29a39c4f07c41ec412" + ], + [ + ".gitignore", + "f6665575bf293d8dfa04b53f3169f5d47822953024ff30d92ca12e7285926c65" + ], + [ + "AGENTS.md", + "d7f62131fe617bad05e1fdee7fc5d689f3756d26b071126819f5476711f75897" + ], + [ + "ARCHITECTURE.md", + "6843f39e844690be01bf6719ae4634e9558e69336ff92ec067774c7d121862e7" + ], + [ + "AUTONOMOUS_IMPROVEMENT.md", + "b3b1bd902365962f8894e84b448372ad8fd693e04ec5220eba31bdc02a94112e" + ], + [ + "AUTONOMY_CONFIG.json", + "a0a9c26b6d30a298d0f24e453124eee9bef853ea775fbb293a7b6dc2a4f1d9dc" + ], + [ + "DATA_RIGHTS_AND_PROVENANCE.md", + "b90c2c5b8179241bb20c672e753629ce412ae970911efd58a1ccd239d6663ff3" + ], + [ + "DOMAIN_NEUTRAL_ADAPTATION_READINESS.md", + "9f6997e284a026ab7e631d804f01580026cd5dca1672abfed1c9b7e44dde4733" + ], + [ + "DOMAIN_NEUTRAL_KERNEL_SPEC.md", + "5b202d4b74981917969a2b630dc576f709efceb5c9b1f5386cae4ced8450633c" + ], + [ + "KNOWN_LIMITATIONS.md", + "cea85fbdb3fdff474c3ae7bc372c73786868d0af1011d61ebebaeb75149a35e5" + ], + [ + "LICENSE", + "4c6516c53530d4eeb6a4773660b7798caeff549b18549d2447a683b2f01487b3" + ], + [ + "PRIOR_ART_REGISTRY.md", + "aa0eeaf6e898ee3642a4ac4c28d70d0b2e5fdc4e74ae3fcb196256a7e519a301" + ], + [ + "pyproject.toml", + "f5024d21929be33b54fd178f9270cb1a205a0bd9bd646e842d5d9034e9300354" + ], + [ + "QUALITY_GATES.yaml", + "1c8cfd93d827e7fa15ee705886bac4f6e7283c03499a06fe42624f7315e659a3" + ], + [ + "README.md", + "9590baf3181a88e1216c99317ebda0f9a9bb6e3e04814013f83a457c977a4445" + ], + [ + "RESEARCH_CONSTITUTION.md", + "2bb9bc09e63e74374e8c1ef0fb773eae74e32d0f2f234666e70b26e1e5d89ddf" + ], + [ + "STATUS.md", + "bdfb72c6fa2732e02680cb9eb1d3f6812a203c35f74b4ae843d3af6a83ed1115" + ], + [ + "TECHNOLOGY_BAKEOFF.md", + "2b1803c5ac8dcbe3437ea507aa7fc2c926097e0aa28afeb39a6647a36ad105d1" + ] + ], + "source_manifest_sha256": "7de72acb29259942070b5d8c0bd1532e8c6a58a5a5ecce6cf8ef76634175e272" +} diff --git a/reports/REPRODUCTION_REPORT.md b/reports/REPRODUCTION_REPORT.md new file mode 100644 index 0000000..00f2478 --- /dev/null +++ b/reports/REPRODUCTION_REPORT.md @@ -0,0 +1,37 @@ +# Reproduction report + +## Result + +**PASS — clean version 0.3 package reproduction** + +The project was built as `universal_sports_engine-0.3.0-py3-none-any.whl`, +installed into the isolated `.repro-v3-venv`, and executed from the Windows +temporary directory without the editable development package. + +- Wheel SHA-256: `4A6C44A0382BF9065DCCE4CAAF333A91BCD8761E142C2AE6EDDCC990F01C9675` +- Imported package version: `0.3.0` +- F1 smoke simulation: NHL, seed 42. +- F0 analytical smoke simulation: NBA, seed 42. +- Seven-league champion bundle verification: passed. +- Seven-record promotion-ledger hash-chain verification: passed. +- Shadow-adapted NBA ensemble simulation: passed. +- Embedded incumbent rollback smoke test: passed in the development environment. + +## Commands + +```powershell +.\.venv\Scripts\python.exe -m pip wheel . --no-deps --wheel-dir dist +python -m venv .repro-v3-venv +.\.repro-v3-venv\Scripts\python.exe -m pip install --no-deps --force-reinstall ` + .\dist\universal_sports_engine-0.3.0-py3-none-any.whl +.\.repro-v3-venv\Scripts\sports-engine.exe verify-autonomy ` + --bundle reports\autonomy_v3_fixture\champion_bundle.json ` + --ledger reports\autonomy_v3_fixture\improvement_ledger.jsonl +.\.repro-v3-venv\Scripts\sports-engine.exe simulate-adaptive ` + --bundle reports\autonomy_v3_fixture\shadow_adapted_bundle.json ` + --league nba --seed 42 --home-strength 1.0 --away-strength 1.0 +``` + +This proves packaging, deterministic execution, adaptive checkpoint loading, +ledger integrity, and governed ensemble simulation. It does not constitute +independent real-world calibration. diff --git a/reports/STRESS_AUDIT_V3.json b/reports/STRESS_AUDIT_V3.json new file mode 100644 index 0000000..1fc942d --- /dev/null +++ b/reports/STRESS_AUDIT_V3.json @@ -0,0 +1,58 @@ +[ + { + "elapsed_seconds": 4.125197799992748, + "events": 173637, + "failures": 0, + "games": 2000, + "league": "mlb", + "maximum_events_per_game": 170 + }, + { + "elapsed_seconds": 8.791729899996426, + "events": 399470, + "failures": 0, + "games": 2000, + "league": "nba", + "maximum_events_per_game": 247 + }, + { + "elapsed_seconds": 7.10191549998126, + "events": 322910, + "failures": 0, + "games": 2000, + "league": "wnba", + "maximum_events_per_game": 213 + }, + { + "elapsed_seconds": 6.147122299997136, + "events": 279714, + "failures": 0, + "games": 2000, + "league": "ncaambb", + "maximum_events_per_game": 195 + }, + { + "elapsed_seconds": 6.622597000008682, + "events": 332517, + "failures": 0, + "games": 2000, + "league": "nfl", + "maximum_events_per_game": 300 + }, + { + "elapsed_seconds": 7.214139299991075, + "events": 361722, + "failures": 0, + "games": 2000, + "league": "ncaaf", + "maximum_events_per_game": 238 + }, + { + "elapsed_seconds": 2.8979713000007905, + "events": 133334, + "failures": 0, + "games": 2000, + "league": "nhl", + "maximum_events_per_game": 104 + } +] diff --git a/reports/VALIDATION_REPORT.json b/reports/VALIDATION_REPORT.json new file mode 100644 index 0000000..935f46d --- /dev/null +++ b/reports/VALIDATION_REPORT.json @@ -0,0 +1,107 @@ +[ + { + "anchor_away_mean": 4.3, + "anchor_home_mean": 4.55, + "anchor_total_sd": 4.6, + "games": 500, + "home_advantage_not_materially_reversed": true, + "home_advantage_z_score": 2.5791139686344238, + "league": "mlb", + "maximum_relative_error": 0.08615384615384623, + "observed_away_mean": 4.426, + "observed_home_mean": 4.942, + "observed_total_sd": 4.696442909266544, + "status": "provisional_reference_anchor", + "total_sd_relative_error": 0.020965849840553136 + }, + { + "anchor_away_mean": 113.0, + "anchor_home_mean": 116.0, + "anchor_total_sd": 20.0, + "games": 500, + "home_advantage_not_materially_reversed": true, + "home_advantage_z_score": 1.1787912709657586, + "league": "nba", + "maximum_relative_error": 0.024088495575221188, + "observed_away_mean": 115.722, + "observed_home_mean": 116.606, + "observed_total_sd": 19.943931808948808, + "status": "provisional_reference_anchor", + "total_sd_relative_error": 0.0028034095525596215 + }, + { + "anchor_away_mean": 80.0, + "anchor_home_mean": 82.0, + "anchor_total_sd": 17.5, + "games": 500, + "home_advantage_not_materially_reversed": true, + "home_advantage_z_score": 1.6769504546694531, + "league": "wnba", + "maximum_relative_error": 0.03870000000000005, + "observed_away_mean": 83.096, + "observed_home_mean": 84.146, + "observed_total_sd": 17.492153555237273, + "status": "provisional_reference_anchor", + "total_sd_relative_error": 0.0004483682721558223 + }, + { + "anchor_away_mean": 72.0, + "anchor_home_mean": 75.0, + "anchor_total_sd": 16.5, + "games": 500, + "home_advantage_not_materially_reversed": true, + "home_advantage_z_score": 3.4668051528335053, + "league": "ncaambb", + "maximum_relative_error": 0.07791999999999992, + "observed_away_mean": 67.136, + "observed_home_mean": 69.156, + "observed_total_sd": 16.456571210309882, + "status": "provisional_reference_anchor", + "total_sd_relative_error": 0.002632047860007164 + }, + { + "anchor_away_mean": 21.8, + "anchor_home_mean": 23.5, + "anchor_total_sd": 13.7, + "games": 500, + "home_advantage_not_materially_reversed": true, + "home_advantage_z_score": 2.259980386724432, + "league": "nfl", + "maximum_relative_error": 0.12385321100917428, + "observed_away_mean": 24.5, + "observed_home_mean": 25.882, + "observed_total_sd": 14.002859565103122, + "status": "provisional_reference_anchor", + "total_sd_relative_error": 0.022106537598768117 + }, + { + "anchor_away_mean": 26.5, + "anchor_home_mean": 29.0, + "anchor_total_sd": 15.7, + "games": 500, + "home_advantage_not_materially_reversed": true, + "home_advantage_z_score": 5.464024826085687, + "league": "ncaaf", + "maximum_relative_error": 0.1536551724137932, + "observed_away_mean": 29.822, + "observed_home_mean": 33.456, + "observed_total_sd": 14.809750706882273, + "status": "provisional_reference_anchor", + "total_sd_relative_error": 0.05670377663170232 + }, + { + "anchor_away_mean": 2.95, + "anchor_home_mean": 3.2, + "anchor_total_sd": 2.3, + "games": 500, + "home_advantage_not_materially_reversed": true, + "home_advantage_z_score": 1.7722479332139913, + "league": "nhl", + "maximum_relative_error": 0.023050847457626988, + "observed_away_mean": 3.018, + "observed_home_mean": 3.204, + "observed_total_sd": 2.3144580359125113, + "status": "provisional_reference_anchor", + "total_sd_relative_error": 0.006286102570657171 + } +] diff --git a/reports/autonomy_v3_fixture/champion_bundle.json b/reports/autonomy_v3_fixture/champion_bundle.json new file mode 100644 index 0000000..41dcdde --- /dev/null +++ b/reports/autonomy_v3_fixture/champion_bundle.json @@ -0,0 +1,937 @@ +{ + "bundle_hash": "56c5551d36093708f6da5ad0165f968a12c69c7ede7216959f7a4d532a40d754", + "champions": [ + { + "ensemble": { + "ensemble_hash": "bb0b6a4184ebbf8489abaf3dc3df077d280c14d888340b59c2871215f6df8360", + "evidence_status": "provisional_reference_anchor", + "league": "mlb", + "profiles": [ + { + "away_strength_exponent": 1.0, + "base_away_score": 4.3, + "base_home_score": 4.55, + "evidence_status": "provisional_reference_anchor", + "generation": 0, + "home_strength_exponent": 1.0, + "league": "mlb", + "margin_sd": 4.4, + "parent_hash": "", + "profile_hash": "ed4a36ddf530bc1e400bae06859869e1b8b9bf4c542f53a193c39d949638d53e", + "total_score_sd": 4.6 + } + ], + "weights": [ + 1.0 + ] + }, + "league": "mlb", + "promoted": false, + "rollback_ensemble": { + "ensemble_hash": "bb0b6a4184ebbf8489abaf3dc3df077d280c14d888340b59c2871215f6df8360", + "evidence_status": "provisional_reference_anchor", + "league": "mlb", + "profiles": [ + { + "away_strength_exponent": 1.0, + "base_away_score": 4.3, + "base_home_score": 4.55, + "evidence_status": "provisional_reference_anchor", + "generation": 0, + "home_strength_exponent": 1.0, + "league": "mlb", + "margin_sd": 4.4, + "parent_hash": "", + "profile_hash": "ed4a36ddf530bc1e400bae06859869e1b8b9bf4c542f53a193c39d949638d53e", + "total_score_sd": 4.6 + } + ], + "weights": [ + 1.0 + ] + }, + "rollback_state": { + "drift_score": 0.050688478956998094, + "drift_threshold": 0.12, + "ensemble_hash": "bb0b6a4184ebbf8489abaf3dc3df077d280c14d888340b59c2871215f6df8360", + "interval_learning_rate": 0.04, + "interval_scale": 0.8869204367171584, + "learning_rate": 0.25, + "long_decay": 0.03, + "long_loss_ewma": 5.59425099372921, + "minimum_drift_observations": 40, + "observations": 50, + "short_decay": 0.2, + "short_loss_ewma": 5.8778150675050185, + "state_hash": "a23ac9f2ccc36db8368dcd341b0025f12d6d2b188c95ca9013b1dc61b461e78a", + "status": "healthy", + "target_coverage": 0.9, + "weights": [ + 1.0 + ] + }, + "state": { + "drift_score": 0.050688478956998094, + "drift_threshold": 0.12, + "ensemble_hash": "bb0b6a4184ebbf8489abaf3dc3df077d280c14d888340b59c2871215f6df8360", + "interval_learning_rate": 0.04, + "interval_scale": 0.8869204367171584, + "learning_rate": 0.25, + "long_decay": 0.03, + "long_loss_ewma": 5.59425099372921, + "minimum_drift_observations": 40, + "observations": 50, + "short_decay": 0.2, + "short_loss_ewma": 5.8778150675050185, + "state_hash": "a23ac9f2ccc36db8368dcd341b0025f12d6d2b188c95ca9013b1dc61b461e78a", + "status": "healthy", + "target_coverage": 0.9, + "weights": [ + 1.0 + ] + } + }, + { + "ensemble": { + "ensemble_hash": "60e7da6020d67de678c0d044f300847fde90bf16161917510b08c83bfd5dbe96", + "evidence_status": "chronological_calibration_challenger", + "league": "nba", + "profiles": [ + { + "away_strength_exponent": 1.0, + "base_away_score": 113.0, + "base_home_score": 116.0, + "evidence_status": "provisional_reference_anchor", + "generation": 0, + "home_strength_exponent": 1.0, + "league": "nba", + "margin_sd": 15.0, + "parent_hash": "", + "profile_hash": "1fd107dd93cec7f199c4979e7334f2ad2c7e7d61e6e5d3fa7a7c6741138e40d1", + "total_score_sd": 20.0 + }, + { + "away_strength_exponent": 0.9632460782428364, + "base_away_score": 100.41120048415034, + "base_home_score": 101.40842755264546, + "evidence_status": "train_search_candidate", + "generation": 8, + "home_strength_exponent": 1.1035181164801218, + "league": "nba", + "margin_sd": 13.736474631089353, + "parent_hash": "d4336fc445a68de29aec9f851052ee18e00a6c18e7198fda34d832e4a727eafd", + "profile_hash": "6dabbdf2c5066b2dc80eb6e6088fbe21f308472a7241c3d76a36ea7ae27b0dc9", + "total_score_sd": 15.530808786290953 + }, + { + "away_strength_exponent": 0.9299864332685756, + "base_away_score": 99.12089973217256, + "base_home_score": 102.60460634910284, + "evidence_status": "train_search_candidate", + "generation": 7, + "home_strength_exponent": 1.0374456736407256, + "league": "nba", + "margin_sd": 14.476747331295748, + "parent_hash": "d4336fc445a68de29aec9f851052ee18e00a6c18e7198fda34d832e4a727eafd", + "profile_hash": "2c3b39a22523f4f245dcd1afe8fcfc54544020073f6153bd180207f50ca0fa67", + "total_score_sd": 15.986652973245818 + }, + { + "away_strength_exponent": 0.9314639467993735, + "base_away_score": 97.55462255575519, + "base_home_score": 100.60692741111606, + "evidence_status": "train_search_candidate", + "generation": 6, + "home_strength_exponent": 1.0481985521298671, + "league": "nba", + "margin_sd": 13.538006034660736, + "parent_hash": "1f29dc288a020ac6049537b27f5d91603e2e548bb8639e20fcd8c9527fa9b748", + "profile_hash": "d4336fc445a68de29aec9f851052ee18e00a6c18e7198fda34d832e4a727eafd", + "total_score_sd": 16.76275018651739 + }, + { + "away_strength_exponent": 0.8862112258764434, + "base_away_score": 100.34400852330234, + "base_home_score": 99.90111885665915, + "evidence_status": "train_search_candidate", + "generation": 7, + "home_strength_exponent": 0.9978232275893777, + "league": "nba", + "margin_sd": 13.709770977740531, + "parent_hash": "4cc9fa6c37e99b992fc73dec931c77bbd815b6a14538deb5d0471d5dfe18a3b3", + "profile_hash": "2b9ae85a9ff77cddc6e95f95feff238a5aad863eb1b5a6d4d0c1682859f773cf", + "total_score_sd": 14.728999472122727 + }, + { + "away_strength_exponent": 0.9168426579759286, + "base_away_score": 98.08693111402029, + "base_home_score": 100.24510182059633, + "evidence_status": "train_search_candidate", + "generation": 8, + "home_strength_exponent": 1.029985233214439, + "league": "nba", + "margin_sd": 13.914503225184157, + "parent_hash": "2c3b39a22523f4f245dcd1afe8fcfc54544020073f6153bd180207f50ca0fa67", + "profile_hash": "c24f67abab0bf80575320bbbed67381f38b4de2aeee0cfe291bee9e80ceba669", + "total_score_sd": 16.713986694619834 + } + ], + "weights": [ + 0.12893045230055183, + 0.17538891395996567, + 0.1747196187644406, + 0.17386258202490054, + 0.17362605222135616, + 0.17347238072878526 + ] + }, + "league": "nba", + "promoted": true, + "rollback_ensemble": { + "ensemble_hash": "938868d720f17e6b997358c0a52254af174a1b3081193720225c3ca7e3eda934", + "evidence_status": "provisional_reference_anchor", + "league": "nba", + "profiles": [ + { + "away_strength_exponent": 1.0, + "base_away_score": 113.0, + "base_home_score": 116.0, + "evidence_status": "provisional_reference_anchor", + "generation": 0, + "home_strength_exponent": 1.0, + "league": "nba", + "margin_sd": 15.0, + "parent_hash": "", + "profile_hash": "1fd107dd93cec7f199c4979e7334f2ad2c7e7d61e6e5d3fa7a7c6741138e40d1", + "total_score_sd": 20.0 + } + ], + "weights": [ + 1.0 + ] + }, + "rollback_state": { + "drift_score": 0.0, + "drift_threshold": 0.12, + "ensemble_hash": "938868d720f17e6b997358c0a52254af174a1b3081193720225c3ca7e3eda934", + "interval_learning_rate": 0.04, + "interval_scale": 1.1502737988572285, + "learning_rate": 0.25, + "long_decay": 0.03, + "long_loss_ewma": 9.584379101566844, + "minimum_drift_observations": 40, + "observations": 50, + "short_decay": 0.2, + "short_loss_ewma": 9.00479793127273, + "state_hash": "8938e17f435856b6ce780263706e625583f33790ccd311fee85a1564aa1988dd", + "status": "healthy", + "target_coverage": 0.9, + "weights": [ + 1.0 + ] + }, + "state": { + "drift_score": 0.0, + "drift_threshold": 0.12, + "ensemble_hash": "60e7da6020d67de678c0d044f300847fde90bf16161917510b08c83bfd5dbe96", + "interval_learning_rate": 0.04, + "interval_scale": 0.9231163463866361, + "learning_rate": 0.25, + "long_decay": 0.03, + "long_loss_ewma": 8.475867039772714, + "minimum_drift_observations": 40, + "observations": 50, + "short_decay": 0.2, + "short_loss_ewma": 8.262049524590857, + "state_hash": "fe671acbd9f09da2f1984ffa82fa65ab28b4110cb59498739477866e598f119d", + "status": "healthy", + "target_coverage": 0.9, + "weights": [ + 5.513615121909581e-07, + 0.43303118967699145, + 0.16262743922906853, + 0.13862691445333053, + 0.14035629144863276, + 0.1253576138304646 + ] + } + }, + { + "ensemble": { + "ensemble_hash": "110e0a2b1d912b65c9c8e2421251514d29e036503be2191a6a19c5418d16b42a", + "evidence_status": "provisional_reference_anchor", + "league": "ncaaf", + "profiles": [ + { + "away_strength_exponent": 1.0, + "base_away_score": 26.5, + "base_home_score": 29.0, + "evidence_status": "provisional_reference_anchor", + "generation": 0, + "home_strength_exponent": 1.0, + "league": "ncaaf", + "margin_sd": 18.0, + "parent_hash": "", + "profile_hash": "6c3ad3401f75976c8a0cc1301c7aa9d9ba28a4d78b9bd5ee2432bf29e58493f7", + "total_score_sd": 15.7 + } + ], + "weights": [ + 1.0 + ] + }, + "league": "ncaaf", + "promoted": false, + "rollback_ensemble": { + "ensemble_hash": "110e0a2b1d912b65c9c8e2421251514d29e036503be2191a6a19c5418d16b42a", + "evidence_status": "provisional_reference_anchor", + "league": "ncaaf", + "profiles": [ + { + "away_strength_exponent": 1.0, + "base_away_score": 26.5, + "base_home_score": 29.0, + "evidence_status": "provisional_reference_anchor", + "generation": 0, + "home_strength_exponent": 1.0, + "league": "ncaaf", + "margin_sd": 18.0, + "parent_hash": "", + "profile_hash": "6c3ad3401f75976c8a0cc1301c7aa9d9ba28a4d78b9bd5ee2432bf29e58493f7", + "total_score_sd": 15.7 + } + ], + "weights": [ + 1.0 + ] + }, + "rollback_state": { + "drift_score": 0.053373359494810986, + "drift_threshold": 0.12, + "ensemble_hash": "110e0a2b1d912b65c9c8e2421251514d29e036503be2191a6a19c5418d16b42a", + "interval_learning_rate": 0.04, + "interval_scale": 0.9607894391523236, + "learning_rate": 0.25, + "long_decay": 0.03, + "long_loss_ewma": 8.821992883967974, + "minimum_drift_observations": 40, + "observations": 50, + "short_decay": 0.2, + "short_loss_ewma": 9.292852281624661, + "state_hash": "6490ed8bb46c72d47e410317f52bba03128f9076dbe23088cbd0748735b8b91b", + "status": "healthy", + "target_coverage": 0.9, + "weights": [ + 1.0 + ] + }, + "state": { + "drift_score": 0.053373359494810986, + "drift_threshold": 0.12, + "ensemble_hash": "110e0a2b1d912b65c9c8e2421251514d29e036503be2191a6a19c5418d16b42a", + "interval_learning_rate": 0.04, + "interval_scale": 0.9607894391523236, + "learning_rate": 0.25, + "long_decay": 0.03, + "long_loss_ewma": 8.821992883967974, + "minimum_drift_observations": 40, + "observations": 50, + "short_decay": 0.2, + "short_loss_ewma": 9.292852281624661, + "state_hash": "6490ed8bb46c72d47e410317f52bba03128f9076dbe23088cbd0748735b8b91b", + "status": "healthy", + "target_coverage": 0.9, + "weights": [ + 1.0 + ] + } + }, + { + "ensemble": { + "ensemble_hash": "3a26b1869b41e662ecdfe044a54229c6e5998280f88a6ad5003326302350654e", + "evidence_status": "chronological_calibration_challenger", + "league": "ncaambb", + "profiles": [ + { + "away_strength_exponent": 1.0, + "base_away_score": 72.0, + "base_home_score": 75.0, + "evidence_status": "provisional_reference_anchor", + "generation": 0, + "home_strength_exponent": 1.0, + "league": "ncaambb", + "margin_sd": 14.0, + "parent_hash": "", + "profile_hash": "14be1ec209dc6acf4030c480c7c2c6abf250986580e5da75595419c8fd8028df", + "total_score_sd": 16.5 + }, + { + "away_strength_exponent": 0.9380995927689169, + "base_away_score": 64.26338685281874, + "base_home_score": 67.90736863209588, + "evidence_status": "train_search_candidate", + "generation": 8, + "home_strength_exponent": 1.1198826403419602, + "league": "ncaambb", + "margin_sd": 12.337242613541385, + "parent_hash": "d908371efab1b1058ff372457736decc2f22f9d69723d762431e794c4f4582e3", + "profile_hash": "05bcc37afeab1ab4bf15ea01c88cd88d9e19e259e85a843ba6904dc94e5285ee", + "total_score_sd": 11.844982854797419 + }, + { + "away_strength_exponent": 0.9839084920688369, + "base_away_score": 64.17862932003789, + "base_home_score": 66.40814600980535, + "evidence_status": "train_search_candidate", + "generation": 7, + "home_strength_exponent": 1.1265354070594922, + "league": "ncaambb", + "margin_sd": 11.751801481101191, + "parent_hash": "5052bec2704f4278b8055686fdfd6fdaca864456ff8480e6bd1c3a16ff8cd5c2", + "profile_hash": "d908371efab1b1058ff372457736decc2f22f9d69723d762431e794c4f4582e3", + "total_score_sd": 12.527208607031197 + }, + { + "away_strength_exponent": 1.0302789332906102, + "base_away_score": 64.56603709857632, + "base_home_score": 68.11959848451264, + "evidence_status": "train_search_candidate", + "generation": 8, + "home_strength_exponent": 1.15845619003206, + "league": "ncaambb", + "margin_sd": 12.172650135194955, + "parent_hash": "d908371efab1b1058ff372457736decc2f22f9d69723d762431e794c4f4582e3", + "profile_hash": "a3426257638d3914687ea87b35c7948a9df8423501f8509cc563d4635f364bad", + "total_score_sd": 12.250367245233734 + }, + { + "away_strength_exponent": 0.9757805013766029, + "base_away_score": 64.69153447008274, + "base_home_score": 67.3019953859376, + "evidence_status": "train_search_candidate", + "generation": 6, + "home_strength_exponent": 1.0463799843916333, + "league": "ncaambb", + "margin_sd": 12.360366882633027, + "parent_hash": "035fdaf6642f08d5f653660bf422cf514301df099700740c1740ff66f014aa95", + "profile_hash": "5052bec2704f4278b8055686fdfd6fdaca864456ff8480e6bd1c3a16ff8cd5c2", + "total_score_sd": 13.12846794130971 + }, + { + "away_strength_exponent": 1.0525771945812439, + "base_away_score": 65.56062542041047, + "base_home_score": 67.05886346685132, + "evidence_status": "train_search_candidate", + "generation": 8, + "home_strength_exponent": 1.0249276075178801, + "league": "ncaambb", + "margin_sd": 11.814617284868469, + "parent_hash": "5052bec2704f4278b8055686fdfd6fdaca864456ff8480e6bd1c3a16ff8cd5c2", + "profile_hash": "d5abde343917aae55c5b655aed7e5ce5542a8d6e94ebb6cc5b1dff7aef5b914b", + "total_score_sd": 12.842103349942267 + } + ], + "weights": [ + 0.15204731462607787, + 0.17053030010320389, + 0.17008865902417303, + 0.1698776799805041, + 0.16875167581778863, + 0.16870437044825248 + ] + }, + "league": "ncaambb", + "promoted": true, + "rollback_ensemble": { + "ensemble_hash": "f4a907f61caeed291d4ccf147414e9c0e97aaef2acbf4a47d08961d80fb12539", + "evidence_status": "provisional_reference_anchor", + "league": "ncaambb", + "profiles": [ + { + "away_strength_exponent": 1.0, + "base_away_score": 72.0, + "base_home_score": 75.0, + "evidence_status": "provisional_reference_anchor", + "generation": 0, + "home_strength_exponent": 1.0, + "league": "ncaambb", + "margin_sd": 14.0, + "parent_hash": "", + "profile_hash": "14be1ec209dc6acf4030c480c7c2c6abf250986580e5da75595419c8fd8028df", + "total_score_sd": 16.5 + } + ], + "weights": [ + 1.0 + ] + }, + "rollback_state": { + "drift_score": 0.0, + "drift_threshold": 0.12, + "ensemble_hash": "f4a907f61caeed291d4ccf147414e9c0e97aaef2acbf4a47d08961d80fb12539", + "interval_learning_rate": 0.04, + "interval_scale": 1.0000000000000013, + "learning_rate": 0.25, + "long_decay": 0.03, + "long_loss_ewma": 8.386494742786283, + "minimum_drift_observations": 40, + "observations": 50, + "short_decay": 0.2, + "short_loss_ewma": 8.246116288876184, + "state_hash": "b00bf20e38b236488432ebaaa42691474cd68631fd141127185217573f05d4f2", + "status": "healthy", + "target_coverage": 0.9, + "weights": [ + 1.0 + ] + }, + "state": { + "drift_score": 0.0, + "drift_threshold": 0.12, + "ensemble_hash": "3a26b1869b41e662ecdfe044a54229c6e5998280f88a6ad5003326302350654e", + "interval_learning_rate": 0.04, + "interval_scale": 0.9607894391523251, + "learning_rate": 0.25, + "long_decay": 0.03, + "long_loss_ewma": 8.174282931387665, + "minimum_drift_observations": 40, + "observations": 50, + "short_decay": 0.2, + "short_loss_ewma": 8.008633532956152, + "state_hash": "924515d3adfcde4094160bbe9a1c1741be83320aad2c0d1e322f724c309fcb9d", + "status": "healthy", + "target_coverage": 0.9, + "weights": [ + 0.001155014984004925, + 0.19050882700932764, + 0.19279742459698684, + 0.14440496188814017, + 0.26188724112414913, + 0.20924653039739127 + ] + } + }, + { + "ensemble": { + "ensemble_hash": "52cc748b0f03d805bc32644c746152d71b8878f6cb3e9e2e6df533e3cb3718e2", + "evidence_status": "provisional_reference_anchor", + "league": "nfl", + "profiles": [ + { + "away_strength_exponent": 1.0, + "base_away_score": 21.8, + "base_home_score": 23.5, + "evidence_status": "provisional_reference_anchor", + "generation": 0, + "home_strength_exponent": 1.0, + "league": "nfl", + "margin_sd": 14.0, + "parent_hash": "", + "profile_hash": "2cf12a1c59550d09aad4853366ee45da48461195286aba2fc665167a2d0accd0", + "total_score_sd": 13.7 + } + ], + "weights": [ + 1.0 + ] + }, + "league": "nfl", + "promoted": false, + "rollback_ensemble": { + "ensemble_hash": "52cc748b0f03d805bc32644c746152d71b8878f6cb3e9e2e6df533e3cb3718e2", + "evidence_status": "provisional_reference_anchor", + "league": "nfl", + "profiles": [ + { + "away_strength_exponent": 1.0, + "base_away_score": 21.8, + "base_home_score": 23.5, + "evidence_status": "provisional_reference_anchor", + "generation": 0, + "home_strength_exponent": 1.0, + "league": "nfl", + "margin_sd": 14.0, + "parent_hash": "", + "profile_hash": "2cf12a1c59550d09aad4853366ee45da48461195286aba2fc665167a2d0accd0", + "total_score_sd": 13.7 + } + ], + "weights": [ + 1.0 + ] + }, + "rollback_state": { + "drift_score": 0.009386081910553939, + "drift_threshold": 0.12, + "ensemble_hash": "52cc748b0f03d805bc32644c746152d71b8878f6cb3e9e2e6df533e3cb3718e2", + "interval_learning_rate": 0.04, + "interval_scale": 0.923116346386637, + "learning_rate": 0.25, + "long_decay": 0.03, + "long_loss_ewma": 8.229562938300864, + "minimum_drift_observations": 40, + "observations": 50, + "short_decay": 0.2, + "short_loss_ewma": 8.306806290127815, + "state_hash": "c56bdf6245fc5a57c735db00e6d448c6670f5a96632121cbfa3e9d7e30dd0e91", + "status": "healthy", + "target_coverage": 0.9, + "weights": [ + 1.0 + ] + }, + "state": { + "drift_score": 0.009386081910553939, + "drift_threshold": 0.12, + "ensemble_hash": "52cc748b0f03d805bc32644c746152d71b8878f6cb3e9e2e6df533e3cb3718e2", + "interval_learning_rate": 0.04, + "interval_scale": 0.923116346386637, + "learning_rate": 0.25, + "long_decay": 0.03, + "long_loss_ewma": 8.229562938300864, + "minimum_drift_observations": 40, + "observations": 50, + "short_decay": 0.2, + "short_loss_ewma": 8.306806290127815, + "state_hash": "c56bdf6245fc5a57c735db00e6d448c6670f5a96632121cbfa3e9d7e30dd0e91", + "status": "healthy", + "target_coverage": 0.9, + "weights": [ + 1.0 + ] + } + }, + { + "ensemble": { + "ensemble_hash": "b4268f4251d62981ee852625c817f48947e9e12aecc39da1b77d4ff869c86289", + "evidence_status": "chronological_calibration_challenger", + "league": "nhl", + "profiles": [ + { + "away_strength_exponent": 1.0, + "base_away_score": 2.95, + "base_home_score": 3.2, + "evidence_status": "provisional_reference_anchor", + "generation": 0, + "home_strength_exponent": 1.0, + "league": "nhl", + "margin_sd": 2.4, + "parent_hash": "", + "profile_hash": "dd2f407da7fec8a4017fec2f6e311c3010d4f874c1d156f9dc4a4d9ba25576c3", + "total_score_sd": 2.3 + }, + { + "away_strength_exponent": 1.3618435036484602, + "base_away_score": 2.8140096579368463, + "base_home_score": 2.9827709409299437, + "evidence_status": "train_search_candidate", + "generation": 6, + "home_strength_exponent": 1.0164249111888564, + "league": "nhl", + "margin_sd": 2.4067107458645474, + "parent_hash": "c0f59c1041dfffbcb83db90b24931ee0acf8315cd86d375cd903b1869716224f", + "profile_hash": "560b8c3bf885c09aba0c04204376e3091a28ca9006785a1e982d822fd4f20158", + "total_score_sd": 1.9217750431804201 + }, + { + "away_strength_exponent": 1.3918430671368498, + "base_away_score": 2.8395546024633056, + "base_home_score": 3.001160815782975, + "evidence_status": "train_search_candidate", + "generation": 8, + "home_strength_exponent": 0.9742513551643636, + "league": "nhl", + "margin_sd": 2.347437453084234, + "parent_hash": "7e6cc68f9f179e60b9ad230a27f90dbac843e9dfc33f83849a71a51a37077069", + "profile_hash": "b792aed5100d5cfef059e56fe733dc686f999130857572ef57702f98220e5374", + "total_score_sd": 1.9848807761955292 + }, + { + "away_strength_exponent": 1.3675649819385032, + "base_away_score": 2.67379397546478, + "base_home_score": 3.2244694632163426, + "evidence_status": "train_search_candidate", + "generation": 5, + "home_strength_exponent": 0.9979113276154296, + "league": "nhl", + "margin_sd": 2.315479052327515, + "parent_hash": "3e88d5bd7565e8748490af5a78d84b19a8534868a6331d4d1752f34943540487", + "profile_hash": "c0f59c1041dfffbcb83db90b24931ee0acf8315cd86d375cd903b1869716224f", + "total_score_sd": 1.8514937320015388 + }, + { + "away_strength_exponent": 1.365373323994108, + "base_away_score": 2.6479224044804157, + "base_home_score": 3.0420403168143237, + "evidence_status": "train_search_candidate", + "generation": 5, + "home_strength_exponent": 0.8190424527874777, + "league": "nhl", + "margin_sd": 2.469930832788603, + "parent_hash": "e20a271760ee2ecb08354c7be8fa7b0533bc22ccf66bb8691f131706d6d4193d", + "profile_hash": "16e8f92eddd415582a8ca2282946b318fe60e8845d896d93538b603bc8cd0555", + "total_score_sd": 1.754941135672797 + }, + { + "away_strength_exponent": 1.444774898245588, + "base_away_score": 2.8087224671677236, + "base_home_score": 2.900908465085074, + "evidence_status": "train_search_candidate", + "generation": 8, + "home_strength_exponent": 0.9764064324855087, + "league": "nhl", + "margin_sd": 2.4088026880950175, + "parent_hash": "560b8c3bf885c09aba0c04204376e3091a28ca9006785a1e982d822fd4f20158", + "profile_hash": "cbd5bb21fef457140e843a9455f9d3d073749dc621ad2bef307a40f02fe73c53", + "total_score_sd": 1.8330168778160287 + } + ], + "weights": [ + 0.16286673514032046, + 0.16755705994655176, + 0.16752559730854746, + 0.1673849214822694, + 0.16737507931387444, + 0.1672906068084365 + ] + }, + "league": "nhl", + "promoted": true, + "rollback_ensemble": { + "ensemble_hash": "382ec0d5185a96f540f91839c8bd6328d408d70a8ec1f16352aa22553c05bcfb", + "evidence_status": "provisional_reference_anchor", + "league": "nhl", + "profiles": [ + { + "away_strength_exponent": 1.0, + "base_away_score": 2.95, + "base_home_score": 3.2, + "evidence_status": "provisional_reference_anchor", + "generation": 0, + "home_strength_exponent": 1.0, + "league": "nhl", + "margin_sd": 2.4, + "parent_hash": "", + "profile_hash": "dd2f407da7fec8a4017fec2f6e311c3010d4f874c1d156f9dc4a4d9ba25576c3", + "total_score_sd": 2.3 + } + ], + "weights": [ + 1.0 + ] + }, + "rollback_state": { + "drift_score": 0.0, + "drift_threshold": 0.12, + "ensemble_hash": "382ec0d5185a96f540f91839c8bd6328d408d70a8ec1f16352aa22553c05bcfb", + "interval_learning_rate": 0.04, + "interval_scale": 0.9801986733067568, + "learning_rate": 0.25, + "long_decay": 0.03, + "long_loss_ewma": 4.723961751695468, + "minimum_drift_observations": 40, + "observations": 50, + "short_decay": 0.2, + "short_loss_ewma": 4.6298625684220225, + "state_hash": "7ce259e0d6f333426389c555b9f568c802b7265ae0ac4a6a55ba9f98184c8ee5", + "status": "healthy", + "target_coverage": 0.9, + "weights": [ + 1.0 + ] + }, + "state": { + "drift_score": 0.0, + "drift_threshold": 0.12, + "ensemble_hash": "b4268f4251d62981ee852625c817f48947e9e12aecc39da1b77d4ff869c86289", + "interval_learning_rate": 0.04, + "interval_scale": 1.0000000000000007, + "learning_rate": 0.25, + "long_decay": 0.03, + "long_loss_ewma": 4.686936849303858, + "minimum_drift_observations": 40, + "observations": 50, + "short_decay": 0.2, + "short_loss_ewma": 4.605010305685448, + "state_hash": "9e103f41a8cf8181c03bde20e2b260ff7daa3ca261c7c760e94c10dca68cc368", + "status": "healthy", + "target_coverage": 0.9, + "weights": [ + 0.10302384802689585, + 0.18136314556312605, + 0.15845422350790386, + 0.20162059073033012, + 0.19463378905316608, + 0.16090440311857798 + ] + } + }, + { + "ensemble": { + "ensemble_hash": "369bd1fab70c4fe9c443214b61e5bb1297947388fae8402061d599b6dc5a975d", + "evidence_status": "chronological_calibration_challenger", + "league": "wnba", + "profiles": [ + { + "away_strength_exponent": 1.0, + "base_away_score": 80.0, + "base_home_score": 82.0, + "evidence_status": "provisional_reference_anchor", + "generation": 0, + "home_strength_exponent": 1.0, + "league": "wnba", + "margin_sd": 14.0, + "parent_hash": "", + "profile_hash": "a9efcb5f3219e8e9a3df4fdce3b3f767165faa82fe41cf874bdb0716548bb4d9", + "total_score_sd": 17.5 + }, + { + "away_strength_exponent": 1.0251595474377284, + "base_away_score": 72.11403431035531, + "base_home_score": 71.69042161368746, + "evidence_status": "train_search_candidate", + "generation": 7, + "home_strength_exponent": 1.0938685005129976, + "league": "wnba", + "margin_sd": 12.553668307327136, + "parent_hash": "468447632d3bbfb0286c4335ad436b5f32c841251c8614e5f2096d0b5dbf0a3f", + "profile_hash": "af6c47cb9cc880f80388c052e42e219dc4055b7d2c8b6e1346332703b49299e5", + "total_score_sd": 13.255930520302046 + }, + { + "away_strength_exponent": 1.0981336628870195, + "base_away_score": 71.11342291633342, + "base_home_score": 73.62693210352198, + "evidence_status": "train_search_candidate", + "generation": 7, + "home_strength_exponent": 1.1420227255585949, + "league": "wnba", + "margin_sd": 12.523765694931425, + "parent_hash": "7d2a641dbd76ac6fde3dfa526718e79569f129327958d60c3dfafc464b75af97", + "profile_hash": "b83f84e526bc77753ff0561968a83ca3f84f62dad23a54b08c2869b88226c810", + "total_score_sd": 13.124434195991878 + }, + { + "away_strength_exponent": 0.9827239039722672, + "base_away_score": 73.07586506043492, + "base_home_score": 73.49995936972549, + "evidence_status": "train_search_candidate", + "generation": 7, + "home_strength_exponent": 1.060463397601755, + "league": "wnba", + "margin_sd": 11.322075003472191, + "parent_hash": "02a8002ff7041946836ce1a1fcb8f53773f3b838801f82797911d7fbf267efec", + "profile_hash": "6b7ed5b7bfddee7d6ec43ca6649a0fd953a0954c5a818b269d146c55b54ea0e2", + "total_score_sd": 13.178399018766651 + }, + { + "away_strength_exponent": 0.9391411924229954, + "base_away_score": 73.82314278504273, + "base_home_score": 72.09561563722342, + "evidence_status": "train_search_candidate", + "generation": 6, + "home_strength_exponent": 1.1340765001951492, + "league": "wnba", + "margin_sd": 12.028397269421774, + "parent_hash": "ab7556213fe56f2a8c712da7df880059839437bfee109b7996749fcea6802355", + "profile_hash": "5fb5c12d474e17f2c2719e637aa37a3fb53d24a5dda665aa15e1c8ce0cc9961b", + "total_score_sd": 14.119914546223248 + }, + { + "away_strength_exponent": 1.0428759542867176, + "base_away_score": 73.34588663690329, + "base_home_score": 70.67653549724727, + "evidence_status": "train_search_candidate", + "generation": 6, + "home_strength_exponent": 1.177066395306101, + "league": "wnba", + "margin_sd": 12.30699982423615, + "parent_hash": "7d2a641dbd76ac6fde3dfa526718e79569f129327958d60c3dfafc464b75af97", + "profile_hash": "468447632d3bbfb0286c4335ad436b5f32c841251c8614e5f2096d0b5dbf0a3f", + "total_score_sd": 13.296762898729602 + } + ], + "weights": [ + 0.14546085482667262, + 0.1713210376892588, + 0.17089076167696657, + 0.1708509658434923, + 0.17074437378062704, + 0.17073200618298265 + ] + }, + "league": "wnba", + "promoted": true, + "rollback_ensemble": { + "ensemble_hash": "92022b12c8dd03d4fb005a8a84b4041b3370ee4e401e2e281230d8f6c3b3d284", + "evidence_status": "provisional_reference_anchor", + "league": "wnba", + "profiles": [ + { + "away_strength_exponent": 1.0, + "base_away_score": 80.0, + "base_home_score": 82.0, + "evidence_status": "provisional_reference_anchor", + "generation": 0, + "home_strength_exponent": 1.0, + "league": "wnba", + "margin_sd": 14.0, + "parent_hash": "", + "profile_hash": "a9efcb5f3219e8e9a3df4fdce3b3f767165faa82fe41cf874bdb0716548bb4d9", + "total_score_sd": 17.5 + } + ], + "weights": [ + 1.0 + ] + }, + "rollback_state": { + "drift_score": 0.0, + "drift_threshold": 0.12, + "ensemble_hash": "92022b12c8dd03d4fb005a8a84b4041b3370ee4e401e2e281230d8f6c3b3d284", + "interval_learning_rate": 0.04, + "interval_scale": 1.06183654654536, + "learning_rate": 0.25, + "long_decay": 0.03, + "long_loss_ewma": 8.828494403135926, + "minimum_drift_observations": 40, + "observations": 50, + "short_decay": 0.2, + "short_loss_ewma": 8.776294316432912, + "state_hash": "bb00abb17fee66e3eb9152039b5e33447479ed0e079dd11c90c8fca060eb8d21", + "status": "healthy", + "target_coverage": 0.9, + "weights": [ + 1.0 + ] + }, + "state": { + "drift_score": 0.0, + "drift_threshold": 0.12, + "ensemble_hash": "369bd1fab70c4fe9c443214b61e5bb1297947388fae8402061d599b6dc5a975d", + "interval_learning_rate": 0.04, + "interval_scale": 1.0, + "learning_rate": 0.25, + "long_decay": 0.03, + "long_loss_ewma": 8.093222299641932, + "minimum_drift_observations": 40, + "observations": 50, + "short_decay": 0.2, + "short_loss_ewma": 8.024674327270455, + "state_hash": "b5389b73b296d8ff3307fa1679ea4534b3e0cdb82aba2cf0f5a58625c2587f88", + "status": "healthy", + "target_coverage": 0.9, + "weights": [ + 1.500943057167728e-05, + 0.2966973670938307, + 0.07172853425048047, + 0.11683643807084605, + 0.25589695988228717, + 0.25882569127198385 + ] + } + } + ], + "created_at": "2036-11-01T15:00:00+00:00", + "protocol_version": "recursive-calibration-v1" +} diff --git a/reports/autonomy_v3_fixture/improvement_ledger.jsonl b/reports/autonomy_v3_fixture/improvement_ledger.jsonl new file mode 100644 index 0000000..4dd207e --- /dev/null +++ b/reports/autonomy_v3_fixture/improvement_ledger.jsonl @@ -0,0 +1,7 @@ +{"bundle_hash":"56c5551d36093708f6da5ad0165f968a12c69c7ede7216959f7a4d532a40d754","challenger_ensemble_hash":"9bab62c591710731cbd22bded37c374b67f4e1696b2d7e5c9c84dea7a104faef","decision":"retained_incumbent:brier_guardrail_failed","incumbent_ensemble_hash":"bb0b6a4184ebbf8489abaf3dc3df077d280c14d888340b59c2871215f6df8360","league":"mlb","previous_hash":"0000000000000000000000000000000000000000000000000000000000000000","promoted":false,"record_hash":"ec6c2c6f847bda1adc27132ce13c22f5601223e9fb846fdf205f5a5fa369289a","relative_improvement":0.04197394890142503,"selected_ensemble_hash":"bb0b6a4184ebbf8489abaf3dc3df077d280c14d888340b59c2871215f6df8360","sequence":0} +{"bundle_hash":"56c5551d36093708f6da5ad0165f968a12c69c7ede7216959f7a4d532a40d754","challenger_ensemble_hash":"60e7da6020d67de678c0d044f300847fde90bf16161917510b08c83bfd5dbe96","decision":"promoted","incumbent_ensemble_hash":"938868d720f17e6b997358c0a52254af174a1b3081193720225c3ca7e3eda934","league":"nba","previous_hash":"ec6c2c6f847bda1adc27132ce13c22f5601223e9fb846fdf205f5a5fa369289a","promoted":true,"record_hash":"800494d7a652c06af1289cd9a0844edff4ad8138e70c3cc5b4071bbea7576b74","relative_improvement":0.11410288763501682,"selected_ensemble_hash":"60e7da6020d67de678c0d044f300847fde90bf16161917510b08c83bfd5dbe96","sequence":1} +{"bundle_hash":"56c5551d36093708f6da5ad0165f968a12c69c7ede7216959f7a4d532a40d754","challenger_ensemble_hash":"dcfcefe31dbce66dfd8f5eeb5a1490dc505fd383498a0b0a641ed2b486e155a1","decision":"retained_incumbent:minimum_relative_improvement_not_met","incumbent_ensemble_hash":"110e0a2b1d912b65c9c8e2421251514d29e036503be2191a6a19c5418d16b42a","league":"ncaaf","previous_hash":"800494d7a652c06af1289cd9a0844edff4ad8138e70c3cc5b4071bbea7576b74","promoted":false,"record_hash":"5d38dc91cb98cc6bf5adf95a706ee375d308f33dd07b8360da533029f4ecc391","relative_improvement":0.005437533472869828,"selected_ensemble_hash":"110e0a2b1d912b65c9c8e2421251514d29e036503be2191a6a19c5418d16b42a","sequence":2} +{"bundle_hash":"56c5551d36093708f6da5ad0165f968a12c69c7ede7216959f7a4d532a40d754","challenger_ensemble_hash":"3a26b1869b41e662ecdfe044a54229c6e5998280f88a6ad5003326302350654e","decision":"promoted","incumbent_ensemble_hash":"f4a907f61caeed291d4ccf147414e9c0e97aaef2acbf4a47d08961d80fb12539","league":"ncaambb","previous_hash":"5d38dc91cb98cc6bf5adf95a706ee375d308f33dd07b8360da533029f4ecc391","promoted":true,"record_hash":"663bab0f65c38070a1ad776a8f3411bf4dd6969e8d3c83cc4a6609d0a99ddcd4","relative_improvement":0.04225006993986202,"selected_ensemble_hash":"3a26b1869b41e662ecdfe044a54229c6e5998280f88a6ad5003326302350654e","sequence":3} +{"bundle_hash":"56c5551d36093708f6da5ad0165f968a12c69c7ede7216959f7a4d532a40d754","challenger_ensemble_hash":"3998dbc995c7a222b6b34536892eb79abfcf27282bf679a7177ff9539d1f2805","decision":"retained_incumbent:minimum_relative_improvement_not_met","incumbent_ensemble_hash":"52cc748b0f03d805bc32644c746152d71b8878f6cb3e9e2e6df533e3cb3718e2","league":"nfl","previous_hash":"663bab0f65c38070a1ad776a8f3411bf4dd6969e8d3c83cc4a6609d0a99ddcd4","promoted":false,"record_hash":"2c03a252df5234b21819047b01af5292852875ad1525ae5e5cc7c5c6eff41201","relative_improvement":0.008933830438470725,"selected_ensemble_hash":"52cc748b0f03d805bc32644c746152d71b8878f6cb3e9e2e6df533e3cb3718e2","sequence":4} +{"bundle_hash":"56c5551d36093708f6da5ad0165f968a12c69c7ede7216959f7a4d532a40d754","challenger_ensemble_hash":"b4268f4251d62981ee852625c817f48947e9e12aecc39da1b77d4ff869c86289","decision":"promoted","incumbent_ensemble_hash":"382ec0d5185a96f540f91839c8bd6328d408d70a8ec1f16352aa22553c05bcfb","league":"nhl","previous_hash":"2c03a252df5234b21819047b01af5292852875ad1525ae5e5cc7c5c6eff41201","promoted":true,"record_hash":"0d75bb6c7e011b661fd2724153e03618f67c4b391a04278e6f514b55810579ed","relative_improvement":0.013416183995751795,"selected_ensemble_hash":"b4268f4251d62981ee852625c817f48947e9e12aecc39da1b77d4ff869c86289","sequence":5} +{"bundle_hash":"56c5551d36093708f6da5ad0165f968a12c69c7ede7216959f7a4d532a40d754","challenger_ensemble_hash":"369bd1fab70c4fe9c443214b61e5bb1297947388fae8402061d599b6dc5a975d","decision":"promoted","incumbent_ensemble_hash":"92022b12c8dd03d4fb005a8a84b4041b3370ee4e401e2e281230d8f6c3b3d284","league":"wnba","previous_hash":"0d75bb6c7e011b661fd2724153e03618f67c4b391a04278e6f514b55810579ed","promoted":true,"record_hash":"a989c50f313f76fdcf811d10fa4a1aeb61229cb92cc6798625c8050ea8ddb49c","relative_improvement":0.08808478401855853,"selected_ensemble_hash":"369bd1fab70c4fe9c443214b61e5bb1297947388fae8402061d599b6dc5a975d","sequence":6} diff --git a/reports/autonomy_v3_fixture/improvement_report.json b/reports/autonomy_v3_fixture/improvement_report.json new file mode 100644 index 0000000..a95aedc --- /dev/null +++ b/reports/autonomy_v3_fixture/improvement_report.json @@ -0,0 +1,2652 @@ +{ + "bundle_hash": "56c5551d36093708f6da5ad0165f968a12c69c7ede7216959f7a4d532a40d754", + "config": { + "brier_weight": 2.0, + "coverage_weight": 2.0, + "drift_threshold": 0.12, + "elite_count": 6, + "experts_per_ensemble": 6, + "generations": 8, + "interval_learning_rate": 0.04, + "long_decay": 0.03, + "maximum_brier_deterioration": 0.01, + "maximum_coverage_error_deterioration": 0.03, + "minimum_calibration_games": 50, + "minimum_drift_observations": 40, + "minimum_relative_improvement": 0.01, + "minimum_train_games": 100, + "mutation_scale": 0.12, + "online_learning_rate": 0.25, + "population_size": 32, + "seed": 20260718, + "short_decay": 0.2, + "target_coverage": 0.9 + }, + "results": [ + { + "challenger_calibration_score": { + "composite_loss": 5.628048038203239, + "games": 50, + "home_win_brier": 0.21438362127823524, + "interval_coverage": 0.88, + "interval_coverage_error": 0.020000000000000018, + "mean_joint_negative_log_likelihood": 5.159280795646769 + }, + "challenger_ensemble": { + "ensemble_hash": "9bab62c591710731cbd22bded37c374b67f4e1696b2d7e5c9c84dea7a104faef", + "evidence_status": "chronological_calibration_challenger", + "league": "mlb", + "profiles": [ + { + "away_strength_exponent": 1.0, + "base_away_score": 4.3, + "base_home_score": 4.55, + "evidence_status": "provisional_reference_anchor", + "generation": 0, + "home_strength_exponent": 1.0, + "league": "mlb", + "margin_sd": 4.4, + "parent_hash": "", + "profile_hash": "ed4a36ddf530bc1e400bae06859869e1b8b9bf4c542f53a193c39d949638d53e", + "total_score_sd": 4.6 + }, + { + "away_strength_exponent": 0.9732227449430256, + "base_away_score": 3.956619631003342, + "base_home_score": 4.265680638905987, + "evidence_status": "train_search_candidate", + "generation": 8, + "home_strength_exponent": 1.3488121287788095, + "league": "mlb", + "margin_sd": 3.096954687689488, + "parent_hash": "546d9ccfa6f1e7bbcbf4b336b82a20ef363c202a0ee7050e6b493d2942c967fd", + "profile_hash": "1e225e3282db44a590d7fe4164c7afedf965d81d37860a56c9e77168877e3048", + "total_score_sd": 2.997971433630624 + }, + { + "away_strength_exponent": 1.0020506194854666, + "base_away_score": 3.7559409203703473, + "base_home_score": 4.304405142231148, + "evidence_status": "train_search_candidate", + "generation": 7, + "home_strength_exponent": 1.3514363631800606, + "league": "mlb", + "margin_sd": 3.281442209037205, + "parent_hash": "39970d6d664e243e4d463473ad46cbdece703e931ee6edd34bd74b01f9c9fd28", + "profile_hash": "dcd7ff0ebffb55b5b359da5efb717025fd8573c660a2e7e345656bc34b9125e0", + "total_score_sd": 2.963222433665948 + }, + { + "away_strength_exponent": 1.0273788549103269, + "base_away_score": 3.6580735600366596, + "base_home_score": 4.437497007165997, + "evidence_status": "train_search_candidate", + "generation": 8, + "home_strength_exponent": 1.259013229482986, + "league": "mlb", + "margin_sd": 3.1642287864092484, + "parent_hash": "dcd7ff0ebffb55b5b359da5efb717025fd8573c660a2e7e345656bc34b9125e0", + "profile_hash": "e4f7053453ee21ce9c6968ae7800eafa3fc2cd9fc8c2b12c81259468196a8850", + "total_score_sd": 3.0880724576747483 + }, + { + "away_strength_exponent": 1.2213532999101544, + "base_away_score": 3.6733701204579843, + "base_home_score": 4.138570240115561, + "evidence_status": "train_search_candidate", + "generation": 8, + "home_strength_exponent": 1.575881182476844, + "league": "mlb", + "margin_sd": 3.253309700750169, + "parent_hash": "413908ce0d6230dab53ca9eceb9f0d3b83749fc271af655243bb6f87e00944ee", + "profile_hash": "c2b3729772a07d9e222d4bf250c2cf45e1355b4a513e6fe2f719fcbac81ef43f", + "total_score_sd": 2.993226404574243 + }, + { + "away_strength_exponent": 1.0106153431340696, + "base_away_score": 3.7409002739768646, + "base_home_score": 4.463304408483676, + "evidence_status": "train_search_candidate", + "generation": 8, + "home_strength_exponent": 1.3765776247768466, + "league": "mlb", + "margin_sd": 3.1815356151223497, + "parent_hash": "546d9ccfa6f1e7bbcbf4b336b82a20ef363c202a0ee7050e6b493d2942c967fd", + "profile_hash": "d9b49e85601c4f1455c8b42d1cc14b756baa5508fd4a0af177d4148e192c2085", + "total_score_sd": 3.1607384763020856 + } + ], + "weights": [ + 0.15405098422511587, + 0.16964579340176075, + 0.16958720057856735, + 0.1689990874422243, + 0.168933830476312, + 0.16878310387601975 + ] + }, + "challenger_state": { + "drift_score": 0.04338281227700721, + "drift_threshold": 0.12, + "ensemble_hash": "9bab62c591710731cbd22bded37c374b67f4e1696b2d7e5c9c84dea7a104faef", + "interval_learning_rate": 0.04, + "interval_scale": 1.0408107741923895, + "learning_rate": 0.25, + "long_decay": 0.03, + "long_loss_ewma": 5.422571148674965, + "minimum_drift_observations": 40, + "observations": 50, + "short_decay": 0.2, + "short_loss_ewma": 5.657817534876647, + "state_hash": "5a6187460d092b3d9df544e4f38d557be9f92398ada8eb83ef2ee8169a0f7183", + "status": "healthy", + "target_coverage": 0.9, + "weights": [ + 0.02167441758465728, + 0.23486336932707236, + 0.2286810040473044, + 0.16955067257669437, + 0.17545680083210652, + 0.16977373563216505 + ] + }, + "decision": "retained_incumbent:brier_guardrail_failed", + "generations": [ + { + "best_profile_hash": "02aea8c53ef3eb59504e2e58a538849985b12f0589ee52a6bb8ce6ec893e048c", + "best_train_score": { + "composite_loss": 5.835661094796842, + "games": 100, + "home_win_brier": 0.22551068271785332, + "interval_coverage": 0.96, + "interval_coverage_error": 0.05999999999999994, + "mean_joint_negative_log_likelihood": 5.264639729361136 + }, + "candidates_evaluated": 32, + "generation": 1 + }, + { + "best_profile_hash": "752e0c630e79d7456bbdec1d1a0df27cb2b686d9b9a61256f6a882de413883fd", + "best_train_score": { + "composite_loss": 5.788737848023197, + "games": 100, + "home_win_brier": 0.22472008581155475, + "interval_coverage": 0.95, + "interval_coverage_error": 0.04999999999999993, + "mean_joint_negative_log_likelihood": 5.239297676400088 + }, + "candidates_evaluated": 32, + "generation": 2 + }, + { + "best_profile_hash": "5621c4dd467e48e7c55f8f7fa2142d5d23ac4b087141237c218d82d481c9a570", + "best_train_score": { + "composite_loss": 5.7453288022752105, + "games": 100, + "home_win_brier": 0.23018265584794523, + "interval_coverage": 0.935, + "interval_coverage_error": 0.03500000000000003, + "mean_joint_negative_log_likelihood": 5.214963490579319 + }, + "candidates_evaluated": 32, + "generation": 3 + }, + { + "best_profile_hash": "650a2de6216b8b4440f6c67d419395d341433f0aed9f9fce5dc05d62c691f57b", + "best_train_score": { + "composite_loss": 5.687847181033798, + "games": 100, + "home_win_brier": 0.22589729964465238, + "interval_coverage": 0.945, + "interval_coverage_error": 0.04499999999999993, + "mean_joint_negative_log_likelihood": 5.1460525817444935 + }, + "candidates_evaluated": 32, + "generation": 4 + }, + { + "best_profile_hash": "c1595c328c3a8682e66e88617bf4fc3680e6809abc137f9d080ac3e0494db3f9", + "best_train_score": { + "composite_loss": 5.653447010662879, + "games": 100, + "home_win_brier": 0.2254208059011755, + "interval_coverage": 0.935, + "interval_coverage_error": 0.03500000000000003, + "mean_joint_negative_log_likelihood": 5.1326053988605285 + }, + "candidates_evaluated": 32, + "generation": 5 + }, + { + "best_profile_hash": "04be3eb56818406958242d0a6387c78404f6918c434f0e05e872f82395a00aa5", + "best_train_score": { + "composite_loss": 5.6210507543139725, + "games": 100, + "home_win_brier": 0.2253722702087558, + "interval_coverage": 0.925, + "interval_coverage_error": 0.025000000000000022, + "mean_joint_negative_log_likelihood": 5.120306213896461 + }, + "candidates_evaluated": 32, + "generation": 6 + }, + { + "best_profile_hash": "dcd7ff0ebffb55b5b359da5efb717025fd8573c660a2e7e345656bc34b9125e0", + "best_train_score": { + "composite_loss": 5.57430045932439, + "games": 100, + "home_win_brier": 0.22645342258387796, + "interval_coverage": 0.905, + "interval_coverage_error": 0.0050000000000000044, + "mean_joint_negative_log_likelihood": 5.1113936141566345 + }, + "candidates_evaluated": 32, + "generation": 7 + }, + { + "best_profile_hash": "1e225e3282db44a590d7fe4164c7afedf965d81d37860a56c9e77168877e3048", + "best_train_score": { + "composite_loss": 5.572918687508186, + "games": 100, + "home_win_brier": 0.2272108241966743, + "interval_coverage": 0.9, + "interval_coverage_error": 0.0, + "mean_joint_negative_log_likelihood": 5.118497039114837 + }, + "candidates_evaluated": 32, + "generation": 8 + } + ], + "incumbent_calibration_score": { + "composite_loss": 5.874629433875539, + "games": 50, + "home_win_brier": 0.20077420341310603, + "interval_coverage": 0.96, + "interval_coverage_error": 0.05999999999999994, + "mean_joint_negative_log_likelihood": 5.353081027049327 + }, + "incumbent_ensemble": { + "ensemble_hash": "bb0b6a4184ebbf8489abaf3dc3df077d280c14d888340b59c2871215f6df8360", + "evidence_status": "provisional_reference_anchor", + "league": "mlb", + "profiles": [ + { + "away_strength_exponent": 1.0, + "base_away_score": 4.3, + "base_home_score": 4.55, + "evidence_status": "provisional_reference_anchor", + "generation": 0, + "home_strength_exponent": 1.0, + "league": "mlb", + "margin_sd": 4.4, + "parent_hash": "", + "profile_hash": "ed4a36ddf530bc1e400bae06859869e1b8b9bf4c542f53a193c39d949638d53e", + "total_score_sd": 4.6 + } + ], + "weights": [ + 1.0 + ] + }, + "incumbent_state": { + "drift_score": 0.050688478956998094, + "drift_threshold": 0.12, + "ensemble_hash": "bb0b6a4184ebbf8489abaf3dc3df077d280c14d888340b59c2871215f6df8360", + "interval_learning_rate": 0.04, + "interval_scale": 0.8869204367171584, + "learning_rate": 0.25, + "long_decay": 0.03, + "long_loss_ewma": 5.59425099372921, + "minimum_drift_observations": 40, + "observations": 50, + "short_decay": 0.2, + "short_loss_ewma": 5.8778150675050185, + "state_hash": "a23ac9f2ccc36db8368dcd341b0025f12d6d2b188c95ca9013b1dc61b461e78a", + "status": "healthy", + "target_coverage": 0.9, + "weights": [ + 1.0 + ] + }, + "league": "mlb", + "promoted": false, + "relative_improvement": 0.04197394890142503, + "selected_ensemble": { + "ensemble_hash": "bb0b6a4184ebbf8489abaf3dc3df077d280c14d888340b59c2871215f6df8360", + "evidence_status": "provisional_reference_anchor", + "league": "mlb", + "profiles": [ + { + "away_strength_exponent": 1.0, + "base_away_score": 4.3, + "base_home_score": 4.55, + "evidence_status": "provisional_reference_anchor", + "generation": 0, + "home_strength_exponent": 1.0, + "league": "mlb", + "margin_sd": 4.4, + "parent_hash": "", + "profile_hash": "ed4a36ddf530bc1e400bae06859869e1b8b9bf4c542f53a193c39d949638d53e", + "total_score_sd": 4.6 + } + ], + "weights": [ + 1.0 + ] + }, + "selected_state": { + "drift_score": 0.050688478956998094, + "drift_threshold": 0.12, + "ensemble_hash": "bb0b6a4184ebbf8489abaf3dc3df077d280c14d888340b59c2871215f6df8360", + "interval_learning_rate": 0.04, + "interval_scale": 0.8869204367171584, + "learning_rate": 0.25, + "long_decay": 0.03, + "long_loss_ewma": 5.59425099372921, + "minimum_drift_observations": 40, + "observations": 50, + "short_decay": 0.2, + "short_loss_ewma": 5.8778150675050185, + "state_hash": "a23ac9f2ccc36db8368dcd341b0025f12d6d2b188c95ca9013b1dc61b461e78a", + "status": "healthy", + "target_coverage": 0.9, + "weights": [ + 1.0 + ] + } + }, + { + "challenger_calibration_score": { + "composite_loss": 8.248030969834542, + "games": 50, + "home_win_brier": 0.0698963567147646, + "interval_coverage": 0.94, + "interval_coverage_error": 0.039999999999999925, + "mean_joint_negative_log_likelihood": 8.028238256405013 + }, + "challenger_ensemble": { + "ensemble_hash": "60e7da6020d67de678c0d044f300847fde90bf16161917510b08c83bfd5dbe96", + "evidence_status": "chronological_calibration_challenger", + "league": "nba", + "profiles": [ + { + "away_strength_exponent": 1.0, + "base_away_score": 113.0, + "base_home_score": 116.0, + "evidence_status": "provisional_reference_anchor", + "generation": 0, + "home_strength_exponent": 1.0, + "league": "nba", + "margin_sd": 15.0, + "parent_hash": "", + "profile_hash": "1fd107dd93cec7f199c4979e7334f2ad2c7e7d61e6e5d3fa7a7c6741138e40d1", + "total_score_sd": 20.0 + }, + { + "away_strength_exponent": 0.9632460782428364, + "base_away_score": 100.41120048415034, + "base_home_score": 101.40842755264546, + "evidence_status": "train_search_candidate", + "generation": 8, + "home_strength_exponent": 1.1035181164801218, + "league": "nba", + "margin_sd": 13.736474631089353, + "parent_hash": "d4336fc445a68de29aec9f851052ee18e00a6c18e7198fda34d832e4a727eafd", + "profile_hash": "6dabbdf2c5066b2dc80eb6e6088fbe21f308472a7241c3d76a36ea7ae27b0dc9", + "total_score_sd": 15.530808786290953 + }, + { + "away_strength_exponent": 0.9299864332685756, + "base_away_score": 99.12089973217256, + "base_home_score": 102.60460634910284, + "evidence_status": "train_search_candidate", + "generation": 7, + "home_strength_exponent": 1.0374456736407256, + "league": "nba", + "margin_sd": 14.476747331295748, + "parent_hash": "d4336fc445a68de29aec9f851052ee18e00a6c18e7198fda34d832e4a727eafd", + "profile_hash": "2c3b39a22523f4f245dcd1afe8fcfc54544020073f6153bd180207f50ca0fa67", + "total_score_sd": 15.986652973245818 + }, + { + "away_strength_exponent": 0.9314639467993735, + "base_away_score": 97.55462255575519, + "base_home_score": 100.60692741111606, + "evidence_status": "train_search_candidate", + "generation": 6, + "home_strength_exponent": 1.0481985521298671, + "league": "nba", + "margin_sd": 13.538006034660736, + "parent_hash": "1f29dc288a020ac6049537b27f5d91603e2e548bb8639e20fcd8c9527fa9b748", + "profile_hash": "d4336fc445a68de29aec9f851052ee18e00a6c18e7198fda34d832e4a727eafd", + "total_score_sd": 16.76275018651739 + }, + { + "away_strength_exponent": 0.8862112258764434, + "base_away_score": 100.34400852330234, + "base_home_score": 99.90111885665915, + "evidence_status": "train_search_candidate", + "generation": 7, + "home_strength_exponent": 0.9978232275893777, + "league": "nba", + "margin_sd": 13.709770977740531, + "parent_hash": "4cc9fa6c37e99b992fc73dec931c77bbd815b6a14538deb5d0471d5dfe18a3b3", + "profile_hash": "2b9ae85a9ff77cddc6e95f95feff238a5aad863eb1b5a6d4d0c1682859f773cf", + "total_score_sd": 14.728999472122727 + }, + { + "away_strength_exponent": 0.9168426579759286, + "base_away_score": 98.08693111402029, + "base_home_score": 100.24510182059633, + "evidence_status": "train_search_candidate", + "generation": 8, + "home_strength_exponent": 1.029985233214439, + "league": "nba", + "margin_sd": 13.914503225184157, + "parent_hash": "2c3b39a22523f4f245dcd1afe8fcfc54544020073f6153bd180207f50ca0fa67", + "profile_hash": "c24f67abab0bf80575320bbbed67381f38b4de2aeee0cfe291bee9e80ceba669", + "total_score_sd": 16.713986694619834 + } + ], + "weights": [ + 0.12893045230055183, + 0.17538891395996567, + 0.1747196187644406, + 0.17386258202490054, + 0.17362605222135616, + 0.17347238072878526 + ] + }, + "challenger_state": { + "drift_score": 0.0, + "drift_threshold": 0.12, + "ensemble_hash": "60e7da6020d67de678c0d044f300847fde90bf16161917510b08c83bfd5dbe96", + "interval_learning_rate": 0.04, + "interval_scale": 0.9231163463866361, + "learning_rate": 0.25, + "long_decay": 0.03, + "long_loss_ewma": 8.475867039772714, + "minimum_drift_observations": 40, + "observations": 50, + "short_decay": 0.2, + "short_loss_ewma": 8.262049524590857, + "state_hash": "fe671acbd9f09da2f1984ffa82fa65ab28b4110cb59498739477866e598f119d", + "status": "healthy", + "target_coverage": 0.9, + "weights": [ + 5.513615121909581e-07, + 0.43303118967699145, + 0.16262743922906853, + 0.13862691445333053, + 0.14035629144863276, + 0.1253576138304646 + ] + }, + "decision": "promoted", + "generations": [ + { + "best_profile_hash": "b011c525e5fcde41f45150880a10d718b49123b4346a972b501117575850c6e7", + "best_train_score": { + "composite_loss": 8.751860362501507, + "games": 100, + "home_win_brier": 0.10868264922047732, + "interval_coverage": 0.965, + "interval_coverage_error": 0.06499999999999995, + "mean_joint_negative_log_likelihood": 8.404495064060551 + }, + "candidates_evaluated": 32, + "generation": 1 + }, + { + "best_profile_hash": "64c7f6fdea7e6368e3c4906023a5721fa005f1ad7e07423c174f515d36c0d58e", + "best_train_score": { + "composite_loss": 8.576444497887572, + "games": 100, + "home_win_brier": 0.10440357380938814, + "interval_coverage": 0.915, + "interval_coverage_error": 0.015000000000000013, + "mean_joint_negative_log_likelihood": 8.337637350268796 + }, + "candidates_evaluated": 32, + "generation": 2 + }, + { + "best_profile_hash": "184ac8a18b48db182cac3afca98f6f5d07a4f9c8c72fcebc344c8494d9fcb7f6", + "best_train_score": { + "composite_loss": 8.569914952766858, + "games": 100, + "home_win_brier": 0.11290839482469123, + "interval_coverage": 0.92, + "interval_coverage_error": 0.020000000000000018, + "mean_joint_negative_log_likelihood": 8.304098163117477 + }, + "candidates_evaluated": 32, + "generation": 3 + }, + { + "best_profile_hash": "1f29dc288a020ac6049537b27f5d91603e2e548bb8639e20fcd8c9527fa9b748", + "best_train_score": { + "composite_loss": 8.38170393584511, + "games": 100, + "home_win_brier": 0.10487332850355903, + "interval_coverage": 0.915, + "interval_coverage_error": 0.015000000000000013, + "mean_joint_negative_log_likelihood": 8.141957278837992 + }, + "candidates_evaluated": 32, + "generation": 4 + }, + { + "best_profile_hash": "1f29dc288a020ac6049537b27f5d91603e2e548bb8639e20fcd8c9527fa9b748", + "best_train_score": { + "composite_loss": 8.38170393584511, + "games": 100, + "home_win_brier": 0.10487332850355903, + "interval_coverage": 0.915, + "interval_coverage_error": 0.015000000000000013, + "mean_joint_negative_log_likelihood": 8.141957278837992 + }, + "candidates_evaluated": 32, + "generation": 5 + }, + { + "best_profile_hash": "d4336fc445a68de29aec9f851052ee18e00a6c18e7198fda34d832e4a727eafd", + "best_train_score": { + "composite_loss": 8.363328105496414, + "games": 100, + "home_win_brier": 0.10512816562646293, + "interval_coverage": 0.915, + "interval_coverage_error": 0.015000000000000013, + "mean_joint_negative_log_likelihood": 8.123071774243488 + }, + "candidates_evaluated": 32, + "generation": 6 + }, + { + "best_profile_hash": "2c3b39a22523f4f245dcd1afe8fcfc54544020073f6153bd180207f50ca0fa67", + "best_train_score": { + "composite_loss": 8.343658978053798, + "games": 100, + "home_win_brier": 0.10515922355057844, + "interval_coverage": 0.91, + "interval_coverage_error": 0.010000000000000009, + "mean_joint_negative_log_likelihood": 8.113340530952641 + }, + "candidates_evaluated": 32, + "generation": 7 + }, + { + "best_profile_hash": "6dabbdf2c5066b2dc80eb6e6088fbe21f308472a7241c3d76a36ea7ae27b0dc9", + "best_train_score": { + "composite_loss": 8.328365525920297, + "games": 100, + "home_win_brier": 0.10894498208710375, + "interval_coverage": 0.9, + "interval_coverage_error": 0.0, + "mean_joint_negative_log_likelihood": 8.11047556174609 + }, + "candidates_evaluated": 32, + "generation": 8 + } + ], + "incumbent_calibration_score": { + "composite_loss": 9.310371209830079, + "games": 50, + "home_win_brier": 0.0654562378544873, + "interval_coverage": 0.83, + "interval_coverage_error": 0.07000000000000006, + "mean_joint_negative_log_likelihood": 9.039458734121103 + }, + "incumbent_ensemble": { + "ensemble_hash": "938868d720f17e6b997358c0a52254af174a1b3081193720225c3ca7e3eda934", + "evidence_status": "provisional_reference_anchor", + "league": "nba", + "profiles": [ + { + "away_strength_exponent": 1.0, + "base_away_score": 113.0, + "base_home_score": 116.0, + "evidence_status": "provisional_reference_anchor", + "generation": 0, + "home_strength_exponent": 1.0, + "league": "nba", + "margin_sd": 15.0, + "parent_hash": "", + "profile_hash": "1fd107dd93cec7f199c4979e7334f2ad2c7e7d61e6e5d3fa7a7c6741138e40d1", + "total_score_sd": 20.0 + } + ], + "weights": [ + 1.0 + ] + }, + "incumbent_state": { + "drift_score": 0.0, + "drift_threshold": 0.12, + "ensemble_hash": "938868d720f17e6b997358c0a52254af174a1b3081193720225c3ca7e3eda934", + "interval_learning_rate": 0.04, + "interval_scale": 1.1502737988572285, + "learning_rate": 0.25, + "long_decay": 0.03, + "long_loss_ewma": 9.584379101566844, + "minimum_drift_observations": 40, + "observations": 50, + "short_decay": 0.2, + "short_loss_ewma": 9.00479793127273, + "state_hash": "8938e17f435856b6ce780263706e625583f33790ccd311fee85a1564aa1988dd", + "status": "healthy", + "target_coverage": 0.9, + "weights": [ + 1.0 + ] + }, + "league": "nba", + "promoted": true, + "relative_improvement": 0.11410288763501682, + "selected_ensemble": { + "ensemble_hash": "60e7da6020d67de678c0d044f300847fde90bf16161917510b08c83bfd5dbe96", + "evidence_status": "chronological_calibration_challenger", + "league": "nba", + "profiles": [ + { + "away_strength_exponent": 1.0, + "base_away_score": 113.0, + "base_home_score": 116.0, + "evidence_status": "provisional_reference_anchor", + "generation": 0, + "home_strength_exponent": 1.0, + "league": "nba", + "margin_sd": 15.0, + "parent_hash": "", + "profile_hash": "1fd107dd93cec7f199c4979e7334f2ad2c7e7d61e6e5d3fa7a7c6741138e40d1", + "total_score_sd": 20.0 + }, + { + "away_strength_exponent": 0.9632460782428364, + "base_away_score": 100.41120048415034, + "base_home_score": 101.40842755264546, + "evidence_status": "train_search_candidate", + "generation": 8, + "home_strength_exponent": 1.1035181164801218, + "league": "nba", + "margin_sd": 13.736474631089353, + "parent_hash": "d4336fc445a68de29aec9f851052ee18e00a6c18e7198fda34d832e4a727eafd", + "profile_hash": "6dabbdf2c5066b2dc80eb6e6088fbe21f308472a7241c3d76a36ea7ae27b0dc9", + "total_score_sd": 15.530808786290953 + }, + { + "away_strength_exponent": 0.9299864332685756, + "base_away_score": 99.12089973217256, + "base_home_score": 102.60460634910284, + "evidence_status": "train_search_candidate", + "generation": 7, + "home_strength_exponent": 1.0374456736407256, + "league": "nba", + "margin_sd": 14.476747331295748, + "parent_hash": "d4336fc445a68de29aec9f851052ee18e00a6c18e7198fda34d832e4a727eafd", + "profile_hash": "2c3b39a22523f4f245dcd1afe8fcfc54544020073f6153bd180207f50ca0fa67", + "total_score_sd": 15.986652973245818 + }, + { + "away_strength_exponent": 0.9314639467993735, + "base_away_score": 97.55462255575519, + "base_home_score": 100.60692741111606, + "evidence_status": "train_search_candidate", + "generation": 6, + "home_strength_exponent": 1.0481985521298671, + "league": "nba", + "margin_sd": 13.538006034660736, + "parent_hash": "1f29dc288a020ac6049537b27f5d91603e2e548bb8639e20fcd8c9527fa9b748", + "profile_hash": "d4336fc445a68de29aec9f851052ee18e00a6c18e7198fda34d832e4a727eafd", + "total_score_sd": 16.76275018651739 + }, + { + "away_strength_exponent": 0.8862112258764434, + "base_away_score": 100.34400852330234, + "base_home_score": 99.90111885665915, + "evidence_status": "train_search_candidate", + "generation": 7, + "home_strength_exponent": 0.9978232275893777, + "league": "nba", + "margin_sd": 13.709770977740531, + "parent_hash": "4cc9fa6c37e99b992fc73dec931c77bbd815b6a14538deb5d0471d5dfe18a3b3", + "profile_hash": "2b9ae85a9ff77cddc6e95f95feff238a5aad863eb1b5a6d4d0c1682859f773cf", + "total_score_sd": 14.728999472122727 + }, + { + "away_strength_exponent": 0.9168426579759286, + "base_away_score": 98.08693111402029, + "base_home_score": 100.24510182059633, + "evidence_status": "train_search_candidate", + "generation": 8, + "home_strength_exponent": 1.029985233214439, + "league": "nba", + "margin_sd": 13.914503225184157, + "parent_hash": "2c3b39a22523f4f245dcd1afe8fcfc54544020073f6153bd180207f50ca0fa67", + "profile_hash": "c24f67abab0bf80575320bbbed67381f38b4de2aeee0cfe291bee9e80ceba669", + "total_score_sd": 16.713986694619834 + } + ], + "weights": [ + 0.12893045230055183, + 0.17538891395996567, + 0.1747196187644406, + 0.17386258202490054, + 0.17362605222135616, + 0.17347238072878526 + ] + }, + "selected_state": { + "drift_score": 0.0, + "drift_threshold": 0.12, + "ensemble_hash": "60e7da6020d67de678c0d044f300847fde90bf16161917510b08c83bfd5dbe96", + "interval_learning_rate": 0.04, + "interval_scale": 0.9231163463866361, + "learning_rate": 0.25, + "long_decay": 0.03, + "long_loss_ewma": 8.475867039772714, + "minimum_drift_observations": 40, + "observations": 50, + "short_decay": 0.2, + "short_loss_ewma": 8.262049524590857, + "state_hash": "fe671acbd9f09da2f1984ffa82fa65ab28b4110cb59498739477866e598f119d", + "status": "healthy", + "target_coverage": 0.9, + "weights": [ + 5.513615121909581e-07, + 0.43303118967699145, + 0.16262743922906853, + 0.13862691445333053, + 0.14035629144863276, + 0.1253576138304646 + ] + } + }, + { + "challenger_calibration_score": { + "composite_loss": 8.761251921087043, + "games": 50, + "home_win_brier": 0.2309784924355083, + "interval_coverage": 0.87, + "interval_coverage_error": 0.030000000000000027, + "mean_joint_negative_log_likelihood": 8.239294936216025 + }, + "challenger_ensemble": { + "ensemble_hash": "dcfcefe31dbce66dfd8f5eeb5a1490dc505fd383498a0b0a641ed2b486e155a1", + "evidence_status": "chronological_calibration_challenger", + "league": "ncaaf", + "profiles": [ + { + "away_strength_exponent": 1.0, + "base_away_score": 26.5, + "base_home_score": 29.0, + "evidence_status": "provisional_reference_anchor", + "generation": 0, + "home_strength_exponent": 1.0, + "league": "ncaaf", + "margin_sd": 18.0, + "parent_hash": "", + "profile_hash": "6c3ad3401f75976c8a0cc1301c7aa9d9ba28a4d78b9bd5ee2432bf29e58493f7", + "total_score_sd": 15.7 + }, + { + "away_strength_exponent": 0.8683260259816346, + "base_away_score": 24.32544109440506, + "base_home_score": 23.9566920544987, + "evidence_status": "train_search_candidate", + "generation": 8, + "home_strength_exponent": 0.9196385820892058, + "league": "ncaaf", + "margin_sd": 12.289222730603594, + "parent_hash": "b39b65c72765ca636d83ad54fac9b5797863f17071eb7aa27e83b50474d7689d", + "profile_hash": "f5c9840786b6201da9a65cdb62997dfa335b97af287d2c6b980c0bbbfcf7542e", + "total_score_sd": 12.43390868510682 + }, + { + "away_strength_exponent": 0.9096078610166126, + "base_away_score": 24.91082983081627, + "base_home_score": 24.123091566470293, + "evidence_status": "train_search_candidate", + "generation": 8, + "home_strength_exponent": 0.888366435770028, + "league": "ncaaf", + "margin_sd": 14.129488015416582, + "parent_hash": "e5ddbd73ccc308ce1e55fa9938054dfa1ea7f908cd5cbf225f7126bf4a02b217", + "profile_hash": "adbde67ffec2e8ed7b8ffcb77148430f8a48f8474b6909042c733f452e3dbe4c", + "total_score_sd": 11.709297245021318 + }, + { + "away_strength_exponent": 0.9584625081284368, + "base_away_score": 24.799301032404774, + "base_home_score": 25.534785198496447, + "evidence_status": "train_search_candidate", + "generation": 7, + "home_strength_exponent": 0.9298150901925718, + "league": "ncaaf", + "margin_sd": 13.296087557104656, + "parent_hash": "00baeb8e12429682c3490b075bc43d5307c45f8795f1d001fcfa2e608b0541d5", + "profile_hash": "e5ddbd73ccc308ce1e55fa9938054dfa1ea7f908cd5cbf225f7126bf4a02b217", + "total_score_sd": 12.09023462837543 + }, + { + "away_strength_exponent": 0.9513800830701528, + "base_away_score": 22.81818833797911, + "base_home_score": 24.38880028596671, + "evidence_status": "train_search_candidate", + "generation": 8, + "home_strength_exponent": 1.0526454209993767, + "league": "ncaaf", + "margin_sd": 12.898275898145506, + "parent_hash": "0ecfc92b2fc37be0a1f5a880489f89291077b6a225975bbae56ff56c346d00e4", + "profile_hash": "801ef2b56e0cd0e67270c0ee8f648ae4ae4533e1e3ea6cfe96c71be0c035c037", + "total_score_sd": 12.434282216811535 + }, + { + "away_strength_exponent": 0.9590291217112736, + "base_away_score": 25.796667803022814, + "base_home_score": 24.230975147547593, + "evidence_status": "train_search_candidate", + "generation": 7, + "home_strength_exponent": 1.0513001032404565, + "league": "ncaaf", + "margin_sd": 12.753597354759721, + "parent_hash": "b14988ce746dfe7e73eefc35385c4118256e0f669c5dbfe5136647943d9ae1c6", + "profile_hash": "0ecfc92b2fc37be0a1f5a880489f89291077b6a225975bbae56ff56c346d00e4", + "total_score_sd": 12.488846147947697 + } + ], + "weights": [ + 0.1571525335874964, + 0.16877838486092833, + 0.16869532526714723, + 0.1685043427773905, + 0.16845570292201564, + 0.16841371058502197 + ] + }, + "challenger_state": { + "drift_score": 0.06318035279011783, + "drift_threshold": 0.12, + "ensemble_hash": "dcfcefe31dbce66dfd8f5eeb5a1490dc505fd383498a0b0a641ed2b486e155a1", + "interval_learning_rate": 0.04, + "interval_scale": 1.0618365465453588, + "learning_rate": 0.25, + "long_decay": 0.03, + "long_loss_ewma": 8.643346312203276, + "minimum_drift_observations": 40, + "observations": 50, + "short_decay": 0.2, + "short_loss_ewma": 9.189435981495443, + "state_hash": "ec797d592bb5998558dfe65b7764ed1f14871486198573b65024651f0af54523", + "status": "healthy", + "target_coverage": 0.9, + "weights": [ + 0.11772211665415457, + 0.08239082301927475, + 0.19548448006488245, + 0.22836666496556968, + 0.2988350366603923, + 0.07720087863572628 + ] + }, + "decision": "retained_incumbent:minimum_relative_improvement_not_met", + "generations": [ + { + "best_profile_hash": "61b843ef21f5631a8c901e13d6f15050d293bb91c8b2edfad3ae1c4332654c38", + "best_train_score": { + "composite_loss": 8.542187514716256, + "games": 100, + "home_win_brier": 0.2225168103741196, + "interval_coverage": 0.895, + "interval_coverage_error": 0.0050000000000000044, + "mean_joint_negative_log_likelihood": 8.087153893968017 + }, + "candidates_evaluated": 32, + "generation": 1 + }, + { + "best_profile_hash": "bc7dccd2da202fe52b2f0d09d79e6cb29c0db9883772c3b7ccc916b1a3026b63", + "best_train_score": { + "composite_loss": 8.485121536582794, + "games": 100, + "home_win_brier": 0.22676481568484308, + "interval_coverage": 0.905, + "interval_coverage_error": 0.0050000000000000044, + "mean_joint_negative_log_likelihood": 8.021591905213107 + }, + "candidates_evaluated": 32, + "generation": 2 + }, + { + "best_profile_hash": "bc7dccd2da202fe52b2f0d09d79e6cb29c0db9883772c3b7ccc916b1a3026b63", + "best_train_score": { + "composite_loss": 8.485121536582794, + "games": 100, + "home_win_brier": 0.22676481568484308, + "interval_coverage": 0.905, + "interval_coverage_error": 0.0050000000000000044, + "mean_joint_negative_log_likelihood": 8.021591905213107 + }, + "candidates_evaluated": 32, + "generation": 3 + }, + { + "best_profile_hash": "6ae365d498c4881221a01750e7aed6ff2ab866801215d6a4debcb41487290f6b", + "best_train_score": { + "composite_loss": 8.44270924721444, + "games": 100, + "home_win_brier": 0.22468358566224475, + "interval_coverage": 0.9, + "interval_coverage_error": 0.0, + "mean_joint_negative_log_likelihood": 7.9933420758899505 + }, + "candidates_evaluated": 32, + "generation": 4 + }, + { + "best_profile_hash": "6ae365d498c4881221a01750e7aed6ff2ab866801215d6a4debcb41487290f6b", + "best_train_score": { + "composite_loss": 8.44270924721444, + "games": 100, + "home_win_brier": 0.22468358566224475, + "interval_coverage": 0.9, + "interval_coverage_error": 0.0, + "mean_joint_negative_log_likelihood": 7.9933420758899505 + }, + "candidates_evaluated": 32, + "generation": 5 + }, + { + "best_profile_hash": "6ae365d498c4881221a01750e7aed6ff2ab866801215d6a4debcb41487290f6b", + "best_train_score": { + "composite_loss": 8.44270924721444, + "games": 100, + "home_win_brier": 0.22468358566224475, + "interval_coverage": 0.9, + "interval_coverage_error": 0.0, + "mean_joint_negative_log_likelihood": 7.9933420758899505 + }, + "candidates_evaluated": 32, + "generation": 6 + }, + { + "best_profile_hash": "e5ddbd73ccc308ce1e55fa9938054dfa1ea7f908cd5cbf225f7126bf4a02b217", + "best_train_score": { + "composite_loss": 8.437901795595902, + "games": 100, + "home_win_brier": 0.22329882116741837, + "interval_coverage": 0.895, + "interval_coverage_error": 0.0050000000000000044, + "mean_joint_negative_log_likelihood": 7.981304153261065 + }, + "candidates_evaluated": 32, + "generation": 7 + }, + { + "best_profile_hash": "f5c9840786b6201da9a65cdb62997dfa335b97af287d2c6b980c0bbbfcf7542e", + "best_train_score": { + "composite_loss": 8.43140179731232, + "games": 100, + "home_win_brier": 0.22248231792931097, + "interval_coverage": 0.9, + "interval_coverage_error": 0.0, + "mean_joint_negative_log_likelihood": 7.986437161453698 + }, + "candidates_evaluated": 32, + "generation": 8 + } + ], + "incumbent_calibration_score": { + "composite_loss": 8.809151979845048, + "games": 50, + "home_win_brier": 0.22737367673106743, + "interval_coverage": 0.92, + "interval_coverage_error": 0.020000000000000018, + "mean_joint_negative_log_likelihood": 8.314404626382911 + }, + "incumbent_ensemble": { + "ensemble_hash": "110e0a2b1d912b65c9c8e2421251514d29e036503be2191a6a19c5418d16b42a", + "evidence_status": "provisional_reference_anchor", + "league": "ncaaf", + "profiles": [ + { + "away_strength_exponent": 1.0, + "base_away_score": 26.5, + "base_home_score": 29.0, + "evidence_status": "provisional_reference_anchor", + "generation": 0, + "home_strength_exponent": 1.0, + "league": "ncaaf", + "margin_sd": 18.0, + "parent_hash": "", + "profile_hash": "6c3ad3401f75976c8a0cc1301c7aa9d9ba28a4d78b9bd5ee2432bf29e58493f7", + "total_score_sd": 15.7 + } + ], + "weights": [ + 1.0 + ] + }, + "incumbent_state": { + "drift_score": 0.053373359494810986, + "drift_threshold": 0.12, + "ensemble_hash": "110e0a2b1d912b65c9c8e2421251514d29e036503be2191a6a19c5418d16b42a", + "interval_learning_rate": 0.04, + "interval_scale": 0.9607894391523236, + "learning_rate": 0.25, + "long_decay": 0.03, + "long_loss_ewma": 8.821992883967974, + "minimum_drift_observations": 40, + "observations": 50, + "short_decay": 0.2, + "short_loss_ewma": 9.292852281624661, + "state_hash": "6490ed8bb46c72d47e410317f52bba03128f9076dbe23088cbd0748735b8b91b", + "status": "healthy", + "target_coverage": 0.9, + "weights": [ + 1.0 + ] + }, + "league": "ncaaf", + "promoted": false, + "relative_improvement": 0.005437533472869828, + "selected_ensemble": { + "ensemble_hash": "110e0a2b1d912b65c9c8e2421251514d29e036503be2191a6a19c5418d16b42a", + "evidence_status": "provisional_reference_anchor", + "league": "ncaaf", + "profiles": [ + { + "away_strength_exponent": 1.0, + "base_away_score": 26.5, + "base_home_score": 29.0, + "evidence_status": "provisional_reference_anchor", + "generation": 0, + "home_strength_exponent": 1.0, + "league": "ncaaf", + "margin_sd": 18.0, + "parent_hash": "", + "profile_hash": "6c3ad3401f75976c8a0cc1301c7aa9d9ba28a4d78b9bd5ee2432bf29e58493f7", + "total_score_sd": 15.7 + } + ], + "weights": [ + 1.0 + ] + }, + "selected_state": { + "drift_score": 0.053373359494810986, + "drift_threshold": 0.12, + "ensemble_hash": "110e0a2b1d912b65c9c8e2421251514d29e036503be2191a6a19c5418d16b42a", + "interval_learning_rate": 0.04, + "interval_scale": 0.9607894391523236, + "learning_rate": 0.25, + "long_decay": 0.03, + "long_loss_ewma": 8.821992883967974, + "minimum_drift_observations": 40, + "observations": 50, + "short_decay": 0.2, + "short_loss_ewma": 9.292852281624661, + "state_hash": "6490ed8bb46c72d47e410317f52bba03128f9076dbe23088cbd0748735b8b91b", + "status": "healthy", + "target_coverage": 0.9, + "weights": [ + 1.0 + ] + } + }, + { + "challenger_calibration_score": { + "composite_loss": 8.216060900552336, + "games": 50, + "home_win_brier": 0.16681685856160933, + "interval_coverage": 0.92, + "interval_coverage_error": 0.020000000000000018, + "mean_joint_negative_log_likelihood": 7.842427183429115 + }, + "challenger_ensemble": { + "ensemble_hash": "3a26b1869b41e662ecdfe044a54229c6e5998280f88a6ad5003326302350654e", + "evidence_status": "chronological_calibration_challenger", + "league": "ncaambb", + "profiles": [ + { + "away_strength_exponent": 1.0, + "base_away_score": 72.0, + "base_home_score": 75.0, + "evidence_status": "provisional_reference_anchor", + "generation": 0, + "home_strength_exponent": 1.0, + "league": "ncaambb", + "margin_sd": 14.0, + "parent_hash": "", + "profile_hash": "14be1ec209dc6acf4030c480c7c2c6abf250986580e5da75595419c8fd8028df", + "total_score_sd": 16.5 + }, + { + "away_strength_exponent": 0.9380995927689169, + "base_away_score": 64.26338685281874, + "base_home_score": 67.90736863209588, + "evidence_status": "train_search_candidate", + "generation": 8, + "home_strength_exponent": 1.1198826403419602, + "league": "ncaambb", + "margin_sd": 12.337242613541385, + "parent_hash": "d908371efab1b1058ff372457736decc2f22f9d69723d762431e794c4f4582e3", + "profile_hash": "05bcc37afeab1ab4bf15ea01c88cd88d9e19e259e85a843ba6904dc94e5285ee", + "total_score_sd": 11.844982854797419 + }, + { + "away_strength_exponent": 0.9839084920688369, + "base_away_score": 64.17862932003789, + "base_home_score": 66.40814600980535, + "evidence_status": "train_search_candidate", + "generation": 7, + "home_strength_exponent": 1.1265354070594922, + "league": "ncaambb", + "margin_sd": 11.751801481101191, + "parent_hash": "5052bec2704f4278b8055686fdfd6fdaca864456ff8480e6bd1c3a16ff8cd5c2", + "profile_hash": "d908371efab1b1058ff372457736decc2f22f9d69723d762431e794c4f4582e3", + "total_score_sd": 12.527208607031197 + }, + { + "away_strength_exponent": 1.0302789332906102, + "base_away_score": 64.56603709857632, + "base_home_score": 68.11959848451264, + "evidence_status": "train_search_candidate", + "generation": 8, + "home_strength_exponent": 1.15845619003206, + "league": "ncaambb", + "margin_sd": 12.172650135194955, + "parent_hash": "d908371efab1b1058ff372457736decc2f22f9d69723d762431e794c4f4582e3", + "profile_hash": "a3426257638d3914687ea87b35c7948a9df8423501f8509cc563d4635f364bad", + "total_score_sd": 12.250367245233734 + }, + { + "away_strength_exponent": 0.9757805013766029, + "base_away_score": 64.69153447008274, + "base_home_score": 67.3019953859376, + "evidence_status": "train_search_candidate", + "generation": 6, + "home_strength_exponent": 1.0463799843916333, + "league": "ncaambb", + "margin_sd": 12.360366882633027, + "parent_hash": "035fdaf6642f08d5f653660bf422cf514301df099700740c1740ff66f014aa95", + "profile_hash": "5052bec2704f4278b8055686fdfd6fdaca864456ff8480e6bd1c3a16ff8cd5c2", + "total_score_sd": 13.12846794130971 + }, + { + "away_strength_exponent": 1.0525771945812439, + "base_away_score": 65.56062542041047, + "base_home_score": 67.05886346685132, + "evidence_status": "train_search_candidate", + "generation": 8, + "home_strength_exponent": 1.0249276075178801, + "league": "ncaambb", + "margin_sd": 11.814617284868469, + "parent_hash": "5052bec2704f4278b8055686fdfd6fdaca864456ff8480e6bd1c3a16ff8cd5c2", + "profile_hash": "d5abde343917aae55c5b655aed7e5ce5542a8d6e94ebb6cc5b1dff7aef5b914b", + "total_score_sd": 12.842103349942267 + } + ], + "weights": [ + 0.15204731462607787, + 0.17053030010320389, + 0.17008865902417303, + 0.1698776799805041, + 0.16875167581778863, + 0.16870437044825248 + ] + }, + "challenger_state": { + "drift_score": 0.0, + "drift_threshold": 0.12, + "ensemble_hash": "3a26b1869b41e662ecdfe044a54229c6e5998280f88a6ad5003326302350654e", + "interval_learning_rate": 0.04, + "interval_scale": 0.9607894391523251, + "learning_rate": 0.25, + "long_decay": 0.03, + "long_loss_ewma": 8.174282931387665, + "minimum_drift_observations": 40, + "observations": 50, + "short_decay": 0.2, + "short_loss_ewma": 8.008633532956152, + "state_hash": "924515d3adfcde4094160bbe9a1c1741be83320aad2c0d1e322f724c309fcb9d", + "status": "healthy", + "target_coverage": 0.9, + "weights": [ + 0.001155014984004925, + 0.19050882700932764, + 0.19279742459698684, + 0.14440496188814017, + 0.26188724112414913, + 0.20924653039739127 + ] + }, + "decision": "promoted", + "generations": [ + { + "best_profile_hash": "64b6bb506c3dcb4cfaa4cd8254d947e885306b344a34b5f7ff92a722eee85063", + "best_train_score": { + "composite_loss": 8.374606997457724, + "games": 100, + "home_win_brier": 0.13776513594898673, + "interval_coverage": 0.935, + "interval_coverage_error": 0.03500000000000003, + "mean_joint_negative_log_likelihood": 8.029076725559749 + }, + "candidates_evaluated": 32, + "generation": 1 + }, + { + "best_profile_hash": "65a5394d5eec4c213c87f895eafdfe93441f7a18d68780cd4bf1c74eb21a4a2c", + "best_train_score": { + "composite_loss": 8.258482092728922, + "games": 100, + "home_win_brier": 0.13574660908041764, + "interval_coverage": 0.935, + "interval_coverage_error": 0.03500000000000003, + "mean_joint_negative_log_likelihood": 7.916988874568086 + }, + "candidates_evaluated": 32, + "generation": 2 + }, + { + "best_profile_hash": "34aae2ca0c08c9865d7f2ce6d4c93dfaf4e723938132713bef356b71cf6a1fc8", + "best_train_score": { + "composite_loss": 8.248697462807533, + "games": 100, + "home_win_brier": 0.1297155386445725, + "interval_coverage": 0.955, + "interval_coverage_error": 0.05499999999999994, + "mean_joint_negative_log_likelihood": 7.879266385518389 + }, + "candidates_evaluated": 32, + "generation": 3 + }, + { + "best_profile_hash": "b349e3227ea38b25cb1a7a65f6e732d435cb774719bcd15da0a46f5d60ae6e4c", + "best_train_score": { + "composite_loss": 8.247592448455915, + "games": 100, + "home_win_brier": 0.13014343448986546, + "interval_coverage": 0.945, + "interval_coverage_error": 0.04499999999999993, + "mean_joint_negative_log_likelihood": 7.897305579476185 + }, + "candidates_evaluated": 32, + "generation": 4 + }, + { + "best_profile_hash": "035fdaf6642f08d5f653660bf422cf514301df099700740c1740ff66f014aa95", + "best_train_score": { + "composite_loss": 8.197193058468407, + "games": 100, + "home_win_brier": 0.13015935604396173, + "interval_coverage": 0.94, + "interval_coverage_error": 0.039999999999999925, + "mean_joint_negative_log_likelihood": 7.856874346380485 + }, + "candidates_evaluated": 32, + "generation": 5 + }, + { + "best_profile_hash": "5052bec2704f4278b8055686fdfd6fdaca864456ff8480e6bd1c3a16ff8cd5c2", + "best_train_score": { + "composite_loss": 8.140852763777975, + "games": 100, + "home_win_brier": 0.13020324963078864, + "interval_coverage": 0.92, + "interval_coverage_error": 0.020000000000000018, + "mean_joint_negative_log_likelihood": 7.840446264516397 + }, + "candidates_evaluated": 32, + "generation": 6 + }, + { + "best_profile_hash": "d908371efab1b1058ff372457736decc2f22f9d69723d762431e794c4f4582e3", + "best_train_score": { + "composite_loss": 8.109286506576968, + "games": 100, + "home_win_brier": 0.12896937059178779, + "interval_coverage": 0.89, + "interval_coverage_error": 0.010000000000000009, + "mean_joint_negative_log_likelihood": 7.831347765393392 + }, + "candidates_evaluated": 32, + "generation": 7 + }, + { + "best_profile_hash": "05bcc37afeab1ab4bf15ea01c88cd88d9e19e259e85a843ba6904dc94e5285ee", + "best_train_score": { + "composite_loss": 8.098913829080733, + "games": 100, + "home_win_brier": 0.13147547236343635, + "interval_coverage": 0.9, + "interval_coverage_error": 0.0, + "mean_joint_negative_log_likelihood": 7.835962884353861 + }, + "candidates_evaluated": 32, + "generation": 8 + } + ], + "incumbent_calibration_score": { + "composite_loss": 8.578503263410775, + "games": 50, + "home_win_brier": 0.16468304335835116, + "interval_coverage": 0.9, + "interval_coverage_error": 0.0, + "mean_joint_negative_log_likelihood": 8.249137176694074 + }, + "incumbent_ensemble": { + "ensemble_hash": "f4a907f61caeed291d4ccf147414e9c0e97aaef2acbf4a47d08961d80fb12539", + "evidence_status": "provisional_reference_anchor", + "league": "ncaambb", + "profiles": [ + { + "away_strength_exponent": 1.0, + "base_away_score": 72.0, + "base_home_score": 75.0, + "evidence_status": "provisional_reference_anchor", + "generation": 0, + "home_strength_exponent": 1.0, + "league": "ncaambb", + "margin_sd": 14.0, + "parent_hash": "", + "profile_hash": "14be1ec209dc6acf4030c480c7c2c6abf250986580e5da75595419c8fd8028df", + "total_score_sd": 16.5 + } + ], + "weights": [ + 1.0 + ] + }, + "incumbent_state": { + "drift_score": 0.0, + "drift_threshold": 0.12, + "ensemble_hash": "f4a907f61caeed291d4ccf147414e9c0e97aaef2acbf4a47d08961d80fb12539", + "interval_learning_rate": 0.04, + "interval_scale": 1.0000000000000013, + "learning_rate": 0.25, + "long_decay": 0.03, + "long_loss_ewma": 8.386494742786283, + "minimum_drift_observations": 40, + "observations": 50, + "short_decay": 0.2, + "short_loss_ewma": 8.246116288876184, + "state_hash": "b00bf20e38b236488432ebaaa42691474cd68631fd141127185217573f05d4f2", + "status": "healthy", + "target_coverage": 0.9, + "weights": [ + 1.0 + ] + }, + "league": "ncaambb", + "promoted": true, + "relative_improvement": 0.04225006993986202, + "selected_ensemble": { + "ensemble_hash": "3a26b1869b41e662ecdfe044a54229c6e5998280f88a6ad5003326302350654e", + "evidence_status": "chronological_calibration_challenger", + "league": "ncaambb", + "profiles": [ + { + "away_strength_exponent": 1.0, + "base_away_score": 72.0, + "base_home_score": 75.0, + "evidence_status": "provisional_reference_anchor", + "generation": 0, + "home_strength_exponent": 1.0, + "league": "ncaambb", + "margin_sd": 14.0, + "parent_hash": "", + "profile_hash": "14be1ec209dc6acf4030c480c7c2c6abf250986580e5da75595419c8fd8028df", + "total_score_sd": 16.5 + }, + { + "away_strength_exponent": 0.9380995927689169, + "base_away_score": 64.26338685281874, + "base_home_score": 67.90736863209588, + "evidence_status": "train_search_candidate", + "generation": 8, + "home_strength_exponent": 1.1198826403419602, + "league": "ncaambb", + "margin_sd": 12.337242613541385, + "parent_hash": "d908371efab1b1058ff372457736decc2f22f9d69723d762431e794c4f4582e3", + "profile_hash": "05bcc37afeab1ab4bf15ea01c88cd88d9e19e259e85a843ba6904dc94e5285ee", + "total_score_sd": 11.844982854797419 + }, + { + "away_strength_exponent": 0.9839084920688369, + "base_away_score": 64.17862932003789, + "base_home_score": 66.40814600980535, + "evidence_status": "train_search_candidate", + "generation": 7, + "home_strength_exponent": 1.1265354070594922, + "league": "ncaambb", + "margin_sd": 11.751801481101191, + "parent_hash": "5052bec2704f4278b8055686fdfd6fdaca864456ff8480e6bd1c3a16ff8cd5c2", + "profile_hash": "d908371efab1b1058ff372457736decc2f22f9d69723d762431e794c4f4582e3", + "total_score_sd": 12.527208607031197 + }, + { + "away_strength_exponent": 1.0302789332906102, + "base_away_score": 64.56603709857632, + "base_home_score": 68.11959848451264, + "evidence_status": "train_search_candidate", + "generation": 8, + "home_strength_exponent": 1.15845619003206, + "league": "ncaambb", + "margin_sd": 12.172650135194955, + "parent_hash": "d908371efab1b1058ff372457736decc2f22f9d69723d762431e794c4f4582e3", + "profile_hash": "a3426257638d3914687ea87b35c7948a9df8423501f8509cc563d4635f364bad", + "total_score_sd": 12.250367245233734 + }, + { + "away_strength_exponent": 0.9757805013766029, + "base_away_score": 64.69153447008274, + "base_home_score": 67.3019953859376, + "evidence_status": "train_search_candidate", + "generation": 6, + "home_strength_exponent": 1.0463799843916333, + "league": "ncaambb", + "margin_sd": 12.360366882633027, + "parent_hash": "035fdaf6642f08d5f653660bf422cf514301df099700740c1740ff66f014aa95", + "profile_hash": "5052bec2704f4278b8055686fdfd6fdaca864456ff8480e6bd1c3a16ff8cd5c2", + "total_score_sd": 13.12846794130971 + }, + { + "away_strength_exponent": 1.0525771945812439, + "base_away_score": 65.56062542041047, + "base_home_score": 67.05886346685132, + "evidence_status": "train_search_candidate", + "generation": 8, + "home_strength_exponent": 1.0249276075178801, + "league": "ncaambb", + "margin_sd": 11.814617284868469, + "parent_hash": "5052bec2704f4278b8055686fdfd6fdaca864456ff8480e6bd1c3a16ff8cd5c2", + "profile_hash": "d5abde343917aae55c5b655aed7e5ce5542a8d6e94ebb6cc5b1dff7aef5b914b", + "total_score_sd": 12.842103349942267 + } + ], + "weights": [ + 0.15204731462607787, + 0.17053030010320389, + 0.17008865902417303, + 0.1698776799805041, + 0.16875167581778863, + 0.16870437044825248 + ] + }, + "selected_state": { + "drift_score": 0.0, + "drift_threshold": 0.12, + "ensemble_hash": "3a26b1869b41e662ecdfe044a54229c6e5998280f88a6ad5003326302350654e", + "interval_learning_rate": 0.04, + "interval_scale": 0.9607894391523251, + "learning_rate": 0.25, + "long_decay": 0.03, + "long_loss_ewma": 8.174282931387665, + "minimum_drift_observations": 40, + "observations": 50, + "short_decay": 0.2, + "short_loss_ewma": 8.008633532956152, + "state_hash": "924515d3adfcde4094160bbe9a1c1741be83320aad2c0d1e322f724c309fcb9d", + "status": "healthy", + "target_coverage": 0.9, + "weights": [ + 0.001155014984004925, + 0.19050882700932764, + 0.19279742459698684, + 0.14440496188814017, + 0.26188724112414913, + 0.20924653039739127 + ] + } + }, + { + "challenger_calibration_score": { + "composite_loss": 8.35269018511674, + "games": 50, + "home_win_brier": 0.23858136537417907, + "interval_coverage": 0.89, + "interval_coverage_error": 0.010000000000000009, + "mean_joint_negative_log_likelihood": 7.855527454368383 + }, + "challenger_ensemble": { + "ensemble_hash": "3998dbc995c7a222b6b34536892eb79abfcf27282bf679a7177ff9539d1f2805", + "evidence_status": "chronological_calibration_challenger", + "league": "nfl", + "profiles": [ + { + "away_strength_exponent": 1.0, + "base_away_score": 21.8, + "base_home_score": 23.5, + "evidence_status": "provisional_reference_anchor", + "generation": 0, + "home_strength_exponent": 1.0, + "league": "nfl", + "margin_sd": 14.0, + "parent_hash": "", + "profile_hash": "2cf12a1c59550d09aad4853366ee45da48461195286aba2fc665167a2d0accd0", + "total_score_sd": 13.7 + }, + { + "away_strength_exponent": 0.8127119203573007, + "base_away_score": 18.73783903969967, + "base_home_score": 20.3629838465437, + "evidence_status": "train_search_candidate", + "generation": 7, + "home_strength_exponent": 0.9969598684496088, + "league": "nfl", + "margin_sd": 13.178752280003424, + "parent_hash": "2a4a54a4e5c8dc6d6a442fef71835692aebb90bc2c1e15966a6519755e2dd08f", + "profile_hash": "afb44b88704aab434182c745d9534462c135846c1314177112e6d2e05e054311", + "total_score_sd": 10.627786627666937 + }, + { + "away_strength_exponent": 0.8020288490154823, + "base_away_score": 17.320637029282533, + "base_home_score": 20.78051406276197, + "evidence_status": "train_search_candidate", + "generation": 8, + "home_strength_exponent": 1.0035162330865472, + "league": "nfl", + "margin_sd": 11.50199515807155, + "parent_hash": "f0c50ab49baa7e660afddfb7e8979b224d34b7e1e51061967bc7c420e3abc67f", + "profile_hash": "766ad7571886b1fadb0ebe6d1ed5fef1256354b23f78160fdb3e476062950d1b", + "total_score_sd": 11.600187239371005 + }, + { + "away_strength_exponent": 0.8512905575954433, + "base_away_score": 18.148896585644028, + "base_home_score": 20.480196992727773, + "evidence_status": "train_search_candidate", + "generation": 8, + "home_strength_exponent": 1.0699568127234662, + "league": "nfl", + "margin_sd": 12.866386653489073, + "parent_hash": "afb44b88704aab434182c745d9534462c135846c1314177112e6d2e05e054311", + "profile_hash": "8731bf2b25c094a319db6036857ab0e1bc3ca4a2bdec5bdf1a9a18d92e808d33", + "total_score_sd": 10.76790465892871 + }, + { + "away_strength_exponent": 0.8523283294650763, + "base_away_score": 18.30063485469079, + "base_home_score": 20.2833663619127, + "evidence_status": "train_search_candidate", + "generation": 6, + "home_strength_exponent": 0.9808436446464107, + "league": "nfl", + "margin_sd": 11.989103506291487, + "parent_hash": "f0c50ab49baa7e660afddfb7e8979b224d34b7e1e51061967bc7c420e3abc67f", + "profile_hash": "d283336f28c8bfedd347d047d6ea0f7f46cc810560a6c68068d61c04a67ed2c1", + "total_score_sd": 11.197421125439893 + }, + { + "away_strength_exponent": 0.889604059819956, + "base_away_score": 18.70864108022062, + "base_home_score": 20.342104976267645, + "evidence_status": "train_search_candidate", + "generation": 6, + "home_strength_exponent": 1.043714861028283, + "league": "nfl", + "margin_sd": 13.018103895884023, + "parent_hash": "26b97d9a0f1cd4902ae67eb0361cc91dfcbac517c715add130f5767739ac22fd", + "profile_hash": "c0d93223fd828846c50d5bcb9ec2740583e4e424ee7a8108d37c97a150094301", + "total_score_sd": 11.834070501789366 + } + ], + "weights": [ + 0.15930283189149294, + 0.168233869783031, + 0.16819825924187087, + 0.16818069024377116, + 0.16810612075332335, + 0.16797822808651072 + ] + }, + "challenger_state": { + "drift_score": 0.013888256227233537, + "drift_threshold": 0.12, + "ensemble_hash": "3998dbc995c7a222b6b34536892eb79abfcf27282bf679a7177ff9539d1f2805", + "interval_learning_rate": 0.04, + "interval_scale": 1.0202013400267562, + "learning_rate": 0.25, + "long_decay": 0.03, + "long_loss_ewma": 8.242249944953999, + "minimum_drift_observations": 40, + "observations": 50, + "short_decay": 0.2, + "short_loss_ewma": 8.356720424078421, + "state_hash": "e8735ad2ed9bbb110415c0882bf2e942dde406a658fd584b5d5d4e29841873a7", + "status": "healthy", + "target_coverage": 0.9, + "weights": [ + 0.1726901759359644, + 0.13078316340900142, + 0.09577431634417387, + 0.10500586465848535, + 0.21457752265512625, + 0.2811689569972487 + ] + }, + "decision": "retained_incumbent:minimum_relative_improvement_not_met", + "generations": [ + { + "best_profile_hash": "c7a4cb7c5b0eeb744b6a4b507293afafbc809472c5aaddd14f68eda78d3356c5", + "best_train_score": { + "composite_loss": 8.244992808341305, + "games": 100, + "home_win_brier": 0.22926206280465272, + "interval_coverage": 0.92, + "interval_coverage_error": 0.020000000000000018, + "mean_joint_negative_log_likelihood": 7.746468682731998 + }, + "candidates_evaluated": 32, + "generation": 1 + }, + { + "best_profile_hash": "41239859f3e42113eda589e768b0d61a34d98dcec911a70132774db077b2f665", + "best_train_score": { + "composite_loss": 8.186941930417184, + "games": 100, + "home_win_brier": 0.22861016981167537, + "interval_coverage": 0.9, + "interval_coverage_error": 0.0, + "mean_joint_negative_log_likelihood": 7.729721590793833 + }, + "candidates_evaluated": 32, + "generation": 2 + }, + { + "best_profile_hash": "41239859f3e42113eda589e768b0d61a34d98dcec911a70132774db077b2f665", + "best_train_score": { + "composite_loss": 8.186941930417184, + "games": 100, + "home_win_brier": 0.22861016981167537, + "interval_coverage": 0.9, + "interval_coverage_error": 0.0, + "mean_joint_negative_log_likelihood": 7.729721590793833 + }, + "candidates_evaluated": 32, + "generation": 3 + }, + { + "best_profile_hash": "41239859f3e42113eda589e768b0d61a34d98dcec911a70132774db077b2f665", + "best_train_score": { + "composite_loss": 8.186941930417184, + "games": 100, + "home_win_brier": 0.22861016981167537, + "interval_coverage": 0.9, + "interval_coverage_error": 0.0, + "mean_joint_negative_log_likelihood": 7.729721590793833 + }, + "candidates_evaluated": 32, + "generation": 4 + }, + { + "best_profile_hash": "f0c50ab49baa7e660afddfb7e8979b224d34b7e1e51061967bc7c420e3abc67f", + "best_train_score": { + "composite_loss": 8.184622781251578, + "games": 100, + "home_win_brier": 0.23308474872372362, + "interval_coverage": 0.895, + "interval_coverage_error": 0.0050000000000000044, + "mean_joint_negative_log_likelihood": 7.708453283804132 + }, + "candidates_evaluated": 32, + "generation": 5 + }, + { + "best_profile_hash": "d283336f28c8bfedd347d047d6ea0f7f46cc810560a6c68068d61c04a67ed2c1", + "best_train_score": { + "composite_loss": 8.177288848514712, + "games": 100, + "home_win_brier": 0.22858760020536478, + "interval_coverage": 0.895, + "interval_coverage_error": 0.0050000000000000044, + "mean_joint_negative_log_likelihood": 7.710113648103983 + }, + "candidates_evaluated": 32, + "generation": 6 + }, + { + "best_profile_hash": "afb44b88704aab434182c745d9534462c135846c1314177112e6d2e05e054311", + "best_train_score": { + "composite_loss": 8.174250279460889, + "games": 100, + "home_win_brier": 0.2287034736025193, + "interval_coverage": 0.9, + "interval_coverage_error": 0.0, + "mean_joint_negative_log_likelihood": 7.716843332255849 + }, + "candidates_evaluated": 32, + "generation": 7 + }, + { + "best_profile_hash": "afb44b88704aab434182c745d9534462c135846c1314177112e6d2e05e054311", + "best_train_score": { + "composite_loss": 8.174250279460889, + "games": 100, + "home_win_brier": 0.2287034736025193, + "interval_coverage": 0.9, + "interval_coverage_error": 0.0, + "mean_joint_negative_log_likelihood": 7.716843332255849 + }, + "candidates_evaluated": 32, + "generation": 8 + } + ], + "incumbent_calibration_score": { + "composite_loss": 8.427984368402127, + "games": 50, + "home_win_brier": 0.23551834611431333, + "interval_coverage": 0.94, + "interval_coverage_error": 0.039999999999999925, + "mean_joint_negative_log_likelihood": 7.8769476761735 + }, + "incumbent_ensemble": { + "ensemble_hash": "52cc748b0f03d805bc32644c746152d71b8878f6cb3e9e2e6df533e3cb3718e2", + "evidence_status": "provisional_reference_anchor", + "league": "nfl", + "profiles": [ + { + "away_strength_exponent": 1.0, + "base_away_score": 21.8, + "base_home_score": 23.5, + "evidence_status": "provisional_reference_anchor", + "generation": 0, + "home_strength_exponent": 1.0, + "league": "nfl", + "margin_sd": 14.0, + "parent_hash": "", + "profile_hash": "2cf12a1c59550d09aad4853366ee45da48461195286aba2fc665167a2d0accd0", + "total_score_sd": 13.7 + } + ], + "weights": [ + 1.0 + ] + }, + "incumbent_state": { + "drift_score": 0.009386081910553939, + "drift_threshold": 0.12, + "ensemble_hash": "52cc748b0f03d805bc32644c746152d71b8878f6cb3e9e2e6df533e3cb3718e2", + "interval_learning_rate": 0.04, + "interval_scale": 0.923116346386637, + "learning_rate": 0.25, + "long_decay": 0.03, + "long_loss_ewma": 8.229562938300864, + "minimum_drift_observations": 40, + "observations": 50, + "short_decay": 0.2, + "short_loss_ewma": 8.306806290127815, + "state_hash": "c56bdf6245fc5a57c735db00e6d448c6670f5a96632121cbfa3e9d7e30dd0e91", + "status": "healthy", + "target_coverage": 0.9, + "weights": [ + 1.0 + ] + }, + "league": "nfl", + "promoted": false, + "relative_improvement": 0.008933830438470725, + "selected_ensemble": { + "ensemble_hash": "52cc748b0f03d805bc32644c746152d71b8878f6cb3e9e2e6df533e3cb3718e2", + "evidence_status": "provisional_reference_anchor", + "league": "nfl", + "profiles": [ + { + "away_strength_exponent": 1.0, + "base_away_score": 21.8, + "base_home_score": 23.5, + "evidence_status": "provisional_reference_anchor", + "generation": 0, + "home_strength_exponent": 1.0, + "league": "nfl", + "margin_sd": 14.0, + "parent_hash": "", + "profile_hash": "2cf12a1c59550d09aad4853366ee45da48461195286aba2fc665167a2d0accd0", + "total_score_sd": 13.7 + } + ], + "weights": [ + 1.0 + ] + }, + "selected_state": { + "drift_score": 0.009386081910553939, + "drift_threshold": 0.12, + "ensemble_hash": "52cc748b0f03d805bc32644c746152d71b8878f6cb3e9e2e6df533e3cb3718e2", + "interval_learning_rate": 0.04, + "interval_scale": 0.923116346386637, + "learning_rate": 0.25, + "long_decay": 0.03, + "long_loss_ewma": 8.229562938300864, + "minimum_drift_observations": 40, + "observations": 50, + "short_decay": 0.2, + "short_loss_ewma": 8.306806290127815, + "state_hash": "c56bdf6245fc5a57c735db00e6d448c6670f5a96632121cbfa3e9d7e30dd0e91", + "status": "healthy", + "target_coverage": 0.9, + "weights": [ + 1.0 + ] + } + }, + { + "challenger_calibration_score": { + "composite_loss": 4.7671157212113355, + "games": 50, + "home_win_brier": 0.2228624325270619, + "interval_coverage": 0.9, + "interval_coverage_error": 0.0, + "mean_joint_negative_log_likelihood": 4.321390856157212 + }, + "challenger_ensemble": { + "ensemble_hash": "b4268f4251d62981ee852625c817f48947e9e12aecc39da1b77d4ff869c86289", + "evidence_status": "chronological_calibration_challenger", + "league": "nhl", + "profiles": [ + { + "away_strength_exponent": 1.0, + "base_away_score": 2.95, + "base_home_score": 3.2, + "evidence_status": "provisional_reference_anchor", + "generation": 0, + "home_strength_exponent": 1.0, + "league": "nhl", + "margin_sd": 2.4, + "parent_hash": "", + "profile_hash": "dd2f407da7fec8a4017fec2f6e311c3010d4f874c1d156f9dc4a4d9ba25576c3", + "total_score_sd": 2.3 + }, + { + "away_strength_exponent": 1.3618435036484602, + "base_away_score": 2.8140096579368463, + "base_home_score": 2.9827709409299437, + "evidence_status": "train_search_candidate", + "generation": 6, + "home_strength_exponent": 1.0164249111888564, + "league": "nhl", + "margin_sd": 2.4067107458645474, + "parent_hash": "c0f59c1041dfffbcb83db90b24931ee0acf8315cd86d375cd903b1869716224f", + "profile_hash": "560b8c3bf885c09aba0c04204376e3091a28ca9006785a1e982d822fd4f20158", + "total_score_sd": 1.9217750431804201 + }, + { + "away_strength_exponent": 1.3918430671368498, + "base_away_score": 2.8395546024633056, + "base_home_score": 3.001160815782975, + "evidence_status": "train_search_candidate", + "generation": 8, + "home_strength_exponent": 0.9742513551643636, + "league": "nhl", + "margin_sd": 2.347437453084234, + "parent_hash": "7e6cc68f9f179e60b9ad230a27f90dbac843e9dfc33f83849a71a51a37077069", + "profile_hash": "b792aed5100d5cfef059e56fe733dc686f999130857572ef57702f98220e5374", + "total_score_sd": 1.9848807761955292 + }, + { + "away_strength_exponent": 1.3675649819385032, + "base_away_score": 2.67379397546478, + "base_home_score": 3.2244694632163426, + "evidence_status": "train_search_candidate", + "generation": 5, + "home_strength_exponent": 0.9979113276154296, + "league": "nhl", + "margin_sd": 2.315479052327515, + "parent_hash": "3e88d5bd7565e8748490af5a78d84b19a8534868a6331d4d1752f34943540487", + "profile_hash": "c0f59c1041dfffbcb83db90b24931ee0acf8315cd86d375cd903b1869716224f", + "total_score_sd": 1.8514937320015388 + }, + { + "away_strength_exponent": 1.365373323994108, + "base_away_score": 2.6479224044804157, + "base_home_score": 3.0420403168143237, + "evidence_status": "train_search_candidate", + "generation": 5, + "home_strength_exponent": 0.8190424527874777, + "league": "nhl", + "margin_sd": 2.469930832788603, + "parent_hash": "e20a271760ee2ecb08354c7be8fa7b0533bc22ccf66bb8691f131706d6d4193d", + "profile_hash": "16e8f92eddd415582a8ca2282946b318fe60e8845d896d93538b603bc8cd0555", + "total_score_sd": 1.754941135672797 + }, + { + "away_strength_exponent": 1.444774898245588, + "base_away_score": 2.8087224671677236, + "base_home_score": 2.900908465085074, + "evidence_status": "train_search_candidate", + "generation": 8, + "home_strength_exponent": 0.9764064324855087, + "league": "nhl", + "margin_sd": 2.4088026880950175, + "parent_hash": "560b8c3bf885c09aba0c04204376e3091a28ca9006785a1e982d822fd4f20158", + "profile_hash": "cbd5bb21fef457140e843a9455f9d3d073749dc621ad2bef307a40f02fe73c53", + "total_score_sd": 1.8330168778160287 + } + ], + "weights": [ + 0.16286673514032046, + 0.16755705994655176, + 0.16752559730854746, + 0.1673849214822694, + 0.16737507931387444, + 0.1672906068084365 + ] + }, + "challenger_state": { + "drift_score": 0.0, + "drift_threshold": 0.12, + "ensemble_hash": "b4268f4251d62981ee852625c817f48947e9e12aecc39da1b77d4ff869c86289", + "interval_learning_rate": 0.04, + "interval_scale": 1.0000000000000007, + "learning_rate": 0.25, + "long_decay": 0.03, + "long_loss_ewma": 4.686936849303858, + "minimum_drift_observations": 40, + "observations": 50, + "short_decay": 0.2, + "short_loss_ewma": 4.605010305685448, + "state_hash": "9e103f41a8cf8181c03bde20e2b260ff7daa3ca261c7c760e94c10dca68cc368", + "status": "healthy", + "target_coverage": 0.9, + "weights": [ + 0.10302384802689585, + 0.18136314556312605, + 0.15845422350790386, + 0.20162059073033012, + 0.19463378905316608, + 0.16090440311857798 + ] + }, + "decision": "promoted", + "generations": [ + { + "best_profile_hash": "c94b647e306de16066fb67ab8667ee126753d89c3b305a6b3165f8ca75169d55", + "best_train_score": { + "composite_loss": 4.712218702954687, + "games": 100, + "home_win_brier": 0.20103508188055405, + "interval_coverage": 0.905, + "interval_coverage_error": 0.0050000000000000044, + "mean_joint_negative_log_likelihood": 4.30014853919358 + }, + "candidates_evaluated": 32, + "generation": 1 + }, + { + "best_profile_hash": "92be3c6a04534df6f98bfb1f13e38f92f9d7a7a28942949b69feea86c3e87074", + "best_train_score": { + "composite_loss": 4.687654166540897, + "games": 100, + "home_win_brier": 0.19897799109894074, + "interval_coverage": 0.9, + "interval_coverage_error": 0.0, + "mean_joint_negative_log_likelihood": 4.289698184343015 + }, + "candidates_evaluated": 32, + "generation": 2 + }, + { + "best_profile_hash": "e20a271760ee2ecb08354c7be8fa7b0533bc22ccf66bb8691f131706d6d4193d", + "best_train_score": { + "composite_loss": 4.683921153937582, + "games": 100, + "home_win_brier": 0.19678729831368805, + "interval_coverage": 0.905, + "interval_coverage_error": 0.0050000000000000044, + "mean_joint_negative_log_likelihood": 4.280346557310206 + }, + "candidates_evaluated": 32, + "generation": 3 + }, + { + "best_profile_hash": "e20a271760ee2ecb08354c7be8fa7b0533bc22ccf66bb8691f131706d6d4193d", + "best_train_score": { + "composite_loss": 4.683921153937582, + "games": 100, + "home_win_brier": 0.19678729831368805, + "interval_coverage": 0.905, + "interval_coverage_error": 0.0050000000000000044, + "mean_joint_negative_log_likelihood": 4.280346557310206 + }, + "candidates_evaluated": 32, + "generation": 4 + }, + { + "best_profile_hash": "c0f59c1041dfffbcb83db90b24931ee0acf8315cd86d375cd903b1869716224f", + "best_train_score": { + "composite_loss": 4.659088406260525, + "games": 100, + "home_win_brier": 0.1968459811406281, + "interval_coverage": 0.9, + "interval_coverage_error": 0.0, + "mean_joint_negative_log_likelihood": 4.265396443979269 + }, + "candidates_evaluated": 32, + "generation": 5 + }, + { + "best_profile_hash": "560b8c3bf885c09aba0c04204376e3091a28ca9006785a1e982d822fd4f20158", + "best_train_score": { + "composite_loss": 4.654976924535752, + "games": 100, + "home_win_brier": 0.1966795654368007, + "interval_coverage": 0.9, + "interval_coverage_error": 0.0, + "mean_joint_negative_log_likelihood": 4.261617793662151 + }, + "candidates_evaluated": 32, + "generation": 6 + }, + { + "best_profile_hash": "560b8c3bf885c09aba0c04204376e3091a28ca9006785a1e982d822fd4f20158", + "best_train_score": { + "composite_loss": 4.654976924535752, + "games": 100, + "home_win_brier": 0.1966795654368007, + "interval_coverage": 0.9, + "interval_coverage_error": 0.0, + "mean_joint_negative_log_likelihood": 4.261617793662151 + }, + "candidates_evaluated": 32, + "generation": 7 + }, + { + "best_profile_hash": "560b8c3bf885c09aba0c04204376e3091a28ca9006785a1e982d822fd4f20158", + "best_train_score": { + "composite_loss": 4.654976924535752, + "games": 100, + "home_win_brier": 0.1966795654368007, + "interval_coverage": 0.9, + "interval_coverage_error": 0.0, + "mean_joint_negative_log_likelihood": 4.261617793662151 + }, + "candidates_evaluated": 32, + "generation": 8 + } + ], + "incumbent_calibration_score": { + "composite_loss": 4.831941943380519, + "games": 50, + "home_win_brier": 0.2199413705553162, + "interval_coverage": 0.91, + "interval_coverage_error": 0.010000000000000009, + "mean_joint_negative_log_likelihood": 4.372059202269887 + }, + "incumbent_ensemble": { + "ensemble_hash": "382ec0d5185a96f540f91839c8bd6328d408d70a8ec1f16352aa22553c05bcfb", + "evidence_status": "provisional_reference_anchor", + "league": "nhl", + "profiles": [ + { + "away_strength_exponent": 1.0, + "base_away_score": 2.95, + "base_home_score": 3.2, + "evidence_status": "provisional_reference_anchor", + "generation": 0, + "home_strength_exponent": 1.0, + "league": "nhl", + "margin_sd": 2.4, + "parent_hash": "", + "profile_hash": "dd2f407da7fec8a4017fec2f6e311c3010d4f874c1d156f9dc4a4d9ba25576c3", + "total_score_sd": 2.3 + } + ], + "weights": [ + 1.0 + ] + }, + "incumbent_state": { + "drift_score": 0.0, + "drift_threshold": 0.12, + "ensemble_hash": "382ec0d5185a96f540f91839c8bd6328d408d70a8ec1f16352aa22553c05bcfb", + "interval_learning_rate": 0.04, + "interval_scale": 0.9801986733067568, + "learning_rate": 0.25, + "long_decay": 0.03, + "long_loss_ewma": 4.723961751695468, + "minimum_drift_observations": 40, + "observations": 50, + "short_decay": 0.2, + "short_loss_ewma": 4.6298625684220225, + "state_hash": "7ce259e0d6f333426389c555b9f568c802b7265ae0ac4a6a55ba9f98184c8ee5", + "status": "healthy", + "target_coverage": 0.9, + "weights": [ + 1.0 + ] + }, + "league": "nhl", + "promoted": true, + "relative_improvement": 0.013416183995751795, + "selected_ensemble": { + "ensemble_hash": "b4268f4251d62981ee852625c817f48947e9e12aecc39da1b77d4ff869c86289", + "evidence_status": "chronological_calibration_challenger", + "league": "nhl", + "profiles": [ + { + "away_strength_exponent": 1.0, + "base_away_score": 2.95, + "base_home_score": 3.2, + "evidence_status": "provisional_reference_anchor", + "generation": 0, + "home_strength_exponent": 1.0, + "league": "nhl", + "margin_sd": 2.4, + "parent_hash": "", + "profile_hash": "dd2f407da7fec8a4017fec2f6e311c3010d4f874c1d156f9dc4a4d9ba25576c3", + "total_score_sd": 2.3 + }, + { + "away_strength_exponent": 1.3618435036484602, + "base_away_score": 2.8140096579368463, + "base_home_score": 2.9827709409299437, + "evidence_status": "train_search_candidate", + "generation": 6, + "home_strength_exponent": 1.0164249111888564, + "league": "nhl", + "margin_sd": 2.4067107458645474, + "parent_hash": "c0f59c1041dfffbcb83db90b24931ee0acf8315cd86d375cd903b1869716224f", + "profile_hash": "560b8c3bf885c09aba0c04204376e3091a28ca9006785a1e982d822fd4f20158", + "total_score_sd": 1.9217750431804201 + }, + { + "away_strength_exponent": 1.3918430671368498, + "base_away_score": 2.8395546024633056, + "base_home_score": 3.001160815782975, + "evidence_status": "train_search_candidate", + "generation": 8, + "home_strength_exponent": 0.9742513551643636, + "league": "nhl", + "margin_sd": 2.347437453084234, + "parent_hash": "7e6cc68f9f179e60b9ad230a27f90dbac843e9dfc33f83849a71a51a37077069", + "profile_hash": "b792aed5100d5cfef059e56fe733dc686f999130857572ef57702f98220e5374", + "total_score_sd": 1.9848807761955292 + }, + { + "away_strength_exponent": 1.3675649819385032, + "base_away_score": 2.67379397546478, + "base_home_score": 3.2244694632163426, + "evidence_status": "train_search_candidate", + "generation": 5, + "home_strength_exponent": 0.9979113276154296, + "league": "nhl", + "margin_sd": 2.315479052327515, + "parent_hash": "3e88d5bd7565e8748490af5a78d84b19a8534868a6331d4d1752f34943540487", + "profile_hash": "c0f59c1041dfffbcb83db90b24931ee0acf8315cd86d375cd903b1869716224f", + "total_score_sd": 1.8514937320015388 + }, + { + "away_strength_exponent": 1.365373323994108, + "base_away_score": 2.6479224044804157, + "base_home_score": 3.0420403168143237, + "evidence_status": "train_search_candidate", + "generation": 5, + "home_strength_exponent": 0.8190424527874777, + "league": "nhl", + "margin_sd": 2.469930832788603, + "parent_hash": "e20a271760ee2ecb08354c7be8fa7b0533bc22ccf66bb8691f131706d6d4193d", + "profile_hash": "16e8f92eddd415582a8ca2282946b318fe60e8845d896d93538b603bc8cd0555", + "total_score_sd": 1.754941135672797 + }, + { + "away_strength_exponent": 1.444774898245588, + "base_away_score": 2.8087224671677236, + "base_home_score": 2.900908465085074, + "evidence_status": "train_search_candidate", + "generation": 8, + "home_strength_exponent": 0.9764064324855087, + "league": "nhl", + "margin_sd": 2.4088026880950175, + "parent_hash": "560b8c3bf885c09aba0c04204376e3091a28ca9006785a1e982d822fd4f20158", + "profile_hash": "cbd5bb21fef457140e843a9455f9d3d073749dc621ad2bef307a40f02fe73c53", + "total_score_sd": 1.8330168778160287 + } + ], + "weights": [ + 0.16286673514032046, + 0.16755705994655176, + 0.16752559730854746, + 0.1673849214822694, + 0.16737507931387444, + 0.1672906068084365 + ] + }, + "selected_state": { + "drift_score": 0.0, + "drift_threshold": 0.12, + "ensemble_hash": "b4268f4251d62981ee852625c817f48947e9e12aecc39da1b77d4ff869c86289", + "interval_learning_rate": 0.04, + "interval_scale": 1.0000000000000007, + "learning_rate": 0.25, + "long_decay": 0.03, + "long_loss_ewma": 4.686936849303858, + "minimum_drift_observations": 40, + "observations": 50, + "short_decay": 0.2, + "short_loss_ewma": 4.605010305685448, + "state_hash": "9e103f41a8cf8181c03bde20e2b260ff7daa3ca261c7c760e94c10dca68cc368", + "status": "healthy", + "target_coverage": 0.9, + "weights": [ + 0.10302384802689585, + 0.18136314556312605, + 0.15845422350790386, + 0.20162059073033012, + 0.19463378905316608, + 0.16090440311857798 + ] + } + }, + { + "challenger_calibration_score": { + "composite_loss": 8.379522034337166, + "games": 50, + "home_win_brier": 0.16069882492885665, + "interval_coverage": 0.9, + "interval_coverage_error": 0.0, + "mean_joint_negative_log_likelihood": 8.058124384479452 + }, + "challenger_ensemble": { + "ensemble_hash": "369bd1fab70c4fe9c443214b61e5bb1297947388fae8402061d599b6dc5a975d", + "evidence_status": "chronological_calibration_challenger", + "league": "wnba", + "profiles": [ + { + "away_strength_exponent": 1.0, + "base_away_score": 80.0, + "base_home_score": 82.0, + "evidence_status": "provisional_reference_anchor", + "generation": 0, + "home_strength_exponent": 1.0, + "league": "wnba", + "margin_sd": 14.0, + "parent_hash": "", + "profile_hash": "a9efcb5f3219e8e9a3df4fdce3b3f767165faa82fe41cf874bdb0716548bb4d9", + "total_score_sd": 17.5 + }, + { + "away_strength_exponent": 1.0251595474377284, + "base_away_score": 72.11403431035531, + "base_home_score": 71.69042161368746, + "evidence_status": "train_search_candidate", + "generation": 7, + "home_strength_exponent": 1.0938685005129976, + "league": "wnba", + "margin_sd": 12.553668307327136, + "parent_hash": "468447632d3bbfb0286c4335ad436b5f32c841251c8614e5f2096d0b5dbf0a3f", + "profile_hash": "af6c47cb9cc880f80388c052e42e219dc4055b7d2c8b6e1346332703b49299e5", + "total_score_sd": 13.255930520302046 + }, + { + "away_strength_exponent": 1.0981336628870195, + "base_away_score": 71.11342291633342, + "base_home_score": 73.62693210352198, + "evidence_status": "train_search_candidate", + "generation": 7, + "home_strength_exponent": 1.1420227255585949, + "league": "wnba", + "margin_sd": 12.523765694931425, + "parent_hash": "7d2a641dbd76ac6fde3dfa526718e79569f129327958d60c3dfafc464b75af97", + "profile_hash": "b83f84e526bc77753ff0561968a83ca3f84f62dad23a54b08c2869b88226c810", + "total_score_sd": 13.124434195991878 + }, + { + "away_strength_exponent": 0.9827239039722672, + "base_away_score": 73.07586506043492, + "base_home_score": 73.49995936972549, + "evidence_status": "train_search_candidate", + "generation": 7, + "home_strength_exponent": 1.060463397601755, + "league": "wnba", + "margin_sd": 11.322075003472191, + "parent_hash": "02a8002ff7041946836ce1a1fcb8f53773f3b838801f82797911d7fbf267efec", + "profile_hash": "6b7ed5b7bfddee7d6ec43ca6649a0fd953a0954c5a818b269d146c55b54ea0e2", + "total_score_sd": 13.178399018766651 + }, + { + "away_strength_exponent": 0.9391411924229954, + "base_away_score": 73.82314278504273, + "base_home_score": 72.09561563722342, + "evidence_status": "train_search_candidate", + "generation": 6, + "home_strength_exponent": 1.1340765001951492, + "league": "wnba", + "margin_sd": 12.028397269421774, + "parent_hash": "ab7556213fe56f2a8c712da7df880059839437bfee109b7996749fcea6802355", + "profile_hash": "5fb5c12d474e17f2c2719e637aa37a3fb53d24a5dda665aa15e1c8ce0cc9961b", + "total_score_sd": 14.119914546223248 + }, + { + "away_strength_exponent": 1.0428759542867176, + "base_away_score": 73.34588663690329, + "base_home_score": 70.67653549724727, + "evidence_status": "train_search_candidate", + "generation": 6, + "home_strength_exponent": 1.177066395306101, + "league": "wnba", + "margin_sd": 12.30699982423615, + "parent_hash": "7d2a641dbd76ac6fde3dfa526718e79569f129327958d60c3dfafc464b75af97", + "profile_hash": "468447632d3bbfb0286c4335ad436b5f32c841251c8614e5f2096d0b5dbf0a3f", + "total_score_sd": 13.296762898729602 + } + ], + "weights": [ + 0.14546085482667262, + 0.1713210376892588, + 0.17089076167696657, + 0.1708509658434923, + 0.17074437378062704, + 0.17073200618298265 + ] + }, + "challenger_state": { + "drift_score": 0.0, + "drift_threshold": 0.12, + "ensemble_hash": "369bd1fab70c4fe9c443214b61e5bb1297947388fae8402061d599b6dc5a975d", + "interval_learning_rate": 0.04, + "interval_scale": 1.0, + "learning_rate": 0.25, + "long_decay": 0.03, + "long_loss_ewma": 8.093222299641932, + "minimum_drift_observations": 40, + "observations": 50, + "short_decay": 0.2, + "short_loss_ewma": 8.024674327270455, + "state_hash": "b5389b73b296d8ff3307fa1679ea4534b3e0cdb82aba2cf0f5a58625c2587f88", + "status": "healthy", + "target_coverage": 0.9, + "weights": [ + 1.500943057167728e-05, + 0.2966973670938307, + 0.07172853425048047, + 0.11683643807084605, + 0.25589695988228717, + 0.25882569127198385 + ] + }, + "decision": "promoted", + "generations": [ + { + "best_profile_hash": "f044c42412e598124ded94ae81c65bf1fff0dbaec09540c8e0a09b41317a6ee3", + "best_train_score": { + "composite_loss": 8.262537971556736, + "games": 100, + "home_win_brier": 0.11956899659901453, + "interval_coverage": 0.93, + "interval_coverage_error": 0.030000000000000027, + "mean_joint_negative_log_likelihood": 7.963399978358707 + }, + "candidates_evaluated": 32, + "generation": 1 + }, + { + "best_profile_hash": "635aaf1f5fe3d09a13232c075cbb713e42ee920b9469346693b2b127f6d6f02c", + "best_train_score": { + "composite_loss": 8.203497289558689, + "games": 100, + "home_win_brier": 0.12021767125431083, + "interval_coverage": 0.91, + "interval_coverage_error": 0.010000000000000009, + "mean_joint_negative_log_likelihood": 7.943061947050068 + }, + "candidates_evaluated": 32, + "generation": 2 + }, + { + "best_profile_hash": "82f9b0b481716af3737e7fa95a906b7e18fedb39acf1b0790ad359b8e955d1bb", + "best_train_score": { + "composite_loss": 8.198183950694297, + "games": 100, + "home_win_brier": 0.12180829510380681, + "interval_coverage": 0.905, + "interval_coverage_error": 0.0050000000000000044, + "mean_joint_negative_log_likelihood": 7.944567360486684 + }, + "candidates_evaluated": 32, + "generation": 3 + }, + { + "best_profile_hash": "82f9b0b481716af3737e7fa95a906b7e18fedb39acf1b0790ad359b8e955d1bb", + "best_train_score": { + "composite_loss": 8.198183950694297, + "games": 100, + "home_win_brier": 0.12180829510380681, + "interval_coverage": 0.905, + "interval_coverage_error": 0.0050000000000000044, + "mean_joint_negative_log_likelihood": 7.944567360486684 + }, + "candidates_evaluated": 32, + "generation": 4 + }, + { + "best_profile_hash": "ab7556213fe56f2a8c712da7df880059839437bfee109b7996749fcea6802355", + "best_train_score": { + "composite_loss": 8.178515753524696, + "games": 100, + "home_win_brier": 0.12331051871446606, + "interval_coverage": 0.9, + "interval_coverage_error": 0.0, + "mean_joint_negative_log_likelihood": 7.931894716095764 + }, + "candidates_evaluated": 32, + "generation": 5 + }, + { + "best_profile_hash": "5fb5c12d474e17f2c2719e637aa37a3fb53d24a5dda665aa15e1c8ce0cc9961b", + "best_train_score": { + "composite_loss": 8.15633041978037, + "games": 100, + "home_win_brier": 0.12327843697814737, + "interval_coverage": 0.905, + "interval_coverage_error": 0.0050000000000000044, + "mean_joint_negative_log_likelihood": 7.8997735458240745 + }, + "candidates_evaluated": 32, + "generation": 6 + }, + { + "best_profile_hash": "af6c47cb9cc880f80388c052e42e219dc4055b7d2c8b6e1346332703b49299e5", + "best_train_score": { + "composite_loss": 8.142843772242365, + "games": 100, + "home_win_brier": 0.12013774010797616, + "interval_coverage": 0.9, + "interval_coverage_error": 0.0, + "mean_joint_negative_log_likelihood": 7.902568292026413 + }, + "candidates_evaluated": 32, + "generation": 7 + }, + { + "best_profile_hash": "af6c47cb9cc880f80388c052e42e219dc4055b7d2c8b6e1346332703b49299e5", + "best_train_score": { + "composite_loss": 8.142843772242365, + "games": 100, + "home_win_brier": 0.12013774010797616, + "interval_coverage": 0.9, + "interval_coverage_error": 0.0, + "mean_joint_negative_log_likelihood": 7.902568292026413 + }, + "candidates_evaluated": 32, + "generation": 8 + } + ], + "incumbent_calibration_score": { + "composite_loss": 9.188926654018786, + "games": 50, + "home_win_brier": 0.16577671914460065, + "interval_coverage": 0.87, + "interval_coverage_error": 0.030000000000000027, + "mean_joint_negative_log_likelihood": 8.797373215729584 + }, + "incumbent_ensemble": { + "ensemble_hash": "92022b12c8dd03d4fb005a8a84b4041b3370ee4e401e2e281230d8f6c3b3d284", + "evidence_status": "provisional_reference_anchor", + "league": "wnba", + "profiles": [ + { + "away_strength_exponent": 1.0, + "base_away_score": 80.0, + "base_home_score": 82.0, + "evidence_status": "provisional_reference_anchor", + "generation": 0, + "home_strength_exponent": 1.0, + "league": "wnba", + "margin_sd": 14.0, + "parent_hash": "", + "profile_hash": "a9efcb5f3219e8e9a3df4fdce3b3f767165faa82fe41cf874bdb0716548bb4d9", + "total_score_sd": 17.5 + } + ], + "weights": [ + 1.0 + ] + }, + "incumbent_state": { + "drift_score": 0.0, + "drift_threshold": 0.12, + "ensemble_hash": "92022b12c8dd03d4fb005a8a84b4041b3370ee4e401e2e281230d8f6c3b3d284", + "interval_learning_rate": 0.04, + "interval_scale": 1.06183654654536, + "learning_rate": 0.25, + "long_decay": 0.03, + "long_loss_ewma": 8.828494403135926, + "minimum_drift_observations": 40, + "observations": 50, + "short_decay": 0.2, + "short_loss_ewma": 8.776294316432912, + "state_hash": "bb00abb17fee66e3eb9152039b5e33447479ed0e079dd11c90c8fca060eb8d21", + "status": "healthy", + "target_coverage": 0.9, + "weights": [ + 1.0 + ] + }, + "league": "wnba", + "promoted": true, + "relative_improvement": 0.08808478401855853, + "selected_ensemble": { + "ensemble_hash": "369bd1fab70c4fe9c443214b61e5bb1297947388fae8402061d599b6dc5a975d", + "evidence_status": "chronological_calibration_challenger", + "league": "wnba", + "profiles": [ + { + "away_strength_exponent": 1.0, + "base_away_score": 80.0, + "base_home_score": 82.0, + "evidence_status": "provisional_reference_anchor", + "generation": 0, + "home_strength_exponent": 1.0, + "league": "wnba", + "margin_sd": 14.0, + "parent_hash": "", + "profile_hash": "a9efcb5f3219e8e9a3df4fdce3b3f767165faa82fe41cf874bdb0716548bb4d9", + "total_score_sd": 17.5 + }, + { + "away_strength_exponent": 1.0251595474377284, + "base_away_score": 72.11403431035531, + "base_home_score": 71.69042161368746, + "evidence_status": "train_search_candidate", + "generation": 7, + "home_strength_exponent": 1.0938685005129976, + "league": "wnba", + "margin_sd": 12.553668307327136, + "parent_hash": "468447632d3bbfb0286c4335ad436b5f32c841251c8614e5f2096d0b5dbf0a3f", + "profile_hash": "af6c47cb9cc880f80388c052e42e219dc4055b7d2c8b6e1346332703b49299e5", + "total_score_sd": 13.255930520302046 + }, + { + "away_strength_exponent": 1.0981336628870195, + "base_away_score": 71.11342291633342, + "base_home_score": 73.62693210352198, + "evidence_status": "train_search_candidate", + "generation": 7, + "home_strength_exponent": 1.1420227255585949, + "league": "wnba", + "margin_sd": 12.523765694931425, + "parent_hash": "7d2a641dbd76ac6fde3dfa526718e79569f129327958d60c3dfafc464b75af97", + "profile_hash": "b83f84e526bc77753ff0561968a83ca3f84f62dad23a54b08c2869b88226c810", + "total_score_sd": 13.124434195991878 + }, + { + "away_strength_exponent": 0.9827239039722672, + "base_away_score": 73.07586506043492, + "base_home_score": 73.49995936972549, + "evidence_status": "train_search_candidate", + "generation": 7, + "home_strength_exponent": 1.060463397601755, + "league": "wnba", + "margin_sd": 11.322075003472191, + "parent_hash": "02a8002ff7041946836ce1a1fcb8f53773f3b838801f82797911d7fbf267efec", + "profile_hash": "6b7ed5b7bfddee7d6ec43ca6649a0fd953a0954c5a818b269d146c55b54ea0e2", + "total_score_sd": 13.178399018766651 + }, + { + "away_strength_exponent": 0.9391411924229954, + "base_away_score": 73.82314278504273, + "base_home_score": 72.09561563722342, + "evidence_status": "train_search_candidate", + "generation": 6, + "home_strength_exponent": 1.1340765001951492, + "league": "wnba", + "margin_sd": 12.028397269421774, + "parent_hash": "ab7556213fe56f2a8c712da7df880059839437bfee109b7996749fcea6802355", + "profile_hash": "5fb5c12d474e17f2c2719e637aa37a3fb53d24a5dda665aa15e1c8ce0cc9961b", + "total_score_sd": 14.119914546223248 + }, + { + "away_strength_exponent": 1.0428759542867176, + "base_away_score": 73.34588663690329, + "base_home_score": 70.67653549724727, + "evidence_status": "train_search_candidate", + "generation": 6, + "home_strength_exponent": 1.177066395306101, + "league": "wnba", + "margin_sd": 12.30699982423615, + "parent_hash": "7d2a641dbd76ac6fde3dfa526718e79569f129327958d60c3dfafc464b75af97", + "profile_hash": "468447632d3bbfb0286c4335ad436b5f32c841251c8614e5f2096d0b5dbf0a3f", + "total_score_sd": 13.296762898729602 + } + ], + "weights": [ + 0.14546085482667262, + 0.1713210376892588, + 0.17089076167696657, + 0.1708509658434923, + 0.17074437378062704, + 0.17073200618298265 + ] + }, + "selected_state": { + "drift_score": 0.0, + "drift_threshold": 0.12, + "ensemble_hash": "369bd1fab70c4fe9c443214b61e5bb1297947388fae8402061d599b6dc5a975d", + "interval_learning_rate": 0.04, + "interval_scale": 1.0, + "learning_rate": 0.25, + "long_decay": 0.03, + "long_loss_ewma": 8.093222299641932, + "minimum_drift_observations": 40, + "observations": 50, + "short_decay": 0.2, + "short_loss_ewma": 8.024674327270455, + "state_hash": "b5389b73b296d8ff3307fa1679ea4534b3e0cdb82aba2cf0f5a58625c2587f88", + "status": "healthy", + "target_coverage": 0.9, + "weights": [ + 1.500943057167728e-05, + 0.2966973670938307, + 0.07172853425048047, + 0.11683643807084605, + 0.25589695988228717, + 0.25882569127198385 + ] + } + } + ], + "status": "chronological_calibration_only_not_real_world_certification" +} diff --git a/reports/autonomy_v3_fixture/shadow_adapted_bundle.json b/reports/autonomy_v3_fixture/shadow_adapted_bundle.json new file mode 100644 index 0000000..b9b5623 --- /dev/null +++ b/reports/autonomy_v3_fixture/shadow_adapted_bundle.json @@ -0,0 +1,937 @@ +{ + "bundle_hash": "7f1903911221d44a29bbed34a9e13e47e25f0bf8c1ccf3ba880dbc2c2d03722a", + "champions": [ + { + "ensemble": { + "ensemble_hash": "bb0b6a4184ebbf8489abaf3dc3df077d280c14d888340b59c2871215f6df8360", + "evidence_status": "provisional_reference_anchor", + "league": "mlb", + "profiles": [ + { + "away_strength_exponent": 1.0, + "base_away_score": 4.3, + "base_home_score": 4.55, + "evidence_status": "provisional_reference_anchor", + "generation": 0, + "home_strength_exponent": 1.0, + "league": "mlb", + "margin_sd": 4.4, + "parent_hash": "", + "profile_hash": "ed4a36ddf530bc1e400bae06859869e1b8b9bf4c542f53a193c39d949638d53e", + "total_score_sd": 4.6 + } + ], + "weights": [ + 1.0 + ] + }, + "league": "mlb", + "promoted": false, + "rollback_ensemble": { + "ensemble_hash": "bb0b6a4184ebbf8489abaf3dc3df077d280c14d888340b59c2871215f6df8360", + "evidence_status": "provisional_reference_anchor", + "league": "mlb", + "profiles": [ + { + "away_strength_exponent": 1.0, + "base_away_score": 4.3, + "base_home_score": 4.55, + "evidence_status": "provisional_reference_anchor", + "generation": 0, + "home_strength_exponent": 1.0, + "league": "mlb", + "margin_sd": 4.4, + "parent_hash": "", + "profile_hash": "ed4a36ddf530bc1e400bae06859869e1b8b9bf4c542f53a193c39d949638d53e", + "total_score_sd": 4.6 + } + ], + "weights": [ + 1.0 + ] + }, + "rollback_state": { + "drift_score": 0.050688478956998094, + "drift_threshold": 0.12, + "ensemble_hash": "bb0b6a4184ebbf8489abaf3dc3df077d280c14d888340b59c2871215f6df8360", + "interval_learning_rate": 0.04, + "interval_scale": 0.8869204367171584, + "learning_rate": 0.25, + "long_decay": 0.03, + "long_loss_ewma": 5.59425099372921, + "minimum_drift_observations": 40, + "observations": 50, + "short_decay": 0.2, + "short_loss_ewma": 5.8778150675050185, + "state_hash": "a23ac9f2ccc36db8368dcd341b0025f12d6d2b188c95ca9013b1dc61b461e78a", + "status": "healthy", + "target_coverage": 0.9, + "weights": [ + 1.0 + ] + }, + "state": { + "drift_score": 0.0, + "drift_threshold": 0.12, + "ensemble_hash": "bb0b6a4184ebbf8489abaf3dc3df077d280c14d888340b59c2871215f6df8360", + "interval_learning_rate": 0.04, + "interval_scale": 0.8187307530779836, + "learning_rate": 0.25, + "long_decay": 0.03, + "long_loss_ewma": 5.772593588641054, + "minimum_drift_observations": 40, + "observations": 90, + "short_decay": 0.2, + "short_loss_ewma": 5.757266428023099, + "state_hash": "93fbe3e1a46c9dfd370e304f2ad7ef6c236dce229fc2296d7093ef4c56f780c8", + "status": "healthy", + "target_coverage": 0.9, + "weights": [ + 1.0 + ] + } + }, + { + "ensemble": { + "ensemble_hash": "60e7da6020d67de678c0d044f300847fde90bf16161917510b08c83bfd5dbe96", + "evidence_status": "chronological_calibration_challenger", + "league": "nba", + "profiles": [ + { + "away_strength_exponent": 1.0, + "base_away_score": 113.0, + "base_home_score": 116.0, + "evidence_status": "provisional_reference_anchor", + "generation": 0, + "home_strength_exponent": 1.0, + "league": "nba", + "margin_sd": 15.0, + "parent_hash": "", + "profile_hash": "1fd107dd93cec7f199c4979e7334f2ad2c7e7d61e6e5d3fa7a7c6741138e40d1", + "total_score_sd": 20.0 + }, + { + "away_strength_exponent": 0.9632460782428364, + "base_away_score": 100.41120048415034, + "base_home_score": 101.40842755264546, + "evidence_status": "train_search_candidate", + "generation": 8, + "home_strength_exponent": 1.1035181164801218, + "league": "nba", + "margin_sd": 13.736474631089353, + "parent_hash": "d4336fc445a68de29aec9f851052ee18e00a6c18e7198fda34d832e4a727eafd", + "profile_hash": "6dabbdf2c5066b2dc80eb6e6088fbe21f308472a7241c3d76a36ea7ae27b0dc9", + "total_score_sd": 15.530808786290953 + }, + { + "away_strength_exponent": 0.9299864332685756, + "base_away_score": 99.12089973217256, + "base_home_score": 102.60460634910284, + "evidence_status": "train_search_candidate", + "generation": 7, + "home_strength_exponent": 1.0374456736407256, + "league": "nba", + "margin_sd": 14.476747331295748, + "parent_hash": "d4336fc445a68de29aec9f851052ee18e00a6c18e7198fda34d832e4a727eafd", + "profile_hash": "2c3b39a22523f4f245dcd1afe8fcfc54544020073f6153bd180207f50ca0fa67", + "total_score_sd": 15.986652973245818 + }, + { + "away_strength_exponent": 0.9314639467993735, + "base_away_score": 97.55462255575519, + "base_home_score": 100.60692741111606, + "evidence_status": "train_search_candidate", + "generation": 6, + "home_strength_exponent": 1.0481985521298671, + "league": "nba", + "margin_sd": 13.538006034660736, + "parent_hash": "1f29dc288a020ac6049537b27f5d91603e2e548bb8639e20fcd8c9527fa9b748", + "profile_hash": "d4336fc445a68de29aec9f851052ee18e00a6c18e7198fda34d832e4a727eafd", + "total_score_sd": 16.76275018651739 + }, + { + "away_strength_exponent": 0.8862112258764434, + "base_away_score": 100.34400852330234, + "base_home_score": 99.90111885665915, + "evidence_status": "train_search_candidate", + "generation": 7, + "home_strength_exponent": 0.9978232275893777, + "league": "nba", + "margin_sd": 13.709770977740531, + "parent_hash": "4cc9fa6c37e99b992fc73dec931c77bbd815b6a14538deb5d0471d5dfe18a3b3", + "profile_hash": "2b9ae85a9ff77cddc6e95f95feff238a5aad863eb1b5a6d4d0c1682859f773cf", + "total_score_sd": 14.728999472122727 + }, + { + "away_strength_exponent": 0.9168426579759286, + "base_away_score": 98.08693111402029, + "base_home_score": 100.24510182059633, + "evidence_status": "train_search_candidate", + "generation": 8, + "home_strength_exponent": 1.029985233214439, + "league": "nba", + "margin_sd": 13.914503225184157, + "parent_hash": "2c3b39a22523f4f245dcd1afe8fcfc54544020073f6153bd180207f50ca0fa67", + "profile_hash": "c24f67abab0bf80575320bbbed67381f38b4de2aeee0cfe291bee9e80ceba669", + "total_score_sd": 16.713986694619834 + } + ], + "weights": [ + 0.12893045230055183, + 0.17538891395996567, + 0.1747196187644406, + 0.17386258202490054, + 0.17362605222135616, + 0.17347238072878526 + ] + }, + "league": "nba", + "promoted": true, + "rollback_ensemble": { + "ensemble_hash": "938868d720f17e6b997358c0a52254af174a1b3081193720225c3ca7e3eda934", + "evidence_status": "provisional_reference_anchor", + "league": "nba", + "profiles": [ + { + "away_strength_exponent": 1.0, + "base_away_score": 113.0, + "base_home_score": 116.0, + "evidence_status": "provisional_reference_anchor", + "generation": 0, + "home_strength_exponent": 1.0, + "league": "nba", + "margin_sd": 15.0, + "parent_hash": "", + "profile_hash": "1fd107dd93cec7f199c4979e7334f2ad2c7e7d61e6e5d3fa7a7c6741138e40d1", + "total_score_sd": 20.0 + } + ], + "weights": [ + 1.0 + ] + }, + "rollback_state": { + "drift_score": 0.0, + "drift_threshold": 0.12, + "ensemble_hash": "938868d720f17e6b997358c0a52254af174a1b3081193720225c3ca7e3eda934", + "interval_learning_rate": 0.04, + "interval_scale": 1.1502737988572285, + "learning_rate": 0.25, + "long_decay": 0.03, + "long_loss_ewma": 9.584379101566844, + "minimum_drift_observations": 40, + "observations": 50, + "short_decay": 0.2, + "short_loss_ewma": 9.00479793127273, + "state_hash": "8938e17f435856b6ce780263706e625583f33790ccd311fee85a1564aa1988dd", + "status": "healthy", + "target_coverage": 0.9, + "weights": [ + 1.0 + ] + }, + "state": { + "drift_score": 0.0, + "drift_threshold": 0.12, + "ensemble_hash": "60e7da6020d67de678c0d044f300847fde90bf16161917510b08c83bfd5dbe96", + "interval_learning_rate": 0.04, + "interval_scale": 0.9417645335842497, + "learning_rate": 0.25, + "long_decay": 0.03, + "long_loss_ewma": 8.228777215687872, + "minimum_drift_observations": 40, + "observations": 90, + "short_decay": 0.2, + "short_loss_ewma": 7.797677877182591, + "state_hash": "50f9153230694ee41939e2c093e5fec4dc1a79666b04a7d65f8ac45dcdf6ff9d", + "status": "healthy", + "target_coverage": 0.9, + "weights": [ + 1.1033602702294112e-10, + 0.5893251182400253, + 0.16346399073534934, + 0.08860378788601675, + 0.08503255688672598, + 0.07357454614154653 + ] + } + }, + { + "ensemble": { + "ensemble_hash": "110e0a2b1d912b65c9c8e2421251514d29e036503be2191a6a19c5418d16b42a", + "evidence_status": "provisional_reference_anchor", + "league": "ncaaf", + "profiles": [ + { + "away_strength_exponent": 1.0, + "base_away_score": 26.5, + "base_home_score": 29.0, + "evidence_status": "provisional_reference_anchor", + "generation": 0, + "home_strength_exponent": 1.0, + "league": "ncaaf", + "margin_sd": 18.0, + "parent_hash": "", + "profile_hash": "6c3ad3401f75976c8a0cc1301c7aa9d9ba28a4d78b9bd5ee2432bf29e58493f7", + "total_score_sd": 15.7 + } + ], + "weights": [ + 1.0 + ] + }, + "league": "ncaaf", + "promoted": false, + "rollback_ensemble": { + "ensemble_hash": "110e0a2b1d912b65c9c8e2421251514d29e036503be2191a6a19c5418d16b42a", + "evidence_status": "provisional_reference_anchor", + "league": "ncaaf", + "profiles": [ + { + "away_strength_exponent": 1.0, + "base_away_score": 26.5, + "base_home_score": 29.0, + "evidence_status": "provisional_reference_anchor", + "generation": 0, + "home_strength_exponent": 1.0, + "league": "ncaaf", + "margin_sd": 18.0, + "parent_hash": "", + "profile_hash": "6c3ad3401f75976c8a0cc1301c7aa9d9ba28a4d78b9bd5ee2432bf29e58493f7", + "total_score_sd": 15.7 + } + ], + "weights": [ + 1.0 + ] + }, + "rollback_state": { + "drift_score": 0.053373359494810986, + "drift_threshold": 0.12, + "ensemble_hash": "110e0a2b1d912b65c9c8e2421251514d29e036503be2191a6a19c5418d16b42a", + "interval_learning_rate": 0.04, + "interval_scale": 0.9607894391523236, + "learning_rate": 0.25, + "long_decay": 0.03, + "long_loss_ewma": 8.821992883967974, + "minimum_drift_observations": 40, + "observations": 50, + "short_decay": 0.2, + "short_loss_ewma": 9.292852281624661, + "state_hash": "6490ed8bb46c72d47e410317f52bba03128f9076dbe23088cbd0748735b8b91b", + "status": "healthy", + "target_coverage": 0.9, + "weights": [ + 1.0 + ] + }, + "state": { + "drift_score": 0.0035082198698251758, + "drift_threshold": 0.12, + "ensemble_hash": "110e0a2b1d912b65c9c8e2421251514d29e036503be2191a6a19c5418d16b42a", + "interval_learning_rate": 0.04, + "interval_scale": 0.8869204367171581, + "learning_rate": 0.25, + "long_decay": 0.03, + "long_loss_ewma": 8.635516325467263, + "minimum_drift_observations": 40, + "observations": 90, + "short_decay": 0.2, + "short_loss_ewma": 8.665811615426467, + "state_hash": "758c2e91c5db77d5717b6af5fe29b9bf40a5d2641f67a1fd31a05ed7a8e2a272", + "status": "healthy", + "target_coverage": 0.9, + "weights": [ + 1.0 + ] + } + }, + { + "ensemble": { + "ensemble_hash": "3a26b1869b41e662ecdfe044a54229c6e5998280f88a6ad5003326302350654e", + "evidence_status": "chronological_calibration_challenger", + "league": "ncaambb", + "profiles": [ + { + "away_strength_exponent": 1.0, + "base_away_score": 72.0, + "base_home_score": 75.0, + "evidence_status": "provisional_reference_anchor", + "generation": 0, + "home_strength_exponent": 1.0, + "league": "ncaambb", + "margin_sd": 14.0, + "parent_hash": "", + "profile_hash": "14be1ec209dc6acf4030c480c7c2c6abf250986580e5da75595419c8fd8028df", + "total_score_sd": 16.5 + }, + { + "away_strength_exponent": 0.9380995927689169, + "base_away_score": 64.26338685281874, + "base_home_score": 67.90736863209588, + "evidence_status": "train_search_candidate", + "generation": 8, + "home_strength_exponent": 1.1198826403419602, + "league": "ncaambb", + "margin_sd": 12.337242613541385, + "parent_hash": "d908371efab1b1058ff372457736decc2f22f9d69723d762431e794c4f4582e3", + "profile_hash": "05bcc37afeab1ab4bf15ea01c88cd88d9e19e259e85a843ba6904dc94e5285ee", + "total_score_sd": 11.844982854797419 + }, + { + "away_strength_exponent": 0.9839084920688369, + "base_away_score": 64.17862932003789, + "base_home_score": 66.40814600980535, + "evidence_status": "train_search_candidate", + "generation": 7, + "home_strength_exponent": 1.1265354070594922, + "league": "ncaambb", + "margin_sd": 11.751801481101191, + "parent_hash": "5052bec2704f4278b8055686fdfd6fdaca864456ff8480e6bd1c3a16ff8cd5c2", + "profile_hash": "d908371efab1b1058ff372457736decc2f22f9d69723d762431e794c4f4582e3", + "total_score_sd": 12.527208607031197 + }, + { + "away_strength_exponent": 1.0302789332906102, + "base_away_score": 64.56603709857632, + "base_home_score": 68.11959848451264, + "evidence_status": "train_search_candidate", + "generation": 8, + "home_strength_exponent": 1.15845619003206, + "league": "ncaambb", + "margin_sd": 12.172650135194955, + "parent_hash": "d908371efab1b1058ff372457736decc2f22f9d69723d762431e794c4f4582e3", + "profile_hash": "a3426257638d3914687ea87b35c7948a9df8423501f8509cc563d4635f364bad", + "total_score_sd": 12.250367245233734 + }, + { + "away_strength_exponent": 0.9757805013766029, + "base_away_score": 64.69153447008274, + "base_home_score": 67.3019953859376, + "evidence_status": "train_search_candidate", + "generation": 6, + "home_strength_exponent": 1.0463799843916333, + "league": "ncaambb", + "margin_sd": 12.360366882633027, + "parent_hash": "035fdaf6642f08d5f653660bf422cf514301df099700740c1740ff66f014aa95", + "profile_hash": "5052bec2704f4278b8055686fdfd6fdaca864456ff8480e6bd1c3a16ff8cd5c2", + "total_score_sd": 13.12846794130971 + }, + { + "away_strength_exponent": 1.0525771945812439, + "base_away_score": 65.56062542041047, + "base_home_score": 67.05886346685132, + "evidence_status": "train_search_candidate", + "generation": 8, + "home_strength_exponent": 1.0249276075178801, + "league": "ncaambb", + "margin_sd": 11.814617284868469, + "parent_hash": "5052bec2704f4278b8055686fdfd6fdaca864456ff8480e6bd1c3a16ff8cd5c2", + "profile_hash": "d5abde343917aae55c5b655aed7e5ce5542a8d6e94ebb6cc5b1dff7aef5b914b", + "total_score_sd": 12.842103349942267 + } + ], + "weights": [ + 0.15204731462607787, + 0.17053030010320389, + 0.17008865902417303, + 0.1698776799805041, + 0.16875167581778863, + 0.16870437044825248 + ] + }, + "league": "ncaambb", + "promoted": true, + "rollback_ensemble": { + "ensemble_hash": "f4a907f61caeed291d4ccf147414e9c0e97aaef2acbf4a47d08961d80fb12539", + "evidence_status": "provisional_reference_anchor", + "league": "ncaambb", + "profiles": [ + { + "away_strength_exponent": 1.0, + "base_away_score": 72.0, + "base_home_score": 75.0, + "evidence_status": "provisional_reference_anchor", + "generation": 0, + "home_strength_exponent": 1.0, + "league": "ncaambb", + "margin_sd": 14.0, + "parent_hash": "", + "profile_hash": "14be1ec209dc6acf4030c480c7c2c6abf250986580e5da75595419c8fd8028df", + "total_score_sd": 16.5 + } + ], + "weights": [ + 1.0 + ] + }, + "rollback_state": { + "drift_score": 0.0, + "drift_threshold": 0.12, + "ensemble_hash": "f4a907f61caeed291d4ccf147414e9c0e97aaef2acbf4a47d08961d80fb12539", + "interval_learning_rate": 0.04, + "interval_scale": 1.0000000000000013, + "learning_rate": 0.25, + "long_decay": 0.03, + "long_loss_ewma": 8.386494742786283, + "minimum_drift_observations": 40, + "observations": 50, + "short_decay": 0.2, + "short_loss_ewma": 8.246116288876184, + "state_hash": "b00bf20e38b236488432ebaaa42691474cd68631fd141127185217573f05d4f2", + "status": "healthy", + "target_coverage": 0.9, + "weights": [ + 1.0 + ] + }, + "state": { + "drift_score": 0.03266449323603947, + "drift_threshold": 0.12, + "ensemble_hash": "3a26b1869b41e662ecdfe044a54229c6e5998280f88a6ad5003326302350654e", + "interval_learning_rate": 0.04, + "interval_scale": 0.9801986733067586, + "learning_rate": 0.25, + "long_decay": 0.03, + "long_loss_ewma": 8.031293500591644, + "minimum_drift_observations": 40, + "observations": 90, + "short_decay": 0.2, + "short_loss_ewma": 8.293631632818368, + "state_hash": "fbb1408030fa3f9b6207b7f4c8e4fb9180cc1f2c2cf1f265f61d489e370eb2b9", + "status": "healthy", + "target_coverage": 0.9, + "weights": [ + 1.3836249818109066e-05, + 0.24749428407453483, + 0.1857774825592718, + 0.18331698809408464, + 0.2330792551989154, + 0.15031815382337527 + ] + } + }, + { + "ensemble": { + "ensemble_hash": "52cc748b0f03d805bc32644c746152d71b8878f6cb3e9e2e6df533e3cb3718e2", + "evidence_status": "provisional_reference_anchor", + "league": "nfl", + "profiles": [ + { + "away_strength_exponent": 1.0, + "base_away_score": 21.8, + "base_home_score": 23.5, + "evidence_status": "provisional_reference_anchor", + "generation": 0, + "home_strength_exponent": 1.0, + "league": "nfl", + "margin_sd": 14.0, + "parent_hash": "", + "profile_hash": "2cf12a1c59550d09aad4853366ee45da48461195286aba2fc665167a2d0accd0", + "total_score_sd": 13.7 + } + ], + "weights": [ + 1.0 + ] + }, + "league": "nfl", + "promoted": false, + "rollback_ensemble": { + "ensemble_hash": "52cc748b0f03d805bc32644c746152d71b8878f6cb3e9e2e6df533e3cb3718e2", + "evidence_status": "provisional_reference_anchor", + "league": "nfl", + "profiles": [ + { + "away_strength_exponent": 1.0, + "base_away_score": 21.8, + "base_home_score": 23.5, + "evidence_status": "provisional_reference_anchor", + "generation": 0, + "home_strength_exponent": 1.0, + "league": "nfl", + "margin_sd": 14.0, + "parent_hash": "", + "profile_hash": "2cf12a1c59550d09aad4853366ee45da48461195286aba2fc665167a2d0accd0", + "total_score_sd": 13.7 + } + ], + "weights": [ + 1.0 + ] + }, + "rollback_state": { + "drift_score": 0.009386081910553939, + "drift_threshold": 0.12, + "ensemble_hash": "52cc748b0f03d805bc32644c746152d71b8878f6cb3e9e2e6df533e3cb3718e2", + "interval_learning_rate": 0.04, + "interval_scale": 0.923116346386637, + "learning_rate": 0.25, + "long_decay": 0.03, + "long_loss_ewma": 8.229562938300864, + "minimum_drift_observations": 40, + "observations": 50, + "short_decay": 0.2, + "short_loss_ewma": 8.306806290127815, + "state_hash": "c56bdf6245fc5a57c735db00e6d448c6670f5a96632121cbfa3e9d7e30dd0e91", + "status": "healthy", + "target_coverage": 0.9, + "weights": [ + 1.0 + ] + }, + "state": { + "drift_score": 0.055956431136555775, + "drift_threshold": 0.12, + "ensemble_hash": "52cc748b0f03d805bc32644c746152d71b8878f6cb3e9e2e6df533e3cb3718e2", + "interval_learning_rate": 0.04, + "interval_scale": 0.8693582353988077, + "learning_rate": 0.25, + "long_decay": 0.03, + "long_loss_ewma": 8.225798650623538, + "minimum_drift_observations": 40, + "observations": 90, + "short_decay": 0.2, + "short_loss_ewma": 8.686084986360328, + "state_hash": "7833a83844625da6d292a09a0ef94572dfbd5ed027e33ab1b249eecc21f9fa97", + "status": "healthy", + "target_coverage": 0.9, + "weights": [ + 1.0 + ] + } + }, + { + "ensemble": { + "ensemble_hash": "b4268f4251d62981ee852625c817f48947e9e12aecc39da1b77d4ff869c86289", + "evidence_status": "chronological_calibration_challenger", + "league": "nhl", + "profiles": [ + { + "away_strength_exponent": 1.0, + "base_away_score": 2.95, + "base_home_score": 3.2, + "evidence_status": "provisional_reference_anchor", + "generation": 0, + "home_strength_exponent": 1.0, + "league": "nhl", + "margin_sd": 2.4, + "parent_hash": "", + "profile_hash": "dd2f407da7fec8a4017fec2f6e311c3010d4f874c1d156f9dc4a4d9ba25576c3", + "total_score_sd": 2.3 + }, + { + "away_strength_exponent": 1.3618435036484602, + "base_away_score": 2.8140096579368463, + "base_home_score": 2.9827709409299437, + "evidence_status": "train_search_candidate", + "generation": 6, + "home_strength_exponent": 1.0164249111888564, + "league": "nhl", + "margin_sd": 2.4067107458645474, + "parent_hash": "c0f59c1041dfffbcb83db90b24931ee0acf8315cd86d375cd903b1869716224f", + "profile_hash": "560b8c3bf885c09aba0c04204376e3091a28ca9006785a1e982d822fd4f20158", + "total_score_sd": 1.9217750431804201 + }, + { + "away_strength_exponent": 1.3918430671368498, + "base_away_score": 2.8395546024633056, + "base_home_score": 3.001160815782975, + "evidence_status": "train_search_candidate", + "generation": 8, + "home_strength_exponent": 0.9742513551643636, + "league": "nhl", + "margin_sd": 2.347437453084234, + "parent_hash": "7e6cc68f9f179e60b9ad230a27f90dbac843e9dfc33f83849a71a51a37077069", + "profile_hash": "b792aed5100d5cfef059e56fe733dc686f999130857572ef57702f98220e5374", + "total_score_sd": 1.9848807761955292 + }, + { + "away_strength_exponent": 1.3675649819385032, + "base_away_score": 2.67379397546478, + "base_home_score": 3.2244694632163426, + "evidence_status": "train_search_candidate", + "generation": 5, + "home_strength_exponent": 0.9979113276154296, + "league": "nhl", + "margin_sd": 2.315479052327515, + "parent_hash": "3e88d5bd7565e8748490af5a78d84b19a8534868a6331d4d1752f34943540487", + "profile_hash": "c0f59c1041dfffbcb83db90b24931ee0acf8315cd86d375cd903b1869716224f", + "total_score_sd": 1.8514937320015388 + }, + { + "away_strength_exponent": 1.365373323994108, + "base_away_score": 2.6479224044804157, + "base_home_score": 3.0420403168143237, + "evidence_status": "train_search_candidate", + "generation": 5, + "home_strength_exponent": 0.8190424527874777, + "league": "nhl", + "margin_sd": 2.469930832788603, + "parent_hash": "e20a271760ee2ecb08354c7be8fa7b0533bc22ccf66bb8691f131706d6d4193d", + "profile_hash": "16e8f92eddd415582a8ca2282946b318fe60e8845d896d93538b603bc8cd0555", + "total_score_sd": 1.754941135672797 + }, + { + "away_strength_exponent": 1.444774898245588, + "base_away_score": 2.8087224671677236, + "base_home_score": 2.900908465085074, + "evidence_status": "train_search_candidate", + "generation": 8, + "home_strength_exponent": 0.9764064324855087, + "league": "nhl", + "margin_sd": 2.4088026880950175, + "parent_hash": "560b8c3bf885c09aba0c04204376e3091a28ca9006785a1e982d822fd4f20158", + "profile_hash": "cbd5bb21fef457140e843a9455f9d3d073749dc621ad2bef307a40f02fe73c53", + "total_score_sd": 1.8330168778160287 + } + ], + "weights": [ + 0.16286673514032046, + 0.16755705994655176, + 0.16752559730854746, + 0.1673849214822694, + 0.16737507931387444, + 0.1672906068084365 + ] + }, + "league": "nhl", + "promoted": true, + "rollback_ensemble": { + "ensemble_hash": "382ec0d5185a96f540f91839c8bd6328d408d70a8ec1f16352aa22553c05bcfb", + "evidence_status": "provisional_reference_anchor", + "league": "nhl", + "profiles": [ + { + "away_strength_exponent": 1.0, + "base_away_score": 2.95, + "base_home_score": 3.2, + "evidence_status": "provisional_reference_anchor", + "generation": 0, + "home_strength_exponent": 1.0, + "league": "nhl", + "margin_sd": 2.4, + "parent_hash": "", + "profile_hash": "dd2f407da7fec8a4017fec2f6e311c3010d4f874c1d156f9dc4a4d9ba25576c3", + "total_score_sd": 2.3 + } + ], + "weights": [ + 1.0 + ] + }, + "rollback_state": { + "drift_score": 0.0, + "drift_threshold": 0.12, + "ensemble_hash": "382ec0d5185a96f540f91839c8bd6328d408d70a8ec1f16352aa22553c05bcfb", + "interval_learning_rate": 0.04, + "interval_scale": 0.9801986733067568, + "learning_rate": 0.25, + "long_decay": 0.03, + "long_loss_ewma": 4.723961751695468, + "minimum_drift_observations": 40, + "observations": 50, + "short_decay": 0.2, + "short_loss_ewma": 4.6298625684220225, + "state_hash": "7ce259e0d6f333426389c555b9f568c802b7265ae0ac4a6a55ba9f98184c8ee5", + "status": "healthy", + "target_coverage": 0.9, + "weights": [ + 1.0 + ] + }, + "state": { + "drift_score": 0.0, + "drift_threshold": 0.12, + "ensemble_hash": "b4268f4251d62981ee852625c817f48947e9e12aecc39da1b77d4ff869c86289", + "interval_learning_rate": 0.04, + "interval_scale": 0.9801986733067561, + "learning_rate": 0.25, + "long_decay": 0.03, + "long_loss_ewma": 4.599749180852678, + "minimum_drift_observations": 40, + "observations": 90, + "short_decay": 0.2, + "short_loss_ewma": 4.349387937549807, + "state_hash": "253f6e160e5566546bedc54a69316b0bed6def3e7c3d378c94dd56375d59de13", + "status": "healthy", + "target_coverage": 0.9, + "weights": [ + 0.11399533888828041, + 0.19661716688245462, + 0.2167900976627797, + 0.18688072694737573, + 0.14146108610804536, + 0.14425558351106413 + ] + } + }, + { + "ensemble": { + "ensemble_hash": "369bd1fab70c4fe9c443214b61e5bb1297947388fae8402061d599b6dc5a975d", + "evidence_status": "chronological_calibration_challenger", + "league": "wnba", + "profiles": [ + { + "away_strength_exponent": 1.0, + "base_away_score": 80.0, + "base_home_score": 82.0, + "evidence_status": "provisional_reference_anchor", + "generation": 0, + "home_strength_exponent": 1.0, + "league": "wnba", + "margin_sd": 14.0, + "parent_hash": "", + "profile_hash": "a9efcb5f3219e8e9a3df4fdce3b3f767165faa82fe41cf874bdb0716548bb4d9", + "total_score_sd": 17.5 + }, + { + "away_strength_exponent": 1.0251595474377284, + "base_away_score": 72.11403431035531, + "base_home_score": 71.69042161368746, + "evidence_status": "train_search_candidate", + "generation": 7, + "home_strength_exponent": 1.0938685005129976, + "league": "wnba", + "margin_sd": 12.553668307327136, + "parent_hash": "468447632d3bbfb0286c4335ad436b5f32c841251c8614e5f2096d0b5dbf0a3f", + "profile_hash": "af6c47cb9cc880f80388c052e42e219dc4055b7d2c8b6e1346332703b49299e5", + "total_score_sd": 13.255930520302046 + }, + { + "away_strength_exponent": 1.0981336628870195, + "base_away_score": 71.11342291633342, + "base_home_score": 73.62693210352198, + "evidence_status": "train_search_candidate", + "generation": 7, + "home_strength_exponent": 1.1420227255585949, + "league": "wnba", + "margin_sd": 12.523765694931425, + "parent_hash": "7d2a641dbd76ac6fde3dfa526718e79569f129327958d60c3dfafc464b75af97", + "profile_hash": "b83f84e526bc77753ff0561968a83ca3f84f62dad23a54b08c2869b88226c810", + "total_score_sd": 13.124434195991878 + }, + { + "away_strength_exponent": 0.9827239039722672, + "base_away_score": 73.07586506043492, + "base_home_score": 73.49995936972549, + "evidence_status": "train_search_candidate", + "generation": 7, + "home_strength_exponent": 1.060463397601755, + "league": "wnba", + "margin_sd": 11.322075003472191, + "parent_hash": "02a8002ff7041946836ce1a1fcb8f53773f3b838801f82797911d7fbf267efec", + "profile_hash": "6b7ed5b7bfddee7d6ec43ca6649a0fd953a0954c5a818b269d146c55b54ea0e2", + "total_score_sd": 13.178399018766651 + }, + { + "away_strength_exponent": 0.9391411924229954, + "base_away_score": 73.82314278504273, + "base_home_score": 72.09561563722342, + "evidence_status": "train_search_candidate", + "generation": 6, + "home_strength_exponent": 1.1340765001951492, + "league": "wnba", + "margin_sd": 12.028397269421774, + "parent_hash": "ab7556213fe56f2a8c712da7df880059839437bfee109b7996749fcea6802355", + "profile_hash": "5fb5c12d474e17f2c2719e637aa37a3fb53d24a5dda665aa15e1c8ce0cc9961b", + "total_score_sd": 14.119914546223248 + }, + { + "away_strength_exponent": 1.0428759542867176, + "base_away_score": 73.34588663690329, + "base_home_score": 70.67653549724727, + "evidence_status": "train_search_candidate", + "generation": 6, + "home_strength_exponent": 1.177066395306101, + "league": "wnba", + "margin_sd": 12.30699982423615, + "parent_hash": "7d2a641dbd76ac6fde3dfa526718e79569f129327958d60c3dfafc464b75af97", + "profile_hash": "468447632d3bbfb0286c4335ad436b5f32c841251c8614e5f2096d0b5dbf0a3f", + "total_score_sd": 13.296762898729602 + } + ], + "weights": [ + 0.14546085482667262, + 0.1713210376892588, + 0.17089076167696657, + 0.1708509658434923, + 0.17074437378062704, + 0.17073200618298265 + ] + }, + "league": "wnba", + "promoted": true, + "rollback_ensemble": { + "ensemble_hash": "92022b12c8dd03d4fb005a8a84b4041b3370ee4e401e2e281230d8f6c3b3d284", + "evidence_status": "provisional_reference_anchor", + "league": "wnba", + "profiles": [ + { + "away_strength_exponent": 1.0, + "base_away_score": 80.0, + "base_home_score": 82.0, + "evidence_status": "provisional_reference_anchor", + "generation": 0, + "home_strength_exponent": 1.0, + "league": "wnba", + "margin_sd": 14.0, + "parent_hash": "", + "profile_hash": "a9efcb5f3219e8e9a3df4fdce3b3f767165faa82fe41cf874bdb0716548bb4d9", + "total_score_sd": 17.5 + } + ], + "weights": [ + 1.0 + ] + }, + "rollback_state": { + "drift_score": 0.0, + "drift_threshold": 0.12, + "ensemble_hash": "92022b12c8dd03d4fb005a8a84b4041b3370ee4e401e2e281230d8f6c3b3d284", + "interval_learning_rate": 0.04, + "interval_scale": 1.06183654654536, + "learning_rate": 0.25, + "long_decay": 0.03, + "long_loss_ewma": 8.828494403135926, + "minimum_drift_observations": 40, + "observations": 50, + "short_decay": 0.2, + "short_loss_ewma": 8.776294316432912, + "state_hash": "bb00abb17fee66e3eb9152039b5e33447479ed0e079dd11c90c8fca060eb8d21", + "status": "healthy", + "target_coverage": 0.9, + "weights": [ + 1.0 + ] + }, + "state": { + "drift_score": 0.034010092999338264, + "drift_threshold": 0.12, + "ensemble_hash": "369bd1fab70c4fe9c443214b61e5bb1297947388fae8402061d599b6dc5a975d", + "interval_learning_rate": 0.04, + "interval_scale": 1.0408107741923893, + "learning_rate": 0.25, + "long_decay": 0.03, + "long_loss_ewma": 8.300244006128747, + "minimum_drift_observations": 40, + "observations": 90, + "short_decay": 0.2, + "short_loss_ewma": 8.582536076694385, + "state_hash": "ae0ce766963e2c17eec1ba08d7b118c8c96dcc80952634178f48041eb2454805", + "status": "healthy", + "target_coverage": 0.9, + "weights": [ + 2.3626823306179495e-07, + 0.398845266826086, + 0.08337764062148095, + 0.15725428716220335, + 0.23035294396144723, + 0.13016962516054942 + ] + } + } + ], + "created_at": "2036-12-11T15:00:00+00:00", + "protocol_version": "recursive-calibration-v1" +} diff --git a/scripts/audit_autonomy.py b/scripts/audit_autonomy.py new file mode 100644 index 0000000..793d7d2 --- /dev/null +++ b/scripts/audit_autonomy.py @@ -0,0 +1,196 @@ +"""Audit recursive improvement on explicitly synthetic, non-certifying fixtures.""" + +from __future__ import annotations + +import argparse +import json +from dataclasses import asdict +from datetime import UTC, datetime, timedelta +from pathlib import Path + +from universal_sports_engine.accuracy import HistoricalGame +from universal_sports_engine.adaptive import ( + create_profile, + reference_profile, + sample_profile_scores, +) +from universal_sports_engine.autotune import ( + adapt_champion_on_shadow, + champion_for, + create_champion_bundle, + load_tuning_config, + read_champion_bundle, + run_recursive_improvement, + verify_improvement_ledger, + write_champion_bundle, + write_improvement_artifacts, +) +from universal_sports_engine.league_packs.registry import league_ids +from universal_sports_engine.reference import ANCHORS + + +def _arguments() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("--config", required=True, type=Path) + parser.add_argument("--output", required=True, type=Path) + parser.add_argument("--artifacts", required=True, type=Path) + return parser.parse_args() + + +def _fixtures(config_path: Path) -> tuple[HistoricalGame, ...]: + config = load_tuning_config(config_path) + games: list[HistoricalGame] = [] + base_time = datetime(2020, 1, 1, 12, tzinfo=UTC) + for league_index, league in enumerate(league_ids()): + anchor = ANCHORS[league] + reference = reference_profile(league) + target = create_profile( + league, + anchor.mean_home_score * (0.86 + league_index * 0.01), + anchor.mean_away_score * (0.88 + league_index * 0.008), + anchor.total_score_sd * (0.72 + league_index * 0.02), + anchor.margin_sd * (0.76 + league_index * 0.015), + 1.12, + 0.92, + 1, + reference.profile_hash, + "synthetic_autonomy_audit_target", + ) + shadow_games = config.minimum_drift_observations + count = ( + config.minimum_train_games + + config.minimum_calibration_games + + shadow_games + ) + for index in range(count): + if index < config.minimum_train_games: + split = "train" + elif index < config.minimum_train_games + config.minimum_calibration_games: + split = "calibration" + else: + split = "shadow" + decision = base_time + timedelta(days=league_index * 1_000 + index) + home_strength = 0.75 + (index % 11) * 0.05 + away_strength = 0.78 + (index % 9) * 0.05 + home_score, away_score = sample_profile_scores( + target, + config.seed + league_index * 100_000 + index, + home_strength, + away_strength, + ) + games.append( + HistoricalGame( + f"{league}-synthetic-{index:04d}", + league, + decision, + decision - timedelta(hours=1), + decision + timedelta(hours=3), + home_score, + away_score, + home_strength, + away_strength, + split, + ) + ) + return tuple(games) + + +def main() -> None: + args = _arguments() + config = load_tuning_config(args.config) + games = _fixtures(args.config) + first = run_recursive_improvement(games, config) + second = run_recursive_improvement(games, config) + deterministic = first == second + calibration_games = tuple(game for game in games if game.split == "calibration") + evidence_cutoff = max(game.outcome_time for game in calibration_games).isoformat() + bundle = create_champion_bundle(first, "recursive-calibration-v1", evidence_cutoff) + write_improvement_artifacts(args.artifacts, first, bundle, config) + reloaded = read_champion_bundle(args.artifacts / "champion_bundle.json") + bundle_reproduced = reloaded == bundle + ledger_records = verify_improvement_ledger( + args.artifacts / "improvement_ledger.jsonl", + bundle.bundle_hash, + ) + adapted = bundle + for league in league_ids(): + shadow = tuple( + game + for game in games + if game.league == league and game.split == "shadow" + ) + adapted = adapt_champion_on_shadow( + adapted, + league, + shadow, + config, + max(game.outcome_time for game in shadow).isoformat(), + ) + shadow_path = args.artifacts / "shadow_adapted_bundle.json" + write_champion_bundle(shadow_path, adapted) + shadow_reproduced = read_champion_bundle(shadow_path) == adapted + shadow_weight_adjustments = sum( + champion_for(adapted, league).state.weights + != champion_for(bundle, league).state.weights + for league in league_ids() + ) + shadow_drift_alerts = sum( + champion_for(adapted, league).state.status == "drift_alert" + for league in league_ids() + ) + promotions = sum(result.promoted for result in first) + adjusted_weights = sum( + result.challenger_state.weights != result.challenger_ensemble.weights + for result in first + ) + report = { + "bundle_hash": bundle.bundle_hash, + "bundle_reproduced": bundle_reproduced, + "deterministic_recursive_search": deterministic, + "evidence_status": "synthetic_autonomy_audit_not_real_world_certification", + "leagues": [ + { + "challenger_score": asdict(result.challenger_calibration_score), + "decision": result.decision, + "generations": len(result.generations), + "incumbent_score": asdict(result.incumbent_calibration_score), + "league": result.league, + "promoted": result.promoted, + "relative_improvement": result.relative_improvement, + "selected_ensemble_hash": result.selected_ensemble.ensemble_hash, + "selected_state_hash": result.selected_state.state_hash, + } + for result in first + ], + "promotions": promotions, + "shadow_bundle_hash": adapted.bundle_hash, + "shadow_bundle_reproduced": shadow_reproduced, + "shadow_drift_alerts": shadow_drift_alerts, + "shadow_weight_adjustments": shadow_weight_adjustments, + "verified_ledger_records": ledger_records, + "total_leagues": len(first), + "weight_adjustments": adjusted_weights, + } + args.output.parent.mkdir(parents=True, exist_ok=True) + if args.output.exists(): + raise FileExistsError(f"Refusing to overwrite autonomy audit: {args.output}") + args.output.write_text( + json.dumps(report, allow_nan=False, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + if not deterministic or not bundle_reproduced or not shadow_reproduced: + raise RuntimeError( + "Autonomy audit failed reproducibility: " + f"deterministic={deterministic}, bundle_reproduced={bundle_reproduced}, " + f"shadow_reproduced={shadow_reproduced}" + ) + if promotions == 0 or adjusted_weights == 0 or shadow_weight_adjustments == 0: + raise RuntimeError( + "Autonomy audit failed learning controls: " + f"promotions={promotions}, weight_adjustments={adjusted_weights}, " + f"shadow_weight_adjustments={shadow_weight_adjustments}" + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/audit_performance.py b/scripts/audit_performance.py new file mode 100644 index 0000000..287b580 --- /dev/null +++ b/scripts/audit_performance.py @@ -0,0 +1,101 @@ +"""Benchmark F0 and F1 throughput across supported worker counts.""" + +from __future__ import annotations + +import argparse +import json +import time +from pathlib import Path + +from universal_sports_engine.analytical import simulate_many_analytical +from universal_sports_engine.league_packs.registry import league_ids +from universal_sports_engine.simulation import simulate_many + + +def _arguments() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("--f1-games", required=True, type=int) + parser.add_argument("--f0-games", required=True, type=int) + parser.add_argument("--workers", required=True, type=int, nargs="+") + parser.add_argument("--seed", required=True, type=int) + parser.add_argument("--output", required=True, type=Path) + parser.add_argument("--minimum-f0-gps", required=True, type=float) + parser.add_argument("--minimum-f1-gps", required=True, type=float) + parser.add_argument("--minimum-speedup", required=True, type=float) + return parser.parse_args() + + +def _f1_row(league: str, games: int, workers: int, seed: int) -> dict[str, object]: + started = time.perf_counter() + results = simulate_many(league, games, seed, workers) + elapsed = time.perf_counter() - started + return { + "fidelity": "F1_EVENT", + "league": league, + "games": games, + "workers": workers, + "elapsed_seconds": elapsed, + "games_per_second": games / elapsed, + "events": sum(len(result.events) for result in results), + } + + +def _f0_row(league: str, games: int, seed: int) -> dict[str, object]: + started = time.perf_counter() + results = simulate_many_analytical(league, games, seed, 1.0, 1.0) + elapsed = time.perf_counter() - started + return { + "fidelity": "F0_ANALYTICAL", + "league": league, + "games": games, + "workers": 1, + "elapsed_seconds": elapsed, + "games_per_second": games / elapsed, + "events": 0, + "results": len(results), + } + + +def _numeric(row: dict[str, object], key: str) -> float: + value = row[key] + if not isinstance(value, int | float): + raise TypeError(f"Expected numeric benchmark field: key={key}, value={value!r}") + return float(value) + + +def main() -> None: + args = _arguments() + if args.f1_games <= 0 or args.f0_games <= 0 or any(worker <= 0 for worker in args.workers): + raise ValueError("game and worker counts must be positive") + rows: list[dict[str, object]] = [] + for index, league in enumerate(league_ids()): + league_seed = args.seed + index * 1_000_000 + rows.append(_f0_row(league, args.f0_games, league_seed)) + rows.extend( + _f1_row(league, args.f1_games, workers, league_seed) + for workers in args.workers + ) + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(json.dumps(rows, indent=2, sort_keys=True) + "\n", encoding="utf-8") + failures: list[str] = [] + for league in league_ids(): + league_rows = tuple(row for row in rows if row["league"] == league) + analytical = next(row for row in league_rows if row["fidelity"] == "F0_ANALYTICAL") + event_rows = tuple(row for row in league_rows if row["fidelity"] == "F1_EVENT") + single = next((row for row in event_rows if row["workers"] == 1), None) + if single is None: + raise ValueError("Performance gate requires a one-worker benchmark") + best = max(event_rows, key=lambda row: _numeric(row, "games_per_second")) + if _numeric(analytical, "games_per_second") < args.minimum_f0_gps: + failures.append(f"{league}.f0_throughput") + if _numeric(single, "games_per_second") < args.minimum_f1_gps: + failures.append(f"{league}.f1_throughput") + speedup = _numeric(best, "games_per_second") / _numeric(single, "games_per_second") + if speedup < args.minimum_speedup: + failures.append(f"{league}.parallel_speedup") + if failures: + raise RuntimeError(f"Performance gates failed: {failures!r}") + + +if __name__ == "__main__": + main() diff --git a/scripts/audit_stress.py b/scripts/audit_stress.py new file mode 100644 index 0000000..b11a933 --- /dev/null +++ b/scripts/audit_stress.py @@ -0,0 +1,57 @@ +"""Run deterministic randomized scenario and integrity stress checks.""" + +from __future__ import annotations + +import argparse +import json +import time +from pathlib import Path + +from universal_sports_engine.core.result_integrity import verify_result +from universal_sports_engine.core.rng import unit +from universal_sports_engine.league_packs.registry import league_ids +from universal_sports_engine.simulation import simulate_scenario + + +def _arguments() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("--games-per-league", required=True, type=int) + parser.add_argument("--seed", required=True, type=int) + parser.add_argument("--output", required=True, type=Path) + return parser.parse_args() + + +def main() -> None: + args = _arguments() + if args.games_per_league <= 0: + raise ValueError(f"games-per-league must be positive: {args.games_per_league}") + rows: list[dict[str, object]] = [] + for league_index, league in enumerate(league_ids()): + started = time.perf_counter() + event_count = 0 + maximum_events = 0 + for game_index in range(args.games_per_league): + game_seed = args.seed + league_index * 1_000_000 + game_index + home_strength = 0.5 + unit(game_seed, league, "stress", "home") + away_strength = 0.5 + unit(game_seed, league, "stress", "away") + result = simulate_scenario(league, game_seed, home_strength, away_strength) + verify_result(result) + event_count += len(result.events) + maximum_events = max(maximum_events, len(result.events)) + elapsed = time.perf_counter() - started + rows.append( + { + "league": league, + "games": args.games_per_league, + "events": event_count, + "maximum_events_per_game": maximum_events, + "failures": 0, + "elapsed_seconds": elapsed, + } + ) + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(json.dumps(rows, indent=2, sort_keys=True) + "\n", encoding="utf-8") + + +if __name__ == "__main__": + main() diff --git a/scripts/build_release_evidence.py b/scripts/build_release_evidence.py new file mode 100644 index 0000000..1a4381b --- /dev/null +++ b/scripts/build_release_evidence.py @@ -0,0 +1,225 @@ +"""Generate the auditable release evidence bundle.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import time +from dataclasses import asdict +from pathlib import Path + +from universal_sports_engine import __version__ +from universal_sports_engine.analytical import simulate_many_analytical +from universal_sports_engine.league_packs.registry import league_ids +from universal_sports_engine.simulation import simulate_many +from universal_sports_engine.validation import run_negative_controls, validate_all + + +def _arguments() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("--games", required=True, type=int) + parser.add_argument("--benchmark-games", required=True, type=int) + parser.add_argument("--seed", required=True, type=int) + parser.add_argument("--workers", required=True, type=int) + parser.add_argument("--output", required=True, type=Path) + return parser.parse_args() + + +def _write_json(path: Path, value: object) -> None: + path.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8") + + +def _source_manifest(root: Path) -> tuple[tuple[str, str], ...]: + allowed_roots = ( + root / ".github", + root / "src", + root / "tests", + root / "scripts", + root / "data", + root / "dummy_bridge", + ) + files = tuple( + sorted( + path + for allowed_root in allowed_roots + for path in allowed_root.rglob("*") + if path.is_file() + and "__pycache__" not in path.parts + and not any(part.endswith(".egg-info") for part in path.parts) + ) + ) + documents = tuple( + sorted( + path + for path in root.iterdir() + if path.is_file() + and ( + path.name in {".gitattributes", ".gitignore", "LICENSE"} + or path.suffix in {".json", ".md", ".toml", ".yaml"} + ) + ) + ) + return tuple( + ( + path.relative_to(root).as_posix(), + hashlib.sha256(path.read_bytes()).hexdigest(), + ) + for path in (*files, *documents) + ) + + +def _benchmark(games: int, seed: int, workers: int) -> tuple[dict[str, object], ...]: + rows: list[dict[str, object]] = [] + for index, league in enumerate(league_ids()): + analytical_games = max(10_000, games * 100) + analytical_started = time.perf_counter() + analytical_results = simulate_many_analytical( + league, + analytical_games, + seed + index * 100_000, + 1.0, + 1.0, + ) + analytical_elapsed = time.perf_counter() - analytical_started + rows.append( + { + "fidelity": "F0_ANALYTICAL", + "league": league, + "games": analytical_games, + "workers": 1, + "elapsed_seconds": analytical_elapsed, + "games_per_second": analytical_games / analytical_elapsed, + "events": 0, + "results": len(analytical_results), + } + ) + started = time.perf_counter() + results = simulate_many(league, games, seed + index * 100_000, workers) + elapsed = time.perf_counter() - started + rows.append( + { + "fidelity": "F1_EVENT", + "league": league, + "games": games, + "workers": workers, + "elapsed_seconds": elapsed, + "games_per_second": games / elapsed, + "events": sum(len(result.events) for result in results), + } + ) + return tuple(rows) + + +def _markdown( + validations: tuple[dict[str, object], ...], + controls: tuple[dict[str, object], ...], + benchmarks: tuple[dict[str, object], ...], + manifest_hash: str, +) -> str: + validation_lines = "\n".join( + f"| {row['league']} | {row['games']} | {row['observed_home_mean']:.3f} | " + f"{row['observed_away_mean']:.3f} | {row['maximum_relative_error']:.3f} | " + f"{row['total_sd_relative_error']:.3f} | {row['home_advantage_z_score']:.2f} | " + f"{row['status']} |" + for row in validations + ) + benchmark_lines = "\n".join( + f"| {row['fidelity']} | {row['league']} | {row['games']} | {row['events']} | " + f"{row['elapsed_seconds']:.3f} | {row['games_per_second']:.2f} |" + for row in benchmarks + ) + control_failures = sum(not bool(row["passed"]) for row in controls) + return f"""# Release Evidence + +## Determination + +**PROVISIONAL MULTI-LEAGUE F0/F1 ENGINE COMPLETE.** + +This bundle verifies mechanics, determinism, replay integrity, league coverage, +reference-anchor sanity, headless execution, and the content-addressed autonomy +surface. It does not certify real-world predictive accuracy. No licensed point-in-time +event or tracking dataset was available. + +## Reference-anchor validation + +| League | Games | Home mean | Away mean | Mean error | Total SD error | Home z | Status | +|---|---:|---:|---:|---:|---:|---:|---| +{validation_lines} + +## Negative controls + +- Controls executed: {len(controls)} +- Failures: {control_failures} + +## Headless benchmark + +| Fidelity | League | Games | Events | Seconds | Games/second | +|---|---|---:|---:|---:|---:| +{benchmark_lines} + +## Reproducibility + +- Source manifest SHA-256: `{manifest_hash}` +- Every event stream is invariant-checked and SHA-256 chained during construction. +- External replay imports reverify both the event chain and complete result envelope. +- Same input and seed equality is exercised for every league. +- Cross-worker batch equality is exercised by the integration suite. +- Recursive profiles, ensembles, adaptive states, champion bundles, and promotion + ledgers have independent content hashes. +- Autonomous search cannot mutate rules, evaluators, split boundaries, or source code. + +## Certification boundary + +`provisional_reference_anchor` is the strongest supported status. F2 spatial +calibration, F3 physics/rendering, player/team models, exhaustive rulebooks, and +held-out predictive validity remain blocked pending licensed data and scoped evidence. +""" + + +def main() -> None: + args = _arguments() + if args.games <= 0 or args.benchmark_games <= 0 or args.workers <= 0: + raise ValueError("games, benchmark-games, and workers must be positive") + root = Path(__file__).resolve().parents[1] + output = args.output.resolve() + output.mkdir(parents=True, exist_ok=True) + validation_objects = validate_all(args.games, args.seed, args.workers) + validations = tuple(asdict(report) for report in validation_objects) + controls = tuple( + {"control": name, "passed": passed, "description": description} + for name, passed, description in run_negative_controls(args.seed) + ) + benchmarks = _benchmark(args.benchmark_games, args.seed, args.workers) + source_manifest = _source_manifest(root) + manifest_json = json.dumps(source_manifest, sort_keys=True, separators=(",", ":")) + manifest_hash = hashlib.sha256(manifest_json.encode()).hexdigest() + release_manifest = { + "engine_version": __version__, + "evidence_status": "provisional_reference_anchor", + "league_ids": league_ids(), + "source_manifest_sha256": manifest_hash, + "source_files": source_manifest, + } + _write_json(output / "VALIDATION_REPORT.json", validations) + _write_json(output / "NEGATIVE_CONTROL_REPORT.json", controls) + _write_json(output / "PERFORMANCE_BENCHMARK.json", benchmarks) + _write_json(output / "RELEASE_MANIFEST.json", release_manifest) + (output / "RELEASE_EVIDENCE.md").write_text( + _markdown(validations, controls, benchmarks, manifest_hash), + encoding="utf-8", + ) + validation_failures = tuple( + row for row in validations if row["status"] != "provisional_reference_anchor" + ) + control_failures = tuple(row for row in controls if not row["passed"]) + if validation_failures or control_failures: + raise RuntimeError( + "Release evidence gates failed: " + f"validation_failures={len(validation_failures)}, " + f"control_failures={len(control_failures)}" + ) + + +if __name__ == "__main__": + main() diff --git a/src/universal_sports_engine/__init__.py b/src/universal_sports_engine/__init__.py new file mode 100644 index 0000000..e1c433a --- /dev/null +++ b/src/universal_sports_engine/__init__.py @@ -0,0 +1,29 @@ +"""Universal Sports Engine public API.""" + +from universal_sports_engine.analytical import ( + AdaptiveAnalyticalResult, + AnalyticalResult, + simulate_analytical, + simulate_ensemble_analytical, + simulate_many_analytical, + simulate_profile_analytical, +) +from universal_sports_engine.simulation import ( + counterfactual_game, + simulate_game, + simulate_many, +) + +__version__ = "0.3.0" + +__all__ = [ + "AdaptiveAnalyticalResult", + "AnalyticalResult", + "counterfactual_game", + "simulate_analytical", + "simulate_ensemble_analytical", + "simulate_game", + "simulate_many", + "simulate_many_analytical", + "simulate_profile_analytical", +] diff --git a/src/universal_sports_engine/accuracy.py b/src/universal_sports_engine/accuracy.py new file mode 100644 index 0000000..26f4dfc --- /dev/null +++ b/src/universal_sports_engine/accuracy.py @@ -0,0 +1,283 @@ +"""Point-in-time historical evaluation with proper distributional scores.""" + +from __future__ import annotations + +import csv +import statistics +from dataclasses import dataclass +from datetime import datetime +from itertools import combinations +from pathlib import Path + +from universal_sports_engine.analytical import simulate_many_analytical +from universal_sports_engine.league_packs.registry import league_ids + + +@dataclass(frozen=True, slots=True) +class HistoricalGame: + game_id: str + league: str + decision_time: datetime + features_available_at: datetime + outcome_time: datetime + home_score: int + away_score: int + home_strength: float + away_strength: float + split: str + + +@dataclass(frozen=True, slots=True) +class GameForecast: + game_id: str + league: str + home_samples: tuple[int, ...] + away_samples: tuple[int, ...] + + +@dataclass(frozen=True, slots=True) +class AccuracyReport: + league: str + games: int + samples_per_game: int + home_score_mae: float + away_score_mae: float + total_score_mae: float + home_win_brier: float + home_score_crps: float + away_score_crps: float + score_interval_50_coverage: float + score_interval_90_coverage: float + evidence_status: str + + +def _timestamp(value: str, field: str, game_id: str) -> datetime: + try: + parsed = datetime.fromisoformat(value) + except ValueError as error: + raise ValueError( + f"Invalid ISO-8601 {field} for game {game_id!r}: {value!r}" + ) from error + if parsed.utcoffset() is None: + raise ValueError(f"Timestamp must include an offset: game={game_id!r}, field={field}") + return parsed + + +def load_historical_games(path: Path) -> tuple[HistoricalGame, ...]: + """Load a strict point-in-time game manifest from CSV.""" + + required = { + "game_id", + "league", + "decision_time", + "features_available_at", + "outcome_time", + "home_score", + "away_score", + "home_strength", + "away_strength", + "split", + } + with path.open("r", encoding="utf-8", newline="") as source: + reader = csv.DictReader(source) + missing = required.difference(reader.fieldnames or ()) + if missing: + raise ValueError(f"Historical CSV is missing columns: {sorted(missing)!r}") + games = tuple( + HistoricalGame( + game_id=row["game_id"], + league=row["league"].strip().lower(), + decision_time=_timestamp(row["decision_time"], "decision_time", row["game_id"]), + features_available_at=_timestamp( + row["features_available_at"], + "features_available_at", + row["game_id"], + ), + outcome_time=_timestamp(row["outcome_time"], "outcome_time", row["game_id"]), + home_score=int(row["home_score"]), + away_score=int(row["away_score"]), + home_strength=float(row["home_strength"]), + away_strength=float(row["away_strength"]), + split=row["split"].strip().lower(), + ) + for row in reader + ) + validate_historical_games(games) + validate_chronological_splits(games) + return games + + +def validate_historical_games(games: tuple[HistoricalGame, ...]) -> None: + """Reject leakage, duplicates, invalid outcomes, and ambiguous evaluation splits.""" + + if not games: + raise ValueError("Historical evaluation requires at least one game") + identifiers = tuple(game.game_id for game in games) + if len(set(identifiers)) != len(identifiers): + raise ValueError("Historical game IDs must be unique") + supported = set(league_ids()) + for game in games: + if game.league not in supported: + raise ValueError(f"Unsupported historical league: {game.league!r}") + if game.features_available_at > game.decision_time: + raise ValueError(f"Feature leakage detected for game: {game.game_id}") + if game.decision_time >= game.outcome_time: + raise ValueError(f"Outcome must occur after decision time: {game.game_id}") + if min(game.home_score, game.away_score) < 0: + raise ValueError(f"Historical scores must be non-negative: {game.game_id}") + if game.split not in {"train", "calibration", "test", "shadow"}: + raise ValueError(f"Invalid chronological split for game: {game.game_id}") + + +def validate_chronological_splits(games: tuple[HistoricalGame, ...]) -> None: + """Ensure earlier split outcomes precede later split decisions for each league.""" + + split_order = ("train", "calibration", "test", "shadow") + for league in {game.league for game in games}: + league_games = tuple(game for game in games if game.league == league) + for earlier, later in combinations(split_order, 2): + earlier_games = tuple(game for game in league_games if game.split == earlier) + later_games = tuple(game for game in league_games if game.split == later) + if not earlier_games or not later_games: + continue + latest_outcome = max(game.outcome_time for game in earlier_games) + earliest_decision = min(game.decision_time for game in later_games) + if latest_outcome > earliest_decision: + raise ValueError( + "Chronological split overlap detected: " + f"league={league}, earlier={earlier}, later={later}" + ) + + +def forecast_historical_games( + games: tuple[HistoricalGame, ...], + samples_per_game: int, + seed: int, +) -> tuple[GameForecast, ...]: + """Generate frozen F0 forecasts for a validated historical manifest.""" + + validate_historical_games(games) + if samples_per_game <= 0: + raise ValueError(f"samples_per_game must be positive: {samples_per_game}") + forecasts: list[GameForecast] = [] + for index, game in enumerate(games): + samples = simulate_many_analytical( + game.league, + samples_per_game, + seed + index * 1_000_000, + game.home_strength, + game.away_strength, + ) + forecasts.append( + GameForecast( + game.game_id, + game.league, + tuple(sample.home_score for sample in samples), + tuple(sample.away_score for sample in samples), + ) + ) + return tuple(forecasts) + + +def _crps(samples: tuple[int, ...], outcome: int) -> float: + ordered = tuple(sorted(samples)) + count = len(ordered) + first_term = statistics.fmean(abs(sample - outcome) for sample in ordered) + pair_term = sum((2 * index - count + 1) * sample for index, sample in enumerate(ordered)) + return first_term - pair_term / count**2 + + +def _quantile(samples: tuple[int, ...], probability: float) -> int: + ordered = tuple(sorted(samples)) + index = min(len(ordered) - 1, max(0, round(probability * (len(ordered) - 1)))) + return ordered[index] + + +def _covered(samples: tuple[int, ...], outcome: int, interval: float) -> bool: + tail = (1.0 - interval) / 2.0 + return _quantile(samples, tail) <= outcome <= _quantile(samples, 1.0 - tail) + + +def evaluate_forecasts( + games: tuple[HistoricalGame, ...], + forecasts: tuple[GameForecast, ...], +) -> AccuracyReport: + """Evaluate score distributions and outcome probabilities on frozen games.""" + + validate_historical_games(games) + if any(game.split != "test" for game in games): + raise ValueError("Accuracy reporting accepts only the frozen test split") + forecast_by_id = {forecast.game_id: forecast for forecast in forecasts} + if len(forecast_by_id) != len(forecasts) or set(forecast_by_id) != { + game.game_id for game in games + }: + raise ValueError("Forecast IDs must exactly match historical game IDs") + sample_counts = {len(forecast.home_samples) for forecast in forecasts} + if len(sample_counts) != 1 or 0 in sample_counts: + raise ValueError("Every forecast must contain the same positive sample count") + league_set = {game.league for game in games} + league = league_set.pop() if len(league_set) == 1 else "multi_league" + home_errors: list[float] = [] + away_errors: list[float] = [] + total_errors: list[float] = [] + brier_scores: list[float] = [] + home_crps: list[float] = [] + away_crps: list[float] = [] + coverage_50: list[bool] = [] + coverage_90: list[bool] = [] + for game in games: + forecast = forecast_by_id[game.game_id] + if forecast.league != game.league or len(forecast.home_samples) != len( + forecast.away_samples + ): + raise ValueError(f"Malformed forecast for game: {game.game_id}") + predicted_home = statistics.fmean(forecast.home_samples) + predicted_away = statistics.fmean(forecast.away_samples) + home_errors.append(abs(predicted_home - game.home_score)) + away_errors.append(abs(predicted_away - game.away_score)) + total_errors.append( + abs(predicted_home + predicted_away - game.home_score - game.away_score) + ) + home_win_probability = statistics.fmean( + float(home > away) + for home, away in zip( + forecast.home_samples, + forecast.away_samples, + strict=True, + ) + ) + if game.home_score > game.away_score: + target = 1.0 + elif game.home_score < game.away_score: + target = 0.0 + else: + target = 0.5 + brier_scores.append((home_win_probability - target) ** 2) + home_crps.append(_crps(forecast.home_samples, game.home_score)) + away_crps.append(_crps(forecast.away_samples, game.away_score)) + coverage_50.extend( + ( + _covered(forecast.home_samples, game.home_score, 0.50), + _covered(forecast.away_samples, game.away_score, 0.50), + ) + ) + coverage_90.extend( + ( + _covered(forecast.home_samples, game.home_score, 0.90), + _covered(forecast.away_samples, game.away_score, 0.90), + ) + ) + return AccuracyReport( + league, + len(games), + sample_counts.pop(), + statistics.fmean(home_errors), + statistics.fmean(away_errors), + statistics.fmean(total_errors), + statistics.fmean(brier_scores), + statistics.fmean(home_crps), + statistics.fmean(away_crps), + statistics.fmean(coverage_50), + statistics.fmean(coverage_90), + "historical_test_evaluation_not_certification", + ) diff --git a/src/universal_sports_engine/adaptive.py b/src/universal_sports_engine/adaptive.py new file mode 100644 index 0000000..07fc07f --- /dev/null +++ b/src/universal_sports_engine/adaptive.py @@ -0,0 +1,927 @@ +"""Immutable adaptive analytical profiles, ensembles, and online calibration state.""" + +from __future__ import annotations + +import hashlib +import json +import math +from dataclasses import dataclass + +from universal_sports_engine.core.rng import choice, normal +from universal_sports_engine.core.types import UnknownLeagueError +from universal_sports_engine.reference import ANCHORS + + +@dataclass(frozen=True, slots=True) +class AnalyticalProfile: + """One bounded, content-addressed analytical score model.""" + + league: str + base_home_score: float + base_away_score: float + total_score_sd: float + margin_sd: float + home_strength_exponent: float + away_strength_exponent: float + generation: int + parent_hash: str + evidence_status: str + profile_hash: str + + +@dataclass(frozen=True, slots=True) +class ModelEnsemble: + """A content-addressed mixture of independently scored profiles.""" + + league: str + profiles: tuple[AnalyticalProfile, ...] + weights: tuple[float, ...] + evidence_status: str + ensemble_hash: str + + +@dataclass(frozen=True, slots=True) +class ForecastMoments: + """Distribution moments used for scoring and calibrated intervals.""" + + home_mean: float + away_mean: float + total_mean: float + margin_mean: float + total_sd: float + margin_sd: float + home_win_probability: float + + +@dataclass(frozen=True, slots=True) +class ObservationLoss: + """Proper and diagnostic scores for one resolved game.""" + + joint_negative_log_likelihood: float + home_win_brier: float + total_covered: bool + margin_covered: bool + composite_loss: float + + +@dataclass(frozen=True, slots=True) +class ScoreInput: + """One immutable outcome supplied to a batch scoring boundary.""" + + home_strength: float + away_strength: float + home_score: int + away_score: int + + +@dataclass(frozen=True, slots=True) +class AdaptiveState: + """Online expert weights, interval calibration, and drift state.""" + + ensemble_hash: str + weights: tuple[float, ...] + learning_rate: float + interval_scale: float + interval_learning_rate: float + target_coverage: float + short_loss_ewma: float + long_loss_ewma: float + short_decay: float + long_decay: float + drift_threshold: float + minimum_drift_observations: int + observations: int + drift_score: float + status: str + state_hash: str + + +def _canonical_hash(value: object, personalization: bytes) -> str: + encoded = json.dumps( + value, + allow_nan=False, + separators=(",", ":"), + sort_keys=True, + ).encode() + return hashlib.blake2b(encoded, digest_size=32, person=personalization).hexdigest() + + +def _profile_content( + league: str, + base_home_score: float, + base_away_score: float, + total_score_sd: float, + margin_sd: float, + home_strength_exponent: float, + away_strength_exponent: float, + generation: int, + parent_hash: str, + evidence_status: str, +) -> dict[str, str | int | float]: + return { + "away_strength_exponent": away_strength_exponent, + "base_away_score": base_away_score, + "base_home_score": base_home_score, + "evidence_status": evidence_status, + "generation": generation, + "home_strength_exponent": home_strength_exponent, + "league": league, + "margin_sd": margin_sd, + "parent_hash": parent_hash, + "total_score_sd": total_score_sd, + } + + +def create_profile( + league: str, + base_home_score: float, + base_away_score: float, + total_score_sd: float, + margin_sd: float, + home_strength_exponent: float, + away_strength_exponent: float, + generation: int, + parent_hash: str, + evidence_status: str, +) -> AnalyticalProfile: + """Validate and content-address a candidate profile.""" + + normalized = league.strip().lower() + if normalized not in ANCHORS: + raise UnknownLeagueError(f"No analytical model for league: {league!r}") + values = ( + base_home_score, + base_away_score, + total_score_sd, + margin_sd, + home_strength_exponent, + away_strength_exponent, + ) + if any(not math.isfinite(value) for value in values): + raise ValueError(f"Profile parameters must be finite: league={normalized!r}") + anchor = ANCHORS[normalized] + bounds = ( + (base_home_score, anchor.mean_home_score * 0.40, anchor.mean_home_score * 1.60), + (base_away_score, anchor.mean_away_score * 0.40, anchor.mean_away_score * 1.60), + (total_score_sd, anchor.total_score_sd * 0.35, anchor.total_score_sd * 2.50), + (margin_sd, anchor.margin_sd * 0.35, anchor.margin_sd * 2.50), + (home_strength_exponent, 0.25, 2.00), + (away_strength_exponent, 0.25, 2.00), + ) + for value, minimum, maximum in bounds: + if not minimum <= value <= maximum: + raise ValueError( + "Profile parameter is outside its fail-closed bound: " + f"league={normalized!r}, value={value}, bounds=[{minimum}, {maximum}]" + ) + if generation < 0: + raise ValueError(f"generation must be non-negative: {generation}") + if generation == 0 and parent_hash: + raise ValueError("Generation-zero profiles must not declare a parent hash") + if generation > 0 and ( + len(parent_hash) != 64 + or any(character not in "0123456789abcdef" for character in parent_hash) + ): + raise ValueError( + "Non-root profiles require a lowercase 64-character parent hash: " + f"generation={generation}, parent_hash={parent_hash!r}" + ) + if not evidence_status: + raise ValueError("evidence_status must be non-empty") + content = _profile_content( + normalized, + base_home_score, + base_away_score, + total_score_sd, + margin_sd, + home_strength_exponent, + away_strength_exponent, + generation, + parent_hash, + evidence_status, + ) + profile_hash = _canonical_hash(content, b"USE-PROFILE-V1") + return AnalyticalProfile( + normalized, + base_home_score, + base_away_score, + total_score_sd, + margin_sd, + home_strength_exponent, + away_strength_exponent, + generation, + parent_hash, + evidence_status, + profile_hash, + ) + + +def reference_profile(league: str) -> AnalyticalProfile: + """Return the immutable built-in reference profile for one league.""" + + normalized = league.strip().lower() + anchor = ANCHORS.get(normalized) + if anchor is None: + raise UnknownLeagueError(f"No analytical model for league: {league!r}") + return create_profile( + normalized, + anchor.mean_home_score, + anchor.mean_away_score, + anchor.total_score_sd, + anchor.margin_sd, + 1.0, + 1.0, + 0, + "", + anchor.evidence_status, + ) + + +def verify_profile(profile: AnalyticalProfile) -> None: + """Reject mutated, malformed, or unsupported profile objects.""" + + rebuilt = create_profile( + profile.league, + profile.base_home_score, + profile.base_away_score, + profile.total_score_sd, + profile.margin_sd, + profile.home_strength_exponent, + profile.away_strength_exponent, + profile.generation, + profile.parent_hash, + profile.evidence_status, + ) + if rebuilt.profile_hash != profile.profile_hash: + raise ValueError( + "Profile content hash mismatch: " + f"expected={rebuilt.profile_hash}, actual={profile.profile_hash}" + ) + + +def profile_moments( + profile: AnalyticalProfile, + home_strength: float, + away_strength: float, +) -> ForecastMoments: + """Calculate analytical moments without sampling.""" + + verify_profile(profile) + return _profile_moments_unchecked(profile, home_strength, away_strength) + + +def create_ensemble( + league: str, + profiles: tuple[AnalyticalProfile, ...], + weights: tuple[float, ...], + evidence_status: str, +) -> ModelEnsemble: + """Create a normalized immutable ensemble with no hidden experts.""" + + normalized = league.strip().lower() + if not evidence_status: + raise ValueError("Ensemble evidence_status must be non-empty") + if not profiles or len(profiles) != len(weights): + raise ValueError("Ensemble profiles and weights must have the same positive length") + if len({profile.profile_hash for profile in profiles}) != len(profiles): + raise ValueError("Ensemble profile hashes must be unique") + for profile in profiles: + verify_profile(profile) + if profile.league != normalized: + raise ValueError( + f"Ensemble league mismatch: ensemble={normalized!r}, profile={profile.league!r}" + ) + normalized_weights = _normalize_weights(weights, len(profiles)) + content = { + "evidence_status": evidence_status, + "league": normalized, + "profiles": [profile.profile_hash for profile in profiles], + "weights": list(normalized_weights), + } + ensemble_hash = _canonical_hash(content, b"USE-ENSEMBLE1") + return ModelEnsemble( + normalized, + profiles, + normalized_weights, + evidence_status, + ensemble_hash, + ) + + +def verify_ensemble(ensemble: ModelEnsemble) -> None: + """Verify every expert and the complete ensemble envelope.""" + + rebuilt = create_ensemble( + ensemble.league, + ensemble.profiles, + ensemble.weights, + ensemble.evidence_status, + ) + if rebuilt.ensemble_hash != ensemble.ensemble_hash: + raise ValueError( + "Ensemble content hash mismatch: " + f"expected={rebuilt.ensemble_hash}, actual={ensemble.ensemble_hash}" + ) + + +def ensemble_moments( + ensemble: ModelEnsemble, + weights: tuple[float, ...], + home_strength: float, + away_strength: float, +) -> ForecastMoments: + """Combine expert distributions with full between-expert variance.""" + + verify_ensemble(ensemble) + normalized_weights = _normalize_weights(weights, len(ensemble.profiles)) + return _ensemble_moments_unchecked( + ensemble, + normalized_weights, + home_strength, + away_strength, + ) + + +def _ensemble_moments_unchecked( + ensemble: ModelEnsemble, + normalized_weights: tuple[float, ...], + home_strength: float, + away_strength: float, +) -> ForecastMoments: + moments = tuple( + _profile_moments_unchecked(profile, home_strength, away_strength) + for profile in ensemble.profiles + ) + total_mean = math.fsum( + weight * item.total_mean for weight, item in zip(normalized_weights, moments, strict=True) + ) + margin_mean = math.fsum( + weight * item.margin_mean + for weight, item in zip(normalized_weights, moments, strict=True) + ) + total_variance = math.fsum( + weight * (item.total_sd**2 + (item.total_mean - total_mean) ** 2) + for weight, item in zip(normalized_weights, moments, strict=True) + ) + margin_variance = math.fsum( + weight * (item.margin_sd**2 + (item.margin_mean - margin_mean) ** 2) + for weight, item in zip(normalized_weights, moments, strict=True) + ) + home_win_probability = math.fsum( + weight * item.home_win_probability + for weight, item in zip(normalized_weights, moments, strict=True) + ) + return ForecastMoments( + (total_mean + margin_mean) / 2.0, + (total_mean - margin_mean) / 2.0, + total_mean, + margin_mean, + math.sqrt(total_variance), + math.sqrt(margin_variance), + home_win_probability, + ) + + +def score_observation( + ensemble: ModelEnsemble, + weights: tuple[float, ...], + home_strength: float, + away_strength: float, + home_score: int, + away_score: int, + interval_scale: float, + brier_weight: float, +) -> ObservationLoss: + """Score one outcome under the exact Gaussian mixture.""" + + verify_ensemble(ensemble) + normalized_weights = _normalize_weights(weights, len(ensemble.profiles)) + return _score_observation_unchecked( + ensemble, + normalized_weights, + home_strength, + away_strength, + home_score, + away_score, + interval_scale, + brier_weight, + ) + + +def score_observations( + ensemble: ModelEnsemble, + weights: tuple[float, ...], + inputs: tuple[ScoreInput, ...], + interval_scale: float, + brier_weight: float, +) -> tuple[ObservationLoss, ...]: + """Score a batch after verifying model integrity exactly once.""" + + verify_ensemble(ensemble) + normalized_weights = _normalize_weights(weights, len(ensemble.profiles)) + return tuple( + _score_observation_unchecked( + ensemble, + normalized_weights, + item.home_strength, + item.away_strength, + item.home_score, + item.away_score, + interval_scale, + brier_weight, + ) + for item in inputs + ) + + +def _score_observation_unchecked( + ensemble: ModelEnsemble, + normalized_weights: tuple[float, ...], + home_strength: float, + away_strength: float, + home_score: int, + away_score: int, + interval_scale: float, + brier_weight: float, +) -> ObservationLoss: + if min(home_score, away_score) < 0: + raise ValueError(f"Scores must be non-negative: home={home_score}, away={away_score}") + if not math.isfinite(interval_scale) or interval_scale <= 0.0: + raise ValueError(f"interval_scale must be finite and positive: {interval_scale}") + if not math.isfinite(brier_weight) or brier_weight < 0.0: + raise ValueError(f"brier_weight must be finite and non-negative: {brier_weight}") + total = home_score + away_score + margin = home_score - away_score + log_components: list[float] = [] + expert_moments: list[ForecastMoments] = [] + for weight, profile in zip(normalized_weights, ensemble.profiles, strict=True): + moments = _profile_moments_unchecked(profile, home_strength, away_strength) + expert_moments.append(moments) + if weight > 0.0: + log_components.append( + math.log(weight) + + _normal_log_density(total, moments.total_mean, moments.total_sd) + + _normal_log_density(margin, moments.margin_mean, moments.margin_sd) + ) + joint_nll = -_log_sum_exp(tuple(log_components)) + win_probability = math.fsum( + weight * moments.home_win_probability + for weight, moments in zip(normalized_weights, expert_moments, strict=True) + ) + target = 1.0 if margin > 0 else 0.0 if margin < 0 else 0.5 + brier = (win_probability - target) ** 2 + combined = _ensemble_moments_unchecked( + ensemble, + normalized_weights, + home_strength, + away_strength, + ) + z_value = 1.6448536269514722 * interval_scale + total_covered = abs(total - combined.total_mean) <= z_value * combined.total_sd + margin_covered = abs(margin - combined.margin_mean) <= z_value * combined.margin_sd + return ObservationLoss( + joint_nll, + brier, + total_covered, + margin_covered, + joint_nll + brier_weight * brier, + ) + + +def expert_losses( + ensemble: ModelEnsemble, + home_strength: float, + away_strength: float, + home_score: int, + away_score: int, + brier_weight: float, +) -> tuple[float, ...]: + """Score each expert independently for online multiplicative weighting.""" + + verify_ensemble(ensemble) + return _expert_losses_unchecked( + ensemble, + home_strength, + away_strength, + home_score, + away_score, + brier_weight, + ) + + +def create_adaptive_state( + ensemble: ModelEnsemble, + learning_rate: float, + interval_learning_rate: float, + target_coverage: float, + short_decay: float, + long_decay: float, + drift_threshold: float, + minimum_drift_observations: int, +) -> AdaptiveState: + """Initialize a bounded adaptive state from a verified champion.""" + + verify_ensemble(ensemble) + if not 0.0 < learning_rate <= 2.0: + raise ValueError(f"learning_rate must be within (0, 2]: {learning_rate}") + if not 0.0 < interval_learning_rate <= 0.25: + raise ValueError( + "interval_learning_rate must be within (0, 0.25]: " + f"{interval_learning_rate}" + ) + if not 0.5 <= target_coverage < 1.0: + raise ValueError(f"target_coverage must be within [0.5, 1): {target_coverage}") + if not 0.0 < long_decay < short_decay < 1.0: + raise ValueError( + "EWMA decays must satisfy 0 < long_decay < short_decay < 1: " + f"long={long_decay}, short={short_decay}" + ) + if drift_threshold <= 0.0 or not math.isfinite(drift_threshold): + raise ValueError(f"drift_threshold must be finite and positive: {drift_threshold}") + if minimum_drift_observations <= 0: + raise ValueError( + f"minimum_drift_observations must be positive: {minimum_drift_observations}" + ) + return _state( + ensemble.ensemble_hash, + ensemble.weights, + learning_rate, + 1.0, + interval_learning_rate, + target_coverage, + 0.0, + 0.0, + short_decay, + long_decay, + drift_threshold, + minimum_drift_observations, + 0, + 0.0, + "healthy", + ) + + +def update_adaptive_state( + state: AdaptiveState, + ensemble: ModelEnsemble, + home_strength: float, + away_strength: float, + home_score: int, + away_score: int, + brier_weight: float, +) -> tuple[AdaptiveState, ObservationLoss]: + """Update weights, coverage, and drift after one chronologically revealed outcome.""" + + verify_adaptive_state(state, ensemble) + normalized_weights = _normalize_weights(state.weights, len(ensemble.profiles)) + observation = _score_observation_unchecked( + ensemble, + normalized_weights, + home_strength, + away_strength, + home_score, + away_score, + state.interval_scale, + brier_weight, + ) + losses = _expert_losses_unchecked( + ensemble, + home_strength, + away_strength, + home_score, + away_score, + brier_weight, + ) + best_loss = min(losses) + raw_weights = tuple( + weight * math.exp(-state.learning_rate * min(50.0, loss - best_loss)) + for weight, loss in zip(state.weights, losses, strict=True) + ) + new_weights = _normalize_weights(raw_weights, len(raw_weights)) + miss_rate = 1.0 - (float(observation.total_covered) + float(observation.margin_covered)) / 2.0 + target_miss_rate = 1.0 - state.target_coverage + interval_scale = min( + 4.0, + max( + 0.25, + state.interval_scale + * math.exp(state.interval_learning_rate * (miss_rate - target_miss_rate)), + ), + ) + observations = state.observations + 1 + short_loss = ( + observation.composite_loss + if state.observations == 0 + else state.short_decay * observation.composite_loss + + (1.0 - state.short_decay) * state.short_loss_ewma + ) + long_loss = ( + observation.composite_loss + if state.observations == 0 + else state.long_decay * observation.composite_loss + + (1.0 - state.long_decay) * state.long_loss_ewma + ) + drift_score = max(0.0, short_loss - long_loss) / max(1.0, abs(long_loss)) + status = ( + "drift_alert" + if observations >= state.minimum_drift_observations + and drift_score > state.drift_threshold + else "healthy" + ) + return ( + _state( + state.ensemble_hash, + new_weights, + state.learning_rate, + interval_scale, + state.interval_learning_rate, + state.target_coverage, + short_loss, + long_loss, + state.short_decay, + state.long_decay, + state.drift_threshold, + state.minimum_drift_observations, + observations, + drift_score, + status, + ), + observation, + ) + + +def verify_adaptive_state(state: AdaptiveState, ensemble: ModelEnsemble) -> None: + """Verify state lineage and its content-addressed envelope.""" + + verify_ensemble(ensemble) + if state.ensemble_hash != ensemble.ensemble_hash: + raise ValueError( + "Adaptive state belongs to a different ensemble: " + f"state={state.ensemble_hash}, ensemble={ensemble.ensemble_hash}" + ) + rebuilt = _state( + state.ensemble_hash, + state.weights, + state.learning_rate, + state.interval_scale, + state.interval_learning_rate, + state.target_coverage, + state.short_loss_ewma, + state.long_loss_ewma, + state.short_decay, + state.long_decay, + state.drift_threshold, + state.minimum_drift_observations, + state.observations, + state.drift_score, + state.status, + ) + if rebuilt.state_hash != state.state_hash: + raise ValueError( + "Adaptive state content hash mismatch: " + f"expected={rebuilt.state_hash}, actual={state.state_hash}" + ) + + +def sample_profile_scores( + profile: AnalyticalProfile, + seed: int, + home_strength: float, + away_strength: float, +) -> tuple[int, int]: + """Sample a score pair from one verified profile.""" + + if seed < 0: + raise ValueError(f"seed must be non-negative: {seed}") + moments = profile_moments(profile, home_strength, away_strength) + total = normal( + seed, + moments.total_mean, + moments.total_sd, + profile.league, + profile.profile_hash, + "adaptive_total", + ) + margin = normal( + seed, + moments.margin_mean, + moments.margin_sd, + profile.league, + profile.profile_hash, + "adaptive_margin", + ) + return ( + _legalize_score(profile.league, (total + margin) / 2.0), + _legalize_score(profile.league, (total - margin) / 2.0), + ) + + +def sample_ensemble_scores( + ensemble: ModelEnsemble, + weights: tuple[float, ...], + seed: int, + home_strength: float, + away_strength: float, +) -> tuple[int, int, str]: + """Sample a mixture expert deterministically, then sample its score distribution.""" + + verify_ensemble(ensemble) + normalized_weights = _normalize_weights(weights, len(ensemble.profiles)) + index = choice(seed, normalized_weights, ensemble.league, ensemble.ensemble_hash, "expert") + profile = ensemble.profiles[index] + home_score, away_score = sample_profile_scores( + profile, + seed, + home_strength, + away_strength, + ) + return home_score, away_score, profile.profile_hash + + +def _state( + ensemble_hash: str, + weights: tuple[float, ...], + learning_rate: float, + interval_scale: float, + interval_learning_rate: float, + target_coverage: float, + short_loss_ewma: float, + long_loss_ewma: float, + short_decay: float, + long_decay: float, + drift_threshold: float, + minimum_drift_observations: int, + observations: int, + drift_score: float, + status: str, +) -> AdaptiveState: + numeric = ( + learning_rate, + interval_scale, + interval_learning_rate, + target_coverage, + short_loss_ewma, + long_loss_ewma, + short_decay, + long_decay, + drift_threshold, + drift_score, + ) + if any(not math.isfinite(value) for value in numeric): + raise ValueError("Adaptive state numeric fields must be finite") + if not 0.0 < learning_rate <= 2.0: + raise ValueError(f"Adaptive learning_rate is invalid: {learning_rate}") + if not 0.25 <= interval_scale <= 4.0: + raise ValueError(f"Adaptive interval_scale is invalid: {interval_scale}") + if not 0.0 < interval_learning_rate <= 0.25: + raise ValueError( + f"Adaptive interval_learning_rate is invalid: {interval_learning_rate}" + ) + if not 0.5 <= target_coverage < 1.0: + raise ValueError(f"Adaptive target_coverage is invalid: {target_coverage}") + if not 0.0 < long_decay < short_decay < 1.0: + raise ValueError( + "Adaptive EWMA decays are invalid: " + f"long_decay={long_decay}, short_decay={short_decay}" + ) + if drift_threshold <= 0.0 or drift_score < 0.0: + raise ValueError( + "Adaptive drift values are invalid: " + f"threshold={drift_threshold}, score={drift_score}" + ) + if minimum_drift_observations <= 0 or observations < 0: + raise ValueError( + "Adaptive observation counts are invalid: " + f"minimum={minimum_drift_observations}, observations={observations}" + ) + if status not in {"healthy", "drift_alert"}: + raise ValueError(f"Adaptive status is invalid: {status!r}") + normalized_weights = _normalize_weights(weights, len(weights)) + content: dict[str, object] = { + "drift_score": drift_score, + "drift_threshold": drift_threshold, + "ensemble_hash": ensemble_hash, + "interval_learning_rate": interval_learning_rate, + "interval_scale": interval_scale, + "learning_rate": learning_rate, + "long_decay": long_decay, + "long_loss_ewma": long_loss_ewma, + "minimum_drift_observations": minimum_drift_observations, + "observations": observations, + "short_decay": short_decay, + "short_loss_ewma": short_loss_ewma, + "status": status, + "target_coverage": target_coverage, + "weights": list(normalized_weights), + } + state_hash = _canonical_hash(content, b"USE-ADAPT-V1") + return AdaptiveState( + ensemble_hash, + normalized_weights, + learning_rate, + interval_scale, + interval_learning_rate, + target_coverage, + short_loss_ewma, + long_loss_ewma, + short_decay, + long_decay, + drift_threshold, + minimum_drift_observations, + observations, + drift_score, + status, + state_hash, + ) + + +def _profile_moments_unchecked( + profile: AnalyticalProfile, + home_strength: float, + away_strength: float, +) -> ForecastMoments: + if not 0.25 <= home_strength <= 2.0 or not 0.25 <= away_strength <= 2.0: + raise ValueError( + "Strength multipliers must be within [0.25, 2.0]: " + f"home={home_strength}, away={away_strength}" + ) + home_mean = profile.base_home_score * home_strength**profile.home_strength_exponent + away_mean = profile.base_away_score * away_strength**profile.away_strength_exponent + margin_mean = home_mean - away_mean + home_win_probability = _normal_cdf(margin_mean / profile.margin_sd) + return ForecastMoments( + home_mean, + away_mean, + home_mean + away_mean, + margin_mean, + profile.total_score_sd, + profile.margin_sd, + home_win_probability, + ) + + +def _expert_losses_unchecked( + ensemble: ModelEnsemble, + home_strength: float, + away_strength: float, + home_score: int, + away_score: int, + brier_weight: float, +) -> tuple[float, ...]: + total = home_score + away_score + margin = home_score - away_score + target = 1.0 if margin > 0 else 0.0 if margin < 0 else 0.5 + losses = [] + for profile in ensemble.profiles: + moments = _profile_moments_unchecked(profile, home_strength, away_strength) + joint_nll = -( + _normal_log_density(total, moments.total_mean, moments.total_sd) + + _normal_log_density(margin, moments.margin_mean, moments.margin_sd) + ) + brier = (moments.home_win_probability - target) ** 2 + losses.append(joint_nll + brier_weight * brier) + return tuple(losses) + + +def _normalize_weights(weights: tuple[float, ...], expected: int) -> tuple[float, ...]: + if len(weights) != expected or expected <= 0: + raise ValueError( + f"Weight count mismatch: expected={expected}, actual={len(weights)}" + ) + if any(not math.isfinite(weight) or weight < 0.0 for weight in weights): + raise ValueError(f"Weights must be finite and non-negative: {weights!r}") + total = math.fsum(weights) + if total <= 0.0: + raise ValueError("At least one weight must be positive") + if abs(total - 1.0) <= 1e-12: + return weights + return tuple(weight / total for weight in weights) + + +def _normal_log_density(value: float, mean: float, standard_deviation: float) -> float: + if standard_deviation <= 0.0: + raise ValueError(f"standard_deviation must be positive: {standard_deviation}") + standardized = (value - mean) / standard_deviation + return ( + -0.5 * standardized**2 + - math.log(standard_deviation) + - 0.5 * math.log(2.0 * math.pi) + ) + + +def _normal_cdf(value: float) -> float: + return 0.5 * (1.0 + math.erf(value / math.sqrt(2.0))) + + +def _log_sum_exp(values: tuple[float, ...]) -> float: + if not values: + raise ValueError("log-sum-exp requires at least one value") + maximum = max(values) + return maximum + math.log(math.fsum(math.exp(value - maximum) for value in values)) + + +def _legalize_score(league: str, value: float) -> int: + score = max(0, round(value)) + if league in {"nfl", "ncaaf"} and score == 1: + return 2 + return score diff --git a/src/universal_sports_engine/analytical.py b/src/universal_sports_engine/analytical.py new file mode 100644 index 0000000..658f6f7 --- /dev/null +++ b/src/universal_sports_engine/analytical.py @@ -0,0 +1,212 @@ +"""High-throughput F0 score-distribution simulator.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from universal_sports_engine.adaptive import ( + AdaptiveState, + AnalyticalProfile, + ModelEnsemble, + sample_ensemble_scores, + sample_profile_scores, + verify_adaptive_state, + verify_ensemble, + verify_profile, +) +from universal_sports_engine.core.rng import normal +from universal_sports_engine.core.types import UnknownLeagueError +from universal_sports_engine.reference import ANCHORS, ReferenceAnchor + + +@dataclass(frozen=True, slots=True) +class AnalyticalResult: + league: str + home_score: int + away_score: int + seed: int + fidelity: str + evidence_status: str + + +@dataclass(frozen=True, slots=True) +class AdaptiveAnalyticalResult: + """F0 result with complete adaptive-model lineage.""" + + league: str + home_score: int + away_score: int + seed: int + fidelity: str + evidence_status: str + ensemble_hash: str + selected_profile_hash: str + adaptive_state_hash: str + + +def _legalize_score(league: str, value: float) -> int: + score = max(0, round(value)) + if league in {"nfl", "ncaaf"} and score == 1: + return 2 + return score + + +def simulate_analytical( + league: str, + seed: int, + home_strength: float, + away_strength: float, +) -> AnalyticalResult: + """Sample a fast aggregate score pair from explicit provisional moments.""" + + normalized = league.strip().lower() + anchor = ANCHORS.get(normalized) + if anchor is None: + raise UnknownLeagueError(f"No analytical model for league: {league!r}") + if seed < 0: + raise ValueError(f"seed must be non-negative: {seed}") + if not 0.25 <= home_strength <= 2.0 or not 0.25 <= away_strength <= 2.0: + raise ValueError( + "Strength multipliers must be within [0.25, 2.0]: " + f"home={home_strength}, away={away_strength}" + ) + return _simulate_analytical_validated( + normalized, + anchor, + seed, + home_strength, + away_strength, + ) + + +def _simulate_analytical_validated( + league: str, + anchor: ReferenceAnchor, + seed: int, + home_strength: float, + away_strength: float, +) -> AnalyticalResult: + home_mean = anchor.mean_home_score * home_strength + away_mean = anchor.mean_away_score * away_strength + total = normal( + seed, + home_mean + away_mean, + anchor.total_score_sd, + league, + "analytical_total", + ) + margin = normal( + seed, + home_mean - away_mean, + anchor.margin_sd, + league, + "analytical_margin", + ) + home_score = _legalize_score(league, (total + margin) / 2.0) + away_score = _legalize_score(league, (total - margin) / 2.0) + return AnalyticalResult( + league, + home_score, + away_score, + seed, + "F0_ANALYTICAL", + anchor.evidence_status, + ) + + +def simulate_many_analytical( + league: str, + games: int, + seed: int, + home_strength: float, + away_strength: float, +) -> tuple[AnalyticalResult, ...]: + """Run aggregate simulations without F1 event construction overhead.""" + + if games <= 0: + raise ValueError(f"games must be positive: {games}") + normalized = league.strip().lower() + anchor = ANCHORS.get(normalized) + if anchor is None: + raise UnknownLeagueError(f"No analytical model for league: {league!r}") + if seed < 0: + raise ValueError(f"seed must be non-negative: {seed}") + if not 0.25 <= home_strength <= 2.0 or not 0.25 <= away_strength <= 2.0: + raise ValueError( + "Strength multipliers must be within [0.25, 2.0]: " + f"home={home_strength}, away={away_strength}" + ) + return tuple( + _simulate_analytical_validated( + normalized, + anchor, + seed + index, + home_strength, + away_strength, + ) + for index in range(games) + ) + + +def simulate_profile_analytical( + profile: AnalyticalProfile, + seed: int, + home_strength: float, + away_strength: float, +) -> AdaptiveAnalyticalResult: + """Sample an explicitly versioned analytical profile.""" + + verify_profile(profile) + home_score, away_score = sample_profile_scores( + profile, + seed, + home_strength, + away_strength, + ) + return AdaptiveAnalyticalResult( + profile.league, + home_score, + away_score, + seed, + "F0_ADAPTIVE_PROFILE", + profile.evidence_status, + profile.profile_hash, + profile.profile_hash, + "", + ) + + +def simulate_ensemble_analytical( + ensemble: ModelEnsemble, + state: AdaptiveState, + seed: int, + home_strength: float, + away_strength: float, +) -> AdaptiveAnalyticalResult: + """Sample a governed ensemble using its current adaptive weights.""" + + verify_ensemble(ensemble) + verify_adaptive_state(state, ensemble) + if state.status != "healthy": + raise RuntimeError( + "Adaptive simulation is fail-closed while drift is unresolved: " + f"status={state.status}, drift_score={state.drift_score}" + ) + home_score, away_score, profile_hash = sample_ensemble_scores( + ensemble, + state.weights, + seed, + home_strength, + away_strength, + ) + return AdaptiveAnalyticalResult( + ensemble.league, + home_score, + away_score, + seed, + "F0_ADAPTIVE_ENSEMBLE", + ensemble.evidence_status, + ensemble.ensemble_hash, + profile_hash, + state.state_hash, + ) diff --git a/src/universal_sports_engine/autotune.py b/src/universal_sports_engine/autotune.py new file mode 100644 index 0000000..44ae19b --- /dev/null +++ b/src/universal_sports_engine/autotune.py @@ -0,0 +1,1036 @@ +"""Deterministic recursive model search and fail-closed champion governance.""" + +from __future__ import annotations + +import hashlib +import json +import math +import os +import statistics +from dataclasses import asdict, dataclass +from datetime import datetime +from pathlib import Path +from typing import cast + +from universal_sports_engine.accuracy import HistoricalGame, validate_historical_games +from universal_sports_engine.adaptive import ( + AdaptiveState, + AnalyticalProfile, + ModelEnsemble, + ObservationLoss, + ScoreInput, + create_adaptive_state, + create_ensemble, + create_profile, + reference_profile, + score_observations, + update_adaptive_state, + verify_adaptive_state, + verify_ensemble, +) +from universal_sports_engine.core.rng import normal +from universal_sports_engine.reference import ANCHORS + +type JsonValue = None | bool | int | float | str | list[JsonValue] | dict[str, JsonValue] + + +@dataclass(frozen=True, slots=True) +class TuningConfig: + """Every autonomous search and promotion limit, declared before outcomes.""" + + generations: int + population_size: int + elite_count: int + experts_per_ensemble: int + mutation_scale: float + minimum_train_games: int + minimum_calibration_games: int + minimum_relative_improvement: float + maximum_brier_deterioration: float + maximum_coverage_error_deterioration: float + brier_weight: float + coverage_weight: float + online_learning_rate: float + interval_learning_rate: float + target_coverage: float + short_decay: float + long_decay: float + drift_threshold: float + minimum_drift_observations: int + seed: int + + +@dataclass(frozen=True, slots=True) +class AggregateScore: + """Chronological aggregate of proper scores and coverage diagnostics.""" + + games: int + mean_joint_negative_log_likelihood: float + home_win_brier: float + interval_coverage: float + interval_coverage_error: float + composite_loss: float + + +@dataclass(frozen=True, slots=True) +class GenerationReport: + """Best immutable candidate from one evolutionary generation.""" + + generation: int + candidates_evaluated: int + best_profile_hash: str + best_train_score: AggregateScore + + +@dataclass(frozen=True, slots=True) +class ImprovementResult: + """Complete evidence for one league's champion/challenger decision.""" + + league: str + promoted: bool + decision: str + relative_improvement: float + incumbent_ensemble: ModelEnsemble + incumbent_state: AdaptiveState + challenger_ensemble: ModelEnsemble + challenger_state: AdaptiveState + selected_ensemble: ModelEnsemble + selected_state: AdaptiveState + incumbent_calibration_score: AggregateScore + challenger_calibration_score: AggregateScore + generations: tuple[GenerationReport, ...] + + +@dataclass(frozen=True, slots=True) +class LeagueChampion: + """One selected league model plus its rollback lineage.""" + + league: str + ensemble: ModelEnsemble + state: AdaptiveState + promoted: bool + rollback_ensemble: ModelEnsemble + rollback_state: AdaptiveState + + +@dataclass(frozen=True, slots=True) +class ChampionBundle: + """Content-addressed, multi-league model checkpoint.""" + + protocol_version: str + created_at: str + champions: tuple[LeagueChampion, ...] + bundle_hash: str + + +def validate_tuning_config(config: TuningConfig) -> None: + """Fail before search when a budget or promotion gate is unsafe.""" + + positive_integers = ( + config.generations, + config.population_size, + config.elite_count, + config.experts_per_ensemble, + config.minimum_train_games, + config.minimum_calibration_games, + config.minimum_drift_observations, + ) + if any(value <= 0 for value in positive_integers): + raise ValueError(f"Tuning integer limits must be positive: {positive_integers!r}") + if config.elite_count > config.population_size: + raise ValueError("elite_count cannot exceed population_size") + if config.experts_per_ensemble > config.population_size + 1: + raise ValueError("experts_per_ensemble exceeds the available candidate pool") + bounded = ( + ("mutation_scale", config.mutation_scale, 0.001, 0.50), + ( + "minimum_relative_improvement", + config.minimum_relative_improvement, + 0.0, + 0.50, + ), + ( + "maximum_brier_deterioration", + config.maximum_brier_deterioration, + 0.0, + 0.25, + ), + ( + "maximum_coverage_error_deterioration", + config.maximum_coverage_error_deterioration, + 0.0, + 0.25, + ), + ("brier_weight", config.brier_weight, 0.0, 20.0), + ("coverage_weight", config.coverage_weight, 0.0, 20.0), + ) + for name, value, minimum, maximum in bounded: + if not math.isfinite(value) or not minimum <= value <= maximum: + raise ValueError(f"{name} must be within [{minimum}, {maximum}]: {value}") + if config.seed < 0: + raise ValueError(f"seed must be non-negative: {config.seed}") + create_adaptive_state( + create_ensemble("nba", (reference_profile("nba"),), (1.0,), "config_validation"), + config.online_learning_rate, + config.interval_learning_rate, + config.target_coverage, + config.short_decay, + config.long_decay, + config.drift_threshold, + config.minimum_drift_observations, + ) + + +def load_tuning_config(path: Path) -> TuningConfig: + """Load an exact, reviewable tuning contract without implicit defaults.""" + + raw = cast(object, json.loads(path.read_text(encoding="utf-8"))) + if not isinstance(raw, dict) or any(not isinstance(key, str) for key in raw): + raise ValueError(f"Tuning config must be a JSON object: {path}") + value = cast(dict[str, object], raw) + expected = { + "brier_weight", + "coverage_weight", + "drift_threshold", + "elite_count", + "experts_per_ensemble", + "generations", + "interval_learning_rate", + "long_decay", + "maximum_brier_deterioration", + "maximum_coverage_error_deterioration", + "minimum_calibration_games", + "minimum_drift_observations", + "minimum_relative_improvement", + "minimum_train_games", + "mutation_scale", + "online_learning_rate", + "population_size", + "seed", + "short_decay", + "target_coverage", + } + actual = set(value) + if actual != expected: + raise ValueError( + f"Tuning config fields mismatch: missing={sorted(expected - actual)!r}, " + f"unexpected={sorted(actual - expected)!r}" + ) + config = TuningConfig( + _required_int(value, "generations"), + _required_int(value, "population_size"), + _required_int(value, "elite_count"), + _required_int(value, "experts_per_ensemble"), + _required_float(value, "mutation_scale"), + _required_int(value, "minimum_train_games"), + _required_int(value, "minimum_calibration_games"), + _required_float(value, "minimum_relative_improvement"), + _required_float(value, "maximum_brier_deterioration"), + _required_float(value, "maximum_coverage_error_deterioration"), + _required_float(value, "brier_weight"), + _required_float(value, "coverage_weight"), + _required_float(value, "online_learning_rate"), + _required_float(value, "interval_learning_rate"), + _required_float(value, "target_coverage"), + _required_float(value, "short_decay"), + _required_float(value, "long_decay"), + _required_float(value, "drift_threshold"), + _required_int(value, "minimum_drift_observations"), + _required_int(value, "seed"), + ) + validate_tuning_config(config) + return config + + +def tune_league( + league: str, + games: tuple[HistoricalGame, ...], + config: TuningConfig, +) -> ImprovementResult: + """Recursively search train data and promote only on chronological calibration data.""" + + validate_tuning_config(config) + validate_historical_games(games) + normalized = league.strip().lower() + league_games = tuple( + sorted( + (game for game in games if game.league == normalized), + key=lambda game: game.decision_time, + ) + ) + training = tuple(game for game in league_games if game.split == "train") + calibration = tuple(game for game in league_games if game.split == "calibration") + if len(training) < config.minimum_train_games: + raise ValueError( + "Insufficient chronological training evidence: " + f"league={normalized}, required={config.minimum_train_games}, actual={len(training)}" + ) + if len(calibration) < config.minimum_calibration_games: + raise ValueError( + "Insufficient chronological calibration evidence: " + f"league={normalized}, required={config.minimum_calibration_games}, " + f"actual={len(calibration)}" + ) + incumbent_profile = reference_profile(normalized) + incumbent = create_ensemble( + normalized, + (incumbent_profile,), + (1.0,), + incumbent_profile.evidence_status, + ) + incumbent_score, incumbent_state = evaluate_prequential(incumbent, calibration, config) + elites: tuple[AnalyticalProfile, ...] = (incumbent_profile,) + all_candidates: dict[str, AnalyticalProfile] = { + incumbent_profile.profile_hash: incumbent_profile + } + reports: list[GenerationReport] = [] + ranked: tuple[tuple[AggregateScore, AnalyticalProfile], ...] = () + for generation in range(1, config.generations + 1): + population: dict[str, AnalyticalProfile] = { + profile.profile_hash: profile for profile in elites + } + candidate_index = 0 + while len(population) < config.population_size: + parent = elites[candidate_index % len(elites)] + candidate = mutate_profile(parent, generation, candidate_index, config) + population[candidate.profile_hash] = candidate + all_candidates[candidate.profile_hash] = candidate + candidate_index += 1 + if candidate_index > config.population_size * 100: + raise RuntimeError( + "Candidate generation could not produce a unique bounded population: " + f"league={normalized}, generation={generation}" + ) + ranked = tuple( + sorted( + [ + (score_profile(profile, training, config), profile) + for profile in population.values() + ], + key=lambda row: (row[0].composite_loss, row[1].profile_hash), + ) + ) + score_rows = tuple((score.composite_loss, profile, score) for score, profile in ranked) + elites = tuple(row[1] for row in score_rows[: config.elite_count]) + best = score_rows[0] + reports.append( + GenerationReport(generation, len(population), best[1].profile_hash, best[2]) + ) + if not ranked: + raise RuntimeError("Recursive tuning produced no evaluated candidates") + globally_ranked = tuple( + sorted( + ( + (score_profile(profile, training, config), profile.profile_hash, profile) + for profile in all_candidates.values() + ), + key=lambda row: (row[0].composite_loss, row[1]), + ) + ) + selected_profiles: list[AnalyticalProfile] = [incumbent_profile] + for _, _, profile in globally_ranked: + if profile.profile_hash != incumbent_profile.profile_hash: + selected_profiles.append(profile) + if len(selected_profiles) >= config.experts_per_ensemble: + break + selected_train_scores = tuple( + score_profile(profile, training, config).composite_loss for profile in selected_profiles + ) + best_train_score = min(selected_train_scores) + initial_weights = tuple( + math.exp( + -config.online_learning_rate + * min(50.0, score - best_train_score) + ) + for score in selected_train_scores + ) + challenger = create_ensemble( + normalized, + tuple(selected_profiles), + initial_weights, + "chronological_calibration_challenger", + ) + challenger_score, challenger_state = evaluate_prequential(challenger, calibration, config) + denominator = max(abs(incumbent_score.composite_loss), 1e-12) + relative_improvement = ( + incumbent_score.composite_loss - challenger_score.composite_loss + ) / denominator + brier_gate = ( + challenger_score.home_win_brier + <= incumbent_score.home_win_brier + config.maximum_brier_deterioration + ) + coverage_gate = ( + challenger_score.interval_coverage_error + <= incumbent_score.interval_coverage_error + + config.maximum_coverage_error_deterioration + ) + improvement_gate = relative_improvement >= config.minimum_relative_improvement + drift_gate = challenger_state.status == "healthy" + promoted = improvement_gate and brier_gate and coverage_gate and drift_gate + reasons: list[str] = [] + if not improvement_gate: + reasons.append("minimum_relative_improvement_not_met") + if not brier_gate: + reasons.append("brier_guardrail_failed") + if not coverage_gate: + reasons.append("coverage_guardrail_failed") + if not drift_gate: + reasons.append("calibration_drift_detected") + decision = "promoted" if promoted else "retained_incumbent:" + ",".join(reasons) + selected_ensemble = challenger if promoted else incumbent + selected_state = challenger_state if promoted else incumbent_state + return ImprovementResult( + normalized, + promoted, + decision, + relative_improvement, + incumbent, + incumbent_state, + challenger, + challenger_state, + selected_ensemble, + selected_state, + incumbent_score, + challenger_score, + tuple(reports), + ) + + +def run_recursive_improvement( + games: tuple[HistoricalGame, ...], + config: TuningConfig, +) -> tuple[ImprovementResult, ...]: + """Tune every league that has explicitly separated train and calibration evidence.""" + + validate_tuning_config(config) + validate_historical_games(games) + eligible = tuple( + sorted( + league + for league in {game.league for game in games} + if any(game.league == league and game.split == "train" for game in games) + and any(game.league == league and game.split == "calibration" for game in games) + ) + ) + if not eligible: + raise ValueError("No league has both train and calibration evidence") + return tuple(tune_league(league, games, config) for league in eligible) + + +def mutate_profile( + parent: AnalyticalProfile, + generation: int, + candidate_index: int, + config: TuningConfig, +) -> AnalyticalProfile: + """Apply deterministic bounded log-space mutations to one parent.""" + + if generation <= 0 or candidate_index < 0: + raise ValueError( + f"Mutation coordinates must be non-negative: generation={generation}, " + f"candidate_index={candidate_index}" + ) + anchor = ANCHORS[parent.league] + scale = config.mutation_scale / math.sqrt(generation) + + def mutate(value: float, minimum: float, maximum: float, field: str) -> float: + changed = value * math.exp( + normal( + config.seed, + 0.0, + scale, + parent.league, + generation, + candidate_index, + field, + ) + ) + return min(maximum, max(minimum, changed)) + + return create_profile( + parent.league, + mutate( + parent.base_home_score, + anchor.mean_home_score * 0.40, + anchor.mean_home_score * 1.60, + "base_home_score", + ), + mutate( + parent.base_away_score, + anchor.mean_away_score * 0.40, + anchor.mean_away_score * 1.60, + "base_away_score", + ), + mutate( + parent.total_score_sd, + anchor.total_score_sd * 0.35, + anchor.total_score_sd * 2.50, + "total_score_sd", + ), + mutate( + parent.margin_sd, + anchor.margin_sd * 0.35, + anchor.margin_sd * 2.50, + "margin_sd", + ), + mutate(parent.home_strength_exponent, 0.25, 2.00, "home_strength_exponent"), + mutate(parent.away_strength_exponent, 0.25, 2.00, "away_strength_exponent"), + generation, + parent.profile_hash, + "train_search_candidate", + ) + + +def score_profile( + profile: AnalyticalProfile, + games: tuple[HistoricalGame, ...], + config: TuningConfig, +) -> AggregateScore: + """Score a fixed profile without allowing outcome-dependent weight changes.""" + + ensemble = create_ensemble( + profile.league, + (profile,), + (1.0,), + profile.evidence_status, + ) + return score_fixed_ensemble(ensemble, ensemble.weights, games, 1.0, config) + + +def score_fixed_ensemble( + ensemble: ModelEnsemble, + weights: tuple[float, ...], + games: tuple[HistoricalGame, ...], + interval_scale: float, + config: TuningConfig, +) -> AggregateScore: + """Score a frozen ensemble on an explicitly supplied chronological block.""" + + if not games: + raise ValueError("At least one game is required for model scoring") + inputs = tuple( + ScoreInput( + game.home_strength, + game.away_strength, + game.home_score, + game.away_score, + ) + for game in games + ) + observations = score_observations( + ensemble, + weights, + inputs, + interval_scale, + config.brier_weight, + ) + return _aggregate(observations, config) + + +def evaluate_prequential( + ensemble: ModelEnsemble, + games: tuple[HistoricalGame, ...], + config: TuningConfig, +) -> tuple[AggregateScore, AdaptiveState]: + """Predict before each outcome, then adjust weights and intervals exactly once.""" + + if not games: + raise ValueError("Prequential evaluation requires at least one chronological game") + state = create_adaptive_state( + ensemble, + config.online_learning_rate, + config.interval_learning_rate, + config.target_coverage, + config.short_decay, + config.long_decay, + config.drift_threshold, + config.minimum_drift_observations, + ) + observations = [] + for game in sorted(games, key=lambda item: item.decision_time): + state, observation = update_adaptive_state( + state, + ensemble, + game.home_strength, + game.away_strength, + game.home_score, + game.away_score, + config.brier_weight, + ) + observations.append(observation) + return _aggregate(tuple(observations), config), state + + +def create_champion_bundle( + results: tuple[ImprovementResult, ...], + protocol_version: str, + created_at: str, +) -> ChampionBundle: + """Freeze selected champions with explicit rollback hashes.""" + + if not results: + raise ValueError("Champion bundle requires at least one improvement result") + _validate_bundle_identity(protocol_version, created_at) + champions = tuple( + LeagueChampion( + result.league, + result.selected_ensemble, + result.selected_state, + result.promoted, + result.incumbent_ensemble, + result.incumbent_state, + ) + for result in sorted(results, key=lambda item: item.league) + ) + content = { + "champions": [_champion_to_dict(champion) for champion in champions], + "created_at": created_at, + "protocol_version": protocol_version, + } + bundle_hash = _json_hash(content, b"USE-BUNDLE-V1") + return ChampionBundle(protocol_version, created_at, champions, bundle_hash) + + +def verify_champion_bundle(bundle: ChampionBundle) -> None: + """Verify all model/state hashes and the bundle checkpoint envelope.""" + + if not bundle.champions: + raise ValueError("Champion bundle must not be empty") + for champion in bundle.champions: + verify_ensemble(champion.ensemble) + verify_adaptive_state(champion.state, champion.ensemble) + verify_ensemble(champion.rollback_ensemble) + verify_adaptive_state(champion.rollback_state, champion.rollback_ensemble) + if champion.league != champion.ensemble.league: + raise ValueError(f"Champion league mismatch: {champion.league!r}") + if champion.league != champion.rollback_ensemble.league: + raise ValueError(f"Rollback league mismatch: {champion.league!r}") + rebuilt = create_champion_bundle_from_champions( + bundle.champions, + bundle.protocol_version, + bundle.created_at, + ) + if rebuilt.bundle_hash != bundle.bundle_hash: + raise ValueError( + "Champion bundle content hash mismatch: " + f"expected={rebuilt.bundle_hash}, actual={bundle.bundle_hash}" + ) + + +def create_champion_bundle_from_champions( + champions: tuple[LeagueChampion, ...], + protocol_version: str, + created_at: str, +) -> ChampionBundle: + """Rebuild a bundle from parsed champion records.""" + + if not champions: + raise ValueError("Champion bundle requires at least one champion") + _validate_bundle_identity(protocol_version, created_at) + content = { + "champions": [_champion_to_dict(champion) for champion in champions], + "created_at": created_at, + "protocol_version": protocol_version, + } + return ChampionBundle( + protocol_version, + created_at, + champions, + _json_hash(content, b"USE-BUNDLE-V1"), + ) + + +def write_improvement_artifacts( + output: Path, + results: tuple[ImprovementResult, ...], + bundle: ChampionBundle, + config: TuningConfig, +) -> None: + """Atomically publish a model checkpoint, report, and chained decision ledger.""" + + verify_champion_bundle(bundle) + output.mkdir(parents=True, exist_ok=True) + targets = ( + output / "champion_bundle.json", + output / "improvement_report.json", + output / "improvement_ledger.jsonl", + ) + existing = tuple(path for path in targets if path.exists()) + if existing: + raise FileExistsError( + "Refusing to overwrite autonomous evidence artifacts: " + f"{tuple(str(path) for path in existing)!r}" + ) + bundle_value = _bundle_to_dict(bundle) + report_value: JsonValue = { + "bundle_hash": bundle.bundle_hash, + "config": cast(JsonValue, asdict(config)), + "results": cast(JsonValue, [asdict(result) for result in results]), + "status": "chronological_calibration_only_not_real_world_certification", + } + ledger = _ledger_records(results, bundle.bundle_hash) + _atomic_write(targets[0], _json_text(bundle_value)) + _atomic_write(targets[1], _json_text(report_value)) + _atomic_write( + targets[2], + "".join(_json_line(record) for record in ledger), + ) + + +def read_champion_bundle(path: Path) -> ChampionBundle: + """Load and fully verify a champion checkpoint.""" + + value = cast(dict[str, JsonValue], json.loads(path.read_text(encoding="utf-8"))) + champions_value = cast(list[JsonValue], value["champions"]) + champions = tuple( + _champion_from_dict(cast(dict[str, JsonValue], item)) for item in champions_value + ) + bundle = ChampionBundle( + cast(str, value["protocol_version"]), + cast(str, value["created_at"]), + champions, + cast(str, value["bundle_hash"]), + ) + verify_champion_bundle(bundle) + return bundle + + +def champion_for(bundle: ChampionBundle, league: str) -> LeagueChampion: + """Resolve one verified league champion or fail explicitly.""" + + verify_champion_bundle(bundle) + normalized = league.strip().lower() + for champion in bundle.champions: + if champion.league == normalized: + return champion + raise KeyError(f"Champion bundle has no league: {normalized!r}") + + +def adapt_champion_on_shadow( + bundle: ChampionBundle, + league: str, + games: tuple[HistoricalGame, ...], + config: TuningConfig, + created_at: str, +) -> ChampionBundle: + """Update one champion only after chronologically revealed shadow outcomes.""" + + validate_tuning_config(config) + validate_historical_games(games) + champion = champion_for(bundle, league) + normalized = champion.league + shadow = tuple( + sorted( + ( + game + for game in games + if game.league == normalized and game.split == "shadow" + ), + key=lambda game: game.decision_time, + ) + ) + if not shadow: + raise ValueError(f"No shadow outcomes available for league: {normalized!r}") + if any(game.league != normalized or game.split != "shadow" for game in games): + raise ValueError( + "Online adaptation accepts exactly one league and shadow-split records only" + ) + state = champion.state + for game in shadow: + state, _ = update_adaptive_state( + state, + champion.ensemble, + game.home_strength, + game.away_strength, + game.home_score, + game.away_score, + config.brier_weight, + ) + updated = LeagueChampion( + champion.league, + champion.ensemble, + state, + champion.promoted, + champion.rollback_ensemble, + champion.rollback_state, + ) + champions = tuple( + updated if item.league == normalized else item for item in bundle.champions + ) + return create_champion_bundle_from_champions( + champions, + bundle.protocol_version, + created_at, + ) + + +def write_champion_bundle(path: Path, bundle: ChampionBundle) -> None: + """Atomically write one independently verifiable model checkpoint.""" + + verify_champion_bundle(bundle) + path.parent.mkdir(parents=True, exist_ok=True) + if path.exists(): + raise FileExistsError(f"Refusing to overwrite champion checkpoint: {path}") + _atomic_write(path, _json_text(_bundle_to_dict(bundle))) + + +def verify_improvement_ledger(path: Path, bundle_hash: str) -> int: + """Verify sequence, bundle binding, and the complete ledger hash chain.""" + + lines = tuple(line for line in path.read_text(encoding="utf-8").splitlines() if line) + if not lines: + raise ValueError(f"Improvement ledger is empty: {path}") + previous_hash = "0" * 64 + for sequence, line in enumerate(lines): + parsed = cast(object, json.loads(line)) + if not isinstance(parsed, dict) or any(not isinstance(key, str) for key in parsed): + raise ValueError(f"Malformed improvement ledger record: sequence={sequence}") + record = cast(dict[str, JsonValue], parsed) + actual_hash = cast(str, record.get("record_hash")) + content = {key: value for key, value in record.items() if key != "record_hash"} + if content.get("sequence") != sequence: + raise ValueError( + "Improvement ledger sequence mismatch: " + f"expected={sequence}, actual={content.get('sequence')!r}" + ) + if content.get("previous_hash") != previous_hash: + raise ValueError(f"Improvement ledger previous hash mismatch: sequence={sequence}") + if content.get("bundle_hash") != bundle_hash: + raise ValueError(f"Improvement ledger bundle mismatch: sequence={sequence}") + expected_hash = _json_hash(content, b"USE-LEDGER-V1") + if actual_hash != expected_hash: + raise ValueError( + "Improvement ledger record hash mismatch: " + f"sequence={sequence}, expected={expected_hash}, actual={actual_hash}" + ) + previous_hash = actual_hash + return len(lines) + + +def rollback_champion( + bundle: ChampionBundle, + league: str, + created_at: str, +) -> ChampionBundle: + """Create a new checkpoint with one league restored to its embedded incumbent.""" + + champion = champion_for(bundle, league) + restored = LeagueChampion( + champion.league, + champion.rollback_ensemble, + champion.rollback_state, + False, + champion.rollback_ensemble, + champion.rollback_state, + ) + champions = tuple( + restored if item.league == champion.league else item for item in bundle.champions + ) + return create_champion_bundle_from_champions( + champions, + bundle.protocol_version, + created_at, + ) + + +def _aggregate( + observations: tuple[ObservationLoss, ...], + config: TuningConfig, +) -> AggregateScore: + if not observations: + raise ValueError("Cannot aggregate an empty observation sequence") + nll = statistics.fmean( + observation.joint_negative_log_likelihood for observation in observations + ) + brier = statistics.fmean( + observation.home_win_brier for observation in observations + ) + coverage = statistics.fmean( + ( + float(observation.total_covered) + + float(observation.margin_covered) + ) + / 2.0 + for observation in observations + ) + coverage_error = abs(coverage - config.target_coverage) + composite = nll + config.brier_weight * brier + config.coverage_weight * coverage_error + if any(not math.isfinite(value) for value in (nll, brier, coverage, composite)): + raise RuntimeError("Non-finite model score blocks autonomous promotion") + return AggregateScore( + len(observations), + nll, + brier, + coverage, + coverage_error, + composite, + ) + + +def _champion_to_dict(champion: LeagueChampion) -> dict[str, JsonValue]: + return { + "ensemble": _ensemble_to_dict(champion.ensemble), + "league": champion.league, + "promoted": champion.promoted, + "rollback_ensemble": _ensemble_to_dict(champion.rollback_ensemble), + "rollback_state": cast(JsonValue, asdict(champion.rollback_state)), + "state": cast(JsonValue, asdict(champion.state)), + } + + +def _champion_from_dict(value: dict[str, JsonValue]) -> LeagueChampion: + ensemble = _ensemble_from_dict(cast(dict[str, JsonValue], value["ensemble"])) + rollback_ensemble = _ensemble_from_dict( + cast(dict[str, JsonValue], value["rollback_ensemble"]) + ) + state = _state_from_dict(cast(dict[str, JsonValue], value["state"])) + rollback_state = _state_from_dict(cast(dict[str, JsonValue], value["rollback_state"])) + return LeagueChampion( + cast(str, value["league"]), + ensemble, + state, + cast(bool, value["promoted"]), + rollback_ensemble, + rollback_state, + ) + + +def _state_from_dict(state_value: dict[str, JsonValue]) -> AdaptiveState: + return AdaptiveState( + cast(str, state_value["ensemble_hash"]), + tuple(cast(list[float], state_value["weights"])), + cast(float, state_value["learning_rate"]), + cast(float, state_value["interval_scale"]), + cast(float, state_value["interval_learning_rate"]), + cast(float, state_value["target_coverage"]), + cast(float, state_value["short_loss_ewma"]), + cast(float, state_value["long_loss_ewma"]), + cast(float, state_value["short_decay"]), + cast(float, state_value["long_decay"]), + cast(float, state_value["drift_threshold"]), + cast(int, state_value["minimum_drift_observations"]), + cast(int, state_value["observations"]), + cast(float, state_value["drift_score"]), + cast(str, state_value["status"]), + cast(str, state_value["state_hash"]), + ) + + +def _ensemble_to_dict(ensemble: ModelEnsemble) -> dict[str, JsonValue]: + return { + "ensemble_hash": ensemble.ensemble_hash, + "evidence_status": ensemble.evidence_status, + "league": ensemble.league, + "profiles": cast(JsonValue, [asdict(profile) for profile in ensemble.profiles]), + "weights": cast(JsonValue, list(ensemble.weights)), + } + + +def _ensemble_from_dict(value: dict[str, JsonValue]) -> ModelEnsemble: + profiles = tuple( + AnalyticalProfile( + cast(str, profile["league"]), + cast(float, profile["base_home_score"]), + cast(float, profile["base_away_score"]), + cast(float, profile["total_score_sd"]), + cast(float, profile["margin_sd"]), + cast(float, profile["home_strength_exponent"]), + cast(float, profile["away_strength_exponent"]), + cast(int, profile["generation"]), + cast(str, profile["parent_hash"]), + cast(str, profile["evidence_status"]), + cast(str, profile["profile_hash"]), + ) + for profile in cast(list[dict[str, JsonValue]], value["profiles"]) + ) + return ModelEnsemble( + cast(str, value["league"]), + profiles, + tuple(cast(list[float], value["weights"])), + cast(str, value["evidence_status"]), + cast(str, value["ensemble_hash"]), + ) + + +def _bundle_to_dict(bundle: ChampionBundle) -> JsonValue: + return { + "bundle_hash": bundle.bundle_hash, + "champions": [_champion_to_dict(champion) for champion in bundle.champions], + "created_at": bundle.created_at, + "protocol_version": bundle.protocol_version, + } + + +def _ledger_records( + results: tuple[ImprovementResult, ...], + bundle_hash: str, +) -> tuple[dict[str, JsonValue], ...]: + previous_hash = "0" * 64 + records: list[dict[str, JsonValue]] = [] + for sequence, result in enumerate(sorted(results, key=lambda item: item.league)): + content: dict[str, JsonValue] = { + "bundle_hash": bundle_hash, + "challenger_ensemble_hash": result.challenger_ensemble.ensemble_hash, + "decision": result.decision, + "incumbent_ensemble_hash": result.incumbent_ensemble.ensemble_hash, + "league": result.league, + "previous_hash": previous_hash, + "promoted": result.promoted, + "relative_improvement": result.relative_improvement, + "selected_ensemble_hash": result.selected_ensemble.ensemble_hash, + "sequence": sequence, + } + record_hash = _json_hash(content, b"USE-LEDGER-V1") + record = {**content, "record_hash": record_hash} + records.append(record) + previous_hash = record_hash + return tuple(records) + + +def _json_hash(value: object, personalization: bytes) -> str: + encoded = json.dumps( + value, + allow_nan=False, + separators=(",", ":"), + sort_keys=True, + ).encode() + return hashlib.blake2b(encoded, digest_size=32, person=personalization).hexdigest() + + +def _json_text(value: JsonValue) -> str: + return json.dumps(value, allow_nan=False, indent=2, sort_keys=True) + "\n" + + +def _json_line(value: JsonValue) -> str: + return json.dumps(value, allow_nan=False, separators=(",", ":"), sort_keys=True) + "\n" + + +def _atomic_write(path: Path, content: str) -> None: + temporary = path.with_name(f".{path.name}.tmp") + temporary.write_text(content, encoding="utf-8") + os.replace(temporary, path) + + +def _required_int(value: dict[str, object], key: str) -> int: + item = value[key] + if isinstance(item, bool) or not isinstance(item, int): + raise ValueError(f"Tuning config field must be an integer: key={key!r}, value={item!r}") + return item + + +def _required_float(value: dict[str, object], key: str) -> float: + item = value[key] + if isinstance(item, bool) or not isinstance(item, (int, float)): + raise ValueError(f"Tuning config field must be numeric: key={key!r}, value={item!r}") + return float(item) + + +def _validate_bundle_identity(protocol_version: str, created_at: str) -> None: + if not protocol_version: + raise ValueError("protocol_version must be non-empty") + try: + timestamp = datetime.fromisoformat(created_at) + except ValueError as error: + raise ValueError(f"created_at must be ISO-8601: {created_at!r}") from error + if timestamp.utcoffset() is None: + raise ValueError(f"created_at must include an offset: {created_at!r}") diff --git a/src/universal_sports_engine/cli.py b/src/universal_sports_engine/cli.py new file mode 100644 index 0000000..636e3b5 --- /dev/null +++ b/src/universal_sports_engine/cli.py @@ -0,0 +1,297 @@ +"""Command-line interface for simulation, validation, and evidence inspection.""" + +from __future__ import annotations + +import argparse +import json +from dataclasses import asdict +from pathlib import Path + +from universal_sports_engine.accuracy import ( + evaluate_forecasts, + forecast_historical_games, + load_historical_games, +) +from universal_sports_engine.analytical import ( + simulate_analytical, + simulate_ensemble_analytical, +) +from universal_sports_engine.autotune import ( + adapt_champion_on_shadow, + champion_for, + create_champion_bundle, + load_tuning_config, + read_champion_bundle, + rollback_champion, + run_recursive_improvement, + verify_improvement_ledger, + write_champion_bundle, + write_improvement_artifacts, +) +from universal_sports_engine.core.replay_io import read_result, write_result +from universal_sports_engine.core.types import GameResult +from universal_sports_engine.league_packs.registry import league_identities, league_ids +from universal_sports_engine.season import round_robin_schedule, simulate_season +from universal_sports_engine.simulation import counterfactual_game, simulate_game +from universal_sports_engine.validation import validate_all, validate_league + + +def _parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(prog="sports-engine") + subparsers = parser.add_subparsers(dest="command", required=True) + subparsers.add_parser("list-leagues") + + simulate = subparsers.add_parser("simulate") + simulate.add_argument("--league", required=True, choices=league_ids()) + simulate.add_argument("--seed", required=True, type=int) + simulate.add_argument("--export", required=False, type=Path) + + simulate_fast = subparsers.add_parser("simulate-fast") + simulate_fast.add_argument("--league", required=True, choices=league_ids()) + simulate_fast.add_argument("--seed", required=True, type=int) + simulate_fast.add_argument("--home-strength", required=True, type=float) + simulate_fast.add_argument("--away-strength", required=True, type=float) + + season = subparsers.add_parser("simulate-season") + season.add_argument("--league", required=True, choices=league_ids()) + season.add_argument("--teams", required=True, type=int) + season.add_argument("--rounds", required=True, type=int) + season.add_argument("--seed", required=True, type=int) + + replay = subparsers.add_parser("verify-replay") + replay.add_argument("--path", required=True, type=Path) + + evaluate = subparsers.add_parser("evaluate-historical") + evaluate.add_argument("--csv", required=True, type=Path) + evaluate.add_argument("--samples", required=True, type=int) + evaluate.add_argument("--seed", required=True, type=int) + + auto_improve = subparsers.add_parser("auto-improve") + auto_improve.add_argument("--csv", required=True, type=Path) + auto_improve.add_argument("--config", required=True, type=Path) + auto_improve.add_argument("--output", required=True, type=Path) + + adaptive = subparsers.add_parser("simulate-adaptive") + adaptive.add_argument("--bundle", required=True, type=Path) + adaptive.add_argument("--league", required=True, choices=league_ids()) + adaptive.add_argument("--seed", required=True, type=int) + adaptive.add_argument("--home-strength", required=True, type=float) + adaptive.add_argument("--away-strength", required=True, type=float) + + adapt_online = subparsers.add_parser("adapt-online") + adapt_online.add_argument("--csv", required=True, type=Path) + adapt_online.add_argument("--config", required=True, type=Path) + adapt_online.add_argument("--bundle", required=True, type=Path) + adapt_online.add_argument("--league", required=True, choices=league_ids()) + adapt_online.add_argument("--output", required=True, type=Path) + + verify_autonomy = subparsers.add_parser("verify-autonomy") + verify_autonomy.add_argument("--bundle", required=True, type=Path) + verify_autonomy.add_argument("--ledger", required=True, type=Path) + + rollback = subparsers.add_parser("rollback-champion") + rollback.add_argument("--bundle", required=True, type=Path) + rollback.add_argument("--league", required=True, choices=league_ids()) + rollback.add_argument("--created-at", required=True) + rollback.add_argument("--output", required=True, type=Path) + + validate = subparsers.add_parser("validate") + validate.add_argument("--league", required=True, choices=("all", *league_ids())) + validate.add_argument("--games", required=True, type=int) + validate.add_argument("--seed", required=True, type=int) + validate.add_argument("--workers", required=False, type=int, default=1) + + counterfactual = subparsers.add_parser("counterfactual") + counterfactual.add_argument("--league", required=True, choices=league_ids()) + counterfactual.add_argument("--seed", required=True, type=int) + counterfactual.add_argument("--home-strength", required=True, type=float) + counterfactual.add_argument("--away-strength", required=True, type=float) + return parser + + +def _result_summary(result: GameResult) -> dict[str, object]: + return { + "league": result.league, + "home_score": result.home_score, + "away_score": result.away_score, + "seed": result.seed, + "event_count": len(result.events), + "final_event_hash": result.events[-1].event_hash, + "result_hash": result.result_hash, + "metadata": dict(result.metadata), + } + + +def main() -> None: + args = _parser().parse_args() + output: object + if args.command == "list-leagues": + output = [asdict(identity) for identity in league_identities()] + elif args.command == "simulate": + result = simulate_game(args.league, args.seed) + if args.export is not None: + write_result(args.export, result) + output = _result_summary(result) + elif args.command == "simulate-fast": + output = asdict( + simulate_analytical( + args.league, + args.seed, + args.home_strength, + args.away_strength, + ) + ) + elif args.command == "simulate-season": + if args.teams < 2: + raise ValueError(f"teams must be at least two: {args.teams}") + teams = tuple(f"team-{index:02d}" for index in range(args.teams)) + strengths = tuple((team, 1.0) for team in teams) + schedule = round_robin_schedule(teams, args.rounds, args.seed) + season_result = simulate_season(args.league, schedule, strengths) + output = { + "league": season_result.league, + "games": len(season_result.games), + "standings": [asdict(row) for row in season_result.standings], + } + elif args.command == "verify-replay": + output = _result_summary(read_result(args.path)) + elif args.command == "evaluate-historical": + manifest_games = load_historical_games(args.csv) + games = tuple(game for game in manifest_games if game.split == "test") + if not games: + raise ValueError("Historical manifest contains no test-split games") + output = asdict( + evaluate_forecasts( + games, + forecast_historical_games(games, args.samples, args.seed), + ) + ) + elif args.command == "auto-improve": + manifest_games = load_historical_games(args.csv) + config = load_tuning_config(args.config) + results = run_recursive_improvement(manifest_games, config) + calibration_games = tuple( + game for game in manifest_games if game.split == "calibration" + ) + if not calibration_games: + raise ValueError("Historical manifest contains no calibration-split games") + evidence_cutoff = max(game.outcome_time for game in calibration_games).isoformat() + bundle = create_champion_bundle(results, "recursive-calibration-v1", evidence_cutoff) + write_improvement_artifacts(args.output, results, bundle, config) + output = { + "bundle_hash": bundle.bundle_hash, + "champions": [ + { + "league": result.league, + "promoted": result.promoted, + "decision": result.decision, + "relative_improvement": result.relative_improvement, + "ensemble_hash": result.selected_ensemble.ensemble_hash, + "adaptive_state_hash": result.selected_state.state_hash, + } + for result in results + ], + "output": str(args.output.resolve()), + "status": "chronological_calibration_only_not_real_world_certification", + } + elif args.command == "simulate-adaptive": + bundle = read_champion_bundle(args.bundle) + champion = champion_for(bundle, args.league) + output = asdict( + simulate_ensemble_analytical( + champion.ensemble, + champion.state, + args.seed, + args.home_strength, + args.away_strength, + ) + ) + elif args.command == "adapt-online": + manifest_games = load_historical_games(args.csv) + shadow_games = tuple( + game + for game in manifest_games + if game.league == args.league and game.split == "shadow" + ) + if not shadow_games: + raise ValueError( + f"Historical manifest contains no shadow games for league: {args.league!r}" + ) + config = load_tuning_config(args.config) + bundle = read_champion_bundle(args.bundle) + evidence_cutoff = max(game.outcome_time for game in shadow_games).isoformat() + adapted = adapt_champion_on_shadow( + bundle, + args.league, + shadow_games, + config, + evidence_cutoff, + ) + if args.output.exists(): + raise FileExistsError(f"Refusing to overwrite adaptive checkpoint: {args.output}") + write_champion_bundle(args.output, adapted) + updated = champion_for(adapted, args.league) + output = { + "bundle_hash": adapted.bundle_hash, + "league": updated.league, + "observations": updated.state.observations, + "status": updated.state.status, + "drift_score": updated.state.drift_score, + "weights": updated.state.weights, + "output": str(args.output.resolve()), + } + elif args.command == "verify-autonomy": + bundle = read_champion_bundle(args.bundle) + output = { + "bundle_hash": bundle.bundle_hash, + "champions": len(bundle.champions), + "ledger_records": verify_improvement_ledger(args.ledger, bundle.bundle_hash), + "status": "verified", + } + elif args.command == "rollback-champion": + bundle = read_champion_bundle(args.bundle) + rolled_back = rollback_champion( + bundle, + args.league, + args.created_at, + ) + if args.output.exists(): + raise FileExistsError(f"Refusing to overwrite rollback checkpoint: {args.output}") + write_champion_bundle(args.output, rolled_back) + restored = champion_for(rolled_back, args.league) + output = { + "bundle_hash": rolled_back.bundle_hash, + "league": restored.league, + "ensemble_hash": restored.ensemble.ensemble_hash, + "state_hash": restored.state.state_hash, + "status": "rolled_back_to_embedded_incumbent", + "output": str(args.output.resolve()), + } + elif args.command == "validate": + reports = ( + validate_all(args.games, args.seed, args.workers) + if args.league == "all" + else (validate_league(args.league, args.games, args.seed, args.workers),) + ) + output = [asdict(report) for report in reports] + elif args.command == "counterfactual": + paired = counterfactual_game( + args.league, + args.seed, + args.home_strength, + args.away_strength, + ) + output = { + "baseline": _result_summary(paired.baseline), + "intervention": _result_summary(paired.intervention), + "home_score_delta": paired.home_score_delta, + "away_score_delta": paired.away_score_delta, + "shared_seed": paired.shared_seed, + } + else: + raise AssertionError(f"Unhandled command: {args.command}") + print(json.dumps(output, indent=2, sort_keys=True)) + + +if __name__ == "__main__": + main() diff --git a/src/universal_sports_engine/core/__init__.py b/src/universal_sports_engine/core/__init__.py new file mode 100644 index 0000000..8accbbd --- /dev/null +++ b/src/universal_sports_engine/core/__init__.py @@ -0,0 +1 @@ +"""Domain-neutral deterministic simulation kernel.""" diff --git a/src/universal_sports_engine/core/event_log.py b/src/universal_sports_engine/core/event_log.py new file mode 100644 index 0000000..19b0a62 --- /dev/null +++ b/src/universal_sports_engine/core/event_log.py @@ -0,0 +1,158 @@ +"""Content-addressed event creation, verification, and replay.""" + +from __future__ import annotations + +import hashlib +import json +import math +from collections.abc import Callable + +from universal_sports_engine.core.types import ( + Event, + EventDraft, + EventIntegrityError, + WorldState, +) + +GENESIS_HASH = "0" * 64 +Reducer = Callable[[WorldState, Event], WorldState] + + +def _event_hash(sequence: int, draft: EventDraft, prior_hash: str) -> str: + body = { + "schema": "USE-EVENT-V2", + "sequence": sequence, + "clock_ms": draft.clock_ms, + "phase": int(draft.phase), + "kind": draft.kind, + "actor_id": draft.actor_id, + "payload": list(draft.payload), + "prior_hash": prior_hash, + } + encoded = json.dumps( + body, + sort_keys=True, + separators=(",", ":"), + ensure_ascii=False, + allow_nan=False, + ).encode() + return hashlib.sha256(encoded).hexdigest() + + +def _validate_draft(draft: EventDraft) -> None: + if draft.clock_ms < 0: + raise ValueError(f"Event clock must be non-negative: {draft.clock_ms}") + if not draft.kind or not draft.actor_id: + raise ValueError(f"Event kind and actor_id must be non-empty: {draft!r}") + keys = tuple(key for key, _ in draft.payload) + if len(keys) != len(set(keys)): + raise ValueError(f"Event payload contains duplicate keys: {keys!r}") + for _, value in draft.payload: + if isinstance(value, float) and not math.isfinite(value): + raise ValueError(f"Event payload floats must be finite: {value}") + + +def append_event(events: tuple[Event, ...], draft: EventDraft) -> tuple[Event, ...]: + """Return a new event tuple containing a correctly chained event.""" + + sequence = len(events) + prior_hash = events[-1].event_hash if events else GENESIS_HASH + _validate_draft(draft) + event = Event( + sequence=sequence, + clock_ms=draft.clock_ms, + phase=draft.phase, + kind=draft.kind, + actor_id=draft.actor_id, + payload=draft.payload, + prior_hash=prior_hash, + event_hash=_event_hash(sequence, draft, prior_hash), + ) + return (*events, event) + + +def chain_drafts(drafts: tuple[EventDraft, ...]) -> tuple[Event, ...]: + """Hash a complete draft stream in linear time without tuple-copy amplification.""" + + events: list[Event] = [] + prior_hash = GENESIS_HASH + prior_clock = 0 + prior_phase = -1 + for sequence, draft in enumerate(drafts): + _validate_draft(draft) + if draft.clock_ms < prior_clock: + raise ValueError( + f"Event clock regressed at index {sequence}: {draft.clock_ms} < {prior_clock}" + ) + if draft.clock_ms == prior_clock and int(draft.phase) < prior_phase: + raise ValueError( + "Event phase regressed at a shared clock: " + f"index={sequence}, phase={draft.phase.name}, prior_phase={prior_phase}" + ) + event_hash = _event_hash(sequence, draft, prior_hash) + events.append( + Event( + sequence, + draft.clock_ms, + draft.phase, + draft.kind, + draft.actor_id, + draft.payload, + prior_hash, + event_hash, + ) + ) + prior_hash = event_hash + prior_clock = draft.clock_ms + prior_phase = int(draft.phase) + return tuple(events) + + +def verify_events(events: tuple[Event, ...]) -> None: + """Verify sequence numbers and the complete SHA-256 chain.""" + + prior_hash = GENESIS_HASH + prior_clock = 0 + prior_phase = -1 + for expected_sequence, event in enumerate(events): + draft = EventDraft( + clock_ms=event.clock_ms, + phase=event.phase, + kind=event.kind, + actor_id=event.actor_id, + payload=event.payload, + ) + _validate_draft(draft) + if event.clock_ms < prior_clock: + raise EventIntegrityError( + f"Event clock regressed at index {expected_sequence}: " + f"{event.clock_ms} < {prior_clock}" + ) + if event.clock_ms == prior_clock and int(event.phase) < prior_phase: + raise EventIntegrityError( + f"Event phase regressed at shared clock index {expected_sequence}" + ) + expected_hash = _event_hash(expected_sequence, draft, prior_hash) + if event.sequence != expected_sequence or event.prior_hash != prior_hash: + raise EventIntegrityError( + f"Broken event chain at index {expected_sequence}: " + f"sequence={event.sequence}, prior_hash={event.prior_hash}" + ) + if event.event_hash != expected_hash: + raise EventIntegrityError( + f"Event hash mismatch at index {expected_sequence}: " + f"expected={expected_hash}, actual={event.event_hash}" + ) + prior_hash = event.event_hash + prior_clock = event.clock_ms + prior_phase = int(event.phase) + + +def replay(initial_state: WorldState, events: tuple[Event, ...], reducer: Reducer) -> WorldState: + """Verify and reduce an event stream into an immutable world state.""" + + verify_events(events) + state = initial_state + for event in events: + state = reducer(state, event) + return state diff --git a/src/universal_sports_engine/core/fidelity.py b/src/universal_sports_engine/core/fidelity.py new file mode 100644 index 0000000..70b1e2f --- /dev/null +++ b/src/universal_sports_engine/core/fidelity.py @@ -0,0 +1,71 @@ +"""Evidence-gated simulation-fidelity contracts and routing.""" + +from __future__ import annotations + +from dataclasses import dataclass +from enum import IntEnum + +from universal_sports_engine.core.types import Payload + + +class Fidelity(IntEnum): + """Supported resolution levels in increasing computational detail.""" + + ANALYTICAL = 0 + EVENT = 1 + SPATIAL = 2 + PHYSICS = 3 + + +@dataclass(frozen=True, slots=True) +class FidelityRequest: + requires_event_path: bool + requires_spatial_path: bool + requires_physics: bool + + +@dataclass(frozen=True, slots=True) +class FidelityDecision: + selected: Fidelity + supported_maximum: Fidelity + reason_codes: tuple[str, ...] + + +@dataclass(frozen=True, slots=True) +class Vector2: + x: float + y: float + + +@dataclass(frozen=True, slots=True) +class SpatialState: + entity_id: str + position: Vector2 + velocity: Vector2 + uncertainty: Payload + + +class FidelityUnavailableError(RuntimeError): + """Raised when a request exceeds the evidence-supported fidelity.""" + + +def route_fidelity(request: FidelityRequest, supported_maximum: Fidelity) -> FidelityDecision: + """Choose the least expensive fidelity satisfying an explicit request.""" + + required = Fidelity.ANALYTICAL + reason = "aggregate_distribution_requested" + if request.requires_event_path: + required = Fidelity.EVENT + reason = "event_path_requested" + if request.requires_spatial_path: + required = Fidelity.SPATIAL + reason = "spatial_path_requested" + if request.requires_physics: + required = Fidelity.PHYSICS + reason = "physics_resolution_requested" + if required > supported_maximum: + raise FidelityUnavailableError( + "Requested fidelity exceeds the calibrated implementation: " + f"required={required.name}, supported={supported_maximum.name}" + ) + return FidelityDecision(required, supported_maximum, (reason,)) diff --git a/src/universal_sports_engine/core/replay_io.py b/src/universal_sports_engine/core/replay_io.py new file mode 100644 index 0000000..1c3ce55 --- /dev/null +++ b/src/universal_sports_engine/core/replay_io.py @@ -0,0 +1,79 @@ +"""Portable JSON export and verified import for simulation results.""" + +from __future__ import annotations + +import json +from pathlib import Path + +from universal_sports_engine.core.result_integrity import verify_result +from universal_sports_engine.core.types import Event, GameResult, Phase + + +def result_to_json(result: GameResult) -> str: + """Serialize a result without discarding evidence or hash-chain fields.""" + + body = { + "league": result.league, + "home_score": result.home_score, + "away_score": result.away_score, + "seed": result.seed, + "events": [ + { + "sequence": event.sequence, + "clock_ms": event.clock_ms, + "phase": int(event.phase), + "kind": event.kind, + "actor_id": event.actor_id, + "payload": list(event.payload), + "prior_hash": event.prior_hash, + "event_hash": event.event_hash, + } + for event in result.events + ], + "metadata": list(result.metadata), + "result_hash": result.result_hash, + } + return json.dumps(body, sort_keys=True, separators=(",", ":")) + + +def result_from_json(encoded: str) -> GameResult: + """Deserialize a result and fail if its event chain was changed.""" + + raw = json.loads(encoded) + events = tuple( + Event( + sequence=int(item["sequence"]), + clock_ms=int(item["clock_ms"]), + phase=Phase(int(item["phase"])), + kind=str(item["kind"]), + actor_id=str(item["actor_id"]), + payload=tuple((str(key), value) for key, value in item["payload"]), + prior_hash=str(item["prior_hash"]), + event_hash=str(item["event_hash"]), + ) + for item in raw["events"] + ) + result = GameResult( + league=str(raw["league"]), + home_score=int(raw["home_score"]), + away_score=int(raw["away_score"]), + seed=int(raw["seed"]), + events=events, + metadata=tuple((str(key), value) for key, value in raw["metadata"]), + result_hash=str(raw["result_hash"]), + ) + verify_result(result) + return result + + +def write_result(path: Path, result: GameResult) -> None: + """Write one portable replay artifact.""" + + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(result_to_json(result), encoding="utf-8") + + +def read_result(path: Path) -> GameResult: + """Read and verify one portable replay artifact.""" + + return result_from_json(path.read_text(encoding="utf-8")) diff --git a/src/universal_sports_engine/core/result_integrity.py b/src/universal_sports_engine/core/result_integrity.py new file mode 100644 index 0000000..bb1bce1 --- /dev/null +++ b/src/universal_sports_engine/core/result_integrity.py @@ -0,0 +1,61 @@ +"""Content addressing and verification for complete result envelopes.""" + +from __future__ import annotations + +import hashlib +import json + +from universal_sports_engine.core.event_log import verify_events +from universal_sports_engine.core.types import Event, EventIntegrityError, GameResult, Payload + + +def compute_result_hash( + league: str, + home_score: int, + away_score: int, + seed: int, + events: tuple[Event, ...], + metadata: Payload, +) -> str: + """Hash every externally meaningful result field.""" + + if not events: + raise ValueError("A complete result requires at least one event") + body = { + "schema": "USE-RESULT-V1", + "league": league, + "home_score": home_score, + "away_score": away_score, + "seed": seed, + "event_count": len(events), + "final_event_hash": events[-1].event_hash, + "metadata": list(metadata), + } + encoded = json.dumps( + body, + sort_keys=True, + separators=(",", ":"), + ensure_ascii=False, + allow_nan=False, + ).encode() + return hashlib.sha256(encoded).hexdigest() + + +def verify_result(result: GameResult) -> None: + """Verify event integrity and the complete published result envelope.""" + + if not result.league or result.seed < 0 or min(result.home_score, result.away_score) < 0: + raise EventIntegrityError(f"Invalid result envelope values: {result!r}") + verify_events(result.events) + expected = compute_result_hash( + result.league, + result.home_score, + result.away_score, + result.seed, + result.events, + result.metadata, + ) + if result.result_hash != expected: + raise EventIntegrityError( + f"Result hash mismatch: expected={expected}, actual={result.result_hash}" + ) diff --git a/src/universal_sports_engine/core/rng.py b/src/universal_sports_engine/core/rng.py new file mode 100644 index 0000000..07b6e26 --- /dev/null +++ b/src/universal_sports_engine/core/rng.py @@ -0,0 +1,45 @@ +"""Order-independent deterministic random primitives.""" + +from __future__ import annotations + +import hashlib +import math + + +def _digest(root_seed: int, labels: tuple[str | int, ...]) -> bytes: + encoded = "\x1f".join((str(root_seed), *(str(label) for label in labels))).encode() + return hashlib.blake2b(encoded, digest_size=8, person=b"USE-RNG2").digest() + + +def unit(root_seed: int, *labels: str | int) -> float: + """Return a deterministic U[0, 1) draw addressed by semantic labels.""" + + return int.from_bytes(_digest(root_seed, labels), "big") / 2**64 + + +def choice(root_seed: int, weights: tuple[float, ...], *labels: str | int) -> int: + """Select a weighted index without mutable global or shared RNG state.""" + + if not weights or any(weight < 0 for weight in weights): + raise ValueError(f"Weights must be a non-empty non-negative tuple: {weights!r}") + total = math.fsum(weights) + if total <= 0: + raise ValueError(f"At least one weight must be positive: {weights!r}") + threshold = unit(root_seed, *labels) * total + cumulative = 0.0 + for index, weight in enumerate(weights): + cumulative += weight + if threshold < cumulative: + return index + return len(weights) - 1 + + +def normal(root_seed: int, mean: float, standard_deviation: float, *labels: str | int) -> float: + """Return a deterministic normal draw via Box-Muller.""" + + if standard_deviation < 0: + raise ValueError("standard_deviation must be non-negative") + u1 = max(unit(root_seed, *labels, "u1"), 1e-15) + u2 = unit(root_seed, *labels, "u2") + z = math.sqrt(-2.0 * math.log(u1)) * math.cos(2.0 * math.pi * u2) + return mean + standard_deviation * z diff --git a/src/universal_sports_engine/core/rules.py b/src/universal_sports_engine/core/rules.py new file mode 100644 index 0000000..697702f --- /dev/null +++ b/src/universal_sports_engine/core/rules.py @@ -0,0 +1,41 @@ +"""Declarative rule-pack validation.""" + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True, slots=True) +class Rule: + rule_id: str + description: str + source: str + effective_season: int + + +@dataclass(frozen=True, slots=True) +class RulePack: + league: str + version: str + coverage_status: str + source_version: str + rules: tuple[Rule, ...] + + +def validate_rule_pack(pack: RulePack) -> None: + """Fail closed on incomplete or duplicate declarative rules.""" + + if ( + not pack.league + or not pack.version + or not pack.coverage_status + or not pack.source_version + or not pack.rules + ): + raise ValueError(f"Incomplete rule pack: {pack!r}") + identifiers = tuple(rule.rule_id for rule in pack.rules) + if len(set(identifiers)) != len(identifiers): + raise ValueError(f"Duplicate rule IDs in {pack.league} {pack.version}: {identifiers!r}") + for rule in pack.rules: + if not rule.description or not rule.source or rule.effective_season < 1900: + raise ValueError(f"Incomplete rule definition: {rule!r}") diff --git a/src/universal_sports_engine/core/types.py b/src/universal_sports_engine/core/types.py new file mode 100644 index 0000000..ea78aa9 --- /dev/null +++ b/src/universal_sports_engine/core/types.py @@ -0,0 +1,114 @@ +"""Immutable domain-neutral simulation contracts.""" + +from __future__ import annotations + +from dataclasses import dataclass +from enum import IntEnum + +type Scalar = str | int | float | bool | None +type Payload = tuple[tuple[str, Scalar], ...] + + +class Phase(IntEnum): + """Stable total ordering for simultaneous simulation work.""" + + PERCEIVE = 0 + DECIDE = 1 + ARBITRATE = 2 + RESOLVE = 3 + APPLY = 4 + PUBLISH = 5 + + +@dataclass(frozen=True, slots=True) +class Event: + """One immutable, content-addressed state transition record.""" + + sequence: int + clock_ms: int + phase: Phase + kind: str + actor_id: str + payload: Payload + prior_hash: str + event_hash: str + + +@dataclass(frozen=True, slots=True) +class EventDraft: + """Hash-free event description accepted by the event log.""" + + clock_ms: int + phase: Phase + kind: str + actor_id: str + payload: Payload + + +@dataclass(frozen=True, slots=True) +class WorldState: + """Immutable generic state represented as sorted key/value pairs.""" + + sequence: int + clock_ms: int + values: Payload + + +@dataclass(frozen=True, slots=True) +class GameResult: + """League-neutral result emitted by every complete-game simulator.""" + + league: str + home_score: int + away_score: int + seed: int + events: tuple[Event, ...] + metadata: Payload + result_hash: str + + +@dataclass(frozen=True, slots=True) +class Evidence: + """Versioned evidence metadata for downstream consumers.""" + + engine_version: str + domain_pack_version: str + scenario_hash: str + seed: int + validation_tier: str + limitations: tuple[str, ...] + + +class SimulationError(RuntimeError): + """Base class for explicit simulation failures.""" + + +class InvalidStateError(SimulationError): + """Raised when a transition would produce an illegal state.""" + + +class EventIntegrityError(SimulationError): + """Raised when an event hash chain cannot be verified.""" + + +class UnknownLeagueError(SimulationError): + """Raised when no registered league pack matches the requested ID.""" + + +class ParallelExecutionUnavailableError(SimulationError): + """Raised when the current host cannot safely spawn deterministic workers.""" + + +def payload(**values: Scalar) -> Payload: + """Create a deterministic immutable payload.""" + + return tuple(sorted(values.items())) + + +def payload_get(values: Payload, key: str) -> Scalar: + """Read a payload key or raise an actionable error.""" + + for item_key, value in values: + if item_key == key: + return value + raise KeyError(f"Payload key is missing: {key}") diff --git a/src/universal_sports_engine/dummy_bridge/__init__.py b/src/universal_sports_engine/dummy_bridge/__init__.py new file mode 100644 index 0000000..44b8d17 --- /dev/null +++ b/src/universal_sports_engine/dummy_bridge/__init__.py @@ -0,0 +1 @@ +"""Domain-neutral bridge for later Dummy world-model adaptation.""" diff --git a/src/universal_sports_engine/dummy_bridge/contracts.py b/src/universal_sports_engine/dummy_bridge/contracts.py new file mode 100644 index 0000000..e62105e --- /dev/null +++ b/src/universal_sports_engine/dummy_bridge/contracts.py @@ -0,0 +1,92 @@ +"""Sport-agnostic state, policy, scenario, and evidence contracts.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from universal_sports_engine.core.types import Evidence, GameResult, Payload, payload, payload_get + + +@dataclass(frozen=True, slots=True) +class State: + state_id: str + observed: Payload + latent: Payload + exogenous: Payload + history_hash: str + + +@dataclass(frozen=True, slots=True) +class Action: + action_id: str + actor_id: str + parameters: Payload + + +@dataclass(frozen=True, slots=True) +class Observation: + observer_id: str + state_id: str + values: Payload + uncertainty: Payload + + +@dataclass(frozen=True, slots=True) +class PolicyDecision: + action_distribution: Payload + selected_action_id: str + confidence: float + explanation_codes: tuple[str, ...] + abstain: bool + + +@dataclass(frozen=True, slots=True) +class Scenario: + scenario_id: str + initial_state: State + constraints: Payload + objective: Payload + + +@dataclass(frozen=True, slots=True) +class Counterfactual: + source_scenario_id: str + intervention: Action + shared_seed: int + + +def evidence_from_result(result: GameResult) -> Evidence: + """Translate a result into a domain-neutral, fail-closed evidence envelope.""" + + return Evidence( + engine_version=str(payload_get(result.metadata, "engine_version")), + domain_pack_version=str(payload_get(result.metadata, "domain_pack_version")), + scenario_hash=str(payload_get(result.metadata, "scenario_hash")), + seed=result.seed, + validation_tier=str(payload_get(result.metadata, "validation_tier")), + limitations=( + "No licensed point-in-time event or tracking data in the local build.", + "Reference-anchor validation does not certify real-world predictive accuracy.", + "No wagering or live-action adapter is present.", + ), + ) + + +def generic_fixture(seed: int) -> tuple[State, Action, Observation]: + """Prove the bridge does not require a sport-specific concept.""" + + state = State( + state_id=f"inventory-{seed}", + observed=payload(inventory=10, demand_signal=0.6), + latent=payload(true_demand=None), + exogenous=payload(weather_index=0.2), + history_hash="fixture", + ) + action = Action("reorder", "controller", payload(quantity=3)) + observation = Observation( + "controller", + state.state_id, + state.observed, + payload(demand_signal_sd=0.1), + ) + return state, action, observation diff --git a/src/universal_sports_engine/league_packs/__init__.py b/src/universal_sports_engine/league_packs/__init__.py new file mode 100644 index 0000000..e0c357b --- /dev/null +++ b/src/universal_sports_engine/league_packs/__init__.py @@ -0,0 +1 @@ +"""League-specific simulation packs.""" diff --git a/src/universal_sports_engine/league_packs/baseball.py b/src/universal_sports_engine/league_packs/baseball.py new file mode 100644 index 0000000..f3521e5 --- /dev/null +++ b/src/universal_sports_engine/league_packs/baseball.py @@ -0,0 +1,173 @@ +"""Pitch/plate-appearance/inning simulation for the MLB pack.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from universal_sports_engine.core.rng import choice, unit +from universal_sports_engine.core.types import EventDraft, GameResult, Phase, payload +from universal_sports_engine.league_packs.common import ( + LeagueIdentity, + draft_event, + finalize_result, +) + +MLB = LeagueIdentity("mlb", "Major League Baseball", "bat_and_ball", "mlb-2026.2", 2026) + + +@dataclass(frozen=True, slots=True) +class BaseballConfig: + innings: int + outcome_weights: tuple[float, ...] + outcome_names: tuple[str, ...] + home_outcome_shift: float + + +CONFIG = BaseballConfig( + innings=9, + outcome_weights=(0.693, 0.083, 0.137, 0.047, 0.005, 0.035), + outcome_names=("out", "walk", "single", "double", "triple", "home_run"), + home_outcome_shift=0.010, +) + + +def _adjusted_weights( + config: BaseballConfig, + strength: float, + outcome_shift: float, +) -> tuple[float, ...]: + delta = max(-0.08, min(0.08, (strength - 1.0) * 0.08 + outcome_shift)) + return ( + config.outcome_weights[0] - delta, + config.outcome_weights[1], + config.outcome_weights[2] + delta * 0.50, + config.outcome_weights[3] + delta * 0.25, + config.outcome_weights[4], + config.outcome_weights[5] + delta * 0.25, + ) + + +def _advance( + outcome: str, + bases: tuple[bool, bool, bool], + seed: int, + labels: tuple[str | int, ...], +) -> tuple[tuple[bool, bool, bool], int, int]: + first, second, third = bases + if outcome == "out": + return bases, 0, 1 + if outcome == "walk": + runs = int(first and second and third) + return (True, first or second, (second and first) or third), runs, 0 + if outcome == "single": + second_scores = second and unit(seed, *labels, "second_scores") < 0.72 + third_scores = third + new_third = (second and not second_scores) or ( + first and unit(seed, *labels, "first_to_third") < 0.28 + ) + return (True, first, new_third), int(second_scores) + int(third_scores), 0 + if outcome == "double": + first_scores = first and unit(seed, *labels, "first_scores") < 0.52 + runs = int(second) + int(third) + int(first_scores) + return (False, True, first and not first_scores), runs, 0 + if outcome == "triple": + return (False, False, True), int(first) + int(second) + int(third), 0 + if outcome == "home_run": + return (False, False, False), 1 + int(first) + int(second) + int(third), 0 + raise ValueError(f"Unknown baseball outcome: {outcome}") + + +def simulate_mlb(seed: int, home_strength: float, away_strength: float) -> GameResult: + """Simulate a complete game with deterministic plate-appearance events.""" + + drafts: list[EventDraft] = [] + scores = {"home": 0, "away": 0} + inning = 1 + clock_ms = 0 + while inning <= CONFIG.innings or scores["home"] == scores["away"]: + if inning > 20: + winner = "home" if unit(seed, "mlb", "tie_break") < 0.5 else "away" + scores[winner] += 1 + clock_ms += 1 + drafts.append( + draft_event( + clock_ms, + Phase.RESOLVE, + "administrative_tie_break", + winner, + payload(inning=inning, runs=1), + ) + ) + break + for batting in ("away", "home"): + if inning >= CONFIG.innings and batting == "home" and scores["home"] > scores["away"]: + break + strength = away_strength if batting == "away" else home_strength + outcome_shift = CONFIG.home_outcome_shift if batting == "home" else 0.0 + bases = (False, inning >= 10, False) + outs = 0 + plate_appearance = 0 + while outs < 3: + outcome_index = choice( + seed, + _adjusted_weights(CONFIG, strength, outcome_shift), + "mlb", + inning, + batting, + plate_appearance, + ) + outcome = CONFIG.outcome_names[outcome_index] + labels: tuple[str | int, ...] = ("mlb", inning, batting, plate_appearance) + bases, runs, outs_added = _advance(outcome, bases, seed, labels) + outs += outs_added + scores[batting] += runs + clock_ms += 24_000 + drafts.append( + draft_event( + clock_ms, + Phase.APPLY, + "plate_appearance", + batting, + payload( + inning=inning, + half=batting, + outcome=outcome, + outs=outs, + runs=runs, + home_score=scores["home"], + away_score=scores["away"], + ), + ) + ) + plate_appearance += 1 + if plate_appearance > 100: + raise RuntimeError(f"MLB half-inning exceeded safety bound: inning={inning}") + if ( + inning >= CONFIG.innings + and batting == "home" + and scores["home"] > scores["away"] + ): + break + drafts.append( + draft_event( + clock_ms, + Phase.PUBLISH, + "inning_complete", + "official", + payload( + inning=inning, + home_score=scores["home"], + away_score=scores["away"], + ), + ) + ) + inning += 1 + return finalize_result( + MLB, + scores["home"], + scores["away"], + seed, + tuple(drafts), + home_strength, + away_strength, + ) diff --git a/src/universal_sports_engine/league_packs/basketball.py b/src/universal_sports_engine/league_packs/basketball.py new file mode 100644 index 0000000..68d1352 --- /dev/null +++ b/src/universal_sports_engine/league_packs/basketball.py @@ -0,0 +1,178 @@ +"""Possession-level basketball family with distinct league packs.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from universal_sports_engine.core.rng import choice, normal +from universal_sports_engine.core.types import EventDraft, GameResult, Phase, payload +from universal_sports_engine.league_packs.common import ( + LeagueIdentity, + draft_event, + finalize_result, +) + + +@dataclass(frozen=True, slots=True) +class BasketballConfig: + identity: LeagueIdentity + team_possessions: int + possession_sd: float + outcome_weights: tuple[float, ...] + game_minutes: int + home_outcome_shift: float + + +CONFIGS = { + "nba": BasketballConfig( + LeagueIdentity( + "nba", + "National Basketball Association", + "court_invasion", + "nba-2025-26.2", + 2025, + ), + 99, + 4.5, + (0.13, 0.34, 0.07, 0.29, 0.17), + 48, + 0.009, + ), + "wnba": BasketballConfig( + LeagueIdentity( + "wnba", + "Women's National Basketball Association", + "court_invasion", + "wnba-2026.2", + 2026, + ), + 80, + 4.0, + (0.16, 0.34, 0.09, 0.29, 0.12), + 40, + 0.0075, + ), + "ncaambb": BasketballConfig( + LeagueIdentity( + "ncaambb", + "NCAA Men's Basketball", + "court_invasion", + "ncaambb-2025-26.2", + 2025, + ), + 69, + 5.0, + (0.18, 0.34, 0.09, 0.28, 0.11), + 40, + 0.013, + ), +} + +OUTCOMES = ("turnover", "empty", "free_throw", "two_pointer", "three_pointer") +POINTS = (0, 0, 1, 2, 3) + + +def _weights( + config: BasketballConfig, + strength: float, + outcome_shift: float, +) -> tuple[float, ...]: + delta = max(-0.06, min(0.06, (strength - 1.0) * 0.10 + outcome_shift)) + return ( + config.outcome_weights[0], + config.outcome_weights[1] - delta, + config.outcome_weights[2], + config.outcome_weights[3] + delta * 0.65, + config.outcome_weights[4] + delta * 0.35, + ) + + +def simulate_basketball( + league: str, + seed: int, + home_strength: float, + away_strength: float, +) -> GameResult: + """Simulate a regulation game plus deterministic overtime possessions.""" + + config = CONFIGS[league] + possessions_per_team = max( + 40, + round(normal(seed, config.team_possessions, config.possession_sd, league, "pace")), + ) + drafts: list[EventDraft] = [] + scores = {"home": 0, "away": 0} + total_possessions = possessions_per_team * 2 + clock_total_ms = config.game_minutes * 60_000 + possession = 0 + overtime = 0 + while possession < total_possessions or scores["home"] == scores["away"]: + if possession >= total_possessions: + overtime += 1 + if overtime > 8: + side = "home" if possession % 2 == 0 else "away" + scores[side] += 1 + drafts.append( + draft_event( + clock_total_ms + overtime * 300_000, + Phase.RESOLVE, + "administrative_tie_break", + side, + payload(points=1, overtime=overtime), + ) + ) + break + total_possessions += 20 + side = "home" if possession % 2 == 0 else "away" + strength = home_strength if side == "home" else away_strength + outcome_shift = config.home_outcome_shift if side == "home" else 0.0 + outcome_index = choice( + seed, + _weights(config, strength, outcome_shift), + league, + "possession", + possession, + ) + points = POINTS[outcome_index] + scores[side] += points + regulation_fraction = min(1.0, (possession + 1) / max(1, possessions_per_team * 2)) + clock_ms = round(regulation_fraction * clock_total_ms) + overtime * 300_000 + drafts.append( + draft_event( + clock_ms, + Phase.APPLY, + "possession", + side, + payload( + possession=possession, + outcome=OUTCOMES[outcome_index], + points=points, + overtime=overtime, + home_score=scores["home"], + away_score=scores["away"], + ), + ), + ) + possession += 1 + drafts.append( + draft_event( + drafts[-1].clock_ms, + Phase.PUBLISH, + "game_complete", + "official", + payload( + home_score=scores["home"], + away_score=scores["away"], + overtime=overtime, + ), + ) + ) + return finalize_result( + config.identity, + scores["home"], + scores["away"], + seed, + tuple(drafts), + home_strength, + away_strength, + ) diff --git a/src/universal_sports_engine/league_packs/common.py b/src/universal_sports_engine/league_packs/common.py new file mode 100644 index 0000000..62aaa5f --- /dev/null +++ b/src/universal_sports_engine/league_packs/common.py @@ -0,0 +1,121 @@ +"""Shared immutable league-pack data types and helpers.""" + +from __future__ import annotations + +import hashlib +import json +from dataclasses import dataclass + +from universal_sports_engine.core.event_log import append_event, chain_drafts +from universal_sports_engine.core.result_integrity import compute_result_hash +from universal_sports_engine.core.types import ( + Event, + EventDraft, + GameResult, + Payload, + Phase, + payload, +) + +ENGINE_VERSION = "0.3.0" + + +@dataclass(frozen=True, slots=True) +class LeagueIdentity: + league_id: str + display_name: str + family: str + pack_version: str + season: int + + +def add_event( + events: tuple[Event, ...], + clock_ms: int, + phase: Phase, + kind: str, + actor_id: str, + values: Payload, +) -> tuple[Event, ...]: + """Append an event without exposing hash-chain mechanics to league packs.""" + + return append_event( + events, + EventDraft( + clock_ms=clock_ms, + phase=phase, + kind=kind, + actor_id=actor_id, + payload=values, + ), + ) + + +def draft_event( + clock_ms: int, + phase: Phase, + kind: str, + actor_id: str, + values: Payload, +) -> EventDraft: + """Create an immutable event draft for linear-time finalization.""" + + return EventDraft(clock_ms, phase, kind, actor_id, values) + + +def scenario_hash(league: str, seed: int, home_strength: float, away_strength: float) -> str: + body = json.dumps( + { + "league": league, + "seed": seed, + "home_strength": home_strength, + "away_strength": away_strength, + }, + sort_keys=True, + separators=(",", ":"), + ).encode() + return hashlib.sha256(body).hexdigest() + + +def finalize_result( + identity: LeagueIdentity, + home_score: int, + away_score: int, + seed: int, + drafts: tuple[EventDraft, ...], + home_strength: float, + away_strength: float, +) -> GameResult: + """Verify the event log and build the common result envelope.""" + + events = chain_drafts(drafts) + metadata = payload( + engine_version=ENGINE_VERSION, + domain_pack_version=identity.pack_version, + family=identity.family, + season=identity.season, + scenario_hash=scenario_hash( + identity.league_id, + seed, + home_strength, + away_strength, + ), + validation_tier="provisional_reference_anchor", + ) + result_hash = compute_result_hash( + identity.league_id, + home_score, + away_score, + seed, + events, + metadata, + ) + return GameResult( + league=identity.league_id, + home_score=home_score, + away_score=away_score, + seed=seed, + events=events, + metadata=metadata, + result_hash=result_hash, + ) diff --git a/src/universal_sports_engine/league_packs/football.py b/src/universal_sports_engine/league_packs/football.py new file mode 100644 index 0000000..2fd553e --- /dev/null +++ b/src/universal_sports_engine/league_packs/football.py @@ -0,0 +1,151 @@ +"""Play/drive/game simulation for professional and college football.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from universal_sports_engine.core.rng import choice, unit +from universal_sports_engine.core.types import EventDraft, GameResult, Phase, payload +from universal_sports_engine.league_packs.common import ( + LeagueIdentity, + draft_event, + finalize_result, +) + + +@dataclass(frozen=True, slots=True) +class FootballConfig: + identity: LeagueIdentity + drives_per_team: int + drive_outcome_weights: tuple[float, ...] + home_outcome_shift: float + + +CONFIGS = { + "nfl": FootballConfig( + LeagueIdentity("nfl", "National Football League", "gridiron", "nfl-2025.2", 2025), + 11, + (0.58, 0.02, 0.16, 0.24), + 0.028, + ), + "ncaaf": FootballConfig( + LeagueIdentity("ncaaf", "NCAA Football", "gridiron", "ncaaf-2025.2", 2025), + 12, + (0.55, 0.01, 0.16, 0.28), + 0.037, + ), +} + +DRIVE_OUTCOMES = ("no_score", "safety", "field_goal", "touchdown") +DRIVE_POINTS = (0, 2, 3, 7) + + +def _weights( + config: FootballConfig, + strength: float, + outcome_shift: float, +) -> tuple[float, ...]: + delta = max(-0.08, min(0.08, (strength - 1.0) * 0.10 + outcome_shift)) + return ( + config.drive_outcome_weights[0] - delta, + config.drive_outcome_weights[1], + config.drive_outcome_weights[2] + delta * 0.35, + config.drive_outcome_weights[3] + delta * 0.65, + ) + + +def _play_count(seed: int, league: str, drive: int, side: str) -> int: + return 3 + int(unit(seed, league, "drive", drive, side, "plays") * 8) + + +def simulate_football( + league: str, + seed: int, + home_strength: float, + away_strength: float, +) -> GameResult: + """Simulate play events grouped into drives, regulation, and overtime.""" + + config = CONFIGS[league] + drafts: list[EventDraft] = [] + scores = {"home": 0, "away": 0} + total_drives = config.drives_per_team * 2 + drive = 0 + clock_ms = 0 + overtime = 0 + while drive < total_drives or scores["home"] == scores["away"]: + if drive >= total_drives: + overtime += 1 + total_drives += 2 + if overtime > 8: + scores["home"] += 1 + clock_ms += 1 + drafts.append( + draft_event( + clock_ms, + Phase.RESOLVE, + "administrative_tie_break", + "home", + payload(points=1, overtime=overtime), + ) + ) + break + side = "home" if drive % 2 == 0 else "away" + strength = home_strength if side == "home" else away_strength + outcome_shift = config.home_outcome_shift if side == "home" else 0.0 + plays = _play_count(seed, league, drive, side) + field_position = 25 + for play in range(plays): + yards = round((unit(seed, league, drive, play, "yards") - 0.35) * 18) + field_position = max(0, min(100, field_position + yards)) + clock_ms += 34_000 + drafts.append( + draft_event( + clock_ms, + Phase.APPLY, + "play", + side, + payload( + drive=drive, + play=play, + yards=yards, + field_position=field_position, + overtime=overtime, + ), + ), + ) + outcome_index = choice( + seed, + _weights(config, strength, outcome_shift), + league, + "drive_result", + drive, + ) + points = DRIVE_POINTS[outcome_index] + scores[side] += points + drafts.append( + draft_event( + clock_ms, + Phase.PUBLISH, + "drive_complete", + side, + payload( + drive=drive, + outcome=DRIVE_OUTCOMES[outcome_index], + points=points, + overtime=overtime, + home_score=scores["home"], + away_score=scores["away"], + ), + ), + ) + drive += 1 + return finalize_result( + config.identity, + scores["home"], + scores["away"], + seed, + tuple(drafts), + home_strength, + away_strength, + ) diff --git a/src/universal_sports_engine/league_packs/hockey.py b/src/universal_sports_engine/league_packs/hockey.py new file mode 100644 index 0000000..91a28d9 --- /dev/null +++ b/src/universal_sports_engine/league_packs/hockey.py @@ -0,0 +1,81 @@ +"""Shift/shot/game simulation for the NHL pack.""" + +from __future__ import annotations + +from universal_sports_engine.core.rng import unit +from universal_sports_engine.core.types import EventDraft, GameResult, Phase, payload +from universal_sports_engine.league_packs.common import ( + LeagueIdentity, + draft_event, + finalize_result, +) + +NHL = LeagueIdentity("nhl", "National Hockey League", "ice_invasion", "nhl-2025-26.2", 2025) + + +def simulate_nhl(seed: int, home_strength: float, away_strength: float) -> GameResult: + """Simulate regulation shifts, shots, overtime, and a shootout if needed.""" + + drafts: list[EventDraft] = [] + scores = {"home": 0, "away": 0} + shifts = 120 + for shift in range(shifts): + side = "home" if shift % 2 == 0 else "away" + strength = home_strength * 1.02 if side == "home" else away_strength + shot_probability = min(0.78, max(0.30, 0.52 * strength)) + clock_ms = round((shift + 1) / shifts * 3_600_000) + if unit(seed, "nhl", "shot", shift) < shot_probability: + goal_probability = min(0.20, max(0.04, 0.095 * strength)) + goal = unit(seed, "nhl", "goal", shift) < goal_probability + if goal: + scores[side] += 1 + drafts.append( + draft_event( + clock_ms, + Phase.APPLY, + "shot", + side, + payload( + shift=shift, + goal=goal, + home_score=scores["home"], + away_score=scores["away"], + ), + ), + ) + if (shift + 1) % 40 == 0: + drafts.append( + draft_event( + clock_ms, + Phase.PUBLISH, + "period_complete", + "official", + payload( + period=(shift + 1) // 40, + home_score=scores["home"], + away_score=scores["away"], + ), + ), + ) + overtime = scores["home"] == scores["away"] + if overtime: + overtime_winner = "home" if unit(seed, "nhl", "overtime") < 0.54 else "away" + scores[overtime_winner] += 1 + drafts.append( + draft_event( + 3_900_000, + Phase.RESOLVE, + "overtime_goal_or_shootout", + overtime_winner, + payload(home_score=scores["home"], away_score=scores["away"]), + ) + ) + return finalize_result( + NHL, + scores["home"], + scores["away"], + seed, + tuple(drafts), + home_strength, + away_strength, + ) diff --git a/src/universal_sports_engine/league_packs/registry.py b/src/universal_sports_engine/league_packs/registry.py new file mode 100644 index 0000000..2bbc96e --- /dev/null +++ b/src/universal_sports_engine/league_packs/registry.py @@ -0,0 +1,45 @@ +"""League-pack registry and strict dispatcher.""" + +from __future__ import annotations + +from collections.abc import Callable + +from universal_sports_engine.core.types import GameResult, UnknownLeagueError +from universal_sports_engine.league_packs.baseball import MLB, simulate_mlb +from universal_sports_engine.league_packs.basketball import CONFIGS as BASKETBALL_CONFIGS +from universal_sports_engine.league_packs.basketball import simulate_basketball +from universal_sports_engine.league_packs.common import LeagueIdentity +from universal_sports_engine.league_packs.football import CONFIGS as FOOTBALL_CONFIGS +from universal_sports_engine.league_packs.football import simulate_football +from universal_sports_engine.league_packs.hockey import NHL, simulate_nhl + +Simulator = Callable[[int, float, float], GameResult] + + +def league_identities() -> tuple[LeagueIdentity, ...]: + """Return every supported league in stable order.""" + + basketball = tuple(config.identity for config in BASKETBALL_CONFIGS.values()) + football = tuple(config.identity for config in FOOTBALL_CONFIGS.values()) + return (MLB, *basketball, *football, NHL) + + +def league_ids() -> tuple[str, ...]: + return tuple(identity.league_id for identity in league_identities()) + + +def simulator_for(league: str) -> Simulator: + """Resolve a simulator or fail explicitly with the supported set.""" + + normalized = league.strip().lower() + if normalized == "mlb": + return simulate_mlb + if normalized in BASKETBALL_CONFIGS: + return lambda seed, home, away: simulate_basketball(normalized, seed, home, away) + if normalized in FOOTBALL_CONFIGS: + return lambda seed, home, away: simulate_football(normalized, seed, home, away) + if normalized == "nhl": + return simulate_nhl + raise UnknownLeagueError( + f"Unknown league {league!r}; supported leagues: {', '.join(league_ids())}" + ) diff --git a/src/universal_sports_engine/league_packs/rule_packs.py b/src/universal_sports_engine/league_packs/rule_packs.py new file mode 100644 index 0000000..da65d62 --- /dev/null +++ b/src/universal_sports_engine/league_packs/rule_packs.py @@ -0,0 +1,165 @@ +"""Versioned rule-source manifests for supported complete-game flows.""" + +from __future__ import annotations + +from universal_sports_engine.core.rules import Rule, RulePack, validate_rule_pack +from universal_sports_engine.core.types import UnknownLeagueError + + +def _rule(rule_id: str, description: str, source: str, season: int) -> Rule: + return Rule(rule_id, description, source, season) + + +PACKS = { + "mlb": RulePack( + "mlb", + "2026.2", + "complete_game_flow_subset", + "2026 Official Baseball Rules", + ( + _rule( + "game.innings", + "Regulation consists of nine innings.", + "https://mktg.mlbstatic.com/mlb/official-information/" + "2026-official-baseball-rules.pdf", + 2026, + ), + _rule( + "extra.runner", + "Regular-season extra innings use the configured automatic runner.", + "https://mktg.mlbstatic.com/mlb/official-information/" + "2026-official-baseball-rules.pdf", + 2026, + ), + ), + ), + "nba": RulePack( + "nba", + "2025-26.2", + "complete_game_flow_subset", + "2025-26 NBA Rulebook", + ( + _rule( + "game.length", + "Regulation uses four 12-minute periods.", + "https://official.nba.com/rulebook/", + 2025, + ), + _rule( + "overtime", + "Tied regulation games continue through five-minute overtime periods.", + "https://official.nba.com/rulebook/", + 2025, + ), + ), + ), + "wnba": RulePack( + "wnba", + "2026.2", + "complete_game_flow_subset", + "2026 WNBA Official Rule Book", + ( + _rule( + "game.length", + "Regulation uses four 10-minute periods.", + "https://www.wnba.com/wnba-rule-book", + 2026, + ), + _rule( + "overtime", + "Tied regulation games continue through overtime.", + "https://www.wnba.com/wnba-rule-book", + 2026, + ), + ), + ), + "ncaambb": RulePack( + "ncaambb", + "2025-26.2", + "complete_game_flow_subset", + "2025-26 NCAA Men's Basketball Rules", + ( + _rule( + "game.length", + "Regulation uses two 20-minute halves.", + "https://www.ncaa.org/sports/2013/11/6/basketball-rules-of-the-game.aspx", + 2025, + ), + _rule( + "overtime", + "Tied regulation games continue through overtime.", + "https://www.ncaa.org/sports/2013/11/6/basketball-rules-of-the-game.aspx", + 2025, + ), + ), + ), + "nfl": RulePack( + "nfl", + "2025.2", + "complete_game_flow_subset", + "2025 NFL Rulebook", + ( + _rule( + "game.length", + "Regulation uses four 15-minute periods.", + "https://operations.nfl.com/rules-officiating/2025-nfl-rulebook", + 2025, + ), + _rule( + "overtime.possessions", + "Both teams receive an overtime possession opportunity under 2025 rules.", + "https://operations.nfl.com/rules-officiating/2025-nfl-rulebook", + 2025, + ), + ), + ), + "ncaaf": RulePack( + "ncaaf", + "2025.2", + "complete_game_flow_subset", + "2025 NCAA Football Rules", + ( + _rule( + "game.length", + "Regulation uses four periods.", + "https://www.ncaa.org/championships/playing-rules/football-playing-rules/", + 2025, + ), + _rule( + "overtime", + "Tied games use alternating overtime possessions.", + "https://www.ncaa.org/championships/playing-rules/football-playing-rules/", + 2025, + ), + ), + ), + "nhl": RulePack( + "nhl", + "2025-26.2", + "complete_game_flow_subset", + "2025-26 NHL Official Rules", + ( + _rule( + "game.length", + "Regulation uses three 20-minute periods.", + "https://media.nhl.com/site/asset/public/ext/2025-26/2025-26Rules.pdf", + 2025, + ), + _rule( + "overtime", + "Tied regular-season games proceed to overtime and then a shootout.", + "https://media.nhl.com/site/asset/public/ext/2025-26/2025-26Rules.pdf", + 2025, + ), + ), + ), +} + + +def rule_pack_for(league: str) -> RulePack: + normalized = league.strip().lower() + pack = PACKS.get(normalized) + if pack is None: + raise UnknownLeagueError(f"No rule pack for league: {league!r}") + validate_rule_pack(pack) + return pack diff --git a/src/universal_sports_engine/reference.py b/src/universal_sports_engine/reference.py new file mode 100644 index 0000000..a3d24e9 --- /dev/null +++ b/src/universal_sports_engine/reference.py @@ -0,0 +1,26 @@ +"""Transparent provisional league-level reference anchors.""" + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True, slots=True) +class ReferenceAnchor: + mean_home_score: float + mean_away_score: float + total_score_sd: float + margin_sd: float + tolerance_fraction: float + evidence_status: str + + +ANCHORS = { + "mlb": ReferenceAnchor(4.55, 4.30, 4.60, 4.40, 0.25, "provisional_reference_anchor"), + "nba": ReferenceAnchor(116.0, 113.0, 20.0, 15.0, 0.20, "provisional_reference_anchor"), + "wnba": ReferenceAnchor(82.0, 80.0, 17.5, 14.0, 0.22, "provisional_reference_anchor"), + "ncaambb": ReferenceAnchor(75.0, 72.0, 16.5, 14.0, 0.22, "provisional_reference_anchor"), + "nfl": ReferenceAnchor(23.5, 21.8, 13.7, 14.0, 0.25, "provisional_reference_anchor"), + "ncaaf": ReferenceAnchor(29.0, 26.5, 15.7, 18.0, 0.25, "provisional_reference_anchor"), + "nhl": ReferenceAnchor(3.20, 2.95, 2.30, 2.40, 0.30, "provisional_reference_anchor"), +} diff --git a/src/universal_sports_engine/season.py b/src/universal_sports_engine/season.py new file mode 100644 index 0000000..fee8ad6 --- /dev/null +++ b/src/universal_sports_engine/season.py @@ -0,0 +1,197 @@ +"""Deterministic schedules, standings, seasons, and knockout tournaments.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from universal_sports_engine.core.types import GameResult +from universal_sports_engine.simulation import simulate_scenario + + +@dataclass(frozen=True, slots=True) +class ScheduledGame: + game_id: str + home_team_id: str + away_team_id: str + seed: int + + +@dataclass(frozen=True, slots=True) +class Standing: + team_id: str + wins: int + losses: int + points_for: int + points_against: int + + +@dataclass(frozen=True, slots=True) +class SeasonResult: + league: str + schedule: tuple[ScheduledGame, ...] + games: tuple[GameResult, ...] + standings: tuple[Standing, ...] + + +@dataclass(frozen=True, slots=True) +class TournamentResult: + league: str + rounds: tuple[tuple[ScheduledGame, ...], ...] + games: tuple[GameResult, ...] + champion_team_id: str + + +def round_robin_schedule( + team_ids: tuple[str, ...], + rounds: int, + seed: int, +) -> tuple[ScheduledGame, ...]: + """Create a reproducible round-robin schedule using the circle method.""" + + if len(team_ids) < 2 or len(set(team_ids)) != len(team_ids): + raise ValueError(f"team_ids must contain at least two unique values: {team_ids!r}") + if rounds <= 0: + raise ValueError(f"rounds must be positive: {rounds}") + rotation: tuple[str | None, ...] = team_ids if len(team_ids) % 2 == 0 else (*team_ids, None) + games: list[ScheduledGame] = [] + sequence = 0 + for repetition in range(rounds): + current = rotation + for round_index in range(len(current) - 1): + half = len(current) // 2 + left = current[:half] + right = tuple(reversed(current[half:])) + for pair_index, (first, second) in enumerate(zip(left, right, strict=True)): + if first is None or second is None: + continue + reverse = (round_index + repetition + pair_index) % 2 == 1 + home, away = (second, first) if reverse else (first, second) + games.append(ScheduledGame(f"game-{sequence:05d}", home, away, seed + sequence)) + sequence += 1 + current = (current[0], current[-1], *current[1:-1]) + return tuple(games) + + +def _strength(team_id: str, strengths: tuple[tuple[str, float], ...]) -> float: + for candidate_id, value in strengths: + if candidate_id == team_id: + return value + raise KeyError(f"Missing team strength: {team_id}") + + +def _validate_strengths( + team_ids: tuple[str, ...], + strengths: tuple[tuple[str, float], ...], +) -> None: + strength_ids = tuple(team_id for team_id, _ in strengths) + if len(strength_ids) != len(set(strength_ids)): + raise ValueError("Team strength IDs must be unique") + if set(strength_ids) != set(team_ids): + raise ValueError("Team strengths must exactly cover scheduled teams") + for team_id, value in strengths: + if not 0.25 <= value <= 2.0: + raise ValueError(f"Team strength is outside [0.25, 2.0]: {team_id}={value}") + + +def simulate_season( + league: str, + schedule: tuple[ScheduledGame, ...], + strengths: tuple[tuple[str, float], ...], +) -> SeasonResult: + """Simulate a frozen schedule and calculate deterministic standings.""" + + if not schedule: + raise ValueError("Season schedule must contain at least one game") + game_ids = tuple(game.game_id for game in schedule) + if len(game_ids) != len(set(game_ids)): + raise ValueError("Scheduled game IDs must be unique") + if any(game.home_team_id == game.away_team_id for game in schedule): + raise ValueError("A team cannot play itself") + scheduled_team_ids = tuple( + sorted( + { + team + for game in schedule + for team in (game.home_team_id, game.away_team_id) + } + ) + ) + _validate_strengths(scheduled_team_ids, strengths) + games = tuple( + simulate_scenario( + league, + game.seed, + _strength(game.home_team_id, strengths), + _strength(game.away_team_id, strengths), + ) + for game in schedule + ) + team_ids = scheduled_team_ids + standings: list[Standing] = [] + for team_id in team_ids: + wins = 0 + points_for = 0 + points_against = 0 + for scheduled, result in zip(schedule, games, strict=True): + if scheduled.home_team_id == team_id: + points_for += result.home_score + points_against += result.away_score + wins += int(result.home_score > result.away_score) + elif scheduled.away_team_id == team_id: + points_for += result.away_score + points_against += result.home_score + wins += int(result.away_score > result.home_score) + played = sum(team_id in (game.home_team_id, game.away_team_id) for game in schedule) + standings.append(Standing(team_id, wins, played - wins, points_for, points_against)) + ordered = tuple( + sorted( + standings, + key=lambda row: ( + -row.wins, + -(row.points_for - row.points_against), + row.team_id, + ), + ) + ) + return SeasonResult(league, schedule, games, ordered) + + +def simulate_tournament( + league: str, + team_ids: tuple[str, ...], + strengths: tuple[tuple[str, float], ...], + seed: int, +) -> TournamentResult: + """Simulate a seeded single-elimination bracket with no hidden reshuffling.""" + + if ( + len(team_ids) < 2 + or len(team_ids) & (len(team_ids) - 1) + or len(set(team_ids)) != len(team_ids) + ): + raise ValueError(f"team count must be a power of two greater than one: {len(team_ids)}") + _validate_strengths(team_ids, strengths) + remaining = team_ids + rounds: list[tuple[ScheduledGame, ...]] = [] + all_games: list[GameResult] = [] + game_index = 0 + while len(remaining) > 1: + scheduled_round: list[ScheduledGame] = [] + winners: list[str] = [] + for pair_index in range(0, len(remaining), 2): + home = remaining[pair_index] + away = remaining[pair_index + 1] + scheduled = ScheduledGame(f"tournament-{game_index:05d}", home, away, seed + game_index) + result = simulate_scenario( + league, + scheduled.seed, + _strength(home, strengths), + _strength(away, strengths), + ) + scheduled_round.append(scheduled) + all_games.append(result) + winners.append(home if result.home_score > result.away_score else away) + game_index += 1 + rounds.append(tuple(scheduled_round)) + remaining = tuple(winners) + return TournamentResult(league, tuple(rounds), tuple(all_games), remaining[0]) diff --git a/src/universal_sports_engine/simulation.py b/src/universal_sports_engine/simulation.py new file mode 100644 index 0000000..80a60ee --- /dev/null +++ b/src/universal_sports_engine/simulation.py @@ -0,0 +1,92 @@ +"""Public simulation, batch, and paired counterfactual APIs.""" + +from __future__ import annotations + +import os +from concurrent.futures import ProcessPoolExecutor +from dataclasses import dataclass + +import __main__ +from universal_sports_engine.core.types import ( + GameResult, + ParallelExecutionUnavailableError, +) +from universal_sports_engine.league_packs.registry import simulator_for + + +@dataclass(frozen=True, slots=True) +class CounterfactualResult: + baseline: GameResult + intervention: GameResult + home_score_delta: int + away_score_delta: int + shared_seed: int + + +def simulate_scenario( + league: str, + seed: int, + home_strength: float, + away_strength: float, +) -> GameResult: + """Simulate one explicit scenario.""" + + if seed < 0: + raise ValueError(f"seed must be non-negative: {seed}") + if not 0.25 <= home_strength <= 2.0 or not 0.25 <= away_strength <= 2.0: + raise ValueError( + "Strength multipliers must be within [0.25, 2.0]: " + f"home={home_strength}, away={away_strength}" + ) + return simulator_for(league)(seed, home_strength, away_strength) + + +def simulate_game(league: str, seed: int) -> GameResult: + """Simulate a neutral-strength complete game.""" + + return simulate_scenario(league, seed, 1.0, 1.0) + + +def _batch_task(task: tuple[str, int]) -> GameResult: + league, seed = task + return simulate_game(league, seed) + + +def simulate_many(league: str, games: int, seed: int, workers: int) -> tuple[GameResult, ...]: + """Simulate a reproducible batch, optionally across processes.""" + + if games <= 0: + raise ValueError(f"games must be positive: {games}") + if workers <= 0: + raise ValueError(f"workers must be positive: {workers}") + if workers == 1: + return tuple(simulate_game(league, seed + index) for index in range(games)) + main_file = str(getattr(__main__, "__file__", "")) + if os.name == "nt" and (not main_file or main_file.startswith("<")): + raise ParallelExecutionUnavailableError( + "Windows process workers require an importable __main__ file; " + "run through sports-engine, pytest, or a script guarded by " + "if __name__ == '__main__'" + ) + tasks = ((league, seed + index) for index in range(games)) + with ProcessPoolExecutor(max_workers=workers) as executor: + return tuple(executor.map(_batch_task, tasks, chunksize=max(1, games // (workers * 4)))) + + +def counterfactual_game( + league: str, + seed: int, + home_strength: float, + away_strength: float, +) -> CounterfactualResult: + """Compare an intervention with a neutral baseline using shared random labels.""" + + baseline = simulate_scenario(league, seed, 1.0, 1.0) + intervention = simulate_scenario(league, seed, home_strength, away_strength) + return CounterfactualResult( + baseline=baseline, + intervention=intervention, + home_score_delta=intervention.home_score - baseline.home_score, + away_score_delta=intervention.away_score - baseline.away_score, + shared_seed=seed, + ) diff --git a/src/universal_sports_engine/validation.py b/src/universal_sports_engine/validation.py new file mode 100644 index 0000000..55359cf --- /dev/null +++ b/src/universal_sports_engine/validation.py @@ -0,0 +1,147 @@ +"""Reference-anchor validation and negative controls.""" + +from __future__ import annotations + +import math +import statistics +from dataclasses import dataclass + +from universal_sports_engine.core.event_log import verify_events +from universal_sports_engine.core.result_integrity import verify_result +from universal_sports_engine.league_packs.registry import league_ids +from universal_sports_engine.reference import ANCHORS +from universal_sports_engine.simulation import simulate_game, simulate_many, simulate_scenario + + +@dataclass(frozen=True, slots=True) +class ValidationReport: + league: str + games: int + observed_home_mean: float + observed_away_mean: float + observed_total_sd: float + anchor_total_sd: float + total_sd_relative_error: float + anchor_home_mean: float + anchor_away_mean: float + maximum_relative_error: float + home_advantage_z_score: float + home_advantage_not_materially_reversed: bool + status: str + + +def validate_league(league: str, games: int, seed: int, workers: int) -> ValidationReport: + """Compare simulations with transparent non-certifying reference anchors.""" + + anchor = ANCHORS[league] + results = simulate_many(league, games, seed, workers) + home_scores = tuple(result.home_score for result in results) + away_scores = tuple(result.away_score for result in results) + totals = tuple(home + away for home, away in zip(home_scores, away_scores, strict=True)) + margins = tuple(home - away for home, away in zip(home_scores, away_scores, strict=True)) + observed_home = statistics.fmean(home_scores) + observed_away = statistics.fmean(away_scores) + relative_home = abs(observed_home - anchor.mean_home_score) / max(1.0, anchor.mean_home_score) + relative_away = abs(observed_away - anchor.mean_away_score) / max(1.0, anchor.mean_away_score) + maximum_error = max(relative_home, relative_away) + observed_total_sd = statistics.pstdev(totals) + total_sd_error = abs(observed_total_sd - anchor.total_score_sd) / anchor.total_score_sd + margin_standard_error = statistics.pstdev(margins) / math.sqrt(games) + home_advantage_z_score = ( + (observed_home - observed_away) / margin_standard_error + if margin_standard_error > 0 + else math.inf + ) + home_advantage_not_materially_reversed = home_advantage_z_score >= -1.96 + status = ( + anchor.evidence_status + if max(maximum_error, total_sd_error) <= anchor.tolerance_fraction + and home_advantage_not_materially_reversed + else "failed_reference_anchor" + ) + return ValidationReport( + league=league, + games=games, + observed_home_mean=observed_home, + observed_away_mean=observed_away, + observed_total_sd=observed_total_sd, + anchor_total_sd=anchor.total_score_sd, + total_sd_relative_error=total_sd_error, + anchor_home_mean=anchor.mean_home_score, + anchor_away_mean=anchor.mean_away_score, + maximum_relative_error=maximum_error, + home_advantage_z_score=home_advantage_z_score, + home_advantage_not_materially_reversed=home_advantage_not_materially_reversed, + status=status, + ) + + +def validate_all(games: int, seed: int, workers: int) -> tuple[ValidationReport, ...]: + return tuple( + validate_league(league, games, seed + index * 10_000, workers) + for index, league in enumerate(league_ids()) + ) + + +def run_negative_controls(seed: int) -> tuple[tuple[str, bool, str], ...]: + """Run deterministic, integrity, independence, and finite-output controls.""" + + controls: list[tuple[str, bool, str]] = [] + for league in league_ids(): + first = simulate_game(league, seed) + second = simulate_game(league, seed) + deterministic = first == second + controls.append((f"{league}.deterministic_replay", deterministic, "same seed and version")) + finite = math.isfinite(first.home_score) and math.isfinite(first.away_score) + controls.append((f"{league}.finite_scores", finite, "scores must be finite")) + try: + verify_events(first.events) + integrity = True + except RuntimeError: + integrity = False + controls.append((f"{league}.event_integrity", integrity, "SHA-256 chain verification")) + try: + verify_result(first) + result_integrity = True + except RuntimeError: + result_integrity = False + controls.append( + ( + f"{league}.result_integrity", + result_integrity, + "complete result envelope hash verification", + ) + ) + changed = simulate_game(league, seed + 1) != first + controls.append((f"{league}.seed_sensitivity", changed, "different seed changes the path")) + strong = tuple( + simulate_scenario(league, seed + index, 1.25, 1.0).home_score + for index in range(64) + ) + weak = tuple( + simulate_scenario(league, seed + index, 0.75, 1.0).home_score + for index in range(64) + ) + monotonic = statistics.fmean(strong) > statistics.fmean(weak) + controls.append( + ( + f"{league}.strength_monotonicity", + monotonic, + "paired stronger home input must increase mean home score", + ) + ) + neutral = tuple(simulate_game(league, seed + 1_000 + index) for index in range(512)) + home_mean = statistics.fmean(result.home_score for result in neutral) + away_mean = statistics.fmean(result.away_score for result in neutral) + margins = tuple(result.home_score - result.away_score for result in neutral) + margin_standard_error = statistics.pstdev(margins) / math.sqrt(len(margins)) + z_score = (home_mean - away_mean) / margin_standard_error + home_advantage = z_score >= -1.96 + controls.append( + ( + f"{league}.home_advantage_direction", + home_advantage, + "home advantage must not be statistically materially reversed", + ) + ) + return tuple(controls) diff --git a/tests/test_accuracy.py b/tests/test_accuracy.py new file mode 100644 index 0000000..d11ed9d --- /dev/null +++ b/tests/test_accuracy.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +from dataclasses import replace +from datetime import UTC, datetime, timedelta + +import pytest + +from universal_sports_engine.accuracy import ( + HistoricalGame, + evaluate_forecasts, + forecast_historical_games, + validate_chronological_splits, + validate_historical_games, +) + + +def _games() -> tuple[HistoricalGame, ...]: + decision = datetime(2025, 1, 1, 12, tzinfo=UTC) + return tuple( + HistoricalGame( + f"game-{index}", + "nba", + decision + timedelta(days=index), + decision + timedelta(days=index, hours=-1), + decision + timedelta(days=index, hours=3), + 112 + index, + 108 + index, + 1.0, + 1.0, + "test", + ) + for index in range(4) + ) + + +def test_historical_forecast_evaluation_is_reproducible() -> None: + games = _games() + first = forecast_historical_games(games, 200, 77) + second = forecast_historical_games(games, 200, 77) + assert first == second + report = evaluate_forecasts(games, first) + assert report.games == 4 + assert report.samples_per_game == 200 + assert 0.0 <= report.home_win_brier <= 1.0 + assert 0.0 <= report.score_interval_90_coverage <= 1.0 + assert report.evidence_status == "historical_test_evaluation_not_certification" + + +def test_historical_manifest_rejects_temporal_leakage() -> None: + games = _games() + leaked = replace(games[0], features_available_at=games[0].decision_time + timedelta(seconds=1)) + with pytest.raises(ValueError, match="leakage"): + validate_historical_games((leaked, *games[1:])) + + +def test_accuracy_report_rejects_non_test_records() -> None: + games = _games() + forecasts = forecast_historical_games(games, 20, 9) + with pytest.raises(ValueError, match="test split"): + evaluate_forecasts((replace(games[0], split="train"), *games[1:]), forecasts) + + +def test_split_validator_rejects_overlapping_outcomes() -> None: + games = _games() + training = replace( + games[0], + split="train", + outcome_time=games[1].decision_time + timedelta(hours=1), + ) + testing = replace(games[1], split="calibration") + with pytest.raises(ValueError, match="overlap"): + validate_chronological_splits((training, testing)) diff --git a/tests/test_analytical.py b/tests/test_analytical.py new file mode 100644 index 0000000..e0dc043 --- /dev/null +++ b/tests/test_analytical.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +import statistics + +import pytest + +from universal_sports_engine.analytical import simulate_analytical, simulate_many_analytical +from universal_sports_engine.core.types import UnknownLeagueError +from universal_sports_engine.league_packs.registry import league_ids +from universal_sports_engine.reference import ANCHORS + + +@pytest.mark.parametrize("league", league_ids()) +def test_analytical_path_is_deterministic_and_tracks_reference_moments(league: str) -> None: + first = simulate_many_analytical(league, 2_000, 44_000, 1.0, 1.0) + second = simulate_many_analytical(league, 2_000, 44_000, 1.0, 1.0) + assert first == second + anchor = ANCHORS[league] + home_mean = statistics.fmean(result.home_score for result in first) + away_mean = statistics.fmean(result.away_score for result in first) + assert abs(home_mean - anchor.mean_home_score) / anchor.mean_home_score < 0.08 + assert abs(away_mean - anchor.mean_away_score) / anchor.mean_away_score < 0.08 + + +def test_analytical_path_rejects_invalid_inputs() -> None: + with pytest.raises(UnknownLeagueError): + simulate_analytical("missing", 1, 1.0, 1.0) + with pytest.raises(ValueError, match="non-negative"): + simulate_analytical("nba", -1, 1.0, 1.0) + with pytest.raises(ValueError, match="Strength"): + simulate_analytical("nba", 1, 3.0, 1.0) diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..ae0d312 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,184 @@ +from __future__ import annotations + +import json +import subprocess +import sys +from pathlib import Path + + +def _run(*arguments: str) -> object: + completed = subprocess.run( + [sys.executable, "-m", "universal_sports_engine.cli", *arguments], + check=True, + capture_output=True, + text=True, + ) + return json.loads(completed.stdout) + + +def test_list_leagues_cli() -> None: + output = _run("list-leagues") + assert {item["league_id"] for item in output} == { + "mlb", + "nba", + "wnba", + "ncaambb", + "nfl", + "ncaaf", + "nhl", + } + + +def test_simulate_cli() -> None: + output = _run("simulate", "--league", "wnba", "--seed", "123") + assert output["league"] == "wnba" + assert output["event_count"] > 0 + + +def test_simulate_fast_cli() -> None: + output = _run( + "simulate-fast", + "--league", + "nba", + "--seed", + "123", + "--home-strength", + "1.0", + "--away-strength", + "1.0", + ) + assert output["league"] == "nba" + assert output["fidelity"] == "F0_ANALYTICAL" + + +def test_export_and_verify_replay_cli(tmp_path) -> None: + replay_path = tmp_path / "game.json" + created = _run("simulate", "--league", "nfl", "--seed", "9", "--export", str(replay_path)) + verified = _run("verify-replay", "--path", str(replay_path)) + assert created == verified + + +def test_simulate_season_cli() -> None: + output = _run( + "simulate-season", + "--league", + "wnba", + "--teams", + "4", + "--rounds", + "1", + "--seed", + "9", + ) + assert output["games"] == 6 + assert len(output["standings"]) == 4 + + +def test_evaluate_historical_cli(tmp_path) -> None: + csv_path = tmp_path / "historical.csv" + csv_path.write_text( + "game_id,league,decision_time,features_available_at,outcome_time," + "home_score,away_score,home_strength,away_strength,split\n" + "g1,nhl,2025-01-01T12:00:00+00:00,2025-01-01T11:00:00+00:00," + "2025-01-01T15:00:00+00:00,3,2,1.0,1.0,test\n", + encoding="utf-8", + ) + output = _run( + "evaluate-historical", + "--csv", + str(csv_path), + "--samples", + "100", + "--seed", + "7", + ) + assert output["games"] == 1 + assert output["evidence_status"] == "historical_test_evaluation_not_certification" + + +def test_windows_interactive_parallelism_fails_with_actionable_error() -> None: + if sys.platform != "win32": + return + completed = subprocess.run( + [ + sys.executable, + "-c", + "from universal_sports_engine.simulation import simulate_many; " + "simulate_many('nba', 2, 1, 2)", + ], + check=False, + capture_output=True, + text=True, + ) + assert completed.returncode != 0 + assert "importable __main__ file" in completed.stderr + + +def test_auto_improve_and_adaptive_simulation_cli(tmp_path: Path) -> None: + csv_path = tmp_path / "autonomy.csv" + rows = [ + "game_id,league,decision_time,features_available_at,outcome_time," + "home_score,away_score,home_strength,away_strength,split" + ] + for index in range(8): + day = index + 1 + split = "train" if index < 4 else "calibration" + rows.append( + f"g{index},nhl,2025-01-{day:02d}T12:00:00+00:00," + f"2025-01-{day:02d}T11:00:00+00:00," + f"2025-01-{day:02d}T15:00:00+00:00,3,2,1.0,1.0,{split}" + ) + csv_path.write_text("\n".join(rows) + "\n", encoding="utf-8") + config_path = tmp_path / "config.json" + config_path.write_text( + json.dumps( + { + "brier_weight": 2.0, + "coverage_weight": 1.0, + "drift_threshold": 1.0, + "elite_count": 2, + "experts_per_ensemble": 2, + "generations": 1, + "interval_learning_rate": 0.04, + "long_decay": 0.03, + "maximum_brier_deterioration": 0.1, + "maximum_coverage_error_deterioration": 0.1, + "minimum_calibration_games": 4, + "minimum_drift_observations": 10, + "minimum_relative_improvement": 0.0, + "minimum_train_games": 4, + "mutation_scale": 0.1, + "online_learning_rate": 0.2, + "population_size": 4, + "seed": 7, + "short_decay": 0.2, + "target_coverage": 0.9, + } + ), + encoding="utf-8", + ) + artifacts = tmp_path / "artifacts" + improved = _run( + "auto-improve", + "--csv", + str(csv_path), + "--config", + str(config_path), + "--output", + str(artifacts), + ) + assert improved["champions"][0]["league"] == "nhl" + adaptive = _run( + "simulate-adaptive", + "--bundle", + str(artifacts / "champion_bundle.json"), + "--league", + "nhl", + "--seed", + "12", + "--home-strength", + "1.0", + "--away-strength", + "1.0", + ) + assert adaptive["fidelity"] == "F0_ADAPTIVE_ENSEMBLE" diff --git a/tests/test_core.py b/tests/test_core.py new file mode 100644 index 0000000..b9f8d24 --- /dev/null +++ b/tests/test_core.py @@ -0,0 +1,138 @@ +from __future__ import annotations + +import json +from dataclasses import replace + +import pytest + +from universal_sports_engine import __version__ +from universal_sports_engine.core.event_log import append_event, chain_drafts, replay, verify_events +from universal_sports_engine.core.fidelity import ( + Fidelity, + FidelityRequest, + FidelityUnavailableError, + route_fidelity, +) +from universal_sports_engine.core.replay_io import result_from_json, result_to_json +from universal_sports_engine.core.rng import choice, normal, unit +from universal_sports_engine.core.types import ( + EventDraft, + EventIntegrityError, + Phase, + WorldState, + payload, + payload_get, +) +from universal_sports_engine.league_packs.common import ENGINE_VERSION +from universal_sports_engine.simulation import simulate_game + + +def test_random_draws_are_label_addressed_and_repeatable() -> None: + first = unit(42, "game", 1, "shot") + assert first == unit(42, "game", 1, "shot") + assert first != unit(42, "game", 2, "shot") + assert choice(42, (0.2, 0.8), "choice") in (0, 1) + assert normal(42, 5.0, 2.0, "normal") == normal(42, 5.0, 2.0, "normal") + + +def test_package_and_evidence_versions_match() -> None: + assert __version__ == ENGINE_VERSION == "0.3.0" + + +def test_rng_rejects_invalid_inputs() -> None: + with pytest.raises(ValueError, match="non-empty"): + choice(1, (), "invalid") + with pytest.raises(ValueError, match="non-negative"): + choice(1, (1.0, -1.0), "invalid") + with pytest.raises(ValueError, match="non-negative"): + normal(1, 0.0, -1.0, "invalid") + + +def test_unit_draw_is_always_half_open() -> None: + draws = tuple(unit(99, "draw", index) for index in range(10_000)) + assert all(0.0 <= draw < 1.0 for draw in draws) + + +def test_event_chain_verifies_and_detects_tampering() -> None: + events = append_event( + (), + EventDraft(10, Phase.APPLY, "increment", "system", payload(amount=2)), + ) + events = append_event( + events, + EventDraft(20, Phase.PUBLISH, "complete", "system", payload(done=True)), + ) + verify_events(events) + tampered = (replace(events[0], event_hash="f" * 64), events[1]) + with pytest.raises(EventIntegrityError, match="hash mismatch"): + verify_events(tampered) + + +def test_replay_uses_a_pure_reducer() -> None: + events = append_event( + (), + EventDraft(10, Phase.APPLY, "increment", "system", payload(amount=2)), + ) + initial = WorldState(0, 0, payload(total=1)) + + def reducer(state: WorldState, event) -> WorldState: + total = int(payload_get(state.values, "total")) + amount = int(payload_get(event.payload, "amount")) + return WorldState(event.sequence + 1, event.clock_ms, payload(total=total + amount)) + + final = replay(initial, events, reducer) + assert payload_get(initial.values, "total") == 1 + assert payload_get(final.values, "total") == 3 + + +def test_portable_result_round_trip_verifies_hash_chain() -> None: + result = simulate_game("mlb", 104) + assert result_from_json(result_to_json(result)) == result + + +def test_portable_result_rejects_score_tampering() -> None: + result = simulate_game("mlb", 104) + body = json.loads(result_to_json(result)) + body["home_score"] += 1 + with pytest.raises(EventIntegrityError, match="Result hash mismatch"): + result_from_json(json.dumps(body)) + + +def test_fidelity_router_fails_closed_above_supported_level() -> None: + event_request = FidelityRequest(True, False, False) + assert route_fidelity(event_request, Fidelity.EVENT).selected is Fidelity.EVENT + spatial_request = FidelityRequest(True, True, False) + with pytest.raises(FidelityUnavailableError, match="exceeds"): + route_fidelity(spatial_request, Fidelity.EVENT) + + +def test_event_builder_rejects_clock_and_phase_regressions() -> None: + with pytest.raises(ValueError, match="clock regressed"): + chain_drafts( + ( + EventDraft(10, Phase.APPLY, "first", "system", payload(value=1)), + EventDraft(9, Phase.APPLY, "second", "system", payload(value=2)), + ) + ) + with pytest.raises(ValueError, match="phase regressed"): + chain_drafts( + ( + EventDraft(10, Phase.PUBLISH, "first", "system", payload(value=1)), + EventDraft(10, Phase.APPLY, "second", "system", payload(value=2)), + ) + ) + + +def test_event_builder_rejects_nonfinite_payload() -> None: + with pytest.raises(ValueError, match="finite"): + chain_drafts( + ( + EventDraft( + 0, + Phase.APPLY, + "invalid", + "system", + payload(value=float("nan")), + ), + ) + ) diff --git a/tests/test_dummy_bridge.py b/tests/test_dummy_bridge.py new file mode 100644 index 0000000..ee2095f --- /dev/null +++ b/tests/test_dummy_bridge.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from universal_sports_engine.dummy_bridge.contracts import evidence_from_result, generic_fixture +from universal_sports_engine.simulation import simulate_game + + +def test_generic_fixture_contains_no_sports_semantics() -> None: + state, action, observation = generic_fixture(8) + serialized = repr((state, action, observation)).lower() + prohibited = ("league", "player", "ball", "puck", "possession", "wager") + assert not any(term in serialized for term in prohibited) + + +def test_evidence_preserves_provisional_boundary() -> None: + evidence = evidence_from_result(simulate_game("nhl", 8)) + assert evidence.validation_tier == "provisional_reference_anchor" + assert any("No licensed point-in-time" in limitation for limitation in evidence.limitations) diff --git a/tests/test_leagues.py b/tests/test_leagues.py new file mode 100644 index 0000000..c1d6b36 --- /dev/null +++ b/tests/test_leagues.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +import pytest + +from universal_sports_engine.core.event_log import verify_events +from universal_sports_engine.core.types import UnknownLeagueError, payload_get +from universal_sports_engine.league_packs.registry import league_ids +from universal_sports_engine.league_packs.rule_packs import rule_pack_for +from universal_sports_engine.simulation import ( + counterfactual_game, + simulate_game, + simulate_many, + simulate_scenario, +) + + +@pytest.mark.parametrize("league", league_ids()) +def test_every_league_completes_with_replayable_events(league: str) -> None: + result = simulate_game(league, 17) + assert result.league == league + assert result.home_score >= 0 + assert result.away_score >= 0 + assert result.events + verify_events(result.events) + assert payload_get(result.metadata, "validation_tier") == "provisional_reference_anchor" + + +@pytest.mark.parametrize("league", league_ids()) +def test_every_league_is_exactly_deterministic(league: str) -> None: + assert simulate_game(league, 2718) == simulate_game(league, 2718) + + +@pytest.mark.parametrize("league", league_ids()) +def test_every_league_has_a_valid_versioned_rule_pack(league: str) -> None: + pack = rule_pack_for(league) + assert pack.league == league + assert pack.rules + assert pack.coverage_status == "complete_game_flow_subset" + assert pack.source_version + + +def test_unknown_league_fails_explicitly() -> None: + with pytest.raises(UnknownLeagueError, match="supported leagues"): + simulate_game("unknown", 1) + + +def test_invalid_scenario_fails_explicitly() -> None: + with pytest.raises(ValueError, match="non-negative"): + simulate_scenario("nba", -1, 1.0, 1.0) + with pytest.raises(ValueError, match="Strength"): + simulate_scenario("nba", 1, 3.0, 1.0) + + +def test_batch_is_stable_across_worker_counts() -> None: + sequential = simulate_many("nba", 6, 100, 1) + parallel = simulate_many("nba", 6, 100, 2) + assert parallel == sequential + + +def test_counterfactual_preserves_seed_and_baseline() -> None: + paired = counterfactual_game("nfl", 99, 1.2, 0.8) + assert paired.shared_seed == 99 + assert paired.baseline == simulate_game("nfl", 99) + assert paired.intervention.seed == paired.baseline.seed diff --git a/tests/test_recursive_improvement.py b/tests/test_recursive_improvement.py new file mode 100644 index 0000000..a99a2ef --- /dev/null +++ b/tests/test_recursive_improvement.py @@ -0,0 +1,183 @@ +from __future__ import annotations + +import json +from dataclasses import replace +from datetime import UTC, datetime, timedelta + +import pytest + +from universal_sports_engine.accuracy import HistoricalGame +from universal_sports_engine.adaptive import ( + create_adaptive_state, + create_ensemble, + create_profile, + reference_profile, + sample_profile_scores, + update_adaptive_state, +) +from universal_sports_engine.analytical import simulate_ensemble_analytical +from universal_sports_engine.autotune import ( + TuningConfig, + champion_for, + create_champion_bundle, + read_champion_bundle, + rollback_champion, + tune_league, + verify_improvement_ledger, + write_improvement_artifacts, +) +from universal_sports_engine.reference import ANCHORS + + +def _config() -> TuningConfig: + return TuningConfig( + 3, + 10, + 3, + 4, + 0.18, + 24, + 16, + 0.0, + 0.03, + 0.08, + 2.0, + 1.0, + 0.35, + 0.04, + 0.90, + 0.20, + 0.03, + 0.50, + 20, + 77, + ) + + +def _synthetic_games() -> tuple[HistoricalGame, ...]: + anchor = ANCHORS["nba"] + target = create_profile( + "nba", + anchor.mean_home_score * 0.84, + anchor.mean_away_score * 0.86, + anchor.total_score_sd * 0.72, + anchor.margin_sd * 0.78, + 1.18, + 0.91, + 1, + reference_profile("nba").profile_hash, + "synthetic_test_target", + ) + start = datetime(2024, 1, 1, 12, tzinfo=UTC) + games: list[HistoricalGame] = [] + for index in range(48): + split = "train" if index < 30 else "calibration" + decision = start + timedelta(days=index) + home_strength = 0.80 + (index % 9) * 0.05 + away_strength = 0.82 + (index % 7) * 0.05 + home_score, away_score = sample_profile_scores( + target, + 9_000 + index, + home_strength, + away_strength, + ) + games.append( + HistoricalGame( + f"nba-synthetic-{index:03d}", + "nba", + decision, + decision - timedelta(hours=1), + decision + timedelta(hours=3), + home_score, + away_score, + home_strength, + away_strength, + split, + ) + ) + return tuple(games) + + +def test_recursive_tuning_is_deterministic_and_checkpointed(tmp_path) -> None: + games = _synthetic_games() + first = tune_league("nba", games, _config()) + second = tune_league("nba", games, _config()) + assert first == second + assert len(first.generations) == 3 + assert first.selected_state.observations == 18 + bundle = create_champion_bundle( + (first,), + "test-recursive-v1", + max(game.outcome_time for game in games).isoformat(), + ) + artifacts = tmp_path / "artifacts" + write_improvement_artifacts(artifacts, (first,), bundle, _config()) + path = artifacts / "champion_bundle.json" + loaded = read_champion_bundle(path) + assert loaded == bundle + assert ( + verify_improvement_ledger( + artifacts / "improvement_ledger.jsonl", + bundle.bundle_hash, + ) + == 1 + ) + champion = champion_for(loaded, "nba") + result = simulate_ensemble_analytical(champion.ensemble, champion.state, 12, 1.0, 1.0) + assert result.ensemble_hash == champion.ensemble.ensemble_hash + rolled_back = rollback_champion(loaded, "nba", "2025-01-01T00:00:00+00:00") + restored = champion_for(rolled_back, "nba") + assert restored.ensemble == champion.rollback_ensemble + assert restored.state == champion.rollback_state + ledger_path = artifacts / "improvement_ledger.jsonl" + record = json.loads(ledger_path.read_text(encoding="utf-8")) + record["decision"] = "tampered" + ledger_path.write_text(json.dumps(record) + "\n", encoding="utf-8") + with pytest.raises(ValueError, match="record hash mismatch"): + verify_improvement_ledger(ledger_path, bundle.bundle_hash) + + +def test_online_weights_reward_the_better_expert() -> None: + anchor = ANCHORS["nba"] + good = create_profile( + "nba", + anchor.mean_home_score * 0.82, + anchor.mean_away_score * 0.83, + anchor.total_score_sd * 0.75, + anchor.margin_sd * 0.80, + 1.0, + 1.0, + 1, + reference_profile("nba").profile_hash, + "synthetic_good", + ) + bad = reference_profile("nba") + ensemble = create_ensemble("nba", (good, bad), (0.5, 0.5), "synthetic_ensemble") + state = create_adaptive_state(ensemble, 0.4, 0.04, 0.90, 0.20, 0.03, 1.0, 20) + for index in range(40): + home_score, away_score = sample_profile_scores(good, 20_000 + index, 1.0, 1.0) + state, _ = update_adaptive_state( + state, + ensemble, + 1.0, + 1.0, + home_score, + away_score, + 2.0, + ) + assert state.weights[0] > state.weights[1] + assert state.observations == 40 + + +def test_adaptive_simulation_fails_closed_on_drift() -> None: + profile = reference_profile("nhl") + ensemble = create_ensemble("nhl", (profile,), (1.0,), "test") + state = create_adaptive_state(ensemble, 0.2, 0.04, 0.90, 0.20, 0.03, 0.10, 4) + drifted = replace(state, status="drift_alert") + with pytest.raises(ValueError, match="content hash mismatch"): + simulate_ensemble_analytical(ensemble, drifted, 1, 1.0, 1.0) + + +def test_tuning_rejects_insufficient_chronological_evidence() -> None: + with pytest.raises(ValueError, match="Insufficient chronological training"): + tune_league("nba", _synthetic_games()[:10], _config()) diff --git a/tests/test_season.py b/tests/test_season.py new file mode 100644 index 0000000..4099a34 --- /dev/null +++ b/tests/test_season.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +import pytest + +from universal_sports_engine.season import ( + round_robin_schedule, + simulate_season, + simulate_tournament, +) + + +def test_round_robin_and_standings_are_reproducible() -> None: + teams = ("alpha", "beta", "gamma", "delta") + strengths = tuple((team, 1.0) for team in teams) + schedule = round_robin_schedule(teams, 2, 55) + assert len(schedule) == 12 + first = simulate_season("ncaambb", schedule, strengths) + second = simulate_season("ncaambb", schedule, strengths) + assert first == second + assert sum(row.wins for row in first.standings) == len(schedule) + assert sum(row.losses for row in first.standings) == len(schedule) + + +def test_knockout_tournament_returns_one_champion() -> None: + teams = ("alpha", "beta", "gamma", "delta") + strengths = tuple((team, 1.0) for team in teams) + result = simulate_tournament("nhl", teams, strengths, 91) + assert result.champion_team_id in teams + assert len(result.games) == 3 + assert tuple(len(round_games) for round_games in result.rounds) == (2, 1) + + +def test_invalid_season_inputs_fail_explicitly() -> None: + with pytest.raises(ValueError, match="unique"): + round_robin_schedule(("same", "same"), 1, 1) + with pytest.raises(ValueError, match="power of two"): + simulate_tournament("nba", ("one", "two", "three"), (), 1) + with pytest.raises(ValueError, match="at least one"): + simulate_season("nba", (), ()) + schedule = round_robin_schedule(("one", "two"), 1, 1) + with pytest.raises(ValueError, match="exactly cover"): + simulate_season("nba", schedule, (("one", 1.0),)) diff --git a/tests/test_validation.py b/tests/test_validation.py new file mode 100644 index 0000000..8b3eb16 --- /dev/null +++ b/tests/test_validation.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from universal_sports_engine.league_packs.registry import league_ids +from universal_sports_engine.validation import run_negative_controls, validate_all + + +def test_all_reference_anchor_validations_are_honestly_provisional() -> None: + reports = validate_all(500, 700, 1) + assert tuple(report.league for report in reports) == league_ids() + assert all(report.status == "provisional_reference_anchor" for report in reports) + + +def test_negative_controls_pass() -> None: + controls = run_negative_controls(2026) + failures = tuple(control for control in controls if not control[1]) + assert not failures, failures