# pyright: reportArgumentType=none, reportCallIssue=none
# Pydantic v2 の ValidationError は pydantic_core 由来で,Pylance/Pyright が
# BaseException のサブクラスと推論しないため pytest.raises(ValidationError) を
# 誤検知する.また「必須フィールドを意図的に欠落させる」テスト(line 78 等)でも
# reportCallIssue が誤発火するため,ファイル単位で抑止する(builder.py と同じ方針).
"""scene/schema.py の NormalizationConfig / EvaluationConfig.normalization 仕様ベーステスト.
仕様出典:
- 実装サマリー(NormalizationConfig クラス,EvaluationConfig.normalization フィールド,
OETF + normalization 同時有効化禁止 model_validator)
- docs/04_SPEC/SPEC_04_評価仕様.md(評価パイプライン)
- .claude/rules/testing.md(テスト方針)
- src/sm_tias_lightsim/scene/schema.py(NormalizationConfig, EvaluationConfig 定義)
テスト方針:
- Pydantic モデルを直接構築して振る舞いを検証する(YAML パースは不要)
- ファイル I/O はモックしない (testing.md)
- 後方互換性(normalization キーなし = 正規化無効)を重点的に検証する
"""
from __future__ import annotations
import pytest
from pydantic import ValidationError
from sm_tias_lightsim.scene.schema import EvaluationConfig, NormalizationConfig
# ---------------------------------------------------------------------------
# 共通ヘルパー
# ---------------------------------------------------------------------------
def _make_minimal_eval_data(**overrides: object) -> dict:
"""最小構成の EvaluationConfig データを返す."""
data: dict = {
"roi": {"type": "image_pixel", "x": 0, "y": 0, "width": 100, "height": 100},
}
data.update(overrides)
return data
# ---------------------------------------------------------------------------
# 1. NormalizationConfig — フィールド検証
# ---------------------------------------------------------------------------
def test_normalization_config_valid_mean_anchor():
"""type=mean_anchor + target_mean で正常にパースされること.
仕様: NormalizationConfig: type=mean_anchor(Literal),target_mean が必須(schema.py)
"""
cfg = NormalizationConfig(enabled=True, type="mean_anchor", target_mean=146.7146)
assert cfg.enabled is True
assert cfg.type == "mean_anchor"
assert cfg.target_mean == pytest.approx(146.7146)
def test_normalization_config_defaults():
"""NormalizationConfig のデフォルト値を確認する.
仕様:
- enabled: bool = False(省略時は正規化無効)
- type: Literal["mean_anchor"] = "mean_anchor"(デフォルト)
(schema.py NormalizationConfig)
"""
cfg = NormalizationConfig(target_mean=100.0)
assert cfg.enabled is False
assert cfg.type == "mean_anchor"
def test_normalization_config_missing_target_mean_raises():
"""target_mean を省略したとき ValidationError が発生すること.
仕様: target_mean: float = Field(..., ...) — 必須フィールド(schema.py)
"""
with pytest.raises(ValidationError):
NormalizationConfig(enabled=True)
def test_normalization_config_invalid_type_raises():
"""type に未対応の値(例: max_anchor)で ValidationError が発生すること.
仕様: type: Literal["mean_anchor"] — mean_anchor のみ(schema.py)
"""
with pytest.raises(ValidationError):
NormalizationConfig(type="max_anchor", target_mean=100.0) # type: ignore[call-arg]
def test_normalization_config_is_frozen():
"""NormalizationConfig が frozen(イミュータブル)であること.
仕様: _FrozenBase → frozen=True(schema.py ConfigDict)
"""
cfg = NormalizationConfig(target_mean=100.0)
with pytest.raises(ValidationError):
cfg.enabled = True # type: ignore[misc]
def test_normalization_config_target_mean_various_values():
"""target_mean に任意の float 値が受け付けられること.
仕様: target_mean: float — 任意 float 可(schema.py NormalizationConfig)
"""
# uint8 スケールの実測 mean 相当
cfg1 = NormalizationConfig(target_mean=146.7146)
assert cfg1.target_mean == pytest.approx(146.7146)
# 0 以外の任意 float
cfg2 = NormalizationConfig(target_mean=255.0)
assert cfg2.target_mean == pytest.approx(255.0)
cfg3 = NormalizationConfig(target_mean=1.0)
assert cfg3.target_mean == pytest.approx(1.0)
# ---------------------------------------------------------------------------
# 2. EvaluationConfig.normalization — 後方互換性
# ---------------------------------------------------------------------------
def test_evaluation_config_normalization_none_when_omitted():
"""normalization キーを省略したとき EvaluationConfig.normalization が None になること.
仕様: normalization: NormalizationConfig | None = Field(default=None, ...)
省略時は正規化無効(schema.py EvaluationConfig)
"""
data = _make_minimal_eval_data()
cfg = EvaluationConfig.model_validate(data)
assert cfg.normalization is None
def test_evaluation_config_normalization_none_means_disabled():
"""normalization=None のとき正規化が適用されないこと(enabled=False 相当を確認).
仕様: 省略時は正規化無効扱い(schema.py / cli/evaluate.py)
"""
data = _make_minimal_eval_data(normalization=None)
cfg = EvaluationConfig.model_validate(data)
assert cfg.normalization is None
# cli/evaluate.py では `if norm_cfg is not None and norm_cfg.enabled:` でスキップ
norm_cfg = cfg.normalization
is_applied = norm_cfg is not None and norm_cfg.enabled
assert is_applied is False
def test_evaluation_config_normalization_disabled_explicit():
"""normalization.enabled=False を明示したとき正規化が無効であること.
仕様: enabled: bool = False(省略時デフォルト)(schema.py)
"""
data = _make_minimal_eval_data(
normalization={"enabled": False, "target_mean": 100.0}
)
cfg = EvaluationConfig.model_validate(data)
assert cfg.normalization is not None
assert cfg.normalization.enabled is False
is_applied = cfg.normalization is not None and cfg.normalization.enabled
assert is_applied is False
def test_evaluation_config_normalization_enabled():
"""normalization.enabled=True で正常にパースされること.
仕様: eval_baseline_linear.yaml の normalization セクション相当
"""
data = _make_minimal_eval_data(
normalization={
"enabled": True,
"type": "mean_anchor",
"target_mean": 146.7146,
}
)
cfg = EvaluationConfig.model_validate(data)
assert cfg.normalization is not None
assert cfg.normalization.enabled is True
assert cfg.normalization.type == "mean_anchor"
assert cfg.normalization.target_mean == pytest.approx(146.7146)
def test_evaluation_config_backward_compat_real_yaml():
"""既存の eval_real.yaml 相当(normalization キーなし)が後方互換で動くこと.
仕様: normalization フィールドを省略した既存 YAML も後方互換(正規化無効扱い)で動作する
(schema.py EvaluationConfig docstring)
"""
data = {
"roi": {"type": "image_pixel", "x": 24, "y": 37, "width": 464, "height": 450},
"zones": {"enabled": True, "boundaries": [0.33, 0.66]},
"metrics": {"cov": True, "std": True, "max_min": True, "radial_profile": True},
}
cfg = EvaluationConfig.model_validate(data)
assert cfg.normalization is None
assert cfg.oetf is None
def test_evaluation_config_normalization_invalid_type_raises():
"""normalization.type に未対応の値を指定したとき ValidationError が発生すること.
仕様: type: Literal["mean_anchor"](schema.py NormalizationConfig)
"""
data = _make_minimal_eval_data(
normalization={"enabled": True, "type": "max_anchor", "target_mean": 100.0}
)
with pytest.raises(ValidationError):
EvaluationConfig.model_validate(data)
def test_evaluation_config_normalization_missing_target_mean_raises():
"""normalization.target_mean を省略したとき ValidationError が発生すること.
仕様: target_mean: float = Field(...) — 必須(schema.py NormalizationConfig)
"""
data = _make_minimal_eval_data(
normalization={"enabled": True, "type": "mean_anchor"}
)
with pytest.raises(ValidationError):
EvaluationConfig.model_validate(data)
# ---------------------------------------------------------------------------
# 3. OETF + normalization 同時有効化の排他制御
# ---------------------------------------------------------------------------
def test_oetf_and_normalization_both_enabled_raises():
"""oetf.enabled=True かつ normalization.enabled=True のとき ValidationError が発生すること.
仕様(実装サマリー「スキーマ排他性」,schema.py EvaluationConfig model_validator):
oetf.enabled=True かつ normalization.enabled=True は ValidationError
(OETF が内部で露出アンカーを持つため,二重アンカーになるのを防ぐ)
"""
data = _make_minimal_eval_data(
oetf={
"enabled": True,
"transfer": "srgb",
"exposure": {"mode": "anchor_mean", "target_mean": 146.7146},
},
normalization={
"enabled": True,
"type": "mean_anchor",
"target_mean": 146.7146,
},
)
with pytest.raises(ValidationError):
EvaluationConfig.model_validate(data)
def test_oetf_enabled_normalization_disabled_is_valid():
"""oetf.enabled=True かつ normalization.enabled=False は有効であること.
仕様: 片方だけ有効化は OK(スキーマ排他性は両方 True のみ禁止)
"""
data = _make_minimal_eval_data(
oetf={
"enabled": True,
"transfer": "srgb",
"exposure": {"mode": "anchor_mean", "target_mean": 146.7146},
},
normalization={
"enabled": False,
"type": "mean_anchor",
"target_mean": 146.7146,
},
)
cfg = EvaluationConfig.model_validate(data)
assert cfg.oetf is not None
assert cfg.oetf.enabled is True
assert cfg.normalization is not None
assert cfg.normalization.enabled is False
def test_oetf_disabled_normalization_enabled_is_valid():
"""oetf.enabled=False かつ normalization.enabled=True は有効であること.
仕様: 片方だけ有効化は OK
"""
data = _make_minimal_eval_data(
oetf={"enabled": False},
normalization={
"enabled": True,
"type": "mean_anchor",
"target_mean": 146.7146,
},
)
cfg = EvaluationConfig.model_validate(data)
assert cfg.oetf is not None
assert cfg.oetf.enabled is False
assert cfg.normalization is not None
assert cfg.normalization.enabled is True
def test_both_disabled_is_valid():
"""oetf.enabled=False かつ normalization.enabled=False は有効であること(無変換).
仕様: 両方 None または両方 disabled は OK(無変換)
"""
data = _make_minimal_eval_data(
oetf={"enabled": False},
normalization={"enabled": False, "target_mean": 100.0},
)
cfg = EvaluationConfig.model_validate(data)
assert cfg.oetf is not None
assert cfg.oetf.enabled is False
assert cfg.normalization is not None
assert cfg.normalization.enabled is False
def test_both_none_is_valid():
"""oetf=None かつ normalization=None は有効であること(既存 YAML 後方互換).
仕様: 両方 None は OK
"""
data = _make_minimal_eval_data()
cfg = EvaluationConfig.model_validate(data)
assert cfg.oetf is None
assert cfg.normalization is None
def test_oetf_none_normalization_enabled_is_valid():
"""oetf=None(省略)かつ normalization.enabled=True は有効であること.
仕様: 片方だけ有効化は OK(None は disabled 扱い)
"""
data = _make_minimal_eval_data(
normalization={
"enabled": True,
"type": "mean_anchor",
"target_mean": 100.0,
}
)
cfg = EvaluationConfig.model_validate(data)
assert cfg.oetf is None
assert cfg.normalization is not None
assert cfg.normalization.enabled is True
def test_oetf_enabled_normalization_none_is_valid():
"""oetf.enabled=True かつ normalization=None(省略)は有効であること.
仕様: 片方だけ有効化は OK
"""
data = _make_minimal_eval_data(
oetf={
"enabled": True,
"transfer": "srgb",
"exposure": {"mode": "anchor_mean", "target_mean": 146.7146},
}
)
cfg = EvaluationConfig.model_validate(data)
assert cfg.oetf is not None
assert cfg.oetf.enabled is True
assert cfg.normalization is None
# ---------------------------------------------------------------------------
# 4. eval_baseline_linear.yaml 相当の YAML ロード検証
# ---------------------------------------------------------------------------
def test_eval_baseline_linear_yaml_equivalent_parses_correctly():
"""eval_baseline_linear.yaml 相当のデータが正しくパースされること.
仕様: configs/eval_baseline_linear.yaml の normalization セクション相当
"""
data = {
"roi": {"type": "image_pixel", "x": 24, "y": 37, "width": 464, "height": 450},
"zones": {"enabled": True, "boundaries": [0.33, 0.66]},
"metrics": {"cov": True, "std": True, "max_min": True, "radial_profile": True},
"normalization": {
"enabled": True,
"type": "mean_anchor",
"target_mean": 146.7146,
},
}
cfg = EvaluationConfig.model_validate(data)
assert cfg.normalization is not None
assert cfg.normalization.enabled is True
assert cfg.normalization.type == "mean_anchor"
assert cfg.normalization.target_mean == pytest.approx(146.7146)
assert cfg.oetf is None
assert cfg.roi.x == 24
assert cfg.roi.y == 37
assert cfg.roi.width == 464
assert cfg.roi.height == 450