GPU-accelerated null geodesic ray tracing through Kerr–Newman spacetime — a CUDA simulator that renders the visual appearance of rotating, electrically charged black holes, validated against Event Horizon Telescope measurements of M87*.
Nulltracer is a from-scratch general-relativistic ray tracer. Each pixel is a CUDA thread integrating a null geodesic backward through curved spacetime in the Kerr–Newman metric (Boyer–Lindquist coordinates) at full float64 precision. The output is what an external observer would actually see — black hole shadow, photon rings, gravitationally lensed background, Doppler-boosted accretion disk, and frame-dragging asymmetry — for any combination of spin (a), charge (Q), and observer inclination (θ).
It's split into a CUDA + FastAPI render server and a thin browser client; the server does all the physics, the client just shows pictures and turns sliders.
| Schwarzschild (a = 0) | Near-extremal Kerr (a = 0.99) | Kerr–Newman charge sweep |
|---|---|---|
![]() |
![]() |
![]() |
Most public black-hole "ray tracers" cut corners — Schwarzschild only, low-order integration, fudged shadows, no quantitative validation. Nulltracer was built to do the full Kerr–Newman case end-to-end on the GPU and to be checkable against real observational constraints. The hero deliverable is a Jupyter notebook (nulltracer.ipynb) that compares simulated M87* shadow diameters against the Event Horizon Telescope Paper VI measurement of 42 ± 3 μas.
Physics engines are easy to make plausible and hard to make right. The repository ships a validation suite — eht_validation.py, compare.py — that benchmarks rendered output against published constraints:
- Shadow diameter at M87* parameters (M = 6.5 × 10⁹ M☉, D = 16.8 Mpc, i ≈ 17°) compared to EHT's 42 ± 3 μas band
- Bardeen analytic shadow boundary (closed-form Kerr critical curve) as ground truth for the spin sweep
- ISCO radii (Kerr–Newman closed form) cross-checked against numerical orbit-stability tests
- M-to-μas unit conversion following the EHT Paper VI conventions
The Schwarzschild limit gives 10.39 M ≈ 39.7 μas at M87* distance — within the EHT band. Spin-dependent results and the full validation report are in nulltracer.ipynb.
Current status. The geodesic engine, Bardeen formulas, and unit conversion are internally consistent; shadow-extraction now uses background-agnostic
classify_shadowfor reliable validation across all background modes. Spin-sweep results are ready for publication.
Spacetime. Kerr and Kerr–Newman metrics in Boyer–Lindquist coordinates with a μ = cos(θ) substitution for robust pole handling.
Integrators. RK4, Yoshida 4th/6th/8th-order symplectic, and adaptive Runge–Kutta–Dormand–Prince (RKDP8). Symplectic methods preserve phase-space structure for long-time stability; RKDP8 wins on accuracy-per-step for hard rays near the horizon.
Kernels. CuPy RawKernel C++ launched one-thread-per-pixel. All geodesic state evolves in float64. Background sampling uses equal-area sphere tiling to eliminate polar pinching; cube-map projection is supported for star fields.
Pipeline. FastAPI accepts parameters → CuPy kernel renders the frame → LRU cache keyed on parameter hash returns it as JPEG/WebP. Single-worker asyncio.Lock serializes GPU access.
Client. A vanilla-JS browser front-end probes /health on the same origin and shows the rendered frame; settings panel exposes a, Q, θ, integrator choice, step count, and quality preset.
# CUDA 12.x + NVIDIA Container Toolkit required
git clone https://github.com/ethank5149/nulltracer && cd nulltracer
docker compose up -d
# open index.html in your browserFor full deployment options (Caddy reverse proxy, Unraid template, local dev), see DEPLOYMENT.md.
nulltracer/
├── nulltracer.ipynb # Hero deliverable: EHT validation notebook
├── index.html, js/, styles.css # Thin browser client
├── server/
│ ├── app.py # FastAPI: /render, /health
│ ├── renderer.py # CuPy RawKernel orchestration
│ ├── isco.py # Kerr–Newman ISCO (closed form)
│ ├── eht_validation.py # EHT Paper VI comparison suite
│ ├── compare.py # Bardeen analytic shadow comparator
│ └── kernels/
│ ├── geodesic_base.cu # Metric + Hamiltonian + RHS
│ ├── disk.cu # Doppler-boosted accretion disk
│ ├── backgrounds.cu # Stars / checker / colormap
│ └── integrators/ # rk4, rkdp8, yoshida4/6, kahan_li8
├── docker-compose.yml, Dockerfile
└── ARCHITECTURE.md # Detailed technical write-up
For the full architecture write-up, see ARCHITECTURE.md.
| Version | Tag | Key changes |
|---|---|---|
| v0.9 | v0.9 |
Current. Polished Kerr–Newman release |
| v0.8 | v0.8 |
Kerr–Newman extension (electric charge parameter) |
| v0.7 | v0.7 |
Adaptive stepping refinements |
| v0.6 | v0.6 |
μ = cos(θ) coordinate substitution for pole handling |
| v0.5 | v0.5 |
Smooth regularization and cube-map projection |
| v0.4 | v0.4 |
Numerical stability improvements |
| v0.3 | v0.3 |
Equal-area sphere tiling (fixes polar pinching) |
| v0.2 | v0.2 |
UX overhaul: legend, settings panel, multiple backgrounds |
| v0.1 | v0.1 |
Refactored to separated first-order equations (~40% faster) |
| v0.0.1 | v0.0.1 |
Initial Kerr black hole with Hamiltonian RK4 integration |
git checkout v0.X to view any release.
Server. Python 3.8+, NVIDIA GPU with CUDA 12.x (RTX 3090 / A100 / H100 tested), cupy-cuda12x. Docker + NVIDIA Container Toolkit recommended.
Client. Any modern browser (Chrome 56+, Firefox 51+, Safari 15+, Edge 79+). No local GPU required.
If this code or the validation methodology is useful in your work, please cite it:
@software{knox_nulltracer_2026,
author = {Knox, Ethan},
title = {Nulltracer: GPU-accelerated null geodesic ray tracing in Kerr–Newman spacetime},
year = {2026},
url = {https://github.com/ethank5149/nulltracer},
version = {0.9}
}Released under the MIT License.
Ethan Knox — B.S. Physics, B.S. Mathematics, Purdue University Champaign, IL · github.com/ethank5149



