I wrote the plan in [docs/research/07-raster-beam-tracer.md](docs/research/07-raster-beam-tracer.md). Nothing is built or committed yet.

**The setup.** Three uint8 images go in: **H** is height at 0.5 m per level (0–127 m), **R** is reflection loss at 0.1 dB per level, and **S** is the scattering share, 0–1. Out comes one image of received power per pixel, in dB. Rays move through the pixels themselves, not along building outlines, so each step costs the same however many buildings there are. All rays move one step at a time together on the Mac GPU, using PyTorch on Metal (MPS). The target is 100k–1M rays per map in about a second. For comparison, our polygon tracer takes 65 s on the 2 km map.

**About the third image.** The standard term for what you described is **diffuse scattering**: a rough surface sends part of the energy in many directions instead of one mirror direction. "Splitting a beam into 2–3" is one way to simulate that. I'd use a GPU-friendly version by default: at each hit, every ray continues as one new ray. With probability 1−S it bounces like a mirror, otherwise it goes off in a random diffuse direction. On average this gives the right answer, and the number of rays never grows. Literal splitting into k rays is a switch for low bounce counts, because the ray count multiplies by k at every bounce.

**Three things that could catch us out:**
- **Energy fall-off in 2D.** On a flat map, rays from a point spread out as 1/d, not 1/d². Each ray has to carry an extra distance weight, which our current tracer already does. With heights (2.5D), the 1/d² comes out naturally.
- **Wall directions from pixels.** A wall drawn in pixels is a staircase, so its local direction is noisy and mirror bounces smear. The plan is to get directions from a slightly blurred image of the buildings. A single-wall test (comparing against the exact mirror answer) will tell us early whether that is good enough.
- **Bending around corners.** Without it, everything behind a building is dark, which is most of a street-level map. Corners can be found from the pixels and given the bending loss we already fitted for the polygon tracer (6 dB plus 20 dB per 90° turn).

**Phases:**
1. **Half-day speed test:** rays crossing an empty map, measuring rays per second.
2. **2D version** with reflection and scattering, checked against our polygon tracer and WinProp on the same 20 maps as before.
3. **Corners**, then the 512 m to 2 km maps.
4. **Optional:** a trainable version that fits the R and S images to WinProp results.
5. **Heights and drone altitudes**, checked against our Sionna drone maps.

These images are also the format the U-Nets read. So this tracer could produce training data for the ray-prediction model in your other session.

**Before I build, three decisions:**
1. **2D first or heights first?** I'd start in 2D, where we have WinProp results to check against, and add heights in phase 5. If drone links are the priority now, we can do heights from the start.
2. **Where do R and S come from?** Real data (OSM material tags, land use) or free values we fit? This decides how much phase 4 matters.
3. **Should I start the speed test now?**