# pyright: reportArgumentType=none, reportCallIssue=none
"""scene/schema.py の MetricsConfig shape_metrics / shape_reference フィールド仕様ベーステスト.
仕様出典:
- docs/05_TECH/TECH_11_形状指標の見直しと新指標導入.md
- docs/04_SPEC/SPEC_02_シーン定義仕様.md(evaluation/metrics セクション)
- src/sm_tias_lightsim/scene/schema.py(MetricsConfig 定義)
テスト方針:
- Pydantic モデルを直接構築して振る舞いを検証する(YAML パースは別途)
- 既存フィールド(cov/std/max_min/radial_profile/radial_min_max)への非干渉を確認する
- 後方互換性(既存 YAML が新スキーマで読める)を確認する
"""
from __future__ import annotations
import textwrap
from pathlib import Path
import pytest
import yaml
from pydantic import ValidationError
from sm_tias_lightsim.scene.schema import EvaluationConfig, MetricsConfig
# ===========================================================================
# F1. 後方互換: 既存 YAML(shape_metrics 未指定)が新スキーマで読める
# ===========================================================================
def test_existing_yaml_without_shape_metrics_is_valid(tmp_path: Path):
"""F1: shape_metrics を含まない既存 eval_baseline_dng_target.yaml 相当の YAML が
新スキーマで ValidationError なしに読み込めること.
仕様: shape_metrics および shape_reference フィールドはいずれも省略可能
(TECH_11 §設定方法 後方互換)
"""
yaml_content = textwrap.dedent("""\
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
radial_min_max: true
normalization:
enabled: true
type: mean_anchor
target_mean: 207.82
""")
yaml_path = tmp_path / "eval_no_shape.yaml"
yaml_path.write_text(yaml_content, encoding="utf-8")
data = yaml.safe_load(yaml_path.read_text(encoding="utf-8"))
config = EvaluationConfig.model_validate(data)
# shape_metrics と shape_reference はデフォルト値(False / None)であること
assert config.metrics is not None
assert config.metrics.shape_metrics is False
assert config.metrics.shape_reference is None
def test_metrics_config_without_shape_fields_defaults_to_false_and_none():
"""F1 補足: MetricsConfig を shape_metrics/shape_reference 未指定で構築すると
shape_metrics=False, shape_reference=None になること(Pydantic デフォルト値).
仕様: shape_metrics: bool = Field(default=False, ...)
shape_reference: str | None = Field(default=None, ...)
(schema.py MetricsConfig)
"""
cfg = MetricsConfig()
assert cfg.shape_metrics is False
assert cfg.shape_reference is None
def test_existing_config_files_are_backward_compatible():
"""F1 補足: プロジェクト既存の eval_baseline_dng_target.yaml が
ValidationError なしに読み込めること(実ファイルでの後方互換確認).
仕様: 既存の YAML は変更不要で動作する (TECH_11 §後方互換)
"""
import os
project_root = Path(os.path.dirname(__file__)).parent.parent
config_path = project_root / "configs" / "eval_baseline_dng_target.yaml"
if not config_path.exists():
pytest.skip("configs/eval_baseline_dng_target.yaml が存在しないためスキップ")
with config_path.open(encoding="utf-8") as f:
data = yaml.safe_load(f)
# EvaluationConfig 直接または SceneConfig.evaluation サブツリーとして読む
eval_data = data.get("evaluation", data)
config = EvaluationConfig.model_validate(eval_data)
# shape_metrics フィールドのデフォルトが有効であること
assert config.metrics is not None
assert config.metrics.shape_metrics is False
# ===========================================================================
# F2. shape_metrics=true + shape_reference=null が valid
# ===========================================================================
def test_shape_metrics_true_with_null_reference_is_valid():
"""F2: shape_metrics=True + shape_reference=None が valid であること.
仕様: shape_metrics=True + shape_reference=null が valid
(TECH_11 §設定方法 基本)
"""
cfg = MetricsConfig(shape_metrics=True, shape_reference=None)
assert cfg.shape_metrics is True
assert cfg.shape_reference is None
def test_shape_metrics_true_null_reference_via_yaml(tmp_path: Path):
"""F2 補足: YAML で shape_metrics=true, shape_reference=null と指定した場合も valid.
仕様: shape_metrics: true, shape_reference: null(YAML 表現)
(configs/eval_baseline_dng_target_shape.yaml 相当)
"""
yaml_content = textwrap.dedent("""\
roi:
type: image_pixel
x: 0
y: 0
width: 100
height: 100
metrics:
cov: true
shape_metrics: true
shape_reference: null
""")
data = yaml.safe_load(yaml_content)
config = EvaluationConfig.model_validate(data)
assert config.metrics is not None
assert config.metrics.shape_metrics is True
assert config.metrics.shape_reference is None
def test_shape_metrics_false_with_null_reference_is_valid():
"""F2 補足: shape_metrics=False + shape_reference=None(デフォルト)も valid.
仕様: 省略時(null)は SSIM / NCC の列は空欄になる
(schema.py MetricsConfig.shape_reference description)
"""
cfg = MetricsConfig(shape_metrics=False, shape_reference=None)
assert cfg.shape_metrics is False
assert cfg.shape_reference is None
# ===========================================================================
# F3. shape_metrics=true + shape_reference="path/to/file.npy" が valid
# ===========================================================================
def test_shape_metrics_true_with_string_reference_is_valid():
"""F3: shape_metrics=True + shape_reference="path/to/file.npy" が valid であること.
仕様: shape_metrics=True + shape_reference="path/to/file.npy" が valid
(TECH_11 §設定方法 SSIM/NCC 有効化)
"""
cfg = MetricsConfig(shape_metrics=True, shape_reference="path/to/reference.npy")
assert cfg.shape_metrics is True
assert cfg.shape_reference == "path/to/reference.npy"
def test_shape_metrics_true_with_reference_via_yaml(tmp_path: Path):
"""F3 補足: YAML で shape_reference をパス文字列で指定した場合も valid.
仕様: shape_reference: reference_measurement/quantitative/expected_results/...npy
(TECH_11 §設定方法 SSIM/NCC 有効化)
"""
yaml_content = textwrap.dedent("""\
roi:
type: image_pixel
x: 0
y: 0
width: 100
height: 100
metrics:
shape_metrics: true
shape_reference: "reference_measurement/quantitative/expected_results/sample.npy"
""")
data = yaml.safe_load(yaml_content)
config = EvaluationConfig.model_validate(data)
assert config.metrics is not None
assert config.metrics.shape_metrics is True
assert config.metrics.shape_reference == "reference_measurement/quantitative/expected_results/sample.npy"
def test_shape_reference_accepts_png_path():
"""F3 補足: shape_reference に .png パスも指定できること.
仕様: shape_reference の .npy または .png パスを受け付ける
(cli/evaluate.py _load_reference_luminance)
"""
cfg = MetricsConfig(shape_metrics=True, shape_reference="path/to/reference.png")
assert cfg.shape_reference == "path/to/reference.png"
# ===========================================================================
# 追加: 既存フィールドへの非干渉確認
# ===========================================================================
def test_existing_fields_unaffected_when_shape_metrics_added():
"""shape_metrics フィールド追加後も既存フィールドのデフォルト値が変わらないこと.
仕様: 後方互換(既存の YAML は変更不要)
(TECH_11 §設定方法 後方互換)
"""
cfg = MetricsConfig()
# 既存フィールドのデフォルト値が維持されている
assert cfg.cov is True
assert cfg.std is True
assert cfg.max_min is True
assert cfg.radial_profile is True
assert cfg.radial_min_max is False
# 新フィールドのデフォルト値
assert cfg.shape_metrics is False
assert cfg.shape_reference is None
def test_all_flags_can_be_enabled_simultaneously():
"""全フラグを True に指定できること(組み合わせ互換性の確認).
仕様: 各フラグは独立してオン/オフ可能
"""
cfg = MetricsConfig(
cov=True,
std=True,
max_min=True,
radial_profile=True,
radial_min_max=True,
shape_metrics=True,
shape_reference="path/to/ref.npy",
)
assert cfg.shape_metrics is True
assert cfg.shape_reference == "path/to/ref.npy"
assert cfg.radial_min_max is True
def test_shape_metrics_config_file_is_valid():
"""プロジェクト既存の eval_baseline_dng_target_shape.yaml が
ValidationError なしに読み込めること.
仕様: configs/eval_baseline_dng_target_shape.yaml が valid な設定ファイルとして
機能すること (TECH_11 §設定方法)
"""
import os
project_root = Path(os.path.dirname(__file__)).parent.parent
config_path = project_root / "configs" / "eval_baseline_dng_target_shape.yaml"
if not config_path.exists():
pytest.skip("configs/eval_baseline_dng_target_shape.yaml が存在しないためスキップ")
with config_path.open(encoding="utf-8") as f:
data = yaml.safe_load(f)
eval_data = data.get("evaluation", data)
config = EvaluationConfig.model_validate(eval_data)
assert config.metrics is not None
assert config.metrics.shape_metrics is True