lawn-estimator-dev / tests /test_geometry.py
TempuraML's picture
fix(robustness): frame guard, atomic tile downloads, retry consistency
3c22a41
Raw
History Blame Contribute Delete
10.4 kB
"""Right-of-way estimation geometry β€” the street-aware extension and its fallbacks.
`build_estimation_geometry` is pure; we drive it with synthetic street/neighbor
callbacks over a real Omaha UTM square so the WGS84<->UTM math is faithful. These
lock in: exact extension area, corner-lot dual frontage, phantom removal via
neighbor subtraction (no-street fallback), and no bleed into an attached unit.
"""
from __future__ import annotations
import pytest
from pyproj import Transformer
from shapely.geometry import LineString
from shapely.ops import transform as shp_tf
from conftest import (
CENTER_LAT,
CENTER_LON,
OMAHA_CX,
OMAHA_CY,
SQFT_PER_SQM,
rect_utm,
square_utm,
utm_to_wgs,
)
from lawn_estimator.geometry import (
area_per_pixel_sqft,
build_estimation_geometry,
geometry_area_sqft,
geometry_exceeds_frame,
)
CRS = "EPSG:26914"
BUFFER_FT = 12.0
DIST_M = BUFFER_FT * 0.3048 # 3.6576 m
SIZE_M = 30.0
def _street(name, line_utm):
return (name, utm_to_wgs(line_utm))
def _streets_fn(*named_lines):
return lambda bounds: list(named_lines)
def _neighbors_fn(*polys_wgs):
return lambda bounds, exclude_id: list(polys_wgs)
# A horizontal centerline 10 m south of the parcel's south edge (within the 22 m
# street-detection window), and a vertical one 10 m east of the east edge.
SOUTH_STREET = _street("South St", LineString([(OMAHA_CX - 50, OMAHA_CY - 25),
(OMAHA_CX + 50, OMAHA_CY - 25)]))
EAST_STREET = _street("East St", LineString([(OMAHA_CX + 25, OMAHA_CY - 50),
(OMAHA_CX + 25, OMAHA_CY + 50)]))
def test_single_frontage_extension_exact_area():
parcel = utm_to_wgs(square_utm(OMAHA_CX, OMAHA_CY, SIZE_M))
_, est_utm, area_sqft, extensions = build_estimation_geometry(
parcel, BUFFER_FT, CRS,
fetch_neighbors=_neighbors_fn(), # no neighbors
fetch_streets=_streets_fn(SOUTH_STREET),
)
assert len(extensions) == 1
ext = extensions[0]
assert ext["street"] == "South St"
assert ext["street_facing"] is True
# One edge (30 m) extended by 12 ft (3.6576 m).
expected_ext = SIZE_M * DIST_M * SQFT_PER_SQM # ~1181.1 sqft
assert ext["area_sqft"] == pytest.approx(expected_ext, rel=2e-3)
expected_total = (SIZE_M * SIZE_M + SIZE_M * DIST_M) * SQFT_PER_SQM
assert area_sqft == pytest.approx(expected_total, rel=2e-3)
def test_corner_lot_gets_two_named_frontages_only():
parcel = utm_to_wgs(square_utm(OMAHA_CX, OMAHA_CY, SIZE_M))
_, _, area_sqft, extensions = build_estimation_geometry(
parcel, BUFFER_FT, CRS,
fetch_neighbors=_neighbors_fn(),
fetch_streets=_streets_fn(SOUTH_STREET, EAST_STREET),
)
# Two street-facing edges β†’ two frontages; the north/west edges get nothing.
assert len(extensions) == 2
assert {e["street"] for e in extensions} == {"South St", "East St"}
per_edge = SIZE_M * DIST_M * SQFT_PER_SQM
assert sum(e["area_sqft"] for e in extensions) == pytest.approx(2 * per_edge, rel=2e-3)
def test_no_street_data_extends_all_then_neighbor_subtraction_removes_phantom():
parcel = utm_to_wgs(square_utm(OMAHA_CX, OMAHA_CY, SIZE_M))
north_neighbor = utm_to_wgs(rect_utm(OMAHA_CX, OMAHA_CY + SIZE_M, SIZE_M, SIZE_M))
# Fallback path: no street source β†’ extend every edge (~4 strips).
_, _, area_no, ext_no = build_estimation_geometry(
parcel, BUFFER_FT, CRS, fetch_neighbors=_neighbors_fn(), fetch_streets=None)
_, est_with, area_with, ext_with = build_estimation_geometry(
parcel, BUFFER_FT, CRS, fetch_neighbors=_neighbors_fn(north_neighbor), fetch_streets=None)
one_strip = SIZE_M * DIST_M * SQFT_PER_SQM # ~1181 sqft
# The north neighbor carves one phantom strip out of both the area and the report.
assert area_no - area_with == pytest.approx(one_strip, rel=5e-2)
assert (sum(e["area_sqft"] for e in ext_no) - sum(e["area_sqft"] for e in ext_with)
== pytest.approx(one_strip, rel=5e-2))
# And the estimate never bleeds into the neighbor lot.
to_utm = Transformer.from_crs("EPSG:4326", CRS, always_xy=True).transform
assert est_with.intersection(shp_tf(to_utm, north_neighbor)).area == pytest.approx(0.0, abs=1.0)
# No-street extensions are reported unverified (street_facing is None).
assert all(e["street"] is None and e["street_facing"] is None for e in ext_with)
def test_attached_unit_no_bleed_into_neighbor_parcel():
# A duplex is one parcel per unit; a narrow lot with an adjoining unit to the
# east must not extend into that unit even in the extend-all fallback.
parcel = utm_to_wgs(rect_utm(OMAHA_CX, OMAHA_CY, 10.0, 30.0))
east_unit_utm = rect_utm(OMAHA_CX + 10.0, OMAHA_CY, 10.0, 30.0) # shares the east edge
east_unit = utm_to_wgs(east_unit_utm)
_, est_utm, _, _ = build_estimation_geometry(
parcel, BUFFER_FT, CRS,
fetch_neighbors=_neighbors_fn(east_unit), fetch_streets=None)
to_utm = Transformer.from_crs("EPSG:4326", CRS, always_xy=True).transform
neighbor_utm = shp_tf(to_utm, east_unit)
overlap = est_utm.intersection(neighbor_utm).area
assert overlap == pytest.approx(0.0, abs=1.0) # no measurable bleed
# ---------------------------------------------------------------------------
# Adaptive ROW-to-curb (row_to_curb flag): extend a deep-setback frontage toward
# the actual centerline (β‰ˆ the curb) instead of the flat 12 ft, floored at 12 ft
# (never shrinks a frontage) and capped at 25 ft (wide arterials can't blow up).
# ---------------------------------------------------------------------------
HALF_ROAD_FT = 15.0
CAP_FT = 25.0
HALF_ROAD_M = HALF_ROAD_FT * 0.3048 # 4.572 m β€” half a typical residential road
CAP_M = CAP_FT * 0.3048 # 7.62 m
def _south_street_at(offset_m, name="South St"):
"""Horizontal centerline `offset_m` south of the 30 m parcel's south edge, so
the south frontage's measured centerline distance is exactly `offset_m`. The
line spans only Β±12 m (< the 15 m half-width) so the side edges' nearest point
is an inward endpoint β†’ cleanly rejected by the outward-side test, isolating a
single south frontage regardless of how close the street sits."""
y = OMAHA_CY - SIZE_M / 2 - offset_m
return _street(name, LineString([(OMAHA_CX - 12, y), (OMAHA_CX + 12, y)]))
def _front_ext(offset_m, *, row_to_curb):
"""(front extension sqft, total estimation sqft) for a lone south frontage
whose centerline sits `offset_m` out."""
parcel = utm_to_wgs(square_utm(OMAHA_CX, OMAHA_CY, SIZE_M))
_, _, area_sqft, extensions = build_estimation_geometry(
parcel, BUFFER_FT, CRS,
fetch_neighbors=_neighbors_fn(),
fetch_streets=_streets_fn(_south_street_at(offset_m)),
row_to_curb=row_to_curb,
road_half_width_ft=HALF_ROAD_FT,
row_curb_cap_ft=CAP_FT,
)
assert len(extensions) == 1 # only the south edge faces the street
return extensions[0]["area_sqft"], area_sqft
def test_row_to_curb_reaches_curb_on_deep_setback():
# Centerline 10 m out β†’ curb β‰ˆ 10 βˆ’ 4.572 = 5.428 m past the property line,
# well beyond the flat 12 ft (3.6576 m) strip.
ext, _ = _front_ext(10.0, row_to_curb=True)
expected = SIZE_M * (10.0 - HALF_ROAD_M) * SQFT_PER_SQM # ~1752.8 sqft
assert ext == pytest.approx(expected, rel=3e-3)
flat, _ = _front_ext(10.0, row_to_curb=False)
assert ext > flat # adaptive reaches farther than the flat strip
def test_row_to_curb_capped_on_very_deep_setback():
# Centerline 16 m out β†’ 16 βˆ’ 4.572 = 11.428 m would overshoot; capped at 25 ft.
ext, _ = _front_ext(16.0, row_to_curb=True)
expected = SIZE_M * CAP_M * SQFT_PER_SQM # ~2460.7 sqft
assert ext == pytest.approx(expected, rel=3e-3)
def test_row_to_curb_floors_at_flat_strip_on_shallow_setback():
# Centerline 5 m out β†’ 5 βˆ’ 4.572 = 0.428 m would shrink the strip; floored at
# 12 ft, so a shallow lot is unchanged from the flat behavior.
ext_on, _ = _front_ext(5.0, row_to_curb=True)
ext_off, _ = _front_ext(5.0, row_to_curb=False)
assert ext_on == pytest.approx(ext_off, rel=1e-6)
def test_row_to_curb_off_leaves_deep_setback_at_flat_strip():
# The flag gates the change: with it off, a deep-setback frontage stays the
# flat 12 ft strip (the estimation path must not move when off).
ext_off, area_off = _front_ext(10.0, row_to_curb=False)
flat = SIZE_M * DIST_M * SQFT_PER_SQM # ~1181.1 sqft
assert ext_off == pytest.approx(flat, rel=2e-3)
_, area_on = _front_ext(10.0, row_to_curb=True)
assert area_on > area_off # the flag, and only the flag, moves the number
# ---------------------------------------------------------------------------
# Pure projection/area helpers
# ---------------------------------------------------------------------------
def test_area_per_pixel_scales_with_scale_factor():
# A scale-2 image has 4x the pixels, so each pixel covers 1/4 the ground area.
at_scale_1 = area_per_pixel_sqft(41.26, 20, 1)
at_scale_2 = area_per_pixel_sqft(41.26, 20, 2)
assert at_scale_1 == pytest.approx(4 * at_scale_2, rel=1e-9)
def test_geometry_area_of_known_square():
parcel = utm_to_wgs(square_utm(OMAHA_CX, OMAHA_CY, SIZE_M))
assert geometry_area_sqft(parcel, CRS) == pytest.approx(SIZE_M * SIZE_M * SQFT_PER_SQM, rel=2e-3)
# ---------------------------------------------------------------------------
# Frame guard (H-3): a parcel larger than the fixed imagery window is measured on
# truncated masks; the pipeline flags it rather than return a confident wrong number.
# ---------------------------------------------------------------------------
_FRAME_KW = dict(center_latitude=CENTER_LAT, center_longitude=CENTER_LON,
zoom=20, image_width_px=1280, image_height_px=1280, scale=2)
def test_normal_lot_fits_the_frame():
parcel = utm_to_wgs(square_utm(OMAHA_CX, OMAHA_CY, 30.0)) # ~30 m, well inside
assert geometry_exceeds_frame(parcel, **_FRAME_KW) is False
def test_oversized_parcel_exceeds_frame():
# The frame is ~71.8 m across at zoom 20; a 120 m lot overflows it.
parcel = utm_to_wgs(square_utm(OMAHA_CX, OMAHA_CY, 120.0))
assert geometry_exceeds_frame(parcel, **_FRAME_KW) is True