Skip to content

Conversation

@codeflash-ai
Copy link

@codeflash-ai codeflash-ai bot commented Dec 17, 2025

📄 40% (0.40x) speedup for make_norm_from_scale in lib/matplotlib/colors.py

⏱️ Runtime : 741 microseconds 528 microseconds (best of 12 runs)

📝 Explanation and details

The optimized code achieves a 40% speedup through several key performance improvements targeting function call overhead and attribute lookups:

What specific optimizations were applied:

  1. Type check optimization: Replaced isinstance(scale_cls, functools.partial) with direct type comparison type(scale_cls) is functools.partial, eliminating the slower isinstance() call overhead.

  2. Pre-allocated variables: Moved scale_args = () and scale_kwargs_items = () outside the conditional block to avoid repeated assignments in the common case.

  3. Static signature creation: For the default case (init is None), replaced dynamic function definition with a pre-constructed inspect.Signature object, eliminating function creation overhead on each call.

  4. Reduced attribute lookups: In the __call__ and inverse methods, cached self.vmin/self.vmax in local variables (vmin/vmax) to avoid repeated attribute access.

  5. Dict construction optimization: In __init__, moved dict(scale_kwargs_items) construction outside the functools.partial call to avoid creating the dictionary on every instance creation.

Why these optimizations lead to speedup:

  • Type comparisons are faster than isinstance() calls in Python
  • Local variable access is significantly faster than attribute lookups (vmin vs self.vmin)
  • Pre-constructed objects avoid repeated creation overhead
  • Eliminating function definitions removes call frame setup costs

How this impacts workloads:
Based on the function_references, this function is called from _auto_norm_from_scale in matplotlib's colormap module, which generates norm classes for color scaling. The 40% improvement will benefit any matplotlib plotting that uses automatic norm generation, particularly in tight loops or when creating many plots with different color scales.

Test case performance:
The annotated tests show consistent 35-47% improvements across all scenarios, with the optimization being particularly effective for both simple linear norms and more complex log scale transforms. Large array processing also benefits significantly, making this optimization valuable for data visualization workloads.

Correctness verification report:

Test Status
⚙️ Existing Unit Tests 4039 Passed
🌀 Generated Regression Tests 20 Passed
⏪ Replay Tests 🔘 None Found
🔎 Concolic Coverage Tests 🔘 None Found
📊 Tests Coverage 100.0%
⚙️ Existing Unit Tests and Runtime
Test File::Test Function Original ⏱️ Optimized ⏱️ Speedup
test_colors.py::test_make_norm_from_scale_name 35.1μs 24.8μs 41.7%✅
test_pickle.py::test_dynamic_norm 34.3μs 24.2μs 41.8%✅
🌀 Generated Regression Tests and Runtime
import numpy as np

# imports
import pytest  # used for our unit tests
from matplotlib.colors import make_norm_from_scale


# Minimal stub for Normalize to allow tests to run
class Normalize:
    def __init__(self, vmin=None, vmax=None, clip=False):
        self.vmin = vmin
        self.vmax = vmax
        self.clip = clip

    def process_value(self, value):
        # Mimic matplotlib's Normalize: returns array and is_scalar flag
        if np.isscalar(value):
            return np.array([value]), True
        else:
            arr = np.asarray(value)
            return arr, False

    def autoscale_None(self, A):
        # Set vmin/vmax to min/max of A if not set
        if self.vmin is None:
            self.vmin = np.min(A)
        if self.vmax is None:
            self.vmax = np.max(A)

    def scaled(self):
        # Return True if vmin/vmax are set
        return self.vmin is not None and self.vmax is not None


# Minimal stub for ScaleBase and a linear scale for testing
class ScaleBase:
    def __init__(self, axis=None):
        self.axis = axis

    def get_transform(self):
        # Return a transform object
        return self

    def transform(self, value):
        # Identity transform for linear scale
        return np.asarray(value)

    def inverted(self):
        # Return self for identity
        return self


# Minimal stub for a non-linear scale (e.g., log scale)
class LogScale(ScaleBase):
    def transform(self, value):
        # Log transform, handle zero/negatives as -inf
        arr = np.asarray(value)
        with np.errstate(divide="ignore", invalid="ignore"):
            return np.log(arr)

    def inverted(self):
        # Return an inverse transform
        class Inv:
            def transform(self, value):
                return np.exp(value)

        return Inv()


# -------------------- UNIT TESTS --------------------

# Basic Test Cases


def test_linear_norm_vmin_equals_vmax():
    # If vmin == vmax, output should be zeros
    codeflash_output = make_norm_from_scale(ScaleBase, Normalize)
    LinearNorm = codeflash_output  # 36.3μs -> 25.9μs (40.3% faster)
    norm = LinearNorm(vmin=5, vmax=5)
    result = norm([5, 5])
    expected = np.array([0, 0])


def test_linear_norm_vmin_greater_than_vmax():
    # If vmin > vmax, should raise ValueError
    codeflash_output = make_norm_from_scale(ScaleBase, Normalize)
    LinearNorm = codeflash_output  # 33.9μs -> 25.0μs (35.6% faster)
    norm = LinearNorm(vmin=10, vmax=0)
    with pytest.raises(ValueError):
        norm([5])


def test_linear_norm_invalid_vmin_vmax():
    # If vmin or vmax is not finite, should raise ValueError
    codeflash_output = make_norm_from_scale(ScaleBase, Normalize)
    LinearNorm = codeflash_output  # 34.3μs -> 24.4μs (40.5% faster)
    norm = LinearNorm(vmin=np.nan, vmax=10)
    with pytest.raises(ValueError):
        norm([5])
    norm = LinearNorm(vmin=0, vmax=np.inf)
    with pytest.raises(ValueError):
        norm([5])


def test_linear_norm_empty_array():
    # Normalizing an empty array should not crash
    codeflash_output = make_norm_from_scale(ScaleBase, Normalize)
    LinearNorm = codeflash_output  # 34.1μs -> 24.6μs (38.8% faster)
    norm = LinearNorm(vmin=0, vmax=10)
    result = norm([])


def test_linear_norm_nan_input():
    # Input containing NaN should be masked
    codeflash_output = make_norm_from_scale(ScaleBase, Normalize)
    LinearNorm = codeflash_output  # 34.6μs -> 24.6μs (40.6% faster)
    norm = LinearNorm(vmin=0, vmax=10)
    values = [0, np.nan, 10]
    result = norm(values)


def test_linear_norm_inverse_unscaled():
    # inverse() before vmin/vmax set should raise
    codeflash_output = make_norm_from_scale(ScaleBase, Normalize)
    LinearNorm = codeflash_output  # 34.3μs -> 25.2μs (36.1% faster)
    norm = LinearNorm()
    with pytest.raises(ValueError):
        norm.inverse([0.5])


def test_log_norm_basic():
    # Test with a log scale
    codeflash_output = make_norm_from_scale(LogScale, Normalize)
    LogNorm = codeflash_output  # 38.8μs -> 26.4μs (46.8% faster)
    norm = LogNorm(vmin=1, vmax=100)
    values = [1, 10, 100]
    result = norm(values)
    # log(1)=0, log(10)=2.302..., log(100)=4.605...
    # normalized = (log(x)-log(1))/(log(100)-log(1))
    expected = (np.log([1, 10, 100]) - np.log(1)) / (np.log(100) - np.log(1))


def test_log_norm_inverse():
    # Inverse for log scale
    codeflash_output = make_norm_from_scale(LogScale, Normalize)
    LogNorm = codeflash_output  # 35.9μs -> 24.8μs (44.9% faster)
    norm = LogNorm(vmin=1, vmax=100)
    normalized = norm([1, 10, 100])
    inversed = norm.inverse(normalized)
    expected = np.array([1, 10, 100])


def test_log_norm_nan_and_zero():
    # Log scale: zero and negative input should be masked
    codeflash_output = make_norm_from_scale(LogScale, Normalize)
    LogNorm = codeflash_output  # 35.0μs -> 24.8μs (41.3% faster)
    norm = LogNorm(vmin=1, vmax=100)
    values = [0, -1, 1, 10]
    result = norm(values)


def test_large_array_inverse():
    # Test inverse normalization of a large array
    codeflash_output = make_norm_from_scale(ScaleBase, Normalize)
    LinearNorm = codeflash_output  # 36.1μs -> 25.7μs (40.3% faster)
    norm = LinearNorm(vmin=0, vmax=999)
    values = np.linspace(0, 1, 1000)
    inversed = norm.inverse(values)
    expected = np.linspace(0, 999, 1000)


def test_large_array_log_norm():
    # Test log normalization of a large array
    codeflash_output = make_norm_from_scale(LogScale, Normalize)
    LogNorm = codeflash_output  # 34.7μs -> 25.0μs (38.9% faster)
    norm = LogNorm(vmin=1, vmax=1000)
    values = np.linspace(1, 1000, 1000)
    result = norm(values)
    expected = (np.log(values) - np.log(1)) / (np.log(1000) - np.log(1))


def test_large_array_nan_masking():
    # Test masking of NaNs in a large array
    codeflash_output = make_norm_from_scale(ScaleBase, Normalize)
    LinearNorm = codeflash_output  # 35.0μs -> 24.7μs (41.5% faster)
    norm = LinearNorm(vmin=0, vmax=999)
    values = np.arange(1000, dtype=float)
    values[100] = np.nan
    result = norm(values)


# 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
from matplotlib.colors import make_norm_from_scale


# Dummy Normalize base class for testing (minimal implementation)
class Normalize:
    def __init__(self, vmin=None, vmax=None, clip=False):
        self.vmin = vmin
        self.vmax = vmax
        self.clip = clip
        self._scaled = False

    def process_value(self, value):
        # Mimic matplotlib.Normalize: flatten, return is_scalar
        arr = np.array(value)
        is_scalar = arr.shape == ()
        arr = arr.reshape(-1) if not is_scalar else arr
        return arr, is_scalar

    def autoscale_None(self, A):
        # Set vmin/vmax to min/max of finite values if not set
        finite = np.array(A)[np.isfinite(A)]
        if finite.size == 0:
            return
        if self.vmin is None:
            self.vmin = np.min(finite)
        if self.vmax is None:
            self.vmax = np.max(finite)
        self._scaled = True

    def scaled(self):
        return self._scaled or (self.vmin is not None and self.vmax is not None)


# Dummy transform class for scale
class DummyTransform:
    def transform(self, value):
        # Identity transform for testing
        return np.array(value)

    def inverted(self):
        return self


# Dummy scale class for testing
class DummyScale:
    def __init__(self, axis=None, offset=0):
        self.axis = axis
        self.offset = offset

    def get_transform(self):
        # Return a dummy transform (identity)
        return DummyTransform()


# ----------- UNIT TESTS ------------

# Basic Test Cases


def test_basic_norm_inverse_array():
    # Test inverse normalization of an array
    codeflash_output = make_norm_from_scale(DummyScale, Normalize)
    Norm = codeflash_output  # 36.1μs -> 25.7μs (40.3% faster)
    norm = Norm(vmin=0, vmax=10)
    norm._scaled = True
    arr = [0.0, 0.5, 1.0]
    result = norm.inverse(arr)
    expected = np.array([0, 5, 10])


def test_edge_vmin_equals_vmax():
    # If vmin == vmax, output should be zeros
    codeflash_output = make_norm_from_scale(DummyScale, Normalize)
    Norm = codeflash_output  # 36.2μs -> 25.9μs (39.8% faster)
    norm = Norm(vmin=5, vmax=5)
    arr = [5, 5, 5]
    result = norm(arr)


def test_edge_vmin_greater_than_vmax_raises():
    # If vmin > vmax, should raise ValueError
    codeflash_output = make_norm_from_scale(DummyScale, Normalize)
    Norm = codeflash_output  # 34.5μs -> 24.8μs (39.5% faster)
    norm = Norm(vmin=10, vmax=5)
    with pytest.raises(ValueError):
        norm([7])


def test_edge_invalid_vmin_vmax_nan():
    # If vmin or vmax is NaN, should raise ValueError
    codeflash_output = make_norm_from_scale(DummyScale, Normalize)
    Norm = codeflash_output  # 34.4μs -> 24.5μs (40.4% faster)
    norm = Norm(vmin=np.nan, vmax=10)
    with pytest.raises(ValueError):
        norm([5])


def test_edge_invalid_vmin_vmax_inf():
    # If vmin or vmax is inf, should raise ValueError
    codeflash_output = make_norm_from_scale(DummyScale, Normalize)
    Norm = codeflash_output  # 34.6μs -> 24.7μs (40.0% faster)
    norm = Norm(vmin=np.inf, vmax=10)
    with pytest.raises(ValueError):
        norm([5])


def test_edge_process_value_scalar_and_array():
    # Test process_value for scalar and array types
    norm = Normalize()
    arr, is_scalar = norm.process_value(5)
    arr2, is_scalar2 = norm.process_value([1, 2, 3])


def test_large_scale_inverse():
    # Test inverse with large array
    codeflash_output = make_norm_from_scale(DummyScale, Normalize)
    Norm = codeflash_output  # 36.8μs -> 26.1μs (41.0% faster)
    norm = Norm(vmin=0, vmax=999)
    norm._scaled = True
    arr = np.linspace(0, 1, 1000)
    result = norm.inverse(arr)
    expected = arr * 999


def test_large_scale_nan_inf_handling():
    # Test normalization with large array containing nan/inf
    codeflash_output = make_norm_from_scale(DummyScale, Normalize)
    Norm = codeflash_output  # 36.0μs -> 25.8μs (39.3% faster)
    norm = Norm(vmin=0, vmax=999)
    arr = np.arange(1000, dtype=float)
    arr[100] = np.nan
    arr[200] = np.inf
    arr[300] = -np.inf
    result = norm(arr)


# codeflash_output is used to check that the output of the original code is the same as that of the optimized code.

To edit these changes git checkout codeflash/optimize-make_norm_from_scale-mja3y9hd and push.

Codeflash Static Badge

The optimized code achieves a **40% speedup** through several key performance improvements targeting function call overhead and attribute lookups:

**What specific optimizations were applied:**

1. **Type check optimization**: Replaced `isinstance(scale_cls, functools.partial)` with direct type comparison `type(scale_cls) is functools.partial`, eliminating the slower isinstance() call overhead.

2. **Pre-allocated variables**: Moved `scale_args = ()` and `scale_kwargs_items = ()` outside the conditional block to avoid repeated assignments in the common case.

3. **Static signature creation**: For the default case (`init is None`), replaced dynamic function definition with a pre-constructed `inspect.Signature` object, eliminating function creation overhead on each call.

4. **Reduced attribute lookups**: In the `__call__` and `inverse` methods, cached `self.vmin`/`self.vmax` in local variables (`vmin`/`vmax`) to avoid repeated attribute access.

5. **Dict construction optimization**: In `__init__`, moved `dict(scale_kwargs_items)` construction outside the functools.partial call to avoid creating the dictionary on every instance creation.

**Why these optimizations lead to speedup:**
- Type comparisons are faster than isinstance() calls in Python
- Local variable access is significantly faster than attribute lookups (`vmin` vs `self.vmin`)
- Pre-constructed objects avoid repeated creation overhead
- Eliminating function definitions removes call frame setup costs

**How this impacts workloads:**
Based on the function_references, this function is called from `_auto_norm_from_scale` in matplotlib's colormap module, which generates norm classes for color scaling. The 40% improvement will benefit any matplotlib plotting that uses automatic norm generation, particularly in tight loops or when creating many plots with different color scales.

**Test case performance:**
The annotated tests show consistent 35-47% improvements across all scenarios, with the optimization being particularly effective for both simple linear norms and more complex log scale transforms. Large array processing also benefits significantly, making this optimization valuable for data visualization workloads.
@codeflash-ai codeflash-ai bot requested a review from mashraf-222 December 17, 2025 14:29
@codeflash-ai codeflash-ai bot added ⚡️ codeflash Optimization PR opened by Codeflash AI 🎯 Quality: Medium Optimization Quality according to Codeflash labels Dec 17, 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: Medium Optimization Quality according to Codeflash

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant