#!/usr/bin/env python3 """ AquaGuard-RL Demo Heuristic Agent ================================== Runs a complete baseline episode using a rule-based heuristic policy. Demonstrates how to interact with the environment via HTTP client. Usage: # Start server first: cd /path/to/AquaGuard-RL uvicorn server.app:app --host 0.0.0.0 --port 8000 # Then run this script: python scripts/run_sample_agent.py python scripts/run_sample_agent.py --task crisis python scripts/run_sample_agent.py --task policy_shift --server http://localhost:8000 """ import sys import os import argparse from dotenv import load_dotenv load_dotenv() # Add src to path sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src")) from client import AquaGuardEnv from models import AquaGuardAction def heuristic_policy(obs) -> AquaGuardAction: """ Simple heuristic policy for demonstration purposes. Decision logic: - If aquifer is critically stressed (> 85% of critical threshold): → Aggressively reduce rice/wheat, deploy drip, cut extraction - If aquifer is moderately stressed (> 65%): → Moderate reduction, sprinkler irrigation - If healthy: → Maintain with gradual diversification improvements """ gw = obs.shared_aquifer_level_m critical = obs.critical_aquifer_threshold_m stress = gw / critical if critical > 0 else 0 food_ok = obs.food_security_ratio >= 1.0 if stress > 0.85: # Critical: aggressive conservation rice_alloc = 0.15 if food_ok else 0.22 return AquaGuardAction( crop_allocation={ "rice": rice_alloc, "wheat": 0.18, "millet": 0.30, "pulses": 0.20, "oilseeds": 0.10, "vegetables": 0.05, }, water_quotas={"zone_a": 580, "zone_b": 530, "zone_c": 480}, irrigation_methods={"zone_a": "drip", "zone_b": "drip", "zone_c": "drip"}, extraction_limits={"zone_a": 14.0, "zone_b": 11.0, "zone_c": 8.0}, subsidy_adjustments={ "rice": -0.20, "wheat": -0.10, "millet": 0.18, "pulses": 0.18, "oilseeds": 0.12, "vegetables": 0.0, }, justification=( f"CRITICAL: Aquifer at {gw:.1f}m ({stress:.0%} of {critical:.0f}m critical). " f"Emergency conservation: rice cut to {rice_alloc:.0%} " f"(saves {(0.3 - rice_alloc) * 1200:.0f}mm water/season). " f"Drip irrigation deployed across all zones (45% water saving). " f"Extraction limits reduced to 14/11/8 m/season. " f"MSP subsidies shifted: -20% rice, +18% millet to incentivize farmers." ), ) elif stress > 0.65: # Warning: moderate measures return AquaGuardAction( crop_allocation={ "rice": 0.23, "wheat": 0.22, "millet": 0.23, "pulses": 0.17, "oilseeds": 0.10, "vegetables": 0.05, }, water_quotas={"zone_a": 720, "zone_b": 680, "zone_c": 620}, irrigation_methods={ "zone_a": "sprinkler", "zone_b": "sprinkler", "zone_c": "drip" }, extraction_limits={"zone_a": 20.0, "zone_b": 17.0, "zone_c": 13.0}, subsidy_adjustments={ "rice": -0.10, "wheat": -0.05, "millet": 0.10, "pulses": 0.10, "oilseeds": 0.05, "vegetables": 0.0, }, justification=( f"Warning: Aquifer at {gw:.1f}m ({stress:.0%} of critical). " f"Moderate action: 7pp shift from rice/wheat to millet/pulses. " f"Sprinkler irrigation in Zones A/B (30% water saving). " f"Food security {obs.food_security_ratio:.2f} maintained above threshold." ), ) else: # Healthy: gradual optimization return AquaGuardAction( crop_allocation={ "rice": 0.27, "wheat": 0.26, "millet": 0.20, "pulses": 0.14, "oilseeds": 0.09, "vegetables": 0.04, }, water_quotas={"zone_a": 850, "zone_b": 800, "zone_c": 740}, irrigation_methods={ "zone_a": "flood", "zone_b": "sprinkler", "zone_c": "sprinkler" }, extraction_limits={"zone_a": 26.0, "zone_b": 23.0, "zone_c": 19.0}, subsidy_adjustments={ "millet": 0.06, "pulses": 0.06, "rice": -0.03, }, justification=( f"Healthy aquifer at {gw:.1f}m. Proactive diversification: " f"millet/pulses nudged up 4-5pp. Zone B/C using sprinkler. " f"Shannon diversity {obs.shannon_diversity_index:.3f} — targeting improvement. " f"Food security {obs.food_security_ratio:.2f} safely above 1.0." ), ) def main(): parser = argparse.ArgumentParser(description="AquaGuard-RL Demo Heuristic Agent") parser.add_argument("--task", default="baseline", choices=["baseline", "crisis", "policy_shift", "climate_shock", "multi_district"], help="Task to run (default: baseline)") parser.add_argument("--server", default="http://localhost:8000", help="Environment server URL (default: http://localhost:8000)") parser.add_argument("--seed", type=int, default=42, help="Random seed (default: 42)") args = parser.parse_args() print(f"\nAquaGuard-RL Demo — Task: {args.task} (seed={args.seed})") print("=" * 60) print(f"Connecting to: {args.server}") print() env = AquaGuardEnv(args.server) if not env.health_check(): print("ERROR: Cannot connect to environment server.") print("Start it with:") print(" cd AquaGuard-RL") print(" uvicorn server.app:app --host 0.0.0.0 --port 8000") sys.exit(1) obs = env.reset(task=args.task, seed=args.seed) print(f"Task: {obs.task_name} | Season: {obs.season} | Year {obs.year}") print(f"Initial GW: {obs.shared_aquifer_level_m:.1f}m | " f"Food: {obs.food_security_ratio:.2f} | " f"Poverty: {obs.percent_farmers_below_poverty:.1f}%") print(f"Shannon diversity: {obs.shannon_diversity_index:.3f}") print() total_reward = 0.0 step = 0 while not obs.done: step += 1 action = heuristic_policy(obs) obs = env.step(action) total_reward += obs.reward or 0.0 print(f"Step {step:2d} [{obs.season:6s} Y{obs.year}]: " f"reward={obs.reward:+.2f} | " f"GW={obs.shared_aquifer_level_m:.1f}m | " f"food={obs.food_security_ratio:.2f} | " f"poverty={obs.percent_farmers_below_poverty:.1f}% | " f"H={obs.shannon_diversity_index:.3f}") if obs.grader_feedback: print(f" → {obs.grader_feedback[:100]}") state = env.state print() print("=" * 60) print(f"Episode complete: {step} seasons") print(f"Total reward: {total_reward:>10.2f}") print(f"Final GW depth (avg): {obs.shared_aquifer_level_m:>10.1f} m") print(f"Final food security: {obs.food_security_ratio:>10.2f}") print(f"Final poverty: {obs.percent_farmers_below_poverty:>10.1f} %") print(f"Best Shannon diversity:{state.best_shannon_diversity:>10.3f}") print(f"Food security failures:{state.food_security_failures:>10d}") print(f"GW crisis triggered: {str(state.groundwater_crisis_triggered):>10}") env.close() if __name__ == "__main__": main()