This commit is contained in:
Andras Schmelczer 2026-07-12 15:03:33 +01:00
parent 982e0cc89c
commit cfaf58dfba
44 changed files with 793 additions and 201 deletions

View file

@ -81,7 +81,7 @@ class TestFirstOADropped:
"""E00000001 is the first OA after sorting. It must appear in offsets."""
df, offsets = load_uprns(uprn_parquet)
assert "E00000001" in offsets, (
"First OA (E00000001) missing from offsets shift(1) null comparison bug"
"First OA (E00000001) missing from offsets: shift(1) null comparison bug"
)
def test_all_oas_present(self, uprn_parquet):
@ -221,7 +221,7 @@ class TestWhitespacePostcodes:
# The remapped point must be grouped under the successor's OA, not the
# terminated postcode's OA.
assert "E00000002" in offsets, "Successor OA missing remap kept old OA"
assert "E00000002" in offsets, "Successor OA missing: remap kept old OA"
assert "E00000001" not in offsets, (
"Remapped point still lives in the terminated postcode's OA"
)
@ -304,10 +304,10 @@ class TestVoronoiDeduplication:
# Plus postcode A has one at a different location
points = np.array(
[
[500020, 180050], # postcode A unique location
[500050, 180050], # postcode A shared location
[500050, 180050], # postcode B shared location (same coords)
[500080, 180050], # postcode B unique location
[500020, 180050], # postcode A: unique location
[500050, 180050], # postcode A: shared location
[500050, 180050], # postcode B: shared location (same coords)
[500080, 180050], # postcode B: unique location
]
)
postcodes = ["A", "A", "B", "B"]
@ -321,7 +321,7 @@ class TestVoronoiDeduplication:
points = np.array(
[
[500050, 180050], # postcode A
[500050, 180050], # postcode B identical coords
[500050, 180050], # postcode B: identical coords
]
)
postcodes = ["A", "B"]
@ -362,11 +362,11 @@ class TestVoronoiCoincidentClusterNotCrushed:
boundary = box(0, 0, 1000, 1000)
points = np.array(
[
[500, 500], # A coincident
[500, 500], # B coincident
[500, 500], # C coincident
[500, 500], # D coincident
[100, 100], # OUT elsewhere in the OA
[500, 500], # A: coincident
[500, 500], # B: coincident
[500, 500], # C: coincident
[500, 500], # D: coincident
[100, 100], # OUT: elsewhere in the OA
],
dtype=np.float64,
)
@ -412,7 +412,7 @@ class TestVoronoiCollinear:
"""Collinear points (handled by dummy corners) must distribute area fairly."""
def test_collinear_points_all_postcodes_get_area(self, square_boundary):
"""Points along a line every postcode must get area."""
"""Points along a line: every postcode must get area."""
points = np.array(
[
[500020, 180050],
@ -483,14 +483,14 @@ class TestProcessOAGeometryTypes:
def test_overlapping_inspire_no_postcode_overlap(self):
"""Overlapping INSPIRE parcels assigned to different postcodes must not overlap."""
oa_geom = box(500000, 180000, 500100, 180100)
# Two overlapping parcels left half and a wider middle section
# Two overlapping parcels: left half and a wider middle section
parcel_left = box(500000, 180000, 500060, 180100)
parcel_right = box(500040, 180000, 500100, 180100) # overlaps left by 20m
# UPRN in left parcel → postcode A, UPRN in right parcel → postcode B
points = np.array(
[
[500020, 180050], # postcode A inside left parcel
[500080, 180050], # postcode B inside right parcel
[500020, 180050], # postcode A: inside left parcel
[500080, 180050], # postcode B: inside right parcel
]
)
postcodes = ["A", "B"]
@ -744,7 +744,7 @@ class TestProcessOAInspireParcelAssignment:
[
[20, 50], # postcode A
[30, 50], # postcode A (majority)
[80, 50], # postcode B (minority would be dropped pre-fix)
[80, 50], # postcode B (minority, would be dropped pre-fix)
]
)
postcodes = ["A", "A", "B"]
@ -762,7 +762,7 @@ class TestProcessOAInspireParcelAssignment:
class TestProcessOASeedFootprintGuarantee:
"""Every postcode with a UPRN seed in a multi-postcode OA must produce a
fragment, even when its partition cell collapses below MIN_GEOM_AREA an
fragment, even when its partition cell collapses below MIN_GEOM_AREA. An
active postcode must never be dropped (validate_outputs is zero-tolerance)."""
def test_collapsed_voronoi_cells_rescued_as_footprints(self):
@ -935,7 +935,7 @@ class TestToWgs84Geojson:
to_bng = pyproj.Transformer.from_crs(
"EPSG:4326", "EPSG:27700", always_xy=True
)
# 0.9m x 0.9m square: area 0.81 m², perimeter 3.6 m — pointlike, yet
# 0.9m x 0.9m square: area 0.81 m², perimeter 3.6 m. Pointlike, yet
# large enough (~8 output-grid cells) to survive the 1e-6 deg snap.
tiny = box(530000, 180000, 530000.9, 180000.9)
from .output import _snap_to_wgs84_geojson
@ -969,7 +969,7 @@ class TestToWgs84Geojson:
def test_thin_sliver_keeps_minimal_buffer(self):
"""A genuine elongated sliver still carries length, so it is NOT inflated
to building scale only truly pointlike inputs are."""
to building scale. Only truly pointlike inputs are."""
import pyproj
from shapely.geometry import LineString, shape
from shapely.ops import transform as transform_geometry
@ -1052,7 +1052,7 @@ class TestFillHoles:
assert result.area == pytest.approx(Polygon(outer).area)
def test_large_hole_kept(self):
"""A large (>=1000 m²) hole is likely a real enclosed postcode keep it."""
"""A large (>=1000 m²) hole is likely a real enclosed postcode, so keep it."""
outer = [(0, 0), (100, 0), (100, 100), (0, 100), (0, 0)]
hole = [(20, 20), (80, 20), (80, 80), (20, 80), (20, 20)] # 60x60 = 3600 m²
poly_with_hole = Polygon(outer, [hole])
@ -1260,7 +1260,7 @@ class TestKeepDetachedParts:
instead of dropping all but the largest, which left ~1.8% uncovered gaps."""
def test_far_apart_parts_both_kept(self):
# Two 50x50m blocks 30m apart wider than the 10m merge buffer.
# Two 50x50m blocks 30m apart, wider than the 10m merge buffer.
a = box(0, 0, 50, 50) # 2500 m²
b = box(80, 0, 130, 50) # 2500 m², 30m gap
geom = merge_fragments([("AA1 1AA", a), ("AA1 1AA", b)])["AA1 1AA"]
@ -1421,7 +1421,7 @@ class TestGeojsonGeometrySliverValidity:
class TestColocatedPostcodesAllRetained:
"""Co-located non-geographic postcodes (e.g. AL1 9xx) have heavily-overlapping
tiny footprints; the de-overlap pass trims most to sub-grid slivers. None may
be dropped every active postcode must keep a (valid, non-empty) boundary."""
be dropped: every active postcode must keep a (valid, non-empty) boundary."""
def test_overlapping_tiny_footprints_none_dropped(self, tmp_path):
from shapely.geometry import Point, shape
@ -1447,7 +1447,7 @@ class TestSafeOverlayHelpers:
"""The robust overlay helpers retry on a fixed-precision grid after a
GEOSException (e.g. ``side location conflict`` from near-coincident OA-seam
edges). The grid is a caller-supplied parameter: metres for the BNG stages,
1e-6 degrees for the WGS84 output stage so the same helper serves both
1e-6 degrees for the WGS84 output stage, so the same helper serves both
without crushing degree-scale shapes on the metre default."""
def test_grid_param_honored_on_clean_inputs(self):
@ -1497,7 +1497,7 @@ class TestSafeOverlayHelpers:
# A self-intersecting bow-tie: invalid. set_precision()'s DEFAULT
# ('valid_output') mode runs its own noding pass that re-raises
# 'side location conflict' on this which is exactly how the production
# 'side location conflict' on this, which is exactly how the production
# crash happened (the fallback re-raised the error it was meant to absorb).
_BOWTIE = Polygon([(0, 0), (1e-5, 1e-5), (1e-5, 0), (0, 1e-5), (0, 0)])
# make_valid() of this spiky ring returns a mixed-dimension
@ -1831,7 +1831,7 @@ class TestFragmentsCache:
def test_missing_input_is_ignored(self, tmp_path):
cache = tmp_path / "fragments_cache.parquet"
cache.write_text("c")
# arcgis is optional/absent it cannot have invalidated the cache.
# arcgis is optional/absent, so it cannot have invalidated the cache.
assert fragments_cache_is_fresh(cache, [tmp_path / "absent.parquet"]) is True
@ -1885,7 +1885,7 @@ class TestEliminateSmallDetachedParts:
assert after == pytest.approx(before, rel=1e-6), "coverage must be conserved"
def test_genuine_large_split_is_kept(self):
# Two substantial parts (both 40000 m²) far apart a real bisection, not
# Two substantial parts (both 40000 m²) far apart: a real bisection, not
# scatter. Neither is below the absolute/fraction thresholds, so both stay.
a = MultiPolygon([_mbox(0, 0, 200, 200), _mbox(400, 0, 600, 200)])
out = _as_dict(_eliminate_small_detached_parts([("A", a)]))
@ -1909,8 +1909,8 @@ class TestEliminateSmallDetachedParts:
assert out["A"].geom_type == "Polygon"
def test_largest_part_always_retained_no_postcode_dropped(self):
# Even a postcode that is ENTIRELY a tiny sliver keeps its (largest) part
# active postcodes must never be dropped (validate_outputs is zero-tolerance).
# Even a postcode that is ENTIRELY a tiny sliver keeps its (largest) part.
# Active postcodes must never be dropped (validate_outputs is zero-tolerance).
tiny = _mbox(500, 500, 505, 505) # 25 m², the whole postcode
big = _mbox(0, 0, 300, 300)
out = _as_dict(
@ -1961,7 +1961,7 @@ class TestEliminateSmallDetachedParts:
assert out["A"].geom_type == "Polygon"
def test_gapped_sliver_within_nearest_radius_is_reassigned_not_dropped(self):
# A 300 m² sliver 1.2 m from its only neighbour N beyond the border probe
# A 300 m² sliver 1.2 m from its only neighbour N: beyond the border probe
# but inside the nearest-fallback radius. Before the gather buffer was
# widened to the accept radius, the old (smaller) gather buffer never
# returned N, so the sliver fell through to the drop branch. Now it is
@ -2017,7 +2017,7 @@ class TestGreenspaceReconnect:
from shapely.strtree import STRtree
postcode = box(0, 0, 200, 100)
barrier = box(60, 0, 130, 100) # 70 m wide beyond 2x the bridge width
barrier = box(60, 0, 130, 100) # 70 m wide, beyond 2x the bridge width
result = subtract_greenspace(postcode, STRtree([barrier]), [barrier])
assert result.geom_type == "MultiPolygon", "wide barrier should stay split"
assert len(result.geoms) == 2
@ -2035,7 +2035,7 @@ class TestGreenspaceReconnect:
def test_result_is_always_polygonal(self):
# Regression: _reconnect_split must never return a GeometryCollection
# (line/point debris) — downstream to_wgs84_geojson_multi would truncate a
# (line/point debris). Downstream to_wgs84_geojson_multi would truncate a
# GC to a single part, silently dropping substantial pieces. Sweep many
# strip widths/offsets (incl. coincident-edge-prone integer geometries).
from shapely.strtree import STRtree