FAILURE MAP
← Case archive

FA-70216 / Map projection transforms / Open access

Azimuthal equidistant range and bearing: bearing argument order · case 01

Bearings are measured counter-clockwise from east.

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

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 fixtureActualExpectedOutcome
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 #23NoneNonePassed
boundary #24NoneNonePassed
boundary #25NoneNonePassed

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 fixtureActualExpectedOutcome
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 #23NoneNonePassed
boundary #24NoneNonePassed
boundary #25NoneNonePassed

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 fixtureActualExpectedOutcome
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 #23NoneNonePassed
boundary #24NoneNonePassed
boundary #25NoneNonePassed

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