anhtld commited on
Commit
eeb1e6f
·
verified ·
1 Parent(s): 7fb1861

auto-sync 2026-07-02T23:07:00Z workspace (part 3)

Browse files
workspace/scripts/export_cil_charts.py CHANGED
@@ -23,6 +23,7 @@ from dovla_cil.generation.tangent_targets import ( # noqa: E402
23
  choose_base_record,
24
  label_from_delta_utility,
25
  record_utility,
 
26
  subtract_actions,
27
  )
28
 
@@ -45,6 +46,18 @@ def main(argv: list[str] | None = None) -> int:
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)
@@ -73,6 +86,7 @@ def main(argv: list[str] | None = None) -> int:
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
@@ -83,6 +97,12 @@ def main(argv: list[str] | None = None) -> int:
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()
@@ -92,6 +112,7 @@ def main(argv: list[str] | None = None) -> int:
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():
@@ -101,6 +122,16 @@ def main(argv: list[str] | None = None) -> int:
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
@@ -160,6 +191,9 @@ def main(argv: list[str] | None = None) -> int:
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",
@@ -167,21 +201,25 @@ def main(argv: list[str] | None = None) -> int:
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))
@@ -203,36 +241,59 @@ def _row_from_record(
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,
@@ -253,12 +314,20 @@ def _row_from_record(
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),
@@ -268,8 +337,13 @@ def _write_shard(np: Any, out_dir: Path, rows: list[dict[str, Any]], shard_index
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
  )
@@ -291,10 +365,73 @@ 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
 
@@ -313,5 +450,32 @@ def _content_hash(index: dict[str, Any]) -> str:
313
  return hashlib.sha256(json.dumps(payload, sort_keys=True).encode()).hexdigest()
314
 
315
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316
  if __name__ == "__main__":
317
  raise SystemExit(main())
 
23
  choose_base_record,
24
  label_from_delta_utility,
25
  record_utility,
26
+ spline_tangent_summary,
27
  subtract_actions,
28
  )
29
 
 
46
  help="Output split directory, e.g. data/cil_charts/train.",
47
  )
48
  parser.add_argument("--split", default="train")
49
+ parser.add_argument(
50
+ "--split-policy",
51
+ choices=("explicit", "stable-hash"),
52
+ default="explicit",
53
+ help="Use explicit split for all groups or deterministic group-id hash splits.",
54
+ )
55
+ parser.add_argument(
56
+ "--split-fractions",
57
+ default="0.70,0.15,0.15",
58
+ help="Train,val,test fractions for --split-policy stable-hash.",
59
+ )
60
+ parser.add_argument("--split-seed", type=int, default=0)
61
  parser.add_argument("--epsilon", type=float, default=0.0)
62
  parser.add_argument("--max-groups", type=int, default=None)
63
  parser.add_argument("--shard-size", type=int, default=50000)
 
86
  parser.error("--max-groups must be positive when provided")
87
  if args.shard_size <= 0:
88
  parser.error("--shard-size must be positive")
89
+ split_fractions = _parse_split_fractions(args.split_fractions)
90
 
91
  try:
92
  import numpy as np
 
97
  dataset = CILDataset(args.dataset)
98
  out_dir = args.out_dir
99
  out_dir.mkdir(parents=True, exist_ok=True)
100
+ for stale in list(out_dir.glob("charts_*.npz")) + [
101
+ out_dir / "index.json",
102
+ out_dir / "candidate_type_counts.json",
103
+ ]:
104
+ if stale.exists():
105
+ stale.unlink()
106
  rows: list[dict[str, Any]] = []
107
  shard_paths: list[dict[str, Any]] = []
108
  counters: Counter[str] = Counter()
 
112
  state_hashes: set[str] = set()
113
  max_flat_dim = 0
114
  group_count = 0
115
+ skipped_by_split = 0
116
  base_candidate_types = _parse_csv(args.base_candidate_types)
117
 
118
  for group in dataset.iter_groups():
 
122
  if not group:
123
  counters["empty_group"] += 1
124
  continue
125
+ assigned_split = _assign_split(
126
+ group[0].group_id,
127
+ policy=args.split_policy,
128
+ split=args.split,
129
+ fractions=split_fractions,
130
+ seed=args.split_seed,
131
+ )
132
+ if assigned_split != args.split:
133
+ skipped_by_split += 1
134
+ continue
135
  base = choose_base_record(group, base_candidate_types=base_candidate_types)
136
  if base is None:
137
  counters["missing_base"] += 1
 
191
  "format": "cil_charts_npz",
192
  "dataset": str(args.dataset),
193
  "split": args.split,
194
+ "split_policy": args.split_policy,
195
+ "split_fractions": split_fractions,
196
+ "split_seed": args.split_seed,
197
  "epsilon": args.epsilon,
198
  "include_outcomes": include_outcomes,
199
  "audience": "train_retrieval" if args.split == "train" else "evaluator_only",
 
201
  "deployment_clean": args.split == "train",
202
  "base_candidate_types": list(base_candidate_types),
203
  "num_groups_scanned": group_count,
204
+ "num_groups_skipped_by_split": skipped_by_split,
205
  "num_groups_exported": len(group_hashes),
206
  "num_rows": sum(int(item["num_rows"]) for item in shard_paths),
207
  "max_flat_action_dim": max_flat_dim,
208
  "label_counts": dict(sorted(label_counts.items())),
209
  "task_counts": dict(sorted(task_counts.items())),
210
+ "candidate_type_counts": _sum_shard_counter(out_dir, "candidate_type_counts.json"),
211
  "skip_counts": dict(sorted(counters.items())),
212
  "group_hashes": sorted(group_hashes),
213
  "state_hashes": sorted(state_hashes),
214
  "shards": shard_paths,
215
+ "deployment_candidate_excludes_expert": True,
216
  "leakage_contract": {
217
  "train_split_only_for_retrieval": True,
218
  "nontrain_outcomes_are_evaluator_only": True,
219
  "deployment_must_not_read_outcomes": args.split != "train",
220
  },
221
  }
222
+ index["split_hash"] = _split_hash(index)
223
  index["content_hash"] = _content_hash(index)
224
  (out_dir / "index.json").write_text(json.dumps(index, indent=2, sort_keys=True) + "\n")
225
  print(json.dumps({k: index[k] for k in ("split", "num_groups_exported", "num_rows", "content_hash")}, indent=2))
 
241
  include_outcomes: bool,
242
  ) -> dict[str, Any]:
243
  outcome = OutcomeVector.from_reward(record.reward, failure=record.failure)
244
+ seed = record.metadata.get("episode_seed", record.metadata.get("random_seed"))
245
+ branch_family = str(record.candidate_type)
246
+ is_base_branch = record.record_id == base.record_id
247
+ is_expert_branch = branch_family == "expert" or branch_family.endswith("_expert")
248
+ tangent_summary = spline_tangent_summary(delta_action)
249
+ source_policy_name = str(record.metadata.get("source_policy_name", "unknown"))
250
  metadata = {
251
+ "chart_id": record.group_id,
252
  "group_id": record.group_id,
253
+ "split": split,
254
  "state_hash": record.state_hash,
255
  "task_id": record.task_id,
256
+ "seed": seed,
257
  "split_id": split,
258
  "instruction": record.instruction,
259
+ "observation_embedding_path": record.metadata.get("observation_embedding_path"),
260
  "observation_ref": record.observation_ref,
261
  "record_id": record.record_id,
262
  "candidate_type": record.candidate_type,
263
+ "branch_family": branch_family,
264
  "base_record_id": base.record_id,
265
  "base_candidate_type": base.candidate_type,
266
+ "source_policy_name": source_policy_name,
267
  "action_id": record.action_chunk.action_id,
268
  "base_action_id": base.action_chunk.action_id,
269
  "scene_id": record.scene_id,
270
  "rank_within_group": record.rank_within_group,
271
  "failure_type": record.failure.type if record.failure else None,
272
+ "is_expert_branch": is_expert_branch,
273
+ "is_base_branch": is_base_branch,
274
+ "xi_obj": None,
275
  "source_dataset": record.metadata.get("source_dataset"),
276
  }
277
  return {
278
+ "chart_id": record.group_id,
279
  "group_id": record.group_id,
280
+ "split": split,
281
  "state_hash": record.state_hash,
282
  "task_id": record.task_id,
283
+ "seed": "" if seed is None else str(seed),
284
  "record_id": record.record_id,
285
  "candidate_type": record.candidate_type,
286
+ "branch_family": branch_family,
287
+ "source_policy_name": source_policy_name,
288
+ "is_expert_branch": is_expert_branch,
289
+ "is_base_branch": is_base_branch,
290
  "label": label,
291
  "label_id": LABEL_TO_ID[label],
292
  "shape": list(action_shape(action)),
293
  "action_flat": _flatten(action),
294
  "base_action_flat": _flatten(base_action),
295
  "delta_action_flat": _flatten(delta_action),
296
+ "spline_tangent_code": _spline_tangent_code(delta_action),
297
  "utility": utility,
298
  "base_utility": base_utility,
299
  "delta_utility": delta_utility,
 
314
 
315
  def _write_shard(np: Any, out_dir: Path, rows: list[dict[str, Any]], shard_index: int) -> dict[str, Any]:
316
  max_dim = max((len(row["action_flat"]) for row in rows), default=0)
317
+ candidate_type_counts = Counter(str(row["candidate_type"]) for row in rows)
318
+ _write_counter(out_dir / "candidate_type_counts.json", candidate_type_counts)
319
  path = out_dir / f"charts_{shard_index:05d}.npz"
320
  np.savez_compressed(
321
  path,
322
+ chart_id=np.asarray([row["chart_id"] for row in rows]),
323
+ split=np.asarray([row["split"] for row in rows]),
324
  action=_pad(np, [row["action_flat"] for row in rows], max_dim),
325
  base_action=_pad(np, [row["base_action_flat"] for row in rows], max_dim),
326
  delta_action=_pad(np, [row["delta_action_flat"] for row in rows], max_dim),
327
+ spline_tangent_code=np.asarray(
328
+ [row["spline_tangent_code"] for row in rows],
329
+ dtype=np.float32,
330
+ ),
331
  action_shape=np.asarray([row["shape"] for row in rows], dtype=np.int32),
332
  utility=np.asarray([row["utility"] for row in rows], dtype=np.float32),
333
  base_utility=np.asarray([row["base_utility"] for row in rows], dtype=np.float32),
 
337
  group_id=np.asarray([row["group_id"] for row in rows]),
338
  state_hash=np.asarray([row["state_hash"] for row in rows]),
339
  task_id=np.asarray([row["task_id"] for row in rows]),
340
+ seed=np.asarray([row["seed"] for row in rows]),
341
  record_id=np.asarray([row["record_id"] for row in rows]),
342
  candidate_type=np.asarray([row["candidate_type"] for row in rows]),
343
+ branch_family=np.asarray([row["branch_family"] for row in rows]),
344
+ source_policy_name=np.asarray([row["source_policy_name"] for row in rows]),
345
+ is_expert_branch=np.asarray([row["is_expert_branch"] for row in rows], dtype=bool),
346
+ is_base_branch=np.asarray([row["is_base_branch"] for row in rows], dtype=bool),
347
  label=np.asarray([row["label"] for row in rows]),
348
  metadata_json=np.asarray([row["metadata_json"] for row in rows]),
349
  )
 
365
  return [float(value) for row in values for value in row]
366
 
367
 
368
+ def _spline_tangent_code(delta_action: list[list[float]]) -> list[float]:
369
+ summary = spline_tangent_summary(delta_action)
370
+ code = summary.get("spline_code", {}) if isinstance(summary, dict) else {}
371
+ pieces = [
372
+ code.get("endpoint_delta_position", []),
373
+ code.get("midpoint_delta_position", []),
374
+ code.get("endpoint_delta_rotation_approx", []),
375
+ [code.get("gripper_gate_shift", 0.0)],
376
+ [code.get("gripper_close_strength", 0.0)],
377
+ [code.get("time_scale", 1.0)],
378
+ [code.get("lift_bias", 0.0)],
379
+ code.get("approach_axis_bias", []),
380
+ ]
381
+ flat = [float(value) for piece in pieces for value in piece]
382
+ # Stable 21D keyframe code: start/mid/end full residual rows when available,
383
+ # padded/truncated for common horizon-16, action-dim-7 chunks.
384
+ keyframes: list[float] = []
385
+ if delta_action:
386
+ for row_index in (0, len(delta_action) // 2, len(delta_action) - 1):
387
+ keyframes.extend(float(value) for value in delta_action[row_index])
388
+ if keyframes:
389
+ flat = keyframes
390
+ if len(flat) < 21:
391
+ flat.extend([0.0] * (21 - len(flat)))
392
+ return flat[:21]
393
+
394
+
395
  def _parse_csv(value: str) -> tuple[str, ...]:
396
  return tuple(item.strip() for item in value.split(",") if item.strip())
397
 
398
 
399
+ def _parse_split_fractions(value: str) -> dict[str, float]:
400
+ parts = [float(item.strip()) for item in value.split(",") if item.strip()]
401
+ if len(parts) != 3:
402
+ raise ValueError("--split-fractions must contain train,val,test values")
403
+ if any(part < 0.0 for part in parts) or sum(parts) <= 0.0:
404
+ raise ValueError("--split-fractions must be non-negative with positive sum")
405
+ total = sum(parts)
406
+ train, val, test = [part / total for part in parts]
407
+ return {"train": train, "val": val, "test": test}
408
+
409
+
410
+ def _assign_split(
411
+ group_id: str,
412
+ *,
413
+ policy: str,
414
+ split: str,
415
+ fractions: dict[str, float],
416
+ seed: int,
417
+ ) -> str:
418
+ if policy == "explicit":
419
+ return split
420
+ value = _stable_uniform(group_id, seed=seed)
421
+ train_cut = fractions["train"]
422
+ val_cut = train_cut + fractions["val"]
423
+ if value < train_cut:
424
+ return "train"
425
+ if value < val_cut:
426
+ return "val"
427
+ return "test"
428
+
429
+
430
+ def _stable_uniform(value: str, *, seed: int) -> float:
431
+ digest = hashlib.sha256(f"{seed}:{value}".encode("utf-8")).digest()
432
+ return int.from_bytes(digest[:8], "big") / float(2**64)
433
+
434
+
435
  def _hash_id(value: str) -> str:
436
  return hashlib.sha256(str(value).encode()).hexdigest()
437
 
 
450
  return hashlib.sha256(json.dumps(payload, sort_keys=True).encode()).hexdigest()
451
 
452
 
453
+ def _split_hash(index: dict[str, Any]) -> str:
454
+ payload = {
455
+ "split": index.get("split"),
456
+ "split_policy": index.get("split_policy"),
457
+ "split_fractions": index.get("split_fractions"),
458
+ "split_seed": index.get("split_seed"),
459
+ "group_hashes": index.get("group_hashes", []),
460
+ "state_hashes": index.get("state_hashes", []),
461
+ }
462
+ return hashlib.sha256(json.dumps(payload, sort_keys=True).encode()).hexdigest()
463
+
464
+
465
+ def _write_counter(path: Path, counts: Counter[str]) -> None:
466
+ existing: Counter[str] = Counter()
467
+ if path.exists():
468
+ existing.update(json.loads(path.read_text()))
469
+ existing.update(counts)
470
+ path.write_text(json.dumps(dict(sorted(existing.items())), indent=2, sort_keys=True) + "\n")
471
+
472
+
473
+ def _sum_shard_counter(out_dir: Path, filename: str) -> dict[str, int]:
474
+ path = out_dir / filename
475
+ if not path.exists():
476
+ return {}
477
+ return {str(key): int(value) for key, value in json.loads(path.read_text()).items()}
478
+
479
+
480
  if __name__ == "__main__":
481
  raise SystemExit(main())