Merge pull request #1255 from PyPSA/fix_building_fec

Use JRC-IDEES thermal energy service instead of FE for buildings heating demand
This commit is contained in:
lisazeyen 2024-09-11 15:25:44 +02:00 committed by GitHub
commit 7acda28ee7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 179 additions and 8 deletions

View File

@ -10,6 +10,8 @@ Release Notes
.. Upcoming Release
* Change the heating demand from final energy which includes losses in legacy equipment to thermal energy service based on JRC-IDEES. Efficiencies of existing heating capacities are lowered according to the conversion of final energy to thermal energy service. For overnight scenarios or future planning horizon this change leads to a reduction in heat supply.
* Updated district heating supply temperatures based on `Euroheat's DHC Market Outlook 2024<https://api.euroheat.org/uploads/Market_Outlook_2024_beeecd62d4.pdf>`__ and `AGFW-Hauptbericht 2022 <https://www.agfw.de/securedl/sdl-eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3MjU2MjI2MTUsImV4cCI6MTcyNTcxMjYxNSwidXNlciI6MCwiZ3JvdXBzIjpbMCwtMV0sImZpbGUiOiJmaWxlYWRtaW4vdXNlcl91cGxvYWQvWmFobGVuX3VuZF9TdGF0aXN0aWtlbi9IYXVwdGJlcmljaHRfMjAyMi9BR0ZXX0hhdXB0YmVyaWNodF8yMDIyLnBkZiIsInBhZ2UiOjQzNn0.Bhma3PKg9uJnC57Ixi2p9STW5-II9VXPTDXS544M208/AGFW_Hauptbericht_2022.pdf>`__. `min_forward_temperature` and `return_temperature` (not given by Euroheat) are extrapolated based on German values.
* Made the overdimensioning factor for heating systems specific for central/decentral heating, defaults to no overdimensionining for central heating and no changes to decentral heating compared to previous version.

View File

@ -366,6 +366,7 @@ rule build_energy_totals:
co2_name=resources("co2_totals.csv"),
transport_name=resources("transport_data.csv"),
district_heat_share=resources("district_heat_share.csv"),
heating_efficiencies=resources("heating_efficiencies.csv"),
threads: 16
resources:
mem_mb=10000,
@ -1015,6 +1016,7 @@ rule prepare_sector_network:
RDIR=RDIR,
heat_pump_sources=config_provider("sector", "heat_pump_sources"),
heat_systems=config_provider("sector", "heat_systems"),
energy_totals_year=config_provider("energy", "energy_totals_year"),
input:
unpack(input_profile_offwind),
**rules.cluster_gas_network.output,
@ -1089,6 +1091,7 @@ rule prepare_sector_network:
district_heat_share=resources(
"district_heat_share_elec_s{simpl}_{clusters}_{planning_horizons}.csv"
),
heating_efficiencies=resources("heating_efficiencies.csv"),
temp_soil_total=resources("temp_soil_total_elec_s{simpl}_{clusters}.nc"),
temp_air_total=resources("temp_air_total_elec_s{simpl}_{clusters}.nc"),
cop_profiles=resources("cop_profiles_elec_s{simpl}_{clusters}.nc"),

View File

@ -10,6 +10,7 @@ rule add_existing_baseyear:
existing_capacities=config_provider("existing_capacities"),
costs=config_provider("costs"),
heat_pump_sources=config_provider("sector", "heat_pump_sources"),
energy_totals_year=config_provider("energy", "energy_totals_year"),
input:
network=RESULTS
+ "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc",
@ -26,6 +27,7 @@ rule add_existing_baseyear:
existing_heating_distribution=resources(
"existing_heating_distribution_elec_s{simpl}_{clusters}_{planning_horizons}.csv"
),
heating_efficiencies=resources("heating_efficiencies.csv"),
output:
RESULTS
+ "prenetworks-brownfield/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc",

View File

@ -8,6 +8,7 @@ rule add_existing_baseyear:
existing_capacities=config_provider("existing_capacities"),
costs=config_provider("costs"),
heat_pump_sources=config_provider("sector", "heat_pump_sources"),
energy_totals_year=config_provider("energy", "energy_totals_year"),
input:
network=RESULTS
+ "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc",
@ -25,6 +26,7 @@ rule add_existing_baseyear:
"existing_heating_distribution_elec_s{simpl}_{clusters}_{planning_horizons}.csv"
),
existing_heating="data/existing_infrastructure/existing_heating_raw.csv",
heating_efficiencies=resources("heating_efficiencies.csv"),
output:
RESULTS
+ "prenetworks-brownfield/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc",

View File

@ -418,6 +418,50 @@ def add_power_capacities_installed_before_baseyear(n, grouping_years, costs, bas
]
def get_efficiency(heat_system, carrier, nodes, heating_efficiencies, costs):
"""
Computes the heating system efficiency based on the sector and carrier
type.
Parameters:
-----------
heat_system : object
carrier : str
The type of fuel or energy carrier (e.g., 'gas', 'oil').
nodes : pandas.Series
A pandas Series containing node information used to match the heating efficiency data.
heating_efficiencies : dict
A dictionary containing efficiency values for different carriers and sectors.
costs : pandas.DataFrame
A DataFrame containing boiler cost and efficiency data for different heating systems.
Returns:
--------
efficiency : pandas.Series or float
A pandas Series mapping the efficiencies based on nodes for residential and services sectors, or a single
efficiency value for other heating systems (e.g., urban central).
Notes:
------
- For residential and services sectors, efficiency is mapped based on the nodes.
- For other sectors, the default boiler efficiency is retrieved from the `costs` database.
"""
if heat_system.value == "urban central":
boiler_costs_name = getattr(heat_system, f"{carrier}_boiler_costs_name")
efficiency = costs.at[boiler_costs_name, "efficiency"]
elif heat_system.sector.value == "residential":
key = f"{carrier} residential space efficiency"
efficiency = nodes.str[:2].map(heating_efficiencies[key])
elif heat_system.sector.value == "services":
key = f"{carrier} services space efficiency"
efficiency = nodes.str[:2].map(heating_efficiencies[key])
else:
logger.warning(f"{heat_system} not defined.")
return efficiency
def add_heating_capacities_installed_before_baseyear(
n: pypsa.Network,
baseyear: int,
@ -546,6 +590,10 @@ def add_heating_capacities_installed_before_baseyear(
lifetime=costs.at[heat_system.resistive_heater_costs_name, "lifetime"],
)
efficiency = get_efficiency(
heat_system, "gas", nodes, heating_efficiencies, costs
)
n.madd(
"Link",
nodes,
@ -554,7 +602,7 @@ def add_heating_capacities_installed_before_baseyear(
bus1=nodes + " " + heat_system.value + " heat",
bus2="co2 atmosphere",
carrier=heat_system.value + " gas boiler",
efficiency=costs.at[heat_system.gas_boiler_costs_name, "efficiency"],
efficiency=efficiency,
efficiency2=costs.at["gas", "CO2 intensity"],
capital_cost=(
costs.at[heat_system.gas_boiler_costs_name, "efficiency"]
@ -569,6 +617,10 @@ def add_heating_capacities_installed_before_baseyear(
lifetime=costs.at[heat_system.gas_boiler_costs_name, "lifetime"],
)
efficiency = get_efficiency(
heat_system, "oil", nodes, heating_efficiencies, costs
)
n.madd(
"Link",
nodes,
@ -577,7 +629,7 @@ def add_heating_capacities_installed_before_baseyear(
bus1=nodes + " " + heat_system.value + " heat",
bus2="co2 atmosphere",
carrier=heat_system.value + " oil boiler",
efficiency=costs.at[heat_system.oil_boiler_costs_name, "efficiency"],
efficiency=efficiency,
efficiency2=costs.at["oil", "CO2 intensity"],
capital_cost=costs.at[heat_system.oil_boiler_costs_name, "efficiency"]
* costs.at[heat_system.oil_boiler_costs_name, "fixed"],
@ -621,12 +673,12 @@ if __name__ == "__main__":
snakemake = mock_snakemake(
"add_existing_baseyear",
configfiles="config/config.yaml",
configfiles="config/test/config.myopic.yaml",
simpl="",
clusters="20",
clusters="5",
ll="v1.5",
opts="",
sector_opts="none",
sector_opts="",
planning_horizons=2030,
)
@ -660,6 +712,11 @@ if __name__ == "__main__":
if options["heating"]:
# one could use baseyear here instead (but dangerous if no data)
fn = snakemake.input.heating_efficiencies
year = int(snakemake.params["energy_totals_year"])
heating_efficiencies = pd.read_csv(fn, index_col=[1, 0]).loc[year]
add_heating_capacities_installed_before_baseyear(
n=n,
baseyear=baseyear,

View File

@ -367,6 +367,24 @@ def idees_per_country(ct: str, base_dir: str) -> pd.DataFrame:
assert df.index[43] == "Thermal uses"
ct_totals["thermal uses residential"] = df.iloc[43]
df = pd.read_excel(fn_residential, "RES_hh_eff", index_col=0)
ct_totals["total residential space efficiency"] = df.loc["Space heating"]
assert df.index[5] == "Diesel oil"
ct_totals["oil residential space efficiency"] = df.iloc[5]
assert df.index[6] == "Natural gas"
ct_totals["gas residential space efficiency"] = df.iloc[6]
ct_totals["total residential water efficiency"] = df.loc["Water heating"]
assert df.index[18] == "Diesel oil"
ct_totals["oil residential water efficiency"] = df.iloc[18]
assert df.index[19] == "Natural gas"
ct_totals["gas residential water efficiency"] = df.iloc[19]
# services
df = pd.read_excel(fn_tertiary, "SER_hh_fec", index_col=0)
@ -400,6 +418,24 @@ def idees_per_country(ct: str, base_dir: str) -> pd.DataFrame:
assert df.index[46] == "Thermal uses"
ct_totals["thermal uses services"] = df.iloc[46]
df = pd.read_excel(fn_tertiary, "SER_hh_eff", index_col=0)
ct_totals["total services space efficiency"] = df.loc["Space heating"]
assert df.index[5] == "Diesel oil"
ct_totals["oil services space efficiency"] = df.iloc[5]
assert df.index[7] == "Conventional gas heaters"
ct_totals["gas services space efficiency"] = df.iloc[7]
ct_totals["total services water efficiency"] = df.loc["Hot water"]
assert df.index[20] == "Diesel oil"
ct_totals["oil services water efficiency"] = df.iloc[20]
assert df.index[21] == "Natural gas"
ct_totals["gas services water efficiency"] = df.iloc[21]
# agriculture, forestry and fishing
start = "Detailed split of energy consumption (ktoe)"
@ -576,7 +612,8 @@ def build_idees(countries: List[str]) -> pd.DataFrame:
# efficiency kgoe/100km -> ktoe/100km so that after conversion TWh/100km
totals.loc[:, "passenger car efficiency"] /= 1e6
# convert ktoe to TWh
exclude = totals.columns.str.fullmatch("passenger cars")
patterns = ["passenger cars", ".*space efficiency", ".*water efficiency"]
exclude = totals.columns.str.fullmatch("|".join(patterns))
totals = totals.copy()
totals.loc[:, ~exclude] *= 11.63 / 1e3
@ -654,11 +691,14 @@ def build_energy_totals(
eurostat_countries = eurostat.index.unique(0)
eurostat_years = eurostat.index.unique(1)
to_drop = ["passenger cars", "passenger car efficiency"]
new_index = pd.MultiIndex.from_product(
[countries, eurostat_years], names=["country", "year"]
)
efficiency_keywords = ["space efficiency", "water efficiency"]
to_drop = idees.columns[idees.columns.str.contains("|".join(efficiency_keywords))]
to_drop = to_drop.append(pd.Index(["passenger cars", "passenger car efficiency"]))
df = idees.reindex(new_index).drop(to_drop, axis=1)
in_eurostat = df.index.levels[0].intersection(eurostat_countries)
@ -1501,6 +1541,59 @@ def build_transformation_output_coke(eurostat, fn):
df.to_csv(fn)
def build_heating_efficiencies(
countries: List[str], idees: pd.DataFrame
) -> pd.DataFrame:
"""
Build heating efficiencies for a set of countries based on IDEES data.
Parameters
----------
countries : List[str]
List of country codes.
idees : pd.DataFrame
DataFrame with IDEES data.
Returns
-------
pd.DataFrame
DataFrame with heating efficiencies.
Notes
-----
- It fills missing data with average data.
"""
years = np.arange(2000, 2022)
cols = idees.columns[
idees.columns.str.contains("space efficiency")
^ idees.columns.str.contains("water efficiency")
]
heating_efficiencies = pd.DataFrame(idees[cols])
new_index = pd.MultiIndex.from_product(
[countries, heating_efficiencies.index.unique(1)],
names=["country", "year"],
)
heating_efficiencies = heating_efficiencies.reindex(index=new_index)
for col in cols:
unstacked = heating_efficiencies[col].unstack()
fillvalue = unstacked.mean()
for ct in unstacked.index:
mask = unstacked.loc[ct].isna()
unstacked.loc[ct, mask] = fillvalue[mask]
heating_efficiencies[col] = unstacked.stack()
return heating_efficiencies
# %%
if __name__ == "__main__":
if "snakemake" not in globals():
@ -1556,3 +1649,6 @@ if __name__ == "__main__":
transport = build_transport_data(countries, population, idees)
transport.to_csv(snakemake.output.transport_name)
heating_efficiencies = build_heating_efficiencies(countries, idees)
heating_efficiencies.to_csv(snakemake.output.heating_efficiencies)

View File

@ -1839,9 +1839,14 @@ def build_heat_demand(n):
for sector, use in product(sectors, uses):
name = f"{sector} {use}"
# efficiency for final energy to thermal energy service
eff = pop_weighted_energy_totals.index.str[:2].map(
heating_efficiencies[f"total {sector} {use} efficiency"]
)
heat_demand[name] = (
heat_demand_shape[name] / heat_demand_shape[name].sum()
).multiply(pop_weighted_energy_totals[f"total {sector} {use}"]) * 1e6
).multiply(pop_weighted_energy_totals[f"total {sector} {use}"] * eff) * 1e6
electric_heat_supply[name] = (
heat_demand_shape[name] / heat_demand_shape[name].sum()
).multiply(pop_weighted_energy_totals[f"electricity {sector} {use}"]) * 1e6
@ -4373,6 +4378,10 @@ if __name__ == "__main__":
)
pop_weighted_energy_totals.update(pop_weighted_heat_totals)
fn = snakemake.input.heating_efficiencies
year = int(snakemake.params["energy_totals_year"])
heating_efficiencies = pd.read_csv(fn, index_col=[1, 0]).loc[year]
patch_electricity_network(n)
spatial = define_spatial(pop_layout.index, options)