twanghcmut/backup-foundation-physics / scripts /sample_appearance_episodes.py
twanghcmut's picture
download
raw
6.05 kB
#!/usr/bin/env python
"""Pick a lab-stratified episode set for the appearance run, and fetch it.
Why stratify rather than take the pool's head: the pool is sorted by uuid, so
its head is one lab, and the 63 episodes already on disk are effectively one lab
too (62 of them share the camera pair 22008760/24400334). A LoRA trained on one
lab learns that lab's bench, lighting and gripper, which is the opposite of what
an *appearance* fine-tune is for -- the robot is the constant across DROID and
everything else is the nuisance variable. Proportional-with-a-floor sampling
keeps the big labs dominant (they really are most of DROID) while guaranteeing
the small ones are represented at all: RAD has 512 episodes against TRI's 12300,
and pure proportional sampling at n=400 would give it 4.
Intrinsics are per physical camera, and the appearance pipeline falls back to a
median-ZED guess for a serial it has never seen (principal point off by up to
29 px at 720). Episodes whose serials are absent from
``configs/droid_camera_intrinsics.json`` are therefore reported, not silently
included -- pass ``--allow-unknown-serials`` to take them anyway.
Usage:
PYTHONPATH=src /home/quang/miniconda3/envs/fpgm/bin/python \\
scripts/sample_appearance_episodes.py --n 400 --out configs/appearance_400.txt
... then --download to fetch mp4 + trajectory.h5 + metadata.json for each.
"""
from __future__ import annotations
import argparse
import collections
import json
import random
import subprocess
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
def stratified(uuids: list[str], n: int, floor: int, seed: int) -> list[str]:
"""``n`` uuids, proportional per lab but never fewer than ``floor`` per lab.
Labs with fewer than ``floor`` episodes contribute all of them. The residual
from rounding is handed to the largest labs, so the total is exactly ``n``
(or the pool size, if that is smaller).
"""
by_lab: dict[str, list[str]] = collections.defaultdict(list)
for u in uuids:
by_lab[u.split("+")[0]].append(u)
rng = random.Random(seed)
for v in by_lab.values():
rng.shuffle(v)
total = sum(len(v) for v in by_lab.values())
quota = {lab: max(floor, round(n * len(v) / total)) for lab, v in by_lab.items()}
quota = {lab: min(q, len(by_lab[lab])) for lab, q in quota.items()}
# Reconcile to exactly n by walking labs largest-first.
order = sorted(by_lab, key=lambda k: -len(by_lab[k]))
while sum(quota.values()) > n:
for lab in order:
if quota[lab] > floor and sum(quota.values()) > n:
quota[lab] -= 1
while sum(quota.values()) < min(n, total):
for lab in order:
if quota[lab] < len(by_lab[lab]) and sum(quota.values()) < n:
quota[lab] += 1
picked: list[str] = []
for lab in order:
picked.extend(by_lab[lab][: quota[lab]])
return sorted(picked)
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--pool", type=Path, default=REPO_ROOT / "configs/appearance_uuid_pool.txt")
ap.add_argument("--cameras-dir", type=Path,
default=REPO_ROOT / "data/pointworld/droid/cameras")
ap.add_argument("--intrinsics", type=Path,
default=REPO_ROOT / "configs/droid_camera_intrinsics.json")
ap.add_argument("--episodes-root", type=Path, default=REPO_ROOT / "data/droid_raw")
ap.add_argument("--n", type=int, default=400)
ap.add_argument("--floor", type=int, default=8, help="minimum episodes per lab")
ap.add_argument("--seed", type=int, default=0)
ap.add_argument("--out", type=Path, required=True)
ap.add_argument("--allow-unknown-serials", action="store_true")
ap.add_argument("--download", action="store_true",
help="fetch the sampled episodes with download_droid_episodes.py")
ap.add_argument("--download-batch", type=int, default=25)
args = ap.parse_args()
pool = [u for u in args.pool.read_text().split() if u]
known = set(json.loads(args.intrinsics.read_text())["cameras"])
have = {p.name for p in args.episodes_root.iterdir() if p.is_dir()} \
if args.episodes_root.exists() else set()
picked = stratified(pool, args.n, args.floor, args.seed)
kept, unknown = [], []
for u in picked:
cam = args.cameras_dir / f"{u}_cameras.json"
serials = [k for k in json.loads(cam.read_text()) if k.isdigit()]
(kept if all(s in known for s in serials) else unknown).append(u)
if args.allow_unknown_serials:
kept += unknown
args.out.write_text("\n".join(sorted(kept)) + "\n")
by_lab = collections.Counter(u.split("+")[0] for u in kept)
print(f"{len(kept)} episodes -> {args.out} ({len(unknown)} dropped for unknown "
f"camera serials{'; kept anyway' if args.allow_unknown_serials else ''})")
for lab, c in by_lab.most_common():
print(f" {lab:12s} {c}")
todo = [u for u in kept if u not in have]
print(f"{len(have & set(kept))} already on disk, {len(todo)} to download "
f"(~{len(todo) * 13 / 1024:.1f} GB)")
if args.download and todo:
script = REPO_ROOT / "scripts/download_droid_episodes.py"
for i in range(0, len(todo), args.download_batch):
batch = todo[i:i + args.download_batch]
print(f" downloading {i + 1}-{i + len(batch)} / {len(todo)}", flush=True)
# Batched rather than one big argv: the downloader exits non-zero if
# ANY episode fails, and a batch bounds how much is re-run on a retry.
subprocess.run([sys.executable, str(script),
"--data-dir", str(args.episodes_root),
"--cameras-dir", str(args.cameras_dir),
"--uuids", *batch], check=False)
return 0
if __name__ == "__main__":
raise SystemExit(main())

Xet Storage Details

Size:
6.05 kB
·
Xet hash:
564898e02660d0320edf71d42f57693b59be3e9398e938a890fab599bd6e8f47

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.