FA-93416 / Solar tracker geometry / Open access
Fixed panel angle of incidence: horizon night test · case 01
A sun exactly on the horizon produces a front-side incidence for tilted panels.
ROOT CAUSE
Elevation zero is not treated as night.
VERIFIED REPAIR
Return night for sun_el <= 0.
Unsuccessful approach: Special-casing only flat panels leaves tilted panels lit by a horizon sun.
Case contract
Panel tilted by tilt degrees from horizontal, facing pan_az (clockwise from north). zenith = 90 - sun_el. cos(aoi) = cos(zen)cos(tilt) + sin(zen)sin(tilt)cos(sun_az - pan_az), clamped to [-1, 1]. Return [aoi degrees rounded 3, max(cos aoi, 0) rounded 4, 'front' or 'back'], or [None, 0.0, 'night'] 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(tilt, pan_az, sun_az, sun_el):
if sun_el < 0:
return [None, 0.0, 'night']
zen = math.radians(90 - sun_el)
t = math.radians(tilt)
c = math.cos(zen) * math.cos(t) + math.sin(zen) * math.sin(t) * math.cos(math.radians(sun_az - pan_az))
c = max(-1.0, min(1.0, c))
aoi = math.degrees(math.acos(c))
side = 'front' if c >= 0 else 'back'
return [round(aoi, 3), round(max(c, 0.0), 4), side]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['boundary: sun on horizon', [30, 180, 180, 0], [None, 0.0, 'night']],
['normal: sun normal to panel', [30, 180, 180, 60], [0.0, 1.0, 'front']],
['boundary: sun behind vertical panel', [90, 180, 0, 20], [160.0, 0.0, 'back']],
['regression: horizon night test', [45, 180, 135, 0], [None, 0.0, 'night']],
['regression: horizon night test (partial repair)', [30, 225, 135, 0], [None, 0.0, 'night']],
['control 1', [0, 180, 225, -5], [None, 0.0, 'night']],
['control 2', [0, 135, 150, 45], [45.0, 0.7071, 'front']]],
[['boundary: sun on horizon', [30, 180, 180, 0], [None, 0.0, 'night']],
['normal: sun normal to panel', [30, 180, 180, 60], [0.0, 1.0, 'front']],
['boundary: sun behind vertical panel', [90, 180, 0, 20], [160.0, 0.0, 'back']],
['regression: horizon night test', [25, 270, 315, 0], [None, 0.0, 'night']],
['regression: horizon night test (partial repair)', [10, 180, 30, 0], [None, 0.0, 'night']],
['control 1', [25, 180, 60, 10], [92.907, 0.0, 'back']],
['control 2', [60, 225, 270, 20], [41.716, 0.7465, 'front']]],
[['boundary: sun on horizon', [30, 180, 180, 0], [None, 0.0, 'night']],
['normal: sun normal to panel', [30, 180, 180, 60], [0.0, 1.0, 'front']],
['boundary: sun behind vertical panel', [90, 180, 0, 20], [160.0, 0.0, 'back']],
['regression: horizon night test', [45, 135, 250, 0], [None, 0.0, 'night']],
['regression: horizon night test (partial repair)', [60, 90, 150, 0], [None, 0.0, 'night']],
['control 1', [60, 90, 135, 30], [38.709, 0.7803, 'front']],
['control 2', [45, 270, 60, 89], [45.868, 0.6963, 'front']]],
[['boundary: sun on horizon', [30, 180, 180, 0], [None, 0.0, 'night']],
['normal: sun normal to panel', [30, 180, 180, 60], [0.0, 1.0, 'front']],
['boundary: sun behind vertical panel', [90, 180, 0, 20], [160.0, 0.0, 'back']],
['regression: horizon night test', [10, 90, 225, 0], [None, 0.0, 'night']],
['regression: horizon night test (partial repair)', [10, 270, 315, 0], [None, 0.0, 'night']],
['control 1', [45, 180, 315, 0], [None, 0.0, 'night']],
['control 2', [30, 90, 225, 45], [68.754, 0.3624, 'front']]],
[['boundary: sun on horizon', [30, 180, 180, 0], [None, 0.0, 'night']],
['normal: sun normal to panel', [30, 180, 180, 60], [0.0, 1.0, 'front']],
['boundary: sun behind vertical panel', [90, 180, 0, 20], [160.0, 0.0, 'back']],
['regression: horizon night test', [20, 270, 100, 0], [None, 0.0, 'night']],
['regression: horizon night test (partial repair)', [20, 90, 150, 0], [None, 0.0, 'night']],
['control 1', [45, 180, 0, 75], [60.0, 0.5, 'front']],
['control 2', [45, 225, 270, -5], [None, 0.0, 'night']]]]
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 horizon | [60.0, 0.5, 'front'] | [None, 0.0, 'night'] | Failed |
| normal: sun normal to panel | [0.0, 1.0, 'front'] | [0.0, 1.0, 'front'] | Passed |
| boundary: sun behind vertical panel | [160.0, 0.0, 'back'] | [160.0, 0.0, 'back'] | Passed |
| regression: horizon night test | [60.0, 0.5, 'front'] | [None, 0.0, 'night'] | Failed |
| regression: horizon night test (partial repair) | [90.0, 0.0, 'front'] | [None, 0.0, 'night'] | Failed |
| control 1 | [None, 0.0, 'night'] | [None, 0.0, 'night'] | Passed |
| control 2 | [45.0, 0.7071, 'front'] | [45.0, 0.7071, 'front'] | Passed |
SHA-256 / 5834ca68bb60af968ee9fa6eacec6534f2c54fb817eb666ef6b546f1bf94b0d1
2 / The unsuccessful fix
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(tilt, pan_az, sun_az, sun_el):
if sun_el < 0 or sun_el == 0 and tilt == 0:
return [None, 0.0, 'night']
zen = math.radians(90 - sun_el)
t = math.radians(tilt)
c = math.cos(zen) * math.cos(t) + math.sin(zen) * math.sin(t) * math.cos(math.radians(sun_az - pan_az))
c = max(-1.0, min(1.0, c))
aoi = math.degrees(math.acos(c))
side = 'front' if c >= 0 else 'back'
return [round(aoi, 3), round(max(c, 0.0), 4), side]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['boundary: sun on horizon', [30, 180, 180, 0], [None, 0.0, 'night']],
['normal: sun normal to panel', [30, 180, 180, 60], [0.0, 1.0, 'front']],
['boundary: sun behind vertical panel', [90, 180, 0, 20], [160.0, 0.0, 'back']],
['regression: horizon night test', [45, 180, 135, 0], [None, 0.0, 'night']],
['regression: horizon night test (partial repair)', [30, 225, 135, 0], [None, 0.0, 'night']],
['control 1', [0, 180, 225, -5], [None, 0.0, 'night']],
['control 2', [0, 135, 150, 45], [45.0, 0.7071, 'front']]],
[['boundary: sun on horizon', [30, 180, 180, 0], [None, 0.0, 'night']],
['normal: sun normal to panel', [30, 180, 180, 60], [0.0, 1.0, 'front']],
['boundary: sun behind vertical panel', [90, 180, 0, 20], [160.0, 0.0, 'back']],
['regression: horizon night test', [25, 270, 315, 0], [None, 0.0, 'night']],
['regression: horizon night test (partial repair)', [10, 180, 30, 0], [None, 0.0, 'night']],
['control 1', [25, 180, 60, 10], [92.907, 0.0, 'back']],
['control 2', [60, 225, 270, 20], [41.716, 0.7465, 'front']]],
[['boundary: sun on horizon', [30, 180, 180, 0], [None, 0.0, 'night']],
['normal: sun normal to panel', [30, 180, 180, 60], [0.0, 1.0, 'front']],
['boundary: sun behind vertical panel', [90, 180, 0, 20], [160.0, 0.0, 'back']],
['regression: horizon night test', [45, 135, 250, 0], [None, 0.0, 'night']],
['regression: horizon night test (partial repair)', [60, 90, 150, 0], [None, 0.0, 'night']],
['control 1', [60, 90, 135, 30], [38.709, 0.7803, 'front']],
['control 2', [45, 270, 60, 89], [45.868, 0.6963, 'front']]],
[['boundary: sun on horizon', [30, 180, 180, 0], [None, 0.0, 'night']],
['normal: sun normal to panel', [30, 180, 180, 60], [0.0, 1.0, 'front']],
['boundary: sun behind vertical panel', [90, 180, 0, 20], [160.0, 0.0, 'back']],
['regression: horizon night test', [10, 90, 225, 0], [None, 0.0, 'night']],
['regression: horizon night test (partial repair)', [10, 270, 315, 0], [None, 0.0, 'night']],
['control 1', [45, 180, 315, 0], [None, 0.0, 'night']],
['control 2', [30, 90, 225, 45], [68.754, 0.3624, 'front']]],
[['boundary: sun on horizon', [30, 180, 180, 0], [None, 0.0, 'night']],
['normal: sun normal to panel', [30, 180, 180, 60], [0.0, 1.0, 'front']],
['boundary: sun behind vertical panel', [90, 180, 0, 20], [160.0, 0.0, 'back']],
['regression: horizon night test', [20, 270, 100, 0], [None, 0.0, 'night']],
['regression: horizon night test (partial repair)', [20, 90, 150, 0], [None, 0.0, 'night']],
['control 1', [45, 180, 0, 75], [60.0, 0.5, 'front']],
['control 2', [45, 225, 270, -5], [None, 0.0, 'night']]]]
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 horizon | [60.0, 0.5, 'front'] | [None, 0.0, 'night'] | Failed |
| normal: sun normal to panel | [0.0, 1.0, 'front'] | [0.0, 1.0, 'front'] | Passed |
| boundary: sun behind vertical panel | [160.0, 0.0, 'back'] | [160.0, 0.0, 'back'] | Passed |
| regression: horizon night test | [60.0, 0.5, 'front'] | [None, 0.0, 'night'] | Failed |
| regression: horizon night test (partial repair) | [90.0, 0.0, 'front'] | [None, 0.0, 'night'] | Failed |
| control 1 | [None, 0.0, 'night'] | [None, 0.0, 'night'] | Passed |
| control 2 | [45.0, 0.7071, 'front'] | [45.0, 0.7071, 'front'] | Passed |
SHA-256 / b569c94458e7fea4d2c491944d6086ed71b91f8c36dcba325a3cd8fd77fe1eb6
3 / The verified repair
Exit 0"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(tilt, pan_az, sun_az, sun_el):
if sun_el <= 0:
return [None, 0.0, 'night']
zen = math.radians(90 - sun_el)
t = math.radians(tilt)
c = math.cos(zen) * math.cos(t) + math.sin(zen) * math.sin(t) * math.cos(math.radians(sun_az - pan_az))
c = max(-1.0, min(1.0, c))
aoi = math.degrees(math.acos(c))
side = 'front' if c >= 0 else 'back'
return [round(aoi, 3), round(max(c, 0.0), 4), side]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['boundary: sun on horizon', [30, 180, 180, 0], [None, 0.0, 'night']],
['normal: sun normal to panel', [30, 180, 180, 60], [0.0, 1.0, 'front']],
['boundary: sun behind vertical panel', [90, 180, 0, 20], [160.0, 0.0, 'back']],
['regression: horizon night test', [45, 180, 135, 0], [None, 0.0, 'night']],
['regression: horizon night test (partial repair)', [30, 225, 135, 0], [None, 0.0, 'night']],
['control 1', [0, 180, 225, -5], [None, 0.0, 'night']],
['control 2', [0, 135, 150, 45], [45.0, 0.7071, 'front']]],
[['boundary: sun on horizon', [30, 180, 180, 0], [None, 0.0, 'night']],
['normal: sun normal to panel', [30, 180, 180, 60], [0.0, 1.0, 'front']],
['boundary: sun behind vertical panel', [90, 180, 0, 20], [160.0, 0.0, 'back']],
['regression: horizon night test', [25, 270, 315, 0], [None, 0.0, 'night']],
['regression: horizon night test (partial repair)', [10, 180, 30, 0], [None, 0.0, 'night']],
['control 1', [25, 180, 60, 10], [92.907, 0.0, 'back']],
['control 2', [60, 225, 270, 20], [41.716, 0.7465, 'front']]],
[['boundary: sun on horizon', [30, 180, 180, 0], [None, 0.0, 'night']],
['normal: sun normal to panel', [30, 180, 180, 60], [0.0, 1.0, 'front']],
['boundary: sun behind vertical panel', [90, 180, 0, 20], [160.0, 0.0, 'back']],
['regression: horizon night test', [45, 135, 250, 0], [None, 0.0, 'night']],
['regression: horizon night test (partial repair)', [60, 90, 150, 0], [None, 0.0, 'night']],
['control 1', [60, 90, 135, 30], [38.709, 0.7803, 'front']],
['control 2', [45, 270, 60, 89], [45.868, 0.6963, 'front']]],
[['boundary: sun on horizon', [30, 180, 180, 0], [None, 0.0, 'night']],
['normal: sun normal to panel', [30, 180, 180, 60], [0.0, 1.0, 'front']],
['boundary: sun behind vertical panel', [90, 180, 0, 20], [160.0, 0.0, 'back']],
['regression: horizon night test', [10, 90, 225, 0], [None, 0.0, 'night']],
['regression: horizon night test (partial repair)', [10, 270, 315, 0], [None, 0.0, 'night']],
['control 1', [45, 180, 315, 0], [None, 0.0, 'night']],
['control 2', [30, 90, 225, 45], [68.754, 0.3624, 'front']]],
[['boundary: sun on horizon', [30, 180, 180, 0], [None, 0.0, 'night']],
['normal: sun normal to panel', [30, 180, 180, 60], [0.0, 1.0, 'front']],
['boundary: sun behind vertical panel', [90, 180, 0, 20], [160.0, 0.0, 'back']],
['regression: horizon night test', [20, 270, 100, 0], [None, 0.0, 'night']],
['regression: horizon night test (partial repair)', [20, 90, 150, 0], [None, 0.0, 'night']],
['control 1', [45, 180, 0, 75], [60.0, 0.5, 'front']],
['control 2', [45, 225, 270, -5], [None, 0.0, 'night']]]]
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 horizon | [None, 0.0, 'night'] | [None, 0.0, 'night'] | Passed |
| normal: sun normal to panel | [0.0, 1.0, 'front'] | [0.0, 1.0, 'front'] | Passed |
| boundary: sun behind vertical panel | [160.0, 0.0, 'back'] | [160.0, 0.0, 'back'] | Passed |
| regression: horizon night test | [None, 0.0, 'night'] | [None, 0.0, 'night'] | Passed |
| regression: horizon night test (partial repair) | [None, 0.0, 'night'] | [None, 0.0, 'night'] | Passed |
| control 1 | [None, 0.0, 'night'] | [None, 0.0, 'night'] | Passed |
| control 2 | [45.0, 0.7071, 'front'] | [45.0, 0.7071, 'front'] | Passed |
SHA-256 / 87a0466d35484ada9d792cca028381d331959a80b87def22523b93cc231f31fc
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.904884+00:00.
Case digest / 70bd902311d86828f758321fb2bdefb06b80d49aa8532f08b039d011cc382400