This commit is contained in:
Andras Schmelczer 2026-06-02 20:14:32 +01:00
parent fbfebc651c
commit aab85fe32e
33 changed files with 2016 additions and 283 deletions

View file

@ -27,7 +27,7 @@ from .output import (
to_wgs84_geojson_multi,
write_district_geojson,
)
from .process_oa import _extract_polygonal, process_oa
from .process_oa import MIN_GEOM_AREA, _extract_polygonal, process_oa
from .uprn import get_oa_uprns, load_uprns
from .voronoi import _equal_split_fallback, compute_voronoi_regions
@ -341,6 +341,65 @@ class TestVoronoiDeduplication:
assert "B" in result, "Postcode B missing with int64 coords"
class TestVoronoiCoincidentClusterNotCrushed:
"""3+ postcodes at one coordinate must each keep a real cell.
Pre-fix, the first coincident postcode stayed unjittered at the exact
cluster centre; with other seeds in the OA its Voronoi cell was squeezed
below MIN_GEOM_AREA, so _clean_polygonal dropped that active postcode
downstream. The fix spreads coincident postcodes onto a small regular
polygon (equal wedges), so none is crushed.
"""
def test_coincident_cluster_plus_outer_seed_no_postcode_crushed(self):
# A block of flats: 4 distinct postcodes share one building coordinate,
# plus one other postcode elsewhere in the OA. Pre-fix, the centre seed's
# cell collapsed to ~0.0001 m^2 (< MIN_GEOM_AREA) and the postcode was
# dropped; every postcode must now keep a non-degenerate cell.
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
],
dtype=np.float64,
)
postcodes = ["A", "B", "C", "D", "OUT"]
result = compute_voronoi_regions(points, postcodes, boundary)
for pc in postcodes:
assert pc in result, f"Postcode {pc} was dropped"
assert result[pc].area > MIN_GEOM_AREA, (
f"Postcode {pc} cell {result[pc].area} <= MIN_GEOM_AREA"
)
def test_coincident_cluster_partitions_into_fair_wedges(self, square_boundary):
# N postcodes sharing one coordinate split the surrounding area into
# roughly equal wedges (regular-polygon seeds), none degenerate.
points = np.array([[500050, 180050]] * 5, dtype=np.float64)
postcodes = ["A", "B", "C", "D", "E"]
result = compute_voronoi_regions(points, postcodes, square_boundary)
fair_share = square_boundary.area / len(postcodes)
for pc in postcodes:
assert pc in result, f"Postcode {pc} was dropped"
# Each wedge is a meaningful fraction of its fair share (not crushed).
assert result[pc].area > 0.3 * fair_share, (
f"Postcode {pc} cell {result[pc].area} far below fair share {fair_share}"
)
def test_two_coincident_split_is_fair(self, square_boundary):
"""Regression: two postcodes at one coordinate split ~50/50."""
points = np.array([[500050, 180050], [500050, 180050]], dtype=np.float64)
postcodes = ["A", "B"]
result = compute_voronoi_regions(points, postcodes, square_boundary)
assert "A" in result and "B" in result
total = result["A"].area + result["B"].area
assert result["A"].area / total > 0.4
assert result["B"].area / total > 0.4
# ---------------------------------------------------------------------------
# Bug 4: Voronoi collinear fallback gives everything to first postcode
# ---------------------------------------------------------------------------