diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 31735c7..020e3b0 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -91,3 +91,5 @@ - **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. - **TEST_17 オフ軸 LED 配置の影響とサイズ依存 — 頑健性は球が勝ち軸上の優劣と逆転 (2026-09-01)**: ユーザー発案「光源が軸からずれた場合の影響と,大きいほど影響が薄まるかの検証」.LED を**深さ方向軸の中点の高さ・+X 側の壁面**に置き板中心へ向ける構成(球 similar_radial 5 半径 + 立方体 5 サイズ・材質/レンダ据え置き)を新設した.円板は壁に対し約 41〜44° 傾くため縁が壁を貫かない分だけ浮かせ,バッフルは軸対称でないので**板 41×41 × LED 円板の全直線がバッフル平面を横切る位置から数値サイジング**(+遮蔽レイキャスト 10 条件 100%.`verify_baffle_occlusion` の LED サンプルを円板の傾きに追従するよう一般化).立方体 L=120 のみバッフルが壁に 2.35mm 食い込む(板の手前縁への視線が壁とほぼ平行なため円板が壁まで届く必要がある=実物積分球の「壁付きバッフル板」と同じ解釈で殻厚以内を許容).**結果 ①(仮説成立)**: 非対称(m=1 符号付き a1・重心オフセット)はサイズとともに減衰し最大サイズで最小の約 4 割(球 -4.0→-1.6% / 立方体 -7.2→-2.3%.ノイズ対照 b1 ≤0.3% の 10〜40 倍で確実に実構造).ただしゼロにはならない.**結果 ②**: a1 は全条件で負=**明るいのは LED の反対側**.バッフル遮蔽下では一次照射キャップ(TEST_11)が反対側の壁にでき,板がそれを非対称に見込むため.**結果 ③(軸上と逆転)**: オフ軸頑健性は**球 > 立方体**.位置ずれの Δ構造CoV は球 ±0.006 以内=実質ゼロ,立方体 +0.010〜+0.033(L=160 で軸上の 2.5 倍).球の「壁上ランバート光源→球面直接照度が一様」という古典的性質の名残.**設計上の含意**: 「立方体 + 軸上配置」か「球 + 配置自由度」の二択構造.**未解決**: 非対称ピークが最小サイズでなく 2 番目(球 r=100 / 立方体 L=160)に来る非単調性の原因,ずれ量の感度曲線(軸上→壁面の中間位置).成果物: `scripts/sweep_offaxis_led.py`・`build_offaxis_comparison.py`,`output/offaxis_led_sweep/`(10 条件 + summary.csv + `offaxis_comparison.png`),記録 [TEST_17](06_TEST/TEST_17_オフ軸LED配置の影響とサイズ依存.md). + +- **発表スライド素材の作成と TEST_16 の 10 条件化 (2026-09-01)**: TEST_15〜16 の発表準備として,スライド用のクリーン図一式を `output/_slide_panels/` に生成するスクリプトを追加した(`build_slide_test15_geometry.py`=構成断面(注釈付き/文字なし・透過)・半径入れ子断面(直径 φ 表記)・接合比較,`build_slide_test15_results.py`=輪帯ピーク正規化の輝度マップ 5/10 条件版・輪帯プロファイル(英語軸ラベル),`build_slide_test16_geometry.py`=立方体版の構成断面・サイズ入れ子).あわせてユーザー要望「球と同数の 10 条件でまとまりを良くする」を受け,**立方体スイープに L=220 を追加**(内面積 291,000mm²=球 φ270 と φ340 の中間.watertight・遮蔽 55,473 本 100% を事前検証.構造 CoV 0.0147・輪帯 min/max 0.944・対球比 0.73・m=4 は −0.004%=すべて既存カーブに単調に整合).`sweep_cube_size.py` の既定グリッドを 10 条件に更新し,TEST_16・TEST_02 の表と記述を 10 条件に改訂.表記の知見: 直径は図面では「φ176」(単位なしが JIS 流),スライド・文書では「φ176 mm」も一般的=どちらでも可で一貫性が重要. diff --git a/scripts/build_slide_test15_geometry.py b/scripts/build_slide_test15_geometry.py new file mode 100644 index 0000000..5e40404 --- /dev/null +++ b/scripts/build_slide_test15_geometry.py @@ -0,0 +1,236 @@ +"""TEST_15(球体筐体実験)の発表スライド用ジオメトリ図を生成する. + +作業用の幾何確認図(build_radius_geometry_figure.py 等)はグリッド・凡例・作業タイトルで +情報密度が高く,スライドには不向き.本スクリプトは同じ断面計算を流用しつつ, +発表向けに要素を絞った 3 枚を `output/_slide_panels/` に出力する. + +1. test15_setup_cutaway.png : 実験構成の注釈付き軸断面(r=100・similar_radial). + 評価板・球キャビティ・軸上 LED・バッフル・遮断される直接光を日本語ラベルで指す +2. test15_radius_nest.png : 半径スイープの入れ子断面(評価板は不変のまま球だけ拡大) +3. test15_junction.png : 接合のさせ方の比較(垂直立ち上げ=喉 vs 水平出し,r=250) + +実行方法: + uv run python scripts/build_slide_test15_geometry.py +""" + +from __future__ import annotations + +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)) + +from build_radius_geometry_figure import loft_profile, sphere_profile # noqa: E402 +from sweep_sphere_radius import ( # noqa: E402 + BAFFLE_GAP, + BOARD_HALF, + BOARD_SURFACE_Z, + baffle_radius, + led_z, +) + +ROOT = Path(__file__).resolve().parents[1] +OUT_DIR = ROOT / "output" / "_slide_panels" + +plt.rcParams["font.family"] = ["Yu Gothic", "MS Gothic", "DejaVu Sans"] +plt.rcParams["font.size"] = 13 + +BOARD_COLOR = "crimson" +WALL_COLOR = "#455a74" +LED_COLOR = "#e6a817" +BAFFLE_COLOR = "black" + +VARIANT = "board_baffle" +CAVITY = "similar_radial" + + +def cavity_outline(radius: float, cavity: str = CAVITY) -> tuple[np.ndarray, np.ndarray]: + """片側(+X)のリム→ロフト→球極の断面プロファイルを返す.""" + lx, lz = loft_profile(radius, cavity) + sx, sz = sphere_profile(radius, cavity) + return np.concatenate([lx, sx]), np.concatenate([lz, sz]) + + +def draw_scale_bar(ax, x: float, z: float, length: float = 100.0) -> None: + """軸の代わりに置くスケールバーを描く.""" + ax.plot([x, x + length], [z, z], color="black", lw=2.0) + ax.plot([x, x], [z - 4, z + 4], color="black", lw=2.0) + ax.plot([x + length, x + length], [z - 4, z + 4], color="black", lw=2.0) + ax.text(x + length / 2, z - 10, f"{length:g} mm", ha="center", va="top", fontsize=12) + + +def make_setup_cutaway(radius: float = 100.0, annotate: bool = True) -> None: + """実験構成の軸断面を描く. + + Args: + radius: 球内半径(mm). + annotate: True で日本語注釈とスケールバー付き(説明用).False で図形のみ + (スライド側で注釈を載せる前提.点線・マーカーは遠目に見えるよう太くする). + """ + fig, ax = plt.subplots(figsize=(8.2, 8.2)) + x, z = cavity_outline(radius) + + # 内壁(左右対称).閉じた輪郭として太線で描く + ax.plot(np.concatenate([x, -x[::-1]]), np.concatenate([z, z[::-1]]), + color=WALL_COLOR, lw=4.0 if annotate else 5.0, solid_capstyle="round") + + # 評価板(上面のフタ) + ax.plot([-BOARD_HALF[0], BOARD_HALF[0]], [BOARD_SURFACE_Z] * 2, + color=BOARD_COLOR, lw=8.0 if annotate else 10.0, solid_capstyle="butt") + + # LED(軸上 1 灯・壁面密着・評価板向き)と バッフル + zl = led_z(radius, VARIANT, CAVITY) + rb = baffle_radius(radius, VARIANT, CAVITY) + ax.plot(0.0, zl, marker="^", color=LED_COLOR, ms=16 if annotate else 22, + mec="black", mew=0.8, zorder=5) + ax.plot([-rb, rb], [zl + BAFFLE_GAP] * 2, color=BAFFLE_COLOR, + lw=5.0 if annotate else 7.0) + + # 遮断される直接光(LED → 板の両端.バッフルで切れることを示す) + for sign in (-1.0, 1.0): + ax.plot([0.0, sign * BOARD_HALF[0]], [zl, BOARD_SURFACE_Z], + color=LED_COLOR, lw=1.4 if annotate else 5.5, ls=":", alpha=0.9) + + if not annotate: + ax.set_xlim(-125, 125) + ax.set_ylim(-210, 15) + ax.set_aspect("equal") + ax.axis("off") + fig.tight_layout() + out = OUT_DIR / "test15_setup_cutaway_plain.png" + fig.savefig(out, dpi=150, bbox_inches="tight", transparent=True) + plt.close(fig) + print(f"出力: {out}") + return + + # 注釈 + common = {"fontsize": 14, "ha": "left", "va": "center", + "arrowprops": {"arrowstyle": "-", "color": "0.3", "lw": 1.2}} + ax.annotate("評価板 99×102 mm\n(上面の開口にフタとして嵌める)", + xy=(BOARD_HALF[0] * 0.55, BOARD_SURFACE_Z), xytext=(85, 30), **common) + ax.annotate("球キャビティ\n(内壁は拡散反射・白)", + xy=(float(np.interp(-120, z[::-1], x[::-1])), -120), xytext=(120, -95), + **common) + ax.annotate("LED(軸上に 1 灯・壁面に密着・\n評価板の方を向ける)", + xy=(3, zl), xytext=(60, -215), **common) + ax.annotate("バッフル\n(評価板への直接光を完全に遮断)", + xy=(rb * 0.8, zl + BAFFLE_GAP), xytext=(60, -175), **common) + ax.text(0, zl + 42, "直接光は届かない\n(多重反射光のみが板を照らす)", + fontsize=12, ha="center", color="0.35") + + draw_scale_bar(ax, -180, -245) + ax.set_xlim(-195, 245) + ax.set_ylim(-260, 45) + ax.set_aspect("equal") + ax.axis("off") + fig.tight_layout() + out = OUT_DIR / "test15_setup_cutaway.png" + fig.savefig(out, dpi=150, bbox_inches="tight") + plt.close(fig) + print(f"出力: {out}") + + +def make_radius_nest( + radii: tuple[float, ...] = (88.0, 100.0, 135.0, 170.0, 250.0), annotate: bool = True +) -> None: + """半径スイープの入れ子断面を描く(評価板は不変のまま球だけ拡大). + + Args: + radii: 描く球内半径(mm). + annotate: True で説明文・スケールバー付き,ラベルは半径表記(説明用). + False でサイズラベル(直径表記・大きい文字)のみ=スライド用. + ラベルの重なりを避けるため左右交互に置く. + """ + fig, ax = plt.subplots(figsize=(8.8, 9.6)) + colors = plt.cm.viridis(np.linspace(0.05, 0.85, len(radii))) + for i, (radius, color) in enumerate(zip(radii, colors, strict=True)): + x, z = cavity_outline(radius) + ax.plot(np.concatenate([x, -x[::-1]]), np.concatenate([z, z[::-1]]), + color=color, lw=2.6 if annotate else 3.4) + zl = led_z(radius, VARIANT, CAVITY) + rb = baffle_radius(radius, VARIANT, CAVITY) + ax.plot(0.0, zl, marker="^", color=color, ms=9 if annotate else 13, + mec="black", mew=0.6, zorder=5) + ax.plot([-rb, rb], [zl + BAFFLE_GAP] * 2, color="black", lw=2.2 if annotate else 3.0) + if annotate: + ax.text(float(x.max()) + 8, float(z[np.argmax(x)]), f"r={radius:g}", + color=color, fontsize=13, va="center", fontweight="bold") + else: + sign = 1.0 if i % 2 == 0 else -1.0 + ax.text(sign * (float(x.max()) + 10), float(z[np.argmax(x)]), + f"φ{2 * radius:g}", color=color, fontsize=30, va="center", + ha="left" if sign > 0 else "right", fontweight="bold") + + ax.plot([-BOARD_HALF[0], BOARD_HALF[0]], [BOARD_SURFACE_Z] * 2, + color=BOARD_COLOR, lw=8.0 if annotate else 10.0, solid_capstyle="butt", zorder=6) + + if annotate: + ax.text(0, 16, "評価板・カメラ・LED 条件は不変", color=BOARD_COLOR, + fontsize=15, ha="center", fontweight="bold") + ax.text(0, -520, "球の内半径だけを 88〜250 mm で変える(LED・バッフルは相似に追従)", + fontsize=13, ha="center", color="0.25") + draw_scale_bar(ax, -255, -490) + ax.set_aspect("equal") + ax.axis("off") + fig.tight_layout() + out = OUT_DIR / ("test15_radius_nest.png" if annotate else "test15_radius_nest_plain.png") + fig.savefig(out, dpi=150, bbox_inches="tight", transparent=not annotate) + plt.close(fig) + print(f"出力: {out}") + + +def make_junction_comparison(radius: float = 250.0) -> None: + """接合のさせ方の比較(垂直立ち上げ=喉 vs 水平出し)を描く.""" + fig, ax = plt.subplots(figsize=(10.5, 5.6)) + designs = [ + ("similar_g2", "tab:red", "板に垂直に立ち上げる(喉ができる)"), + ("similar_radial", "tab:blue", "水平に出して球へ回り込む(喉が消える)"), + ] + for cavity, color, label in designs: + x, z = cavity_outline(radius, cavity) + ax.plot(x, z, color=color, lw=3.2, label=label) + ax.plot(-x, z, color=color, lw=3.2) + + ax.plot([-BOARD_HALF[0], BOARD_HALF[0]], [BOARD_SURFACE_Z] * 2, + color=BOARD_COLOR, lw=8.0, solid_capstyle="butt", zorder=6) + ax.text(0, 10, "評価板", color=BOARD_COLOR, fontsize=14, ha="center", fontweight="bold") + + # 喉(板直下のくびれ)の注釈: 垂直版は板縁の下でほとんど広がらない + ax.annotate("喉(くびれ): 板の直下で通路が広がらず\n周辺の照度が落ちる", + xy=(54, -25), xytext=(85, -105), fontsize=13, ha="left", va="center", + arrowprops={"arrowstyle": "->", "color": "tab:red", "lw": 1.6}, + color="tab:red", + bbox={"boxstyle": "round,pad=0.35", "fc": "white", "ec": "none", "alpha": 0.9}) + + ax.set_xlim(-270, 270) + ax.set_ylim(-140, 30) + ax.set_aspect("equal") + ax.axis("off") + ax.legend(fontsize=13, loc="lower left", frameon=False) + ax.set_title("評価板と球の接合のさせ方(r=250 の断面・上部を拡大)", fontsize=15) + fig.tight_layout() + out = OUT_DIR / "test15_junction.png" + fig.savefig(out, dpi=150, bbox_inches="tight") + plt.close(fig) + print(f"出力: {out}") + + +def main() -> int: + """スライド用ジオメトリ図 3 枚を生成する.""" + OUT_DIR.mkdir(parents=True, exist_ok=True) + make_setup_cutaway() + make_setup_cutaway(annotate=False) + make_radius_nest() + make_radius_nest(annotate=False) + make_junction_comparison() + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/build_slide_test15_results.py b/scripts/build_slide_test15_results.py new file mode 100644 index 0000000..67b5a3f --- /dev/null +++ b/scripts/build_slide_test15_results.py @@ -0,0 +1,211 @@ +"""TEST_15(球体筐体実験)の発表スライド用の結果図(輝度マップ)を生成する. + +スイープ一覧図(build_radius_sweep_visual.py)はカーブや CoV 分解を含む作業用の +高密度な図なので,スライド向けに輝度マップだけを 1 行に並べた版を出力する. + +正規化は TEST_15 の作法どおり **20 分割の輪帯平均の最大値を 1.0** とする +(画素単位の最大値は MC ノイズの外れ値に張り付くため使わない).全マップで +カラースケールを共有し,カラーバーを 1 つ添える.サイズ表記は他のスライド素材と +揃えて直径(φ)とする. + +実行方法: + uv run python scripts/build_slide_test15_results.py +""" + +from __future__ import annotations + +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)) + +from build_radius_sweep_visual import normalized_map # noqa: E402 +from sweep_sphere_radius import OUTPUT_BASE, job_name # noqa: E402 + +ROOT = Path(__file__).resolve().parents[1] +OUT_DIR = ROOT / "output" / "_slide_panels" + +plt.rcParams["font.family"] = ["Yu Gothic", "MS Gothic", "DejaVu Sans"] + +RUN_DIR = "board_baffle_similar_radial" # TEST_15 最終設計(リム水平出し) +RADII_5 = (88.0, 100.0, 135.0, 170.0, 250.0) # 幾何スライドの入れ子断面と同じ代表 5 条件 +RADII_ALL = (88.0, 95.0, 100.0, 110.0, 120.0, 135.0, 150.0, 170.0, 200.0, 250.0) + + +def sphere_items(radii: tuple[float, ...]) -> list[tuple[str, Path]]: + """球スイープの (ラベル, luminance.npy パス) を返す(直径 φ 表記).""" + return [(f"φ{2 * r:g}", OUTPUT_BASE / RUN_DIR / job_name(r) / "luminance.npy") + for r in radii] + + +def cube_items(sizes: tuple[float, ...]) -> list[tuple[str, Path]]: + """立方体スイープの (ラベル, luminance.npy パス) を返す(一辺 L 表記).""" + from sweep_cube_size import OUTPUT_BASE as CUBE_BASE + from sweep_cube_size import RUN_DIR as CUBE_RUN + from sweep_cube_size import job_name as cube_job + + return [(f"L={s:g}", CUBE_BASE / CUBE_RUN / cube_job(s) / "luminance.npy") + for s in sizes] + + +def make_luminance_grid(items: list[tuple[str, Path]], n_cols: int, out_name: str) -> None: + """輪帯ピーク正規化の輝度マップをグリッドに並べ,共有カラーバーを添える.""" + maps = [(label, normalized_map(path)) for label, path in items] + stacked = np.concatenate([m.ravel() for _, m in maps]) + # 上端は 1.0(=最大輪帯)に固定し,下端だけデータに合わせる(作業図と同じ規約) + vmin = float(np.percentile(stacked, 0.5)) + + n_rows = int(np.ceil(len(maps) / n_cols)) + fig, axes = plt.subplots(n_rows, n_cols, + figsize=(3.1 * n_cols + 1.2, 3.9 * n_rows), squeeze=False) + for ax in axes.ravel(): + ax.axis("off") + for ax, (label, data) in zip(axes.ravel(), maps, strict=False): + image = ax.imshow(data, cmap="hot", vmin=vmin, vmax=1.0) + ax.set_title(label, fontsize=26, fontweight="bold", pad=10) + cbar = fig.colorbar(image, ax=axes, fraction=0.03, pad=0.02) + cbar.set_label("輝度(輪帯平均の最大値 = 1.0)", fontsize=15) + cbar.ax.tick_params(labelsize=14) + + out = OUT_DIR / out_name + OUT_DIR.mkdir(parents=True, exist_ok=True) + fig.savefig(out, dpi=150, bbox_inches="tight") + plt.close(fig) + print(f"出力: {out}") + + +def make_radial_profiles( + items: list[tuple[str, Path]] | None = None, + out_name: str = "test15_radial_profiles.png", +) -> None: + """同心円(輪帯 20 分割)プロファイルを全条件で重ね描きする(ピーク=1 正規化). + + 輪帯の定義・正規化は評価パイプラインと同一(build_radial_comparison の関数を流用). + 帯は ±1 標準誤差=方位平均でノイズが潰れていることを図の上で示す. + """ + from build_radial_comparison import normalize_to_peak, radial_stats + + if items is None: + items = sphere_items(RADII_ALL) + fig, ax = plt.subplots(figsize=(9.5, 6.8)) + colors = plt.cm.viridis(np.linspace(0.0, 0.9, len(items))) + for (label, lum), color in zip(items, colors, strict=True): + centers, means, sems = radial_stats(lum, 20) + means, sems = normalize_to_peak(centers, means, sems) + ax.plot(centers, means, color=color, lw=2.4, label=label) + ax.fill_between(centers, means - sems, means + sems, color=color, alpha=0.35, lw=0) + ax.axhline(1.0, color="gray", lw=1.0, ls="--") + ax.set_xlabel("Distance from center", fontsize=18) + ax.set_ylabel("Luminance", fontsize=18) + ax.tick_params(labelsize=15) + ax.legend(fontsize=14, ncol=2, framealpha=0.9) + ax.grid(alpha=0.3) + for spine in ("top", "right"): + ax.spines[spine].set_visible(False) + fig.tight_layout() + + out = OUT_DIR / out_name + fig.savefig(out, dpi=150, bbox_inches="tight") + plt.close(fig) + print(f"出力: {out}") + + +CUBE_SIZES_ALL = (120.0, 140.0, 160.0, 180.0, 200.0, 220.0, 240.0, 280.0, 320.0, 360.0) + +# 球と立方体の直接比較ペア(内面積が最も近いグリッド点どうし) +MATCHED_PAIRS = ((88.0, 120.0), (100.0, 140.0), (135.0, 180.0), (170.0, 240.0), (250.0, 360.0)) + + +def make_matched_pairs(pairs: tuple[tuple[float, float], ...] = MATCHED_PAIRS) -> None: + """内面積の近い球と立方体の輪帯プロファイルをペアごとに重ねて 1 行に並べる.""" + from build_radial_comparison import normalize_to_peak, radial_stats + from sweep_cube_size import OUTPUT_BASE as CUBE_BASE + from sweep_cube_size import RUN_DIR as CUBE_RUN + from sweep_cube_size import cavity_geometry as cube_geo + from sweep_cube_size import job_name as cube_job + from sweep_sphere_radius import cavity_geometry as sphere_geo + + fig, axes = plt.subplots(1, len(pairs), figsize=(4.0 * len(pairs), 4.6), sharey=True) + for i, (ax, (radius, size)) in enumerate(zip(axes, pairs, strict=True)): + targets = [ + (OUTPUT_BASE / RUN_DIR / job_name(radius) / "luminance.npy", + f"球 φ{2 * radius:g}", "tab:blue", "--"), + (CUBE_BASE / CUBE_RUN / cube_job(size) / "luminance.npy", + f"立方体 L={size:g}", "tab:red", "-"), + ] + for lum, label, color, ls in targets: + centers, means, sems = radial_stats(lum, 20) + means, sems = normalize_to_peak(centers, means, sems) + ax.plot(centers, means, color=color, ls=ls, lw=2.6, 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_title(f"φ{2 * radius:g} vs L={size:g}", fontsize=19, fontweight="bold", pad=8) + area_s = sphere_geo(radius, "similar_radial")["cavity_area_mm2"] / 1000 + area_c = cube_geo(size)["cavity_area_mm2"] / 1000 + ax.text(0.03, 0.05, f"内面積 {area_s:.0f}k / {area_c:.0f}k mm²", + transform=ax.transAxes, ha="left", fontsize=12, color="0.35") + ax.tick_params(labelsize=13) + ax.grid(alpha=0.3) + for spine in ("top", "right"): + ax.spines[spine].set_visible(False) + ax.legend(fontsize=12, loc="upper right", frameon=False) + if i == 0: + ax.set_ylabel("Luminance", fontsize=17) + ax.set_xlabel("Distance from center", fontsize=14) + fig.tight_layout() + + out = OUT_DIR / "test16_matched_pairs.png" + fig.savefig(out, dpi=150, bbox_inches="tight") + plt.close(fig) + print(f"出力: {out}") + + +def make_matched_pair_maps(pairs: tuple[tuple[float, float], ...] = MATCHED_PAIRS) -> None: + """内面積の近いペアの輝度マップを上段=球・下段=立方体で並べる(全マップ共有スケール).""" + from sweep_cube_size import OUTPUT_BASE as CUBE_BASE + from sweep_cube_size import RUN_DIR as CUBE_RUN + from sweep_cube_size import job_name as cube_job + + items = [] + for radius, size in pairs: + items.append((f"球 φ{2 * radius:g}", + OUTPUT_BASE / RUN_DIR / job_name(radius) / "luminance.npy")) + items.append((f"立方体 L={size:g}", + CUBE_BASE / CUBE_RUN / cube_job(size) / "luminance.npy")) + maps = {label: normalized_map(path) for label, path in items} + stacked = np.concatenate([m.ravel() for m in maps.values()]) + vmin = float(np.percentile(stacked, 0.5)) + + n = len(pairs) + fig, axes = plt.subplots(2, n, figsize=(3.1 * n + 1.2, 7.6)) + for col, (radius, size) in enumerate(pairs): + for row, label in enumerate((f"球 φ{2 * radius:g}", f"立方体 L={size:g}")): + ax = axes[row, col] + image = ax.imshow(maps[label], cmap="hot", vmin=vmin, vmax=1.0) + ax.set_title(label, fontsize=20, fontweight="bold", pad=8) + ax.axis("off") + cbar = fig.colorbar(image, ax=axes, fraction=0.03, pad=0.02) + cbar.set_label("輝度(輪帯平均の最大値 = 1.0)", fontsize=15) + cbar.ax.tick_params(labelsize=14) + + out = OUT_DIR / "test16_matched_pair_maps.png" + fig.savefig(out, dpi=150, bbox_inches="tight") + plt.close(fig) + print(f"出力: {out}") + + +if __name__ == "__main__": + make_luminance_grid(sphere_items(RADII_5), 5, "test15_luminance_row.png") + make_luminance_grid(sphere_items(RADII_ALL), 5, "test15_luminance_all.png") + make_luminance_grid(cube_items(CUBE_SIZES_ALL), 5, "test16_luminance_all.png") + make_radial_profiles(sphere_items(RADII_ALL), "test15_radial_profiles.png") + make_radial_profiles(cube_items(CUBE_SIZES_ALL), "test16_radial_profiles.png") + make_matched_pairs() + make_matched_pair_maps() + sys.exit(0) diff --git a/scripts/build_slide_test16_geometry.py b/scripts/build_slide_test16_geometry.py new file mode 100644 index 0000000..ea4badd --- /dev/null +++ b/scripts/build_slide_test16_geometry.py @@ -0,0 +1,160 @@ +"""TEST_16(立方体キャビティ実験)の発表スライド用ジオメトリ図を生成する. + +球版(build_slide_test15_geometry.py)と同じ体裁で,立方体の構成断面とサイズ入れ子図を +`output/_slide_panels/` に出力する.annotate=False 版は文字なし・要素太め・透過背景で, +注釈をスライド側で載せる前提. + +1. test16_setup_cutaway(.png / _plain.png) : 実験構成の軸断面(L=200). + 評価板が上面開口にそのまま嵌まる・平らな天井・底面中心の LED・バッフル +2. test16_size_nest(.png / _plain.png) : サイズスイープの入れ子断面(板は不変) + +実行方法: + uv run python scripts/build_slide_test16_geometry.py +""" + +from __future__ import annotations + +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)) + +from sweep_cube_size import baffle_radius, led_z # noqa: E402 +from sweep_sphere_radius import ( # noqa: E402 + BAFFLE_GAP, + BOARD_HALF, + BOARD_SURFACE_Z, + RIM_INSET, + RIM_Z, +) + +ROOT = Path(__file__).resolve().parents[1] +OUT_DIR = ROOT / "output" / "_slide_panels" + +plt.rcParams["font.family"] = ["Yu Gothic", "MS Gothic", "DejaVu Sans"] +plt.rcParams["font.size"] = 13 + +BOARD_COLOR = "crimson" +WALL_COLOR = "#455a74" +LED_COLOR = "#e6a817" + +CUBE_SIZES = (120.0, 160.0, 200.0, 280.0, 360.0) + + +def cube_outline(size: float) -> tuple[np.ndarray, np.ndarray]: + """片側(+X)のリム→天井→側壁→底面中心の断面ポリラインを返す(mm).""" + rim_x = BOARD_HALF[0] - RIM_INSET + half = size / 2.0 + x = np.array([rim_x, half, half, 0.0]) + z = np.array([RIM_Z, RIM_Z, RIM_Z - size, RIM_Z - size]) + return x, z + + +def draw_cube(ax, size: float, color, lw: float) -> None: + """立方体断面(左右対称)と LED・バッフルを描く.""" + x, z = cube_outline(size) + ax.plot(np.concatenate([x, -x[::-1]]), np.concatenate([z, z[::-1]]), + color=color, lw=lw, solid_capstyle="round") + + +def make_setup_cutaway(size: float = 200.0, annotate: bool = True) -> None: + """実験構成の軸断面を描く(annotate=False で文字なし・スライド用).""" + fig, ax = plt.subplots(figsize=(8.2, 8.2)) + draw_cube(ax, size, WALL_COLOR, 4.0 if annotate else 5.0) + + ax.plot([-BOARD_HALF[0], BOARD_HALF[0]], [BOARD_SURFACE_Z] * 2, + color=BOARD_COLOR, lw=8.0 if annotate else 10.0, solid_capstyle="butt") + + zl = led_z(size) + rb = baffle_radius(size) + ax.plot(0.0, zl, marker="^", color=LED_COLOR, ms=16 if annotate else 22, + mec="black", mew=0.8, zorder=5) + ax.plot([-rb, rb], [zl + BAFFLE_GAP] * 2, color="black", lw=5.0 if annotate else 7.0) + for sign in (-1.0, 1.0): + ax.plot([0.0, sign * BOARD_HALF[0]], [zl, BOARD_SURFACE_Z], + color=LED_COLOR, lw=1.4 if annotate else 5.5, ls=":", alpha=0.9) + + if annotate: + common = {"fontsize": 14, "ha": "left", "va": "center", + "arrowprops": {"arrowstyle": "-", "color": "0.3", "lw": 1.2}} + ax.annotate("評価板 99×102 mm\n(上面の開口にそのまま嵌まる)", + xy=(BOARD_HALF[0] * 0.55, BOARD_SURFACE_Z), xytext=(115, 25), **common) + ax.annotate("平らな天井\n(板の縁から水平に広がる)", + xy=((BOARD_HALF[0] + size / 2.0) / 2.0, RIM_Z), xytext=(125, -35), + **common) + ax.annotate("立方体キャビティ 一辺 L\n(内壁は拡散反射・白)", + xy=(size / 2.0, -size * 0.55), xytext=(115, -120), **common) + ax.annotate("LED(底面中心に 1 灯・壁面に密着・\n評価板の方を向ける)", + xy=(3, zl), xytext=(60, -230), **common) + ax.annotate("バッフル\n(評価板への直接光を完全に遮断)", + xy=(rb * 0.8, zl + BAFFLE_GAP), xytext=(60, -180), **common) + ax.plot([-100, 0], [-245, -245], color="black", lw=2.0) + ax.plot([-100, -100], [-241, -249], color="black", lw=2.0) + ax.plot([0, 0], [-241, -249], color="black", lw=2.0) + ax.text(-50, -255, "100 mm", ha="center", va="top", fontsize=12) + ax.set_xlim(-130, 250) + ax.set_ylim(-265, 40) + + else: + ax.set_xlim(-125, 125) + ax.set_ylim(-215, 15) + ax.set_aspect("equal") + ax.axis("off") + fig.tight_layout() + out = OUT_DIR / ("test16_setup_cutaway.png" if annotate else "test16_setup_cutaway_plain.png") + fig.savefig(out, dpi=150, bbox_inches="tight", transparent=not annotate) + plt.close(fig) + print(f"出力: {out}") + + +def make_size_nest(sizes: tuple[float, ...] = CUBE_SIZES, annotate: bool = True) -> None: + """サイズスイープの入れ子断面を描く(評価板は不変のまま立方体だけ拡大).""" + fig, ax = plt.subplots(figsize=(8.8, 9.6)) + colors = plt.cm.viridis(np.linspace(0.05, 0.85, len(sizes))) + for size, color in zip(sizes, colors, strict=True): + draw_cube(ax, size, color, 2.6 if annotate else 3.4) + zl = led_z(size) + rb = baffle_radius(size) + ax.plot(0.0, zl, marker="^", color=color, ms=9 if annotate else 13, + mec="black", mew=0.6, zorder=5) + ax.plot([-rb, rb], [zl + BAFFLE_GAP] * 2, color="black", lw=2.2 if annotate else 3.0) + # ラベルは自分の底面の右上(自キューブの内側)に置く.入れ子の外側ほど底が + # 深いので互いに重ならず,他サイズの壁線とも交差しない + ax.text(size / 2.0 - 8, RIM_Z - size + 12, f"L={size:g}", color=color, + fontsize=13 if annotate else 30, va="bottom", ha="right", fontweight="bold") + + ax.plot([-BOARD_HALF[0], BOARD_HALF[0]], [BOARD_SURFACE_Z] * 2, + color=BOARD_COLOR, lw=8.0 if annotate else 10.0, solid_capstyle="butt", zorder=6) + + if annotate: + ax.text(0, 16, "評価板・カメラ・LED 条件は不変", color=BOARD_COLOR, + fontsize=15, ha="center", fontweight="bold") + ax.text(0, -395, "立方体の一辺だけを 120〜360 mm で変える(LED・バッフルは追従)", + fontsize=13, ha="center", color="0.25") + ax.set_aspect("equal") + ax.axis("off") + fig.tight_layout() + out = OUT_DIR / ("test16_size_nest.png" if annotate else "test16_size_nest_plain.png") + fig.savefig(out, dpi=150, bbox_inches="tight", transparent=not annotate) + plt.close(fig) + print(f"出力: {out}") + + +def main() -> int: + """スライド用ジオメトリ図を生成する.""" + OUT_DIR.mkdir(parents=True, exist_ok=True) + make_setup_cutaway() + make_setup_cutaway(annotate=False) + make_size_nest() + make_size_nest(annotate=False) + return 0 + + +if __name__ == "__main__": + sys.exit(main())