FA-71241 / Weather index computation / Open access
Vector-mean wind and steadiness: compass wrap · case 01
North-westerly winds are reported with negative bearings.
ROOT CAUSE
The atan2 result is not wrapped into [0, 360).
VERIFIED REPAIR
Wrap the bearing modulo 360.
Unsuccessful approach: Taking the absolute value mirrors west into east.
Case contract
obs are [speed, direction_from_deg]. Components u=-s*sin(d), v=-s*cos(d); average them. Return [vector speed rounded 0.01, direction the wind blows FROM in [0,360) rounded 0.1 or None if the vector speed is below 1e-9, steadiness = vector speed / scalar mean speed rounded 0.001 (0.0 if calm)]; empty input returns None.
Why this case matters
Wind roses, dispersion inputs and runway selection need vector-mean wind direction.
1 / The failure
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(obs):
if not obs:
return None
u = v = s = 0.0
for spd, d in obs:
r = math.radians(d)
u += -spd * math.sin(r)
v += -spd * math.cos(r)
s += spd
n = len(obs)
u /= n
v /= n
vs = math.hypot(u, v)
if vs < 1e-9:
direction = None
else:
direction = round(math.degrees(math.atan2(-u, -v)), 1)
steadiness = round(vs / (s / n), 3) if s > 0 else 0.0
return [round(vs, 2), direction, steadiness]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['mast 0', [[[2, 180], [2, 170], [5, 180]]], [2.99, 177.8, 0.997]], ['mast 1', [[[2, 170], [8, 210], [5, 210], [5, 190], [12, 140]]], [5.47, 178.5, 0.855]], ['mast 2', [[[0, 50], [5, 340], [2, 340]]], [2.33, 340.0, 1.0]], ['mast 3', [[[2, 10], [5, 60], [0, 10], [12, 10], [12, 320]]], [5.1, 357.9, 0.823]], ['mast 4', [[[2, 190], [2, 180], [0, 170]]], [1.33, 185.0, 0.996]], ['mast 5', [[[8, 90], [0, 90], [5, 50], [0, 100], [0, 50], [5, 50]]], [2.82, 67.7, 0.94]], ['mast 6', [[[0, 30], [12, 350], [12, 350], [12, 30], [0, 320], [0, 0]]], [5.68, 3.1, 0.947]], ['mast 7', [[[8, 120], [8, 120], [5, 90]]], [6.83, 113.0, 0.975]]], [['mast 7', [[[8, 120], [8, 120], [5, 90]]], [6.83, 113.0, 0.975]], ['mast 8', [[[2, 300], [12, 270], [12, 260], [8, 260], [2, 330]]], [6.89, 268.8, 0.957]], ['mast 9', [[[8, 320], [5, 30], [12, 320], [0, 60], [5, 350]]], [5.4, 335.5, 0.901]], ['mast 10', [[[5, 240], [2, 240], [8, 180], [0, 210], [5, 170]]], [3.45, 197.5, 0.861]], ['mast 11', [[[2, 35], [12, 45], [8, 75], [8, 105]]], [6.76, 68.0, 0.902]], ['mast 12', [[[12, 100], [5, 100], [0, 80]]], [5.67, 100.0, 1.0]], ['mast 13', [[[8, 300], [8, 330], [12, 270], [8, 330]]], [8.08, 303.5, 0.898]], ['mast 14', [[[12, 45], [8, 55], [8, 35], [8, 45], [12, 105], [8, 35]]], [8.41, 55.3, 0.901]]], [['mast 13', [[[8, 300], [8, 330], [12, 270], [8, 330]]], [8.08, 303.5, 0.898]], ['mast 14', [[[12, 45], [8, 55], [8, 35], [8, 45], [12, 105], [8, 35]]], [8.41, 55.3, 0.901]], ['mast 15', [[[2, 60], [5, 320], [2, 0], [0, 0]]], [1.75, 347.8, 0.777]], ['mast 16', [[[8, 330], [12, 270], [12, 280]]], [9.75, 288.0, 0.914]], ['mast 17', [[[12, 30], [0, 350], [12, 0], [8, 0], [8, 60]]], [7.35, 20.6, 0.919]], ['mast 18', [[[2, 330], [2, 230], [2, 280], [8, 270]]], [3.13, 273.6, 0.895]], ['mast 19', [[[8, 5], [2, 45], [0, 105]]], [3.21, 12.7, 0.962]], ['mast 22', [[[0, 260], [8, 270], [5, 260], [12, 260]]], [6.23, 263.2, 0.997]]], [['mast 18', [[[2, 330], [2, 230], [2, 280], [8, 270]]], [3.13, 273.6, 0.895]], ['mast 21', [[[12, 170], [5, 180], [5, 240], [0, 190]]], [4.86, 186.6, 0.884]], ['mast 22', [[[0, 260], [8, 270], [5, 260], [12, 260]]], [6.23, 263.2, 0.997]], ['mast 23', [[[5, 180], [8, 240], [0, 190], [0, 140], [2, 180], [0, 240]]], [2.17, 212.2, 0.867]], ['mast 24', [[[12, 190], [12, 240], [0, 170]]], [7.25, 215.0, 0.906]], ['mast 25', [[[5, 210], [2, 190], [8, 210], [8, 210], [5, 240]]], [5.45, 213.8, 0.974]], ['mast 26', [[[8, 350], [8, 0], [5, 30], [0, 60], [8, 350]]], [5.62, 359.4, 0.969]], ['mast 28', [[[12, 310], [12, 20], [2, 0], [8, 350], [5, 0], [12, 350]]], [7.75, 349.4, 0.911]]], [['mast 3', [[[2, 10], [5, 60], [0, 10], [12, 10], [12, 320]]], [5.1, 357.9, 0.823]], ['mast 23', [[[5, 180], [8, 240], [0, 190], [0, 140], [2, 180], [0, 240]]], [2.17, 212.2, 0.867]], ['mast 28', [[[12, 310], [12, 20], [2, 0], [8, 350], [5, 0], [12, 350]]], [7.75, 349.4, 0.911]], ['mast 29', [[[0, 340], [0, 20], [5, 350]]], [1.67, 350.0, 1.0]], ['north wrap', [[[5, 350], [5, 10]]], [4.92, 360.0, 0.985]], ['opposed winds', [[[5, 90], [5, 270]]], [0.0, None, 0.0]], ['calm', [[[0, 0], [0, 180]]], [0.0, None, 0.0]], ['empty', [[]], None]]]
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 |
|---|---|---|---|
| mast 0 | [2.99, 177.8, 0.997] | [2.99, 177.8, 0.997] | Passed |
| mast 1 | [5.47, 178.5, 0.855] | [5.47, 178.5, 0.855] | Passed |
| mast 2 | [2.33, -20.0, 1.0] | [2.33, 340.0, 1.0] | Failed |
| mast 3 | [5.1, -2.1, 0.823] | [5.1, 357.9, 0.823] | Failed |
| mast 4 | [1.33, -175.0, 0.996] | [1.33, 185.0, 0.996] | Failed |
| mast 5 | [2.82, 67.7, 0.94] | [2.82, 67.7, 0.94] | Passed |
| mast 6 | [5.68, 3.1, 0.947] | [5.68, 3.1, 0.947] | Passed |
| mast 7 | [6.83, 113.0, 0.975] | [6.83, 113.0, 0.975] | Passed |
SHA-256 / b340c194d976582c4797f70750f1707507320ae2a94f37293f3bcd811d8f1ea9
2 / The unsuccessful fix
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(obs):
if not obs:
return None
u = v = s = 0.0
for spd, d in obs:
r = math.radians(d)
u += -spd * math.sin(r)
v += -spd * math.cos(r)
s += spd
n = len(obs)
u /= n
v /= n
vs = math.hypot(u, v)
if vs < 1e-9:
direction = None
else:
direction = round(abs(math.degrees(math.atan2(-u, -v))), 1)
steadiness = round(vs / (s / n), 3) if s > 0 else 0.0
return [round(vs, 2), direction, steadiness]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['mast 0', [[[2, 180], [2, 170], [5, 180]]], [2.99, 177.8, 0.997]], ['mast 1', [[[2, 170], [8, 210], [5, 210], [5, 190], [12, 140]]], [5.47, 178.5, 0.855]], ['mast 2', [[[0, 50], [5, 340], [2, 340]]], [2.33, 340.0, 1.0]], ['mast 3', [[[2, 10], [5, 60], [0, 10], [12, 10], [12, 320]]], [5.1, 357.9, 0.823]], ['mast 4', [[[2, 190], [2, 180], [0, 170]]], [1.33, 185.0, 0.996]], ['mast 5', [[[8, 90], [0, 90], [5, 50], [0, 100], [0, 50], [5, 50]]], [2.82, 67.7, 0.94]], ['mast 6', [[[0, 30], [12, 350], [12, 350], [12, 30], [0, 320], [0, 0]]], [5.68, 3.1, 0.947]], ['mast 7', [[[8, 120], [8, 120], [5, 90]]], [6.83, 113.0, 0.975]]], [['mast 7', [[[8, 120], [8, 120], [5, 90]]], [6.83, 113.0, 0.975]], ['mast 8', [[[2, 300], [12, 270], [12, 260], [8, 260], [2, 330]]], [6.89, 268.8, 0.957]], ['mast 9', [[[8, 320], [5, 30], [12, 320], [0, 60], [5, 350]]], [5.4, 335.5, 0.901]], ['mast 10', [[[5, 240], [2, 240], [8, 180], [0, 210], [5, 170]]], [3.45, 197.5, 0.861]], ['mast 11', [[[2, 35], [12, 45], [8, 75], [8, 105]]], [6.76, 68.0, 0.902]], ['mast 12', [[[12, 100], [5, 100], [0, 80]]], [5.67, 100.0, 1.0]], ['mast 13', [[[8, 300], [8, 330], [12, 270], [8, 330]]], [8.08, 303.5, 0.898]], ['mast 14', [[[12, 45], [8, 55], [8, 35], [8, 45], [12, 105], [8, 35]]], [8.41, 55.3, 0.901]]], [['mast 13', [[[8, 300], [8, 330], [12, 270], [8, 330]]], [8.08, 303.5, 0.898]], ['mast 14', [[[12, 45], [8, 55], [8, 35], [8, 45], [12, 105], [8, 35]]], [8.41, 55.3, 0.901]], ['mast 15', [[[2, 60], [5, 320], [2, 0], [0, 0]]], [1.75, 347.8, 0.777]], ['mast 16', [[[8, 330], [12, 270], [12, 280]]], [9.75, 288.0, 0.914]], ['mast 17', [[[12, 30], [0, 350], [12, 0], [8, 0], [8, 60]]], [7.35, 20.6, 0.919]], ['mast 18', [[[2, 330], [2, 230], [2, 280], [8, 270]]], [3.13, 273.6, 0.895]], ['mast 19', [[[8, 5], [2, 45], [0, 105]]], [3.21, 12.7, 0.962]], ['mast 22', [[[0, 260], [8, 270], [5, 260], [12, 260]]], [6.23, 263.2, 0.997]]], [['mast 18', [[[2, 330], [2, 230], [2, 280], [8, 270]]], [3.13, 273.6, 0.895]], ['mast 21', [[[12, 170], [5, 180], [5, 240], [0, 190]]], [4.86, 186.6, 0.884]], ['mast 22', [[[0, 260], [8, 270], [5, 260], [12, 260]]], [6.23, 263.2, 0.997]], ['mast 23', [[[5, 180], [8, 240], [0, 190], [0, 140], [2, 180], [0, 240]]], [2.17, 212.2, 0.867]], ['mast 24', [[[12, 190], [12, 240], [0, 170]]], [7.25, 215.0, 0.906]], ['mast 25', [[[5, 210], [2, 190], [8, 210], [8, 210], [5, 240]]], [5.45, 213.8, 0.974]], ['mast 26', [[[8, 350], [8, 0], [5, 30], [0, 60], [8, 350]]], [5.62, 359.4, 0.969]], ['mast 28', [[[12, 310], [12, 20], [2, 0], [8, 350], [5, 0], [12, 350]]], [7.75, 349.4, 0.911]]], [['mast 3', [[[2, 10], [5, 60], [0, 10], [12, 10], [12, 320]]], [5.1, 357.9, 0.823]], ['mast 23', [[[5, 180], [8, 240], [0, 190], [0, 140], [2, 180], [0, 240]]], [2.17, 212.2, 0.867]], ['mast 28', [[[12, 310], [12, 20], [2, 0], [8, 350], [5, 0], [12, 350]]], [7.75, 349.4, 0.911]], ['mast 29', [[[0, 340], [0, 20], [5, 350]]], [1.67, 350.0, 1.0]], ['north wrap', [[[5, 350], [5, 10]]], [4.92, 360.0, 0.985]], ['opposed winds', [[[5, 90], [5, 270]]], [0.0, None, 0.0]], ['calm', [[[0, 0], [0, 180]]], [0.0, None, 0.0]], ['empty', [[]], None]]]
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 |
|---|---|---|---|
| mast 0 | [2.99, 177.8, 0.997] | [2.99, 177.8, 0.997] | Passed |
| mast 1 | [5.47, 178.5, 0.855] | [5.47, 178.5, 0.855] | Passed |
| mast 2 | [2.33, 20.0, 1.0] | [2.33, 340.0, 1.0] | Failed |
| mast 3 | [5.1, 2.1, 0.823] | [5.1, 357.9, 0.823] | Failed |
| mast 4 | [1.33, 175.0, 0.996] | [1.33, 185.0, 0.996] | Failed |
| mast 5 | [2.82, 67.7, 0.94] | [2.82, 67.7, 0.94] | Passed |
| mast 6 | [5.68, 3.1, 0.947] | [5.68, 3.1, 0.947] | Passed |
| mast 7 | [6.83, 113.0, 0.975] | [6.83, 113.0, 0.975] | Passed |
SHA-256 / 831d08169b8a6fac4ddcdbb30caa7afbff257c44bb5ffff5da731f9449fe5963
3 / The verified repair
Exit 0"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(obs):
if not obs:
return None
u = v = s = 0.0
for spd, d in obs:
r = math.radians(d)
u += -spd * math.sin(r)
v += -spd * math.cos(r)
s += spd
n = len(obs)
u /= n
v /= n
vs = math.hypot(u, v)
if vs < 1e-9:
direction = None
else:
direction = round(math.degrees(math.atan2(-u, -v)) % 360, 1)
steadiness = round(vs / (s / n), 3) if s > 0 else 0.0
return [round(vs, 2), direction, steadiness]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['mast 0', [[[2, 180], [2, 170], [5, 180]]], [2.99, 177.8, 0.997]], ['mast 1', [[[2, 170], [8, 210], [5, 210], [5, 190], [12, 140]]], [5.47, 178.5, 0.855]], ['mast 2', [[[0, 50], [5, 340], [2, 340]]], [2.33, 340.0, 1.0]], ['mast 3', [[[2, 10], [5, 60], [0, 10], [12, 10], [12, 320]]], [5.1, 357.9, 0.823]], ['mast 4', [[[2, 190], [2, 180], [0, 170]]], [1.33, 185.0, 0.996]], ['mast 5', [[[8, 90], [0, 90], [5, 50], [0, 100], [0, 50], [5, 50]]], [2.82, 67.7, 0.94]], ['mast 6', [[[0, 30], [12, 350], [12, 350], [12, 30], [0, 320], [0, 0]]], [5.68, 3.1, 0.947]], ['mast 7', [[[8, 120], [8, 120], [5, 90]]], [6.83, 113.0, 0.975]]], [['mast 7', [[[8, 120], [8, 120], [5, 90]]], [6.83, 113.0, 0.975]], ['mast 8', [[[2, 300], [12, 270], [12, 260], [8, 260], [2, 330]]], [6.89, 268.8, 0.957]], ['mast 9', [[[8, 320], [5, 30], [12, 320], [0, 60], [5, 350]]], [5.4, 335.5, 0.901]], ['mast 10', [[[5, 240], [2, 240], [8, 180], [0, 210], [5, 170]]], [3.45, 197.5, 0.861]], ['mast 11', [[[2, 35], [12, 45], [8, 75], [8, 105]]], [6.76, 68.0, 0.902]], ['mast 12', [[[12, 100], [5, 100], [0, 80]]], [5.67, 100.0, 1.0]], ['mast 13', [[[8, 300], [8, 330], [12, 270], [8, 330]]], [8.08, 303.5, 0.898]], ['mast 14', [[[12, 45], [8, 55], [8, 35], [8, 45], [12, 105], [8, 35]]], [8.41, 55.3, 0.901]]], [['mast 13', [[[8, 300], [8, 330], [12, 270], [8, 330]]], [8.08, 303.5, 0.898]], ['mast 14', [[[12, 45], [8, 55], [8, 35], [8, 45], [12, 105], [8, 35]]], [8.41, 55.3, 0.901]], ['mast 15', [[[2, 60], [5, 320], [2, 0], [0, 0]]], [1.75, 347.8, 0.777]], ['mast 16', [[[8, 330], [12, 270], [12, 280]]], [9.75, 288.0, 0.914]], ['mast 17', [[[12, 30], [0, 350], [12, 0], [8, 0], [8, 60]]], [7.35, 20.6, 0.919]], ['mast 18', [[[2, 330], [2, 230], [2, 280], [8, 270]]], [3.13, 273.6, 0.895]], ['mast 19', [[[8, 5], [2, 45], [0, 105]]], [3.21, 12.7, 0.962]], ['mast 22', [[[0, 260], [8, 270], [5, 260], [12, 260]]], [6.23, 263.2, 0.997]]], [['mast 18', [[[2, 330], [2, 230], [2, 280], [8, 270]]], [3.13, 273.6, 0.895]], ['mast 21', [[[12, 170], [5, 180], [5, 240], [0, 190]]], [4.86, 186.6, 0.884]], ['mast 22', [[[0, 260], [8, 270], [5, 260], [12, 260]]], [6.23, 263.2, 0.997]], ['mast 23', [[[5, 180], [8, 240], [0, 190], [0, 140], [2, 180], [0, 240]]], [2.17, 212.2, 0.867]], ['mast 24', [[[12, 190], [12, 240], [0, 170]]], [7.25, 215.0, 0.906]], ['mast 25', [[[5, 210], [2, 190], [8, 210], [8, 210], [5, 240]]], [5.45, 213.8, 0.974]], ['mast 26', [[[8, 350], [8, 0], [5, 30], [0, 60], [8, 350]]], [5.62, 359.4, 0.969]], ['mast 28', [[[12, 310], [12, 20], [2, 0], [8, 350], [5, 0], [12, 350]]], [7.75, 349.4, 0.911]]], [['mast 3', [[[2, 10], [5, 60], [0, 10], [12, 10], [12, 320]]], [5.1, 357.9, 0.823]], ['mast 23', [[[5, 180], [8, 240], [0, 190], [0, 140], [2, 180], [0, 240]]], [2.17, 212.2, 0.867]], ['mast 28', [[[12, 310], [12, 20], [2, 0], [8, 350], [5, 0], [12, 350]]], [7.75, 349.4, 0.911]], ['mast 29', [[[0, 340], [0, 20], [5, 350]]], [1.67, 350.0, 1.0]], ['north wrap', [[[5, 350], [5, 10]]], [4.92, 360.0, 0.985]], ['opposed winds', [[[5, 90], [5, 270]]], [0.0, None, 0.0]], ['calm', [[[0, 0], [0, 180]]], [0.0, None, 0.0]], ['empty', [[]], None]]]
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 |
|---|---|---|---|
| mast 0 | [2.99, 177.8, 0.997] | [2.99, 177.8, 0.997] | Passed |
| mast 1 | [5.47, 178.5, 0.855] | [5.47, 178.5, 0.855] | Passed |
| mast 2 | [2.33, 340.0, 1.0] | [2.33, 340.0, 1.0] | Passed |
| mast 3 | [5.1, 357.9, 0.823] | [5.1, 357.9, 0.823] | Passed |
| mast 4 | [1.33, 185.0, 0.996] | [1.33, 185.0, 0.996] | Passed |
| mast 5 | [2.82, 67.7, 0.94] | [2.82, 67.7, 0.94] | Passed |
| mast 6 | [5.68, 3.1, 0.947] | [5.68, 3.1, 0.947] | Passed |
| mast 7 | [6.83, 113.0, 0.975] | [6.83, 113.0, 0.975] | Passed |
SHA-256 / 229408789318f96da6b6aedb76480fe9652a16d0fb992f1a2ae96c16d2a84637
Verification & scope
Stipulated deterministic teaching model of an operational weather index; coefficients and thresholds are fixed by the contract and no claim of standards conformance is made. 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:48:27.916226+00:00.
Case digest / 77f3fdd88994dec8e4e57919c33f6f422827cc717621120ac4b16d55b7af0540