FAILURE MAP
← Case archive

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.

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

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 fixtureActualExpectedOutcome
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 fixtureActualExpectedOutcome
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 fixtureActualExpectedOutcome
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