""" 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)