From a4802b701843062436d0f36562f562fb1c60bf24 Mon Sep 17 00:00:00 2001
From: Philipp Glaum
Date: Thu, 11 Jan 2024 17:23:25 +0100
Subject: [PATCH 1/6] initial implementation tech specific renewable profiles
---
config/config.default.yaml | 17 ++++--
rules/build_electricity.smk | 1 +
rules/solve_myopic.smk | 6 +++
scripts/add_brownfield.py | 80 +++++++++++++++++++++++++++++
scripts/build_renewable_profiles.py | 42 ++++++++++++++-
5 files changed, 141 insertions(+), 5 deletions(-)
diff --git a/config/config.default.yaml b/config/config.default.yaml
index 33078468..aed7c61b 100644
--- a/config/config.default.yaml
+++ b/config/config.default.yaml
@@ -153,11 +153,14 @@ 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: Vestas_V112_3MW
+ turbine:
+ 2020: Vestas_V112_3MW
+ 2030: NREL_ReferenceTurbine_2020ATB_5.5MW
add_cutout_windspeed: true
capacity_per_sqkm: 3
# correction_factor: 0.93
@@ -176,7 +179,9 @@ renewable:
cutout: europe-2013-era5
resource:
method: wind
- turbine: NREL_ReferenceTurbine_2020ATB_5.5MW
+ turbine:
+ 2020: NREL_ReferenceTurbine_5MW_offshore.yaml
+ 2030: NREL_ReferenceTurbine_2020ATB_15MW_offshore
add_cutout_windspeed: true
capacity_per_sqkm: 2
correction_factor: 0.8855
@@ -192,7 +197,10 @@ renewable:
cutout: europe-2013-era5
resource:
method: wind
- turbine: NREL_ReferenceTurbine_2020ATB_5.5MW
+ turbine:
+ 2020: Vestas_V164_7MW_offshore
+ 2025: NREL_ReferenceTurbine_2020ATB_15MW_offshore
+ 2030: NREL_ReferenceTurbine_2020ATB_18MW_offshore
add_cutout_windspeed: true
capacity_per_sqkm: 2
correction_factor: 0.8855
@@ -208,7 +216,8 @@ renewable:
cutout: europe-2013-sarah
resource:
method: pv
- panel: CSi
+ panel:
+ 2020: CSi
orientation:
slope: 35.
azimuth: 180.
diff --git a/rules/build_electricity.smk b/rules/build_electricity.smk
index 055cffca..95103c47 100644
--- a/rules/build_electricity.smk
+++ b/rules/build_electricity.smk
@@ -259,6 +259,7 @@ else:
rule build_renewable_profiles:
params:
renewable=config["renewable"],
+ foresight=config["foresight"],
input:
**opt,
base_network=RESOURCES + "networks/base.nc",
diff --git a/rules/solve_myopic.smk b/rules/solve_myopic.smk
index 7ca8857d..a1bad5b1 100644
--- a/rules/solve_myopic.smk
+++ b/rules/solve_myopic.smk
@@ -51,6 +51,12 @@ rule add_brownfield:
H2_retrofit_capacity_per_CH4=config["sector"]["H2_retrofit_capacity_per_CH4"],
threshold_capacity=config["existing_capacities"]["threshold_capacity"],
input:
+ **{
+ f"profile_{tech}": RESOURCES + f"profile_{tech}.nc"
+ for tech in config["electricity"]["renewable_carriers"]
+ },
+ 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
diff --git a/scripts/add_brownfield.py b/scripts/add_brownfield.py
index 9ddd3d99..4ce2bd4d 100644
--- a/scripts/add_brownfield.py
+++ b/scripts/add_brownfield.py
@@ -16,8 +16,10 @@ idx = pd.IndexSlice
import numpy as np
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
def add_brownfield(n, n_p, year):
@@ -147,6 +149,82 @@ 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):
+ """
+ Adjusts renewable profiles according to the renewable technology specified.
+
+ If the planning horizon is not available, the closest year is used
+ instead.
+ """
+
+ 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")
+ 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
+ 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()
+ )
+ # spatial clustering
+ weight = ds["weight"].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
@@ -171,6 +249,8 @@ if __name__ == "__main__":
n = pypsa.Network(snakemake.input.network)
+ adjust_renewable_profiles(n, snakemake.input, snakemake.config, year)
+
add_build_year_to_new_assets(n, year)
n_p = pypsa.Network(snakemake.input.network_p)
diff --git a/scripts/build_renewable_profiles.py b/scripts/build_renewable_profiles.py
index b736f68a..4b326da6 100644
--- a/scripts/build_renewable_profiles.py
+++ b/scripts/build_renewable_profiles.py
@@ -200,14 +200,25 @@ if __name__ == "__main__":
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="onwind")
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]
+
correction_factor = params.get("correction_factor", 1.0)
capacity_per_sqkm = params["capacity_per_sqkm"]
@@ -334,6 +345,29 @@ if __name__ == "__main__":
**resource,
)
+ 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)
+
duration = time.time() - start
logger.info(
f"Completed weighted capacity factor time series calculation ({duration:2.2f}s)"
@@ -372,6 +406,9 @@ if __name__ == "__main__":
]
)
+ 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
@@ -395,6 +432,9 @@ 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)
From 5ef317ac399ac86efcd6b6c907f1a8c8426c757e Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Thu, 11 Jan 2024 16:27:29 +0000
Subject: [PATCH 2/6] [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
---
doc/release_notes.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/doc/release_notes.rst b/doc/release_notes.rst
index be6be9fd..52b2ddc2 100644
--- a/doc/release_notes.rst
+++ b/doc/release_notes.rst
@@ -12,7 +12,7 @@ Upcoming Release
* New configuration option ``everywhere_powerplants`` to build conventional powerplants everywhere, irrespective of existing powerplants locations, in the network (https://github.com/PyPSA/pypsa-eur/pull/850).
-* Remove option for wave energy as technology data is not maintained.
+* Remove option for wave energy as technology data is not maintained.
PyPSA-Eur 0.9.0 (5th January 2024)
From a834ff222acac9175b66285077b35baf7ceeb037 Mon Sep 17 00:00:00 2001
From: Fabian Neumann
Date: Mon, 5 Feb 2024 12:10:35 +0100
Subject: [PATCH 3/6] streamline code for year-dependent technologies
(turbines/panels)
---
config/config.default.yaml | 17 ++----
doc/configtables/offwind-ac.csv | 2 +-
doc/configtables/offwind-dc.csv | 2 +-
doc/configtables/onwind.csv | 2 +-
doc/configtables/solar.csv | 2 +-
doc/release_notes.rst | 6 ++
rules/build_electricity.smk | 1 -
rules/solve_myopic.smk | 3 +
scripts/add_brownfield.py | 73 ++++++++--------------
scripts/add_electricity.py | 4 ++
scripts/build_renewable_profiles.py | 95 ++++++++++++-----------------
scripts/prepare_sector_network.py | 5 ++
12 files changed, 90 insertions(+), 122 deletions(-)
diff --git a/config/config.default.yaml b/config/config.default.yaml
index bdbc046e..bc420b36 100644
--- a/config/config.default.yaml
+++ b/config/config.default.yaml
@@ -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.
diff --git a/doc/configtables/offwind-ac.csv b/doc/configtables/offwind-ac.csv
index 9dc0614c..b2533f04 100644
--- a/doc/configtables/offwind-ac.csv
+++ b/doc/configtables/offwind-ac.csv
@@ -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 `_","Specifies the turbine type and its characteristic power curve."
+-- turbine,--,"One of turbine types included in `atlite `_. 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."
diff --git a/doc/configtables/offwind-dc.csv b/doc/configtables/offwind-dc.csv
index c947f358..7c537543 100644
--- a/doc/configtables/offwind-dc.csv
+++ b/doc/configtables/offwind-dc.csv
@@ -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 `__","Specifies the turbine type and its characteristic power curve."
+-- turbine,--,"One of turbine types included in `atlite `_. 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."
diff --git a/doc/configtables/onwind.csv b/doc/configtables/onwind.csv
index f6b36e5d..3b09214b 100644
--- a/doc/configtables/onwind.csv
+++ b/doc/configtables/onwind.csv
@@ -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 `__","Specifies the turbine type and its characteristic power curve."
+-- turbine,--,"One of turbine types included in `atlite `_. 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 `_","Specifies areas according to CORINE Land Cover codes which are generally eligible for wind turbine placement."
diff --git a/doc/configtables/solar.csv b/doc/configtables/solar.csv
index 8328d342..18587694 100644
--- a/doc/configtables/solar.csv
+++ b/doc/configtables/solar.csv
@@ -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 `__","Specifies the solar panel technology and its characteristic attributes."
+-- panel,--,"One of {'Csi', 'CdTe', 'KANENA'} as defined in `atlite `_ . 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 `_ orientation of the solar panel. South corresponds to 180.°."
diff --git a/doc/release_notes.rst b/doc/release_notes.rst
index a3331748..6bf78fcd 100644
--- a/doc/release_notes.rst
+++ b/doc/release_notes.rst
@@ -71,6 +71,12 @@ Upcoming Release
Energiewende (2021)
`_.
+* 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)
==================================
diff --git a/rules/build_electricity.smk b/rules/build_electricity.smk
index 5846dd9d..6b092638 100644
--- a/rules/build_electricity.smk
+++ b/rules/build_electricity.smk
@@ -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",
diff --git a/rules/solve_myopic.smk b/rules/solve_myopic.smk
index a5f2b12b..49df9d1e 100644
--- a/rules/solve_myopic.smk
+++ b/rules/solve_myopic.smk
@@ -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",
diff --git a/scripts/add_brownfield.py b/scripts/add_brownfield.py
index 912b01ae..3b77c437 100644
--- a/scripts/add_brownfield.py
+++ b/scripts/add_brownfield.py
@@ -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)
diff --git a/scripts/add_electricity.py b/scripts/add_electricity.py
index c9e5abca..431f1cfa 100755
--- a/scripts/add_electricity.py
+++ b/scripts/add_electricity.py
@@ -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()
diff --git a/scripts/build_renewable_profiles.py b/scripts/build_renewable_profiles.py
index 66c888e6..99b177a8 100644
--- a/scripts/build_renewable_profiles.py
+++ b/scripts/build_renewable_profiles.py
@@ -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)
diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py
index 03cba48b..59184b79 100755
--- a/scripts/prepare_sector_network.py
+++ b/scripts/prepare_sector_network.py
@@ -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
From 3e6394b23c3e5361eb8e97bf5ce4e8d62727a666 Mon Sep 17 00:00:00 2001
From: Fabian Neumann
Date: Tue, 6 Feb 2024 13:55:27 +0100
Subject: [PATCH 4/6] revert to previous offshore wind turbine models
---
config/config.default.yaml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/config/config.default.yaml b/config/config.default.yaml
index bc420b36..5afc6f8e 100644
--- a/config/config.default.yaml
+++ b/config/config.default.yaml
@@ -187,7 +187,7 @@ renewable:
cutout: europe-2013-era5
resource:
method: wind
- turbine: NREL_ReferenceTurbine_5MW_offshore
+ turbine: NREL_ReferenceTurbine_2020ATB_5.5MW
add_cutout_windspeed: true
capacity_per_sqkm: 2
correction_factor: 0.8855
@@ -203,7 +203,7 @@ renewable:
cutout: europe-2013-era5
resource:
method: wind
- turbine: Vestas_V164_7MW_offshore
+ turbine: NREL_ReferenceTurbine_2020ATB_5.5MW
add_cutout_windspeed: true
capacity_per_sqkm: 2
correction_factor: 0.8855
From ba409c2f1e1943c57ddc3b63460cd21ba2cd308c Mon Sep 17 00:00:00 2001
From: Fabian Neumann
Date: Tue, 6 Feb 2024 13:55:51 +0100
Subject: [PATCH 5/6] backwards compatibility with old profile_{tech}.nc files
---
scripts/build_renewable_profiles.py | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/scripts/build_renewable_profiles.py b/scripts/build_renewable_profiles.py
index 99b177a8..a24045a8 100644
--- a/scripts/build_renewable_profiles.py
+++ b/scripts/build_renewable_profiles.py
@@ -411,9 +411,13 @@ if __name__ == "__main__":
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.max("year")
+
ds = ds.sel(
bus=(
- (ds["profile"].mean("time").max("year") > 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))
)
)
From c8e18298803fa9c2dd838008902fa6f41b5857d0 Mon Sep 17 00:00:00 2001
From: Fabian Neumann
Date: Tue, 6 Feb 2024 18:38:53 +0100
Subject: [PATCH 6/6] fix to backwards compatibility with old profile_{tech}.nc
files
---
scripts/build_renewable_profiles.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/scripts/build_renewable_profiles.py b/scripts/build_renewable_profiles.py
index a24045a8..a075450d 100644
--- a/scripts/build_renewable_profiles.py
+++ b/scripts/build_renewable_profiles.py
@@ -413,7 +413,7 @@ if __name__ == "__main__":
# select only buses with some capacity and minimal capacity factor
mean_profile = ds["profile"].mean("time")
if "year" in ds.indexes:
- mean_profile.max("year")
+ mean_profile = mean_profile.max("year")
ds = ds.sel(
bus=(