Newer
Older
TIASJudgeModel / tests / test_training_config.py
"""学習の設定(`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