From d3cd865defa20b47e55d44794946f449a30c2907 Mon Sep 17 00:00:00 2001 From: TomKae00 Date: Wed, 21 May 2025 17:35:12 +0200 Subject: [PATCH 01/14] feat: introduce resistive_heater_booster for ptes --- config/config.default.yaml | 9 +- rules/build_sector.smk | 13 +++ .../ptes_temperature_approximator.py | 16 +++ scripts/build_ptes_operations/run.py | 8 ++ scripts/prepare_sector_network.py | 100 ++++++++++++++++-- 5 files changed, 136 insertions(+), 10 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 35e99eebea..ffc423a893 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -465,10 +465,10 @@ sector: max_forward_temperature_baseyear: FR: 110 DK: 75 - DE: 109 + DE: 150 CZ: 130 FI: 115 - PL: 130 + PL: 180 SE: 102 IT: 90 min_forward_temperature_baseyear: @@ -482,8 +482,9 @@ sector: ptes: dynamic_capacity: true supplemental_heating: - enable: false - booster_heat_pump: false + enable: true + booster_heat_pump: true + booster_resistive_heater: true max_top_temperature: 90 min_bottom_temperature: 35 ates: diff --git a/rules/build_sector.smk b/rules/build_sector.smk index 8fe4b7b6ac..20ab18ef38 100755 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -502,6 +502,9 @@ rule build_ptes_operations: ptes_e_max_pu_profiles=resources( "ptes_e_max_pu_profiles_base_s_{clusters}_{planning_horizons}.nc" ), + ptes_reheat_ratio_profiles=resources( + "ptes_reheat_ratio_profiles_base_s_{clusters}_{planning_horizons}.nc" + ), resources: mem_mb=2000, log: @@ -1399,6 +1402,16 @@ rule prepare_sector_network: )(w) else [] ), + ptes_reheat_ratio_profiles= lambda w: ( + resources( + "ptes_reheat_ratio_profiles_base_s_{clusters}_{planning_horizons}.nc" + ) + if config_provider( + "sector","district_heating","ptes","supplemental_heating","enable" + # dies noch ausberessern, iist aber in ordnung für den Moment + )(w) + else[] + ), solar_thermal_total=lambda w: ( resources("solar_thermal_total_base_s_{clusters}.nc") if config_provider("sector", "solar_thermal")(w) diff --git a/scripts/build_ptes_operations/ptes_temperature_approximator.py b/scripts/build_ptes_operations/ptes_temperature_approximator.py index 51197eff1b..1f0d7342ae 100644 --- a/scripts/build_ptes_operations/ptes_temperature_approximator.py +++ b/scripts/build_ptes_operations/ptes_temperature_approximator.py @@ -106,3 +106,19 @@ def e_max_pu(self) -> xr.DataArray: self.max_ptes_top_temperature - self.bottom_temperature ) return normalized_delta_t.clip(min=0) # Ensure non-negative values + + + @property + def reheat_ratio(self) -> xr.DataArray: + """ + Calculate the reheat ratio: + (clipped_top_temperature - bottom_temperature) + ----------------------------------------------- + (forward_temperature_celsius - clipped_top_temperature) + Returns + ------- + xr.DataArray + The reheat ratio profile; squared if supplemental heating is enabled. + """ + return (1 + (self.forward_temperature - self.top_temperature) / + (self.top_temperature - self.return_temperature)) \ No newline at end of file diff --git a/scripts/build_ptes_operations/run.py b/scripts/build_ptes_operations/run.py index 169e7612ef..0f14e49a3d 100644 --- a/scripts/build_ptes_operations/run.py +++ b/scripts/build_ptes_operations/run.py @@ -116,3 +116,11 @@ ptes_temperature_approximator.e_max_pu.to_netcdf( snakemake.output.ptes_e_max_pu_profiles ) + + # Get PTES reheat ratio for boiler + logger.info( + f"Saving PTES reheat ratio profiles to {snakemake.output.ptes_reheat_ratio_profiles}" + ) + ptes_temperature_approximator.reheat_ratio.to_netcdf( + snakemake.output.ptes_reheat_ratio_profiles + ) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 5ed3fe206e..ae5e7bff04 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2742,6 +2742,7 @@ def add_heat( hourly_heat_demand_total_file: str, ptes_e_max_pu_file: str, ptes_direct_utilisation_profile: str, + ptes_reheat_ratio_profiles: str, ates_e_nom_max: str, ates_capex_as_fraction_of_geothermal_heat_source: float, ates_recovery_factor: float, @@ -2775,7 +2776,7 @@ def add_heat( Path to NetCDF file containing direct heat source utilisation profiles hourly_heat_demand_total_file : str Path to CSV file containing hourly heat demand data - ptes_supplemental_heating_required_file: str + ptes_direct_utilisation_profile : str Path to CSV file indicating when supplemental heating for thermal energy storage (TES) is needed district_heat_share_file : str Path to CSV file containing district heating share information @@ -3042,12 +3043,69 @@ def add_heat( if options["district_heating"]["ptes"]["supplemental_heating"][ "enable" ]: + n.add("Carrier", f"{heat_system} water pits boosting") + + n.add( + "Bus", + nodes + f" {heat_system} water pits boosting", + location=nodes, + carrier=f"{heat_system} water pits boosting", + unit="MWh_th", + ) + ptes_supplemental_heating_required = ( xr.open_dataarray(ptes_direct_utilisation_profile) .sel(name=nodes) .to_pandas() .reindex(index=n.snapshots) ) + + n.add( + "Link", + nodes, + suffix=f" {heat_system} water pits discharger", + bus0=nodes + f" {heat_system} water pits", + bus1=nodes + f" {heat_system} heat", + bus2=nodes + f" {heat_system} water pits boosting", + carrier=f"{heat_system} water pits discharger", + efficiency=costs.at[ + "central water pit discharger", + "efficiency", + ] + * ptes_supplemental_heating_required, + efficiency2=costs.at[ + "central water pit discharger", + "efficiency", + ] + * (ptes_supplemental_heating_required - 1) * (- 1), + p_nom_extendable=True, + lifetime=costs.at["central water pit storage", "lifetime"], + ) + n.links.loc[ + nodes + f" {heat_system} water pits charger", + "energy to power ratio", + ] = energy_to_power_ratio_water_pit + +# n.add( +# "Link", +# nodes, +# suffix=f" {heat_system} water pits discharger", +# bus0=nodes + f" {heat_system} water pits", +# bus1=nodes + f" {heat_system} heat", +# carrier=f"{heat_system} water pits discharger", +# efficiency=costs.at[ +# "central water pit discharger", +# "efficiency", +# ] +# * ptes_supplemental_heating_required, +# p_nom_extendable=True, +# lifetime=costs.at["central water pit storage", "lifetime"], +# ) +## n.links.loc[ + # nodes + f" {heat_system} water pits charger", + # "energy to power ratio", + # ] = energy_to_power_ratio_water_pit + else: ptes_supplemental_heating_required = 1 @@ -3275,10 +3333,10 @@ def add_heat( nodes, suffix=f" {heat_system} {heat_source} heat pump", bus0=nodes, - bus1=nodes + f" {heat_system} water pits", + bus1=nodes + f" {heat_system} water pits boosting", bus2=nodes + f" {heat_system} heat", carrier=f"{heat_system} {heat_source} heat pump", - efficiency=(-(cop_heat_pump - 1)).clip(upper=0), + efficiency=(-(cop_heat_pump - 1)).clip(upper=0), # (-(1 / (ptes_reheat_ratio - 1))).where(ptes_reheat_ratio > 1, 0.0) efficiency2=cop_heat_pump, capital_cost=costs.at[costs_name_heat_pump, "efficiency"] * costs.at[costs_name_heat_pump, "capital_cost"] @@ -3320,6 +3378,35 @@ def add_heat( lifetime=costs.at[key, "lifetime"], ) + if options["district_heating"]["ptes"]["supplemental_heating"][ + "booster_resistive_heater"] and heat_system == HeatSystem.URBAN_CENTRAL: + + ptes_reheat_ratio = ( + xr.open_dataarray(ptes_reheat_ratio_profiles) + .sel(name=nodes) + .to_pandas() + .reindex(index=n.snapshots) + ) + + n.add( + "Link", + nodes, + suffix=f" {heat_system} ptes resistive heater", + bus0=nodes, + bus1=nodes + f" {heat_system} water pits boosting", #boosting + bus2=nodes + f" {heat_system} heat", + carrier=f"{heat_system} resistive heater", + efficiency=(-(1 / (ptes_reheat_ratio - 1))).where(ptes_reheat_ratio > 1, 0.0), #nochmal überprüfen, ob das soweit passt, IM AKTUELLEN RUN NICHT DRIN + efficiency2=(ptes_reheat_ratio / (ptes_reheat_ratio - 1)).where(ptes_reheat_ratio > 1, 0.0) * (costs.at[key, "efficiency"]), + capital_cost=(costs.at[key, "efficiency"] + * costs.at[key, "capital_cost"] + * overdim_factor), + #* (ptes_reheat_ratio.max() -1)), + p_nom_extendable=True, + lifetime=costs.at[key, "lifetime"], + ) + + if options["boilers"]: key = f"{heat_system.central_or_decentral} gas boiler" @@ -6123,9 +6210,9 @@ def add_import_options( snakemake = mock_snakemake( "prepare_sector_network", opts="", - clusters="10", + clusters="8", sector_opts="", - planning_horizons="2050", + planning_horizons="2030", ) configure_logging(snakemake) # pylint: disable=E0606 @@ -6247,6 +6334,8 @@ def add_import_options( direct_heat_source_utilisation_profile_file=snakemake.input.direct_heat_source_utilisation_profiles, hourly_heat_demand_total_file=snakemake.input.hourly_heat_demand_total, ptes_e_max_pu_file=snakemake.input.ptes_e_max_pu_profiles, + ptes_direct_utilisation_profile=snakemake.input.ptes_direct_utilisation_profiles, + ptes_reheat_ratio_profiles=snakemake.input.ptes_reheat_ratio_profiles, ates_e_nom_max=snakemake.input.ates_potentials, ates_capex_as_fraction_of_geothermal_heat_source=snakemake.params.sector[ "district_heating" @@ -6258,7 +6347,6 @@ def add_import_options( "recovery_factor" ], enable_ates=snakemake.params.sector["district_heating"]["ates"]["enable"], - ptes_direct_utilisation_profile=snakemake.input.ptes_direct_utilisation_profiles, district_heat_share_file=snakemake.input.district_heat_share, solar_thermal_total_file=snakemake.input.solar_thermal_total, retro_cost_file=snakemake.input.retro_cost, From 22b17fdc9b53f69d53b3a064d78e96f1f720b9ff Mon Sep 17 00:00:00 2001 From: TomKae00 Date: Wed, 28 May 2025 10:17:01 +0200 Subject: [PATCH 02/14] feat: add ptes water water heat pump --- config/config.default.yaml | 5 +- config/plotting.default.yaml | 1 + doc/configtables/sector.csv | 1 + .../CentralHeatingCopApproximator.py | 120 +++++++++++------- scripts/build_cop_profiles/run.py | 8 +- .../ptes_temperature_approximator.py | 4 +- scripts/prepare_sector_network.py | 39 ++---- 7 files changed, 95 insertions(+), 83 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index ffc423a893..b01f7c6b0c 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -465,10 +465,10 @@ sector: max_forward_temperature_baseyear: FR: 110 DK: 75 - DE: 150 + DE: 108 CZ: 130 FI: 115 - PL: 180 + PL: 130 SE: 102 IT: 90 min_forward_temperature_baseyear: @@ -504,6 +504,7 @@ sector: heat_exchanger_pinch_point_temperature_difference: 5 #K isentropic_compressor_efficiency: 0.8 heat_loss: 0.0 + min_delta_t_lift: 10 #K limited_heat_sources: geothermal: constant_temperature_celsius: 65 diff --git a/config/plotting.default.yaml b/config/plotting.default.yaml index 16d14e6524..0451cae1ac 100644 --- a/config/plotting.default.yaml +++ b/config/plotting.default.yaml @@ -423,6 +423,7 @@ plotting: urban central water pits: "#d96f4c" urban central water pits charger: "#a85d47" urban central water pits discharger: "#b36452" + urban central water pits boosting: "#ffa36c" aquifer thermal energy storage: "#6d00fc" aquifer thermal energy storage charger: "#6d00fc" aquifer thermal energy storage discharger: "#6d00fc" diff --git a/doc/configtables/sector.csv b/doc/configtables/sector.csv index 2032390d34..b5fb916e7d 100644 --- a/doc/configtables/sector.csv +++ b/doc/configtables/sector.csv @@ -47,6 +47,7 @@ district_heating,--,, -- -- 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. +-- -- min_delta_t_lift,--,float,Minimum feasible temperature lift for heat pumps assumed for approximation. -- limited_heat_sources,--,Dictionary with names of limited heat sources (not air) for which data by Fraunhofer ISI (`Manz et al. 2024 ) is used, -- -- geothermal,-,Name of the heat source. Must be the same as in ``heat_pump_sources``, -- -- -- constant_temperature_celsius,°C,heat source temperature, diff --git a/scripts/build_cop_profiles/CentralHeatingCopApproximator.py b/scripts/build_cop_profiles/CentralHeatingCopApproximator.py index 0d872b5671..383c01e3b3 100644 --- a/scripts/build_cop_profiles/CentralHeatingCopApproximator.py +++ b/scripts/build_cop_profiles/CentralHeatingCopApproximator.py @@ -21,6 +21,9 @@ class CentralHeatingCopApproximator(BaseCopApproximator): a thermodynamic heat pump model with some hard-to-know parameters being approximated. + Uses the minimum feasible temperature lift for heat pumps as stated + by Aleksandrs et al. (2020), referring to Meggers et al. (2010). + Attributes ---------- forward_temperature_celsius : Union[xr.DataArray, np.array] @@ -31,12 +34,16 @@ class CentralHeatingCopApproximator(BaseCopApproximator): 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. + refrigerant : str + The type of refrigerant. + delta_t_pinch_point : float + The pinch point temperature difference. + isentropic_compressor_efficiency : float + The isentropic compressor efficiency. + heat_loss : float + The heat loss. + min_delta_t_lift : float + The minimum feasible temperature lift. Methods ------- @@ -45,9 +52,11 @@ class CentralHeatingCopApproximator(BaseCopApproximator): 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, + refrigerant: str, + delta_t_pinch_point: float, + isentropic_compressor_efficiency: float, + heat_loss: float, + min_delta_t_lift : float, ) -> None: Initializes the CentralHeatingCopApproximator object. @@ -61,7 +70,7 @@ class CentralHeatingCopApproximator(BaseCopApproximator): _approximate_delta_t_refrigerant_sink( self, - refrigerant: str = "ammonia", + refrigerant: str, a: float = {"ammonia": 0.2, "isobutane": -0.0011}, b: float = {"ammonia": 0.2, "isobutane": 0.3}, c: float = {"ammonia": 0.016, "isobutane": 2.4}, @@ -70,14 +79,14 @@ class CentralHeatingCopApproximator(BaseCopApproximator): _ratio_evaporation_compression_work_approximation( self, - refrigerant: str = "ammonia", + refrigerant: str, 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", + refrigerant: str, a: float = {"ammonia": 0.2, "isobutane": -0.0011}, b: float = {"ammonia": 0.2, "isobutane": 0.3}, c: float = {"ammonia": 0.016, "isobutane": 2.4}, @@ -86,7 +95,7 @@ class CentralHeatingCopApproximator(BaseCopApproximator): _ratio_evaporation_compression_work_approximation( self, - refrigerant: str = "ammonia", + refrigerant: str, a: float = {"ammonia": 0.0014, "isobutane": 0.0035}, ) -> Union[xr.DataArray, np.array]: Calculate the ratio of evaporation to compression work based on approximation. @@ -98,9 +107,11 @@ def __init__( 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, + refrigerant : str, + delta_t_pinch_point: float, + isentropic_compressor_efficiency: float, + heat_loss: float, + min_delta_t_lift: float, ) -> None: """ Initializes the CentralHeatingCopApproximator object. @@ -115,12 +126,16 @@ def __init__( 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. + refrigerant : str + The type of refrigerant. + delta_t_pinch_point : float + The pinch point temperature difference. + isentropic_compressor_efficiency : float + The isentropic compressor efficiency. + heat_loss : float + The heat loss. + min_delta_t_lift : float + The minimum feasible temperature lift. """ self.t_source_in_kelvin = BaseCopApproximator.celsius_to_kelvin( source_inlet_temperature_celsius @@ -135,10 +150,11 @@ def __init__( self.t_source_out = BaseCopApproximator.celsius_to_kelvin( source_outlet_temperature_celsius ) - + self.refrigerant = refrigerant self.isentropic_efficiency_compressor_kelvin = isentropic_compressor_efficiency self.heat_loss = heat_loss self.delta_t_pinch = delta_t_pinch_point + self.min_delta_t_lift = min_delta_t_lift def approximate_cop(self) -> Union[xr.DataArray, np.array]: """ @@ -153,7 +169,7 @@ def approximate_cop(self) -> Union[xr.DataArray, np.array]: Union[xr.DataArray, np.array]: The calculated COP values. """ return xr.where( - self.t_source_in_kelvin >= self.t_sink_out_kelvin, + self.delta_t_lift <= self.min_delta_t_lift, 0, self.ideal_lorenz_cop * ( @@ -169,7 +185,7 @@ def approximate_cop(self) -> Union[xr.DataArray, np.array]: + self.delta_t_refrigerant_source + 2 * self.delta_t_pinch ) - / self.delta_t_lift + / self.delta_t_mean_lift ) ) * self.isentropic_efficiency_compressor_kelvin @@ -209,7 +225,7 @@ def t_source_mean_kelvin(self) -> Union[xr.DataArray, np.array]: ) @property - def delta_t_lift(self) -> Union[xr.DataArray, np.array]: + def delta_t_mean_lift(self) -> Union[xr.DataArray, np.array]: """ Calculate the temperature lift as the difference between the logarithmic sink and source temperatures. @@ -217,10 +233,18 @@ def delta_t_lift(self) -> Union[xr.DataArray, np.array]: Returns ------- Union[xr.DataArray, np.array] - The temperature difference between the sink and source. + The temperature difference between the logarithmic sink and source temperatures. """ return self.t_sink_mean_kelvin - self.t_source_mean_kelvin + @property + def delta_t_lift(self) -> Union[xr.DataArray, np.array]: + """ + Calculate the temperature lift as the difference between the + sink and source temperatures. + """ + return self.t_sink_out_kelvin - self.t_source_in_kelvin + @property def ideal_lorenz_cop(self) -> Union[xr.DataArray, np.array]: """ @@ -234,7 +258,7 @@ def ideal_lorenz_cop(self) -> Union[xr.DataArray, np.array]: np.array The ideal Lorenz COP. """ - return self.t_sink_mean_kelvin / self.delta_t_lift + return self.t_sink_mean_kelvin / self.delta_t_mean_lift @property def delta_t_refrigerant_source(self) -> Union[xr.DataArray, np.array]: @@ -262,7 +286,7 @@ def delta_t_refrigerant_sink(self) -> Union[xr.DataArray, np.array]: Union[xr.DataArray, np.array] The temperature difference between the refrigerant and the sink. """ - return self._approximate_delta_t_refrigerant_sink() + return self._approximate_delta_t_refrigerant_sink(self.refrigerant) @property def ratio_evaporation_compression_work(self) -> Union[xr.DataArray, np.array]: @@ -275,7 +299,7 @@ def ratio_evaporation_compression_work(self) -> Union[xr.DataArray, np.array]: Union[xr.DataArray, np.array] The calculated ratio of evaporation to compression work. """ - return self._ratio_evaporation_compression_work_approximation() + return self._ratio_evaporation_compression_work_approximation(self.refrigerant) @property def delta_t_sink(self) -> Union[xr.DataArray, np.array]: @@ -310,7 +334,7 @@ def _approximate_delta_t_refrigerant_source( def _approximate_delta_t_refrigerant_sink( self, - refrigerant: str = "ammonia", + refrigerant: str, a: float = {"ammonia": 0.2, "isobutane": -0.0011}, b: float = {"ammonia": 0.2, "isobutane": 0.3}, c: float = {"ammonia": 0.016, "isobutane": 2.4}, @@ -321,14 +345,14 @@ def _approximate_delta_t_refrigerant_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. + refrigerant : str + The refrigerant used in the system. Either 'isobutane' or 'ammonia'. + a : float + Coefficient for the temperature difference between the sink and source. + b : float + Coefficient for the temperature difference at the sink. + c : float + Constant term. Returns ------- @@ -355,7 +379,7 @@ def _approximate_delta_t_refrigerant_sink( def _ratio_evaporation_compression_work_approximation( self, - refrigerant: str = "ammonia", + refrigerant: str, a: float = {"ammonia": 0.0014, "isobutane": 0.0035}, b: float = {"ammonia": -0.0015, "isobutane": -0.0033}, c: float = {"ammonia": 0.039, "isobutane": 0.053}, @@ -365,14 +389,14 @@ def _ratio_evaporation_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. + refrigerant : str + The refrigerant used in the system. Either 'isobutane' or 'ammonia. + a : float + Coefficient 'a' in the approximation equation. + b : float + Coefficient 'b' in the approximation equation. + c : float + Coefficient 'c' in the approximation equation. Returns ------- diff --git a/scripts/build_cop_profiles/run.py b/scripts/build_cop_profiles/run.py index 81f4319001..f792094174 100644 --- a/scripts/build_cop_profiles/run.py +++ b/scripts/build_cop_profiles/run.py @@ -80,6 +80,11 @@ def get_cop( source_inlet_temperature_celsius=source_inlet_temperature_celsius, source_outlet_temperature_celsius=source_inlet_temperature_celsius - snakemake.params.heat_source_cooling_central_heating, + refrigerant=snakemake.params.heat_pump_cop_approximation_central_heating["refrigerant"], + delta_t_pinch_point=snakemake.params.heat_pump_cop_approximation_central_heating["heat_exchanger_pinch_point_temperature_difference"], + isentropic_compressor_efficiency=snakemake.params.heat_pump_cop_approximation_central_heating["isentropic_compressor_efficiency"], + heat_loss=snakemake.params.heat_pump_cop_approximation_central_heating["heat_loss"], + min_delta_t_lift=snakemake.params.heat_pump_cop_approximation_central_heating["min_delta_t_lift"], ).approximate_cop() else: @@ -96,7 +101,8 @@ def get_cop( snakemake = mock_snakemake( "build_cop_profiles", - clusters=48, + clusters=8, + planning_horizons=2030, ) set_scenario_config(snakemake) diff --git a/scripts/build_ptes_operations/ptes_temperature_approximator.py b/scripts/build_ptes_operations/ptes_temperature_approximator.py index 1f0d7342ae..9295dc4f66 100644 --- a/scripts/build_ptes_operations/ptes_temperature_approximator.py +++ b/scripts/build_ptes_operations/ptes_temperature_approximator.py @@ -120,5 +120,5 @@ def reheat_ratio(self) -> xr.DataArray: xr.DataArray The reheat ratio profile; squared if supplemental heating is enabled. """ - return (1 + (self.forward_temperature - self.top_temperature) / - (self.top_temperature - self.return_temperature)) \ No newline at end of file + return ((self.forward_temperature - self.top_temperature) + / (self.top_temperature - self.return_temperature)) \ No newline at end of file diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index ae5e7bff04..469861aac2 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3086,30 +3086,10 @@ def add_heat( "energy to power ratio", ] = energy_to_power_ratio_water_pit -# n.add( -# "Link", -# nodes, -# suffix=f" {heat_system} water pits discharger", -# bus0=nodes + f" {heat_system} water pits", -# bus1=nodes + f" {heat_system} heat", -# carrier=f"{heat_system} water pits discharger", -# efficiency=costs.at[ -# "central water pit discharger", -# "efficiency", -# ] -# * ptes_supplemental_heating_required, -# p_nom_extendable=True, -# lifetime=costs.at["central water pit storage", "lifetime"], -# ) -## n.links.loc[ - # nodes + f" {heat_system} water pits charger", - # "energy to power ratio", - # ] = energy_to_power_ratio_water_pit - else: ptes_supplemental_heating_required = 1 - n.add( + n.add( "Link", nodes, suffix=f" {heat_system} water pits discharger", @@ -3123,11 +3103,11 @@ def add_heat( * ptes_supplemental_heating_required, p_nom_extendable=True, lifetime=costs.at["central water pit storage", "lifetime"], - ) - n.links.loc[ - nodes + f" {heat_system} water pits charger", - "energy to power ratio", - ] = energy_to_power_ratio_water_pit + ) + n.links.loc[ + nodes + f" {heat_system} water pits charger", + "energy to power ratio", + ] = energy_to_power_ratio_water_pit if options["district_heating"]["ptes"]["dynamic_capacity"]: # Load pre-calculated e_max_pu profiles @@ -3336,7 +3316,7 @@ def add_heat( bus1=nodes + f" {heat_system} water pits boosting", bus2=nodes + f" {heat_system} heat", carrier=f"{heat_system} {heat_source} heat pump", - efficiency=(-(cop_heat_pump - 1)).clip(upper=0), # (-(1 / (ptes_reheat_ratio - 1))).where(ptes_reheat_ratio > 1, 0.0) + efficiency=(-(cop_heat_pump - 1)).clip(upper=0), efficiency2=cop_heat_pump, capital_cost=costs.at[costs_name_heat_pump, "efficiency"] * costs.at[costs_name_heat_pump, "capital_cost"] @@ -3396,12 +3376,11 @@ def add_heat( bus1=nodes + f" {heat_system} water pits boosting", #boosting bus2=nodes + f" {heat_system} heat", carrier=f"{heat_system} resistive heater", - efficiency=(-(1 / (ptes_reheat_ratio - 1))).where(ptes_reheat_ratio > 1, 0.0), #nochmal überprüfen, ob das soweit passt, IM AKTUELLEN RUN NICHT DRIN - efficiency2=(ptes_reheat_ratio / (ptes_reheat_ratio - 1)).where(ptes_reheat_ratio > 1, 0.0) * (costs.at[key, "efficiency"]), + efficiency=(-(costs.at[key, "efficiency"]/ptes_reheat_ratio)-1).where(ptes_reheat_ratio != 0, 0.0), + efficiency2=(costs.at[key, "efficiency"]*(1/ptes_reheat_ratio)).where(ptes_reheat_ratio != 0, 0.0), capital_cost=(costs.at[key, "efficiency"] * costs.at[key, "capital_cost"] * overdim_factor), - #* (ptes_reheat_ratio.max() -1)), p_nom_extendable=True, lifetime=costs.at[key, "lifetime"], ) From 72121153031067a04e76737bbf6573963a60401f Mon Sep 17 00:00:00 2001 From: TomKae00 Date: Tue, 3 Jun 2025 14:17:22 +0200 Subject: [PATCH 03/14] code: saving work before merge --- .../build_cop_profiles/BaseCopApproximator.py | 2 +- scripts/build_cop_profiles/run.py | 40 ++++++++++++++----- scripts/prepare_sector_network.py | 7 ++-- 3 files changed, 35 insertions(+), 14 deletions(-) diff --git a/scripts/build_cop_profiles/BaseCopApproximator.py b/scripts/build_cop_profiles/BaseCopApproximator.py index e127ee31aa..2ee246cc72 100644 --- a/scripts/build_cop_profiles/BaseCopApproximator.py +++ b/scripts/build_cop_profiles/BaseCopApproximator.py @@ -105,6 +105,6 @@ def logarithmic_mean( Union[float, xr.DataArray, np.ndarray] Logarithmic mean temperature difference. """ - if (np.asarray(t_hot <= t_cold)).any(): + 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) diff --git a/scripts/build_cop_profiles/run.py b/scripts/build_cop_profiles/run.py index 81f4319001..af1bf7f97f 100644 --- a/scripts/build_cop_profiles/run.py +++ b/scripts/build_cop_profiles/run.py @@ -96,7 +96,8 @@ def get_cop( snakemake = mock_snakemake( "build_cop_profiles", - clusters=48, + clusters=8, + planning_horizons=2030 ) set_scenario_config(snakemake) @@ -112,12 +113,24 @@ def get_cop( for heat_system_type, heat_sources in snakemake.params.heat_pump_sources.items(): cop_this_system_type = [] for heat_source in heat_sources: - if heat_source in ["ground", "air", "ptes"]: + if heat_source in ["ground", "air", #"ptes" + ]: source_inlet_temperature_celsius = xr.open_dataarray( snakemake.input[ f"temp_{heat_source.replace('ground', 'soil')}_total" ] ) + elif heat_source == "ptes": # just for the case that ptes is only boosted by air sourced hp + source_inlet_temperature_celsius = xr.open_dataarray( + snakemake.input[ + f"temp_air_total" + ] + ) + ptes_temperature_celsius = xr.open_dataarray( + snakemake.input[ + f"temp_{heat_source}_total" + ] + ) elif heat_source in snakemake.params.limited_heat_sources.keys(): source_inlet_temperature_celsius = ( snakemake.params.limited_heat_sources[heat_source][ @@ -129,13 +142,22 @@ def get_cop( f"Unknown heat source {heat_source}. Must be one of [ground, air] or {snakemake.params.heat_sources.keys()}." ) - cop_da = get_cop( - heat_system_type=heat_system_type, - heat_source=heat_source, - source_inlet_temperature_celsius=source_inlet_temperature_celsius, - forward_temperature_by_node_and_time=central_heating_forward_temperature, - return_temperature_by_node_and_time=central_heating_return_temperature, - ) + if heat_source == "ptes": + cop_da = get_cop( + heat_system_type=heat_system_type, + heat_source=heat_source, + source_inlet_temperature_celsius=source_inlet_temperature_celsius, + forward_temperature_by_node_and_time=central_heating_forward_temperature, + return_temperature_by_node_and_time=ptes_temperature_celsius, + ) + else: + cop_da = get_cop( + heat_system_type=heat_system_type, + heat_source=heat_source, + source_inlet_temperature_celsius=source_inlet_temperature_celsius, + forward_temperature_by_node_and_time=central_heating_forward_temperature, + return_temperature_by_node_and_time=central_heating_return_temperature, + ) cop_this_system_type.append(cop_da) cop_all_system_types.append( xr.concat( diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index ae5e7bff04..a51ebe18b2 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3331,7 +3331,7 @@ def add_heat( n.add( "Link", nodes, - suffix=f" {heat_system} {heat_source} heat pump", + suffix=f" {heat_system} {heat_source} air heat pump", bus0=nodes, bus1=nodes + f" {heat_system} water pits boosting", bus2=nodes + f" {heat_system} heat", @@ -3396,12 +3396,11 @@ def add_heat( bus1=nodes + f" {heat_system} water pits boosting", #boosting bus2=nodes + f" {heat_system} heat", carrier=f"{heat_system} resistive heater", - efficiency=(-(1 / (ptes_reheat_ratio - 1))).where(ptes_reheat_ratio > 1, 0.0), #nochmal überprüfen, ob das soweit passt, IM AKTUELLEN RUN NICHT DRIN - efficiency2=(ptes_reheat_ratio / (ptes_reheat_ratio - 1)).where(ptes_reheat_ratio > 1, 0.0) * (costs.at[key, "efficiency"]), + efficiency=(costs.at[key, "efficiency"] / ptes_reheat_ratio).where(ptes_reheat_ratio > 0, 0.0), + efficiency2=(costs.at[key, "efficiency"]) * (1 + (1 / ptes_reheat_ratio)).where(ptes_reheat_ratio > 0, 0.0), capital_cost=(costs.at[key, "efficiency"] * costs.at[key, "capital_cost"] * overdim_factor), - #* (ptes_reheat_ratio.max() -1)), p_nom_extendable=True, lifetime=costs.at[key, "lifetime"], ) From bed46a5dde77bd4b3ae4827a043eeb3cf66c2279 Mon Sep 17 00:00:00 2001 From: TomKae00 Date: Tue, 3 Jun 2025 18:27:01 +0200 Subject: [PATCH 04/14] feat: add ptes air sourced boosting heat pump --- config/config.default.yaml | 4 +- .../build_cop_profiles/BaseCopApproximator.py | 2 +- .../CentralHeatingCopApproximator.py | 20 +++++++++- scripts/build_cop_profiles/run.py | 1 + scripts/prepare_sector_network.py | 38 +++++++++++++------ 5 files changed, 49 insertions(+), 16 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index b01f7c6b0c..306cd70ea3 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -483,8 +483,7 @@ sector: dynamic_capacity: true supplemental_heating: enable: true - booster_heat_pump: true - booster_resistive_heater: true + booster_technologies: ["heat_pump", "resistive_heater"] max_top_temperature: 90 min_bottom_temperature: 35 ates: @@ -505,6 +504,7 @@ sector: isentropic_compressor_efficiency: 0.8 heat_loss: 0.0 min_delta_t_lift: 10 #K + min_delta_t_condenser: 5 #K limited_heat_sources: geothermal: constant_temperature_celsius: 65 diff --git a/scripts/build_cop_profiles/BaseCopApproximator.py b/scripts/build_cop_profiles/BaseCopApproximator.py index 2ee246cc72..c905f6bf56 100644 --- a/scripts/build_cop_profiles/BaseCopApproximator.py +++ b/scripts/build_cop_profiles/BaseCopApproximator.py @@ -105,6 +105,6 @@ def logarithmic_mean( Union[float, xr.DataArray, np.ndarray] Logarithmic mean temperature difference. """ - if (np.asarray(t_hot < t_cold)).any(): + if (np.asarray(t_hot < t_cold)).any(): # raus genommen damit die Logik funktioniert. Mit Amos nochmal absprechen raise ValueError("t_hot must be greater than t_cold") return (t_hot - t_cold) / np.log(t_hot / t_cold) diff --git a/scripts/build_cop_profiles/CentralHeatingCopApproximator.py b/scripts/build_cop_profiles/CentralHeatingCopApproximator.py index 383c01e3b3..c840ad12b3 100644 --- a/scripts/build_cop_profiles/CentralHeatingCopApproximator.py +++ b/scripts/build_cop_profiles/CentralHeatingCopApproximator.py @@ -44,6 +44,8 @@ class CentralHeatingCopApproximator(BaseCopApproximator): The heat loss. min_delta_t_lift : float The minimum feasible temperature lift. + min_delta_t_condenser: float + The minimum feasible dT condenser. Methods ------- @@ -57,6 +59,7 @@ class CentralHeatingCopApproximator(BaseCopApproximator): isentropic_compressor_efficiency: float, heat_loss: float, min_delta_t_lift : float, + min_delta_t_condenser : float, ) -> None: Initializes the CentralHeatingCopApproximator object. @@ -112,6 +115,7 @@ def __init__( isentropic_compressor_efficiency: float, heat_loss: float, min_delta_t_lift: float, + min_delta_t_condenser: float, ) -> None: """ Initializes the CentralHeatingCopApproximator object. @@ -136,6 +140,8 @@ def __init__( The heat loss. min_delta_t_lift : float The minimum feasible temperature lift. + min_delta_t_condenser : float + The minimum feasible dT condenser. """ self.t_source_in_kelvin = BaseCopApproximator.celsius_to_kelvin( source_inlet_temperature_celsius @@ -155,6 +161,7 @@ def __init__( self.heat_loss = heat_loss self.delta_t_pinch = delta_t_pinch_point self.min_delta_t_lift = min_delta_t_lift + self.min_delta_t_condenser = min_delta_t_condenser def approximate_cop(self) -> Union[xr.DataArray, np.array]: """ @@ -169,7 +176,7 @@ def approximate_cop(self) -> Union[xr.DataArray, np.array]: Union[xr.DataArray, np.array]: The calculated COP values. """ return xr.where( - self.delta_t_lift <= self.min_delta_t_lift, + (self.delta_t_lift < self.min_delta_t_lift) | (self.delta_t_sink < self.min_delta_t_condenser), 0, self.ideal_lorenz_cop * ( @@ -210,6 +217,17 @@ def t_sink_mean_kelvin(self) -> Union[xr.DataArray, np.array]: t_cold=self.t_sink_in_kelvin, t_hot=self.t_sink_out_kelvin ) + def delta_t_sink(self) -> Union[xr.DataArray, np.array]: + """ + Calculate the temperature difference between the cold and hot sinks. + + Returns + ------- + Union[xr.DataArray, np.array] + The temperature difference. + """ + return self.t_sink_out_kelvin - self.t_sink_in_kelvin + @property def t_source_mean_kelvin(self) -> Union[xr.DataArray, np.array]: """ diff --git a/scripts/build_cop_profiles/run.py b/scripts/build_cop_profiles/run.py index d0df47e0ee..ccbfc493d2 100644 --- a/scripts/build_cop_profiles/run.py +++ b/scripts/build_cop_profiles/run.py @@ -85,6 +85,7 @@ def get_cop( isentropic_compressor_efficiency=snakemake.params.heat_pump_cop_approximation_central_heating["isentropic_compressor_efficiency"], heat_loss=snakemake.params.heat_pump_cop_approximation_central_heating["heat_loss"], min_delta_t_lift=snakemake.params.heat_pump_cop_approximation_central_heating["min_delta_t_lift"], + min_delta_t_condenser=snakemake.params.heat_pump_cop_approximation_central_heating["min_delta_t_condenser"] ).approximate_cop() else: diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 0f4bc0378a..b8b096d7f1 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3291,12 +3291,10 @@ def add_heat( not options["district_heating"]["ptes"]["supplemental_heating"][ "enable" ] - and options["district_heating"]["ptes"]["supplemental_heating"][ - "booster_heat_pump" - ] + and "heat_pump" in options["district_heating"]["ptes"]["supplemental_heating"]["booster_technologies"] ): raise ValueError( - "'booster_heat_pump' is true, but 'enable' is false in 'supplemental_heating'." + "Supplemental heating: 'booster_technologies' contains 'heat_pump', but 'enable' is false." ) if ( @@ -3304,10 +3302,15 @@ def add_heat( and options["district_heating"]["ptes"]["supplemental_heating"][ "enable" ] - and options["district_heating"]["ptes"]["supplemental_heating"][ - "booster_heat_pump" - ] + and "heat_pump" in options["district_heating"]["ptes"]["supplemental_heating"]["booster_technologies"] ): + ptes_reheat_ratio = ( + xr.open_dataarray(ptes_reheat_ratio_profiles) + .sel(name=nodes) + .to_pandas() + .reindex(index=n.snapshots) + ) + n.add( "Link", nodes, @@ -3316,8 +3319,8 @@ def add_heat( bus1=nodes + f" {heat_system} water pits boosting", bus2=nodes + f" {heat_system} heat", carrier=f"{heat_system} {heat_source} heat pump", - efficiency=(-(cop_heat_pump - 1)).clip(upper=0), - efficiency2=cop_heat_pump, + efficiency=-(cop_heat_pump / ptes_reheat_ratio).where(ptes_reheat_ratio > 0, 0.0), + efficiency2=(cop_heat_pump * (1 + (1 / ptes_reheat_ratio))).where(ptes_reheat_ratio > 0, 0.0), capital_cost=costs.at[costs_name_heat_pump, "efficiency"] * costs.at[costs_name_heat_pump, "capital_cost"] * overdim_factor, @@ -3358,8 +3361,19 @@ def add_heat( lifetime=costs.at[key, "lifetime"], ) - if options["district_heating"]["ptes"]["supplemental_heating"][ - "booster_resistive_heater"] and heat_system == HeatSystem.URBAN_CENTRAL: + if ( + not options["district_heating"]["ptes"]["supplemental_heating"]["enable"] + and "resistive_heater" in options["district_heating"]["ptes"]["supplemental_heating"][ + "booster_technologies"] + ): + raise ValueError( + "Supplemental heating: 'booster_technologies' contains 'resistive_heater', but 'enable' is false." + ) + if ( + "resistive_heaters" in options["district_heating"]["ptes"]["supplemental_heating"][ + "booster_technologies"] + and heat_system == HeatSystem.URBAN_CENTRAL + ): ptes_reheat_ratio = ( xr.open_dataarray(ptes_reheat_ratio_profiles) @@ -3376,7 +3390,7 @@ def add_heat( bus1=nodes + f" {heat_system} water pits boosting", #boosting bus2=nodes + f" {heat_system} heat", carrier=f"{heat_system} resistive heater", - efficiency=(costs.at[key, "efficiency"] / ptes_reheat_ratio).where(ptes_reheat_ratio > 0, 0.0), + efficiency=-(costs.at[key, "efficiency"] / ptes_reheat_ratio).where(ptes_reheat_ratio > 0, 0.0), efficiency2=(costs.at[key, "efficiency"]) * (1 + (1 / ptes_reheat_ratio)).where(ptes_reheat_ratio > 0, 0.0), capital_cost=(costs.at[key, "efficiency"] * costs.at[key, "capital_cost"] From fd76f1637724ecae7d962b421960a15e59303933 Mon Sep 17 00:00:00 2001 From: TomKae00 Date: Wed, 4 Jun 2025 15:43:31 +0200 Subject: [PATCH 05/14] feat: introduce resistive_heaters and air_sourced_hp for ptes temperature boosting --- config/config.default.yaml | 6 +- config/plotting.default.yaml | 2 + config/test/config.myopic.yaml | 2 +- config/test/config.overnight.yaml | 2 +- config/test/config.perfect.yaml | 2 +- doc/configtables/sector.csv | 6 +- rules/build_sector.smk | 22 ++------ .../build_cop_profiles/BaseCopApproximator.py | 14 ++--- .../CentralHeatingCopApproximator.py | 28 +++++----- .../DecentralHeatingCopApproximator.py | 14 ++--- scripts/build_cop_profiles/run.py | 56 ++++++++----------- .../ptes_temperature_approximator.py | 18 +++--- scripts/build_ptes_operations/run.py | 10 ++-- scripts/prepare_sector_network.py | 43 +++++++------- 14 files changed, 104 insertions(+), 121 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 1123a54b61..267097a963 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -468,7 +468,7 @@ sector: max_forward_temperature_baseyear: FR: 110 DK: 75 - DE: 108 + DE: 109 CZ: 130 FI: 115 PL: 130 @@ -485,8 +485,8 @@ sector: ptes: dynamic_capacity: true supplemental_heating: - enable: true - booster_technologies: ["heat_pump", "resistive_heater"] + enable: false + booster_technologies: [] max_top_temperature: 90 min_bottom_temperature: 35 ates: diff --git a/config/plotting.default.yaml b/config/plotting.default.yaml index 1beee2f421..79b76896ee 100644 --- a/config/plotting.default.yaml +++ b/config/plotting.default.yaml @@ -471,11 +471,13 @@ plotting: CHP electric: '#8a5751' district heating: '#e8beac' resistive heater: '#d8f9b8' + ptes resistive heater: '#a0e78d' residential rural resistive heater: '#bef5b5' residential urban decentral resistive heater: '#b2f1a9' services rural resistive heater: '#a5ed9d' services urban decentral resistive heater: '#98e991' urban central resistive heater: '#8cdf85' + urban central ptes resistive heater: '#71c96b' retrofitting: '#8487e8' building retrofitting: '#8487e8' # hydrogen diff --git a/config/test/config.myopic.yaml b/config/test/config.myopic.yaml index 44182c1496..8144b6b427 100644 --- a/config/test/config.myopic.yaml +++ b/config/test/config.myopic.yaml @@ -39,7 +39,7 @@ sector: ptes: supplemental_heating: enable: true - booster_heat_pump: true + booster_technologies: ["heat_pump", "resistive_heater"] electricity: extendable_carriers: diff --git a/config/test/config.overnight.yaml b/config/test/config.overnight.yaml index cd3730b0c1..be50efbcd9 100644 --- a/config/test/config.overnight.yaml +++ b/config/test/config.overnight.yaml @@ -65,7 +65,7 @@ sector: ptes: supplemental_heating: enable: true - booster_heat_pump: true + booster_technologies: ["heat_pump", "resistive_heater"] ates: enable: true diff --git a/config/test/config.perfect.yaml b/config/test/config.perfect.yaml index 291088ac4c..202f5032b5 100644 --- a/config/test/config.perfect.yaml +++ b/config/test/config.perfect.yaml @@ -48,7 +48,7 @@ sector: ptes: supplemental_heating: enable: true - booster_heat_pump: true + booster_technologies: ["heat_pump", "resistive_heater"] ates: enable: true diff --git a/doc/configtables/sector.csv b/doc/configtables/sector.csv index b5fb916e7d..131c616d53 100644 --- a/doc/configtables/sector.csv +++ b/doc/configtables/sector.csv @@ -22,8 +22,9 @@ district_heating,--,, -- ptes,,, -- -- dynamic_capacity,--,"{true, false}",Add option for dynamic temperature-dependent energy capacity of pit storage in district heating -- -- supplemental_heating,,, --- -- -- enable,--,"{true, false}",Add option to enable supplemental heating of pit storage in district heating --- -- -- booster_heat_pump: true,--,"{true, false}",Add option to enable a booster heat pump for supplemental heating of pit storage in district heating +-- -- -- enable,--,"{true, false}",Add option to enable supplemental heating of pit storage (PTES) in district heating +-- -- -- booster_technologies,--,List of booster technologies suitable for supplemental heating of PTES. Must be subset of ['heat_pump'; + 'resistive_heaters'], -- -- max_top_temperature,C,float,The maximum top temperature of the pit storage according to DEA technology catalogue (2018) -- -- min_bottom_temperature,C,float,The minimum bottom temperature of the pit storage according to DEA technology catalogue (2018) -- ates,,, @@ -48,6 +49,7 @@ district_heating,--,, -- -- 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. -- -- min_delta_t_lift,--,float,Minimum feasible temperature lift for heat pumps assumed for approximation. +-- -- min_delta_t_condenser,--,float,Minimum feasible temperature difference on the heat sink side of the heat pump condenser assumed for approximation. -- limited_heat_sources,--,Dictionary with names of limited heat sources (not air) for which data by Fraunhofer ISI (`Manz et al. 2024 ) is used, -- -- geothermal,-,Name of the heat source. Must be the same as in ``heat_pump_sources``, -- -- -- constant_temperature_celsius,°C,heat source temperature, diff --git a/rules/build_sector.smk b/rules/build_sector.smk index 1f52ddabdc..a06597726c 100755 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -470,19 +470,6 @@ rule build_ptes_operations: "ptes", "min_bottom_temperature", ), - # enable_supplemental_heating=config_provider( - # "sector", - # "district_heating", - # "ptes", - # "supplemental_heating", - # "enable", - # ), - # enable_dynamic_capacity=config_provider( - # "sector", - # "district_heating", - # "ptes", - # "dynamic_capacity", - # ), snapshots=config_provider("snapshots"), input: central_heating_forward_temperature_profiles=resources( @@ -502,8 +489,8 @@ rule build_ptes_operations: ptes_e_max_pu_profiles=resources( "ptes_e_max_pu_profiles_base_s_{clusters}_{planning_horizons}.nc" ), - ptes_reheat_ratio_profiles=resources( - "ptes_reheat_ratio_profiles_base_s_{clusters}_{planning_horizons}.nc" + ptes_temperature_boost_ratio_profiles=resources( + "ptes_temperature_boost_ratio_profiles_base_s_{clusters}_{planning_horizons}.nc" ), resources: mem_mb=2000, @@ -1404,13 +1391,12 @@ rule prepare_sector_network: )(w) else [] ), - ptes_reheat_ratio_profiles= lambda w: ( + ptes_temperature_boost_ratio_profiles= lambda w: ( resources( - "ptes_reheat_ratio_profiles_base_s_{clusters}_{planning_horizons}.nc" + "ptes_temperature_boost_ratio_profiles_base_s_{clusters}_{planning_horizons}.nc" ) if config_provider( "sector","district_heating","ptes","supplemental_heating","enable" - # dies noch ausberessern, iist aber in ordnung für den Moment )(w) else[] ), diff --git a/scripts/build_cop_profiles/BaseCopApproximator.py b/scripts/build_cop_profiles/BaseCopApproximator.py index c905f6bf56..3fb70c684e 100644 --- a/scripts/build_cop_profiles/BaseCopApproximator.py +++ b/scripts/build_cop_profiles/BaseCopApproximator.py @@ -16,14 +16,14 @@ class BaseCopApproximator(ABC): Attributes ---------- - forward_temperature_celsius : Union[xr.DataArray, np.array] - The forward temperature in Celsius. + sink_outlet_temperature_celsius : Union[xr.DataArray, np.array] + The sink outlet 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) + __init__(self, sink_outlet_temperature_celsius, source_inlet_temperature_celsius) Initialize CopApproximator. approximate_cop(self) Approximate heat pump coefficient of performance (COP). @@ -35,7 +35,7 @@ class BaseCopApproximator(ABC): def __init__( self, - forward_temperature_celsius: Union[xr.DataArray, np.array], + sink_outlet_temperature_celsius: Union[xr.DataArray, np.array], source_inlet_temperature_celsius: Union[xr.DataArray, np.array], ): """ @@ -43,8 +43,8 @@ def __init__( Parameters ---------- - forward_temperature_celsius : Union[xr.DataArray, np.array] - The forward temperature in Celsius. + sink_outlet_temperature_celsius : Union[xr.DataArray, np.array] + The sink outlet temperature in Celsius. source_inlet_temperature_celsius : Union[xr.DataArray, np.array] The source inlet temperature in Celsius. """ @@ -105,6 +105,6 @@ def logarithmic_mean( Union[float, xr.DataArray, np.ndarray] Logarithmic mean temperature difference. """ - if (np.asarray(t_hot < t_cold)).any(): # raus genommen damit die Logik funktioniert. Mit Amos nochmal absprechen + 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) diff --git a/scripts/build_cop_profiles/CentralHeatingCopApproximator.py b/scripts/build_cop_profiles/CentralHeatingCopApproximator.py index c840ad12b3..f1a4849eb1 100644 --- a/scripts/build_cop_profiles/CentralHeatingCopApproximator.py +++ b/scripts/build_cop_profiles/CentralHeatingCopApproximator.py @@ -26,10 +26,10 @@ class CentralHeatingCopApproximator(BaseCopApproximator): 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. + sink_outlet_temperature_celsius : Union[xr.DataArray, np.array] + The sink outlet temperature in Celsius. + sink_inlet_temperature_celsius : Union[xr.DataArray, np.array] + The sink inlet 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] @@ -50,9 +50,9 @@ class CentralHeatingCopApproximator(BaseCopApproximator): Methods ------- __init__( - forward_temperature_celsius: Union[xr.DataArray, np.array], + sink_outlet_temperature_celsius: Union[xr.DataArray, np.array], source_inlet_temperature_celsius: Union[xr.DataArray, np.array], - return_temperature_celsius: Union[xr.DataArray, np.array], + sink_inlet_temperature_celsius: Union[xr.DataArray, np.array], source_outlet_temperature_celsius: Union[xr.DataArray, np.array], refrigerant: str, delta_t_pinch_point: float, @@ -106,9 +106,9 @@ class CentralHeatingCopApproximator(BaseCopApproximator): def __init__( self, - forward_temperature_celsius: Union[xr.DataArray, np.array], + sink_outlet_temperature_celsius: Union[xr.DataArray, np.array], source_inlet_temperature_celsius: Union[xr.DataArray, np.array], - return_temperature_celsius: Union[xr.DataArray, np.array], + sink_inlet_temperature_celsius: Union[xr.DataArray, np.array], source_outlet_temperature_celsius: Union[xr.DataArray, np.array], refrigerant : str, delta_t_pinch_point: float, @@ -122,10 +122,10 @@ def __init__( 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. + sink_outlet_temperature_celsius : Union[xr.DataArray, np.array] + The sink outlet temperature in Celsius. + sink_inlet_temperature_celsius : Union[xr.DataArray, np.array] + The sink inlet 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] @@ -147,11 +147,11 @@ def __init__( source_inlet_temperature_celsius ) self.t_sink_out_kelvin = BaseCopApproximator.celsius_to_kelvin( - forward_temperature_celsius + sink_outlet_temperature_celsius ) self.t_sink_in_kelvin = BaseCopApproximator.celsius_to_kelvin( - return_temperature_celsius + sink_inlet_temperature_celsius ) self.t_source_out = BaseCopApproximator.celsius_to_kelvin( source_outlet_temperature_celsius diff --git a/scripts/build_cop_profiles/DecentralHeatingCopApproximator.py b/scripts/build_cop_profiles/DecentralHeatingCopApproximator.py index 36f990cca6..8687de0f74 100644 --- a/scripts/build_cop_profiles/DecentralHeatingCopApproximator.py +++ b/scripts/build_cop_profiles/DecentralHeatingCopApproximator.py @@ -20,8 +20,8 @@ class DecentralHeatingCopApproximator(BaseCopApproximator): Attributes ---------- - forward_temperature_celsius : Union[xr.DataArray, np.array] - The forward temperature in Celsius. + sink_outlet_temperature_celsius : Union[xr.DataArray, np.array] + The sink outlet temperature in Celsius. source_inlet_temperature_celsius : Union[xr.DataArray, np.array] The source inlet temperature in Celsius. source_type : str @@ -29,7 +29,7 @@ class DecentralHeatingCopApproximator(BaseCopApproximator): Methods ------- - __init__(forward_temperature_celsius, source_inlet_temperature_celsius, source_type) + __init__(sink_outlet_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. @@ -45,7 +45,7 @@ class DecentralHeatingCopApproximator(BaseCopApproximator): def __init__( self, - forward_temperature_celsius: Union[xr.DataArray, np.array], + sink_outlet_temperature_celsius: Union[xr.DataArray, np.array], source_inlet_temperature_celsius: Union[xr.DataArray, np.array], source_type: str, ): @@ -54,15 +54,15 @@ def __init__( Parameters ---------- - forward_temperature_celsius : Union[xr.DataArray, np.array] - The forward temperature in Celsius. + sink_outlet_temperature_celsius : Union[xr.DataArray, np.array] + The sink outlet 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 + self.delta_t = sink_outlet_temperature_celsius - source_inlet_temperature_celsius if source_type not in ["air", "ground"]: raise ValueError("'source_type' must be one of ['air', 'ground']") else: diff --git a/scripts/build_cop_profiles/run.py b/scripts/build_cop_profiles/run.py index ccbfc493d2..db46bd3c9d 100644 --- a/scripts/build_cop_profiles/run.py +++ b/scripts/build_cop_profiles/run.py @@ -22,6 +22,8 @@ heat_exchanger_pinch_point_temperature_difference isentropic_compressor_efficiency: heat_loss: + min_delta_t_lift: + min_delta_t_condenser: heat_pump_sources: urban central: urban decentral: @@ -53,8 +55,8 @@ def get_cop( heat_system_type: str, heat_source: str, source_inlet_temperature_celsius: xr.DataArray, - forward_temperature_by_node_and_time: xr.DataArray = None, - return_temperature_by_node_and_time: xr.DataArray = None, + sink_outlet_temperature_celsius: xr.DataArray = None, + sink_inlet_temperature_celsius: xr.DataArray = None, ) -> xr.DataArray: """ Calculate the coefficient of performance (COP) for a heating system. @@ -75,8 +77,8 @@ def get_cop( """ if HeatSystemType(heat_system_type).is_central: return CentralHeatingCopApproximator( - forward_temperature_celsius=forward_temperature_by_node_and_time, - return_temperature_celsius=return_temperature_by_node_and_time, + sink_outlet_temperature_celsius=sink_outlet_temperature_celsius, + sink_inlet_temperature_celsius=sink_inlet_temperature_celsius, source_inlet_temperature_celsius=source_inlet_temperature_celsius, source_outlet_temperature_celsius=source_inlet_temperature_celsius - snakemake.params.heat_source_cooling_central_heating, @@ -90,7 +92,7 @@ def get_cop( else: return DecentralHeatingCopApproximator( - forward_temperature_celsius=snakemake.params.heat_pump_sink_T_decentral_heating, + sink_outlet_temperature_celsius=snakemake.params.heat_pump_sink_T_decentral_heating, source_inlet_temperature_celsius=source_inlet_temperature_celsius, source_type=heat_source, ).approximate_cop() @@ -119,51 +121,37 @@ def get_cop( for heat_system_type, heat_sources in snakemake.params.heat_pump_sources.items(): cop_this_system_type = [] for heat_source in heat_sources: - if heat_source in ["ground", "air", #"ptes" - ]: - source_inlet_temperature_celsius = xr.open_dataarray( - snakemake.input[ - f"temp_{heat_source.replace('ground', 'soil')}_total" - ] - ) - elif heat_source == "ptes": # just for the case that ptes is only boosted by air sourced hp + if heat_source == "ptes": source_inlet_temperature_celsius = xr.open_dataarray( snakemake.input[ f"temp_air_total" ] ) - ptes_temperature_celsius = xr.open_dataarray( + sink_inlet_temperature_celsius = xr.open_dataarray( snakemake.input[ f"temp_{heat_source}_total" ] ) - elif heat_source in snakemake.params.limited_heat_sources.keys(): - source_inlet_temperature_celsius = ( - snakemake.params.limited_heat_sources[heat_source][ - "constant_temperature_celsius" + elif heat_source in ["ground", "air", + ]: + source_inlet_temperature_celsius = xr.open_dataarray( + snakemake.input[ + f"temp_{heat_source.replace('ground', 'soil')}_total" ] ) + sink_inlet_temperature_celsius = central_heating_return_temperature else: raise ValueError( f"Unknown heat source {heat_source}. Must be one of [ground, air] or {snakemake.params.heat_sources.keys()}." ) - if heat_source == "ptes": - cop_da = get_cop( - heat_system_type=heat_system_type, - heat_source=heat_source, - source_inlet_temperature_celsius=source_inlet_temperature_celsius, - forward_temperature_by_node_and_time=central_heating_forward_temperature, - return_temperature_by_node_and_time=ptes_temperature_celsius, - ) - else: - cop_da = get_cop( - heat_system_type=heat_system_type, - heat_source=heat_source, - source_inlet_temperature_celsius=source_inlet_temperature_celsius, - forward_temperature_by_node_and_time=central_heating_forward_temperature, - return_temperature_by_node_and_time=central_heating_return_temperature, - ) + cop_da = get_cop( + heat_system_type=heat_system_type, + heat_source=heat_source, + source_inlet_temperature_celsius=source_inlet_temperature_celsius, + sink_outlet_temperature_celsius=central_heating_forward_temperature, + sink_inlet_temperature_celsius=sink_inlet_temperature_celsius, + ) cop_this_system_type.append(cop_da) cop_all_system_types.append( xr.concat( diff --git a/scripts/build_ptes_operations/ptes_temperature_approximator.py b/scripts/build_ptes_operations/ptes_temperature_approximator.py index 9295dc4f66..d8f65e16f0 100644 --- a/scripts/build_ptes_operations/ptes_temperature_approximator.py +++ b/scripts/build_ptes_operations/ptes_temperature_approximator.py @@ -109,16 +109,18 @@ def e_max_pu(self) -> xr.DataArray: @property - def reheat_ratio(self) -> xr.DataArray: + def temperature_boost_ratio(self) -> xr.DataArray: """ - Calculate the reheat ratio: - (clipped_top_temperature - bottom_temperature) - ----------------------------------------------- - (forward_temperature_celsius - clipped_top_temperature) + Calculate the additional lift required between the store's + current top temperature and the forward temperature with the lift + already achieved inside the store. + Returns ------- xr.DataArray - The reheat ratio profile; squared if supplemental heating is enabled. + The resulting fraction of PTES charge that must be further heated. """ - return ((self.forward_temperature - self.top_temperature) - / (self.top_temperature - self.return_temperature)) \ No newline at end of file + return ( + (self.forward_temperature - self.top_temperature) + / (self.top_temperature - self.return_temperature) + ) \ No newline at end of file diff --git a/scripts/build_ptes_operations/run.py b/scripts/build_ptes_operations/run.py index 0f14e49a3d..eff42fb049 100644 --- a/scripts/build_ptes_operations/run.py +++ b/scripts/build_ptes_operations/run.py @@ -43,6 +43,8 @@ Binary indicator for additional heating (1 = direct PTES use, 0 = supplemental heating required). - `resources//ptes_e_max_pu_profiles.nc` Normalized PTES capacity profiles. +- `resources//ptes_temperature_boost_ratio_profiles.nc` + Ratio of PTES charge that requires additional heating due to temperature differences. Source ------ @@ -117,10 +119,10 @@ snakemake.output.ptes_e_max_pu_profiles ) - # Get PTES reheat ratio for boiler + # Get PTES temperature boost ratio logger.info( - f"Saving PTES reheat ratio profiles to {snakemake.output.ptes_reheat_ratio_profiles}" + f"Saving PTES reheat ratio profiles to {snakemake.output.ptes_temperature_boost_ratio_profiles}" ) - ptes_temperature_approximator.reheat_ratio.to_netcdf( - snakemake.output.ptes_reheat_ratio_profiles + ptes_temperature_approximator.temperature_boost_ratio.to_netcdf( + snakemake.output.ptes_temperature_boost_ratio_profiles ) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index b511f0328b..f509e591e5 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2752,8 +2752,8 @@ def add_heat( direct_heat_source_utilisation_profile_file: str, hourly_heat_demand_total_file: str, ptes_e_max_pu_file: str, - ptes_direct_utilisation_profile: str, - ptes_reheat_ratio_profiles: str, + ptes_direct_utilisation_profile_file: str, + ptes_temperature_boost_ratio_profile_file: str, ates_e_nom_max: str, ates_capex_as_fraction_of_geothermal_heat_source: float, ates_recovery_factor: float, @@ -2787,8 +2787,10 @@ def add_heat( Path to NetCDF file containing direct heat source utilisation profiles hourly_heat_demand_total_file : str Path to CSV file containing hourly heat demand data - ptes_direct_utilisation_profile : str - Path to CSV file indicating when supplemental heating for thermal energy storage (TES) is needed + ptes_direct_utilisation_profile_file : str + Path to CSV file containing when supplemental heating for thermal energy storage (TES) is needed + ptes_temperature_boost_ratio_profile_file : str + Path to CSV file containing Ratio of PTES charge that requires additional heating district_heat_share_file : str Path to CSV file containing district heating share information solar_thermal_total_file : str @@ -3065,7 +3067,7 @@ def add_heat( ) ptes_supplemental_heating_required = ( - xr.open_dataarray(ptes_direct_utilisation_profile) + xr.open_dataarray(ptes_direct_utilisation_profile_file) .sel(name=nodes) .to_pandas() .reindex(index=n.snapshots) @@ -3315,8 +3317,8 @@ def add_heat( ] and "heat_pump" in options["district_heating"]["ptes"]["supplemental_heating"]["booster_technologies"] ): - ptes_reheat_ratio = ( - xr.open_dataarray(ptes_reheat_ratio_profiles) + ptes_temperature_boost_ratio = ( + xr.open_dataarray(ptes_temperature_boost_ratio_profile_file) .sel(name=nodes) .to_pandas() .reindex(index=n.snapshots) @@ -3325,13 +3327,13 @@ def add_heat( n.add( "Link", nodes, - suffix=f" {heat_system} {heat_source} air heat pump", + suffix=f" {heat_system} {heat_source} heat pump", bus0=nodes, bus1=nodes + f" {heat_system} water pits boosting", bus2=nodes + f" {heat_system} heat", carrier=f"{heat_system} {heat_source} heat pump", - efficiency=-(cop_heat_pump / ptes_reheat_ratio).where(ptes_reheat_ratio > 0, 0.0), - efficiency2=(cop_heat_pump * (1 + (1 / ptes_reheat_ratio))).where(ptes_reheat_ratio > 0, 0.0), + efficiency=-(cop_heat_pump / ptes_temperature_boost_ratio).where(ptes_temperature_boost_ratio > 0, 0.0), + efficiency2=(cop_heat_pump * (1 + (1 / ptes_temperature_boost_ratio))).where(ptes_temperature_boost_ratio > 0, 0.0), capital_cost=costs.at[costs_name_heat_pump, "efficiency"] * costs.at[costs_name_heat_pump, "capital_cost"] * overdim_factor, @@ -3374,20 +3376,19 @@ def add_heat( if ( not options["district_heating"]["ptes"]["supplemental_heating"]["enable"] - and "resistive_heater" in options["district_heating"]["ptes"]["supplemental_heating"][ + and "resistive_heaters" in options["district_heating"]["ptes"]["supplemental_heating"][ "booster_technologies"] ): raise ValueError( - "Supplemental heating: 'booster_technologies' contains 'resistive_heater', but 'enable' is false." + "Supplemental heating: 'booster_technologies' contains 'resistive_heaters', but 'enable' is false." ) if ( "resistive_heaters" in options["district_heating"]["ptes"]["supplemental_heating"][ "booster_technologies"] and heat_system == HeatSystem.URBAN_CENTRAL ): - - ptes_reheat_ratio = ( - xr.open_dataarray(ptes_reheat_ratio_profiles) + ptes_temperature_boost_ratio = ( + xr.open_dataarray(ptes_temperature_boost_ratio_profile_file) .sel(name=nodes) .to_pandas() .reindex(index=n.snapshots) @@ -3401,8 +3402,8 @@ def add_heat( bus1=nodes + f" {heat_system} water pits boosting", #boosting bus2=nodes + f" {heat_system} heat", carrier=f"{heat_system} resistive heater", - efficiency=-(costs.at[key, "efficiency"] / ptes_reheat_ratio).where(ptes_reheat_ratio > 0, 0.0), - efficiency2=(costs.at[key, "efficiency"]) * (1 + (1 / ptes_reheat_ratio)).where(ptes_reheat_ratio > 0, 0.0), + efficiency=-(costs.at[key, "efficiency"] / ptes_temperature_boost_ratio).where(ptes_temperature_boost_ratio > 0, 0.0), + efficiency2=(costs.at[key, "efficiency"]) * (1 + (1 / ptes_temperature_boost_ratio)).where(ptes_temperature_boost_ratio > 0, 0.0), capital_cost=(costs.at[key, "efficiency"] * costs.at[key, "capital_cost"] * overdim_factor), @@ -6214,9 +6215,9 @@ def add_import_options( snakemake = mock_snakemake( "prepare_sector_network", opts="", - clusters="8", + clusters="10", sector_opts="", - planning_horizons="2030", + planning_horizons="2050", ) configure_logging(snakemake) # pylint: disable=E0606 @@ -6338,8 +6339,8 @@ def add_import_options( direct_heat_source_utilisation_profile_file=snakemake.input.direct_heat_source_utilisation_profiles, hourly_heat_demand_total_file=snakemake.input.hourly_heat_demand_total, ptes_e_max_pu_file=snakemake.input.ptes_e_max_pu_profiles, - ptes_direct_utilisation_profile=snakemake.input.ptes_direct_utilisation_profiles, - ptes_reheat_ratio_profiles=snakemake.input.ptes_reheat_ratio_profiles, + ptes_direct_utilisation_profile_file=snakemake.input.ptes_direct_utilisation_profiles, + ptes_temperature_boost_ratio_profile_file=snakemake.input.ptes_temperature_boost_ratio_profiles, ates_e_nom_max=snakemake.input.ates_potentials, ates_capex_as_fraction_of_geothermal_heat_source=snakemake.params.sector[ "district_heating" From 45e03efc51895c66d07646d152401a2bff1c6147 Mon Sep 17 00:00:00 2001 From: TomKae00 Date: Wed, 4 Jun 2025 16:03:24 +0200 Subject: [PATCH 06/14] improvement: update release_notes --- doc/release_notes.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 12b03d439d..dc2e288f2c 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -29,13 +29,15 @@ Release Notes **Changes** +* Added resistive heaters as a temperature boosting technology for PTES. Additionally, the heat pump booster now uses ambient air as its heat source for boosting. + * Introduce the ability to use the bidding zones as administrative zones for the clustering (https://github.com/PyPSA/pypsa-eur/pull/1578). This also introduces the ability to create a custom `busmap` from custom `busshapes`. To use bidding zones as clustering mode, a `bz` mode has been introduced for `administrative` clustering. This feature is compatible with the general NUTS clustering approach. Custom `busshapes` must be provided as `data/busshapes/base_s_{clusters}_{base_network}.geojson`. * Improved balance map plotting: Carriers in the balance map legends which can serve as both supply and consumption (e.g. H2 for industry) are now placed in the legend category where its total absolute value is larger in the total system balance. * Added aquifer thermal energy storage (ATES) to district heating. Some parameters (CAPEX, standing losses) might require tuning by the user. Eligibility computation is relatively basic. Turned off by default. -* Added supplemental heating of thermal energy storages (currently implemented for PTES). This can be enabled by setting: ``sector: district_heating: ptes: supplemental_heating: true`` . To enable a boosting heat pump as the supplemental heating technology, use: ``sector: district_heating: ptes: supplemental_heating: booster_heat_pump: true`` +* Added supplemental heating of thermal energy storages (currently implemented for PTES). This can be enabled by setting: ``sector: district_heating: ptes: supplemental_heating: true`` . * Non-sequestered HVC (plastic waste) is now allocated based on the population instead of production. It can be either burned without energetic utilization or in CHPs to support the district heating system. From 1331715ac1137d862f03dfd90db389794fbf97cf Mon Sep 17 00:00:00 2001 From: TomKae00 Date: Thu, 5 Jun 2025 17:03:25 +0200 Subject: [PATCH 07/14] feat: introduce resistive heaters and air sourced hp boosting --- config/config.default.yaml | 1 - config/test/config.myopic.yaml | 3 ++ config/test/config.overnight.yaml | 3 ++ config/test/config.perfect.yaml | 3 ++ .../build_cop_profiles/BaseCopApproximator.py | 6 +++- .../CentralHeatingCopApproximator.py | 29 ++++--------------- scripts/build_cop_profiles/run.py | 1 - .../ptes_temperature_approximator.py | 12 ++++---- scripts/build_ptes_operations/run.py | 1 + 9 files changed, 27 insertions(+), 32 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 267097a963..85d2ac511f 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -519,7 +519,6 @@ sector: heat_pump_sources: urban central: - air - - ptes urban decentral: - air rural: diff --git a/config/test/config.myopic.yaml b/config/test/config.myopic.yaml index 8144b6b427..c709ec5747 100644 --- a/config/test/config.myopic.yaml +++ b/config/test/config.myopic.yaml @@ -40,6 +40,9 @@ sector: supplemental_heating: enable: true booster_technologies: ["heat_pump", "resistive_heater"] + heat_pump_sources: + urban central: + - ptes electricity: extendable_carriers: diff --git a/config/test/config.overnight.yaml b/config/test/config.overnight.yaml index be50efbcd9..7b610e17bc 100644 --- a/config/test/config.overnight.yaml +++ b/config/test/config.overnight.yaml @@ -68,6 +68,9 @@ sector: booster_technologies: ["heat_pump", "resistive_heater"] ates: enable: true + heat_pump_sources: + urban central: + - ptes industry: diff --git a/config/test/config.perfect.yaml b/config/test/config.perfect.yaml index 202f5032b5..adb8b93c9e 100644 --- a/config/test/config.perfect.yaml +++ b/config/test/config.perfect.yaml @@ -51,6 +51,9 @@ sector: booster_technologies: ["heat_pump", "resistive_heater"] ates: enable: true + heat_pump_sources: + urban central: + - ptes atlite: diff --git a/scripts/build_cop_profiles/BaseCopApproximator.py b/scripts/build_cop_profiles/BaseCopApproximator.py index 3fb70c684e..46e1cf8544 100644 --- a/scripts/build_cop_profiles/BaseCopApproximator.py +++ b/scripts/build_cop_profiles/BaseCopApproximator.py @@ -107,4 +107,8 @@ def logarithmic_mean( """ 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) + return xr.where( + t_hot == t_cold, + t_hot, + (t_hot - t_cold) / np.log(t_hot / t_cold), + ) \ No newline at end of file diff --git a/scripts/build_cop_profiles/CentralHeatingCopApproximator.py b/scripts/build_cop_profiles/CentralHeatingCopApproximator.py index f1a4849eb1..df96c3fb3f 100644 --- a/scripts/build_cop_profiles/CentralHeatingCopApproximator.py +++ b/scripts/build_cop_profiles/CentralHeatingCopApproximator.py @@ -44,8 +44,6 @@ class CentralHeatingCopApproximator(BaseCopApproximator): The heat loss. min_delta_t_lift : float The minimum feasible temperature lift. - min_delta_t_condenser: float - The minimum feasible dT condenser. Methods ------- @@ -115,7 +113,6 @@ def __init__( isentropic_compressor_efficiency: float, heat_loss: float, min_delta_t_lift: float, - min_delta_t_condenser: float, ) -> None: """ Initializes the CentralHeatingCopApproximator object. @@ -140,8 +137,6 @@ def __init__( The heat loss. min_delta_t_lift : float The minimum feasible temperature lift. - min_delta_t_condenser : float - The minimum feasible dT condenser. """ self.t_source_in_kelvin = BaseCopApproximator.celsius_to_kelvin( source_inlet_temperature_celsius @@ -153,7 +148,7 @@ def __init__( self.t_sink_in_kelvin = BaseCopApproximator.celsius_to_kelvin( sink_inlet_temperature_celsius ) - self.t_source_out = BaseCopApproximator.celsius_to_kelvin( + self.t_source_out_kelvin = BaseCopApproximator.celsius_to_kelvin( source_outlet_temperature_celsius ) self.refrigerant = refrigerant @@ -161,7 +156,6 @@ def __init__( self.heat_loss = heat_loss self.delta_t_pinch = delta_t_pinch_point self.min_delta_t_lift = min_delta_t_lift - self.min_delta_t_condenser = min_delta_t_condenser def approximate_cop(self) -> Union[xr.DataArray, np.array]: """ @@ -176,7 +170,7 @@ def approximate_cop(self) -> Union[xr.DataArray, np.array]: Union[xr.DataArray, np.array]: The calculated COP values. """ return xr.where( - (self.delta_t_lift < self.min_delta_t_lift) | (self.delta_t_sink < self.min_delta_t_condenser), + (self.delta_t_lift < self.min_delta_t_lift), 0, self.ideal_lorenz_cop * ( @@ -217,17 +211,6 @@ def t_sink_mean_kelvin(self) -> Union[xr.DataArray, np.array]: t_cold=self.t_sink_in_kelvin, t_hot=self.t_sink_out_kelvin ) - def delta_t_sink(self) -> Union[xr.DataArray, np.array]: - """ - Calculate the temperature difference between the cold and hot sinks. - - Returns - ------- - Union[xr.DataArray, np.array] - The temperature difference. - """ - return self.t_sink_out_kelvin - self.t_sink_in_kelvin - @property def t_source_mean_kelvin(self) -> Union[xr.DataArray, np.array]: """ @@ -239,7 +222,7 @@ def t_source_mean_kelvin(self) -> 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 + t_hot=self.t_source_in_kelvin, t_cold=self.t_source_out_kelvin ) @property @@ -290,7 +273,7 @@ def delta_t_refrigerant_source(self) -> 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 + delta_t_source=self.t_source_in_kelvin - self.t_source_out_kelvin ) @property @@ -390,7 +373,7 @@ def _approximate_delta_t_refrigerant_sink( ) return ( a[refrigerant] - * (self.t_sink_out_kelvin - self.t_source_out + 2 * self.delta_t_pinch) + * (self.t_sink_out_kelvin - self.t_source_out_kelvin + 2 * self.delta_t_pinch) + b[refrigerant] * self.delta_t_sink + c[refrigerant] ) @@ -434,7 +417,7 @@ def _ratio_evaporation_compression_work_approximation( ) return ( a[refrigerant] - * (self.t_sink_out_kelvin - self.t_source_out + 2 * self.delta_t_pinch) + * (self.t_sink_out_kelvin - self.t_source_out_kelvin + 2 * self.delta_t_pinch) + b[refrigerant] * self.delta_t_sink + c[refrigerant] ) diff --git a/scripts/build_cop_profiles/run.py b/scripts/build_cop_profiles/run.py index db46bd3c9d..4177718562 100644 --- a/scripts/build_cop_profiles/run.py +++ b/scripts/build_cop_profiles/run.py @@ -87,7 +87,6 @@ def get_cop( isentropic_compressor_efficiency=snakemake.params.heat_pump_cop_approximation_central_heating["isentropic_compressor_efficiency"], heat_loss=snakemake.params.heat_pump_cop_approximation_central_heating["heat_loss"], min_delta_t_lift=snakemake.params.heat_pump_cop_approximation_central_heating["min_delta_t_lift"], - min_delta_t_condenser=snakemake.params.heat_pump_cop_approximation_central_heating["min_delta_t_condenser"] ).approximate_cop() else: diff --git a/scripts/build_ptes_operations/ptes_temperature_approximator.py b/scripts/build_ptes_operations/ptes_temperature_approximator.py index d8f65e16f0..7e58baf042 100644 --- a/scripts/build_ptes_operations/ptes_temperature_approximator.py +++ b/scripts/build_ptes_operations/ptes_temperature_approximator.py @@ -19,17 +19,17 @@ class PtesTemperatureApproximator: return_temperature : xr.DataArray The return temperature profile from the district heating network. max_ptes_top_temperature : float - Maximum operational temperature of top layer in PTES, default 90°C. + Maximum operational temperature of top layer in PTES. min_ptes_bottom_temperature : float - Minimum operational temperature of bottom layer in PTES, default 35°C. + Minimum operational temperature of bottom layer in PTES. """ def __init__( self, forward_temperature: xr.DataArray, return_temperature: xr.DataArray, - max_ptes_top_temperature: float = 90, - min_ptes_bottom_temperature: float = 35, + max_ptes_top_temperature: float, + min_ptes_bottom_temperature: float, ): """ Initialize PtesTemperatureApproximator. @@ -41,9 +41,9 @@ def __init__( return_temperature : xr.DataArray The return temperature profile from the district heating network. max_ptes_top_temperature : float, optional - Maximum operational temperature of top layer in PTES, default 90°C. + Maximum operational temperature of top layer in PTES. min_ptes_bottom_temperature : float, optional - Minimum operational temperature of bottom layer in PTES, default 35°C. + Minimum operational temperature of bottom layer in PTES. """ self.forward_temperature = forward_temperature self.return_temperature = return_temperature diff --git a/scripts/build_ptes_operations/run.py b/scripts/build_ptes_operations/run.py index eff42fb049..b0e5993736 100644 --- a/scripts/build_ptes_operations/run.py +++ b/scripts/build_ptes_operations/run.py @@ -27,6 +27,7 @@ supplemental_heating: enable: max_top_temperature: + min_bottom_temperature: Inputs ------ From af3ddcc37d5bf5d706fad4ebde3682d66df2f6f4 Mon Sep 17 00:00:00 2001 From: TomKae00 Date: Thu, 5 Jun 2025 17:07:44 +0200 Subject: [PATCH 08/14] documentation: update configtables/sector --- doc/configtables/sector.csv | 1 - scripts/build_cop_profiles/run.py | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/doc/configtables/sector.csv b/doc/configtables/sector.csv index 131c616d53..604253d5c2 100644 --- a/doc/configtables/sector.csv +++ b/doc/configtables/sector.csv @@ -49,7 +49,6 @@ district_heating,--,, -- -- 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. -- -- min_delta_t_lift,--,float,Minimum feasible temperature lift for heat pumps assumed for approximation. --- -- min_delta_t_condenser,--,float,Minimum feasible temperature difference on the heat sink side of the heat pump condenser assumed for approximation. -- limited_heat_sources,--,Dictionary with names of limited heat sources (not air) for which data by Fraunhofer ISI (`Manz et al. 2024 ) is used, -- -- geothermal,-,Name of the heat source. Must be the same as in ``heat_pump_sources``, -- -- -- constant_temperature_celsius,°C,heat source temperature, diff --git a/scripts/build_cop_profiles/run.py b/scripts/build_cop_profiles/run.py index 4177718562..e5f49cbdb2 100644 --- a/scripts/build_cop_profiles/run.py +++ b/scripts/build_cop_profiles/run.py @@ -103,8 +103,7 @@ def get_cop( snakemake = mock_snakemake( "build_cop_profiles", - clusters=8, - planning_horizons=2030 + clusters=48, ) set_scenario_config(snakemake) From 6395193bc04a73220378e5d685721392b48e35cf Mon Sep 17 00:00:00 2001 From: TomKae00 Date: Thu, 5 Jun 2025 17:12:34 +0200 Subject: [PATCH 09/14] code: fix test settings --- config/test/config.myopic.yaml | 2 +- config/test/config.overnight.yaml | 2 +- config/test/config.perfect.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config/test/config.myopic.yaml b/config/test/config.myopic.yaml index c709ec5747..c27a89e372 100644 --- a/config/test/config.myopic.yaml +++ b/config/test/config.myopic.yaml @@ -39,7 +39,7 @@ sector: ptes: supplemental_heating: enable: true - booster_technologies: ["heat_pump", "resistive_heater"] + booster_technologies: ["heat_pump", "resistive_heaters"] heat_pump_sources: urban central: - ptes diff --git a/config/test/config.overnight.yaml b/config/test/config.overnight.yaml index 7b610e17bc..2de5ad1aaa 100644 --- a/config/test/config.overnight.yaml +++ b/config/test/config.overnight.yaml @@ -65,7 +65,7 @@ sector: ptes: supplemental_heating: enable: true - booster_technologies: ["heat_pump", "resistive_heater"] + booster_technologies: ["heat_pump", "resistive_heaters"] ates: enable: true heat_pump_sources: diff --git a/config/test/config.perfect.yaml b/config/test/config.perfect.yaml index adb8b93c9e..d7dc55d4ca 100644 --- a/config/test/config.perfect.yaml +++ b/config/test/config.perfect.yaml @@ -48,7 +48,7 @@ sector: ptes: supplemental_heating: enable: true - booster_technologies: ["heat_pump", "resistive_heater"] + booster_technologies: ["heat_pump", "resistive_heaters"] ates: enable: true heat_pump_sources: From 56e6a4fe713c82110e31edd7825645f44f7615a5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 5 Jun 2025 15:13:35 +0000 Subject: [PATCH 10/14] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- config/test/config.myopic.yaml | 2 +- config/test/config.overnight.yaml | 2 +- config/test/config.perfect.yaml | 2 +- rules/build_sector.smk | 10 +- .../build_cop_profiles/BaseCopApproximator.py | 2 +- .../CentralHeatingCopApproximator.py | 14 ++- .../DecentralHeatingCopApproximator.py | 4 +- scripts/build_cop_profiles/run.py | 34 +++-- .../ptes_temperature_approximator.py | 8 +- scripts/prepare_sector_network.py | 117 +++++++++++------- 10 files changed, 117 insertions(+), 78 deletions(-) diff --git a/config/test/config.myopic.yaml b/config/test/config.myopic.yaml index c27a89e372..69a68a1d2a 100644 --- a/config/test/config.myopic.yaml +++ b/config/test/config.myopic.yaml @@ -42,7 +42,7 @@ sector: booster_technologies: ["heat_pump", "resistive_heaters"] heat_pump_sources: urban central: - - ptes + - ptes electricity: extendable_carriers: diff --git a/config/test/config.overnight.yaml b/config/test/config.overnight.yaml index 2de5ad1aaa..5a40f59f87 100644 --- a/config/test/config.overnight.yaml +++ b/config/test/config.overnight.yaml @@ -70,7 +70,7 @@ sector: enable: true heat_pump_sources: urban central: - - ptes + - ptes industry: diff --git a/config/test/config.perfect.yaml b/config/test/config.perfect.yaml index d7dc55d4ca..1817adf863 100644 --- a/config/test/config.perfect.yaml +++ b/config/test/config.perfect.yaml @@ -53,7 +53,7 @@ sector: enable: true heat_pump_sources: urban central: - - ptes + - ptes atlite: diff --git a/rules/build_sector.smk b/rules/build_sector.smk index a06597726c..187dcf018a 100755 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -1391,15 +1391,15 @@ rule prepare_sector_network: )(w) else [] ), - ptes_temperature_boost_ratio_profiles= lambda w: ( + ptes_temperature_boost_ratio_profiles=lambda w: ( resources( - "ptes_temperature_boost_ratio_profiles_base_s_{clusters}_{planning_horizons}.nc" + "ptes_temperature_boost_ratio_profiles_base_s_{clusters}_{planning_horizons}.nc" ) if config_provider( - "sector","district_heating","ptes","supplemental_heating","enable" + "sector", "district_heating", "ptes", "supplemental_heating", "enable" )(w) - else[] - ), + else [] + ), solar_thermal_total=lambda w: ( resources("solar_thermal_total_base_s_{clusters}.nc") if config_provider("sector", "solar_thermal")(w) diff --git a/scripts/build_cop_profiles/BaseCopApproximator.py b/scripts/build_cop_profiles/BaseCopApproximator.py index 46e1cf8544..c3b93d26fb 100644 --- a/scripts/build_cop_profiles/BaseCopApproximator.py +++ b/scripts/build_cop_profiles/BaseCopApproximator.py @@ -111,4 +111,4 @@ def logarithmic_mean( t_hot == t_cold, t_hot, (t_hot - t_cold) / np.log(t_hot / t_cold), - ) \ No newline at end of file + ) diff --git a/scripts/build_cop_profiles/CentralHeatingCopApproximator.py b/scripts/build_cop_profiles/CentralHeatingCopApproximator.py index df96c3fb3f..edfe165509 100644 --- a/scripts/build_cop_profiles/CentralHeatingCopApproximator.py +++ b/scripts/build_cop_profiles/CentralHeatingCopApproximator.py @@ -108,7 +108,7 @@ def __init__( source_inlet_temperature_celsius: Union[xr.DataArray, np.array], sink_inlet_temperature_celsius: Union[xr.DataArray, np.array], source_outlet_temperature_celsius: Union[xr.DataArray, np.array], - refrigerant : str, + refrigerant: str, delta_t_pinch_point: float, isentropic_compressor_efficiency: float, heat_loss: float, @@ -373,7 +373,11 @@ def _approximate_delta_t_refrigerant_sink( ) return ( a[refrigerant] - * (self.t_sink_out_kelvin - self.t_source_out_kelvin + 2 * self.delta_t_pinch) + * ( + self.t_sink_out_kelvin + - self.t_source_out_kelvin + + 2 * self.delta_t_pinch + ) + b[refrigerant] * self.delta_t_sink + c[refrigerant] ) @@ -417,7 +421,11 @@ def _ratio_evaporation_compression_work_approximation( ) return ( a[refrigerant] - * (self.t_sink_out_kelvin - self.t_source_out_kelvin + 2 * self.delta_t_pinch) + * ( + self.t_sink_out_kelvin + - self.t_source_out_kelvin + + 2 * self.delta_t_pinch + ) + b[refrigerant] * self.delta_t_sink + c[refrigerant] ) diff --git a/scripts/build_cop_profiles/DecentralHeatingCopApproximator.py b/scripts/build_cop_profiles/DecentralHeatingCopApproximator.py index 8687de0f74..741b2ea281 100644 --- a/scripts/build_cop_profiles/DecentralHeatingCopApproximator.py +++ b/scripts/build_cop_profiles/DecentralHeatingCopApproximator.py @@ -62,7 +62,9 @@ def __init__( The source of the heat pump. Must be either 'air' or 'ground'. """ - self.delta_t = sink_outlet_temperature_celsius - source_inlet_temperature_celsius + self.delta_t = ( + sink_outlet_temperature_celsius - source_inlet_temperature_celsius + ) if source_type not in ["air", "ground"]: raise ValueError("'source_type' must be one of ['air', 'ground']") else: diff --git a/scripts/build_cop_profiles/run.py b/scripts/build_cop_profiles/run.py index e5f49cbdb2..df5765bc9b 100644 --- a/scripts/build_cop_profiles/run.py +++ b/scripts/build_cop_profiles/run.py @@ -82,11 +82,21 @@ def get_cop( source_inlet_temperature_celsius=source_inlet_temperature_celsius, source_outlet_temperature_celsius=source_inlet_temperature_celsius - snakemake.params.heat_source_cooling_central_heating, - refrigerant=snakemake.params.heat_pump_cop_approximation_central_heating["refrigerant"], - delta_t_pinch_point=snakemake.params.heat_pump_cop_approximation_central_heating["heat_exchanger_pinch_point_temperature_difference"], - isentropic_compressor_efficiency=snakemake.params.heat_pump_cop_approximation_central_heating["isentropic_compressor_efficiency"], - heat_loss=snakemake.params.heat_pump_cop_approximation_central_heating["heat_loss"], - min_delta_t_lift=snakemake.params.heat_pump_cop_approximation_central_heating["min_delta_t_lift"], + refrigerant=snakemake.params.heat_pump_cop_approximation_central_heating[ + "refrigerant" + ], + delta_t_pinch_point=snakemake.params.heat_pump_cop_approximation_central_heating[ + "heat_exchanger_pinch_point_temperature_difference" + ], + isentropic_compressor_efficiency=snakemake.params.heat_pump_cop_approximation_central_heating[ + "isentropic_compressor_efficiency" + ], + heat_loss=snakemake.params.heat_pump_cop_approximation_central_heating[ + "heat_loss" + ], + min_delta_t_lift=snakemake.params.heat_pump_cop_approximation_central_heating[ + "min_delta_t_lift" + ], ).approximate_cop() else: @@ -121,17 +131,15 @@ def get_cop( for heat_source in heat_sources: if heat_source == "ptes": source_inlet_temperature_celsius = xr.open_dataarray( - snakemake.input[ - f"temp_air_total" - ] + snakemake.input["temp_air_total"] ) sink_inlet_temperature_celsius = xr.open_dataarray( - snakemake.input[ - f"temp_{heat_source}_total" - ] + snakemake.input[f"temp_{heat_source}_total"] ) - elif heat_source in ["ground", "air", - ]: + elif heat_source in [ + "ground", + "air", + ]: source_inlet_temperature_celsius = xr.open_dataarray( snakemake.input[ f"temp_{heat_source.replace('ground', 'soil')}_total" diff --git a/scripts/build_ptes_operations/ptes_temperature_approximator.py b/scripts/build_ptes_operations/ptes_temperature_approximator.py index 7e58baf042..a80a34fbae 100644 --- a/scripts/build_ptes_operations/ptes_temperature_approximator.py +++ b/scripts/build_ptes_operations/ptes_temperature_approximator.py @@ -107,7 +107,6 @@ def e_max_pu(self) -> xr.DataArray: ) return normalized_delta_t.clip(min=0) # Ensure non-negative values - @property def temperature_boost_ratio(self) -> xr.DataArray: """ @@ -120,7 +119,6 @@ def temperature_boost_ratio(self) -> xr.DataArray: xr.DataArray The resulting fraction of PTES charge that must be further heated. """ - return ( - (self.forward_temperature - self.top_temperature) - / (self.top_temperature - self.return_temperature) - ) \ No newline at end of file + return (self.forward_temperature - self.top_temperature) / ( + self.top_temperature - self.return_temperature + ) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index f509e591e5..9e95e292a8 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3082,15 +3082,16 @@ def add_heat( bus2=nodes + f" {heat_system} water pits boosting", carrier=f"{heat_system} water pits discharger", efficiency=costs.at[ - "central water pit discharger", - "efficiency", - ] - * ptes_supplemental_heating_required, + "central water pit discharger", + "efficiency", + ] + * ptes_supplemental_heating_required, efficiency2=costs.at[ - "central water pit discharger", - "efficiency", - ] - * (ptes_supplemental_heating_required - 1) * (- 1), + "central water pit discharger", + "efficiency", + ] + * (ptes_supplemental_heating_required - 1) + * (-1), p_nom_extendable=True, lifetime=costs.at["central water pit storage", "lifetime"], ) @@ -3103,19 +3104,19 @@ def add_heat( ptes_supplemental_heating_required = 1 n.add( - "Link", - nodes, - suffix=f" {heat_system} water pits discharger", - bus0=nodes + f" {heat_system} water pits", - bus1=nodes + f" {heat_system} heat", - carrier=f"{heat_system} water pits discharger", - efficiency=costs.at[ - "central water pit discharger", - "efficiency", - ] - * ptes_supplemental_heating_required, - p_nom_extendable=True, - lifetime=costs.at["central water pit storage", "lifetime"], + "Link", + nodes, + suffix=f" {heat_system} water pits discharger", + bus0=nodes + f" {heat_system} water pits", + bus1=nodes + f" {heat_system} heat", + carrier=f"{heat_system} water pits discharger", + efficiency=costs.at[ + "central water pit discharger", + "efficiency", + ] + * ptes_supplemental_heating_required, + p_nom_extendable=True, + lifetime=costs.at["central water pit storage", "lifetime"], ) n.links.loc[ nodes + f" {heat_system} water pits charger", @@ -3304,7 +3305,10 @@ def add_heat( not options["district_heating"]["ptes"]["supplemental_heating"][ "enable" ] - and "heat_pump" in options["district_heating"]["ptes"]["supplemental_heating"]["booster_technologies"] + and "heat_pump" + in options["district_heating"]["ptes"]["supplemental_heating"][ + "booster_technologies" + ] ): raise ValueError( "Supplemental heating: 'booster_technologies' contains 'heat_pump', but 'enable' is false." @@ -3315,14 +3319,17 @@ def add_heat( and options["district_heating"]["ptes"]["supplemental_heating"][ "enable" ] - and "heat_pump" in options["district_heating"]["ptes"]["supplemental_heating"]["booster_technologies"] + and "heat_pump" + in options["district_heating"]["ptes"]["supplemental_heating"][ + "booster_technologies" + ] ): ptes_temperature_boost_ratio = ( - xr.open_dataarray(ptes_temperature_boost_ratio_profile_file) - .sel(name=nodes) - .to_pandas() - .reindex(index=n.snapshots) - ) + xr.open_dataarray(ptes_temperature_boost_ratio_profile_file) + .sel(name=nodes) + .to_pandas() + .reindex(index=n.snapshots) + ) n.add( "Link", @@ -3332,8 +3339,12 @@ def add_heat( bus1=nodes + f" {heat_system} water pits boosting", bus2=nodes + f" {heat_system} heat", carrier=f"{heat_system} {heat_source} heat pump", - efficiency=-(cop_heat_pump / ptes_temperature_boost_ratio).where(ptes_temperature_boost_ratio > 0, 0.0), - efficiency2=(cop_heat_pump * (1 + (1 / ptes_temperature_boost_ratio))).where(ptes_temperature_boost_ratio > 0, 0.0), + efficiency=-(cop_heat_pump / ptes_temperature_boost_ratio).where( + ptes_temperature_boost_ratio > 0, 0.0 + ), + efficiency2=( + cop_heat_pump * (1 + (1 / ptes_temperature_boost_ratio)) + ).where(ptes_temperature_boost_ratio > 0, 0.0), capital_cost=costs.at[costs_name_heat_pump, "efficiency"] * costs.at[costs_name_heat_pump, "capital_cost"] * overdim_factor, @@ -3375,43 +3386,55 @@ def add_heat( ) if ( - not options["district_heating"]["ptes"]["supplemental_heating"]["enable"] - and "resistive_heaters" in options["district_heating"]["ptes"]["supplemental_heating"][ - "booster_technologies"] + not options["district_heating"]["ptes"]["supplemental_heating"][ + "enable" + ] + and "resistive_heaters" + in options["district_heating"]["ptes"]["supplemental_heating"][ + "booster_technologies" + ] ): raise ValueError( "Supplemental heating: 'booster_technologies' contains 'resistive_heaters', but 'enable' is false." ) if ( - "resistive_heaters" in options["district_heating"]["ptes"]["supplemental_heating"][ - "booster_technologies"] + "resistive_heaters" + in options["district_heating"]["ptes"]["supplemental_heating"][ + "booster_technologies" + ] and heat_system == HeatSystem.URBAN_CENTRAL ): ptes_temperature_boost_ratio = ( - xr.open_dataarray(ptes_temperature_boost_ratio_profile_file) - .sel(name=nodes) - .to_pandas() - .reindex(index=n.snapshots) - ) + xr.open_dataarray(ptes_temperature_boost_ratio_profile_file) + .sel(name=nodes) + .to_pandas() + .reindex(index=n.snapshots) + ) n.add( "Link", nodes, suffix=f" {heat_system} ptes resistive heater", bus0=nodes, - bus1=nodes + f" {heat_system} water pits boosting", #boosting + bus1=nodes + f" {heat_system} water pits boosting", # boosting bus2=nodes + f" {heat_system} heat", carrier=f"{heat_system} resistive heater", - efficiency=-(costs.at[key, "efficiency"] / ptes_temperature_boost_ratio).where(ptes_temperature_boost_ratio > 0, 0.0), - efficiency2=(costs.at[key, "efficiency"]) * (1 + (1 / ptes_temperature_boost_ratio)).where(ptes_temperature_boost_ratio > 0, 0.0), - capital_cost=(costs.at[key, "efficiency"] - * costs.at[key, "capital_cost"] - * overdim_factor), + efficiency=-( + costs.at[key, "efficiency"] / ptes_temperature_boost_ratio + ).where(ptes_temperature_boost_ratio > 0, 0.0), + efficiency2=(costs.at[key, "efficiency"]) + * (1 + (1 / ptes_temperature_boost_ratio)).where( + ptes_temperature_boost_ratio > 0, 0.0 + ), + capital_cost=( + costs.at[key, "efficiency"] + * costs.at[key, "capital_cost"] + * overdim_factor + ), p_nom_extendable=True, lifetime=costs.at[key, "lifetime"], ) - if options["boilers"]: key = f"{heat_system.central_or_decentral} gas boiler" From 315d814d74536847465fe104a9f616663c026418 Mon Sep 17 00:00:00 2001 From: TomKae00 Date: Thu, 5 Jun 2025 17:14:56 +0200 Subject: [PATCH 11/14] fix: delete min_delta_t_condenser in configs --- config/config.default.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 85d2ac511f..c6014500f9 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -507,7 +507,6 @@ sector: isentropic_compressor_efficiency: 0.8 heat_loss: 0.0 min_delta_t_lift: 10 #K - min_delta_t_condenser: 5 #K limited_heat_sources: geothermal: constant_temperature_celsius: 65 From 5b3e5f09325e65d085c8f70b34ef52531edbfa2a Mon Sep 17 00:00:00 2001 From: TomKae00 Date: Wed, 11 Jun 2025 11:24:53 +0200 Subject: [PATCH 12/14] fix: remove nominal efficiency from hp ivest costs --- config/config.default.yaml | 6 ++- scripts/prepare_sector_network.py | 74 ++++++++++++++++++------------- 2 files changed, 46 insertions(+), 34 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index c6014500f9..78c2f95eef 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -485,8 +485,8 @@ sector: ptes: dynamic_capacity: true supplemental_heating: - enable: false - booster_technologies: [] + enable: true + booster_technologies: ["heat_pump", "resistive_heaters"] max_top_temperature: 90 min_bottom_temperature: 35 ates: @@ -517,7 +517,9 @@ sector: - ptes heat_pump_sources: urban central: + - ground - air + - ptes urban decentral: - air rural: diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 9e95e292a8..83d23cbd35 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1576,7 +1576,7 @@ def insert_electricity_distribution_grid( n.links.loc[v2gs, "bus1"] += " low voltage" hps = n.links.index[n.links.carrier.str.contains("heat pump")] - n.links.loc[hps, "bus0"] += " low voltage" + n.links.loc[hps, "bus1"] += " low voltage" rh = n.links.index[n.links.carrier.str.contains("resistive heater")] n.links.loc[rh, "bus0"] += " low voltage" @@ -3066,7 +3066,7 @@ def add_heat( unit="MWh_th", ) - ptes_supplemental_heating_required = ( + ptes_direct_utilisiation_profile = ( xr.open_dataarray(ptes_direct_utilisation_profile_file) .sel(name=nodes) .to_pandas() @@ -3085,12 +3085,12 @@ def add_heat( "central water pit discharger", "efficiency", ] - * ptes_supplemental_heating_required, + * ptes_direct_utilisiation_profile, efficiency2=costs.at[ "central water pit discharger", "efficiency", ] - * (ptes_supplemental_heating_required - 1) + * (ptes_direct_utilisiation_profile - 1) * (-1), p_nom_extendable=True, lifetime=costs.at["central water pit storage", "lifetime"], @@ -3101,7 +3101,7 @@ def add_heat( ] = energy_to_power_ratio_water_pit else: - ptes_supplemental_heating_required = 1 + ptes_direct_utilisiation_profile = 1 n.add( "Link", @@ -3114,7 +3114,7 @@ def add_heat( "central water pit discharger", "efficiency", ] - * ptes_supplemental_heating_required, + * ptes_direct_utilisiation_profile, p_nom_extendable=True, lifetime=costs.at["central water pit storage", "lifetime"], ) @@ -3266,14 +3266,19 @@ def add_heat( "Link", nodes, suffix=f" {heat_system} {heat_source} heat pump", - bus0=nodes, - bus1=nodes + f" {heat_carrier}", - bus2=nodes + f" {heat_system} heat", + bus0=nodes + f" {heat_system} heat", + bus1=nodes, + bus2=nodes + f" {heat_carrier}", carrier=f"{heat_system} {heat_source} heat pump", - efficiency=(-(cop_heat_pump - 1)).clip(upper=0), - efficiency2=cop_heat_pump, - capital_cost=costs.at[costs_name_heat_pump, "efficiency"] - * costs.at[costs_name_heat_pump, "capital_cost"] + efficiency=(1 / cop_heat_pump).where( + cop_heat_pump > 0, 0.0 + ), + efficiency2=((cop_heat_pump - 1) / cop_heat_pump).where( + cop_heat_pump > 0, 0.0 + ), + p_min_pu=-1, + p_max_pu=0, + capital_cost=costs.at[costs_name_heat_pump, "capital_cost"] * overdim_factor, p_nom_extendable=True, lifetime=costs.at[costs_name_heat_pump, "lifetime"], @@ -3335,19 +3340,21 @@ def add_heat( "Link", nodes, suffix=f" {heat_system} {heat_source} heat pump", - bus0=nodes, - bus1=nodes + f" {heat_system} water pits boosting", - bus2=nodes + f" {heat_system} heat", + bus0=nodes + f" {heat_system} heat", + bus1=nodes, + bus2=nodes + f" {heat_system} water pits boosting", carrier=f"{heat_system} {heat_source} heat pump", - efficiency=-(cop_heat_pump / ptes_temperature_boost_ratio).where( + efficiency=( + ptes_temperature_boost_ratio / ((1 + ptes_temperature_boost_ratio) * cop_heat_pump) + ).where((ptes_temperature_boost_ratio > 0) & (cop_heat_pump > 0), 0.0), + efficiency2=(1 / (1 + ptes_temperature_boost_ratio)).where( ptes_temperature_boost_ratio > 0, 0.0 ), - efficiency2=( - cop_heat_pump * (1 + (1 / ptes_temperature_boost_ratio)) - ).where(ptes_temperature_boost_ratio > 0, 0.0), - capital_cost=costs.at[costs_name_heat_pump, "efficiency"] - * costs.at[costs_name_heat_pump, "capital_cost"] - * overdim_factor, + p_min_pu=(ptes_direct_utilisiation_profile - 1), + p_max_pu=0, + capital_cost=costs.at[costs_name_heat_pump, "capital_cost"] + * overdim_factor + * (ptes_temperature_boost_ratio / (1 + ptes_temperature_boost_ratio)).max(), p_nom_extendable=True, lifetime=costs.at[costs_name_heat_pump, "lifetime"], ) @@ -3357,12 +3364,15 @@ def add_heat( "Link", nodes, suffix=f" {heat_system} {heat_source} heat pump", - bus0=nodes, - bus1=nodes + f" {heat_system} heat", + bus0=nodes + f" {heat_system} heat", + bus1=nodes, carrier=f"{heat_system} {heat_source} heat pump", - efficiency=cop_heat_pump, - capital_cost=costs.at[costs_name_heat_pump, "efficiency"] - * costs.at[costs_name_heat_pump, "capital_cost"] + efficiency=(1 / cop_heat_pump).where( + cop_heat_pump > 0, 0.0 + ), + p_min_pu=-1, + p_max_pu=0, + capital_cost=costs.at[costs_name_heat_pump, "capital_cost"] * overdim_factor, p_nom_extendable=True, lifetime=costs.at[costs_name_heat_pump, "lifetime"], @@ -3416,9 +3426,9 @@ def add_heat( nodes, suffix=f" {heat_system} ptes resistive heater", bus0=nodes, - bus1=nodes + f" {heat_system} water pits boosting", # boosting + bus1=nodes + f" {heat_system} water pits boosting", bus2=nodes + f" {heat_system} heat", - carrier=f"{heat_system} resistive heater", + carrier=f"{heat_system} ptes resistive heater", efficiency=-( costs.at[key, "efficiency"] / ptes_temperature_boost_ratio ).where(ptes_temperature_boost_ratio > 0, 0.0), @@ -6238,9 +6248,9 @@ def add_import_options( snakemake = mock_snakemake( "prepare_sector_network", opts="", - clusters="10", + clusters="8", sector_opts="", - planning_horizons="2050", + planning_horizons="2030", ) configure_logging(snakemake) # pylint: disable=E0606 From c937fe0c7ad0a7390d86ae036bbc63451ea6c0b4 Mon Sep 17 00:00:00 2001 From: TomKae00 Date: Wed, 11 Jun 2025 15:38:52 +0200 Subject: [PATCH 13/14] add release note and set correct config.default parameters --- config/config.default.yaml | 6 ++---- doc/release_notes.rst | 2 ++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/config/config.default.yaml b/config/config.default.yaml index 78c2f95eef..c6014500f9 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -485,8 +485,8 @@ sector: ptes: dynamic_capacity: true supplemental_heating: - enable: true - booster_technologies: ["heat_pump", "resistive_heaters"] + enable: false + booster_technologies: [] max_top_temperature: 90 min_bottom_temperature: 35 ates: @@ -517,9 +517,7 @@ sector: - ptes heat_pump_sources: urban central: - - ground - air - - ptes urban decentral: - air rural: diff --git a/doc/release_notes.rst b/doc/release_notes.rst index dc2e288f2c..55fed8dfdc 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -29,6 +29,8 @@ Release Notes **Changes** +* Fix: Heat pump investment costs now calculated on the heat bus directly, with the nominal efficiency factor removed + * Added resistive heaters as a temperature boosting technology for PTES. Additionally, the heat pump booster now uses ambient air as its heat source for boosting. * Introduce the ability to use the bidding zones as administrative zones for the clustering (https://github.com/PyPSA/pypsa-eur/pull/1578). This also introduces the ability to create a custom `busmap` from custom `busshapes`. To use bidding zones as clustering mode, a `bz` mode has been introduced for `administrative` clustering. This feature is compatible with the general NUTS clustering approach. Custom `busshapes` must be provided as `data/busshapes/base_s_{clusters}_{base_network}.geojson`. From 102e4f640b0be98e923012973848b99ab5d14fb6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 11 Jun 2025 15:29:58 +0000 Subject: [PATCH 14/14] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/prepare_sector_network.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 83d23cbd35..cddf4bc6d9 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3270,9 +3270,7 @@ def add_heat( bus1=nodes, bus2=nodes + f" {heat_carrier}", carrier=f"{heat_system} {heat_source} heat pump", - efficiency=(1 / cop_heat_pump).where( - cop_heat_pump > 0, 0.0 - ), + efficiency=(1 / cop_heat_pump).where(cop_heat_pump > 0, 0.0), efficiency2=((cop_heat_pump - 1) / cop_heat_pump).where( cop_heat_pump > 0, 0.0 ), @@ -3345,8 +3343,11 @@ def add_heat( bus2=nodes + f" {heat_system} water pits boosting", carrier=f"{heat_system} {heat_source} heat pump", efficiency=( - ptes_temperature_boost_ratio / ((1 + ptes_temperature_boost_ratio) * cop_heat_pump) - ).where((ptes_temperature_boost_ratio > 0) & (cop_heat_pump > 0), 0.0), + ptes_temperature_boost_ratio + / ((1 + ptes_temperature_boost_ratio) * cop_heat_pump) + ).where( + (ptes_temperature_boost_ratio > 0) & (cop_heat_pump > 0), 0.0 + ), efficiency2=(1 / (1 + ptes_temperature_boost_ratio)).where( ptes_temperature_boost_ratio > 0, 0.0 ), @@ -3354,7 +3355,10 @@ def add_heat( p_max_pu=0, capital_cost=costs.at[costs_name_heat_pump, "capital_cost"] * overdim_factor - * (ptes_temperature_boost_ratio / (1 + ptes_temperature_boost_ratio)).max(), + * ( + ptes_temperature_boost_ratio + / (1 + ptes_temperature_boost_ratio) + ).max(), p_nom_extendable=True, lifetime=costs.at[costs_name_heat_pump, "lifetime"], ) @@ -3367,9 +3371,7 @@ def add_heat( bus0=nodes + f" {heat_system} heat", bus1=nodes, carrier=f"{heat_system} {heat_source} heat pump", - efficiency=(1 / cop_heat_pump).where( - cop_heat_pump > 0, 0.0 - ), + efficiency=(1 / cop_heat_pump).where(cop_heat_pump > 0, 0.0), p_min_pu=-1, p_max_pu=0, capital_cost=costs.at[costs_name_heat_pump, "capital_cost"]