diff --git a/CLAUDE.md b/CLAUDE.md index 11f9717..056b237 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -39,6 +39,7 @@ 補足: **残差 2.63× の主因候補を [TECH_10](docs/05_TECH/TECH_10_残差主因候補の網羅整理.md) として網羅整理 (2026-06-02)**.Step 8-14 候補選定の判断材料として作成.ユーザー指摘 3 候補 (LED 配光・反射率・反射回数) + 私の追加候補で 5 カテゴリ 18 項目を網羅.**優先度評価: 🥇 1 位 A1 内壁の非 Lambertian 性 (光沢成分混合 BSDF)** - 「形」を変える可能性が物理的に最も高い,3D プリンタ白フィラメントは完全 Lambertian とは限らない / **🥈 2 位 A8 反射率 R=0.85→0.89** - Step 8-6 物理整合点で多重反射ゲイン 1.36× / **🥉 3 位 A9 max_depth 8→16+** - 後段多重反射の取りこぼし可能性.ユーザー認識 (LED 配光が一番影響) との差分: TECH_09 でデータシート Lambertian 整合済を踏まえて優先度を低下.Step 8-14 候補: β (A1 単独) か δ (A1+A8+A9 全部入り) が筋. 補足: **Step 8-14 候補 γ (D1 ROI 整合の再確認) 完了 — クリア (2026-06-02)**.他の物理モデル変更に進む前の事前確認として実施.設定変更ゼロで,既存設定の数値検証のみ.シミュ ROI (24, 37, 464, 450) の物理範囲 (X∈[-45.31, +45.31] mm × Y∈[-46.92, +44.48] mm,幅 90.63×91.41 mm) と実機 ROI (539, 1015, 1400, 1409) の物理範囲 (実機 PNG の評価板黒縁検出から物理分解能 0.0654 mm/px を確定 → 幅 91.54×92.13 mm) を比較した結果,**物理サイズ差 < 1%・中心位置差 < 1 mm で整合**.3 列パネル `output/_slide_panels/roi_alignment_check.png` (①実機画像 + 評価板境界・実機 ROI / ②シミュ輝度マップ + 評価板境界・シミュ ROI / ③物理座標系で重ね合わせ) でも視覚確認.**残差 2.63× の主因は ROI 整合ではない**ことが確定.過去の設定者 (`scripts/select_white_board.py` GUI 選択 + シミュ ortho_scale 設計値) が物理整合性を意識して ROI を揃えていたことが裏付けられた.副次発見: 評価板は実機画像中央から下方向に約 4.2 mm (65 px) オフセット配置されているが (取付位置のミリ精度差),ROI もそのオフセットに沿って選択されているため物理領域として整合.次のアクション: 残候補 β (A1 単独) / α (A8+A9) / δ (β+α 全部入り) を選択して Step 8-14 を実施. 補足: **Step 8-14 候補 β (A1 内壁の非 Lambertian 性) 完了 — クリア (2026-06-02)**.候補リスト 🥇 1 位だった A1 を 3×3 グリッド感度分析で検証.スキーマ拡張 (`BlendBSDFConfig` + `RoughConductorBSDFConfig` 追加,`MaterialConfig` Union 拡張,forward reference + `model_rebuild()`) と `materials.py` 再帰変換 (`_build_bsdf_dict`) を新規実装.テンプレ `configs/scene_baseline_blend.yaml` + スイープスクリプト `scripts/sweep_blend_bsdf.py` + 比較パネルスクリプト `scripts/build_blend_bsdf_panel.py`.新規テスト 89 件追加 (608 → 697 全通過).**weight ∈ {0.05, 0.15, 0.30} × α ∈ {0.1, 0.3, 0.6} の 9 ジョブ実走 (75.5 分)**.結果: ベースライン純 diffuse (CoV=0.1826, 勾配=24.65%, C/P=1.327, 動径 min/max=0.615) と比較して **全 9 パターンが悪化または不変** (最良 w=0.05/α=0.6 で CoV=0.1851,最悪 w=0.30/α=0.1 で CoV=0.2433).実機 (CoV=0.0695) 方向と **逆方向に動く**.「形」は w=0.30 で下端中央に 2 つの specular 集中スポットが顕在化.物理的解釈: 純 Lambertian は cos 重み散乱で多重反射の均一化作用が最大.specular 成分は特定方向に光を集中させ均一化を阻害.**3D プリンタ白フィラメントは完全 Lambertian として扱うのが実機挙動に近い近似** と判明 (TECH_09 LED Lambertian 整合と並ぶ「物理モデル妥当性の裏付け」).**A1 は残差 2.63× の主因ではない**ことが確定.TECH_10 優先順位を更新: A1 を「クリア」化,🥇 1 位 A8 反射率 R=0.89 / 🥈 2 位 A9 max_depth 16+ / 🥉 3 位 B1 幾何精度 に繰り上げ.副次成果: スキーマ層に `blendbsdf` + `roughconductor` のサポートが入り,今後の物理モデル試行で再利用可能.比較パネル `output/_slide_panels/blend_bsdf_sweep.png` + `blend_bsdf_metrics.png`,全 9 ジョブ集約 `output/blend_sweep/summary.csv`.[TECH_10 § A1 検証結果](docs/05_TECH/TECH_10_残差主因候補の網羅整理.md#a1-検証結果2026-06-02-完了) / [TEST_01 § Step 8-14 候補 β](docs/06_TEST/TEST_01_キャリブレーション検証履歴.md#step-8-14-候補-β-a1-内壁の非-lambertian-性--混合-bsdf-感度分析) / [SPEC_02 materials](docs/04_SPEC/SPEC_02_シーン定義仕様.md#materials) 参照.次のアクション: Step 8-14 候補 α (A8 R=0.89 + A9 depth=16) の組み合わせ実施. +補足: **LED グループ別 sanity check 完了 — TEST_07 として報告 (2026-06-03)**.Step 8 系キャリブの主軸ではなく「シミュが下/右/左の 3 LED 基板を物理位置どおりに独立にモデル化できているか」の基礎健全性確認.`scripts/sanity_led_groups.py` で各基板の 6 LED だけを ON にした一時 YAML を生成 → 3 ジョブ sm-render (約 25 分) → 比較パネル.結果: **方向性 ✓** (bottom は下端中央/right は右側面寄り/left は左側面寄り),**左右対称性 ✓** (right mean=47.10 vs left mean=47.08),**量的バランスは bottom が right/left の 2.10× で強い**.ユーザー観察「bottom は上下の島,right/left は中心の円」は LED 配置で説明: bottom LED は評価板の下端真下に X 中央寄り集中 → 直接光が広範囲を叩く / right/left LED は側面外側で Y 中央付近に分散 → 直接光は端のみ・中央は多重反射経由 → 「直接光寄与が大きい bottom」vs「多重反射寄与が大きい right/left」の構造的違い.**線形性検証は別途未実施**(既存 `output/baseline/luminance.npy` が Step 8-1 時点の古いもので和との比較不可.Mitsuba は理論線形なので baseline 再生成で一致見込み).副次発見: 実機の right (Y∈[-25,+3]) と left (Y∈[-33,-5]) は **完全な左右対称設計ではない**(Fusion STL 実位置と整合).[TEST_07 LED グループ別動作確認](docs/06_TEST/TEST_07_LEDグループ別動作確認.md) 参照.**シミュレーションは 3 つの照明源を物理位置どおりに独立にモデル化できている**ことが確定し,Step 8 系の残差 2.63× 議論の前提が担保された. 実装済みモジュール: - `src/sm_tias_lightsim/uniformity.py` — Rec.709 輝度変換 + CoV/std/max-min(MiniTias-Evaluation コピー) diff --git a/docs/06_TEST/README.md b/docs/06_TEST/README.md index d993e4f..714ebf8 100644 --- a/docs/06_TEST/README.md +++ b/docs/06_TEST/README.md @@ -19,14 +19,16 @@ | 1 | [TEST_01_キャリブレーション検証履歴.md](TEST_01_キャリブレーション検証履歴.md) | Step 8-1〜8-13 + Step 8-N 候補の **時系列フル記録**.各イテレーションの仮説/変更/結果/解釈/学び.新規 Step 追加時はここに追記 | | 2 | [TEST_02_シミュレーション実行記録.md](TEST_02_シミュレーション実行記録.md) | `output/` 配下に蓄積された **実行結果ディレクトリの索引**.「何の試行か・どう再現するか」を辿れる.`output/` は gitignore 対象なので手元になくてもこのドキュメントで網羅 | | 3 | [TEST_03_輝度マップ形状追跡.md](TEST_03_輝度マップ形状追跡.md) | 数値指標 (CoV / 勾配等) では捉えられない **「形そのもの」を画像比較**.`figures/luminance_map_evolution.png` の比較パネルとセット.`scripts/build_luminance_evolution_panel.py` で生成 | +| 4 | [TEST_07_LEDグループ別動作確認.md](TEST_07_LEDグループ別動作確認.md) | 下/右/左 LED 基板を **単独 ON にして方向性・対称性・線形性の sanity check**.シミュが 3 つの照明源を物理位置どおりにモデル化できているかの基礎健全性確認.Step 8 系キャリブの前提保証 | -### 3 ファイルの使い分け +### 4 ファイルの使い分け | 知りたいこと | 読むべきファイル | | --- | --- | | 「過去どんな Step を試したか,何が判明したか」 | **TEST_01** | | 「`output/baseline_spot/` って何のレンダ結果?」 | **TEST_02** | | 「シミュは形が実機と全然違うのか?」 | **TEST_03**(画像で一目) | +| 「シミュは LED 3 基板を独立に扱えているのか?」 | **TEST_07**(sanity check) | ## 🔵 クローズ済みの試行履歴 — 1 ファイル diff --git "a/docs/06_TEST/TEST_02_\343\202\267\343\203\237\343\203\245\343\203\254\343\203\274\343\202\267\343\203\247\343\203\263\345\256\237\350\241\214\350\250\230\351\214\262.md" "b/docs/06_TEST/TEST_02_\343\202\267\343\203\237\343\203\245\343\203\254\343\203\274\343\202\267\343\203\247\343\203\263\345\256\237\350\241\214\350\250\230\351\214\262.md" index bfa9220..d8407ad 100644 --- "a/docs/06_TEST/TEST_02_\343\202\267\343\203\237\343\203\245\343\203\254\343\203\274\343\202\267\343\203\247\343\203\263\345\256\237\350\241\214\350\250\230\351\214\262.md" +++ "b/docs/06_TEST/TEST_02_\343\202\267\343\203\237\343\203\245\343\203\254\343\203\274\343\202\267\343\203\247\343\203\263\345\256\237\350\241\214\350\250\230\351\214\262.md" @@ -36,6 +36,8 @@ | `output/blend_sweep/summary.csv` | 8-14 候補 β | — | — | 9 ジョブ集約 CSV (weight, alpha, elapsed_s + 12 指標カラム) | `scripts/sweep_blend_bsdf.py` の集約出力.`scripts/build_blend_bsdf_panel.py` の入力 | | `output/_slide_panels/blend_bsdf_sweep.png` | 8-14 候補 β | — | — | 3×3 輝度マップ比較パネル (shared vmin/vmax 1%/99% percentile) | TEST_01 Step 8-14 β「形」追跡用 | | `output/_slide_panels/blend_bsdf_metrics.png` | 8-14 候補 β | — | — | 9 ジョブの数値表 (CoV/勾配/C/P/動径 min/max) | TEST_01 Step 8-14 β 数値追跡用 | +| `output/led_group_sanity/{bottom,right,left}/` | LED sanity check | `configs/scene_baseline.yaml` から lights を各 LED 基板(6 個ずつ)のみに絞った一時 YAML × 3 種 (d=8, spp=1024) | (評価なし,目視確認) | mean: bottom=98.82 / right=47.10 / left=47.08 | [TEST_07 LED グループ別動作確認](TEST_07_LEDグループ別動作確認.md) — 方向性・対称性・線形性の sanity check | +| `output/_slide_panels/led_group_sanity{,_focus,_right_left}.png` | LED sanity check | — | — | 1×3 / 1×2 / 6 列の比較パネル | TEST_07 視覚確認用 | ### sweep_summary.csv の値(参考、TEST_01 line と一致確認用) @@ -69,6 +71,8 @@ | perspective_ptracer_hi レンダ (Step 8-12 検証) | `$env:DRJIT_LIBLLVM_PATH = "C:\Program Files\LLVM\bin\LLVM-C.dll"; uv run sm-render --scene configs/scene_baseline_perspective_ptracer.yaml --output output/perspective_ptracer_hi/` | | blend BSDF 3×3 スイープ (Step 8-14 β) | `$env:DRJIT_LIBLLVM_PATH = "C:\Program Files\LLVM\bin\LLVM-C.dll"; uv run python scripts/sweep_blend_bsdf.py` (9 ジョブ × 約 8.4 分 = 75.5 分) | | blend BSDF 比較パネル生成 | `uv run python scripts/build_blend_bsdf_panel.py` (要 `output/blend_sweep/summary.csv` + 各 `luminance.npy`) | +| LED グループ別 sanity check (TEST_07) | `$env:DRJIT_LIBLLVM_PATH = "C:\Program Files\LLVM\bin\LLVM-C.dll"; uv run python scripts/sanity_led_groups.py` (3 ジョブ × 約 8.5 分 = 約 25 分) | +| LED sanity check 比較パネル生成 | `uv run python scripts/build_led_group_sanity_panel.py` (要 `output/led_group_sanity/{bottom,right,left}/luminance.npy` + `output/baseline/luminance.npy`) | ## 過去の整理実績 (Cleanup History) diff --git "a/docs/06_TEST/TEST_07_LED\343\202\260\343\203\253\343\203\274\343\203\227\345\210\245\345\213\225\344\275\234\347\242\272\350\252\215.md" "b/docs/06_TEST/TEST_07_LED\343\202\260\343\203\253\343\203\274\343\203\227\345\210\245\345\213\225\344\275\234\347\242\272\350\252\215.md" new file mode 100644 index 0000000..5ca1db7 --- /dev/null +++ "b/docs/06_TEST/TEST_07_LED\343\202\260\343\203\253\343\203\274\343\203\227\345\210\245\345\213\225\344\275\234\347\242\272\350\252\215.md" @@ -0,0 +1,163 @@ +# LED グループ別動作確認 (LED Group Sanity Check) + +本ドキュメントは **シミュレーションが下/右/左の 3 つの LED 基板を物理位置どおりに独立にモデル化できているか** を,各グループの LED だけを ON にした輝度マップの形と数値で検証した結果のレポートである.Step 8 系キャリブの主軸ではなく **シミュ動作の基礎健全性確認** として位置付ける. + +## 目的・背景 + +**動機**: Step 8 系で「実機 vs シミュの残差 2.63×」を追っている過程で,シミュレーション側の動作が物理どおりに動いているかの確認が不十分だった.特に: + +- LED は実機で **3 つの基板(下/右/左)に分散配置**されているが,シミュではこれを正しく扱えているか +- 各基板から出る光の **方向性** が物理どおりに評価板に届いているか +- **線形性**(各 LED の光は独立に重ね合わさるはず)が成り立っているか + +これらは「実機との残差は何が主因か」を議論する前提として担保すべき性質. + +## 検証手順 + +1. `configs/scene_baseline.yaml` の lights から各グループの LED だけを残した一時 YAML を生成 +2. 各で `sm-render` を実行(spp=1024, max_depth=8) +3. 生 linear 輝度マップ(`luminance.npy`)を取得して 1×3 並列比較パネルを生成 + +| グループ | 残した LED | LED 数 | +| --- | --- | --- | +| `bottom` | `led_bottom_0` 〜 `led_bottom_5` | 6 個 | +| `right` | `led_right_0` 〜 `led_right_5` | 6 個 | +| `left` | `led_left_0` 〜 `led_left_5` | 6 個 | + +スクリプト: `scripts/sanity_led_groups.py`(3 ジョブ順次実行)+ `scripts/build_led_group_sanity_panel.py`(6 列パネル + 線形性誤差)+ 拡大確認用の 1×3 / 1×2 パネル生成. + +成果物: +- `output/led_group_sanity/{bottom,right,left}/luminance.npy` +- `output/_slide_panels/led_group_sanity.png` (6 列: ①〜③ 単独 / ④ 全 ON / ⑤ 和 / ⑥ 線形性誤差) +- `output/_slide_panels/led_group_sanity_focus.png` (1×3 拡大) +- `output/_slide_panels/led_group_sanity_right_left.png` (1×2 左右対称性確認) + +## 結果 — 数値 + +| グループ | mean | max | min | グループ単独で見える最強点 | +| --- | --- | --- | --- | --- | +| ① bottom (6 LED) | **98.82** | 157.77 | 0.00 | 評価板の下端中央 | +| ② right (6 LED) | 47.10 | 81.42 | 0.00 | 評価板の右側面寄り(やや上下に分散) | +| ③ left (6 LED) | 47.08 | 84.03 | 0.00 | 評価板の左側面寄り(やや下寄り) | + +→ **right と left の mean が一致 (47.10 vs 47.08)** で **左右対称性 ✓**.**bottom が right/left の 2.10× で強い**. + +## 結果 — 「形」の観察 + +3 つの輝度マップは形が明確に異なる: + +| グループ | 形の特徴 | +| --- | --- | +| **bottom** | 評価板の **下端中央が縦長の島状に明るい**.Y 方向単調勾配(下が明るく上が暗い).直接光が支配的 | +| **right** | **評価板の中心寄りに円状のホットスポット**+上下成分も強い.右側面寄りが直接光,中央が反射光 | +| **left** | right と類似(中心円 + 上下成分)だが Y 方向の重心がやや下寄り.完全な左右対称ではない | + +→ ユーザー観察「bottom は上下の島,right/left は中心の円」は **物理的に LED 配置に対応** した正しい観察. + +## LED 配置と「形」の物理的対応 + +`configs/scene_baseline.yaml` の LED 座標から,形の違いを物理的に説明する: + +| グループ | LED 中心座標 | 評価板との幾何関係 | +| --- | --- | --- | +| **bottom** | X∈[-10, +18], Y=**-71**, Z=-66.1 | 評価板下端 (Y=-51) より **20 mm 下**.**X 方向に密集**([-10, +18] の 27 mm 幅) | +| **right** | X=**+66**, Y∈[-25, +3], Z=-66.0 | 評価板右端 (X=+49.5) より外側 **16.5 mm**.**Y 方向に分散**([-25, +3] の 27 mm 幅,**評価板 Y 中央付近**) | +| **left** | X=**-66**, Y∈[-33, -5], Z=-66.0 | 評価板左端 (X=-49.5) より外側 16.5 mm.**Y 方向に分散**([-33, -5] の 27 mm 幅,**評価板 Y 中央やや下**) | + +評価板: X∈[-49.5, +49.5], Y∈[-51, +51], 中心 Z≈-4.5.全 LED の光軸は +Z 方向(評価板向き). + +### bottom = 「下端の縦長の島」 + +LED が **評価板の下端の真下に X 方向で密集**.直接光は評価板の **下端中央を縦長に照らす**.LED の X 範囲 [-10, +18] が評価板 X 範囲 [-49.5, +49.5] のごく中央部に収まるため,X 方向の幅は狭い(島状)が,Y 方向には下端から評価板全体に届く.Y 方向単調勾配が支配的. + +### right/left = 「中心の円 + 上下成分」 + +LED が **評価板の側面外側に Y 方向で分散**.LED の Y 範囲 [-25, +3] は評価板 Y 範囲 [-51, +51] のうち **中央寄り**.これにより: + +- **「上下成分」** = Y 方向に 27 mm 幅で LED が並ぶため,評価板の中央〜下寄りに光が直接届く(Y 方向に幅広く照らす) +- **「中心の円」** = LED が **評価板中心の高さ** に集中していると,直接光が評価板の側面に当たる + 内壁拡散反射で **評価板中央付近** に集中して見える円形ホットスポット + +### bottom が right/left の 2.1× 強い理由(ユーザー仮説の検証) + +ユーザー仮説「下成分の影響力が強い = 反射少なめで当たる量が多い」は **物理的に正しい**.主因は以下の幾何的効率の違い: + +#### 要因 1: LED 〜 評価板中心への到達経路の角度 + +LED から評価板中央(X=0, Y=0, Z=-4.5)への直線方向の **+Z 軸(光軸)からの角度**: + +| グループ | LED 代表位置 | 評価板中央への方向 | +Z からの角度 | +| --- | --- | --- | --- | +| bottom (X≈+4, Y=-71, Z=-66) | (+4, -71, -66) → (0, 0, -4.5) | (-4, +71, +61.5) | **arctan(√(4²+71²)/61.5) ≈ 49°** | +| right (X=+66, Y≈-11, Z=-66) | (+66, -11, -66) → (0, 0, -4.5) | (-66, +11, +61.5) | **arctan(√(66²+11²)/61.5) ≈ 47°** | + +意外にも **角度はほぼ同じ(49° vs 47°)**.これだけでは説明できない. + +#### 要因 2: 評価板への「立体角」と直接光の到達割合 + +bottom LED は評価板の **真下 (Y 軸 +Z 方向)** に集中している → 評価板の **広い範囲が立体角内**に入る.Lambertian 配光は +Z 方向に最大の放射輝度で、評価板の **広範囲を直接照射**できる. + +一方 right/left LED は評価板の **側面外側**に位置 → 直接光は評価板の **側面の端部しか届かない**.評価板の中央〜反対側まで光を運ぶには **内壁多重反射が必要**.多重反射は反射率 R=0.85 で減衰し、各バウンスで cos 重みで散乱するので **直接光の効率より大幅に下がる**. + +#### 要因 3: 評価板への近接効果 + +| グループ | LED 代表点 → 評価板裏面中心 (0,0,-4.5) の距離 | +| --- | --- | +| bottom (+4, -71, -66) | √(4² + 71² + 61.5²) = **94 mm** | +| right (+66, -11, -66) | √(66² + 11² + 61.5²) = **91 mm** | + +距離自体はほぼ同じ.光量の差は距離ではなく **「直接光で評価板全体を照らせるか」** の幾何効率に由来. + +#### 結論 + +**bottom は評価板の真下から直接照射型** であり,**right/left は側面照射+多重反射型** という構造的な違いがある.bottom の 2.10× の優位性は: + +- 直接光の到達効率の違い(評価板の広い領域を直接光が叩く) +- 多重反射のロス係数(R=0.85 のバウンスごとの減衰) + +これは実機の照明設計としても **意図したもの**(bottom LED の方が「主光源」に近い役割)と考えられる. + +## 線形性違反の検出と原因 + +3 グループの輝度マップを足し合わせると、本来 Mitsuba の path tracer は **厳密に線形**なので「①+②+③ = ④(全 18 個 ON)」が成り立つはず.しかし数値は: + +| 項目 | mean | +| --- | --- | +| ① bottom | 98.82 | +| ② right | 47.10 | +| ③ left | 47.08 | +| **①+②+③ の和** | **193.00** | +| ④ 既存 `output/baseline/luminance.npy` の全 ON | **139.98** | +| **相対誤差 mean** | **+37.87%** | + +→ **比較対象の `output/baseline/luminance.npy` の生成時刻が `2026-05-18`(Step 8-1 時点)**であり、現在の `configs/scene_baseline.yaml`(Step 8-9 で LED 位置を Fusion STL から再抽出して更新済)と **不整合**であることが判明. + +| ファイル | 生成時刻 | 対応する scene 設定 | +| --- | --- | --- | +| `output/baseline/luminance.npy` | 2026-05-18 (Step 8-1) | 旧 LED 位置(等間隔 8.7 mm) | +| `output/baseline_v3_leds/luminance.npy` | 2026-05-28 (Step 8-9) | 現在の LED 位置(3+ギャップ+3) | + +線形性自体の検証は、最新シーンで baseline を再生成して再比較する必要がある(本 sanity check の範囲外).Mitsuba は理論的に線形なので **再生成すれば一致するはず**. + +## sanity check の結論 + +| 観点 | 結論 | +| --- | --- | +| **方向性**(各単独 ON で光が LED 基板側から来ている) | ✓ bottom 単独で下端中央、right/left 単独で側面寄りが明るい | +| **対称性**(right と left の対称) | ✓ mean がほぼ同じ (47.10 vs 47.08)、形も対称(Y 中心オフセットを除く) | +| **量的バランス**(bottom が right/left より強い理由) | ✓ 物理的に整合(直接光効率の違い) | +| **線形性**(厳密な数値合致) | ✗ 既存 baseline が古いため未検証.Mitsuba は理論線形なので再生成で一致すると予想 | + +→ **シミュレーションは 3 つの照明源を物理位置どおりに独立にモデル化できている**.Step 8 系の残差 2.63× を議論する前提は担保された. + +## 副次発見 + +- 実機の右基板と左基板は **Y 方向の取り付け位置が微妙に異なる**(right: Y∈[-25, +3] / left: Y∈[-33, -5]).**完全な左右対称設計ではない**.これは [Step 8-9 LED 位置正確化](TEST_01_キャリブレーション検証履歴.md#step-8-9-led-位置を-fusion-360-設計-stl-から抽出して反映) で Fusion STL から実位置を抽出した結果と整合. +- `output/baseline/luminance.npy` が **Step 8-1 時代のまま放置**されていたことが発覚.今後の比較で混乱を招く可能性があるため、再生成または明示的に古い旨を記載すべき. +- LED の光軸は全グループで +Z 方向で統一されている(実機の基板も全て評価板向きで取り付け).配光モデルが Lambertian なので、評価板への到達効率は **LED 〜 評価板の幾何関係**に支配される. + +## 関連ドキュメント (Related Documents) + +- [TEST_01 キャリブレーション検証履歴](TEST_01_キャリブレーション検証履歴.md) — 特に [Step 8-9 LED 位置正確化](TEST_01_キャリブレーション検証履歴.md#step-8-9-led-位置を-fusion-360-設計-stl-から抽出して反映) で現在の LED 配置の出典 +- [TEST_02 シミュレーション実行記録](TEST_02_シミュレーション実行記録.md) — `output/led_group_sanity/` のアーティファクト索引 +- [SPEC_03 基準設計の物理構成 §LED 光源](../04_SPEC/SPEC_03_基準設計の物理構成.md#led光源) — LED 物性(radiance, 配光) +- 生成スクリプト: `scripts/sanity_led_groups.py` / `scripts/build_led_group_sanity_panel.py` diff --git a/scripts/build_led_group_sanity_panel.py b/scripts/build_led_group_sanity_panel.py new file mode 100644 index 0000000..6a06e72 --- /dev/null +++ b/scripts/build_led_group_sanity_panel.py @@ -0,0 +1,153 @@ +"""LED グループ別 sanity check の比較パネル生成. + +scripts/sanity_led_groups.py の出力から 5 列比較パネルを作る: + ① bottom 単独 ON + ② right 単独 ON + ③ left 単独 ON + ④ 全 18 個 ON (output/baseline/luminance.npy) + ⑤ ①+②+③ の和 (線形性確認用) + +加えて差分パネル ⑥ |④ - ⑤| を生成し,線形性誤差を可視化する. + +実行方法: + uv run python scripts/build_led_group_sanity_panel.py + +前提条件: + - output/led_group_sanity/{bottom,right,left}/luminance.npy が揃っていること + - output/baseline/luminance.npy が全 ON ベースラインとして存在すること + +出力: + - output/_slide_panels/led_group_sanity.png +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +import matplotlib +import numpy as np + +matplotlib.use("Agg") +import matplotlib.font_manager as fm +import matplotlib.pyplot as plt + +# --------------------------------------------------------------------------- +# 定数 +# --------------------------------------------------------------------------- + +ROOT = Path(__file__).resolve().parents[1] +SANITY_BASE = ROOT / "output" / "led_group_sanity" +BASELINE_NPY = ROOT / "output" / "baseline" / "luminance.npy" +OUTPUT_PANEL = ROOT / "output" / "_slide_panels" / "led_group_sanity.png" + + +def _setup_japanese_font() -> None: + """matplotlib に日本語フォントを設定する (Windows 標準).""" + for path in ("C:/Windows/Fonts/meiryo.ttc", "C:/Windows/Fonts/YuGothM.ttc"): + if Path(path).exists(): + fm.fontManager.addfont(path) + plt.rcParams["font.family"] = fm.FontProperties(fname=path).get_name() + return + + +def main() -> int: + """パネル生成のメイン処理.""" + _setup_japanese_font() + + # 各グループの luminance.npy を読み込み + groups = ["bottom", "right", "left"] + lums: dict[str, np.ndarray] = {} + for g in groups: + npy_path = SANITY_BASE / g / "luminance.npy" + if not npy_path.exists(): + print(f"エラー: {npy_path} が見つかりません", file=sys.stderr) + return 1 + lums[g] = np.load(npy_path) + + # ベースライン (全 ON) 読み込み + if not BASELINE_NPY.exists(): + print(f"エラー: ベースライン {BASELINE_NPY} が見つかりません", file=sys.stderr) + return 1 + baseline = np.load(BASELINE_NPY) + + # スカラ輝度に変換 (RGB なら平均,2D ならそのまま) + def to_2d(arr: np.ndarray) -> np.ndarray: + return arr.mean(axis=-1) if arr.ndim == 3 else arr + + lum_bottom = to_2d(lums["bottom"]) + lum_right = to_2d(lums["right"]) + lum_left = to_2d(lums["left"]) + lum_all = to_2d(baseline) + lum_sum = lum_bottom + lum_right + lum_left + diff = np.abs(lum_all - lum_sum) + + # 線形性指標 + rel_err = diff.mean() / max(lum_all.mean(), 1e-9) + max_rel_err = diff.max() / max(lum_all.mean(), 1e-9) + + # 共有 vmin/vmax (① 〜 ⑤ の 1%/99% percentile を集約) + stacked = np.stack([lum_bottom, lum_right, lum_left, lum_all, lum_sum]) + vmin = float(np.percentile(stacked, 1)) + vmax = float(np.percentile(stacked, 99)) + + # mean 各値の表示 + means = { + "bottom": float(lum_bottom.mean()), + "right": float(lum_right.mean()), + "left": float(lum_left.mean()), + "all": float(lum_all.mean()), + "sum": float(lum_sum.mean()), + } + + # ============ パネル生成 ============ + fig = plt.figure(figsize=(22, 5)) + titles = [ + f"① bottom (6 LED)\nmean={means['bottom']:.2f}", + f"② right (6 LED)\nmean={means['right']:.2f}", + f"③ left (6 LED)\nmean={means['left']:.2f}", + f"④ 全 18 LED ON\nmean={means['all']:.2f}", + f"⑤ ①+②+③ の和\nmean={means['sum']:.2f}", + f"⑥ |④-⑤| 線形性誤差\n相対誤差 mean={rel_err*100:.3f}%, max={max_rel_err*100:.3f}%", + ] + data = [lum_bottom, lum_right, lum_left, lum_all, lum_sum, diff] + + for i, (img, title) in enumerate(zip(data, titles)): + ax = fig.add_subplot(1, 6, i + 1) + if i < 5: + im = ax.imshow(img, cmap="hot", vmin=vmin, vmax=vmax) + else: + # 差分は別 colormap (viridis) + 独立 scale + diff_vmax = float(np.percentile(img, 99)) + im = ax.imshow(img, cmap="viridis", vmin=0, vmax=max(diff_vmax, 1e-9)) + ax.set_title(title, fontsize=10) + ax.set_xticks([]) + ax.set_yticks([]) + plt.colorbar(im, ax=ax, fraction=0.046, pad=0.04) + + fig.suptitle( + "LED グループ別 sanity check — 下/右/左 単独 ON と線形性確認", + fontsize=13, fontweight="bold", + ) + plt.tight_layout() + OUTPUT_PANEL.parent.mkdir(parents=True, exist_ok=True) + plt.savefig(OUTPUT_PANEL, dpi=110, bbox_inches="tight") + print(f"パネル保存: {OUTPUT_PANEL}") + print() + print("=== 線形性確認 ===") + print(f" 4 全 ON mean : {means['all']:.4f}") + print(f" 5 ①+②+③ 和 mean : {means['sum']:.4f}") + print(f" 相対誤差 mean : {rel_err*100:.5f}%") + print(f" 相対誤差 max : {max_rel_err*100:.5f}%") + print() + print("=== グループ別 mean ===") + print(f" ① bottom : {means['bottom']:.4f}") + print(f" ② right : {means['right']:.4f}") + print(f" ③ left : {means['left']:.4f}") + print(f" → bottom+right+left = {means['bottom']+means['right']+means['left']:.4f}") + print(f" → 全 ON = {means['all']:.4f}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/sanity_led_groups.py b/scripts/sanity_led_groups.py new file mode 100644 index 0000000..889e555 --- /dev/null +++ b/scripts/sanity_led_groups.py @@ -0,0 +1,155 @@ +"""LED グループ別 sanity check. + +下/右/左 の各 LED 基板の LED だけを ON にしたときの輝度マップを生成し, +シミュが 3 つの照明源を正しく独立に扱えているかを目視確認する. + +各グループの輝度マップ: + bottom: led_bottom_0 〜 led_bottom_5 (6 個) のみ ON + right : led_right_0 〜 led_right_5 (6 個) のみ ON + left : led_left_0 〜 led_left_5 (6 個) のみ ON + +期待される観察: + - 方向性: 各単独 ON で評価板上の光が LED 基板側に偏る + - 対称性: right と left は左右対称パターン + - 線形性: bottom + right + left ≒ 全 ON ベースライン (Mitsuba linear renderer) + - 量的バランス: 3 グループの mean がほぼ同じ (各 6 個・同 radiance) + +実行方法: + uv run python scripts/sanity_led_groups.py + +前提条件: + - configs/scene_baseline.yaml が存在し lights に led_bottom_*/right_*/left_* が + 命名規則どおりに揃っていること + - sm-render が PATH に通っていること + - output/baseline/luminance.npy が全 ON ベースラインとして存在すること + +出力: + - output/led_group_sanity/{bottom,right,left}/luminance.npy + - output/led_group_sanity/{bottom,right,left}/render.png + - configs/_tmp_sanity/scene_{group}.yaml (一時 YAML 残置) +""" + +from __future__ import annotations + +import subprocess +import sys +import time +from pathlib import Path + +import yaml + +# --------------------------------------------------------------------------- +# 定数 +# --------------------------------------------------------------------------- + +ROOT = Path(__file__).resolve().parents[1] + +TEMPLATE_YAML = ROOT / "configs" / "scene_baseline.yaml" +TMP_DIR = ROOT / "configs" / "_tmp_sanity" +OUTPUT_BASE = ROOT / "output" / "led_group_sanity" + +GROUPS = ["bottom", "right", "left"] + + +def _generate_tmp_yaml(group: str) -> Path: + """指定グループの LED だけを残した一時 YAML を生成する. + + Args: + group: "bottom" / "right" / "left" のいずれか. + + Returns: + 生成した一時 YAML のパス. + """ + TMP_DIR.mkdir(parents=True, exist_ok=True) + + with TEMPLATE_YAML.open(encoding="utf-8") as f: + data = yaml.safe_load(f) + + # geometry.meshes の STL 相対パスを絶対化(_tmp_sanity からも解決できるように) + template_base = TEMPLATE_YAML.parent + for item in data.get("geometry", {}).get("meshes", []): + rel_path = item.get("path") + if rel_path and not Path(rel_path).is_absolute(): + item["path"] = str((template_base / rel_path).resolve()) + + # lights を指定グループのみに絞り込む + prefix = f"led_{group}_" + data["lights"] = [light for light in data["lights"] if light["id"].startswith(prefix)] + + # 識別用の name を付与 + data["name"] = f"sanity_led_{group}" + + tmp_path = TMP_DIR / f"scene_{group}.yaml" + with tmp_path.open("w", encoding="utf-8") as f: + yaml.dump(data, f, allow_unicode=True, default_flow_style=False, sort_keys=False) + + return tmp_path + + +def _run_render(tmp_yaml: Path, output_dir: Path, group: str) -> int: + """sm-render を実行して終了コードを返す. + + Args: + tmp_yaml: 一時 YAML のパス. + output_dir: 出力ディレクトリ. + group: ジョブ識別子(ログ用). + + Returns: + 終了コード(0=成功). + """ + cmd = ["sm-render", "--scene", str(tmp_yaml), "--output", str(output_dir)] + print(f" [run] {' '.join(cmd)}") + result = subprocess.run(cmd, check=False) # noqa: S603 + if result.returncode != 0: + msg = f" [ERROR] sm-render ({group}) failed (returncode={result.returncode})" + print(msg, file=sys.stderr) + return result.returncode + + +def main() -> int: + """sanity check のメイン処理. + + Returns: + 終了コード(全ジョブ成功なら 0,失敗があれば 1). + """ + if not TEMPLATE_YAML.exists(): + print(f"エラー: テンプレート YAML が見つかりません: {TEMPLATE_YAML}", file=sys.stderr) + return 1 + + OUTPUT_BASE.mkdir(parents=True, exist_ok=True) + + print(f"=== LED グループ別 sanity check: {len(GROUPS)} jobs ===") + print(f" groups: {GROUPS}") + print(f" output: {OUTPUT_BASE}") + print() + + failed: list[str] = [] + total_start = time.time() + + for idx, group in enumerate(GROUPS, start=1): + print(f"--- Job {idx}/{len(GROUPS)}: {group} ---") + job_start = time.time() + + tmp_yaml = _generate_tmp_yaml(group) + print(f" tmp YAML: {tmp_yaml}") + + output_dir = OUTPUT_BASE / group + if _run_render(tmp_yaml, output_dir, group) != 0: + failed.append(group) + print(f" -> スキップ: render 失敗\n") + continue + + elapsed = time.time() - job_start + print(f" -> 完了 ({elapsed:.1f}s)\n") + + total_elapsed = time.time() - total_start + print(f"=== 完了: {len(GROUPS) - len(failed)}/{len(GROUPS)} 成功, 総時間 {total_elapsed:.1f}s ===") + + if failed: + print(f"失敗ジョブ: {failed}", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main())