FAILURE MAP
← Case archive

FA-70061 / Map projection transforms / Open access

Equirectangular projection with standard parallel: inverse longitude wrap · case 01

Inverse results near the antimeridian report longitudes beyond 180.

Verified by executionVariant 1 · 8 checks per implementationDownload source bundle ↓JSON ↗

ROOT CAUSE

After adding lon0 the inverse longitude is never wrapped back into [-180, 180).

VERIFIED REPAIR

At the inverse longitude wrap step restore `lon = ((lon + 180.0) % 360.0) - 180.0`, leaving the rest of the model unchanged.

Unsuccessful approach: Wrapping modulo 360 yields the range [0, 360), so western longitudes come back positive.

Case contract

Input [mode, a, b, lon0, lat0, lat1] on a sphere R = 6371007. mode "fwd": a=lon, b=lat; dlon = lon-lon0 wrapped into [-180, 180); x = R*rad(dlon)*cos(rad(lat1)), y = R*rad(lat-lat0). mode "inv": a=x, b=y; lon = lon0 + deg(x/(R*cos(rad(lat1)))) wrapped into [-180, 180), lat = lat0 + deg(y/R). Forward results round to 2 decimals, inverse results to 6.

Why this case matters

Plate carree style grids with a true-scale parallel are common for regional rasters and simple web previews.

1 / The failure

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(x):
    mode, a, b, lon0, lat0, lat1 = x
    R = 6371007.0
    k = math.cos(math.radians(lat1))
    if mode == 'fwd':
        dlon = ((a - lon0 + 180.0) % 360.0) - 180.0
        return [round(R * math.radians(dlon) * k, 2), round(R * math.radians(b - lat0), 2)]
    lon = lon0 + math.degrees(a / (R * k))
    lon = lon
    lat = lat0 + math.degrees(b / R)
    return [round(lon, 6), round(lat, 6)]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('control #0', ['fwd', 29.215, -48.839, -100.0, 20.0, 0.0], [14368068.23, -7654555.97]), ('control #1', ['fwd', 58.849, -45.676, 10.0, 20.0, -37.5], [4309310.45, -7302846.03]), ('control #2', ['fwd', 69.272, -46.02, 150.0, 0.0, 45.0], [-6347382.14, -5117196.15]), ('control #3', ['fwd', -110.417, -65.782, -100.0, 0.0, -37.5], [-918956.11, -7314632.7]), ('control #4', ['fwd', 103.06, -41.789, 150.0, 20.0, 30.0], [-4520215.78, -6870630.87]), ('control #5', ['fwd', -157.356, -5.165, 10.0, 0.0, 45.0], [-13158662.23, -574322.43]), ('control #10', ['inv', -9574741.6, -6948175.3, 10.0, 20.0, 45.0], [-111.774572, -42.486373]), ('control #11', ['inv', 14726764.0, 5249990.3, 150.0, 0.0, 30.0], [-57.070508, 47.214245])], [('control #3', ['fwd', -110.417, -65.782, -100.0, 0.0, -37.5], [-918956.11, -7314632.7]), ('control #4', ['fwd', 103.06, -41.789, 150.0, 20.0, 30.0], [-4520215.78, -6870630.87]), ('control #5', ['fwd', -157.356, -5.165, 10.0, 0.0, 45.0], [-13158662.23, -574322.43]), ('control #6', ['fwd', 59.325, 61.191, 150.0, 20.0, 30.0], [-8731797.31, 4580235.26]), ('control #7', ['fwd', -27.732, -15.271, 150.0, 0.0, -37.5], [-15678977.34, -1698059.59]), ('control #8', ['fwd', -101.988, -64.848, 0.0, 20.0, 45.0], [-8018987.33, -9434677.5]), ('control #11', ['inv', 14726764.0, 5249990.3, 150.0, 0.0, 30.0], [-57.070508, 47.214245]), ('control #15', ['inv', 6171735.4, 4391661.0, 150.0, -10.0, 0.0], [-154.496311, 29.495113])], [('control #6', ['fwd', 59.325, 61.191, 150.0, 20.0, 30.0], [-8731797.31, 4580235.26]), ('control #7', ['fwd', -27.732, -15.271, 150.0, 0.0, -37.5], [-15678977.34, -1698059.59]), ('control #8', ['fwd', -101.988, -64.848, 0.0, 20.0, 45.0], [-8018987.33, -9434677.5]), ('control #9', ['fwd', -176.6, 10.306, 10.0, 0.0, -37.5], [15296821.45, 1145976.17]), ('control #10', ['inv', -9574741.6, -6948175.3, 10.0, 20.0, 45.0], [-111.774572, -42.486373]), ('control #11', ['inv', 14726764.0, 5249990.3, 150.0, 0.0, 30.0], [-57.070508, 47.214245]), ('control #15', ['inv', 6171735.4, 4391661.0, 150.0, -10.0, 0.0], [-154.496311, 29.495113]), ('regression #23', ['inv', 3000000.0, 100000.0, 170.0, 5.0, 40.0], [-154.780609, 5.899321])], [('control #9', ['fwd', -176.6, 10.306, 10.0, 0.0, -37.5], [15296821.45, 1145976.17]), ('control #10', ['inv', -9574741.6, -6948175.3, 10.0, 20.0, 45.0], [-111.774572, -42.486373]), ('control #11', ['inv', 14726764.0, 5249990.3, 150.0, 0.0, 30.0], [-57.070508, 47.214245]), ('control #12', ['inv', 10315651.1, 166980.2, 10.0, -10.0, 45.0], [141.197691, -8.498313]), ('control #13', ['inv', 5503696.2, -7904836.4, 0.0, 0.0, 0.0], [49.495875, -71.089824]), ('control #14', ['inv', 2270255.8, 547031.0, 150.0, 0.0, 45.0], [178.873826, 4.919563]), ('control #17', ['inv', -1605712.3, 6233155.6, 10.0, 0.0, 0.0], [-4.440502, 56.056053]), ('regression #24', ['inv', -3000000.0, 0.0, -170.0, 0.0, 0.0], [163.020381, 0.0])], [('control #10', ['inv', -9574741.6, -6948175.3, 10.0, 20.0, 45.0], [-111.774572, -42.486373]), ('control #11', ['inv', 14726764.0, 5249990.3, 150.0, 0.0, 30.0], [-57.070508, 47.214245]), ('control #14', ['inv', 2270255.8, 547031.0, 150.0, 0.0, 45.0], [178.873826, 4.919563]), ('control #15', ['inv', 6171735.4, 4391661.0, 150.0, -10.0, 0.0], [-154.496311, 29.495113]), ('control #16', ['inv', 5573045.0, 6790547.5, 10.0, -10.0, 45.0], [80.879737, 51.068794]), ('control #17', ['inv', -1605712.3, 6233155.6, 10.0, 0.0, 0.0], [-4.440502, 56.056053]), ('control #18', ['inv', 11534249.9, -2599094.3, 0.0, 0.0, 45.0], [146.696214, -23.374191]), ('control #19', ['inv', 1510017.4, 7976956.2, 150.0, 0.0, 0.0], [163.579898, 71.738412])]]
for label, args, expected in fixtures[N-1]:
    check(label, solve(args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
Boundary fixtureActualExpectedOutcome
control #0[14368068.23, -7654555.97][14368068.23, -7654555.97]Passed
control #1[4309310.45, -7302846.03][4309310.45, -7302846.03]Passed
control #2[-6347382.14, -5117196.15][-6347382.14, -5117196.15]Passed
control #3[-918956.11, -7314632.7][-918956.11, -7314632.7]Passed
control #4[-4520215.78, -6870630.87][-4520215.78, -6870630.87]Passed
control #5[-13158662.23, -574322.43][-13158662.23, -574322.43]Passed
control #10[-111.774572, -42.486373][-111.774572, -42.486373]Passed
control #11[302.929492, 47.214245][-57.070508, 47.214245]Failed

SHA-256 / 300d8220229b02ff741fc6ab717992b66a70eaef5d9dea6d5e220012b8112916

2 / The unsuccessful fix

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(x):
    mode, a, b, lon0, lat0, lat1 = x
    R = 6371007.0
    k = math.cos(math.radians(lat1))
    if mode == 'fwd':
        dlon = ((a - lon0 + 180.0) % 360.0) - 180.0
        return [round(R * math.radians(dlon) * k, 2), round(R * math.radians(b - lat0), 2)]
    lon = lon0 + math.degrees(a / (R * k))
    lon = lon % 360.0
    lat = lat0 + math.degrees(b / R)
    return [round(lon, 6), round(lat, 6)]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('control #0', ['fwd', 29.215, -48.839, -100.0, 20.0, 0.0], [14368068.23, -7654555.97]), ('control #1', ['fwd', 58.849, -45.676, 10.0, 20.0, -37.5], [4309310.45, -7302846.03]), ('control #2', ['fwd', 69.272, -46.02, 150.0, 0.0, 45.0], [-6347382.14, -5117196.15]), ('control #3', ['fwd', -110.417, -65.782, -100.0, 0.0, -37.5], [-918956.11, -7314632.7]), ('control #4', ['fwd', 103.06, -41.789, 150.0, 20.0, 30.0], [-4520215.78, -6870630.87]), ('control #5', ['fwd', -157.356, -5.165, 10.0, 0.0, 45.0], [-13158662.23, -574322.43]), ('control #10', ['inv', -9574741.6, -6948175.3, 10.0, 20.0, 45.0], [-111.774572, -42.486373]), ('control #11', ['inv', 14726764.0, 5249990.3, 150.0, 0.0, 30.0], [-57.070508, 47.214245])], [('control #3', ['fwd', -110.417, -65.782, -100.0, 0.0, -37.5], [-918956.11, -7314632.7]), ('control #4', ['fwd', 103.06, -41.789, 150.0, 20.0, 30.0], [-4520215.78, -6870630.87]), ('control #5', ['fwd', -157.356, -5.165, 10.0, 0.0, 45.0], [-13158662.23, -574322.43]), ('control #6', ['fwd', 59.325, 61.191, 150.0, 20.0, 30.0], [-8731797.31, 4580235.26]), ('control #7', ['fwd', -27.732, -15.271, 150.0, 0.0, -37.5], [-15678977.34, -1698059.59]), ('control #8', ['fwd', -101.988, -64.848, 0.0, 20.0, 45.0], [-8018987.33, -9434677.5]), ('control #11', ['inv', 14726764.0, 5249990.3, 150.0, 0.0, 30.0], [-57.070508, 47.214245]), ('control #15', ['inv', 6171735.4, 4391661.0, 150.0, -10.0, 0.0], [-154.496311, 29.495113])], [('control #6', ['fwd', 59.325, 61.191, 150.0, 20.0, 30.0], [-8731797.31, 4580235.26]), ('control #7', ['fwd', -27.732, -15.271, 150.0, 0.0, -37.5], [-15678977.34, -1698059.59]), ('control #8', ['fwd', -101.988, -64.848, 0.0, 20.0, 45.0], [-8018987.33, -9434677.5]), ('control #9', ['fwd', -176.6, 10.306, 10.0, 0.0, -37.5], [15296821.45, 1145976.17]), ('control #10', ['inv', -9574741.6, -6948175.3, 10.0, 20.0, 45.0], [-111.774572, -42.486373]), ('control #11', ['inv', 14726764.0, 5249990.3, 150.0, 0.0, 30.0], [-57.070508, 47.214245]), ('control #15', ['inv', 6171735.4, 4391661.0, 150.0, -10.0, 0.0], [-154.496311, 29.495113]), ('regression #23', ['inv', 3000000.0, 100000.0, 170.0, 5.0, 40.0], [-154.780609, 5.899321])], [('control #9', ['fwd', -176.6, 10.306, 10.0, 0.0, -37.5], [15296821.45, 1145976.17]), ('control #10', ['inv', -9574741.6, -6948175.3, 10.0, 20.0, 45.0], [-111.774572, -42.486373]), ('control #11', ['inv', 14726764.0, 5249990.3, 150.0, 0.0, 30.0], [-57.070508, 47.214245]), ('control #12', ['inv', 10315651.1, 166980.2, 10.0, -10.0, 45.0], [141.197691, -8.498313]), ('control #13', ['inv', 5503696.2, -7904836.4, 0.0, 0.0, 0.0], [49.495875, -71.089824]), ('control #14', ['inv', 2270255.8, 547031.0, 150.0, 0.0, 45.0], [178.873826, 4.919563]), ('control #17', ['inv', -1605712.3, 6233155.6, 10.0, 0.0, 0.0], [-4.440502, 56.056053]), ('regression #24', ['inv', -3000000.0, 0.0, -170.0, 0.0, 0.0], [163.020381, 0.0])], [('control #10', ['inv', -9574741.6, -6948175.3, 10.0, 20.0, 45.0], [-111.774572, -42.486373]), ('control #11', ['inv', 14726764.0, 5249990.3, 150.0, 0.0, 30.0], [-57.070508, 47.214245]), ('control #14', ['inv', 2270255.8, 547031.0, 150.0, 0.0, 45.0], [178.873826, 4.919563]), ('control #15', ['inv', 6171735.4, 4391661.0, 150.0, -10.0, 0.0], [-154.496311, 29.495113]), ('control #16', ['inv', 5573045.0, 6790547.5, 10.0, -10.0, 45.0], [80.879737, 51.068794]), ('control #17', ['inv', -1605712.3, 6233155.6, 10.0, 0.0, 0.0], [-4.440502, 56.056053]), ('control #18', ['inv', 11534249.9, -2599094.3, 0.0, 0.0, 45.0], [146.696214, -23.374191]), ('control #19', ['inv', 1510017.4, 7976956.2, 150.0, 0.0, 0.0], [163.579898, 71.738412])]]
for label, args, expected in fixtures[N-1]:
    check(label, solve(args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
Boundary fixtureActualExpectedOutcome
control #0[14368068.23, -7654555.97][14368068.23, -7654555.97]Passed
control #1[4309310.45, -7302846.03][4309310.45, -7302846.03]Passed
control #2[-6347382.14, -5117196.15][-6347382.14, -5117196.15]Passed
control #3[-918956.11, -7314632.7][-918956.11, -7314632.7]Passed
control #4[-4520215.78, -6870630.87][-4520215.78, -6870630.87]Passed
control #5[-13158662.23, -574322.43][-13158662.23, -574322.43]Passed
control #10[248.225428, -42.486373][-111.774572, -42.486373]Failed
control #11[302.929492, 47.214245][-57.070508, 47.214245]Failed

SHA-256 / 817d34e279561befbc0a4b86479ed364d2acc3e960c3479727da3acf9284673f

3 / The verified repair

Exit 0
"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(x):
    mode, a, b, lon0, lat0, lat1 = x
    R = 6371007.0
    k = math.cos(math.radians(lat1))
    if mode == 'fwd':
        dlon = ((a - lon0 + 180.0) % 360.0) - 180.0
        return [round(R * math.radians(dlon) * k, 2), round(R * math.radians(b - lat0), 2)]
    lon = lon0 + math.degrees(a / (R * k))
    lon = ((lon + 180.0) % 360.0) - 180.0
    lat = lat0 + math.degrees(b / R)
    return [round(lon, 6), round(lat, 6)]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('control #0', ['fwd', 29.215, -48.839, -100.0, 20.0, 0.0], [14368068.23, -7654555.97]), ('control #1', ['fwd', 58.849, -45.676, 10.0, 20.0, -37.5], [4309310.45, -7302846.03]), ('control #2', ['fwd', 69.272, -46.02, 150.0, 0.0, 45.0], [-6347382.14, -5117196.15]), ('control #3', ['fwd', -110.417, -65.782, -100.0, 0.0, -37.5], [-918956.11, -7314632.7]), ('control #4', ['fwd', 103.06, -41.789, 150.0, 20.0, 30.0], [-4520215.78, -6870630.87]), ('control #5', ['fwd', -157.356, -5.165, 10.0, 0.0, 45.0], [-13158662.23, -574322.43]), ('control #10', ['inv', -9574741.6, -6948175.3, 10.0, 20.0, 45.0], [-111.774572, -42.486373]), ('control #11', ['inv', 14726764.0, 5249990.3, 150.0, 0.0, 30.0], [-57.070508, 47.214245])], [('control #3', ['fwd', -110.417, -65.782, -100.0, 0.0, -37.5], [-918956.11, -7314632.7]), ('control #4', ['fwd', 103.06, -41.789, 150.0, 20.0, 30.0], [-4520215.78, -6870630.87]), ('control #5', ['fwd', -157.356, -5.165, 10.0, 0.0, 45.0], [-13158662.23, -574322.43]), ('control #6', ['fwd', 59.325, 61.191, 150.0, 20.0, 30.0], [-8731797.31, 4580235.26]), ('control #7', ['fwd', -27.732, -15.271, 150.0, 0.0, -37.5], [-15678977.34, -1698059.59]), ('control #8', ['fwd', -101.988, -64.848, 0.0, 20.0, 45.0], [-8018987.33, -9434677.5]), ('control #11', ['inv', 14726764.0, 5249990.3, 150.0, 0.0, 30.0], [-57.070508, 47.214245]), ('control #15', ['inv', 6171735.4, 4391661.0, 150.0, -10.0, 0.0], [-154.496311, 29.495113])], [('control #6', ['fwd', 59.325, 61.191, 150.0, 20.0, 30.0], [-8731797.31, 4580235.26]), ('control #7', ['fwd', -27.732, -15.271, 150.0, 0.0, -37.5], [-15678977.34, -1698059.59]), ('control #8', ['fwd', -101.988, -64.848, 0.0, 20.0, 45.0], [-8018987.33, -9434677.5]), ('control #9', ['fwd', -176.6, 10.306, 10.0, 0.0, -37.5], [15296821.45, 1145976.17]), ('control #10', ['inv', -9574741.6, -6948175.3, 10.0, 20.0, 45.0], [-111.774572, -42.486373]), ('control #11', ['inv', 14726764.0, 5249990.3, 150.0, 0.0, 30.0], [-57.070508, 47.214245]), ('control #15', ['inv', 6171735.4, 4391661.0, 150.0, -10.0, 0.0], [-154.496311, 29.495113]), ('regression #23', ['inv', 3000000.0, 100000.0, 170.0, 5.0, 40.0], [-154.780609, 5.899321])], [('control #9', ['fwd', -176.6, 10.306, 10.0, 0.0, -37.5], [15296821.45, 1145976.17]), ('control #10', ['inv', -9574741.6, -6948175.3, 10.0, 20.0, 45.0], [-111.774572, -42.486373]), ('control #11', ['inv', 14726764.0, 5249990.3, 150.0, 0.0, 30.0], [-57.070508, 47.214245]), ('control #12', ['inv', 10315651.1, 166980.2, 10.0, -10.0, 45.0], [141.197691, -8.498313]), ('control #13', ['inv', 5503696.2, -7904836.4, 0.0, 0.0, 0.0], [49.495875, -71.089824]), ('control #14', ['inv', 2270255.8, 547031.0, 150.0, 0.0, 45.0], [178.873826, 4.919563]), ('control #17', ['inv', -1605712.3, 6233155.6, 10.0, 0.0, 0.0], [-4.440502, 56.056053]), ('regression #24', ['inv', -3000000.0, 0.0, -170.0, 0.0, 0.0], [163.020381, 0.0])], [('control #10', ['inv', -9574741.6, -6948175.3, 10.0, 20.0, 45.0], [-111.774572, -42.486373]), ('control #11', ['inv', 14726764.0, 5249990.3, 150.0, 0.0, 30.0], [-57.070508, 47.214245]), ('control #14', ['inv', 2270255.8, 547031.0, 150.0, 0.0, 45.0], [178.873826, 4.919563]), ('control #15', ['inv', 6171735.4, 4391661.0, 150.0, -10.0, 0.0], [-154.496311, 29.495113]), ('control #16', ['inv', 5573045.0, 6790547.5, 10.0, -10.0, 45.0], [80.879737, 51.068794]), ('control #17', ['inv', -1605712.3, 6233155.6, 10.0, 0.0, 0.0], [-4.440502, 56.056053]), ('control #18', ['inv', 11534249.9, -2599094.3, 0.0, 0.0, 45.0], [146.696214, -23.374191]), ('control #19', ['inv', 1510017.4, 7976956.2, 150.0, 0.0, 0.0], [163.579898, 71.738412])]]
for label, args, expected in fixtures[N-1]:
    check(label, solve(args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
Boundary fixtureActualExpectedOutcome
control #0[14368068.23, -7654555.97][14368068.23, -7654555.97]Passed
control #1[4309310.45, -7302846.03][4309310.45, -7302846.03]Passed
control #2[-6347382.14, -5117196.15][-6347382.14, -5117196.15]Passed
control #3[-918956.11, -7314632.7][-918956.11, -7314632.7]Passed
control #4[-4520215.78, -6870630.87][-4520215.78, -6870630.87]Passed
control #5[-13158662.23, -574322.43][-13158662.23, -574322.43]Passed
control #10[-111.774572, -42.486373][-111.774572, -42.486373]Passed
control #11[-57.070508, 47.214245][-57.070508, 47.214245]Passed

SHA-256 / a1f4931fbba276cf67ac375dc6570e1f54b314f5f75679702ecbaca055004b6b

Verification & scope

Stipulated deterministic toy contract on a bounded input domain; results are rounded as stated and no conformance with any published standard or library is claimed. This reproducer isolates one failure mechanism. Results cover the supplied fixtures. Variants within a family share a test contract and should remain grouped when constructing evaluation splits. Related mechanisms with a shared evaluation_group must also remain together; these controlled models are not independent production incidents.

Observations recorded using Python 3.12.14 at 2026-09-29T14:48:17.081754+00:00.

Case digest / a4720199053cae8f4037b4cbf14e45e57f2dee443d5f9829b4e3330168296c71