AWS Builder Center
Plan Like an Optimizer, Explain Like an Agent

Plan Like an Optimizer, Explain Like an Agent

How a 40-van EV depot stays under a 250 kW limit using CP-SAT, an independent validator, and an LLM that only writes the explanation.

Plan Like an Optimizer, Explain Like an Agent

Carbon-aware charging for a 40-van EV depot on AWS

Roopanshu Gupta · Team AWSome Builds · AWS x WeMakeDevs Environmental Hacks, Waste & Energy track


At 7 pm, forty delivery vans roll into a depot with ten 50 kW bays and a 250 kW grid connection. Plug them all in and the site asks for 482.6 kW, almost twice what the transformer allows, at the hour the grid is dirtiest and the tariff is highest. By morning every van still has to be at its target charge.
We built a planner that gets every van out on time, never exceeds the limit, and moves charging into the cheap, clean hours. This post covers the four design decisions behind it and the metrics we used to hold ourselves honest.




The problem has two kinds of rules

Hard rules cannot bend: the site limit, one van per bay, charging only while a van is parked, and target state of charge at departure.
Soft goals can be traded: tariff (modelled at ₹4.20 to ₹11.50 per kWh), grid carbon (modelled at 0.38 to 0.86 kg CO₂e per kWh), and the number of times a depot attendant has to walk out and move a plug.
Every decision below follows from one principle: soft goals never get to override hard rules, and nothing that decides a hard rule is allowed to be probabilistic.

Architecture


Each module has one job and a narrow interface.  synthetic_data.py  produces the fleet and the 96-interval grid profile.  simulator.py  holds the depot's state and injects disruptions.  metrics.py  scores a schedule.  agent.py  runs the re-planning loop.  main.py  exposes it all over REST. The planners and the validator sit behind plain function calls, so either planner can be swapped without touching the rest.
AWS is a boundary, not a dependency. With credentials, the service writes fleet, charger and schedule state to DynamoDB, publishes each disruption to EventBridge, pushes operational metrics to CloudWatch and asks Claude on Bedrock to write the operator-facing explanation. Without credentials, it falls back to local mode and keeps working.



Decision 1: The LLM never touches the plan


When something breaks, the agent runs a fixed pipeline: re-solve the night with CP-SAT, validate the result, compute the deltas against the current plan, and only then ask Bedrock to explain it.
The validator is a separate module that trusts nothing the solver says. It rebuilds each van's final charge from the schedule's slots and checks seven things: site limit, one van per bay, one bay per van, charger power rating, no power to faulted bays, charging only inside the dwell window, and target charge at departure. A single violation fails the plan.
The model sees numbers and writes three sentences. Its output is never never fed back into the plan into the schedule, and if Bedrock is unreachable the operator gets a templated message instead. The worst thing the LLM can do is write a poor explanation, and the operator still has to approve.
Next on our list: make a failed validation block the approve button, not just reach the explanation.

Decision 2: Two planners, because "optimal" and "dependable" are different jobs


Safe Mode (EDF)
Green Mode (CP-SAT)
Rule
Earliest departure first, then largest remaining energy; at most ⌊cap / 50⌋ bays active; vans keep their bay between intervals
Minimise weighted cost + carbon + peak, with a bonus for staying on the same bay
Solve time
milliseconds
about 5 s (time-boxed)
Extra plug-in sessions per night
14
125
Bay swaps
0
16
Cost vs unmanaged
-16.2%
-51.3%
Carbon vs unmanaged
-3.2%
-34.6%
Safe Mode is a heuristic with a simple property: it sorts by deadline and charges the vans that must leave soonest. Because the number of active bays is  floor(cap / 50) , a mid-night utility cap drop from 250 kW to 180 kW just means three bays instead of five. In our run, all 40 vans were still ready, with load held to 150 kW after 21:15.
Green Mode is the opposite trade. It pushes charging into the cleanest, cheapest hours, so it has less slack when something goes wrong, and it asks attendants to do far more work. The operator picks the mode with  POST /api/mode/switch , and the dashboard shows what each choice costs.

Decision 3: A digital twin you can break on purpose

 simulator.py  is a state machine over the fleet, the chargers and the grid. Five injectors change that state and emit an EventBridge event: charger inverter fault, utility cap drop, carbon spike, late arrival and charger restore. Every one goes through the same path the live system uses, so the demo, the tests and the real loop exercise the same code.
The fleet itself is deliberately deterministic: 40 vans in three arrival waves with fixed dwell windows and charge targets, so any number in this post can be reproduced exactly.

Decision 4: Metrics are a pure function of two schedules

 compute_impact_metrics(baseline, optimized)  takes two finished schedules and returns cost, carbon, peak, energy shifted and departure readiness. Nothing in it can be influenced by the planner or the LLM, and the same function feeds the dashboard and the CloudWatch metrics, so there is one definition of "carbon avoided".
We also size the connection itself. The sizing oracle uses CP-SAT to binary-search the smallest connection level (in 25 kW steps) that still meets every departure. Across our Loose, Base and Tight 60-night benchmark presets:
Preset
Naive FIFO needs
EDF with sticky bays needs
Loose dwell
200 kW
175 kW
Base
250 kW
200 kW
Tight turnaround
275 kW
250 kW



Results


For the 40-van night, unmanaged charging is infeasible: the validator flags 17 violations. Safe Mode is feasible, 16.2% cheaper, and plans the whole night in milliseconds. Green Mode cuts cost by 51.3% and modelled carbon by 34.6%, at the price of slack and attendant work.

What fought back

The greenest plan was the worst to operate. Green Mode produced 125 extra plug-in sessions against Safe Mode's 14. Our switching penalty discouraged moving a van between bays, but nothing discouraged pausing and resuming a session, and the optimiser happily did that to chase cheap quarter-hours.
More solver time did not help. Raising the time limit from 5 s to 20 s returned the same objective. The model, not the clock, was the limit, and that pushed us to look at the objective terms instead of buying compute.
Units matter. Carbon in kilograms and cost in rupees live on different scales, so the objective multiplies carbon by ten before weighting it, and everything is scaled to integers for CP-SAT. Getting those weights wrong silently produced plans that were "optimal" for the wrong thing.

Limits, stated plainly

  • The fleet is synthetic, and the tariff and carbon profiles are modelled. India does not publish hourly grid carbon intensity, so savings are relative to the declared profile.
  • Sizing results are empirical over our benchmark presets, not a theorem about every depot.
  • Green Mode currently reads the site limit from a constant, so mid-night cap drops are Safe Mode's job today.

What's next

Penalise session starts in the CP-SAT objective, make the site limit an input to both planners, let a failed validation block approval, warm-start CP-SAT from the EDF plan, and replace the synthetic fleet with real arrival logs.

Live demo

Credits

Team AWSome Builds: Sarthak Verma, Taksh Pal, Riddhima Bhargava and Roopanshu Gupta. Thanks to DTU and the AWS x WeMakeDevs organisers.
#AWS #Hackathon #EVCharging #Sustainability #Bedrock #SmartGrid #ElectricVehicles #DTU
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article