From e2d56ffb769a4879fac015d9cc36e95967e77dd6 Mon Sep 17 00:00:00 2001 From: Sam Date: Sun, 9 Nov 2025 18:58:09 -0600 Subject: [PATCH] Add EntityConfig for entity hyperparameters and integrate into simulation configuration --- config/__init__.py | 4 +- config/config_loader.py | 22 +++++++-- config/simulation_config.py | 69 ++++++++++++++++------------ core/renderer.py | 1 - core/simulation_core.py | 5 ++- core/simulation_engine.py | 58 +++++++++++++++--------- engines/headless_engine.py | 1 - entry_points/interactive_main.py | 4 +- pyproject.toml | 1 + uv.lock | 2 + world/objects.py | 77 +++++++++++++++++++++----------- world/world.py | 2 +- 12 files changed, 157 insertions(+), 89 deletions(-) diff --git a/config/__init__.py b/config/__init__.py index e159e2e..9e71b5f 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -5,7 +5,8 @@ from .simulation_config import ( HeadlessConfig, InteractiveConfig, ExperimentConfig, - OutputConfig + OutputConfig, + EntityConfig ) from .config_loader import ConfigLoader @@ -15,5 +16,6 @@ __all__ = [ 'InteractiveConfig', 'ExperimentConfig', 'OutputConfig', + 'EntityConfig', 'ConfigLoader' ] \ No newline at end of file diff --git a/config/config_loader.py b/config/config_loader.py index bc33253..6efb007 100644 --- a/config/config_loader.py +++ b/config/config_loader.py @@ -101,7 +101,14 @@ class ConfigLoader: # Extract simulation config if present sim_data = data.get('simulation', {}) - simulation_config = SimulationConfig(**sim_data) + + # Extract entities config if present + entities_data = sim_data.get('entities', {}) + entities_config = EntityConfig(**entities_data) + + # Create simulation config with entities + sim_data_without_entities = {k: v for k, v in sim_data.items() if k != 'entities'} + simulation_config = SimulationConfig(entities=entities_config, **sim_data_without_entities) return HeadlessConfig( max_ticks=data.get('max_ticks'), @@ -115,7 +122,14 @@ class ConfigLoader: """Convert dictionary to InteractiveConfig.""" # Extract simulation config if present sim_data = data.get('simulation', {}) - simulation_config = SimulationConfig(**sim_data) + + # Extract entities config if present + entities_data = sim_data.get('entities', {}) + entities_config = EntityConfig(**entities_data) + + # Create simulation config with entities + sim_data_without_entities = {k: v for k, v in sim_data.items() if k != 'entities'} + simulation_config = SimulationConfig(entities=entities_config, **sim_data_without_entities) return InteractiveConfig( window_width=data.get('window_width', 0), @@ -190,5 +204,5 @@ class ConfigLoader: print(f"Sample configs created in {output_path}") -# Import OutputConfig for the loader -from .simulation_config import OutputConfig, SimulationConfig \ No newline at end of file +# Import Config classes for the loader +from .simulation_config import OutputConfig, SimulationConfig, EntityConfig \ No newline at end of file diff --git a/config/simulation_config.py b/config/simulation_config.py index 2402489..5d9da4b 100644 --- a/config/simulation_config.py +++ b/config/simulation_config.py @@ -1,21 +1,50 @@ """Simulation configuration classes for different modes.""" from dataclasses import dataclass, field -from typing import List, Optional -from config.constants import * +from typing import List, Optional, Dict, Any + + +@dataclass +class EntityConfig: + """Configuration for entity hyperparameters.""" + + # Global entity settings (apply to all entity types unless overridden) + max_acceleration: float = 0.125 + max_angular_acceleration: float = 0.25 + max_velocity: float = 1.0 + max_rotational_velocity: float = 3.0 + + # Entity type specific configs (all entity parameters should be defined here) + entity_types: Dict[str, Dict[str, Any]] = field(default_factory=lambda: { + "default_cell": { + "reproduction_energy": 1700, + "starting_energy": 1000, + "interaction_radius": 50, + "drag_coefficient": 0.02, + "energy_cost_base": 1.5, + "neural_network_complexity_cost": 0.08, + "movement_cost": 0.25, + "food_energy_value": 140, + "max_visual_width": 10, + "reproduction_count": 2, + "mutation_rate": 0.05, + "offspring_offset_range": 10 + } + }) @dataclass class SimulationConfig: """Configuration for simulation setup.""" - grid_width: int = GRID_WIDTH - grid_height: int = GRID_HEIGHT - cell_size: int = CELL_SIZE + grid_width: int = 50 + grid_height: int = 50 + cell_size: int = 20 initial_cells: int = 50 - initial_food: int = FOOD_OBJECTS_COUNT - food_spawning: bool = FOOD_SPAWNING - random_seed: int = RANDOM_SEED - default_tps: float = DEFAULT_TPS + initial_food: int = 500 + food_spawning: bool = True + random_seed: int = 0 + default_tps: float = 40.0 + entities: EntityConfig = field(default_factory=EntityConfig) @dataclass @@ -44,16 +73,7 @@ class HeadlessConfig: output: OutputConfig = field(default_factory=OutputConfig) # Simulation core config - simulation: SimulationConfig = field(default_factory=lambda: SimulationConfig( - grid_width=GRID_WIDTH, - grid_height=GRID_HEIGHT, - cell_size=CELL_SIZE, - initial_cells=50, - initial_food=FOOD_OBJECTS_COUNT, - food_spawning=FOOD_SPAWNING, - random_seed=RANDOM_SEED, - default_tps=DEFAULT_TPS - )) + simulation: SimulationConfig = field(default_factory=SimulationConfig) @dataclass @@ -75,16 +95,7 @@ class InteractiveConfig: console_height: int = 120 # Simulation core config - simulation: SimulationConfig = field(default_factory=lambda: SimulationConfig( - grid_width=GRID_WIDTH, - grid_height=GRID_HEIGHT, - cell_size=CELL_SIZE, - initial_cells=350, - initial_food=FOOD_OBJECTS_COUNT, - food_spawning=FOOD_SPAWNING, - random_seed=RANDOM_SEED, - default_tps=DEFAULT_TPS - )) + simulation: SimulationConfig = field(default_factory=lambda: SimulationConfig(initial_cells=350)) @dataclass diff --git a/core/renderer.py b/core/renderer.py index 30f030c..6841a2d 100644 --- a/core/renderer.py +++ b/core/renderer.py @@ -4,7 +4,6 @@ import pygame import math from config.constants import * -from world.base.brain import CellBrain from world.objects import DefaultCell, FoodObject diff --git a/core/simulation_core.py b/core/simulation_core.py index b282bad..4784e56 100644 --- a/core/simulation_core.py +++ b/core/simulation_core.py @@ -9,7 +9,7 @@ from world.world import World, Position, Rotation from world.simulation_interface import Camera from .event_bus import EventBus, EventType, Event from .timing import TimingController -from config.constants import * +from config.constants import RENDER_BUFFER from config.simulation_config import SimulationConfig @@ -82,7 +82,8 @@ class SimulationCore: x=random.randint(-half_width // 2, half_width // 2), y=random.randint(-half_height // 2, half_height // 2) ), - Rotation(angle=0) + Rotation(angle=0), + entity_config=getattr(self.config, 'entities', None) ) # Mutate the initial behavioral model for variety cell.behavioral_model = cell.behavioral_model.mutate(3) diff --git a/core/simulation_engine.py b/core/simulation_engine.py index cf3a6f8..a88c310 100644 --- a/core/simulation_engine.py +++ b/core/simulation_engine.py @@ -4,23 +4,22 @@ import sys from pygame_gui import UIManager -from world.simulation_interface import Camera -from config.constants import * +from config.constants import BLACK, DEFAULT_TPS, MAX_FPS from core.input_handler import InputHandler from core.renderer import Renderer -from core.simulation_core import SimulationCore, SimulationConfig +from core.simulation_core import SimulationCore from core.event_bus import EventBus from ui.hud import HUD import cProfile -import pstats class SimulationEngine: """Interactive simulation engine with UI (wrapper around SimulationCore).""" - def __init__(self): + def __init__(self, config=None): pygame.init() + self.config = config self.event_bus = EventBus() self._init_window() self._init_simulation() @@ -47,11 +46,23 @@ class SimulationEngine: def _init_window(self): info = pygame.display.Info() - self.window_width = int(info.current_w // 1.5) - self.window_height = int(info.current_h // 1.5) + + # Use config or defaults + if self.config: + self.window_width = self.config.window_width or int(info.current_w // 1.5) + self.window_height = self.config.window_height or int(info.current_h // 1.5) + vsync = 1 if self.config.vsync else 0 + resizable = self.config.resizable + else: + self.window_width = int(info.current_w // 1.5) + self.window_height = int(info.current_h // 1.5) + vsync = 1 + resizable = True + + screen_flags = pygame.RESIZABLE if resizable else 0 self.screen = pygame.display.set_mode( (self.window_width, self.window_height), - pygame.RESIZABLE, vsync=1 + screen_flags, vsync=vsync ) pygame.display.set_caption("Dynamic Abstraction System Testing") self.clock = pygame.time.Clock() @@ -67,26 +78,30 @@ class SimulationEngine: # Set HUD reference in input handler after both are created self.input_handler.set_hud(self.hud) + # Pass config settings to HUD and input handler + if self.config: + self.input_handler.show_grid = self.config.show_grid + self.input_handler.show_interaction_radius = self.config.show_interaction_radius + self.input_handler.show_legend = self.config.show_legend + self._update_simulation_view() def _init_simulation(self): # Initialize default sim view rect (will be updated by _init_ui) - self.sim_view_width = self.window_width - 400 # Rough estimate for inspector width - self.sim_view_height = self.window_height - 200 # Rough estimate for control bar height + if self.config: + self.sim_view_width = self.window_width - self.config.inspector_width + self.sim_view_height = self.window_height - self.config.control_bar_height + else: + self.sim_view_width = self.window_width - 400 # Rough estimate for inspector width + self.sim_view_height = self.window_height - 200 # Rough estimate for control bar height self.sim_view = pygame.Surface((self.sim_view_width, self.sim_view_height)) self.sim_view_rect = self.sim_view.get_rect(topleft=(200, 48)) # Rough estimate - # Create simulation core - sim_config = SimulationConfig( - grid_width=GRID_WIDTH, - grid_height=GRID_HEIGHT, - cell_size=CELL_SIZE, - initial_cells=350, - initial_food=FOOD_OBJECTS_COUNT, - food_spawning=FOOD_SPAWNING, - random_seed=RANDOM_SEED, - default_tps=DEFAULT_TPS - ) + # Create simulation core with config + if self.config and self.config.simulation: + sim_config = self.config.simulation + else: + raise(ValueError("Simulation configuration must be provided for SimulationEngine.")) self.simulation_core = SimulationCore(sim_config, self.event_bus) @@ -147,7 +162,6 @@ class SimulationEngine: def run(self): """Run the interactive simulation engine.""" - print(f"World buffer: {self.simulation_core.world.current_buffer}") self.simulation_core.start() while self.running: diff --git a/engines/headless_engine.py b/engines/headless_engine.py index 15a4f8c..dfd0749 100644 --- a/engines/headless_engine.py +++ b/engines/headless_engine.py @@ -2,7 +2,6 @@ import time import signal -import sys from typing import Dict, Any, Optional, List from dataclasses import dataclass diff --git a/entry_points/interactive_main.py b/entry_points/interactive_main.py index bdbe657..30ec725 100644 --- a/entry_points/interactive_main.py +++ b/entry_points/interactive_main.py @@ -9,7 +9,7 @@ from pathlib import Path project_root = Path(__file__).parent.parent sys.path.insert(0, str(project_root)) -from config import ConfigLoader, InteractiveConfig +from config import ConfigLoader from core.simulation_engine import SimulationEngine @@ -49,7 +49,7 @@ def main(): # Run simulation try: print("Starting interactive simulation...") - engine = SimulationEngine() + engine = SimulationEngine(config) engine.run() except KeyboardInterrupt: diff --git a/pyproject.toml b/pyproject.toml index bf9e2e7..a9d9610 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,7 @@ dependencies = [ "numpy>=2.3.0", "pandas>=2.3.3", "pre-commit>=4.2.0", + "psutil>=7.0.0", "pydantic>=2.11.5", "pygame>=2.6.1", "pygame-gui>=0.6.14", diff --git a/uv.lock b/uv.lock index 8600034..1b83902 100644 --- a/uv.lock +++ b/uv.lock @@ -142,6 +142,7 @@ dependencies = [ { name = "numpy" }, { name = "pandas" }, { name = "pre-commit" }, + { name = "psutil" }, { name = "pydantic" }, { name = "pygame" }, { name = "pygame-gui" }, @@ -162,6 +163,7 @@ requires-dist = [ { name = "numpy", specifier = ">=2.3.0" }, { name = "pandas", specifier = ">=2.3.3" }, { name = "pre-commit", specifier = ">=4.2.0" }, + { name = "psutil", specifier = ">=7.0.0" }, { name = "pydantic", specifier = ">=2.11.5" }, { name = "pygame", specifier = ">=2.6.1" }, { name = "pygame-gui", specifier = ">=0.6.14" }, diff --git a/world/objects.py b/world/objects.py index b353e75..3069f56 100644 --- a/world/objects.py +++ b/world/objects.py @@ -2,17 +2,14 @@ import math import random from config.constants import MAX_VELOCITY, MAX_ACCELERATION, MAX_ROTATIONAL_VELOCITY, MAX_ANGULAR_ACCELERATION +from typing import List, Any, Union, Optional from world.base.brain import CellBrain -from world.behavioral import BehavioralModel from world.world import Position, BaseEntity, Rotation import pygame -from typing import Optional, List, Any, Union from world.utils import get_distance_between_objects from world.physics import Physics -from math import atan2, degrees - class DebugRenderObject(BaseEntity): """ @@ -254,15 +251,45 @@ class DefaultCell(BaseEntity): """ Cell object """ - def __init__(self, starting_position: Position, starting_rotation: Rotation) -> None: + def __init__(self, starting_position: Position, starting_rotation: Rotation, entity_config=None) -> None: """ Initializes the cell. :param starting_position: The position of the object. + :param entity_config: Configuration for entity hyperparameters. """ super().__init__(starting_position, starting_rotation) - self.drag_coefficient: float = 0.1 + + # Use entity config or defaults + if entity_config: + cell_config = entity_config.entity_types.get("default_cell", {}) + self.drag_coefficient: float = cell_config.get("drag_coefficient", 0.02) + self.energy: int = cell_config.get("starting_energy", 1000) + self.max_visual_width: int = cell_config.get("max_visual_width", 10) + self.interaction_radius: int = cell_config.get("interaction_radius", 50) + self.reproduction_energy: int = cell_config.get("reproduction_energy", 1700) + self.food_energy_value: int = cell_config.get("food_energy_value", 140) + self.energy_cost_base: float = cell_config.get("energy_cost_base", 1.5) + self.neural_network_complexity_cost: float = cell_config.get("neural_network_complexity_cost", 0.08) + self.movement_cost: float = cell_config.get("movement_cost", 0.25) + self.reproduction_count: int = cell_config.get("reproduction_count", 2) + self.mutation_rate: float = cell_config.get("mutation_rate", 0.05) + self.offspring_offset_range: int = cell_config.get("offspring_offset_range", 10) + else: + # Fallback to hardcoded defaults + self.drag_coefficient: float = 0.02 + self.energy: int = 1000 + self.max_visual_width: int = 10 + self.interaction_radius: int = 50 + self.reproduction_energy: int = 1700 + self.food_energy_value: int = 140 + self.energy_cost_base: float = 1.5 + self.neural_network_complexity_cost: float = 0.08 + self.movement_cost: float = 0.25 + self.reproduction_count: int = 2 + self.mutation_rate: float = 0.05 + self.offspring_offset_range: int = 10 self.velocity: tuple[int, int] = (0, 0) self.acceleration: tuple[int, int] = (0, 0) @@ -270,20 +297,17 @@ class DefaultCell(BaseEntity): self.rotational_velocity: int = 0 self.angular_acceleration: int = 0 - self.energy: int = 1000 - self.behavioral_model: CellBrain = CellBrain() - self.max_visual_width: int = 10 - self.interaction_radius: int = 50 self.flags: dict[str, bool] = { "death": False, "can_interact": True, } self.tick_count = 0 + self.entity_config = entity_config - self.physics = Physics(0.02, 0.05) + self.physics = Physics(self.drag_coefficient, self.drag_coefficient*1.5) def set_brain(self, behavioral_model: CellBrain) -> None: @@ -321,27 +345,28 @@ class DefaultCell(BaseEntity): if distance_to_food < self.max_visual_width and food_objects: # Use atomic consumption to prevent race conditions if food_object.try_claim(): - self.energy += 140 + self.energy += self.food_energy_value food_object.flag_for_death() return self - if self.energy >= 1700: + if self.energy >= self.reproduction_energy: # too much energy, split - duplicate_x, duplicate_y = self.position.get_position() - duplicate_x += random.randint(-self.max_visual_width, self.max_visual_width) - duplicate_y += random.randint(-self.max_visual_width, self.max_visual_width) + offspring = [] - duplicate_x_2, duplicate_y_2 = self.position.get_position() - duplicate_x_2 += random.randint(-self.max_visual_width, self.max_visual_width) - duplicate_y_2 += random.randint(-self.max_visual_width, self.max_visual_width) + for _ in range(self.reproduction_count): + duplicate_x, duplicate_y = self.position.get_position() + duplicate_x += random.randint(-self.offspring_offset_range, self.offspring_offset_range) + duplicate_y += random.randint(-self.offspring_offset_range, self.offspring_offset_range) - new_cell = DefaultCell(Position(x=int(duplicate_x), y=int(duplicate_y)), Rotation(angle=random.randint(0, 359))) - new_cell.set_brain(self.behavioral_model.mutate(0.05)) + new_cell = DefaultCell( + Position(x=int(duplicate_x), y=int(duplicate_y)), + Rotation(angle=random.randint(0, 359)), + entity_config=getattr(self, 'entity_config', None) + ) + new_cell.set_brain(self.behavioral_model.mutate(self.mutation_rate)) + offspring.append(new_cell) - new_cell_2 = DefaultCell(Position(x=int(duplicate_x_2), y=int(duplicate_y_2)), Rotation(angle=random.randint(0, 359))) - new_cell_2.set_brain(self.behavioral_model.mutate(0.05)) - - return [new_cell, new_cell_2] + return offspring input_data = { "distance": distance_to_food, @@ -371,7 +396,7 @@ class DefaultCell(BaseEntity): movement_cost = abs(output_data["angular_acceleration"]) + abs(output_data["linear_acceleration"]) - self.energy -= (self.behavioral_model.neural_network.network_cost * 0.08) + 1.5 + (0.25 * movement_cost) + self.energy -= (self.behavioral_model.neural_network.network_cost * self.neural_network_complexity_cost) + self.energy_cost_base + (self.movement_cost * movement_cost) return self diff --git a/world/world.py b/world/world.py index 5731985..ebb3284 100644 --- a/world/world.py +++ b/world/world.py @@ -1,6 +1,6 @@ from collections import defaultdict from abc import ABC, abstractmethod -from typing import List, Dict, Tuple, Optional, Any, TypeVar, Union +from typing import List, Dict, Tuple, Optional, Any, TypeVar from pydantic import BaseModel, Field T = TypeVar("T", bound="BaseEntity")