FAILURE MAP
← Case archive

FA-93826 / Solar tracker geometry / Open access

Dual-axis pointing error: elevation trig terms · case 01

Azimuth errors near the zenith are reported as huge.

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

ROOT CAUSE

Elevations are treated as zenith angles in the spherical law of cosines.

VERIFIED REPAIR

Use sin e1 sin e2 + cos e1 cos e2 cos dAz.

Unsuccessful approach: Dropping the cos e factors lets azimuth error dominate at any elevation.

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.cos(e1) * math.cos(e2) + math.sin(e1) * math.sin(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: elevation trig terms', [340, 45, 60, 80, 0.5], [44.136, False]],
  ['regression: elevation trig terms (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: elevation trig terms', [200, 60, 60, 45, 5], [70.03, False]],
  ['regression: elevation trig terms (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: elevation trig terms', [135, 60, 45, 45, 5], [52.239, False]],
  ['regression: elevation trig terms (partial repair)', [270, 45, 340, 60, 5], [42.837, 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: elevation trig terms', [200, 30, 60, 80, 0.5], [67.839, False]],
  ['regression: elevation trig terms (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: elevation trig terms', [270, 30, 250, 30, 5], [17.298, False]],
  ['regression: elevation trig terms (partial repair)', [300, 30, 135, 60, 1], [89.155, 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[4.995, False][8.658, False]Failed
normal: elevation only[10.0, False][10.0, False]Passed
regression: elevation trig terms[75.894, False][44.136, False]Failed
regression: elevation trig terms (partial repair)[42.837, False][55.737, False]Failed
control 1[37.676, False][121.081, False]Failed
control 2[99.876, False][49.874, False]Failed

SHA-256 / 12ae00c9fb9b2081c9f2c130adf51368905793ec0046c2f3d1f7936ccf7e2e47

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(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: elevation trig terms', [340, 45, 60, 80, 0.5], [44.136, False]],
  ['regression: elevation trig terms (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: elevation trig terms', [200, 60, 60, 45, 5], [70.03, False]],
  ['regression: elevation trig terms (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: elevation trig terms', [135, 60, 45, 45, 5], [52.239, False]],
  ['regression: elevation trig terms (partial repair)', [270, 45, 340, 60, 5], [42.837, 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: elevation trig terms', [200, 30, 60, 80, 0.5], [67.839, False]],
  ['regression: elevation trig terms (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: elevation trig terms', [270, 30, 250, 30, 5], [17.298, False]],
  ['regression: elevation trig terms (partial repair)', [300, 30, 135, 60, 1], [89.155, 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[0.0, True][8.658, False]Failed
normal: elevation only[0.0, True][10.0, False]Failed
regression: elevation trig terms[29.54, False][44.136, False]Failed
regression: elevation trig terms (partial repair)[45.927, False][55.737, False]Failed
control 1[128.337, False][121.081, False]Failed
control 2[74.113, False][49.874, False]Failed

SHA-256 / dc39dc85d73026d175a0a05a7b911992b796f2745f4939e7ed89e1382f44bc12

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: elevation trig terms', [340, 45, 60, 80, 0.5], [44.136, False]],
  ['regression: elevation trig terms (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: elevation trig terms', [200, 60, 60, 45, 5], [70.03, False]],
  ['regression: elevation trig terms (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: elevation trig terms', [135, 60, 45, 45, 5], [52.239, False]],
  ['regression: elevation trig terms (partial repair)', [270, 45, 340, 60, 5], [42.837, 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: elevation trig terms', [200, 30, 60, 80, 0.5], [67.839, False]],
  ['regression: elevation trig terms (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: elevation trig terms', [270, 30, 250, 30, 5], [17.298, False]],
  ['regression: elevation trig terms (partial repair)', [300, 30, 135, 60, 1], [89.155, 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: elevation trig terms[44.136, False][44.136, False]Passed
regression: elevation trig terms (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 / feed4f40c0ff098eb5470b609ab9c49d8d0f2ea5136e5ddcae0c814ded8ac430

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

Case digest / a8c71cfbb0f59565c2ec045d139f8d158448f284ba5948866c6bd21e69683cda