This commit is contained in:
Andras Schmelczer 2026-07-03 18:28:56 +01:00
parent 909e241907
commit 1ee796b282
29 changed files with 250 additions and 126 deletions

View file

@ -45,7 +45,7 @@ POSTCODE_DENSITY_PERCENTILE_COL = "Tree canopy density percentile within {radius
POSTCODE_AREA_COL = "Tree canopy area within {radius}m (sqm)"
POSTCODE_HEIGHT_COL = "Mean TOW height within {radius}m (m)"
# National Forest Inventory (NFI) woodland the geometric complement of TOW.
# National Forest Inventory (NFI) woodland: the geometric complement of TOW.
# NFI ships as a zipped shapefile of woodland parcels (>=0.5 ha) in EPSG:27700.
# Field names are from the NFI Woodland England 2022 release; re-check on bumps.
NFI_CATEGORY_COL = "CATEGORY"
@ -263,7 +263,7 @@ def _postcode_buffers(
return circles, shapely.STRtree(circles)
# 0.1 mm in the BNG working CRS (EPSG:27700) far below survey resolution; the
# 0.1 mm in the BNG working CRS (EPSG:27700), far below survey resolution; the
# same grid the postcode_boundaries overlay uses.
_OVERLAY_GRID_M = 1e-4
@ -274,13 +274,13 @@ def _robust_intersection_area(a: np.ndarray, b: np.ndarray) -> np.ndarray:
External Forest Research TOW/NFI polygons are occasionally invalid
(self-intersections), and a single bad polygon makes the batched
``shapely.intersection`` raise ``TopologyException: side location conflict``,
aborting the whole run. The fast path is the raw batched overlay unchanged,
full-speed, when the data is clean and only a failure triggers repair.
aborting the whole run. The fast path is the raw batched overlay (unchanged,
full-speed, when the data is clean), and only a failure triggers repair.
The repair deliberately uses a *plain* overlay rather than the fixed-precision
(``grid_size``) one: ``make_valid`` can emit a mixed-dimension
``GeometryCollection`` (a polygon plus a dangling line), which OverlayNG
rejects with ``Overlay input is mixed-dimension`` whereas a plain overlay
rejects with ``Overlay input is mixed-dimension``, whereas a plain overlay
accepts it, and its non-polygonal debris has zero area and is dropped by the
``clipped_area > 0`` filter downstream anyway. A final pointwise coordinate
snap (which never raises) collapses the near-coincident edges behind any
@ -496,7 +496,7 @@ def _finalize_metrics(
if over_count:
print(
f" note: {over_count:,} postcode(s) exceeded 100% raw canopy and were "
"capped — indicates overlapping TOW/NFI canopy within the buffer"
"capped, indicating overlapping TOW/NFI canopy within the buffer"
)
mean_height = np.divide(