Download app/analysis/compound.py from MERLx/Aperture: direct link, hf CLI and curl.
- Browser
- Download file 5.17 kB
-
https://huggingface.co/spaces/MERLx/Aperture/resolve/main/app/analysis/compound.py
- Command line
-
hf download hf://spaces/MERLx/Aperture/app/analysis/compound.py
-
curl -L -o compound.py https://huggingface.co/spaces/MERLx/Aperture/resolve/main/app/analysis/compound.py
5.17 kB
| """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 | |