Stage 1 is finished: a model that traces one ray now gets 80% of held-out rays exactly right (every hit within 1 m, up to 2 reflections). Everything is committed on branch `claude/serene-agnesi-18d752`. The write-up is in [07-learned-ray-tracing.md](docs/research/07-learned-ray-tracing.md) and results are in `runs/raynet/`.

**What I added**
- **Ray recorder** ([raypaths.py](rcm_ml/raypaths.py)): records each ray's path from our tracer: hit points, what happens at each hit (reflect, leave the tile, or stop after the last bounce), wall index, incidence angle and power. A test checks that these recorded paths, turned back into power, reproduce the tracer's direct and reflected map to 0.001 dB.
- **Data**: 256k training rays on the 501 official training maps, and 12,672 test rays on the 99 held-out test maps. The scene is the tracer's own building outlines, drawn at 0.5 m with smoothed edges, so walls line up to a few centimetres.

**Two ways to learn it, same data, 25 min of GPU each** (the GPU was shared with the other session's Sionna job):

| Model | Rays fully right, within 0.5 / 1 / 2 m | Same, if each leg starts from the tracer's true point | Wall-angle error (median) |
|---|---|---|---|
| Whole-map CNN that outputs the whole path at once (7.9M params) | 0.4 / 1.3 / 3.9% | – | – |
| Step model: predicts the next wall hit from a strip sampled along the ray (152k params) | 28 / 39 / 52% | 96 / 98 / 99% | 0.69° |
| **Step model + wall-angle refinement** (1.34M params) | **67 / 80 / 87%** | 96 / 98 / 99% | **0.06°** |

- **Why the step model:** a path is the same small problem repeated: find the first wall, then mirror. The model looks along the ray, scores each 0.5 m bin for "wall here", and the first likely wall wins automatically. It doesn't depend on the map's size, so it should carry over to big mosaics. The ray-marching itself (where to look) is hand-coded; the network learns where the wall is and which way it faces. The whole-map CNN is the comparison without that help, and it never learns to trace rays: its first hit is off by 3.6 m (median).
- **One step is solved; errors pile up over bounces.** The first wall is found to 5 cm (median), with the right outcome 99.9% of the time. In a full run, though, a wall-angle error δ turns the reflected ray by 2δ. That is why the refinement helped so much: a small network looks at an 8 m patch around each hit, and I switched the angle loss to one with a stronger gradient. Those two changes are confounded in this run.
- **Longer paths:** with up to 4 reflections, 63% of rays are fully right at 1 m.
- **Not converged:** validation accuracy was still rising at 25 min. This is one seed, and the timings are rough because of the shared GPU.
- **Not covered yet:** diffraction around corners, and learned power (power currently follows from path length and the fixed 10 dB per reflection).

**Proposed stage 2 (and the start of 3).** Every ray from a Tx already runs in one batch and shares the same scene raster, so 100 rays per Tx needs evaluation rather than new machinery:
1. **Fans of rays:** trace 100 evenly spaced rays per test Tx with the model and the tracer. Report how many are right per Tx, and rays per second on the GPU against the numba tracer on one core.
2. **Radio maps straight away:** turn predicted and true fans into power maps with the tracer's own code, and report outdoor RMSE in dB (floor −127 dB) at 100 rays and at the tracer's 7,200. I expect 80% exactly-right rays to give a nearly identical map, because a ray that clips a corner on the "wrong" side takes a different but equally plausible path.
3. **Cheap fixes first:** about 1 h of training, and training from slightly perturbed starting points so the model learns to recover from its own errors. Optionally, a variant that reads the wall segments directly, which gives exact wall angles by design.

Diffraction is the main thing missing to match the full tracer and WinProp IRT2; I'd add it in stage 4 as corner detection plus fans launched from the predicted corners.

Shall I go ahead with stage 2 as proposed, or change the order?