LOG256R2R2R2026/6/26

mechArm 270 Pi 実機データを土台にデジタルツインを作る Log#07 | 実機の前に、データフローをコードで確定する

予定では、このセッションで実機を動かすはずだった。

センサを使わない dry-run で計画ダンプを確認し、本番の PICK→PLACE を一度通す。そのあと、台帳に残った実測角と pose_id を使って、ツインの予測と実機の結果を突き合わせる。そういう順番で進めるつもりだった。

だが、実機の前で止めた。

比較テーブルは、どのファイルを実機が読み、どのファイルに書くかを正しく知っていて初めて意味を持つ。そこが推測なら、比較は別物どうしを突き合わせることになる。だから先に、mechArm が読み書きするデータの地図を描いた。

live・twin・real と pose_id

mechArm のデータは役割で三つに分かれる。live は入力で、アームが読む採用姿勢の JSON。twin は変換器で、関節角から爪先位置を予測する FK テーブル。これはデジタルツイン側の予測器で、アームは動作時には読まない。real は出力で、試行の結果が書かれる台帳。

この三者を同じ pose_id で結ぶと、「twin が予測した爪先位置」と「real で実機が出した結果」を同じ姿勢として突き合わせられる。pose_id はそのための鍵だ。比較テーブルは、この三系統の関係が固まっていることを前提にしている。だから地図を先に描く。

ここで言う地図は、概念図ではなく、read / write の実体パスとコード行の対応表である。このセッションで確定した対応は次のとおり。本文の残りは、この一行ずつをどうコードに当てて確かめたかの記録になる。

系統

役割

実体

実機との関係

live

採用姿勢

dynamic_tracking_limits/logs/j0_sweep/j0_sweep_adopted/*.json

実機が読む

twin

FK予測器

fk_scan_motion_based_v2.csv

runtime では読まない

real

試行台帳

/Volumes/KIOXIA/r2r2r-data/runs/pick_place_ledger_v3.csv

実機が書く

bridge

照合キー

pose_id

live / twin / real を後から結ぶ

code

実行本体

dynamic_tracking_limits/place/place_modules/

read / write の根拠

パスを推測で焼かない

地図を描くときに最初に決めたのは、パスを推測で書かないことだった。設計図に省略形のディレクトリを焼くと、それ自体が後で事故の種になる。~/r2r2r-scripts/ のように略すと、実際にはその下に何階層もネストしている場合に取り違える。

チャット側からは Mac のファイルシステムを直接読めない。だから実体のパスは Mac 上で調べるのが正しい分担になる。調査用のコマンドは Claude が用意し、Mac で実行したのは私だ。すべて参照系(find / grep / ls / md5)に限り、書き込み・移動・削除はしない。そして「誰がどのファイルを読み書きするか」はドキュメントの記述ではなくコードを正とする。構造的な結論を実測の前に置かない、という原則は今回もそのまま効いた。

最初の調査では、コード本体を外していた

最初に live / twin / real / code とコード根拠をまとめて探した。次がそのコマンドと出力の全量。後半の [code] と [根拠] が空で返っている点が、この回の入口になった。

echo "===== HOME ====="; echo "$HOME"

echo; echo "===== [scripts] r2r2r-scripts 実体 ====="
ls -ld "$HOME/r2r2r-scripts" 2>/dev/null && (cd "$HOME/r2r2r-scripts" && pwd -P)

echo; echo "===== [live] 採用JSON ディレクトリと中身 ====="
find "$HOME/r2r2r-scripts" -type d -name 'j0_sweep*' 2>/dev/null
echo "-- adopted を含む .json の所在(ディレクトリ単位)--"
find "$HOME/r2r2r-scripts" -type f -name '*.json' -path '*adopted*' 2>/dev/null \
  | sed 's#/[^/]*$##' | sort | uniq -c
echo "-- sweep を含む .json の所在(adopted 以外も含め俯瞰)--"
find "$HOME/r2r2r-scripts" -type f -name '*.json' -path '*sweep*' 2>/dev/null \
  | sed 's#/[^/]*$##' | sort | uniq -c

echo; echo "===== [twin] FKテーブル(Mac側)====="
find "$HOME/r2r2r-scripts" -type f -name 'fk_scan_motion_based*.csv' 2>/dev/null

echo; echo "===== [twin] FKテーブル(Unity側 Assets/Data)====="
ls -ld "$HOME/Robotics_UX" 2>/dev/null
find "$HOME/Robotics_UX" -type f -name 'fk_scan_motion_based*.csv' 2>/dev/null

echo; echo "===== [real] ledger(Mac側)====="
find "$HOME/r2r2r-scripts" -type f -name 'pick_place_ledger*.csv' 2>/dev/null

echo; echo "===== [real] ledger(KIOXIA側)+ runs/ 構成 ====="
ls -ld /Volumes/KIOXIA/r2r2r-data 2>/dev/null || echo "KIOXIA 未マウント"
find /Volumes/KIOXIA/r2r2r-data -maxdepth 3 -type f -name 'pick_place_ledger*.csv' 2>/dev/null
ls -la /Volumes/KIOXIA/r2r2r-data/runs/ 2>/dev/null | head -40

echo; echo "===== [code] スクリプト本体の実体 ====="
find "$HOME/r2r2r-scripts" -maxdepth 3 -type f \
  \( -name 'run_pick_place.py' -o -name 'j0_lookup.py' -o -name 'j0_sweep_scan.py' \) 2>/dev/null

echo; echo "===== [根拠] run_pick_place.py が読む JSON / 書く ledger をコードから確認 ====="
grep -nEi 'adopted|j0_sweep|\.json|ledger|\.csv|glob|open\(|Path\(|os\.path|dir' \
  "$HOME/r2r2r-scripts/run_pick_place.py" 2>/dev/null | head -50

echo; echo "===== [根拠] j0_lookup.py が読む JSON をコードから確認 ====="
grep -nEi 'adopted|j0_sweep|\.json|glob|open\(|Path\(|os\.path|dir' \
  "$HOME/r2r2r-scripts/j0_lookup.py" 2>/dev/null | head -50

echo; echo "===== [根拠] j0_sweep_scan.py の出力先(採用JSON 生成先)====="
grep -nEi 'adopted|j0_sweep|\.json|out|save|write|open\(|Path\(|dir' \
  "$HOME/r2r2r-scripts/j0_sweep_scan.py" 2>/dev/null | head -50

出力の全量。[live] の採用 JSON が深くネストしていること、[real] の ledger が Mac 側に無く KIOXIA にあること、そして [code] と [根拠] が空で返った事実が並ぶ。

===== HOME =====
/Users/shimazueiichi

===== [scripts] r2r2r-scripts 実体 =====
drwxr-xr-x  233 shimazueiichi  staff  7456  6月 26 09:27 /Users/shimazueiichi/r2r2r-scripts
/Users/shimazueiichi/r2r2r-scripts

===== [live] 採用JSON ディレクトリと中身 =====
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_pick_02_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_place_09_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_pick_10_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_place_05_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_approach_place_10_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_approach_place_02_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_approach_pick_01_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_approach_pick_11_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_approach_pick_03_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_place_07_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_pick_12_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_adopted
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_pick_simple_02_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_approach_place_12_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_pick_24_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_approach_pick_07_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_approach_place_08_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_pick_20_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_approach_place_16_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_approach_place_04_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_place_11_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_pick_04_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_place_03_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_pick_08_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_pick_16_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_approach_place_20_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_approach_place_06_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_approach_place_18_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_pick_22_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_approach_place_14_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_pick_14_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_approach_place_22_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_place_01_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_pick_06_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_pick_18_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_approach_pick_09_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_approach_pick_05_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_approach_place_11_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_pick_simple_01_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_approach_place_03_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_place_08_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_pick_03_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_place_04_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_pick_11_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_approach_place_01_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_pick_25_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_approach_place_13_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_pick_13_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_place_06_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_pick_01_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_approach_pick_10_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_approach_pick_02_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_pick_05_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_place_10_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_approach_place_21_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_pick_17_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_pick_09_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_place_02_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_approach_place_17_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_pick_21_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_approach_place_09_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_approach_place_05_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_approach_pick_06_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_approach_pick_08_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_approach_pick_04_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_approach_place_23_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_pick_15_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_place_12_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_pick_19_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_pick_07_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_approach_place_19_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_classified
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_approach_place_07_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_approach_place_15_frames
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_pick_23_frames
-- adopted を含む .json の所在(ディレクトリ単位)--
   5 /Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_adopted
   1 /Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_classified/approach_pick
   1 /Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_classified/approach_place
   1 /Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_classified/pick
   1 /Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_classified/place_noblock
   1 /Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_classified/place_withblock
-- sweep を含む .json の所在(adopted 以外も含め俯瞰)--
  76 /Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep
   5 /Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_adopted
  10 /Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_classified/approach_pick
  23 /Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_classified/approach_place
  27 /Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_classified/pick
   1 /Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_classified/place_noblock
  11 /Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_classified/place_withblock

===== [twin] FKテーブル(Mac側)=====
/Users/shimazueiichi/r2r2r-scripts/fk_scan_motion_based_v2.csv
/Users/shimazueiichi/r2r2r-scripts/fk_scan_motion_based.csv

===== [twin] FKテーブル(Unity側 Assets/Data)=====
drwxr-xr-x  34 shimazueiichi  staff  1088  4月 10 09:00 /Users/shimazueiichi/Robotics_UX
/Users/shimazueiichi/Robotics_UX/Robotics_UX_New/Assets/Data/fk_scan_motion_based_v2.csv
/Users/shimazueiichi/Robotics_UX/Robotics_UX_New/Assets/Data/fk_scan_motion_based.csv

===== [real] ledger(Mac側)=====

===== [real] ledger(KIOXIA側)+ runs/ 構成 =====
drwx------  1 shimazueiichi  staff  524288  6月 15 19:21 /Volumes/KIOXIA/r2r2r-data
/Volumes/KIOXIA/r2r2r-data/runs/pick_place_ledger_v3.csv
/Volumes/KIOXIA/r2r2r-data/archive/pick_place_ledger_v2_30col_pre_rundir_DAY242.csv
/Volumes/KIOXIA/r2r2r-data/archive/pick_place_ledger_v2_31col_DAY243.csv
/Volumes/KIOXIA/r2r2r-data/archive/ledger_premerge_20260623/pick_place_ledger.csv
/Volumes/KIOXIA/r2r2r-data/archive/ledger_premerge_20260623/pick_place_ledger_v2.csv
/Volumes/KIOXIA/r2r2r-data/archive/ledger_premerge_20260623/pick_place_ledger_v3.csv
/Volumes/KIOXIA/r2r2r-data/archive/ledger_superseded_20260623/pick_place_ledger.csv
/Volumes/KIOXIA/r2r2r-data/archive/ledger_superseded_20260623/pick_place_ledger_v2.csv
/Volumes/KIOXIA/r2r2r-data/archive/ledger_pre_day242add_20260624/pick_place_ledger_v3.csv
/Volumes/KIOXIA/r2r2r-data/archive/pick_place_ledger_v3_pre_pose_cols_20260626.csv
total 7168
drwx------  1 shimazueiichi  staff  524288  6月 24 09:03 .
drwx------  1 shimazueiichi  staff  524288  6月 15 19:21 ..
drwx------  1 shimazueiichi  staff  524288  6月 19 22:35 experiments
drwx------  1 shimazueiichi  staff  524288  6月 21 20:40 motion_stamp
-rwx------  1 shimazueiichi  staff   51872  6月 26 11:27 pick_place_ledger_v3.csv
-rwx------  1 shimazueiichi  staff    2332  6月 24 09:03 README.md
drwx------  1 shimazueiichi  staff  524288  6月 24 09:02 run_pick_place

===== [code] スクリプト本体の実体 =====

===== [根拠] run_pick_place.py が読む JSON / 書く ledger をコードから確認 =====

===== [根拠] j0_lookup.py が読む JSON をコードから確認 =====

===== [根拠] j0_sweep_scan.py の出力先(採用JSON 生成先)=====

分かった事実は二つだ。一つは、live の採用 JSON が ~/r2r2r-scripts/ 直下ではなく dynamic_tracking_limits/logs/j0_sweep/j0_sweep_adopted/ という深い場所にあったこと。略したパスを焼いていたら確実に外していた。二つ目は、real の ledger が Mac 側に一件も無く、KIOXIA の runs/pick_place_ledger_v3.csv が実体だったこと。

そして [code] が空。スクリプト本体が ~/r2r2r-scripts/ の浅い階層に無かった。コード根拠が空なら、地図はまだ描けない。二度目に進む。

実体は place_modules の下にあった

スクリプト本体を深さ無制限で探し、ついでに採用 JSON の実ファイル名も確定した。

echo "--- スクリプト本体を r2r2r-scripts 全体から(深さ無制限)---"
find "$HOME/r2r2r-scripts" -type f \( -name 'run_pick_place.py' -o -name 'j0_lookup.py' -o -name 'j0_sweep_scan.py' \) 2>/dev/null

echo "--- 念のため HOME 直下の主要場所も広く(Desktop/Documents等も)---"
find "$HOME" -maxdepth 5 -type f \( -name 'run_pick_place.py' -o -name 'j0_lookup.py' -o -name 'j0_sweep_scan.py' \) 2>/dev/null | grep -v 'Library/' | head -30

echo "--- adopted JSON の実ファイル名(5本)も確定 ---"
ls -1 "$HOME/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_adopted/"

出力(全量)。

--- スクリプト本体を r2r2r-scripts 全体から(深さ無制限)---
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/place/place_modules/run_pick_place.py
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/place/place_modules/j0_sweep_scan.py
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/place/place_modules/j0_lookup.py
--- 念のため HOME 直下の主要場所も広く(Desktop/Documents等も)---
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/place/place_modules/run_pick_place.py
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/place/place_modules/j0_sweep_scan.py
/Users/shimazueiichi/r2r2r-scripts/dynamic_tracking_limits/place/place_modules/j0_lookup.py
--- adopted JSON の実ファイル名(5本)も確定 ---
_INDEX_source.md
approach_pick_NA_20pt_adopted_DAY235_01_frames
approach_pick_NA_20pt_adopted_DAY235_01.json
approach_place_NA_20pt_adopted_DAY235_01_frames
approach_place_NA_20pt_adopted_DAY235_01.json
pick_NA_20pt_adopted_DAY235_01_frames
pick_NA_20pt_adopted_DAY235_01.json
place_noblock_20pt_adopted_DAY232_01_frames
place_noblock_20pt_adopted_DAY232_01.json
place_withblock_NA_20pt_adopted_DAY235_01_frames
place_withblock_NA_20pt_adopted_DAY235_01.json

スクリプト本体は三本とも dynamic_tracking_limits/place/place_modules/ の下にまとまっていた。採用 JSON は五本。ここで一つ気になる点が出る。採用フォルダに place_noblock の JSON が実在している。引き継ぎには「place は place_withblock を引く/noblock は検出が取れず lookup できない」とあったが、ファイルとしては noblock も残っている。これも、実際に lookup がどちらを読むかをコードで確かめるべき理由になる。

場所が割れたので、本体三本が「実際にどのパスを読み書きするか」を grep で当てた。これが地図の read / write をドキュメントではなくコードに合わせる根拠になる。

D="$HOME/r2r2r-scripts/dynamic_tracking_limits/place/place_modules"

echo "===== run_pick_place.py:読む/書く パス根拠 ====="
grep -nEi 'adopted|j0_sweep|classified|ledger|KIOXIA|/Volumes|\.json|\.csv|glob|open\(|Path\(|read_csv|to_csv|os\.path|join|dir *=' "$D/run_pick_place.py" 2>/dev/null

echo; echo "===== j0_lookup.py:読むJSON 根拠(withblock/noblock の選択ロジック含む)====="
grep -nEi 'adopted|classified|withblock|noblock|phase|j0_sweep|\.json|glob|open\(|Path\(|join|dir *=' "$D/j0_lookup.py" 2>/dev/null

echo; echo "===== j0_sweep_scan.py:採用JSON 出力先 根拠 ====="
grep -nEi 'adopted|classified|j0_sweep|\.json|out|save|write|open\(|Path\(|join|dir *=' "$D/j0_sweep_scan.py" 2>/dev/null

出力(全量)。長いが、行番号つきで「どの定数がどのパスを指すか」がそのまま残っているので省かない。

===== run_pick_place.py:読む/書く パス根拠 =====
10:- 流用: j0_lookup / j0_sweep_scan._send_and_wait_arrival / visual_servo_pick の検出と daemon I/F
13:- ガード: joint_limit_ok / method=interp / x_residual<=THRESH の3条件を送信前に検査
21:- fk_scan_motion_based_v2.csv を書き換えない
43:_THIS = Path(__file__).resolve()
44:_PLACE_MODULES_DIR = _THIS.parent
46:_DRAG_TEACHING_DIR = _SCRIPTS_ROOT / "dynamic_tracking_limits" / "drag_teaching"
73:from j0_sweep_scan import (  # noqa: E402
132:# JSON・画像の出力先(KIOXIA・1 run = 1 フォルダで集約)。
133:# LEDGER_DIR と同じドライブ。未マウント時の誤書き込みは run() 冒頭ガードで防ぐ。
134:LOG_ROOT = Path("/Volumes/KIOXIA/r2r2r-data/runs/run_pick_place")
137:# ---- 試行台帳 CSV(KIOXIA SSD 専用・ローカルへフォールバックしない)-------
138:LEDGER_DIR = Path("/Volumes/KIOXIA/r2r2r-data/runs")
139:LEDGER_CSV_PATH = LEDGER_DIR / "pick_place_ledger_v3.csv"
143:LEDGER_COLS = [
192:    "tip_pick": None,   # PICK到達時の爪先vs block差分(_append_ledger が読む)
193:    "run_dir": None,    # この run の出力フォルダ(KIOXIA)。画面のファイル情報表示用
352:    # in_box は台帳と同じ _ledger_in_box_geometry を再利用(判定の二重実装を避ける).
355:    _ibg, _ = _ledger_in_box_geometry(
372:        run_dir=_OBSERVE_STATE["run_dir"],
380:def _next_log_path():
385:        run_dir = LOG_ROOT / stem
388:            return run_dir / (stem + ".json")
393:    tmp = Path(str(path) + ".tmp")
394:    with open(tmp, "w") as f:
400:    """KIOXIA SSD の LEDGER_DIR が存在・書込可なら True、それ以外 False.
403:        if not LEDGER_DIR.exists():
405:        if not LEDGER_DIR.is_dir():
408:        test = LEDGER_DIR / ".write_test"
419:def _ledger_extract_trial_no(run_stem):
431:def _ledger_mm_or_blank(arr, idx):
441:def _ledger_in_box_geometry(block_place_xz, box_xz):
456:def _append_ledger(log_record, run_stem):
457:    """試行 1 行を KIOXIA SSD の CSV 台帳へ追記.
463:            print("[LEDGER] KIOXIA SSD ({}) 未マウント or 書込不可 → CSV スキップ"
464:                  .format(LEDGER_DIR))
496:        in_box_geom, in_box_dist_mm = _ledger_in_box_geometry(
502:            "trial_no":          _ledger_extract_trial_no(run_stem),
513:            "block_pick_x":      _ledger_mm_or_blank(block_xz, 0),
514:            "block_pick_z":      _ledger_mm_or_blank(block_xz, 1),
515:            "block_place_x":     _ledger_mm_or_blank(block_place_xz, 0),
516:            "block_place_z":     _ledger_mm_or_blank(block_place_xz, 1),
517:            "box_x":             _ledger_mm_or_blank(box_xz, 0),
518:            "box_z":             _ledger_mm_or_blank(box_xz, 1),
573:        is_new = not LEDGER_CSV_PATH.exists()
574:        with open(LEDGER_CSV_PATH, "a", newline="") as f:
575:            w = csv.DictWriter(f, fieldnames=LEDGER_COLS)
579:        print("[LEDGER] append OK: {} (in_box_geom={} dist={})".format(
580:            LEDGER_CSV_PATH, in_box_geom, in_box_dist_mm))
582:        print("[LEDGER] [WARN] append 例外(制御は継続): {}".format(e))
593:      1. joint_limit_ok == False → ABORT
599:    if not r.get("joint_limit_ok"):
600:        return False, "joint_limit違反: {}".format(r.get("joint_limit_violations"))
653:        run_dir=_OBSERVE_STATE["run_dir"],
685:        "joint_limit_ok": r["joint_limit_ok"],
686:        "joint_limit_violations": r["joint_limit_violations"],
696:          "method={}  x_resid={:.5f}m  z_resid={:.5f}m  joint_ok={}".format(
699:              r["joint_limit_ok"]))
905:def stage1_home_and_open(log_record, dry_run):
1042:    # KIOXIA 未マウント時に Mac 内蔵ディスクへ誤書き込みするのを防ぐ.
1045:        print("[ABORT ] KIOXIA SSD 未マウント。run データの出力先が無いため中断します。"
1046:              "({} を確認)".format(LEDGER_DIR))
1048:    log_path = _next_log_path()
1051:    frames_dir = log_path.parent
1121:        if not stage1_home_and_open(log_record, dry_run):
1228:            adopted = vlm_verdict   # 分岐は常に VLM。human_verdict は記録のみ
1229:            _observe_verdict(adopted, pj["confidence"], pj["reason"])
1232:                "verdict": adopted,
1241:            print("[JUDGE ] pick attempt={} adopted={} (vlm={} human={}) "
1243:                      attempt, adopted, vlm_verdict, human_verdict,
1245:            # judgments["pick"] は最終試行の dict を保存(verdict は adopted・VLM/人を併記)
1247:            final_pick_judgment["verdict"] = adopted
1251:            if adopted == "yes":
1254:            if adopted != "no":
1259:                      .format(adopted))
1395:            place_adopted = place_vlm_verdict   # 分岐は常に VLM。記録のみ人
1396:            _observe_verdict(place_adopted, place_vlm["confidence"], place_vlm["reason"])
1400:            place_judgment["verdict"] = place_adopted
1404:            print("[JUDGE ] place: adopted={} (vlm={} human={}) "
1406:                      place_adopted, place_vlm_verdict, place_human_verdict,
1478:        # 試行台帳 CSV を KIOXIA SSD に追記(fire-and-forget・未マウントなら skip).
1482:            _append_ledger(log_record, run_stem)

===== j0_lookup.py:読むJSON 根拠(withblock/noblock の選択ロジック含む)=====
5:採用版JSON (~/r2r2r-scripts/dynamic_tracking_limits/logs/j0_sweep/j0_sweep_adopted/
6:{phase}_*_adopted_*.json) の sweep[] の (detect_xz_m, j0_cmd) を j0_cmd 昇順に
20:- place_noblock は detect 全失敗の整合データで XZ→j0 対応を持たないため lookup 不可(ValueError)
23:  j0_lookup(phase, target_xz) -> dict
26:  python3 j0_lookup.py --phase pick --x -0.010 --z 0.108
27:  python3 j0_lookup.py --self-check --phase pick
28:  python3 j0_lookup.py --self-check --phase all
42:_THIS = Path(__file__).resolve()
43:_PLACE_MODULES_DIR = _THIS.parent
45:_DRAG_TEACHING_DIR = _SCRIPTS_ROOT / "dynamic_tracking_limits" / "drag_teaching"
51:# ---- pymycobot スタブ(waypoint_replay の JOINT_LIMITS 流用のため) ----------
60:                "j0_lookup は JOINT_LIMITS 参照のためだけに waypoint_replay を import します。")
71:ADOPTED_DIR = (
73:    / "logs" / "j0_sweep" / "j0_sweep_adopted"
75:VALID_PHASES = ("approach_pick", "pick", "approach_place",
76:                "place_withblock", "place_noblock")
77:# place_noblock は detect 全失敗の整合データのため、XZ→j0 対応を持たない。
78:LOOKUP_PHASES = ("approach_pick", "pick", "approach_place", "place_withblock")
82:def _find_adopted_json(phase):
83:    """phase に対応する採用版 JSON を adopted/ から 1 件解決。
85:    pattern = "{}_*_adopted_*.json".format(phase)
86:    matches = sorted(ADOPTED_DIR.glob(pattern))
89:            "phase={} の採用版 JSON が見つかりません: {}/{}".format(
90:                phase, ADOPTED_DIR, pattern))
93:            "phase={} の採用版 JSON が複数ヒット(adopted 平場は 1 phase 1 本のはず): {}".format(
94:                phase, [m.name for m in matches]))
123:def _load_arc(phase):
129:    path = _find_adopted_json(phase)
146:            "phase={} の採用点が 2 点未満({} 個): {}".format(
147:                phase, len(pts), path.name))
226:def j0_lookup(phase, target_xz):
230:      phase: "approach_pick" / "pick" / "approach_place" / "place_withblock"
231:             ("place_noblock" は detect 全失敗の整合データのため lookup 不可、ValueError)
236:        "phase": str,
247:        "joint_limit_ok": bool,
248:        "joint_limit_violations": [...],
258:    if phase == "place_noblock":
260:            "place_noblock は detect 全失敗の整合データのため "
262:    if phase not in LOOKUP_PHASES:
264:            "未知の phase: {!r}(許可: {})".format(phase, LOOKUP_PHASES))
269:    arc_points, base_angles, src_path, pose_id = _load_arc(phase)
275:    # JOINT_LIMITS 検査(waypoint_replay の実測値を流用。違反時は例外で落とさずフラグ)
276:    viol = wr._check_joint_limits(target_angles)
277:    joint_limit_ok = (len(viol) == 0)
280:        "phase": phase,
294:        "joint_limit_ok": joint_limit_ok,
295:        "joint_limit_violations": [
312:def _self_check_one(phase):
313:    """phase の各 detect_ok 点を target に入れて lookup → 往復誤差を表示."""
314:    arc_points, base, src, _pid = _load_arc(phase)
315:    print("[SELF-CHECK] phase={}  source={}  採用点 {} 個".format(
316:        phase, src.name, len(arc_points)))
324:        r = j0_lookup(phase, (x, z))
333:    print("[SELF-CHECK] phase={}  max|Δj0|={:.5f}°  "
335:              phase, max_abs_dj0, max_x_resid, max_z_resid))
343:        "--phase",
344:        choices=list(VALID_PHASES) + ["all"],
345:        help="phase('all' は --self-check 専用)",
356:        if args.phase is None:
357:            print("[ABORT] --self-check には --phase が必要(個別 or 'all')")
359:        phases = LOOKUP_PHASES if args.phase == "all" else [args.phase]
360:        for p in phases:
361:            if p == "place_noblock":
362:                print("[SELF-CHECK] phase=place_noblock はスキップ(lookup 不可)")
367:                print("[SELF-CHECK] phase={} ERROR: {}".format(p, e))
372:    if args.phase is None or args.x is None or args.z is None:
373:        print("[ABORT] --phase / --x / --z すべて指定するか、--self-check を使ってください")
375:    if args.phase == "all":
376:        print("[ABORT] --phase=all は --self-check 専用")
379:        r = j0_lookup(args.phase, (args.x, args.z))

===== j0_sweep_scan.py:採用JSON 出力先 根拠 =====
3:"""j0_sweep_scan.py — j0 単軸スイープ自動計測(工程姿勢ベース)
20:- 全送信前に JOINT_LIMITS(waypoint_replay の実測値)一括検査
21:- 1点ごとに JSON を atomic write(途中 ABORT でもデータが残る)
24:    python3 j0_sweep_scan.py --phase pick --j0-min -20 --j0-max 20 --step 2
25:    python3 j0_sweep_scan.py --phase approach_pick --j0-min -20 --j0-max 20 --step 2
28:    ~/r2r2r-scripts/logs/j0_sweep/j0_sweep_{phase}_{NN}.json
29:    ~/r2r2r-scripts/logs/j0_sweep/j0_sweep_{phase}_{NN}_frames/j0_{j0_cmd:+06.1f}.jpg
47:_THIS = Path(__file__).resolve()
48:_PLACE_MODULES_DIR = _THIS.parent
50:_DRAG_TEACHING_DIR = _SCRIPTS_ROOT / "dynamic_tracking_limits" / "drag_teaching"
99:DEFAULT_TEACH_JSON = _DRAG_TEACHING_DIR / "drag_v2_20221013_152840_with_pos.json"
100:DEFAULT_OUTPUT_ROOT = Path.home() / "r2r2r-scripts" / "dynamic_tracking_limits" / "logs" / "j0_sweep"
312:def _next_output_paths(out_root, phase):
313:    """j0_sweep_{phase}_{NN}.json と j0_sweep_{phase}_{NN}_frames/ の次の連番を返す。
315:    out_root.mkdir(parents=True, exist_ok=True)
318:        json_path = out_root / "j0_sweep_{}_{}.json".format(phase, seq)
319:        frames_dir = out_root / "j0_sweep_{}_{}_frames".format(phase, seq)
551:    p.add_argument("--out-dir", type=str, default=str(DEFAULT_OUTPUT_ROOT),
745:        out_root = Path(args.out_dir).expanduser()
746:        json_path, frames_dir = _next_output_paths(out_root, phase)

コード上の根拠は四点に絞れた。run_pick_place.py は LEDGER_CSV_PATH を KIOXIA の pick_place_ledger_v3.csv に固定し、未マウントなら中断する。j0_lookup.py は ADOPTED_DIR を j0_sweep_adopted/ に置き、lookup 対象を approach_pick / pick / approach_place / place_withblock に限定する。place_noblock はファイルとして存在しても、lookup では ValueError になる。j0_sweep_scan.py の出力先は adopted ではなく raw の logs/j0_sweep/ だった。

コードが思い込みを正した

三度の調査で、設計図に仮置きしていた前提のいくつかが実体とずれていたと分かった。

一つ目。real が二重化しているという疑いは消えた。台帳が Mac 側の working と KIOXIA 側で別々に存在し、比較がどちらを読むか曖昧になる、と疑っていた。だが Mac 側に ledger は一件も無い。run_pick_place.py の 138〜139 行が LEDGER_CSV_PATH = /Volumes/KIOXIA/r2r2r-data/runs/pick_place_ledger_v3.csv を直接指していて、KIOXIA 専用、ローカルへフォールバックしない。1042〜1046 行では、KIOXIA が未マウントなら run の冒頭で中断し、Mac 内蔵へは書かない。台帳は単一ソースで、しかも v3 だった。

二つ目。place_noblock は採用フォルダに実在するが、lookup は設計上これを拒否する。j0_lookup.py の 78 行で LOOKUP_PHASES から noblock を外し、258 行以降で noblock を渡すと ValueError を返す。detect が全失敗していて XZ→j0 の対応を持たないからだ。つまり place の lookup は place_withblock を引く、というのがコードで確定した。

三つ目。私が以前まとめた「j0_sweep_scan が採用フォルダに生成する」は誤りだった。j0_sweep_scan.py の 100 行 DEFAULT_OUTPUT_ROOT は .../dynamic_tracking_limits/logs/j0_sweep/ で、ここに raw のスイープ JSON を吐く。採用フォルダ(j0_sweep_adopted/)への配置は別工程だ。ついでに、docstring の 28 行が ~/r2r2r-scripts/logs/j0_sweep/ と省略表記になっていて、実体の dynamic_tracking_limits 配下とずれていた。ドキュメント側の省略が、こういう形でも食い違いを生む。

四つ目。twin の FK テーブルは runtime では読まれない。run_pick_place.py にも j0_lookup.py にも FK テーブルを読む箇所が無く、21 行に「書き換えない」というガードコメントがあるだけだった。FK テーブルは Unity のデジタルツインと planning が使う予測器で、アームの動作経路の外にある。pose_id による突合は事後に行う、という構図がコードで裏づけられた。

出来上がった地図

確定した実体で、全体図を一枚にまとめた。タブを二つに分け、片方は図解、もう片方は絶対パスと根拠を載せた解説にしてある。

図解側は、live を読み・real に書く中央のアームと、読まれない予測器として一段沈めた twin、そして三系統を結ぶ pose_id の関係を、概念名と実体ファイル名(live=j0_sweep_adopted/*.json、twin=fk_scan_motion_based_v2.csv、real=pick_place_ledger_v3.csv)の両方で示した。実行時のフローチャートには、各処理がどのコードのどの関数で、どのファイルを読み書きするかを併記した。解説側には、上の調査で確定した絶対パスを省略なしで並べ、read / write はコードの行番号を根拠に据えた。

この図は、実機の runtime が読む線と、比較時に後から読む線を分けている。FK テーブルは runtime の線から外し、pose_id による事後照合の線に置いた。クリックすると各系統の実体ファイルと役割が出る。

<embed src="DAY256_data_map.html" style="width:100%;border:none;" />

この地図の価値は、装飾ではなく「どのファイルを見ればいいか」を一意に決めたことにある。比較テーブルがどの台帳を読むか、live のどの JSON が runtime に効くか、twin がどこにあって誰が使うか。これらが推測から実測に変わった。

次に実機へ戻る

地図はコードに一致した。残った実アクションは一つ、ドキュメント側の保守だ。保管ポリシーの記述が台帳を v2 と書いているが、コードは v3 を指している。ここを v3 にそろえる。

そのうえで、止めていた実機タスクに戻る。dry-run で pose_id 入りの JSON が吐かれるかを目視し、本番の通しで pose_id と実測角が KIOXIA の台帳に記録されるかを確かめ、溜まったところで pose_id を鍵に twin の予測と real の実測を突き合わせる。地図がある分、どのファイルを見ればいいかはもう迷わない。

実機を動かす前に、どのファイルを読むのか、どのファイルに書くのかを確定した。地図がないまま比較すれば、別物どうしを突き合わせることになる。この一枚は回り道ではない。比較の前提だった。

※ プロジェクト名「R2R2R」は、UC Berkeley・Toyota Research Instituteの研究「Real2Render2Real」(Yu, Fu et al., 2025)の概念に触発されています。現実空間をデジタルツイン化し、仮想空間での試行から得た知見を実世界のモーター駆動へ還元する——その設計と実装過程を毎日記録しています。技術アプローチおよび目的は独自のものです。