FA-92071 / PLC ladder logic scan cycles / Open access
Watchdog fault time is reported at the end of the overrunning execution · case 01
The major fault is logged later than it occurred, after the watchdog should already have stopped the task.
ROOT CAUSE
The fault time adds the full execution time rather than the watchdog limit to the start time.
VERIFIED REPAIR
The fault occurs at start time + watchdog.
Unsuccessful approach: Reporting the watchdog limit alone drops the task start time.
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
continue
starts.append(trig)
if ex > watchdog:
fault = trig + ex
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: watchdog exceeded', [10, 12, [5, 13, 4]], [[0, 10], 0, 22]],
['regression: scenario 4',
[50, 60, [21, 100, 100, 60, 100, 100, 60, 60, 45, 50, 100, 100]],
[[0, 50], 0, 110]],
['control: mixed overlaps', [10, 50, [5, 15, 3, 10, 12, 4]], [[0, 10, 30, 40], 2, None]],
['control: exact watchdog', [20, 30, [30, 5, 5, 5]], [[0, 40, 60], 1, None]],
['regression: first execution overruns', [10, 25, [30, 5]], [[0], 0, 25]],
['control: skip then drift', [10, 100, [25, 1, 1, 1, 1, 1]], [[0, 30, 40, 50], 2, None]],
['control: scenario 1', [50, 100, [100, 100, 100, 100, 50, 25]], [[0, 100, 200, 250], 2, None]],
['control: scenario 2', [20, 100, [100, 100, 60, 39, 20, 40, 54, 40]], [[0, 100, 140], 5, None]]],
[['regression: first execution overruns', [10, 25, [30, 5]], [[0], 0, 25]],
['regression: scenario 4', [10, 30, [20, 20, 51, 30, 26, 20, 54, 2]], [[0, 20], 1, 50]],
['control: mixed overlaps', [10, 50, [5, 15, 3, 10, 12, 4]], [[0, 10, 30, 40], 2, None]],
['regression: watchdog exceeded', [10, 12, [5, 13, 4]], [[0, 10], 0, 22]],
['control: exact watchdog', [20, 30, [30, 5, 5, 5]], [[0, 40, 60], 1, None]],
['control: skip then drift', [10, 100, [25, 1, 1, 1, 1, 1]], [[0, 30, 40, 50], 2, None]],
['control: scenario 1', [20, 60, [60, 20, 36, 40, 20]], [[0, 60], 3, None]],
['control: 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 2', [10, 40, [50, 40, 10, 40, 10, 61, 60, 40, 20, 20, 10, 10]], [[0], 0, 40]],
['regression: scenario 4', [25, 30, [30, 30, 6, 16, 50, 30]], [[0, 50, 75, 100], 1, 130]],
['control: mixed overlaps', [10, 50, [5, 15, 3, 10, 12, 4]], [[0, 10, 30, 40], 2, None]],
['regression: watchdog exceeded', [10, 12, [5, 13, 4]], [[0, 10], 0, 22]],
['control: exact watchdog', [20, 30, [30, 5, 5, 5]], [[0, 40, 60], 1, None]],
['regression: first execution overruns', [10, 25, [30, 5]], [[0], 0, 25]],
['control: skip then drift', [10, 100, [25, 1, 1, 1, 1, 1]], [[0, 30, 40, 50], 2, None]],
['control: scenario 1', [20, 100, [100, 20, 60, 100, 40, 20, 30, 12]], [[0, 100, 120], 5, 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 22', [25, 40, [25, 64, 50, 50, 40, 67, 25]], [[0, 25], 0, 65]],
['control: mixed overlaps', [10, 50, [5, 15, 3, 10, 12, 4]], [[0, 10, 30, 40], 2, None]],
['regression: watchdog exceeded', [10, 12, [5, 13, 4]], [[0, 10], 0, 22]],
['control: exact watchdog', [20, 30, [30, 5, 5, 5]], [[0, 40, 60], 1, None]],
['regression: first execution overruns', [10, 25, [30, 5]], [[0], 0, 25]],
['control: skip then drift', [10, 100, [25, 1, 1, 1, 1, 1]], [[0, 30, 40, 50], 2, None]],
['regression: scenario 1',
[20, 30, [21, 20, 20, 20, 40, 30, 20, 30, 40, 30, 40]],
[[0, 40, 60, 80], 1, 110]]],
[['regression: scenario 3', [50, 30, [50, 50, 100, 30, 25, 30, 50, 100]], [[0], 0, 30]],
['regression: scenario 13', [50, 40, [40, 50, 50, 10, 50, 100, 40, 20, 37, 41]], [[0, 50], 0, 90]],
['control: mixed overlaps', [10, 50, [5, 15, 3, 10, 12, 4]], [[0, 10, 30, 40], 2, None]],
['regression: watchdog exceeded', [10, 12, [5, 13, 4]], [[0, 10], 0, 22]],
['control: exact watchdog', [20, 30, [30, 5, 5, 5]], [[0, 40, 60], 1, None]],
['regression: first execution overruns', [10, 25, [30, 5]], [[0], 0, 25]],
['control: skip then drift', [10, 100, [25, 1, 1, 1, 1, 1]], [[0, 30, 40, 50], 2, None]],
['regression: 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: watchdog exceeded | [[0, 10], 0, 23] | [[0, 10], 0, 22] | Failed |
| regression: scenario 4 | [[0, 50], 0, 150] | [[0, 50], 0, 110] | Failed |
| control: mixed overlaps | [[0, 10, 30, 40], 2, None] | [[0, 10, 30, 40], 2, None] | Passed |
| control: exact watchdog | [[0, 40, 60], 1, None] | [[0, 40, 60], 1, None] | Passed |
| regression: first execution overruns | [[0], 0, 30] | [[0], 0, 25] | Failed |
| control: skip then drift | [[0, 30, 40, 50], 2, None] | [[0, 30, 40, 50], 2, None] | Passed |
| control: scenario 1 | [[0, 100, 200, 250], 2, None] | [[0, 100, 200, 250], 2, None] | Passed |
| control: scenario 2 | [[0, 100, 140], 5, None] | [[0, 100, 140], 5, None] | Passed |
SHA-256 / c55ff300918faa46dabab84623434f042c565c9afefe4f421edf1ea0a42a8848
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
continue
starts.append(trig)
if ex > watchdog:
fault = 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: watchdog exceeded', [10, 12, [5, 13, 4]], [[0, 10], 0, 22]],
['regression: scenario 4',
[50, 60, [21, 100, 100, 60, 100, 100, 60, 60, 45, 50, 100, 100]],
[[0, 50], 0, 110]],
['control: mixed overlaps', [10, 50, [5, 15, 3, 10, 12, 4]], [[0, 10, 30, 40], 2, None]],
['control: exact watchdog', [20, 30, [30, 5, 5, 5]], [[0, 40, 60], 1, None]],
['regression: first execution overruns', [10, 25, [30, 5]], [[0], 0, 25]],
['control: skip then drift', [10, 100, [25, 1, 1, 1, 1, 1]], [[0, 30, 40, 50], 2, None]],
['control: scenario 1', [50, 100, [100, 100, 100, 100, 50, 25]], [[0, 100, 200, 250], 2, None]],
['control: scenario 2', [20, 100, [100, 100, 60, 39, 20, 40, 54, 40]], [[0, 100, 140], 5, None]]],
[['regression: first execution overruns', [10, 25, [30, 5]], [[0], 0, 25]],
['regression: scenario 4', [10, 30, [20, 20, 51, 30, 26, 20, 54, 2]], [[0, 20], 1, 50]],
['control: mixed overlaps', [10, 50, [5, 15, 3, 10, 12, 4]], [[0, 10, 30, 40], 2, None]],
['regression: watchdog exceeded', [10, 12, [5, 13, 4]], [[0, 10], 0, 22]],
['control: exact watchdog', [20, 30, [30, 5, 5, 5]], [[0, 40, 60], 1, None]],
['control: skip then drift', [10, 100, [25, 1, 1, 1, 1, 1]], [[0, 30, 40, 50], 2, None]],
['control: scenario 1', [20, 60, [60, 20, 36, 40, 20]], [[0, 60], 3, None]],
['control: 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 2', [10, 40, [50, 40, 10, 40, 10, 61, 60, 40, 20, 20, 10, 10]], [[0], 0, 40]],
['regression: scenario 4', [25, 30, [30, 30, 6, 16, 50, 30]], [[0, 50, 75, 100], 1, 130]],
['control: mixed overlaps', [10, 50, [5, 15, 3, 10, 12, 4]], [[0, 10, 30, 40], 2, None]],
['regression: watchdog exceeded', [10, 12, [5, 13, 4]], [[0, 10], 0, 22]],
['control: exact watchdog', [20, 30, [30, 5, 5, 5]], [[0, 40, 60], 1, None]],
['regression: first execution overruns', [10, 25, [30, 5]], [[0], 0, 25]],
['control: skip then drift', [10, 100, [25, 1, 1, 1, 1, 1]], [[0, 30, 40, 50], 2, None]],
['control: scenario 1', [20, 100, [100, 20, 60, 100, 40, 20, 30, 12]], [[0, 100, 120], 5, 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 22', [25, 40, [25, 64, 50, 50, 40, 67, 25]], [[0, 25], 0, 65]],
['control: mixed overlaps', [10, 50, [5, 15, 3, 10, 12, 4]], [[0, 10, 30, 40], 2, None]],
['regression: watchdog exceeded', [10, 12, [5, 13, 4]], [[0, 10], 0, 22]],
['control: exact watchdog', [20, 30, [30, 5, 5, 5]], [[0, 40, 60], 1, None]],
['regression: first execution overruns', [10, 25, [30, 5]], [[0], 0, 25]],
['control: skip then drift', [10, 100, [25, 1, 1, 1, 1, 1]], [[0, 30, 40, 50], 2, None]],
['regression: scenario 1',
[20, 30, [21, 20, 20, 20, 40, 30, 20, 30, 40, 30, 40]],
[[0, 40, 60, 80], 1, 110]]],
[['regression: scenario 3', [50, 30, [50, 50, 100, 30, 25, 30, 50, 100]], [[0], 0, 30]],
['regression: scenario 13', [50, 40, [40, 50, 50, 10, 50, 100, 40, 20, 37, 41]], [[0, 50], 0, 90]],
['control: mixed overlaps', [10, 50, [5, 15, 3, 10, 12, 4]], [[0, 10, 30, 40], 2, None]],
['regression: watchdog exceeded', [10, 12, [5, 13, 4]], [[0, 10], 0, 22]],
['control: exact watchdog', [20, 30, [30, 5, 5, 5]], [[0, 40, 60], 1, None]],
['regression: first execution overruns', [10, 25, [30, 5]], [[0], 0, 25]],
['control: skip then drift', [10, 100, [25, 1, 1, 1, 1, 1]], [[0, 30, 40, 50], 2, None]],
['regression: 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: watchdog exceeded | [[0, 10], 0, 12] | [[0, 10], 0, 22] | Failed |
| regression: scenario 4 | [[0, 50], 0, 60] | [[0, 50], 0, 110] | Failed |
| control: mixed overlaps | [[0, 10, 30, 40], 2, None] | [[0, 10, 30, 40], 2, None] | Passed |
| control: exact watchdog | [[0, 40, 60], 1, None] | [[0, 40, 60], 1, None] | Passed |
| regression: first execution overruns | [[0], 0, 25] | [[0], 0, 25] | Passed |
| control: skip then drift | [[0, 30, 40, 50], 2, None] | [[0, 30, 40, 50], 2, None] | Passed |
| control: scenario 1 | [[0, 100, 200, 250], 2, None] | [[0, 100, 200, 250], 2, None] | Passed |
| control: scenario 2 | [[0, 100, 140], 5, None] | [[0, 100, 140], 5, None] | Passed |
SHA-256 / 5d2b49ffb2ed235e03f16c3ffe0585e162e09ceb017b40f2b4c24913310b0f03
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: watchdog exceeded', [10, 12, [5, 13, 4]], [[0, 10], 0, 22]],
['regression: scenario 4',
[50, 60, [21, 100, 100, 60, 100, 100, 60, 60, 45, 50, 100, 100]],
[[0, 50], 0, 110]],
['control: mixed overlaps', [10, 50, [5, 15, 3, 10, 12, 4]], [[0, 10, 30, 40], 2, None]],
['control: exact watchdog', [20, 30, [30, 5, 5, 5]], [[0, 40, 60], 1, None]],
['regression: first execution overruns', [10, 25, [30, 5]], [[0], 0, 25]],
['control: skip then drift', [10, 100, [25, 1, 1, 1, 1, 1]], [[0, 30, 40, 50], 2, None]],
['control: scenario 1', [50, 100, [100, 100, 100, 100, 50, 25]], [[0, 100, 200, 250], 2, None]],
['control: scenario 2', [20, 100, [100, 100, 60, 39, 20, 40, 54, 40]], [[0, 100, 140], 5, None]]],
[['regression: first execution overruns', [10, 25, [30, 5]], [[0], 0, 25]],
['regression: scenario 4', [10, 30, [20, 20, 51, 30, 26, 20, 54, 2]], [[0, 20], 1, 50]],
['control: mixed overlaps', [10, 50, [5, 15, 3, 10, 12, 4]], [[0, 10, 30, 40], 2, None]],
['regression: watchdog exceeded', [10, 12, [5, 13, 4]], [[0, 10], 0, 22]],
['control: exact watchdog', [20, 30, [30, 5, 5, 5]], [[0, 40, 60], 1, None]],
['control: skip then drift', [10, 100, [25, 1, 1, 1, 1, 1]], [[0, 30, 40, 50], 2, None]],
['control: scenario 1', [20, 60, [60, 20, 36, 40, 20]], [[0, 60], 3, None]],
['control: 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 2', [10, 40, [50, 40, 10, 40, 10, 61, 60, 40, 20, 20, 10, 10]], [[0], 0, 40]],
['regression: scenario 4', [25, 30, [30, 30, 6, 16, 50, 30]], [[0, 50, 75, 100], 1, 130]],
['control: mixed overlaps', [10, 50, [5, 15, 3, 10, 12, 4]], [[0, 10, 30, 40], 2, None]],
['regression: watchdog exceeded', [10, 12, [5, 13, 4]], [[0, 10], 0, 22]],
['control: exact watchdog', [20, 30, [30, 5, 5, 5]], [[0, 40, 60], 1, None]],
['regression: first execution overruns', [10, 25, [30, 5]], [[0], 0, 25]],
['control: skip then drift', [10, 100, [25, 1, 1, 1, 1, 1]], [[0, 30, 40, 50], 2, None]],
['control: scenario 1', [20, 100, [100, 20, 60, 100, 40, 20, 30, 12]], [[0, 100, 120], 5, 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 22', [25, 40, [25, 64, 50, 50, 40, 67, 25]], [[0, 25], 0, 65]],
['control: mixed overlaps', [10, 50, [5, 15, 3, 10, 12, 4]], [[0, 10, 30, 40], 2, None]],
['regression: watchdog exceeded', [10, 12, [5, 13, 4]], [[0, 10], 0, 22]],
['control: exact watchdog', [20, 30, [30, 5, 5, 5]], [[0, 40, 60], 1, None]],
['regression: first execution overruns', [10, 25, [30, 5]], [[0], 0, 25]],
['control: skip then drift', [10, 100, [25, 1, 1, 1, 1, 1]], [[0, 30, 40, 50], 2, None]],
['regression: scenario 1',
[20, 30, [21, 20, 20, 20, 40, 30, 20, 30, 40, 30, 40]],
[[0, 40, 60, 80], 1, 110]]],
[['regression: scenario 3', [50, 30, [50, 50, 100, 30, 25, 30, 50, 100]], [[0], 0, 30]],
['regression: scenario 13', [50, 40, [40, 50, 50, 10, 50, 100, 40, 20, 37, 41]], [[0, 50], 0, 90]],
['control: mixed overlaps', [10, 50, [5, 15, 3, 10, 12, 4]], [[0, 10, 30, 40], 2, None]],
['regression: watchdog exceeded', [10, 12, [5, 13, 4]], [[0, 10], 0, 22]],
['control: exact watchdog', [20, 30, [30, 5, 5, 5]], [[0, 40, 60], 1, None]],
['regression: first execution overruns', [10, 25, [30, 5]], [[0], 0, 25]],
['control: skip then drift', [10, 100, [25, 1, 1, 1, 1, 1]], [[0, 30, 40, 50], 2, None]],
['regression: 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: watchdog exceeded | [[0, 10], 0, 22] | [[0, 10], 0, 22] | Passed |
| regression: scenario 4 | [[0, 50], 0, 110] | [[0, 50], 0, 110] | Passed |
| control: mixed overlaps | [[0, 10, 30, 40], 2, None] | [[0, 10, 30, 40], 2, None] | Passed |
| control: exact watchdog | [[0, 40, 60], 1, None] | [[0, 40, 60], 1, None] | Passed |
| regression: first execution overruns | [[0], 0, 25] | [[0], 0, 25] | Passed |
| control: skip then drift | [[0, 30, 40, 50], 2, None] | [[0, 30, 40, 50], 2, None] | Passed |
| control: scenario 1 | [[0, 100, 200, 250], 2, None] | [[0, 100, 200, 250], 2, None] | Passed |
| control: scenario 2 | [[0, 100, 140], 5, None] | [[0, 100, 140], 5, None] | Passed |
SHA-256 / 7619cf21a96a905c75a198f108394518ac67a9d6261372371c09f555ab183710
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.328781+00:00.
Case digest / f3d5a8c46e97df068f92f588cd7caf29bb38b475c5adad34ce4be2820bef6752