use tru::{impulse, Context, Fx, FocusingParams, Link};
pub struct Contribution {
pub neuron: [u8; 32],
pub links: Vec<Link>,
pub surprise: Fx,
}
impl Contribution {
fn weighted(&self) -> Vec<Link> {
let rho = clamp01(self.surprise);
self.links
.iter()
.cloned()
.map(|mut l| {
l.price = clamp01(l.price) * rho;
l
})
.collect()
}
}
fn clamp01(x: Fx) -> Fx {
if x < Fx::ZERO {
Fx::ZERO
} else if x > Fx::ONE {
Fx::ONE
} else {
x
}
}
pub fn value(base: &[Link], coalition: &[&Contribution], ctx: &Context, params: &FocusingParams) -> Fx {
let mut batch = Vec::new();
for c in coalition {
batch.extend(c.weighted());
}
impulse(base, &batch, ctx, params, params.epsilon).directed
}
pub fn marginals(base: &[Link], contribs: &[Contribution], order: &[usize], ctx: &Context, params: &FocusingParams) -> Vec<Fx> {
let mut out = vec![Fx::ZERO; contribs.len()];
let mut prefix: Vec<&Contribution> = Vec::new();
let mut prev = Fx::ZERO; for &i in order {
prefix.push(&contribs[i]);
let v = value(base, &prefix, ctx, params);
out[i] = v - prev;
prev = v;
}
out
}
pub fn shapley_exact(base: &[Link], contribs: &[Contribution], ctx: &Context, params: &FocusingParams) -> Vec<([u8; 32], Fx)> {
let n = contribs.len();
if n == 0 {
return vec![];
}
let orders = permutations(n);
let mut acc = vec![Fx::ZERO; n];
for order in &orders {
let m = marginals(base, contribs, order, ctx, params);
for i in 0..n {
acc[i] = acc[i] + m[i];
}
}
let inv = Fx::ONE.div(Fx::from_int(orders.len() as i64));
contribs.iter().enumerate().map(|(i, c)| (c.neuron, acc[i] * inv)).collect()
}
fn permutations(n: usize) -> Vec<Vec<usize>> {
let mut out = Vec::new();
let mut cur: Vec<usize> = (0..n).collect();
fn go(a: &mut Vec<usize>, k: usize, out: &mut Vec<Vec<usize>>) {
if k == a.len() {
out.push(a.clone());
return;
}
for i in k..a.len() {
a.swap(k, i);
go(a, k + 1, out);
a.swap(k, i);
}
}
go(&mut cur, 0, &mut out);
out
}
pub fn ordering(n: usize, beacon: &[u8; 32], nonce: u64) -> Vec<usize> {
let mut perm: Vec<usize> = (0..n).collect();
if n < 2 {
return perm;
}
let mut buf = [0u8; 40];
buf[..32].copy_from_slice(beacon);
buf[32..].copy_from_slice(&nonce.to_le_bytes());
let mut digest = *cyber_hemera::hash(&buf).as_bytes();
let mut byte = 0usize;
let next = |digest: &mut [u8; 32], byte: &mut usize| -> u64 {
if *byte + 8 > 32 {
*digest = *cyber_hemera::hash(digest).as_bytes();
*byte = 0;
}
let v = u64::from_le_bytes(digest[*byte..*byte + 8].try_into().unwrap());
*byte += 8;
v
};
for i in (1..n).rev() {
let j = (next(&mut digest, &mut byte) % (i as u64 + 1)) as usize;
perm.swap(i, j);
}
perm
}
pub fn shapley(base: &[Link], contribs: &[Contribution], ctx: &Context, params: &FocusingParams, samples: u64, beacon: &[u8; 32]) -> Vec<([u8; 32], Fx)> {
let n = contribs.len();
if n == 0 || samples == 0 {
return contribs.iter().map(|c| (c.neuron, Fx::ZERO)).collect();
}
let mut acc = vec![Fx::ZERO; n];
for s in 0..samples {
let order = ordering(n, beacon, s);
let m = marginals(base, contribs, &order, ctx, params);
for i in 0..n {
acc[i] = acc[i] + m[i];
}
}
let inv = Fx::ONE.div(Fx::from_int(samples as i64));
contribs.iter().enumerate().map(|(i, c)| (c.neuron, acc[i] * inv)).collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn hash(b: u8) -> [u8; 32] {
let mut h = [0u8; 32];
h[0] = b;
h
}
fn contrib(neuron: u8, links: Vec<Link>, rho: Fx) -> Contribution {
Contribution { neuron: hash(neuron), links, surprise: rho }
}
fn beacon() -> [u8; 32] {
hash(0xBE)
}
fn base() -> Vec<Link> {
vec![
Link::stake(hash(1), hash(2), 100),
Link::stake(hash(2), hash(3), 100),
Link::stake(hash(3), hash(1), 100),
]
}
#[test]
fn value_of_empty_coalition_is_zero() {
let v = value(&base(), &[], &Context::none(), &FocusingParams::default());
assert_eq!(v.raw(), Fx::ZERO.raw());
}
#[test]
fn a_copy_contributes_nothing() {
let c = contrib(9, vec![Link::stake(hash(3), hash(1), 400)], Fx::ZERO); let v = value(&base(), &[&c], &Context::none(), &FocusingParams::default());
assert_eq!(v.raw(), Fx::ZERO.raw(), "a ρ=0 copy must add zero value");
}
#[test]
fn shapley_exact_is_symmetric_efficient_and_null() {
let params = FocusingParams::default();
let a = contrib(10, vec![Link::stake(hash(2), hash(1), 8000)], Fx::ONE);
let b = contrib(11, vec![Link::stake(hash(2), hash(1), 8000)], Fx::ONE);
let s = shapley_exact(&base(), &[a, b], &Context::none(), ¶ms);
assert!((s[0].1.to_f64() - s[1].1.to_f64()).abs() < 1e-9, "symmetric split equally");
let a = contrib(10, vec![Link::stake(hash(2), hash(1), 8000)], Fx::ONE);
let copy = contrib(11, vec![Link::stake(hash(3), hash(1), 8000)], Fx::ZERO); let all = value(&base(), &[&a, ©], &Context::none(), ¶ms);
let a = contrib(10, vec![Link::stake(hash(2), hash(1), 8000)], Fx::ONE);
let copy = contrib(11, vec![Link::stake(hash(3), hash(1), 8000)], Fx::ZERO);
let s = shapley_exact(&base(), &[a, copy], &Context::none(), ¶ms);
let sum: f64 = s.iter().map(|x| x.1.to_f64()).sum();
assert!((sum - all.to_f64()).abs() < 1e-9, "Σ Shapley = v★(N)");
assert!(s[1].1.to_f64().abs() < 1e-9, "ρ=0 copy earns 0");
assert!(s[0].1.to_f64() > 0.0, "the real contributor earns the value");
}
#[test]
fn ordering_is_deterministic_and_a_permutation() {
let p1 = ordering(6, &beacon(), 3);
assert_eq!(p1, ordering(6, &beacon(), 3), "same beacon+nonce → same ordering");
let mut sorted = p1.clone();
sorted.sort();
assert_eq!(sorted, vec![0, 1, 2, 3, 4, 5], "a permutation of 0..n");
assert_ne!(p1, ordering(6, &beacon(), 4), "different nonce → different ordering");
}
#[test]
fn lottery_estimates_the_exact_division() {
let params = FocusingParams::default();
let mk = || [contrib(10, vec![Link::stake(hash(2), hash(1), 8000)], Fx::ONE), contrib(11, vec![Link::stake(hash(3), hash(1), 6000)], Fx::ONE)];
let exact = shapley_exact(&base(), &mk(), &Context::none(), ¶ms);
let est = shapley(&base(), &mk(), &Context::none(), ¶ms, 64, &beacon());
for i in 0..2 {
assert!(
(est[i].1.to_f64() - exact[i].1.to_f64()).abs() < 5e-3,
"MC estimate {} ≠ exact {} for contributor {i}",
est[i].1.to_f64(),
exact[i].1.to_f64()
);
}
}
}