# SpectrumNet: km-scale training data for range

Our only ground truth (RadioMapSeer, WinProp, 5.9 GHz, Tx/Rx 1.5 m) comes in 256 m tiles, and U-Nets trained on it paint
signal far beyond the Tx on the 2 km mosaic (06-cloudrf.md). The capped hybrid fixes that by anchoring to physics. This
note tests the other route: learn range from km-scale data. SpectrumNet (arXiv 2408.15252, CC0) has 1.28 km maps at
10 m, Rx 1.5 / 30 / 200 m, five frequencies (0.15 / 1.5 / 1.7 / 3.5 / 22 GHz), MATLAB ray tracing over OSM buildings
and terrain. It has no 5.9 GHz, so this is a proof of concept at 3.5 GHz.

Code: `rcm_ml/spectrumnet.py`, `scripts/get_spectrumnet.sh` (download), `scripts/spectrumnet.sh` (all results).
Numbers: `runs/spectrumnet/inspect.json`, `runs/spectrumnet/<run>/test.json`, `runs/spectrumnet/mosaic_2km.json`.
Figures: `figures/08_spectrumnet_inspect.png`, `figures/08_spectrumnet_2km.png`.


## Data

- The GitHub repo has per-environment samples (`RadioMaps01..11.*.zip`, 6–20 MB) but building/terrain files only for
  environments 01–04 (`npz_01..04`), none for DenseUrban/OrdinaryUrban, and the README's Google Drive link returns 404.
  The project page (spectrum-net.github.io) links the full set: `PPData5D-success.zip`, 1.43 GB (sha256 596c8196…),
  downloaded with approval by `scripts/get_spectrumnet.sh --full` into `data/spectrumnet/` (git-ignored). It holds
  94,458 maps (`png/<env>/`) and 9,558 building/terrain files (`npz/`).
- File names follow Appendix A of the manuscript: `T<env>C<climate>D<area>_n<Tx sampling>_f<freq>_ss_z<height>.png`,
  f00..f04 = 0.15 / 1.5 / 1.7 / 3.5 / 22 GHz, z00..z02 = Rx 1.5 / 30 / 200 m above the terrain. Not every map has all
  15 frequency × height images.
- Used here: urban, 3.5 GHz, Rx 1.5 m: DenseUrban 1,086 maps (281 areas), OrdinaryUrban 579 maps (182 areas).
- `npz`: `inBldg_zyx` (3 × 128 × 128, 0/1 = building present at each Rx height) and `terrain_yx` (m). No building heights
  beyond those three layers, no Tx position, no Tx power. Buildings cover 14.3 % of pixels at 1.5 m and 0.9 % at 30 m
  (almost all buildings are lower than 30 m); the RadioMapSeer 2 km mosaic has 25 % (`mosaic_2km.json`). Terrain varies by
  25.5 m within an area (median; p10 10.4 m, p90 69.8 m). Building pixels are always 0 in the 1.5 m maps.

## Inspection (`inspect.json`, `figures/08_spectrumnet_inspect.png`)

### Encoding: path gain dB = (gray − 242.6) / 1.44

Nothing in the paper or repo gives the pixel → dB rule. Gray 0 means "no ray found" (or building). For the rest, a joint
fit of all three heights against free-space loss at 3.5 GHz (line-of-sight street pixels 10–100 m from the Tx at 1.5 m,
everything within 30 m of the Tx at 30 m, within 300 m at 200 m; 199 single-Tx maps, 499,428 pixels):

| Tx height assumed | gray at 0 dB | gray per dB | RMSE (gray) | mean residual 1.5 / 30 / 200 m |
|---|---|---|---|---|
| 0 m | 243.0 | 1.434 | 9.38 | 0.25 / 0.11 / −0.02 |
| **1.5 m** | **242.7** | **1.431** | 9.38 | 0.18 / 0.44 / −0.02 |
| 5 m | 242.5 | 1.429 | 9.38 | −0.01 / 1.34 / −0.02 |
| 10 m | 242.8 | 1.434 | 9.38 | −0.31 / 2.86 / −0.01 |
| 30 m | 244.2 | 1.456 | 9.49 | −2.32 / 8.32 / 0.11 |

Bootstrap over maps (Tx 1.5 m): 240.6–245.3 gray at 0 dB, 1.409–1.460 gray per dB (95 %). We use 242.6 and 1.44
(inside both intervals; the offset uncertainty is about ±1.5 dB). An independent check: the same pixel at two frequencies
differs by about 1.4–1.5 gray per dB of free-space loss difference, e.g. 3.5 vs 1.7 GHz: 9.3 gray for 6.3 dB at 1.5 m
(1.49), 8.9 gray at 200 m (1.43); 22 GHz comes out at 1.56–1.58 because it also has atmospheric loss.
There is no per-image normalisation. The RMSE of 9 gray (≈ 6.5 dB) is the ground-reflection interference, visible as
rings in the 200 m layer.

**Tx height: low, consistent with the paper's 1.5 m.** The 30 m layer pins it: the residual there grows from 0.1 gray
at 0 m to 2.9 gray at 10 m and 8.3 gray at 30 m. The data cannot tell 0 m from 5 m.

### Transmitters: one to five per map (an assumption that was wrong)

The task assumed one Tx per map. The paper's comparison table says "multiple transmitters", and the 200 m layer shows a
ring pattern centred on each one (`figures/08_spectrumnet_inspect.png`). Positions are not stored, so `find_txs` detects
them: local maxima of the 1.5 m map at least as strong as free space at 15 m, kept only where the smoothed 30 m map also
peaks within 2 px (without that, bright street spots near a Tx were counted too). Result over 1,665 urban maps:
1 Tx 251, 2 Tx 350, 3 Tx 426, 4 Tx 414, 5 Tx 222, 6 Tx 2. Check: 93 % / 98 % of these Tx are also found at 150 MHz /
22 GHz (within 1 px). No Tx sits on a building pixel; Tx are anywhere in the area (median 18 px from the edge), not centred.
All distances below are to the **nearest** Tx; the training input marks all of them.

### Coverage by distance at Rx 1.5 m

Outdoor share above −127.2 dB (the RadioMapSeer floor). SpectrumNet at 3.5 GHz unless stated; the "5.9 GHz-equivalent"
rows subtract the free-space difference (4.5 dB), a lower bound on the real extra loss.

| | 0–100 m | 100–200 m | 200–400 m | 400–800 m | 800–1200 m |
|---|---|---|---|---|---|
| SpectrumNet DenseUrban | 85.5 % | 56.4 % | 33.1 % | 17.2 % | 8.5 % |
| SpectrumNet OrdinaryUrban | 90.5 % | 72.2 % | 55.1 % | 38.5 % | 24.6 % |
| DenseUrban, single-Tx maps only | 88.7 % | 60.6 % | 33.3 % | 16.4 % | 8.9 % |
| DenseUrban, 5.9 GHz-equivalent | 85.1 % | 55.9 % | 32.8 % | 17.0 % | 8.4 % |
| DenseUrban, 22 GHz | 77.4 % | 44.5 % | 23.0 % | 9.3 % | 2.3 % |
| WinProp DPM, 256 m tiles (`winprop_rings.json`) | 96.5 % | 77.1 % | 45.5 % | – | – |
| WinProp IRT2 | 85.9 % | 60.7 % | 33.3 % | – | – |
| WinProp IRT4 | 95.7 % | 78.5 % | 49.6 % | – | – |
| our tracer, 4 interactions, 2 km mosaic (`report.json`) | 92 % | 58 % | 15 % | 1 % | – |
| Sionna + two-corner, depth 5, 2 km mosaic | 96 % | 71 % | 12 % | 1 % | – |

WinProp's 200–400 m ring is mostly 200–260 m (the tile corners), so it is biased high against the others there.

What the SpectrumNet signal looks like (DenseUrban, 3.5 GHz, 1.5 m, `coverage.f03_1.5m_detail`):

| | 0–100 m | 100–200 m | 200–400 m | 400–800 m | 800–1200 m |
|---|---|---|---|---|---|
| outdoor pixels with no ray | 14.4 % | 43.4 % | 66.8 % | 82.8 % | 91.5 % |
| outdoor pixels with a ray but below −127 dB | 0.1 % | 0.1 % | 0.1 % | 0.0 % | 0.0 % |
| median gain where covered | −79.6 dB | −89.3 dB | −94.2 dB | −98.3 dB | −100.4 dB |
| free space at the ring middle | −77.3 dB | −86.8 dB | −92.9 dB | −98.9 dB | −103.3 dB |

- **The maps are binary.** A pixel either has a ray at about free-space level (within 3 dB at every distance) or none.
  Almost nothing is weak. The 5.9 GHz shift therefore changes coverage by less than 1 point: coverage here is
  "a ray path exists", not "the signal is above a threshold". The maps show hard shadow edges (no visible diffraction
  fringe), and covered pixels show no 40 dB/decade two-ray fall-off at 1.5 m.
- **Range falls off like WinProp IRT2 within 400 m** (DenseUrban 56 / 33 % vs IRT2 61 / 33 % at 100–200 / 200–400 m) and
  keeps going: 17 % of outdoor pixels are covered at 400–800 m and 8.5 % at 800–1200 m, where our tracer and Sionna have
  1 % on the mosaic. OrdinaryUrban (fewer buildings) covers about twice as much far out.
- At 30 m and 200 m almost everything is covered (DenseUrban 400–800 m: 75.6 % and 97.4 %).
- DenseUrban here means 14 % building cover; the RadioMapSeer mosaic has 25 %. Fewer buildings means more line of sight.

## Training (`runs/spectrumnet/<run>/test.json`)

Same U-Net as `runs/poc` / `runs/capped` (`models.UNet`, base 16, depth 5) at the native 128 px (10 m, 1.28 km). Inputs:
buildings at 1.5 m, Tx one-hot (all Tx), terrain relative to the strongest Tx (/50 m), optionally log distance to the
nearest Tx (`--dist`, as `train.py --dist`). Target: path gain on the RadioMapSeer training scale (floor −127.2 dB,
"no ray" = floor). MSE, Adam 1e-4, batch 15, lr × 0.1 after 60 %, random rotations/flips, best-on-val checkpoint.
Split by area (all Tx samplings of an area in one split): 1,173 / 173 / 319 maps (312 / 45 / 90 areas).
Budget: 480,960 samples seen (32,064 steps), the same as the 12-epoch `runs/capped` runs; 12 epochs of 1,173 maps
would be 35× less. 32–34 min per run on the M2 Max, two runs in parallel. Validation stops improving after ~10k steps.

`--gate` adds the coverage gate of `train.py --gate` (a "covered" logit trained with BCE, weight 0.02; prediction set to
the floor where it is negative). It is needed because the targets are binary: with MSE alone the network predicts a small
positive value wherever coverage is uncertain, and anything above 0 counts as covered.

Test set (319 maps), outdoor pixels:

| run | RMSE, floor-clipped | RMSE where truth covered | dead painted covered | covered painted dead | 0–100 m | 100–200 m | 200–400 m | 400–800 m | 800–1200 m |
|---|---|---|---|---|---|---|---|---|---|
| truth (coverage by ring) | | | | | 86.5 % | 61.4 % | 40.8 % | 25.5 % | 14.3 % |
| distance-only baseline (mean training profile) | 15.79 dB | 20.50 dB | 100 % | – | 100 % | 100 % | 100 % | 100 % | 100 % |
| geom | 9.44 dB | 12.46 dB | 89.9 % | 0.4 % | 99.9 % | 99.2 % | 95.9 % | 90.2 % | 87.0 % |
| geom + dist | 9.40 dB | 12.35 dB | 90.0 % | 0.3 % | 100 % | 99.5 % | 96.4 % | 90.3 % | 85.8 % |
| geom + gate | 9.90 dB | 13.99 dB | 7.4 % | 17.3 % | 88.3 % | 59.8 % | 38.0 % | 23.5 % | 13.8 % |
| geom + dist + gate, seed 0 | 9.85 dB | 13.21 dB | 10.0 % | 15.0 % | 90.4 % | 63.5 % | 41.7 % | 25.2 % | 13.2 % |
| geom + dist + gate, seed 1 | 9.86 dB | 13.92 dB | 7.2 % | 17.3 % | 88.5 % | 59.1 % | 38.1 % | 23.5 % | 13.2 % |
| geom + dist + gate, seed 2 | 9.88 dB | 13.70 dB | 8.1 % | 16.8 % | 90.2 % | 61.4 % | 38.8 % | 23.7 % | 13.0 % |

- **With the gate, the network learns range from km-scale data.** Its coverage by distance matches the truth within
  4 points in every ring out to 1.2 km, across three seeds; without the gate it marks 90 % of dead pixels as covered.
- The dB error is large (≈ 9.9 dB outdoor, 13–14 dB where covered, biased low by 5–7 dB). Most of it is the coverage
  boundary: a pixel that is wrongly gated costs tens of dB. These maps are much harder than RadioMapSeer tiles (10 m
  pixels, several Tx, terrain, binary targets); the numbers are not comparable with the 1.8 dB of `runs/capped`.
- The distance input helps little (13.2–13.9 vs 14.0 dB where covered). Seed spread is ≈ 0.7 dB there.

## On the 2 km mosaic (`mosaic_2km.json`, `figures/08_spectrumnet_2km.png`)

The SpectrumNet nets are applied unchanged to the 8 × 8 RadioMapSeer mosaic of 06-cloudrf.md (same Tx). Buildings are
averaged to 10 m cells (≥ 50 % = building; 23.4 % of cells vs 25.0 % of 1 m pixels), terrain is flat, the prediction is
repeated to 1 m and scored on the same 1 m outdoor mask, rings and −127.2 dB floor as `compare.json`. The other rows are
copied from `compare.json`. Outdoor % above the floor, and agreement with our tracer:

| method | 0–50 m | 50–100 m | 100–200 m | 200–400 m | 400–800 m | 800–1100 m |
|---|---|---|---|---|---|---|
| our tracer (proxy reference) | 95.9 % | 89.7 % | 53.7 % | 12.4 % | 0.2 % | 0.0 % |
| Sionna | 94.9 % | 91.7 % | 61.2 % | 11.2 % | 0.1 % | 0.0 % |
| B2 physics map | 100 % | 100 % | 87.5 % | 20.0 % | 0.2 % | 0.0 % |
| RadioMapSeer U-Net, geometry only | 100 % | 100 % | 99.0 % | 97.8 % | 93.7 % | 93.0 % |
| RadioMapSeer U-Net + B2, distance+gate+windows | 99.6 % | 99.1 % | 84.8 % | 41.0 % | 82.9 % | 97.7 % |
| capped hybrid +8 / −60 dB | 99.9 % | 99.4 % | 85.8 % | 36.0 % | 1.9 % | 0.0 % |
| SpectrumNet geom (3.5 GHz) | 97.7 % | 96.3 % | 97.8 % | 85.2 % | 79.5 % | 82.5 % |
| SpectrumNet geom, 5.9 GHz-equivalent (−4.5 dB) | 95.2 % | 85.7 % | 34.8 % | 4.5 % | 0.5 % | 0.0 % |
| SpectrumNet geom + gate | 94.6 % | 73.7 % | 27.9 % | 0.4 % | 0.0 % | 0.0 % |
| SpectrumNet geom + dist + gate, seed 0 / 1 / 2 | 94.6 % | 75.1 / 59.3 / 74.5 % | 51.4 / 13.0 / 26.3 % | 5.9 / 0.8 / 1.1 % | 0.3 / 0.0 / 0.0 % | 0.0 % |
| agreement with the tracer, dist + gate seed 0 | 92.3 % | 78.4 % | 74.2 % | 91.2 % | 99.6 % | 100 % |
| agreement with the tracer, capped +8 / −60 dB | 96.0 % | 90.2 % | 67.8 % | 74.3 % | 98.1 % | 100 % |

- **The gated SpectrumNet nets do not paint the far field.** At 400–800 m they cover 0–0.3 % (tracer 0.2 %, Sionna
  0.1 %) and agree with the tracer on 99.6–99.8 % of pixels, as the capped hybrid does, but without a physics channel:
  the range comes from training on 1.28 km maps.
- **They are too short-ranged near the Tx.** At 100–200 m they cover 13–51 % (tracer 54 %, Sionna 61 %, and 07-far-convergence.md
  says those references are themselves low), and the three seeds disagree by almost 40 points. On SpectrumNet's own test
  set the same nets keep 15 % coverage at 400–800 m in DenseUrban; on the mosaic, with 25 % buildings and narrow,
  irregular streets at 10 m resolution, they find almost no open path beyond 200 m. A 50–100 m street is 5–10 pixels
  here, so the 10 m grid itself cuts many street canyons.
- Without the gate, the 4.5 dB frequency shift alone moves 400–800 m coverage from 79.5 % to 0.5 %: those far pixels are
  faint values just above the floor, i.e. the regression artefact, not coverage.

## What this does and does not show

- SpectrumNet is usable as km-scale training data, after two corrections to the task's assumptions: the encoding had to be
  inferred (1.44 gray per dB, ±1.5 dB offset), and maps hold up to five Tx, whose positions had to be detected.
- A U-Net with a coverage gate learns coverage versus distance from it, and transfers that to our mosaic as "no far field".
  That matches the tracer and Sionna. It is a second, independent route to the far-field fix of the capped hybrid.
- It is not a 5.9 GHz model and not WinProp. SpectrumNet's propagation is different in kind: binary ray reachability at
  about free-space level, no diffraction, no weak tail, so coverage depends on line-of-sight geometry and hardly on
  frequency or the floor. Its buildings are sparser than RadioMapSeer's, and 10 m pixels blur streets. Its dB values are
  not a good target for our maps (13–14 dB error where covered).
- Near-Tx coverage (100–400 m) remains the open question for every method; this one errs low and is seed-sensitive.
- Next steps if pursued: train on 3.5 GHz + 1.5 GHz together (more maps, coverage hardly depends on frequency), use the
  SpectrumNet gate as a far-field mask on top of the RadioMapSeer U-Net (fine detail from RadioMapSeer, range from
  SpectrumNet), or fine-tune at 2–5 m pixels on resampled SpectrumNet maps.

