FA-93706 / Solar tracker geometry / Open access
Tilted single-axis tracker frame: incidence magnitude · case 01
Incidence angles come out negative when the sun is toward the low end of the axis.
ROOT CAUSE
The along-axis component is used without its absolute value.
VERIFIED REPAIR
Incidence = asin(|s.A|).
Unsuccessful approach: Squaring the component changes its magnitude.
Case contract
The axis points toward axis_az rising by axis_tilt: A = (sin a cos b, cos a cos b, sin b). The zero-rotation panel normal is N0 = (-sin a sin b, -cos a sin b, cos b) and positive rotation turns toward R = (cos a, -sin a, 0). With unit sun vector s, rotation = atan2(s.R, s.N0) and the incidence at ideal tracking is asin(|s.A|). Return [rotation, incidence] rounded to 3, or None when sun_el <= 0.
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, axis_tilt):
if sun_el <= 0:
return None
az = math.radians(sun_az)
el = math.radians(sun_el)
a = math.radians(axis_az)
b = math.radians(axis_tilt)
s = [math.cos(el) * math.sin(az), math.cos(el) * math.cos(az), math.sin(el)]
A = [math.sin(a) * math.cos(b), math.cos(a) * math.cos(b), math.sin(b)]
N0 = [-math.sin(a) * math.sin(b), -math.cos(a) * math.sin(b), math.cos(b)]
R = [math.cos(a), -math.sin(a), 0.0]
dot = lambda u, v: sum(p * q for p, q in zip(u, v))
theta = math.degrees(math.atan2(dot(s, R), dot(s, N0)))
aoi = math.degrees(math.asin(min(1.0, dot(s, A))))
return [round(theta, 3), round(aoi, 3)]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['normal: horizontal axis at noon', [180, 60, 180, 0], [0.0, 30.0]],
['normal: tilted axis morning', [100, 20, 180, 10], [-71.564, 12.714]],
['boundary: night', [270, 0, 180, 10], None],
['regression: incidence magnitude', [180, 20, 0, 5], [0.0, 65.0]],
['regression: incidence magnitude (partial repair)', [0, 89, 180, 5], [-0.0, 4.0]],
['control 1', [300, 0, 180, 0], None], ['control 2', [225, 3, 180, 20], [105.236, 42.957]]],
[['normal: horizontal axis at noon', [180, 60, 180, 0], [0.0, 30.0]],
['normal: tilted axis morning', [100, 20, 180, 10], [-71.564, 12.714]],
['boundary: night', [270, 0, 180, 10], None],
['regression: incidence magnitude', [315, 10, 180, 0], [75.998, 44.136]],
['regression: incidence magnitude (partial repair)', [225, 89, 180, 5], [0.711, 5.707]],
['control 1', [45, 60, 170, 10], [-24.406, 7.588]],
['control 2', [135, 75, 170, 20], [-10.079, 31.978]]],
[['normal: horizontal axis at noon', [180, 60, 180, 0], [0.0, 30.0]],
['normal: tilted axis morning', [100, 20, 180, 10], [-71.564, 12.714]],
['boundary: night', [270, 0, 180, 10], None],
['regression: incidence magnitude', [315, 10, 170, 10], [61.157, 49.845]],
['regression: incidence magnitude (partial repair)', [225, 30, 180, 10], [57.771, 43.622]],
['control 1', [340, 45, 170, 20], [7.747, 24.363]],
['control 2', [340, 10, 180, 20], [35.076, 54.117]]],
[['normal: horizontal axis at noon', [180, 60, 180, 0], [0.0, 30.0]],
['normal: tilted axis morning', [100, 20, 180, 10], [-71.564, 12.714]],
['boundary: night', [270, 0, 180, 10], None],
['regression: incidence magnitude', [100, 30, 0, 5], [59.062, 6.098]],
['regression: incidence magnitude (partial repair)', [150, 60, 170, 0], [-11.17, 28.024]],
['control 1', [340, 20, 180, 0], [43.219, 62.009]],
['control 2', [30, 3, 0, 20], [116.285, 56.159]]],
[['normal: horizontal axis at noon', [180, 60, 180, 0], [0.0, 30.0]],
['normal: tilted axis morning', [100, 20, 180, 10], [-71.564, 12.714]],
['boundary: night', [270, 0, 180, 10], None],
['regression: incidence magnitude', [225, 10, 0, 10], [-67.255, 40.968]],
['regression: incidence magnitude (partial repair)', [340, 30, 0, 0], [-30.642, 54.469]],
['control 1', [100, 30, 0, 5], [59.062, 6.098]],
['control 2', [135, 75, 180, 10], [-11.257, 20.363]]]]
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 |
|---|---|---|---|
| normal: horizontal axis at noon | [0.0, 30.0] | [0.0, 30.0] | Passed |
| normal: tilted axis morning | [-71.564, 12.714] | [-71.564, 12.714] | Passed |
| boundary: night | None | None | Passed |
| regression: incidence magnitude | [0.0, -65.0] | [0.0, 65.0] | Failed |
| regression: incidence magnitude (partial repair) | [-0.0, 4.0] | [-0.0, 4.0] | Passed |
| control 1 | None | None | Passed |
| control 2 | [105.236, 42.957] | [105.236, 42.957] | Passed |
SHA-256 / 968e3ec35dc5f9750dc64ac049ce46b45717f83dcc156301bea15ce4d8db7932
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, axis_tilt):
if sun_el <= 0:
return None
az = math.radians(sun_az)
el = math.radians(sun_el)
a = math.radians(axis_az)
b = math.radians(axis_tilt)
s = [math.cos(el) * math.sin(az), math.cos(el) * math.cos(az), math.sin(el)]
A = [math.sin(a) * math.cos(b), math.cos(a) * math.cos(b), math.sin(b)]
N0 = [-math.sin(a) * math.sin(b), -math.cos(a) * math.sin(b), math.cos(b)]
R = [math.cos(a), -math.sin(a), 0.0]
dot = lambda u, v: sum(p * q for p, q in zip(u, v))
theta = math.degrees(math.atan2(dot(s, R), dot(s, N0)))
aoi = math.degrees(math.asin(min(1.0, dot(s, A) ** 2)))
return [round(theta, 3), round(aoi, 3)]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['normal: horizontal axis at noon', [180, 60, 180, 0], [0.0, 30.0]],
['normal: tilted axis morning', [100, 20, 180, 10], [-71.564, 12.714]],
['boundary: night', [270, 0, 180, 10], None],
['regression: incidence magnitude', [180, 20, 0, 5], [0.0, 65.0]],
['regression: incidence magnitude (partial repair)', [0, 89, 180, 5], [-0.0, 4.0]],
['control 1', [300, 0, 180, 0], None], ['control 2', [225, 3, 180, 20], [105.236, 42.957]]],
[['normal: horizontal axis at noon', [180, 60, 180, 0], [0.0, 30.0]],
['normal: tilted axis morning', [100, 20, 180, 10], [-71.564, 12.714]],
['boundary: night', [270, 0, 180, 10], None],
['regression: incidence magnitude', [315, 10, 180, 0], [75.998, 44.136]],
['regression: incidence magnitude (partial repair)', [225, 89, 180, 5], [0.711, 5.707]],
['control 1', [45, 60, 170, 10], [-24.406, 7.588]],
['control 2', [135, 75, 170, 20], [-10.079, 31.978]]],
[['normal: horizontal axis at noon', [180, 60, 180, 0], [0.0, 30.0]],
['normal: tilted axis morning', [100, 20, 180, 10], [-71.564, 12.714]],
['boundary: night', [270, 0, 180, 10], None],
['regression: incidence magnitude', [315, 10, 170, 10], [61.157, 49.845]],
['regression: incidence magnitude (partial repair)', [225, 30, 180, 10], [57.771, 43.622]],
['control 1', [340, 45, 170, 20], [7.747, 24.363]],
['control 2', [340, 10, 180, 20], [35.076, 54.117]]],
[['normal: horizontal axis at noon', [180, 60, 180, 0], [0.0, 30.0]],
['normal: tilted axis morning', [100, 20, 180, 10], [-71.564, 12.714]],
['boundary: night', [270, 0, 180, 10], None],
['regression: incidence magnitude', [100, 30, 0, 5], [59.062, 6.098]],
['regression: incidence magnitude (partial repair)', [150, 60, 170, 0], [-11.17, 28.024]],
['control 1', [340, 20, 180, 0], [43.219, 62.009]],
['control 2', [30, 3, 0, 20], [116.285, 56.159]]],
[['normal: horizontal axis at noon', [180, 60, 180, 0], [0.0, 30.0]],
['normal: tilted axis morning', [100, 20, 180, 10], [-71.564, 12.714]],
['boundary: night', [270, 0, 180, 10], None],
['regression: incidence magnitude', [225, 10, 0, 10], [-67.255, 40.968]],
['regression: incidence magnitude (partial repair)', [340, 30, 0, 0], [-30.642, 54.469]],
['control 1', [100, 30, 0, 5], [59.062, 6.098]],
['control 2', [135, 75, 180, 10], [-11.257, 20.363]]]]
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 |
|---|---|---|---|
| normal: horizontal axis at noon | [0.0, 14.478] | [0.0, 30.0] | Failed |
| normal: tilted axis morning | [-71.564, 2.776] | [-71.564, 12.714] | Failed |
| boundary: night | None | None | Passed |
| regression: incidence magnitude | [0.0, 55.225] | [0.0, 65.0] | Failed |
| regression: incidence magnitude (partial repair) | [-0.0, 0.279] | [-0.0, 4.0] | Failed |
| control 1 | None | None | Passed |
| control 2 | [105.236, 27.67] | [105.236, 42.957] | Failed |
SHA-256 / 25e487d88475605f76b2699c11abac0b105deb620c345e1022937e3c1f944265
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, axis_tilt):
if sun_el <= 0:
return None
az = math.radians(sun_az)
el = math.radians(sun_el)
a = math.radians(axis_az)
b = math.radians(axis_tilt)
s = [math.cos(el) * math.sin(az), math.cos(el) * math.cos(az), math.sin(el)]
A = [math.sin(a) * math.cos(b), math.cos(a) * math.cos(b), math.sin(b)]
N0 = [-math.sin(a) * math.sin(b), -math.cos(a) * math.sin(b), math.cos(b)]
R = [math.cos(a), -math.sin(a), 0.0]
dot = lambda u, v: sum(p * q for p, q in zip(u, v))
theta = math.degrees(math.atan2(dot(s, R), dot(s, N0)))
aoi = math.degrees(math.asin(min(1.0, abs(dot(s, A)))))
return [round(theta, 3), round(aoi, 3)]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['normal: horizontal axis at noon', [180, 60, 180, 0], [0.0, 30.0]],
['normal: tilted axis morning', [100, 20, 180, 10], [-71.564, 12.714]],
['boundary: night', [270, 0, 180, 10], None],
['regression: incidence magnitude', [180, 20, 0, 5], [0.0, 65.0]],
['regression: incidence magnitude (partial repair)', [0, 89, 180, 5], [-0.0, 4.0]],
['control 1', [300, 0, 180, 0], None], ['control 2', [225, 3, 180, 20], [105.236, 42.957]]],
[['normal: horizontal axis at noon', [180, 60, 180, 0], [0.0, 30.0]],
['normal: tilted axis morning', [100, 20, 180, 10], [-71.564, 12.714]],
['boundary: night', [270, 0, 180, 10], None],
['regression: incidence magnitude', [315, 10, 180, 0], [75.998, 44.136]],
['regression: incidence magnitude (partial repair)', [225, 89, 180, 5], [0.711, 5.707]],
['control 1', [45, 60, 170, 10], [-24.406, 7.588]],
['control 2', [135, 75, 170, 20], [-10.079, 31.978]]],
[['normal: horizontal axis at noon', [180, 60, 180, 0], [0.0, 30.0]],
['normal: tilted axis morning', [100, 20, 180, 10], [-71.564, 12.714]],
['boundary: night', [270, 0, 180, 10], None],
['regression: incidence magnitude', [315, 10, 170, 10], [61.157, 49.845]],
['regression: incidence magnitude (partial repair)', [225, 30, 180, 10], [57.771, 43.622]],
['control 1', [340, 45, 170, 20], [7.747, 24.363]],
['control 2', [340, 10, 180, 20], [35.076, 54.117]]],
[['normal: horizontal axis at noon', [180, 60, 180, 0], [0.0, 30.0]],
['normal: tilted axis morning', [100, 20, 180, 10], [-71.564, 12.714]],
['boundary: night', [270, 0, 180, 10], None],
['regression: incidence magnitude', [100, 30, 0, 5], [59.062, 6.098]],
['regression: incidence magnitude (partial repair)', [150, 60, 170, 0], [-11.17, 28.024]],
['control 1', [340, 20, 180, 0], [43.219, 62.009]],
['control 2', [30, 3, 0, 20], [116.285, 56.159]]],
[['normal: horizontal axis at noon', [180, 60, 180, 0], [0.0, 30.0]],
['normal: tilted axis morning', [100, 20, 180, 10], [-71.564, 12.714]],
['boundary: night', [270, 0, 180, 10], None],
['regression: incidence magnitude', [225, 10, 0, 10], [-67.255, 40.968]],
['regression: incidence magnitude (partial repair)', [340, 30, 0, 0], [-30.642, 54.469]],
['control 1', [100, 30, 0, 5], [59.062, 6.098]],
['control 2', [135, 75, 180, 10], [-11.257, 20.363]]]]
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 |
|---|---|---|---|
| normal: horizontal axis at noon | [0.0, 30.0] | [0.0, 30.0] | Passed |
| normal: tilted axis morning | [-71.564, 12.714] | [-71.564, 12.714] | Passed |
| boundary: night | None | None | Passed |
| regression: incidence magnitude | [0.0, 65.0] | [0.0, 65.0] | Passed |
| regression: incidence magnitude (partial repair) | [-0.0, 4.0] | [-0.0, 4.0] | Passed |
| control 1 | None | None | Passed |
| control 2 | [105.236, 42.957] | [105.236, 42.957] | Passed |
SHA-256 / cd64ba8086a8f07a4c420b37f6ccaa11f22b777aaaf2585aa7bfb569d35a3d6b
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:57.544779+00:00.
Case digest / ab428cdb718001f533a50bd085dffd14950878fcc0dfefeab146e86e52e82060