Why I built twilight in Rust
Every prayer app picked a number in 1986. The atmosphere didn't.
Why a Monte Carlo radiative-transfer engine for prayer times needs to be #![no_std] Rust with GPU offload — and why Python and JavaScript would have lost an order of magnitude before the first photon.
§ The problem isn’t religion. The problem is computation.
Every prayer app on the App Store hardcodes a solar depression angle. MWL says 18°. Egypt says 15°. Umm al-Qura says 19.5°. They disagree because “when does twilight end?” depends on the atmosphere — and nobody wanted to model the atmosphere. So in 1986 someone picked a number, and three decades later we’re still using it.
The number is wrong every day in different amounts. Cloud cover can shift the answer by 8 minutes. Urban aerosol haze can shift it by another 7. Latitude, altitude, terrain shadowing, ozone column, water vapor, NO₂ pollution — every one of these affects the photon path that determines twilight, and every one of them gets ignored when you pick a fixed angle.
I wanted a calculator that actually models the atmosphere. The right tool is Monte Carlo radiative transfer. The right language is Rust.
§ Why not Python
Python is the default for atmospheric physics — MetPy, pyrt, clmm, the whole scientific stack. For prototyping, it’s fine. For production?
A single-scatter Monte Carlo simulation through a 50-shell spherical atmosphere with 5-species molecular gas absorption and aerosol scattering needs to launch on the order of 10⁶ photon packets per location per query to converge to a usable signal-to-noise ratio. Each packet requires ~50 layer crossings, each crossing requires several optical-depth integrals, each integral hits the absorption tables.
In Python, that’s seconds per query. In Rust with GPU offload, it’s 30 ms. Two orders of magnitude — not because Python is “slow,” but because Python’s interpreter overhead per inner-loop iteration dwarfs the actual physics.
For a calculator that has to answer in real time on a phone, the choice writes itself.
§ Why not WebAssembly-from-other-languages
Could compile from C++ to WASM and ship in a browser. Could write Go and compile down. The reasons not to:
- C++ ABI fragility + GPU bindings = a compilation matrix from hell
- Go’s GC introduces tail latency that ruins consistent benchmark numbers
- Rust + wgpu gives me one shader stack that targets Metal, Vulkan, CUDA, and WebGPU from one source
#![no_std]core means the engine itself is embeddable anywhere — bare-metal, microcontroller, browser, native
The host crate handles everything OS-specific (file I/O, network, weather pulls). The core is pure math.
§ Architecture
┌──────────────────────────────────────────┐
│ twilight-host (std) │
│ ├── Open-Meteo client (live weather) │
│ ├── JPL DE440 ephemeris loader │
│ ├── Copernicus GLO-30 DEM cache │
│ ├── HITRAN absorption table loader │
│ └── CLI / WASM bindings │
└────────────────┬─────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ twilight-core (#![no_std]) │
│ ├── 50-shell spherical atmosphere │
│ ├── Rayleigh scatterer │
│ ├── 5-species absorber (O3/O2/H2O/NO2) │
│ ├── OPAC aerosol model │
│ ├── Cloud layer simulator │
│ ├── Snell's-law refractor │
│ └── Stokes-vector polarization │
└────────────────┬─────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ twilight-gpu (wgpu) │
│ Metal · Vulkan · CUDA · WebGPU │
│ Photon-packet kernel · ~30 ms / query │
└──────────────────────────────────────────┘
§ What the numbers prove
The point of doing all this work is that the answer changes. From the README:
Mecca, March equinox, clear sky:
Fajr: 05:23 depression 14.97 (Egypt 15: 05:23 — spot on)
Isha: 19:32 depression 14.83 (ISNA 18: 19:45 — off by 13 min)
Mecca, March equinox, urban aerosol (AOD 0.30):
Fajr: 05:32 depression 13.03 (+8 min vs clear sky)
Isha: 19:25 depression 13.21 (-7 min vs clear sky)
Padborg, Denmark, 2026-03-06, live weather:
Fajr: 04:28 depression 14.05 AOD 0.11, O3 289 DU
Isha: 18:37 depression 13.59
The traditional methods agree with each other in the median case and disagree wildly in the tails. twilight computes the tails correctly because it never simplified them away in the first place.
§ The bigger lesson
Most “pick a number” engineering decisions are downstream of “modeling that thing is too hard.” This was true for prayer times in 1986 — modeling atmospheric scattering on a microcontroller was infeasible. It is no longer true. We just kept using the number.
A lot of software still works this way. Investment-grade portfolio rebalancing thresholds. Battery-state-of-charge estimators in EVs. Drug interaction tables in clinical decision support. Satellite collision-risk windows. Speed-limit defaults in self-driving lane-changes. Any time you see a constant where there should be a model, there’s headroom.
Monte Carlo isn’t free — but it’s not 1986 anymore.