"""calc_radial_min_max_ratio の単体テスト.
仕様 (実装サマリー / TECH_05_MiniTiasEvaluation_DNG対応要求仕様.md):
- calc_radial_min_max_ratio(radial_profile) -> dict
- 引数: 20 点動径プロファイル配列 (N x 2, float64)
- 返り値キー:
"radial_min_max_ratio" : min(profile[:,1]) / max(profile[:,1])
"radial_min_distance" : 最小輝度ビンの正規化距離
"radial_min" : プロファイルの最小輝度値
"radial_max" : プロファイルの最大輝度値
- max=0 のゼロ除算は float("inf") でガード
- calc_spatial_uniformity の戻り値トップレベルに上記 4 キーを追加
(既存キー zone_stats / radial_profile / zone_map は不変)
- SPATIAL_FIELDNAMES に radial_min_max_ratio / radial_min_distance を追加
- export_spatial_summary が新指標を CSV に書き出せること
テスト方針 (GUIDE_08_テスト方針.md):
- pytest を使用
- numpy で合成した既知のプロファイルで期待値と比較する
"""
import csv
from pathlib import Path
import numpy
import numpy as np
import pytest
from src.analysis.spatial import (
calc_radial_min_max_ratio,
calc_spatial_uniformity,
)
from src.export.exporter import SPATIAL_FIELDNAMES, export_spatial_summary
# ---------------------------------------------------------------------------
# calc_radial_min_max_ratio テスト
# ---------------------------------------------------------------------------
class TestCalcRadialMinMaxRatio:
"""calc_radial_min_max_ratio 関数のテスト群."""
def _make_profile(self, luminances: list[float]) -> numpy.ndarray:
"""輝度値リストから動径プロファイル配列 (N x 2) を生成するヘルパー."""
n = len(luminances)
distances = numpy.linspace(0.025, 0.975, n)
lums = numpy.array(luminances, dtype=numpy.float64)
return numpy.column_stack([distances, lums])
def test_return_value_has_all_required_keys(self) -> None:
"""仕様: 返り値に radial_min_max_ratio / radial_min_distance / radial_min / radial_max
の 4 キーが含まれること."""
profile = self._make_profile([100.0, 90.0, 80.0, 70.0, 60.0])
result = calc_radial_min_max_ratio(profile)
assert "radial_min_max_ratio" in result
assert "radial_min_distance" in result
assert "radial_min" in result
assert "radial_max" in result
def test_ratio_is_min_divided_by_max(self) -> None:
"""仕様: radial_min_max_ratio が min / max と一致すること(手計算値)."""
# min=60, max=100 → ratio=0.6
profile = self._make_profile([100.0, 90.0, 80.0, 70.0, 60.0])
result = calc_radial_min_max_ratio(profile)
assert result["radial_min"] == pytest.approx(60.0)
assert result["radial_max"] == pytest.approx(100.0)
assert result["radial_min_max_ratio"] == pytest.approx(60.0 / 100.0)
def test_ratio_exact_computation_known_values(self) -> None:
"""仕様: 既知の min/max で ratio が手計算値と一致すること."""
# min=40, max=200 → ratio=0.2
profile = self._make_profile([200.0, 150.0, 100.0, 70.0, 40.0])
result = calc_radial_min_max_ratio(profile)
assert result["radial_min_max_ratio"] == pytest.approx(40.0 / 200.0)
def test_uniform_profile_ratio_is_1(self) -> None:
"""仕様: 均一プロファイル(全ビン同値)では ratio=1.0 であること."""
profile = self._make_profile([150.0] * 20)
result = calc_radial_min_max_ratio(profile)
assert result["radial_min_max_ratio"] == pytest.approx(1.0)
def test_ratio_is_between_0_and_1_for_positive_values(self) -> None:
"""仕様: 全値が正のプロファイルでは 0 < ratio <= 1 であること."""
profile = self._make_profile([100.0, 80.0, 60.0, 40.0, 20.0])
result = calc_radial_min_max_ratio(profile)
assert 0.0 < result["radial_min_max_ratio"] <= 1.0
def test_max_is_zero_returns_inf(self) -> None:
"""仕様: max=0 のゼロ除算ガード — float("inf") が返ること."""
profile = self._make_profile([0.0, 0.0, 0.0, 0.0, 0.0])
result = calc_radial_min_max_ratio(profile)
assert result["radial_min_max_ratio"] == float("inf")
def test_min_distance_is_distance_of_minimum_luminance_bin(self) -> None:
"""仕様: radial_min_distance が最小輝度をとるビンの正規化距離であること."""
# 最小値が最後のビン(最外)にある場合
luminances = [100.0, 90.0, 80.0, 70.0, 60.0] # 最後が最小
profile = self._make_profile(luminances)
result = calc_radial_min_max_ratio(profile)
# プロファイルの最小値インデックス = 4(最後)
expected_distance = profile[4, 0]
assert result["radial_min_distance"] == pytest.approx(expected_distance)
def test_min_distance_for_non_monotonic_profile(self) -> None:
"""仕様: 非単調プロファイルでも最小輝度ビンの距離を正しく返すこと."""
# 中間のビンが最小
luminances = [100.0, 120.0, 40.0, 90.0, 80.0] # index 2 が最小
profile = self._make_profile(luminances)
result = calc_radial_min_max_ratio(profile)
expected_distance = profile[2, 0]
assert result["radial_min_distance"] == pytest.approx(expected_distance)
def test_radial_min_equals_min_of_luminance_column(self) -> None:
"""仕様: radial_min がプロファイルの輝度列の最小値であること."""
luminances = [100.0, 50.0, 80.0, 30.0, 70.0]
profile = self._make_profile(luminances)
result = calc_radial_min_max_ratio(profile)
assert result["radial_min"] == pytest.approx(30.0)
def test_radial_max_equals_max_of_luminance_column(self) -> None:
"""仕様: radial_max がプロファイルの輝度列の最大値であること."""
luminances = [100.0, 50.0, 80.0, 30.0, 70.0]
profile = self._make_profile(luminances)
result = calc_radial_min_max_ratio(profile)
assert result["radial_max"] == pytest.approx(100.0)
def test_return_values_are_float_type(self) -> None:
"""仕様: 戻り値の各フィールドが Python float 型であること."""
profile = self._make_profile([100.0, 80.0, 60.0])
result = calc_radial_min_max_ratio(profile)
for key in result:
assert isinstance(result[key], float), f"{key} が float でない: {type(result[key])}"
def test_20_point_profile_standard_size(self) -> None:
"""仕様: 20 点の動径プロファイル(仕様の標準サイズ)で正常に動作すること."""
# 単調減衰 20 点プロファイル(仕様の標準サイズ)
luminances = numpy.linspace(200.0, 100.0, 20).tolist()
profile = self._make_profile(luminances)
result = calc_radial_min_max_ratio(profile)
assert result["radial_min_max_ratio"] == pytest.approx(100.0 / 200.0)
assert result["radial_min"] == pytest.approx(100.0)
assert result["radial_max"] == pytest.approx(200.0)
def test_monotone_decreasing_ratio_is_less_than_1(self) -> None:
"""仕様: 単調減衰プロファイルでは ratio < 1.0 であること."""
luminances = numpy.linspace(200.0, 100.0, 20).tolist()
profile = self._make_profile(luminances)
result = calc_radial_min_max_ratio(profile)
assert result["radial_min_max_ratio"] < 1.0
def test_monotone_decreasing_min_distance_is_at_periphery(self) -> None:
"""仕様: 単調減衰プロファイルでは radial_min_distance が最外付近にあること.
単調減衰(中心明・周辺暗)の合成画像では最小輝度ビンが最外(距離 1.0 付近)になる.
"""
luminances = numpy.linspace(200.0, 100.0, 20).tolist()
profile = self._make_profile(luminances)
result = calc_radial_min_max_ratio(profile)
# 最外ビン(距離が最大のビン)に最小値があること
max_distance = profile[-1, 0]
assert result["radial_min_distance"] == pytest.approx(max_distance)
# ---------------------------------------------------------------------------
# calc_spatial_uniformity の新 4 キー テスト
# ---------------------------------------------------------------------------
class TestCalcSpatialUniformityNewKeys:
"""calc_spatial_uniformity が新 4 キーを含む仕様検証テスト群."""
@pytest.fixture
def sample_luminance(self) -> numpy.ndarray:
"""テスト用の 20x20 均一輝度画像を返す fixture."""
return numpy.full((20, 20), 128.0, dtype=numpy.float64)
@pytest.fixture
def decaying_luminance(self) -> numpy.ndarray:
"""中心明・周辺暗の合成輝度画像(30x30)を返す fixture.
中心座標からの距離に応じて輝度が低下する画像.
"""
h, w = 30, 30
cy, cx = h / 2, w / 2
y_coords, x_coords = numpy.mgrid[0:h, 0:w]
dist = numpy.sqrt(((x_coords - cx) / cx) ** 2 + ((y_coords - cy) / cy) ** 2)
dist_norm = dist / dist.max()
# 中心=200, 周辺=100 の線形減衰
luminance = 200.0 - 100.0 * dist_norm
return luminance.astype(numpy.float64)
def test_return_value_has_radial_min_max_ratio_key(
self, sample_luminance: numpy.ndarray
) -> None:
"""仕様: calc_spatial_uniformity の戻り値に 'radial_min_max_ratio' キーが存在すること."""
result = calc_spatial_uniformity(sample_luminance)
assert "radial_min_max_ratio" in result
def test_return_value_has_radial_min_distance_key(
self, sample_luminance: numpy.ndarray
) -> None:
"""仕様: calc_spatial_uniformity の戻り値に 'radial_min_distance' キーが存在すること."""
result = calc_spatial_uniformity(sample_luminance)
assert "radial_min_distance" in result
def test_return_value_has_radial_min_key(
self, sample_luminance: numpy.ndarray
) -> None:
"""仕様: calc_spatial_uniformity の戻り値に 'radial_min' キーが存在すること."""
result = calc_spatial_uniformity(sample_luminance)
assert "radial_min" in result
def test_return_value_has_radial_max_key(
self, sample_luminance: numpy.ndarray
) -> None:
"""仕様: calc_spatial_uniformity の戻り値に 'radial_max' キーが存在すること."""
result = calc_spatial_uniformity(sample_luminance)
assert "radial_max" in result
def test_existing_keys_are_preserved(self, sample_luminance: numpy.ndarray) -> None:
"""仕様: 既存キー zone_stats / radial_profile / zone_map が変わらず保持されること."""
result = calc_spatial_uniformity(sample_luminance)
assert "zone_stats" in result
assert "radial_profile" in result
assert "zone_map" in result
def test_radial_min_max_ratio_type_is_float(
self, sample_luminance: numpy.ndarray
) -> None:
"""仕様: radial_min_max_ratio が float 型であること."""
result = calc_spatial_uniformity(sample_luminance)
assert isinstance(result["radial_min_max_ratio"], float)
def test_radial_min_distance_type_is_float(
self, sample_luminance: numpy.ndarray
) -> None:
"""仕様: radial_min_distance が float 型であること."""
result = calc_spatial_uniformity(sample_luminance)
assert isinstance(result["radial_min_distance"], float)
def test_uniform_image_ratio_is_1(self, sample_luminance: numpy.ndarray) -> None:
"""仕様: 均一輝度画像では radial_min_max_ratio が 1.0 であること."""
result = calc_spatial_uniformity(sample_luminance)
assert result["radial_min_max_ratio"] == pytest.approx(1.0, abs=1e-5)
def test_decaying_image_ratio_is_between_0_and_1(
self, decaying_luminance: numpy.ndarray
) -> None:
"""仕様: 単調減衰画像では 0 < radial_min_max_ratio < 1 であること."""
result = calc_spatial_uniformity(decaying_luminance)
ratio = result["radial_min_max_ratio"]
assert 0.0 < ratio < 1.0
def test_decaying_image_min_distance_is_at_periphery(
self, decaying_luminance: numpy.ndarray
) -> None:
"""仕様: 単調減衰画像では radial_min_distance が最外付近(> 0.5)にあること."""
result = calc_spatial_uniformity(decaying_luminance)
# 中心明・周辺暗なので最小輝度は周辺側にあるはず
assert result["radial_min_distance"] > 0.5
def test_radial_min_max_ratio_is_consistent_with_radial_profile(
self, sample_luminance: numpy.ndarray
) -> None:
"""仕様: radial_min_max_ratio が radial_profile の min/max 比と一致すること."""
result = calc_spatial_uniformity(sample_luminance)
profile = result["radial_profile"]
luminances = profile[:, 1]
expected_ratio = float(numpy.min(luminances)) / float(numpy.max(luminances)) if float(numpy.max(luminances)) != 0.0 else float("inf")
assert result["radial_min_max_ratio"] == pytest.approx(expected_ratio, abs=1e-6)
def test_new_keys_do_not_overwrite_zone_stats_content(
self, sample_luminance: numpy.ndarray
) -> None:
"""仕様: 新 4 キーの追加が zone_stats の内容を変更しないこと."""
result = calc_spatial_uniformity(sample_luminance)
zone_stats = result["zone_stats"]
expected_zone_keys = {
"center_mean", "center_std",
"middle_mean", "middle_std",
"periphery_mean", "periphery_std",
"center_periphery_ratio", "gradient_magnitude",
}
assert set(zone_stats.keys()) == expected_zone_keys
# ---------------------------------------------------------------------------
# SPATIAL_FIELDNAMES の新カラム テスト
# ---------------------------------------------------------------------------
class TestSpatialFieldnamesNewColumns:
"""SPATIAL_FIELDNAMES に新カラムが追加された仕様検証テスト群."""
def test_spatial_fieldnames_contains_radial_min_max_ratio(self) -> None:
"""仕様: SPATIAL_FIELDNAMES に 'radial_min_max_ratio' が含まれること."""
assert "radial_min_max_ratio" in SPATIAL_FIELDNAMES
def test_spatial_fieldnames_contains_radial_min_distance(self) -> None:
"""仕様: SPATIAL_FIELDNAMES に 'radial_min_distance' が含まれること."""
assert "radial_min_distance" in SPATIAL_FIELDNAMES
def test_spatial_fieldnames_still_contains_base_columns(self) -> None:
"""仕様: SPATIAL_FIELDNAMES が既存の 6 基本カラムをすべて含むこと(削除・改名なし)."""
base_fields = [
"image_name",
"center_mean",
"middle_mean",
"periphery_mean",
"center_periphery_ratio",
"gradient_magnitude",
]
for field in base_fields:
assert field in SPATIAL_FIELDNAMES, (
f"必須フィールド '{field}' が SPATIAL_FIELDNAMES から消えている(削除・改名は禁止)"
)
def test_spatial_fieldnames_has_at_least_8_columns(self) -> None:
"""仕様: SPATIAL_FIELDNAMES が基本 6 + 新規 2 = 最低 8 カラムを持つこと."""
assert len(SPATIAL_FIELDNAMES) >= 8
# ---------------------------------------------------------------------------
# export_spatial_summary の新指標書き出し テスト
# ---------------------------------------------------------------------------
class TestExportSpatialSummaryNewMetrics:
"""export_spatial_summary が新指標を書き出せる仕様検証テスト群."""
@pytest.fixture
def sample_spatial_results_with_new_metrics(self) -> list[dict]:
"""新 2 指標(radial_min_max_ratio / radial_min_distance)を含む空間解析結果リスト."""
return [
{
"image_name": "image_001",
"center_mean": 180.0,
"middle_mean": 165.0,
"periphery_mean": 140.0,
"center_periphery_ratio": 1.29,
"gradient_magnitude": 22.2,
"radial_min_max_ratio": 0.75,
"radial_min_distance": 0.95,
},
{
"image_name": "image_002",
"center_mean": 200.0,
"middle_mean": 190.0,
"periphery_mean": 170.0,
"center_periphery_ratio": 1.18,
"gradient_magnitude": 15.0,
"radial_min_max_ratio": 0.82,
"radial_min_distance": 0.90,
},
]
def test_csv_contains_radial_min_max_ratio_column(
self,
sample_spatial_results_with_new_metrics: list[dict],
tmp_path: Path,
) -> None:
"""仕様: 出力 CSV のヘッダーに 'radial_min_max_ratio' が含まれること."""
output_path = str(tmp_path / "spatial_new.csv")
export_spatial_summary(sample_spatial_results_with_new_metrics, output_path)
with open(output_path, encoding="utf-8") as f:
reader = csv.DictReader(f)
fieldnames = reader.fieldnames
assert "radial_min_max_ratio" in fieldnames
def test_csv_contains_radial_min_distance_column(
self,
sample_spatial_results_with_new_metrics: list[dict],
tmp_path: Path,
) -> None:
"""仕様: 出力 CSV のヘッダーに 'radial_min_distance' が含まれること."""
output_path = str(tmp_path / "spatial_new.csv")
export_spatial_summary(sample_spatial_results_with_new_metrics, output_path)
with open(output_path, encoding="utf-8") as f:
reader = csv.DictReader(f)
fieldnames = reader.fieldnames
assert "radial_min_distance" in fieldnames
def test_csv_radial_min_max_ratio_values_are_written_correctly(
self,
sample_spatial_results_with_new_metrics: list[dict],
tmp_path: Path,
) -> None:
"""仕様: radial_min_max_ratio の値が正しく書き込まれること."""
output_path = str(tmp_path / "spatial_new.csv")
export_spatial_summary(sample_spatial_results_with_new_metrics, output_path)
with open(output_path, encoding="utf-8") as f:
reader = csv.DictReader(f)
rows = list(reader)
assert float(rows[0]["radial_min_max_ratio"]) == pytest.approx(0.75)
assert float(rows[1]["radial_min_max_ratio"]) == pytest.approx(0.82)
def test_csv_radial_min_distance_values_are_written_correctly(
self,
sample_spatial_results_with_new_metrics: list[dict],
tmp_path: Path,
) -> None:
"""仕様: radial_min_distance の値が正しく書き込まれること."""
output_path = str(tmp_path / "spatial_new.csv")
export_spatial_summary(sample_spatial_results_with_new_metrics, output_path)
with open(output_path, encoding="utf-8") as f:
reader = csv.DictReader(f)
rows = list(reader)
assert float(rows[0]["radial_min_distance"]) == pytest.approx(0.95)
assert float(rows[1]["radial_min_distance"]) == pytest.approx(0.90)
def test_csv_fieldnames_match_spatial_fieldnames_constant(
self,
sample_spatial_results_with_new_metrics: list[dict],
tmp_path: Path,
) -> None:
"""仕様: CSV ヘッダーが SPATIAL_FIELDNAMES 定数と完全一致すること."""
output_path = str(tmp_path / "spatial_new.csv")
export_spatial_summary(sample_spatial_results_with_new_metrics, output_path)
with open(output_path, encoding="utf-8") as f:
reader = csv.DictReader(f)
fieldnames = reader.fieldnames
assert list(fieldnames) == SPATIAL_FIELDNAMES
# ---------------------------------------------------------------------------
# calc_batch_statistics の新指標集計 テスト
# ---------------------------------------------------------------------------
class TestCalcBatchStatisticsNewMetrics:
"""calc_batch_statistics が新 2 指標(radial_min_max_ratio / radial_min_distance)を
集計する仕様検証テスト群."""
@pytest.fixture
def results_with_radial_metrics(self) -> list[dict]:
"""radial_min_max_ratio / radial_min_distance を含む 2 画像分の結果リスト."""
return [
{
"image_name": "image_001",
"mean": 200.0,
"std": 10.0,
"cov": 0.05,
"max_min_ratio": 1.2,
"max": 220.0,
"min": 180.0,
"center_mean": 180.0,
"middle_mean": 165.0,
"periphery_mean": 140.0,
"center_periphery_ratio": 1.286,
"gradient_magnitude": 22.2,
"radial_min_max_ratio": 0.75,
"radial_min_distance": 0.95,
},
{
"image_name": "image_002",
"mean": 210.0,
"std": 20.0,
"cov": 0.095,
"max_min_ratio": 1.4,
"max": 240.0,
"min": 160.0,
"center_mean": 200.0,
"middle_mean": 185.0,
"periphery_mean": 160.0,
"center_periphery_ratio": 1.25,
"gradient_magnitude": 20.0,
"radial_min_max_ratio": 0.80,
"radial_min_distance": 0.90,
},
]
def test_spatial_contains_radial_min_max_ratio(
self, results_with_radial_metrics: list[dict]
) -> None:
"""仕様: calc_batch_statistics の spatial 部に 'radial_min_max_ratio' が含まれること."""
from src.export.exporter import calc_batch_statistics
result = calc_batch_statistics(results_with_radial_metrics)
assert "radial_min_max_ratio" in result["spatial"]
def test_spatial_contains_radial_min_distance(
self, results_with_radial_metrics: list[dict]
) -> None:
"""仕様: calc_batch_statistics の spatial 部に 'radial_min_distance' が含まれること."""
from src.export.exporter import calc_batch_statistics
result = calc_batch_statistics(results_with_radial_metrics)
assert "radial_min_distance" in result["spatial"]
def test_radial_min_max_ratio_mean_is_correct(
self, results_with_radial_metrics: list[dict]
) -> None:
"""仕様: radial_min_max_ratio の mean が (0.75 + 0.80) / 2 = 0.775 であること."""
from src.export.exporter import calc_batch_statistics
result = calc_batch_statistics(results_with_radial_metrics)
assert result["spatial"]["radial_min_max_ratio"]["mean"] == pytest.approx(0.775)
def test_radial_min_distance_mean_is_correct(
self, results_with_radial_metrics: list[dict]
) -> None:
"""仕様: radial_min_distance の mean が (0.95 + 0.90) / 2 = 0.925 であること."""
from src.export.exporter import calc_batch_statistics
result = calc_batch_statistics(results_with_radial_metrics)
assert result["spatial"]["radial_min_distance"]["mean"] == pytest.approx(0.925)
def test_radial_min_max_ratio_std_is_population_std(
self, results_with_radial_metrics: list[dict]
) -> None:
"""仕様: radial_min_max_ratio の std が母集団標準偏差(ddof=0)で算出されること."""
from src.export.exporter import calc_batch_statistics
result = calc_batch_statistics(results_with_radial_metrics)
expected_std = float(np.std([0.75, 0.80], ddof=0))
assert result["spatial"]["radial_min_max_ratio"]["std"] == pytest.approx(expected_std)
def test_radial_min_max_ratio_has_five_stat_subkeys(
self, results_with_radial_metrics: list[dict]
) -> None:
"""仕様: radial_min_max_ratio が mean/std/min/max/cv の 5 サブキーを持つこと."""
from src.export.exporter import calc_batch_statistics
result = calc_batch_statistics(results_with_radial_metrics)
expected_subkeys = {"mean", "std", "min", "max", "cv"}
assert expected_subkeys == set(
result["spatial"]["radial_min_max_ratio"].keys()
)
def test_radial_min_distance_has_five_stat_subkeys(
self, results_with_radial_metrics: list[dict]
) -> None:
"""仕様: radial_min_distance が mean/std/min/max/cv の 5 サブキーを持つこと."""
from src.export.exporter import calc_batch_statistics
result = calc_batch_statistics(results_with_radial_metrics)
expected_subkeys = {"mean", "std", "min", "max", "cv"}
assert expected_subkeys == set(
result["spatial"]["radial_min_distance"].keys()
)
# ---------------------------------------------------------------------------
# plot_radial_profile の最大/最小マーカー テスト
# ---------------------------------------------------------------------------
class TestPlotRadialProfileMinMaxMarkers:
"""plot_radial_profile に最大/最小マーカーと注記が追加された仕様検証テスト群.
テスト方針: 可視化テストは「出力ファイルが生成されること程度でよい」(GUIDE_08).
ここではファイル生成確認と、既知のプロファイルで例外が発生しないことを確認する.
"""
@pytest.fixture(autouse=True)
def _setup_mpl(self) -> None:
"""GUI なし環境でも動作するバックエンドを設定する."""
import matplotlib
matplotlib.use("Agg")
@pytest.fixture
def monotone_profile(self) -> numpy.ndarray:
"""単調減衰の 20 点動径プロファイル(min/max が両端に存在)."""
distances = numpy.linspace(0.025, 0.975, 20)
luminances = numpy.linspace(200.0, 100.0, 20)
return numpy.column_stack([distances, luminances])
@pytest.fixture
def non_monotone_profile(self) -> numpy.ndarray:
"""非単調の 20 点動径プロファイル(中間に極値が存在)."""
distances = numpy.linspace(0.025, 0.975, 20)
# 中間に谷があるプロファイル
luminances = numpy.array(
[200.0, 190.0, 180.0, 170.0, 160.0, 150.0, 140.0, 130.0, 120.0, 110.0,
80.0, 110.0, 120.0, 130.0, 140.0, 150.0, 160.0, 170.0, 180.0, 190.0],
dtype=numpy.float64,
)
return numpy.column_stack([distances, luminances])
@pytest.fixture
def uniform_profile(self) -> numpy.ndarray:
"""均一の 20 点動径プロファイル(min=max の境界ケース)."""
distances = numpy.linspace(0.025, 0.975, 20)
luminances = numpy.full(20, 150.0)
return numpy.column_stack([distances, luminances])
def test_output_file_is_created_with_monotone_profile(
self, monotone_profile: numpy.ndarray, tmp_path: Path
) -> None:
"""仕様: 単調減衰プロファイルで出力ファイルが生成されること."""
from src.visualization.plotter import plot_radial_profile
output_path = str(tmp_path / "radial_monotone.png")
plot_radial_profile(monotone_profile, output_path)
assert Path(output_path).exists()
assert Path(output_path).stat().st_size > 0
def test_output_file_is_created_with_non_monotone_profile(
self, non_monotone_profile: numpy.ndarray, tmp_path: Path
) -> None:
"""仕様: 非単調プロファイルでも出力ファイルが生成されること(例外なし)."""
from src.visualization.plotter import plot_radial_profile
output_path = str(tmp_path / "radial_non_monotone.png")
plot_radial_profile(non_monotone_profile, output_path)
assert Path(output_path).exists()
def test_output_file_is_created_with_uniform_profile(
self, uniform_profile: numpy.ndarray, tmp_path: Path
) -> None:
"""仕様: min=max の均一プロファイルでもゼロ除算なく出力ファイルが生成されること."""
from src.visualization.plotter import plot_radial_profile
output_path = str(tmp_path / "radial_uniform.png")
# min=max → ratio=1.0 のケース(ゼロ除算は発生しない)
plot_radial_profile(uniform_profile, output_path)
assert Path(output_path).exists()
def test_signature_is_unchanged(self) -> None:
"""仕様: plot_radial_profile のシグネチャが不変であること(引数 radial_profile, output_path)."""
import inspect
from src.visualization.plotter import plot_radial_profile
sig = inspect.signature(plot_radial_profile)
params = list(sig.parameters.keys())
assert params == ["radial_profile", "output_path"], (
f"シグネチャが変更されている: {params}"
)