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_1035/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 buses have carriers which are not defined:
Index(['place of work', 'battery'], dtype='object', name='Bus')
WARNING:pypsa.consistency:The following links have carriers which are not defined:
Index(['charger'], dtype='object', name='Link')
WARNING:pypsa.consistency:The following stores have carriers which are not defined:
Index(['battery storage'], dtype='object', name='Store')
INFO:linopy.model: Solve problem using Highs solver
INFO:linopy.io: Writing time: 0.04s
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.
Running HiGHS 1.10.0 (git hash: fd86653): Copyright (c) 2025 HiGHS under MIT licence terms
LP   linopy-problem-gwdwyila has 217 rows; 97 cols; 325 nonzeros
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
12 rows, 13 cols, 32 nonzeros  0s
10 rows, 11 cols, 28 nonzeros  0s
Dependent equations search running on 1 equations with time limit of 1000.00s
Dependent equations search removed 0 rows and 0 nonzeros in 0.00s (limit = 1000.00s)
9 rows, 9 cols, 24 nonzeros  0s
Presolve : Reductions: rows 9(-208); columns 9(-88); elements 24(-301)
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 name          : linopy-problem-gwdwyila
Model status        : Optimal
Simplex   iterations: 9
Objective value     :  7.0175438596e+03
Relative P-D gap    :  1.2960299500e-16
HiGHS run time      :          0.00
Writing the solution to /tmp/linopy-solve-76qec6pe.sol
Objective: 7017.543859649121

The optimal panel size in kW is

[6]:
network.generators.p_nom_opt["PV panel"]
[6]:
np.float64(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]:
np.float64(3.999999999999986)
[10]:
network.links_t.p0.plot.area(figsize=(9, 5))
plt.tight_layout()
../_images/examples_battery-electric-vehicle-charging_13_0.png