Note

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

LOPF with coupling to heating sector

In this example three locations are optimised, each with an electric bus and a heating bus and corresponding loads. At each location the electric and heating buses are connected with heat pumps; heat can also be supplied to the heat bus with a boiler. The electric buses are connected with transmission lines and there are electrical generators at two of the nodes.

[1]:
import pypsa
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
sns.set(rc={"figure.figsize":(9, 5)})
[2]:
network = pypsa.Network()

Add three buses of AC and heat carrier each

[3]:
for i in range(3):
    network.add("Bus","electric bus {}".format(i),v_nom=20.)
    network.add("Bus","heat bus {}".format(i),carrier="heat")
network.buses
[3]:
attribute v_nom type x y carrier unit v_mag_pu_set v_mag_pu_min v_mag_pu_max control sub_network
Bus
electric bus 0 20.0 0.0 0.0 AC None 1.0 0.0 inf PQ
heat bus 0 1.0 0.0 0.0 heat None 1.0 0.0 inf PQ
electric bus 1 20.0 0.0 0.0 AC None 1.0 0.0 inf PQ
heat bus 1 1.0 0.0 0.0 heat None 1.0 0.0 inf PQ
electric bus 2 20.0 0.0 0.0 AC None 1.0 0.0 inf PQ
heat bus 2 1.0 0.0 0.0 heat None 1.0 0.0 inf PQ
[4]:
network.buses["carrier"].value_counts()
[4]:
AC      3
heat    3
Name: carrier, dtype: int64

Add three lines in a ring

[5]:
for i in range(3):
    network.add("Line","line {}".format(i),
                bus0="electric bus {}".format(i),
                bus1="electric bus {}".format((i+1)%3),
                x=0.1,
                s_nom=1000)
network.lines
[5]:
attribute bus0 bus1 type x r g b s_nom s_nom_extendable s_nom_min ... v_ang_min v_ang_max sub_network x_pu r_pu g_pu b_pu x_pu_eff r_pu_eff s_nom_opt
Line
line 0 electric bus 0 electric bus 1 0.1 0.0 0.0 0.0 1000.0 False 0.0 ... -inf inf 0.0 0.0 0.0 0.0 0.0 0.0 0.0
line 1 electric bus 1 electric bus 2 0.1 0.0 0.0 0.0 1000.0 False 0.0 ... -inf inf 0.0 0.0 0.0 0.0 0.0 0.0 0.0
line 2 electric bus 2 electric bus 0 0.1 0.0 0.0 0.0 1000.0 False 0.0 ... -inf inf 0.0 0.0 0.0 0.0 0.0 0.0 0.0

3 rows × 29 columns

Connect the electric to the heat buses with heat pumps with COP 3

[6]:
for i in range(3):
    network.add("Link",
                "heat pump {}".format(i),
                bus0="electric bus {}".format(i),
                bus1="heat bus {}".format(i),
                p_nom=100,
                efficiency=3.)
network.links
[6]:
attribute bus0 bus1 type carrier efficiency build_year lifetime p_nom p_nom_extendable p_nom_min ... p_set p_min_pu p_max_pu capital_cost marginal_cost length terrain_factor ramp_limit_up ramp_limit_down p_nom_opt
Link
heat pump 0 electric bus 0 heat bus 0 3.0 0 inf 100.0 False 0.0 ... 0.0 0.0 1.0 0.0 0.0 0.0 1.0 NaN NaN 0.0
heat pump 1 electric bus 1 heat bus 1 3.0 0 inf 100.0 False 0.0 ... 0.0 0.0 1.0 0.0 0.0 0.0 1.0 NaN NaN 0.0
heat pump 2 electric bus 2 heat bus 2 3.0 0 inf 100.0 False 0.0 ... 0.0 0.0 1.0 0.0 0.0 0.0 1.0 NaN NaN 0.0

3 rows × 21 columns

Add carriers

[7]:
network.add("Carrier","gas", co2_emissions=0.27)
network.add("Carrier","biomass", co2_emissions=0.)
network.carriers
[7]:
attribute co2_emissions color nice_name max_growth
Carrier
gas 0.27 inf
biomass 0.00 inf

Add a gas generator at bus 0, a biomass generator at bus 1 and a boiler at all heat buses

[8]:
network.add("Generator","gas generator",
            bus="electric bus 0",
            p_nom=100,
            marginal_cost=50,
            carrier="gas",
            efficiency=0.3)

network.add("Generator","biomass generator",
            bus="electric bus 1",
            p_nom=100,
            marginal_cost=100,
            efficiency=0.3,
            carrier="biomass")

for i in range(3):
    network.add("Generator","boiler {}".format(i),
            bus="heat bus {}".format(i),
            p_nom=1000,
            efficiency=0.9,
            marginal_cost=20.,
            carrier="gas")

network.generators
[8]:
attribute bus control type p_nom p_nom_extendable p_nom_min p_nom_max p_min_pu p_max_pu p_set ... shut_down_cost min_up_time min_down_time up_time_before down_time_before ramp_limit_up ramp_limit_down ramp_limit_start_up ramp_limit_shut_down p_nom_opt
Generator
gas generator electric bus 0 PQ 100.0 False 0.0 inf 0.0 1.0 0.0 ... 0.0 0 0 1 0 NaN NaN 1.0 1.0 0.0
biomass generator electric bus 1 PQ 100.0 False 0.0 inf 0.0 1.0 0.0 ... 0.0 0 0 1 0 NaN NaN 1.0 1.0 0.0
boiler 0 heat bus 0 PQ 1000.0 False 0.0 inf 0.0 1.0 0.0 ... 0.0 0 0 1 0 NaN NaN 1.0 1.0 0.0
boiler 1 heat bus 1 PQ 1000.0 False 0.0 inf 0.0 1.0 0.0 ... 0.0 0 0 1 0 NaN NaN 1.0 1.0 0.0
boiler 2 heat bus 2 PQ 1000.0 False 0.0 inf 0.0 1.0 0.0 ... 0.0 0 0 1 0 NaN NaN 1.0 1.0 0.0

5 rows × 30 columns

Add electric loads and heat loads.

[9]:
for i in range(3):
    network.add("Load","electric load {}".format(i),
                bus="electric bus {}".format(i),
                p_set=i*10)

for i in range(3):
    network.add("Load","heat load {}".format(i),
                bus="heat bus {}".format(i),
                p_set=(3-i)*10)

network.loads
[9]:
attribute bus carrier type p_set q_set sign
Load
electric load 0 electric bus 0 0.0 0.0 -1.0
electric load 1 electric bus 1 10.0 0.0 -1.0
electric load 2 electric bus 2 20.0 0.0 -1.0
heat load 0 heat bus 0 30.0 0.0 -1.0
heat load 1 heat bus 1 20.0 0.0 -1.0
heat load 2 heat bus 2 10.0 0.0 -1.0

We define a function for the LOPF

[10]:
def run_lopf():
    network.lopf()
    df = pd.concat([network.generators_t.p.loc['now'],
                   network.links_t.p0.loc['now'],
                   network.loads_t.p.loc['now']],
                keys=['Generators', 'Links', 'Line'],
                names=['Component', 'index']).reset_index(name='Production')

    sns.barplot(data=df, x='index', y='Production', hue='Component')
    plt.title(f'Objective: {network.objective}')
    plt.xticks(rotation=90)
    plt.tight_layout()
[11]:
run_lopf()
INFO:pypsa.opf:Performed preliminary steps
INFO:pypsa.opf:Building pyomo model using `kirchhoff` formulation
INFO:pypsa.opf:Solving model using glpk
INFO:pypsa.opf:Optimization successful
# ==========================================================
# = Solver Results                                         =
# ==========================================================
# ----------------------------------------------------------
#   Problem Information
# ----------------------------------------------------------
Problem:
- Name: unknown
  Lower bound: 2499.99999999999
  Upper bound: 2499.99999999999
  Number of objectives: 1
  Number of constraints: 20
  Number of variables: 12
  Number of nonzeros: 33
  Sense: minimize
# ----------------------------------------------------------
#   Solver Information
# ----------------------------------------------------------
Solver:
- Status: ok
  Termination condition: optimal
  Statistics:
    Branch and bound:
      Number of bounded subproblems: 0
      Number of created subproblems: 0
  Error rc: 0
  Time: 0.002468109130859375
# ----------------------------------------------------------
#   Solution Information
# ----------------------------------------------------------
Solution:
- number of solutions: 0
  number of solutions displayed: 0
/home/docs/checkouts/readthedocs.org/user_builds/pypsa/conda/v0.19.1/lib/python3.10/site-packages/pypsa/opf.py:1293: FutureWarning: Using the level keyword in DataFrame and Series aggregations is deprecated and will be removed in a future version. Use groupby instead. df.sum(level=1) should use df.groupby(level=1).sum().
  pd.concat({c.name:
../_images/examples_lopf-with-heating_18_3.png

Now, rerun with marginal costs for the heat pump operation.

[12]:
network.links.marginal_cost = 10
run_lopf()
INFO:pypsa.opf:Performed preliminary steps
INFO:pypsa.opf:Building pyomo model using `kirchhoff` formulation
INFO:pypsa.opf:Solving model using glpk
INFO:pypsa.opf:Optimization successful
/home/docs/checkouts/readthedocs.org/user_builds/pypsa/conda/v0.19.1/lib/python3.10/site-packages/pypsa/opf.py:1293: FutureWarning: Using the level keyword in DataFrame and Series aggregations is deprecated and will be removed in a future version. Use groupby instead. df.sum(level=1) should use df.groupby(level=1).sum().
  pd.concat({c.name:
# ==========================================================
# = Solver Results                                         =
# ==========================================================
# ----------------------------------------------------------
#   Problem Information
# ----------------------------------------------------------
Problem:
- Name: unknown
  Lower bound: 2700.00000000001
  Upper bound: 2700.00000000001
  Number of objectives: 1
  Number of constraints: 20
  Number of variables: 12
  Number of nonzeros: 33
  Sense: minimize
# ----------------------------------------------------------
#   Solver Information
# ----------------------------------------------------------
Solver:
- Status: ok
  Termination condition: optimal
  Statistics:
    Branch and bound:
      Number of bounded subproblems: 0
      Number of created subproblems: 0
  Error rc: 0
  Time: 0.0025758743286132812
# ----------------------------------------------------------
#   Solution Information
# ----------------------------------------------------------
Solution:
- number of solutions: 0
  number of solutions displayed: 0
../_images/examples_lopf-with-heating_20_2.png

Finally, rerun with no CO2 emissions.

[13]:
network.add("GlobalConstraint",
            "co2_limit",
            sense="<=",
            constant=0.)

run_lopf()
INFO:pypsa.opf:Performed preliminary steps
INFO:pypsa.opf:Building pyomo model using `kirchhoff` formulation
INFO:pypsa.opf:Solving model using glpk
INFO:pypsa.opf:Optimization successful
/home/docs/checkouts/readthedocs.org/user_builds/pypsa/conda/v0.19.1/lib/python3.10/site-packages/pypsa/opf.py:1293: FutureWarning: Using the level keyword in DataFrame and Series aggregations is deprecated and will be removed in a future version. Use groupby instead. df.sum(level=1) should use df.groupby(level=1).sum().
  pd.concat({c.name:
# ==========================================================
# = Solver Results                                         =
# ==========================================================
# ----------------------------------------------------------
#   Problem Information
# ----------------------------------------------------------
Problem:
- Name: unknown
  Lower bound: 5199.99999999999
  Upper bound: 5199.99999999999
  Number of objectives: 1
  Number of constraints: 21
  Number of variables: 12
  Number of nonzeros: 37
  Sense: minimize
# ----------------------------------------------------------
#   Solver Information
# ----------------------------------------------------------
Solver:
- Status: ok
  Termination condition: optimal
  Statistics:
    Branch and bound:
      Number of bounded subproblems: 0
      Number of created subproblems: 0
  Error rc: 0
  Time: 0.0025272369384765625
# ----------------------------------------------------------
#   Solution Information
# ----------------------------------------------------------
Solution:
- number of solutions: 0
  number of solutions displayed: 0
../_images/examples_lopf-with-heating_22_2.png