DHDRL commited on
Commit
db8571e
·
verified ·
1 Parent(s): fcc2f55

Update weather_forecast_env.py

Browse files
Files changed (1) hide show
  1. 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 = _make_multi_zone_context(self.config, effective_seed)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 = bool(options and "context" in options)
 
 
 
 
 
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
- # both the injected (real-eval) and synthetic (training) reset paths
413
- # below must read this same value, not their own copies of it.
414
  prior_w = float(getattr(self.config, "belief_prior_weight", 0.70))
415
 
416
- if injected:
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"Injected EpisodeContext zone payload length mismatch: "
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 real eval supply zone_obs/zone_forecasts "
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"Injected EpisodeContext real-eval path refuses n_zones>1 "
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 eval."
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"Injected context zone mismatch at slot {i}: "
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
- for i in range(n_active):
445
- signal = float(self._zone_obs[i].composite_risk())
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: