Add EntityConfig for entity hyperparameters and integrate into simulation configuration

This commit is contained in:
Sam 2025-11-09 18:58:09 -06:00
parent 19b946949d
commit e2d56ffb76
12 changed files with 157 additions and 89 deletions

View File

@ -5,7 +5,8 @@ from .simulation_config import (
HeadlessConfig, HeadlessConfig,
InteractiveConfig, InteractiveConfig,
ExperimentConfig, ExperimentConfig,
OutputConfig OutputConfig,
EntityConfig
) )
from .config_loader import ConfigLoader from .config_loader import ConfigLoader
@ -15,5 +16,6 @@ __all__ = [
'InteractiveConfig', 'InteractiveConfig',
'ExperimentConfig', 'ExperimentConfig',
'OutputConfig', 'OutputConfig',
'EntityConfig',
'ConfigLoader' 'ConfigLoader'
] ]

View File

@ -101,7 +101,14 @@ class ConfigLoader:
# Extract simulation config if present # Extract simulation config if present
sim_data = data.get('simulation', {}) 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( return HeadlessConfig(
max_ticks=data.get('max_ticks'), max_ticks=data.get('max_ticks'),
@ -115,7 +122,14 @@ class ConfigLoader:
"""Convert dictionary to InteractiveConfig.""" """Convert dictionary to InteractiveConfig."""
# Extract simulation config if present # Extract simulation config if present
sim_data = data.get('simulation', {}) 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( return InteractiveConfig(
window_width=data.get('window_width', 0), window_width=data.get('window_width', 0),
@ -190,5 +204,5 @@ class ConfigLoader:
print(f"Sample configs created in {output_path}") print(f"Sample configs created in {output_path}")
# Import OutputConfig for the loader # Import Config classes for the loader
from .simulation_config import OutputConfig, SimulationConfig from .simulation_config import OutputConfig, SimulationConfig, EntityConfig

View File

@ -1,21 +1,50 @@
"""Simulation configuration classes for different modes.""" """Simulation configuration classes for different modes."""
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import List, Optional from typing import List, Optional, Dict, Any
from config.constants import *
@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 @dataclass
class SimulationConfig: class SimulationConfig:
"""Configuration for simulation setup.""" """Configuration for simulation setup."""
grid_width: int = GRID_WIDTH grid_width: int = 50
grid_height: int = GRID_HEIGHT grid_height: int = 50
cell_size: int = CELL_SIZE cell_size: int = 20
initial_cells: int = 50 initial_cells: int = 50
initial_food: int = FOOD_OBJECTS_COUNT initial_food: int = 500
food_spawning: bool = FOOD_SPAWNING food_spawning: bool = True
random_seed: int = RANDOM_SEED random_seed: int = 0
default_tps: float = DEFAULT_TPS default_tps: float = 40.0
entities: EntityConfig = field(default_factory=EntityConfig)
@dataclass @dataclass
@ -44,16 +73,7 @@ class HeadlessConfig:
output: OutputConfig = field(default_factory=OutputConfig) output: OutputConfig = field(default_factory=OutputConfig)
# Simulation core config # Simulation core config
simulation: SimulationConfig = field(default_factory=lambda: SimulationConfig( simulation: SimulationConfig = field(default_factory=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
))
@dataclass @dataclass
@ -75,16 +95,7 @@ class InteractiveConfig:
console_height: int = 120 console_height: int = 120
# Simulation core config # Simulation core config
simulation: SimulationConfig = field(default_factory=lambda: SimulationConfig( simulation: SimulationConfig = field(default_factory=lambda: SimulationConfig(initial_cells=350))
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
))
@dataclass @dataclass

View File

@ -4,7 +4,6 @@
import pygame import pygame
import math import math
from config.constants import * from config.constants import *
from world.base.brain import CellBrain
from world.objects import DefaultCell, FoodObject from world.objects import DefaultCell, FoodObject

View File

@ -9,7 +9,7 @@ from world.world import World, Position, Rotation
from world.simulation_interface import Camera from world.simulation_interface import Camera
from .event_bus import EventBus, EventType, Event from .event_bus import EventBus, EventType, Event
from .timing import TimingController from .timing import TimingController
from config.constants import * from config.constants import RENDER_BUFFER
from config.simulation_config import SimulationConfig from config.simulation_config import SimulationConfig
@ -82,7 +82,8 @@ class SimulationCore:
x=random.randint(-half_width // 2, half_width // 2), x=random.randint(-half_width // 2, half_width // 2),
y=random.randint(-half_height // 2, half_height // 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 # Mutate the initial behavioral model for variety
cell.behavioral_model = cell.behavioral_model.mutate(3) cell.behavioral_model = cell.behavioral_model.mutate(3)

View File

@ -4,23 +4,22 @@ import sys
from pygame_gui import UIManager from pygame_gui import UIManager
from world.simulation_interface import Camera from config.constants import BLACK, DEFAULT_TPS, MAX_FPS
from config.constants import *
from core.input_handler import InputHandler from core.input_handler import InputHandler
from core.renderer import Renderer 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 core.event_bus import EventBus
from ui.hud import HUD from ui.hud import HUD
import cProfile import cProfile
import pstats
class SimulationEngine: class SimulationEngine:
"""Interactive simulation engine with UI (wrapper around SimulationCore).""" """Interactive simulation engine with UI (wrapper around SimulationCore)."""
def __init__(self): def __init__(self, config=None):
pygame.init() pygame.init()
self.config = config
self.event_bus = EventBus() self.event_bus = EventBus()
self._init_window() self._init_window()
self._init_simulation() self._init_simulation()
@ -47,11 +46,23 @@ class SimulationEngine:
def _init_window(self): def _init_window(self):
info = pygame.display.Info() info = pygame.display.Info()
# 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_width = int(info.current_w // 1.5)
self.window_height = int(info.current_h // 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.screen = pygame.display.set_mode(
(self.window_width, self.window_height), (self.window_width, self.window_height),
pygame.RESIZABLE, vsync=1 screen_flags, vsync=vsync
) )
pygame.display.set_caption("Dynamic Abstraction System Testing") pygame.display.set_caption("Dynamic Abstraction System Testing")
self.clock = pygame.time.Clock() self.clock = pygame.time.Clock()
@ -67,26 +78,30 @@ class SimulationEngine:
# Set HUD reference in input handler after both are created # Set HUD reference in input handler after both are created
self.input_handler.set_hud(self.hud) 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() self._update_simulation_view()
def _init_simulation(self): def _init_simulation(self):
# Initialize default sim view rect (will be updated by _init_ui) # Initialize default sim view rect (will be updated by _init_ui)
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_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_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 = pygame.Surface((self.sim_view_width, self.sim_view_height))
self.sim_view_rect = self.sim_view.get_rect(topleft=(200, 48)) # Rough estimate self.sim_view_rect = self.sim_view.get_rect(topleft=(200, 48)) # Rough estimate
# Create simulation core # Create simulation core with config
sim_config = SimulationConfig( if self.config and self.config.simulation:
grid_width=GRID_WIDTH, sim_config = self.config.simulation
grid_height=GRID_HEIGHT, else:
cell_size=CELL_SIZE, raise(ValueError("Simulation configuration must be provided for SimulationEngine."))
initial_cells=350,
initial_food=FOOD_OBJECTS_COUNT,
food_spawning=FOOD_SPAWNING,
random_seed=RANDOM_SEED,
default_tps=DEFAULT_TPS
)
self.simulation_core = SimulationCore(sim_config, self.event_bus) self.simulation_core = SimulationCore(sim_config, self.event_bus)
@ -147,7 +162,6 @@ class SimulationEngine:
def run(self): def run(self):
"""Run the interactive simulation engine.""" """Run the interactive simulation engine."""
print(f"World buffer: {self.simulation_core.world.current_buffer}")
self.simulation_core.start() self.simulation_core.start()
while self.running: while self.running:

View File

@ -2,7 +2,6 @@
import time import time
import signal import signal
import sys
from typing import Dict, Any, Optional, List from typing import Dict, Any, Optional, List
from dataclasses import dataclass from dataclasses import dataclass

View File

@ -9,7 +9,7 @@ from pathlib import Path
project_root = Path(__file__).parent.parent project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root)) sys.path.insert(0, str(project_root))
from config import ConfigLoader, InteractiveConfig from config import ConfigLoader
from core.simulation_engine import SimulationEngine from core.simulation_engine import SimulationEngine
@ -49,7 +49,7 @@ def main():
# Run simulation # Run simulation
try: try:
print("Starting interactive simulation...") print("Starting interactive simulation...")
engine = SimulationEngine() engine = SimulationEngine(config)
engine.run() engine.run()
except KeyboardInterrupt: except KeyboardInterrupt:

View File

@ -8,6 +8,7 @@ dependencies = [
"numpy>=2.3.0", "numpy>=2.3.0",
"pandas>=2.3.3", "pandas>=2.3.3",
"pre-commit>=4.2.0", "pre-commit>=4.2.0",
"psutil>=7.0.0",
"pydantic>=2.11.5", "pydantic>=2.11.5",
"pygame>=2.6.1", "pygame>=2.6.1",
"pygame-gui>=0.6.14", "pygame-gui>=0.6.14",

2
uv.lock generated
View File

@ -142,6 +142,7 @@ dependencies = [
{ name = "numpy" }, { name = "numpy" },
{ name = "pandas" }, { name = "pandas" },
{ name = "pre-commit" }, { name = "pre-commit" },
{ name = "psutil" },
{ name = "pydantic" }, { name = "pydantic" },
{ name = "pygame" }, { name = "pygame" },
{ name = "pygame-gui" }, { name = "pygame-gui" },
@ -162,6 +163,7 @@ requires-dist = [
{ name = "numpy", specifier = ">=2.3.0" }, { name = "numpy", specifier = ">=2.3.0" },
{ name = "pandas", specifier = ">=2.3.3" }, { name = "pandas", specifier = ">=2.3.3" },
{ name = "pre-commit", specifier = ">=4.2.0" }, { name = "pre-commit", specifier = ">=4.2.0" },
{ name = "psutil", specifier = ">=7.0.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" },
{ name = "pygame-gui", specifier = ">=0.6.14" }, { name = "pygame-gui", specifier = ">=0.6.14" },

View File

@ -2,17 +2,14 @@ import math
import random import random
from config.constants import MAX_VELOCITY, MAX_ACCELERATION, MAX_ROTATIONAL_VELOCITY, MAX_ANGULAR_ACCELERATION 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.base.brain import CellBrain
from world.behavioral import BehavioralModel
from world.world import Position, BaseEntity, Rotation from world.world import Position, BaseEntity, Rotation
import pygame import pygame
from typing import Optional, List, Any, Union
from world.utils import get_distance_between_objects from world.utils import get_distance_between_objects
from world.physics import Physics from world.physics import Physics
from math import atan2, degrees
class DebugRenderObject(BaseEntity): class DebugRenderObject(BaseEntity):
""" """
@ -254,15 +251,45 @@ class DefaultCell(BaseEntity):
""" """
Cell object 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. Initializes the cell.
:param starting_position: The position of the object. :param starting_position: The position of the object.
:param entity_config: Configuration for entity hyperparameters.
""" """
super().__init__(starting_position, starting_rotation) 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.velocity: tuple[int, int] = (0, 0)
self.acceleration: 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.rotational_velocity: int = 0
self.angular_acceleration: int = 0 self.angular_acceleration: int = 0
self.energy: int = 1000
self.behavioral_model: CellBrain = CellBrain() self.behavioral_model: CellBrain = CellBrain()
self.max_visual_width: int = 10
self.interaction_radius: int = 50
self.flags: dict[str, bool] = { self.flags: dict[str, bool] = {
"death": False, "death": False,
"can_interact": True, "can_interact": True,
} }
self.tick_count = 0 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: 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: if distance_to_food < self.max_visual_width and food_objects:
# Use atomic consumption to prevent race conditions # Use atomic consumption to prevent race conditions
if food_object.try_claim(): if food_object.try_claim():
self.energy += 140 self.energy += self.food_energy_value
food_object.flag_for_death() food_object.flag_for_death()
return self return self
if self.energy >= 1700: if self.energy >= self.reproduction_energy:
# too much energy, split # too much energy, split
offspring = []
for _ in range(self.reproduction_count):
duplicate_x, duplicate_y = self.position.get_position() duplicate_x, duplicate_y = self.position.get_position()
duplicate_x += random.randint(-self.max_visual_width, self.max_visual_width) duplicate_x += random.randint(-self.offspring_offset_range, self.offspring_offset_range)
duplicate_y += random.randint(-self.max_visual_width, self.max_visual_width) duplicate_y += random.randint(-self.offspring_offset_range, self.offspring_offset_range)
duplicate_x_2, duplicate_y_2 = self.position.get_position() new_cell = DefaultCell(
duplicate_x_2 += random.randint(-self.max_visual_width, self.max_visual_width) Position(x=int(duplicate_x), y=int(duplicate_y)),
duplicate_y_2 += random.randint(-self.max_visual_width, self.max_visual_width) 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 = DefaultCell(Position(x=int(duplicate_x), y=int(duplicate_y)), Rotation(angle=random.randint(0, 359))) return offspring
new_cell.set_brain(self.behavioral_model.mutate(0.05))
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]
input_data = { input_data = {
"distance": distance_to_food, "distance": distance_to_food,
@ -371,7 +396,7 @@ class DefaultCell(BaseEntity):
movement_cost = abs(output_data["angular_acceleration"]) + abs(output_data["linear_acceleration"]) 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 return self

View File

@ -1,6 +1,6 @@
from collections import defaultdict from collections import defaultdict
from abc import ABC, abstractmethod 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 from pydantic import BaseModel, Field
T = TypeVar("T", bound="BaseEntity") T = TypeVar("T", bound="BaseEntity")