Spaces:
Sleeping
Sleeping
File size: 5,386 Bytes
de9025d 1e06aa1 de9025d 1e06aa1 de9025d 1e06aa1 de9025d 1e06aa1 5b6f952 de9025d 1e06aa1 de9025d 1e06aa1 de9025d 5b6f952 de9025d 1e06aa1 de9025d 1e06aa1 de9025d 1e06aa1 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 | """
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()
|