barcli-mesa / mesa_single.py
Jokar-man's picture
Update mesa_single.py
1e06aa1 verified
Raw
History Blame Contribute Delete
5.39 kB
"""
Single-agent Mesa pathfinding model.
Agent finds the route from start β†’ end minimising a blend of
geographic distance and climate vulnerability cost.
"""
import heapq
import math
import mesa
def amp_cost(score: float) -> float:
if score >= 0.8: return 2.5
if score >= 0.7: return 2.0
if score >= 0.6: return 1.5
if score >= 0.4: return 1.1
if score >= 0.2: return 1.0
return 0.85
# ── Spatial grid for O(1) nearest-neighbour vulnerability lookup ──────────────
# Without this, sample_vuln is O(N) per node β€” for a long A* path exploring
# thousands of nodes Γ— thousands of vuln points = millions of Python ops β†’ timeout.
_CELL_DEG = 0.003 # ~330m per cell; 3Γ—3 search radius covers ~1km
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
# ── Mesa agent ────────────────────────────────────────────────────────────────
class PathfinderAgent(mesa.Agent):
def __init__(self, model):
super().__init__(model)
self.current_node: str = model.start_id
self.arrived: bool = False
self._remaining: list = []
def step(self):
if self.arrived or not self._remaining:
self.arrived = True
return
self.current_node = self._remaining.pop(0)
if self.current_node == self.model.end_id:
self.arrived = True
# ── Mesa model ────────────────────────────────────────────────────────────────
class SingleAgentModel(mesa.Model):
"""
Computes a climate-weighted A* path at construction time.
climate_weight=0 β†’ pure shortest path
climate_weight=1 β†’ maximum climate avoidance
"""
def __init__(
self,
nodes: dict,
edges: dict,
start_id: str,
end_id: str,
vuln_index: list,
climate_weight: float = 0.5,
):
super().__init__()
self.nodes = nodes
self.edges = edges
self.start_id = start_id
self.end_id = end_id
self.climate_weight = climate_weight
self._vuln_cache: dict = {}
self._vuln_grid = _build_grid(vuln_index) # O(M) once; lookups are O(1)
self.agent = PathfinderAgent(self)
path = self._astar(start_id, end_id)
self.agent._remaining = list(path[1:])
self.path_coords = [nodes[nid] for nid in path]
self.vuln_profile = [self._get_vuln(nid) for nid in path]
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 _edge_cost(self, node_id: str, dist: float) -> float:
v = self._get_vuln(node_id)
return dist * (1.0 + self.climate_weight * (amp_cost(v) - 1.0))
def _heuristic(self, a_id: str, b_id: str) -> float:
alng, alat = self.nodes[a_id]
blng, blat = self.nodes[b_id]
dx = (alng - blng) * 111320 * math.cos(math.radians(alat))
dy = (alat - blat) * 110540
return math.sqrt(dx * dx + dy * dy)
def _astar(self, start_id: str, goal_id: str) -> list:
g_score = {start_id: 0.0}
came_from: dict = {}
counter = 0
open_heap = [(self._heuristic(start_id, goal_id), counter, start_id)]
closed: set = set()
while open_heap:
_, _, current = heapq.heappop(open_heap)
if current == goal_id:
path, node = [], current
while node is not None:
path.append(node)
node = came_from.get(node)
return list(reversed(path))
if current in closed:
continue
closed.add(current)
for edge in self.edges.get(current, []):
nb = edge["to"]
if nb in closed:
continue
tg = g_score[current] + self._edge_cost(nb, edge["dist"])
if tg < g_score.get(nb, math.inf):
came_from[nb] = current
g_score[nb] = tg
counter += 1
heapq.heappush(open_heap, (tg + self._heuristic(nb, goal_id), counter, nb))
return []
def step(self):
self.agent.step()