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.07s
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.06s
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.06s
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.05s
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.09s
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