Newer
Older
SmTIAS-LightSim / scripts / build_slide_test15_geometry.py
"""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())