FA-71246 / Weather index computation / Open access
Vector-mean wind and steadiness: steadiness denominator · case 01
Steadiness falls as more samples are averaged even when the wind is constant.
ROOT CAUSE
The vector mean is divided by the total rather than the mean scalar speed.
VERIFIED REPAIR
Divide by the scalar mean speed s/n.
Unsuccessful approach: Dividing by the peak speed penalises gusty but steady winds.
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)) % 360, 1)
steadiness = round(vs / s, 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 3', [[[2, 10], [5, 60], [0, 10], [12, 10], [12, 320]]], [5.1, 357.9, 0.823]], ['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 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 6', [[[0, 30], [12, 350], [12, 350], [12, 30], [0, 320], [0, 0]]], [5.68, 3.1, 0.947]], ['mast 11', [[[2, 35], [12, 45], [8, 75], [8, 105]]], [6.76, 68.0, 0.902]], ['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 9', [[[8, 320], [5, 30], [12, 320], [0, 60], [5, 350]]], [5.4, 335.5, 0.901]], ['mast 16', [[[8, 330], [12, 270], [12, 280]]], [9.75, 288.0, 0.914]], ['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 12', [[[12, 100], [5, 100], [0, 80]]], [5.67, 100.0, 1.0]], ['mast 21', [[[12, 170], [5, 180], [5, 240], [0, 190]]], [4.86, 186.6, 0.884]], ['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.332] | [2.99, 177.8, 0.997] | Failed |
| mast 1 | [5.47, 178.5, 0.171] | [5.47, 178.5, 0.855] | Failed |
| mast 2 | [2.33, 340.0, 0.333] | [2.33, 340.0, 1.0] | Failed |
| mast 3 | [5.1, 357.9, 0.165] | [5.1, 357.9, 0.823] | Failed |
| mast 4 | [1.33, 185.0, 0.332] | [1.33, 185.0, 0.996] | Failed |
| mast 5 | [2.82, 67.7, 0.157] | [2.82, 67.7, 0.94] | Failed |
| mast 6 | [5.68, 3.1, 0.158] | [5.68, 3.1, 0.947] | Failed |
| mast 7 | [6.83, 113.0, 0.325] | [6.83, 113.0, 0.975] | Failed |
SHA-256 / 88006a7f22ed337efc7d0a047af0f5c0396c81c2a6d016022b156275e3e897ed
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(math.degrees(math.atan2(-u, -v)) % 360, 1)
steadiness = round(vs / max(o[0] for o in obs), 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 3', [[[2, 10], [5, 60], [0, 10], [12, 10], [12, 320]]], [5.1, 357.9, 0.823]], ['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 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 6', [[[0, 30], [12, 350], [12, 350], [12, 30], [0, 320], [0, 0]]], [5.68, 3.1, 0.947]], ['mast 11', [[[2, 35], [12, 45], [8, 75], [8, 105]]], [6.76, 68.0, 0.902]], ['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 9', [[[8, 320], [5, 30], [12, 320], [0, 60], [5, 350]]], [5.4, 335.5, 0.901]], ['mast 16', [[[8, 330], [12, 270], [12, 280]]], [9.75, 288.0, 0.914]], ['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 12', [[[12, 100], [5, 100], [0, 80]]], [5.67, 100.0, 1.0]], ['mast 21', [[[12, 170], [5, 180], [5, 240], [0, 190]]], [4.86, 186.6, 0.884]], ['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.598] | [2.99, 177.8, 0.997] | Failed |
| mast 1 | [5.47, 178.5, 0.456] | [5.47, 178.5, 0.855] | Failed |
| mast 2 | [2.33, 340.0, 0.467] | [2.33, 340.0, 1.0] | Failed |
| mast 3 | [5.1, 357.9, 0.425] | [5.1, 357.9, 0.823] | Failed |
| mast 4 | [1.33, 185.0, 0.664] | [1.33, 185.0, 0.996] | Failed |
| mast 5 | [2.82, 67.7, 0.353] | [2.82, 67.7, 0.94] | Failed |
| mast 6 | [5.68, 3.1, 0.473] | [5.68, 3.1, 0.947] | Failed |
| mast 7 | [6.83, 113.0, 0.853] | [6.83, 113.0, 0.975] | Failed |
SHA-256 / de95f06452bcf79d7519de3cc27610edf4c15e16bcb9d0d1065ab6f196e1d9b4
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 3', [[[2, 10], [5, 60], [0, 10], [12, 10], [12, 320]]], [5.1, 357.9, 0.823]], ['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 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 6', [[[0, 30], [12, 350], [12, 350], [12, 30], [0, 320], [0, 0]]], [5.68, 3.1, 0.947]], ['mast 11', [[[2, 35], [12, 45], [8, 75], [8, 105]]], [6.76, 68.0, 0.902]], ['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 9', [[[8, 320], [5, 30], [12, 320], [0, 60], [5, 350]]], [5.4, 335.5, 0.901]], ['mast 16', [[[8, 330], [12, 270], [12, 280]]], [9.75, 288.0, 0.914]], ['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 12', [[[12, 100], [5, 100], [0, 80]]], [5.67, 100.0, 1.0]], ['mast 21', [[[12, 170], [5, 180], [5, 240], [0, 190]]], [4.86, 186.6, 0.884]], ['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 / 9f1a939e16f5bdf7922fb9ab64b44cadbb1bee589cb8ed548ad5765e5a4b51b8
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.938123+00:00.
Case digest / a8e1feff1100eed46cbb7226c17137a01ea5e80f7b7b7a9be0912ca52983b2e7