You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: RF-MODEL.md
+51-1Lines changed: 51 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -103,6 +103,38 @@ The full per-class P.452 (hₐ, dₖ) and P.833 (γ, A) parameters are visible i
103
103
104
104
Outside the United States (or anywhere the NLCD bake doesn't cover), the model falls back to **Mixed Forest** as a conservative default for every pixel. Future work tracks adding ESA WorldCover as a global fallback.
105
105
106
+
## Per-pixel building heights — JRC GHS-BUILT-H 100 m (optional)
107
+
108
+
NLCD's developed-intensity classes (21/22/23/24) ship with class-nominal heights of 4 / 9 / 15 / 25 m from P.452 Table 4. These are continental averages — wrong by ±15 m for any specific neighbourhood. They also can't put real diffractors (apartment blocks, industrial silos, isolated office towers) into the ITM propagation profile.
109
+
110
+
When building tiles are baked from **JRC GHS-BUILT-H R2023A ANBH** (Average Net Building Height, 100 m global, CC BY 4.0), two integration sites in the model use the measurements:
111
+
112
+
1.**DSM into the ITM profile.** Mid-path samples become `terrain + measured_building_height` so ITM's diffraction algorithm sees rooftops as actual obstacles. Endpoints (TX and RX) stay bare-earth — your `txHeightM` / `rxHeightM` are AGL above local ground, not on top of a presumed building. Operators with rooftop-mounted antennas should add the building's height to their antenna AGL field manually.
113
+
2.**Measured `h_a` in P.452 endpoint clutter.** For developed-class pixels (21/22/23/24) at TX and RX, the measured ANBH replaces `cls.nominalHeightM` in the height-gain formula. A 2 m handheld in a 100 m cell that ANBH says is 4 m gets the same ~19 dB endpoint loss; in a cell that's actually 14 m, the cell-specific value drives the calculation. Non-developed classes (vegetation, water, etc.) keep their class-nominal heights so trees don't get erased when a forest pixel happens to read ANBH = 0.
114
+
115
+
**100 m averaging caveats.** ANBH averages over the *built portion* of each 100 m cell, so a neighbourhood of 30% 9 m houses + 5% 25 m apartment block reads as ~11 m — the apartment block is captured as elevated DSM but its sharp edges get smeared. ITM's above-rooftop diffraction (the dominant path at typical link distances) is well-served by this; ground-level street-canyon waveguide effects need ray-tracing, not raster, regardless of resolution.
116
+
117
+
The "Building heights" toggle in the Coverage and Scan settings panels lets operators turn the refinement off (compare measured-vs-nominal, or use bare-earth ITM for a baseline). Default is on.
118
+
119
+
**Operator runbook for the building bake:**[scripts/README-buildings.md](scripts/README-buildings.md).
120
+
121
+
## Per-pixel canopy heights — ETH 10 m (optional)
122
+
123
+
NLCD tells the model *what kind* of canopy is at a pixel; class-nominal heights from P.452 Table 4 (15 m deciduous, 20 m evergreen, 15 m mixed, 10 m woody-wetland, 2 m emergent-wetland) tell it *how tall* that canopy is. Real canopy varies — a 50 m redwood next to a 5 m madrone in the same NLCD "Evergreen Forest" pixel both got coded as 20 m without measurement.
124
+
125
+
When canopy tiles are baked from **ETH Global Canopy Height 2020** (Lang et al. 2023, 10 m global, CC BY 4.0), the P.833 MED loop uses the measured height per sample instead of the class-nominal:
126
+
127
+
- The path-integral gate `zPath < terrain + canopyHeight` evaluates against measured canopy at every profile sample.
128
+
- Where the measured value is 0 (clearings, fire scars, recent logging), MED contributes nothing for that sample — the path is above any canopy.
129
+
- Where the bake doesn't cover a pixel (out-of-bbox / ocean adjacency), the loop falls back to the class-nominal value silently.
130
+
- Endpoint clutter (P.452) still uses class-nominal heights — Tier 3 only refines the path-integrated MED. Endpoint heights are a Tier-2 (per-pixel building heights) refinement.
131
+
132
+
If the bake includes the optional standard-deviation file (`scripts/canopy_tiles.py --include-stddev`), pixels with σ ≥ height are blended 50/50 with the class-nominal so a single noisy ETH pixel can't dominate the integral.
133
+
134
+
The "Canopy heights" toggle in the Coverage and Scan settings panels lets operators turn the refinement off (e.g. to compare measured-vs-nominal predictions, or to validate the older behavior). Default is on.
135
+
136
+
**Operator runbook for the canopy bake:**[scripts/README-canopy.md](scripts/README-canopy.md).
137
+
106
138
## Setup — running the bake
107
139
108
140
NLCD data is not bundled with MeshInfo (it's ~2 GB). Operators run a one-shot bake script after deploying:
@@ -119,6 +151,22 @@ CONUS at full resolution takes 15–90 min depending on CPU. Tiles are bind-moun
119
151
120
152
Once tiles exist, the API mounts them at `/tiles/landcover/{z}/{x}/{y}.png` and the frontend fetches them on every coverage compute. The "Land cover: USGS NLCD" status chip in the Coverage panel shows whether tiles are healthy or the model is using the fallback class.
121
153
154
+
For the optional canopy-height refinement, run the canopy bake separately:
155
+
156
+
```bash
157
+
docker compose --profile bake run --rm canopy-bake # CONUS default; --scope global also supported
158
+
```
159
+
160
+
CONUS canopy bake is ~30 GB of source COGs + ~1–2 GB of output tiles. Time: 1–4 hours depending on the ETH file server and CPU. Without this bake, the model uses class-nominal canopy heights — the coverage tool still works.
161
+
162
+
For the optional building-height refinement, run the building bake:
163
+
164
+
```bash
165
+
docker compose --profile bake run --rm building-bake # global default; --scope conus also supported
166
+
```
167
+
168
+
Global building bake is ~1.8 GB of source GeoTIFF + ~1–2 GB of output tiles. Time: 1–3 hours. Without this bake, the model uses class-nominal heights for developed classes and bare-earth ITM — the coverage tool still works.
169
+
122
170
## The aggression scaler
123
171
124
172
Inside the Coverage and Scan panels, the **Clutter** control is a 3-stop slider:
@@ -142,7 +190,7 @@ The default (1.0×) reflects ITU-R P.452 / P.833 published values. Don't sit at
142
190
These are scope limits that affect prediction accuracy in specific scenarios:
143
191
144
192
-**Indoor receivers (ITU-R P.2109)** — RX inside a building gets +10–20 dB additional loss not modeled. Outdoor-to-outdoor only.
145
-
-**Per-tree / per-building height** — uses class-nominal canopy heights (15 m deciduous, 20 m evergreen, etc.), not measured heights from LIDAR. A single tall redwood next to your antenna isn't picked up.
193
+
-**Per-building height** — when the building bake is in place, JRC GHS-BUILT-H 100 m measurements replace class-nominal heights for developed classes and feed buildings into the ITM DSM. The 100 m averaging captures first-order diffraction physics but smooths individual rooftops; per-building precision (a single tall office tower across the street from your antenna) needs vector pipelines beyond this raster model.
146
194
-**Seasonal foliage** — deciduous classes assume leaf-on (summer) at 0.5 dB/m. Out-of-leaf is ~0.15 dB/m; not modeled. Future work.
147
195
-**Frequencies other than 915 MHz** — the P.833 specific-attenuation values are calibrated at 915 MHz. Adding 868 MHz (EU) or 433 MHz would need a per-band lookup.
148
196
-**Polarization-dependent vegetation loss** — uses unpolarized averages (the slight per-polarization difference is in the noise floor here).
@@ -156,6 +204,8 @@ These are scope limits that affect prediction accuracy in specific scenarios:
0 commit comments