Newer
Older
SmTIAS-LightSim / docs / 06_TEST / TEST_13_物理モデル健全性の一括監査.md

物理モデル健全性の一括監査 (Physical Model Sanity Audit)

本ドキュメントは,TEST_12 と同じ 「エラーも警告も出ないまま結果だけが静かに歪む」故障モードが他に残っていないかを一括で 洗い出し,見つかった問題の修正と再発防止テストの常設化を行った記録である(2026-08-30).

動機 (Motivation)

TEST_12 のバグ(PLY 自動スムーズ法線 × 評価板の巨大三角形)は,CoV の 6 割超を占める 系統誤差を生みながら Step 8-13 まで気づかれなかった.損失は計算コストではなく, 間違った前提の上に積み上げた考察と設計判断そのものだった.同種の故障は TEST_08(面光源スケール軸バグ)でも起きており, そのときは TEST_07 が誤った出力を「物理的に正しい」と 一度は正当化してしまっている.

このパターンは 2 度繰り返されている.3 度目を防ぐため,レンダリング規約・幾何・単位系・ エネルギー収支を横断的に検査し,検出力を実証したテストだけを通常実行に常設した.

監査結果のまとめ (Summary)

#監査項目結果対応
1シェーディング法線 == 幾何法線健全(実測逸脱 0.044 度)再発防止テストを追加(負のコントロール付き)
2片面 BSDF による無警告の黒化潜在的脆弱性ありtwosided ラップで恒久修正
3メッシュ法線の向き健全(裏面ヒット 0 件 / 全 15 STL の巻き順一貫)監査関数と回帰テストを追加
4絶対放射量(単位系・反射率・光源パワー・センサ)健全(解析解と 0.2% 一致)積分球テストを常設
5多重反射の打ち切り(max_depth)問題あり(基準設計シーンで 9.7% のエネルギー欠損)警告を実装.設定変更は要判断
6面光源のサイズ→軸マッピング(TEST_08 の再発)健全だがテストが存在しなかった実効的な回帰テストを追加
7レンダ経路のテスト(slow 層)長期間全滅していたLLVM 初期化順序のバグを修正
83D 可視化のテスト(slow 層)全滅(Smart App Control が VTK の DLL をブロック)vtk==9.4.2 に固定して解消.import 監視テストを常設

検査 1・3 — 法線の健全性 (Normals)

読み込み済み Mitsuba シーンの空洞内部からレイを飛ばし,si.sh_frame.n(シェーディング法線)と si.n(幾何法線)を実測比較する監査関数 scene/audit.py: audit_scene_normals を追加した.

基準設計シーン(configs/scene_baseline.yaml,実 STL 8 個)での実測:

  • ヒット 26,853 本に対し シェーディング法線と幾何法線の最大なす角 0.044 度 (=浮動小数点誤差のみ.face_normals: True が Mitsuba レベルで実効)
  • 裏面ヒット 0 件(空洞内壁の法線は全て内向き)
  • 全 15 STL について巻き順一貫・退化三角形ゼロ・体積正を確認

既存の再発防止テストは scene dict に face_normals キーが存在することしか見ていなかった. 本監査は実際にレンダラへ読み込ませて挙動を測るので,キーの意味が変わった場合も検出できる.

負のコントロール: face_normals を外すと逸脱が 30 度以上に跳ね上がることをテストで確認している. 検出力を実証できない検査は再発防止にならないため,この対照実験は必ず維持する.

検査 2 — 片面 BSDF による無警告の黒化 (One-sided BSDF)

Mitsuba の diffuse / roughconductor は片面 BSDF であり,法線が裏返った面は 入射光を一切反射せずエラーも警告もなく真っ黒になる.SmTIAS は LED 直接光が評価板に 届かない間接照明のみの構成(SPEC_03)なので, 内壁が 1 面でも黒化すれば輝度分布が静かに壊れる.

現行の STL は全て法線が正しいため実害は出ていなかったが,STL を差し替えた瞬間に 発現しうる「気づけない」構造だった.積分球の解析解で定量化した結果:

法線BSDF観測放射輝度解析解比
内向き(正常)diffuse0.4072800.9996
内向き(正常)twosided0.4072800.9996(完全に同一=無影響)
外向き(裏返り)diffuse0.0000000.0000
外向き(裏返り)twosided0.4074751.0001

修正: scene/materials.py に build_scene_bsdf_dict を追加し,不透明反射材 (diffuse / roughconductor / 不透明のみで構成される blendbsdf)を twosided で包む. 透過を伴う誘電体は両面挙動が本質なのでラップしない.

実シーン(scene_baseline,spp=256 / max_depth=16)での修正前後比較は mean +0.015% / CoV -0.009%(MC ノイズの範囲内)で,過去の結果に影響しないことを確認した.

検査 4 — 絶対放射量の検証 (Absolute Radiometry)

内面反射率 rho の閉じた球殻(積分球)は厳密な閉形式解を持つ.全放射束 Phi の光源を 内部に置くと壁面の全照度は E = Phi / (A (1 - rho))(A = 4 pi R^2)となり, 直接光が届かない領域の壁面放射輝度は L = rho^2 Phi / (pi A (1 - rho)) になる.

これを 1 本のテストで突き合わせると,単位系(mm→m)・反射率・発光面の放射束・ センサ校正・多重反射の収束をまとめて検証できる.いずれかが壊れていれば一致しない.

実測(rho=0.8・R=100mm・発光面 4x4mm・max_depth 収束後):

max_depth観測 L解析解比
80.3000610.7365
160.3888040.9543
320.4061550.9969
640.4066300.9980
1280.4066300.9980

収束値は解析解の 0.998(残差 0.2% は icosphere が内接多面体であることによる面積欠損). パイプラインの絶対放射量は正しいと結論できる.

検査 5 — 多重反射の打ち切りバイアス (Truncation Bias)

上表が示すとおり max_depth の打ち切りは無視できない.基準設計シーンは LED 直接光が評価板に届かない間接照明のみの構成であり,輝度は白筐体内壁 (rho=0.85)の多重反射の累積だけで決まるため,影響はより大きい.

configs/scene_baseline.yaml(spp=256)での実測:

max_depthmeanCoV最深値比(mean)
812.6340.260350.6176
16(現行設定)18.4760.252700.9032
3220.3340.251810.9940
6420.4570.251811.0000

現行の max_depth=16 は収束値の 90.3% しかエネルギーを拾えていない(約 10% の過小評価).

ただし CoV への影響は +0.35% と軽微である.これまでの検討の主題は形状指標 (CoV・勾配・C/P・NCC)であり,絶対輝度は実測へのアンカー(mean anchor / OETF 露出ソルバ)で 吸収されるため,過去の形状に関する結論は影響を受けない.

一方で以下には影響する.

  • LED radiance の校正値は打ち切り分を含んだ値になっている(自己整合的ではあるが物理的な絶対値ではない)
  • 反射回数の分布が異なる筐体形状どうしの絶対輝度比較(現行筐体 vs 球ロフト等)には系統差が乗る

対応: scene/audit.py: check_truncation_bias を実装し,build_scene 時に 警告ログを出す.基準設計シーン(rho=0.85)では以下が出力される.

render.max_depth=16 は最大反射率 0.850 の多重反射を拾いきれていません
(相対エネルギー欠損の見積もり 8.7%).絶対輝度が系統的に過小評価されます.
max_depth >= 30 を推奨します.

欠損の見積もり式 rho^(max_depth - 1) は実測とよく一致する(積分球 rho=0.8: 予測 3.5% / 実測 4.6%, 基準設計 rho=0.85: 予測 8.7% / 実測 9.7%).

⚠ configs/*.yaml の max_depth は未変更.値を上げると絶対輝度の基準が動き LED radiance の再校正が必要になるため,設定変更は人間の判断に委ねる. 計算コストは max_depth=16 で 80 秒 / 64 で 96 秒(spp=256・512x512)と +20% 程度.

検査 6 — 面光源のサイズ→軸マッピング (Area-Light Axis Mapping)

TEST_08 のバグ(発光面の高さを縦 Y ではなく奥行き Z に入れていた)に対する 実効的な回帰テストが存在しなかった.既存テストは to_world キーの存在と, position_scale を変えたときに文字列表現が変わることしか検証しておらず, 軸を入れ替えても検出できない.

単位図形の頂点を実際に to_world で変換し,世界座標での寸法を検証するテストを追加した (現状は健全: size=(2.8, 3.5) が X=2.8mm / Y=3.5mm / Z=0 に正しく対応).

検査 7 — レンダ経路のテストが全滅していた (Broken Render Tests)

slow マークのテストを実行したところ 47 件中 47 件が失敗していた. うち 8 件はレンダリング経路で,原因は LLVM 初期化の順序バグ.

render/runner.py は mi.set_variant の前に ensure_drjit_libllvm_path() を呼んで DRJIT_LIBLLVM_PATH を自動探索する設計だったが,scene/lights.py と scene/sensors.py が モジュール読み込み時に mitsuba を import するため,runner を import した時点で 既に Dr.Jit の LLVM 初期化が走っており,環境変数の設定が手遅れになっていた.

開発機では環境変数が User scope に永続設定されているため症状が隠れていたが, 環境変数が無い環境ではレンダリングが一切できない(ENV_02 が 案内する自動探索が機能していなかった).

修正: scene/lights.py / scene/sensors.py の mitsuba import 直前と, パッケージの __init__.py で ensure_drjit_libllvm_path() を呼ぶ.これで 8 件が復活した.

教訓: 物理的健全性の検査を slow に隔離すると,通常実行から外れて壊れても気づかない. 本監査で追加した積分球テストは 1 本 0.2 秒(32x32 / spp=128)で合計 1.5 秒程度なので, slow を付けず通常実行に含めている.

検査 8 — 3D 可視化テストの全滅 (Broken Visualization Tests)

残る slow 失敗 39 件は全て tests/visualize/ と tests/cli/test_visualize_cli.py で, sm-visualize も実行できない状態だった.

原因 — Windows Smart App Control による DLL のブロック

依存関係の破損ではなく,OS のアプリケーション制御ポリシーが VTK の DLL を弾いていた.

DLL load failed while importing vtkRenderingVolume:
アプリケーション制御ポリシーによってこのファイルがブロックされました。

開発機の Smart App Control が**オン(強制モード)**になっていた.

確認項目値
HKLM:\SYSTEM\CurrentControlSet\Control\CI\Policy\VerifiedAndReputablePolicyState1(オン・強制)
Win32_DeviceGuard.UsermodeCodeIntegrityPolicyEnforcementStatus2(強制中)

Smart App Control は署名の有無ではなくファイル単位のクラウドレピュテーションで判定する. そのため vtk 9.6.1 では 209 個の DLL のうち 14 個だけがブロックされ,残りは通るという 状態になっていた.ブロックされたのは以下(vtkRenderingAnnotation はスカラーバー・座標軸, vtkInteractionWidgets はウィジェットを担い,どちらも PyVista の描画に必須).

vtkRenderingVolume        vtkRenderingVolumeOpenGL2   vtkRenderingVolumeAMR
vtkRenderingAnnotation    vtkInteractionWidgets       vtkInteractionImage
vtkImagingColor           vtkRenderingVR              vtkRenderingOpenXR
vtkRenderingOpenXRRemoting  vtkTestingRendering       vtkViewsInfovis
vtkWebCore                vtkWebGLExporter

依存の破損でないことの根拠:

  • VTK の 153 モジュール分の .pyd はすべて正しく存在していた
  • 通った vtkRenderingCore も弾かれた vtkRenderingVolume もどちらも未署名 (Get-AuthenticodeSignature).署名の有無は判定基準ではない
  • uv.lock の vtk 9.6.1 は Step 7(2026-05-17)から変わっておらず, Dependabot による更新も入っていない

症状のメッセージが 2 通りあったことについて

最初に観測した症状は ImportError: Failed to load vtkCommonMath: No module named vtkmodules.vtkCommonCore で,上記のアプリケーション制御エラーとは別のメッセージだった.両者の関係は完全には 確定していない.事実として確認できたのは以下である.

  • vtkCommonCore はブロックされた 14 個に含まれず,単独では import できた
  • 同一環境で import pyvista が一度失敗し,のちに成功した(判定に揺れがある)
  • 一貫して再現したのはアプリケーション制御によるブロックの方

vtkmodules は独自のローダ(vtkmodules/__init__.py の exec_module)で依存モジュールを 読み込むため,DLL がブロックされた事実を握りつぶして「モジュールが無い」という 誤解を招くメッセージに化けさせた二次症状と推定しているが,機構までは確認していない. 最初の切り分けで「VTK のインストール破損」と誤診したのはこのメッセージが原因であり, ネイティブ拡張の ImportError は表示されたメッセージを額面どおり受け取らないという 教訓として記録しておく.

対処 — vtk を 9.4.2 に固定

より長く配布されレピュテーションが確立しているバージョンへ下げる方針を採った (pyvista 0.48.0 の要求は vtk>=9.2.2, <9.7.0, !=9.4.1).

vtk==9.4.2 では 147 モジュール全てが読み込め,ブロック 0 件.sm-visualize の 3D 表示(筐体・LED マーカー 18 個・センサマーカー・座標軸)も復旧し, slow の可視化テスト 54 件が全て通るようになった.

pyproject.toml に固定理由をコメントで残している(/deps-update や Dependabot が 9.6 系へ戻して静かに再破損するのを防ぐため).

Smart App Control の無効化は一度オフにすると Windows を再インストールしない限り 再びオンにできないため採用しなかった.可視化ツール 1 本のために恒久的に マルウェア保護を手放すのは釣り合わない.

再発を即座に検知する仕組み

破損は 2026-08-17 から 8-30 まで約 2 週間気づかれなかった.可視化テストが全て slow マークで通常実行から外れていたことが原因であり,検査 7(LLVM 初期化バグで レンダテストが長期間全滅していた件)とまったく同じ構図である.

そこで tests/visualize/test_vtk_import_guard.py を slow を付けずに追加した. pyvista とブロック実績のあるモジュールの import,および固定バージョンの確認のみを行い, レンダリングはしないので 1 秒未満で完了する.

採用を見送った検査 (Rejected Checks)

軸対称シーンの対称性テストは検出力を実証できなかったため採用しなかった.

立方体空洞+軸上点光源+軸上 orthographic カメラで,face_normals の有無による 象限比を比較したところ 1.0020 vs 1.0028 とほぼ差が出なかった(一方で mean は 394.4 → 300.2 と 24% 変化した).TEST_11 で異方性が顕在化したのは評価板メッシュの 非対称な三角形分割が効いていたためで,合成シーンでは再現できない.

検出できない検査をテストとして常設すると「検査済み」という誤った安心を与えるため, 対称性チェックは手動の診断手段として残し,自動テストには含めない.

追加した再発防止テスト (Added Regression Tests)

すべて通常実行(uv run pytest)に含まれる.合計約 2 秒.

テストファイル内容負のコントロール
tests/scene/test_audit.py打ち切りバイアスの見積もり・警告生成—
tests/scene/test_audit_normals.pyシェーディング法線 == 幾何法線 / 裏面ヒット 0あり(face_normals を外す・メッシュを裏返す)
tests/scene/test_materials_twosided.py不透明反射材の twosided ラップと誘電体の除外あり
tests/scene/test_lights_axis_mapping.py面光源サイズの世界座標軸マッピング(TEST_08)—
tests/render/test_physics_validation.py積分球の解析解一致・線形性・法線裏返り耐性・打ち切りあり

tests/scene/test_audit_normals.py には基準設計シーン(実 STL 8 個)に対する slow マークのテストも含む.

過去の結論への影響 (Impact on Past Conclusions)

  • 形状指標に関する結論は影響を受けない.twosided 化は無影響(MC ノイズ以内)で, max_depth の打ち切りが CoV に与える影響も +0.35% にとどまる.
  • 絶対輝度の解釈には注意が必要.現行の LED radiance 校正値は打ち切り分(約 10%)を 含んでいる.筐体形状の異なるシーン間で絶対 mean を比較する際は, max_depth を揃えるだけでなく,反射回数分布の差による系統差が残ることに留意する.

次にやること (Next Steps)

  1. max_depth を 32〜64 に引き上げるかの判断(引き上げる場合は LED radiance の再校正が必要)
  2. 残る差分=「形」(実機の斜め構造・相補性の振幅)の切り分けへ復帰