Skip to content

Commit

Permalink
Merge pull request #1255 from PyPSA/fix_building_fec
Browse files Browse the repository at this point in the history
Use JRC-IDEES thermal energy service instead of FE for buildings heating demand
  • Loading branch information
lisazeyen authored Sep 11, 2024
2 parents f118d15 + d9ab494 commit 7acda28
Show file tree
Hide file tree
Showing 7 changed files with 179 additions and 8 deletions.
2 changes: 2 additions & 0 deletions doc/release_notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
3 changes: 3 additions & 0 deletions rules/build_sector.smk
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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"),
Expand Down
2 changes: 2 additions & 0 deletions rules/solve_myopic.smk
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions rules/solve_perfect.smk
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
67 changes: 62 additions & 5 deletions scripts/add_existing_baseyear.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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"]
Expand All @@ -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,
Expand All @@ -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"],
Expand Down Expand Up @@ -621,12 +673,12 @@ def add_heating_capacities_installed_before_baseyear(

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,
)

Expand Down Expand Up @@ -660,6 +712,11 @@ def add_heating_capacities_installed_before_baseyear(

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,
Expand Down
100 changes: 98 additions & 2 deletions scripts/build_energy_totals.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)"
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -1556,3 +1649,6 @@ def build_transformation_output_coke(eurostat, fn):

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)
11 changes: 10 additions & 1 deletion scripts/prepare_sector_network.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -4373,6 +4378,10 @@ def add_enhanced_geothermal(n, egs_potentials, egs_overlap, costs):
)
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)
Expand Down

0 comments on commit 7acda28

Please sign in to comment.