refactor!: make Competitor::message an Option, and compute_elapsed loud
Two of the five remaining items on #23. `Competitor.message` was a `Gaussian` using the improper `N_INF` as an "unset" sentinel, so `message != N_INF` meant "has a message" and every reader had to know that convention. It is now `Option<Gaussian>`, which makes "no message yet" and "a legitimately improper message" distinguishable at the type level instead of by float comparison. Worth noting what the change surfaced: switching the type turned every read site into a compile error, and there were eight — two in the convergence sweep, five in ingestion, one in new_backward_info. The last is the interesting one: `skill.backward = agents[agent].message` needed `unwrap_or(N_INF)` rather than an unwrap, because an absent message genuinely does mean the improper identity there. A sentinel-based refactor would have had to find that by reading. This is a breaking change: `message` is a public field. It rides the next minor bump. `compute_elapsed` clamped a negative elapsed to zero silently. Negative elapsed means slices are being visited out of time order, which would otherwise make drift *reduce* uncertainty. Release still clamps, so a bad timestamp degrades to "no drift" rather than corrupting a posterior, but debug now trips — getting there is a slice-ordering bug, not something callers can cause with ordinary data. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T5SYDExxL4vZgvunrcNSMc
This commit is contained in:
+23
-17
@@ -1,5 +1,4 @@
|
||||
use crate::{
|
||||
N_INF,
|
||||
drift::{ConstantDrift, Drift},
|
||||
gaussian::Gaussian,
|
||||
rating::Rating,
|
||||
@@ -13,7 +12,14 @@ use crate::{
|
||||
#[derive(Debug)]
|
||||
pub struct Competitor<T: Time = i64, D: Drift<T> = ConstantDrift> {
|
||||
pub rating: Rating<T, D>,
|
||||
pub message: Gaussian,
|
||||
/// The forward message carried from this competitor's last appearance, or
|
||||
/// `None` before they have appeared anywhere.
|
||||
///
|
||||
/// Previously an improper `N_INF` served as the unset sentinel, which made
|
||||
/// "no message yet" indistinguishable from "a legitimately improper
|
||||
/// message" at the type level and required every reader to know the
|
||||
/// convention.
|
||||
pub message: Option<Gaussian>,
|
||||
pub last_time: Option<T>,
|
||||
}
|
||||
|
||||
@@ -21,14 +27,16 @@ impl<T: Time, D: Drift<T>> Competitor<T, D> {
|
||||
/// Compute the message received at time `now`, with drift accumulated
|
||||
/// from `self.last_time` (if any) to `now`.
|
||||
pub(crate) fn receive(&self, now: &T) -> Gaussian {
|
||||
if self.message != N_INF {
|
||||
let elapsed_variance = match &self.last_time {
|
||||
Some(last) => self.rating.drift.variance_delta(last, now),
|
||||
None => 0.0,
|
||||
};
|
||||
self.message.forget(elapsed_variance)
|
||||
} else {
|
||||
self.rating.prior
|
||||
match self.message {
|
||||
Some(message) => {
|
||||
let elapsed_variance = match &self.last_time {
|
||||
Some(last) => self.rating.drift.variance_delta(last, now),
|
||||
None => 0.0,
|
||||
};
|
||||
|
||||
message.forget(elapsed_variance)
|
||||
}
|
||||
None => self.rating.prior,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,11 +45,9 @@ impl<T: Time, D: Drift<T>> Competitor<T, D> {
|
||||
/// Used in convergence sweeps where the elapsed was cached at slice-construction time
|
||||
/// and should not be recomputed from `last_time` (which may have shifted).
|
||||
pub(crate) fn receive_for_elapsed(&self, elapsed: i64) -> Gaussian {
|
||||
if self.message != N_INF {
|
||||
self.message
|
||||
.forget(self.rating.drift.variance_for_elapsed(elapsed))
|
||||
} else {
|
||||
self.rating.prior
|
||||
match self.message {
|
||||
Some(message) => message.forget(self.rating.drift.variance_for_elapsed(elapsed)),
|
||||
None => self.rating.prior,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,7 +56,7 @@ impl Default for Competitor<i64, ConstantDrift> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
rating: Rating::default(),
|
||||
message: N_INF,
|
||||
message: None,
|
||||
last_time: None,
|
||||
}
|
||||
}
|
||||
@@ -63,7 +69,7 @@ where
|
||||
C: Iterator<Item = &'a mut Competitor<T, D>>,
|
||||
{
|
||||
for c in competitors {
|
||||
c.message = N_INF;
|
||||
c.message = None;
|
||||
if last_time {
|
||||
c.last_time = None;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user