"""Cross-EO-product compound signal detection.""" from __future__ import annotations import numpy as np from app.models import CompoundSignal def compute_overlap_pct(mask_a: np.ndarray, mask_b: np.ndarray) -> float: """Compute overlap percentage: intersection / min(count_a, count_b) * 100.""" intersection = np.sum(mask_a & mask_b) min_count = min(np.sum(mask_a), np.sum(mask_b)) if min_count == 0: return 0.0 return float(intersection / min_count * 100) def _tag_confidence(n_products: int, overlap_pct: float) -> str: if n_products >= 3 and overlap_pct > 20: return "strong" if n_products >= 2 and overlap_pct >= 10: return "moderate" return "weak" def detect_compound_signals( zscore_rasters: dict[str, np.ndarray], pixel_area_ha: float, threshold: float = 2.0, ) -> list[CompoundSignal]: """Test for compound signal patterns across EO product z-score rasters.""" decline: dict[str, np.ndarray] = {} increase: dict[str, np.ndarray] = {} for product_id, z in zscore_rasters.items(): decline[product_id] = z < -threshold increase[product_id] = z > threshold signals: list[CompoundSignal] = [] # 1. Land conversion: NDVI decline + Settlement growth if "ndvi" in decline and "buildup" in increase: overlap = compute_overlap_pct(decline["ndvi"], increase["buildup"]) triggered = overlap > 10 affected = float(np.sum(decline["ndvi"] & increase["buildup"])) * pixel_area_ha signals.append(CompoundSignal( name="land_conversion", triggered=triggered, confidence=_tag_confidence(2, overlap) if triggered else "weak", description=( f"NDVI decline overlaps with settlement growth ({overlap:.0f}% overlap, " f"{affected:.1f} ha affected). Suggests possible vegetation loss to urbanization." ) if triggered else "No land conversion signal detected.", indicators=["ndvi", "buildup"], overlap_pct=overlap, affected_ha=affected, )) # 2. Flood event: SAR decrease + Water increase if "sar" in decline and "water" in increase: overlap = compute_overlap_pct(decline["sar"], increase["water"]) triggered = overlap > 10 affected = float(np.sum(decline["sar"] & increase["water"])) * pixel_area_ha signals.append(CompoundSignal( name="flood_event", triggered=triggered, confidence=_tag_confidence(2, overlap) if triggered else "weak", description=( f"SAR backscatter decrease coincides with water extent increase " f"({overlap:.0f}% overlap, {affected:.1f} ha). Suggests potential flooding." ) if triggered else "No flood signal detected.", indicators=["sar", "water"], overlap_pct=overlap, affected_ha=affected, )) # 3. Drought stress: NDVI decline + Water decline + SAR increase if "ndvi" in decline and "water" in decline and "sar" in increase: combined = decline["ndvi"] & decline["water"] & increase["sar"] n_combined = int(np.sum(combined)) min_single = min(np.sum(decline["ndvi"]), np.sum(decline["water"]), np.sum(increase["sar"])) overlap = float(n_combined / min_single * 100) if min_single > 0 else 0.0 triggered = overlap > 10 affected = n_combined * pixel_area_ha signals.append(CompoundSignal( name="drought_stress", triggered=triggered, confidence=_tag_confidence(3, overlap) if triggered else "weak", description=( f"NDVI decline, water decline, and SAR increase co-occur " f"({overlap:.0f}% overlap, {affected:.1f} ha). Suggests possible drought." ) if triggered else "No drought signal detected.", indicators=["ndvi", "water", "sar"], overlap_pct=overlap, affected_ha=affected, )) # 4. Displacement pressure: Settlement growth + NDVI decline adjacent if "buildup" in increase and "ndvi" in decline: from scipy.ndimage import binary_dilation expanded_buildup = binary_dilation(increase["buildup"], iterations=1) adjacent_decline = expanded_buildup & decline["ndvi"] & ~increase["buildup"] n_adjacent = int(np.sum(adjacent_decline)) n_buildup = int(np.sum(increase["buildup"])) overlap = float(n_adjacent / max(n_buildup, 1) * 100) triggered = overlap > 10 and n_adjacent > 0 affected = n_adjacent * pixel_area_ha signals.append(CompoundSignal( name="displacement_pressure", triggered=triggered, confidence=_tag_confidence(2, overlap) if triggered else "weak", description=( f"Settlement growth hotspots are adjacent to NDVI decline areas " f"({affected:.1f} ha of surrounding vegetation loss). " f"Suggests expansion into previously vegetated land." ) if triggered else "No displacement pressure signal detected.", indicators=["ndvi", "buildup"], overlap_pct=overlap, affected_ha=affected, )) return signals