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