use neuropool::{
SpatialNeuron, SpatialSynapse, SpatialRuntime, SpatialRuntimeConfig, Axon, Nuclei,
WiringConfig, wire_by_proximity,
};
use crate::body::{Environment, WormBody};
use crate::coupling::CouplingState;
use crate::disc::{
self, DifferentiationState, DiscProgram,
accumulate_spatial_pressure, accumulate_activity_pressure, differentiate,
};
use crate::phase::{DevelopmentalPhase, PhaseConfig, PhaseController};
#[derive(Clone, Debug)]
pub struct WormConfig {
pub neuron_count: usize,
pub phase: PhaseConfig,
pub runtime: SpatialRuntimeConfig,
pub frame_interval_us: u64,
pub wiring: WiringConfig,
pub cache_rebuild_interval: u64,
pub disc_interval: u64,
pub disc_activity_window_us: u64,
}
impl Default for WormConfig {
fn default() -> Self {
Self {
neuron_count: 302,
phase: PhaseConfig::default(),
runtime: {
let mut rt = SpatialRuntimeConfig::default();
rt.pruning_interval = 10; rt.hard_prune_interval = 15; rt
},
frame_interval_us: 10_000, wiring: WiringConfig {
max_distance: 3.0, max_fanout: 8, max_fanin: 20,
default_magnitude: 50, },
cache_rebuild_interval: 50,
disc_interval: 10,
disc_activity_window_us: 50_000,
}
}
}
#[derive(Clone, Debug)]
pub struct SimDiagnostics {
pub phase: DevelopmentalPhase,
pub phase_frame: u64,
pub total_frame: u64,
pub sensory_count: usize,
pub motor_count: usize,
pub inter_count: usize,
pub differentiated_count: usize,
pub total_spikes: u64,
pub distance_to_food: f32,
pub body_center: [f32; 3],
pub energy: f32,
pub distress: f32,
}
pub struct WormSim {
pub body: WormBody,
pub brain: SpatialRuntime,
pub coupling: CouplingState,
pub discs: Vec<DiscProgram>,
pub diff_state: DifferentiationState,
pub phase: PhaseController,
pub config: WormConfig,
prev_energy: f32,
delta_ema: f32,
}
impl WormSim {
pub fn new(config: WormConfig) -> Self {
let environment = Environment::default();
let body = WormBody::new(environment);
let neurons = scatter_neurons(config.neuron_count);
let mut synapses = initial_wiring(&neurons, &config.wiring);
wire_commissure(&neurons, &mut synapses);
synapses.rebuild_index(neurons.len());
let brain = SpatialRuntime::new(neurons, synapses, config.runtime.clone());
let mut coupling = CouplingState::new();
coupling.rebuild_caches(&brain.cascade.neurons);
let discs = disc::elegans_discs();
let diff_state = DifferentiationState::new(config.neuron_count, discs.len());
let phase = PhaseController::new(config.phase.clone());
let prev_energy = body.metabolism.energy;
Self {
body,
brain,
coupling,
discs,
diff_state,
phase,
config,
prev_energy,
delta_ema: 0.0,
}
}
pub fn tick(&mut self) {
let snapshot = self.body.sense();
self.coupling.inject_sensory(&snapshot, &mut self.brain);
self.brain.step(self.config.frame_interval_us);
let cmd = self.coupling.read_motor(&self.brain);
self.body.actuate(&cmd);
self.body.physics_step();
self.coupling.adapt();
self.body.metabolic_tick();
{
let energy = self.body.metabolism.energy;
let delta = energy - self.prev_energy;
self.prev_energy = energy;
self.delta_ema = 0.9 * self.delta_ema + 0.1 * delta;
let base = self.config.runtime.mastery_budget_per_cycle;
let valence = if self.phase.current_phase == DevelopmentalPhase::Differentiation {
1.0
} else if self.delta_ema > 0.00005 {
2.0f32
} else if self.delta_ema < -0.00005 {
0.5
} else {
1.0
};
let phase_rate = self.phase.plasticity_rate();
self.brain.set_mastery_budget((base as f32 * valence * phase_rate) as u32);
}
let total_frame = self.phase.total_frame;
if self.phase.discs_accumulate()
&& total_frame % self.config.disc_interval == 0
{
accumulate_spatial_pressure(
&self.brain.cascade.neurons,
&self.discs,
&mut self.diff_state,
);
let active_sensory = self.coupling.active_sensory_channels(&snapshot);
let active_motor = self.coupling.active_motor_channels(&self.brain);
accumulate_activity_pressure(
&self.brain.cascade.neurons,
&self.discs,
&mut self.diff_state,
&active_sensory,
&active_motor,
self.brain.time_us(),
self.config.disc_activity_window_us,
);
}
if self.phase.discs_differentiate()
&& total_frame % self.config.disc_interval == 0
{
let count = differentiate(
&mut self.brain.cascade.neurons,
&self.discs,
&mut self.diff_state,
5, );
if count > 0 {
self.coupling.rebuild_caches(&self.brain.cascade.neurons);
}
}
if total_frame % self.config.cache_rebuild_interval == 0 {
self.coupling.rebuild_caches(&self.brain.cascade.neurons);
}
self.phase.advance();
}
pub fn develop(&mut self) {
let total = self.config.phase.total_frames();
for _ in 0..total {
self.tick();
}
}
pub fn run(&mut self, frames: u64) {
for _ in 0..frames {
self.tick();
}
}
pub fn diagnostics(&self) -> SimDiagnostics {
let (s, m, i) = disc::count_roles(&self.brain.cascade.neurons);
SimDiagnostics {
phase: self.phase.current_phase,
phase_frame: self.phase.phase_frame,
total_frame: self.phase.total_frame,
sensory_count: s,
motor_count: m,
inter_count: i,
differentiated_count: self.diff_state.differentiated_count(),
total_spikes: self.brain.cascade.total_spikes(),
distance_to_food: self.body.distance_to_food(),
body_center: self.body.center_of_mass(),
energy: self.body.metabolism.energy,
distress: self.body.metabolism.distress,
}
}
}
fn scatter_neurons(count: usize) -> Vec<SpatialNeuron> {
let mut neurons = Vec::with_capacity(count);
let mut seed = 12345u64;
const PYRAMIDAL_END: u64 = 40;
const INHIBITORY_END: u64 = 65;
const RELAY_END: u64 = 80;
const GATE_END: u64 = 90;
const OSCILLATOR_END: u64 = 95;
let head_count = count * 40 / 100;
let body_count = count * 45 / 100;
for i in 0..count {
let (x, y_spread, z_spread) = if i < head_count {
seed = xorshift(seed);
let x = -((seed % 1000) as f32 / 1000.0 * 2.5);
(x, 1.5, 1.5)
} else if i < head_count + body_count {
seed = xorshift(seed);
let x = -2.5 - ((seed % 1000) as f32 / 1000.0 * 4.5);
(x, 1.2, 1.2)
} else {
seed = xorshift(seed);
let x = -7.0 - ((seed % 1000) as f32 / 1000.0 * 2.0);
(x, 0.8, 0.8)
};
seed = xorshift(seed);
let y = ((seed % 1000) as f32 / 1000.0 - 0.5) * 2.0 * y_spread;
seed = xorshift(seed);
let z = (seed % 1000) as f32 / 1000.0 * z_spread;
seed = xorshift(seed);
let type_roll = seed % 100;
let base_nuclei = if type_roll < PYRAMIDAL_END {
Nuclei::pyramidal()
} else if type_roll < INHIBITORY_END {
Nuclei::interneuron()
} else if type_roll < RELAY_END {
Nuclei::relay()
} else if type_roll < GATE_END {
Nuclei::gate()
} else if type_roll < OSCILLATOR_END {
seed = xorshift(seed);
let period = 50_000 + (seed % 150_000) as u32;
Nuclei::oscillator(period)
} else {
seed = xorshift(seed);
Nuclei::modulator((seed % 4) as u8)
};
let mut nuclei = base_nuclei;
seed = xorshift(seed);
nuclei.soma_size = jitter_u8(nuclei.soma_size, 15, &mut seed);
nuclei.leak = jitter_u8(nuclei.leak, 15, &mut seed);
nuclei.metabolic_rate = jitter_u8(nuclei.metabolic_rate, 15, &mut seed);
nuclei.axon_affinity = jitter_u8(nuclei.axon_affinity, 15, &mut seed);
nuclei.myelin_affinity = jitter_u8(nuclei.myelin_affinity, 15, &mut seed);
let mut neuron = SpatialNeuron::at([x, y, z], nuclei);
seed = xorshift(seed);
let ax_dx = if i < head_count {
-1.0 + ((seed % 1000) as f32 / 1000.0 - 0.5) * 3.0
} else if i >= head_count + body_count {
1.0 + ((seed % 1000) as f32 / 1000.0 - 0.5) * 2.0
} else {
((seed % 1000) as f32 / 1000.0 - 0.5) * 4.0
};
seed = xorshift(seed);
let ax_dy = ((seed % 1000) as f32 / 1000.0 - 0.5) * 2.0;
seed = xorshift(seed);
let ax_dz = ((seed % 1000) as f32 / 1000.0 - 0.5) * 1.0;
neuron.axon = Axon::toward([
neuron.soma.position[0] + ax_dx,
neuron.soma.position[1] + ax_dy,
neuron.soma.position[2] + ax_dz,
]);
neurons.push(neuron);
}
neurons
}
fn jitter_u8(value: u8, percent: u8, seed: &mut u64) -> u8 {
*seed = xorshift(*seed);
let range = (value as u64 * percent as u64) / 100;
if range == 0 {
return value;
}
let delta = (*seed % (range * 2 + 1)) as i64 - range as i64;
(value as i64 + delta).clamp(1, 255) as u8
}
fn initial_wiring(neurons: &[SpatialNeuron], config: &WiringConfig) -> neuropool::spatial::SpatialSynapseStore {
wire_by_proximity(neurons, config)
}
fn wire_commissure(
neurons: &[SpatialNeuron],
synapses: &mut neuropool::spatial::SpatialSynapseStore,
) {
const MAGNITUDE: u8 = 40; const DELAY_US: u32 = 1_000; const MAX_PAIRS: usize = 20;
let mut left: Vec<(u32, [f32; 3])> = Vec::new();
let mut right: Vec<(u32, [f32; 3])> = Vec::new();
for (idx, n) in neurons.iter().enumerate() {
let pos = n.soma.position;
if pos[0] > -2.5 && pos[1].abs() > 0.1 {
if pos[1] < 0.0 {
left.push((idx as u32, pos));
} else {
right.push((idx as u32, pos));
}
}
}
if left.is_empty() || right.is_empty() {
return;
}
let mut pairs: Vec<(u32, u32, f32)> = Vec::with_capacity(left.len());
for &(l_idx, l_pos) in &left {
let mut best_r = right[0].0;
let mut best_dsq = f32::MAX;
for &(r_idx, r_pos) in &right {
let dx = l_pos[0] - r_pos[0];
let dy = l_pos[1] - r_pos[1];
let dz = l_pos[2] - r_pos[2];
let dsq = dx * dx + dy * dy + dz * dz;
if dsq < best_dsq {
best_dsq = dsq;
best_r = r_idx;
}
}
pairs.push((l_idx, best_r, best_dsq));
}
pairs.sort_by(|a, b| a.2.partial_cmp(&b.2).unwrap_or(std::cmp::Ordering::Equal));
pairs.truncate(MAX_PAIRS);
for (l, r, _) in pairs {
synapses.add(SpatialSynapse::excitatory(l, r, MAGNITUDE, DELAY_US));
synapses.add(SpatialSynapse::excitatory(r, l, MAGNITUDE, DELAY_US));
}
}
fn xorshift(mut state: u64) -> u64 {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
state
}
#[cfg(test)]
mod tests {
use super::*;
use crate::body::SEGMENT_COUNT;
#[test]
fn sim_creation() {
let sim = WormSim::new(WormConfig::default());
assert_eq!(sim.brain.cascade.neurons.len(), 302);
assert_eq!(sim.body.segments.len(), SEGMENT_COUNT);
assert_eq!(sim.phase.current_phase, DevelopmentalPhase::Genesis);
}
#[test]
fn sim_single_tick() {
let mut sim = WormSim::new(WormConfig::default());
sim.tick();
assert_eq!(sim.phase.total_frame, 1);
}
#[test]
fn sim_runs_genesis() {
let config = WormConfig {
phase: PhaseConfig {
genesis_frames: 50,
exposure_frames: 10,
differentiation_frames: 10,
crystallization_frames: 10,
},
..Default::default()
};
let mut sim = WormSim::new(config);
for _ in 0..50 {
sim.tick();
}
assert_eq!(sim.phase.current_phase, DevelopmentalPhase::Exposure);
}
#[test]
fn sim_diagnostics() {
let mut sim = WormSim::new(WormConfig::default());
sim.tick();
let diag = sim.diagnostics();
assert_eq!(diag.sensory_count + diag.motor_count + diag.inter_count, 302);
assert_eq!(diag.inter_count, 302); }
#[test]
fn scatter_produces_correct_count() {
let neurons = scatter_neurons(302);
assert_eq!(neurons.len(), 302);
let excitatory = neurons.iter().filter(|n| n.nuclei.is_excitatory()).count();
let inhibitory = neurons.iter().filter(|n| n.nuclei.is_inhibitory()).count();
assert!(excitatory > 0, "no excitatory neurons");
assert!(inhibitory > 0, "no inhibitory neurons");
let inhib_pct = inhibitory as f32 / 302.0;
assert!(
inhib_pct > 0.15 && inhib_pct < 0.35,
"inhibitory ratio out of range: {inhib_pct:.2} ({inhibitory}/302)",
);
for n in &neurons {
assert!(!n.nuclei.is_sensory(), "no sensory at genesis");
assert!(!n.nuclei.is_motor(), "no motor at genesis");
}
eprintln!("Genomic mix: excitatory={excitatory}, inhibitory={inhibitory}");
}
#[test]
fn scatter_neurons_in_brain_volume() {
let neurons = scatter_neurons(302);
for n in &neurons {
let pos = n.soma.position;
assert!(pos[0] <= 0.0 && pos[0] >= -9.0, "x out of range: {}", pos[0]);
assert!(pos[1] >= -1.5 && pos[1] <= 1.5, "y out of range: {}", pos[1]);
assert!(pos[2] >= 0.0 && pos[2] <= 1.5, "z out of range: {}", pos[2]);
}
}
#[test]
fn initial_wiring_creates_connections() {
let neurons = scatter_neurons(302);
let config = WiringConfig {
max_distance: 4.0,
max_fanout: 12,
max_fanin: 25,
default_magnitude: 80,
};
let store = initial_wiring(&neurons, &config);
assert!(store.len() > 0, "should have some synapses");
}
}