# -*- coding: utf-8 -*-
"""
Created on Tue Sep 10 14:09:18 2024
@author: Elise Neven
@email: elise.neven@uliege.be
"""
from connector.mass_connector import MassConnector
from connector.work_connector import WorkConnector
from connector.heat_connector import HeatConnector
import matplotlib.pyplot as plt
import numpy as np
from CoolProp.CoolProp import PropsSI
import CoolProp.CoolProp as CP
[docs]
class BaseComponent:
"""
**Attributes**:
calculable : bool
Indicates whether the component has enough inputs to perform calculations.
parametrized : bool
Indicates whether the component has all required parameters set.
solved : bool
Indicates whether the component has been successfully solved (i.e., its state has been computed).
inputs : dict
A dictionary holding the input variables for the component.
params : dict
A dictionary holding the parameters required for the component.
guesses : dict
A dictionary holding initial guesses for solving the component.
**Methods**:
set_inputs(inputs):
Sets the input values for the component.
sync_inputs():
Synchronizes the inputs dictionary with the current state of the component's connectors.
set_parameters(parameters):
Sets the parameter values for the component and checks if it is fully parametrized.
set_guesses(guesses):
Sets initial guesses for variables to be solved.
check_calculable():
Checks if the component has all the required inputs to perform calculations.
check_parametrized():
Checks if the component has all the required parameters set.
get_required_inputs():
Returns a list of required input variables for the component. Meant to be overridden in derived classes.
get_required_parameters():
Returns a list of required parameters for the component. Meant to be overridden in derived classes.
get_required_guesses():
Returns a list of required guesses for the component.
solve():
Solves the component's state, to be implemented in derived classes.
**Notes**:
- This is a base class and should be extended for specific types of components (e.g., heat exchangers, pumps, turbines).
- The `solve` method is not implemented here and must be defined in derived classes for actual computation.
"""
def __init__(self):
self.calculable = False
self.parametrized = False
self.solved = False
self.inputs = {}
self.params = {}
self.guesses = {}
self.print_flag = 1
def set_inputs(self, **kwargs):
"""Set inputs directly through a dictionary and update connector properties."""
self.inputs.update(kwargs)
# Define mappings from input keys to methods
input_methods = {
# su connector inputs
'fluid': lambda val: self.su.set_fluid(val),
'x_su': lambda val: self.su.set_x(val),
'T_su': lambda val: self.su.set_T(val),
'h_su': lambda val: self.su.set_h(val),
'P_su': lambda val: self.su.set_p(val),
'm_dot': lambda val: self.su.set_m_dot(val),
# su_1 connector inputs
'fluid_su_1': lambda val: self.su_1.set_fluid(val),
'T_su_1': lambda val: self.su_1.set_T(val),
'h_su_1': lambda val: self.su_1.set_h(val),
'P_su_1': lambda val: self.su_1.set_p(val),
'm_dot_su_1': lambda val: self.su_1.set_m_dot(val),
# su_2 connector inputs
'fluid_su_2': lambda val: self.su_2.set_fluid(val),
'T_su_2': lambda val: self.su_2.set_T(val),
'h_su_2': lambda val: self.su_2.set_h(val),
'P_su_2': lambda val: self.su_2.set_p(val),
'm_dot_su_2': lambda val: self.su_2.set_m_dot(val),
# su_H connector inputs
'fluid_H': lambda val: self.su_H.set_fluid(val),
'T_su_H': lambda val: self.su_H.set_T(val),
'h_su_H': lambda val: self.su_H.set_h(val),
'P_su_H': lambda val: self.su_H.set_p(val),
'm_dot_H': lambda val: self.su_H.set_m_dot(val),
# su_C connector inputs
'fluid_C': lambda val: self.su_C.set_fluid(val),
'T_su_C': lambda val: self.su_C.set_T(val),
'h_su_C': lambda val: self.su_C.set_h(val),
'P_su_C': lambda val: self.su_C.set_p(val),
'm_dot_C': lambda val: self.su_C.set_m_dot(val),
# ex connector inputs
'P_ex': lambda val: self.ex.set_p(val),
'T_ex': lambda val: self.ex.set_T(val),
'h_ex': lambda val: self.ex.set_h(val),
'x_ex': lambda val: self.ex.set_x(val),
# ex_1 connector inputs
'P_ex_1': lambda val: self.ex_1.set_p(val),
'T_ex_1': lambda val: self.ex_1.set_T(val),
'h_ex_1': lambda val: self.ex_1.set_h(val),
# ex_2 connector inputs
'P_ex_2': lambda val: self.ex_2.set_p(val),
'T_ex_2': lambda val: self.ex_2.set_T(val),
'h_ex_2': lambda val: self.ex_2.set_h(val),
# ex_H connector inputs
'P_ex_H': lambda val: self.ex_H.set_p(val),
'T_ex_H': lambda val: self.ex_H.set_T(val),
'h_ex_H': lambda val: self.ex_H.set_h(val),
# ex_C connector inputs
'P_ex_C': lambda val: self.ex_C.set_p(val),
'T_ex_C': lambda val: self.ex_C.set_T(val),
'h_ex_C': lambda val: self.ex_C.set_h(val),
# W connector inputs
'N_rot': lambda val: self.W.set_N_rot(val),
# Q connector inputs
'Q_dot': lambda val: self.Q.set_Q_dot(val),
# Q_amb connector inputs
'T_amb': lambda val: self.Q_amb.set_T_amb(val),
# Solar
'DNI': lambda val:val,
'Theta': lambda val:val,
'v_wind': lambda val:val,
}
unknown_keys = [] # To collect any keys that do not match the input methods
for key, value in self.inputs.items():
method = input_methods.get(key)
if method:
try:
method(value)
except Exception as e:
# Optionally log the exception or raise with more context
pass # Replace with logging if desired
else:
unknown_keys.append(key)
if unknown_keys:
raise ValueError(f"Unrecognized input keys: {', '.join(unknown_keys)}")
return
def sync_inputs(self):
"""Synchronize the inputs dictionary with the connector states."""
# Lazy getters: only access if the connector exists
attribute_map = {
# su connectors
'fluid': lambda: self.su.fluid if hasattr(self,'su') else None,
'T_su': lambda: self.su.T if hasattr(self,'su') else None,
'h_su': lambda: self.su.h if hasattr(self,'su') else None,
'P_su': lambda: self.su.p if hasattr(self,'su') else None,
'm_dot': lambda: self.su.m_dot if hasattr(self, 'su') else None,
# su_H connector
'fluid_H': lambda: self.su_H.fluid if hasattr(self,'su_H') else None,
'T_su_H': lambda: self.su_H.T if hasattr(self,'su_H') else None,
'h_su_H': lambda: self.su_H.h if hasattr(self,'su_H') else None,
'P_su_H': lambda: self.su_H.p if hasattr(self,'su_H') else None,
'm_dot_H': lambda: self.su_H.m_dot if hasattr(self,'su_H') else None,
# su_C connector
'fluid_C': lambda: self.su_C.fluid if hasattr(self,'su_C') else None,
'T_su_C': lambda: self.su_C.T if hasattr(self,'su_C') else None,
'h_su_C': lambda: self.su_C.h if hasattr(self,'su_C') else None,
'P_su_C': lambda: self.su_C.p if hasattr(self,'su_C') else None,
'm_dot_C': lambda: self.su_C.m_dot if hasattr(self,'su_C') else None,
# ex connector
'P_ex': lambda: self.ex.p if hasattr(self,'ex') else None,
'T_ex': lambda: self.ex.T if hasattr(self,'ex') else None,
'h_ex': lambda: self.ex.h if hasattr(self,'ex') else None,
# ex_C connector
'P_ex_C': lambda: self.ex_C.p if hasattr(self,'ex_C') else None,
'T_ex_C': lambda: self.ex_C.T if hasattr(self,'ex_C') else None,
'h_ex_C': lambda: self.ex_C.h if hasattr(self,'ex_C') else None,
# ex_H connector
'P_ex_H': lambda: self.ex_H.p if hasattr(self,'ex_H') else None,
'T_ex_H': lambda: self.ex_H.T if hasattr(self,'ex_H') else None,
'h_ex_H': lambda: self.ex_H.h if hasattr(self,'ex_H') else None,
# W connector
'N_rot': lambda: self.W.N_rot if hasattr(self,'W') else None,
# Q connector
'Q_dot': lambda: self.Q.Q_dot if hasattr(self,'Q') else None,
# Q_amb connector
'T_amb': lambda: self.Q_amb.T_amb if hasattr(self,'Q_amb') else None,
# Solar
'DNI': lambda: self.DNI if hasattr(self,'DNI') else None,
'Theta': lambda: self.Theta if hasattr(self,'Theta') else None,
'v_wind': lambda: self.v_wind if hasattr(self,'v_wind') else None,
}
self.inputs = getattr(self,'inputs',{})
for key, getter in attribute_map.items():
try:
value = getter()
if value is not None:
self.inputs[key] = value
except Exception:
pass # Optional: add logging for debugging
return
def mute_print(self):
self.print_flag = 0
return
def print_setup(self):
self.sync_inputs()
print("=== Component Setup ===")
print("\nInputs:")
for input in self.get_required_inputs():
if input in self.inputs:
print(f" - {input}: {self.inputs[input]}")
else:
print(f" - {input}: Not set")
print("\nParameters:")
for param in self.get_required_parameters():
if param in self.params:
print(f" - {param}: {self.params[param]}")
else:
print(f" - {param}: Not set")
print("======================")
def set_parameters(self, **parameters):
for key, value in parameters.items():
self.params[key] = value
def set_guesses(self, **guesses):
for key, value in guesses.items():
self.guesses[key] = value
def check_calculable(self):
self.sync_inputs()
required_inputs = self.get_required_inputs()
self.calculable = all(self.inputs.get(inp) is not None for inp in required_inputs) # check if all required inputs are set
if not self.calculable:
if self.print_flag:
print(f"Component {self.__class__.__name__} is not calculable. Missing inputs: {', '.join([inp for inp in required_inputs if self.inputs.get(inp) is None])}")
return self.calculable
def check_parametrized(self):
required_params = self.get_required_parameters()
self.parametrized = all(self.params.get(param) is not None for param in required_params) # check if all required parameters are set
if not self.parametrized:
if self.print_flag:
print(f"Component {self.__class__.__name__} is not parametrized. Missing parameters: {', '.join([param for param in required_params if self.params.get(param) is None])}")
return self.parametrized
def get_required_inputs(self):
# This method should be overridden in derived classes
return []
def get_required_parameters(self):
# This method should be overridden in derived classes
return []
def get_required_guesses(self):
return []
def solve(self):
# This method should be overridden in derived classes
raise NotImplementedError("The 'solve' method should be implemented in derived classes.")
def plot_Ts(self, fig = None, color = 'b', choose_HX_side = None):
"1) Initialize the graph and inlet, outlet property containers"
if fig is None:
fig = plt.figure()
su = []
ex = []
prop_2 = 'T'
prop_1 = 's'
"2) Determine the component supply and exhaust ports"
for attr, val in self.__dict__.items():
if "su" in attr and isinstance(val, MassConnector):
su.append([attr, val, attr[2:]])
if "ex" in attr and isinstance(val, MassConnector):
ex.append([attr, val, attr[2:]])
"3) Get properties"
for i in range(len(su)):
su[i].append({prop_1 : getattr(su[i][1], prop_1),
prop_2 : getattr(su[i][1], prop_2)})
for i in range(len(ex)):
ex[i].append({prop_1 : getattr(ex[i][1], prop_1),
prop_2 : getattr(ex[i][1], prop_2)})
"4) Form couples"
# Separate by suffix
su_by_suffix = { s[2]: s for s in su }
ex_by_suffix = { e[2]: e for e in ex }
if choose_HX_side is not None:
su_by_suffix = {
k: v for k, v in su_by_suffix.items()
if choose_HX_side in k
}
ex_by_suffix = {
k: v for k, v in ex_by_suffix.items()
if choose_HX_side in k
}
couple_1 = []
couple_2 = []
su_keys = set(su_by_suffix.keys())
ex_keys = set(ex_by_suffix.keys())
# Case 1: normal one-to-one matching
if su_keys == ex_keys:
for suf in su_keys:
su_elem = su_by_suffix[suf]
ex_elem = ex_by_suffix[suf]
couple_1.append([
su_elem[3][prop_1],
ex_elem[3][prop_1]
])
couple_2.append([
su_elem[3][prop_2],
ex_elem[3][prop_2]
])
# Case 2: one supply, many exhaust
elif len(su_keys) == 1:
su_elem = su_by_suffix[next(iter(su_keys))]
for suf in ex_keys:
ex_elem = ex_by_suffix[suf]
couple_1.append([
su_elem[3][prop_1],
ex_elem[3][prop_1]
])
couple_2.append([
su_elem[3][prop_2],
ex_elem[3][prop_2]
])
# Case 3: many supply, one exhaust
elif len(ex_keys) == 1:
ex_elem = ex_by_suffix[next(iter(ex_keys))]
for suf in su_keys:
su_elem = su_by_suffix[suf]
couple_1.append([
su_elem[3][prop_1],
ex_elem[3][prop_1]
])
couple_2.append([
su_elem[3][prop_2],
ex_elem[3][prop_2]
])
# Case 4: incompatible
else:
raise ValueError(
f"Incompatible suffix sets: su={su_keys}, ex={ex_keys}"
)
# Check whether the component is a HX, if yes, multi-phase shall be considered
su_suffixes = su_by_suffix.keys()
ex_suffixes = ex_by_suffix.keys()
has_H = any('_H' in suf for suf in su_suffixes) or any('_H' in suf for suf in ex_suffixes)
has_C = any('_C' in suf for suf in su_suffixes) or any('_C' in suf for suf in ex_suffixes)
"3.1) IF the component is a HX : Detect multi phase operation by plotting along the isobar"
if has_H or has_C:
couple_1_discretized = []
couple_2_discretized = []
n_points = 10000
if choose_HX_side is not None:
# Only generate couples for the chosen side
suffix = "_" + choose_HX_side
su_conn = su_by_suffix[suffix][1]
ex_conn = ex_by_suffix[suffix][1]
h_in, h_out = su_conn.h, ex_conn.h
s_in, s_out = su_conn.s, ex_conn.s
P_in, P_out = su_conn.p, ex_conn.p
fluid = su_conn.fluid
AS = CP.AbstractState('BICUBIC&HEOS', fluid)
h_array = np.linspace(h_in, h_out, n_points)
s_array = np.linspace(s_in, s_out, n_points)
P_array = np.linspace(P_in, P_out, n_points)
T_array = np.zeros(len(h_array))
for i in range(len(h_array)):
AS.update(CP.HmassP_INPUTS, h_array[i], P_array[i])
T_array[i] = AS.T()
couple_1_discretized.append(s_array)
couple_2_discretized.append(T_array)
else:
# Generate couples for all sides present in su_by_suffix
for suf, su_item in su_by_suffix.items():
# Only consider matching exhaust
if suf not in ex_by_suffix:
continue
su_conn = su_item[1]
ex_conn = ex_by_suffix[suf][1]
h_in, h_out = su_conn.h, ex_conn.h
s_in, s_out = su_conn.s, ex_conn.s
P_in, P_out = su_conn.p, ex_conn.p
fluid = su_conn.fluid
AS = CP.AbstractState('BICUBIC&HEOS', fluid)
h_array = np.linspace(h_in, h_out, n_points)
s_array = np.linspace(s_in, s_out, n_points)
P_array = np.linspace(P_in, P_out, n_points)
T_array = np.zeros(len(h_array))
for i in range(len(h_array)):
AS.update(CP.HmassP_INPUTS, h_array[i], P_array[i])
T_array[i] = AS.T()
couple_1_discretized.append(s_array)
couple_2_discretized.append(T_array)
# Replace couple_1 / couple_2 with the discretized arrays
couple_1 = couple_1_discretized
couple_2 = couple_2_discretized
# ------------------------------------------------------
"4) Plot couples"
for i in range(len(couple_1)):
c1 = couple_1[i]
c2 = couple_2[i]
plt.plot(c1, c2, color = color)
plt.scatter([c1[0], c1[-1]], [c2[0], c2[-1]], color=color, zorder=5)
plt.grid()
plt.xlabel("Entropy [J/(kg*K)]")
plt.ylabel("Temperature [K]")
return fig
def reset_inputs(self):
for attr, val in self.__dict__.items():
if hasattr(val, "reset"):
val.reset()
self.inputs = {}