# Simple electricity market examples

## Contents

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

ERROR 1: PROJ: proj_create_from_database: Open of /home/docs/checkouts/readthedocs.org/user_builds/pypsa/conda/latest/share/proj failed

[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()

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


[4]:

# Run optimisation to determine market dispatch
network.optimize()

INFO:linopy.model: Solve problem using Glpk solver

INFO:linopy.io: Writing time: 0.02s

INFO:linopy.solvers:GLPSOL--GLPK LP/MIP Solver 5.0
Parameter(s) specified in the command line:
--lp /tmp/linopy-problem-gcf96raz.lp --output /tmp/linopy-solve-wdm4ilkl.sol
9 rows, 4 columns, 12 non-zeros
GLPK Simplex Optimizer 5.0
9 rows, 4 columns, 12 non-zeros
Preprocessing...
1 row, 3 columns, 3 non-zeros
Scaling...
A: min|aij| =  1.000e+00  max|aij| =  1.000e+00  ratio =  1.000e+00
Problem data seem to be well scaled
Constructing initial basis...
Size of triangular part is 1
0: obj =   1.260000000e+06 inf =   7.000e+03 (1)
1: obj =   1.470000000e+06 inf =   0.000e+00 (0)
*     2: obj =   1.290000000e+06 inf =   0.000e+00 (0)
OPTIMAL LP SOLUTION FOUND
Time used:   0.0 secs
Memory used: 0.0 Mb (40400 bytes)
Writing basic solution to '/tmp/linopy-solve-wdm4ilkl.sol'...


INFO:linopy.constants: Optimization successful:
Status: ok
Termination condition: optimal
Solution: 4 primals, 9 duals
Objective: 1.29e+06
Solver model: not available
Solver message: optimal


INFO:pypsa.optimization.optimize:The shadow-prices of the constraints Generator-fix-p-lower, Generator-fix-p-upper were not assigned to the network.

[4]:

('ok', 'optimal')

[5]:

# print the load active power (P) consumption

[5]:

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:

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

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
bus0=country,
bus1=other_country,
p_nom=transmission[country][other_country],
p_min_pu=-1,
)

[9]:

network.optimize()

INFO:linopy.model: Solve problem using Glpk solver

INFO:linopy.io: Writing time: 0.02s

INFO:linopy.solvers:GLPSOL--GLPK LP/MIP Solver 5.0
Parameter(s) specified in the command line:
--lp /tmp/linopy-problem-hl5hiiaw.lp --output /tmp/linopy-solve-c2n1107a.sol
14 rows, 6 columns, 19 non-zeros
GLPK Simplex Optimizer 5.0
14 rows, 6 columns, 19 non-zeros
Preprocessing...
1 row, 4 columns, 4 non-zeros
Scaling...
A: min|aij| =  1.000e+00  max|aij| =  1.000e+00  ratio =  1.000e+00
Problem data seem to be well scaled
Constructing initial basis...
Size of triangular part is 1
0: obj =   1.245000000e+06 inf =   6.500e+03 (1)
1: obj =   1.440000000e+06 inf =   0.000e+00 (0)
*     2: obj =   1.260000000e+06 inf =   0.000e+00 (0)
OPTIMAL LP SOLUTION FOUND
Time used:   0.0 secs
Memory used: 0.0 Mb (40404 bytes)
Writing basic solution to '/tmp/linopy-solve-c2n1107a.sol'...


INFO:linopy.constants: Optimization successful:
Status: ok
Termination condition: optimal
Solution: 6 primals, 14 duals
Objective: 1.26e+06
Solver model: not available
Solver message: optimal


INFO:pypsa.optimization.optimize:The shadow-prices of the constraints Generator-fix-p-lower, Generator-fix-p-upper, Link-fix-p-lower, Link-fix-p-upper were not assigned to the network.

[9]:

('ok', 'optimal')

[10]:

network.loads_t.p

[10]:

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]:

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

[14]:

snapshot
now

## 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:

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

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
bus0=country,
bus1=other_country,
p_nom=transmission[country][other_country],
p_min_pu=-1,
)

[16]:

network.optimize()

INFO:linopy.model: Solve problem using Glpk solver

INFO:linopy.io: Writing time: 0.02s

INFO:linopy.solvers:GLPSOL--GLPK LP/MIP Solver 5.0
Parameter(s) specified in the command line:
--lp /tmp/linopy-problem-lzhr5hx2.lp --output /tmp/linopy-solve-rnult3p8.sol
21 rows, 9 columns, 30 non-zeros
GLPK Simplex Optimizer 5.0
21 rows, 9 columns, 30 non-zeros
Preprocessing...
3 rows, 6 columns, 9 non-zeros
Scaling...
A: min|aij| =  1.000e+00  max|aij| =  1.000e+00  ratio =  1.000e+00
Problem data seem to be well scaled
Constructing initial basis...
Size of triangular part is 3
0: obj =   1.237500000e+06 inf =   6.250e+03 (1)
1: obj =   1.425000000e+06 inf =   0.000e+00 (0)
*     2: obj =   1.245000000e+06 inf =   0.000e+00 (0)
OPTIMAL LP SOLUTION FOUND
Time used:   0.0 secs
Memory used: 0.0 Mb (40424 bytes)
Writing basic solution to '/tmp/linopy-solve-rnult3p8.sol'...


INFO:linopy.constants: Optimization successful:
Status: ok
Termination condition: optimal
Solution: 9 primals, 21 duals
Objective: 1.24e+06
Solver model: not available
Solver message: optimal


INFO:pypsa.optimization.optimize:The shadow-prices of the constraints Generator-fix-p-lower, Generator-fix-p-upper, Link-fix-p-lower, Link-fix-p-upper were not assigned to the network.

[16]:

('ok', 'optimal')

[17]:

network.loads_t.p

[17]:

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]:

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

[21]:

snapshot
now

## 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()

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

# standard high marginal utility consumers

# add an industrial load as a dummy negative-dispatch generator with marginal utility of 70 EUR/MWh for 8000 MW
"Generator",
bus=country,
p_max_pu=0,
p_min_pu=-1,
p_nom=8000,
marginal_cost=70,
)

[23]:

network.optimize()

INFO:linopy.model: Solve problem using Glpk solver

INFO:linopy.io: Writing time: 0.01s

INFO:linopy.solvers:GLPSOL--GLPK LP/MIP Solver 5.0
Parameter(s) specified in the command line:
--lp /tmp/linopy-problem-52t2aa46.lp --output /tmp/linopy-solve-8darm1u0.sol
11 rows, 5 columns, 15 non-zeros
GLPK Simplex Optimizer 5.0
11 rows, 5 columns, 15 non-zeros
Preprocessing...
1 row, 4 columns, 4 non-zeros
Scaling...
A: min|aij| =  1.000e+00  max|aij| =  1.000e+00  ratio =  1.000e+00
Problem data seem to be well scaled
Constructing initial basis...
Size of triangular part is 1
0: obj =   9.400000000e+05 inf =   1.500e+04 (1)
3: obj =   1.480000000e+06 inf =   0.000e+00 (0)
*     5: obj =   1.250000000e+06 inf =   0.000e+00 (0)
OPTIMAL LP SOLUTION FOUND
Time used:   0.0 secs
Memory used: 0.0 Mb (40404 bytes)
Writing basic solution to '/tmp/linopy-solve-8darm1u0.sol'...


INFO:linopy.constants: Optimization successful:
Status: ok
Termination condition: optimal
Solution: 5 primals, 11 duals
Objective: 1.25e+06
Solver model: not available
Solver message: optimal


INFO:pypsa.optimization.optimize:The shadow-prices of the constraints Generator-fix-p-lower, Generator-fix-p-upper were not assigned to the network.

[23]:

('ok', 'optimal')

[24]:

network.loads_t.p

[24]:

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))

# p_max_pu is variable for wind
for tech in power_plant_p_nom[country]:
"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
bus=country,
p_set=loads[country] + np.array([0, 1000, 3000, 4000]),
)

[28]:

network.optimize()

INFO:linopy.model: Solve problem using Glpk solver

INFO:linopy.io: Writing time: 0.02s

INFO:linopy.solvers:GLPSOL--GLPK LP/MIP Solver 5.0
Parameter(s) specified in the command line:
--lp /tmp/linopy-problem-19ybwm50.lp --output /tmp/linopy-solve-2fd2vjho.sol
36 rows, 16 columns, 48 non-zeros
GLPK Simplex Optimizer 5.0
36 rows, 16 columns, 48 non-zeros
Preprocessing...
4 rows, 12 columns, 12 non-zeros
Scaling...
A: min|aij| =  1.000e+00  max|aij| =  1.000e+00  ratio =  1.000e+00
Problem data seem to be well scaled
Constructing initial basis...
Size of triangular part is 4
0: obj =   5.280000000e+06 inf =   3.600e+04 (4)
7: obj =   6.380000000e+06 inf =   0.000e+00 (0)
*    13: obj =   6.082000000e+06 inf =   0.000e+00 (0)
OPTIMAL LP SOLUTION FOUND
Time used:   0.0 secs
Memory used: 0.0 Mb (48739 bytes)
Writing basic solution to '/tmp/linopy-solve-2fd2vjho.sol'...


INFO:linopy.constants: Optimization successful:
Status: ok
Termination condition: optimal
Solution: 16 primals, 36 duals
Objective: 6.08e+06
Solver model: not available
Solver message: optimal


INFO:pypsa.optimization.optimize:The shadow-prices of the constraints Generator-fix-p-lower, Generator-fix-p-upper were not assigned to the network.

[28]:

('ok', 'optimal')

[29]:

network.loads_t.p

[29]:

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))

# p_max_pu is variable for wind
for tech in power_plant_p_nom[country]:
"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
bus=country,
p_set=loads[country] + np.array([0, 1000, 3000, 4000]),
)

# storage unit to do price arbitrage
"StorageUnit",
"{} pumped hydro".format(country),
bus=country,
p_nom=1000,
max_hours=6,  # energy storage in terms of hours at full power
)

[33]:

network.optimize()

INFO:linopy.model: Solve problem using Glpk solver

INFO:linopy.io: Writing time: 0.04s

INFO:linopy.solvers:GLPSOL--GLPK LP/MIP Solver 5.0
Parameter(s) specified in the command line:
--lp /tmp/linopy-problem-jydh5xt7.lp --output /tmp/linopy-solve-m7spgrt4.sol
64 rows, 28 columns, 95 non-zeros
GLPK Simplex Optimizer 5.0
64 rows, 28 columns, 95 non-zeros
Preprocessing...
8 rows, 23 columns, 34 non-zeros
Scaling...
A: min|aij| =  1.000e+00  max|aij| =  1.000e+00  ratio =  1.000e+00
Problem data seem to be well scaled
Constructing initial basis...
Size of triangular part is 8
0: obj =   5.280000000e+06 inf =   3.600e+04 (4)
7: obj =   6.380000000e+06 inf =   0.000e+00 (0)
*    17: obj =   6.046000000e+06 inf =   0.000e+00 (0)
OPTIMAL LP SOLUTION FOUND
Time used:   0.0 secs
Memory used: 0.1 Mb (72636 bytes)
Writing basic solution to '/tmp/linopy-solve-m7spgrt4.sol'...


INFO:linopy.constants: Optimization successful:
Status: ok
Termination condition: optimal
Solution: 28 primals, 64 duals
Objective: 6.05e+06
Solver model: not available
Solver message: optimal


INFO:pypsa.optimization.optimize:The shadow-prices of the constraints Generator-fix-p-lower, Generator-fix-p-upper, StorageUnit-fix-p_dispatch-lower, StorageUnit-fix-p_dispatch-upper, StorageUnit-fix-p_store-lower, StorageUnit-fix-p_store-upper, StorageUnit-fix-state_of_charge-lower, StorageUnit-fix-state_of_charge-upper, StorageUnit-energy_balance were not assigned to the network.

[33]:

('ok', 'optimal')

[34]:

network.loads_t.p

[34]:

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