Files
wehub-resource-sync 16031aae96
CPU tests Workflow / Testing (ubuntu-latest, 3.12) (push) Failing after 1s
CPU tests Workflow / Testing (ubuntu-latest, 3.13) (push) Failing after 0s
Mypy Type Check / Type Check (push) Failing after 0s
Docs/Test WorkFlow / Test docs build (push) Failing after 1s
PR Conflict Labeler / labeling (push) Failing after 1s
Dependency resolution / Resolve [tflite] extra — Python 3.12 (push) Failing after 0s
Smoke Tests / try-all-models (ubuntu-latest, 3.10) (push) Failing after 0s
Smoke Tests / try-all-models (ubuntu-latest, 3.13) (push) Failing after 1s
CPU tests Workflow / build-pkg (push) Failing after 1s
CPU tests Workflow / Testing (ubuntu-latest, 3.10) (push) Failing after 0s
CPU tests Workflow / Testing (ubuntu-latest, 3.11) (push) Failing after 0s
Smoke Tests / try-all-models (macos-latest, 3.10) (push) Has been cancelled
Smoke Tests / try-all-models (macos-latest, 3.13) (push) Has been cancelled
Smoke Tests / try-all-models (windows-latest, 3.10) (push) Has been cancelled
Smoke Tests / try-all-models (windows-latest, 3.13) (push) Has been cancelled
CPU tests Workflow / Testing (macos-latest, 3.10) (push) Has been cancelled
CPU tests Workflow / Testing (macos-latest, 3.13) (push) Has been cancelled
CPU tests Workflow / Testing (windows-latest, 3.10) (push) Has been cancelled
CPU tests Workflow / Testing (windows-latest, 3.13) (push) Has been cancelled
CPU tests Workflow / testing-guardian (push) Has been cancelled
GPU tests Workflow / Testing (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:26:24 +08:00

163 lines
7.1 KiB
Python

# ------------------------------------------------------------------------
# RF-DETR
# Copyright (c) 2025 Roboflow. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 [see LICENSE for details]
# ------------------------------------------------------------------------
"""Characterization tests for config-native builder functions.
These tests validate build_model_from_config() and build_criterion_from_config() which accept Pydantic config objects
directly instead of requiring a pre-built SimpleNamespace. If these functions cannot be imported, all tests skip via the
module-level pytestmark.
"""
import pytest
from rfdetr.config import (
RFDETRBaseConfig,
RFDETRSegNanoConfig,
SegmentationTrainConfig,
TrainConfig,
)
try:
from rfdetr.models import build_criterion_from_config, build_model_from_config
HAS_CONFIG_BUILDERS = True
except ImportError:
HAS_CONFIG_BUILDERS = False
pytestmark = pytest.mark.skipif(
not HAS_CONFIG_BUILDERS,
reason="config-native builder functions are not importable",
)
class TestBuildModelFromConfig:
"""Tests for build_model_from_config(model_config, defaults=MODEL_DEFAULTS)."""
def test_returns_lwdetr_for_base_config(self) -> None:
"""build_model_from_config with RFDETRBaseConfig returns an LWDETR instance."""
from rfdetr.models.lwdetr import LWDETR
mc = RFDETRBaseConfig(num_classes=80)
model = build_model_from_config(mc)
assert isinstance(model, LWDETR), f"Expected LWDETR instance, got {type(model).__name__}"
def test_num_classes_correct(self) -> None:
"""num_classes=5 in config should produce class_embed with out_features=6.
build_model adds +1 to num_classes (background class convention).
"""
mc = RFDETRBaseConfig(num_classes=5)
model = build_model_from_config(mc)
assert model.class_embed.out_features == 6, (
f"Expected class_embed.out_features=6 (num_classes+1), got {model.class_embed.out_features}"
)
def test_parity_with_build_model_via_namespace(self) -> None:
"""Parameter count must match between config-native and namespace paths."""
from rfdetr._namespace import _namespace_from_configs
from rfdetr.models.lwdetr import build_model
mc = RFDETRBaseConfig(num_classes=80)
tc = TrainConfig(dataset_dir="/tmp")
model_config_native = build_model_from_config(mc, tc)
ns = _namespace_from_configs(mc, tc)
model_namespace = build_model(ns)
params_native = sum(p.numel() for p in model_config_native.parameters())
params_namespace = sum(p.numel() for p in model_namespace.parameters())
assert params_native == params_namespace, (
f"Parameter count mismatch: config-native={params_native}, namespace={params_namespace}"
)
def test_segmentation_head_created_when_true(self) -> None:
"""RFDETRSegNanoConfig has segmentation_head=True; model must have it."""
mc = RFDETRSegNanoConfig()
model = build_model_from_config(mc)
assert model.segmentation_head is not None, "Expected segmentation_head to be created for RFDETRSegNanoConfig"
def test_drop_path_uses_train_config_value(self) -> None:
"""Non-default TrainConfig.drop_path must reach the model builder path."""
mc = RFDETRBaseConfig(num_classes=80)
tc = TrainConfig(dataset_dir="/tmp", drop_path=0.2)
model = build_model_from_config(mc, tc)
layers = model._get_backbone_encoder_layers()
assert layers is not None
assert hasattr(layers[-1], "drop_path")
assert layers[-1].drop_path.drop_prob == pytest.approx(0.2)
def test_rejects_encoder_only_defaults(self) -> None:
"""The config-native builder guarantees an LWDETR return value."""
from dataclasses import replace
from rfdetr.models import MODEL_DEFAULTS
mc = RFDETRBaseConfig(num_classes=80)
with pytest.raises(ValueError, match="encoder_only=False"):
build_model_from_config(mc, defaults=replace(MODEL_DEFAULTS, encoder_only=True))
def test_rejects_backbone_only_defaults(self) -> None:
"""backbone_only=True in defaults must also raise ValueError."""
from dataclasses import replace
from rfdetr.models import MODEL_DEFAULTS
mc = RFDETRBaseConfig(num_classes=80)
with pytest.raises(ValueError, match="backbone_only=False"):
build_model_from_config(mc, defaults=replace(MODEL_DEFAULTS, backbone_only=True))
def test_none_train_config_uses_dummy(self) -> None:
"""build_model_from_config with train_config=None must not raise."""
mc = RFDETRBaseConfig(num_classes=80)
model = build_model_from_config(mc, train_config=None)
assert model is not None, "Expected a model, got None"
class TestBuildCriterionFromConfig:
"""Tests for build_criterion_from_config(model_config, train_config, defaults)."""
def test_returns_tuple(self) -> None:
"""build_criterion_from_config must return a 2-tuple (SetCriterion, PostProcess)."""
from rfdetr.models.criterion import SetCriterion
from rfdetr.models.postprocess import PostProcess
mc = RFDETRBaseConfig(num_classes=80)
tc = TrainConfig(dataset_dir="/tmp")
result = build_criterion_from_config(mc, tc)
assert isinstance(result, tuple), f"Expected tuple, got {type(result).__name__}"
assert len(result) == 2, f"Expected 2-tuple, got {len(result)}-tuple"
criterion, postprocess = result
assert isinstance(criterion, SetCriterion), f"Expected SetCriterion, got {type(criterion).__name__}"
assert isinstance(postprocess, PostProcess), f"Expected PostProcess, got {type(postprocess).__name__}"
def test_num_select_postprocess(self) -> None:
"""RFDETRSegNanoConfig has num_select=100; PostProcess must reflect it."""
mc = RFDETRSegNanoConfig()
tc = SegmentationTrainConfig(dataset_dir="/tmp")
_, postprocess = build_criterion_from_config(mc, tc)
assert postprocess.num_select == 100, f"Expected PostProcess.num_select=100, got {postprocess.num_select}"
def test_segmentation_losses_included(self) -> None:
"""With segmentation config, 'masks' must be in criterion.losses."""
mc = RFDETRSegNanoConfig()
tc = SegmentationTrainConfig(dataset_dir="/tmp")
criterion, _ = build_criterion_from_config(mc, tc)
assert "masks" in criterion.losses, f"Expected 'masks' in criterion.losses, got {criterion.losses}"
def test_custom_defaults_focal_alpha_applied(self) -> None:
"""Custom focal_alpha in ModelDefaults must reach SetCriterion."""
from dataclasses import replace
from rfdetr.models import MODEL_DEFAULTS
mc = RFDETRBaseConfig(num_classes=80)
tc = TrainConfig(dataset_dir="/tmp")
custom_defaults = replace(MODEL_DEFAULTS, focal_alpha=0.5)
criterion, _ = build_criterion_from_config(mc, tc, defaults=custom_defaults)
assert criterion.focal_alpha == pytest.approx(0.5), f"Expected focal_alpha=0.5, got {criterion.focal_alpha}"