diff --git a/scripts/build_cop_profiles.py b/scripts/build_cop_profiles.py index 16e44c18..2a47198b 100644 --- a/scripts/build_cop_profiles.py +++ b/scripts/build_cop_profiles.py @@ -6,11 +6,40 @@ Build coefficient of performance (COP) time series for air- or ground-sourced heat pumps. -The COP is a function of the temperature difference between source and -sink. +The COP is approximated as a quatratic function of the temperature difference between source and +sink, based on Staffell et al. 2012. -The quadratic regression used is based on Staffell et al. (2012) -https://doi.org/10.1039/C2EE22653G. +This rule is executed in ``build_sector.smk``. + +Relevant Settings +----------------- + +.. code:: yaml + heat_pump_sink_T: + + +Inputs: +------- +- ``resources//temp_soil_total_elec_s_.nc``: Soil temperature (total) time series. +- ``resources//temp_soil_rural_elec_s_.nc``: Soil temperature (rural) time series. +- ``resources//temp_soil_urban_elec_s_.nc``: Soil temperature (urban) time series. +- ``resources//temp_air_total_elec_s_.nc``: Ambient air temperature (total) time series. +- ``resources//temp_air_rural_elec_s_.nc``: Ambient air temperature (rural) time series. +- ``resources//temp_air_urban_elec_s_.nc``: Ambient air temperature (urban) time series. + +Outputs: +-------- +- ``resources/cop_soil_total_elec_s_.nc``: COP (ground-sourced) time series (total). +- ``resources/cop_soil_rural_elec_s_.nc``: COP (ground-sourced) time series (rural). +- ``resources/cop_soil_urban_elec_s_.nc``: COP (ground-sourced) time series (urban). +- ``resources/cop_air_total_elec_s_.nc``: COP (air-sourced) time series (total). +- ``resources/cop_air_rural_elec_s_.nc``: COP (air-sourced) time series (rural). +- ``resources/cop_air_urban_elec_s_.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. """ import xarray as xr diff --git a/scripts/build_daily_heat_demand.py b/scripts/build_daily_heat_demand.py index b5a69e89..97d1368e 100644 --- a/scripts/build_daily_heat_demand.py +++ b/scripts/build_daily_heat_demand.py @@ -3,7 +3,45 @@ # # 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 `_ + +Relevant Settings +----------------- + +.. code:: yaml + + snapshots: + drop_leap_day: + +Inputs +------ + +- ``resources//pop_layout_.nc``: Population layout (spatial population distribution). +- ``resources//regions_onshore_elec_s_.geojson``: Onshore region shapes. +- ``cutout``: Weather data cutout, as specified in config + +Outputs +------- + +- ``resources/daily_heat_demand__elec_s_.nc``: + +Relevant settings +----------------- + +.. code:: yaml + + atlite: + default_cutout``: """ import atlite diff --git a/scripts/build_district_heat_share.py b/scripts/build_district_heat_share.py index 178f2c0d..d62d2ab0 100644 --- a/scripts/build_district_heat_share.py +++ b/scripts/build_district_heat_share.py @@ -4,6 +4,29 @@ # SPDX-License-Identifier: MIT """ Build district heat shares at each node, depending on investment year. + +Inputs: +------- +- `resources//pop_layout.csv`: Population layout for each node: Total, urban and rural population. +- `resources//district_heat_share.csv`: Historical district heat share at each country. Output of `scripts/build_energy_totals.py`. + +Outputs: +-------- +- `resources//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 diff --git a/scripts/build_energy_totals.py b/scripts/build_energy_totals.py index fff95733..a476ec65 100644 --- a/scripts/build_energy_totals.py +++ b/scripts/build_energy_totals.py @@ -3,12 +3,45 @@ # # 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//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//energy_totals.csv`: Energy totals per country, sector and year. +- `resources//co2_totals.csv`: CO2 emissions per country, sector and year. +- `resources//transport_data.csv`: Transport data per country and year. +- `resources//district_heat_share.csv`: District heating share per by country and year. """ import logging import multiprocessing as mp from functools import partial +from typing import List import country_converter as coco import geopandas as gpd @@ -22,16 +55,54 @@ logger = logging.getLogger(__name__) 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) -def reverse(dictionary): +def reverse(dictionary: dict) -> dict: """ - 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()} @@ -68,7 +139,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 `/.-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 = ( f"{input_eurostat}/{country}-Energy-balance-sheets-April-2023-edition.xlsb" ) @@ -83,10 +175,38 @@ def eurostat_per_country(input_eurostat, country): 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. + + 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"} func = partial(eurostat_per_country, input_eurostat) @@ -152,9 +272,20 @@ def build_eurostat(input_eurostat, countries, nprocesses=1, disable_progressbar= return df -def build_swiss(): +def build_swiss() -> pd.DataFrame: """ 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 @@ -174,7 +305,29 @@ def build_swiss(): 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 indices of JRC-IDEES data are as expected. + """ + ct_idees = idees_rename.get(ct, ct) fn_residential = f"{base_dir}/JRC-IDEES-2015_Residential_{ct_idees}.xlsx" fn_tertiary = f"{base_dir}/JRC-IDEES-2015_Tertiary_{ct_idees}.xlsx" @@ -372,7 +525,27 @@ def idees_per_country(ct, base_dir): return pd.DataFrame(ct_totals) -def build_idees(countries): +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. + """ + nprocesses = snakemake.threads disable_progress = snakemake.config["run"].get("disable_progressbar", False) @@ -403,7 +576,42 @@ def build_idees(countries): 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 `_ + """ + eurostat_fuels = {"electricity": "Electricity", "total": "Total all products"} eurostat_countries = eurostat.index.levels[0] eurostat_years = eurostat.index.levels[1] @@ -591,7 +799,30 @@ def build_energy_totals(countries, eurostat, swiss, idees): 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_heat = idees[["derived heat residential", "derived heat services"]].sum( axis=1 @@ -625,9 +856,37 @@ def build_district_heat_share(countries, idees): return district_heat_share -def build_eea_co2(input_co2, year=1990, emissions_scope="CO2"): - # 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) +def build_eea_co2( + input_co2: str, year: int = 1990, emissions_scope: str = "CO2" +) -> pd.DataFrame: + """ + 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 `_ (downloaded 201228, modified by EEA last on 201221) + """ + df = pd.read_csv(input_co2, encoding="latin-1", low_memory=False) df.replace(dict(Year="1985-1987"), 1986, inplace=True) @@ -673,11 +932,43 @@ def build_eea_co2(input_co2, year=1990, emissions_scope="CO2"): ] emissions.drop(columns=to_drop, inplace=True) - # convert from Gg to Mt + # convert from Gt to Mt 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 `_ + - Distillate oil (No. 2) 0.276 + - Residual oil (No. 6) 0.298 + - `EIA Electricity Annual `_ + """ + eurostat_year = eurostat.xs(year, level="year") specific_emissions = pd.Series(index=eurostat.columns, dtype=float) @@ -687,15 +978,34 @@ def build_eurostat_co2(eurostat, year=1990): specific_emissions["Oil (total)"] = 0.285 # Average of distillate and residue 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) -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) for ct in pd.Index(countries).intersection(["BA", "RS", "AL", "ME", "MK"]): @@ -722,9 +1032,38 @@ def build_co2_totals(countries, eea_co2, eurostat_co2): return co2 -def build_transport_data(countries, population, idees): - # first collect number of cars +def build_transport_data( + countries: List[str], population: pd.DataFrame, idees: pd.DataFrame +) -> pd.DataFrame: + """ + 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 `_ + """ + + # first collect number of cars transport_data = pd.DataFrame(idees["passenger cars"]) countries_without_ch = set(countries) - {"CH"} @@ -735,7 +1074,6 @@ def build_transport_data(countries, population, idees): 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: fn = snakemake.input.swiss_transport swiss_cars = pd.read_csv(fn, index_col=0).loc[2000:2015, ["passenger cars"]] @@ -782,16 +1120,38 @@ def build_transport_data(countries, population, idees): def rescale_idees_from_eurostat( - idees_countries, - energy, - eurostat, -): + idees_countries: List[str], energy: pd.DataFrame, eurostat: pd.DataFrame +) -> pd.DataFrame: """ - Takes JRC IDEES data from 2015 and rescales it by the ratio of the eurostat - data and the 2015 eurostat data. + Takes JRC IDEES data from 2015 and rescales it by the ratio of the Eurostat + 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 `_ + - Eurostat data: `Eurostat `_ """ + main_cols = ["Total all products", "Electricity"] # read in the eurostat data for 2015 eurostat_2015 = eurostat.xs(2015, level="year")[main_cols] @@ -959,10 +1319,25 @@ def rescale_idees_from_eurostat( 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 - Eurostat. + Eurostat by mutating input data DataFrame. + + 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) diff --git a/scripts/build_existing_heating_distribution.py b/scripts/build_existing_heating_distribution.py index af4a50a7..d37fcfee 100644 --- a/scripts/build_existing_heating_distribution.py +++ b/scripts/build_existing_heating_distribution.py @@ -5,6 +5,38 @@ """ Builds table of existing heat generation capacities for initial planning 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_.csv`. Output of `scripts/build_clustered_population_layout.py` +- Population layout with energy demands: `resources//pop_weighted_energy_totals_s_.csv` +- District heating share: `resources//district_heat_share_elec_s__.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 numpy as np diff --git a/scripts/build_heat_totals.py b/scripts/build_heat_totals.py index 9bee63e5..7a43c9ca 100644 --- a/scripts/build_heat_totals.py +++ b/scripts/build_heat_totals.py @@ -4,6 +4,17 @@ # SPDX-License-Identifier: MIT """ 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//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//heat_totals.csv`: Approximated annual heat demand for each country. """ from itertools import product @@ -14,7 +25,30 @@ from numpy.polynomial import Polynomial 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 number 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]) diff --git a/scripts/build_hourly_heat_demand.py b/scripts/build_hourly_heat_demand.py index 1fb4f5a4..0dcf3524 100644 --- a/scripts/build_hourly_heat_demand.py +++ b/scripts/build_hourly_heat_demand.py @@ -3,7 +3,31 @@ # # 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 for water and space heating demand for the residential and services sectors for weekends and weekdays. +- ``resources/daily_heat_demand__elec_s_.nc``: Daily heat demand per cluster. + +Outputs +------- + +- ``resources/hourly_heat_demand__elec_s_.nc``: """ from itertools import product diff --git a/scripts/build_solar_thermal_profiles.py b/scripts/build_solar_thermal_profiles.py index bb5180b9..9de04f45 100644 --- a/scripts/build_solar_thermal_profiles.py +++ b/scripts/build_solar_thermal_profiles.py @@ -3,7 +3,36 @@ # # 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 `_ + +Relevant Settings +----------------- + +.. code:: yaml + + snapshots: + drop_leap_day: + solar_thermal: + atlite: + default_cutout: + +Inputs +------ + +- ``resources/.nc``: +- ``resources/_.geojson``: +- ``cutout``: Weather data cutout, as specified in config + +Outputs +------- + +- ``resources/solar_thermal__elec_s_.nc``: """ import atlite diff --git a/scripts/build_temperature_profiles.py b/scripts/build_temperature_profiles.py index 00c88b5b..493bd08f 100644 --- a/scripts/build_temperature_profiles.py +++ b/scripts/build_temperature_profiles.py @@ -4,6 +4,36 @@ # SPDX-License-Identifier: MIT """ 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 `_ + `Atlite.Cutout.soil_temperature `_ + +Relevant Settings +----------------- + +.. code:: yaml + + snapshots: + drop_leap_day: + atlite: + default_cutout: + +Inputs +------ + +- ``resources//pop_layout_.nc``: +- ``resources//regions_onshore_elec_s_.geojson``: +- ``cutout``: Weather data cutout, as specified in config + +Outputs +------- + +- ``resources/temp_soil__elec_s_.nc``: +- ``resources/temp_air__elec_s_.nc` """ import atlite