FAILURE MAP
← Case archive

FA-93646 / Solar tracker geometry / Open access

Precomputed tracker angle table: table ordering · case 01

Angles for tables delivered out of order interpolate across the wrong neighbours.

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

ROOT CAUSE

Entries are used in insertion order instead of sorted by minute.

VERIFIED REPAIR

Sort the minutes before interpolating.

Unsuccessful approach: Sorting descending breaks the range checks and neighbour search.

Case contract

table lists [minute, angle] entries in any order; for duplicate minutes the last listed entry wins. Outside [first, last] minute the tracker is flat (0.0). Inside, the angle is linearly interpolated between neighbouring entries and rounded to 2. Return the angle.

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(table, t):
    pts = {}
    for m, a in table:
        pts[m] = a
    keys = list(pts)
    if not keys or t < keys[0] or t > keys[-1]:
        return 0.0
    for m0, m1 in zip(keys, keys[1:]):
        if m0 <= t <= m1:
            a0, a1 = pts[m0], pts[m1]
            return round(a0 + (a1 - a0) * (t - m0) / (m1 - m0), 2)
    return round(float(pts[keys[0]]), 2)
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['boundary: at last entry', [[[360, -60], [720, 0], [1080, 60]], 1080], 60.0],
  ['normal: midway', [[[360, -60], [720, 0], [1080, 60]], 540], -30.0],
  ['boundary: before first entry', [[[360, -60], [720, 0]], 300], 0.0],
  ['regression: table ordering', [[[600, 60], [480, 0], [1080, 15]], 795], 41.72],
  ['regression: table ordering (partial repair)',
   [[[420, 60], [600, -30], [900, -30], [600, 30]], 525], 42.5],
  ['control 1', [[[840, 45], [1140, -60]], 300], 0.0],
  ['control 2', [[[540, -60], [600, 60], [480, 45]], 870], 0.0]],
 [['boundary: at last entry', [[[360, -60], [720, 0], [1080, 60]], 1080], 60.0],
  ['normal: midway', [[[360, -60], [720, 0], [1080, 60]], 540], -30.0],
  ['boundary: before first entry', [[[360, -60], [720, 0]], 300], 0.0],
  ['regression: table ordering', [[[900, 60], [420, 60], [1080, -60]], 1065], -50.0],
  ['regression: table ordering (partial repair)',
   [[[480, -30], [1020, 15], [540, -45], [1080, 45], [1080, 30], [420, 45]], 690], -26.25],
  ['control 1', [[[540, 30], [1020, 45]], 945], 42.66],
  ['control 2', [[[1140, -60], [360, -30], [900, 30], [900, 15], [480, 0]], 855], 13.39]],
 [['boundary: at last entry', [[[360, -60], [720, 0], [1080, 60]], 1080], 60.0],
  ['normal: midway', [[[360, -60], [720, 0], [1080, 60]], 540], -30.0],
  ['boundary: before first entry', [[[360, -60], [720, 0]], 300], 0.0],
  ['regression: table ordering', [[[360, -30], [1140, 30], [840, -15]], 1140], 30.0],
  ['regression: table ordering (partial repair)',
   [[[1020, -60], [660, -30], [600, -45], [480, 45], [600, -15]], 525], 22.5],
  ['control 1', [[[1080, -45], [540, 45]], 1110], 0.0],
  ['control 2', [[[600, 45], [1020, 45]], 585], 0.0]],
 [['boundary: at last entry', [[[360, -60], [720, 0], [1080, 60]], 1080], 60.0],
  ['normal: midway', [[[360, -60], [720, 0], [1080, 60]], 540], -30.0],
  ['boundary: before first entry', [[[360, -60], [720, 0]], 300], 0.0],
  ['regression: table ordering', [[[1080, 60], [480, 30], [660, 0], [960, -15]], 750], -4.5],
  ['regression: table ordering (partial repair)', [[[780, 45], [1140, 30], [960, -30]], 1020],
   -10.0],
  ['control 1', [[[600, -15], [1080, 30], [1080, 0]], 420], 0.0],
  ['control 2', [[[1140, 0], [480, -15]], 1110], -0.68]],
 [['boundary: at last entry', [[[360, -60], [720, 0], [1080, 60]], 1080], 60.0],
  ['normal: midway', [[[360, -60], [720, 0], [1080, 60]], 540], -30.0],
  ['boundary: before first entry', [[[360, -60], [720, 0]], 300], 0.0],
  ['regression: table ordering', [[[840, -60], [420, -45]], 765], -57.32],
  ['regression: table ordering (partial repair)',
   [[[840, -60], [780, -45], [960, 45], [420, -30]], 795], -48.75],
  ['control 1', [[[1080, 0]], 345], 0.0], ['control 2', [[[1080, 45]], 600], 0.0]]]
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: at last entry60.060.0Passed
normal: midway-30.0-30.0Passed
boundary: before first entry0.00.0Passed
regression: table ordering7.8841.72Failed
regression: table ordering (partial repair)42.542.5Passed
control 10.00.0Passed
control 20.00.0Passed

SHA-256 / 0acbace56909891703c5ac2099def74aef674f9b9e06c8eed7e03cdbf7b5ecae

2 / The unsuccessful fix

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

N = 1
observations = []
def solve(table, t):
    pts = {}
    for m, a in table:
        pts[m] = a
    keys = sorted(pts, reverse=True)
    if not keys or t < keys[0] or t > keys[-1]:
        return 0.0
    for m0, m1 in zip(keys, keys[1:]):
        if m0 <= t <= m1:
            a0, a1 = pts[m0], pts[m1]
            return round(a0 + (a1 - a0) * (t - m0) / (m1 - m0), 2)
    return round(float(pts[keys[0]]), 2)
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['boundary: at last entry', [[[360, -60], [720, 0], [1080, 60]], 1080], 60.0],
  ['normal: midway', [[[360, -60], [720, 0], [1080, 60]], 540], -30.0],
  ['boundary: before first entry', [[[360, -60], [720, 0]], 300], 0.0],
  ['regression: table ordering', [[[600, 60], [480, 0], [1080, 15]], 795], 41.72],
  ['regression: table ordering (partial repair)',
   [[[420, 60], [600, -30], [900, -30], [600, 30]], 525], 42.5],
  ['control 1', [[[840, 45], [1140, -60]], 300], 0.0],
  ['control 2', [[[540, -60], [600, 60], [480, 45]], 870], 0.0]],
 [['boundary: at last entry', [[[360, -60], [720, 0], [1080, 60]], 1080], 60.0],
  ['normal: midway', [[[360, -60], [720, 0], [1080, 60]], 540], -30.0],
  ['boundary: before first entry', [[[360, -60], [720, 0]], 300], 0.0],
  ['regression: table ordering', [[[900, 60], [420, 60], [1080, -60]], 1065], -50.0],
  ['regression: table ordering (partial repair)',
   [[[480, -30], [1020, 15], [540, -45], [1080, 45], [1080, 30], [420, 45]], 690], -26.25],
  ['control 1', [[[540, 30], [1020, 45]], 945], 42.66],
  ['control 2', [[[1140, -60], [360, -30], [900, 30], [900, 15], [480, 0]], 855], 13.39]],
 [['boundary: at last entry', [[[360, -60], [720, 0], [1080, 60]], 1080], 60.0],
  ['normal: midway', [[[360, -60], [720, 0], [1080, 60]], 540], -30.0],
  ['boundary: before first entry', [[[360, -60], [720, 0]], 300], 0.0],
  ['regression: table ordering', [[[360, -30], [1140, 30], [840, -15]], 1140], 30.0],
  ['regression: table ordering (partial repair)',
   [[[1020, -60], [660, -30], [600, -45], [480, 45], [600, -15]], 525], 22.5],
  ['control 1', [[[1080, -45], [540, 45]], 1110], 0.0],
  ['control 2', [[[600, 45], [1020, 45]], 585], 0.0]],
 [['boundary: at last entry', [[[360, -60], [720, 0], [1080, 60]], 1080], 60.0],
  ['normal: midway', [[[360, -60], [720, 0], [1080, 60]], 540], -30.0],
  ['boundary: before first entry', [[[360, -60], [720, 0]], 300], 0.0],
  ['regression: table ordering', [[[1080, 60], [480, 30], [660, 0], [960, -15]], 750], -4.5],
  ['regression: table ordering (partial repair)', [[[780, 45], [1140, 30], [960, -30]], 1020],
   -10.0],
  ['control 1', [[[600, -15], [1080, 30], [1080, 0]], 420], 0.0],
  ['control 2', [[[1140, 0], [480, -15]], 1110], -0.68]],
 [['boundary: at last entry', [[[360, -60], [720, 0], [1080, 60]], 1080], 60.0],
  ['normal: midway', [[[360, -60], [720, 0], [1080, 60]], 540], -30.0],
  ['boundary: before first entry', [[[360, -60], [720, 0]], 300], 0.0],
  ['regression: table ordering', [[[840, -60], [420, -45]], 765], -57.32],
  ['regression: table ordering (partial repair)',
   [[[840, -60], [780, -45], [960, 45], [420, -30]], 795], -48.75],
  ['control 1', [[[1080, 0]], 345], 0.0], ['control 2', [[[1080, 45]], 600], 0.0]]]
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: at last entry0.060.0Failed
normal: midway0.0-30.0Failed
boundary: before first entry0.00.0Passed
regression: table ordering0.041.72Failed
regression: table ordering (partial repair)0.042.5Failed
control 10.00.0Passed
control 20.00.0Passed

SHA-256 / 8225210241b561aee09ce69cd8ebb1b8bfe35c0f8628d4c81a34a0154debd46c

3 / The verified repair

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

N = 1
observations = []
def solve(table, t):
    pts = {}
    for m, a in table:
        pts[m] = a
    keys = sorted(pts)
    if not keys or t < keys[0] or t > keys[-1]:
        return 0.0
    for m0, m1 in zip(keys, keys[1:]):
        if m0 <= t <= m1:
            a0, a1 = pts[m0], pts[m1]
            return round(a0 + (a1 - a0) * (t - m0) / (m1 - m0), 2)
    return round(float(pts[keys[0]]), 2)
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['boundary: at last entry', [[[360, -60], [720, 0], [1080, 60]], 1080], 60.0],
  ['normal: midway', [[[360, -60], [720, 0], [1080, 60]], 540], -30.0],
  ['boundary: before first entry', [[[360, -60], [720, 0]], 300], 0.0],
  ['regression: table ordering', [[[600, 60], [480, 0], [1080, 15]], 795], 41.72],
  ['regression: table ordering (partial repair)',
   [[[420, 60], [600, -30], [900, -30], [600, 30]], 525], 42.5],
  ['control 1', [[[840, 45], [1140, -60]], 300], 0.0],
  ['control 2', [[[540, -60], [600, 60], [480, 45]], 870], 0.0]],
 [['boundary: at last entry', [[[360, -60], [720, 0], [1080, 60]], 1080], 60.0],
  ['normal: midway', [[[360, -60], [720, 0], [1080, 60]], 540], -30.0],
  ['boundary: before first entry', [[[360, -60], [720, 0]], 300], 0.0],
  ['regression: table ordering', [[[900, 60], [420, 60], [1080, -60]], 1065], -50.0],
  ['regression: table ordering (partial repair)',
   [[[480, -30], [1020, 15], [540, -45], [1080, 45], [1080, 30], [420, 45]], 690], -26.25],
  ['control 1', [[[540, 30], [1020, 45]], 945], 42.66],
  ['control 2', [[[1140, -60], [360, -30], [900, 30], [900, 15], [480, 0]], 855], 13.39]],
 [['boundary: at last entry', [[[360, -60], [720, 0], [1080, 60]], 1080], 60.0],
  ['normal: midway', [[[360, -60], [720, 0], [1080, 60]], 540], -30.0],
  ['boundary: before first entry', [[[360, -60], [720, 0]], 300], 0.0],
  ['regression: table ordering', [[[360, -30], [1140, 30], [840, -15]], 1140], 30.0],
  ['regression: table ordering (partial repair)',
   [[[1020, -60], [660, -30], [600, -45], [480, 45], [600, -15]], 525], 22.5],
  ['control 1', [[[1080, -45], [540, 45]], 1110], 0.0],
  ['control 2', [[[600, 45], [1020, 45]], 585], 0.0]],
 [['boundary: at last entry', [[[360, -60], [720, 0], [1080, 60]], 1080], 60.0],
  ['normal: midway', [[[360, -60], [720, 0], [1080, 60]], 540], -30.0],
  ['boundary: before first entry', [[[360, -60], [720, 0]], 300], 0.0],
  ['regression: table ordering', [[[1080, 60], [480, 30], [660, 0], [960, -15]], 750], -4.5],
  ['regression: table ordering (partial repair)', [[[780, 45], [1140, 30], [960, -30]], 1020],
   -10.0],
  ['control 1', [[[600, -15], [1080, 30], [1080, 0]], 420], 0.0],
  ['control 2', [[[1140, 0], [480, -15]], 1110], -0.68]],
 [['boundary: at last entry', [[[360, -60], [720, 0], [1080, 60]], 1080], 60.0],
  ['normal: midway', [[[360, -60], [720, 0], [1080, 60]], 540], -30.0],
  ['boundary: before first entry', [[[360, -60], [720, 0]], 300], 0.0],
  ['regression: table ordering', [[[840, -60], [420, -45]], 765], -57.32],
  ['regression: table ordering (partial repair)',
   [[[840, -60], [780, -45], [960, 45], [420, -30]], 795], -48.75],
  ['control 1', [[[1080, 0]], 345], 0.0], ['control 2', [[[1080, 45]], 600], 0.0]]]
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: at last entry60.060.0Passed
normal: midway-30.0-30.0Passed
boundary: before first entry0.00.0Passed
regression: table ordering41.7241.72Passed
regression: table ordering (partial repair)42.542.5Passed
control 10.00.0Passed
control 20.00.0Passed

SHA-256 / 4258b551705a56e1595a79c68611f7d58bb5f34aef1a7aef3550c6b676c2f4ba

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

Case digest / e666d16e2d2e26a4da38cb1d48bb204e9707a04b7efa7b2f143800e73c4c4e05