Newer
Older
SmTIAS-LightSim / tests / cli / test_evaluate_normalization.py
"""cli/evaluate.py の normalization(linear mean anchor 正規化)に関する仕様ベーステスト.

仕様出典:
- 実装サマリー(CLI 後方互換・ログ出力・metrics.csv mean 一致・形状指標不変性)
- docs/04_SPEC/SPEC_04_評価仕様.md(評価パイプライン Step 6: normalization)
- docs/04_SPEC/SPEC_03_基準設計の物理構成.md(「形状指標への影響(重要)」節)
- .claude/rules/testing.md(テスト方針)

テスト方針:
  - subprocess ではなく main() を直接呼ぶ(速度・カバレッジのため)
  - ファイル I/O はモックせず tmp_path フィクスチャで実ファイルを使う (testing.md)
  - 乱数を使う場合は seed を必ず固定する (testing.md)
  - CLI 統合テストとして「パイプライン全体を通した振る舞い」を検証する

注意:
  matplotlib の Agg バックエンドは evaluate.py 内部で設定される.
  テスト側でも安全のため import 前に設定する.
"""

from __future__ import annotations

import matplotlib  # noqa: I001

# headless 環境でも動くよう Agg バックエンドを pyplot より前に設定する
matplotlib.use("Agg")

from pathlib import Path

import numpy
import pytest

from sm_tias_lightsim.cli.evaluate import main

# ---------------------------------------------------------------------------
# テスト用定数
# ---------------------------------------------------------------------------

#: output/baseline/luminance.npy の絶対パス(実際に存在する前提)
BASELINE_LUMINANCE_NPY = (
    Path(__file__).parent.parent.parent / "output" / "baseline" / "luminance.npy"
)

#: configs/eval_baseline_linear.yaml の絶対パス
EVAL_BASELINE_LINEAR_YAML = (
    Path(__file__).parent.parent.parent / "configs" / "eval_baseline_linear.yaml"
)

#: configs/eval_baseline.yaml の絶対パス(OETF on, normalization なし)
EVAL_BASELINE_YAML = (
    Path(__file__).parent.parent.parent / "configs" / "eval_baseline.yaml"
)

#: 基準 baseline 絶対値スナップショット(CoV / max-min / C/P / 勾配)の xfail 理由.
#  これらの期待値は特定時点の output/baseline/luminance.npy(.gitignore 対象・キャリブ中に
#  再生成される一時成果物)に紐づくため,キャリブレーション進行中は実値とドリフトする.
#  評価パイプライン自体の正しさは本ファイルの「正規化不変性 / mean 一致」テスト群が担保する
#  (絶対値スナップショットはそれとは別の脆い検証).キャリブ確定時に期待値を更新するか撤去する.
_CALIBRATION_XFAIL_REASON = (
    "基準 baseline の絶対値スナップショット.期待値は特定時点の render 成果物"
    "(output/baseline/luminance.npy・gitignore 対象)に依存し,キャリブレーション進行中は"
    "ドリフトする.キャリブ確定時に期待値更新 or 撤去する."
)

# ---------------------------------------------------------------------------
# テスト用ヘルパー
# ---------------------------------------------------------------------------


def _make_luminance_array(seed: int = 0, size: int = 256) -> numpy.ndarray:
    """ゼロを含まない合成輝度配列を返す(seed 固定).

    max/min 計算が走るようゼロを含まない値にする (testing.md 補助ガイダンス)
    """
    rng = numpy.random.RandomState(seed)
    return numpy.full((size, size), 100.0) + rng.normal(0, 5, (size, size))


def _make_normalization_yaml(
    tmp_path: Path,
    target_mean: float = 146.7146,
    x: int = 32,
    y: int = 32,
    width: int = 192,
    height: int = 192,
    zones_enabled: bool = True,
    radial_profile: bool = True,
) -> Path:
    """normalization.enabled=true を含む評価 YAML を tmp_path に書き出す.

    仕様: eval_baseline_linear.yaml の normalization セクション相当
    """
    content = f"""\
roi:
  type: image_pixel
  x: {x}
  y: {y}
  width: {width}
  height: {height}
zones:
  enabled: {'true' if zones_enabled else 'false'}
  boundaries: [0.33, 0.66]
metrics:
  cov: true
  std: true
  max_min: true
  radial_profile: {'true' if radial_profile else 'false'}
normalization:
  enabled: true
  type: mean_anchor
  target_mean: {target_mean}
"""
    yaml_path = tmp_path / "eval_normalization.yaml"
    yaml_path.write_text(content, encoding="utf-8")
    return yaml_path


def _make_plain_yaml(
    tmp_path: Path,
    x: int = 32,
    y: int = 32,
    width: int = 192,
    height: int = 192,
) -> Path:
    """normalization なし(後方互換)の評価 YAML を tmp_path に書き出す."""
    content = f"""\
roi:
  type: image_pixel
  x: {x}
  y: {y}
  width: {width}
  height: {height}
zones:
  enabled: true
  boundaries: [0.33, 0.66]
metrics:
  cov: true
  std: true
  max_min: true
  radial_profile: true
"""
    yaml_path = tmp_path / "eval_plain.yaml"
    yaml_path.write_text(content, encoding="utf-8")
    return yaml_path


def _run_main(npy_path: Path, config_path: Path, output_dir: Path) -> int:
    """main() を呼ぶ共通ラッパー(行長削減のため)."""
    return main(
        ["--image", str(npy_path), "--config", str(config_path), "--output", str(output_dir)]
    )


# ---------------------------------------------------------------------------
# 1. 正常系 — normalization.enabled=true で main() が 0 を返すこと
# ---------------------------------------------------------------------------


def test_normalization_enabled_returns_0(tmp_path: Path):
    """normalization.enabled=true を含む YAML で main() が 0 を返すこと.

    仕様: normalization.enabled=True のとき ROI 抽出後・OETF 前に apply_mean_anchor を適用する
         (cli/evaluate.py main 処理フロー)
    """
    luminance = _make_luminance_array(seed=300)
    npy_path = tmp_path / "luminance.npy"
    numpy.save(npy_path, luminance)
    config_path = _make_normalization_yaml(tmp_path, target_mean=80.0)
    output_dir = tmp_path / "output"
    result = _run_main(npy_path, config_path, output_dir)
    assert result == 0


def test_normalization_enabled_creates_metrics_csv(tmp_path: Path):
    """normalization.enabled=true のとき metrics.csv が生成されること.

    仕様: normalization 後の ROI で均一性指標を算出し CSV に保存する
    """
    luminance = _make_luminance_array(seed=301)
    npy_path = tmp_path / "luminance.npy"
    numpy.save(npy_path, luminance)
    config_path = _make_normalization_yaml(tmp_path, target_mean=100.0)
    output_dir = tmp_path / "output"
    _run_main(npy_path, config_path, output_dir)
    assert (output_dir / "metrics.csv").exists()


# ---------------------------------------------------------------------------
# 2. mean が target_mean に一致すること
# ---------------------------------------------------------------------------


def test_normalization_mean_equals_target(tmp_path: Path):
    """normalization.enabled=true のとき metrics.csv の mean が target_mean に一致すること.

    仕様(実装サマリー「線形ゲインの不変性」,CLI 後方互換):
        metrics.csv の mean が target_mean に一致する
    NOTE: 合成データは ROI = 配列全体と一致する設定にする
          (size=256, ROI x=0,y=0,w=256,h=256 → ROI mean == 配列 mean)
    """
    import pandas

    rng = numpy.random.RandomState(302)
    luminance = numpy.full((256, 256), 100.0) + rng.normal(0, 5, (256, 256))
    npy_path = tmp_path / "luminance.npy"
    numpy.save(npy_path, luminance)

    target_mean = 146.7146
    # ROI = 配列全体にして ROI mean が配列 mean に一致するようにする
    content = f"""\
roi:
  type: image_pixel
  x: 0
  y: 0
  width: 256
  height: 256
normalization:
  enabled: true
  type: mean_anchor
  target_mean: {target_mean}
"""
    config_path = tmp_path / "eval_full_roi.yaml"
    config_path.write_text(content, encoding="utf-8")

    output_dir = tmp_path / "output"
    _run_main(npy_path, config_path, output_dir)

    df = pandas.read_csv(output_dir / "metrics.csv")
    mean_val = float(df["mean"].iloc[0])
    # apply_mean_anchor の後 mean は target_mean に厳密一致するはず
    assert mean_val == pytest.approx(target_mean, rel=1e-6)


# ---------------------------------------------------------------------------
# 3. 形状指標の不変性(CoV / max-min / C/P / 勾配)
# ---------------------------------------------------------------------------


def test_normalization_cov_same_as_without_normalization(tmp_path: Path):
    """normalization 適用前後で CoV が不変であること(CLI レベル検証).

    仕様(実装サマリー「線形ゲインの不変性」):
        線形ゲインは CoV に不変
        normalization 後の CoV == normalization 前の CoV
    """
    import pandas

    rng = numpy.random.RandomState(303)
    luminance = numpy.full((256, 256), 80.0) + rng.normal(0, 5, (256, 256))
    npy_path = tmp_path / "luminance.npy"
    numpy.save(npy_path, luminance)

    roi_kwargs = {"x": 0, "y": 0, "width": 256, "height": 256}

    # normalization なし
    config_no_norm = _make_plain_yaml(tmp_path, **roi_kwargs)
    out_no_norm = tmp_path / "output_no_norm"
    _run_main(npy_path, config_no_norm, out_no_norm)
    df_no_norm = pandas.read_csv(out_no_norm / "metrics.csv")
    cov_no_norm = float(df_no_norm["cov"].iloc[0])

    # normalization あり
    content_norm = """\
roi:
  type: image_pixel
  x: 0
  y: 0
  width: 256
  height: 256
normalization:
  enabled: true
  type: mean_anchor
  target_mean: 200.0
"""
    config_norm = tmp_path / "eval_norm.yaml"
    config_norm.write_text(content_norm, encoding="utf-8")
    out_norm = tmp_path / "output_norm"
    _run_main(npy_path, config_norm, out_norm)
    df_norm = pandas.read_csv(out_norm / "metrics.csv")
    cov_norm = float(df_norm["cov"].iloc[0])

    # 線形ゲインで CoV は変わらない
    assert cov_norm == pytest.approx(cov_no_norm, rel=1e-8)


def test_normalization_max_min_ratio_same_as_without(tmp_path: Path):
    """normalization 適用前後で max/min 比が不変であること(CLI レベル検証).

    仕様(実装サマリー「線形ゲインの不変性」):
        線形ゲインは max/min 比に不変
    """
    import pandas

    rng = numpy.random.RandomState(304)
    luminance = numpy.full((256, 256), 80.0) + rng.uniform(1.0, 20.0, (256, 256))
    npy_path = tmp_path / "luminance.npy"
    numpy.save(npy_path, luminance)

    roi_kwargs = {"x": 0, "y": 0, "width": 256, "height": 256}

    config_no_norm = _make_plain_yaml(tmp_path, **roi_kwargs)
    out_no_norm = tmp_path / "output_no_norm"
    _run_main(npy_path, config_no_norm, out_no_norm)
    df_no_norm = pandas.read_csv(out_no_norm / "metrics.csv")
    ratio_before = float(df_no_norm["max_min_ratio"].iloc[0])

    content_norm = """\
roi:
  type: image_pixel
  x: 0
  y: 0
  width: 256
  height: 256
normalization:
  enabled: true
  type: mean_anchor
  target_mean: 50.0
"""
    config_norm = tmp_path / "eval_norm.yaml"
    config_norm.write_text(content_norm, encoding="utf-8")
    out_norm = tmp_path / "output_norm"
    _run_main(npy_path, config_norm, out_norm)
    df_norm = pandas.read_csv(out_norm / "metrics.csv")
    ratio_after = float(df_norm["max_min_ratio"].iloc[0])

    assert ratio_after == pytest.approx(ratio_before, rel=1e-8)


def test_normalization_gradient_same_as_without(tmp_path: Path):
    """normalization 適用前後で勾配(gradient_magnitude)が不変であること.

    仕様(実装サマリー「線形ゲインの不変性」):
        線形ゲインは勾配 % に不変
    """
    import pandas

    rng = numpy.random.RandomState(305)
    luminance = numpy.full((256, 256), 80.0) + rng.normal(0, 5, (256, 256))
    npy_path = tmp_path / "luminance.npy"
    numpy.save(npy_path, luminance)

    roi_kwargs = {"x": 0, "y": 0, "width": 256, "height": 256}

    config_no_norm = _make_plain_yaml(tmp_path, **roi_kwargs)
    out_no_norm = tmp_path / "output_no_norm"
    _run_main(npy_path, config_no_norm, out_no_norm)
    df_no_norm = pandas.read_csv(out_no_norm / "metrics.csv")
    grad_before = float(df_no_norm["gradient_magnitude"].iloc[0])

    content_norm = """\
roi:
  type: image_pixel
  x: 0
  y: 0
  width: 256
  height: 256
zones:
  enabled: true
  boundaries: [0.33, 0.66]
normalization:
  enabled: true
  type: mean_anchor
  target_mean: 200.0
"""
    config_norm = tmp_path / "eval_norm.yaml"
    config_norm.write_text(content_norm, encoding="utf-8")
    out_norm = tmp_path / "output_norm"
    _run_main(npy_path, config_norm, out_norm)
    df_norm = pandas.read_csv(out_norm / "metrics.csv")
    grad_after = float(df_norm["gradient_magnitude"].iloc[0])

    assert grad_after == pytest.approx(grad_before, rel=1e-8)


def test_normalization_cp_ratio_same_as_without(tmp_path: Path):
    """normalization 適用前後で C/P 比(center_periphery_ratio)が不変であること.

    仕様(実装サマリー「線形ゲインの不変性」):
        線形ゲインは C/P 比に不変
    """
    import pandas

    rng = numpy.random.RandomState(306)
    luminance = numpy.full((256, 256), 80.0) + rng.normal(0, 5, (256, 256))
    npy_path = tmp_path / "luminance.npy"
    numpy.save(npy_path, luminance)

    roi_kwargs = {"x": 0, "y": 0, "width": 256, "height": 256}

    config_no_norm = _make_plain_yaml(tmp_path, **roi_kwargs)
    out_no_norm = tmp_path / "output_no_norm"
    _run_main(npy_path, config_no_norm, out_no_norm)
    df_no_norm = pandas.read_csv(out_no_norm / "metrics.csv")
    cp_before = float(df_no_norm["center_periphery_ratio"].iloc[0])

    content_norm = """\
roi:
  type: image_pixel
  x: 0
  y: 0
  width: 256
  height: 256
zones:
  enabled: true
  boundaries: [0.33, 0.66]
normalization:
  enabled: true
  type: mean_anchor
  target_mean: 150.0
"""
    config_norm = tmp_path / "eval_norm.yaml"
    config_norm.write_text(content_norm, encoding="utf-8")
    out_norm = tmp_path / "output_norm"
    _run_main(npy_path, config_norm, out_norm)
    df_norm = pandas.read_csv(out_norm / "metrics.csv")
    cp_after = float(df_norm["center_periphery_ratio"].iloc[0])

    assert cp_after == pytest.approx(cp_before, rel=1e-8)


# ---------------------------------------------------------------------------
# 4. ログ出力の確認
# ---------------------------------------------------------------------------


def test_normalization_prints_alpha_and_target_mean(tmp_path: Path, capsys):
    """normalization.enabled=true のとき標準出力に alpha と target_mean が表示されること.

    仕様(実装サマリー「CLI 後方互換」):
        `normalization  : alpha=...  target_mean=...` のログが出る
    """
    luminance = _make_luminance_array(seed=307)
    npy_path = tmp_path / "luminance.npy"
    numpy.save(npy_path, luminance)
    config_path = _make_normalization_yaml(tmp_path, target_mean=100.0)
    output_dir = tmp_path / "output"
    _run_main(npy_path, config_path, output_dir)

    captured = capsys.readouterr()
    # cli/evaluate.py の print フォーマット: "  normalization  : alpha=...  target_mean=..."
    assert "normalization  : alpha=" in captured.out
    assert "target_mean=" in captured.out


def test_normalization_disabled_no_normalization_log(tmp_path: Path, capsys):
    """normalization.enabled=false(または省略)のとき normalization ログが出ないこと.

    仕様: normalization が無効なとき _apply_normalization_to_luminance は呼ばれない
         (cli/evaluate.py main: `if norm_cfg is not None and norm_cfg.enabled`)
    NOTE: ログフォーマット "normalization  : alpha=" を検索する(パス名との混同を避ける)
    """
    luminance = _make_luminance_array(seed=308)
    npy_path = tmp_path / "luminance.npy"
    numpy.save(npy_path, luminance)
    config_path = _make_plain_yaml(tmp_path)
    output_dir = tmp_path / "output"
    _run_main(npy_path, config_path, output_dir)

    captured = capsys.readouterr()
    # "normalization  : alpha=" はログとして出力されるフォーマット(cli/evaluate.py)
    # normalization が無効なときはこのログは出ない
    assert "normalization  : alpha=" not in captured.out


# ---------------------------------------------------------------------------
# 5. 後方互換性 — normalization なし既存 YAML は動作変更なし
# ---------------------------------------------------------------------------


def test_backward_compat_no_normalization_key(tmp_path: Path):
    """normalization キーなしの既存 YAML が後方互換で動くこと.

    仕様(実装サマリー「CLI 後方互換」):
        normalization セクションがない既存 config(eval_baseline.yaml, eval_real.yaml)は
        動作変更なし
    """
    luminance = _make_luminance_array(seed=309)
    npy_path = tmp_path / "luminance.npy"
    numpy.save(npy_path, luminance)
    config_path = _make_plain_yaml(tmp_path)
    output_dir = tmp_path / "output"
    result = _run_main(npy_path, config_path, output_dir)
    assert result == 0
    assert (output_dir / "metrics.csv").exists()


def test_backward_compat_oetf_yaml_unaffected(tmp_path: Path):
    """oetf.enabled=true の既存 YAML(eval_baseline.yaml 相当)が動作変更なしであること.

    仕様(実装サマリー「CLI 後方互換」):
        eval_baseline.yaml (OETF on) は動作変更なし
    """
    luminance = _make_luminance_array(seed=310)
    npy_path = tmp_path / "luminance.npy"
    numpy.save(npy_path, luminance)

    # eval_baseline.yaml 相当(OETF on, normalization なし)
    content = """\
roi:
  type: image_pixel
  x: 32
  y: 32
  width: 192
  height: 192
oetf:
  enabled: true
  transfer: srgb
  exposure:
    mode: anchor_mean
    target_mean: 146.7146
  quantize_uint8: true
"""
    config_path = tmp_path / "eval_oetf_only.yaml"
    config_path.write_text(content, encoding="utf-8")
    output_dir = tmp_path / "output"
    result = _run_main(npy_path, config_path, output_dir)
    assert result == 0
    assert (output_dir / "metrics.csv").exists()


# ---------------------------------------------------------------------------
# 6. 異常系 — OETF + normalization 同時有効化で main() が 1 を返すこと
# ---------------------------------------------------------------------------


def test_oetf_and_normalization_both_enabled_returns_1(tmp_path: Path):
    """oetf.enabled=true かつ normalization.enabled=true のとき main() が 1 を返すこと.

    仕様(実装サマリー「スキーマ排他性」):
        oetf.enabled=True かつ normalization.enabled=True は ValidationError
        → cli/evaluate.py は return 1
    """
    luminance = _make_luminance_array(seed=311)
    npy_path = tmp_path / "luminance.npy"
    numpy.save(npy_path, luminance)

    content = """\
roi:
  type: image_pixel
  x: 32
  y: 32
  width: 192
  height: 192
oetf:
  enabled: true
  transfer: srgb
  exposure:
    mode: anchor_mean
    target_mean: 146.7146
  quantize_uint8: true
normalization:
  enabled: true
  type: mean_anchor
  target_mean: 146.7146
"""
    config_path = tmp_path / "eval_both_enabled.yaml"
    config_path.write_text(content, encoding="utf-8")
    output_dir = tmp_path / "output"
    result = _run_main(npy_path, config_path, output_dir)
    assert result == 1


def test_oetf_and_normalization_both_enabled_stderr_message(tmp_path: Path, capsys):
    """oetf + normalization 同時有効化時にエラーメッセージが stderr に出ること."""
    luminance = _make_luminance_array(seed=312)
    npy_path = tmp_path / "luminance.npy"
    numpy.save(npy_path, luminance)

    content = """\
roi:
  type: image_pixel
  x: 32
  y: 32
  width: 192
  height: 192
oetf:
  enabled: true
  transfer: srgb
  exposure:
    mode: anchor_mean
    target_mean: 146.7146
normalization:
  enabled: true
  type: mean_anchor
  target_mean: 146.7146
"""
    config_path = tmp_path / "eval_both_enabled.yaml"
    config_path.write_text(content, encoding="utf-8")
    output_dir = tmp_path / "output"
    main(["--image", str(npy_path), "--config", str(config_path), "--output", str(output_dir)])

    captured = capsys.readouterr()
    assert "エラー" in captured.err


# ---------------------------------------------------------------------------
# 7. 統合テスト — output/baseline/luminance.npy + eval_baseline_linear.yaml
# ---------------------------------------------------------------------------


@pytest.mark.skipif(
    not BASELINE_LUMINANCE_NPY.exists(),
    reason="output/baseline/luminance.npy が存在しない場合はスキップ",
)
def test_baseline_linear_yaml_returns_0():
    """output/baseline/luminance.npy + configs/eval_baseline_linear.yaml で main() が 0 を返すこと.

    仕様(実装サマリー「動作確認済み(参考)」):
        入力: output/baseline/luminance.npy
        設定: configs/eval_baseline_linear.yaml(normalization.enabled=true, target_mean=146.7146)
        期待: 正常終了
    """
    import tempfile

    with tempfile.TemporaryDirectory() as tmpdir:
        output_dir = Path(tmpdir) / "output"
        result = main(
            [
                "--image", str(BASELINE_LUMINANCE_NPY),
                "--config", str(EVAL_BASELINE_LINEAR_YAML),
                "--output", str(output_dir),
            ]
        )
        assert result == 0


@pytest.mark.skipif(
    not BASELINE_LUMINANCE_NPY.exists(),
    reason="output/baseline/luminance.npy が存在しない場合はスキップ",
)
def test_baseline_linear_yaml_mean_close_to_target():
    """eval_baseline_linear.yaml 実行後 metrics.csv の mean が target_mean に一致.

    仕様(実装サマリー「動作確認済み(参考)」):
        target_mean=146.7146
        期待出力: mean=146.7146
        apply_mean_anchor の後 mean は target_mean に厳密一致する
    """
    import tempfile

    import pandas

    with tempfile.TemporaryDirectory() as tmpdir:
        output_dir = Path(tmpdir) / "output"
        main(
            [
                "--image", str(BASELINE_LUMINANCE_NPY),
                "--config", str(EVAL_BASELINE_LINEAR_YAML),
                "--output", str(output_dir),
            ]
        )
        df = pandas.read_csv(output_dir / "metrics.csv")
        mean_val = float(df["mean"].iloc[0])
        # apply_mean_anchor は厳密に mean = target_mean にする
        assert mean_val == pytest.approx(146.7146, rel=1e-5)


@pytest.mark.xfail(reason=_CALIBRATION_XFAIL_REASON, strict=False)
@pytest.mark.skipif(
    not BASELINE_LUMINANCE_NPY.exists(),
    reason="output/baseline/luminance.npy が存在しない場合はスキップ",
)
def test_baseline_linear_yaml_cov_close_to_expected():
    """eval_baseline_linear.yaml 実行後,metrics.csv の CoV が仕様値 0.182577 に近いこと.

    仕様(実装サマリー「動作確認済み(参考)」):
        期待出力: CoV=0.182577
        形状指標は linear ゲインに不変なので normalization 前後で同一
    """
    import tempfile

    import pandas

    with tempfile.TemporaryDirectory() as tmpdir:
        output_dir = Path(tmpdir) / "output"
        main(
            [
                "--image", str(BASELINE_LUMINANCE_NPY),
                "--config", str(EVAL_BASELINE_LINEAR_YAML),
                "--output", str(output_dir),
            ]
        )
        df = pandas.read_csv(output_dir / "metrics.csv")
        cov_val = float(df["cov"].iloc[0])
        # 仕様値 CoV=0.182577 に ±0.005 程度の許容範囲で一致すること
        assert cov_val == pytest.approx(0.182577, abs=0.005)


@pytest.mark.xfail(reason=_CALIBRATION_XFAIL_REASON, strict=False)
@pytest.mark.skipif(
    not BASELINE_LUMINANCE_NPY.exists(),
    reason="output/baseline/luminance.npy が存在しない場合はスキップ",
)
def test_baseline_linear_yaml_max_min_close_to_expected():
    """eval_baseline_linear.yaml 実行後,metrics.csv の max/min 比が仕様値 4.2759 に近いこと.

    仕様(実装サマリー「動作確認済み(参考)」):
        期待出力: max/min=4.2759
    """
    import tempfile

    import pandas

    with tempfile.TemporaryDirectory() as tmpdir:
        output_dir = Path(tmpdir) / "output"
        main(
            [
                "--image", str(BASELINE_LUMINANCE_NPY),
                "--config", str(EVAL_BASELINE_LINEAR_YAML),
                "--output", str(output_dir),
            ]
        )
        df = pandas.read_csv(output_dir / "metrics.csv")
        ratio_val = float(df["max_min_ratio"].iloc[0])
        # 仕様値 max/min=4.2759 に ±0.05 の許容範囲
        assert ratio_val == pytest.approx(4.2759, abs=0.05)


@pytest.mark.xfail(reason=_CALIBRATION_XFAIL_REASON, strict=False)
@pytest.mark.skipif(
    not BASELINE_LUMINANCE_NPY.exists(),
    reason="output/baseline/luminance.npy が存在しない場合はスキップ",
)
def test_baseline_linear_yaml_cp_ratio_close_to_expected():
    """eval_baseline_linear.yaml 実行後,metrics.csv の C/P 比が仕様値 1.3271 に近いこと.

    仕様(実装サマリー「動作確認済み(参考)」):
        期待出力: C/P=1.3271
    """
    import tempfile

    import pandas

    with tempfile.TemporaryDirectory() as tmpdir:
        output_dir = Path(tmpdir) / "output"
        main(
            [
                "--image", str(BASELINE_LUMINANCE_NPY),
                "--config", str(EVAL_BASELINE_LINEAR_YAML),
                "--output", str(output_dir),
            ]
        )
        df = pandas.read_csv(output_dir / "metrics.csv")
        cp_val = float(df["center_periphery_ratio"].iloc[0])
        # 仕様値 C/P=1.3271 に ±0.02 の許容範囲
        assert cp_val == pytest.approx(1.3271, abs=0.02)


@pytest.mark.xfail(reason=_CALIBRATION_XFAIL_REASON, strict=False)
@pytest.mark.skipif(
    not BASELINE_LUMINANCE_NPY.exists(),
    reason="output/baseline/luminance.npy が存在しない場合はスキップ",
)
def test_baseline_linear_yaml_gradient_close_to_expected():
    """eval_baseline_linear.yaml 実行後,metrics.csv の勾配が仕様値 24.65 に近いこと.

    仕様(実装サマリー「動作確認済み(参考)」):
        期待出力: 勾配=24.65
    """
    import tempfile

    import pandas

    with tempfile.TemporaryDirectory() as tmpdir:
        output_dir = Path(tmpdir) / "output"
        main(
            [
                "--image", str(BASELINE_LUMINANCE_NPY),
                "--config", str(EVAL_BASELINE_LINEAR_YAML),
                "--output", str(output_dir),
            ]
        )
        df = pandas.read_csv(output_dir / "metrics.csv")
        grad_val = float(df["gradient_magnitude"].iloc[0])
        # 仕様値 勾配=24.65 に ±0.5 の許容範囲
        assert grad_val == pytest.approx(24.65, abs=0.5)