A real-world AI agent benchmark for water utility operations.
Water authority operators make daily decisions about how much water to transfer
between a network of reservoirs — balancing overflow prevention, demand fulfilment,
and pumping costs, all under uncertain seasonal rainfall and demand. This
environment lets you train and benchmark any RL or LLM-based agent on that task.
🌍 Motivation
Every day, water utility operators sit at dashboards and manually decide how much
water to transfer between reservoirs, tanks, and distribution zones. They must
account for:
Stochastic rainfall — highly seasonal, varying widely by location
Stochastic demand — spikes in summer, drops in winter
Asymmetric risk — overflow (flooding, water loss) is 10× more costly than a temporary shortage
Transfer costs — pumping water long distances is expensive
Drought events — sudden supply shocks that reward strategic pre-planning
This is exactly the kind of high-stakes, repetitive, partially-observable
scheduling task where AI agents should outperform human intuition — but
no public benchmark has existed for it. Until now.
1classObservation(BaseModel):2 day:int# Current episode day (1-indexed)3 month:int# Calendar month 1–12 (seasonal awareness)4 storage: List[float]# Current storage per reservoir (units)5 capacity: List[float]# Max capacity per reservoir (units)6 storage_pct: List[float]# Storage as fraction of capacity [0, 1]7 recent_rain: List[float]# Rain received at previous step8 forecast_demand: List[float]# Expected demand this step (dist. mean)9 overflow_last_step: List[float]# Overflow at previous step10 shortage_last_step: List[float]# Unmet demand at previous step11 episode_step:int# Steps elapsed12 drought_active:bool# True during drought events
Observations are also available as a natural-language prompt for LLM agents:
python
1print(obs.to_prompt())2# === Water Utility Status — Day 12 (Month 1) ===3#4# Reservoir status:5# Reservoir 1: 6.82 / 10.0 units (68%) 🟢 OK6# Reservoir 2: 3.21 / 10.0 units (32%) 🟡 LOW7# ...
Action
python
1classAction(BaseModel):2 transfers: List[List[float]]3# n×n matrix. transfers[i][j] = units moved FROM reservoir i TO j.4# Diagonal must be 0. Invalid values are clipped (not rejected).
Feasibility constraints (enforced by environment via clipping):
Objective: Navigate a dry → wet → dry cycle. Agents must learn to conserve
water during dry months and redistribute wet-season surplus to under-served
reservoirs.
Task 3 — Hard: Five-Reservoir Annual Operations with Drought
Property
Value
Reservoirs
5 (heterogeneous capacities: 10, 12, 8, 15, 10)
Horizon
365 days
Seasonality
Full 12-month seasonal cycle
Special events
Drought days 200–230 (rain ×0.1, demand ×1.5)
Objective: Manage a full year with complete seasonal variation. A sudden
drought on days 200–230 cuts rainfall to 10% of normal and raises demand by 50%.
Agents that don't build strategic reserves before day 200 will fail this task.
1# rain_monthly_params: Dict[month, List[(mu, sigma)]]2# One (mu, sigma) tuple per reservoir, for each of the 12 months.3# Sampled from Normal(mu, sigma) truncated at 0.45rain_monthly_params ={61:[(0.3,0.4),(0.5,0.4),(0.2,0.3)],# January (dry)7...86:[(2.5,1.0),(2.8,1.2),(2.0,0.9)],# June (monsoon peak)9...1012:[(0.4,0.4),(0.5,0.4),(0.3,0.3)],# December (dry)11}
🤖 Writing Your Own Agent
Any callable with this signature works:
python
1defmy_agent(obs: Observation, config: ReservoirConfig)-> Action:2# obs.storage → current storage levels3# obs.storage_pct → fraction of capacity (0–1)4# obs.forecast_demand → expected demand today5# obs.drought_active → True during drought6# config.capacities → max capacity per reservoir7# config.cost_matrix_array → transfer costs89 n = config.n_reservoirs
10 transfers =[[0.0]* n for _ inrange(n)]11# ... your logic here ...12return Action(transfers=transfers)
Then run it:
python
1result = env.run_episode(my_agent)2score = grade1(result)# or grade2 / grade33print(f"Score: {score:.4f}")