"""Forecast-based parametric insurance trigger decision. Given a 5-day WBGT forecast, decides whether to issue no trigger, an alert cash transfer, or a full insurance payout. Used by the LastMileBench insurance benchmark to test forecast-driven trigger accuracy against ERA5 actuals ("perfect forecast") and GraphCast. The logic counts consecutive days from the start of the window where forecast WBGT meets the heat-wave threshold. Duration + severity determine the trigger level. """ from __future__ import annotations import sys from pathlib import Path # Import CRE's calculate_wbgt _CRE_ROOT = str(Path(__file__).resolve().parent.parent.parent) if _CRE_ROOT not in sys.path: sys.path.insert(0, _CRE_ROOT) from src.indexing.heat_index import calculate_wbgt def forecast_trigger_decision( forecast_wbgt: list[float], # 5 days of forecast WBGT values alert_duration_days: int = 2, payout_duration_days: int = 5, window_threshold_c: float = 35.1, # threshold for "heat wave day" payout_severity_c: float = 30.7, # min peak WBGT for full payout (always met in practice) ) -> str: """Predict trigger action from a 5-day WBGT forecast. Counts consecutive forecast days (from day 1) above window_threshold_c. Returns 'no_trigger', 'alert_cash', or 'full_payout'. Args: forecast_wbgt: List of daily WBGT values for the forecast window (typically 5 days starting from window_start + 1). alert_duration_days: Minimum consecutive days above threshold for an alert_cash trigger (default 2). payout_duration_days: Minimum consecutive days above threshold for a full_payout trigger (default 5). window_threshold_c: WBGT threshold for counting a day as a heat-wave day (default 35.1, the ERA5-based P90). payout_severity_c: Minimum peak WBGT required for full payout in addition to the duration requirement (default 30.7, always met in practice for Dar es Salaam). Returns: One of 'no_trigger', 'alert_cash', or 'full_payout'. """ # Count consecutive days from day 1 where WBGT >= threshold consecutive_days = 0 for wbgt in forecast_wbgt: if wbgt >= window_threshold_c: consecutive_days += 1 else: break peak_wbgt = max(forecast_wbgt) if forecast_wbgt else 0.0 if ( consecutive_days >= payout_duration_days and peak_wbgt >= payout_severity_c ): return "full_payout" if consecutive_days >= alert_duration_days: return "alert_cash" return "no_trigger" def compute_wbgt_from_forecast(temp_c: float, humidity_pct: float) -> float: """Compute WBGT from forecast temperature and humidity. Thin wrapper around the CRE project's calculate_wbgt function, which uses the Liljegren simplified outdoor WBGT formula. Args: temp_c: Air temperature in degrees Celsius. humidity_pct: Relative humidity in percent (0-100). Returns: Estimated outdoor WBGT in degrees Celsius. """ return calculate_wbgt(temp_c, humidity_pct)