nemocity / engine /traffic.py
AndresCarreon's picture
NEMOCITY v0 — mock backend, gradio 6.16.0 (pre-SSR)
d72231c verified
Raw
History Blame Contribute Delete
13.9 kB
"""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
@dataclass
class RoadNet:
"""Road cells + class/name, optionally with a candidate fix applied."""
cells: dict[Cell, tuple[str, str]] # cell -> (klass, name)
@classmethod
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
@dataclass
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)
@property
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