"""学習の設定(`model`・`train` 節)のテスト(SPEC_02「学習の設定」・SPEC_01「設定ファイル」).
- 既定値が SPEC_02 の表どおり
- `model`・`train` を省略した既存の設定(`tias-dataset` 用)も読める
- 未知のキー・不正な値は拒否する
- リポジトリの `configs/cv_*.yaml` がすべて読め,モデル比較用の設定はモデル名以外が元の設定と同一
"""
from __future__ import annotations
from pathlib import Path
import pytest
from pydantic import ValidationError
from tias_judge_model.schema import ExperimentConfig, load_config
CONFIGS_DIR = Path(__file__).resolve().parents[1] / "configs"
CV_CONFIGS = sorted(CONFIGS_DIR.glob("cv_*.yaml"))
NO_ENV: dict[str, str] = {}
def _minimal() -> dict:
return {"data": {"root": "somewhere"}}
def test_model_defaults_follow_spec() -> None:
c = ExperimentConfig.model_validate(_minimal())
assert c.model.name == "convnext_tiny"
assert c.model.pretrained is True
def test_train_defaults_follow_spec() -> None:
t = ExperimentConfig.model_validate(_minimal()).train
assert t.image_size == 224
assert t.batch_size == 32
assert t.max_epochs == 40
assert t.patience == 8
assert t.lr == pytest.approx(1e-4)
assert t.weight_decay == pytest.approx(0.05)
assert t.warmup_epochs == 2
assert t.inner_val_ratio == pytest.approx(0.15)
assert t.num_workers == 4
assert t.amp is True
@pytest.mark.parametrize("name", ["synthetic.yaml", "newton.yaml"])
def test_existing_configs_without_model_train_still_load(name: str) -> None:
c = load_config(CONFIGS_DIR / name, environ=NO_ENV)
assert c.model.name == "convnext_tiny"
assert c.train.image_size == 224
@pytest.mark.parametrize(
"raw",
[
{"model": {"nme": "resnet18"}},
{"train": {"epochs": 3}},
{"train": {"batch_size": 0}},
{"train": {"inner_val_ratio": 0.0}},
{"train": {"inner_val_ratio": 1.0}},
{"train": {"lr": 0}},
{"model": {"name": ""}},
],
)
def test_invalid_training_settings_are_rejected(raw: dict) -> None:
with pytest.raises(ValidationError):
ExperimentConfig.model_validate({**_minimal(), **raw})
def test_cv_configs_exist() -> None:
names = {p.name for p in CV_CONFIGS}
assert {"cv_with_mild.yaml", "cv_without_mild.yaml", "cv_synthetic.yaml"} <= names
@pytest.mark.parametrize("path", CV_CONFIGS, ids=lambda p: p.name)
def test_every_cv_config_loads(path: Path) -> None:
load_config(path, environ=NO_ENV)
def test_with_and_without_mild_differ_only_in_severities() -> None:
w = load_config(CONFIGS_DIR / "cv_with_mild.yaml", environ=NO_ENV)
wo = load_config(CONFIGS_DIR / "cv_without_mild.yaml", environ=NO_ENV)
assert w.data.severities == ("strong", "mild", "normal")
assert wo.data.severities == ("strong", "normal")
dw = w.model_dump()
dwo = wo.model_dump()
dw["data"].pop("severities")
dwo["data"].pop("severities")
assert dw == dwo
def test_production_cv_configs_use_spec_defaults() -> None:
c = load_config(CONFIGS_DIR / "cv_with_mild.yaml", environ=NO_ENV)
assert c.model == ExperimentConfig.model_validate(_minimal()).model
assert c.train == ExperimentConfig.model_validate(_minimal()).train
assert c.split.n_folds == 5
def test_synthetic_cv_config_does_not_download_weights() -> None:
c = load_config(CONFIGS_DIR / "cv_synthetic.yaml", environ=NO_ENV)
assert c.model.pretrained is False
def _model_comparison_pairs() -> list[tuple[Path, Path]]:
pairs = []
for p in CV_CONFIGS:
if p.name in {"cv_with_mild.yaml", "cv_without_mild.yaml", "cv_synthetic.yaml"}:
continue
suffix = "with_mild" if p.stem.endswith("_with_mild") else "without_mild"
pairs.append((p, CONFIGS_DIR / f"cv_{suffix}.yaml"))
return pairs
@pytest.mark.parametrize(("variant", "base"), _model_comparison_pairs(), ids=lambda p: Path(p).name)
def test_model_comparison_config_differs_only_in_model_name(variant: Path, base: Path) -> None:
v = load_config(variant, environ=NO_ENV).model_dump()
b = load_config(base, environ=NO_ENV).model_dump()
assert v["model"]["name"] != b["model"]["name"]
v["model"].pop("name")
b["model"].pop("name")
assert v == b
def test_model_comparison_configs_are_present() -> None:
assert len(_model_comparison_pairs()) >= 2