diff --git a/CLAUDE.md b/CLAUDE.md index 2739976..4939702 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,7 +23,7 @@ ## 開発進捗 -最新: フェーズ 7(実装)— 幾何レバーの探索として **[TEST_15](docs/06_TEST/TEST_15_バッフル導入と球半径スイープ.md) バッフル導入と球半径スイープ**を実施.LED を評価板と反対側の**壁面に密着**させて評価板へ向け,直前のバッフルで直接光を遮る構成を新設(遮蔽は指標ではなく**レイキャストで直接検証**=55,473 本が全条件 100% 遮蔽).同じ r=100 で構造 CoV 0.0579→0.0312.**当初「球を大きくすると悪化」という想定と逆の結果が出た**が,原因はロフト接合の幾何だった.**効き目の序列は 板直下の「喉」> 接合の相似・G2 化 > 球半径そのもの**.リムで壁を板に垂直に立ち上げると喉ができ外側の落ち込みを支配する.ロフトを 2 次元パラメトリック化して**リム接線を半径方向(水平)**に取り喉を消すと,外側の落ち込みは r=250 で 17.97%→**2.27%**,構造 CoV **0.0145**・輪帯 min/max **0.944**=出発点(旧 axial_wall r=100)の **4.0 倍**.内側・外側とも半径で単調改善し頭打ち未到達(ただし r≲95 では逆転).**方法論**: 絶対輝度が 1/r² で落ちるため生の CoV は MC ノイズ支配で読めない.**同心円プロファイルの方位平均**で比較し,輝度マップは**20 分割の輪帯平均の最大値**で正規化する(画素最大値は firefly に張り付く).**未実施**: リム張り出し量の最適化・`similar_g2` の残り 7 半径・ノイズフロア.詳細は [docs/PROGRESS.md](docs/PROGRESS.md). +最新: フェーズ 7(実装)— **[TEST_16](docs/06_TEST/TEST_16_立方体キャビティ導入と球との比較.md) 立方体キャビティ導入と球との比較**を完了(全 9 条件).TEST_15 の序列「喉 > 接合 > サイズ」を受け,ロフト・接合・喉が構造的に存在しない**立方体キャビティ**(内寸 L・board_baffle 構成据え置き)を L=120〜360mm の 9 条件でスイープし,球 `similar_radial` と**内面積を横軸に**比較.**同じ内面積なら立方体が球より均一**(構造 CoV で球の 0.62〜0.89 倍,L=120 のみ互角.比は L に単調に開き交差点は内面積 10 万 mm² 前後)で,全指標が L に厳密単調改善・頭打ち未到達.L=360 は構造 CoV **0.0090**・輪帯 min/max **0.964**=全条件を通じた最良.同内面積で深さも約 3 割浅く,**設計探索の主軸は立方体系へ**.ムラの「形」は立方体でも同心円ドームのままで,四隅の写り込みは方位フーリエ **m=4 符号付き係数 a4**(b4 をノイズ対照に併用)で全条件 \|a4\| ≤ 0.26%(系統誤差床 ~0.1%)=**実質ゼロ・フィレット不要**(有意なのは小サイズの逆符号=辺余白の減光のみ.L=120→160 で単調減衰).**環境**: mitsuba は Smart App Control の DLL ブロック(vtk 9.6.1 と同型)により **3.7.1 に固定**.混在の妥当性は全テスト+L=120 アンカー再レンダ(差 1.17σ 以内)で検証済み,テストは `python -m pytest` で実行する(ENV_04 参照).**未実施**: フラット天井版(rim_z=−4.5)・直方体 L×L×D・ノイズフロア.詳細は [docs/PROGRESS.md](docs/PROGRESS.md). 実装済みモジュール: - `src/sm_tias_lightsim/uniformity.py` — Rec.709 輝度変換 + CoV/std/max-min(SmTIAS-Evaluation コピー) @@ -148,3 +148,4 @@ - 物理モデル健全性の一括監査: docs/06_TEST/TEST_13_物理モデル健全性の一括監査.md(「無警告で結果だけが歪む」故障モードの横断監査.解析解による絶対放射量検証・法線監査・打ち切りバイアス・常設した再発防止テスト) - 材質パラメータ再スイープと残差の構造分解: docs/06_TEST/TEST_14_材質パラメータ再スイープと残差の構造分解.md(面法線修正後の ρ×ω×α 全 60 条件スイープ.ρ ≈ 0.81 純 diffuse の確定と,材質では直せない傾き成分の分離) - バッフル導入と球半径スイープ: docs/06_TEST/TEST_15_バッフル導入と球半径スイープ.md(LED 壁面密着+バッフルで直接光を遮る新構成と,球内半径 88〜250mm のスイープ.ロフト接合の 2 段階の作り直しで「大きいほど悪化」が逆転し,板直下の喉が最大のレバーと判明した経緯) +- 立方体キャビティ導入と球との比較: docs/06_TEST/TEST_16_立方体キャビティ導入と球との比較.md(ロフト・接合・喉の無い立方体を L=120〜360mm でスイープし内面積で球と対応づけ.同面積なら立方体が一貫して均一で,四隅の写り込みは m=4 方位フーリエで検出限界以下と確認) diff --git "a/docs/02_ENV/ENV_04_\351\226\213\347\231\272\343\202\263\343\203\236\343\203\263\343\203\211.md" "b/docs/02_ENV/ENV_04_\351\226\213\347\231\272\343\202\263\343\203\236\343\203\263\343\203\211.md" index 27898c3..5746689 100644 --- "a/docs/02_ENV/ENV_04_\351\226\213\347\231\272\343\202\263\343\203\236\343\203\263\343\203\211.md" +++ "b/docs/02_ENV/ENV_04_\351\226\213\347\231\272\343\202\263\343\203\236\343\203\263\343\203\211.md" @@ -46,6 +46,14 @@ > バージョンを上げるときは必ず `sm-visualize` の起動確認を行うこと. > `tests/visualize/test_vtk_import_guard.py` が通常実行で import 可否を常時検査している. +> ⚠ `mitsuba` は `pyproject.toml` で **3.7.1 に固定**している.3.8.0 では Smart App Control が +> 同梱の `IlmThread-mitsuba.dll` 1 個をブロックし mitsuba が import 不能=レンダが全滅する +> (2026-09-01 実測.2 週間動いた後にクラウド側の判定だけが反転した. +> [TEST_16](../06_TEST/TEST_16_立方体キャビティ導入と球との比較.md) 失敗パターン参照). +> バージョンを上げるときは import 確認と `uv run python -m pytest -m ""`(絶対放射量検証込み)を +> 必ず通し,過去レンダとの比較が続く実験中ならアンカー再レンダで等価性を確認すること. +> なお `pytest.exe` ランチャも同時にブロックされたため,テスト実行は `uv run python -m pytest` を使う. + ## Lint / フォーマット (Lint & Format) 規約の詳細は `.claude/rules/coding-style.md` を参照.ruff を唯一の lint / format ツールとする. 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 f7f4407..418b91f 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" @@ -87,6 +87,9 @@ | `output/sphere_radius_sweep/board_baffle_similar_g2/` (3 ディレクトリ) | TEST_15 | 同上の `--cavity similar_g2` 版(キャビティ全体を半径に相似 center_z=-0.92r・cut_z=-0.40r,ロフトを 5 次 Hermite にして接合部で曲率まで球に一致.r ∈ {100, 170, 250}mm) | 同上 | 外側の落ち込みが半減(r=250 で 18.0%→8.8%)し,輪帯 min/max が半径とともに**改善**に転じる(0.852→0.883→0.891) — [TEST_15](TEST_15_バッフル導入と球半径スイープ.md) 参照 | **接合を自然にすると傾向が逆転**.r=250 の 0.891 は全条件で最良.残り 7 半径は未実行 | | `output/sphere_radius_sweep/board_baffle_similar_radial/` (10 ディレクトリ) | TEST_15 | 同上の `--cavity similar_radial` 版(相似スケールのまま**ロフトのリム接線を半径方向(水平)**に取り,板直下の「喉」を無くした形.ロフトを (ρ, z) の 2 次元パラメトリック Hermite 化して実現.r ∈ 全 10 半径) | `configs/eval_baseline_dng_target_shape.yaml` | 外側の落ち込みが r=250 で 2.27%(fixed_cut 17.97% の 8 分の 1).輪帯 min/max は r=95 以降単調改善し r=250 で **0.944**,構造 CoV **0.0145** — [TEST_15](TEST_15_バッフル導入と球半径スイープ.md) 参照 | **全条件で最良**.旧 axial_wall r=100(0.0579 / 0.797)の 4.0 倍.内側・外側とも半径で改善し頭打ち未到達.ただし r≲95 では fixed_cut に負ける | | `output/sphere_radius_sweep/*/summary.csv`・`{sweep_overview,radial_comparison}.png`・`cavity_comparison.png`・`cavity_{design,effect}_comparison.png`・`rim_tangent_comparison.png`・`geometry_check_*.png`・`cutaway_*.png` | TEST_15 | — | — | 条件別の集約 CSV(幾何・生 mean・各指標)/輝度マップ一覧 + CoV 分解カーブ/同心円プロファイル比較/接合設計の断面 before-after/幾何確認図・半割り断面 | `scripts/build_radius_sweep_visual.py`・`build_radial_comparison.py`・`build_cavity_comparison.py`・`build_radius_geometry_figure.py`・`show_scene_cutaway.py` の出力.輝度マップは 20 分割の輪帯平均の最大値を 1.0 に正規化(画素最大値は firefly に張り付くため).レンダ前 3D 確認(CLAUDE.md 必須ルール)の記録を兼ねる | +| `output/cube_size_sweep/board_baffle/` (9 ディレクトリ) | TEST_16 | `configs/_tmp_cube_size/scene_board_baffle_L*.yaml`(`scripts/sweep_cube_size.py` が生成.筐体を立方体(内寸 L・天井 z=0・底面 z=-L)に変えた board_baffle 構成.LED は底面中心に密着し評価板向き,バッフルは球版と同じ閉形式.L ∈ {120〜360}mm の全 9 条件.材質・spp=1024・d=16 は据え置き.**先行 5 条件は mitsuba 3.8.0・後続 4 条件は 3.7.1**=SAC ブロックによる固定,アンカー検証済み) | `configs/eval_baseline_dng_target_shape.yaml` | L=360 で構造 CoV **0.0090**・輪帯 min/max **0.964**=全条件を通じた最良.同一内面積の球(対数内挿)に対し構造 CoV 比 0.62〜1.05・全 9 条件で単調改善 — [TEST_16](TEST_16_立方体キャビティ導入と球との比較.md) 参照 | **同じ内面積なら立方体が球より均一**(L=120 のみ互角)を実証.L=280 で球の全 10 条件の最良(r=250)を超える.遮蔽は 55,473 本 100%・法線逸脱 ≤0.06° を事前検証済み | +| `output/cube_size_sweep/_anchor_check_mitsuba371/L120/` | TEST_16 | `configs/_tmp_cube_size/scene_board_baffle_L120.yaml`(本体スイープと同一.mitsuba **3.7.1** でレンダ) | 同上 | 3.8.0 版と比較して構造 CoV 一致(0.0379)・輪帯 min/max 差 +0.0002・輪帯プロファイル最大差 1.17σ | mitsuba 3.8.0→3.7.1 固定(SAC が `IlmThread-mitsuba.dll` をブロック)の等価性検証アンカー.バージョン混在の妥当性の根拠 | +| `output/cube_size_sweep/board_baffle/{summary.csv,cube_vs_sphere.png,fourfold_component.png}`・`output/cube_size_sweep/_placement_check/cutaway_L*.png` | TEST_16 | — | — | 条件別集約 CSV/球との比較図(輝度マップ 5 枚 + 輪帯プロファイル + 内面積軸の指標カーブ + 直接ペア 3 組)/m=4(cos 4φ)成分の定量図/レンダ前 3D 確認の半割り断面 5 枚 | `scripts/build_cube_sphere_comparison.py`・`analyze_fourfold_component.py`・`show_scene_cutaway.py` の出力.m=4 は全条件 \|a4\| ≤ 0.26%(系統誤差床 ~0.1%)=四隅の写り込みは実質ゼロ.レンダ前 3D 確認(CLAUDE.md 必須ルール)の記録を兼ねる | | `output/_slide_panels/sphere_loft_disk_before_after.png`・`normal_artifact_explainer.png` | TEST_12 | — | — | 球ロフト円形光源の修正前後 1×2 比較(相対輝度・共通スケール)/法線アーティファクトの機構説明図(模式図+実物メッシュの補間法線再現+修正前マップの 4 パネル) | 発表スライド素材(アドホック生成.修正前後比較は CoV 0.1205→0.0628 の視覚版) | ### sweep_summary.csv の値(参考、TEST_01 line と一致確認用) diff --git "a/docs/06_TEST/TEST_16_\347\253\213\346\226\271\344\275\223\343\202\255\343\203\243\343\203\223\343\203\206\343\202\243\345\260\216\345\205\245\343\201\250\347\220\203\343\201\250\343\201\256\346\257\224\350\274\203.md" "b/docs/06_TEST/TEST_16_\347\253\213\346\226\271\344\275\223\343\202\255\343\203\243\343\203\223\343\203\206\343\202\243\345\260\216\345\205\245\343\201\250\347\220\203\343\201\250\343\201\256\346\257\224\350\274\203.md" new file mode 100644 index 0000000..cacbaa9 --- /dev/null +++ "b/docs/06_TEST/TEST_16_\347\253\213\346\226\271\344\275\223\343\202\255\343\203\243\343\203\223\343\203\206\343\202\243\345\260\216\345\205\245\343\201\250\347\220\203\343\201\250\343\201\256\346\257\224\350\274\203.md" @@ -0,0 +1,289 @@ +# 立方体キャビティ導入と球との比較 (Cube Cavity Introduction and Comparison with Sphere) + +球ではなく**立方体**のキャビティを新設し,一辺 L を 120〜360mm の全 9 条件でスイープして, +球(TEST_15 の最良設計 `similar_radial`)と**内面積で対応づけて**比較した(2026-08-31〜09-01). +「球のほうが理想積分球だから有利」という素朴な予想は成り立たず,**同じ内面積なら立方体が +一貫して球より均一**(構造 CoV で球の 0.62〜0.89 倍,L=120 のみ互角)で,L=280 の時点で +球の全 10 条件の最良値を超えた.両指標とも L に対し厳密に単調改善で頭打ち未到達. +懸念だった「四隅の写り込み」は方位フーリエ m=4 成分で直接定量し, +検出限界(系統誤差床 ±0.1%)レベルでほぼゼロであることを確認した. + +関連: + +- 前提となる結論と方法論: [TEST_15_バッフル導入と球半径スイープ](TEST_15_バッフル導入と球半径スイープ.md) +- 球実験の起点: [TEST_11_球体筐体実験](TEST_11_球体筐体実験.md) +- 実行記録: [TEST_02_シミュレーション実行記録](TEST_02_シミュレーション実行記録.md) + +## 背景・動機 (Motivation) + +TEST_15 の結論は「効き目の序列は 板直下の喉 > 接合の相似・G2 化 > 球半径そのもの」だった. +つまり律速はキャビティの大域形状ではなく,評価板まわりの局所幾何である.ここから仮説を立てた. + +**立方体にはロフトも接合も存在しない.** 評価板は矩形,立方体の上面も矩形なので,板は上面の +開口にそのまま嵌まる.TEST_15 で苦労した「矩形→円の遷移」「接合の曲率不連続」「板直下の喉」と +いう問題群が**構造的に消える**.しかも立方体の上面は板の縁から外へ広がる水平な天井そのもので, +TEST_15 最良の「リム水平出し」(`similar_radial`)が目指した形と一致する. + +一方で立方体は「ランバート光源を壁に置くと壁面照度が一様になる」という球だけが持つ性質を失う. +どちらが勝つかは実際に回さないと分からない——これが実験の主眼である.製作面でも立方体は +3D プリント・組立が容易なはずで,実機化の観点でも意味がある. + +## 実験条件 (Setup) + +材質・センサ・レンダ設定・LED の面積と radiance・評価板の位置は TEST_15 から一切変更していない. +変えたのは筐体メッシュ(球ロフト → 立方体)と,それに従属する LED の Z・バッフル径だけである. + +| 項目 | 値 | +| --- | --- | +| レンダ | spp=1024 / max_depth=16 / 512² / llvm_ad_rgb / path / orthographic (50×52) | +| 内壁材質 | blendbsdf ρ=0.85 / ω=0.05 / α=0.1(TEST_11〜15 と直接比較するため校正値 ρ≈0.81 は不使用) | +| 光源 | 円板 半径 1.7662mm(面積 9.8mm² 保存)・`board_baffle` 構成(底面中心に密着・評価板向き +Z) | +| バッフル | LED の 10mm 手前・板の全点から LED を隠す最小半径 ×1.05・厚み 1mm(TEST_15 と同じ閉形式) | +| 評価 | `configs/eval_baseline_dng_target_shape.yaml`(linear + mean アンカー 207.82) | +| 評価板 | 99×102mm・キャビティ側の面は z=−4.5mm・**全条件で不変** | + +### 立方体の幾何 (Cube Geometry) + +`scripts/generate_cube_housing.py` で閉ソリッドをスクリプト生成する.上から順に, + +1. リム矩形(板輪郭から 1.5mm 内側・z=0)=球版と同じく板の厚み内に食い込んでシール +2. 天井: z=0 の水平面.リム矩形から内壁正方形(L×L)まで広がる +3. 側壁 4 枚: z=0 から z=−L +4. 底面: z=−L.中心に LED(壁面から 0.5mm 浮かせ・+Z 向き),その 10mm 上にバッフル + +天井 z=0 と板照明面 z=−4.5 の間の 4.5mm の窪みは球スイープと同一条件で残した(交絡なし). +球版と違い**全ての面が平面なので三角形分割は物理(法線・交差)に影響しない**.周方向サンプルは +半ステップずらしてメッシュエッジが XZ / YZ 対称面に乗らないようにし(TEST_15 のすり抜け対策), +矩形の隅には厳密角の頂点を挿入して面取りを防いだ.外面の天板は z=+3 に持ち上げ,内面天井との +共面(ゼロ厚)を避けている. + +L の下限は 102mm(評価板 99×102mm が上面に収まる限界).天井余白を考慮し 120mm から振った. +実行は 2 段階で,まず傾向を安く見るため両端+中間の 5 条件(120/160/200/280/360), +傾向が確認できた後に残り 4 条件(140/180/240/320)を埋めた(ユーザー指示). + +| L [mm] | LED_z | 内面積 [mm²] | 開口比 | バッフル R | +| --- | --- | --- | --- | --- | +| 120 | −119.5 | 86,994 | 11.61% | 8.18mm | +| 140 | −139.5 | 118,194 | 8.54% | 7.24mm | +| 160 | −159.5 | 154,194 | 6.55% | 6.55mm | +| 180 | −179.5 | 194,994 | 5.18% | 6.01mm | +| 200 | −199.5 | 240,594 | 4.20% | 5.59mm | +| 240 | −239.5 | 346,194 | 2.92% | 4.95mm | +| 280 | −279.5 | 470,994 | 2.14% | 4.50mm | +| 320 | −319.5 | 614,994 | 1.64% | 4.16mm | +| 360 | −359.5 | 778,194 | 1.30% | 3.90mm | + +**レンダラのバージョンについて**: 先行 5 条件は mitsuba 3.8.0,後続 4 条件は 3.7.1 で +レンダしている.2 段階目の直前に Windows Smart App Control が 3.8.0 同梱の +`IlmThread-mitsuba.dll` 1 個をレピュテーション判定でブロックし始め(vtk 9.6.1 と同型= +[TEST_13](TEST_13_物理モデル健全性の一括監査.md) 検査 8.ファイルは 2 週間無変更のまま +クラウド側の判定だけが反転),mitsuba が import 不能になったため 3.7.1 に固定した +(`pyproject.toml` に理由コメント).混在の妥当性は 2 通りで検証した.全テスト +(絶対放射量の解析解検証込み)1043 件が 3.7.1 で全通過,および **L=120 のアンカー再レンダ**で +3.8.0 と比較して構造 CoV が小数 4 桁まで一致(0.0379)・輪帯 min/max 差 +0.0002・ +輪帯プロファイルの最大差 1.17σ(中央値 0.46σ)=統計的に区別不能. + +### 事前検証 (Pre-render Verification) + +レンダ前に TEST_13 / TEST_15 で常設した検証を一通り当てた. + +- watertight: 全 9 筐体+バッフルで確認(生成時に強制チェック) +- 3D 配置確認: 半割り断面をユーザーに提示し OK を得てからレンダ(CLAUDE.md 必須ルール. + 後続 4 条件は承認済みパラメトリック設計の中間サイズとして同様に断面生成・確認) +- 遮蔽: 板全面 41×41 点 × LED 33 点 = 55,473 本のレイキャストが**全 9 条件で 100% 遮蔽**(最小余裕 9.6〜10.5mm) +- 法線監査: `audit_scene_normals` で裏向き面 0・シェーディング法線逸脱 ≤0.06°(L=120 / 360 の 2 条件) + +### 「同じ大きさ」の定義 (Size-matching Definition) + +球と立方体の比較では「同じ大きさ」の定義が自明でない.多重反射の理論では放射輝度は総内面積と +開口比で決まるため,**内面積を揃える**解釈を採用した(積分球としての比較).スイープ自体は +キリの良い L で振り,比較は内面積を横軸にした曲線の重ね描き+対数内挿で行う. +なお内面積を揃えると立方体は球より**深さが約 3 割浅い**(球は中心が板から −0.92r 下にあるため +縦に長い.例: 球 r=250 は深さ 480mm,同内面積の立方体は 341mm). + +## 結果 1 — 立方体でもムラの「形」は同心円ドームのまま (Shape Stays Concentric) + +立方体化しても輝度分布は球と同じ**同心円状のドーム**で,四角い模様は現れない. +極座標分解では L=120 で動径分散 118 に対し方位分散 5.96(angular_ratio 0.048)=構造の +95% 以上が同心円成分で,球(r=88 で 116 vs 4.0)と同じ内訳である. + +理由は 3 つ.① `board_baffle` 構成の一次照射(バッフル裏 → 床の明るいリング)が軸対称で, +板の照度は cos 重みで真下方向が支配するため,1 バウンス目の分布が筐体形状によらず軸対称になる. +② ρ=0.85 の多重拡散反射が 2 バウンス目以降の放射を等方化し,壁の形の情報を洗い流す. +③ 仮に板縁基準の成分があっても,その等高線が四角く見えるのは四隅の近傍だけである. + +これは TEST_15 の「効くのは板直下の局所幾何であって大域形状ではない」の裏返しの確認であり, +**輪帯(方位平均)プロファイルによる比較が立方体に対しても正当**であることを意味する. + +## 結果 2 — 同一内面積で立方体が球を上回る (Cube Beats Sphere at Equal Area) + +立方体 9 条件の実測(構造 CoV / 輪帯 min/max は TEST_15 と同一の定義・スクリプトで算出): + +| L [mm] | 生 CoV | 構造 CoV | 輪帯 min/max | 中心/周辺比 | 勾配 | 生 mean | +| --- | --- | --- | --- | --- | --- | --- | +| 120 | 0.0802 | 0.0379 | 0.858 | 1.105 | 9.49% | 1.336 | +| 140 | 0.0831 | 0.0280 | 0.894 | 1.078 | 7.20% | 1.022 | +| 160 | 0.0842 | 0.0220 | 0.919 | 1.060 | 5.68% | 0.801 | +| 180 | 0.0916 | 0.0184 | 0.930 | 1.050 | 4.74% | 0.643 | +| 200 | 0.0939 | 0.0163 | 0.934 | 1.044 | 4.19% | 0.525 | +| 240 | 0.1077 | 0.0135 | 0.945 | 1.035 | 3.41% | 0.368 | +| 280 | 0.1143 | 0.0109 | 0.960 | 1.026 | 2.55% | 0.272 | +| 320 | 0.1296 | 0.0098 | 0.961 | 1.022 | 2.13% | 0.209 | +| 360 | 0.1355 | **0.0090** | **0.964** | 1.018 | 1.76% | 0.165 | + +構造 CoV・輪帯 min/max・中心/周辺比・勾配のいずれも **L に対して厳密に単調改善**で, +9 本の輪帯プロファイルは交差せずサイズ順に並ぶ(球 `similar_radial` と同じ素直な効き方). + +生 CoV が L とともに増えるのは輝度低下(おおむね 1/面積)による MC ノイズで,TEST_15 の作法 +どおり構造成分で比較する.内面積を横軸に球 `similar_radial`(10 条件)と重ね,対数内挿で +同一面積の値を比べると: + +| L [mm] | 構造 CoV 立方体 | 球(内挿) | 比 | 輪帯 min/max 立方体 | 球(内挿) | +| --- | --- | --- | --- | --- | --- | +| 120 | 0.0379 | 0.0359 | 1.05 | 0.858 | 0.853 | +| 140 | 0.0280 | 0.0313 | 0.89 | 0.894 | 0.873 | +| 160 | 0.0220 | 0.0271 | 0.81 | 0.919 | 0.891 | +| 180 | 0.0184 | 0.0240 | 0.77 | 0.930 | 0.901 | +| 200 | 0.0163 | 0.0217 | 0.75 | 0.934 | 0.916 | +| 240 | 0.0135 | 0.0189 | 0.72 | 0.945 | 0.921 | +| 280 | 0.0109 | 0.0166 | **0.66** | 0.960 | 0.939 | +| 320 | 0.0098 | 0.0152 | 0.65 | 0.961 | 0.944 | +| 360 | 0.0090 | 0.0145 | **0.62** | 0.964 | 0.947 | + +- 最小サイズ(L=120 ↔ 球 r≈88)ではほぼ互角.**L=140 以上で立方体が一貫して勝ち**, + 比は L に対して単調に開く(構造 CoV で球の 0.62〜0.89 倍).交差点は内面積 10 万 mm² 前後 +- **L=280 の立方体(0.0109)が球の全 10 条件の最良値(r=250 の 0.0145)を既に下回る**. + L=360 は 0.0090 / 0.964 で本プロジェクトの全条件を通じた最良 +- 輪帯プロファイルの標準誤差帯に対して差は十分大きく,内面積の近い直接ペア + (L=120↔r=88 / L=200↔r=150 / L=360↔r=250)でもどの距離帯でも立方体の曲線が上にある +- 内側・外側とも L に対して単調改善で頭打ち未到達(球 `similar_radial` と同じ傾向) + +つまり **ムラの「形」は球と同じで,「量」(ドームの深さ)が 2〜4 割浅い**.TEST_15 で球側に +施した改良(喉の除去・リム水平出し)は,立方体では最初から構造的に備わっている形であり, +それを上回った事実は「球固有の等照度性より,板まわりの局所幾何の素直さが効く」ことを示す. + +## 結果 3 — 四隅の痕跡は検出限界以下 (No Four-fold Imprint) + +立方体固有の懸念「四隅が暗くなる」を,輪帯ごとの方位プロファイルの **cos 4φ 成分(m=4)**で +直接定量した(`scripts/analyze_fourfold_component.py`).四回対称の構造は対称性から m=4 にしか +現れず,板は鏡映対称なので信号は板軸に揃った符号付き係数 a4(正=四隅が暗い)だけに出る. +直交成分 b4 は対称性からゼロのはずで,**生きたノイズ対照**になる.球側の a4 は評価系 +(長方形 ROI・楕円ビン・ノイズ)が作る背景値を与える. + +| ペア(同内面積) | 立方体 a4 | 球 a4 | 差 | +| --- | --- | --- | --- | +| L=120 vs r=88 | **−0.264±0.027%** | −0.031% | −0.233% | +| L=140 vs r=100 | **−0.212±0.030%** | +0.020% | −0.231% | +| L=160 vs r=110 | −0.058% | −0.032% | −0.026% | +| L=180 vs r=135 | −0.074% | +0.012% | −0.086% | +| L=200 vs r=150 | +0.017% | −0.037% | +0.054% | +| L=240 vs r=170 | −0.082% | −0.049% | −0.033% | +| L=280 vs r=200 | +0.105% | −0.073% | +0.177% | +| L=320 vs r=250 | −0.037% | −0.020% | −0.017% | +| L=360 vs r=250 | −0.144% | −0.020% | −0.124% | + +- **振幅が極小**.全条件で |a4| ≤ 0.26%.輪帯プロファイルの落ち込み(4〜15%)の 1/30〜1/50 で, + 均一性への実害としては無視できる +- **「四隅が暗い」符号(a4 > 0)の一貫した信号は無い**.符号はサイズに対してばらつき, + 輪帯プロファイルにも外側へ向かう成長がない +- **ノイズ対照 b4 が系統誤差床 ~0.1% を示した**(球でも +0.09% 出る).MC ノイズ伝播だけの + 形式的な σ は有意性を過大評価するので,|a4| ≲ 0.1% はゼロと読むべきである +- 床を明確に超えるのは **L=120 の −0.264% と L=140 の −0.212% だけで,どちらも + 「四隅が暗い」の逆**(同じ楕円距離なら辺の中央方向が暗い).小サイズでは板縁から内壁までの + 余白が辺中央方向で最小になり(L=120 で 9〜12mm,対角方向は約 14mm),壁・天井の接合が + 最も近い辺中央方向が先に減光する=TEST_15 の「板直下の局所幾何」の名残であり,隅の光トラップ + ではない.振幅が L=120→140→160 で −0.26%→−0.21%→−0.06% と余白の拡大とともに減衰することも + この解釈と整合する + +結論: **立方体化による四回対称ムラは ±0.1%(系統誤差床)〜最大 0.26% で,角のフィレット等の +対策は不要**.小型化の下限を決める要因は隅ではなく辺の余白である. + +## 結論 (Conclusions) + +- **同じ内面積なら立方体が球より均一**.構造 CoV で球の 0.62〜0.89 倍(L=120 のみ互角)で + 比は L に対し単調に開き,L=280 で球の全条件最良を超え,L=360(0.0090 / 0.964)が全条件を + 通じた最良.全 9 条件で単調改善・頭打ち未到達 +- **ムラの「形」は球と同一の同心円ドーム**で,立方体の四角さは写り込まない(m=4 成分は + 検出限界レベル).差が出るのはドームの「深さ」だけ +- TEST_15 の序列「喉 > 接合 > サイズ」の帰結として自然な結果.立方体はロフト・接合・喉が + 構造的に存在せず,最良設計『リム水平出し』の形を最初から備えている +- **実用面でも立方体が有利**.同内面積で深さが約 3 割浅く(球 r=250: 480mm vs L=341mm), + 平面のみで製作も容易.設計探索の主軸は今後立方体系に移してよい +- 絶対輝度は面積にほぼ反比例して落ちる(L=360 は L=120 の 1/8).均一性・寸法・明るさの + 三つ巴の trade-off は球と同様に残る + +## 限界・注意点 (Limitations) + +- **レンダラのバージョンが混在**(先行 5 条件 = mitsuba 3.8.0,後続 4 条件 = 3.7.1). + アンカー再レンダで統計的に区別不能(上記 Setup 参照)を確認済みだが,bit 一致ではない +- **全条件 1 シードのみ**.ノイズフロア(seed 違い)は球スイープから引き続き未測定 +- **m=4 解析は楕円距離 d ≤ 0.75 の輪帯のみ**.それより外側(四隅にしか画素が無い帯)は方位 + カバレッジが欠けてフーリエが偽信号を作るため除外した.四隅そのものの落ち込みは輪帯 + プロファイルの動径成分(min/max)に計上されており,取りこぼしではない +- 天井の 4.5mm の窪み(板周りの段差)は球と条件を揃えるため残した.立方体では完全に平らな + 天井(rim_z=−4.5)にでき,未検証の改善余地がある +- 材質は校正値(ρ≈0.81 純 diffuse)ではなく旧条件(ρ=0.85 blendbsdf).TEST_11〜15 と直接 + 比較するための選択で,絶対値の実機予測には引き直しが必要 +- `board_baffle` は実機の LED 配置(下/左/右 3 基板)とは無関係な理想化である点も球と同じ + +## 失敗パターン・方法論の学び (Pitfalls and Methodology) + +- **楕円距離ビンの外側輪帯に方位フーリエを当ててはならない**.d > 0.707(辺中点)の輪帯は + 四隅方向にしか画素が無く,カバレッジの欠けが m=0(平均)を m=4 に漏れ込ませて巨大な偽信号を + 作る.全セクタに画素がある輪帯だけを使うこと +- **対称性からゼロのはずの直交成分(b4)をノイズ対照として同時に計算する**と,MC ノイズ以外の + 系統誤差床(今回 ~0.1%)が実測でき,形式的な σ による有意性の過大評価を防げる +- **`angular_ratio` の跳ね上がり(L=360 で 0.71)は今回も見かけ**.分母の動径分散が 118→7.1 に + 潰れたためで,方位分散の絶対値(6→17,mean 比 ~2%)で読むこと(TEST_15 の再確認) +- 立方体の幾何検証はすべて一発で通った(watertight・遮蔽 100%・法線逸脱 0.06°).球版で + 確立した対策(対称面回避の半ステップずらし・バッフル半セクタ回転・板面 z=−4.5 基準)を + 最初から織り込んだため.**幾何を単純化すると検証も単純になる**ことの好例 +- **Smart App Control のレピュテーション判定は無変更のファイルに対しても一晩で反転しうる**. + mitsuba 3.8.0 の DLL は 2 週間問題なく動いた後に突然ブロックされた(uv が生成した + `pytest.exe` ランチャも同時にブロック.こちらは `python -m pytest` で回避).犯人の特定は + `LoadLibraryExW` を `DONT_RESOLVE_DLL_REFERENCES` 付きで全 DLL に当て,WinError 4551 + (ポリシーブロック)と 126(依存欠落の巻き添え)を切り分けるのが確実.同一バージョンの + 再インストールはハッシュが同じため無効で,対処は別バージョンへの固定(vtk 9.4.2 と同じ). + **レンダラの途中変更はアンカー再レンダで等価性を確認してから**混在させること + +## 次にやること (Next Steps) + +1. フラット天井版(rim_z=−4.5,板周りの段差を消す)の追試.立方体で初めて可能になる形 +2. 深さを別軸にした直方体スイープ(L×L×D).内面積を揃えると立方体は球より浅い事実の延長で, + 実機の厚み制約に直結する +3. ノイズフロア(seed 違い 2 本)と,バッフル構成での spp 増(球スイープと共通の宿題) +4. 実機予測に使うなら材質を TEST_14 の校正値(ρ≈0.81 純 diffuse)で引き直す + +## 再現コマンド (Reproduction) + +```bash +# 幾何生成のみ(3D 配置確認用.レンダはしない) +uv run python scripts/sweep_cube_size.py --prepare-only + +# 半割り断面(レンダ前の配置確認) +uv run python scripts/show_scene_cutaway.py --scene configs/_tmp_cube_size/scene_board_baffle_L200.yaml + +# 遮蔽の検証(評価板から LED へのレイが全て遮られることを実測) +uv run python scripts/verify_baffle_occlusion.py --scene configs/_tmp_cube_size/scene_board_baffle_L200.yaml + +# スイープ本体(全 9 条件.レンダ + 評価.既存結果は再利用される) +uv run python scripts/sweep_cube_size.py + +# 球との比較図 + 集約表(内面積で対応づけ) +uv run python scripts/build_cube_sphere_comparison.py + +# 四隅の痕跡の定量(m=4 成分) +uv run python scripts/analyze_fourfold_component.py +``` + +出力: `output/cube_size_sweep/board_baffle/`(条件別 9 ディレクトリ + `summary.csv`). +mitsuba 3.7.1↔3.8.0 のアンカー比較は `output/cube_size_sweep/_anchor_check_mitsuba371/L120/`. +比較図は `cube_vs_sphere.png`,m=4 解析は `fourfold_component.png`, +レンダ前 3D 確認は `output/cube_size_sweep/_placement_check/cutaway_L*.png`. + +## 参照 (References) + +- [TEST_15_バッフル導入と球半径スイープ](TEST_15_バッフル導入と球半径スイープ.md): `board_baffle` 構成・評価の作法・球側の全データ +- [TEST_14_材質パラメータ再スイープと残差の構造分解](TEST_14_材質パラメータ再スイープと残差の構造分解.md): 材質の確定と「形は材質で動かない」の実証 +- [TEST_02_シミュレーション実行記録](TEST_02_シミュレーション実行記録.md): 出力ディレクトリの索引 +- [SPEC_03_基準設計の物理構成](../04_SPEC/SPEC_03_基準設計の物理構成.md): 座標系と評価板の物理仕様 diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 9e4bbc8..aa7aaf9 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -85,3 +85,7 @@ - **TEST_15 バッフル導入と球半径スイープ — 接合の不自然さが「大きい球ほど悪い」の原因と判明 (2026-08-31)**: TEST_14 で「形は材質では動かない」と確定したため幾何レバーに移行.ユーザー指示で LED を評価板と反対側の**壁面に密着**させて評価板へ向け,直前のバッフルで直接光を遮る構成を新設した(バッフル径は板の全点から LED を隠す最小値の閉形式+安全率.効果は指標ではなく**レイキャストで直接検証**=板全面 41×41 点 × LED 33 点の 55,473 本が全条件 100% 遮蔽).**結果 ①**: 同じ r=100 で構造 CoV 0.0579→**0.0312**(46% 減)・輪帯 min/max 0.797→0.861・上下の傾きも消失=バッフルは有効.**結果 ②(想定と逆)**: 半径 88〜250mm の 10 条件で,輪帯 min/max は r=135 を境に悪化し r=250 は旧構成より悪い.内側(ピーク→d=0.5)は期待どおり単調改善(8.1%→3.5%)だが**外側(d=0.5→0.95)が同じ勢いで悪化**(6.4%→18.0%)して差し引きで負ける.**結果 ③**: ユーザー指摘「半径が大きいほど不自然に接合している」を数値化したところ,接続円を固定する設計では**r≥120 で球の頂点が評価板より下にもぐり**(=ほぼ閉じた玉に細い首),接合の傾きが 0.25→2.75 と 11 倍に急峻化していた.**結果 ④**: キャビティ全体を半径に相似(center_z=-0.92r・cut_z=-0.40r)させ,ロフトを 5 次 Hermite にして接合部で**曲率まで球に一致**させると(傾き 0.609 一定・曲率比 厳密に 1.00・球の頂点は常に板より上),外側の落ち込みが半減し(r=250 で 18.0%→8.8%),**輪帯 min/max が半径とともに改善に転じた**(0.852→0.883→0.891).r=250 の 0.891 は全条件で最良.内側は両設計でほぼ同一=変更が予測どおり外側だけに効いた.**方法論上の学び**: 絶対輝度が半径で 1 桁変わる(1/r²)ため生の CoV は MC ノイズ支配(ノイズが構造の 2〜3 倍)で,そのまま読むと「大きい球ほど不均一」という誤った結論になる.**同心円(輪帯)プロファイルの方位平均**ならノイズが 1/√N に落ち,標準誤差が条件間の差の 1/20〜1/100 になるので公平に比較できる(ユーザー提案).また輪帯プロファイルは ROI 平均ではなく**ピークで正規化**しないと「どれだけ落ちるか」が読み取れない.失敗パターン: ① 評価板のキャビティ側の面を z=0 と取り違え(実際は z=-4.5 の下面)→ バッフル径が過小 / ② バッフル円板の三角形ファンのエッジが XZ/YZ 平面と一致し,その平面内のレイが数値的にすり抜け(55,473 本中 4〜32 本)→ 円板を半セクタ回転して解消 / ③ `subprocess.run` が Windows 既定の cp932 で復号して読取スレッドが落ち,レンダ失敗時にエラー本文が読めない → `encoding="utf-8"` 明示.**未完**: `similar_g2` は 3 条件のみで残り 7 半径が未実行.ノイズフロア(seed 違い)も未測定.バッフル構成は MC ノイズが 3 倍になるので絶対量の議論には spp 増が必要.成果物: `output/sphere_radius_sweep/{board_baffle,board_baffle_similar_g2}/`,`scripts/sweep_sphere_radius.py`・`verify_baffle_occlusion.py`・`build_radial_comparison.py`・`build_radius_sweep_visual.py`・`build_radius_geometry_figure.py`・`show_scene_cutaway.py`,記録 [TEST_15](06_TEST/TEST_15_バッフル導入と球半径スイープ.md). - **TEST_15 続き — 板直下の「喉」が最大のレバーと判明し,出発点の 4.0 倍まで改善 (2026-08-31)**: `similar_g2`(接合の相似・G2 化)でも r=250 で外側の落ち込みが 8.75% 残っていた.断面を見たユーザーの指摘「くびれがないようにしたい」「評価板に対して垂直方向にまず伸ばしているが,水平方向に伸ばしたらくびれにならないのでは」を数値化すると,**板の直下に喉ができていた**(r=250 で板縁から 12.8mm 下でも半径が 48→49.2mm しか広がらない).原因は実装形式そのもので,ロフトを「z を等間隔に進めながら ρ(z) を決める」形で書いていたためリムの傾きが dρ/dz=0(=板に垂直)しか表現できなかった.**ロフトを (ρ, z) の 2 次元パラメトリック Hermite に変えてリム接線を半径方向(水平)に取れるようにした**(`similar_radial`).**結果**: 外側の落ち込みが r=250 で 17.97%(fixed_cut)→ 8.75%(similar_g2)→ **2.27%**,構造 CoV **0.0145**・輪帯 min/max **0.944** で全条件の最良.旧 axial_wall r=100(0.0579 / 0.797)の **4.0 倍**で,内側・外側とも半径に対し単調改善し頭打ちが見えていない.**設計上の結論**: 効き目の序列は **喉 > 接合の相似・G2 化 > 球半径そのもの**.球半径は単独ではレバーにならず,喉と接合を直して初めて「大きいほど均一」という素直な効き方をする.ただし **r≲95 では喉なし設計が負ける**(小型機は fixed_cut).**方法論の追加**: 輝度マップの正規化に画素単位の最大値を使うと firefly に張り付く(max/min が 2.9〜15.5 まで暴れる)ため,**20 分割の輪帯平均の最大値**を 1.0 とする(ユーザー提案).分割数を評価パイプラインの既定と揃えると輝度マップの「最大輪帯」と輪帯プロファイルの「ピーク」が同じ輪帯を指し,図をまたいで数値が突き合わせられる.また `angular_ratio` は分母(動径分散)が潰れると跳ねるので単独で読んではならない(similar_radial r=250 で角度分散は 12.9→11.9 と微減しているのに比は 0.06→0.49).**未実施**: リム張り出し量 `RADIAL_TANGENT_SCALE`(現在 0.6)の最適化,`similar_g2` の残り 7 半径,ノイズフロア.実機観点ではリム水平出しは壁が板の縁に薄い刃先で当たるため襟の作り方を別途詰める必要がある.成果物: `output/sphere_radius_sweep/board_baffle_similar_radial/`,`scripts/build_cavity_comparison.py`,図 `cavity_comparison.png`・`rim_tangent_comparison.png`,記録 [TEST_15](06_TEST/TEST_15_バッフル導入と球半径スイープ.md). + +- **TEST_16 立方体キャビティ導入と球との比較 — 同じ内面積なら立方体が球に勝つと判明 (2026-08-31〜09-01)**: TEST_15 の序列「喉 > 接合 > サイズ」から「ロフトも接合も喉も構造的に存在しない立方体はどうか」という仮説を立て,立方体キャビティ(内寸 L・天井 z=0・底面 z=-L・board_baffle 構成据え置き)を新設して L ∈ {120, 160, 200, 280, 360} の 5 条件(既定 9 グリッドの部分集合=傾向を先に安く見るユーザー判断)をスイープ,球 `similar_radial` 10 条件と**内面積を横軸に**比較した.**結果 ①**: 「球のほうが理想積分球だから有利」は不成立.同一内面積(対数内挿)で立方体の構造 CoV は球の **0.62〜0.81 倍**(L=120 のみ互角),L=280(0.0109)で球の全条件最良(r=250 の 0.0145)を超え,L=360 は **0.0090 / 輪帯 min/max 0.964** で全条件を通じた最良.同内面積で深さも約 3 割浅く実用面でも有利=**設計探索の主軸は立方体系へ**.**結果 ②**: ムラの「形」は立方体でも球と同一の同心円ドーム(L=120 で方位分散 5.96 vs 動径分散 118).一次照射(バッフル裏→床の明るいリング)が軸対称で cos 重みの真下方向が支配し,多重拡散反射が壁の形を洗い流すため=輪帯プロファイル比較の正当性が立方体でも成立.**結果 ③(四隅の痕跡の定量)**: ユーザーとの議論「差があるはずかを白黒つける検定」として,輪帯ごとの方位プロファイルの **cos 4φ 符号付き係数 a4**(正=四隅が暗い.鏡映対称からゼロのはずの直交成分 b4 をノイズ対照に併用)を抽出.全条件 \|a4\| ≤ 0.26% で輪帯構造(4〜15%)の 1/30 以下,「四隅が暗い」符号の一貫した信号なし=**角のフィレット等は不要**.唯一有意な L=120 の −0.26% は逆符号(辺中央方向が暗い=壁余白 9mm の局所幾何の名残)で,小型化の下限を決めるのは隅でなく辺の余白と判明.**方法論の学び**: ① 楕円距離ビンの外側輪帯(d>0.707)は四隅方向にしか画素が無く,方位フーリエに掛けるとカバレッジの欠けが m=0 を m=4 に漏れ込ませ偽信号を作る→全セクタ被覆の輪帯のみ使用 / ② b4 対照で系統誤差床 ~0.1% を実測=MC 伝播だけの σ は有意性を過大評価する / ③ `angular_ratio` の跳ね(L=360 で 0.71)は今回も分母潰れの見かけ(TEST_15 の再確認).幾何検証(watertight・遮蔽 55,473 本 100%・法線逸脱 ≤0.06°)は球版の対策を織り込み済みで全て一発通過.**未実施**: 残り 4 条件(L=140/180/240/320)・フラット天井版(rim_z=-4.5)・直方体 L×L×D・ノイズフロア.成果物: `scripts/generate_cube_housing.py`・`sweep_cube_size.py`・`build_cube_sphere_comparison.py`・`analyze_fourfold_component.py`,`output/cube_size_sweep/board_baffle/`(summary.csv・`cube_vs_sphere.png`・`fourfold_component.png`),記録 [TEST_16](06_TEST/TEST_16_立方体キャビティ導入と球との比較.md). + +- **TEST_16 追試 — 残り 4 条件で全 9 条件が揃い単調性を確定・mitsuba を SAC 起因で 3.7.1 に固定 (2026-09-01)**: 残り L ∈ {140, 180, 240, 320} を実行しようとしたところ,**mitsuba 3.8.0 が import 不能**(Smart App Control が同梱 `IlmThread-mitsuba.dll` 1 個をレピュテーション判定でブロック.ファイルは 8/17 から無変更=クラウド側の判定が一晩で反転.vtk 9.6.1=TEST_13 検査 8 と同型).`LoadLibraryExW` + `DONT_RESOLVE_DLL_REFERENCES` で WinError 4551(ポリシー)と 126(依存の巻き添え)を切り分けてブロック対象を 1 ファイルに特定し,一時 venv で 3.7.1 が全 DLL 通ることを確認して **`mitsuba==3.7.1` に固定**(ユーザー承認.`pyproject.toml` に理由コメント,ENV_04 に vtk と並べて注意書き).uv 生成の `pytest.exe` ランチャも同時にブロックされたため `python -m pytest` で回避.**バージョン混在の妥当性検証**: 全 1043 テスト通過(絶対放射量の解析解検証込み)+ L=120 アンカー再レンダで 3.8.0 と構造 CoV 一致(0.0379)・輪帯プロファイル最大差 1.17σ=統計的に区別不能.**結果**: 全 9 条件で構造 CoV(0.0379→0.0090)・輪帯 min/max(0.858→0.964)・C/P・勾配のすべてが **L に対し厳密に単調改善**,9 本の輪帯プロファイルは交差なし.球との構造 CoV 比は 1.05→0.62 と単調に開き,交差点は内面積 10 万 mm² 前後.m=4 は L=140 が −0.212%(L=120 と同じ逆符号=辺余白の名残)で L=120→140→160 と単調減衰し「小型化の下限は隅でなく辺の余白」の解釈を補強.TEST_16 の限界から「5 条件のみ」が消え「レンダラ混在(検証済み)」に置き換わった.成果物: `output/cube_size_sweep/board_baffle/`(全 9 条件)・`_anchor_check_mitsuba371/L120/`,`pyproject.toml`(mitsuba 固定),記録 [TEST_16](06_TEST/TEST_16_立方体キャビティ導入と球との比較.md)・ENV_04. diff --git a/pyproject.toml b/pyproject.toml index cb07826..d5f639c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,13 @@ requires-python = ">=3.12" dependencies = [ "matplotlib>=3.10.9", - "mitsuba>=3.8.0", + # mitsuba は 3.7.1 に固定する.3.8.0 では Windows の Smart App Control が + # 同梱の IlmThread-mitsuba.dll(OpenEXR)1 個をレピュテーション判定でブロックし, + # mitsuba が import 不能=レンダ・遮蔽検証が全滅する(2026-09-01 実測. + # vtk 9.6.1 と同型の故障モード=docs/06_TEST/TEST_13 検査 8).3.7.1 は全 DLL が + # 読み込める.バージョンを上げる際は import 確認と絶対放射量テスト + # (uv run pytest -m "")を必ず通すこと. + "mitsuba==3.7.1", "numpy>=2.4.4", "opencv-python>=4.13.0.92", "pandas>=3.0.2", diff --git a/scripts/analyze_fourfold_component.py b/scripts/analyze_fourfold_component.py new file mode 100644 index 0000000..59c72c1 --- /dev/null +++ b/scripts/analyze_fourfold_component.py @@ -0,0 +1,252 @@ +"""輪帯ごとの方位プロファイルから cos 4φ 成分を抽出し「四隅の痕跡」を定量する(TEST_16). + +立方体キャビティの壁の四角さが評価板に写り込むなら,対称性から必ず 90° 回転対称 +(m=4)の方位成分として現れる.球は四回対称の壁を持たないため,球側の m=4 は +評価系(長方形 ROI・楕円ビン・MC ノイズ)が作る背景値を与える. +**立方体 − 球(同内面積)の m=4 が「立方体化が持ち込む四隅ムラ」の正味の量**になる. + +設計上の要点: + - 板は左右・上下に鏡映対称なので,四隅の構造は板軸に揃った **cos 4φ の符号付き + 係数 a4 だけ**に出る(a4 > 0 = 四隅が暗い,a4 < 0 = 辺の中央が暗い).直交成分 + b4 は対称性からゼロのはずで,**b4 が生きたノイズ対照**になる(a4 と同じ統計で + 計算されるが真の信号を含まない) + - 輪帯は評価パイプラインと同じ正規化楕円距離 20 ビン.ただし d が辺中点 + (d≈0.707)を超える輪帯は ROI の四隅方向にしか画素が無く,方位カバレッジの + 欠けが m=0(平均)を m=4 に漏れ込ませる偽信号を作るため,**全 72 セクタに + 画素がある輪帯だけ**を使う + - セクタ平均を先に取ってからフーリエ係数を出す(セクタ間の画素数ムラで方位の + 重みが偏るのを防ぐ).誤差はセクタ平均の標準誤差を伝播する + +実行方法: + uv run python scripts/analyze_fourfold_component.py +""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import matplotlib +import numpy as np + +matplotlib.use("Agg") +import matplotlib.pyplot as plt # noqa: E402 + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) + +from build_cube_sphere_comparison import ( # noqa: E402 + SPHERE_RUN_DIR, + nearest_by_area, + read_summary, +) +from sweep_cube_size import OUTPUT_BASE as CUBE_OUTPUT_BASE # noqa: E402 +from sweep_cube_size import RUN_DIR as CUBE_RUN_DIR # noqa: E402 +from sweep_cube_size import job_name as cube_job_name # noqa: E402 +from sweep_sphere_radius import OUTPUT_BASE as SPHERE_OUTPUT_BASE # noqa: E402 +from sweep_sphere_radius import job_name as sphere_job_name # noqa: E402 + +from sm_tias_lightsim.evaluation.roi_presets import SIM_ROI, extract_roi_xywh # noqa: E402 +from sm_tias_lightsim.evaluation.zones import compute_normalized_distance_map # noqa: E402 + +plt.rcParams["font.family"] = ["Yu Gothic", "MS Gothic", "DejaVu Sans"] + +N_RINGS = 20 # 評価パイプラインの輪帯分割と同一 +N_SECTORS = 72 # 方位セクタ数(m=4 の周期 90° あたり 18 セクタ) +MIN_PIXELS_PER_SECTOR = 5 # 全セクタがこれ以上の画素を持つ輪帯だけを「全方位カバー」とする +HARMONICS = (1, 2, 4) +# 集約に使う輪帯の距離範囲.内側は信号が原理的に小さく(どの方向も壁が遠い), +# 外側はカバレッジ限界(辺中点 d≈0.707)まで +AGGREGATE_RANGE = (0.35, 0.75) + + +def load_roi(luminance_npy: Path) -> np.ndarray: + """luminance.npy を読み ROI(輝度 2 次元配列)を返す.""" + arr = np.load(luminance_npy).astype(np.float64) + if arr.ndim == 3: + arr = 0.2126 * arr[..., 0] + 0.7152 * arr[..., 1] + 0.0722 * arr[..., 2] + return extract_roi_xywh(arr, SIM_ROI) + + +def ring_harmonics(roi: np.ndarray) -> list[dict[str, float]]: + """全方位カバーの各輪帯について方位フーリエ係数(ring mean 比)を返す. + + Returns: + 輪帯ごとの dict のリスト.キーは + d_center / n_pixels と,各 m について am / bm / se_m + (am, bm はセクタ平均のフーリエ係数を輪帯平均で割った相対値, + se_m は両係数共通の 1σ 伝播誤差). + """ + height, width = roi.shape + distance = compute_normalized_distance_map(height, width) + ys = np.arange(height, dtype=np.float64).reshape(-1, 1) - height / 2.0 + xs = np.arange(width, dtype=np.float64).reshape(1, -1) - width / 2.0 + phi = np.arctan2(np.broadcast_to(ys, roi.shape), np.broadcast_to(xs, roi.shape)) + + ring_edges = np.linspace(0.0, 1.0, N_RINGS + 1) + ring_index = np.digitize(distance.ravel(), ring_edges, right=True) + sector_index = ((phi.ravel() + np.pi) / (2.0 * np.pi) * N_SECTORS).astype(int) % N_SECTORS + sector_phi = -np.pi + (np.arange(N_SECTORS) + 0.5) * (2.0 * np.pi / N_SECTORS) + flat = roi.ravel() + + results: list[dict[str, float]] = [] + for ring in range(1, N_RINGS + 1): + mask = ring_index == ring + if not mask.any(): + continue + values = flat[mask] + sectors = sector_index[mask] + counts = np.bincount(sectors, minlength=N_SECTORS) + if counts.min() < MIN_PIXELS_PER_SECTOR: + continue # 方位カバレッジが欠けた輪帯は偽の m=4 を作るため除外 + sums = np.bincount(sectors, weights=values, minlength=N_SECTORS) + sq_sums = np.bincount(sectors, weights=values**2, minlength=N_SECTORS) + means = sums / counts + # セクタ内標準偏差 → セクタ平均の標準誤差(MC ノイズは画素間独立とみなす) + variances = np.maximum(sq_sums / counts - means**2, 0.0) + sems = np.sqrt(variances / counts) + ring_mean = float(np.mean(means)) + + row: dict[str, float] = { + "d_center": float((ring_edges[ring - 1] + ring_edges[ring]) / 2.0), + "n_pixels": float(mask.sum()), + } + for m in HARMONICS: + cos_m, sin_m = np.cos(m * sector_phi), np.sin(m * sector_phi) + row[f"a{m}"] = float(2.0 / N_SECTORS * np.sum(means * cos_m) / ring_mean) + row[f"b{m}"] = float(2.0 / N_SECTORS * np.sum(means * sin_m) / ring_mean) + # Var(a) = (2/K)^2 Σ sem^2 cos^2 ≈ (2/K^2) Σ sem^2(cos^2 の平均 1/2) + row[f"se_{m}"] = float( + np.sqrt(4.0 / N_SECTORS**2 * np.sum(sems**2 * cos_m**2)) / ring_mean + ) + results.append(row) + return results + + +def aggregate(rings: list[dict[str, float]], key: str) -> tuple[float, float]: + """AGGREGATE_RANGE の輪帯を逆分散重みで集約し (推定値, 標準誤差) を返す.""" + m = key.lstrip("ab") + selected = [r for r in rings if AGGREGATE_RANGE[0] <= r["d_center"] <= AGGREGATE_RANGE[1]] + weights = np.array([1.0 / r[f"se_{m}"] ** 2 for r in selected]) + values = np.array([r[key] for r in selected]) + return float(np.sum(weights * values) / np.sum(weights)), float(1.0 / np.sqrt(np.sum(weights))) + + +def analyze(label: str, luminance_npy: Path) -> dict[str, object]: + """1 条件を解析して集約値と輪帯プロファイルを返す.""" + rings = ring_harmonics(load_roi(luminance_npy)) + result: dict[str, object] = {"label": label, "rings": rings} + for key in ("a4", "b4", "a2", "a1", "b1"): + result[key], result[f"{key}_se"] = aggregate(rings, key) + return result + + +def print_report(cube_results: list[dict], sphere_results: list[dict]) -> None: + """集約表と立方体−球の差を標準出力に書く.""" + print(f"\n=== 方位フーリエ成分(輪帯 d∈[{AGGREGATE_RANGE[0]}, {AGGREGATE_RANGE[1]}] " + "の逆分散重み集約・輪帯平均比 %)===") + print("a4 > 0 が「四隅が暗い」.b4 は対称性からゼロのはずのノイズ対照.") + print(f"{'条件':>14} {'a4 [%]':>14} {'b4 [%](対照)':>16} {'a2 [%]':>14} {'|m1| [%]':>10}") + for res in cube_results + sphere_results: + m1 = float(np.hypot(res["a1"], res["b1"])) + print(f"{res['label']:>14} {res['a4'] * 100:7.3f}±{res['a4_se'] * 100:5.3f} " + f"{res['b4'] * 100:8.3f}±{res['b4_se'] * 100:5.3f} " + f"{res['a2'] * 100:7.3f}±{res['a2_se'] * 100:5.3f} {m1 * 100:10.3f}") + + print("\n=== 立方体 − 球(同内面積ペア)の a4 差 ===") + print(f"{'ペア':>22} {'Δa4 [%]':>14} {'有意性':>8}") + for cube, sphere in zip(cube_results, sphere_results, strict=True): + delta = cube["a4"] - sphere["a4"] + se = float(np.hypot(cube["a4_se"], sphere["a4_se"])) + sigma = abs(delta) / se + print(f"{cube['label'] + ' vs ' + sphere['label']:>22} " + f"{delta * 100:7.3f}±{se * 100:5.3f} {sigma:6.1f}σ") + + +def draw_figure(cube_results: list[dict], sphere_results: list[dict], output: Path) -> None: + """a4 の輪帯プロファイルと集約値の比較図を描く.""" + fig, axes = plt.subplots(1, 3, figsize=(19, 5.5)) + + colors = plt.cm.viridis(np.linspace(0.0, 0.9, len(cube_results))) + for results, ax, kind in ((cube_results, axes[0], "立方体"), (sphere_results, axes[1], "球")): + for res, color in zip(results, colors, strict=True): + d = [r["d_center"] for r in res["rings"]] + a4 = [r["a4"] * 100 for r in res["rings"]] + se = [r["se_4"] * 100 for r in res["rings"]] + ax.errorbar(d, a4, yerr=se, marker="o", ms=3, lw=1.4, color=color, + label=res["label"]) + ax.axhline(0.0, color="gray", lw=0.8, ls="--") + ax.set_xlabel("正規化楕円距離(輪帯中心)") + ax.set_ylabel("a4(輪帯平均比 %・正=四隅が暗い)") + ax.set_title(f"{kind}: cos 4φ 係数の輪帯プロファイル(±1σ)", fontsize=11) + ax.legend(fontsize=8) + ax.grid(alpha=0.3) + y_all = [abs(r["a4"] * 100) + r["se_4"] * 100 + for res in cube_results + sphere_results for r in res["rings"]] + for ax in axes[:2]: + ax.set_ylim(-max(y_all) * 1.05, max(y_all) * 1.05) + + ax = axes[2] + x = np.arange(len(cube_results)) + for results, offset, color, label in ( + (cube_results, -0.12, "tab:red", "立方体 a4"), + (sphere_results, 0.12, "tab:blue", "球 a4"), + ): + ax.errorbar(x + offset, [r["a4"] * 100 for r in results], + yerr=[r["a4_se"] * 100 for r in results], + fmt="s", capsize=4, color=color, label=label) + ax.errorbar(x, [r["b4"] * 100 for r in cube_results], + yerr=[r["b4_se"] * 100 for r in cube_results], + fmt="x", capsize=3, color="gray", label="立方体 b4(ノイズ対照)") + ax.axhline(0.0, color="gray", lw=0.8, ls="--") + ax.set_xticks(x) + ax.set_xticklabels([f"{c['label']}\nvs {s['label']}" + for c, s in zip(cube_results, sphere_results, strict=True)], fontsize=8) + ax.set_ylabel("集約 a4(輪帯平均比 %)") + ax.set_title(f"集約値の比較(d∈[{AGGREGATE_RANGE[0]}, {AGGREGATE_RANGE[1]}]・±1σ)", + fontsize=11) + ax.legend(fontsize=8) + ax.grid(alpha=0.3) + + fig.suptitle( + "四隅の痕跡の定量: 輪帯ごとの方位プロファイルの cos 4φ 成分(板軸フレーム・符号付き)", + fontsize=14, + ) + fig.tight_layout() + output.parent.mkdir(parents=True, exist_ok=True) + fig.savefig(output, dpi=110) + print(f"\n出力: {output}") + + +def main() -> int: + """立方体全条件と同内面積の球について m=4 成分を抽出する.""" + parser = argparse.ArgumentParser(description="cos 4φ 成分の抽出(四隅の痕跡の定量)") + parser.add_argument("--output", type=Path, default=None, help="出力 PNG パス") + args = parser.parse_args() + + cube_run = CUBE_OUTPUT_BASE / CUBE_RUN_DIR + sphere_run = SPHERE_OUTPUT_BASE / SPHERE_RUN_DIR + cube_rows = read_summary(cube_run / "summary.csv", "size_mm") + sphere_rows = read_summary(sphere_run / "summary.csv", "radius_mm") + + cube_results, sphere_results = [], [] + for row in cube_rows: + lum = cube_run / cube_job_name(row["size_mm"]) / "luminance.npy" + if not lum.exists(): + continue + cube_results.append(analyze(f"L={row['size_mm']:g}", lum)) + sphere = nearest_by_area(row["cavity_area_mm2"], sphere_rows) + sphere_results.append(analyze( + f"r={sphere['radius_mm']:g}", + sphere_run / sphere_job_name(sphere["radius_mm"]) / "luminance.npy", + )) + + print_report(cube_results, sphere_results) + draw_figure(cube_results, sphere_results, + args.output or cube_run / "fourfold_component.png") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/build_cube_sphere_comparison.py b/scripts/build_cube_sphere_comparison.py new file mode 100644 index 0000000..0ae1cbb --- /dev/null +++ b/scripts/build_cube_sphere_comparison.py @@ -0,0 +1,320 @@ +"""立方体キャビティと球キャビティを内面積で対応づけて比較する図を生成する(TEST_16). + +球(similar_radial・10 条件・レンダ済み)と立方体(サイズスイープ)は「同じ大きさ」の +定義が自明でない.多重反射の理論では放射輝度は総内面積と開口比で決まるため, +**内面積を横軸**に取って両者の指標カーブを重ねる(積分球としての比較). + +評価の作法は TEST_15 で確立したものを踏襲する: + - 生の CoV では比較しない(MC ノイズ支配).構造 CoV(decompose_cov)を使う + - 形の比較は同心円(輪帯)プロファイルの方位平均で行い,ピークで正規化する + - 輝度マップは 20 分割の輪帯平均の最大値を 1.0 に正規化する + - angular_ratio は分母(動径分散)で跳ねるため,angular/radial 分散の内訳を併記する + +解析関数は既存スクリプト(build_radial_comparison / build_radius_sweep_visual)から +import し,輪帯・正規化・CoV 分解の定義が図をまたいで厳密に一致するようにする. + +実行方法: + uv run python scripts/build_cube_sphere_comparison.py +""" + +from __future__ import annotations + +import argparse +import csv +import sys +from pathlib import Path + +import matplotlib +import numpy as np + +matplotlib.use("Agg") +import matplotlib.pyplot as plt # noqa: E402 + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) + +from build_radial_comparison import ( # noqa: E402 + normalize_to_peak, + radial_stats, + ring_min_max_ratio, +) +from build_radius_sweep_visual import decompose_cov, normalized_map # noqa: E402 +from sweep_cube_size import OUTPUT_BASE as CUBE_OUTPUT_BASE # noqa: E402 +from sweep_cube_size import RUN_DIR as CUBE_RUN_DIR # noqa: E402 +from sweep_cube_size import job_name as cube_job_name # noqa: E402 +from sweep_sphere_radius import OUTPUT_BASE as SPHERE_OUTPUT_BASE # noqa: E402 +from sweep_sphere_radius import job_name as sphere_job_name # noqa: E402 + +plt.rcParams["font.family"] = ["Yu Gothic", "MS Gothic", "DejaVu Sans"] + +# 球側は TEST_15 で最良だった similar_radial(リム水平出し)を比較対象にする +SPHERE_RUN_DIR = "board_baffle_similar_radial" + +# 輪帯の分割数(評価パイプライン・既存比較図と同一) +N_BINS = 20 + +CUBE_COLOR = "tab:red" +SPHERE_COLOR = "tab:blue" + + +def read_summary(path: Path, key: str) -> list[dict[str, float]]: + """summary.csv を読み,key 列の昇順の辞書リストで返す.""" + with path.open(encoding="utf-8") as fh: + rows = [ + {k: (float(v) if v else float("nan")) for k, v in row.items()} + for row in csv.DictReader(fh) + ] + return sorted(rows, key=lambda r: r[key]) + + +def read_full_metrics(path: Path) -> dict[str, float]: + """metrics.csv の 1 行目を全列 float で返す(angular/radial 分散の内訳併記用).""" + with path.open(encoding="utf-8") as fh: + row = next(csv.DictReader(fh)) + return {k: (float(v) if v else float("nan")) for k, v in row.items()} + + +def collect( + rows: list[dict[str, float]], run_dir: Path, size_key: str, job_name_fn +) -> list[dict[str, float]]: + """summary の各行に輪帯 min/max・構造 CoV・分散内訳を実測で付加して返す.""" + result = [] + for row in rows: + job_dir = run_dir / job_name_fn(row[size_key]) + lum = job_dir / "luminance.npy" + if not lum.exists(): + continue + centers, means, _ = radial_stats(lum, N_BINS) + _, structure, noise = decompose_cov(lum) + enriched = dict(row) + enriched["ring_min_max"] = ring_min_max_ratio(centers, means) + enriched["structure_cov"] = structure + enriched["noise_cov"] = noise + enriched.update( + { + k: v + for k, v in read_full_metrics(job_dir / "metrics.csv").items() + if k in ("angular_variance", "radial_variance", "pca_primary_axis_angle_deg") + } + ) + result.append(enriched) + return result + + +def nearest_by_area(area: float, rows: list[dict[str, float]]) -> dict[str, float]: + """内面積が最も近い行を返す(対数距離で比較).""" + return min(rows, key=lambda r: abs(np.log(r["cavity_area_mm2"] / area))) + + +def draw_cube_maps(fig, gs, cube_rows: list[dict[str, float]], run_dir: Path) -> None: + """各サイズの輝度マップを「輪帯平均の最大値=1.0」で正規化して並べる(共通スケール). + + 条件数が 5 を超えるときは全範囲から等間隔に 5 条件を選ぶ(先頭 5 条件だけだと + サイズ範囲の半分しか見えない). + """ + indices = np.unique(np.linspace(0, len(cube_rows) - 1, min(5, len(cube_rows))).round()) + maps = [ + (row["size_mm"], normalized_map(run_dir / cube_job_name(row["size_mm"]) / "luminance.npy")) + for row in (cube_rows[int(i)] for i in indices) + ] + if not maps: + return + stacked = np.concatenate([m.ravel() for _, m in maps]) + vmin = float(np.percentile(stacked, 0.5)) + for i, (size, data) in enumerate(maps[:5]): + ax = fig.add_subplot(gs[0, i]) + image = ax.imshow(data, cmap="hot", vmin=vmin, vmax=1.0) + ax.set_title(f"L={size:g}mm", fontsize=10) + ax.axis("off") + if i == len(maps[:5]) - 1: + fig.colorbar(image, ax=ax, fraction=0.046, label="最大輪帯(20 分割)=1.0") + + +def draw_cube_profiles(ax, cube_rows: list[dict[str, float]], run_dir: Path) -> None: + """立方体全サイズの輪帯プロファイルを重ね描きする(ピーク=1 正規化・標準誤差帯付き).""" + colors = plt.cm.viridis(np.linspace(0.0, 0.9, len(cube_rows))) + for row, color in zip(cube_rows, colors, strict=True): + lum = run_dir / cube_job_name(row["size_mm"]) / "luminance.npy" + centers, means, sems = radial_stats(lum, N_BINS) + means, sems = normalize_to_peak(centers, means, sems) + ax.plot(centers, means, color=color, lw=1.6, label=f"L={row['size_mm']:g}") + ax.fill_between(centers, means - sems, means + sems, color=color, alpha=0.35, lw=0) + ax.axhline(1.0, color="gray", lw=0.8, ls="--") + ax.set_xlabel("正規化楕円距離(0=中心, 1=ROI 四隅)") + ax.set_ylabel("輪帯平均輝度(ピーク=1)") + ax.set_title("立方体: 同心円プロファイルのサイズ依存(帯は ±1 標準誤差)", fontsize=11) + ax.legend(fontsize=8) + ax.grid(alpha=0.3) + + +def draw_metric_vs_area( + ax, + cube_rows: list[dict[str, float]], + sphere_rows: list[dict[str, float]], + key: str, + ylabel: str, + title: str, + ideal: float | None, +) -> None: + """指標を内面積(対数軸)に対して球・立方体で重ね描きする.""" + for rows, color, marker, label in ( + (sphere_rows, SPHERE_COLOR, "o", "球 similar_radial(r=88〜250)"), + (cube_rows, CUBE_COLOR, "s", "立方体(L スイープ)"), + ): + areas = [r["cavity_area_mm2"] for r in rows] + ax.plot(areas, [r[key] for r in rows], marker=marker, color=color, label=label) + if ideal is not None: + ax.axhline(ideal, color="gray", lw=0.8, ls="--") + ax.set_xscale("log") + ax.set_xlabel("キャビティ内面積 (mm²・対数)") + ax.set_ylabel(ylabel) + ax.set_title(title, fontsize=11) + ax.legend(fontsize=8) + ax.grid(alpha=0.3, which="both") + + +def draw_matched_pair( + ax, + cube_row: dict[str, float], + sphere_row: dict[str, float], + cube_run: Path, + sphere_run: Path, +) -> None: + """内面積が最も近い球と立方体の輪帯プロファイルを 1 枚に重ねる.""" + targets = [ + ( + cube_run / cube_job_name(cube_row["size_mm"]) / "luminance.npy", + f"立方体 L={cube_row['size_mm']:g}({cube_row['cavity_area_mm2'] / 1000:.0f}k mm²)", + CUBE_COLOR, + "-", + ), + ( + sphere_run / sphere_job_name(sphere_row["radius_mm"]) / "luminance.npy", + f"球 r={sphere_row['radius_mm']:g}({sphere_row['cavity_area_mm2'] / 1000:.0f}k mm²)", + SPHERE_COLOR, + "--", + ), + ] + for path, label, color, ls in targets: + centers, means, sems = radial_stats(path, N_BINS) + means, sems = normalize_to_peak(centers, means, sems) + ax.plot(centers, means, color=color, ls=ls, lw=2.0, label=label) + ax.fill_between(centers, means - sems, means + sems, color=color, alpha=0.3, lw=0) + ax.axhline(1.0, color="gray", lw=0.8, ls="--") + ax.set_xlabel("正規化楕円距離") + ax.set_ylabel("輪帯平均輝度(ピーク=1)") + ax.set_title("内面積で対応づけた直接比較", fontsize=11) + ax.legend(fontsize=8) + ax.grid(alpha=0.3) + + +def print_table(cube_rows: list[dict[str, float]], sphere_rows: list[dict[str, float]]) -> None: + """比較表を標準出力に書く(angular/radial 分散の内訳を必ず併記する).""" + print("\n=== 立方体(実測) ===") + print( + f"{'L':>6} {'内面積mm2':>10} {'構造CoV':>8} {'輪帯min/max':>11} " + f"{'angular_ratio':>13} {'ang_var':>8} {'rad_var':>8} {'PCA_ecc':>8}" + ) + for r in cube_rows: + print( + f"{r['size_mm']:6.0f} {r['cavity_area_mm2']:10.0f} {r['structure_cov']:8.4f} " + f"{r['ring_min_max']:11.4f} {r['angular_ratio']:13.4f} " + f"{r['angular_variance']:8.2f} {r['radial_variance']:8.2f} " + f"{r['pca_eccentricity']:8.4f}" + ) + print("\n=== 球 similar_radial(参照) ===") + print( + f"{'r':>6} {'内面積mm2':>10} {'構造CoV':>8} {'輪帯min/max':>11} " + f"{'angular_ratio':>13} {'ang_var':>8} {'rad_var':>8} {'PCA_ecc':>8}" + ) + for r in sphere_rows: + print( + f"{r['radius_mm']:6.0f} {r['cavity_area_mm2']:10.0f} {r['structure_cov']:8.4f} " + f"{r['ring_min_max']:11.4f} {r['angular_ratio']:13.4f} " + f"{r['angular_variance']:8.2f} {r['radial_variance']:8.2f} " + f"{r['pca_eccentricity']:8.4f}" + ) + + # 同じ内面積での球の値を対数内挿して,立方体との比を出す(1 対 1 対応が無いため) + log_area = np.log([r["cavity_area_mm2"] for r in sphere_rows]) + print("\n=== 同一内面積での比較(球は対数内挿) ===") + print( + f"{'L':>6} {'構造CoV 立方体':>13} {'球(内挿)':>10} {'比':>6} " + f"{'輪帯min/max 立方体':>17} {'球(内挿)':>10}" + ) + for r in cube_rows: + x = np.log(r["cavity_area_mm2"]) + sphere_cov = float(np.interp(x, log_area, [s["structure_cov"] for s in sphere_rows])) + sphere_ring = float(np.interp(x, log_area, [s["ring_min_max"] for s in sphere_rows])) + print( + f"{r['size_mm']:6.0f} {r['structure_cov']:13.4f} {sphere_cov:10.4f} " + f"{r['structure_cov'] / sphere_cov:6.2f} " + f"{r['ring_min_max']:17.4f} {sphere_ring:10.4f}" + ) + + +def main() -> int: + """立方体 vs 球の比較図を生成する.""" + parser = argparse.ArgumentParser(description="立方体と球のキャビティ比較図を生成") + parser.add_argument("--output", type=Path, default=None, help="出力 PNG パス") + args = parser.parse_args() + + cube_run = CUBE_OUTPUT_BASE / CUBE_RUN_DIR + sphere_run = SPHERE_OUTPUT_BASE / SPHERE_RUN_DIR + cube_summary = cube_run / "summary.csv" + sphere_summary = sphere_run / "summary.csv" + for path in (cube_summary, sphere_summary): + if not path.exists(): + print(f"エラー: {path} がありません(先にスイープを実行してください)") + return 1 + + cube_rows = collect(read_summary(cube_summary, "size_mm"), cube_run, "size_mm", cube_job_name) + sphere_rows = collect( + read_summary(sphere_summary, "radius_mm"), sphere_run, "radius_mm", sphere_job_name + ) + output = args.output or cube_run / "cube_vs_sphere.png" + + fig = plt.figure(figsize=(20, 14)) + gs = fig.add_gridspec(3, 6, height_ratios=[1.2, 1, 1]) + draw_cube_maps(fig, gs, cube_rows, cube_run) + draw_cube_profiles(fig.add_subplot(gs[1, 0:2]), cube_rows, cube_run) + draw_metric_vs_area( + fig.add_subplot(gs[1, 2:4]), + cube_rows, + sphere_rows, + "ring_min_max", + "輪帯 min/max(1 が理想)", + "ドームの深さ vs 内面積", + 1.0, + ) + draw_metric_vs_area( + fig.add_subplot(gs[1, 4:6]), + cube_rows, + sphere_rows, + "structure_cov", + "構造 CoV(小さいほど均一)", + "構造 CoV vs 内面積", + None, + ) + # 内面積の近い代表 3 ペア(小・中・大)で直接重ねる + picks = [cube_rows[0], cube_rows[len(cube_rows) // 2], cube_rows[-1]] + for i, cube_row in enumerate(picks): + sphere_row = nearest_by_area(cube_row["cavity_area_mm2"], sphere_rows) + draw_matched_pair( + fig.add_subplot(gs[2, 2 * i : 2 * i + 2]), cube_row, sphere_row, cube_run, sphere_run + ) + fig.suptitle( + "立方体キャビティ vs 球キャビティ(board_baffle・内面積で対応づけ)" + " 輪帯 20 分割・方位平均・ピーク正規化", + fontsize=15, + ) + fig.tight_layout() + output.parent.mkdir(parents=True, exist_ok=True) + fig.savefig(output, dpi=100) + print(f"出力: {output}") + print_table(cube_rows, sphere_rows) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/generate_cube_housing.py b/scripts/generate_cube_housing.py new file mode 100644 index 0000000..7f2196e --- /dev/null +++ b/scripts/generate_cube_housing.py @@ -0,0 +1,264 @@ +# TEST_16(立方体キャビティ実験)用: 評価板の矩形輪郭に整合する立方体筐体メッシュ生成. +# +# 球(generate_sphere_loft_housing.py)では「矩形の評価板 → 円形の球断面」を繋ぐ +# ロフトが必要で,その接合の幾何(板直下の喉・曲率不連続)が TEST_15 の主要な +# 交絡要因だった.立方体は評価板もキャビティ断面も矩形なのでロフトが構造的に不要になり, +# 上面はそのまま「板の縁から外へ広がる水平な天井」=TEST_15 で最良だった radial ロフトが +# 目指した形になる. +"""立方体キャビティ筐体(評価板輪郭に整合する閉ソリッド)STL を生成する. + +構成(上から下へ,単位 mm): + 1. リム矩形: 評価板輪郭から rim_inset だけ内側の矩形.z=rim_z(板の実体に + 食い込む高さ)に置き,板とのオーバーラップで隙間なくシールする + 2. 天井: z=rim_z の水平面.リム矩形から内壁正方形(L×L)まで広がる + 3. 側壁 4 枚: z=rim_z から z=rim_z-L まで + 4. 底面: z=rim_z-L の水平面 + +内面と外面(thickness オフセット.外面の天板は rim_z+thickness に持ち上げて +内面天井との共面を避ける)を生成し,リム帯で縫合した閉ソリッドを出力する. + +球版と違い全ての面が平面なので,三角形分割は物理(法線・交差)に影響しない. +分割解像度はレイの数値的なすり抜け回避と可視化の見やすさのためだけにある. +周方向サンプルは半ステップずらし,メッシュエッジが XZ / YZ 対称面に乗らないようにする +(球スイープの遮蔽検証で実測したすり抜けの再発防止). + +使用例: + uv run python scripts/generate_cube_housing.py --size 200 \ + --output configs/meshes/立方体_L200.stl +""" + +from __future__ import annotations + +import argparse +from pathlib import Path + +import numpy as np +import trimesh +from generate_sphere_loft_housing import _rect_radius, _stitch_rings + + +def _corner_angles(half_x: float, half_y: float) -> np.ndarray: + """矩形(半幅 half_x, half_y)の 4 隅の方位角を返す.""" + t = float(np.arctan2(half_y, half_x)) + return np.array([t, np.pi - t, np.pi + t, 2.0 * np.pi - t]) + + +def _build_theta(n_theta: int, rect_halves: list[tuple[float, float]]) -> np.ndarray: + """周方向サンプル角を返す(均等分割 + 全矩形の隅の厳密角). + + 均等分割は半ステップずらして 0 / 90 / 180 / 270 度を避ける(対称面上の + メッシュエッジをレイが数値的にすり抜ける現象の回避).隅の角度を明示的に + 含めることで,矩形の角が theta 分割で面取りされず厳密に表現される. + """ + base = (np.arange(n_theta) + 0.5) * (2.0 * np.pi / n_theta) + corners = np.concatenate([_corner_angles(hx, hy) for hx, hy in rect_halves]) + return np.unique(np.concatenate([base, corners])) + + +def _build_surface( + theta: np.ndarray, + rim_r: np.ndarray, + wall_r: np.ndarray, + rim_z: float, + top_z: float, + bottom_z: float, + n_ceiling: int, + n_wall: int, + n_bottom: int, +) -> tuple[np.ndarray, np.ndarray]: + """リム矩形 →(襟)→ 天井 → 側壁 → 底面の 1 枚曲面を生成する. + + top_z > rim_z のとき,リムから top_z まで垂直に立ち上がる襟を先頭に挿入する + (外面用.天板を持ち上げて内面天井 z=rim_z との共面=ゼロ厚を避ける). + + Returns: + (vertices, rings): vertices は (N, 3),rings は (n_ring, n_theta) の頂点 index. + リングは上(リム)から下(底面中心直前)の順.末尾に底面中心の極頂点を 1 つ追加. + """ + rings_rho: list[np.ndarray] = [] + rings_z: list[float] = [] + if top_z > rim_z: + for z in np.linspace(rim_z, top_z, 2, endpoint=False): + rings_rho.append(rim_r) + rings_z.append(float(z)) + for t in np.linspace(0.0, 1.0, n_ceiling, endpoint=False): + rings_rho.append((1.0 - t) * rim_r + t * wall_r) + rings_z.append(top_z) + for z in np.linspace(top_z, bottom_z, n_wall, endpoint=False): + rings_rho.append(wall_r) + rings_z.append(float(z)) + for t in np.linspace(0.0, 1.0, n_bottom, endpoint=False): + rings_rho.append((1.0 - t) * wall_r) + rings_z.append(bottom_z) + + n_ring, n_theta = len(rings_rho), len(theta) + vertices = np.empty((n_ring * n_theta + 1, 3), dtype=np.float64) + rings = np.arange(n_ring * n_theta).reshape(n_ring, n_theta) + for i, (rho, z) in enumerate(zip(rings_rho, rings_z, strict=True)): + vertices[rings[i], 0] = rho * np.cos(theta) + vertices[rings[i], 1] = rho * np.sin(theta) + vertices[rings[i], 2] = z + vertices[-1] = (0.0, 0.0, bottom_z) + return vertices, rings + + +def build_cube_housing( + size: float, + thickness: float, + board_half_x: float, + board_half_y: float, + rim_inset: float, + rim_z: float, + n_theta: int = 256, + n_ceiling: int = 24, + n_wall: int = 64, + n_bottom: int = 32, +) -> trimesh.Trimesh: + """立方体キャビティ筐体の閉ソリッド(内面+外面+リム帯)を生成する. + + Args: + size: キャビティ内寸の一辺 L(mm).内壁は x, y = ±L/2,深さは rim_z から L. + thickness: 殻厚(mm).外面は内面をおおむね thickness オフセットした形. + board_half_x: 評価板輪郭の X 半幅(mm). + board_half_y: 評価板輪郭の Y 半幅(mm). + rim_inset: リム矩形を板輪郭から内側へ寄せる量(mm).板とのオーバーラップ代. + rim_z: リム矩形と天井の Z 座標(mm).板の実体に食い込む高さに置いてシールする. + n_theta: 周方向の均等分割数(隅の厳密角が追加されるため実分割数は少し増える). + n_ceiling: 天井(リム矩形 → 内壁正方形)の分割リング数. + n_wall: 側壁の Z 方向リング数. + n_bottom: 底面(内壁正方形 → 中心)の分割リング数. + + Returns: + watertight な筐体ソリッドメッシュ(mm 単位). + + Raises: + ValueError: 評価板が上面に収まらない size が指定された場合,または + 生成メッシュが watertight にならなかった場合. + """ + if size / 2.0 <= max(board_half_x, board_half_y): + msg = ( + f"size={size}mm では評価板({2 * board_half_x:g}×{2 * board_half_y:g}mm)が" + f"上面に収まりません(size > {2 * max(board_half_x, board_half_y):g} が必要)" + ) + raise ValueError(msg) + + hx_in = board_half_x - rim_inset + hy_in = board_half_y - rim_inset + half_in = size / 2.0 + half_out = half_in + thickness + theta = _build_theta( + n_theta, + [ + (hx_in, hy_in), + (half_in, half_in), + (hx_in + thickness, hy_in + thickness), + (half_out, half_out), + ], + ) + + v_in, rings_in = _build_surface( + theta, + _rect_radius(theta, hx_in, hy_in), + _rect_radius(theta, half_in, half_in), + rim_z, + rim_z, + rim_z - size, + n_ceiling, + n_wall, + n_bottom, + ) + v_out, rings_out = _build_surface( + theta, + _rect_radius(theta, hx_in + thickness, hy_in + thickness), + _rect_radius(theta, half_out, half_out), + rim_z, + rim_z + thickness, + rim_z - size - thickness, + n_ceiling, + n_wall, + n_bottom, + ) + + f_in = _stitch_rings(rings_in, len(v_in) - 1) + f_out = _stitch_rings(rings_out, len(v_out) - 1) + + offset = len(v_in) + vertices = np.vstack([v_in, v_out]) + # リム帯: 内面上端リングと外面上端リングを z=rim_z の平面内で接続して閉じる + top_in, top_out = rings_in[0], rings_out[0] + offset + n_ring_theta = rings_in.shape[1] + band: list[tuple[int, int, int]] = [] + for j in range(n_ring_theta): + k = (j + 1) % n_ring_theta + band.append((top_in[j], top_out[j], top_out[k])) + band.append((top_in[j], top_out[k], top_in[k])) + faces = np.vstack([f_in, f_out + offset, np.asarray(band, dtype=np.int64)]) + + mesh = trimesh.Trimesh(vertices=vertices, faces=faces, process=False) + mesh.fix_normals() # 閉ソリッドとして法線を外向きに統一(内面は自動的にキャビティ向き) + if not mesh.is_watertight: + raise ValueError("生成メッシュが watertight になりませんでした(縫合パラメータを確認)") + return mesh + + +def _parse_args(argv: list[str] | None = None) -> argparse.Namespace: + """コマンドライン引数をパースして返す.""" + parser = argparse.ArgumentParser( + prog="generate_cube_housing", + description="評価板輪郭に整合する立方体キャビティ筐体 STL(mm 単位)を生成する.", + ) + parser.add_argument( + "--size", type=float, default=200.0, help="キャビティ内寸 L(mm).既定 200" + ) + parser.add_argument("--thickness", type=float, default=3.0, help="殻厚(mm).既定 3") + parser.add_argument( + "--board-half", + type=float, + nargs=2, + default=(49.5, 51.0), + metavar=("HX", "HY"), + help="評価板輪郭の半幅 X Y(mm).既定 49.5 51(99×102mm 板)", + ) + parser.add_argument( + "--rim-inset", + type=float, + default=1.5, + help="リムを板輪郭から内側へ寄せる量(mm).既定 1.5", + ) + parser.add_argument( + "--rim-z", + type=float, + default=0.0, + help="リムと天井の Z 座標(mm).既定 0(板裏面 -4.5 より 4.5mm 食い込みシール)", + ) + parser.add_argument( + "--output", + type=Path, + default=Path("configs/meshes/立方体_L200.stl"), + help="出力 STL パス.既定 configs/meshes/立方体_L200.stl", + ) + return parser.parse_args(argv) + + +def main(argv: list[str] | None = None) -> int: + """筐体を生成して STL 出力し,検証情報を表示する.""" + args = _parse_args(argv) + mesh = build_cube_housing( + size=args.size, + thickness=args.thickness, + board_half_x=args.board_half[0], + board_half_y=args.board_half[1], + rim_inset=args.rim_inset, + rim_z=args.rim_z, + ) + args.output.parent.mkdir(parents=True, exist_ok=True) + mesh.export(args.output) + print(f"出力: {args.output}") + print(f"faces: {len(mesh.faces)}, watertight: {mesh.is_watertight}") + print(f"bounds (mm): {mesh.bounds.round(2).tolist()}") + print(f"体積 (mm^3): {mesh.volume:.0f}(正値なら法線外向きで正常)") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/show_scene_cutaway.py b/scripts/show_scene_cutaway.py index 36925d5..b16d6b8 100644 --- a/scripts/show_scene_cutaway.py +++ b/scripts/show_scene_cutaway.py @@ -30,7 +30,7 @@ LED_COLOR = "gold" BAFFLE_COLOR = "darkorange" # 半割りにして内部を見せるのは筐体だけ.評価板とバッフルは丸ごと残す -CLIPPED_MESH_IDS = ("sphere_housing",) +CLIPPED_MESH_IDS = ("sphere_housing", "cube_housing") def _load_mesh(path: Path, unit: str) -> pv.PolyData: diff --git a/scripts/sweep_cube_size.py b/scripts/sweep_cube_size.py new file mode 100644 index 0000000..850e4dd --- /dev/null +++ b/scripts/sweep_cube_size.py @@ -0,0 +1,344 @@ +"""立方体キャビティのサイズスイープ(球との形状比較・TEST_16). + +TEST_15 の結論「効き目の序列は 板直下の喉 > 接合の相似・G2 化 > 球半径そのもの」を受け, +ロフトも接合も構造的に存在しない立方体キャビティで同じスイープを行う.立方体の上面は +板の縁から外へ広がる水平な天井そのもので,TEST_15 最良の similar_radial(リム水平出し)が +目指した形と一致する.「球のほうが理想積分球だから有利」が成り立つかを実測で確かめる. + +幾何のパラメータ化: + キャビティ内寸 L の立方体.天井 z=0(リムと同一平面),底面 z=-L,内壁 x, y = ±L/2. + 評価板・リム・材質・レンダ条件は球スイープ(sweep_sphere_radius.py)と完全に同一. + + led_z = -L + LED_WALL_STANDOFF (底面中心に密着・評価板向き +Z) + バッフル = led_z + BAFFLE_GAP (球版と同じ閉形式 baffle_radius) + +固定条件(球スイープと完全に同一に保つ): + 材質 : blendbsdf rho=0.85 / omega=0.05 / alpha=0.1 + 光源 : 円板 radius=1.7662mm(面積 9.8mm^2 保存).board_baffle 構成のみ + レンダ: spp=1024 / max_depth=16 / 512^2 / llvm_ad_rgb / path / orthographic (50x52) + 評価 : configs/eval_baseline_dng_target_shape.yaml(shape_metrics 込み) + +球との対応(内面積を揃える解釈): + 球 r=88/100/135/170/250 の内面積は L=126/142/188/234/341 の立方体に相当する. + スイープはキリの良い L で振り,比較は内面積を横軸にして行う. + +実行方法: + uv run python scripts/sweep_cube_size.py --prepare-only # メッシュ + YAML のみ + uv run python scripts/sweep_cube_size.py --sizes 120 160 200 280 360 +""" + +from __future__ import annotations + +import argparse +import copy +import csv +import subprocess +import sys +import time +from pathlib import Path + +import numpy as np +import trimesh +import yaml + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) + +from generate_cube_housing import build_cube_housing # noqa: E402 +from sweep_sphere_radius import ( # noqa: E402 + BAFFLE_GAP, + BAFFLE_MARGIN, + BAFFLE_THICKNESS, + BOARD_CORNER, + BOARD_HALF, + BOARD_SURFACE_Z, + EVAL_CONFIG_YAML, + LED_RADIUS, + LED_WALL_STANDOFF, + RIM_INSET, + RIM_Z, + TEMPLATE_YAML, + THICKNESS, + raw_stats, + read_metrics_csv, +) + +ROOT = Path(__file__).resolve().parents[1] + +TMP_DIR = ROOT / "configs" / "_tmp_cube_size" +MESH_DIR = TMP_DIR / "meshes" +OUTPUT_BASE = ROOT / "output" / "cube_size_sweep" +RUN_DIR = "board_baffle" + +# 評価板 99×102mm が上面に収まる下限は L=102.天井の余白(Y 側片 9mm)を考え 120 から振る +SIZES = [120.0, 140.0, 160.0, 180.0, 200.0, 240.0, 280.0, 320.0, 360.0] + +# 板の実面積とリム開口(板輪郭から RIM_INSET 内側)の面積(mm^2).内面積の集計に使う +BOARD_AREA = (2 * BOARD_HALF[0]) * (2 * BOARD_HALF[1]) +RIM_OPENING_AREA = (2 * (BOARD_HALF[0] - RIM_INSET)) * (2 * (BOARD_HALF[1] - RIM_INSET)) + +SUMMARY_COLUMNS = [ + "size_mm", + "led_z_mm", + "baffle_radius_mm", + "cavity_area_mm2", + "board_area_ratio", + "elapsed_s", + "raw_mean_roi", + "raw_mean_full", + "raw_rel_std_roi", + "cov", + "center_periphery_ratio", + "gradient_magnitude", + "radial_min_max_ratio", + "angular_ratio", + "pca_eccentricity", + "intensity_centroid_offset_norm", + "max_min_ratio", +] + + +def led_z(size: float) -> float: + """LED 円板の Z 座標(底面 z=-L から壁面密着分だけ手前)を返す.""" + return RIM_Z - size + LED_WALL_STANDOFF + + +def job_name(size: float) -> str: + """一辺の長さからジョブ名(ディレクトリ名)を作る.""" + return f"L{size:g}" + + +def baffle_radius(size: float) -> float: + """評価板の全点から LED を隠すのに必要なバッフル円板の半径(mm)を返す. + + 導出は sweep_sphere_radius.baffle_radius と同一(LED と板の内分点による厳密上界). + LED_z と板の外接円だけで決まる式なので,キャビティが球でも立方体でも変わらない. + """ + f = BAFFLE_GAP / (BOARD_SURFACE_Z - led_z(size)) + return BAFFLE_MARGIN * ((1.0 - f) * LED_RADIUS + f * BOARD_CORNER) + + +def build_baffle_mesh(size: float, dest: Path) -> trimesh.Trimesh: + """LED 直前のバッフル円板(薄い円柱ソリッド)を生成して書き出す. + + 半セクタ回転は球版と同じ理由(蓋の三角形ファンのエッジを XZ / YZ 対称面から外し, + 遮蔽検証レイの数値的なすり抜けを防ぐ). + """ + sections = 128 + mesh = trimesh.creation.cylinder( + radius=baffle_radius(size), height=BAFFLE_THICKNESS, sections=sections + ) + mesh.apply_transform(trimesh.transformations.rotation_matrix(np.pi / sections, [0, 0, 1])) + mesh.apply_translation([0.0, 0.0, led_z(size) + BAFFLE_GAP]) + dest.parent.mkdir(parents=True, exist_ok=True) + mesh.export(dest) + return mesh + + +def build_mesh(size: float, dest: Path) -> trimesh.Trimesh: + """指定サイズの立方体筐体 STL を生成して書き出す.""" + mesh = build_cube_housing( + size=size, + thickness=THICKNESS, + board_half_x=BOARD_HALF[0], + board_half_y=BOARD_HALF[1], + rim_inset=RIM_INSET, + rim_z=RIM_Z, + ) + dest.parent.mkdir(parents=True, exist_ok=True) + mesh.export(dest) + return mesh + + +def cavity_geometry(size: float) -> dict[str, float]: + """キャビティ内面積と,評価板がそれに占める割合(開口比)を返す. + + 内面積 = 側壁 4L^2 + 底面 L^2 + 天井 (L^2 - リム開口) + 評価板. + 球版 cavity_geometry と同じ「板込みの総内面積」の定義で,比較の横軸に使う. + """ + total = 6.0 * size**2 - RIM_OPENING_AREA + BOARD_AREA + return { + "cavity_area_mm2": round(total, 1), + "board_area_ratio": round(BOARD_AREA / total, 5), + } + + +def build_scene_yaml(size: float, mesh_path: Path, dest: Path, baffle_path: Path) -> None: + """テンプレートから当該サイズのシーン YAML を書き出す. + + 材質・センサ・レンダ設定・LED の半径/radiance は一切変更しない.変えるのは + 筐体メッシュ(球 → 立方体)・LED の Z・LED の向き・バッフルだけ. + メッシュの相対パスは YAML 自身のディレクトリ基準で解決されるため絶対パス化する. + """ + config = copy.deepcopy(yaml.safe_load(TEMPLATE_YAML.read_text(encoding="utf-8"))) + config["name"] = f"cube_size_{RUN_DIR}_{job_name(size)}" + config["description"] = ( + f"立方体キャビティのサイズスイープ [board_baffle]: 内寸 {size:g}mm" + f"(天井 z={RIM_Z:g}・底面 z={RIM_Z - size:g})." + f"LED は底面中心に密着し評価板向き(+Z).バッフルで直接光を遮断." + ) + for mesh in config["geometry"]["meshes"]: + if mesh["id"] == "sphere_housing": + mesh["id"] = "cube_housing" + mesh["path"] = str(mesh_path.resolve()) + else: + mesh["path"] = str((TEMPLATE_YAML.parent / mesh["path"]).resolve()) + config["geometry"]["meshes"].append( + { + "id": "baffle", + "path": str(baffle_path.resolve()), + "unit": "mm", + "material": "printer_white", + } + ) + for light in config["lights"]: + light["center"] = [0.0, 0.0, led_z(size)] + light["direction"] = [0.0, 0.0, 1.0] + dest.parent.mkdir(parents=True, exist_ok=True) + dest.write_text(yaml.safe_dump(config, allow_unicode=True, sort_keys=False), encoding="utf-8") + + +def prepare(sizes: list[float], log) -> dict[float, Path]: + """全サイズについて筐体・バッフルのメッシュとシーン YAML を生成し,watertight を検証する.""" + scenes: dict[float, Path] = {} + log("=== 幾何生成(立方体 board_baffle)===") + for size in sizes: + mesh_path = MESH_DIR / f"立方体_{job_name(size)}.stl" + mesh = build_mesh(size, mesh_path) + if not mesh.is_watertight: + raise ValueError(f"L={size}: watertight でないメッシュが生成されました") + baffle_path = MESH_DIR / f"バッフル_{job_name(size)}.stl" + baffle = build_baffle_mesh(size, baffle_path) + if not baffle.is_watertight: + raise ValueError(f"L={size}: バッフルが watertight になりませんでした") + scene_yaml = TMP_DIR / f"scene_{RUN_DIR}_{job_name(size)}.yaml" + build_scene_yaml(size, mesh_path, scene_yaml, baffle_path) + scenes[size] = scene_yaml + geo = cavity_geometry(size) + log( + f" L={size:6.1f} LED_z={led_z(size):9.3f} " + f"内面積={geo['cavity_area_mm2']:9.0f}mm2 " + f"開口比={geo['board_area_ratio'] * 100:5.2f}% " + f"バッフル R={baffle_radius(size):5.2f}mm z={led_z(size) + BAFFLE_GAP:8.2f}" + ) + return scenes + + +def run_job(size: float, scene_yaml: Path, log) -> dict[str, float] | None: + """1 サイズをレンダ→評価まで実行して集約行を返す(失敗時 None). + + 既に metrics.csv と luminance.npy が揃っているジョブは再レンダせず再利用する. + """ + name = job_name(size) + out_dir = OUTPUT_BASE / RUN_DIR / name + row: dict[str, float] = { + "size_mm": size, + "led_z_mm": round(led_z(size), 3), + "baffle_radius_mm": round(baffle_radius(size), 3), + "elapsed_s": 0.0, + } + row.update(cavity_geometry(size)) + if (out_dir / "metrics.csv").exists() and (out_dir / "luminance.npy").exists(): + log(f"--- {name}: 既存結果を再利用") + else: + log(f"--- {name}: レンダ開始(LED_z={led_z(size):.2f})") + out_dir.mkdir(parents=True, exist_ok=True) + start = time.time() + for cmd in ( + ["uv", "run", "sm-render", "--scene", str(scene_yaml), "--output", str(out_dir)], + [ + "uv", + "run", + "sm-evaluate", + "--image", + str(out_dir / "luminance.npy"), + "--config", + str(EVAL_CONFIG_YAML), + "--output", + str(out_dir), + ], + ): + # encoding を明示する.既定では Windows の cp932 で復号され, + # sm-render が出す日本語ログで UnicodeDecodeError になる(球スイープで実測) + result = subprocess.run( + cmd, + cwd=ROOT, + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + ) + if result.returncode != 0: + log(f" !! 失敗 ({cmd[2]}): {result.stderr[-800:]}") + return None + row["elapsed_s"] = round(time.time() - start, 1) + row.update(read_metrics_csv(out_dir / "metrics.csv")) + row.update(raw_stats(out_dir / "luminance.npy")) + log( + f" -> {row['elapsed_s']:.0f}s CoV={row['cov']:.4f} " + f"angular_ratio={row['angular_ratio']:.4f} C/P={row['center_periphery_ratio']:.4f} " + f"勾配={row['gradient_magnitude']:.2f}% 生mean={row['raw_mean_roi']:.4f}" + ) + return row + + +def write_summary(rows: list[dict[str, float]]) -> None: + """集約 CSV をサイズ昇順で書き直す(途中終了でも最新状態が残る).""" + summary_csv = OUTPUT_BASE / RUN_DIR / "summary.csv" + summary_csv.parent.mkdir(parents=True, exist_ok=True) + with summary_csv.open("w", newline="", encoding="utf-8") as fh: + writer = csv.DictWriter(fh, fieldnames=SUMMARY_COLUMNS) + writer.writeheader() + for row in sorted(rows, key=lambda r: r["size_mm"]): + writer.writerow({k: row.get(k, "") for k in SUMMARY_COLUMNS}) + + +def main() -> int: + """サイズスイープを実行するメインエントリポイント.""" + parser = argparse.ArgumentParser(description="立方体キャビティのサイズスイープ") + parser.add_argument( + "--prepare-only", + action="store_true", + help="メッシュとシーン YAML の生成だけ行う(3D 配置確認用)", + ) + parser.add_argument( + "--sizes", + type=float, + nargs="*", + default=None, + help="一辺の長さリスト(mm).省略時は既定グリッド", + ) + args = parser.parse_args() + + sizes = args.sizes if args.sizes else SIZES + log_path = ROOT / "output" / "_sweep_cube_size.log" + log_path.parent.mkdir(parents=True, exist_ok=True) + log_file = log_path.open("a", encoding="utf-8") + + def log(message: str) -> None: + print(message, flush=True) + log_file.write(message + "\n") + log_file.flush() + + log("=== 立方体キャビティ サイズスイープ(board_baffle)===") + scenes = prepare(sizes, log) + if args.prepare_only: + log(f"--prepare-only のため終了.シーン YAML: {TMP_DIR}") + log_file.close() + return 0 + + rows: list[dict[str, float]] = [] + for size in sizes: + row = run_job(size, scenes[size], log) + if row is not None: + rows.append(row) + write_summary(rows) + + log(f"\n=== 完了: {len(rows)} 条件 ===") + log(f" 集約 CSV: {OUTPUT_BASE / RUN_DIR / 'summary.csv'}") + log_file.close() + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/uv.lock b/uv.lock index 29a64cd..76b9053 100644 --- a/uv.lock +++ b/uv.lock @@ -229,25 +229,21 @@ [[package]] name = "drjit" -version = "1.3.1" +version = "1.2.0" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/59/ad6676b8489bac52f414d95d3c1f04282b1c7f0b9605546bc1e93e0819d9/drjit-1.3.1-cp312-abi3-macosx_11_0_arm64.whl", hash = "sha256:15c80f88a9d61a243ef11a120435d1809c01fd30010367b00869a26c76ee6032", size = 3370045, upload-time = "2026-02-23T09:02:27.689Z" }, - { url = "https://files.pythonhosted.org/packages/a1/55/df14df15a0985d1df614a2f8d0bc88b761f99513e5ae082ea17930297245/drjit-1.3.1-cp312-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6664819fa67165d8241a9a83631e1f86a49192fa8d03890cce4830fdef8d85be", size = 4836430, upload-time = "2026-02-23T09:02:29.311Z" }, - { url = "https://files.pythonhosted.org/packages/83/f2/66137ea23e4b592b366edb3db30e065f2bb5b680e2276b1d2ec581049d5f/drjit-1.3.1-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3fe4207ac5382e15e66ae4a22557e5087f6ff09cf09d3b8eda682e08cbb85fc", size = 4533222, upload-time = "2026-02-23T09:02:30.479Z" }, - { url = "https://files.pythonhosted.org/packages/dd/20/f71d81a7e33925a9e63e2d612b10b0b77edbc7cb0dfa4b823645c7aa96db/drjit-1.3.1-cp312-abi3-win_amd64.whl", hash = "sha256:d667ae91a390ecb04a69071a18641b2b5e583cbcab1b67a03ff69565bf7a89f5", size = 4099930, upload-time = "2026-02-23T09:02:31.803Z" }, - { url = "https://files.pythonhosted.org/packages/b1/c6/5db4c4bfa35d4e88278990164fa55096242b5b95a5e813f975d2c4d94482/drjit-1.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8533ba00f2b1c116475631c7a74072436eaa9504833e3d35ebe18cbf73f83b05", size = 3344444, upload-time = "2026-02-23T09:02:33.032Z" }, - { url = "https://files.pythonhosted.org/packages/0a/b4/9c605027284392b201243089ab39829602d6e6a5ad24421eddebe897bfa5/drjit-1.3.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:14bcd7759dc67d7c2b9fb407af635c65f0aa4d1ea16819366c9e26bf46a236d4", size = 4835272, upload-time = "2026-02-23T09:02:35.034Z" }, - { url = "https://files.pythonhosted.org/packages/2a/86/fd461d1106176adbd72b37809c219226b27e38e7d0d0fa683a5399d71502/drjit-1.3.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7382972c0fa46f335db887dbecd583f7b6bfa67f4b9a8e8dc3681ad033724057", size = 4519842, upload-time = "2026-02-23T09:02:37.319Z" }, - { url = "https://files.pythonhosted.org/packages/f1/60/4f9a5901bc18e88ea314d6365294a008db963656f1fd144bf13b9596dd86/drjit-1.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:6d95f6be04e6bbd3f2c4986ba4966129ea828640652b2b168ad8748cede90591", size = 4096337, upload-time = "2026-02-23T09:02:38.781Z" }, - { url = "https://files.pythonhosted.org/packages/ca/bb/79459303cae77d460b59aaed3537e079a413f56ec9dd0e083241d0264a4d/drjit-1.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:456726d4fc691fd3539bbef2c3e8ef2b7deadc11f9babb464e6519f181cb23c9", size = 3343948, upload-time = "2026-02-23T09:02:40.245Z" }, - { url = "https://files.pythonhosted.org/packages/f6/10/54c04152964661fe98a48b34b5f7a74bce63fa4c2a12d49bdb9bac257e99/drjit-1.3.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2845e372335e02cc1e920262d345a0c69cce79798a9a5dad7dc3b72f33549a34", size = 4835090, upload-time = "2026-02-23T09:02:41.675Z" }, - { url = "https://files.pythonhosted.org/packages/21/3d/16a52136b3c961ced81e6031d04b8041895d8403a440c5596662fd30a08f/drjit-1.3.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3946702ebad62fca60c9b734af40f6aa615655ab0d822ba8dda60a8e02886936", size = 4519622, upload-time = "2026-02-23T09:02:43.089Z" }, - { url = "https://files.pythonhosted.org/packages/20/6f/1d79672b1b5eb2f8eb220fe054b0cb3dde4c10c973a26cc9e2b973cb1146/drjit-1.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:a26e395e9f8d0e084768eeb7183b047ae425ef30a5eecf262baf60d4114e2a31", size = 4097374, upload-time = "2026-02-23T09:02:44.329Z" }, - { url = "https://files.pythonhosted.org/packages/e9/8d/b395c9635a7d5da189643a275472df6ab85119361d54420847252f0012b6/drjit-1.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9dd03dfa66d550ec39e196d13e71835d21e01a68eb5c6af5ef1f8f149637a655", size = 3347581, upload-time = "2026-02-23T09:02:45.506Z" }, - { url = "https://files.pythonhosted.org/packages/1b/e1/2b50043fcdad216ae66120a4f3e3592c9ebe4c9538d19eea32006c52fbf8/drjit-1.3.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:031af772ef614d1c596331ecfc4f224a7d62947251dc1e707a953e5e15218b70", size = 4836109, upload-time = "2026-02-23T09:02:46.581Z" }, - { url = "https://files.pythonhosted.org/packages/41/44/122fabade60ed015835be0497ed1b5cc300bcb5a17fd233de3b7048831cb/drjit-1.3.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a15d4ddc9aaeca3fbe16402ef56341f7046354a2f312bc1a31da90d177a18b35", size = 4520961, upload-time = "2026-02-23T09:02:47.887Z" }, - { url = "https://files.pythonhosted.org/packages/c3/eb/f9f135ea59409b285f821dae782a4ff0b6fe9a35ad48fa345bf56466c130/drjit-1.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:31003a83856580fd98f2f4943f5d987b6f03dfa4eef3fbf97cc5f20c5b769027", size = 4202961, upload-time = "2026-02-23T09:02:49.294Z" }, + { url = "https://files.pythonhosted.org/packages/42/91/a6e729debde26f7a8fa0c98f4b20bdbbe8db53305943a7a715ca3797e940/drjit-1.2.0-cp312-abi3-macosx_10_14_x86_64.whl", hash = "sha256:5a5a070acf1b228c9f3b755a9b08a2f5fd1e8b67ddd4b9d4762fb4154a00d51d", size = 3934394, upload-time = "2025-09-17T13:32:40.573Z" }, + { url = "https://files.pythonhosted.org/packages/87/56/d3963cc44db6a40affe2d3402fc2f03f000e4582ae3f082d4ee2fb746e32/drjit-1.2.0-cp312-abi3-macosx_11_0_arm64.whl", hash = "sha256:5571b785be8ffb7c84d971a8c479e8c56af84207c75fce0da71d5827c003f92c", size = 3326716, upload-time = "2025-09-17T13:32:42.086Z" }, + { url = "https://files.pythonhosted.org/packages/fe/7c/976831406ebb4c432d63a34b95ccd010bd51450c798d12c8f1a2040f54f7/drjit-1.2.0-cp312-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4888569b5db6f75e9eedbfdbfba775a8d3d27ed338cbabdd175b73f125cd7237", size = 4987244, upload-time = "2025-09-17T13:32:43.343Z" }, + { url = "https://files.pythonhosted.org/packages/60/57/ea75562289df726748ef8ca275d6328bfbbe45ce1bfa5d20c2de19deff5a/drjit-1.2.0-cp312-abi3-win_amd64.whl", hash = "sha256:085de7d7e9fd3b46fde2ded13c49bcfa79c99bd8f36c7dea3889376f776f2849", size = 4063578, upload-time = "2025-09-17T13:32:44.737Z" }, + { url = "https://files.pythonhosted.org/packages/a1/04/b369d1899be07ae215bd6e4c3f2b83e7947df6185de93bbf99285b2cae92/drjit-1.2.0-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:6407337f812f85247f2e9fb0d3801436f93de164d1b72352232609b54d9d646a", size = 3923863, upload-time = "2025-09-17T13:32:45.861Z" }, + { url = "https://files.pythonhosted.org/packages/f6/14/654da7679070b297a99911be107e7b8ae8cf998c8ba8ddd6c42a286349c2/drjit-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39a28f2875f776978d6767aef7e058bda684c333cb374e42790ed46d0307f56f", size = 3304422, upload-time = "2025-09-17T13:32:47.297Z" }, + { url = "https://files.pythonhosted.org/packages/35/66/3a95a2293bd8f7ffbaf31dff37ef894ef60eb137f1d8562f0f326e364aa9/drjit-1.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4ddc78ce78453cecf6e877fbd7ea937f53f966af586d10265af64b0d85c21ec", size = 4997250, upload-time = "2025-09-17T13:32:48.56Z" }, + { url = "https://files.pythonhosted.org/packages/45/01/8cc159722ea9c728d9ce3a38fc7814ba8b485a03fb0a6ff9bbfb8b3916b8/drjit-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:cac9b8b90b27e49ab2effc3eeeff0bfdb5574acca619ed78721ce8670a6b7ef6", size = 4051105, upload-time = "2025-09-17T13:32:49.815Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b1/02e7bb2bd1a3ea9b6433666ca4c1d4919cee3a431807be1c3e87f6507340/drjit-1.2.0-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5110d4f4e1ec89b3dce5de610eea244c318d398df0a31c18597c288a29373121", size = 3923888, upload-time = "2025-09-17T13:32:51.869Z" }, + { url = "https://files.pythonhosted.org/packages/d4/cf/f77c0a7397f85455a6eb69e4580c0c99636f8fc044cd4644413148464a54/drjit-1.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fb0607d6b7b01da2d0a8d6a3c727baabde3e91fec391d53f088b66bf18a24d51", size = 3304345, upload-time = "2025-09-17T13:32:53.057Z" }, + { url = "https://files.pythonhosted.org/packages/49/b9/85e07502a42393e32add4184a650acabe9b645742e3c990d8799fe77094d/drjit-1.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1808dd43bf6d80d862bd62651ac8e55a6b69f19288d95d187789a6601243b99", size = 4997239, upload-time = "2025-09-17T13:32:54.593Z" }, + { url = "https://files.pythonhosted.org/packages/c0/6b/16b72889c8e92d014830eeba0dbc19ea099030342e95d2969e9f6b2a165c/drjit-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:85fce1f4ad269d66fa6c9721efb272c6b0c20fe78d7b916560748a051c4c63cb", size = 4050629, upload-time = "2025-09-17T13:32:56.209Z" }, ] [[package]] @@ -497,28 +493,24 @@ [[package]] name = "mitsuba" -version = "3.8.0" +version = "3.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "drjit" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/85/90fefee087141a3ef50c725dee9b0479d07dfc352a8476388c05b5226957/mitsuba-3.8.0-cp312-abi3-macosx_11_0_arm64.whl", hash = "sha256:c1f3f3ed87925f97572e99b762c7bd999ac2227ad7493b5f88567a97468f2a23", size = 37574705, upload-time = "2026-02-23T16:06:50.18Z" }, - { url = "https://files.pythonhosted.org/packages/77/26/609dcb0998b994f05102a2b6c41634092cf00c475b7286c4e3c8593e9e46/mitsuba-3.8.0-cp312-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9c826a7d934d73b59db17591c390de69ddfa1c91473541794a6a433cbb710fe6", size = 53989259, upload-time = "2026-02-23T16:06:53.57Z" }, - { url = "https://files.pythonhosted.org/packages/b6/b4/4cfc7ea1305461acc6145852c1073da8422f64da229be3e5fe1ea52931a5/mitsuba-3.8.0-cp312-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:991cd863bb47afc5b885117bb2d90750d75f46dbea41007e47bb24cd8767de0e", size = 63046965, upload-time = "2026-02-23T16:06:57.05Z" }, - { url = "https://files.pythonhosted.org/packages/52/d8/3a69c5d061d0e7f749356fe9298d1cb6c91a085ee4dece600f817ca7b18c/mitsuba-3.8.0-cp312-abi3-win_amd64.whl", hash = "sha256:f30621af86bd6d99374940f16c193d812855cc54d774dd43220c02ac6628d04b", size = 46450881, upload-time = "2026-02-23T16:07:00.304Z" }, - { url = "https://files.pythonhosted.org/packages/1b/f0/0198ac7d1dc8769a4b6a8d6bcee1cfa598bee2c77cd8da412ed73b763ca4/mitsuba-3.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c3b15649b81d85d9796ad27a25b4beaf4139a776e0c02cdb6a1461a62b887416", size = 37500823, upload-time = "2026-02-23T16:07:03.019Z" }, - { url = "https://files.pythonhosted.org/packages/4f/61/af29ff70fa07c53eac67ca731e8a67d1cf3a7d735837280a5fd24994e04e/mitsuba-3.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:212f5a19df757dad12a782aeaa0a4764f9be375091c2badb0734ffbe3c1507b6", size = 53949828, upload-time = "2026-02-23T16:07:05.605Z" }, - { url = "https://files.pythonhosted.org/packages/d2/bb/ddd5260b29db01941aa3d6ac82e391445ba69b237381e10a9f5eb444f959/mitsuba-3.8.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ab3f290bba4576cd0951a9bb71a9907d12a6b74a9ae8df2af2ad7ee48f781a2b", size = 62977153, upload-time = "2026-02-23T16:07:14.258Z" }, - { url = "https://files.pythonhosted.org/packages/a7/8b/aa6db9f8572dbf2f17575399d7ac9c50a053b9c77d94ee6955289aa6331b/mitsuba-3.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:aa5d2a3212868aec0f0d4f40c962c8c88e576ac3d2297cff1b729e3d2b91c475", size = 46457053, upload-time = "2026-02-23T16:07:17.661Z" }, - { url = "https://files.pythonhosted.org/packages/b5/3b/d6e3562c0c544138a8385cf6badee6f551b7c76ef9f77caa4a52dcf75f5b/mitsuba-3.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:159a3a45147acdcd22a8c0aa2d37beb0ca000104cfc497533568d27022950549", size = 37500855, upload-time = "2026-02-23T16:07:20.897Z" }, - { url = "https://files.pythonhosted.org/packages/74/7b/c600e185618d568442a7f15b66afc84c2de419721c5a3977f7dd9720f42d/mitsuba-3.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:72fd23a41dc3db6edd52b5b4551bac49164fed2e098f517b8ad598f5cb454d1c", size = 53949983, upload-time = "2026-02-23T16:07:24.399Z" }, - { url = "https://files.pythonhosted.org/packages/64/71/33a5d33b1f3f3dd856dd65be10692c621a8e9f26a7f3aaaf24933f5f869e/mitsuba-3.8.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e7fb4b1eb9943dfdc0bfb64ee2f1e87382ffa1954b1b948dd9d166e7e705df6a", size = 62976950, upload-time = "2026-02-23T16:07:27.259Z" }, - { url = "https://files.pythonhosted.org/packages/8a/b4/c712a4081a3892f5ca27f8584738b8ced40170d6621de20e6a5350a1e756/mitsuba-3.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:d94a0883906a0d1493b2b6c37e7f7a9b61676e30e65752d747984de6567c34fe", size = 46456629, upload-time = "2026-02-23T16:07:30.053Z" }, - { url = "https://files.pythonhosted.org/packages/ac/8e/f820d02fb006e3e50a61705e53600213d6d5ee9b2f1b865e9fe4960b155f/mitsuba-3.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ed35a166148879635f1a0f7b09103864ea2abe17570dddf9cbc4631b5fa5bec6", size = 37509793, upload-time = "2026-02-23T16:07:32.702Z" }, - { url = "https://files.pythonhosted.org/packages/fe/85/2cf9c909d2fd17ea1f1b6971d93657ee4a047574b41b45594b7c542bb121/mitsuba-3.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:85acc8085aa06f25a5452590021c9c7cb75976f2d41563e7d34b07015a740295", size = 53958320, upload-time = "2026-02-23T16:07:35.792Z" }, - { url = "https://files.pythonhosted.org/packages/b2/2d/69238f42531ae8eaef8a9b279d4ad025e6d260dcb18976906bc2ec7001e2/mitsuba-3.8.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:812fcd3bcc11db2fc735a7f201a71f0ec2679671d6560a4591dd01ad6271a78d", size = 62981883, upload-time = "2026-02-23T16:07:38.74Z" }, - { url = "https://files.pythonhosted.org/packages/5c/a0/22b8eb1774545cae5f45c708e27f187d92095ee2088ce5e63329e1f1b4ff/mitsuba-3.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:314aba39467273d52e7eff277bd25f7c524e514d2f907645f52f22e83a7f566e", size = 47424301, upload-time = "2026-02-23T16:07:41.639Z" }, + { url = "https://files.pythonhosted.org/packages/81/6a/729cf049c19104e22c9f9687ede34796c371d3ac8c4a6c5eb6af02784594/mitsuba-3.7.1-cp312-abi3-macosx_10_14_x86_64.whl", hash = "sha256:f2fa3d40906e7d3ed59beb3e8b009eedf72d6b0e9aebe1bfa808c133e0ec9e55", size = 33042933, upload-time = "2025-09-17T19:39:37.495Z" }, + { url = "https://files.pythonhosted.org/packages/0d/94/a8a1535e4b1aa6f71deac9bb63ee0458b4a63d83b573dd5c694eeb9709f8/mitsuba-3.7.1-cp312-abi3-macosx_11_0_arm64.whl", hash = "sha256:8af4b18f474a8f9cc0b57cad68986a63ab47c7032b6953b5541e55a202bd2eb1", size = 26598772, upload-time = "2025-09-17T19:39:40.284Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b0/1df133a3745fd021bc6a3bd47976f947c17d3af53cc766416385a07baefa/mitsuba-3.7.1-cp312-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:97135d32537f397c93022b1dc878523b4a44c71b0ed3d2f3917a685920dc34fa", size = 39477720, upload-time = "2025-09-17T19:39:43.716Z" }, + { url = "https://files.pythonhosted.org/packages/6d/0a/644754ac0268fe1da89e97c9c6b4069c7f7a93770b12e7191331fe37066c/mitsuba-3.7.1-cp312-abi3-win_amd64.whl", hash = "sha256:205690cf22cae534783210ba77a2aaada59520989eee4f862b4a62da8409b0b6", size = 30886426, upload-time = "2025-09-17T19:39:47.21Z" }, + { url = "https://files.pythonhosted.org/packages/67/5b/842b9e2848cb6173f9fcb9dd8da1b98a7f5120c4c4e7768262d10d4b2c94/mitsuba-3.7.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:1deaa708058fc37661375b2146b7c1d87f455f5c3841b5cbb27bf3bd9766ff89", size = 45047403, upload-time = "2025-09-17T19:39:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/0b/62/46b6dc5c0700fee99b12957e1931957c0bb5d03e1e21a4f1d1e5b9cc9509/mitsuba-3.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ca812c1aaab576d25e67ba6ea3d76fa540dc977369ce660c2f14bb140d16d1ef", size = 36946700, upload-time = "2025-09-17T19:39:54.093Z" }, + { url = "https://files.pythonhosted.org/packages/d2/57/2ffa6af93c18794275732e6bd01b3b6763a2a6f17f439f5ef3cf50e1a8b8/mitsuba-3.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:32590fcca2fc18d228fa73aae5fe2453b0a5f0357150dc8983334924fb578065", size = 63046035, upload-time = "2025-09-17T19:39:58.084Z" }, + { url = "https://files.pythonhosted.org/packages/31/7a/36277ae9d3a36f2085afd938424a4b8be4099d18af90ea2054d9c4041659/mitsuba-3.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:8c12154d02caa9c5de94e552a1a00df37aea0a6f234df82afc7be8d821d1d5a0", size = 45256364, upload-time = "2025-09-17T19:40:03.304Z" }, + { url = "https://files.pythonhosted.org/packages/68/42/583df3a181639c5d85fc59c04fc305a34fb62798a6442c3c55c6d202f650/mitsuba-3.7.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:f03acb1c584e40db4b4931e5789ee060a7954cdac10c8b1344bdcaa27d7b4a5e", size = 45048174, upload-time = "2025-09-17T19:40:07.022Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/4a99b4a55ff866ae591e6cc1f600c736cab9d9a1f9e0b48bf0b6239b2d73/mitsuba-3.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f22d29c46b199ebdc1d841999e9ca914cb15968ca243bbf7fadc32083443d01", size = 36946365, upload-time = "2025-09-17T19:40:11.46Z" }, + { url = "https://files.pythonhosted.org/packages/13/35/73b554d0623c305dbb00055b750bdf9818ba820c333b6e41c6261714fbcb/mitsuba-3.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d0d313bab408f22a803c358b6e2400381584e62be7af14ec5c7bc2e392af4ab4", size = 63045439, upload-time = "2025-09-17T19:40:15.008Z" }, + { url = "https://files.pythonhosted.org/packages/37/ab/05eb342bfa79454e813391ed1d87c59e0c6adad800cdae0b261fa32dcb43/mitsuba-3.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:e6b768abe8e1537e56ebc65cb890bcb1ce4fa2a09b7cd04ebde845f73ff4d401", size = 45255737, upload-time = "2025-09-17T19:40:18.487Z" }, ] [[package]] @@ -1227,7 +1219,7 @@ [package.metadata] requires-dist = [ { name = "matplotlib", specifier = ">=3.10.9" }, - { name = "mitsuba", specifier = ">=3.8.0" }, + { name = "mitsuba", specifier = "==3.7.1" }, { name = "numpy", specifier = ">=2.4.4" }, { name = "opencv-python", specifier = ">=4.13.0.92" }, { name = "pandas", specifier = ">=3.0.2" },