Spaces:
Running
Running
| """ | |
| 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() | |
| 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 | |
| def delay(self): | |
| base_delay = self.length / self.propagation_speed | |
| if self.myelinated: | |
| return base_delay * 0.2 | |
| return base_delay | |
| 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) |