Spaces:
Runtime error
Runtime error
| """Regression tests for the classifier threshold bug. | |
| Bug: ``VariantAPipeline.predict(threshold=...)`` declared the parameter but | |
| ``_forward`` ignored it and hardcoded ``>= 0.5``. This file pins the | |
| threshold-honored behavior so it can't silently regress. | |
| We avoid loading real ResNet50 weights β instead we stub the pipeline's | |
| classifier and transform with the smallest possible callables that produce | |
| known sigmoid probabilities. | |
| """ | |
| from __future__ import annotations | |
| import pytest | |
| import torch | |
| import torch.nn as nn | |
| from PIL import Image | |
| from ccdp.data.schema import DAMAGE_TYPES | |
| from ccdp.infer import variant_a as variant_a_mod | |
| from ccdp.infer.variant_a import VariantAPipeline | |
| def _stub_extract_features(monkeypatch): | |
| """``_forward`` calls ``extract_features`` which expects a real ResNet50. | |
| Replace it with a no-op so we can swap in a tiny test classifier.""" | |
| monkeypatch.setattr(variant_a_mod, "extract_features", | |
| lambda model, x: torch.zeros(1, 2048)) | |
| class _ConstantLogits(nn.Module): | |
| """Returns the same logits regardless of input.""" | |
| def __init__(self, logits: torch.Tensor): | |
| super().__init__() | |
| self.register_buffer("_logits", logits) | |
| def forward(self, _x): | |
| return self._logits | |
| def fc(self): # extract_features() expects a `.fc` attribute | |
| return nn.Identity() | |
| def _stub_pipeline(pipeline: VariantAPipeline, logits_per_class: list[float]) -> None: | |
| """Replace the heavy classifier with a deterministic 6-class logit emitter.""" | |
| logits = torch.tensor([logits_per_class], dtype=torch.float32) | |
| pipeline.classifier = _ConstantLogits(logits) | |
| pipeline.device = torch.device("cpu") | |
| # Tiny transform β feature extraction in `_forward` will run, but our | |
| # _ConstantLogits ignores its input so the actual values don't matter. | |
| pipeline.transform = lambda img: torch.zeros(3, 4, 4) | |
| def _make_pipeline_skipping_init() -> VariantAPipeline: | |
| """Bypass __init__ β we only want to test the _forward thresholding.""" | |
| return VariantAPipeline.__new__(VariantAPipeline) | |
| def test_threshold_honored_high_filters_all(): | |
| pipe = _make_pipeline_skipping_init() | |
| # Sigmoid(2.0) β 0.88 β would clear the default 0.5 threshold. | |
| _stub_pipeline(pipe, [2.0] * len(DAMAGE_TYPES)) | |
| img = Image.new("RGB", (8, 8)) | |
| # With a strict threshold, nothing should be reported. | |
| types, probs, _ = pipe._forward(img, threshold=0.95) | |
| assert types == [] | |
| # Probability dict must still report all classes regardless of threshold. | |
| assert set(probs.keys()) == set(DAMAGE_TYPES) | |
| def test_threshold_default_keeps_above_half(): | |
| pipe = _make_pipeline_skipping_init() | |
| # logits = [3, -3, 3, -3, 3, -3] β sigmoids alternate ~0.95 / ~0.05 | |
| logits = [3.0 if i % 2 == 0 else -3.0 for i in range(len(DAMAGE_TYPES))] | |
| _stub_pipeline(pipe, logits) | |
| img = Image.new("RGB", (8, 8)) | |
| types, probs, _ = pipe._forward(img, threshold=0.5) | |
| # Even-indexed classes pass; odd ones are filtered out. | |
| expected = [DAMAGE_TYPES[i] for i in range(len(DAMAGE_TYPES)) if i % 2 == 0] | |
| assert types == expected | |
| for i, name in enumerate(DAMAGE_TYPES): | |
| if i % 2 == 0: | |
| assert probs[name] > 0.5 | |
| else: | |
| assert probs[name] < 0.5 | |
| def test_threshold_low_admits_all(): | |
| pipe = _make_pipeline_skipping_init() | |
| # Logits of 0 β sigmoid 0.5 exactly. With threshold=0.1 all should pass. | |
| _stub_pipeline(pipe, [0.0] * len(DAMAGE_TYPES)) | |
| img = Image.new("RGB", (8, 8)) | |
| types, _, _ = pipe._forward(img, threshold=0.1) | |
| assert set(types) == set(DAMAGE_TYPES) | |