FAILURE MAP
← Case archive

FA-93741 / Solar tracker geometry / Open access

Module edge ground clearance limit: half module width · case 01

Rotation limits are far too restrictive and clearances too small.

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

ROOT CAUSE

The full module width is used as the lever arm about the hub.

VERIFIED REPAIR

Use half the module width.

Unsuccessful approach: Floor division drops half a millimetre for odd module widths.

Case contract

Hub height h and module width w in mm. Maximum rotation keeps the low edge at least clear above flat ground: asin((h - clear)/(w/2)), 90 when that ratio >= 1, 0 when <= 0. The command is theta clamped to +-max. Reported clearance is h - (w/2)|sin cmd| minus the ground rise under the low edge, side*(w/2)cos(cmd)tan(slope), where side is the sign of cmd or, at 0, the sign of slope (>= 0 counts positive). Return [cmd rounded 3, clearance rounded 1].

Why this case matters

Single-axis and dual-axis solar trackers turn a sun direction into actuator commands; a sign, frame or limit mistake points a whole plant away from the sun or into a mechanical stop.

1 / The failure

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(theta, h, w, clear, slope):
    half = w
    lim = (h - clear) / half
    if lim >= 1:
        amax = 90.0
    elif lim <= 0:
        amax = 0.0
    else:
        amax = math.degrees(math.asin(lim))
    cmd = max(-amax, min(amax, theta))
    r = math.radians(cmd)
    low = h - half * abs(math.sin(r))
    if cmd != 0:
        side = 1 if cmd > 0 else -1
    else:
        side = 1 if slope >= 0 else -1
    ground = side * half * math.cos(r) * math.tan(math.radians(slope))
    return [round(cmd, 3), round(low - ground, 1)]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['normal: rotation limited by clearance', [60, 1500, 2256, 500, 0], [60, 523.1]],
  ['boundary: hub below required clearance', [30, 1200, 2000, 1300, 0], [-0.0, 1200.0]],
  ['boundary: flat on sloped ground', [0, 1500, 2000, 500, -5], [0, 1412.5]],
  ['regression: half module width', [20, 2500, 2000, 2600, -10], [-0.0, 2323.7]],
  ['regression: half module width (partial repair)', [-60, 1500, 1133, 500, 0], [-60, 1009.4]],
  ['control 1', [-45, 1200, 2384, 300, -10], [-45, 208.5]],
  ['control 2', [0, 1500, 1133, 2600, -10], [-0.0, 1400.1]]],
 [['normal: rotation limited by clearance', [60, 1500, 2256, 500, 0], [60, 523.1]],
  ['boundary: hub below required clearance', [30, 1200, 2000, 1300, 0], [-0.0, 1200.0]],
  ['boundary: flat on sloped ground', [0, 1500, 2000, 500, -5], [0, 1412.5]],
  ['regression: half module width', [-30, 1500, 1133, 1300, 0], [-20.674, 1300.0]],
  ['regression: half module width (partial repair)', [60, 2000, 1133, 500, 0], [60, 1509.4]],
  ['control 1', [20, 2000, 2256, 500, -10], [20, 1801.1]],
  ['control 2', [0, 2500, 2000, 300, -5], [0, 2412.5]]],
 [['normal: rotation limited by clearance', [60, 1500, 2256, 500, 0], [60, 523.1]],
  ['boundary: hub below required clearance', [30, 1200, 2000, 1300, 0], [-0.0, 1200.0]],
  ['boundary: flat on sloped ground', [0, 1500, 2000, 500, -5], [0, 1412.5]],
  ['regression: half module width', [45, 1200, 2256, 300, 0], [45, 402.4]],
  ['regression: half module width (partial repair)', [20, 2500, 1133, 2600, 8], [-0.0, 2420.4]],
  ['control 1', [60, 2500, 1133, 1300, 8], [60, 1969.6]],
  ['control 2', [20, 1200, 2000, 1300, 3], [-0.0, 1147.6]]],
 [['normal: rotation limited by clearance', [60, 1500, 2256, 500, 0], [60, 523.1]],
  ['boundary: hub below required clearance', [30, 1200, 2000, 1300, 0], [-0.0, 1200.0]],
  ['boundary: flat on sloped ground', [0, 1500, 2000, 500, -5], [0, 1412.5]],
  ['regression: half module width', [45, 2000, 4000, 500, 0], [45, 585.8]],
  ['regression: half module width (partial repair)', [0, 2500, 1133, 300, -5], [0, 2450.4]],
  ['control 1', [60, 2000, 1133, 2600, -5], [-0.0, 1950.4]],
  ['control 2', [45, 1200, 1133, 300, 3], [45, 778.4]]],
 [['normal: rotation limited by clearance', [60, 1500, 2256, 500, 0], [60, 523.1]],
  ['boundary: hub below required clearance', [30, 1200, 2000, 1300, 0], [-0.0, 1200.0]],
  ['boundary: flat on sloped ground', [0, 1500, 2000, 500, -5], [0, 1412.5]],
  ['regression: half module width', [20, 2500, 2384, 300, -10], [20, 2289.8]],
  ['regression: half module width (partial repair)', [60, 1500, 1133, 1300, 8], [20.674, 1225.5]],
  ['control 1', [-30, 1500, 2384, 500, -5], [-30, 813.7]],
  ['control 2', [-30, 1200, 4000, 2600, 0], [-0.0, 1200.0]]]]
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
normal: rotation limited by clearance[26.312, 500.0][60, 523.1]Failed
boundary: hub below required clearance[-0.0, 1200.0][-0.0, 1200.0]Passed
boundary: flat on sloped ground[0, 1325.0][0, 1412.5]Failed
regression: half module width[-0.0, 2147.3][-0.0, 2323.7]Failed
regression: half module width (partial repair)[-60, 518.8][-60, 1009.4]Failed
control 1[-22.18, -89.3][-45, 208.5]Failed
control 2[-0.0, 1300.2][-0.0, 1400.1]Failed

SHA-256 / 1e77d782e328380f5fda6a07122ac0d9a83a169efde9c9080e3039da39a067f9

2 / The unsuccessful fix

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(theta, h, w, clear, slope):
    half = w // 2
    lim = (h - clear) / half
    if lim >= 1:
        amax = 90.0
    elif lim <= 0:
        amax = 0.0
    else:
        amax = math.degrees(math.asin(lim))
    cmd = max(-amax, min(amax, theta))
    r = math.radians(cmd)
    low = h - half * abs(math.sin(r))
    if cmd != 0:
        side = 1 if cmd > 0 else -1
    else:
        side = 1 if slope >= 0 else -1
    ground = side * half * math.cos(r) * math.tan(math.radians(slope))
    return [round(cmd, 3), round(low - ground, 1)]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['normal: rotation limited by clearance', [60, 1500, 2256, 500, 0], [60, 523.1]],
  ['boundary: hub below required clearance', [30, 1200, 2000, 1300, 0], [-0.0, 1200.0]],
  ['boundary: flat on sloped ground', [0, 1500, 2000, 500, -5], [0, 1412.5]],
  ['regression: half module width', [20, 2500, 2000, 2600, -10], [-0.0, 2323.7]],
  ['regression: half module width (partial repair)', [-60, 1500, 1133, 500, 0], [-60, 1009.4]],
  ['control 1', [-45, 1200, 2384, 300, -10], [-45, 208.5]],
  ['control 2', [0, 1500, 1133, 2600, -10], [-0.0, 1400.1]]],
 [['normal: rotation limited by clearance', [60, 1500, 2256, 500, 0], [60, 523.1]],
  ['boundary: hub below required clearance', [30, 1200, 2000, 1300, 0], [-0.0, 1200.0]],
  ['boundary: flat on sloped ground', [0, 1500, 2000, 500, -5], [0, 1412.5]],
  ['regression: half module width', [-30, 1500, 1133, 1300, 0], [-20.674, 1300.0]],
  ['regression: half module width (partial repair)', [60, 2000, 1133, 500, 0], [60, 1509.4]],
  ['control 1', [20, 2000, 2256, 500, -10], [20, 1801.1]],
  ['control 2', [0, 2500, 2000, 300, -5], [0, 2412.5]]],
 [['normal: rotation limited by clearance', [60, 1500, 2256, 500, 0], [60, 523.1]],
  ['boundary: hub below required clearance', [30, 1200, 2000, 1300, 0], [-0.0, 1200.0]],
  ['boundary: flat on sloped ground', [0, 1500, 2000, 500, -5], [0, 1412.5]],
  ['regression: half module width', [45, 1200, 2256, 300, 0], [45, 402.4]],
  ['regression: half module width (partial repair)', [20, 2500, 1133, 2600, 8], [-0.0, 2420.4]],
  ['control 1', [60, 2500, 1133, 1300, 8], [60, 1969.6]],
  ['control 2', [20, 1200, 2000, 1300, 3], [-0.0, 1147.6]]],
 [['normal: rotation limited by clearance', [60, 1500, 2256, 500, 0], [60, 523.1]],
  ['boundary: hub below required clearance', [30, 1200, 2000, 1300, 0], [-0.0, 1200.0]],
  ['boundary: flat on sloped ground', [0, 1500, 2000, 500, -5], [0, 1412.5]],
  ['regression: half module width', [45, 2000, 4000, 500, 0], [45, 585.8]],
  ['regression: half module width (partial repair)', [0, 2500, 1133, 300, -5], [0, 2450.4]],
  ['control 1', [60, 2000, 1133, 2600, -5], [-0.0, 1950.4]],
  ['control 2', [45, 1200, 1133, 300, 3], [45, 778.4]]],
 [['normal: rotation limited by clearance', [60, 1500, 2256, 500, 0], [60, 523.1]],
  ['boundary: hub below required clearance', [30, 1200, 2000, 1300, 0], [-0.0, 1200.0]],
  ['boundary: flat on sloped ground', [0, 1500, 2000, 500, -5], [0, 1412.5]],
  ['regression: half module width', [20, 2500, 2384, 300, -10], [20, 2289.8]],
  ['regression: half module width (partial repair)', [60, 1500, 1133, 1300, 8], [20.674, 1225.5]],
  ['control 1', [-30, 1500, 2384, 500, -5], [-30, 813.7]],
  ['control 2', [-30, 1200, 4000, 2600, 0], [-0.0, 1200.0]]]]
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
normal: rotation limited by clearance[60, 523.1][60, 523.1]Passed
boundary: hub below required clearance[-0.0, 1200.0][-0.0, 1200.0]Passed
boundary: flat on sloped ground[0, 1412.5][0, 1412.5]Passed
regression: half module width[-0.0, 2323.7][-0.0, 2323.7]Passed
regression: half module width (partial repair)[-60, 1009.8][-60, 1009.4]Failed
control 1[-45, 208.5][-45, 208.5]Passed
control 2[-0.0, 1400.2][-0.0, 1400.1]Failed

SHA-256 / 46166dd0cc8a9eea41678f17940a4ec05fd25dd0b3c4406b392c5c6942ff7aa2

3 / The verified repair

Exit 0
"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(theta, h, w, clear, slope):
    half = w / 2
    lim = (h - clear) / half
    if lim >= 1:
        amax = 90.0
    elif lim <= 0:
        amax = 0.0
    else:
        amax = math.degrees(math.asin(lim))
    cmd = max(-amax, min(amax, theta))
    r = math.radians(cmd)
    low = h - half * abs(math.sin(r))
    if cmd != 0:
        side = 1 if cmd > 0 else -1
    else:
        side = 1 if slope >= 0 else -1
    ground = side * half * math.cos(r) * math.tan(math.radians(slope))
    return [round(cmd, 3), round(low - ground, 1)]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['normal: rotation limited by clearance', [60, 1500, 2256, 500, 0], [60, 523.1]],
  ['boundary: hub below required clearance', [30, 1200, 2000, 1300, 0], [-0.0, 1200.0]],
  ['boundary: flat on sloped ground', [0, 1500, 2000, 500, -5], [0, 1412.5]],
  ['regression: half module width', [20, 2500, 2000, 2600, -10], [-0.0, 2323.7]],
  ['regression: half module width (partial repair)', [-60, 1500, 1133, 500, 0], [-60, 1009.4]],
  ['control 1', [-45, 1200, 2384, 300, -10], [-45, 208.5]],
  ['control 2', [0, 1500, 1133, 2600, -10], [-0.0, 1400.1]]],
 [['normal: rotation limited by clearance', [60, 1500, 2256, 500, 0], [60, 523.1]],
  ['boundary: hub below required clearance', [30, 1200, 2000, 1300, 0], [-0.0, 1200.0]],
  ['boundary: flat on sloped ground', [0, 1500, 2000, 500, -5], [0, 1412.5]],
  ['regression: half module width', [-30, 1500, 1133, 1300, 0], [-20.674, 1300.0]],
  ['regression: half module width (partial repair)', [60, 2000, 1133, 500, 0], [60, 1509.4]],
  ['control 1', [20, 2000, 2256, 500, -10], [20, 1801.1]],
  ['control 2', [0, 2500, 2000, 300, -5], [0, 2412.5]]],
 [['normal: rotation limited by clearance', [60, 1500, 2256, 500, 0], [60, 523.1]],
  ['boundary: hub below required clearance', [30, 1200, 2000, 1300, 0], [-0.0, 1200.0]],
  ['boundary: flat on sloped ground', [0, 1500, 2000, 500, -5], [0, 1412.5]],
  ['regression: half module width', [45, 1200, 2256, 300, 0], [45, 402.4]],
  ['regression: half module width (partial repair)', [20, 2500, 1133, 2600, 8], [-0.0, 2420.4]],
  ['control 1', [60, 2500, 1133, 1300, 8], [60, 1969.6]],
  ['control 2', [20, 1200, 2000, 1300, 3], [-0.0, 1147.6]]],
 [['normal: rotation limited by clearance', [60, 1500, 2256, 500, 0], [60, 523.1]],
  ['boundary: hub below required clearance', [30, 1200, 2000, 1300, 0], [-0.0, 1200.0]],
  ['boundary: flat on sloped ground', [0, 1500, 2000, 500, -5], [0, 1412.5]],
  ['regression: half module width', [45, 2000, 4000, 500, 0], [45, 585.8]],
  ['regression: half module width (partial repair)', [0, 2500, 1133, 300, -5], [0, 2450.4]],
  ['control 1', [60, 2000, 1133, 2600, -5], [-0.0, 1950.4]],
  ['control 2', [45, 1200, 1133, 300, 3], [45, 778.4]]],
 [['normal: rotation limited by clearance', [60, 1500, 2256, 500, 0], [60, 523.1]],
  ['boundary: hub below required clearance', [30, 1200, 2000, 1300, 0], [-0.0, 1200.0]],
  ['boundary: flat on sloped ground', [0, 1500, 2000, 500, -5], [0, 1412.5]],
  ['regression: half module width', [20, 2500, 2384, 300, -10], [20, 2289.8]],
  ['regression: half module width (partial repair)', [60, 1500, 1133, 1300, 8], [20.674, 1225.5]],
  ['control 1', [-30, 1500, 2384, 500, -5], [-30, 813.7]],
  ['control 2', [-30, 1200, 4000, 2600, 0], [-0.0, 1200.0]]]]
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
normal: rotation limited by clearance[60, 523.1][60, 523.1]Passed
boundary: hub below required clearance[-0.0, 1200.0][-0.0, 1200.0]Passed
boundary: flat on sloped ground[0, 1412.5][0, 1412.5]Passed
regression: half module width[-0.0, 2323.7][-0.0, 2323.7]Passed
regression: half module width (partial repair)[-60, 1009.4][-60, 1009.4]Passed
control 1[-45, 208.5][-45, 208.5]Passed
control 2[-0.0, 1400.1][-0.0, 1400.1]Passed

SHA-256 / f479bbb93ed31cd347f81da9f6a48442b05ccdb8f8486b270b196b897db6f487

Verification & scope

Deterministic stipulated toy contract for teaching; no claim of conformance with any standard, vendor protocol or production controller. 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:58.059690+00:00.

Case digest / cabec349c1aedaba7bec21a8c32c2e2929f67cf4bcff59c17ad4ae6050ac8758