diff --git a/src/gsy_e/models/strategy/energy_parameters/heatpump/cop_models/cop_models.py b/src/gsy_e/models/strategy/energy_parameters/heatpump/cop_models/cop_models.py index 8f518cb9a..d358dd9ec 100644 --- a/src/gsy_e/models/strategy/energy_parameters/heatpump/cop_models/cop_models.py +++ b/src/gsy_e/models/strategy/energy_parameters/heatpump/cop_models/cop_models.py @@ -2,10 +2,11 @@ import os from abc import abstractmethod from enum import Enum -from typing import Optional from logging import getLogger -import sympy as sp +from typing import Optional +import sympy as sp +from gsy_framework.constants_limits import FLOATING_POINT_TOLERANCE from gsy_framework.enums import HeatPumpSourceType log = getLogger(__name__) @@ -19,6 +20,7 @@ class COPModelType(Enum): ELCO_AEROTOP_G07_14M = 2 HOVAL_ULTRASOURCE_B_COMFORT_C11 = 3 AERMEC_NXP_0600_4L_HEAT = 4 + AERMEC_NXP_0600_4L_COOL = 5 MODEL_FILE_DIR = os.path.join(os.path.dirname(__file__), "model_data") @@ -29,6 +31,7 @@ class COPModelType(Enum): COPModelType.HOVAL_ULTRASOURCE_B_COMFORT_C11: "hoval_UltraSource_B_comfort_C_11_model_" "parameters.json", COPModelType.AERMEC_NXP_0600_4L_HEAT: "AERMEC_NXP_0600_4L_HEAT_model_parameters.json", + COPModelType.AERMEC_NXP_0600_4L_COOL: "AERMEC_NXP_0600_4L_COOL_model_parameters.json", } @@ -40,8 +43,7 @@ def calc_cop( self, source_temp_C: float, condenser_temp_C: float, - heat_demand_kW: Optional[float], - electrical_demand_kW: Optional[float] = None, + heat_demand_kW: Optional[float] = None, ): """Return COP value for provided inputs""" @@ -67,8 +69,57 @@ def __init__(self, model_type: COPModelType): self._model = json.load(fp) self.model_type = model_type - def _calc_power(self, source_temp_C: float, condenser_temp_C: float, heat_demand_kW: float): - CAPFT = ( + def calc_q_from_p_kW( + self, + source_temp_C: float, + condenser_temp_C: float, + electrical_demand_kW: Optional[float] = None, + ) -> Optional[float]: + return self._resolve_heat( + source_temp_C=source_temp_C, + condenser_temp_C=condenser_temp_C, + electricity_demand_kW=electrical_demand_kW, + ) + + def calc_cop( + self, + source_temp_C: float, + condenser_temp_C: float, + heat_demand_kW: Optional[float] = None, + ): + heat_demand_kW = self._limit_heat_demand_kW(heat_demand_kW) + if heat_demand_kW < FLOATING_POINT_TOLERANCE: + return 0 + electrical_power_kW = self._calc_power(source_temp_C, condenser_temp_C, heat_demand_kW) + if electrical_power_kW <= 0: + log.error( + "calculated power is negative: " + "hp model: %s source_temp: %s, " + "condenser_temp: %s, heat_demand_kW: %s, calculated power: %s", + self.model_type.name, + round(source_temp_C, 2), + round(condenser_temp_C, 2), + round(heat_demand_kW, 2), + round(electrical_power_kW, 2), + ) + return 0 + cop = heat_demand_kW / electrical_power_kW + if cop > self._model["COP_max"] or cop < self._model["COP_min"]: + log.error( + "calculated COP (%s) is unrealistic: " + "hp model: %s source_temp: %s, " + "condenser_temp: %s, heat_demand_kW: %s, calculated power: %s", + round(cop, 2), + self.model_type.name, + round(source_temp_C, 2), + round(condenser_temp_C, 2), + round(heat_demand_kW, 2), + round(electrical_power_kW, 2), + ) + return cop + + def _capft(self, source_temp_C: float, condenser_temp_C: float): + return ( self._model["CAPFT"][0] + self._model["CAPFT"][1] * source_temp_C + self._model["CAPFT"][3] * source_temp_C**2 @@ -77,7 +128,8 @@ def _calc_power(self, source_temp_C: float, condenser_temp_C: float, heat_demand + self._model["CAPFT"][4] * source_temp_C * condenser_temp_C ) - HEIRFT = ( + def _heirft(self, source_temp_C: float, condenser_temp_C: float): + return ( self._model["HEIRFT"][0] + self._model["HEIRFT"][1] * source_temp_C + self._model["HEIRFT"][3] * source_temp_C**2 @@ -86,59 +138,43 @@ def _calc_power(self, source_temp_C: float, condenser_temp_C: float, heat_demand + self._model["HEIRFT"][4] * source_temp_C * condenser_temp_C ) - # Partial Load Ratio (PLR) - PLR = heat_demand_kW / (self._model["Qref"] * CAPFT) - - # HEIRFPLR calculation - HEIRFPLR = ( + def _heirfplr(self, plr): + return ( self._model["HEIRFPLR"][0] - + self._model["HEIRFPLR"][1] * PLR - + self._model["HEIRFPLR"][2] * PLR**2 + + self._model["HEIRFPLR"][1] * plr + + self._model["HEIRFPLR"][2] * plr**2 ) - # Power consumption (P) calculation - return self._model["Pref"] * CAPFT * HEIRFT * HEIRFPLR + def _calc_power(self, source_temp_C: float, condenser_temp_C: float, heat_demand_kW: float): + CAPFT = self._capft(source_temp_C, condenser_temp_C) + PLR = heat_demand_kW / (self._model["Qref"] * CAPFT) + + return ( + self._model["Pref"] + * CAPFT + * self._heirft(source_temp_C, condenser_temp_C) + * self._heirfplr(PLR) + ) def _resolve_heat( self, source_temp_C: float, condenser_temp_C: float, electricity_demand_kW: float ): - CAPFT = ( - self._model["CAPFT"][0] - + self._model["CAPFT"][1] * source_temp_C - + self._model["CAPFT"][3] * source_temp_C**2 - + self._model["CAPFT"][2] * condenser_temp_C - + self._model["CAPFT"][5] * condenser_temp_C**2 - + self._model["CAPFT"][4] * source_temp_C * condenser_temp_C - ) - - HEIRFT = ( - self._model["HEIRFT"][0] - + self._model["HEIRFT"][1] * source_temp_C - + self._model["HEIRFT"][3] * source_temp_C**2 - + self._model["HEIRFT"][2] * condenser_temp_C - + self._model["HEIRFT"][5] * condenser_temp_C**2 - + self._model["HEIRFT"][4] * source_temp_C * condenser_temp_C - ) - - # Partial Load Ratio (PLR) + CAPFT = self._capft(source_temp_C, condenser_temp_C) Q = sp.symbols("Q") PLR = Q / (self._model["Qref"] * CAPFT) - # HEIRFPLR calculation - HEIRFPLR = ( - self._model["HEIRFPLR"][0] - + self._model["HEIRFPLR"][1] * PLR - + self._model["HEIRFPLR"][2] * PLR**2 - ) - solutions = sp.solve( - sp.Eq(electricity_demand_kW, self._model["Pref"] * CAPFT * HEIRFT * HEIRFPLR), Q + sp.Eq( + electricity_demand_kW, + self._model["Pref"] + * CAPFT + * self._heirft(source_temp_C, condenser_temp_C) + * self._heirfplr(PLR), + ), + Q, ) - Q = self._select_Q_solution(solutions, CAPFT) - if Q is None: - # fallback: use median COP of training dataset to calculate Q - Q = self._model["COP_med"] * electricity_demand_kW - return Q + + return self._select_Q_solution(solutions, CAPFT) def _select_Q_solution(self, Q_solutions, CAPFT) -> Optional[float]: """ @@ -148,91 +184,39 @@ def _select_Q_solution(self, Q_solutions, CAPFT) -> Optional[float]: - the correct branch is the one with the LARGER PLR (as indicated by the training dataset) """ - PLR_dict = { q / (self._model["Qref"] * CAPFT): q for q in Q_solutions - if 0 <= q / (self._model["Qref"] * CAPFT) <= 1 + if isinstance(q, sp.core.numbers.Float) and 0 <= q / (self._model["Qref"] * CAPFT) <= 1 } if not PLR_dict: PLR_list = [q / (self._model["Qref"] * CAPFT) for q in Q_solutions] - log.error("IndividualCOPModel: No physically feasible PLR solutions. %s", PLR_list) + log.error( + "IndividualCOPModel: No physically feasible PLR solutions Q: %s, PLR: %s ", + Q_solutions, + PLR_list, + ) return None return float(PLR_dict[max(PLR_dict)]) def _limit_heat_demand_kW(self, heat_demand_kW: float) -> float: assert heat_demand_kW is not None, "heat demand should be provided" if heat_demand_kW > self._model["Q_max"]: - log.debug( - "calc_cop: heat demand exceeds maximum heat_demand_kW: %s", self._model["Q_max"] + log.error( + "calc_cop: heat demand (%s kW) exceeds maximum heat_demand_kW: %s", + heat_demand_kW, + self._model["Q_max"], ) return self._model["Q_max"] if heat_demand_kW < self._model["Q_min"]: - log.debug( - "calc_cop: heat demand exceeds minimum heat_demand_kW: %s", self._model["Q_min"] + log.error( + "calc_cop: heat demand (%s kW) exceeds minimum heat_demand_kW: %s", + heat_demand_kW, + self._model["Q_min"], ) return self._model["Q_min"] return heat_demand_kW - def calc_q_from_p_kW( - self, - source_temp_C: float, - condenser_temp_C: float, - electrical_demand_kW: Optional[float] = None, - ): - return self._resolve_heat( - source_temp_C=source_temp_C, - condenser_temp_C=condenser_temp_C, - electricity_demand_kW=electrical_demand_kW, - ) - - def calc_cop( - self, - source_temp_C: float, - condenser_temp_C: float, - heat_demand_kW: float, - electrical_demand_kW: Optional[float] = None, - ): - - if (electrical_demand_kW is None) == (heat_demand_kW is None): - assert False, "heat_demand_kW and electrical_demand_kW can only be set exclusively" - - if electrical_demand_kW: - # estimate the heat demand by using the median COP of the model fitting data - # this is only for heaving an initial value for the model call - heat_demand_kW = electrical_demand_kW * self._model["COP_med"] - - heat_demand_kW = self._limit_heat_demand_kW(heat_demand_kW) - if heat_demand_kW == 0: - return 0 - electrical_power_kW = self._calc_power(source_temp_C, condenser_temp_C, heat_demand_kW) - if electrical_power_kW <= 0: - log.error( - "calculated power is negative: " - "hp model: %s source_temp: %s, " - "condenser_temp: %s, heat_demand_kW: %s, calculated power: %s", - self.model_type.name, - round(source_temp_C, 2), - round(condenser_temp_C, 2), - round(heat_demand_kW, 2), - round(electrical_power_kW, 2), - ) - return 0 - cop = heat_demand_kW / electrical_power_kW - if cop > self._model["COP_max"] or cop < self._model["COP_min"]: - log.error( - "calculated COP (%s) is unrealistic: " - "hp model: %s source_temp: %s, " - "condenser_temp: %s, heat_demand_kW: %s, calculated power: %s", - round(cop, 2), - self.model_type.name, - round(source_temp_C, 2), - round(condenser_temp_C, 2), - round(heat_demand_kW, 2), - round(electrical_power_kW, 2), - ) - return cop - class UniversalCOPModel(BaseCOPModel): """Handle cop calculation independent of the heat pump model""" @@ -256,8 +240,7 @@ def calc_cop( self, source_temp_C: float, condenser_temp_C: float, - heat_demand_kW: Optional[float], - electrical_demand_kW: Optional[float] = None, + heat_demand_kW: Optional[float] = None, ) -> float: """COP model following https://www.nature.com/articles/s41597-019-0199-y""" return self._calc_cop_from_temps(source_temp_C, condenser_temp_C) diff --git a/src/gsy_e/models/strategy/energy_parameters/heatpump/cop_models/model_data/AERMEC_NXP_0600_4L_COOL_model_parameters.json b/src/gsy_e/models/strategy/energy_parameters/heatpump/cop_models/model_data/AERMEC_NXP_0600_4L_COOL_model_parameters.json index 2ca253e3e..440f0a2dd 100644 --- a/src/gsy_e/models/strategy/energy_parameters/heatpump/cop_models/model_data/AERMEC_NXP_0600_4L_COOL_model_parameters.json +++ b/src/gsy_e/models/strategy/energy_parameters/heatpump/cop_models/model_data/AERMEC_NXP_0600_4L_COOL_model_parameters.json @@ -1 +1 @@ -{"CAPFT": [0.8454605267956957, 0.027689073804528678, 0.006404959469341046, 0.0001496419458169494, -4.569496057765451e-05, -0.00021118596972354869], "HEIRFT": [0.7418220836760441, -0.0022664853115584786, -0.007181589206940325, 0.000674105489882314, -0.000946546960846392, 0.0005940100532001941], "HEIRFPLR": [-0.5215602319631321, 1.6972347812119102, -0.1618280142560721], "Qref": 141.5, "Pref": 30.9, "Q_min": 118.9, "Q_max": 180.5, "PLR_min": 0.25, "COP_min": 3.22, "COP_max": 6.51, "COP_med": 4.8} +{"CAPFT": [0.8454604995767081, 0.027689079173849773, 0.0064049595989519315, 0.00014964176134313334, -4.56950079232854e-05, -0.00021118596488212127], "HEIRFT": [0.7418220749382124, -0.002266483345133352, -0.007181589246322728, 0.000674105435500838, -0.0009465469846055008, 0.0005940100570227536], "HEIRFPLR": [-0.16182801674209818, 1.6972347891613433, -0.5215602370806782], "Qref": 141.5, "Pref": 30.9, "Q_min": 118.9, "Q_max": 180.5, "PLR_min": 0.25, "COP_min": 3.22, "COP_max": 6.51, "COP_med": 4.8} diff --git a/src/gsy_e/models/strategy/energy_parameters/heatpump/cop_models/model_data/AERMEC_NXP_0600_4L_HEAT_model_parameters.json b/src/gsy_e/models/strategy/energy_parameters/heatpump/cop_models/model_data/AERMEC_NXP_0600_4L_HEAT_model_parameters.json index 73e28c0f5..8b35208e3 100644 --- a/src/gsy_e/models/strategy/energy_parameters/heatpump/cop_models/model_data/AERMEC_NXP_0600_4L_HEAT_model_parameters.json +++ b/src/gsy_e/models/strategy/energy_parameters/heatpump/cop_models/model_data/AERMEC_NXP_0600_4L_HEAT_model_parameters.json @@ -1 +1 @@ -{"CAPFT": [0.7841944915827704, 0.027025683620693207, 0.005222590512291558, 0.00021692430116427368, -0.00019218411177690167, -0.00011531764270024707], "HEIRFT": [0.7162755985430916, -0.004161089855566163, -0.0005729077062670702, 0.00041800740384663796, -0.0006154588026100481, 0.00039346652665741823], "HEIRFPLR": [-1.3954158938415628, 2.7629166672566634, -0.33808079196917734], "Qref": 175.7, "Pref": 32.2, "Q_min": 142.7, "Q_max": 226.3, "PLR_min": 0.25, "COP_min": 3.21, "COP_max": 7.04, "COP_med": 5.15} +{"CAPFT": [0.7841944989955758, 0.027025683297829506, 0.005222590218886269, 0.00021692431037390048, -0.00019218410884458415, -0.00011531763958783081], "HEIRFT": [0.7162754899754193, -0.0041610888650991384, -0.0005729023829936786, 0.00041800736954067386, -0.0006154588109192893, 0.00039346646350357783], "HEIRFPLR": [-0.33808077124534075, 2.762916582736726, -1.3954158311342828], "Qref": 175.7, "Pref": 32.2, "Q_min": 142.7, "Q_max": 226.3, "PLR_min": 0.25, "COP_min": 3.21, "COP_max": 7.04, "COP_med": 5.15} diff --git a/src/gsy_e/models/strategy/energy_parameters/heatpump/heat_pump.py b/src/gsy_e/models/strategy/energy_parameters/heatpump/heat_pump.py index a7f0fdc93..48f05a470 100644 --- a/src/gsy_e/models/strategy/energy_parameters/heatpump/heat_pump.py +++ b/src/gsy_e/models/strategy/energy_parameters/heatpump/heat_pump.py @@ -9,7 +9,6 @@ convert_kJ_to_kWh, convert_kWh_to_kJ, convert_kJ_to_kW, - convert_kWh_to_W, convert_kWh_to_kW, convert_kW_to_kWh, ) @@ -264,12 +263,19 @@ def update_cop_after_dis_charging( bought_energy_kWh: float, ): """Update the COP of the heat pump in its state class.""" - cop = self._calc_cop( - heat_energy_kJ=None, + if bought_energy_kWh < FLOATING_POINT_TOLERANCE: + self._hp_state.set_cop(time_slot, self._hp_state.get_cop(last_time_slot)) + return + bought_energy_kW = convert_kWh_to_kW(bought_energy_kWh, GlobalConfig.slot_length) + heat_energy_kW = self._cop_model.calc_q_from_p_kW( source_temp_C=source_temp_C, - time_slot=last_time_slot, - electrical_energy_kWh=bought_energy_kWh, + condenser_temp_C=self._charger.get_average_inlet_temperature_C(last_time_slot), + electrical_demand_kW=bought_energy_kW, ) + if heat_energy_kW is None: + cop = self._hp_state.get_cop(last_time_slot) + else: + cop = heat_energy_kW / bought_energy_kW # Set the calculated COP on both the last and the current time slot to use in calculations self._hp_state.set_cop(last_time_slot, cop) @@ -280,7 +286,6 @@ def _calc_cop( heat_energy_kJ: float, source_temp_C: float, time_slot: DateTime, - electrical_energy_kWh: Optional[float] = None, ) -> float: """ Return the coefficient of performance (COP) for a given ambient and storage temperature. @@ -289,22 +294,10 @@ def _calc_cop( Generally, the higher the temperature difference between the source and the sink, the lower the efficiency of the heat pump (the lower COP). """ - if electrical_energy_kWh is None: - if heat_energy_kJ < FLOATING_POINT_TOLERANCE: - return 0 - heat_demand_kW = convert_kJ_to_kW(heat_energy_kJ, GlobalConfig.slot_length) - electrical_energy_kW = None - else: - heat_demand_kW = None - electrical_energy_kW = ( - convert_kWh_to_W(electrical_energy_kWh, GlobalConfig.slot_length) / 1000 - ) - return self._cop_model.calc_cop( source_temp_C=source_temp_C, condenser_temp_C=self._charger.get_average_inlet_temperature_C(time_slot), - heat_demand_kW=heat_demand_kW, - electrical_demand_kW=electrical_energy_kW, + heat_demand_kW=convert_kJ_to_kW(heat_energy_kJ, GlobalConfig.slot_length), ) def calc_Q_kJ_from_energy_kWh( @@ -316,6 +309,8 @@ def calc_Q_kJ_from_energy_kWh( condenser_temp_C=self._charger.get_average_inlet_temperature_C(time_slot), electrical_demand_kW=convert_kWh_to_kW(energy_kWh, GlobalConfig.slot_length), ) + if heat_energy_kW is None: + heat_energy_kW = energy_kWh * self._hp_state.get_cop(self._last_time_slot(time_slot)) return convert_kWh_to_kJ(convert_kW_to_kWh(heat_energy_kW, GlobalConfig.slot_length)) def event_activate(self): @@ -326,6 +321,10 @@ def event_market_cycle(self, time_slot: DateTime): """Runs on market_cycle event.""" self._charger.event_market_cycle(time_slot) + def _last_time_slot(self, current_market_slot: DateTime) -> DateTime: + """Calculate the previous time slot from the current one.""" + return current_market_slot - GlobalConfig.slot_length + class HeatPumpEnergyParametersBase(ABC): """ @@ -683,26 +682,24 @@ def _update_cop_after_dis_charging( bought_energy_kWh: float, ): """Update the COP of the heat pump in its state class.""" - cop = self._calc_cop( - time_slot=last_time_slot, - electrical_energy_kWh=bought_energy_kWh, + if bought_energy_kWh < FLOATING_POINT_TOLERANCE: + self._hp_state.set_cop(time_slot, self._hp_state.get_cop(last_time_slot)) + return + bought_energy_kW = convert_kWh_to_kW(bought_energy_kWh, GlobalConfig.slot_length) + heat_energy_kW = self._cop_model.calc_q_from_p_kW( + source_temp_C=self._source_temp_C.get_value(last_time_slot), + condenser_temp_C=self._target_temp_C.get_value(last_time_slot), + electrical_demand_kW=bought_energy_kW, ) + if heat_energy_kW is None: + cop = self.state.get_cop(last_time_slot) + else: + cop = heat_energy_kW / bought_energy_kW # Set the calculated COP on both the last and the current time slot to use in calculations self.state.set_cop(last_time_slot, cop) self.state.set_cop(time_slot, cop) - def _calc_cop(self, time_slot: DateTime, electrical_energy_kWh: float) -> float: - electrical_energy_kW = ( - convert_kWh_to_W(electrical_energy_kWh, GlobalConfig.slot_length) / 1000 - ) - return self._cop_model.calc_cop( - source_temp_C=self._source_temp_C.get_value(time_slot), - condenser_temp_C=self._target_temp_C.get_value(time_slot), - heat_demand_kW=None, - electrical_demand_kW=electrical_energy_kW, - ) - def _calc_Q_kJ_from_energy_kWh(self, time_slot: DateTime, energy_kWh: float) -> float: """Calculate heat in kJ from energy in kWh.""" heat_energy_kW = self._cop_model.calc_q_from_p_kW( @@ -710,6 +707,8 @@ def _calc_Q_kJ_from_energy_kWh(self, time_slot: DateTime, energy_kWh: float) -> condenser_temp_C=self._target_temp_C.get_value(time_slot), electrical_demand_kW=convert_kWh_to_kW(energy_kWh, GlobalConfig.slot_length), ) + if heat_energy_kW is None: + heat_energy_kW = energy_kWh * self.state.get_cop(self.last_time_slot(time_slot)) return convert_kWh_to_kJ(convert_kW_to_kWh(heat_energy_kW, GlobalConfig.slot_length)) def _calc_energy_kWh_from_Q_kJ(self, time_slot: DateTime, Q_energy_kJ: float) -> float: diff --git a/src/gsy_e/setup/tekniker_cop_model/GSHP_model_fitter.py b/src/gsy_e/setup/tekniker_cop_model/GSHP_model_fitter.py index 60858eda6..c7407945a 100644 --- a/src/gsy_e/setup/tekniker_cop_model/GSHP_model_fitter.py +++ b/src/gsy_e/setup/tekniker_cop_model/GSHP_model_fitter.py @@ -53,6 +53,9 @@ def _poly2(PLR, a, b, c): """Quadratic polynomial: a*PLR^2 + b*PLR + c.""" return a * PLR**2 + b * PLR + c + def _eer_fplr(self, plr): + return 4.1922 + 2.6988 * plr - 4.0581 * plr**2 + def _fit_model(self): df = self.raw_data.copy() @@ -120,7 +123,7 @@ def _fit_model(self): synthetic_rows = [] for plr in plr_targets: Q_syn = Qref * capft_ref * plr - EER_syn = self.eer_fplr(plr) + EER_syn = self._eer_fplr(plr) P_syn = Q_syn / EER_syn EIR_syn = P_syn / (Pref * capft_ref * eirft_ref) @@ -156,7 +159,7 @@ def _fit_model(self): model = { "CAPFT": popt_capft, "HEIRFT": popt_eirft, - "HEIRFPLR": popt_eirfplr, + "HEIRFPLR": popt_eirfplr[::-1], "Qref": Qref, "Pref": Pref, "Q_min": df["Q"].min(), diff --git a/src/gsy_e/setup/tekniker_cop_model/model_parameters/AERMEC_NXP_0600_4L_COOL_model_parameters.json b/src/gsy_e/setup/tekniker_cop_model/model_parameters/AERMEC_NXP_0600_4L_COOL_model_parameters.json index 2ca253e3e..440f0a2dd 100644 --- a/src/gsy_e/setup/tekniker_cop_model/model_parameters/AERMEC_NXP_0600_4L_COOL_model_parameters.json +++ b/src/gsy_e/setup/tekniker_cop_model/model_parameters/AERMEC_NXP_0600_4L_COOL_model_parameters.json @@ -1 +1 @@ -{"CAPFT": [0.8454605267956957, 0.027689073804528678, 0.006404959469341046, 0.0001496419458169494, -4.569496057765451e-05, -0.00021118596972354869], "HEIRFT": [0.7418220836760441, -0.0022664853115584786, -0.007181589206940325, 0.000674105489882314, -0.000946546960846392, 0.0005940100532001941], "HEIRFPLR": [-0.5215602319631321, 1.6972347812119102, -0.1618280142560721], "Qref": 141.5, "Pref": 30.9, "Q_min": 118.9, "Q_max": 180.5, "PLR_min": 0.25, "COP_min": 3.22, "COP_max": 6.51, "COP_med": 4.8} +{"CAPFT": [0.8454604995767081, 0.027689079173849773, 0.0064049595989519315, 0.00014964176134313334, -4.56950079232854e-05, -0.00021118596488212127], "HEIRFT": [0.7418220749382124, -0.002266483345133352, -0.007181589246322728, 0.000674105435500838, -0.0009465469846055008, 0.0005940100570227536], "HEIRFPLR": [-0.16182801674209818, 1.6972347891613433, -0.5215602370806782], "Qref": 141.5, "Pref": 30.9, "Q_min": 118.9, "Q_max": 180.5, "PLR_min": 0.25, "COP_min": 3.22, "COP_max": 6.51, "COP_med": 4.8} diff --git a/src/gsy_e/setup/tekniker_cop_model/model_parameters/AERMEC_NXP_0600_4L_HEAT_model_parameters.json b/src/gsy_e/setup/tekniker_cop_model/model_parameters/AERMEC_NXP_0600_4L_HEAT_model_parameters.json index 73e28c0f5..8b35208e3 100644 --- a/src/gsy_e/setup/tekniker_cop_model/model_parameters/AERMEC_NXP_0600_4L_HEAT_model_parameters.json +++ b/src/gsy_e/setup/tekniker_cop_model/model_parameters/AERMEC_NXP_0600_4L_HEAT_model_parameters.json @@ -1 +1 @@ -{"CAPFT": [0.7841944915827704, 0.027025683620693207, 0.005222590512291558, 0.00021692430116427368, -0.00019218411177690167, -0.00011531764270024707], "HEIRFT": [0.7162755985430916, -0.004161089855566163, -0.0005729077062670702, 0.00041800740384663796, -0.0006154588026100481, 0.00039346652665741823], "HEIRFPLR": [-1.3954158938415628, 2.7629166672566634, -0.33808079196917734], "Qref": 175.7, "Pref": 32.2, "Q_min": 142.7, "Q_max": 226.3, "PLR_min": 0.25, "COP_min": 3.21, "COP_max": 7.04, "COP_med": 5.15} +{"CAPFT": [0.7841944989955758, 0.027025683297829506, 0.005222590218886269, 0.00021692431037390048, -0.00019218410884458415, -0.00011531763958783081], "HEIRFT": [0.7162754899754193, -0.0041610888650991384, -0.0005729023829936786, 0.00041800736954067386, -0.0006154588109192893, 0.00039346646350357783], "HEIRFPLR": [-0.33808077124534075, 2.762916582736726, -1.3954158311342828], "Qref": 175.7, "Pref": 32.2, "Q_min": 142.7, "Q_max": 226.3, "PLR_min": 0.25, "COP_min": 3.21, "COP_max": 7.04, "COP_med": 5.15} diff --git a/tests/strategies/energy_parameters/test_combined_heatpump_state.py b/tests/strategies/energy_parameters/test_combined_heatpump_state.py index f8e458467..f564adb6d 100644 --- a/tests/strategies/energy_parameters/test_combined_heatpump_state.py +++ b/tests/strategies/energy_parameters/test_combined_heatpump_state.py @@ -39,7 +39,7 @@ def test_get_energy_to_buy_maximum_kWh_calls_correct_methods(self, combined_stat CURRENT_MARKET_SLOT, 5000 ) combined_state._cop_model.calc_cop.assert_called_with( - source_temp_C=20, condenser_temp_C=30, heat_demand_kW=2.0, electrical_demand_kW=None + source_temp_C=20, condenser_temp_C=30, heat_demand_kW=2.0 ) def test_get_energy_to_buy_maximum_kWh_limits_cop_to_global_setting(self, combined_state): @@ -68,7 +68,7 @@ def test_get_energy_to_buy_minimum_kWh_calls_correct_methods(self, combined_stat CURRENT_MARKET_SLOT, 5000 ) combined_state._cop_model.calc_cop.assert_called_with( - source_temp_C=20, condenser_temp_C=30, heat_demand_kW=2.0, electrical_demand_kW=None + source_temp_C=20, condenser_temp_C=30, heat_demand_kW=2.0 ) @pytest.mark.parametrize( diff --git a/tests/strategies/energy_parameters/test_cop_models.py b/tests/strategies/energy_parameters/test_cop_models.py new file mode 100644 index 000000000..b2fce109e --- /dev/null +++ b/tests/strategies/energy_parameters/test_cop_models.py @@ -0,0 +1,33 @@ +from math import isclose + +import numpy as np +import pytest + +from gsy_e.models.strategy.energy_parameters.heatpump.cop_models.cop_models import ( + IndividualCOPModel, + COPModelType, +) + + +class TestCopModels: + + @pytest.mark.parametrize( + "model_type, min_heat, max_heat", + [ + [COPModelType.ELCO_AEROTOP_S09_IR, 5, 18], + [COPModelType.ELCO_AEROTOP_G07_14M, 5, 18], + [COPModelType.HOVAL_ULTRASOURCE_B_COMFORT_C11, 4, 11], + [COPModelType.AERMEC_NXP_0600_4L_HEAT, 140, 180], + [COPModelType.AERMEC_NXP_0600_4L_COOL, 110, 160], + ], + ) + def test_if_forward_equals_backward(self, model_type, min_heat, max_heat): + # pylint: disable=protected-access + cop_model = IndividualCOPModel(model_type=model_type) + source_temp = 11 + condenser_temp = 30 + for input_heat in np.linspace(min_heat, max_heat, 4): + print(input_heat) + power = cop_model._calc_power(source_temp, condenser_temp, input_heat) + heat = cop_model.calc_q_from_p_kW(source_temp, condenser_temp, power) + assert isclose(heat, input_heat), f"input_heat: {input_heat}" diff --git a/tests/strategies/energy_parameters/test_heat_pump.py b/tests/strategies/energy_parameters/test_heat_pump.py index a3f290f8c..3feaf86db 100644 --- a/tests/strategies/energy_parameters/test_heat_pump.py +++ b/tests/strategies/energy_parameters/test_heat_pump.py @@ -218,11 +218,11 @@ def test_if_profiles_are_rotated_on_market_cycle(energy_params): def test_cop_model_is_correctly_selected(energy_params_heat_profile): energy_params_heat_profile.event_activate() energy_params_heat_profile.event_market_cycle(CURRENT_MARKET_SLOT) - energy_params_heat_profile._bought_energy_kWh = 3.6 + energy_params_heat_profile._bought_energy_kWh = 1 energy_params_heat_profile.event_market_cycle(CURRENT_MARKET_SLOT + duration(minutes=60)) assert isclose( energy_params_heat_profile._state.heatpump.get_cop(CURRENT_MARKET_SLOT), - 3.163, + 3.979, abs_tol=0.001, ) @@ -235,3 +235,44 @@ def test_event_market_cycle_triggers_delete_past_state_values(energy_params): energy_params.event_market_cycle(CURRENT_MARKET_SLOT) # Then energy_params._state.delete_past_state_values.assert_called_once() + + @staticmethod + def test_event_market_cycle_populates_state_correctly(energy_params): + # Given + current_market_slot = CURRENT_MARKET_SLOT + duration(minutes=60) + last_market_slot = CURRENT_MARKET_SLOT + energy_params.event_activate() + energy_params._bought_energy_kWh = 5 + energy_params.combined_state._cop_model.calc_q_from_p_kW = Mock(return_value=25) + energy_params.combined_state._cop_model.calc_cop = Mock(return_value=5) + # When + # Event market cycle has to be called twice in order to have a last_market_slot + energy_params.event_market_cycle(last_market_slot) + energy_params.event_market_cycle(current_market_slot) + # Then + assert energy_params.combined_state._hp_state._cop[last_market_slot] == 5 + assert energy_params.combined_state._hp_state._cop[current_market_slot] == 5 + assert energy_params._bought_energy_kWh == 0 + assert energy_params._consumption_kWh.get_value(current_market_slot) == 5 + assert energy_params.combined_state._hp_state._energy_demand_kWh[current_market_slot] == 5 + + @staticmethod + def test_event_market_cycle_populates_state_correctly_even_if_cop_model_fails(energy_params): + # Given + current_market_slot = CURRENT_MARKET_SLOT + duration(minutes=60) + last_market_slot = CURRENT_MARKET_SLOT + energy_params.event_activate() + energy_params._bought_energy_kWh = 1 + energy_params.combined_state._cop_model.calc_q_from_p_kW = Mock(return_value=None) + energy_params.combined_state._cop_model.calc_cop = Mock(return_value=6) + energy_params.combined_state._hp_state.get_cop = Mock(return_value=6) + # When + # Event market cycle has to be called twice in order to have a last_market_slot + energy_params.event_market_cycle(last_market_slot) + energy_params.event_market_cycle(current_market_slot) + # Then + assert energy_params.combined_state._hp_state._cop[last_market_slot] == 6 + assert energy_params.combined_state._hp_state._cop[current_market_slot] == 6 + assert energy_params._bought_energy_kWh == 0 + assert energy_params._consumption_kWh.get_value(current_market_slot) == 5 + assert energy_params.combined_state._hp_state._energy_demand_kWh[current_market_slot] == 5 diff --git a/tests/strategies/energy_parameters/test_heat_pump_without_tanks.py b/tests/strategies/energy_parameters/test_heat_pump_without_tanks.py index 7111903b2..7e5aa8a50 100644 --- a/tests/strategies/energy_parameters/test_heat_pump_without_tanks.py +++ b/tests/strategies/energy_parameters/test_heat_pump_without_tanks.py @@ -84,7 +84,7 @@ def test_event_market_cycle_populates_state_correctly(self, energy_params): current_market_slot = CURRENT_MARKET_SLOT + duration(minutes=60) last_market_slot = CURRENT_MARKET_SLOT energy_params.event_activate() - energy_params._calc_cop = Mock(return_value=5) + energy_params._cop_model.calc_q_from_p_kW = Mock(return_value=5) energy_params._calc_Q_kJ_from_energy_kWh = Mock(return_value=5000) energy_params._bought_energy_kWh = 1 # When @@ -92,8 +92,8 @@ def test_event_market_cycle_populates_state_correctly(self, energy_params): energy_params.event_market_cycle(last_market_slot) energy_params.event_market_cycle(current_market_slot) # Then - energy_params._state._cop[last_market_slot] = 5 - energy_params._state._cop[current_market_slot] = 5 + assert energy_params._state._cop[last_market_slot] == 5 + assert energy_params._state._cop[current_market_slot] == 5 assert energy_params._bought_energy_kWh == 0 assert energy_params._consumption_kWh.get_value(current_market_slot) == 1 assert energy_params._state._heat_demand_kJ[current_market_slot] == 5000 @@ -106,7 +106,7 @@ def test_event_market_cycle_populates_state_correctly_heat_profile( current_market_slot = CURRENT_MARKET_SLOT + duration(minutes=60) last_market_slot = CURRENT_MARKET_SLOT energy_params_heat_profile.event_activate() - energy_params_heat_profile._calc_cop = Mock(return_value=5) + energy_params_heat_profile._cop_model.calc_q_from_p_kW = Mock(return_value=5) energy_params_heat_profile._calc_Q_kJ_from_energy_kWh = Mock(return_value=5000) energy_params_heat_profile._bought_energy_kWh = 1 # When @@ -114,8 +114,8 @@ def test_event_market_cycle_populates_state_correctly_heat_profile( energy_params_heat_profile.event_market_cycle(last_market_slot) energy_params_heat_profile.event_market_cycle(current_market_slot) # Then - energy_params_heat_profile._state._cop[last_market_slot] = 5 - energy_params_heat_profile._state._cop[current_market_slot] = 5 + assert energy_params_heat_profile._state._cop[last_market_slot] == 5 + assert energy_params_heat_profile._state._cop[current_market_slot] == 5 assert energy_params_heat_profile._bought_energy_kWh == 0 assert current_market_slot not in energy_params_heat_profile._consumption_kWh.profile assert energy_params_heat_profile._state._heat_demand_kJ[current_market_slot] == 18 @@ -161,3 +161,25 @@ def test_get_energy_demand_kWh_returns_correct_value_heat_profile( energy_params_heat_profile.event_market_cycle(CURRENT_MARKET_SLOT) # When / Then assert energy_params_heat_profile.get_energy_demand_kWh(CURRENT_MARKET_SLOT) == 0.001 + + def test_event_market_cycle_populates_state_correctly_even_if_cop_model_fails( + self, energy_params + ): + # Given + current_market_slot = CURRENT_MARKET_SLOT + duration(minutes=60) + last_market_slot = CURRENT_MARKET_SLOT + energy_params.event_activate() + energy_params._bought_energy_kWh = 1 + energy_params._cop_model.calc_q_from_p_kW = Mock(return_value=None) + energy_params.state.get_cop = Mock(return_value=6) + # When + # Event market cycle has to be called twice in order to have a last_market_slot + energy_params.event_market_cycle(last_market_slot) + energy_params.event_market_cycle(current_market_slot) + # Then + assert energy_params._state._cop[last_market_slot] == 6 + assert energy_params._state._cop[current_market_slot] == 6 + assert energy_params._bought_energy_kWh == 0 + assert energy_params._consumption_kWh.get_value(current_market_slot) == 1 + assert energy_params._state._heat_demand_kJ[current_market_slot] == 21600.0 + assert energy_params._state._energy_demand_kWh[current_market_slot] == 1