Electric Vehicles
The epl.EVs
asset is suitable for modelling electric vehicle charging, with multiple chargers and charge events.
Assumptions
Chargers are configured by their size given in charger_mws
.
A charge_event
is a time interval where an EV can be charged. This is given as a boolean 2D array, with one binary digit for each charge event, interval pairs.
Each charge event has a required amount of electricity charge_event_mwh
, that can be delivered when the charge_event
is 1. The model is constrained so that each charge event receives all of it's charge_event_mwh
.
Use
Optimize two 100 MWe chargers for 4 charge events over 5 intervals:
import energypylinear as epl
electricity_prices = [-100, 50, 30, 50, 40]
charge_events = [
[1, 0, 0, 0, 0],
[0, 1, 1, 1, 0],
[0, 0, 0, 1, 1],
[0, 1, 0, 0, 0],
]
# 2 100 MW EV chargers
asset = epl.EVs(
chargers_power_mw=[100, 100],
charge_events_capacity_mwh=[50, 100, 30, 40],
charger_turndown=0.1,
electricity_prices=electricity_prices,
charge_events=charge_events,
)
simulation = asset.optimize()
assert all(
simulation.results.columns
== [
"site-import_power_mwh",
"site-export_power_mwh",
"site-electricity_prices",
"site-electricity_carbon_intensities",
"site-high_temperature_load_mwh",
"site-low_temperature_load_mwh",
"site-low_temperature_generation_mwh",
"site-gas_prices",
"site-electric_load_mwh",
"spill-electric_generation_mwh",
"spill-electric_load_mwh",
"spill-high_temperature_generation_mwh",
"spill-low_temperature_generation_mwh",
"spill-high_temperature_load_mwh",
"spill-low_temperature_load_mwh",
"spill-gas_consumption_mwh",
"evs-charger-0-electric_charge_mwh",
"evs-charger-0-electric_charge_binary",
"evs-charger-0-electric_discharge_mwh",
"evs-charger-0-electric_discharge_binary",
"evs-charger-0-electric_loss_mwh",
"evs-charger-1-electric_charge_mwh",
"evs-charger-1-electric_charge_binary",
"evs-charger-1-electric_discharge_mwh",
"evs-charger-1-electric_discharge_binary",
"evs-charger-1-electric_loss_mwh",
"evs-charge-event-0-electric_charge_mwh",
"evs-charge-event-0-electric_discharge_mwh",
"evs-charge-event-0-electric_loss_mwh",
"evs-charge-event-1-electric_charge_mwh",
"evs-charge-event-1-electric_discharge_mwh",
"evs-charge-event-1-electric_loss_mwh",
"evs-charge-event-2-electric_charge_mwh",
"evs-charge-event-2-electric_discharge_mwh",
"evs-charge-event-2-electric_loss_mwh",
"evs-charge-event-3-electric_charge_mwh",
"evs-charge-event-3-electric_discharge_mwh",
"evs-charge-event-3-electric_loss_mwh",
"evs-charge-event-0-initial_soc_mwh",
"evs-charge-event-1-initial_soc_mwh",
"evs-charge-event-2-initial_soc_mwh",
"evs-charge-event-3-initial_soc_mwh",
"evs-charge-event-0-final_soc_mwh",
"evs-charge-event-1-final_soc_mwh",
"evs-charge-event-2-final_soc_mwh",
"evs-charge-event-3-final_soc_mwh",
"evs-charger-spill-evs-electric_charge_mwh",
"evs-charger-spill-evs-electric_charge_binary",
"evs-charger-spill-evs-electric_discharge_mwh",
"evs-charger-spill-evs-electric_discharge_binary",
"evs-charger-spill-evs-electric_loss_mwh",
"total-electric_generation_mwh",
"total-electric_load_mwh",
"total-high_temperature_generation_mwh",
"total-low_temperature_generation_mwh",
"total-high_temperature_load_mwh",
"total-low_temperature_load_mwh",
"total-gas_consumption_mwh",
"total-electric_charge_mwh",
"total-electric_discharge_mwh",
"total-spills_mwh",
"total-electric_loss_mwh",
"site-electricity_balance_mwh",
]
)
Validation
A natural response when you get access to something someone else built is to wonder - does this work correctly?
This section will give you confidence in the implementation of the EV asset.
Fully Constrained EV Charging
import energypylinear as epl
asset = epl.EVs(
chargers_power_mw=[100, 100],
charge_events_capacity_mwh=[50, 100, 30],
charger_turndown=0.0,
charge_event_efficiency=1.0,
electricity_prices=[-100, 50, 30, 10, 40],
charge_events=[
[1, 0, 0, 0, 0],
[0, 1, 0, 0, 0],
[0, 0, 1, 0, 0],
]
)
simulation = asset.optimize()
asset.plot(simulation, path="./docs/docs/static/ev-validation-1.png")
The third charger is the spill charger.
Expanding a Charge Event Window
Let's expand out the charge event window to the last three intervals for the last charge event:
import energypylinear as epl
asset = epl.EVs(
chargers_power_mw=[100, 100],
charge_events_capacity_mwh=[50, 100, 30],
charger_turndown=0.0,
charge_event_efficiency=1.0,
electricity_prices=[-100, 50, 300, 10, 40],
charge_events=[
[1, 0, 0, 0, 0],
[0, 1, 0, 0, 0],
[0, 0, 1, 1, 1],
]
)
simulation = asset.optimize()
asset.plot(simulation, path="./docs/docs/static/ev-validation-2.png")
Now we see that the charge has happened in interval 3, this is because electricity prices are lowest in this interval.
Overlapping Charge Events
When charge events overlap at low prices, both (but only two) chargers are used:
import energypylinear as epl
asset = epl.EVs(
chargers_power_mw=[100, 100],
charge_events_capacity_mwh=[50, 100, 30],
charger_turndown=0.0,
charge_event_efficiency=1.0,
electricity_prices=[-100, 50, 300, 10, 40],
charge_events=[
[1, 0, 0, 1, 0],
[0, 1, 1, 1, 1],
[0, 0, 1, 1, 1],
]
)
simulation = asset.optimize()
asset.plot(simulation, path="./docs/docs/static/ev-validation-3.png")
Adding V2G
import energypylinear as epl
asset = epl.EVs(
chargers_power_mw=[100, 100],
charge_events_capacity_mwh=[50, 100, 30],
charger_turndown=0.0,
charge_event_efficiency=1.0,
electricity_prices=[-100, 50, 300, 10, 40],
charge_events=[
[1, 0, 0, 0, 0],
[0, 1, 1, 1, 1],
[0, 0, 1, 1, 1],
],
)
simulation = asset.optimize(
flags=epl.Flags(allow_evs_discharge=True)
)
asset.plot(simulation, path="./docs/docs/static/ev-validation-4.png")
The key takeaway here is that we discharge during interval 2. All our charge events still end up at the correct state of charge at the end of the program.
Spill Chargers
import energypylinear as epl
asset = epl.EVs(
chargers_power_mw=[100, 100],
charge_events_capacity_mwh=[50, 100, 30, 500],
charger_turndown=0.0,
charge_event_efficiency=1.0,
electricity_prices=[-100, 50, 300, 10, 40],
charge_events=[
[1, 0, 0, 0, 0],
[0, 1, 1, 1, 1],
[0, 0, 1, 1, 1],
[1, 0, 0, 0, 0],
],
)
simulation = asset.optimize(
flags=epl.Flags(allow_evs_discharge=True)
)
asset.plot(simulation, path="./docs/docs/static/ev-validation-5.png")
Key takeaway here is the use of the spill charger - we have a 500 MWh charge event, but only 200 MWh of capacity. We meet the remaining demand from a spill charger.
This allows the linear program to be feasible, while communicating directly which intervals or charge events are causing the mismatch between charge event demand and spill charger capacity.