#!/usr/bin/env python3 """grip_drop_analysis.py — 爪を閉じている間に、爪の電流(gripper_current)が急に 0 付近へ落ちる所を探す(Log30・LOG487)。 入力: runs/pi05_policy/pi05_20261111_*/recording/states.csv・currents.csv・log.jsonl(読むだけ) 見ること(試行ごと) - 閉じた指令(gripper_cmd の gripper_state=1)の時刻。1 回目を 0 秒にする - 閉じて 0.8〜1.5 秒の電流の中央値 - 「落ちた」所: 1 つ前の行が 400 を超え, 次の行が 150 を下回った所(爪が閉じている間だけ) そのとき, 直前の send_angles(推論が送った目標)から何秒か, 前 1 秒の肩 j1・ひじ j2 の動き(大きい方) - 締め直し r(log.jsonl の regrip_tighten)の時刻と送った値 - 爪を閉じてから開くまで, 肩 j1 がいちばん上がった所(閉じた所からの差。マイナス=持ち上げ) 出力: 表を標準出力に, 同じ中身を grip_drop_summary.csv に, 図とサムネイル用の時系列を grip_drop_series.json に(このフォルダ) """ import csv import json import sys from pathlib import Path import numpy as np R = Path('/Volumes/KIOXIA/r2r2r-data/runs/pi05_policy') if not R.exists(): R = Path.home() / 'mnt/KIOXIA/r2r2r-data/runs/pi05_policy' OUT = Path(__file__).resolve().parent SERIES_RUNS = ['pi05_20261111_100659', 'pi05_20261111_101557', 'pi05_20261111_102043', 'pi05_20261111_160851', 'pi05_20261111_161402', 'pi05_20261111_161743'] def load(d): rows = list(csv.DictReader(open(d / 'recording/states.csv'))) cur = [(float(r['ts']), float(r['gripper_current'])) for r in csv.DictReader(open(d / 'recording/currents.csv')) if r['gripper_current'] not in ('', None)] log = [json.loads(l) for l in open(d / 'log.jsonl')] return rows, cur, log def analyze(d): rows, cur, log = load(d) gv = next((j.get('grip_value') for j in log if j.get('type') == 'model'), None) gc = [(float(r['ts']), r['gripper_state']) for r in rows if r['event_type'] == 'gripper_cmd'] closes = [t for t, s in gc if s == '1'] if not closes: return None t0 = closes[0] sends = [float(r['ts']) for r in rows if r['event_type'] == 'send_angles' and r['stage'] != 'home'] act = [(float(r['ts']), float(r['j1']), float(r['j2'])) for r in rows if r['event_type'] == 'actual' and r['j1'] and r['stage'] != 'home'] c1 = [v for t, v in cur if t0 + 0.8 < t < t0 + 1.5] drops = [] for (ta, va), (tb, vb) in zip(cur, cur[1:]): if va > 400 and vb < 150: g = [s for t, s in gc if t < tb] if not g or g[-1] != '1': continue last = max([s for s in sends if s < tb], default=None) a = [x for x in act if tb - 1.0 < x[0] < tb + 0.3] mv = max(abs(a[-1][1] - a[0][1]), abs(a[-1][2] - a[0][2])) if len(a) > 1 else 0.0 drops.append({'t': round(tb - t0, 1), 'since_send': None if last is None else round(tb - last, 1), 'move_1s': round(mv, 1)}) regrips = [{'t': round(j['t'] - t0, 1), 'value': j.get('grip_value_sent')} for j in log if j.get('action') == 'regrip_tighten' and j.get('sent')] opens = [t for t, s in gc if s == '0' and t > t0] t_open = opens[0] if opens else (act[-1][0] if act else t0) seg = [x for x in act if t0 <= x[0] <= t_open] j1_close = seg[0][1] if seg else None lift = round(min(x[1] for x in seg) - j1_close, 1) if seg else None return {'run': d.name, 'grip_value': gv if gv is not None else 20, 'closes': [round(t - t0, 1) for t in closes], 'cur_1s': round(float(np.median(c1)), 0) if c1 else None, 'drops': drops, 'regrips': regrips, 'opened': bool(opens), 'j1_lift': lift} def series(d): rows, cur, log = load(d) t0 = next(float(r['ts']) for r in rows if r['event_type'] == 'gripper_cmd' and r['gripper_state'] == '1') act = [(float(r['ts']) - t0, float(r['j1'])) for r in rows if r['event_type'] == 'actual' and r['j1'] and r['stage'] != 'home'] gc = [(float(r['ts']) - t0, int(r['gripper_state'])) for r in rows if r['event_type'] == 'gripper_cmd'] sends = [round(float(r['ts']) - t0, 2) for r in rows if r['event_type'] == 'send_angles' and r['stage'] != 'home'] end = max(t for t, _ in act) cs = [(round(t - t0, 2), v) for t, v in cur if -2 <= t - t0 <= end] step = 0.25 grid = np.arange(-2, end, step) def resample(pts): out, i = [], 0 for g in grid: while i + 1 < len(pts) and pts[i + 1][0] <= g: i += 1 out.append(round(pts[i][1], 1)) return out return {'run': d.name, 't0': -2, 'step': step, 'current': resample(cs), 'j1': resample(act), 'grip': gc, 'sends': sends} def main(): res = [] print('run, 閉じる値, 閉じて1秒の電流, 閉じた時刻(秒), 落ちた所(秒・直前の送りから・前1秒の動き), 締め直し r(秒・値), 開いた, 肩j1の持ち上げ(°)') for d in sorted(R.glob('pi05_20261111_*')): a = analyze(d) if a is None: continue res.append(a) dr = ' / '.join(f"{x['t']}s(送り{x['since_send']}s・{x['move_1s']}°)" for x in a['drops']) or '-' rg = ' / '.join(f"{x['t']}s({x['value']})" for x in a['regrips']) or '-' print(f"{a['run'][-6:]}, {a['grip_value']}, {a['cur_1s']}, {a['closes']}, {dr}, {rg}, {a['opened']}, {a['j1_lift']}") with open(OUT / 'grip_drop_summary.csv', 'w', newline='') as f: w = csv.writer(f) w.writerow(['run', 'grip_value', 'cur_1s', 'closes', 'drops', 'regrips', 'opened', 'j1_lift']) for a in res: w.writerow([a['run'], a['grip_value'], a['cur_1s'], json.dumps(a['closes']), json.dumps(a['drops'], ensure_ascii=False), json.dumps(a['regrips']), a['opened'], a['j1_lift']]) ser = {r: series(R / r) for r in SERIES_RUNS} json.dump({'summary': res, 'series': ser}, open(OUT / 'grip_drop_series.json', 'w'), ensure_ascii=False) print('書き出し', OUT / 'grip_drop_summary.csv', OUT / 'grip_drop_series.json') if __name__ == '__main__': main()