"""evaluation/visualize.py の仕様ベーステスト(smoke テスト).
仕様出典:
- docs/04_SPEC/SPEC_01_アーキテクチャ設計.md(evaluation/visualize 層の責務)
- docs/01_GUIDE/GUIDE_08_テスト方針.md(可視化は smoke テストで十分)
テスト方針 (GUIDE_08):
可視化の smoke テストは「ファイルが生成されたか」「ファイルサイズが 0 でないか」程度で十分.
図の見栄えはテストしない.
matplotlib.use("Agg") を pyplot の import より前に呼ぶこと.
"""
from __future__ import annotations
import matplotlib
# headless 環境でも動くよう Agg バックエンドを pyplot より前に設定する
matplotlib.use("Agg")
from pathlib import Path
import numpy
from sm_tias_lightsim.evaluation.visualize import (
save_histogram,
save_luminance_map,
save_radial_profile,
save_zone_map,
)
# ---------------------------------------------------------------------------
# テスト用ヘルパー:合成輝度配列
# ---------------------------------------------------------------------------
def _make_luminance(size: int = 16) -> numpy.ndarray:
"""テスト用の合成輝度配列(全画素 100.0 ± 小さなノイズ)を返す.
乱数 seed を固定して再現性を保証する (GUIDE_08: seed を必ず固定する)
"""
rng = numpy.random.RandomState(0)
return numpy.full((size, size), 100.0) + rng.normal(0, 5, (size, size))
# ---------------------------------------------------------------------------
# 1. save_luminance_map
# ---------------------------------------------------------------------------
def test_save_luminance_map_creates_file(tmp_path: Path):
"""save_luminance_map を呼ぶと PNG ファイルが生成されること.
仕様: 輝度マップを PNG として保存する(visualize.py save_luminance_map)
"""
luminance = _make_luminance()
output_path = tmp_path / "luminance_map.png"
save_luminance_map(luminance, output_path)
assert output_path.exists()
def test_save_luminance_map_file_not_empty(tmp_path: Path):
"""生成された PNG ファイルのサイズが 0 より大きいこと.
仕様: 正しい PNG として書き出されること(visualize.py save_luminance_map)
"""
luminance = _make_luminance()
output_path = tmp_path / "luminance_map.png"
save_luminance_map(luminance, output_path)
assert output_path.stat().st_size > 0
# ---------------------------------------------------------------------------
# 2. save_histogram
# ---------------------------------------------------------------------------
def test_save_histogram_creates_file(tmp_path: Path):
"""save_histogram を呼ぶと PNG ファイルが生成されること.
仕様: 輝度ヒストグラムを PNG として保存する(visualize.py save_histogram)
"""
luminance = _make_luminance()
output_path = tmp_path / "histogram.png"
save_histogram(luminance, output_path)
assert output_path.exists()
def test_save_histogram_file_not_empty(tmp_path: Path):
"""生成された histogram PNG ファイルのサイズが 0 より大きいこと."""
luminance = _make_luminance()
output_path = tmp_path / "histogram.png"
save_histogram(luminance, output_path)
assert output_path.stat().st_size > 0
def test_save_histogram_custom_bins(tmp_path: Path):
"""bins パラメータを変えても PNG ファイルが生成されること.
仕様: bins=50(デフォルト)(visualize.py save_histogram)
"""
luminance = _make_luminance()
output_path = tmp_path / "histogram_10bins.png"
save_histogram(luminance, output_path, bins=10)
assert output_path.exists()
assert output_path.stat().st_size > 0
# ---------------------------------------------------------------------------
# 3. save_radial_profile
# ---------------------------------------------------------------------------
def test_save_radial_profile_creates_file(tmp_path: Path):
"""save_radial_profile を呼ぶと PNG ファイルが生成されること.
仕様: 放射状輝度プロファイルを線グラフとして PNG に保存する
profile shape: (n_bins, 2),列 0: 距離,列 1: 輝度(visualize.py)
"""
rng = numpy.random.RandomState(1)
distances = numpy.linspace(0.0, 1.0, 20)
luminances = 100.0 + rng.normal(0, 5, 20)
profile = numpy.column_stack([distances, luminances]) # (20, 2)
output_path = tmp_path / "radial_profile.png"
save_radial_profile(profile, output_path)
assert output_path.exists()
def test_save_radial_profile_file_not_empty(tmp_path: Path):
"""生成された radial_profile PNG ファイルのサイズが 0 より大きいこと."""
distances = numpy.linspace(0.0, 1.0, 20)
luminances = numpy.linspace(120.0, 80.0, 20) # 線形減衰
profile = numpy.column_stack([distances, luminances])
output_path = tmp_path / "radial_profile.png"
save_radial_profile(profile, output_path)
assert output_path.stat().st_size > 0
def test_save_radial_profile_with_nan_does_not_crash(tmp_path: Path):
"""NaN を含む行がある profile を渡してもクラッシュしないこと.
仕様: 空ビンは nan で表現される(評価仕様の空ビン扱い)
matplotlib の plot は NaN を自動でスキップする(線が途切れる挙動)ため,
クラッシュせずにファイルが生成されること.
"""
distances = numpy.linspace(0.0, 1.0, 20)
luminances = numpy.linspace(120.0, 80.0, 20)
# 空ビンを模擬するため,いくつかの行を NaN にする
luminances[5] = float("nan")
luminances[10] = float("nan")
luminances[15] = float("nan")
profile = numpy.column_stack([distances, luminances])
output_path = tmp_path / "radial_profile_nan.png"
save_radial_profile(profile, output_path)
assert output_path.exists()
assert output_path.stat().st_size > 0
# ---------------------------------------------------------------------------
# 4. save_zone_map
# ---------------------------------------------------------------------------
def test_save_zone_map_creates_file(tmp_path: Path):
"""save_zone_map を呼ぶと PNG ファイルが生成されること.
仕様: ゾーンマップを 3 色で PNG として保存する(visualize.py save_zone_map)
入力: int8 配列(0=center, 1=middle, 2=periphery)
"""
from sm_tias_lightsim.evaluation.zones import classify_zones, compute_normalized_distance_map
distance_map = compute_normalized_distance_map(16, 16)
zone_map = classify_zones(distance_map) # dtype: int8
output_path = tmp_path / "zone_map.png"
save_zone_map(zone_map, output_path)
assert output_path.exists()
def test_save_zone_map_file_not_empty(tmp_path: Path):
"""生成された zone_map PNG ファイルのサイズが 0 より大きいこと."""
from sm_tias_lightsim.evaluation.zones import classify_zones, compute_normalized_distance_map
distance_map = compute_normalized_distance_map(16, 16)
zone_map = classify_zones(distance_map)
output_path = tmp_path / "zone_map.png"
save_zone_map(zone_map, output_path)
assert output_path.stat().st_size > 0
def test_save_zone_map_accepts_all_zone_values(tmp_path: Path):
"""全 3 ゾーン(0/1/2)が含まれる zone_map を渡しても PNG が生成されること.
仕様: 値は 0(center), 1(middle), 2(periphery)のいずれか(zones.py classify_zones)
"""
# 手動で 3 ゾーン全てを含む 3x3 マップを作成する
zone_map = numpy.array(
[[0, 0, 1], [0, 1, 2], [1, 2, 2]], dtype=numpy.int8
)
output_path = tmp_path / "zone_map_all_zones.png"
save_zone_map(zone_map, output_path)
assert output_path.exists()
assert output_path.stat().st_size > 0