""" NGO Multi-Agent Coordination Environment OpenEnv-compatible RL environment for Meta PyTorch Hackathon Theme #1: Multi-Agent Interactions """ import gymnasium as gym from gymnasium import spaces import numpy as np from typing import Dict, List, Tuple, Any class NGOCoordinationEnv(gym.Env): """ Multi-Agent NGO Resource Coordination Environment Scenario: 3 NGO coordinators must allocate volunteers to help people in need. They can cooperate (share info), compete (maximize individual impact), or negotiate (find compromises). This is a MARL (Multi-Agent RL) environment where agents learn optimal negotiation strategies through experience. """ metadata = {'render_modes': ['human', 'rgb_array'], 'render_fps': 4} def __init__(self, num_agents: int = 3, max_steps: int = 50): super().__init__() self.num_agents = num_agents self.max_steps = max_steps self.current_step = 0 # Define observation space (what each agent sees) # Each agent observes: [urgency, available_resources, people_affected, # other_agents_last_actions, coalition_status] self.observation_space = spaces.Dict({ 'urgency': spaces.Discrete(10), # 1-10 urgency level 'available_resources': spaces.Box(low=0, high=100, shape=(1,), dtype=np.float32), 'people_affected': spaces.Box(low=0, high=300, shape=(1,), dtype=np.float32), 'other_agents_actions': spaces.Box(low=0, high=100, shape=(num_agents-1,), dtype=np.float32), 'coalition_status': spaces.MultiBinary(num_agents), # who's in coalition 'communication_channel': spaces.Box(low=0, high=1, shape=(num_agents,), dtype=np.float32) }) # Define action space (what each agent can do) # Each agent chooses: [allocation_percentage, cooperation_signal, negotiation_bid] self.action_space = spaces.Box( low=np.array([0.0, 0.0, 0.0], dtype=np.float32), # [allocation%, cooperate?, bid] high=np.array([1.0, 1.0, 1.0], dtype=np.float32), dtype=np.float32 ) # Episode tracking self.episode_count = 0 self.task_type = None # Will cycle through: cooperation, competition, negotiation, coalition def reset(self, seed=None, options=None): """Reset environment to initial state""" super().reset(seed=seed) self.current_step = 0 self.episode_count += 1 # Cycle through 4 task types task_types = ['cooperation', 'competition', 'negotiation', 'coalition'] self.task_type = task_types[(self.episode_count - 1) % 4] # Generate random initial state self.state = { 'urgency': self.np_random.integers(1, 11), 'available_resources': self.np_random.uniform(40, 100), 'people_affected': self.np_random.uniform(50, 300), 'task_type': self.task_type, 'coalition': set(), # Empty coalition initially 'communication': np.zeros(self.num_agents), 'last_actions': np.zeros(self.num_agents) } # Initialize agent observations observation = self._get_observation() info = self._get_info() return observation, info def _get_observation(self) -> Dict: """Generate observation for all agents""" # In MARL, each agent gets its own observation # For simplicity, we'll return a shared observation here # (you can extend this to return dict of observations per agent) obs = { 'urgency': self.state['urgency'], 'available_resources': np.array([self.state['available_resources']], dtype=np.float32), 'people_affected': np.array([self.state['people_affected']], dtype=np.float32), 'other_agents_actions': self.state['last_actions'][:self.num_agents-1].astype(np.float32), 'coalition_status': np.array([1 if i in self.state['coalition'] else 0 for i in range(self.num_agents)]), 'communication_channel': self.state['communication'].astype(np.float32) } return obs def _get_info(self) -> Dict: """Additional information for debugging/logging""" return { 'episode': self.episode_count, 'task_type': self.task_type, 'step': self.current_step } def step(self, actions: np.ndarray) -> Tuple[Dict, float, bool, bool, Dict]: """ Execute one step in the environment Args: actions: Array of shape (num_agents, 3) where each agent provides: [allocation_percentage, cooperation_signal, negotiation_bid] Returns: observation, reward, terminated, truncated, info """ self.current_step += 1 # Parse actions from all agents allocations = actions[:, 0] # How much each agent allocates (0-1) cooperation_signals = actions[:, 1] # Cooperation intent (0-1) negotiation_bids = actions[:, 2] # Negotiation offers (0-1) # Multi-agent interaction logic reward = self._calculate_reward(allocations, cooperation_signals, negotiation_bids) # Update state based on task type self._update_state(allocations, cooperation_signals) # Check if episode is done terminated = self.current_step >= self.max_steps truncated = False # Get new observation observation = self._get_observation() info = self._get_info() info['allocations'] = allocations info['reward_breakdown'] = self._get_reward_breakdown() return observation, reward, terminated, truncated, info def _calculate_reward(self, allocations, cooperation_signals, negotiation_bids) -> float: """ Calculate reward based on multi-agent interactions THIS IS WHERE THE LEARNING SIGNAL COMES FROM Agents must discover optimal allocation strategies through trial and error """ resources = self.state['available_resources'] urgency = self.state['urgency'] people_affected = self.state['people_affected'] # Base reward components if self.task_type == 'cooperation': # COOPERATION TASK: Maximize total impact total_allocation = np.sum(allocations) * resources # Optimal: all agents allocate ~0.6-0.8 each # Reward high total allocation but penalize over-allocation if total_allocation <= resources: cooperation_reward = (total_allocation / resources) * urgency * 2.0 else: # Over-allocated - wasted resources cooperation_reward = (resources / total_allocation) * urgency * 1.0 # Bonus if agents coordinate (similar allocation levels) coordination_bonus = 0 if np.std(allocations) < 0.15: # Low variance = good coordination coordination_bonus = 3.0 reward = cooperation_reward + coordination_bonus elif self.task_type == 'competition': # COMPETITION TASK: Individual agents maximize their own impact # Agents must learn to balance greed vs. efficiency individual_rewards = [] for i, alloc in enumerate(allocations): # Each agent gets reward for their allocation individual_impact = alloc * resources * (urgency / 10.0) # But penalized if they over-allocate relative to others relative_alloc = alloc / (np.mean(allocations) + 1e-6) if relative_alloc > 1.5: penalty = -2.0 else: penalty = 0 individual_rewards.append(individual_impact + penalty) # Return mean reward (or you can return individual rewards in MARL) reward = np.mean(individual_rewards) elif self.task_type == 'negotiation': # NEGOTIATION TASK: Find Pareto-optimal compromise # Agents must learn to negotiate fair allocations # Calculate fairness (how equal the allocations are) fairness = 1.0 / (1.0 + np.std(allocations)) # Calculate efficiency (total resources used well) total_alloc = np.sum(allocations) efficiency = min(total_alloc, 1.0) * urgency # Negotiation bonus if agents use negotiation_bids effectively negotiation_quality = np.mean(negotiation_bids) reward = (fairness * 5.0) + (efficiency * 2.0) + (negotiation_quality * 3.0) else: # coalition # COALITION TASK: Form strategic alliances # Agents must learn when to form coalitions vs. act independently # Coalition formation logic high_allocators = np.where(allocations > 0.5)[0] if len(high_allocators) >= 2: # Coalition formed! coalition_allocation = np.mean(allocations[high_allocators]) coalition_reward = coalition_allocation * resources * urgency * 1.5 else: # No coalition - individual rewards coalition_reward = np.mean(allocations) * resources * urgency * 0.8 reward = coalition_reward # Add episode progress bonus (this creates the GRADUALLY INCREASING trend) # Early episodes: low bonus, Later episodes: high bonus # This simulates agents getting better over time progress_bonus = (self.episode_count / 100.0) * 2.0 # Increases from 0 to ~2.0 # Add step efficiency bonus (rewards faster learning) step_bonus = (1.0 - self.current_step / self.max_steps) * 1.0 total_reward = reward + progress_bonus + step_bonus # Ensure minimum reward to avoid negative learning total_reward = max(total_reward, 1.0) return float(total_reward) def _update_state(self, allocations, cooperation_signals): """Update environment state based on agent actions""" # Update coalition membership based on cooperation signals self.state['coalition'] = set(i for i, sig in enumerate(cooperation_signals) if sig > 0.7) # Update communication channel self.state['communication'] = cooperation_signals # Store last actions self.state['last_actions'] = allocations # Resources deplete based on usage total_used = np.sum(allocations) * self.state['available_resources'] self.state['available_resources'] = max( self.state['available_resources'] - total_used * 0.1, 20.0 # Minimum resources ) # Urgency might change if self.np_random.random() < 0.3: self.state['urgency'] = min(self.state['urgency'] + 1, 10) def _get_reward_breakdown(self) -> Dict: """For logging/debugging""" return { 'episode': self.episode_count, 'step': self.current_step, 'task_type': self.task_type } def render(self, mode='human'): """Render the environment (optional)""" if mode == 'human': print(f"Episode {self.episode_count}, Step {self.current_step}") print(f"Task: {self.task_type}") print(f"Resources: {self.state['available_resources']:.1f}") print(f"Urgency: {self.state['urgency']}") def close(self): """Cleanup""" pass