FAILURE MAP
← Case archive

FA-93531 / Solar tracker geometry / Open access

Tracker actuator step command: step truncation direction · case 01

Eastward moves overshoot the slew limit by one motor step.

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

ROOT CAUSE

Floor division rounds negative moves away from zero.

VERIFIED REPAIR

Truncate the step count toward zero.

Unsuccessful approach: Rounding to nearest can exceed the slew limit in both directions.

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 = min(max(target, lo), hi)
    err = tgt - current
    if abs(err) < deadband:
        return [current, 0]
    move = max(-max_step, min(max_step, err))
    steps = move // dps
    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: step truncation direction', [87, 3902, 250, 500, 90, -52000, 55000], [3452, -5]],
  ['regression: step truncation direction (partial repair)',
   [1899, -3811, 250, 5000, 300, -60000, 60000], [989, 16]],
  ['control 1', [-57417, -56117, 500, 2000, 300, -52000, 60000], [-54317, 6]],
  ['control 2', [-62089, -29689, 500, 5000, 300, -55000, 60000], [-34489, -16]]],
 [['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: step truncation direction', [-54271, -9833, 0, 2000, 90, -52000, 52000],
   [-11813, -22]],
  ['regression: step truncation direction (partial repair)',
   [-47305, -34773, 1000, 5000, 300, -60000, 55000], [-39573, -16]],
  ['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: step truncation direction', [25571, 48224, 500, 2000, 90, -52000, 55000],
   [46244, -22]],
  ['regression: step truncation direction (partial repair)',
   [-24432, -36322, 1000, 5000, 300, -60000, 52000], [-31522, 16]],
  ['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: step truncation direction', [-50922, -33896, 1000, 2000, 7, -52000, 60000],
   [-35891, -285]],
  ['regression: step truncation direction (partial repair)',
   [56539, -49336, 500, 2000, 7, -60000, 55000], [-47341, 285]],
  ['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: step truncation direction', [3491, 18980, 250, 2000, 7, -55000, 60000],
   [16985, -285]],
  ['regression: step truncation direction (partial repair)',
   [60232, -13012, 500, 500, 300, -60000, 55000], [-12712, 1]],
  ['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 fixtureActualExpectedOutcome
boundary: error equal to deadband[1500, 4][1500, 4]Passed
boundary: target past west stop[59994, 142][59994, 142]Passed
normal: negative move truncation[-2100, -7][-1800, -6]Failed
regression: step truncation direction[3362, -6][3452, -5]Failed
regression: step truncation direction (partial repair)[989, 16][989, 16]Passed
control 1[-54317, 6][-54317, 6]Passed
control 2[-34789, -17][-34489, -16]Failed

SHA-256 / cdcabf9aef4070ec237f260c443ccaa1736061a6ba047c8d03f1c6483ac1adf3

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(max(target, lo), hi)
    err = tgt - current
    if abs(err) < deadband:
        return [current, 0]
    move = max(-max_step, min(max_step, err))
    steps = round(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: step truncation direction', [87, 3902, 250, 500, 90, -52000, 55000], [3452, -5]],
  ['regression: step truncation direction (partial repair)',
   [1899, -3811, 250, 5000, 300, -60000, 60000], [989, 16]],
  ['control 1', [-57417, -56117, 500, 2000, 300, -52000, 60000], [-54317, 6]],
  ['control 2', [-62089, -29689, 500, 5000, 300, -55000, 60000], [-34489, -16]]],
 [['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: step truncation direction', [-54271, -9833, 0, 2000, 90, -52000, 52000],
   [-11813, -22]],
  ['regression: step truncation direction (partial repair)',
   [-47305, -34773, 1000, 5000, 300, -60000, 55000], [-39573, -16]],
  ['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: step truncation direction', [25571, 48224, 500, 2000, 90, -52000, 55000],
   [46244, -22]],
  ['regression: step truncation direction (partial repair)',
   [-24432, -36322, 1000, 5000, 300, -60000, 52000], [-31522, 16]],
  ['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: step truncation direction', [-50922, -33896, 1000, 2000, 7, -52000, 60000],
   [-35891, -285]],
  ['regression: step truncation direction (partial repair)',
   [56539, -49336, 500, 2000, 7, -60000, 55000], [-47341, 285]],
  ['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: step truncation direction', [3491, 18980, 250, 2000, 7, -55000, 60000],
   [16985, -285]],
  ['regression: step truncation direction (partial repair)',
   [60232, -13012, 500, 500, 300, -60000, 55000], [-12712, 1]],
  ['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 fixtureActualExpectedOutcome
boundary: error equal to deadband[1500, 4][1500, 4]Passed
boundary: target past west stop[60001, 143][59994, 142]Failed
normal: negative move truncation[-2100, -7][-1800, -6]Failed
regression: step truncation direction[3362, -6][3452, -5]Failed
regression: step truncation direction (partial repair)[1289, 17][989, 16]Failed
control 1[-54017, 7][-54317, 6]Failed
control 2[-34789, -17][-34489, -16]Failed

SHA-256 / 6af45ef7d68f86f4cf010618838167592c8fdfc1ae0f19d2f41ec34dafbb5438

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: step truncation direction', [87, 3902, 250, 500, 90, -52000, 55000], [3452, -5]],
  ['regression: step truncation direction (partial repair)',
   [1899, -3811, 250, 5000, 300, -60000, 60000], [989, 16]],
  ['control 1', [-57417, -56117, 500, 2000, 300, -52000, 60000], [-54317, 6]],
  ['control 2', [-62089, -29689, 500, 5000, 300, -55000, 60000], [-34489, -16]]],
 [['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: step truncation direction', [-54271, -9833, 0, 2000, 90, -52000, 52000],
   [-11813, -22]],
  ['regression: step truncation direction (partial repair)',
   [-47305, -34773, 1000, 5000, 300, -60000, 55000], [-39573, -16]],
  ['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: step truncation direction', [25571, 48224, 500, 2000, 90, -52000, 55000],
   [46244, -22]],
  ['regression: step truncation direction (partial repair)',
   [-24432, -36322, 1000, 5000, 300, -60000, 52000], [-31522, 16]],
  ['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: step truncation direction', [-50922, -33896, 1000, 2000, 7, -52000, 60000],
   [-35891, -285]],
  ['regression: step truncation direction (partial repair)',
   [56539, -49336, 500, 2000, 7, -60000, 55000], [-47341, 285]],
  ['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: step truncation direction', [3491, 18980, 250, 2000, 7, -55000, 60000],
   [16985, -285]],
  ['regression: step truncation direction (partial repair)',
   [60232, -13012, 500, 500, 300, -60000, 55000], [-12712, 1]],
  ['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 fixtureActualExpectedOutcome
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: step truncation direction[3452, -5][3452, -5]Passed
regression: step truncation direction (partial repair)[989, 16][989, 16]Passed
control 1[-54317, 6][-54317, 6]Passed
control 2[-34489, -16][-34489, -16]Passed

SHA-256 / 347a4e041f03e82829a5ed59082d82ba908ab91c1a4b4fa082f1bacc5b3bd9ab

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:56.152532+00:00.

Case digest / b0e79c9d6639198b817863db04d6e91c65d224e3e00a38ab5555c3071d6f40cf