| |
| from __future__ import annotations |
|
|
| import argparse |
| import json |
| import sys |
| from pathlib import Path |
| from typing import Any |
|
|
| PROJECT_ROOT = Path(__file__).resolve().parents[1] |
| if str(PROJECT_ROOT) not in sys.path: |
| sys.path.insert(0, str(PROJECT_ROOT)) |
|
|
| from dovla_cil.data.datasets import CILDataset |
| from dovla_cil.eval.lattice_eval import _validation_group_ids |
| from dovla_cil.eval.maniskill_policy_rollout import _numeric_action_values |
| from dovla_cil.models.dovla import ( |
| DoVLAConfig, |
| DoVLAModel, |
| load_model_state, |
| vectorize_toy_observation, |
| ) |
|
|
|
|
| def main(argv: list[str] | None = None) -> int: |
| parser = argparse.ArgumentParser( |
| description="Export policy BC targets chosen by a trained field on CIL action lattices." |
| ) |
| parser.add_argument("--checkpoint", type=Path, required=True) |
| parser.add_argument("--dataset", type=Path, required=True) |
| parser.add_argument("--out", type=Path, required=True) |
| parser.add_argument("--device", default="auto") |
| parser.add_argument("--split", choices=("train", "val", "all"), default="train") |
| parser.add_argument("--batch-groups", type=int, default=32) |
| parser.add_argument( |
| "--exclude-types", |
| default="expert", |
| help="Comma-separated candidate_type values to exclude before field selection.", |
| ) |
| parser.add_argument("--max-groups", type=int, default=None) |
| args = parser.parse_args(argv) |
|
|
| if args.batch_groups <= 0: |
| parser.error("--batch-groups must be positive") |
| if args.max_groups is not None and args.max_groups <= 0: |
| parser.error("--max-groups must be positive when provided") |
|
|
| try: |
| import torch |
| except ImportError as exc: |
| raise ImportError("export_field_selected_policy_targets.py requires torch") from exc |
|
|
| checkpoint = torch.load( |
| args.checkpoint, |
| map_location=_resolve_device(args.device), |
| weights_only=False, |
| ) |
| model_config = DoVLAConfig(**checkpoint["model_config"]) |
| if model_config.observation_mode != "state": |
| raise ValueError("field-selected target export currently supports state observations only") |
| device = _resolve_device(args.device) |
| model = DoVLAModel(model_config).to(device) |
| load_model_state(model, checkpoint) |
| model.eval() |
|
|
| dataset = CILDataset(args.dataset) |
| trainer_config = checkpoint.get("trainer_config", {}) |
| val_ids = set( |
| _validation_group_ids( |
| dataset.group_ids, |
| val_fraction=float(trainer_config.get("val_fraction", 0.2)), |
| seed=int(trainer_config.get("seed", 0)), |
| ) |
| ) |
| if args.split == "train": |
| group_ids = [group_id for group_id in dataset.group_ids if group_id not in val_ids] |
| elif args.split == "val": |
| group_ids = [group_id for group_id in dataset.group_ids if group_id in val_ids] |
| else: |
| group_ids = list(dataset.group_ids) |
| if args.max_groups is not None: |
| group_ids = group_ids[: args.max_groups] |
|
|
| excluded = {item.strip() for item in args.exclude_types.split(",") if item.strip()} |
| targets: dict[str, dict[str, Any]] = {} |
| counts: dict[str, int] = {} |
| with torch.no_grad(): |
| for start in range(0, len(group_ids), args.batch_groups): |
| for group_id in group_ids[start : start + args.batch_groups]: |
| records = [ |
| record |
| for record in dataset.get_group(group_id) |
| if record.candidate_type not in excluded |
| ] |
| if not records: |
| records = dataset.get_group(group_id) |
| if not records: |
| continue |
| obs = torch.tensor( |
| [ |
| vectorize_toy_observation( |
| records[0].observation_inline or {}, |
| obs_dim=model_config.obs_dim, |
| ) |
| ] |
| * len(records), |
| dtype=torch.float32, |
| device=device, |
| ) |
| actions = torch.tensor( |
| [_numeric_action_values(record) for record in records], |
| dtype=torch.float32, |
| device=device, |
| ) |
| field = model.forward_field( |
| obs, |
| [record.instruction for record in records], |
| actions, |
| ) |
| best_idx = int(torch.argmax(field["potential"].reshape(len(records))).item()) |
| best = records[best_idx] |
| counts[best.candidate_type] = counts.get(best.candidate_type, 0) + 1 |
| targets[group_id] = { |
| "record_id": best.record_id, |
| "candidate_type": best.candidate_type, |
| "task_id": best.task_id, |
| "score": float(best.reward.score), |
| "rank_within_group": best.rank_within_group, |
| } |
|
|
| payload = { |
| "checkpoint": str(args.checkpoint), |
| "dataset": str(args.dataset), |
| "split": args.split, |
| "excluded_candidate_types": sorted(excluded), |
| "num_groups": len(group_ids), |
| "num_targets": len(targets), |
| "selected_candidate_type_counts": counts, |
| "targets": targets, |
| } |
| args.out.parent.mkdir(parents=True, exist_ok=True) |
| args.out.write_text(json.dumps(payload, indent=2) + "\n") |
| print(json.dumps({k: v for k, v in payload.items() if k != "targets"}, indent=2)) |
| print(f"Wrote {args.out}") |
| return 0 |
|
|
|
|
| def _resolve_device(device: str) -> str: |
| if device != "auto": |
| return device |
| try: |
| import torch |
| except ImportError: |
| return "cpu" |
| return "cuda" if torch.cuda.is_available() else "cpu" |
|
|
|
|
| if __name__ == "__main__": |
| raise SystemExit(main()) |
|
|