broken_source:
"""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(c, 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: back-side projection', [30, 135, 0, 10], [101.408, 0.0, 'back']],
  ['regression: back-side projection (partial repair)', [90, 180, 30, 20], [144.469, 0.0, 'back']],
  ['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: back-side projection', [90, 180, 30, 75], [102.953, 0.0, 'back']],
  ['regression: back-side projection (partial repair)', [25, 180, 60, 10], [92.907, 0.0, 'back']],
  ['control 1', [60, 225, 270, 20], [41.716, 0.7465, 'front']],
  ['control 2', [20, 90, 0, 89], [20.024, 0.9395, '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: back-side projection', [90, 90, 200, 30], [107.229, 0.0, 'back']],
  ['regression: back-side projection (partial repair)', [20, 180, 30, 10], [97.384, 0.0, 'back']],
  ['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: back-side projection', [25, 270, 100, 3], [111.604, 0.0, 'back']],
  ['regression: back-side projection (partial repair)', [10, 180, 60, 3], [92.015, 0.0, 'back']],
  ['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: back-side projection', [90, 270, 150, 45], [110.705, 0.0, 'back']],
  ['regression: back-side projection (partial repair)', [30, 270, 90, 10], [110.0, 0.0, 'back']],
  ['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)

source:
"""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(abs(c), 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: back-side projection', [30, 135, 0, 10], [101.408, 0.0, 'back']],
  ['regression: back-side projection (partial repair)', [90, 180, 30, 20], [144.469, 0.0, 'back']],
  ['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: back-side projection', [90, 180, 30, 75], [102.953, 0.0, 'back']],
  ['regression: back-side projection (partial repair)', [25, 180, 60, 10], [92.907, 0.0, 'back']],
  ['control 1', [60, 225, 270, 20], [41.716, 0.7465, 'front']],
  ['control 2', [20, 90, 0, 89], [20.024, 0.9395, '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: back-side projection', [90, 90, 200, 30], [107.229, 0.0, 'back']],
  ['regression: back-side projection (partial repair)', [20, 180, 30, 10], [97.384, 0.0, 'back']],
  ['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: back-side projection', [25, 270, 100, 3], [111.604, 0.0, 'back']],
  ['regression: back-side projection (partial repair)', [10, 180, 60, 3], [92.015, 0.0, 'back']],
  ['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: back-side projection', [90, 270, 150, 45], [110.705, 0.0, 'back']],
  ['regression: back-side projection (partial repair)', [30, 270, 90, 10], [110.0, 0.0, 'back']],
  ['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)

prompt:
Irradiance projection becomes negative when the sun is behind the panel.

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.

split: open-access


broken_source:
"""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 = 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: tilt units', [30, 135, 0, 10], [101.408, 0.0, 'back']],
  ['regression: tilt units (partial repair)', [10, 225, 60, 89], [10.969, 0.9817, 'front']],
  ['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: tilt units', [20, 225, 270, 3], [73.101, 0.2907, 'front']],
  ['regression: tilt units (partial repair)', [90, 180, 250, 45], [76.005, 0.2418, 'front']],
  ['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: tilt units', [10, 135, 30, 45], [48.35, 0.6646, 'front']],
  ['regression: tilt units (partial repair)', [30, 270, 225, 10], [60.095, 0.4986, 'front']],
  ['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: tilt units', [30, 135, 100, 10], [56.376, 0.5537, 'front']],
  ['regression: tilt units (partial repair)', [20, 225, 270, 3], [73.101, 0.2907, 'front']],
  ['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: tilt units', [10, 180, 100, 30], [58.767, 0.5185, 'front']],
  ['regression: tilt units (partial repair)', [30, 225, 270, 60], [22.062, 0.9268, 'front']],
  ['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)

meaning: Fixture-count band, not measured model difficulty

deps: stdlib

source:
"""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(90 - 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: tilt units', [30, 135, 0, 10], [101.408, 0.0, 'back']],
  ['regression: tilt units (partial repair)', [10, 225, 60, 89], [10.969, 0.9817, 'front']],
  ['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: tilt units', [20, 225, 270, 3], [73.101, 0.2907, 'front']],
  ['regression: tilt units (partial repair)', [90, 180, 250, 45], [76.005, 0.2418, 'front']],
  ['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: tilt units', [10, 135, 30, 45], [48.35, 0.6646, 'front']],
  ['regression: tilt units (partial repair)', [30, 270, 225, 10], [60.095, 0.4986, 'front']],
  ['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: tilt units', [30, 135, 100, 10], [56.376, 0.5537, 'front']],
  ['regression: tilt units (partial repair)', [20, 225, 270, 3], [73.101, 0.2907, 'front']],
  ['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: tilt units', [10, 180, 100, 30], [58.767, 0.5185, 'front']],
  ['regression: tilt units (partial repair)', [30, 225, 270, 60], [22.062, 0.9268, 'front']],
  ['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)

prompt:
Incidence angles are nonsense for any tilted panel.

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.

split: open-access


broken_source:
"""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)

source:
"""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)

prompt:
A sun exactly on the horizon produces a front-side incidence for tilted panels.

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.

split: open-access


broken_source:
"""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.sin(zen) * math.cos(t) + math.cos(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: incidence formula terms', [30, 135, 0, 10], [101.408, 0.0, 'back']],
  ['regression: incidence formula terms (partial repair)', [10, 225, 60, 89],
   [10.969, 0.9817, 'front']],
  ['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: incidence formula terms', [20, 225, 270, 3], [73.101, 0.2907, 'front']],
  ['regression: incidence formula terms (partial repair)', [90, 180, 250, 45],
   [76.005, 0.2418, 'front']],
  ['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: incidence formula terms', [30, 270, 225, 10], [60.095, 0.4986, 'front']],
  ['regression: incidence formula terms (partial repair)', [10, 135, 30, 45],
   [48.35, 0.6646, 'front']],
  ['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: incidence formula terms', [30, 135, 100, 10], [56.376, 0.5537, 'front']],
  ['regression: incidence formula terms (partial repair)', [20, 225, 270, 3],
   [73.101, 0.2907, 'front']],
  ['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: incidence formula terms', [10, 180, 100, 30], [58.767, 0.5185, 'front']],
  ['regression: incidence formula terms (partial repair)', [30, 225, 270, 60],
   [22.062, 0.9268, 'front']],
  ['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)

source:
"""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)
    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: incidence formula terms', [30, 135, 0, 10], [101.408, 0.0, 'back']],
  ['regression: incidence formula terms (partial repair)', [10, 225, 60, 89],
   [10.969, 0.9817, 'front']],
  ['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: incidence formula terms', [20, 225, 270, 3], [73.101, 0.2907, 'front']],
  ['regression: incidence formula terms (partial repair)', [90, 180, 250, 45],
   [76.005, 0.2418, 'front']],
  ['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: incidence formula terms', [30, 270, 225, 10], [60.095, 0.4986, 'front']],
  ['regression: incidence formula terms (partial repair)', [10, 135, 30, 45],
   [48.35, 0.6646, 'front']],
  ['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: incidence formula terms', [30, 135, 100, 10], [56.376, 0.5537, 'front']],
  ['regression: incidence formula terms (partial repair)', [20, 225, 270, 3],
   [73.101, 0.2907, 'front']],
  ['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: incidence formula terms', [10, 180, 100, 30], [58.767, 0.5185, 'front']],
  ['regression: incidence formula terms (partial repair)', [30, 225, 270, 60],
   [22.062, 0.9268, 'front']],
  ['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)

prompt:
Incidence is wrong even for a flat panel.

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.

split: open-access