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 evaluate_checkpoint_real.py
Browse files- evaluate_checkpoint_real.py +2 -112
evaluate_checkpoint_real.py
CHANGED
|
@@ -79,8 +79,7 @@ from weather_forecast_env import make_weather_env
|
|
| 79 |
|
| 80 |
logger = logging.getLogger(__name__)
|
| 81 |
|
| 82 |
-
|
| 83 |
-
# (not one-shot — multi-zone real eval can diverge on a subset of days).
|
| 84 |
_PATH_CHECK_N = 0
|
| 85 |
_PATH_CHECK_DIVERGE = 0
|
| 86 |
|
|
@@ -88,8 +87,6 @@ _PATH_CHECK_DIVERGE = 0
|
|
| 88 |
# Entities
|
| 89 |
# ---------------------------------------------------------------------------
|
| 90 |
|
| 91 |
-
# Relative belief movement threshold (policy-sensitive by construction).
|
| 92 |
-
# Fixed bars against prior/rational saturate when prior > rational.
|
| 93 |
BELIEF_RAISE_EPS = 0.02
|
| 94 |
|
| 95 |
|
|
@@ -100,7 +97,6 @@ def _path_check_reset() -> None:
|
|
| 100 |
|
| 101 |
|
| 102 |
def _path_check_report() -> None:
|
| 103 |
-
"""Print aggregate PATH_CHECK after an eval pass."""
|
| 104 |
if _PATH_CHECK_N <= 0:
|
| 105 |
return
|
| 106 |
n_ok = _PATH_CHECK_N - _PATH_CHECK_DIVERGE
|
|
@@ -115,7 +111,6 @@ def _path_check_report() -> None:
|
|
| 115 |
|
| 116 |
@dataclass
|
| 117 |
class EvalDayRecord:
|
| 118 |
-
"""One evaluated zone-day."""
|
| 119 |
|
| 120 |
date: str
|
| 121 |
zone_id: str
|
|
@@ -131,8 +126,6 @@ class EvalDayRecord:
|
|
| 131 |
product_emit_code: str = "N/A" # not emitted to bus in this harness
|
| 132 |
ep_len: int = 0
|
| 133 |
basin_dim: int = 0
|
| 134 |
-
# Policy-sensitive: terminal belief vs episode initial belief.
|
| 135 |
-
# Scorer product_alert is independent of inspections; belief_delta is not.
|
| 136 |
believed_p: float = 0.0
|
| 137 |
initial_belief: float = 0.0
|
| 138 |
belief_delta: float = 0.0
|
|
@@ -148,30 +141,20 @@ class EvalMetrics:
|
|
| 148 |
tn: int = 0
|
| 149 |
n_l1: int = 0 # days inside an L1 event span (positive labels only in v1)
|
| 150 |
n_product: int = 0
|
| 151 |
-
# Days outside every L1 span for the zone — excluded from P/R/F1
|
| 152 |
n_unlabeled: int = 0
|
| 153 |
n_unlabeled_product: int = 0
|
| 154 |
n_unlabeled_belief_raised: int = 0
|
| 155 |
-
# Belief-raised on L1 event days only (positive spans → TP/FN; no FP/TN path)
|
| 156 |
belief_tp: int = 0
|
| 157 |
belief_fn: int = 0
|
| 158 |
-
# Kept for schema stability; never incremented under positive-only L1 v1
|
| 159 |
belief_fp: int = 0
|
| 160 |
belief_tn: int = 0
|
| 161 |
n_belief_raised: int = 0
|
| 162 |
-
# Raw belief-delta distribution (all days; also split for diagnostics)
|
| 163 |
belief_deltas: List[float] = field(default_factory=list)
|
| 164 |
belief_deltas_l1: List[float] = field(default_factory=list)
|
| 165 |
belief_deltas_unlabeled: List[float] = field(default_factory=list)
|
| 166 |
|
| 167 |
@property
|
| 168 |
def precision(self) -> Optional[float]:
|
| 169 |
-
# Positive-only L1 catalog: no confirmed negatives → FP stays 0 by
|
| 170 |
-
# construction, so precision is undefined (not 1.0).
|
| 171 |
-
# Self-healing: once a catalog provides real TN/FP paths, fp/tn leave
|
| 172 |
-
# zero and this guard stops firing. Re-audit if negatives arrive for
|
| 173 |
-
# only some hazards while others stay positive-only (per-Metrics-object
|
| 174 |
-
# guard is not per-hazard).
|
| 175 |
if self.fp == 0 and self.tn == 0 and (self.tp + self.fn) > 0:
|
| 176 |
return None
|
| 177 |
d = self.tp + self.fp
|
|
@@ -191,19 +174,15 @@ class EvalMetrics:
|
|
| 191 |
|
| 192 |
@property
|
| 193 |
def belief_precision(self) -> Optional[float]:
|
| 194 |
-
# Same structural issue as product precision: no confirmed-negative
|
| 195 |
-
# path under positive-only L1 → never report a fake 1.0.
|
| 196 |
return None
|
| 197 |
|
| 198 |
@property
|
| 199 |
def belief_recall(self) -> Optional[float]:
|
| 200 |
-
"""Fraction of L1 event days where belief_delta > BELIEF_RAISE_EPS."""
|
| 201 |
d = self.belief_tp + self.belief_fn
|
| 202 |
return self.belief_tp / d if d else None
|
| 203 |
|
| 204 |
@property
|
| 205 |
def belief_f1(self) -> Optional[float]:
|
| 206 |
-
# Undefined without belief_precision.
|
| 207 |
return None
|
| 208 |
|
| 209 |
def belief_delta_stats(self) -> Dict[str, float]:
|
|
@@ -239,7 +218,6 @@ class EvalMetrics:
|
|
| 239 |
|
| 240 |
@property
|
| 241 |
def belief_unlabeled_raise_rate(self) -> Optional[float]:
|
| 242 |
-
"""How often belief crosses the raise bar on days the catalog is silent."""
|
| 243 |
if self.n_unlabeled <= 0:
|
| 244 |
return None
|
| 245 |
return self.n_unlabeled_belief_raised / self.n_unlabeled
|
|
@@ -249,7 +227,6 @@ class EvalMetrics:
|
|
| 249 |
return None if x is None else round(x, 6)
|
| 250 |
|
| 251 |
bd = self.belief_delta_stats()
|
| 252 |
-
# Temporary swap for L1 / unlabeled delta stats without mutating permanently
|
| 253 |
def _subset_stats(xs: List[float]) -> Dict[str, Any]:
|
| 254 |
saved = self.belief_deltas
|
| 255 |
self.belief_deltas = xs
|
|
@@ -315,14 +292,6 @@ def load_historical_points(
|
|
| 315 |
start: date,
|
| 316 |
end: date,
|
| 317 |
) -> List[Dict[str, Any]]:
|
| 318 |
-
"""
|
| 319 |
-
Flatten trajectory cache into zone-day points inside [start, end].
|
| 320 |
-
|
| 321 |
-
Contract:
|
| 322 |
-
purpose: SSOT historical points for real eval
|
| 323 |
-
forbidden: mutating the pkl; inventing missing obs fields
|
| 324 |
-
response: list of point dicts with obs/forecast/basin_context keys
|
| 325 |
-
"""
|
| 326 |
with open(pkl_path, "rb") as f:
|
| 327 |
cache = pickle.load(f)
|
| 328 |
trajs = cache.get("trajectories") or []
|
|
@@ -348,7 +317,6 @@ def point_to_episode(
|
|
| 348 |
pt: Dict[str, Any],
|
| 349 |
cfg: ForecastConfig,
|
| 350 |
) -> EpisodeContext:
|
| 351 |
-
"""Deserialize one cache point into a typed single-zone EpisodeContext."""
|
| 352 |
obs = ZoneObs.from_dict(dict(pt["obs"]))
|
| 353 |
fc = ForecastResult.from_dict(dict(pt["forecast"]))
|
| 354 |
basin = None
|
|
@@ -372,11 +340,6 @@ def points_to_multi_zone_episode(
|
|
| 372 |
cfg: ForecastConfig,
|
| 373 |
zone_order: Sequence[str],
|
| 374 |
) -> EpisodeContext:
|
| 375 |
-
"""
|
| 376 |
-
Build a multi-zone EpisodeContext from same-day points (Blocker B).
|
| 377 |
-
|
| 378 |
-
Requires one point per zone_id in zone_order. Primary obs/forecast = slot 0.
|
| 379 |
-
"""
|
| 380 |
by_z = {}
|
| 381 |
for pt in pts:
|
| 382 |
obs = ZoneObs.from_dict(dict(pt["obs"]))
|
|
@@ -412,7 +375,6 @@ def points_to_multi_zone_episode(
|
|
| 412 |
def group_points_by_date(
|
| 413 |
points: Sequence[Dict[str, Any]],
|
| 414 |
) -> Dict[str, List[Dict[str, Any]]]:
|
| 415 |
-
"""Map ISO date string → list of points on that calendar day."""
|
| 416 |
out: Dict[str, List[Dict[str, Any]]] = {}
|
| 417 |
for pt in points:
|
| 418 |
obs = pt.get("obs") or {}
|
|
@@ -433,11 +395,9 @@ def decide_scorer_oracle(
|
|
| 433 |
cfg: ForecastConfig,
|
| 434 |
gate: ProductGateConfig,
|
| 435 |
) -> Tuple[bool, bool, RiskScore, int, float, float]:
|
| 436 |
-
"""Product decision from deterministic scorer only (no agent)."""
|
| 437 |
rs = compute_risk_score(obs, fc, cfg)
|
| 438 |
product = is_product_actionable(rs, gate)
|
| 439 |
elevated = rs.is_elevated()
|
| 440 |
-
# No agent → no belief state; return zeros so delta is 0.
|
| 441 |
return product, elevated, rs, 0, 0.0, 0.0
|
| 442 |
|
| 443 |
|
|
@@ -450,14 +410,6 @@ def decide_never() -> Tuple[bool, bool, None, int, float, float]:
|
|
| 450 |
|
| 451 |
|
| 452 |
def _initial_belief_from_info(info: Dict[str, Any], cfg: ForecastConfig) -> float:
|
| 453 |
-
"""
|
| 454 |
-
Belief at reset (before any inspect), matching terminate aggregation.
|
| 455 |
-
|
| 456 |
-
Terminal believed_p is max(belief_map[:n_active]). Using mean_belief or
|
| 457 |
-
zone_belief[0] here made multi-zone zero-inspect deltas nonzero even with
|
| 458 |
-
no inspections (max of three post-reset blends ≠ mean or zone0). Prefer
|
| 459 |
-
the same max aggregation the env exposes as info['believed_p'].
|
| 460 |
-
"""
|
| 461 |
if "believed_p" in info and info["believed_p"] is not None:
|
| 462 |
return float(info["believed_p"])
|
| 463 |
zb = info.get("zone_belief")
|
|
@@ -472,7 +424,6 @@ def _initial_belief_from_info(info: Dict[str, Any], cfg: ForecastConfig) -> floa
|
|
| 472 |
return float(_np.max(arr))
|
| 473 |
except Exception:
|
| 474 |
pass
|
| 475 |
-
# Last resort only — not symmetric with terminal max.
|
| 476 |
if "mean_belief" in info and info["mean_belief"] is not None:
|
| 477 |
return float(info["mean_belief"])
|
| 478 |
return float(cfg.prior_belief)
|
|
@@ -484,17 +435,6 @@ def decide_checkpoint(
|
|
| 484 |
ctx: EpisodeContext,
|
| 485 |
gate: ProductGateConfig,
|
| 486 |
) -> Tuple[bool, bool, Optional[RiskScore], int, float, float]:
|
| 487 |
-
"""
|
| 488 |
-
Roll MaskablePPO until terminate / budget exhaust.
|
| 489 |
-
|
| 490 |
-
Returns:
|
| 491 |
-
product, elevated from env info at terminal step (scorer-based flags —
|
| 492 |
-
these do NOT depend on inspections; kept for product-bus parity).
|
| 493 |
-
rs for display magnitudes only — MUST NOT overwrite product/elevated.
|
| 494 |
-
ep_len, believed_p (terminal), initial_belief (at reset).
|
| 495 |
-
|
| 496 |
-
Policy-sensitive signal: belief_delta = believed_p - initial_belief.
|
| 497 |
-
"""
|
| 498 |
global _PATH_CHECK_N, _PATH_CHECK_DIVERGE
|
| 499 |
obs, info = env.reset(options={"context": ctx})
|
| 500 |
initial_belief = _initial_belief_from_info(info, ctx.config)
|
|
@@ -518,13 +458,8 @@ def decide_checkpoint(
|
|
| 518 |
if "believed_p" in info:
|
| 519 |
believed_p = float(info["believed_p"])
|
| 520 |
elif "mean_belief" in info:
|
| 521 |
-
# Prefer max-aggregation; mean is only a fallback if believed_p missing.
|
| 522 |
believed_p = float(info["mean_belief"])
|
| 523 |
|
| 524 |
-
# PATH_CHECK: product from the loop must be the returned values.
|
| 525 |
-
# Trailing compute_risk_score is DISPLAY ONLY — must not overwrite.
|
| 526 |
-
# Multi-zone: primary ctx.obs is only slot 0; rs may diverge from the
|
| 527 |
-
# env's worst-case multi-zone product — count divergences, do not print once.
|
| 528 |
loop_product, loop_elevated = product, elevated
|
| 529 |
rs = None
|
| 530 |
try:
|
|
@@ -535,7 +470,6 @@ def decide_checkpoint(
|
|
| 535 |
_PATH_CHECK_DIVERGE += 1
|
| 536 |
except Exception:
|
| 537 |
pass
|
| 538 |
-
# Explicit: return loop-captured values, never rs-derived product.
|
| 539 |
return loop_product, loop_elevated, rs, ep_len, believed_p, initial_belief
|
| 540 |
|
| 541 |
|
|
@@ -544,11 +478,6 @@ def decide_zero_inspect(
|
|
| 544 |
ctx: EpisodeContext,
|
| 545 |
gate: ProductGateConfig,
|
| 546 |
) -> Tuple[bool, bool, Optional[RiskScore], int, float, float]:
|
| 547 |
-
"""Terminate on step 1 with zero inspections (policy-insensitivity control).
|
| 548 |
-
|
| 549 |
-
With symmetric max-aggregation on initial and terminal believed_p, belief
|
| 550 |
-
delta must be exactly 0.0 when no inspect updates the map.
|
| 551 |
-
"""
|
| 552 |
obs, info = env.reset(options={"context": ctx})
|
| 553 |
initial_belief = _initial_belief_from_info(info, ctx.config)
|
| 554 |
base = env.env if hasattr(env, "env") else env
|
|
@@ -565,7 +494,6 @@ def decide_zero_inspect(
|
|
| 565 |
rs = compute_risk_score(ctx.obs, ctx.forecast, ctx.config)
|
| 566 |
except Exception:
|
| 567 |
pass
|
| 568 |
-
# Zero inspect: belief should equal initial (no inspect → no update).
|
| 569 |
return product, elevated, rs, 1, believed_p, initial_belief
|
| 570 |
|
| 571 |
|
|
@@ -584,14 +512,6 @@ def evaluate_multi_zone_days(
|
|
| 584 |
model: Any = None,
|
| 585 |
env: Any = None,
|
| 586 |
) -> Tuple[List[EvalDayRecord], EvalMetrics]:
|
| 587 |
-
"""
|
| 588 |
-
Multi-zone real eval (Blocker B).
|
| 589 |
-
|
| 590 |
-
Groups points by calendar day; requires all zone_order zones present that
|
| 591 |
-
day. GT: l1 if ANY packed zone has an L1 event that day; else unlabeled
|
| 592 |
-
when the impact store is loaded. One EvalDayRecord per complete day
|
| 593 |
-
(zone_id is the joined zone list).
|
| 594 |
-
"""
|
| 595 |
if mode not in ("checkpoint", "zero_inspect"):
|
| 596 |
raise ValueError(
|
| 597 |
f"evaluate_multi_zone_days only supports checkpoint/zero_inspect "
|
|
@@ -730,20 +650,6 @@ def evaluate_points(
|
|
| 730 |
model: Any = None,
|
| 731 |
env: Any = None,
|
| 732 |
) -> Tuple[List[EvalDayRecord], EvalMetrics]:
|
| 733 |
-
"""
|
| 734 |
-
Contract: evaluate_points
|
| 735 |
-
|
| 736 |
-
Purpose: Score product decisions on real historical zone-days (single-zone).
|
| 737 |
-
For multi-zone agent eval (n_zones>1), use evaluate_multi_zone_days.
|
| 738 |
-
GT: if impact_store is loaded:
|
| 739 |
-
day inside any event span for the zone → gt_source="l1"
|
| 740 |
-
otherwise → gt_source="unlabeled" (NOT a confirmed negative)
|
| 741 |
-
if no store → gt_source="none"
|
| 742 |
-
Confusion matrix (TP/FP/FN/TN) uses only gt_source="l1" rows.
|
| 743 |
-
Unlabeled rows contribute only to n_unlabeled / unlabeled_alert_rate.
|
| 744 |
-
Alert: product gate only.
|
| 745 |
-
Idempotency: pure function of inputs; no transport side effects.
|
| 746 |
-
"""
|
| 747 |
_path_check_reset()
|
| 748 |
records: List[EvalDayRecord] = []
|
| 749 |
m = EvalMetrics()
|
|
@@ -764,11 +670,8 @@ def evaluate_points(
|
|
| 764 |
ld, lf = impact_store.labels_for_day(zid, day)
|
| 765 |
event_d, event_f = bool(ld), bool(lf)
|
| 766 |
if event_d or event_f:
|
| 767 |
-
# Day falls inside an L1 event span → positive label
|
| 768 |
gt_source = "l1"
|
| 769 |
else:
|
| 770 |
-
# Store loaded, day outside every span for this zone.
|
| 771 |
-
# Sparse catalogs must NOT treat these as confirmed TN/FP.
|
| 772 |
gt_source = "unlabeled"
|
| 773 |
except Exception as e:
|
| 774 |
logger.warning("L1 query failed %s %s: %s", zid, day, e)
|
|
@@ -807,15 +710,10 @@ def evaluate_points(
|
|
| 807 |
drought_risk = float(rs.drought_risk) if rs is not None else 0.0
|
| 808 |
flood_risk = float(rs.flood_risk) if rs is not None else 0.0
|
| 809 |
|
| 810 |
-
# Policy-sensitive: relative movement, not fixed bar vs prior/rational.
|
| 811 |
-
# Fixed bars saturate when prior > rational (prior=0.12, rational=0.0625).
|
| 812 |
belief_delta = float(believed_p) - float(initial_belief)
|
| 813 |
belief_raised = bool(belief_delta > BELIEF_RAISE_EPS)
|
| 814 |
|
| 815 |
-
# Product / belief confusion matrix: L1 event days only.
|
| 816 |
-
# Unlabeled days are excluded from TP/FP/FN/TN (sparse catalog honesty).
|
| 817 |
if gt_source == "l1":
|
| 818 |
-
# v1 catalog only stores positive impact spans → TP or FN
|
| 819 |
if product:
|
| 820 |
m.tp += 1
|
| 821 |
else:
|
|
@@ -833,7 +731,6 @@ def evaluate_points(
|
|
| 833 |
if belief_raised:
|
| 834 |
m.n_unlabeled_belief_raised += 1
|
| 835 |
m.belief_deltas_unlabeled.append(belief_delta)
|
| 836 |
-
# No TN/FP: catalog never asserted "no impact" for this day
|
| 837 |
|
| 838 |
m.n_days += 1
|
| 839 |
if product:
|
|
@@ -1061,8 +958,6 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
| 1061 |
except Exception as e:
|
| 1062 |
logger.warning("could not cross-check policy obs space: %s", e)
|
| 1063 |
|
| 1064 |
-
# Multi-zone agent eval needs same-day packs with zone_obs lists (Blocker B).
|
| 1065 |
-
# Scorer/always/never stay single-zone day metrics.
|
| 1066 |
if (
|
| 1067 |
args.mode in ("checkpoint", "zero_inspect")
|
| 1068 |
and int(args.n_zones) > 1
|
|
@@ -1098,7 +993,6 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
| 1098 |
)
|
| 1099 |
_print_metrics(args.mode, metrics)
|
| 1100 |
|
| 1101 |
-
# Per-zone breakdown (same unlabeled exclusion rule)
|
| 1102 |
by_zone: Dict[str, EvalMetrics] = {}
|
| 1103 |
for r in records:
|
| 1104 |
zm = by_zone.setdefault(r.zone_id, EvalMetrics())
|
|
@@ -1175,13 +1069,11 @@ def _self_test() -> None:
|
|
| 1175 |
assert rs is not None
|
| 1176 |
print(f" drought scorer product={product} elevated={elevated} "
|
| 1177 |
f"alert={rs.alert_level.value} drought_risk={rs.drought_risk:.3f}")
|
| 1178 |
-
# Metrics arithmetic — with confirmed negatives, P is real
|
| 1179 |
m = EvalMetrics(n_days=4, tp=1, fp=1, fn=1, tn=1, n_l1=2, n_product=2)
|
| 1180 |
assert abs((m.precision or 0) - 0.5) < 1e-9
|
| 1181 |
assert abs((m.recall or 0) - 0.5) < 1e-9
|
| 1182 |
print(" metrics arithmetic OK")
|
| 1183 |
|
| 1184 |
-
# Positive-only L1: precision/f1 must be None, not 1.0
|
| 1185 |
m_pos = EvalMetrics(n_days=10, tp=7, fn=3, n_l1=10, fp=0, tn=0)
|
| 1186 |
assert m_pos.precision is None, m_pos.precision
|
| 1187 |
assert m_pos.f1 is None
|
|
@@ -1190,7 +1082,6 @@ def _self_test() -> None:
|
|
| 1190 |
assert m_pos.belief_f1 is None
|
| 1191 |
print(" positive-only null precision OK")
|
| 1192 |
|
| 1193 |
-
# Unlabeled rate symmetry
|
| 1194 |
m_u = EvalMetrics(
|
| 1195 |
n_unlabeled=20,
|
| 1196 |
n_unlabeled_product=8,
|
|
@@ -1200,7 +1091,6 @@ def _self_test() -> None:
|
|
| 1200 |
assert abs((m_u.belief_unlabeled_raise_rate or 0) - 0.55) < 1e-9
|
| 1201 |
print(" unlabeled rates OK")
|
| 1202 |
|
| 1203 |
-
# Console formatter must not turn None into 0.000
|
| 1204 |
def _f(x):
|
| 1205 |
return f"{x:.3f}" if x is not None else " - "
|
| 1206 |
assert _f(None) == " - "
|
|
@@ -1215,4 +1105,4 @@ if __name__ == "__main__":
|
|
| 1215 |
if len(sys.argv) == 1:
|
| 1216 |
_self_test()
|
| 1217 |
else:
|
| 1218 |
-
raise SystemExit(main())
|
|
|
|
| 79 |
|
| 80 |
logger = logging.getLogger(__name__)
|
| 81 |
|
| 82 |
+
|
|
|
|
| 83 |
_PATH_CHECK_N = 0
|
| 84 |
_PATH_CHECK_DIVERGE = 0
|
| 85 |
|
|
|
|
| 87 |
# Entities
|
| 88 |
# ---------------------------------------------------------------------------
|
| 89 |
|
|
|
|
|
|
|
| 90 |
BELIEF_RAISE_EPS = 0.02
|
| 91 |
|
| 92 |
|
|
|
|
| 97 |
|
| 98 |
|
| 99 |
def _path_check_report() -> None:
|
|
|
|
| 100 |
if _PATH_CHECK_N <= 0:
|
| 101 |
return
|
| 102 |
n_ok = _PATH_CHECK_N - _PATH_CHECK_DIVERGE
|
|
|
|
| 111 |
|
| 112 |
@dataclass
|
| 113 |
class EvalDayRecord:
|
|
|
|
| 114 |
|
| 115 |
date: str
|
| 116 |
zone_id: str
|
|
|
|
| 126 |
product_emit_code: str = "N/A" # not emitted to bus in this harness
|
| 127 |
ep_len: int = 0
|
| 128 |
basin_dim: int = 0
|
|
|
|
|
|
|
| 129 |
believed_p: float = 0.0
|
| 130 |
initial_belief: float = 0.0
|
| 131 |
belief_delta: float = 0.0
|
|
|
|
| 141 |
tn: int = 0
|
| 142 |
n_l1: int = 0 # days inside an L1 event span (positive labels only in v1)
|
| 143 |
n_product: int = 0
|
|
|
|
| 144 |
n_unlabeled: int = 0
|
| 145 |
n_unlabeled_product: int = 0
|
| 146 |
n_unlabeled_belief_raised: int = 0
|
|
|
|
| 147 |
belief_tp: int = 0
|
| 148 |
belief_fn: int = 0
|
|
|
|
| 149 |
belief_fp: int = 0
|
| 150 |
belief_tn: int = 0
|
| 151 |
n_belief_raised: int = 0
|
|
|
|
| 152 |
belief_deltas: List[float] = field(default_factory=list)
|
| 153 |
belief_deltas_l1: List[float] = field(default_factory=list)
|
| 154 |
belief_deltas_unlabeled: List[float] = field(default_factory=list)
|
| 155 |
|
| 156 |
@property
|
| 157 |
def precision(self) -> Optional[float]:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
if self.fp == 0 and self.tn == 0 and (self.tp + self.fn) > 0:
|
| 159 |
return None
|
| 160 |
d = self.tp + self.fp
|
|
|
|
| 174 |
|
| 175 |
@property
|
| 176 |
def belief_precision(self) -> Optional[float]:
|
|
|
|
|
|
|
| 177 |
return None
|
| 178 |
|
| 179 |
@property
|
| 180 |
def belief_recall(self) -> Optional[float]:
|
|
|
|
| 181 |
d = self.belief_tp + self.belief_fn
|
| 182 |
return self.belief_tp / d if d else None
|
| 183 |
|
| 184 |
@property
|
| 185 |
def belief_f1(self) -> Optional[float]:
|
|
|
|
| 186 |
return None
|
| 187 |
|
| 188 |
def belief_delta_stats(self) -> Dict[str, float]:
|
|
|
|
| 218 |
|
| 219 |
@property
|
| 220 |
def belief_unlabeled_raise_rate(self) -> Optional[float]:
|
|
|
|
| 221 |
if self.n_unlabeled <= 0:
|
| 222 |
return None
|
| 223 |
return self.n_unlabeled_belief_raised / self.n_unlabeled
|
|
|
|
| 227 |
return None if x is None else round(x, 6)
|
| 228 |
|
| 229 |
bd = self.belief_delta_stats()
|
|
|
|
| 230 |
def _subset_stats(xs: List[float]) -> Dict[str, Any]:
|
| 231 |
saved = self.belief_deltas
|
| 232 |
self.belief_deltas = xs
|
|
|
|
| 292 |
start: date,
|
| 293 |
end: date,
|
| 294 |
) -> List[Dict[str, Any]]:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 295 |
with open(pkl_path, "rb") as f:
|
| 296 |
cache = pickle.load(f)
|
| 297 |
trajs = cache.get("trajectories") or []
|
|
|
|
| 317 |
pt: Dict[str, Any],
|
| 318 |
cfg: ForecastConfig,
|
| 319 |
) -> EpisodeContext:
|
|
|
|
| 320 |
obs = ZoneObs.from_dict(dict(pt["obs"]))
|
| 321 |
fc = ForecastResult.from_dict(dict(pt["forecast"]))
|
| 322 |
basin = None
|
|
|
|
| 340 |
cfg: ForecastConfig,
|
| 341 |
zone_order: Sequence[str],
|
| 342 |
) -> EpisodeContext:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 343 |
by_z = {}
|
| 344 |
for pt in pts:
|
| 345 |
obs = ZoneObs.from_dict(dict(pt["obs"]))
|
|
|
|
| 375 |
def group_points_by_date(
|
| 376 |
points: Sequence[Dict[str, Any]],
|
| 377 |
) -> Dict[str, List[Dict[str, Any]]]:
|
|
|
|
| 378 |
out: Dict[str, List[Dict[str, Any]]] = {}
|
| 379 |
for pt in points:
|
| 380 |
obs = pt.get("obs") or {}
|
|
|
|
| 395 |
cfg: ForecastConfig,
|
| 396 |
gate: ProductGateConfig,
|
| 397 |
) -> Tuple[bool, bool, RiskScore, int, float, float]:
|
|
|
|
| 398 |
rs = compute_risk_score(obs, fc, cfg)
|
| 399 |
product = is_product_actionable(rs, gate)
|
| 400 |
elevated = rs.is_elevated()
|
|
|
|
| 401 |
return product, elevated, rs, 0, 0.0, 0.0
|
| 402 |
|
| 403 |
|
|
|
|
| 410 |
|
| 411 |
|
| 412 |
def _initial_belief_from_info(info: Dict[str, Any], cfg: ForecastConfig) -> float:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 413 |
if "believed_p" in info and info["believed_p"] is not None:
|
| 414 |
return float(info["believed_p"])
|
| 415 |
zb = info.get("zone_belief")
|
|
|
|
| 424 |
return float(_np.max(arr))
|
| 425 |
except Exception:
|
| 426 |
pass
|
|
|
|
| 427 |
if "mean_belief" in info and info["mean_belief"] is not None:
|
| 428 |
return float(info["mean_belief"])
|
| 429 |
return float(cfg.prior_belief)
|
|
|
|
| 435 |
ctx: EpisodeContext,
|
| 436 |
gate: ProductGateConfig,
|
| 437 |
) -> Tuple[bool, bool, Optional[RiskScore], int, float, float]:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 438 |
global _PATH_CHECK_N, _PATH_CHECK_DIVERGE
|
| 439 |
obs, info = env.reset(options={"context": ctx})
|
| 440 |
initial_belief = _initial_belief_from_info(info, ctx.config)
|
|
|
|
| 458 |
if "believed_p" in info:
|
| 459 |
believed_p = float(info["believed_p"])
|
| 460 |
elif "mean_belief" in info:
|
|
|
|
| 461 |
believed_p = float(info["mean_belief"])
|
| 462 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 463 |
loop_product, loop_elevated = product, elevated
|
| 464 |
rs = None
|
| 465 |
try:
|
|
|
|
| 470 |
_PATH_CHECK_DIVERGE += 1
|
| 471 |
except Exception:
|
| 472 |
pass
|
|
|
|
| 473 |
return loop_product, loop_elevated, rs, ep_len, believed_p, initial_belief
|
| 474 |
|
| 475 |
|
|
|
|
| 478 |
ctx: EpisodeContext,
|
| 479 |
gate: ProductGateConfig,
|
| 480 |
) -> Tuple[bool, bool, Optional[RiskScore], int, float, float]:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 481 |
obs, info = env.reset(options={"context": ctx})
|
| 482 |
initial_belief = _initial_belief_from_info(info, ctx.config)
|
| 483 |
base = env.env if hasattr(env, "env") else env
|
|
|
|
| 494 |
rs = compute_risk_score(ctx.obs, ctx.forecast, ctx.config)
|
| 495 |
except Exception:
|
| 496 |
pass
|
|
|
|
| 497 |
return product, elevated, rs, 1, believed_p, initial_belief
|
| 498 |
|
| 499 |
|
|
|
|
| 512 |
model: Any = None,
|
| 513 |
env: Any = None,
|
| 514 |
) -> Tuple[List[EvalDayRecord], EvalMetrics]:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 515 |
if mode not in ("checkpoint", "zero_inspect"):
|
| 516 |
raise ValueError(
|
| 517 |
f"evaluate_multi_zone_days only supports checkpoint/zero_inspect "
|
|
|
|
| 650 |
model: Any = None,
|
| 651 |
env: Any = None,
|
| 652 |
) -> Tuple[List[EvalDayRecord], EvalMetrics]:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 653 |
_path_check_reset()
|
| 654 |
records: List[EvalDayRecord] = []
|
| 655 |
m = EvalMetrics()
|
|
|
|
| 670 |
ld, lf = impact_store.labels_for_day(zid, day)
|
| 671 |
event_d, event_f = bool(ld), bool(lf)
|
| 672 |
if event_d or event_f:
|
|
|
|
| 673 |
gt_source = "l1"
|
| 674 |
else:
|
|
|
|
|
|
|
| 675 |
gt_source = "unlabeled"
|
| 676 |
except Exception as e:
|
| 677 |
logger.warning("L1 query failed %s %s: %s", zid, day, e)
|
|
|
|
| 710 |
drought_risk = float(rs.drought_risk) if rs is not None else 0.0
|
| 711 |
flood_risk = float(rs.flood_risk) if rs is not None else 0.0
|
| 712 |
|
|
|
|
|
|
|
| 713 |
belief_delta = float(believed_p) - float(initial_belief)
|
| 714 |
belief_raised = bool(belief_delta > BELIEF_RAISE_EPS)
|
| 715 |
|
|
|
|
|
|
|
| 716 |
if gt_source == "l1":
|
|
|
|
| 717 |
if product:
|
| 718 |
m.tp += 1
|
| 719 |
else:
|
|
|
|
| 731 |
if belief_raised:
|
| 732 |
m.n_unlabeled_belief_raised += 1
|
| 733 |
m.belief_deltas_unlabeled.append(belief_delta)
|
|
|
|
| 734 |
|
| 735 |
m.n_days += 1
|
| 736 |
if product:
|
|
|
|
| 958 |
except Exception as e:
|
| 959 |
logger.warning("could not cross-check policy obs space: %s", e)
|
| 960 |
|
|
|
|
|
|
|
| 961 |
if (
|
| 962 |
args.mode in ("checkpoint", "zero_inspect")
|
| 963 |
and int(args.n_zones) > 1
|
|
|
|
| 993 |
)
|
| 994 |
_print_metrics(args.mode, metrics)
|
| 995 |
|
|
|
|
| 996 |
by_zone: Dict[str, EvalMetrics] = {}
|
| 997 |
for r in records:
|
| 998 |
zm = by_zone.setdefault(r.zone_id, EvalMetrics())
|
|
|
|
| 1069 |
assert rs is not None
|
| 1070 |
print(f" drought scorer product={product} elevated={elevated} "
|
| 1071 |
f"alert={rs.alert_level.value} drought_risk={rs.drought_risk:.3f}")
|
|
|
|
| 1072 |
m = EvalMetrics(n_days=4, tp=1, fp=1, fn=1, tn=1, n_l1=2, n_product=2)
|
| 1073 |
assert abs((m.precision or 0) - 0.5) < 1e-9
|
| 1074 |
assert abs((m.recall or 0) - 0.5) < 1e-9
|
| 1075 |
print(" metrics arithmetic OK")
|
| 1076 |
|
|
|
|
| 1077 |
m_pos = EvalMetrics(n_days=10, tp=7, fn=3, n_l1=10, fp=0, tn=0)
|
| 1078 |
assert m_pos.precision is None, m_pos.precision
|
| 1079 |
assert m_pos.f1 is None
|
|
|
|
| 1082 |
assert m_pos.belief_f1 is None
|
| 1083 |
print(" positive-only null precision OK")
|
| 1084 |
|
|
|
|
| 1085 |
m_u = EvalMetrics(
|
| 1086 |
n_unlabeled=20,
|
| 1087 |
n_unlabeled_product=8,
|
|
|
|
| 1091 |
assert abs((m_u.belief_unlabeled_raise_rate or 0) - 0.55) < 1e-9
|
| 1092 |
print(" unlabeled rates OK")
|
| 1093 |
|
|
|
|
| 1094 |
def _f(x):
|
| 1095 |
return f"{x:.3f}" if x is not None else " - "
|
| 1096 |
assert _f(None) == " - "
|
|
|
|
| 1105 |
if len(sys.argv) == 1:
|
| 1106 |
_self_test()
|
| 1107 |
else:
|
| 1108 |
+
raise SystemExit(main())
|