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
[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])
[4]:
# Run optimisation to determine market dispatch
network.lopf()
WARNING:pypsa.components:Solving optimisation problem with pyomo.In PyPSA version 0.21 the default will change to ``n.lopf(pyomo=False)``.Explicitly set ``n.lopf(pyomo=True)`` to retain current behaviour.
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: 1290000.0
Upper bound: 1290000.0
Number of objectives: 1
Number of constraints: 2
Number of variables: 5
Number of nonzeros: 5
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.0024046897888183594
# ----------------------------------------------------------
# Solution Information
# ----------------------------------------------------------
Solution:
- number of solutions: 0
number of solutions displayed: 0
/home/docs/checkouts/readthedocs.org/user_builds/pypsa/conda/v0.20.1/lib/python3.10/site-packages/pypsa/opf.py:2097: FutureWarning: In a future version, `df.iloc[:, i] = newvals` will attempt to set the values inplace instead of always setting a new array. To retain the old behavior, use either `df[df.columns[i]] = newvals` or, if columns are non-unique, `df.isetitem(i, newvals)`
network.global_constraints.loc[:, "mu"] = -get_shadows(
[4]:
(<SolverStatus.ok: 'ok'>, <TerminationCondition.optimal: 'optimal'>)
[5]:
# print the load active power (P) consumption
network.loads_t.p
[5]:
Load | South Africa load |
---|---|
snapshot | |
now | 42000.0 |
[6]:
# print the generator active power (P) dispatch
network.generators_t.p
[6]:
Generator | South Africa Coal | South Africa Wind | South Africa Gas | South Africa Oil |
---|---|---|---|---|
snapshot | ||||
now | 35000.0 | 3000.0 | 4000.0 | 0.0 |
[7]:
# print the clearing price (corresponding to gas)
network.buses_t.marginal_price
[7]:
Bus | South Africa |
---|---|
snapshot | |
now | 60.0 |
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,
)
[9]:
network.lopf()
WARNING:pypsa.components:Solving optimisation problem with pyomo.In PyPSA version 0.21 the default will change to ``n.lopf(pyomo=False)``.Explicitly set ``n.lopf(pyomo=True)`` to retain current behaviour.
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: 1260000.0
Upper bound: 1260000.0
Number of objectives: 1
Number of constraints: 5
Number of variables: 7
Number of nonzeros: 10
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.0024271011352539062
# ----------------------------------------------------------
# Solution Information
# ----------------------------------------------------------
Solution:
- number of solutions: 0
number of solutions displayed: 0
/home/docs/checkouts/readthedocs.org/user_builds/pypsa/conda/v0.20.1/lib/python3.10/site-packages/pypsa/opf.py:2097: FutureWarning: In a future version, `df.iloc[:, i] = newvals` will attempt to set the values inplace instead of always setting a new array. To retain the old behavior, use either `df[df.columns[i]] = newvals` or, if columns are non-unique, `df.isetitem(i, newvals)`
network.global_constraints.loc[:, "mu"] = -get_shadows(
[9]:
(<SolverStatus.ok: 'ok'>, <TerminationCondition.optimal: 'optimal'>)
[10]:
network.loads_t.p
[10]:
Load | Mozambique load | South Africa load |
---|---|---|
snapshot | ||
now | 650.0 | 42000.0 |
[11]:
network.generators_t.p
[11]:
Generator | Mozambique Hydro | South Africa Coal | South Africa Wind | South Africa Gas | South Africa Oil |
---|---|---|---|---|---|
snapshot | |||||
now | 1150.0 | 35000.0 | 3000.0 | 3500.0 | 0.0 |
[12]:
network.links_t.p0
[12]:
Link | South Africa - Mozambique link |
---|---|
snapshot | |
now | -500.0 |
[13]:
# print the clearing price (corresponding to water in Mozambique and gas in SA)
network.buses_t.marginal_price
[13]:
Bus | Mozambique | South Africa |
---|---|---|
snapshot | ||
now | 0.0 | 60.0 |
[14]:
# link shadow prices
network.links_t.mu_lower
[14]:
Link | South Africa - Mozambique link |
---|---|
snapshot | |
now | 60.0 |
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,
)
[16]:
network.lopf()
WARNING:pypsa.components:Solving optimisation problem with pyomo.In PyPSA version 0.21 the default will change to ``n.lopf(pyomo=False)``.Explicitly set ``n.lopf(pyomo=True)`` to retain current behaviour.
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: 1245000.0
Upper bound: 1245000.0
Number of objectives: 1
Number of constraints: 10
Number of variables: 10
Number of nonzeros: 19
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.0025076866149902344
# ----------------------------------------------------------
# Solution Information
# ----------------------------------------------------------
Solution:
- number of solutions: 0
number of solutions displayed: 0
/home/docs/checkouts/readthedocs.org/user_builds/pypsa/conda/v0.20.1/lib/python3.10/site-packages/pypsa/opf.py:2097: FutureWarning: In a future version, `df.iloc[:, i] = newvals` will attempt to set the values inplace instead of always setting a new array. To retain the old behavior, use either `df[df.columns[i]] = newvals` or, if columns are non-unique, `df.isetitem(i, newvals)`
network.global_constraints.loc[:, "mu"] = -get_shadows(
[16]:
(<SolverStatus.ok: 'ok'>, <TerminationCondition.optimal: 'optimal'>)
[17]:
network.loads_t.p
[17]:
Load | Swaziland load | Mozambique load | South Africa load |
---|---|---|---|
snapshot | |||
now | 250.0 | 650.0 | 42000.0 |
[18]:
network.generators_t.p
[18]:
Generator | Swaziland Hydro | Mozambique Hydro | South Africa Coal | South Africa Wind | South Africa Gas | South Africa Oil |
---|---|---|---|---|---|---|
snapshot | ||||||
now | 600.0 | 1050.0 | 35000.0 | 3000.0 | 3250.0 | 0.0 |
[19]:
network.links_t.p0
[19]:
Link | Mozambique - Swaziland link | South Africa - Swaziland link | South Africa - Mozambique link |
---|---|---|---|
snapshot | |||
now | -100.0 | -250.0 | -500.0 |
[20]:
# print the clearing price (corresponding to hydro in S and M, and gas in SA)
network.buses_t.marginal_price
[20]:
Bus | Swaziland | Mozambique | South Africa |
---|---|---|---|
snapshot | |||
now | 0.0 | 0.0 | 60.0 |
[21]:
# link shadow prices
network.links_t.mu_lower
[21]:
Link | Mozambique - Swaziland link | South Africa - Swaziland link | South Africa - Mozambique link |
---|---|---|---|
snapshot | |||
now | 0.0 | 60.0 | 60.0 |
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,
)
[23]:
network.lopf()
WARNING:pypsa.components:Solving optimisation problem with pyomo.In PyPSA version 0.21 the default will change to ``n.lopf(pyomo=False)``.Explicitly set ``n.lopf(pyomo=True)`` to retain current behaviour.
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: 1250000.0
Upper bound: 1250000.0
Number of objectives: 1
Number of constraints: 2
Number of variables: 6
Number of nonzeros: 6
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.0024390220642089844
# ----------------------------------------------------------
# Solution Information
# ----------------------------------------------------------
Solution:
- number of solutions: 0
number of solutions displayed: 0
/home/docs/checkouts/readthedocs.org/user_builds/pypsa/conda/v0.20.1/lib/python3.10/site-packages/pypsa/opf.py:2097: FutureWarning: In a future version, `df.iloc[:, i] = newvals` will attempt to set the values inplace instead of always setting a new array. To retain the old behavior, use either `df[df.columns[i]] = newvals` or, if columns are non-unique, `df.isetitem(i, newvals)`
network.global_constraints.loc[:, "mu"] = -get_shadows(
[23]:
(<SolverStatus.ok: 'ok'>, <TerminationCondition.optimal: 'optimal'>)
[24]:
network.loads_t.p
[24]:
Load | South Africa load |
---|---|
snapshot | |
now | 42000.0 |
[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
[25]:
Generator | South Africa Coal | South Africa Wind | South Africa Gas | South Africa Oil | South Africa industrial load |
---|---|---|---|---|---|
snapshot | |||||
now | 35000.0 | 3000.0 | 8000.0 | 0.0 | -4000.0 |
[26]:
network.buses_t.marginal_price
[26]:
Bus | South Africa |
---|---|
snapshot | |
now | 70.0 |
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]),
)
[28]:
# specify that we consider all snapshots
network.lopf(network.snapshots)
WARNING:pypsa.components:Solving optimisation problem with pyomo.In PyPSA version 0.21 the default will change to ``n.lopf(pyomo=False)``.Explicitly set ``n.lopf(pyomo=True)`` to retain current behaviour.
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: 6082000.0
Upper bound: 6082000.0
Number of objectives: 1
Number of constraints: 5
Number of variables: 17
Number of nonzeros: 17
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.003052949905395508
# ----------------------------------------------------------
# Solution Information
# ----------------------------------------------------------
Solution:
- number of solutions: 0
number of solutions displayed: 0
/home/docs/checkouts/readthedocs.org/user_builds/pypsa/conda/v0.20.1/lib/python3.10/site-packages/pypsa/opf.py:2097: FutureWarning: In a future version, `df.iloc[:, i] = newvals` will attempt to set the values inplace instead of always setting a new array. To retain the old behavior, use either `df[df.columns[i]] = newvals` or, if columns are non-unique, `df.isetitem(i, newvals)`
network.global_constraints.loc[:, "mu"] = -get_shadows(
[28]:
(<SolverStatus.ok: 'ok'>, <TerminationCondition.optimal: 'optimal'>)
[29]:
network.loads_t.p
[29]:
Load | South Africa load |
---|---|
snapshot | |
0 | 42000.0 |
1 | 43000.0 |
2 | 45000.0 |
3 | 46000.0 |
[30]:
network.generators_t.p
[30]:
Generator | South Africa Coal | South Africa Wind | South Africa Gas | South Africa Oil |
---|---|---|---|---|
snapshot | ||||
0 | 35000.0 | 900.0 | 6100.0 | 0.0 |
1 | 35000.0 | 1800.0 | 6200.0 | 0.0 |
2 | 35000.0 | 1200.0 | 8000.0 | 800.0 |
3 | 35000.0 | 1500.0 | 8000.0 | 1500.0 |
[31]:
network.buses_t.marginal_price
[31]:
Bus | South Africa |
---|---|
snapshot | |
0 | 60.0 |
1 | 60.0 |
2 | 80.0 |
3 | 80.0 |
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
)
[33]:
network.lopf(network.snapshots)
WARNING:pypsa.components:Solving optimisation problem with pyomo.In PyPSA version 0.21 the default will change to ``n.lopf(pyomo=False)``.Explicitly set ``n.lopf(pyomo=True)`` to retain current behaviour.
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: 6046000.0
Upper bound: 6046000.0
Number of objectives: 1
Number of constraints: 13
Number of variables: 29
Number of nonzeros: 44
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.0028841495513916016
# ----------------------------------------------------------
# Solution Information
# ----------------------------------------------------------
Solution:
- number of solutions: 0
number of solutions displayed: 0
/home/docs/checkouts/readthedocs.org/user_builds/pypsa/conda/v0.20.1/lib/python3.10/site-packages/pypsa/opf.py:2097: FutureWarning: In a future version, `df.iloc[:, i] = newvals` will attempt to set the values inplace instead of always setting a new array. To retain the old behavior, use either `df[df.columns[i]] = newvals` or, if columns are non-unique, `df.isetitem(i, newvals)`
network.global_constraints.loc[:, "mu"] = -get_shadows(
[33]:
(<SolverStatus.ok: 'ok'>, <TerminationCondition.optimal: 'optimal'>)
[34]:
network.loads_t.p
[34]:
Load | South Africa load |
---|---|
snapshot | |
0 | 42000.0 |
1 | 43000.0 |
2 | 45000.0 |
3 | 46000.0 |
[35]:
network.generators_t.p
[35]:
Generator | South Africa Coal | South Africa Wind | South Africa Gas | South Africa Oil |
---|---|---|---|---|
snapshot | ||||
0 | 35000.0 | 900.0 | 6900.0 | 0.0 |
1 | 35000.0 | 1800.0 | 7200.0 | 0.0 |
2 | 35000.0 | 1200.0 | 8000.0 | 0.0 |
3 | 35000.0 | 1500.0 | 8000.0 | 500.0 |
[36]:
network.storage_units_t.p
[36]:
StorageUnit | South Africa pumped hydro |
---|---|
snapshot | |
0 | -800.0 |
1 | -1000.0 |
2 | 800.0 |
3 | 1000.0 |
[37]:
network.storage_units_t.state_of_charge
[37]:
StorageUnit | South Africa pumped hydro |
---|---|
snapshot | |
0 | 800.0 |
1 | 1800.0 |
2 | 1000.0 |
3 | 0.0 |
[38]:
network.buses_t.marginal_price
[38]:
Bus | South Africa |
---|---|
snapshot | |
0 | 60.0 |
1 | 60.0 |
2 | 60.0 |
3 | 80.0 |