Skip to content

Commit

Permalink
streamline code for year-dependent technologies (turbines/panels)
Browse files Browse the repository at this point in the history
  • Loading branch information
fneum committed Feb 5, 2024
1 parent bb4eb12 commit a834ff2
Show file tree
Hide file tree
Showing 12 changed files with 90 additions and 122 deletions.
17 changes: 4 additions & 13 deletions config/config.default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -227,8 +219,7 @@ renewable:
cutout: europe-2013-sarah
resource:
method: pv
panel:
2020: CSi
panel: CSi
orientation:
slope: 35.
azimuth: 180.
Expand Down
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 @@ -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)
==================================
Expand Down
1 change: 0 additions & 1 deletion rules/build_electricity.smk
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions rules/solve_myopic.smk
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
73 changes: 24 additions & 49 deletions scripts/add_brownfield.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
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")
# 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"]:

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

Expand Down Expand Up @@ -245,7 +220,7 @@ def adjust_renewable_profiles(n, input_profiles, config, year):

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)

Expand Down
4 changes: 4 additions & 0 deletions scripts/add_electricity.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
95 changes: 40 additions & 55 deletions scripts/build_renewable_profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,24 +200,20 @@
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"]
Expand Down Expand Up @@ -334,45 +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()

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
Expand All @@ -399,17 +390,14 @@

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
Expand All @@ -425,17 +413,14 @@
# 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))
)
)

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)

Expand Down
Loading

0 comments on commit a834ff2

Please sign in to comment.