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);
/home/runner/work/PyPSA/PyPSA/pypsa/network/io.py:2082: FutureWarning: pandas infers the `str` dtype for string data since its version 3.0. PyPSA still converts it back to numpy object dtype on import, but will keep it from PyPSA 2.0 on. Set `pypsa.options.api.legacy_string_dtype` explicitly to suppress this warning. new_static = _coerce_string_dtypes(new_static)
n.optimize()
print("Objective:", n.objective)
/tmp/ipykernel_2554/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 Gurobi solver
INFO:linopy.model:Solver options: - log_to_console: False
INFO:linopy.io: Writing time: 0.05s
Set parameter WLSAccessID
Set parameter WLSSecret
Set parameter LicenseID to value 2537914
Academic license 2537914 - for non-commercial use only - registered to l.___@tu-berlin.de
Read LP format model from file /tmp/linopy-problem-8ryc_ov6.lp
Reading time = 0.00 seconds
obj: 217 rows, 97 columns, 325 nonzeros
Set parameter LogToConsole to value 0
INFO:linopy.constants: Optimization successful: Status: ok Termination condition: optimal Solution: 97 primals, 217 duals Objective: 7.02e+03 Solver: gurobi Runtime: 0.00s Dual bound: 7.02e+03 Solver model: available Solver message: 2
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 0x7fd111fec2d0>
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'>