FAILURE MAP
← Case archive

FA-70051 / Map projection transforms / Open access

Equirectangular projection with standard parallel: forward origin latitude · case 01

Northings ignore the grid origin latitude.

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

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 fixtureActualExpectedOutcome
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 fixtureActualExpectedOutcome
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 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 #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