FAILURE MAP
← Case archive

FA-93046 / EV charging session scheduling / Open access

Ready-by departure latest start: buffer direction · case 01

Charging is planned to finish after the driver leaves.

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

ROOT CAUSE

The pre-departure buffer is added to the departure time instead of subtracted.

VERIFIED REPAIR

Subtract the buffer from the departure time.

Unsuccessful approach: Dropping the buffer finishes exactly at departure with no margin.

Case contract

Minutes. The battery must receive need_wh; the charger draws rate_w at eff_pct percent efficiency, so duration = ceil(need_wh*6000/(rate_w*eff_pct)) minutes. Latest start = depart - buffer_min - duration, rounded down to a 15-minute boundary. If that is before now, start now with status 'late'. need_wh <= 0 returns [None, now, 'done']; rate_w or eff_pct <= 0 returns [None, None, 'no-power']. Return [start, finish, status].

Why this case matters

Depot, workplace and public EV chargers schedule sessions against prices, circuit limits and departure deadlines; a wrong decision silently strands a driver or overloads a feeder.

1 / The failure

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json

N = 1
observations = []
def solve(now, depart, need_wh, rate_w, eff_pct, buffer_min):
    if need_wh <= 0:
        return [None, now, 'done']
    if rate_w <= 0 or eff_pct <= 0:
        return [None, None, 'no-power']
    dur = -(-need_wh * 6000 // (rate_w * eff_pct))
    latest = depart + buffer_min - dur
    latest -= latest % 15
    if latest < now:
        return [now, now + dur, 'late']
    return [latest, latest + dur, 'ok']
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['boundary: nothing to charge', [100, 400, 0, 0, 90, 10], [None, 100, 'done']],
  ['boundary: latest start exactly now', [300, 390, 7400, 7400, 100, 0], [330, 390, 'ok']],
  ['boundary: no charger power', [100, 400, 5000, 0, 90, 10], [None, None, 'no-power']],
  ['regression: buffer direction', [153, 434, 2000, 11000, 85, 30], [390, 403, 'ok']],
  ['regression: buffer direction (partial repair)', [387, 1026, 30000, 11000, 95, 30],
   [810, 983, 'ok']],
  ['control 1', [117, 637, 11000, 11000, 95, 10], [555, 619, 'ok']],
  ['control 2', [448, 1142, 0, 3700, 85, 0], [None, 448, 'done']]],
 [['boundary: nothing to charge', [100, 400, 0, 0, 90, 10], [None, 100, 'done']],
  ['boundary: latest start exactly now', [300, 390, 7400, 7400, 100, 0], [330, 390, 'ok']],
  ['boundary: no charger power', [100, 400, 5000, 0, 90, 10], [None, None, 'no-power']],
  ['regression: buffer direction', [351, 1109, 18000, 3700, 95, 15], [780, 1088, 'ok']],
  ['regression: buffer direction (partial repair)', [20, 911, 30000, 7400, 100, 15],
   [645, 889, 'ok']],
  ['control 1', [416, 1003, 5000, 0, 85, 15], [None, None, 'no-power']],
  ['control 2', [221, 503, 5000, 0, 85, 30], [None, None, 'no-power']]],
 [['boundary: nothing to charge', [100, 400, 0, 0, 90, 10], [None, 100, 'done']],
  ['boundary: latest start exactly now', [300, 390, 7400, 7400, 100, 0], [330, 390, 'ok']],
  ['boundary: no charger power', [100, 400, 5000, 0, 90, 10], [None, None, 'no-power']],
  ['regression: buffer direction', [159, 777, 11000, 3700, 100, 30], [555, 734, 'ok']],
  ['regression: buffer direction (partial repair)', [12, 862, 30000, 11000, 95, 15],
   [660, 833, 'ok']],
  ['control 1', [381, 632, 18000, 3700, 95, 30], [381, 689, 'late']],
  ['control 2', [19, 584, 30000, 11000, 90, 15], [375, 557, 'ok']]],
 [['boundary: nothing to charge', [100, 400, 0, 0, 90, 10], [None, 100, 'done']],
  ['boundary: latest start exactly now', [300, 390, 7400, 7400, 100, 0], [330, 390, 'ok']],
  ['boundary: no charger power', [100, 400, 5000, 0, 90, 10], [None, None, 'no-power']],
  ['regression: buffer direction', [496, 868, 11000, 3700, 95, 30], [645, 833, 'ok']],
  ['regression: buffer direction (partial repair)', [577, 1400, 11000, 7400, 85, 15],
   [1275, 1380, 'ok']],
  ['control 1', [66, 229, 11000, 0, 90, 15], [None, None, 'no-power']],
  ['control 2', [44, 377, 2000, 0, 100, 0], [None, None, 'no-power']]],
 [['boundary: nothing to charge', [100, 400, 0, 0, 90, 10], [None, 100, 'done']],
  ['boundary: latest start exactly now', [300, 390, 7400, 7400, 100, 0], [330, 390, 'ok']],
  ['boundary: no charger power', [100, 400, 5000, 0, 90, 10], [None, None, 'no-power']],
  ['regression: buffer direction', [522, 1168, 30000, 3700, 85, 30], [555, 1128, 'ok']],
  ['regression: buffer direction (partial repair)', [546, 1243, 11000, 7400, 85, 30],
   [1095, 1200, 'ok']],
  ['control 1', [342, 397, 18000, 11000, 95, 10], [342, 446, 'late']],
  ['control 2', [577, 1400, 11000, 7400, 85, 15], [1275, 1380, 'ok']]]]
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
boundary: nothing to charge[None, 100, 'done'][None, 100, 'done']Passed
boundary: latest start exactly now[330, 390, 'ok'][330, 390, 'ok']Passed
boundary: no charger power[None, None, 'no-power'][None, None, 'no-power']Passed
regression: buffer direction[450, 463, 'ok'][390, 403, 'ok']Failed
regression: buffer direction (partial repair)[870, 1043, 'ok'][810, 983, 'ok']Failed
control 1[570, 634, 'ok'][555, 619, 'ok']Failed
control 2[None, 448, 'done'][None, 448, 'done']Passed

SHA-256 / d0c189d21951202be1a79c6a1c5b8beac7aba1f5073e58a7b49ca050079c0bd1

2 / The unsuccessful fix

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json

N = 1
observations = []
def solve(now, depart, need_wh, rate_w, eff_pct, buffer_min):
    if need_wh <= 0:
        return [None, now, 'done']
    if rate_w <= 0 or eff_pct <= 0:
        return [None, None, 'no-power']
    dur = -(-need_wh * 6000 // (rate_w * eff_pct))
    latest = depart - dur
    latest -= latest % 15
    if latest < now:
        return [now, now + dur, 'late']
    return [latest, latest + dur, 'ok']
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['boundary: nothing to charge', [100, 400, 0, 0, 90, 10], [None, 100, 'done']],
  ['boundary: latest start exactly now', [300, 390, 7400, 7400, 100, 0], [330, 390, 'ok']],
  ['boundary: no charger power', [100, 400, 5000, 0, 90, 10], [None, None, 'no-power']],
  ['regression: buffer direction', [153, 434, 2000, 11000, 85, 30], [390, 403, 'ok']],
  ['regression: buffer direction (partial repair)', [387, 1026, 30000, 11000, 95, 30],
   [810, 983, 'ok']],
  ['control 1', [117, 637, 11000, 11000, 95, 10], [555, 619, 'ok']],
  ['control 2', [448, 1142, 0, 3700, 85, 0], [None, 448, 'done']]],
 [['boundary: nothing to charge', [100, 400, 0, 0, 90, 10], [None, 100, 'done']],
  ['boundary: latest start exactly now', [300, 390, 7400, 7400, 100, 0], [330, 390, 'ok']],
  ['boundary: no charger power', [100, 400, 5000, 0, 90, 10], [None, None, 'no-power']],
  ['regression: buffer direction', [351, 1109, 18000, 3700, 95, 15], [780, 1088, 'ok']],
  ['regression: buffer direction (partial repair)', [20, 911, 30000, 7400, 100, 15],
   [645, 889, 'ok']],
  ['control 1', [416, 1003, 5000, 0, 85, 15], [None, None, 'no-power']],
  ['control 2', [221, 503, 5000, 0, 85, 30], [None, None, 'no-power']]],
 [['boundary: nothing to charge', [100, 400, 0, 0, 90, 10], [None, 100, 'done']],
  ['boundary: latest start exactly now', [300, 390, 7400, 7400, 100, 0], [330, 390, 'ok']],
  ['boundary: no charger power', [100, 400, 5000, 0, 90, 10], [None, None, 'no-power']],
  ['regression: buffer direction', [159, 777, 11000, 3700, 100, 30], [555, 734, 'ok']],
  ['regression: buffer direction (partial repair)', [12, 862, 30000, 11000, 95, 15],
   [660, 833, 'ok']],
  ['control 1', [381, 632, 18000, 3700, 95, 30], [381, 689, 'late']],
  ['control 2', [19, 584, 30000, 11000, 90, 15], [375, 557, 'ok']]],
 [['boundary: nothing to charge', [100, 400, 0, 0, 90, 10], [None, 100, 'done']],
  ['boundary: latest start exactly now', [300, 390, 7400, 7400, 100, 0], [330, 390, 'ok']],
  ['boundary: no charger power', [100, 400, 5000, 0, 90, 10], [None, None, 'no-power']],
  ['regression: buffer direction', [496, 868, 11000, 3700, 95, 30], [645, 833, 'ok']],
  ['regression: buffer direction (partial repair)', [577, 1400, 11000, 7400, 85, 15],
   [1275, 1380, 'ok']],
  ['control 1', [66, 229, 11000, 0, 90, 15], [None, None, 'no-power']],
  ['control 2', [44, 377, 2000, 0, 100, 0], [None, None, 'no-power']]],
 [['boundary: nothing to charge', [100, 400, 0, 0, 90, 10], [None, 100, 'done']],
  ['boundary: latest start exactly now', [300, 390, 7400, 7400, 100, 0], [330, 390, 'ok']],
  ['boundary: no charger power', [100, 400, 5000, 0, 90, 10], [None, None, 'no-power']],
  ['regression: buffer direction', [522, 1168, 30000, 3700, 85, 30], [555, 1128, 'ok']],
  ['regression: buffer direction (partial repair)', [546, 1243, 11000, 7400, 85, 30],
   [1095, 1200, 'ok']],
  ['control 1', [342, 397, 18000, 11000, 95, 10], [342, 446, 'late']],
  ['control 2', [577, 1400, 11000, 7400, 85, 15], [1275, 1380, 'ok']]]]
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
boundary: nothing to charge[None, 100, 'done'][None, 100, 'done']Passed
boundary: latest start exactly now[330, 390, 'ok'][330, 390, 'ok']Passed
boundary: no charger power[None, None, 'no-power'][None, None, 'no-power']Passed
regression: buffer direction[420, 433, 'ok'][390, 403, 'ok']Failed
regression: buffer direction (partial repair)[840, 1013, 'ok'][810, 983, 'ok']Failed
control 1[570, 634, 'ok'][555, 619, 'ok']Failed
control 2[None, 448, 'done'][None, 448, 'done']Passed

SHA-256 / 9a6e7fe6b3e2f03fb0826b33525d7703f5c9e3c70c92bdac493f80220388d649

3 / The verified repair

Exit 0
"""Failure Map reference implementation. Python standard library only."""
import json

N = 1
observations = []
def solve(now, depart, need_wh, rate_w, eff_pct, buffer_min):
    if need_wh <= 0:
        return [None, now, 'done']
    if rate_w <= 0 or eff_pct <= 0:
        return [None, None, 'no-power']
    dur = -(-need_wh * 6000 // (rate_w * eff_pct))
    latest = depart - buffer_min - dur
    latest -= latest % 15
    if latest < now:
        return [now, now + dur, 'late']
    return [latest, latest + dur, 'ok']
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['boundary: nothing to charge', [100, 400, 0, 0, 90, 10], [None, 100, 'done']],
  ['boundary: latest start exactly now', [300, 390, 7400, 7400, 100, 0], [330, 390, 'ok']],
  ['boundary: no charger power', [100, 400, 5000, 0, 90, 10], [None, None, 'no-power']],
  ['regression: buffer direction', [153, 434, 2000, 11000, 85, 30], [390, 403, 'ok']],
  ['regression: buffer direction (partial repair)', [387, 1026, 30000, 11000, 95, 30],
   [810, 983, 'ok']],
  ['control 1', [117, 637, 11000, 11000, 95, 10], [555, 619, 'ok']],
  ['control 2', [448, 1142, 0, 3700, 85, 0], [None, 448, 'done']]],
 [['boundary: nothing to charge', [100, 400, 0, 0, 90, 10], [None, 100, 'done']],
  ['boundary: latest start exactly now', [300, 390, 7400, 7400, 100, 0], [330, 390, 'ok']],
  ['boundary: no charger power', [100, 400, 5000, 0, 90, 10], [None, None, 'no-power']],
  ['regression: buffer direction', [351, 1109, 18000, 3700, 95, 15], [780, 1088, 'ok']],
  ['regression: buffer direction (partial repair)', [20, 911, 30000, 7400, 100, 15],
   [645, 889, 'ok']],
  ['control 1', [416, 1003, 5000, 0, 85, 15], [None, None, 'no-power']],
  ['control 2', [221, 503, 5000, 0, 85, 30], [None, None, 'no-power']]],
 [['boundary: nothing to charge', [100, 400, 0, 0, 90, 10], [None, 100, 'done']],
  ['boundary: latest start exactly now', [300, 390, 7400, 7400, 100, 0], [330, 390, 'ok']],
  ['boundary: no charger power', [100, 400, 5000, 0, 90, 10], [None, None, 'no-power']],
  ['regression: buffer direction', [159, 777, 11000, 3700, 100, 30], [555, 734, 'ok']],
  ['regression: buffer direction (partial repair)', [12, 862, 30000, 11000, 95, 15],
   [660, 833, 'ok']],
  ['control 1', [381, 632, 18000, 3700, 95, 30], [381, 689, 'late']],
  ['control 2', [19, 584, 30000, 11000, 90, 15], [375, 557, 'ok']]],
 [['boundary: nothing to charge', [100, 400, 0, 0, 90, 10], [None, 100, 'done']],
  ['boundary: latest start exactly now', [300, 390, 7400, 7400, 100, 0], [330, 390, 'ok']],
  ['boundary: no charger power', [100, 400, 5000, 0, 90, 10], [None, None, 'no-power']],
  ['regression: buffer direction', [496, 868, 11000, 3700, 95, 30], [645, 833, 'ok']],
  ['regression: buffer direction (partial repair)', [577, 1400, 11000, 7400, 85, 15],
   [1275, 1380, 'ok']],
  ['control 1', [66, 229, 11000, 0, 90, 15], [None, None, 'no-power']],
  ['control 2', [44, 377, 2000, 0, 100, 0], [None, None, 'no-power']]],
 [['boundary: nothing to charge', [100, 400, 0, 0, 90, 10], [None, 100, 'done']],
  ['boundary: latest start exactly now', [300, 390, 7400, 7400, 100, 0], [330, 390, 'ok']],
  ['boundary: no charger power', [100, 400, 5000, 0, 90, 10], [None, None, 'no-power']],
  ['regression: buffer direction', [522, 1168, 30000, 3700, 85, 30], [555, 1128, 'ok']],
  ['regression: buffer direction (partial repair)', [546, 1243, 11000, 7400, 85, 30],
   [1095, 1200, 'ok']],
  ['control 1', [342, 397, 18000, 11000, 95, 10], [342, 446, 'late']],
  ['control 2', [577, 1400, 11000, 7400, 85, 15], [1275, 1380, 'ok']]]]
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
boundary: nothing to charge[None, 100, 'done'][None, 100, 'done']Passed
boundary: latest start exactly now[330, 390, 'ok'][330, 390, 'ok']Passed
boundary: no charger power[None, None, 'no-power'][None, None, 'no-power']Passed
regression: buffer direction[390, 403, 'ok'][390, 403, 'ok']Passed
regression: buffer direction (partial repair)[810, 983, 'ok'][810, 983, 'ok']Passed
control 1[555, 619, 'ok'][555, 619, 'ok']Passed
control 2[None, 448, 'done'][None, 448, 'done']Passed

SHA-256 / 6f802d5b0650a7ae40045848e9e4b2b67c62a279ef0fbd26469352de0108c9a5

Verification & scope

Deterministic stipulated toy contract for teaching; no claim of conformance with any standard, vendor protocol or production controller. 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:51.664953+00:00.

Case digest / ceff30c581aa487824c0d2d457201dcc44347cb7a0267c0fb33703e540dd9663