FA-70031 / Map projection transforms / Open access
Spherical transverse mercator in a UTM grid: northing longitude term · case 01
Northings are wrong for points away from the central meridian.
ROOT CAUSE
The meridional term ignores cos(dl) and reduces to the plain latitude.
VERIFIED REPAIR
At the northing longitude term step restore `math.atan2(math.tan(phi), math.cos(dl))`, leaving the rest of the model unchanged.
Unsuccessful approach: Using sin(phi) instead of tan(phi) in the numerator changes the northing everywhere except the equator.
Case contract
Input [lon, lat, zone, hemisphere] with |lat| <= 80 and lon within 4 degrees of the zone central meridian 6*zone-183. On a sphere R = 6371000 with k0 = 0.9996: B = cos(phi)*sin(dl); x = 0.5*k0*R*ln((1+B)/(1-B)); y = k0*R*atan2(tan(phi), cos(dl)). Easting = 500000 + x. Northing = y plus 10000000 when the requested grid hemisphere is "S" (the caller may deliberately project points just north of the equator in a southern grid). Return [easting, northing] rounded to 2 decimals.
Why this case matters
Survey exports and GPS receivers express positions as UTM eastings and northings; a wrong false origin or meridian moves points by hundreds of kilometres.
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, zone, hemi = x
R = 6371000.0
k0 = 0.9996
lon0 = zone * 6 - 183
dl = math.radians(lon - lon0)
phi = math.radians(lat)
B = math.cos(phi) * math.sin(dl)
xm = 0.5 * k0 * R * math.log((1 + B) / (1 - B))
ym = k0 * R * math.atan2(math.tan(phi), 1.0)
easting = 500000.0 + xm
northing = ym + (10000000.0 if hemi == 'S' else 0.0)
return [round(easting, 2), round(northing, 2)]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('control #0', [-146.456, 65.451, 6, 'S'], [525121.58, 17275016.5]), ('control #1', [-24.305, 48.206, 26, 'S'], [699628.48, 15361620.03]), ('control #2', [-3.252, -34.222, 30, 'N'], [476839.56, -3803819.3]), ('control #3', [140.223, -1.339, 54, 'S'], [413657.04, 9851155.87]), ('control #4', [-99.681, -57.299, 14, 'S'], [459106.63, 3630985.93]), ('control #5', [131.956, -7.201, 52, 'S'], [826109.36, 9198550.44]), ('control #6', [-63.683, 39.81, 20, 'S'], [441683.41, 14425121.91]), ('boundary #16', [15.0, 0.0, 33, 'N'], [500000.0, 0.0])], [('control #1', [-24.305, 48.206, 26, 'S'], [699628.48, 15361620.03]), ('control #2', [-3.252, -34.222, 30, 'N'], [476839.56, -3803819.3]), ('control #5', [131.956, -7.201, 52, 'S'], [826109.36, 9198550.44]), ('control #6', [-63.683, 39.81, 20, 'S'], [441683.41, 14425121.91]), ('control #7', [-31.602, 39.795, 25, 'S'], [619393.11, 14424164.48]), ('control #8', [87.283, -45.722, 45, 'N'], [521960.41, -5082059.64]), ('control #9', [74.243, 54.044, 43, 'S'], [450595.96, 16007279.04]), ('boundary #16', [15.0, 0.0, 33, 'N'], [500000.0, 0.0])], [('control #2', [-3.252, -34.222, 30, 'N'], [476839.56, -3803819.3]), ('control #3', [140.223, -1.339, 54, 'S'], [413657.04, 9851155.87]), ('control #8', [87.283, -45.722, 45, 'N'], [521960.41, -5082059.64]), ('control #9', [74.243, 54.044, 43, 'S'], [450595.96, 16007279.04]), ('control #10', [-96.557, 39.544, 14, 'S'], [709406.66, 14398176.63]), ('control #11', [162.356, -15.269, 58, 'N'], [216405.53, -1698880.3]), ('control #12', [-107.58, 5.652, 13, 'N'], [214531.37, 628855.65]), ('boundary #16', [15.0, 0.0, 33, 'N'], [500000.0, 0.0])], [('control #3', [140.223, -1.339, 54, 'S'], [413657.04, 9851155.87]), ('control #4', [-99.681, -57.299, 14, 'S'], [459106.63, 3630985.93]), ('control #11', [162.356, -15.269, 58, 'N'], [216405.53, -1698880.3]), ('control #12', [-107.58, 5.652, 13, 'N'], [214531.37, 628855.65]), ('control #13', [100.982, 3.293, 47, 'S'], [719980.03, 10366237.05]), ('regression #14', [9.0, 0.5, 32, 'S'], [500000.0, 10055575.22]), ('regression #15', [-75.5, 0.2, 18, 'S'], [444424.41, 10022230.94]), ('boundary #16', [15.0, 0.0, 33, 'N'], [500000.0, 0.0])], [('control #4', [-99.681, -57.299, 14, 'S'], [459106.63, 3630985.93]), ('control #5', [131.956, -7.201, 52, 'S'], [826109.36, 9198550.44]), ('regression #14', [9.0, 0.5, 32, 'S'], [500000.0, 10055575.22]), ('regression #15', [-75.5, 0.2, 18, 'S'], [444424.41, 10022230.94]), ('boundary #16', [15.0, 0.0, 33, 'N'], [500000.0, 0.0]), ('regression #17', [30.0, -0.3, 36, 'N'], [166400.77, -33390.89]), ('boundary #18', [3.0, 45.0, 31, 'N'], [500000.0, 5001770.19]), ('control #19', [12.3, -35.0, 33, 'S'], [254136.28, 6106410.11])]]
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 | [525121.58, 17274908.02] | [525121.58, 17275016.5] | Failed |
| control #1 | [699628.48, 15358118.53] | [699628.48, 15361620.03] | Failed |
| control #2 | [476839.56, -3803790.65] | [476839.56, -3803819.3] | Failed |
| control #3 | [413657.04, 9851169.55] | [413657.04, 9851155.87] | Failed |
| control #4 | [459106.63, 3631190.44] | [459106.63, 3630985.93] | Failed |
| control #5 | [826109.36, 9199605.62] | [826109.36, 9198550.44] | Failed |
| control #6 | [441683.41, 14424899.36] | [441683.41, 14425121.91] | Failed |
| boundary #16 | [500000.0, 0.0] | [500000.0, 0.0] | Passed |
SHA-256 / 29db67c6e97d30ce24d39c4992300ce0aed670d689ff4607d1b81631fde2d40d
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, zone, hemi = x
R = 6371000.0
k0 = 0.9996
lon0 = zone * 6 - 183
dl = math.radians(lon - lon0)
phi = math.radians(lat)
B = math.cos(phi) * math.sin(dl)
xm = 0.5 * k0 * R * math.log((1 + B) / (1 - B))
ym = k0 * R * math.atan2(math.sin(phi), math.cos(dl))
easting = 500000.0 + xm
northing = ym + (10000000.0 if hemi == 'S' else 0.0)
return [round(easting, 2), round(northing, 2)]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('control #0', [-146.456, 65.451, 6, 'S'], [525121.58, 17275016.5]), ('control #1', [-24.305, 48.206, 26, 'S'], [699628.48, 15361620.03]), ('control #2', [-3.252, -34.222, 30, 'N'], [476839.56, -3803819.3]), ('control #3', [140.223, -1.339, 54, 'S'], [413657.04, 9851155.87]), ('control #4', [-99.681, -57.299, 14, 'S'], [459106.63, 3630985.93]), ('control #5', [131.956, -7.201, 52, 'S'], [826109.36, 9198550.44]), ('control #6', [-63.683, 39.81, 20, 'S'], [441683.41, 14425121.91]), ('boundary #16', [15.0, 0.0, 33, 'N'], [500000.0, 0.0])], [('control #1', [-24.305, 48.206, 26, 'S'], [699628.48, 15361620.03]), ('control #2', [-3.252, -34.222, 30, 'N'], [476839.56, -3803819.3]), ('control #5', [131.956, -7.201, 52, 'S'], [826109.36, 9198550.44]), ('control #6', [-63.683, 39.81, 20, 'S'], [441683.41, 14425121.91]), ('control #7', [-31.602, 39.795, 25, 'S'], [619393.11, 14424164.48]), ('control #8', [87.283, -45.722, 45, 'N'], [521960.41, -5082059.64]), ('control #9', [74.243, 54.044, 43, 'S'], [450595.96, 16007279.04]), ('boundary #16', [15.0, 0.0, 33, 'N'], [500000.0, 0.0])], [('control #2', [-3.252, -34.222, 30, 'N'], [476839.56, -3803819.3]), ('control #3', [140.223, -1.339, 54, 'S'], [413657.04, 9851155.87]), ('control #8', [87.283, -45.722, 45, 'N'], [521960.41, -5082059.64]), ('control #9', [74.243, 54.044, 43, 'S'], [450595.96, 16007279.04]), ('control #10', [-96.557, 39.544, 14, 'S'], [709406.66, 14398176.63]), ('control #11', [162.356, -15.269, 58, 'N'], [216405.53, -1698880.3]), ('control #12', [-107.58, 5.652, 13, 'N'], [214531.37, 628855.65]), ('boundary #16', [15.0, 0.0, 33, 'N'], [500000.0, 0.0])], [('control #3', [140.223, -1.339, 54, 'S'], [413657.04, 9851155.87]), ('control #4', [-99.681, -57.299, 14, 'S'], [459106.63, 3630985.93]), ('control #11', [162.356, -15.269, 58, 'N'], [216405.53, -1698880.3]), ('control #12', [-107.58, 5.652, 13, 'N'], [214531.37, 628855.65]), ('control #13', [100.982, 3.293, 47, 'S'], [719980.03, 10366237.05]), ('regression #14', [9.0, 0.5, 32, 'S'], [500000.0, 10055575.22]), ('regression #15', [-75.5, 0.2, 18, 'S'], [444424.41, 10022230.94]), ('boundary #16', [15.0, 0.0, 33, 'N'], [500000.0, 0.0])], [('control #4', [-99.681, -57.299, 14, 'S'], [459106.63, 3630985.93]), ('control #5', [131.956, -7.201, 52, 'S'], [826109.36, 9198550.44]), ('regression #14', [9.0, 0.5, 32, 'S'], [500000.0, 10055575.22]), ('regression #15', [-75.5, 0.2, 18, 'S'], [444424.41, 10022230.94]), ('boundary #16', [15.0, 0.0, 33, 'N'], [500000.0, 0.0]), ('regression #17', [30.0, -0.3, 36, 'N'], [166400.77, -33390.89]), ('boundary #18', [3.0, 45.0, 31, 'N'], [500000.0, 5001770.19]), ('control #19', [12.3, -35.0, 33, 'S'], [254136.28, 6106410.11])]]
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 | [525121.58, 14700678.95] | [525121.58, 17275016.5] | Failed |
| control #1 | [699628.48, 14083289.98] | [699628.48, 15361620.03] | Failed |
| control #2 | [476839.56, -3262674.42] | [476839.56, -3803819.3] | Failed |
| control #3 | [413657.04, 9851196.5] | [413657.04, 9851155.87] | Failed |
| control #4 | [459106.63, 5544795.43] | [459106.63, 3630985.93] | Failed |
| control #5 | [826109.36, 9204806.1] | [826109.36, 9198550.44] | Failed |
| control #6 | [441683.41, 13626950.24] | [441683.41, 14425121.91] | Failed |
| boundary #16 | [500000.0, 0.0] | [500000.0, 0.0] | Passed |
SHA-256 / dd4881595534cd91f8848dfa0e5ef57a37e80baf066ac8cd4199c50b43efc9fb
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, zone, hemi = x
R = 6371000.0
k0 = 0.9996
lon0 = zone * 6 - 183
dl = math.radians(lon - lon0)
phi = math.radians(lat)
B = math.cos(phi) * math.sin(dl)
xm = 0.5 * k0 * R * math.log((1 + B) / (1 - B))
ym = k0 * R * math.atan2(math.tan(phi), math.cos(dl))
easting = 500000.0 + xm
northing = ym + (10000000.0 if hemi == 'S' else 0.0)
return [round(easting, 2), round(northing, 2)]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('control #0', [-146.456, 65.451, 6, 'S'], [525121.58, 17275016.5]), ('control #1', [-24.305, 48.206, 26, 'S'], [699628.48, 15361620.03]), ('control #2', [-3.252, -34.222, 30, 'N'], [476839.56, -3803819.3]), ('control #3', [140.223, -1.339, 54, 'S'], [413657.04, 9851155.87]), ('control #4', [-99.681, -57.299, 14, 'S'], [459106.63, 3630985.93]), ('control #5', [131.956, -7.201, 52, 'S'], [826109.36, 9198550.44]), ('control #6', [-63.683, 39.81, 20, 'S'], [441683.41, 14425121.91]), ('boundary #16', [15.0, 0.0, 33, 'N'], [500000.0, 0.0])], [('control #1', [-24.305, 48.206, 26, 'S'], [699628.48, 15361620.03]), ('control #2', [-3.252, -34.222, 30, 'N'], [476839.56, -3803819.3]), ('control #5', [131.956, -7.201, 52, 'S'], [826109.36, 9198550.44]), ('control #6', [-63.683, 39.81, 20, 'S'], [441683.41, 14425121.91]), ('control #7', [-31.602, 39.795, 25, 'S'], [619393.11, 14424164.48]), ('control #8', [87.283, -45.722, 45, 'N'], [521960.41, -5082059.64]), ('control #9', [74.243, 54.044, 43, 'S'], [450595.96, 16007279.04]), ('boundary #16', [15.0, 0.0, 33, 'N'], [500000.0, 0.0])], [('control #2', [-3.252, -34.222, 30, 'N'], [476839.56, -3803819.3]), ('control #3', [140.223, -1.339, 54, 'S'], [413657.04, 9851155.87]), ('control #8', [87.283, -45.722, 45, 'N'], [521960.41, -5082059.64]), ('control #9', [74.243, 54.044, 43, 'S'], [450595.96, 16007279.04]), ('control #10', [-96.557, 39.544, 14, 'S'], [709406.66, 14398176.63]), ('control #11', [162.356, -15.269, 58, 'N'], [216405.53, -1698880.3]), ('control #12', [-107.58, 5.652, 13, 'N'], [214531.37, 628855.65]), ('boundary #16', [15.0, 0.0, 33, 'N'], [500000.0, 0.0])], [('control #3', [140.223, -1.339, 54, 'S'], [413657.04, 9851155.87]), ('control #4', [-99.681, -57.299, 14, 'S'], [459106.63, 3630985.93]), ('control #11', [162.356, -15.269, 58, 'N'], [216405.53, -1698880.3]), ('control #12', [-107.58, 5.652, 13, 'N'], [214531.37, 628855.65]), ('control #13', [100.982, 3.293, 47, 'S'], [719980.03, 10366237.05]), ('regression #14', [9.0, 0.5, 32, 'S'], [500000.0, 10055575.22]), ('regression #15', [-75.5, 0.2, 18, 'S'], [444424.41, 10022230.94]), ('boundary #16', [15.0, 0.0, 33, 'N'], [500000.0, 0.0])], [('control #4', [-99.681, -57.299, 14, 'S'], [459106.63, 3630985.93]), ('control #5', [131.956, -7.201, 52, 'S'], [826109.36, 9198550.44]), ('regression #14', [9.0, 0.5, 32, 'S'], [500000.0, 10055575.22]), ('regression #15', [-75.5, 0.2, 18, 'S'], [444424.41, 10022230.94]), ('boundary #16', [15.0, 0.0, 33, 'N'], [500000.0, 0.0]), ('regression #17', [30.0, -0.3, 36, 'N'], [166400.77, -33390.89]), ('boundary #18', [3.0, 45.0, 31, 'N'], [500000.0, 5001770.19]), ('control #19', [12.3, -35.0, 33, 'S'], [254136.28, 6106410.11])]]
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 | [525121.58, 17275016.5] | [525121.58, 17275016.5] | Passed |
| control #1 | [699628.48, 15361620.03] | [699628.48, 15361620.03] | Passed |
| control #2 | [476839.56, -3803819.3] | [476839.56, -3803819.3] | Passed |
| control #3 | [413657.04, 9851155.87] | [413657.04, 9851155.87] | Passed |
| control #4 | [459106.63, 3630985.93] | [459106.63, 3630985.93] | Passed |
| control #5 | [826109.36, 9198550.44] | [826109.36, 9198550.44] | Passed |
| control #6 | [441683.41, 14425121.91] | [441683.41, 14425121.91] | Passed |
| boundary #16 | [500000.0, 0.0] | [500000.0, 0.0] | Passed |
SHA-256 / 2a839282c7721d2c42f0c52bc3fe1e296c0ccb3dab9632a3c15b7f7b0dffefb0
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:16.949860+00:00.
Case digest / ba85a0245666d4c43787679812f23b1c58260da45484ef60943c955ac786b9c3