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.
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 fixture | Actual | Expected | Outcome |
|---|---|---|---|
| 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 fixture | Actual | Expected | Outcome |
|---|---|---|---|
| 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 fixture | Actual | Expected | Outcome |
|---|---|---|---|
| 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