move calculation of district heating share to its own script

Now the script build_district_heat_share.py does what the old function
create_nodes_for_heating() in prepare_sector_networks.py did.

There is no need to build nodes lists for each heating sector, since
all nodes have district heating now.
This commit is contained in:
Tom Brown 2024-01-15 18:55:09 +01:00 committed by Fabian Neumann
parent bd8a5ecf2b
commit 1a477d6b32
5 changed files with 164 additions and 107 deletions

View File

@ -710,6 +710,27 @@ rule build_transport_demand:
"../scripts/build_transport_demand.py" "../scripts/build_transport_demand.py"
rule build_district_heat_share:
params:
sector=config["sector"],
input:
district_heat_share=RESOURCES + "district_heat_share.csv",
clustered_pop_layout=RESOURCES + "pop_layout_elec_s{simpl}_{clusters}.csv",
output:
district_heat_share=RESOURCES + "district_heat_share_elec_s{simpl}_{clusters}_{planning_horizons}.csv",
threads: 1
resources:
mem_mb=1000,
log:
LOGS + "build_district_heat_share_s{simpl}_{clusters}_{planning_horizons}.log",
conda:
"../envs/environment.yaml"
script:
"../scripts/build_district_heat_share.py"
rule prepare_sector_network: rule prepare_sector_network:
params: params:
co2_budget=config["co2_budget"], co2_budget=config["co2_budget"],
@ -762,6 +783,7 @@ rule prepare_sector_network:
industrial_demand=RESOURCES industrial_demand=RESOURCES
+ "industrial_energy_demand_elec_s{simpl}_{clusters}_{planning_horizons}.csv", + "industrial_energy_demand_elec_s{simpl}_{clusters}_{planning_horizons}.csv",
hourly_heat_demand_total=RESOURCES + "hourly_heat_demand_total_elec_s{simpl}_{clusters}.nc", hourly_heat_demand_total=RESOURCES + "hourly_heat_demand_total_elec_s{simpl}_{clusters}.nc",
district_heat_share=RESOURCES + "district_heat_share_elec_s{simpl}_{clusters}_{planning_horizons}.csv",
temp_soil_total=RESOURCES + "temp_soil_total_elec_s{simpl}_{clusters}.nc", temp_soil_total=RESOURCES + "temp_soil_total_elec_s{simpl}_{clusters}.nc",
temp_soil_rural=RESOURCES + "temp_soil_rural_elec_s{simpl}_{clusters}.nc", temp_soil_rural=RESOURCES + "temp_soil_rural_elec_s{simpl}_{clusters}.nc",
temp_soil_urban=RESOURCES + "temp_soil_urban_elec_s{simpl}_{clusters}.nc", temp_soil_urban=RESOURCES + "temp_soil_urban_elec_s{simpl}_{clusters}.nc",

View File

@ -0,0 +1,77 @@
# -*- coding: utf-8 -*-
# SPDX-FileCopyrightText: : 2020-2023 The PyPSA-Eur Authors
#
# SPDX-License-Identifier: MIT
"""
Build district heat shares at each node, depending on investment year.
"""
import pandas as pd
from prepare_sector_network import get
import logging
logger = logging.getLogger(__name__)
if __name__ == "__main__":
if "snakemake" not in globals():
from _helpers import mock_snakemake
snakemake = mock_snakemake(
"build_heat_demands",
simpl="",
clusters=48,
)
investment_year = int(snakemake.wildcards.planning_horizons[-4:])
pop_layout = pd.read_csv(snakemake.input.clustered_pop_layout,
index_col=0)
district_heat_share = pd.read_csv(snakemake.input.district_heat_share,
index_col=0).squeeze()
# make ct-based share nodal
district_heat_share = district_heat_share.loc[pop_layout.ct]
district_heat_share.index = pop_layout.index
# total urban population per country
ct_urban = pop_layout.urban.groupby(pop_layout.ct).sum()
# distribution of urban population within a country
pop_layout["urban_ct_fraction"] = pop_layout.urban / pop_layout.ct.map(ct_urban.get)
# fraction of node that is urban
urban_fraction = pop_layout.urban / pop_layout[["rural", "urban"]].sum(axis=1)
# maximum potential of urban demand covered by district heating
central_fraction = snakemake.config["sector"]["district_heating"]["potential"]
# district heating share at each node
dist_fraction_node = (
district_heat_share * pop_layout["urban_ct_fraction"] / pop_layout["fraction"]
)
# if district heating share larger than urban fraction -> set urban
# fraction to district heating share
urban_fraction = pd.concat([urban_fraction, dist_fraction_node], axis=1).max(axis=1)
# difference of max potential and today's share of district heating
diff = (urban_fraction * central_fraction) - dist_fraction_node
progress = get(snakemake.config["sector"]["district_heating"]["progress"], investment_year)
dist_fraction_node += diff * progress
logger.info(
f"Increase district heating share by a progress factor of {progress:.2%} "
f"resulting in new average share of {dist_fraction_node.mean():.2%}"
)
df = pd.DataFrame(dtype=float)
df["original district heat share"] = district_heat_share
df["district fraction of node"] = dist_fraction_node
df["urban fraction"] = urban_fraction
df.to_csv(snakemake.output.district_heat_share)

View File

@ -568,7 +568,7 @@ def build_energy_totals(countries, eurostat, swiss, idees):
return df return df
def build_district_heat_share(idees): def build_district_heat_share(countries, idees):
# district heating share # district heating share
district_heat = idees[ district_heat = idees[
@ -578,6 +578,8 @@ def build_district_heat_share(idees):
district_heat_share = district_heat/total_heat district_heat_share = district_heat/total_heat
district_heat_share = district_heat_share.reindex(countries)
# Missing district heating share # Missing district heating share
dh_share = pd.read_csv( dh_share = pd.read_csv(
snakemake.input.district_heat_share, index_col=0, usecols=[0, 1] snakemake.input.district_heat_share, index_col=0, usecols=[0, 1]
@ -758,7 +760,7 @@ if __name__ == "__main__":
energy = build_energy_totals(countries, eurostat, swiss, idees) energy = build_energy_totals(countries, eurostat, swiss, idees)
energy.to_csv(snakemake.output.energy_name) energy.to_csv(snakemake.output.energy_name)
district_heat_share = build_district_heat_share(idees) district_heat_share = build_district_heat_share(countries, idees)
district_heat_share.to_csv(snakemake.output.district_heat_share) district_heat_share.to_csv(snakemake.output.district_heat_share)
base_year_emissions = params["base_emissions_year"] base_year_emissions = params["base_emissions_year"]

View File

@ -26,9 +26,4 @@ if __name__ == "__main__":
nodal_energy_totals.index = pop_layout.index nodal_energy_totals.index = pop_layout.index
nodal_energy_totals = nodal_energy_totals.multiply(pop_layout.fraction, axis=0) nodal_energy_totals = nodal_energy_totals.multiply(pop_layout.fraction, axis=0)
# district heating share should not be divided by population fraction
dh_share = energy_totals["district heat share"].loc[pop_layout.ct].fillna(0.0)
dh_share.index = pop_layout.index
nodal_energy_totals["district heat share"] = dh_share
nodal_energy_totals.to_csv(snakemake.output[0]) nodal_energy_totals.to_csv(snakemake.output[0])

View File

@ -1678,7 +1678,10 @@ def add_heat(n, costs):
heat_demand = build_heat_demand(n) heat_demand = build_heat_demand(n)
nodes, dist_fraction, urban_fraction = create_nodes_for_heat_sector() district_heat_info = pd.read_csv(snakemake.input.district_heat_share,
index_col=0)
dist_fraction = district_heat_info["district fraction of node"]
urban_fraction = district_heat_info["urban fraction"]
# NB: must add costs of central heating afterwards (EUR 400 / kWpeak, 50a, 1% FOM from Fraunhofer ISE) # NB: must add costs of central heating afterwards (EUR 400 / kWpeak, 50a, 1% FOM from Fraunhofer ISE)
@ -1715,6 +1718,8 @@ def add_heat(n, costs):
# 1e3 converts from W/m^2 to MW/(1000m^2) = kW/m^2 # 1e3 converts from W/m^2 to MW/(1000m^2) = kW/m^2
solar_thermal = options["solar_cf_correction"] * solar_thermal / 1e3 solar_thermal = options["solar_cf_correction"] * solar_thermal / 1e3
nodes = pop_layout.index
for name in heat_systems: for name in heat_systems:
name_type = "central" if name == "urban central" else "decentral" name_type = "central" if name == "urban central" else "decentral"
@ -1722,8 +1727,8 @@ def add_heat(n, costs):
n.madd( n.madd(
"Bus", "Bus",
nodes[name] + f" {name} heat", nodes + f" {name} heat",
location=nodes[name], location=nodes,
carrier=name + " heat", carrier=name + " heat",
unit="MWh_th", unit="MWh_th",
) )
@ -1731,9 +1736,9 @@ def add_heat(n, costs):
if name == "urban central" and options.get("central_heat_vent"): if name == "urban central" and options.get("central_heat_vent"):
n.madd( n.madd(
"Generator", "Generator",
nodes[name] + f" {name} heat vent", nodes + f" {name} heat vent",
bus=nodes[name] + f" {name} heat", bus=nodes + f" {name} heat",
location=nodes[name], location=nodes,
carrier=name + " heat vent", carrier=name + " heat vent",
p_nom_extendable=True, p_nom_extendable=True,
p_max_pu=0, p_max_pu=0,
@ -1746,11 +1751,11 @@ def add_heat(n, costs):
for sector in sectors: for sector in sectors:
# heat demand weighting # heat demand weighting
if "rural" in name: if "rural" in name:
factor = 1 - urban_fraction[nodes[name]] factor = 1 - urban_fraction[nodes]
elif "urban central" in name: elif "urban central" in name:
factor = dist_fraction[nodes[name]] factor = dist_fraction[nodes]
elif "urban decentral" in name: elif "urban decentral" in name:
factor = urban_fraction[nodes[name]] - dist_fraction[nodes[name]] factor = urban_fraction[nodes] - dist_fraction[nodes]
else: else:
raise NotImplementedError( raise NotImplementedError(
f" {name} not in " f"heat systems: {heat_systems}" f" {name} not in " f"heat systems: {heat_systems}"
@ -1761,7 +1766,7 @@ def add_heat(n, costs):
heat_demand[[sector + " water", sector + " space"]] heat_demand[[sector + " water", sector + " space"]]
.T.groupby(level=1) .T.groupby(level=1)
.sum() .sum()
.T[nodes[name]] .T[nodes]
.multiply(factor) .multiply(factor)
) )
@ -1769,7 +1774,7 @@ def add_heat(n, costs):
heat_load = ( heat_load = (
heat_demand.T.groupby(level=1) heat_demand.T.groupby(level=1)
.sum() .sum()
.T[nodes[name]] .T[nodes]
.multiply( .multiply(
factor * (1 + options["district_heating"]["district_heating_loss"]) factor * (1 + options["district_heating"]["district_heating_loss"])
) )
@ -1777,9 +1782,9 @@ def add_heat(n, costs):
n.madd( n.madd(
"Load", "Load",
nodes[name], nodes,
suffix=f" {name} heat", suffix=f" {name} heat",
bus=nodes[name] + f" {name} heat", bus=nodes + f" {name} heat",
carrier=name + " heat", carrier=name + " heat",
p_set=heat_load, p_set=heat_load,
) )
@ -1790,17 +1795,17 @@ def add_heat(n, costs):
costs_name = f"{name_type} {heat_pump_type}-sourced heat pump" costs_name = f"{name_type} {heat_pump_type}-sourced heat pump"
efficiency = ( efficiency = (
cop[heat_pump_type][nodes[name]] cop[heat_pump_type][nodes]
if options["time_dep_hp_cop"] if options["time_dep_hp_cop"]
else costs.at[costs_name, "efficiency"] else costs.at[costs_name, "efficiency"]
) )
n.madd( n.madd(
"Link", "Link",
nodes[name], nodes,
suffix=f" {name} {heat_pump_type} heat pump", suffix=f" {name} {heat_pump_type} heat pump",
bus0=nodes[name], bus0=nodes,
bus1=nodes[name] + f" {name} heat", bus1=nodes + f" {name} heat",
carrier=f"{name} {heat_pump_type} heat pump", carrier=f"{name} {heat_pump_type} heat pump",
efficiency=efficiency, efficiency=efficiency,
capital_cost=costs.at[costs_name, "efficiency"] capital_cost=costs.at[costs_name, "efficiency"]
@ -1814,17 +1819,17 @@ def add_heat(n, costs):
n.madd( n.madd(
"Bus", "Bus",
nodes[name] + f" {name} water tanks", nodes + f" {name} water tanks",
location=nodes[name], location=nodes,
carrier=name + " water tanks", carrier=name + " water tanks",
unit="MWh_th", unit="MWh_th",
) )
n.madd( n.madd(
"Link", "Link",
nodes[name] + f" {name} water tanks charger", nodes + f" {name} water tanks charger",
bus0=nodes[name] + f" {name} heat", bus0=nodes + f" {name} heat",
bus1=nodes[name] + f" {name} water tanks", bus1=nodes + f" {name} water tanks",
efficiency=costs.at["water tank charger", "efficiency"], efficiency=costs.at["water tank charger", "efficiency"],
carrier=name + " water tanks charger", carrier=name + " water tanks charger",
p_nom_extendable=True, p_nom_extendable=True,
@ -1832,9 +1837,9 @@ def add_heat(n, costs):
n.madd( n.madd(
"Link", "Link",
nodes[name] + f" {name} water tanks discharger", nodes + f" {name} water tanks discharger",
bus0=nodes[name] + f" {name} water tanks", bus0=nodes + f" {name} water tanks",
bus1=nodes[name] + f" {name} heat", bus1=nodes + f" {name} heat",
carrier=name + " water tanks discharger", carrier=name + " water tanks discharger",
efficiency=costs.at["water tank discharger", "efficiency"], efficiency=costs.at["water tank discharger", "efficiency"],
p_nom_extendable=True, p_nom_extendable=True,
@ -1853,8 +1858,8 @@ def add_heat(n, costs):
n.madd( n.madd(
"Store", "Store",
nodes[name] + f" {name} water tanks", nodes + f" {name} water tanks",
bus=nodes[name] + f" {name} water tanks", bus=nodes + f" {name} water tanks",
e_cyclic=True, e_cyclic=True,
e_nom_extendable=True, e_nom_extendable=True,
carrier=name + " water tanks", carrier=name + " water tanks",
@ -1868,9 +1873,9 @@ def add_heat(n, costs):
n.madd( n.madd(
"Link", "Link",
nodes[name] + f" {name} resistive heater", nodes + f" {name} resistive heater",
bus0=nodes[name], bus0=nodes,
bus1=nodes[name] + f" {name} heat", bus1=nodes + f" {name} heat",
carrier=name + " resistive heater", carrier=name + " resistive heater",
efficiency=costs.at[key, "efficiency"], efficiency=costs.at[key, "efficiency"],
capital_cost=costs.at[key, "efficiency"] * costs.at[key, "fixed"], capital_cost=costs.at[key, "efficiency"] * costs.at[key, "fixed"],
@ -1883,10 +1888,10 @@ def add_heat(n, costs):
n.madd( n.madd(
"Link", "Link",
nodes[name] + f" {name} gas boiler", nodes + f" {name} gas boiler",
p_nom_extendable=True, p_nom_extendable=True,
bus0=spatial.gas.df.loc[nodes[name], "nodes"].values, bus0=spatial.gas.df.loc[nodes, "nodes"].values,
bus1=nodes[name] + f" {name} heat", bus1=nodes + f" {name} heat",
bus2="co2 atmosphere", bus2="co2 atmosphere",
carrier=name + " gas boiler", carrier=name + " gas boiler",
efficiency=costs.at[key, "efficiency"], efficiency=costs.at[key, "efficiency"],
@ -1900,13 +1905,13 @@ def add_heat(n, costs):
n.madd( n.madd(
"Generator", "Generator",
nodes[name], nodes,
suffix=f" {name} solar thermal collector", suffix=f" {name} solar thermal collector",
bus=nodes[name] + f" {name} heat", bus=nodes + f" {name} heat",
carrier=name + " solar thermal", carrier=name + " solar thermal",
p_nom_extendable=True, p_nom_extendable=True,
capital_cost=costs.at[name_type + " solar thermal", "fixed"], capital_cost=costs.at[name_type + " solar thermal", "fixed"],
p_max_pu=solar_thermal[nodes[name]], p_max_pu=solar_thermal[nodes],
lifetime=costs.at[name_type + " solar thermal", "lifetime"], lifetime=costs.at[name_type + " solar thermal", "lifetime"],
) )
@ -1914,10 +1919,10 @@ def add_heat(n, costs):
# add gas CHP; biomass CHP is added in biomass section # add gas CHP; biomass CHP is added in biomass section
n.madd( n.madd(
"Link", "Link",
nodes[name] + " urban central gas CHP", nodes + " urban central gas CHP",
bus0=spatial.gas.df.loc[nodes[name], "nodes"].values, bus0=spatial.gas.df.loc[nodes, "nodes"].values,
bus1=nodes[name], bus1=nodes,
bus2=nodes[name] + " urban central heat", bus2=nodes + " urban central heat",
bus3="co2 atmosphere", bus3="co2 atmosphere",
carrier="urban central gas CHP", carrier="urban central gas CHP",
p_nom_extendable=True, p_nom_extendable=True,
@ -1933,12 +1938,12 @@ def add_heat(n, costs):
n.madd( n.madd(
"Link", "Link",
nodes[name] + " urban central gas CHP CC", nodes + " urban central gas CHP CC",
bus0=spatial.gas.df.loc[nodes[name], "nodes"].values, bus0=spatial.gas.df.loc[nodes, "nodes"].values,
bus1=nodes[name], bus1=nodes,
bus2=nodes[name] + " urban central heat", bus2=nodes + " urban central heat",
bus3="co2 atmosphere", bus3="co2 atmosphere",
bus4=spatial.co2.df.loc[nodes[name], "nodes"].values, bus4=spatial.co2.df.loc[nodes, "nodes"].values,
carrier="urban central gas CHP CC", carrier="urban central gas CHP CC",
p_nom_extendable=True, p_nom_extendable=True,
capital_cost=costs.at["central gas CHP", "fixed"] capital_cost=costs.at["central gas CHP", "fixed"]
@ -1970,11 +1975,11 @@ def add_heat(n, costs):
if options["chp"] and options["micro_chp"] and name != "urban central": if options["chp"] and options["micro_chp"] and name != "urban central":
n.madd( n.madd(
"Link", "Link",
nodes[name] + f" {name} micro gas CHP", nodes + f" {name} micro gas CHP",
p_nom_extendable=True, p_nom_extendable=True,
bus0=spatial.gas.df.loc[nodes[name], "nodes"].values, bus0=spatial.gas.df.loc[nodes, "nodes"].values,
bus1=nodes[name], bus1=nodes,
bus2=nodes[name] + f" {name} heat", bus2=nodes + f" {name} heat",
bus3="co2 atmosphere", bus3="co2 atmosphere",
carrier=name + " micro gas CHP", carrier=name + " micro gas CHP",
efficiency=costs.at["micro CHP", "efficiency"], efficiency=costs.at["micro CHP", "efficiency"],
@ -2105,50 +2110,6 @@ def add_heat(n, costs):
) )
def create_nodes_for_heat_sector():
# TODO pop_layout
# rural are areas with low heating density and individual heating
# urban are areas with high heating density
# urban can be split into district heating (central) and individual heating (decentral)
ct_urban = pop_layout.urban.groupby(pop_layout.ct).sum()
# distribution of urban population within a country
pop_layout["urban_ct_fraction"] = pop_layout.urban / pop_layout.ct.map(ct_urban.get)
sectors = ["residential", "services"]
nodes = {}
urban_fraction = pop_layout.urban / pop_layout[["rural", "urban"]].sum(axis=1)
for sector in sectors:
nodes[sector + " rural"] = pop_layout.index
nodes[sector + " urban decentral"] = pop_layout.index
district_heat_share = pop_weighted_energy_totals["district heat share"]
# maximum potential of urban demand covered by district heating
central_fraction = options["district_heating"]["potential"]
# district heating share at each node
dist_fraction_node = (
district_heat_share * pop_layout["urban_ct_fraction"] / pop_layout["fraction"]
)
nodes["urban central"] = dist_fraction_node.index
# if district heating share larger than urban fraction -> set urban
# fraction to district heating share
urban_fraction = pd.concat([urban_fraction, dist_fraction_node], axis=1).max(axis=1)
# difference of max potential and today's share of district heating
diff = (urban_fraction * central_fraction) - dist_fraction_node
progress = get(options["district_heating"]["progress"], investment_year)
dist_fraction_node += diff * progress
logger.info(
f"Increase district heating share by a progress factor of {progress:.2%} "
f"resulting in new average share of {dist_fraction_node.mean():.2%}"
)
return nodes, dist_fraction_node, urban_fraction
def add_biomass(n, costs): def add_biomass(n, costs):
logger.info("Add biomass") logger.info("Add biomass")
@ -2366,7 +2327,7 @@ def add_biomass(n, costs):
if options["biomass_boiler"]: if options["biomass_boiler"]:
# TODO: Add surcharge for pellets # TODO: Add surcharge for pellets
nodes_heat = create_nodes_for_heat_sector()[0] nodes = pop_layout.index
for name in [ for name in [
"residential rural", "residential rural",
"services rural", "services rural",
@ -2375,10 +2336,10 @@ def add_biomass(n, costs):
]: ]:
n.madd( n.madd(
"Link", "Link",
nodes_heat[name] + f" {name} biomass boiler", nodes + f" {name} biomass boiler",
p_nom_extendable=True, p_nom_extendable=True,
bus0=spatial.biomass.df.loc[nodes_heat[name], "nodes"].values, bus0=spatial.biomass.df.loc[nodes, "nodes"].values,
bus1=nodes_heat[name] + f" {name} heat", bus1=nodes + f" {name} heat",
carrier=name + " biomass boiler", carrier=name + " biomass boiler",
efficiency=costs.at["biomass boiler", "efficiency"], efficiency=costs.at["biomass boiler", "efficiency"],
capital_cost=costs.at["biomass boiler", "efficiency"] capital_cost=costs.at["biomass boiler", "efficiency"]
@ -2821,7 +2782,7 @@ def add_industry(n, costs):
) )
if options["oil_boilers"]: if options["oil_boilers"]:
nodes_heat = create_nodes_for_heat_sector()[0] nodes = pop_layout.index
for name in [ for name in [
"residential rural", "residential rural",
@ -2831,10 +2792,10 @@ def add_industry(n, costs):
]: ]:
n.madd( n.madd(
"Link", "Link",
nodes_heat[name] + f" {name} oil boiler", nodes + f" {name} oil boiler",
p_nom_extendable=True, p_nom_extendable=True,
bus0=spatial.oil.nodes, bus0=spatial.oil.nodes,
bus1=nodes_heat[name] + f" {name} heat", bus1=nodes + f" {name} heat",
bus2="co2 atmosphere", bus2="co2 atmosphere",
carrier=f"{name} oil boiler", carrier=f"{name} oil boiler",
efficiency=costs.at["decentral oil boiler", "efficiency"], efficiency=costs.at["decentral oil boiler", "efficiency"],