Electric Vehicles¶
In this example, a battery electric vehicle (BEV) is driven 100 km in the morning and 100 km in the evening, to simulate commuting, and charged during the day by a solar panel at the driver's place of work. The size of the panel is computed by the optimisation.
The BEV has a battery of size 100 kWh and an electricity consumption of 0.18 kWh/km.
This example will use units of kW and kWh, unlike the PyPSA defaults. This is unproblematic as long as no power flow simulations are performed.
import matplotlib.pyplot as plt
import pandas as pd
import pypsa
As time index, we use a 24 hour period.
index = pd.date_range("2016-01-01 00:00", "2016-01-01 23:00", freq="h")
The consumption pattern in kW of the BEV is defined as follows
bev_usage = pd.Series([0] * 7 + [9] * 2 + [0] * 8 + [9] * 2 + [0] * 5, index)
The capacity factor profile of the solar panel in per-unit of capacity is given by:
pv_pu = pd.Series(
[0.0] * 7
+ [0.2, 0.4, 0.6, 0.75, 0.85, 0.9, 0.85, 0.75, 0.6, 0.4, 0.2, 0.1]
+ [0.0] * 5,
index,
)
The availability of charging - i.e. only when parked at office - is constrained as follows:
charger_p_max_pu = pd.Series(0, index=index)
charger_p_max_pu["2016-01-01 09:00":"2016-01-01 16:00"] = 1
Together, this gives:
df = pd.concat({"BEV": bev_usage, "PV": pv_pu, "Charger": charger_p_max_pu}, axis=1)
df.plot.area(subplots=True);
Now initialise the network and add the relevant components. Then optimise:
n = pypsa.Network()
n.set_snapshots(index)
n.add("Bus", "place of work")
n.add("Bus", "car battery")
n.add(
"Generator",
"PV panel",
bus="place of work",
p_nom_extendable=True,
p_max_pu=pv_pu,
capital_cost=1000, # dummy cost value
)
n.add("Load", "driving", bus="car battery", p_set=bev_usage)
n.add(
"Link",
"charger",
bus0="place of work",
bus1="car battery",
p_nom=120,
p_max_pu=charger_p_max_pu,
efficiency=0.9,
)
n.add("Store", "battery", bus="car battery", e_cyclic=True, e_nom=100);
n.optimize()
print("Objective:", n.objective)
/tmp/ipykernel_3413/1147324918.py:1: FutureWarning: The default value of `include_objective_constant` will change from True to False in version 2.0. Set `include_objective_constant` explicitly to suppress this warning. Using False improves LP numerical conditioning by not including the objective constant as a variable. n.optimize() WARNING:pypsa.consistency:The following buses have carriers which are not defined. Run n.sanitize() to add them. Components with undefined carriers: Index(['place of work', 'car battery'], dtype='object', name='name')
WARNING:pypsa.consistency:The following links have carriers which are not defined. Run n.sanitize() to add them. Components with undefined carriers: Index(['charger'], dtype='object', name='name')
WARNING:pypsa.consistency:The following stores have carriers which are not defined. Run n.sanitize() to add them. Components with undefined carriers: Index(['battery'], dtype='object', name='name')
INFO:linopy.model: Solve problem using Highs solver
INFO:linopy.model:Solver options: - log_to_console: False
INFO:linopy.io: Writing time: 0.05s
INFO:linopy.constants: Optimization successful: Status: ok Termination condition: optimal Solution: 97 primals, 217 duals Objective: 7.02e+03 Solver model: available Solver message: Optimal
INFO:pypsa.optimization.optimize:The shadow-prices of the constraints Generator-ext-p-lower, Generator-ext-p-upper, Link-fix-p-lower, Link-fix-p-upper, Store-fix-e-lower, Store-fix-e-upper, Store-energy_balance were not assigned to the network.
Objective: 7017.543859649121
The optimal panel size in kW is:
n.generators.p_nom_opt["PV panel"]
np.float64(7.0175438596491215)
n.generators_t.p.plot.area()
<Axes: xlabel='snapshot'>
The battery operation is optimised to follow:
df = pd.DataFrame({attr: n.stores_t[attr]["battery"] for attr in ["p", "e"]})
df.plot(grid=True, ylim=(-10, 40))
plt.legend(labels=["Energy output", "State of charge"])
<matplotlib.legend.Legend at 0x70aa93d011d0>
The losses in kWh per pay are:
(n.generators_t.p.loc[:, "PV panel"].sum() - n.loads_t.p.loc[:, "driving"].sum())
np.float64(3.999999999999986)
n.links_t.p0.plot.area()
<Axes: xlabel='snapshot'>