FA-92076 / PLC ladder logic scan cycles / Open access
Periodic trigger is scheduled relative to the previous start · case 01
After a skipped trigger the task phase shifts permanently instead of returning to the fixed period grid.
ROOT CAUSE
The next trigger time is computed as last start + period rather than k * period.
VERIFIED REPAIR
Trigger k occurs at k * period from t=0 regardless of skips.
Unsuccessful approach: Adding the skipped periods to the last start still miscounts after earlier skips.
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 = starts[-1] + period if starts else 0
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: exact watchdog', [20, 30, [30, 5, 5, 5]], [[0, 40, 60], 1, 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', [50, 100, [100, 100, 100, 100, 50, 25]], [[0, 100, 200, 250], 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: skip then drift', [10, 100, [25, 1, 1, 1, 1, 1]], [[0, 30, 40, 50], 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: scenario 1', [20, 60, [60, 20, 36, 40, 20]], [[0, 60], 3, 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: 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 1', [20, 100, [100, 20, 60, 100, 40, 20, 30, 12]], [[0, 100, 120], 5, 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]],
['control: scenario 2', [10, 40, [50, 40, 10, 40, 10, 61, 60, 40, 20, 20, 10, 10]], [[0], 0, 40]],
['regression: scenario 3',
[20, 30, [20, 30, 6, 30, 40, 39, 61, 36, 40, 40, 30, 20]],
[[0, 20, 60, 100], 2, 130]]],
[['regression: scenario 1',
[20, 30, [21, 20, 20, 20, 40, 30, 20, 30, 40, 30, 40]],
[[0, 40, 60, 80], 1, 110]],
['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: 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 3',
[10, 40, [20, 20, 20, 20, 10, 61, 55, 66, 20, 37, 40, 40]],
[[0, 20, 40, 50], 2, 90]]],
[['regression: scenario 5', [25, 40, [25, 40, 40, 40, 24, 25, 50, 40, 50]], [[0, 25, 75, 125, 150], 2, 190]],
['regression: scenario 6', [20, 30, [30, 20, 30, 24, 49, 59, 40, 49, 30, 50, 20]], [[0, 40, 80], 2, 110]],
['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], 4, None] | [[0, 10, 30, 40], 2, None] | Failed |
| regression: exact watchdog | [[0], 3, None] | [[0, 40, 60], 1, None] | Failed |
| control: watchdog exceeded | [[0, 10], 0, 22] | [[0, 10], 0, 22] | Passed |
| control: first execution overruns | [[0], 0, 25] | [[0], 0, 25] | Passed |
| regression: skip then drift | [[0], 5, None] | [[0, 30, 40, 50], 2, None] | Failed |
| regression: scenario 1 | [[0], 5, None] | [[0, 100, 200, 250], 2, None] | Failed |
| regression: scenario 2 | [[0], 7, None] | [[0, 100, 140], 5, None] | Failed |
| control: scenario 3 | [[0], 0, 40] | [[0], 0, 40] | Passed |
SHA-256 / 6527549ac413c1645fcdff4f2905608d76e6d1b75a22e8177439d8ab0a68804b
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 = (starts[-1] + period * (k - len(starts) + 1)) if starts else 0
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: exact watchdog', [20, 30, [30, 5, 5, 5]], [[0, 40, 60], 1, 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', [50, 100, [100, 100, 100, 100, 50, 25]], [[0, 100, 200, 250], 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: skip then drift', [10, 100, [25, 1, 1, 1, 1, 1]], [[0, 30, 40, 50], 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: scenario 1', [20, 60, [60, 20, 36, 40, 20]], [[0, 60], 3, 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: 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 1', [20, 100, [100, 20, 60, 100, 40, 20, 30, 12]], [[0, 100, 120], 5, 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]],
['control: scenario 2', [10, 40, [50, 40, 10, 40, 10, 61, 60, 40, 20, 20, 10, 10]], [[0], 0, 40]],
['regression: scenario 3',
[20, 30, [20, 30, 6, 30, 40, 39, 61, 36, 40, 40, 30, 20]],
[[0, 20, 60, 100], 2, 130]]],
[['regression: scenario 1',
[20, 30, [21, 20, 20, 20, 40, 30, 20, 30, 40, 30, 40]],
[[0, 40, 60, 80], 1, 110]],
['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: 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 3',
[10, 40, [20, 20, 20, 20, 10, 61, 55, 66, 20, 37, 40, 40]],
[[0, 20, 40, 50], 2, 90]]],
[['regression: scenario 5', [25, 40, [25, 40, 40, 40, 24, 25, 50, 40, 50]], [[0, 25, 75, 125, 150], 2, 190]],
['regression: scenario 6', [20, 30, [30, 20, 30, 24, 49, 59, 40, 49, 30, 50, 20]], [[0, 40, 80], 2, 110]],
['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, 50, 70], 1, None] | [[0, 10, 30, 40], 2, None] | Failed |
| regression: exact watchdog | [[0, 40, 80], 1, None] | [[0, 40, 60], 1, None] | Failed |
| control: watchdog exceeded | [[0, 10], 0, 22] | [[0, 10], 0, 22] | Passed |
| control: first execution overruns | [[0], 0, 25] | [[0], 0, 25] | Passed |
| regression: skip then drift | [[0, 30, 60, 90], 2, None] | [[0, 30, 40, 50], 2, None] | Failed |
| regression: scenario 1 | [[0, 100, 200, 300, 400], 1, None] | [[0, 100, 200, 250], 2, None] | Failed |
| regression: scenario 2 | [[0, 100, 200, 300], 4, None] | [[0, 100, 140], 5, None] | Failed |
| control: scenario 3 | [[0], 0, 40] | [[0], 0, 40] | Passed |
SHA-256 / ec0536ffa4075efad9ba9a0ba5e4c9dcc149ae3d4dce809307eb2506fab1124c
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: exact watchdog', [20, 30, [30, 5, 5, 5]], [[0, 40, 60], 1, 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', [50, 100, [100, 100, 100, 100, 50, 25]], [[0, 100, 200, 250], 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: skip then drift', [10, 100, [25, 1, 1, 1, 1, 1]], [[0, 30, 40, 50], 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: scenario 1', [20, 60, [60, 20, 36, 40, 20]], [[0, 60], 3, 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: 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 1', [20, 100, [100, 20, 60, 100, 40, 20, 30, 12]], [[0, 100, 120], 5, 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]],
['control: scenario 2', [10, 40, [50, 40, 10, 40, 10, 61, 60, 40, 20, 20, 10, 10]], [[0], 0, 40]],
['regression: scenario 3',
[20, 30, [20, 30, 6, 30, 40, 39, 61, 36, 40, 40, 30, 20]],
[[0, 20, 60, 100], 2, 130]]],
[['regression: scenario 1',
[20, 30, [21, 20, 20, 20, 40, 30, 20, 30, 40, 30, 40]],
[[0, 40, 60, 80], 1, 110]],
['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: 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 3',
[10, 40, [20, 20, 20, 20, 10, 61, 55, 66, 20, 37, 40, 40]],
[[0, 20, 40, 50], 2, 90]]],
[['regression: scenario 5', [25, 40, [25, 40, 40, 40, 24, 25, 50, 40, 50]], [[0, 25, 75, 125, 150], 2, 190]],
['regression: scenario 6', [20, 30, [30, 20, 30, 24, 49, 59, 40, 49, 30, 50, 20]], [[0, 40, 80], 2, 110]],
['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: exact watchdog | [[0, 40, 60], 1, None] | [[0, 40, 60], 1, None] | Passed |
| control: watchdog exceeded | [[0, 10], 0, 22] | [[0, 10], 0, 22] | 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 1 | [[0, 100, 200, 250], 2, None] | [[0, 100, 200, 250], 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 / abd1e7287e16d7b7a11cd1adcbc8bf3a9094a95330b981bc214104aa0969f1d5
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.330775+00:00.
Case digest / f816d6208b4b3bb03582d3b7d38c4b2b98c19534b6c9eae331bc5b2ee54be000