FAILURE MAP
← Case archive

FA-71351 / Seismic magnitude estimation / Open access

Hutton-Boore local magnitude: hypocentral distance · case 01

Events recorded directly above the source crash or read far too small.

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

ROOT CAUSE

The epicentral distance is used instead of the hypocentral distance.

VERIFIED REPAIR

Use r = sqrt(epi^2 + depth^2).

Unsuccessful approach: Adding depth to epicentral distance overstates r at every oblique path.

Case contract

Input the Wood-Anderson peak-to-peak amplitude in nm, epicentral distance and depth in km. Non-positive amplitude returns None. A (mm, zero-to-peak) = p2p/2 * 1e-6; r = hypocentral distance; ML = log10(A) + 1.11 log10(r/100) + 0.00189 (r - 100) + 3.0, rounded to 0.01.

Why this case matters

Regional networks publish ML for most small and moderate earthquakes.

1 / The failure

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(amp_p2p_nm, epi_km, depth_km):
    if amp_p2p_nm <= 0:
        return None
    a_mm = amp_p2p_nm / 2.0 * 1e-6
    r = epi_km
    ml = math.log10(a_mm) + 1.11 * math.log10(r / 100.0) + 0.00189 * (r - 100.0) + 3.0
    return round(ml, 2)
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['0 nm p2p at 0 km (depth 8)', [0, 0, 8], None], ['0 nm p2p at 12 km (depth 5)', [0, 12, 5], None], ['0 nm p2p at 40 km (depth 10)', [0, 40, 10], None], ['0 nm p2p at 100 km (depth 0)', [0, 100, 0], None], ['0 nm p2p at 100 km (depth 15)', [0, 100, 15], None], ['0 nm p2p at 250 km (depth 12)', [0, 250, 12], None], ['150 nm p2p at 12 km (depth 5)', [150, 12, 5], -2.27], ['150 nm p2p at 40 km (depth 10)', [150, 40, 10], -1.66]], [['150 nm p2p at 12 km (depth 5)', [150, 12, 5], -2.27], ['150 nm p2p at 40 km (depth 10)', [150, 40, 10], -1.66], ['150 nm p2p at 100 km (depth 0)', [150, 100, 0], -1.12], ['150 nm p2p at 100 km (depth 15)', [150, 100, 15], -1.12], ['150 nm p2p at 250 km (depth 12)', [150, 250, 12], -0.4], ['150 nm p2p at 480 km (depth 30)', [150, 480, 30], 0.35], ['2000 nm p2p at 12 km (depth 5)', [2000, 12, 5], -1.15], ['2000 nm p2p at 40 km (depth 10)', [2000, 40, 10], -0.54]], [['2000 nm p2p at 40 km (depth 10)', [2000, 40, 10], -0.54], ['2000 nm p2p at 100 km (depth 0)', [2000, 100, 0], 0.0], ['2000 nm p2p at 100 km (depth 15)', [2000, 100, 15], 0.01], ['2000 nm p2p at 250 km (depth 12)', [2000, 250, 12], 0.73], ['2000 nm p2p at 480 km (depth 30)', [2000, 480, 30], 1.48], ['48000 nm p2p at 12 km (depth 5)', [48000, 12, 5], 0.23], ['48000 nm p2p at 40 km (depth 10)', [48000, 40, 10], 0.84], ['48000 nm p2p at 100 km (depth 0)', [48000, 100, 0], 1.38]], [['48000 nm p2p at 100 km (depth 0)', [48000, 100, 0], 1.38], ['48000 nm p2p at 100 km (depth 15)', [48000, 100, 15], 1.39], ['48000 nm p2p at 250 km (depth 12)', [48000, 250, 12], 2.11], ['48000 nm p2p at 480 km (depth 30)', [48000, 480, 30], 2.86], ['900000 nm p2p at 12 km (depth 5)', [900000, 12, 5], 1.51], ['900000 nm p2p at 40 km (depth 10)', [900000, 40, 10], 2.12], ['900000 nm p2p at 100 km (depth 0)', [900000, 100, 0], 2.65], ['900000 nm p2p at 100 km (depth 15)', [900000, 100, 15], 2.66]], [['0 nm p2p at 0 km (depth 8)', [0, 0, 8], None], ['0 nm p2p at 12 km (depth 5)', [0, 12, 5], None], ['0 nm p2p at 40 km (depth 10)', [0, 40, 10], None], ['0 nm p2p at 100 km (depth 0)', [0, 100, 0], None], ['150 nm p2p at 40 km (depth 10)', [150, 40, 10], -1.66], ['900000 nm p2p at 100 km (depth 15)', [900000, 100, 15], 2.66], ['900000 nm p2p at 250 km (depth 12)', [900000, 250, 12], 3.38], ['900000 nm p2p at 480 km (depth 30)', [900000, 480, 30], 4.13]]]
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
0 nm p2p at 0 km (depth 8)NoneNonePassed
0 nm p2p at 12 km (depth 5)NoneNonePassed
0 nm p2p at 40 km (depth 10)NoneNonePassed
0 nm p2p at 100 km (depth 0)NoneNonePassed
0 nm p2p at 100 km (depth 15)NoneNonePassed
0 nm p2p at 250 km (depth 12)NoneNonePassed
150 nm p2p at 12 km (depth 5)-2.31-2.27Failed
150 nm p2p at 40 km (depth 10)-1.68-1.66Failed

SHA-256 / 5bca77d91c83071ec672179259ba9ad71a3e92bb924a8f0ede48d8b92a4b82d6

2 / The unsuccessful fix

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(amp_p2p_nm, epi_km, depth_km):
    if amp_p2p_nm <= 0:
        return None
    a_mm = amp_p2p_nm / 2.0 * 1e-6
    r = epi_km + depth_km
    ml = math.log10(a_mm) + 1.11 * math.log10(r / 100.0) + 0.00189 * (r - 100.0) + 3.0
    return round(ml, 2)
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['0 nm p2p at 0 km (depth 8)', [0, 0, 8], None], ['0 nm p2p at 12 km (depth 5)', [0, 12, 5], None], ['0 nm p2p at 40 km (depth 10)', [0, 40, 10], None], ['0 nm p2p at 100 km (depth 0)', [0, 100, 0], None], ['0 nm p2p at 100 km (depth 15)', [0, 100, 15], None], ['0 nm p2p at 250 km (depth 12)', [0, 250, 12], None], ['150 nm p2p at 12 km (depth 5)', [150, 12, 5], -2.27], ['150 nm p2p at 40 km (depth 10)', [150, 40, 10], -1.66]], [['150 nm p2p at 12 km (depth 5)', [150, 12, 5], -2.27], ['150 nm p2p at 40 km (depth 10)', [150, 40, 10], -1.66], ['150 nm p2p at 100 km (depth 0)', [150, 100, 0], -1.12], ['150 nm p2p at 100 km (depth 15)', [150, 100, 15], -1.12], ['150 nm p2p at 250 km (depth 12)', [150, 250, 12], -0.4], ['150 nm p2p at 480 km (depth 30)', [150, 480, 30], 0.35], ['2000 nm p2p at 12 km (depth 5)', [2000, 12, 5], -1.15], ['2000 nm p2p at 40 km (depth 10)', [2000, 40, 10], -0.54]], [['2000 nm p2p at 40 km (depth 10)', [2000, 40, 10], -0.54], ['2000 nm p2p at 100 km (depth 0)', [2000, 100, 0], 0.0], ['2000 nm p2p at 100 km (depth 15)', [2000, 100, 15], 0.01], ['2000 nm p2p at 250 km (depth 12)', [2000, 250, 12], 0.73], ['2000 nm p2p at 480 km (depth 30)', [2000, 480, 30], 1.48], ['48000 nm p2p at 12 km (depth 5)', [48000, 12, 5], 0.23], ['48000 nm p2p at 40 km (depth 10)', [48000, 40, 10], 0.84], ['48000 nm p2p at 100 km (depth 0)', [48000, 100, 0], 1.38]], [['48000 nm p2p at 100 km (depth 0)', [48000, 100, 0], 1.38], ['48000 nm p2p at 100 km (depth 15)', [48000, 100, 15], 1.39], ['48000 nm p2p at 250 km (depth 12)', [48000, 250, 12], 2.11], ['48000 nm p2p at 480 km (depth 30)', [48000, 480, 30], 2.86], ['900000 nm p2p at 12 km (depth 5)', [900000, 12, 5], 1.51], ['900000 nm p2p at 40 km (depth 10)', [900000, 40, 10], 2.12], ['900000 nm p2p at 100 km (depth 0)', [900000, 100, 0], 2.65], ['900000 nm p2p at 100 km (depth 15)', [900000, 100, 15], 2.66]], [['0 nm p2p at 0 km (depth 8)', [0, 0, 8], None], ['0 nm p2p at 12 km (depth 5)', [0, 12, 5], None], ['0 nm p2p at 40 km (depth 10)', [0, 40, 10], None], ['0 nm p2p at 100 km (depth 0)', [0, 100, 0], None], ['150 nm p2p at 40 km (depth 10)', [150, 40, 10], -1.66], ['900000 nm p2p at 100 km (depth 15)', [900000, 100, 15], 2.66], ['900000 nm p2p at 250 km (depth 12)', [900000, 250, 12], 3.38], ['900000 nm p2p at 480 km (depth 30)', [900000, 480, 30], 4.13]]]
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
0 nm p2p at 0 km (depth 8)NoneNonePassed
0 nm p2p at 12 km (depth 5)NoneNonePassed
0 nm p2p at 40 km (depth 10)NoneNonePassed
0 nm p2p at 100 km (depth 0)NoneNonePassed
0 nm p2p at 100 km (depth 15)NoneNonePassed
0 nm p2p at 250 km (depth 12)NoneNonePassed
150 nm p2p at 12 km (depth 5)-2.14-2.27Failed
150 nm p2p at 40 km (depth 10)-1.55-1.66Failed

SHA-256 / 71dc23857047835f3d7eefd677bff65bfcbd8ecf6de451dcf3750f2a7c933371

3 / The verified repair

Exit 0
"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(amp_p2p_nm, epi_km, depth_km):
    if amp_p2p_nm <= 0:
        return None
    a_mm = amp_p2p_nm / 2.0 * 1e-6
    r = math.hypot(epi_km, depth_km)
    ml = math.log10(a_mm) + 1.11 * math.log10(r / 100.0) + 0.00189 * (r - 100.0) + 3.0
    return round(ml, 2)
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['0 nm p2p at 0 km (depth 8)', [0, 0, 8], None], ['0 nm p2p at 12 km (depth 5)', [0, 12, 5], None], ['0 nm p2p at 40 km (depth 10)', [0, 40, 10], None], ['0 nm p2p at 100 km (depth 0)', [0, 100, 0], None], ['0 nm p2p at 100 km (depth 15)', [0, 100, 15], None], ['0 nm p2p at 250 km (depth 12)', [0, 250, 12], None], ['150 nm p2p at 12 km (depth 5)', [150, 12, 5], -2.27], ['150 nm p2p at 40 km (depth 10)', [150, 40, 10], -1.66]], [['150 nm p2p at 12 km (depth 5)', [150, 12, 5], -2.27], ['150 nm p2p at 40 km (depth 10)', [150, 40, 10], -1.66], ['150 nm p2p at 100 km (depth 0)', [150, 100, 0], -1.12], ['150 nm p2p at 100 km (depth 15)', [150, 100, 15], -1.12], ['150 nm p2p at 250 km (depth 12)', [150, 250, 12], -0.4], ['150 nm p2p at 480 km (depth 30)', [150, 480, 30], 0.35], ['2000 nm p2p at 12 km (depth 5)', [2000, 12, 5], -1.15], ['2000 nm p2p at 40 km (depth 10)', [2000, 40, 10], -0.54]], [['2000 nm p2p at 40 km (depth 10)', [2000, 40, 10], -0.54], ['2000 nm p2p at 100 km (depth 0)', [2000, 100, 0], 0.0], ['2000 nm p2p at 100 km (depth 15)', [2000, 100, 15], 0.01], ['2000 nm p2p at 250 km (depth 12)', [2000, 250, 12], 0.73], ['2000 nm p2p at 480 km (depth 30)', [2000, 480, 30], 1.48], ['48000 nm p2p at 12 km (depth 5)', [48000, 12, 5], 0.23], ['48000 nm p2p at 40 km (depth 10)', [48000, 40, 10], 0.84], ['48000 nm p2p at 100 km (depth 0)', [48000, 100, 0], 1.38]], [['48000 nm p2p at 100 km (depth 0)', [48000, 100, 0], 1.38], ['48000 nm p2p at 100 km (depth 15)', [48000, 100, 15], 1.39], ['48000 nm p2p at 250 km (depth 12)', [48000, 250, 12], 2.11], ['48000 nm p2p at 480 km (depth 30)', [48000, 480, 30], 2.86], ['900000 nm p2p at 12 km (depth 5)', [900000, 12, 5], 1.51], ['900000 nm p2p at 40 km (depth 10)', [900000, 40, 10], 2.12], ['900000 nm p2p at 100 km (depth 0)', [900000, 100, 0], 2.65], ['900000 nm p2p at 100 km (depth 15)', [900000, 100, 15], 2.66]], [['0 nm p2p at 0 km (depth 8)', [0, 0, 8], None], ['0 nm p2p at 12 km (depth 5)', [0, 12, 5], None], ['0 nm p2p at 40 km (depth 10)', [0, 40, 10], None], ['0 nm p2p at 100 km (depth 0)', [0, 100, 0], None], ['150 nm p2p at 40 km (depth 10)', [150, 40, 10], -1.66], ['900000 nm p2p at 100 km (depth 15)', [900000, 100, 15], 2.66], ['900000 nm p2p at 250 km (depth 12)', [900000, 250, 12], 3.38], ['900000 nm p2p at 480 km (depth 30)', [900000, 480, 30], 4.13]]]
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
0 nm p2p at 0 km (depth 8)NoneNonePassed
0 nm p2p at 12 km (depth 5)NoneNonePassed
0 nm p2p at 40 km (depth 10)NoneNonePassed
0 nm p2p at 100 km (depth 0)NoneNonePassed
0 nm p2p at 100 km (depth 15)NoneNonePassed
0 nm p2p at 250 km (depth 12)NoneNonePassed
150 nm p2p at 12 km (depth 5)-2.27-2.27Passed
150 nm p2p at 40 km (depth 10)-1.66-1.66Passed

SHA-256 / 98ad23423d126e2f53408c2889106a53fecf0bf0676f9d373447caf220433a40

Verification & scope

Stipulated deterministic teaching model of a seismological magnitude procedure; constants and tables are fixed by the contract and no claim of agency or standards conformance is made. 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:28.861857+00:00.

Case digest / d1add5dd475b13bb12e67639517e0ce018d3a3a8f3e92d62cf54967ad6054822