Load management of a single EV¶
In this tutorial we will optimize the loading of an EV.
The tutorial is set up in 5 different steps
Step 1: Plugged EV as load
Step 2: Unidircetional charging
Step 3: Free charging with PV system at work
Step 4: Fix free charging artefact and allow bidirectional use of the battery
Step 5: Variable electricity prices
Each section contains a step by step explanation of how the a management of an ev loading can be done is using oemof.solph. Additionally, the repository contains a fully functional python file of all five main steps for you to execute yourself or modify and play around with.
Step 1: Plugged EV as load¶
Within the first step we want to simulate a plugged EV as load with pre-calculated charging time series Charged EV with predefined trips for load.
First of all, we create some input data. We use Pandas to do so and will also
import matplotlib to plot the data.
Further for plotting we use a helper function from
helpers.py.
import matplotlib.pyplot as plt
import networkx as nx
import numpy as np
import pandas as pd
from helpers import plot_results
from oemof.network.graph import create_nx_graph
Now we can begin the modeling process of ev loading management. Every oemof.solph model starts be creating (also called “initializing”) the energy system.
To create an instance of the solph.EnergySystem class, we have to import the solph
package at first. Further we need a timeindex for the simulation.Within this example we will regard the first day of the year of 2025.
We will use the date_range() function from the pandas package to create a timeindex with 5 minute resolution for one day.
import oemof.solph as solph
time_index = pd.date_range(
start="2025-01-01",
end="2025-01-02",
freq="5min",
inclusive="both",
)
ev_energy_system = solph.EnergySystem(
timeindex=time_index,
infer_last_interval=False,
)
After that, we need to define the trip demand series for the real trip scenario. As the demand is a power time series, it has N-1 entries when compared to the N entires of time axis for the energy. There is a morning drive from 07:10 a.m. to 08:10 a.m.. The power of 10 kW is required. Further there is an evening drive from 4:13 p.m. to 5:45 p.m.. The power of 9 kW is required.
ev_demand = pd.Series(0, index=time_index[:-1])
driving_start_morning = pd.Timestamp("2025-01-01 07:10")
driving_end_morning = pd.Timestamp("2025-01-01 08:10")
ev_demand.loc[driving_start_morning:driving_end_morning] = 10 # kW
driving_start_evening = pd.Timestamp("2025-01-01 16:13:37")
driving_end_evening = pd.Timestamp("2025-01-01 17:45:11")
ev_demand.loc[driving_start_evening:driving_end_evening] = 9 # kW
Note
Keep in mind that the units used in your energy system model are only implicit and that you have to check their consistency yourself.
Lets look at the driving pattern
plt.figure()
# plt.style.use("dark_background")
plt.title("Driving pattern")
plt.plot(ev_demand)
plt.ylabel("Power (kW)")
plt.gcf().autofmt_xdate()
Fig. 19 Driving pattern¶
Now we need to set up the electric energy carrying bus. We make sure to set a label to reference them later when we
analyze the results. After initialization, we add them to the ev_energy_system object.
bus_car = solph.Bus(label="Car Electricity")
ev_energy_system.add(bus_car)
After setting up the energy system and the buses, we can now add the components to the energy system.
We adding the driving demand as solph.components.Sink, where the loading profile is added as
fix and nominal_capacity is set to one, because the loading profile is absolute.
As we have a demand time series which is actually in kW, we use a common
“hack” here: We set the nominal capacity to 1 (kW), so that
multiplication by the time series will just yield the correct result.
The driving demand input is connected with the the electric energy carrying bus.
The car battery is added as solph.components.GenericStorage.
The following parameters are set:
nominal_capacityis set to 50 (kWh), which is the capacity of the battery.capacity_lossis set to 0.001. This means the battery loss per hour is 0.1% percent.initial_capacityis set to 1. This indicates that the battery is full at the beginning of the simulation.inflow_conversion_factoris set to 0.9, so the charging efficency is 90%.balancedis set to False. This means the battery storage level at the end has not to be the same as at the beginning.storage_costsis set to the defined storage revenue. Where “storage revenue” is defined as list with negative costs (of 60 ct/kWh) for the last time step, so that energy inside the storage in the last time step is worth something.
This leads to the fact that the battery is not necessary emptied at the end of the simulation.
The car battery inputs and outputs are connected with the the electric energy carrying bus.
demand_driving = solph.components.Sink(
label="Driving Demand",
inputs={bus_car: solph.Flow(nominal_capacity=1, fix=ev_demand)},
)
ev_energy_system.add(demand_driving)
storage_revenue = np.zeros(len(time_index) - 1)
storage_revenue[-1] = -0.6 # 60 ct/kWh in the last time step
car_battery = solph.components.GenericStorage(
label="Car Battery",
nominal_capacity=50, # kWh
inputs={bus_car: solph.Flow()},
outputs={bus_car: solph.Flow()},
initial_storage_level=1, # full in the beginning
loss_rate=0.001, # 0.1 % / hr
inflow_conversion_factor=0.9, # 90 % charging efficiency
balanced=False, # True: content at beginning and end need to be equal
storage_costs=storage_revenue, # Only has an effect on charging.
)
ev_energy_system.add(car_battery)
As our system is complete for this step. Before we start the unit commitment optimization, let us have a look at the energy system graph
plt.figure()
graph = create_nx_graph(ev_energy_system)
nx.draw(graph, with_labels=True, font_size=8)
While nx.draw is handy to just have a quick look without too many extra tools, writing the graph dot allows for handling in specialised programs. The folloing has been created using
For the actual optimisation, we first have to create a solph.Model
instance from our ev_energy_system. Then we can use its solve()
method to run the optimization.
We decide to use the open source solver CBC and add the additional
solve_kwargs parameter 'tee' to True, in order to
get a more verbose solver logging output in the console.
model = solph.Model(ev_energy_system)
results = model.solve(solver="cbc", solve_kwargs={"tee": True})
Note
Optimisation will fail if supply and demand cannot be balanced. You can try this by setting initial_storage_level=0.
Now plot the results using the helper function from helpers.py.
The results are showing that the EV is using the battery while driving.
plot_results(
results=results, plot_title="Driving demand only", dark_mode=False
)
plt.show()
Fig. 20 Driving pattern¶
Learning
The model balances supply and demand along flows in a graph based model. The operation is optimised so that the total costs are minimised.
You can get the complete (uncommented) code for this step:
ev_charging_1.py
Click to display the code
# %%[imports_start]
import matplotlib.pyplot as plt
import networkx as nx
import numpy as np
import pandas as pd
from helpers import plot_results
from oemof.network.graph import create_nx_graph
# %%[imports_end]
# %%[create_time_index_set_up_energysystem_start]
import oemof.solph as solph
time_index = pd.date_range(
start="2025-01-01",
end="2025-01-02",
freq="5min",
inclusive="both",
)
ev_energy_system = solph.EnergySystem(
timeindex=time_index,
infer_last_interval=False,
)
# %%[create_time_index_set_up_energysystem_end]
# %%[trip_data_start]
ev_demand = pd.Series(0, index=time_index[:-1])
driving_start_morning = pd.Timestamp("2025-01-01 07:10")
driving_end_morning = pd.Timestamp("2025-01-01 08:10")
ev_demand.loc[driving_start_morning:driving_end_morning] = 10 # kW
driving_start_evening = pd.Timestamp("2025-01-01 16:13:37")
driving_end_evening = pd.Timestamp("2025-01-01 17:45:11")
ev_demand.loc[driving_start_evening:driving_end_evening] = 9 # kW
# %%[trip_data_end]
## %%[plot_trip_data_start]
plt.figure()
# plt.style.use("dark_background")
plt.title("Driving pattern")
plt.plot(ev_demand)
plt.ylabel("Power (kW)")
plt.gcf().autofmt_xdate()
## %%[plot_trip_data_end]
# %%[energysystem_and_bus_start]
bus_car = solph.Bus(label="Car Electricity")
ev_energy_system.add(bus_car)
# %%[energysystem_and_bus_end]
# %%[car_start]
demand_driving = solph.components.Sink(
label="Driving Demand",
inputs={bus_car: solph.Flow(nominal_capacity=1, fix=ev_demand)},
)
ev_energy_system.add(demand_driving)
storage_revenue = np.zeros(len(time_index) - 1)
storage_revenue[-1] = -0.6 # 60 ct/kWh in the last time step
car_battery = solph.components.GenericStorage(
label="Car Battery",
nominal_capacity=50, # kWh
inputs={bus_car: solph.Flow()},
outputs={bus_car: solph.Flow()},
initial_storage_level=1, # full in the beginning
loss_rate=0.001, # 0.1 % / hr
inflow_conversion_factor=0.9, # 90 % charging efficiency
balanced=False, # True: content at beginning and end need to be equal
storage_costs=storage_revenue, # Only has an effect on charging.
)
ev_energy_system.add(car_battery)
# %%[car_end]
# %%[graph_start]
plt.figure()
graph = create_nx_graph(ev_energy_system)
nx.draw(graph, with_labels=True, font_size=8)
# %%[graph_end]
# %%[solve_start]
model = solph.Model(ev_energy_system)
results = model.solve(solver="cbc", solve_kwargs={"tee": True})
# %%[solve_end]
# %%[plot_results_start]
plot_results(
results=results, plot_title="Driving demand only", dark_mode=False
)
plt.show()
# %%[plot_results_end]
Step 2: Plugged EV as load¶
Now, let’s assume the car battery can be charged at home. To be able to load the battery a charger is necessary. Unfortunately, there is only a power socket available, limiting the charging process to 16 A at 230 V. The costs for charging are 30 ct/kWh.
Note
Costs in the model are understood as a mathematical term, i.e. you could minimise emissions by choosing 363 g/kWh (German average for 2024) instead of the monetary costs.
So the charging is added as solph.components.Sink,
where the nominal_capacity is set to 3.68 kW (= 230 V * 16 A) and variable_costs are set to 0.3 (30 ct/kWh).
This, of course, can only happen while the car is present at home.
To connect the car while it is at home, we create avalibility data series.
The value 1 means that the car is at home, chargable.
When it is set to 0, car is not present
(between morning departure and the arrival back home in the evening).
The timeseries is set as max.
car_at_home = pd.Series(1, index=time_index[:-1])
car_at_home.loc[driving_start_morning:driving_end_evening] = 0
charger230V = solph.components.Source(
label="230V AC",
outputs={
bus_car: solph.Flow(
nominal_capacity=3.68, # 230 V * 16 A = 3.68 kW
variable_costs=0.3, # 30 ct/kWh
maximum=car_at_home,
)
},
)
ev_energy_system.add(charger230V)
Now we are looking at the results: The EV will be charged as much as possible. As it’s not possible to load it completely in the afternoon, it is charged to 100 % just before the first leaving.
Learning
Optimisation is carried out under perfect foresight, meaning that things that happen later can imply things earlier in the time horison.
You can get the complete (uncommented) code for this step:
ev_charging_2.py
Click to display the code
# %%[imports_start]
import matplotlib.pyplot as plt
import networkx as nx
import numpy as np
import pandas as pd
from helpers import plot_results
from oemof.network.graph import create_nx_graph
# %%[imports_end]
# %%[create_time_index_set_up_energysystem_start]
import oemof.solph as solph
time_index = pd.date_range(
start="2025-01-01",
end="2025-01-02",
freq="5min",
inclusive="both",
)
ev_energy_system = solph.EnergySystem(
timeindex=time_index,
infer_last_interval=False,
)
# %%[create_time_index_set_up_energysystem_end]
# %%[trip_data_start]
ev_demand = pd.Series(0, index=time_index[:-1])
driving_start_morning = pd.Timestamp("2025-01-01 07:10")
driving_end_morning = pd.Timestamp("2025-01-01 08:10")
ev_demand.loc[driving_start_morning:driving_end_morning] = 10 # kW
driving_start_evening = pd.Timestamp("2025-01-01 16:13:37")
driving_end_evening = pd.Timestamp("2025-01-01 17:45:11")
ev_demand.loc[driving_start_evening:driving_end_evening] = 9 # kW
# %%[trip_data_end]
## %%[plot_trip_data_start]
plt.figure()
# plt.style.use("dark_background")
plt.title("Driving pattern")
plt.plot(ev_demand)
plt.ylabel("Power (kW)")
plt.gcf().autofmt_xdate()
## %%[plot_trip_data_end]
# %%[energysystem_and_bus_start]
bus_car = solph.Bus(label="Car Electricity")
ev_energy_system.add(bus_car)
# %%[energysystem_and_bus_end]
# %%[car_start]
demand_driving = solph.components.Sink(
label="Driving Demand",
inputs={bus_car: solph.Flow(nominal_capacity=1, fix=ev_demand)},
)
ev_energy_system.add(demand_driving)
storage_revenue = np.zeros(len(time_index) - 1)
storage_revenue[-1] = -0.6 # 60 ct/kWh in the last time step
car_battery = solph.components.GenericStorage(
label="Car Battery",
nominal_capacity=50, # kWh
inputs={bus_car: solph.Flow()},
outputs={bus_car: solph.Flow()},
initial_storage_level=1, # full in the beginning
loss_rate=0.001, # 0.1 % / hr
inflow_conversion_factor=0.9, # 90 % charging efficiency
balanced=False, # True: content at beginning and end need to be equal
storage_costs=storage_revenue, # Only has an effect on charging.
)
ev_energy_system.add(car_battery)
# %%[car_end]
# %%[AC_30ct_charging_start]
car_at_home = pd.Series(1, index=time_index[:-1])
car_at_home.loc[driving_start_morning:driving_end_evening] = 0
charger230V = solph.components.Source(
label="230V AC",
outputs={
bus_car: solph.Flow(
nominal_capacity=3.68, # 230 V * 16 A = 3.68 kW
variable_costs=0.3, # 30 ct/kWh
maximum=car_at_home,
)
},
)
ev_energy_system.add(charger230V)
# %%[AC_30ct_charging_end]
# %%[graph_start]
plt.figure()
graph = create_nx_graph(ev_energy_system)
nx.draw(graph, with_labels=True, font_size=8)
# %%[graph_end]
# %%[solve_start]# %%[solve_start]
model = solph.Model(ev_energy_system)
results = model.solve(solver="cbc", solve_kwargs={"tee": True})
# %%[solve_end]
# %%[plot_results_start]
plot_results(
results=results,
plot_title="Domestic power socket charging",
dark_mode=False,
)
plt.show()
# %%[plot_results_end]
Step 3: Free charging with PV system at work¶
Within this step we are regarding a free charging option at work. So we add an 11 kW charger (free of charge) which is available at work. This, of course, can only happen while the car is present at work. Same with avalibility data at home charging, we will create a data set for avalibility at work. When it is set to 0, car is not present at the work. When it is set to 1, car is able to connect to work charge station (between morning arrival and the evening departure from work).
car_at_work = pd.Series(0, index=time_index[:-1])
car_at_work.loc[driving_end_morning:driving_start_evening] = 1
# variable_costs in the Flow default to 0, so it's free
charger11kW = solph.components.Source(
label="11kW",
outputs={
bus_car: solph.Flow(
nominal_capacity=11, # 11 kW
maximum=car_at_work,
)
},
)
ev_energy_system.add(charger11kW)
Looking at the results we see the battery is charging and discharing at the same time within the beginning. Charging and discharging at the same time is almost always a sign that something is not moddeled accurately in the energy system. A possible solution will be introducted within the next step.
Further we can see, the battery is charged when the car is at work, because the charging is free.
Learning
Among multiple optimal solutions, any one can be your results. If something is free in the model, this can include unintuitive things.
You can get the complete (uncommented) code for this step:
ev_charging_3.py
Click to display the code
# %%[imports_start]
import matplotlib.pyplot as plt
import networkx as nx
import numpy as np
import pandas as pd
from helpers import plot_results
from oemof.network.graph import create_nx_graph
# %%[imports_end]
# %%[create_time_index_set_up_energysystem_start]
import oemof.solph as solph
time_index = pd.date_range(
start="2025-01-01",
end="2025-01-02",
freq="5min",
inclusive="both",
)
ev_energy_system = solph.EnergySystem(
timeindex=time_index,
infer_last_interval=False,
)
# %%[create_time_index_set_up_energysystem_end]
# %%[trip_data_start]
ev_demand = pd.Series(0, index=time_index[:-1])
driving_start_morning = pd.Timestamp("2025-01-01 07:10")
driving_end_morning = pd.Timestamp("2025-01-01 08:10")
ev_demand.loc[driving_start_morning:driving_end_morning] = 10 # kW
driving_start_evening = pd.Timestamp("2025-01-01 16:13:37")
driving_end_evening = pd.Timestamp("2025-01-01 17:45:11")
ev_demand.loc[driving_start_evening:driving_end_evening] = 9 # kW
# %%[trip_data_end]
## %%[plot_trip_data_start]
plt.figure()
# plt.style.use("dark_background")
plt.title("Driving pattern")
plt.plot(ev_demand)
plt.ylabel("Power (kW)")
plt.gcf().autofmt_xdate()
## %%[plot_trip_data_end]
# %%[energysystem_and_bus_start]
bus_car = solph.Bus(label="Car Electricity")
ev_energy_system.add(bus_car)
# %%[energysystem_and_bus_end]
# %%[car_start]
demand_driving = solph.components.Sink(
label="Driving Demand",
inputs={bus_car: solph.Flow(nominal_capacity=1, fix=ev_demand)},
)
ev_energy_system.add(demand_driving)
storage_revenue = np.zeros(len(time_index) - 1)
storage_revenue[-1] = -0.6 # 60 ct/kWh in the last time step
car_battery = solph.components.GenericStorage(
label="Car Battery",
nominal_capacity=50, # kWh
inputs={bus_car: solph.Flow()},
outputs={bus_car: solph.Flow()},
initial_storage_level=1, # full in the beginning
loss_rate=0.001, # 0.1 % / hr
inflow_conversion_factor=0.9, # 90 % charging efficiency
balanced=False, # True: content at beginning and end need to be equal
storage_costs=storage_revenue, # Only has an effect on charging.
)
ev_energy_system.add(car_battery)
# %%[car_end]
# %%[AC_30ct_charging_start]
car_at_home = pd.Series(1, index=time_index[:-1])
car_at_home.loc[driving_start_morning:driving_end_evening] = 0
# To be able to load the battery a electric source e.g. electric grid is
# necessary. We set the maximum use to 1 if the car is present, while it
# is 0 between the morning start and the evening arrival back home.
# While the car itself can potentially charge with at a higher power,
# we just add an AC source with 16 A at 230 V.
charger230V = solph.components.Source(
label="230V AC",
outputs={
bus_car: solph.Flow(
nominal_capacity=3.68, # 230 V * 16 A = 3.68 kW
variable_costs=0.3, # 30 ct/kWh
maximum=car_at_home,
)
},
)
ev_energy_system.add(charger230V)
# %%[AC_30ct_charging_end]
# %%[DC_charging_start]
car_at_work = pd.Series(0, index=time_index[:-1])
car_at_work.loc[driving_end_morning:driving_start_evening] = 1
# variable_costs in the Flow default to 0, so it's free
charger11kW = solph.components.Source(
label="11kW",
outputs={
bus_car: solph.Flow(
nominal_capacity=11, # 11 kW
maximum=car_at_work,
)
},
)
ev_energy_system.add(charger11kW)
# %%[DC_charging_end]
# %%[graph_start]
plt.figure()
graph = create_nx_graph(ev_energy_system)
nx.draw(graph, with_labels=True, font_size=8)
# %%[graph_end]
# %%[solve_start]
# %%[solve_start]
model = solph.Model(ev_energy_system)
results = model.solve(solver="cbc", solve_kwargs={"tee": True})
# %%[solve_end]
# %%[plot_results_start]
plot_results(
results=results,
plot_title="Home and work charging",
dark_mode=False,
)
plt.show()
# %%[plot_results_end]
Step 4: Fix free charging artefact and allow bidirectional use of the battery¶
To avoid the energy from looping in the battery, we introduce costs
to battery charging. This is a way to model cyclic aging of the battery.
This is done by adding some variable_costs on flow of to the input.
Further in this step we want to allow the bidirectional use of the battery.
So we are setting the balanced to the default True.
This means the battery storage level at the end has to be the same as at the beginning.
We also have to remove the initial_capacity, so SOC(T=0) = SOC(T=T_max) is valid.
To ensure to have a reserve of 10 % within the battery, the min_storage_level is set to 0.1.
car_battery = solph.components.GenericStorage(
label="Car Battery",
nominal_capacity=50,
inputs={bus_car: solph.Flow(variable_costs=0.1)},
outputs={bus_car: solph.Flow()},
loss_rate=0.001,
inflow_conversion_factor=0.9,
balanced=True, # this is the default: SOC(T=0) = SOC(T=T_max)
min_storage_level=0.1, # 10 % as reserve
)
ev_energy_system.add(car_battery)
To be able to discharge the battery we need a discharger, which will be added as solph.components.Sink.
The nominal_capacity is set to 3.68 kW (= 230 V * 16 A) and
variable_costs are set to -0.3 to save 30 ct/kWh.
The battery can only be discharged is the car is at home,
so the created timeseries from above is used and set to max.
discharger230V = solph.components.Sink(
label="230V AC discharge",
inputs={
bus_car: solph.Flow(
nominal_capacity=3.68, # 230 V * 16 A = 3.68 kW
variable_costs=-0.3,
maximum=car_at_home,
)
},
)
ev_energy_system.add(discharger230V)
The final energy system graph now looks like this:
Looking at the results:
The charging and discharing within the beginning does not accure anymore
The battery will be loaded for free within the working period
There is a discharging at home at the evening to save money
Learning
Looped energy flows can be used as an indicator for a flawed model. If possible, it should be fixed (instead of suppressed).
You can get the complete (uncommented) code for this step:
ev_charging_4.py
Click to display the code
# %%[imports_start]
import matplotlib.pyplot as plt
import networkx as nx
import pandas as pd
from helpers import plot_results
from oemof.network.graph import create_nx_graph
# %%[imports_end]
# %%[create_time_index_set_up_energysystem_start]
import oemof.solph as solph
time_index = pd.date_range(
start="2025-01-01",
end="2025-01-02",
freq="5min",
inclusive="both",
)
ev_energy_system = solph.EnergySystem(
timeindex=time_index,
infer_last_interval=False,
)
# %%[create_time_index_set_up_energysystem_end]
# %%[trip_data_start]
ev_demand = pd.Series(0, index=time_index[:-1])
driving_start_morning = pd.Timestamp("2025-01-01 07:10")
driving_end_morning = pd.Timestamp("2025-01-01 08:10")
ev_demand.loc[driving_start_morning:driving_end_morning] = 10 # kW
driving_start_evening = pd.Timestamp("2025-01-01 16:13:37")
driving_end_evening = pd.Timestamp("2025-01-01 17:45:11")
ev_demand.loc[driving_start_evening:driving_end_evening] = 9 # kW
# %%[trip_data_end]
## %%[plot_trip_data_start]
plt.figure()
# plt.style.use("dark_background")
plt.title("Driving pattern")
plt.plot(ev_demand)
plt.ylabel("Power (kW)")
plt.gcf().autofmt_xdate()
## %%[plot_trip_data_end]
# %%[energysystem_and_bus_start]
bus_car = solph.Bus(label="Car Electricity")
ev_energy_system.add(bus_car)
# %%[energysystem_and_bus_end]
# %%[car_start]
demand_driving = solph.components.Sink(
label="Driving Demand",
inputs={bus_car: solph.Flow(nominal_capacity=1, fix=ev_demand)},
)
ev_energy_system.add(demand_driving)
# %%[car_end]
# %%[car_battery_start]
car_battery = solph.components.GenericStorage(
label="Car Battery",
nominal_capacity=50,
inputs={bus_car: solph.Flow(variable_costs=0.1)},
outputs={bus_car: solph.Flow()},
loss_rate=0.001,
inflow_conversion_factor=0.9,
balanced=True, # this is the default: SOC(T=0) = SOC(T=T_max)
min_storage_level=0.1, # 10 % as reserve
)
ev_energy_system.add(car_battery)
# %%[car_battery_end]
# %%[AC_30ct_charging_start]
car_at_home = pd.Series(1, index=time_index[:-1])
car_at_home.loc[driving_start_morning:driving_end_evening] = 0
charger230V = solph.components.Source(
label="230V AC",
outputs={
bus_car: solph.Flow(
nominal_capacity=3.68, # 230 V * 16 A = 3.68 kW
variable_costs=0.3, # 30 ct/kWh
maximum=car_at_home,
)
},
)
ev_energy_system.add(charger230V)
# %%[AC_30ct_charging_end]
# %%[DC_charging_start]
car_at_work = pd.Series(0, index=time_index[:-1])
car_at_work.loc[driving_end_morning:driving_start_evening] = 1
charger11kW = solph.components.Source(
label="11kW",
outputs={
bus_car: solph.Flow(
nominal_capacity=11, # 11 kW
maximum=car_at_work,
)
},
)
ev_energy_system.add(charger11kW)
# %%[DC_charging_end]
# %%[AC_discharging_start]
discharger230V = solph.components.Sink(
label="230V AC discharge",
inputs={
bus_car: solph.Flow(
nominal_capacity=3.68, # 230 V * 16 A = 3.68 kW
variable_costs=-0.3,
maximum=car_at_home,
)
},
)
ev_energy_system.add(discharger230V)
# %%[AC_discharging_end]
# %%[graph_start]
plt.figure()
graph = create_nx_graph(ev_energy_system)
nx.draw(graph, with_labels=True, font_size=8)
# %%[graph_end]
# %%[solve_start]
# %%[solve_and_plot_start]
model = solph.Model(ev_energy_system)
results = model.solve(solver="cbc", solve_kwargs={"tee": False})
plot_results(results=results, plot_title="Bidirectional use constant costs")
plt.show()
# %%[solve_and_plot_end]
Step 5: Variable electricity prices¶
Within the last step we want to regard dynamic prices for the the charging and discharging at home. So the optimization is going to load when the prices are are load and discharge if the prices are high. Assuming the following prices:
Before 6 a.m.: 5 ct/kWh
After 4 p.m. 70 ct/kWh
Otherwise: 50 ct/kWh
dynamic_price = pd.Series(0.5, index=time_index[:-1])
dynamic_price.loc[: pd.Timestamp("2025-01-01 06:00")] = 0.05
dynamic_price.loc[
pd.Timestamp("2025-01-01 06:00") : pd.Timestamp("2025-01-01 10:00")
] = 0.5
dynamic_price.loc[pd.Timestamp("2025-01-01 16:00") :] = 0.7
Lets have a look on the dynamic prices
The prices have to be set within the charger and discharger instead of the 30 ct/kWh before,
we set the variable_costs to the dynamic price or rather to the negative dynamic price.
charger230V = solph.components.Source(
label="230V AC",
outputs={
bus_car: solph.Flow(
nominal_capacity=3.68, # 230 V * 16 A = 3.68 kW
variable_costs=dynamic_price,
maximum=car_at_home,
)
},
)
discharger230V = solph.components.Sink(
label="230V AC discharge",
inputs={
bus_car: solph.Flow(
nominal_capacity=3.68, # 230 V * 16 A = 3.68 kW
variable_costs=-dynamic_price,
maximum=car_at_home,
)
},
)
ev_energy_system.add(charger230V, discharger230V)
Looking at the results the battery is loaded before 6 a.m. with the cheap price of 5 ct/kWh right before the first leaving to get 50 ct/kWh. The battery is recharged for free at the work and in the evening discharged to get 70 ct/kWh.
Learning
Costs can be time-dependent. The optimal operation can be changed this way if the model includes a storage.
You can get the complete (uncommented) code for this step:
ev_charging_5.py
Click to display the code
# %%[imports_start]
import matplotlib.pyplot as plt
import networkx as nx
import pandas as pd
from helpers import plot_results
from oemof.network.graph import create_nx_graph
# %%[imports_end]
# %%[create_time_index_set_up_energysystem_start]
import oemof.solph as solph
time_index = pd.date_range(
start="2025-01-01",
end="2025-01-02",
freq="5min",
inclusive="both",
)
ev_energy_system = solph.EnergySystem(
timeindex=time_index,
infer_last_interval=False,
)
# %%[create_time_index_set_up_energysystem_end]
# %%[trip_data_start]
ev_demand = pd.Series(0, index=time_index[:-1])
driving_start_morning = pd.Timestamp("2025-01-01 07:10")
driving_end_morning = pd.Timestamp("2025-01-01 08:10")
ev_demand.loc[driving_start_morning:driving_end_morning] = 10 # kW
driving_start_evening = pd.Timestamp("2025-01-01 16:13:37")
driving_end_evening = pd.Timestamp("2025-01-01 17:45:11")
ev_demand.loc[driving_start_evening:driving_end_evening] = 9 # kW
# %%[trip_data_end]
## %%[plot_trip_data_start]
plt.figure()
# plt.style.use("dark_background")
plt.title("Driving pattern")
plt.plot(ev_demand)
plt.ylabel("Power (kW)")
plt.gcf().autofmt_xdate()
## %%[plot_trip_data_end]
# %%[energysystem_and_bus_start]
bus_car = solph.Bus(label="Car Electricity")
ev_energy_system.add(bus_car)
# %%[energysystem_and_bus_end]
# %%[car_start]
demand_driving = solph.components.Sink(
label="Driving Demand",
inputs={bus_car: solph.Flow(nominal_capacity=1, fix=ev_demand)},
)
ev_energy_system.add(demand_driving)
# %%[car_end]
# %%[car_battery_start]
car_battery = solph.components.GenericStorage(
label="Car Battery",
nominal_capacity=50,
inputs={bus_car: solph.Flow(variable_costs=0.1)},
outputs={bus_car: solph.Flow()},
loss_rate=0.001,
inflow_conversion_factor=0.9,
balanced=True, # this is the default: SOC(T=0) = SOC(T=T_max)
min_storage_level=0.1, # 10 % as reserve
)
ev_energy_system.add(car_battery)
# %%[car_battery_end]
# %%[AC_dynamic_price_start]
dynamic_price = pd.Series(0.5, index=time_index[:-1])
dynamic_price.loc[: pd.Timestamp("2025-01-01 06:00")] = 0.05
dynamic_price.loc[
pd.Timestamp("2025-01-01 06:00") : pd.Timestamp("2025-01-01 10:00")
] = 0.5
dynamic_price.loc[pd.Timestamp("2025-01-01 16:00") :] = 0.7
# %%[AC_dynamic_price_end]
## %%[plot_dynamic_price_start]
plt.figure()
# plt.style.use("dark_background")
plt.title("Dynamic prices")
plt.plot(dynamic_price)
plt.ylabel("€/MWh")
plt.gcf().autofmt_xdate()
## %%[plot_dynamic_price_end]
# %%[Car_at_home_start]
car_at_home = pd.Series(1, index=time_index[:-1])
car_at_home.loc[driving_start_morning:driving_end_evening] = 0
# %%[Car_at_home_end]
# %%[Charging_discharging_with_dynamic_prices_start]
charger230V = solph.components.Source(
label="230V AC",
outputs={
bus_car: solph.Flow(
nominal_capacity=3.68, # 230 V * 16 A = 3.68 kW
variable_costs=dynamic_price,
maximum=car_at_home,
)
},
)
discharger230V = solph.components.Sink(
label="230V AC discharge",
inputs={
bus_car: solph.Flow(
nominal_capacity=3.68, # 230 V * 16 A = 3.68 kW
variable_costs=-dynamic_price,
maximum=car_at_home,
)
},
)
ev_energy_system.add(charger230V, discharger230V)
# %%[Charging_discharging_with_dynamic_prices_end]
# %%[DC_charging_start]
"""
Now, we add an 11 kW charger (free of charge) which is available at work.
This, of course, can only happen while the car is present at work.
"""
car_at_work = pd.Series(0, index=time_index[:-1])
car_at_work.loc[driving_end_morning:driving_start_evening] = 1
# variable_costs in the Flow default to 0, so it's free
charger11kW = solph.components.Source(
label="11kW",
outputs={
bus_car: solph.Flow(
nominal_capacity=11, # 11 kW
maximum=car_at_work,
)
},
)
ev_energy_system.add(charger11kW)
# %%[DC_charging_end]
# %%[graph_start]
plt.figure()
graph = create_nx_graph(ev_energy_system)
nx.draw(graph, with_labels=True, font_size=8)
# %%[graph_end]
# %%[solve_and_plot_start]
model = solph.Model(ev_energy_system)
results = model.solve()
plot_results(
results=results,
plot_title="Bidirectional use dynamic prices",
dark_mode=False,
)
plt.show()
# %%[solve_and_plot_end]