Merge pull request 'Implement FlexibleNeuralNetwork and enhance CellBrain with input normalization and neural network integration' (#2) from move into master
Some checks failed
Build Simulation and Test / Run All Tests (push) Failing after 31s
Some checks failed
Build Simulation and Test / Run All Tests (push) Failing after 31s
Reviewed-on: #2
This commit is contained in:
commit
8f6b9c8322
@ -4,6 +4,7 @@
|
|||||||
import pygame
|
import pygame
|
||||||
import math
|
import math
|
||||||
from config.constants import *
|
from config.constants import *
|
||||||
|
from world.base.brain import CellBrain
|
||||||
|
|
||||||
|
|
||||||
class Renderer:
|
class Renderer:
|
||||||
@ -238,4 +239,4 @@ class Renderer:
|
|||||||
screen_x, screen_y = camera.world_to_screen(obj_x, obj_y)
|
screen_x, screen_y = camera.world_to_screen(obj_x, obj_y)
|
||||||
size = camera.get_relative_size(width)
|
size = camera.get_relative_size(width)
|
||||||
rect = pygame.Rect(screen_x - size // 2, screen_y - size // 2, size, size)
|
rect = pygame.Rect(screen_x - size // 2, screen_y - size // 2, size, size)
|
||||||
pygame.draw.rect(self.screen, SELECTION_BLUE, rect, 1)
|
pygame.draw.rect(self.screen, SELECTION_BLUE, rect, 1)
|
||||||
|
|||||||
107
core/simulation_engine.py
Normal file
107
core/simulation_engine.py
Normal file
@ -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)
|
||||||
118
main.py
118
main.py
@ -1,117 +1,5 @@
|
|||||||
import math
|
from core.simulation_engine import SimulationEngine
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
engine = SimulationEngine()
|
||||||
|
engine.run()
|
||||||
|
|||||||
@ -4,6 +4,7 @@ version = "0.1.0"
|
|||||||
description = "Add your description here"
|
description = "Add your description here"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"numpy>=2.3.0",
|
||||||
"pre-commit>=4.2.0",
|
"pre-commit>=4.2.0",
|
||||||
"pydantic>=2.11.5",
|
"pydantic>=2.11.5",
|
||||||
"pygame>=2.6.1",
|
"pygame>=2.6.1",
|
||||||
|
|||||||
252
ui/hud.py
252
ui/hud.py
@ -3,6 +3,8 @@
|
|||||||
|
|
||||||
import pygame
|
import pygame
|
||||||
from config.constants import *
|
from config.constants import *
|
||||||
|
from world.base.brain import CellBrain, FlexibleNeuralNetwork
|
||||||
|
from world.objects import DefaultCell
|
||||||
|
|
||||||
|
|
||||||
class HUD:
|
class HUD:
|
||||||
@ -123,4 +125,252 @@ class HUD:
|
|||||||
text_rect = text.get_rect()
|
text_rect = text.get_rect()
|
||||||
text_rect.left = legend_x + left_width + column_gap
|
text_rect.left = legend_x + left_width + column_gap
|
||||||
text_rect.top = legend_y + 5 + i * legend_font_height
|
text_rect.top = legend_y + 5 + i * legend_font_height
|
||||||
screen.blit(text, text_rect)
|
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)
|
||||||
|
|||||||
60
uv.lock
generated
60
uv.lock
generated
@ -43,6 +43,7 @@ name = "dynamicsystemabstraction"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
{ name = "numpy" },
|
||||||
{ name = "pre-commit" },
|
{ name = "pre-commit" },
|
||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
{ name = "pygame" },
|
{ name = "pygame" },
|
||||||
@ -56,6 +57,7 @@ dev = [
|
|||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
|
{ name = "numpy", specifier = ">=2.3.0" },
|
||||||
{ name = "pre-commit", specifier = ">=4.2.0" },
|
{ name = "pre-commit", specifier = ">=4.2.0" },
|
||||||
{ name = "pydantic", specifier = ">=2.11.5" },
|
{ name = "pydantic", specifier = ">=2.11.5" },
|
||||||
{ name = "pygame", specifier = ">=2.6.1" },
|
{ 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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "packaging"
|
name = "packaging"
|
||||||
version = "25.0"
|
version = "25.0"
|
||||||
|
|||||||
@ -1,45 +1,331 @@
|
|||||||
|
import numpy as np
|
||||||
|
import random
|
||||||
|
from copy import deepcopy
|
||||||
from world.behavioral import BehavioralModel
|
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):
|
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__()
|
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
|
# Define input and output keys
|
||||||
self.outputs = {
|
self.input_keys = ['distance', 'angle']
|
||||||
'linear_acceleration': 0.0, # Linear acceleration
|
self.output_keys = ['linear_acceleration', 'angular_acceleration']
|
||||||
'angular_acceleration': 0.0 # Angular acceleration
|
|
||||||
}
|
|
||||||
|
|
||||||
self.weights = {
|
# Initialize inputs and outputs
|
||||||
'distance': 0.1,
|
self.inputs = {key: 0.0 for key in self.input_keys}
|
||||||
'angle': 0.5
|
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:
|
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
|
:param input_data: Dictionary containing input values
|
||||||
:return: Dictionary with 'linear_acceleration' and 'angular_acceleration' values
|
:return: Dictionary with output values
|
||||||
"""
|
"""
|
||||||
# Update internal input state
|
# Update internal input state
|
||||||
self.inputs['distance'] = input_data.get('distance', 0.0)
|
for key in self.input_keys:
|
||||||
self.inputs['angle'] = input_data.get('angle', 0.0)
|
self.inputs[key] = input_data.get(key, 0.0)
|
||||||
|
|
||||||
# Initialize output dictionary
|
# Normalize inputs
|
||||||
self.outputs = {'linear_acceleration': self.inputs['distance'] * self.weights['distance'],
|
input_array = [self._normalize_input(key, self.inputs[key]) for key in self.input_keys]
|
||||||
'angular_acceleration': self.inputs['angle'] * self.weights['angle']}
|
|
||||||
|
|
||||||
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):
|
def __repr__(self):
|
||||||
inputs = {key: round(value, 5) for key, value in self.inputs.items()}
|
inputs = {key: round(value, 5) for key, value in self.inputs.items()}
|
||||||
outputs = {key: round(value, 5) for key, value in self.outputs.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()}
|
network_info = self.get_network_info()
|
||||||
return f"CellBrain(inputs={inputs}, outputs={outputs}, weights={weights})"
|
|
||||||
|
return (f"CellBrain(inputs={inputs}, outputs={outputs}, "
|
||||||
|
f"network_layers={network_info['layer_sizes']}, "
|
||||||
|
f"connections={network_info['total_connections']})")
|
||||||
0
world/base/neural.py
Normal file
0
world/base/neural.py
Normal file
@ -418,4 +418,4 @@ class DefaultCell(BaseEntity):
|
|||||||
position = f"({round(self.position.x, 1)}, {round(self.position.y, 1)})"
|
position = f"({round(self.position.x, 1)}, {round(self.position.y, 1)})"
|
||||||
velocity = tuple(round(value, 1) for value in self.velocity)
|
velocity = tuple(round(value, 1) for value in self.velocity)
|
||||||
acceleration = tuple(round(value, 1) for value in self.acceleration)
|
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}"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user