Spaces:
Sleeping
Sleeping
File size: 7,546 Bytes
de9025d 8147138 de9025d 8147138 de9025d 8147138 de9025d 674bac5 de9025d 8147138 de9025d 674bac5 de9025d 8147138 de9025d | 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 | """
Multi-agent Mesa shelter-seeking model with 4 personas Γ 2 genders.
20 agents total (5 per persona, 10F + 10M).
Each agent random-walks until cumulative heat stress crosses the
persona-specific threshold, then greedily seeks the nearest shelter.
Uses explicit per-step loops instead of mesa.time schedulers so the
code works with any Mesa 2.x version.
"""
import math
import mesa
# ββ Personas ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
PERSONAS = [
{"id": "old", "label": "Elderly", "stress_threshold": 2.0, "heat_trigger": 0.30},
{"id": "middle", "label": "Middle-aged", "stress_threshold": 4.0, "heat_trigger": 0.45},
{"id": "young", "label": "Young Adult", "stress_threshold": 6.0, "heat_trigger": 0.55},
{"id": "kids", "label": "Kids", "stress_threshold": 2.5, "heat_trigger": 0.35},
]
_GENDER_PATTERNS = [
["F", "M", "F", "M", "F"], # old β 3F 2M
["M", "F", "M", "F", "M"], # middle β 2F 3M
["F", "M", "F", "M", "F"], # young β 3F 2M
["M", "F", "M", "F", "M"], # kids β 2F 3M
]
AGENT_ROSTER = [
{"persona": p["id"], "gender": g}
for pi, p in enumerate(PERSONAS)
for g in _GENDER_PATTERNS[pi]
] # exactly 20 entries
# ββ Spatial grid for O(1) nearest-neighbour lookup βββββββββββββββββββββββββββ
_CELL_DEG = 0.003
def _build_grid(vuln_index: list) -> dict:
grid = {}
for p in vuln_index:
cx = int(p["lng"] / _CELL_DEG)
cy = int(p["lat"] / _CELL_DEG)
grid.setdefault((cx, cy), []).append(p)
return grid
def _sample_grid(lng: float, lat: float, grid: dict) -> float:
cx = int(lng / _CELL_DEG)
cy = int(lat / _CELL_DEG)
cos_lat = math.cos(math.radians(lat))
best_d2, best_score = math.inf, 0.0
for dx in range(-2, 3):
for dy in range(-2, 3):
for p in grid.get((cx + dx, cy + dy), []):
ddx = (p["lng"] - lng) * 111320 * cos_lat
ddy = (p["lat"] - lat) * 110540
d2 = ddx * ddx + ddy * ddy
if d2 < best_d2:
best_d2 = d2
best_score = p["score"]
return best_score
def dist_sq(coord_a, coord_b) -> float:
lng1, lat1 = coord_a
lng2, lat2 = coord_b
dx = (lng1 - lng2) * 111320 * math.cos(math.radians(lat1))
dy = (lat1 - lat2) * 110540
return dx * dx + dy * dy
# ββ Mesa agent ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
class ShelterSeekingAgent(mesa.Agent):
"""Accumulates stress while exposed to heat; switches to greedy shelter-seeking once threshold crossed."""
def __init__(self, model, start_node: str, persona: dict, gender: str):
super().__init__(model)
self.current_node = start_node
self.persona = persona
self.gender = gender
self.stress_accum = 0.0
self.seeking = False
self.arrived = (start_node == model.shelter_node)
self.visited = {start_node}
def step(self):
if self.arrived:
return
vuln = self.model.get_vuln(self.current_node)
trigger = self.persona["heat_trigger"]
if vuln > trigger:
self.stress_accum += (vuln - trigger) * 2.0
if not self.seeking and self.stress_accum >= self.persona["stress_threshold"]:
self.seeking = True
neighbours = [e["to"] for e in self.model.edges.get(self.current_node, [])]
if not neighbours:
return
if self.seeking:
shelter_coord = self.model.nodes[self.model.shelter_node]
unvisited = [n for n in neighbours if n not in self.visited]
pool = unvisited if unvisited else neighbours
next_node = min(pool, key=lambda n: dist_sq(self.model.nodes[n], shelter_coord))
if next_node == self.model.shelter_node or self.current_node == self.model.shelter_node:
self.arrived = True
self.current_node = self.model.shelter_node
return
else:
unvisited = [n for n in neighbours if n not in self.visited]
pool = unvisited if unvisited else neighbours
next_node = self.model.random.choice(pool)
self.visited.add(next_node)
self.current_node = next_node
# ββ Mesa model ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
class MultiAgentModel(mesa.Model):
MAX_STEPS = 200
SNAPSHOT_EVERY = 5
def __init__(
self,
nodes: dict,
edges: dict,
shelter_node: str,
start_positions: list,
vuln_index: list,
seed: int = 42,
):
super().__init__(seed=seed)
self.nodes = nodes
self.edges = edges
self.shelter_node = shelter_node
self.vuln_index = vuln_index
self._vuln_cache: dict = {}
self._vuln_grid = _build_grid(vuln_index) # O(M) once; lookups are O(1)
# Create all 20 agents (mesa.Model tracks them in self.agents automatically)
self._agent_list = []
for i, (start_node, entry) in enumerate(zip(start_positions, AGENT_ROSTER)):
persona = next(p for p in PERSONAS if p["id"] == entry["persona"])
agent = ShelterSeekingAgent(self, start_node, persona, entry["gender"])
self._agent_list.append(agent)
self._snapshots = [self._snapshot()]
def get_vuln(self, node_id: str) -> float:
if node_id not in self._vuln_cache:
lng, lat = self.nodes[node_id]
self._vuln_cache[node_id] = _sample_grid(lng, lat, self._vuln_grid)
return self._vuln_cache[node_id]
def _snapshot(self) -> list:
return [
{
"id": a.unique_id,
"lng": self.nodes[a.current_node][0],
"lat": self.nodes[a.current_node][1],
"arrived": a.arrived,
"persona": a.persona["id"],
"gender": a.gender,
}
for a in self._agent_list
]
def step(self):
"""Simultaneous activation β all agents read state from t, write to t+1."""
for agent in self._agent_list:
agent.step()
def run(self) -> dict:
for s in range(self.MAX_STEPS):
self.step()
if (s + 1) % self.SNAPSHOT_EVERY == 0 or s == self.MAX_STEPS - 1:
self._snapshots.append(self._snapshot())
arrived = [a for a in self._agent_list if a.arrived]
breakdown = {}
for p in PERSONAS:
pid = p["id"]
group = [a for a in self._agent_list if a.persona["id"] == pid]
breakdown[pid] = {
"label": p["label"],
"total": len(group),
"arrived": sum(1 for a in group if a.arrived),
}
return {
"arrived_count": len(arrived),
"snapshots": self._snapshots,
"persona_breakdown": breakdown,
}
|