use std::collections::BTreeMap;
use crate::state::BbgState;
use crate::types::Particle;
pub struct PruneConfig {
pub max_bytes: u64,
pub rank_floor_pct: u8,
pub half_life_epochs: u64,
}
impl Default for PruneConfig {
fn default() -> Self {
Self {
max_bytes: 1 << 30, rank_floor_pct: 20,
half_life_epochs: 1_000,
}
}
}
pub struct PruneState {
pub last_touched: BTreeMap<Particle, u64>,
}
impl Default for PruneState {
fn default() -> Self {
Self { last_touched: BTreeMap::new() }
}
}
impl PruneState {
pub fn touch(&mut self, axon_id: Particle, epoch: u64) {
self.last_touched.insert(axon_id, epoch);
}
}
pub fn prune(state: &mut BbgState, ps: &mut PruneState, config: &PruneConfig, epoch: u64) {
let floor = rank_floor(state, config.rank_floor_pct);
let over_budget = estimate_bytes(state) > config.max_bytes;
let candidates: Vec<(Particle, u64)> = if over_budget {
state
.particles
.iter()
.filter(|(_, p)| p.pi_star < floor)
.map(|(id, p)| (*id, p.weight))
.collect()
} else {
ps.last_touched
.iter()
.filter(|(id, last)| {
let pi = state.particles.get(*id).map_or(0, |p| p.pi_star);
let stale = epoch.saturating_sub(**last) > config.half_life_epochs;
pi < floor && stale
})
.map(|(id, _)| {
let weight = state.particles.get(id).map_or(0, |p| p.weight);
(*id, weight)
})
.collect()
};
let mut sorted = candidates;
sorted.sort_by_key(|(_, w)| *w);
for (axon_id, _) in sorted {
if over_budget && estimate_bytes(state) <= config.max_bytes {
break;
}
remove_axon(state, ps, &axon_id);
}
if !state.particles.is_empty() {
state.root = state.compute_root();
}
}
fn rank_floor(state: &BbgState, pct: u8) -> u64 {
if pct == 0 {
return u64::MAX;
}
let mut scores: Vec<u64> = state.particles.values().map(|p| p.pi_star).collect();
if scores.is_empty() {
return 0;
}
scores.sort_unstable();
let protected_count = (scores.len() * pct as usize + 99) / 100; let boundary = scores.len().saturating_sub(protected_count);
scores[boundary]
}
pub fn estimate_bytes(state: &BbgState) -> u64 {
let particles = state.particles.len() as u64 * 80;
let axons_out: u64 = state.axons_out.values().map(|v| 32 + v.len() as u64 * 32).sum();
let axons_in: u64 = state.axons_in.values().map(|v| 32 + v.len() as u64 * 32).sum();
let neurons = state.neurons.len() as u64 * 56;
particles + axons_out + axons_in + neurons
}
fn remove_axon(state: &mut BbgState, ps: &mut PruneState, axon_id: &Particle) {
state.particles.remove(axon_id);
ps.last_touched.remove(axon_id);
if let Some((from, to)) = state.axon_edges.remove(axon_id) {
if let Some(list) = state.axons_out.get_mut(&from) {
list.retain(|id| id != axon_id);
if list.is_empty() {
state.axons_out.remove(&from);
}
}
if let Some(list) = state.axons_in.get_mut(&to) {
list.retain(|id| id != axon_id);
if list.is_empty() {
state.axons_in.remove(&to);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::signal::{Cyberlink, Signal};
use crate::state::EPOCH_BLOCKS;
use crate::types::NeuronRecord;
fn particle(seed: u8) -> Particle { [seed; 32] }
fn neuron_id(seed: u8) -> Particle { [seed; 32] }
fn make_state() -> BbgState {
let mut s = BbgState::new();
s.neurons.insert(neuron_id(1), NeuronRecord { focus: 10_000, karma: 0, stake: 0 });
s
}
fn insert_link(state: &mut BbgState, ps: &mut PruneState, from: Particle, to: Particle, amount: u64, epoch: u64) {
let sig = Signal {
neuron: neuron_id(1),
links: vec![Cyberlink { from, to, token: particle(0), amount, valence: 1 }],
box_moves: vec![],
height: epoch * EPOCH_BLOCKS,
};
state.insert(&sig).unwrap();
let aid = crate::state::axon_id(&from, &to);
ps.touch(aid, epoch);
}
#[test]
fn stale_low_weight_edge_is_pruned() {
let mut state = make_state();
let mut ps = PruneState::default();
let config = PruneConfig { half_life_epochs: 10, rank_floor_pct: 0, max_bytes: u64::MAX };
insert_link(&mut state, &mut ps, particle(1), particle(2), 100, 0);
assert!(state.particles.contains_key(&crate::state::axon_id(&particle(1), &particle(2))));
prune(&mut state, &mut ps, &config, 11);
assert!(!state.particles.contains_key(&crate::state::axon_id(&particle(1), &particle(2))));
}
#[test]
fn fresh_edge_survives_normal_pruning() {
let mut state = make_state();
let mut ps = PruneState::default();
let config = PruneConfig { half_life_epochs: 100, rank_floor_pct: 0, max_bytes: u64::MAX };
insert_link(&mut state, &mut ps, particle(1), particle(2), 100, 5);
prune(&mut state, &mut ps, &config, 10);
assert!(state.particles.contains_key(&crate::state::axon_id(&particle(1), &particle(2))));
}
#[test]
fn high_rank_edge_is_protected() {
let mut state = make_state();
let mut ps = PruneState::default();
let config = PruneConfig { half_life_epochs: 1, rank_floor_pct: 50, max_bytes: u64::MAX };
insert_link(&mut state, &mut ps, particle(1), particle(2), 100, 0);
let aid = crate::state::axon_id(&particle(1), &particle(2));
state.particles.get_mut(&aid).unwrap().pi_star = 9_999;
prune(&mut state, &mut ps, &config, 100);
assert!(state.particles.contains_key(&aid));
}
#[test]
fn gravity_prunes_fresh_low_density_when_over_budget() {
let mut state = make_state();
let mut ps = PruneState::default();
let config = PruneConfig { half_life_epochs: 10_000, rank_floor_pct: 0, max_bytes: 0 };
insert_link(&mut state, &mut ps, particle(1), particle(2), 50, 0);
insert_link(&mut state, &mut ps, particle(3), particle(4), 200, 0);
prune(&mut state, &mut ps, &config, 1);
let low_aid = crate::state::axon_id(&particle(1), &particle(2));
let hi_aid = crate::state::axon_id(&particle(3), &particle(4));
if state.particles.contains_key(&hi_aid) {
assert!(!state.particles.contains_key(&low_aid));
}
}
#[test]
fn refresh_resets_staleness() {
let mut state = make_state();
let mut ps = PruneState::default();
let config = PruneConfig { half_life_epochs: 10, rank_floor_pct: 0, max_bytes: u64::MAX };
insert_link(&mut state, &mut ps, particle(1), particle(2), 100, 0);
let aid = crate::state::axon_id(&particle(1), &particle(2));
ps.touch(aid, 8);
prune(&mut state, &mut ps, &config, 15);
assert!(state.particles.contains_key(&aid));
}
}