FAILURE MAP
← Case archive

FA-70146 / Map projection transforms / Open access

Spherical Albers equal-area conic forward: northing orientation · case 01

The map is flipped north-south about the origin.

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

ROOT CAUSE

The northing subtracts rho0 from rho*cos(theta) instead of the reverse.

VERIFIED REPAIR

At the northing orientation step restore `round(rho0 - rho * math.cos(theta), 2)`, leaving the rest of the model unchanged.

Unsuccessful approach: Using the absolute value folds everything south of the origin onto the north.

Case contract

Input [lon, lat, lon0, lat0, lat1, lat2] degrees, sphere R = 6371000, lat1 != -lat2. n = (sin p1 + sin p2)/2; C = cos(p1)**2 + 2*n*sin(p1); rho = R*sqrt(C - 2*n*sin(phi))/n; rho0 likewise at lat0; theta = n*dlon with dlon wrapped to [-180, 180). x = rho*sin(theta), y = rho0 - rho*cos(theta), rounded to 2 decimals.

Why this case matters

Continental statistics maps use Albers because it preserves area; a wrong constant silently biases area totals.

1 / The failure

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(x):
    lon, lat, lon0, lat0, lat1, lat2 = x
    R = 6371000.0
    p0, p1, p2, phi = [math.radians(v) for v in (lat0, lat1, lat2, lat)]
    n = (math.sin(p1) + math.sin(p2)) / 2
    C = math.cos(p1) ** 2 + 2 * n * math.sin(p1)
    rho = R * math.sqrt(C - 2 * n * math.sin(phi)) / n
    rho0 = R * math.sqrt(C - 2 * n * math.sin(p0)) / n
    dl = math.radians(((lon - lon0 + 180.0) % 360.0) - 180.0)
    theta = n * dl
    return [round(rho * math.sin(theta), 2), round(rho * math.cos(theta) - rho0, 2)]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('control #0', [-74.411, 16.812, -96.0, 23.0, 33.0, 45.0], [2405679.11, -375515.8]), ('control #1', [-7.054, -2.555, 10.0, 52.0, 35.0, 65.0], [-2368752.85, -5460256.95]), ('control #2', [163.654, -41.81, 132.0, 0.0, -18.0, -36.0], [2661851.12, -4920407.16]), ('control #3', [-52.492, -45.412, -60.0, -32.0, -5.0, -42.0], [601606.86, -1530950.07]), ('control #4', [-26.25, -1.472, 0.0, 40.0, 40.0, 40.0], [-3459655.33, -3813601.11]), ('control #5', [24.242, -15.571, 20.0, -45.0, -45.0, -45.0], [499045.1, 3152788.69]), ('boundary #28', [-96.0, 23.0, -96.0, 23.0, 33.0, 45.0], [0.0, 0.0]), ('boundary #29', [5.0, 40.0, 5.0, 40.0, 40.0, 40.0], [0.0, 0.0])], [('control #1', [-7.054, -2.555, 10.0, 52.0, 35.0, 65.0], [-2368752.85, -5460256.95]), ('control #2', [163.654, -41.81, 132.0, 0.0, -18.0, -36.0], [2661851.12, -4920407.16]), ('control #5', [24.242, -15.571, 20.0, -45.0, -45.0, -45.0], [499045.1, 3152788.69]), ('control #6', [95.34, 51.232, 100.0, 30.0, 25.0, 47.0], [-331403.06, 2390895.63]), ('control #7', [-119.961, 31.565, -96.0, 23.0, 33.0, 45.0], [-2250314.51, 1235327.67]), ('control #8', [-25.931, 8.989, 10.0, 52.0, 35.0, 65.0], [-4374379.83, -3619965.56]), ('boundary #28', [-96.0, 23.0, -96.0, 23.0, 33.0, 45.0], [0.0, 0.0]), ('boundary #29', [5.0, 40.0, 5.0, 40.0, 40.0, 40.0], [0.0, 0.0])], [('control #2', [163.654, -41.81, 132.0, 0.0, -18.0, -36.0], [2661851.12, -4920407.16]), ('control #3', [-52.492, -45.412, -60.0, -32.0, -5.0, -42.0], [601606.86, -1530950.07]), ('control #8', [-25.931, 8.989, 10.0, 52.0, 35.0, 65.0], [-4374379.83, -3619965.56]), ('control #9', [128.053, -60.735, 132.0, 0.0, -18.0, -36.0], [-277286.95, -6470938.42]), ('control #10', [-40.992, -24.699, -60.0, -32.0, -5.0, -42.0], [1816236.48, 739740.04]), ('control #11', [25.244, 44.637, 0.0, 40.0, 40.0, 40.0], [1977746.83, 796959.72]), ('boundary #28', [-96.0, 23.0, -96.0, 23.0, 33.0, 45.0], [0.0, 0.0]), ('boundary #29', [5.0, 40.0, 5.0, 40.0, 40.0, 40.0], [0.0, 0.0])], [('control #3', [-52.492, -45.412, -60.0, -32.0, -5.0, -42.0], [601606.86, -1530950.07]), ('control #4', [-26.25, -1.472, 0.0, 40.0, 40.0, 40.0], [-3459655.33, -3813601.11]), ('control #11', [25.244, 44.637, 0.0, 40.0, 40.0, 40.0], [1977746.83, 796959.72]), ('control #12', [-19.298, -12.334, 20.0, -45.0, -45.0, -45.0], [-4597338.48, 2353100.16]), ('control #13', [99.747, 65.471, 100.0, 30.0, 25.0, 47.0], [-14324.17, 3825527.6]), ('control #14', [-128.991, 27.029, -96.0, 23.0, 33.0, 45.0], [-3243071.92, 1029568.66]), ('boundary #28', [-96.0, 23.0, -96.0, 23.0, 33.0, 45.0], [0.0, 0.0]), ('boundary #29', [5.0, 40.0, 5.0, 40.0, 40.0, 40.0], [0.0, 0.0])], [('control #4', [-26.25, -1.472, 0.0, 40.0, 40.0, 40.0], [-3459655.33, -3813601.11]), ('control #8', [-25.931, 8.989, 10.0, 52.0, 35.0, 65.0], [-4374379.83, -3619965.56]), ('control #14', [-128.991, 27.029, -96.0, 23.0, 33.0, 45.0], [-3243071.92, 1029568.66]), ('control #15', [-24.721, 3.031, 10.0, 52.0, 35.0, 65.0], [-4481724.64, -4195384.79]), ('control #16', [154.974, -67.172, 132.0, 0.0, -18.0, -36.0], [1514049.99, -7114831.09]), ('control #17', [-37.667, -43.269, -60.0, -32.0, -5.0, -42.0], [1818158.31, -1416145.5]), ('boundary #28', [-96.0, 23.0, -96.0, 23.0, 33.0, 45.0], [0.0, 0.0]), ('boundary #29', [5.0, 40.0, 5.0, 40.0, 40.0, 40.0], [0.0, 0.0])]]
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[2405679.11, 375515.8][2405679.11, -375515.8]Failed
control #1[-2368752.85, 5460256.95][-2368752.85, -5460256.95]Failed
control #2[2661851.12, 4920407.16][2661851.12, -4920407.16]Failed
control #3[601606.86, 1530950.07][601606.86, -1530950.07]Failed
control #4[-3459655.33, 3813601.11][-3459655.33, -3813601.11]Failed
control #5[499045.1, -3152788.69][499045.1, 3152788.69]Failed
boundary #28[0.0, 0.0][0.0, 0.0]Passed
boundary #29[0.0, 0.0][0.0, 0.0]Passed

SHA-256 / a9ee1be7b0166371efdfca3ba61d0b558ec3e37f32556a6a693c48ee572b276f

2 / The unsuccessful fix

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(x):
    lon, lat, lon0, lat0, lat1, lat2 = x
    R = 6371000.0
    p0, p1, p2, phi = [math.radians(v) for v in (lat0, lat1, lat2, lat)]
    n = (math.sin(p1) + math.sin(p2)) / 2
    C = math.cos(p1) ** 2 + 2 * n * math.sin(p1)
    rho = R * math.sqrt(C - 2 * n * math.sin(phi)) / n
    rho0 = R * math.sqrt(C - 2 * n * math.sin(p0)) / n
    dl = math.radians(((lon - lon0 + 180.0) % 360.0) - 180.0)
    theta = n * dl
    return [round(rho * math.sin(theta), 2), round(abs(rho0 - rho * math.cos(theta)), 2)]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('control #0', [-74.411, 16.812, -96.0, 23.0, 33.0, 45.0], [2405679.11, -375515.8]), ('control #1', [-7.054, -2.555, 10.0, 52.0, 35.0, 65.0], [-2368752.85, -5460256.95]), ('control #2', [163.654, -41.81, 132.0, 0.0, -18.0, -36.0], [2661851.12, -4920407.16]), ('control #3', [-52.492, -45.412, -60.0, -32.0, -5.0, -42.0], [601606.86, -1530950.07]), ('control #4', [-26.25, -1.472, 0.0, 40.0, 40.0, 40.0], [-3459655.33, -3813601.11]), ('control #5', [24.242, -15.571, 20.0, -45.0, -45.0, -45.0], [499045.1, 3152788.69]), ('boundary #28', [-96.0, 23.0, -96.0, 23.0, 33.0, 45.0], [0.0, 0.0]), ('boundary #29', [5.0, 40.0, 5.0, 40.0, 40.0, 40.0], [0.0, 0.0])], [('control #1', [-7.054, -2.555, 10.0, 52.0, 35.0, 65.0], [-2368752.85, -5460256.95]), ('control #2', [163.654, -41.81, 132.0, 0.0, -18.0, -36.0], [2661851.12, -4920407.16]), ('control #5', [24.242, -15.571, 20.0, -45.0, -45.0, -45.0], [499045.1, 3152788.69]), ('control #6', [95.34, 51.232, 100.0, 30.0, 25.0, 47.0], [-331403.06, 2390895.63]), ('control #7', [-119.961, 31.565, -96.0, 23.0, 33.0, 45.0], [-2250314.51, 1235327.67]), ('control #8', [-25.931, 8.989, 10.0, 52.0, 35.0, 65.0], [-4374379.83, -3619965.56]), ('boundary #28', [-96.0, 23.0, -96.0, 23.0, 33.0, 45.0], [0.0, 0.0]), ('boundary #29', [5.0, 40.0, 5.0, 40.0, 40.0, 40.0], [0.0, 0.0])], [('control #2', [163.654, -41.81, 132.0, 0.0, -18.0, -36.0], [2661851.12, -4920407.16]), ('control #3', [-52.492, -45.412, -60.0, -32.0, -5.0, -42.0], [601606.86, -1530950.07]), ('control #8', [-25.931, 8.989, 10.0, 52.0, 35.0, 65.0], [-4374379.83, -3619965.56]), ('control #9', [128.053, -60.735, 132.0, 0.0, -18.0, -36.0], [-277286.95, -6470938.42]), ('control #10', [-40.992, -24.699, -60.0, -32.0, -5.0, -42.0], [1816236.48, 739740.04]), ('control #11', [25.244, 44.637, 0.0, 40.0, 40.0, 40.0], [1977746.83, 796959.72]), ('boundary #28', [-96.0, 23.0, -96.0, 23.0, 33.0, 45.0], [0.0, 0.0]), ('boundary #29', [5.0, 40.0, 5.0, 40.0, 40.0, 40.0], [0.0, 0.0])], [('control #3', [-52.492, -45.412, -60.0, -32.0, -5.0, -42.0], [601606.86, -1530950.07]), ('control #4', [-26.25, -1.472, 0.0, 40.0, 40.0, 40.0], [-3459655.33, -3813601.11]), ('control #11', [25.244, 44.637, 0.0, 40.0, 40.0, 40.0], [1977746.83, 796959.72]), ('control #12', [-19.298, -12.334, 20.0, -45.0, -45.0, -45.0], [-4597338.48, 2353100.16]), ('control #13', [99.747, 65.471, 100.0, 30.0, 25.0, 47.0], [-14324.17, 3825527.6]), ('control #14', [-128.991, 27.029, -96.0, 23.0, 33.0, 45.0], [-3243071.92, 1029568.66]), ('boundary #28', [-96.0, 23.0, -96.0, 23.0, 33.0, 45.0], [0.0, 0.0]), ('boundary #29', [5.0, 40.0, 5.0, 40.0, 40.0, 40.0], [0.0, 0.0])], [('control #4', [-26.25, -1.472, 0.0, 40.0, 40.0, 40.0], [-3459655.33, -3813601.11]), ('control #8', [-25.931, 8.989, 10.0, 52.0, 35.0, 65.0], [-4374379.83, -3619965.56]), ('control #14', [-128.991, 27.029, -96.0, 23.0, 33.0, 45.0], [-3243071.92, 1029568.66]), ('control #15', [-24.721, 3.031, 10.0, 52.0, 35.0, 65.0], [-4481724.64, -4195384.79]), ('control #16', [154.974, -67.172, 132.0, 0.0, -18.0, -36.0], [1514049.99, -7114831.09]), ('control #17', [-37.667, -43.269, -60.0, -32.0, -5.0, -42.0], [1818158.31, -1416145.5]), ('boundary #28', [-96.0, 23.0, -96.0, 23.0, 33.0, 45.0], [0.0, 0.0]), ('boundary #29', [5.0, 40.0, 5.0, 40.0, 40.0, 40.0], [0.0, 0.0])]]
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[2405679.11, 375515.8][2405679.11, -375515.8]Failed
control #1[-2368752.85, 5460256.95][-2368752.85, -5460256.95]Failed
control #2[2661851.12, 4920407.16][2661851.12, -4920407.16]Failed
control #3[601606.86, 1530950.07][601606.86, -1530950.07]Failed
control #4[-3459655.33, 3813601.11][-3459655.33, -3813601.11]Failed
control #5[499045.1, 3152788.69][499045.1, 3152788.69]Passed
boundary #28[0.0, 0.0][0.0, 0.0]Passed
boundary #29[0.0, 0.0][0.0, 0.0]Passed

SHA-256 / 238a16edeb210797cd6acfc2d0079f413117830ee458158dc475ae1cc4990cf2

3 / The verified repair

Exit 0
"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(x):
    lon, lat, lon0, lat0, lat1, lat2 = x
    R = 6371000.0
    p0, p1, p2, phi = [math.radians(v) for v in (lat0, lat1, lat2, lat)]
    n = (math.sin(p1) + math.sin(p2)) / 2
    C = math.cos(p1) ** 2 + 2 * n * math.sin(p1)
    rho = R * math.sqrt(C - 2 * n * math.sin(phi)) / n
    rho0 = R * math.sqrt(C - 2 * n * math.sin(p0)) / n
    dl = math.radians(((lon - lon0 + 180.0) % 360.0) - 180.0)
    theta = n * dl
    return [round(rho * math.sin(theta), 2), round(rho0 - rho * math.cos(theta), 2)]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('control #0', [-74.411, 16.812, -96.0, 23.0, 33.0, 45.0], [2405679.11, -375515.8]), ('control #1', [-7.054, -2.555, 10.0, 52.0, 35.0, 65.0], [-2368752.85, -5460256.95]), ('control #2', [163.654, -41.81, 132.0, 0.0, -18.0, -36.0], [2661851.12, -4920407.16]), ('control #3', [-52.492, -45.412, -60.0, -32.0, -5.0, -42.0], [601606.86, -1530950.07]), ('control #4', [-26.25, -1.472, 0.0, 40.0, 40.0, 40.0], [-3459655.33, -3813601.11]), ('control #5', [24.242, -15.571, 20.0, -45.0, -45.0, -45.0], [499045.1, 3152788.69]), ('boundary #28', [-96.0, 23.0, -96.0, 23.0, 33.0, 45.0], [0.0, 0.0]), ('boundary #29', [5.0, 40.0, 5.0, 40.0, 40.0, 40.0], [0.0, 0.0])], [('control #1', [-7.054, -2.555, 10.0, 52.0, 35.0, 65.0], [-2368752.85, -5460256.95]), ('control #2', [163.654, -41.81, 132.0, 0.0, -18.0, -36.0], [2661851.12, -4920407.16]), ('control #5', [24.242, -15.571, 20.0, -45.0, -45.0, -45.0], [499045.1, 3152788.69]), ('control #6', [95.34, 51.232, 100.0, 30.0, 25.0, 47.0], [-331403.06, 2390895.63]), ('control #7', [-119.961, 31.565, -96.0, 23.0, 33.0, 45.0], [-2250314.51, 1235327.67]), ('control #8', [-25.931, 8.989, 10.0, 52.0, 35.0, 65.0], [-4374379.83, -3619965.56]), ('boundary #28', [-96.0, 23.0, -96.0, 23.0, 33.0, 45.0], [0.0, 0.0]), ('boundary #29', [5.0, 40.0, 5.0, 40.0, 40.0, 40.0], [0.0, 0.0])], [('control #2', [163.654, -41.81, 132.0, 0.0, -18.0, -36.0], [2661851.12, -4920407.16]), ('control #3', [-52.492, -45.412, -60.0, -32.0, -5.0, -42.0], [601606.86, -1530950.07]), ('control #8', [-25.931, 8.989, 10.0, 52.0, 35.0, 65.0], [-4374379.83, -3619965.56]), ('control #9', [128.053, -60.735, 132.0, 0.0, -18.0, -36.0], [-277286.95, -6470938.42]), ('control #10', [-40.992, -24.699, -60.0, -32.0, -5.0, -42.0], [1816236.48, 739740.04]), ('control #11', [25.244, 44.637, 0.0, 40.0, 40.0, 40.0], [1977746.83, 796959.72]), ('boundary #28', [-96.0, 23.0, -96.0, 23.0, 33.0, 45.0], [0.0, 0.0]), ('boundary #29', [5.0, 40.0, 5.0, 40.0, 40.0, 40.0], [0.0, 0.0])], [('control #3', [-52.492, -45.412, -60.0, -32.0, -5.0, -42.0], [601606.86, -1530950.07]), ('control #4', [-26.25, -1.472, 0.0, 40.0, 40.0, 40.0], [-3459655.33, -3813601.11]), ('control #11', [25.244, 44.637, 0.0, 40.0, 40.0, 40.0], [1977746.83, 796959.72]), ('control #12', [-19.298, -12.334, 20.0, -45.0, -45.0, -45.0], [-4597338.48, 2353100.16]), ('control #13', [99.747, 65.471, 100.0, 30.0, 25.0, 47.0], [-14324.17, 3825527.6]), ('control #14', [-128.991, 27.029, -96.0, 23.0, 33.0, 45.0], [-3243071.92, 1029568.66]), ('boundary #28', [-96.0, 23.0, -96.0, 23.0, 33.0, 45.0], [0.0, 0.0]), ('boundary #29', [5.0, 40.0, 5.0, 40.0, 40.0, 40.0], [0.0, 0.0])], [('control #4', [-26.25, -1.472, 0.0, 40.0, 40.0, 40.0], [-3459655.33, -3813601.11]), ('control #8', [-25.931, 8.989, 10.0, 52.0, 35.0, 65.0], [-4374379.83, -3619965.56]), ('control #14', [-128.991, 27.029, -96.0, 23.0, 33.0, 45.0], [-3243071.92, 1029568.66]), ('control #15', [-24.721, 3.031, 10.0, 52.0, 35.0, 65.0], [-4481724.64, -4195384.79]), ('control #16', [154.974, -67.172, 132.0, 0.0, -18.0, -36.0], [1514049.99, -7114831.09]), ('control #17', [-37.667, -43.269, -60.0, -32.0, -5.0, -42.0], [1818158.31, -1416145.5]), ('boundary #28', [-96.0, 23.0, -96.0, 23.0, 33.0, 45.0], [0.0, 0.0]), ('boundary #29', [5.0, 40.0, 5.0, 40.0, 40.0, 40.0], [0.0, 0.0])]]
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[2405679.11, -375515.8][2405679.11, -375515.8]Passed
control #1[-2368752.85, -5460256.95][-2368752.85, -5460256.95]Passed
control #2[2661851.12, -4920407.16][2661851.12, -4920407.16]Passed
control #3[601606.86, -1530950.07][601606.86, -1530950.07]Passed
control #4[-3459655.33, -3813601.11][-3459655.33, -3813601.11]Passed
control #5[499045.1, 3152788.69][499045.1, 3152788.69]Passed
boundary #28[0.0, 0.0][0.0, 0.0]Passed
boundary #29[0.0, 0.0][0.0, 0.0]Passed

SHA-256 / 7062fa0ca0f02513c01b0d0e72707052748bbf589beb206dab058413dc3b444c

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.872031+00:00.

Case digest / 03aa0271992d91ced0dc446a0b7a4ba5584839a13082e765542d48781cc74e24