Source code for egon.data.datasets.heat_supply.geothermal

"""The module containing all code dealing with geothermal potentials and costs

Main source: Ableitung eines Korridors für den Ausbau
der erneuerbaren Wärme im Gebäudebereich, Beuth Hochschule für Technik
Berlin ifeu – Institut für Energie- und Umweltforschung Heidelberg GmbH
Februar 2017

"""
from pathlib import Path

import numpy as np

import geopandas as gpd
import pandas as pd
from egon.data import config, db


[docs]def calc_geothermal_potentials(): # Set parameters ## specific thermal capacity of water in kJ/kg*K (p. 95) c_p = 4 ## full load hours per year in h (p. 95) flh = 3000 ## mass flow per reservoir in kg/s (p. 95) m_flow = pd.Series(data={"NDB": 35, "ORG": 90, "SMB": 125}, name="m_flow") ## geothermal potentials per temperature (p. 94) file_path = ( Path(".") / "data_bundle_egon_data" / "geothermal_potential" / "geothermal_potential_germany.shp" ) potentials = gpd.read_file(file_path) ## temperature heating system in °C (p. 95) sys_temp = 60 ## temeprature losses heat recuperator in °C (p. 95) loss_temp = 5 # calc mean temperatures per region (p. 93/94): potentials["mean_temperature"] = potentials["min_temper"] + 15 # exclude regions with mean_temp < 60°C (p. 93): potentials = potentials[potentials.mean_temperature >= 60] # exclude regions outside of NDB, ORG or SMB because of missing mass flow potentials = potentials[~potentials.reservoir.isnull()] ## set mass flows per region potentials["m_flow"] = potentials.join(m_flow, on="reservoir").m_flow # calculate flow in kW potentials["Q_flow"] = ( potentials.m_flow * c_p * (potentials.mean_temperature - loss_temp - sys_temp) ) potentials["Q"] = potentials.Q_flow * flh return potentials
[docs]def calc_geothermal_costs(max_costs=np.inf, min_costs=0): # Set parameters ## drilling depth per reservoir in m (p. 99) depth = pd.Series( data={"NDB": 2500, "ORG": 3400, "SMB": 2800}, name="depth" ) ## drillings costs in EUR/m (p. 99) depth_costs = 1500 ## ratio of investment costs to drilling costs (p. 99) ratio = 1.4 ## annulazaion factors p = 0.045 T = 30 PVA = 1 / p - 1 / (p * (1 + p) ** T) # calculate overnight investment costs per drilling and region overnight_investment = depth * depth_costs * ratio investment_per_year = overnight_investment / PVA # investment costs per well according to p.99 costs = pd.Series( data={"NDB": 12.5e6, "ORG": 17e6, "SMB": 14e6}, name="costs" ) potentials = calc_geothermal_potentials() potentials["cost_per_well"] = potentials.join(costs, on="reservoir").costs potentials["cost_per_well_mw"] = ( potentials.cost_per_well / 1000 / potentials.Q_flow ) potentials = potentials.to_crs(3035) # area weighted mean costs per well and mw np.average(potentials.cost_per_well_mw, weights=potentials.area) return potentials[ (potentials["cost_per_well_mw"] <= max_costs) & (potentials["cost_per_well_mw"] > min_costs) ]
[docs]def calc_usable_geothermal_potential(max_costs=2, min_costs=0): """ Calculate geothermal potentials close to district heating demands Parameters ---------- max_costs : float, optional Maximum accepted costs for geo thermal in EUR/MW_th. The default is 2. min_costs : float, optional Minimum accepted costs for geo thermal in EUR/MW_th. The default is 0. Returns ------- float Geothermal potential close to district heating areas in MW """ sources = config.datasets()["heat_supply"]["sources"] # Select 1km buffer arround large district heating areas as possible areas district_heating = db.select_geodataframe( f""" SELECT area_id, residential_and_service_demand as demand, ST_Difference( ST_Buffer(geom_polygon, 1000), geom_polygon) as geom FROM {sources['district_heating_areas']['schema']}. {sources['district_heating_areas']['table']} WHERE scenario = 'eGon100RE' AND residential_and_service_demand > 96000 """, index_col="area_id", ) # Select geothermal potential areas where investments costs per MW # are in given range geothermal_potential = calc_geothermal_costs( max_costs=max_costs, min_costs=min_costs ) # Intersect geothermal potential areas with district heating areas: # geothermal will be build only if demand of a large district heating # grid is close overlay = gpd.overlay(district_heating.reset_index(), geothermal_potential) if len(overlay) > 0: # Calculate available area for geothermal power plants overlay["area_sqkm"] = overlay.area * 1e-6 # Assmue needed area per well pw_km = 0.25 # Calculate number of possible wells per intersecting area overlay["number_wells"] = overlay["area_sqkm"].mul(pw_km) # Calculate share of overlaying areas per district heating grid overlay["area_share"] = ( overlay.groupby("area_id") .apply(lambda grp: grp.area_sqkm / grp.area_sqkm.sum()) .values ) # Possible installable capacity per intersecting area overlay["Q_per_area"] = overlay.Q_flow.mul(overlay.number_wells) # Prepare geothermal potenital per district heating area gt_potential_dh = pd.DataFrame(index=district_heating.index) gt_potential_dh["demand"] = district_heating.demand # Group intersecting areas by district heating area grouped = overlay[ overlay.area_id.isin( gt_potential_dh[ gt_potential_dh.index.isin(overlay.area_id) ].index ) ].groupby(overlay.area_id) # Calculate geo thermal capacity per district heating area gt_potential_dh["Q_flow"] = grouped.Q_per_area.sum() / 1000 gt_potential_dh["installed_MW"] = gt_potential_dh["Q_flow"] # Demand resitriction: If technical potential exceeds demand of # district heating area, reduce potential according to demand idx_demand_restriction = ( gt_potential_dh["Q_flow"] * 3000 > gt_potential_dh["demand"] ) gt_potential_dh.loc[idx_demand_restriction, "installed_MW"] = ( gt_potential_dh.loc[idx_demand_restriction, "demand"] / 3000 ) print( f"""Geothermal potential in Germany: {round(gt_potential_dh["Q_flow"].sum()/1000, 3)} GW_th""" ) print( f""" Geothermal potential in Germany close to large district heating: {round(gt_potential_dh['installed_MW'].sum()/1000, 3)} GW_th """ ) return gt_potential_dh["installed_MW"].sum() else: return 0
[docs]def potential_germany(): """Calculates geothermal potentials for different investment costs. The investment costs for geothermal district heating highly depend on the location because of different mass flows and drilling depths. Thsi functions calcultaes the geothermal potentials close to germany for five different costs ranges. This data can be used in pypsa-eur-sec to optimise the share of geothermal district heating by considering different investment costs. Returns ------- None. """ geothermal_costs_and_potentials = pd.Series(index=[0.5, 1, 2, 5, 10]) geothermal_costs_and_potentials[0.5] = calc_usable_geothermal_potential( max_costs=0.5, min_costs=0 ) geothermal_costs_and_potentials[1] = calc_usable_geothermal_potential( max_costs=1, min_costs=0.5 ) geothermal_costs_and_potentials[2] = calc_usable_geothermal_potential( max_costs=2, min_costs=1 ) geothermal_costs_and_potentials[5] = calc_usable_geothermal_potential( max_costs=5, min_costs=2 ) geothermal_costs_and_potentials[10] = calc_usable_geothermal_potential( max_costs=10, min_costs=5 ) pd.DataFrame(geothermal_costs_and_potentials).reset_index().rename( {"index": "cost [EUR/kW]", 0: "potential [MW]"}, axis=1 ).to_csv("geothermal_potential_germany.csv")