use fibertract::{FiberBundle, FiberTractKind, LimbProfile, ReceptorMode};
use fibertract::adapt::{AdaptationConfig, adapt_bundle};
use fibertract::profile::TractSpec;
use neuropool::{Signal, SpatialNeuron, SpatialRuntime};
use crate::body::{
SensorySnapshot, MotorCommand,
SEGMENT_COUNT, CHEMO_CHANNELS, DISTRESS_CHANNELS,
TOUCH_CHANNELS_PER_SEGMENT, PROPRIO_CHANNELS_PER_SEGMENT,
MOTOR_CHANNELS_PER_SEGMENT,
};
#[derive(Clone, Debug)]
pub struct BundleAnchor {
pub name: String,
pub position: [f32; 3],
pub search_radius: f32,
}
#[derive(Clone, Debug)]
pub struct CoupledBundle {
pub bundle: FiberBundle,
pub anchor: BundleAnchor,
pub sensory_neuron_indices: Vec<u32>,
pub motor_neuron_indices: Vec<u32>,
}
pub struct CouplingState {
pub head: CoupledBundle,
pub segments: Vec<CoupledBundle>,
rng_seed: u64,
pub adapt_config: AdaptationConfig,
}
const SENSORY_INJECT_SCALE: f32 = 3.0;
impl CouplingState {
pub fn new() -> Self {
let head = CoupledBundle {
bundle: head_bundle(),
anchor: BundleAnchor {
name: "head".into(),
position: [0.5, 0.0, 0.5],
search_radius: 3.0,
},
sensory_neuron_indices: Vec::new(),
motor_neuron_indices: Vec::new(),
};
let segments = (0..SEGMENT_COUNT)
.map(|i| CoupledBundle {
bundle: segment_bundle(i),
anchor: BundleAnchor {
name: format!("segment_{i}"),
position: [-(i as f32), 0.0, 0.5],
search_radius: 2.5,
},
sensory_neuron_indices: Vec::new(),
motor_neuron_indices: Vec::new(),
})
.collect();
Self {
head,
segments,
rng_seed: 42,
adapt_config: AdaptationConfig::default(),
}
}
pub fn rebuild_caches(&mut self, neurons: &[SpatialNeuron]) {
rebuild_bundle_cache(&mut self.head, neurons);
stabilize_head_channels(&mut self.head, neurons);
for seg in &mut self.segments {
rebuild_bundle_cache(seg, neurons);
}
}
pub fn inject_sensory(
&mut self,
snapshot: &SensorySnapshot,
runtime: &mut SpatialRuntime,
) {
self.rng_seed = self.rng_seed.wrapping_add(1);
let mut head_sensory: Vec<i32> = snapshot.chemosensory.to_vec();
head_sensory.push(snapshot.distress);
inject_sensory_bundle(&mut self.head, &head_sensory, runtime, self.rng_seed);
for (seg_idx, coupled) in self.segments.iter_mut().enumerate() {
let mut seg_sensory = Vec::with_capacity(
TOUCH_CHANNELS_PER_SEGMENT + PROPRIO_CHANNELS_PER_SEGMENT,
);
seg_sensory.extend_from_slice(&snapshot.touch[seg_idx]);
seg_sensory.extend_from_slice(&snapshot.proprioception[seg_idx]);
inject_sensory_bundle(coupled, &seg_sensory, runtime, self.rng_seed.wrapping_add(seg_idx as u64));
}
}
pub fn read_motor(
&mut self,
runtime: &SpatialRuntime,
) -> MotorCommand {
let mut cmd = MotorCommand::default();
for (seg_idx, coupled) in self.segments.iter_mut().enumerate() {
let motor_signals = read_motor_bundle(coupled, runtime, self.rng_seed.wrapping_add(100 + seg_idx as u64));
for (ch, sig) in motor_signals.iter().enumerate() {
let activation = if sig.polarity > 0 {
sig.magnitude as f32 / 255.0
} else {
0.0
};
match ch {
0 => cmd.dorsal[seg_idx] = activation,
1 => cmd.ventral[seg_idx] = activation,
2 => cmd.left[seg_idx] = activation,
3 => cmd.right[seg_idx] = activation,
_ => {}
}
}
}
cmd
}
pub fn active_sensory_channels(&self, snapshot: &SensorySnapshot) -> Vec<u16> {
let mut channels = Vec::new();
let mut ch = 0u16;
for &v in &snapshot.chemosensory {
if v.abs() > 10 { channels.push(ch); }
ch += 1;
}
if snapshot.distress.abs() > 10 { channels.push(ch); }
ch += 1;
for seg_idx in 0..SEGMENT_COUNT {
for &v in &snapshot.touch[seg_idx] {
if v > 10 { channels.push(ch); }
ch += 1;
}
for &v in &snapshot.proprioception[seg_idx] {
if v.abs() > 10 { channels.push(ch); }
ch += 1;
}
}
channels
}
pub fn active_motor_channels(&self, runtime: &SpatialRuntime) -> Vec<u16> {
let motors = runtime.read_motors();
motors.iter().map(|&(ch, _)| ch).collect()
}
pub fn adapt(&mut self) {
adapt_bundle(&mut self.head.bundle, &self.adapt_config);
for seg in &mut self.segments {
adapt_bundle(&mut seg.bundle, &self.adapt_config);
}
}
}
fn rebuild_bundle_cache(coupled: &mut CoupledBundle, neurons: &[SpatialNeuron]) {
let anchor = coupled.anchor.position;
let radius = coupled.anchor.search_radius;
let mut candidates: Vec<(u32, f32)> = neurons
.iter()
.enumerate()
.filter_map(|(idx, n)| {
let d = distance_3d(n.soma.position, anchor);
if d < radius {
Some((idx as u32, d))
} else {
None
}
})
.collect();
candidates.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
let sensory_count: usize = coupled
.bundle
.sensory_tracts()
.map(|t| t.dim)
.sum();
let motor_count: usize = coupled
.bundle
.motor_tracts()
.map(|t| t.dim)
.sum();
coupled.sensory_neuron_indices = assign_neurons_prefer_type(
&candidates, neurons, sensory_count, NeuronRole::Sensory,
);
coupled.motor_neuron_indices = assign_neurons_prefer_type(
&candidates, neurons, motor_count, NeuronRole::Motor,
);
}
#[derive(Clone, Copy)]
enum NeuronRole { Sensory, Motor }
fn assign_neurons_prefer_type(
candidates: &[(u32, f32)],
neurons: &[SpatialNeuron],
count: usize,
role: NeuronRole,
) -> Vec<u32> {
if candidates.is_empty() || count == 0 {
return Vec::new();
}
let mut typed: Vec<u32> = Vec::new();
let mut untyped: Vec<u32> = Vec::new();
for &(idx, _) in candidates {
let n = &neurons[idx as usize];
let matches = match role {
NeuronRole::Sensory => n.nuclei.is_sensory(),
NeuronRole::Motor => n.nuclei.is_motor(),
};
if matches {
typed.push(idx);
} else {
untyped.push(idx);
}
}
let mut indices = Vec::with_capacity(count);
let mut all = typed;
all.extend(untyped);
for i in 0..count {
indices.push(all[i % all.len()]);
}
indices
}
fn stabilize_head_channels(coupled: &mut CoupledBundle, neurons: &[SpatialNeuron]) {
if coupled.sensory_neuron_indices.len() < CHEMO_CHANNELS {
return;
}
let mut chemo: Vec<u32> = coupled.sensory_neuron_indices[..CHEMO_CHANNELS].to_vec();
chemo.sort();
chemo.dedup();
if chemo.len() < CHEMO_CHANNELS {
return; }
let positions: Vec<[f32; 3]> = chemo.iter()
.map(|&idx| neurons[idx as usize].soma.position)
.collect();
let mut assigned = [0u32; 4]; let mut used = [false; 4];
if let Some(i) = (0..chemo.len())
.filter(|i| !used[*i])
.min_by(|&a, &b| positions[a][1].partial_cmp(&positions[b][1]).unwrap())
{
assigned[0] = chemo[i]; used[i] = true;
}
if let Some(i) = (0..chemo.len())
.filter(|i| !used[*i])
.max_by(|&a, &b| positions[a][1].partial_cmp(&positions[b][1]).unwrap())
{
assigned[1] = chemo[i]; used[i] = true;
}
if let Some(i) = (0..chemo.len())
.filter(|i| !used[*i])
.max_by(|&a, &b| positions[a][2].partial_cmp(&positions[b][2]).unwrap())
{
assigned[2] = chemo[i]; used[i] = true;
}
if let Some(i) = (0..chemo.len()).find(|i| !used[*i]) {
assigned[3] = chemo[i];
}
for ch in 0..CHEMO_CHANNELS {
coupled.sensory_neuron_indices[ch] = assigned[ch];
}
}
fn inject_sensory_bundle(
coupled: &mut CoupledBundle,
raw_sensory: &[i32],
runtime: &mut SpatialRuntime,
rng_seed: u64,
) {
let mut tract_idx = 0;
let mut channel_offset = 0;
for tract in coupled.bundle.tracts.iter_mut() {
if !tract.kind.is_afferent() {
continue;
}
let end = (channel_offset + tract.dim).min(raw_sensory.len());
if channel_offset < end {
tract.transmit_sensory(&raw_sensory[channel_offset..end], rng_seed.wrapping_add(tract_idx));
}
for (ch, &shaped_val) in tract.sensory_signals.iter().enumerate() {
let neuron_ch = channel_offset + ch;
if neuron_ch < coupled.sensory_neuron_indices.len() {
let neuron_idx = coupled.sensory_neuron_indices[neuron_ch];
if shaped_val != 0 {
let current = (shaped_val as f32 * SENSORY_INJECT_SCALE).clamp(-32000.0, 32000.0) as i16;
runtime.inject(neuron_idx, current);
}
}
}
channel_offset = end;
tract_idx += 1;
}
}
fn read_motor_bundle(
coupled: &mut CoupledBundle,
runtime: &SpatialRuntime,
rng_seed: u64,
) -> Vec<Signal> {
let mut all_motor_signals = Vec::new();
let mut tract_idx = 0;
for tract in coupled.bundle.tracts.iter_mut() {
if !tract.kind.is_efferent() {
continue;
}
let mut input_signals = vec![Signal::default(); tract.dim];
for (ch, sig) in input_signals.iter_mut().enumerate() {
let neuron_ch = all_motor_signals.len() + ch;
if neuron_ch < coupled.motor_neuron_indices.len() {
let neuron_idx = coupled.motor_neuron_indices[neuron_ch] as usize;
if neuron_idx < runtime.cascade.neurons.len() {
let neuron = &runtime.cascade.neurons[neuron_idx];
if neuron.trace > 0 {
*sig = Signal {
polarity: 1,
magnitude: (neuron.trace as u8).min(255),
};
}
}
}
}
tract.transmit_motor(&input_signals, rng_seed.wrapping_add(tract_idx));
all_motor_signals.extend_from_slice(&tract.motor_signals);
tract_idx += 1;
}
all_motor_signals
}
fn head_bundle() -> FiberBundle {
let profile = LimbProfile {
name: "head".into(),
tracts: vec![
TractSpec {
kind: FiberTractKind::Interoceptive,
dim: CHEMO_CHANNELS,
receptor_mode: Some(ReceptorMode::Tonic),
..TractSpec::new(FiberTractKind::Interoceptive, CHEMO_CHANNELS)
},
TractSpec {
kind: FiberTractKind::Interoceptive,
dim: DISTRESS_CHANNELS,
receptor_mode: Some(ReceptorMode::Tonic),
..TractSpec::new(FiberTractKind::Interoceptive, DISTRESS_CHANNELS)
},
],
};
profile.build()
}
fn segment_bundle(segment_idx: usize) -> FiberBundle {
let name = format!("segment_{segment_idx}");
let profile = LimbProfile {
name,
tracts: vec![
TractSpec {
kind: FiberTractKind::Mechanoreceptive,
dim: TOUCH_CHANNELS_PER_SEGMENT,
receptor_mode: Some(ReceptorMode::Tonic),
..TractSpec::new(FiberTractKind::Mechanoreceptive, TOUCH_CHANNELS_PER_SEGMENT)
},
TractSpec {
kind: FiberTractKind::Proprioceptive,
dim: PROPRIO_CHANNELS_PER_SEGMENT,
receptor_mode: Some(ReceptorMode::Tonic),
..TractSpec::new(FiberTractKind::Proprioceptive, PROPRIO_CHANNELS_PER_SEGMENT)
},
TractSpec::new(FiberTractKind::MotorSkeletal, MOTOR_CHANNELS_PER_SEGMENT),
],
};
profile.build()
}
fn distance_3d(a: [f32; 3], b: [f32; 3]) -> f32 {
let dx = a[0] - b[0];
let dy = a[1] - b[1];
let dz = a[2] - b[2];
(dx * dx + dy * dy + dz * dz).sqrt()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::body::{Environment, WormBody};
#[test]
fn coupling_creation() {
let coupling = CouplingState::new();
assert_eq!(coupling.segments.len(), SEGMENT_COUNT);
}
#[test]
fn head_bundle_has_sensory() {
let bundle = head_bundle();
assert!(bundle.sensory_tracts().count() > 0);
}
#[test]
fn segment_bundle_has_motor_and_sensory() {
let bundle = segment_bundle(0);
assert!(bundle.sensory_tracts().count() > 0);
assert!(bundle.motor_tracts().count() > 0);
}
#[test]
fn all_segment_bundles_consistent() {
for i in 0..SEGMENT_COUNT {
let b = segment_bundle(i);
assert_eq!(b.name, format!("segment_{i}"));
assert!(b.tract_count() >= 3);
}
}
#[test]
fn sensory_snapshot_maps_to_channels() {
let body = WormBody::new(Environment::default());
let snap = body.sense();
let coupling = CouplingState::new();
let active = coupling.active_sensory_channels(&snap);
assert!(!active.is_empty(), "ground contact should produce sensory activity");
}
}