From 16bc67e25cdf594a69957b40a1752ce9d38c41d3 Mon Sep 17 00:00:00 2001 From: malice Date: Mon, 6 Jul 2026 12:51:54 -0600 Subject: [PATCH] feat(coverage): widen adapter fetch scope to the enclosing box of coverage areas (#71) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The multi-box gate is authoritative, but adapters still need to FETCH the right data — otherwise a box crossing a state line never pulls the cross- state side. Feed each adapter's fetch scope (fires envelope, nws area=states, hydro bBox, etc.) from the enclosing bbox of config.coverage.areas (falling back to legacy coverage.bbox). The Shapely gate still narrows to the exact areas; the enclosing box just ensures cross-state / multi-area data is pulled. Co-authored-by: Matt Johnson Co-authored-by: Claude Opus 4.8 (1M context) --- work/meshai/coverage.py | 22 ++++++ work/meshai/main.py | 10 ++- work/tests/test_coverage_r3.py | 139 +++++++++++++++++++++++++++++++++ 3 files changed, 169 insertions(+), 2 deletions(-) create mode 100644 work/tests/test_coverage_r3.py diff --git a/work/meshai/coverage.py b/work/meshai/coverage.py index b487fc4..74693ad 100644 --- a/work/meshai/coverage.py +++ b/work/meshai/coverage.py @@ -104,6 +104,28 @@ def grid_points(bbox, cols: int = 3, rows: int = 3) -> list[tuple[float, float]] return points +def enclosing_bbox(areas) -> list: + """Union bounding box [west, south, east, north] of a list of area dicts + ({'west','south','east','north'}), or [] if empty/invalid. Used to widen + adapter FETCH scope so multi-area / cross-state data is pulled; the Shapely + gate narrows to the exact areas afterward.""" + boxes = [] + for a in areas or []: + try: + w, s, e, n = float(a["west"]), float(a["south"]), float(a["east"]), float(a["north"]) + boxes.append((w, s, e, n)) + except (KeyError, TypeError, ValueError): + continue + if not boxes: + return [] + return [ + round(min(b[0] for b in boxes), 6), + round(min(b[1] for b in boxes), 6), + round(max(b[2] for b in boxes), 6), + round(max(b[3] for b in boxes), 6), + ] + + def arcgis_envelope(bbox) -> dict: """Return ArcGIS FeatureServer spatial-filter query params for *bbox*. diff --git a/work/meshai/main.py b/work/meshai/main.py index 795889f..a8cf59a 100644 --- a/work/meshai/main.py +++ b/work/meshai/main.py @@ -409,9 +409,14 @@ class MeshAI: from meshai import coverage as _cov from types import SimpleNamespace _satpass_excluded = "satpass" in self.config.coverage.excluded_adapters + _cov_bbox = ( + (_cov.enclosing_bbox(self.config.coverage.areas) or self.config.coverage.bbox) + if (self.config.coverage.enabled and not _satpass_excluded) + else [] + ) _sat_scope = _cov.resolve_adapter_coverage( "satpass", - (self.config.coverage.bbox if (self.config.coverage.enabled and not _satpass_excluded) else []), + _cov_bbox, "native", ) if _sat_scope is not None: @@ -635,8 +640,9 @@ class MeshAI: from .env.store import EnvironmentalStore # Pass region anchors for fire proximity calculation region_anchors = self.config.mesh_intelligence.regions if self.config.mesh_intelligence.enabled else [] + from meshai.coverage import enclosing_bbox cov = self.config.coverage - coverage_bbox = cov.bbox if (cov.enabled and cov.bbox) else [] + coverage_bbox = (enclosing_bbox(cov.areas) or cov.bbox) if cov.enabled else [] self.env_store = EnvironmentalStore( config=env_cfg, region_anchors=region_anchors, coverage_bbox=coverage_bbox, event_bus=self.event_bus, diff --git a/work/tests/test_coverage_r3.py b/work/tests/test_coverage_r3.py new file mode 100644 index 0000000..a7f63fc --- /dev/null +++ b/work/tests/test_coverage_r3.py @@ -0,0 +1,139 @@ +"""Tests for R3 enclosing_bbox helper and cross-state fetch scope. + +Verifies: +- enclosing_bbox: two non-contiguous ID boxes → union covers both +- enclosing_bbox: single ID/OR-crossing box → enclosing == that box +- enclosing_bbox: empty input → [] +- enclosing_bbox: malformed entries are skipped +- resolve_adapter_coverage("nws", enclosing_bbox([ID/OR box]), "native")["areas"] + includes both ID and OR, proving cross-state fetch scope +""" + +from __future__ import annotations + +import pytest + +from meshai.coverage import enclosing_bbox, resolve_adapter_coverage + +# --------------------------------------------------------------------------- +# Test fixtures +# --------------------------------------------------------------------------- + +# Two non-contiguous Idaho boxes +ID_NORTH = {"name": "North Idaho", "west": -117.3, "south": 46.5, "east": -115.0, "north": 49.1} +ID_SOUTH = {"name": "Magic Valley", "west": -116.5, "south": 42.0, "east": -112.0, "north": 44.0} + +# Single box straddling ID/OR border (west edge is inside Oregon; OR bbox west=-124.6) +ID_OR_BOX = {"name": "ID/OR border", "west": -118.0, "south": 43.5, "east": -115.0, "north": 46.0} + + +# --------------------------------------------------------------------------- +# enclosing_bbox — two non-contiguous Idaho boxes +# --------------------------------------------------------------------------- + +def test_enclosing_two_idaho_boxes(): + result = enclosing_bbox([ID_NORTH, ID_SOUTH]) + assert result == [ + round(min(-117.3, -116.5), 6), # westernmost + round(min(46.5, 42.0), 6), # southernmost + round(max(-115.0, -112.0), 6), # easternmost + round(max(49.1, 44.0), 6), # northernmost + ] + west, south, east, north = result + # Enclosing box must contain both sub-boxes + assert west <= ID_NORTH["west"] and west <= ID_SOUTH["west"] + assert south <= ID_NORTH["south"] and south <= ID_SOUTH["south"] + assert east >= ID_NORTH["east"] and east >= ID_SOUTH["east"] + assert north >= ID_NORTH["north"] and north >= ID_SOUTH["north"] + + +# --------------------------------------------------------------------------- +# enclosing_bbox — single box that crosses the ID/OR line +# --------------------------------------------------------------------------- + +def test_enclosing_single_id_or_crossing_box(): + result = enclosing_bbox([ID_OR_BOX]) + assert result == [ + round(ID_OR_BOX["west"], 6), + round(ID_OR_BOX["south"], 6), + round(ID_OR_BOX["east"], 6), + round(ID_OR_BOX["north"], 6), + ] + # The box's west longitude is well into Oregon (OR west = -124.6) + west, _, _, _ = result + assert west < -117.0, "enclosing box should extend into Oregon longitude range" + + +# --------------------------------------------------------------------------- +# enclosing_bbox — empty / malformed inputs +# --------------------------------------------------------------------------- + +def test_enclosing_empty_list(): + assert enclosing_bbox([]) == [] + + +def test_enclosing_none_input(): + assert enclosing_bbox(None) == [] + + +def test_enclosing_malformed_entries_skipped(): + areas = [ + {"name": "bad", "west": "X", "south": 42.0, "east": -112.0, "north": 44.0}, + {"name": "missing key"}, + ID_SOUTH, + ] + result = enclosing_bbox(areas) + # Only ID_SOUTH should contribute + assert result == [ + round(ID_SOUTH["west"], 6), + round(ID_SOUTH["south"], 6), + round(ID_SOUTH["east"], 6), + round(ID_SOUTH["north"], 6), + ] + + +def test_enclosing_all_malformed_returns_empty(): + areas = [{"name": "bad"}, None, {"west": "X"}] + assert enclosing_bbox(areas) == [] + + +# --------------------------------------------------------------------------- +# Cross-state fetch scope: ID/OR crossing box → nws areas includes ID and OR +# --------------------------------------------------------------------------- + +def test_nws_cross_state_fetch_scope_id_or(): + """enclosing_bbox of a box crossing the ID/OR line → nws adapter returns + areas that include both ID and OR, so cross-state NWS data gets fetched.""" + bbox = enclosing_bbox([ID_OR_BOX]) + assert bbox, "enclosing_bbox should return a non-empty list" + scope = resolve_adapter_coverage("nws", bbox, "native") + assert scope is not None, "resolve_adapter_coverage should return scope for nws with valid bbox" + areas = scope["areas"] + assert "ID" in areas, f"ID should be in nws areas for ID/OR bbox, got: {areas}" + assert "OR" in areas, f"OR should be in nws areas for ID/OR bbox, got: {areas}" + + +# --------------------------------------------------------------------------- +# Cross-state fetch scope: two non-contiguous boxes → enclosing spans both +# --------------------------------------------------------------------------- + +def test_nws_fetch_scope_two_id_boxes(): + """enclosing_bbox of two separate ID boxes → nws areas includes ID.""" + bbox = enclosing_bbox([ID_NORTH, ID_SOUTH]) + scope = resolve_adapter_coverage("nws", bbox, "native") + assert scope is not None + areas = scope["areas"] + assert "ID" in areas + + +# --------------------------------------------------------------------------- +# Precision: enclosing_bbox rounds to 6 decimal places +# --------------------------------------------------------------------------- + +def test_enclosing_bbox_rounds_to_6dp(): + areas = [{"name": "precise", "west": -116.12345678, "south": 42.12345678, + "east": -112.12345678, "north": 44.12345678}] + result = enclosing_bbox(areas) + for val in result: + # 6 decimal places max (repr may be shorter if trailing zeros) + assert len(str(val).split(".")[-1]) <= 6