# -*- coding: utf-8 -*- # SPDX-FileCopyrightText: : 2020-2024 The PyPSA-Eur Authors # # SPDX-License-Identifier: MIT """ Build coefficient of performance (COP) time series for air- or ground-sourced heat pumps. The COP is approximated as a quatratic function of the temperature difference between source and sink, based on Staffell et al. 2012. This rule is executed in ``build_sector.smk``. Relevant Settings ----------------- .. code:: yaml heat_pump_sink_T: Inputs: ------- - ``resources//temp_soil_total_elec_s_.nc``: Soil temperature (total) time series. - ``resources//temp_soil_rural_elec_s_.nc``: Soil temperature (rural) time series. - ``resources//temp_soil_urban_elec_s_.nc``: Soil temperature (urban) time series. - ``resources//temp_air_total_elec_s_.nc``: Ambient air temperature (total) time series. - ``resources//temp_air_rural_elec_s_.nc``: Ambient air temperature (rural) time series. - ``resources//temp_air_urban_elec_s_.nc``: Ambient air temperature (urban) time series. Outputs: -------- - ``resources/cop_soil_total_elec_s_.nc``: COP (ground-sourced) time series (total). - ``resources/cop_soil_rural_elec_s_.nc``: COP (ground-sourced) time series (rural). - ``resources/cop_soil_urban_elec_s_.nc``: COP (ground-sourced) time series (urban). - ``resources/cop_air_total_elec_s_.nc``: COP (air-sourced) time series (total). - ``resources/cop_air_rural_elec_s_.nc``: COP (air-sourced) time series (rural). - ``resources/cop_air_urban_elec_s_.nc``: COP (air-sourced) time series (urban). References ---------- [1] Staffell et al., Energy & Environmental Science 11 (2012): A review of domestic heat pumps, https://doi.org/10.1039/C2EE22653G. """ from typing import Union from enum import Enum import xarray as xr import numpy as np from _helpers import set_scenario_config def coefficient_of_performance_individual_heating(delta_T, source="air"): if source == "air": return 6.81 - 0.121 * delta_T + 0.000630 * delta_T**2 elif source == "soil": return 8.77 - 0.150 * delta_T + 0.000734 * delta_T**2 else: raise NotImplementedError("'source' must be one of ['air', 'soil']") def celsius_to_kelvin(t_celsius: Union[float, xr.DataArray, np.array]) -> Union[float, xr.DataArray, np.array]: if (np.asarray(t_celsius) > 200).any(): raise ValueError("t_celsius > 200. Are you sure you are using the right units?") return t_celsius + 273.15 def logarithmic_mean(t_hot: Union[float, xr.DataArray, np.ndarray], t_cold: Union[float, xr.DataArray, np.ndarray]) -> Union[float, xr.DataArray, np.ndarray]: 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) class CopDistrictHeating: def __init__( self, forward_temperature_celsius: Union[xr.DataArray, np.array], source_inlet_temperature_celsius: Union[xr.DataArray, np.array], return_temperature_celsius: Union[xr.DataArray, np.array], source_outlet_temperature_celsius: Union[xr.DataArray, np.array], delta_t_pinch_point: float = 5, isentropic_compressor_efficiency: float = 0.8, heat_loss: float = 0.0, ) -> None: """ Initialize the COPProfileBuilder object. Parameters: ---------- forward_temperature_celsius : Union[xr.DataArray, np.array] The forward temperature in Celsius. return_temperature_celsius : Union[xr.DataArray, np.array] The return temperature in Celsius. source_inlet_temperature_celsius : Union[xr.DataArray, np.array] The source inlet temperature in Celsius. source_outlet_temperature_celsius : Union[xr.DataArray, np.array] The source outlet temperature in Celsius. delta_t_pinch_point : float, optional The pinch point temperature difference, by default 5. isentropic_compressor_efficiency : float, optional The isentropic compressor efficiency, by default 0.8. heat_loss : float, optional The heat loss, by default 0.0. """ self.t_source_in = celsius_to_kelvin(source_inlet_temperature_celsius) self.t_sink_out = celsius_to_kelvin(forward_temperature_celsius) self.t_sink_in = celsius_to_kelvin(return_temperature_celsius) self.t_source_out = celsius_to_kelvin(source_outlet_temperature_celsius) self.isentropic_efficiency_compressor = isentropic_compressor_efficiency self.heat_loss = heat_loss self.delta_t_pinch = delta_t_pinch_point def cop(self) -> Union[xr.DataArray, np.array]: """ Calculate the coefficient of performance (COP) for the system. Returns: Union[xr.DataArray, np.array]: The calculated COP values. """ return ( self.ideal_lorenz_cop * ( ( 1 + (self.delta_t_refrigerant_sink + self.delta_t_pinch) / self.t_sink_mean ) / ( 1 + ( self.delta_t_refrigerant_sink + self.delta_t_refrigerant_source + 2 * self.delta_t_pinch ) / self.delta_t_lift ) ) * self.isentropic_efficiency_compressor * (1 - self.ratio_evaporation_compression_work) + 1 - self.isentropic_efficiency_compressor - self.heat_loss ) @property def t_sink_mean(self) -> Union[xr.DataArray, np.array]: """ Calculate the logarithmic mean temperature difference between the cold and hot sinks. Returns ------- Union[xr.DataArray, np.array] The mean temperature difference. """ return logarithmic_mean(t_cold=self.t_sink_in, t_hot=self.t_sink_out) @property def t_source_mean(self) -> Union[xr.DataArray, np.array]: """ Calculate the logarithmic mean temperature of the heat source. Returns ------- Union[xr.DataArray, np.array] The mean temperature of the heat source. """ return logarithmic_mean(t_hot=self.t_source_in, t_cold=self.t_source_out) @property def delta_t_lift(self) -> Union[xr.DataArray, np.array]: """ Calculate the temperature lift as the difference between the logarithmic sink and source temperatures. Returns ------- Union[xr.DataArray, np.array] The temperature difference between the sink and source. """ return self.t_sink_mean - self.t_source_mean @property def ideal_lorenz_cop(self) -> Union[xr.DataArray, np.array]: """ Ideal Lorenz coefficient of performance (COP). The ideal Lorenz COP is calculated as the ratio of the mean sink temperature to the lift temperature difference. Returns ------- np.array The ideal Lorenz COP. """ return self.t_sink_mean / self.delta_t_lift @property def delta_t_refrigerant_source(self) -> Union[xr.DataArray, np.array]: """ Calculate the temperature difference between the refrigerant source inlet and outlet. Returns ------- Union[xr.DataArray, np.array] The temperature difference between the refrigerant source inlet and outlet. """ return self._approximate_delta_t_refrigerant_source( delta_t_source=self.t_source_in - self.t_source_out ) @property def delta_t_refrigerant_sink(self) -> Union[xr.DataArray, np.array]: """ Temperature difference between the refrigerant and the sink based on approximation. Returns ------- Union[xr.DataArray, np.array] The temperature difference between the refrigerant and the sink. """ return self._approximate_delta_t_refrigerant_sink() @property def ratio_evaporation_compression_work(self) -> Union[xr.DataArray, np.array]: """ Calculate the ratio of evaporation to compression work based on approximation. Returns ------- Union[xr.DataArray, np.array] The calculated ratio of evaporation to compression work. """ return self._ratio_evaporation_compression_work_approximation() @property def delta_t_sink(self) -> Union[xr.DataArray, np.array]: """ Calculate the temperature difference at the sink. Returns ------- Union[xr.DataArray, np.array] The temperature difference at the sink. """ return self.t_sink_out - self.t_sink_in def _approximate_delta_t_refrigerant_source( self, delta_t_source: Union[xr.DataArray, np.array] ) -> Union[xr.DataArray, np.array]: """ Approximates the temperature difference between the refrigerant and the source. Parameters ---------- delta_t_source : Union[xr.DataArray, np.array] The temperature difference for the refrigerant source. Returns ------- Union[xr.DataArray, np.array] The approximate temperature difference for the refrigerant source. """ return delta_t_source / 2 def _approximate_delta_t_refrigerant_sink( self, refrigerant: str = "ammonia", a: float = {"ammonia": 0.2, "isobutane": -0.0011}, b: float = {"ammonia": 0.2, "isobutane": 0.3}, c: float = {"ammonia": 0.016, "isobutane": 2.4}, ) -> Union[xr.DataArray, np.array]: """ Approximates the temperature difference at the 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. Returns: ------- Union[xr.DataArray, np.array] The approximate temperature difference at the refrigerant sink. Notes: ------ This function assumes ammonia as the refrigerant. The approximate temperature difference at the refrigerant sink is calculated using the following formula: a * (t_sink_out - t_source_out + 2 * delta_t_pinch) + b * delta_t_sink + c """ if refrigerant not in a.keys(): raise ValueError( f"Invalid refrigerant '{refrigerant}'. Must be one of {a.keys()}" ) return ( a[refrigerant] * (self.t_sink_out - self.t_source_out + 2 * self.delta_t_pinch) + b[refrigerant] * self.delta_t_sink + c[refrigerant] ) def _ratio_evaporation_compression_work_approximation( self, refrigerant: str = "ammonia", a: float = {"ammonia": 0.0014, "isobutane": 0.0035}, b: float = {"ammonia": -0.0015, "isobutane": -0.0033}, c: float = {"ammonia": 0.039, "isobutane": 0.053}, ) -> Union[xr.DataArray, np.array]: """ Calculate the ratio of evaporation to compression work approximation. Parameters: ---------- refrigerant : str, optional The refrigerant used in the system. Either 'isobutane' or 'ammonia. Default is 'ammonia'. a : float, optional Coefficient 'a' in the approximation equation. Default is 0.0014. b : float, optional Coefficient 'b' in the approximation equation. Default is -0.0015. c : float, optional Coefficient 'c' in the approximation equation. Default is 0.039. Returns: ------- Union[xr.DataArray, np.array] The calculated ratio of evaporation to compression work. Notes: ------ This function assumes ammonia as the refrigerant. The approximation equation used is: ratio = a * (t_sink_out - t_source_out + 2 * delta_t_pinch) + b * delta_t_sink + c """ if refrigerant not in a.keys(): raise ValueError( f"Invalid refrigerant '{refrigerant}'. Must be one of {a.keys()}" ) return ( a[refrigerant] * (self.t_sink_out - self.t_source_out + 2 * self.delta_t_pinch) + b[refrigerant] * self.delta_t_sink + c[refrigerant] ) if __name__ == "__main__": if "snakemake" not in globals(): from _helpers import mock_snakemake snakemake = mock_snakemake( "build_cop_profiles", simpl="", clusters=48, ) set_scenario_config(snakemake) for source in ["air", "soil"]: source_T = xr.open_dataarray(snakemake.input[f"temp_{source}_total"]) delta_T = snakemake.params.heat_pump_sink_T_individual_heating - source_T cop_individual_heating = coefficient_of_performance_individual_heating(delta_T, source) cop_individual_heating.to_netcdf(snakemake.output[f"cop_{source}_individual_heating"]) cop_district_heating = CopDistrictHeating( forward_temperature_celsius=snakemake.params.forward_temperature_district_heating, return_temperature_celsius=snakemake.params.return_temperature_district_heating, source_inlet_temperature_celsius=source_T, source_outlet_temperature_celsius=source_T - snakemake.params.heat_source_cooling_district_heating, ).cop() cop_district_heating.to_netcdf(snakemake.output[f"cop_{source}_district_heating"])