DHDRL commited on
Commit
6555f13
·
verified ·
1 Parent(s): b91a0db

Update evaluate_checkpoint_real.py

Browse files
Files changed (1) hide show
  1. 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
- # PATH_CHECK: count loop_product vs fresh-scorer product across all episodes
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())