Merge pull request #1105 from PyPSA/document-heat-rules
Document heating rules
This commit is contained in:
commit
0ead0dc6a4
@ -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/<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.
|
||||
"""
|
||||
|
||||
import xarray as xr
|
||||
|
@ -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 <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
|
||||
|
@ -4,6 +4,29 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
"""
|
||||
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
|
||||
|
@ -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/<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 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 `<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 = (
|
||||
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 <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_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 <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.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 <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")
|
||||
|
||||
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 <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"])
|
||||
|
||||
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 <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"]
|
||||
# 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)
|
||||
|
||||
|
@ -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<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 numpy as np
|
||||
|
@ -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/<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
|
||||
@ -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])
|
||||
|
||||
|
@ -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_<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
|
||||
|
@ -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 <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
|
||||
|
@ -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 <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
|
||||
|
Loading…
Reference in New Issue
Block a user