Reinforcement Learning
stable-baselines3
deep-reinforcement-learning
agricultural-ai
weather-modelling
curriculum-learning
edge-ai
Instructions to use DHDRL/monsoon-rl with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- stable-baselines3
How to use DHDRL/monsoon-rl with stable-baselines3:
from huggingface_sb3 import load_from_hub checkpoint = load_from_hub( repo_id="DHDRL/monsoon-rl", filename="{MODEL FILENAME}.zip", ) - Notebooks
- Google Colab
- Kaggle
| #!/usr/bin/env python3 | |
| """ | |
| build_dynamics_pairs.py | |
| ======================= | |
| Last-mile path: historical cache → (current, next) ZoneStateTensor pairs | |
| for physics_dynamics.DynamicsTrainer. | |
| Previously the Dyna module was fully unit-tested but had no script that | |
| turned real trajectories into training pairs. This closes that gap. | |
| Contract | |
| -------- | |
| Purpose: Emit list of (ZoneStateTensor, ZoneStateTensor) consecutive pairs | |
| from historical_continuous_indonesia_v1.pkl (or compatible). | |
| Allowed caller: offline data jobs, train_curriculum prep. | |
| Forbidden: inventing precip; using look-ahead climatology labels as targets. | |
| Writes: optional .pt file of pair list; JSON manifest. | |
| Side effects: none on live systems. | |
| Response: exit 0 + counts; structured manifest. | |
| Notes on physics framing | |
| ------------------------ | |
| Pairs are consecutive valid_times along a trajectory (step_days from meta). | |
| Precip channel = forecast precip_mm horizon (padded/truncated to horizon_days). | |
| Belief / uncertainty are derived proxies from obs anomalies (not agent | |
| beliefs) so DynamicsTrainer has a real-data fuel path without requiring a | |
| trained policy. This is intentional for the first real-data dynamics fit. | |
| Usage | |
| ----- | |
| python build_dynamics_pairs.py \\ | |
| --pkl historical_continuous_indonesia_v1.pkl \\ | |
| --zones karawang_rice,indramayu_rice \\ | |
| --horizon-days 14 \\ | |
| --out dynamics_pairs.pt \\ | |
| --manifest dynamics_pairs_manifest.json | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import logging | |
| import pickle | |
| import sys | |
| from datetime import date, datetime | |
| from pathlib import Path | |
| from typing import Any, Dict, List, Optional, Sequence, Tuple | |
| import numpy as np | |
| logger = logging.getLogger(__name__) | |
| def _parse_day(s: str) -> date: | |
| return date.fromisoformat(str(s)[:10]) | |
| def _forecast_precip_array(fc: Dict[str, Any], horizon_days: int) -> np.ndarray: | |
| precip = fc.get("precip_mm") or () | |
| arr = np.array(list(precip)[:horizon_days], dtype=np.float32) | |
| if arr.shape[0] < horizon_days: | |
| pad = np.zeros(horizon_days - arr.shape[0], dtype=np.float32) | |
| arr = np.concatenate([arr, pad]) | |
| return np.clip(arr, 0.0, 500.0) | |
| def _belief_uncertainty_from_obs(obs: Dict[str, Any]) -> Tuple[float, float]: | |
| """ | |
| Proxy scalars so pairs need no trained agent. | |
| belief ~ max of clipped anomaly magnitudes mapped to [0,1] | |
| uncertainty ~ higher when quality_flag low / cloud high | |
| """ | |
| anom = abs(float(obs.get("precip_anomaly_idx") or 0.0)) | |
| soil = abs(float(obs.get("soil_moisture_anom") or 0.0)) | |
| belief = float(np.clip(max(anom, soil) / 3.0, 0.0, 1.0)) | |
| q = int(obs.get("quality_flag") or 3) | |
| cloud = float(obs.get("cloud_cover_pct") or 0.0) | |
| uncertainty = float(np.clip(0.2 + 0.1 * max(0, 3 - q) + cloud / 200.0, 0.05, 0.95)) | |
| return belief, uncertainty | |
| def extract_pairs_from_pkl( | |
| pkl_path: Path, | |
| zone_ids: Sequence[str], | |
| horizon_days: int = 14, | |
| start: Optional[date] = None, | |
| end: Optional[date] = None, | |
| ) -> Tuple[List[Tuple[Any, Any]], List[float], Dict[str, Any]]: | |
| """ | |
| Build consecutive pairs within each trajectory (same zone, ordered time). | |
| Returns (pairs, dts, manifest_stats). dts[i] is the real-time gap in days | |
| for pairs[i] (exact step_days only). Requires torch only when materializing | |
| ZoneStateTensor — import deferred so --help works offline. | |
| """ | |
| import torch | |
| from physics_dynamics import ZoneStateTensor | |
| with open(pkl_path, "rb") as f: | |
| cache = pickle.load(f) | |
| zone_set = set(zone_ids) | |
| pairs: List[Tuple[ZoneStateTensor, ZoneStateTensor]] = [] | |
| dts: List[float] = [] | |
| stats = { | |
| "n_trajectories_seen": 0, | |
| "n_trajectories_used": 0, | |
| "n_points": 0, | |
| "n_pairs": 0, | |
| "zones": list(zone_ids), | |
| "horizon_days": horizon_days, | |
| "skipped_non_consecutive": 0, | |
| "skipped_non_exact_step": 0, | |
| "dt_values": {}, | |
| "default_dt_hint": None, | |
| } | |
| for traj in cache.get("trajectories") or []: | |
| stats["n_trajectories_seen"] += 1 | |
| meta = traj.get("meta") or {} | |
| zid = meta.get("zone_id") | |
| if zid not in zone_set: | |
| continue | |
| points = list(traj.get("trajectory") or []) | |
| if len(points) < 2: | |
| continue | |
| # filter by date if requested | |
| filtered = [] | |
| for pt in points: | |
| d = _parse_day(pt.get("valid_time", "1970-01-01")) | |
| if start and d < start: | |
| continue | |
| if end and d > end: | |
| continue | |
| filtered.append(pt) | |
| if len(filtered) < 2: | |
| continue | |
| stats["n_trajectories_used"] += 1 | |
| stats["n_points"] += len(filtered) | |
| step_days = int(meta.get("step_days") or 5) | |
| if stats["default_dt_hint"] is None: | |
| stats["default_dt_hint"] = float(step_days) | |
| for i in range(len(filtered) - 1): | |
| a, b = filtered[i], filtered[i + 1] | |
| da = _parse_day(a["valid_time"]) | |
| db = _parse_day(b["valid_time"]) | |
| gap = (db - da).days | |
| if gap <= 0: | |
| stats["skipped_non_consecutive"] += 1 | |
| continue | |
| # Exact step only — avoids dt mismatch with PDE residual. | |
| # (Previously allowed 2x step while physics_loss used dt=1.0.) | |
| if gap != step_days: | |
| stats["skipped_non_exact_step"] += 1 | |
| continue | |
| pa = _forecast_precip_array(a["forecast"], horizon_days) | |
| pb = _forecast_precip_array(b["forecast"], horizon_days) | |
| ba, ua = _belief_uncertainty_from_obs(a["obs"]) | |
| bb, ub = _belief_uncertainty_from_obs(b["obs"]) | |
| # shapes: precip [1, 1, H], uncertainty [1, 1], belief [1, 1] | |
| curr = ZoneStateTensor.from_numpy( | |
| precip=pa.reshape(1, 1, -1), | |
| uncertainty=np.array([ua], dtype=np.float32), | |
| belief=np.array([ba], dtype=np.float32), | |
| ) | |
| nxt = ZoneStateTensor.from_numpy( | |
| precip=pb.reshape(1, 1, -1), | |
| uncertainty=np.array([ub], dtype=np.float32), | |
| belief=np.array([bb], dtype=np.float32), | |
| ) | |
| pairs.append((curr, nxt)) | |
| dts.append(float(gap)) | |
| key = str(int(gap)) | |
| stats["dt_values"][key] = int(stats["dt_values"].get(key, 0)) + 1 | |
| stats["n_pairs"] += 1 | |
| return pairs, dts, stats | |
| def main(argv: Optional[Sequence[str]] = None) -> int: | |
| logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s") | |
| p = argparse.ArgumentParser(description="Build DynamicsTrainer pairs from historical pkl") | |
| p.add_argument("--pkl", required=True) | |
| p.add_argument("--zones", default="karawang_rice,indramayu_rice") | |
| p.add_argument("--horizon-days", type=int, default=14) | |
| p.add_argument("--start", default=None) | |
| p.add_argument("--end", default=None) | |
| p.add_argument("--out", default="dynamics_pairs.pt") | |
| p.add_argument("--manifest", default="dynamics_pairs_manifest.json") | |
| args = p.parse_args(list(argv) if argv is not None else None) | |
| pkl_path = Path(args.pkl) | |
| if not pkl_path.is_file(): | |
| print(f"FILE_NOT_FOUND: {pkl_path}", file=sys.stderr) | |
| return 2 | |
| zones = [z.strip() for z in args.zones.split(",") if z.strip()] | |
| start = _parse_day(args.start) if args.start else None | |
| end = _parse_day(args.end) if args.end else None | |
| try: | |
| pairs, dts, stats = extract_pairs_from_pkl( | |
| pkl_path, zones, args.horizon_days, start, end | |
| ) | |
| except ImportError as e: | |
| print(f"IMPORT_FAILED (need torch + physics_dynamics): {e}", file=sys.stderr) | |
| return 3 | |
| print( | |
| f"pairs={stats['n_pairs']} points={stats['n_points']} " | |
| f"traj_used={stats['n_trajectories_used']}/{stats['n_trajectories_seen']} " | |
| f"skipped_gap={stats['skipped_non_consecutive']} " | |
| f"skipped_non_exact={stats['skipped_non_exact_step']} " | |
| f"dt_values={stats['dt_values']}" | |
| ) | |
| if not pairs: | |
| print("NO_PAIRS", file=sys.stderr) | |
| return 4 | |
| import torch | |
| # Bundle pairs + dts so DynamicsTrainer.train(..., dts=...) gets the | |
| # real gap. Legacy code that only expects a list of (curr, nxt) can still | |
| # torch.load and take payload["pairs"]. | |
| payload = { | |
| "pairs": pairs, | |
| "dts": dts, | |
| "default_dt": float(stats.get("default_dt_hint") or 5.0), | |
| "note": "exact step_days pairs only; use dts with DynamicsTrainer", | |
| } | |
| torch.save(payload, args.out) | |
| print(f"Wrote {args.out}") | |
| manifest = { | |
| **stats, | |
| "out": str(args.out), | |
| "start": start.isoformat() if start else None, | |
| "end": end.isoformat() if end else None, | |
| "note": ( | |
| "Belief/uncertainty are obs-derived proxies, not agent beliefs. " | |
| "Temporal residual is a smoothness prior along forecast lead axis, " | |
| "not spatial advection-diffusion. " | |
| "Pairs are exact step_days only; payload includes dts for " | |
| "DynamicsTrainer.train(..., dts=dts, default_dt=default_dt)." | |
| ), | |
| } | |
| with open(args.manifest, "w") as f: | |
| json.dump(manifest, f, indent=2) | |
| print(f"Wrote {args.manifest}") | |
| return 0 | |
| def _self_test() -> None: | |
| print("build_dynamics_pairs self-test (synthetic dicts)") | |
| # Minimal offline check without pkl | |
| pa = _forecast_precip_array({"precip_mm": tuple(range(20))}, 14) | |
| assert pa.shape == (14,) | |
| b, u = _belief_uncertainty_from_obs({"precip_anomaly_idx": 3.0, "quality_flag": 2}) | |
| assert 0.0 <= b <= 1.0 and 0.0 <= u <= 1.0 | |
| print(" precip pad/clip OK") | |
| print(" belief/uncertainty proxy OK") | |
| print("All build_dynamics_pairs self-tests passed.") | |
| if __name__ == "__main__": | |
| if len(sys.argv) == 1: | |
| _self_test() | |
| else: | |
| raise SystemExit(main()) | |