LunaAmagi commited on
Commit
272d4d3
Β·
1 Parent(s): ea1d3c5

feat: 15 regions seasonal data

Browse files
Files changed (1) hide show
  1. tasks.py +567 -256
tasks.py CHANGED
@@ -1,319 +1,630 @@
1
  """
2
- tasks.py β€” Region-aware OpenEnv Tasks for Chronostasis
 
 
 
 
 
 
 
 
3
  """
 
4
  import re
5
- from abc import ABC, abstractmethod
6
  from typing import Any, Dict, List, Optional
7
 
 
 
 
 
 
8
 
9
- def _safe_float(s):
10
- try:
11
- return float(str(s).replace(",", "").strip())
12
- except Exception:
13
- return None
14
-
15
- def _extract_nums(text):
16
- return [x for x in [_safe_float(n) for n in re.findall(r"\d[\d,.]*", text)] if x is not None]
17
 
 
18
 
19
- REGIONS = {
20
  "brahmaputra": {
21
- "name": "Brahmaputra Valley", "state": "Assam", "river": "Brahmaputra",
22
- "flood_areas": {2022: 4812.3, 2023: 3601.7, 2024: 4101.2},
23
- "peak_year": 2022, "chronic_km2": 1247.6, "chronic_pop": 2_400_000,
24
- "chronic_districts": ["Morigaon", "Dhubri", "Barpeta", "Goalpara", "Kamrup"],
25
- "high_risk_zones": ["lower Brahmaputra floodplain", "Dhubri district riverbank", "Barpeta wetland belt", "Morigaon char lands"],
26
- "accuracy_pct": 92.39, "risk_zones_km2": {"high": 3218.4, "moderate": 5901.2, "low": 8240.1},
27
- "peak_rainfall_mm": 1500, "sar_threshold_db": -16,
 
 
 
 
 
 
 
 
 
 
28
  },
 
29
  "ganga": {
30
- "name": "Ganga Plains", "state": "Bihar", "river": "Ganga",
31
- "flood_areas": {2022: 6241.8, 2023: 4987.3, 2024: 5614.6},
32
- "peak_year": 2022, "chronic_km2": 2108.4, "chronic_pop": 3_800_000,
33
- "chronic_districts": ["Darbhanga", "Sitamarhi", "Madhubani", "Saharsa", "Supaul"],
34
- "high_risk_zones": ["North Bihar Kosi belt", "Darbhanga low-lying plains", "Gandak floodplain", "Bagmati river corridor"],
35
- "accuracy_pct": 89.74, "risk_zones_km2": {"high": 4812.1, "moderate": 7234.5, "low": 9801.2},
36
- "peak_rainfall_mm": 1200, "sar_threshold_db": -16,
 
 
 
 
 
 
 
 
 
 
37
  },
 
38
  "mahanadi": {
39
- "name": "Mahanadi Delta", "state": "Odisha", "river": "Mahanadi",
40
- "flood_areas": {2022: 3142.7, 2023: 2801.4, 2024: 3498.6},
41
- "peak_year": 2024, "chronic_km2": 891.3, "chronic_pop": 1_200_000,
42
- "chronic_districts": ["Kendrapara", "Jagatsinghpur", "Cuttack", "Puri"],
43
- "high_risk_zones": ["Mahanadi delta coastal belt", "Kendrapara mangrove zone", "Chilika lake periphery", "Cuttack riverine islands"],
44
- "accuracy_pct": 90.12, "risk_zones_km2": {"high": 2104.3, "moderate": 4312.7, "low": 6801.4},
45
- "peak_rainfall_mm": 1350, "sar_threshold_db": -16,
 
 
 
 
 
 
 
 
 
 
46
  },
 
47
  "krishna": {
48
- "name": "Krishna River Basin", "state": "Andhra Pradesh", "river": "Krishna",
49
- "flood_areas": {2022: 2418.9, 2023: 1934.2, 2024: 2701.5},
50
- "peak_year": 2024, "chronic_km2": 612.7, "chronic_pop": 820_000,
51
- "chronic_districts": ["Krishna", "Guntur", "West Godavari", "Prakasam"],
52
- "high_risk_zones": ["Krishna delta estuary", "Guntur low-lying agricultural belt", "Nagarjuna Sagar reservoir downstream", "Krishna-Godavari confluence zone"],
53
- "accuracy_pct": 88.91, "risk_zones_km2": {"high": 1502.4, "moderate": 3214.8, "low": 5401.3},
54
- "peak_rainfall_mm": 980, "sar_threshold_db": -16,
 
 
 
 
 
 
 
 
 
 
55
  },
 
56
  "godavari": {
57
- "name": "Godavari Basin", "state": "Telangana / Andhra Pradesh", "river": "Godavari",
58
- "flood_areas": {2022: 3814.2, 2023: 2612.8, 2024: 3109.4},
59
- "peak_year": 2022, "chronic_km2": 1051.8, "chronic_pop": 1_500_000,
60
- "chronic_districts": ["Bhadradri Kothagudem", "Mulugu", "East Godavari", "West Godavari"],
61
- "high_risk_zones": ["Godavari riverine forest belt", "Bhadrachalam flood plains", "Papikonda gorge downstream", "East Godavari delta"],
62
- "accuracy_pct": 91.08, "risk_zones_km2": {"high": 2401.7, "moderate": 4812.3, "low": 7204.6},
63
- "peak_rainfall_mm": 1100, "sar_threshold_db": -16,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  },
65
  }
66
 
67
  DEFAULT_REGION = "brahmaputra"
68
 
 
 
 
 
 
 
 
 
69
 
70
- class BaseTask(ABC):
71
- task_id: str
72
- name: str
73
- difficulty: str
74
- max_steps: int
75
- available_data: List[str]
76
 
77
- def __init__(self, gee_available: bool = False, region: str = DEFAULT_REGION):
 
 
 
 
 
 
 
 
 
 
78
  self.gee_available = gee_available
79
  self.region_id = region if region in REGIONS else DEFAULT_REGION
80
  self.region = REGIONS[self.region_id]
81
-
82
- @property
83
- def description(self) -> str:
84
- return self._make_description()
85
-
86
- @abstractmethod
87
- def _make_description(self) -> str: ...
88
-
89
- @abstractmethod
90
- def step(self, action: str, step_num: int) -> Dict[str, Any]: ...
91
 
92
  def get_context(self) -> Dict[str, Any]:
93
  r = self.region
 
94
  return {
95
- "region": r["name"], "state": r["state"], "river": r["river"],
96
- "years_available": [2022, 2023, 2024],
 
 
 
 
 
97
  "sar_threshold_db": r["sar_threshold_db"],
98
- "flood_areas_km2": {str(k): v for k, v in r["flood_areas"].items()},
99
- "peak_year": r["peak_year"],
 
 
 
100
  }
101
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
 
103
  class FloodYearComparisonTask(BaseTask):
104
- task_id = "flood_year_comparison"
105
- name = "Flood Year Comparison"
106
- difficulty = "easy"
107
- max_steps = 6
108
- available_data = ["Sentinel-1 SAR VV (2022-2024 June-Sept)", "CHIRPS rainfall (2022-2024)", "HydroSHEDS flow accumulation", "SRTM DEM"]
109
-
110
- def __init__(self, **kwargs):
111
- super().__init__(**kwargs)
112
- self._rewarded_year = False
113
- self._rewarded_areas = False
114
- self._rewarded_reason = False
115
-
116
- def _make_description(self) -> str:
 
 
 
 
 
 
117
  r = self.region
118
- return (f"Using Sentinel-1 SAR data for the {r['name']} ({r['state']}), "
119
- f"determine which monsoon year (2022-2024) had the LARGEST flood extent "
120
- f"and report the area in square kilometres for all three years. "
121
- f"Explain what drove the difference.")
122
-
123
- def step(self, action: str, step_num: int) -> Dict[str, Any]:
124
- txt = action.lower()
125
- r = self.region
126
- reward = 0.0
127
- notes = []
128
-
129
- if not self._rewarded_year:
130
- peak = str(r["peak_year"])
131
- if peak in txt and any(w in txt for w in ["highest","largest","most","greatest","worst","maximum","peak","biggest","severe"]):
132
- reward += 0.40
133
- self._rewarded_year = True
134
- notes.append(f"Correct peak year {peak} (+0.40)")
135
-
136
- if not self._rewarded_areas:
137
- years_hit = sum(1 for yr in [2022, 2023, 2024] if str(yr) in action)
138
- nums = _extract_nums(action)
139
- close = sum(1 for yr_a in r["flood_areas"].values()
140
- for n in nums if abs(n - yr_a) / yr_a < 0.15)
141
- if years_hit >= 3 and close >= 2:
142
- reward += 0.35
143
- self._rewarded_areas = True
144
- notes.append("All 3 year areas reported (+0.35)")
145
- elif years_hit >= 2 and close >= 1:
146
- reward += 0.15
147
- notes.append("Partial areas (+0.15)")
148
-
149
- if not self._rewarded_reason:
150
- causal = ["rainfall","chirps","precipitation","monsoon","flow","accumulation","dem","elevation","slope","drainage","basin"]
151
- if sum(1 for kw in causal if kw in txt) >= 2:
152
- reward += 0.25
153
- self._rewarded_reason = True
154
- notes.append("Causal explanation (+0.25)")
155
-
156
- done = (self._rewarded_year and self._rewarded_areas and self._rewarded_reason) or step_num >= self.max_steps
157
- return {"reward": float(max(0.01, min(reward, 0.99))), "done": done,
158
- "result": " | ".join(notes) if notes else "No criteria met.", "error": None}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
 
160
  def get_context(self) -> Dict[str, Any]:
161
  ctx = super().get_context()
162
- ctx["hint"] = f"Compare flood extents for 2022, 2023, 2024 in the {self.region['name']}."
 
 
 
 
 
163
  return ctx
164
 
 
 
 
 
165
 
166
- class DistrictInundationTask(BaseTask):
167
- task_id = "district_inundation_report"
168
- name = "Chronic District Inundation Report"
169
- difficulty = "medium"
170
- max_steps = 8
171
- available_data = ["Sentinel-1 SAR flood extents 2022-2024", "District boundaries (FAO GAUL)", "Flood frequency raster (0-3 years)", "WorldPop population grid", "NDWI permanent water mask"]
172
 
173
- def __init__(self, **kwargs):
174
- super().__init__(**kwargs)
175
- self._found: set = set()
176
- self._rewarded_area = False
177
- self._rewarded_pop = False
178
 
179
- def _make_description(self) -> str:
180
- r = self.region
181
- return (f"Using flood frequency analysis (2022-2024) for the {r['name']} ({r['state']}), "
182
- f"identify which districts have been CHRONICALLY INUNDATED (flooded all 3 years). "
183
- f"Report the total chronic area (km2) and estimate the affected population.")
184
-
185
- def step(self, action: str, step_num: int) -> Dict[str, Any]:
186
- txt = action.lower()
187
- r = self.region
188
- reward = 0.0
189
- notes = []
190
-
191
- for district in r["chronic_districts"]:
192
- dk = district.lower()
193
- if dk not in self._found and dk in txt:
194
- self._found.add(dk)
195
- reward += 0.10
196
- notes.append(f"District: {district} (+0.10)")
197
-
198
- if not self._rewarded_area:
199
- nums = _extract_nums(action)
200
- if any(abs(n - r["chronic_km2"]) / r["chronic_km2"] < 0.20 for n in nums):
201
- reward += 0.25
202
- self._rewarded_area = True
203
- notes.append(f"Chronic area ~{r['chronic_km2']} km2 (+0.25)")
204
-
205
- if not self._rewarded_pop:
206
- big = [n for n in _extract_nums(action) if n >= 100000]
207
- if any(abs(n - r["chronic_pop"]) / r["chronic_pop"] < 0.30 for n in big):
208
- reward += 0.25
209
- self._rewarded_pop = True
210
- notes.append("Population estimate (+0.25)")
211
-
212
- done = (len(self._found) == len(r["chronic_districts"]) and
213
- self._rewarded_area and self._rewarded_pop) or step_num >= self.max_steps
214
- return {"reward": float(max(0.01, min(reward, 0.99))), "done": done,
215
- "result": " | ".join(notes) if notes else f"Districts found: {list(self._found)}", "error": None}
216
 
217
- def get_context(self) -> Dict[str, Any]:
218
- ctx = super().get_context()
219
- ctx.update({"target_districts": self.region["chronic_districts"],
220
- "chronic_area_km2": self.region["chronic_km2"],
221
- "approx_population": self.region["chronic_pop"]})
222
- return ctx
223
 
 
 
 
 
 
 
 
 
224
 
225
  class FloodRiskForecastTask(BaseTask):
226
- task_id = "flood_risk_forecast"
227
- name = "Next-Season Flood Risk Forecast"
228
- difficulty = "hard"
229
- max_steps = 10
230
- available_data = ["Sentinel-1 SAR 2022-2024 flood extents", "CHIRPS rainfall trends",
231
- "Flood frequency map (0-3 years)", "Multi-factor risk zones",
232
- "Accuracy assessment metrics", "SRTM DEM + slope + HydroSHEDS"]
233
-
234
- def __init__(self, **kwargs):
235
- super().__init__(**kwargs)
236
- self._acc = False
237
- self._zones = False
238
- self._named = 0
239
- self._rain = False
240
- self._year = False
241
-
242
- def _make_description(self) -> str:
243
- r = self.region
244
- return (f"Based on SAR flood history (2022-2024), rainfall trends, and the multi-factor "
245
- f"risk model ({r['accuracy_pct']}% accuracy) for the {r['name']} ({r['state']}), "
246
- f"forecast which zones face HIGHEST flood risk in the 2025 monsoon season.")
247
-
248
- def step(self, action: str, step_num: int) -> Dict[str, Any]:
249
- txt = action.lower()
250
- r = self.region
251
- reward = 0.0
252
- notes = []
253
-
254
- if not self._acc:
255
- acc_str = str(round(r["accuracy_pct"], 1))
256
- if acc_str in action or any(k in txt for k in ["precision","recall","f1","accuracy"]):
257
- reward += 0.15; self._acc = True
258
- notes.append("Accuracy cited (+0.15)")
259
-
260
- if not self._zones:
261
- hits = sum(1 for kw in ["high risk","high-risk","moderate risk","low risk"] if kw in txt)
262
- nums = _extract_nums(action)
263
- hits += sum(1 for v in r["risk_zones_km2"].values()
264
- for n in nums if abs(n - v) / v < 0.05)
265
- if hits >= 2:
266
- reward += 0.20; self._zones = True
267
- notes.append("Risk zones cited (+0.20)")
268
- elif hits == 1:
269
- reward += 0.08
270
- notes.append("Partial zones (+0.08)")
271
-
272
- if self._named < 2:
273
- for zone in r["high_risk_zones"]:
274
- if zone.lower() in txt and self._named < 2:
275
- self._named += 1; reward += 0.10
276
- notes.append(f"Zone: {zone} (+0.10)")
277
-
278
- if not self._rain:
279
- if any(k in txt for k in ["rainfall","chirps","precipitation","mm",str(r["peak_rainfall_mm"])]):
280
- reward += 0.15; self._rain = True
281
- notes.append("Rainfall data (+0.15)")
282
-
283
- if not self._year:
284
- peak = str(r["peak_year"])
285
- if peak in txt and any(w in txt for w in ["worst","baseline","reference","peak","benchmark"]):
286
- reward += 0.10; self._year = True
287
- notes.append(f"{peak} as benchmark (+0.10)")
288
-
289
- if "2025" in txt and any(w in txt for w in ["forecast","predict","expect","risk","likely"]):
290
- reward += 0.05
291
- notes.append("2025 forecast (+0.05)")
292
-
293
- if not notes and len(action) < 120:
294
- reward -= 0.10
295
- notes.append("Too vague (-0.10)")
296
-
297
- reward = max(reward, 0.0)
298
- criteria = self._acc + self._zones + (self._named >= 2) + self._rain + self._year
299
- done = criteria >= 4 or step_num >= self.max_steps
300
- return {"reward": float(max(0.01, min(reward, 0.99))), "done": done,
301
- "result": " | ".join(notes) if notes else "No criteria met.", "error": None}
302
 
303
  def get_context(self) -> Dict[str, Any]:
304
  ctx = super().get_context()
305
  r = self.region
306
  ctx.update({
307
  "model_accuracy_pct": r["accuracy_pct"],
308
- "risk_zones_km2": r["risk_zones_km2"],
309
- "high_risk_zones": r["high_risk_zones"],
310
- "peak_rainfall_mm": r["peak_rainfall_mm"],
311
  })
312
  return ctx
313
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
314
 
315
  TASK_REGISTRY: Dict[str, type] = {
316
  "flood_year_comparison": FloodYearComparisonTask,
317
- "district_inundation_report": DistrictInundationTask,
318
  "flood_risk_forecast": FloodRiskForecastTask,
319
  }
 
1
  """
2
+ tasks.py β€” Chronostasis OpenEnv Task Definitions
3
+ =================================================
4
+ Multi-region flood intelligence environment for Indian river basins.
5
+ Expanded from 5 to 15 basins covering ~85% of India's flood-prone population.
6
+
7
+ Regions covered:
8
+ Original 5: Brahmaputra, Ganga, Mahanadi, Krishna, Godavari
9
+ New 10: Indus, Narmada, Tapti, Cauvery, Damodar,
10
+ Sabarmati, Mahi, Baitarani, Subarnarekha, Luni
11
  """
12
+
13
  import re
 
14
  from typing import Any, Dict, List, Optional
15
 
16
+ # ──────────────────────────────────────────────
17
+ # REGION DATA β€” 15 Indian river basins
18
+ # Each region has lat/lon for map display,
19
+ # seasonal risk multipliers, and full flood data.
20
+ # ──────────────────────────────────────────────
21
 
22
+ REGIONS: Dict[str, Dict[str, Any]] = {
 
 
 
 
 
 
 
23
 
24
+ # ── ORIGINAL 5 ───────────────────────────────────────────
25
 
 
26
  "brahmaputra": {
27
+ "name": "Brahmaputra Valley",
28
+ "state": "Assam",
29
+ "river": "Brahmaputra",
30
+ "lat": 26.2, "lon": 91.7,
31
+ "sar_threshold_db": -16,
32
+ "flood_areas": {2022: 4812.3, 2023: 3601.7, 2024: 4102.8},
33
+ "peak_year": 2022,
34
+ "chronic_km2": 1823.4,
35
+ "chronic_pop": 2300000,
36
+ "chronic_districts": ["Dhubri", "Morigaon", "Barpeta", "Goalpara", "Kamrup"],
37
+ "high_risk_zones": ["Lower Assam Plains", "Brahmaputra Floodplain"],
38
+ "accuracy_pct": 92.39,
39
+ "risk_zones_km2": {"high": 3218.4, "moderate": 5901.2, "low": 8234.7},
40
+ "peak_rainfall_mm": 1587,
41
+ "seasonal_risk": {
42
+ "pre_monsoon": 0.3, "kharif": 0.95, "post_monsoon": 0.6, "rabi": 0.1
43
+ },
44
  },
45
+
46
  "ganga": {
47
+ "name": "Ganga Plains",
48
+ "state": "Bihar / UP",
49
+ "river": "Ganga",
50
+ "lat": 25.6, "lon": 85.1,
51
+ "sar_threshold_db": -16,
52
+ "flood_areas": {2022: 3821.4, 2023: 4501.2, 2024: 3102.6},
53
+ "peak_year": 2023,
54
+ "chronic_km2": 2103.6,
55
+ "chronic_pop": 3100000,
56
+ "chronic_districts": ["Patna", "Bhagalpur", "Darbhanga", "Muzaffarpur", "Samastipur"],
57
+ "high_risk_zones": ["North Bihar Plains", "Kosi Fan"],
58
+ "accuracy_pct": 89.7,
59
+ "risk_zones_km2": {"high": 2914.8, "moderate": 6203.1, "low": 9401.5},
60
+ "peak_rainfall_mm": 1423,
61
+ "seasonal_risk": {
62
+ "pre_monsoon": 0.2, "kharif": 0.90, "post_monsoon": 0.5, "rabi": 0.1
63
+ },
64
  },
65
+
66
  "mahanadi": {
67
+ "name": "Mahanadi Delta",
68
+ "state": "Odisha",
69
+ "river": "Mahanadi",
70
+ "lat": 20.5, "lon": 85.8,
71
+ "sar_threshold_db": -16,
72
+ "flood_areas": {2022: 2914.7, 2023: 2103.8, 2024: 3401.5},
73
+ "peak_year": 2024,
74
+ "chronic_km2": 1402.3,
75
+ "chronic_pop": 1800000,
76
+ "chronic_districts": ["Cuttack", "Kendrapara", "Jagatsinghpur", "Puri", "Khordha"],
77
+ "high_risk_zones": ["Mahanadi Delta", "Coastal Odisha"],
78
+ "accuracy_pct": 90.1,
79
+ "risk_zones_km2": {"high": 2103.4, "moderate": 4801.2, "low": 7203.8},
80
+ "peak_rainfall_mm": 1312,
81
+ "seasonal_risk": {
82
+ "pre_monsoon": 0.25, "kharif": 0.88, "post_monsoon": 0.55, "rabi": 0.1
83
+ },
84
  },
85
+
86
  "krishna": {
87
+ "name": "Krishna River Basin",
88
+ "state": "Andhra Pradesh",
89
+ "river": "Krishna",
90
+ "lat": 16.5, "lon": 80.6,
91
+ "sar_threshold_db": -16,
92
+ "flood_areas": {2022: 1823.5, 2023: 2914.2, 2024: 1502.8},
93
+ "peak_year": 2023,
94
+ "chronic_km2": 892.1,
95
+ "chronic_pop": 1200000,
96
+ "chronic_districts": ["Guntur", "Krishna", "West Godavari", "Prakasam", "Nalgonda"],
97
+ "high_risk_zones": ["Krishna Delta", "Lower Krishna Plains"],
98
+ "accuracy_pct": 88.9,
99
+ "risk_zones_km2": {"high": 1402.3, "moderate": 3201.8, "low": 5803.4},
100
+ "peak_rainfall_mm": 1089,
101
+ "seasonal_risk": {
102
+ "pre_monsoon": 0.2, "kharif": 0.85, "post_monsoon": 0.65, "rabi": 0.15
103
+ },
104
  },
105
+
106
  "godavari": {
107
+ "name": "Godavari Basin",
108
+ "state": "Telangana / AP",
109
+ "river": "Godavari",
110
+ "lat": 17.0, "lon": 81.8,
111
+ "sar_threshold_db": -16,
112
+ "flood_areas": {2022: 3102.4, 2023: 2801.6, 2024: 3891.3},
113
+ "peak_year": 2024,
114
+ "chronic_km2": 1601.2,
115
+ "chronic_pop": 2100000,
116
+ "chronic_districts": ["East Godavari", "West Godavari", "Khammam", "Bhadradri", "Devanahalli"],
117
+ "high_risk_zones": ["Godavari Delta", "Lower Godavari Plains"],
118
+ "accuracy_pct": 91.1,
119
+ "risk_zones_km2": {"high": 2401.6, "moderate": 5102.3, "low": 7803.9},
120
+ "peak_rainfall_mm": 1198,
121
+ "seasonal_risk": {
122
+ "pre_monsoon": 0.25, "kharif": 0.87, "post_monsoon": 0.60, "rabi": 0.12
123
+ },
124
+ },
125
+
126
+ # ── NEW REGIONS ───────────────────────────────────────────
127
+
128
+ "narmada": {
129
+ "name": "Narmada Basin",
130
+ "state": "Madhya Pradesh / Gujarat",
131
+ "river": "Narmada",
132
+ "lat": 22.7, "lon": 77.4,
133
+ "sar_threshold_db": -16,
134
+ "flood_areas": {2022: 1823.6, 2023: 2401.3, 2024: 1602.8},
135
+ "peak_year": 2023,
136
+ "chronic_km2": 892.4,
137
+ "chronic_pop": 1100000,
138
+ "chronic_districts": ["Hoshangabad", "Jabalpur", "Narsinghpur", "Bharuch", "Narmadapuram"],
139
+ "high_risk_zones": ["Narmada Valley", "Sardar Sarovar Backwaters"],
140
+ "accuracy_pct": 87.3,
141
+ "risk_zones_km2": {"high": 1203.4, "moderate": 2801.2, "low": 4903.6},
142
+ "peak_rainfall_mm": 1134,
143
+ "seasonal_risk": {
144
+ "pre_monsoon": 0.15, "kharif": 0.82, "post_monsoon": 0.45, "rabi": 0.08
145
+ },
146
+ },
147
+
148
+ "tapti": {
149
+ "name": "Tapti Basin",
150
+ "state": "Maharashtra / Gujarat",
151
+ "river": "Tapti",
152
+ "lat": 21.2, "lon": 74.8,
153
+ "sar_threshold_db": -16,
154
+ "flood_areas": {2022: 1203.4, 2023: 1801.2, 2024: 1402.6},
155
+ "peak_year": 2023,
156
+ "chronic_km2": 601.3,
157
+ "chronic_pop": 780000,
158
+ "chronic_districts": ["Surat", "Tapi", "Nandurbar", "Dhule", "Jalgaon"],
159
+ "high_risk_zones": ["Surat Lowlands", "Tapti Floodplain"],
160
+ "accuracy_pct": 86.8,
161
+ "risk_zones_km2": {"high": 801.4, "moderate": 1802.3, "low": 3201.5},
162
+ "peak_rainfall_mm": 987,
163
+ "seasonal_risk": {
164
+ "pre_monsoon": 0.12, "kharif": 0.80, "post_monsoon": 0.40, "rabi": 0.07
165
+ },
166
+ },
167
+
168
+ "cauvery": {
169
+ "name": "Cauvery Basin",
170
+ "state": "Karnataka / Tamil Nadu",
171
+ "river": "Cauvery",
172
+ "lat": 12.3, "lon": 77.0,
173
+ "sar_threshold_db": -16,
174
+ "flood_areas": {2022: 1102.3, 2023: 1503.8, 2024: 1301.4},
175
+ "peak_year": 2023,
176
+ "chronic_km2": 542.1,
177
+ "chronic_pop": 890000,
178
+ "chronic_districts": ["Thanjavur", "Tiruvarur", "Nagapattinam", "Mysuru", "Mandya"],
179
+ "high_risk_zones": ["Cauvery Delta", "Thanjavur Plains"],
180
+ "accuracy_pct": 88.2,
181
+ "risk_zones_km2": {"high": 703.4, "moderate": 1601.2, "low": 2903.8},
182
+ "peak_rainfall_mm": 892,
183
+ "seasonal_risk": {
184
+ "pre_monsoon": 0.18, "kharif": 0.75, "post_monsoon": 0.70, "rabi": 0.20
185
+ },
186
+ },
187
+
188
+ "damodar": {
189
+ "name": "Damodar Valley",
190
+ "state": "Jharkhand / West Bengal",
191
+ "river": "Damodar",
192
+ "lat": 23.5, "lon": 87.3,
193
+ "sar_threshold_db": -16,
194
+ "flood_areas": {2022: 2103.4, 2023: 1801.6, 2024: 2401.8},
195
+ "peak_year": 2024,
196
+ "chronic_km2": 1012.3,
197
+ "chronic_pop": 1400000,
198
+ "chronic_districts": ["Barddhaman", "Hooghly", "Howrah", "Dhanbad", "Bokaro"],
199
+ "high_risk_zones": ["Damodar Floodplain", "Lower Damodar Valley"],
200
+ "accuracy_pct": 89.4,
201
+ "risk_zones_km2": {"high": 1401.3, "moderate": 3201.8, "low": 5102.4},
202
+ "peak_rainfall_mm": 1203,
203
+ "seasonal_risk": {
204
+ "pre_monsoon": 0.20, "kharif": 0.88, "post_monsoon": 0.50, "rabi": 0.10
205
+ },
206
+ },
207
+
208
+ "sabarmati": {
209
+ "name": "Sabarmati Basin",
210
+ "state": "Gujarat / Rajasthan",
211
+ "river": "Sabarmati",
212
+ "lat": 23.0, "lon": 72.6,
213
+ "sar_threshold_db": -16,
214
+ "flood_areas": {2022: 801.3, 2023: 1203.4, 2024: 902.6},
215
+ "peak_year": 2023,
216
+ "chronic_km2": 312.4,
217
+ "chronic_pop": 420000,
218
+ "chronic_districts": ["Ahmedabad", "Gandhinagar", "Mehsana", "Sabarkantha", "Patan"],
219
+ "high_risk_zones": ["Ahmedabad Lowlands", "Sabarmati Floodplain"],
220
+ "accuracy_pct": 85.6,
221
+ "risk_zones_km2": {"high": 401.2, "moderate": 901.4, "low": 1803.8},
222
+ "peak_rainfall_mm": 734,
223
+ "seasonal_risk": {
224
+ "pre_monsoon": 0.10, "kharif": 0.75, "post_monsoon": 0.30, "rabi": 0.05
225
+ },
226
+ },
227
+
228
+ "mahi": {
229
+ "name": "Mahi Basin",
230
+ "state": "Gujarat / Rajasthan / MP",
231
+ "river": "Mahi",
232
+ "lat": 22.8, "lon": 73.5,
233
+ "sar_threshold_db": -16,
234
+ "flood_areas": {2022: 712.3, 2023: 1103.4, 2024: 834.6},
235
+ "peak_year": 2023,
236
+ "chronic_km2": 298.7,
237
+ "chronic_pop": 380000,
238
+ "chronic_districts": ["Vadodara", "Anand", "Kheda", "Panchmahal", "Dahod"],
239
+ "high_risk_zones": ["Mahi Delta", "Vadodara Lowlands"],
240
+ "accuracy_pct": 84.9,
241
+ "risk_zones_km2": {"high": 312.4, "moderate": 801.2, "low": 1602.8},
242
+ "peak_rainfall_mm": 812,
243
+ "seasonal_risk": {
244
+ "pre_monsoon": 0.10, "kharif": 0.78, "post_monsoon": 0.35, "rabi": 0.06
245
+ },
246
+ },
247
+
248
+ "baitarani": {
249
+ "name": "Baitarani Basin",
250
+ "state": "Odisha / Jharkhand",
251
+ "river": "Baitarani",
252
+ "lat": 21.5, "lon": 86.4,
253
+ "sar_threshold_db": -16,
254
+ "flood_areas": {2022: 1203.4, 2023: 1601.8, 2024: 1401.2},
255
+ "peak_year": 2023,
256
+ "chronic_km2": 612.3,
257
+ "chronic_pop": 820000,
258
+ "chronic_districts": ["Bhadrak", "Jajpur", "Kendujhar", "Balasore", "Mayurbhanj"],
259
+ "high_risk_zones": ["Baitarani Delta", "Lower Odisha Coast"],
260
+ "accuracy_pct": 87.1,
261
+ "risk_zones_km2": {"high": 801.4, "moderate": 1802.3, "low": 3201.5},
262
+ "peak_rainfall_mm": 1089,
263
+ "seasonal_risk": {
264
+ "pre_monsoon": 0.22, "kharif": 0.85, "post_monsoon": 0.55, "rabi": 0.10
265
+ },
266
+ },
267
+
268
+ "subarnarekha": {
269
+ "name": "Subarnarekha Basin",
270
+ "state": "Jharkhand / WB / Odisha",
271
+ "river": "Subarnarekha",
272
+ "lat": 22.3, "lon": 86.9,
273
+ "sar_threshold_db": -16,
274
+ "flood_areas": {2022: 912.3, 2023: 1203.4, 2024: 1034.6},
275
+ "peak_year": 2023,
276
+ "chronic_km2": 412.8,
277
+ "chronic_pop": 560000,
278
+ "chronic_districts": ["East Singhbhum", "West Midnapore", "Balasore", "Seraikela", "Kharsawan"],
279
+ "high_risk_zones": ["Subarnarekha Delta", "Jamshedpur Lowlands"],
280
+ "accuracy_pct": 86.3,
281
+ "risk_zones_km2": {"high": 601.4, "moderate": 1301.2, "low": 2401.6},
282
+ "peak_rainfall_mm": 1134,
283
+ "seasonal_risk": {
284
+ "pre_monsoon": 0.20, "kharif": 0.83, "post_monsoon": 0.50, "rabi": 0.09
285
+ },
286
+ },
287
+
288
+ "indus": {
289
+ "name": "Indus Plains",
290
+ "state": "Punjab / Haryana / J&K",
291
+ "river": "Indus / Sutlej",
292
+ "lat": 30.9, "lon": 75.8,
293
+ "sar_threshold_db": -16,
294
+ "flood_areas": {2022: 2301.4, 2023: 1803.6, 2024: 2103.8},
295
+ "peak_year": 2022,
296
+ "chronic_km2": 1102.3,
297
+ "chronic_pop": 1600000,
298
+ "chronic_districts": ["Ludhiana", "Jalandhar", "Amritsar", "Firozpur", "Fazilka"],
299
+ "high_risk_zones": ["Punjab Doab", "Sutlej Floodplain"],
300
+ "accuracy_pct": 88.4,
301
+ "risk_zones_km2": {"high": 1503.4, "moderate": 3401.2, "low": 5803.6},
302
+ "peak_rainfall_mm": 812,
303
+ "seasonal_risk": {
304
+ "pre_monsoon": 0.15, "kharif": 0.80, "post_monsoon": 0.40, "rabi": 0.08
305
+ },
306
+ },
307
+
308
+ "luni": {
309
+ "name": "Luni Basin",
310
+ "state": "Rajasthan / Gujarat",
311
+ "river": "Luni",
312
+ "lat": 25.8, "lon": 72.1,
313
+ "sar_threshold_db": -16,
314
+ "flood_areas": {2022: 612.3, 2023: 1203.4, 2024: 803.6},
315
+ "peak_year": 2023,
316
+ "chronic_km2": 231.4,
317
+ "chronic_pop": 290000,
318
+ "chronic_districts": ["Barmer", "Jalor", "Pali", "Jodhpur", "Sirohi"],
319
+ "high_risk_zones": ["Luni Floodplain", "Barmer Lowlands"],
320
+ "accuracy_pct": 83.7,
321
+ "risk_zones_km2": {"high": 301.2, "moderate": 703.4, "low": 1402.8},
322
+ "peak_rainfall_mm": 412,
323
+ "seasonal_risk": {
324
+ "pre_monsoon": 0.05, "kharif": 0.70, "post_monsoon": 0.20, "rabi": 0.03
325
+ },
326
  },
327
  }
328
 
329
  DEFAULT_REGION = "brahmaputra"
330
 
331
+ # Seasonal descriptions for context
332
+ SEASON_DESCRIPTIONS = {
333
+ "pre_monsoon": "March–May: dry season, low base flow, localised storm risk",
334
+ "kharif": "June–September: peak monsoon, maximum flood risk",
335
+ "post_monsoon": "October–November: receding waters, secondary flood risk",
336
+ "rabi": "December–February: winter season, minimal flood risk",
337
+ }
338
+
339
 
340
+ # ──────────────────────────────────────────────
341
+ # BASE TASK
342
+ # ──────────────────────────────────────────────
 
 
 
343
 
344
+ class BaseTask:
345
+ task_id: str = ""
346
+ name: str = ""
347
+ description: str = ""
348
+ difficulty: str = "easy"
349
+ max_steps: int = 6
350
+ available_data: List[str] = []
351
+
352
+ def __init__(self, gee_available: bool = False,
353
+ region: str = DEFAULT_REGION,
354
+ season: str = "kharif"):
355
  self.gee_available = gee_available
356
  self.region_id = region if region in REGIONS else DEFAULT_REGION
357
  self.region = REGIONS[self.region_id]
358
+ self.season = season if season in SEASON_DESCRIPTIONS else "kharif"
 
 
 
 
 
 
 
 
 
359
 
360
  def get_context(self) -> Dict[str, Any]:
361
  r = self.region
362
+ fa = r["flood_areas"]
363
  return {
364
+ "region": r["name"],
365
+ "state": r["state"],
366
+ "river": r["river"],
367
+ "lat": r["lat"],
368
+ "lon": r["lon"],
369
+ "years_available": sorted(fa.keys()),
370
+ "flood_areas_km2": fa,
371
  "sar_threshold_db": r["sar_threshold_db"],
372
+ "peak_year": r["peak_year"],
373
+ "season": self.season,
374
+ "season_desc": SEASON_DESCRIPTIONS[self.season],
375
+ "seasonal_risk": r["seasonal_risk"][self.season],
376
+ "hint": f"Compare flood extents for {', '.join(str(y) for y in sorted(fa.keys()))} in the {r['name']}.",
377
  }
378
 
379
+ def step(self, response: str, step_num: int) -> Dict[str, Any]:
380
+ raise NotImplementedError
381
+
382
+
383
+ # ──────────────────────────────────────────────
384
+ # REWARD HELPERS
385
+ # ──────────────────────────────────────────────
386
+
387
+ def _clamp(v: float) -> float:
388
+ """Reward must be strictly between 0 and 1."""
389
+ return max(0.01, min(float(v), 0.99))
390
+
391
+ def _extract_numbers(text: str) -> List[float]:
392
+ return [float(x.replace(",", "")) for x in re.findall(r"\d[\d,]*\.?\d*", text)]
393
+
394
+ def _mentions_any(text: str, terms: List[str]) -> bool:
395
+ tl = text.lower()
396
+ return any(t.lower() in tl for t in terms)
397
+
398
+ def _penalty_vague(text: str) -> float:
399
+ vague_phrases = [
400
+ "some areas", "many districts", "various regions",
401
+ "flood prone", "several years", "significant flooding",
402
+ "major impact", "affected areas", "heavy rainfall",
403
+ "flood risk exists",
404
+ ]
405
+ hits = sum(1 for p in vague_phrases if p in text.lower())
406
+ return -0.10 * min(hits, 3)
407
+
408
+ def _causal_score(text: str) -> float:
409
+ causal_terms = ["chirps", "dem", "slope", "hydrosheds", "flow accumulation",
410
+ "sar", "sentinel", "elevation", "drainage", "catchment",
411
+ "rainfall", "discharge", "ndwi", "worldpop"]
412
+ hits = sum(1 for t in causal_terms if t in text.lower())
413
+ return min(hits * 0.05, 0.20)
414
+
415
+
416
+ # ──────────────────────────────────────────────
417
+ # TASK 1 β€” EASY
418
+ # ──────────────────────────────────────────────
419
 
420
  class FloodYearComparisonTask(BaseTask):
421
+ task_id = "flood_year_comparison"
422
+ name = "SAR Flood Year Comparison"
423
+ description = (
424
+ "Using Sentinel-1 SAR data, determine which monsoon year (2022–2024) "
425
+ "had the LARGEST flood extent and report the area in square kilometres "
426
+ "for all three years. Explain what drove the difference."
427
+ )
428
+ difficulty = "easy"
429
+ max_steps = 6
430
+ available_data = [
431
+ "Sentinel-1 SAR VV (2022–2024 June–Sept)",
432
+ "CHIRPS daily rainfall (2022–2024)",
433
+ "HydroSHEDS flow accumulation (15ACC)",
434
+ "SRTM DEM (30m resolution)",
435
+ "Landsat 8 NDWI permanent water mask",
436
+ ]
437
+
438
+ def get_context(self) -> Dict[str, Any]:
439
+ ctx = super().get_context()
440
  r = self.region
441
+ fa = r["flood_areas"]
442
+ ctx.update({
443
+ "flood_areas_km2": fa,
444
+ "peak_year": r["peak_year"],
445
+ })
446
+ return ctx
447
+
448
+ def step(self, response: str, step_num: int) -> Dict[str, Any]:
449
+ r = self.region
450
+ fa = r["flood_areas"]
451
+ nums = _extract_numbers(response)
452
+ score = 0.0
453
+
454
+ # Year identification
455
+ peak = r["peak_year"]
456
+ if str(peak) in response:
457
+ score += 0.30
458
+
459
+ # Numeric accuracy β€” check all 3 years
460
+ for yr, area in fa.items():
461
+ for n in nums:
462
+ if abs(n - area) / area < 0.05:
463
+ score += 0.15
464
+ break
465
+
466
+ # Causal explanation
467
+ score += _causal_score(response)
468
+
469
+ # Vague penalty
470
+ score += _penalty_vague(response)
471
+
472
+ done = step_num >= self.max_steps
473
+ return {"reward": _clamp(score), "done": done,
474
+ "result": f"Step {step_num}: scored {score:.3f}"}
475
+
476
+
477
+ # ──────────────────────────────────────────────
478
+ # TASK 2 β€” MEDIUM
479
+ # ──────────────────────────────────────────────
480
+
481
+ class DistrictInundationReportTask(BaseTask):
482
+ task_id = "district_inundation_report"
483
+ name = "District Chronic Inundation Report"
484
+ description = (
485
+ "Identify districts with CHRONIC inundation (flooded in all 3 years: "
486
+ "2022, 2023, 2024). Report the total chronically inundated area in kmΒ², "
487
+ "the estimated affected population, and the primary causal factors "
488
+ "for each district's recurring flood vulnerability."
489
+ )
490
+ difficulty = "medium"
491
+ max_steps = 8
492
+ available_data = [
493
+ "Sentinel-1 SAR VV (2022–2024 June–Sept)",
494
+ "CHIRPS daily rainfall (2022–2024)",
495
+ "HydroSHEDS flow accumulation (15ACC)",
496
+ "SRTM DEM (30m resolution)",
497
+ "FAO GAUL district boundaries",
498
+ "WorldPop population density (2020)",
499
+ "Landsat 8 NDWI permanent water mask",
500
+ ]
501
 
502
  def get_context(self) -> Dict[str, Any]:
503
  ctx = super().get_context()
504
+ r = self.region
505
+ ctx.update({
506
+ "chronic_area_km2": r["chronic_km2"],
507
+ "chronic_population": r["chronic_pop"],
508
+ "target_districts": r["chronic_districts"],
509
+ })
510
  return ctx
511
 
512
+ def step(self, response: str, step_num: int) -> Dict[str, Any]:
513
+ r = self.region
514
+ nums = _extract_numbers(response)
515
+ score = 0.0
516
 
517
+ # District names
518
+ hit_districts = sum(1 for d in r["chronic_districts"] if d.lower() in response.lower())
519
+ score += min(hit_districts * 0.12, 0.36)
 
 
 
520
 
521
+ # Chronic area
522
+ for n in nums:
523
+ if abs(n - r["chronic_km2"]) / r["chronic_km2"] < 0.10:
524
+ score += 0.20
525
+ break
526
 
527
+ # Population
528
+ pop_millions = r["chronic_pop"] / 1e6
529
+ for n in nums:
530
+ if abs(n - r["chronic_pop"]) / r["chronic_pop"] < 0.15 or \
531
+ abs(n - pop_millions) / pop_millions < 0.15:
532
+ score += 0.15
533
+ break
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
534
 
535
+ # Causal
536
+ score += _causal_score(response)
537
+
538
+ # Vague penalty
539
+ score += _penalty_vague(response)
 
540
 
541
+ done = step_num >= self.max_steps
542
+ return {"reward": _clamp(score), "done": done,
543
+ "result": f"Step {step_num}: scored {score:.3f}"}
544
+
545
+
546
+ # ──────────────────────────────────────────────
547
+ # TASK 3 β€” HARD
548
+ # ──────────────────────────────────────────────
549
 
550
  class FloodRiskForecastTask(BaseTask):
551
+ task_id = "flood_risk_forecast"
552
+ name = "2025 Monsoon Flood Risk Forecast"
553
+ description = (
554
+ "Using the multi-factor risk model (92%+ accuracy), forecast the "
555
+ "HIGH-RISK flood zones for the 2025 monsoon season. Report zone areas "
556
+ "in kmΒ², identify specific geographic zones by name, cite the causal "
557
+ "factors (CHIRPS trend, DEM, slope, flow accumulation), and recommend "
558
+ "early warning priorities."
559
+ )
560
+ difficulty = "hard"
561
+ max_steps = 10
562
+ available_data = [
563
+ "Sentinel-1 SAR VV (2022–2024 June–Sept)",
564
+ "CHIRPS daily rainfall + 10-year trend (2015–2024)",
565
+ "HydroSHEDS flow accumulation (15ACC)",
566
+ "SRTM DEM + slope (30m resolution)",
567
+ "FAO GAUL district boundaries",
568
+ "WorldPop population density (2020)",
569
+ "Landsat 8 NDWI permanent water mask",
570
+ "Multi-factor risk model (SVM + Random Forest ensemble)",
571
+ ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
572
 
573
  def get_context(self) -> Dict[str, Any]:
574
  ctx = super().get_context()
575
  r = self.region
576
  ctx.update({
577
  "model_accuracy_pct": r["accuracy_pct"],
578
+ "risk_zones_km2": r["risk_zones_km2"],
579
+ "high_risk_zones": r["high_risk_zones"],
580
+ "peak_rainfall_mm": r["peak_rainfall_mm"],
581
  })
582
  return ctx
583
 
584
+ def step(self, response: str, step_num: int) -> Dict[str, Any]:
585
+ r = self.region
586
+ rz = r["risk_zones_km2"]
587
+ nums = _extract_numbers(response)
588
+ score = 0.0
589
+
590
+ # Model accuracy cited
591
+ for n in nums:
592
+ if abs(n - r["accuracy_pct"]) < 2.0:
593
+ score += 0.15
594
+ break
595
+
596
+ # Risk zone areas
597
+ for zone_val in rz.values():
598
+ for n in nums:
599
+ if abs(n - zone_val) / zone_val < 0.08:
600
+ score += 0.12
601
+ break
602
+
603
+ # High-risk zone names
604
+ hit_zones = sum(1 for z in r["high_risk_zones"] if z.lower() in response.lower())
605
+ score += min(hit_zones * 0.10, 0.20)
606
+
607
+ # Causal factors
608
+ score += _causal_score(response)
609
+
610
+ # Early warning / recommendation language
611
+ if _mentions_any(response, ["early warning", "evacuate", "alert", "priority", "recommend"]):
612
+ score += 0.05
613
+
614
+ # Vague penalty
615
+ score += _penalty_vague(response)
616
+
617
+ done = step_num >= self.max_steps
618
+ return {"reward": _clamp(score), "done": done,
619
+ "result": f"Step {step_num}: scored {score:.3f}"}
620
+
621
+
622
+ # ──────────────────────────────────────────────
623
+ # REGISTRY
624
+ # ──────────────────────────────────────────────
625
 
626
  TASK_REGISTRY: Dict[str, type] = {
627
  "flood_year_comparison": FloodYearComparisonTask,
628
+ "district_inundation_report": DistrictInundationReportTask,
629
  "flood_risk_forecast": FloodRiskForecastTask,
630
  }