Battery Electric Vehicle Charging

Note

You can download this example as a Jupyter notebook or start it in interactive mode.

Battery Electric Vehicle Charging#

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.

NB: this example will use units of kW and kWh, unlike the PyPSA defaults

[1]:
import matplotlib.pyplot as plt
import pandas as pd

import pypsa

%matplotlib inline
[2]:
# use 24 hour period for consideration
index = pd.date_range("2016-01-01 00:00", "2016-01-01 23:00", freq="H")

# consumption pattern of BEV
bev_usage = pd.Series([0.0] * 7 + [9.0] * 2 + [0.0] * 8 + [9.0] * 2 + [0.0] * 5, index)

# solar PV panel generation per unit of capacity
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,
)

# availability of charging - i.e. only when parked at office
charger_p_max_pu = pd.Series(0, index=index)
charger_p_max_pu["2016-01-01 09:00":"2016-01-01 16:00"] = 1.0
/tmp/ipykernel_958/2212892536.py:2: FutureWarning: 'H' is deprecated and will be removed in a future version, please use 'h' instead.
  index = pd.date_range("2016-01-01 00:00", "2016-01-01 23:00", freq="H")
[3]:
df = pd.concat({"BEV": bev_usage, "PV": pv_pu, "Charger": charger_p_max_pu}, axis=1)
df.plot.area(subplots=True, figsize=(10, 7))
plt.tight_layout()
../_images/examples_battery-electric-vehicle-charging_3_0.png

Initialize the network

[4]:
network = pypsa.Network()
network.set_snapshots(index)

network.add("Bus", "place of work", carrier="AC")

network.add("Bus", "battery", carrier="Li-ion")

network.add(
    "Generator",
    "PV panel",
    bus="place of work",
    p_nom_extendable=True,
    p_max_pu=pv_pu,
    capital_cost=1000.0,
)

network.add("Load", "driving", bus="battery", p_set=bev_usage)

network.add(
    "Link",
    "charger",
    bus0="place of work",
    bus1="battery",
    p_nom=120,  # super-charger with 120 kW
    p_max_pu=charger_p_max_pu,
    efficiency=0.9,
)


network.add("Store", "battery storage", bus="battery", e_cyclic=True, e_nom=100.0)
[4]:
Index(['battery storage'], dtype='object')
[5]:
network.optimize()
print("Objective:", network.objective)
WARNING:pypsa.consistency:The following links have carriers which are not defined:
Index(['charger'], dtype='object', name='Link')
WARNING:pypsa.consistency:The following buses have carriers which are not defined:
Index(['place of work', 'battery'], dtype='object', name='Bus')
WARNING:pypsa.consistency:The following stores have carriers which are not defined:
Index(['battery storage'], dtype='object', name='Store')
WARNING:pypsa.consistency:The following links have carriers which are not defined:
Index(['charger'], dtype='object', name='Link')
WARNING:pypsa.consistency:The following buses have carriers which are not defined:
Index(['place of work', 'battery'], dtype='object', name='Bus')
WARNING:pypsa.consistency:The following stores have carriers which are not defined:
Index(['battery storage'], dtype='object', name='Store')
/home/docs/checkouts/readthedocs.org/user_builds/pypsa/envs/latest/lib/python3.12/site-packages/linopy/common.py:147: UserWarning: coords for dimension(s) ['Generator'] is not aligned with the pandas object. Previously, the indexes of the pandas were ignored and overwritten in these cases. Now, the pandas object's coordinates are taken considered for alignment.
  warn(
INFO:linopy.model: Solve problem using Highs solver
INFO:linopy.io: Writing time: 0.05s
INFO:linopy.solvers:Log file at /tmp/highs.log
INFO:linopy.constants: Optimization successful:
Status: ok
Termination condition: optimal
Solution: 97 primals, 218 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.
Running HiGHS 1.7.2 (git hash: 184e327): Copyright (c) 2024 HiGHS under MIT licence terms
Coefficient ranges:
  Matrix [1e-01, 1e+00]
  Cost   [1e+03, 1e+03]
  Bound  [0e+00, 0e+00]
  RHS    [9e+00, 1e+02]
Presolving model
16 rows, 17 cols, 40 nonzeros  0s
11 rows, 12 cols, 30 nonzeros  0s
9 rows, 10 cols, 24 nonzeros  0s
9 rows, 9 cols, 24 nonzeros  0s
Presolve : Reductions: rows 9(-209); columns 9(-88); elements 24(-325)
Solving the presolved LP
Using EKK dual simplex solver - serial
  Iteration        Objective     Infeasibilities num(sum)
          0     0.0000000000e+00 Pr: 1(36) 0s
          9     7.0175438596e+03 Pr: 0(0) 0s
Solving the original LP from the solution after postsolve
Model   status      : Optimal
Simplex   iterations: 9
Objective value     :  7.0175438596e+03
HiGHS run time      :          0.00
Writing the solution to /tmp/linopy-solve-bj7i84pe.sol
Objective: 7017.543859649121

The optimal panel size in kW is

[6]:
network.generators.p_nom_opt["PV panel"]
[6]:
7.0175438596491215
[7]:
network.generators_t.p.plot.area(figsize=(9, 4))
plt.tight_layout()
../_images/examples_battery-electric-vehicle-charging_9_0.png
[8]:
df = pd.DataFrame(
    {attr: network.stores_t[attr]["battery storage"] for attr in ["p", "e"]}
)
df.plot(grid=True, figsize=(10, 5))
plt.legend(labels=["Energy output", "State of charge"])
plt.tight_layout()
../_images/examples_battery-electric-vehicle-charging_10_0.png

The losses in kWh per pay are:

[9]:
(
    network.generators_t.p.loc[:, "PV panel"].sum()
    - network.loads_t.p.loc[:, "driving"].sum()
)
[9]:
3.999999999999986
[10]:
network.links_t.p0.plot.area(figsize=(9, 5))
plt.tight_layout()
../_images/examples_battery-electric-vehicle-charging_13_0.png