Spaces:
Sleeping
Sleeping
| """ | |
| 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() | |