FA-93361 / Solar tracker geometry / Open access
Horizontal single-axis true tracking: symmetric rotation limit · case 01
In the morning the tracker is commanded past its east mechanical stop.
ROOT CAUSE
Only the positive rotation limit is enforced.
VERIFIED REPAIR
Clamp both directions with copysign(max_angle, rot).
Unsuccessful approach: Clamping to +max_angle alone sends morning limits to the west stop.
Case contract
Sun azimuth clockwise from north and elevation in degrees. The horizontal axis points to axis_az; rotation is positive toward azimuth axis_az+90. Sun vector east=cos(el)sin(az), north=cos(el)cos(az), up=sin(el). Rotation = atan2(component along axis_az+90, up). Clamp to +-max_angle (status 'limited', else 'tracking'); sun_el <= 0 returns [0.0, 'night']. Round to 3 decimals.
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(sun_az, sun_el, axis_az, max_angle):
if sun_el <= 0:
return [0.0, 'night']
az = math.radians(sun_az)
el = math.radians(sun_el)
e = math.cos(el) * math.sin(az)
n = math.cos(el) * math.cos(az)
u = math.sin(el)
pa = math.radians(axis_az + 90)
perp = e * math.sin(pa) + n * math.cos(pa)
rot = math.degrees(math.atan2(perp, u))
status = 'tracking'
if rot > max_angle:
rot = math.copysign(max_angle, rot)
status = 'limited'
return [round(rot, 3), status]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['boundary: sun on the horizon', [90, 0, 180, 60], [0.0, 'night']],
['normal: solar noon on N-S axis', [180, 60, 180, 60], [0.0, 'tracking']],
['boundary: low morning sun hits east limit', [90, 10, 180, 60], [-60.0, 'limited']],
['regression: symmetric rotation limit', [30, 10, 180, 55], [-55.0, 'limited']],
['regression: symmetric rotation limit (partial repair)', [250, 10, 0, 45], [-45.0, 'limited']],
['control 1', [150, 10, 190, 45], [-45.0, 'limited']],
['control 2', [180, 89, 0, 55], [0.0, 'tracking']]],
[['boundary: sun on the horizon', [90, 0, 180, 60], [0.0, 'night']],
['normal: solar noon on N-S axis', [180, 60, 180, 60], [0.0, 'tracking']],
['boundary: low morning sun hits east limit', [90, 10, 180, 60], [-60.0, 'limited']],
['regression: symmetric rotation limit', [150, 10, 170, 60], [-60.0, 'limited']],
['regression: symmetric rotation limit (partial repair)', [100, 20, 170, 60], [-60.0, 'limited']],
['control 1', [60, 0, 20, 45], [0.0, 'night']],
['control 2', [340, 60, 170, 45], [5.725, 'tracking']]],
[['boundary: sun on the horizon', [90, 0, 180, 60], [0.0, 'night']],
['normal: solar noon on N-S axis', [180, 60, 180, 60], [0.0, 'tracking']],
['boundary: low morning sun hits east limit', [90, 10, 180, 60], [-60.0, 'limited']],
['regression: symmetric rotation limit', [100, 20, 180, 45], [-45.0, 'limited']],
['regression: symmetric rotation limit (partial repair)', [100, 45, 190, 45], [-45.0, 'limited']],
['control 1', [135, 0, 0, 60], [0.0, 'night']],
['control 2', [60, 20, 170, 55], [-55.0, 'limited']]],
[['boundary: sun on the horizon', [90, 0, 180, 60], [0.0, 'night']],
['normal: solar noon on N-S axis', [180, 60, 180, 60], [0.0, 'tracking']],
['boundary: low morning sun hits east limit', [90, 10, 180, 60], [-60.0, 'limited']],
['regression: symmetric rotation limit', [90, 3, 180, 60], [-60.0, 'limited']],
['regression: symmetric rotation limit (partial repair)', [100, 3, 190, 60], [-60.0, 'limited']],
['control 1', [90, 20, 0, 60], [60.0, 'limited']],
['control 2', [30, 3, 190, 45], [-45.0, 'limited']]],
[['boundary: sun on the horizon', [90, 0, 180, 60], [0.0, 'night']],
['normal: solar noon on N-S axis', [180, 60, 180, 60], [0.0, 'tracking']],
['boundary: low morning sun hits east limit', [90, 10, 180, 60], [-60.0, 'limited']],
['regression: symmetric rotation limit', [30, 20, 170, 45], [-45.0, 'limited']],
['regression: symmetric rotation limit (partial repair)', [340, 3, 20, 55], [-55.0, 'limited']],
['control 1', [100, 3, 190, 60], [-60.0, 'limited']],
['control 2', [90, 75, 170, 55], [-14.782, 'tracking']]]]
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: sun on the horizon | [0.0, 'night'] | [0.0, 'night'] | Passed |
| normal: solar noon on N-S axis | [0.0, 'tracking'] | [0.0, 'tracking'] | Passed |
| boundary: low morning sun hits east limit | [-80.0, 'tracking'] | [-60.0, 'limited'] | Failed |
| regression: symmetric rotation limit | [-70.575, 'tracking'] | [-55.0, 'limited'] | Failed |
| regression: symmetric rotation limit (partial repair) | [-79.372, 'tracking'] | [-45.0, 'limited'] | Failed |
| control 1 | [-74.66, 'tracking'] | [-45.0, 'limited'] | Failed |
| control 2 | [0.0, 'tracking'] | [0.0, 'tracking'] | Passed |
SHA-256 / a2522390c219d2a7bd69178cdd1111a70d2326f1bc5d709e1503eeb6b377bd37
2 / The unsuccessful fix
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(sun_az, sun_el, axis_az, max_angle):
if sun_el <= 0:
return [0.0, 'night']
az = math.radians(sun_az)
el = math.radians(sun_el)
e = math.cos(el) * math.sin(az)
n = math.cos(el) * math.cos(az)
u = math.sin(el)
pa = math.radians(axis_az + 90)
perp = e * math.sin(pa) + n * math.cos(pa)
rot = math.degrees(math.atan2(perp, u))
status = 'tracking'
if abs(rot) > max_angle:
rot = max_angle
status = 'limited'
return [round(rot, 3), status]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['boundary: sun on the horizon', [90, 0, 180, 60], [0.0, 'night']],
['normal: solar noon on N-S axis', [180, 60, 180, 60], [0.0, 'tracking']],
['boundary: low morning sun hits east limit', [90, 10, 180, 60], [-60.0, 'limited']],
['regression: symmetric rotation limit', [30, 10, 180, 55], [-55.0, 'limited']],
['regression: symmetric rotation limit (partial repair)', [250, 10, 0, 45], [-45.0, 'limited']],
['control 1', [150, 10, 190, 45], [-45.0, 'limited']],
['control 2', [180, 89, 0, 55], [0.0, 'tracking']]],
[['boundary: sun on the horizon', [90, 0, 180, 60], [0.0, 'night']],
['normal: solar noon on N-S axis', [180, 60, 180, 60], [0.0, 'tracking']],
['boundary: low morning sun hits east limit', [90, 10, 180, 60], [-60.0, 'limited']],
['regression: symmetric rotation limit', [150, 10, 170, 60], [-60.0, 'limited']],
['regression: symmetric rotation limit (partial repair)', [100, 20, 170, 60], [-60.0, 'limited']],
['control 1', [60, 0, 20, 45], [0.0, 'night']],
['control 2', [340, 60, 170, 45], [5.725, 'tracking']]],
[['boundary: sun on the horizon', [90, 0, 180, 60], [0.0, 'night']],
['normal: solar noon on N-S axis', [180, 60, 180, 60], [0.0, 'tracking']],
['boundary: low morning sun hits east limit', [90, 10, 180, 60], [-60.0, 'limited']],
['regression: symmetric rotation limit', [100, 20, 180, 45], [-45.0, 'limited']],
['regression: symmetric rotation limit (partial repair)', [100, 45, 190, 45], [-45.0, 'limited']],
['control 1', [135, 0, 0, 60], [0.0, 'night']],
['control 2', [60, 20, 170, 55], [-55.0, 'limited']]],
[['boundary: sun on the horizon', [90, 0, 180, 60], [0.0, 'night']],
['normal: solar noon on N-S axis', [180, 60, 180, 60], [0.0, 'tracking']],
['boundary: low morning sun hits east limit', [90, 10, 180, 60], [-60.0, 'limited']],
['regression: symmetric rotation limit', [90, 3, 180, 60], [-60.0, 'limited']],
['regression: symmetric rotation limit (partial repair)', [100, 3, 190, 60], [-60.0, 'limited']],
['control 1', [90, 20, 0, 60], [60.0, 'limited']],
['control 2', [30, 3, 190, 45], [-45.0, 'limited']]],
[['boundary: sun on the horizon', [90, 0, 180, 60], [0.0, 'night']],
['normal: solar noon on N-S axis', [180, 60, 180, 60], [0.0, 'tracking']],
['boundary: low morning sun hits east limit', [90, 10, 180, 60], [-60.0, 'limited']],
['regression: symmetric rotation limit', [30, 20, 170, 45], [-45.0, 'limited']],
['regression: symmetric rotation limit (partial repair)', [340, 3, 20, 55], [-55.0, 'limited']],
['control 1', [100, 3, 190, 60], [-60.0, 'limited']],
['control 2', [90, 75, 170, 55], [-14.782, 'tracking']]]]
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: sun on the horizon | [0.0, 'night'] | [0.0, 'night'] | Passed |
| normal: solar noon on N-S axis | [0.0, 'tracking'] | [0.0, 'tracking'] | Passed |
| boundary: low morning sun hits east limit | [60, 'limited'] | [-60.0, 'limited'] | Failed |
| regression: symmetric rotation limit | [55, 'limited'] | [-55.0, 'limited'] | Failed |
| regression: symmetric rotation limit (partial repair) | [45, 'limited'] | [-45.0, 'limited'] | Failed |
| control 1 | [45, 'limited'] | [-45.0, 'limited'] | Failed |
| control 2 | [0.0, 'tracking'] | [0.0, 'tracking'] | Passed |
SHA-256 / b76fc8c4298c4e2e6419f73745754336eb6ede858c677c4b86d3e73b5a0c2a6c
3 / The verified repair
Exit 0"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(sun_az, sun_el, axis_az, max_angle):
if sun_el <= 0:
return [0.0, 'night']
az = math.radians(sun_az)
el = math.radians(sun_el)
e = math.cos(el) * math.sin(az)
n = math.cos(el) * math.cos(az)
u = math.sin(el)
pa = math.radians(axis_az + 90)
perp = e * math.sin(pa) + n * math.cos(pa)
rot = math.degrees(math.atan2(perp, u))
status = 'tracking'
if abs(rot) > max_angle:
rot = math.copysign(max_angle, rot)
status = 'limited'
return [round(rot, 3), status]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['boundary: sun on the horizon', [90, 0, 180, 60], [0.0, 'night']],
['normal: solar noon on N-S axis', [180, 60, 180, 60], [0.0, 'tracking']],
['boundary: low morning sun hits east limit', [90, 10, 180, 60], [-60.0, 'limited']],
['regression: symmetric rotation limit', [30, 10, 180, 55], [-55.0, 'limited']],
['regression: symmetric rotation limit (partial repair)', [250, 10, 0, 45], [-45.0, 'limited']],
['control 1', [150, 10, 190, 45], [-45.0, 'limited']],
['control 2', [180, 89, 0, 55], [0.0, 'tracking']]],
[['boundary: sun on the horizon', [90, 0, 180, 60], [0.0, 'night']],
['normal: solar noon on N-S axis', [180, 60, 180, 60], [0.0, 'tracking']],
['boundary: low morning sun hits east limit', [90, 10, 180, 60], [-60.0, 'limited']],
['regression: symmetric rotation limit', [150, 10, 170, 60], [-60.0, 'limited']],
['regression: symmetric rotation limit (partial repair)', [100, 20, 170, 60], [-60.0, 'limited']],
['control 1', [60, 0, 20, 45], [0.0, 'night']],
['control 2', [340, 60, 170, 45], [5.725, 'tracking']]],
[['boundary: sun on the horizon', [90, 0, 180, 60], [0.0, 'night']],
['normal: solar noon on N-S axis', [180, 60, 180, 60], [0.0, 'tracking']],
['boundary: low morning sun hits east limit', [90, 10, 180, 60], [-60.0, 'limited']],
['regression: symmetric rotation limit', [100, 20, 180, 45], [-45.0, 'limited']],
['regression: symmetric rotation limit (partial repair)', [100, 45, 190, 45], [-45.0, 'limited']],
['control 1', [135, 0, 0, 60], [0.0, 'night']],
['control 2', [60, 20, 170, 55], [-55.0, 'limited']]],
[['boundary: sun on the horizon', [90, 0, 180, 60], [0.0, 'night']],
['normal: solar noon on N-S axis', [180, 60, 180, 60], [0.0, 'tracking']],
['boundary: low morning sun hits east limit', [90, 10, 180, 60], [-60.0, 'limited']],
['regression: symmetric rotation limit', [90, 3, 180, 60], [-60.0, 'limited']],
['regression: symmetric rotation limit (partial repair)', [100, 3, 190, 60], [-60.0, 'limited']],
['control 1', [90, 20, 0, 60], [60.0, 'limited']],
['control 2', [30, 3, 190, 45], [-45.0, 'limited']]],
[['boundary: sun on the horizon', [90, 0, 180, 60], [0.0, 'night']],
['normal: solar noon on N-S axis', [180, 60, 180, 60], [0.0, 'tracking']],
['boundary: low morning sun hits east limit', [90, 10, 180, 60], [-60.0, 'limited']],
['regression: symmetric rotation limit', [30, 20, 170, 45], [-45.0, 'limited']],
['regression: symmetric rotation limit (partial repair)', [340, 3, 20, 55], [-55.0, 'limited']],
['control 1', [100, 3, 190, 60], [-60.0, 'limited']],
['control 2', [90, 75, 170, 55], [-14.782, 'tracking']]]]
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: sun on the horizon | [0.0, 'night'] | [0.0, 'night'] | Passed |
| normal: solar noon on N-S axis | [0.0, 'tracking'] | [0.0, 'tracking'] | Passed |
| boundary: low morning sun hits east limit | [-60.0, 'limited'] | [-60.0, 'limited'] | Passed |
| regression: symmetric rotation limit | [-55.0, 'limited'] | [-55.0, 'limited'] | Passed |
| regression: symmetric rotation limit (partial repair) | [-45.0, 'limited'] | [-45.0, 'limited'] | Passed |
| control 1 | [-45.0, 'limited'] | [-45.0, 'limited'] | Passed |
| control 2 | [0.0, 'tracking'] | [0.0, 'tracking'] | Passed |
SHA-256 / 977ce1d56fc5bdde46e5aa01173ed4b39dbabb161101e0f9384aff6d776dfd3d
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:54.391464+00:00.
Case digest / 0e59509505bf98d4056abdc85ba147d98a45aa84b8741b31b89365dbf1b51d06