docs: complete the public API documentation contract
Closes the last open item in #25. `cargo clippy -W missing_errors_doc -W missing_panics_doc -W must_use_candidate -W doc_markdown` went from 56 warnings to zero. The 13 hand-written sections name the actual variants each function returns rather than gesturing at "an error". Establishing that meant reading the error paths — `Game::ranked` alone returns four distinct variants, and `record_draw` can hit TieWithoutDrawProbability where `record_winner` provably cannot, since a two-team decisive outcome has nothing to tie. Documenting those as interchangeable would have been worse than leaving them undocumented, because a reader would trust it. Two existing doc comments already described panics in prose but not under a `# Panics` heading, so neither rustdoc nor clippy surfaced them: `Outcome::winner` and `EventBuilder::weights`. Both now carry the heading, and `Outcome::winner` gained the note that it ties every loser, so `n >= 3` needs a positive p_draw — the crate's easiest error to hit by accident. The 43 mechanical fixes (31 `#[must_use]` on pure accessors, 11 missing backticks) were applied with `cargo clippy --fix`. `#[must_use]` on Gaussian's arithmetic and on `posteriors()` matters: discarding those results is always a bug, and until now nothing said so. Also documented why `[profile.release] debug = true` exists — cargo-flamegraph needs the symbols, and library profile settings are ignored downstream, so it reads as an oversight without the note. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T5SYDExxL4vZgvunrcNSMc
This commit is contained in:
@@ -65,6 +65,10 @@ plotters-backend = "0.3"
|
||||
time = { version = "0.3", features = ["parsing"] }
|
||||
trueskill-tt = { path = ".", features = ["approx"] }
|
||||
|
||||
# Debug symbols in release are for `just flame` (cargo-flamegraph), which needs
|
||||
# them to symbolicate. Profile settings in a library are ignored by downstream
|
||||
# consumers, so these only affect local builds — this is deliberate, not an
|
||||
# oversight.
|
||||
[profile.release]
|
||||
debug = true
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ pub struct Team<K> {
|
||||
}
|
||||
|
||||
impl<K> Team<K> {
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
members: SmallVec::new(),
|
||||
|
||||
@@ -50,8 +50,10 @@ where
|
||||
|
||||
/// Set per-member weights for the most recently added team.
|
||||
///
|
||||
/// Panics in debug builds if called before `.team(...)` or if the length
|
||||
/// doesn't match the team's member count.
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if called before any `.team(...)`. In debug builds, also panics
|
||||
/// if the number of weights does not match the team's member count.
|
||||
pub fn weights<I: IntoIterator<Item = f64>>(mut self, weights: I) -> Self {
|
||||
let idx = self
|
||||
.current_team_idx
|
||||
@@ -103,6 +105,10 @@ where
|
||||
}
|
||||
|
||||
/// Commit the event to the history.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Forwards to [`History::add_events`] and returns its errors.
|
||||
pub fn commit(self) -> Result<(), InferenceError> {
|
||||
self.history.add_events(std::iter::once(self.event))
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ pub struct MarginFactor {
|
||||
}
|
||||
|
||||
impl MarginFactor {
|
||||
#[must_use]
|
||||
pub fn new(diff: VarId, m_obs: f64, sigma: f64) -> Self {
|
||||
debug_assert!(sigma > 0.0, "score sigma must be positive");
|
||||
Self {
|
||||
|
||||
@@ -20,6 +20,7 @@ pub struct VarStore {
|
||||
}
|
||||
|
||||
impl VarStore {
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
@@ -28,10 +29,12 @@ impl VarStore {
|
||||
self.marginals.clear();
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn len(&self) -> usize {
|
||||
self.marginals.len()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.marginals.is_empty()
|
||||
}
|
||||
@@ -42,6 +45,7 @@ impl VarStore {
|
||||
id
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get(&self, id: VarId) -> Gaussian {
|
||||
self.marginals[id.0 as usize]
|
||||
}
|
||||
|
||||
@@ -5,12 +5,12 @@ use crate::factor::{Factor, VarId, VarStore};
|
||||
/// On each propagation:
|
||||
/// - Reads marginals at `team_a` and `team_b` (which already incorporate any
|
||||
/// incoming messages from neighboring factors).
|
||||
/// - Computes `new_diff = team_a - team_b` (variance addition; see Gaussian::Sub).
|
||||
/// - Computes `new_diff = team_a - team_b` (variance addition; see `Gaussian::Sub`).
|
||||
/// - Writes the new marginal to `diff`.
|
||||
/// - Returns the delta against the previous diff value.
|
||||
///
|
||||
/// This factor does NOT store an outgoing message; the diff variable is
|
||||
/// effectively replaced on each propagation. The TruncFactor on the same diff
|
||||
/// effectively replaced on each propagation. The `TruncFactor` on the same diff
|
||||
/// var holds the EP-divide message that produces the cavity.
|
||||
#[derive(Debug)]
|
||||
pub struct RankDiffFactor {
|
||||
|
||||
+2
-1
@@ -15,13 +15,14 @@ pub struct TruncFactor {
|
||||
pub diff: VarId,
|
||||
pub margin: f64,
|
||||
pub tie: bool,
|
||||
/// Outgoing message to the diff variable (initial: N_INF, the EP identity).
|
||||
/// Outgoing message to the diff variable (initial: `N_INF`, the EP identity).
|
||||
pub(crate) msg: Gaussian,
|
||||
/// Cached evidence (linear, not log) computed from the cavity on first propagation.
|
||||
pub(crate) evidence_cached: Option<f64>,
|
||||
}
|
||||
|
||||
impl TruncFactor {
|
||||
#[must_use]
|
||||
pub fn new(diff: VarId, margin: f64, tie: bool) -> Self {
|
||||
Self {
|
||||
diff,
|
||||
|
||||
+28
@@ -145,6 +145,7 @@ impl<T: Time, D: Drift<T>> OwnedGame<T, D> {
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn posteriors(&self) -> Vec<Vec<Gaussian>> {
|
||||
self.likelihoods
|
||||
.iter()
|
||||
@@ -153,6 +154,7 @@ impl<T: Time, D: Drift<T>> OwnedGame<T, D> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn log_evidence(&self) -> f64 {
|
||||
self.log_evidence
|
||||
}
|
||||
@@ -409,6 +411,7 @@ impl<'a, T: Time, D: Drift<T>> Game<'a, T, D> {
|
||||
self.likelihoods = likelihoods;
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn posteriors(&self) -> Vec<Vec<Gaussian>> {
|
||||
self.likelihoods
|
||||
.iter()
|
||||
@@ -422,12 +425,21 @@ impl<'a, T: Time, D: Drift<T>> Game<'a, T, D> {
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn log_evidence(&self) -> f64 {
|
||||
self.log_evidence
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Time, D: Drift<T>> Game<'_, T, D> {
|
||||
/// # Errors
|
||||
///
|
||||
/// - `InvalidProbability` if `options.p_draw` is outside `[0.0, 1.0)`.
|
||||
/// - `MismatchedShape` if the outcome's rank count differs from `teams.len()`.
|
||||
/// - `WrongOutcomeKind` if `outcome` is not `Outcome::Ranked`.
|
||||
/// - `TieWithoutDrawProbability` if the outcome ties two teams while
|
||||
/// `p_draw` is zero: the truncation margin is then zero and the two-sided
|
||||
/// tie update evaluates `0/0`.
|
||||
pub fn ranked(
|
||||
teams: &[&[Rating<T, D>]],
|
||||
outcome: crate::Outcome,
|
||||
@@ -478,6 +490,12 @@ impl<T: Time, D: Drift<T>> Game<'_, T, D> {
|
||||
))
|
||||
}
|
||||
|
||||
/// # Errors
|
||||
///
|
||||
/// - `InvalidParameter` if `options.score_sigma` is not strictly positive,
|
||||
/// or is NaN.
|
||||
/// - `MismatchedShape` if the outcome's score count differs from `teams.len()`.
|
||||
/// - `WrongOutcomeKind` if `outcome` is not `Outcome::Scored`.
|
||||
pub fn scored(
|
||||
teams: &[&[Rating<T, D>]],
|
||||
outcome: crate::Outcome,
|
||||
@@ -515,6 +533,12 @@ impl<T: Time, D: Drift<T>> Game<'_, T, D> {
|
||||
))
|
||||
}
|
||||
|
||||
/// # Errors
|
||||
///
|
||||
/// Delegates to [`Game::ranked`] with default options, so it returns the
|
||||
/// same errors — in practice `WrongOutcomeKind` for a non-ranked outcome,
|
||||
/// or `TieWithoutDrawProbability` for a draw, since the default `p_draw`
|
||||
/// applies rather than one you chose.
|
||||
pub fn one_v_one(
|
||||
a: &Rating<T, D>,
|
||||
b: &Rating<T, D>,
|
||||
@@ -525,6 +549,10 @@ impl<T: Time, D: Drift<T>> Game<'_, T, D> {
|
||||
Ok((post[0][0], post[1][0]))
|
||||
}
|
||||
|
||||
/// # Errors
|
||||
///
|
||||
/// Wraps each player in a one-member team and delegates to
|
||||
/// [`Game::ranked`], so it returns the same errors.
|
||||
pub fn free_for_all(
|
||||
players: &[&Rating<T, D>],
|
||||
outcome: crate::Outcome,
|
||||
|
||||
@@ -18,6 +18,7 @@ pub struct Gaussian {
|
||||
|
||||
impl Gaussian {
|
||||
/// Construct from mean and standard deviation.
|
||||
#[must_use]
|
||||
pub const fn from_ms(mu: f64, sigma: f64) -> Self {
|
||||
if sigma == f64::INFINITY {
|
||||
Self { pi: 0.0, tau: 0.0 }
|
||||
@@ -64,16 +65,19 @@ impl Gaussian {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn pi(&self) -> f64 {
|
||||
self.pi
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn tau(&self) -> f64 {
|
||||
self.tau
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn mu(&self) -> f64 {
|
||||
// A non-positive precision is an improper (uninformative) Gaussian — its mean is
|
||||
// undefined. Treat it like `pi == 0` and return 0. EP message cancellation can land
|
||||
@@ -102,6 +106,7 @@ impl Gaussian {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn sigma(&self) -> f64 {
|
||||
// A non-positive precision is improper → infinite standard deviation. Guarding
|
||||
// `pi <= 0.0` (not just `== 0.0`) keeps `1.0 / pi.sqrt()` from returning NaN when EP
|
||||
@@ -145,6 +150,7 @@ impl Gaussian {
|
||||
/// Used by within-game inference to stabilise oscillating fixed-point
|
||||
/// loops on hard graphs. `alpha = 1.0` returns `new` exactly;
|
||||
/// `alpha < 1.0` shrinks each per-step update.
|
||||
#[must_use]
|
||||
pub fn damp_natural(self, new: Gaussian, alpha: f64) -> Gaussian {
|
||||
Gaussian::from_natural(
|
||||
alpha * new.pi() + (1.0 - alpha) * self.pi(),
|
||||
|
||||
+41
-1
@@ -198,6 +198,7 @@ impl Default for History<i64, ConstantDrift, NullObserver, &'static str> {
|
||||
}
|
||||
|
||||
impl History<i64, ConstantDrift, NullObserver, &'static str> {
|
||||
#[must_use]
|
||||
pub fn builder() -> HistoryBuilder<i64, ConstantDrift, NullObserver, &'static str> {
|
||||
HistoryBuilder::default()
|
||||
}
|
||||
@@ -205,6 +206,7 @@ impl History<i64, ConstantDrift, NullObserver, &'static str> {
|
||||
|
||||
impl<K: Eq + Hash + Clone> History<i64, ConstantDrift, NullObserver, K> {
|
||||
/// Like `builder()` but uses a custom key type `K` instead of the default `&'static str`.
|
||||
#[must_use]
|
||||
pub fn builder_with_key() -> HistoryBuilder<i64, ConstantDrift, NullObserver, K> {
|
||||
HistoryBuilder {
|
||||
mu: MU,
|
||||
@@ -552,7 +554,11 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
||||
|
||||
/// 2-team win probability: returns `[P(team0 wins), P(team1 wins)]`.
|
||||
///
|
||||
/// Panics if `teams.len() != 2`. N-team support lands in T4.
|
||||
/// N-team support lands in T4.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `teams.len() != 2`.
|
||||
pub fn predict_outcome(&self, teams: &[&[&K]]) -> Vec<f64> {
|
||||
assert_eq!(teams.len(), 2, "predict_outcome T2: 2 teams only");
|
||||
let gather = |team: &[&K]| -> Gaussian {
|
||||
@@ -574,6 +580,15 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
||||
}
|
||||
|
||||
/// Run the full forward+backward convergence loop and return a summary.
|
||||
///
|
||||
/// Failing to reach `epsilon` within `max_iter` is not an error: the
|
||||
/// returned report carries `converged: false` and the final step.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// `NonFiniteResult` if a sweep produces a NaN or infinite step. EP has
|
||||
/// broken down at that point and further iterations cannot recover, so the
|
||||
/// loop stops rather than reporting a NaN step as convergence.
|
||||
pub fn converge(&mut self) -> Result<ConvergenceReport, InferenceError> {
|
||||
use std::time::Instant;
|
||||
|
||||
@@ -830,6 +845,13 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Record a single two-competitor event that `winner` won.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Ingests through the same path as [`History::add_events`], so it returns
|
||||
/// the same errors. A two-team decisive outcome cannot tie, so
|
||||
/// `TieWithoutDrawProbability` is not reachable here.
|
||||
pub fn record_winner<Q>(&mut self, winner: &Q, loser: &Q, time: T) -> Result<(), InferenceError>
|
||||
where
|
||||
K: Borrow<Q>,
|
||||
@@ -847,6 +869,13 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
||||
)
|
||||
}
|
||||
|
||||
/// Record a single two-competitor event that ended level.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Ingests through the same path as [`History::add_events`]. Note
|
||||
/// `TieWithoutDrawProbability` *is* reachable here: a draw needs a
|
||||
/// positive `p_draw`.
|
||||
pub fn record_draw<Q>(&mut self, a: &Q, b: &Q, time: T) -> Result<(), InferenceError>
|
||||
where
|
||||
K: Borrow<Q>,
|
||||
@@ -870,6 +899,17 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
||||
}
|
||||
|
||||
/// Bulk-ingest typed events.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// - `MismatchedShape` if an event's outcome does not describe the same
|
||||
/// number of teams the event has, or if per-member weights do not match
|
||||
/// the team's membership.
|
||||
/// - `InvalidParameter` if a per-event `score_sigma` override is not
|
||||
/// strictly positive.
|
||||
/// - `TieWithoutDrawProbability` if an event ties two teams while the
|
||||
/// history's `p_draw` is zero. This includes `Outcome::winner(w, n)` for
|
||||
/// `n >= 3`, which ties every loser.
|
||||
pub fn add_events<I>(&mut self, events: I) -> Result<(), InferenceError>
|
||||
where
|
||||
I: IntoIterator<Item = crate::event::Event<T, K>>,
|
||||
|
||||
@@ -25,6 +25,7 @@ impl<K> KeyTable<K>
|
||||
where
|
||||
K: Eq + Hash + Clone,
|
||||
{
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
forward: HashMap::new(),
|
||||
@@ -54,6 +55,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn key(&self, idx: Index) -> Option<&K> {
|
||||
self.reverse.get(idx.0)
|
||||
}
|
||||
@@ -62,10 +64,12 @@ where
|
||||
self.forward.keys()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn len(&self) -> usize {
|
||||
self.reverse.len()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.reverse.is_empty()
|
||||
}
|
||||
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
//! TrueSkill Through Time — Bayesian skill rating over a time axis.
|
||||
//! `TrueSkill` Through Time — Bayesian skill rating over a time axis.
|
||||
//!
|
||||
//! Where plain TrueSkill gives each competitor one running estimate, TrueSkill
|
||||
//! Where plain `TrueSkill` gives each competitor one running estimate, `TrueSkill`
|
||||
//! Through Time treats a whole history as a single model and infers skill *at
|
||||
//! every point in time*. Evidence flows both directions: a result today
|
||||
//! sharpens the estimate of who someone was last year, so early estimates stop
|
||||
@@ -361,6 +361,7 @@ pub(crate) fn sort_time<T: Copy + Ord>(xs: &[T], reverse: bool) -> Vec<usize> {
|
||||
/// Panics if fewer than two rating groups are supplied, or if any group is
|
||||
/// empty — match quality is a property of a contest between at least two
|
||||
/// non-empty sides.
|
||||
#[must_use]
|
||||
pub fn quality(rating_groups: &[&[Gaussian]], beta: f64) -> f64 {
|
||||
assert!(
|
||||
rating_groups.len() >= 2,
|
||||
|
||||
@@ -29,7 +29,13 @@ pub enum Outcome {
|
||||
impl Outcome {
|
||||
/// `n`-team outcome where team `winner` won and everyone else tied for last.
|
||||
///
|
||||
/// Note this ties every loser, so for `n >= 3` it needs a positive
|
||||
/// `p_draw` — see `InferenceError::TieWithoutDrawProbability`.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `winner >= n`.
|
||||
#[must_use]
|
||||
pub fn winner(winner: u32, n: u32) -> Self {
|
||||
assert!(winner < n, "winner index {winner} out of range 0..{n}");
|
||||
let ranks: SmallVec<[u32; 4]> = (0..n).map(|i| if i == winner { 0 } else { 1 }).collect();
|
||||
@@ -37,6 +43,7 @@ impl Outcome {
|
||||
}
|
||||
|
||||
/// All `n` teams tied.
|
||||
#[must_use]
|
||||
pub fn draw(n: u32) -> Self {
|
||||
Self::Ranked(SmallVec::from_vec(vec![0; n as usize]))
|
||||
}
|
||||
@@ -68,6 +75,7 @@ impl Outcome {
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn team_count(&self) -> usize {
|
||||
match self {
|
||||
Self::Ranked(r) => r.len(),
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
//! Schedule trait and built-in implementations.
|
||||
//!
|
||||
//! A schedule drives factor propagation to convergence. The default
|
||||
//! `EpsilonOrMax` performs one TeamSum sweep (setup) then alternating
|
||||
//! `EpsilonOrMax` performs one `TeamSum` sweep (setup) then alternating
|
||||
//! forward/backward sweeps over the iterating factors until the max
|
||||
//! delta drops below epsilon or `max` iterations is reached.
|
||||
|
||||
@@ -23,7 +23,7 @@ pub trait Schedule: Send + Sync {
|
||||
/// Default schedule: sweep forward then backward until step ≤ eps or iter == max.
|
||||
///
|
||||
/// Matches the existing `Game::likelihoods` loop bit-for-bit when given the
|
||||
/// same factor layout (TeamSums first, then alternating RankDiff/Trunc pairs).
|
||||
/// same factor layout (`TeamSums` first, then alternating RankDiff/Trunc pairs).
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct EpsilonOrMax {
|
||||
pub eps: f64,
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::{Index, competitor::Competitor, drift::Drift, time::Time};
|
||||
|
||||
/// Dense Vec-backed store for competitor state in History.
|
||||
///
|
||||
/// Indexed directly by Index.0, eliminating HashMap hashing in the
|
||||
/// Indexed directly by Index.0, eliminating `HashMap` hashing in the
|
||||
/// forward/backward sweep. Uses `Vec<Option<Competitor<T, D>>>` so slots can be
|
||||
/// absent without an explicit present mask.
|
||||
#[derive(Debug)]
|
||||
@@ -21,6 +21,7 @@ impl<T: Time, D: Drift<T>> Default for CompetitorStore<T, D> {
|
||||
}
|
||||
|
||||
impl<T: Time, D: Drift<T>> CompetitorStore<T, D> {
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
@@ -39,6 +40,7 @@ impl<T: Time, D: Drift<T>> CompetitorStore<T, D> {
|
||||
self.competitors[idx.0] = Some(competitor);
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get(&self, idx: Index) -> Option<&Competitor<T, D>> {
|
||||
self.competitors.get(idx.0).and_then(|slot| slot.as_ref())
|
||||
}
|
||||
@@ -49,14 +51,17 @@ impl<T: Time, D: Drift<T>> CompetitorStore<T, D> {
|
||||
.and_then(|slot| slot.as_mut())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn contains(&self, idx: Index) -> bool {
|
||||
self.get(idx).is_some()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn len(&self) -> usize {
|
||||
self.n_present
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.n_present == 0
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::{Index, time_slice::Skill};
|
||||
|
||||
/// Dense Vec-backed store for per-agent skill state within a TimeSlice.
|
||||
/// Dense Vec-backed store for per-agent skill state within a `TimeSlice`.
|
||||
///
|
||||
/// Indexed directly by Index.0, eliminating HashMap hashing in the inner
|
||||
/// Indexed directly by Index.0, eliminating `HashMap` hashing in the inner
|
||||
/// convergence loop. Uses a parallel `present` mask so iteration skips
|
||||
/// absent slots without incurring per-slot Option overhead in the hot path.
|
||||
#[derive(Debug, Default)]
|
||||
|
||||
@@ -370,6 +370,13 @@ impl<T: Time> TimeSlice<T> {
|
||||
.collect::<HashMap<_, _>>()
|
||||
}
|
||||
|
||||
/// Sweep this slice's events once, starting at index `from`.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if an event references a competitor with no entry in this
|
||||
/// slice's skill store. `add_events` inserts one for every participant, so
|
||||
/// this cannot happen for slices built through the public API.
|
||||
pub fn iteration<D: Drift<T>>(&mut self, from: usize, agents: &CompetitorStore<T, D>) {
|
||||
if from == 0 && self.color_groups_dirty {
|
||||
self.recompute_color_groups();
|
||||
|
||||
Reference in New Issue
Block a user