Skip to content

Repository files navigation

Traffic Simulator

A browser-based microscopic traffic simulation. Pick any area of the world and it pulls the real road network from OpenStreetMap, expands it into individual lanes and junction connections, and drives vehicles over it.

No API key needed.

It runs at nitai-traffic-simulator.web.app if you would rather look before cloning.

Run

npm install
npm run dev

Open http://localhost:5173, pan to an area, and press "Load this city".

Once it is running you can change how the traffic lights are run and how much traffic there is, and you can pause and set how fast time passes, anywhere from a tenth of real time to sixteen times it. The roads recolour by how held up traffic on them is, in five bands from free to stopped, so a change reads as a corridor going from red to green rather than as a number moving. The panel carries the same five colours as one bar, which is what share of the traffic is in each of them, and underneath it the figures from before your last change beside the ones since.

Cycle length and the seed are set in the harness rather than in the panel. They are questions worth asking and not questions worth asking with a mouse: one needs an opinion about signal timing before you have watched a junction, and the other spends a minute rebuilding a city to change something you cannot see.

That panel is one window on one seed. It is enough to see what a change does and not enough to prove it; npm run experiment is what settles a difference.

Which way of running the lights is best?

Eighteen strategies ship. Seven are whole-network policies: a fixed timetable, a vehicle-actuated controller and three repairs to it, and two queue-driven ones. Nine are the same actuated controller deployed at only some junctions, chosen either as the busiest, the quietest, or at random, which is how the question "would it help to put the sensors where the traffic is" gets an answer rather than an opinion. The last two are bounds rather than proposals: a random order as a floor and every light green at once as a ceiling.

npm run experiment -- experiments/strategy-lineup.json runs them against each other and writes a ranked table per load level, sharing a rank between any it could not tell apart. See experiments/README.md for what each one does and how to read the result.

Build and test

npm run build
npm run test

The test suite is pure Node. Nothing in it needs a browser, including the vehicle layer: the shader's precision behaviour is asserted with Math.fround, which is exactly what a GPU does to a float, rather than through a real WebGL context. Playwright is a dependency anyway, for looking at what the shader actually draws when a bug is visual rather than numerical. It is not on the test path.

How it works

Each direction of a street becomes its own set of lanes, offset to the driving side of the centreline, so opposing traffic is genuinely apart rather than drawn apart. Intersections are not points: every permitted movement through one is a curved link with real length, so crossing a junction takes time and can be checked for conflicts.

Vehicles follow the Intelligent Driver Model and look ahead across link boundaries, not only along the road they are on, which is what lets queues back up through a junction.

Traffic goes where traffic actually goes. Most journeys start and finish somewhere inside the area you loaded, on the kind of street people live and park on, and a car pulls out partway along its street rather than appearing at the junction. Some traffic arrives from outside, some leaves, and a little crosses without stopping, which is what the motorways carry. Nobody's journey begins in the middle of a motorway, so none do here. That last point sounds obvious and was not always true: trips used to be able to start and end only at the edge of the loaded area, weighted towards the fastest road there, which put the whole fleet on the ring road and left the city empty.

A run is reproducible from its scenario and seed alone. The simulation is pure and headless: it reads no wall clock and touches no browser API, so the same scenario replays identically and the whole model is testable under Node.

Signalised junctions are phased from the real conflict list rather than from opposing bearings, and a turn may run on the same green as the traffic it crosses provided it gives way, which is what a real signal does and what keeps a crossroads to two phases instead of four. Junctions that OpenStreetMap models as several nodes, which is most large ones, are planned as a single junction on one cycle.

The map is MapLibre GL. Static network geometry is handed over as GeoJSON; vehicles are drawn in a custom WebGL layer as instanced rectangles shaded in the fragment shader, a single draw call for the whole fleet. There are no sprites and no texture atlas: at the size traffic is actually viewed a car is a few pixels, so its body, glazing and lights are described as numbers instead.

See ARCHITECTURE.md for the layer breakdown.

Status

The rebuild described in docs/superpowers/specs/2026-07-25-traffic-engine-rebuild-design.md is well advanced. Lanes, junction connections, the driving model, traffic signals and right of way, lane changing, demand modelling and the metrics the HUD reports are all in place.

Signal control is now pluggable rather than a pure function of the clock, and there is a headless experiment harness that compares one control strategy against another and reports the difference with a confidence interval. See docs/superpowers/specs/2026-07-27-signal-strategy-evaluation-design.md and experiments/README.md.

npm run experiment -- experiments/cycle-length.json

The harness has been shown to measure: the baseline against itself gives exactly zero, delay rises with load, and fixed-time delay traces Webster's U in cycle length with the minimum inside the range.

Its first findings are about the model rather than the control. Every network has a capacity, and above it the simulation is not congested but collapsing: vehicles deadlock, and delay figures from that regime describe the seizure rather than the signals. Most of the early runs here were three to ten times above capacity without anyone noticing. Below capacity the model behaves: the Tel Aviv district runs at 24 km/h free-flowing, falling to 9.5 km/h under load, and Webster's U comes out clean and symmetric.

Vehicles that deadlock are now removed and counted rather than left to accumulate, so every report states how much of its result is artefact, and zero is the normal answer below capacity.

The harness then earned its keep. The Tel Aviv district was choking at a tenth of an arrival a second, absurd for 2,749 lanes, and tracing it led to a single 2.8 m lane between two closely spaced junctions. OSM splits a way at every junction node, a quarter of the district's lanes are under 15 m, and requiring a vehicle to fit wholly inside one made it an airlock admitting one vehicle per cycle while a third of all routes queued behind it. Vehicles now span lanes too short to stop on, as real cars do. The clean operating range tripled and throughput under load went from about 950 trips an hour to 2,688.

The strategies the harness was built to compare now exist: actuated, longest queue, max pressure, repairs aimed at what goes wrong past capacity, deployments that signalise only part of the district, and sweeps of the two settings that turn out to decide the whole thing, against a fixed-time baseline with a random floor and an all-green ceiling. Twenty-three of them, over 960 paired runs. What they do to a real district, junction by junction, is written up as a paper in paper/ and readable at paper/main.pdf.

The short version, and the reason the paper exists: adaptive control beats the best fixed plan we ran at every load tested, and by more as the network fills, by 14.9 per cent at the lightest load and 18.5 per cent at the heaviest, in all ten seeds. It only does that if it gives up a green as soon as that green stops discharging. Give the same controller a conventional maximum green and it inverts, finishing 58 per cent worse than the plan it replaced.

What the harness ranks on changed with them. It was delay per offered trip, which is fair in its denominator and still leaks in its numerator: a traveller who gives up at the kerb stops being counted, and a vehicle removed for deadlocking stops being counted, so the two worst things that can happen to somebody are also the two cheapest for the strategy responsible. The headline is now total time spent per offered trip, which counts everybody's time until the hour ends including the people already given up on. Nothing can improve it except getting more people where they were going, sooner. src/sim/metrics.ts carries the proof; it is one line of conservation.

The harness also now records why each green ended, as gap-out, ceiling, or forced off, and that measurement refuted the explanation everybody reaches for first, including this repository until it looked. The story was that past capacity actuation stops actuating, because no approach ever clears and every green runs to its ceiling. It does not. Gap-out keeps firing and still ends 96.8 per cent of greens. The damage is a tail: the 3 per cent of greens a presence test cannot end are a third of all green by time, because presence cannot tell an approach that is busy from one that is blocked.

Read SYSTEM-REPORT.md before quoting any of it. It sets out what the model assumes, how strategies are compared, what has actually been demonstrated, and what is not real.

The paper puts the same runs and the same numbers next to the literature, with the limitations stated at length and every table generated from the run records rather than transcribed. cd paper && make builds it, make check runs the pre-submission checks, and paper/SUBMITTING.md covers getting it onto arXiv.

Still missing: coordination between junctions worth having (a green wave was built, measured worse on undirected demand, and removed), strategic lane changing so that turn lanes can overflow the way they do in reality, rerouting around congestion, a demand model derived from land use rather than road class, pedestrian and public transport modelling, and any validation against measured traffic counts. That last one is the largest gap: the system is internally rigorous and has never been checked against the streets it models.

npm run checks narrows it a little, without closing it. A standing queue discharges at 1,520 vehicles an hour per lane, a free ring peaks at 1,474 at 45 vehicles a kilometre, and those two independent numbers agree to 3 per cent. Delay at an isolated junction tracks Webster plus the incremental term within 10 per cent from a third to nine tenths of saturation. But saturation flow is a fifth below the Highway Capacity Manual's base figure, so every capacity here is conservative, and all of it is measured on synthetic geometry rather than on a real street.

Licence

PolyForm Noncommercial 1.0.0. Read it, run it, fork it, change it, and publish what you build on it, for any noncommercial purpose: research, teaching, study, or a hobby. Selling it, or using it inside something you sell, needs a separate licence; ask.

This is a source-available licence rather than an open source one in the OSI sense, because it restricts the field of use.

About

Browser-based microscopic traffic simulation on real OpenStreetMap road networks, with a harness for comparing traffic signal control strategies.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages