diff --git a/core/renderer.py b/core/renderer.py index 509b9bc..309c680 100644 --- a/core/renderer.py +++ b/core/renderer.py @@ -4,6 +4,7 @@ import pygame import math from config.constants import * +from world.base.brain import CellBrain class Renderer: @@ -238,4 +239,4 @@ class Renderer: screen_x, screen_y = camera.world_to_screen(obj_x, obj_y) size = camera.get_relative_size(width) rect = pygame.Rect(screen_x - size // 2, screen_y - size // 2, size, size) - pygame.draw.rect(self.screen, SELECTION_BLUE, rect, 1) \ No newline at end of file + pygame.draw.rect(self.screen, SELECTION_BLUE, rect, 1) diff --git a/core/simulation_engine.py b/core/simulation_engine.py new file mode 100644 index 0000000..0ac08ef --- /dev/null +++ b/core/simulation_engine.py @@ -0,0 +1,107 @@ +import pygame +import time +import random +import sys + +from world.world import World, Position, Rotation +from world.objects import FoodObject, DefaultCell +from world.simulation_interface import Camera +from config.constants import * +from core.input_handler import InputHandler +from core.renderer import Renderer +from ui.hud import HUD + + +class SimulationEngine: + def __init__(self): + pygame.init() + self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT), vsync=1) + pygame.display.set_caption("Dynamic Abstraction System Testing") + self.clock = pygame.time.Clock() + self.camera = Camera(SCREEN_WIDTH, SCREEN_HEIGHT, RENDER_BUFFER) + + self.last_tick_time = time.perf_counter() + self.last_tps_time = time.perf_counter() + self.tick_counter = 0 + self.actual_tps = 0 + self.total_ticks = 0 + + self.world = self._setup_world() + self.input_handler = InputHandler(self.camera, self.world) + self.renderer = Renderer(self.screen) + self.hud = HUD() + + self.running = True + + def _setup_world(self): + world = World(CELL_SIZE, (CELL_SIZE * GRID_WIDTH, CELL_SIZE * GRID_HEIGHT)) + random.seed(0) + + if FOOD_SPAWNING: + world.add_object(FoodObject(Position(x=random.randint(-100, 100), y=random.randint(-100, 100)))) + + for _ in range(10): + world.add_object(DefaultCell(Position(x=random.randint(-100, 100), y=random.randint(-100, 100)), Rotation(angle=0))) + + return world + + def run(self): + while self.running: + self._handle_frame() + + pygame.quit() + sys.exit() + + def _handle_frame(self): + deltatime = self.clock.get_time() / 1000.0 + tick_interval = 1.0 / self.input_handler.tps + + # Handle events + self.running = self.input_handler.handle_events(pygame.event.get()) + + if not self.input_handler.is_paused: + current_time = time.perf_counter() + while current_time - self.last_tick_time >= tick_interval: + self.last_tick_time += tick_interval + self.tick_counter += 1 + self.total_ticks += 1 + + self.input_handler.update_selected_objects() + self.world.tick_all() + + if current_time - self.last_tps_time >= 1.0: + self.actual_tps = self.tick_counter + self.tick_counter = 0 + self.last_tps_time += 1.0 + else: + self.last_tick_time = time.perf_counter() + self.last_tps_time = time.perf_counter() + + self._update(deltatime) + self._render() + + def _update(self, deltatime): + keys = pygame.key.get_pressed() + self.input_handler.update_camera(keys, deltatime) + + def _render(self): + self.renderer.clear_screen() + self.renderer.draw_grid(self.camera, self.input_handler.show_grid) + self.renderer.render_world(self.world, self.camera) + self.renderer.render_interaction_radius(self.world, self.camera, self.input_handler.selected_objects, self.input_handler.show_interaction_radius) + self.renderer.render_selection_rectangle(self.input_handler.get_selection_rect()) + self.renderer.render_selected_objects_outline(self.input_handler.selected_objects, self.camera) + + self.hud.render_mouse_position(self.screen, self.camera) + self.hud.render_fps(self.screen, self.clock) + self.hud.render_tps(self.screen, self.actual_tps) + self.hud.render_tick_count(self.screen, self.total_ticks) + self.hud.render_selected_objects_info(self.screen, self.input_handler.selected_objects) + self.hud.render_legend(self.screen, self.input_handler.show_legend) + self.hud.render_pause_indicator(self.screen, self.input_handler.is_paused) + + if self.input_handler.selected_objects: + self.hud.render_neural_network_visualization(self.screen, self.input_handler.selected_objects[0]) + + pygame.display.flip() + self.clock.tick(MAX_FPS) diff --git a/main.py b/main.py index 7e8fe6a..91f7cd6 100644 --- a/main.py +++ b/main.py @@ -1,117 +1,5 @@ -import math - -import pygame -import time -import sys -import random - -from world.world import World, Position, Rotation -from world.objects import FoodObject, TestVelocityObject, DefaultCell -from world.simulation_interface import Camera - -from config.constants import * - -from core.input_handler import InputHandler -from core.renderer import Renderer - -from ui.hud import HUD - -# Initialize Pygame -pygame.init() - - -def setup(world: World): - if FOOD_SPAWNING: - world.add_object(FoodObject(Position(x=random.randint(-100, 100), y=random.randint(-100, 100)))) - - for i in range(100): - world.add_object(DefaultCell(Position(x=random.randint(-100, 100),y=random.randint(-100, 100)), Rotation(angle=0))) - - return world - - -def main(): - screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT), vsync=1) - pygame.display.set_caption("Dynamic Abstraction System Testing") - clock = pygame.time.Clock() - camera = Camera(SCREEN_WIDTH, SCREEN_HEIGHT, RENDER_BUFFER) - - last_tick_time = time.perf_counter() # Tracks the last tick time - last_tps_time = time.perf_counter() # Tracks the last TPS calculation time - tick_counter = 0 # Counts ticks executed - actual_tps = 0 # Stores the calculated TPS - total_ticks = 0 # Total ticks executed - - # Initialize world - world = World(CELL_SIZE, (CELL_SIZE * GRID_WIDTH, CELL_SIZE * GRID_HEIGHT)) - - # sets seed to 67 >_< - random.seed(0) - - world = setup(world) - - input_handler = InputHandler(camera, world) - renderer = Renderer(screen) - hud = HUD() - - running = True - while running: - deltatime = clock.get_time() / 1000.0 # Convert milliseconds to seconds - tick_interval = 1.0 / input_handler.tps # Time per tick - - # Handle events - running = input_handler.handle_events(pygame.event.get()) - - if not input_handler.is_paused: - # Tick logic (runs every tick interval) - current_time = time.perf_counter() - while current_time - last_tick_time >= tick_interval: - last_tick_time += tick_interval - tick_counter += 1 - total_ticks += 1 - - # ensure selected objects are still valid or have not changed position, if so, reselect them - input_handler.update_selected_objects() - - world.tick_all() - - # Calculate TPS every second - if current_time - last_tps_time >= 1.0: - actual_tps = tick_counter - tick_counter = 0 - last_tps_time += 1.0 - else: - last_tick_time = time.perf_counter() - last_tps_time = time.perf_counter() - - # Get pressed keys for smooth movement - keys = pygame.key.get_pressed() - input_handler.update_camera(keys, deltatime) - - renderer.clear_screen() - renderer.draw_grid(camera, input_handler.show_grid) - renderer.render_world(world, camera) - - renderer.render_interaction_radius(world, camera, input_handler.selected_objects, input_handler.show_interaction_radius) - - renderer.render_selection_rectangle(input_handler.get_selection_rect()) - renderer.render_selected_objects_outline(input_handler.selected_objects, camera) - - hud.render_mouse_position(screen, camera) - hud.render_fps(screen, clock) - hud.render_tps(screen, actual_tps) - hud.render_tick_count(screen, total_ticks) - hud.render_selected_objects_info(screen, input_handler.selected_objects) - hud.render_legend(screen, input_handler.show_legend) - hud.render_pause_indicator(screen, input_handler.is_paused) - - # Update display - pygame.display.flip() - clock.tick(MAX_FPS) - - pygame.quit() - sys.exit() - +from core.simulation_engine import SimulationEngine if __name__ == "__main__": - main() + engine = SimulationEngine() + engine.run() diff --git a/pyproject.toml b/pyproject.toml index b2ef26e..327e732 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,6 +4,7 @@ version = "0.1.0" description = "Add your description here" requires-python = ">=3.11" dependencies = [ + "numpy>=2.3.0", "pre-commit>=4.2.0", "pydantic>=2.11.5", "pygame>=2.6.1", diff --git a/ui/hud.py b/ui/hud.py index f25701e..cee64aa 100644 --- a/ui/hud.py +++ b/ui/hud.py @@ -3,6 +3,8 @@ import pygame from config.constants import * +from world.base.brain import CellBrain, FlexibleNeuralNetwork +from world.objects import DefaultCell class HUD: @@ -123,4 +125,252 @@ class HUD: text_rect = text.get_rect() text_rect.left = legend_x + left_width + column_gap text_rect.top = legend_y + 5 + i * legend_font_height - screen.blit(text, text_rect) \ No newline at end of file + screen.blit(text, text_rect) + + def render_neural_network_visualization(self, screen, cell: DefaultCell) -> None: + """Render neural network visualization. This is fixed to the screen size and is not dependent on zoom or camera position.""" + + # Visualization layout constants + VIZ_WIDTH = 280 # Width of the neural network visualization area + VIZ_HEIGHT = 300 # Height of the neural network visualization area + VIZ_RIGHT_MARGIN = VIZ_WIDTH + 50 # Distance from right edge of screen to visualization + + # Background styling constants + BACKGROUND_PADDING = 30 # Padding around the visualization background + BACKGROUND_BORDER_WIDTH = 2 # Width of the background border + BACKGROUND_COLOR = (30, 30, 30) # Dark gray background color + + # Title positioning constants + TITLE_TOP_MARGIN = 30 # Distance above visualization for title + + # Neuron appearance constants + NEURON_RADIUS = 8 # Radius of neuron circles + NEURON_BORDER_WIDTH = 2 # Width of neuron circle borders + + # Layer spacing constants + LAYER_VERTICAL_MARGIN = 30 # Top and bottom margin within visualization for neurons + + # Connection appearance constants + WEIGHT_NORMALIZATION_DIVISOR = 2 # Divisor for normalizing weights to [-1, 1] range + MAX_CONNECTION_THICKNESS = 3 # Maximum thickness for connection lines + MIN_CONNECTION_THICKNESS = 1 # Minimum thickness for connection lines + + # Connection colors (RGB values) + CONNECTION_BASE_INTENSITY = 128 # Base color intensity for connections + CONNECTION_POSITIVE_GREEN = 128 # Green component for positive weights + CONNECTION_NEGATIVE_RED = 128 # Red component for negative weights + + # Neuron activation colors + NEURON_BASE_INTENSITY = 100 # Base color intensity for neurons + NEURON_ACTIVATION_INTENSITY = 155 # Additional intensity based on activation + + # Text positioning constants + ACTIVATION_TEXT_OFFSET = 15 # Distance below neuron for activation value text + ACTIVATION_DISPLAY_THRESHOLD = 0.01 # Minimum activation value to display as text + ACTIVATION_TEXT_PRECISION = 2 # Decimal places for activation values + + # Layer label positioning constants + LAYER_LABEL_BOTTOM_MARGIN = 15 # Distance below visualization for layer labels + + # Info text positioning constants + INFO_TEXT_TOP_MARGIN = 35 # Distance below visualization for info text + INFO_TEXT_LINE_SPACING = 15 # Vertical spacing between info text lines + + # Activation value clamping + ACTIVATION_CLAMP_MIN = -1 # Minimum activation value for visualization + ACTIVATION_CLAMP_MAX = 1 # Maximum activation value for visualization + + if not hasattr(cell, 'behavioral_model'): + return + + cell_brain: CellBrain = cell.behavioral_model + + if not hasattr(cell_brain, 'neural_network'): + return + + network: FlexibleNeuralNetwork = cell_brain.neural_network + + # Calculate visualization position + viz_x = SCREEN_WIDTH - VIZ_RIGHT_MARGIN # Right side of screen + viz_y = (SCREEN_HEIGHT // 2) - (VIZ_HEIGHT // 2) # Centered vertically + + layer_spacing = VIZ_WIDTH // max(1, len(network.layers) - 1) if len(network.layers) > 1 else VIZ_WIDTH + + # Draw background + background_rect = pygame.Rect(viz_x - BACKGROUND_PADDING, viz_y - BACKGROUND_PADDING, + VIZ_WIDTH + 2 * BACKGROUND_PADDING, VIZ_HEIGHT + 2 * BACKGROUND_PADDING) + pygame.draw.rect(screen, BACKGROUND_COLOR, background_rect) + pygame.draw.rect(screen, WHITE, background_rect, BACKGROUND_BORDER_WIDTH) + + # Title + title_text = self.font.render("Neural Network", True, WHITE) + title_rect = title_text.get_rect() + title_rect.centerx = viz_x + VIZ_WIDTH // 2 + title_rect.top = viz_y - TITLE_TOP_MARGIN + screen.blit(title_text, title_rect) + + # Get current activations by running a forward pass with current inputs + input_values = [cell_brain.inputs[key] for key in cell_brain.input_keys] + + # Store activations for each layer + activations = [input_values] # Input layer + + # Calculate activations for each layer + for layer_idx in range(1, len(network.layers)): + layer_activations = [] + + for neuron in network.layers[layer_idx]: + if neuron['type'] == 'input': + continue + + # Calculate weighted sum + weighted_sum = neuron.get('bias', 0) + + for source_layer, source_neuron, weight in neuron.get('connections', []): + if source_layer < len(activations) and source_neuron < len(activations[source_layer]): + weighted_sum += activations[source_layer][source_neuron] * weight + + # Apply activation function + activation = max(ACTIVATION_CLAMP_MIN, min(ACTIVATION_CLAMP_MAX, weighted_sum)) + layer_activations.append(activation) + + activations.append(layer_activations) + + # Calculate neuron positions + neuron_positions = {} + + for layer_idx, layer in enumerate(network.layers): + layer_neurons = [n for n in layer if n['type'] != 'input' or layer_idx == 0] + layer_size = len(layer_neurons) + + if layer_size == 0: + continue + + # X position based on layer + if len(network.layers) == 1: + x = viz_x + VIZ_WIDTH // 2 + else: + x = viz_x + (layer_idx * layer_spacing) + + # Y positions distributed vertically + if layer_size == 1: + y_positions = [viz_y + VIZ_HEIGHT // 2] + else: + y_start = viz_y + LAYER_VERTICAL_MARGIN + y_end = viz_y + VIZ_HEIGHT - LAYER_VERTICAL_MARGIN + y_positions = [y_start + i * (y_end - y_start) / (layer_size - 1) for i in range(layer_size)] + + for neuron_idx, neuron in enumerate(layer_neurons): + if neuron_idx < len(y_positions): + neuron_positions[(layer_idx, neuron_idx)] = (int(x), int(y_positions[neuron_idx])) + + # Draw connections first (so they appear behind neurons) + for layer_idx in range(1, len(network.layers)): + for neuron_idx, neuron in enumerate(network.layers[layer_idx]): + if neuron['type'] == 'input': + continue + + target_pos = neuron_positions.get((layer_idx, neuron_idx)) + if not target_pos: + continue + + for source_layer, source_neuron, weight in neuron.get('connections', []): + source_pos = neuron_positions.get((source_layer, source_neuron)) + if not source_pos: + continue + + # Color based on weight: red for negative, green for positive + weight_normalized = max(ACTIVATION_CLAMP_MIN, + min(ACTIVATION_CLAMP_MAX, weight / WEIGHT_NORMALIZATION_DIVISOR)) + + if weight_normalized >= 0: + # Positive weight: interpolate from gray to green + intensity = int(weight_normalized * 255) + color = (max(0, CONNECTION_BASE_INTENSITY - intensity), + CONNECTION_BASE_INTENSITY + intensity // 2, + max(0, CONNECTION_BASE_INTENSITY - intensity)) + else: + # Negative weight: interpolate from gray to red + intensity = int(-weight_normalized * 255) + color = (CONNECTION_BASE_INTENSITY + intensity // 2, + max(0, CONNECTION_BASE_INTENSITY - intensity), + max(0, CONNECTION_BASE_INTENSITY - intensity)) + + # Line thickness based on weight magnitude + thickness = max(MIN_CONNECTION_THICKNESS, int(abs(weight_normalized) * MAX_CONNECTION_THICKNESS)) + + pygame.draw.line(screen, color, source_pos, target_pos, thickness) + + # Draw neurons + for layer_idx, layer in enumerate(network.layers): + layer_activations = activations[layer_idx] if layer_idx < len(activations) else [] + + for neuron_idx, neuron in enumerate(layer): + if neuron['type'] == 'input' and layer_idx != 0: + continue + + pos = neuron_positions.get((layer_idx, neuron_idx)) + if not pos: + continue + + # Get activation value + activation = 0 + if neuron_idx < len(layer_activations): + activation = layer_activations[neuron_idx] + + # Color based on activation: brightness represents magnitude + activation_normalized = max(ACTIVATION_CLAMP_MIN, min(ACTIVATION_CLAMP_MAX, activation)) + activation_intensity = int(abs(activation_normalized) * NEURON_ACTIVATION_INTENSITY) + + if activation_normalized >= 0: + # Positive activation: blue tint + color = (NEURON_BASE_INTENSITY, NEURON_BASE_INTENSITY, NEURON_BASE_INTENSITY + activation_intensity) + else: + # Negative activation: red tint + color = (NEURON_BASE_INTENSITY + activation_intensity, NEURON_BASE_INTENSITY, NEURON_BASE_INTENSITY) + + # Draw neuron + pygame.draw.circle(screen, color, pos, NEURON_RADIUS) + pygame.draw.circle(screen, WHITE, pos, NEURON_RADIUS, NEURON_BORDER_WIDTH) + + # Draw activation value as text + if abs(activation) > ACTIVATION_DISPLAY_THRESHOLD: + activation_text = self.legend_font.render(f"{activation:.{ACTIVATION_TEXT_PRECISION}f}", True, + WHITE) + text_rect = activation_text.get_rect() + text_rect.center = (pos[0], pos[1] + NEURON_RADIUS + ACTIVATION_TEXT_OFFSET) + screen.blit(activation_text, text_rect) + + # Draw layer labels + layer_labels = ["Input", "Hidden", "Output"] + for layer_idx in range(len(network.layers)): + if layer_idx >= len(layer_labels): + label = f"Layer {layer_idx}" + else: + label = layer_labels[layer_idx] if layer_idx < len(layer_labels) else f"Hidden {layer_idx - 1}" + + # Find average x position for this layer + x_positions = [pos[0] for (l_idx, n_idx), pos in neuron_positions.items() if l_idx == layer_idx] + if x_positions: + avg_x = sum(x_positions) // len(x_positions) + + label_text = self.legend_font.render(label, True, WHITE) + label_rect = label_text.get_rect() + label_rect.centerx = avg_x + label_rect.bottom = viz_y + VIZ_HEIGHT + LAYER_LABEL_BOTTOM_MARGIN + screen.blit(label_text, label_rect) + + # Draw network info + info = network.get_structure_info() + info_lines = [ + f"Layers: {info['total_layers']}", + f"Neurons: {info['total_neurons']}", + f"Connections: {info['total_connections']}" + ] + + for i, line in enumerate(info_lines): + info_text = self.legend_font.render(line, True, WHITE) + info_rect = info_text.get_rect() + info_rect.left = viz_x + info_rect.top = viz_y + VIZ_HEIGHT + INFO_TEXT_TOP_MARGIN + i * INFO_TEXT_LINE_SPACING + screen.blit(info_text, info_rect) diff --git a/uv.lock b/uv.lock index a1733cc..601252d 100644 --- a/uv.lock +++ b/uv.lock @@ -43,6 +43,7 @@ name = "dynamicsystemabstraction" version = "0.1.0" source = { virtual = "." } dependencies = [ + { name = "numpy" }, { name = "pre-commit" }, { name = "pydantic" }, { name = "pygame" }, @@ -56,6 +57,7 @@ dev = [ [package.metadata] requires-dist = [ + { name = "numpy", specifier = ">=2.3.0" }, { name = "pre-commit", specifier = ">=4.2.0" }, { name = "pydantic", specifier = ">=2.11.5" }, { name = "pygame", specifier = ">=2.6.1" }, @@ -101,6 +103,64 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, ] +[[package]] +name = "numpy" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/db/8e12381333aea300890829a0a36bfa738cac95475d88982d538725143fd9/numpy-2.3.0.tar.gz", hash = "sha256:581f87f9e9e9db2cba2141400e160e9dd644ee248788d6f90636eeb8fd9260a6", size = 20382813, upload-time = "2025-06-07T14:54:32.608Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/5f/df67435257d827eb3b8af66f585223dc2c3f2eb7ad0b50cb1dae2f35f494/numpy-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c3c9fdde0fa18afa1099d6257eb82890ea4f3102847e692193b54e00312a9ae9", size = 21199688, upload-time = "2025-06-07T14:36:52.067Z" }, + { url = "https://files.pythonhosted.org/packages/e5/ce/aad219575055d6c9ef29c8c540c81e1c38815d3be1fe09cdbe53d48ee838/numpy-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:46d16f72c2192da7b83984aa5455baee640e33a9f1e61e656f29adf55e406c2b", size = 14359277, upload-time = "2025-06-07T14:37:15.325Z" }, + { url = "https://files.pythonhosted.org/packages/29/6b/2d31da8e6d2ec99bed54c185337a87f8fbeccc1cd9804e38217e92f3f5e2/numpy-2.3.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a0be278be9307c4ab06b788f2a077f05e180aea817b3e41cebbd5aaf7bd85ed3", size = 5376069, upload-time = "2025-06-07T14:37:25.636Z" }, + { url = "https://files.pythonhosted.org/packages/7d/2a/6c59a062397553ec7045c53d5fcdad44e4536e54972faa2ba44153bca984/numpy-2.3.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:99224862d1412d2562248d4710126355d3a8db7672170a39d6909ac47687a8a4", size = 6913057, upload-time = "2025-06-07T14:37:37.215Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5a/8df16f258d28d033e4f359e29d3aeb54663243ac7b71504e89deeb813202/numpy-2.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:2393a914db64b0ead0ab80c962e42d09d5f385802006a6c87835acb1f58adb96", size = 14568083, upload-time = "2025-06-07T14:37:59.337Z" }, + { url = "https://files.pythonhosted.org/packages/0a/92/0528a563dfc2cdccdcb208c0e241a4bb500d7cde218651ffb834e8febc50/numpy-2.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:7729c8008d55e80784bd113787ce876ca117185c579c0d626f59b87d433ea779", size = 16929402, upload-time = "2025-06-07T14:38:24.343Z" }, + { url = "https://files.pythonhosted.org/packages/e4/2f/e7a8c8d4a2212c527568d84f31587012cf5497a7271ea1f23332142f634e/numpy-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:06d4fb37a8d383b769281714897420c5cc3545c79dc427df57fc9b852ee0bf58", size = 15879193, upload-time = "2025-06-07T14:38:48.007Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c3/dada3f005953847fe35f42ac0fe746f6e1ea90b4c6775e4be605dcd7b578/numpy-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c39ec392b5db5088259c68250e342612db82dc80ce044cf16496cf14cf6bc6f8", size = 18665318, upload-time = "2025-06-07T14:39:15.794Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ae/3f448517dedefc8dd64d803f9d51a8904a48df730e00a3c5fb1e75a60620/numpy-2.3.0-cp311-cp311-win32.whl", hash = "sha256:ee9d3ee70d62827bc91f3ea5eee33153212c41f639918550ac0475e3588da59f", size = 6601108, upload-time = "2025-06-07T14:39:27.176Z" }, + { url = "https://files.pythonhosted.org/packages/8c/4a/556406d2bb2b9874c8cbc840c962683ac28f21efbc9b01177d78f0199ca1/numpy-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:43c55b6a860b0eb44d42341438b03513cf3879cb3617afb749ad49307e164edd", size = 13021525, upload-time = "2025-06-07T14:39:46.637Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ee/bf54278aef30335ffa9a189f869ea09e1a195b3f4b93062164a3b02678a7/numpy-2.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:2e6a1409eee0cb0316cb64640a49a49ca44deb1a537e6b1121dc7c458a1299a8", size = 10170327, upload-time = "2025-06-07T14:40:02.703Z" }, + { url = "https://files.pythonhosted.org/packages/89/59/9df493df81ac6f76e9f05cdbe013cdb0c9a37b434f6e594f5bd25e278908/numpy-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:389b85335838155a9076e9ad7f8fdba0827496ec2d2dc32ce69ce7898bde03ba", size = 20897025, upload-time = "2025-06-07T14:40:33.558Z" }, + { url = "https://files.pythonhosted.org/packages/2f/86/4ff04335901d6cf3a6bb9c748b0097546ae5af35e455ae9b962ebff4ecd7/numpy-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9498f60cd6bb8238d8eaf468a3d5bb031d34cd12556af53510f05fcf581c1b7e", size = 14129882, upload-time = "2025-06-07T14:40:55.034Z" }, + { url = "https://files.pythonhosted.org/packages/71/8d/a942cd4f959de7f08a79ab0c7e6cecb7431d5403dce78959a726f0f57aa1/numpy-2.3.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:622a65d40d8eb427d8e722fd410ac3ad4958002f109230bc714fa551044ebae2", size = 5110181, upload-time = "2025-06-07T14:41:04.4Z" }, + { url = "https://files.pythonhosted.org/packages/86/5d/45850982efc7b2c839c5626fb67fbbc520d5b0d7c1ba1ae3651f2f74c296/numpy-2.3.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:b9446d9d8505aadadb686d51d838f2b6688c9e85636a0c3abaeb55ed54756459", size = 6647581, upload-time = "2025-06-07T14:41:14.695Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c0/c871d4a83f93b00373d3eebe4b01525eee8ef10b623a335ec262b58f4dc1/numpy-2.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:50080245365d75137a2bf46151e975de63146ae6d79f7e6bd5c0e85c9931d06a", size = 14262317, upload-time = "2025-06-07T14:41:35.862Z" }, + { url = "https://files.pythonhosted.org/packages/b7/f6/bc47f5fa666d5ff4145254f9e618d56e6a4ef9b874654ca74c19113bb538/numpy-2.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c24bb4113c66936eeaa0dc1e47c74770453d34f46ee07ae4efd853a2ed1ad10a", size = 16633919, upload-time = "2025-06-07T14:42:00.622Z" }, + { url = "https://files.pythonhosted.org/packages/f5/b4/65f48009ca0c9b76df5f404fccdea5a985a1bb2e34e97f21a17d9ad1a4ba/numpy-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4d8d294287fdf685281e671886c6dcdf0291a7c19db3e5cb4178d07ccf6ecc67", size = 15567651, upload-time = "2025-06-07T14:42:24.429Z" }, + { url = "https://files.pythonhosted.org/packages/f1/62/5367855a2018578e9334ed08252ef67cc302e53edc869666f71641cad40b/numpy-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6295f81f093b7f5769d1728a6bd8bf7466de2adfa771ede944ce6711382b89dc", size = 18361723, upload-time = "2025-06-07T14:42:51.167Z" }, + { url = "https://files.pythonhosted.org/packages/d4/75/5baed8cd867eabee8aad1e74d7197d73971d6a3d40c821f1848b8fab8b84/numpy-2.3.0-cp312-cp312-win32.whl", hash = "sha256:e6648078bdd974ef5d15cecc31b0c410e2e24178a6e10bf511e0557eed0f2570", size = 6318285, upload-time = "2025-06-07T14:43:02.052Z" }, + { url = "https://files.pythonhosted.org/packages/bc/49/d5781eaa1a15acb3b3a3f49dc9e2ff18d92d0ce5c2976f4ab5c0a7360250/numpy-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:0898c67a58cdaaf29994bc0e2c65230fd4de0ac40afaf1584ed0b02cd74c6fdd", size = 12732594, upload-time = "2025-06-07T14:43:21.071Z" }, + { url = "https://files.pythonhosted.org/packages/c2/1c/6d343e030815c7c97a1f9fbad00211b47717c7fe446834c224bd5311e6f1/numpy-2.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:bd8df082b6c4695753ad6193018c05aac465d634834dca47a3ae06d4bb22d9ea", size = 9891498, upload-time = "2025-06-07T14:43:36.332Z" }, + { url = "https://files.pythonhosted.org/packages/73/fc/1d67f751fd4dbafc5780244fe699bc4084268bad44b7c5deb0492473127b/numpy-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5754ab5595bfa2c2387d241296e0381c21f44a4b90a776c3c1d39eede13a746a", size = 20889633, upload-time = "2025-06-07T14:44:06.839Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/73ffdb69e5c3f19ec4530f8924c4386e7ba097efc94b9c0aff607178ad94/numpy-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d11fa02f77752d8099573d64e5fe33de3229b6632036ec08f7080f46b6649959", size = 14151683, upload-time = "2025-06-07T14:44:28.847Z" }, + { url = "https://files.pythonhosted.org/packages/64/d5/06d4bb31bb65a1d9c419eb5676173a2f90fd8da3c59f816cc54c640ce265/numpy-2.3.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:aba48d17e87688a765ab1cd557882052f238e2f36545dfa8e29e6a91aef77afe", size = 5102683, upload-time = "2025-06-07T14:44:38.417Z" }, + { url = "https://files.pythonhosted.org/packages/12/8b/6c2cef44f8ccdc231f6b56013dff1d71138c48124334aded36b1a1b30c5a/numpy-2.3.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4dc58865623023b63b10d52f18abaac3729346a7a46a778381e0e3af4b7f3beb", size = 6640253, upload-time = "2025-06-07T14:44:49.359Z" }, + { url = "https://files.pythonhosted.org/packages/62/aa/fca4bf8de3396ddb59544df9b75ffe5b73096174de97a9492d426f5cd4aa/numpy-2.3.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:df470d376f54e052c76517393fa443758fefcdd634645bc9c1f84eafc67087f0", size = 14258658, upload-time = "2025-06-07T14:45:10.156Z" }, + { url = "https://files.pythonhosted.org/packages/1c/12/734dce1087eed1875f2297f687e671cfe53a091b6f2f55f0c7241aad041b/numpy-2.3.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:87717eb24d4a8a64683b7a4e91ace04e2f5c7c77872f823f02a94feee186168f", size = 16628765, upload-time = "2025-06-07T14:45:35.076Z" }, + { url = "https://files.pythonhosted.org/packages/48/03/ffa41ade0e825cbcd5606a5669962419528212a16082763fc051a7247d76/numpy-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d8fa264d56882b59dcb5ea4d6ab6f31d0c58a57b41aec605848b6eb2ef4a43e8", size = 15564335, upload-time = "2025-06-07T14:45:58.797Z" }, + { url = "https://files.pythonhosted.org/packages/07/58/869398a11863310aee0ff85a3e13b4c12f20d032b90c4b3ee93c3b728393/numpy-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e651756066a0eaf900916497e20e02fe1ae544187cb0fe88de981671ee7f6270", size = 18360608, upload-time = "2025-06-07T14:46:25.687Z" }, + { url = "https://files.pythonhosted.org/packages/2f/8a/5756935752ad278c17e8a061eb2127c9a3edf4ba2c31779548b336f23c8d/numpy-2.3.0-cp313-cp313-win32.whl", hash = "sha256:e43c3cce3b6ae5f94696669ff2a6eafd9a6b9332008bafa4117af70f4b88be6f", size = 6310005, upload-time = "2025-06-07T14:50:13.138Z" }, + { url = "https://files.pythonhosted.org/packages/08/60/61d60cf0dfc0bf15381eaef46366ebc0c1a787856d1db0c80b006092af84/numpy-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:81ae0bf2564cf475f94be4a27ef7bcf8af0c3e28da46770fc904da9abd5279b5", size = 12729093, upload-time = "2025-06-07T14:50:31.82Z" }, + { url = "https://files.pythonhosted.org/packages/66/31/2f2f2d2b3e3c32d5753d01437240feaa32220b73258c9eef2e42a0832866/numpy-2.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:c8738baa52505fa6e82778580b23f945e3578412554d937093eac9205e845e6e", size = 9885689, upload-time = "2025-06-07T14:50:47.888Z" }, + { url = "https://files.pythonhosted.org/packages/f1/89/c7828f23cc50f607ceb912774bb4cff225ccae7131c431398ad8400e2c98/numpy-2.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:39b27d8b38942a647f048b675f134dd5a567f95bfff481f9109ec308515c51d8", size = 20986612, upload-time = "2025-06-07T14:46:56.077Z" }, + { url = "https://files.pythonhosted.org/packages/dd/46/79ecf47da34c4c50eedec7511e53d57ffdfd31c742c00be7dc1d5ffdb917/numpy-2.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0eba4a1ea88f9a6f30f56fdafdeb8da3774349eacddab9581a21234b8535d3d3", size = 14298953, upload-time = "2025-06-07T14:47:18.053Z" }, + { url = "https://files.pythonhosted.org/packages/59/44/f6caf50713d6ff4480640bccb2a534ce1d8e6e0960c8f864947439f0ee95/numpy-2.3.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:b0f1f11d0a1da54927436505a5a7670b154eac27f5672afc389661013dfe3d4f", size = 5225806, upload-time = "2025-06-07T14:47:27.524Z" }, + { url = "https://files.pythonhosted.org/packages/a6/43/e1fd1aca7c97e234dd05e66de4ab7a5be54548257efcdd1bc33637e72102/numpy-2.3.0-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:690d0a5b60a47e1f9dcec7b77750a4854c0d690e9058b7bef3106e3ae9117808", size = 6735169, upload-time = "2025-06-07T14:47:38.057Z" }, + { url = "https://files.pythonhosted.org/packages/84/89/f76f93b06a03177c0faa7ca94d0856c4e5c4bcaf3c5f77640c9ed0303e1c/numpy-2.3.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:8b51ead2b258284458e570942137155978583e407babc22e3d0ed7af33ce06f8", size = 14330701, upload-time = "2025-06-07T14:47:59.113Z" }, + { url = "https://files.pythonhosted.org/packages/aa/f5/4858c3e9ff7a7d64561b20580cf7cc5d085794bd465a19604945d6501f6c/numpy-2.3.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:aaf81c7b82c73bd9b45e79cfb9476cb9c29e937494bfe9092c26aece812818ad", size = 16692983, upload-time = "2025-06-07T14:48:24.196Z" }, + { url = "https://files.pythonhosted.org/packages/08/17/0e3b4182e691a10e9483bcc62b4bb8693dbf9ea5dc9ba0b77a60435074bb/numpy-2.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f420033a20b4f6a2a11f585f93c843ac40686a7c3fa514060a97d9de93e5e72b", size = 15641435, upload-time = "2025-06-07T14:48:47.712Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d5/463279fda028d3c1efa74e7e8d507605ae87f33dbd0543cf4c4527c8b882/numpy-2.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d344ca32ab482bcf8735d8f95091ad081f97120546f3d250240868430ce52555", size = 18433798, upload-time = "2025-06-07T14:49:14.866Z" }, + { url = "https://files.pythonhosted.org/packages/0e/1e/7a9d98c886d4c39a2b4d3a7c026bffcf8fbcaf518782132d12a301cfc47a/numpy-2.3.0-cp313-cp313t-win32.whl", hash = "sha256:48a2e8eaf76364c32a1feaa60d6925eaf32ed7a040183b807e02674305beef61", size = 6438632, upload-time = "2025-06-07T14:49:25.67Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ab/66fc909931d5eb230107d016861824f335ae2c0533f422e654e5ff556784/numpy-2.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ba17f93a94e503551f154de210e4d50c5e3ee20f7e7a1b5f6ce3f22d419b93bb", size = 12868491, upload-time = "2025-06-07T14:49:44.898Z" }, + { url = "https://files.pythonhosted.org/packages/ee/e8/2c8a1c9e34d6f6d600c83d5ce5b71646c32a13f34ca5c518cc060639841c/numpy-2.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f14e016d9409680959691c109be98c436c6249eaf7f118b424679793607b5944", size = 9935345, upload-time = "2025-06-07T14:50:02.311Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a2/f8c1133f90eaa1c11bbbec1dc28a42054d0ce74bc2c9838c5437ba5d4980/numpy-2.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:80b46117c7359de8167cc00a2c7d823bdd505e8c7727ae0871025a86d668283b", size = 21070759, upload-time = "2025-06-07T14:51:18.241Z" }, + { url = "https://files.pythonhosted.org/packages/6c/e0/4c05fc44ba28463096eee5ae2a12832c8d2759cc5bcec34ae33386d3ff83/numpy-2.3.0-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:5814a0f43e70c061f47abd5857d120179609ddc32a613138cbb6c4e9e2dbdda5", size = 5301054, upload-time = "2025-06-07T14:51:27.413Z" }, + { url = "https://files.pythonhosted.org/packages/8a/3b/6c06cdebe922bbc2a466fe2105f50f661238ea223972a69c7deb823821e7/numpy-2.3.0-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:ef6c1e88fd6b81ac6d215ed71dc8cd027e54d4bf1d2682d362449097156267a2", size = 6817520, upload-time = "2025-06-07T14:51:38.015Z" }, + { url = "https://files.pythonhosted.org/packages/9d/a3/1e536797fd10eb3c5dbd2e376671667c9af19e241843548575267242ea02/numpy-2.3.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:33a5a12a45bb82d9997e2c0b12adae97507ad7c347546190a18ff14c28bbca12", size = 14398078, upload-time = "2025-06-07T14:52:00.122Z" }, + { url = "https://files.pythonhosted.org/packages/7c/61/9d574b10d9368ecb1a0c923952aa593510a20df4940aa615b3a71337c8db/numpy-2.3.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:54dfc8681c1906d239e95ab1508d0a533c4a9505e52ee2d71a5472b04437ef97", size = 16751324, upload-time = "2025-06-07T14:52:25.077Z" }, + { url = "https://files.pythonhosted.org/packages/39/de/bcad52ce972dc26232629ca3a99721fd4b22c1d2bda84d5db6541913ef9c/numpy-2.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:e017a8a251ff4d18d71f139e28bdc7c31edba7a507f72b1414ed902cbe48c74d", size = 12924237, upload-time = "2025-06-07T14:52:44.713Z" }, +] + [[package]] name = "packaging" version = "25.0" diff --git a/world/base/brain.py b/world/base/brain.py index 064cbc6..7ee3a39 100644 --- a/world/base/brain.py +++ b/world/base/brain.py @@ -1,45 +1,331 @@ +import numpy as np +import random +from copy import deepcopy from world.behavioral import BehavioralModel +class FlexibleNeuralNetwork: + """ + A flexible neural network that can mutate its structure and weights. + Supports variable topology with cross-layer connections. + """ + + def __init__(self, input_size=2, output_size=2): + self.input_size = input_size + self.output_size = output_size + + # Network structure: list of layers, each layer is a list of neurons + # Each neuron is represented by its connections and bias + self.layers = [] + + # Initialize with just input and output layers (no hidden layers) + self._initialize_basic_network() + + def _initialize_basic_network(self): + """Initialize a basic network with input->output connections only.""" + # Input layer (no actual neurons, just placeholders) + input_layer = [{'type': 'input', 'id': i} for i in range(self.input_size)] + + # Output layer with connections to all inputs + output_layer = [] + for i in range(self.output_size): + neuron = { + 'type': 'output', + 'id': f'out_{i}', + 'bias': random.uniform(-1, 1), + 'connections': [] # List of (source_layer, source_neuron, weight) + } + + # Connect to all input neurons + for j in range(self.input_size): + neuron['connections'].append((0, j, random.uniform(-2, 2))) + + output_layer.append(neuron) + + self.layers = [input_layer, output_layer] + + def forward(self, inputs): + """ + Forward pass through the network. + + :param inputs: List or array of input values + :return: List of output values + """ + if len(inputs) != self.input_size: + raise ValueError(f"Expected {self.input_size} inputs, got {len(inputs)}") + + # Store activations for each layer + activations = [inputs] # Input layer activations + + # Process each subsequent layer + for layer_idx in range(1, len(self.layers)): + layer_activations = [] + + for neuron in self.layers[layer_idx]: + if neuron['type'] == 'input': + continue # Skip input neurons in hidden layers + + # Calculate weighted sum of inputs + weighted_sum = neuron['bias'] + + for source_layer, source_neuron, weight in neuron['connections']: + if source_layer < len(activations): + if source_neuron < len(activations[source_layer]): + weighted_sum += activations[source_layer][source_neuron] * weight + + # Apply activation function (tanh for bounded output) + activation = np.tanh(weighted_sum) + layer_activations.append(activation) + + activations.append(layer_activations) + + return activations[-1] # Return output layer activations + + def mutate(self, mutation_rate=0.1): + """ + Create a mutated copy of this network. + + :param mutation_rate: Probability of each type of mutation + :return: New mutated FlexibleNeuralNetwork instance + """ + mutated = deepcopy(self) + + # Different types of mutations + mutations = [ + mutated._mutate_weights, + mutated._mutate_biases, + mutated._add_connection, + mutated._remove_connection, + mutated._add_neuron, + mutated._remove_neuron + ] + + # Apply random mutations + for mutation_func in mutations: + if random.random() < mutation_rate: + mutation_func() + + return mutated + + def _mutate_weights(self): + """Slightly modify existing connection weights.""" + for layer in self.layers[1:]: # Skip input layer + for neuron in layer: + if 'connections' in neuron: + for i in range(len(neuron['connections'])): + if random.random() < 0.3: # 30% chance to mutate each weight + source_layer, source_neuron, weight = neuron['connections'][i] + # Add small random change + new_weight = weight + random.uniform(-0.5, 0.5) + neuron['connections'][i] = (source_layer, source_neuron, new_weight) + + def _mutate_biases(self): + """Slightly modify neuron biases.""" + for layer in self.layers[1:]: # Skip input layer + for neuron in layer: + if 'bias' in neuron and random.random() < 0.3: + neuron['bias'] += random.uniform(-0.5, 0.5) + + def _add_connection(self): + """Add a new random connection.""" + if len(self.layers) < 2: + return + + # Pick a random target neuron (not in input layer) + target_layer_idx = random.randint(1, len(self.layers) - 1) + target_neuron_idx = random.randint(0, len(self.layers[target_layer_idx]) - 1) + target_neuron = self.layers[target_layer_idx][target_neuron_idx] + + if 'connections' not in target_neuron: + return + + # Pick a random source (from any previous layer) + source_layer_idx = random.randint(0, target_layer_idx - 1) + if len(self.layers[source_layer_idx]) == 0: + return + + source_neuron_idx = random.randint(0, len(self.layers[source_layer_idx]) - 1) + + # Check if connection already exists + for conn in target_neuron['connections']: + if conn[0] == source_layer_idx and conn[1] == source_neuron_idx: + return # Connection already exists + + # Add new connection + new_weight = random.uniform(-2, 2) + target_neuron['connections'].append((source_layer_idx, source_neuron_idx, new_weight)) + + def _remove_connection(self): + """Remove a random connection.""" + for layer in self.layers[1:]: + for neuron in layer: + if 'connections' in neuron and len(neuron['connections']) > 1: + if random.random() < 0.1: # 10% chance to remove a connection + neuron['connections'].pop(random.randint(0, len(neuron['connections']) - 1)) + + def _add_neuron(self): + """Add a new neuron to a random hidden layer or create a new hidden layer.""" + if random.random() < 0.05: # 5% chance to add neuron + if len(self.layers) == 2: # Only input and output layers + # Create a new hidden layer + hidden_neuron = { + 'type': 'hidden', + 'id': f'hidden_{random.randint(1000, 9999)}', + 'bias': random.uniform(-1, 1), + 'connections': [] + } + + # Connect to some input neurons + for i in range(self.input_size): + if random.random() < 0.7: # 70% chance to connect to each input + hidden_neuron['connections'].append((0, i, random.uniform(-2, 2))) + + # Insert hidden layer + self.layers.insert(1, [hidden_neuron]) + + # Update output layer connections to potentially use new hidden neuron + for neuron in self.layers[-1]: # Output layer + if random.random() < 0.5: # 50% chance to connect to new hidden neuron + neuron['connections'].append((1, 0, random.uniform(-2, 2))) + + else: + # Add neuron to existing hidden layer + hidden_layer_idx = random.randint(1, len(self.layers) - 2) + new_neuron = { + 'type': 'hidden', + 'id': f'hidden_{random.randint(1000, 9999)}', + 'bias': random.uniform(-1, 1), + 'connections': [] + } + + # Connect to some neurons from previous layers + for layer_idx in range(hidden_layer_idx): + for neuron_idx in range(len(self.layers[layer_idx])): + if random.random() < 0.3: # 30% chance to connect + new_neuron['connections'].append((layer_idx, neuron_idx, random.uniform(-2, 2))) + + self.layers[hidden_layer_idx].append(new_neuron) + + def _remove_neuron(self): + """Remove a random neuron from hidden layers.""" + if len(self.layers) > 2: # Has hidden layers + for layer_idx in range(1, len(self.layers) - 1): # Only hidden layers + if len(self.layers[layer_idx]) > 0 and random.random() < 0.02: # 2% chance + neuron_idx = random.randint(0, len(self.layers[layer_idx]) - 1) + self.layers[layer_idx].pop(neuron_idx) + + # Remove connections to this neuron from later layers + for later_layer_idx in range(layer_idx + 1, len(self.layers)): + for neuron in self.layers[later_layer_idx]: + if 'connections' in neuron: + neuron['connections'] = [ + (src_layer, src_neuron, weight) + for src_layer, src_neuron, weight in neuron['connections'] + if not (src_layer == layer_idx and src_neuron == neuron_idx) + ] + break + + def get_structure_info(self): + """Return information about the network structure.""" + info = { + 'total_layers': len(self.layers), + 'layer_sizes': [len(layer) for layer in self.layers], + 'total_connections': 0, + 'total_neurons': sum(len(layer) for layer in self.layers) + } + + for layer in self.layers[1:]: + for neuron in layer: + if 'connections' in neuron: + info['total_connections'] += len(neuron['connections']) + + return info + + class CellBrain(BehavioralModel): - def __init__(self): + """ + Enhanced CellBrain using a flexible neural network with input normalization. + """ + + def __init__(self, neural_network=None, input_ranges=None): super().__init__() - # Define input keys - self.inputs = { - 'distance': 0.0, # Distance from a food object - 'angle': 0.0 # Relative angle to a food object - } - # Define output keys - self.outputs = { - 'linear_acceleration': 0.0, # Linear acceleration - 'angular_acceleration': 0.0 # Angular acceleration - } + # Define input and output keys + self.input_keys = ['distance', 'angle'] + self.output_keys = ['linear_acceleration', 'angular_acceleration'] - self.weights = { - 'distance': 0.1, - 'angle': 0.5 + # Initialize inputs and outputs + self.inputs = {key: 0.0 for key in self.input_keys} + self.outputs = {key: 0.0 for key in self.output_keys} + + # Set input ranges for normalization + default_ranges = { + 'distance': (0, 50), + 'angle': (-180, 180) } + self.input_ranges = input_ranges if input_ranges is not None else default_ranges + + # Use provided network or create new one + if neural_network is None: + self.neural_network = FlexibleNeuralNetwork( + input_size=len(self.input_keys), + output_size=len(self.output_keys) + ) + else: + self.neural_network = neural_network + + def _normalize_input(self, key, value): + min_val, max_val = self.input_ranges.get(key, (0.0, 1.0)) + # Avoid division by zero + if max_val == min_val: + return 0.0 + # Normalize to [-1, 1] + return 2 * (value - min_val) / (max_val - min_val) - 1 def tick(self, input_data) -> dict: """ - Process inputs and produce corresponding outputs. + Process inputs through neural network and produce outputs. - :param input_data: Dictionary containing 'distance' and 'angle' values - :return: Dictionary with 'linear_acceleration' and 'angular_acceleration' values + :param input_data: Dictionary containing input values + :return: Dictionary with output values """ # Update internal input state - self.inputs['distance'] = input_data.get('distance', 0.0) - self.inputs['angle'] = input_data.get('angle', 0.0) + for key in self.input_keys: + self.inputs[key] = input_data.get(key, 0.0) - # Initialize output dictionary - self.outputs = {'linear_acceleration': self.inputs['distance'] * self.weights['distance'], - 'angular_acceleration': self.inputs['angle'] * self.weights['angle']} + # Normalize inputs + input_array = [self._normalize_input(key, self.inputs[key]) for key in self.input_keys] - return self.outputs + # Process through neural network + output_array = self.neural_network.forward(input_array) + + # Map outputs back to dictionary + self.outputs = { + key: output_array[i] if i < len(output_array) else 0.0 + for i, key in enumerate(self.output_keys) + } + + return self.outputs.copy() + + def mutate(self, mutation_rate=0.1): + """ + Create a mutated copy of this CellBrain. + + :param mutation_rate: Rate of mutation for the neural network + :return: New CellBrain with mutated neural network + """ + mutated_network = self.neural_network.mutate(mutation_rate) + return CellBrain(neural_network=mutated_network, input_ranges=self.input_ranges.copy()) + + def get_network_info(self): + """Get information about the underlying neural network.""" + return self.neural_network.get_structure_info() def __repr__(self): inputs = {key: round(value, 5) for key, value in self.inputs.items()} outputs = {key: round(value, 5) for key, value in self.outputs.items()} - weights = {key: round(value, 5) for key, value in self.weights.items()} - return f"CellBrain(inputs={inputs}, outputs={outputs}, weights={weights})" + network_info = self.get_network_info() + + return (f"CellBrain(inputs={inputs}, outputs={outputs}, " + f"network_layers={network_info['layer_sizes']}, " + f"connections={network_info['total_connections']})") \ No newline at end of file diff --git a/world/base/neural.py b/world/base/neural.py new file mode 100644 index 0000000..e69de29 diff --git a/world/objects.py b/world/objects.py index 57b538a..5292509 100644 --- a/world/objects.py +++ b/world/objects.py @@ -418,4 +418,4 @@ class DefaultCell(BaseEntity): position = f"({round(self.position.x, 1)}, {round(self.position.y, 1)})" velocity = tuple(round(value, 1) for value in self.velocity) acceleration = tuple(round(value, 1) for value in self.acceleration) - return f"DefaultCell(position={position}, velocity={velocity}, acceleration={acceleration}, behavioral_model={self.behavioral_model})" + return f"DefaultCell(position={position}, velocity={velocity}, acceleration={acceleration}"