use std::collections::HashMap;
use crate::arithmetic::Fx;
use crate::focusing::Karma;
pub struct Report {
pub neuron: [u8; 32],
pub belief: Fx,
pub prediction: Fx,
}
fn clampp(x: Fx) -> Fx {
let d = Fx::from_ratio(1, 1_000_000);
let hi = Fx::ONE - d;
if x < d {
d
} else if x > hi {
hi
} else {
x
}
}
fn kl(a: Fx, b: Fx) -> Fx {
let a = clampp(a);
let b = clampp(b);
a * a.div(b).ln() + (Fx::ONE - a) * (Fx::ONE - a).div(Fx::ONE - b).ln()
}
fn geo_mean(yes: &[Fx]) -> Fx {
let m = yes.len();
if m == 0 {
return Fx::from_ratio(1, 2);
}
let mut sly = Fx::ZERO;
let mut sln = Fx::ZERO;
for &x in yes {
let x = clampp(x);
sly = sly + x.ln();
sln = sln + (Fx::ONE - x).ln();
}
let inv = Fx::ONE.div(Fx::from_int(m as i64));
let gy = (sly * inv).exp();
let gn = (sln * inv).exp();
gy.div(gy + gn)
}
pub fn bts_scores(reports: &[Report]) -> Vec<Fx> {
let n = reports.len();
if n < 2 {
return vec![Fx::ZERO; n];
}
let beliefs: Vec<Fx> = reports.iter().map(|r| r.belief).collect();
let preds: Vec<Fx> = reports.iter().map(|r| r.prediction).collect();
(0..n)
.map(|i| {
let others_b: Vec<Fx> = (0..n).filter(|&k| k != i).map(|k| beliefs[k]).collect();
let others_m: Vec<Fx> = (0..n).filter(|&k| k != i).map(|k| preds[k]).collect();
let pbar = geo_mean(&others_b);
let mbar = geo_mean(&others_m);
let (p, m) = (beliefs[i], preds[i]);
let info_gain = kl(p, mbar) - kl(p, pbar);
let pred_acc = kl(pbar, m);
info_gain - pred_acc
})
.collect()
}
pub fn karma_step(kappa: Fx, score: Fx, eta: Fx) -> Fx {
let next = kappa + eta * score;
if next < Fx::ZERO {
Fx::ZERO
} else {
next
}
}
pub fn surprise(score: Fx, s_max: Fx) -> Fx {
if s_max <= Fx::ZERO {
return Fx::ZERO;
}
let r = score.div(s_max);
if r < Fx::ZERO {
Fx::ZERO
} else if r > Fx::ONE {
Fx::ONE
} else {
r
}
}
pub fn accumulate(prior: &Karma, reports: &[Report], eta: Fx) -> Karma {
let scores = bts_scores(reports);
let mut next: HashMap<[u8; 32], Fx> = HashMap::new();
for (r, &s) in reports.iter().zip(&scores) {
let base = next
.get(&r.neuron)
.copied()
.unwrap_or_else(|| prior.get(&r.neuron));
next.insert(r.neuron, karma_step(base, s, eta));
}
Karma::from_pairs(next)
}
#[cfg(test)]
mod tests {
use super::*;
fn hash(b: u8) -> [u8; 32] {
let mut h = [0u8; 32];
h[0] = b;
h
}
fn report(id: u8, belief: f64, prediction: f64) -> Report {
Report {
neuron: hash(id),
belief: Fx::from_ratio((belief * 1000.0) as i64, 1000),
prediction: Fx::from_ratio((prediction * 1000.0) as i64, 1000),
}
}
#[test]
fn consensus_copier_scores_near_zero_information() {
let reports = vec![
report(1, 0.7, 0.7),
report(2, 0.7, 0.7),
report(3, 0.7, 0.7),
report(4, 0.7, 0.7),
];
let s = bts_scores(&reports);
for (i, &si) in s.iter().enumerate() {
assert!(
si.to_f64().abs() < 1e-2,
"copier {i} should score โ 0 (got {})",
si.to_f64()
);
}
}
#[test]
fn accurate_meta_prediction_beats_an_inaccurate_one() {
let reports = vec![
report(1, 0.6, 0.6), report(2, 0.6, 0.6),
report(3, 0.6, 0.6),
report(10, 0.6, 0.6), report(11, 0.6, 0.05), ];
let s = bts_scores(&reports);
assert!(
s[3] > s[4],
"accurate predictor ({}) must beat inaccurate ({})",
s[3].to_f64(),
s[4].to_f64()
);
}
#[test]
fn surprisingly_popular_report_beats_a_follower() {
let reports = vec![
report(1, 0.8, 0.8),
report(2, 0.8, 0.8),
report(3, 0.8, 0.8),
report(4, 0.15, 0.8), ];
let s = bts_scores(&reports);
let follower = s[0].to_f64();
let contrarian = s[3].to_f64();
assert!(
contrarian > follower,
"contrarian ({contrarian}) should beat follower ({follower})"
);
}
#[test]
fn karma_accumulates_up_on_signal_and_floors_at_zero() {
let eta = Fx::from_ratio(1, 2);
let up = karma_step(Fx::ONE, Fx::from_ratio(1, 2), eta);
assert!(up > Fx::ONE, "positive BTS score should raise karma");
let floored = karma_step(Fx::from_ratio(1, 10), Fx::from_int(-100), eta);
assert_eq!(floored.raw(), Fx::ZERO.raw(), "karma is floored at zero");
}
#[test]
fn surprise_clips_to_unit_interval() {
let smax = Fx::ONE;
assert_eq!(
surprise(Fx::from_int(-5), smax).raw(),
Fx::ZERO.raw(),
"noise โ ฯ = 0"
);
assert_eq!(
surprise(Fx::from_int(5), smax).raw(),
Fx::ONE.raw(),
"very surprising โ ฯ = 1"
);
let mid = surprise(Fx::from_ratio(1, 2), smax);
assert!(
(mid.to_f64() - 0.5).abs() < 1e-6,
"ฯ scales linearly in-range"
);
}
#[test]
fn accumulate_closes_the_loop_to_a_karma_table() {
let reports = vec![
report(1, 0.8, 0.8),
report(2, 0.8, 0.8),
report(3, 0.8, 0.8),
report(4, 0.15, 0.8),
];
let karma = accumulate(&Karma::none(), &reports, Fx::from_ratio(1, 4));
assert!(
karma.get(&hash(4)) > karma.get(&hash(1)),
"the contrarian's karma ({}) should exceed the follower's ({})",
karma.get(&hash(4)).to_f64(),
karma.get(&hash(1)).to_f64()
);
}
}