FAILURE MAP
← Case archive

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.

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

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 fixtureActualExpectedOutcome
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 fixtureActualExpectedOutcome
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 fixtureActualExpectedOutcome
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