Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand All @@ -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")
Expand All @@ -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",
}


Expand All @@ -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"""

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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]:
"""
Expand All @@ -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"""
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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}
Original file line number Diff line number Diff line change
@@ -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}
Loading
Loading