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
Update weather_forecast_env.py
Browse files- weather_forecast_env.py +100 -16
weather_forecast_env.py
CHANGED
|
@@ -67,6 +67,14 @@ except ImportError:
|
|
| 67 |
DEFAULT_PRODUCT_GATE = None
|
| 68 |
_product_gate_fn = None
|
| 69 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
logger = logging.getLogger(__name__)
|
| 71 |
|
| 72 |
|
|
@@ -275,6 +283,28 @@ def _per_zone_beliefs(
|
|
| 275 |
return np.clip(np.array(beliefs, dtype=np.float32), 0.0, 1.0)
|
| 276 |
|
| 277 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 278 |
def _make_per_zone_obs(
|
| 279 |
zone_ids: List[str],
|
| 280 |
base_seed: int,
|
|
@@ -351,6 +381,34 @@ class WeatherForecastEnv(gym.Env):
|
|
| 351 |
self._episode_count: int = 0
|
| 352 |
self._episode_seed: int = 0
|
| 353 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 354 |
logger.info(
|
| 355 |
"WeatherForecastEnv: n_zones=%d horizon=%dd max_steps=%d "
|
| 356 |
"terminate_action=%d",
|
|
@@ -382,8 +440,31 @@ class WeatherForecastEnv(gym.Env):
|
|
| 382 |
f"got {type(ctx).__name__}"
|
| 383 |
)
|
| 384 |
self._context = ctx
|
|
|
|
| 385 |
else:
|
| 386 |
-
self._context =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 387 |
|
| 388 |
self._zone_ids = list(self._context.zone_ids)
|
| 389 |
n_active = len(self._zone_ids)
|
|
@@ -394,7 +475,12 @@ class WeatherForecastEnv(gym.Env):
|
|
| 394 |
f"max {self.max_zones}."
|
| 395 |
)
|
| 396 |
|
| 397 |
-
injected
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 398 |
|
| 399 |
self._forecast_arr.fill(0.0)
|
| 400 |
self._uncertainty.fill(0.0)
|
|
@@ -409,43 +495,40 @@ class WeatherForecastEnv(gym.Env):
|
|
| 409 |
)
|
| 410 |
|
| 411 |
# Single source of truth for the prior/signal belief-blend weight --
|
| 412 |
-
#
|
| 413 |
-
#
|
| 414 |
prior_w = float(getattr(self.config, "belief_prior_weight", 0.70))
|
| 415 |
|
| 416 |
-
if
|
| 417 |
z_obs = self._context.resolved_zone_obs()
|
| 418 |
z_fc = self._context.resolved_zone_forecasts()
|
| 419 |
if len(z_obs) != n_active or len(z_fc) != n_active:
|
| 420 |
raise ValueError(
|
| 421 |
-
f"
|
| 422 |
-
f"n_active={n_active} resolved_obs={len(z_obs)} "
|
| 423 |
f"resolved_fc={len(z_fc)} zone_ids={self._zone_ids}. "
|
| 424 |
-
f"For multi-zone
|
| 425 |
f"parallel to zone_ids (do not pad one zone into N slots)."
|
| 426 |
)
|
| 427 |
if n_active > 1 and not self._context.zone_obs:
|
| 428 |
raise ValueError(
|
| 429 |
-
f"
|
| 430 |
f"without explicit zone_obs lists (got n_active={n_active}, "
|
| 431 |
f"zone_ids={self._zone_ids}). Padding the primary obs into "
|
| 432 |
-
f"multiple slots is not multi-zone
|
| 433 |
)
|
| 434 |
for i, zid in enumerate(self._zone_ids):
|
| 435 |
if z_obs[i].zone_id != zid or z_fc[i].zone_id != zid:
|
| 436 |
raise ValueError(
|
| 437 |
-
f"
|
| 438 |
f"zone_ids[{i}]={zid!r} obs={z_obs[i].zone_id!r} "
|
| 439 |
f"fc={z_fc[i].zone_id!r}"
|
| 440 |
)
|
| 441 |
self._zone_obs = list(z_obs)
|
| 442 |
self._zone_forecasts = list(z_fc)
|
| 443 |
prior = float(self.config.prior_belief)
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
self._belief_map[i] = float(
|
| 447 |
-
np.clip(prior_w * prior + (1.0 - prior_w) * signal, 0.0, 1.0)
|
| 448 |
-
)
|
| 449 |
self._belief_map[n_active:] = 0.0
|
| 450 |
else:
|
| 451 |
rho = float(getattr(self.config, "event_spatial_correlation", 0.85))
|
|
@@ -486,6 +569,7 @@ class WeatherForecastEnv(gym.Env):
|
|
| 486 |
|
| 487 |
info = self._get_info()
|
| 488 |
info["outcome_code"] = "RESET_OK"
|
|
|
|
| 489 |
return self._get_obs(), info
|
| 490 |
|
| 491 |
def _maybe_shuffle_zone_order(self, effective_seed: int, n_active: int) -> None:
|
|
|
|
| 67 |
DEFAULT_PRODUCT_GATE = None
|
| 68 |
_product_gate_fn = None
|
| 69 |
|
| 70 |
+
try:
|
| 71 |
+
from real_episode_sampler import RealEpisodeIndex
|
| 72 |
+
except ImportError:
|
| 73 |
+
RealEpisodeIndex = None # training real-data sampling unavailable;
|
| 74 |
+
# ForecastConfig.real_data_pkl_path is then
|
| 75 |
+
# ignored and training stays fully synthetic,
|
| 76 |
+
# same as before this module existed.
|
| 77 |
+
|
| 78 |
logger = logging.getLogger(__name__)
|
| 79 |
|
| 80 |
|
|
|
|
| 283 |
return np.clip(np.array(beliefs, dtype=np.float32), 0.0, 1.0)
|
| 284 |
|
| 285 |
|
| 286 |
+
def _resolved_zone_beliefs(
|
| 287 |
+
zone_obs: List[Any],
|
| 288 |
+
prior: float,
|
| 289 |
+
prior_weight: float,
|
| 290 |
+
) -> np.ndarray:
|
| 291 |
+
"""Belief-map values for zone_obs that are already resolved (real data,
|
| 292 |
+
whether externally injected via options={"context": ...} or sampled by
|
| 293 |
+
RealEpisodeIndex during training) -- as opposed to _per_zone_beliefs()
|
| 294 |
+
above, which generates synthetic obs itself from a seed.
|
| 295 |
+
|
| 296 |
+
Single source of truth for the prior/signal blend across both
|
| 297 |
+
real-data code paths in reset(), so they cannot drift apart the way
|
| 298 |
+
the injected path and the synthetic path's hard-coded 0.70 literal
|
| 299 |
+
once did.
|
| 300 |
+
"""
|
| 301 |
+
beliefs = np.zeros(len(zone_obs), dtype=np.float32)
|
| 302 |
+
for i, zo in enumerate(zone_obs):
|
| 303 |
+
signal = float(zo.composite_risk())
|
| 304 |
+
beliefs[i] = prior_weight * prior + (1.0 - prior_weight) * signal
|
| 305 |
+
return np.clip(beliefs, 0.0, 1.0)
|
| 306 |
+
|
| 307 |
+
|
| 308 |
def _make_per_zone_obs(
|
| 309 |
zone_ids: List[str],
|
| 310 |
base_seed: int,
|
|
|
|
| 381 |
self._episode_count: int = 0
|
| 382 |
self._episode_seed: int = 0
|
| 383 |
|
| 384 |
+
self._real_index = None
|
| 385 |
+
pkl_path = getattr(self.config, "real_data_pkl_path", None)
|
| 386 |
+
if pkl_path:
|
| 387 |
+
if RealEpisodeIndex is None:
|
| 388 |
+
logger.warning(
|
| 389 |
+
"WeatherForecastEnv: config.real_data_pkl_path=%r set, "
|
| 390 |
+
"but real_episode_sampler.py is not importable -- "
|
| 391 |
+
"training will remain fully synthetic.",
|
| 392 |
+
pkl_path,
|
| 393 |
+
)
|
| 394 |
+
else:
|
| 395 |
+
try:
|
| 396 |
+
self._real_index = RealEpisodeIndex(pkl_path)
|
| 397 |
+
logger.info(
|
| 398 |
+
"WeatherForecastEnv: real-data training sampling "
|
| 399 |
+
"ENABLED from %s (%d eligible dates, "
|
| 400 |
+
"real_data_ratio=%.2f, inject_noise=%s)",
|
| 401 |
+
pkl_path, self._real_index.n_eligible_dates,
|
| 402 |
+
self.config.real_data_ratio, self.config.inject_noise,
|
| 403 |
+
)
|
| 404 |
+
except Exception as e:
|
| 405 |
+
logger.error(
|
| 406 |
+
"WeatherForecastEnv: failed to load "
|
| 407 |
+
"real_data_pkl_path=%r (%s) -- training will "
|
| 408 |
+
"remain fully synthetic.", pkl_path, e,
|
| 409 |
+
)
|
| 410 |
+
self._real_index = None
|
| 411 |
+
|
| 412 |
logger.info(
|
| 413 |
"WeatherForecastEnv: n_zones=%d horizon=%dd max_steps=%d "
|
| 414 |
"terminate_action=%d",
|
|
|
|
| 440 |
f"got {type(ctx).__name__}"
|
| 441 |
)
|
| 442 |
self._context = ctx
|
| 443 |
+
context_source = "injected"
|
| 444 |
else:
|
| 445 |
+
self._context = None
|
| 446 |
+
context_source = "synthetic"
|
| 447 |
+
if (
|
| 448 |
+
self._real_index is not None
|
| 449 |
+
and float(self.config.real_data_ratio) > 0.0
|
| 450 |
+
):
|
| 451 |
+
# Dedicated RNG stream, offset from the streams
|
| 452 |
+
# _maybe_shuffle_zone_order and other reset-time randomness
|
| 453 |
+
# use, so enabling real-data sampling doesn't change the
|
| 454 |
+
# synthetic path's own random draws when it IS taken.
|
| 455 |
+
real_rng = random.Random(int(effective_seed) + 9001)
|
| 456 |
+
if real_rng.random() < float(self.config.real_data_ratio):
|
| 457 |
+
sampled = self._real_index.sample(
|
| 458 |
+
real_rng, self.config.n_zones, self.config,
|
| 459 |
+
inject_noise=bool(self.config.inject_noise),
|
| 460 |
+
noise_scale=float(self.config.noise_scale),
|
| 461 |
+
)
|
| 462 |
+
if sampled is not None:
|
| 463 |
+
self._context = sampled
|
| 464 |
+
context_source = "real_sampled"
|
| 465 |
+
if self._context is None:
|
| 466 |
+
self._context = _make_multi_zone_context(self.config, effective_seed)
|
| 467 |
+
context_source = "synthetic"
|
| 468 |
|
| 469 |
self._zone_ids = list(self._context.zone_ids)
|
| 470 |
n_active = len(self._zone_ids)
|
|
|
|
| 475 |
f"max {self.max_zones}."
|
| 476 |
)
|
| 477 |
|
| 478 |
+
# "injected" (explicit real-eval context) and "real_sampled"
|
| 479 |
+
# (RealEpisodeIndex draw during training) both arrive as a fully
|
| 480 |
+
# resolved EpisodeContext with real zone_obs/zone_forecasts already
|
| 481 |
+
# attached -- they share the same validation and belief-computation
|
| 482 |
+
# path below. Only "synthetic" generates obs/forecasts itself.
|
| 483 |
+
use_resolved_path = context_source in ("injected", "real_sampled")
|
| 484 |
|
| 485 |
self._forecast_arr.fill(0.0)
|
| 486 |
self._uncertainty.fill(0.0)
|
|
|
|
| 495 |
)
|
| 496 |
|
| 497 |
# Single source of truth for the prior/signal belief-blend weight --
|
| 498 |
+
# the injected, real-sampled, and synthetic reset paths below must
|
| 499 |
+
# all read this same value, not their own copies of it.
|
| 500 |
prior_w = float(getattr(self.config, "belief_prior_weight", 0.70))
|
| 501 |
|
| 502 |
+
if use_resolved_path:
|
| 503 |
z_obs = self._context.resolved_zone_obs()
|
| 504 |
z_fc = self._context.resolved_zone_forecasts()
|
| 505 |
if len(z_obs) != n_active or len(z_fc) != n_active:
|
| 506 |
raise ValueError(
|
| 507 |
+
f"{context_source} EpisodeContext zone payload length "
|
| 508 |
+
f"mismatch: n_active={n_active} resolved_obs={len(z_obs)} "
|
| 509 |
f"resolved_fc={len(z_fc)} zone_ids={self._zone_ids}. "
|
| 510 |
+
f"For multi-zone episodes supply zone_obs/zone_forecasts "
|
| 511 |
f"parallel to zone_ids (do not pad one zone into N slots)."
|
| 512 |
)
|
| 513 |
if n_active > 1 and not self._context.zone_obs:
|
| 514 |
raise ValueError(
|
| 515 |
+
f"{context_source} EpisodeContext refuses n_zones>1 "
|
| 516 |
f"without explicit zone_obs lists (got n_active={n_active}, "
|
| 517 |
f"zone_ids={self._zone_ids}). Padding the primary obs into "
|
| 518 |
+
f"multiple slots is not multi-zone data."
|
| 519 |
)
|
| 520 |
for i, zid in enumerate(self._zone_ids):
|
| 521 |
if z_obs[i].zone_id != zid or z_fc[i].zone_id != zid:
|
| 522 |
raise ValueError(
|
| 523 |
+
f"{context_source} context zone mismatch at slot {i}: "
|
| 524 |
f"zone_ids[{i}]={zid!r} obs={z_obs[i].zone_id!r} "
|
| 525 |
f"fc={z_fc[i].zone_id!r}"
|
| 526 |
)
|
| 527 |
self._zone_obs = list(z_obs)
|
| 528 |
self._zone_forecasts = list(z_fc)
|
| 529 |
prior = float(self.config.prior_belief)
|
| 530 |
+
risks = _resolved_zone_beliefs(self._zone_obs, prior, prior_w)
|
| 531 |
+
self._belief_map[:n_active] = risks[:n_active]
|
|
|
|
|
|
|
|
|
|
| 532 |
self._belief_map[n_active:] = 0.0
|
| 533 |
else:
|
| 534 |
rho = float(getattr(self.config, "event_spatial_correlation", 0.85))
|
|
|
|
| 569 |
|
| 570 |
info = self._get_info()
|
| 571 |
info["outcome_code"] = "RESET_OK"
|
| 572 |
+
info["context_source"] = context_source
|
| 573 |
return self._get_obs(), info
|
| 574 |
|
| 575 |
def _maybe_shuffle_zone_order(self, effective_seed: int, n_active: int) -> None:
|