#!/usr/bin/env python3 """diag_fill_flags.py — 既存の補正データセットで, 実測角の抜けを埋めたコマの割合を測る (2026-11-14 依頼 196). datasets/ の名前に corr / deep_even10 が入るデータセットの, 補正エピソード (meta.json の source が pi05_policy) ごとに, 元の runs/pi05_policy/{run_stem}/ から build_correction_episodes.build_one を meta.json と同じ条件 (gap_fill の mode・trim_mid_idle・trim_hold) で /tmp に作り直し, state_source (コマごとの印) を数える. 作り直した state / actions / t がデータセットの episode.npz と一致しない本は数えない (理由を CSV に残す). データセット・元の記録は読むだけ (書かない). 書くのは /tmp と diag/fill_flags_20261114/ だけ. python3 pi05/diag_fill_flags.py """ import argparse import csv import json import sys from pathlib import Path from types import SimpleNamespace import numpy as np _HERE = Path(__file__).resolve().parent sys.path.insert(0, str(_HERE)) import build_correction_episodes as bce # noqa: E402 import build_demo_episodes as bde # noqa: E402 OUT_NAME = "fill_flags_20261114" TMP_ROOT = Path("/tmp") / OUT_NAME # 学習に使ったことがあるデータセットを先に並べる (依頼 196) TRAINED_FIRST = ("mecharm_redblock_box_teach_v1_trim2_corr_v5", "mecharm_redblock_box_deep_even10_v1", "mecharm_redblock_box_teach_v1_trim2_corr_v5_deep") def target_datasets(ds_root): names = sorted(p.name for p in ds_root.iterdir() if p.is_dir() and not p.name.startswith(".") and ("corr" in p.name or "deep_even10" in p.name)) return sorted(names, key=lambda n: (n not in TRAINED_FIRST, TRAINED_FIRST.index(n) if n in TRAINED_FIRST else 0, n)) def rebuild(run_stem, meta, cache): """meta.json と同じ条件で /tmp に作り直す. 戻り値: (npz の中身 dict / None, 理由).""" gf = meta.get("gap_fill") or {} key = (run_stem, gf.get("mode"), bool(meta.get("trim_mid_idle")), meta.get("trim_hold") is not None) if key in cache: return cache[key] out = TMP_ROOT / "rebuild" / "_".join(str(k) for k in key) / run_stem args = SimpleNamespace(gap_fill=gf.get("mode") or "linear", trim_hold=key[3], no_trim_mid_idle=not key[2], max_protrude_px=None, force=True) res = bce.build_one(bce.RUNS_ROOT / run_stem, out, args) if not res.get("used"): cache[key] = (None, f"作り直せない: {res.get('reason')}") else: z = np.load(out / "episode.npz") cache[key] = ({k: z[k] for k in z.files}, "ok") return cache[key] def main(): ap = argparse.ArgumentParser() ap.add_argument("--dataset", action="append", help="このデータセットだけ (複数可)") args = ap.parse_args() bde.ensure_ssd() ds_root = bde.DATA_ROOT / "datasets" out_dir = bde.DATA_ROOT / "diag" / OUT_NAME if out_dir.exists(): sys.exit(f"[ABORT] 出力先がもうある: {out_dir}") names = args.dataset or target_datasets(ds_root) rows, cache = [], {} for name in names: for mp in sorted((ds_root / name / "raw_episodes").glob("[!.]*/meta.json")): meta = json.loads(mp.read_text()) if not str(meta.get("source", "")).startswith("pi05_policy"): continue ep = mp.parent stem = meta.get("run_stem", ep.name) z = np.load(ep / "episode.npz") row = {"dataset": name, "episode": ep.name, "run_stem": stem, "gap_fill": (meta.get("gap_fill") or {}).get("mode"), "trim_hold": meta.get("trim_hold") is not None, "steps": len(z["state"]), "filled_steps": "", "filled_motion_steps": "", "filled_ratio": "", "gaps": (meta.get("gap_fill") or {}).get("gaps"), "motion_gaps": (meta.get("gap_fill") or {}).get("motion_gaps"), "status": ""} new, why = rebuild(stem, meta, cache) if new is None: row["status"] = why elif not all(k in new and new[k].shape == z[k].shape and new[k].tobytes() == z[k].tobytes() for k in ("state", "actions", "t")): row["status"] = "state が一致しない (数えない)" else: src = new["state_source"] n_f = int((src != bce.SRC_MEASURED).sum()) row.update(filled_steps=n_f, filled_motion_steps=int((src == bce.SRC_FILLED_MOVE).sum()), filled_ratio=round(n_f / len(src), 4), status="ok") rows.append(row) print(f"{name} {ep.name} {row['status']} {row['filled_steps']}/{row['steps']}") if not rows: # codex P2: 出力先を作る前に止める (空のまま作ると次に走らせられない) sys.exit("[ABORT] 対象の補正エピソードがない") out_dir.mkdir(parents=True) with (out_dir / "episodes.csv").open("w", newline="") as f: w = csv.DictWriter(f, fieldnames=list(rows[0].keys())) w.writeheader(); w.writerows(rows) summ = [] for name in names: rs = [r for r in rows if r["dataset"] == name] ok = [r for r in rs if r["status"] == "ok"] steps = sum(r["steps"] for r in ok); filled = sum(r["filled_steps"] for r in ok) top = max(ok, key=lambda r: r["filled_ratio"]) if ok else None summ.append({"dataset": name, "corr_episodes": len(rs), "counted": len(ok), "not_counted": len(rs) - len(ok), "steps": steps, "filled_steps": filled, "filled_motion_steps": sum(r["filled_motion_steps"] for r in ok), "filled_ratio": round(filled / steps, 4) if steps else "", "max_episode": top["episode"] if top else "", "max_episode_ratio": top["filled_ratio"] if top else ""}) with (out_dir / "summary.csv").open("w", newline="") as f: w = csv.DictWriter(f, fieldnames=list(summ[0].keys())) w.writeheader(); w.writerows(summ) (out_dir / "README.md").write_text( "# fill_flags_20261114 (依頼 196)\n\n" "既存の補正データセットで, 実測角の抜けを埋めたコマ (state_source が 1 または 2) の数と割合。\n" "`python3 pi05/diag_fill_flags.py` で作った。データセットは読むだけ。作り直しは /tmp。\n\n" "- state_source: 0 = 実測角 / 1 = 動いた抜けを埋めた値 / 2 = 動かなかった抜けを埋めた値\n" "- filled_ratio = (1 と 2 のコマ) / 全コマ。filled_motion_steps は 1 のコマの数\n" "- status が ok でない本 (作り直した state / actions / t がデータセットと一致しない・作り直せない) は数えない\n" "- episodes.csv: 本ごと / summary.csv: データセットごと (max_episode = 割合がいちばん多い本)\n") print(f"→ {out_dir}") return 0 if __name__ == "__main__": sys.exit(main())