PerceptionLabPortable / app /nodes /GrowingNeuralArchitectureNode.py
Aluode's picture
Upload folder using huggingface_hub
3bb804c verified
"""
Growing Neural Architecture Node v2
====================================
Now with NEUROGENESIS (birth) and APOPTOSIS (death).
Neurons are no longer fixed at initialization. The network:
- Spawns new neurons in active regions (neurogenesis)
- Kills inactive neurons (apoptosis/programmed cell death)
- Maintains homeostatic balance
The population evolves. The fittest neurons survive.
Author: Built for Antti's consciousness crystallography research
"""
import numpy as np
import cv2
from collections import deque
import json
# --- HOST IMPORT BLOCK ---
import __main__
try:
BaseNode = __main__.BaseNode
QtGui = __main__.QtGui
except Exception:
from PyQt6 import QtGui
class BaseNode:
def __init__(self):
self.inputs = {}
self.outputs = {}
# =============================================================================
# CORE STRUCTURES (same as before, included for completeness)
# =============================================================================
class GrowthCone:
"""Growth cone at axon tip - senses and navigates."""
def __init__(self, position, parent_neuron_id):
self.position = np.array(position, dtype=np.float32)
self.parent_id = parent_neuron_id
self.velocity = np.zeros(3, dtype=np.float32)
self.age = 0
self.active = True
self.sensitivity = 1.0
def sense_gradient(self, activity_field, chemical_field, target_positions):
gradient = np.zeros(3, dtype=np.float32)
if activity_field is not None:
pos_int = self.position.astype(int)
pos_int = np.clip(pos_int, 1, np.array(activity_field.shape) - 2)
for axis in range(3):
pos_plus = pos_int.copy()
pos_minus = pos_int.copy()
pos_plus[axis] = min(pos_plus[axis] + 1, activity_field.shape[axis] - 1)
pos_minus[axis] = max(pos_minus[axis] - 1, 0)
grad = activity_field[tuple(pos_plus)] - activity_field[tuple(pos_minus)]
gradient[axis] += grad * 0.5
if chemical_field is not None:
pos_int = self.position.astype(int)
pos_int = np.clip(pos_int, 1, np.array(chemical_field.shape[:3]) - 2)
for axis in range(3):
pos_plus = pos_int.copy()
pos_minus = pos_int.copy()
pos_plus[axis] = min(pos_plus[axis] + 1, chemical_field.shape[axis] - 1)
pos_minus[axis] = max(pos_minus[axis] - 1, 0)
chem_grad = np.mean(chemical_field[tuple(pos_plus)] - chemical_field[tuple(pos_minus)])
gradient[axis] += chem_grad * 0.3
if target_positions and len(target_positions) > 0:
targets = np.array(target_positions)
distances = np.linalg.norm(targets - self.position, axis=1)
for target, dist in zip(targets, distances):
if 1.0 < dist < 20.0:
direction = (target - self.position) / (dist + 0.1)
attraction = 1.0 / (dist * dist + 1.0)
gradient += direction * attraction * 2.0
gradient += np.random.randn(3) * 0.1
norm = np.linalg.norm(gradient)
if norm > 0.01:
gradient = gradient / norm
return gradient * self.sensitivity
def step(self, gradient, growth_rate=0.5):
self.velocity = 0.7 * self.velocity + 0.3 * gradient
self.position = self.position + self.velocity * growth_rate
self.age += 1
self.sensitivity *= 0.999
class Axon:
"""Growing axon with path, length, delay."""
def __init__(self, soma_position, neuron_id):
self.neuron_id = neuron_id
self.path = [np.array(soma_position, dtype=np.float32)]
self.growth_cone = GrowthCone(soma_position, neuron_id)
self.synapses = []
self.myelinated = False
self.propagation_speed = 1.0
self.active_signals = deque()
@property
def length(self):
total = 0.0
for i in range(len(self.path) - 1):
total += np.linalg.norm(self.path[i+1] - self.path[i])
return total
@property
def delay(self):
base_delay = self.length / self.propagation_speed
if self.myelinated:
return base_delay * 0.2
return base_delay
@property
def tip(self):
return self.path[-1] if self.path else None
def grow(self, activity_field, chemical_field, target_positions, growth_rate=0.5):
if not self.growth_cone.active:
return
gradient = self.growth_cone.sense_gradient(
activity_field, chemical_field, target_positions
)
self.growth_cone.step(gradient, growth_rate)
self.path.append(self.growth_cone.position.copy())
if len(self.path) > 500:
self.path = self.path[-500:]
def send_spike(self, strength, current_time):
arrival_time = current_time + self.delay
self.active_signals.append((strength, arrival_time))
def get_output(self, current_time):
total = 0.0
while self.active_signals and self.active_signals[0][1] <= current_time:
strength, _ = self.active_signals.popleft()
total += strength
return total
def form_synapse(self, target_id, initial_strength=0.5):
self.synapses.append([target_id, initial_strength])
self.growth_cone.active = False
def myelinate(self):
self.myelinated = True
self.propagation_speed = 5.0
class Dendrite:
"""Dendritic branch for receiving input."""
def __init__(self, soma_position, neuron_id, branch_id=0):
self.neuron_id = neuron_id
self.branch_id = branch_id
self.root = np.array(soma_position, dtype=np.float32)
self.tip = self.root.copy()
self.path = [self.root.copy()]
self.receptive_radius = 2.0
self.input_buffer = 0.0
def grow_toward(self, axon_tips, growth_rate=0.3):
if not axon_tips:
return
tips = np.array(axon_tips)
distances = np.linalg.norm(tips - self.tip, axis=1)
mask = distances < 15.0
if not np.any(mask):
direction = np.random.randn(3) * 0.5
else:
nearest_idx = np.argmin(distances[mask])
nearest = tips[mask][nearest_idx]
direction = nearest - self.tip
direction = direction / (np.linalg.norm(direction) + 0.01)
self.tip = self.tip + direction * growth_rate
self.path.append(self.tip.copy())
if len(self.path) > 100:
self.path = self.path[-100:]
def receive(self, signal):
self.input_buffer += signal
def drain(self):
val = self.input_buffer
self.input_buffer *= 0.9
return val
class GrowingNeuron:
"""Neuron with soma, axon, dendrites, and Izhikevich dynamics."""
def __init__(self, neuron_id, position, neuron_type='regular'):
self.id = neuron_id
self.soma = np.array(position, dtype=np.float32)
self.neuron_type = neuron_type
self.axon = Axon(position, neuron_id)
self.dendrites = [Dendrite(position, neuron_id, i) for i in range(3)]
# Izhikevich parameters
if neuron_type == 'fast':
self.a, self.b, self.c, self.d = 0.1, 0.2, -65.0, 2.0
elif neuron_type == 'burst':
self.a, self.b, self.c, self.d = 0.02, 0.2, -55.0, 4.0
else:
self.a, self.b, self.c, self.d = 0.02, 0.2, -65.0, 8.0
self.v = -65.0
self.u = self.b * self.v
self.spike = False
self.spike_trace = 0.0
self.activity = 0.0
self.I_ext = 0.0
# Lifetime tracking for apoptosis
self.age = 0
self.total_spikes = 0
self.connections_formed = 0
self.marked_for_death = False
def collect_dendritic_input(self):
total = 0.0
for dendrite in self.dendrites:
total += dendrite.drain()
return total
def step(self, dt=0.5, current_time=0):
self.age += 1
I_syn = self.collect_dendritic_input()
I_axon = self.axon.get_output(current_time) * 10.0
I = self.I_ext + I_syn + I_axon
dv = (0.04 * self.v * self.v + 5.0 * self.v + 140.0 - self.u + I) * dt
du = self.a * (self.b * self.v - self.u) * dt
self.v += dv
self.u += du
self.v = np.clip(self.v, -100, 50)
self.spike = self.v >= 30.0
if self.spike:
self.v = self.c
self.u += self.d
self.axon.send_spike(1.0, current_time)
self.spike_trace = 1.0
self.total_spikes += 1
self.spike_trace *= 0.95
self.activity = 0.9 * self.activity + 0.1 * float(self.spike)
self.I_ext = 0.0
return self.spike
def fitness(self):
"""Calculate neuron fitness for survival decisions."""
# Fitness based on: activity, connections, age
activity_score = self.activity * 10.0
connection_score = len(self.axon.synapses) * 2.0
spike_rate = self.total_spikes / max(1, self.age) * 1000.0
return activity_score + connection_score + spike_rate
class AxonBundle:
"""Multiple axons for ephaptic coupling."""
def __init__(self):
self.axons = []
self.z_depths = {}
self.coupling_strength = 0.1
def add_axon(self, axon, z_depth):
self.axons.append(axon)
self.z_depths[axon.neuron_id] = z_depth
def remove_axon(self, neuron_id):
"""Remove an axon when neuron dies."""
self.axons = [a for a in self.axons if a.neuron_id != neuron_id]
if neuron_id in self.z_depths:
del self.z_depths[neuron_id]
def get_ephaptic_coupling(self, position, z_depth, radius=3.0):
coupling = 0.0
for axon in self.axons:
if axon.neuron_id in self.z_depths:
axon_z = self.z_depths[axon.neuron_id]
z_dist = abs(z_depth - axon_z)
if z_dist > radius:
continue
for point in axon.path[-50:]:
xy_dist = np.linalg.norm(position[:2] - point[:2])
if xy_dist < radius:
dist = np.sqrt(xy_dist**2 + z_dist**2)
coupling += self.coupling_strength / (dist + 0.1)
return coupling
# =============================================================================
# MAIN NODE WITH NEUROGENESIS AND APOPTOSIS
# =============================================================================
class GrowingNeuralArchitectureNode(BaseNode):
"""
Self-organizing neural tissue with:
- Axonal/dendritic growth
- Synapse formation and pruning
- NEUROGENESIS: New neurons born in active regions
- APOPTOSIS: Inactive neurons die
The population evolves. The fittest survive.
"""
NODE_NAME = "Growing Neural Net v2"
NODE_CATEGORY = "Neural"
NODE_COLOR = QtGui.QColor(50, 150, 100) if QtGui else None
def __init__(self):
super().__init__()
self.inputs = {
'image_in': 'image',
'signal_in': 'signal',
'growth_drive': 'signal',
'pruning_signal': 'signal',
'birth_rate': 'signal',
'death_rate': 'signal',
'reset': 'signal'
}
self.outputs = {
'structure_view': 'image',
'activity_view': 'image',
'axon_view': 'image',
'output_signal': 'signal',
'total_synapses': 'signal',
'total_length': 'signal',
'mean_delay': 'signal',
'layer_count': 'signal',
'neuron_count': 'signal',
'births': 'signal',
'deaths': 'signal'
}
# Configuration
self.space_size = 64
self.initial_neurons = 120
self.min_neurons = 50
self.max_neurons = 500
self.growth_rate = 0.5
self.prune_threshold = 0.1
self.myelination_threshold = 0.8
# Neurogenesis/Apoptosis parameters
self.birth_rate = 0.02 # Probability of birth per step (in active regions)
self.death_rate = 0.01 # Probability of death for unfit neurons
self.min_age_for_death = 500 # Neurons must live this long before dying
self.fitness_threshold = 0.5 # Below this fitness, risk death
# Tracking
self.next_neuron_id = 0
self.total_births = 0
self.total_deaths = 0
self.recent_births = 0
self.recent_deaths = 0
# Initialize
self._init_substrate()
# Fields
self.activity_field = np.zeros((self.space_size,)*3, dtype=np.float32)
self.chemical_field = np.zeros((self.space_size,)*3 + (3,), dtype=np.float32)
self._init_chemical_gradients()
# Bundles
self.bundles = AxonBundle()
for neuron in self.neurons:
self.bundles.add_axon(neuron.axon, neuron.soma[2])
# Simulation
self.step_count = 0
self.current_time = 0.0
# Display
self.display_array = None
self.activity_display = None
self.axon_display = None
# Statistics
self.total_synapses = 0
self.emergent_layers = []
def _init_substrate(self):
"""Initialize starting neurons."""
self.neurons = []
self.neuron_map = {} # id -> neuron for fast lookup
for i in range(self.initial_neurons):
layer_bias = i / self.initial_neurons
x = np.random.uniform(5, self.space_size - 5)
y = np.random.uniform(5, self.space_size - 5)
z = layer_bias * (self.space_size - 10) + 5 + np.random.randn() * 3
z = np.clip(z, 0, self.space_size - 1)
if layer_bias < 0.3:
ntype = 'fast'
elif layer_bias > 0.7:
ntype = 'burst'
else:
ntype = 'regular'
neuron = GrowingNeuron(self.next_neuron_id, [x, y, z], ntype)
self.neurons.append(neuron)
self.neuron_map[self.next_neuron_id] = neuron
self.next_neuron_id += 1
def _init_chemical_gradients(self):
"""Initialize chemical guidance gradients."""
for z in range(self.space_size):
self.chemical_field[:, :, z, 0] = z / self.space_size
center = self.space_size / 2
for x in range(self.space_size):
for y in range(self.space_size):
dist = np.sqrt((x - center)**2 + (y - center)**2)
self.chemical_field[x, y, :, 1] = 1.0 - dist / center
self.chemical_field[:, :, :, 2] = np.random.rand(self.space_size, self.space_size, self.space_size) * 0.3
def _read_input(self, name, default=None):
fn = getattr(self, "get_blended_input", None)
if callable(fn):
try:
val = fn(name, "mean")
return val if val is not None else default
except:
return default
return default
def _read_image_input(self, name):
fn = getattr(self, "get_blended_input", None)
if callable(fn):
try:
val = fn(name, "first")
if val is None:
return None
if hasattr(val, 'shape') and hasattr(val, 'dtype'):
return val
except:
pass
return None
# =========================================================================
# NEUROGENESIS - Birth of new neurons
# =========================================================================
def _neurogenesis_step(self, birth_rate):
"""Spawn new neurons in active regions."""
if len(self.neurons) >= self.max_neurons:
return
self.recent_births = 0
# Find highly active regions
activity_threshold = np.percentile(self.activity_field, 90)
active_positions = np.where(self.activity_field > activity_threshold)
if len(active_positions[0]) == 0:
return
# Probability of birth based on activity level
n_attempts = int(birth_rate * 10) + 1
for _ in range(n_attempts):
if len(self.neurons) >= self.max_neurons:
break
if np.random.rand() > birth_rate:
continue
# Pick a random active location
idx = np.random.randint(len(active_positions[0]))
x = active_positions[0][idx] + np.random.randn() * 2
y = active_positions[1][idx] + np.random.randn() * 2
z = active_positions[2][idx] + np.random.randn() * 2
# Clamp to space
x = np.clip(x, 2, self.space_size - 2)
y = np.clip(y, 2, self.space_size - 2)
z = np.clip(z, 2, self.space_size - 2)
# Determine type based on z position
z_norm = z / self.space_size
if z_norm < 0.3:
ntype = 'fast'
elif z_norm > 0.7:
ntype = 'burst'
else:
ntype = 'regular'
# Birth!
neuron = GrowingNeuron(self.next_neuron_id, [x, y, z], ntype)
self.neurons.append(neuron)
self.neuron_map[self.next_neuron_id] = neuron
self.bundles.add_axon(neuron.axon, z)
self.next_neuron_id += 1
self.total_births += 1
self.recent_births += 1
# =========================================================================
# APOPTOSIS - Programmed cell death
# =========================================================================
def _apoptosis_step(self, death_rate):
"""Kill unfit neurons."""
if len(self.neurons) <= self.min_neurons:
return
self.recent_deaths = 0
# Calculate fitness for all neurons
fitness_scores = [(n, n.fitness()) for n in self.neurons]
# Find fitness threshold (bottom 20%)
all_fitness = [f for _, f in fitness_scores]
if not all_fitness:
return
fitness_cutoff = np.percentile(all_fitness, 20)
neurons_to_remove = []
for neuron, fitness in fitness_scores:
# Skip young neurons
if neuron.age < self.min_age_for_death:
continue
# Skip if above fitness threshold
if fitness > fitness_cutoff:
continue
# Probability of death
if np.random.rand() < death_rate:
neurons_to_remove.append(neuron)
# Actually remove neurons
for neuron in neurons_to_remove:
if len(self.neurons) <= self.min_neurons:
break
self._kill_neuron(neuron)
def _kill_neuron(self, neuron):
"""Remove a neuron and clean up its connections."""
neuron_id = neuron.id
# Remove from lists
if neuron in self.neurons:
self.neurons.remove(neuron)
if neuron_id in self.neuron_map:
del self.neuron_map[neuron_id]
# Remove from bundle
self.bundles.remove_axon(neuron_id)
# Clean up synapses pointing to this neuron
for other in self.neurons:
surviving_synapses = [
(tid, strength) for tid, strength in other.axon.synapses
if tid != neuron_id
]
removed = len(other.axon.synapses) - len(surviving_synapses)
self.total_synapses -= removed
other.axon.synapses = surviving_synapses
self.total_deaths += 1
self.recent_deaths += 1
# =========================================================================
# MAIN STEP
# =========================================================================
def step(self):
self.step_count += 1
self.current_time += 1.0
# Read inputs
growth = self._read_input('growth_drive', self.growth_rate)
prune = self._read_input('pruning_signal', self.prune_threshold)
birth = self._read_input('birth_rate', self.birth_rate)
death = self._read_input('death_rate', self.death_rate)
image = self._read_image_input('image_in')
signal = self._read_input('signal_in', 0.0)
# Apply inputs
if image is not None:
self._apply_image_input(image)
if signal:
input_neurons = [n for n in self.neurons if n.soma[2] < 15][:20]
for neuron in input_neurons:
neuron.I_ext += float(signal) * 10.0
# Update activity field
self._update_activity_field()
# Growth
if self.step_count % 5 == 0:
self._growth_step(growth)
# Neural dynamics
self._dynamics_step()
# Synapse formation
if self.step_count % 10 == 0:
self._synapse_formation_step()
# Pruning
if self.step_count % 50 == 0:
self._pruning_step(prune)
# NEUROGENESIS - birth new neurons
if self.step_count % 100 == 0:
self._neurogenesis_step(birth)
# APOPTOSIS - kill unfit neurons
if self.step_count % 150 == 0:
self._apoptosis_step(death)
# Myelination
if self.step_count % 100 == 0:
self._myelination_step()
# Detect layers
if self.step_count % 200 == 0:
self._detect_layers()
# Display
if self.step_count % 4 == 0:
self._update_display()
def _apply_image_input(self, image):
if len(image.shape) == 3:
gray = cv2.cvtColor(image.astype(np.uint8), cv2.COLOR_RGB2GRAY)
else:
gray = image
gray = cv2.resize(gray.astype(np.float32), (self.space_size, self.space_size))
gray = gray / 255.0
for neuron in self.neurons:
if neuron.soma[2] < 15:
x, y = int(neuron.soma[0]), int(neuron.soma[1])
x, y = np.clip(x, 0, self.space_size-1), np.clip(y, 0, self.space_size-1)
neuron.I_ext += gray[y, x] * 20.0
def _update_activity_field(self):
self.activity_field *= 0.95
for neuron in self.neurons:
if neuron.spike:
pos = neuron.soma.astype(int)
pos = np.clip(pos, 0, self.space_size - 1)
for dx in range(-2, 3):
for dy in range(-2, 3):
for dz in range(-2, 3):
px = np.clip(pos[0] + dx, 0, self.space_size - 1)
py = np.clip(pos[1] + dy, 0, self.space_size - 1)
pz = np.clip(pos[2] + dz, 0, self.space_size - 1)
dist = np.sqrt(dx*dx + dy*dy + dz*dz)
self.activity_field[px, py, pz] += np.exp(-dist) * 0.5
def _growth_step(self, growth_rate):
dendrite_tips = []
for neuron in self.neurons:
for dendrite in neuron.dendrites:
dendrite_tips.append(dendrite.tip)
axon_tips = []
for neuron in self.neurons:
if neuron.axon.growth_cone.active:
axon_tips.append(neuron.axon.tip)
for neuron in self.neurons:
neuron.axon.grow(
self.activity_field,
self.chemical_field,
dendrite_tips,
growth_rate
)
for neuron in self.neurons:
for dendrite in neuron.dendrites:
dendrite.grow_toward(axon_tips, growth_rate * 0.5)
def _dynamics_step(self):
for neuron in self.neurons:
neuron.step(dt=0.5, current_time=self.current_time)
for neuron in self.neurons:
if neuron.spike:
for target_id, strength in neuron.axon.synapses:
if target_id in self.neuron_map:
target = self.neuron_map[target_id]
dendrite = np.random.choice(target.dendrites)
dendrite.receive(strength * 5.0)
def _synapse_formation_step(self):
for neuron in self.neurons:
if not neuron.axon.growth_cone.active:
continue
axon_tip = neuron.axon.tip
for target in self.neurons:
if target.id == neuron.id:
continue
for dendrite in target.dendrites:
dist = np.linalg.norm(axon_tip - dendrite.tip)
if dist < dendrite.receptive_radius:
correlation = neuron.spike_trace * target.spike_trace
if correlation > 0.1 or np.random.rand() < 0.01:
neuron.axon.form_synapse(target.id, 0.5)
neuron.connections_formed += 1
self.total_synapses += 1
break
def _pruning_step(self, threshold):
for neuron in self.neurons:
surviving = []
for target_id, strength in neuron.axon.synapses:
if target_id in self.neuron_map:
if self.neuron_map[target_id].activity < 0.01:
strength *= 0.95
if strength > threshold:
surviving.append([target_id, strength])
else:
self.total_synapses -= 1
neuron.axon.synapses = surviving
def _myelination_step(self):
for neuron in self.neurons:
if neuron.axon.myelinated:
continue
total_strength = sum(s for _, s in neuron.axon.synapses)
if total_strength > self.myelination_threshold and neuron.activity > 0.1:
neuron.axon.myelinate()
def _detect_layers(self):
if not self.neurons:
return
z_positions = [n.soma[2] for n in self.neurons]
hist, bins = np.histogram(z_positions, bins=10)
self.emergent_layers = []
for i in range(1, len(hist) - 1):
if hist[i] > hist[i-1] and hist[i] > hist[i+1]:
layer_z = (bins[i] + bins[i+1]) / 2
self.emergent_layers.append(layer_z)
def _update_display(self):
size = 400
# Structure view
img = np.zeros((size, size, 3), dtype=np.uint8)
scale = size / self.space_size
for neuron in self.neurons:
path = neuron.axon.path
if len(path) < 2:
continue
for i in range(len(path) - 1):
p1 = path[i]
p2 = path[i + 1]
x1, y1 = int(p1[0] * scale), int(p1[1] * scale)
x2, y2 = int(p2[0] * scale), int(p2[1] * scale)
z_norm = p1[2] / self.space_size
color = (
int(50 + 150 * z_norm),
int(200 * (1 - z_norm)),
int(50 + 200 * z_norm)
)
if neuron.axon.myelinated:
color = tuple(min(255, c + 50) for c in color)
cv2.line(img, (x1, y1), (x2, y2), color, 1)
for neuron in self.neurons:
x, y = int(neuron.soma[0] * scale), int(neuron.soma[1] * scale)
z_norm = neuron.soma[2] / self.space_size
radius = 2 + int(neuron.activity * 5)
if z_norm < 0.3:
color = (255, 255, 0)
elif z_norm > 0.7:
color = (255, 0, 255)
else:
color = (0, 255, 0)
cv2.circle(img, (x, y), radius, color, -1)
# Info
cv2.putText(img, f"GROWING NEURAL NET", (10, 25),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 1)
cv2.putText(img, f"Step: {self.step_count}", (10, 45),
cv2.FONT_HERSHEY_SIMPLEX, 0.4, (200, 200, 200), 1)
cv2.putText(img, f"Neurons: {len(self.neurons)}", (10, 65),
cv2.FONT_HERSHEY_SIMPLEX, 0.4, (100, 255, 255), 1)
cv2.putText(img, f"Synapses: {self.total_synapses}", (10, 85),
cv2.FONT_HERSHEY_SIMPLEX, 0.4, (100, 255, 100), 1)
cv2.putText(img, f"Layers: {len(self.emergent_layers)}", (10, 105),
cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 200, 100), 1)
total_length = sum(n.axon.length for n in self.neurons)
mean_delay = np.mean([n.axon.delay for n in self.neurons]) if self.neurons else 0
myelinated = sum(1 for n in self.neurons if n.axon.myelinated)
cv2.putText(img, f"Total length: {total_length:.0f}", (10, 125),
cv2.FONT_HERSHEY_SIMPLEX, 0.35, (150, 150, 150), 1)
cv2.putText(img, f"Mean delay: {mean_delay:.1f}", (10, 145),
cv2.FONT_HERSHEY_SIMPLEX, 0.35, (150, 150, 150), 1)
cv2.putText(img, f"Myelinated: {myelinated}", (10, 165),
cv2.FONT_HERSHEY_SIMPLEX, 0.35, (150, 150, 150), 1)
# Birth/death stats
cv2.putText(img, f"Births: {self.total_births} (+{self.recent_births})", (10, 185),
cv2.FONT_HERSHEY_SIMPLEX, 0.35, (100, 255, 100), 1)
cv2.putText(img, f"Deaths: {self.total_deaths} (+{self.recent_deaths})", (10, 205),
cv2.FONT_HERSHEY_SIMPLEX, 0.35, (255, 100, 100), 1)
self.display_array = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
# Activity view
activity_2d = np.max(self.activity_field, axis=2)
activity_norm = np.clip(activity_2d / (activity_2d.max() + 0.01), 0, 1)
activity_img = (activity_norm * 255).astype(np.uint8)
activity_img = cv2.resize(activity_img, (size, size))
activity_img = cv2.applyColorMap(activity_img, cv2.COLORMAP_INFERNO)
self.activity_display = cv2.cvtColor(activity_img, cv2.COLOR_BGR2RGB)
# Axon view (X-Z side)
axon_img = np.zeros((size, size, 3), dtype=np.uint8)
for neuron in self.neurons:
path = neuron.axon.path
for i in range(len(path) - 1):
x1 = int(path[i][0] * scale)
z1 = int(path[i][2] * scale)
x2 = int(path[i+1][0] * scale)
z2 = int(path[i+1][2] * scale)
color = (100, 200, 100) if not neuron.axon.myelinated else (200, 255, 200)
cv2.line(axon_img, (x1, z1), (x2, z2), color, 1)
for layer_z in self.emergent_layers:
y = int(layer_z * scale)
cv2.line(axon_img, (0, y), (size, y), (100, 100, 255), 1)
cv2.putText(axon_img, "X-Z SIDE VIEW", (10, 25),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1)
self.axon_display = cv2.cvtColor(axon_img, cv2.COLOR_BGR2RGB)
def get_output(self, port_name):
if port_name == 'structure_view':
return self.display_array
elif port_name == 'activity_view':
return self.activity_display
elif port_name == 'axon_view':
return self.axon_display
elif port_name == 'output_signal':
output_neurons = [n for n in self.neurons if n.soma[2] > self.space_size * 0.7]
if output_neurons:
return float(np.mean([n.activity for n in output_neurons]))
return 0.0
elif port_name == 'total_synapses':
return float(self.total_synapses)
elif port_name == 'total_length':
return float(sum(n.axon.length for n in self.neurons))
elif port_name == 'mean_delay':
return float(np.mean([n.axon.delay for n in self.neurons])) if self.neurons else 0.0
elif port_name == 'layer_count':
return float(len(self.emergent_layers))
elif port_name == 'neuron_count':
return float(len(self.neurons))
elif port_name == 'births':
return float(self.recent_births)
elif port_name == 'deaths':
return float(self.recent_deaths)
return None
def get_display_image(self):
if self.display_array is not None and QtGui:
h, w = self.display_array.shape[:2]
return QtGui.QImage(self.display_array.data, w, h, w * 3,
QtGui.QImage.Format.Format_RGB888).copy()
return None
def get_config_options(self):
return [
("Initial Neurons", "initial_neurons", self.initial_neurons, None),
("Min Neurons", "min_neurons", self.min_neurons, None),
("Max Neurons", "max_neurons", self.max_neurons, None),
("Space Size", "space_size", self.space_size, None),
("Growth Rate", "growth_rate", self.growth_rate, None),
("Prune Threshold", "prune_threshold", self.prune_threshold, None),
("Birth Rate", "birth_rate", self.birth_rate, None),
("Death Rate", "death_rate", self.death_rate, None),
("Min Age for Death", "min_age_for_death", self.min_age_for_death, None),
]
def set_config_options(self, options):
if isinstance(options, dict):
for key, value in options.items():
if hasattr(self, key):
setattr(self, key, value)