FAILURE MAP
← Case archive

FA-69851 / Map projection transforms / Open access

Spherical web mercator forward projection: half-angle term · case 01

Northings are wildly exaggerated even at mid latitudes.

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

ROOT CAUSE

The isometric latitude uses tan(pi/4 + phi) instead of tan(pi/4 + phi/2), doubling the latitude inside the tangent.

VERIFIED REPAIR

At the half-angle term step restore `math.pi / 4 + math.radians(lat) / 2`, leaving the rest of the model unchanged.

Unsuccessful approach: The halving was restored but the latitude is no longer converted to radians.

Case contract

Input [lon, lat] in degrees. Longitude is first wrapped into the half-open range [-180, 180); latitude is clamped to +/-85.05112878. Return [x, y] metres on a sphere of radius 6378137 with y growing northward, each rounded to 3 decimals.

Why this case matters

Web map tiles, vector tile encoders and map viewers all start from this forward transform; small mistakes shift features by kilometres or mirror them across the equator.

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 = x
    lon = ((lon + 180.0) % 360.0) - 180.0
    lat = max(-85.05112878, min(85.05112878, lat))
    R = 6378137.0
    px = R * math.radians(lon)
    py = R * math.log(math.tan(math.pi / 4 + math.radians(lat)))
    return [round(px, 3), round(py, 3)]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('regression #1', [-200.0, -20.0], [17811118.527, -2273030.927]), ('boundary #2', [180.0, 0.0], [-20037508.343, -0.0]), ('boundary #3', [-180.0, 45.0], [-20037508.343, 5621521.486]), ('regression #4', [545.0, 5.0], [-19480910.889, 557305.257]), ('control #8', [0.0, 0.0], [0.0, -0.0]), ('regression #9', [-540.0, 30.0], [-20037508.343, 3503549.844]), ('regression #10', [-250.0, 12.5], [12245143.987, 1402665.19]), ('regression #14', [-181.0, 0.5], [19926188.852, 55660.452])], [('boundary #2', [180.0, 0.0], [-20037508.343, -0.0]), ('boundary #3', [-180.0, 45.0], [-20037508.343, 5621521.486]), ('regression #4', [545.0, 5.0], [-19480910.889, 557305.257]), ('control #8', [0.0, 0.0], [0.0, -0.0]), ('regression #9', [-540.0, 30.0], [-20037508.343, 3503549.844]), ('regression #10', [-250.0, 12.5], [12245143.987, 1402665.19]), ('regression #14', [-181.0, 0.5], [19926188.852, 55660.452]), ('control #16', [151.867, -5.771], [16905757.108, -643513.79])], [('boundary #2', [180.0, 0.0], [-20037508.343, -0.0]), ('regression #4', [545.0, 5.0], [-19480910.889, 557305.257]), ('control #8', [0.0, 0.0], [0.0, -0.0]), ('regression #9', [-540.0, 30.0], [-20037508.343, 3503549.844]), ('control #16', [151.867, -5.771], [16905757.108, -643513.79]), ('control #24', [55.104, 19.415], [6134149.221, 2203856.898]), ('control #27', [-110.905, -43.354], [-12345888.126, -5366010.423]), ('control #28', [-168.23, -6.059], [-18727277.936, -675745.443])], [('regression #1', [-200.0, -20.0], [17811118.527, -2273030.927]), ('boundary #2', [180.0, 0.0], [-20037508.343, -0.0]), ('control #8', [0.0, 0.0], [0.0, -0.0]), ('regression #9', [-540.0, 30.0], [-20037508.343, 3503549.844]), ('regression #10', [-250.0, 12.5], [12245143.987, 1402665.19]), ('control #28', [-168.23, -6.059], [-18727277.936, -675745.443]), ('control #30', [6.846, 23.569], [762093.234, 2700976.142]), ('control #32', [-15.276, -37.269], [-1700516.541, -4476668.54])], [('regression #1', [-200.0, -20.0], [17811118.527, -2273030.927]), ('boundary #2', [180.0, 0.0], [-20037508.343, -0.0]), ('boundary #3', [-180.0, 45.0], [-20037508.343, 5621521.486]), ('regression #4', [545.0, 5.0], [-19480910.889, 557305.257]), ('control #8', [0.0, 0.0], [0.0, -0.0]), ('regression #9', [-540.0, 30.0], [-20037508.343, 3503549.844]), ('regression #10', [-250.0, 12.5], [12245143.987, 1402665.19]), ('regression #14', [-181.0, 0.5], [19926188.852, 55660.452])]]
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
regression #1[17811118.527, -4865942.28][17811118.527, -2273030.927]Failed
boundary #2[-20037508.343, -0.0][-20037508.343, -0.0]Passed
boundary #3[-20037508.343, 238107693.265][-20037508.343, 5621521.486]Failed
regression #4[-19480910.889, 1118889.975][-19480910.889, 557305.257]Failed
control #8[0.0, -0.0][0.0, -0.0]Passed
regression #9[-20037508.343, 8399737.89][-20037508.343, 3503549.844]Failed
regression #10[12245143.987, 2875744.624][12245143.987, 1402665.19]Failed
regression #14[19926188.852, 111325.143][19926188.852, 55660.452]Failed

SHA-256 / 9090b65603d15fe5738e2db2bbb641bcea13b7295ec43314d281c8ce27ce4d8d

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 = x
    lon = ((lon + 180.0) % 360.0) - 180.0
    lat = max(-85.05112878, min(85.05112878, lat))
    R = 6378137.0
    px = R * math.radians(lon)
    py = R * math.log(math.tan(math.pi / 4 + lat / 2))
    return [round(px, 3), round(py, 3)]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('regression #1', [-200.0, -20.0], [17811118.527, -2273030.927]), ('boundary #2', [180.0, 0.0], [-20037508.343, -0.0]), ('boundary #3', [-180.0, 45.0], [-20037508.343, 5621521.486]), ('regression #4', [545.0, 5.0], [-19480910.889, 557305.257]), ('control #8', [0.0, 0.0], [0.0, -0.0]), ('regression #9', [-540.0, 30.0], [-20037508.343, 3503549.844]), ('regression #10', [-250.0, 12.5], [12245143.987, 1402665.19]), ('regression #14', [-181.0, 0.5], [19926188.852, 55660.452])], [('boundary #2', [180.0, 0.0], [-20037508.343, -0.0]), ('boundary #3', [-180.0, 45.0], [-20037508.343, 5621521.486]), ('regression #4', [545.0, 5.0], [-19480910.889, 557305.257]), ('control #8', [0.0, 0.0], [0.0, -0.0]), ('regression #9', [-540.0, 30.0], [-20037508.343, 3503549.844]), ('regression #10', [-250.0, 12.5], [12245143.987, 1402665.19]), ('regression #14', [-181.0, 0.5], [19926188.852, 55660.452]), ('control #16', [151.867, -5.771], [16905757.108, -643513.79])], [('boundary #2', [180.0, 0.0], [-20037508.343, -0.0]), ('regression #4', [545.0, 5.0], [-19480910.889, 557305.257]), ('control #8', [0.0, 0.0], [0.0, -0.0]), ('regression #9', [-540.0, 30.0], [-20037508.343, 3503549.844]), ('control #16', [151.867, -5.771], [16905757.108, -643513.79]), ('control #24', [55.104, 19.415], [6134149.221, 2203856.898]), ('control #27', [-110.905, -43.354], [-12345888.126, -5366010.423]), ('control #28', [-168.23, -6.059], [-18727277.936, -675745.443])], [('regression #1', [-200.0, -20.0], [17811118.527, -2273030.927]), ('boundary #2', [180.0, 0.0], [-20037508.343, -0.0]), ('control #8', [0.0, 0.0], [0.0, -0.0]), ('regression #9', [-540.0, 30.0], [-20037508.343, 3503549.844]), ('regression #10', [-250.0, 12.5], [12245143.987, 1402665.19]), ('control #28', [-168.23, -6.059], [-18727277.936, -675745.443]), ('control #30', [6.846, 23.569], [762093.234, 2700976.142]), ('control #32', [-15.276, -37.269], [-1700516.541, -4476668.54])], [('regression #1', [-200.0, -20.0], [17811118.527, -2273030.927]), ('boundary #2', [180.0, 0.0], [-20037508.343, -0.0]), ('boundary #3', [-180.0, 45.0], [-20037508.343, 5621521.486]), ('regression #4', [545.0, 5.0], [-19480910.889, 557305.257]), ('control #8', [0.0, 0.0], [0.0, -0.0]), ('regression #9', [-540.0, 30.0], [-20037508.343, 3503549.844]), ('regression #10', [-250.0, 12.5], [12245143.987, 1402665.19]), ('regression #14', [-181.0, 0.5], [19926188.852, 55660.452])]]
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
regression #1[17811118.527, -9853781.978][17811118.527, -2273030.927]Failed
boundary #2[-20037508.343, -0.0][-20037508.343, -0.0]Passed
boundary #3[-20037508.343, 8032739.293][-20037508.343, 5621521.486]Failed
regression #4[-19480910.889, -12324899.673][-19480910.889, 557305.257]Failed
control #8[0.0, -0.0][0.0, -0.0]Passed
regression #9[-20037508.343, -16304535.145][-20037508.343, 3503549.844]Failed
regression #10[12245143.987, -423632.006][12245143.987, 1402665.19]Failed
regression #14[19926188.852, 3330906.169][19926188.852, 55660.452]Failed

SHA-256 / 90ae36c9e8750157877c1800e55c184da1e6502030491842bae08c56e3d3770a

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 = x
    lon = ((lon + 180.0) % 360.0) - 180.0
    lat = max(-85.05112878, min(85.05112878, lat))
    R = 6378137.0
    px = R * math.radians(lon)
    py = R * math.log(math.tan(math.pi / 4 + math.radians(lat) / 2))
    return [round(px, 3), round(py, 3)]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('regression #1', [-200.0, -20.0], [17811118.527, -2273030.927]), ('boundary #2', [180.0, 0.0], [-20037508.343, -0.0]), ('boundary #3', [-180.0, 45.0], [-20037508.343, 5621521.486]), ('regression #4', [545.0, 5.0], [-19480910.889, 557305.257]), ('control #8', [0.0, 0.0], [0.0, -0.0]), ('regression #9', [-540.0, 30.0], [-20037508.343, 3503549.844]), ('regression #10', [-250.0, 12.5], [12245143.987, 1402665.19]), ('regression #14', [-181.0, 0.5], [19926188.852, 55660.452])], [('boundary #2', [180.0, 0.0], [-20037508.343, -0.0]), ('boundary #3', [-180.0, 45.0], [-20037508.343, 5621521.486]), ('regression #4', [545.0, 5.0], [-19480910.889, 557305.257]), ('control #8', [0.0, 0.0], [0.0, -0.0]), ('regression #9', [-540.0, 30.0], [-20037508.343, 3503549.844]), ('regression #10', [-250.0, 12.5], [12245143.987, 1402665.19]), ('regression #14', [-181.0, 0.5], [19926188.852, 55660.452]), ('control #16', [151.867, -5.771], [16905757.108, -643513.79])], [('boundary #2', [180.0, 0.0], [-20037508.343, -0.0]), ('regression #4', [545.0, 5.0], [-19480910.889, 557305.257]), ('control #8', [0.0, 0.0], [0.0, -0.0]), ('regression #9', [-540.0, 30.0], [-20037508.343, 3503549.844]), ('control #16', [151.867, -5.771], [16905757.108, -643513.79]), ('control #24', [55.104, 19.415], [6134149.221, 2203856.898]), ('control #27', [-110.905, -43.354], [-12345888.126, -5366010.423]), ('control #28', [-168.23, -6.059], [-18727277.936, -675745.443])], [('regression #1', [-200.0, -20.0], [17811118.527, -2273030.927]), ('boundary #2', [180.0, 0.0], [-20037508.343, -0.0]), ('control #8', [0.0, 0.0], [0.0, -0.0]), ('regression #9', [-540.0, 30.0], [-20037508.343, 3503549.844]), ('regression #10', [-250.0, 12.5], [12245143.987, 1402665.19]), ('control #28', [-168.23, -6.059], [-18727277.936, -675745.443]), ('control #30', [6.846, 23.569], [762093.234, 2700976.142]), ('control #32', [-15.276, -37.269], [-1700516.541, -4476668.54])], [('regression #1', [-200.0, -20.0], [17811118.527, -2273030.927]), ('boundary #2', [180.0, 0.0], [-20037508.343, -0.0]), ('boundary #3', [-180.0, 45.0], [-20037508.343, 5621521.486]), ('regression #4', [545.0, 5.0], [-19480910.889, 557305.257]), ('control #8', [0.0, 0.0], [0.0, -0.0]), ('regression #9', [-540.0, 30.0], [-20037508.343, 3503549.844]), ('regression #10', [-250.0, 12.5], [12245143.987, 1402665.19]), ('regression #14', [-181.0, 0.5], [19926188.852, 55660.452])]]
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
regression #1[17811118.527, -2273030.927][17811118.527, -2273030.927]Passed
boundary #2[-20037508.343, -0.0][-20037508.343, -0.0]Passed
boundary #3[-20037508.343, 5621521.486][-20037508.343, 5621521.486]Passed
regression #4[-19480910.889, 557305.257][-19480910.889, 557305.257]Passed
control #8[0.0, -0.0][0.0, -0.0]Passed
regression #9[-20037508.343, 3503549.844][-20037508.343, 3503549.844]Passed
regression #10[12245143.987, 1402665.19][12245143.987, 1402665.19]Passed
regression #14[19926188.852, 55660.452][19926188.852, 55660.452]Passed

SHA-256 / fee2a80c1f4f472122733f166bc8ec407a712ddd295d470621e84bd94faa020c

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

Case digest / 826f17626088ea6a8223dd2181bca6fa11e536d6fd7fc7e0839ea2862cfe5477