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:
commit
7acda28ee7
@ -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.
|
||||
|
||||
|
@ -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"),
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user