FA-70056 / Map projection transforms / Open access
Equirectangular projection with standard parallel: inverse scale division · case 01
Inverse longitudes shrink toward the central meridian.
ROOT CAUSE
The inverse multiplies by the parallel scale instead of dividing by it.
VERIFIED REPAIR
At the inverse scale division step restore `math.degrees(a / (R * k))`, leaving the rest of the model unchanged.
Unsuccessful approach: Dropping the scale entirely is right only when the standard parallel is the equator.
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 * k / R)
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 #12', ['inv', 10315651.1, 166980.2, 10.0, -10.0, 45.0], [141.197691, -8.498313])], [('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 #12', ['inv', 10315651.1, 166980.2, 10.0, -10.0, 45.0], [141.197691, -8.498313]), ('control #14', ['inv', 2270255.8, 547031.0, 150.0, 0.0, 45.0], [178.873826, 4.919563])], [('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 #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 #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 #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 | [-50.887286, -42.486373] | [-111.774572, -42.486373] | Failed |
| control #11 | [-95.302881, 47.214245] | [-57.070508, 47.214245] | Failed |
SHA-256 / 1f067e3227b2b0e81d13b808580dfcaddda7acd66ffa2d184c559f2a916f4bc5
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)
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 #12', ['inv', 10315651.1, 166980.2, 10.0, -10.0, 45.0], [141.197691, -8.498313])], [('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 #12', ['inv', 10315651.1, 166980.2, 10.0, -10.0, 45.0], [141.197691, -8.498313]), ('control #14', ['inv', 2270255.8, 547031.0, 150.0, 0.0, 45.0], [178.873826, 4.919563])], [('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 #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 #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 #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 | [-76.107625, -42.486373] | [-111.774572, -42.486373] | Failed |
| control #11 | [-77.559175, 47.214245] | [-57.070508, 47.214245] | Failed |
SHA-256 / 07905c98df1f344279cf2f8c6b56acc466f7e7be1844d9f20e986dc50d0524ec
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 #12', ['inv', 10315651.1, 166980.2, 10.0, -10.0, 45.0], [141.197691, -8.498313])], [('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 #12', ['inv', 10315651.1, 166980.2, 10.0, -10.0, 45.0], [141.197691, -8.498313]), ('control #14', ['inv', 2270255.8, 547031.0, 150.0, 0.0, 45.0], [178.873826, 4.919563])], [('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 #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 #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 #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 / 49ff32f63e9230598d901564947db065adc204efbaf1a8c751bf62457ea8d20d
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.078028+00:00.
Case digest / 90953057587fcdbe44a946ba5fa21a776d9d62bd6c7f364ef368a2cb88ac3724