Add LCB dominance safety calibration metrics
Browse files
workspace/scripts/eval_dominance_selector.py
CHANGED
|
@@ -17,7 +17,11 @@ if str(PROJECT_ROOT) not in sys.path:
|
|
| 17 |
|
| 18 |
import torch # noqa: E402
|
| 19 |
|
| 20 |
-
from cil.metrics import
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
from cil.models import CTTConfig, ChartEncoder, TangentNormalizer, UtilityEnergy # noqa: E402
|
| 22 |
from scripts.eval_ctt_generated_rollout import load_chart_items # noqa: E402
|
| 23 |
|
|
@@ -103,17 +107,29 @@ def main(argv: list[str] | None = None) -> int:
|
|
| 103 |
else float(args.tau)
|
| 104 |
)
|
| 105 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
evaluated_cases = [
|
| 107 |
_evaluate_case(
|
| 108 |
-
|
| 109 |
residual_quantile=residual_quantile,
|
| 110 |
tau=tau,
|
|
|
|
| 111 |
)
|
| 112 |
-
for
|
| 113 |
]
|
| 114 |
calibration_eval_cases = [
|
| 115 |
-
_evaluate_case(
|
| 116 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
]
|
| 118 |
metric_names = sorted(
|
| 119 |
{
|
|
@@ -155,8 +171,12 @@ def main(argv: list[str] | None = None) -> int:
|
|
| 155 |
"eval_target_split_hash": eval_index.get("split_hash"),
|
| 156 |
"num_calibration_rows": len(calibration_cases),
|
| 157 |
"num_eval_rows": len(evaluated_cases),
|
| 158 |
-
"calibration_summary":
|
| 159 |
-
"eval_summary":
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
"summary": summary,
|
| 161 |
"rows": evaluated_cases,
|
| 162 |
}
|
|
@@ -292,14 +312,23 @@ def _dominance_case(
|
|
| 292 |
if chart_id not in charts:
|
| 293 |
raise KeyError(f"chart_id {chart_id!r} not found in target index")
|
| 294 |
predicted_scores = scorer.candidate_scores(row, charts[chart_id], k=k)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 295 |
if not generated_utilities or not predicted_scores:
|
| 296 |
raise ValueError("dominance evaluation requires generated utilities and predicted scores")
|
|
|
|
| 297 |
top_index = max(range(len(predicted_scores)), key=lambda index: predicted_scores[index])
|
| 298 |
base_score = scorer.base_score(row, charts[chart_id])
|
| 299 |
base_utility = float(row["base_utility"])
|
| 300 |
base_success = float(bool(row.get("base_success", False)))
|
|
|
|
| 301 |
selected_generated_utility = generated_utilities[top_index]
|
| 302 |
selected_generated_success = candidate_success[top_index]
|
|
|
|
|
|
|
|
|
|
| 303 |
proposal_oracle_utility = max(generated_utilities)
|
| 304 |
proposal_oracle_success = float(any(candidate_success))
|
| 305 |
hidden = [float(value) for value in row.get("hidden_chart_utilities", [])]
|
|
@@ -315,12 +344,20 @@ def _dominance_case(
|
|
| 315 |
"top_index": top_index,
|
| 316 |
"base_predicted_score": base_score,
|
| 317 |
"top_predicted_score": predicted_scores[top_index],
|
|
|
|
| 318 |
"predicted_margin": predicted_margin,
|
| 319 |
"measured_margin": measured_margin,
|
| 320 |
"base_utility": base_utility,
|
| 321 |
"base_success": base_success,
|
|
|
|
|
|
|
| 322 |
"top_generated_utility": selected_generated_utility,
|
| 323 |
"top_generated_success": selected_generated_success,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 324 |
"proposal_oracle_utility": proposal_oracle_utility,
|
| 325 |
"proposal_oracle_success": proposal_oracle_success,
|
| 326 |
"hidden_chart_oracle_utility": hidden_oracle_utility,
|
|
@@ -329,7 +366,13 @@ def _dominance_case(
|
|
| 329 |
}
|
| 330 |
|
| 331 |
|
| 332 |
-
def _evaluate_case(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 333 |
lcb = float(case["predicted_margin"]) - float(residual_quantile)
|
| 334 |
execute_generated = lcb > float(tau)
|
| 335 |
selected_utility = (
|
|
@@ -342,6 +385,11 @@ def _evaluate_case(case: dict[str, Any], *, residual_quantile: float, tau: float
|
|
| 342 |
proposal_oracle_success = float(case["proposal_oracle_success"])
|
| 343 |
hidden_utility = float(case["hidden_chart_oracle_utility"])
|
| 344 |
hidden_success = float(case["hidden_chart_oracle_success"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 345 |
output = dict(case)
|
| 346 |
output.update(
|
| 347 |
{
|
|
@@ -352,6 +400,10 @@ def _evaluate_case(case: dict[str, Any], *, residual_quantile: float, tau: float
|
|
| 352 |
"fallback_rate": float(not execute_generated),
|
| 353 |
"selected_utility": selected_utility,
|
| 354 |
"selected_success": selected_success,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 355 |
"selected_utility_gain_over_base": selected_utility - float(case["base_utility"]),
|
| 356 |
"selected_success_gain_over_base": selected_success - float(case["base_success"]),
|
| 357 |
"selector_regret": max(0.0, proposal_oracle_utility - selected_utility),
|
|
@@ -368,6 +420,8 @@ def _evaluate_case(case: dict[str, Any], *, residual_quantile: float, tau: float
|
|
| 368 |
else math.nan,
|
| 369 |
}
|
| 370 |
)
|
|
|
|
|
|
|
| 371 |
return output
|
| 372 |
|
| 373 |
|
|
@@ -434,6 +488,16 @@ def _simple_summary(rows: list[dict[str, Any]]) -> dict[str, float | None]:
|
|
| 434 |
"selected_success_gain_over_base",
|
| 435 |
"coverage",
|
| 436 |
"fallback_rate",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 437 |
"outcome_ptr",
|
| 438 |
"success_support_gap",
|
| 439 |
"success_selector_gap",
|
|
@@ -447,6 +511,96 @@ def _simple_summary(rows: list[dict[str, Any]]) -> dict[str, float | None]:
|
|
| 447 |
return {key: _mean([row.get(key) for row in rows]) for key in keys}
|
| 448 |
|
| 449 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 450 |
def _group_means(
|
| 451 |
rows: list[dict[str, Any]],
|
| 452 |
key: str,
|
|
@@ -475,19 +629,71 @@ def _mean(values: list[Any]) -> float | None:
|
|
| 475 |
return sum(clean) / len(clean) if clean else None
|
| 476 |
|
| 477 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 478 |
def _table(metrics: dict[str, Any]) -> str:
|
| 479 |
summary = metrics["eval_summary"]
|
| 480 |
lines = [
|
| 481 |
"% Auto-generated by scripts/eval_dominance_selector.py",
|
| 482 |
-
"\\begin{tabular}{
|
| 483 |
"\\toprule",
|
| 484 |
-
"Rows & Coverage & Fallback & Base succ. & Selected succ. & Oracle succ. & OutcomePTR & Succ. support gap & Succ. selector gap \\\\",
|
| 485 |
"\\midrule",
|
| 486 |
f"{metrics['num_eval_rows']} & {_fmt(summary.get('coverage'))} & "
|
| 487 |
-
f"{_fmt(summary.get('fallback_rate'))} & {_fmt(summary.get('
|
|
|
|
| 488 |
f"{_fmt(summary.get('selected_success'))} & {_fmt(summary.get('proposal_oracle_success'))} & "
|
| 489 |
f"{_fmt(summary.get('outcome_ptr'))} & {_fmt(summary.get('success_support_gap'))} & "
|
| 490 |
-
f"{_fmt(summary.get('success_selector_gap'))}
|
|
|
|
| 491 |
"\\bottomrule",
|
| 492 |
"\\end{tabular}",
|
| 493 |
]
|
|
@@ -508,18 +714,22 @@ def _report(metrics: dict[str, Any]) -> str:
|
|
| 508 |
"",
|
| 509 |
"The threshold is fit on calibration rows only. Eval outcomes are used only for reporting.",
|
| 510 |
"",
|
| 511 |
-
"| Split | Coverage | Fallback | Base success | Selected success | Proposal oracle | OutcomePTR | Success support gap | Success selector gap |",
|
| 512 |
-
"| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |",
|
| 513 |
f"| calibration | {_fmt(calibration.get('coverage'))} | {_fmt(calibration.get('fallback_rate'))} | "
|
|
|
|
| 514 |
f"{_fmt(calibration.get('base_success'))} | {_fmt(calibration.get('selected_success'))} | "
|
| 515 |
f"{_fmt(calibration.get('proposal_oracle_success'))} | {_fmt(calibration.get('outcome_ptr'))} | "
|
| 516 |
-
f"{_fmt(calibration.get('success_support_gap'))} | {_fmt(calibration.get('success_selector_gap'))} |"
|
|
|
|
| 517 |
f"| eval | {_fmt(summary.get('coverage'))} | {_fmt(summary.get('fallback_rate'))} | "
|
|
|
|
| 518 |
f"{_fmt(summary.get('base_success'))} | {_fmt(summary.get('selected_success'))} | "
|
| 519 |
f"{_fmt(summary.get('proposal_oracle_success'))} | {_fmt(summary.get('outcome_ptr'))} | "
|
| 520 |
-
f"{_fmt(summary.get('success_support_gap'))} | {_fmt(summary.get('success_selector_gap'))} |"
|
|
|
|
| 521 |
"",
|
| 522 |
-
"This is a calibrated fallback diagnostic
|
| 523 |
]
|
| 524 |
return "\n".join(lines)
|
| 525 |
|
|
|
|
| 17 |
|
| 18 |
import torch # noqa: E402
|
| 19 |
|
| 20 |
+
from cil.metrics import ( # noqa: E402
|
| 21 |
+
macro_micro_summary,
|
| 22 |
+
outcome_safety_violation,
|
| 23 |
+
pairwise_causal_dominance_ece,
|
| 24 |
+
)
|
| 25 |
from cil.models import CTTConfig, ChartEncoder, TangentNormalizer, UtilityEnergy # noqa: E402
|
| 26 |
from scripts.eval_ctt_generated_rollout import load_chart_items # noqa: E402
|
| 27 |
|
|
|
|
| 107 |
else float(args.tau)
|
| 108 |
)
|
| 109 |
|
| 110 |
+
eval_cases = [
|
| 111 |
+
_dominance_case(row, eval_charts, scorer=calibrator, k=args.k)
|
| 112 |
+
for row in eval_rows
|
| 113 |
+
]
|
| 114 |
+
eval_pairwise = _pairwise_calibration_summary(eval_cases)
|
| 115 |
+
calibration_pairwise = _pairwise_calibration_summary(calibration_cases)
|
| 116 |
evaluated_cases = [
|
| 117 |
_evaluate_case(
|
| 118 |
+
case,
|
| 119 |
residual_quantile=residual_quantile,
|
| 120 |
tau=tau,
|
| 121 |
+
pairwise_calibration=eval_pairwise["rows"].get(index, {}),
|
| 122 |
)
|
| 123 |
+
for index, case in enumerate(eval_cases)
|
| 124 |
]
|
| 125 |
calibration_eval_cases = [
|
| 126 |
+
_evaluate_case(
|
| 127 |
+
case,
|
| 128 |
+
residual_quantile=residual_quantile,
|
| 129 |
+
tau=tau,
|
| 130 |
+
pairwise_calibration=calibration_pairwise["rows"].get(index, {}),
|
| 131 |
+
)
|
| 132 |
+
for index, case in enumerate(calibration_cases)
|
| 133 |
]
|
| 134 |
metric_names = sorted(
|
| 135 |
{
|
|
|
|
| 171 |
"eval_target_split_hash": eval_index.get("split_hash"),
|
| 172 |
"num_calibration_rows": len(calibration_cases),
|
| 173 |
"num_eval_rows": len(evaluated_cases),
|
| 174 |
+
"calibration_summary": _summary_with_pairwise(calibration_eval_cases, calibration_pairwise),
|
| 175 |
+
"eval_summary": _summary_with_pairwise(evaluated_cases, eval_pairwise),
|
| 176 |
+
"pairwise_causal_calibration": {
|
| 177 |
+
"calibration": _pairwise_calibration_global(calibration_pairwise),
|
| 178 |
+
"eval": _pairwise_calibration_global(eval_pairwise),
|
| 179 |
+
},
|
| 180 |
"summary": summary,
|
| 181 |
"rows": evaluated_cases,
|
| 182 |
}
|
|
|
|
| 312 |
if chart_id not in charts:
|
| 313 |
raise KeyError(f"chart_id {chart_id!r} not found in target index")
|
| 314 |
predicted_scores = scorer.candidate_scores(row, charts[chart_id], k=k)
|
| 315 |
+
count = min(len(generated_utilities), len(predicted_scores), len(candidate_success))
|
| 316 |
+
generated_utilities = generated_utilities[:count]
|
| 317 |
+
predicted_scores = predicted_scores[:count]
|
| 318 |
+
candidate_success = candidate_success[:count]
|
| 319 |
if not generated_utilities or not predicted_scores:
|
| 320 |
raise ValueError("dominance evaluation requires generated utilities and predicted scores")
|
| 321 |
+
candidate_safety_labels = _candidate_safety_labels(row, count=count)
|
| 322 |
top_index = max(range(len(predicted_scores)), key=lambda index: predicted_scores[index])
|
| 323 |
base_score = scorer.base_score(row, charts[chart_id])
|
| 324 |
base_utility = float(row["base_utility"])
|
| 325 |
base_success = float(bool(row.get("base_success", False)))
|
| 326 |
+
base_safety_label = _base_safety_label(row)
|
| 327 |
selected_generated_utility = generated_utilities[top_index]
|
| 328 |
selected_generated_success = candidate_success[top_index]
|
| 329 |
+
top_safety_label = (
|
| 330 |
+
candidate_safety_labels[top_index] if top_index < len(candidate_safety_labels) else None
|
| 331 |
+
)
|
| 332 |
proposal_oracle_utility = max(generated_utilities)
|
| 333 |
proposal_oracle_success = float(any(candidate_success))
|
| 334 |
hidden = [float(value) for value in row.get("hidden_chart_utilities", [])]
|
|
|
|
| 344 |
"top_index": top_index,
|
| 345 |
"base_predicted_score": base_score,
|
| 346 |
"top_predicted_score": predicted_scores[top_index],
|
| 347 |
+
"predicted_scores": predicted_scores,
|
| 348 |
"predicted_margin": predicted_margin,
|
| 349 |
"measured_margin": measured_margin,
|
| 350 |
"base_utility": base_utility,
|
| 351 |
"base_success": base_success,
|
| 352 |
+
"base_safety_label_known": float(base_safety_label is not None),
|
| 353 |
+
"base_unsafe_known": _label_to_float(base_safety_label),
|
| 354 |
"top_generated_utility": selected_generated_utility,
|
| 355 |
"top_generated_success": selected_generated_success,
|
| 356 |
+
"top_candidate_safety_label_known": float(top_safety_label is not None),
|
| 357 |
+
"top_candidate_unsafe_known": _label_to_float(top_safety_label),
|
| 358 |
+
"candidate_safety_label_coverage": _safety_label_coverage(candidate_safety_labels),
|
| 359 |
+
"candidate_unsafe_rate_known": _unsafe_rate_known(candidate_safety_labels),
|
| 360 |
+
"generated_utilities": generated_utilities,
|
| 361 |
"proposal_oracle_utility": proposal_oracle_utility,
|
| 362 |
"proposal_oracle_success": proposal_oracle_success,
|
| 363 |
"hidden_chart_oracle_utility": hidden_oracle_utility,
|
|
|
|
| 366 |
}
|
| 367 |
|
| 368 |
|
| 369 |
+
def _evaluate_case(
|
| 370 |
+
case: dict[str, Any],
|
| 371 |
+
*,
|
| 372 |
+
residual_quantile: float,
|
| 373 |
+
tau: float,
|
| 374 |
+
pairwise_calibration: dict[str, Any] | None = None,
|
| 375 |
+
) -> dict[str, Any]:
|
| 376 |
lcb = float(case["predicted_margin"]) - float(residual_quantile)
|
| 377 |
execute_generated = lcb > float(tau)
|
| 378 |
selected_utility = (
|
|
|
|
| 385 |
proposal_oracle_success = float(case["proposal_oracle_success"])
|
| 386 |
hidden_utility = float(case["hidden_chart_oracle_utility"])
|
| 387 |
hidden_success = float(case["hidden_chart_oracle_success"])
|
| 388 |
+
selected_safety = (
|
| 389 |
+
_float_to_label(case.get("top_candidate_unsafe_known"))
|
| 390 |
+
if execute_generated
|
| 391 |
+
else _float_to_label(case.get("base_unsafe_known"))
|
| 392 |
+
)
|
| 393 |
output = dict(case)
|
| 394 |
output.update(
|
| 395 |
{
|
|
|
|
| 400 |
"fallback_rate": float(not execute_generated),
|
| 401 |
"selected_utility": selected_utility,
|
| 402 |
"selected_success": selected_success,
|
| 403 |
+
"selected_safety_label_known": float(selected_safety is not None),
|
| 404 |
+
"selected_unsafe_known": _label_to_float(selected_safety),
|
| 405 |
+
"unsafe_execution_label_known": float(selected_safety is not None),
|
| 406 |
+
"unsafe_execution_known": _label_to_float(selected_safety),
|
| 407 |
"selected_utility_gain_over_base": selected_utility - float(case["base_utility"]),
|
| 408 |
"selected_success_gain_over_base": selected_success - float(case["base_success"]),
|
| 409 |
"selector_regret": max(0.0, proposal_oracle_utility - selected_utility),
|
|
|
|
| 420 |
else math.nan,
|
| 421 |
}
|
| 422 |
)
|
| 423 |
+
if pairwise_calibration is not None:
|
| 424 |
+
output.update(_pairwise_calibration_scalars(pairwise_calibration))
|
| 425 |
return output
|
| 426 |
|
| 427 |
|
|
|
|
| 488 |
"selected_success_gain_over_base",
|
| 489 |
"coverage",
|
| 490 |
"fallback_rate",
|
| 491 |
+
"base_safety_label_known",
|
| 492 |
+
"base_unsafe_known",
|
| 493 |
+
"candidate_safety_label_coverage",
|
| 494 |
+
"candidate_unsafe_rate_known",
|
| 495 |
+
"top_candidate_safety_label_known",
|
| 496 |
+
"top_candidate_unsafe_known",
|
| 497 |
+
"selected_safety_label_known",
|
| 498 |
+
"selected_unsafe_known",
|
| 499 |
+
"unsafe_execution_label_known",
|
| 500 |
+
"unsafe_execution_known",
|
| 501 |
"outcome_ptr",
|
| 502 |
"success_support_gap",
|
| 503 |
"success_selector_gap",
|
|
|
|
| 511 |
return {key: _mean([row.get(key) for row in rows]) for key in keys}
|
| 512 |
|
| 513 |
|
| 514 |
+
def _pairwise_calibration_summary(cases: list[dict[str, Any]], *, n_bins: int = 10) -> dict[str, Any]:
|
| 515 |
+
bins = [
|
| 516 |
+
{
|
| 517 |
+
"count": 0,
|
| 518 |
+
"accuracy_sum": 0.0,
|
| 519 |
+
"confidence_sum": 0.0,
|
| 520 |
+
"lower": index / n_bins,
|
| 521 |
+
"upper": (index + 1) / n_bins,
|
| 522 |
+
}
|
| 523 |
+
for index in range(n_bins)
|
| 524 |
+
]
|
| 525 |
+
rows: dict[int, dict[str, Any]] = {}
|
| 526 |
+
total_pairs = 0
|
| 527 |
+
correct_sum = 0.0
|
| 528 |
+
confidence_sum = 0.0
|
| 529 |
+
for index, case in enumerate(cases):
|
| 530 |
+
row_metrics = pairwise_causal_dominance_ece(
|
| 531 |
+
case.get("predicted_scores", []),
|
| 532 |
+
case.get("generated_utilities", []),
|
| 533 |
+
n_bins=n_bins,
|
| 534 |
+
)
|
| 535 |
+
rows[index] = row_metrics
|
| 536 |
+
row_pairs = int(row_metrics.get("num_pairs") or 0)
|
| 537 |
+
if row_pairs <= 0:
|
| 538 |
+
continue
|
| 539 |
+
total_pairs += row_pairs
|
| 540 |
+
correct_sum += float(row_metrics.get("accuracy") or 0.0) * row_pairs
|
| 541 |
+
confidence_sum += float(row_metrics.get("mean_confidence") or 0.0) * row_pairs
|
| 542 |
+
for bin_index, row_bin in enumerate(row_metrics.get("bins", [])):
|
| 543 |
+
if bin_index >= len(bins):
|
| 544 |
+
break
|
| 545 |
+
count = int(row_bin.get("count") or 0)
|
| 546 |
+
bins[bin_index]["count"] += count
|
| 547 |
+
bins[bin_index]["accuracy_sum"] += float(row_bin.get("accuracy") or 0.0) * count
|
| 548 |
+
bins[bin_index]["confidence_sum"] += float(row_bin.get("confidence") or 0.0) * count
|
| 549 |
+
|
| 550 |
+
ece = 0.0
|
| 551 |
+
rendered_bins: list[dict[str, float | int]] = []
|
| 552 |
+
for bucket in bins:
|
| 553 |
+
count = int(bucket["count"])
|
| 554 |
+
accuracy = bucket["accuracy_sum"] / count if count else 0.0
|
| 555 |
+
confidence = bucket["confidence_sum"] / count if count else 0.0
|
| 556 |
+
if total_pairs:
|
| 557 |
+
ece += (count / total_pairs) * abs(accuracy - confidence)
|
| 558 |
+
rendered_bins.append(
|
| 559 |
+
{
|
| 560 |
+
"lower": float(bucket["lower"]),
|
| 561 |
+
"upper": float(bucket["upper"]),
|
| 562 |
+
"count": count,
|
| 563 |
+
"accuracy": accuracy,
|
| 564 |
+
"confidence": confidence,
|
| 565 |
+
"abs_gap": abs(accuracy - confidence),
|
| 566 |
+
}
|
| 567 |
+
)
|
| 568 |
+
return {
|
| 569 |
+
"n_bins": int(n_bins),
|
| 570 |
+
"num_rows": len(cases),
|
| 571 |
+
"ece": ece if total_pairs else math.nan,
|
| 572 |
+
"num_pairs": int(total_pairs),
|
| 573 |
+
"accuracy": correct_sum / total_pairs if total_pairs else math.nan,
|
| 574 |
+
"mean_confidence": confidence_sum / total_pairs if total_pairs else math.nan,
|
| 575 |
+
"bins": rendered_bins,
|
| 576 |
+
"rows": rows,
|
| 577 |
+
}
|
| 578 |
+
|
| 579 |
+
|
| 580 |
+
def _pairwise_calibration_scalars(calibration: dict[str, Any]) -> dict[str, float]:
|
| 581 |
+
return {
|
| 582 |
+
"pairwise_causal_calibration_ece": _finite_or_nan(calibration.get("ece")),
|
| 583 |
+
"pairwise_causal_calibration_pairs": float(calibration.get("num_pairs") or 0),
|
| 584 |
+
"pairwise_causal_calibration_accuracy": _finite_or_nan(calibration.get("accuracy")),
|
| 585 |
+
"pairwise_causal_calibration_confidence": _finite_or_nan(
|
| 586 |
+
calibration.get("mean_confidence")
|
| 587 |
+
),
|
| 588 |
+
}
|
| 589 |
+
|
| 590 |
+
|
| 591 |
+
def _pairwise_calibration_global(calibration: dict[str, Any]) -> dict[str, Any]:
|
| 592 |
+
return {key: value for key, value in calibration.items() if key != "rows"}
|
| 593 |
+
|
| 594 |
+
|
| 595 |
+
def _summary_with_pairwise(
|
| 596 |
+
rows: list[dict[str, Any]],
|
| 597 |
+
pairwise_calibration: dict[str, Any],
|
| 598 |
+
) -> dict[str, float | None]:
|
| 599 |
+
summary = _simple_summary(rows)
|
| 600 |
+
summary.update(_pairwise_calibration_scalars(pairwise_calibration))
|
| 601 |
+
return summary
|
| 602 |
+
|
| 603 |
+
|
| 604 |
def _group_means(
|
| 605 |
rows: list[dict[str, Any]],
|
| 606 |
key: str,
|
|
|
|
| 629 |
return sum(clean) / len(clean) if clean else None
|
| 630 |
|
| 631 |
|
| 632 |
+
def _finite_or_nan(value: Any) -> float:
|
| 633 |
+
return float(value) if isinstance(value, (int, float)) and math.isfinite(float(value)) else math.nan
|
| 634 |
+
|
| 635 |
+
|
| 636 |
+
def _base_safety_label(row: dict[str, Any]) -> bool | None:
|
| 637 |
+
label = outcome_safety_violation(row.get("base_outcome"))
|
| 638 |
+
if label is not None:
|
| 639 |
+
return label
|
| 640 |
+
if "base_safety_violation" in row:
|
| 641 |
+
return outcome_safety_violation({"safety_violation": row.get("base_safety_violation")})
|
| 642 |
+
return None
|
| 643 |
+
|
| 644 |
+
|
| 645 |
+
def _candidate_safety_labels(row: dict[str, Any], *, count: int) -> list[bool | None]:
|
| 646 |
+
outcomes = row.get("candidate_outcomes", [])
|
| 647 |
+
flags = row.get("candidate_safety_violation", [])
|
| 648 |
+
labels: list[bool | None] = []
|
| 649 |
+
for index in range(count):
|
| 650 |
+
label = None
|
| 651 |
+
if isinstance(outcomes, list) and index < len(outcomes):
|
| 652 |
+
label = outcome_safety_violation(outcomes[index])
|
| 653 |
+
if label is None and isinstance(flags, list) and index < len(flags):
|
| 654 |
+
label = outcome_safety_violation({"safety_violation": flags[index]})
|
| 655 |
+
labels.append(label)
|
| 656 |
+
return labels
|
| 657 |
+
|
| 658 |
+
|
| 659 |
+
def _safety_label_coverage(labels: list[bool | None]) -> float:
|
| 660 |
+
if not labels:
|
| 661 |
+
return math.nan
|
| 662 |
+
return sum(label is not None for label in labels) / len(labels)
|
| 663 |
+
|
| 664 |
+
|
| 665 |
+
def _unsafe_rate_known(labels: list[bool | None]) -> float:
|
| 666 |
+
known = [label for label in labels if label is not None]
|
| 667 |
+
if not known:
|
| 668 |
+
return math.nan
|
| 669 |
+
return sum(float(label) for label in known) / len(known)
|
| 670 |
+
|
| 671 |
+
|
| 672 |
+
def _label_to_float(label: bool | None) -> float:
|
| 673 |
+
return math.nan if label is None else float(label)
|
| 674 |
+
|
| 675 |
+
|
| 676 |
+
def _float_to_label(value: Any) -> bool | None:
|
| 677 |
+
if not isinstance(value, (int, float)) or not math.isfinite(float(value)):
|
| 678 |
+
return None
|
| 679 |
+
return bool(float(value))
|
| 680 |
+
|
| 681 |
+
|
| 682 |
def _table(metrics: dict[str, Any]) -> str:
|
| 683 |
summary = metrics["eval_summary"]
|
| 684 |
lines = [
|
| 685 |
"% Auto-generated by scripts/eval_dominance_selector.py",
|
| 686 |
+
"\\begin{tabular}{lrrrrrrrrrr}",
|
| 687 |
"\\toprule",
|
| 688 |
+
"Rows & Coverage & Fallback & Unsafe exec. & Base succ. & Selected succ. & Oracle succ. & OutcomePTR & Succ. support gap & Succ. selector gap & Cal. ECE \\\\",
|
| 689 |
"\\midrule",
|
| 690 |
f"{metrics['num_eval_rows']} & {_fmt(summary.get('coverage'))} & "
|
| 691 |
+
f"{_fmt(summary.get('fallback_rate'))} & {_fmt(summary.get('unsafe_execution_known'))} & "
|
| 692 |
+
f"{_fmt(summary.get('base_success'))} & "
|
| 693 |
f"{_fmt(summary.get('selected_success'))} & {_fmt(summary.get('proposal_oracle_success'))} & "
|
| 694 |
f"{_fmt(summary.get('outcome_ptr'))} & {_fmt(summary.get('success_support_gap'))} & "
|
| 695 |
+
f"{_fmt(summary.get('success_selector_gap'))} & "
|
| 696 |
+
f"{_fmt(summary.get('pairwise_causal_calibration_ece'))} \\\\",
|
| 697 |
"\\bottomrule",
|
| 698 |
"\\end{tabular}",
|
| 699 |
]
|
|
|
|
| 714 |
"",
|
| 715 |
"The threshold is fit on calibration rows only. Eval outcomes are used only for reporting.",
|
| 716 |
"",
|
| 717 |
+
"| Split | Coverage | Fallback | Unsafe exec. | Safety label coverage | Base success | Selected success | Proposal oracle | OutcomePTR | Success support gap | Success selector gap | Calibration ECE |",
|
| 718 |
+
"| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |",
|
| 719 |
f"| calibration | {_fmt(calibration.get('coverage'))} | {_fmt(calibration.get('fallback_rate'))} | "
|
| 720 |
+
f"{_fmt(calibration.get('unsafe_execution_known'))} | {_fmt(calibration.get('unsafe_execution_label_known'))} | "
|
| 721 |
f"{_fmt(calibration.get('base_success'))} | {_fmt(calibration.get('selected_success'))} | "
|
| 722 |
f"{_fmt(calibration.get('proposal_oracle_success'))} | {_fmt(calibration.get('outcome_ptr'))} | "
|
| 723 |
+
f"{_fmt(calibration.get('success_support_gap'))} | {_fmt(calibration.get('success_selector_gap'))} | "
|
| 724 |
+
f"{_fmt(calibration.get('pairwise_causal_calibration_ece'))} |",
|
| 725 |
f"| eval | {_fmt(summary.get('coverage'))} | {_fmt(summary.get('fallback_rate'))} | "
|
| 726 |
+
f"{_fmt(summary.get('unsafe_execution_known'))} | {_fmt(summary.get('unsafe_execution_label_known'))} | "
|
| 727 |
f"{_fmt(summary.get('base_success'))} | {_fmt(summary.get('selected_success'))} | "
|
| 728 |
f"{_fmt(summary.get('proposal_oracle_success'))} | {_fmt(summary.get('outcome_ptr'))} | "
|
| 729 |
+
f"{_fmt(summary.get('success_support_gap'))} | {_fmt(summary.get('success_selector_gap'))} | "
|
| 730 |
+
f"{_fmt(summary.get('pairwise_causal_calibration_ece'))} |",
|
| 731 |
"",
|
| 732 |
+
"This is a calibrated fallback diagnostic over already measured candidates; unsafe rates use available action-bound safety labels only.",
|
| 733 |
]
|
| 734 |
return "\n".join(lines)
|
| 735 |
|