From ca645167d24e3a84a8465e3c05c1bd2b7da83d9f Mon Sep 17 00:00:00 2001 From: Daniel Ari Friedman Date: Fri, 7 Feb 2025 13:49:44 -0800 Subject: [PATCH] In Stream --- .obsidian/graph.json | 2 +- .obsidian/workspace.json | 73 ++-- .../Ant_Colony/ant_colony/agents/nestmate.py | 36 +- Things/Ant_Colony/ant_colony/colony.py | 35 +- .../ant_colony/environment/world.py | 9 + Things/Ant_Colony/ant_colony/simulation.py | 30 +- .../ant_colony/utils/data_collection.py | 10 +- .../ant_colony/visualization/renderer.py | 40 +- .../output/simulation_20250207_132721.h5 | Bin 0 -> 6240 bytes .../output/simulation_20250207_132802.h5 | Bin 0 -> 6240 bytes Things/Path_Network/LICENSE | 21 + Things/Path_Network/Path_Network_README.md | 137 ++++++ Things/Path_Network/README.md | 114 +++++ Things/Path_Network/config.yaml | 91 ++++ Things/Path_Network/example.py | 228 ++++++++++ .../path_network/config/__init__.py | 0 .../path_network/core/__init__.py | 0 .../Path_Network/path_network/core/agent.py | 112 +++++ .../path_network/core/dynamics.py | 126 ++++++ .../Path_Network/path_network/core/network.py | 173 ++++++++ .../path_network/simulation/__init__.py | 0 .../path_network/simulation/runner.py | 120 ++++++ .../path_network/utils/__init__.py | 0 .../utils/advanced_visualization.py | 390 ++++++++++++++++++ .../path_network/utils/math_utils.py | 150 +++++++ .../path_network/utils/visualization.py | 233 +++++++++++ Things/Path_Network/requirements.txt | 19 + Things/Path_Network/run_simulation.sh | 166 ++++++++ ant_colony/agents/nestmate.py | 68 +++ ant_colony/simulation.py | 18 + requirements.txt | 14 +- 31 files changed, 2343 insertions(+), 72 deletions(-) create mode 100644 Things/Ant_Colony/output/simulation_20250207_132721.h5 create mode 100644 Things/Ant_Colony/output/simulation_20250207_132802.h5 create mode 100644 Things/Path_Network/LICENSE create mode 100644 Things/Path_Network/Path_Network_README.md create mode 100644 Things/Path_Network/README.md create mode 100644 Things/Path_Network/config.yaml create mode 100644 Things/Path_Network/example.py create mode 100644 Things/Path_Network/path_network/config/__init__.py create mode 100644 Things/Path_Network/path_network/core/__init__.py create mode 100644 Things/Path_Network/path_network/core/agent.py create mode 100644 Things/Path_Network/path_network/core/dynamics.py create mode 100644 Things/Path_Network/path_network/core/network.py create mode 100644 Things/Path_Network/path_network/simulation/__init__.py create mode 100644 Things/Path_Network/path_network/simulation/runner.py create mode 100644 Things/Path_Network/path_network/utils/__init__.py create mode 100644 Things/Path_Network/path_network/utils/advanced_visualization.py create mode 100644 Things/Path_Network/path_network/utils/math_utils.py create mode 100644 Things/Path_Network/path_network/utils/visualization.py create mode 100644 Things/Path_Network/requirements.txt create mode 100755 Things/Path_Network/run_simulation.sh create mode 100644 ant_colony/agents/nestmate.py create mode 100644 ant_colony/simulation.py diff --git a/.obsidian/graph.json b/.obsidian/graph.json index 29bb70f..0012d93 100644 --- a/.obsidian/graph.json +++ b/.obsidian/graph.json @@ -17,6 +17,6 @@ "repelStrength": 10, "linkStrength": 1, "linkDistance": 250, - "scale": 2.2318821973563394, + "scale": 0.10124266675309304, "close": false } \ No newline at end of file diff --git a/.obsidian/workspace.json b/.obsidian/workspace.json index 9fa563e..2da99f9 100644 --- a/.obsidian/workspace.json +++ b/.obsidian/workspace.json @@ -6,6 +6,7 @@ { "id": "dea337cf76935a08", "type": "tabs", + "dimension": 37.73006134969325, "children": [ { "id": "0ba8a1b9b9dd949a", @@ -26,19 +27,16 @@ { "id": "99b33ec97f002ea4", "type": "tabs", + "dimension": 62.26993865030674, "children": [ { "id": "283b4112120e60a0", "type": "leaf", "state": { - "type": "markdown", - "state": { - "file": "Things/BioFirm/BioFirm_README.md", - "mode": "source", - "source": false - }, - "icon": "lucide-file", - "title": "BioFirm_README" + "type": "graph", + "state": {}, + "icon": "lucide-git-fork", + "title": "Graph view" } } ] @@ -114,7 +112,6 @@ "state": { "type": "backlink", "state": { - "file": "Things/Continuous_Generic/Continuous_Generic_README.md", "collapseAll": false, "extraContext": false, "sortOrder": "alphabetical", @@ -124,7 +121,7 @@ "unlinkedCollapsed": true }, "icon": "links-coming-in", - "title": "Backlinks for Continuous_Generic_README" + "title": "Backlinks" } }, { @@ -133,12 +130,11 @@ "state": { "type": "outgoing-link", "state": { - "file": "Things/Continuous_Generic/Continuous_Generic_README.md", "linksCollapsed": false, "unlinkedCollapsed": true }, "icon": "links-going-out", - "title": "Outgoing links from Continuous_Generic_README" + "title": "Outgoing links" } }, { @@ -162,13 +158,12 @@ "state": { "type": "outline", "state": { - "file": "Things/Continuous_Generic/Continuous_Generic_README.md", "followCursor": false, "showSearch": false, "searchQuery": "" }, "icon": "lucide-list", - "title": "Outline of Continuous_Generic_README" + "title": "Outline" } } ] @@ -188,32 +183,34 @@ "command-palette:Open command palette": false } }, - "active": "0ba8a1b9b9dd949a", + "active": "283b4112120e60a0", "lastOpenFiles": [ - "ant_colony/agents/nestmate.py", - "ant_colony/agents", - "ant_colony", - "Things/Ant_Colony/ant_colony/__pycache__/main.cpython-310.pyc", - "Things/Ant_Colony/ant_colony/utils/__pycache__/data_collection.cpython-310.pyc", - "Things/Ant_Colony/ant_colony/utils/__pycache__/__init__.cpython-310.pyc", - "Things/Ant_Colony/ant_colony/utils/__pycache__", - "Things/Ant_Colony/ant_colony/environment/__pycache__/world.cpython-310.pyc", - "Things/Ant_Colony/ant_colony/utils/__init__.py", - "Things/Ant_Colony/ant_colony/utils/data_collection.py", - "Things/Ant_Colony/ant_colony/utils", + "Things/Path_Network/venv/lib/python3.10/site-packages/pip-25.0.dist-info/top_level.txt", + "Things/Path_Network/venv/lib/python3.10/site-packages/pip-25.0.dist-info/entry_points.txt", + "Things/Path_Network/venv/lib/python3.10/site-packages/pip-25.0.dist-info/WHEEL", + "Things/Path_Network/venv/lib/python3.10/site-packages/pip-25.0.dist-info/REQUESTED", + "Things/Path_Network/venv/lib/python3.10/site-packages/pip-25.0.dist-info/RECORD", + "Things/Path_Network/venv/lib/python3.10/site-packages/pip-25.0.dist-info/METADATA", + "Things/Path_Network/venv/lib/python3.10/site-packages/pip-25.0.dist-info/LICENSE.txt", + "Things/Path_Network/venv/lib/python3.10/site-packages/pip-25.0.dist-info/AUTHORS.txt", + "Things/Path_Network/venv/lib/python3.10/site-packages/pip-25.0.dist-info/INSTALLER", + "Things/Path_Network/venv/lib/python3.10/site-packages/pip-25.0.dist-info", + "Things/Path_Network/venv/bin/Activate.ps1", + "Things/Path_Network/output/20250207_134114/agent_height_variations.png", + "Things/Path_Network/output/20250207_134114/water_level_distribution.png", + "Things/Path_Network/output/20250207_134114/final_state.png", + "Things/Path_Network/output/20250207_134114/initial_state.png", + "Things/Path_Network/output/20250207_134014/agent_height_variations.png", + "Things/Path_Network/output/20250207_134014/water_level_distribution.png", + "Things/Path_Network/output/20250207_134014/final_state.png", + "Things/Path_Network/output/20250207_134014/initial_state.png", + "Things/Path_Network/README.md", + "Things/Path_Network/Path_Network_README.md", + "Things/BioFirm/BioFirm_README.md", + "Things/Continuous_Generic/Continuous_Generic_README.md", "Output/tests/visualization/detailed_summary.png", "Output/tests/visualization/summary.png", - "Output/tests/visualization/action_phase_space.png", - "Output/tests/visualization/action_evolution.png", - "Output/tests/visualization/free_energy_landscape.png", - "Output/tests/visualization/belief_animation.gif", - "Things/BioFirm/BioFirm_README.md", "knowledge_base/cognitive/continuous_time_active_inference.md", - "Output/tests/visualization/generalized_coordinates.png", - "Output/tests/visualization/energy_ratio.png", - "Output/tests/visualization/energy_components.png", - "Output/tests/visualization/phase_space_energy.png", - "Things/Continuous_Generic/Continuous_Generic_README.md", "Things/Generic_POMDP/Generic_POMDP_README.md", "src/models/active_inference/docs/biofirm_schema.md", "knowledge_base/agents/GenericPOMDP/matrices/A_matrix.md", @@ -234,8 +231,6 @@ "knowledge_base/mathematics/expected_free_energy.md", "knowledge_base/mathematics/active_inference_pomdp.md", "knowledge_base/mathematics/compute_efe.md", - "knowledge_base/mathematics/belief_updating.md", - "knowledge_base/cognitive/perceptual_inference.md", - "knowledge_base/cognitive/metacognition.md" + "knowledge_base/mathematics/belief_updating.md" ] } \ No newline at end of file diff --git a/Things/Ant_Colony/ant_colony/agents/nestmate.py b/Things/Ant_Colony/ant_colony/agents/nestmate.py index cca6be8..7f533c1 100644 --- a/Things/Ant_Colony/ant_colony/agents/nestmate.py +++ b/Things/Ant_Colony/ant_colony/agents/nestmate.py @@ -34,6 +34,9 @@ class Nestmate: """Initialize the agent.""" self.config = config + # Initialize beliefs first + self.beliefs = Belief() + # Physical state self.position = None self.orientation = 0.0 @@ -47,6 +50,13 @@ class Nestmate: self.current_task = TaskType.EXPLORATION self.task_time = 0.0 + # Memory state + self.memory = { + 'temporal': [], # List of temporal memories (past states/actions) + 'spatial': {}, # Dict of spatial memories (locations) + 'social': {} # Dict of social memories (interactions) + } + # Sensory state self.observations = { 'pheromones': {}, @@ -56,7 +66,6 @@ class Nestmate: } # Internal model - self.beliefs = Belief() self.preferences = {task: 1.0 for task in TaskType} # Learning parameters @@ -77,6 +86,31 @@ class Nestmate: # Select and execute actions self._select_actions() + # Update memory with current state + self._update_memory() + + def _update_memory(self) -> None: + """Update agent's memory with current state.""" + # Add current state to temporal memory + current_state = { + 'state': self.current_task, + 'position': self.position, + 'energy': self.energy, + 'time': self.task_time + } + self.memory['temporal'].append(current_state) + + # Limit memory size to prevent unbounded growth + max_memory = 100 + if len(self.memory['temporal']) > max_memory: + self.memory['temporal'] = self.memory['temporal'][-max_memory:] + + # Update spatial memory if we found something interesting + if self.beliefs.food_location: + self.memory['spatial']['food'] = self.beliefs.food_location + + # Could add more memory updates here (social interactions, etc.) + def _update_physical_state(self, dt: float) -> None: """Update physical state of the agent.""" # Update position based on velocity and orientation diff --git a/Things/Ant_Colony/ant_colony/colony.py b/Things/Ant_Colony/ant_colony/colony.py index be7a510..a49d737 100644 --- a/Things/Ant_Colony/ant_colony/colony.py +++ b/Things/Ant_Colony/ant_colony/colony.py @@ -10,8 +10,8 @@ import numpy as np from typing import Dict, List, Tuple, Optional from dataclasses import dataclass import networkx as nx -from agents.nestmate import Nestmate, TaskType -from environment.world import World, Position, Resource +from ant_colony.agents.nestmate import Nestmate, TaskType +from ant_colony.environment.world import World, Position, Resource @dataclass class ColonyStats: @@ -46,6 +46,10 @@ class Colony: 'building_materials': 0.0 } + # Initialize task needs and allocation + self.task_needs = {task: 0.0 for task in TaskType} + self.task_allocation = {task: set() for task in TaskType} + # Initialize agents self.agents: List[Nestmate] = [] self._initialize_agents() @@ -91,6 +95,8 @@ class Colony: agent = Nestmate(agent_config) agent.position = pos + + # Initialize beliefs after creating agent agent.beliefs.nest_location = self.nest_position self.agents.append(agent) @@ -181,11 +187,19 @@ class Colony: 'energy_efficiency': self._compute_energy_efficiency() } + # Compute coordination metrics + coordination = { + 'task_specialization': self._calculate_specialization(), + 'social_network_density': nx.density(self.interaction_network), + 'task_balance': self._compute_task_balance(task_dist) + } + return ColonyStats( population=len(self.agents), task_distribution=task_dist, resource_levels=self.resources.copy(), - efficiency_metrics=efficiency + efficiency_metrics=efficiency, + coordination_metrics=coordination ) def _compute_food_efficiency(self) -> float: @@ -459,4 +473,17 @@ class Colony: break if agent not in current_defenders: agent.current_task = TaskType.DEFENSE - current_defenders.add(agent) \ No newline at end of file + current_defenders.add(agent) + + def _compute_task_balance(self, task_dist: Dict[TaskType, int]) -> float: + """Compute how well-balanced the task distribution is.""" + if not self.agents: + return 0.0 + + # Ideal distribution would be uniform across tasks + ideal_per_task = len(self.agents) / len(TaskType) + deviations = [abs(count - ideal_per_task) for count in task_dist.values()] + max_deviation = len(self.agents) # Worst case: all agents on one task + + # Return normalized score (1.0 = perfect balance, 0.0 = worst balance) + return 1.0 - sum(deviations) / (2 * max_deviation) \ No newline at end of file diff --git a/Things/Ant_Colony/ant_colony/environment/world.py b/Things/Ant_Colony/ant_colony/environment/world.py index 4f2f556..994f9af 100644 --- a/Things/Ant_Colony/ant_colony/environment/world.py +++ b/Things/Ant_Colony/ant_colony/environment/world.py @@ -78,6 +78,7 @@ class World: for _ in range(self.config['food_sources']['count']): pos = self._random_position() amount = np.random.uniform(*self.config['food_sources']['value_range']) + size = np.random.uniform(*self.config['food_sources']['size_range']) resource = Resource( position=pos, @@ -137,6 +138,14 @@ class World: resource.max_amount ) + def get_state(self) -> dict: + """Get the current state of the world.""" + return { + 'pheromones': self.pheromones, + 'resources': [resource for resources in self.resources.values() for resource in resources], + 'terrain': self.terrain + } + def get_local_state(self, position: Position, radius: float) -> dict: """Get the local state around a position within given radius.""" x, y = position.x, position.y diff --git a/Things/Ant_Colony/ant_colony/simulation.py b/Things/Ant_Colony/ant_colony/simulation.py index 1c39376..5638090 100644 --- a/Things/Ant_Colony/ant_colony/simulation.py +++ b/Things/Ant_Colony/ant_colony/simulation.py @@ -6,6 +6,8 @@ import yaml import numpy as np from pathlib import Path from typing import Optional +import matplotlib +matplotlib.use('Agg') # Use non-GUI backend import matplotlib.pyplot as plt from ant_colony.environment import World @@ -27,8 +29,18 @@ class Simulation: # Initialize components self.environment = World(self.config['environment']) self.colony = Colony(self.config['colony'], self.environment) - self.visualizer = ColonyVisualizer(self.config['visualization']) - self.data_collector = DataCollector(self.config['data_collection']) + + # Initialize visualization if enabled + if self.config.get('visualization', {}).get('enabled', True): + self.visualizer = ColonyVisualizer(self.config) + else: + self.visualizer = None + + # Initialize data collection if enabled + if self.config.get('data_collection', {}).get('enabled', True): + self.data_collector = DataCollector(self.config['data_collection']) + else: + self.data_collector = None # Simulation state self.current_step = 0 @@ -55,7 +67,7 @@ class Simulation: self.colony.update(dt) # Update visualization if enabled - if self.config['visualization']['enabled']: + if self.visualizer is not None: world_state = { 'agents': self.colony.agents, 'resources': self.environment.resources, @@ -64,8 +76,8 @@ class Simulation: } self.visualizer.update(world_state) - # Collect data - if self.config['data_collection']['enabled']: + # Collect data if enabled + if self.data_collector is not None: self.data_collector.collect(self.current_time, world_state) # Save data periodically @@ -89,7 +101,7 @@ class Simulation: print(f"Step {self.current_step}/{max_steps}") # Update visualization - if not headless and self.config['visualization']['enabled']: + if not headless and self.visualizer is not None: plt.pause(0.001) # Allow GUI to update except KeyboardInterrupt: @@ -97,11 +109,11 @@ class Simulation: finally: # Save final data - if self.config['data_collection']['enabled']: + if self.data_collector is not None: self.data_collector.save_data() # Show final visualization - if not headless and self.config['visualization']['enabled']: + if not headless and self.visualizer is not None: plt.show() def pause(self) -> None: @@ -123,5 +135,5 @@ class Simulation: self.colony = Colony(self.config['colony'], self.environment) # Clear data - if self.config['data_collection']['enabled']: + if self.data_collector is not None: self.data_collector = DataCollector(self.config['data_collection']) \ No newline at end of file diff --git a/Things/Ant_Colony/ant_colony/utils/data_collection.py b/Things/Ant_Colony/ant_colony/utils/data_collection.py index 2c426f7..e8986bd 100644 --- a/Things/Ant_Colony/ant_colony/utils/data_collection.py +++ b/Things/Ant_Colony/ant_colony/utils/data_collection.py @@ -134,13 +134,13 @@ class DataCollector: 'run_id': self.run_id, 'duration': len(self.data['time']), 'colony_stats': { - 'mean_population': float(colony_stats['population'].mean()), - 'peak_population': int(colony_stats['population'].max()), - 'total_food_collected': float(colony_stats['total_food_collected'].sum()) + 'mean_population': float(np.mean([stats['population'] for stats in self.data['colony_stats']])), + 'peak_population': int(max(stats['population'] for stats in self.data['colony_stats'])), + 'total_food_collected': float(sum(stats['total_food_collected'] for stats in self.data['colony_stats'])) }, 'task_distribution': { - task: float(task_dist[task].mean()) - for task in task_dist.columns + task: float(np.mean([dist.get(task, 0) for dist in self.data['task_distribution']])) + for task in set().union(*self.data['task_distribution']) } } diff --git a/Things/Ant_Colony/ant_colony/visualization/renderer.py b/Things/Ant_Colony/ant_colony/visualization/renderer.py index a5526cd..a820700 100644 --- a/Things/Ant_Colony/ant_colony/visualization/renderer.py +++ b/Things/Ant_Colony/ant_colony/visualization/renderer.py @@ -14,9 +14,33 @@ class SimulationRenderer: def __init__(self, config: dict): """Initialize the renderer with configuration settings.""" self.config = config - self.viz_config = config['visualization'] + self.viz_config = config.get('visualization', {}) self.env_size = config['environment']['size'] + # Default colors if not specified in config + self.colors = { + 'nest': 'brown', + 'food': 'green', + 'obstacles': 'gray', + 'agents': { + 'foraging': 'red', + 'maintenance': 'blue', + 'nursing': 'pink', + 'defense': 'darkred', + 'exploration': 'orange' + }, + 'pheromones': { + 'food': 'green', + 'home': 'red', + 'alarm': 'orange', + 'trail': 'blue' + } + } + + # Override defaults with config values if present + if 'colors' in self.viz_config: + self.colors.update(self.viz_config['colors']) + # Setup the figure and axis self.fig, self.ax = plt.subplots(figsize=(10, 10)) self.ax.set_xlim(0, self.env_size[0]) @@ -31,7 +55,7 @@ class SimulationRenderer: # Setup the nest nest_loc = config['environment']['nest_location'] - nest = Circle(nest_loc, 5, color=self.viz_config['colors']['nest']) + nest = Circle(nest_loc, 5, color=self.colors['nest']) self.ax.add_patch(nest) def update(self, world_state: dict) -> None: @@ -46,7 +70,7 @@ class SimulationRenderer: # Update pheromones for p_type, grid in world_state['pheromones'].items(): if p_type not in self.pheromone_plots: - color = self.viz_config['colors']['pheromones'][p_type] + color = self.colors['pheromones'][p_type] self.pheromone_plots[p_type] = self.ax.imshow( grid, extent=[0, self.env_size[0], 0, self.env_size[1]], @@ -60,7 +84,7 @@ class SimulationRenderer: # Draw agents for agent in world_state['agents']: - color = self.viz_config['colors']['agents'][agent.current_task.value] + color = self.colors['agents'][agent.current_task.value] agent_patch = self._create_agent_patch(agent, color) self.ax.add_patch(agent_patch) self.agent_patches.append(agent_patch) @@ -69,8 +93,8 @@ class SimulationRenderer: for food in world_state['resources']: food_patch = Circle( (food.position.x, food.position.y), - food.size, - color=self.viz_config['colors']['food'] + food.size if hasattr(food, 'size') else 1.0, + color=self.colors['food'] ) self.ax.add_patch(food_patch) self.food_patches.append(food_patch) @@ -79,8 +103,8 @@ class SimulationRenderer: for obstacle in world_state['obstacles']: obstacle_patch = Circle( (obstacle.position.x, obstacle.position.y), - obstacle.size, - color=self.viz_config['colors']['obstacles'] + obstacle.size if hasattr(obstacle, 'size') else 1.0, + color=self.colors['obstacles'] ) self.ax.add_patch(obstacle_patch) self.obstacle_patches.append(obstacle_patch) diff --git a/Things/Ant_Colony/output/simulation_20250207_132721.h5 b/Things/Ant_Colony/output/simulation_20250207_132721.h5 new file mode 100644 index 0000000000000000000000000000000000000000..1fa22503587f09f215d79f51abff7dd2fc7438e7 GIT binary patch literal 6240 zcmeHL&2G~`5S~pM8ns1Gi34!5eB!{tF-5I2V>4;dWqjEuRLVs?E4IwwY>~8KuuHLcY+VRXUMLUK*(`+Qyqz z);I}m;dLY!~0Ia(T83SY$WGrNiakUPV2J6%R9xuesAYJ zF8Cv@dW+;Qsxvad-D{!^C4V%lbdDV7t9qUEB)oL{fx|FXiZdW|#!qf0%y@CV;{QS>>j*rYI&B%cnu8YBQ>hZKhggM(J~lkT0}om5ybSmqzMwT3Bc!imZ1iZR5=< zYkvwyYE%~~_F#;kYRu@=vQo9qWKzhnEGl%3zDlizs+38l#!{yzGW`7A_EB7f9S@y= zIEtS~ar7eDcouI(F?j(34!HJs#WUa;@C dict: + """Load configuration from YAML file.""" + with open(config_path, 'r') as f: + return yaml.safe_load(f) + +def setup_logging(output_dir: Path) -> logging.Logger: + """Set up logging configuration.""" + logger = logging.getLogger('path_network') + logger.setLevel(logging.INFO) + + # Create handlers + console_handler = logging.StreamHandler() + file_handler = logging.FileHandler(output_dir / 'simulation.log') + + # Create formatters and add it to handlers + log_format = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + console_handler.setFormatter(log_format) + file_handler.setFormatter(log_format) + + # Add handlers to the logger + logger.addHandler(console_handler) + logger.addHandler(file_handler) + + return logger + +def setup_matplotlib_backend(logger: logging.Logger) -> bool: + """ + Set up appropriate matplotlib backend. + + Returns: + bool: True if interactive backend is available + """ + interactive = True + + # First try Qt5Agg + try: + matplotlib.use('Qt5Agg') + logger.info("Using Qt5Agg backend") + return interactive + except Exception: + logger.warning("Could not use Qt5Agg backend") + + # Then try TkAgg + try: + matplotlib.use('TkAgg') + logger.info("Using TkAgg backend") + return interactive + except Exception: + logger.warning("Could not use TkAgg backend") + + # Finally fallback to Agg + try: + matplotlib.use('Agg') + logger.info("Using non-interactive Agg backend") + interactive = False + return interactive + except Exception as e: + logger.error(f"Could not set up any matplotlib backend: {e}") + raise + +def create_output_directory() -> Path: + """Create and return output directory for simulation results.""" + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + output_dir = Path('output') / timestamp + output_dir.mkdir(parents=True, exist_ok=True) + return output_dir + +def run_simulation( + config: dict, + logger: logging.Logger, + output_dir: Path, + interactive: bool +): + """Run the main simulation with comprehensive logging and visualization.""" + logger.info("Initializing simulation configurations...") + + # Create network configuration + network_config = NetworkConfig( + num_nodes=config['network']['num_nodes'], + initial_connectivity=config['network']['initial_connectivity'], + min_weight=config['network']['min_weight'], + max_weight=config['network']['max_weight'], + dynamic_topology=config['network']['dynamic_topology'], + topology_update_interval=config['network']['topology_update_interval'] + ) + + # Create dynamics configuration + dynamics_config = DynamicsConfig( + base_components=[ + WaveComponent( + amplitude=comp['amplitude'], + frequency=comp['frequency'], + phase=comp['phase'] + ) + for comp in config['dynamics']['wave_components'] + ], + noise_std=config['dynamics']['noise_std'], + time_scale=config['dynamics']['time_scale'] + ) + + # Create simulation configuration + sim_config = SimulationConfig( + network_config=network_config, + dynamics_config=dynamics_config, + num_steps=config['simulation']['total_steps'], + save_interval=config['simulation']['save_interval'], + visualization_interval=( + config['simulation']['visualization_interval'] + if interactive + else config['simulation']['total_steps'] // 10 + ) + ) + + logger.info("Creating visualizers...") + basic_visualizer = NetworkVisualizer() + advanced_visualizer = AdvancedVisualizer() + + logger.info("Initializing simulation runner...") + runner = SimulationRunner(sim_config, basic_visualizer) + + # Initial simulation period + logger.info("Running initial simulation period...") + initial_states = runner.run(config['simulation']['initial_period']) + + # Save initial period results + basic_visualizer.save(output_dir / 'initial_state.png') + logger.info("Saved initial state visualization") + + # Add perturbation + logger.info("Adding perturbation to the system...") + runner.add_perturbation( + magnitude=config['perturbation']['magnitude'], + duration=config['perturbation']['duration'], + decay=config['perturbation']['decay'] + ) + + # Continue simulation + logger.info("Running post-perturbation simulation...") + final_states = runner.run( + config['simulation']['total_steps'] - + config['simulation']['initial_period'] + ) + + # Save final basic visualization + basic_visualizer.save(output_dir / 'final_state.png') + logger.info("Saved final state visualization") + + # Create advanced visualizations + logger.info("Creating advanced visualizations...") + + # Get full history + history = runner.get_history() + + # Create animations + if config['visualization']['create_animations']: + logger.info("Creating animations...") + advanced_visualizer.create_network_animation(history, output_dir) + advanced_visualizer.create_phase_space_animation(history, output_dir) + + # Create static visualizations + if config['visualization']['plots']['spectral_analysis']: + logger.info("Creating spectral analysis...") + advanced_visualizer.create_spectral_analysis(history, output_dir) + + if config['visualization']['plots']['correlation_matrix']: + logger.info("Creating correlation analysis...") + advanced_visualizer.create_correlation_analysis(history, output_dir) + + if config['visualization']['enable_interactive']: + logger.info("Creating interactive dashboard...") + advanced_visualizer.create_interactive_dashboard(history, output_dir) + + basic_visualizer.close() + logger.info("Simulation complete!") + + return initial_states, final_states + +def main(): + """Main entry point for the simulation.""" + # Load configuration + config = load_config() + + # Create output directory + output_dir = create_output_directory() + + # Setup logging + logger = setup_logging(output_dir) + + try: + # Setup matplotlib backend + interactive = setup_matplotlib_backend(logger) + + # Run simulation + initial_states, final_states = run_simulation( + config, + logger, + output_dir, + interactive + ) + + logger.info(f"Results saved to: {output_dir}") + + except Exception as e: + logger.error(f"Simulation failed: {str(e)}", exc_info=True) + sys.exit(1) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/Things/Path_Network/path_network/config/__init__.py b/Things/Path_Network/path_network/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Things/Path_Network/path_network/core/__init__.py b/Things/Path_Network/path_network/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Things/Path_Network/path_network/core/agent.py b/Things/Path_Network/path_network/core/agent.py new file mode 100644 index 0000000..e7aa00a --- /dev/null +++ b/Things/Path_Network/path_network/core/agent.py @@ -0,0 +1,112 @@ +""" +Active Inference Agent implementation for the Path Network simulation. +This module contains the core agent class that performs continuous-time active inference. +""" + +import numpy as np +import torch +from dataclasses import dataclass +from typing import List, Optional, Tuple + +@dataclass +class AgentConfig: + """Configuration parameters for an Active Inference agent.""" + dt: float = 0.01 # Integration time step + taylor_order: int = 2 # Order of Taylor expansion for generalized coordinates + tolerance: float = 0.1 # Tolerance range for water level + learning_rate: float = 0.01 # Learning rate for parameter updates + initial_height: Optional[float] = None # Initial height of the agent + +class ActiveInferenceAgent: + """ + An Active Inference agent that infers and responds to water level changes. + + The agent uses generalized coordinates and continuous-time inference to + maintain its height within a tolerable range of the global water level. + """ + + def __init__(self, config: AgentConfig): + self.config = config + self.height = ( + config.initial_height + if config.initial_height is not None + else np.random.normal(0, 0.1) + ) + + # Generalized coordinates for position (height) + self.gen_coords = torch.zeros(config.taylor_order) + self.gen_coords[0] = self.height + + # Free energy components + self.mu = torch.zeros(config.taylor_order) # Expected states + self.pi = torch.ones(config.taylor_order) # Precision (inverse variance) + + # History for analysis + self.height_history: List[float] = [self.height] + self.prediction_error_history: List[float] = [] + + def update_beliefs(self, sensory_input: float) -> None: + """ + Update beliefs about the environment based on sensory input. + + Args: + sensory_input: The current water level measurement + """ + # Compute prediction error + prediction_error = sensory_input - self.mu[0] + self.prediction_error_history.append(prediction_error.item()) + + # Update generalized coordinates + for i in range(self.config.taylor_order - 1): + self.gen_coords[i] += ( + self.gen_coords[i + 1] * self.config.dt + + self.config.learning_rate * prediction_error * self.pi[i] + ) + + # Update expectations + self.mu = self.gen_coords.clone() + + def act(self, water_level: float) -> float: + """ + Generate an action (height adjustment) based on current beliefs. + + Args: + water_level: Current global water level + + Returns: + float: The new height of the agent + """ + # Update beliefs based on sensory input + self.update_beliefs(water_level) + + # Compute desired height adjustment + error = water_level - self.height + adjustment = np.clip( + error * self.config.learning_rate, + -self.config.tolerance, + self.config.tolerance + ) + + # Update height + self.height += adjustment + self.height_history.append(self.height) + + return self.height + + def get_state(self) -> Tuple[float, List[float]]: + """ + Get the current state of the agent. + + Returns: + Tuple containing current height and generalized coordinates + """ + return self.height, self.gen_coords.tolist() + + def get_history(self) -> Tuple[List[float], List[float]]: + """ + Get the agent's history of heights and prediction errors. + + Returns: + Tuple containing height history and prediction error history + """ + return self.height_history, self.prediction_error_history \ No newline at end of file diff --git a/Things/Path_Network/path_network/core/dynamics.py b/Things/Path_Network/path_network/core/dynamics.py new file mode 100644 index 0000000..e910d53 --- /dev/null +++ b/Things/Path_Network/path_network/core/dynamics.py @@ -0,0 +1,126 @@ +""" +Environmental dynamics for the Path Network simulation. +Generates nested sinusoidal waves that represent the global water level. +""" + +import numpy as np +from dataclasses import dataclass +from typing import List, Optional, Tuple + +@dataclass +class WaveComponent: + """Parameters for a single sinusoidal wave component.""" + amplitude: float + frequency: float + phase: float = 0.0 + +@dataclass +class DynamicsConfig: + """Configuration for the environmental dynamics.""" + base_components: List[WaveComponent] = None + noise_std: float = 0.05 + time_scale: float = 1.0 + + def __post_init__(self): + if self.base_components is None: + # Default wave components + self.base_components = [ + WaveComponent(amplitude=1.0, frequency=0.1), # Slow wave + WaveComponent(amplitude=0.5, frequency=0.5), # Medium wave + WaveComponent(amplitude=0.2, frequency=2.0) # Fast wave + ] + +class EnvironmentalDynamics: + """ + Generates and manages the environmental dynamics (water level) through + nested sinusoidal waves with optional noise and perturbations. + """ + + def __init__(self, config: DynamicsConfig): + self.config = config + self.time = 0.0 + self.history: List[float] = [] + self.perturbation: Optional[Tuple[float, float, float]] = None + + def step(self) -> float: + """ + Compute the next water level value. + + Returns: + float: The current global water level + """ + # Base level from superposition of waves + level = sum( + component.amplitude * np.sin( + 2 * np.pi * component.frequency * self.time * self.config.time_scale + + component.phase + ) + for component in self.config.base_components + ) + + # Add noise + if self.config.noise_std > 0: + level += np.random.normal(0, self.config.noise_std) + + # Add any active perturbation + if self.perturbation is not None: + magnitude, duration, decay = self.perturbation + if self.time < duration: + level += magnitude * (1 - self.time / duration) + else: + # Remove perturbation after it expires + self.perturbation = None + + # Update time and history + self.time += 1 + self.history.append(level) + + return level + + def add_perturbation( + self, + magnitude: float, + duration: float, + decay: float = 1.0 + ) -> None: + """ + Add a temporary perturbation to the water level. + + Args: + magnitude: Size of the perturbation + duration: How long the perturbation lasts + decay: How quickly the perturbation decays (1.0 = linear) + """ + self.perturbation = (magnitude, duration, decay) + + def add_wave_component( + self, + amplitude: float, + frequency: float, + phase: float = 0.0 + ) -> None: + """ + Add a new wave component to the dynamics. + + Args: + amplitude: Wave amplitude + frequency: Wave frequency + phase: Wave phase shift + """ + self.config.base_components.append( + WaveComponent(amplitude, frequency, phase) + ) + + def get_history(self) -> List[float]: + """Get the history of water levels.""" + return self.history.copy() + + def get_wave_components(self) -> List[WaveComponent]: + """Get the current wave components.""" + return self.config.base_components.copy() + + def reset(self) -> None: + """Reset the dynamics to initial state.""" + self.time = 0.0 + self.history.clear() + self.perturbation = None \ No newline at end of file diff --git a/Things/Path_Network/path_network/core/network.py b/Things/Path_Network/path_network/core/network.py new file mode 100644 index 0000000..a03a611 --- /dev/null +++ b/Things/Path_Network/path_network/core/network.py @@ -0,0 +1,173 @@ +""" +Network topology management for the Path Network simulation. +Handles the creation and management of the network of Active Inference agents. +""" + +import networkx as nx +import numpy as np +from typing import Dict, List, Optional, Tuple +from dataclasses import dataclass +from .agent import ActiveInferenceAgent, AgentConfig + +@dataclass +class NetworkConfig: + """Configuration parameters for the network topology.""" + num_nodes: int = 10 + initial_connectivity: float = 0.3 # Probability of edge creation + min_weight: float = 0.1 + max_weight: float = 1.0 + dynamic_topology: bool = True + topology_update_interval: int = 100 # Steps between topology updates + +class PathNetwork: + """ + Manages a network of Active Inference agents and their interactions. + + The network topology is represented as a weighted directed graph where + edges represent influence relationships between agents. + """ + + def __init__(self, network_config: NetworkConfig): + self.config = network_config + self.graph = nx.DiGraph() + self.agents: Dict[int, ActiveInferenceAgent] = {} + self.step_counter = 0 + + # Initialize network + self._initialize_network() + + def _initialize_network(self) -> None: + """Initialize the network topology and create agents.""" + # Create nodes with agents + for i in range(self.config.num_nodes): + agent_config = AgentConfig( + initial_height=np.random.normal(0, 0.1) + ) + self.agents[i] = ActiveInferenceAgent(agent_config) + self.graph.add_node(i) + + # Create random edges + for i in range(self.config.num_nodes): + for j in range(self.config.num_nodes): + if i != j and np.random.random() < self.config.initial_connectivity: + weight = np.random.uniform( + self.config.min_weight, + self.config.max_weight + ) + self.graph.add_edge(i, j, weight=weight) + + def update_topology(self) -> None: + """ + Update network topology based on agent interactions and performance. + This is called periodically to evolve the network structure. + """ + if not self.config.dynamic_topology: + return + + # Update edge weights based on prediction error correlation + for i, j in self.graph.edges(): + agent_i = self.agents[i] + agent_j = self.agents[j] + + # Compute correlation between agents' prediction errors + corr = np.corrcoef( + agent_i.prediction_error_history[-100:], + agent_j.prediction_error_history[-100:] + )[0, 1] + + # Update edge weight + new_weight = np.clip( + abs(corr), + self.config.min_weight, + self.config.max_weight + ) + self.graph[i][j]['weight'] = new_weight + + # Potentially add or remove edges based on agent performance + self._update_connections() + + def _update_connections(self) -> None: + """Update network connections based on agent performance.""" + # Add edges between well-performing agents + for i in range(self.config.num_nodes): + for j in range(self.config.num_nodes): + if i != j and not self.graph.has_edge(i, j): + # Add edge if agents have similar performance + if self._should_connect(i, j): + weight = np.random.uniform( + self.config.min_weight, + self.config.max_weight + ) + self.graph.add_edge(i, j, weight=weight) + + # Remove edges with very low weights + edges_to_remove = [ + (i, j) for i, j, w in self.graph.edges(data='weight') + if w < self.config.min_weight + ] + self.graph.remove_edges_from(edges_to_remove) + + def _should_connect(self, i: int, j: int) -> bool: + """Determine if two agents should be connected based on their performance.""" + agent_i = self.agents[i] + agent_j = self.agents[j] + + # Compare recent performance + errors_i = np.mean(agent_i.prediction_error_history[-50:]) + errors_j = np.mean(agent_j.prediction_error_history[-50:]) + + # Connect if agents have similar error levels + return abs(errors_i - errors_j) < 0.1 + + def step(self, global_water_level: float) -> Dict[int, float]: + """ + Perform one step of network simulation. + + Args: + global_water_level: The current global water level + + Returns: + Dict mapping node IDs to their new heights + """ + self.step_counter += 1 + + # Update agent states + new_heights = {} + for node_id, agent in self.agents.items(): + # Compute weighted average of neighbors' heights + neighbor_influence = 0.0 + total_weight = 0.0 + + for neighbor_id in self.graph.predecessors(node_id): + weight = self.graph[neighbor_id][node_id]['weight'] + neighbor_height = self.agents[neighbor_id].height + neighbor_influence += weight * neighbor_height + total_weight += weight + + if total_weight > 0: + effective_level = ( + 0.7 * global_water_level + + 0.3 * (neighbor_influence / total_weight) + ) + else: + effective_level = global_water_level + + # Update agent + new_height = agent.act(effective_level) + new_heights[node_id] = new_height + + # Periodically update topology + if (self.step_counter % self.config.topology_update_interval) == 0: + self.update_topology() + + return new_heights + + def get_network_state(self) -> Tuple[nx.DiGraph, Dict[int, float]]: + """ + Get the current state of the network. + + Returns: + Tuple containing the network graph and current agent heights + """ + heights = {i: agent.height for i, agent in self.agents.items()} + return self.graph, heights \ No newline at end of file diff --git a/Things/Path_Network/path_network/simulation/__init__.py b/Things/Path_Network/path_network/simulation/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Things/Path_Network/path_network/simulation/runner.py b/Things/Path_Network/path_network/simulation/runner.py new file mode 100644 index 0000000..3699efb --- /dev/null +++ b/Things/Path_Network/path_network/simulation/runner.py @@ -0,0 +1,120 @@ +""" +Main simulation runner for the Path Network. +Orchestrates the interaction between the network of agents and environmental dynamics. +""" + +import numpy as np +from typing import Dict, List, Optional, Tuple +from dataclasses import dataclass +from ..core.network import PathNetwork, NetworkConfig +from ..core.dynamics import EnvironmentalDynamics, DynamicsConfig +from ..utils.visualization import NetworkVisualizer + +@dataclass +class SimulationConfig: + """Configuration for the simulation.""" + network_config: NetworkConfig + dynamics_config: DynamicsConfig + num_steps: int = 1000 + save_interval: int = 10 + visualization_interval: int = 50 + +class SimulationRunner: + """ + Main simulation runner that coordinates the network of agents + and environmental dynamics. + """ + + def __init__( + self, + config: SimulationConfig, + visualizer: Optional[NetworkVisualizer] = None + ): + self.config = config + self.network = PathNetwork(config.network_config) + self.dynamics = EnvironmentalDynamics(config.dynamics_config) + self.visualizer = visualizer + + self.current_step = 0 + self.history: List[Dict] = [] + + def step(self) -> Dict: + """ + Perform one step of the simulation. + + Returns: + Dict containing the current state of the simulation + """ + # Get current water level + water_level = self.dynamics.step() + + # Update network + agent_heights = self.network.step(water_level) + + # Record state + state = { + 'step': self.current_step, + 'water_level': water_level, + 'agent_heights': agent_heights + } + + if self.current_step % self.config.save_interval == 0: + self.history.append(state) + + # Visualize if needed + if ( + self.visualizer is not None and + self.current_step % self.config.visualization_interval == 0 + ): + self.visualizer.update(self.network, water_level) + + self.current_step += 1 + return state + + def run(self, num_steps: Optional[int] = None) -> List[Dict]: + """ + Run the simulation for a specified number of steps. + + Args: + num_steps: Number of steps to run (default: config.num_steps) + + Returns: + List of recorded states + """ + steps_to_run = num_steps or self.config.num_steps + + for _ in range(steps_to_run): + self.step() + + return self.history + + def add_perturbation( + self, + magnitude: float, + duration: float, + decay: float = 1.0 + ) -> None: + """Add a perturbation to the environmental dynamics.""" + self.dynamics.add_perturbation(magnitude, duration, decay) + + def get_network_state(self) -> Tuple[Dict[int, float], float]: + """ + Get the current state of the network and environment. + + Returns: + Tuple of agent heights and current water level + """ + _, heights = self.network.get_network_state() + water_level = self.dynamics.history[-1] if self.dynamics.history else 0.0 + return heights, water_level + + def get_history(self) -> List[Dict]: + """Get the simulation history.""" + return self.history.copy() + + def reset(self) -> None: + """Reset the simulation to initial state.""" + self.current_step = 0 + self.history.clear() + self.dynamics.reset() + # Note: Network is not reset as it maintains its learned structure \ No newline at end of file diff --git a/Things/Path_Network/path_network/utils/__init__.py b/Things/Path_Network/path_network/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Things/Path_Network/path_network/utils/advanced_visualization.py b/Things/Path_Network/path_network/utils/advanced_visualization.py new file mode 100644 index 0000000..6eea744 --- /dev/null +++ b/Things/Path_Network/path_network/utils/advanced_visualization.py @@ -0,0 +1,390 @@ +""" +Advanced visualization utilities for the Path Network simulation. +Provides comprehensive visualization options including animations and 3D plots. +""" + +import os +import numpy as np +import matplotlib.pyplot as plt +import networkx as nx +import seaborn as sns +import plotly.graph_objects as go +from plotly.subplots import make_subplots +import plotly.express as px +from matplotlib.animation import FuncAnimation, PillowWriter +from mpl_toolkits.mplot3d import Axes3D +from scipy import signal +from scipy.stats import gaussian_kde +from typing import Dict, List, Optional, Tuple, Any +import yaml +from pathlib import Path +from tqdm import tqdm +import imageio +from sklearn.decomposition import PCA +from ..core.network import PathNetwork + +class AdvancedVisualizer: + """Advanced visualization tools for the Path Network simulation.""" + + def __init__(self, config_path: str = "config.yaml"): + # Load configuration + with open(config_path, 'r') as f: + self.config = yaml.safe_load(f)['visualization'] + + # Set style + plt.style.use(self.config['style']) + sns.set_context("notebook", font_scale=self.config['font_scale']) + + # Initialize storage for animation frames + self.network_frames: List[plt.Figure] = [] + self.phase_space_frames: List[plt.Figure] = [] + + def create_network_animation( + self, + history: List[Dict], + output_path: Path + ) -> None: + """Create an animated visualization of the network evolution.""" + fig = plt.figure(figsize=self.config['animation_figsize']) + + def update(frame): + plt.clf() + state = history[frame] + self._draw_network_state(state, fig.gca()) + return fig.gca() + + anim = FuncAnimation( + fig, + update, + frames=len(history), + interval=1000/self.config['fps'] + ) + + # Save as GIF + writer = PillowWriter(fps=self.config['fps']) + anim.save(output_path / 'network_evolution.gif', writer=writer) + plt.close() + + def create_phase_space_animation( + self, + history: List[Dict], + output_path: Path + ) -> None: + """Create an animated 3D phase space visualization.""" + if not self.config['enable_3d']: + return + + fig = plt.figure(figsize=self.config['animation_figsize']) + ax = fig.add_subplot(111, projection='3d') + + # Compute PCA for dimensionality reduction + heights_matrix = [] + for state in history: + heights = list(state['agent_heights'].values()) + heights_matrix.append(heights) + + pca = PCA(n_components=3) + transformed = pca.fit_transform(heights_matrix) + + def update(frame): + ax.clear() + ax.scatter( + transformed[:frame+1, 0], + transformed[:frame+1, 1], + transformed[:frame+1, 2], + c=range(frame+1), + cmap='viridis' + ) + ax.view_init( + elev=30, + azim=frame * self.config['3d_rotation_speed'] + ) + return ax + + anim = FuncAnimation( + fig, + update, + frames=len(history), + interval=1000/self.config['fps'] + ) + + anim.save(output_path / 'phase_space.gif', writer=PillowWriter(fps=self.config['fps'])) + plt.close() + + def create_spectral_analysis( + self, + history: List[Dict], + output_path: Path + ) -> None: + """Create spectral analysis visualizations.""" + water_levels = [state['water_level'] for state in history] + + # Compute spectrograms for water level + f, t, Sxx = signal.spectrogram(water_levels, fs=1.0) + + plt.figure(figsize=self.config['timeseries_figsize']) + plt.pcolormesh(t, f, Sxx, shading='gouraud') + plt.ylabel('Frequency') + plt.xlabel('Time') + plt.title('Water Level Spectrogram') + plt.colorbar(label='Intensity') + plt.savefig(output_path / 'water_level_spectrogram.png', dpi=self.config['dpi']) + plt.close() + + # Compute and plot power spectrum + plt.figure(figsize=self.config['timeseries_figsize']) + f, Pxx = signal.welch(water_levels) + plt.semilogy(f, Pxx) + plt.xlabel('Frequency') + plt.ylabel('Power Spectral Density') + plt.title('Water Level Power Spectrum') + plt.savefig(output_path / 'power_spectrum.png', dpi=self.config['dpi']) + plt.close() + + def create_correlation_analysis( + self, + history: List[Dict], + output_path: Path + ) -> None: + """Create correlation analysis visualizations.""" + # Extract agent height histories + agent_histories = {} + for state in history: + for agent_id, height in state['agent_heights'].items(): + if agent_id not in agent_histories: + agent_histories[agent_id] = [] + agent_histories[agent_id].append(height) + + # Compute correlation matrix + num_agents = len(agent_histories) + corr_matrix = np.zeros((num_agents, num_agents)) + + for i in range(num_agents): + for j in range(num_agents): + corr = np.corrcoef( + agent_histories[i], + agent_histories[j] + )[0, 1] + corr_matrix[i, j] = corr + + # Plot correlation matrix + plt.figure(figsize=self.config['network_figsize']) + sns.heatmap( + corr_matrix, + cmap='RdBu_r', + center=0, + vmin=-1, + vmax=1, + annot=True, + fmt='.2f' + ) + plt.title('Agent Height Correlation Matrix') + plt.savefig(output_path / 'correlation_matrix.png', dpi=self.config['dpi']) + plt.close() + + def create_interactive_dashboard( + self, + history: List[Dict], + output_path: Path + ) -> None: + """Create an interactive HTML dashboard using plotly.""" + if not self.config['enable_interactive']: + return + + # Create subplot figure + fig = make_subplots( + rows=2, + cols=2, + specs=[[{'type': 'scatter3d'}, {'type': 'heatmap'}], + [{'type': 'scatter'}, {'type': 'histogram'}]], + subplot_titles=( + '3D Phase Space', + 'Agent Correlation', + 'Height Evolution', + 'Height Distribution' + ) + ) + + # Add traces + self._add_phase_space_trace(fig, history, row=1, col=1) + self._add_correlation_trace(fig, history, row=1, col=2) + self._add_height_evolution_trace(fig, history, row=2, col=1) + self._add_height_distribution_trace(fig, history, row=2, col=2) + + # Update layout + fig.update_layout(height=1000, showlegend=True) + + # Save + fig.write_html(output_path / 'interactive_dashboard.html') + + def _add_phase_space_trace( + self, + fig: go.Figure, + history: List[Dict], + row: int, + col: int + ) -> None: + """Add 3D phase space trace to plotly figure.""" + heights_matrix = [] + for state in history: + heights = list(state['agent_heights'].values()) + heights_matrix.append(heights) + + pca = PCA(n_components=3) + transformed = pca.fit_transform(heights_matrix) + + fig.add_trace( + go.Scatter3d( + x=transformed[:, 0], + y=transformed[:, 1], + z=transformed[:, 2], + mode='lines+markers', + marker=dict( + size=2, + color=range(len(transformed)), + colorscale='Viridis', + opacity=0.8 + ), + name='Phase Space' + ), + row=row, + col=col + ) + + def _add_correlation_trace( + self, + fig: go.Figure, + history: List[Dict], + row: int, + col: int + ) -> None: + """Add correlation matrix trace to plotly figure.""" + agent_histories = {} + for state in history: + for agent_id, height in state['agent_heights'].items(): + if agent_id not in agent_histories: + agent_histories[agent_id] = [] + agent_histories[agent_id].append(height) + + num_agents = len(agent_histories) + corr_matrix = np.zeros((num_agents, num_agents)) + + for i in range(num_agents): + for j in range(num_agents): + corr = np.corrcoef( + agent_histories[i], + agent_histories[j] + )[0, 1] + corr_matrix[i, j] = corr + + fig.add_trace( + go.Heatmap( + z=corr_matrix, + colorscale='RdBu', + zmid=0, + name='Correlation' + ), + row=row, + col=col + ) + + def _add_height_evolution_trace( + self, + fig: go.Figure, + history: List[Dict], + row: int, + col: int + ) -> None: + """Add height evolution traces to plotly figure.""" + water_levels = [state['water_level'] for state in history] + time_points = list(range(len(history))) + + fig.add_trace( + go.Scatter( + x=time_points, + y=water_levels, + mode='lines', + name='Water Level', + line=dict(color='red', dash='dash') + ), + row=row, + col=col + ) + + for agent_id in history[0]['agent_heights'].keys(): + heights = [ + state['agent_heights'][agent_id] + for state in history + ] + fig.add_trace( + go.Scatter( + x=time_points, + y=heights, + mode='lines', + name=f'Agent {agent_id}', + opacity=0.6 + ), + row=row, + col=col + ) + + def _add_height_distribution_trace( + self, + fig: go.Figure, + history: List[Dict], + row: int, + col: int + ) -> None: + """Add height distribution trace to plotly figure.""" + all_heights = [] + for state in history: + all_heights.extend(state['agent_heights'].values()) + + fig.add_trace( + go.Histogram( + x=all_heights, + nbinsx=30, + name='Height Distribution' + ), + row=row, + col=col + ) + + def _draw_network_state( + self, + state: Dict, + ax: plt.Axes + ) -> None: + """Draw network state for animation frame.""" + graph = state['network'] + heights = state['agent_heights'] + + pos = nx.spring_layout(graph, seed=42) + + nx.draw_networkx_nodes( + graph, + pos, + node_color=[heights[node] for node in graph.nodes()], + node_size=500, + cmap=self.config['network_cmap'], + ax=ax + ) + + nx.draw_networkx_edges( + graph, + pos, + edge_color='gray', + width=[ + graph[u][v]['weight'] * 2 + for u, v in graph.edges() + ], + alpha=0.6, + ax=ax + ) + + nx.draw_networkx_labels( + graph, + pos, + {node: str(node) for node in graph.nodes()}, + ax=ax + ) \ No newline at end of file diff --git a/Things/Path_Network/path_network/utils/math_utils.py b/Things/Path_Network/path_network/utils/math_utils.py new file mode 100644 index 0000000..ca4b4b7 --- /dev/null +++ b/Things/Path_Network/path_network/utils/math_utils.py @@ -0,0 +1,150 @@ +""" +Mathematical utilities for the Path Network simulation. +""" + +import numpy as np +from typing import List, Tuple, Union +import torch + +def compute_free_energy( + mu: torch.Tensor, + sigma: torch.Tensor, + sensory_input: torch.Tensor +) -> torch.Tensor: + """ + Compute the variational free energy. + + Args: + mu: Expected state + sigma: Variance of state estimation + sensory_input: Observed sensory input + + Returns: + Free energy value + """ + precision = 1.0 / sigma + prediction_error = sensory_input - mu + return 0.5 * (prediction_error**2 * precision + torch.log(sigma)) + +def generalized_coordinates_update( + coords: torch.Tensor, + dt: float, + order: int +) -> torch.Tensor: + """ + Update generalized coordinates using Taylor series expansion. + + Args: + coords: Current generalized coordinates + dt: Time step + order: Order of Taylor expansion + + Returns: + Updated generalized coordinates + """ + new_coords = coords.clone() + + for i in range(order - 1): + new_coords[i] += coords[i + 1] * dt + + return new_coords + +def compute_correlation_matrix( + time_series: List[np.ndarray] +) -> np.ndarray: + """ + Compute correlation matrix between multiple time series. + + Args: + time_series: List of time series data + + Returns: + Correlation matrix + """ + n = len(time_series) + corr_matrix = np.zeros((n, n)) + + for i in range(n): + for j in range(n): + corr_matrix[i, j] = np.corrcoef(time_series[i], time_series[j])[0, 1] + + return corr_matrix + +def compute_prediction_error_metrics( + predictions: np.ndarray, + observations: np.ndarray +) -> Tuple[float, float, float]: + """ + Compute various prediction error metrics. + + Args: + predictions: Predicted values + observations: Observed values + + Returns: + Tuple of (MSE, MAE, RMSE) + """ + mse = np.mean((predictions - observations) ** 2) + mae = np.mean(np.abs(predictions - observations)) + rmse = np.sqrt(mse) + return mse, mae, rmse + +def sigmoid_scale( + x: Union[float, np.ndarray], + scale: float = 1.0, + offset: float = 0.0 +) -> Union[float, np.ndarray]: + """ + Apply sigmoid scaling to a value or array. + + Args: + x: Input value(s) + scale: Scaling factor + offset: Offset value + + Returns: + Scaled value(s) + """ + return 1 / (1 + np.exp(-(x - offset) / scale)) + +def compute_entropy( + probabilities: np.ndarray, + epsilon: float = 1e-10 +) -> float: + """ + Compute the entropy of a probability distribution. + + Args: + probabilities: Probability distribution + epsilon: Small value to avoid log(0) + + Returns: + Entropy value + """ + # Ensure probabilities sum to 1 and are positive + p = np.clip(probabilities, epsilon, 1.0) + p = p / np.sum(p) + + return -np.sum(p * np.log(p)) + +def exponential_moving_average( + data: np.ndarray, + alpha: float = 0.1 +) -> np.ndarray: + """ + Compute exponential moving average of a time series. + + Args: + data: Input time series + alpha: Smoothing factor (0 < alpha < 1) + + Returns: + Smoothed time series + """ + result = np.zeros_like(data) + result[0] = data[0] + + for t in range(1, len(data)): + result[t] = alpha * data[t] + (1 - alpha) * result[t-1] + + return result \ No newline at end of file diff --git a/Things/Path_Network/path_network/utils/visualization.py b/Things/Path_Network/path_network/utils/visualization.py new file mode 100644 index 0000000..84d0645 --- /dev/null +++ b/Things/Path_Network/path_network/utils/visualization.py @@ -0,0 +1,233 @@ +""" +Visualization utilities for the Path Network simulation. +Provides real-time visualization of the network topology and agent states. +""" + +import numpy as np +import matplotlib.pyplot as plt +import networkx as nx +import seaborn as sns +from matplotlib.animation import FuncAnimation +from typing import Dict, List, Optional, Tuple +from ..core.network import PathNetwork + +class NetworkVisualizer: + """ + Visualizes the network topology and agent states in real-time. + """ + + def __init__(self, figsize: Tuple[int, int] = (15, 10)): + # Set up style + try: + sns.set_style("whitegrid") + sns.set_context("notebook", font_scale=1.2) + except Exception as e: + print(f"Warning: Could not set seaborn style: {e}") + plt.style.use('default') + + self.fig = plt.figure(figsize=figsize) + + # Create subplots + self.network_ax = self.fig.add_subplot(221) + self.heights_ax = self.fig.add_subplot(222) + self.history_ax = self.fig.add_subplot(212) + + # Initialize data storage + self.water_level_history: List[float] = [] + self.height_histories: Dict[int, List[float]] = {} + self.time_points: List[int] = [] + + # Style settings + self.fig.tight_layout(pad=3.0) + + def update(self, network: PathNetwork, water_level: float) -> None: + """ + Update the visualization with current network state. + + Args: + network: The current network state + water_level: Current global water level + """ + self._clear_axes() + + # Get current network state + graph, heights = network.get_network_state() + + # Update histories + self.water_level_history.append(water_level) + self.time_points.append(len(self.water_level_history)) + + for node_id, height in heights.items(): + if node_id not in self.height_histories: + self.height_histories[node_id] = [] + self.height_histories[node_id].append(height) + + # Draw network topology + self._draw_network(graph, heights) + + # Draw current heights + self._draw_heights(heights, water_level) + + # Draw history + self._draw_history() + + # Update display + try: + plt.draw() + plt.pause(0.01) + except Exception as e: + print(f"Warning: Could not update display in real-time: {e}") + + def _clear_axes(self) -> None: + """Clear all axes for redrawing.""" + for ax in [self.network_ax, self.heights_ax, self.history_ax]: + ax.clear() + + def _draw_network(self, graph: nx.DiGraph, heights: Dict[int, float]) -> None: + """Draw the network topology with node colors based on heights.""" + self.network_ax.set_title('Network Topology') + + # Calculate node colors based on heights + vmin = min(heights.values()) + vmax = max(heights.values()) + node_colors = [heights[node] for node in graph.nodes()] + + # Calculate edge weights for width + edge_weights = [ + graph[u][v]['weight'] * 2 for u, v in graph.edges() + ] + + # Draw the network + pos = nx.spring_layout(graph, seed=42) # Fixed seed for consistency + nx.draw_networkx_nodes( + graph, + pos, + node_color=node_colors, + node_size=500, + cmap='coolwarm', + vmin=vmin, + vmax=vmax, + ax=self.network_ax + ) + nx.draw_networkx_edges( + graph, + pos, + edge_color='gray', + width=edge_weights, + alpha=0.6, + ax=self.network_ax + ) + nx.draw_networkx_labels( + graph, + pos, + {node: str(node) for node in graph.nodes()}, + ax=self.network_ax + ) + + # Add colorbar + sm = plt.cm.ScalarMappable( + cmap='coolwarm', + norm=plt.Normalize(vmin=vmin, vmax=vmax) + ) + plt.colorbar(sm, ax=self.network_ax, label='Height') + + def _draw_heights( + self, + heights: Dict[int, float], + water_level: float + ) -> None: + """Draw current agent heights and water level.""" + self.heights_ax.set_title('Current Heights') + + # Plot agent heights + nodes = list(heights.keys()) + height_values = [heights[node] for node in nodes] + + # Create bar plot with custom colors based on height + colors = plt.cm.coolwarm( + plt.Normalize()(height_values) + ) + self.heights_ax.bar( + nodes, + height_values, + alpha=0.6, + color=colors + ) + + # Plot water level + self.heights_ax.axhline( + y=water_level, + color='r', + linestyle='--', + label='Water Level' + ) + + self.heights_ax.set_xlabel('Agent ID') + self.heights_ax.set_ylabel('Height') + self.heights_ax.legend() + + # Set y-limits with some padding + ymin = min(min(height_values), water_level) + ymax = max(max(height_values), water_level) + padding = (ymax - ymin) * 0.1 + self.heights_ax.set_ylim(ymin - padding, ymax + padding) + + def _draw_history(self) -> None: + """Draw the history of water level and agent heights.""" + self.history_ax.set_title('Height History') + + # Create color palette for agents + num_agents = len(self.height_histories) + colors = plt.cm.viridis(np.linspace(0, 1, num_agents)) + + # Plot agent height histories + for (node_id, history), color in zip( + self.height_histories.items(), colors + ): + self.history_ax.plot( + self.time_points[-len(history):], + history, + alpha=0.6, + label=f'Agent {node_id}', + color=color + ) + + # Plot water level history + self.history_ax.plot( + self.time_points, + self.water_level_history, + 'r--', + label='Water Level', + linewidth=2, + alpha=0.8 + ) + + self.history_ax.set_xlabel('Time Step') + self.history_ax.set_ylabel('Height') + + # Adjust legend + self.history_ax.legend( + bbox_to_anchor=(1.05, 1), + loc='upper left', + borderaxespad=0., + ncol=2 + ) + + def save(self, filename: str) -> None: + """Save the current figure to a file.""" + try: + # Adjust layout before saving + self.fig.tight_layout() + self.fig.savefig( + filename, + bbox_inches='tight', + dpi=300, + facecolor='white', + edgecolor='none' + ) + except Exception as e: + print(f"Warning: Could not save figure to {filename}: {e}") + + def close(self) -> None: + """Close the figure.""" + plt.close(self.fig) \ No newline at end of file diff --git a/Things/Path_Network/requirements.txt b/Things/Path_Network/requirements.txt new file mode 100644 index 0000000..f00f3c5 --- /dev/null +++ b/Things/Path_Network/requirements.txt @@ -0,0 +1,19 @@ +numpy>=1.21.0 +networkx>=2.6.0 +scipy>=1.7.0 +matplotlib>=3.4.0 +torch>=1.9.0 +pandas>=1.3.0 +seaborn>=0.11.0 +pytest>=6.2.0 +black>=21.5b2 +mypy>=0.910 +pylint>=2.8.0 +pyyaml>=5.4.1 +plotly>=5.3.0 +imageio>=2.9.0 +tqdm>=4.62.0 +scikit-learn>=0.24.0 +ipywidgets>=7.6.0 +moviepy>=1.0.3 +mayavi>=4.7.0 \ No newline at end of file diff --git a/Things/Path_Network/run_simulation.sh b/Things/Path_Network/run_simulation.sh new file mode 100755 index 0000000..b4d6f2e --- /dev/null +++ b/Things/Path_Network/run_simulation.sh @@ -0,0 +1,166 @@ +#!/bin/bash + +# Exit on error +set -e + +# Function to check if a command exists +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +# Function to setup Python virtual environment +setup_venv() { + echo "Setting up Python virtual environment..." + + # Check if python3-venv is installed + if command_exists apt-get; then + if ! dpkg -l | grep -q python3-venv; then + echo "Installing python3-venv..." + sudo apt-get update + sudo apt-get install -y python3-venv + fi + fi + + # Create and activate virtual environment + if [ ! -d "venv" ]; then + python3 -m venv venv + fi + source venv/bin/activate + + # Upgrade pip + python3 -m pip install --upgrade pip +} + +# Function to install system dependencies +install_system_deps() { + echo "Checking system dependencies..." + + if command_exists apt-get; then + echo "Debian/Ubuntu system detected" + sudo apt-get update + sudo apt-get install -y \ + python3-tk \ + python3-qt5 \ + libxcb-cursor0 \ + python3-dev \ + build-essential \ + ffmpeg \ + python3-vtk7 \ + libvtk7-dev \ + mayavi2 \ + python3-mayavi \ + imagemagick + elif command_exists dnf; then + echo "Fedora system detected" + sudo dnf install -y \ + python3-tkinter \ + python3-qt5 \ + xcb-util-cursor \ + python3-devel \ + gcc \ + ffmpeg \ + vtk \ + vtk-devel \ + mayavi \ + ImageMagick + elif command_exists pacman; then + echo "Arch system detected" + sudo pacman -S --noconfirm \ + python-tk \ + qt5-base \ + xcb-util-cursor \ + python-dev \ + base-devel \ + ffmpeg \ + vtk \ + mayavi \ + imagemagick + else + echo "Warning: Unknown package manager. Please install dependencies manually if needed." + fi + + # Configure ImageMagick to allow PDF operations if needed + if command_exists convert; then + sudo sed -i 's/rights="none" pattern="PDF"/rights="read|write" pattern="PDF"/' /etc/ImageMagick-6/policy.xml 2>/dev/null || true + sudo sed -i 's/rights="none" pattern="PDF"/rights="read|write" pattern="PDF"/' /etc/ImageMagick-7/policy.xml 2>/dev/null || true + fi +} + +# Function to install Python dependencies +install_python_deps() { + echo "Installing Python dependencies..." + + # First install numpy and other core dependencies + pip install numpy wheel setuptools + + # Install VTK separately if needed + if ! python3 -c "import vtk" 2>/dev/null; then + pip install vtk + fi + + # Then install other dependencies + pip install -r requirements.txt +} + +# Function to check installation +check_installation() { + echo "Checking installation..." + + # Try importing required packages + python3 -c " +import numpy +import torch +import matplotlib +import networkx +import seaborn +import plotly +import imageio +import vtk +import mayavi.mlab +print('All required packages are installed correctly!') +" +} + +# Function to check output directory +check_output_dir() { + echo "Checking output directory..." + if [ ! -d "output" ]; then + mkdir output + fi + + # Test write permissions + if ! touch output/.test 2>/dev/null; then + echo "Error: Cannot write to output directory" + exit 1 + fi + rm output/.test +} + +# Function to run the simulation +run_simulation() { + echo "Running simulation..." + python example.py +} + +# Main execution +echo "=== Path Network Simulation Setup ===" + +# Setup virtual environment +setup_venv + +# Install dependencies +install_system_deps +install_python_deps + +# Check installation +check_installation + +# Check output directory +check_output_dir + +# Run simulation +echo "=== Starting Simulation ===" +run_simulation + +echo "=== Simulation Complete ===" +echo "Check the 'output' directory for results and visualizations." \ No newline at end of file diff --git a/ant_colony/agents/nestmate.py b/ant_colony/agents/nestmate.py new file mode 100644 index 0000000..f39f0b5 --- /dev/null +++ b/ant_colony/agents/nestmate.py @@ -0,0 +1,68 @@ +""" +Nestmate agent implementation using active inference. +""" + +from enum import Enum +import numpy as np +from typing import Dict, Optional, List +from dataclasses import dataclass, field +from ant_colony.environment.world import Position, Resource + +class TaskType(Enum): + """Types of tasks an ant can perform.""" + FORAGING = 'foraging' + MAINTENANCE = 'maintenance' + NURSING = 'nursing' + DEFENSE = 'defense' + EXPLORATION = 'exploration' + +@dataclass +class Belief: + """Represents an agent's beliefs about the world state.""" + food_location: Optional[Position] = None + nest_location: Optional[Position] = None + danger_level: float = 0.0 + energy_level: float = 1.0 + task_urgency: Dict[TaskType, float] = field(default_factory=lambda: {task: 0.0 for task in TaskType}) + +class Nestmate: + """ + Implementation of an ant agent using active inference principles. + """ + + def __init__(self, config: dict): + """Initialize the agent.""" + self.config = config + + # Initialize beliefs first + self.beliefs = Belief() + + # Physical state + self.position = None + self.orientation = 0.0 + self.speed = 0.0 + self.energy = config['physical']['energy']['initial'] + + # Carrying state + self.carrying: Optional[Resource] = None + + # Task state + self.current_task = TaskType.EXPLORATION + self.task_time = 0.0 + + # Sensory state + self.observations = { + 'pheromones': {}, + 'resources': [], + 'nestmates': [], + 'terrain': None + } + + # Internal model + self.preferences = {task: 1.0 for task in TaskType} + + # Learning parameters + self.learning_rate = config['behavior'].get('learning_rate', 0.1) + self.exploration_rate = config['behavior'].get('exploration_rate', 0.2) + + # ... existing code ... \ No newline at end of file diff --git a/ant_colony/simulation.py b/ant_colony/simulation.py new file mode 100644 index 0000000..4a3c521 --- /dev/null +++ b/ant_colony/simulation.py @@ -0,0 +1,18 @@ +""" +Main simulation coordinator for ant colony simulation. +""" + +import yaml +import numpy as np +from pathlib import Path +from typing import Optional +import matplotlib +matplotlib.use('Agg') # Use non-GUI backend +import matplotlib.pyplot as plt + +from ant_colony.environment import World +from ant_colony.colony import Colony +from ant_colony.visualization import ColonyVisualizer +from ant_colony.utils.data_collection import DataCollector + +# ... existing code ... \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 5447c9b..40a88d6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,16 +16,16 @@ pomegranate>=0.14.0 matplotlib>=3.4.0 seaborn>=0.11.0 networkx>=2.6.0 -plotly>=5.1.0 +plotly>=5.3.0 # Data Processing -pyyaml>=5.4.0 +pyyaml>=5.4.1 jinja2>=3.0.0 markdown>=3.3.0 # Testing and Development pytest>=6.2.0 -black>=21.6b0 +black>=21.5b2 flake8>=3.9.0 mypy>=0.910 @@ -34,7 +34,7 @@ sphinx>=4.0.0 sphinx-rtd-theme>=0.5.0 # Utilities -tqdm>=4.61.0 +tqdm>=4.62.0 click>=8.0.0 python-dotenv>=0.19.0 @@ -44,4 +44,8 @@ hyperopt>=0.2.5 # Monitoring and Logging wandb>=0.12.0 -mlflow>=1.19.0 \ No newline at end of file +mlflow>=1.19.0 + +# Additional dependencies +pylint>=2.8.0 +imageio>=2.9.0 \ No newline at end of file