anhtld commited on
Commit
8c9f49d
·
verified ·
1 Parent(s): 3496751

ctt chart export script

Browse files
workspace/scripts/export_cil_charts.py ADDED
@@ -0,0 +1,317 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import hashlib
6
+ import json
7
+ import math
8
+ import sys
9
+ from collections import Counter
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
+ from dovla_cil.data.datasets import CILDataset # noqa: E402
18
+ from dovla_cil.data.schema import CILRecord, OutcomeVector # noqa: E402
19
+ from dovla_cil.generation.tangent_targets import ( # noqa: E402
20
+ DEFAULT_BASE_CANDIDATE_TYPES,
21
+ action_matrix,
22
+ action_shape,
23
+ choose_base_record,
24
+ label_from_delta_utility,
25
+ record_utility,
26
+ subtract_actions,
27
+ )
28
+
29
+
30
+ LABEL_TO_ID = {"negative": -1, "neutral": 0, "positive": 1, "hidden": 9}
31
+
32
+
33
+ def main(argv: list[str] | None = None) -> int:
34
+ parser = argparse.ArgumentParser(
35
+ description=(
36
+ "Export same-state CIL charts to NPZ shards for train-only CIL-Atlas "
37
+ "retrieval/generator training, with an index that can be leakage-audited."
38
+ )
39
+ )
40
+ parser.add_argument("--dataset", type=Path, required=True)
41
+ parser.add_argument(
42
+ "--out-dir",
43
+ type=Path,
44
+ default=Path("data/cil_charts/train"),
45
+ help="Output split directory, e.g. data/cil_charts/train.",
46
+ )
47
+ parser.add_argument("--split", default="train")
48
+ parser.add_argument("--epsilon", type=float, default=0.0)
49
+ parser.add_argument("--max-groups", type=int, default=None)
50
+ parser.add_argument("--shard-size", type=int, default=50000)
51
+ parser.add_argument(
52
+ "--base-candidate-types",
53
+ default=",".join(DEFAULT_BASE_CANDIDATE_TYPES),
54
+ )
55
+ parser.add_argument(
56
+ "--include-outcomes",
57
+ action="store_true",
58
+ help=(
59
+ "Include measured utilities/outcome vectors. Defaults to true only for "
60
+ "the train split; non-train outcome exports are evaluator-only."
61
+ ),
62
+ )
63
+ parser.add_argument(
64
+ "--no-outcomes",
65
+ action="store_true",
66
+ help="Force-hide utilities/outcomes even for train export.",
67
+ )
68
+ args = parser.parse_args(argv)
69
+
70
+ if args.epsilon < 0.0:
71
+ parser.error("--epsilon must be non-negative")
72
+ if args.max_groups is not None and args.max_groups <= 0:
73
+ parser.error("--max-groups must be positive when provided")
74
+ if args.shard_size <= 0:
75
+ parser.error("--shard-size must be positive")
76
+
77
+ try:
78
+ import numpy as np
79
+ except ImportError as exc: # pragma: no cover - environment guard
80
+ raise SystemExit("export_cil_charts.py requires numpy to write .npz shards") from exc
81
+
82
+ include_outcomes = (args.split == "train" or args.include_outcomes) and not args.no_outcomes
83
+ dataset = CILDataset(args.dataset)
84
+ out_dir = args.out_dir
85
+ out_dir.mkdir(parents=True, exist_ok=True)
86
+ rows: list[dict[str, Any]] = []
87
+ shard_paths: list[dict[str, Any]] = []
88
+ counters: Counter[str] = Counter()
89
+ task_counts: Counter[str] = Counter()
90
+ label_counts: Counter[str] = Counter()
91
+ group_hashes: set[str] = set()
92
+ state_hashes: set[str] = set()
93
+ max_flat_dim = 0
94
+ group_count = 0
95
+ base_candidate_types = _parse_csv(args.base_candidate_types)
96
+
97
+ for group in dataset.iter_groups():
98
+ if args.max_groups is not None and group_count >= args.max_groups:
99
+ break
100
+ group_count += 1
101
+ if not group:
102
+ counters["empty_group"] += 1
103
+ continue
104
+ base = choose_base_record(group, base_candidate_types=base_candidate_types)
105
+ if base is None:
106
+ counters["missing_base"] += 1
107
+ continue
108
+ base_action = action_matrix(base)
109
+ if not base_action:
110
+ counters["empty_base_action"] += 1
111
+ continue
112
+ base_shape = action_shape(base_action)
113
+ base_utility = record_utility(base) if include_outcomes else math.nan
114
+ emitted_for_group = 0
115
+ for record in group:
116
+ action = action_matrix(record)
117
+ if not action:
118
+ counters["empty_action"] += 1
119
+ continue
120
+ if action_shape(action) != base_shape:
121
+ counters["action_shape_mismatch"] += 1
122
+ continue
123
+ utility = record_utility(record) if include_outcomes else math.nan
124
+ delta_utility = utility - base_utility if include_outcomes else math.nan
125
+ label = (
126
+ label_from_delta_utility(delta_utility, epsilon=args.epsilon)
127
+ if include_outcomes and record.record_id != base.record_id
128
+ else ("neutral" if include_outcomes else "hidden")
129
+ )
130
+ delta_action = subtract_actions(action, base_action)
131
+ row = _row_from_record(
132
+ record,
133
+ base=base,
134
+ split=args.split,
135
+ action=action,
136
+ base_action=base_action,
137
+ delta_action=delta_action,
138
+ utility=utility,
139
+ base_utility=base_utility,
140
+ delta_utility=delta_utility,
141
+ label=label,
142
+ include_outcomes=include_outcomes,
143
+ )
144
+ max_flat_dim = max(max_flat_dim, len(row["action_flat"]))
145
+ rows.append(row)
146
+ emitted_for_group += 1
147
+ task_counts[str(record.task_id)] += 1
148
+ label_counts[label] += 1
149
+ if emitted_for_group:
150
+ group_hashes.add(_hash_id(group[0].group_id))
151
+ state_hashes.add(_hash_id(group[0].state_hash))
152
+ if len(rows) >= args.shard_size:
153
+ shard_paths.append(_write_shard(np, out_dir, rows, len(shard_paths)))
154
+ rows = []
155
+ if rows:
156
+ shard_paths.append(_write_shard(np, out_dir, rows, len(shard_paths)))
157
+
158
+ index = {
159
+ "schema_version": 1,
160
+ "format": "cil_charts_npz",
161
+ "dataset": str(args.dataset),
162
+ "split": args.split,
163
+ "epsilon": args.epsilon,
164
+ "include_outcomes": include_outcomes,
165
+ "audience": "train_retrieval" if args.split == "train" else "evaluator_only",
166
+ "retrieval_index_allowed": args.split == "train",
167
+ "deployment_clean": args.split == "train",
168
+ "base_candidate_types": list(base_candidate_types),
169
+ "num_groups_scanned": group_count,
170
+ "num_groups_exported": len(group_hashes),
171
+ "num_rows": sum(int(item["num_rows"]) for item in shard_paths),
172
+ "max_flat_action_dim": max_flat_dim,
173
+ "label_counts": dict(sorted(label_counts.items())),
174
+ "task_counts": dict(sorted(task_counts.items())),
175
+ "skip_counts": dict(sorted(counters.items())),
176
+ "group_hashes": sorted(group_hashes),
177
+ "state_hashes": sorted(state_hashes),
178
+ "shards": shard_paths,
179
+ "leakage_contract": {
180
+ "train_split_only_for_retrieval": True,
181
+ "nontrain_outcomes_are_evaluator_only": True,
182
+ "deployment_must_not_read_outcomes": args.split != "train",
183
+ },
184
+ }
185
+ index["content_hash"] = _content_hash(index)
186
+ (out_dir / "index.json").write_text(json.dumps(index, indent=2, sort_keys=True) + "\n")
187
+ print(json.dumps({k: index[k] for k in ("split", "num_groups_exported", "num_rows", "content_hash")}, indent=2))
188
+ return 0
189
+
190
+
191
+ def _row_from_record(
192
+ record: CILRecord,
193
+ *,
194
+ base: CILRecord,
195
+ split: str,
196
+ action: list[list[float]],
197
+ base_action: list[list[float]],
198
+ delta_action: list[list[float]],
199
+ utility: float,
200
+ base_utility: float,
201
+ delta_utility: float,
202
+ label: str,
203
+ include_outcomes: bool,
204
+ ) -> dict[str, Any]:
205
+ outcome = OutcomeVector.from_reward(record.reward, failure=record.failure)
206
+ metadata = {
207
+ "group_id": record.group_id,
208
+ "state_hash": record.state_hash,
209
+ "task_id": record.task_id,
210
+ "split_id": split,
211
+ "instruction": record.instruction,
212
+ "observation_ref": record.observation_ref,
213
+ "record_id": record.record_id,
214
+ "candidate_type": record.candidate_type,
215
+ "base_record_id": base.record_id,
216
+ "base_candidate_type": base.candidate_type,
217
+ "action_id": record.action_chunk.action_id,
218
+ "base_action_id": base.action_chunk.action_id,
219
+ "scene_id": record.scene_id,
220
+ "rank_within_group": record.rank_within_group,
221
+ "failure_type": record.failure.type if record.failure else None,
222
+ "source_dataset": record.metadata.get("source_dataset"),
223
+ }
224
+ return {
225
+ "group_id": record.group_id,
226
+ "state_hash": record.state_hash,
227
+ "task_id": record.task_id,
228
+ "record_id": record.record_id,
229
+ "candidate_type": record.candidate_type,
230
+ "label": label,
231
+ "label_id": LABEL_TO_ID[label],
232
+ "shape": list(action_shape(action)),
233
+ "action_flat": _flatten(action),
234
+ "base_action_flat": _flatten(base_action),
235
+ "delta_action_flat": _flatten(delta_action),
236
+ "utility": utility,
237
+ "base_utility": base_utility,
238
+ "delta_utility": delta_utility,
239
+ "outcome_vector": [
240
+ outcome.success,
241
+ outcome.progress,
242
+ outcome.contact_quality,
243
+ outcome.safety_violation,
244
+ outcome.task_stage_quality,
245
+ outcome.smoothness,
246
+ outcome.recovery,
247
+ ]
248
+ if include_outcomes
249
+ else [math.nan] * 7,
250
+ "metadata_json": json.dumps(metadata, sort_keys=True),
251
+ }
252
+
253
+
254
+ def _write_shard(np: Any, out_dir: Path, rows: list[dict[str, Any]], shard_index: int) -> dict[str, Any]:
255
+ max_dim = max((len(row["action_flat"]) for row in rows), default=0)
256
+ path = out_dir / f"charts_{shard_index:05d}.npz"
257
+ np.savez_compressed(
258
+ path,
259
+ action=_pad(np, [row["action_flat"] for row in rows], max_dim),
260
+ base_action=_pad(np, [row["base_action_flat"] for row in rows], max_dim),
261
+ delta_action=_pad(np, [row["delta_action_flat"] for row in rows], max_dim),
262
+ action_shape=np.asarray([row["shape"] for row in rows], dtype=np.int32),
263
+ utility=np.asarray([row["utility"] for row in rows], dtype=np.float32),
264
+ base_utility=np.asarray([row["base_utility"] for row in rows], dtype=np.float32),
265
+ delta_utility=np.asarray([row["delta_utility"] for row in rows], dtype=np.float32),
266
+ label_id=np.asarray([row["label_id"] for row in rows], dtype=np.int8),
267
+ outcome_vector=np.asarray([row["outcome_vector"] for row in rows], dtype=np.float32),
268
+ group_id=np.asarray([row["group_id"] for row in rows]),
269
+ state_hash=np.asarray([row["state_hash"] for row in rows]),
270
+ task_id=np.asarray([row["task_id"] for row in rows]),
271
+ record_id=np.asarray([row["record_id"] for row in rows]),
272
+ candidate_type=np.asarray([row["candidate_type"] for row in rows]),
273
+ label=np.asarray([row["label"] for row in rows]),
274
+ metadata_json=np.asarray([row["metadata_json"] for row in rows]),
275
+ )
276
+ return {
277
+ "path": path.name,
278
+ "num_rows": len(rows),
279
+ "sha256": _sha256(path),
280
+ }
281
+
282
+
283
+ def _pad(np: Any, vectors: list[list[float]], width: int) -> Any:
284
+ array = np.full((len(vectors), width), np.nan, dtype=np.float32)
285
+ for index, vector in enumerate(vectors):
286
+ array[index, : len(vector)] = np.asarray(vector, dtype=np.float32)
287
+ return array
288
+
289
+
290
+ def _flatten(values: list[list[float]]) -> list[float]:
291
+ return [float(value) for row in values for value in row]
292
+
293
+
294
+ def _parse_csv(value: str) -> tuple[str, ...]:
295
+ return tuple(item.strip() for item in value.split(",") if item.strip())
296
+
297
+
298
+ def _hash_id(value: str) -> str:
299
+ return hashlib.sha256(str(value).encode()).hexdigest()
300
+
301
+
302
+ def _sha256(path: Path) -> str:
303
+ digest = hashlib.sha256()
304
+ with path.open("rb") as handle:
305
+ for chunk in iter(lambda: handle.read(1024 * 1024), b""):
306
+ digest.update(chunk)
307
+ return digest.hexdigest()
308
+
309
+
310
+ def _content_hash(index: dict[str, Any]) -> str:
311
+ payload = dict(index)
312
+ payload.pop("content_hash", None)
313
+ return hashlib.sha256(json.dumps(payload, sort_keys=True).encode()).hexdigest()
314
+
315
+
316
+ if __name__ == "__main__":
317
+ raise SystemExit(main())