|
| 1 | +--- |
| 2 | +title: 'Miasteczko.jl' |
| 3 | +date: 2026-04-18 |
| 4 | +draft: true |
| 5 | +topics: ['urban transport'] |
| 6 | +tags: ['simulation', 'optimization', 'gravity-model', 'public-transport', 'osm'] |
| 7 | +tech: ['Julia', 'JuMP', 'React', 'TypeScript', 'MapLibre', 'Electron', 'Luxor.jl', 'Cairo'] |
| 8 | +description: 'Bottom-up transport planning for Polish towns — simulate demand first, design supply second. 266 corridors discovered automatically from a gravity model over real OSM data.' |
| 9 | +status: 'in-progress' |
| 10 | +--- |
| 11 | + |
| 12 | +## Goal |
| 13 | + |
| 14 | +Design public transport networks from the ground up: simulate how people actually move, then build routes that serve that demand. The opposite of the usual top-down approach where planners draw lines on a map and hope ridership follows. |
| 15 | + |
| 16 | +The library works at two scales — town-level (MicroSim) and regional (MacroSim) — both built on real [OpenStreetMap][osm] data for Polish settlements. |
| 17 | + |
| 18 | +## Approach |
| 19 | + |
| 20 | +### MicroSim — how does this town move? |
| 21 | + |
| 22 | +Per-person, per-minute simulation of a single town: |
| 23 | + |
| 24 | +1. **OSM extraction** — buildings, roads, points of interest, rail stations from a Poland [PBF file][pbf] (a compact binary format for OpenStreetMap data) |
| 25 | +2. **Population synthesis** — households placed in real residential buildings, workers assigned to workplaces, students to schools ([GUS][gus] census demographics) |
| 26 | +3. **Activity simulation** — agents follow daily schedules driven by a needs system (hunger, energy, social). 60-second tick resolution over multiple days. |
| 27 | +4. **Demand aggregation** — individual trips become an [origin-destination (OD) matrix][od-matrix] by zone and time-of-day |
| 28 | +5. **Bus network generation** — greedy stop placement at intersections, demand-driven route building, overlap detection, express routes for long corridors |
| 29 | +6. **Timetable generation** — per-route demand-proportional departures, bidirectional schedules, timed transfer hubs ([Taktfahrplan][taktfahrplan]-style pulse — a Swiss method where all lines meet at hubs simultaneously) |
| 30 | +7. **Feedback loop** — simulate with transit → measure ridership → adjust timetable → repeat until stable |
| 31 | + |
| 32 | +Mode choice is probabilistic ([logit model][logit]) across walk, bike, e-bike, bus, and car — using actual road network distances, weather conditions, and trip chaining (if you bused to work, you bus back). |
| 33 | + |
| 34 | +### MacroSim — how does this region connect? |
| 35 | + |
| 36 | +Regional-scale simulation where vehicles (trains) are the agents, not people: |
| 37 | + |
| 38 | +1. **Settlement graph** — 2,987 settlements extracted from Poland PBF, three-tier hierarchy (cities >50k, towns 5–50k, villages <5k) |
| 39 | +2. **Corridor discovery** — 109 backbone + 157 feeder = 266 corridors found automatically from rail connectivity (see examples below) |
| 40 | +3. **[Gravity demand model][gravity-model]** — OD matrix with population-based attraction, distance decay, and employment scoring from OSM amenity density |
| 41 | +4. **10-pulse system** — composable demand generators (morning commute, school, university weekly, shopping, nightlife, healthcare, leisure, family visits). Each pulse fires on specific day types — the simulation distinguishes 4: regular weekday (Mon–Thu), Friday (university exodus + nightlife), Saturday (shopping peak), and Sunday (leisure + family). Averaging into just "weekday" and "weekend" would hide real phenomena like the Friday afternoon wave of students heading home. |
| 42 | +5. **Vehicle simulation** — deterministic train traversal with capacity constraints and automatic relief train insertion |
| 43 | +6. **Junction transfers** — detect stations on 2+ corridors, route transfer demand through them with 5-minute penalty, align timetables at junctions |
| 44 | + |
| 45 | +Calibrated against Poland PBF to produce 3.8M daily passengers — intentionally optimistic for a green transport scenario. |
| 46 | + |
| 47 | +### Corridor discovery |
| 48 | + |
| 49 | +The system discovers rail corridors automatically from OpenStreetMap track connectivity — no manual line definitions, no [PKP][pkp] data. It finds city-to-city paths through the station graph, then generates feeder corridors from uncovered towns to their nearest city. |
| 50 | + |
| 51 | +Top 5 discovered corridors by length: |
| 52 | + |
| 53 | +| Corridor | Distance | Stations | |
| 54 | +|----------|----------|----------| |
| 55 | +| Szczecin — Gdańsk | 356 km | 52 | |
| 56 | +| Łódź — Gdańsk | 352 km | 45 | |
| 57 | +| Elbląg — Łódź | 334 km | 43 | |
| 58 | +| Gliwice — Warszawa | 333 km | 51 | |
| 59 | +| Ełk — Gdańsk | 327 km | 52 | |
| 60 | + |
| 61 | +These roughly match real PKP lines — without being told they exist. The gravity model then assigns demand to each corridor based on population and distance. |
| 62 | + |
| 63 | +<small>Validated on Poland PBF, commit `4fc5d0f` (2026-04-18).</small> |
| 64 | + |
| 65 | +### Economics module |
| 66 | + |
| 67 | +Full financial modeling per route: distance-based fares (power-law), fuel/energy costs (diesel and electric), CO₂ accounting with car-counterfactual savings, and four subsidy types (deficiency grant, per-passenger-km, CO₂ bonus, accessibility bonus for underserved areas). |
| 68 | + |
| 69 | +### UI — Desktop preview app |
| 70 | + |
| 71 | +The presentation layer went through several iterations. First attempt was [CImGui][cimgui] (immediate-mode GUI with GLFW/ModernGL) — functional but limited for map-heavy workflows. A quick [Bonito.jl][bonito] proof-of-concept validated that Electron + web rendering was the right direction. The production app landed on React + TypeScript + [MapLibre][maplibre] + [Oxygen.jl][oxygen] + Electron. |
| 72 | + |
| 73 | +The app is a submodule (`supapp/lib`) primarily used for presenting simulation results: [MapLibre][maplibre] maps, floating detail windows, sortable data tables, route/stop/vehicle detail panels, time-distance diagrams, passenger flow charts, and a system-wide economics dashboard. The Julia backend serves a JSON API via Oxygen.jl, so generated results can also be integrated with other frontends through the web server. |
| 74 | + |
| 75 | + |
| 76 | + |
| 77 | +### UI — Isometric city renderer (in progress) |
| 78 | + |
| 79 | +IsoRender module — a proof-of-concept to render towns in a [Simutrans][simutrans]/[OpenTTD][openttd]-style isometric view using real OSM geometry. Unlike those games, it doesn't snap to a tile grid — buildings and roads keep their actual footprints, projected into 2:1 [dimetric perspective][dimetric]. Built with [Luxor.jl][luxor] (Cairo), using building extrusion for 3D appearance and [painter's algorithm][painters-algo] depth sorting. Outputs high-resolution PNG images. |
| 80 | + |
| 81 | +## Outcome |
| 82 | + |
| 83 | +- **17 modules**, 3,900+ tests, all passing |
| 84 | +- Validated on three towns: Szamocin (17k), Chodzież (64k, 3 rail stations), Margonin (22k) |
| 85 | +- MacroSim pipeline runs on full Poland PBF — 266 corridors, 92 semi-fast + 23 express service patterns |
| 86 | +- MicroSim feedback converges in 2–3 cycles to stable timetables |
| 87 | +- Three train models with physics-based travel times (EN57, Elf 2, SA134) |
| 88 | + |
| 89 | +## What I learned |
| 90 | + |
| 91 | +- Bottom-up transport planning produces surprisingly reasonable networks — the gravity model discovers corridors that roughly match real PKP lines, without being told they exist |
| 92 | +- Mode choice modeling is extremely sensitive to small parameter changes — [distance decay][distance-decay] β controls how fast demand drops with distance (higher β = people less willing to travel far). A 0.1 shift can flip 20% of trips between bus and car |
| 93 | +- The pulse system for demand generation is more expressive than expected — composing 10 independent travel patterns across 4 day types captures real phenomena that a simple weekday/weekend split would miss |
| 94 | +- Julia's type system and multiple dispatch made the module architecture natural — each module produces typed structs, the next module consumes them, no serialization layer needed |
| 95 | + |
| 96 | +[osm]: https://en.wikipedia.org/wiki/OpenStreetMap |
| 97 | +[pbf]: https://wiki.openstreetmap.org/wiki/PBF_Format |
| 98 | +[gus]: https://en.wikipedia.org/wiki/Statistics_Poland |
| 99 | +[od-matrix]: https://en.wikipedia.org/wiki/Origin%E2%80%93destination_matrix |
| 100 | +[taktfahrplan]: https://en.wikipedia.org/wiki/Taktfahrplan |
| 101 | +[logit]: https://en.wikipedia.org/wiki/Discrete_choice#Logit |
| 102 | +[gravity-model]: https://en.wikipedia.org/wiki/Gravity_model_of_trade |
| 103 | +[pkp]: https://en.wikipedia.org/wiki/Polish_State_Railways |
| 104 | +[maplibre]: https://en.wikipedia.org/wiki/MapLibre |
| 105 | +[simutrans]: https://en.wikipedia.org/wiki/Simutrans |
| 106 | +[painters-algo]: https://en.wikipedia.org/wiki/Painter%27s_algorithm |
| 107 | +[distance-decay]: https://en.wikipedia.org/wiki/Distance_decay |
| 108 | +[cimgui]: https://github.com/ocornut/imgui |
| 109 | +[bonito]: https://github.com/MakieOrg/Bonito.jl |
| 110 | +[oxygen]: https://github.com/ndortega/Oxygen.jl |
| 111 | +[openttd]: https://en.wikipedia.org/wiki/OpenTTD |
| 112 | +[dimetric]: https://en.wikipedia.org/wiki/Axonometric_projection#Three_types |
| 113 | +[luxor]: https://github.com/JuliaGraphics/Luxor.jl |
0 commit comments