anhtld commited on
Commit
68d0545
·
verified ·
1 Parent(s): 733a6d7

expand CTT proxy comparison metrics

Browse files
Files changed (1) hide show
  1. scripts/build_ctt_proxy_comparison.py +94 -10
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("{}\n")
85
- (out_dir / "metrics_by_seed.json").write_text("{}\n")
 
 
 
 
 
 
86
  (out_dir / "table.tex").write_text(_table(rows) + "\n")
87
- (out_dir / "report.md").write_text(_report(rows, args.safety_slack) + "\n")
 
 
 
 
 
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}{lrrrrrrrc}",
136
  "\\toprule",
137
- "Method & Seeds & Rows & PPTC@0.20 & PPTC@0.40 & Neg@0.20 & MeanPosDist & Diversity & Gate \\\\",
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['mean_positive_distance'])} & "
145
- f"{_fmt(row['candidate_diversity'])} & {_gate(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 | MeanPosDist | Diversity | Gate | Run |",
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['mean_positive_distance'])} | {_fmt(row['candidate_diversity'])} | "
 
 
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"