FAILURE MAP
← Case archive

FA-69016 / Tide and marine navigation tables / Open access

Course to steer across a stream: Heading correction steers with the stream · case 01

The vessel is set twice as far off track.

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

ROOT CAUSE

The drift angle is computed as asin(c/boat), turning downstream instead of into the stream.

VERIFIED REPAIR

Steer up-stream with asin(-c/boat).

Unsuccessful approach: Using -atan2(c,boat) has the right sign but underestimates the angle.

Case contract

Input [track, boat_speed, set, drift] with degrees true (set is the direction the stream flows toward) and knots. Cross-track stream c=drift*sin(set-track); heading = track+asin(-c/boat); SOG=boat*cos(asin(-c/boat))+drift*cos(set-track). Return None when |c|>boat or SOG<=0, else [heading mod 360 rounded to 0.1, SOG rounded to 0.01].

Why this case matters

Tide tables and passage plans turn published predictions into go/no-go decisions about depth, clearance and timing.

1 / The failure

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(x):
    track,bs,setd,drift=x
    rel=math.radians(setd-track)
    cross=drift*math.sin(rel)
    if abs(cross)>bs: return None
    a=math.asin(cross/bs)
    sog=bs*math.cos(a)+drift*math.cos(rel)
    if sog<=0: return None
    return [round(math.degrees(math.radians(track)+a)%360,1),round(sog,2)]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('course to steer across a stream [90, 6, 180, 1.5]', [90, 6, 180, 1.5], [75.5, 5.81]), ('course to steer across a stream [90, 6, 0, 1.5]', [90, 6, 0, 1.5], [104.5, 5.81]), ('course to steer across a stream [10, 5, 100, 2]', [10, 5, 100, 2], [346.4, 4.58]), ('course to steer across a stream [350, 5, 80, 2]', [350, 5, 80, 2], [326.4, 4.58]), ('course to steer across a stream [200, 4, 200, 3]', [200, 4, 200, 3], [200.0, 7.0]), ('course to steer across a stream [200, 4, 20, 3]', [200, 4, 20, 3], [200.0, 1.0]), ('course to steer across a stream [45, 5, 135, 6]', [45, 5, 135, 6], None)], [('course to steer across a stream [90, 6, 0, 1.5]', [90, 6, 0, 1.5], [104.5, 5.81]), ('course to steer across a stream [350, 5, 80, 2]', [350, 5, 80, 2], [326.4, 4.58]), ('course to steer across a stream [200, 4, 200, 3]', [200, 4, 200, 3], [200.0, 7.0]), ('course to steer across a stream [200, 4, 20, 3]', [200, 4, 20, 3], [200.0, 1.0]), ('course to steer across a stream [45, 5, 135, 6]', [45, 5, 135, 6], None), ('course to steer across a stream [45, 5, 60, 7]', [45, 5, 60, 7], [23.8, 11.42]), ('course to steer across a stream [120, 5, 300, 5.5]', [120, 5, 300, 5.5], None)], [('course to steer across a stream [10, 5, 100, 2]', [10, 5, 100, 2], [346.4, 4.58]), ('course to steer across a stream [45, 5, 135, 6]', [45, 5, 135, 6], None), ('course to steer across a stream [45, 5, 60, 7]', [45, 5, 60, 7], [23.8, 11.42]), ('course to steer across a stream [120, 5, 300, 5.5]', [120, 5, 300, 5.5], None), ('course to steer across a stream [0, 5, 90, 0]', [0, 5, 90, 0], [0.0, 5.0]), ('course to steer across a stream [270, 8, 315, 2.5]', [270, 8, 315, 2.5], [257.2, 9.57]), ('course to steer across a stream [5, 6, 275, 1.2]', [5, 6, 275, 1.2], [16.5, 5.88])], [('course to steer across a stream [350, 5, 80, 2]', [350, 5, 80, 2], [326.4, 4.58]), ('course to steer across a stream [0, 5, 90, 0]', [0, 5, 90, 0], [0.0, 5.0]), ('course to steer across a stream [270, 8, 315, 2.5]', [270, 8, 315, 2.5], [257.2, 9.57]), ('course to steer across a stream [5, 6, 275, 1.2]', [5, 6, 275, 1.2], [16.5, 5.88]), ('course to steer across a stream [180, 3, 90, 2.9]', [180, 3, 90, 2.9], [255.2, 0.77]), ('course to steer across a stream [300, 7, 120, 2]', [300, 7, 120, 2], [300.0, 5.0]), ('course to steer across a stream [5, 6, 90, 1.5]', [5, 6, 90, 1.5], [350.6, 5.94])], [('course to steer across a stream [45, 5, 60, 7]', [45, 5, 60, 7], [23.8, 11.42]), ('course to steer across a stream [180, 3, 90, 2.9]', [180, 3, 90, 2.9], [255.2, 0.77]), ('course to steer across a stream [300, 7, 120, 2]', [300, 7, 120, 2], [300.0, 5.0]), ('course to steer across a stream [5, 6, 90, 1.5]', [5, 6, 90, 1.5], [350.6, 5.94]), ('course to steer across a stream [355, 6, 260, 1.5]', [355, 6, 260, 1.5], [9.4, 5.68]), ('course to steer across a stream [0, 5, 10, 7]', [0, 5, 10, 7], [345.9, 11.74]), ('course to steer across a stream [90, 4, 100, 4.5]', [90, 4, 100, 4.5], [78.7, 8.35])]]
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
course to steer across a stream [90, 6, 180, 1.5][104.5, 5.81][75.5, 5.81]Failed
course to steer across a stream [90, 6, 0, 1.5][75.5, 5.81][104.5, 5.81]Failed
course to steer across a stream [10, 5, 100, 2][33.6, 4.58][346.4, 4.58]Failed
course to steer across a stream [350, 5, 80, 2][13.6, 4.58][326.4, 4.58]Failed
course to steer across a stream [200, 4, 200, 3][200.0, 7.0][200.0, 7.0]Passed
course to steer across a stream [200, 4, 20, 3][200.0, 1.0][200.0, 1.0]Passed
course to steer across a stream [45, 5, 135, 6]NoneNonePassed

SHA-256 / 1920e82982e2c2a8666a0fc0472ce01344655d1e391c9ee96d635c6976fde0ed

2 / The unsuccessful fix

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(x):
    track,bs,setd,drift=x
    rel=math.radians(setd-track)
    cross=drift*math.sin(rel)
    if abs(cross)>bs: return None
    a=-math.atan2(cross,bs)
    sog=bs*math.cos(a)+drift*math.cos(rel)
    if sog<=0: return None
    return [round(math.degrees(math.radians(track)+a)%360,1),round(sog,2)]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('course to steer across a stream [90, 6, 180, 1.5]', [90, 6, 180, 1.5], [75.5, 5.81]), ('course to steer across a stream [90, 6, 0, 1.5]', [90, 6, 0, 1.5], [104.5, 5.81]), ('course to steer across a stream [10, 5, 100, 2]', [10, 5, 100, 2], [346.4, 4.58]), ('course to steer across a stream [350, 5, 80, 2]', [350, 5, 80, 2], [326.4, 4.58]), ('course to steer across a stream [200, 4, 200, 3]', [200, 4, 200, 3], [200.0, 7.0]), ('course to steer across a stream [200, 4, 20, 3]', [200, 4, 20, 3], [200.0, 1.0]), ('course to steer across a stream [45, 5, 135, 6]', [45, 5, 135, 6], None)], [('course to steer across a stream [90, 6, 0, 1.5]', [90, 6, 0, 1.5], [104.5, 5.81]), ('course to steer across a stream [350, 5, 80, 2]', [350, 5, 80, 2], [326.4, 4.58]), ('course to steer across a stream [200, 4, 200, 3]', [200, 4, 200, 3], [200.0, 7.0]), ('course to steer across a stream [200, 4, 20, 3]', [200, 4, 20, 3], [200.0, 1.0]), ('course to steer across a stream [45, 5, 135, 6]', [45, 5, 135, 6], None), ('course to steer across a stream [45, 5, 60, 7]', [45, 5, 60, 7], [23.8, 11.42]), ('course to steer across a stream [120, 5, 300, 5.5]', [120, 5, 300, 5.5], None)], [('course to steer across a stream [10, 5, 100, 2]', [10, 5, 100, 2], [346.4, 4.58]), ('course to steer across a stream [45, 5, 135, 6]', [45, 5, 135, 6], None), ('course to steer across a stream [45, 5, 60, 7]', [45, 5, 60, 7], [23.8, 11.42]), ('course to steer across a stream [120, 5, 300, 5.5]', [120, 5, 300, 5.5], None), ('course to steer across a stream [0, 5, 90, 0]', [0, 5, 90, 0], [0.0, 5.0]), ('course to steer across a stream [270, 8, 315, 2.5]', [270, 8, 315, 2.5], [257.2, 9.57]), ('course to steer across a stream [5, 6, 275, 1.2]', [5, 6, 275, 1.2], [16.5, 5.88])], [('course to steer across a stream [350, 5, 80, 2]', [350, 5, 80, 2], [326.4, 4.58]), ('course to steer across a stream [0, 5, 90, 0]', [0, 5, 90, 0], [0.0, 5.0]), ('course to steer across a stream [270, 8, 315, 2.5]', [270, 8, 315, 2.5], [257.2, 9.57]), ('course to steer across a stream [5, 6, 275, 1.2]', [5, 6, 275, 1.2], [16.5, 5.88]), ('course to steer across a stream [180, 3, 90, 2.9]', [180, 3, 90, 2.9], [255.2, 0.77]), ('course to steer across a stream [300, 7, 120, 2]', [300, 7, 120, 2], [300.0, 5.0]), ('course to steer across a stream [5, 6, 90, 1.5]', [5, 6, 90, 1.5], [350.6, 5.94])], [('course to steer across a stream [45, 5, 60, 7]', [45, 5, 60, 7], [23.8, 11.42]), ('course to steer across a stream [180, 3, 90, 2.9]', [180, 3, 90, 2.9], [255.2, 0.77]), ('course to steer across a stream [300, 7, 120, 2]', [300, 7, 120, 2], [300.0, 5.0]), ('course to steer across a stream [5, 6, 90, 1.5]', [5, 6, 90, 1.5], [350.6, 5.94]), ('course to steer across a stream [355, 6, 260, 1.5]', [355, 6, 260, 1.5], [9.4, 5.68]), ('course to steer across a stream [0, 5, 10, 7]', [0, 5, 10, 7], [345.9, 11.74]), ('course to steer across a stream [90, 4, 100, 4.5]', [90, 4, 100, 4.5], [78.7, 8.35])]]
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
course to steer across a stream [90, 6, 180, 1.5][76.0, 5.82][75.5, 5.81]Failed
course to steer across a stream [90, 6, 0, 1.5][104.0, 5.82][104.5, 5.81]Failed
course to steer across a stream [10, 5, 100, 2][348.2, 4.64][346.4, 4.58]Failed
course to steer across a stream [350, 5, 80, 2][328.2, 4.64][326.4, 4.58]Failed
course to steer across a stream [200, 4, 200, 3][200.0, 7.0][200.0, 7.0]Passed
course to steer across a stream [200, 4, 20, 3][200.0, 1.0][200.0, 1.0]Passed
course to steer across a stream [45, 5, 135, 6]NoneNonePassed

SHA-256 / cb0adef38d4f1344b4f1383eb3be8599d367f651ba56c629f4abbdebb408b24f

3 / The verified repair

Exit 0
"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(x):
    track,bs,setd,drift=x
    rel=math.radians(setd-track)
    cross=drift*math.sin(rel)
    if abs(cross)>bs: return None
    a=math.asin(-cross/bs)
    sog=bs*math.cos(a)+drift*math.cos(rel)
    if sog<=0: return None
    return [round(math.degrees(math.radians(track)+a)%360,1),round(sog,2)]
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('course to steer across a stream [90, 6, 180, 1.5]', [90, 6, 180, 1.5], [75.5, 5.81]), ('course to steer across a stream [90, 6, 0, 1.5]', [90, 6, 0, 1.5], [104.5, 5.81]), ('course to steer across a stream [10, 5, 100, 2]', [10, 5, 100, 2], [346.4, 4.58]), ('course to steer across a stream [350, 5, 80, 2]', [350, 5, 80, 2], [326.4, 4.58]), ('course to steer across a stream [200, 4, 200, 3]', [200, 4, 200, 3], [200.0, 7.0]), ('course to steer across a stream [200, 4, 20, 3]', [200, 4, 20, 3], [200.0, 1.0]), ('course to steer across a stream [45, 5, 135, 6]', [45, 5, 135, 6], None)], [('course to steer across a stream [90, 6, 0, 1.5]', [90, 6, 0, 1.5], [104.5, 5.81]), ('course to steer across a stream [350, 5, 80, 2]', [350, 5, 80, 2], [326.4, 4.58]), ('course to steer across a stream [200, 4, 200, 3]', [200, 4, 200, 3], [200.0, 7.0]), ('course to steer across a stream [200, 4, 20, 3]', [200, 4, 20, 3], [200.0, 1.0]), ('course to steer across a stream [45, 5, 135, 6]', [45, 5, 135, 6], None), ('course to steer across a stream [45, 5, 60, 7]', [45, 5, 60, 7], [23.8, 11.42]), ('course to steer across a stream [120, 5, 300, 5.5]', [120, 5, 300, 5.5], None)], [('course to steer across a stream [10, 5, 100, 2]', [10, 5, 100, 2], [346.4, 4.58]), ('course to steer across a stream [45, 5, 135, 6]', [45, 5, 135, 6], None), ('course to steer across a stream [45, 5, 60, 7]', [45, 5, 60, 7], [23.8, 11.42]), ('course to steer across a stream [120, 5, 300, 5.5]', [120, 5, 300, 5.5], None), ('course to steer across a stream [0, 5, 90, 0]', [0, 5, 90, 0], [0.0, 5.0]), ('course to steer across a stream [270, 8, 315, 2.5]', [270, 8, 315, 2.5], [257.2, 9.57]), ('course to steer across a stream [5, 6, 275, 1.2]', [5, 6, 275, 1.2], [16.5, 5.88])], [('course to steer across a stream [350, 5, 80, 2]', [350, 5, 80, 2], [326.4, 4.58]), ('course to steer across a stream [0, 5, 90, 0]', [0, 5, 90, 0], [0.0, 5.0]), ('course to steer across a stream [270, 8, 315, 2.5]', [270, 8, 315, 2.5], [257.2, 9.57]), ('course to steer across a stream [5, 6, 275, 1.2]', [5, 6, 275, 1.2], [16.5, 5.88]), ('course to steer across a stream [180, 3, 90, 2.9]', [180, 3, 90, 2.9], [255.2, 0.77]), ('course to steer across a stream [300, 7, 120, 2]', [300, 7, 120, 2], [300.0, 5.0]), ('course to steer across a stream [5, 6, 90, 1.5]', [5, 6, 90, 1.5], [350.6, 5.94])], [('course to steer across a stream [45, 5, 60, 7]', [45, 5, 60, 7], [23.8, 11.42]), ('course to steer across a stream [180, 3, 90, 2.9]', [180, 3, 90, 2.9], [255.2, 0.77]), ('course to steer across a stream [300, 7, 120, 2]', [300, 7, 120, 2], [300.0, 5.0]), ('course to steer across a stream [5, 6, 90, 1.5]', [5, 6, 90, 1.5], [350.6, 5.94]), ('course to steer across a stream [355, 6, 260, 1.5]', [355, 6, 260, 1.5], [9.4, 5.68]), ('course to steer across a stream [0, 5, 10, 7]', [0, 5, 10, 7], [345.9, 11.74]), ('course to steer across a stream [90, 4, 100, 4.5]', [90, 4, 100, 4.5], [78.7, 8.35])]]
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
course to steer across a stream [90, 6, 180, 1.5][75.5, 5.81][75.5, 5.81]Passed
course to steer across a stream [90, 6, 0, 1.5][104.5, 5.81][104.5, 5.81]Passed
course to steer across a stream [10, 5, 100, 2][346.4, 4.58][346.4, 4.58]Passed
course to steer across a stream [350, 5, 80, 2][326.4, 4.58][326.4, 4.58]Passed
course to steer across a stream [200, 4, 200, 3][200.0, 7.0][200.0, 7.0]Passed
course to steer across a stream [200, 4, 20, 3][200.0, 1.0][200.0, 1.0]Passed
course to steer across a stream [45, 5, 135, 6]NoneNonePassed

SHA-256 / 233cca8de0a7d4a82625da8be682ffab748fcb52326e705828298dcb8104ab99

Verification & scope

A deterministic toy model with stipulated rules and constants; not certified hydrographic software or a substitute for official tide tables. 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:07.527757+00:00.

Case digest / 1d5ff829db7342ddd65be7b440e2494935105663fcf55965e2f58691c74acd41