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()
INFO:pypsa.linopf:Prepare linear problem
INFO:pypsa.linopf:Total preparation time: 0.06s
INFO:pypsa.linopf:Solve linear problem using Glpk solver
INFO:pypsa.linopf:Optimization successful. Objective value: 1.29e+06
[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.lopf()
INFO:pypsa.linopf:Prepare linear problem
INFO:pypsa.linopf:Total preparation time: 0.07s
INFO:pypsa.linopf:Solve linear problem using Glpk solver
INFO:pypsa.linopf:Optimization successful. Objective value: 1.26e+06
[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.lopf()
INFO:pypsa.linopf:Prepare linear problem
INFO:pypsa.linopf:Total preparation time: 0.07s
INFO:pypsa.linopf:Solve linear problem using Glpk solver
INFO:pypsa.linopf:Optimization successful. Objective value: 1.24e+06
[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.lopf()
INFO:pypsa.linopf:Prepare linear problem
INFO:pypsa.linopf:Total preparation time: 0.06s
INFO:pypsa.linopf:Solve linear problem using Glpk solver
INFO:pypsa.linopf:Optimization successful. Objective value: 1.25e+06
[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]:
# specify that we consider all snapshots
network.lopf(network.snapshots)
INFO:pypsa.linopf:Prepare linear problem
INFO:pypsa.linopf:Total preparation time: 0.06s
INFO:pypsa.linopf:Solve linear problem using Glpk solver
INFO:pypsa.linopf:Optimization successful. Objective value: 6.08e+06
[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.lopf(network.snapshots)
INFO:pypsa.linopf:Prepare linear problem
INFO:pypsa.linopf:Total preparation time: 0.1s
INFO:pypsa.linopf:Solve linear problem using Glpk solver
INFO:pypsa.linopf:Optimization successful. Objective value: 6.05e+06
[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