anhtld commited on
Commit
2ad8b3a
·
verified ·
1 Parent(s): c452a96

auto-sync 2026-07-04T04:49:49Z workspace (part 2)

Browse files
workspace/scripts/audit_ctt_paper_artifacts.py CHANGED
@@ -86,6 +86,7 @@ REQUIRED_PATHS: tuple[tuple[str, str, str], ...] = (
86
  ("ctt_rollout_eval", "scripts/eval_ctt_rollout.py", "Measured rollout evaluation."),
87
  ("utility_train", "scripts/train_utility_energy.py", "Utility energy training."),
88
  ("dominance_calibration", "scripts/calibrate_dominance.py", "Calibrated dominance rule."),
 
89
  ("theory_tex", "paper/sections/theory.tex", "Theory section included by paper."),
90
  ("paper_pdf", "latex/main.pdf", "Compiled paper PDF."),
91
  )
 
86
  ("ctt_rollout_eval", "scripts/eval_ctt_rollout.py", "Measured rollout evaluation."),
87
  ("utility_train", "scripts/train_utility_energy.py", "Utility energy training."),
88
  ("dominance_calibration", "scripts/calibrate_dominance.py", "Calibrated dominance rule."),
89
+ ("selector_diagnostic_sweep", "scripts/build_selector_diagnostic_sweep.py", "Selector diagnostic sweep summary."),
90
  ("theory_tex", "paper/sections/theory.tex", "Theory section included by paper."),
91
  ("paper_pdf", "latex/main.pdf", "Compiled paper PDF."),
92
  )
workspace/scripts/build_selector_diagnostic_sweep.py ADDED
@@ -0,0 +1,349 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import glob
6
+ import hashlib
7
+ import json
8
+ import math
9
+ import subprocess
10
+ import sys
11
+ from collections import defaultdict
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+
16
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
17
+
18
+
19
+ DEFAULT_PATTERNS = (
20
+ "runs/ctt_base_context_obs_learned_dominance_chartcompat_obs_utility_task_envclip_k16_train_to_test/metrics.json",
21
+ "runs/ctt_base_context_obs_dominance_envclip_k16_train_to_test/metrics.json",
22
+ "runs/ctt_base_context_obs_dominance_envclip_k16_train_to_test_tau0/metrics.json",
23
+ "runs/ctt_base_context_obs_learned_dominance_*_tanh_train_to_test/metrics.json",
24
+ "runs/ctt_base_context_obs_dominance_tanh_train_to_test/metrics.json",
25
+ "runs/ctt_base_context_obs_learned_dominance_*_perdim_trainmax_train_to_test/metrics.json",
26
+ "runs/ctt_base_context_obs_dominance_perdim_trainmax_train_to_test/metrics.json",
27
+ )
28
+
29
+
30
+ def main(argv: list[str] | None = None) -> int:
31
+ parser = argparse.ArgumentParser(
32
+ description=(
33
+ "Build a non-cherry-picked selector diagnostic sweep table from "
34
+ "completed CTT selector metrics.json files."
35
+ )
36
+ )
37
+ parser.add_argument(
38
+ "--metrics",
39
+ action="append",
40
+ default=[],
41
+ help="Metrics file or glob. Defaults cover current env_clip/tanh/per-dim selector runs.",
42
+ )
43
+ parser.add_argument("--out-dir", type=Path, default=Path("runs/ctt_selector_diagnostic_sweep"))
44
+ parser.add_argument("--selected-min", type=float, default=0.4745)
45
+ parser.add_argument("--proposal-oracle-min", type=float, default=0.50)
46
+ parser.add_argument("--selector-gap-max", type=float, default=0.03)
47
+ args = parser.parse_args(argv)
48
+
49
+ metric_paths = _resolve_metric_paths(args.metrics or list(DEFAULT_PATTERNS))
50
+ if not metric_paths:
51
+ raise SystemExit("no selector metrics found")
52
+
53
+ rows = [_row(path) for path in metric_paths]
54
+ best_rows = _best_by_family(rows)
55
+ gates = [_gate(row, args) for row in best_rows]
56
+
57
+ out_dir = args.out_dir
58
+ out_dir.mkdir(parents=True, exist_ok=True)
59
+ payload = {
60
+ "report_type": "ctt_selector_diagnostic_sweep",
61
+ "schema_version": 1,
62
+ "selection_rule": "best selected_success per diagnostic family; all candidate rows retained",
63
+ "thresholds": {
64
+ "selected_min": args.selected_min,
65
+ "proposal_oracle_min": args.proposal_oracle_min,
66
+ "selector_gap_max": args.selector_gap_max,
67
+ },
68
+ "num_inputs": len(metric_paths),
69
+ "input_metrics": [str(path) for path in metric_paths],
70
+ "rows": rows,
71
+ "best_by_family": best_rows,
72
+ "gates": gates,
73
+ "overall_pass": all(gate["pass"] for gate in gates),
74
+ "data_hash": _combined_hash([row.get("data_hash") for row in rows]),
75
+ "split_hash": _combined_hash([row.get("split_hash") for row in rows]),
76
+ }
77
+ (out_dir / "metrics.json").write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n")
78
+ (out_dir / "metrics_by_task.json").write_text(
79
+ json.dumps(_group_rows(rows, "family"), indent=2, sort_keys=True) + "\n"
80
+ )
81
+ (out_dir / "metrics_by_seed.json").write_text(
82
+ json.dumps(_group_rows(rows, "k"), indent=2, sort_keys=True) + "\n"
83
+ )
84
+ (out_dir / "table.tex").write_text(_table(best_rows) + "\n")
85
+ (out_dir / "config.yaml").write_text(_config(args, metric_paths) + "\n")
86
+ (out_dir / "command.txt").write_text(
87
+ "python scripts/build_selector_diagnostic_sweep.py " + " ".join(sys.argv[1:]) + "\n"
88
+ )
89
+ (out_dir / "git_hash.txt").write_text(_git_hash() + "\n")
90
+ (out_dir / "data_hash.txt").write_text(str(payload["data_hash"]) + "\n")
91
+ (out_dir / "split_hash.txt").write_text(str(payload["split_hash"]) + "\n")
92
+ (out_dir / "train.log").write_text("selector sweep artifact; source selectors trained separately\n")
93
+ (out_dir / "eval.log").write_text(
94
+ "\n".join(
95
+ [
96
+ f"num_inputs={len(metric_paths)}",
97
+ f"families={','.join(row['family'] for row in best_rows)}",
98
+ f"overall_pass={payload['overall_pass']}",
99
+ ]
100
+ )
101
+ + "\n"
102
+ )
103
+ print(
104
+ json.dumps(
105
+ {
106
+ "out_dir": str(out_dir),
107
+ "num_inputs": len(metric_paths),
108
+ "families": [row["family"] for row in best_rows],
109
+ "overall_pass": payload["overall_pass"],
110
+ },
111
+ indent=2,
112
+ )
113
+ )
114
+ return 0
115
+
116
+
117
+ def _resolve_metric_paths(patterns: list[str]) -> list[Path]:
118
+ paths: list[Path] = []
119
+ for pattern in patterns:
120
+ if any(char in pattern for char in "*?[]"):
121
+ matches = [Path(item) for item in sorted(glob.glob(pattern))]
122
+ else:
123
+ matches = [Path(pattern)]
124
+ for path in matches:
125
+ if path.exists() and path.name == "metrics.json" and path not in paths:
126
+ paths.append(path)
127
+ return paths
128
+
129
+
130
+ def _row(path: Path) -> dict[str, Any]:
131
+ data = json.loads(path.read_text())
132
+ summary = data.get("eval_summary") or _micro_summary(data.get("summary", {}))
133
+ run_name = path.parent.name
134
+ family = _family(run_name, data)
135
+ selector = _selector_name(run_name, data)
136
+ return {
137
+ "run_path": str(path.parent),
138
+ "family": family,
139
+ "selector": selector,
140
+ "report_type": data.get("report_type", "unknown"),
141
+ "k": int(data.get("k") or _infer_k(run_name)),
142
+ "base_success": _num(summary.get("base_success")),
143
+ "selected_success": _num(summary.get("selected_success")),
144
+ "proposal_oracle_success": _num(summary.get("proposal_oracle_success")),
145
+ "hidden_chart_oracle_success": _num(summary.get("hidden_chart_oracle_success")),
146
+ "coverage": _num(summary.get("coverage")),
147
+ "fallback_rate": _num(summary.get("fallback_rate")),
148
+ "success_support_gap": _num(summary.get("success_support_gap")),
149
+ "success_selector_gap": _num(summary.get("success_selector_gap")),
150
+ "outcome_ptr": _num(summary.get("outcome_ptr")),
151
+ "calibration_ece": _num(summary.get("pairwise_causal_calibration_ece")),
152
+ "selector_regret": _num(summary.get("selector_regret")),
153
+ "data_hash": _first_hash(data, ("data_hash", "eval_target_content_hash", "selector_eval_target_content_hash")),
154
+ "split_hash": _first_hash(data, ("split_hash", "eval_target_split_hash", "selector_eval_target_split_hash")),
155
+ }
156
+
157
+
158
+ def _micro_summary(summary: dict[str, Any]) -> dict[str, Any]:
159
+ output: dict[str, Any] = {}
160
+ for name, payload in summary.items():
161
+ if isinstance(payload, dict):
162
+ output[name] = payload.get("micro", {}).get("mean")
163
+ return output
164
+
165
+
166
+ def _family(run_name: str, data: dict[str, Any]) -> str:
167
+ k = int(data.get("k") or _infer_k(run_name))
168
+ if "envclip_k16" in run_name:
169
+ return "K16 env_clip"
170
+ if "envclip" in run_name:
171
+ return f"K{k} env_clip"
172
+ if "tanh" in run_name:
173
+ return f"K{k} tanh"
174
+ if "perdim_trainmax" in run_name:
175
+ return f"K{k} per-dim trainmax"
176
+ return f"K{k} other"
177
+
178
+
179
+ def _selector_name(run_name: str, data: dict[str, Any]) -> str:
180
+ report_type = str(data.get("report_type", ""))
181
+ if report_type == "dominance_calibrated_selector_eval":
182
+ tau_mode = str(data.get("tau_mode", "auto"))
183
+ return f"LCB {tau_mode}"
184
+ feature_set = str(data.get("feature_set", "unknown"))
185
+ target = str(data.get("target", "unknown"))
186
+ extras = []
187
+ if data.get("success_bonus") not in {None, 0, 0.0}:
188
+ extras.append(f"bonus={data['success_bonus']}")
189
+ if "chartcompat_obs" in run_name and "chartcompat" not in feature_set:
190
+ extras.append("chartcompat_obs")
191
+ suffix = ", " + ", ".join(extras) if extras else ""
192
+ return f"{feature_set}/{target}{suffix}"
193
+
194
+
195
+ def _infer_k(run_name: str) -> int:
196
+ return 16 if "k16" in run_name else 8
197
+
198
+
199
+ def _best_by_family(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
200
+ grouped: dict[str, list[dict[str, Any]]] = defaultdict(list)
201
+ for row in rows:
202
+ grouped[row["family"]].append(row)
203
+ best = []
204
+ for family, items in grouped.items():
205
+ best.append(
206
+ max(
207
+ items,
208
+ key=lambda row: (
209
+ _sort_num(row.get("selected_success")),
210
+ _sort_num(row.get("proposal_oracle_success")),
211
+ -_sort_num(row.get("success_selector_gap")),
212
+ ),
213
+ )
214
+ )
215
+ return sorted(best, key=lambda row: (row["k"], row["family"]))
216
+
217
+
218
+ def _gate(row: dict[str, Any], args: argparse.Namespace) -> dict[str, Any]:
219
+ selected = _num(row.get("selected_success"))
220
+ proposal = _num(row.get("proposal_oracle_success"))
221
+ selector_gap = _num(row.get("success_selector_gap"))
222
+ passed = (
223
+ selected is not None
224
+ and proposal is not None
225
+ and selector_gap is not None
226
+ and selected >= args.selected_min
227
+ and proposal >= args.proposal_oracle_min
228
+ and selector_gap <= args.selector_gap_max
229
+ )
230
+ return {
231
+ "family": row["family"],
232
+ "selector": row["selector"],
233
+ "pass": bool(passed),
234
+ "status": "method_success" if passed else "diagnostic_only",
235
+ "selected_success": selected,
236
+ "proposal_oracle_success": proposal,
237
+ "success_selector_gap": selector_gap,
238
+ }
239
+
240
+
241
+ def _group_rows(rows: list[dict[str, Any]], group_key: str) -> dict[str, dict[str, float]]:
242
+ metrics = (
243
+ "base_success",
244
+ "selected_success",
245
+ "proposal_oracle_success",
246
+ "coverage",
247
+ "success_support_gap",
248
+ "success_selector_gap",
249
+ "outcome_ptr",
250
+ "calibration_ece",
251
+ )
252
+ grouped: dict[str, list[dict[str, Any]]] = defaultdict(list)
253
+ for row in rows:
254
+ grouped[str(row.get(group_key, "unknown"))].append(row)
255
+ output: dict[str, dict[str, float]] = {}
256
+ for group, items in sorted(grouped.items()):
257
+ output[group] = {}
258
+ for metric in metrics:
259
+ values = [_num(item.get(metric)) for item in items]
260
+ clean = [value for value in values if value is not None]
261
+ if clean:
262
+ output[group][metric] = sum(clean) / len(clean)
263
+ return output
264
+
265
+
266
+ def _table(rows: list[dict[str, Any]]) -> str:
267
+ lines = [
268
+ "% Auto-generated by scripts/build_selector_diagnostic_sweep.py",
269
+ "\\begin{tabular}{llrrrrrr}",
270
+ "\\toprule",
271
+ "Family & Best selector & Base & Selected & Proposal & Coverage & Sel. gap & Support gap \\\\",
272
+ "\\midrule",
273
+ ]
274
+ for row in rows:
275
+ lines.append(
276
+ f"{_latex(row['family'])} & {_latex(row['selector'])} & "
277
+ f"{_fmt(row.get('base_success'))} & {_fmt(row.get('selected_success'))} & "
278
+ f"{_fmt(row.get('proposal_oracle_success'))} & {_fmt(row.get('coverage'))} & "
279
+ f"{_fmt(row.get('success_selector_gap'))} & {_fmt(row.get('success_support_gap'))} \\\\"
280
+ )
281
+ lines.extend(["\\bottomrule", "\\end{tabular}"])
282
+ return "\n".join(lines)
283
+
284
+
285
+ def _config(args: argparse.Namespace, paths: list[Path]) -> str:
286
+ return "\n".join(
287
+ [
288
+ f"out_dir: {args.out_dir}",
289
+ f"selected_min: {args.selected_min}",
290
+ f"proposal_oracle_min: {args.proposal_oracle_min}",
291
+ f"selector_gap_max: {args.selector_gap_max}",
292
+ "metrics:",
293
+ *[f" - {path}" for path in paths],
294
+ ]
295
+ )
296
+
297
+
298
+ def _first_hash(data: dict[str, Any], keys: tuple[str, ...]) -> str | None:
299
+ for key in keys:
300
+ value = data.get(key)
301
+ if isinstance(value, str) and value:
302
+ return value
303
+ return None
304
+
305
+
306
+ def _combined_hash(values: list[Any]) -> str:
307
+ clean = [str(value) for value in values if value not in {None, ""}]
308
+ blob = json.dumps(sorted(clean), separators=(",", ":")).encode()
309
+ return hashlib.sha256(blob).hexdigest()
310
+
311
+
312
+ def _git_hash() -> str:
313
+ try:
314
+ return subprocess.check_output(
315
+ ["git", "rev-parse", "HEAD"],
316
+ cwd=PROJECT_ROOT,
317
+ text=True,
318
+ stderr=subprocess.DEVNULL,
319
+ ).strip()
320
+ except (OSError, subprocess.CalledProcessError):
321
+ return "unknown"
322
+
323
+
324
+ def _num(value: Any) -> float | None:
325
+ if value is None:
326
+ return None
327
+ try:
328
+ numeric = float(value)
329
+ except (TypeError, ValueError):
330
+ return None
331
+ return numeric if math.isfinite(numeric) else None
332
+
333
+
334
+ def _sort_num(value: Any) -> float:
335
+ numeric = _num(value)
336
+ return -math.inf if numeric is None else numeric
337
+
338
+
339
+ def _fmt(value: Any) -> str:
340
+ numeric = _num(value)
341
+ return "n/a" if numeric is None else f"{numeric:.4f}"
342
+
343
+
344
+ def _latex(value: Any) -> str:
345
+ return str(value).replace("\\", "\\textbackslash{}").replace("_", "\\_").replace("&", "\\&").replace("%", "\\%")
346
+
347
+
348
+ if __name__ == "__main__":
349
+ raise SystemExit(main())