diff --git a/climate_tookit/calculate_hazards/ensemble_hazards.py b/climate_tookit/calculate_hazards/ensemble_hazards.py index 43cd3b0..f7f6dde 100644 --- a/climate_tookit/calculate_hazards/ensemble_hazards.py +++ b/climate_tookit/calculate_hazards/ensemble_hazards.py @@ -32,6 +32,7 @@ add_et0, water_balance_hazards, heat_stress_hazards, + dry_days_hazard, _severity_symbol, DEFAULT_SOILCP, DEFAULT_SOILSAT, @@ -216,6 +217,8 @@ def _evaluate(crop: str, lat: float, lon: float, hazards.update(water_balance_hazards(stats)) # NTx35 / NTx40 heat-stress severity (crop-specific bands) hazards.update(heat_stress_hazards(stats, crop)) + # NDD dry-days severity + hazards.update(dry_days_hazard(stats)) length = (datetime.fromisoformat(w['end']) - datetime.fromisoformat(w['start'])).days return { @@ -286,6 +289,8 @@ def _avg_hazards(crop: str, agg: Dict) -> Dict: out.update(water_balance_hazards(agg)) # NTx35 / NTx40 heat-stress severity on the ensemble-mean day counts out.update(heat_stress_hazards(agg, crop)) + # NDD dry-days severity on the ensemble-mean day counts + out.update(dry_days_hazard(agg)) return out def _agg_hazard_statuses(bucket: List[Dict]) -> Dict: @@ -295,7 +300,7 @@ def _agg_hazard_statuses(bucket: List[Dict]) -> Dict: """ from collections import Counter indicators = ['precipitation', 'temperature', 'water_stress', 'water_logging', - 'heat_stress', 'extreme_heat_stress'] + 'heat_stress', 'extreme_heat_stress', 'dry_days'] out = {} for ind in indicators: statuses = [ @@ -632,6 +637,10 @@ def _print_block(a: Dict, crop: str, lat: float, lon: float, ehs = h['extreme_heat_stress'] print(f" {'Extreme Heat (NTx40)':<25} {s.get('NTx40', 0):>16.2f} d " f"[{_severity_symbol(ehs['status'])}] {ehs['status'].replace('_', ' ').upper()}") + if 'dry_days' in h: + dd = h['dry_days'] + print(f" {'Dry Days (NDD)':<25} {s.get('NDD', 0):>16.2f} d " + f"[{_severity_symbol(dd['status'])}] {dd['status'].replace('_', ' ').upper()}") print(f"\n{'='*70}") def print_results(r: Dict) -> None: @@ -699,6 +708,9 @@ def print_results(r: Dict) -> None: if 'extreme_heat_stress' in h: print(f" NTx40 status: [{_severity_symbol(h['extreme_heat_stress']['status'])}] " f"{h['extreme_heat_stress']['status'].replace('_', ' ').upper()}") + if 'dry_days' in h: + print(f" NDD status: [{_severity_symbol(h['dry_days']['status'])}] " + f"{h['dry_days']['status'].replace('_', ' ').upper()}") print(f"\n{'='*70}\n") # CLI diff --git a/climate_tookit/calculate_hazards/hazards.py b/climate_tookit/calculate_hazards/hazards.py index 62b4e65..3432959 100644 --- a/climate_tookit/calculate_hazards/hazards.py +++ b/climate_tookit/calculate_hazards/hazards.py @@ -284,6 +284,8 @@ def evaluate_threshold(value: float, thresholds: Dict[str, Tuple]) -> str: # Water-balance hazard severity classes (unit: days), per the Adaptation Atlas NDWS_SEVERITY = (15, 20, 25) NDWL0_SEVERITY = (2, 5, 8) +# Dry-days (NDD) severity class (unit: days), generic per the AE-project wiki +NDD_SEVERITY = (15, 20, 25) def classify_water_hazard(value: float, bounds: Tuple[int, int, int]) -> str: """Map a day count to a severity class using the Atlas water-balance bands.""" @@ -315,6 +317,18 @@ def water_balance_hazards(stats: Dict[str, Any]) -> Dict[str, Any]: } return out +def dry_days_hazard(stats: Dict[str, Any]) -> Dict[str, Any]: + """Build an NDD (dry-days) severity entry from season statistics.""" + out: Dict[str, Any] = {} + if stats.get('NDD') is not None: + v = stats['NDD'] + out['dry_days'] = { + 'index': 'NDD', + 'value_days': round(float(v), 2), + 'status': classify_water_hazard(v, NDD_SEVERITY), + } + return out + # Heat-stress severity classes (unit: days per season), per the AE-project # crop-specific hazard thresholds wiki. Bands give (moderate, severe, extreme) # floors; values below the moderate floor are no_significant_stress. @@ -474,6 +488,9 @@ def compute_ltm_baseline( } # NDWS / NDWL0 water-balance severity on the LTM means hazard_eval.update(water_balance_hazards(agg)) + # NTx35 / NTx40 heat-stress (crop-specific) and NDD dry-days on LTM means + hazard_eval.update(heat_stress_hazards(agg, crop_name)) + hazard_eval.update(dry_days_hazard(agg)) ltm_blocks.append({ 'season_number': sn, @@ -642,6 +659,9 @@ def calculate_hazards( } # NDWS / NDWL0 water-balance severity (Adaptation Atlas classes) hazard_eval.update(water_balance_hazards(stats)) + # NTx35 / NTx40 heat-stress severity (crop-specific) and NDD dry-days + hazard_eval.update(heat_stress_hazards(stats, crop_name)) + hazard_eval.update(dry_days_hazard(stats)) assessments.append({ 'crop': crop_name, 'location': {'latitude': lat, 'longitude': lon}, @@ -742,6 +762,18 @@ def _print_ltm_block(ltm: Dict[str, Any]) -> None: wl = h['water_logging'] sym = _severity_symbol(wl['status']) print(f" {'Water Logging (NDWL0)':<25} {wl['value_days']:>16.2f} d [{sym}] {wl['status'].replace('_', ' ').upper()}") + if 'heat_stress' in h: + hs = h['heat_stress'] + sym = _severity_symbol(hs['status']) + print(f" {'Heat Stress (NTx35)':<25} {hs['value_days']:>16.2f} d [{sym}] {hs['status'].replace('_', ' ').upper()}") + if 'extreme_heat_stress' in h: + ehs = h['extreme_heat_stress'] + sym = _severity_symbol(ehs['status']) + print(f" {'Extreme Heat (NTx40)':<25} {ehs['value_days']:>16.2f} d [{sym}] {ehs['status'].replace('_', ' ').upper()}") + if 'dry_days' in h: + dd = h['dry_days'] + sym = _severity_symbol(dd['status']) + print(f" {'Dry Days (NDD)':<25} {dd['value_days']:>16.2f} d [{sym}] {dd['status'].replace('_', ' ').upper()}") print(f"\n{'='*70}\n") def print_hazard_results(result: Dict[str, Any]) -> None: @@ -874,6 +906,18 @@ def print_hazard_results(result: Dict[str, Any]) -> None: wl = hazards['water_logging'] sym = _severity_symbol(wl['status']) print(f" {'Water Logging (NDWL0)':<25} {wl['value_days']:>16.2f} d [{sym}] {wl['status'].replace('_', ' ').upper()}") + if 'heat_stress' in hazards: + hs = hazards['heat_stress'] + sym = _severity_symbol(hs['status']) + print(f" {'Heat Stress (NTx35)':<25} {hs['value_days']:>16.2f} d [{sym}] {hs['status'].replace('_', ' ').upper()}") + if 'extreme_heat_stress' in hazards: + ehs = hazards['extreme_heat_stress'] + sym = _severity_symbol(ehs['status']) + print(f" {'Extreme Heat (NTx40)':<25} {ehs['value_days']:>16.2f} d [{sym}] {ehs['status'].replace('_', ' ').upper()}") + if 'dry_days' in hazards: + dd = hazards['dry_days'] + sym = _severity_symbol(dd['status']) + print(f" {'Dry Days (NDD)':<25} {dd['value_days']:>16.2f} d [{sym}] {dd['status'].replace('_', ' ').upper()}") print(f"\n{'='*70}\n")