FA-70216 / Map projection transforms / Open access
Azimuthal equidistant range and bearing: bearing argument order · case 01
Bearings are measured counter-clockwise from east.
ROOT CAUSE
atan2 is called with (y, x), producing a mathematical angle instead of a compass bearing.
VERIFIED REPAIR
At the bearing argument order step restore `math.atan2(px, py)`, leaving the rest of the model unchanged.
Unsuccessful approach: Negating y measures the bearing from south instead of north.
Case contract
Input [lon, lat, lon0, lat0]; sphere R = 6371000 centred on (lon0, lat0). cos c is clamped to [-1, 1], c = acos. Points with c > pi - 1e-6 (the antipode neighbourhood) return None. k = 1 at the centre (c < 1e-12) else c/sin c. x = R k cos(p) sin(dl); y = R k (cos(p0) sin(p) - sin(p0) cos(p) cos(dl)). Return [x rounded to 1 decimal, y rounded to 1 decimal, hypot(x, y)/1000 km rounded to 3, azimuth clockwise from north in [0, 360) rounded to 4].
Why this case matters
Radio coverage, flight-range rings and seismology plots use this projection to read range and bearing directly.
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 = x
R = 6371000.0
phi, phi0 = math.radians(lat), math.radians(lat0)
dl = math.radians(lon - lon0)
cosc = math.sin(phi0) * math.sin(phi) + math.cos(phi0) * math.cos(phi) * math.cos(dl)
cosc = max(-1.0, min(1.0, cosc))
c = math.acos(cosc)
if c > math.pi - 1e-6:
return None
k = 1.0 if c < 1e-12 else c / math.sin(c)
px = R * k * math.cos(phi) * math.sin(dl)
py = R * k * (math.cos(phi0) * math.sin(phi) - math.sin(phi0) * math.cos(phi) * math.cos(dl))
dist = math.hypot(px, py) / 1000.0
az = math.degrees(math.atan2(py, px)) % 360.0
return [round(px, 1), round(py, 1), round(dist, 3), round(az, 4)]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('control #0', [-179.674, 72.844, -100.0, -60.0], [-7721194.5, 13929763.9, 15926.555, 331.0006]), ('control #1', [201.072, 34.17, 140.0, 0.0], [5834953.3, 4525637.3, 7384.313, 52.2026]), ('control #2', [194.99, -6.711, 140.0, 70.0], [7727165.8, -5465888.2, 9464.937, 125.2741]), ('control #3', [80.599, 78.457, 0.0, 70.0], [1287102.6, 1984537.9, 2365.38, 32.966]), ('control #4', [233.972, 56.233, 140.0, 0.0], [5689261.3, 8529602.6, 10252.893, 33.7034]), ('boundary #23', [180.0, 0.0, 0.0, 0.0], None), ('boundary #24', [180.0, 1e-05, 0.0, 0.0], None), ('boundary #25', [-80.0, -40.0, 100.0, 40.0], None)], [('control #1', [201.072, 34.17, 140.0, 0.0], [5834953.3, 4525637.3, 7384.313, 52.2026]), ('control #2', [194.99, -6.711, 140.0, 70.0], [7727165.8, -5465888.2, 9464.937, 125.2741]), ('control #5', [-8.83, 69.176, 45.0, -60.0], [-6061244.1, 13708058.5, 14988.314, 336.1466]), ('control #6', [70.317, -25.779, 140.0, 70.0], [-10593752.0, -5551791.2, 11960.35, 242.3426]), ('control #7', [82.76, -58.875, 140.0, -60.0], [-2884562.0, -1232854.6, 3136.978, 246.8583]), ('boundary #23', [180.0, 0.0, 0.0, 0.0], None), ('boundary #24', [180.0, 1e-05, 0.0, 0.0], None), ('boundary #25', [-80.0, -40.0, 100.0, 40.0], None)], [('control #2', [194.99, -6.711, 140.0, 70.0], [7727165.8, -5465888.2, 9464.937, 125.2741]), ('control #3', [80.599, 78.457, 0.0, 70.0], [1287102.6, 1984537.9, 2365.38, 32.966]), ('control #8', [-80.409, -20.217, 0.0, 70.0], [-11302865.4, -3238552.9, 11757.678, 254.0117]), ('control #9', [24.308, 5.805, 140.0, 0.0], [-12767659.1, 1440420.7, 12848.655, 276.4368]), ('control #10', [171.761, -8.363, 140.0, -60.0], [3932496.4, 4951958.7, 6323.482, 38.4541]), ('boundary #23', [180.0, 0.0, 0.0, 0.0], None), ('boundary #24', [180.0, 1e-05, 0.0, 0.0], None), ('boundary #25', [-80.0, -40.0, 100.0, 40.0], None)], [('control #3', [80.599, 78.457, 0.0, 70.0], [1287102.6, 1984537.9, 2365.38, 32.966]), ('control #4', [233.972, 56.233, 140.0, 0.0], [5689261.3, 8529602.6, 10252.893, 33.7034]), ('control #11', [86.324, 1.66, 0.0, -35.0], [9760962.6, 591892.2, 9778.892, 86.5299]), ('control #12', [51.045, 75.896, 45.0, 40.0], [174765.9, 3998901.2, 4002.718, 2.5024]), ('control #13', [-125.02, -49.187, -100.0, 40.0], [-2825638.9, -9817991.9, 10216.516, 196.0559]), ('boundary #23', [180.0, 0.0, 0.0, 0.0], None), ('boundary #24', [180.0, 1e-05, 0.0, 0.0], None), ('boundary #25', [-80.0, -40.0, 100.0, 40.0], None)], [('control #4', [233.972, 56.233, 140.0, 0.0], [5689261.3, 8529602.6, 10252.893, 33.7034]), ('control #5', [-8.83, 69.176, 45.0, -60.0], [-6061244.1, 13708058.5, 14988.314, 336.1466]), ('control #14', [82.118, -2.719, 0.0, -60.0], [9265529.2, 888764.4, 9308.057, 84.5209]), ('control #15', [-64.341, 43.206, 0.0, 40.0], [-4695245.9, 2298059.6, 5227.467, 296.0792]), ('control #16', [42.81, -75.807, 0.0, -60.0], [1087216.3, -2146485.8, 2406.126, 153.1373]), ('boundary #23', [180.0, 0.0, 0.0, 0.0], None), ('boundary #24', [180.0, 1e-05, 0.0, 0.0], None), ('boundary #25', [-80.0, -40.0, 100.0, 40.0], None)]]
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 | [-7721194.5, 13929763.9, 15926.555, 118.9994] | [-7721194.5, 13929763.9, 15926.555, 331.0006] | Failed |
| control #1 | [5834953.3, 4525637.3, 7384.313, 37.7974] | [5834953.3, 4525637.3, 7384.313, 52.2026] | Failed |
| control #2 | [7727165.8, -5465888.2, 9464.937, 324.7259] | [7727165.8, -5465888.2, 9464.937, 125.2741] | Failed |
| control #3 | [1287102.6, 1984537.9, 2365.38, 57.034] | [1287102.6, 1984537.9, 2365.38, 32.966] | Failed |
| control #4 | [5689261.3, 8529602.6, 10252.893, 56.2966] | [5689261.3, 8529602.6, 10252.893, 33.7034] | Failed |
| boundary #23 | None | None | Passed |
| boundary #24 | None | None | Passed |
| boundary #25 | None | None | Passed |
SHA-256 / ac4c99a57399d390ad9ad2b3330aa85d0bf319ef01cf3205d2fbfd5b8f2fa6e5
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 = x
R = 6371000.0
phi, phi0 = math.radians(lat), math.radians(lat0)
dl = math.radians(lon - lon0)
cosc = math.sin(phi0) * math.sin(phi) + math.cos(phi0) * math.cos(phi) * math.cos(dl)
cosc = max(-1.0, min(1.0, cosc))
c = math.acos(cosc)
if c > math.pi - 1e-6:
return None
k = 1.0 if c < 1e-12 else c / math.sin(c)
px = R * k * math.cos(phi) * math.sin(dl)
py = R * k * (math.cos(phi0) * math.sin(phi) - math.sin(phi0) * math.cos(phi) * math.cos(dl))
dist = math.hypot(px, py) / 1000.0
az = math.degrees(math.atan2(px, -py)) % 360.0
return [round(px, 1), round(py, 1), round(dist, 3), round(az, 4)]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('control #0', [-179.674, 72.844, -100.0, -60.0], [-7721194.5, 13929763.9, 15926.555, 331.0006]), ('control #1', [201.072, 34.17, 140.0, 0.0], [5834953.3, 4525637.3, 7384.313, 52.2026]), ('control #2', [194.99, -6.711, 140.0, 70.0], [7727165.8, -5465888.2, 9464.937, 125.2741]), ('control #3', [80.599, 78.457, 0.0, 70.0], [1287102.6, 1984537.9, 2365.38, 32.966]), ('control #4', [233.972, 56.233, 140.0, 0.0], [5689261.3, 8529602.6, 10252.893, 33.7034]), ('boundary #23', [180.0, 0.0, 0.0, 0.0], None), ('boundary #24', [180.0, 1e-05, 0.0, 0.0], None), ('boundary #25', [-80.0, -40.0, 100.0, 40.0], None)], [('control #1', [201.072, 34.17, 140.0, 0.0], [5834953.3, 4525637.3, 7384.313, 52.2026]), ('control #2', [194.99, -6.711, 140.0, 70.0], [7727165.8, -5465888.2, 9464.937, 125.2741]), ('control #5', [-8.83, 69.176, 45.0, -60.0], [-6061244.1, 13708058.5, 14988.314, 336.1466]), ('control #6', [70.317, -25.779, 140.0, 70.0], [-10593752.0, -5551791.2, 11960.35, 242.3426]), ('control #7', [82.76, -58.875, 140.0, -60.0], [-2884562.0, -1232854.6, 3136.978, 246.8583]), ('boundary #23', [180.0, 0.0, 0.0, 0.0], None), ('boundary #24', [180.0, 1e-05, 0.0, 0.0], None), ('boundary #25', [-80.0, -40.0, 100.0, 40.0], None)], [('control #2', [194.99, -6.711, 140.0, 70.0], [7727165.8, -5465888.2, 9464.937, 125.2741]), ('control #3', [80.599, 78.457, 0.0, 70.0], [1287102.6, 1984537.9, 2365.38, 32.966]), ('control #8', [-80.409, -20.217, 0.0, 70.0], [-11302865.4, -3238552.9, 11757.678, 254.0117]), ('control #9', [24.308, 5.805, 140.0, 0.0], [-12767659.1, 1440420.7, 12848.655, 276.4368]), ('control #10', [171.761, -8.363, 140.0, -60.0], [3932496.4, 4951958.7, 6323.482, 38.4541]), ('boundary #23', [180.0, 0.0, 0.0, 0.0], None), ('boundary #24', [180.0, 1e-05, 0.0, 0.0], None), ('boundary #25', [-80.0, -40.0, 100.0, 40.0], None)], [('control #3', [80.599, 78.457, 0.0, 70.0], [1287102.6, 1984537.9, 2365.38, 32.966]), ('control #4', [233.972, 56.233, 140.0, 0.0], [5689261.3, 8529602.6, 10252.893, 33.7034]), ('control #11', [86.324, 1.66, 0.0, -35.0], [9760962.6, 591892.2, 9778.892, 86.5299]), ('control #12', [51.045, 75.896, 45.0, 40.0], [174765.9, 3998901.2, 4002.718, 2.5024]), ('control #13', [-125.02, -49.187, -100.0, 40.0], [-2825638.9, -9817991.9, 10216.516, 196.0559]), ('boundary #23', [180.0, 0.0, 0.0, 0.0], None), ('boundary #24', [180.0, 1e-05, 0.0, 0.0], None), ('boundary #25', [-80.0, -40.0, 100.0, 40.0], None)], [('control #4', [233.972, 56.233, 140.0, 0.0], [5689261.3, 8529602.6, 10252.893, 33.7034]), ('control #5', [-8.83, 69.176, 45.0, -60.0], [-6061244.1, 13708058.5, 14988.314, 336.1466]), ('control #14', [82.118, -2.719, 0.0, -60.0], [9265529.2, 888764.4, 9308.057, 84.5209]), ('control #15', [-64.341, 43.206, 0.0, 40.0], [-4695245.9, 2298059.6, 5227.467, 296.0792]), ('control #16', [42.81, -75.807, 0.0, -60.0], [1087216.3, -2146485.8, 2406.126, 153.1373]), ('boundary #23', [180.0, 0.0, 0.0, 0.0], None), ('boundary #24', [180.0, 1e-05, 0.0, 0.0], None), ('boundary #25', [-80.0, -40.0, 100.0, 40.0], None)]]
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 | [-7721194.5, 13929763.9, 15926.555, 208.9994] | [-7721194.5, 13929763.9, 15926.555, 331.0006] | Failed |
| control #1 | [5834953.3, 4525637.3, 7384.313, 127.7974] | [5834953.3, 4525637.3, 7384.313, 52.2026] | Failed |
| control #2 | [7727165.8, -5465888.2, 9464.937, 54.7259] | [7727165.8, -5465888.2, 9464.937, 125.2741] | Failed |
| control #3 | [1287102.6, 1984537.9, 2365.38, 147.034] | [1287102.6, 1984537.9, 2365.38, 32.966] | Failed |
| control #4 | [5689261.3, 8529602.6, 10252.893, 146.2966] | [5689261.3, 8529602.6, 10252.893, 33.7034] | Failed |
| boundary #23 | None | None | Passed |
| boundary #24 | None | None | Passed |
| boundary #25 | None | None | Passed |
SHA-256 / 48cae7e25179bd283b9bf0b05388ff4c379dab31a8987015400ecb8d5ecf0052
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 = x
R = 6371000.0
phi, phi0 = math.radians(lat), math.radians(lat0)
dl = math.radians(lon - lon0)
cosc = math.sin(phi0) * math.sin(phi) + math.cos(phi0) * math.cos(phi) * math.cos(dl)
cosc = max(-1.0, min(1.0, cosc))
c = math.acos(cosc)
if c > math.pi - 1e-6:
return None
k = 1.0 if c < 1e-12 else c / math.sin(c)
px = R * k * math.cos(phi) * math.sin(dl)
py = R * k * (math.cos(phi0) * math.sin(phi) - math.sin(phi0) * math.cos(phi) * math.cos(dl))
dist = math.hypot(px, py) / 1000.0
az = math.degrees(math.atan2(px, py)) % 360.0
return [round(px, 1), round(py, 1), round(dist, 3), round(az, 4)]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('control #0', [-179.674, 72.844, -100.0, -60.0], [-7721194.5, 13929763.9, 15926.555, 331.0006]), ('control #1', [201.072, 34.17, 140.0, 0.0], [5834953.3, 4525637.3, 7384.313, 52.2026]), ('control #2', [194.99, -6.711, 140.0, 70.0], [7727165.8, -5465888.2, 9464.937, 125.2741]), ('control #3', [80.599, 78.457, 0.0, 70.0], [1287102.6, 1984537.9, 2365.38, 32.966]), ('control #4', [233.972, 56.233, 140.0, 0.0], [5689261.3, 8529602.6, 10252.893, 33.7034]), ('boundary #23', [180.0, 0.0, 0.0, 0.0], None), ('boundary #24', [180.0, 1e-05, 0.0, 0.0], None), ('boundary #25', [-80.0, -40.0, 100.0, 40.0], None)], [('control #1', [201.072, 34.17, 140.0, 0.0], [5834953.3, 4525637.3, 7384.313, 52.2026]), ('control #2', [194.99, -6.711, 140.0, 70.0], [7727165.8, -5465888.2, 9464.937, 125.2741]), ('control #5', [-8.83, 69.176, 45.0, -60.0], [-6061244.1, 13708058.5, 14988.314, 336.1466]), ('control #6', [70.317, -25.779, 140.0, 70.0], [-10593752.0, -5551791.2, 11960.35, 242.3426]), ('control #7', [82.76, -58.875, 140.0, -60.0], [-2884562.0, -1232854.6, 3136.978, 246.8583]), ('boundary #23', [180.0, 0.0, 0.0, 0.0], None), ('boundary #24', [180.0, 1e-05, 0.0, 0.0], None), ('boundary #25', [-80.0, -40.0, 100.0, 40.0], None)], [('control #2', [194.99, -6.711, 140.0, 70.0], [7727165.8, -5465888.2, 9464.937, 125.2741]), ('control #3', [80.599, 78.457, 0.0, 70.0], [1287102.6, 1984537.9, 2365.38, 32.966]), ('control #8', [-80.409, -20.217, 0.0, 70.0], [-11302865.4, -3238552.9, 11757.678, 254.0117]), ('control #9', [24.308, 5.805, 140.0, 0.0], [-12767659.1, 1440420.7, 12848.655, 276.4368]), ('control #10', [171.761, -8.363, 140.0, -60.0], [3932496.4, 4951958.7, 6323.482, 38.4541]), ('boundary #23', [180.0, 0.0, 0.0, 0.0], None), ('boundary #24', [180.0, 1e-05, 0.0, 0.0], None), ('boundary #25', [-80.0, -40.0, 100.0, 40.0], None)], [('control #3', [80.599, 78.457, 0.0, 70.0], [1287102.6, 1984537.9, 2365.38, 32.966]), ('control #4', [233.972, 56.233, 140.0, 0.0], [5689261.3, 8529602.6, 10252.893, 33.7034]), ('control #11', [86.324, 1.66, 0.0, -35.0], [9760962.6, 591892.2, 9778.892, 86.5299]), ('control #12', [51.045, 75.896, 45.0, 40.0], [174765.9, 3998901.2, 4002.718, 2.5024]), ('control #13', [-125.02, -49.187, -100.0, 40.0], [-2825638.9, -9817991.9, 10216.516, 196.0559]), ('boundary #23', [180.0, 0.0, 0.0, 0.0], None), ('boundary #24', [180.0, 1e-05, 0.0, 0.0], None), ('boundary #25', [-80.0, -40.0, 100.0, 40.0], None)], [('control #4', [233.972, 56.233, 140.0, 0.0], [5689261.3, 8529602.6, 10252.893, 33.7034]), ('control #5', [-8.83, 69.176, 45.0, -60.0], [-6061244.1, 13708058.5, 14988.314, 336.1466]), ('control #14', [82.118, -2.719, 0.0, -60.0], [9265529.2, 888764.4, 9308.057, 84.5209]), ('control #15', [-64.341, 43.206, 0.0, 40.0], [-4695245.9, 2298059.6, 5227.467, 296.0792]), ('control #16', [42.81, -75.807, 0.0, -60.0], [1087216.3, -2146485.8, 2406.126, 153.1373]), ('boundary #23', [180.0, 0.0, 0.0, 0.0], None), ('boundary #24', [180.0, 1e-05, 0.0, 0.0], None), ('boundary #25', [-80.0, -40.0, 100.0, 40.0], None)]]
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 | [-7721194.5, 13929763.9, 15926.555, 331.0006] | [-7721194.5, 13929763.9, 15926.555, 331.0006] | Passed |
| control #1 | [5834953.3, 4525637.3, 7384.313, 52.2026] | [5834953.3, 4525637.3, 7384.313, 52.2026] | Passed |
| control #2 | [7727165.8, -5465888.2, 9464.937, 125.2741] | [7727165.8, -5465888.2, 9464.937, 125.2741] | Passed |
| control #3 | [1287102.6, 1984537.9, 2365.38, 32.966] | [1287102.6, 1984537.9, 2365.38, 32.966] | Passed |
| control #4 | [5689261.3, 8529602.6, 10252.893, 33.7034] | [5689261.3, 8529602.6, 10252.893, 33.7034] | Passed |
| boundary #23 | None | None | Passed |
| boundary #24 | None | None | Passed |
| boundary #25 | None | None | Passed |
SHA-256 / 9c04b8b1352f68d3ae453afd435e7c61815c5505836194b820156489ab8abe22
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:18.506743+00:00.
Case digest / 8edda1679c0ae0729e183b62875d900d0543c47cf6360e0e331f6fac0585ebe3