use crate::arithmetic::Fx;
use super::focusing::{compute_focusing, Context, FocusingGraph, FocusingParams, Link};
use super::measures::entropy;
pub struct Impulse {
pub delta: Vec<([u8; 32], Fx)>,
pub delta_j: Fx,
pub directed: Fx,
pub entropy_drop: Fx,
pub discovery: Fx,
pub norm_l1: Fx,
}
fn abs(x: Fx) -> Fx {
if x < Fx::ZERO {
Fx::ZERO - x
} else {
x
}
}
pub fn impulse(
base: &[Link],
batch: &[Link],
ctx: &Context,
params: &FocusingParams,
epsilon: Fx,
) -> Impulse {
let g0 = FocusingGraph::build(base.iter().cloned(), ctx);
let r0 = compute_focusing(&g0, params);
let union: Vec<Link> = base.iter().cloned().chain(batch.iter().cloned()).collect();
let g1 = FocusingGraph::build(union, ctx);
let r1 = compute_focusing(&g1, params);
let before = |p: &[u8; 32]| -> Fx {
match g0.node_ids().iter().position(|h| h == p) {
Some(i) => r0.focus[i],
None => Fx::ZERO,
}
};
let mut delta = Vec::new();
let mut norm_l1 = Fx::ZERO;
for (i, pid) in g1.node_ids().iter().enumerate() {
let d = r1.focus[i] - before(pid);
let m = abs(d);
norm_l1 = norm_l1 + m;
if m >= epsilon {
delta.push((*pid, d));
}
}
let delta_j = r1.syntropy - r0.syntropy;
let entropy_drop = entropy(&r0.focus) - entropy(&r1.focus);
let (n0, n1) = (g0.n(), g1.n());
let discovery = if n0 == 0 || n1 == 0 {
Fx::ZERO
} else {
Fx::from_ratio(n1 as i64, n0 as i64).ln()
};
let directed = if delta_j > Fx::ZERO {
delta_j
} else {
Fx::ZERO
};
Impulse {
delta,
delta_j,
directed,
entropy_drop,
discovery,
norm_l1,
}
}
pub fn propose(base: &[Link], links: &[Link], ctx: &Context, params: &FocusingParams) -> Fx {
impulse(base, links, ctx, params, params.epsilon).directed
}
#[cfg(test)]
mod tests {
use super::*;
fn hash(b: u8) -> [u8; 32] {
let mut h = [0u8; 32];
h[0] = b;
h
}
fn link(from: u8, to: u8, amount: u128) -> Link {
Link::stake(hash(from), hash(to), amount)
}
fn eps() -> Fx {
Fx::from_ratio(1, 100_000)
}
#[test]
fn a_new_link_shifts_focus_and_is_local() {
let base = vec![link(1, 2, 100), link(2, 3, 100), link(3, 1, 100)];
let batch = vec![link(3, 1, 400)]; let imp = impulse(
&base,
&batch,
&Context::none(),
&FocusingParams::default(),
eps(),
);
assert!(!imp.delta.is_empty(), "a reshaping link must move ฯ*");
assert!(imp.norm_l1 > Fx::ZERO, "โฮฯ*โโ must be positive");
let expect = if imp.delta_j > Fx::ZERO {
imp.delta_j
} else {
Fx::ZERO
};
assert_eq!(
imp.directed.raw(),
expect.raw(),
"directed must equal [ฮJ]โ"
);
}
#[test]
fn delta_j_decomposes_into_entropy_drop_plus_discovery() {
let base = vec![link(1, 2, 100), link(2, 3, 100), link(3, 1, 100)];
let batch = vec![link(1, 4, 250)]; let imp = impulse(
&base,
&batch,
&Context::none(),
&FocusingParams::default(),
eps(),
);
let lhs = imp.delta_j.to_f64();
let rhs = imp.entropy_drop.to_f64() + imp.discovery.to_f64();
assert!(
(lhs - rhs).abs() < 1e-3,
"ฮJ ({lhs}) โ entropy_drop + discovery ({rhs})"
);
}
#[test]
fn discovery_term_fires_only_on_new_particles() {
let base = vec![link(1, 2, 100), link(2, 3, 100), link(3, 1, 100)];
let disc = impulse(
&base,
&[link(1, 9, 200)],
&Context::none(),
&FocusingParams::default(),
eps(),
);
assert!(
disc.discovery > Fx::ZERO,
"a new particle must charge discovery > 0"
);
let reuse = impulse(
&base,
&[link(1, 3, 200)],
&Context::none(),
&FocusingParams::default(),
eps(),
);
assert!(
abs(reuse.discovery) < eps(),
"no new particle โ discovery โ 0 (got {})",
reuse.discovery.to_f64()
);
}
#[test]
fn first_links_from_an_empty_base() {
let batch = vec![link(1, 2, 100), link(2, 3, 100), link(3, 1, 100)];
let imp = impulse(
&[],
&batch,
&Context::none(),
&FocusingParams::default(),
eps(),
);
assert!(!imp.delta.is_empty(), "first links must register a shift");
assert_eq!(
imp.discovery.raw(),
Fx::ZERO.raw(),
"empty base has no discovery ratio"
);
let mass: f64 = imp.delta.iter().map(|(_, d)| d.to_f64()).sum();
assert!(
(mass - 1.0).abs() < 1e-3,
"ฮฯ* from empty base should sum to 1 (ฯ*_after), got {mass}"
);
}
#[test]
fn directed_clips_a_flattening_batch() {
let base = vec![link(2, 1, 1000), link(3, 1, 1000), link(1, 2, 10)];
let batch = vec![link(1, 2, 5000), link(1, 3, 5000)];
let imp = impulse(
&base,
&batch,
&Context::none(),
&FocusingParams::default(),
eps(),
);
assert!(
imp.delta_j < Fx::ZERO,
"this batch should lower syntropy (ฮJ={})",
imp.delta_j.to_f64()
);
assert_eq!(
imp.directed.raw(),
Fx::ZERO.raw(),
"a syntropy-lowering batch must mint nothing"
);
}
#[test]
fn propose_is_the_standalone_directed_impulse() {
let base = vec![link(1, 2, 100), link(2, 3, 100), link(3, 1, 100)];
let mine = vec![link(2, 1, 8000)]; let params = FocusingParams::default();
let claim = propose(&base, &mine, &Context::none(), ¶ms);
let directed = impulse(&base, &mine, &Context::none(), ¶ms, eps()).directed;
assert_eq!(
claim.raw(),
directed.raw(),
"propose = impulse(...).directed"
);
assert!(
claim.to_f64() > 0.0,
"a real contribution proposes a positive reward ceiling"
);
}
}