anhtld commited on
Commit
7406f6f
·
verified ·
1 Parent(s): 52d0e83

sync chart feature audit script 2026-07-03

Browse files
workspace/scripts/audit_chart_feature_sources.py ADDED
@@ -0,0 +1,236 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import math
7
+ import subprocess
8
+ import sys
9
+ from collections import Counter, defaultdict
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
14
+ if str(PROJECT_ROOT) not in sys.path:
15
+ sys.path.insert(0, str(PROJECT_ROOT))
16
+
17
+ import numpy as np # noqa: E402
18
+
19
+ from cil.chart_features import CHART_FEATURE_MODES, chart_feature_dim # noqa: E402
20
+
21
+
22
+ def main(argv: list[str] | None = None) -> int:
23
+ parser = argparse.ArgumentParser(
24
+ description="Audit deployment-visible chart feature sources in CIL chart shards."
25
+ )
26
+ parser.add_argument(
27
+ "--indexes",
28
+ nargs="+",
29
+ type=Path,
30
+ default=[
31
+ Path("data/cil_charts/train/index.json"),
32
+ Path("data/cil_charts/val/index.json"),
33
+ Path("data/cil_charts/test/index.json"),
34
+ ],
35
+ )
36
+ parser.add_argument("--out-dir", type=Path, default=Path("runs/chart_feature_audit"))
37
+ args = parser.parse_args(argv)
38
+
39
+ out_dir = args.out_dir
40
+ out_dir.mkdir(parents=True, exist_ok=True)
41
+ _write_provenance(out_dir, args)
42
+
43
+ split_rows = []
44
+ row_details = []
45
+ for index_path in args.indexes:
46
+ index = json.loads(index_path.read_text())
47
+ split = str(index.get("split", index_path.parent.name))
48
+ counts: Counter[str] = Counter()
49
+ task_counts: Counter[str] = Counter()
50
+ feature_dims: dict[str, int] = {}
51
+ for shard in index.get("shards", []):
52
+ shard_path = index_path.parent / shard["path"]
53
+ with np.load(shard_path, allow_pickle=False) as data:
54
+ metadata_values = data["metadata_json"]
55
+ base_actions = data["base_action"]
56
+ action_shapes = data["action_shape"]
57
+ task_ids = data["task_id"]
58
+ chart_ids = data["chart_id"]
59
+ for row in range(metadata_values.shape[0]):
60
+ metadata = _json_loads(str(metadata_values[row]))
61
+ task_id = str(task_ids[row])
62
+ task_counts[task_id] += 1
63
+ counts["rows"] += 1
64
+ for key in (
65
+ "observation_embedding_path",
66
+ "observation_ref",
67
+ "scene_id",
68
+ "instruction",
69
+ "source_dataset",
70
+ ):
71
+ if metadata.get(key):
72
+ counts[f"{key}_present"] += 1
73
+ if counts["sample_details"] < 5:
74
+ shape = tuple(int(value) for value in action_shapes[row])
75
+ flat_count = int(math.prod(shape))
76
+ base = np.asarray(base_actions[row][:flat_count], dtype=np.float32).reshape(shape)
77
+ for mode in CHART_FEATURE_MODES:
78
+ feature_dims[mode] = chart_feature_dim(base, mode=mode)
79
+ row_details.append(
80
+ {
81
+ "split": split,
82
+ "chart_id": str(chart_ids[row]),
83
+ "task_id": task_id,
84
+ "has_observation_embedding_path": bool(
85
+ metadata.get("observation_embedding_path")
86
+ ),
87
+ "has_observation_ref": bool(metadata.get("observation_ref")),
88
+ "has_scene_id": bool(metadata.get("scene_id")),
89
+ "has_instruction": bool(metadata.get("instruction")),
90
+ }
91
+ )
92
+ counts["sample_details"] += 1
93
+ rows = int(counts["rows"])
94
+ split_rows.append(
95
+ {
96
+ "split": split,
97
+ "index": str(index_path),
98
+ "rows": rows,
99
+ "charts": int(index.get("num_groups_exported", 0)),
100
+ "retrieval_index_allowed": bool(index.get("retrieval_index_allowed")),
101
+ "include_outcomes": bool(index.get("include_outcomes")),
102
+ "observation_embedding_path_present": int(
103
+ counts["observation_embedding_path_present"]
104
+ ),
105
+ "observation_embedding_path_rate": _rate(
106
+ counts["observation_embedding_path_present"], rows
107
+ ),
108
+ "observation_ref_present": int(counts["observation_ref_present"]),
109
+ "observation_ref_rate": _rate(counts["observation_ref_present"], rows),
110
+ "scene_id_present": int(counts["scene_id_present"]),
111
+ "scene_id_rate": _rate(counts["scene_id_present"], rows),
112
+ "instruction_present": int(counts["instruction_present"]),
113
+ "instruction_rate": _rate(counts["instruction_present"], rows),
114
+ "source_dataset_present": int(counts["source_dataset_present"]),
115
+ "source_dataset_rate": _rate(counts["source_dataset_present"], rows),
116
+ "feature_dims": feature_dims,
117
+ "task_counts": dict(sorted(task_counts.items())),
118
+ }
119
+ )
120
+
121
+ metrics = {
122
+ "report_type": "chart_feature_source_audit",
123
+ "schema_version": 1,
124
+ "indexes": [str(path) for path in args.indexes],
125
+ "splits": split_rows,
126
+ "sample_details": row_details,
127
+ "conclusion": _conclusion(split_rows),
128
+ }
129
+ (out_dir / "metrics.json").write_text(json.dumps(metrics, indent=2, sort_keys=True) + "\n")
130
+ (out_dir / "metrics_by_task.json").write_text(_metrics_by_task(split_rows) + "\n")
131
+ (out_dir / "metrics_by_seed.json").write_text("{}\n")
132
+ (out_dir / "table.tex").write_text(_table(split_rows) + "\n")
133
+ (out_dir / "report.md").write_text(_report(metrics) + "\n")
134
+ (out_dir / "train.log").write_text("not a training run; audited chart feature sources\n")
135
+ (out_dir / "eval.log").write_text("audited chart feature sources in exported chart indexes\n")
136
+ print(json.dumps({"out_dir": str(out_dir), "splits": len(split_rows)}, indent=2))
137
+ return 0
138
+
139
+
140
+ def _json_loads(value: str) -> dict[str, Any]:
141
+ try:
142
+ payload = json.loads(value)
143
+ except json.JSONDecodeError:
144
+ return {}
145
+ return payload if isinstance(payload, dict) else {}
146
+
147
+
148
+ def _rate(count: int, total: int) -> float:
149
+ return float(count) / float(total) if total else 0.0
150
+
151
+
152
+ def _conclusion(rows: list[dict[str, Any]]) -> str:
153
+ if all(float(row["observation_embedding_path_rate"]) == 0.0 for row in rows):
154
+ return (
155
+ "Current chart exports do not contain observation embeddings or raw observation refs; "
156
+ "visual/object-centric chart tokens require a new export or embedding pass."
157
+ )
158
+ return "At least one split exposes observation references; visual chart features can be wired next."
159
+
160
+
161
+ def _metrics_by_task(rows: list[dict[str, Any]]) -> str:
162
+ payload: dict[str, dict[str, int]] = defaultdict(dict)
163
+ for row in rows:
164
+ for task, count in row["task_counts"].items():
165
+ payload[task][str(row["split"])] = int(count)
166
+ return json.dumps(payload, indent=2, sort_keys=True)
167
+
168
+
169
+ def _table(rows: list[dict[str, Any]]) -> str:
170
+ lines = [
171
+ "% Auto-generated by scripts/audit_chart_feature_sources.py",
172
+ "\\begin{tabular}{lrrrr}",
173
+ "\\toprule",
174
+ "Split & Rows & ObsEmbed & ObsRef & Instruction \\\\",
175
+ "\\midrule",
176
+ ]
177
+ for row in rows:
178
+ lines.append(
179
+ f"{row['split']} & {row['rows']} & "
180
+ f"{row['observation_embedding_path_present']} & "
181
+ f"{row['observation_ref_present']} & "
182
+ f"{row['instruction_present']} \\\\"
183
+ )
184
+ lines.extend(["\\bottomrule", "\\end{tabular}"])
185
+ return "\n".join(lines)
186
+
187
+
188
+ def _report(metrics: dict[str, Any]) -> str:
189
+ lines = [
190
+ "# Chart Feature Source Audit",
191
+ "",
192
+ metrics["conclusion"],
193
+ "",
194
+ "| Split | Rows | Obs embedding path | Obs ref | Scene id | Instruction | Feature dims |",
195
+ "| --- | ---: | ---: | ---: | ---: | ---: | --- |",
196
+ ]
197
+ for row in metrics["splits"]:
198
+ lines.append(
199
+ f"| {row['split']} | {row['rows']} | "
200
+ f"{row['observation_embedding_path_present']} ({row['observation_embedding_path_rate']:.2%}) | "
201
+ f"{row['observation_ref_present']} ({row['observation_ref_rate']:.2%}) | "
202
+ f"{row['scene_id_present']} ({row['scene_id_rate']:.2%}) | "
203
+ f"{row['instruction_present']} ({row['instruction_rate']:.2%}) | "
204
+ f"{json.dumps(row['feature_dims'], sort_keys=True)} |"
205
+ )
206
+ return "\n".join(lines)
207
+
208
+
209
+ def _write_provenance(out_dir: Path, args: argparse.Namespace) -> None:
210
+ (out_dir / "config.yaml").write_text(
211
+ "\n".join(f"{key}: {value}" for key, value in sorted(vars(args).items())) + "\n"
212
+ )
213
+ (out_dir / "command.txt").write_text(
214
+ "python scripts/audit_chart_feature_sources.py " + " ".join(sys.argv[1:]) + "\n"
215
+ )
216
+ (out_dir / "git_hash.txt").write_text(_run(["git", "rev-parse", "HEAD"]) + "\n")
217
+ hashes = {}
218
+ for index_path in args.indexes:
219
+ index = json.loads(index_path.read_text())
220
+ hashes[str(index_path)] = {
221
+ "content_hash": index.get("content_hash"),
222
+ "split_hash": index.get("split_hash"),
223
+ }
224
+ (out_dir / "data_hash.txt").write_text(json.dumps(hashes, indent=2, sort_keys=True) + "\n")
225
+ (out_dir / "split_hash.txt").write_text(json.dumps(hashes, indent=2, sort_keys=True) + "\n")
226
+
227
+
228
+ def _run(command: list[str]) -> str:
229
+ try:
230
+ return subprocess.check_output(command, cwd=PROJECT_ROOT, text=True).strip()
231
+ except (subprocess.CalledProcessError, FileNotFoundError):
232
+ return ""
233
+
234
+
235
+ if __name__ == "__main__":
236
+ raise SystemExit(main())