2020-07-07 16:20:51 +00:00
|
|
|
# -*- coding: utf-8 -*-
|
2024-02-19 15:21:48 +00:00
|
|
|
# SPDX-FileCopyrightText: : 2020-2024 The PyPSA-Eur Authors
|
2023-03-06 17:49:23 +00:00
|
|
|
#
|
|
|
|
# SPDX-License-Identifier: MIT
|
2023-03-09 11:45:43 +00:00
|
|
|
"""
|
|
|
|
Prepares brownfield data from previous planning horizon.
|
|
|
|
"""
|
|
|
|
|
2020-07-07 16:20:51 +00:00
|
|
|
import logging
|
2023-03-06 08:27:45 +00:00
|
|
|
|
2022-01-07 10:38:25 +00:00
|
|
|
import numpy as np
|
2024-01-19 09:47:58 +00:00
|
|
|
import pandas as pd
|
2020-07-07 16:20:51 +00:00
|
|
|
import pypsa
|
2024-01-11 16:23:25 +00:00
|
|
|
import xarray as xr
|
2024-02-12 10:53:20 +00:00
|
|
|
from _helpers import (
|
|
|
|
configure_logging,
|
2024-03-14 18:06:21 +00:00
|
|
|
get_snapshots,
|
2024-02-12 10:53:20 +00:00
|
|
|
set_scenario_config,
|
2024-02-17 16:16:28 +00:00
|
|
|
update_config_from_wildcards,
|
2024-02-12 10:54:13 +00:00
|
|
|
)
|
2020-08-12 16:08:01 +00:00
|
|
|
from add_existing_baseyear import add_build_year_to_new_assets
|
2024-01-11 16:23:25 +00:00
|
|
|
from pypsa.clustering.spatial import normed_or_uniform
|
2020-07-07 16:20:51 +00:00
|
|
|
|
2024-01-19 09:47:58 +00:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
idx = pd.IndexSlice
|
|
|
|
|
2020-07-30 06:27:33 +00:00
|
|
|
|
2020-07-08 14:28:08 +00:00
|
|
|
def add_brownfield(n, n_p, year):
|
2023-02-24 13:42:51 +00:00
|
|
|
logger.info(f"Preparing brownfield for the year {year}")
|
2020-07-30 06:27:33 +00:00
|
|
|
|
2022-04-12 08:03:04 +00:00
|
|
|
# electric transmission grid set optimised capacities of previous as minimum
|
|
|
|
n.lines.s_nom_min = n_p.lines.s_nom_opt
|
|
|
|
dc_i = n.links[n.links.carrier == "DC"].index
|
|
|
|
n.links.loc[dc_i, "p_nom_min"] = n_p.links.loc[dc_i, "p_nom_opt"]
|
|
|
|
|
2020-08-12 16:08:01 +00:00
|
|
|
for c in n_p.iterate_components(["Link", "Generator", "Store"]):
|
|
|
|
attr = "e" if c.name == "Store" else "p"
|
|
|
|
|
2021-07-01 18:09:04 +00:00
|
|
|
# first, remove generators, links and stores that track
|
|
|
|
# CO2 or global EU values since these are already in n
|
2022-01-07 10:38:25 +00:00
|
|
|
n_p.mremove(c.name, c.df.index[c.df.lifetime == np.inf])
|
2020-08-12 16:08:01 +00:00
|
|
|
|
2024-04-11 11:54:33 +00:00
|
|
|
# remove assets whose build_year + lifetime <= year
|
|
|
|
n_p.mremove(c.name, c.df.index[c.df.build_year + c.df.lifetime <= year])
|
2020-08-12 16:08:01 +00:00
|
|
|
|
2021-07-01 18:09:04 +00:00
|
|
|
# remove assets if their optimized nominal capacity is lower than a threshold
|
|
|
|
# since CHP heat Link is proportional to CHP electric Link, make sure threshold is compatible
|
|
|
|
chp_heat = c.df.index[
|
|
|
|
(c.df[f"{attr}_nom_extendable"] & c.df.index.str.contains("urban central"))
|
2023-10-08 09:20:36 +00:00
|
|
|
& c.df.index.str.contains("CHP")
|
|
|
|
& c.df.index.str.contains("heat")
|
2021-07-01 18:09:04 +00:00
|
|
|
]
|
|
|
|
|
2023-06-15 16:52:25 +00:00
|
|
|
threshold = snakemake.params.threshold_capacity
|
2022-01-07 10:38:25 +00:00
|
|
|
|
2020-08-14 07:11:19 +00:00
|
|
|
if not chp_heat.empty:
|
2021-07-01 18:09:04 +00:00
|
|
|
threshold_chp_heat = (
|
|
|
|
threshold
|
|
|
|
* c.df.efficiency[chp_heat.str.replace("heat", "electric")].values
|
|
|
|
* c.df.p_nom_ratio[chp_heat.str.replace("heat", "electric")].values
|
|
|
|
/ c.df.efficiency[chp_heat].values
|
|
|
|
)
|
|
|
|
n_p.mremove(
|
|
|
|
c.name,
|
2023-10-08 09:20:36 +00:00
|
|
|
chp_heat[c.df.loc[chp_heat, f"{attr}_nom_opt"] < threshold_chp_heat],
|
2021-07-01 18:09:04 +00:00
|
|
|
)
|
2022-01-07 10:38:25 +00:00
|
|
|
|
2021-07-01 18:09:04 +00:00
|
|
|
n_p.mremove(
|
|
|
|
c.name,
|
|
|
|
c.df.index[
|
2023-10-08 09:20:36 +00:00
|
|
|
(c.df[f"{attr}_nom_extendable"] & ~c.df.index.isin(chp_heat))
|
|
|
|
& (c.df[f"{attr}_nom_opt"] < threshold)
|
2023-03-06 08:27:45 +00:00
|
|
|
],
|
2021-07-01 18:09:04 +00:00
|
|
|
)
|
2020-08-12 16:08:01 +00:00
|
|
|
|
2021-07-01 18:09:04 +00:00
|
|
|
# copy over assets but fix their capacity
|
2023-10-08 09:20:36 +00:00
|
|
|
c.df[f"{attr}_nom"] = c.df[f"{attr}_nom_opt"]
|
|
|
|
c.df[f"{attr}_nom_extendable"] = False
|
2020-08-12 16:08:01 +00:00
|
|
|
|
2021-07-01 18:09:04 +00:00
|
|
|
n.import_components_from_dataframe(c.df, c.name)
|
2020-08-12 16:08:01 +00:00
|
|
|
|
2021-07-01 18:09:04 +00:00
|
|
|
# copy time-dependent
|
|
|
|
selection = n.component_attrs[c.name].type.str.contains(
|
|
|
|
"series"
|
|
|
|
) & n.component_attrs[c.name].status.str.contains("Input")
|
|
|
|
for tattr in n.component_attrs[c.name].index[selection]:
|
|
|
|
n.import_series_from_dataframe(c.pnl[tattr], c.name, tattr)
|
2020-07-30 06:27:33 +00:00
|
|
|
|
2024-04-23 11:58:24 +00:00
|
|
|
# deal with gas network
|
|
|
|
if snakemake.params.H2_retrofit:
|
2024-09-25 11:23:50 +00:00
|
|
|
# subtract the already retrofitted from the maximum capacity
|
2024-04-23 11:58:24 +00:00
|
|
|
h2_retrofitted_fixed_i = n.links[
|
|
|
|
(n.links.carrier == "H2 pipeline retrofitted")
|
|
|
|
& (n.links.build_year != year)
|
|
|
|
].index
|
2024-09-25 11:23:50 +00:00
|
|
|
h2_retrofitted = n.links[
|
|
|
|
(n.links.carrier == "H2 pipeline retrofitted")
|
|
|
|
& (n.links.build_year == year)
|
|
|
|
].index
|
|
|
|
|
|
|
|
# pipe capacity always set in prepare_sector_network to todays gas grid capacity * H2_per_CH4
|
|
|
|
# and is therefore constant up to this point
|
|
|
|
pipe_capacity = n.links.loc[h2_retrofitted, "p_nom_max"]
|
2024-04-23 11:58:24 +00:00
|
|
|
# already retrofitted capacity from gas -> H2
|
|
|
|
already_retrofitted = (
|
|
|
|
n.links.loc[h2_retrofitted_fixed_i, "p_nom"]
|
2024-09-25 11:23:50 +00:00
|
|
|
.rename(lambda x: x.split("-2")[0] + f"-{year}")
|
2024-04-23 11:58:24 +00:00
|
|
|
.groupby(level=0)
|
|
|
|
.sum()
|
|
|
|
)
|
2024-09-25 11:23:50 +00:00
|
|
|
remaining_capacity = pipe_capacity - already_retrofitted.reindex(
|
2024-04-23 11:58:24 +00:00
|
|
|
index=pipe_capacity.index
|
|
|
|
).fillna(0)
|
2024-09-25 11:23:50 +00:00
|
|
|
n.links.loc[h2_retrofitted, "p_nom_max"] = remaining_capacity
|
|
|
|
|
|
|
|
# reduce gas network capacity
|
|
|
|
gas_pipes_i = n.links[n.links.carrier == "gas pipeline"].index
|
|
|
|
if not gas_pipes_i.empty:
|
|
|
|
# subtract the already retrofitted from today's gas grid capacity
|
|
|
|
pipe_capacity = n.links.loc[gas_pipes_i, "p_nom"]
|
|
|
|
fr = "H2 pipeline retrofitted"
|
|
|
|
to = "gas pipeline"
|
|
|
|
CH4_per_H2 = 1 / snakemake.params.H2_retrofit_capacity_per_CH4
|
|
|
|
already_retrofitted.index = already_retrofitted.index.str.replace(fr, to)
|
|
|
|
remaining_capacity = (
|
|
|
|
pipe_capacity
|
|
|
|
- CH4_per_H2
|
|
|
|
* already_retrofitted.reindex(index=pipe_capacity.index).fillna(0)
|
|
|
|
)
|
|
|
|
n.links.loc[gas_pipes_i, "p_nom"] = remaining_capacity
|
|
|
|
n.links.loc[gas_pipes_i, "p_nom_max"] = remaining_capacity
|
2020-07-30 06:27:33 +00:00
|
|
|
|
2024-01-03 12:35:11 +00:00
|
|
|
|
2024-02-28 14:01:36 +00:00
|
|
|
def disable_grid_expansion_if_limit_hit(n):
|
|
|
|
"""
|
|
|
|
Check if transmission expansion limit is already reached; then turn off.
|
|
|
|
|
|
|
|
In particular, this function checks if the total transmission
|
|
|
|
capital cost or volume implied by s_nom_min and p_nom_min are
|
|
|
|
numerically close to the respective global limit set in
|
|
|
|
n.global_constraints. If so, the nominal capacities are set to the
|
|
|
|
minimum and extendable is turned off; the corresponding global
|
|
|
|
constraint is then dropped.
|
|
|
|
"""
|
2024-05-21 15:47:11 +00:00
|
|
|
types = {"expansion_cost": "capital_cost", "volume_expansion": "length"}
|
|
|
|
for limit_type in types:
|
|
|
|
glcs = n.global_constraints.query(f"type == 'transmission_{limit_type}_limit'")
|
2023-12-08 16:53:28 +00:00
|
|
|
|
2024-02-28 14:01:36 +00:00
|
|
|
for name, glc in glcs.iterrows():
|
|
|
|
total_expansion = (
|
|
|
|
(
|
2024-03-19 07:54:58 +00:00
|
|
|
n.lines.query("s_nom_extendable")
|
2024-05-21 15:47:11 +00:00
|
|
|
.eval(f"s_nom_min * {types[limit_type]}")
|
2024-02-28 14:01:36 +00:00
|
|
|
.sum()
|
|
|
|
)
|
|
|
|
+ (
|
|
|
|
n.links.query("carrier == 'DC' and p_nom_extendable")
|
2024-05-21 15:47:11 +00:00
|
|
|
.eval(f"p_nom_min * {types[limit_type]}")
|
2024-02-28 14:01:36 +00:00
|
|
|
.sum()
|
|
|
|
)
|
|
|
|
).sum()
|
|
|
|
|
|
|
|
# Allow small numerical differences
|
|
|
|
if np.abs(glc.constant - total_expansion) / glc.constant < 1e-6:
|
|
|
|
logger.info(
|
|
|
|
f"Transmission expansion {limit_type} is already reached, disabling expansion and limit"
|
|
|
|
)
|
|
|
|
extendable_acs = n.lines.query("s_nom_extendable").index
|
|
|
|
n.lines.loc[extendable_acs, "s_nom_extendable"] = False
|
|
|
|
n.lines.loc[extendable_acs, "s_nom"] = n.lines.loc[
|
|
|
|
extendable_acs, "s_nom_min"
|
|
|
|
]
|
|
|
|
|
|
|
|
extendable_dcs = n.links.query(
|
|
|
|
"carrier == 'DC' and p_nom_extendable"
|
|
|
|
).index
|
|
|
|
n.links.loc[extendable_dcs, "p_nom_extendable"] = False
|
|
|
|
n.links.loc[extendable_dcs, "p_nom"] = n.links.loc[
|
|
|
|
extendable_dcs, "p_nom_min"
|
|
|
|
]
|
|
|
|
|
|
|
|
n.global_constraints.drop(name, inplace=True)
|
2023-12-08 16:53:28 +00:00
|
|
|
|
2022-04-12 08:03:04 +00:00
|
|
|
|
2024-02-05 11:10:35 +00:00
|
|
|
def adjust_renewable_profiles(n, input_profiles, params, year):
|
2024-01-11 16:23:25 +00:00
|
|
|
"""
|
2024-02-05 11:10:35 +00:00
|
|
|
Adjusts renewable profiles according to the renewable technology specified,
|
|
|
|
using the latest year below or equal to the selected year.
|
2024-01-11 16:23:25 +00:00
|
|
|
"""
|
|
|
|
|
2024-02-05 11:10:35 +00:00
|
|
|
# temporal clustering
|
2024-03-14 17:46:45 +00:00
|
|
|
dr = get_snapshots(params["snapshots"], params["drop_leap_day"])
|
2024-01-11 16:23:25 +00:00
|
|
|
snapshotmaps = (
|
|
|
|
pd.Series(dr, index=dr).where(lambda x: x.isin(n.snapshots), pd.NA).ffill()
|
|
|
|
)
|
2024-02-05 11:10:35 +00:00
|
|
|
|
|
|
|
for carrier in params["carriers"]:
|
2024-05-15 16:02:15 +00:00
|
|
|
if carrier == "hydro":
|
2024-05-15 13:04:04 +00:00
|
|
|
continue
|
|
|
|
|
2024-01-11 16:23:25 +00:00
|
|
|
with xr.open_dataset(getattr(input_profiles, "profile_" + carrier)) as ds:
|
|
|
|
if ds.indexes["bus"].empty or "year" not in ds.indexes:
|
|
|
|
continue
|
2024-02-05 11:10:35 +00:00
|
|
|
|
|
|
|
closest_year = max(
|
|
|
|
(y for y in ds.year.values if y <= year), default=min(ds.year.values)
|
|
|
|
)
|
|
|
|
|
|
|
|
p_max_pu = (
|
|
|
|
ds["profile"]
|
|
|
|
.sel(year=closest_year)
|
|
|
|
.transpose("time", "bus")
|
|
|
|
.to_pandas()
|
|
|
|
)
|
2024-01-11 16:23:25 +00:00
|
|
|
p_max_pu.columns = p_max_pu.columns + f" {carrier}"
|
2024-02-05 11:10:35 +00:00
|
|
|
|
2024-01-11 16:23:25 +00:00
|
|
|
# temporal_clustering
|
|
|
|
p_max_pu = p_max_pu.groupby(snapshotmaps).mean()
|
2024-02-05 11:10:35 +00:00
|
|
|
|
2024-01-11 16:23:25 +00:00
|
|
|
# replace renewable time series
|
|
|
|
n.generators_t.p_max_pu.loc[:, p_max_pu.columns] = p_max_pu
|
|
|
|
|
|
|
|
|
2020-07-07 16:20:51 +00:00
|
|
|
if __name__ == "__main__":
|
|
|
|
if "snakemake" not in globals():
|
2023-03-06 18:09:45 +00:00
|
|
|
from _helpers import mock_snakemake
|
2023-03-06 08:27:45 +00:00
|
|
|
|
2021-07-01 18:09:04 +00:00
|
|
|
snakemake = mock_snakemake(
|
|
|
|
"add_brownfield",
|
2022-03-18 12:46:40 +00:00
|
|
|
clusters="37",
|
2022-01-07 11:45:33 +00:00
|
|
|
opts="",
|
2023-03-09 07:36:18 +00:00
|
|
|
ll="v1.0",
|
2024-01-24 09:17:26 +00:00
|
|
|
sector_opts="168H-T-H-B-I-dist1",
|
2021-07-01 18:09:04 +00:00
|
|
|
planning_horizons=2030,
|
2020-07-07 16:20:51 +00:00
|
|
|
)
|
|
|
|
|
2024-02-12 10:53:20 +00:00
|
|
|
configure_logging(snakemake)
|
|
|
|
set_scenario_config(snakemake)
|
2023-03-06 08:27:45 +00:00
|
|
|
|
2024-02-17 10:57:16 +00:00
|
|
|
update_config_from_wildcards(snakemake.config, snakemake.wildcards)
|
2022-07-20 09:35:12 +00:00
|
|
|
|
2023-02-24 13:42:51 +00:00
|
|
|
logger.info(f"Preparing brownfield from the file {snakemake.input.network_p}")
|
2020-07-07 16:20:51 +00:00
|
|
|
|
2021-07-01 18:09:04 +00:00
|
|
|
year = int(snakemake.wildcards.planning_horizons)
|
2020-07-07 16:20:51 +00:00
|
|
|
|
2023-07-13 20:31:55 +00:00
|
|
|
n = pypsa.Network(snakemake.input.network)
|
2020-07-30 06:27:33 +00:00
|
|
|
|
2024-02-05 11:10:35 +00:00
|
|
|
adjust_renewable_profiles(n, snakemake.input, snakemake.params, year)
|
2024-01-11 16:23:25 +00:00
|
|
|
|
2020-08-12 16:08:01 +00:00
|
|
|
add_build_year_to_new_assets(n, year)
|
|
|
|
|
2023-07-13 20:31:55 +00:00
|
|
|
n_p = pypsa.Network(snakemake.input.network_p)
|
2021-07-01 18:09:04 +00:00
|
|
|
|
2020-07-08 14:28:08 +00:00
|
|
|
add_brownfield(n, n_p, year)
|
2020-07-30 06:27:33 +00:00
|
|
|
|
2024-02-28 14:01:36 +00:00
|
|
|
disable_grid_expansion_if_limit_hit(n)
|
2023-12-08 16:53:28 +00:00
|
|
|
|
2022-06-30 06:42:18 +00:00
|
|
|
n.meta = dict(snakemake.config, **dict(wildcards=dict(snakemake.wildcards)))
|
2020-07-30 06:27:33 +00:00
|
|
|
n.export_to_netcdf(snakemake.output[0])
|