"""
Refrigerant cycle calculations and optimization.
"""
from collections.abc import Callable
from typing import Any
import CoolProp.CoolProp as CP
import numpy as np
from . import calc_util as cu
[docs]
def calc_ref_state(
T_evap_K: float, # 증발 온도 [K] (포화 온도로 해석)
T_cond_K: float, # 응축 온도 [K] (포화 온도로 해석)
refrigerant: str, # 냉매 이름
eta_cmp_isen: float | Callable, # 압축기 등엔트로피 효율 (Float 또는 함수)
mode: str = "heating", # 작동 모드 ('heating' 또는 'cooling')
dT_superheat: float = 0.0, # [K] 증발기 출구 과열도 (State 1* → 1)
dT_subcool: float = 0.0, # [K] 응축기 출구 과냉각도 (State 3* → 3)
is_active: bool = True, # 활성화 여부 (False일 때 nan 값 반환)
) -> dict[str, Any]:
"""
냉매 사이클의 State 1-4 열역학 물성치를 계산하는 공통 함수.
증기압축 사이클의 4개 주요 상태점을 계산합니다:
- State 1 (cmp_in): 압축기 입구 (증발기 출구, 저압 과열 증기)
- State 2 (cmp_out): 압축기 출구 (응축기 입구, 고압 과열 증기)
- State 3 (exp_in): 팽창밸브 입구 (응축기 출구, 고압 과냉 액체)
- State 4 (exp_out): 팽창밸브 출구 (증발기 입구, 저압 2상 혼합물)
키 배정은 항상 물리적 압축기 입출구 기준이며, mode 값은
결과 dict의 ``"mode"`` 키에만 기록됩니다.
Note
----
냉방/난방 모드에서 어느 HX가 증발기/응축기인지는 호출측
(``_calc_state``)에서 T_evap_K, T_cond_K를 결정하여 전달합니다.
"""
# is_active=False일 때 nan 값으로 채워진 딕셔너리 반환
if not is_active:
return {
"P_ref_cmp_in [Pa]": np.nan,
"P_ref_cmp_out [Pa]": np.nan,
"P_ref_exp_in [Pa]": np.nan,
"P_ref_exp_out [Pa]": np.nan,
"P_ref_evap_sat [Pa]": np.nan,
"P_ref_cond_sat_l [Pa]": np.nan,
"P_ref_cond_sat_v [Pa]": np.nan,
"T_ref_cmp_in_K": np.nan,
"T_ref_cmp_out_K": np.nan,
"T_ref_exp_in_K": np.nan,
"T_ref_exp_out_K": np.nan,
"T_ref_evap_sat_K": np.nan,
"T_ref_cond_sat_v_K": np.nan,
"T_ref_cond_sat_l_K": np.nan,
"T_ref_cmp_in [°C]": np.nan,
"T_ref_cmp_out [°C]": np.nan,
"T_ref_exp_in [°C]": np.nan,
"T_ref_exp_out [°C]": np.nan,
"T_ref_evap_sat [°C]": np.nan,
"T_ref_cond_sat_v [°C]": np.nan,
"T_ref_cond_sat_l [°C]": np.nan,
"h_ref_cmp_in [J/kg]": np.nan,
"h_ref_cmp_out [J/kg]": np.nan,
"h_ref_cond_sat_v [J/kg]": np.nan,
"h_ref_exp_in [J/kg]": np.nan,
"h_ref_exp_out [J/kg]": np.nan,
"h_ref_evap_sat [J/kg]": np.nan,
"h_ref_cond_sat_l [J/kg]": np.nan,
"s_ref_cmp_in [J/(kg·K)]": np.nan,
"s_ref_cmp_out [J/(kg·K)]": np.nan,
"s_ref_cond_sat_v [J/(kg·K)]": np.nan,
"s_ref_exp_in [J/(kg·K)]": np.nan,
"s_ref_exp_out [J/(kg·K)]": np.nan,
"s_ref_evap_sat [J/(kg·K)]": np.nan,
"s_ref_cond_sat_l [J/(kg·K)]": np.nan,
"rho_ref_cmp_in [kg/m3]": np.nan,
"mode": mode,
}
# 1단계: 포화 온도 및 압력 계산
T_ref_evap_sat_K = T_evap_K
T_ref_cond_sat_l_K = T_cond_K
P_evap = CP.PropsSI("P", "T", T_ref_evap_sat_K, "Q", 1, refrigerant)
P_cond = CP.PropsSI("P", "T", T_ref_cond_sat_l_K, "Q", 0, refrigerant)
# 포화 상태 추가 계산
h_ref_evap_sat = CP.PropsSI("H", "T", T_ref_evap_sat_K, "Q", 1, refrigerant)
s_ref_evap_sat = CP.PropsSI("S", "T", T_ref_evap_sat_K, "Q", 1, refrigerant)
h_ref_cond_sat_l = CP.PropsSI("H", "T", T_ref_cond_sat_l_K, "Q", 0, refrigerant)
s_ref_cond_sat_l = CP.PropsSI("S", "T", T_ref_cond_sat_l_K, "Q", 0, refrigerant)
# 2단계: State 1 (실제 과열 증기) 계산
T_ref_cmp_in_K = T_ref_evap_sat_K + dT_superheat
if abs(dT_superheat) < 1e-6:
h_ref_cmp_in = h_ref_evap_sat
s_ref_cmp_in = s_ref_evap_sat
rho_ref_cmp_in = CP.PropsSI("D", "T", T_ref_evap_sat_K, "Q", 1, refrigerant)
else:
h_ref_cmp_in = CP.PropsSI("H", "T", T_ref_cmp_in_K, "P", P_evap, refrigerant)
s_ref_cmp_in = CP.PropsSI("S", "T", T_ref_cmp_in_K, "P", P_evap, refrigerant)
rho_ref_cmp_in = CP.PropsSI("D", "T", T_ref_cmp_in_K, "P", P_evap, refrigerant)
# 3단계: State 2 (압축기 출구 - 고압 과열 증기) 계산
h2_isen = CP.PropsSI("H", "P", P_cond, "S", s_ref_cmp_in, refrigerant)
if callable(eta_cmp_isen):
val_eta_cmp_isen = eta_cmp_isen(P_cond / P_evap)
else:
val_eta_cmp_isen = eta_cmp_isen
h_ref_cmp_out = h_ref_cmp_in + (h2_isen - h_ref_cmp_in) / val_eta_cmp_isen
try:
T_ref_cmp_out_K = CP.PropsSI("T", "P", P_cond, "H", h_ref_cmp_out, refrigerant)
s_ref_cmp_out = CP.PropsSI("S", "P", P_cond, "H", h_ref_cmp_out, refrigerant)
except ValueError:
# If H is too high, it exceeds Tmax of CoolProp (e.g. 435K for R32).
# We MUST NOT modify h_ref_cmp_out as it breaks the energy balance.
# Just set T and s to NaN.
T_ref_cmp_out_K = np.nan
s_ref_cmp_out = np.nan
P_ref_cmp_out = P_cond
# 3.5단계: State 2* (응축기 포화 증기 도달 지점) 계산
T_ref_cond_sat_v_K = T_ref_cond_sat_l_K
P_ref_cond_sat_v = P_cond
h_ref_cond_sat_v = CP.PropsSI("H", "P", P_cond, "Q", 1, refrigerant)
s_ref_cond_sat_v = CP.PropsSI("S", "P", P_cond, "Q", 1, refrigerant)
# 4단계: State 3 (실제 과냉 액체) 계산
T_ref_exp_in_K = T_ref_cond_sat_l_K - dT_subcool
if abs(dT_subcool) < 1e-6:
h_ref_exp_in = h_ref_cond_sat_l
s_ref_exp_in = s_ref_cond_sat_l
else:
h_ref_exp_in = CP.PropsSI("H", "T", T_ref_exp_in_K, "P", P_cond, refrigerant)
s_ref_exp_in = CP.PropsSI("S", "T", T_ref_exp_in_K, "P", P_cond, refrigerant)
# 5단계: State 4 (팽창밸브 출구) 계산
h_ref_exp_out = h_ref_exp_in
P_ref_exp_out = P_evap
T_ref_exp_out_K = CP.PropsSI("T", "P", P_evap, "H", h_ref_exp_out, refrigerant)
s_ref_exp_out = CP.PropsSI("S", "P", P_evap, "H", h_ref_exp_out, refrigerant)
result = {
"P_ref_cmp_in [Pa]": P_evap,
"P_ref_cmp_out [Pa]": P_cond,
"P_ref_exp_in [Pa]": P_cond,
"P_ref_exp_out [Pa]": P_evap,
"P_ref_evap_sat [Pa]": P_evap,
"P_ref_cond_sat_l [Pa]": P_cond,
"P_ref_cond_sat_v [Pa]": P_ref_cond_sat_v,
"T_ref_cmp_in_K": T_ref_cmp_in_K,
"T_ref_cmp_out_K": T_ref_cmp_out_K,
"T_ref_exp_in_K": T_ref_exp_in_K,
"T_ref_exp_out_K": T_ref_exp_out_K,
"T_ref_evap_sat_K": T_ref_evap_sat_K,
"T_ref_cond_sat_v_K": T_ref_cond_sat_v_K,
"T_ref_cond_sat_l_K": T_ref_cond_sat_l_K,
"T_ref_cmp_in [°C]": cu.K2C(T_ref_cmp_in_K),
"T_ref_cmp_out [°C]": cu.K2C(T_ref_cmp_out_K),
"T_ref_exp_in [°C]": cu.K2C(T_ref_exp_in_K),
"T_ref_exp_out [°C]": cu.K2C(T_ref_exp_out_K),
"T_ref_evap_sat [°C]": cu.K2C(T_ref_evap_sat_K),
"T_ref_cond_sat_l [°C]": cu.K2C(T_ref_cond_sat_l_K),
"T_ref_cond_sat_v [°C]": cu.K2C(T_ref_cond_sat_v_K),
"h_ref_cmp_in [J/kg]": h_ref_cmp_in,
"h_ref_cmp_out [J/kg]": h_ref_cmp_out,
"h_ref_cond_sat_v [J/kg]": h_ref_cond_sat_v,
"h_ref_exp_in [J/kg]": h_ref_exp_in,
"h_ref_exp_out [J/kg]": h_ref_exp_out,
"h_ref_evap_sat [J/kg]": h_ref_evap_sat,
"h_ref_cond_sat_l [J/kg]": h_ref_cond_sat_l,
"s_ref_cmp_in [J/(kg·K)]": s_ref_cmp_in,
"s_ref_cmp_out [J/(kg·K)]": s_ref_cmp_out,
"s_ref_cond_sat_v [J/(kg·K)]": s_ref_cond_sat_v,
"s_ref_exp_in [J/(kg·K)]": s_ref_exp_in,
"s_ref_exp_out [J/(kg·K)]": s_ref_exp_out,
"s_ref_evap_sat [J/(kg·K)]": s_ref_evap_sat,
"s_ref_cond_sat_l [J/(kg·K)]": s_ref_cond_sat_l,
"rho_ref_cmp_in [kg/m3]": rho_ref_cmp_in,
"mode": mode,
}
return result
[docs]
def create_lmtd_constraints() -> tuple[Any, Any]:
"""Create LMTD-based constraint functions for cycle optimization.
Optimization requires that the heat transfer calculated by LMTD matches
the heat transferred by the refrigerant cycle.
Returns
-------
tuple[Any, Any]
Tuple of constraint functions (constraint_tank, constraint_hx).
"""
def constraint_tank(perf: dict[str, Any]) -> float:
"""Condenser constraint: Q_LMTD_cond - Q_ref_cond = 0"""
if perf is None or "Q_cond" not in perf or "Q_cond_LMTD" not in perf:
return 1e6
return float(perf["Q_cond_LMTD"] - perf["Q_cond"])
def constraint_hx(perf: dict[str, Any]) -> float:
"""Evaporator constraint: Q_LMTD_evap - Q_ref_evap = 0"""
if perf is None or "Q_evap" not in perf or "Q_evap_LMTD" not in perf:
return 1e6
return float(perf["Q_evap_LMTD"] - perf["Q_evap"])
return constraint_tank, constraint_hx
[docs]
def find_ref_loop_optimal_operation(
simulator_func: Any,
refrigerant: str,
load_W: float,
initial_guess: list[float],
bounds: list[tuple[float, float]],
constraint_funcs: list[Any] | None = None,
) -> dict[str, Any] | None:
"""Find the optimal operation point for the refrigerant loop.
Minimizes compressor power while satisfying target load and LMTD constraints.
Parameters
----------
simulator_func : callable
Function that takes `[dT_ref_HX, dT_ref_tank]` and returns a perf dict.
refrigerant : str
Refrigerant name.
load_W : float
Target heat load [W].
initial_guess : list[float]
Initial guess for `[dT_evap, dT_cond]`.
bounds : list[tuple[float, float]]
Bounds for `[dT_evap, dT_cond]`.
constraint_funcs : list[callable], optional
List of constraint functions. Each takes `perf` and returns a value
that should be 0.
Returns
-------
dict[str, Any] | None
Optimal performance dictionary, or None if optimization fails.
"""
from scipy.optimize import minimize
def objective(x: np.ndarray) -> float:
perf = simulator_func(x)
if perf is None or "W_comp" not in perf:
return 1e6
# Add penalty if load is not met
load_diff = abs(perf.get("Q_cond", 0) - load_W)
penalty = (load_diff / load_W) ** 2 * 1e5 if load_W > 0 else 0
return float(perf["W_comp"] + penalty)
constraints = []
if constraint_funcs:
for cf in constraint_funcs:
def make_constraint(c_func: Any) -> Any:
def constraint(x: np.ndarray) -> float:
perf = simulator_func(x)
return float(c_func(perf))
return constraint
constraints.append({"type": "eq", "fun": make_constraint(cf)})
try:
res = minimize(
objective,
initial_guess,
bounds=bounds,
constraints=constraints,
method="SLSQP",
options={"disp": False, "ftol": 1e-4, "maxiter": 50},
)
if res.success:
return simulator_func(res.x) # type: ignore[no-any-return]
except Exception:
pass
return None