Skip to content

⚡️ Speed up method _Sum.__call__ by 23% - #12

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

⚡️ Speed up method _Sum.__call__ by 23%#12
codeflash-ai[bot] wants to merge 1 commit into
mainfrom
codeflash/optimize-_Sum.__call__-mifmxm73

Conversation

@codeflash-ai

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

Copy link
Copy Markdown

📄 23% (0.23x) speedup for _Sum.__call__ in src/titiler/core/titiler/core/algorithm/math.py

⏱️ Runtime : 517 microseconds 422 microseconds (best of 250 runs)

📝 Explanation and details

The optimization replaces the blanket use of numpy.ma.sum() with conditional array type checking to avoid the overhead of masked array operations when they're unnecessary.

Key changes:

  1. Caches array reference (arr = img.array) to avoid repeated attribute access
  2. Type-specific sum operations:
    • For MaskedArray with no actual masked values: uses arr.data.sum() directly on the underlying data
    • For regular ndarray: uses native .sum() method
    • Only falls back to numpy.ma.sum() when masking is actually present

Why this is faster:

  • numpy.ma.sum() always performs mask checking and special handling even when no values are masked, adding significant overhead (79.1% of original runtime)
  • Direct array .sum() operations bypass this overhead entirely
  • The optimized version reduces sum operation time from ~2.09ms to ~0.68ms (67% reduction in sum operation time)

Performance by test case:

  • Best gains (29-34% faster): Regular arrays and simple cases benefit most from bypassing masked array overhead
  • Masked arrays with actual masks show slight slowdown (4.4%) due to added type checking, but this preserves correctness
  • Large arrays still see 14-26% improvements, indicating the optimization scales well

The optimization is particularly effective because most real-world image data uses MaskedArray containers for consistency but often contains no actual masked values, making the masked array overhead pure waste.

Correctness verification report:

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

# function to test
import numpy
import numpy as np
# imports
import pytest
from rio_tiler.models import ImageData
from titiler.core.algorithm.base import BaseAlgorithm
from titiler.core.algorithm.math import _Sum

# Helper: minimal ImageData mock for testing
class DummyImageData:
    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

    # To support attribute copying in __call__ return
    def __eq__(self, other):
        # Used for debugging only, not in tests
        return (
            np.all(self.array == other.array)
            and self.assets == other.assets
            and self.crs == other.crs
            and self.bounds == other.bounds
            and self.band_names == other.band_names
            and self.metadata == other.metadata
            and np.all(self.cutline_mask == other.cutline_mask)
        )

# Test suite for _Sum.__call__
class TestSumCall:
    # --- Basic Test Cases ---
    def test_single_band(self):
        # One band, shape (1, 2, 2)
        arr = np.array([[[1, 2], [3, 4]]])
        img = DummyImageData(arr)
        codeflash_output = _Sum().__call__(img); result = codeflash_output # 38.2μs -> 29.6μs (29.3% faster)
        # Should be same as input
        expected = np.sum(arr, axis=0, keepdims=True)

    def test_two_bands(self):
        # Two bands, shape (2, 2, 2)
        arr = np.array([
            [[1, 2], [3, 4]],
            [[10, 20], [30, 40]]
        ])
        img = DummyImageData(arr)
        codeflash_output = _Sum().__call__(img); result = codeflash_output # 33.3μs -> 25.7μs (29.6% faster)
        expected = np.sum(arr, axis=0, keepdims=True)

    def test_negative_numbers(self):
        # Negative and positive numbers
        arr = np.array([
            [[-1, 2], [3, -4]],
            [[1, -2], [-3, 4]]
        ])
        img = DummyImageData(arr)
        codeflash_output = _Sum().__call__(img); result = codeflash_output # 32.5μs -> 24.4μs (32.8% faster)
        expected = np.sum(arr, axis=0, keepdims=True)

    def test_float_values(self):
        # Float values
        arr = np.array([
            [[1.5, 2.5], [3.5, 4.5]],
            [[0.5, 0.5], [0.5, 0.5]]
        ])
        img = DummyImageData(arr)
        codeflash_output = _Sum().__call__(img); result = codeflash_output # 33.3μs -> 24.8μs (34.1% faster)
        expected = np.sum(arr, axis=0, keepdims=True)

    # --- Edge Test Cases ---
    def test_zero_bands(self):
        # No bands: shape (0, 2, 2)
        arr = np.zeros((0, 2, 2))
        img = DummyImageData(arr)
        codeflash_output = _Sum().__call__(img); result = codeflash_output # 32.3μs -> 24.4μs (32.4% faster)

    def test_single_pixel(self):
        # Shape (3, 1, 1)
        arr = np.array([[[1]], [[2]], [[3]]])
        img = DummyImageData(arr)
        codeflash_output = _Sum().__call__(img); result = codeflash_output # 33.3μs -> 25.3μs (31.6% faster)

    def test_empty_spatial(self):
        # Shape (2, 0, 0)
        arr = np.zeros((2, 0, 0))
        img = DummyImageData(arr)
        codeflash_output = _Sum().__call__(img); result = codeflash_output # 32.3μs -> 25.2μs (28.4% faster)

    def test_masked_array(self):
        # Masked array: mask a value
        arr = np.ma.array([[[1, 2], [3, 4]], [[10, 20], [30, 40]]], mask=[[[0, 1], [0, 0]], [[0, 0], [0, 0]]])
        img = DummyImageData(arr)
        codeflash_output = _Sum().__call__(img); result = codeflash_output # 38.4μs -> 40.1μs (4.40% slower)
        # The sum should ignore masked values
        expected = np.ma.sum(arr, axis=0, keepdims=True)

    def test_preserves_metadata(self):
        # Metadata and cutline_mask are preserved
        arr = np.ones((2, 2, 2))
        metadata = {"foo": "bar"}
        cutline_mask = np.array([[1, 0], [0, 1]])
        img = DummyImageData(arr, assets=["a"], crs="epsg:4326", bounds=(0, 0, 1, 1), band_names=["b1", "b2"], metadata=metadata, cutline_mask=cutline_mask)
        codeflash_output = _Sum().__call__(img); result = codeflash_output # 32.7μs -> 24.7μs (32.3% faster)

    # --- Large Scale Test Cases ---
    def test_large_band_count(self):
        # Many bands, shape (500, 10, 10)
        arr = np.random.randint(0, 100, size=(500, 10, 10))
        img = DummyImageData(arr)
        codeflash_output = _Sum().__call__(img); result = codeflash_output # 50.0μs -> 43.7μs (14.4% faster)
        expected = np.sum(arr, axis=0, keepdims=True)

    def test_large_spatial_size(self):
        # Large spatial size, shape (3, 100, 100)
        arr = np.random.rand(3, 100, 100)
        img = DummyImageData(arr)
        codeflash_output = _Sum().__call__(img); result = codeflash_output # 46.1μs -> 38.4μs (20.1% faster)
        expected = np.sum(arr, axis=0, keepdims=True)

    def test_large_bands_and_spatial(self):
        # Large bands and spatial, shape (20, 50, 20)
        arr = np.random.randint(-100, 100, size=(20, 50, 20))
        img = DummyImageData(arr)
        codeflash_output = _Sum().__call__(img); result = codeflash_output # 39.1μs -> 31.0μs (26.4% faster)
        expected = np.sum(arr, axis=0, keepdims=True)

    def test_performance_reasonable(self):
        # Should run within a reasonable time for 1000x10x10
        arr = np.random.rand(1000, 10, 10)
        img = DummyImageData(arr)
        codeflash_output = _Sum().__call__(img); result = codeflash_output # 75.0μs -> 64.3μs (16.7% faster)
        expected = np.sum(arr, axis=0, keepdims=True)
# codeflash_output is used to check that the output of the original code is the same as that of the optimized code.
from types import SimpleNamespace

# function to test
import numpy
import numpy as np
# imports
import pytest  # used for our unit tests
from rio_tiler.models import ImageData
from titiler.core.algorithm.base import BaseAlgorithm
from titiler.core.algorithm.math import _Sum

# Helper function to create ImageData objects for testing
def make_imagedata(array, assets=None, crs=None, bounds=None, band_names=None, metadata=None, cutline_mask=None):
    # Use SimpleNamespace for minimal mock of ImageData if not available
    return ImageData(
        array=array,
        assets=assets if assets is not None else ["asset1"],
        crs=crs if crs is not None else "EPSG:4326",
        bounds=bounds if bounds is not None else (0, 0, 1, 1),
        band_names=band_names if band_names is not None else ["b1"] * array.shape[0],
        metadata=metadata,
        cutline_mask=cutline_mask,
    )

# Instantiate the algorithm
sum_algo = _Sum()

# 1. BASIC TEST CASES

def test_single_band_sum():
    # Single band, simple values
    arr = np.ma.array([[[1, 2], [3, 4]]])  # shape (1, 2, 2)
    img = make_imagedata(arr)
    out = sum_algo(img)

def test_two_band_sum():
    # Two bands, simple values
    arr = np.ma.array([
        [[1, 2], [3, 4]],  # band 1
        [[10, 20], [30, 40]]  # band 2
    ])  # shape (2, 2, 2)
    img = make_imagedata(arr)
    out = sum_algo(img)
    expected = np.ma.sum(arr, axis=0, keepdims=True)

def test_three_band_sum():
    # Three bands, mixed values
    arr = np.ma.array([
        [[1, 2], [3, 4]],
        [[10, 20], [30, 40]],
        [[100, 200], [300, 400]]
    ])  # shape (3, 2, 2)
    img = make_imagedata(arr)
    out = sum_algo(img)
    expected = np.ma.sum(arr, axis=0, keepdims=True)

def test_band_names_and_metadata_preserved():
    arr = np.ma.array([
        [[1, 2], [3, 4]],
        [[5, 6], [7, 8]]
    ])
    img = make_imagedata(arr, band_names=["B1", "B2"], metadata={"foo": "bar"})
    out = sum_algo(img)

# 2. EDGE TEST CASES

def test_empty_array():
    # No bands, empty array
    arr = np.ma.array([]).reshape((0, 2, 2))
    img = make_imagedata(arr, band_names=[])
    out = sum_algo(img)

def test_nan_and_masked_values():
    # Array with NaN and masked values
    arr = np.ma.array([
        [[1, 2], [3, np.nan]],
        [[np.nan, 20], [np.ma.masked, 40]]
    ])
    img = make_imagedata(arr)
    out = sum_algo(img)
    # Masked values should be ignored in sum
    expected = np.ma.sum(arr, axis=0, keepdims=True)

def test_all_masked_band():
    # All values masked
    arr = np.ma.masked_all((2, 2, 2))
    img = make_imagedata(arr)
    out = sum_algo(img)

def test_single_pixel():
    # 1x1 pixel, multiple bands
    arr = np.ma.array([
        [[5]],
        [[10]],
        [[-3]]
    ])
    img = make_imagedata(arr)
    out = sum_algo(img)
    expected = np.ma.sum(arr, axis=0, keepdims=True)

def test_single_band_single_pixel():
    arr = np.ma.array([[[42]]])
    img = make_imagedata(arr)
    out = sum_algo(img)

def test_cutline_mask_preserved():
    arr = np.ma.array([[[1, 2], [3, 4]]])
    cutline_mask = np.array([[True, False], [False, True]])
    img = make_imagedata(arr, cutline_mask=cutline_mask)
    out = sum_algo(img)

# 3. LARGE SCALE TEST CASES

def test_large_number_of_bands():
    # 1000 bands, 2x2 pixels
    arr = np.ma.array(np.arange(1000*2*2).reshape((1000, 2, 2)))
    img = make_imagedata(arr)
    out = sum_algo(img)
    expected = np.ma.sum(arr, axis=0, keepdims=True)

def test_large_image():
    # 3 bands, 1000x1 pixels
    arr = np.ma.array(np.arange(3*1000).reshape((3, 1000, 1)))
    img = make_imagedata(arr)
    out = sum_algo(img)
    expected = np.ma.sum(arr, axis=0, keepdims=True)

def test_large_masked_array():
    # 10 bands, 100x10 pixels, half masked
    arr = np.ma.array(np.random.rand(10, 100, 10))
    mask = np.random.rand(10, 100, 10) > 0.5
    arr.mask = mask
    img = make_imagedata(arr)
    out = sum_algo(img)
    expected = np.ma.sum(arr, axis=0, keepdims=True)

def test_large_all_masked_array():
    # 5 bands, 500x2 pixels, all masked
    arr = np.ma.masked_all((5, 500, 2))
    img = make_imagedata(arr)
    out = sum_algo(img)

# Additional: Check that other attributes are preserved
def test_assets_crs_bounds_preserved():
    arr = np.ma.array([[[1, 2], [3, 4]]])
    assets = ["assetA", "assetB"]
    crs = "EPSG:3857"
    bounds = (10, 20, 30, 40)
    img = make_imagedata(arr, assets=assets, crs=crs, bounds=bounds)
    out = sum_algo(img)
# 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 _Sum

To edit these changes git checkout codeflash/optimize-_Sum.__call__-mifmxm73 and push.

Codeflash Static Badge

The optimization replaces the blanket use of `numpy.ma.sum()` with conditional array type checking to avoid the overhead of masked array operations when they're unnecessary.

**Key changes:**
1. **Caches array reference** (`arr = img.array`) to avoid repeated attribute access
2. **Type-specific sum operations**: 
   - For `MaskedArray` with no actual masked values: uses `arr.data.sum()` directly on the underlying data
   - For regular `ndarray`: uses native `.sum()` method
   - Only falls back to `numpy.ma.sum()` when masking is actually present

**Why this is faster:**
- `numpy.ma.sum()` always performs mask checking and special handling even when no values are masked, adding significant overhead (79.1% of original runtime)
- Direct array `.sum()` operations bypass this overhead entirely
- The optimized version reduces sum operation time from ~2.09ms to ~0.68ms (67% reduction in sum operation time)

**Performance by test case:**
- **Best gains** (29-34% faster): Regular arrays and simple cases benefit most from bypassing masked array overhead
- **Masked arrays with actual masks** show slight slowdown (4.4%) due to added type checking, but this preserves correctness
- **Large arrays** still see 14-26% improvements, indicating the optimization scales well

The optimization is particularly effective because most real-world image data uses `MaskedArray` containers for consistency but often contains no actual masked values, making the masked array overhead pure waste.
@codeflash-ai
codeflash-ai Bot requested a review from mashraf-222 November 26, 2025 06:40
@codeflash-ai codeflash-ai Bot added ⚡️ codeflash Optimization PR opened by Codeflash AI 🎯 Quality: High Optimization Quality according to Codeflash labels Nov 26, 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