Skip to content

⚡️ Speed up method _Min.__call__ by 9% - #18

Open
codeflash-ai[bot] wants to merge 1 commit into
mainfrom
codeflash/optimize-_Min.__call__-mih93p2q
Open

⚡️ Speed up method _Min.__call__ by 9%#18
codeflash-ai[bot] wants to merge 1 commit into
mainfrom
codeflash/optimize-_Min.__call__-mih93p2q

Conversation

@codeflash-ai

@codeflash-ai codeflash-ai Bot commented Nov 27, 2025

Copy link
Copy Markdown

📄 9% (0.09x) speedup for _Min.__call__ in src/titiler/core/titiler/core/algorithm/math.py

⏱️ Runtime : 527 microseconds 484 microseconds (best of 88 runs)

📝 Explanation and details

The optimized code introduces a fast-path optimization for single-band images that avoids unnecessary computation when the input array already has only one band.

Key optimization:

  • Single-band shortcut: When arr.shape[0] == 1, the code directly uses the original array instead of calling numpy.ma.min(), since a single-band array is already its own minimum along the band axis.
  • Multi-band preservation: For arrays with multiple bands, the original numpy.ma.min() computation is preserved exactly.

Why this leads to speedup:

  • numpy.ma.min() involves axis reduction computation, memory allocation for the result array, and masked array handling overhead, even when there's only one band to "reduce"
  • The single-band case simply reuses the existing array reference, eliminating all computation and allocation overhead
  • This is particularly effective because the line profiler shows numpy.ma.min() accounts for 80.4% of the original runtime

Performance impact based on test results:

  • Single-band images: Show dramatic improvements (139% faster in test_min_basic_single_band, 922% faster in test_min_edge_single_band_masked_pixel)
  • Multi-band images: Maintain nearly identical performance with minimal overhead from the condition check (typically 0-4% slower due to the extra conditional)
  • Overall: 8% speedup suggests a mixed workload where single-band cases provide significant gains that outweigh the small multi-band overhead

This optimization is particularly valuable in geospatial workflows where single-band raster processing (like elevation models, temperature data, or derived indices) is common, providing substantial performance gains without any behavioral changes.

Correctness verification report:

Test Status
⚙️ Existing Unit Tests 🔘 None Found
🌀 Generated Regression Tests 62 Passed
⏪ Replay Tests 🔘 None Found
🔎 Concolic Coverage Tests 🔘 None Found
📊 Tests Coverage 100.0%
🌀 Generated Regression Tests and Runtime
from types import SimpleNamespace

import numpy as np
# imports
import pytest  # used for our unit tests
from titiler.core.algorithm.math import _Min

# function to test
# (copy-pasted from titiler/core/algorithm/math.py for self-containment)
class ImageData:
    def __init__(
        self,
        array,
        assets=None,
        crs=None,
        bounds=None,
        band_names=None,
        metadata=None,
        cutline_mask=None,
    ):
        self.array = array
        self.assets = assets
        self.crs = crs
        self.bounds = bounds
        self.band_names = band_names
        self.metadata = metadata
        self.cutline_mask = cutline_mask

    def __eq__(self, other):
        # For testing: compare all attributes
        if not isinstance(other, ImageData):
            return False
        # Compare arrays (handle masked arrays)
        if not np.ma.allequal(self.array, other.array):
            return False
        # Compare other attributes
        for attr in ['assets', 'crs', 'bounds', 'band_names', 'metadata', 'cutline_mask']:
            if getattr(self, attr) != getattr(other, attr):
                return False
        return True

class BaseAlgorithm:
    pass
from titiler.core.algorithm.math import _Min

# ---------------------------
# Unit tests for _Min.__call__
# ---------------------------

@pytest.fixture
def default_metadata():
    # Provide a default metadata dictionary for tests
    return {'foo': 'bar'}

@pytest.fixture
def default_cutline_mask():
    # Provide a default cutline mask (for test completeness)
    return np.array([[1, 0], [0, 1]])

# 1. Basic Test Cases

def test_single_band(default_metadata, default_cutline_mask):
    # Test with a single band (should be returned as-is)
    arr = np.array([[[1, 2], [3, 4]]])  # shape (1, 2, 2)
    img = ImageData(arr, assets=['a'], crs='epsg:4326', bounds=(0,0,1,1),
                    band_names=['b1'], metadata=default_metadata, cutline_mask=default_cutline_mask)
    result = _Min()(img)
    # The min along axis=0 (bands) is just the same array
    expected = ImageData(arr, assets=['a'], crs='epsg:4326', bounds=(0,0,1,1),
                         band_names=['min'], metadata=default_metadata, cutline_mask=default_cutline_mask)

def test_multiple_bands(default_metadata):
    # Test with multiple bands
    arr = np.array([
        [[1, 5], [3, 7]],
        [[2, 4], [0, 8]],
        [[-1, 6], [6, 2]],
    ])  # shape (3, 2, 2)
    img = ImageData(arr, assets=['a'], crs='epsg:3857', bounds=(1,2,3,4),
                    band_names=['b1','b2','b3'], metadata=default_metadata)
    result = _Min()(img)
    # min over bands axis (axis=0)
    expected_array = np.min(arr, axis=0, keepdims=True)

def test_negative_and_positive_values():
    # Test with negative and positive values
    arr = np.array([
        [[-10, 0], [1, 2]],
        [[-5, 3], [-1, 4]],
    ])  # shape (2, 2, 2)
    img = ImageData(arr)
    result = _Min()(img)
    expected = np.min(arr, axis=0, keepdims=True)

def test_float_values():
    # Test with float values
    arr = np.array([
        [[0.1, 2.5], [3.3, 4.4]],
        [[-1.2, 2.5], [3.1, 0.0]],
    ])
    img = ImageData(arr)
    result = _Min()(img)
    expected = np.min(arr, axis=0, keepdims=True)

def test_different_assets_and_bandnames():
    # Test that assets and band_names are handled correctly
    arr = np.ones((2, 2, 2))
    img = ImageData(arr, assets=['asset1', 'asset2'], band_names=['red', 'green'])
    result = _Min()(img)

# 2. Edge Test Cases

def test_all_equal_values():
    # All values are the same
    arr = np.full((3, 4, 5), 7)
    img = ImageData(arr)
    result = _Min()(img)
    expected = np.full((1, 4, 5), 7)

def test_masked_array():
    # Test with masked values
    arr = np.array([
        [[1, 2], [3, 4]],
        [[2, 1], [4, 3]],
    ])
    mask = np.array([
        [[0, 1], [1, 0]],
        [[1, 0], [0, 1]],
    ], dtype=bool)
    marr = np.ma.array(arr, mask=mask)
    img = ImageData(marr)
    result = _Min()(img)
    # min should ignore masked values
    expected = np.ma.min(marr, axis=0, keepdims=True)

def test_all_masked():
    # All values are masked: result should be masked
    arr = np.arange(8).reshape(2,2,2)
    marr = np.ma.masked_all(arr.shape)
    img = ImageData(marr)
    result = _Min()(img)

def test_empty_array():
    # Test with empty array (should raise ValueError)
    arr = np.empty((0, 2, 2))
    img = ImageData(arr)
    with pytest.raises(ValueError):
        _Min()(img)

def test_single_pixel():
    # Test with a single pixel
    arr = np.array([[[42]]])
    img = ImageData(arr)
    result = _Min()(img)

def test_nan_values():
    # Test with NaN values
    arr = np.array([
        [[np.nan, 2], [3, np.nan]],
        [[1, np.nan], [np.nan, 4]],
    ])
    marr = np.ma.masked_invalid(arr)
    img = ImageData(marr)
    result = _Min()(img)
    expected = np.ma.min(marr, axis=0, keepdims=True)

def test_inf_values():
    # Test with inf values
    arr = np.array([
        [[np.inf, -np.inf], [0, 1]],
        [[1, 2], [np.inf, -np.inf]],
    ])
    img = ImageData(arr)
    result = _Min()(img)
    expected = np.min(arr, axis=0, keepdims=True)

def test_metadata_and_cutline_mask_preserved(default_metadata, default_cutline_mask):
    # Test that metadata and cutline_mask are preserved
    arr = np.arange(8).reshape(2,2,2)
    img = ImageData(arr, metadata=default_metadata, cutline_mask=default_cutline_mask)
    result = _Min()(img)

# 3. Large Scale Test Cases

def test_large_array_performance():
    # Test with a large array (100 bands, 50x50 pixels)
    arr = np.random.randint(-1000, 1000, size=(100, 50, 50))
    img = ImageData(arr)
    result = _Min()(img)
    expected = np.min(arr, axis=0, keepdims=True)

def test_large_masked_array():
    # Test with a large masked array (50 bands, 30x30 pixels)
    arr = np.random.rand(50, 30, 30)
    mask = np.random.rand(50, 30, 30) > 0.95  # ~5% masked
    marr = np.ma.array(arr, mask=mask)
    img = ImageData(marr)
    result = _Min()(img)
    expected = np.ma.min(marr, axis=0, keepdims=True)

def test_large_all_masked():
    # All masked, large array
    marr = np.ma.masked_all((10, 100, 5))
    img = ImageData(marr)
    result = _Min()(img)

def test_large_single_band():
    # Large single band (1, 500, 2)
    arr = np.random.rand(1, 500, 2)
    img = ImageData(arr)
    result = _Min()(img)
# codeflash_output is used to check that the output of the original code is the same as that of the optimized code.
import numpy as np
# imports
import pytest  # used for our unit tests
from titiler.core.algorithm.math import _Min

# Mocks for ImageData and BaseAlgorithm
class ImageData:
    def __init__(self, array, assets=None, crs=None, bounds=None, band_names=None, metadata=None, cutline_mask=None):
        self.array = array
        self.assets = assets
        self.crs = crs
        self.bounds = bounds
        self.band_names = band_names
        self.metadata = metadata
        self.cutline_mask = cutline_mask

    def __eq__(self, other):
        # Equality for testing purposes: compare arrays and band_names only
        return (
            np.ma.allequal(self.array, other.array)
            and self.band_names == other.band_names
        )

class BaseAlgorithm:
    pass
from titiler.core.algorithm.math import _Min

# unit tests

# ---- BASIC TEST CASES ----

def test_min_basic_2bands_2x2():
    # Test with a simple 2-band, 2x2 image
    arr = np.ma.array([
        [[1, 2],
         [3, 4]],
        [[4, 3],
         [2, 1]]
    ])  # shape (2,2,2)
    img = ImageData(arr, band_names=["b1", "b2"])
    codeflash_output = _Min().__call__(img); result = codeflash_output # 24.4μs -> 26.6μs (8.17% slower)
    # The minimum across bands for each pixel:
    # [[min(1,4), min(2,3)],
    #  [min(3,2), min(4,1)]]
    expected = np.ma.array([[[1,2],[2,1]]])  # shape (1,2,2)

def test_min_basic_single_band():
    # Test with a single band: result should be identical, but with band_names=["min"]
    arr = np.ma.array([[[5,6],[7,8]]])  # shape (1,2,2)
    img = ImageData(arr, band_names=["b1"])
    codeflash_output = _Min().__call__(img); result = codeflash_output # 24.1μs -> 10.1μs (139% faster)
    expected = arr  # shape (1,2,2)

def test_min_basic_negative_values():
    # Test with negative values
    arr = np.ma.array([
        [[-1,  0],
         [ 2, -3]],
        [[-2,  5],
         [ 1, -4]]
    ])  # shape (2,2,2)
    img = ImageData(arr)
    codeflash_output = _Min().__call__(img); result = codeflash_output # 24.2μs -> 25.3μs (4.16% slower)
    expected = np.ma.array([[[-2, 0], [1, -4]]])  # min across bands

def test_min_basic_all_same_values():
    # All values are the same
    arr = np.ma.array([
        [[7,7],[7,7]],
        [[7,7],[7,7]]
    ])
    img = ImageData(arr)
    codeflash_output = _Min().__call__(img); result = codeflash_output # 23.6μs -> 24.8μs (5.01% slower)
    expected = np.ma.array([[[7,7],[7,7]]])

# ---- EDGE TEST CASES ----

def test_min_edge_all_masked():
    # All values masked: result should be masked everywhere
    arr = np.ma.masked_all((2,2,2))
    img = ImageData(arr)
    codeflash_output = _Min().__call__(img); result = codeflash_output # 44.1μs -> 43.7μs (0.919% faster)

def test_min_edge_some_masked():
    # Some masked values: min should ignore masked values
    arr = np.ma.array([
        [[1, 2],
         [3, 4]],
        [[4, 3],
         [2, 1]]
    ], mask=[
        [[False, True],
         [False, False]],
        [[False, False],
         [True, False]]
    ])
    img = ImageData(arr)
    codeflash_output = _Min().__call__(img); result = codeflash_output # 42.0μs -> 41.7μs (0.804% faster)
    # For (0,0): min(1,4)=1; (0,1): masked in first band, min(3,3)=3
    # (1,0): min(3,masked)=3; (1,1): min(4,1)=1
    expected = np.ma.array([[[1,3],[3,1]]], mask=[[False,False],[False,False]])

def test_min_edge_nan_values():
    # Test with np.nan values (unmasked): np.nan is treated as a value, so min(np.nan, x) is np.nan
    arr = np.ma.array([
        [[np.nan, 2],
         [3, np.nan]],
        [[4, np.nan],
         [np.nan, 1]]
    ])
    img = ImageData(arr)
    codeflash_output = _Min().__call__(img); result = codeflash_output # 24.3μs -> 25.0μs (3.10% slower)
    # (0,0): min(np.nan,4)=np.nan; (0,1): min(2,np.nan)=2
    # (1,0): min(3,np.nan)=3; (1,1): min(np.nan,1)=np.nan
    expected = np.ma.array([[[np.nan,2],[3,np.nan]]])

def test_min_edge_empty_array():
    # Empty array: shape (0,2,2) or (2,0,2) or (2,2,0)
    arr = np.ma.array([]).reshape((0,2,2))
    img = ImageData(arr)
    with pytest.raises(ValueError):
        _Min().__call__(img) # 12.4μs -> 12.9μs (3.67% slower)

def test_min_edge_single_pixel():
    # Single pixel, multiple bands
    arr = np.ma.array([
        [[5]],
        [[3]],
        [[7]]
    ])  # shape (3,1,1)
    img = ImageData(arr)
    codeflash_output = _Min().__call__(img); result = codeflash_output # 25.2μs -> 26.2μs (3.82% slower)
    expected = np.ma.array([[[3]]])

def test_min_edge_single_band_masked_pixel():
    # Single band, single pixel, masked
    arr = np.ma.masked_array([[[5]]], mask=[[[True]]])
    img = ImageData(arr)
    codeflash_output = _Min().__call__(img); result = codeflash_output # 43.5μs -> 4.26μs (922% faster)

# ---- LARGE SCALE TEST CASES ----

def test_min_large_10bands_100x100():
    # Large array, 10 bands, 100x100 pixels
    np.random.seed(42)
    arr = np.ma.array(np.random.randint(0,1000,(10,100,100)))
    img = ImageData(arr)
    codeflash_output = _Min().__call__(img); result = codeflash_output # 52.9μs -> 54.9μs (3.59% slower)
    # Check that each pixel is the minimum across bands
    for i in range(100):
        for j in range(100):
            expected = arr[:,i,j].min()

def test_min_large_masked_random():
    # Large array with random masking
    np.random.seed(123)
    arr = np.ma.array(np.random.randint(-100,100,(5,50,50)))
    mask = np.random.rand(5,50,50) < 0.1  # 10% masked
    arr.mask = mask
    img = ImageData(arr)
    codeflash_output = _Min().__call__(img); result = codeflash_output # 87.8μs -> 87.9μs (0.107% slower)
    # For a few random pixels, check that result is correct (ignoring masked values)
    for i,j in [(0,0),(25,25),(49,49)]:
        vals = arr[:,i,j]
        if vals.mask.all():
            pass
        else:
            expected = vals.compressed().min()

def test_min_large_all_masked_band():
    # One band is masked everywhere, others are not
    arr = np.ma.array(np.random.randint(0,100,(3,20,20)))
    arr.mask = np.zeros_like(arr, dtype=bool)
    arr.mask[0,:,:] = True  # Mask first band everywhere
    img = ImageData(arr)
    codeflash_output = _Min().__call__(img); result = codeflash_output # 43.4μs -> 43.7μs (0.753% slower)
    # Should be min of bands 1 and 2
    expected = np.ma.min(arr[1:,:,:], axis=0, keepdims=True)

def test_min_large_constant_and_random():
    # One band is constant, others are random
    np.random.seed(99)
    arr = np.ma.array(np.vstack([
        np.full((1,50,50), 123),
        np.random.randint(0,200,(4,50,50))
    ]))
    img = ImageData(arr)
    codeflash_output = _Min().__call__(img); result = codeflash_output # 27.9μs -> 28.8μs (3.07% slower)
    # For each pixel, result should be min of 123 and the random values
    for i in range(50):
        for j in range(50):
            expected = min([123] + [arr[k,i,j] for k in range(1,5)])

# ---- METADATA/COPY TEST CASES ----

def test_min_metadata_and_assets_preserved():
    # Check that assets, crs, bounds, metadata, cutline_mask are preserved
    arr = np.ma.array([[[1,2],[3,4]],[[4,3],[2,1]]])
    img = ImageData(
        arr,
        assets=["foo"],
        crs="EPSG:4326",
        bounds=(0,0,1,1),
        band_names=["b1","b2"],
        metadata={"sensor":"test"},
        cutline_mask=np.array([[True,False],[False,True]])
    )
    codeflash_output = _Min().__call__(img); result = codeflash_output # 27.2μs -> 28.1μs (3.19% slower)
# codeflash_output is used to check that the output of the original code is the same as that of the optimized code.
from titiler.core.algorithm.math import _Min

To edit these changes git checkout codeflash/optimize-_Min.__call__-mih93p2q and push.

Codeflash Static Badge

The optimized code introduces a **fast-path optimization for single-band images** that avoids unnecessary computation when the input array already has only one band.

**Key optimization:**
- **Single-band shortcut**: When `arr.shape[0] == 1`, the code directly uses the original array instead of calling `numpy.ma.min()`, since a single-band array is already its own minimum along the band axis.
- **Multi-band preservation**: For arrays with multiple bands, the original `numpy.ma.min()` computation is preserved exactly.

**Why this leads to speedup:**
- `numpy.ma.min()` involves axis reduction computation, memory allocation for the result array, and masked array handling overhead, even when there's only one band to "reduce"
- The single-band case simply reuses the existing array reference, eliminating all computation and allocation overhead
- This is particularly effective because the line profiler shows `numpy.ma.min()` accounts for 80.4% of the original runtime

**Performance impact based on test results:**
- **Single-band images**: Show dramatic improvements (139% faster in `test_min_basic_single_band`, 922% faster in `test_min_edge_single_band_masked_pixel`)
- **Multi-band images**: Maintain nearly identical performance with minimal overhead from the condition check (typically 0-4% slower due to the extra conditional)
- **Overall**: 8% speedup suggests a mixed workload where single-band cases provide significant gains that outweigh the small multi-band overhead

This optimization is particularly valuable in geospatial workflows where single-band raster processing (like elevation models, temperature data, or derived indices) is common, providing substantial performance gains without any behavioral changes.
@codeflash-ai
codeflash-ai Bot requested a review from mashraf-222 November 27, 2025 09:48
@codeflash-ai codeflash-ai Bot added ⚡️ codeflash Optimization PR opened by Codeflash AI 🎯 Quality: High Optimization Quality according to Codeflash labels Nov 27, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

⚡️ codeflash Optimization PR opened by Codeflash AI 🎯 Quality: High Optimization Quality according to Codeflash

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants