# pyright: reportArgumentType=none, reportCallIssue=none
"""scene/schema.py の MetricsConfig.fullimage_min_max / fullimage_sigma の仕様ベーステスト.
仕様出典:
- docs/04_SPEC/SPEC_02_シーン定義仕様.md(evaluation/metrics セクション.オプション指標は
bool フラグで有効化し,既定 False で既存 YAML はそのまま動く)
- docs/04_SPEC/SPEC_04_評価仕様.md「動径 min/max 比(オプション)」(既定 False・
metrics.csv に列を追加するオプション指標の扱い)
- evaluation/fullimage.py(新指標の定義.sigma はガウス平滑化の σ で正の値のみ意味を持つ)
検証する仕様:
- fullimage_min_max の既定は False(後方互換: フィールドを書いていない既存 YAML が読める)
- fullimage_sigma の既定は 12.0(464x450 の ROI で検証した値)
- fullimage_sigma は正の値のみ受理する(0・負は ValidationError)
- 既存フィールド(cov / std / max_min / radial_profile / radial_min_max / shape_metrics)に
干渉しない
テスト方針(.claude/rules/testing.md): 入力バリデーションは必ずテストする.
Pydantic モデルの直接構築と YAML ロードの両方で確認する.
"""
from __future__ import annotations
from pathlib import Path
import pytest
import yaml
from pydantic import ValidationError
from sm_tias_lightsim.scene.schema import EvaluationConfig, MetricsConfig
_REPO_ROOT = Path(__file__).resolve().parents[2]
_MINIMAL_ROI_YAML = """\
roi:
type: image_pixel
x: 0
y: 0
width: 100
height: 100
metrics:
cov: true
std: true
max_min: true
radial_profile: true
"""
def _eval_config_from_metrics_yaml(metrics_lines: str) -> EvaluationConfig:
"""metrics セクションに追記した YAML を EvaluationConfig にする."""
data = yaml.safe_load(_MINIMAL_ROI_YAML + metrics_lines)
return EvaluationConfig.model_validate(data)
# ---------------------------------------------------------------------------
# A. 既定値(後方互換)
# ---------------------------------------------------------------------------
def test_fullimage_min_max_default_is_false():
"""MetricsConfig() の fullimage_min_max 既定値が False であること.
仕様: オプション指標なので既定 False(既存挙動互換).
"""
assert MetricsConfig().fullimage_min_max is False
def test_fullimage_sigma_default_is_twelve():
"""MetricsConfig() の fullimage_sigma 既定値が 12.0 であること.
仕様: 既定 12.0 は 464x450 の ROI で検証した値(fullimage.py docstring).
"""
assert MetricsConfig().fullimage_sigma == pytest.approx(12.0)
def test_existing_yaml_without_fullimage_fields_is_loadable():
"""フィールドを持たない既存 YAML がそのまま読め,既定値になること(後方互換)."""
config = _eval_config_from_metrics_yaml("")
assert config.metrics is not None
assert config.metrics.fullimage_min_max is False
assert config.metrics.fullimage_sigma == pytest.approx(12.0)
@pytest.mark.parametrize(
"config_name",
["eval_baseline.yaml", "eval_baseline_linear.yaml", "eval_real.yaml"],
)
def test_shipped_existing_configs_keep_fullimage_disabled(config_name: str):
"""同梱の既存評価 YAML が無修正で読め,新指標が無効のままであること(後方互換)."""
data = yaml.safe_load((_REPO_ROOT / "configs" / config_name).read_text(encoding="utf-8"))
config = EvaluationConfig.model_validate(data)
assert config.metrics is not None
assert config.metrics.fullimage_min_max is False
def test_shipped_dng_target_shape_config_enables_fullimage():
"""configs/eval_baseline_dng_target_shape.yaml で新指標が有効になっていること.
仕様: 実機比較用の評価 config で metrics.fullimage_min_max: true を有効化する.
"""
path = _REPO_ROOT / "configs" / "eval_baseline_dng_target_shape.yaml"
config = EvaluationConfig.model_validate(yaml.safe_load(path.read_text(encoding="utf-8")))
assert config.metrics is not None
assert config.metrics.fullimage_min_max is True
assert config.metrics.fullimage_sigma == pytest.approx(12.0)
# ---------------------------------------------------------------------------
# B. 明示指定と YAML ロード
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("enabled", [True, False])
def test_fullimage_min_max_explicit_value_is_kept(enabled: bool):
"""MetricsConfig(fullimage_min_max=...) の明示指定がそのまま保持されること."""
assert MetricsConfig(fullimage_min_max=enabled).fullimage_min_max is enabled
def test_yaml_load_fullimage_min_max_true():
"""YAML の metrics.fullimage_min_max: true が反映されること."""
config = _eval_config_from_metrics_yaml(" fullimage_min_max: true\n")
assert config.metrics is not None
assert config.metrics.fullimage_min_max is True
def test_yaml_load_fullimage_min_max_false():
"""YAML の metrics.fullimage_min_max: false が反映されること."""
config = _eval_config_from_metrics_yaml(" fullimage_min_max: false\n")
assert config.metrics is not None
assert config.metrics.fullimage_min_max is False
@pytest.mark.parametrize("sigma", [0.5, 8.0, 12.0, 30.0])
def test_yaml_load_fullimage_sigma_positive_values(sigma: float):
"""正の fullimage_sigma が YAML から読めること."""
config = _eval_config_from_metrics_yaml(
f" fullimage_min_max: true\n fullimage_sigma: {sigma}\n"
)
assert config.metrics is not None
assert config.metrics.fullimage_sigma == pytest.approx(sigma)
def test_fullimage_sigma_accepts_integer_yaml_value():
"""YAML の整数値 sigma が float として読めること."""
config = _eval_config_from_metrics_yaml(" fullimage_sigma: 8\n")
assert config.metrics is not None
assert config.metrics.fullimage_sigma == pytest.approx(8.0)
# ---------------------------------------------------------------------------
# C. バリデーション(不正値は ValidationError)
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("sigma", [0.0, -1.0, -12.0])
def test_non_positive_fullimage_sigma_raises_validation_error(sigma: float):
"""fullimage_sigma が 0 以下なら ValidationError になること.
仕様: sigma はガウス平滑化の σ なので正の値のみ(fullimage.py も 0 以下を拒否する).
"""
with pytest.raises(ValidationError):
MetricsConfig(fullimage_sigma=sigma)
@pytest.mark.parametrize("sigma", ["0", "-3.0"])
def test_non_positive_fullimage_sigma_in_yaml_raises_validation_error(sigma: str):
"""YAML 経由でも 0 以下の fullimage_sigma が ValidationError になること."""
with pytest.raises(ValidationError):
_eval_config_from_metrics_yaml(f" fullimage_sigma: {sigma}\n")
def test_non_numeric_fullimage_sigma_raises_validation_error():
"""数値でない fullimage_sigma は ValidationError になること."""
with pytest.raises(ValidationError):
MetricsConfig(fullimage_sigma="wide")
def test_non_boolean_fullimage_min_max_raises_validation_error():
"""bool に解釈できない fullimage_min_max は ValidationError になること."""
with pytest.raises(ValidationError):
MetricsConfig(fullimage_min_max="maybe")
# ---------------------------------------------------------------------------
# D. 既存フィールドへの非干渉
# ---------------------------------------------------------------------------
def test_enabling_fullimage_does_not_change_other_metric_flags():
"""新フィールドを有効にしても既存フラグの既定値が変わらないこと."""
default = MetricsConfig()
enabled = MetricsConfig(fullimage_min_max=True, fullimage_sigma=20.0)
assert enabled.cov == default.cov
assert enabled.std == default.std
assert enabled.max_min == default.max_min
assert enabled.radial_profile == default.radial_profile
assert enabled.radial_min_max == default.radial_min_max
assert enabled.shape_metrics == default.shape_metrics
def test_metrics_config_is_frozen():
"""MetricsConfig が immutable であること(読み込み後に書き換えない)."""
config = MetricsConfig(fullimage_min_max=True)
with pytest.raises(ValidationError):
config.fullimage_min_max = False