# -*- coding: utf-8 -*-
"""
Central module containing all code dealing with the future heat demand import.
This module obtains the residential and service-sector heat demand data for
2015 from Peta5.0.1, calculates future heat demands and saves them in the
database with assigned census cell IDs.
"""
from pathlib import Path # for database import
from urllib.request import urlretrieve
# for metadata creation
import json
import os
import zipfile
from jinja2 import Template
from rasterio.mask import mask
# packages for ORM class definition
from sqlalchemy import Column, Float, ForeignKey, Integer, Sequence, String
from sqlalchemy.ext.declarative import declarative_base
import geopandas as gpd
# for raster operations
import rasterio
from egon.data import db, subprocess
from egon.data.datasets import Dataset
from egon.data.datasets.scenario_parameters import (
EgonScenario,
get_sector_parameters,
)
import egon.data.config
# import time
[docs]class HeatDemandImport(Dataset):
"""
Insert the annual heat demand per census cell for each scenario
This dataset downloads the heat demand raster data for private households
and CTS from Peta 5.0.1 (https://s-eenergies-open-data-euf.hub.arcgis.com/maps/d7d18b63250240a49eb81db972aa573e/about)
and stores it into files in the working directory.
The data from Peta 5.0.1 represents the status quo of the year 2015.
To model future heat demands, the data is scaled to meet target values
from external sources. These target values are defined for each scenario
in :py:class:`ScenarioParameters <egon.data.datasets.scenario_parameters.ScenarioParameters>`.
*Dependencies*
* :py:class:`ScenarioParameters <egon.data.datasets.scenario_parameters.ScenarioParameters>`
* :py:class:`Vg250 <egon.data.datasets.vg250.Vg250>`
* :py:class:`ZensusVg250 <egon.data.datasets.zensus_vg250.ZensusVg250>`
*Resulting tables*
* :py:class:`demand.egon_peta_heat <egon.data.datasets.heat_demand.EgonPetaHeat>` is created and filled
"""
#:
name: str = "heat-demands"
#:
version: str = "0.0.1"
def __init__(self, dependencies):
super().__init__(
name=self.name,
# version=self.target_files + "_0.0",
version=self.version, # maybe rethink the naming
dependencies=dependencies,
tasks=(scenario_data_import),
)
Base = declarative_base()
# class for the final dataset in the database
[docs]class EgonPetaHeat(Base):
__tablename__ = "egon_peta_heat"
__table_args__ = {"schema": "demand"}
id = Column(
Integer,
Sequence("egon_peta_heat_seq", schema="demand"),
server_default=Sequence(
"egon_peta_heat_seq", schema="demand"
).next_value(),
primary_key=True,
)
demand = Column(Float)
sector = Column(String)
scenario = Column(String, ForeignKey(EgonScenario.name))
zensus_population_id = Column(Integer)
[docs]def download_peta5_0_1_heat_demands():
"""
Download Peta5.0.1 tiff files.
The downloaded data contain residential and service-sector heat demands
per hectar grid cell for 2015.
Parameters
----------
None
Returns
-------
None
Notes
-----
The heat demand data in the Peta5.0.1 dataset are assumed not change.
An upgrade to a higher Peta version is currently not foreseen.
Therefore, for the version management we can assume that the dataset
will not change, unless the code is changed.
"""
data_config = egon.data.config.datasets()
# residential heat demands 2015
peta5_resheatdemands_config = data_config["peta5_0_1_res_heat_demands"][
"original_data"
]
target_file_res = peta5_resheatdemands_config["target"]["path"]
if not os.path.isfile(target_file_res):
urlretrieve(
peta5_resheatdemands_config["source"]["url"], target_file_res
)
# service-sector heat demands 2015
peta5_serheatdemands_config = data_config["peta5_0_1_ser_heat_demands"][
"original_data"
]
target_file_ser = peta5_serheatdemands_config["target"]["path"]
if not os.path.isfile(target_file_ser):
urlretrieve(
peta5_serheatdemands_config["source"]["url"], target_file_ser
)
return None
[docs]def unzip_peta5_0_1_heat_demands():
"""
Unzip the downloaded Peta5.0.1 tiff files.
Parameters
----------
None
Returns
-------
None
Notes
-----
It is assumed that the Peta5.0.1 dataset does not change and that the
version number does not need to be checked.
"""
# Get information from data configuration file
data_config = egon.data.config.datasets()
peta5_res_heatdemands_orig = data_config["peta5_0_1_res_heat_demands"][
"original_data"
]
# path to the downloaded residential heat demand 2015 data
filepath_zip_res = peta5_res_heatdemands_orig["target"]["path"]
peta5_ser_heatdemands_orig = data_config["peta5_0_1_ser_heat_demands"][
"original_data"
]
# path to the downloaded service-sector heat demand 2015 data
filepath_zip_ser = peta5_ser_heatdemands_orig["target"]["path"]
directory_to_extract_to = "Peta_5_0_1"
# Create the folder, if it does not exists already
if not os.path.exists(directory_to_extract_to):
os.mkdir(directory_to_extract_to)
# Unzip the tiffs, if they do not exist
if not os.path.isfile(
directory_to_extract_to + "/HD_2015_res_Peta5_0_1_GJ.tif"
):
with zipfile.ZipFile(filepath_zip_res, "r") as zf:
zf.extractall(directory_to_extract_to)
if not os.path.isfile(
directory_to_extract_to + "/HD_2015_ser_Peta5_0_1_GJ.tif"
):
with zipfile.ZipFile(filepath_zip_ser, "r") as zf:
zf.extractall(directory_to_extract_to)
return None
[docs]def cutout_heat_demand_germany():
"""
Save cutouts of Germany's 2015 heat demand densities from Europe-wide tifs.
1. Get the German state boundaries
2. Load the unzip 2015 heat demand data (Peta5_0_1) and
3. Cutout Germany's residential and service-sector heat demand densities
4. Save the cutouts as tiffs
Parameters
----------
None
Returns
-------
None
Notes
----
The alternative of cutting out Germany from the pan-European raster
based on German census cells, instead of using state boundaries with
low resolution (to avoid inaccuracies), was not implemented in order to
achieve consistency with other datasets (e.g. egon_mv_grid_district).
Besides, all attempts to read, (union) and load cells from the local
database failed, but were documented as commented code within this
function and afterwards removed.
If you want to have a look at the comments, please check out commit
ec3391e182215b32cd8b741557a747118ab61664, which is the last commit
still containing them.
Also the usage of a buffer around the boundaries and the subsequent
selection of German cells was not implemented.
could be used, but then it must be ensured that later only heat demands
of cells belonging to Germany are used.
"""
# Load the German boundaries from the local database using a dissolved
# dataset which provides one multipolygon
table_name = "vg250_sta_union"
schema = "boundaries"
local_engine = db.engine()
# Recommened way: gpd.read_postgis()
# https://geopandas.readthedocs.io/en/latest/docs/reference/api/geopandas.GeoDataFrame.from_postgis.html?highlight=postgis#geopandas.GeoDataFrame.from_postgis
# multipolygon can be converted into polygons for outcut function
# using ST_Dump: https://postgis.net/docs/ST_Dump.html
gdf_boundaries = gpd.read_postgis(
(
f"SELECT (ST_Dump(geometry)).geom As geometry"
f" FROM {schema}.{table_name}"
),
local_engine,
geom_col="geometry",
)
# rasterio wants the mask to be a GeoJSON-like dict or an object that
# implements the Python geo interface protocol (such as a Shapely Polygon)
# look at the data, to understanding it
# gdf_boundaries.head
# len(gdf_boundaries)
# type(gdf_boundaries)
# gdf_boundaries.crs
# gdf_boundaries.iloc[:,0]
# gdf_boundaries.iloc[0,:]
# gdf_boundaries.plot()
# Load the unzipped heat demand data and cutout Germany
# path to the downloaded and unzipped rediential heat demand 2015 data
res_hd_2015 = "Peta_5_0_1/HD_2015_res_Peta5_0_1_GJ.tif"
# path to the downloaded and unzipped service-sector heat demand 2015 data
ser_hd_2015 = "Peta_5_0_1/HD_2015_ser_Peta5_0_1_GJ.tif"
with rasterio.open(res_hd_2015) as dataset:
# https://rasterio.readthedocs.io/en/latest/topics/masking-by-shapefile.html
out_image, out_transform = mask(
dataset, gdf_boundaries.iloc[:, 0], crop=True
)
out_meta = dataset.meta
# Understanding the outputs
# show(out_image)
# out_transform
out_meta.update(
{
"driver": "GTiff",
"height": out_image.shape[1],
"width": out_image.shape[2],
"transform": out_transform,
}
)
with rasterio.open(
"Peta_5_0_1/res_hd_2015_GER.tif", "w", **out_meta
) as dest:
dest.write(out_image)
# Do the same for the service-sector
with rasterio.open(ser_hd_2015) as dataset:
# https://rasterio.readthedocs.io/en/latest/topics/masking-by-shapefile.html
out_image, out_transform = mask(
dataset, gdf_boundaries.iloc[:, 0], crop=True
)
out_meta = dataset.meta
# Understanding the outputs
# show(out_image)
# out_transform
out_meta.update(
{
"driver": "GTiff",
"height": out_image.shape[1],
"width": out_image.shape[2],
"transform": out_transform,
}
)
with rasterio.open(
"Peta_5_0_1/ser_hd_2015_GER.tif", "w", **out_meta
) as dest:
dest.write(out_image)
return None
[docs]def future_heat_demand_germany(scenario_name):
"""
Calculate the future residential and service-sector heat demand per ha.
The calculation is based on Peta5_0_1 heat demand densities, cutout for
Germany, for the year 2015. The given scenario name is used to read the
adjustment factors for the heat demand rasters from the scenario table.
Parameters
----------
scenario_name: str
Selected scenario name for which assumptions will be loaded.
Returns
-------
None
Notes
-----
None
TODO
----
Specify the crs of the created heat demand tiffs: EPSG 3035
Check if the raster calculations are correct.
Check, if there are populated cells without heat demand.
"""
# Load the values
if scenario_name == "eGon2015":
res_hd_reduction = 1
ser_hd_reduction = 1
else:
heat_parameters = get_sector_parameters("heat", scenario=scenario_name)
res_hd_reduction = heat_parameters["DE_demand_reduction_residential"]
ser_hd_reduction = heat_parameters["DE_demand_reduction_service"]
# Define the directory where the created rasters will be saved
scenario_raster_directory = "heat_scenario_raster"
if not os.path.exists(scenario_raster_directory):
os.mkdir(scenario_raster_directory)
# Open, read and adjust the cutout heat demand distributions for Germany
# https://rasterio.readthedocs.io/en/latest/topics/writing.html
# https://gis.stackexchange.com/questions/338282/applying-equation-to-a-numpy-array-while-preserving-tiff-metadata-coordinates
# Write an array as a raster band to a new 16-bit file. For
# the new file's profile, the profile of the source is adjusted.
# Residential heat demands first
res_cutout = "Peta_5_0_1/res_hd_2015_GER.tif"
with rasterio.open(res_cutout) as src: # open raster dataset
res_hd_2015 = src.read(1) # read as numpy array; band 1; masked=True??
res_profile = src.profile
# adjusting and connversion to MWh
res_scenario_raster = res_hd_reduction * res_hd_2015 / 3.6
res_profile.update(
dtype=rasterio.float32, # set the dtype to float32
count=1, # change the band count to 1
compress="lzw", # specify LZW compression
)
# Save the scenario's residential heat demands as tif file
# Define the filename for export
res_result_filename = (
scenario_raster_directory + "/res_HD_" + scenario_name + ".tif"
)
# Open raster dataset in 'w' write mode using the adjusted meta data
with rasterio.open(res_result_filename, "w", **res_profile) as dst:
dst.write(res_scenario_raster.astype(rasterio.float32), 1)
# Do the same for the service-sector
ser_cutout = "Peta_5_0_1/ser_hd_2015_GER.tif"
with rasterio.open(ser_cutout) as src: # open raster dataset
ser_hd_2015 = src.read(1) # read as numpy array; band 1; masked=True??
ser_profile = src.profile
# adjusting and connversion to MWh
ser_scenario_raster = ser_hd_reduction * ser_hd_2015 / 3.6
ser_profile.update(dtype=rasterio.float32, count=1, compress="lzw")
# Save the scenario's service-sector heat demands as tif file
# Define the filename for export
ser_result_filename = (
scenario_raster_directory + "/ser_HD_" + scenario_name + ".tif"
)
# Open raster dataset in 'w' write mode using the adjusted meta data
with rasterio.open(ser_result_filename, "w", **ser_profile) as dst:
dst.write(ser_scenario_raster.astype(rasterio.float32), 1)
return None
[docs]def heat_demand_to_db_table():
"""
Import heat demand rasters and convert them to vector data.
Specify the rasters to import as raster file patterns (file type and
directory containing raster files, which all will be imported).
The rasters are stored in a temporary table called "heat_demand_rasters".
The final demand data, having the census IDs as foreign key (from the
census population table), are genetated
by the provided sql script (raster2cells-and-centroids.sql) and
are stored in the table "demand.egon_peta_heat".
Parameters
----------
None
Returns
-------
None
Notes
-----
Please note that the data from "demand.egon_peta_heat" is deleted
prior to the import, so make sure you're not loosing valuable data.
TODO
----
Check if data already exists in database or if the function needs
to be executed: data version management.
Define version number correctly
"""
# Define the raster file type to be imported
sources = ["*.tif"]
# Define the directory from with all raster files having the defined type
# will be imported
sources = [
path
for pattern in sources
for path in Path("heat_scenario_raster").glob(pattern)
]
# Create the schema for the final table, if needed
engine = db.engine()
db.execute_sql("CREATE SCHEMA IF NOT EXISTS demand;")
sql_script = os.path.join(
os.path.dirname(__file__), "raster2cells-and-centroids.sql"
)
db.execute_sql("DELETE FROM demand.egon_peta_heat;")
for source in sources:
if not "2015" in source.stem:
# Create a temporary table and fill the final table using the sql script
rasters = f"heat_demand_rasters_{source.stem.lower()}"
import_rasters = subprocess.run(
["raster2pgsql", "-e", "-s", "3035", "-I", "-C", "-F", "-a"]
+ [source]
+ [f"{rasters}"],
text=True,
).stdout
with engine.begin() as connection:
print(
f'CREATE TEMPORARY TABLE "{rasters}"'
' ("rid" serial PRIMARY KEY,"rast" raster,"filename" text);'
)
connection.execute(
f'CREATE TEMPORARY TABLE "{rasters}"'
' ("rid" serial PRIMARY KEY,"rast" raster,"filename" text);'
)
connection.execute(import_rasters)
connection.execute(f'ANALYZE "{rasters}"')
with open(sql_script) as convert:
connection.execute(
Template(convert.read()).render(source=rasters)
)
return None
[docs]def adjust_residential_heat_to_zensus(scenario):
"""
Adjust residential heat demands to fit to zensus population.
In some cases, Peta assigns residential heat demand to unpopulated cells.
This can be caused by the different population data used in Peta or
buildings in zenus cells without a population
(see :func:`egon.data.importing.zensus.adjust_zensus_misc`)
Residential heat demand in cells without zensus population is dropped.
Residential heat demand in cells with zensus population is scaled to meet
the overall residential heat demands.
Parameters
----------
scenario : str
Name of the scenario.
Returns
-------
None
"""
# Select overall residential heat demand
overall_demand = db.select_dataframe(
f"""SELECT SUM(demand) as overall_demand
FROM demand.egon_peta_heat
WHERE scenario = {'scenario'} and sector = 'residential'
"""
).overall_demand[0]
# Select heat demand in populated cells
df = db.select_dataframe(
f"""SELECT *
FROM demand.egon_peta_heat
WHERE scenario = {'scenario'} and sector = 'residential'
AND zensus_population_id IN (
SELECT id
FROM society.destatis_zensus_population_per_ha_inside_germany
)""",
index_col="id",
)
# Scale heat demands in populated cells
df.loc[:, "demand"] *= overall_demand / df.loc[:, "demand"].sum()
# Drop residential heat demands
db.execute_sql(
f"""DELETE FROM demand.egon_peta_heat
WHERE scenario = {'scenario'} and sector = 'residential'"""
)
# Insert adjusted heat demands in populated cells
df.to_sql(
"egon_peta_heat", schema="demand", con=db.engine(), if_exists="append"
)
return None
[docs]def scenario_data_import():
"""
Call all heat demand import related functions.
This function executes the functions that download, unzip and adjust
the heat demand distributions from Peta5.0.1
and that save the future heat demand distributions for Germany as tiffs
as well as with census grid IDs as foreign key in the database.
Parameters
----------
None
Returns
-------
None
Notes
-----
None
"""
# create schema if not exists
db.execute_sql("CREATE SCHEMA IF NOT EXISTS demand;")
# drop table if exists
# can be removed when table structure doesn't change anymore
db.execute_sql("DROP TABLE IF EXISTS demand.egon_peta_heat CASCADE")
db.execute_sql("DROP SEQUENCE IF EXISTS demand.egon_peta_heat_seq CASCADE")
# create table
EgonPetaHeat.__table__.create(bind=db.engine(), checkfirst=True)
download_peta5_0_1_heat_demands()
unzip_peta5_0_1_heat_demands()
cutout_heat_demand_germany()
# Specifiy the scenario names for loading factors from csv file
future_heat_demand_germany("eGon2035")
future_heat_demand_germany("eGon100RE")
# future_heat_demand_germany("eGon2015")
heat_demand_to_db_table()
adjust_residential_heat_to_zensus("eGon2035")
adjust_residential_heat_to_zensus("eGon100RE")
# future_heat_demand_germany("eGon2015")
add_metadata()
return None