Spaces:
Running on Zero
Running on Zero
File size: 7,223 Bytes
38ec721 | 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 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 | """
Route-file generation, generalized from BanTRel.py Cell 4.
The original notebook hardcoded one schedule (the 16,400 veh/day Bangalore
split) and one mix (60/30/10 car/bike/auto). This module keeps that as the
"preset" option, and adds a "custom" path that scales the SAME 7-period
day-shape (so the model still sees a realistic time-of-day pattern it was
trained on) by a user-chosen total volume, with a user-chosen vehicle mix.
Scaling the known shape (rather than inventing an arbitrary schedule) keeps
state normalisation (MAX_QUEUE=80, MAX_WAIT=600 in env.py) meaningful —
wildly different shapes would push the model into state regions it never
saw during training.
"""
import os
from typing import Literal
SECONDS_PER_HOUR = 3600
# Each entry: (begin_s, end_s, total_vehicles, label) — verbatim from BanTRel.py
DEFAULT_SCHEDULE = [
(0, 8 * SECONDS_PER_HOUR, 550, "Night (00:00-08:00)"),
(8 * SECONDS_PER_HOUR, 11 * SECONDS_PER_HOUR, 4000, "Morning rush (08:00-11:00)"),
(11 * SECONDS_PER_HOUR, 13 * SECONDS_PER_HOUR, 2850, "Mid-morning (11:00-13:00)"),
(13 * SECONDS_PER_HOUR, 16 * SECONDS_PER_HOUR, 2100, "Afternoon (13:00-16:00)"),
(16 * SECONDS_PER_HOUR, 20 * SECONDS_PER_HOUR, 4400, "Evening rush (16:00-20:00)"),
(20 * SECONDS_PER_HOUR, 23 * SECONDS_PER_HOUR, 2500, "Evening (20:00-23:00)"),
(23 * SECONDS_PER_HOUR, 86400, 550, "Night (23:00-24:00)"),
]
DEFAULT_TOTAL = sum(t for _, _, t, _ in DEFAULT_SCHEDULE) # 16,400
DEFAULT_MIX = {"car": 0.60, "bike": 0.30, "auto": 0.10}
PRESET_PERIOD_IDS = {
"night": DEFAULT_SCHEDULE[0],
"morning_rush": DEFAULT_SCHEDULE[1],
"mid_morning": DEFAULT_SCHEDULE[2],
"afternoon": DEFAULT_SCHEDULE[3],
"evening_rush": DEFAULT_SCHEDULE[4],
"evening": DEFAULT_SCHEDULE[5],
}
ALL_ROUTES = [
("route_ns", "N2C C2S"), ("route_nw", "N2C C2W"), ("route_ne", "N2C C2E"),
("route_sn", "S2C C2N"), ("route_sw", "S2C C2W"), ("route_se", "S2C C2E"),
("route_en", "E2C C2N"), ("route_ew", "E2C C2W"), ("route_es", "E2C C2S"),
("route_wn", "W2C C2N"), ("route_we", "W2C C2E"), ("route_ws", "W2C C2S"),
]
VEHICLE_TYPES_XML = (
' <vType id="car" accel="2.6" decel="4.5" sigma="0.5" '
'length="5" minGap="2.5" maxSpeed="13.89"/>\n'
' <vType id="bike" accel="3.0" decel="5.0" sigma="0.6" '
'length="2" minGap="1.5" maxSpeed="11.11" vClass="motorcycle"/>\n'
' <vType id="auto" accel="2.0" decel="4.0" sigma="0.5" '
'length="4" minGap="2.0" maxSpeed="11.11"/>'
)
def build_schedule(
mode: Literal["preset", "custom"],
total_vehicles: int | None = None,
period: str | None = None,
) -> list[tuple[int, int, int, str]]:
"""
mode="preset", period=None -> full 24h canonical Bangalore day
mode="preset", period="morning_rush" -> just that one period, run in isolation
mode="custom", total_vehicles=N -> full 24h shape, each period scaled so
the day sums to N vehicles instead of 16,400
"""
if mode == "preset":
if period is None:
return DEFAULT_SCHEDULE
if period not in PRESET_PERIOD_IDS:
raise ValueError(f"Unknown period '{period}'. Options: {list(PRESET_PERIOD_IDS)}")
begin, end, total, label = PRESET_PERIOD_IDS[period]
# Re-anchor to start at t=0 so the episode is just this period's length.
return [(0, end - begin, total, label)]
if mode == "custom":
if not total_vehicles or total_vehicles <= 0:
raise ValueError("custom mode requires total_vehicles > 0")
scale = total_vehicles / DEFAULT_TOTAL
return [
(begin, end, max(1, round(total * scale)), label)
for begin, end, total, label in DEFAULT_SCHEDULE
]
raise ValueError(f"Unknown mode '{mode}'")
def make_route_file(
filepath: str,
schedule: list[tuple[int, int, int, str]],
mix: dict[str, float] | None = None,
) -> int:
"""Writes the SUMO route file. Returns the number of <flow> entries written."""
mix = mix or DEFAULT_MIX
total_pct = sum(mix.values())
if abs(total_pct - 1.0) > 0.01:
raise ValueError(f"Vehicle mix must sum to 1.0 (got {total_pct:.3f})")
route_tags = "\n".join(
f' <route id="{rid}" edges="{edges}"/>' for rid, edges in ALL_ROUTES
)
flow_tags = []
flow_id = 0
for begin, end, total_veh, _label in schedule:
duration_hr = max((end - begin) / SECONDS_PER_HOUR, 1e-6)
total_per_hr = total_veh / duration_hr
per_route_hr = total_per_hr / len(ALL_ROUTES)
for route_id, _ in ALL_ROUTES:
tag = route_id.removeprefix("route_")
for vtype, proportion in mix.items():
rate = per_route_hr * proportion
if rate <= 0:
continue
flow_tags.append(
f' <flow id="f{flow_id:04d}_{vtype}_{tag}" '
f'route="{route_id}" type="{vtype}" '
f'begin="{begin}" end="{end}" '
f'vehsPerHour="{rate:.1f}" '
f'departSpeed="max" departPos="base" departLane="best"/>'
)
flow_id += 1
xml = (
'<?xml version="1.0" encoding="UTF-8"?>\n<routes>\n'
+ VEHICLE_TYPES_XML + "\n\n"
+ route_tags + "\n\n"
+ "\n".join(flow_tags) + "\n</routes>"
)
with open(filepath, "w") as f:
f.write(xml)
return flow_id
def make_sumocfg(route_filename: str, net_filename: str, cfg_path: str,
duration: int, step_length: int = 10) -> None:
xml = f"""<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<input>
<net-file value="{net_filename}"/>
<route-files value="{route_filename}"/>
</input>
<time>
<begin value="0"/>
<end value="{duration}"/>
<step-length value="{step_length}"/>
</time>
</configuration>"""
with open(cfg_path, "w") as f:
f.write(xml)
def prepare_run_config(
run_dir: str,
net_path: str,
mode: Literal["preset", "custom"],
total_vehicles: int | None = None,
period: str | None = None,
mix: dict[str, float] | None = None,
step_length: int = 10,
) -> str:
"""
Builds a fresh .rou.xml + .sumocfg for one simulation run inside run_dir
(a per-job temp directory, so concurrent requests never collide on files).
Returns the .sumocfg path.
"""
os.makedirs(run_dir, exist_ok=True)
schedule = build_schedule(mode, total_vehicles, period)
duration = schedule[-1][1] # end time of the last period
route_path = os.path.join(run_dir, "routes.rou.xml")
cfg_path = os.path.join(run_dir, "sim.sumocfg")
n_flows = make_route_file(route_path, schedule, mix)
if n_flows == 0:
raise ValueError("Generated route file has zero flows — check demand/mix inputs.")
# sumocfg references the shared, pre-built network by relative path.
net_filename = os.path.relpath(net_path, run_dir)
make_sumocfg("routes.rou.xml", net_filename, cfg_path, duration, step_length)
return cfg_path
|