FA-93041 / EV charging session scheduling / Open access
Ready-by departure latest start: duration rounding · case 01
The session plan ends a minute before the energy is actually delivered.
ROOT CAUSE
Charging duration is floored instead of rounded up.
VERIFIED REPAIR
Round the duration up to whole minutes.
Unsuccessful approach: Nearest rounding still truncates fractional minutes below one half.
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: duration rounding', [153, 434, 2000, 11000, 85, 30], [390, 403, 'ok']],
['regression: duration rounding (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: duration rounding', [351, 1109, 18000, 3700, 95, 15], [780, 1088, 'ok']],
['regression: duration rounding (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: duration rounding', [159, 777, 11000, 3700, 100, 30], [555, 734, 'ok']],
['regression: duration rounding (partial repair)', [81, 103, 5000, 3700, 100, 10],
[81, 163, 'late']],
['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: duration rounding', [496, 868, 11000, 3700, 95, 30], [645, 833, 'ok']],
['regression: duration rounding (partial repair)', [342, 397, 18000, 11000, 95, 10],
[342, 446, 'late']],
['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: duration rounding', [522, 1168, 30000, 3700, 85, 30], [555, 1128, 'ok']],
['regression: duration rounding (partial repair)', [341, 315, 5000, 3700, 100, 30],
[341, 423, 'late']],
['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 fixture | Actual | Expected | Outcome |
|---|---|---|---|
| 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: duration rounding | [390, 402, 'ok'] | [390, 403, 'ok'] | Failed |
| regression: duration rounding (partial repair) | [810, 982, 'ok'] | [810, 983, 'ok'] | Failed |
| control 1 | [555, 618, 'ok'] | [555, 619, 'ok'] | Failed |
| control 2 | [None, 448, 'done'] | [None, 448, 'done'] | Passed |
SHA-256 / ba5393b2035dc529dcc962f5ef5f48b52043245cae991f9d74b3097df4992fdf
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 = round(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: duration rounding', [153, 434, 2000, 11000, 85, 30], [390, 403, 'ok']],
['regression: duration rounding (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: duration rounding', [351, 1109, 18000, 3700, 95, 15], [780, 1088, 'ok']],
['regression: duration rounding (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: duration rounding', [159, 777, 11000, 3700, 100, 30], [555, 734, 'ok']],
['regression: duration rounding (partial repair)', [81, 103, 5000, 3700, 100, 10],
[81, 163, 'late']],
['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: duration rounding', [496, 868, 11000, 3700, 95, 30], [645, 833, 'ok']],
['regression: duration rounding (partial repair)', [342, 397, 18000, 11000, 95, 10],
[342, 446, 'late']],
['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: duration rounding', [522, 1168, 30000, 3700, 85, 30], [555, 1128, 'ok']],
['regression: duration rounding (partial repair)', [341, 315, 5000, 3700, 100, 30],
[341, 423, 'late']],
['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 fixture | Actual | Expected | Outcome |
|---|---|---|---|
| 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: duration rounding | [390, 403, 'ok'] | [390, 403, 'ok'] | Passed |
| regression: duration rounding (partial repair) | [810, 982, 'ok'] | [810, 983, 'ok'] | Failed |
| control 1 | [555, 618, 'ok'] | [555, 619, 'ok'] | Failed |
| control 2 | [None, 448, 'done'] | [None, 448, 'done'] | Passed |
SHA-256 / 342c881ff2478717fdee3e8df377855843de2cab29a3921039412dfb4b38c2a2
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: duration rounding', [153, 434, 2000, 11000, 85, 30], [390, 403, 'ok']],
['regression: duration rounding (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: duration rounding', [351, 1109, 18000, 3700, 95, 15], [780, 1088, 'ok']],
['regression: duration rounding (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: duration rounding', [159, 777, 11000, 3700, 100, 30], [555, 734, 'ok']],
['regression: duration rounding (partial repair)', [81, 103, 5000, 3700, 100, 10],
[81, 163, 'late']],
['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: duration rounding', [496, 868, 11000, 3700, 95, 30], [645, 833, 'ok']],
['regression: duration rounding (partial repair)', [342, 397, 18000, 11000, 95, 10],
[342, 446, 'late']],
['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: duration rounding', [522, 1168, 30000, 3700, 85, 30], [555, 1128, 'ok']],
['regression: duration rounding (partial repair)', [341, 315, 5000, 3700, 100, 30],
[341, 423, 'late']],
['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 fixture | Actual | Expected | Outcome |
|---|---|---|---|
| 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: duration rounding | [390, 403, 'ok'] | [390, 403, 'ok'] | Passed |
| regression: duration rounding (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 / 1155ebc3e157395e64102ff15f69c889b8f7d861c3862e9fb0712695445b4332
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.634051+00:00.
Case digest / 041464a78440d0032f62b69da51ec916ce00d6dbf818e8213db695a7dc8c9c15