FA-70051 / Map projection transforms / Open access
Equirectangular projection with standard parallel: forward origin latitude · case 01
Northings ignore the grid origin latitude.
ROOT CAUSE
The forward northing is measured from the equator instead of from lat0.
VERIFIED REPAIR
At the forward origin latitude step restore `round(R * math.radians(b - lat0), 2)`, leaving the rest of the model unchanged.
Unsuccessful approach: The origin latitude is added instead of subtracted.
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), 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 #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 #1', ['fwd', 58.849, -45.676, 10.0, 20.0, -37.5], [4309310.45, -7302846.03]), ('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 #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 #4', ['fwd', 103.06, -41.789, 150.0, 20.0, 30.0], [-4520215.78, -6870630.87]), ('control #6', ['fwd', 59.325, 61.191, 150.0, 20.0, 30.0], [-8731797.31, 4580235.26]), ('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 #13', ['inv', 5503696.2, -7904836.4, 0.0, 0.0, 0.0], [49.495875, -71.089824])], [('control #6', ['fwd', 59.325, 61.191, 150.0, 20.0, 30.0], [-8731797.31, 4580235.26]), ('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 #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 #0', ['fwd', 29.215, -48.839, -100.0, 20.0, 0.0], [14368068.23, -7654555.97]), ('control #8', ['fwd', -101.988, -64.848, 0.0, 20.0, 45.0], [-8018987.33, -9434677.5]), ('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, -5430654.99] | [14368068.23, -7654555.97] | Failed |
| control #1 | [4309310.45, -5078945.05] | [4309310.45, -7302846.03] | Failed |
| 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, -4646729.9] | [-4520215.78, -6870630.87] | Failed |
| control #5 | [-13158662.23, -574322.43] | [-13158662.23, -574322.43] | Passed |
| control #6 | [-8731797.31, 6804136.23] | [-8731797.31, 4580235.26] | Failed |
| control #7 | [-15678977.34, -1698059.59] | [-15678977.34, -1698059.59] | Passed |
SHA-256 / 1254a09bc5546ca66e3ae7960fb86c48fe131cfdd3155f633aeef3d98729da89
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 + 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 #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 #1', ['fwd', 58.849, -45.676, 10.0, 20.0, -37.5], [4309310.45, -7302846.03]), ('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 #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 #4', ['fwd', 103.06, -41.789, 150.0, 20.0, 30.0], [-4520215.78, -6870630.87]), ('control #6', ['fwd', 59.325, 61.191, 150.0, 20.0, 30.0], [-8731797.31, 4580235.26]), ('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 #13', ['inv', 5503696.2, -7904836.4, 0.0, 0.0, 0.0], [49.495875, -71.089824])], [('control #6', ['fwd', 59.325, 61.191, 150.0, 20.0, 30.0], [-8731797.31, 4580235.26]), ('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 #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 #0', ['fwd', 29.215, -48.839, -100.0, 20.0, 0.0], [14368068.23, -7654555.97]), ('control #8', ['fwd', -101.988, -64.848, 0.0, 20.0, 45.0], [-8018987.33, -9434677.5]), ('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, -3206754.01] | [14368068.23, -7654555.97] | Failed |
| control #1 | [4309310.45, -2855044.07] | [4309310.45, -7302846.03] | Failed |
| 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, -2422828.92] | [-4520215.78, -6870630.87] | Failed |
| control #5 | [-13158662.23, -574322.43] | [-13158662.23, -574322.43] | Passed |
| control #6 | [-8731797.31, 9028037.21] | [-8731797.31, 4580235.26] | Failed |
| control #7 | [-15678977.34, -1698059.59] | [-15678977.34, -1698059.59] | Passed |
SHA-256 / 5fbd23e8fad64c829c44eeee41e2b5a752ab708d59e6cee03167c02b34800cf9
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 #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 #1', ['fwd', 58.849, -45.676, 10.0, 20.0, -37.5], [4309310.45, -7302846.03]), ('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 #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 #4', ['fwd', 103.06, -41.789, 150.0, 20.0, 30.0], [-4520215.78, -6870630.87]), ('control #6', ['fwd', 59.325, 61.191, 150.0, 20.0, 30.0], [-8731797.31, 4580235.26]), ('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 #13', ['inv', 5503696.2, -7904836.4, 0.0, 0.0, 0.0], [49.495875, -71.089824])], [('control #6', ['fwd', 59.325, 61.191, 150.0, 20.0, 30.0], [-8731797.31, 4580235.26]), ('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 #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 #0', ['fwd', 29.215, -48.839, -100.0, 20.0, 0.0], [14368068.23, -7654555.97]), ('control #8', ['fwd', -101.988, -64.848, 0.0, 20.0, 45.0], [-8018987.33, -9434677.5]), ('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 #6 | [-8731797.31, 4580235.26] | [-8731797.31, 4580235.26] | Passed |
| control #7 | [-15678977.34, -1698059.59] | [-15678977.34, -1698059.59] | Passed |
SHA-256 / f0d108b807aa614d28f8acb0dffe207655fa964e3503b494f4026ee0eb587a78
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.073661+00:00.
Case digest / e57f3e9c7e9293b2d45cf9e96243a926d61f618ed6ef4c9b901e9328c7e46167