フィジカルAI研究室を作る Log#32 | mechArm 270 を人が直す時間を測り、縮める先を決めた

この記事の素材(コード・ログ・データ 2ファイル・まとめてダウンロードあり)
AIで読む ChatGPT・Claude などで、この記事に質問できます

実物の小型ロボットアーム mechArm 270 で、Physical Intelligence の π0.5 が失敗した所を人がゲームパッドで直すのにかかる時間は、1回あたり中央値14.5秒だった。そのうち関節を実際に動かしているのは中央値2.6秒で、1回ごとに見ると約8割は、直しを始めてから最初の操作までと、最後の操作から直しを終えるまでの「出入り」の時間だった。 実機推論の記録に残っていた77回の直しを、あとから時間ごとに分けて測りました。

測ったことで、次のことが分かりました。

  • 直しの道具を替えた効果が数字で出た:キーボードで直していた45回は中央値43.2秒、ゲームパッドに替えた32回は中央値14.5秒で、約3分の1になっていた
  • 縮める先が2つに絞れた:1つは爪を開け閉めしてつかみ直す回で、4回しかないのに中央値57.7秒、長いと91.8秒かかる。もう1つは直しの出入りで、ゲームパッドの1回ごとに、最初の操作まで中央値5.9秒、終わるまで中央値4.9秒かかっている
  • 電源の比べ方が決まった:注文した12V・5A の電源アダプターが届いたら、同じ動きで今の12V・2A と並べて数字で比べる道具と手順書を、Claude Code が待ち時間の間に作り終えた。持ち上げで爪が緩むのが電源のせいかを、届いた日に数字で言える

この回は、mechArm 270 の電源アダプターが届くのを待つ間の作業です。前の回(LOG490)で、失敗から仕事に戻るまでの時間を「気づく・止める・戻す・再開」の4つに分けて測ると決めていました。現場でロボットを使い続けられるかは、失敗しないことより、失敗してから何秒で元の仕事に戻れるかで決まる場面が多いと考えているからです。新しい実機の試しができないので、すでにある記録から「戻す」の時間を取り出しました。

やったことなぜ結果
電源の前後比較の道具づくりを、依頼箱で Claude Code に頼んだ
電源が届いた日に、同じ動きで数字を並べるため
爪の位置と電圧を記録する引数・集計のスクリプト・手順書ができた。決めてほしいことが5つ残った
位置5 のお手本の入れ替えを、コマンド1つにまとめた
撮ったあとの作業を短くするため
今の6本は、閉じてから5秒の間に1本も持ち上がっていなかった(30mm 上がるまで中央値20.0秒)
実機推論の記録から、人が直した77回の時間を測った
失敗からの戻しのどこに時間がかかっているかを知るため
ゲームパッドで中央値14.5秒。動かしているのは2.6秒
日ごとに、実機の時間のうち直しが占める割合を出した
直しが試行全体をどれだけ止めているかを見るため
11/6 は33%、11/10 は26%、11/11 は8%

時間の数字は、実機の記録(log.jsonl)の時刻から計算した値です。縮めたあとにどれだけ短くなるかは、まだ試していない見込みです。

GPU を買うか借りるかの判断は、Runpod で実際に借りて学習させた費用をもとに、別のページにまとめています。

導入比較ガイドロボットAIの学習用GPU、買うか借りるか実際に借りて学習させた費用と、損益分岐の計算機で決められます

作業ログ ― 電源待ちの間に、mechArm 270 の直しの時間を測り、電源の比べ方を用意するまで

ここから下は作業の記録です。コードと出力は全量で載せます。効果を書いた所のうち、数字で書いたものは記録から計算した値、「見込み」と書いたものはまだ確かめていない値です。

この回で何度も出てくる言葉を先に言い換えておきます。

  • π0.5:Physical Intelligence が公開しているロボット用の AI のモデル。カメラの画像と関節の角度を受け取り、次の動きを出す。mechArm のお手本50本で学習させて使っている
  • 直し(補正):π0.5 が失敗しそうな所で人が割り込み、関節を手で動かして直すこと。記録では correction の行に残る。直したあとはまた π0.5 に戻す
  • 出入り:直しを始めてから最初のボタンを押すまでと、最後のボタンを押してから直しを終えるまでの時間。関節は動いていない
  • 依頼箱:Claude Code への依頼を置くと、Mac の見張り役が自動で Claude Code に渡し、報告を書き戻す仕組み。iCloud の 依頼箱/ フォルダで動いている
  • 位置5:ブロックを置く5か所の印のうちの1つ。お手本は位置ごとに10本ずつそろえている

電源アダプターが届くまでにできることを4つに分けた

mechArm 270 は、つかんだブロックを持ち上げるときに爪が緩むことがあります。前の回(LOG487)で、付いてきた電源アダプター(12V・2A)が足りないとみて、12V・5A を注文しました。届くまでは、新しい電源で実機を試せません。そこで、実機を使わずにできることを4つに分けました。

  1. 電源の前後比較の準備:届いたら同じ動きで2A と5A を並べられるように、記録の道具と手順を先に作る
  2. 位置5 のお手本の入れ替えの準備:撮ったあとの作業をコマンド1つにする
  3. 失敗からの戻しの時間を、これまでの記録から測る
  4. この回の記事

1 と 2 は依頼箱に出せば、その間に 3 か 4 を進めて

1 と 2 はコードを書く仕事なので、依頼箱で Claude Code に回し、その間に 3 を進めました。

依頼156:電源の前後比較の準備と、位置5 の入れ替えをコマンド1つにする

この記事のコード・ログ・データは、1ファイルずつ下に置いています。全文はリンク先にあります。

ファイルの一覧(2本)

01_sec03.txt ― 依頼156 の報告(claude_code_reports/20261112_156.txt)・4,623字

02_recovery_time_detail_20261112.txt ― recovery_time_detail_20261112.txt(recovery_time_detail.py の出力)・5,672字

依頼箱には、1件ずつ置く決まりがあります(前の依頼が止まっても次が走り出して崩れたことがあるため)。このとき依頼箱では、病院の搬送ロボットのシミュレーションの依頼155 が動いていました。156 は mechArm のコードで、155 とは作業フォルダが別なので、続けて置きました。

比べる動きは、毎回同じ動きになる「位置5 の教えた動きの再生」にしました。π0.5 の推論は毎回少しずつ違う動きを出すので、電源の差と動きの差が混ざってしまいます。

置いた依頼の全文です。

依頼156 の依頼文(20261112_156_電源の前後比較の準備と位置5の入れ替えをコマンド1つにする.md)

---
作業フォルダ: /Users/shimazueiichi/r2r2r-scripts
上限時間: 180
---
# 依頼156:電源(12V・2A → 12V・5A)の前後比較の準備と、位置5 の入れ替えをコマンド1つにする(Log30)

## 背景
- 持ち上げで爪が緩む件が、電源不足(今は 12V・2A のアダプター)のせいかを確かめたい。12V・5A のアダプターを注文済みで、届くのを待っている(2026-11-12)。
- 届いたあと「動きが良くなった気がする」で終わらせないため、2A のうちに同じ動きで数字を取り、5A で同じものを取って並べる。
- 位置5 の補正入り 6 本を、教えた動きの再生で撮る深いお手本に入れ替える準備は依頼152 で済んでいる(DEEP_DEMOS.md 5 節・pi05/excludes/deep_even10_v2.json)。撮ったあとの作業を短くしたい。

## この依頼でやらないこと
- 実機は動かさない。アーム・爪へ指令を送るコードは実行しない(テストはモックで)。
- 動きそのもの(姿勢・速さ・待ち時間・閉じる値の既定 20)は変えない。足すのは「読む・記録する・集計する」だけ。
- send_coords() は使わない。time.sleep() は削除・短縮しない。git add . と git clean は使わない(ファイルを1つずつ add)。

## A. 電源の前後比較
1. 比べる動きは、毎回同じになる「位置5 の教えた動きの再生」(record_teach_demo.py run、教えた記録 drag_v2_20221013_233323.json)にする。推論は毎回動きが変わるので使わない。
2. 再生の記録に、今すでに残っているもの(爪と肩の電流など。依頼116・121)を確かめ、足りないものだけを足す:
   - 爪の位置(get_gripper_value など、読める値)を、閉じてから持ち上げ終わるまで
   - サーボの電圧(servo_check_pi.py が読んでいる番地 62。0.1V。この機体で未確認なので生の値のまま)を、動かす前の静止・閉じた直後・持ち上げ中で、読める範囲で
   - 読む頻度を上げすぎて動きの間隔が変わらないこと(今の再生の指令の間隔が変わらないことをテストで確かめる)。読めなければ止めずに「読めない」と記録する
   - 電源の種類を記録に残す引数(例 --power 2A / 5A)。試行 JSON に残る
3. 集計のスクリプト(例 pi05/power_ab_compare.py。読むだけ):
   - 2A と 5A の試行を並べ、試行ごとに次を1行で出す:閉じて1〜2秒の爪の電流の中央値、持ち上げ中の爪の電流の最小と中央値、持ち上げ中の爪の位置の変化(閉じた直後との差)、電圧の静止時と持ち上げ中の最小、つかみの深さ(grasp_depth.py の値。取れれば)
   - 2A・5A それぞれの中央値と、差を出す。CSV と summary.md
   - 合否の線は決め打ちしない。「緩んだ」の目安を仮で1つ置くなら、仮と明記し、2A の結果を見てエイイチと決める形にする
4. 出力の置き場所は MASTER の保存先テーブルに従う:実機の単体実験 → KIOXIA の r2r2r-data/runs/experiments/{script_name}/。集計は同じ階層に。KIOXIA がマウントされていなければ止まる(ensure_ssd_mounted)。出し先に既にあれば止まる。
5. 手順書 pi05/POWER_AB.md:
   - 今(2A)で 3 回、届いたら(5A)で 3 回。どちらも同じ閉じる値(既定 20。依頼152 の --grip-value を使う場合はその値を両方で同じに)・同じブロック・同じ位置
   - 各回のあいだに置く時間(サーボの温度をそろえるため)を書く
   - 実行コマンド、集計コマンド、見るべき行
   - 電源を差し替えるときは Pi ごと落としてから、の一文

## B. 位置5 の入れ替えをコマンド1つにする
1. DEEP_DEMOS.md 5-2 の手順(新しい 6 本のデータセット → deep_even10_v1 から 6 本を外して合わせる → count_by_position.py で位置ごとに 10 本を確かめる)を、1つのコマンドにまとめる(例 pi05/build_deep_even10_v2.sh)。途中で 1 つでも失敗したら止まる。元のデータセットは変えない。出し先に既にあれば止まる。
2. 今の位置5 の補正入り 6 本(175527・180435・184016・190347・183926・184650)について、つかみの深さ(grasp_depth.py)と持ち上げの高さを1本1行で出し、ファイルに残す(新しい 6 本と後で比べるため)。
   - 持ち上げの高さは、記録にある値から決める(例:持ち上げ区間の j1・j2 から FK で出す手先の高さの最大、など)。何で測ったかを報告と出力の頭に書く
   - 比べるスクリプトは、新しい 6 本を撮ったあと同じコマンドで並べて出せる形にする
3. DEEP_DEMOS.md 5 節に、このコマンドと比べ方を足す。

## 確かめ
- 足したテストと既存テストをすべて流す(pi05/、tools/、test_lab_start.sh)。
- codex にレビューを頼み、必須の指摘は直す。
- 実機で試す手順(A は 2A で今日やる分)を報告に書く。

## 報告
- やったこと/結果/変えたファイル/実機で試す手順/決めてほしいこと
- 最後の1行に「判定:合格/止めた(決めてほしいことあり)/失敗」

依頼156 の報告(claude_code_reports/20261112_156.txt)

Claude Code は63分で終え、テストと codex のレビュー2回を通しました。実機は動かしていません。判定は「止めた(決めてほしいことあり)」で、届いた電源で試す前に決めることが5つ残りました。

01_sec03.txt4,623字・66行・text
依頼156 報告:電源(12V・2A → 5A)の前後比較の準備と、位置5 の入れ替えをコマンド1つにする(Log30)

■ 1. やったこと
A. 電源の前後比較
- record_teach_demo.py run に --power 2A / 5A を足した。試行 JSON の power に残る。付けたときだけ、爪の位置(サーボ7 の番地 56/57)と電圧(番地 62。肩2・爪7)を生の値で recording/power_regs.csv に読む。数は power_regs_meta.json と試行 JSON の currents.power_regs に残す。
  - 今あるもの(爪と肩の電流 currents.csv。依頼116・121)は変えていない。currents.csv の列、currents_meta.json、実機推論の記録(PolicyRecorder)も変えていない。
  - 足した番地は電流と同じ時だけ読む(本体が daemon を使わずに待っている間だけ。爪の命令から待ちが終わるまでは読まない)。1 回に足すのは 2 つまで(肩の上位を読む回には足さない。爪の位置と電圧を交互に読む)。足す回だけ、読み終わる見込みを 0.15 秒から 0.19 秒にとる。
  - 番地が 2 回続けて返事なしなら、その run ではもう頼まない(本体を待たせるのは多くて 2 回)。止めずに、空の値と no_reply を残す。
別のタブで開く保存

今の位置5 の6本は、閉じてから5秒の間に1本も持ち上がっていなかった

報告の中でいちばん大きかったのは、入れ替える予定の位置5 の6本(ゲームパッドの直しが入ったお手本)の持ち上げ方です。

比べるもの閉じてから30mm 上がるまで
今の補正入りの6本
8.3〜32.7秒(中央値20.0秒)。閉じてから5秒の間に持ち上がった本は0本
前の教えた動きの再生(194913)
3.2秒

ゲームパッドで直したお手本は、人が爪を閉じたあと、どの関節をどれだけ上げるかを考えながらボタンを押すので、持ち上げまでの間が長くなったとみています。π0.5 はお手本の間もまねるので、このままだと、閉じたあと持ち上げずに待つ動きを覚える見込みです。

  • ねらい:位置5 のお手本の持ち上げ方を、ほかの位置と同じ「閉じたらすぐ上げる」にそろえる
  • 変わったこと:入れ替える前の6本の持ち上げを数字で残した(30mm 上がるまで中央値20.0秒)。新しい6本を撮ったら、同じコマンドで並べて出せる
  • 見込める効果:入れ替えたあとに学習し直すと、位置5 でつかんだあとの止まりが短くなる見込み。新しい6本の値と、学習し直したあとの実機推論で確かめる

届いた電源で試す前に決めること(5つ)

報告の「決めてほしいこと」を、そのまま並べます。

  1. 爪の位置(番地56/57)と電圧(番地62)は、この mechArm で読めるかをまだ確かめていない。1回目で読めない・電圧が120前後(12V)でないときに、その列を使うか
  2. 閉じて1〜2秒の電流は、再生では読まない決まりの時間に入る。代わりに「待ちのあと最初の1秒」でよいか。持ち上げ中を「放すまで(運ぶ間を含む)」とする決め方でよいか
  3. 各回のあいだを5分(サーボの温度をそろえるため)でよいか
  4. 「緩んだ」の目安(仮に、持ち上げ中の電流の最小が閉じた直後の中央値の0.8倍を下回ったら)を、2A の結果を見て決める
  5. 2A・5A で撮った試行を、位置5 の新しい6本と兼ねるか、分けるか

失敗からの戻しの時間を、実機推論の記録77回から測った

記録のどこから時間を取ったか

π0.5 の実機推論の記録は、KIOXIA の runs/pi05_policy/ に、試行ごとに log.jsonl として残っています。直しの時間は、次の2種類の行から取りました。

  • correction で action が accept の行:直しを始めた時刻(t_start)と終えた時刻(t_end)。キーボードの直しにもゲームパッドの直しにもある
  • pad の行:ゲームパッドのボタンを押した時刻(t_unix)と押したものの種類(関節を動かす・爪を開ける・閉じるなど)。ゲームパッドの直しだけにある

ゲームパッドの直しは、t_start から最初の pad の行までを「最初の操作まで」、最後の pad の行から t_end までを「終わるまで」、その間を「動かしている間」としました。キーボードの直しはボタンの記録がないので、全体の長さだけです。

元の記録は読むだけで、書き換えていません。スクリプトと出力は KIOXIA の lab/Log31/analysis/ に置きました(分析を始めたときに Log31 のフォルダを使ったため)。

recovery_time.py(直しの長さを、キーボードとゲームパッドに分けて集計する)

"""π0.5 の実機推論で人が直した(補正した)回の長さを、キーボードとゲームパッドに分けて集計する(読むだけ)。
使い方: python3 recovery_time.py /Volumes/KIOXIA/r2r2r-data/runs/pi05_policy"""
import json,glob,statistics as st,collections,os,sys
base=sys.argv[1]
rows=[]
for f in sorted(glob.glob(base+'/pi05_*/log.jsonl')):
    run=f.split('/')[-2]
    L=[]
    for l in open(f):
        try:L.append(json.loads(l))
        except:pass
    meta=[d for d in L if d.get('script')]
    meta=meta[-1] if meta else {}
    chunks=[d for d in L if 'chunk' in d and d.get('type') is None]
    pads=collections.defaultdict(list)
    for d in L:
        if d.get('type')=='pad': pads[d.get('correction_id')].append(d)
    for d in L:
        if d.get('type')=='correction' and d.get('action')=='accept' and d.get('t_start'):
            cid=d['correction_id']; dev=d.get('device') or 'keyboard'
            P=[p for p in pads.get(cid,[]) if p.get('t_unix')]
            # chunk before correction: end time
            prev=[c for c in chunks if c.get('chunk')==d.get('after_chunk')]
            r=dict(run=run,cid=cid,dev=dev,dur=d['t_end']-d['t_start'],grip=d.get('grip_used'),regrip=d.get('regrip_used'),
                   outcome=meta.get('outcome'),rewinds=meta.get('rewinds'),date=run[5:13])
            if P:
                ts=[p['t_unix'] for p in P]
                r['to_first']=ts[0]-d['t_start']; r['after_last']=d['t_end']-ts[-1]
                kinds=collections.Counter(p.get('kind') for p in P)
                r['n_move']=kinds.get('move',0); r['n_grip']=kinds.get('grip_open',0)+kinds.get('grip_close',0)
                # gaps before each event by kind
                g=collections.defaultdict(float)
                for a,b in zip(P,P[1:]): g[b.get('kind')]+=b['t_unix']-a['t_unix']
                r['gap_before_grip']=g['grip_open']+g['grip_close']
                r['gap_before_move']=g['move']+g['modifier']
                r['gap_halt']=g['halt']
            rows.append(r)
json.dump(rows,open(os.path.join(os.path.dirname(os.path.abspath(__file__)),'recovery_time_rows.json'),'w'),ensure_ascii=False)  # 1回ずつの値
def s(x): 
    x=[v for v in x if v is not None]
    return f"n={len(x)} 中央{st.median(x):.1f} 平均{st.mean(x):.1f} 最大{max(x):.1f} 合計{sum(x):.0f}" if x else '-'
for dev in ('keyboard','gamepad'):
    R=[r for r in rows if r['dev']==dev]
    print('==',dev,len(R),'回 日付',sorted(set(r['date'] for r in R)))
    print(' 1回の補正の長さ(秒)',s([r['dur'] for r in R]))
    print('  爪を使った',s([r['dur'] for r in R if r['grip']]))
    print('  爪を使わない',s([r['dur'] for r in R if not r['grip']]))
    if dev=='gamepad':
        for k in ('to_first','after_last','gap_before_grip','gap_before_move','gap_halt','n_move','n_grip'):
            print(' ',k,s([r.get(k) for r in R]))

recovery_time_20261112.txt(recovery_time.py の出力)

== keyboard 45 回 日付 ['20261030', '20261031', '20261101']
 1回の補正の長さ(秒) n=45 中央43.2 平均48.6 最大116.0 合計2188
  爪を使った -
  爪を使わない n=45 中央43.2 平均48.6 最大116.0 合計2188
== gamepad 32 回 日付 ['20261106', '20261110', '20261111']
 1回の補正の長さ(秒) n=32 中央14.5 平均24.6 最大91.8 合計786
  爪を使った n=4 中央57.6 平均57.9 最大91.8 合計232
  爪を使わない n=28 中央13.8 平均19.8 最大88.5 合計554
  to_first n=32 中央5.9 平均6.5 最大12.0 合計208
  after_last n=32 中央4.9 平均7.0 最大72.2 合計223
  gap_before_grip n=32 中央0.0 平均0.9 最大19.3 合計29
  gap_before_move n=32 中央2.5 平均8.2 最大59.0 合計262
  gap_halt n=32 中央0.2 平均0.5 最大6.4 合計16
  n_move n=32 中央5.5 平均8.8 最大47.0 合計280
  n_grip n=32 中央0.0 平均0.3 最大6.0 合計9

to_first が最初の操作まで、after_last が終わるまで、gap_before_move などは、ボタンとボタンの間の時間を次に押したボタンの種類ごとに足したもの、n_move と n_grip は関節を動かした回数と爪を開け閉めした回数です。

recovery_time_detail.py(ゲームパッドの直しを1回ずつ見る)

"""ゲームパッドで直した回ごとに、直しを始める直前の記録からの待ち・最初の操作まで・最初と最後の操作の種類・終わり方を出す(読むだけ)。
使い方: python3 recovery_time_detail.py /Volumes/KIOXIA/r2r2r-data/runs/pi05_policy"""
import json,glob,statistics as st,collections,sys
base=sys.argv[1]
out=[]
for f in sorted(glob.glob(base+'/pi05_*/log.jsonl')):
    L=[]
    for l in open(f):
        try:L.append(json.loads(l))
        except:pass
    chunks={d['chunk']:d for d in L if d.get('type') is None and 'chunk' in d and 't' in d}
    for d in L:
        if d.get('type')=='correction' and d.get('action')=='accept' and d.get('device')=='gamepad':
            c=chunks.get(d['after_chunk'])
            # last event before correction start (any with t numeric)
            prev=[x.get('t') for x in L if isinstance(x.get('t'),(int,float)) and x.get('t')<d['t_start']]
            pads=[p for p in L if p.get('type')=='pad' and p.get('correction_id')==d['correction_id']]
            out.append(dict(run=f.split('/')[-2],dur=d['duration_s'],wait_before=d['t_start']-max(prev) if prev else None,
               first=pads[0].get('t_unix')-d['t_start'] if pads else None,
               first_kind=pads[0].get('kind') if pads else None,end=d.get('pad_end_reason'),
               last_kind=pads[-1].get('kind') if pads else None))
for o in out: print(o)
w=[o['wait_before'] for o in out if o['wait_before'] is not None]
print('補正の前の待ち 中央',st.median(w),'合計',sum(w))

recovery_time_detail_20261112.txt(recovery_time_detail.py の出力)

{'run': 'pi05_20261106_183926', 'dur': 17.86, 'wait_before': 5.63610315322876, 'first': 7.3181610107421875, 'first_kind': 'move', 'end': 'finish_button', 'last_kind': 'move'}
{'run': 'pi05_20261106_183926', 'dur': 33.38, 'wait_before': 6.62411904335022, 'first': 9.844610929489136, 'first_kind': 'move', 'end': 'finish_button', 'last_kind': 'halt'}
{'run': 'pi05_20261106_184312', 'dur': 13.16, 'wait_before': 3.9122698307037354, 'first': 4.961295127868652, 'first_kind': 'move', 'end': 'finish_button', 'last_kind': 'move'}
{'run': 'pi05_20261106_184312', 'dur': 12.68, 'wait_before': 4.878342151641846, 'first': 7.697403907775879, 'first_kind': 'move', 'end': 'finish_button', 'last_kind': 'halt'}
{'run': 'pi05_20261106_184452', 'dur': 20.05, 'wait_before': 2.2865936756134033, 'first': 4.762716054916382, 'first_kind': 'move', 'end': 'finish_button', 'last_kind': 'move'}
{'run': 'pi05_20261106_184452', 'dur': 11.12, 'wait_before': 1.7676568031311035, 'first': 6.31832218170166, 'first_kind': 'move', 'end': 'finish_button', 'last_kind': 'halt'}
{'run': 'pi05_20261106_184650', 'dur': 9.79, 'wait_before': 4.60254693031311, 'first': 4.830996990203857, 'first_kind': 'move', 'end': 'finish_button', 'last_kind': 'move'}
{'run': 'pi05_20261106_184650', 'dur': 26.56, 'wait_before': 2.0469629764556885, 'first': 12.008638143539429, 'first_kind': 'move', 'end': 'finish_button', 'last_kind': 'halt'}
別のタブで開く保存

wait_before は、直しを始める直前に記録された出来事(チャンクの実行の終わりなど)から、直しを始めるまでの秒数です。最後の行の中央値2.8秒が、人が失敗に気づいてゲームパッドの直しを始めるまでの目安になります。直前の出来事がチャンクの実行の終わりとは限らないので、目安として扱います。

end は直しの終わり方で、finish_button はゲームパッドの終わりのボタン、ctrl_c はキーボードの Ctrl-C で抜けた回です。

recovery_share_by_day.py(日ごとに、実機の時間のうち直しが占める割合を出す)

"""π0.5 の実機推論で、日ごとに 試行の数・巻き戻しの数・全体の秒数・ゲームパッドで直した秒数とその割合・直した試行の数 を出す(読むだけ)。
全体の秒数は、log.jsonl に記録された時刻の最初から最後まで(試行ごと)を足したもの。
使い方: python3 recovery_share_by_day.py /Volumes/KIOXIA/r2r2r-data/runs/pi05_policy"""
import json, glob, collections, sys, os
base = sys.argv[1]
agg = collections.defaultdict(lambda: [0, 0, 0.0, 0.0, 0])
for f in sorted(glob.glob(os.path.join(base, 'pi05_2026110[6-9]*/log.jsonl')) + glob.glob(os.path.join(base, 'pi05_202611[1][01]*/log.jsonl'))):
    L = []
    for l in open(f):
        try: L.append(json.loads(l))
        except ValueError: pass
    ts = [x['t'] for x in L if isinstance(x.get('t'), (int, float))] + [x['t_end'] for x in L if x.get('t_end')]
    if len(ts) < 2: continue
    m = [x for x in L if x.get('script')]; m = m[-1] if m else {}
    corr = sum(x['duration_s'] for x in L if x.get('type') == 'correction' and x.get('device') == 'gamepad')
    day = os.path.basename(os.path.dirname(f))[5:13]; a = agg[day]
    a[0] += 1; a[1] += m.get('rewinds') or 0; a[2] += max(ts) - min(ts); a[3] += corr; a[4] += 1 if corr > 0 else 0
for d, a in sorted(agg.items()):
    print(d, '試行', a[0], '巻き戻し', a[1], '全体', round(a[2]), '秒 うち補正', round(a[3]), '秒', f'({a[3]/a[2]*100:.0f}%)', '補正した試行', a[4])

recovery_share_by_day_20261112.txt(recovery_share_by_day.py の出力)

20261106 試行 9 巻き戻し 0 全体 845 秒 うち補正 281 秒 (33%) 補正した試行 9
20261110 試行 13 巻き戻し 0 全体 1334 秒 うち補正 344 秒 (26%) 補正した試行 6
20261111 試行 20 巻き戻し 0 全体 1954 秒 うち補正 161 秒 (8%) 補正した試行 7

11/6 は9本すべてで直し、実機の時間の33%が直しでした。11/11 は20本のうち7本で直し、8% でした。11/6 は前のモデル(corr2)、11/10 と 11/11 は位置1〜5 のお手本を各10本にそろえて学習し直したモデル(deep1)でした。モデルを替えたあと、直す回そのものが減っています。巻き戻し(失敗した所まで動きを戻す仕組み)は、3日とも0回でした。

1回の直しを3つに分けると、関節を動かしているのは中央値2.6秒

出力の値から、ゲームパッドの直し32回を、もう一度まとめ直しました(1回ごとの値は recovery_time_rows.json)。

ゲームパッドの直し回数1回の長さ(中央値)動かしている間(中央値)
出入りの割合(1回ごとの中央値)
全部3214.5秒2.6秒79%
関節だけ動かした回2813.8秒2.5秒81%
爪を開け閉めした回(つかみ直し)
457.7秒40.2秒31%

関節だけを動かす直しは、ボタンを押している時間は短く、ほとんどが出入りです。つかみ直しは逆で、爪を開け、位置を合わせ、閉じ、持ち上げるまでを人が考えながら進めるので、動かしている間そのものが長くなります。

  • ねらい:失敗からの戻しのうち、どこを縮めると効くのかを先に決める
  • 変わったこと:キーボードの直しは中央値43.2秒、ゲームパッドの直しは中央値14.5秒と、道具を替えた差が測れた。ゲームパッドの直しのうち、関節だけの回は出入りが約8割、つかみ直しの回は動かしている間が長い、と分かれた
  • 見込める効果:出入りを縮めると、関節だけの直し28回の1回あたり最大で約10秒が浮く見込み。つかみ直しを減らすと、1回あたり約1分が浮く見込み。どちらも、縮めたあとの直しを同じスクリプトで測って確かめる

LOG490 の4つの区間に当てはめた

区間どこからどこまで
今の mechArm で測れたこと
気づく
失敗が起きてから、人が気づいて直しを始めるまで
直前の記録から直しを始めるまで中央値2.8秒(目安)
止める
気づいてから、アームが止まるまで
今の記録では「戻す」と分けられない
戻すやり直せる姿勢に戻すまで
ゲームパッドで中央値14.5秒(つかみ直しは57.7秒)、キーボードは43.2秒
再開
戻してから、自動の動きに戻るまで
直しを終えると次のチャンクの推論に戻る。人の合図待ちは LOG490 の「人の合図待ち」に入っている

「気づく」は中央値2.8秒で、人の目で見ていても遅くはありません。いちばん長いのは「戻す」です。

縮める先は、つかみ直しと直しの出入りの2つ

つかみ直しは、電源の比較とつながっている

つかみ直しが要るのは、つかんだブロックが持ち上げで抜けた・ずれたときです。その原因が電源の不足なら、5A のアダプターで抜けること自体が減り、つかみ直しの1分がまるごと要らなくなります。依頼156 で作った比べ方は、このつかみ直しの元を確かめる道具でもあります。

直しの出入りは、まず記録を足してから縮める

11/10 から、ゲームパッドを読む仕組みを、直しのたびに別のプロセスとして立ち上げる形にしています(依頼145。同じプロセスで動かすと固まることがあったため)。直しを始めてから最初の操作までの5.9秒には、このプロセスが立ち上がる時間と、人がどの関節から動かすかを考える時間の両方が入っています。今の記録には「立ち上がり終えた時刻」がないので、2つを分けられません。

11/6(同じプロセスで動かしていた日)の19回も、最初の操作までは4.8〜12.0秒でした。立ち上がりだけが原因ではなさそうです。そこで、次の順にします。

  1. ゲームパッドのプロセスが立ち上がり終えた時刻を、log.jsonl に足す
  2. 立ち上がりの分が大きければ、推論の始めに一度だけ立ち上げておく形にする
  3. 人が考えている分が大きければ、直しを始めた時点で「どの関節がどれだけずれているか」を画面に出して、最初の操作までを短くする

終わるまでの4.9秒は、最後のボタンから終わりのボタンを押すまでの時間で、人が「もう直った」と確かめている時間だとみています。11/10 の1回は72.2秒あり、この回に何をしていたかは記録からは分かりません。横カメラの録画を試行ごとに残せるようになれば、こうした回も見返せます。

12V・5A のアダプターが届いたら

  1. 依頼156 の「決めてほしいこと」5つを決める
  2. pi05/POWER_AB.md の手順で、今の2A で位置5 の再生を3回撮る
  3. 届いたら、5A で同じく3回撮り、pi05/power_ab_compare.py で並べる
  4. 位置5 の新しい6本を撮り、pi05/build_deep_even10_v2.sh で入れ替える
  5. ゲームパッドのプロセスの立ち上がりの時刻を記録に足す依頼を出し、直しの時間を同じスクリプトでもう一度測る

出典

  • LOG490(mechArm 270 の待ちを測り、Claude Fable 5.2 の使い所を決めた)
  • LOG487(爪の電流と、電源アダプターの不足)
  • Physical Intelligence(π0.5)