File size: 3,140 Bytes
277cbcf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
"""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)