Notebook¶
Interactive notebook workflow - best for exploration, testing bounds, and verifying output.
Setup¶
Configure¶
CATALOG = "catalog"
SCHEMA = "data"
TABLE = "aef_embeddings"
YEARS = [2024, 2025]
BOUNDS = (520830, 170402, 542137, 187507) # London
TABLE_NAME = f"{CATALOG}.{SCHEMA}.{TABLE}"
Boundary filtering (optional)¶
Restrict ingestion to pixels intersecting a boundary file - chunks entirely outside it are dropped before any S3 read. Any boundary works (GeoParquet recommended); for Great Britain we recommend the ONS BFC (full resolution, clipped to the coastline) countries - see the download example in CLI.
BOUNDARY_PATH = f"/Volumes/{CATALOG}/{SCHEMA}/raw/boundaries/countries/Countries_December_2025_Boundaries_UK_BFC.parquet"
BOUNDARY_QUERY = "CTRY25NM in ['England', 'Scotland', 'Wales']"
Preview the processing grid¶
from aef_bng.config import AEFBNGConfig
from aef_bng.grid import BNGOutputGrid
config = AEFBNGConfig(
years=YEARS,
bounds=BOUNDS,
table_name=TABLE_NAME,
boundary_path=BOUNDARY_PATH, # omit for unfiltered ingestion
boundary_query=BOUNDARY_QUERY,
chunk_size=5_000, # serverless: workers cap out near one 10km chunk's peak
)
grid = BNGOutputGrid(config.bounds, config.chunk_size)
chunks = grid.enumerate_chunks()
print(f"10km chunks: {len(chunks)}")
print(f"Max possible rows: {len(chunks) * len(config.years) * 1_000_000:,}")
With a boundary configured, preview how many chunks will actually be processed (FULL chunks skip per-pixel masking; PARTIAL chunks rasterise their clipped boundary; OUTSIDE chunks are never read):
from collections import Counter
import shapely
from aef_bng.boundary import classify_chunk, load_boundary
boundary = load_boundary(
config.boundary_path, config.boundary_query, config.boundary_buffer_m
)
shapely.prepare(boundary)
counts = Counter(classify_chunk(c.bounds_bng, boundary)[0].value for c in chunks)
print(dict(counts)) # e.g. {'full': ..., 'partial': ..., 'outside': ...}
Run the pipeline¶
Apply liquid clustering¶
The parent grid references derived at extract time (grid_10km_ref,
grid_1km_ref) make good clustering keys for regional access - filtering or
aggregating by square - because a 10km/1km square's cells are scattered through
bng_ref string order (the ref interleaves easting and northing digits), so a
square can never be expressed as a bng_ref range:
spark.sql(f"ALTER TABLE {TABLE_NAME} CLUSTER BY (year, grid_10km_ref, grid_1km_ref)")
spark.sql(f"OPTIMIZE {TABLE_NAME}")
spark.sql(f"ANALYZE TABLE {TABLE_NAME} COMPUTE STATISTICS")
spark.sql(f"VACUUM {TABLE_NAME}")
Prefer CLUSTER BY (year, bng_ref) instead if your dominant access is exact-ref
point lookups or ref-keyed merges - narrow per-file bng_ref ranges prune those
better. All four key columns sit ahead of the 64 band columns, inside the
default 32-column Delta statistics window, so either choice has the file-level
min/max stats it needs.
Verify output¶
df = spark.table(TABLE_NAME)
df.printSchema()
print(f"Total rows: {df.count():,}")
df.groupBy("year").count().orderBy("year").show()
Spatial queries¶
The geometry column supports Databricks spatial functions. Liquid clustering
on the year and grid-reference columns means queries filtering on them skip
irrelevant files automatically.
from pyspark.sql import functions as F
result = df.filter(
F.expr(
"ST_Intersects(geometry, ST_GeomFromWKT("
"'POLYGON ((532322 176872, 532322 181313, 526975 181313, 526975 176872, 532322 176872))',"
" 27700))"
)
)
print(f"Cells in bbox: {result.count():,}")
Other examples¶
# Wales
config = AEFBNGConfig(
years=[2025],
bounds=(153325, 157362, 381182, 400529),
table_name="catalog.schema.aef_wales",
)
# All of Great Britain (all years), land only via the BFC boundary (clipped to
# the coastline at Mean High Water).
# Without the boundary the full bbox also ingests coastal water, Ireland,
# the Isle of Man, and the continental coast wherever AEF has data.
from aef_bng.constants import BNG_BOUNDS
config = AEFBNGConfig(
years=[2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025],
bounds=BNG_BOUNDS, # (0, 0, 700_000, 1_300_000)
table_name="catalog.schema.aef_bng_gb",
boundary_path=BOUNDARY_PATH,
boundary_query=BOUNDARY_QUERY,
)