Spaces:
Sleeping
Sleeping
File size: 12,224 Bytes
7e69b8f | 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 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 | # src/server/utils/description_builder.py
"""
Natural language scenario description builder for AquaGuard-RL.
Generates human-readable (and LLM-readable) summaries of the current
agricultural system state. These descriptions are included in every
observation as `scenario_description` to enable LLM-based agents to
understand the situation without parsing raw numerical data.
"""
from __future__ import annotations
import math
from typing import Dict, Optional
# Shannon entropy maximum for 6 crops (log(6) β 1.7918)
H_MAX = math.log(6)
def build_scenario_description(
season: str,
year: int,
zones: Dict,
crops: Dict,
food_ratio: float,
poverty_frac: float,
shannon: float,
rainfall_forecast: float,
task_name: str,
step_number: int = 0,
reward: Optional[float] = None,
) -> str:
"""
Build a comprehensive natural language description of the current simulation state.
The description is structured for LLM consumption:
1. Context header (season, year, task)
2. Groundwater status (per zone, danger indicators)
3. Food security status
4. Farmer welfare status
5. Crop portfolio summary
6. Climate forecast
7. Action guidance
Args:
season: Current season name (kharif/rabi/zaid).
year: Current year number.
zones: Dict of ZoneObservation objects.
crops: Dict of CropObservation objects.
food_ratio: Current food security ratio.
poverty_frac: Fraction of farmers below poverty line [0,1].
shannon: Shannon diversity index of current allocation.
rainfall_forecast: Forecast rainfall for this season in mm.
task_name: Active task name.
step_number: Current step number.
reward: Previous step's reward (None at reset).
Returns:
Multi-line natural language description (200β500 words).
"""
lines = []
# βββ Header βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
season_label = {"kharif": "Kharif (Monsoon, June-Oct)", "rabi": "Rabi (Winter, Oct-Mar)",
"zaid": "Zaid (Summer, Mar-Jun)"}.get(season, season.title())
lines.append(f"=== AquaGuard-RL: {_task_display(task_name)} ===")
lines.append(f"Season: {season_label} | Year {year} | Step {step_number}")
if reward is not None:
lines.append(f"Previous step reward: {reward:+.2f}")
lines.append("")
# βββ Groundwater Status βββββββββββββββββββββββββββββββββββββββββββββββββββ
lines.append("GROUNDWATER STATUS:")
avg_depth = sum(z.groundwater_depth_m for z in zones.values()) / len(zones)
any_danger = any(z.is_in_danger_zone for z in zones.values())
any_collapsed = any(z.is_collapsed for z in zones.values())
for zone_id, zone_obs in sorted(zones.items()):
depth = zone_obs.groundwater_depth_m
critical = zone_obs.groundwater_depth_m # use zone's actual depth for relative assessment
if zone_obs.is_collapsed:
status = "COLLAPSED (irreversible)"
elif zone_obs.is_in_danger_zone:
status = f"DANGER ZONE ({depth:.1f}m --near critical threshold)"
elif depth > 30:
status = f"Warning ({depth:.1f}m --approaching stress)"
elif depth > 20:
status = f"Moderate ({depth:.1f}m)"
else:
status = f"Healthy ({depth:.1f}m)"
lines.append(
f" {zone_id.upper()}: {status} | "
f"Soil fertility: {zone_obs.soil_fertility:.0%} | "
f"Salinity: {zone_obs.soil_salinity:.0%} | "
f"Irrigation: {zone_obs.active_irrigation_method}"
)
lines.append(f" Average aquifer depth: {avg_depth:.1f}m")
if any_collapsed:
lines.append(" [!] AQUIFER COLLAPSE DETECTED -- Permanent land degradation active")
elif any_danger:
lines.append(" [!] WARNING: One or more zones in danger zone -- reduce extraction immediately")
lines.append("")
# βββ Food Security ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
lines.append("FOOD SECURITY:")
if food_ratio >= 1.2:
food_status = f"Excellent surplus ({food_ratio:.2f}x requirement)"
elif food_ratio >= 1.0:
food_status = f"Meeting requirement ({food_ratio:.2f}x)"
elif food_ratio >= 0.85:
food_status = f"Mild shortfall ({food_ratio:.2f}x --{(1-food_ratio)*100:.0f}% deficit)"
elif food_ratio >= 0.7:
food_status = f"SERIOUS SHORTFALL ({food_ratio:.2f}x --emergency)"
else:
food_status = f"FAMINE RISK ({food_ratio:.2f}x --critical)"
lines.append(f" Food production ratio: {food_status}")
lines.append("")
# βββ Farmer Welfare βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
lines.append("FARMER WELFARE:")
poverty_pct = poverty_frac * 100
if poverty_pct < 20:
welfare_status = f"Good ({poverty_pct:.0f}% below poverty)"
elif poverty_pct < 35:
welfare_status = f"Moderate ({poverty_pct:.0f}% below poverty --attention needed)"
elif poverty_pct < 55:
welfare_status = f"Poor ({poverty_pct:.0f}% in poverty --intervention required)"
else:
welfare_status = f"CRISIS ({poverty_pct:.0f}% in poverty --social instability risk)"
lines.append(f" Farmer welfare: {welfare_status}")
lines.append("")
# βββ Current Crop Portfolio βββββββββββββββββββββββββββββββββββββββββββββββ
lines.append("CURRENT CROP PORTFOLIO:")
sorted_crops = sorted(crops.items(), key=lambda x: x[1].allocated_fraction, reverse=True)
for crop_id, crop_obs in sorted_crops:
if crop_obs.allocated_fraction < 0.01:
continue
alloc_pct = crop_obs.allocated_fraction * 100
yield_str = f"{crop_obs.actual_yield_t_per_ha:.1f} t/ha"
msp_str = f"INR {crop_obs.msp_price_inr_per_ton:,.0f}/t"
lines.append(
f" {crop_id.title():<12}: {alloc_pct:.0f}% land | "
f"Yield: {yield_str} | MSP: {msp_str} | "
f"Water req: {crop_obs.water_requirement_mm:.0f}mm"
)
diversity_pct = shannon / H_MAX * 100
diversity_label = _diversity_label(shannon)
lines.append(f" Shannon diversity: {shannon:.3f} ({diversity_pct:.0f}% of maximum) --{diversity_label}")
lines.append("")
# βββ Climate Forecast βββββββββββββββββββββββββββββββββββββββββββββββββββββ
lines.append("CLIMATE FORECAST:")
season_normal = {"kharif": 800, "rabi": 120, "zaid": 30}.get(season, 300)
dev_pct = (rainfall_forecast - season_normal) / season_normal * 100
if dev_pct > 20:
rain_desc = f"above normal (+{dev_pct:.0f}%)"
elif dev_pct > -10:
rain_desc = "near normal"
elif dev_pct > -30:
rain_desc = f"below normal ({dev_pct:.0f}%)"
else:
rain_desc = f"SIGNIFICANTLY BELOW NORMAL ({dev_pct:.0f}%) --drought conditions"
lines.append(f" {season.title()} rainfall forecast: {rainfall_forecast:.0f}mm ({rain_desc})")
lines.append(f" Historical season average: {season_normal:.0f}mm")
lines.append("")
# βββ Strategic Context ββββββββββββββββββββββββββββββββββββββββββββββββββββ
lines.append(_build_strategic_context(task_name, avg_depth, food_ratio, poverty_frac, shannon, season))
return "\n".join(lines)
def _task_display(task_name: str) -> str:
"""Convert task name to display format."""
return {
"baseline": "Baseline Stability Challenge",
"crisis": "Aquifer Crisis Recovery",
"policy_shift": "Green Revolution Policy Shift",
"climate_shock": "El NiΓ±o Drought Management",
"multi_district": "Multi-District Equity Coordination",
}.get(task_name, task_name.replace("_", " ").title())
def _diversity_label(shannon: float) -> str:
"""Convert Shannon index to qualitative label."""
if shannon >= 1.5:
return "Excellent diversity"
elif shannon >= 1.0:
return "Good diversity"
elif shannon >= 0.6:
return "Moderate diversity"
elif shannon >= 0.3:
return "Low diversity --rice/wheat dominant"
else:
return "Near monoculture --very high water/pest risk"
def _build_strategic_context(
task_name: str,
avg_depth: float,
food_ratio: float,
poverty_frac: float,
shannon: float,
season: str,
) -> str:
"""
Build task-specific strategic guidance paragraph.
This helps LLM agents understand what they should optimize for.
"""
context_lines = ["STRATEGIC CONTEXT:"]
# Common observations
warnings = []
if avg_depth > 35:
warnings.append(f"Aquifer critically low ({avg_depth:.1f}m avg) --reduce rice/wheat, enable drip irrigation")
elif avg_depth > 28:
warnings.append(f"Aquifer under stress ({avg_depth:.1f}m avg) --monitor extraction closely")
if food_ratio < 0.9:
warnings.append(f"Food shortfall ({food_ratio:.2f}x) --maintain grain production, avoid crop cuts")
if poverty_frac > 0.4:
warnings.append(f"High poverty ({poverty_frac:.0%}) --subsidy increases for water-efficient crops recommended")
# Task-specific guidance
task_guidance = {
"baseline": (
"Goal: Maintain all zones below critical thresholds for 10 seasons. "
"Gradually shift from water-intensive rice/wheat toward millets and pulses. "
"Target: cumulative reward > 40, GW depth <= 38m throughout."
),
"crisis": (
"EMERGENCY: Zone C near critical. Immediate aquifer intervention required. "
"Slash rice allocation, deploy drip irrigation, reduce extraction limits. "
"Food security may need to be temporarily compromised to prevent collapse."
),
"policy_shift": (
"Goal: Achieve Shannon diversity >= 1.2 (from ~0.85 initial) over 8 seasons. "
"Transition speed is limited --max 8pp reduction per crop per season. "
"Use subsidy adjustments to make water-efficient crops economically attractive."
),
"climate_shock": (
"DROUGHT ACTIVE: Only 40% of normal rainfall expected. "
"Critical: do NOT over-extract groundwater to compensate --this causes collapse. "
"Prioritize drought-tolerant crops (millet, pulses). Reduce rice allocation urgently."
),
"multi_district": (
"Goal: Balance all 3 zones with equity constraint (poorest zone income >= 70% of richest). "
"Zone C needs extra protection --vulnerable dryland with stressed aquifer. "
"Use targeted subsidies for Zone C. 15 seasons available for gradual transition."
),
}
guidance = task_guidance.get(task_name, "Optimize all four objectives simultaneously.")
context_lines.append(f" Task: {guidance}")
if warnings:
context_lines.append(" Alerts:")
for w in warnings:
context_lines.append(f" - {w}")
context_lines.append(
" Action format: Specify crop_allocation (sum<=1.0), water_quotas, "
"irrigation_methods, extraction_limits, subsidy_adjustments, and justification."
)
return "\n".join(context_lines) |