streamline code for year-dependent technologies (turbines/panels)

This commit is contained in:
Fabian Neumann 2024-02-05 12:10:35 +01:00
parent bb4eb123e5
commit a834ff222a
12 changed files with 90 additions and 122 deletions

View File

@ -164,14 +164,11 @@ atlite:
# docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#renewable
renewable:
year: 2020
onwind:
cutout: europe-2013-era5
resource:
method: wind
turbine:
2020: Vestas_V112_3MW
2030: NREL_ReferenceTurbine_2020ATB_5.5MW
turbine: Vestas_V112_3MW
add_cutout_windspeed: true
capacity_per_sqkm: 3
# correction_factor: 0.93
@ -190,9 +187,7 @@ renewable:
cutout: europe-2013-era5
resource:
method: wind
turbine:
2020: NREL_ReferenceTurbine_5MW_offshore.yaml
2030: NREL_ReferenceTurbine_2020ATB_15MW_offshore
turbine: NREL_ReferenceTurbine_5MW_offshore
add_cutout_windspeed: true
capacity_per_sqkm: 2
correction_factor: 0.8855
@ -208,10 +203,7 @@ renewable:
cutout: europe-2013-era5
resource:
method: wind
turbine:
2020: Vestas_V164_7MW_offshore
2025: NREL_ReferenceTurbine_2020ATB_15MW_offshore
2030: NREL_ReferenceTurbine_2020ATB_18MW_offshore
turbine: Vestas_V164_7MW_offshore
add_cutout_windspeed: true
capacity_per_sqkm: 2
correction_factor: 0.8855
@ -227,8 +219,7 @@ renewable:
cutout: europe-2013-sarah
resource:
method: pv
panel:
2020: CSi
panel: CSi
orientation:
slope: 35.
azimuth: 180.

View File

@ -2,7 +2,7 @@
cutout,--,"Should be a folder listed in the configuration ``atlite: cutouts:`` (e.g. 'europe-2013-era5') or reference an existing folder in the directory ``cutouts``. Source module must be ERA5.","Specifies the directory where the relevant weather data ist stored."
resource,,,
-- method,--,"Must be 'wind'","A superordinate technology type."
-- turbine,--,"One of turbine types included in `atlite <https://github.com/PyPSA/atlite/tree/master/atlite/resources/windturbine>`_","Specifies the turbine type and its characteristic power curve."
-- turbine,--,"One of turbine types included in `atlite <https://github.com/PyPSA/atlite/tree/master/atlite/resources/windturbine>`_. Can be a string or a dictionary with years as keys which denote the year another turbine model becomes available.","Specifies the turbine type and its characteristic power curve."
capacity_per_sqkm,:math:`MW/km^2`,float,"Allowable density of wind turbine placement."
correction_factor,--,float,"Correction factor for capacity factor time series."
excluder_resolution,m,float,"Resolution on which to perform geographical elibility analysis."

1 Unit Values Description
2 cutout -- Should be a folder listed in the configuration ``atlite: cutouts:`` (e.g. 'europe-2013-era5') or reference an existing folder in the directory ``cutouts``. Source module must be ERA5. Specifies the directory where the relevant weather data ist stored.
3 resource
4 -- method -- Must be 'wind' A superordinate technology type.
5 -- turbine -- One of turbine types included in `atlite <https://github.com/PyPSA/atlite/tree/master/atlite/resources/windturbine>`_ One of turbine types included in `atlite <https://github.com/PyPSA/atlite/tree/master/atlite/resources/windturbine>`_. Can be a string or a dictionary with years as keys which denote the year another turbine model becomes available. Specifies the turbine type and its characteristic power curve.
6 capacity_per_sqkm :math:`MW/km^2` float Allowable density of wind turbine placement.
7 correction_factor -- float Correction factor for capacity factor time series.
8 excluder_resolution m float Resolution on which to perform geographical elibility analysis.

View File

@ -2,7 +2,7 @@
cutout,--,"Should be a folder listed in the configuration ``atlite: cutouts:`` (e.g. 'europe-2013-era5') or reference an existing folder in the directory ``cutouts``. Source module must be ERA5.","Specifies the directory where the relevant weather data ist stored."
resource,,,
-- method,--,"Must be 'wind'","A superordinate technology type."
-- turbine,--,"One of turbine types included in `atlite <https://github.com/PyPSA/atlite/tree/master/atlite/resources/windturbine>`__","Specifies the turbine type and its characteristic power curve."
-- turbine,--,"One of turbine types included in `atlite <https://github.com/PyPSA/atlite/tree/master/atlite/resources/windturbine>`_. Can be a string or a dictionary with years as keys which denote the year another turbine model becomes available.","Specifies the turbine type and its characteristic power curve."
capacity_per_sqkm,:math:`MW/km^2`,float,"Allowable density of wind turbine placement."
correction_factor,--,float,"Correction factor for capacity factor time series."
excluder_resolution,m,float,"Resolution on which to perform geographical elibility analysis."

1 Unit Values Description
2 cutout -- Should be a folder listed in the configuration ``atlite: cutouts:`` (e.g. 'europe-2013-era5') or reference an existing folder in the directory ``cutouts``. Source module must be ERA5. Specifies the directory where the relevant weather data ist stored.
3 resource
4 -- method -- Must be 'wind' A superordinate technology type.
5 -- turbine -- One of turbine types included in `atlite <https://github.com/PyPSA/atlite/tree/master/atlite/resources/windturbine>`__ One of turbine types included in `atlite <https://github.com/PyPSA/atlite/tree/master/atlite/resources/windturbine>`_. Can be a string or a dictionary with years as keys which denote the year another turbine model becomes available. Specifies the turbine type and its characteristic power curve.
6 capacity_per_sqkm :math:`MW/km^2` float Allowable density of wind turbine placement.
7 correction_factor -- float Correction factor for capacity factor time series.
8 excluder_resolution m float Resolution on which to perform geographical elibility analysis.

View File

@ -2,7 +2,7 @@
cutout,--,"Should be a folder listed in the configuration ``atlite: cutouts:`` (e.g. 'europe-2013-era5') or reference an existing folder in the directory ``cutouts``. Source module must be ERA5.","Specifies the directory where the relevant weather data ist stored."
resource,,,
-- method,--,"Must be 'wind'","A superordinate technology type."
-- turbine,--,"One of turbine types included in `atlite <https://github.com/PyPSA/atlite/tree/master/atlite/resources/windturbine>`__","Specifies the turbine type and its characteristic power curve."
-- turbine,--,"One of turbine types included in `atlite <https://github.com/PyPSA/atlite/tree/master/atlite/resources/windturbine>`_. Can be a string or a dictionary with years as keys which denote the year another turbine model becomes available.","Specifies the turbine type and its characteristic power curve."
capacity_per_sqkm,:math:`MW/km^2`,float,"Allowable density of wind turbine placement."
corine,,,
-- grid_codes,--,"Any subset of the `CORINE Land Cover code list <http://www.eea.europa.eu/data-and-maps/data/corine-land-cover-2006-raster-1/corine-land-cover-classes-and/clc_legend.csv/at_download/file>`_","Specifies areas according to CORINE Land Cover codes which are generally eligible for wind turbine placement."

1 Unit Values Description
2 cutout -- Should be a folder listed in the configuration ``atlite: cutouts:`` (e.g. 'europe-2013-era5') or reference an existing folder in the directory ``cutouts``. Source module must be ERA5. Specifies the directory where the relevant weather data ist stored.
3 resource
4 -- method -- Must be 'wind' A superordinate technology type.
5 -- turbine -- One of turbine types included in `atlite <https://github.com/PyPSA/atlite/tree/master/atlite/resources/windturbine>`__ One of turbine types included in `atlite <https://github.com/PyPSA/atlite/tree/master/atlite/resources/windturbine>`_. Can be a string or a dictionary with years as keys which denote the year another turbine model becomes available. Specifies the turbine type and its characteristic power curve.
6 capacity_per_sqkm :math:`MW/km^2` float Allowable density of wind turbine placement.
7 corine
8 -- grid_codes -- Any subset of the `CORINE Land Cover code list <http://www.eea.europa.eu/data-and-maps/data/corine-land-cover-2006-raster-1/corine-land-cover-classes-and/clc_legend.csv/at_download/file>`_ Specifies areas according to CORINE Land Cover codes which are generally eligible for wind turbine placement.

View File

@ -2,7 +2,7 @@
cutout,--,"Should be a folder listed in the configuration ``atlite: cutouts:`` (e.g. 'europe-2013-era5') or reference an existing folder in the directory ``cutouts``. Source module can be ERA5 or SARAH-2.","Specifies the directory where the relevant weather data ist stored that is specified at ``atlite/cutouts`` configuration. Both ``sarah`` and ``era5`` work."
resource,,,
-- method,--,"Must be 'pv'","A superordinate technology type."
-- panel,--,"One of {'Csi', 'CdTe', 'KANENA'} as defined in `atlite <https://github.com/PyPSA/atlite/tree/master/atlite/resources/solarpanel>`__","Specifies the solar panel technology and its characteristic attributes."
-- panel,--,"One of {'Csi', 'CdTe', 'KANENA'} as defined in `atlite <https://github.com/PyPSA/atlite/tree/master/atlite/resources/solarpanel>`_ . Can be a string or a dictionary with years as keys which denote the year another turbine model becomes available.","Specifies the solar panel technology and its characteristic attributes."
-- orientation,,,
-- -- slope,°,"Realistically any angle in [0., 90.]","Specifies the tilt angle (or slope) of the solar panel. A slope of zero corresponds to the face of the panel aiming directly overhead. A positive tilt angle steers the panel towards the equator."
-- -- azimuth,°,"Any angle in [0., 360.]","Specifies the `azimuth <https://en.wikipedia.org/wiki/Azimuth>`_ orientation of the solar panel. South corresponds to 180.°."

1 Unit Values Description
2 cutout -- Should be a folder listed in the configuration ``atlite: cutouts:`` (e.g. 'europe-2013-era5') or reference an existing folder in the directory ``cutouts``. Source module can be ERA5 or SARAH-2. Specifies the directory where the relevant weather data ist stored that is specified at ``atlite/cutouts`` configuration. Both ``sarah`` and ``era5`` work.
3 resource
4 -- method -- Must be 'pv' A superordinate technology type.
5 -- panel -- One of {'Csi', 'CdTe', 'KANENA'} as defined in `atlite <https://github.com/PyPSA/atlite/tree/master/atlite/resources/solarpanel>`__ One of {'Csi', 'CdTe', 'KANENA'} as defined in `atlite <https://github.com/PyPSA/atlite/tree/master/atlite/resources/solarpanel>`_ . Can be a string or a dictionary with years as keys which denote the year another turbine model becomes available. Specifies the solar panel technology and its characteristic attributes.
6 -- orientation
7 -- -- slope ° Realistically any angle in [0., 90.] Specifies the tilt angle (or slope) of the solar panel. A slope of zero corresponds to the face of the panel aiming directly overhead. A positive tilt angle steers the panel towards the equator.
8 -- -- azimuth ° Any angle in [0., 360.] Specifies the `azimuth <https://en.wikipedia.org/wiki/Azimuth>`_ orientation of the solar panel. South corresponds to 180.°.

View File

@ -71,6 +71,12 @@ Upcoming Release
Energiewende (2021)
<https://static.agora-energiewende.de/fileadmin/Projekte/2021/2021_02_EU_CEAP/A-EW_254_Mobilising-circular-economy_study_WEB.pdf>`_.
* Added option to specify turbine and solar panel models for specific years as a
dictionary (e.g. ``renewable: onwind: resource: turbine:``). The years will be
interpreted as years from when the the corresponding turbine model substitutes
the previous model for new installations. This will only have an effect on
workflows with foresight "myopic" and still needs to be added foresight option
"perfect".
PyPSA-Eur 0.9.0 (5th January 2024)
==================================

View File

@ -261,7 +261,6 @@ rule build_renewable_profiles:
params:
snapshots={k: config["snapshots"][k] for k in ["start", "end", "inclusive"]},
renewable=config["renewable"],
foresight=config["foresight"],
input:
**opt,
base_network=RESOURCES + "networks/base.nc",

View File

@ -85,10 +85,13 @@ rule add_brownfield:
H2_retrofit=config["sector"]["H2_retrofit"],
H2_retrofit_capacity_per_CH4=config["sector"]["H2_retrofit_capacity_per_CH4"],
threshold_capacity=config["existing_capacities"]["threshold_capacity"],
snapshots={k: config["snapshots"][k] for k in ["start", "end", "inclusive"]},
carriers=config["electricity"]["renewable_carriers"],
input:
**{
f"profile_{tech}": RESOURCES + f"profile_{tech}.nc"
for tech in config["electricity"]["renewable_carriers"]
if tech != "hydro"
},
simplify_busmap=RESOURCES + "busmap_elec_s{simpl}.csv",
cluster_busmap=RESOURCES + "busmap_elec_s{simpl}_{clusters}.csv",

View File

@ -145,78 +145,53 @@ def disable_grid_expansion_if_LV_limit_hit(n):
n.global_constraints.drop("lv_limit", inplace=True)
def adjust_renewable_profiles(n, input_profiles, config, year):
def adjust_renewable_profiles(n, input_profiles, params, year):
"""
Adjusts renewable profiles according to the renewable technology specified.
If the planning horizon is not available, the closest year is used
instead.
Adjusts renewable profiles according to the renewable technology specified,
using the latest year below or equal to the selected year.
"""
# spatial clustering
cluster_busmap = pd.read_csv(snakemake.input.cluster_busmap, index_col=0).squeeze()
simplify_busmap = pd.read_csv(
snakemake.input.simplify_busmap, index_col=0
).squeeze()
clustermaps = simplify_busmap.map(cluster_busmap)
clustermaps.index = clustermaps.index.astype(str)
dr = pd.date_range(**config["snapshots"], freq="H")
# temporal clustering
dr = pd.date_range(**params["snapshots"], freq="h")
snapshotmaps = (
pd.Series(dr, index=dr).where(lambda x: x.isin(n.snapshots), pd.NA).ffill()
)
for carrier in config["electricity"]["renewable_carriers"]:
if carrier == "hydro":
continue
clustermaps.index = clustermaps.index.astype(str)
dr = pd.date_range(**config["snapshots"], freq="H")
snapshotmaps = (
pd.Series(dr, index=dr).where(lambda x: x.isin(n.snapshots), pd.NA).ffill()
)
for carrier in config["electricity"]["renewable_carriers"]:
for carrier in params["carriers"]:
if carrier == "hydro":
continue
with xr.open_dataset(getattr(input_profiles, "profile_" + carrier)) as ds:
if ds.indexes["bus"].empty or "year" not in ds.indexes:
continue
if year in ds.indexes["year"]:
p_max_pu = (
ds["year_profiles"]
.sel(year=year)
.transpose("time", "bus")
.to_pandas()
)
else:
available_previous_years = [
available_year
for available_year in ds.indexes["year"]
if available_year < year
]
available_following_years = [
available_year
for available_year in ds.indexes["year"]
if available_year > year
]
if available_previous_years:
closest_year = max(available_previous_years)
if available_following_years:
closest_year = min(available_following_years)
logging.warning(
f"Planning horizon {year} not in {carrier} profiles. Using closest year {closest_year} instead."
)
p_max_pu = (
ds["year_profiles"]
.sel(year=closest_year)
.transpose("time", "bus")
.to_pandas()
)
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()
)
# spatial clustering
weight = ds["weight"].to_pandas()
weight = ds["weight"].sel(year=closest_year).to_pandas()
weight = weight.groupby(clustermaps).transform(normed_or_uniform)
p_max_pu = (p_max_pu * weight).T.groupby(clustermaps).sum().T
p_max_pu.columns = p_max_pu.columns + f" {carrier}"
# temporal_clustering
p_max_pu = p_max_pu.groupby(snapshotmaps).mean()
# replace renewable time series
n.generators_t.p_max_pu.loc[:, p_max_pu.columns] = p_max_pu
@ -245,7 +220,7 @@ if __name__ == "__main__":
n = pypsa.Network(snakemake.input.network)
adjust_renewable_profiles(n, snakemake.input, snakemake.config, year)
adjust_renewable_profiles(n, snakemake.input, snakemake.params, year)
add_build_year_to_new_assets(n, year)

View File

@ -374,6 +374,10 @@ def attach_wind_and_solar(
if ds.indexes["bus"].empty:
continue
# if-statement for compatibility with old profiles
if "year" in ds.indexes:
ds = ds.sel(year=ds.year.min(), drop=True)
supcar = car.split("-", 2)[0]
if supcar == "offwind":
underwater_fraction = ds["underwater_fraction"].to_pandas()

View File

@ -200,24 +200,20 @@ if __name__ == "__main__":
if "snakemake" not in globals():
from _helpers import mock_snakemake
snakemake = mock_snakemake("build_renewable_profiles", technology="onwind")
snakemake = mock_snakemake("build_renewable_profiles", technology="offwind-dc")
configure_logging(snakemake)
nprocesses = int(snakemake.threads)
noprogress = snakemake.config["run"].get("disable_progressbar", True)
noprogress = noprogress or not snakemake.config["atlite"]["show_progress"]
year = snakemake.params.renewable["year"]
foresight = snakemake.params.foresight
params = snakemake.params.renewable[snakemake.wildcards.technology]
resource = params["resource"] # pv panel params / wind turbine params
year_dependent_techs = {
k: resource.get(k)
for k in ["panel", "turbine"]
if isinstance(resource.get(k), dict)
}
for key, techs in year_dependent_techs.items():
resource[key] = resource[key][year]
tech = next(t for t in ["panel", "turbine"] if t in resource)
models = resource[tech]
if not isinstance(models, dict):
models = {0: models}
resource[tech] = models[next(iter(models))]
correction_factor = params.get("correction_factor", 1.0)
capacity_per_sqkm = params["capacity_per_sqkm"]
@ -334,45 +330,40 @@ if __name__ == "__main__":
duration = time.time() - start
logger.info(f"Completed average capacity factor calculation ({duration:2.2f}s)")
logger.info("Calculate weighted capacity factor time series...")
start = time.time()
profiles = []
capacities = []
for year, model in models.items():
profile, capacities = func(
matrix=availability.stack(spatial=["y", "x"]),
layout=layout,
index=buses,
per_unit=True,
return_capacity=True,
**resource,
)
logger.info(
f"Calculate weighted capacity factor time series for model {model}..."
)
start = time.time()
if year_dependent_techs and foresight != "overnight":
for key, techs in year_dependent_techs.items():
year_profiles = list()
tech_profiles = dict()
tech_profiles[resource[key]] = profile
for year, tech in techs.items():
resource[key] = tech
if tech not in tech_profiles:
tech_profiles[tech] = func(
matrix=availability.stack(spatial=["y", "x"]),
layout=layout,
index=buses,
per_unit=True,
return_capacity=False,
**resource,
)
year_profile = tech_profiles[tech]
year_profile = year_profile.expand_dims({"year": [year]}).rename(
"year_profiles"
)
year_profiles.append(year_profile)
year_profiles = xr.merge(year_profiles)
resource[tech] = model
duration = time.time() - start
logger.info(
f"Completed weighted capacity factor time series calculation ({duration:2.2f}s)"
)
profile, capacity = func(
matrix=availability.stack(spatial=["y", "x"]),
layout=layout,
index=buses,
per_unit=True,
return_capacity=True,
**resource,
)
dim = {"year": [year]}
profile = profile.expand_dims(dim)
capacity = capacity.expand_dims(dim)
profiles.append(profile.rename("profile"))
capacities.append(capacity.rename("weight"))
duration = time.time() - start
logger.info(
f"Completed weighted capacity factor time series calculation for model {model} ({duration:2.2f}s)"
)
profiles = xr.merge(profiles)
capacities = xr.merge(capacities)
logger.info("Calculating maximal capacity per bus")
p_nom_max = capacity_per_sqkm * availability @ area
@ -399,17 +390,14 @@ if __name__ == "__main__":
ds = xr.merge(
[
(correction_factor * profile).rename("profile"),
capacities.rename("weight"),
correction_factor * profiles,
capacities,
p_nom_max.rename("p_nom_max"),
potential.rename("potential"),
average_distance.rename("average_distance"),
]
)
if year_dependent_techs:
ds = xr.merge([ds, year_profiles * correction_factor])
if snakemake.wildcards.technology.startswith("offwind"):
logger.info("Calculate underwater fraction of connections.")
offshore_shape = gpd.read_file(snakemake.input["offshore_shapes"]).unary_union
@ -425,7 +413,7 @@ if __name__ == "__main__":
# select only buses with some capacity and minimal capacity factor
ds = ds.sel(
bus=(
(ds["profile"].mean("time") > params.get("min_p_max_pu", 0.0))
(ds["profile"].mean("time").max("year") > params.get("min_p_max_pu", 0.0))
& (ds["p_nom_max"] > params.get("min_p_nom_max", 0.0))
)
)
@ -433,9 +421,6 @@ if __name__ == "__main__":
if "clip_p_max_pu" in params:
min_p_max_pu = params["clip_p_max_pu"]
ds["profile"] = ds["profile"].where(ds["profile"] >= min_p_max_pu, 0)
ds["year_profiles"] = ds["year_profiles"].where(
ds["year_profiles"] >= min_p_max_pu, 0
)
ds.to_netcdf(snakemake.output.profile)

View File

@ -421,6 +421,11 @@ def update_wind_solar_costs(n, costs):
tech = "offwind-" + connection
profile = snakemake.input["profile_offwind_" + connection]
with xr.open_dataset(profile) as ds:
# if-statement for compatibility with old profiles
if "year" in ds.indexes:
ds = ds.sel(year=ds.year.min(), drop=True)
underwater_fraction = ds["underwater_fraction"].to_pandas()
connection_cost = (
snakemake.params.length_factor