Electricity Markets
Adapted from: PyPSA Examples – Electricity Markets
PyPSA seeks to minimize the total cost of a network. Because this includes operational costs of electricity market resources such as generators, PyPSA can be used to model electricity markets and clear them. When an electricity market is cleared, it determines which generators to schedule/dispatch as well as the market-clearing price(s).
This example shows how to use TyPSA to model increasingly complex electricity markets.
import datetime as dt
from typing import Literal
import pandas as pd
import typsa
from typsa.components import Bus, Carrier, Generator, Link, Load, StorageUnit
from typsa.time_variation import IntegerSnapshots, RangedSeries, Static
type Tech = Literal["Wind", "Hydro", "Coal", "Gas", "Oil"]
type Country = Literal["South Africa", "Mozambique", "Eswatini"]
MARGINAL_COST_LOOKUP: dict[Tech, float] = {
"Wind": 0,
"Hydro": 0,
"Coal": 30,
"Gas": 60,
"Oil": 80,
}
POWER_PLANT_P_NOM_LOOKUP: dict[Country, dict[Tech, float]] = {
"South Africa": {"Coal": 35000, "Wind": 3000, "Gas": 8000, "Oil": 2000},
"Mozambique": {"Hydro": 1200},
"Eswatini": {"Hydro": 600},
}
TRANSMISSION_LOOKUP: dict[tuple[Country, Country], float] = {
("South Africa", "Mozambique"): 500,
("South Africa", "Eswatini"): 250,
("Mozambique", "Eswatini"): 100,
}
LOAD_LOOKUP: dict[Country, float] = {
"South Africa": 42000,
"Mozambique": 650,
"Eswatini": 250,
}
def create_network(countries: list[Country]) -> typsa.Network:
network = typsa.Network()
network.add_components(Carrier(name="AC"))
for country in countries:
network.add_components(Bus(name=country))
for tech in POWER_PLANT_P_NOM_LOOKUP[country]:
network.add_components(
Generator(
name=f"{country} {tech}",
bus=country,
p_nom=POWER_PLANT_P_NOM_LOOKUP[country][tech],
marginal_cost=MARGINAL_COST_LOOKUP[tech],
)
)
network.add_components(
Load(name=f"{country} load", bus=country, p_set=LOAD_LOOKUP[country])
)
for (country, other_country), p_nom in TRANSMISSION_LOOKUP.items():
if country in countries and other_country in countries:
network.add_components(
Link(
name=f"{country} - {other_country} link",
bus0=country,
bus1=other_country,
p_nom=p_nom,
p_min_pu=-1,
)
)
return network
def report_results(
optimized_network: (
typsa.network.OptimizedNetwork[Static]
| typsa.network.OptimizedNetwork[IntegerSnapshots]
),
) -> None:
print("Generator dispatched power:\n")
print(optimized_network.dynamic_results.of_all_generators.p.T)
print()
print("Zone marginal price:\n")
print(optimized_network.dynamic_results.of_all_buses.marginal_price.T)
if len(optimized_network.links) > 0:
print()
print("Transmission:\n")
link_dynamic_results = optimized_network.dynamic_results.of_all_links
print(
pd.concat(
{
"power": link_dynamic_results.p0.T,
"shadow price": link_dynamic_results.mu_lower.T,
},
axis="columns",
).reorder_levels([1, 0], axis="columns")
)
if len(optimized_network.storage_units) > 0:
print()
print("Storage unit:\n")
storage_unit_dynamic_results = (
optimized_network.dynamic_results.of_all_storage_units
)
print(
pd.concat(
{
"dispatched power": storage_unit_dynamic_results.p,
"state of charge": storage_unit_dynamic_results.state_of_charge,
},
axis="columns",
).reorder_levels([1, 0], axis="columns")
)
Single bidding zone with fixed load, one period
Generator dispatched power:
snapshot now
name
South Africa Coal 35000.0
South Africa Wind 3000.0
South Africa Gas 4000.0
South Africa Oil -0.0
Zone marginal price:
snapshot now
name
South Africa 60.0
Three bidding zones connected by transmission, one period
optimized_three_country_network, _ = create_network(
countries=["South Africa", "Mozambique", "Eswatini"]
).optimize()
Generator dispatched power:
snapshot now
name
South Africa Coal 35000.0
South Africa Wind 3000.0
South Africa Gas 3250.0
South Africa Oil -0.0
Mozambique Hydro 1050.0
Eswatini Hydro 600.0
Zone marginal price:
snapshot now
name
South Africa 60.0
Mozambique -0.0
Eswatini -0.0
Transmission:
snapshot now
power shadow price
name
South Africa - Mozambique link -500.0 NaN
South Africa - Eswatini link -250.0 NaN
Mozambique - Eswatini link -100.0 NaN
Single bidding zone with price-sensitive industrial load, one period
sa_network = create_network(countries=[country := "South Africa"])
sa_network.add_components(
Generator(
name=f"{country} industrial load",
bus=country,
p_max_pu=0,
p_min_pu=-1,
p_nom=8000,
marginal_cost=70,
)
)
optimized_sa_network, _ = sa_network.optimize()
Generator dispatched power:
snapshot now
name
South Africa Coal 35000.0
South Africa Wind 3000.0
South Africa Gas 8000.0
South Africa Oil -0.0
South Africa industrial load -4000.0
Zone marginal price:
snapshot now
name
South Africa 70.0
Single bidding zone with fixed load, several periods
country: Country = "South Africa"
sa_network = typsa.Network(IntegerSnapshots(range(4), spacing=dt.timedelta(hours=1)))
sa_network.add_components(Carrier(name="AC"))
sa_network.add_components(Bus(name=country))
for tech in POWER_PLANT_P_NOM_LOOKUP[country]:
sa_network.add_components(
Generator(
name=f"{country} {tech}",
bus=country,
p_nom=POWER_PLANT_P_NOM_LOOKUP[country][tech],
marginal_cost=MARGINAL_COST_LOOKUP[tech],
p_max_pu=(
RangedSeries(pd.Series([0.3, 0.6, 0.4, 0.5])) if tech == "Wind" else 1
),
)
)
sa_network.add_components(
Load(
name=f"{country} load",
bus=country,
p_set=RangedSeries(LOAD_LOOKUP[country] + pd.Series([0, 1000, 3000, 4000])),
)
)
optimized_sa_network, _ = sa_network.optimize()
Generator dispatched power:
snapshot 0 1 2 3
name
South Africa Coal 35000.0 35000.0 35000.0 35000.0
South Africa Wind 900.0 1800.0 1200.0 1500.0
South Africa Gas 6100.0 6200.0 8000.0 8000.0
South Africa Oil -0.0 -0.0 800.0 1500.0
Zone marginal price:
snapshot 0 1 2 3
name
South Africa 60.0 60.0 80.0 80.0
Single bidding zone with fixed load and storage, several periods
sa_network.add_components(
StorageUnit(
name=f"{country} pumped hydro",
bus=country,
p_nom=1000,
max_hours=6,
)
)
optimized_sa_network, _ = sa_network.optimize()
Generator dispatched power:
snapshot 0 1 2 3
name
South Africa Coal 35000.0 35000.0 35000.0 35000.0
South Africa Wind 900.0 1800.0 1200.0 1500.0
South Africa Gas 6900.0 7200.0 8000.0 8000.0
South Africa Oil -0.0 -0.0 -0.0 500.0
Zone marginal price:
snapshot 0 1 2 3
name
South Africa 60.0 60.0 60.0 80.0
Storage unit:
name South Africa pumped hydro
dispatched power state of charge
snapshot
0 -800.0 800.0
1 -1000.0 1800.0
2 800.0 1000.0
3 1000.0 -0.0