Skip to main content

Wireless Coverage — LiDAR to Tower Siting

Wireless Coverage turns a USGS 3DEP LiDAR point cloud over San Francisco into ranked candidate cell-tower sites, using GeoBrix on Databricks: LiDAR returns become DSM / DTM / CHM surfaces, the surfaces are indexed to H3, and exact H3 line-of-sight viewsheds score each candidate site. It comes in two flavors: a production-ready Lakeflow pipeline that runs the whole flow as a scheduled job, and an interactive notebook series that walks through each step and the GeoBrix functions behind it.

Requires GeoBrix 0.5.2+

Both flavors depend on capabilities introduced in the 0.5.2 release — the lidar_gbx reader, the point-cloud surface functions (rst_binpoints, rst_chm, rst_isoband), and h3_viewshed_towers — so install GeoBrix 0.5.2 or newer.

Executed output — full San Francisco​

These screenshots are pulled from the executed Notebook Series run end to end over the whole San Francisco AOI (DEMO=False); the committed notebooks ship the smaller Golden Gate Park demo (DEMO=True).

First, the surface stack over all of San Francisco — bare earth (DEM) beside the full surface (DSM), so buildings and canopy read as the difference between them:

Full San Francisco — DEM (bare earth) vs DSM (surface: buildings + canopy) elevation bands at H3 resolution 10, side by side (Part 3a). The surface model rises above bare earth over downtown and the ridgelines.

Zooming into one neighborhood, Part 3a cross-checks that the H3 grid faithfully summarizes the underlying 1 m pixels — a hex reads as the color of the pixels beneath it:

Golden Gate Park detail — the 1 m-pixel CHM raster beneath the H3-gridded maximum canopy height, same window and colormap, so a hex reads as the color of the pixels it summarizes (Part 3a).

Finally — zooming to the Golden Gate Park demo, where the ranked sites are legible — Part 4 scores each candidate by how many res-12 cells it can see, so the best sites rise to the top:

Top-10 tower sites by coverage over Golden Gate Park — each site's res-12 H3 line-of-sight viewshed and 2.5 km buffer, with the sited towers marked (Part 4).


A production-ready Wireless Coverage pipeline: a Lakeflow Declarative Pipeline (SDP) plus a post-pipeline tower-siting task, packaged as a Databricks Asset Bundle and running entirely on the GeoBrix lightweight tier over Serverless compute.

This is the production counterpart to the notebook series (see the other tab) — same San Francisco AOI and USGS 3DEP LiDAR sources, but running as a scheduled medallion pipeline instead of one-off notebook outputs: a land downloader task stages the data, then the pipeline cascades bronze → silver → gold through declared table-to-table dependencies, a siting task runs the H3-native tower analysis, and a validate notebook confirms all outputs.

View on GitHub

notebooks/examples/wireless-coverage/lakeflow — the bundle, pipeline transformations, siting task, validation notebook, and full setup instructions live here. See its README for deploy/run steps, prerequisites, and configuration notes.

Runs on the lightweight tier (Serverless)

The pipeline and its land and siting tasks install the GeoBrix wheel with the extra that matches the Lakeflow runtime — as of this example DBR 18.3, so geobrix[light_dbr18,vizx] (pipeline) and geobrix[light_env5,vizx] (serverless job tasks), both the protobuf-5 regime — as a single pip dependency entry; pure Python/PySpark bindings (databricks.labs.gbx.pyrx, pygx), no JAR, Serverless throughout. Lakeflow pins compute by a runtime channel: check the current Lakeflow runtime channel when you deploy and switch the extra to match if it has moved (e.g. DBR 19 → [light_dbr19]/[light_env6]). The bundle defaults to Serverless; for heavier runs (notably full San Francisco) you can switch the pipeline and job tasks to classic compute (serverless: false + a cluster spec) for dedicated memory and throughput. See Execution Tiers.

How it works: from staged LiDAR to ranked tower sites​

Wireless Coverage series — config_nb → 01 → 02a/02b → 03a → 03b → 04

A scheduled run takes raw USGS 3DEP LiDAR .laz files through six stages:

  1. Land — a downloader task stages USGS 3DEP LiDAR EPT nodes and Overture water polygons onto a Unity Catalog Volume. Nothing is interpreted yet — data just arrives.
  2. Bronze — inventory — Auto Loader records one row per .laz file as it appears, so re-runs pick up only newly staged data.
  3. Silver — surface stack — classified returns are tile-indexed and binned into 1 m DSM and TIN DTM rasters per tile; rst_chm derives the Canopy Height Model.
  4. Gold — H3 grids — the surfaces are reprojected to EPSG:4326, isobanded, and indexed to H3 cells at resolution 10. Each table carries a stable res-9 join key so downstream consumers join on one column.
  5. Siting — a post-pipeline Spark task runs the H3-native tower-siting analysis: a res-9 candidate lattice, a coarse quick-pass to rule out weak sites, and exact res-12 H3 line-of-sight viewsheds on survivors (distributed via h3_viewshed_towers — one tower per Spark task).
  6. Validate — a final notebook task confirms all output tables are populated and spot-checks quality assertions.

Each stage reads only the one before it — declared as table-to-table dependencies — so Lakeflow builds the lineage graph, runs stages in order, and recomputes only what changed.

What a run looks like​

The bundle deploys one job, wireless_coverage_lf_job, whose four tasks run in sequence — land stages the data, pipeline refreshes the Lakeflow pipeline, siting ranks the tower sites, and validate checks the outputs:

The wireless_coverage_lf_job run graph in Databricks: the land, pipeline, siting, and validate tasks chained left to right, all on Serverless.

Opening the pipeline task shows the medallion lineage Lakeflow derived from the table-to-table dependencies. On a full San Francisco run, ~817 M classified LiDAR returns (wc_lidar_pts) bin into a few hundred 1 m DSM / DTM / CHM surface tiles, which roll up into the res-10 H3 gold grids:

A full San Francisco wireless_coverage_lf_pipeline run: wc_lidar_pts (817 M records) feeding wc_surface_dsm, wc_surface_dtm, and wc_surface_chm, then wc_dem_clipped and wc_dem_isobands, and the gold wc_h3_dem_res10, wc_h3_dsm_res10, and wc_h3_chm_res10 grids, each materialized view with its duration, output record count, and expectations.

Table catalog​

All tables land in {catalog}.{schema} (defaults: geospatial_docs.wireless_coverage_lf).

Bronze​

TableDescription
laz_inventoryAuto Loader streaming table — one row per staged .laz EPT file (path, file_size, _ingested_at).

Silver​

TableDescription
wc_lidar_ptsClassified LiDAR returns, tile-indexed with a res-15 H3 cell ID (EPSG:3857).
wc_surface_dsmDSM — max elevation of all returns per 1 m tile.
wc_surface_dtmDTM — Delaunay-TIN bare earth over class-2 ground returns (gap-free).
wc_surface_chmCHM — rst_chm(DSM, DTM): canopy/structure height in metres, clamped ≥ 0.

Gold​

TableDescription
wc_h3_dem_res10Bare-earth elevation-band H3 cells (res 10), land-masked, with res-9 parent join key.
wc_h3_dsm_res10Surface elevation-band H3 cells (res 10), land-masked, with res-9 parent join key.
wc_h3_chm_res10Max canopy/structure height per H3 cell (res 10), gap-filled, land-masked, with res-9 parent join key.

Siting​

The siting tables are regular Delta tables, overwritten each run. H3-indexed tables (*_res*) carry both a BIGINT cellid column and a STRING h3 column (for map tools that query string H3 keys) and are liquid-clustered on the BIGINT cell column for fast spatial queries. Coverage outputs are H3-indexed at resolution 12 and carry the res-9 parent join key.

TableDescription
wc_h3_surf_r13H3 res-13 surface bin (DSM max elevation per cell) — finest-resolution blocker surface. Columns: cellid BIGINT, h3 STRING, z DOUBLE; liquid-clustered on cellid.
wc_h3_grnd_r13H3 res-13 ground bin (DTM min elevation per cell) — finest-resolution bare-earth base. Columns: cellid BIGINT, h3 STRING, z DOUBLE; liquid-clustered on cellid.
wc_h3_quickpass_res9Every res-9 candidate's coarse quick-pass coverage fraction (pre-rule-out screen).
wc_h3_candidate_sites_res9Ranked tower survivors: cellid BIGINT, h3 STRING, lon, lat, required_mast, viewshed_cells, viewshed_pct, observer_z; liquid-clustered on cellid.
wc_h3_coverage_res12Per-cell line-of-sight depth at res 12 — how many sited towers can see each cell. Columns: cellid BIGINT, h3 STRING, tower_los_count LONG, res INT, parent_cellid, parent_cellid_res; liquid-clustered on cellid. Suitable for external H3 map layers.
wc_h3_coverage_top10_res12Same, aggregated over only the top-10 sites by coverage. Columns: cellid BIGINT, h3 STRING, tower_los_count LONG, res INT, parent_cellid, parent_cellid_res; liquid-clustered on cellid.
wc_h3_site_viewshed_res12Per-site fine-resolution viewshed: one row per sited tower + visible cell pair. Columns: site_cellid BIGINT, site_h3 STRING, cellid BIGINT, h3 STRING, res INT, parent_cellid, parent_cellid_res; liquid-clustered on (site_cellid, cellid) for per-site lookups and per-cell map tiles.

Deploy and run​

databricks bundle deploy
databricks bundle run wireless_coverage_lf_job

The bundle validates and deploys with the committed defaults in databricks.yml out of the box. To point it at your own workspace, copy databricks.override.yml.example to databricks.override.yml (git-ignored) and fill in your CLI profile; it is layered in automatically via include: ["*.override.yml"]. A fresh clone with no override file still validates/deploys.

  • Schedule: the job ships paused (pause_status: PAUSED, daily at 0 0 7 * * ? America/Chicago) — unpause it in the workspace once you've confirmed a manual run.
  • Shared LiDAR: if you have already run the notebook series and staged the USGS 3DEP data, set laz_dir (and optionally water_mask_dir) in your override file to reuse those files and skip the ~830 MB download. See databricks.override.yml.example for the exact paths.
  • GeoBrix wheel: gbx_wheel points at a staged geobrix-0.5.2-py3-none-any.whl. Stage your own build at that path or override the variable to wherever your wheel lives.

Configuration​

Catalog and schema names with hyphens

Unity Catalog catalog and schema names can include hyphens (e.g. solutions-catalog-default, sdsc-nyc-workshop-2026). Both the notebook series and the Lakeflow bundle properly quote all identifiers with backticks, so hyphenated names work without issue. If you encounter errors like "invalid identifier," check that your catalog/schema parameters are spelled correctly (case-sensitive).

Bundle variables (set in databricks.yml; override per-workspace in databricks.override.yml):

VariableDefaultDescription
cataloggeospatial_docsUnity Catalog catalog name.
schemawireless_coverage_lfUC schema; all pipeline and siting tables land here.
volumedataUC volume; raw data lands under {volume}/wireless-coverage-lf/.
full_aoitruetrue for the full San Francisco AOI; false for the Golden Gate Park demo window.
simplifytrueApply product st_simplify to isobands before H3 covering (default True = fast, no coverage loss, needed for full-SF performance; False = exact isobands, heavy/slow).
laz_dir(derived)Path to staged .laz EPT files. Leave empty to auto-derive under the Volume. Set to the notebook series' Volume path to reuse already-staged LiDAR.
water_mask_dir(derived)Path to staged Overture water GeoParquet. Leave empty to auto-derive.
gbx_wheel/Volumes/geospatial_docs/geobrix/sample-data/geobrix-0.5.2-py3-none-any.whlPath to the staged GeoBrix wheel.

Algorithm and resolution parameters are tunable via wireless_coverage.* keys in the pipeline configuration block. Key defaults:

KeyDefaultDescription
h3_res10H3 resolution for the gold elevation/canopy grids.
tower_res9H3 resolution for the naive candidate-tower lattice.
quickpass_res10Coarse quick-pass screen resolution.
viewshed_res12Exact H3 line-of-sight resolution (coverage output).
surface_bin_res13Finest H3 "pixel" bin resolution for siting surfaces.
max_dist_m2500Line-of-sight buffer radius in metres.
antenna_above_surface_m10Observer height above the DSM surface in metres.
quick_thresh_frac0.30Minimum quick-pass coverage fraction to survive rule-out.
simplifytrueSimplify isoband geometry via product st_simplify before H3 covering (default True; set False for exact isobands at the cost of heavy performance at full scale).

Prerequisites​

  • Unity Catalog: a catalog and schema (defaults geospatial_docs.wireless_coverage_lf) and a Volume named data under it. The pipeline creates sub-directories inside the Volume, not the Volume itself.
  • GeoBrix wheel staged at the gbx_wheel path. See Execution Tiers.
  • Network access: the land task fetches USGS 3DEP and Overture Maps data over HTTPS — Serverless has outbound internet by default.

Validation notebook​

The validate task (validate/validate_wc_lf.ipynb) runs after the siting task completes, reads all output tables, confirms each is non-empty, and prints summary statistics. Run it manually from the workspace as a quick sanity check after a fresh deploy or configuration change.