FAILURE MAP
← Case archive

FA-70111 / Map projection transforms / Open access

Spherical Lambert conformal conic forward: origin radius · case 01

False origins are shifted north or south by hundreds of kilometres.

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

ROOT CAUSE

The radius at the origin latitude is taken as the cone apex radius R*F without the t(p0) term.

VERIFIED REPAIR

At the origin radius step restore `rho0 = R * Fc / t(p0) ** n`, leaving the rest of the model unchanged.

Unsuccessful approach: The origin radius is evaluated at the first standard parallel rather than at the latitude of origin.

Case contract

Input [lon, lat, lon0, lat0, lat1, lat2] degrees, sphere R = 6371000, all latitudes strictly inside (-90, 90) and on the cone side of the equator. t(p) = tan(pi/4 + p/2). n = ln(cos p1/cos p2)/ln(t(p2)/t(p1)), or sin(p1) for a tangent cone (lat1 == lat2). F = cos(p1)*t(p1)**n/n; rho = R*F/t(phi)**n; rho0 = R*F/t(p0)**n; 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

Aeronautical charts and many national grids use conformal conics; a wrong cone constant distorts whole regions.

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)]
    def t(p):
        return math.tan(math.pi / 4 + p / 2)
    if abs(lat1 - lat2) < 1e-9:
        n = math.sin(p1)
    else:
        n = math.log(math.cos(p1) / math.cos(p2)) / math.log(t(p2) / t(p1))
    Fc = math.cos(p1) * t(p1) ** n / n
    rho = R * Fc / t(phi) ** n
    rho0 = R * Fc
    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', [-89.295, -2.579, -96.0, 23.0, 33.0, 45.0], [944572.37, -3177093.78]), ('control #1', [43.173, 23.809, 10.0, 52.0, 35.0, 65.0], [3477264.95, -2333351.52]), ('control #2', [133.802, -22.769, 132.0, 0.0, -18.0, -36.0], [182997.23, -2612489.01]), ('control #3', [-77.146, 4.744, -60.0, -32.0, -5.0, -42.0], [-2030234.59, 3881165.57]), ('control #4', [-6.229, 62.13, 0.0, 40.0, 40.0, 40.0], [-353408.33, 2543656.82]), ('control #5', [57.493, -17.538, 20.0, -45.0, -45.0, -45.0], [4256679.54, 2162268.41]), ('control #6', [108.376, 58.574, 100.0, 30.0, 25.0, 47.0], [519881.13, 3212621.55]), ('control #7', [-134.048, 58.473, -96.0, 23.0, 33.0, 45.0], [-2281076.94, 4475835.48])], [('control #1', [43.173, 23.809, 10.0, 52.0, 35.0, 65.0], [3477264.95, -2333351.52]), ('control #2', [133.802, -22.769, 132.0, 0.0, -18.0, -36.0], [182997.23, -2612489.01]), ('control #5', [57.493, -17.538, 20.0, -45.0, -45.0, -45.0], [4256679.54, 2162268.41]), ('control #6', [108.376, 58.574, 100.0, 30.0, 25.0, 47.0], [519881.13, 3212621.55]), ('control #7', [-134.048, 58.473, -96.0, 23.0, 33.0, 45.0], [-2281076.94, 4475835.48]), ('control #8', [38.766, 26.431, 10.0, 52.0, 35.0, 65.0], [2923972.77, -2242833.53]), ('control #9', [128.089, -10.995, 132.0, 0.0, -18.0, -36.0], [-438054.92, -1302331.66]), ('control #10', [-72.231, -55.506, -60.0, -32.0, -5.0, -42.0], [-871966.44, -2710910.26])], [('control #2', [133.802, -22.769, 132.0, 0.0, -18.0, -36.0], [182997.23, -2612489.01]), ('control #3', [-77.146, 4.744, -60.0, -32.0, -5.0, -42.0], [-2030234.59, 3881165.57]), ('control #8', [38.766, 26.431, 10.0, 52.0, 35.0, 65.0], [2923972.77, -2242833.53]), ('control #9', [128.089, -10.995, 132.0, 0.0, -18.0, -36.0], [-438054.92, -1302331.66]), ('control #10', [-72.231, -55.506, -60.0, -32.0, -5.0, -42.0], [-871966.44, -2710910.26]), ('control #11', [-30.764, 26.528, 0.0, 40.0, 40.0, 40.0], [-3080105.21, -974518.51]), ('control #12', [31.835, 4.636, 20.0, -45.0, -45.0, -45.0], [1831181.26, 6076923.26]), ('control #13', [74.565, 35.028, 100.0, 30.0, 25.0, 47.0], [-2247809.44, 846928.26])], [('control #3', [-77.146, 4.744, -60.0, -32.0, -5.0, -42.0], [-2030234.59, 3881165.57]), ('control #4', [-6.229, 62.13, 0.0, 40.0, 40.0, 40.0], [-353408.33, 2543656.82]), ('control #11', [-30.764, 26.528, 0.0, 40.0, 40.0, 40.0], [-3080105.21, -974518.51]), ('control #12', [31.835, 4.636, 20.0, -45.0, -45.0, -45.0], [1831181.26, 6076923.26]), ('control #13', [74.565, 35.028, 100.0, 30.0, 25.0, 47.0], [-2247809.44, 846928.26]), ('control #14', [-102.998, 17.408, -96.0, 23.0, 33.0, 45.0], [-788758.63, -620642.12]), ('control #15', [-3.245, 2.365, 10.0, 52.0, 35.0, 65.0], [-1927899.63, -5755732.58]), ('control #16', [150.921, -43.877, 132.0, 0.0, -18.0, -36.0], [1562347.67, -5075599.0])], [('control #5', [57.493, -17.538, 20.0, -45.0, -45.0, -45.0], [4256679.54, 2162268.41]), ('control #6', [108.376, 58.574, 100.0, 30.0, 25.0, 47.0], [519881.13, 3212621.55]), ('control #14', [-102.998, 17.408, -96.0, 23.0, 33.0, 45.0], [-788758.63, -620642.12]), ('control #15', [-3.245, 2.365, 10.0, 52.0, 35.0, 65.0], [-1927899.63, -5755732.58]), ('control #16', [150.921, -43.877, 132.0, 0.0, -18.0, -36.0], [1562347.67, -5075599.0]), ('control #17', [-99.291, 2.109, -60.0, -32.0, -5.0, -42.0], [-4518513.04, 3061080.05]), ('control #18', [28.411, 1.618, 0.0, 40.0, 40.0, 40.0], [3815361.61, -3969481.26]), ('control #19', [0.654, -74.153, 20.0, -45.0, -45.0, -45.0], [-696797.89, -3508209.88])]]
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[944572.37, -323794.79][944572.37, -3177093.78]Failed
control #1[3477264.95, 3947397.08][3477264.95, -2333351.52]Failed
control #2[182997.23, -2612489.01][182997.23, -2612489.01]Passed
control #3[-2030234.59, 430890.37][-2030234.59, 3881165.57]Failed
control #4[-353408.33, 7349440.71][-353408.33, 2543656.82]Failed
control #5[4256679.54, -3348221.07][4256679.54, 2162268.41]Failed
control #6[519881.13, 6748298.92][519881.13, 3212621.55]Failed
control #7[-2281076.94, 7329134.47][-2281076.94, 4475835.48]Failed

SHA-256 / d0f50074df742417d4320d79ae47e76defa21fb9389647ca7fe6254c9a45e7f6

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)]
    def t(p):
        return math.tan(math.pi / 4 + p / 2)
    if abs(lat1 - lat2) < 1e-9:
        n = math.sin(p1)
    else:
        n = math.log(math.cos(p1) / math.cos(p2)) / math.log(t(p2) / t(p1))
    Fc = math.cos(p1) * t(p1) ** n / n
    rho = R * Fc / t(phi) ** n
    rho0 = R * Fc / t(p1) ** 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', [-89.295, -2.579, -96.0, 23.0, 33.0, 45.0], [944572.37, -3177093.78]), ('control #1', [43.173, 23.809, 10.0, 52.0, 35.0, 65.0], [3477264.95, -2333351.52]), ('control #2', [133.802, -22.769, 132.0, 0.0, -18.0, -36.0], [182997.23, -2612489.01]), ('control #3', [-77.146, 4.744, -60.0, -32.0, -5.0, -42.0], [-2030234.59, 3881165.57]), ('control #4', [-6.229, 62.13, 0.0, 40.0, 40.0, 40.0], [-353408.33, 2543656.82]), ('control #5', [57.493, -17.538, 20.0, -45.0, -45.0, -45.0], [4256679.54, 2162268.41]), ('control #6', [108.376, 58.574, 100.0, 30.0, 25.0, 47.0], [519881.13, 3212621.55]), ('control #7', [-134.048, 58.473, -96.0, 23.0, 33.0, 45.0], [-2281076.94, 4475835.48])], [('control #1', [43.173, 23.809, 10.0, 52.0, 35.0, 65.0], [3477264.95, -2333351.52]), ('control #2', [133.802, -22.769, 132.0, 0.0, -18.0, -36.0], [182997.23, -2612489.01]), ('control #5', [57.493, -17.538, 20.0, -45.0, -45.0, -45.0], [4256679.54, 2162268.41]), ('control #6', [108.376, 58.574, 100.0, 30.0, 25.0, 47.0], [519881.13, 3212621.55]), ('control #7', [-134.048, 58.473, -96.0, 23.0, 33.0, 45.0], [-2281076.94, 4475835.48]), ('control #8', [38.766, 26.431, 10.0, 52.0, 35.0, 65.0], [2923972.77, -2242833.53]), ('control #9', [128.089, -10.995, 132.0, 0.0, -18.0, -36.0], [-438054.92, -1302331.66]), ('control #10', [-72.231, -55.506, -60.0, -32.0, -5.0, -42.0], [-871966.44, -2710910.26])], [('control #2', [133.802, -22.769, 132.0, 0.0, -18.0, -36.0], [182997.23, -2612489.01]), ('control #3', [-77.146, 4.744, -60.0, -32.0, -5.0, -42.0], [-2030234.59, 3881165.57]), ('control #8', [38.766, 26.431, 10.0, 52.0, 35.0, 65.0], [2923972.77, -2242833.53]), ('control #9', [128.089, -10.995, 132.0, 0.0, -18.0, -36.0], [-438054.92, -1302331.66]), ('control #10', [-72.231, -55.506, -60.0, -32.0, -5.0, -42.0], [-871966.44, -2710910.26]), ('control #11', [-30.764, 26.528, 0.0, 40.0, 40.0, 40.0], [-3080105.21, -974518.51]), ('control #12', [31.835, 4.636, 20.0, -45.0, -45.0, -45.0], [1831181.26, 6076923.26]), ('control #13', [74.565, 35.028, 100.0, 30.0, 25.0, 47.0], [-2247809.44, 846928.26])], [('control #3', [-77.146, 4.744, -60.0, -32.0, -5.0, -42.0], [-2030234.59, 3881165.57]), ('control #4', [-6.229, 62.13, 0.0, 40.0, 40.0, 40.0], [-353408.33, 2543656.82]), ('control #11', [-30.764, 26.528, 0.0, 40.0, 40.0, 40.0], [-3080105.21, -974518.51]), ('control #12', [31.835, 4.636, 20.0, -45.0, -45.0, -45.0], [1831181.26, 6076923.26]), ('control #13', [74.565, 35.028, 100.0, 30.0, 25.0, 47.0], [-2247809.44, 846928.26]), ('control #14', [-102.998, 17.408, -96.0, 23.0, 33.0, 45.0], [-788758.63, -620642.12]), ('control #15', [-3.245, 2.365, 10.0, 52.0, 35.0, 65.0], [-1927899.63, -5755732.58]), ('control #16', [150.921, -43.877, 132.0, 0.0, -18.0, -36.0], [1562347.67, -5075599.0])], [('control #5', [57.493, -17.538, 20.0, -45.0, -45.0, -45.0], [4256679.54, 2162268.41]), ('control #6', [108.376, 58.574, 100.0, 30.0, 25.0, 47.0], [519881.13, 3212621.55]), ('control #14', [-102.998, 17.408, -96.0, 23.0, 33.0, 45.0], [-788758.63, -620642.12]), ('control #15', [-3.245, 2.365, 10.0, 52.0, 35.0, 65.0], [-1927899.63, -5755732.58]), ('control #16', [150.921, -43.877, 132.0, 0.0, -18.0, -36.0], [1562347.67, -5075599.0]), ('control #17', [-99.291, 2.109, -60.0, -32.0, -5.0, -42.0], [-4518513.04, 3061080.05]), ('control #18', [28.411, 1.618, 0.0, 40.0, 40.0, 40.0], [3815361.61, -3969481.26]), ('control #19', [0.654, -74.153, 20.0, -45.0, -45.0, -45.0], [-696797.89, -3508209.88])]]
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[944572.37, -4304304.56][944572.37, -3177093.78]Failed
control #1[3477264.95, -487756.31][3477264.95, -2333351.52]Failed
control #2[182997.23, -528785.91][182997.23, -2612489.01]Failed
control #3[-2030234.59, 995407.84][-2030234.59, 3881165.57]Failed
control #4[-353408.33, 2543656.82][-353408.33, 2543656.82]Passed
control #5[4256679.54, 2162268.41][4256679.54, 2162268.41]Passed
control #6[519881.13, 3764741.84][519881.13, 3212621.55]Failed
control #7[-2281076.94, 3348624.7][-2281076.94, 4475835.48]Failed

SHA-256 / 831db2510532534961bc7dd7a3d61c2ad7f68d733bbcc2ad5cc0c892b5a697b4

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)]
    def t(p):
        return math.tan(math.pi / 4 + p / 2)
    if abs(lat1 - lat2) < 1e-9:
        n = math.sin(p1)
    else:
        n = math.log(math.cos(p1) / math.cos(p2)) / math.log(t(p2) / t(p1))
    Fc = math.cos(p1) * t(p1) ** n / n
    rho = R * Fc / t(phi) ** n
    rho0 = R * Fc / t(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', [-89.295, -2.579, -96.0, 23.0, 33.0, 45.0], [944572.37, -3177093.78]), ('control #1', [43.173, 23.809, 10.0, 52.0, 35.0, 65.0], [3477264.95, -2333351.52]), ('control #2', [133.802, -22.769, 132.0, 0.0, -18.0, -36.0], [182997.23, -2612489.01]), ('control #3', [-77.146, 4.744, -60.0, -32.0, -5.0, -42.0], [-2030234.59, 3881165.57]), ('control #4', [-6.229, 62.13, 0.0, 40.0, 40.0, 40.0], [-353408.33, 2543656.82]), ('control #5', [57.493, -17.538, 20.0, -45.0, -45.0, -45.0], [4256679.54, 2162268.41]), ('control #6', [108.376, 58.574, 100.0, 30.0, 25.0, 47.0], [519881.13, 3212621.55]), ('control #7', [-134.048, 58.473, -96.0, 23.0, 33.0, 45.0], [-2281076.94, 4475835.48])], [('control #1', [43.173, 23.809, 10.0, 52.0, 35.0, 65.0], [3477264.95, -2333351.52]), ('control #2', [133.802, -22.769, 132.0, 0.0, -18.0, -36.0], [182997.23, -2612489.01]), ('control #5', [57.493, -17.538, 20.0, -45.0, -45.0, -45.0], [4256679.54, 2162268.41]), ('control #6', [108.376, 58.574, 100.0, 30.0, 25.0, 47.0], [519881.13, 3212621.55]), ('control #7', [-134.048, 58.473, -96.0, 23.0, 33.0, 45.0], [-2281076.94, 4475835.48]), ('control #8', [38.766, 26.431, 10.0, 52.0, 35.0, 65.0], [2923972.77, -2242833.53]), ('control #9', [128.089, -10.995, 132.0, 0.0, -18.0, -36.0], [-438054.92, -1302331.66]), ('control #10', [-72.231, -55.506, -60.0, -32.0, -5.0, -42.0], [-871966.44, -2710910.26])], [('control #2', [133.802, -22.769, 132.0, 0.0, -18.0, -36.0], [182997.23, -2612489.01]), ('control #3', [-77.146, 4.744, -60.0, -32.0, -5.0, -42.0], [-2030234.59, 3881165.57]), ('control #8', [38.766, 26.431, 10.0, 52.0, 35.0, 65.0], [2923972.77, -2242833.53]), ('control #9', [128.089, -10.995, 132.0, 0.0, -18.0, -36.0], [-438054.92, -1302331.66]), ('control #10', [-72.231, -55.506, -60.0, -32.0, -5.0, -42.0], [-871966.44, -2710910.26]), ('control #11', [-30.764, 26.528, 0.0, 40.0, 40.0, 40.0], [-3080105.21, -974518.51]), ('control #12', [31.835, 4.636, 20.0, -45.0, -45.0, -45.0], [1831181.26, 6076923.26]), ('control #13', [74.565, 35.028, 100.0, 30.0, 25.0, 47.0], [-2247809.44, 846928.26])], [('control #3', [-77.146, 4.744, -60.0, -32.0, -5.0, -42.0], [-2030234.59, 3881165.57]), ('control #4', [-6.229, 62.13, 0.0, 40.0, 40.0, 40.0], [-353408.33, 2543656.82]), ('control #11', [-30.764, 26.528, 0.0, 40.0, 40.0, 40.0], [-3080105.21, -974518.51]), ('control #12', [31.835, 4.636, 20.0, -45.0, -45.0, -45.0], [1831181.26, 6076923.26]), ('control #13', [74.565, 35.028, 100.0, 30.0, 25.0, 47.0], [-2247809.44, 846928.26]), ('control #14', [-102.998, 17.408, -96.0, 23.0, 33.0, 45.0], [-788758.63, -620642.12]), ('control #15', [-3.245, 2.365, 10.0, 52.0, 35.0, 65.0], [-1927899.63, -5755732.58]), ('control #16', [150.921, -43.877, 132.0, 0.0, -18.0, -36.0], [1562347.67, -5075599.0])], [('control #5', [57.493, -17.538, 20.0, -45.0, -45.0, -45.0], [4256679.54, 2162268.41]), ('control #6', [108.376, 58.574, 100.0, 30.0, 25.0, 47.0], [519881.13, 3212621.55]), ('control #14', [-102.998, 17.408, -96.0, 23.0, 33.0, 45.0], [-788758.63, -620642.12]), ('control #15', [-3.245, 2.365, 10.0, 52.0, 35.0, 65.0], [-1927899.63, -5755732.58]), ('control #16', [150.921, -43.877, 132.0, 0.0, -18.0, -36.0], [1562347.67, -5075599.0]), ('control #17', [-99.291, 2.109, -60.0, -32.0, -5.0, -42.0], [-4518513.04, 3061080.05]), ('control #18', [28.411, 1.618, 0.0, 40.0, 40.0, 40.0], [3815361.61, -3969481.26]), ('control #19', [0.654, -74.153, 20.0, -45.0, -45.0, -45.0], [-696797.89, -3508209.88])]]
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[944572.37, -3177093.78][944572.37, -3177093.78]Passed
control #1[3477264.95, -2333351.52][3477264.95, -2333351.52]Passed
control #2[182997.23, -2612489.01][182997.23, -2612489.01]Passed
control #3[-2030234.59, 3881165.57][-2030234.59, 3881165.57]Passed
control #4[-353408.33, 2543656.82][-353408.33, 2543656.82]Passed
control #5[4256679.54, 2162268.41][4256679.54, 2162268.41]Passed
control #6[519881.13, 3212621.55][519881.13, 3212621.55]Passed
control #7[-2281076.94, 4475835.48][-2281076.94, 4475835.48]Passed

SHA-256 / 4dab6fdcbadcd7cacf9d9e572421ad0aaa1eedb21eac404f1d401d49fdd0468d

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

Case digest / 04c7abc12691b904d44cbef872a33703951f65ff995d8fd6e88a364b0fbf668d