FAILURE MAP
← Case archive

FA-93831 / Solar tracker geometry / Open access

Dual-axis pointing error: angular distance metric · case 01

Small commanded offsets across north read as nearly 360 degrees of error.

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

ROOT CAUSE

The error adds raw azimuth and elevation differences.

VERIFIED REPAIR

Use the great-circle angle.

Unsuccessful approach: A planar hypotenuse still ignores azimuth wrap and convergence toward the zenith.

Case contract

Pointing error is the great-circle angle between the commanded direction and the sun: cos(err) = sin e1 sin e2 + cos e1 cos e2 cos(az1 - az2), clamped to [-1, 1]. Return [err degrees rounded 3, err <= tol].

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(cmd_az, cmd_el, sun_az, sun_el, tol):
    e1 = math.radians(cmd_el)
    e2 = math.radians(sun_el)
    d = math.radians(cmd_az - sun_az)
    c = math.sin(e1) * math.sin(e2) + math.cos(e1) * math.cos(e2) * math.cos(d)
    c = max(-1.0, min(1.0, c))
    err = abs(cmd_az - sun_az) + abs(cmd_el - sun_el)
    return [round(err, 3), err <= tol]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['normal: on target', [180, 45, 180, 45, 1], [0.0, True]],
  ['boundary: across north', [355, 30, 5, 30, 1], [8.658, False]],
  ['normal: elevation only', [90, 30, 90, 40, 5], [10.0, False]],
  ['regression: angular distance metric', [340, 45, 60, 80, 0.5], [44.136, False]],
  ['regression: angular distance metric (partial repair)', [340, 30, 270, 45, 1], [55.737, False]],
  ['control 1', [90, 10, 225, 30, 0.5], [121.081, False]],
  ['control 2', [340, 80, 225, 45, 0.5], [49.874, False]]],
 [['normal: on target', [180, 45, 180, 45, 1], [0.0, True]],
  ['boundary: across north', [355, 30, 5, 30, 1], [8.658, False]],
  ['normal: elevation only', [90, 30, 90, 40, 5], [10.0, False]],
  ['regression: angular distance metric', [200, 60, 60, 45, 5], [70.03, False]],
  ['regression: angular distance metric (partial repair)', [180, 60, 0, 60, 0.5], [60.0, False]],
  ['control 1', [180, 80, 200, 10, 5], [70.628, False]],
  ['control 2', [45, 60, 60, 80, 1], [20.49, False]]],
 [['normal: on target', [180, 45, 180, 45, 1], [0.0, True]],
  ['boundary: across north', [355, 30, 5, 30, 1], [8.658, False]],
  ['normal: elevation only', [90, 30, 90, 40, 5], [10.0, False]],
  ['regression: angular distance metric', [135, 60, 45, 45, 5], [52.239, False]],
  ['regression: angular distance metric (partial repair)', [270, 30, 0, 80, 5], [60.501, False]],
  ['control 1', [135, 80, 60, 60, 5], [28.915, False]],
  ['control 2', [60, 30, 315, 10, 0.5], [97.696, False]]],
 [['normal: on target', [180, 45, 180, 45, 1], [0.0, True]],
  ['boundary: across north', [355, 30, 5, 30, 1], [8.658, False]],
  ['normal: elevation only', [90, 30, 90, 40, 5], [10.0, False]],
  ['regression: angular distance metric', [200, 30, 60, 80, 0.5], [67.839, False]],
  ['regression: angular distance metric (partial repair)', [0, 45, 250, 60, 2], [60.564, False]],
  ['control 1', [180, 45, 45, 60, 1], [68.754, False]],
  ['control 2', [300, 10, 135, 45, 5], [123.357, False]]],
 [['normal: on target', [180, 45, 180, 45, 1], [0.0, True]],
  ['boundary: across north', [355, 30, 5, 30, 1], [8.658, False]],
  ['normal: elevation only', [90, 30, 90, 40, 5], [10.0, False]],
  ['regression: angular distance metric', [300, 30, 135, 60, 1], [89.155, False]],
  ['regression: angular distance metric (partial repair)', [270, 30, 250, 30, 5], [17.298, False]],
  ['control 1', [270, 10, 250, 60, 0.5], [52.187, False]],
  ['control 2', [300, 30, 180, 30, 2], [97.181, False]]]]
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: on target[0, True][0.0, True]Passed
boundary: across north[350, False][8.658, False]Failed
normal: elevation only[10, False][10.0, False]Passed
regression: angular distance metric[315, False][44.136, False]Failed
regression: angular distance metric (partial repair)[85, False][55.737, False]Failed
control 1[155, False][121.081, False]Failed
control 2[150, False][49.874, False]Failed

SHA-256 / ef5e92e27a4962b0de412891d0d2cdd1ec1d73de00d09ad641799bc775bb5391

2 / The unsuccessful fix

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(cmd_az, cmd_el, sun_az, sun_el, tol):
    e1 = math.radians(cmd_el)
    e2 = math.radians(sun_el)
    d = math.radians(cmd_az - sun_az)
    c = math.sin(e1) * math.sin(e2) + math.cos(e1) * math.cos(e2) * math.cos(d)
    c = max(-1.0, min(1.0, c))
    err = math.hypot(cmd_az - sun_az, cmd_el - sun_el)
    return [round(err, 3), err <= tol]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['normal: on target', [180, 45, 180, 45, 1], [0.0, True]],
  ['boundary: across north', [355, 30, 5, 30, 1], [8.658, False]],
  ['normal: elevation only', [90, 30, 90, 40, 5], [10.0, False]],
  ['regression: angular distance metric', [340, 45, 60, 80, 0.5], [44.136, False]],
  ['regression: angular distance metric (partial repair)', [340, 30, 270, 45, 1], [55.737, False]],
  ['control 1', [90, 10, 225, 30, 0.5], [121.081, False]],
  ['control 2', [340, 80, 225, 45, 0.5], [49.874, False]]],
 [['normal: on target', [180, 45, 180, 45, 1], [0.0, True]],
  ['boundary: across north', [355, 30, 5, 30, 1], [8.658, False]],
  ['normal: elevation only', [90, 30, 90, 40, 5], [10.0, False]],
  ['regression: angular distance metric', [200, 60, 60, 45, 5], [70.03, False]],
  ['regression: angular distance metric (partial repair)', [180, 60, 0, 60, 0.5], [60.0, False]],
  ['control 1', [180, 80, 200, 10, 5], [70.628, False]],
  ['control 2', [45, 60, 60, 80, 1], [20.49, False]]],
 [['normal: on target', [180, 45, 180, 45, 1], [0.0, True]],
  ['boundary: across north', [355, 30, 5, 30, 1], [8.658, False]],
  ['normal: elevation only', [90, 30, 90, 40, 5], [10.0, False]],
  ['regression: angular distance metric', [135, 60, 45, 45, 5], [52.239, False]],
  ['regression: angular distance metric (partial repair)', [270, 30, 0, 80, 5], [60.501, False]],
  ['control 1', [135, 80, 60, 60, 5], [28.915, False]],
  ['control 2', [60, 30, 315, 10, 0.5], [97.696, False]]],
 [['normal: on target', [180, 45, 180, 45, 1], [0.0, True]],
  ['boundary: across north', [355, 30, 5, 30, 1], [8.658, False]],
  ['normal: elevation only', [90, 30, 90, 40, 5], [10.0, False]],
  ['regression: angular distance metric', [200, 30, 60, 80, 0.5], [67.839, False]],
  ['regression: angular distance metric (partial repair)', [0, 45, 250, 60, 2], [60.564, False]],
  ['control 1', [180, 45, 45, 60, 1], [68.754, False]],
  ['control 2', [300, 10, 135, 45, 5], [123.357, False]]],
 [['normal: on target', [180, 45, 180, 45, 1], [0.0, True]],
  ['boundary: across north', [355, 30, 5, 30, 1], [8.658, False]],
  ['normal: elevation only', [90, 30, 90, 40, 5], [10.0, False]],
  ['regression: angular distance metric', [300, 30, 135, 60, 1], [89.155, False]],
  ['regression: angular distance metric (partial repair)', [270, 30, 250, 30, 5], [17.298, False]],
  ['control 1', [270, 10, 250, 60, 0.5], [52.187, False]],
  ['control 2', [300, 30, 180, 30, 2], [97.181, False]]]]
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: on target[0.0, True][0.0, True]Passed
boundary: across north[350.0, False][8.658, False]Failed
normal: elevation only[10.0, False][10.0, False]Passed
regression: angular distance metric[282.179, False][44.136, False]Failed
regression: angular distance metric (partial repair)[71.589, False][55.737, False]Failed
control 1[136.473, False][121.081, False]Failed
control 2[120.208, False][49.874, False]Failed

SHA-256 / e2bd277b4a5ebedcaa8ba44bfddbb86e4b56537472e3f0d0530d83f1ef3e5849

3 / The verified repair

Exit 0
"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(cmd_az, cmd_el, sun_az, sun_el, tol):
    e1 = math.radians(cmd_el)
    e2 = math.radians(sun_el)
    d = math.radians(cmd_az - sun_az)
    c = math.sin(e1) * math.sin(e2) + math.cos(e1) * math.cos(e2) * math.cos(d)
    c = max(-1.0, min(1.0, c))
    err = math.degrees(math.acos(c))
    return [round(err, 3), err <= tol]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['normal: on target', [180, 45, 180, 45, 1], [0.0, True]],
  ['boundary: across north', [355, 30, 5, 30, 1], [8.658, False]],
  ['normal: elevation only', [90, 30, 90, 40, 5], [10.0, False]],
  ['regression: angular distance metric', [340, 45, 60, 80, 0.5], [44.136, False]],
  ['regression: angular distance metric (partial repair)', [340, 30, 270, 45, 1], [55.737, False]],
  ['control 1', [90, 10, 225, 30, 0.5], [121.081, False]],
  ['control 2', [340, 80, 225, 45, 0.5], [49.874, False]]],
 [['normal: on target', [180, 45, 180, 45, 1], [0.0, True]],
  ['boundary: across north', [355, 30, 5, 30, 1], [8.658, False]],
  ['normal: elevation only', [90, 30, 90, 40, 5], [10.0, False]],
  ['regression: angular distance metric', [200, 60, 60, 45, 5], [70.03, False]],
  ['regression: angular distance metric (partial repair)', [180, 60, 0, 60, 0.5], [60.0, False]],
  ['control 1', [180, 80, 200, 10, 5], [70.628, False]],
  ['control 2', [45, 60, 60, 80, 1], [20.49, False]]],
 [['normal: on target', [180, 45, 180, 45, 1], [0.0, True]],
  ['boundary: across north', [355, 30, 5, 30, 1], [8.658, False]],
  ['normal: elevation only', [90, 30, 90, 40, 5], [10.0, False]],
  ['regression: angular distance metric', [135, 60, 45, 45, 5], [52.239, False]],
  ['regression: angular distance metric (partial repair)', [270, 30, 0, 80, 5], [60.501, False]],
  ['control 1', [135, 80, 60, 60, 5], [28.915, False]],
  ['control 2', [60, 30, 315, 10, 0.5], [97.696, False]]],
 [['normal: on target', [180, 45, 180, 45, 1], [0.0, True]],
  ['boundary: across north', [355, 30, 5, 30, 1], [8.658, False]],
  ['normal: elevation only', [90, 30, 90, 40, 5], [10.0, False]],
  ['regression: angular distance metric', [200, 30, 60, 80, 0.5], [67.839, False]],
  ['regression: angular distance metric (partial repair)', [0, 45, 250, 60, 2], [60.564, False]],
  ['control 1', [180, 45, 45, 60, 1], [68.754, False]],
  ['control 2', [300, 10, 135, 45, 5], [123.357, False]]],
 [['normal: on target', [180, 45, 180, 45, 1], [0.0, True]],
  ['boundary: across north', [355, 30, 5, 30, 1], [8.658, False]],
  ['normal: elevation only', [90, 30, 90, 40, 5], [10.0, False]],
  ['regression: angular distance metric', [300, 30, 135, 60, 1], [89.155, False]],
  ['regression: angular distance metric (partial repair)', [270, 30, 250, 30, 5], [17.298, False]],
  ['control 1', [270, 10, 250, 60, 0.5], [52.187, False]],
  ['control 2', [300, 30, 180, 30, 2], [97.181, False]]]]
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: on target[0.0, True][0.0, True]Passed
boundary: across north[8.658, False][8.658, False]Passed
normal: elevation only[10.0, False][10.0, False]Passed
regression: angular distance metric[44.136, False][44.136, False]Passed
regression: angular distance metric (partial repair)[55.737, False][55.737, False]Passed
control 1[121.081, False][121.081, False]Passed
control 2[49.874, False][49.874, False]Passed

SHA-256 / 12bb03d0c21a4981b67e4921fe29bc28d7681b5910c06e863cbd6311c1471a8e

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

Case digest / 0cc9f01ee04ef8584ae09770c2c41980ea48c8357979d18e9e7d27df6e9580ed