"""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)