use neuropool::{Interface, Nuclei, Polarity, SpatialNeuron};
#[derive(Clone, Debug)]
pub enum DiscRole {
Sensory { channel: u16, modality: u8 },
Motor { channel: u16, modality: u8 },
CommandInterneuron,
Oscillator { period_us: u32 },
}
#[derive(Clone, Debug)]
pub struct DiscProgram {
pub target_role: DiscRole,
pub anchor: [f32; 3],
pub radius: f32,
pub threshold: f32,
}
impl DiscProgram {
pub fn sensory(channel: u16, modality: u8, anchor: [f32; 3], radius: f32) -> Self {
Self {
target_role: DiscRole::Sensory { channel, modality },
anchor,
radius,
threshold: 35.0, }
}
pub fn motor(channel: u16, modality: u8, anchor: [f32; 3], radius: f32) -> Self {
Self {
target_role: DiscRole::Motor { channel, modality },
anchor,
radius,
threshold: 20.0, }
}
pub fn command_interneuron(anchor: [f32; 3], radius: f32) -> Self {
Self {
target_role: DiscRole::CommandInterneuron,
anchor,
radius,
threshold: 35.0, }
}
pub fn oscillator(period_us: u32, anchor: [f32; 3], radius: f32) -> Self {
Self {
target_role: DiscRole::Oscillator { period_us },
anchor,
radius,
threshold: 30.0, }
}
pub fn spatial_influence(&self, neuron_pos: [f32; 3]) -> f32 {
let dx = neuron_pos[0] - self.anchor[0];
let dy = neuron_pos[1] - self.anchor[1];
let dz = neuron_pos[2] - self.anchor[2];
let dist = (dx * dx + dy * dy + dz * dz).sqrt();
if dist >= self.radius {
0.0
} else {
1.0 - dist / self.radius
}
}
fn target_nuclei(&self) -> Nuclei {
match &self.target_role {
DiscRole::Sensory { channel, modality } => {
let mut n = Nuclei::sensory(*channel, *modality);
n.leak = 200; n
}
DiscRole::Motor { channel, modality } => Nuclei::motor(*channel, *modality),
DiscRole::CommandInterneuron => Nuclei {
soma_size: 220,
axon_affinity: 200,
myelin_affinity: 180,
metabolic_rate: 120,
leak: 90,
refractory: 1500,
oscillation_period: 0,
interface: Interface::none(),
polarity: Polarity::Positive,
},
DiscRole::Oscillator { period_us } => Nuclei::oscillator(*period_us),
}
}
}
pub struct DifferentiationState {
pressure: Vec<Vec<f32>>,
pub differentiated: Vec<bool>,
pub committed_disc: Vec<Option<usize>>,
}
impl DifferentiationState {
pub fn new(neuron_count: usize, disc_count: usize) -> Self {
Self {
pressure: vec![vec![0.0; disc_count]; neuron_count],
differentiated: vec![false; neuron_count],
committed_disc: vec![None; neuron_count],
}
}
pub fn pressure(&self, neuron_idx: usize, disc_idx: usize) -> f32 {
self.pressure[neuron_idx][disc_idx]
}
pub fn accumulate(&mut self, neuron_idx: usize, disc_idx: usize, amount: f32) {
if self.differentiated[neuron_idx] {
return; }
self.pressure[neuron_idx][disc_idx] += amount;
}
pub fn winning_disc(&self, neuron_idx: usize, discs: &[DiscProgram]) -> Option<usize> {
if self.differentiated[neuron_idx] {
return None; }
let mut best_disc = None;
let mut best_pressure = 0.0f32;
for (disc_idx, disc) in discs.iter().enumerate() {
let p = self.pressure[neuron_idx][disc_idx];
if p >= disc.threshold && p > best_pressure {
best_disc = Some(disc_idx);
best_pressure = p;
}
}
best_disc
}
pub fn differentiated_count(&self) -> usize {
self.differentiated.iter().filter(|&&d| d).count()
}
pub fn disc_populations(&self, disc_count: usize) -> Vec<usize> {
let mut counts = vec![0; disc_count];
for disc_idx in self.committed_disc.iter().flatten() {
counts[*disc_idx] += 1;
}
counts
}
}
pub fn accumulate_spatial_pressure(
neurons: &[SpatialNeuron],
discs: &[DiscProgram],
state: &mut DifferentiationState,
) {
for (n_idx, neuron) in neurons.iter().enumerate() {
if state.differentiated[n_idx] {
continue;
}
if neuron.nuclei.is_sensory() || neuron.nuclei.is_motor() {
continue;
}
for (d_idx, disc) in discs.iter().enumerate() {
let influence = disc.spatial_influence(neuron.soma.position);
if influence > 0.0 {
state.accumulate(n_idx, d_idx, influence * 1.5);
}
}
}
}
pub fn accumulate_activity_pressure(
neurons: &[SpatialNeuron],
discs: &[DiscProgram],
state: &mut DifferentiationState,
active_sensory_channels: &[u16],
active_motor_channels: &[u16],
current_time_us: u64,
window_us: u64,
) {
for (n_idx, neuron) in neurons.iter().enumerate() {
if state.differentiated[n_idx] {
continue;
}
if neuron.nuclei.is_sensory() || neuron.nuclei.is_motor() {
continue;
}
let fired = neuron.last_spike_us > current_time_us.saturating_sub(window_us)
&& neuron.last_spike_us > 0;
if !fired {
continue;
}
for (d_idx, disc) in discs.iter().enumerate() {
let spatial = disc.spatial_influence(neuron.soma.position);
if spatial <= 0.0 {
continue;
}
let correlation_boost = match &disc.target_role {
DiscRole::Sensory { channel, .. } => {
if active_sensory_channels.contains(channel) { 1.0 } else { 0.0 }
}
DiscRole::Motor { channel, .. } => {
if active_motor_channels.contains(channel) { 1.0 } else { 0.0 }
}
DiscRole::CommandInterneuron => {
if neuron.trace > 10 { 0.5 } else { 0.0 }
}
DiscRole::Oscillator { .. } => {
if neuron.trace > 15 { 0.3 } else { 0.0 }
}
};
if correlation_boost > 0.0 {
state.accumulate(n_idx, d_idx, spatial * correlation_boost);
}
}
}
}
struct RoleCaps {
max_sensory: usize,
max_motor: usize,
max_command: usize,
max_oscillator: usize,
}
impl RoleCaps {
fn for_population(n: usize) -> Self {
Self {
max_sensory: (n * 30 / 100).max(1),
max_motor: (n * 40 / 100).max(1),
max_command: (n * 5 / 100).max(1),
max_oscillator: (n * 3 / 100).max(1),
}
}
}
pub fn differentiate(
neurons: &mut [SpatialNeuron],
discs: &[DiscProgram],
state: &mut DifferentiationState,
max_per_round: usize,
) -> usize {
let caps = RoleCaps::for_population(neurons.len());
let mut sensory_count = 0usize;
let mut motor_count = 0usize;
let mut command_count = 0usize;
let mut oscillator_count = 0usize;
for n in neurons.iter() {
if n.nuclei.is_sensory() { sensory_count += 1; }
else if n.nuclei.is_motor() { motor_count += 1; }
else if n.nuclei.is_oscillator() { oscillator_count += 1; }
}
for disc_idx in state.committed_disc.iter().flatten() {
if matches!(discs[*disc_idx].target_role, DiscRole::CommandInterneuron) {
command_count += 1;
}
}
let mut count = 0;
let limit = if max_per_round == 0 { usize::MAX } else { max_per_round };
for n_idx in 0..neurons.len() {
if count >= limit {
break;
}
if let Some(disc_idx) = state.winning_disc(n_idx, discs) {
let disc = &discs[disc_idx];
let allowed = match &disc.target_role {
DiscRole::Sensory { .. } => sensory_count < caps.max_sensory,
DiscRole::Motor { .. } => motor_count < caps.max_motor,
DiscRole::CommandInterneuron => command_count < caps.max_command,
DiscRole::Oscillator { .. } => oscillator_count < caps.max_oscillator,
};
if !allowed {
continue; }
let target = disc.target_nuclei();
let neuron = &mut neurons[n_idx];
blend_nuclei(&mut neuron.nuclei, &target, 0.5);
neuron.nuclei.interface = target.interface;
state.differentiated[n_idx] = true;
state.committed_disc[n_idx] = Some(disc_idx);
count += 1;
match &disc.target_role {
DiscRole::Sensory { .. } => sensory_count += 1,
DiscRole::Motor { .. } => motor_count += 1,
DiscRole::CommandInterneuron => command_count += 1,
DiscRole::Oscillator { .. } => oscillator_count += 1,
}
}
}
count
}
fn blend_nuclei(current: &mut Nuclei, target: &Nuclei, factor: f32) {
current.soma_size = lerp_u8(current.soma_size, target.soma_size, factor);
current.axon_affinity = lerp_u8(current.axon_affinity, target.axon_affinity, factor);
current.myelin_affinity = lerp_u8(current.myelin_affinity, target.myelin_affinity, factor);
current.metabolic_rate = lerp_u8(current.metabolic_rate, target.metabolic_rate, factor);
current.leak = lerp_u8(current.leak, target.leak, factor);
current.refractory = lerp_u32(current.refractory, target.refractory, factor);
if current.polarity == Polarity::Zero {
current.polarity = target.polarity;
}
if target.oscillation_period > 0 {
current.oscillation_period = target.oscillation_period;
}
}
fn lerp_u8(a: u8, b: u8, t: f32) -> u8 {
let result = a as f32 * (1.0 - t) + b as f32 * t;
result.round().clamp(0.0, 255.0) as u8
}
fn lerp_u32(a: u32, b: u32, t: f32) -> u32 {
let result = a as f32 * (1.0 - t) + b as f32 * t;
result.round().max(0.0) as u32
}
pub fn count_roles(neurons: &[SpatialNeuron]) -> (usize, usize, usize) {
let mut sensory = 0;
let mut motor = 0;
let mut inter = 0;
for n in neurons {
if n.nuclei.is_sensory() {
sensory += 1;
} else if n.nuclei.is_motor() {
motor += 1;
} else {
inter += 1;
}
}
(sensory, motor, inter)
}
pub const MODALITY_MECHANO: u8 = 10;
pub const MODALITY_CHEMO: u8 = 11;
pub const MODALITY_PROPRIO: u8 = 12;
pub const MODALITY_MOTOR: u8 = 13;
pub fn elegans_discs() -> Vec<DiscProgram> {
let mut discs = Vec::new();
let mut channel = 0u16;
for i in 0..4u16 {
let y_offset = (i as f32 - 1.5) * 0.5;
discs.push(DiscProgram::sensory(
channel, MODALITY_CHEMO,
[0.5, y_offset, 1.0],
1.5, ));
channel += 1;
}
for seg in 0..10u16 {
let x = -(seg as f32); for dir in 0..4u16 {
let (y_off, z_off) = match dir {
0 => (0.0, 1.5), 1 => (0.0, -0.5), 2 => (-1.0, 0.5), _ => (1.0, 0.5), };
discs.push(DiscProgram::sensory(
channel, MODALITY_MECHANO,
[x, y_off, z_off],
1.5, ));
channel += 1;
}
}
for seg in 0..10u16 {
let x = -(seg as f32);
discs.push(DiscProgram::sensory(
channel, MODALITY_PROPRIO,
[x, 0.0, 0.5],
1.5, ));
channel += 1;
discs.push(DiscProgram::sensory(
channel, MODALITY_PROPRIO,
[x, 0.0, 0.5],
1.5,
));
channel += 1;
}
let mut motor_channel = 0u16;
for seg in 0..10u16 {
let x = -(seg as f32);
for dir in 0..4u16 {
let (y_off, z_off) = match dir {
0 => (0.0, 1.0), 1 => (0.0, 0.2), 2 => (-0.5, 0.5), _ => (0.5, 0.5), };
discs.push(DiscProgram::motor(
motor_channel, MODALITY_MOTOR,
[x, y_off, z_off],
1.5,
));
motor_channel += 1;
}
}
for i in 0..4 {
let angle = i as f32 * std::f32::consts::PI * 0.5;
discs.push(DiscProgram::command_interneuron(
[0.0, angle.cos() * 0.8, angle.sin() * 0.8 + 0.5],
2.5,
));
}
discs.push(DiscProgram::oscillator(
5_000, [-2.0, 0.0, 0.0],
3.0,
));
discs.push(DiscProgram::oscillator(
5_000,
[-7.0, 0.0, 0.0],
3.0,
));
discs
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn disc_spatial_influence() {
let disc = DiscProgram::sensory(0, 1, [0.0, 0.0, 0.0], 5.0);
assert!((disc.spatial_influence([0.0, 0.0, 0.0]) - 1.0).abs() < 0.001);
assert!((disc.spatial_influence([2.5, 0.0, 0.0]) - 0.5).abs() < 0.001);
assert!((disc.spatial_influence([5.0, 0.0, 0.0])).abs() < 0.001);
assert_eq!(disc.spatial_influence([6.0, 0.0, 0.0]), 0.0);
}
#[test]
fn accumulation_and_differentiation() {
let discs = vec![
DiscProgram::sensory(0, 1, [0.0, 0.0, 0.0], 5.0),
DiscProgram::motor(0, 1, [10.0, 0.0, 0.0], 5.0),
];
let mut neurons = vec![
SpatialNeuron::pyramidal_at([0.5, 0.0, 0.0]), SpatialNeuron::pyramidal_at([9.5, 0.0, 0.0]), SpatialNeuron::pyramidal_at([5.0, 0.0, 0.0]), ];
let mut state = DifferentiationState::new(3, 2);
for _ in 0..200 {
accumulate_spatial_pressure(&neurons, &discs, &mut state);
}
assert!(state.pressure(0, 0) > state.pressure(0, 1));
assert!(state.pressure(1, 1) > state.pressure(1, 0));
assert!(state.pressure(2, 0) < state.pressure(0, 0));
let count = differentiate(&mut neurons, &discs, &mut state, 0);
assert!(count >= 2, "at least 2 neurons should differentiate");
assert!(neurons[0].nuclei.is_sensory(), "near sensory disc should become sensory");
assert!(neurons[1].nuclei.is_motor(), "near motor disc should become motor");
}
#[test]
fn already_differentiated_neurons_skip() {
let discs = vec![
DiscProgram::sensory(0, 1, [0.0, 0.0, 0.0], 5.0),
];
let neurons = vec![
SpatialNeuron::sensory_at([0.0, 0.0, 0.0], 0, 1), ];
let mut state = DifferentiationState::new(1, 1);
accumulate_spatial_pressure(&neurons, &discs, &mut state);
assert_eq!(state.pressure(0, 0), 0.0);
}
#[test]
fn irreversible_differentiation() {
let discs = vec![
DiscProgram::sensory(0, 1, [0.0, 0.0, 0.0], 5.0),
DiscProgram::motor(0, 1, [0.0, 0.0, 0.0], 5.0),
];
let mut neurons = vec![SpatialNeuron::pyramidal_at([0.0, 0.0, 0.0])];
let mut state = DifferentiationState::new(1, 2);
for _ in 0..200 {
accumulate_spatial_pressure(&neurons, &discs, &mut state);
}
differentiate(&mut neurons, &discs, &mut state, 0);
assert!(state.differentiated[0]);
let old_pressure = state.pressure(0, 1);
accumulate_spatial_pressure(&neurons, &discs, &mut state);
assert_eq!(state.pressure(0, 1), old_pressure);
}
#[test]
fn elegans_disc_count() {
let discs = elegans_discs();
assert_eq!(discs.len(), 110);
}
#[test]
fn count_roles_works() {
let neurons = vec![
SpatialNeuron::sensory_at([0.0, 0.0, 0.0], 0, 1),
SpatialNeuron::motor_at([1.0, 0.0, 0.0], 0, 1),
SpatialNeuron::pyramidal_at([2.0, 0.0, 0.0]),
SpatialNeuron::pyramidal_at([3.0, 0.0, 0.0]),
];
let (s, m, i) = count_roles(&neurons);
assert_eq!(s, 1);
assert_eq!(m, 1);
assert_eq!(i, 2);
}
}