FA-87831 / Collision detection broadphase / Open access
Proxy margin is added only on the maximum corner · case 01
Contacts from the left and below begin late.
ROOT CAUSE
The min corner omits the margin.
VERIFIED REPAIR
Fatten all four sides.
Unsuccessful approach: Halving the margin on the min side leaves the proxy asymmetric.
Case contract
solve(cx, cy, hx, hy, angle, margin): broadphase proxy for a box with centre, half extents and rotation in degrees. Half sizes ex=|cos|hx+|sin|hy, ey=|sin|hx+|cos|hy; fatten by margin on every side. Return [minx, miny, maxx, maxy] rounded to 6.
Why this case matters
Broadphase stages decide which object pairs ever reach narrowphase; a wrong boundary, ordering or bookkeeping rule silently drops real contacts or floods the solver with false candidates.
1 / The failure
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(cx, cy, hx, hy, angle, margin):
c, s = abs(math.cos(math.radians(angle))), abs(math.sin(math.radians(angle)))
ex = c * hx + s * hy
ey = s * hx + c * hy
return [round(cx - ex, 6), round(cy - ey, 6), round(cx + ex + margin, 6), round(cy + ey + margin, 6)]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[[[-3.46, 4.17, 1.32, 0.37, -60, 0.0], [-4.440429, 2.841846, -2.479571, 5.498154]], [[-3.78, 2.17, 1.99, 0.73, 315, 0.0], [-5.70333, 0.24667, -1.85667, 4.09333]], [[-4.33, 0.92, 2.68, 2.29, 30, 0.0], [-7.795948, -2.403198, -0.864052, 4.243198]], [[1.84, -3.67, 1.72, 0.21, 200, 0.25], [-0.098096, -4.70561, 3.778096, -2.63439]], [[4.84, 4.55, 2.87, 2.62, -60, 0.1], [1.036013, 0.654507, 8.643987, 8.445493]], [[3.56, 1.21, 1.64, 2.85, 120, 0.25], [0.021828, -1.885282, 7.098172, 4.305282]], [[3.5, -2.67, 1.45, 1.16, 45, 0.25], [1.404451, -4.765549, 5.595549, -0.574451]], [[0.67, -3.79, 1.53, 0.28, 30, 0.25], [-1.045019, -5.047487, 2.385019, -2.532513]]], [[[0.48, -3.95, 0.42, 0.48, 0, 0.25], [-0.19, -4.68, 1.15, -3.22]], [[2.55, 1.32, 1.52, 0.28, 45, 0.1], [1.177208, -0.052792, 3.922792, 2.692792]], [[2.76, -3.6, 1.52, 2.6, -60, 0.25], [-0.501666, -6.466359, 6.021666, -0.733641]], [[4.41, -1.11, 1.75, 0.58, 200, 0.0], [2.567166, -2.253557, 6.252834, 0.033557]], [[-0.4, -1.69, 1.89, 2.23, 45, 0.25], [-3.56328, -4.85328, 2.76328, 1.47328]], [[1.15, -0.99, 0.95, 1.77, -60, 0.0], [-0.857865, -2.697724, 3.157865, 0.717724]], [[-3.76, -4.4, 1.23, 2.09, -60, 0.25], [-6.434993, -6.760211, -1.085007, -2.039789]], [[2.76, -1.11, 1.7, 2.71, 120, 0.0], [-0.436929, -3.937243, 5.956929, 1.717243]]], [[[-1.56, -2.03, 1.18, 0.51, -60, 0.0], [-2.591673, -3.30691, -0.528327, -0.75309]], [[0.56, -3.49, 2.07, 2.95, 200, 0.0], [-2.394123, -6.970075, 3.514123, -0.009925]], [[2.99, 4.84, 1.0, 2.55, 90, 0.1], [0.34, 3.74, 5.64, 5.94]], [[-4.67, 4.48, 2.36, 1.01, 200, 0.25], [-7.483115, 2.473743, -1.856885, 6.486257]], [[-4.07, 1.15, 1.87, 2.11, 45, 0.25], [-7.134285, -1.914285, -1.005715, 4.214285]], [[-1.09, -2.14, 1.0, 0.5, 315, 0.0], [-2.15066, -3.20066, -0.02934, -1.07934]], [[-2.56, 2.58, 1.91, 2.37, -60, 0.25], [-5.81748, -0.509109, 0.69748, 5.669109]], [[4.54, -1.65, 1.92, 0.9, 45, 0.1], [2.445959, -3.744041, 6.634041, 0.444041]]], [[[4.08, 4.42, 0.73, 1.48, 90, 0.25], [2.35, 3.44, 5.81, 5.4]], [[-2.9, -3.38, 0.84, 1.4, -60, 0.25], [-4.782436, -5.057461, -1.017564, -1.702539]], [[3.13, 3.89, 2.4, 2.51, 45, 0.0], [-0.341894, 0.418106, 6.601894, 7.361894]], [[2.4, 3.01, 2.65, 2.18, 45, 0.25], [-1.265326, -0.655326, 6.065326, 6.675326]], [[1.18, 1.1, 0.22, 2.53, 120, 0.25], [-1.371044, -0.605526, 3.731044, 2.805526]], [[-1.41, -0.13, 0.56, 0.96, 120, 0.0], [-2.521384, -1.094974, -0.298616, 0.834974]], [[-0.49, -1.31, 2.52, 2.84, 200, 0.1], [-3.929363, -4.940618, 2.949363, 2.320618]], [[-1.66, -2.43, 0.41, 1.42, 120, 0.0], [-3.094756, -3.49507, -0.225244, -1.36493]]], [[[2.08, -3.48, 0.39, 0.88, 30, 0.1], [1.20225, -4.537102, 2.95775, -2.422898]], [[-3.04, 2.12, 1.02, 1.96, 90, 0.25], [-5.25, 0.85, -0.83, 3.39]], [[0.01, 3.08, 2.32, 2.81, 30, 0.0], [-3.404179, -0.513531, 3.424179, 6.673531]], [[-1.87, 3.02, 0.69, 1.01, -60, 0.0], [-3.089686, 1.917442, -0.650314, 4.122558]], [[4.13, -2.65, 2.98, 1.36, 0, 0.25], [0.9, -4.26, 7.36, -1.04]], [[-2.22, -2.6, 0.84, 1.79, 0, 0.0], [-3.06, -4.39, -1.38, -0.81]], [[-4.71, -4.06, 1.22, 0.6, 120, 0.25], [-6.089615, -5.666551, -3.330385, -2.453449]], [[-1.07, -4.21, 0.74, 2.72, 120, 0.1], [-3.895589, -6.310859, 1.755589, -2.109141]]]]
for i, (args, expected) in enumerate(fixtures[N-1]):
check("case %d" % i, 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 |
|---|---|---|---|
| case 0 | [-4.440429, 2.841846, -2.479571, 5.498154] | [-4.440429, 2.841846, -2.479571, 5.498154] | Passed |
| case 1 | [-5.70333, 0.24667, -1.85667, 4.09333] | [-5.70333, 0.24667, -1.85667, 4.09333] | Passed |
| case 2 | [-7.795948, -2.403198, -0.864052, 4.243198] | [-7.795948, -2.403198, -0.864052, 4.243198] | Passed |
| case 3 | [0.151904, -4.45561, 3.778096, -2.63439] | [-0.098096, -4.70561, 3.778096, -2.63439] | Failed |
| case 4 | [1.136013, 0.754507, 8.643987, 8.445493] | [1.036013, 0.654507, 8.643987, 8.445493] | Failed |
| case 5 | [0.271828, -1.635282, 7.098172, 4.305282] | [0.021828, -1.885282, 7.098172, 4.305282] | Failed |
| case 6 | [1.654451, -4.515549, 5.595549, -0.574451] | [1.404451, -4.765549, 5.595549, -0.574451] | Failed |
| case 7 | [-0.795019, -4.797487, 2.385019, -2.532513] | [-1.045019, -5.047487, 2.385019, -2.532513] | Failed |
SHA-256 / 2e674120f78a7523bb4b7f8737cf13defff4b26f2d6da5705e381e9ce2ca3d76
2 / The unsuccessful fix
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(cx, cy, hx, hy, angle, margin):
c, s = abs(math.cos(math.radians(angle))), abs(math.sin(math.radians(angle)))
ex = c * hx + s * hy
ey = s * hx + c * hy
return [round(cx - ex - margin / 2, 6), round(cy - ey - margin / 2, 6), round(cx + ex + margin, 6), round(cy + ey + margin, 6)]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[[[-3.46, 4.17, 1.32, 0.37, -60, 0.0], [-4.440429, 2.841846, -2.479571, 5.498154]], [[-3.78, 2.17, 1.99, 0.73, 315, 0.0], [-5.70333, 0.24667, -1.85667, 4.09333]], [[-4.33, 0.92, 2.68, 2.29, 30, 0.0], [-7.795948, -2.403198, -0.864052, 4.243198]], [[1.84, -3.67, 1.72, 0.21, 200, 0.25], [-0.098096, -4.70561, 3.778096, -2.63439]], [[4.84, 4.55, 2.87, 2.62, -60, 0.1], [1.036013, 0.654507, 8.643987, 8.445493]], [[3.56, 1.21, 1.64, 2.85, 120, 0.25], [0.021828, -1.885282, 7.098172, 4.305282]], [[3.5, -2.67, 1.45, 1.16, 45, 0.25], [1.404451, -4.765549, 5.595549, -0.574451]], [[0.67, -3.79, 1.53, 0.28, 30, 0.25], [-1.045019, -5.047487, 2.385019, -2.532513]]], [[[0.48, -3.95, 0.42, 0.48, 0, 0.25], [-0.19, -4.68, 1.15, -3.22]], [[2.55, 1.32, 1.52, 0.28, 45, 0.1], [1.177208, -0.052792, 3.922792, 2.692792]], [[2.76, -3.6, 1.52, 2.6, -60, 0.25], [-0.501666, -6.466359, 6.021666, -0.733641]], [[4.41, -1.11, 1.75, 0.58, 200, 0.0], [2.567166, -2.253557, 6.252834, 0.033557]], [[-0.4, -1.69, 1.89, 2.23, 45, 0.25], [-3.56328, -4.85328, 2.76328, 1.47328]], [[1.15, -0.99, 0.95, 1.77, -60, 0.0], [-0.857865, -2.697724, 3.157865, 0.717724]], [[-3.76, -4.4, 1.23, 2.09, -60, 0.25], [-6.434993, -6.760211, -1.085007, -2.039789]], [[2.76, -1.11, 1.7, 2.71, 120, 0.0], [-0.436929, -3.937243, 5.956929, 1.717243]]], [[[-1.56, -2.03, 1.18, 0.51, -60, 0.0], [-2.591673, -3.30691, -0.528327, -0.75309]], [[0.56, -3.49, 2.07, 2.95, 200, 0.0], [-2.394123, -6.970075, 3.514123, -0.009925]], [[2.99, 4.84, 1.0, 2.55, 90, 0.1], [0.34, 3.74, 5.64, 5.94]], [[-4.67, 4.48, 2.36, 1.01, 200, 0.25], [-7.483115, 2.473743, -1.856885, 6.486257]], [[-4.07, 1.15, 1.87, 2.11, 45, 0.25], [-7.134285, -1.914285, -1.005715, 4.214285]], [[-1.09, -2.14, 1.0, 0.5, 315, 0.0], [-2.15066, -3.20066, -0.02934, -1.07934]], [[-2.56, 2.58, 1.91, 2.37, -60, 0.25], [-5.81748, -0.509109, 0.69748, 5.669109]], [[4.54, -1.65, 1.92, 0.9, 45, 0.1], [2.445959, -3.744041, 6.634041, 0.444041]]], [[[4.08, 4.42, 0.73, 1.48, 90, 0.25], [2.35, 3.44, 5.81, 5.4]], [[-2.9, -3.38, 0.84, 1.4, -60, 0.25], [-4.782436, -5.057461, -1.017564, -1.702539]], [[3.13, 3.89, 2.4, 2.51, 45, 0.0], [-0.341894, 0.418106, 6.601894, 7.361894]], [[2.4, 3.01, 2.65, 2.18, 45, 0.25], [-1.265326, -0.655326, 6.065326, 6.675326]], [[1.18, 1.1, 0.22, 2.53, 120, 0.25], [-1.371044, -0.605526, 3.731044, 2.805526]], [[-1.41, -0.13, 0.56, 0.96, 120, 0.0], [-2.521384, -1.094974, -0.298616, 0.834974]], [[-0.49, -1.31, 2.52, 2.84, 200, 0.1], [-3.929363, -4.940618, 2.949363, 2.320618]], [[-1.66, -2.43, 0.41, 1.42, 120, 0.0], [-3.094756, -3.49507, -0.225244, -1.36493]]], [[[2.08, -3.48, 0.39, 0.88, 30, 0.1], [1.20225, -4.537102, 2.95775, -2.422898]], [[-3.04, 2.12, 1.02, 1.96, 90, 0.25], [-5.25, 0.85, -0.83, 3.39]], [[0.01, 3.08, 2.32, 2.81, 30, 0.0], [-3.404179, -0.513531, 3.424179, 6.673531]], [[-1.87, 3.02, 0.69, 1.01, -60, 0.0], [-3.089686, 1.917442, -0.650314, 4.122558]], [[4.13, -2.65, 2.98, 1.36, 0, 0.25], [0.9, -4.26, 7.36, -1.04]], [[-2.22, -2.6, 0.84, 1.79, 0, 0.0], [-3.06, -4.39, -1.38, -0.81]], [[-4.71, -4.06, 1.22, 0.6, 120, 0.25], [-6.089615, -5.666551, -3.330385, -2.453449]], [[-1.07, -4.21, 0.74, 2.72, 120, 0.1], [-3.895589, -6.310859, 1.755589, -2.109141]]]]
for i, (args, expected) in enumerate(fixtures[N-1]):
check("case %d" % i, 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 |
|---|---|---|---|
| case 0 | [-4.440429, 2.841846, -2.479571, 5.498154] | [-4.440429, 2.841846, -2.479571, 5.498154] | Passed |
| case 1 | [-5.70333, 0.24667, -1.85667, 4.09333] | [-5.70333, 0.24667, -1.85667, 4.09333] | Passed |
| case 2 | [-7.795948, -2.403198, -0.864052, 4.243198] | [-7.795948, -2.403198, -0.864052, 4.243198] | Passed |
| case 3 | [0.026904, -4.58061, 3.778096, -2.63439] | [-0.098096, -4.70561, 3.778096, -2.63439] | Failed |
| case 4 | [1.086013, 0.704507, 8.643987, 8.445493] | [1.036013, 0.654507, 8.643987, 8.445493] | Failed |
| case 5 | [0.146828, -1.760282, 7.098172, 4.305282] | [0.021828, -1.885282, 7.098172, 4.305282] | Failed |
| case 6 | [1.529451, -4.640549, 5.595549, -0.574451] | [1.404451, -4.765549, 5.595549, -0.574451] | Failed |
| case 7 | [-0.920019, -4.922487, 2.385019, -2.532513] | [-1.045019, -5.047487, 2.385019, -2.532513] | Failed |
SHA-256 / 573f52a3b83c167ab786bb7eb8a9805e2d774694075a88e2a9ebc25435e983de
3 / The verified repair
Exit 0"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(cx, cy, hx, hy, angle, margin):
c, s = abs(math.cos(math.radians(angle))), abs(math.sin(math.radians(angle)))
ex = c * hx + s * hy
ey = s * hx + c * hy
return [round(cx - ex - margin, 6), round(cy - ey - margin, 6), round(cx + ex + margin, 6), round(cy + ey + margin, 6)]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[[[-3.46, 4.17, 1.32, 0.37, -60, 0.0], [-4.440429, 2.841846, -2.479571, 5.498154]], [[-3.78, 2.17, 1.99, 0.73, 315, 0.0], [-5.70333, 0.24667, -1.85667, 4.09333]], [[-4.33, 0.92, 2.68, 2.29, 30, 0.0], [-7.795948, -2.403198, -0.864052, 4.243198]], [[1.84, -3.67, 1.72, 0.21, 200, 0.25], [-0.098096, -4.70561, 3.778096, -2.63439]], [[4.84, 4.55, 2.87, 2.62, -60, 0.1], [1.036013, 0.654507, 8.643987, 8.445493]], [[3.56, 1.21, 1.64, 2.85, 120, 0.25], [0.021828, -1.885282, 7.098172, 4.305282]], [[3.5, -2.67, 1.45, 1.16, 45, 0.25], [1.404451, -4.765549, 5.595549, -0.574451]], [[0.67, -3.79, 1.53, 0.28, 30, 0.25], [-1.045019, -5.047487, 2.385019, -2.532513]]], [[[0.48, -3.95, 0.42, 0.48, 0, 0.25], [-0.19, -4.68, 1.15, -3.22]], [[2.55, 1.32, 1.52, 0.28, 45, 0.1], [1.177208, -0.052792, 3.922792, 2.692792]], [[2.76, -3.6, 1.52, 2.6, -60, 0.25], [-0.501666, -6.466359, 6.021666, -0.733641]], [[4.41, -1.11, 1.75, 0.58, 200, 0.0], [2.567166, -2.253557, 6.252834, 0.033557]], [[-0.4, -1.69, 1.89, 2.23, 45, 0.25], [-3.56328, -4.85328, 2.76328, 1.47328]], [[1.15, -0.99, 0.95, 1.77, -60, 0.0], [-0.857865, -2.697724, 3.157865, 0.717724]], [[-3.76, -4.4, 1.23, 2.09, -60, 0.25], [-6.434993, -6.760211, -1.085007, -2.039789]], [[2.76, -1.11, 1.7, 2.71, 120, 0.0], [-0.436929, -3.937243, 5.956929, 1.717243]]], [[[-1.56, -2.03, 1.18, 0.51, -60, 0.0], [-2.591673, -3.30691, -0.528327, -0.75309]], [[0.56, -3.49, 2.07, 2.95, 200, 0.0], [-2.394123, -6.970075, 3.514123, -0.009925]], [[2.99, 4.84, 1.0, 2.55, 90, 0.1], [0.34, 3.74, 5.64, 5.94]], [[-4.67, 4.48, 2.36, 1.01, 200, 0.25], [-7.483115, 2.473743, -1.856885, 6.486257]], [[-4.07, 1.15, 1.87, 2.11, 45, 0.25], [-7.134285, -1.914285, -1.005715, 4.214285]], [[-1.09, -2.14, 1.0, 0.5, 315, 0.0], [-2.15066, -3.20066, -0.02934, -1.07934]], [[-2.56, 2.58, 1.91, 2.37, -60, 0.25], [-5.81748, -0.509109, 0.69748, 5.669109]], [[4.54, -1.65, 1.92, 0.9, 45, 0.1], [2.445959, -3.744041, 6.634041, 0.444041]]], [[[4.08, 4.42, 0.73, 1.48, 90, 0.25], [2.35, 3.44, 5.81, 5.4]], [[-2.9, -3.38, 0.84, 1.4, -60, 0.25], [-4.782436, -5.057461, -1.017564, -1.702539]], [[3.13, 3.89, 2.4, 2.51, 45, 0.0], [-0.341894, 0.418106, 6.601894, 7.361894]], [[2.4, 3.01, 2.65, 2.18, 45, 0.25], [-1.265326, -0.655326, 6.065326, 6.675326]], [[1.18, 1.1, 0.22, 2.53, 120, 0.25], [-1.371044, -0.605526, 3.731044, 2.805526]], [[-1.41, -0.13, 0.56, 0.96, 120, 0.0], [-2.521384, -1.094974, -0.298616, 0.834974]], [[-0.49, -1.31, 2.52, 2.84, 200, 0.1], [-3.929363, -4.940618, 2.949363, 2.320618]], [[-1.66, -2.43, 0.41, 1.42, 120, 0.0], [-3.094756, -3.49507, -0.225244, -1.36493]]], [[[2.08, -3.48, 0.39, 0.88, 30, 0.1], [1.20225, -4.537102, 2.95775, -2.422898]], [[-3.04, 2.12, 1.02, 1.96, 90, 0.25], [-5.25, 0.85, -0.83, 3.39]], [[0.01, 3.08, 2.32, 2.81, 30, 0.0], [-3.404179, -0.513531, 3.424179, 6.673531]], [[-1.87, 3.02, 0.69, 1.01, -60, 0.0], [-3.089686, 1.917442, -0.650314, 4.122558]], [[4.13, -2.65, 2.98, 1.36, 0, 0.25], [0.9, -4.26, 7.36, -1.04]], [[-2.22, -2.6, 0.84, 1.79, 0, 0.0], [-3.06, -4.39, -1.38, -0.81]], [[-4.71, -4.06, 1.22, 0.6, 120, 0.25], [-6.089615, -5.666551, -3.330385, -2.453449]], [[-1.07, -4.21, 0.74, 2.72, 120, 0.1], [-3.895589, -6.310859, 1.755589, -2.109141]]]]
for i, (args, expected) in enumerate(fixtures[N-1]):
check("case %d" % i, 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 |
|---|---|---|---|
| case 0 | [-4.440429, 2.841846, -2.479571, 5.498154] | [-4.440429, 2.841846, -2.479571, 5.498154] | Passed |
| case 1 | [-5.70333, 0.24667, -1.85667, 4.09333] | [-5.70333, 0.24667, -1.85667, 4.09333] | Passed |
| case 2 | [-7.795948, -2.403198, -0.864052, 4.243198] | [-7.795948, -2.403198, -0.864052, 4.243198] | Passed |
| case 3 | [-0.098096, -4.70561, 3.778096, -2.63439] | [-0.098096, -4.70561, 3.778096, -2.63439] | Passed |
| case 4 | [1.036013, 0.654507, 8.643987, 8.445493] | [1.036013, 0.654507, 8.643987, 8.445493] | Passed |
| case 5 | [0.021828, -1.885282, 7.098172, 4.305282] | [0.021828, -1.885282, 7.098172, 4.305282] | Passed |
| case 6 | [1.404451, -4.765549, 5.595549, -0.574451] | [1.404451, -4.765549, 5.595549, -0.574451] | Passed |
| case 7 | [-1.045019, -5.047487, 2.385019, -2.532513] | [-1.045019, -5.047487, 2.385019, -2.532513] | Passed |
SHA-256 / fc642dcac465688f2ff6cfb3c432e063d38a33907bb53d1c50deea6df23e49e0
Verification & scope
A deterministic bounded teaching model with stipulated toy conventions; not a production physics engine or spatial index. 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:51:02.493642+00:00.
Case digest / 599e00ede896138a59372f8812018a0eeee4461e2294f07ab22534fe25952f43