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.
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 fixture | Actual | Expected | Outcome |
|---|---|---|---|
| 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 fixture | Actual | Expected | Outcome |
|---|---|---|---|
| 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 fixture | Actual | Expected | Outcome |
|---|---|---|---|
| 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