Skip to content

Commit

Permalink
Merge pull request #912 from PyPSA/fneum/year-specific-techs
Browse files Browse the repository at this point in the history
 REVIEWED: Add technology specific renewable profiles for different planning horizons
  • Loading branch information
fneum authored Feb 6, 2024
2 parents 6c7d79f + e8df321 commit dce3d81
Show file tree
Hide file tree
Showing 10 changed files with 130 additions and 22 deletions.
2 changes: 1 addition & 1 deletion doc/configtables/offwind-ac.csv
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down
2 changes: 1 addition & 1 deletion doc/configtables/offwind-dc.csv
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down
2 changes: 1 addition & 1 deletion doc/configtables/onwind.csv
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down
2 changes: 1 addition & 1 deletion doc/configtables/solar.csv
Original file line number Diff line number Diff line change
Expand Up @@ -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.°."
Expand Down
6 changes: 6 additions & 0 deletions doc/release_notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,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)
Expand Down
9 changes: 9 additions & 0 deletions rules/solve_myopic.smk
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,16 @@ 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",
network=RESULTS
+ "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc",
network_p=solved_previous_horizon, #solved network at previous time step
Expand Down
55 changes: 55 additions & 0 deletions scripts/add_brownfield.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
import numpy as np
import pandas as pd
import pypsa
import xarray as xr
from _helpers import update_config_with_sector_opts
from add_existing_baseyear import add_build_year_to_new_assets
from pypsa.clustering.spatial import normed_or_uniform

logger = logging.getLogger(__name__)
idx = pd.IndexSlice
Expand Down Expand Up @@ -143,6 +145,57 @@ def disable_grid_expansion_if_LV_limit_hit(n):
n.global_constraints.drop("lv_limit", inplace=True)


def adjust_renewable_profiles(n, input_profiles, params, year):
"""
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)

# 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 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

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"].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


if __name__ == "__main__":
if "snakemake" not in globals():
from _helpers import mock_snakemake
Expand All @@ -167,6 +220,8 @@ def disable_grid_expansion_if_LV_limit_hit(n):

n = pypsa.Network(snakemake.input.network)

adjust_renewable_profiles(n, snakemake.input, snakemake.params, year)

add_build_year_to_new_assets(n, year)

n_p = pypsa.Network(snakemake.input.network_p)
Expand Down
4 changes: 4 additions & 0 deletions scripts/add_electricity.py
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,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()
Expand Down
65 changes: 47 additions & 18 deletions scripts/build_renewable_profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,14 +200,21 @@
if "snakemake" not in globals():
from _helpers import mock_snakemake

snakemake = mock_snakemake("build_renewable_profiles", technology="solar")
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"]
params = snakemake.params.renewable[snakemake.wildcards.technology]
resource = params["resource"] # pv panel params / wind turbine params

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"]
snapshots = snakemake.params.snapshots
Expand Down Expand Up @@ -323,22 +330,40 @@
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()

duration = time.time() - start
logger.info(
f"Completed weighted capacity factor time series calculation ({duration:2.2f}s)"
)
resource[tech] = model

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
Expand All @@ -365,8 +390,8 @@

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"),
Expand All @@ -386,9 +411,13 @@
ds["underwater_fraction"] = xr.DataArray(underwater_fraction, [buses])

# select only buses with some capacity and minimal capacity factor
mean_profile = ds["profile"].mean("time")
if "year" in ds.indexes:
mean_profile = mean_profile.max("year")

ds = ds.sel(
bus=(
(ds["profile"].mean("time") > params.get("min_p_max_pu", 0.0))
(mean_profile > params.get("min_p_max_pu", 0.0))
& (ds["p_nom_max"] > params.get("min_p_nom_max", 0.0))
)
)
Expand Down
5 changes: 5 additions & 0 deletions scripts/prepare_sector_network.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit dce3d81

Please sign in to comment.