anhtld commited on
Commit
b61a569
·
verified ·
1 Parent(s): da9aa5c

perdim trainmax CTT 2026-07-03 workspace/scripts/build_action_scale_vector.py

Browse files
workspace/scripts/build_action_scale_vector.py ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import subprocess
7
+ import sys
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+
12
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
13
+
14
+
15
+ def main(argv: list[str] | None = None) -> int:
16
+ parser = argparse.ArgumentParser(
17
+ description=(
18
+ "Build a reproducible per-dimension action scale vector from an "
19
+ "action-bound audit. The default uses train split only, so deployment "
20
+ "diagnostics do not fit action conventions on validation/test outcomes."
21
+ )
22
+ )
23
+ parser.add_argument(
24
+ "--audit",
25
+ type=Path,
26
+ default=Path("runs/action_bound_audit_rgb_refs/metrics.json"),
27
+ help="Action-bound audit metrics.json.",
28
+ )
29
+ parser.add_argument(
30
+ "--out-dir",
31
+ type=Path,
32
+ default=Path("runs/action_scale_vector_train_base_branch_max"),
33
+ )
34
+ parser.add_argument(
35
+ "--splits",
36
+ default="train",
37
+ help="Comma-separated audit splits to use. Default is train only.",
38
+ )
39
+ parser.add_argument(
40
+ "--source",
41
+ choices=("action", "base_action", "base_branch"),
42
+ default="base_branch",
43
+ help="Per-dimension audit source used for max-to-unit scaling.",
44
+ )
45
+ parser.add_argument(
46
+ "--floor",
47
+ type=float,
48
+ default=1.0e-6,
49
+ help="Minimum allowed scale value.",
50
+ )
51
+ parser.add_argument(
52
+ "--ceil",
53
+ type=float,
54
+ default=1.0,
55
+ help="Maximum allowed scale value.",
56
+ )
57
+ args = parser.parse_args(argv)
58
+
59
+ if args.floor <= 0.0:
60
+ parser.error("--floor must be positive")
61
+ if args.ceil <= 0.0:
62
+ parser.error("--ceil must be positive")
63
+ if args.floor > args.ceil:
64
+ parser.error("--floor must be <= --ceil")
65
+
66
+ audit = json.loads(args.audit.read_text())
67
+ requested_splits = [item.strip() for item in args.splits.split(",") if item.strip()]
68
+ if not requested_splits:
69
+ parser.error("--splits must name at least one split")
70
+
71
+ rows_by_split = {str(row.get("split")): row for row in audit.get("rows", [])}
72
+ missing = [split for split in requested_splits if split not in rows_by_split]
73
+ if missing:
74
+ raise SystemExit(f"missing split(s) in audit: {', '.join(missing)}")
75
+
76
+ vectors: list[list[float]] = []
77
+ for split in requested_splits:
78
+ per_dim = rows_by_split[split].get("per_dim", {}).get(args.source, {})
79
+ vector = per_dim.get("suggested_per_dim_scale_to_unit_max")
80
+ if not vector:
81
+ raise SystemExit(f"audit split {split!r} has no per-dim scale for {args.source!r}")
82
+ vectors.append([float(value) for value in vector])
83
+
84
+ width = len(vectors[0])
85
+ if any(len(vector) != width for vector in vectors):
86
+ raise SystemExit("requested split vectors have different widths")
87
+
88
+ # Use the most conservative per-dimension scale across requested splits.
89
+ scale = [
90
+ min(float(args.ceil), max(float(args.floor), min(vector[dim] for vector in vectors)))
91
+ for dim in range(width)
92
+ ]
93
+
94
+ out_dir = args.out_dir
95
+ out_dir.mkdir(parents=True, exist_ok=True)
96
+ payload: dict[str, Any] = {
97
+ "report_type": "action_scale_vector",
98
+ "schema_version": 1,
99
+ "audit": str(args.audit),
100
+ "audit_report_type": audit.get("report_type"),
101
+ "chart_root": audit.get("chart_root"),
102
+ "splits": requested_splits,
103
+ "source": args.source,
104
+ "fit_scope": "train_only" if requested_splits == ["train"] else "multi_split_diagnostic",
105
+ "scale": scale,
106
+ "scale_env": ",".join(f"{value:.12g}" for value in scale),
107
+ "data_hashes": {split: audit.get("data_hashes", {}).get(split) for split in requested_splits},
108
+ "split_hashes": {split: audit.get("split_hashes", {}).get(split) for split in requested_splits},
109
+ }
110
+
111
+ (out_dir / "vector.json").write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n")
112
+ (out_dir / "vector_env.txt").write_text(payload["scale_env"] + "\n")
113
+ (out_dir / "config.yaml").write_text(
114
+ "\n".join(
115
+ [
116
+ f"audit: {args.audit}",
117
+ f"splits: {args.splits}",
118
+ f"source: {args.source}",
119
+ f"floor: {args.floor}",
120
+ f"ceil: {args.ceil}",
121
+ ]
122
+ )
123
+ + "\n"
124
+ )
125
+ (out_dir / "command.txt").write_text(
126
+ "python scripts/build_action_scale_vector.py " + " ".join(sys.argv[1:]) + "\n"
127
+ )
128
+ (out_dir / "git_hash.txt").write_text(_run(["git", "rev-parse", "HEAD"]) + "\n")
129
+ (out_dir / "data_hash.txt").write_text(json.dumps(payload["data_hashes"], sort_keys=True) + "\n")
130
+ (out_dir / "split_hash.txt").write_text(json.dumps(payload["split_hashes"], sort_keys=True) + "\n")
131
+ (out_dir / "train.log").write_text(
132
+ "fit per-dimension action scale from action-bound audit rows only\n"
133
+ )
134
+ (out_dir / "eval.log").write_text("no eval; action convention artifact only\n")
135
+ (out_dir / "table.tex").write_text(_table(payload) + "\n")
136
+ (out_dir / "report.md").write_text(_report(payload) + "\n")
137
+ print(json.dumps({"out_dir": str(out_dir), "scale_env": payload["scale_env"]}, indent=2))
138
+ return 0
139
+
140
+
141
+ def _table(payload: dict[str, Any]) -> str:
142
+ values = payload["scale"]
143
+ lines = [
144
+ "% Auto-generated by scripts/build_action_scale_vector.py",
145
+ "\\begin{tabular}{lrrrrrrr}",
146
+ "\\toprule",
147
+ "Source & d0 & d1 & d2 & d3 & d4 & d5 & d6 \\\\",
148
+ "\\midrule",
149
+ (
150
+ f"{_latex_escape(str(payload['source']))} & "
151
+ + " & ".join(f"{float(value):.4f}" for value in values)
152
+ + " \\\\"
153
+ ),
154
+ "\\bottomrule",
155
+ "\\end{tabular}",
156
+ ]
157
+ return "\n".join(lines)
158
+
159
+
160
+ def _report(payload: dict[str, Any]) -> str:
161
+ return "\n".join(
162
+ [
163
+ "# Action Scale Vector",
164
+ "",
165
+ f"Audit: `{payload['audit']}`",
166
+ f"Splits used: `{','.join(payload['splits'])}`",
167
+ f"Source: `{payload['source']}`",
168
+ f"Fit scope: `{payload['fit_scope']}`",
169
+ "",
170
+ "Scale vector:",
171
+ "",
172
+ f"`{payload['scale_env']}`",
173
+ "",
174
+ "This artifact defines an action-convention diagnostic only. It does "
175
+ "not measure collision/contact safety and does not use validation/test "
176
+ "outcomes when `fit_scope=train_only`.",
177
+ ]
178
+ )
179
+
180
+
181
+ def _latex_escape(value: str) -> str:
182
+ return value.replace("_", "\\_").replace("%", "\\%").replace("&", "\\&")
183
+
184
+
185
+ def _run(command: list[str]) -> str:
186
+ try:
187
+ return subprocess.check_output(command, cwd=PROJECT_ROOT, text=True).strip()
188
+ except (subprocess.CalledProcessError, FileNotFoundError):
189
+ return ""
190
+
191
+
192
+ if __name__ == "__main__":
193
+ raise SystemExit(main())