#!/usr/bin/env python3 """rewind_correct.py — 実機推論の「巻き戻し (b)」と「関節の補正 (c)」の計算部分 (2026-10-29). アームにも daemon にも触らない (計算だけ). run_policy_client.py と build_correction_episodes.py の両方から使い, test_rewind_correct.py で確かめる. - rewind_waypoints: 実行した区間 (推論1回分 / 補正1回分) で送った目標を逆順にたどり, 始めの姿勢で終わる中継点の並びを作る - parse_correction_key: 補正モードのキー 1 文字を操作に読み替える - excluded_windows / step_keep_mask: 学習データから除く時間の窓 (巻き戻しで取り消した区間) """ import math import numpy as np # 補正の刻み (度). [ / ] で切り替える. 既定は 1° CORRECTION_STEPS = (0.5, 1.0, 2.0) CORRECTION_DEFAULT_STEP_INDEX = 1 # 記録の 1 コマごとの状態 (states.csv の stage 列 / top_frame_log.csv の mode 列) MODE_MODEL = "model" # モデルの動き (推論結果の実行・推論待ち・確認待ち) MODE_REWIND = "rewind" # 巻き戻し中 (補正の取り消しで戻る間も含む) MODE_CORRECT = "correct" # 補正中 MODE_HOME = "home" # HOME へ戻る間 MODE_WAIT = "wait" # 推論のあとの確認で入力 (Enter / b / c / y/n) を待っている間. # 人がブロックや箱を手で置き直すと手が写るので学習データに使わない (2026-10-29 依頼 27) TRAIN_MODES = (MODE_MODEL, MODE_CORRECT) # 学習データに使う状態 (rewind / home / wait は使わない) # つかんだ区間 (グリッパーを閉じた推論 1 回分) を巻き戻すときの答え (log.jsonl の rewind 行の grasp_undo) GRASP_PLACED = "placed" # y = 持っている: つかんだ位置まで下ろして開き (置いて), 始めの姿勢へ戻る GRASP_DROPPED = "dropped" # n = 落とした: 今の位置ですぐ開いてから, 始めの姿勢へ戻る CORRECTION_HELP = ( " [補正] 1〜6=関節を選ぶ (j0〜j5) / + か . = 選んだ関節を +1 刻み / - か , = -1 刻み\n" " [ ] = 刻みを 0.5° / 1° / 2° で切り替え / g = グリッパー開閉\n" " Enter = 補正を終えてモデルの推論に戻る / b = 補正を取り消して始めの姿勢へ戻る\n" " ? = この一覧" ) def split_leg(a, b, max_move): """a から b へ, どの関節も 1 回 max_move 以内になるよう等分した点を返す (a は含めず b は含む).""" a = np.asarray(a, dtype=float) b = np.asarray(b, dtype=float) span = float(np.max(np.abs(b - a))) if len(a) else 0.0 n = max(1, int(math.ceil(span / max_move - 1e-9))) if max_move and max_move > 0 else 1 return [a + (b - a) * (k / n) for k in range(1, n + 1)] def rewind_waypoints(sent_targets, start_pose, current=None, max_move=None): """実行した区間を逆にたどる中継点の並び. sent_targets: その区間で送った目標 (送った順). start_pose: 区間を始めたときの姿勢 (推論なら measured_state の 6 関節, 補正なら補正を始めたときの実測). 戻り値は「最後に送った目標の 1 つ手前 → … → 最初に送った目標 → start_pose」. (最後に送った目標は今いる場所なので送らない.) max_move を渡すと, current (既定は最後に送った目標) から順に, 1 回の移動が max_move を超える区間を等分する (apply_limits の移動量制限で途中止まりにしないため). """ start = np.asarray(start_pose, dtype=float)[:6] sent = [np.asarray(x, dtype=float)[:6] for x in sent_targets] pts = sent[:-1][::-1] + [start] if not max_move: return pts prev = (np.asarray(current, dtype=float)[:6] if current is not None else (sent[-1] if sent else start)) out = [] for p in pts: out.extend(split_leg(prev, p, max_move)) prev = p return out def parse_correction_key(ch): """補正モードの 1 文字を操作に読み替える. 戻り値は (操作, 値) か None (関係ないキー). ("select", j) / ("move", +1|-1) / ("step", +1|-1) / ("grip", None) / ("accept", None) / ("cancel", None) / ("help", None) """ if ch in ("1", "2", "3", "4", "5", "6"): return ("select", int(ch) - 1) if ch in ("+", "=", "."): return ("move", +1) if ch in ("-", "_", ","): return ("move", -1) if ch == "]": return ("step", +1) if ch == "[": return ("step", -1) if ch in ("g", "G"): return ("grip", None) if ch in ("\n", "\r", ""): # "" = 入力の終わり (EOF). 姿勢はそのまま補正を終える return ("accept", None) if ch in ("b", "B"): return ("cancel", None) if ch == "?": return ("help", None) return None def next_step_index(i, d): return min(max(i + d, 0), len(CORRECTION_STEPS) - 1) def excluded_windows(records): """log.jsonl の記録から, 学習データに入れない時間の窓 [(t0, t1), ...] を作る. - 巻き戻した区間: 区間を始めた時刻 (segment_t[0]) から 巻き戻しが終わった時刻 (t_end) まで. 区間の実行・その後の推論待ち (間違った姿勢で止まっている間)・巻き戻しの動きをまとめて除く - 取り消した補正: 補正を始めた時刻から 始めの姿勢へ戻り終えた時刻まで """ out = [] for r in records: if r.get("type") == "rewind" and r.get("rewound"): seg = r.get("segment_t") or [None, None] t0 = seg[0] if seg[0] is not None else r.get("t_start") t1 = r.get("t_end") if t0 is not None and t1 is not None: out.append((float(t0), float(t1))) elif r.get("type") == "correction" and r.get("action") == "cancel": t0, t1 = r.get("t_start"), r.get("t_end") if t0 is not None and t1 is not None: out.append((float(t0), float(t1))) return out def mode_at(t, mode_ts, modes, default=MODE_MODEL): """各時刻の状態 (その時刻までで最後に切り替わった状態).""" t = np.asarray(t, dtype=float) if len(mode_ts) == 0: return np.array([default] * len(t), dtype=object) order = np.argsort(mode_ts) mts = np.asarray(mode_ts, dtype=float)[order] ms = np.asarray(modes, dtype=object)[order] k = np.searchsorted(mts, t, side="right") - 1 return np.where(k >= 0, ms[np.clip(k, 0, None)], default) def step_keep_mask(t, mode_ts, modes, windows): """学習に使うステップ: 状態が model / correct で, 除く窓のどれにも入らないもの.""" m = mode_at(t, mode_ts, modes) keep = np.isin(m, TRAIN_MODES) t = np.asarray(t, dtype=float) for t0, t1 in windows: keep &= ~((t >= t0) & (t <= t1)) return keep, m