FAILURE MAP
← Case archive

FA-66921 / Railway interlocking logic / Open access

Approach locking time release: approach clearance reset · case 01

A route is approach-locked although the approaching train has already left.

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

ROOT CAUSE

Clearing the approach section never resets the approach flag.

VERIFIED REPAIR

Reset the approach flag whenever the approach section clears.

Unsuccessful approach: Ignoring clearance while a route is set still leaves a stale approach flag at cancel time.

Case contract

A route can be set only when free. pass_signal moves a set route to in-use; train_clear frees an in-use route. Cancelling a set route frees it at once unless a train is in the approach section, in which case it is approach-locked until cancel time + release_s (released when an event time reaches the deadline). approach_occ/approach_clear track the approach section. query appends set, free, in-use or locked-until:<t>.

Why this case matters

Interlocking logic decides whether trains may be given authority; a wrong decision at this point either grants unsafe movements or strands traffic.

1 / The failure

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

N = 1
observations = []
def solve(x):
    T = x['release_s']
    state = 'free'
    until = None
    approach = False
    out = []
    for t, kind in x['events']:
        if state == 'locked' and t >= until:
            state = 'free'
        if kind == 'approach_occ':
            approach = True
        elif kind == 'approach_clear':
            pass
        elif kind == 'set' and state == 'free':
            state = 'set'
        elif kind == 'cancel' and state == 'set':
            if approach:
                state = 'locked'
                until = t + T
            else:
                state = 'free'
        elif kind == 'pass_signal':
            approach = False
            if state == 'set':
                state = 'in-use'
        elif kind == 'train_clear' and state == 'in-use':
            state = 'free'
        elif kind == 'query':
            out.append('locked-until:%d' % until if state == 'locked' else state)
    return out
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('regression: train backs out of the approach', {'release_s': 60, 'events': [[0, 'approach_occ'], [1, 'approach_clear'], [2, 'set'], [3, 'cancel'], [4, 'query']]}, ['free']), ('sampled regression 62', {'release_s': 120, 'events': [[1, 'pass_signal'], [6, 'cancel'], [126, 'approach_clear'], [131, 'set'], [141, 'approach_clear'], [171, 'train_clear'], [172, 'approach_occ'], [172, 'approach_clear'], [202, 'approach_clear'], [207, 'train_clear'], [217, 'cancel'], [247, 'train_clear'], [307, 'query']]}, ['free']), ('boundary: query at exactly the release time', {'release_s': 120, 'events': [[0, 'set'], [5, 'approach_occ'], [10, 'cancel'], [130, 'query']]}, ['free']), ('boundary: re-set at exactly the release time', {'release_s': 60, 'events': [[0, 'set'], [1, 'approach_occ'], [2, 'cancel'], [62, 'set'], [63, 'query']]}, ['set']), ('boundary: second lock after an earlier one', {'release_s': 60, 'events': [[0, 'set'], [1, 'approach_occ'], [2, 'cancel'], [70, 'set'], [80, 'cancel'], [81, 'query']]}, ['locked-until:140']), ('control 1', {'release_s': 120, 'events': [[120, 'query'], [125, 'set'], [185, 'set'], [215, 'pass_signal'], [275, 'query'], [475, 'query']]}, ['free', 'in-use', 'in-use']), ('control 4', {'release_s': 60, 'events': [[30, 'query'], [60, 'query'], [120, 'query'], [180, 'cancel'], [240, 'query'], [245, 'approach_occ'], [445, 'query']]}, ['free', 'free', 'free', 'free', 'free']), ('control 7', {'release_s': 120, 'events': [[0, 'cancel'], [0, 'query'], [0, 'approach_occ'], [1, 'set'], [11, 'approach_clear'], [41, 'query'], [42, 'approach_occ'], [242, 'query']]}, ['free', 'set', 'set'])], [('regression: train backs out of the approach', {'release_s': 60, 'events': [[0, 'approach_occ'], [1, 'approach_clear'], [2, 'set'], [3, 'cancel'], [4, 'query']]}, ['free']), ('sampled regression 62', {'release_s': 120, 'events': [[1, 'pass_signal'], [6, 'cancel'], [126, 'approach_clear'], [131, 'set'], [141, 'approach_clear'], [171, 'train_clear'], [172, 'approach_occ'], [172, 'approach_clear'], [202, 'approach_clear'], [207, 'train_clear'], [217, 'cancel'], [247, 'train_clear'], [307, 'query']]}, ['free']), ('sampled regression 45', {'release_s': 120, 'events': [[0, 'set'], [0, 'cancel'], [0, 'train_clear'], [0, 'approach_occ'], [1, 'approach_clear'], [6, 'approach_clear'], [126, 'query'], [136, 'set'], [141, 'train_clear'], [151, 'cancel'], [152, 'approach_clear'], [152, 'pass_signal'], [212, 'query']]}, ['free', 'free']), ('boundary: second lock after an earlier one', {'release_s': 60, 'events': [[0, 'set'], [1, 'approach_occ'], [2, 'cancel'], [70, 'set'], [80, 'cancel'], [81, 'query']]}, ['locked-until:140']), ('boundary: cancel under a train', {'release_s': 60, 'events': [[0, 'set'], [1, 'approach_occ'], [2, 'pass_signal'], [3, 'cancel'], [4, 'query'], [5, 'train_clear'], [6, 'query']]}, ['in-use', 'free']), ('control 12', {'release_s': 120, 'events': [[1, 'query'], [121, 'approach_clear'], [241, 'set'], [271, 'set'], [391, 'approach_occ'], [451, 'query']]}, ['free', 'set']), ('control 15', {'release_s': 60, 'events': [[5, 'set'], [6, 'query'], [6, 'cancel'], [36, 'set'], [46, 'query'], [106, 'approach_clear'], [226, 'query'], [227, 'query'], [237, 'query'], [267, 'set'], [327, 'set'], [327, 'query']]}, ['set', 'set', 'set', 'set', 'set', 'set']), ('sampled regression 18', {'release_s': 60, 'events': [[5, 'approach_occ'], [10, 'approach_clear'], [70, 'approach_clear'], [190, 'query'], [195, 'set'], [195, 'cancel'], [195, 'cancel'], [200, 'query'], [260, 'set'], [265, 'set'], [325, 'query']]}, ['free', 'free', 'set'])], [('regression: train backs out of the approach', {'release_s': 60, 'events': [[0, 'approach_occ'], [1, 'approach_clear'], [2, 'set'], [3, 'cancel'], [4, 'query']]}, ['free']), ('sampled regression 62', {'release_s': 120, 'events': [[1, 'pass_signal'], [6, 'cancel'], [126, 'approach_clear'], [131, 'set'], [141, 'approach_clear'], [171, 'train_clear'], [172, 'approach_occ'], [172, 'approach_clear'], [202, 'approach_clear'], [207, 'train_clear'], [217, 'cancel'], [247, 'train_clear'], [307, 'query']]}, ['free']), ('sampled regression 18', {'release_s': 60, 'events': [[5, 'approach_occ'], [10, 'approach_clear'], [70, 'approach_clear'], [190, 'query'], [195, 'set'], [195, 'cancel'], [195, 'cancel'], [200, 'query'], [260, 'set'], [265, 'set'], [325, 'query']]}, ['free', 'free', 'set']), ('boundary: cancel under a train', {'release_s': 60, 'events': [[0, 'set'], [1, 'approach_occ'], [2, 'pass_signal'], [3, 'cancel'], [4, 'query'], [5, 'train_clear'], [6, 'query']]}, ['in-use', 'free']), ('boundary: set with a train approaching', {'release_s': 60, 'events': [[0, 'approach_occ'], [1, 'set'], [2, 'query']]}, ['set']), ('control 23', {'release_s': 60, 'events': [[10, 'cancel'], [15, 'set'], [25, 'approach_occ'], [26, 'set'], [36, 'cancel'], [156, 'query']]}, ['free']), ('control 26', {'release_s': 60, 'events': [[1, 'approach_occ'], [6, 'set'], [66, 'set'], [66, 'cancel'], [96, 'query'], [106, 'approach_clear'], [136, 'query'], [141, 'approach_clear'], [151, 'approach_occ'], [211, 'train_clear'], [271, 'query']]}, ['locked-until:126', 'free', 'free']), ('control 29', {'release_s': 60, 'events': [[0, 'set'], [0, 'set'], [30, 'train_clear'], [40, 'set'], [40, 'query'], [100, 'cancel'], [110, 'approach_occ'], [170, 'query']]}, ['set', 'free'])], [('regression: train backs out of the approach', {'release_s': 60, 'events': [[0, 'approach_occ'], [1, 'approach_clear'], [2, 'set'], [3, 'cancel'], [4, 'query']]}, ['free']), ('sampled regression 62', {'release_s': 120, 'events': [[1, 'pass_signal'], [6, 'cancel'], [126, 'approach_clear'], [131, 'set'], [141, 'approach_clear'], [171, 'train_clear'], [172, 'approach_occ'], [172, 'approach_clear'], [202, 'approach_clear'], [207, 'train_clear'], [217, 'cancel'], [247, 'train_clear'], [307, 'query']]}, ['free']), ('boundary: cancel with empty approach', {'release_s': 60, 'events': [[0, 'set'], [1, 'cancel'], [2, 'query']]}, ['free']), ('boundary: set attempted while locked', {'release_s': 60, 'events': [[0, 'set'], [1, 'approach_occ'], [2, 'cancel'], [30, 'set'], [31, 'query']]}, ['locked-until:62']), ('boundary: query at exactly the release time', {'release_s': 120, 'events': [[0, 'set'], [5, 'approach_occ'], [10, 'cancel'], [130, 'query']]}, ['free']), ('control 34', {'release_s': 120, 'events': [[120, 'set'], [121, 'set'], [121, 'cancel'], [241, 'approach_occ'], [361, 'pass_signal'], [371, 'cancel'], [491, 'approach_occ'], [611, 'query']]}, ['free']), ('control 37', {'release_s': 60, 'events': [[60, 'query'], [61, 'train_clear'], [71, 'set'], [131, 'train_clear'], [131, 'pass_signal'], [136, 'approach_occ'], [196, 'set'], [197, 'cancel'], [202, 'set'], [262, 'train_clear'], [382, 'query'], [382, 'query']]}, ['free', 'free', 'free']), ('control 40', {'release_s': 120, 'events': [[0, 'query'], [30, 'cancel'], [90, 'approach_occ'], [100, 'set'], [130, 'query'], [135, 'approach_clear'], [255, 'train_clear'], [260, 'query'], [265, 'approach_occ'], [270, 'set'], [275, 'train_clear'], [305, 'cancel'], [365, 'query']]}, ['free', 'set', 'set', 'locked-until:425'])], [('regression: train backs out of the approach', {'release_s': 60, 'events': [[0, 'approach_occ'], [1, 'approach_clear'], [2, 'set'], [3, 'cancel'], [4, 'query']]}, ['free']), ('sampled regression 62', {'release_s': 120, 'events': [[1, 'pass_signal'], [6, 'cancel'], [126, 'approach_clear'], [131, 'set'], [141, 'approach_clear'], [171, 'train_clear'], [172, 'approach_occ'], [172, 'approach_clear'], [202, 'approach_clear'], [207, 'train_clear'], [217, 'cancel'], [247, 'train_clear'], [307, 'query']]}, ['free']), ('boundary: query at exactly the release time', {'release_s': 120, 'events': [[0, 'set'], [5, 'approach_occ'], [10, 'cancel'], [130, 'query']]}, ['free']), ('boundary: re-set at exactly the release time', {'release_s': 60, 'events': [[0, 'set'], [1, 'approach_occ'], [2, 'cancel'], [62, 'set'], [63, 'query']]}, ['set']), ('boundary: second lock after an earlier one', {'release_s': 60, 'events': [[0, 'set'], [1, 'approach_occ'], [2, 'cancel'], [70, 'set'], [80, 'cancel'], [81, 'query']]}, ['locked-until:140']), ('sampled regression 45', {'release_s': 120, 'events': [[0, 'set'], [0, 'cancel'], [0, 'train_clear'], [0, 'approach_occ'], [1, 'approach_clear'], [6, 'approach_clear'], [126, 'query'], [136, 'set'], [141, 'train_clear'], [151, 'cancel'], [152, 'approach_clear'], [152, 'pass_signal'], [212, 'query']]}, ['free', 'free']), ('control 48', {'release_s': 60, 'events': [[120, 'set'], [121, 'train_clear'], [122, 'set'], [242, 'query'], [242, 'query'], [252, 'pass_signal'], [257, 'approach_occ'], [257, 'query']]}, ['set', 'set', 'in-use']), ('control 51', {'release_s': 60, 'events': [[120, 'pass_signal'], [120, 'cancel'], [125, 'approach_clear'], [126, 'cancel'], [136, 'cancel'], [256, 'approach_clear'], [316, 'pass_signal'], [376, 'query'], [377, 'query'], [387, 'cancel'], [387, 'query']]}, ['free', 'free', 'free'])]]
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
regression: train backs out of the approach['locked-until:63']['free']Failed
sampled regression 62['locked-until:337']['free']Failed
boundary: query at exactly the release time['free']['free']Passed
boundary: re-set at exactly the release time['set']['set']Passed
boundary: second lock after an earlier one['locked-until:140']['locked-until:140']Passed
control 1['free', 'in-use', 'in-use']['free', 'in-use', 'in-use']Passed
control 4['free', 'free', 'free', 'free', 'free']['free', 'free', 'free', 'free', 'free']Passed
control 7['free', 'set', 'set']['free', 'set', 'set']Passed

SHA-256 / 7100dac53d82823b54eeed3bba5f429298a90847c4967dc76a7ce314b0f86978

2 / The unsuccessful fix

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

N = 1
observations = []
def solve(x):
    T = x['release_s']
    state = 'free'
    until = None
    approach = False
    out = []
    for t, kind in x['events']:
        if state == 'locked' and t >= until:
            state = 'free'
        if kind == 'approach_occ':
            approach = True
        elif kind == 'approach_clear' and state != 'set':
            approach = False
        elif kind == 'set' and state == 'free':
            state = 'set'
        elif kind == 'cancel' and state == 'set':
            if approach:
                state = 'locked'
                until = t + T
            else:
                state = 'free'
        elif kind == 'pass_signal':
            approach = False
            if state == 'set':
                state = 'in-use'
        elif kind == 'train_clear' and state == 'in-use':
            state = 'free'
        elif kind == 'query':
            out.append('locked-until:%d' % until if state == 'locked' else state)
    return out
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('regression: train backs out of the approach', {'release_s': 60, 'events': [[0, 'approach_occ'], [1, 'approach_clear'], [2, 'set'], [3, 'cancel'], [4, 'query']]}, ['free']), ('sampled regression 62', {'release_s': 120, 'events': [[1, 'pass_signal'], [6, 'cancel'], [126, 'approach_clear'], [131, 'set'], [141, 'approach_clear'], [171, 'train_clear'], [172, 'approach_occ'], [172, 'approach_clear'], [202, 'approach_clear'], [207, 'train_clear'], [217, 'cancel'], [247, 'train_clear'], [307, 'query']]}, ['free']), ('boundary: query at exactly the release time', {'release_s': 120, 'events': [[0, 'set'], [5, 'approach_occ'], [10, 'cancel'], [130, 'query']]}, ['free']), ('boundary: re-set at exactly the release time', {'release_s': 60, 'events': [[0, 'set'], [1, 'approach_occ'], [2, 'cancel'], [62, 'set'], [63, 'query']]}, ['set']), ('boundary: second lock after an earlier one', {'release_s': 60, 'events': [[0, 'set'], [1, 'approach_occ'], [2, 'cancel'], [70, 'set'], [80, 'cancel'], [81, 'query']]}, ['locked-until:140']), ('control 1', {'release_s': 120, 'events': [[120, 'query'], [125, 'set'], [185, 'set'], [215, 'pass_signal'], [275, 'query'], [475, 'query']]}, ['free', 'in-use', 'in-use']), ('control 4', {'release_s': 60, 'events': [[30, 'query'], [60, 'query'], [120, 'query'], [180, 'cancel'], [240, 'query'], [245, 'approach_occ'], [445, 'query']]}, ['free', 'free', 'free', 'free', 'free']), ('control 7', {'release_s': 120, 'events': [[0, 'cancel'], [0, 'query'], [0, 'approach_occ'], [1, 'set'], [11, 'approach_clear'], [41, 'query'], [42, 'approach_occ'], [242, 'query']]}, ['free', 'set', 'set'])], [('regression: train backs out of the approach', {'release_s': 60, 'events': [[0, 'approach_occ'], [1, 'approach_clear'], [2, 'set'], [3, 'cancel'], [4, 'query']]}, ['free']), ('sampled regression 62', {'release_s': 120, 'events': [[1, 'pass_signal'], [6, 'cancel'], [126, 'approach_clear'], [131, 'set'], [141, 'approach_clear'], [171, 'train_clear'], [172, 'approach_occ'], [172, 'approach_clear'], [202, 'approach_clear'], [207, 'train_clear'], [217, 'cancel'], [247, 'train_clear'], [307, 'query']]}, ['free']), ('sampled regression 45', {'release_s': 120, 'events': [[0, 'set'], [0, 'cancel'], [0, 'train_clear'], [0, 'approach_occ'], [1, 'approach_clear'], [6, 'approach_clear'], [126, 'query'], [136, 'set'], [141, 'train_clear'], [151, 'cancel'], [152, 'approach_clear'], [152, 'pass_signal'], [212, 'query']]}, ['free', 'free']), ('boundary: second lock after an earlier one', {'release_s': 60, 'events': [[0, 'set'], [1, 'approach_occ'], [2, 'cancel'], [70, 'set'], [80, 'cancel'], [81, 'query']]}, ['locked-until:140']), ('boundary: cancel under a train', {'release_s': 60, 'events': [[0, 'set'], [1, 'approach_occ'], [2, 'pass_signal'], [3, 'cancel'], [4, 'query'], [5, 'train_clear'], [6, 'query']]}, ['in-use', 'free']), ('control 12', {'release_s': 120, 'events': [[1, 'query'], [121, 'approach_clear'], [241, 'set'], [271, 'set'], [391, 'approach_occ'], [451, 'query']]}, ['free', 'set']), ('control 15', {'release_s': 60, 'events': [[5, 'set'], [6, 'query'], [6, 'cancel'], [36, 'set'], [46, 'query'], [106, 'approach_clear'], [226, 'query'], [227, 'query'], [237, 'query'], [267, 'set'], [327, 'set'], [327, 'query']]}, ['set', 'set', 'set', 'set', 'set', 'set']), ('sampled regression 18', {'release_s': 60, 'events': [[5, 'approach_occ'], [10, 'approach_clear'], [70, 'approach_clear'], [190, 'query'], [195, 'set'], [195, 'cancel'], [195, 'cancel'], [200, 'query'], [260, 'set'], [265, 'set'], [325, 'query']]}, ['free', 'free', 'set'])], [('regression: train backs out of the approach', {'release_s': 60, 'events': [[0, 'approach_occ'], [1, 'approach_clear'], [2, 'set'], [3, 'cancel'], [4, 'query']]}, ['free']), ('sampled regression 62', {'release_s': 120, 'events': [[1, 'pass_signal'], [6, 'cancel'], [126, 'approach_clear'], [131, 'set'], [141, 'approach_clear'], [171, 'train_clear'], [172, 'approach_occ'], [172, 'approach_clear'], [202, 'approach_clear'], [207, 'train_clear'], [217, 'cancel'], [247, 'train_clear'], [307, 'query']]}, ['free']), ('sampled regression 18', {'release_s': 60, 'events': [[5, 'approach_occ'], [10, 'approach_clear'], [70, 'approach_clear'], [190, 'query'], [195, 'set'], [195, 'cancel'], [195, 'cancel'], [200, 'query'], [260, 'set'], [265, 'set'], [325, 'query']]}, ['free', 'free', 'set']), ('boundary: cancel under a train', {'release_s': 60, 'events': [[0, 'set'], [1, 'approach_occ'], [2, 'pass_signal'], [3, 'cancel'], [4, 'query'], [5, 'train_clear'], [6, 'query']]}, ['in-use', 'free']), ('boundary: set with a train approaching', {'release_s': 60, 'events': [[0, 'approach_occ'], [1, 'set'], [2, 'query']]}, ['set']), ('control 23', {'release_s': 60, 'events': [[10, 'cancel'], [15, 'set'], [25, 'approach_occ'], [26, 'set'], [36, 'cancel'], [156, 'query']]}, ['free']), ('control 26', {'release_s': 60, 'events': [[1, 'approach_occ'], [6, 'set'], [66, 'set'], [66, 'cancel'], [96, 'query'], [106, 'approach_clear'], [136, 'query'], [141, 'approach_clear'], [151, 'approach_occ'], [211, 'train_clear'], [271, 'query']]}, ['locked-until:126', 'free', 'free']), ('control 29', {'release_s': 60, 'events': [[0, 'set'], [0, 'set'], [30, 'train_clear'], [40, 'set'], [40, 'query'], [100, 'cancel'], [110, 'approach_occ'], [170, 'query']]}, ['set', 'free'])], [('regression: train backs out of the approach', {'release_s': 60, 'events': [[0, 'approach_occ'], [1, 'approach_clear'], [2, 'set'], [3, 'cancel'], [4, 'query']]}, ['free']), ('sampled regression 62', {'release_s': 120, 'events': [[1, 'pass_signal'], [6, 'cancel'], [126, 'approach_clear'], [131, 'set'], [141, 'approach_clear'], [171, 'train_clear'], [172, 'approach_occ'], [172, 'approach_clear'], [202, 'approach_clear'], [207, 'train_clear'], [217, 'cancel'], [247, 'train_clear'], [307, 'query']]}, ['free']), ('boundary: cancel with empty approach', {'release_s': 60, 'events': [[0, 'set'], [1, 'cancel'], [2, 'query']]}, ['free']), ('boundary: set attempted while locked', {'release_s': 60, 'events': [[0, 'set'], [1, 'approach_occ'], [2, 'cancel'], [30, 'set'], [31, 'query']]}, ['locked-until:62']), ('boundary: query at exactly the release time', {'release_s': 120, 'events': [[0, 'set'], [5, 'approach_occ'], [10, 'cancel'], [130, 'query']]}, ['free']), ('control 34', {'release_s': 120, 'events': [[120, 'set'], [121, 'set'], [121, 'cancel'], [241, 'approach_occ'], [361, 'pass_signal'], [371, 'cancel'], [491, 'approach_occ'], [611, 'query']]}, ['free']), ('control 37', {'release_s': 60, 'events': [[60, 'query'], [61, 'train_clear'], [71, 'set'], [131, 'train_clear'], [131, 'pass_signal'], [136, 'approach_occ'], [196, 'set'], [197, 'cancel'], [202, 'set'], [262, 'train_clear'], [382, 'query'], [382, 'query']]}, ['free', 'free', 'free']), ('control 40', {'release_s': 120, 'events': [[0, 'query'], [30, 'cancel'], [90, 'approach_occ'], [100, 'set'], [130, 'query'], [135, 'approach_clear'], [255, 'train_clear'], [260, 'query'], [265, 'approach_occ'], [270, 'set'], [275, 'train_clear'], [305, 'cancel'], [365, 'query']]}, ['free', 'set', 'set', 'locked-until:425'])], [('regression: train backs out of the approach', {'release_s': 60, 'events': [[0, 'approach_occ'], [1, 'approach_clear'], [2, 'set'], [3, 'cancel'], [4, 'query']]}, ['free']), ('sampled regression 62', {'release_s': 120, 'events': [[1, 'pass_signal'], [6, 'cancel'], [126, 'approach_clear'], [131, 'set'], [141, 'approach_clear'], [171, 'train_clear'], [172, 'approach_occ'], [172, 'approach_clear'], [202, 'approach_clear'], [207, 'train_clear'], [217, 'cancel'], [247, 'train_clear'], [307, 'query']]}, ['free']), ('boundary: query at exactly the release time', {'release_s': 120, 'events': [[0, 'set'], [5, 'approach_occ'], [10, 'cancel'], [130, 'query']]}, ['free']), ('boundary: re-set at exactly the release time', {'release_s': 60, 'events': [[0, 'set'], [1, 'approach_occ'], [2, 'cancel'], [62, 'set'], [63, 'query']]}, ['set']), ('boundary: second lock after an earlier one', {'release_s': 60, 'events': [[0, 'set'], [1, 'approach_occ'], [2, 'cancel'], [70, 'set'], [80, 'cancel'], [81, 'query']]}, ['locked-until:140']), ('sampled regression 45', {'release_s': 120, 'events': [[0, 'set'], [0, 'cancel'], [0, 'train_clear'], [0, 'approach_occ'], [1, 'approach_clear'], [6, 'approach_clear'], [126, 'query'], [136, 'set'], [141, 'train_clear'], [151, 'cancel'], [152, 'approach_clear'], [152, 'pass_signal'], [212, 'query']]}, ['free', 'free']), ('control 48', {'release_s': 60, 'events': [[120, 'set'], [121, 'train_clear'], [122, 'set'], [242, 'query'], [242, 'query'], [252, 'pass_signal'], [257, 'approach_occ'], [257, 'query']]}, ['set', 'set', 'in-use']), ('control 51', {'release_s': 60, 'events': [[120, 'pass_signal'], [120, 'cancel'], [125, 'approach_clear'], [126, 'cancel'], [136, 'cancel'], [256, 'approach_clear'], [316, 'pass_signal'], [376, 'query'], [377, 'query'], [387, 'cancel'], [387, 'query']]}, ['free', 'free', 'free'])]]
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
regression: train backs out of the approach['free']['free']Passed
sampled regression 62['locked-until:337']['free']Failed
boundary: query at exactly the release time['free']['free']Passed
boundary: re-set at exactly the release time['set']['set']Passed
boundary: second lock after an earlier one['locked-until:140']['locked-until:140']Passed
control 1['free', 'in-use', 'in-use']['free', 'in-use', 'in-use']Passed
control 4['free', 'free', 'free', 'free', 'free']['free', 'free', 'free', 'free', 'free']Passed
control 7['free', 'set', 'set']['free', 'set', 'set']Passed

SHA-256 / c1a5dbd81e376996a9c671ea034fd61e37f1c5e2b07d5bdb849410232242e338

3 / The verified repair

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

N = 1
observations = []
def solve(x):
    T = x['release_s']
    state = 'free'
    until = None
    approach = False
    out = []
    for t, kind in x['events']:
        if state == 'locked' and t >= until:
            state = 'free'
        if kind == 'approach_occ':
            approach = True
        elif kind == 'approach_clear':
            approach = False
        elif kind == 'set' and state == 'free':
            state = 'set'
        elif kind == 'cancel' and state == 'set':
            if approach:
                state = 'locked'
                until = t + T
            else:
                state = 'free'
        elif kind == 'pass_signal':
            approach = False
            if state == 'set':
                state = 'in-use'
        elif kind == 'train_clear' and state == 'in-use':
            state = 'free'
        elif kind == 'query':
            out.append('locked-until:%d' % until if state == 'locked' else state)
    return out
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('regression: train backs out of the approach', {'release_s': 60, 'events': [[0, 'approach_occ'], [1, 'approach_clear'], [2, 'set'], [3, 'cancel'], [4, 'query']]}, ['free']), ('sampled regression 62', {'release_s': 120, 'events': [[1, 'pass_signal'], [6, 'cancel'], [126, 'approach_clear'], [131, 'set'], [141, 'approach_clear'], [171, 'train_clear'], [172, 'approach_occ'], [172, 'approach_clear'], [202, 'approach_clear'], [207, 'train_clear'], [217, 'cancel'], [247, 'train_clear'], [307, 'query']]}, ['free']), ('boundary: query at exactly the release time', {'release_s': 120, 'events': [[0, 'set'], [5, 'approach_occ'], [10, 'cancel'], [130, 'query']]}, ['free']), ('boundary: re-set at exactly the release time', {'release_s': 60, 'events': [[0, 'set'], [1, 'approach_occ'], [2, 'cancel'], [62, 'set'], [63, 'query']]}, ['set']), ('boundary: second lock after an earlier one', {'release_s': 60, 'events': [[0, 'set'], [1, 'approach_occ'], [2, 'cancel'], [70, 'set'], [80, 'cancel'], [81, 'query']]}, ['locked-until:140']), ('control 1', {'release_s': 120, 'events': [[120, 'query'], [125, 'set'], [185, 'set'], [215, 'pass_signal'], [275, 'query'], [475, 'query']]}, ['free', 'in-use', 'in-use']), ('control 4', {'release_s': 60, 'events': [[30, 'query'], [60, 'query'], [120, 'query'], [180, 'cancel'], [240, 'query'], [245, 'approach_occ'], [445, 'query']]}, ['free', 'free', 'free', 'free', 'free']), ('control 7', {'release_s': 120, 'events': [[0, 'cancel'], [0, 'query'], [0, 'approach_occ'], [1, 'set'], [11, 'approach_clear'], [41, 'query'], [42, 'approach_occ'], [242, 'query']]}, ['free', 'set', 'set'])], [('regression: train backs out of the approach', {'release_s': 60, 'events': [[0, 'approach_occ'], [1, 'approach_clear'], [2, 'set'], [3, 'cancel'], [4, 'query']]}, ['free']), ('sampled regression 62', {'release_s': 120, 'events': [[1, 'pass_signal'], [6, 'cancel'], [126, 'approach_clear'], [131, 'set'], [141, 'approach_clear'], [171, 'train_clear'], [172, 'approach_occ'], [172, 'approach_clear'], [202, 'approach_clear'], [207, 'train_clear'], [217, 'cancel'], [247, 'train_clear'], [307, 'query']]}, ['free']), ('sampled regression 45', {'release_s': 120, 'events': [[0, 'set'], [0, 'cancel'], [0, 'train_clear'], [0, 'approach_occ'], [1, 'approach_clear'], [6, 'approach_clear'], [126, 'query'], [136, 'set'], [141, 'train_clear'], [151, 'cancel'], [152, 'approach_clear'], [152, 'pass_signal'], [212, 'query']]}, ['free', 'free']), ('boundary: second lock after an earlier one', {'release_s': 60, 'events': [[0, 'set'], [1, 'approach_occ'], [2, 'cancel'], [70, 'set'], [80, 'cancel'], [81, 'query']]}, ['locked-until:140']), ('boundary: cancel under a train', {'release_s': 60, 'events': [[0, 'set'], [1, 'approach_occ'], [2, 'pass_signal'], [3, 'cancel'], [4, 'query'], [5, 'train_clear'], [6, 'query']]}, ['in-use', 'free']), ('control 12', {'release_s': 120, 'events': [[1, 'query'], [121, 'approach_clear'], [241, 'set'], [271, 'set'], [391, 'approach_occ'], [451, 'query']]}, ['free', 'set']), ('control 15', {'release_s': 60, 'events': [[5, 'set'], [6, 'query'], [6, 'cancel'], [36, 'set'], [46, 'query'], [106, 'approach_clear'], [226, 'query'], [227, 'query'], [237, 'query'], [267, 'set'], [327, 'set'], [327, 'query']]}, ['set', 'set', 'set', 'set', 'set', 'set']), ('sampled regression 18', {'release_s': 60, 'events': [[5, 'approach_occ'], [10, 'approach_clear'], [70, 'approach_clear'], [190, 'query'], [195, 'set'], [195, 'cancel'], [195, 'cancel'], [200, 'query'], [260, 'set'], [265, 'set'], [325, 'query']]}, ['free', 'free', 'set'])], [('regression: train backs out of the approach', {'release_s': 60, 'events': [[0, 'approach_occ'], [1, 'approach_clear'], [2, 'set'], [3, 'cancel'], [4, 'query']]}, ['free']), ('sampled regression 62', {'release_s': 120, 'events': [[1, 'pass_signal'], [6, 'cancel'], [126, 'approach_clear'], [131, 'set'], [141, 'approach_clear'], [171, 'train_clear'], [172, 'approach_occ'], [172, 'approach_clear'], [202, 'approach_clear'], [207, 'train_clear'], [217, 'cancel'], [247, 'train_clear'], [307, 'query']]}, ['free']), ('sampled regression 18', {'release_s': 60, 'events': [[5, 'approach_occ'], [10, 'approach_clear'], [70, 'approach_clear'], [190, 'query'], [195, 'set'], [195, 'cancel'], [195, 'cancel'], [200, 'query'], [260, 'set'], [265, 'set'], [325, 'query']]}, ['free', 'free', 'set']), ('boundary: cancel under a train', {'release_s': 60, 'events': [[0, 'set'], [1, 'approach_occ'], [2, 'pass_signal'], [3, 'cancel'], [4, 'query'], [5, 'train_clear'], [6, 'query']]}, ['in-use', 'free']), ('boundary: set with a train approaching', {'release_s': 60, 'events': [[0, 'approach_occ'], [1, 'set'], [2, 'query']]}, ['set']), ('control 23', {'release_s': 60, 'events': [[10, 'cancel'], [15, 'set'], [25, 'approach_occ'], [26, 'set'], [36, 'cancel'], [156, 'query']]}, ['free']), ('control 26', {'release_s': 60, 'events': [[1, 'approach_occ'], [6, 'set'], [66, 'set'], [66, 'cancel'], [96, 'query'], [106, 'approach_clear'], [136, 'query'], [141, 'approach_clear'], [151, 'approach_occ'], [211, 'train_clear'], [271, 'query']]}, ['locked-until:126', 'free', 'free']), ('control 29', {'release_s': 60, 'events': [[0, 'set'], [0, 'set'], [30, 'train_clear'], [40, 'set'], [40, 'query'], [100, 'cancel'], [110, 'approach_occ'], [170, 'query']]}, ['set', 'free'])], [('regression: train backs out of the approach', {'release_s': 60, 'events': [[0, 'approach_occ'], [1, 'approach_clear'], [2, 'set'], [3, 'cancel'], [4, 'query']]}, ['free']), ('sampled regression 62', {'release_s': 120, 'events': [[1, 'pass_signal'], [6, 'cancel'], [126, 'approach_clear'], [131, 'set'], [141, 'approach_clear'], [171, 'train_clear'], [172, 'approach_occ'], [172, 'approach_clear'], [202, 'approach_clear'], [207, 'train_clear'], [217, 'cancel'], [247, 'train_clear'], [307, 'query']]}, ['free']), ('boundary: cancel with empty approach', {'release_s': 60, 'events': [[0, 'set'], [1, 'cancel'], [2, 'query']]}, ['free']), ('boundary: set attempted while locked', {'release_s': 60, 'events': [[0, 'set'], [1, 'approach_occ'], [2, 'cancel'], [30, 'set'], [31, 'query']]}, ['locked-until:62']), ('boundary: query at exactly the release time', {'release_s': 120, 'events': [[0, 'set'], [5, 'approach_occ'], [10, 'cancel'], [130, 'query']]}, ['free']), ('control 34', {'release_s': 120, 'events': [[120, 'set'], [121, 'set'], [121, 'cancel'], [241, 'approach_occ'], [361, 'pass_signal'], [371, 'cancel'], [491, 'approach_occ'], [611, 'query']]}, ['free']), ('control 37', {'release_s': 60, 'events': [[60, 'query'], [61, 'train_clear'], [71, 'set'], [131, 'train_clear'], [131, 'pass_signal'], [136, 'approach_occ'], [196, 'set'], [197, 'cancel'], [202, 'set'], [262, 'train_clear'], [382, 'query'], [382, 'query']]}, ['free', 'free', 'free']), ('control 40', {'release_s': 120, 'events': [[0, 'query'], [30, 'cancel'], [90, 'approach_occ'], [100, 'set'], [130, 'query'], [135, 'approach_clear'], [255, 'train_clear'], [260, 'query'], [265, 'approach_occ'], [270, 'set'], [275, 'train_clear'], [305, 'cancel'], [365, 'query']]}, ['free', 'set', 'set', 'locked-until:425'])], [('regression: train backs out of the approach', {'release_s': 60, 'events': [[0, 'approach_occ'], [1, 'approach_clear'], [2, 'set'], [3, 'cancel'], [4, 'query']]}, ['free']), ('sampled regression 62', {'release_s': 120, 'events': [[1, 'pass_signal'], [6, 'cancel'], [126, 'approach_clear'], [131, 'set'], [141, 'approach_clear'], [171, 'train_clear'], [172, 'approach_occ'], [172, 'approach_clear'], [202, 'approach_clear'], [207, 'train_clear'], [217, 'cancel'], [247, 'train_clear'], [307, 'query']]}, ['free']), ('boundary: query at exactly the release time', {'release_s': 120, 'events': [[0, 'set'], [5, 'approach_occ'], [10, 'cancel'], [130, 'query']]}, ['free']), ('boundary: re-set at exactly the release time', {'release_s': 60, 'events': [[0, 'set'], [1, 'approach_occ'], [2, 'cancel'], [62, 'set'], [63, 'query']]}, ['set']), ('boundary: second lock after an earlier one', {'release_s': 60, 'events': [[0, 'set'], [1, 'approach_occ'], [2, 'cancel'], [70, 'set'], [80, 'cancel'], [81, 'query']]}, ['locked-until:140']), ('sampled regression 45', {'release_s': 120, 'events': [[0, 'set'], [0, 'cancel'], [0, 'train_clear'], [0, 'approach_occ'], [1, 'approach_clear'], [6, 'approach_clear'], [126, 'query'], [136, 'set'], [141, 'train_clear'], [151, 'cancel'], [152, 'approach_clear'], [152, 'pass_signal'], [212, 'query']]}, ['free', 'free']), ('control 48', {'release_s': 60, 'events': [[120, 'set'], [121, 'train_clear'], [122, 'set'], [242, 'query'], [242, 'query'], [252, 'pass_signal'], [257, 'approach_occ'], [257, 'query']]}, ['set', 'set', 'in-use']), ('control 51', {'release_s': 60, 'events': [[120, 'pass_signal'], [120, 'cancel'], [125, 'approach_clear'], [126, 'cancel'], [136, 'cancel'], [256, 'approach_clear'], [316, 'pass_signal'], [376, 'query'], [377, 'query'], [387, 'cancel'], [387, 'query']]}, ['free', 'free', 'free'])]]
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
regression: train backs out of the approach['free']['free']Passed
sampled regression 62['free']['free']Passed
boundary: query at exactly the release time['free']['free']Passed
boundary: re-set at exactly the release time['set']['set']Passed
boundary: second lock after an earlier one['locked-until:140']['locked-until:140']Passed
control 1['free', 'in-use', 'in-use']['free', 'in-use', 'in-use']Passed
control 4['free', 'free', 'free', 'free', 'free']['free', 'free', 'free', 'free', 'free']Passed
control 7['free', 'set', 'set']['free', 'set', 'set']Passed

SHA-256 / 59b206aa0fa8915cbcda6704e38d7f785fa23e038d54f8a45b5eef7a3a4763ea

Verification & scope

Stipulated toy interlocking contract for a bounded teaching model; it makes no claim of conformance to any railway signalling standard and omits real safety cases. 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:47:48.146105+00:00.

Case digest / d66a26ef9ed3322214d49ee5fac668411e011e952d3d2664588aed9e72cef126