FAILURE MAP
← Case archive

FA-70101 / Map projection transforms / Open access

Spherical Lambert conformal conic forward: secant ratio order · case 01

Secant cones open the wrong way and produce mirrored, inflated output.

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

ROOT CAUSE

The denominator ratio of the cone constant is inverted, negating n.

VERIFIED REPAIR

At the secant ratio order step restore `math.log(t(p2) / t(p1))`, leaving the rest of the model unchanged.

Unsuccessful approach: Taking the absolute value repairs northern secant cones but forces southern cones to a positive n.

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(p1) / t(p2))
    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 #11', [-30.764, 26.528, 0.0, 40.0, 40.0, 40.0], [-3080105.21, -974518.51])], [('control #1', [43.173, 23.809, 10.0, 52.0, 35.0, 65.0], [3477264.95, -2333351.52]), ('control #3', [-77.146, 4.744, -60.0, -32.0, -5.0, -42.0], [-2030234.59, 3881165.57]), ('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 #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 #2', [133.802, -22.769, 132.0, 0.0, -18.0, -36.0], [182997.23, -2612489.01]), ('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 #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 #18', [28.411, 1.618, 0.0, 40.0, 40.0, 40.0], [3815361.61, -3969481.26])], [('control #3', [-77.146, 4.744, -60.0, -32.0, -5.0, -42.0], [-2030234.59, 3881165.57]), ('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 #14', [-102.998, 17.408, -96.0, 23.0, 33.0, 45.0], [-788758.63, -620642.12]), ('control #16', [150.921, -43.877, 132.0, 0.0, -18.0, -36.0], [1562347.67, -5075599.0]), ('control #18', [28.411, 1.618, 0.0, 40.0, 40.0, 40.0], [3815361.61, -3969481.26])], [('control #6', [108.376, 58.574, 100.0, 30.0, 25.0, 47.0], [519881.13, 3212621.55]), ('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 #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]), ('control #25', [24.182, 45.341, 0.0, 40.0, 40.0, 40.0], [1875269.88, 850716.48])]]
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[413168.19, -1890168.26][944572.37, -3177093.78]Failed
control #1[2453938.75, -4179542.56][3477264.95, -2333351.52]Failed
control #2[198434.83, -2349156.72][182997.23, -2612489.01]Failed
control #3[-1768071.12, 4690788.93][-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[1367124.51, 5424191.38][519881.13, 3212621.55]Failed
control #11[-3080105.21, -974518.51][-3080105.21, -974518.51]Passed

SHA-256 / 9520851c2a4be341a7d5ed139c9f44b6142cf7793ea9da3df421c80a895a94ec

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)) / abs(math.log(t(p1) / t(p2)))
    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 #11', [-30.764, 26.528, 0.0, 40.0, 40.0, 40.0], [-3080105.21, -974518.51])], [('control #1', [43.173, 23.809, 10.0, 52.0, 35.0, 65.0], [3477264.95, -2333351.52]), ('control #3', [-77.146, 4.744, -60.0, -32.0, -5.0, -42.0], [-2030234.59, 3881165.57]), ('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 #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 #2', [133.802, -22.769, 132.0, 0.0, -18.0, -36.0], [182997.23, -2612489.01]), ('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 #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 #18', [28.411, 1.618, 0.0, 40.0, 40.0, 40.0], [3815361.61, -3969481.26])], [('control #3', [-77.146, 4.744, -60.0, -32.0, -5.0, -42.0], [-2030234.59, 3881165.57]), ('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 #14', [-102.998, 17.408, -96.0, 23.0, 33.0, 45.0], [-788758.63, -620642.12]), ('control #16', [150.921, -43.877, 132.0, 0.0, -18.0, -36.0], [1562347.67, -5075599.0]), ('control #18', [28.411, 1.618, 0.0, 40.0, 40.0, 40.0], [3815361.61, -3969481.26])], [('control #6', [108.376, 58.574, 100.0, 30.0, 25.0, 47.0], [519881.13, 3212621.55]), ('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 #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]), ('control #25', [24.182, 45.341, 0.0, 40.0, 40.0, 40.0], [1875269.88, 850716.48])]]
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[198434.83, -2349156.72][182997.23, -2612489.01]Failed
control #3[-1768071.12, 4690788.93][-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, 3212621.55][519881.13, 3212621.55]Passed
control #11[-3080105.21, -974518.51][-3080105.21, -974518.51]Passed

SHA-256 / f0f1c6bf9cfa60199c96578f5554e5c6ec1eedcc1e13892adeb1b47d6a32ff1d

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 #11', [-30.764, 26.528, 0.0, 40.0, 40.0, 40.0], [-3080105.21, -974518.51])], [('control #1', [43.173, 23.809, 10.0, 52.0, 35.0, 65.0], [3477264.95, -2333351.52]), ('control #3', [-77.146, 4.744, -60.0, -32.0, -5.0, -42.0], [-2030234.59, 3881165.57]), ('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 #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 #2', [133.802, -22.769, 132.0, 0.0, -18.0, -36.0], [182997.23, -2612489.01]), ('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 #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 #18', [28.411, 1.618, 0.0, 40.0, 40.0, 40.0], [3815361.61, -3969481.26])], [('control #3', [-77.146, 4.744, -60.0, -32.0, -5.0, -42.0], [-2030234.59, 3881165.57]), ('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 #14', [-102.998, 17.408, -96.0, 23.0, 33.0, 45.0], [-788758.63, -620642.12]), ('control #16', [150.921, -43.877, 132.0, 0.0, -18.0, -36.0], [1562347.67, -5075599.0]), ('control #18', [28.411, 1.618, 0.0, 40.0, 40.0, 40.0], [3815361.61, -3969481.26])], [('control #6', [108.376, 58.574, 100.0, 30.0, 25.0, 47.0], [519881.13, 3212621.55]), ('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 #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]), ('control #25', [24.182, 45.341, 0.0, 40.0, 40.0, 40.0], [1875269.88, 850716.48])]]
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 #11[-3080105.21, -974518.51][-3080105.21, -974518.51]Passed

SHA-256 / bd78d5ac883b1b1ce0f05c907cfe1b5e30034fad7bb4f4d201988dc82a733ddb

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

Case digest / 864c7f02de79066d998ae53c657aa65e53394b00e049ed206274a67f18649f03