FA-92061 / PLC ladder logic scan cycles / Open access
Overlapped periodic triggers are queued behind the running execution · case 01
Start times drift off the period grid after an overrun because missed triggers run late.
ROOT CAUSE
An overlapped trigger is deferred to the completion time instead of being discarded.
VERIFIED REPAIR
Count and discard an overlapped trigger.
Unsuccessful approach: Discarding the trigger but still charging its execution time delays later triggers.
Case contract
Periodic task triggered every period ms from t=0; exec_times[k] is how long the k-th triggered execution would take. A trigger arriving while the previous execution is still running (trigger time < completion time) is an overlap: it is counted and skipped, not queued; a trigger exactly at completion starts normally. An execution longer than watchdog raises a major fault at start + watchdog and scheduling stops. Return [start_times, overlap_count, fault_time or None].
Why this case matters
Ladder programs are executed as repeated scans; each defect here changes what a rung, timer, counter or data-table instruction reports on a particular scan, which is how commissioning and field faults are actually observed.
1 / The failure
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(period, watchdog, exec_times):
t_free = 0
starts = []
overlaps = 0
fault = None
for k, ex in enumerate(exec_times):
trig = k * period
if trig < t_free:
overlaps += 1
trig = t_free
starts.append(trig)
if ex > watchdog:
fault = trig + watchdog
break
t_free = trig + ex
return [starts, overlaps, fault]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: mixed overlaps', [10, 50, [5, 15, 3, 10, 12, 4]], [[0, 10, 30, 40], 2, None]],
['regression: scenario 1', [50, 100, [100, 100, 100, 100, 50, 25]], [[0, 100, 200, 250], 2, None]],
['control: watchdog exceeded', [10, 12, [5, 13, 4]], [[0, 10], 0, 22]],
['regression: exact watchdog', [20, 30, [30, 5, 5, 5]], [[0, 40, 60], 1, None]],
['control: first execution overruns', [10, 25, [30, 5]], [[0], 0, 25]],
['regression: skip then drift', [10, 100, [25, 1, 1, 1, 1, 1]], [[0, 30, 40, 50], 2, None]],
['regression: scenario 2', [20, 100, [100, 100, 60, 39, 20, 40, 54, 40]], [[0, 100, 140], 5, None]],
['control: scenario 3', [50, 40, [50, 100, 50, 100, 100, 40]], [[0], 0, 40]]],
[['regression: exact watchdog', [20, 30, [30, 5, 5, 5]], [[0, 40, 60], 1, None]],
['regression: scenario 2',
[10, 100, [10, 10, 10, 10, 10, 20, 100, 3, 20, 10, 100]],
[[0, 10, 20, 30, 40, 50, 70, 80, 100], 2, None]],
['regression: mixed overlaps', [10, 50, [5, 15, 3, 10, 12, 4]], [[0, 10, 30, 40], 2, None]],
['control: watchdog exceeded', [10, 12, [5, 13, 4]], [[0, 10], 0, 22]],
['control: first execution overruns', [10, 25, [30, 5]], [[0], 0, 25]],
['regression: skip then drift', [10, 100, [25, 1, 1, 1, 1, 1]], [[0, 30, 40, 50], 2, None]],
['regression: scenario 1', [20, 60, [60, 20, 36, 40, 20]], [[0, 60], 3, None]],
['regression: scenario 3', [20, 100, [40, 40, 51, 40, 100]], [[0, 40], 3, None]]],
[['regression: skip then drift', [10, 100, [25, 1, 1, 1, 1, 1]], [[0, 30, 40, 50], 2, None]],
['regression: scenario 4', [25, 30, [30, 30, 6, 16, 50, 30]], [[0, 50, 75, 100], 1, 130]],
['regression: mixed overlaps', [10, 50, [5, 15, 3, 10, 12, 4]], [[0, 10, 30, 40], 2, None]],
['control: watchdog exceeded', [10, 12, [5, 13, 4]], [[0, 10], 0, 22]],
['regression: exact watchdog', [20, 30, [30, 5, 5, 5]], [[0, 40, 60], 1, None]],
['control: first execution overruns', [10, 25, [30, 5]], [[0], 0, 25]],
['regression: scenario 1', [20, 100, [100, 20, 60, 100, 40, 20, 30, 12]], [[0, 100, 120], 5, None]],
['control: scenario 2', [10, 40, [50, 40, 10, 40, 10, 61, 60, 40, 20, 20, 10, 10]], [[0], 0, 40]]],
[['regression: scenario 1',
[20, 30, [21, 20, 20, 20, 40, 30, 20, 30, 40, 30, 40]],
[[0, 40, 60, 80], 1, 110]],
['regression: scenario 5', [10, 30, [30, 30, 30, 28, 10, 30]], [[0, 30], 4, None]],
['regression: mixed overlaps', [10, 50, [5, 15, 3, 10, 12, 4]], [[0, 10, 30, 40], 2, None]],
['control: watchdog exceeded', [10, 12, [5, 13, 4]], [[0, 10], 0, 22]],
['regression: exact watchdog', [20, 30, [30, 5, 5, 5]], [[0, 40, 60], 1, None]],
['control: first execution overruns', [10, 25, [30, 5]], [[0], 0, 25]],
['regression: skip then drift', [10, 100, [25, 1, 1, 1, 1, 1]], [[0, 30, 40, 50], 2, None]],
['regression: scenario 2',
[50, 100, [100, 100, 100, 50, 100, 50, 100, 50, 100, 15, 100, 50]],
[[0, 100, 200, 300, 400, 500], 6, None]]],
[['regression: scenario 5', [25, 40, [25, 40, 40, 40, 24, 25, 50, 40, 50]], [[0, 25, 75, 125, 150], 2, 190]],
['regression: scenario 14',
[20, 60, [40, 60, 40, 20, 60, 60, 43, 20, 12, 68, 20, 20]],
[[0, 40, 80, 140, 160, 180], 4, 240]],
['regression: mixed overlaps', [10, 50, [5, 15, 3, 10, 12, 4]], [[0, 10, 30, 40], 2, None]],
['control: watchdog exceeded', [10, 12, [5, 13, 4]], [[0, 10], 0, 22]],
['regression: exact watchdog', [20, 30, [30, 5, 5, 5]], [[0, 40, 60], 1, None]],
['control: first execution overruns', [10, 25, [30, 5]], [[0], 0, 25]],
['regression: skip then drift', [10, 100, [25, 1, 1, 1, 1, 1]], [[0, 30, 40, 50], 2, None]],
['control: scenario 1', [25, 60, [70, 25, 60, 35, 25, 15, 60, 60, 25, 50, 25, 4]], [[0], 0, 60]]]]
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: mixed overlaps | [[0, 10, 25, 30, 40, 52], 2, None] | [[0, 10, 30, 40], 2, None] | Failed |
| regression: scenario 1 | [[0, 100, 200, 300, 400, 450], 5, None] | [[0, 100, 200, 250], 2, None] | Failed |
| control: watchdog exceeded | [[0, 10], 0, 22] | [[0, 10], 0, 22] | Passed |
| regression: exact watchdog | [[0, 30, 40, 60], 1, None] | [[0, 40, 60], 1, None] | Failed |
| control: first execution overruns | [[0], 0, 25] | [[0], 0, 25] | Passed |
| regression: skip then drift | [[0, 25, 26, 30, 40, 50], 2, None] | [[0, 30, 40, 50], 2, None] | Failed |
| regression: scenario 2 | [[0, 100, 200, 260, 299, 319, 359, 413], 7, None] | [[0, 100, 140], 5, None] | Failed |
| control: scenario 3 | [[0], 0, 40] | [[0], 0, 40] | Passed |
SHA-256 / a0b3ebf1b666c288403c6e56760bc8922affb58ec27aea48bae36fd98e397353
2 / The unsuccessful fix
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(period, watchdog, exec_times):
t_free = 0
starts = []
overlaps = 0
fault = None
for k, ex in enumerate(exec_times):
trig = k * period
if trig < t_free:
overlaps += 1
t_free += ex
continue
starts.append(trig)
if ex > watchdog:
fault = trig + watchdog
break
t_free = trig + ex
return [starts, overlaps, fault]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: mixed overlaps', [10, 50, [5, 15, 3, 10, 12, 4]], [[0, 10, 30, 40], 2, None]],
['regression: scenario 1', [50, 100, [100, 100, 100, 100, 50, 25]], [[0, 100, 200, 250], 2, None]],
['control: watchdog exceeded', [10, 12, [5, 13, 4]], [[0, 10], 0, 22]],
['regression: exact watchdog', [20, 30, [30, 5, 5, 5]], [[0, 40, 60], 1, None]],
['control: first execution overruns', [10, 25, [30, 5]], [[0], 0, 25]],
['regression: skip then drift', [10, 100, [25, 1, 1, 1, 1, 1]], [[0, 30, 40, 50], 2, None]],
['regression: scenario 2', [20, 100, [100, 100, 60, 39, 20, 40, 54, 40]], [[0, 100, 140], 5, None]],
['control: scenario 3', [50, 40, [50, 100, 50, 100, 100, 40]], [[0], 0, 40]]],
[['regression: exact watchdog', [20, 30, [30, 5, 5, 5]], [[0, 40, 60], 1, None]],
['regression: scenario 2',
[10, 100, [10, 10, 10, 10, 10, 20, 100, 3, 20, 10, 100]],
[[0, 10, 20, 30, 40, 50, 70, 80, 100], 2, None]],
['regression: mixed overlaps', [10, 50, [5, 15, 3, 10, 12, 4]], [[0, 10, 30, 40], 2, None]],
['control: watchdog exceeded', [10, 12, [5, 13, 4]], [[0, 10], 0, 22]],
['control: first execution overruns', [10, 25, [30, 5]], [[0], 0, 25]],
['regression: skip then drift', [10, 100, [25, 1, 1, 1, 1, 1]], [[0, 30, 40, 50], 2, None]],
['regression: scenario 1', [20, 60, [60, 20, 36, 40, 20]], [[0, 60], 3, None]],
['regression: scenario 3', [20, 100, [40, 40, 51, 40, 100]], [[0, 40], 3, None]]],
[['regression: skip then drift', [10, 100, [25, 1, 1, 1, 1, 1]], [[0, 30, 40, 50], 2, None]],
['regression: scenario 4', [25, 30, [30, 30, 6, 16, 50, 30]], [[0, 50, 75, 100], 1, 130]],
['regression: mixed overlaps', [10, 50, [5, 15, 3, 10, 12, 4]], [[0, 10, 30, 40], 2, None]],
['control: watchdog exceeded', [10, 12, [5, 13, 4]], [[0, 10], 0, 22]],
['regression: exact watchdog', [20, 30, [30, 5, 5, 5]], [[0, 40, 60], 1, None]],
['control: first execution overruns', [10, 25, [30, 5]], [[0], 0, 25]],
['regression: scenario 1', [20, 100, [100, 20, 60, 100, 40, 20, 30, 12]], [[0, 100, 120], 5, None]],
['control: scenario 2', [10, 40, [50, 40, 10, 40, 10, 61, 60, 40, 20, 20, 10, 10]], [[0], 0, 40]]],
[['regression: scenario 1',
[20, 30, [21, 20, 20, 20, 40, 30, 20, 30, 40, 30, 40]],
[[0, 40, 60, 80], 1, 110]],
['regression: scenario 5', [10, 30, [30, 30, 30, 28, 10, 30]], [[0, 30], 4, None]],
['regression: mixed overlaps', [10, 50, [5, 15, 3, 10, 12, 4]], [[0, 10, 30, 40], 2, None]],
['control: watchdog exceeded', [10, 12, [5, 13, 4]], [[0, 10], 0, 22]],
['regression: exact watchdog', [20, 30, [30, 5, 5, 5]], [[0, 40, 60], 1, None]],
['control: first execution overruns', [10, 25, [30, 5]], [[0], 0, 25]],
['regression: skip then drift', [10, 100, [25, 1, 1, 1, 1, 1]], [[0, 30, 40, 50], 2, None]],
['regression: scenario 2',
[50, 100, [100, 100, 100, 50, 100, 50, 100, 50, 100, 15, 100, 50]],
[[0, 100, 200, 300, 400, 500], 6, None]]],
[['regression: scenario 5', [25, 40, [25, 40, 40, 40, 24, 25, 50, 40, 50]], [[0, 25, 75, 125, 150], 2, 190]],
['regression: scenario 14',
[20, 60, [40, 60, 40, 20, 60, 60, 43, 20, 12, 68, 20, 20]],
[[0, 40, 80, 140, 160, 180], 4, 240]],
['regression: mixed overlaps', [10, 50, [5, 15, 3, 10, 12, 4]], [[0, 10, 30, 40], 2, None]],
['control: watchdog exceeded', [10, 12, [5, 13, 4]], [[0, 10], 0, 22]],
['regression: exact watchdog', [20, 30, [30, 5, 5, 5]], [[0, 40, 60], 1, None]],
['control: first execution overruns', [10, 25, [30, 5]], [[0], 0, 25]],
['regression: skip then drift', [10, 100, [25, 1, 1, 1, 1, 1]], [[0, 30, 40, 50], 2, None]],
['control: scenario 1', [25, 60, [70, 25, 60, 35, 25, 15, 60, 60, 25, 50, 25, 4]], [[0], 0, 60]]]]
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: mixed overlaps | [[0, 10, 30, 40], 2, None] | [[0, 10, 30, 40], 2, None] | Passed |
| regression: scenario 1 | [[0], 5, None] | [[0, 100, 200, 250], 2, None] | Failed |
| control: watchdog exceeded | [[0, 10], 0, 22] | [[0, 10], 0, 22] | Passed |
| regression: exact watchdog | [[0, 40, 60], 1, None] | [[0, 40, 60], 1, None] | Passed |
| control: first execution overruns | [[0], 0, 25] | [[0], 0, 25] | Passed |
| regression: skip then drift | [[0, 30, 40, 50], 2, None] | [[0, 30, 40, 50], 2, None] | Passed |
| regression: scenario 2 | [[0], 7, None] | [[0, 100, 140], 5, None] | Failed |
| control: scenario 3 | [[0], 0, 40] | [[0], 0, 40] | Passed |
SHA-256 / e1d3daf29cc8094c2c85e0c9b61322dc04bc4fa01323ddad6e58a4ecab76669f
3 / The verified repair
Exit 0"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(period, watchdog, exec_times):
t_free = 0
starts = []
overlaps = 0
fault = None
for k, ex in enumerate(exec_times):
trig = k * period
if trig < t_free:
overlaps += 1
continue
starts.append(trig)
if ex > watchdog:
fault = trig + watchdog
break
t_free = trig + ex
return [starts, overlaps, fault]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression: mixed overlaps', [10, 50, [5, 15, 3, 10, 12, 4]], [[0, 10, 30, 40], 2, None]],
['regression: scenario 1', [50, 100, [100, 100, 100, 100, 50, 25]], [[0, 100, 200, 250], 2, None]],
['control: watchdog exceeded', [10, 12, [5, 13, 4]], [[0, 10], 0, 22]],
['regression: exact watchdog', [20, 30, [30, 5, 5, 5]], [[0, 40, 60], 1, None]],
['control: first execution overruns', [10, 25, [30, 5]], [[0], 0, 25]],
['regression: skip then drift', [10, 100, [25, 1, 1, 1, 1, 1]], [[0, 30, 40, 50], 2, None]],
['regression: scenario 2', [20, 100, [100, 100, 60, 39, 20, 40, 54, 40]], [[0, 100, 140], 5, None]],
['control: scenario 3', [50, 40, [50, 100, 50, 100, 100, 40]], [[0], 0, 40]]],
[['regression: exact watchdog', [20, 30, [30, 5, 5, 5]], [[0, 40, 60], 1, None]],
['regression: scenario 2',
[10, 100, [10, 10, 10, 10, 10, 20, 100, 3, 20, 10, 100]],
[[0, 10, 20, 30, 40, 50, 70, 80, 100], 2, None]],
['regression: mixed overlaps', [10, 50, [5, 15, 3, 10, 12, 4]], [[0, 10, 30, 40], 2, None]],
['control: watchdog exceeded', [10, 12, [5, 13, 4]], [[0, 10], 0, 22]],
['control: first execution overruns', [10, 25, [30, 5]], [[0], 0, 25]],
['regression: skip then drift', [10, 100, [25, 1, 1, 1, 1, 1]], [[0, 30, 40, 50], 2, None]],
['regression: scenario 1', [20, 60, [60, 20, 36, 40, 20]], [[0, 60], 3, None]],
['regression: scenario 3', [20, 100, [40, 40, 51, 40, 100]], [[0, 40], 3, None]]],
[['regression: skip then drift', [10, 100, [25, 1, 1, 1, 1, 1]], [[0, 30, 40, 50], 2, None]],
['regression: scenario 4', [25, 30, [30, 30, 6, 16, 50, 30]], [[0, 50, 75, 100], 1, 130]],
['regression: mixed overlaps', [10, 50, [5, 15, 3, 10, 12, 4]], [[0, 10, 30, 40], 2, None]],
['control: watchdog exceeded', [10, 12, [5, 13, 4]], [[0, 10], 0, 22]],
['regression: exact watchdog', [20, 30, [30, 5, 5, 5]], [[0, 40, 60], 1, None]],
['control: first execution overruns', [10, 25, [30, 5]], [[0], 0, 25]],
['regression: scenario 1', [20, 100, [100, 20, 60, 100, 40, 20, 30, 12]], [[0, 100, 120], 5, None]],
['control: scenario 2', [10, 40, [50, 40, 10, 40, 10, 61, 60, 40, 20, 20, 10, 10]], [[0], 0, 40]]],
[['regression: scenario 1',
[20, 30, [21, 20, 20, 20, 40, 30, 20, 30, 40, 30, 40]],
[[0, 40, 60, 80], 1, 110]],
['regression: scenario 5', [10, 30, [30, 30, 30, 28, 10, 30]], [[0, 30], 4, None]],
['regression: mixed overlaps', [10, 50, [5, 15, 3, 10, 12, 4]], [[0, 10, 30, 40], 2, None]],
['control: watchdog exceeded', [10, 12, [5, 13, 4]], [[0, 10], 0, 22]],
['regression: exact watchdog', [20, 30, [30, 5, 5, 5]], [[0, 40, 60], 1, None]],
['control: first execution overruns', [10, 25, [30, 5]], [[0], 0, 25]],
['regression: skip then drift', [10, 100, [25, 1, 1, 1, 1, 1]], [[0, 30, 40, 50], 2, None]],
['regression: scenario 2',
[50, 100, [100, 100, 100, 50, 100, 50, 100, 50, 100, 15, 100, 50]],
[[0, 100, 200, 300, 400, 500], 6, None]]],
[['regression: scenario 5', [25, 40, [25, 40, 40, 40, 24, 25, 50, 40, 50]], [[0, 25, 75, 125, 150], 2, 190]],
['regression: scenario 14',
[20, 60, [40, 60, 40, 20, 60, 60, 43, 20, 12, 68, 20, 20]],
[[0, 40, 80, 140, 160, 180], 4, 240]],
['regression: mixed overlaps', [10, 50, [5, 15, 3, 10, 12, 4]], [[0, 10, 30, 40], 2, None]],
['control: watchdog exceeded', [10, 12, [5, 13, 4]], [[0, 10], 0, 22]],
['regression: exact watchdog', [20, 30, [30, 5, 5, 5]], [[0, 40, 60], 1, None]],
['control: first execution overruns', [10, 25, [30, 5]], [[0], 0, 25]],
['regression: skip then drift', [10, 100, [25, 1, 1, 1, 1, 1]], [[0, 30, 40, 50], 2, None]],
['control: scenario 1', [25, 60, [70, 25, 60, 35, 25, 15, 60, 60, 25, 50, 25, 4]], [[0], 0, 60]]]]
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: mixed overlaps | [[0, 10, 30, 40], 2, None] | [[0, 10, 30, 40], 2, None] | Passed |
| regression: scenario 1 | [[0, 100, 200, 250], 2, None] | [[0, 100, 200, 250], 2, None] | Passed |
| control: watchdog exceeded | [[0, 10], 0, 22] | [[0, 10], 0, 22] | Passed |
| regression: exact watchdog | [[0, 40, 60], 1, None] | [[0, 40, 60], 1, None] | Passed |
| control: first execution overruns | [[0], 0, 25] | [[0], 0, 25] | Passed |
| regression: skip then drift | [[0, 30, 40, 50], 2, None] | [[0, 30, 40, 50], 2, None] | Passed |
| regression: scenario 2 | [[0, 100, 140], 5, None] | [[0, 100, 140], 5, None] | Passed |
| control: scenario 3 | [[0], 0, 40] | [[0], 0, 40] | Passed |
SHA-256 / aa6f4cddba4bdbb58d9ba9984ef01ab522cd130618c4d744ccefca7c3685580f
Verification & scope
A deterministic bounded teaching model of one PLC instruction or rung pattern evaluated scan by scan. The stated contract is a stipulated toy convention, not a claim of conformance to any vendor controller or IEC 61131-3 runtime. 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:51:42.289959+00:00.
Case digest / 7d4e0dd48f2b3ca7b4124ebf557783383928afd90cb474626815028059f6836c