1-Node Capacity Expansion¶
In this example, we build a replica of model.energy. This tool calculates the cost of meeting a constant electricity demand from a combination of wind power, solar power and storage for different regions of the world. It includes capacity investments and dispatch optimisation. We deviate from model.energy by including an electricity demand profiles rather than a constant electricity demand.
import pandas as pd
import pypsa
Techno-economic assumptions¶
We take techno-economic assumptions from the technology-data repository which collects assumptions on costs and efficiencies:
YEAR = 2030
url = f"https://raw.githubusercontent.com/PyPSA/technology-data/master/outputs/costs_{YEAR}.csv"
costs = pd.read_csv(url, index_col=[0, 1])
costs.loc[costs.unit.str.contains("/kW"), "value"] *= 1e3
costs = costs.value.unstack().fillna({"discount rate": 0.07, "lifetime": 20, "FOM": 0})
Based on this, we calculate the marginal costs (€/MWh):
costs["marginal_cost"] = costs["VOM"] + costs["fuel"] / costs["efficiency"]
We also calculate the capital costs (i.e. annualised investment costs, €/MW/a or €/MWh/a for storage), using a small utility function to calculate the annuity factor to annualise investment costs based on is the discount rate $r$ and lifetime $n$.
from pypsa.costs import annuity
a = costs.apply(lambda x: annuity(x["discount rate"], x["lifetime"]), axis=1)
costs["capital_cost"] = (a + costs["FOM"] / 100) * costs["investment"]
Wind, solar and load time series¶
RESOLUTION = 3 # hours
url = "https://tubcloud.tu-berlin.de/s/9toBssWEdaLgHzq/download/time-series.csv"
ts = pd.read_csv(url, index_col=0, parse_dates=True)[::RESOLUTION]
ts.head(3)
Model initialisation¶
n = pypsa.Network()
n.add("Bus", "electricity", carrier="electricity")
n.set_snapshots(ts.index)
The weighting of the snapshots (e.g. how many hours they represent, see $w_t$ in problem formulation above) must be set in n.snapshot_weightings.
n.snapshot_weightings.loc[:, :] = RESOLUTION
Adding carriers (this is only necessary for convenience in plotting later):
carriers = [
"wind",
"solar",
"hydrogen storage",
"battery storage",
"load shedding",
"electrolysis",
"turbine",
"electricity",
"hydrogen",
]
colors = [
"dodgerblue",
"gold",
"black",
"yellowgreen",
"darkorange",
"magenta",
"red",
"grey",
"grey",
]
n.add("Carrier", carriers, color=colors)
Adding load:
n.add(
"Load",
"demand",
bus="electricity",
p_set=ts.load_mw,
)
Add a load shedding generator with high marginal cost of 2000 €/MWh:
n.add(
"Generator",
"load shedding",
bus="electricity",
carrier="load shedding",
marginal_cost=2000,
p_nom=ts.load_mw.max(),
)
Adding the variable renewable generators works almost identically, but we also need to supply the capacity factors to the model via the attribute p_max_pu.
n.add(
"Generator",
"wind",
bus="electricity",
carrier="wind",
p_max_pu=ts.wind_pu,
capital_cost=costs.at["onwind", "capital_cost"],
marginal_cost=costs.at["onwind", "marginal_cost"],
p_nom_extendable=True,
)
n.add(
"Generator",
"solar",
bus="electricity",
carrier="solar",
p_max_pu=ts.pv_pu,
capital_cost=costs.at["solar", "capital_cost"],
marginal_cost=costs.at["solar", "marginal_cost"],
p_nom_extendable=True,
)
Adding a 3-hour battery:
n.add(
"StorageUnit",
"battery storage",
bus="electricity",
carrier="battery storage",
max_hours=3,
capital_cost=costs.at["battery inverter", "capital_cost"]
+ 3 * costs.at["battery storage", "capital_cost"],
efficiency_store=costs.at["battery inverter", "efficiency"],
efficiency_dispatch=costs.at["battery inverter", "efficiency"],
p_nom_extendable=True,
cyclic_state_of_charge=True,
)
Adding a hydrogen storage system consisting of electrolyser, hydrogen turbine and underground storage, which can all be flexibly optimised:
n.add("Bus", "hydrogen", carrier="hydrogen")
n.add(
"Link",
"electrolysis",
bus0="electricity",
bus1="hydrogen",
carrier="electrolysis",
p_nom_extendable=True,
efficiency=costs.at["electrolysis", "efficiency"],
capital_cost=costs.at["electrolysis", "capital_cost"],
)
n.add(
"Link",
"turbine",
bus0="hydrogen",
bus1="electricity",
carrier="turbine",
p_nom_extendable=True,
efficiency=costs.at["OCGT", "efficiency"],
capital_cost=costs.at["OCGT", "capital_cost"] / costs.at["OCGT", "efficiency"],
)
n.add(
"Store",
"hydrogen storage",
bus="hydrogen",
carrier="hydrogen storage",
capital_cost=costs.at["hydrogen storage underground", "capital_cost"],
e_nom_extendable=True,
e_cyclic=True,
)
Model run¶
n.optimize()
Model evaluation¶
Total system cost by technology:
tsc = (
pd.concat([n.statistics.capex(), n.statistics.opex()], axis=1).sum(axis=1).div(1e9)
)
tsc
tsc.sum()
The optimised capacities in GW (GWh for Store component):
n.statistics.optimal_capacity().div(1e3)
Energy balances on electricity side (in TWh):
n.statistics.energy_balance(bus_carrier="electricity").sort_values().div(1e6)
Energy balances plot as time series (in MW):
n.statistics.energy_balance.plot.area(linewidth=0, bus_carrier="electricity")
Price time series for electricity and hydrogen:
n.buses_t.marginal_price.plot(figsize=(7, 2))