Doppelcity
Digital twin cities modeling with multiple potential product surfaces.
Doppelcity
A city model built from open data with its evidence attached, and a browser explorer that opens whole cities from files alone. In development; nothing is deployed.
Doppelcity models a city as ten layers, from buildings and transit networks to venues, parking and events, under one schema for every city and bindings specific to each. Every value carries its source, licence, as-of time, method and confidence, so the model can say what it knows, how well and since when; an absence is named, never filled. The first four cities are Bangkok, Kuala Lumpur, Sao Paulo and Jakarta, chosen from nine global-south cities surveyed for data reachability, licensing and cost. Each city is published as one immutable composition of independently versioned dataset snapshots and derived products, reproduced byte for byte, and opens in a WebGPU explorer for search, inspection, comparison and saved views over the whole city: from 397,554 OpenStreetMap structures in Kuala Lumpur to 2,177,383 in Sao Paulo. Housing, venue discovery, solar analysis and transit are planned as features of that one explorer, each released when its data is supported.
Architecture
The code is split across four repositories by ownership and release cadence. Build and pack link the modelβs Rust crates by sibling path; everything else crosses as a producer record that lists every published file by length and SHA-256, which the consumer stages and verifies. The repositories are private. Design: overview Β· system architecture Β· decisions Β· conventions Β· ADRs for model, pack and explore.
| Layer | Responsibility |
|---|---|
Contracts (model) | What a composition, product, evidence envelope, saved view and query are; the reader ports and their one error taxonomy; the reference reader with its timetable, fare and scene codecs; JSON Schemas and generated TypeScript bindings. A leaf: it depends on nothing above it |
City build (build) | One adapter per source class, per-city bindings, canonical Parquet tables with licence partitions, product producers keyed by their exact inputs, composition assembly, verification and atomic promotion |
Data plane (pack) | Rust compiled to WebAssembly: the asset runtime that fetches, verifies and caches a compositionβs bytes, the family hosts over the reference reader, one program split after linking into an opening part and one part per optional family, and the TypeScript Worker loader that delivers it |
Explorer (explore) | The TypeScript app (three.js on WebGPU, Preact): 3D scene, search, inspection, comparison, transit and saved views; its serving host; the benchmark harness and evidence tools |
ββ SOURCES ββββββββββββββββββββββββββββββββββββββββββββββββββ
β GTFS feeds Β· OpenStreetMap extracts Β· footprint and β
β height datasets Β· rasters β
βββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββββββββββ
β one adapter per source class Β· per-city bindings
βΌ
ββ BUILD β Rust batch driver ββββββββββββββββββββββββββββββββ
β canonical rows (Parquet) Β· envelope Β· licence partitions β
β dataset snapshots β products β city composition β
β verify Β· promote atomically Β· publish producer record β
βββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββββββββββ
β immutable files + manifest Β· served as static files
βΌ
ββ BROWSER ββββββββββββββββββββββββββββββββββββββββββββββββββ
β Worker: pack's wasm program, opening part first and β
β family parts on intent, over model's reader β
β page: explore Β· WebGPU scene Β· search Β· inspector Β· β
β comparison Β· saved views β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
model (contracts, reader, codecs) is linked by build and pack
and reaches explore as generated types and schemas
Why this shape.
- Evidence travels with the value. Every canonical row carries an envelope: source, licence and its gate verdict, as-of time and validity, method and derivation, confidence and what it rests on, extent and ingestion time. An eight-check conformance suite holds every adapter to it, and a row an adapter drops is counted, not lost.
- Licence is a physical partition. Rows are partitioned by layer, licence class and extent: share-alike data sits in its own partition, an unknown licence is quarantined and never published (Jakartaβs 37,165 transit rows among them), and incumbent map and traffic output, whose terms bar derived databases, has no partition at all. Every dependency has a licence row read from its LICENSE file, and each repositoryβs gate fails on a locked package without one.
- A city is an immutable composition. Datasets and products are versioned independently and pinned by content, so a network-only update reuses every structure product byte for byte, a saved view reopens its exact composition, and publication is atomic with rollback by pointer.
- Static first, loaded on intent. A city opens in the browser from files alone, read by Rust compiled to WebAssembly in a Worker. The opening loads only what the first view needs; each optional family (query, inspection, transit, reopen) loads its code and data when the user asks for it, and a part that fails to load leaves exploration usable.
- Milestones close on evidence. Every gate runs in Docker on pinned toolchains, in three tiers: self-contained (what CI runs), against the siblingsβ verified records, and against the real city stores. Performance is judged against budgets frozen before the run, on a benchmark with a pinned Chrome, and each miss is recorded by name.
Tech stack
- Build: Rust (2024 edition) Β· Apache Arrow + Parquet Β· osmpbf Β· tiff Β· i_overlay Β· ureq Β· Planetiler + PMTiles (basemaps, in an offline Docker image) Β· Python (PyO3 data path, build tooling)
- Contracts: Rust Β· serde Β· ts-rs (TypeScript bindings) Β· JSON Schema
- Data plane: Rust β WebAssembly (wasm-bindgen, web-sys) Β· one program split after linking Β· TypeScript Worker loader and mediator
- Explorer: TypeScript Β· three.js (WebGPU renderer, no WebGL fallback) Β· Preact Β· Vite Β· Node serving host
- Live layer (skeleton): TypeScript on Cloudflare Workers Β· Durable Objects
- Quality: Docker-only gates Β· GitHub Actions Β· clippy with
unwrap,expectandpanicdenied andunsafeforbidden Β· per-repository licence coverage Β· Chrome for Testing benchmark in Docker with Python judges
Roadmap
The development plan has the full ladder and its dependencies; each milestone has its own build plan.
| Milestone | Capability | Status |
|---|---|---|
| M0 - Scaffold and slice | Bangkokβs network and fares end to end: the Rust workspace, the adapter conformance suite and CI; two builds byte-identical | β Done |
| M1 - The spine | Network and fares for all four cities, with multi-source merging and every quarantined or unreachable source recorded in its cityβs manifest | β Done |
| M2 - Built environment | Structures and a vector basemap per city; two licence partitions in one layer; per-property source and method on mixed-source rows | β Done |
| M3 - Shared 3D explorer | Four whole cities open in Chrome from files alone: three.js on WebGPU over a Rust wasm data plane, with search, inspection with evidence, comparison, saved views and local-language labels | β Done |
| M4 - Composition | One immutable composition per city of independently versioned datasets and products; atomic publication; native and browser reader parity; crate boundaries enforced by test | β Done |
| M5 - Repository split | The data plane split into an opening part and per-family parts loaded on intent; protocol types generated from Rust; model, pack and explore extracted, each passing its own gate | β Done |
| M6 - Four-city visual preview | Source-backed building parts, inferred and authored massing, rail shown as elevated, surface or underground only where evidenced, hover, per-city palettes; six load-time and memory misses accepted for the internal preview | β Done |
| M7 - Place discovery and MVP | Places from OpenStreetMap, Overture and Foursquare kept as separate data types; category and area search; the memory investigation M6 left; the four-city MVP | Next |
| M8 - Timetable routing | Journeys answered in the browser from the compact timetable, with missing services explicit | Planned |
| M9 - Native client | The four cities on device through the shared Rust readers (UniFFI, Swift and Kotlin) | Planned |
| M10-M17 - Breadth | Live vehicle positions (Kuala Lumpur), a first road-state forecast (Bangkok), demand, labels, parking, events, procedural street level, a sensing client | Planned |
| M18 - The declaration | A per-layer, per-metric quality record for each city against the modelβs six criteria | Planned |
Keywords
- Languages:
RustΒ·TypeScriptΒ·Python - Architecture:
Ports and AdaptersΒ·Immutable CompositionsΒ·Content AddressingΒ·Producer RecordsΒ·Multi-RepositoryΒ·Generated BindingsΒ·Static-First - Data:
GeospatialΒ·GTFSΒ·OpenStreetMapΒ·Arrow / ParquetΒ·PMTilesΒ·Provenance EnvelopeΒ·Licence PartitioningΒ·Reproducible Builds - Browser:
WebGPUΒ·three.jsΒ·WebAssemblyΒ·Web WorkersΒ·Load on Intent - Quality:
Hermetic Docker GatesΒ·Conformance SuitesΒ·Licence CoverageΒ·Frozen-Budget BenchmarksΒ·Evidence-Gated Milestones