FA-93516 / Solar tracker geometry / Open access
Tracker actuator step command: target soft limits · case 01
The actuator drives into the mechanical stop when asked for an out-of-range angle.
ROOT CAUSE
The target is not clamped to the soft limits.
VERIFIED REPAIR
Clamp the target to [lo, hi].
Unsuccessful approach: Clamping only the upper limit leaves the east stop unprotected.
Case contract
Angles in millidegrees. The target is clamped to [lo, hi]. If |target - current| < deadband no move is made. Otherwise the move is limited to +-max_step, converted to whole motor steps of dps millidegrees truncated toward zero. Return [new position, signed steps].
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
N = 1
observations = []
def solve(target, current, deadband, max_step, dps, lo, hi):
tgt = target
err = tgt - current
if abs(err) < deadband:
return [current, 0]
move = max(-max_step, min(max_step, err))
steps = abs(move) // dps
if move < 0:
steps = -steps
return [current + steps * dps, steps]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['boundary: error equal to deadband', [1500, 1000, 500, 2000, 125, -60000, 60000], [1500, 4]],
['boundary: target past west stop', [80000, 59000, 250, 5000, 7, -60000, 60000], [59994, 142]],
['normal: negative move truncation', [-10000, 0, 250, 2000, 300, -60000, 60000], [-1800, -6]],
['regression: target soft limits', [-57417, -56117, 500, 2000, 300, -52000, 60000], [-54317, 6]],
['regression: target soft limits (partial repair)',
[-57509, -53543, 250, 5000, 90, -52000, 52000], [-52013, 17]],
['control 1', [-62089, -29689, 500, 5000, 300, -55000, 60000], [-34489, -16]],
['control 2', [-4975, 2618, 1000, 5000, 7, -55000, 60000], [-2380, -714]]],
[['boundary: error equal to deadband', [1500, 1000, 500, 2000, 125, -60000, 60000], [1500, 4]],
['boundary: target past west stop', [80000, 59000, 250, 5000, 7, -60000, 60000], [59994, 142]],
['normal: negative move truncation', [-10000, 0, 250, 2000, 300, -60000, 60000], [-1800, -6]],
['regression: target soft limits', [-57509, -53543, 250, 5000, 90, -52000, 52000], [-52013, 17]],
['regression: target soft limits (partial repair)',
[-57417, -56117, 500, 2000, 300, -52000, 60000], [-54317, 6]],
['control 1', [48241, -7501, 500, 500, 125, -55000, 60000], [-7001, 4]],
['control 2', [53151, 50604, 0, 5000, 7, -55000, 55000], [53145, 363]]],
[['boundary: error equal to deadband', [1500, 1000, 500, 2000, 125, -60000, 60000], [1500, 4]],
['boundary: target past west stop', [80000, 59000, 250, 5000, 7, -60000, 60000], [59994, 142]],
['normal: negative move truncation', [-10000, 0, 250, 2000, 300, -60000, 60000], [-1800, -6]],
['regression: target soft limits', [63584, 52627, 500, 5000, 90, -52000, 55000], [54967, 26]],
['regression: target soft limits (partial repair)',
[-57417, -56117, 500, 2000, 300, -52000, 60000], [-54317, 6]],
['control 1', [-63679, -20673, 250, 5000, 125, -52000, 60000], [-25673, -40]],
['control 2', [13642, -49671, 1000, 5000, 7, -52000, 55000], [-44673, 714]]],
[['boundary: error equal to deadband', [1500, 1000, 500, 2000, 125, -60000, 60000], [1500, 4]],
['boundary: target past west stop', [80000, 59000, 250, 5000, 7, -60000, 60000], [59994, 142]],
['normal: negative move truncation', [-10000, 0, 250, 2000, 300, -60000, 60000], [-1800, -6]],
['regression: target soft limits', [-57417, -56117, 500, 2000, 300, -52000, 60000], [-54317, 6]],
['regression: target soft limits (partial repair)',
[-57509, -53543, 250, 5000, 90, -52000, 52000], [-52013, 17]],
['control 1', [-66913, 56864, 250, 5000, 125, -52000, 52000], [51864, -40]],
['control 2', [19310, -10964, 250, 5000, 125, -52000, 52000], [-5964, 40]]],
[['boundary: error equal to deadband', [1500, 1000, 500, 2000, 125, -60000, 60000], [1500, 4]],
['boundary: target past west stop', [80000, 59000, 250, 5000, 7, -60000, 60000], [59994, 142]],
['normal: negative move truncation', [-10000, 0, 250, 2000, 300, -60000, 60000], [-1800, -6]],
['regression: target soft limits', [-57417, -56117, 500, 2000, 300, -52000, 60000], [-54317, 6]],
['regression: target soft limits (partial repair)',
[-57509, -53543, 250, 5000, 90, -52000, 52000], [-52013, 17]],
['control 1', [22704, 24328, 1000, 5000, 300, -60000, 55000], [22828, -5]],
['control 2', [56539, -49336, 500, 2000, 7, -60000, 55000], [-47341, 285]]]]
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 |
|---|---|---|---|
| boundary: error equal to deadband | [1500, 4] | [1500, 4] | Passed |
| boundary: target past west stop | [63998, 714] | [59994, 142] | Failed |
| normal: negative move truncation | [-1800, -6] | [-1800, -6] | Passed |
| regression: target soft limits | [-57317, -4] | [-54317, 6] | Failed |
| regression: target soft limits (partial repair) | [-57503, -44] | [-52013, 17] | Failed |
| control 1 | [-34489, -16] | [-34489, -16] | Passed |
| control 2 | [-2380, -714] | [-2380, -714] | Passed |
SHA-256 / c7a182878cfd15a58b6d599c577cdb47dfa009c66bdca77e9d0e858125df9ff3
2 / The unsuccessful fix
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(target, current, deadband, max_step, dps, lo, hi):
tgt = min(target, hi)
err = tgt - current
if abs(err) < deadband:
return [current, 0]
move = max(-max_step, min(max_step, err))
steps = abs(move) // dps
if move < 0:
steps = -steps
return [current + steps * dps, steps]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['boundary: error equal to deadband', [1500, 1000, 500, 2000, 125, -60000, 60000], [1500, 4]],
['boundary: target past west stop', [80000, 59000, 250, 5000, 7, -60000, 60000], [59994, 142]],
['normal: negative move truncation', [-10000, 0, 250, 2000, 300, -60000, 60000], [-1800, -6]],
['regression: target soft limits', [-57417, -56117, 500, 2000, 300, -52000, 60000], [-54317, 6]],
['regression: target soft limits (partial repair)',
[-57509, -53543, 250, 5000, 90, -52000, 52000], [-52013, 17]],
['control 1', [-62089, -29689, 500, 5000, 300, -55000, 60000], [-34489, -16]],
['control 2', [-4975, 2618, 1000, 5000, 7, -55000, 60000], [-2380, -714]]],
[['boundary: error equal to deadband', [1500, 1000, 500, 2000, 125, -60000, 60000], [1500, 4]],
['boundary: target past west stop', [80000, 59000, 250, 5000, 7, -60000, 60000], [59994, 142]],
['normal: negative move truncation', [-10000, 0, 250, 2000, 300, -60000, 60000], [-1800, -6]],
['regression: target soft limits', [-57509, -53543, 250, 5000, 90, -52000, 52000], [-52013, 17]],
['regression: target soft limits (partial repair)',
[-57417, -56117, 500, 2000, 300, -52000, 60000], [-54317, 6]],
['control 1', [48241, -7501, 500, 500, 125, -55000, 60000], [-7001, 4]],
['control 2', [53151, 50604, 0, 5000, 7, -55000, 55000], [53145, 363]]],
[['boundary: error equal to deadband', [1500, 1000, 500, 2000, 125, -60000, 60000], [1500, 4]],
['boundary: target past west stop', [80000, 59000, 250, 5000, 7, -60000, 60000], [59994, 142]],
['normal: negative move truncation', [-10000, 0, 250, 2000, 300, -60000, 60000], [-1800, -6]],
['regression: target soft limits', [63584, 52627, 500, 5000, 90, -52000, 55000], [54967, 26]],
['regression: target soft limits (partial repair)',
[-57417, -56117, 500, 2000, 300, -52000, 60000], [-54317, 6]],
['control 1', [-63679, -20673, 250, 5000, 125, -52000, 60000], [-25673, -40]],
['control 2', [13642, -49671, 1000, 5000, 7, -52000, 55000], [-44673, 714]]],
[['boundary: error equal to deadband', [1500, 1000, 500, 2000, 125, -60000, 60000], [1500, 4]],
['boundary: target past west stop', [80000, 59000, 250, 5000, 7, -60000, 60000], [59994, 142]],
['normal: negative move truncation', [-10000, 0, 250, 2000, 300, -60000, 60000], [-1800, -6]],
['regression: target soft limits', [-57417, -56117, 500, 2000, 300, -52000, 60000], [-54317, 6]],
['regression: target soft limits (partial repair)',
[-57509, -53543, 250, 5000, 90, -52000, 52000], [-52013, 17]],
['control 1', [-66913, 56864, 250, 5000, 125, -52000, 52000], [51864, -40]],
['control 2', [19310, -10964, 250, 5000, 125, -52000, 52000], [-5964, 40]]],
[['boundary: error equal to deadband', [1500, 1000, 500, 2000, 125, -60000, 60000], [1500, 4]],
['boundary: target past west stop', [80000, 59000, 250, 5000, 7, -60000, 60000], [59994, 142]],
['normal: negative move truncation', [-10000, 0, 250, 2000, 300, -60000, 60000], [-1800, -6]],
['regression: target soft limits', [-57417, -56117, 500, 2000, 300, -52000, 60000], [-54317, 6]],
['regression: target soft limits (partial repair)',
[-57509, -53543, 250, 5000, 90, -52000, 52000], [-52013, 17]],
['control 1', [22704, 24328, 1000, 5000, 300, -60000, 55000], [22828, -5]],
['control 2', [56539, -49336, 500, 2000, 7, -60000, 55000], [-47341, 285]]]]
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 |
|---|---|---|---|
| boundary: error equal to deadband | [1500, 4] | [1500, 4] | Passed |
| boundary: target past west stop | [59994, 142] | [59994, 142] | Passed |
| normal: negative move truncation | [-1800, -6] | [-1800, -6] | Passed |
| regression: target soft limits | [-57317, -4] | [-54317, 6] | Failed |
| regression: target soft limits (partial repair) | [-57503, -44] | [-52013, 17] | Failed |
| control 1 | [-34489, -16] | [-34489, -16] | Passed |
| control 2 | [-2380, -714] | [-2380, -714] | Passed |
SHA-256 / a8daafe1df5b81f3f3b5c27bc0e277904badc9d4bb5248d8c8a5b9f47ebf4657
3 / The verified repair
Exit 0"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(target, current, deadband, max_step, dps, lo, hi):
tgt = min(max(target, lo), hi)
err = tgt - current
if abs(err) < deadband:
return [current, 0]
move = max(-max_step, min(max_step, err))
steps = abs(move) // dps
if move < 0:
steps = -steps
return [current + steps * dps, steps]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['boundary: error equal to deadband', [1500, 1000, 500, 2000, 125, -60000, 60000], [1500, 4]],
['boundary: target past west stop', [80000, 59000, 250, 5000, 7, -60000, 60000], [59994, 142]],
['normal: negative move truncation', [-10000, 0, 250, 2000, 300, -60000, 60000], [-1800, -6]],
['regression: target soft limits', [-57417, -56117, 500, 2000, 300, -52000, 60000], [-54317, 6]],
['regression: target soft limits (partial repair)',
[-57509, -53543, 250, 5000, 90, -52000, 52000], [-52013, 17]],
['control 1', [-62089, -29689, 500, 5000, 300, -55000, 60000], [-34489, -16]],
['control 2', [-4975, 2618, 1000, 5000, 7, -55000, 60000], [-2380, -714]]],
[['boundary: error equal to deadband', [1500, 1000, 500, 2000, 125, -60000, 60000], [1500, 4]],
['boundary: target past west stop', [80000, 59000, 250, 5000, 7, -60000, 60000], [59994, 142]],
['normal: negative move truncation', [-10000, 0, 250, 2000, 300, -60000, 60000], [-1800, -6]],
['regression: target soft limits', [-57509, -53543, 250, 5000, 90, -52000, 52000], [-52013, 17]],
['regression: target soft limits (partial repair)',
[-57417, -56117, 500, 2000, 300, -52000, 60000], [-54317, 6]],
['control 1', [48241, -7501, 500, 500, 125, -55000, 60000], [-7001, 4]],
['control 2', [53151, 50604, 0, 5000, 7, -55000, 55000], [53145, 363]]],
[['boundary: error equal to deadband', [1500, 1000, 500, 2000, 125, -60000, 60000], [1500, 4]],
['boundary: target past west stop', [80000, 59000, 250, 5000, 7, -60000, 60000], [59994, 142]],
['normal: negative move truncation', [-10000, 0, 250, 2000, 300, -60000, 60000], [-1800, -6]],
['regression: target soft limits', [63584, 52627, 500, 5000, 90, -52000, 55000], [54967, 26]],
['regression: target soft limits (partial repair)',
[-57417, -56117, 500, 2000, 300, -52000, 60000], [-54317, 6]],
['control 1', [-63679, -20673, 250, 5000, 125, -52000, 60000], [-25673, -40]],
['control 2', [13642, -49671, 1000, 5000, 7, -52000, 55000], [-44673, 714]]],
[['boundary: error equal to deadband', [1500, 1000, 500, 2000, 125, -60000, 60000], [1500, 4]],
['boundary: target past west stop', [80000, 59000, 250, 5000, 7, -60000, 60000], [59994, 142]],
['normal: negative move truncation', [-10000, 0, 250, 2000, 300, -60000, 60000], [-1800, -6]],
['regression: target soft limits', [-57417, -56117, 500, 2000, 300, -52000, 60000], [-54317, 6]],
['regression: target soft limits (partial repair)',
[-57509, -53543, 250, 5000, 90, -52000, 52000], [-52013, 17]],
['control 1', [-66913, 56864, 250, 5000, 125, -52000, 52000], [51864, -40]],
['control 2', [19310, -10964, 250, 5000, 125, -52000, 52000], [-5964, 40]]],
[['boundary: error equal to deadband', [1500, 1000, 500, 2000, 125, -60000, 60000], [1500, 4]],
['boundary: target past west stop', [80000, 59000, 250, 5000, 7, -60000, 60000], [59994, 142]],
['normal: negative move truncation', [-10000, 0, 250, 2000, 300, -60000, 60000], [-1800, -6]],
['regression: target soft limits', [-57417, -56117, 500, 2000, 300, -52000, 60000], [-54317, 6]],
['regression: target soft limits (partial repair)',
[-57509, -53543, 250, 5000, 90, -52000, 52000], [-52013, 17]],
['control 1', [22704, 24328, 1000, 5000, 300, -60000, 55000], [22828, -5]],
['control 2', [56539, -49336, 500, 2000, 7, -60000, 55000], [-47341, 285]]]]
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 |
|---|---|---|---|
| boundary: error equal to deadband | [1500, 4] | [1500, 4] | Passed |
| boundary: target past west stop | [59994, 142] | [59994, 142] | Passed |
| normal: negative move truncation | [-1800, -6] | [-1800, -6] | Passed |
| regression: target soft limits | [-54317, 6] | [-54317, 6] | Passed |
| regression: target soft limits (partial repair) | [-52013, 17] | [-52013, 17] | Passed |
| control 1 | [-34489, -16] | [-34489, -16] | Passed |
| control 2 | [-2380, -714] | [-2380, -714] | Passed |
SHA-256 / 5416bf820943f8d61f88c9e3c9e7f1998f1b0aec85f24c79a4678055a7b0aa39
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:55.807197+00:00.
Case digest / 590e71e8275b77a532362b17bbd9a9d4302cd663958d44c89b6f9dce53d2a87e