from __future__ import annotations from contextlib import ExitStack as DoesNotRaise import numpy as np import pytest from supervision.classification.core import Classifications class _MockTensor: """Minimal tensor double that supports the tensor chain used by adapters.""" def __init__(self, value: np.ndarray) -> None: self.value = value def softmax(self, dim: int) -> _MockTensor: """Return a tensor double with softmax applied along the requested axis.""" if self.value.shape[dim] == 0: return _MockTensor(self.value) exp = np.exp(self.value - np.max(self.value, axis=dim, keepdims=True)) return _MockTensor(exp / np.sum(exp, axis=dim, keepdims=True)) def cpu(self) -> _MockTensor: """Return the tensor double for chained CPU conversion calls.""" return self def detach(self) -> _MockTensor: """Return the tensor double for chained graph-detach calls.""" return self def numpy(self) -> np.ndarray: """Return the wrapped NumPy array.""" return self.value @pytest.mark.parametrize( ("class_id", "confidence", "k", "expected_result", "exception"), [ ( np.array([0, 1, 2, 3, 4]), np.array([0.1, 0.2, 0.9, 0.4, 0.5]), 5, (np.array([2, 4, 3, 1, 0]), np.array([0.9, 0.5, 0.4, 0.2, 0.1])), DoesNotRaise(), ), # class_id with 5 numbers and 5 confidences ( np.array([5, 1, 2, 3, 4]), np.array([0.1, 0.2, 0.9, 0.4, 0.5]), 1, (np.array([2]), np.array([0.9])), DoesNotRaise(), ), # class_id with 5 numbers and 5 confidences, retrieve where k = 1 ( np.array([4, 1, 2, 3, 6, 5]), np.array([0.8, 0.2, 0.9, 0.4, 0.5, 0.1]), 2, (np.array([2, 4]), np.array([0.9, 0.8])), DoesNotRaise(), ), # class_id with 5 numbers and 5 confidences, retrieve where k = 3 ( np.array([0, 1, 2, 3, 4]), np.array([]), 5, None, pytest.raises(ValueError, match=r"confidence must be 1d np\.ndarray"), ), # class_id with 5 numbers and 0 confidences ( [0, 1, 2, 3, 4], [0.1, 0.2, 0.3, 0.4], 5, None, pytest.raises(ValueError, match="\\(n, \\) shape"), ), # class_id with 5 numbers and 4 confidences ], ) def test_top_k( class_id: np.ndarray, confidence: np.ndarray | None, k: int, expected_result: tuple[np.ndarray, np.ndarray] | None, exception: Exception, ) -> None: """Retrieves requested top-k values or raises for malformed confidence input.""" with exception: result = Classifications( class_id=np.array(class_id), confidence=np.array(confidence) ).get_top_k(k) assert np.array_equal(result[0], expected_result[0]) assert np.array_equal(result[1], expected_result[1]) def test_from_clip_empty_output_dtypes() -> None: """Empty CLIP logits produce typed empty classification arrays.""" result = Classifications.from_clip(_MockTensor(np.empty((1, 0), dtype=np.float32))) assert result.class_id.dtype == np.int_ assert result.confidence is not None assert result.confidence.dtype == np.float32 def test_from_timm_empty_output_dtypes() -> None: """Empty timm logits produce typed empty classification arrays.""" result = Classifications.from_timm(_MockTensor(np.empty((1, 0), dtype=np.float32))) assert result.class_id.dtype == np.int_ assert result.confidence is not None assert result.confidence.dtype == np.float32 def test_from_timm_softmaxes_logits() -> None: """Timm logits are converted to normalized confidence scores.""" logits = np.array([[0.0, 1.0, 2.0]], dtype=np.float32) result = Classifications.from_timm(_MockTensor(logits)) assert result.confidence is not None assert np.allclose( result.confidence, _MockTensor(logits).softmax(dim=-1).numpy()[0] ) assert np.isclose(np.sum(result.confidence), 1.0) def test_classifications_compare_numpy_fields_by_value() -> None: """Classifications equality handles NumPy arrays and confidence values.""" left = Classifications( class_id=np.array([0, 1], dtype=np.int_), confidence=np.array([0.25, 0.75], dtype=np.float32), ) right = Classifications( class_id=np.array([0, 1], dtype=np.int_), confidence=np.array([0.25, 0.75], dtype=np.float32), ) different = Classifications( class_id=np.array([0, 1], dtype=np.int_), confidence=np.array([0.25, 0.5], dtype=np.float32), ) assert left == right assert left != different