Source code for component.base_component

# -*- 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 = {}