FA-70151 / Map projection transforms / Open access
Spherical polar stereographic forward: hemisphere acceptance · case 01
Equator points are rejected by both polar grids.
ROOT CAUSE
The hemisphere guard treats the equator as outside both aspects.
VERIFIED REPAIR
At the hemisphere acceptance step restore `(pole == 'N' and lat < 0) or (pole == 'S' and lat > 0)`, leaving the rest of the model unchanged.
Unsuccessful approach: The north guard was fixed but the south guard still rejects the equator.
Case contract
Input [lon, lat, pole, lon0, k0]. pole "N" accepts lat >= 0 and "S" accepts lat <= 0; other points return None. Sphere R = 6371000, dl = rad(lon - lon0). North: rho = 2*R*k0*tan(pi/4 - phi/2), x = rho*sin(dl), y = -rho*cos(dl). South: rho = 2*R*k0*tan(pi/4 + phi/2), x = rho*sin(dl), y = rho*cos(dl). Rounded to 2 decimals.
Why this case matters
Sea-ice, Antarctic and Arctic products are gridded in polar stereographic; a sign slip rotates whole continents.
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, pole, lon0, k0 = x
R = 6371000.0
if (pole == 'N' and lat <= 0) or (pole == 'S' and lat >= 0):
return None
phi = math.radians(lat)
dl = math.radians(lon - lon0)
if pole == 'N':
rho = 2 * R * k0 * math.tan(math.pi / 4 - phi / 2)
return [round(rho * math.sin(dl), 2), round(-rho * math.cos(dl), 2)]
rho = 2 * R * k0 * math.tan(math.pi / 4 + phi / 2)
return [round(rho * math.sin(dl), 2), round(rho * math.cos(dl), 2)]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('control #0', [-8.993, 52.361, 'N', 180.0, 1.0], [678803.65, 4289189.08]), ('control #1', [-175.479, -58.613, 'S', 70.0, 0.994], [3237624.01, -1476903.61]), ('control #2', [-54.642, 24.763, 'N', 180.0, 0.9996], [6647880.23, 4717072.09]), ('control #3', [-55.98, 14.924, 'N', -45.0, 0.9996], [-1864055.44, -9607637.84]), ('control #4', [-10.59, -42.995, 'S', 180.0, 0.994], [1012221.27, -5413982.3]), ('control #5', [123.778, -15.586, 'S', 70.0, 1.0], [7804467.33, 5716609.82]), ('boundary #20', [30.0, 0.0, 'N', 0.0, 1.0], [6371000.0, -11034895.7]), ('boundary #21', [30.0, 0.0, 'S', 0.0, 1.0], [6371000.0, 11034895.7])], [('control #3', [-55.98, 14.924, 'N', -45.0, 0.9996], [-1864055.44, -9607637.84]), ('control #4', [-10.59, -42.995, 'S', 180.0, 0.994], [1012221.27, -5413982.3]), ('control #5', [123.778, -15.586, 'S', 70.0, 1.0], [7804467.33, 5716609.82]), ('control #6', [48.904, -48.41, 'S', 180.0, 1.0], [-3646684.72, -3180757.54]), ('control #7', [-170.617, 3.39, 'N', 180.0, 0.994], [1946208.51, -11777778.83]), ('control #8', [27.496, -32.858, 'S', 180.0, 0.9996], [-3202270.07, -6152552.24]), ('control #9', [119.432, 24.56, 'N', 0.0, 0.994], [7087167.41, 3998630.2]), ('boundary #21', [30.0, 0.0, 'S', 0.0, 1.0], [6371000.0, 11034895.7])], [('control #6', [48.904, -48.41, 'S', 180.0, 1.0], [-3646684.72, -3180757.54]), ('control #7', [-170.617, 3.39, 'N', 180.0, 0.994], [1946208.51, -11777778.83]), ('control #8', [27.496, -32.858, 'S', 180.0, 0.9996], [-3202270.07, -6152552.24]), ('control #9', [119.432, 24.56, 'N', 0.0, 0.994], [7087167.41, 3998630.2]), ('control #10', [112.92, -7.484, 'S', 0.0, 0.994], [10233335.79, -4326940.4]), ('control #11', [38.777, -30.127, 'S', 0.0, 1.0], [4595585.04, 5720455.9]), ('boundary #20', [30.0, 0.0, 'N', 0.0, 1.0], [6371000.0, -11034895.7]), ('boundary #21', [30.0, 0.0, 'S', 0.0, 1.0], [6371000.0, 11034895.7])], [('control #9', [119.432, 24.56, 'N', 0.0, 0.994], [7087167.41, 3998630.2]), ('control #10', [112.92, -7.484, 'S', 0.0, 0.994], [10233335.79, -4326940.4]), ('control #11', [38.777, -30.127, 'S', 0.0, 1.0], [4595585.04, 5720455.9]), ('control #12', [-24.24, 42.676, 'N', -45.0, 0.994], [1967139.89, -5189443.03]), ('control #13', [-123.503, 27.165, 'N', 180.0, 1.0], [6489985.88, -4296116.65]), ('control #14', [-71.556, 31.408, 'N', 70.0, 1.0], [-4445077.72, 5599453.4]), ('control #15', [133.789, -17.059, 'S', -45.0, 0.9996], [198973.01, -9412565.04]), ('boundary #21', [30.0, 0.0, 'S', 0.0, 1.0], [6371000.0, 11034895.7])], [('control #12', [-24.24, 42.676, 'N', -45.0, 0.994], [1967139.89, -5189443.03]), ('control #13', [-123.503, 27.165, 'N', 180.0, 1.0], [6489985.88, -4296116.65]), ('control #14', [-71.556, 31.408, 'N', 70.0, 1.0], [-4445077.72, 5599453.4]), ('control #15', [133.789, -17.059, 'S', -45.0, 0.9996], [198973.01, -9412565.04]), ('control #16', [-69.256, 8.234, 'N', 70.0, 1.0], [-7199620.38, 8357340.39]), ('control #17', [97.814, -49.911, 'S', 0.0, 1.0], [4605752.41, -632056.15]), ('boundary #20', [30.0, 0.0, 'N', 0.0, 1.0], [6371000.0, -11034895.7]), ('boundary #21', [30.0, 0.0, 'S', 0.0, 1.0], [6371000.0, 11034895.7])]]
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 | [678803.65, 4289189.08] | [678803.65, 4289189.08] | Passed |
| control #1 | [3237624.01, -1476903.61] | [3237624.01, -1476903.61] | Passed |
| control #2 | [6647880.23, 4717072.09] | [6647880.23, 4717072.09] | Passed |
| control #3 | [-1864055.44, -9607637.84] | [-1864055.44, -9607637.84] | Passed |
| control #4 | [1012221.27, -5413982.3] | [1012221.27, -5413982.3] | Passed |
| control #5 | [7804467.33, 5716609.82] | [7804467.33, 5716609.82] | Passed |
| boundary #20 | None | [6371000.0, -11034895.7] | Failed |
| boundary #21 | None | [6371000.0, 11034895.7] | Failed |
SHA-256 / bc45539e23373e5eff3bf3434771cc632bbaeab82d1cdc00ff919795259b0af6
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, pole, lon0, k0 = x
R = 6371000.0
if (pole == 'N' and lat < 0) or (pole == 'S' and lat >= 0):
return None
phi = math.radians(lat)
dl = math.radians(lon - lon0)
if pole == 'N':
rho = 2 * R * k0 * math.tan(math.pi / 4 - phi / 2)
return [round(rho * math.sin(dl), 2), round(-rho * math.cos(dl), 2)]
rho = 2 * R * k0 * math.tan(math.pi / 4 + phi / 2)
return [round(rho * math.sin(dl), 2), round(rho * math.cos(dl), 2)]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('control #0', [-8.993, 52.361, 'N', 180.0, 1.0], [678803.65, 4289189.08]), ('control #1', [-175.479, -58.613, 'S', 70.0, 0.994], [3237624.01, -1476903.61]), ('control #2', [-54.642, 24.763, 'N', 180.0, 0.9996], [6647880.23, 4717072.09]), ('control #3', [-55.98, 14.924, 'N', -45.0, 0.9996], [-1864055.44, -9607637.84]), ('control #4', [-10.59, -42.995, 'S', 180.0, 0.994], [1012221.27, -5413982.3]), ('control #5', [123.778, -15.586, 'S', 70.0, 1.0], [7804467.33, 5716609.82]), ('boundary #20', [30.0, 0.0, 'N', 0.0, 1.0], [6371000.0, -11034895.7]), ('boundary #21', [30.0, 0.0, 'S', 0.0, 1.0], [6371000.0, 11034895.7])], [('control #3', [-55.98, 14.924, 'N', -45.0, 0.9996], [-1864055.44, -9607637.84]), ('control #4', [-10.59, -42.995, 'S', 180.0, 0.994], [1012221.27, -5413982.3]), ('control #5', [123.778, -15.586, 'S', 70.0, 1.0], [7804467.33, 5716609.82]), ('control #6', [48.904, -48.41, 'S', 180.0, 1.0], [-3646684.72, -3180757.54]), ('control #7', [-170.617, 3.39, 'N', 180.0, 0.994], [1946208.51, -11777778.83]), ('control #8', [27.496, -32.858, 'S', 180.0, 0.9996], [-3202270.07, -6152552.24]), ('control #9', [119.432, 24.56, 'N', 0.0, 0.994], [7087167.41, 3998630.2]), ('boundary #21', [30.0, 0.0, 'S', 0.0, 1.0], [6371000.0, 11034895.7])], [('control #6', [48.904, -48.41, 'S', 180.0, 1.0], [-3646684.72, -3180757.54]), ('control #7', [-170.617, 3.39, 'N', 180.0, 0.994], [1946208.51, -11777778.83]), ('control #8', [27.496, -32.858, 'S', 180.0, 0.9996], [-3202270.07, -6152552.24]), ('control #9', [119.432, 24.56, 'N', 0.0, 0.994], [7087167.41, 3998630.2]), ('control #10', [112.92, -7.484, 'S', 0.0, 0.994], [10233335.79, -4326940.4]), ('control #11', [38.777, -30.127, 'S', 0.0, 1.0], [4595585.04, 5720455.9]), ('boundary #20', [30.0, 0.0, 'N', 0.0, 1.0], [6371000.0, -11034895.7]), ('boundary #21', [30.0, 0.0, 'S', 0.0, 1.0], [6371000.0, 11034895.7])], [('control #9', [119.432, 24.56, 'N', 0.0, 0.994], [7087167.41, 3998630.2]), ('control #10', [112.92, -7.484, 'S', 0.0, 0.994], [10233335.79, -4326940.4]), ('control #11', [38.777, -30.127, 'S', 0.0, 1.0], [4595585.04, 5720455.9]), ('control #12', [-24.24, 42.676, 'N', -45.0, 0.994], [1967139.89, -5189443.03]), ('control #13', [-123.503, 27.165, 'N', 180.0, 1.0], [6489985.88, -4296116.65]), ('control #14', [-71.556, 31.408, 'N', 70.0, 1.0], [-4445077.72, 5599453.4]), ('control #15', [133.789, -17.059, 'S', -45.0, 0.9996], [198973.01, -9412565.04]), ('boundary #21', [30.0, 0.0, 'S', 0.0, 1.0], [6371000.0, 11034895.7])], [('control #12', [-24.24, 42.676, 'N', -45.0, 0.994], [1967139.89, -5189443.03]), ('control #13', [-123.503, 27.165, 'N', 180.0, 1.0], [6489985.88, -4296116.65]), ('control #14', [-71.556, 31.408, 'N', 70.0, 1.0], [-4445077.72, 5599453.4]), ('control #15', [133.789, -17.059, 'S', -45.0, 0.9996], [198973.01, -9412565.04]), ('control #16', [-69.256, 8.234, 'N', 70.0, 1.0], [-7199620.38, 8357340.39]), ('control #17', [97.814, -49.911, 'S', 0.0, 1.0], [4605752.41, -632056.15]), ('boundary #20', [30.0, 0.0, 'N', 0.0, 1.0], [6371000.0, -11034895.7]), ('boundary #21', [30.0, 0.0, 'S', 0.0, 1.0], [6371000.0, 11034895.7])]]
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 | [678803.65, 4289189.08] | [678803.65, 4289189.08] | Passed |
| control #1 | [3237624.01, -1476903.61] | [3237624.01, -1476903.61] | Passed |
| control #2 | [6647880.23, 4717072.09] | [6647880.23, 4717072.09] | Passed |
| control #3 | [-1864055.44, -9607637.84] | [-1864055.44, -9607637.84] | Passed |
| control #4 | [1012221.27, -5413982.3] | [1012221.27, -5413982.3] | Passed |
| control #5 | [7804467.33, 5716609.82] | [7804467.33, 5716609.82] | Passed |
| boundary #20 | [6371000.0, -11034895.7] | [6371000.0, -11034895.7] | Passed |
| boundary #21 | None | [6371000.0, 11034895.7] | Failed |
SHA-256 / e25c341bb63590ad97ba078431e1ff06d621ea10a08263122dfacbf51188a298
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, pole, lon0, k0 = x
R = 6371000.0
if (pole == 'N' and lat < 0) or (pole == 'S' and lat > 0):
return None
phi = math.radians(lat)
dl = math.radians(lon - lon0)
if pole == 'N':
rho = 2 * R * k0 * math.tan(math.pi / 4 - phi / 2)
return [round(rho * math.sin(dl), 2), round(-rho * math.cos(dl), 2)]
rho = 2 * R * k0 * math.tan(math.pi / 4 + phi / 2)
return [round(rho * math.sin(dl), 2), round(rho * math.cos(dl), 2)]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('control #0', [-8.993, 52.361, 'N', 180.0, 1.0], [678803.65, 4289189.08]), ('control #1', [-175.479, -58.613, 'S', 70.0, 0.994], [3237624.01, -1476903.61]), ('control #2', [-54.642, 24.763, 'N', 180.0, 0.9996], [6647880.23, 4717072.09]), ('control #3', [-55.98, 14.924, 'N', -45.0, 0.9996], [-1864055.44, -9607637.84]), ('control #4', [-10.59, -42.995, 'S', 180.0, 0.994], [1012221.27, -5413982.3]), ('control #5', [123.778, -15.586, 'S', 70.0, 1.0], [7804467.33, 5716609.82]), ('boundary #20', [30.0, 0.0, 'N', 0.0, 1.0], [6371000.0, -11034895.7]), ('boundary #21', [30.0, 0.0, 'S', 0.0, 1.0], [6371000.0, 11034895.7])], [('control #3', [-55.98, 14.924, 'N', -45.0, 0.9996], [-1864055.44, -9607637.84]), ('control #4', [-10.59, -42.995, 'S', 180.0, 0.994], [1012221.27, -5413982.3]), ('control #5', [123.778, -15.586, 'S', 70.0, 1.0], [7804467.33, 5716609.82]), ('control #6', [48.904, -48.41, 'S', 180.0, 1.0], [-3646684.72, -3180757.54]), ('control #7', [-170.617, 3.39, 'N', 180.0, 0.994], [1946208.51, -11777778.83]), ('control #8', [27.496, -32.858, 'S', 180.0, 0.9996], [-3202270.07, -6152552.24]), ('control #9', [119.432, 24.56, 'N', 0.0, 0.994], [7087167.41, 3998630.2]), ('boundary #21', [30.0, 0.0, 'S', 0.0, 1.0], [6371000.0, 11034895.7])], [('control #6', [48.904, -48.41, 'S', 180.0, 1.0], [-3646684.72, -3180757.54]), ('control #7', [-170.617, 3.39, 'N', 180.0, 0.994], [1946208.51, -11777778.83]), ('control #8', [27.496, -32.858, 'S', 180.0, 0.9996], [-3202270.07, -6152552.24]), ('control #9', [119.432, 24.56, 'N', 0.0, 0.994], [7087167.41, 3998630.2]), ('control #10', [112.92, -7.484, 'S', 0.0, 0.994], [10233335.79, -4326940.4]), ('control #11', [38.777, -30.127, 'S', 0.0, 1.0], [4595585.04, 5720455.9]), ('boundary #20', [30.0, 0.0, 'N', 0.0, 1.0], [6371000.0, -11034895.7]), ('boundary #21', [30.0, 0.0, 'S', 0.0, 1.0], [6371000.0, 11034895.7])], [('control #9', [119.432, 24.56, 'N', 0.0, 0.994], [7087167.41, 3998630.2]), ('control #10', [112.92, -7.484, 'S', 0.0, 0.994], [10233335.79, -4326940.4]), ('control #11', [38.777, -30.127, 'S', 0.0, 1.0], [4595585.04, 5720455.9]), ('control #12', [-24.24, 42.676, 'N', -45.0, 0.994], [1967139.89, -5189443.03]), ('control #13', [-123.503, 27.165, 'N', 180.0, 1.0], [6489985.88, -4296116.65]), ('control #14', [-71.556, 31.408, 'N', 70.0, 1.0], [-4445077.72, 5599453.4]), ('control #15', [133.789, -17.059, 'S', -45.0, 0.9996], [198973.01, -9412565.04]), ('boundary #21', [30.0, 0.0, 'S', 0.0, 1.0], [6371000.0, 11034895.7])], [('control #12', [-24.24, 42.676, 'N', -45.0, 0.994], [1967139.89, -5189443.03]), ('control #13', [-123.503, 27.165, 'N', 180.0, 1.0], [6489985.88, -4296116.65]), ('control #14', [-71.556, 31.408, 'N', 70.0, 1.0], [-4445077.72, 5599453.4]), ('control #15', [133.789, -17.059, 'S', -45.0, 0.9996], [198973.01, -9412565.04]), ('control #16', [-69.256, 8.234, 'N', 70.0, 1.0], [-7199620.38, 8357340.39]), ('control #17', [97.814, -49.911, 'S', 0.0, 1.0], [4605752.41, -632056.15]), ('boundary #20', [30.0, 0.0, 'N', 0.0, 1.0], [6371000.0, -11034895.7]), ('boundary #21', [30.0, 0.0, 'S', 0.0, 1.0], [6371000.0, 11034895.7])]]
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 | [678803.65, 4289189.08] | [678803.65, 4289189.08] | Passed |
| control #1 | [3237624.01, -1476903.61] | [3237624.01, -1476903.61] | Passed |
| control #2 | [6647880.23, 4717072.09] | [6647880.23, 4717072.09] | Passed |
| control #3 | [-1864055.44, -9607637.84] | [-1864055.44, -9607637.84] | Passed |
| control #4 | [1012221.27, -5413982.3] | [1012221.27, -5413982.3] | Passed |
| control #5 | [7804467.33, 5716609.82] | [7804467.33, 5716609.82] | Passed |
| boundary #20 | [6371000.0, -11034895.7] | [6371000.0, -11034895.7] | Passed |
| boundary #21 | [6371000.0, 11034895.7] | [6371000.0, 11034895.7] | Passed |
SHA-256 / f579de73943fd1ef9e413f82e8e962123a91a1499032f573ed2f5ae332a9c974
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.908204+00:00.
Case digest / 9430dad25d41bf65543f2e4ec08d578959235f6c1e94fd064774bd24665cf3ac