mpe2 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
mpe2/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.0.1"
File without changes
@@ -0,0 +1,218 @@
1
+ import numpy as np
2
+
3
+
4
+ class EntityState: # physical/external base state of all entities
5
+ def __init__(self):
6
+ # physical position
7
+ self.p_pos = None
8
+ # physical velocity
9
+ self.p_vel = None
10
+
11
+
12
+ class AgentState(
13
+ EntityState
14
+ ): # state of agents (including communication and internal/mental state)
15
+ def __init__(self):
16
+ super().__init__()
17
+ # communication utterance
18
+ self.c = None
19
+
20
+
21
+ class Action: # action of the agent
22
+ def __init__(self):
23
+ # physical action
24
+ self.u = None
25
+ # communication action
26
+ self.c = None
27
+
28
+
29
+ class Entity: # properties and state of physical world entity
30
+ def __init__(self):
31
+ # name
32
+ self.name = ""
33
+ # properties:
34
+ self.size = 0.050
35
+ # entity can move / be pushed
36
+ self.movable = False
37
+ # entity collides with others
38
+ self.collide = True
39
+ # material density (affects mass)
40
+ self.density = 25.0
41
+ # color
42
+ self.color = None
43
+ # max speed and accel
44
+ self.max_speed = None
45
+ self.accel = None
46
+ # state
47
+ self.state = EntityState()
48
+ # mass
49
+ self.initial_mass = 1.0
50
+
51
+ @property
52
+ def mass(self):
53
+ return self.initial_mass
54
+
55
+
56
+ class Landmark(Entity): # properties of landmark entities
57
+ def __init__(self):
58
+ super().__init__()
59
+
60
+
61
+ class Agent(Entity): # properties of agent entities
62
+ def __init__(self):
63
+ super().__init__()
64
+ # agents are movable by default
65
+ self.movable = True
66
+ # cannot send communication signals
67
+ self.silent = False
68
+ # cannot observe the world
69
+ self.blind = False
70
+ # physical motor noise amount
71
+ self.u_noise = None
72
+ # communication noise amount
73
+ self.c_noise = None
74
+ # control range
75
+ self.u_range = 1.0
76
+ # state
77
+ self.state = AgentState()
78
+ # action
79
+ self.action = Action()
80
+ # script behavior to execute
81
+ self.action_callback = None
82
+
83
+
84
+ class World: # multi-agent world
85
+ def __init__(self):
86
+ # list of agents and entities (can change at execution-time!)
87
+ self.agents = []
88
+ self.landmarks = []
89
+ # communication channel dimensionality
90
+ self.dim_c = 0
91
+ # position dimensionality
92
+ self.dim_p = 2
93
+ # color dimensionality
94
+ self.dim_color = 3
95
+ # simulation timestep
96
+ self.dt = 0.1
97
+ # physical damping
98
+ self.damping = 0.25
99
+ # contact response parameters
100
+ self.contact_force = 1e2
101
+ self.contact_margin = 1e-3
102
+
103
+ # return all entities in the world
104
+ @property
105
+ def entities(self):
106
+ return self.agents + self.landmarks
107
+
108
+ # return all agents controllable by external policies
109
+ @property
110
+ def policy_agents(self):
111
+ return [agent for agent in self.agents if agent.action_callback is None]
112
+
113
+ # return all agents controlled by world scripts
114
+ @property
115
+ def scripted_agents(self):
116
+ return [agent for agent in self.agents if agent.action_callback is not None]
117
+
118
+ # update state of the world
119
+ def step(self):
120
+ # set actions for scripted agents
121
+ for agent in self.scripted_agents:
122
+ agent.action = agent.action_callback(agent, self)
123
+ # gather forces applied to entities
124
+ p_force = [None] * len(self.entities)
125
+ # apply agent physical controls
126
+ p_force = self.apply_action_force(p_force)
127
+ # apply environment forces
128
+ p_force = self.apply_environment_force(p_force)
129
+ # integrate physical state
130
+ self.integrate_state(p_force)
131
+ # update agent state
132
+ for agent in self.agents:
133
+ self.update_agent_state(agent)
134
+
135
+ # gather agent action forces
136
+ def apply_action_force(self, p_force):
137
+ # set applied forces
138
+ for i, agent in enumerate(self.agents):
139
+ if agent.movable:
140
+ noise = (
141
+ np.random.randn(*agent.action.u.shape) * agent.u_noise
142
+ if agent.u_noise
143
+ else 0.0
144
+ )
145
+ p_force[i] = agent.action.u + noise
146
+ return p_force
147
+
148
+ # gather physical forces acting on entities
149
+ def apply_environment_force(self, p_force):
150
+ # simple (but inefficient) collision response
151
+ for a, entity_a in enumerate(self.entities):
152
+ for b, entity_b in enumerate(self.entities):
153
+ if b <= a:
154
+ continue
155
+ [f_a, f_b] = self.get_collision_force(entity_a, entity_b)
156
+ if f_a is not None:
157
+ if p_force[a] is None:
158
+ p_force[a] = 0.0
159
+ p_force[a] = f_a + p_force[a]
160
+ if f_b is not None:
161
+ if p_force[b] is None:
162
+ p_force[b] = 0.0
163
+ p_force[b] = f_b + p_force[b]
164
+ return p_force
165
+
166
+ # integrate physical state
167
+ def integrate_state(self, p_force):
168
+ for i, entity in enumerate(self.entities):
169
+ if not entity.movable:
170
+ continue
171
+ entity.state.p_pos += entity.state.p_vel * self.dt
172
+ entity.state.p_vel = entity.state.p_vel * (1 - self.damping)
173
+ if p_force[i] is not None:
174
+ entity.state.p_vel += (p_force[i] / entity.mass) * self.dt
175
+ if entity.max_speed is not None:
176
+ speed = np.sqrt(
177
+ np.square(entity.state.p_vel[0]) + np.square(entity.state.p_vel[1])
178
+ )
179
+ if speed > entity.max_speed:
180
+ entity.state.p_vel = (
181
+ entity.state.p_vel
182
+ / np.sqrt(
183
+ np.square(entity.state.p_vel[0])
184
+ + np.square(entity.state.p_vel[1])
185
+ )
186
+ * entity.max_speed
187
+ )
188
+
189
+ def update_agent_state(self, agent):
190
+ # set communication state (directly for now)
191
+ if agent.silent:
192
+ agent.state.c = np.zeros(self.dim_c)
193
+ else:
194
+ noise = (
195
+ np.random.randn(*agent.action.c.shape) * agent.c_noise
196
+ if agent.c_noise
197
+ else 0.0
198
+ )
199
+ agent.state.c = agent.action.c + noise
200
+
201
+ # get collision forces for any contact between two entities
202
+ def get_collision_force(self, entity_a, entity_b):
203
+ if (not entity_a.collide) or (not entity_b.collide):
204
+ return [None, None] # not a collider
205
+ if entity_a is entity_b:
206
+ return [None, None] # don't collide against itself
207
+ # compute actual distance between entities
208
+ delta_pos = entity_a.state.p_pos - entity_b.state.p_pos
209
+ dist = np.sqrt(np.sum(np.square(delta_pos)))
210
+ # minimum allowable distance
211
+ dist_min = entity_a.size + entity_b.size
212
+ # softmax penetration
213
+ k = self.contact_margin
214
+ penetration = np.logaddexp(0, -(dist - dist_min) / k) * k
215
+ force = self.contact_force * delta_pos / dist * penetration
216
+ force_a = +force if entity_a.movable else None
217
+ force_b = -force if entity_b.movable else None
218
+ return [force_a, force_b]
@@ -0,0 +1,6 @@
1
+ class BaseScenario: # defines scenario upon which the world is built
2
+ def make_world(self): # create elements of the world
3
+ raise NotImplementedError()
4
+
5
+ def reset_world(self, world, np_random): # create initial conditions of the world
6
+ raise NotImplementedError()
Binary file
@@ -0,0 +1,360 @@
1
+ import os
2
+
3
+ import gymnasium
4
+ import numpy as np
5
+ import pygame
6
+ from gymnasium import spaces
7
+ from gymnasium.utils import seeding
8
+ from pettingzoo import AECEnv
9
+ from pettingzoo.utils import wrappers
10
+ from pettingzoo.utils.agent_selector import AgentSelector
11
+
12
+ from mpe2._mpe_utils.core import Agent
13
+
14
+ alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
15
+
16
+
17
+ def make_env(raw_env):
18
+ def env(**kwargs):
19
+ env = raw_env(**kwargs)
20
+ if env.continuous_actions:
21
+ env = wrappers.ClipOutOfBoundsWrapper(env)
22
+ else:
23
+ env = wrappers.AssertOutOfBoundsWrapper(env)
24
+ env = wrappers.OrderEnforcingWrapper(env)
25
+ return env
26
+
27
+ return env
28
+
29
+
30
+ class SimpleEnv(AECEnv):
31
+ metadata = {
32
+ "render_modes": ["human", "rgb_array"],
33
+ "is_parallelizable": True,
34
+ "render_fps": 10,
35
+ }
36
+
37
+ def __init__(
38
+ self,
39
+ scenario,
40
+ world,
41
+ max_cycles,
42
+ render_mode=None,
43
+ continuous_actions=False,
44
+ local_ratio=None,
45
+ dynamic_rescaling=False,
46
+ ):
47
+ super().__init__()
48
+
49
+ self.render_mode = render_mode
50
+ pygame.init()
51
+ self.viewer = None
52
+ self.width = 700
53
+ self.height = 700
54
+ self.screen = pygame.Surface([self.width, self.height])
55
+ self.max_size = 1
56
+ self.game_font = pygame.freetype.Font(
57
+ os.path.join(os.path.dirname(__file__), "secrcode.ttf"), 24
58
+ )
59
+
60
+ # Set up the drawing window
61
+
62
+ self.renderOn = False
63
+ self._seed()
64
+
65
+ self.max_cycles = max_cycles
66
+ self.scenario = scenario
67
+ self.world = world
68
+ self.continuous_actions = continuous_actions
69
+ self.local_ratio = local_ratio
70
+ self.dynamic_rescaling = dynamic_rescaling
71
+
72
+ self.scenario.reset_world(self.world, self.np_random)
73
+
74
+ self.agents = [agent.name for agent in self.world.agents]
75
+ self.possible_agents = self.agents[:]
76
+ self._index_map = {
77
+ agent.name: idx for idx, agent in enumerate(self.world.agents)
78
+ }
79
+
80
+ self._agent_selector = AgentSelector(self.agents)
81
+
82
+ # set spaces
83
+ self.action_spaces = dict()
84
+ self.observation_spaces = dict()
85
+ state_dim = 0
86
+ for agent in self.world.agents:
87
+ if agent.movable:
88
+ space_dim = self.world.dim_p * 2 + 1
89
+ elif self.continuous_actions:
90
+ space_dim = 0
91
+ else:
92
+ space_dim = 1
93
+ if not agent.silent:
94
+ if self.continuous_actions:
95
+ space_dim += self.world.dim_c
96
+ else:
97
+ space_dim *= self.world.dim_c
98
+
99
+ obs_dim = len(self.scenario.observation(agent, self.world))
100
+ state_dim += obs_dim
101
+ if self.continuous_actions:
102
+ self.action_spaces[agent.name] = spaces.Box(
103
+ low=0, high=1, shape=(space_dim,)
104
+ )
105
+ else:
106
+ self.action_spaces[agent.name] = spaces.Discrete(space_dim)
107
+ self.observation_spaces[agent.name] = spaces.Box(
108
+ low=-np.float32(np.inf),
109
+ high=+np.float32(np.inf),
110
+ shape=(obs_dim,),
111
+ dtype=np.float32,
112
+ )
113
+
114
+ self.state_space = spaces.Box(
115
+ low=-np.float32(np.inf),
116
+ high=+np.float32(np.inf),
117
+ shape=(state_dim,),
118
+ dtype=np.float32,
119
+ )
120
+
121
+ # Get the original cam_range
122
+ # This will be used to scale the rendering
123
+ all_poses = [entity.state.p_pos for entity in self.world.entities]
124
+ self.original_cam_range = np.max(np.abs(np.array(all_poses)))
125
+
126
+ self.steps = 0
127
+
128
+ self.current_actions = [None] * self.num_agents
129
+
130
+ def observation_space(self, agent):
131
+ return self.observation_spaces[agent]
132
+
133
+ def action_space(self, agent):
134
+ return self.action_spaces[agent]
135
+
136
+ def _seed(self, seed=None):
137
+ self.np_random, seed = seeding.np_random(seed)
138
+
139
+ def observe(self, agent):
140
+ return self.scenario.observation(
141
+ self.world.agents[self._index_map[agent]], self.world
142
+ ).astype(np.float32)
143
+
144
+ def state(self):
145
+ states = tuple(
146
+ self.scenario.observation(
147
+ self.world.agents[self._index_map[agent]], self.world
148
+ ).astype(np.float32)
149
+ for agent in self.possible_agents
150
+ )
151
+ return np.concatenate(states, axis=None)
152
+
153
+ def reset(self, seed=None, options=None):
154
+ if seed is not None:
155
+ self._seed(seed=seed)
156
+ self.scenario.reset_world(self.world, self.np_random)
157
+
158
+ self.agents = self.possible_agents[:]
159
+ self.rewards = {name: 0.0 for name in self.agents}
160
+ self._cumulative_rewards = {name: 0.0 for name in self.agents}
161
+ self.terminations = {name: False for name in self.agents}
162
+ self.truncations = {name: False for name in self.agents}
163
+ self.infos = {name: {} for name in self.agents}
164
+
165
+ self.agent_selection = self._agent_selector.reset()
166
+ self.steps = 0
167
+
168
+ self.current_actions = [None] * self.num_agents
169
+
170
+ def _execute_world_step(self):
171
+ # set action for each agent
172
+ for i, agent in enumerate(self.world.agents):
173
+ action = self.current_actions[i]
174
+ scenario_action = []
175
+ if agent.movable:
176
+ mdim = self.world.dim_p * 2 + 1
177
+ if self.continuous_actions:
178
+ scenario_action.append(action[0:mdim])
179
+ action = action[mdim:]
180
+ else:
181
+ scenario_action.append(action % mdim)
182
+ action //= mdim
183
+ if not agent.silent:
184
+ scenario_action.append(action)
185
+ self._set_action(scenario_action, agent, self.action_spaces[agent.name])
186
+
187
+ self.world.step()
188
+
189
+ global_reward = 0.0
190
+ if self.local_ratio is not None:
191
+ global_reward = float(self.scenario.global_reward(self.world))
192
+
193
+ for agent in self.world.agents:
194
+ agent_reward = float(self.scenario.reward(agent, self.world))
195
+ if self.local_ratio is not None:
196
+ reward = (
197
+ global_reward * (1 - self.local_ratio)
198
+ + agent_reward * self.local_ratio
199
+ )
200
+ else:
201
+ reward = agent_reward
202
+
203
+ self.rewards[agent.name] = reward
204
+
205
+ # set env action for a particular agent
206
+ def _set_action(self, action, agent, action_space, time=None):
207
+ agent.action.u = np.zeros(self.world.dim_p)
208
+ agent.action.c = np.zeros(self.world.dim_c)
209
+
210
+ if agent.movable:
211
+ # physical action
212
+ agent.action.u = np.zeros(self.world.dim_p)
213
+ if self.continuous_actions:
214
+ # Process continuous action as in OpenAI MPE
215
+ # Note: this ordering preserves the same movement direction as in the discrete case
216
+ agent.action.u[0] += action[0][2] - action[0][1]
217
+ agent.action.u[1] += action[0][4] - action[0][3]
218
+ else:
219
+ # process discrete action
220
+ if action[0] == 1:
221
+ agent.action.u[0] = -1.0
222
+ if action[0] == 2:
223
+ agent.action.u[0] = +1.0
224
+ if action[0] == 3:
225
+ agent.action.u[1] = -1.0
226
+ if action[0] == 4:
227
+ agent.action.u[1] = +1.0
228
+ sensitivity = 5.0
229
+ if agent.accel is not None:
230
+ sensitivity = agent.accel
231
+ agent.action.u *= sensitivity
232
+ action = action[1:]
233
+ if not agent.silent:
234
+ # communication action
235
+ if self.continuous_actions:
236
+ agent.action.c = action[0]
237
+ else:
238
+ agent.action.c = np.zeros(self.world.dim_c)
239
+ agent.action.c[action[0]] = 1.0
240
+ action = action[1:]
241
+ # make sure we used all elements of action
242
+ assert len(action) == 0
243
+
244
+ def step(self, action):
245
+ if (
246
+ self.terminations[self.agent_selection]
247
+ or self.truncations[self.agent_selection]
248
+ ):
249
+ self._was_dead_step(action)
250
+ return
251
+ cur_agent = self.agent_selection
252
+ current_idx = self._index_map[self.agent_selection]
253
+ next_idx = (current_idx + 1) % self.num_agents
254
+ self.agent_selection = self._agent_selector.next()
255
+
256
+ self.current_actions[current_idx] = action
257
+
258
+ if next_idx == 0:
259
+ self._execute_world_step()
260
+ self.steps += 1
261
+ if self.steps >= self.max_cycles:
262
+ for a in self.agents:
263
+ self.truncations[a] = True
264
+ else:
265
+ self._clear_rewards()
266
+
267
+ self._cumulative_rewards[cur_agent] = 0
268
+ self._accumulate_rewards()
269
+
270
+ if self.render_mode == "human":
271
+ self.render()
272
+
273
+ def enable_render(self, mode="human"):
274
+ if not self.renderOn and mode == "human":
275
+ self.screen = pygame.display.set_mode(self.screen.get_size())
276
+ self.clock = pygame.time.Clock()
277
+ self.renderOn = True
278
+
279
+ def render(self):
280
+ if self.render_mode is None:
281
+ gymnasium.logger.warn(
282
+ "You are calling render method without specifying any render mode."
283
+ )
284
+ return
285
+
286
+ self.enable_render(self.render_mode)
287
+
288
+ self.draw()
289
+ if self.render_mode == "rgb_array":
290
+ observation = np.array(pygame.surfarray.pixels3d(self.screen))
291
+ return np.transpose(observation, axes=(1, 0, 2))
292
+ elif self.render_mode == "human":
293
+ pygame.display.flip()
294
+ self.clock.tick(self.metadata["render_fps"])
295
+ return
296
+
297
+ def draw(self):
298
+ # clear screen
299
+ self.screen.fill((255, 255, 255))
300
+
301
+ # update bounds to center around agent
302
+ all_poses = [entity.state.p_pos for entity in self.world.entities]
303
+ cam_range = np.max(np.abs(np.array(all_poses)))
304
+
305
+ # The scaling factor is used for dynamic rescaling of the rendering - a.k.a Zoom In/Zoom Out effect
306
+ # The 0.9 is a factor to keep the entities from appearing "too" out-of-bounds
307
+ scaling_factor = 0.9 * self.original_cam_range / cam_range
308
+
309
+ # update geometry and text positions
310
+ text_line = 0
311
+ for e, entity in enumerate(self.world.entities):
312
+ # geometry
313
+ x, y = entity.state.p_pos
314
+ y *= (
315
+ -1
316
+ ) # this makes the display mimic the old pyglet setup (ie. flips image)
317
+ x = (
318
+ (x / cam_range) * self.width // 2 * 0.9
319
+ ) # the .9 is just to keep entities from appearing "too" out-of-bounds
320
+ y = (y / cam_range) * self.height // 2 * 0.9
321
+ x += self.width // 2
322
+ y += self.height // 2
323
+
324
+ # 350 is an arbitrary scale factor to get pygame to render similar sizes as pyglet
325
+ if self.dynamic_rescaling:
326
+ radius = entity.size * 350 * scaling_factor
327
+ else:
328
+ radius = entity.size * 350
329
+
330
+ pygame.draw.circle(self.screen, entity.color * 200, (x, y), radius)
331
+ pygame.draw.circle(self.screen, (0, 0, 0), (x, y), radius, 1) # borders
332
+ assert (
333
+ 0 < x < self.width and 0 < y < self.height
334
+ ), f"Coordinates {(x, y)} are out of bounds."
335
+
336
+ # text
337
+ if isinstance(entity, Agent):
338
+ if entity.silent:
339
+ continue
340
+ if np.all(entity.state.c == 0):
341
+ word = "_"
342
+ elif self.continuous_actions:
343
+ word = (
344
+ "[" + ",".join([f"{comm:.2f}" for comm in entity.state.c]) + "]"
345
+ )
346
+ else:
347
+ word = alphabet[np.argmax(entity.state.c)]
348
+
349
+ message = entity.name + " sends " + word + " "
350
+ message_x_pos = self.width * 0.05
351
+ message_y_pos = self.height * 0.95 - (self.height * 0.05 * text_line)
352
+ self.game_font.render_to(
353
+ self.screen, (message_x_pos, message_y_pos), message, (0, 0, 0)
354
+ )
355
+ text_line += 1
356
+
357
+ def close(self):
358
+ if self.screen is not None:
359
+ pygame.quit()
360
+ self.screen = None
mpe2/all_modules.py ADDED
@@ -0,0 +1,23 @@
1
+ from mpe2 import (
2
+ simple_adversary_v3,
3
+ simple_crypto_v3,
4
+ simple_push_v3,
5
+ simple_reference_v3,
6
+ simple_speaker_listener_v4,
7
+ simple_spread_v3,
8
+ simple_tag_v3,
9
+ simple_v3,
10
+ simple_world_comm_v3,
11
+ )
12
+
13
+ mpe_environments = {
14
+ "mpe/simple_adversary_v3": simple_adversary_v3,
15
+ "mpe/simple_crypto_v3": simple_crypto_v3,
16
+ "mpe/simple_push_v3": simple_push_v3,
17
+ "mpe/simple_reference_v3": simple_reference_v3,
18
+ "mpe/simple_speaker_listener_v4": simple_speaker_listener_v4,
19
+ "mpe/simple_spread_v3": simple_spread_v3,
20
+ "mpe/simple_tag_v3": simple_tag_v3,
21
+ "mpe/simple_world_comm_v3": simple_world_comm_v3,
22
+ "mpe/simple_v3": simple_v3,
23
+ }