Course project#
This notebook includes the steps to optimize the capacity and dispatch of generators in a stylized energy system. It has been prepared to serve as a tutorial for a simple PyPSA model representing the energy system in one country, city or region.
For the (optional) project of the course Integrated Energy Grids you need to deliver a report including the sections described at the end of this notebook.
Please, review the PyPSA tutorial before starting this project.
Note
If you have not yet set up Python on your computer, you can execute this tutorial in your browser via Google Colab. Click on the rocket in the top right corner and launch “Colab”. If that doesn’t work download the .ipynb file and import it in Google Colab.
Then install pandas and pypsa by executing the following command in a Jupyter cell at the top of the notebook.
!pip install pandas pypsa
import pandas as pd
import pypsa
Set parameter Username
Set parameter LicenseID to value 2767832
Academic license - for non-commercial use only - expires 2027-01-20
We start by creating the network. In this example, the country is modelled as a single node, so the network includes only one bus.
We select the year 2015 and set the hours in that year as snapshots.
We select a country, in this case Denmark (DNK), and add one node (electricity bus) to the network.
network = pypsa.Network()
hours_in_2015 = pd.date_range('2015-01-01 00:00Z',
'2015-12-31 23:00Z',
freq='h')
network.set_snapshots(hours_in_2015.values)
network.add("Bus",
"electricity bus")
network.snapshots
DatetimeIndex(['2015-01-01 00:00:00', '2015-01-01 01:00:00',
'2015-01-01 02:00:00', '2015-01-01 03:00:00',
'2015-01-01 04:00:00', '2015-01-01 05:00:00',
'2015-01-01 06:00:00', '2015-01-01 07:00:00',
'2015-01-01 08:00:00', '2015-01-01 09:00:00',
...
'2015-12-31 14:00:00', '2015-12-31 15:00:00',
'2015-12-31 16:00:00', '2015-12-31 17:00:00',
'2015-12-31 18:00:00', '2015-12-31 19:00:00',
'2015-12-31 20:00:00', '2015-12-31 21:00:00',
'2015-12-31 22:00:00', '2015-12-31 23:00:00'],
dtype='datetime64[ns]', name='snapshot', length=8760, freq=None)
The demand is represented by the historical electricity demand in 2015 with hourly resolution.
The file with historical hourly electricity demand for every European country is available in the data folder.
The electricity demand time series were obtained from ENTSOE through the very convenient compilation carried out by the Open Power System Data (OPSD). https://data.open-power-system-data.org/time_series/
# load electricity demand data
df_elec = pd.read_csv('data/electricity_demand.csv', sep=';', index_col=0) # in MWh
df_elec.index = pd.to_datetime(df_elec.index) #change index to datatime
country='DNK'
print(df_elec[country].head())
utc_time
2015-01-01 00:00:00+00:00 3210.98
2015-01-01 01:00:00+00:00 3100.02
2015-01-01 02:00:00+00:00 2980.39
2015-01-01 03:00:00+00:00 2933.49
2015-01-01 04:00:00+00:00 2941.54
Name: DNK, dtype: float64
# add load to the bus
network.add("Load",
"load",
bus="electricity bus",
p_set=df_elec[country].values)
Print the load time series to check that it has been properly added (you should see numbers and not ‘NaN’)
network.loads_t.p_set
| name | load |
|---|---|
| snapshot | |
| 2015-01-01 00:00:00 | 3210.98 |
| 2015-01-01 01:00:00 | 3100.02 |
| 2015-01-01 02:00:00 | 2980.39 |
| 2015-01-01 03:00:00 | 2933.49 |
| 2015-01-01 04:00:00 | 2941.54 |
| ... | ... |
| 2015-12-31 19:00:00 | 3687.87 |
| 2015-12-31 20:00:00 | 3535.55 |
| 2015-12-31 21:00:00 | 3389.26 |
| 2015-12-31 22:00:00 | 3262.27 |
| 2015-12-31 23:00:00 | 3158.85 |
8760 rows × 1 columns
In the optimization, we will minimize the annualized system costs.
We will need to annualize the cost of every generator, we build a function to do it.
def annuity(n,r):
""" Calculate the annuity factor for an asset with lifetime n years and
discount rate r """
if r > 0:
return r/(1. - 1./(1.+r)**n)
else:
return 1/n
We include solar PV and onshore wind generators.
The capacity factors representing the availability of those generators for every European country can be downloaded from the following repositories (select ‘optimal’ for PV and onshore for wind).
https://zenodo.org/record/3253876#.XSiVOEdS8l0
https://zenodo.org/record/2613651#.XSiVOkdS8l0
We include also Open Cycle Gas Turbine (OCGT) generators
The cost assumed for the generators are the same as in Table 1 in the paper https://doi.org/10.1016/j.enconman.2019.111977 (open version: https://arxiv.org/pdf/1906.06936.pdf)
# add the different carriers, only gas emits CO2
network.add("Carrier", "gas", co2_emissions=0.19) # in t_CO2/MWh_th
network.add("Carrier", "onshorewind")
network.add("Carrier", "solar")
# add onshore wind generator
df_onshorewind = pd.read_csv('data/onshore_wind_1979-2017.csv', sep=';', index_col=0)
df_onshorewind.index = pd.to_datetime(df_onshorewind.index)
CF_wind = df_onshorewind[country][[hour.strftime("%Y-%m-%dT%H:%M:%SZ") for hour in network.snapshots]]
capital_cost_onshorewind = annuity(30,0.07)*910000*(1+0.033) # in €/MW
network.add("Generator",
"onshorewind",
bus="electricity bus",
p_nom_extendable=True,
carrier="onshorewind",
#p_nom_max=1000, # maximum capacity can be limited due to environmental constraints
capital_cost = capital_cost_onshorewind,
marginal_cost = 0,
p_max_pu = CF_wind.values)
# add solar PV generator
df_solar = pd.read_csv('data/pv_optimal.csv', sep=';', index_col=0)
df_solar.index = pd.to_datetime(df_solar.index)
CF_solar = df_solar[country][[hour.strftime("%Y-%m-%dT%H:%M:%SZ") for hour in network.snapshots]]
capital_cost_solar = annuity(25,0.07)*425000*(1+0.03) # in €/MW
network.add("Generator",
"solar",
bus="electricity bus",
p_nom_extendable=True,
carrier="solar",
#p_nom_max=1000, # maximum capacity can be limited due to environmental constraints
capital_cost = capital_cost_solar,
marginal_cost = 0,
p_max_pu = CF_solar.values)
# add OCGT (Open Cycle Gas Turbine) generator
capital_cost_OCGT = annuity(25,0.07)*560000*(1+0.033) # in €/MW
fuel_cost = 21.6 # in €/MWh_th
efficiency = 0.39 # MWh_elec/MWh_th
marginal_cost_OCGT = fuel_cost/efficiency # in €/MWh_el
network.add("Generator",
"OCGT",
bus="electricity bus",
p_nom_extendable=True,
carrier="gas",
#p_nom_max=1000,
capital_cost = capital_cost_OCGT,
marginal_cost = marginal_cost_OCGT)
Print the generator Capacity factor time series to check that it has been properly added (you should see numbers and not ‘NaN’)
network.generators_t.p_max_pu
| name | onshorewind | solar |
|---|---|---|
| snapshot | ||
| 2015-01-01 00:00:00 | 0.460 | 0.0 |
| 2015-01-01 01:00:00 | 0.465 | 0.0 |
| 2015-01-01 02:00:00 | 0.478 | 0.0 |
| 2015-01-01 03:00:00 | 0.548 | 0.0 |
| 2015-01-01 04:00:00 | 0.593 | 0.0 |
| ... | ... | ... |
| 2015-12-31 19:00:00 | 0.174 | 0.0 |
| 2015-12-31 20:00:00 | 0.145 | 0.0 |
| 2015-12-31 21:00:00 | 0.141 | 0.0 |
| 2015-12-31 22:00:00 | 0.164 | 0.0 |
| 2015-12-31 23:00:00 | 0.204 | 0.0 |
8760 rows × 2 columns
We find the optimal solution using Gurobi as solver.
In this case, we are optimizing the installed capacity and dispatch of every generator to minimize the total system cost.
network.optimize(solver_name='gurobi')
WARNING:pypsa.consistency:The following buses have carriers which are not defined:
Index(['electricity bus'], dtype='object', name='name')
INFO:linopy.model: Solve problem using Gurobi solver
INFO:linopy.io:Writing objective.
Writing constraints.: 100%|██████████| 4/4 [00:00<00:00, 46.54it/s]
Writing continuous variables.: 100%|██████████| 2/2 [00:00<00:00, 280.07it/s]
INFO:linopy.io: Writing time: 0.14s
Set parameter Username
INFO:gurobipy:Set parameter Username
Set parameter LicenseID to value 2767832
INFO:gurobipy:Set parameter LicenseID to value 2767832
Academic license - for non-commercial use only - expires 2027-01-20
INFO:gurobipy:Academic license - for non-commercial use only - expires 2027-01-20
Read LP format model from file /private/var/folders/zg/by4_k0616s98pw41wld9475c0000gp/T/linopy-problem-twkasntr.lp
INFO:gurobipy:Read LP format model from file /private/var/folders/zg/by4_k0616s98pw41wld9475c0000gp/T/linopy-problem-twkasntr.lp
Reading time = 0.06 seconds
INFO:gurobipy:Reading time = 0.06 seconds
obj: 61323 rows, 26283 columns, 100750 nonzeros
INFO:gurobipy:obj: 61323 rows, 26283 columns, 100750 nonzeros
Gurobi Optimizer version 13.0.0 build v13.0.0rc1 (mac64[arm] - Darwin 25.2.0 25C56)
INFO:gurobipy:Gurobi Optimizer version 13.0.0 build v13.0.0rc1 (mac64[arm] - Darwin 25.2.0 25C56)
INFO:gurobipy:
CPU model: Apple M3
INFO:gurobipy:CPU model: Apple M3
Thread count: 8 physical cores, 8 logical processors, using up to 8 threads
INFO:gurobipy:Thread count: 8 physical cores, 8 logical processors, using up to 8 threads
INFO:gurobipy:
Optimize a model with 61323 rows, 26283 columns and 100750 nonzeros (Min)
INFO:gurobipy:Optimize a model with 61323 rows, 26283 columns and 100750 nonzeros (Min)
Model fingerprint: 0x140a0528
INFO:gurobipy:Model fingerprint: 0x140a0528
Model has 8763 linear objective coefficients
INFO:gurobipy:Model has 8763 linear objective coefficients
Coefficient statistics:
INFO:gurobipy:Coefficient statistics:
Matrix range [1e-03, 1e+00]
INFO:gurobipy: Matrix range [1e-03, 1e+00]
Objective range [6e+01, 8e+04]
INFO:gurobipy: Objective range [6e+01, 8e+04]
Bounds range [0e+00, 0e+00]
INFO:gurobipy: Bounds range [0e+00, 0e+00]
RHS range [2e+03, 6e+03]
INFO:gurobipy: RHS range [2e+03, 6e+03]
Presolve removed 35029 rows and 8746 columns
INFO:gurobipy:Presolve removed 35029 rows and 8746 columns
Presolve time: 0.04s
INFO:gurobipy:Presolve time: 0.04s
Presolved: 26294 rows, 17537 columns, 56975 nonzeros
INFO:gurobipy:Presolved: 26294 rows, 17537 columns, 56975 nonzeros
INFO:gurobipy:
Concurrent LP optimizer: primal simplex, dual simplex, and barrier
INFO:gurobipy:Concurrent LP optimizer: primal simplex, dual simplex, and barrier
Showing barrier log only...
INFO:gurobipy:Showing barrier log only...
INFO:gurobipy:
Ordering time: 0.00s
INFO:gurobipy:Ordering time: 0.00s
INFO:gurobipy:
Barrier statistics:
INFO:gurobipy:Barrier statistics:
Dense cols : 3
INFO:gurobipy: Dense cols : 3
AA' NZ : 3.944e+04
INFO:gurobipy: AA' NZ : 3.944e+04
Factor NZ : 1.271e+05 (roughly 20 MB of memory)
INFO:gurobipy: Factor NZ : 1.271e+05 (roughly 20 MB of memory)
Factor Ops : 6.621e+05 (less than 1 second per iteration)
INFO:gurobipy: Factor Ops : 6.621e+05 (less than 1 second per iteration)
Threads : 1
INFO:gurobipy: Threads : 1
INFO:gurobipy:
Objective Residual
INFO:gurobipy: Objective Residual
Iter Primal Dual Primal Dual Compl Time
INFO:gurobipy:Iter Primal Dual Primal Dual Compl Time
0 1.98363508e+10 -9.13438261e+10 1.16e+05 2.10e-13 8.89e+07 0s
INFO:gurobipy: 0 1.98363508e+10 -9.13438261e+10 1.16e+05 2.10e-13 8.89e+07 0s
1 3.75743722e+10 -1.22851865e+11 1.41e+04 3.41e+02 1.52e+07 0s
INFO:gurobipy: 1 3.75743722e+10 -1.22851865e+11 1.41e+04 3.41e+02 1.52e+07 0s
2 3.22805135e+10 -5.43856591e+10 2.04e-09 1.25e+00 1.56e+06 0s
INFO:gurobipy: 2 3.22805135e+10 -5.43856591e+10 2.04e-09 1.25e+00 1.56e+06 0s
3 6.10408686e+09 -5.45625859e+09 1.60e-10 1.16e-10 2.04e+05 0s
INFO:gurobipy: 3 6.10408686e+09 -5.45625859e+09 1.60e-10 1.16e-10 2.04e+05 0s
4 3.30166837e+09 -8.28825667e+08 5.78e-11 9.90e-10 7.26e+04 0s
INFO:gurobipy: 4 3.30166837e+09 -8.28825667e+08 5.78e-11 9.90e-10 7.26e+04 0s
5 2.69353301e+09 -2.06900025e+07 3.89e-11 3.49e-10 4.77e+04 0s
INFO:gurobipy: 5 2.69353301e+09 -2.06900025e+07 3.89e-11 3.49e-10 4.77e+04 0s
6 2.41465117e+09 3.76690089e+08 2.15e-10 7.46e-11 3.58e+04 0s
INFO:gurobipy: 6 2.41465117e+09 3.76690089e+08 2.15e-10 7.46e-11 3.58e+04 0s
7 2.16561864e+09 7.60939649e+08 1.78e-10 3.46e-11 2.47e+04 0s
INFO:gurobipy: 7 2.16561864e+09 7.60939649e+08 1.78e-10 3.46e-11 2.47e+04 0s
8 1.96179555e+09 1.07246356e+09 3.92e-11 6.40e-10 1.56e+04 0s
INFO:gurobipy: 8 1.96179555e+09 1.07246356e+09 3.92e-11 6.40e-10 1.56e+04 0s
9 1.84976925e+09 1.41822408e+09 9.25e-11 7.57e-10 7.58e+03 0s
INFO:gurobipy: 9 1.84976925e+09 1.41822408e+09 9.25e-11 7.57e-10 7.58e+03 0s
10 1.78859484e+09 1.55288613e+09 9.31e-10 1.82e-11 4.14e+03 0s
INFO:gurobipy: 10 1.78859484e+09 1.55288613e+09 9.31e-10 1.82e-11 4.14e+03 0s
11 1.76146834e+09 1.61708950e+09 1.46e-10 5.82e-11 2.54e+03 0s
INFO:gurobipy: 11 1.76146834e+09 1.61708950e+09 1.46e-10 5.82e-11 2.54e+03 0s
12 1.74321989e+09 1.63053424e+09 1.80e-09 3.49e-10 1.98e+03 0s
INFO:gurobipy: 12 1.74321989e+09 1.63053424e+09 1.80e-09 3.49e-10 1.98e+03 0s
13 1.73235869e+09 1.65035597e+09 7.02e-10 1.75e-10 1.44e+03 0s
INFO:gurobipy: 13 1.73235869e+09 1.65035597e+09 7.02e-10 1.75e-10 1.44e+03 0s
14 1.72170858e+09 1.66322924e+09 1.23e-09 1.16e-10 1.03e+03 0s
INFO:gurobipy: 14 1.72170858e+09 1.66322924e+09 1.23e-09 1.16e-10 1.03e+03 0s
15 1.70708500e+09 1.67979419e+09 2.93e-09 1.64e-11 4.79e+02 0s
INFO:gurobipy: 15 1.70708500e+09 1.67979419e+09 2.93e-09 1.64e-11 4.79e+02 0s
16 1.70408697e+09 1.68447324e+09 1.42e-09 6.66e-14 3.45e+02 0s
INFO:gurobipy: 16 1.70408697e+09 1.68447324e+09 1.42e-09 6.66e-14 3.45e+02 0s
17 1.70288539e+09 1.68682199e+09 8.15e-10 4.95e-10 2.82e+02 0s
INFO:gurobipy: 17 1.70288539e+09 1.68682199e+09 8.15e-10 4.95e-10 2.82e+02 0s
18 1.70251051e+09 1.68766857e+09 7.02e-10 2.04e-10 2.61e+02 0s
INFO:gurobipy: 18 1.70251051e+09 1.68766857e+09 7.02e-10 2.04e-10 2.61e+02 0s
19 1.70221308e+09 1.69010235e+09 8.45e-10 1.46e-10 2.13e+02 0s
INFO:gurobipy: 19 1.70221308e+09 1.69010235e+09 8.45e-10 1.46e-10 2.13e+02 0s
20 1.70092070e+09 1.69272662e+09 8.16e-10 2.62e-10 1.44e+02 0s
INFO:gurobipy: 20 1.70092070e+09 1.69272662e+09 8.16e-10 2.62e-10 1.44e+02 0s
21 1.70035322e+09 1.69463944e+09 2.62e-09 3.09e-11 1.00e+02 0s
INFO:gurobipy: 21 1.70035322e+09 1.69463944e+09 2.62e-09 3.09e-11 1.00e+02 0s
22 1.70018464e+09 1.69528870e+09 2.81e-09 3.20e-10 8.60e+01 0s
INFO:gurobipy: 22 1.70018464e+09 1.69528870e+09 2.81e-09 3.20e-10 8.60e+01 0s
23 1.70006228e+09 1.69569245e+09 2.63e-09 2.62e-10 7.68e+01 0s
INFO:gurobipy: 23 1.70006228e+09 1.69569245e+09 2.63e-09 2.62e-10 7.68e+01 0s
24 1.69991958e+09 1.69617106e+09 1.87e-09 3.49e-10 6.59e+01 0s
INFO:gurobipy: 24 1.69991958e+09 1.69617106e+09 1.87e-09 3.49e-10 6.59e+01 0s
25 1.69980129e+09 1.69638106e+09 2.45e-09 8.73e-11 6.01e+01 0s
INFO:gurobipy: 25 1.69980129e+09 1.69638106e+09 2.45e-09 8.73e-11 6.01e+01 0s
26 1.69957687e+09 1.69778117e+09 1.74e-09 2.91e-10 3.15e+01 0s
INFO:gurobipy: 26 1.69957687e+09 1.69778117e+09 1.74e-09 2.91e-10 3.15e+01 0s
27 1.69951682e+09 1.69800590e+09 9.31e-10 2.04e-10 2.65e+01 0s
INFO:gurobipy: 27 1.69951682e+09 1.69800590e+09 9.31e-10 2.04e-10 2.65e+01 0s
28 1.69948227e+09 1.69827551e+09 3.42e-09 1.47e-12 2.12e+01 0s
INFO:gurobipy: 28 1.69948227e+09 1.69827551e+09 3.42e-09 1.47e-12 2.12e+01 0s
29 1.69945864e+09 1.69845490e+09 2.43e-09 1.16e-10 1.76e+01 0s
INFO:gurobipy: 29 1.69945864e+09 1.69845490e+09 2.43e-09 1.16e-10 1.76e+01 0s
30 1.69943922e+09 1.69849725e+09 1.41e-09 6.11e-10 1.65e+01 0s
INFO:gurobipy: 30 1.69943922e+09 1.69849725e+09 1.41e-09 6.11e-10 1.65e+01 0s
31 1.69940911e+09 1.69898014e+09 4.08e-09 4.07e-10 7.54e+00 0s
INFO:gurobipy: 31 1.69940911e+09 1.69898014e+09 4.08e-09 4.07e-10 7.54e+00 0s
32 1.69940917e+09 1.69916451e+09 5.20e-05 2.91e-10 4.30e+00 0s
INFO:gurobipy: 32 1.69940917e+09 1.69916451e+09 5.20e-05 2.91e-10 4.30e+00 0s
33 1.69940745e+09 1.69917297e+09 4.09e-05 4.95e-10 4.12e+00 0s
INFO:gurobipy: 33 1.69940745e+09 1.69917297e+09 4.09e-05 4.95e-10 4.12e+00 0s
34 1.69940821e+09 1.69940128e+09 2.96e-05 1.16e-10 1.22e-01 0s
INFO:gurobipy: 34 1.69940821e+09 1.69940128e+09 2.96e-05 1.16e-10 1.22e-01 0s
35 1.69940582e+09 1.69940574e+09 1.66e-07 3.78e-10 1.37e-03 0s
INFO:gurobipy: 35 1.69940582e+09 1.69940574e+09 1.66e-07 3.78e-10 1.37e-03 0s
36 1.69940581e+09 1.69940581e+09 4.92e-09 5.82e-11 1.37e-06 1s
INFO:gurobipy: 36 1.69940581e+09 1.69940581e+09 4.92e-09 5.82e-11 1.37e-06 1s
37 1.69940581e+09 1.69940581e+09 2.49e-09 2.36e-09 1.37e-09 1s
INFO:gurobipy: 37 1.69940581e+09 1.69940581e+09 2.49e-09 2.36e-09 1.37e-09 1s
INFO:gurobipy:
Barrier solved model in 37 iterations and 0.51 seconds (0.34 work units)
INFO:gurobipy:Barrier solved model in 37 iterations and 0.51 seconds (0.34 work units)
Optimal objective 1.69940581e+09
INFO:gurobipy:Optimal objective 1.69940581e+09
INFO:gurobipy:
Crossover log...
INFO:gurobipy:Crossover log...
INFO:gurobipy:
18 DPushes remaining with DInf 0.0000000e+00 1s
INFO:gurobipy: 18 DPushes remaining with DInf 0.0000000e+00 1s
0 DPushes remaining with DInf 0.0000000e+00 1s
INFO:gurobipy: 0 DPushes remaining with DInf 0.0000000e+00 1s
INFO:gurobipy:
982 PPushes remaining with PInf 0.0000000e+00 1s
INFO:gurobipy: 982 PPushes remaining with PInf 0.0000000e+00 1s
0 PPushes remaining with PInf 0.0000000e+00 1s
INFO:gurobipy: 0 PPushes remaining with PInf 0.0000000e+00 1s
INFO:gurobipy:
Push phase complete: Pinf 0.0000000e+00, Dinf 1.3711503e-09 1s
INFO:gurobipy: Push phase complete: Pinf 0.0000000e+00, Dinf 1.3711503e-09 1s
INFO:gurobipy:
Crossover time: 0.04 seconds (0.02 work units)
INFO:gurobipy:Crossover time: 0.04 seconds (0.02 work units)
INFO:gurobipy:
Solved with barrier
INFO:gurobipy:Solved with barrier
Iteration Objective Primal Inf. Dual Inf. Time
INFO:gurobipy:Iteration Objective Primal Inf. Dual Inf. Time
1003 1.6994058e+09 0.000000e+00 0.000000e+00 1s
INFO:gurobipy: 1003 1.6994058e+09 0.000000e+00 0.000000e+00 1s
INFO:gurobipy:
Solved in 1003 iterations and 0.58 seconds (0.37 work units)
INFO:gurobipy:Solved in 1003 iterations and 0.58 seconds (0.37 work units)
Optimal objective 1.699405810e+09
INFO:gurobipy:Optimal objective 1.699405810e+09
INFO:linopy.constants: Optimization successful:
Status: ok
Termination condition: optimal
Solution: 26283 primals, 61323 duals
Objective: 1.70e+09
Solver model: available
Solver message: 2
INFO:pypsa.optimization.optimize:The shadow-prices of the constraints Generator-ext-p-lower, Generator-ext-p-upper were not assigned to the network.
('ok', 'optimal')
The message (‘ok’ , ‘optimal”) indicates that the optimizer has found an optimal solution.
The total cost can be read from the network objetive.
print(network.objective/1000000) #in 10^6 €
1699.4058099829083
The cost per MWh of electricity produced can also be calculated.
print(network.objective/network.loads_t.p.sum()) # EUR/MWh
name
load 51.789144
dtype: float64
The optimal capacity for every generator can be shown.
network.generators.p_nom_opt # in MW
name
onshorewind 7010.549199
solar 2871.404793
OCGT 5405.382311
Name: p_nom_opt, dtype: float64
We can plot now the dispatch of every generator during the first week of the year and the electricity demand. We import the matplotlib package which is very useful to plot results.
We can also plot the electricity mix.
import matplotlib.pyplot as plt
plt.plot(network.loads_t.p['load'][0:96], color='black', label='demand')
plt.plot(network.generators_t.p['onshorewind'][0:96], color='blue', label='onshore wind')
plt.plot(network.generators_t.p['solar'][0:96], color='orange', label='solar')
plt.plot(network.generators_t.p['OCGT'][0:96], color='brown', label='gas (OCGT)')
plt.legend(fancybox=True, shadow=True, loc='best')
<matplotlib.legend.Legend at 0x307ce23c0>
labels = ['onshore wind',
'solar',
'gas (OCGT)']
sizes = [network.generators_t.p['onshorewind'].sum(),
network.generators_t.p['solar'].sum(),
network.generators_t.p['OCGT'].sum()]
colors=['blue', 'orange', 'brown']
plt.pie(sizes,
colors=colors,
labels=labels,
wedgeprops={'linewidth':0})
plt.axis('equal')
plt.title('Electricity mix', y=1.07)
Text(0.5, 1.07, 'Electricity mix')
We can add a global CO2 constraint and solve again.
co2_limit=1000000 #tonCO2
network.add("GlobalConstraint",
"co2_limit",
type="primary_energy",
carrier_attribute="co2_emissions",
sense="<=",
constant=co2_limit)
network.optimize(solver_name='gurobi')
WARNING:pypsa.consistency:The following buses have carriers which are not defined:
Index(['electricity bus'], dtype='object', name='name')
WARNING:pypsa.consistency:The following sub_networks have carriers which are not defined:
Index(['0'], dtype='object', name='name')
INFO:linopy.model: Solve problem using Gurobi solver
INFO:linopy.io:Writing objective.
Writing constraints.: 100%|██████████| 5/5 [00:00<00:00, 46.86it/s]
Writing continuous variables.: 100%|██████████| 2/2 [00:00<00:00, 468.19it/s]
INFO:linopy.io: Writing time: 0.12s
Set parameter Username
INFO:gurobipy:Set parameter Username
Set parameter LicenseID to value 2767832
INFO:gurobipy:Set parameter LicenseID to value 2767832
Academic license - for non-commercial use only - expires 2027-01-20
INFO:gurobipy:Academic license - for non-commercial use only - expires 2027-01-20
Read LP format model from file /private/var/folders/zg/by4_k0616s98pw41wld9475c0000gp/T/linopy-problem-6w4vq_15.lp
INFO:gurobipy:Read LP format model from file /private/var/folders/zg/by4_k0616s98pw41wld9475c0000gp/T/linopy-problem-6w4vq_15.lp
Reading time = 0.07 seconds
INFO:gurobipy:Reading time = 0.07 seconds
obj: 61324 rows, 26283 columns, 109510 nonzeros
INFO:gurobipy:obj: 61324 rows, 26283 columns, 109510 nonzeros
Gurobi Optimizer version 13.0.0 build v13.0.0rc1 (mac64[arm] - Darwin 25.2.0 25C56)
INFO:gurobipy:Gurobi Optimizer version 13.0.0 build v13.0.0rc1 (mac64[arm] - Darwin 25.2.0 25C56)
INFO:gurobipy:
CPU model: Apple M3
INFO:gurobipy:CPU model: Apple M3
Thread count: 8 physical cores, 8 logical processors, using up to 8 threads
INFO:gurobipy:Thread count: 8 physical cores, 8 logical processors, using up to 8 threads
INFO:gurobipy:
Optimize a model with 61324 rows, 26283 columns and 109510 nonzeros (Min)
INFO:gurobipy:Optimize a model with 61324 rows, 26283 columns and 109510 nonzeros (Min)
Model fingerprint: 0x1a726413
INFO:gurobipy:Model fingerprint: 0x1a726413
Model has 8763 linear objective coefficients
INFO:gurobipy:Model has 8763 linear objective coefficients
Coefficient statistics:
INFO:gurobipy:Coefficient statistics:
Matrix range [1e-03, 1e+00]
INFO:gurobipy: Matrix range [1e-03, 1e+00]
Objective range [6e+01, 8e+04]
INFO:gurobipy: Objective range [6e+01, 8e+04]
Bounds range [0e+00, 0e+00]
INFO:gurobipy: Bounds range [0e+00, 0e+00]
RHS range [2e+03, 1e+06]
INFO:gurobipy: RHS range [2e+03, 1e+06]
Presolve removed 35029 rows and 8746 columns
INFO:gurobipy:Presolve removed 35029 rows and 8746 columns
Presolve time: 0.03s
INFO:gurobipy:Presolve time: 0.03s
Presolved: 26295 rows, 17537 columns, 65735 nonzeros
INFO:gurobipy:Presolved: 26295 rows, 17537 columns, 65735 nonzeros
INFO:gurobipy:
Concurrent LP optimizer: primal simplex, dual simplex, and barrier
INFO:gurobipy:Concurrent LP optimizer: primal simplex, dual simplex, and barrier
Showing barrier log only...
INFO:gurobipy:Showing barrier log only...
INFO:gurobipy:
Ordering time: 0.00s
INFO:gurobipy:Ordering time: 0.00s
INFO:gurobipy:
Barrier statistics:
INFO:gurobipy:Barrier statistics:
Dense cols : 3
INFO:gurobipy: Dense cols : 3
AA' NZ : 5.696e+04
INFO:gurobipy: AA' NZ : 5.696e+04
Factor NZ : 1.536e+05 (roughly 20 MB of memory)
INFO:gurobipy: Factor NZ : 1.536e+05 (roughly 20 MB of memory)
Factor Ops : 9.458e+05 (less than 1 second per iteration)
INFO:gurobipy: Factor Ops : 9.458e+05 (less than 1 second per iteration)
Threads : 1
INFO:gurobipy: Threads : 1
INFO:gurobipy:
Objective Residual
INFO:gurobipy: Objective Residual
Iter Primal Dual Primal Dual Compl Time
INFO:gurobipy:Iter Primal Dual Primal Dual Compl Time
0 2.94119004e+11 -1.11451635e+11 4.58e+07 1.05e-13 1.02e+09 0s
INFO:gurobipy: 0 2.94119004e+11 -1.11451635e+11 4.58e+07 1.05e-13 1.02e+09 0s
1 5.33634949e+11 -1.53566777e+11 4.98e+06 2.13e+02 1.43e+08 0s
INFO:gurobipy: 1 5.33634949e+11 -1.53566777e+11 4.98e+06 2.13e+02 1.43e+08 0s
2 3.71030736e+11 -1.46538231e+11 1.27e+05 8.73e-10 1.05e+07 0s
INFO:gurobipy: 2 3.71030736e+11 -1.46538231e+11 1.27e+05 8.73e-10 1.05e+07 0s
3 6.16870731e+10 -5.84407747e+10 1.77e+04 5.24e-10 2.20e+06 0s
INFO:gurobipy: 3 6.16870731e+10 -5.84407747e+10 1.77e+04 5.24e-10 2.20e+06 0s
4 5.08966419e+10 -5.10672255e+10 1.43e+04 9.60e-10 1.86e+06 0s
INFO:gurobipy: 4 5.08966419e+10 -5.10672255e+10 1.43e+04 9.60e-10 1.86e+06 0s
5 2.25693425e+10 -3.00006735e+10 5.21e+03 1.16e-10 9.39e+05 0s
INFO:gurobipy: 5 2.25693425e+10 -3.00006735e+10 5.21e+03 1.16e-10 9.39e+05 0s
6 1.63203080e+10 -1.89676442e+10 3.45e+03 5.53e-10 6.27e+05 0s
INFO:gurobipy: 6 1.63203080e+10 -1.89676442e+10 3.45e+03 5.53e-10 6.27e+05 0s
7 9.31801285e+09 -1.09558254e+10 1.60e+03 4.95e-10 3.58e+05 0s
INFO:gurobipy: 7 9.31801285e+09 -1.09558254e+10 1.60e+03 4.95e-10 3.58e+05 0s
8 6.95861133e+09 -8.61403926e+09 1.02e+03 2.18e-11 2.75e+05 0s
INFO:gurobipy: 8 6.95861133e+09 -8.61403926e+09 1.02e+03 2.18e-11 2.75e+05 0s
9 5.90046566e+09 -4.47912849e+09 7.89e+02 2.04e-10 1.83e+05 0s
INFO:gurobipy: 9 5.90046566e+09 -4.47912849e+09 7.89e+02 2.04e-10 1.83e+05 0s
10 5.02948120e+09 -1.77188711e+09 6.23e+02 7.28e-10 1.20e+05 0s
INFO:gurobipy: 10 5.02948120e+09 -1.77188711e+09 6.23e+02 7.28e-10 1.20e+05 0s
11 4.50201254e+09 -1.30587343e+09 5.16e+02 7.86e-10 1.02e+05 0s
INFO:gurobipy: 11 4.50201254e+09 -1.30587343e+09 5.16e+02 7.86e-10 1.02e+05 0s
12 3.95933836e+09 -7.66635420e+08 4.15e+02 6.98e-10 8.32e+04 0s
INFO:gurobipy: 12 3.95933836e+09 -7.66635420e+08 4.15e+02 6.98e-10 8.32e+04 0s
13 3.61608472e+09 3.47505627e+08 3.38e+02 2.33e-10 5.76e+04 0s
INFO:gurobipy: 13 3.61608472e+09 3.47505627e+08 3.38e+02 2.33e-10 5.76e+04 0s
14 3.39761330e+09 5.37286458e+08 2.89e+02 2.04e-10 5.04e+04 0s
INFO:gurobipy: 14 3.39761330e+09 5.37286458e+08 2.89e+02 2.04e-10 5.04e+04 0s
15 3.18459805e+09 1.10818850e+09 2.41e+02 4.73e-11 3.66e+04 0s
INFO:gurobipy: 15 3.18459805e+09 1.10818850e+09 2.41e+02 4.73e-11 3.66e+04 0s
16 2.94747283e+09 1.42691087e+09 1.80e+02 2.91e-11 2.68e+04 0s
INFO:gurobipy: 16 2.94747283e+09 1.42691087e+09 1.80e+02 2.91e-11 2.68e+04 0s
17 2.78443852e+09 1.67783788e+09 1.34e+02 2.04e-10 1.95e+04 0s
INFO:gurobipy: 17 2.78443852e+09 1.67783788e+09 1.34e+02 2.04e-10 1.95e+04 0s
18 2.68794375e+09 1.81199310e+09 1.08e+02 1.75e-10 1.54e+04 0s
INFO:gurobipy: 18 2.68794375e+09 1.81199310e+09 1.08e+02 1.75e-10 1.54e+04 0s
19 2.61234531e+09 1.95343943e+09 8.61e+01 5.15e-14 1.16e+04 0s
INFO:gurobipy: 19 2.61234531e+09 1.95343943e+09 8.61e+01 5.15e-14 1.16e+04 0s
20 2.55185932e+09 2.06355566e+09 6.78e+01 1.75e-10 8.60e+03 0s
INFO:gurobipy: 20 2.55185932e+09 2.06355566e+09 6.78e+01 1.75e-10 8.60e+03 0s
21 2.51217334e+09 2.13702685e+09 5.38e+01 1.16e-10 6.61e+03 0s
INFO:gurobipy: 21 2.51217334e+09 2.13702685e+09 5.38e+01 1.16e-10 6.61e+03 0s
22 2.49097303e+09 2.19740758e+09 4.73e+01 3.49e-10 5.17e+03 0s
INFO:gurobipy: 22 2.49097303e+09 2.19740758e+09 4.73e+01 3.49e-10 5.17e+03 0s
23 2.46855658e+09 2.24076505e+09 3.90e+01 7.28e-10 4.01e+03 0s
INFO:gurobipy: 23 2.46855658e+09 2.24076505e+09 3.90e+01 7.28e-10 4.01e+03 0s
24 2.42512257e+09 2.26502741e+09 2.23e+01 4.95e-10 2.82e+03 0s
INFO:gurobipy: 24 2.42512257e+09 2.26502741e+09 2.23e+01 4.95e-10 2.82e+03 0s
25 2.40616534e+09 2.29949405e+09 1.50e+01 3.78e-10 1.88e+03 0s
INFO:gurobipy: 25 2.40616534e+09 2.29949405e+09 1.50e+01 3.78e-10 1.88e+03 0s
26 2.39551856e+09 2.31735585e+09 1.09e+01 7.64e-11 1.38e+03 0s
INFO:gurobipy: 26 2.39551856e+09 2.31735585e+09 1.09e+01 7.64e-11 1.38e+03 0s
27 2.38887715e+09 2.32269809e+09 8.35e+00 3.49e-10 1.16e+03 0s
INFO:gurobipy: 27 2.38887715e+09 2.32269809e+09 8.35e+00 3.49e-10 1.16e+03 0s
28 2.38711106e+09 2.32718049e+09 7.68e+00 2.91e-10 1.05e+03 0s
INFO:gurobipy: 28 2.38711106e+09 2.32718049e+09 7.68e+00 2.91e-10 1.05e+03 0s
29 2.37843115e+09 2.33290536e+09 4.09e+00 8.15e-10 8.00e+02 0s
INFO:gurobipy: 29 2.37843115e+09 2.33290536e+09 4.09e+00 8.15e-10 8.00e+02 0s
30 2.37530087e+09 2.34558291e+09 2.93e+00 1.46e-10 5.23e+02 0s
INFO:gurobipy: 30 2.37530087e+09 2.34558291e+09 2.93e+00 1.46e-10 5.23e+02 0s
31 2.37139598e+09 2.34991118e+09 1.54e+00 6.11e-10 3.78e+02 0s
INFO:gurobipy: 31 2.37139598e+09 2.34991118e+09 1.54e+00 6.11e-10 3.78e+02 0s
32 2.37067267e+09 2.35123794e+09 1.25e+00 7.02e-14 3.42e+02 0s
INFO:gurobipy: 32 2.37067267e+09 2.35123794e+09 1.25e+00 7.02e-14 3.42e+02 0s
33 2.37036798e+09 2.35323694e+09 1.13e+00 3.09e-11 3.01e+02 0s
INFO:gurobipy: 33 2.37036798e+09 2.35323694e+09 1.13e+00 3.09e-11 3.01e+02 0s
34 2.36922899e+09 2.35749132e+09 7.26e-01 1.89e-10 2.06e+02 0s
INFO:gurobipy: 34 2.36922899e+09 2.35749132e+09 7.26e-01 1.89e-10 2.06e+02 0s
35 2.36905258e+09 2.35886502e+09 6.66e-01 6.98e-10 1.79e+02 0s
INFO:gurobipy: 35 2.36905258e+09 2.35886502e+09 6.66e-01 6.98e-10 1.79e+02 0s
36 2.36819214e+09 2.36099709e+09 3.50e-01 3.49e-10 1.26e+02 0s
INFO:gurobipy: 36 2.36819214e+09 2.36099709e+09 3.50e-01 3.49e-10 1.26e+02 0s
37 2.36780542e+09 2.36153964e+09 2.26e-01 2.87e-10 1.10e+02 0s
INFO:gurobipy: 37 2.36780542e+09 2.36153964e+09 2.26e-01 2.87e-10 1.10e+02 0s
38 2.36768226e+09 2.36367492e+09 1.83e-01 4.11e-10 7.04e+01 0s
INFO:gurobipy: 38 2.36768226e+09 2.36367492e+09 1.83e-01 4.11e-10 7.04e+01 0s
39 2.36744927e+09 2.36518560e+09 1.07e-01 2.04e-10 3.98e+01 0s
INFO:gurobipy: 39 2.36744927e+09 2.36518560e+09 1.07e-01 2.04e-10 3.98e+01 0s
40 2.36736240e+09 2.36560965e+09 7.88e-02 7.57e-10 3.08e+01 0s
INFO:gurobipy: 40 2.36736240e+09 2.36560965e+09 7.88e-02 7.57e-10 3.08e+01 0s
41 2.36726229e+09 2.36607019e+09 4.57e-02 2.24e-10 2.09e+01 0s
INFO:gurobipy: 41 2.36726229e+09 2.36607019e+09 4.57e-02 2.24e-10 2.09e+01 0s
42 2.36717316e+09 2.36649363e+09 1.55e-02 5.53e-10 1.19e+01 1s
INFO:gurobipy: 42 2.36717316e+09 2.36649363e+09 1.55e-02 5.53e-10 1.19e+01 1s
43 2.36715243e+09 2.36666451e+09 9.22e-03 7.28e-10 8.57e+00 1s
INFO:gurobipy: 43 2.36715243e+09 2.36666451e+09 9.22e-03 7.28e-10 8.57e+00 1s
44 2.36712256e+09 2.36712160e+09 1.20e-06 1.27e-11 1.70e-02 1s
INFO:gurobipy: 44 2.36712256e+09 2.36712160e+09 1.20e-06 1.27e-11 1.70e-02 1s
45 2.36712251e+09 2.36712251e+09 1.12e-08 1.01e-08 2.65e-08 1s
INFO:gurobipy: 45 2.36712251e+09 2.36712251e+09 1.12e-08 1.01e-08 2.65e-08 1s
46 2.36712251e+09 2.36712251e+09 3.73e-09 9.55e-09 2.65e-14 1s
INFO:gurobipy: 46 2.36712251e+09 2.36712251e+09 3.73e-09 9.55e-09 2.65e-14 1s
INFO:gurobipy:
Barrier solved model in 46 iterations and 0.54 seconds (0.44 work units)
INFO:gurobipy:Barrier solved model in 46 iterations and 0.54 seconds (0.44 work units)
Optimal objective 2.36712251e+09
INFO:gurobipy:Optimal objective 2.36712251e+09
INFO:gurobipy:
Crossover log...
INFO:gurobipy:Crossover log...
INFO:gurobipy:
3 DPushes remaining with DInf 0.0000000e+00 1s
INFO:gurobipy: 3 DPushes remaining with DInf 0.0000000e+00 1s
0 DPushes remaining with DInf 0.0000000e+00 1s
INFO:gurobipy: 0 DPushes remaining with DInf 0.0000000e+00 1s
Warning: Markowitz tolerance tightened to 0.25
INFO:gurobipy:Warning: Markowitz tolerance tightened to 0.25
INFO:gurobipy:
3212 PPushes remaining with PInf 0.0000000e+00 1s
INFO:gurobipy: 3212 PPushes remaining with PInf 0.0000000e+00 1s
0 PPushes remaining with PInf 0.0000000e+00 1s
INFO:gurobipy: 0 PPushes remaining with PInf 0.0000000e+00 1s
INFO:gurobipy:
Push phase complete: Pinf 0.0000000e+00, Dinf 1.0191736e-09 1s
INFO:gurobipy: Push phase complete: Pinf 0.0000000e+00, Dinf 1.0191736e-09 1s
INFO:gurobipy:
Crossover time: 0.09 seconds (0.29 work units)
INFO:gurobipy:Crossover time: 0.09 seconds (0.29 work units)
INFO:gurobipy:
Solved with barrier
INFO:gurobipy:Solved with barrier
Iteration Objective Primal Inf. Dual Inf. Time
INFO:gurobipy:Iteration Objective Primal Inf. Dual Inf. Time
3218 2.3671225e+09 0.000000e+00 0.000000e+00 1s
INFO:gurobipy: 3218 2.3671225e+09 0.000000e+00 0.000000e+00 1s
INFO:gurobipy:
Solved in 3218 iterations and 0.68 seconds (0.82 work units)
INFO:gurobipy:Solved in 3218 iterations and 0.68 seconds (0.82 work units)
Optimal objective 2.367122510e+09
INFO:gurobipy:Optimal objective 2.367122510e+09
INFO:linopy.constants: Optimization successful:
Status: ok
Termination condition: optimal
Solution: 26283 primals, 61324 duals
Objective: 2.37e+09
Solver model: available
Solver message: 2
INFO:pypsa.optimization.optimize:The shadow-prices of the constraints Generator-ext-p-lower, Generator-ext-p-upper were not assigned to the network.
('ok', 'optimal')
network.generators.p_nom_opt #in MW
name
onshorewind 19572.434599
solar 8874.293348
OCGT 5229.515916
Name: p_nom_opt, dtype: float64
plt.plot(network.loads_t.p['load'][0:96], color='black', label='demand')
plt.plot(network.generators_t.p['onshorewind'][0:96], color='blue', label='onshore wind')
plt.plot(network.generators_t.p['solar'][0:96], color='orange', label='solar')
plt.plot(network.generators_t.p['OCGT'][0:96], color='brown', label='gas (OCGT)')
plt.legend(fancybox=True, shadow=True, loc='best')
<matplotlib.legend.Legend at 0x328434cd0>
labels = ['onshore wind', 'solar', 'gas (OCGT)' ]
sizes = [network.generators_t.p['onshorewind'].sum(),
network.generators_t.p['solar'].sum(),
network.generators_t.p['OCGT'].sum()]
colors = ['blue', 'orange', 'brown']
plt.pie(sizes,
colors=colors,
labels=labels,
wedgeprops={'linewidth':0})
plt.axis('equal')
plt.title('Electricity mix', y=1.07)
Text(0.5, 1.07, 'Electricity mix')
PROJECT INSTRUCTIONS#
Based on the previous example, you are asked to carry out the following tasks:
—- Part 1 —-
A. Choose a different country/region/city/system and calculate the optimal capacities for renewable and non-renewable generators. You can add as many technologies as you want. Remember to provide a reference for the cost assumptions. Plot the dispatch time series for a week in summer and winter. Plot the annual electricity mix. Use the duration curves or the capacity factor to investigate the contribution of different technologies.
B. Investigate how sensitive your results are to the interannual variability of solar and wind generation. Plot the average capacity and variability obtained for every generator using different weather years.
C. Add some storage technology/ies and investigate how they behave and what their impact is on the optimal system configuration. Discuss what strategies your system is using to balance the renewable generation at different time scales (intraday, seasonal, etc.)
D. Connect your country to at least three neighbouring countries using HVAC lines, making sure that the network includes at least one closed cycle. Look for information on the existing capacities of those interconnectors and set the capacities fixed. Assume a voltage level of 400 kV and a unitary reactance \(x\)=0.1. You can assume that the generation capacities in the neighbouring countries are fixed or co-optimise the whole system. Optimise the whole system, assuming linearised AC power flow (DC approximation) and discuss the results.
(E. WITH PEN AND PAPER)
—- Part 2 —-
F. Investigate how sensitive the optimal capacity mix is to the global CO\(_2\) constraint. E.g., plot the generation mix as a function of the CO\(_2\) constraint that you impose. Search for the CO\(_2\) emissions in your country (today or in 1990) and refer to the emissions allowance for that historical data.
G. Assume that the countries are now also connected via gas pipelines transporting either H\(_2\) or CH\(_4\). Use a linear approach to represent gas transport in pipelines. Optimise the network again and discuss your results, including in the discussion which of the two energy transport networks modelled is transporting more energy.
G. Connect the electricity sector with, at least, another sector( e.g. heating or transport), and co-optimise all the sectors. Discuss your results.
H. Select one target for decarbonization (i.e., one CO\(_2\) allowance limit). What is the CO\(_2\) price required to achieve that decarbonization level? Search for information on the existing CO\(_2\) tax in your country (if any) and discuss your results. Is the model in agreement with the existing CO\(_2\) tax? Why or why not?
I. Connect the electricity sector with, at least another sector (e.g. heating or transport), and co-optimise all the sectors. Discuss your results.
J. Finally, select one topic that is under discussion in your region. Design and implement an experiment to obtain relevant information regarding that topic. E.g.
What are the consequences if Denmark decides not to install more onshore wind?
Would it be more expensive if France decides to close its nuclear power plants?
What will be the main impacts of the Viking link?
How does gas scarcity impact the optimal system configuration?
Write a short report (maximum length 10 pages) in groups of 4 students including your main findings.
Hints#
HINT 1: You can add a link with the following code
The efficiency will be 1 if you are connecting two countries and different from one if, for example, you are connecting the electricity bus to the heating bus using a heat pump. Setting p_min_pu=-1 makes the link reversible.
network.add("Link",
'country a - country b',
bus0="electricity bus country a",
bus1="electricity bus country b",
p_nom_extendable=True, # capacity is optimised
p_min_pu=-1,
length=600, # length (in km) between country a and country b
capital_cost=400*600) # capital cost * length
WARNING:pypsa.consistency:The following links have buses which are not defined:
Index(['country a - country b'], dtype='object', name='name')
WARNING:pypsa.consistency:The following links have buses which are not defined:
Index(['country a - country b'], dtype='object', name='name')
HINT 2: You can check the KKT multiplier associated with the constraint with the following code
print(network.global_constraints.constant) # CO2 limit (constant in the constraint)
print(network.global_constraints.mu) # CO2 price (Lagrance multiplier in the constraint)
name
co2_limit 1000000.0
Name: constant, dtype: float64
name
co2_limit -1404.832604
Name: mu, dtype: float64
HINT 3: You can add a H2 store connected to the electricity bus via an electrolyzer and a fuel cell with the following code
#Create a new carrier
network.add("Carrier",
"H2")
#Create a new bus
network.add("Bus",
"H2",
carrier = "H2")
#Connect the store to the bus
network.add("Store",
"H2 Tank",
bus = "H2",
e_nom_extendable = True,
e_cyclic = True,
capital_cost = annuity(25, 0.07)*57000*(1+0.011))
#Add the link "H2 Electrolysis" that transport energy from the electricity bus (bus0) to the H2 bus (bus1)
#with 80% efficiency
network.add("Link",
"H2 Electrolysis",
bus0 = "electricity bus",
bus1 = "H2",
p_nom_extendable = True,
efficiency = 0.8,
capital_cost = annuity(25, 0.07)*600000*(1+0.05))
#Add the link "H2 Fuel Cell" that transports energy from the H2 bus (bus0) to the electricity bus (bus1)
#with 58% efficiency
network.add("Link",
"H2 Fuel Cell",
bus0 = "H2",
bus1 = "electricity bus",
p_nom_extendable = True,
efficiency = 0.58,
capital_cost = annuity(10, 0.07)*1300000*(1+0.05))
WARNING:pypsa.consistency:The following links have buses which are not defined:
Index(['country a - country b'], dtype='object', name='name')
WARNING:pypsa.consistency:The following links have buses which are not defined:
Index(['country a - country b'], dtype='object', name='name')
WARNING:pypsa.consistency:The following links have buses which are not defined:
Index(['country a - country b'], dtype='object', name='name')
WARNING:pypsa.consistency:The following links have buses which are not defined:
Index(['country a - country b'], dtype='object', name='name')
HINT 4: You can get inspiration for plotting the flows in the network in the following example
https://docs.pypsa.org/latest/user-guide/plotting/static-map/#balances-and-flow