expand CTT proxy comparison metrics
Browse files
scripts/build_ctt_proxy_comparison.py
CHANGED
|
@@ -43,6 +43,11 @@ def main(argv: list[str] | None = None) -> int:
|
|
| 43 |
parser = argparse.ArgumentParser(description="Build CTT validation proxy comparison table.")
|
| 44 |
parser.add_argument("--out-dir", type=Path, default=Path("runs/ctt_val_proxy_comparison"))
|
| 45 |
parser.add_argument("--safety-slack", type=float, default=0.01)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
args = parser.parse_args(argv)
|
| 47 |
|
| 48 |
rows = [
|
|
@@ -74,6 +79,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
| 74 |
out_dir.mkdir(parents=True, exist_ok=True)
|
| 75 |
payload = {
|
| 76 |
"report_type": "ctt_val_proxy_comparison",
|
|
|
|
| 77 |
"baseline": "local_atlas",
|
| 78 |
"safety_slack": args.safety_slack,
|
| 79 |
"rows": rows,
|
|
@@ -81,10 +87,21 @@ def main(argv: list[str] | None = None) -> int:
|
|
| 81 |
"split_hash": _first(rows, "split_hash"),
|
| 82 |
}
|
| 83 |
(out_dir / "metrics.json").write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n")
|
| 84 |
-
(out_dir / "metrics_by_task.json").write_text(
|
| 85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
(out_dir / "table.tex").write_text(_table(rows) + "\n")
|
| 87 |
-
(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
(out_dir / "train.log").write_text("comparison artifact; no training\n")
|
| 89 |
(out_dir / "eval.log").write_text(
|
| 90 |
"\n".join(f"{row['method']}: {row['run_path']}" for row in rows) + "\n"
|
|
@@ -113,7 +130,10 @@ def _row(method: str, run_dirs: list[Path]) -> dict[str, Any]:
|
|
| 113 |
"negative_near_0p20": _mean_across(summaries, "negative_near_at_16_thr_0p20"),
|
| 114 |
"negative_near_0p40": _mean_across(summaries, "negative_near_at_16_thr_0p40"),
|
| 115 |
"mean_positive_distance": _mean_across(summaries, "mean_positive_distance_at_16"),
|
|
|
|
|
|
|
| 116 |
"candidate_diversity": _mean_across(summaries, "candidate_diversity_at_16"),
|
|
|
|
| 117 |
"proxy_support_distance": _mean_across(summaries, "proxy_support_distance_at_16"),
|
| 118 |
"data_hash": payloads[0].get("data_hash"),
|
| 119 |
"split_hash": payloads[0].get("target_split_hash", payloads[0].get("split_hash")),
|
|
@@ -132,17 +152,19 @@ def _mean_across(summaries: list[dict[str, Any]], key: str) -> float:
|
|
| 132 |
def _table(rows: list[dict[str, Any]]) -> str:
|
| 133 |
lines = [
|
| 134 |
"% Auto-generated by scripts/build_ctt_proxy_comparison.py",
|
| 135 |
-
"\\begin{tabular}{
|
| 136 |
"\\toprule",
|
| 137 |
-
"Method & Seeds & Rows & PPTC@0.20 & PPTC@0.40 & Neg@0.20 &
|
| 138 |
"\\midrule",
|
| 139 |
]
|
| 140 |
for row in rows:
|
| 141 |
lines.append(
|
| 142 |
f"{_latex_escape(row['method'])} & {row['train_seeds']} & {row['num_rows']} & "
|
| 143 |
f"{_fmt(row['pptc_0p20'])} & {_fmt(row['pptc_0p40'])} & "
|
| 144 |
-
f"{_fmt(row['negative_near_0p20'])} & {_fmt(row['
|
| 145 |
-
f"{_fmt(row['
|
|
|
|
|
|
|
| 146 |
)
|
| 147 |
lines.extend(["\\bottomrule", "\\end{tabular}"])
|
| 148 |
return "\n".join(lines)
|
|
@@ -154,15 +176,17 @@ def _report(rows: list[dict[str, Any]], safety_slack: float) -> str:
|
|
| 154 |
"",
|
| 155 |
f"Safety slack over local-atlas NegativeNear@0.20: `{safety_slack:.4f}`",
|
| 156 |
"",
|
| 157 |
-
"| Method | Seeds | Rows | PPTC@0.20 | PPTC@0.40 | Neg@0.20 |
|
| 158 |
-
"| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | --- | --- |",
|
| 159 |
]
|
| 160 |
for row in rows:
|
| 161 |
lines.append(
|
| 162 |
f"| {row['method']} | {row['train_seeds']} | {row['num_rows']} | "
|
| 163 |
f"{_fmt(row['pptc_0p20'])} | "
|
| 164 |
f"{_fmt(row['pptc_0p40'])} | {_fmt(row['negative_near_0p20'])} | "
|
| 165 |
-
f"{_fmt(row['
|
|
|
|
|
|
|
| 166 |
f"{_gate(row)} | `{row['run_path']}` |"
|
| 167 |
)
|
| 168 |
lines.append("")
|
|
@@ -170,6 +194,66 @@ def _report(rows: list[dict[str, Any]], safety_slack: float) -> str:
|
|
| 170 |
return "\n".join(lines)
|
| 171 |
|
| 172 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
def _fmt(value: Any) -> str:
|
| 174 |
if not isinstance(value, (int, float)) or not math.isfinite(float(value)):
|
| 175 |
return "n/a"
|
|
|
|
| 43 |
parser = argparse.ArgumentParser(description="Build CTT validation proxy comparison table.")
|
| 44 |
parser.add_argument("--out-dir", type=Path, default=Path("runs/ctt_val_proxy_comparison"))
|
| 45 |
parser.add_argument("--safety-slack", type=float, default=0.01)
|
| 46 |
+
parser.add_argument(
|
| 47 |
+
"--no-markdown-report",
|
| 48 |
+
action="store_true",
|
| 49 |
+
help="Do not write report.md; the persistent prose summary lives in README.md.",
|
| 50 |
+
)
|
| 51 |
args = parser.parse_args(argv)
|
| 52 |
|
| 53 |
rows = [
|
|
|
|
| 79 |
out_dir.mkdir(parents=True, exist_ok=True)
|
| 80 |
payload = {
|
| 81 |
"report_type": "ctt_val_proxy_comparison",
|
| 82 |
+
"schema_version": 2,
|
| 83 |
"baseline": "local_atlas",
|
| 84 |
"safety_slack": args.safety_slack,
|
| 85 |
"rows": rows,
|
|
|
|
| 87 |
"split_hash": _first(rows, "split_hash"),
|
| 88 |
}
|
| 89 |
(out_dir / "metrics.json").write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n")
|
| 90 |
+
(out_dir / "metrics_by_task.json").write_text(
|
| 91 |
+
json.dumps(_grouped_metrics(DEFAULT_RUNS, group_key="task_id"), indent=2, sort_keys=True)
|
| 92 |
+
+ "\n"
|
| 93 |
+
)
|
| 94 |
+
(out_dir / "metrics_by_seed.json").write_text(
|
| 95 |
+
json.dumps(_grouped_metrics(DEFAULT_RUNS, group_key="seed"), indent=2, sort_keys=True)
|
| 96 |
+
+ "\n"
|
| 97 |
+
)
|
| 98 |
(out_dir / "table.tex").write_text(_table(rows) + "\n")
|
| 99 |
+
_write_report_artifact(
|
| 100 |
+
out_dir,
|
| 101 |
+
rows,
|
| 102 |
+
safety_slack=args.safety_slack,
|
| 103 |
+
no_markdown_report=args.no_markdown_report,
|
| 104 |
+
)
|
| 105 |
(out_dir / "train.log").write_text("comparison artifact; no training\n")
|
| 106 |
(out_dir / "eval.log").write_text(
|
| 107 |
"\n".join(f"{row['method']}: {row['run_path']}" for row in rows) + "\n"
|
|
|
|
| 130 |
"negative_near_0p20": _mean_across(summaries, "negative_near_at_16_thr_0p20"),
|
| 131 |
"negative_near_0p40": _mean_across(summaries, "negative_near_at_16_thr_0p40"),
|
| 132 |
"mean_positive_distance": _mean_across(summaries, "mean_positive_distance_at_16"),
|
| 133 |
+
"mean_negative_distance": _mean_across(summaries, "mean_negative_distance_at_16"),
|
| 134 |
+
"pos_closer_than_neg": _mean_across(summaries, "pos_closer_than_neg_at_16"),
|
| 135 |
"candidate_diversity": _mean_across(summaries, "candidate_diversity_at_16"),
|
| 136 |
+
"collapse_rate": _mean_across(summaries, "collapse_rate_at_16"),
|
| 137 |
"proxy_support_distance": _mean_across(summaries, "proxy_support_distance_at_16"),
|
| 138 |
"data_hash": payloads[0].get("data_hash"),
|
| 139 |
"split_hash": payloads[0].get("target_split_hash", payloads[0].get("split_hash")),
|
|
|
|
| 152 |
def _table(rows: list[dict[str, Any]]) -> str:
|
| 153 |
lines = [
|
| 154 |
"% Auto-generated by scripts/build_ctt_proxy_comparison.py",
|
| 155 |
+
"\\begin{tabular}{lrrrrrrrrrrc}",
|
| 156 |
"\\toprule",
|
| 157 |
+
"Method & Seeds & Rows & PPTC@0.20 & PPTC@0.40 & Neg@0.20 & Neg@0.40 & Pos<Neg & MeanPos & MeanNeg & Collapse & Gate \\\\",
|
| 158 |
"\\midrule",
|
| 159 |
]
|
| 160 |
for row in rows:
|
| 161 |
lines.append(
|
| 162 |
f"{_latex_escape(row['method'])} & {row['train_seeds']} & {row['num_rows']} & "
|
| 163 |
f"{_fmt(row['pptc_0p20'])} & {_fmt(row['pptc_0p40'])} & "
|
| 164 |
+
f"{_fmt(row['negative_near_0p20'])} & {_fmt(row['negative_near_0p40'])} & "
|
| 165 |
+
f"{_fmt(row['pos_closer_than_neg'])} & {_fmt(row['mean_positive_distance'])} & "
|
| 166 |
+
f"{_fmt(row['mean_negative_distance'])} & {_fmt(row['collapse_rate'])} & "
|
| 167 |
+
f"{_gate(row)} \\\\"
|
| 168 |
)
|
| 169 |
lines.extend(["\\bottomrule", "\\end{tabular}"])
|
| 170 |
return "\n".join(lines)
|
|
|
|
| 176 |
"",
|
| 177 |
f"Safety slack over local-atlas NegativeNear@0.20: `{safety_slack:.4f}`",
|
| 178 |
"",
|
| 179 |
+
"| Method | Seeds | Rows | PPTC@0.20 | PPTC@0.40 | Neg@0.20 | Neg@0.40 | Pos<Neg | MeanPos | MeanNeg | Diversity | Collapse | Gate | Run |",
|
| 180 |
+
"| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | --- | --- |",
|
| 181 |
]
|
| 182 |
for row in rows:
|
| 183 |
lines.append(
|
| 184 |
f"| {row['method']} | {row['train_seeds']} | {row['num_rows']} | "
|
| 185 |
f"{_fmt(row['pptc_0p20'])} | "
|
| 186 |
f"{_fmt(row['pptc_0p40'])} | {_fmt(row['negative_near_0p20'])} | "
|
| 187 |
+
f"{_fmt(row['negative_near_0p40'])} | {_fmt(row['pos_closer_than_neg'])} | "
|
| 188 |
+
f"{_fmt(row['mean_positive_distance'])} | {_fmt(row['mean_negative_distance'])} | "
|
| 189 |
+
f"{_fmt(row['candidate_diversity'])} | {_fmt(row['collapse_rate'])} | "
|
| 190 |
f"{_gate(row)} | `{row['run_path']}` |"
|
| 191 |
)
|
| 192 |
lines.append("")
|
|
|
|
| 194 |
return "\n".join(lines)
|
| 195 |
|
| 196 |
|
| 197 |
+
def _write_report_artifact(
|
| 198 |
+
out_dir: Path,
|
| 199 |
+
rows: list[dict[str, Any]],
|
| 200 |
+
*,
|
| 201 |
+
safety_slack: float,
|
| 202 |
+
no_markdown_report: bool,
|
| 203 |
+
) -> None:
|
| 204 |
+
report_path = out_dir / "report.md"
|
| 205 |
+
if no_markdown_report:
|
| 206 |
+
if report_path.exists():
|
| 207 |
+
report_path.unlink()
|
| 208 |
+
return
|
| 209 |
+
report_path.write_text(_report(rows, safety_slack) + "\n")
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
def _grouped_metrics(
|
| 213 |
+
run_specs: list[tuple[str, list[Path]]],
|
| 214 |
+
*,
|
| 215 |
+
group_key: str,
|
| 216 |
+
) -> dict[str, dict[str, dict[str, float]]]:
|
| 217 |
+
output: dict[str, dict[str, dict[str, float]]] = {}
|
| 218 |
+
for method, run_dirs in run_specs:
|
| 219 |
+
if not run_dirs or not all((run_dir / "metrics.json").exists() for run_dir in run_dirs):
|
| 220 |
+
continue
|
| 221 |
+
rows: list[dict[str, Any]] = []
|
| 222 |
+
for run_dir in run_dirs:
|
| 223 |
+
payload = json.loads((run_dir / "metrics.json").read_text())
|
| 224 |
+
rows.extend(payload.get("rows", []))
|
| 225 |
+
if not rows:
|
| 226 |
+
continue
|
| 227 |
+
method_groups: dict[str, dict[str, list[float]]] = {}
|
| 228 |
+
for row in rows:
|
| 229 |
+
group = str(row.get(group_key, "unknown"))
|
| 230 |
+
metrics = method_groups.setdefault(group, {})
|
| 231 |
+
for source_key, target_key in _row_metric_key_map().items():
|
| 232 |
+
value = row.get(source_key)
|
| 233 |
+
if isinstance(value, (int, float)) and math.isfinite(float(value)):
|
| 234 |
+
metrics.setdefault(target_key, []).append(float(value))
|
| 235 |
+
output[method] = {
|
| 236 |
+
group: {name: sum(values) / len(values) for name, values in sorted(metrics.items())}
|
| 237 |
+
for group, metrics in sorted(method_groups.items())
|
| 238 |
+
}
|
| 239 |
+
return output
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
def _row_metric_key_map() -> dict[str, str]:
|
| 243 |
+
return {
|
| 244 |
+
"pptc_at_16_thr_0p20": "pptc_0p20",
|
| 245 |
+
"pptc_at_16_thr_0p40": "pptc_0p40",
|
| 246 |
+
"negative_near_at_16_thr_0p20": "negative_near_0p20",
|
| 247 |
+
"negative_near_at_16_thr_0p40": "negative_near_0p40",
|
| 248 |
+
"mean_positive_distance_at_16": "mean_positive_distance",
|
| 249 |
+
"mean_negative_distance_at_16": "mean_negative_distance",
|
| 250 |
+
"pos_closer_than_neg_at_16": "pos_closer_than_neg",
|
| 251 |
+
"candidate_diversity_at_16": "candidate_diversity",
|
| 252 |
+
"collapse_rate_at_16": "collapse_rate",
|
| 253 |
+
"proxy_support_distance_at_16": "proxy_support_distance",
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
|
| 257 |
def _fmt(value: Any) -> str:
|
| 258 |
if not isinstance(value, (int, float)) or not math.isfinite(float(value)):
|
| 259 |
return "n/a"
|