FAILURE MAP
← Case archive

FA-93776 / Solar tracker geometry / Open access

Hazard stow angle selection: manual override precedence · case 01

An operator manual angle keeps rows tilted during a wind storm.

Verified by executionVariant 1 · 7 checks per implementationDownload source bundle ↓JSON ↗

ROOT CAUSE

Manual commands are evaluated before the wind safety stow.

VERIFIED REPAIR

Evaluate wind stow before manual override.

Unsuccessful approach: Suppressing wind stow whenever a manual angle is set keeps the same hazard.

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 manual is not None:
        return [float(max(-max_angle, min(max_angle, manual))), 'manual']
    if wind >= 20:
        return [0.0, 'wind']
    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: manual override precedence',
   [{'wind': 25, 'hail_from': 225, 'snow': True}, 5, 180, 60, 65], [0.0, 'wind']],
  ['regression: manual override precedence (partial repair)',
   [{'wind': 25, 'hail_from': 0}, 3, 0, 50, 0], [0.0, 'wind']],
  ['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: manual override precedence', [{'wind': 20}, 4, 0, 60, -70], [0.0, 'wind']],
  ['regression: manual override precedence (partial repair)',
   [{'wind': 20, 'hail_from': 180}, 1, 180, 50, 0], [0.0, 'wind']],
  ['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: manual override precedence', [{'wind': 21}, 4, 0, 50, -70], [0.0, 'wind']],
  ['regression: manual override precedence (partial repair)',
   [{'wind': 20, 'hail_from': 0}, 1, 0, 60, 30], [0.0, 'wind']],
  ['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: manual override precedence', [{'wind': 20, 'hail_from': 225}, 0, 0, 50, 65],
   [0.0, 'wind']],
  ['regression: manual override precedence (partial repair)',
   [{'wind': 21, 'snow': True}, 1, 180, 50, -20], [0.0, 'wind']],
  ['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: manual override precedence', [{'wind': 21, 'hail_from': 45}, 5, 180, 50, 30],
   [0.0, 'wind']],
  ['regression: manual override precedence (partial repair)',
   [{'wind': 20, 'snow': True}, 4, 180, 50, -70], [0.0, 'wind']],
  ['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 fixtureActualExpectedOutcome
boundary: wind exactly at stow limit[30.0, 'manual'][0.0, 'wind']Failed
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: manual override precedence[60.0, 'manual'][0.0, 'wind']Failed
regression: manual override precedence (partial repair)[0.0, 'manual'][0.0, 'wind']Failed
control 1[0.0, 'manual'][0.0, 'manual']Passed
control 2[-50.0, 'manual'][0.0, 'wind']Failed

SHA-256 / 3b9286898c8c57589c1f46729f7471c0b9ac8241727e50105e75a7a76a29db5f

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 and manual is None:
        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: manual override precedence',
   [{'wind': 25, 'hail_from': 225, 'snow': True}, 5, 180, 60, 65], [0.0, 'wind']],
  ['regression: manual override precedence (partial repair)',
   [{'wind': 25, 'hail_from': 0}, 3, 0, 50, 0], [0.0, 'wind']],
  ['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: manual override precedence', [{'wind': 20}, 4, 0, 60, -70], [0.0, 'wind']],
  ['regression: manual override precedence (partial repair)',
   [{'wind': 20, 'hail_from': 180}, 1, 180, 50, 0], [0.0, 'wind']],
  ['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: manual override precedence', [{'wind': 21}, 4, 0, 50, -70], [0.0, 'wind']],
  ['regression: manual override precedence (partial repair)',
   [{'wind': 20, 'hail_from': 0}, 1, 0, 60, 30], [0.0, 'wind']],
  ['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: manual override precedence', [{'wind': 20, 'hail_from': 225}, 0, 0, 50, 65],
   [0.0, 'wind']],
  ['regression: manual override precedence (partial repair)',
   [{'wind': 21, 'snow': True}, 1, 180, 50, -20], [0.0, 'wind']],
  ['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: manual override precedence', [{'wind': 21, 'hail_from': 45}, 5, 180, 50, 30],
   [0.0, 'wind']],
  ['regression: manual override precedence (partial repair)',
   [{'wind': 20, 'snow': True}, 4, 180, 50, -70], [0.0, 'wind']],
  ['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 fixtureActualExpectedOutcome
boundary: wind exactly at stow limit[30.0, 'manual'][0.0, 'wind']Failed
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: manual override precedence[60.0, 'manual'][0.0, 'wind']Failed
regression: manual override precedence (partial repair)[0.0, 'manual'][0.0, 'wind']Failed
control 1[0.0, 'manual'][0.0, 'manual']Passed
control 2[-50.0, 'manual'][0.0, 'wind']Failed

SHA-256 / 4fc33ed26e4895d64b57a20b2d371f636ab4667db92576c8493e4bb0c4ce237d

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: manual override precedence',
   [{'wind': 25, 'hail_from': 225, 'snow': True}, 5, 180, 60, 65], [0.0, 'wind']],
  ['regression: manual override precedence (partial repair)',
   [{'wind': 25, 'hail_from': 0}, 3, 0, 50, 0], [0.0, 'wind']],
  ['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: manual override precedence', [{'wind': 20}, 4, 0, 60, -70], [0.0, 'wind']],
  ['regression: manual override precedence (partial repair)',
   [{'wind': 20, 'hail_from': 180}, 1, 180, 50, 0], [0.0, 'wind']],
  ['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: manual override precedence', [{'wind': 21}, 4, 0, 50, -70], [0.0, 'wind']],
  ['regression: manual override precedence (partial repair)',
   [{'wind': 20, 'hail_from': 0}, 1, 0, 60, 30], [0.0, 'wind']],
  ['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: manual override precedence', [{'wind': 20, 'hail_from': 225}, 0, 0, 50, 65],
   [0.0, 'wind']],
  ['regression: manual override precedence (partial repair)',
   [{'wind': 21, 'snow': True}, 1, 180, 50, -20], [0.0, 'wind']],
  ['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: manual override precedence', [{'wind': 21, 'hail_from': 45}, 5, 180, 50, 30],
   [0.0, 'wind']],
  ['regression: manual override precedence (partial repair)',
   [{'wind': 20, 'snow': True}, 4, 180, 50, -70], [0.0, 'wind']],
  ['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 fixtureActualExpectedOutcome
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: manual override precedence[0.0, 'wind'][0.0, 'wind']Passed
regression: manual override precedence (partial repair)[0.0, 'wind'][0.0, 'wind']Passed
control 1[0.0, 'manual'][0.0, 'manual']Passed
control 2[0.0, 'wind'][0.0, 'wind']Passed

SHA-256 / 9e9ee875891ec22594cf13b45df6bb11ec88b21122dbc608047e1d26b8360baa

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.192343+00:00.

Case digest / e9356f11d9dc75464715851755f66e9434d284d6994217815f95643057a89072