use std::collections::HashMap;
use crate::graph::Cyberlink;
pub fn axon(p: &[u8; 32], q: &[u8; 32]) -> [u8; 32] {
let mut buf = [0u8; 64];
buf[..32].copy_from_slice(p);
buf[32..].copy_from_slice(q);
let mut out = [0u8; 32];
out.copy_from_slice(cyber_hemera::hash(&buf).as_bytes());
out
}
pub fn effective_stake(valence: i8, amount: u128, rho: i128) -> i128 {
match valence {
1 => amount as i128 * rho,
-1 => -(amount as i128 * rho),
_ => 0,
}
}
#[derive(Clone, Copy)]
pub struct Edge {
pub src: u32,
pub tgt: u32,
pub axon: u32,
pub stake: i128,
pub valence: i8,
}
pub struct ParticleIndex {
particles: Vec<[u8; 32]>,
index: HashMap<[u8; 32], u32>,
}
impl ParticleIndex {
fn new() -> Self {
Self {
particles: Vec::new(),
index: HashMap::new(),
}
}
fn intern(&mut self, p: [u8; 32]) -> u32 {
if let Some(&i) = self.index.get(&p) {
return i;
}
let i = self.particles.len() as u32;
self.index.insert(p, i);
self.particles.push(p);
i
}
pub fn idx(&self, p: &[u8; 32]) -> Option<u32> {
self.index.get(p).copied()
}
pub fn particle(&self, i: u32) -> [u8; 32] {
self.particles[i as usize]
}
pub fn particles(&self) -> &[[u8; 32]] {
&self.particles
}
pub fn len(&self) -> usize {
self.particles.len()
}
pub fn is_empty(&self) -> bool {
self.particles.is_empty()
}
}
pub struct Adjacency {
pub n: usize,
pub out: Vec<Vec<(u32, i128)>>,
}
impl Adjacency {
pub fn out_strength(&self, i: u32) -> i128 {
self.out[i as usize].iter().map(|&(_, w)| w).sum()
}
}
pub fn build(
vocab_seed: &span>; 32,
links: &[Cyberlink],
) -> (ParticleIndex, Vec<Edge>, Adjacency) {
let mut v = ParticleIndex::new();
for &p in vocab_seed {
v.intern(p);
}
let mut edges = Vec::with_capacity(links.len());
for l in links {
let a = axon(&l.from, &l.to);
let si = v.intern(l.from);
let ti = v.intern(l.to);
let ai = v.intern(a);
edges.push(Edge {
src: si,
tgt: ti,
axon: ai,
stake: effective_stake(l.valence, l.amount, 1),
valence: l.valence,
});
}
let n = v.len();
let mut acc: Vec<HashMap<u32, i128>> = vec![HashMap::new(); n];
for e in &edges {
if e.stake > 0 {
*acc[e.src as usize].entry(e.tgt).or_insert(0) += e.stake;
}
}
let out: Vec<Vec<(u32, i128)>> = acc
.into_iter()
.map(|row| {
let mut cols: Vec<(u32, i128)> = row.into_iter().collect();
cols.sort_by_key(|&(j, _)| j);
cols
})
.collect();
(v, edges, Adjacency { n, out })
}
#[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, valence: i8) -> Cyberlink {
Cyberlink {
neuron: hash(from),
from: hash(from),
to: hash(to),
token: 0,
amount,
valence,
block: 0,
}
}
#[test]
fn axon_is_deterministic_and_oriented() {
let (p, q) = (hash(1), hash(2));
assert_eq!(axon(&p, &q), axon(&p, &q), "axon must be a pure function");
assert_ne!(
axon(&p, &q),
axon(&q, &p),
"axon is oriented: H(pโq) โ H(qโp)"
);
}
#[test]
fn ids_follow_insertion_order_p_q_axon() {
let links = vec![link(1, 2, 100, 1)];
let (v, _e, _a) = build(&[], &links);
assert_eq!(v.idx(&hash(1)), Some(0));
assert_eq!(v.idx(&hash(2)), Some(1));
assert_eq!(v.idx(&axon(&hash(1), &hash(2))), Some(2));
assert_eq!(v.len(), 3);
}
#[test]
fn vocab_seed_takes_the_low_ids() {
let seed = [hash(9), hash(8)];
let links = vec![link(1, 2, 100, 1)];
let (v, _e, _a) = build(&seed, &links);
assert_eq!(v.idx(&hash(9)), Some(0), "vocab seed comes first");
assert_eq!(v.idx(&hash(8)), Some(1));
assert_eq!(v.idx(&hash(1)), Some(2), "signal particles follow the seed");
}
#[test]
fn adjacency_sums_affirmations_and_drops_the_rest() {
let links = vec![
link(1, 2, 100, 1),
link(1, 2, 50, 1),
link(1, 3, 200, -1), link(1, 4, 200, 0), ];
let (v, _e, a) = build(&[], &links);
let (i1, i2) = (v.idx(&hash(1)).unwrap(), v.idx(&hash(2)).unwrap());
let row = &a.out[i1 as usize];
let w12 = row.iter().find(|&&(j, _)| j == i2).map(|&(_, w)| w);
assert_eq!(w12, Some(150), "affirmations 100+50 accumulate");
assert!(
a.out[i1 as usize].iter().all(|&(j, _)| j == i2),
"only the affirmed edge survives"
);
}
}