diff --git a/Snakefile b/Snakefile index 76dca9ab..5f8f78d0 100644 --- a/Snakefile +++ b/Snakefile @@ -209,6 +209,19 @@ else: build_biomass_transport_costs_output = {} +rule build_salt_cavern_potentials: + input: + salt_caverns="data/h2_salt_caverns_GWh_per_sqkm.geojson", + regions_onshore=pypsaeur("resources/regions_onshore_elec_s{simpl}_{clusters}.geojson"), + regions_offshore=pypsaeur("resources/regions_offshore_elec_s{simpl}_{clusters}.geojson"), + output: + h2_cavern_potential="resources/salt_cavern_potentials_s{simpl}_{clusters}.csv" + threads: 1 + resources: mem_mb=2000 + benchmark: "benchmarks/build_salt_cavern_potentials_s{simpl}_{clusters}" + script: "scripts/build_salt_cavern_potentials.py" + + rule build_ammonia_production: input: usgs="data/myb1-2017-nitro.xls" @@ -357,7 +370,7 @@ rule prepare_sector_network: costs=CDIR + "costs_{planning_horizons}.csv", profile_offwind_ac=pypsaeur("resources/profile_offwind-ac.nc"), profile_offwind_dc=pypsaeur("resources/profile_offwind-dc.nc"), - h2_cavern="data/hydrogen_salt_cavern_potentials.csv", + h2_cavern="resources/salt_cavern_potentials_s{simpl}_{clusters}.csv", busmap_s=pypsaeur("resources/busmap_elec_s{simpl}.csv"), busmap=pypsaeur("resources/busmap_elec_s{simpl}_{clusters}.csv"), clustered_pop_layout="resources/pop_layout_elec_s{simpl}_{clusters}.csv", diff --git a/config.default.yaml b/config.default.yaml index 48630b89..4145edea 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -238,6 +238,10 @@ sector: co2_network: false cc_fraction: 0.9 # default fraction of CO2 captured with post-combustion capture hydrogen_underground_storage: true + hydrogen_underground_storage_locations: + - onshore # more than 50 km from sea + # - nearshore # within 50 km of sea + # - offshore use_fischer_tropsch_waste_heat: true use_fuel_cell_waste_heat: true electricity_distribution_grid: false diff --git a/doc/data.csv b/doc/data.csv index cde8c559..2c6ac8c8 100644 --- a/doc/data.csv +++ b/doc/data.csv @@ -15,7 +15,7 @@ co2 budgets,co2_budget.csv,CC BY 4.0,https://arxiv.org/abs/2004.11009 existing heating potentials,existing_infrastructure/existing_heating_raw.csv,unknown,https://ec.europa.eu/energy/studies/mapping-and-analyses-current-and-future-2020-2030-heatingcooling-fuel-deployment_en?redir=1 IRENA existing VRE capacities,existing_infrastructure/{solar|onwind|offwind}_capcity_IRENA.csv,unknown,https://www.irena.org/Statistics/Download-Data USGS ammonia production,myb1-2017-nitro.xls,unknown,https://www.usgs.gov/centers/nmic/nitrogen-statistics-and-information -hydrogen salt cavern potentials,hydrogen_salt_cavern_potentials.csv,CC BY 4.0,https://doi.org/10.1016/j.ijhydene.2019.12.161 +hydrogen salt cavern potentials,h2_salt_caverns_GWh_per_sqkm.geojson,CC BY 4.0,https://doi.org/10.1016/j.ijhydene.2019.12.161 https://doi.org/10.20944/preprints201910.0187.v1 hotmaps industrial site database,Industrial_Database.csv,CC BY 4.0,https://gitlab.com/hotmaps/industrial_sites/industrial_sites_Industrial_Database Hotmaps building stock data,data_building_stock.csv,CC BY 4.0,https://gitlab.com/hotmaps/building-stock U-values Poland,u_values_poland.csv,unknown,https://data.europa.eu/euodp/de/data/dataset/building-stock-observatory diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 593c420a..2f7846d8 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -8,6 +8,9 @@ Future release .. note:: This unreleased version currently may require the master branches of PyPSA, PyPSA-Eur, and the technology-data repository. +* Add regionalised hydrogen salt cavern storage potentials from `Technical Potential of Salt Caverns for Hydrogen Storage in Europe `_. + + PyPSA-Eur-Sec 0.6.0 (4 October 2021) ==================================== diff --git a/scripts/build_salt_cavern_potentials.py b/scripts/build_salt_cavern_potentials.py new file mode 100644 index 00000000..4b45a65d --- /dev/null +++ b/scripts/build_salt_cavern_potentials.py @@ -0,0 +1,78 @@ +""" +Build salt cavern potentials for hydrogen storage. + +Technical Potential of Salt Caverns for Hydrogen Storage in Europe +CC-BY 4.0 +https://doi.org/10.20944/preprints201910.0187.v1 +https://doi.org/10.1016/j.ijhydene.2019.12.161 + +Figure 6. Distribution of potential salt cavern sites across Europe with their corresponding +energy densities (cavern storage potential divided by the volume). + +Figure 7. Total cavern storage potential in European countries +classified as onshore, offshore and within 50 km of shore. + +The regional distribution is taken from the map (Figure 6) and scaled to the +capacities from the bar chart split by nearshore (<50km from sea), +onshore (>50km from sea), offshore (Figure 7). +""" + + +import geopandas as gpd +import pandas as pd + + +def concat_gdf(gdf_list, crs='EPSG:4326'): + """Concatenate multiple geopandas dataframes with common coordinate reference system (crs).""" + return gpd.GeoDataFrame(pd.concat(gdf_list), crs=crs) + + +def load_bus_regions(onshore_path, offshore_path): + """Load pypsa-eur on- and offshore regions and concat.""" + + bus_regions_offshore = gpd.read_file(offshore_path) + bus_regions_onshore = gpd.read_file(onshore_path) + bus_regions = concat_gdf([bus_regions_offshore, bus_regions_onshore]) + bus_regions = bus_regions.dissolve(by='name', aggfunc='sum') + + return bus_regions + + +def area(gdf): + """Returns area of GeoDataFrame geometries in square kilometers.""" + return gdf.to_crs(epsg=3035).area.div(1e6) + + +def salt_cavern_potential_by_region(caverns, regions): + + # calculate area of caverns shapes + caverns["area_caverns"] = area(caverns) + + overlay = gpd.overlay(regions.reset_index(), caverns, keep_geom_type=True) + + # calculate share of cavern area inside region + overlay["share"] = area(overlay) / overlay["area_caverns"] + + overlay["e_nom"] = overlay.eval("capacity_per_area * share * area_caverns / 1000") # TWh + + caverns_regions = overlay.groupby(['name', "storage_type"]).e_nom.sum().unstack("storage_type") + + return caverns_regions + + +if __name__ == '__main__': + if 'snakemake' not in globals(): + from helper import mock_snakemake + snakemake = mock_snakemake('build_salt_cavern_potentials', simpl='', clusters='37') + + + fn_onshore = snakemake.input.regions_onshore + fn_offshore = snakemake.input.regions_offshore + + regions = load_bus_regions(fn_onshore, fn_offshore) + + caverns = gpd.read_file(snakemake.input.salt_caverns) # GWh/sqkm + + caverns_regions = salt_cavern_potential_by_region(caverns, regions) + + caverns_regions.to_csv(snakemake.output.h2_cavern_potential) \ No newline at end of file diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 6d020539..c3d2136e 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1040,26 +1040,27 @@ def add_storage(n, costs): lifetime=costs.at['fuel cell', 'lifetime'] ) - cavern_nodes = pd.DataFrame() + cavern_types = snakemake.config["sector"]["hydrogen_underground_storage_locations"] + h2_caverns = pd.read_csv(snakemake.input.h2_cavern, index_col=0)[cavern_types].sum(axis=1) + + # only use sites with at least 2 TWh potential + h2_caverns = h2_caverns[h2_caverns > 2] + + # convert TWh to MWh + h2_caverns = h2_caverns * 1e6 + + # clip at 1000 TWh for one location + h2_caverns.clip(upper=1e9, inplace=True) + if options['hydrogen_underground_storage']: - h2_salt_cavern_potential = pd.read_csv(snakemake.input.h2_cavern, index_col=0, squeeze=True) - h2_cavern_ct = h2_salt_cavern_potential[~h2_salt_cavern_potential.isna()] - cavern_nodes = pop_layout[pop_layout.ct.isin(h2_cavern_ct.index)] - h2_capital_cost = costs.at["hydrogen storage underground", "fixed"] + h2_capital_cost = costs.at["hydrogen storage underground", "fixed"] - # assumptions: weight storage potential in a country by population - # TODO: fix with real geographic potentials - # convert TWh to MWh with 1e6 - h2_pot = h2_cavern_ct.loc[cavern_nodes.ct] - h2_pot.index = cavern_nodes.index - h2_pot = h2_pot * cavern_nodes.fraction * 1e6 - - n.madd("Store", - cavern_nodes.index + " H2 Store", - bus=cavern_nodes.index + " H2", + n.madd("Store", + h2_caverns.index + " H2 Store", + bus=h2_caverns.index + " H2", e_nom_extendable=True, - e_nom_max=h2_pot.values, + e_nom_max=h2_caverns.values, e_cyclic=True, carrier="H2 Store", capital_cost=h2_capital_cost @@ -1067,7 +1068,7 @@ def add_storage(n, costs): # hydrogen stored overground (where not already underground) h2_capital_cost = costs.at["hydrogen storage tank incl. compressor", "fixed"] - nodes_overground = cavern_nodes.index.symmetric_difference(nodes) + nodes_overground = h2_caverns.index.symmetric_difference(nodes) n.madd("Store", nodes_overground + " H2 Store",