Merge pull request #1175 from PyPSA/update-district-heating-cops
Approximate district heating COPs via Jensen et al. 2018
This commit is contained in:
commit
d7436a6405
@ -410,6 +410,22 @@ sector:
|
||||
2045: 0.8
|
||||
2050: 1.0
|
||||
district_heating_loss: 0.15
|
||||
forward_temperature: 90 #C
|
||||
return_temperature: 50 #C
|
||||
heat_source_cooling: 6 #K
|
||||
heat_pump_cop_approximation:
|
||||
refrigerant: ammonia
|
||||
heat_exchanger_pinch_point_temperature_difference: 5 #K
|
||||
isentropic_compressor_efficiency: 0.8
|
||||
heat_loss: 0.0
|
||||
heat_pump_sources:
|
||||
urban central:
|
||||
- air
|
||||
urban decentral:
|
||||
- air
|
||||
rural:
|
||||
- air
|
||||
- ground
|
||||
cluster_heat_buses: true
|
||||
heat_demand_cutout: default
|
||||
bev_dsm_restriction_value: 0.75
|
||||
@ -492,7 +508,7 @@ sector:
|
||||
aviation_demand_factor: 1.
|
||||
HVC_demand_factor: 1.
|
||||
time_dep_hp_cop: true
|
||||
heat_pump_sink_T: 55.
|
||||
heat_pump_sink_T_individual_heating: 55.
|
||||
reduce_space_heat_exogenously: true
|
||||
reduce_space_heat_exogenously_factor:
|
||||
2020: 0.10 # this results in a space heat demand reduction of 10%
|
||||
|
@ -342,5 +342,5 @@ texinfo_documents = [
|
||||
|
||||
# Example configuration for intersphinx: refer to the Python standard library.
|
||||
intersphinx_mapping = {
|
||||
'https://docs.python.org/': ('https://docs.python.org/3', None),
|
||||
"https://docs.python.org/": ("https://docs.python.org/3", None),
|
||||
}
|
||||
|
@ -9,6 +9,18 @@ district_heating,--,,`prepare_sector_network.py <https://github.com/PyPSA/pypsa-
|
||||
-- potential,--,float,maximum fraction of urban demand which can be supplied by district heating
|
||||
-- progress,--,Dictionary with planning horizons as keys., Increase of today's district heating demand to potential maximum district heating share. Progress = 0 means today's district heating share. Progress = 1 means maximum fraction of urban demand is supplied by district heating
|
||||
-- district_heating_loss,--,float,Share increase in district heat demand in urban central due to heat losses
|
||||
-- forward_temperature,°C,float,Forward temperature in district heating
|
||||
-- return_temperature,°C,float,Return temperature in district heating. Must be lower than forward temperature
|
||||
-- heat_source_cooling,K,float,Cooling of heat source for heat pumps
|
||||
-- heat_pump_cop_approximation,,,
|
||||
-- -- refrigerant,--,"{ammonia, isobutane}",Heat pump refrigerant assumed for COP approximation
|
||||
-- -- heat_exchanger_pinch_point_temperature_difference,K,float,Heat pump pinch point temperature difference in heat exchangers assumed for approximation.
|
||||
-- -- isentropic_compressor_efficiency,--,float,Isentropic efficiency of heat pump compressor assumed for approximation. Must be between 0 and 1.
|
||||
-- -- heat_loss,--,float,Heat pump heat loss assumed for approximation. Must be between 0 and 1.
|
||||
-- heat_pump_sources,--,,
|
||||
-- -- urban central,--,List of heat sources for heat pumps in urban central heating,
|
||||
-- -- urban decentral,--,List of heat sources for heat pumps in urban decentral heating,
|
||||
-- -- rural,--,List of heat sources for heat pumps in rural heating,
|
||||
cluster_heat_buses,--,"{true, false}",Cluster residential and service heat buses in `prepare_sector_network.py <https://github.com/PyPSA/pypsa-eur-sec/blob/master/scripts/prepare_sector_network.py>`_ to one to save memory.
|
||||
,,,
|
||||
bev_dsm_restriction _value,--,float,Adds a lower state of charge (SOC) limit for battery electric vehicles (BEV) to manage its own energy demand (DSM). Located in `build_transport_demand.py <https://github.com/PyPSA/pypsa-eur-sec/blob/master/scripts/build_transport_demand.py>`_. Set to 0 for no restriction on BEV DSM
|
||||
|
|
@ -10,6 +10,8 @@ Release Notes
|
||||
Upcoming Release
|
||||
================
|
||||
|
||||
* Changed heat pump COP approximation for central heating to be based on `Jensen et al. (2018) <https://backend.orbit.dtu.dk/ws/portalfiles/portal/151965635/MAIN_Final.pdf>`__ and a default forward temperature of 90C. This is more realistic for district heating than the previously used approximation method.
|
||||
|
||||
* split solid biomass potentials into solid biomass and municipal solid waste. Add option to use municipal solid waste. This option is only activated in combination with the flag ``waste_to_energy``
|
||||
|
||||
* Add option to import solid biomass
|
||||
|
@ -217,13 +217,27 @@ rule build_temperature_profiles:
|
||||
|
||||
rule build_cop_profiles:
|
||||
params:
|
||||
heat_pump_sink_T=config_provider("sector", "heat_pump_sink_T"),
|
||||
heat_pump_sink_T_decentral_heating=config_provider(
|
||||
"sector", "heat_pump_sink_T_individual_heating"
|
||||
),
|
||||
forward_temperature_central_heating=config_provider(
|
||||
"sector", "district_heating", "forward_temperature"
|
||||
),
|
||||
return_temperature_central_heating=config_provider(
|
||||
"sector", "district_heating", "return_temperature"
|
||||
),
|
||||
heat_source_cooling_central_heating=config_provider(
|
||||
"sector", "district_heating", "heat_source_cooling"
|
||||
),
|
||||
heat_pump_cop_approximation_central_heating=config_provider(
|
||||
"sector", "district_heating", "heat_pump_cop_approximation"
|
||||
),
|
||||
heat_pump_sources=config_provider("sector", "heat_pump_sources"),
|
||||
input:
|
||||
temp_soil_total=resources("temp_soil_total_elec_s{simpl}_{clusters}.nc"),
|
||||
temp_air_total=resources("temp_air_total_elec_s{simpl}_{clusters}.nc"),
|
||||
output:
|
||||
cop_soil_total=resources("cop_soil_total_elec_s{simpl}_{clusters}.nc"),
|
||||
cop_air_total=resources("cop_air_total_elec_s{simpl}_{clusters}.nc"),
|
||||
cop_profiles=resources("cop_profiles_elec_s{simpl}_{clusters}.nc"),
|
||||
resources:
|
||||
mem_mb=20000,
|
||||
log:
|
||||
@ -233,7 +247,7 @@ rule build_cop_profiles:
|
||||
conda:
|
||||
"../envs/environment.yaml"
|
||||
script:
|
||||
"../scripts/build_cop_profiles.py"
|
||||
"../scripts/build_cop_profiles/run.py"
|
||||
|
||||
|
||||
def solar_thermal_cutout(wildcards):
|
||||
@ -941,6 +955,8 @@ rule prepare_sector_network:
|
||||
adjustments=config_provider("adjustments", "sector"),
|
||||
emissions_scope=config_provider("energy", "emissions"),
|
||||
RDIR=RDIR,
|
||||
heat_pump_sources=config_provider("sector", "heat_pump_sources"),
|
||||
heat_systems=config_provider("sector", "heat_systems"),
|
||||
input:
|
||||
unpack(input_profile_offwind),
|
||||
**rules.cluster_gas_network.output,
|
||||
@ -1017,8 +1033,7 @@ rule prepare_sector_network:
|
||||
),
|
||||
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_soil_total=resources("cop_soil_total_elec_s{simpl}_{clusters}.nc"),
|
||||
cop_air_total=resources("cop_air_total_elec_s{simpl}_{clusters}.nc"),
|
||||
cop_profiles=resources("cop_profiles_elec_s{simpl}_{clusters}.nc"),
|
||||
solar_thermal_total=lambda w: (
|
||||
resources("solar_thermal_total_elec_s{simpl}_{clusters}.nc")
|
||||
if config_provider("sector", "solar_thermal")(w)
|
||||
|
@ -9,6 +9,7 @@ rule add_existing_baseyear:
|
||||
sector=config_provider("sector"),
|
||||
existing_capacities=config_provider("existing_capacities"),
|
||||
costs=config_provider("costs"),
|
||||
heat_pump_sources=config_provider("sector", "heat_pump_sources"),
|
||||
input:
|
||||
network=RESULTS
|
||||
+ "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc",
|
||||
@ -21,8 +22,7 @@ rule add_existing_baseyear:
|
||||
config_provider("scenario", "planning_horizons", 0)(w)
|
||||
)
|
||||
),
|
||||
cop_soil_total=resources("cop_soil_total_elec_s{simpl}_{clusters}.nc"),
|
||||
cop_air_total=resources("cop_air_total_elec_s{simpl}_{clusters}.nc"),
|
||||
cop_profiles=resources("cop_profiles_elec_s{simpl}_{clusters}.nc"),
|
||||
existing_heating_distribution=resources(
|
||||
"existing_heating_distribution_elec_s{simpl}_{clusters}_{planning_horizons}.csv"
|
||||
),
|
||||
@ -69,6 +69,7 @@ rule add_brownfield:
|
||||
snapshots=config_provider("snapshots"),
|
||||
drop_leap_day=config_provider("enable", "drop_leap_day"),
|
||||
carriers=config_provider("electricity", "renewable_carriers"),
|
||||
heat_pump_sources=config_provider("sector", "heat_pump_sources"),
|
||||
input:
|
||||
unpack(input_profile_tech_brownfield),
|
||||
simplify_busmap=resources("busmap_elec_s{simpl}.csv"),
|
||||
@ -77,8 +78,7 @@ rule add_brownfield:
|
||||
+ "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc",
|
||||
network_p=solved_previous_horizon, #solved network at previous time step
|
||||
costs=resources("costs_{planning_horizons}.csv"),
|
||||
cop_soil_total=resources("cop_soil_total_elec_s{simpl}_{clusters}.nc"),
|
||||
cop_air_total=resources("cop_air_total_elec_s{simpl}_{clusters}.nc"),
|
||||
cop_profiles=resources("cop_profiles_elec_s{simpl}_{clusters}.nc"),
|
||||
output:
|
||||
RESULTS
|
||||
+ "prenetworks-brownfield/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc",
|
||||
|
@ -7,6 +7,7 @@ rule add_existing_baseyear:
|
||||
sector=config_provider("sector"),
|
||||
existing_capacities=config_provider("existing_capacities"),
|
||||
costs=config_provider("costs"),
|
||||
heat_pump_sources=config_provider("sector", "heat_pump_sources"),
|
||||
input:
|
||||
network=RESULTS
|
||||
+ "prenetworks/elec_s{simpl}_{clusters}_l{ll}_{opts}_{sector_opts}_{planning_horizons}.nc",
|
||||
@ -19,8 +20,7 @@ rule add_existing_baseyear:
|
||||
config_provider("scenario", "planning_horizons", 0)(w)
|
||||
)
|
||||
),
|
||||
cop_soil_total=resources("cop_soil_total_elec_s{simpl}_{clusters}.nc"),
|
||||
cop_air_total=resources("cop_air_total_elec_s{simpl}_{clusters}.nc"),
|
||||
cop_profiles=resources("cop_profiles_elec_s{simpl}_{clusters}.nc"),
|
||||
existing_heating_distribution=resources(
|
||||
"existing_heating_distribution_elec_s{simpl}_{clusters}_{planning_horizons}.csv"
|
||||
),
|
||||
|
@ -24,6 +24,10 @@ from _helpers import (
|
||||
from add_electricity import sanitize_carriers
|
||||
from prepare_sector_network import cluster_heat_buses, define_spatial, prepare_costs
|
||||
|
||||
from scripts.definitions.heat_sector import HeatSector
|
||||
from scripts.definitions.heat_system import HeatSystem
|
||||
from scripts.definitions.heat_system_type import HeatSystemType
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
cc = coco.CountryConverter()
|
||||
idx = pd.IndexSlice
|
||||
@ -416,14 +420,14 @@ def add_power_capacities_installed_before_baseyear(n, grouping_years, costs, bas
|
||||
|
||||
|
||||
def add_heating_capacities_installed_before_baseyear(
|
||||
n,
|
||||
baseyear,
|
||||
grouping_years,
|
||||
ashp_cop,
|
||||
gshp_cop,
|
||||
time_dep_hp_cop,
|
||||
costs,
|
||||
default_lifetime,
|
||||
n: pypsa.Network,
|
||||
baseyear: int,
|
||||
grouping_years: list,
|
||||
cop: dict,
|
||||
time_dep_hp_cop: bool,
|
||||
costs: pd.DataFrame,
|
||||
default_lifetime: int,
|
||||
existing_heating: pd.DataFrame,
|
||||
):
|
||||
"""
|
||||
Parameters
|
||||
@ -435,36 +439,30 @@ def add_heating_capacities_installed_before_baseyear(
|
||||
currently assumed heating capacities split between residential and
|
||||
services proportional to heating load in both 50% capacities
|
||||
in rural buses 50% in urban buses
|
||||
cop: xr.DataArray
|
||||
DataArray with time-dependent coefficients of performance (COPs) heat pumps. Coordinates are heat sources (see config), heat system types (see :file:`scripts/enums/HeatSystemType.py`), nodes and snapshots.
|
||||
time_dep_hp_cop: bool
|
||||
If True, time-dependent (dynamic) COPs are used for heat pumps
|
||||
"""
|
||||
logger.debug(f"Adding heating capacities installed before {baseyear}")
|
||||
|
||||
existing_heating = pd.read_csv(
|
||||
snakemake.input.existing_heating_distribution, header=[0, 1], index_col=0
|
||||
for heat_system in existing_heating.columns.get_level_values(0).unique():
|
||||
heat_system = HeatSystem(heat_system)
|
||||
|
||||
nodes = pd.Index(
|
||||
n.buses.location[n.buses.index.str.contains(f"{heat_system} heat")]
|
||||
)
|
||||
|
||||
for name in existing_heating.columns.get_level_values(0).unique():
|
||||
name_type = "central" if name == "urban central" else "decentral"
|
||||
|
||||
nodes = pd.Index(n.buses.location[n.buses.index.str.contains(f"{name} heat")])
|
||||
|
||||
if (name_type != "central") and options["electricity_distribution_grid"]:
|
||||
if (not heat_system == HeatSystem.URBAN_CENTRAL) and options[
|
||||
"electricity_distribution_grid"
|
||||
]:
|
||||
nodes_elec = nodes + " low voltage"
|
||||
else:
|
||||
nodes_elec = nodes
|
||||
|
||||
heat_pump_type = "air" if "urban" in name else "ground"
|
||||
|
||||
# Add heat pumps
|
||||
costs_name = f"decentral {heat_pump_type}-sourced heat pump"
|
||||
|
||||
cop = {"air": ashp_cop, "ground": gshp_cop}
|
||||
|
||||
if time_dep_hp_cop:
|
||||
efficiency = cop[heat_pump_type][nodes]
|
||||
else:
|
||||
efficiency = costs.at[costs_name, "efficiency"]
|
||||
|
||||
too_large_grouping_years = [gy for gy in grouping_years if gy >= int(baseyear)]
|
||||
too_large_grouping_years = [
|
||||
gy for gy in grouping_years if gy >= int(baseyear)
|
||||
]
|
||||
if too_large_grouping_years:
|
||||
logger.warning(
|
||||
f"Grouping years >= baseyear are ignored. Dropping {too_large_grouping_years}."
|
||||
@ -488,18 +486,37 @@ def add_heating_capacities_installed_before_baseyear(
|
||||
ratios = _years / _years.sum()
|
||||
|
||||
for ratio, grouping_year in zip(ratios, valid_grouping_years):
|
||||
# Add heat pumps
|
||||
for heat_source in snakemake.params.heat_pump_sources[
|
||||
heat_system.system_type.value
|
||||
]:
|
||||
costs_name = heat_system.heat_pump_costs_name(heat_source)
|
||||
|
||||
efficiency = (
|
||||
cop.sel(
|
||||
heat_system=heat_system.system_type.value,
|
||||
heat_source=heat_source,
|
||||
name=nodes,
|
||||
)
|
||||
.to_pandas()
|
||||
.reindex(index=n.snapshots)
|
||||
if time_dep_hp_cop
|
||||
else costs.at[costs_name, "efficiency"]
|
||||
)
|
||||
|
||||
n.madd(
|
||||
"Link",
|
||||
nodes,
|
||||
suffix=f" {name} {heat_pump_type} heat pump-{grouping_year}",
|
||||
suffix=f" {heat_system} {heat_source} heat pump-{grouping_year}",
|
||||
bus0=nodes_elec,
|
||||
bus1=nodes + " " + name + " heat",
|
||||
carrier=f"{name} {heat_pump_type} heat pump",
|
||||
bus1=nodes + " " + heat_system.value + " heat",
|
||||
carrier=f"{heat_system} {heat_source} heat pump",
|
||||
efficiency=efficiency,
|
||||
capital_cost=costs.at[costs_name, "efficiency"]
|
||||
* costs.at[costs_name, "fixed"],
|
||||
p_nom=existing_heating.loc[nodes, (name, f"{heat_pump_type} heat pump")]
|
||||
p_nom=existing_heating.loc[
|
||||
nodes, (heat_system.value, f"{heat_source} heat pump")
|
||||
]
|
||||
* ratio
|
||||
/ costs.at[costs_name, "efficiency"],
|
||||
build_year=int(grouping_year),
|
||||
@ -510,66 +527,70 @@ def add_heating_capacities_installed_before_baseyear(
|
||||
n.madd(
|
||||
"Link",
|
||||
nodes,
|
||||
suffix=f" {name} resistive heater-{grouping_year}",
|
||||
suffix=f" {heat_system} resistive heater-{grouping_year}",
|
||||
bus0=nodes_elec,
|
||||
bus1=nodes + " " + name + " heat",
|
||||
carrier=name + " resistive heater",
|
||||
efficiency=costs.at[f"{name_type} resistive heater", "efficiency"],
|
||||
bus1=nodes + " " + heat_system.value + " heat",
|
||||
carrier=heat_system.value + " resistive heater",
|
||||
efficiency=costs.at[
|
||||
heat_system.resistive_heater_costs_name, "efficiency"
|
||||
],
|
||||
capital_cost=(
|
||||
costs.at[f"{name_type} resistive heater", "efficiency"]
|
||||
* costs.at[f"{name_type} resistive heater", "fixed"]
|
||||
costs.at[heat_system.resistive_heater_costs_name, "efficiency"]
|
||||
* costs.at[heat_system.resistive_heater_costs_name, "fixed"]
|
||||
),
|
||||
p_nom=(
|
||||
existing_heating.loc[nodes, (name, "resistive heater")]
|
||||
existing_heating.loc[nodes, (heat_system.value, "resistive heater")]
|
||||
* ratio
|
||||
/ costs.at[f"{name_type} resistive heater", "efficiency"]
|
||||
/ costs.at[heat_system.resistive_heater_costs_name, "efficiency"]
|
||||
),
|
||||
build_year=int(grouping_year),
|
||||
lifetime=costs.at[f"{name_type} resistive heater", "lifetime"],
|
||||
lifetime=costs.at[heat_system.resistive_heater_costs_name, "lifetime"],
|
||||
)
|
||||
|
||||
n.madd(
|
||||
"Link",
|
||||
nodes,
|
||||
suffix=f" {name} gas boiler-{grouping_year}",
|
||||
suffix=f"{heat_system} gas boiler-{grouping_year}",
|
||||
bus0="EU gas" if "EU gas" in spatial.gas.nodes else nodes + " gas",
|
||||
bus1=nodes + " " + name + " heat",
|
||||
bus1=nodes + " " + heat_system.value + " heat",
|
||||
bus2="co2 atmosphere",
|
||||
carrier=name + " gas boiler",
|
||||
efficiency=costs.at[f"{name_type} gas boiler", "efficiency"],
|
||||
carrier=heat_system.value + " gas boiler",
|
||||
efficiency=costs.at[heat_system.gas_boiler_costs_name, "efficiency"],
|
||||
efficiency2=costs.at["gas", "CO2 intensity"],
|
||||
capital_cost=(
|
||||
costs.at[f"{name_type} gas boiler", "efficiency"]
|
||||
* costs.at[f"{name_type} gas boiler", "fixed"]
|
||||
costs.at[heat_system.gas_boiler_costs_name, "efficiency"]
|
||||
* costs.at[heat_system.gas_boiler_costs_name, "fixed"]
|
||||
),
|
||||
p_nom=(
|
||||
existing_heating.loc[nodes, (name, "gas boiler")]
|
||||
existing_heating.loc[nodes, (heat_system.value, "gas boiler")]
|
||||
* ratio
|
||||
/ costs.at[f"{name_type} gas boiler", "efficiency"]
|
||||
/ costs.at[heat_system.gas_boiler_costs_name, "efficiency"]
|
||||
),
|
||||
build_year=int(grouping_year),
|
||||
lifetime=costs.at[f"{name_type} gas boiler", "lifetime"],
|
||||
lifetime=costs.at[heat_system.gas_boiler_costs_name, "lifetime"],
|
||||
)
|
||||
|
||||
n.madd(
|
||||
"Link",
|
||||
nodes,
|
||||
suffix=f" {name} oil boiler-{grouping_year}",
|
||||
suffix=f" {heat_system} oil boiler-{grouping_year}",
|
||||
bus0=spatial.oil.nodes,
|
||||
bus1=nodes + " " + name + " heat",
|
||||
bus1=nodes + " " + heat_system.value + " heat",
|
||||
bus2="co2 atmosphere",
|
||||
carrier=name + " oil boiler",
|
||||
efficiency=costs.at["decentral oil boiler", "efficiency"],
|
||||
carrier=heat_system.value + " oil boiler",
|
||||
efficiency=costs.at[heat_system.oil_boiler_costs_name, "efficiency"],
|
||||
efficiency2=costs.at["oil", "CO2 intensity"],
|
||||
capital_cost=costs.at["decentral oil boiler", "efficiency"]
|
||||
* costs.at["decentral oil boiler", "fixed"],
|
||||
capital_cost=costs.at[heat_system.oil_boiler_costs_name, "efficiency"]
|
||||
* costs.at[heat_system.oil_boiler_costs_name, "fixed"],
|
||||
p_nom=(
|
||||
existing_heating.loc[nodes, (name, "oil boiler")]
|
||||
existing_heating.loc[nodes, (heat_system.value, "oil boiler")]
|
||||
* ratio
|
||||
/ costs.at["decentral oil boiler", "efficiency"]
|
||||
/ costs.at[heat_system.oil_boiler_costs_name, "efficiency"]
|
||||
),
|
||||
build_year=int(grouping_year),
|
||||
lifetime=costs.at[f"{name_type} gas boiler", "lifetime"],
|
||||
lifetime=costs.at[
|
||||
f"{heat_system.central_or_decentral} gas boiler", "lifetime"
|
||||
],
|
||||
)
|
||||
|
||||
# delete links with p_nom=nan corresponding to extra nodes in country
|
||||
@ -639,29 +660,22 @@ if __name__ == "__main__":
|
||||
)
|
||||
|
||||
if options["heating"]:
|
||||
time_dep_hp_cop = options["time_dep_hp_cop"]
|
||||
ashp_cop = (
|
||||
xr.open_dataarray(snakemake.input.cop_air_total)
|
||||
.to_pandas()
|
||||
.reindex(index=n.snapshots)
|
||||
)
|
||||
gshp_cop = (
|
||||
xr.open_dataarray(snakemake.input.cop_soil_total)
|
||||
.to_pandas()
|
||||
.reindex(index=n.snapshots)
|
||||
)
|
||||
default_lifetime = snakemake.params.existing_capacities[
|
||||
"default_heating_lifetime"
|
||||
]
|
||||
|
||||
add_heating_capacities_installed_before_baseyear(
|
||||
n,
|
||||
baseyear,
|
||||
grouping_years_heat,
|
||||
ashp_cop,
|
||||
gshp_cop,
|
||||
time_dep_hp_cop,
|
||||
costs,
|
||||
default_lifetime,
|
||||
n=n,
|
||||
baseyear=baseyear,
|
||||
grouping_years=grouping_years_heat,
|
||||
cop=xr.open_dataarray(snakemake.input.cop_profiles),
|
||||
time_dep_hp_cop=options["time_dep_hp_cop"],
|
||||
costs=costs,
|
||||
default_lifetime=snakemake.params.existing_capacities[
|
||||
"default_heating_lifetime"
|
||||
],
|
||||
existing_heating=pd.read_csv(
|
||||
snakemake.input.existing_heating_distribution,
|
||||
header=[0, 1],
|
||||
index_col=0,
|
||||
),
|
||||
)
|
||||
|
||||
if options.get("cluster_heat_buses", False):
|
||||
|
@ -1,69 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-FileCopyrightText: : 2020-2024 The PyPSA-Eur Authors
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
"""
|
||||
Build coefficient of performance (COP) time series for air- or ground-sourced
|
||||
heat pumps.
|
||||
|
||||
The COP is approximated as a quatratic function of the temperature difference between source and
|
||||
sink, based on Staffell et al. 2012.
|
||||
|
||||
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_air_total_elec_s<simpl>_<clusters>.nc``: Ambient air temperature (total) time series.
|
||||
|
||||
Outputs:
|
||||
--------
|
||||
- ``resources/cop_soil_total_elec_s<simpl>_<clusters>.nc``: COP (ground-sourced) time series (total).
|
||||
- ``resources/cop_air_total_elec_s<simpl>_<clusters>.nc``: COP (air-sourced) time series (total).
|
||||
|
||||
|
||||
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
|
||||
from _helpers import set_scenario_config
|
||||
|
||||
|
||||
def coefficient_of_performance(delta_T, source="air"):
|
||||
if source == "air":
|
||||
return 6.81 - 0.121 * delta_T + 0.000630 * delta_T**2
|
||||
elif source == "soil":
|
||||
return 8.77 - 0.150 * delta_T + 0.000734 * delta_T**2
|
||||
else:
|
||||
raise NotImplementedError("'source' must be one of ['air', 'soil']")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if "snakemake" not in globals():
|
||||
from _helpers import mock_snakemake
|
||||
|
||||
snakemake = mock_snakemake(
|
||||
"build_cop_profiles",
|
||||
simpl="",
|
||||
clusters=48,
|
||||
)
|
||||
|
||||
set_scenario_config(snakemake)
|
||||
|
||||
for source in ["air", "soil"]:
|
||||
source_T = xr.open_dataarray(snakemake.input[f"temp_{source}_total"])
|
||||
|
||||
delta_T = snakemake.params.heat_pump_sink_T - source_T
|
||||
|
||||
cop = coefficient_of_performance(delta_T, source)
|
||||
|
||||
cop.to_netcdf(snakemake.output[f"cop_{source}_total"])
|
111
scripts/build_cop_profiles/BaseCopApproximator.py
Normal file
111
scripts/build_cop_profiles/BaseCopApproximator.py
Normal file
@ -0,0 +1,111 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-FileCopyrightText: : 2020-2024 The PyPSA-Eur Authors
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Union
|
||||
|
||||
import numpy as np
|
||||
import xarray as xr
|
||||
|
||||
|
||||
class BaseCopApproximator(ABC):
|
||||
"""
|
||||
Abstract class for approximating the coefficient of performance (COP) of a
|
||||
heat pump.
|
||||
|
||||
Attributes:
|
||||
----------
|
||||
forward_temperature_celsius : Union[xr.DataArray, np.array]
|
||||
The forward temperature in Celsius.
|
||||
source_inlet_temperature_celsius : Union[xr.DataArray, np.array]
|
||||
The source inlet temperature in Celsius.
|
||||
|
||||
Methods:
|
||||
-------
|
||||
__init__(self, forward_temperature_celsius, source_inlet_temperature_celsius)
|
||||
Initialize CopApproximator.
|
||||
approximate_cop(self)
|
||||
Approximate heat pump coefficient of performance (COP).
|
||||
celsius_to_kelvin(t_celsius)
|
||||
Convert temperature from Celsius to Kelvin.
|
||||
logarithmic_mean(t_hot, t_cold)
|
||||
Calculate the logarithmic mean temperature difference.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
forward_temperature_celsius: Union[xr.DataArray, np.array],
|
||||
source_inlet_temperature_celsius: Union[xr.DataArray, np.array],
|
||||
):
|
||||
"""
|
||||
Initialize CopApproximator.
|
||||
|
||||
Parameters:
|
||||
----------
|
||||
forward_temperature_celsius : Union[xr.DataArray, np.array]
|
||||
The forward temperature in Celsius.
|
||||
source_inlet_temperature_celsius : Union[xr.DataArray, np.array]
|
||||
The source inlet temperature in Celsius.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def approximate_cop(self) -> Union[xr.DataArray, np.array]:
|
||||
"""
|
||||
Approximate heat pump coefficient of performance (COP).
|
||||
|
||||
Returns:
|
||||
-------
|
||||
Union[xr.DataArray, np.array]
|
||||
The calculated COP values.
|
||||
"""
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def celsius_to_kelvin(
|
||||
t_celsius: Union[float, xr.DataArray, np.array]
|
||||
) -> Union[float, xr.DataArray, np.array]:
|
||||
"""
|
||||
Convert temperature from Celsius to Kelvin.
|
||||
|
||||
Parameters:
|
||||
----------
|
||||
t_celsius : Union[float, xr.DataArray, np.array]
|
||||
Temperature in Celsius.
|
||||
|
||||
Returns:
|
||||
-------
|
||||
Union[float, xr.DataArray, np.array]
|
||||
Temperature in Kelvin.
|
||||
"""
|
||||
if (np.asarray(t_celsius) > 200).any():
|
||||
raise ValueError(
|
||||
"t_celsius > 200. Are you sure you are using the right units?"
|
||||
)
|
||||
return t_celsius + 273.15
|
||||
|
||||
@staticmethod
|
||||
def logarithmic_mean(
|
||||
t_hot: Union[float, xr.DataArray, np.ndarray],
|
||||
t_cold: Union[float, xr.DataArray, np.ndarray],
|
||||
) -> Union[float, xr.DataArray, np.ndarray]:
|
||||
"""
|
||||
Calculate the logarithmic mean temperature difference.
|
||||
|
||||
Parameters:
|
||||
----------
|
||||
t_hot : Union[float, xr.DataArray, np.ndarray]
|
||||
Hot temperature.
|
||||
t_cold : Union[float, xr.DataArray, np.ndarray]
|
||||
Cold temperature.
|
||||
|
||||
Returns:
|
||||
-------
|
||||
Union[float, xr.DataArray, np.ndarray]
|
||||
Logarithmic mean temperature difference.
|
||||
"""
|
||||
if (np.asarray(t_hot <= t_cold)).any():
|
||||
raise ValueError("t_hot must be greater than t_cold")
|
||||
return (t_hot - t_cold) / np.log(t_hot / t_cold)
|
392
scripts/build_cop_profiles/CentralHeatingCopApproximator.py
Normal file
392
scripts/build_cop_profiles/CentralHeatingCopApproximator.py
Normal file
@ -0,0 +1,392 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-FileCopyrightText: : 2020-2024 The PyPSA-Eur Authors
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
|
||||
from typing import Union
|
||||
|
||||
import numpy as np
|
||||
import xarray as xr
|
||||
from BaseCopApproximator import BaseCopApproximator
|
||||
|
||||
|
||||
class CentralHeatingCopApproximator(BaseCopApproximator):
|
||||
"""
|
||||
Approximate the coefficient of performance (COP) for a heat pump in a
|
||||
central heating system (district heating).
|
||||
|
||||
Uses an approximation method proposed by Jensen et al. (2018) and
|
||||
default parameters from Pieper et al. (2020). The method is based on
|
||||
a thermodynamic heat pump model with some hard-to-know parameters
|
||||
being approximated.
|
||||
|
||||
Attributes:
|
||||
----------
|
||||
forward_temperature_celsius : Union[xr.DataArray, np.array]
|
||||
The forward temperature in Celsius.
|
||||
return_temperature_celsius : Union[xr.DataArray, np.array]
|
||||
The return temperature in Celsius.
|
||||
source_inlet_temperature_celsius : Union[xr.DataArray, np.array]
|
||||
The source inlet temperature in Celsius.
|
||||
source_outlet_temperature_celsius : Union[xr.DataArray, np.array]
|
||||
The source outlet temperature in Celsius.
|
||||
delta_t_pinch_point : float, optional
|
||||
The pinch point temperature difference, by default 5.
|
||||
isentropic_compressor_efficiency : float, optional
|
||||
The isentropic compressor efficiency, by default 0.8.
|
||||
heat_loss : float, optional
|
||||
The heat loss, by default 0.0.
|
||||
|
||||
Methods:
|
||||
-------
|
||||
__init__(
|
||||
forward_temperature_celsius: Union[xr.DataArray, np.array],
|
||||
source_inlet_temperature_celsius: Union[xr.DataArray, np.array],
|
||||
return_temperature_celsius: Union[xr.DataArray, np.array],
|
||||
source_outlet_temperature_celsius: Union[xr.DataArray, np.array],
|
||||
delta_t_pinch_point: float = 5,
|
||||
isentropic_compressor_efficiency: float = 0.8,
|
||||
heat_loss: float = 0.0,
|
||||
) -> None:
|
||||
Initializes the CentralHeatingCopApproximator object.
|
||||
|
||||
approximate_cop(self) -> Union[xr.DataArray, np.array]:
|
||||
Calculate the coefficient of performance (COP) for the system.
|
||||
|
||||
_approximate_delta_t_refrigerant_source(
|
||||
self, delta_t_source: Union[xr.DataArray, np.array]
|
||||
) -> Union[xr.DataArray, np.array]:
|
||||
Approximates the temperature difference between the refrigerant and the source.
|
||||
|
||||
_approximate_delta_t_refrigerant_sink(
|
||||
self,
|
||||
refrigerant: str = "ammonia",
|
||||
a: float = {"ammonia": 0.2, "isobutane": -0.0011},
|
||||
b: float = {"ammonia": 0.2, "isobutane": 0.3},
|
||||
c: float = {"ammonia": 0.016, "isobutane": 2.4},
|
||||
) -> Union[xr.DataArray, np.array]:
|
||||
Approximates the temperature difference between the refrigerant and heat sink.
|
||||
|
||||
_ratio_evaporation_compression_work_approximation(
|
||||
self,
|
||||
refrigerant: str = "ammonia",
|
||||
a: float = {"ammonia": 0.0014, "isobutane": 0.0035},
|
||||
) -> Union[xr.DataArray, np.array]:
|
||||
Calculate the ratio of evaporation to compression work based on approximation.
|
||||
|
||||
_approximate_delta_t_refrigerant_sink(
|
||||
self,
|
||||
refrigerant: str = "ammonia",
|
||||
a: float = {"ammonia": 0.2, "isobutane": -0.0011},
|
||||
b: float = {"ammonia": 0.2, "isobutane": 0.3},
|
||||
c: float = {"ammonia": 0.016, "isobutane": 2.4},
|
||||
) -> Union[xr.DataArray, np.array]:
|
||||
Approximates the temperature difference between the refrigerant and heat sink.
|
||||
|
||||
_ratio_evaporation_compression_work_approximation(
|
||||
self,
|
||||
refrigerant: str = "ammonia",
|
||||
a: float = {"ammonia": 0.0014, "isobutane": 0.0035},
|
||||
) -> Union[xr.DataArray, np.array]:
|
||||
Calculate the ratio of evaporation to compression work based on approximation.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
forward_temperature_celsius: Union[xr.DataArray, np.array],
|
||||
source_inlet_temperature_celsius: Union[xr.DataArray, np.array],
|
||||
return_temperature_celsius: Union[xr.DataArray, np.array],
|
||||
source_outlet_temperature_celsius: Union[xr.DataArray, np.array],
|
||||
delta_t_pinch_point: float = 5,
|
||||
isentropic_compressor_efficiency: float = 0.8,
|
||||
heat_loss: float = 0.0,
|
||||
) -> None:
|
||||
"""
|
||||
Initializes the CentralHeatingCopApproximator object.
|
||||
|
||||
Parameters:
|
||||
----------
|
||||
forward_temperature_celsius : Union[xr.DataArray, np.array]
|
||||
The forward temperature in Celsius.
|
||||
return_temperature_celsius : Union[xr.DataArray, np.array]
|
||||
The return temperature in Celsius.
|
||||
source_inlet_temperature_celsius : Union[xr.DataArray, np.array]
|
||||
The source inlet temperature in Celsius.
|
||||
source_outlet_temperature_celsius : Union[xr.DataArray, np.array]
|
||||
The source outlet temperature in Celsius.
|
||||
delta_t_pinch_point : float, optional
|
||||
The pinch point temperature difference, by default 5.
|
||||
isentropic_compressor_efficiency : float, optional
|
||||
The isentropic compressor efficiency, by default 0.8.
|
||||
heat_loss : float, optional
|
||||
The heat loss, by default 0.0.
|
||||
"""
|
||||
self.t_source_in_kelvin = BaseCopApproximator.celsius_to_kelvin(
|
||||
source_inlet_temperature_celsius
|
||||
)
|
||||
self.t_sink_out_kelvin = BaseCopApproximator.celsius_to_kelvin(
|
||||
forward_temperature_celsius
|
||||
)
|
||||
|
||||
self.t_sink_in_kelvin = BaseCopApproximator.celsius_to_kelvin(
|
||||
return_temperature_celsius
|
||||
)
|
||||
self.t_source_out = BaseCopApproximator.celsius_to_kelvin(
|
||||
source_outlet_temperature_celsius
|
||||
)
|
||||
|
||||
self.isentropic_efficiency_compressor_kelvin = isentropic_compressor_efficiency
|
||||
self.heat_loss = heat_loss
|
||||
self.delta_t_pinch = delta_t_pinch_point
|
||||
|
||||
def approximate_cop(self) -> Union[xr.DataArray, np.array]:
|
||||
"""
|
||||
Calculate the coefficient of performance (COP) for the system.
|
||||
|
||||
Returns:
|
||||
--------
|
||||
Union[xr.DataArray, np.array]: The calculated COP values.
|
||||
"""
|
||||
return (
|
||||
self.ideal_lorenz_cop
|
||||
* (
|
||||
(
|
||||
1
|
||||
+ (self.delta_t_refrigerant_sink + self.delta_t_pinch)
|
||||
/ self.t_sink_mean_kelvin
|
||||
)
|
||||
/ (
|
||||
1
|
||||
+ (
|
||||
self.delta_t_refrigerant_sink
|
||||
+ self.delta_t_refrigerant_source
|
||||
+ 2 * self.delta_t_pinch
|
||||
)
|
||||
/ self.delta_t_lift
|
||||
)
|
||||
)
|
||||
* self.isentropic_efficiency_compressor_kelvin
|
||||
* (1 - self.ratio_evaporation_compression_work)
|
||||
+ 1
|
||||
- self.isentropic_efficiency_compressor_kelvin
|
||||
- self.heat_loss
|
||||
)
|
||||
|
||||
@property
|
||||
def t_sink_mean_kelvin(self) -> Union[xr.DataArray, np.array]:
|
||||
"""
|
||||
Calculate the logarithmic mean temperature difference between the cold
|
||||
and hot sinks.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Union[xr.DataArray, np.array]
|
||||
The mean temperature difference.
|
||||
"""
|
||||
return BaseCopApproximator.logarithmic_mean(
|
||||
t_cold=self.t_sink_in_kelvin, t_hot=self.t_sink_out_kelvin
|
||||
)
|
||||
|
||||
@property
|
||||
def t_source_mean_kelvin(self) -> Union[xr.DataArray, np.array]:
|
||||
"""
|
||||
Calculate the logarithmic mean temperature of the heat source.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Union[xr.DataArray, np.array]
|
||||
The mean temperature of the heat source.
|
||||
"""
|
||||
return BaseCopApproximator.logarithmic_mean(
|
||||
t_hot=self.t_source_in_kelvin, t_cold=self.t_source_out
|
||||
)
|
||||
|
||||
@property
|
||||
def delta_t_lift(self) -> Union[xr.DataArray, np.array]:
|
||||
"""
|
||||
Calculate the temperature lift as the difference between the
|
||||
logarithmic sink and source temperatures.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Union[xr.DataArray, np.array]
|
||||
The temperature difference between the sink and source.
|
||||
"""
|
||||
return self.t_sink_mean_kelvin - self.t_source_mean_kelvin
|
||||
|
||||
@property
|
||||
def ideal_lorenz_cop(self) -> Union[xr.DataArray, np.array]:
|
||||
"""
|
||||
Ideal Lorenz coefficient of performance (COP).
|
||||
|
||||
The ideal Lorenz COP is calculated as the ratio of the mean sink temperature
|
||||
to the lift temperature difference.
|
||||
|
||||
Returns
|
||||
-------
|
||||
np.array
|
||||
The ideal Lorenz COP.
|
||||
"""
|
||||
return self.t_sink_mean_kelvin / self.delta_t_lift
|
||||
|
||||
@property
|
||||
def delta_t_refrigerant_source(self) -> Union[xr.DataArray, np.array]:
|
||||
"""
|
||||
Calculate the temperature difference between the refrigerant source
|
||||
inlet and outlet.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Union[xr.DataArray, np.array]
|
||||
The temperature difference between the refrigerant source inlet and outlet.
|
||||
"""
|
||||
return self._approximate_delta_t_refrigerant_source(
|
||||
delta_t_source=self.t_source_in_kelvin - self.t_source_out
|
||||
)
|
||||
|
||||
@property
|
||||
def delta_t_refrigerant_sink(self) -> Union[xr.DataArray, np.array]:
|
||||
"""
|
||||
Temperature difference between the refrigerant and the sink based on
|
||||
approximation.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Union[xr.DataArray, np.array]
|
||||
The temperature difference between the refrigerant and the sink.
|
||||
"""
|
||||
return self._approximate_delta_t_refrigerant_sink()
|
||||
|
||||
@property
|
||||
def ratio_evaporation_compression_work(self) -> Union[xr.DataArray, np.array]:
|
||||
"""
|
||||
Calculate the ratio of evaporation to compression work based on
|
||||
approximation.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Union[xr.DataArray, np.array]
|
||||
The calculated ratio of evaporation to compression work.
|
||||
"""
|
||||
return self._ratio_evaporation_compression_work_approximation()
|
||||
|
||||
@property
|
||||
def delta_t_sink(self) -> Union[xr.DataArray, np.array]:
|
||||
"""
|
||||
Calculate the temperature difference at the sink.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Union[xr.DataArray, np.array]
|
||||
The temperature difference at the sink.
|
||||
"""
|
||||
return self.t_sink_out_kelvin - self.t_sink_in_kelvin
|
||||
|
||||
def _approximate_delta_t_refrigerant_source(
|
||||
self, delta_t_source: Union[xr.DataArray, np.array]
|
||||
) -> Union[xr.DataArray, np.array]:
|
||||
"""
|
||||
Approximates the temperature difference between the refrigerant and the
|
||||
source.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
delta_t_source : Union[xr.DataArray, np.array]
|
||||
The temperature difference for the refrigerant source.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Union[xr.DataArray, np.array]
|
||||
The approximate temperature difference between the refrigerant and heat source.
|
||||
"""
|
||||
return delta_t_source / 2
|
||||
|
||||
def _approximate_delta_t_refrigerant_sink(
|
||||
self,
|
||||
refrigerant: str = "ammonia",
|
||||
a: float = {"ammonia": 0.2, "isobutane": -0.0011},
|
||||
b: float = {"ammonia": 0.2, "isobutane": 0.3},
|
||||
c: float = {"ammonia": 0.016, "isobutane": 2.4},
|
||||
) -> Union[xr.DataArray, np.array]:
|
||||
"""
|
||||
Approximates the temperature difference between the refrigerant and
|
||||
heat sink.
|
||||
|
||||
Parameters:
|
||||
----------
|
||||
refrigerant : str, optional
|
||||
The refrigerant used in the system. Either 'isobutane' or 'ammonia. Default is 'ammonia'.
|
||||
a : float, optional
|
||||
Coefficient for the temperature difference between the sink and source, default is 0.2.
|
||||
b : float, optional
|
||||
Coefficient for the temperature difference at the sink, default is 0.2.
|
||||
c : float, optional
|
||||
Constant term, default is 0.016.
|
||||
|
||||
Returns:
|
||||
-------
|
||||
Union[xr.DataArray, np.array]
|
||||
The approximate temperature difference between the refrigerant and heat sink.
|
||||
|
||||
Notes:
|
||||
------
|
||||
This function assumes ammonia as the refrigerant.
|
||||
|
||||
The approximate temperature difference at the refrigerant sink is calculated using the following formula:
|
||||
a * (t_sink_out - t_source_out + 2 * delta_t_pinch) + b * delta_t_sink + c
|
||||
"""
|
||||
if refrigerant not in a.keys():
|
||||
raise ValueError(
|
||||
f"Invalid refrigerant '{refrigerant}'. Must be one of {a.keys()}"
|
||||
)
|
||||
return (
|
||||
a[refrigerant]
|
||||
* (self.t_sink_out_kelvin - self.t_source_out + 2 * self.delta_t_pinch)
|
||||
+ b[refrigerant] * self.delta_t_sink
|
||||
+ c[refrigerant]
|
||||
)
|
||||
|
||||
def _ratio_evaporation_compression_work_approximation(
|
||||
self,
|
||||
refrigerant: str = "ammonia",
|
||||
a: float = {"ammonia": 0.0014, "isobutane": 0.0035},
|
||||
b: float = {"ammonia": -0.0015, "isobutane": -0.0033},
|
||||
c: float = {"ammonia": 0.039, "isobutane": 0.053},
|
||||
) -> Union[xr.DataArray, np.array]:
|
||||
"""
|
||||
Calculate the ratio of evaporation to compression work approximation.
|
||||
|
||||
Parameters:
|
||||
----------
|
||||
refrigerant : str, optional
|
||||
The refrigerant used in the system. Either 'isobutane' or 'ammonia. Default is 'ammonia'.
|
||||
a : float, optional
|
||||
Coefficient 'a' in the approximation equation. Default is 0.0014.
|
||||
b : float, optional
|
||||
Coefficient 'b' in the approximation equation. Default is -0.0015.
|
||||
c : float, optional
|
||||
Coefficient 'c' in the approximation equation. Default is 0.039.
|
||||
|
||||
Returns:
|
||||
-------
|
||||
Union[xr.DataArray, np.array]
|
||||
The approximated ratio of evaporation to compression work.
|
||||
|
||||
Notes:
|
||||
------
|
||||
This function assumes ammonia as the refrigerant.
|
||||
|
||||
The approximation equation used is:
|
||||
ratio = a * (t_sink_out - t_source_out + 2 * delta_t_pinch) + b * delta_t_sink + c
|
||||
"""
|
||||
if refrigerant not in a.keys():
|
||||
raise ValueError(
|
||||
f"Invalid refrigerant '{refrigerant}'. Must be one of {a.keys()}"
|
||||
)
|
||||
return (
|
||||
a[refrigerant]
|
||||
* (self.t_sink_out_kelvin - self.t_source_out + 2 * self.delta_t_pinch)
|
||||
+ b[refrigerant] * self.delta_t_sink
|
||||
+ c[refrigerant]
|
||||
)
|
110
scripts/build_cop_profiles/DecentralHeatingCopApproximator.py
Normal file
110
scripts/build_cop_profiles/DecentralHeatingCopApproximator.py
Normal file
@ -0,0 +1,110 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-FileCopyrightText: : 2020-2024 The PyPSA-Eur Authors
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
|
||||
from typing import Union
|
||||
|
||||
import numpy as np
|
||||
import xarray as xr
|
||||
from BaseCopApproximator import BaseCopApproximator
|
||||
|
||||
|
||||
class DecentralHeatingCopApproximator(BaseCopApproximator):
|
||||
"""
|
||||
Approximate the coefficient of performance (COP) for a heat pump in a
|
||||
decentral heating system (individual/household heating).
|
||||
|
||||
Uses a quadratic regression on the temperature difference between the source and sink based on empirical data proposed by Staffell et al. 2012.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
forward_temperature_celsius : Union[xr.DataArray, np.array]
|
||||
The forward temperature in Celsius.
|
||||
source_inlet_temperature_celsius : Union[xr.DataArray, np.array]
|
||||
The source inlet temperature in Celsius.
|
||||
source_type : str
|
||||
The source of the heat pump. Must be either 'air' or 'ground'.
|
||||
|
||||
Methods
|
||||
-------
|
||||
__init__(forward_temperature_celsius, source_inlet_temperature_celsius, source_type)
|
||||
Initialize the DecentralHeatingCopApproximator object.
|
||||
approximate_cop()
|
||||
Compute the COP values using quadratic regression for air-/ground-source heat pumps.
|
||||
_approximate_cop_air_source()
|
||||
Evaluate quadratic regression for an air-sourced heat pump.
|
||||
_approximate_cop_ground_source()
|
||||
Evaluate quadratic regression for a ground-sourced heat pump.
|
||||
|
||||
References
|
||||
----------
|
||||
[1] Staffell et al., Energy & Environmental Science 11 (2012): A review of domestic heat pumps, https://doi.org/10.1039/C2EE22653G.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
forward_temperature_celsius: Union[xr.DataArray, np.array],
|
||||
source_inlet_temperature_celsius: Union[xr.DataArray, np.array],
|
||||
source_type: str,
|
||||
):
|
||||
"""
|
||||
Initialize the DecentralHeatingCopApproximator object.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
forward_temperature_celsius : Union[xr.DataArray, np.array]
|
||||
The forward temperature in Celsius.
|
||||
source_inlet_temperature_celsius : Union[xr.DataArray, np.array]
|
||||
The source inlet temperature in Celsius.
|
||||
source_type : str
|
||||
The source of the heat pump. Must be either 'air' or 'ground'.
|
||||
"""
|
||||
|
||||
self.delta_t = forward_temperature_celsius - source_inlet_temperature_celsius
|
||||
if source_type not in ["air", "ground"]:
|
||||
raise ValueError("'source_type' must be one of ['air', 'ground']")
|
||||
else:
|
||||
self.source_type = source_type
|
||||
|
||||
def approximate_cop(self) -> Union[xr.DataArray, np.array]:
|
||||
"""
|
||||
Compute the COP values using quadratic regression for air-/ground-
|
||||
source heat pumps.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Union[xr.DataArray, np.array]
|
||||
The calculated COP values.
|
||||
"""
|
||||
if self.source_type == "air":
|
||||
return self._approximate_cop_air_source()
|
||||
elif self.source_type == "ground":
|
||||
return self._approximate_cop_ground_source()
|
||||
|
||||
def _approximate_cop_air_source(self) -> Union[xr.DataArray, np.array]:
|
||||
"""
|
||||
Evaluate quadratic regression for an air-sourced heat pump.
|
||||
|
||||
COP = 6.81 - 0.121 * delta_T + 0.000630 * delta_T^2
|
||||
|
||||
Returns
|
||||
-------
|
||||
Union[xr.DataArray, np.array]
|
||||
The calculated COP values.
|
||||
"""
|
||||
return 6.81 - 0.121 * self.delta_t + 0.000630 * self.delta_t**2
|
||||
|
||||
def _approximate_cop_ground_source(self) -> Union[xr.DataArray, np.array]:
|
||||
"""
|
||||
Evaluate quadratic regression for a ground-sourced heat pump.
|
||||
|
||||
COP = 8.77 - 0.150 * delta_T + 0.000734 * delta_T^2
|
||||
|
||||
Returns
|
||||
-------
|
||||
Union[xr.DataArray, np.array]
|
||||
The calculated COP values.
|
||||
"""
|
||||
return 8.77 - 0.150 * self.delta_t + 0.000734 * self.delta_t**2
|
95
scripts/build_cop_profiles/run.py
Normal file
95
scripts/build_cop_profiles/run.py
Normal file
@ -0,0 +1,95 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-FileCopyrightText: : 2020-2024 The PyPSA-Eur Authors
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import xarray as xr
|
||||
from _helpers import set_scenario_config
|
||||
from CentralHeatingCopApproximator import CentralHeatingCopApproximator
|
||||
from DecentralHeatingCopApproximator import DecentralHeatingCopApproximator
|
||||
|
||||
from scripts.definitions.heat_system_type import HeatSystemType
|
||||
|
||||
sys.path.append("..")
|
||||
|
||||
|
||||
def get_cop(
|
||||
heat_system_type: str,
|
||||
heat_source: str,
|
||||
source_inlet_temperature_celsius: xr.DataArray,
|
||||
) -> xr.DataArray:
|
||||
"""
|
||||
Calculate the coefficient of performance (COP) for a heating system.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
heat_system_type : str
|
||||
The type of heating system.
|
||||
heat_source : str
|
||||
The heat source used in the heating system.
|
||||
source_inlet_temperature_celsius : xr.DataArray
|
||||
The inlet temperature of the heat source in Celsius.
|
||||
|
||||
Returns
|
||||
-------
|
||||
xr.DataArray
|
||||
The calculated coefficient of performance (COP) for the heating system.
|
||||
"""
|
||||
if HeatSystemType(heat_system_type).is_central:
|
||||
return CentralHeatingCopApproximator(
|
||||
forward_temperature_celsius=snakemake.params.forward_temperature_central_heating,
|
||||
return_temperature_celsius=snakemake.params.return_temperature_central_heating,
|
||||
source_inlet_temperature_celsius=source_inlet_temperature_celsius,
|
||||
source_outlet_temperature_celsius=source_inlet_temperature_celsius
|
||||
- snakemake.params.heat_source_cooling_central_heating,
|
||||
).approximate_cop()
|
||||
|
||||
else:
|
||||
return DecentralHeatingCopApproximator(
|
||||
forward_temperature_celsius=snakemake.params.heat_pump_sink_T_decentral_heating,
|
||||
source_inlet_temperature_celsius=source_inlet_temperature_celsius,
|
||||
source_type=heat_source,
|
||||
).approximate_cop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if "snakemake" not in globals():
|
||||
from _helpers import mock_snakemake
|
||||
|
||||
snakemake = mock_snakemake(
|
||||
"build_cop_profiles",
|
||||
simpl="",
|
||||
clusters=48,
|
||||
)
|
||||
|
||||
set_scenario_config(snakemake)
|
||||
|
||||
cop_all_system_types = []
|
||||
for heat_system_type, heat_sources in snakemake.params.heat_pump_sources.items():
|
||||
cop_this_system_type = []
|
||||
for heat_source in heat_sources:
|
||||
source_inlet_temperature_celsius = xr.open_dataarray(
|
||||
snakemake.input[f"temp_{heat_source.replace('ground', 'soil')}_total"]
|
||||
)
|
||||
cop_da = get_cop(
|
||||
heat_system_type=heat_system_type,
|
||||
heat_source=heat_source,
|
||||
source_inlet_temperature_celsius=source_inlet_temperature_celsius,
|
||||
)
|
||||
cop_this_system_type.append(cop_da)
|
||||
cop_all_system_types.append(
|
||||
xr.concat(
|
||||
cop_this_system_type, dim=pd.Index(heat_sources, name="heat_source")
|
||||
)
|
||||
)
|
||||
|
||||
cop_dataarray = xr.concat(
|
||||
cop_all_system_types,
|
||||
dim=pd.Index(snakemake.params.heat_pump_sources.keys(), name="heat_system"),
|
||||
)
|
||||
|
||||
cop_dataarray.to_netcdf(snakemake.output.cop_profiles)
|
28
scripts/definitions/heat_sector.py
Normal file
28
scripts/definitions/heat_sector.py
Normal file
@ -0,0 +1,28 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-FileCopyrightText: : 2020-2024 The PyPSA-Eur Authors
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class HeatSector(Enum):
|
||||
"""
|
||||
Enumeration class representing different heat sectors.
|
||||
|
||||
Attributes:
|
||||
RESIDENTIAL (str): Represents the residential heat sector.
|
||||
SERVICES (str): Represents the services heat sector.
|
||||
"""
|
||||
|
||||
RESIDENTIAL = "residential"
|
||||
SERVICES = "services"
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""
|
||||
Returns the string representation of the heat sector.
|
||||
|
||||
Returns:
|
||||
str: The string representation of the heat sector.
|
||||
"""
|
||||
return self.value
|
267
scripts/definitions/heat_system.py
Normal file
267
scripts/definitions/heat_system.py
Normal file
@ -0,0 +1,267 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-FileCopyrightText: : 2020-2024 The PyPSA-Eur Authors
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
from enum import Enum
|
||||
|
||||
from scripts.definitions.heat_sector import HeatSector
|
||||
from scripts.definitions.heat_system_type import HeatSystemType
|
||||
|
||||
|
||||
class HeatSystem(Enum):
|
||||
"""
|
||||
Enumeration representing different heat systems.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
RESIDENTIAL_RURAL : str
|
||||
Heat system for residential areas in rural locations.
|
||||
SERVICES_RURAL : str
|
||||
Heat system for service areas in rural locations.
|
||||
RESIDENTIAL_URBAN_DECENTRAL : str
|
||||
Heat system for residential areas in urban decentralized locations.
|
||||
SERVICES_URBAN_DECENTRAL : str
|
||||
Heat system for service areas in urban decentralized locations.
|
||||
URBAN_CENTRAL : str
|
||||
Heat system for urban central areas.
|
||||
|
||||
Methods
|
||||
-------
|
||||
__str__()
|
||||
Returns the string representation of the heat system.
|
||||
central_or_decentral()
|
||||
Returns whether the heat system is central or decentralized.
|
||||
system_type()
|
||||
Returns the type of the heat system.
|
||||
sector()
|
||||
Returns the sector of the heat system.
|
||||
rural()
|
||||
Returns whether the heat system is for rural areas.
|
||||
urban_decentral()
|
||||
Returns whether the heat system is for urban decentralized areas.
|
||||
urban()
|
||||
Returns whether the heat system is for urban areas.
|
||||
heat_demand_weighting(urban_fraction=None, dist_fraction=None)
|
||||
Calculates the heat demand weighting based on urban fraction and distribution fraction.
|
||||
heat_pump_costs_name(heat_source)
|
||||
Generates the name for the heat pump costs based on the heat source.
|
||||
"""
|
||||
|
||||
RESIDENTIAL_RURAL = "residential rural"
|
||||
SERVICES_RURAL = "services rural"
|
||||
RESIDENTIAL_URBAN_DECENTRAL = "residential urban decentral"
|
||||
SERVICES_URBAN_DECENTRAL = "services urban decentral"
|
||||
URBAN_CENTRAL = "urban central"
|
||||
|
||||
def __init__(self, *args):
|
||||
super().__init__(*args)
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""
|
||||
Returns the string representation of the heat system.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The string representation of the heat system.
|
||||
"""
|
||||
return self.value
|
||||
|
||||
@property
|
||||
def central_or_decentral(self) -> str:
|
||||
"""
|
||||
Returns whether the heat system is central or decentralized.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
"central" if the heat system is central, "decentral" otherwise.
|
||||
"""
|
||||
if self == HeatSystem.URBAN_CENTRAL:
|
||||
return "central"
|
||||
else:
|
||||
return "decentral"
|
||||
|
||||
@property
|
||||
def system_type(self) -> HeatSystemType:
|
||||
"""
|
||||
Returns the type of the heat system.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The type of the heat system.
|
||||
|
||||
Raises
|
||||
------
|
||||
RuntimeError
|
||||
If the heat system is invalid.
|
||||
"""
|
||||
if self == HeatSystem.URBAN_CENTRAL:
|
||||
return HeatSystemType.URBAN_CENTRAL
|
||||
elif (
|
||||
self == HeatSystem.RESIDENTIAL_URBAN_DECENTRAL
|
||||
or self == HeatSystem.SERVICES_URBAN_DECENTRAL
|
||||
):
|
||||
return HeatSystemType.URBAN_DECENTRAL
|
||||
elif self == HeatSystem.RESIDENTIAL_RURAL or self == HeatSystem.SERVICES_RURAL:
|
||||
return HeatSystemType.RURAL
|
||||
else:
|
||||
raise RuntimeError(f"Invalid heat system: {self}")
|
||||
|
||||
@property
|
||||
def sector(self) -> HeatSector:
|
||||
"""
|
||||
Returns the sector of the heat system.
|
||||
|
||||
Returns
|
||||
-------
|
||||
HeatSector
|
||||
The sector of the heat system.
|
||||
"""
|
||||
if (
|
||||
self == HeatSystem.RESIDENTIAL_RURAL
|
||||
or self == HeatSystem.RESIDENTIAL_URBAN_DECENTRAL
|
||||
):
|
||||
return HeatSector.RESIDENTIAL
|
||||
elif (
|
||||
self == HeatSystem.SERVICES_RURAL
|
||||
or self == HeatSystem.SERVICES_URBAN_DECENTRAL
|
||||
):
|
||||
return HeatSector.SERVICES
|
||||
else:
|
||||
"tot"
|
||||
|
||||
@property
|
||||
def is_rural(self) -> bool:
|
||||
"""
|
||||
Returns whether the heat system is for rural areas.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True if the heat system is for rural areas, False otherwise.
|
||||
"""
|
||||
if self == HeatSystem.RESIDENTIAL_RURAL or self == HeatSystem.SERVICES_RURAL:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_urban_decentral(self) -> bool:
|
||||
"""
|
||||
Returns whether the heat system is for urban decentralized areas.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True if the heat system is for urban decentralized areas, False otherwise.
|
||||
"""
|
||||
if (
|
||||
self == HeatSystem.RESIDENTIAL_URBAN_DECENTRAL
|
||||
or self == HeatSystem.SERVICES_URBAN_DECENTRAL
|
||||
):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_urban(self) -> bool:
|
||||
"""
|
||||
Returns whether the heat system is for urban areas.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool True if the heat system is for urban areas, False otherwise.
|
||||
"""
|
||||
return not self.is_rural
|
||||
|
||||
def heat_demand_weighting(self, urban_fraction=None, dist_fraction=None) -> float:
|
||||
"""
|
||||
Calculates the heat demand weighting based on urban fraction and
|
||||
distribution fraction.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
urban_fraction : float, optional
|
||||
The fraction of urban heat demand.
|
||||
dist_fraction : float, optional
|
||||
The fraction of distributed heat demand.
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
The heat demand weighting.
|
||||
|
||||
Raises
|
||||
------
|
||||
RuntimeError
|
||||
If the heat system is invalid.
|
||||
"""
|
||||
if "rural" in self.value:
|
||||
return 1 - urban_fraction
|
||||
elif "urban central" in self.value:
|
||||
return dist_fraction
|
||||
elif "urban decentral" in self.value:
|
||||
return urban_fraction - dist_fraction
|
||||
else:
|
||||
raise RuntimeError(f"Invalid heat system: {self}")
|
||||
|
||||
def heat_pump_costs_name(self, heat_source: str) -> str:
|
||||
"""
|
||||
Generates the name for the heat pump costs based on the heat source and
|
||||
system.
|
||||
Used to retrieve data from `technology-data <https://github.com/PyPSA/technology-data>`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
heat_source : str
|
||||
The heat source.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The name for the heat pump costs.
|
||||
"""
|
||||
return f"{self.central_or_decentral} {heat_source}-sourced heat pump"
|
||||
|
||||
@property
|
||||
def resistive_heater_costs_name(self) -> str:
|
||||
"""
|
||||
Generates the name for the resistive heater costs based on the heat
|
||||
system.
|
||||
Used to retrieve data from `technology-data <https://github.com/PyPSA/technology-data>`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The name for the heater costs.
|
||||
"""
|
||||
return f"{self.central_or_decentral} resistive heater"
|
||||
|
||||
@property
|
||||
def gas_boiler_costs_name(self) -> str:
|
||||
"""
|
||||
Generates the name for the gas boiler costs based on the heat system.
|
||||
Used to retrieve data from `technology-data <https://github.com/PyPSA/technology-data>`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The name for the gas boiler costs.
|
||||
"""
|
||||
return f"{self.central_or_decentral} gas boiler"
|
||||
|
||||
@property
|
||||
def oil_boiler_costs_name(self) -> str:
|
||||
"""
|
||||
Generates the name for the oil boiler costs based on the heat system.
|
||||
Used to retrieve data from `technology-data <https://github.com/PyPSA/technology-data>`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The name for the oil boiler costs.
|
||||
"""
|
||||
return "decentral oil boiler"
|
35
scripts/definitions/heat_system_type.py
Normal file
35
scripts/definitions/heat_system_type.py
Normal file
@ -0,0 +1,35 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-FileCopyrightText: : 2020-2024 The PyPSA-Eur Authors
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class HeatSystemType(Enum):
|
||||
"""
|
||||
Enumeration representing different types of heat systems.
|
||||
"""
|
||||
|
||||
URBAN_CENTRAL = "urban central"
|
||||
URBAN_DECENTRAL = "urban decentral"
|
||||
RURAL = "rural"
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""
|
||||
Returns the string representation of the heat system type.
|
||||
|
||||
Returns:
|
||||
str: The string representation of the heat system type.
|
||||
"""
|
||||
return self.value
|
||||
|
||||
@property
|
||||
def is_central(self) -> bool:
|
||||
"""
|
||||
Returns whether the heat system type is central.
|
||||
|
||||
Returns:
|
||||
bool: True if the heat system type is central, False otherwise.
|
||||
"""
|
||||
return self == HeatSystemType.URBAN_CENTRAL
|
@ -37,6 +37,10 @@ from pypsa.geo import haversine_pts
|
||||
from pypsa.io import import_components_from_dataframe
|
||||
from scipy.stats import beta
|
||||
|
||||
from scripts.definitions.heat_sector import HeatSector
|
||||
from scripts.definitions.heat_system import HeatSystem
|
||||
from scripts.definitions.heat_system_type import HeatSystemType
|
||||
|
||||
spatial = SimpleNamespace()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -1776,7 +1780,7 @@ def build_heat_demand(n):
|
||||
.unstack(level=1)
|
||||
)
|
||||
|
||||
sectors = ["residential", "services"]
|
||||
sectors = [sector.value for sector in HeatSector]
|
||||
uses = ["water", "space"]
|
||||
|
||||
heat_demand = {}
|
||||
@ -1804,10 +1808,21 @@ def build_heat_demand(n):
|
||||
return heat_demand
|
||||
|
||||
|
||||
def add_heat(n, costs):
|
||||
def add_heat(n: pypsa.Network, costs: pd.DataFrame, cop: xr.DataArray):
|
||||
"""
|
||||
Add heat sector to the network.
|
||||
|
||||
Parameters:
|
||||
n (pypsa.Network): The PyPSA network object.
|
||||
costs (pd.DataFrame): DataFrame containing cost information.
|
||||
cop (xr.DataArray): DataArray containing coefficient of performance (COP) values.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
logger.info("Add heat sector")
|
||||
|
||||
sectors = ["residential", "services"]
|
||||
sectors = [sector.value for sector in HeatSector]
|
||||
|
||||
heat_demand = build_heat_demand(n)
|
||||
|
||||
@ -1826,23 +1841,6 @@ def add_heat(n, costs):
|
||||
for sector in sectors:
|
||||
heat_demand[sector + " space"] = (1 - dE) * heat_demand[sector + " space"]
|
||||
|
||||
heat_systems = [
|
||||
"residential rural",
|
||||
"services rural",
|
||||
"residential urban decentral",
|
||||
"services urban decentral",
|
||||
"urban central",
|
||||
]
|
||||
|
||||
cop = {
|
||||
"air": xr.open_dataarray(snakemake.input.cop_air_total)
|
||||
.to_pandas()
|
||||
.reindex(index=n.snapshots),
|
||||
"ground": xr.open_dataarray(snakemake.input.cop_soil_total)
|
||||
.to_pandas()
|
||||
.reindex(index=n.snapshots),
|
||||
}
|
||||
|
||||
if options["solar_thermal"]:
|
||||
solar_thermal = (
|
||||
xr.open_dataarray(snakemake.input.solar_thermal_total)
|
||||
@ -1852,31 +1850,34 @@ def add_heat(n, costs):
|
||||
# 1e3 converts from W/m^2 to MW/(1000m^2) = kW/m^2
|
||||
solar_thermal = options["solar_cf_correction"] * solar_thermal / 1e3
|
||||
|
||||
for name in heat_systems:
|
||||
name_type = "central" if name == "urban central" else "decentral"
|
||||
for (
|
||||
heat_system
|
||||
) in (
|
||||
HeatSystem
|
||||
): # this loops through all heat systems defined in _entities.HeatSystem
|
||||
|
||||
if name == "urban central":
|
||||
if heat_system == HeatSystem.URBAN_CENTRAL:
|
||||
nodes = dist_fraction.index[dist_fraction > 0]
|
||||
else:
|
||||
nodes = pop_layout.index
|
||||
|
||||
n.add("Carrier", name + " heat")
|
||||
n.add("Carrier", f"{heat_system} heat")
|
||||
|
||||
n.madd(
|
||||
"Bus",
|
||||
nodes + f" {name} heat",
|
||||
nodes + f" {heat_system.value} heat",
|
||||
location=nodes,
|
||||
carrier=name + " heat",
|
||||
carrier=f"{heat_system.value} heat",
|
||||
unit="MWh_th",
|
||||
)
|
||||
|
||||
if name == "urban central" and options.get("central_heat_vent"):
|
||||
if heat_system == HeatSystem.URBAN_CENTRAL and options.get("central_heat_vent"):
|
||||
n.madd(
|
||||
"Generator",
|
||||
nodes + f" {name} heat vent",
|
||||
bus=nodes + f" {name} heat",
|
||||
nodes + f" {heat_system} heat vent",
|
||||
bus=nodes + f" {heat_system} heat",
|
||||
location=nodes,
|
||||
carrier=name + " heat vent",
|
||||
carrier=f"{heat_system} heat vent",
|
||||
p_nom_extendable=True,
|
||||
p_max_pu=0,
|
||||
p_min_pu=-1,
|
||||
@ -1884,30 +1885,24 @@ def add_heat(n, costs):
|
||||
)
|
||||
|
||||
## Add heat load
|
||||
|
||||
for sector in sectors:
|
||||
# heat demand weighting
|
||||
if "rural" in name:
|
||||
factor = 1 - urban_fraction[nodes]
|
||||
elif "urban central" in name:
|
||||
factor = dist_fraction[nodes]
|
||||
elif "urban decentral" in name:
|
||||
factor = urban_fraction[nodes] - dist_fraction[nodes]
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f" {name} not in " f"heat systems: {heat_systems}"
|
||||
factor = heat_system.heat_demand_weighting(
|
||||
urban_fraction=urban_fraction[nodes], dist_fraction=dist_fraction[nodes]
|
||||
)
|
||||
|
||||
if sector in name:
|
||||
if not heat_system == HeatSystem.URBAN_CENTRAL:
|
||||
heat_load = (
|
||||
heat_demand[[sector + " water", sector + " space"]]
|
||||
heat_demand[
|
||||
[
|
||||
heat_system.sector.value + " water",
|
||||
heat_system.sector.value + " space",
|
||||
]
|
||||
]
|
||||
.T.groupby(level=1)
|
||||
.sum()
|
||||
.T[nodes]
|
||||
.multiply(factor)
|
||||
)
|
||||
|
||||
if name == "urban central":
|
||||
if heat_system == HeatSystem.URBAN_CENTRAL:
|
||||
heat_load = (
|
||||
heat_demand.T.groupby(level=1)
|
||||
.sum()
|
||||
@ -1920,20 +1915,25 @@ def add_heat(n, costs):
|
||||
n.madd(
|
||||
"Load",
|
||||
nodes,
|
||||
suffix=f" {name} heat",
|
||||
bus=nodes + f" {name} heat",
|
||||
carrier=name + " heat",
|
||||
suffix=f" {heat_system} heat",
|
||||
bus=nodes + f" {heat_system} heat",
|
||||
carrier=f"{heat_system} heat",
|
||||
p_set=heat_load,
|
||||
)
|
||||
|
||||
## Add heat pumps
|
||||
|
||||
heat_pump_types = ["air"] if "urban" in name else ["ground", "air"]
|
||||
|
||||
for heat_pump_type in heat_pump_types:
|
||||
costs_name = f"{name_type} {heat_pump_type}-sourced heat pump"
|
||||
for heat_source in snakemake.params.heat_pump_sources[
|
||||
heat_system.system_type.value
|
||||
]:
|
||||
costs_name = heat_system.heat_pump_costs_name(heat_source)
|
||||
efficiency = (
|
||||
cop[heat_pump_type][nodes]
|
||||
cop.sel(
|
||||
heat_system=heat_system.system_type.value,
|
||||
heat_source=heat_source,
|
||||
name=nodes,
|
||||
)
|
||||
.to_pandas()
|
||||
.reindex(index=n.snapshots)
|
||||
if options["time_dep_hp_cop"]
|
||||
else costs.at[costs_name, "efficiency"]
|
||||
)
|
||||
@ -1941,10 +1941,10 @@ def add_heat(n, costs):
|
||||
n.madd(
|
||||
"Link",
|
||||
nodes,
|
||||
suffix=f" {name} {heat_pump_type} heat pump",
|
||||
suffix=f" {heat_system} {heat_source} heat pump",
|
||||
bus0=nodes,
|
||||
bus1=nodes + f" {name} heat",
|
||||
carrier=f"{name} {heat_pump_type} heat pump",
|
||||
bus1=nodes + f" {heat_system} heat",
|
||||
carrier=f"{heat_system} {heat_source} heat pump",
|
||||
efficiency=efficiency,
|
||||
capital_cost=costs.at[costs_name, "efficiency"]
|
||||
* costs.at[costs_name, "fixed"]
|
||||
@ -1954,59 +1954,65 @@ def add_heat(n, costs):
|
||||
)
|
||||
|
||||
if options["tes"]:
|
||||
n.add("Carrier", name + " water tanks")
|
||||
n.add("Carrier", f"{heat_system} water tanks")
|
||||
|
||||
n.madd(
|
||||
"Bus",
|
||||
nodes + f" {name} water tanks",
|
||||
nodes + f" {heat_system} water tanks",
|
||||
location=nodes,
|
||||
carrier=name + " water tanks",
|
||||
carrier=f"{heat_system} water tanks",
|
||||
unit="MWh_th",
|
||||
)
|
||||
|
||||
n.madd(
|
||||
"Link",
|
||||
nodes + f" {name} water tanks charger",
|
||||
bus0=nodes + f" {name} heat",
|
||||
bus1=nodes + f" {name} water tanks",
|
||||
nodes + f" {heat_system} water tanks charger",
|
||||
bus0=nodes + f" {heat_system} heat",
|
||||
bus1=nodes + f" {heat_system} water tanks",
|
||||
efficiency=costs.at["water tank charger", "efficiency"],
|
||||
carrier=name + " water tanks charger",
|
||||
carrier=f"{heat_system} water tanks charger",
|
||||
p_nom_extendable=True,
|
||||
)
|
||||
|
||||
n.madd(
|
||||
"Link",
|
||||
nodes + f" {name} water tanks discharger",
|
||||
bus0=nodes + f" {name} water tanks",
|
||||
bus1=nodes + f" {name} heat",
|
||||
carrier=name + " water tanks discharger",
|
||||
nodes + f" {heat_system} water tanks discharger",
|
||||
bus0=nodes + f" {heat_system} water tanks",
|
||||
bus1=nodes + f" {heat_system} heat",
|
||||
carrier=f"{heat_system} water tanks discharger",
|
||||
efficiency=costs.at["water tank discharger", "efficiency"],
|
||||
p_nom_extendable=True,
|
||||
)
|
||||
|
||||
tes_time_constant_days = options["tes_tau"][name_type]
|
||||
tes_time_constant_days = options["tes_tau"][
|
||||
heat_system.central_or_decentral
|
||||
]
|
||||
|
||||
n.madd(
|
||||
"Store",
|
||||
nodes + f" {name} water tanks",
|
||||
bus=nodes + f" {name} water tanks",
|
||||
nodes + f" {heat_system} water tanks",
|
||||
bus=nodes + f" {heat_system} water tanks",
|
||||
e_cyclic=True,
|
||||
e_nom_extendable=True,
|
||||
carrier=name + " water tanks",
|
||||
carrier=f"{heat_system} water tanks",
|
||||
standing_loss=1 - np.exp(-1 / 24 / tes_time_constant_days),
|
||||
capital_cost=costs.at[name_type + " water tank storage", "fixed"],
|
||||
lifetime=costs.at[name_type + " water tank storage", "lifetime"],
|
||||
capital_cost=costs.at[
|
||||
heat_system.central_or_decentral + " water tank storage", "fixed"
|
||||
],
|
||||
lifetime=costs.at[
|
||||
heat_system.central_or_decentral + " water tank storage", "lifetime"
|
||||
],
|
||||
)
|
||||
|
||||
if options["resistive_heaters"]:
|
||||
key = f"{name_type} resistive heater"
|
||||
key = f"{heat_system.central_or_decentral} resistive heater"
|
||||
|
||||
n.madd(
|
||||
"Link",
|
||||
nodes + f" {name} resistive heater",
|
||||
nodes + f" {heat_system} resistive heater",
|
||||
bus0=nodes,
|
||||
bus1=nodes + f" {name} heat",
|
||||
carrier=name + " resistive heater",
|
||||
bus1=nodes + f" {heat_system} heat",
|
||||
carrier=f"{heat_system} resistive heater",
|
||||
efficiency=costs.at[key, "efficiency"],
|
||||
capital_cost=costs.at[key, "efficiency"]
|
||||
* costs.at[key, "fixed"]
|
||||
@ -2016,16 +2022,16 @@ def add_heat(n, costs):
|
||||
)
|
||||
|
||||
if options["boilers"]:
|
||||
key = f"{name_type} gas boiler"
|
||||
key = f"{heat_system.central_or_decentral} gas boiler"
|
||||
|
||||
n.madd(
|
||||
"Link",
|
||||
nodes + f" {name} gas boiler",
|
||||
nodes + f" {heat_system} gas boiler",
|
||||
p_nom_extendable=True,
|
||||
bus0=spatial.gas.df.loc[nodes, "nodes"].values,
|
||||
bus1=nodes + f" {name} heat",
|
||||
bus1=nodes + f" {heat_system} heat",
|
||||
bus2="co2 atmosphere",
|
||||
carrier=name + " gas boiler",
|
||||
carrier=f"{heat_system} gas boiler",
|
||||
efficiency=costs.at[key, "efficiency"],
|
||||
efficiency2=costs.at["gas", "CO2 intensity"],
|
||||
capital_cost=costs.at[key, "efficiency"]
|
||||
@ -2035,22 +2041,26 @@ def add_heat(n, costs):
|
||||
)
|
||||
|
||||
if options["solar_thermal"]:
|
||||
n.add("Carrier", name + " solar thermal")
|
||||
n.add("Carrier", f"{heat_system} solar thermal")
|
||||
|
||||
n.madd(
|
||||
"Generator",
|
||||
nodes,
|
||||
suffix=f" {name} solar thermal collector",
|
||||
bus=nodes + f" {name} heat",
|
||||
carrier=name + " solar thermal",
|
||||
suffix=f" {heat_system} solar thermal collector",
|
||||
bus=nodes + f" {heat_system} heat",
|
||||
carrier=f"{heat_system} solar thermal",
|
||||
p_nom_extendable=True,
|
||||
capital_cost=costs.at[name_type + " solar thermal", "fixed"]
|
||||
capital_cost=costs.at[
|
||||
heat_system.central_or_decentral + " solar thermal", "fixed"
|
||||
]
|
||||
* overdim_factor,
|
||||
p_max_pu=solar_thermal[nodes],
|
||||
lifetime=costs.at[name_type + " solar thermal", "lifetime"],
|
||||
lifetime=costs.at[
|
||||
heat_system.central_or_decentral + " solar thermal", "lifetime"
|
||||
],
|
||||
)
|
||||
|
||||
if options["chp"] and name == "urban central":
|
||||
if options["chp"] and heat_system == HeatSystem.URBAN_CENTRAL:
|
||||
# add gas CHP; biomass CHP is added in biomass section
|
||||
n.madd(
|
||||
"Link",
|
||||
@ -2107,16 +2117,20 @@ def add_heat(n, costs):
|
||||
lifetime=costs.at["central gas CHP", "lifetime"],
|
||||
)
|
||||
|
||||
if options["chp"] and options["micro_chp"] and name != "urban central":
|
||||
if (
|
||||
options["chp"]
|
||||
and options["micro_chp"]
|
||||
and heat_system.value != "urban central"
|
||||
):
|
||||
n.madd(
|
||||
"Link",
|
||||
nodes + f" {name} micro gas CHP",
|
||||
nodes + f" {heat_system} micro gas CHP",
|
||||
p_nom_extendable=True,
|
||||
bus0=spatial.gas.df.loc[nodes, "nodes"].values,
|
||||
bus1=nodes,
|
||||
bus2=nodes + f" {name} heat",
|
||||
bus2=nodes + f" {heat_system} heat",
|
||||
bus3="co2 atmosphere",
|
||||
carrier=name + " micro gas CHP",
|
||||
carrier=heat_system.value + " micro gas CHP",
|
||||
efficiency=costs.at["micro CHP", "efficiency"],
|
||||
efficiency2=costs.at["micro CHP", "efficiency-heat"],
|
||||
efficiency3=costs.at["gas", "CO2 intensity"],
|
||||
@ -2152,7 +2166,7 @@ def add_heat(n, costs):
|
||||
) / heat_demand.T.groupby(level=[1]).sum().T
|
||||
|
||||
for name in n.loads[
|
||||
n.loads.carrier.isin([x + " heat" for x in heat_systems])
|
||||
n.loads.carrier.isin([x + " heat" for x in HeatSystem])
|
||||
].index:
|
||||
node = n.buses.loc[name, "location"]
|
||||
ct = pop_layout.loc[node, "ct"]
|
||||
@ -3110,20 +3124,16 @@ def add_industry(n, costs):
|
||||
if options["oil_boilers"]:
|
||||
nodes = pop_layout.index
|
||||
|
||||
for name in [
|
||||
"residential rural",
|
||||
"services rural",
|
||||
"residential urban decentral",
|
||||
"services urban decentral",
|
||||
]:
|
||||
for heat_system in HeatSystem:
|
||||
if not heat_system == HeatSystem.URBAN_CENTRAL:
|
||||
n.madd(
|
||||
"Link",
|
||||
nodes + f" {name} oil boiler",
|
||||
nodes + f" {heat_system} oil boiler",
|
||||
p_nom_extendable=True,
|
||||
bus0=spatial.oil.nodes,
|
||||
bus1=nodes + f" {name} heat",
|
||||
bus1=nodes + f" {heat_system} heat",
|
||||
bus2="co2 atmosphere",
|
||||
carrier=f"{name} oil boiler",
|
||||
carrier=f"{heat_system} oil boiler",
|
||||
efficiency=costs.at["decentral oil boiler", "efficiency"],
|
||||
efficiency2=costs.at["oil", "CO2 intensity"],
|
||||
capital_cost=costs.at["decentral oil boiler", "efficiency"]
|
||||
@ -4186,7 +4196,7 @@ if __name__ == "__main__":
|
||||
add_land_transport(n, costs)
|
||||
|
||||
if options["heating"]:
|
||||
add_heat(n, costs)
|
||||
add_heat(n=n, costs=costs, cop=xr.open_dataarray(snakemake.input.cop_profiles))
|
||||
|
||||
if options["biomass"]:
|
||||
add_biomass(n, costs)
|
||||
|
Loading…
Reference in New Issue
Block a user