File size: 12,082 Bytes
a9d8689
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
"""
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