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
163 lines
7.1 KiB
Python
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}"
|