Spaces:
Running on Zero
Running on Zero
| """NEMOCITY traffic — the Python static-assignment mirror. | |
| Same trip generation + A* + demand semantics as web/city/traffic.js, from the | |
| same event list and constants, so the ENGINE owns the facts the LLM sees (the | |
| live car queueing client-side is presentation and may drift a car or two). | |
| Determinism contract with the JS mirror: | |
| * mulberry32 (bit-exact port of web/city/rng.js); ONE draw per commuter, | |
| seeded `building.seed ^ i`; | |
| * trip order = building event order, commuter index ascending; | |
| * destination roulette over completed jobs/attract buildings in event order, | |
| weight = (jobs + 2*attract) / (1 + manhattanCentroidDist^1.5); | |
| * door = adjacent road cell with smallest (cz, cx); | |
| * incremental loading: each commuter routes by A* with | |
| cost(cell) = (1/speed) * (1 + 2*demandRatio(cell)) over demand loaded so | |
| far, then adds DEMAND_PER_COMMUTER to every cell of its path. | |
| Stats are evaluated AT PEAK RUSH (rushFactor = 1.0). Pure stdlib. | |
| """ | |
| from __future__ import annotations | |
| import heapq | |
| import time | |
| from dataclasses import dataclass, field | |
| from typing import Any, Optional | |
| from . import constants as C | |
| from .city import WATER_CELLS, Building, Cell, CityState, in_bounds, neighbors4 | |
| from .tools import street_name_for | |
| _MIN_CELL_COST = 1.0 / C.ROAD_CLASSES["avenue"]["speed"] # admissible A* heuristic | |
| def mulberry32(seed: int): | |
| """Bit-exact port of web/city/rng.js mulberry32 — keep byte-stable.""" | |
| a = seed & 0xFFFFFFFF | |
| def rng() -> float: | |
| nonlocal a | |
| a = (a + 0x6D2B79F5) & 0xFFFFFFFF | |
| t = a | |
| t = ((t ^ (t >> 15)) * (t | 1)) & 0xFFFFFFFF | |
| t = (t ^ ((t + (((t ^ (t >> 7)) * (t | 61)) & 0xFFFFFFFF)) & 0xFFFFFFFF)) & 0xFFFFFFFF | |
| return ((t ^ (t >> 14)) & 0xFFFFFFFF) / 4294967296.0 | |
| return rng | |
| # -------------------------------------------------------------------- road net | |
| class RoadNet: | |
| """Road cells + class/name, optionally with a candidate fix applied.""" | |
| cells: dict[Cell, tuple[str, str]] # cell -> (klass, name) | |
| def from_city(cls, city: CityState, fix: Optional[dict] = None) -> "RoadNet": | |
| cells = {cell: (rc.klass, rc.name) for cell, rc in city.roads.items()} | |
| if fix: | |
| if fix["action"] == "new_road": | |
| for cx, cz in fix["cells"]: | |
| cells[(cx, cz)] = (fix["klass"], fix["name"]) | |
| elif fix["action"] == "upgrade_avenue": | |
| for cx, cz in fix["cells"]: | |
| cell = (cx, cz) | |
| if cell in cells: | |
| cells[cell] = ("avenue", cells[cell][1]) | |
| return cls(cells=cells) | |
| def capacity(self, cell: Cell) -> float: | |
| return C.ROAD_CLASSES[self.cells[cell][0]]["capacity"] | |
| def speed(self, cell: Cell) -> float: | |
| return C.ROAD_CLASSES[self.cells[cell][0]]["speed"] | |
| def name(self, cell: Cell) -> str: | |
| return self.cells[cell][1] | |
| def door(self, b: Building) -> Optional[Cell]: | |
| adjacent = { | |
| n for cell in b.cells for n in neighbors4(cell) if n in self.cells | |
| } | |
| if not adjacent: | |
| return None | |
| return min(adjacent, key=lambda c: (c[1], c[0])) | |
| # -------------------------------------------------------------------- trip gen | |
| def trip_table(city: CityState, now_s: float) -> list[tuple[Building, Building]]: | |
| """Static OD pairs (gravity model, seeded roulette). Completed buildings | |
| only on both ends; recomputed only when buildings change.""" | |
| completed = [b for b in city.buildings if b.progress(now_s) >= 1.0] | |
| homes = [b for b in completed if C.BUILDINGS[b.kind]["residents"] > 0] | |
| dests = [ | |
| b for b in completed | |
| if C.BUILDINGS[b.kind]["jobs"] + C.BUILDINGS[b.kind]["attract"] > 0 | |
| ] | |
| if not dests: | |
| return [] | |
| trips: list[tuple[Building, Building]] = [] | |
| for home in homes: | |
| hx, hz = home.centroid | |
| weights: list[float] = [] | |
| for dest in dests: | |
| dx, dz = dest.centroid | |
| dist = abs(dx - hx) + abs(dz - hz) | |
| spec = C.BUILDINGS[dest.kind] | |
| weights.append( | |
| (spec["jobs"] + C.TRIP_ATTRACT_FACTOR * spec["attract"]) | |
| / (1 + dist ** C.TRIP_DIST_EXP) | |
| ) | |
| total = sum(weights) | |
| if total <= 0: | |
| continue | |
| for i in range(C.BUILDINGS[home.kind]["residents"]): | |
| r = mulberry32((home.seed ^ i) & 0xFFFFFFFF)() * total | |
| acc = 0.0 | |
| chosen = dests[-1] | |
| for dest, weight in zip(dests, weights): | |
| acc += weight | |
| if r < acc: | |
| chosen = dest | |
| break | |
| trips.append((home, chosen)) | |
| return trips | |
| # ------------------------------------------------------------------ assignment | |
| class Assignment: | |
| net: RoadNet | |
| demand: dict[Cell, float] = field(default_factory=dict) | |
| paths: list[tuple[Cell, Cell, tuple[Cell, ...]]] = field(default_factory=list) | |
| traffic_index: int = 0 | |
| def ratio(self, cell: Cell) -> float: | |
| return self.demand.get(cell, 0.0) / self.net.capacity(cell) | |
| def max_ratio(self) -> float: | |
| if not self.net.cells: | |
| return 0.0 | |
| return max((self.ratio(c) for c in self.net.cells), default=0.0) | |
| def top_cells(self, n: int) -> list[Cell]: | |
| return sorted( | |
| self.net.cells, | |
| key=lambda c: (-self.ratio(c), c[1], c[0]), | |
| )[:n] | |
| def _astar(net: RoadNet, demand: dict[Cell, float], start: Cell, goal: Cell | |
| ) -> Optional[list[Cell]]: | |
| if start == goal: | |
| return [start] | |
| def cell_cost(cell: Cell) -> float: | |
| ratio = demand.get(cell, 0.0) / net.capacity(cell) | |
| return (1.0 / net.speed(cell)) * (1 + C.CONGESTION_COST_FACTOR * ratio) | |
| def h(cell: Cell) -> float: | |
| return (abs(cell[0] - goal[0]) + abs(cell[1] - goal[1])) * _MIN_CELL_COST | |
| g: dict[Cell, float] = {start: 0.0} | |
| prev: dict[Cell, Cell] = {} | |
| heap: list[tuple[float, int, int, Cell]] = [(h(start), start[1], start[0], start)] | |
| closed: set[Cell] = set() | |
| while heap: | |
| _, _, _, cell = heapq.heappop(heap) | |
| if cell in closed: | |
| continue | |
| if cell == goal: | |
| path = [cell] | |
| while path[-1] != start: | |
| path.append(prev[path[-1]]) | |
| path.reverse() | |
| return path | |
| closed.add(cell) | |
| for n in neighbors4(cell): | |
| if n not in net.cells or n in closed: | |
| continue | |
| cand = g[cell] + cell_cost(n) | |
| if cand < g.get(n, float("inf")): | |
| g[n] = cand | |
| prev[n] = cell | |
| heapq.heappush(heap, (cand + h(n), n[1], n[0], n)) | |
| return None | |
| def assign(city: CityState, now_s: float, fix: Optional[dict] = None) -> Assignment: | |
| """Incremental static assignment at peak rush. <2s for a genesis city.""" | |
| net = RoadNet.from_city(city, fix) | |
| out = Assignment(net=net) | |
| for home, dest in trip_table(city, now_s): | |
| od, dd = net.door(home), net.door(dest) | |
| if od is None or dd is None: | |
| continue | |
| path = _astar(net, out.demand, od, dd) | |
| if path is None: | |
| continue | |
| for cell in path: | |
| out.demand[cell] = out.demand.get(cell, 0.0) + C.DEMAND_PER_COMMUTER | |
| out.paths.append((od, dd, tuple(path))) | |
| ratios = sorted((out.ratio(c) for c in net.cells), reverse=True) | |
| top = ratios[: C.TRAFFIC_TOP_CELLS] | |
| if top: | |
| # pad: a city with fewer road cells than the window still reads honestly | |
| mean = sum(top) / C.TRAFFIC_TOP_CELLS | |
| out.traffic_index = int(round(100 * mean)) | |
| return out | |
| # ----------------------------------------------------------------------- stats | |
| def _intersection_name(net: RoadNet, cell: Cell) -> str: | |
| names = [net.name(cell)] | |
| for n in neighbors4(cell): | |
| if n in net.cells and net.name(n) not in names: | |
| names.append(net.name(n)) | |
| return " & ".join(names[:2]) | |
| def stats(city: CityState, assignment: Assignment) -> dict: | |
| net = assignment.net | |
| top = [ | |
| c for c in assignment.top_cells(5) if assignment.ratio(c) > 0 | |
| ] | |
| worst = top[0] if top else None | |
| intersections = [ | |
| c for c in net.cells | |
| if sum(1 for n in neighbors4(c) if n in net.cells) >= 3 | |
| ] | |
| worst_x = max( | |
| intersections, key=lambda c: (assignment.ratio(c), -c[1], -c[0]), default=None | |
| ) | |
| return { | |
| "traffic_index": assignment.traffic_index, | |
| "max_ratio": round(assignment.max_ratio, 2), | |
| "worst": net.name(worst) if worst else None, | |
| "worst_intersection": _intersection_name(net, worst_x) if worst_x else None, | |
| "top": [ | |
| { | |
| "street": net.name(c), | |
| "cell": [c[0], c[1]], | |
| "ratio": round(assignment.ratio(c), 2), | |
| "klass": net.cells[c][0], | |
| } | |
| for c in top | |
| ], | |
| } | |
| # ------------------------------------------------------------------ candidates | |
| def _bypass_candidate( | |
| city: CityState, assignment: Assignment, now_s: float | |
| ) -> Optional[dict]: | |
| """Engine-routed relief road around the worst cell: Dijkstra over the grid, | |
| new cells cost extra, water at 4x (a fix may deliberately bridge), jammed | |
| cells forbidden.""" | |
| net = assignment.net | |
| jammed = { | |
| c for c in net.cells if assignment.ratio(c) >= C.FIX_AVOID_RATIO | |
| } | |
| if not jammed: | |
| return None | |
| worst = min(jammed, key=lambda c: (-assignment.ratio(c), c[1], c[0])) | |
| od_counts: dict[tuple[Cell, Cell], int] = {} | |
| for od, dd, path in assignment.paths: | |
| if worst in path: | |
| od_counts[(od, dd)] = od_counts.get((od, dd), 0) + 1 | |
| if not od_counts: | |
| return None | |
| (start, goal), _ = min( | |
| od_counts.items(), key=lambda kv: (-kv[1], kv[0][0][1], kv[0][0][0]) | |
| ) | |
| def cost(cell: Cell) -> Optional[float]: | |
| if cell in jammed and cell not in (start, goal): | |
| return None | |
| if cell in net.cells: | |
| return 1.0 / net.speed(cell) | |
| if cell in city.building_cells: | |
| return None | |
| if cell in WATER_CELLS: | |
| return C.FIX_NEW_CELL_COST * C.FIX_WATER_COST_MULT | |
| return C.FIX_NEW_CELL_COST | |
| dist: dict[Cell, float] = {start: 0.0} | |
| prev: dict[Cell, Cell] = {} | |
| heap: list[tuple[float, int, int, Cell]] = [(0.0, start[1], start[0], start)] | |
| closed: set[Cell] = set() | |
| while heap: | |
| d, _, _, cell = heapq.heappop(heap) | |
| if cell in closed: | |
| continue | |
| if cell == goal: | |
| break | |
| closed.add(cell) | |
| for n in neighbors4(cell): | |
| if n in closed: | |
| continue | |
| c = cost(n) | |
| if c is None: | |
| continue | |
| cand = d + c | |
| if cand < dist.get(n, float("inf")): | |
| dist[n] = cand | |
| prev[n] = cell | |
| heapq.heappush(heap, (cand, n[1], n[0], n)) | |
| if goal not in prev and goal != start: | |
| return None | |
| path = [goal] | |
| while path[-1] != start: | |
| path.append(prev[path[-1]]) | |
| path.reverse() | |
| new_cells = [c for c in path if c not in net.cells] | |
| if not new_cells or len(new_cells) > C.FIX_MAX_NEW_CELLS: | |
| return None | |
| name = street_name_for(f"fix:{city.road_version}:bypass") | |
| bridges = sum(1 for c in new_cells if c in WATER_CELLS) | |
| fix = { | |
| "action": "new_road", | |
| "cells": [[c[0], c[1]] for c in new_cells], | |
| "klass": "avenue", | |
| "name": name, | |
| } | |
| desc = f"new avenue {name}, {len(new_cells)} cells" | |
| if bridges: | |
| desc += f" incl. a {bridges}-cell second bridge" | |
| fix["desc"] = desc | |
| fix["predicted_index"] = assign(city, now_s, fix).traffic_index | |
| return fix | |
| def _upgrade_candidates( | |
| city: CityState, assignment: Assignment, now_s: float, limit: int = 2 | |
| ) -> list[dict]: | |
| net = assignment.net | |
| names: list[str] = [] | |
| for cell in assignment.top_cells(C.TRAFFIC_TOP_CELLS): | |
| klass, name = net.cells[cell] | |
| if klass == "street" and assignment.ratio(cell) > 0 and name not in names: | |
| names.append(name) | |
| if len(names) >= limit: | |
| break | |
| out = [] | |
| for name in names: | |
| cells = [ | |
| [c[0], c[1]] | |
| for c, (klass, n) in net.cells.items() | |
| if n == name and klass == "street" | |
| ] | |
| if not cells: | |
| continue | |
| cells.sort(key=lambda c: (c[1], c[0])) | |
| fix = { | |
| "action": "upgrade_avenue", | |
| "cells": cells, | |
| "klass": "avenue", | |
| "name": name, | |
| "desc": f"upgrade {name} to a 6-lane avenue ({len(cells)} cells)", | |
| } | |
| fix["predicted_index"] = assign(city, now_s, fix).traffic_index | |
| out.append(fix) | |
| return out | |
| def candidates(city: CityState, assignment: Assignment, now_s: float) -> list[dict]: | |
| """2-4 pre-validated fixes, best predicted first, ids F1..Fn.""" | |
| found: list[dict] = [] | |
| bypass = _bypass_candidate(city, assignment, now_s) | |
| if bypass: | |
| found.append(bypass) | |
| found.extend(_upgrade_candidates(city, assignment, now_s)) | |
| found.sort(key=lambda f: (f["predicted_index"], f["action"], f["name"])) | |
| for i, fix in enumerate(found[:4]): | |
| fix["id"] = f"F{i + 1}" | |
| return found[:4] | |
| def snapshot(city: CityState, now_s: Optional[float] = None) -> tuple[dict, list[dict]]: | |
| """(stats, candidates) for the fix mechanic — the engine owns the facts.""" | |
| if now_s is None: | |
| now_s = time.time() | |
| assignment = assign(city, now_s) | |
| st = stats(city, assignment) | |
| cands = candidates(city, assignment, now_s) if assignment.max_ratio > 0 else [] | |
| return st, cands | |