Document heating rules

This commit is contained in:
AmosSchledorn 2024-06-17 11:38:23 +02:00
parent 72af49a78d
commit d2018e1088
9 changed files with 640 additions and 45 deletions

View File

@ -6,11 +6,41 @@
Build coefficient of performance (COP) time series for air- or ground-sourced Build coefficient of performance (COP) time series for air- or ground-sourced
heat pumps. heat pumps.
The COP is a function of the temperature difference between source and The COP is approximated as a quatratic function of the temperature difference between source and
sink. sink, based on Staffell et al. 2012.
This rule is executed in ``build_sector.smk``.
Relevant Settings
-----------------
.. code:: yaml
heat_pump_sink_T:
Inputs:
-------
- ``resources/<run_name>/temp_soil_total_elec_s<simpl>_<clusters>.nc``: Soil temperature (total) time series.
- ``resources/<run_name>/temp_soil_rural_elec_s<simpl>_<clusters>.nc``: Soil temperature (rural) time series.
- ``resources/<run_name>/temp_soil_urban_elec_s<simpl>_<clusters>.nc``: Soil temperature (urban) time series.
- ``resources/<run_name>/temp_air_total_elec_s<simpl>_<clusters>.nc``: Ambient air temperature (total) time series.
- ``resources/<run_name>/temp_air_rural_elec_s<simpl>_<clusters>.nc``: Ambient air temperature (rural) time series.
- ``resources/<run_name>/temp_air_urban_elec_s<simpl>_<clusters>.nc``: Ambient air temperature (urban) time series.
Outputs:
--------
- ``resources/cop_soil_total_elec_s<simpl>_<clusters>.nc``: COP (ground-sourced) time series (total).
- ``resources/cop_soil_rural_elec_s<simpl>_<clusters>.nc``: COP (ground-sourced) time series (rural).
- ``resources/cop_soil_urban_elec_s<simpl>_<clusters>.nc``: COP (ground-sourced) time series (urban).
- ``resources/cop_air_total_elec_s<simpl>_<clusters>.nc``: COP (air-sourced) time series (total).
- ``resources/cop_air_rural_elec_s<simpl>_<clusters>.nc``: COP (air-sourced) time series (rural).
- ``resources/cop_air_urban_elec_s<simpl>_<clusters>.nc``: COP (air-sourced) time series (urban).
References
----------
[1] Staffell et al., Energy & Environmental Science 11 (2012): A review of domestic heat pumps, https://doi.org/10.1039/C2EE22653G.
The quadratic regression used is based on Staffell et al. (2012)
https://doi.org/10.1039/C2EE22653G.
""" """
import xarray as xr import xarray as xr

View File

@ -3,7 +3,44 @@
# #
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
""" """
Build heat demand time series using heating degree day (HDD) approximation. This rule builds heat demand time series using heating degree day (HDD) approximation.
Snapshots are resampled to daily time resolution and ``Atlite.convert.heat_demand`` is used to convert ambient temperature from the default weather cutout to heat demand time series for the respective cutout.
Heat demand is distributed by population to clustered onshore regions.
The rule is executed in ``build_sector.smk``.
.. seealso::
`Atlite.Cutout.heat_demand <https://atlite.readthedocs.io/en/master/ref_api.html#module-atlite.convert>`_
Relevant Settings
-----------------
.. code:: yaml
snapshots:
drop_leap_day:
Inputs
------
- ``resources/<run_name>/pop_layout_<scope>.nc``: Population layout (spatial population distribution).
- ``resources/<run_name>/regions_onshore_elec_s<simpl>_<clusters>.geojson``: Onshore region shapes.
- ``cutout``: Weather data cutout, as specified in config
Outputs
-------
- ``resources/daily_heat_demand_<scope>_elec_s<simpl>_<clusters>.nc``:
Relevant settings
-----------------
.. code:: yaml
atlite:
default_cutout``:
""" """
import atlite import atlite

View File

@ -4,6 +4,29 @@
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
""" """
Build district heat shares at each node, depending on investment year. Build district heat shares at each node, depending on investment year.
Inputs:
-------
- `resources/<run_name>/pop_layout.csv`: Population layout for each node: Total, urban and rural population.
- `resources/<run_name>/district_heat_share.csv`: Historical district heat share at each country. Output of `scripts/build_energy_totals.py`.
Outputs:
--------
- `resources/<run_name>/district_heat_share.csv`: District heat share at each node, potential for each investment year.
Relevant settings:
------------------
.. code:: yaml
sector:
district_heating:
energy:
energy_totals_year:
Notes:
------
- The district heat share is calculated as the share of urban population at each node, multiplied by the share of district heating in the respective country.
- The `sector.district_heating.potential` setting defines the max. district heating share.
- The max. share of district heating is increased by a progress factor, depending on the investment year (See `sector.district_heating.progress` setting).
""" """
import logging import logging

View File

@ -3,7 +3,39 @@
# #
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
""" """
Build total energy demands per country using JRC IDEES, eurostat, and EEA data. Build total energy demands and carbon emissions per country using JRC IDEES, eurostat, and EEA data.
- Country-specific data is read in :func:`build_eurostat`, :func:`build_idees` and `build_swiss`.
- :func:`build_energy_totals` then combines energy data from Eurostat, Swiss, and IDEES data and :func:`rescale_idees_from_eurostat` rescales IDEES data to match Eurostat data.
- :func:`build_district_heat_share` calculates the share of district heating for each country from IDEES data.
- Historical CO2 emissions are calculated in :func:`build_eea_co2` and :func:`build_eurostat_co2` and combined in :func:`build_co2_totals`.
Relevant Settings
-----------------
.. code:: yaml
countries:
energy:
Inputs
------
- `resources/<run_name>/nuts3_shapes.gejson`: NUTS3 shapes.
- `data/bundle/eea_UNFCCC_v23.csv`: CO2 emissions data from EEA.
- `data/switzerland-new_format-all_years.csv`: Swiss energy data.
- `data/gr-e-11.03.02.01.01-cc.csv`: Swiss transport data
- `data/bundle/jrc-idees`: JRC IDEES data.
- `data/district_heat_share.csv`: District heating shares.
- `data/eurostat/Balances-April2023`: Eurostat energy balances.
- `data/eurostat/eurostat-household_energy_balances-february_2024.csv`: Eurostat household energy balances.
Outputs
-------
- `resources/<run_name>/energy_totals.csv`: Energy totals per country, sector and year.
- `resources/<run_name>/co2_totals.csv`: CO2 emissions per country, sector and year.
- `resources/<run_name>/transport_data.csv`: Transport data per country and year.
- `resources/<run_name>/district_heat_share.csv`: District heating share per by country and year.
""" """
import logging import logging
@ -16,22 +48,60 @@ import numpy as np
import pandas as pd import pandas as pd
from _helpers import configure_logging, mute_print, set_scenario_config from _helpers import configure_logging, mute_print, set_scenario_config
from tqdm import tqdm from tqdm import tqdm
from typing import List
cc = coco.CountryConverter() cc = coco.CountryConverter()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
idx = pd.IndexSlice idx = pd.IndexSlice
def cartesian(s1, s2): def cartesian(s1: pd.Series, s2: pd.Series) -> pd.DataFrame:
""" """
Cartesian product of two pd.Series. Compute the Cartesian product of two pandas Series.
Parameters
----------
s1: pd.Series
The first pandas Series
s2: pd.Series:
The second pandas Series.
Returns
----------
pd.DataFrame
A DataFrame representing the Cartesian product of s1 and s2.
Examples
--------
>>> s1 = pd.Series([1, 2, 3], index=['a', 'b', 'c'])
>>> s2 = pd.Series([4, 5, 6], index=['d', 'e', 'f'])
>>> cartesian(s1, s2)
d e f
a 4 5 6
b 8 10 12
c 12 15 18
""" """
return pd.DataFrame(np.outer(s1, s2), index=s1.index, columns=s2.index) return pd.DataFrame(np.outer(s1, s2), index=s1.index, columns=s2.index)
def reverse(dictionary: dict) -> dict:
def reverse(dictionary):
""" """
Reverses a keys and values of a dictionary. Reverses the keys and values of a dictionary.
Parameters
----------
dictionary : dict
The dictionary to be reversed.
Returns
-------
dict
A new dictionary with the keys and values reversed.
Examples
--------
>>> d = {'a': 1, 'b': 2, 'c': 3}
>>> reverse(d)
{1: 'a', 2: 'b', 3: 'c'}
""" """
return {v: k for k, v in dictionary.items()} return {v: k for k, v in dictionary.items()}
@ -68,7 +138,28 @@ to_ipcc = {
} }
def eurostat_per_country(input_eurostat, country): def eurostat_per_country(input_eurostat: str, country: str) -> pd.DataFrame:
"""
Read energy balance data for a specific country from Eurostat.
Parameters
----------
input_eurostat : str
Path to the directory containing Eurostat data files.
country : str
Country code for the specific country.
Returns
-------
pd.DataFrame
Concatenated energy balance data for the specified country.
Notes
-----
- The function reads `<input_eurostat>/<country>.-Energy-balance-sheets-April-2023-edition.xlsb`
- It removes the "Cover" sheet from the data and concatenates all the remaining sheets into a single DataFrame.
"""
filename = ( filename = (
f"{input_eurostat}/{country}-Energy-balance-sheets-April-2023-edition.xlsb" f"{input_eurostat}/{country}-Energy-balance-sheets-April-2023-edition.xlsb"
) )
@ -83,10 +174,33 @@ def eurostat_per_country(input_eurostat, country):
return pd.concat(sheet) return pd.concat(sheet)
def build_eurostat(input_eurostat, countries, nprocesses=1, disable_progressbar=False): def build_eurostat(input_eurostat: str, countries: List[str], nprocesses: int=1, disable_progressbar: bool=False) -> pd.DataFrame:
""" """
Return multi-index for all countries' energy data in TWh/a. Return multi-index for all countries' energy data in TWh/a.
Parameters:
-----------
input_eurostat : str
Path to the Eurostat database.
countries : List[str]
List of countries for which energy data is to be retrieved.
nprocesses : int, optional
Number of processes to use for parallel execution, by default 1.
disable_progressbar : bool, optional
Whether to disable the progress bar, by default False.
Returns:
--------
pd.DataFrame
Multi-index DataFrame containing energy data for all countries in TWh/a.
Notes:
------
- The function first renames the countries in the input list using the `idees_rename` mapping and removes "CH".
- It then reads country-wise data using :func:`eurostat_per_country` into a single DataFrame.
- The data is reordered, converted to TWh/a, and missing values are filled.
""" """
countries = {idees_rename.get(country, country) for country in countries} - {"CH"} countries = {idees_rename.get(country, country) for country in countries} - {"CH"}
func = partial(eurostat_per_country, input_eurostat) func = partial(eurostat_per_country, input_eurostat)
@ -152,9 +266,21 @@ def build_eurostat(input_eurostat, countries, nprocesses=1, disable_progressbar=
return df return df
def build_swiss(): def build_swiss() -> pd.DataFrame:
""" """
Return a pd.DataFrame of Swiss energy data in TWh/a. Return a pd.DataFrame of Swiss energy data in TWh/a.
Returns
--------
pd.DataFrame
Swiss energy data in TWh/a.
Notes
-----
- Reads Swiss energy data from `data/switzerland-new_format-all_years.csv`.
- Reshapes and renames data.
- Converts energy units from PJ/a to TWh/a.
""" """
fn = snakemake.input.swiss fn = snakemake.input.swiss
@ -174,7 +300,29 @@ def build_swiss():
return df return df
def idees_per_country(ct, base_dir): def idees_per_country(ct: str, base_dir: str) -> pd.DataFrame:
"""
Calculate energy totals per country using JRC-IDEES data.
Parameters
----------
ct : str
The country code.
base_dir : str
The base directory where the JRC-IDEES data files are located.
Returns
-------
pd.DataFrame
A DataFrame containing the energy totals per country. Columns are energy uses.
Notes
-----
- Retrieves JRC-IDEES data for the specified country from `base_dir` for residential, tertiary, and transport sectors.
- Calculates energy totals for each sector, stores them in a dictionary and returns them as data frame.
- Assertions ensure indeces of JRC-IDEES data are as expected.
"""
ct_idees = idees_rename.get(ct, ct) ct_idees = idees_rename.get(ct, ct)
fn_residential = f"{base_dir}/JRC-IDEES-2015_Residential_{ct_idees}.xlsx" fn_residential = f"{base_dir}/JRC-IDEES-2015_Residential_{ct_idees}.xlsx"
fn_tertiary = f"{base_dir}/JRC-IDEES-2015_Tertiary_{ct_idees}.xlsx" fn_tertiary = f"{base_dir}/JRC-IDEES-2015_Tertiary_{ct_idees}.xlsx"
@ -371,8 +519,26 @@ def idees_per_country(ct, base_dir):
return pd.DataFrame(ct_totals) return pd.DataFrame(ct_totals)
def build_idees(countries: List[str]) -> pd.DataFrame:
"""
Build energy totals from IDEES database for the given list of countries using :func:`idees_per_country`.
Parameters
----------
countries : List[str]
List of country names for which energy totals need to be built.
Returns
-------
pd.DataFrame
Energy totals for the given countries.
Notes
-----
- Retrieves energy totals per country and year using :func:`idees_per_country`.
- Returns a DataFrame with columns: country, year, and energy totals for different categories.
"""
def build_idees(countries):
nprocesses = snakemake.threads nprocesses = snakemake.threads
disable_progress = snakemake.config["run"].get("disable_progressbar", False) disable_progress = snakemake.config["run"].get("disable_progressbar", False)
@ -403,7 +569,36 @@ def build_idees(countries):
return totals return totals
def build_energy_totals(countries, eurostat, swiss, idees): def build_energy_totals(countries: List[str], eurostat: pd.DataFrame, swiss: pd.DataFrame, idees: pd.DataFrame) -> pd.DataFrame:
"""
Combine energy totals for the specified countries from Eurostat, Swiss, and IDEES data.
Parameters
----------
countries : List[str]
List of country codes for which energy totals are to be calculated.
eurostat : pd.DataFrame
Eurostat energy balances dataframe.
swiss : pd.DataFrame
Swiss energy data dataframe.
idees : pd.DataFrame
IDEES energy data dataframe.
Returns
-------
pd.DataFrame
Energy totals dataframe for the given countries.
Notes
-----
- Missing values are filled based on Eurostat energy balances and average values in EU28.
- The function also performs specific calculations for Norway and splits road, rail, and aviation traffic for non-IDEES data.
References
----------
- `Norway heating data <http://www.ssb.no/en/energi-og-industri/statistikker/husenergi/hvert-3-aar/2014-07-14>`_
"""
eurostat_fuels = {"electricity": "Electricity", "total": "Total all products"} eurostat_fuels = {"electricity": "Electricity", "total": "Total all products"}
eurostat_countries = eurostat.index.levels[0] eurostat_countries = eurostat.index.levels[0]
eurostat_years = eurostat.index.levels[1] eurostat_years = eurostat.index.levels[1]
@ -591,7 +786,30 @@ def build_energy_totals(countries, eurostat, swiss, idees):
return df return df
def build_district_heat_share(countries, idees): def build_district_heat_share(countries: List[str], idees: pd.DataFrame) -> pd.Series:
"""
Calculate the share of district heating for each country.
Parameters
----------
countries : List[str]
List of country codes for which to calculate district heating share.
idees : pd.DataFrame
IDEES energy data dataframe.
Returns
-------
pd.Series
Series with the district heating share for each country.
Notes
-----
- The function calculates the district heating share as the sum of residential and services derived heat, divided by the sum of residential and services thermal uses.
- The district heating share is then reindexed to match the provided list of countries.
- Missing district heating shares are filled from `data/district_heat_share.csv`.
- The function makes a conservative assumption and takes the minimum district heating share from both the IDEES data and `data/district_heat_share.csv`.
"""
# district heating share # district heating share
district_heat = idees[["derived heat residential", "derived heat services"]].sum( district_heat = idees[["derived heat residential", "derived heat services"]].sum(
axis=1 axis=1
@ -625,9 +843,35 @@ def build_district_heat_share(countries, idees):
return district_heat_share return district_heat_share
def build_eea_co2(input_co2, year=1990, emissions_scope="CO2"): def build_eea_co2(input_co2: str, year: int = 1990, emissions_scope: str = "CO2") -> pd.DataFrame:
# https://www.eea.europa.eu/data-and-maps/data/national-emissions-reported-to-the-unfccc-and-to-the-eu-greenhouse-gas-monitoring-mechanism-16 """
# downloaded 201228 (modified by EEA last on 201221) Calculate CO2 emissions for a given year based on EEA data in Mt
Parameters
----------
input_co2 : str
Path to the input CSV file with CO2 data.
year : int, optional
Year for which to calculate emissions, by default 1990.
emissions_scope : str, optional
Scope of the emissions to consider, by default "CO2".
Returns
-------
pd.DataFrame
DataFrame with CO2 emissions for the given year.
Notes
-----
- The function reads the `input_co2` data and for a specific `year` and `emission scope`
- It calculates "industrial non-elec" and "agriculture" emissions from that data
- It drops unneeded columns and converts the emissions to Mt.
References
---------
- `EEA CO2 data <https://www.eea.europa.eu/data-and-maps/data/national-emissions-reported-to-the-unfccc-and-to-the-eu-greenhouse-gas-monitoring-mechanism-16>`_ (downloaded 201228, modified by EEA last on 201221)
"""
df = pd.read_csv(input_co2, encoding="latin-1", low_memory=False) df = pd.read_csv(input_co2, encoding="latin-1", low_memory=False)
df.replace(dict(Year="1985-1987"), 1986, inplace=True) df.replace(dict(Year="1985-1987"), 1986, inplace=True)
@ -673,11 +917,42 @@ def build_eea_co2(input_co2, year=1990, emissions_scope="CO2"):
] ]
emissions.drop(columns=to_drop, inplace=True) emissions.drop(columns=to_drop, inplace=True)
# convert from Gg to Mt # convert from Gt to Mt
return emissions / 1e3 return emissions / 1e3
def build_eurostat_co2(eurostat, year=1990): def build_eurostat_co2(eurostat: pd.DataFrame, year: int = 1990) -> pd.Series:
"""
Calculate CO2 emissions for a given year based on Eurostat fuel consumption data and fuel-specific emissions.
Parameters
----------
eurostat : pd.DataFrame
DataFrame with Eurostat data.
year : int, optional
Year for which to calculate emissions, by default 1990.
Returns
-------
pd.Series
Series with CO2 emissions for the given year.
Notes
-----
- The function hard-sets fuel-specific emissions:
- solid fuels: 0.36 tCO2_equi/MW_th (approximates coal)
- oil: 0.285 tCO2_equi/MW_th (average of distillate and residue)
- natural gas: 0.2 tCO2_equi/MW_th
- It then multiplies the Eurostat fuel consumption data for `year` by the specific emissions and sums the result.
References
----------
- Oil values from `EIA <https://www.eia.gov/tools/faqs/faq.cfm?id=74&t=11>`_
- Distillate oil (No. 2) 0.276
- Residual oil (No. 6) 0.298
- `EIA Electricity Annual <https://www.eia.gov/electricity/annual/html/epa_a_03.html>`_
"""
eurostat_year = eurostat.xs(year, level="year") eurostat_year = eurostat.xs(year, level="year")
specific_emissions = pd.Series(index=eurostat.columns, dtype=float) specific_emissions = pd.Series(index=eurostat.columns, dtype=float)
@ -687,15 +962,32 @@ def build_eurostat_co2(eurostat, year=1990):
specific_emissions["Oil (total)"] = 0.285 # Average of distillate and residue specific_emissions["Oil (total)"] = 0.285 # Average of distillate and residue
specific_emissions["Gas"] = 0.2 # For natural gas specific_emissions["Gas"] = 0.2 # For natural gas
# oil values from https://www.eia.gov/tools/faqs/faq.cfm?id=74&t=11
# Distillate oil (No. 2) 0.276
# Residual oil (No. 6) 0.298
# https://www.eia.gov/electricity/annual/html/epa_a_03.html
return eurostat_year.multiply(specific_emissions).sum(axis=1) return eurostat_year.multiply(specific_emissions).sum(axis=1)
def build_co2_totals(countries, eea_co2, eurostat_co2): def build_co2_totals(countries: List[str], eea_co2: pd.DataFrame, eurostat_co2: pd.DataFrame) -> pd.DataFrame:
"""
Combine CO2 emissions data from EEA and Eurostat for a list of countries.
Parameters
----------
countries : List[str]
List of country codes for which CO2 totals need to be built.
eea_co2 : pd.DataFrame
DataFrame with EEA CO2 emissions data.
eurostat_co2 : pd.DataFrame
DataFrame with Eurostat CO2 emissions data.
Returns
-------
pd.DataFrame
Combined CO2 emissions data for the given countries.
Notes
-----
- The function combines the CO2 emissions from EEA and Eurostat into a single DataFrame for the given countries.
"""
co2 = eea_co2.reindex(countries) co2 = eea_co2.reindex(countries)
for ct in pd.Index(countries).intersection(["BA", "RS", "AL", "ME", "MK"]): for ct in pd.Index(countries).intersection(["BA", "RS", "AL", "ME", "MK"]):
@ -722,9 +1014,36 @@ def build_co2_totals(countries, eea_co2, eurostat_co2):
return co2 return co2
def build_transport_data(countries, population, idees): def build_transport_data(countries: List[str], population: pd.DataFrame, idees: pd.DataFrame) -> pd.DataFrame:
# first collect number of cars """
Build transport data for a set of countries based on IDEES data.
Parameters
----------
countries : List[str]
List of country codes.
population : pd.DataFrame
DataFrame with population data.
idees : pd.DataFrame
DataFrame with IDEES data.
Returns
-------
pd.DataFrame
DataFrame with transport data.
Notes
-----
- The function first collects the number of passenger cars.
- For Switzerland, it reads the data from `data/gr-e-11.03.02.01.01-cc.csv`.
- It fills missing data on the number of cars and fuel efficiency with average data.
References
----------
- Swiss transport data: `BFS <https://www.bfs.admin.ch/bfs/en/home/statistics/mobility-transport/transport-infrastructure-vehicles/vehicles/road-vehicles-stock-level-motorisation.html>`_
"""
# first collect number of cars
transport_data = pd.DataFrame(idees["passenger cars"]) transport_data = pd.DataFrame(idees["passenger cars"])
countries_without_ch = set(countries) - {"CH"} countries_without_ch = set(countries) - {"CH"}
@ -735,7 +1054,6 @@ def build_transport_data(countries, population, idees):
transport_data = transport_data.reindex(index=new_index) transport_data = transport_data.reindex(index=new_index)
# https://www.bfs.admin.ch/bfs/en/home/statistics/mobility-transport/transport-infrastructure-vehicles/vehicles/road-vehicles-stock-level-motorisation.html
if "CH" in countries: if "CH" in countries:
fn = snakemake.input.swiss_transport fn = snakemake.input.swiss_transport
swiss_cars = pd.read_csv(fn, index_col=0).loc[2000:2015, ["passenger cars"]] swiss_cars = pd.read_csv(fn, index_col=0).loc[2000:2015, ["passenger cars"]]
@ -782,16 +1100,40 @@ def build_transport_data(countries, population, idees):
def rescale_idees_from_eurostat( def rescale_idees_from_eurostat(
idees_countries, idees_countries: List[str],
energy, energy: pd.DataFrame,
eurostat, eurostat: pd.DataFrame
): ) -> pd.DataFrame:
""" """
Takes JRC IDEES data from 2015 and rescales it by the ratio of the eurostat Takes JRC IDEES data from 2015 and rescales it by the ratio of the Eurostat
data and the 2015 eurostat data. data and the 2015 Eurostat data.
Missing data: ['passenger car efficiency', 'passenger cars']
missing data: ['passenger car efficiency', 'passenger cars'] Parameters
----------
idees_countries : List[str]
List of IDEES country codes.
energy : pd.DataFrame
DataFrame with JRC IDEES data.
eurostat : pd.DataFrame
DataFrame with Eurostat data.
Returns
-------
pd.DataFrame
DataFrame with rescaled IDEES data.
Notes
-----
- The function first reads in the Eurostat data for 2015 and calculates the ratio of that data with other Eurostat data.
- This ratio is mapped to the IDEES data.
References
----------
- JRC IDEES data: `JRC IDEES <https://ec.europa.eu/jrc/en/publication/eur-scientific-and-technical-research-reports/jrc-idees>`_
- Eurostat data: `Eurostat <https://ec.europa.eu/eurostat/data/database>`_
""" """
main_cols = ["Total all products", "Electricity"] main_cols = ["Total all products", "Electricity"]
# read in the eurostat data for 2015 # read in the eurostat data for 2015
eurostat_2015 = eurostat.xs(2015, level="year")[main_cols] eurostat_2015 = eurostat.xs(2015, level="year")[main_cols]
@ -959,10 +1301,24 @@ def rescale_idees_from_eurostat(
return energy return energy
def update_residential_from_eurostat(energy): def update_residential_from_eurostat(energy: pd.DataFrame) -> pd.DataFrame:
""" """
Updates energy balances for residential from disaggregated data from Updates energy balances for residential from disaggregated data from Eurostat by mutating input data DataFrame.
Eurostat.
Parameters
----------
energy : pd.DataFrame
DataFrame with energy data.
Returns
-------
pd.DataFrame
DataFrame with updated energy balances.
Notes
-----
- The function first reads in the Eurostat data for households and maps the energy types to the corresponding Eurostat codes.
- For each energy type, it selects the corresponding data, converts units, and drops unnecessary data.
""" """
eurostat_households = pd.read_csv(snakemake.input.eurostat_households) eurostat_households = pd.read_csv(snakemake.input.eurostat_households)

View File

@ -5,6 +5,39 @@
""" """
Builds table of existing heat generation capacities for initial planning Builds table of existing heat generation capacities for initial planning
horizon. horizon.
Existing heat generation capacities are distributed to nodes based on population.
Within the nodes, the capacities are distributed to sectors (residential and services) based on sectoral consumption and urban/rural based population distribution.
Inputs:
-------
- Existing heating generators: `data/existing_heating_raw.csv` per country
- Population layout: `resources/{run_name}/pop_layout_s<simpl>_<clusters>.csv`. Output of `scripts/build_clustered_population_layout.py`
- Population layout with energy demands: `resources/<run_name>/pop_weighted_energy_totals_s<simpl>_<clusters>.csv`
- District heating share: `resources/<run_name>/district_heat_share_elec_s<simpl>_<clusters>_<planning_horizons>.csv`
Outputs:
--------
- Existing heat generation capacities distributed to nodes: `resources/{run_name}/existing_heating_distribution_elec_s{simpl}_{clusters}_{planning_horizons}.csv`
Relevant settings:
------------------
.. code:: yaml
scenario:
planning_horizons
sector:
existing_capacities:
Notes:
------
- Data for Albania, Montenegro and Macedonia is not included in input database and assumed 0.
- Coal and oil boilers are assimilated to oil boilers.
- All ground-source heat pumps are assumed in rural areas and all air-source heat pumps are assumed to be in urban areas.
References:
-----------
- "Mapping and analyses of the current and future (2020 - 2030) heating/cooling fuel deployment (fossil/renewables)" (https://energy.ec.europa.eu/publications/mapping-and-analyses-current-and-future-2020-2030-heatingcooling-fuel-deployment-fossilrenewables-1_en)
""" """
import country_converter as coco import country_converter as coco
import numpy as np import numpy as np

View File

@ -4,6 +4,17 @@
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
""" """
Approximate heat demand for all weather years. Approximate heat demand for all weather years.
:func:`approximate_heat_demand` approximates annual heat demand based on energy totals and heating degree days (HDD) using a regression of heat demand on HDDs.
Inputs
------
- `resources/<run_name>/energy_totals.csv`: Energy consumption by sector (columns), country and year. Output of :func:`scripts.build_energy_totals.py`.
- `data/era5-annual-HDD-per-country.csv`: Number of heating degree days by year (columns) and country (index).
Outputs
-------
- `resources/<run_name>/heat_totals.csv`: Approximated annual heat demand for each country.
""" """
from itertools import product from itertools import product
@ -14,7 +25,28 @@ from numpy.polynomial import Polynomial
idx = pd.IndexSlice idx = pd.IndexSlice
def approximate_heat_demand(energy_totals, hdd): def approximate_heat_demand(energy_totals: pd.DataFrame, hdd: pd.DataFrame):
"""
Approximate heat demand for a set of countries based on energy totals and heating degree days (HDD).
A polynomial regression of heat demand on HDDs is performed on the data from 2007 to 2021. Then, for 2022 and 2023, the heat demand is estimated from known HDDs based on the regression.
Parameters
----------
energy_totals : pd.DataFrame
DataFrame with energy consumption by sector (columns), country and year. Output of :func:`scripts.build_energy_totals.py`.
hdd : pd.DataFrame
DataFrame with numer of heating degree days by year (columns) and country (index).
Returns
-------
pd.DataFrame
DataFrame with approximated heat demand for each country.
Notes
-----
- Missing data is filled forward for GB in 2020 and backward for CH from 2007 to 2009.
- If only one year of heating data is available for a country, a point (0, 0) is added to make the polynomial fit work.
"""
countries = hdd.columns.intersection(energy_totals.index.levels[0]) countries = hdd.columns.intersection(energy_totals.index.levels[0])

View File

@ -3,7 +3,31 @@
# #
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
""" """
Build hourly heat demand time series from daily ones. Build hourly heat demand time series from daily heat demand.
Water and space heating demand profiles are generated using intraday profiles from BDEW. Different profiles are used for the residential and services sectors as well as weekdays and weekend.
The daily heat demand is multiplied by the intraday profile to obtain the hourly heat demand time series. The rule is executed in ``build_sector.smk``.
Relevant Settings
-----------------
.. code:: yaml
snapshots:
drop_leap_day:
Inputs
------
- ``data/heat_load_profile_BDEW.csv``: Intraday heat profile fpr water and space heating demand for the residential and services sectors for weekends and weekdays.
- ``resources/daily_heat_demand_<scope>_elec_s<simpl>_<clusters>.nc``: Daily heat demand per cluster.
Outputs
-------
- ``resources/hourly_heat_demand_<scope>_elec_s<simpl>_<clusters>.nc``:
""" """
from itertools import product from itertools import product

View File

@ -3,7 +3,36 @@
# #
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
""" """
Build solar thermal collector time series. Build solar thermal collector profile time series.
Uses ``atlite.Cutout.solar_thermal` to compute heat generation for clustered onshore regions from population layout and weather data cutout.
The rule is executed in ``build_sector.smk``.
.. seealso::
`Atlite.Cutout.solar_thermal <https://atlite.readthedocs.io/en/master/ref_api.html#module-atlite.convert>`_
Relevant Settings
-----------------
.. code:: yaml
snapshots:
drop_leap_day:
solar_thermal:
atlite:
default_cutout:
Inputs
------
- ``resources/<run_name/pop_layout_<scope>.nc``:
- ``resources/<run_name/regions_onshore_elec_s<simpl>_<clusters>.geojson``:
- ``cutout``: Weather data cutout, as specified in config
Outputs
-------
- ``resources/solar_thermal_<scope>_elec_s<simpl>_<clusters>.nc``:
""" """
import atlite import atlite

View File

@ -4,6 +4,37 @@
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
""" """
Build time series for air and soil temperatures per clustered model region. Build time series for air and soil temperatures per clustered model region.
Uses ``atlite.Cutout.temperature`` and ``atlite.Cutout.soil_temperature compute temperature ambient air and soil temperature for the respective cutout. The rule is executed in ``build_sector.smk``.
.. seealso::
`Atlite.Cutout.temperature <https://atlite.readthedocs.io/en/master/ref_api.html#module-atlite.convert>`_
`Atlite.Cutout.soil_temperature <https://atlite.readthedocs.io/en/master/ref_api.html#module-atlite.convert>`_
Relevant Settings
-----------------
.. code:: yaml
snapshots:
drop_leap_day:
atlite:
default_cutout:
Inputs
------
- ``resources/<run_name>/pop_layout_<scope>.nc``:
- ``resources/<run_name>/regions_onshore_elec_s<simpl>_<clusters>.geojson``:
- ``cutout``: Weather data cutout, as specified in config
Outputs
-------
- ``resources/temp_soil_<scope>_elec_s<simpl>_<clusters>.nc``:
- ``resources/temp_air_<scope>_elec_s<simpl>_<clusters>.nc`
""" """
import atlite import atlite