FAILURE MAP
← Case archive

FA-71131 / Weather index computation / Open access

Beaufort force and marine warning: observation rounding · case 01

A 33.4 kt wind (17.18 m/s, reported 17.2) is classified force 7.

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

ROOT CAUSE

The unrounded speed is compared with the tenth-of-m/s bounds.

VERIFIED REPAIR

Round the converted speed to 0.1 m/s before classification.

Unsuccessful approach: Truncating to 0.1 m/s still under-reads speeds just below a bound.

Case contract

Convert knots to m/s (x0.514444) and round to 0.1 m/s. Beaufort force n is the first n with speed < bounds[n], bounds=[0.5,1.6,3.4,5.5,8.0,10.8,13.9,17.2,20.8,24.5,28.5,32.7], else 12. If the gust force is at least sustained force + 2 (and force < 12) upgrade by one. Warning: <6 none, <8 small craft, <10 gale, <12 storm, 12 hurricane force. Return [force, warning].

Why this case matters

Coastal marine forecasts issue small-craft, gale and storm warnings from Beaufort force.

1 / The failure

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

N = 1
observations = []
def solve(sustained_kt, gust_kt):
    bounds = [0.5, 1.6, 3.4, 5.5, 8.0, 10.8, 13.9, 17.2, 20.8, 24.5, 28.5, 32.7]
    def force(kt):
        ms = kt * 0.514444
        for n, upper in enumerate(bounds):
            if ms < upper:
                return n
        return 12
    b = force(sustained_kt)
    g = force(gust_kt)
    if g >= b + 2 and b < 12:
        b += 1
    if b < 6:
        warn = "none"
    elif b < 8:
        warn = "small craft"
    elif b < 10:
        warn = "gale"
    elif b < 12:
        warn = "storm"
    else:
        warn = "hurricane force"
    return [b, warn]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['sustained 0 kt gust +0', [0, 0], [0, 'none']], ['sustained 0 kt gust +2', [0, 2], [0, 'none']], ['sustained 0 kt gust +8', [0, 8], [1, 'none']], ['sustained 0 kt gust +16', [0, 16], [1, 'none']], ['sustained 0.9 kt gust +0', [0.9, 0.9], [1, 'none']], ['sustained 0.9 kt gust +2', [0.9, 2.9], [1, 'none']], ['sustained 0.9 kt gust +8', [0.9, 8.9], [2, 'none']], ['sustained 0.9 kt gust +16', [0.9, 16.9], [2, 'none']]], [['sustained 0.9 kt gust +16', [0.9, 16.9], [2, 'none']], ['sustained 1 kt gust +0', [1, 1], [1, 'none']], ['sustained 1 kt gust +2', [1, 3], [1, 'none']], ['sustained 1 kt gust +8', [1, 9], [2, 'none']], ['sustained 1 kt gust +16', [1, 17], [2, 'none']], ['sustained 3 kt gust +0', [3, 3], [1, 'none']], ['sustained 3 kt gust +2', [3, 5], [1, 'none']], ['sustained 6.6 kt gust +16', [6.6, 22.6], [4, 'none']]], [['sustained 3 kt gust +8', [3, 11], [2, 'none']], ['sustained 3 kt gust +16', [3, 19], [2, 'none']], ['sustained 6.6 kt gust +0', [6.6, 6.6], [3, 'none']], ['sustained 6.6 kt gust +2', [6.6, 8.6], [3, 'none']], ['sustained 6.6 kt gust +8', [6.6, 14.6], [3, 'none']], ['sustained 6.6 kt gust +16', [6.6, 22.6], [4, 'none']], ['sustained 10.7 kt gust +0', [10.7, 10.7], [4, 'none']], ['sustained 33.4 kt gust +2', [33.4, 35.4], [8, 'gale']]], [['sustained 10.7 kt gust +2', [10.7, 12.7], [4, 'none']], ['sustained 10.7 kt gust +8', [10.7, 18.7], [4, 'none']], ['sustained 10.7 kt gust +16', [10.7, 26.7], [5, 'none']], ['sustained 16 kt gust +0', [16, 16], [5, 'none']], ['sustained 16 kt gust +2', [16, 18], [5, 'none']], ['sustained 16 kt gust +8', [16, 24], [5, 'none']], ['sustained 27 kt gust +16', [27, 43], [8, 'gale']], ['sustained 47.6 kt gust +0', [47.6, 47.6], [10, 'storm']]], [['sustained 0.9 kt gust +8', [0.9, 8.9], [2, 'none']], ['sustained 21 kt gust +0', [21, 21], [6, 'small craft']], ['sustained 21 kt gust +2', [21, 23], [6, 'small craft']], ['sustained 21 kt gust +8', [21, 29], [6, 'small craft']], ['sustained 21 kt gust +16', [21, 37], [7, 'small craft']], ['sustained 27 kt gust +0', [27, 27], [7, 'small craft']], ['sustained 27 kt gust +2', [27, 29], [7, 'small craft']], ['sustained 33.4 kt gust +16', [33.4, 49.4], [9, 'gale']]]]
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
sustained 0 kt gust +0[0, 'none'][0, 'none']Passed
sustained 0 kt gust +2[0, 'none'][0, 'none']Passed
sustained 0 kt gust +8[1, 'none'][1, 'none']Passed
sustained 0 kt gust +16[1, 'none'][1, 'none']Passed
sustained 0.9 kt gust +0[0, 'none'][1, 'none']Failed
sustained 0.9 kt gust +2[0, 'none'][1, 'none']Failed
sustained 0.9 kt gust +8[1, 'none'][2, 'none']Failed
sustained 0.9 kt gust +16[1, 'none'][2, 'none']Failed

SHA-256 / 1f58fd7c18b259e07bfed9342504c5b4290b7fe0d4003a0f82f2128a13ae77db

2 / The unsuccessful fix

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

N = 1
observations = []
def solve(sustained_kt, gust_kt):
    bounds = [0.5, 1.6, 3.4, 5.5, 8.0, 10.8, 13.9, 17.2, 20.8, 24.5, 28.5, 32.7]
    def force(kt):
        ms = int(kt * 0.514444 * 10) / 10
        for n, upper in enumerate(bounds):
            if ms < upper:
                return n
        return 12
    b = force(sustained_kt)
    g = force(gust_kt)
    if g >= b + 2 and b < 12:
        b += 1
    if b < 6:
        warn = "none"
    elif b < 8:
        warn = "small craft"
    elif b < 10:
        warn = "gale"
    elif b < 12:
        warn = "storm"
    else:
        warn = "hurricane force"
    return [b, warn]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['sustained 0 kt gust +0', [0, 0], [0, 'none']], ['sustained 0 kt gust +2', [0, 2], [0, 'none']], ['sustained 0 kt gust +8', [0, 8], [1, 'none']], ['sustained 0 kt gust +16', [0, 16], [1, 'none']], ['sustained 0.9 kt gust +0', [0.9, 0.9], [1, 'none']], ['sustained 0.9 kt gust +2', [0.9, 2.9], [1, 'none']], ['sustained 0.9 kt gust +8', [0.9, 8.9], [2, 'none']], ['sustained 0.9 kt gust +16', [0.9, 16.9], [2, 'none']]], [['sustained 0.9 kt gust +16', [0.9, 16.9], [2, 'none']], ['sustained 1 kt gust +0', [1, 1], [1, 'none']], ['sustained 1 kt gust +2', [1, 3], [1, 'none']], ['sustained 1 kt gust +8', [1, 9], [2, 'none']], ['sustained 1 kt gust +16', [1, 17], [2, 'none']], ['sustained 3 kt gust +0', [3, 3], [1, 'none']], ['sustained 3 kt gust +2', [3, 5], [1, 'none']], ['sustained 6.6 kt gust +16', [6.6, 22.6], [4, 'none']]], [['sustained 3 kt gust +8', [3, 11], [2, 'none']], ['sustained 3 kt gust +16', [3, 19], [2, 'none']], ['sustained 6.6 kt gust +0', [6.6, 6.6], [3, 'none']], ['sustained 6.6 kt gust +2', [6.6, 8.6], [3, 'none']], ['sustained 6.6 kt gust +8', [6.6, 14.6], [3, 'none']], ['sustained 6.6 kt gust +16', [6.6, 22.6], [4, 'none']], ['sustained 10.7 kt gust +0', [10.7, 10.7], [4, 'none']], ['sustained 33.4 kt gust +2', [33.4, 35.4], [8, 'gale']]], [['sustained 10.7 kt gust +2', [10.7, 12.7], [4, 'none']], ['sustained 10.7 kt gust +8', [10.7, 18.7], [4, 'none']], ['sustained 10.7 kt gust +16', [10.7, 26.7], [5, 'none']], ['sustained 16 kt gust +0', [16, 16], [5, 'none']], ['sustained 16 kt gust +2', [16, 18], [5, 'none']], ['sustained 16 kt gust +8', [16, 24], [5, 'none']], ['sustained 27 kt gust +16', [27, 43], [8, 'gale']], ['sustained 47.6 kt gust +0', [47.6, 47.6], [10, 'storm']]], [['sustained 0.9 kt gust +8', [0.9, 8.9], [2, 'none']], ['sustained 21 kt gust +0', [21, 21], [6, 'small craft']], ['sustained 21 kt gust +2', [21, 23], [6, 'small craft']], ['sustained 21 kt gust +8', [21, 29], [6, 'small craft']], ['sustained 21 kt gust +16', [21, 37], [7, 'small craft']], ['sustained 27 kt gust +0', [27, 27], [7, 'small craft']], ['sustained 27 kt gust +2', [27, 29], [7, 'small craft']], ['sustained 33.4 kt gust +16', [33.4, 49.4], [9, 'gale']]]]
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
sustained 0 kt gust +0[0, 'none'][0, 'none']Passed
sustained 0 kt gust +2[0, 'none'][0, 'none']Passed
sustained 0 kt gust +8[1, 'none'][1, 'none']Passed
sustained 0 kt gust +16[1, 'none'][1, 'none']Passed
sustained 0.9 kt gust +0[0, 'none'][1, 'none']Failed
sustained 0.9 kt gust +2[0, 'none'][1, 'none']Failed
sustained 0.9 kt gust +8[1, 'none'][2, 'none']Failed
sustained 0.9 kt gust +16[1, 'none'][2, 'none']Failed

SHA-256 / 73e6a60f67f355c84645ea44d55bb074ae3d485c221b6ef30bf1b09819a24de6

3 / The verified repair

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

N = 1
observations = []
def solve(sustained_kt, gust_kt):
    bounds = [0.5, 1.6, 3.4, 5.5, 8.0, 10.8, 13.9, 17.2, 20.8, 24.5, 28.5, 32.7]
    def force(kt):
        ms = round(kt * 0.514444, 1)
        for n, upper in enumerate(bounds):
            if ms < upper:
                return n
        return 12
    b = force(sustained_kt)
    g = force(gust_kt)
    if g >= b + 2 and b < 12:
        b += 1
    if b < 6:
        warn = "none"
    elif b < 8:
        warn = "small craft"
    elif b < 10:
        warn = "gale"
    elif b < 12:
        warn = "storm"
    else:
        warn = "hurricane force"
    return [b, warn]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['sustained 0 kt gust +0', [0, 0], [0, 'none']], ['sustained 0 kt gust +2', [0, 2], [0, 'none']], ['sustained 0 kt gust +8', [0, 8], [1, 'none']], ['sustained 0 kt gust +16', [0, 16], [1, 'none']], ['sustained 0.9 kt gust +0', [0.9, 0.9], [1, 'none']], ['sustained 0.9 kt gust +2', [0.9, 2.9], [1, 'none']], ['sustained 0.9 kt gust +8', [0.9, 8.9], [2, 'none']], ['sustained 0.9 kt gust +16', [0.9, 16.9], [2, 'none']]], [['sustained 0.9 kt gust +16', [0.9, 16.9], [2, 'none']], ['sustained 1 kt gust +0', [1, 1], [1, 'none']], ['sustained 1 kt gust +2', [1, 3], [1, 'none']], ['sustained 1 kt gust +8', [1, 9], [2, 'none']], ['sustained 1 kt gust +16', [1, 17], [2, 'none']], ['sustained 3 kt gust +0', [3, 3], [1, 'none']], ['sustained 3 kt gust +2', [3, 5], [1, 'none']], ['sustained 6.6 kt gust +16', [6.6, 22.6], [4, 'none']]], [['sustained 3 kt gust +8', [3, 11], [2, 'none']], ['sustained 3 kt gust +16', [3, 19], [2, 'none']], ['sustained 6.6 kt gust +0', [6.6, 6.6], [3, 'none']], ['sustained 6.6 kt gust +2', [6.6, 8.6], [3, 'none']], ['sustained 6.6 kt gust +8', [6.6, 14.6], [3, 'none']], ['sustained 6.6 kt gust +16', [6.6, 22.6], [4, 'none']], ['sustained 10.7 kt gust +0', [10.7, 10.7], [4, 'none']], ['sustained 33.4 kt gust +2', [33.4, 35.4], [8, 'gale']]], [['sustained 10.7 kt gust +2', [10.7, 12.7], [4, 'none']], ['sustained 10.7 kt gust +8', [10.7, 18.7], [4, 'none']], ['sustained 10.7 kt gust +16', [10.7, 26.7], [5, 'none']], ['sustained 16 kt gust +0', [16, 16], [5, 'none']], ['sustained 16 kt gust +2', [16, 18], [5, 'none']], ['sustained 16 kt gust +8', [16, 24], [5, 'none']], ['sustained 27 kt gust +16', [27, 43], [8, 'gale']], ['sustained 47.6 kt gust +0', [47.6, 47.6], [10, 'storm']]], [['sustained 0.9 kt gust +8', [0.9, 8.9], [2, 'none']], ['sustained 21 kt gust +0', [21, 21], [6, 'small craft']], ['sustained 21 kt gust +2', [21, 23], [6, 'small craft']], ['sustained 21 kt gust +8', [21, 29], [6, 'small craft']], ['sustained 21 kt gust +16', [21, 37], [7, 'small craft']], ['sustained 27 kt gust +0', [27, 27], [7, 'small craft']], ['sustained 27 kt gust +2', [27, 29], [7, 'small craft']], ['sustained 33.4 kt gust +16', [33.4, 49.4], [9, 'gale']]]]
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
sustained 0 kt gust +0[0, 'none'][0, 'none']Passed
sustained 0 kt gust +2[0, 'none'][0, 'none']Passed
sustained 0 kt gust +8[1, 'none'][1, 'none']Passed
sustained 0 kt gust +16[1, 'none'][1, 'none']Passed
sustained 0.9 kt gust +0[1, 'none'][1, 'none']Passed
sustained 0.9 kt gust +2[1, 'none'][1, 'none']Passed
sustained 0.9 kt gust +8[2, 'none'][2, 'none']Passed
sustained 0.9 kt gust +16[2, 'none'][2, 'none']Passed

SHA-256 / 5aa4f8e7f60e960f7b5309584374e1690e3d0edd51a6b750ad5c9848c15289d4

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

Case digest / bfca0cb5204cf481e2307656b9fd23582bfd4c85f6b42403fa5665a17dd6c08b