|
|
|
|
|
|
|
|
|
|
|
|
| """Characterization tests for build_model() and build_criterion_and_postprocessors().
|
|
|
| These tests pin the current behavior of the legacy namespace-based builder
|
| functions. They serve as a safety net during the config-native builder
|
| refactoring: any change that alters these outputs is a regression.
|
|
|
| All tests in this file must pass against the CURRENT codebase.
|
| """
|
|
|
| import pytest
|
| import torch
|
|
|
| from rfdetr._namespace import _namespace_from_configs
|
| from rfdetr.config import (
|
| RFDETRBaseConfig,
|
| RFDETRNanoConfig,
|
| RFDETRSegNanoConfig,
|
| SegmentationTrainConfig,
|
| TrainConfig,
|
| )
|
| from rfdetr.models.criterion import SetCriterion
|
| from rfdetr.models.lwdetr import LWDETR, build_criterion_and_postprocessors, build_model
|
| from rfdetr.models.postprocess import PostProcess
|
|
|
|
|
|
|
|
|
|
|
|
|
| def _make_ns(mc=None, tc=None):
|
| """Build a namespace suitable for builder functions."""
|
| mc = mc or RFDETRBaseConfig(num_classes=80, pretrain_weights=None, device="cpu")
|
| tc = tc or TrainConfig(dataset_dir="/tmp")
|
| return _namespace_from_configs(mc, tc)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| class TestBuildModelCharacterization:
|
| """Pin current build_model() behaviour for the standard code path."""
|
|
|
| def test_returns_lwdetr_instance(self) -> None:
|
| ns = _make_ns()
|
| model = build_model(ns)
|
| assert isinstance(model, LWDETR)
|
|
|
| def test_num_classes_plus_one(self) -> None:
|
| """build_model applies the +1 background class convention."""
|
| mc = RFDETRBaseConfig(num_classes=5, pretrain_weights=None, device="cpu")
|
| ns = _make_ns(mc=mc)
|
| model = build_model(ns)
|
| assert model.class_embed.out_features == 6
|
|
|
| def test_num_queries_forwarded(self) -> None:
|
| mc = RFDETRBaseConfig(num_classes=80, pretrain_weights=None, device="cpu")
|
| ns = _make_ns(mc=mc)
|
| model = build_model(ns)
|
| assert model.num_queries == mc.num_queries
|
|
|
| @pytest.mark.parametrize(
|
| "config_class, expected_queries",
|
| [
|
| pytest.param(RFDETRBaseConfig, 300, id="base"),
|
| pytest.param(RFDETRNanoConfig, 300, id="nano"),
|
| pytest.param(RFDETRSegNanoConfig, 100, id="seg_nano"),
|
| ],
|
| )
|
| def test_num_queries_per_config_variant(self, config_class, expected_queries) -> None:
|
| mc = config_class(pretrain_weights=None, device="cpu")
|
| ns = _make_ns(mc=mc)
|
| model = build_model(ns)
|
| assert model.num_queries == expected_queries
|
|
|
| def test_segmentation_head_none_for_detection(self) -> None:
|
| ns = _make_ns()
|
| model = build_model(ns)
|
| assert model.segmentation_head is None
|
|
|
| def test_segmentation_head_present_for_seg_config(self) -> None:
|
| mc = RFDETRSegNanoConfig(pretrain_weights=None, device="cpu")
|
| ns = _make_ns(mc=mc)
|
| model = build_model(ns)
|
| assert model.segmentation_head is not None
|
|
|
| def test_aux_loss_enabled_by_default(self) -> None:
|
| ns = _make_ns()
|
| model = build_model(ns)
|
| assert model.aux_loss is True
|
|
|
| def test_group_detr_forwarded(self) -> None:
|
| mc = RFDETRBaseConfig(num_classes=80, pretrain_weights=None, device="cpu")
|
| ns = _make_ns(mc=mc)
|
| model = build_model(ns)
|
| assert model.group_detr == mc.group_detr
|
|
|
| def test_num_feature_levels_set_on_args(self) -> None:
|
| """build_model mutates args.num_feature_levels = len(projector_scale)."""
|
| mc = RFDETRBaseConfig(num_classes=80, pretrain_weights=None, device="cpu")
|
| ns = _make_ns(mc=mc)
|
| build_model(ns)
|
| assert ns.num_feature_levels == len(mc.projector_scale)
|
|
|
| @pytest.mark.parametrize(
|
| "config_class, expected_param_count_range",
|
| [
|
| pytest.param(RFDETRBaseConfig, (25_000_000, 40_000_000), id="base"),
|
| pytest.param(RFDETRNanoConfig, (25_000_000, 40_000_000), id="nano"),
|
| ],
|
| )
|
| def test_param_count_in_expected_range(self, config_class, expected_param_count_range) -> None:
|
| """Sanity check that the model has a plausible number of parameters."""
|
| mc = config_class(num_classes=80, pretrain_weights=None, device="cpu")
|
| ns = _make_ns(mc=mc)
|
| model = build_model(ns)
|
| total = sum(p.numel() for p in model.parameters())
|
| low, high = expected_param_count_range
|
| assert low <= total <= high, f"Expected param count in [{low}, {high}], got {total}"
|
|
|
| def test_encoder_only_returns_triple(self) -> None:
|
| """When encoder_only=True, build_model returns (encoder, None, None)."""
|
| mc = RFDETRBaseConfig(num_classes=80, pretrain_weights=None, device="cpu")
|
| ns = _make_ns(mc=mc)
|
| ns.encoder_only = True
|
| result = build_model(ns)
|
| assert isinstance(result, tuple), f"Expected tuple, got {type(result)}"
|
| assert len(result) == 3
|
| encoder, second, third = result
|
| assert second is None
|
| assert third is None
|
| assert encoder is not None
|
|
|
| def test_backbone_only_returns_triple(self) -> None:
|
| """When backbone_only=True, build_model returns (backbone, None, None)."""
|
| mc = RFDETRBaseConfig(num_classes=80, pretrain_weights=None, device="cpu")
|
| ns = _make_ns(mc=mc)
|
| ns.backbone_only = True
|
| result = build_model(ns)
|
| assert isinstance(result, tuple), f"Expected tuple, got {type(result)}"
|
| assert len(result) == 3
|
| backbone, second, third = result
|
| assert second is None
|
| assert third is None
|
| assert backbone is not None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| class TestBuildCriterionCharacterization:
|
| """Pin current build_criterion_and_postprocessors() behaviour."""
|
|
|
| def test_returns_criterion_and_postprocess(self) -> None:
|
| ns = _make_ns()
|
| criterion, postprocess = build_criterion_and_postprocessors(ns)
|
| assert isinstance(criterion, SetCriterion)
|
| assert isinstance(postprocess, PostProcess)
|
|
|
| def test_detection_losses_list(self) -> None:
|
| """Detection-only config has exactly ['labels', 'boxes', 'cardinality']."""
|
| ns = _make_ns()
|
| criterion, _ = build_criterion_and_postprocessors(ns)
|
| assert criterion.losses == ["labels", "boxes", "cardinality"]
|
|
|
| def test_segmentation_losses_include_masks(self) -> None:
|
| mc = RFDETRSegNanoConfig(pretrain_weights=None, device="cpu")
|
| tc = SegmentationTrainConfig(dataset_dir="/tmp")
|
| ns = _make_ns(mc=mc, tc=tc)
|
| criterion, _ = build_criterion_and_postprocessors(ns)
|
| assert "masks" in criterion.losses
|
|
|
| def test_num_select_forwarded_to_postprocess(self) -> None:
|
| mc = RFDETRSegNanoConfig(pretrain_weights=None, device="cpu")
|
| ns = _make_ns(mc=mc)
|
| _, postprocess = build_criterion_and_postprocessors(ns)
|
| assert postprocess.num_select == 100
|
|
|
| def test_num_select_default_for_base(self) -> None:
|
| ns = _make_ns()
|
| _, postprocess = build_criterion_and_postprocessors(ns)
|
| assert postprocess.num_select == 300
|
|
|
| def test_weight_dict_contains_base_losses(self) -> None:
|
| ns = _make_ns()
|
| criterion, _ = build_criterion_and_postprocessors(ns)
|
| assert "loss_ce" in criterion.weight_dict
|
| assert "loss_bbox" in criterion.weight_dict
|
| assert "loss_giou" in criterion.weight_dict
|
|
|
| def test_weight_dict_values_match_namespace(self) -> None:
|
| ns = _make_ns()
|
| criterion, _ = build_criterion_and_postprocessors(ns)
|
| assert criterion.weight_dict["loss_ce"] == ns.cls_loss_coef
|
| assert criterion.weight_dict["loss_bbox"] == ns.bbox_loss_coef
|
| assert criterion.weight_dict["loss_giou"] == ns.giou_loss_coef
|
|
|
| def test_segmentation_weight_dict_contains_mask_losses(self) -> None:
|
| mc = RFDETRSegNanoConfig(pretrain_weights=None, device="cpu")
|
| tc = SegmentationTrainConfig(dataset_dir="/tmp")
|
| ns = _make_ns(mc=mc, tc=tc)
|
| criterion, _ = build_criterion_and_postprocessors(ns)
|
| assert "loss_mask_ce" in criterion.weight_dict
|
| assert "loss_mask_dice" in criterion.weight_dict
|
|
|
| def test_aux_loss_expands_weight_dict(self) -> None:
|
| """With aux_loss=True and 3 dec_layers, weight_dict has aux entries _0 and _1."""
|
| mc = RFDETRBaseConfig(num_classes=80, pretrain_weights=None, device="cpu")
|
| ns = _make_ns(mc=mc)
|
| assert ns.aux_loss is True
|
| criterion, _ = build_criterion_and_postprocessors(ns)
|
|
|
| assert "loss_ce_0" in criterion.weight_dict
|
| assert "loss_ce_1" in criterion.weight_dict
|
|
|
| def test_two_stage_adds_enc_losses(self) -> None:
|
| """With two_stage=True, weight_dict has '_enc' suffix entries."""
|
| mc = RFDETRBaseConfig(num_classes=80, pretrain_weights=None, device="cpu")
|
| ns = _make_ns(mc=mc)
|
| assert ns.two_stage is True
|
| criterion, _ = build_criterion_and_postprocessors(ns)
|
| assert "loss_ce_enc" in criterion.weight_dict
|
| assert "loss_bbox_enc" in criterion.weight_dict
|
| assert "loss_giou_enc" in criterion.weight_dict
|
|
|
| def test_criterion_num_classes_plus_one(self) -> None:
|
| mc = RFDETRBaseConfig(num_classes=5, pretrain_weights=None, device="cpu")
|
| ns = _make_ns(mc=mc)
|
| criterion, _ = build_criterion_and_postprocessors(ns)
|
| assert criterion.num_classes == 6
|
|
|
| def test_focal_alpha_forwarded(self) -> None:
|
| ns = _make_ns()
|
| criterion, _ = build_criterion_and_postprocessors(ns)
|
| assert criterion.focal_alpha == pytest.approx(0.25)
|
|
|
| def test_group_detr_forwarded_to_criterion(self) -> None:
|
| mc = RFDETRBaseConfig(num_classes=80, pretrain_weights=None, device="cpu")
|
| ns = _make_ns(mc=mc)
|
| criterion, _ = build_criterion_and_postprocessors(ns)
|
| assert criterion.group_detr == mc.group_detr
|
|
|
| def test_segmentation_criterion_has_mask_point_sample_ratio(self) -> None:
|
| mc = RFDETRSegNanoConfig(pretrain_weights=None, device="cpu")
|
| tc = SegmentationTrainConfig(dataset_dir="/tmp")
|
| ns = _make_ns(mc=mc, tc=tc)
|
| criterion, _ = build_criterion_and_postprocessors(ns)
|
| assert criterion.mask_point_sample_ratio == 16
|
|
|
| def test_ia_bce_loss_forwarded(self) -> None:
|
| mc = RFDETRBaseConfig(num_classes=80, pretrain_weights=None, device="cpu")
|
| ns = _make_ns(mc=mc)
|
| criterion, _ = build_criterion_and_postprocessors(ns)
|
| assert criterion.ia_bce_loss == mc.ia_bce_loss
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| class TestBuildModelContextCharacterization:
|
| """Pin current _build_model_context() behaviour.
|
|
|
| _build_model_context is the inference-path factory used by RFDETR.get_model().
|
| It has zero test coverage today.
|
| """
|
|
|
| def test_returns_model_context(self) -> None:
|
| from rfdetr.detr import ModelContext, _build_model_context
|
|
|
| mc = RFDETRBaseConfig(num_classes=80, pretrain_weights=None, device="cpu")
|
| ctx = _build_model_context(mc)
|
| assert isinstance(ctx, ModelContext)
|
|
|
| def test_model_is_lwdetr(self) -> None:
|
| from rfdetr.detr import _build_model_context
|
|
|
| mc = RFDETRBaseConfig(num_classes=80, pretrain_weights=None, device="cpu")
|
| ctx = _build_model_context(mc)
|
| assert isinstance(ctx.model, LWDETR)
|
|
|
| def test_postprocess_is_postprocess(self) -> None:
|
| from rfdetr.detr import _build_model_context
|
|
|
| mc = RFDETRBaseConfig(num_classes=80, pretrain_weights=None, device="cpu")
|
| ctx = _build_model_context(mc)
|
| assert isinstance(ctx.postprocess, PostProcess)
|
|
|
| def test_resolution_from_config(self) -> None:
|
| from rfdetr.detr import _build_model_context
|
|
|
| mc = RFDETRBaseConfig(num_classes=80, pretrain_weights=None, device="cpu")
|
| ctx = _build_model_context(mc)
|
| assert ctx.resolution == mc.resolution
|
|
|
| def test_device_from_config(self) -> None:
|
| from rfdetr.detr import _build_model_context
|
|
|
| mc = RFDETRBaseConfig(num_classes=80, pretrain_weights=None, device="cpu")
|
| ctx = _build_model_context(mc)
|
| assert ctx.device == torch.device("cpu")
|
|
|
| def test_torch_device_cpu_from_config(self) -> None:
|
| from rfdetr.detr import _build_model_context
|
|
|
| mc = RFDETRBaseConfig(num_classes=80, pretrain_weights=None, device=torch.device("cpu"))
|
| ctx = _build_model_context(mc)
|
| assert ctx.device == torch.device("cpu")
|
|
|
| def test_class_names_none_without_pretrain(self) -> None:
|
| from rfdetr.detr import _build_model_context
|
|
|
| mc = RFDETRBaseConfig(num_classes=80, pretrain_weights=None, device="cpu")
|
| ctx = _build_model_context(mc)
|
| assert ctx.class_names is None
|
|
|
| def test_num_select_on_postprocess(self) -> None:
|
| from rfdetr.detr import _build_model_context
|
|
|
| mc = RFDETRSegNanoConfig(pretrain_weights=None, device="cpu")
|
| ctx = _build_model_context(mc)
|
| assert ctx.postprocess.num_select == 100
|
|
|
| def test_args_namespace_attached(self) -> None:
|
| from rfdetr.detr import _build_model_context
|
|
|
| mc = RFDETRBaseConfig(num_classes=80, pretrain_weights=None, device="cpu")
|
| ctx = _build_model_context(mc)
|
| assert hasattr(ctx.args, "num_classes")
|
| assert hasattr(ctx.args, "num_select")
|
|
|
| def test_inference_model_initially_none(self) -> None:
|
| from rfdetr.detr import _build_model_context
|
|
|
| mc = RFDETRBaseConfig(num_classes=80, pretrain_weights=None, device="cpu")
|
| ctx = _build_model_context(mc)
|
| assert ctx.inference_model is None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| class TestRFDETRModelModuleInitCharacterization:
|
| """Pin RFDETRModelModule.__init__() structural outputs.
|
|
|
| The existing test_module_model.py tests the init via mocked build_model and
|
| build_namespace. These tests exercise the REAL init path (no mocks) to
|
| characterize what a freshly built module looks like.
|
| """
|
|
|
| def _make_module(self, mc=None, tc=None):
|
| from rfdetr.training.module_model import RFDETRModelModule
|
|
|
| mc = mc or RFDETRBaseConfig(num_classes=5, pretrain_weights=None, device="cpu")
|
| tc = tc or TrainConfig(dataset_dir="/tmp")
|
| return RFDETRModelModule(mc, tc)
|
|
|
| def test_model_attribute_is_lwdetr(self) -> None:
|
| module = self._make_module()
|
|
|
| underlying = getattr(module.model, "_orig_mod", module.model)
|
| assert isinstance(underlying, LWDETR)
|
|
|
| def test_criterion_is_set_criterion(self) -> None:
|
| module = self._make_module()
|
| assert isinstance(module.criterion, SetCriterion)
|
|
|
| def test_postprocess_is_postprocess(self) -> None:
|
| module = self._make_module()
|
| assert isinstance(module.postprocess, PostProcess)
|
|
|
| def test_strict_loading_false(self) -> None:
|
| """strict_loading=False allows partial state-dict loading."""
|
| module = self._make_module()
|
| assert module.strict_loading is False
|
|
|
| def test_configs_stored(self) -> None:
|
| mc = RFDETRBaseConfig(num_classes=5, pretrain_weights=None, device="cpu")
|
| tc = TrainConfig(dataset_dir="/tmp")
|
| module = self._make_module(mc=mc, tc=tc)
|
| assert module.model_config is mc
|
| assert module.train_config is tc
|
|
|
| def test_criterion_num_classes_matches_model(self) -> None:
|
| """Criterion and model must agree on num_classes (both use +1 convention)."""
|
| mc = RFDETRBaseConfig(num_classes=5, pretrain_weights=None, device="cpu")
|
| module = self._make_module(mc=mc)
|
| underlying = getattr(module.model, "_orig_mod", module.model)
|
| assert module.criterion.num_classes == underlying.class_embed.out_features
|
|
|
| def test_postprocess_num_select_matches_config(self) -> None:
|
| mc = RFDETRSegNanoConfig(pretrain_weights=None, device="cpu")
|
| tc = SegmentationTrainConfig(dataset_dir="/tmp")
|
| module = self._make_module(mc=mc, tc=tc)
|
| assert module.postprocess.num_select == mc.num_select
|
|
|
| def test_segmentation_criterion_with_seg_config(self) -> None:
|
| mc = RFDETRSegNanoConfig(pretrain_weights=None, device="cpu")
|
| tc = SegmentationTrainConfig(dataset_dir="/tmp")
|
| module = self._make_module(mc=mc, tc=tc)
|
| assert "masks" in module.criterion.losses
|
|
|