Skip to content

⚡️ Speed up method GlobalMercator.LatLonToMeters by 26%#12

Open
codeflash-ai[bot] wants to merge 1 commit intomasterfrom
codeflash/optimize-GlobalMercator.LatLonToMeters-mh4i83mu
Open

⚡️ Speed up method GlobalMercator.LatLonToMeters by 26%#12
codeflash-ai[bot] wants to merge 1 commit intomasterfrom
codeflash/optimize-GlobalMercator.LatLonToMeters-mh4i83mu

Conversation

@codeflash-ai
Copy link
Copy Markdown

@codeflash-ai codeflash-ai Bot commented Oct 24, 2025

📄 26% (0.26x) speedup for GlobalMercator.LatLonToMeters in opendm/tiles/gdal2tiles.py

⏱️ Runtime : 1.76 milliseconds 1.40 milliseconds (best of 119 runs)

📝 Explanation and details

The optimization achieves a 25% speedup by eliminating expensive attribute lookups and redundant mathematical operations in the LatLonToMeters method.

Key optimizations applied:

  1. Module-level constants: Pre-computed _PI, _ORIGIN_SHIFT, _DEG2RAD, _RAD2DEG, _INV_180, _LOG, and _TAN eliminate repeated lookups to the math module on every function call.

  2. Arithmetic consolidation: The original lon * self.originShift / 180.0 becomes lon * _ORIGIN_SHIFT * _INV_180, replacing division with multiplication (faster) and removing the self. attribute lookup.

  3. Expression decomposition: The complex nested expression math.log(math.tan((90 + lat) * math.pi / 360.0)) / (math.pi / 180.0) is broken into intermediate variables, allowing the compiler to optimize better and reducing function call overhead.

  4. Constant folding: Pre-computing _DEG2RAD * 0.5 and using _INV_180 avoids redundant calculations per call.

Why this works: Python's attribute lookup mechanism (self.originShift, math.log) involves dictionary searches that are expensive when called thousands of times. Moving these to module-level constants provides direct memory access. The test results show consistent 15-40% improvements across various coordinate ranges, with particularly strong gains for edge cases like polar regions (37.4% faster for north pole limit) where the mathematical computations are most intensive.

This optimization is especially effective for bulk coordinate transformation scenarios, as evidenced by the 27% improvement in the bulk random coordinate test cases.

Correctness verification report:

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

# imports
import pytest
from opendm.tiles.gdal2tiles import GlobalMercator

# unit tests

# Helper: Known values for Spherical Mercator (EPSG:3857)
EARTH_RADIUS = 6378137.0
ORIGIN_SHIFT = 2 * math.pi * EARTH_RADIUS / 2.0

def almost_equal(a, b, tol=1e-7):
    """Helper for approximate float comparison."""
    return abs(a - b) <= tol

@pytest.fixture
def merc():
    """Fixture for GlobalMercator instance."""
    return GlobalMercator()

#############################
# 1. Basic Test Cases
#############################

def test_origin(merc):
    # At (lat, lon) = (0, 0), meters should be (0, 0)
    mx, my = merc.LatLonToMeters(0, 0) # 1.72μs -> 1.45μs (19.3% faster)

def test_equator_prime_meridian(merc):
    # At (lat, lon) = (0, 0), meters should be (0, 0)
    mx, my = merc.LatLonToMeters(0, 0) # 1.69μs -> 1.43μs (18.2% faster)

def test_equator_east(merc):
    # At (lat, lon) = (0, 180), meters should be (ORIGIN_SHIFT, 0)
    mx, my = merc.LatLonToMeters(0, 180) # 1.72μs -> 1.39μs (23.6% faster)

def test_equator_west(merc):
    # At (lat, lon) = (0, -180), meters should be (-ORIGIN_SHIFT, 0)
    mx, my = merc.LatLonToMeters(0, -180) # 1.69μs -> 1.45μs (16.6% faster)

def test_north_pole_limit(merc):
    # At (lat, lon) = (85.05112878, 0), y should be close to ORIGIN_SHIFT
    mx, my = merc.LatLonToMeters(85.05112878, 0) # 1.97μs -> 1.44μs (37.4% faster)

def test_south_pole_limit(merc):
    # At (lat, lon) = (-85.05112878, 0), y should be close to -ORIGIN_SHIFT
    mx, my = merc.LatLonToMeters(-85.05112878, 0) # 1.75μs -> 1.35μs (29.5% faster)

def test_known_city_london(merc):
    # London: (lat, lon) = (51.5074, -0.1278)
    mx, my = merc.LatLonToMeters(51.5074, -0.1278) # 1.80μs -> 1.30μs (38.2% faster)

def test_known_city_sydney(merc):
    # Sydney: (lat, lon) = (-33.8688, 151.2093)
    mx, my = merc.LatLonToMeters(-33.8688, 151.2093) # 1.68μs -> 1.27μs (32.1% faster)

def test_known_city_newyork(merc):
    # New York: (lat, lon) = (40.7128, -74.0060)
    mx, my = merc.LatLonToMeters(40.7128, -74.0060) # 1.68μs -> 1.32μs (27.1% faster)

#############################
# 2. Edge Test Cases
#############################

def test_latitude_upper_bound(merc):
    # Maximum latitude supported: 85.05112878
    mx, my = merc.LatLonToMeters(85.05112878, 0) # 1.82μs -> 1.38μs (31.9% faster)

def test_latitude_lower_bound(merc):
    # Minimum latitude supported: -85.05112878
    mx, my = merc.LatLonToMeters(-85.05112878, 0) # 1.68μs -> 1.32μs (27.8% faster)

def test_latitude_beyond_upper_bound(merc):
    # Latitude > 85.05112878 is mathematically valid but not in Mercator; should not error, but returns large values
    mx, my = merc.LatLonToMeters(89.0, 0) # 1.76μs -> 1.28μs (38.2% faster)

def test_latitude_beyond_lower_bound(merc):
    # Latitude < -85.05112878
    mx, my = merc.LatLonToMeters(-89.0, 0) # 1.63μs -> 1.36μs (19.7% faster)

def test_longitude_beyond_bounds(merc):
    # Longitude > 180
    mx, my = merc.LatLonToMeters(0, 200) # 1.74μs -> 1.46μs (19.2% faster)

    # Longitude < -180
    mx, my = merc.LatLonToMeters(0, -200) # 733ns -> 796ns (7.91% slower)

def test_latitude_zero_longitude_edges(merc):
    # At (lat, lon) = (0, 180) and (0, -180)
    mx1, my1 = merc.LatLonToMeters(0, 180) # 1.54μs -> 1.26μs (22.3% faster)
    mx2, my2 = merc.LatLonToMeters(0, -180) # 666ns -> 768ns (13.3% slower)

def test_negative_zero_longitude(merc):
    # Test that -0.0 longitude is handled the same as 0.0
    mx1, my1 = merc.LatLonToMeters(0, 0.0) # 1.59μs -> 1.20μs (32.7% faster)
    mx2, my2 = merc.LatLonToMeters(0, -0.0) # 665ns -> 675ns (1.48% slower)


def test_latitude_type_error(merc):
    # Test string input raises TypeError
    with pytest.raises(TypeError):
        merc.LatLonToMeters("not_a_number", 0) # 2.34μs -> 2.06μs (13.7% faster)
    with pytest.raises(TypeError):
        merc.LatLonToMeters(0, "not_a_number") # 1.00μs -> 978ns (2.45% faster)

def test_latitude_none(merc):
    # Test None input raises TypeError
    with pytest.raises(TypeError):
        merc.LatLonToMeters(None, 0) # 1.78μs -> 1.63μs (9.38% faster)
    with pytest.raises(TypeError):
        merc.LatLonToMeters(0, None) # 1.01μs -> 928ns (8.41% faster)

#############################
# 3. Large Scale Test Cases
#############################

def test_bulk_latlon_to_meters_grid(merc):
    # Test a grid of points across the world
    step = 10
    lats = list(range(-80, 81, step))  # avoid pole singularity
    lons = list(range(-180, 181, step))
    for lat in lats:
        for lon in lons:
            mx, my = merc.LatLonToMeters(lat, lon)
            # mx should be proportional to longitude
            expected_mx = lon * ORIGIN_SHIFT / 180.0

def test_bulk_latlon_to_meters_random(merc):
    # Test 1000 random points within valid latitude/longitude
    import random
    random.seed(42)
    for _ in range(1000):
        lat = random.uniform(-85, 85)
        lon = random.uniform(-180, 180)
        mx, my = merc.LatLonToMeters(lat, lon) # 423μs -> 331μs (27.6% faster)

def test_bulk_latlon_to_meters_performance(merc):
    # Performance: process 1000 calls in reasonable time
    import time
    lats = [i * 0.17 for i in range(-500, 500)]
    lons = [i * 0.36 for i in range(-500, 500)]
    start = time.time()
    for lat, lon in zip(lats, lons):
        mx, my = merc.LatLonToMeters(lat, lon) # 410μs -> 322μs (27.1% faster)
    elapsed = time.time() - start

#############################
# 4. Mutation Testing Guards
#############################

def test_mutation_guard_latlon_order(merc):
    # Ensure argument order is (lat, lon), not (lon, lat)
    mx1, my1 = merc.LatLonToMeters(10, 20) # 2.12μs -> 1.93μs (10.1% faster)
    mx2, my2 = merc.LatLonToMeters(20, 10) # 709ns -> 606ns (17.0% faster)

def test_mutation_guard_signs(merc):
    # Changing the sign of lat/lon should change the output sign
    mx1, my1 = merc.LatLonToMeters(10, 20) # 1.39μs -> 1.25μs (11.2% faster)
    mx2, my2 = merc.LatLonToMeters(-10, -20) # 738ns -> 719ns (2.64% faster)
# codeflash_output is used to check that the output of the original code is the same as that of the optimized code.
#------------------------------------------------
import math

# imports
import pytest  # used for our unit tests
from opendm.tiles.gdal2tiles import GlobalMercator

# unit tests

# Helper function for approximate equality with tolerance for floating point
def approx_equal(a, b, tol=1e-7):
    return abs(a - b) <= tol

# Basic Test Cases
def test_latlon_to_meters_equator_prime_meridian():
    # Test at the intersection of the equator and prime meridian (0, 0)
    merc = GlobalMercator()
    mx, my = merc.LatLonToMeters(0, 0) # 1.46μs -> 1.31μs (11.4% faster)

def test_latlon_to_meters_equator_180E():
    # Test at the equator, 180 degrees east
    merc = GlobalMercator()
    mx, my = merc.LatLonToMeters(0, 180) # 1.39μs -> 1.31μs (5.80% faster)

def test_latlon_to_meters_equator_180W():
    # Test at the equator, 180 degrees west
    merc = GlobalMercator()
    mx, my = merc.LatLonToMeters(0, -180) # 1.42μs -> 1.25μs (13.2% faster)

def test_latlon_to_meters_45N_45E():
    # Test at 45N, 45E
    merc = GlobalMercator()
    mx, my = merc.LatLonToMeters(45, 45) # 1.45μs -> 1.23μs (18.0% faster)
    # Check expected values from authoritative sources or reverse calculation
    expected_mx = 45 * merc.originShift / 180.0
    expected_my = math.log(math.tan((90 + 45) * math.pi / 360.0)) / (math.pi / 180.0)
    expected_my = expected_my * merc.originShift / 180.0

def test_latlon_to_meters_45S_45W():
    # Test at 45S, 45W
    merc = GlobalMercator()
    mx, my = merc.LatLonToMeters(-45, -45) # 1.46μs -> 1.27μs (14.5% faster)
    expected_mx = -45 * merc.originShift / 180.0
    expected_my = math.log(math.tan((90 - 45) * math.pi / 360.0)) / (math.pi / 180.0)
    expected_my = expected_my * merc.originShift / 180.0

# Edge Test Cases
def test_latlon_to_meters_north_pole_limit():
    # Latitude limit for Mercator is 85.05112878
    merc = GlobalMercator()
    lat = 85.05112878
    lon = 0
    mx, my = merc.LatLonToMeters(lat, lon) # 1.62μs -> 1.30μs (25.0% faster)

def test_latlon_to_meters_south_pole_limit():
    # Latitude limit for Mercator is -85.05112878
    merc = GlobalMercator()
    lat = -85.05112878
    lon = 0
    mx, my = merc.LatLonToMeters(lat, lon) # 1.58μs -> 1.23μs (28.4% faster)

def test_latlon_to_meters_max_longitude():
    # Test longitude just over the edge
    merc = GlobalMercator()
    lat = 0
    lon = 180.0000001
    mx, my = merc.LatLonToMeters(lat, lon) # 1.48μs -> 1.24μs (19.3% faster)

def test_latlon_to_meters_min_longitude():
    # Test longitude just under the edge
    merc = GlobalMercator()
    lat = 0
    lon = -180.0000001
    mx, my = merc.LatLonToMeters(lat, lon) # 1.51μs -> 1.38μs (9.26% faster)

def test_latlon_to_meters_latitude_out_of_bounds():
    # Latitude beyond Mercator bounds should produce a result (not error)
    merc = GlobalMercator()
    lat = 90
    lon = 0
    mx, my = merc.LatLonToMeters(lat, lon) # 1.55μs -> 1.42μs (9.22% faster)


def test_latlon_to_meters_zero_longitude_various_latitudes():
    # Test several latitudes at longitude 0
    merc = GlobalMercator()
    for lat in [-85.05112878, -45, 0, 45, 85.05112878]:
        mx, my = merc.LatLonToMeters(lat, 0) # 5.40μs -> 4.37μs (23.6% faster)

def test_latlon_to_meters_zero_latitude_various_longitudes():
    # Test several longitudes at latitude 0
    merc = GlobalMercator()
    for lon in [-180, -90, 0, 90, 180]:
        mx, my = merc.LatLonToMeters(0, lon) # 3.47μs -> 3.10μs (12.0% faster)
        expected_mx = lon * merc.originShift / 180.0

def test_latlon_to_meters_type_errors():
    # Should raise TypeError if non-numeric input is given
    merc = GlobalMercator()
    with pytest.raises(TypeError):
        merc.LatLonToMeters("a", 0) # 1.93μs -> 1.82μs (6.39% faster)
    with pytest.raises(TypeError):
        merc.LatLonToMeters(0, "b") # 1.05μs -> 965ns (9.33% faster)
    with pytest.raises(TypeError):
        merc.LatLonToMeters(None, 0) # 931ns -> 838ns (11.1% faster)
    with pytest.raises(TypeError):
        merc.LatLonToMeters(0, None) # 799ns -> 768ns (4.04% faster)

# Large Scale Test Cases
def test_latlon_to_meters_large_grid():
    # Test a grid of lat/lon values for performance and correctness
    merc = GlobalMercator()
    lats = [i for i in range(-85, 86, 10)]  # from -85 to 85 in steps of 10
    lons = [i for i in range(-180, 181, 20)]  # from -180 to 180 in steps of 20
    for lat in lats:
        for lon in lons:
            mx, my = merc.LatLonToMeters(lat, lon)


def test_latlon_to_meters_performance_large_list():
    # Performance test: process 1000 points
    merc = GlobalMercator()
    points = [(lat, lon) for lat in range(-85, 86, 1) for lon in range(-180, 181, 4)]
    points = points[:1000]  # limit to 1000 points
    for lat, lon in points:
        mx, my = merc.LatLonToMeters(lat, lon) # 421μs -> 334μs (26.1% faster)


def test_latlon_to_meters_symmetry():
    # For longitude symmetry: (lat, lon) and (lat, -lon) should have mx symmetric
    merc = GlobalMercator()
    for lat in [-85, 0, 85]:
        mx1, my1 = merc.LatLonToMeters(lat, 45) # 3.79μs -> 3.27μs (15.8% faster)
        mx2, my2 = merc.LatLonToMeters(lat, -45)

# Additional edge: test for monotonicity
def test_latlon_to_meters_monotonicity():
    # Increasing latitude should increase my
    merc = GlobalMercator()
    prev_my = None
    for lat in range(-85, 86, 5):
        mx, my = merc.LatLonToMeters(lat, 0) # 16.5μs -> 13.6μs (21.2% faster)
        if prev_my is not None:
            pass
        prev_my = my
# 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-GlobalMercator.LatLonToMeters-mh4i83mu and push.

Codeflash

The optimization achieves a 25% speedup by eliminating expensive attribute lookups and redundant mathematical operations in the `LatLonToMeters` method.

**Key optimizations applied:**

1. **Module-level constants:** Pre-computed `_PI`, `_ORIGIN_SHIFT`, `_DEG2RAD`, `_RAD2DEG`, `_INV_180`, `_LOG`, and `_TAN` eliminate repeated lookups to the `math` module on every function call.

2. **Arithmetic consolidation:** The original `lon * self.originShift / 180.0` becomes `lon * _ORIGIN_SHIFT * _INV_180`, replacing division with multiplication (faster) and removing the `self.` attribute lookup.

3. **Expression decomposition:** The complex nested expression `math.log(math.tan((90 + lat) * math.pi / 360.0)) / (math.pi / 180.0)` is broken into intermediate variables, allowing the compiler to optimize better and reducing function call overhead.

4. **Constant folding:** Pre-computing `_DEG2RAD * 0.5` and using `_INV_180` avoids redundant calculations per call.

**Why this works:** Python's attribute lookup mechanism (`self.originShift`, `math.log`) involves dictionary searches that are expensive when called thousands of times. Moving these to module-level constants provides direct memory access. The test results show consistent 15-40% improvements across various coordinate ranges, with particularly strong gains for edge cases like polar regions (37.4% faster for north pole limit) where the mathematical computations are most intensive.

This optimization is especially effective for bulk coordinate transformation scenarios, as evidenced by the 27% improvement in the bulk random coordinate test cases.
@codeflash-ai codeflash-ai Bot requested a review from mashraf-222 October 24, 2025 07:03
@codeflash-ai codeflash-ai Bot added ⚡️ codeflash Optimization PR opened by Codeflash AI 🎯 Quality: High Optimization Quality according to Codeflash labels Oct 24, 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