Note

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

Simple electricity market examples

This example gradually builds up more and more complicated energy-only electricity markets in PyPSA, starting from a single bidding zone, going up to multiple bidding zones connected with transmission (NTCs) along with variable renewables and storage.

Preliminaries

Here libraries are imported and data is defined.

[1]:
import pypsa, numpy as np
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In [1], line 1
----> 1 import pypsa, numpy as np

File ~/checkouts/readthedocs.org/user_builds/pypsa/conda/v0.21.2/lib/python3.11/site-packages/pypsa/__init__.py:10
      1 # -*- coding: utf-8 -*-
      4 """
      5 Python for Power Systems Analysis (PyPSA)
      6
      7 Grid calculation library.
      8 """
---> 10 from pypsa import (
     11     components,
     12     contingency,
     13     descriptors,
     14     examples,
     15     geo,
     16     io,
     17     linopf,
     18     linopt,
     19     networkclustering,
     20     opf,
     21     opt,
     22     optimization,
     23     pf,
     24     plot,
     25 )
     26 from pypsa.components import Network, SubNetwork
     28 __version__ = "0.21.2"

File ~/checkouts/readthedocs.org/user_builds/pypsa/conda/v0.21.2/lib/python3.11/site-packages/pypsa/components.py:50
     37 from pypsa.io import (
     38     export_to_csv_folder,
     39     export_to_hdf5,
   (...)
     47     import_series_from_dataframe,
     48 )
     49 from pypsa.opf import network_lopf, network_opf
---> 50 from pypsa.optimization.optimize import OptimizationAccessor
     51 from pypsa.pf import (
     52     calculate_B_H,
     53     calculate_dependent_values,
   (...)
     62     sub_network_pf,
     63 )
     64 from pypsa.plot import iplot, plot

File ~/checkouts/readthedocs.org/user_builds/pypsa/conda/v0.21.2/lib/python3.11/site-packages/pypsa/optimization/__init__.py:7
      1 #!/usr/bin/env python3
      2 # -*- coding: utf-8 -*-
      3 """
      4 Build optimisation problems from PyPSA networks with Linopy.
      5 """
----> 7 from pypsa.optimization import abstract, constraints, optimize, variables
      8 from pypsa.optimization.optimize import create_model

File ~/checkouts/readthedocs.org/user_builds/pypsa/conda/v0.21.2/lib/python3.11/site-packages/pypsa/optimization/constraints.py:9
      6 import logging
      8 import pandas as pd
----> 9 from linopy.expressions import LinearExpression, merge
     10 from numpy import arange, cumsum, inf, nan, roll
     11 from scipy import sparse

File ~/checkouts/readthedocs.org/user_builds/pypsa/conda/v0.21.2/lib/python3.11/site-packages/linopy/__init__.py:9
      1 #!/usr/bin/env python3
      2 # -*- coding: utf-8 -*-
      3 """
      4 Created on Wed Mar 10 11:03:06 2021.
      5
      6 @author: fabulous
      7 """
----> 9 from linopy import model, remote
     10 from linopy.expressions import merge
     11 from linopy.io import read_netcdf

File ~/checkouts/readthedocs.org/user_builds/pypsa/conda/v0.21.2/lib/python3.11/site-packages/linopy/model.py:22
     20 from linopy import solvers
     21 from linopy.common import best_int, replace_by_map
---> 22 from linopy.constraints import (
     23     AnonymousConstraint,
     24     AnonymousScalarConstraint,
     25     Constraints,
     26 )
     27 from linopy.eval import Expr
     28 from linopy.expressions import LinearExpression, ScalarLinearExpression

File ~/checkouts/readthedocs.org/user_builds/pypsa/conda/v0.21.2/lib/python3.11/site-packages/linopy/constraints.py:21
     18 from scipy.sparse import coo_matrix
     19 from xarray import DataArray, Dataset
---> 21 from linopy import expressions, variables
     22 from linopy.common import (
     23     _merge_inplace,
     24     has_assigned_model,
   (...)
     27     replace_by_map,
     28 )
     31 class Constraint(DataArray):

File ~/checkouts/readthedocs.org/user_builds/pypsa/conda/v0.21.2/lib/python3.11/site-packages/linopy/expressions.py:23
     20 from xarray.core.dataarray import DataArrayCoordinates
     21 from xarray.core.groupby import _maybe_reorder, peek_at
---> 23 from linopy import constraints, variables
     24 from linopy.common import as_dataarray
     27 def exprwrap(method, *default_args, **new_default_kwargs):

File ~/checkouts/readthedocs.org/user_builds/pypsa/conda/v0.21.2/lib/python3.11/site-packages/linopy/variables.py:398
    393     roll = varwrap(DataArray.roll)
    395     rolling = varwrap(DataArray.rolling)
--> 398 @dataclass(repr=False)
    399 class Variables:
    400     """
    401     A variables container used for storing multiple variable arrays.
    402     """
    404     labels: Dataset = Dataset()

File ~/checkouts/readthedocs.org/user_builds/pypsa/conda/v0.21.2/lib/python3.11/dataclasses.py:1211, in dataclass.<locals>.wrap(cls)
   1210 def wrap(cls):
-> 1211     return _process_class(cls, init, repr, eq, order, unsafe_hash,
   1212                           frozen, match_args, kw_only, slots,
   1213                           weakref_slot)

File ~/checkouts/readthedocs.org/user_builds/pypsa/conda/v0.21.2/lib/python3.11/dataclasses.py:959, in _process_class(cls, init, repr, eq, order, unsafe_hash, frozen, match_args, kw_only, slots, weakref_slot)
    956         kw_only = True
    957     else:
    958         # Otherwise it's a field of some type.
--> 959         cls_fields.append(_get_field(cls, name, type, kw_only))
    961 for f in cls_fields:
    962     fields[f.name] = f

File ~/checkouts/readthedocs.org/user_builds/pypsa/conda/v0.21.2/lib/python3.11/dataclasses.py:816, in _get_field(cls, a_name, a_type, default_kw_only)
    812 # For real fields, disallow mutable defaults.  Use unhashable as a proxy
    813 # indicator for mutability.  Read the __hash__ attribute from the class,
    814 # not the instance.
    815 if f._field_type is _FIELD and f.default.__class__.__hash__ is None:
--> 816     raise ValueError(f'mutable default {type(f.default)} for field '
    817                      f'{f.name} is not allowed: use default_factory')
    819 return f

ValueError: mutable default <class 'xarray.core.dataset.Dataset'> for field labels is not allowed: use default_factory
[2]:
# marginal costs in EUR/MWh
marginal_costs = {"Wind": 0, "Hydro": 0, "Coal": 30, "Gas": 60, "Oil": 80}

# power plant capacities (nominal powers in MW) in each country (not necessarily realistic)
power_plant_p_nom = {
    "South Africa": {"Coal": 35000, "Wind": 3000, "Gas": 8000, "Oil": 2000},
    "Mozambique": {
        "Hydro": 1200,
    },
    "Swaziland": {
        "Hydro": 600,
    },
}

# transmission capacities in MW (not necessarily realistic)
transmission = {
    "South Africa": {"Mozambique": 500, "Swaziland": 250},
    "Mozambique": {"Swaziland": 100},
}

# country electrical loads in MW (not necessarily realistic)
loads = {"South Africa": 42000, "Mozambique": 650, "Swaziland": 250}

Single bidding zone with fixed load, one period

In this example we consider a single market bidding zone, South Africa.

The inelastic load has essentially infinite marginal utility (or higher than the marginal cost of any generator).

[3]:
country = "South Africa"

network = pypsa.Network()

network.add("Bus", country)

for tech in power_plant_p_nom[country]:
    network.add(
        "Generator",
        "{} {}".format(country, tech),
        bus=country,
        p_nom=power_plant_p_nom[country][tech],
        marginal_cost=marginal_costs[tech],
    )


network.add("Load", "{} load".format(country), bus=country, p_set=loads[country])
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [3], line 3
      1 country = "South Africa"
----> 3 network = pypsa.Network()
      5 network.add("Bus", country)
      7 for tech in power_plant_p_nom[country]:

NameError: name 'pypsa' is not defined
[4]:
# Run optimisation to determine market dispatch
network.lopf()
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [4], line 2
      1 # Run optimisation to determine market dispatch
----> 2 network.lopf()

NameError: name 'network' is not defined
[5]:
# print the load active power (P) consumption
network.loads_t.p
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [5], line 2
      1 # print the load active power (P) consumption
----> 2 network.loads_t.p

NameError: name 'network' is not defined
[6]:
# print the generator active power (P) dispatch
network.generators_t.p
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [6], line 2
      1 # print the generator active power (P) dispatch
----> 2 network.generators_t.p

NameError: name 'network' is not defined
[7]:
# print the clearing price (corresponding to gas)
network.buses_t.marginal_price
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [7], line 2
      1 # print the clearing price (corresponding to gas)
----> 2 network.buses_t.marginal_price

NameError: name 'network' is not defined

Two bidding zones connected by transmission, one period

In this example we have bidirectional transmission capacity between two bidding zones. The power transfer is treated as controllable (like an A/NTC (Available/Net Transfer Capacity) or HVDC line). Note that in the physical grid, power flows passively according to the network impedances.

[8]:
network = pypsa.Network()

countries = ["Mozambique", "South Africa"]

for country in countries:

    network.add("Bus", country)

    for tech in power_plant_p_nom[country]:
        network.add(
            "Generator",
            "{} {}".format(country, tech),
            bus=country,
            p_nom=power_plant_p_nom[country][tech],
            marginal_cost=marginal_costs[tech],
        )

    network.add("Load", "{} load".format(country), bus=country, p_set=loads[country])

    # add transmission as controllable Link
    if country not in transmission:
        continue

    for other_country in countries:
        if other_country not in transmission[country]:
            continue

        # NB: Link is by default unidirectional, so have to set p_min_pu = -1
        # to allow bidirectional (i.e. also negative) flow
        network.add(
            "Link",
            "{} - {} link".format(country, other_country),
            bus0=country,
            bus1=other_country,
            p_nom=transmission[country][other_country],
            p_min_pu=-1,
        )
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [8], line 1
----> 1 network = pypsa.Network()
      3 countries = ["Mozambique", "South Africa"]
      5 for country in countries:

NameError: name 'pypsa' is not defined
[9]:
network.lopf()
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [9], line 1
----> 1 network.lopf()

NameError: name 'network' is not defined
[10]:
network.loads_t.p
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [10], line 1
----> 1 network.loads_t.p

NameError: name 'network' is not defined
[11]:
network.generators_t.p
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [11], line 1
----> 1 network.generators_t.p

NameError: name 'network' is not defined
[12]:
network.links_t.p0
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [12], line 1
----> 1 network.links_t.p0

NameError: name 'network' is not defined
[13]:
# print the clearing price (corresponding to water in Mozambique and gas in SA)
network.buses_t.marginal_price
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [13], line 2
      1 # print the clearing price (corresponding to water in Mozambique and gas in SA)
----> 2 network.buses_t.marginal_price

NameError: name 'network' is not defined
[14]:
# link shadow prices
network.links_t.mu_lower
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [14], line 2
      1 # link shadow prices
----> 2 network.links_t.mu_lower

NameError: name 'network' is not defined

Three bidding zones connected by transmission, one period

In this example we have bidirectional transmission capacity between three bidding zones. The power transfer is treated as controllable (like an A/NTC (Available/Net Transfer Capacity) or HVDC line). Note that in the physical grid, power flows passively according to the network impedances.

[15]:
network = pypsa.Network()

countries = ["Swaziland", "Mozambique", "South Africa"]

for country in countries:

    network.add("Bus", country)

    for tech in power_plant_p_nom[country]:
        network.add(
            "Generator",
            "{} {}".format(country, tech),
            bus=country,
            p_nom=power_plant_p_nom[country][tech],
            marginal_cost=marginal_costs[tech],
        )

    network.add("Load", "{} load".format(country), bus=country, p_set=loads[country])

    # add transmission as controllable Link
    if country not in transmission:
        continue

    for other_country in countries:
        if other_country not in transmission[country]:
            continue

        # NB: Link is by default unidirectional, so have to set p_min_pu = -1
        # to allow bidirectional (i.e. also negative) flow
        network.add(
            "Link",
            "{} - {} link".format(country, other_country),
            bus0=country,
            bus1=other_country,
            p_nom=transmission[country][other_country],
            p_min_pu=-1,
        )
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [15], line 1
----> 1 network = pypsa.Network()
      3 countries = ["Swaziland", "Mozambique", "South Africa"]
      5 for country in countries:

NameError: name 'pypsa' is not defined
[16]:
network.lopf()
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [16], line 1
----> 1 network.lopf()

NameError: name 'network' is not defined
[17]:
network.loads_t.p
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [17], line 1
----> 1 network.loads_t.p

NameError: name 'network' is not defined
[18]:
network.generators_t.p
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [18], line 1
----> 1 network.generators_t.p

NameError: name 'network' is not defined
[19]:
network.links_t.p0
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [19], line 1
----> 1 network.links_t.p0

NameError: name 'network' is not defined
[20]:
# print the clearing price (corresponding to hydro in S and M, and gas in SA)
network.buses_t.marginal_price
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [20], line 2
      1 # print the clearing price (corresponding to hydro in S and M, and gas in SA)
----> 2 network.buses_t.marginal_price

NameError: name 'network' is not defined
[21]:
# link shadow prices
network.links_t.mu_lower
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [21], line 2
      1 # link shadow prices
----> 2 network.links_t.mu_lower

NameError: name 'network' is not defined

Single bidding zone with price-sensitive industrial load, one period

In this example we consider a single market bidding zone, South Africa.

Now there is a large industrial load with a marginal utility which is low enough to interact with the generation marginal cost.

[22]:
country = "South Africa"

network = pypsa.Network()

network.add("Bus", country)

for tech in power_plant_p_nom[country]:
    network.add(
        "Generator",
        "{} {}".format(country, tech),
        bus=country,
        p_nom=power_plant_p_nom[country][tech],
        marginal_cost=marginal_costs[tech],
    )

# standard high marginal utility consumers
network.add("Load", "{} load".format(country), bus=country, p_set=loads[country])

# add an industrial load as a dummy negative-dispatch generator with marginal utility of 70 EUR/MWh for 8000 MW
network.add(
    "Generator",
    "{} industrial load".format(country),
    bus=country,
    p_max_pu=0,
    p_min_pu=-1,
    p_nom=8000,
    marginal_cost=70,
)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [22], line 3
      1 country = "South Africa"
----> 3 network = pypsa.Network()
      5 network.add("Bus", country)
      7 for tech in power_plant_p_nom[country]:

NameError: name 'pypsa' is not defined
[23]:
network.lopf()
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [23], line 1
----> 1 network.lopf()

NameError: name 'network' is not defined
[24]:
network.loads_t.p
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [24], line 1
----> 1 network.loads_t.p

NameError: name 'network' is not defined
[25]:
# NB only half of industrial load is served, because this maxes out
# Gas. Oil is too expensive with a marginal cost of 80 EUR/MWh
network.generators_t.p
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [25], line 3
      1 # NB only half of industrial load is served, because this maxes out
      2 # Gas. Oil is too expensive with a marginal cost of 80 EUR/MWh
----> 3 network.generators_t.p

NameError: name 'network' is not defined
[26]:
network.buses_t.marginal_price
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [26], line 1
----> 1 network.buses_t.marginal_price

NameError: name 'network' is not defined

Single bidding zone with fixed load, several periods

In this example we consider a single market bidding zone, South Africa.

We consider multiple time periods (labelled [0,1,2,3]) to represent variable wind generation.

[27]:
country = "South Africa"

network = pypsa.Network()

# snapshots labelled by [0,1,2,3]
network.set_snapshots(range(4))

network.add("Bus", country)

# p_max_pu is variable for wind
for tech in power_plant_p_nom[country]:
    network.add(
        "Generator",
        "{} {}".format(country, tech),
        bus=country,
        p_nom=power_plant_p_nom[country][tech],
        marginal_cost=marginal_costs[tech],
        p_max_pu=([0.3, 0.6, 0.4, 0.5] if tech == "Wind" else 1),
    )

# load which varies over the snapshots
network.add(
    "Load",
    "{} load".format(country),
    bus=country,
    p_set=loads[country] + np.array([0, 1000, 3000, 4000]),
)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [27], line 3
      1 country = "South Africa"
----> 3 network = pypsa.Network()
      5 # snapshots labelled by [0,1,2,3]
      6 network.set_snapshots(range(4))

NameError: name 'pypsa' is not defined
[28]:
# specify that we consider all snapshots
network.lopf(network.snapshots)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [28], line 2
      1 # specify that we consider all snapshots
----> 2 network.lopf(network.snapshots)

NameError: name 'network' is not defined
[29]:
network.loads_t.p
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [29], line 1
----> 1 network.loads_t.p

NameError: name 'network' is not defined
[30]:
network.generators_t.p
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [30], line 1
----> 1 network.generators_t.p

NameError: name 'network' is not defined
[31]:
network.buses_t.marginal_price
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [31], line 1
----> 1 network.buses_t.marginal_price

NameError: name 'network' is not defined

Single bidding zone with fixed load and storage, several periods

In this example we consider a single market bidding zone, South Africa.

We consider multiple time periods (labelled [0,1,2,3]) to represent variable wind generation. Storage is allowed to do price arbitrage to reduce oil consumption.

[32]:
country = "South Africa"

network = pypsa.Network()

# snapshots labelled by [0,1,2,3]
network.set_snapshots(range(4))

network.add("Bus", country)

# p_max_pu is variable for wind
for tech in power_plant_p_nom[country]:
    network.add(
        "Generator",
        "{} {}".format(country, tech),
        bus=country,
        p_nom=power_plant_p_nom[country][tech],
        marginal_cost=marginal_costs[tech],
        p_max_pu=([0.3, 0.6, 0.4, 0.5] if tech == "Wind" else 1),
    )

# load which varies over the snapshots
network.add(
    "Load",
    "{} load".format(country),
    bus=country,
    p_set=loads[country] + np.array([0, 1000, 3000, 4000]),
)

# storage unit to do price arbitrage
network.add(
    "StorageUnit",
    "{} pumped hydro".format(country),
    bus=country,
    p_nom=1000,
    max_hours=6,  # energy storage in terms of hours at full power
)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [32], line 3
      1 country = "South Africa"
----> 3 network = pypsa.Network()
      5 # snapshots labelled by [0,1,2,3]
      6 network.set_snapshots(range(4))

NameError: name 'pypsa' is not defined
[33]:
network.lopf(network.snapshots)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [33], line 1
----> 1 network.lopf(network.snapshots)

NameError: name 'network' is not defined
[34]:
network.loads_t.p
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [34], line 1
----> 1 network.loads_t.p

NameError: name 'network' is not defined
[35]:
network.generators_t.p
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [35], line 1
----> 1 network.generators_t.p

NameError: name 'network' is not defined
[36]:
network.storage_units_t.p
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [36], line 1
----> 1 network.storage_units_t.p

NameError: name 'network' is not defined
[37]:
network.storage_units_t.state_of_charge
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [37], line 1
----> 1 network.storage_units_t.state_of_charge

NameError: name 'network' is not defined
[38]:
network.buses_t.marginal_price
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In [38], line 1
----> 1 network.buses_t.marginal_price

NameError: name 'network' is not defined