FAILURE MAP
← Case archive

FA-93731 / Solar tracker geometry / Open access

Anemometer fail-safe stow voting: wind aggregation · case 01

A single anemometer measuring a storm is averaged away and the plant keeps tracking.

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

ROOT CAUSE

Valid readings are averaged instead of taking the maximum.

VERIFIED REPAIR

Use the maximum valid reading.

Unsuccessful approach: The median also hides the one sensor seeing the gust.

Case contract

readings are [value or None, age_s] per anemometer. A reading is valid when value is not None and age <= max_age. Fewer than n_required valid readings: ['stow', 'sensor', None]. Otherwise use the highest valid value: >= stow_at gives ['stow', 'wind', v], else ['track', 'ok', v].

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(readings, max_age, stow_at, n_required):
    valid = [v for v, age in readings if v is not None and age <= max_age]
    if len(valid) < n_required:
        return ['stow', 'sensor', None]
    peak = sum(valid) / len(valid)
    if peak >= stow_at:
        return ['stow', 'wind', peak]
    return ['track', 'ok', peak]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['boundary: reading exactly max_age old', [[[10, 60]], 60, 15, 1], ['track', 'ok', 10]],
  ['boundary: calm zero reading', [[[0, 5], [None, 5]], 60, 15, 1], ['track', 'ok', 0]],
  ['boundary: wind at stow limit', [[[15, 5], [4, 5]], 60, 15, 2], ['stow', 'wind', 15]],
  ['regression: wind aggregation', [[[3.5, 5], [0, 5], [8, 0]], 60, 15, 2], ['track', 'ok', 8]],
  ['regression: wind aggregation (partial repair)',
   [[[0, 0], [None, 61], [16.5, 60], [8, 30]], 60, 18, 2], ['track', 'ok', 16.5]],
  ['control 1', [[[14.5, 5], [8, 5], [22, 60]], 60, 15, 2], ['stow', 'wind', 22]],
  ['control 2', [[[8, 60], [8, 120]], 60, 18, 1], ['track', 'ok', 8]]],
 [['boundary: reading exactly max_age old', [[[10, 60]], 60, 15, 1], ['track', 'ok', 10]],
  ['boundary: calm zero reading', [[[0, 5], [None, 5]], 60, 15, 1], ['track', 'ok', 0]],
  ['boundary: wind at stow limit', [[[15, 5], [4, 5]], 60, 15, 2], ['stow', 'wind', 15]],
  ['regression: wind aggregation', [[[22, 120], [0, 30], [14.5, 0]], 30, 15, 2],
   ['track', 'ok', 14.5]],
  ['regression: wind aggregation (partial repair)', [[[14.5, 30], [8, 5], [8, 10]], 60, 18, 1],
   ['track', 'ok', 14.5]],
  ['control 1', [[[8, 60], [0, 10], [14.5, 10]], 60, 18, 1], ['track', 'ok', 14.5]],
  ['control 2', [[[3.5, 61], [22, 61], [22, 10], [16.5, 30]], 60, 18, 1], ['stow', 'wind', 22]]],
 [['boundary: reading exactly max_age old', [[[10, 60]], 60, 15, 1], ['track', 'ok', 10]],
  ['boundary: calm zero reading', [[[0, 5], [None, 5]], 60, 15, 1], ['track', 'ok', 0]],
  ['boundary: wind at stow limit', [[[15, 5], [4, 5]], 60, 15, 2], ['stow', 'wind', 15]],
  ['regression: wind aggregation', [[[14.5, 30], [0, 0], [None, 10], [14.5, 60]], 30, 15, 1],
   ['track', 'ok', 14.5]],
  ['regression: wind aggregation (partial repair)',
   [[[22, 30], [12, 30], [0, 0], [16.5, 120]], 60, 18, 2], ['stow', 'wind', 22]],
  ['control 1', [[[22, 61], [22, 120]], 30, 18, 2], ['stow', 'sensor', None]],
  ['control 2', [[[3.5, 30], [15, 0], [None, 61], [None, 30]], 60, 18, 2], ['track', 'ok', 15]]],
 [['boundary: reading exactly max_age old', [[[10, 60]], 60, 15, 1], ['track', 'ok', 10]],
  ['boundary: calm zero reading', [[[0, 5], [None, 5]], 60, 15, 1], ['track', 'ok', 0]],
  ['boundary: wind at stow limit', [[[15, 5], [4, 5]], 60, 15, 2], ['stow', 'wind', 15]],
  ['regression: wind aggregation', [[[8, 10], [16.5, 60], [22, 61], [15, 5]], 60, 18, 1],
   ['track', 'ok', 16.5]],
  ['regression: wind aggregation (partial repair)',
   [[[22, 10], [8, 60], [14.5, 5], [12, 0]], 60, 15, 2], ['stow', 'wind', 22]],
  ['control 1', [[[3.5, 5], [22, 30]], 30, 18, 2], ['stow', 'wind', 22]],
  ['control 2', [[[22, 0], [3.5, 60], [12, 30], [14.5, 5]], 30, 15, 1], ['stow', 'wind', 22]]],
 [['boundary: reading exactly max_age old', [[[10, 60]], 60, 15, 1], ['track', 'ok', 10]],
  ['boundary: calm zero reading', [[[0, 5], [None, 5]], 60, 15, 1], ['track', 'ok', 0]],
  ['boundary: wind at stow limit', [[[15, 5], [4, 5]], 60, 15, 2], ['stow', 'wind', 15]],
  ['regression: wind aggregation', [[[22, 0], [12, 61], [16.5, 10]], 30, 15, 2],
   ['stow', 'wind', 22]],
  ['regression: wind aggregation (partial repair)',
   [[[12, 30], [16.5, 10], [15, 61], [12, 10]], 60, 18, 2], ['track', 'ok', 16.5]],
  ['control 1', [[[3.5, 61], [0, 5], [0, 60], [14.5, 120]], 60, 18, 1], ['track', 'ok', 0]],
  ['control 2', [[[15, 10], [12, 61], [14.5, 60], [None, 60]], 30, 15, 1], ['stow', 'wind', 15]]]]
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: reading exactly max_age old['track', 'ok', 10.0]['track', 'ok', 10]Passed
boundary: calm zero reading['track', 'ok', 0.0]['track', 'ok', 0]Passed
boundary: wind at stow limit['track', 'ok', 9.5]['stow', 'wind', 15]Failed
regression: wind aggregation['track', 'ok', 3.8333333333333335]['track', 'ok', 8]Failed
regression: wind aggregation (partial repair)['track', 'ok', 8.166666666666666]['track', 'ok', 16.5]Failed
control 1['track', 'ok', 14.833333333333334]['stow', 'wind', 22]Failed
control 2['track', 'ok', 8.0]['track', 'ok', 8]Passed

SHA-256 / d6947cc7d87057c9327d2eb21a94157bf9c6f7380fa6e63bb865c998b3f3d7a3

2 / The unsuccessful fix

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json

N = 1
observations = []
def solve(readings, max_age, stow_at, n_required):
    valid = [v for v, age in readings if v is not None and age <= max_age]
    if len(valid) < n_required:
        return ['stow', 'sensor', None]
    peak = sorted(valid)[len(valid) // 2]
    if peak >= stow_at:
        return ['stow', 'wind', peak]
    return ['track', 'ok', peak]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['boundary: reading exactly max_age old', [[[10, 60]], 60, 15, 1], ['track', 'ok', 10]],
  ['boundary: calm zero reading', [[[0, 5], [None, 5]], 60, 15, 1], ['track', 'ok', 0]],
  ['boundary: wind at stow limit', [[[15, 5], [4, 5]], 60, 15, 2], ['stow', 'wind', 15]],
  ['regression: wind aggregation', [[[3.5, 5], [0, 5], [8, 0]], 60, 15, 2], ['track', 'ok', 8]],
  ['regression: wind aggregation (partial repair)',
   [[[0, 0], [None, 61], [16.5, 60], [8, 30]], 60, 18, 2], ['track', 'ok', 16.5]],
  ['control 1', [[[14.5, 5], [8, 5], [22, 60]], 60, 15, 2], ['stow', 'wind', 22]],
  ['control 2', [[[8, 60], [8, 120]], 60, 18, 1], ['track', 'ok', 8]]],
 [['boundary: reading exactly max_age old', [[[10, 60]], 60, 15, 1], ['track', 'ok', 10]],
  ['boundary: calm zero reading', [[[0, 5], [None, 5]], 60, 15, 1], ['track', 'ok', 0]],
  ['boundary: wind at stow limit', [[[15, 5], [4, 5]], 60, 15, 2], ['stow', 'wind', 15]],
  ['regression: wind aggregation', [[[22, 120], [0, 30], [14.5, 0]], 30, 15, 2],
   ['track', 'ok', 14.5]],
  ['regression: wind aggregation (partial repair)', [[[14.5, 30], [8, 5], [8, 10]], 60, 18, 1],
   ['track', 'ok', 14.5]],
  ['control 1', [[[8, 60], [0, 10], [14.5, 10]], 60, 18, 1], ['track', 'ok', 14.5]],
  ['control 2', [[[3.5, 61], [22, 61], [22, 10], [16.5, 30]], 60, 18, 1], ['stow', 'wind', 22]]],
 [['boundary: reading exactly max_age old', [[[10, 60]], 60, 15, 1], ['track', 'ok', 10]],
  ['boundary: calm zero reading', [[[0, 5], [None, 5]], 60, 15, 1], ['track', 'ok', 0]],
  ['boundary: wind at stow limit', [[[15, 5], [4, 5]], 60, 15, 2], ['stow', 'wind', 15]],
  ['regression: wind aggregation', [[[14.5, 30], [0, 0], [None, 10], [14.5, 60]], 30, 15, 1],
   ['track', 'ok', 14.5]],
  ['regression: wind aggregation (partial repair)',
   [[[22, 30], [12, 30], [0, 0], [16.5, 120]], 60, 18, 2], ['stow', 'wind', 22]],
  ['control 1', [[[22, 61], [22, 120]], 30, 18, 2], ['stow', 'sensor', None]],
  ['control 2', [[[3.5, 30], [15, 0], [None, 61], [None, 30]], 60, 18, 2], ['track', 'ok', 15]]],
 [['boundary: reading exactly max_age old', [[[10, 60]], 60, 15, 1], ['track', 'ok', 10]],
  ['boundary: calm zero reading', [[[0, 5], [None, 5]], 60, 15, 1], ['track', 'ok', 0]],
  ['boundary: wind at stow limit', [[[15, 5], [4, 5]], 60, 15, 2], ['stow', 'wind', 15]],
  ['regression: wind aggregation', [[[8, 10], [16.5, 60], [22, 61], [15, 5]], 60, 18, 1],
   ['track', 'ok', 16.5]],
  ['regression: wind aggregation (partial repair)',
   [[[22, 10], [8, 60], [14.5, 5], [12, 0]], 60, 15, 2], ['stow', 'wind', 22]],
  ['control 1', [[[3.5, 5], [22, 30]], 30, 18, 2], ['stow', 'wind', 22]],
  ['control 2', [[[22, 0], [3.5, 60], [12, 30], [14.5, 5]], 30, 15, 1], ['stow', 'wind', 22]]],
 [['boundary: reading exactly max_age old', [[[10, 60]], 60, 15, 1], ['track', 'ok', 10]],
  ['boundary: calm zero reading', [[[0, 5], [None, 5]], 60, 15, 1], ['track', 'ok', 0]],
  ['boundary: wind at stow limit', [[[15, 5], [4, 5]], 60, 15, 2], ['stow', 'wind', 15]],
  ['regression: wind aggregation', [[[22, 0], [12, 61], [16.5, 10]], 30, 15, 2],
   ['stow', 'wind', 22]],
  ['regression: wind aggregation (partial repair)',
   [[[12, 30], [16.5, 10], [15, 61], [12, 10]], 60, 18, 2], ['track', 'ok', 16.5]],
  ['control 1', [[[3.5, 61], [0, 5], [0, 60], [14.5, 120]], 60, 18, 1], ['track', 'ok', 0]],
  ['control 2', [[[15, 10], [12, 61], [14.5, 60], [None, 60]], 30, 15, 1], ['stow', 'wind', 15]]]]
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: reading exactly max_age old['track', 'ok', 10]['track', 'ok', 10]Passed
boundary: calm zero reading['track', 'ok', 0]['track', 'ok', 0]Passed
boundary: wind at stow limit['stow', 'wind', 15]['stow', 'wind', 15]Passed
regression: wind aggregation['track', 'ok', 3.5]['track', 'ok', 8]Failed
regression: wind aggregation (partial repair)['track', 'ok', 8]['track', 'ok', 16.5]Failed
control 1['track', 'ok', 14.5]['stow', 'wind', 22]Failed
control 2['track', 'ok', 8]['track', 'ok', 8]Passed

SHA-256 / 836dc14b756bc3d2122877f353895e66d2984643bb510b69e41ad7814c3145b1

3 / The verified repair

Exit 0
"""Failure Map reference implementation. Python standard library only."""
import json

N = 1
observations = []
def solve(readings, max_age, stow_at, n_required):
    valid = [v for v, age in readings if v is not None and age <= max_age]
    if len(valid) < n_required:
        return ['stow', 'sensor', None]
    peak = max(valid)
    if peak >= stow_at:
        return ['stow', 'wind', peak]
    return ['track', 'ok', peak]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['boundary: reading exactly max_age old', [[[10, 60]], 60, 15, 1], ['track', 'ok', 10]],
  ['boundary: calm zero reading', [[[0, 5], [None, 5]], 60, 15, 1], ['track', 'ok', 0]],
  ['boundary: wind at stow limit', [[[15, 5], [4, 5]], 60, 15, 2], ['stow', 'wind', 15]],
  ['regression: wind aggregation', [[[3.5, 5], [0, 5], [8, 0]], 60, 15, 2], ['track', 'ok', 8]],
  ['regression: wind aggregation (partial repair)',
   [[[0, 0], [None, 61], [16.5, 60], [8, 30]], 60, 18, 2], ['track', 'ok', 16.5]],
  ['control 1', [[[14.5, 5], [8, 5], [22, 60]], 60, 15, 2], ['stow', 'wind', 22]],
  ['control 2', [[[8, 60], [8, 120]], 60, 18, 1], ['track', 'ok', 8]]],
 [['boundary: reading exactly max_age old', [[[10, 60]], 60, 15, 1], ['track', 'ok', 10]],
  ['boundary: calm zero reading', [[[0, 5], [None, 5]], 60, 15, 1], ['track', 'ok', 0]],
  ['boundary: wind at stow limit', [[[15, 5], [4, 5]], 60, 15, 2], ['stow', 'wind', 15]],
  ['regression: wind aggregation', [[[22, 120], [0, 30], [14.5, 0]], 30, 15, 2],
   ['track', 'ok', 14.5]],
  ['regression: wind aggregation (partial repair)', [[[14.5, 30], [8, 5], [8, 10]], 60, 18, 1],
   ['track', 'ok', 14.5]],
  ['control 1', [[[8, 60], [0, 10], [14.5, 10]], 60, 18, 1], ['track', 'ok', 14.5]],
  ['control 2', [[[3.5, 61], [22, 61], [22, 10], [16.5, 30]], 60, 18, 1], ['stow', 'wind', 22]]],
 [['boundary: reading exactly max_age old', [[[10, 60]], 60, 15, 1], ['track', 'ok', 10]],
  ['boundary: calm zero reading', [[[0, 5], [None, 5]], 60, 15, 1], ['track', 'ok', 0]],
  ['boundary: wind at stow limit', [[[15, 5], [4, 5]], 60, 15, 2], ['stow', 'wind', 15]],
  ['regression: wind aggregation', [[[14.5, 30], [0, 0], [None, 10], [14.5, 60]], 30, 15, 1],
   ['track', 'ok', 14.5]],
  ['regression: wind aggregation (partial repair)',
   [[[22, 30], [12, 30], [0, 0], [16.5, 120]], 60, 18, 2], ['stow', 'wind', 22]],
  ['control 1', [[[22, 61], [22, 120]], 30, 18, 2], ['stow', 'sensor', None]],
  ['control 2', [[[3.5, 30], [15, 0], [None, 61], [None, 30]], 60, 18, 2], ['track', 'ok', 15]]],
 [['boundary: reading exactly max_age old', [[[10, 60]], 60, 15, 1], ['track', 'ok', 10]],
  ['boundary: calm zero reading', [[[0, 5], [None, 5]], 60, 15, 1], ['track', 'ok', 0]],
  ['boundary: wind at stow limit', [[[15, 5], [4, 5]], 60, 15, 2], ['stow', 'wind', 15]],
  ['regression: wind aggregation', [[[8, 10], [16.5, 60], [22, 61], [15, 5]], 60, 18, 1],
   ['track', 'ok', 16.5]],
  ['regression: wind aggregation (partial repair)',
   [[[22, 10], [8, 60], [14.5, 5], [12, 0]], 60, 15, 2], ['stow', 'wind', 22]],
  ['control 1', [[[3.5, 5], [22, 30]], 30, 18, 2], ['stow', 'wind', 22]],
  ['control 2', [[[22, 0], [3.5, 60], [12, 30], [14.5, 5]], 30, 15, 1], ['stow', 'wind', 22]]],
 [['boundary: reading exactly max_age old', [[[10, 60]], 60, 15, 1], ['track', 'ok', 10]],
  ['boundary: calm zero reading', [[[0, 5], [None, 5]], 60, 15, 1], ['track', 'ok', 0]],
  ['boundary: wind at stow limit', [[[15, 5], [4, 5]], 60, 15, 2], ['stow', 'wind', 15]],
  ['regression: wind aggregation', [[[22, 0], [12, 61], [16.5, 10]], 30, 15, 2],
   ['stow', 'wind', 22]],
  ['regression: wind aggregation (partial repair)',
   [[[12, 30], [16.5, 10], [15, 61], [12, 10]], 60, 18, 2], ['track', 'ok', 16.5]],
  ['control 1', [[[3.5, 61], [0, 5], [0, 60], [14.5, 120]], 60, 18, 1], ['track', 'ok', 0]],
  ['control 2', [[[15, 10], [12, 61], [14.5, 60], [None, 60]], 30, 15, 1], ['stow', 'wind', 15]]]]
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: reading exactly max_age old['track', 'ok', 10]['track', 'ok', 10]Passed
boundary: calm zero reading['track', 'ok', 0]['track', 'ok', 0]Passed
boundary: wind at stow limit['stow', 'wind', 15]['stow', 'wind', 15]Passed
regression: wind aggregation['track', 'ok', 8]['track', 'ok', 8]Passed
regression: wind aggregation (partial repair)['track', 'ok', 16.5]['track', 'ok', 16.5]Passed
control 1['stow', 'wind', 22]['stow', 'wind', 22]Passed
control 2['track', 'ok', 8]['track', 'ok', 8]Passed

SHA-256 / 6ab02d56ed3721fc5877c16741c36854abef930c8d6fa53457f61c80d0e2abbd

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

Case digest / c359578998f653b73c48fff0ac97585215ef66b3c796ac8c69639286a21fc0de