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.optimize()
```

```
<__array_function__ internals>:200: RuntimeWarning: invalid value encountered in cast
INFO:linopy.model: Solve linear problem using Glpk solver
INFO:linopy.io: Writing time: 0.04s
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
```

```
GLPSOL--GLPK LP/MIP Solver 5.0
Parameter(s) specified in the command line:
--lp /tmp/linopy-problem-grjwzfov.lp --output /tmp/linopy-solve-rdwa14xt.sol
Reading problem data from '/tmp/linopy-problem-grjwzfov.lp'...
9 rows, 4 columns, 12 non-zeros
75 lines were read
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-rdwa14xt.sol'...
```

```
[4]:
```

```
('ok', '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.optimize()
```

```
INFO:linopy.model: Solve linear problem using Glpk solver
INFO:linopy.io: Writing time: 0.06s
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
```

```
GLPSOL--GLPK LP/MIP Solver 5.0
Parameter(s) specified in the command line:
--lp /tmp/linopy-problem-i1avr_gq.lp --output /tmp/linopy-solve-6g4jhj4e.sol
Reading problem data from '/tmp/linopy-problem-i1avr_gq.lp'...
14 rows, 6 columns, 19 non-zeros
104 lines were read
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-6g4jhj4e.sol'...
```

```
[9]:
```

```
('ok', '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-fix | 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.optimize()
```

```
INFO:linopy.model: Solve linear problem using Glpk solver
INFO:linopy.io: Writing time: 0.06s
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
```

```
GLPSOL--GLPK LP/MIP Solver 5.0
Parameter(s) specified in the command line:
--lp /tmp/linopy-problem-ks4cnwkn.lp --output /tmp/linopy-solve-y6weii6f.sol
Reading problem data from '/tmp/linopy-problem-ks4cnwkn.lp'...
21 rows, 9 columns, 30 non-zeros
146 lines were read
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-y6weii6f.sol'...
```

```
[16]:
```

```
('ok', '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-fix | 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.optimize()
```

```
<__array_function__ internals>:200: RuntimeWarning: invalid value encountered in cast
INFO:linopy.model: Solve linear problem using Glpk solver
INFO:linopy.io: Writing time: 0.04s
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
```

```
GLPSOL--GLPK LP/MIP Solver 5.0
Parameter(s) specified in the command line:
--lp /tmp/linopy-problem-igfmuhlw.lp --output /tmp/linopy-solve-hig6vg0s.sol
Reading problem data from '/tmp/linopy-problem-igfmuhlw.lp'...
11 rows, 5 columns, 15 non-zeros
88 lines were read
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-hig6vg0s.sol'...
```

```
[23]:
```

```
('ok', '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]:
```

```
network.optimize()
```

```
<__array_function__ internals>:200: RuntimeWarning: invalid value encountered in cast
INFO:linopy.model: Solve linear problem using Glpk solver
INFO:linopy.io: Writing time: 0.04s
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
```

```
GLPSOL--GLPK LP/MIP Solver 5.0
Parameter(s) specified in the command line:
--lp /tmp/linopy-problem-zm5hkgjl.lp --output /tmp/linopy-solve-3dkgd0sp.sol
Reading problem data from '/tmp/linopy-problem-zm5hkgjl.lp'...
36 rows, 16 columns, 48 non-zeros
240 lines were read
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-3dkgd0sp.sol'...
```

```
[28]:
```

```
('ok', '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.optimize()
```

```
<__array_function__ internals>:200: RuntimeWarning: invalid value encountered in cast
INFO:linopy.model: Solve linear problem using Glpk solver
INFO:linopy.io: Writing time: 0.13s
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
```

```
GLPSOL--GLPK LP/MIP Solver 5.0
Parameter(s) specified in the command line:
--lp /tmp/linopy-problem-_zpj01mv.lp --output /tmp/linopy-solve-75lhxbcr.sol
Reading problem data from '/tmp/linopy-problem-_zpj01mv.lp'...
64 rows, 28 columns, 95 non-zeros
411 lines were read
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-75lhxbcr.sol'...
```

```
[33]:
```

```
('ok', '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 |