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.
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 fixture | Actual | Expected | Outcome |
|---|---|---|---|
| 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 fixture | Actual | Expected | Outcome |
|---|---|---|---|
| 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 fixture | Actual | Expected | Outcome |
|---|---|---|---|
| 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