FA-93051 / EV charging session scheduling / Open access
Ready-by departure latest start: quarter-hour quantization · case 01
Planned starts snap to the next quarter hour and finish after the target time.
ROOT CAUSE
The latest start is rounded up to the grid instead of down.
VERIFIED REPAIR
Round the latest start down to a 15-minute boundary.
Unsuccessful approach: Rounding to the nearest boundary still rounds up half the time.
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: quarter-hour quantization', [153, 434, 2000, 11000, 85, 30], [390, 403, 'ok']],
['regression: quarter-hour quantization (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: quarter-hour quantization', [351, 1109, 18000, 3700, 95, 15], [780, 1088, 'ok']],
['regression: quarter-hour quantization (partial repair)', [224, 999, 2000, 11000, 100, 15],
[960, 971, '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: quarter-hour quantization', [159, 777, 11000, 3700, 100, 30], [555, 734, 'ok']],
['regression: quarter-hour quantization (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: quarter-hour quantization', [496, 868, 11000, 3700, 95, 30], [645, 833, 'ok']],
['regression: quarter-hour quantization (partial repair)', [53, 403, 11000, 3700, 90, 10],
[180, 379, '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: quarter-hour quantization', [522, 1168, 30000, 3700, 85, 30], [555, 1128, 'ok']],
['regression: quarter-hour quantization (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 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: quarter-hour quantization | [405, 418, 'ok'] | [390, 403, 'ok'] | Failed |
| regression: quarter-hour quantization (partial repair) | [825, 998, '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 / 0e1a6ccaa7a35c24f03538f2b1a97e618b2ccaa6f57598c5230a2345e4edc461
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 - buffer_min - dur
latest = round(latest / 15) * 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: quarter-hour quantization', [153, 434, 2000, 11000, 85, 30], [390, 403, 'ok']],
['regression: quarter-hour quantization (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: quarter-hour quantization', [351, 1109, 18000, 3700, 95, 15], [780, 1088, 'ok']],
['regression: quarter-hour quantization (partial repair)', [224, 999, 2000, 11000, 100, 15],
[960, 971, '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: quarter-hour quantization', [159, 777, 11000, 3700, 100, 30], [555, 734, 'ok']],
['regression: quarter-hour quantization (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: quarter-hour quantization', [496, 868, 11000, 3700, 95, 30], [645, 833, 'ok']],
['regression: quarter-hour quantization (partial repair)', [53, 403, 11000, 3700, 90, 10],
[180, 379, '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: quarter-hour quantization', [522, 1168, 30000, 3700, 85, 30], [555, 1128, 'ok']],
['regression: quarter-hour quantization (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 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: quarter-hour quantization | [390, 403, 'ok'] | [390, 403, 'ok'] | Passed |
| regression: quarter-hour quantization (partial repair) | [825, 998, '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 / ae1299ef17c3b4ba3bbfea6eab66976eb46619db1cafa8cf623bb3551736a770
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: quarter-hour quantization', [153, 434, 2000, 11000, 85, 30], [390, 403, 'ok']],
['regression: quarter-hour quantization (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: quarter-hour quantization', [351, 1109, 18000, 3700, 95, 15], [780, 1088, 'ok']],
['regression: quarter-hour quantization (partial repair)', [224, 999, 2000, 11000, 100, 15],
[960, 971, '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: quarter-hour quantization', [159, 777, 11000, 3700, 100, 30], [555, 734, 'ok']],
['regression: quarter-hour quantization (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: quarter-hour quantization', [496, 868, 11000, 3700, 95, 30], [645, 833, 'ok']],
['regression: quarter-hour quantization (partial repair)', [53, 403, 11000, 3700, 90, 10],
[180, 379, '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: quarter-hour quantization', [522, 1168, 30000, 3700, 85, 30], [555, 1128, 'ok']],
['regression: quarter-hour quantization (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 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: quarter-hour quantization | [390, 403, 'ok'] | [390, 403, 'ok'] | Passed |
| regression: quarter-hour quantization (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 / cff1b9267d292092db8f892a648fc61fb0a24d7f7c082fa555102e54c7950330
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.664857+00:00.
Case digest / f4a5efd6561faff6ebaa885a05421e701f4f766cf4110dde6a9d0e8bdb7a54d0