The field that learned traffic by solving it, not by memorizing it.
A continuity-plus-potential PDE on a 256 × 64 grid that evolves a scalar risk surface across the 40 m in front of the ego. No learned weights. No hidden heuristics. Every cell is the arithmetic result of the equation on the cells next to it.
Three equations. That's it.
This is the whole model. The implementation is a direct finite-difference port in ~300 lines of Rust. Read it, step through it, prove it, there's nothing hidden.
∂ₜρ + ∇·(ρu) = 0
Traffic density ρ is conserved as it flows through the grid, mass in equals mass out, same property that governs fluids and crowds.
u = −κ∇φ − α∇ρ
Flow descends the potential gradient (goal-seeking) while spreading away from high density (repulsion). Two constants, κ and α, set the trade-off.
∂ₜφ = cφ²∇²φ + Jego
The scalar potential diffuses across the grid and is sourced by the ego's intended path. The CFL condition sets dt ≤ dx² / (4·cφ²).
Why a PDE beside a neural stack?
This isn't an ideology. A deterministic PDE layer is useful in four places neural stacks are hardest to review.
Determinism, bit for bit
Same inputs produce the same observer outputs on any supported CPU. Replay an incident log and inspect the same hazard sequence every time.
Auditable without a PhD
A reviewer can step through the finite-difference update cell by cell. The risk surface at (i, j) is derived from explicit neighboring values.
Bounded failure modes
The CFL condition gives the field update an explicit stability envelope. Learned components can still be used around it, but the observer layer stays bounded.
Runs on commodity CPU
The field solve is small enough for commodity CPU evaluation, so teams can start replaying scenes without a new GPU-heavy training program.
Trajectories in, hazards out
Predicted tracks from the motion-prediction stage get converted to weighted density seeds and deposited into the ρ field. The solver advances for one 20 Hz tick, then a threshold check surfaces any cell whose density crosses the hazard boundary.
- ›Confidence-weighted seeds: lower confidence → softer density injection.
- ›Weight-bucketed for deterministic summation independent of input order.
- ›Neumann boundary (zero-gradient) keeps the field stable at the grid edges.
// Finite-difference step (excerpt)
// Continuity: ∂ₜρ + ∇·(ρu) = 0
let flux_x = upwind_flux(&rho, &u, dx);
let flux_y = upwind_flux(&rho, &v, dy);
rho -= dt * (flux_x + flux_y);
// Velocity from gradients
let grad_phi = gradient(&phi, dx, dy);
let grad_rho = gradient(&rho, dx, dy);
u = -kappa * grad_phi.0 - alpha * grad_rho.0;
v = -kappa * grad_phi.1 - alpha * grad_rho.1;
// Potential diffusion + ego source
phi += dt * (c_phi.powi(2) * laplacian(&phi)
+ j_ego(&ego_pose));
// Hazard threshold
let hazard = rho.iter()
.any(|&r| r > cfg.hazard_threshold);Read the solver. Run the solver.
The full PDE solver is in the nfs-traffic crate. Under 400 lines of Rust. No dependencies you haven't heard of.