203 lines
6.7 KiB
Rust
203 lines
6.7 KiB
Rust
//! Belief policies: streaming fold over observations.
|
||
//!
|
||
//! A policy reduces an unordered stream of matching observations down to a
|
||
//! single accumulated `Belief` (an `Observation`) via [`BeliefPolicy::step`].
|
||
//! The fold is intentionally iterative rather than iterator-based so that a
|
||
//! caller (e.g. `Node::belief`) never has to buffer all observations at once —
|
||
//! a 1 KiB payload × N buffer is unacceptable on the MCU target.
|
||
|
||
use crate::observation::{NodeId, Observation};
|
||
|
||
/// Reduces an ordered stream of observations to a single accumulated belief.
|
||
pub trait BeliefPolicy {
|
||
type Belief;
|
||
/// Streaming fold: called once per matching observation (already
|
||
/// age-adjusted to `now`), in no particular order. `acc` is `None` before
|
||
/// the first call. Return the new accumulator.
|
||
fn step(
|
||
&self,
|
||
me: NodeId,
|
||
acc: Option<Self::Belief>,
|
||
obs: &Observation,
|
||
) -> Option<Self::Belief>;
|
||
}
|
||
|
||
/// Latest observation wins: the observation with the smallest `age_ms`; ties
|
||
/// are broken deterministically by `(observer, seq)` ascending.
|
||
pub struct LatestWins;
|
||
|
||
/// Prefer the agent's own observation: the youngest of its own observations
|
||
/// if any exist, otherwise the same as [`LatestWins`]. An observation from
|
||
/// `me` beats any observation from another observer regardless of age.
|
||
pub struct SelfFirst;
|
||
|
||
/// The [`LatestWins`] ordering: `obs` beats `cur` when it is younger, with a
|
||
/// deterministic `(observer, seq)` ascending tie-break on equal age.
|
||
fn latest_beats(obs: &Observation, cur: &Observation) -> bool {
|
||
obs.age_ms < cur.age_ms
|
||
|| (obs.age_ms == cur.age_ms && (obs.observer, obs.seq) < (cur.observer, cur.seq))
|
||
}
|
||
|
||
impl BeliefPolicy for LatestWins {
|
||
type Belief = Observation;
|
||
|
||
fn step(
|
||
&self,
|
||
_me: NodeId,
|
||
acc: Option<Self::Belief>,
|
||
obs: &Observation,
|
||
) -> Option<Self::Belief> {
|
||
match acc {
|
||
None => Some(obs.clone()),
|
||
Some(cur) => {
|
||
if latest_beats(obs, &cur) {
|
||
Some(obs.clone())
|
||
} else {
|
||
Some(cur)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
impl BeliefPolicy for SelfFirst {
|
||
type Belief = Observation;
|
||
|
||
fn step(
|
||
&self,
|
||
me: NodeId,
|
||
acc: Option<Self::Belief>,
|
||
obs: &Observation,
|
||
) -> Option<Self::Belief> {
|
||
match acc {
|
||
None => Some(obs.clone()),
|
||
Some(cur) => {
|
||
let obs_is_me = obs.observer == me;
|
||
let cur_is_me = cur.observer == me;
|
||
let winner = if obs_is_me && !cur_is_me {
|
||
obs.clone()
|
||
} else if !obs_is_me && cur_is_me {
|
||
cur
|
||
} else {
|
||
// Both from `me` or both from someone else: fall back to
|
||
// the LatestWins rule (youngest, tie broken by
|
||
// (observer, seq) ascending).
|
||
if latest_beats(obs, &cur) {
|
||
obs.clone()
|
||
} else {
|
||
cur
|
||
}
|
||
};
|
||
Some(winner)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use crate::observation::{Payload, Topic};
|
||
|
||
fn obs(observer: [u8; 16], seq: u64, age_ms: u64) -> Observation {
|
||
Observation {
|
||
observer,
|
||
seq,
|
||
age_ms,
|
||
observed_at: None,
|
||
topic: Topic::from_slice(b"door/3").unwrap(),
|
||
payload: Payload::from_slice(b"open").unwrap(),
|
||
to: None,
|
||
lifetime_ms: None,
|
||
}
|
||
}
|
||
|
||
fn fold<P: BeliefPolicy>(policy: &P, me: NodeId, obs: &[Observation]) -> Option<P::Belief> {
|
||
let mut acc: Option<P::Belief> = None;
|
||
for o in obs {
|
||
acc = policy.step(me, acc, o);
|
||
}
|
||
acc
|
||
}
|
||
|
||
fn fold_rev<P: BeliefPolicy>(policy: &P, me: NodeId, obs: &[Observation]) -> Option<P::Belief> {
|
||
let mut acc: Option<P::Belief> = None;
|
||
for i in (0..obs.len()).rev() {
|
||
acc = policy.step(me, acc, &obs[i]);
|
||
}
|
||
acc
|
||
}
|
||
|
||
#[test]
|
||
fn latest_wins_picks_smallest_age() {
|
||
let me = [7; 16];
|
||
let obs = [
|
||
obs([10; 16], 1, 300),
|
||
obs([11; 16], 2, 50),
|
||
obs([12; 16], 3, 10),
|
||
obs([13; 16], 4, 200),
|
||
];
|
||
// Forward and reverse order must both select the youngest (age 10).
|
||
assert_eq!(fold(&LatestWins, me, &obs).unwrap().seq, 3);
|
||
assert_eq!(fold_rev(&LatestWins, me, &obs).unwrap().seq, 3);
|
||
}
|
||
|
||
#[test]
|
||
fn latest_wins_none_on_empty() {
|
||
let me = [7; 16];
|
||
let obs: [Observation; 0] = [];
|
||
assert_eq!(fold(&LatestWins, me, &obs), None);
|
||
assert_eq!(fold_rev(&LatestWins, me, &obs), None);
|
||
}
|
||
|
||
#[test]
|
||
fn latest_wins_tie_is_deterministic() {
|
||
let me = [7; 16];
|
||
// Same age, distinct (observer, seq): winner must be the smaller tuple
|
||
// regardless of iteration order.
|
||
let lo = obs([2; 16], 5, 100);
|
||
let hi = obs([9; 16], 1, 100);
|
||
let obs = [lo, hi];
|
||
assert_eq!(fold(&LatestWins, me, &obs).unwrap().observer, [2; 16]);
|
||
assert_eq!(fold_rev(&LatestWins, me, &obs).unwrap().observer, [2; 16]);
|
||
}
|
||
|
||
#[test]
|
||
fn latest_wins_tie_same_observer_breaks_by_seq() {
|
||
let me = [7; 16];
|
||
// Same observer, same age: the tie-break must fall to the smaller seq.
|
||
let hi_seq = obs([5; 16], 9, 100);
|
||
let lo_seq = obs([5; 16], 2, 100);
|
||
let obs = [hi_seq, lo_seq];
|
||
assert_eq!(fold(&LatestWins, me, &obs).unwrap().seq, 2);
|
||
assert_eq!(fold_rev(&LatestWins, me, &obs).unwrap().seq, 2);
|
||
}
|
||
|
||
#[test]
|
||
fn self_first_prefers_own_even_if_older() {
|
||
let me = [7; 16];
|
||
let mine = obs([7; 16], 1, 500);
|
||
let theirs = obs([8; 16], 2, 10);
|
||
let obs = [mine, theirs];
|
||
// Own observation (age 500) beats theirs (age 10) regardless of order.
|
||
assert_eq!(fold(&SelfFirst, me, &obs).unwrap().observer, [7; 16]);
|
||
assert_eq!(fold_rev(&SelfFirst, me, &obs).unwrap().observer, [7; 16]);
|
||
}
|
||
|
||
#[test]
|
||
fn self_first_falls_back_to_latest() {
|
||
let me = [7; 16];
|
||
// No observation belongs to `me`, so SelfFirst must match LatestWins.
|
||
let a = obs([10; 16], 1, 300);
|
||
let b = obs([11; 16], 2, 50);
|
||
let c = obs([12; 16], 3, 10);
|
||
let obs = [a, b, c];
|
||
assert_eq!(fold(&SelfFirst, me, &obs).unwrap().seq, 3);
|
||
assert_eq!(fold_rev(&SelfFirst, me, &obs).unwrap().seq, 3);
|
||
assert_eq!(
|
||
fold(&SelfFirst, me, &obs).unwrap(),
|
||
fold(&LatestWins, me, &obs).unwrap()
|
||
);
|
||
}
|
||
}
|