-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathphase1.py
More file actions
244 lines (202 loc) · 7.81 KB
/
phase1.py
File metadata and controls
244 lines (202 loc) · 7.81 KB
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
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from mesa import Agent, Model
from mesa.space import ContinuousSpace
# World
WORLD_SIZE = 100
N_INITIAL_AGENTS = 60
INTEREST_DIM = 4
# Resource clouds
N_CLOUDS = 3
CLOUD_RADIUS = 10
PELLETS_PER_CLOUD_PER_TICK = 3
PELLET_LIFETIME = 8
BACKGROUND_PELLET_RATE = 5 # pellets spawned anywhere on map per tick
# Agent behavior
PERCEPTION_RANGE = 20
MOVE_SPEED = 2.5
CONSUMPTION_PER_TICK = 0.2
INITIAL_RESOURCES = 15
# Reproduction
REPRODUCTION_RESOURCE_THRESHOLD = 10
REPRODUCTION_FERTILITY_THRESHOLD = 3 # ticks of being well-fed
WELL_FED_LEVEL = 3 # resource level above which fertility accumulates
REPRODUCTION_COST = 2
REPRODUCTION_DISTANCE = 3
REPRODUCTION_ALIGNMENT_THRESHOLD = 0.3
MUTATION_RATE = 0.15
def cosine_similarity(v1, v2):
n1 = np.linalg.norm(v1)
n2 = np.linalg.norm(v2)
if n1 == 0 or n2 == 0:
return 0
return np.dot(v1, v2) / (n1 * n2)
class Pellet:
"""A single resource unit that exists at a position for a limited time."""
def __init__(self, pos, lifetime):
self.pos = np.array(pos, dtype=float)
self.lifetime = lifetime
self.alive = True
def tick(self):
self.lifetime -= 1
if self.lifetime <= 0:
self.alive = False
class ResourceCloud:
"""A region that periodically spawns pellets within its radius."""
def __init__(self, center, radius, spawn_rate):
self.center = np.array(center, dtype=float)
self.radius = radius
self.spawn_rate = spawn_rate
def spawn_pellets(self):
pellets = []
for _ in range(self.spawn_rate):
# Sample point inside circle
angle = np.random.uniform(0, 2 * np.pi)
r = np.random.uniform(0, self.radius)
pos = self.center + np.array([np.cos(angle), np.sin(angle)]) * r
pos = np.clip(pos, 0, WORLD_SIZE)
pellets.append(Pellet(pos, PELLET_LIFETIME))
return pellets
class PoliticalAgent(Agent):
def __init__(self, model, pos, interest_vector, resources):
super().__init__(model)
self.pos = np.array(pos, dtype=float)
self.interest_vector = interest_vector
self.resources = resources
self.alive = True
self.fertility = 0 # accumulates while well-fed
def step(self):
if not self.alive:
return
# Find visible pellets
visible = [
p for p in self.model.pellets
if p.alive and np.linalg.norm(p.pos - self.pos) <= PERCEPTION_RANGE
]
# Move toward nearest visible pellet
if visible:
target = min(visible, key=lambda p: np.linalg.norm(p.pos - self.pos))
direction = target.pos - self.pos
dist = np.linalg.norm(direction)
if dist > 0:
step_vec = (direction / dist) * min(MOVE_SPEED, dist)
self.pos += step_vec
self.pos = np.clip(self.pos, 0, WORLD_SIZE)
else:
# Random walk if nothing visible
angle = np.random.uniform(0, 2 * np.pi)
self.pos += np.array([np.cos(angle), np.sin(angle)]) * MOVE_SPEED * 0.5
self.pos = np.clip(self.pos, 0, WORLD_SIZE)
# Eat any pellet within reach
for p in self.model.pellets:
if p.alive and np.linalg.norm(p.pos - self.pos) < 1.5:
self.resources += 1
p.alive = False
break # one pellet per tick
# Consume
self.resources -= CONSUMPTION_PER_TICK
if self.resources <= 0:
self.alive = False
return
# Fertility accumulates while well-fed
if self.resources >= WELL_FED_LEVEL:
self.fertility += 1
else:
self.fertility = max(0, self.fertility - 1)
self.try_reproduce()
def try_reproduce(self):
if self.resources < REPRODUCTION_RESOURCE_THRESHOLD:
return
if self.fertility < REPRODUCTION_FERTILITY_THRESHOLD:
return
for other in list(self.model.agents):
if other is self or not other.alive:
continue
if other.resources < REPRODUCTION_RESOURCE_THRESHOLD:
continue
if other.fertility < REPRODUCTION_FERTILITY_THRESHOLD:
continue
if np.linalg.norm(other.pos - self.pos) > REPRODUCTION_DISTANCE:
continue
alignment = cosine_similarity(self.interest_vector, other.interest_vector)
if alignment < REPRODUCTION_ALIGNMENT_THRESHOLD:
continue
# Reproduce
self.resources -= REPRODUCTION_COST
other.resources -= REPRODUCTION_COST
self.fertility = 0
other.fertility = 0
child_pos = (self.pos + other.pos) / 2
child_vec = (self.interest_vector + other.interest_vector) / 2
mutation = np.random.normal(0, MUTATION_RATE, INTEREST_DIM)
child_vec = child_vec + mutation
PoliticalAgent(self.model, child_pos, child_vec, REPRODUCTION_COST * 2)
return
class PoliticalModel(Model):
def __init__(self):
super().__init__()
self.space = ContinuousSpace(WORLD_SIZE, WORLD_SIZE, torus=False)
self.clouds = self._generate_clouds()
self.pellets = []
for _ in range(N_INITIAL_AGENTS):
pos = np.random.uniform(0, WORLD_SIZE, 2)
vec = np.random.uniform(-1, 1, INTEREST_DIM)
PoliticalAgent(self, pos, vec, INITIAL_RESOURCES)
def _generate_clouds(self):
clouds = []
centers = np.random.uniform(20, WORLD_SIZE - 20, (N_CLOUDS, 2))
for c in centers:
clouds.append(ResourceCloud(c, CLOUD_RADIUS, PELLETS_PER_CLOUD_PER_TICK))
return clouds
def step(self):
# Spawn new pellets
for cloud in self.clouds:
self.pellets.extend(cloud.spawn_pellets())
# Background pellets anywhere
for _ in range(BACKGROUND_PELLET_RATE):
pos = np.random.uniform(0, WORLD_SIZE, 2)
self.pellets.append(Pellet(pos, PELLET_LIFETIME))
# Age pellets
for p in self.pellets:
p.tick()
self.pellets = [p for p in self.pellets if p.alive]
# Agents act
self.agents.shuffle_do("step")
# Remove dead
dead = [a for a in self.agents if not a.alive]
for a in dead:
a.remove()
def living_agents(self):
return [a for a in self.agents if a.alive]
def run_simulation():
model = PoliticalModel()
fig, ax = plt.subplots(figsize=(10, 10))
def update(frame):
ax.clear()
ax.set_xlim(0, WORLD_SIZE)
ax.set_ylim(0, WORLD_SIZE)
agents = model.living_agents()
ax.set_title(f"Tick {frame}, agents: {len(agents)}, pellets: {len(model.pellets)}")
# Draw cloud regions as faint circles
for cloud in model.clouds:
circle = plt.Circle(cloud.center, cloud.radius, color="green", alpha=0.08)
ax.add_patch(circle)
# Draw pellets
if model.pellets:
px = [p.pos[0] for p in model.pellets]
py = [p.pos[1] for p in model.pellets]
ax.scatter(px, py, c="green", s=8, alpha=0.6, marker="s")
# Draw agents
if agents:
ax_x = [a.pos[0] for a in agents]
ax_y = [a.pos[1] for a in agents]
ax_c = [a.interest_vector[0] for a in agents]
ax_s = [max(15, a.resources * 3) for a in agents]
ax.scatter(ax_x, ax_y, c=ax_c, cmap="coolwarm", s=ax_s,
edgecolors="black", linewidths=0.5, vmin=-1, vmax=1)
model.step()
anim = FuncAnimation(fig, update, frames=400, interval=80, repeat=False)
plt.show()
if __name__ == "__main__":
run_simulation()