FA-93791 / Solar tracker geometry / Open access
Hazard stow angle selection: hail bearing boundary · case 01
Hail arriving along the axis tilts rows to the positive side.
ROOT CAUSE
A bearing of exactly 0 counts as the positive side.
VERIFIED REPAIR
Only bearings strictly between 0 and 180 are positive.
Unsuccessful approach: Including 180 moves the ambiguity to the other end of the axis.
Case contract
Priority: wind >= 20 m/s gives [0.0, 'wind']; then a manual angle (clamped to +-max_angle) gives 'manual'; then hail from azimuth hail_from gives +max_angle when (hail_from - axis_az) mod 360 is strictly between 0 and 180, else -max_angle ('hail'); then snow alternates rows: even rows +max_angle, odd rows -max_angle ('snow'); otherwise [None, 'normal'].
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
N = 1
observations = []
def solve(hazards, row, axis_az, max_angle, manual):
wind = hazards.get('wind', 0)
if wind >= 20:
return [0.0, 'wind']
if manual is not None:
return [float(max(-max_angle, min(max_angle, manual))), 'manual']
hail = hazards.get('hail_from')
if hail is not None:
rel = (hail - axis_az) % 360
return [float(max_angle if 0 <= rel < 180 else -max_angle), 'hail']
if hazards.get('snow'):
return [float(max_angle if row % 2 == 0 else -max_angle), 'snow']
return [None, 'normal']
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['boundary: wind exactly at stow limit', [{'wind': 20}, 0, 180, 60, 30], [0.0, 'wind']],
['boundary: hail along the axis', [{'hail_from': 180}, 1, 180, 60, None], [-60.0, 'hail']],
['normal: snow on odd row', [{'snow': True}, 3, 180, 60, None], [-60.0, 'snow']],
['regression: hail bearing boundary', [{'wind': 5, 'hail_from': 0, 'snow': True}, 4, 0, 60, None],
[-60.0, 'hail']],
['regression: hail bearing boundary (partial repair)', [{'hail_from': 0}, 2, 180, 60, None],
[-60.0, 'hail']],
['control 1', [{'wind': 15, 'hail_from': 180}, 5, 180, 50, 0], [0.0, 'manual']],
['control 2', [{'wind': 20, 'hail_from': 90, 'snow': False}, 2, 180, 50, -70], [0.0, 'wind']]],
[['boundary: wind exactly at stow limit', [{'wind': 20}, 0, 180, 60, 30], [0.0, 'wind']],
['boundary: hail along the axis', [{'hail_from': 180}, 1, 180, 60, None], [-60.0, 'hail']],
['normal: snow on odd row', [{'snow': True}, 3, 180, 60, None], [-60.0, 'snow']],
['regression: hail bearing boundary', [{'hail_from': 0, 'snow': True}, 2, 0, 50, None],
[-50.0, 'hail']],
['regression: hail bearing boundary (partial repair)',
[{'wind': 19, 'hail_from': 0, 'snow': True}, 2, 180, 60, None], [-60.0, 'hail']],
['control 1', [{}, 3, 0, 60, 65], [60.0, 'manual']],
['control 2', [{'wind': 21, 'hail_from': 225, 'snow': True}, 1, 180, 60, -70], [0.0, 'wind']]],
[['boundary: wind exactly at stow limit', [{'wind': 20}, 0, 180, 60, 30], [0.0, 'wind']],
['boundary: hail along the axis', [{'hail_from': 180}, 1, 180, 60, None], [-60.0, 'hail']],
['normal: snow on odd row', [{'snow': True}, 3, 180, 60, None], [-60.0, 'snow']],
['regression: hail bearing boundary', [{'hail_from': 0, 'snow': True}, 2, 0, 50, None],
[-50.0, 'hail']],
['regression: hail bearing boundary (partial repair)',
[{'wind': 19, 'hail_from': 0, 'snow': True}, 2, 180, 60, None], [-60.0, 'hail']],
['control 1', [{}, 3, 180, 50, -20], [-20.0, 'manual']],
['control 2', [{'wind': 21, 'snow': False}, 1, 0, 60, 65], [0.0, 'wind']]],
[['boundary: wind exactly at stow limit', [{'wind': 20}, 0, 180, 60, 30], [0.0, 'wind']],
['boundary: hail along the axis', [{'hail_from': 180}, 1, 180, 60, None], [-60.0, 'hail']],
['normal: snow on odd row', [{'snow': True}, 3, 180, 60, None], [-60.0, 'snow']],
['regression: hail bearing boundary', [{'hail_from': 0, 'snow': True}, 2, 0, 50, None],
[-50.0, 'hail']],
['regression: hail bearing boundary (partial repair)', [{'hail_from': 0}, 1, 180, 50, None],
[-50.0, 'hail']],
['control 1', [{'hail_from': None, 'snow': True}, 1, 180, 50, 30], [30.0, 'manual']],
['control 2', [{'wind': 15, 'snow': True}, 3, 0, 50, -20], [-20.0, 'manual']]],
[['boundary: wind exactly at stow limit', [{'wind': 20}, 0, 180, 60, 30], [0.0, 'wind']],
['boundary: hail along the axis', [{'hail_from': 180}, 1, 180, 60, None], [-60.0, 'hail']],
['normal: snow on odd row', [{'snow': True}, 3, 180, 60, None], [-60.0, 'snow']],
['regression: hail bearing boundary', [{'hail_from': 0, 'snow': True}, 2, 0, 50, None],
[-50.0, 'hail']],
['regression: hail bearing boundary (partial repair)', [{'hail_from': 0}, 2, 180, 60, None],
[-60.0, 'hail']],
['control 1', [{'wind': 19, 'snow': False}, 1, 180, 60, None], [None, 'normal']],
['control 2', [{'snow': True}, 4, 0, 50, -20], [-20.0, 'manual']]]]
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: wind exactly at stow limit | [0.0, 'wind'] | [0.0, 'wind'] | Passed |
| boundary: hail along the axis | [60.0, 'hail'] | [-60.0, 'hail'] | Failed |
| normal: snow on odd row | [-60.0, 'snow'] | [-60.0, 'snow'] | Passed |
| regression: hail bearing boundary | [60.0, 'hail'] | [-60.0, 'hail'] | Failed |
| regression: hail bearing boundary (partial repair) | [-60.0, 'hail'] | [-60.0, 'hail'] | Passed |
| control 1 | [0.0, 'manual'] | [0.0, 'manual'] | Passed |
| control 2 | [0.0, 'wind'] | [0.0, 'wind'] | Passed |
SHA-256 / 2818dc8f0c3b138635f252e130f4247caf6b9da5bf7fafe549238f37d566ea37
2 / The unsuccessful fix
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(hazards, row, axis_az, max_angle, manual):
wind = hazards.get('wind', 0)
if wind >= 20:
return [0.0, 'wind']
if manual is not None:
return [float(max(-max_angle, min(max_angle, manual))), 'manual']
hail = hazards.get('hail_from')
if hail is not None:
rel = (hail - axis_az) % 360
return [float(max_angle if 0 < rel <= 180 else -max_angle), 'hail']
if hazards.get('snow'):
return [float(max_angle if row % 2 == 0 else -max_angle), 'snow']
return [None, 'normal']
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['boundary: wind exactly at stow limit', [{'wind': 20}, 0, 180, 60, 30], [0.0, 'wind']],
['boundary: hail along the axis', [{'hail_from': 180}, 1, 180, 60, None], [-60.0, 'hail']],
['normal: snow on odd row', [{'snow': True}, 3, 180, 60, None], [-60.0, 'snow']],
['regression: hail bearing boundary', [{'wind': 5, 'hail_from': 0, 'snow': True}, 4, 0, 60, None],
[-60.0, 'hail']],
['regression: hail bearing boundary (partial repair)', [{'hail_from': 0}, 2, 180, 60, None],
[-60.0, 'hail']],
['control 1', [{'wind': 15, 'hail_from': 180}, 5, 180, 50, 0], [0.0, 'manual']],
['control 2', [{'wind': 20, 'hail_from': 90, 'snow': False}, 2, 180, 50, -70], [0.0, 'wind']]],
[['boundary: wind exactly at stow limit', [{'wind': 20}, 0, 180, 60, 30], [0.0, 'wind']],
['boundary: hail along the axis', [{'hail_from': 180}, 1, 180, 60, None], [-60.0, 'hail']],
['normal: snow on odd row', [{'snow': True}, 3, 180, 60, None], [-60.0, 'snow']],
['regression: hail bearing boundary', [{'hail_from': 0, 'snow': True}, 2, 0, 50, None],
[-50.0, 'hail']],
['regression: hail bearing boundary (partial repair)',
[{'wind': 19, 'hail_from': 0, 'snow': True}, 2, 180, 60, None], [-60.0, 'hail']],
['control 1', [{}, 3, 0, 60, 65], [60.0, 'manual']],
['control 2', [{'wind': 21, 'hail_from': 225, 'snow': True}, 1, 180, 60, -70], [0.0, 'wind']]],
[['boundary: wind exactly at stow limit', [{'wind': 20}, 0, 180, 60, 30], [0.0, 'wind']],
['boundary: hail along the axis', [{'hail_from': 180}, 1, 180, 60, None], [-60.0, 'hail']],
['normal: snow on odd row', [{'snow': True}, 3, 180, 60, None], [-60.0, 'snow']],
['regression: hail bearing boundary', [{'hail_from': 0, 'snow': True}, 2, 0, 50, None],
[-50.0, 'hail']],
['regression: hail bearing boundary (partial repair)',
[{'wind': 19, 'hail_from': 0, 'snow': True}, 2, 180, 60, None], [-60.0, 'hail']],
['control 1', [{}, 3, 180, 50, -20], [-20.0, 'manual']],
['control 2', [{'wind': 21, 'snow': False}, 1, 0, 60, 65], [0.0, 'wind']]],
[['boundary: wind exactly at stow limit', [{'wind': 20}, 0, 180, 60, 30], [0.0, 'wind']],
['boundary: hail along the axis', [{'hail_from': 180}, 1, 180, 60, None], [-60.0, 'hail']],
['normal: snow on odd row', [{'snow': True}, 3, 180, 60, None], [-60.0, 'snow']],
['regression: hail bearing boundary', [{'hail_from': 0, 'snow': True}, 2, 0, 50, None],
[-50.0, 'hail']],
['regression: hail bearing boundary (partial repair)', [{'hail_from': 0}, 1, 180, 50, None],
[-50.0, 'hail']],
['control 1', [{'hail_from': None, 'snow': True}, 1, 180, 50, 30], [30.0, 'manual']],
['control 2', [{'wind': 15, 'snow': True}, 3, 0, 50, -20], [-20.0, 'manual']]],
[['boundary: wind exactly at stow limit', [{'wind': 20}, 0, 180, 60, 30], [0.0, 'wind']],
['boundary: hail along the axis', [{'hail_from': 180}, 1, 180, 60, None], [-60.0, 'hail']],
['normal: snow on odd row', [{'snow': True}, 3, 180, 60, None], [-60.0, 'snow']],
['regression: hail bearing boundary', [{'hail_from': 0, 'snow': True}, 2, 0, 50, None],
[-50.0, 'hail']],
['regression: hail bearing boundary (partial repair)', [{'hail_from': 0}, 2, 180, 60, None],
[-60.0, 'hail']],
['control 1', [{'wind': 19, 'snow': False}, 1, 180, 60, None], [None, 'normal']],
['control 2', [{'snow': True}, 4, 0, 50, -20], [-20.0, 'manual']]]]
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: wind exactly at stow limit | [0.0, 'wind'] | [0.0, 'wind'] | Passed |
| boundary: hail along the axis | [-60.0, 'hail'] | [-60.0, 'hail'] | Passed |
| normal: snow on odd row | [-60.0, 'snow'] | [-60.0, 'snow'] | Passed |
| regression: hail bearing boundary | [-60.0, 'hail'] | [-60.0, 'hail'] | Passed |
| regression: hail bearing boundary (partial repair) | [60.0, 'hail'] | [-60.0, 'hail'] | Failed |
| control 1 | [0.0, 'manual'] | [0.0, 'manual'] | Passed |
| control 2 | [0.0, 'wind'] | [0.0, 'wind'] | Passed |
SHA-256 / 75d1b367547178f9be9914e23b7647d6a1af4b42084a9dce406c6de13d9ee324
3 / The verified repair
Exit 0"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(hazards, row, axis_az, max_angle, manual):
wind = hazards.get('wind', 0)
if wind >= 20:
return [0.0, 'wind']
if manual is not None:
return [float(max(-max_angle, min(max_angle, manual))), 'manual']
hail = hazards.get('hail_from')
if hail is not None:
rel = (hail - axis_az) % 360
return [float(max_angle if 0 < rel < 180 else -max_angle), 'hail']
if hazards.get('snow'):
return [float(max_angle if row % 2 == 0 else -max_angle), 'snow']
return [None, 'normal']
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['boundary: wind exactly at stow limit', [{'wind': 20}, 0, 180, 60, 30], [0.0, 'wind']],
['boundary: hail along the axis', [{'hail_from': 180}, 1, 180, 60, None], [-60.0, 'hail']],
['normal: snow on odd row', [{'snow': True}, 3, 180, 60, None], [-60.0, 'snow']],
['regression: hail bearing boundary', [{'wind': 5, 'hail_from': 0, 'snow': True}, 4, 0, 60, None],
[-60.0, 'hail']],
['regression: hail bearing boundary (partial repair)', [{'hail_from': 0}, 2, 180, 60, None],
[-60.0, 'hail']],
['control 1', [{'wind': 15, 'hail_from': 180}, 5, 180, 50, 0], [0.0, 'manual']],
['control 2', [{'wind': 20, 'hail_from': 90, 'snow': False}, 2, 180, 50, -70], [0.0, 'wind']]],
[['boundary: wind exactly at stow limit', [{'wind': 20}, 0, 180, 60, 30], [0.0, 'wind']],
['boundary: hail along the axis', [{'hail_from': 180}, 1, 180, 60, None], [-60.0, 'hail']],
['normal: snow on odd row', [{'snow': True}, 3, 180, 60, None], [-60.0, 'snow']],
['regression: hail bearing boundary', [{'hail_from': 0, 'snow': True}, 2, 0, 50, None],
[-50.0, 'hail']],
['regression: hail bearing boundary (partial repair)',
[{'wind': 19, 'hail_from': 0, 'snow': True}, 2, 180, 60, None], [-60.0, 'hail']],
['control 1', [{}, 3, 0, 60, 65], [60.0, 'manual']],
['control 2', [{'wind': 21, 'hail_from': 225, 'snow': True}, 1, 180, 60, -70], [0.0, 'wind']]],
[['boundary: wind exactly at stow limit', [{'wind': 20}, 0, 180, 60, 30], [0.0, 'wind']],
['boundary: hail along the axis', [{'hail_from': 180}, 1, 180, 60, None], [-60.0, 'hail']],
['normal: snow on odd row', [{'snow': True}, 3, 180, 60, None], [-60.0, 'snow']],
['regression: hail bearing boundary', [{'hail_from': 0, 'snow': True}, 2, 0, 50, None],
[-50.0, 'hail']],
['regression: hail bearing boundary (partial repair)',
[{'wind': 19, 'hail_from': 0, 'snow': True}, 2, 180, 60, None], [-60.0, 'hail']],
['control 1', [{}, 3, 180, 50, -20], [-20.0, 'manual']],
['control 2', [{'wind': 21, 'snow': False}, 1, 0, 60, 65], [0.0, 'wind']]],
[['boundary: wind exactly at stow limit', [{'wind': 20}, 0, 180, 60, 30], [0.0, 'wind']],
['boundary: hail along the axis', [{'hail_from': 180}, 1, 180, 60, None], [-60.0, 'hail']],
['normal: snow on odd row', [{'snow': True}, 3, 180, 60, None], [-60.0, 'snow']],
['regression: hail bearing boundary', [{'hail_from': 0, 'snow': True}, 2, 0, 50, None],
[-50.0, 'hail']],
['regression: hail bearing boundary (partial repair)', [{'hail_from': 0}, 1, 180, 50, None],
[-50.0, 'hail']],
['control 1', [{'hail_from': None, 'snow': True}, 1, 180, 50, 30], [30.0, 'manual']],
['control 2', [{'wind': 15, 'snow': True}, 3, 0, 50, -20], [-20.0, 'manual']]],
[['boundary: wind exactly at stow limit', [{'wind': 20}, 0, 180, 60, 30], [0.0, 'wind']],
['boundary: hail along the axis', [{'hail_from': 180}, 1, 180, 60, None], [-60.0, 'hail']],
['normal: snow on odd row', [{'snow': True}, 3, 180, 60, None], [-60.0, 'snow']],
['regression: hail bearing boundary', [{'hail_from': 0, 'snow': True}, 2, 0, 50, None],
[-50.0, 'hail']],
['regression: hail bearing boundary (partial repair)', [{'hail_from': 0}, 2, 180, 60, None],
[-60.0, 'hail']],
['control 1', [{'wind': 19, 'snow': False}, 1, 180, 60, None], [None, 'normal']],
['control 2', [{'snow': True}, 4, 0, 50, -20], [-20.0, 'manual']]]]
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: wind exactly at stow limit | [0.0, 'wind'] | [0.0, 'wind'] | Passed |
| boundary: hail along the axis | [-60.0, 'hail'] | [-60.0, 'hail'] | Passed |
| normal: snow on odd row | [-60.0, 'snow'] | [-60.0, 'snow'] | Passed |
| regression: hail bearing boundary | [-60.0, 'hail'] | [-60.0, 'hail'] | Passed |
| regression: hail bearing boundary (partial repair) | [-60.0, 'hail'] | [-60.0, 'hail'] | Passed |
| control 1 | [0.0, 'manual'] | [0.0, 'manual'] | Passed |
| control 2 | [0.0, 'wind'] | [0.0, 'wind'] | Passed |
SHA-256 / 5cbd8b77c249b2ad277172a3ee107f4cc4f254309d56c4e71b9215c71dd58d39
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.327051+00:00.
Case digest / 9b202db2b3ecd45016cb081525fcba88ee7205cda836cd5f95edb53641bcd9b9