mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-08-26 17:31:34 +00:00
fix(coverage): stop out-of-region NWS alerts (LA leak) — tighten state select + zone scoping
Two defects let an LA Heat Advisory broadcast under a southern-Idaho box: (1) states_for_bbox included states that merely clipped the box corner (CA/NV/UT/WY) → area=CA fetched California alerts. Now require >=0.25deg overlap in BOTH dimensions via _significant_overlap (also applied to avalanche_centers_for_bbox to prevent analogous over-inclusion there). (2) _in_coverage kept any zone-only alert (no polygon centroid). Now scope zone-only alerts by their UGC state prefix and fail closed when no zone info is available — an unlocatable alert must not broadcast under an active coverage box. Result: only alerts genuinely within the coverage box broadcast. Includes the LA-repro regression test (CAZ041 zone-only → False). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8125ba0978
commit
44344ad6ba
5 changed files with 269 additions and 18 deletions
|
|
@ -56,6 +56,21 @@ def point_in_bbox(lat: float, lon: float, bbox) -> bool:
|
|||
return south <= lat <= north and west <= lon <= east
|
||||
|
||||
|
||||
def _significant_overlap(a: list, b: list, min_deg: float = 0.25) -> bool:
|
||||
"""True iff boxes a,b [W,S,E,N] overlap by at least min_deg in BOTH lon and lat.
|
||||
|
||||
A mere corner or edge clip (tiny overlap in one dimension) does not count
|
||||
as meaningful coverage. Default of 0.25 degrees is intentionally
|
||||
conservative: it prevents states that barely nick the coverage box (e.g.
|
||||
CA clipping 0.04 deg in latitude, or WY clipping 0.12 deg in longitude)
|
||||
from polluting the NWS area query, while still capturing genuinely adjacent
|
||||
states like OR that overlap by ~0.5 deg or more in every dimension.
|
||||
"""
|
||||
ow = min(a[2], b[2]) - max(a[0], b[0]) # overlap width (lon)
|
||||
oh = min(a[3], b[3]) - max(a[1], b[1]) # overlap height (lat)
|
||||
return ow >= min_deg and oh >= min_deg
|
||||
|
||||
|
||||
def bbox_intersects(a, b) -> bool:
|
||||
"""Return True iff two [W, S, E, N] bounding boxes overlap (share any area).
|
||||
|
||||
|
|
@ -203,14 +218,17 @@ US_STATE_BBOXES: dict[str, list[float]] = {
|
|||
|
||||
|
||||
def states_for_bbox(bbox) -> list[str]:
|
||||
"""Return sorted state codes whose bbox intersects the given coverage bbox.
|
||||
"""Return sorted state codes whose bbox overlaps the given coverage bbox.
|
||||
|
||||
Uses bbox_intersects; a state whose bounding rectangle overlaps the coverage
|
||||
rectangle is included. Deduped and sorted alphabetically.
|
||||
Uses _significant_overlap (>= 0.25 deg in both lon and lat) rather than
|
||||
a bare intersection test. This prevents states that merely clip a corner
|
||||
of the coverage box from being included in NWS area queries — the root
|
||||
cause of the CA/LA-alert-under-Idaho-box defect. Deduped and sorted
|
||||
alphabetically.
|
||||
"""
|
||||
result = []
|
||||
for code, state_bbox in US_STATE_BBOXES.items():
|
||||
if bbox_intersects(bbox, state_bbox):
|
||||
if _significant_overlap(bbox, state_bbox):
|
||||
result.append(code)
|
||||
return sorted(set(result))
|
||||
|
||||
|
|
@ -258,10 +276,14 @@ AVALANCHE_CENTER_BBOXES: dict[str, list[float]] = {
|
|||
|
||||
|
||||
def avalanche_centers_for_bbox(bbox) -> list[str]:
|
||||
"""Return sorted center IDs whose area intersects the given coverage bbox."""
|
||||
"""Return sorted center IDs whose area overlaps the given coverage bbox.
|
||||
|
||||
Uses _significant_overlap (same conservative threshold as states_for_bbox)
|
||||
to avoid including avalanche centers that merely clip the coverage box corner.
|
||||
"""
|
||||
result = []
|
||||
for center_id, center_bbox in AVALANCHE_CENTER_BBOXES.items():
|
||||
if bbox_intersects(bbox, center_bbox):
|
||||
if _significant_overlap(bbox, center_bbox):
|
||||
result.append(center_id)
|
||||
return sorted(set(result))
|
||||
|
||||
|
|
|
|||
28
work/meshai/env/nws.py
vendored
28
work/meshai/env/nws.py
vendored
|
|
@ -177,18 +177,32 @@ class NWSAlertsAdapter:
|
|||
|
||||
Rules:
|
||||
- If no coverage bbox is set, always keep.
|
||||
- If the event has a numeric lat/lon centroid, drop it when outside the box.
|
||||
- If the event has no centroid (lat/lon absent or None), keep it — the
|
||||
state area= filter already scopes it; we can't box-filter without coords.
|
||||
- Polygon alerts (lat/lon centroid present): authoritative geographic
|
||||
check — drop when the centroid lies outside the coverage box.
|
||||
- Zone-only alerts (no polygon / no centroid): scope by UGC zone STATE.
|
||||
Keep if at least one affected UGC zone belongs to a state the coverage
|
||||
box covers (self._areas). Fail CLOSED when no zone info is available
|
||||
— an unlocatable alert must NOT broadcast under an active coverage box.
|
||||
"""
|
||||
if not self._coverage_bbox:
|
||||
return True
|
||||
lat = event.get("lat")
|
||||
lon = event.get("lon")
|
||||
if lat is None or lon is None:
|
||||
return True # no geometry — pass through
|
||||
from meshai.coverage import point_in_bbox
|
||||
return point_in_bbox(lat, lon, self._coverage_bbox)
|
||||
# Polygon alerts: authoritative geographic check on the centroid.
|
||||
if lat is not None and lon is not None:
|
||||
from meshai.coverage import point_in_bbox
|
||||
return point_in_bbox(lat, lon, self._coverage_bbox)
|
||||
# Zone-only alerts (no polygon/centroid): scope by the UGC zone STATE.
|
||||
# Keep only if at least one affected zone is in a state the box covers.
|
||||
# Fail CLOSED if no zone info — an unlocatable alert must not broadcast.
|
||||
ugcs = event.get("areas") or []
|
||||
if not ugcs:
|
||||
return False
|
||||
area_states = {a.upper() for a in (self._areas or [])}
|
||||
for ugc in ugcs:
|
||||
if isinstance(ugc, str) and len(ugc) >= 2 and ugc[:2].upper() in area_states:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _fetch(self) -> bool:
|
||||
"""Fetch alerts from NWS API.
|
||||
|
|
|
|||
|
|
@ -286,3 +286,95 @@ def test_map_nws_severity_moderate_to_routine(adapter):
|
|||
def test_map_nws_severity_minor_to_routine(adapter):
|
||||
"""Minor NWS severity maps to routine."""
|
||||
assert adapter._map_nws_severity("minor") == "routine"
|
||||
|
||||
|
||||
# ============================================================
|
||||
# _in_coverage TESTS — coverage-bbox filtering (LA leak fix)
|
||||
# ============================================================
|
||||
|
||||
# Southern-Idaho coverage box used in all _in_coverage tests.
|
||||
_IDAHO_COVERAGE_BBOX = [-116.993, 41.959, -110.984, 44.095]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def adapter_with_coverage(mock_config):
|
||||
"""NWSAlertsAdapter with a southern-Idaho coverage dict (areas=ID,OR)."""
|
||||
coverage = {
|
||||
"areas": ["ID", "OR"],
|
||||
"bbox": _IDAHO_COVERAGE_BBOX,
|
||||
}
|
||||
return NWSAlertsAdapter(mock_config, coverage=coverage)
|
||||
|
||||
|
||||
def test_in_coverage_no_bbox_always_true(mock_config):
|
||||
"""Without a coverage bbox every event passes through."""
|
||||
adapter_no_cov = NWSAlertsAdapter(mock_config)
|
||||
event = {"lat": 34.05, "lon": -118.24, "areas": ["CAZ041"]}
|
||||
assert adapter_no_cov._in_coverage(event) is True
|
||||
|
||||
|
||||
def test_in_coverage_la_repro_zone_only_dropped(adapter_with_coverage):
|
||||
"""LA-leak regression: a CAZ041 zone-only alert must be DROPPED.
|
||||
|
||||
This is the exact scenario that caused the bug: a Heat Advisory for
|
||||
Los Angeles carried no polygon (zone-based), so the old code returned True
|
||||
unconditionally for any event lacking lat/lon. The fixed code checks the
|
||||
UGC state prefix instead and rejects CA when coverage is ID/OR-only.
|
||||
"""
|
||||
event = {"lat": None, "lon": None, "areas": ["CAZ041", "CAZ042"]}
|
||||
assert adapter_with_coverage._in_coverage(event) is False, (
|
||||
"CAZ* (California) zone-only alert must be DROPPED under Idaho coverage"
|
||||
)
|
||||
|
||||
|
||||
def test_in_coverage_idaho_zone_only_kept(adapter_with_coverage):
|
||||
"""An Idaho zone-only alert (IDZ016) must be kept."""
|
||||
event = {"lat": None, "lon": None, "areas": ["IDZ016"]}
|
||||
assert adapter_with_coverage._in_coverage(event) is True
|
||||
|
||||
|
||||
def test_in_coverage_oregon_zone_only_kept(adapter_with_coverage):
|
||||
"""An Oregon zone-only alert (ORZ601) must be kept (OR is in coverage)."""
|
||||
event = {"lat": None, "lon": None, "areas": ["ORZ601"]}
|
||||
assert adapter_with_coverage._in_coverage(event) is True
|
||||
|
||||
|
||||
def test_in_coverage_polygon_centroid_inside_kept(adapter_with_coverage):
|
||||
"""A polygon alert whose centroid is inside the Idaho bbox is kept."""
|
||||
# Twin Falls, ID — clearly inside the coverage box
|
||||
event = {"lat": 42.56, "lon": -114.46, "areas": ["IDZ016"]}
|
||||
assert adapter_with_coverage._in_coverage(event) is True
|
||||
|
||||
|
||||
def test_in_coverage_polygon_centroid_outside_dropped(adapter_with_coverage):
|
||||
"""A polygon alert whose centroid is outside the bbox is dropped.
|
||||
|
||||
Los Angeles centroid (34.05, -118.24) is far south-west of the Idaho box.
|
||||
"""
|
||||
event = {"lat": 34.05, "lon": -118.24, "areas": ["CAZ041"]}
|
||||
assert adapter_with_coverage._in_coverage(event) is False
|
||||
|
||||
|
||||
def test_in_coverage_no_areas_no_centroid_dropped(adapter_with_coverage):
|
||||
"""Fail closed: an event with no UGC zones and no centroid must be DROPPED."""
|
||||
event = {"areas": [], "lat": None, "lon": None}
|
||||
assert adapter_with_coverage._in_coverage(event) is False
|
||||
|
||||
|
||||
def test_in_coverage_missing_areas_key_dropped(adapter_with_coverage):
|
||||
"""Fail closed: event with no 'areas' key and no centroid is dropped."""
|
||||
event = {}
|
||||
assert adapter_with_coverage._in_coverage(event) is False
|
||||
|
||||
|
||||
def test_in_coverage_multi_zone_one_match_kept(adapter_with_coverage):
|
||||
"""If any UGC zone is in-state the event is kept (at least one match)."""
|
||||
# Mix of out-of-state and in-state zones (e.g. a boundary advisory)
|
||||
event = {"lat": None, "lon": None, "areas": ["NVZ001", "IDZ016", "UTZ001"]}
|
||||
assert adapter_with_coverage._in_coverage(event) is True
|
||||
|
||||
|
||||
def test_in_coverage_ugc_case_insensitive(adapter_with_coverage):
|
||||
"""UGC state prefix matching must be case-insensitive."""
|
||||
event = {"lat": None, "lon": None, "areas": ["idz016"]}
|
||||
assert adapter_with_coverage._in_coverage(event) is True
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import pytest
|
|||
from meshai.coverage import (
|
||||
AVALANCHE_CENTER_BBOXES,
|
||||
US_STATE_BBOXES,
|
||||
_significant_overlap,
|
||||
arcgis_envelope,
|
||||
avalanche_centers_for_bbox,
|
||||
bbox_intersects,
|
||||
|
|
@ -539,3 +540,104 @@ def test_high_precision_grid_points_rounded():
|
|||
for i, (plat, plon) in enumerate(result["points"]):
|
||||
_assert_max_6dp(plat, f"traffic.points[{i}].lat")
|
||||
_assert_max_6dp(plon, f"traffic.points[{i}].lon")
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# _significant_overlap
|
||||
# ===========================================================================
|
||||
|
||||
# Southern-Idaho coverage box from the LA-leak bug report (high-precision form
|
||||
# as produced by a Leaflet map click — used in several tests below).
|
||||
IDAHO_BUG_BOX = [
|
||||
-116.99340820312501,
|
||||
41.95949009892467,
|
||||
-110.98388671875001,
|
||||
44.09547572946637,
|
||||
]
|
||||
|
||||
|
||||
def test_significant_overlap_solid_overlap_true():
|
||||
"""Two boxes that genuinely overlap return True."""
|
||||
a = [-116.5, 42.0, -112.0, 44.0]
|
||||
b = [-117.3, 41.9, -111.0, 49.1] # ID bbox — fully contains a
|
||||
assert _significant_overlap(a, b) is True
|
||||
|
||||
|
||||
def test_significant_overlap_corner_clip_false():
|
||||
"""A corner clip smaller than min_deg in one dimension returns False."""
|
||||
# CA bbox north=42.0, coverage box south=41.959 → lat overlap ~0.04 deg
|
||||
ca_bbox = [-124.5, 32.5, -114.1, 42.0]
|
||||
assert _significant_overlap(IDAHO_BUG_BOX, ca_bbox) is False
|
||||
|
||||
|
||||
def test_significant_overlap_lon_edge_clip_false():
|
||||
"""A lon-only edge clip smaller than min_deg returns False."""
|
||||
# WY bbox west=-111.1, coverage box east=-110.984 → lon overlap ~0.12 deg
|
||||
wy_bbox = [-111.1, 40.9, -104.0, 45.1]
|
||||
assert _significant_overlap(IDAHO_BUG_BOX, wy_bbox) is False
|
||||
|
||||
|
||||
def test_significant_overlap_both_small_false():
|
||||
"""Both dimensions below min_deg → False."""
|
||||
a = [-116.0, 42.0, -115.9, 42.09] # tiny box
|
||||
b = [-115.95, 42.05, -115.85, 42.12]
|
||||
# overlap_lon = -115.9 - (-115.95) = 0.05 < 0.25 → False
|
||||
assert _significant_overlap(a, b) is False
|
||||
|
||||
|
||||
def test_significant_overlap_custom_min_deg():
|
||||
"""Caller can supply a tighter or looser min_deg."""
|
||||
a = [-116.5, 42.0, -112.0, 44.0]
|
||||
# NV bbox lat overlap ~0.14 deg — excluded at 0.25 but included at 0.10
|
||||
nv_bbox = [-120.0, 35.0, -114.0, 42.1]
|
||||
assert _significant_overlap(a, nv_bbox, min_deg=0.10) is True
|
||||
assert _significant_overlap(a, nv_bbox, min_deg=0.25) is False
|
||||
|
||||
|
||||
def test_significant_overlap_symmetric():
|
||||
"""Order of arguments should not matter."""
|
||||
a = [-116.5, 42.0, -112.0, 44.0]
|
||||
b = [-117.3, 41.9, -111.0, 49.1]
|
||||
assert _significant_overlap(a, b) == _significant_overlap(b, a)
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# states_for_bbox — LA leak regression + significant-overlap behavior
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
def test_la_bug_box_yields_id_and_or_only():
|
||||
"""Regression: the southern-Idaho box that caused the LA-alert leak must
|
||||
return only ID and OR — not CA, NV, UT, or WY."""
|
||||
states = states_for_bbox(IDAHO_BUG_BOX)
|
||||
assert states == ["ID", "OR"], (
|
||||
f"Expected ['ID', 'OR'], got {states} — "
|
||||
"CA/NV/UT/WY must be excluded (corner-clip, not meaningful overlap)"
|
||||
)
|
||||
|
||||
|
||||
def test_la_bug_box_ca_not_present():
|
||||
"""CA must NOT appear — its inclusion caused real LA alerts to broadcast."""
|
||||
assert "CA" not in states_for_bbox(IDAHO_BUG_BOX)
|
||||
|
||||
|
||||
def test_la_bug_box_nv_ut_wy_not_present():
|
||||
"""NV, UT, WY must NOT appear — they clip only a thin sliver of the box."""
|
||||
states = states_for_bbox(IDAHO_BUG_BOX)
|
||||
for dropped in ("NV", "UT", "WY"):
|
||||
assert dropped not in states, f"{dropped} unexpectedly in {states}"
|
||||
|
||||
|
||||
def test_la_bug_box_id_and_or_present():
|
||||
"""ID and OR must appear — they have genuine significant overlap."""
|
||||
states = states_for_bbox(IDAHO_BUG_BOX)
|
||||
assert "ID" in states
|
||||
assert "OR" in states
|
||||
|
||||
|
||||
def test_genuine_two_state_box_returns_both():
|
||||
"""A box that genuinely straddles ID and OR with >0.25 deg overlap returns both."""
|
||||
# ID_OR_BOX is [-118.0, 43.5, -115.0, 46.0] — overlaps both by >1 degree
|
||||
states = states_for_bbox(ID_OR_BOX)
|
||||
assert "ID" in states
|
||||
assert "OR" in states
|
||||
|
|
|
|||
|
|
@ -186,14 +186,35 @@ def test_nws_in_coverage_outside_bbox_dropped():
|
|||
assert adapter._in_coverage(event) is False
|
||||
|
||||
|
||||
def test_nws_in_coverage_no_coords_kept():
|
||||
"""Alert with no lat/lon is always kept (can't filter without coords)."""
|
||||
def test_nws_in_coverage_zone_only_no_areas_dropped():
|
||||
"""Zone-only alert with no UGC areas and no centroid is dropped (fail closed).
|
||||
|
||||
The old behavior (always keep when no lat/lon) was the root cause of the
|
||||
LA-alert-under-Idaho-box defect. After the fix, zone-only alerts are
|
||||
scoped by their UGC state prefix; if there are no UGC codes at all the
|
||||
event cannot be located and must be dropped.
|
||||
"""
|
||||
adapter = _make_nws_with_bbox(IDAHO_BOX)
|
||||
event = {} # no lat/lon keys at all
|
||||
event = {} # no lat/lon keys, no areas
|
||||
assert adapter._in_coverage(event) is False
|
||||
|
||||
event_none = {"lat": None, "lon": None, "areas": []} # explicit empty areas
|
||||
assert adapter._in_coverage(event_none) is False
|
||||
|
||||
|
||||
def test_nws_in_coverage_zone_only_in_state_kept():
|
||||
"""Zone-only alert with an in-state UGC code is kept."""
|
||||
adapter = _make_nws_with_bbox(IDAHO_BOX)
|
||||
event = {"lat": None, "lon": None, "areas": ["IDZ016"]}
|
||||
assert adapter._in_coverage(event) is True
|
||||
|
||||
event_none = {"lat": None, "lon": None}
|
||||
assert adapter._in_coverage(event_none) is True
|
||||
|
||||
def test_nws_in_coverage_zone_only_out_of_state_dropped():
|
||||
"""Zone-only alert whose UGC zones are all out-of-state is dropped."""
|
||||
adapter = _make_nws_with_bbox(IDAHO_BOX)
|
||||
# CA zones — coverage is areas=["ID"]
|
||||
event = {"lat": None, "lon": None, "areas": ["CAZ041", "CAZ042"]}
|
||||
assert adapter._in_coverage(event) is False
|
||||
|
||||
|
||||
def test_nws_in_coverage_no_bbox_always_passes():
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue