use std::collections::HashMap;
use crate::arithmetic::Fx;
use super::csr::{CsrBuilder, CsrMatrix};
use super::operators::{diffusion_step, heat_step, normalize_l1, springs_step};
// โโ Parameters โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
/// Tri-kernel parameters, fixed-point over the Goldilocks field.
pub struct FocusingParams {
/// Diffusion teleport probability ฮฑ.
pub alpha: Fx,
/// Springs screening strength ฮผ.
pub mu: Fx,
/// Heat kernel time ฯ.
pub tau: Fx,
/// Blend weight for diffusion (ฮป_d + ฮป_s + ฮป_h = 1).
pub lambda_d: Fx,
/// Blend weight for springs.
pub lambda_s: Fx,
/// Blend weight for heat.
pub lambda_h: Fx,
/// Convergence target ฮต: the iteration runs T(ฮต) = min t with ฮบ^t โค ฮต.
pub epsilon: Fx,
/// Hard cap on outer iterations (used when ฮบ is degenerate).
pub iter_cap: usize,
}
impl Default for FocusingParams {
fn default() -> Self {
Self {
alpha: Fx::from_ratio(15, 100),
mu: Fx::ONE,
tau: Fx::ONE,
lambda_d: Fx::from_ratio(5, 10),
lambda_s: Fx::from_ratio(3, 10),
lambda_h: Fx::from_ratio(2, 10),
epsilon: Fx::from_ratio(1, 1_000_000),
iter_cap: 500,
}
}
}
// โโ Input link type โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
/// A single cyberlink contributing to the field.
#[derive(Clone)]
pub struct Link {
/// Signing neuron ฮฝ โ the key karma is looked up under.
pub neuron: [u8; 32],
/// Source particle (32-byte hemera hash).
pub from: [u8; 32],
/// Target particle (32-byte hemera hash).
pub to: [u8; 32],
/// Stake amount (raw token units).
pub amount: u128,
/// Valence: +1 affirm, -1 challenge, 0 void/hold. Does not enter `A_eff`
/// directly (focusing): its epistemic effect is mediated through `price`.
pub valence: i8,
/// Market believability `f(price(โ)) โ [0,1]`: the ICBS price mapped to an
/// edge multiplier. `Fx::ONE` is market-neutral (fully believed); `ZERO`
/// is fully doubted, structurally pruning the edge. Supplied by bbg.
pub price: Fx,
}
impl Link {
/// A stake-only link with a neutral market (`price = 1`) and its own neuron
/// as author. Recovers the pre-karma/price behaviour exactly.
pub fn stake(from: [u8; 32], to: [u8; 32], amount: u128) -> Self {
Self {
neuron: from,
from,
to,
amount,
valence: 1,
price: Fx::ONE,
}
}
}
/// Per-neuron karma `ฮบ(ฮฝ)`: the non-transferable BTS trust multiplier read
/// from bbg each epoch. An unknown neuron scores the neutral baseline
/// `Fx::ONE` โ new identities are karma-light, never karma-negative.
#[derive(Default)]
pub struct Karma(HashMap<[u8; 32], Fx>);
impl Karma {
/// No karma data โ every neuron scores the neutral baseline. Recovers the
/// stake-only weighting.
pub fn none() -> Self {
Self(HashMap::new())
}
/// Karma from an explicit `(neuron, ฮบ)` table.
pub fn from_pairs(pairs: impl IntoIterator<Item = ([u8; 32], Fx)>) -> Self {
Self(pairs.into_iter().collect())
}
/// `ฮบ(ฮฝ)`, defaulting to the neutral baseline `Fx::ONE`.
pub fn get(&self, neuron: &[u8; 32]) -> Fx {
self.0.get(neuron).copied().unwrap_or(Fx::ONE)
}
}
/// Per-neuron will: the broad-staking budget (locked span> ร duration)
/// read from bbg each epoch. Unlike per-link conviction, will is auto-shared
/// across every link the neuron creates (attention). An unknown neuron has
/// zero will โ it contributes only its explicit conviction. Units match `amount`
/// (smallest token units), so it adds to the conviction stake before weighting.
#[derive(Default)]
pub struct Will(HashMap<[u8; 32], u128>);
impl Will {
/// No will data โ every neuron's attention is its conviction alone. Recovers
/// the conviction-only stake.
pub fn none() -> Self {
Self(HashMap::new())
}
/// Will from an explicit `(neuron, budget)` table.
pub fn from_pairs(pairs: impl IntoIterator<Item = ([u8; 32], u128)>) -> Self {
Self(pairs.into_iter().collect())
}
/// `will(ฮฝ)`, defaulting to zero.
pub fn get(&self, neuron: &[u8; 32]) -> u128 {
self.0.get(neuron).copied().unwrap_or(0)
}
}
/// The per-epoch attention context read from bbg: the inputs to `A_eff`
/// beyond the raw links โ karma (the trust multiplier) and will (the
/// broad-staking budget). Bundled so new epoch inputs extend one type rather
/// than every `build` signature.
#[derive(Default)]
pub struct Context {
pub karma: Karma,
pub will: Will,
}
impl Context {
/// Neutral context: no karma, no will. Recovers conviction-and-price-only
/// weighting.
pub fn none() -> Self {
Self::default()
}
/// A context carrying karma but no will.
pub fn with_karma(karma: Karma) -> Self {
Self {
karma,
will: Will::none(),
}
}
}
/// The believability multiplier `f(price)`, clamped to `[0,1]`.
fn clamp01(x: Fx) -> Fx {
if x < Fx::ZERO {
Fx::ZERO
} else if x > Fx::ONE {
Fx::ONE
} else {
x
}
}
// โโ FocusingGraph โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
/// Pre-built adjacency structures for one coupled tri-kernel computation.
///
/// Effective adjacency is the honesty-weighted sum (focusing, attention,
/// truth-scoring): `A_eff(p,q) = ฮฃ_{โ: pโq} stake(โ)ยทฮบ(ฮฝ(โ))ยทf(price(โ))`,
/// where the per-link stake is a neuron's attention โ its explicit
/// conviction `amount` plus its share of broad will. `ฮบ(ฮฝ)` is the neuron's
/// karma, `f(price)` the ICBS believability. With `Context::none()` and
/// neutral `price = 1` this reduces to conviction-only stake-weighting.
pub struct FocusingGraph {
n: usize,
/// Particle hash at each node index.
node_ids: Vec<[u8; 32]>,
/// Column-stochastic transition: `transition[q][p] = A_eff(p,q)/out_strength(p)`.
transition: CsrMatrix,
/// True for nodes with no outgoing strength.
dangling: Vec<bool>,
/// Symmetric weights `A_sym(i,j) = A_eff(i,j) + A_eff(j,i)`.
sym_weights: CsrMatrix,
/// Weighted undirected degree `d(i) = ฮฃ_j A_sym(i,j)`.
und_degree: Vec<Fx>,
/// Stake-weighted teleport prior, normalized to sum 1.
teleport: Vec<Fx>,
/// Largest Laplacian eigenvalue โLโ (for the contraction ฮบ).
lambda_max: Fx,
/// Algebraic connectivity ฮปโ (Fiedler value).
lambda_2: Fx,
}
impl FocusingGraph {
/// Build the honesty-weighted effective adjacency from cyberlinks and the
/// epoch's attention context (karma + will). Self-loops, links with no
/// effective stake (no conviction and no will), and links the market fully
/// doubts (`f(price)ยทฮบ = 0`) are skipped โ a market-rejected link is
/// structurally absent, not merely light.
pub fn build(links: impl IntoIterator<Item = Link>, ctx: &Context) -> Self {
// Self-loops carry no focus; drop them. Zero-stake links are kept here
// and fall out below once their effective weight resolves to zero (a
// pure-will link has amount 0 but nonzero attention).
let kept: Vec<Link> = links.into_iter().filter(|l| l.from != l.to).collect();
if kept.is_empty() {
return Self::empty();
}
// Attention = conviction + will share. Will is auto-distributed equally
// across every link a neuron authored (attention); count them first.
let mut link_count: HashMap<[u8; 32], u128> = HashMap::new();
for l in &kept {
*link_count.entry(l.neuron).or_insert(0) += 1;
}
// Effective raw stake per link, in token units (integer floor share).
let attention = |l: &Link| -> u128 {
let share = ctx.will.get(&l.neuron) / link_count[&l.neuron];
l.amount + share
};
// Stake weights are scale-invariant for ฯ*; normalize by the largest so
// they land in (0,1] (comparable to ฮผ=ฯ=1) and never overflow the field.
// Effective weight w = stakeยทฮบ(ฮฝ)ยทf(price): ฮบ(ฮฝ) โฅ 0 (default 1),
// f(price) โ [0,1], so w โค stake โค 1 and overflow safety is preserved.
let max_amount = kept.iter().map(&attention).max().unwrap_or(1).max(1);
let raw: Vec<([u8; 32], [u8; 32], Fx)> = kept
.iter()
.map(|l| {
let stake = Fx::ratio_u128(attention(l), max_amount);
let w = stake * ctx.karma.get(&l.neuron) * clamp01(l.price);
(l.from, l.to, w)
})
.filter(|&(_, _, w)| !w.is_zero())
.collect();
if raw.is_empty() {
return Self::empty();
}
// Node indices, assigned by first appearance (deterministic).
let mut node_ids: Vec<[u8; 32]> = Vec::new();
let mut node_index: HashMap<[u8; 32], usize> = HashMap::new();
for &(from, to, _) in &raw {
for hash in [from, to] {
if let std::collections::hash_map::Entry::Vacant(e) = node_index.entry(hash) {
e.insert(node_ids.len());
node_ids.push(hash);
}
}
}
let n = node_ids.len();
// Directed A_eff and per-node stake mass (for the teleport prior).
let mut dir_weight: HashMap<(usize, usize), Fx> = HashMap::new();
let mut out_strength = vec![Fx::ZERO; n];
let mut node_stake = vec![Fx::ZERO; n];
for &(from, to, w) in &raw {
let (fi, ti) = (node_index[&from], node_index[&to]);
let e = dir_weight.entry((fi, ti)).or_insert(Fx::ZERO);
*e = *e + w;
out_strength[fi] = out_strength[fi] + w;
node_stake[fi] = node_stake[fi] + w;
node_stake[ti] = node_stake[ti] + w;
}
// Transition (col-stochastic) and symmetric weights + degree.
let mut trans = CsrBuilder::new(n);
let mut sym = CsrBuilder::new(n);
let mut und_degree = vec![Fx::ZERO; n];
for (&(fi, ti), &w) in &dir_weight {
trans.add(ti, fi, w.div(out_strength[fi])); // T[to][from]
sym.add(fi, ti, w);
sym.add(ti, fi, w);
und_degree[fi] = und_degree[fi] + w;
und_degree[ti] = und_degree[ti] + w;
}
let dangling: Vec<bool> = (0..n).map(|i| out_strength[i].is_zero()).collect();
let teleport = normalize_l1(&node_stake);
let sym_weights = sym.build();
// Spectrum for the contraction ฮบ (graph-only; params fold in at compute).
let lambda_max = super::spectral::lambda_max(&sym_weights, &und_degree, n, 60);
let lambda_2 = super::spectral::lambda_2(&sym_weights, &und_degree, n, lambda_max, 120);
Self {
n,
node_ids,
transition: trans.build(),
dangling,
sym_weights,
und_degree,
teleport,
lambda_max,
lambda_2,
}
}
fn empty() -> Self {
Self {
n: 0,
node_ids: vec![],
transition: CsrBuilder::new(0).build(),
dangling: vec![],
sym_weights: CsrBuilder::new(0).build(),
und_degree: vec![],
teleport: vec![],
lambda_max: Fx::ZERO,
lambda_2: Fx::ZERO,
}
}
pub fn n(&self) -> usize {
self.n
}
pub fn node_id(&self, idx: usize) -> &[u8; 32] {
&self.node_ids[idx]
}
pub fn node_ids(&self) -> &[[u8; 32]] {
&self.node_ids
}
/// Largest Laplacian eigenvalue โLโ.
pub fn lambda_max(&self) -> Fx {
self.lambda_max
}
/// Algebraic connectivity ฮปโ (Fiedler value).
pub fn lambda_2(&self) -> Fx {
self.lambda_2
}
/// The spectral embedding mir reads: each particle's coordinate in the
/// space of the Laplacian's `k` lowest nontrivial eigenvectors (Fiedler
/// first). `iters` is the subspace-iteration count. Structurally similar
/// particles receive nearby coordinates.
pub fn embedding(&self, k: usize, iters: usize) -> SpectralEmbedding {
let (vectors, eigenvalues) = super::spectral::spectral_vectors(
&self.sym_weights,
&self.und_degree,
self.n,
self.lambda_max,
k,
iters,
);
let kk = vectors.len();
// Transpose the k eigenvectors into per-particle coordinate rows.
let coords: Vec<Vec<Fx>> = (0..self.n)
.map(|i| (0..kk).map(|c| vectors[c][i]).collect())
.collect();
SpectralEmbedding {
k: kk,
coords,
eigenvalues,
}
}
}
/// The spectral embedding focusing emits to mir every epoch: each
/// particle's position in the low-frequency Laplacian eigenspace.
pub struct SpectralEmbedding {
/// Coordinates per particle (= number of eigenvectors extracted).
pub k: usize,
/// `coords[i]` is the k-vector for particle [`FocusingGraph::node_id`]`(i)`.
pub coords: Vec<Vec<Fx>>,
/// The k Laplacian eigenvalues, ascending (ฮปโ first).
pub eigenvalues: Vec<Fx>,
}
// โโ Output โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
/// Result of one tri-kernel computation.
pub struct FocusingResult {
/// ฯ* focus distribution (fixed-point), indexed as [`FocusingGraph::node_ids`].
pub focus: Vec<Fx>,
/// Syntropy J(ฯ*) = D_KL(ฯ* โ u), emitted alongside ฯ* every epoch.
pub syntropy: Fx,
/// Diffusion component of the final step.
pub diffusion: Vec<Fx>,
/// Springs component of the final step.
pub springs: Vec<Fx>,
/// Heat component of the final step.
pub heat: Vec<Fx>,
}
// โโ Composite: one coupled iteration to the fixed point โโโโโโโโโโโโโโโ
/// The composite contraction coefficient ฮบ for this graph and params.
pub fn contraction(g: &FocusingGraph, p: &FocusingParams) -> Fx {
super::spectral::kappa(p, g.lambda_max, g.lambda_2)
}
/// The step count T(ฮต) the coupled iteration runs: the smallest T with ฮบ^T โค ฮต
/// (tri-kernel ยง2.2), capped by `p.iter_cap`.
pub fn derived_steps(g: &FocusingGraph, p: &FocusingParams) -> usize {
super::spectral::steps_for(contraction(g, p), p.epsilon, p.iter_cap)
}
/// Compute ฯ* by iterating the coupled tri-kernel: each step applies D, S, and
/// H_ฯ to the same current ฯ, blends `ฮป_dยทD + ฮป_sยทS + ฮป_hยทH`, normalizes onto
/// the simplex, and feeds ฯ back โ for a fixed T(ฮต) steps derived from the
/// contraction ฮบ. Fixed-point throughout, so two runs are bit-identical.
pub fn compute_focusing(g: &FocusingGraph, p: &FocusingParams) -> FocusingResult {
iterate(g, p, derived_steps(g, p))
}
/// The coupled iteration run for an explicit step count.
pub fn iterate(g: &FocusingGraph, p: &FocusingParams, steps: usize) -> FocusingResult {
if g.n == 0 {
return FocusingResult {
focus: vec![],
syntropy: Fx::ZERO,
diffusion: vec![],
springs: vec![],
heat: vec![],
};
}
let n = g.n;
let uniform = Fx::from_ratio(1, n as i64);
let x0 = vec![uniform; n];
let mut phi = vec![uniform; n];
let mut diffusion = vec![Fx::ZERO; n];
let mut springs = vec![Fx::ZERO; n];
let mut heat = vec![Fx::ZERO; n];
for _ in 0..steps {
diffusion = diffusion_step(&phi, &g.transition, &g.dangling, &g.teleport, p.alpha);
springs = springs_step(&phi, &g.sym_weights, &g.und_degree, p.mu, &x0);
heat = heat_step(&phi, &g.sym_weights, &g.und_degree, g.lambda_max, p.tau);
let blend: Vec<Fx> = (0..n)
.map(|i| p.lambda_d * diffusion[i] + p.lambda_s * springs[i] + p.lambda_h * heat[i])
.collect();
phi = normalize_l1(&blend);
}
let syntropy = super::measures::syntropy(&phi);
FocusingResult {
focus: phi,
syntropy,
diffusion,
springs,
heat,
}
}
// โโ Tests โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
#[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 node_idx(g: &FocusingGraph, b: u8) -> usize {
g.node_ids()
.iter()
.position(|h| h[0] == b && h[1..] == [0u8; 31][..])
.unwrap()
}
#[test]
fn focus_sums_to_one() {
let links = vec![link(1, 2, 100), link(2, 3, 50), link(3, 1, 200)];
let g = FocusingGraph::build(links, &Context::none());
let r = compute_focusing(&g, &FocusingParams::default());
let total: f64 = r.focus.iter().map(|x| x.to_f64()).sum();
assert!((total - 1.0).abs() < 1e-6, "focus sums to {total}");
assert!(r.focus.iter().all(|x| *x > Fx::ZERO), "all focus positive");
}
#[test]
fn deterministic_bit_identical() {
let mk = || {
vec![
link(1, 2, 100),
link(2, 3, 50),
link(3, 1, 200),
link(4, 1, 300),
]
};
let a = compute_focusing(
&FocusingGraph::build(mk(), &Context::none()),
&FocusingParams::default(),
);
let b = compute_focusing(
&FocusingGraph::build(mk(), &Context::none()),
&FocusingParams::default(),
);
assert!(
a.focus
.iter()
.zip(&b.focus)
.all(|(x, y)| x.raw() == y.raw()),
"ฯ* not bit-identical across runs"
);
}
#[test]
fn contraction_below_one() {
let links = vec![
link(1, 2, 100),
link(2, 3, 100),
link(3, 1, 100),
link(4, 1, 100),
];
let g = FocusingGraph::build(links, &Context::none());
let p = FocusingParams::default();
let kappa = contraction(&g, &p);
assert!(kappa < Fx::ONE, "ฮบ = {} must be < 1", kappa.to_f64());
assert!(kappa > Fx::ZERO, "ฮบ = {} must be > 0", kappa.to_f64());
// ฮป_max is real (positive) for a nonempty graph.
assert!(g.lambda_max() > Fx::ZERO, "ฮป_max should be positive");
}
#[test]
fn derived_steps_reach_the_fixed_point() {
let links = vec![
link(1, 2, 100),
link(2, 3, 100),
link(3, 1, 100),
link(4, 1, 100),
];
let g = FocusingGraph::build(links, &Context::none());
let p = FocusingParams::default();
let t = derived_steps(&g, &p);
assert!(
t > 0 && t < p.iter_cap,
"derived T = {t} should be a real step count"
);
// Past T(ฮต) the iterate barely moves โ the fixed point is reached.
let at_t = iterate(&g, &p, t);
let past_t = iterate(&g, &p, t + 20);
let drift: f64 = at_t
.focus
.iter()
.zip(&past_t.focus)
.map(|(a, b)| (a.to_f64() - b.to_f64()).abs())
.sum();
assert!(drift < 1e-4, "drift past T = {drift}, not converged");
}
#[test]
fn heat_conserves_mass_and_smooths() {
let links = vec![
link(1, 2, 100),
link(2, 3, 100),
link(3, 1, 100),
link(4, 1, 100),
];
let g = FocusingGraph::build(links, &Context::none());
let n = g.n();
let i1 = node_idx(&g, 1);
// A delta spike at node 1.
let mut v = vec![Fx::ZERO; n];
v[i1] = Fx::ONE;
let h = heat_step(&v, &g.sym_weights, &g.und_degree, g.lambda_max, Fx::ONE);
// exp(โฯL) conserves mass (Lยท1 = 0), up to series truncation.
let mass: f64 = h.iter().map(|x| x.to_f64()).sum();
assert!(
(mass - 1.0).abs() < 1e-2,
"heat should conserve mass, got {mass}"
);
// and it diffuses the spike off the peak while staying ~positive.
assert!(h[i1].to_f64() < 1.0, "heat should spread mass off node 1");
assert!(
h.iter().all(|x| x.to_f64() > -1e-3),
"heat stays approximately positive"
);
}
#[test]
fn high_in_stake_ranks_higher() {
let links = vec![
link(1, 2, 100),
link(2, 3, 100),
link(3, 1, 100),
link(4, 1, 1000),
];
let g = FocusingGraph::build(links, &Context::none());
let r = compute_focusing(&g, &FocusingParams::default());
let (i1, i3) = (node_idx(&g, 1), node_idx(&g, 3));
assert!(
r.focus[i1] > r.focus[i3],
"high-in-stake node 1 should outrank node 3"
);
}
#[test]
fn well_linked_node_ranks_higher() {
let links = vec![
link(1, 2, 100),
link(2, 3, 100),
link(3, 1, 100),
link(4, 1, 100),
];
let g = FocusingGraph::build(links, &Context::none());
let r = compute_focusing(&g, &FocusingParams::default());
let (i1, i2) = (node_idx(&g, 1), node_idx(&g, 2));
assert!(
r.focus[i1] > r.focus[i2],
"well-linked node 1 should outrank node 2"
);
}
#[test]
fn large_stakes_are_scale_invariant() {
// 10^15-scale stakes must not overflow and must give the SAME ฯ* as the
// proportionally-smaller graph (weights are scale-invariant).
let big = 1_000_000_000_000_000u128;
let large = vec![
Link::stake(hash(1), hash(2), big),
Link::stake(hash(2), hash(3), big / 2),
Link::stake(hash(4), hash(1), big * 3),
];
let small = vec![link(1, 2, 1000), link(2, 3, 500), link(4, 1, 3000)];
let rl = compute_focusing(
&FocusingGraph::build(large, &Context::none()),
&FocusingParams::default(),
);
let rs = compute_focusing(
&FocusingGraph::build(small, &Context::none()),
&FocusingParams::default(),
);
let total: f64 = rl.focus.iter().map(|x| x.to_f64()).sum();
assert!((total - 1.0).abs() < 1e-6, "large-stake ฯ* sums to {total}");
assert!(rl.focus.iter().all(|x| *x > Fx::ZERO));
assert!(
rl.focus
.iter()
.zip(&rs.focus)
.all(|(a, b)| a.raw() == b.raw()),
"ฯ* not scale-invariant"
);
}
#[test]
fn empty_graph() {
let g = FocusingGraph::build(vec![], &Context::none());
assert_eq!(g.n(), 0);
assert!(compute_focusing(&g, &FocusingParams::default())
.focus
.is_empty());
}
#[test]
fn self_loops_excluded() {
let g = FocusingGraph::build(vec![Link::stake(hash(1), hash(1), 100)], &Context::none());
assert_eq!(g.n(), 0);
}
#[test]
fn zero_amount_excluded() {
let g = FocusingGraph::build(vec![link(1, 2, 0), link(2, 3, 50)], &Context::none());
assert_eq!(g.n(), 2);
}
// โโ honesty weighting: karma and market price โโโโโโโโโโโโโโโโโโโโโ
// A voter V(=5) splits its focus between two otherwise-symmetric
// candidates A(=1) and B(=2), each of which feeds a sink C(=3) that returns
// to V. V has two out-edges, so re-weighting one genuinely redirects its
// diffusion mass โ the regime where honesty weighting shows up in the rank.
fn voter_graph(w_a: (/*neuron*/ [u8; 32], /*price*/ Fx), w_b: ([u8; 32], Fx)) -> Vec<Link> {
vec![
Link {
neuron: w_a.0,
from: hash(5),
to: hash(1),
amount: 100,
valence: 1,
price: w_a.1,
},
Link {
neuron: w_b.0,
from: hash(5),
to: hash(2),
amount: 100,
valence: 1,
price: w_b.1,
},
link(1, 3, 100), // A โ C
link(2, 3, 100), // B โ C
link(3, 5, 100), // C โ V
]
}
#[test]
fn karma_amplifies_the_link_it_weights() {
let (sign_a, sign_b) = (hash(7), hash(8));
let mk = || voter_graph((sign_a, Fx::ONE), (sign_b, Fx::ONE));
// Neutral: A and B are symmetric, so they share focus exactly.
let g0 = FocusingGraph::build(mk(), &Context::none());
let r0 = compute_focusing(&g0, &FocusingParams::default());
let gap = (r0.focus[node_idx(&g0, 1)].to_f64() - r0.focus[node_idx(&g0, 2)].to_f64()).abs();
assert!(
gap < 1e-6,
"A and B must be symmetric under no karma (ฮ={gap})"
);
// ฮบ=3 on A's signer sends more of V's focus to A: A outranks B.
let g1 = FocusingGraph::build(
mk(),
&Context::with_karma(Karma::from_pairs([(sign_a, Fx::from_int(3))])),
);
let r1 = compute_focusing(&g1, &FocusingParams::default());
assert!(
r1.focus[node_idx(&g1, 1)] > r1.focus[node_idx(&g1, 2)],
"ฮบ=3 on the A-link should make A outrank the symmetric B"
);
}
#[test]
fn market_price_scales_the_link_it_weights() {
let (na, nb) = (hash(7), hash(8));
// f(price)=0.5 on the B-link, full belief on A: A outranks B.
let g = FocusingGraph::build(
voter_graph((na, Fx::ONE), (nb, Fx::from_ratio(1, 2))),
&Context::none(),
);
let r = compute_focusing(&g, &FocusingParams::default());
assert!(
r.focus[node_idx(&g, 1)] > r.focus[node_idx(&g, 2)],
"a fully-believed link should outrank a half-believed one"
);
}
#[test]
fn market_doubt_prunes_the_edge() {
// f(price) = 0 (fully doubted): the edge is structurally absent, so its
// endpoints never enter the graph.
let doubted = Link {
neuron: hash(9),
from: hash(7),
to: hash(8),
amount: 100,
valence: 1,
price: Fx::ZERO,
};
let g = FocusingGraph::build(vec![doubted, link(1, 2, 100)], &Context::none());
assert_eq!(g.n(), 2, "a fully-doubted edge must create no nodes");
assert!(
!g.node_ids().iter().any(|h| h[0] == 7 || h[0] == 8),
"endpoints of the doubted edge must be absent"
);
}
// โโ attention: broad will vs per-link conviction โโโโโโโโโโโโโโโโโโ
#[test]
fn will_adds_broad_attention_to_a_neurons_links() {
// sign_a authors only the VโA spoke, so its whole will lands there:
// effective stake 100 + will โซ B's 100, and A outranks the symmetric B.
let (sign_a, sign_b) = (hash(7), hash(8));
let mk = || voter_graph((sign_a, Fx::ONE), (sign_b, Fx::ONE));
let g0 = FocusingGraph::build(mk(), &Context::none());
let r0 = compute_focusing(&g0, &FocusingParams::default());
let gap = (r0.focus[node_idx(&g0, 1)].to_f64() - r0.focus[node_idx(&g0, 2)].to_f64()).abs();
assert!(gap < 1e-6, "A and B symmetric under no will (ฮ={gap})");
let ctx = Context {
karma: Karma::none(),
will: Will::from_pairs([(sign_a, 300)]),
};
let g1 = FocusingGraph::build(mk(), &ctx);
let r1 = compute_focusing(&g1, &FocusingParams::default());
assert!(
r1.focus[node_idx(&g1, 1)] > r1.focus[node_idx(&g1, 2)],
"broad will on A's author should make A outrank the symmetric B"
);
}
#[test]
fn will_is_shared_equally_across_a_neurons_links() {
// One neuron authors both spokes VโA and VโB. Its will splits equally,
// so A and B stay symmetric โ were it not shared, one would dominate.
let s = hash(9);
let spokes = || {
vec![
Link {
neuron: s,
from: hash(5),
to: hash(1),
amount: 100,
valence: 1,
price: Fx::ONE,
},
Link {
neuron: s,
from: hash(5),
to: hash(2),
amount: 100,
valence: 1,
price: Fx::ONE,
},
link(1, 3, 100),
link(2, 3, 100),
link(3, 5, 100),
]
};
let ctx = Context {
karma: Karma::none(),
will: Will::from_pairs([(s, 1000)]),
};
let g_will = FocusingGraph::build(spokes(), &ctx);
let r_will = compute_focusing(&g_will, &FocusingParams::default());
let (a, b) = (
r_will.focus[node_idx(&g_will, 1)],
r_will.focus[node_idx(&g_will, 2)],
);
assert!(
(a.to_f64() - b.to_f64()).abs() < 1e-6,
"equal will split must keep A,B symmetric"
);
// And the will genuinely acted: V's spokes now carry more of its focus,
// so the sink C (node 3) draws more than in the will-free graph.
let g0 = FocusingGraph::build(spokes(), &Context::none());
let r0 = compute_focusing(&g0, &FocusingParams::default());
let c_will = r_will.focus[node_idx(&g_will, 3)].to_f64();
let c_none = r0.focus[node_idx(&g0, 3)].to_f64();
assert!(
(c_will - c_none).abs() > 1e-6,
"will should change the distribution, not vanish"
);
}
// โโ spectral embedding (positions for mir) โโโโโโโโโโโโโโโโโโโโโโโโ
fn sgn(x: Fx) -> i32 {
if x > Fx::ZERO {
1
} else if x < Fx::ZERO {
-1
} else {
0
}
}
/// Undirected edge โ a pair of unit-stake directed links.
fn undirected(a: u8, b: u8) -> [Link; 2] {
[
Link::stake(hash(a), hash(b), 100),
Link::stake(hash(b), hash(a), 100),
]
}
fn barbell() -> FocusingGraph {
// Two triangles {1,2,3} and {4,5,6} joined by the single bridge 3โ4.
let mut links = Vec::new();
for (a, b) in [(1, 2), (2, 3), (1, 3), (4, 5), (5, 6), (4, 6), (3, 4)] {
links.extend(undirected(a, b));
}
FocusingGraph::build(links, &Context::none())
}
#[test]
fn fiedler_vector_separates_the_two_communities() {
let g = barbell();
let emb = g.embedding(1, 300);
assert_eq!(emb.k, 1);
let coord = |b: u8| emb.coords[node_idx(&g, b)][0];
// All of cluster A share one sign; all of cluster B the opposite.
let sa = sgn(coord(1));
assert!(sa != 0, "Fiedler entry should not be zero");
for b in [2, 3] {
assert_eq!(sgn(coord(b)), sa, "node {b} must share cluster A's sign");
}
for b in [4, 5, 6] {
assert_eq!(
sgn(coord(b)),
-sa,
"node {b} must take cluster B's opposite sign"
);
}
}
#[test]
fn embedding_is_centered_and_orthogonal() {
let g = barbell();
let emb = g.embedding(2, 300);
assert_eq!(emb.k, 2);
// Each eigenvector is centered (orthogonal to the constant vector).
for c in 0..2 {
let sum: f64 = (0..g.n()).map(|i| emb.coords[i][c].to_f64()).sum();
assert!(sum.abs() < 1e-3, "eigenvector {c} not centered (ฮฃ={sum})");
}
// The two eigenvectors are mutually orthogonal.
let d: f64 = (0..g.n())
.map(|i| emb.coords[i][0].to_f64() * emb.coords[i][1].to_f64())
.sum();
assert!(d.abs() < 1e-2, "eigenvectors not orthogonal (โจvโ,vโโฉ={d})");
// Eigenvalues ascending and nonnegative (ฮปโ โค ฮปโ).
assert!(emb.eigenvalues[0].to_f64() >= -1e-6);
assert!(
emb.eigenvalues[1] >= emb.eigenvalues[0],
"eigenvalues must be ascending"
);
}
#[test]
fn embedding_k_clamps_to_n_minus_one() {
let g = barbell(); // 6 nodes
let emb = g.embedding(100, 50);
assert_eq!(
emb.k, 5,
"cannot extract more than nโ1 nontrivial eigenvectors"
);
assert!(emb.coords.iter().all(|row| row.len() == 5));
}
}
use HashMap;
use crateFx;
use ;
use ;
// โโ Parameters โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
/// Tri-kernel parameters, fixed-point over the Goldilocks field.
// โโ Input link type โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
/// A single cyberlink contributing to the field.
/// Per-neuron karma `ฮบ(ฮฝ)`: the non-transferable BTS trust multiplier read
/// from bbg each epoch. An unknown neuron scores the neutral baseline
/// `Fx::ONE` โ new identities are karma-light, never karma-negative.
;
/// Per-neuron will: the broad-staking budget (locked span> ร duration)
/// read from bbg each epoch. Unlike per-link conviction, will is auto-shared
/// across every link the neuron creates (attention). An unknown neuron has
/// zero will โ it contributes only its explicit conviction. Units match `amount`
/// (smallest token units), so it adds to the conviction stake before weighting.
;
/// The per-epoch attention context read from bbg: the inputs to `A_eff`
/// beyond the raw links โ karma (the trust multiplier) and will (the
/// broad-staking budget). Bundled so new epoch inputs extend one type rather
/// than every `build` signature.
/// The believability multiplier `f(price)`, clamped to `[0,1]`.
// โโ FocusingGraph โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
/// Pre-built adjacency structures for one coupled tri-kernel computation.
///
/// Effective adjacency is the honesty-weighted sum (focusing, attention,
/// truth-scoring): `A_eff(p,q) = ฮฃ_{โ: pโq} stake(โ)ยทฮบ(ฮฝ(โ))ยทf(price(โ))`,
/// where the per-link stake is a neuron's attention โ its explicit
/// conviction `amount` plus its share of broad will. `ฮบ(ฮฝ)` is the neuron's
/// karma, `f(price)` the ICBS believability. With `Context::none()` and
/// neutral `price = 1` this reduces to conviction-only stake-weighting.
/// The spectral embedding focusing emits to mir every epoch: each
/// particle's position in the low-frequency Laplacian eigenspace.
// โโ Output โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
/// Result of one tri-kernel computation.
// โโ Composite: one coupled iteration to the fixed point โโโโโโโโโโโโโโโ
/// The composite contraction coefficient ฮบ for this graph and params.
/// The step count T(ฮต) the coupled iteration runs: the smallest T with ฮบ^T โค ฮต
/// (tri-kernel ยง2.2), capped by `p.iter_cap`.
/// Compute ฯ* by iterating the coupled tri-kernel: each step applies D, S, and
/// H_ฯ to the same current ฯ, blends `ฮป_dยทD + ฮป_sยทS + ฮป_hยทH`, normalizes onto
/// the simplex, and feeds ฯ back โ for a fixed T(ฮต) steps derived from the
/// contraction ฮบ. Fixed-point throughout, so two runs are bit-identical.
/// The coupled iteration run for an explicit step count.
// โโ Tests โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ