Source code for carbatpy.models.components.heat_exchanger_thermo_v2

# -*- coding: utf-8 -*-
"""
Created on Sun May 21 08:51:33 2023

@author: atakan
"""


import copy
import json
import yaml

# import src.models.fluids.fluid_props as fprop
import numpy as np
from scipy.optimize import minimize, Bounds  # root, root_scalar
import matplotlib.pyplot as plt
import carbatpy as cb


[docs] class StaticHeatExchanger: """ Class for static counter-flow heat exchanger means: no time dependence and no heat transfer coefficients * areas are used (UA)! Instead a minimum approach temperature is tried to be met. At the moment, this is mainly done by varying one of the mass flow rates. But this is sometimes not enough and a variation of the working fluid pressure will soon be included. Only the first law and second law will be checked (the latter must be improved). """ def __init__(self, fluids, h_dot_min, h_out_w, h_limit_s, **kwargs): # points=50, d_temp_separation_min=0.5, calc_type="const",name="evaporator"): """ class to calculate (static/steady state) heat-exchangers includes pinch-point analysis and plotting, only implemented for simple thermodynamic calculations (no convection coefficients and heat exchanger areas regarded yet) Parameters ---------- fluids : list of 2 fprop.Fluid The definition of the two fluids, as they enter the heat exchangers. Typically at room temperature. h_dot_min : float enthalpy flow rate (W) which has to be transfered. h_out_w : float exit enthalpy of the working fluid. h_limit_s : float, if there is a limit in enthalpy for the secondary fluid, it can be given here. It is also a starting value for possible iterations. kwargs : dict, optional Optional parameters for the heat exchanger configuration: - points : int, for how many points (array) shall the minimum approach temperature be checked and properties be returned (for plotting etc.). default 50 - d_temp_separation_min : float, Minimium approach temperature ( pinch point) between the two fluids. default 0.5 - calc_type : str, which calculation type shall be performed; only one implemented so far, default "const" - name : str, name of the heat exchanger .default "evaporator" - plot_info : dict, if not None, a Figure, an Axes, a list of What shall be plotted, a list with the colour/styles and a list with the labels must be passed. in "what", the two numbers coincide with the fluid THERMO order. The x-shift can be used in cycle calculations, to shift the curves, by the value (it will be added). The names in the dictionary are: "fig", "ax", "what","col", "label", "x-shift". Default is empty.default {} Returns ------- None. """
[docs] self.fluids = fluids
state_in_w = fluids[0].properties.state
[docs] self.m_dot_s = 0
[docs] self.h_limit_s = h_limit_s
[docs] self.q_dot = h_dot_min
[docs] self.m_dot_w = np.abs(h_dot_min/(h_out_w-state_in_w[2]))
[docs] self.h_out_w = h_out_w
[docs] self.h_out_s = float(copy.copy(h_limit_s)) # this may be varied
[docs] self.points = kwargs.get('points', 50)
[docs] self.d_temp_separation_min = kwargs.get('d_temp_separation_min', 0.5)
[docs] self.calc_type = kwargs.get('calc_type', "const")
[docs] self.name = kwargs.get('name', "evaporator")
self.plot_info = kwargs.get('plot_info', None) # self.points = points # self. d_temp_separation_min = d_temp_separation_min
[docs] self.heating = -1
if h_out_w < state_in_w[2]: self.heating = 1 # condenser (heating of the secondary fluid) # self.calc_type = calc_type # self.name = name
[docs] self.all_states = np.zeros( (2, self.points, len(cb.fprop._THERMO_STRING.split(";"))))
[docs] self.h_in_out = np.zeros((2, 4))
[docs] self.dt_mean = None
[docs] self.dt_min = None
[docs] self.dt_max = None
[docs] self.warning = 0
[docs] self.warning_message = "All o.k."
@property
[docs] def plot_info(self): return self._plot_info
@plot_info.setter def plot_info(self, value): if value is None: value = {} elif not isinstance(value, dict): raise ValueError("plot_info must be a dictionary") self._plot_info = value @property
[docs] def all(self): # Erstelle ein Dictionary aller Attribute außer self.all return {key: value for key, value in self.__dict__.items() if key != 'all'}
[docs] def to_dict(self): # Alle Attribute extrahieren, keine Methoden und self.all ausschließen result = {} for key, value in self.__dict__.items(): if key != 'all': # Beispiel für ein zu ignorierendes Attribut if isinstance(value, np.ndarray): result[key] = value.tolist() # NumPy-Arrays in Listen konvertieren elif hasattr(value, 'to_dict'): result[key] = value.to_dict() # Konvertieren, wenn `to_dict` vorhanden else: result[key] = value return result
@classmethod
[docs] def from_dict_old(cls, data): instance = cls.__new__(cls) for key, value in data.items(): if isinstance(value, list) and len(value) > 0 and isinstance(value[0], (int, float)): # Annahme: Listen sind NumPy-Arrays setattr(instance, key, np.array(value)) elif isinstance(value, dict) and 'value' in value: # Annahme: Dictionary ist eine Instanz von AnotherClass #setattr(instance, key, AnotherClass.from_dict(value)) pass else: setattr(instance, key, value) return instance
[docs] def save_to_file(self, file_path): try: data = self.to_dict() if file_path.endswith('.json'): with open(file_path, 'w') as file: json.dump(data, file, indent=4) elif file_path.endswith('.yaml') or file_path.endswith('.yml'): with open(file_path, 'w') as file: yaml.dump(data, file) else: raise ValueError("Unsupported file format. Use .json or .yaml/.yml") except Exception as e: print(f"Error saving to file: {e}")
@classmethod
[docs] def load_from_file(cls, file_path): try: if file_path.endswith('.json'): with open(file_path, 'r') as file: data = json.load(file) elif file_path.endswith('.yaml') or file_path.endswith('.yml'): with open(file_path, 'r') as file: data = yaml.load(file, Loader=yaml.SafeLoader) else: raise ValueError("Unsupported file format. Use .json or .yaml/.yml") return cls.from_dict(data) except Exception as e: print(f"Error loading from file: {e}") return None
@classmethod
[docs] def from_dict(cls, input_hex, exchanger_name="condenser", temp_ambient=None, **kwargs ): # Find the working fluid and storage fluid details if not temp_ambient: temp_ambient =cb.CB_DEFAULTS["General"]["T_SUR"] working_fluid_info = input_hex.get("working_fluid") storage_fluid_info = None for key, value in input_hex.items(): if "storage" in key.lower(): storage_fluid_info = value break if not working_fluid_info or not storage_fluid_info: raise ValueError("Missing working fluid or storage fluid information in input data.") # Initialize fluids working_fluid = cb.init_fluid(working_fluid_info["species"], working_fluid_info["fractions"], props=working_fluid_info["props"] ) storage_fluid = cb.init_fluid(storage_fluid_info["species"], storage_fluid_info["fractions"], props=storage_fluid_info["props"]) # Determine the species mappings for the given exchanger species_mapping = input_hex[exchanger_name]["species"] working_fluid_states = species_mapping["working_fluid"] storage_fluid_states = next(value for key, value in species_mapping.items() if "storage" in key.lower()) # Extract temperatures and pressures for storage fluid temp_out_storage = storage_fluid_info[storage_fluid_states["out"]] temp_in_storage = storage_fluid_info[storage_fluid_states["in"]] if temp_in_storage == "ambient": temp_in_storage = temp_ambient if temp_out_storage == "ambient": temp_out_storage = temp_ambient p_low_storage = storage_fluid_info["p_low"] # Calculate enthalpies for storage fluid state_out_storage = storage_fluid.set_state([temp_out_storage, p_low_storage], "TP") state_in_storage = storage_fluid.set_state([temp_in_storage, p_low_storage], "TP") h_in_storage = state_in_storage[2] h_out_storage = state_out_storage[2] # Determine direction of heat exchange dt_min = input_hex[exchanger_name]["dt_min"] temp_in_working = working_fluid_info[working_fluid_states["in"]] temp_out_working = working_fluid_info[working_fluid_states["out"]] if temp_in_working == "ambient": temp_in_working = temp_ambient if temp_out_working == "ambient": temp_out_working = temp_ambient if temp_in_working > temp_in_storage: heating = 1 # Evaporator (heating of secondary fluid) temp_in_working += dt_min temp_out_working += dt_min else: heating = -1 # Condenser temp_in_working -= dt_min temp_out_working -= dt_min # Calculate enthalpies for working fluid p_low_working = working_fluid_info["p_low"] state_out_working = working_fluid.set_state([temp_out_working, p_low_working], "TP") state_in_working = working_fluid.set_state([temp_in_working, p_low_working], "TP") h_in_working = state_in_working[2] h_out_working = state_out_working[2] # Create StaticHeatExchanger instance return cls( fluids=[working_fluid, storage_fluid], h_dot_min=input_hex[exchanger_name]["q_dot"], h_out_w=h_out_working, h_limit_s=h_out_storage, d_temp_separation_min=dt_min, calc_type=input_hex[exchanger_name].get("calc_type", "const"), points=input_hex[exchanger_name].get("points", 50), name=input_hex[exchanger_name].get("name", exchanger_name), plot_info=input_hex[exchanger_name].get("plot_info", None), )
[docs] def pinch_calc(self, verbose=False): """ Calculate the changes in enthalpy and temperature in the heat exchanger counter-flow hex assumed! Both flows are isobaric. Is used to check, whether the second law is violated. The factor can be used to vary the mass flow rate of the working fluid, until no violation is found (done in root finding). Parameters ---------- verbose : boolean, optional if True, several variables will be printed out. Default is False. Raises ------ Exception if temperatures are not consistent. Returns ------- m_dot_s : float secondary fluid mass flow rate (kg/s. d_tempall : numpy-array The temperature differences along the counter-flow heat exchanger. w_array : array properties of the working fluid along the heat exchanger (T,p,h, etc. see fluid class). s_array : array properties of the secondary fluid along the heat exchanger (T,p,h, etc. see fluid class). """ self.warning = 0 self.warning_message = "All o.k." w_in = copy.copy(self.fluids[0]) s_in = copy.copy(self.fluids[1]) w_out = copy.copy(self.fluids[0]) # not yet the correct state! s_out = copy.copy(self.fluids[1]) # fixed values self. h_in_out[1, 0] = s_in.properties.enthalpy self. h_in_out[0, 0] = w_in.properties.enthalpy self. h_in_out[0, 1] = self.h_out_w # fixed limiting state, secondary fluid state_out_s = s_out.set_state([self.h_out_s, s_in.properties.pressure], "HP") self. h_in_out[1, 1] = state_out_s[2] h_delta_s = np.abs( state_out_s[2] - s_in.properties.enthalpy) # fixed heat flow, determines mass flow rate self.m_dot_s = self.q_dot / h_delta_s s_array = self._calculate_state_array(s_out, self.h_in_out[1, :2]) w_array = self._calculate_state_array(w_out, self.h_in_out[0, 1::-1]) # temperature difference, ok? d_tempall = w_array[:, 0]-s_array[:, 0] self.dt_mean, self.dt_min, self.dt_max = d_tempall.mean(), np.abs( d_tempall).min(), np.abs(d_tempall).max() self._check_temperature_consistency(d_tempall) if self.plot_info: self._plot_heat_exchanger(w_array, s_array) if verbose: self._print_verbose(d_tempall) self.all_states[0,:,:] = w_array self.all_states[1,:,:] = s_array return self.m_dot_s, d_tempall*self.heating, w_array, s_array
def _pinch_root(self, h_out_s, secondary, verbose=False): """ Function for finding the minimum mean temperature difference in a heat exchanger, while not going below the minimum approach temperature. The output enthalpy of the secondary fluid is set (as default) and the heat exchanger is evaluated. Parameters ---------- h_out_s : float the output enthalpy, default would be of the secondary fluid. secondary : boolean shall the secondary fluid output be varied (or the working fluid)? Returns ------- mean T-difference, float root tries to reach a value of 0. """ if isinstance(h_out_s, float): # numpy expects a float (later) value = h_out_s else: value = h_out_s[0] if secondary: self.h_out_s = value else: self.h_out_w = value mdot_s, d_temps, wf_states, sf_states = self.pinch_calc() if self.warning < 100: return abs(self.dt_mean) return 500.0 def _p_opti_help(self, values, secondary=True, verbose=False, **kwargs): what =kwargs.get("what","TP") pressure_w, h_out = values name_o = what.replace("P","") if verbose: print("p_opti:", values, name_o,self.dt_mean) if name_o == "T": other =self.fluids[0].val_dict["Temperature"] else: raise NotImplementedError(f"pressure optimization not implemnted for {name_o}") state_in = self.fluids[0].set_state([pressure_w, other], what) self.find_pinch(secondary) if verbose: print("p_opti:", values, name_o,self.dt_mean) if self.warning ==0: return self.dt_mean return 999.0
[docs] def opti_hex_p_pinch(self, secondary=True): """ Varies fluid pressure to reach the minimum approach temperature. The function adjusts the pressure (and consequently the mass flow rate) until the specified minimum approach temperature is achieved. This defines the new exit state within the heat exchanger. If no solution is found, `self.warning` is set to 1. Parameters ---------- secondary : bool, optional Determines which fluid's output state is varied. If True, the secondary fluid is adjusted; if False, the working fluid is adjusted. The default is True. Returns ------- h_opt : float The optimized enthalpy of the secondary (or working) fluid in J/kg. Notes ----- Check the `warning` attribute of the component after execution to ensure the optimization converged. """ verbose = False h0 = copy.copy(self.h_out_s if secondary else self.h_out_w) p0 = self.fluids[0].val_dict["Pressure"] # for testing bound = Bounds(lb=[p0*.9, h0*.99], ub=[p0*1.1,h0*1.01]) # if secondary: # x0 = copy.copy(self.h_out_s) # else: # x0 = copy.copy(self.h_out_w) initial =np.array([p0, h0]) tolerance = 1e-3 try: result = minimize(self._p_opti_help, initial, args=(secondary, True), method='Nelder-Mead', tol=tolerance, bounds=bound) if verbose: print( f"result {result}, heating {self.heating}") if result.success or result.status == 2: if result.status == 2: self.warning = 2 # T-difference probably smaller self.warning_message = "Minimization problem: "+result.message if secondary: self.h_out_s = result.x[0] else: self.h_out_w = result.x[0] print( f"Min T-distance {self.dt_min:.3f}, Mean T-distance {self.dt_mean:.3f}") return result.x[0] # except: except Exception as e: self._handle_exception(e) return 10000 print("root-finding problem! (in heat_exchanger_thermo_v2.find_pinch)", result) print(f"Heating: {self.heating}") self.warning = 1 return self.warning
[docs] def find_pinch(self, secondary=True, verbose = False): """ Function tries to vary the secondary fluid enthalpy until a minimum approach temperature is reached. This also changes the mass flow rate. This is then also the new exit state within the heat exchanger. If this is also not succesful, self.warning is set to 1. This should be checked. Parameters ---------- secondary : Boolean, optional shall the output state of the secondary (True) or the working (False) fluid be varied? default = True Returns ------- float the optimized enthalpy of the secondary fluid. """ x0 = copy.copy(self.h_out_s if secondary else self.h_out_w) # if secondary: # x0 = copy.copy(self.h_out_s) # else: # x0 = copy.copy(self.h_out_w) tolerance = 1e-3 try: result = minimize(self._pinch_root, x0, args=(secondary,), method='Nelder-Mead', tol=tolerance) if verbose: print( f"result {result}, heating {self.heating}") if result.success or result.status == 2: if result.status == 2: self.warning = 2 # T-difference probably smaller self.warning_message = "Minimization problem: "+result.message if secondary: self.h_out_s = result.x[0] else: self.h_out_w = result.x[0] if verbose: print(f"Min T-distance {self.dt_min:.3f}, Mean T-distance {self.dt_mean:.3f}") return result.x[0] # except: except Exception as e: self._handle_exception(e) return 10000 print("root-finding problem! (in heat_exchanger_thermo_v2.find_pinch)", result) print(f"Heating: {self.heating}") self.warning = 1 return self.warning
[docs] def pinch_plot(self, plot_fname="", plotting=True, **kwargs): """ calculates the secondary fluid output state and mass flow, for the minimum approach temperature of the HeatExchanger instance. When wanted, this is also plotted Parameters ---------- plot_fname : string, optional file-name to store the plot. The default is "". plotting : Boolean, optional should it be plotted? The default is True. Returns ------- m_dot_s : float mass flow raete of the secondary fluid in SI units (kg/s). d_tempall : np.array the temperature differences between the two fluids along the heat exchanger. w_array : np.array [self.points, 7] the states of the working fluid along the heat exchanger. s_array : np.array [self.points, 7] the states of the secondary fluid along the heat exchanger. """ verbose =kwargs.get("verbose",False) if verbose: print(f"------pinch-plot running -----plot:{plotting}") m_dot_s, d_tempall, w_array, s_array = self.pinch_calc() if plotting: h_w_plot_array = ( w_array[:, 2] - w_array[:, 2].min()) * self.m_dot_w fig, ax_one = plt.subplots(1, 1) ax_one.plot((s_array[:, 2] - s_array[:, 2].min()) * self.m_dot_s, s_array[:, 0], "v") ax_one.plot(h_w_plot_array, w_array[:, 0], "o") ax_one.set_xlabel( "specific enthalpy flow per mass of secondary fluid / (J / kg)") ax_one.set_ylabel("temperature / (K)") ax_one.set_title("heat exchanger, simple") if plot_fname != "": fig.savefig(plot_fname) return m_dot_s, d_tempall, w_array, s_array
def _calculate_state_array(self, fluid, h_range): h_array = np.linspace(h_range[0], h_range[1], self.points) values = np.zeros((self.points, 2)) values[:, 0] = h_array values[:, 1] = fluid.properties.pressure return fluid.set_state_v(values, "HP") def _check_temperature_consistency(self, d_tempall): eps_min = -1e-3 positive = np.any(d_tempall > 0) negative = np.any(d_tempall < 0) below = True if self.heating < 0: below = False crossing = (positive > 0 and negative > 0) wrong_side = (positive > 0 and not below) or (negative > 0 and below) abs_dt_min = np.abs(self.dt_min) difference = abs_dt_min - self.d_temp_separation_min # print(f"Debug: abs_dt_min = {abs_dt_min}, d_temp_separation_min = {self.d_temp_separation_min}, difference = {difference}") if difference < eps_min: self.warning = 907 self.warning_message = "Below minimum approach temperature!" # print(f"907: {difference}, {abs_dt_min}, {self.d_temp_separation_min}") elif crossing or wrong_side: self.warning = 999 self.dt_mean = 1e6 self.warning_message = "Temperatures crossing or wrong side!" else: self.warning = 0 self.warning_message = "All o.k." def _plot_heat_exchanger(self, w_array, s_array): plot_info = self.plot_info # print("Plot info:", plot_info) # Debugging-Ausgabe if plot_info["what"][0] == 2: data_w = (w_array[:, plot_info["what"][0]] - w_array[:, plot_info["what"][0]].min()) * self.m_dot_w + plot_info["x-shift"][0] data_s = (s_array[:, plot_info["what"][0]] - s_array[:, plot_info["what"][0]].min()) * self.m_dot_s + plot_info["x-shift"][1] plot_info["ax"].plot(data_w, w_array[:, plot_info["what"][1]], plot_info["col"][0], label=plot_info["label"][0]) plot_info["ax"].plot(data_s, s_array[:, plot_info["what"][1]], plot_info["col"][1], label=plot_info["label"][1]) else: print( f"H-Ex: plotting only implemented for T-H_dot [2,0]. You requested {plot_info['what']}") def _print_verbose(self, d_tempall): print(f"Min T-distance {self.dt_min}, Mean T-distance {self.dt_mean}") if self.heating > 0: print("cond", d_tempall[0], d_tempall[-1], d_tempall.min(), d_tempall.max()) else: print("evap", d_tempall[0], d_tempall[-1], d_tempall.max(), d_tempall.min()) def _handle_exception(self, e): print("find pinch:", type(e)) print(e.args) print(e) print("root-exception", self.heating)
if __name__ == "__main__": # two test cases condenser and evaporator:
[docs] FLUID = "Propane * Pentane" # working fluid
FLS = "Methanol" # "Water" # secondary fluid comp = [.50, 0.5] flm = cb.fprop.FluidModel(FLUID) myFluid = cb.fprop.Fluid(flm, comp) secFlm = cb.fprop.FluidModel(FLS) secFluid = cb.fprop.Fluid(secFlm, [1.]) D_TEMP_MIN = 5.0 # Condenser, working fluid fixes all, secondary output enthalpy can be varied: SEC_TEMP_IN = 300.0 SEC_TEMP_OUT_MAX = 370.0 SEC_PRES_IN = 5e5 H_DOT = 1e3 state_sec_out = secFluid.set_state([SEC_TEMP_OUT_MAX, SEC_PRES_IN], "TP") state_sec_in = secFluid.set_state( [SEC_TEMP_IN, SEC_PRES_IN], "TP") # this is the entering state # working fluid TEMP_SAT_VAP = SEC_TEMP_OUT_MAX + D_TEMP_MIN state_in = myFluid.set_state( [TEMP_SAT_VAP, 1.], "TQ") # find minimum pressure WF_TEMP_IN = TEMP_SAT_VAP + D_TEMP_MIN WF_TEMP_OUT = SEC_TEMP_IN + D_TEMP_MIN state_out = myFluid.set_state([WF_TEMP_OUT, state_in[1]], "TP") # now plotting can directly be done in pinch_calc 2024-05-24 fig_act, ax_act = plt.subplots(1) PLOT_INFO = {"fig": fig_act, "ax": ax_act, "what": [2, 0], "col": ["r:", "ko"], "label": ["work,c", "sec,c"], "x-shift": [0, 0]} # a simple way to find an optimal/better pressure level pressures_good =[] p_start = state_out[1] p_range = np.linspace(p_start*.87, p_start*1.015,3) for p_act in p_range: state_in = myFluid.set_state([p_act, WF_TEMP_IN], "PT") # myFluid.print_state() hex0 = StaticHeatExchanger([myFluid, secFluid], H_DOT, state_out[2], state_sec_out[2], d_temp_separation_min=D_TEMP_MIN) factor0 = hex0.find_pinch() if hex0.warning ==0: pressures_good.append([p_act, hex0.dt_mean, hex0.warning, hex0.warning_message]) print(f"{p_act/1e5:.2f} bar", hex0.dt_mean, hex0.warning, hex0.warning_message) if hex0.warning > 0: print(hex0.warning_message) print("useful pressures:\n",pressures_good,"\n\n") # # pressure optimization 2024-07-22 # p_opt =hex0.opti_hex_p_pinch() # print(f"Optimal pressure {p_opt}, Warning: {hex0.warning_message}") hex0.plot_info = PLOT_INFO hex0.plot_info = PLOT_INFO hex0.pinch_calc(verbose=True) ax_act.legend() # ----------------------------------------------- # Previous way # ms0, d_tempall0, w0, s0 = hex0.pinch_plot("hex-plot.png") # Evaporator: ---------------------------- SEC_TEMP_IN = 300.0 SEC_TEMP_OUT = 285 SEC_PRES_IN = 15e5 H_DOT = 1e3 extra = 2 # D_TEMP_SUPER = 5. D_TEMP_MIN = 6.0 state_sec_out = secFluid.set_state([SEC_TEMP_OUT, SEC_PRES_IN], "TP") # this mus be the last set_state before the hex is constructed: state_sec_in = secFluid.set_state([SEC_TEMP_IN, SEC_PRES_IN], "TP") # WF_TEMP_IN = SEC_TEMP_OUT # - D_TEMP_MIN state_out = myFluid.set_state([SEC_TEMP_IN-D_TEMP_MIN - extra, 1.0], "TQ") state_in = myFluid.set_state( [SEC_TEMP_OUT-D_TEMP_MIN - extra, state_out[1]], "TP") # print("state in", state_in) hex1 = StaticHeatExchanger([myFluid, secFluid], H_DOT, state_out[2], state_sec_out[2], d_temp_separation_min=D_TEMP_MIN) # ms1, d_tempall1, w1, s1 = hex1.pinch_calc() factor_out = hex1.find_pinch() if hex1.warning > 2: print("Second heat exchanger:", hex1.warning_message, hex1.dt_min) else: # plotting in the same figure PLOT_INFO = {"fig": fig_act, "ax": ax_act, "what": [2, 0], "col": ["k:", "bo"], "label": ["work,e", "sec,e"], "x-shift": [0, 0]} hex1.plot_info = PLOT_INFO hex1.pinch_calc(verbose=True) ax_act.legend() #---------------------------------- # create from dictionary input_hex = { 'condenser': { 'model': "StaticHeatExchanger", "calc_type": "const", "species": {"working_fluid": {"in": "temp_high", "out": "temp_low"}, "hot_storage": {"in": "temp_low", "out": "temp_high"}}, "dt_min": 3., "q_dot": 1000., "overall_u": 100, 'area': 1, }, 'hot_storage': { "species": "Water", "fractions": [1.0], "p_low": 2e5, "temp_low": "ambient", "temp_high": 360.0, 'props': "REFPROP", }, 'working_fluid': { "species": "Propane * Butane * Pentane * Hexane", "fractions": [ 7.71733787e-01, 2.22759488e-02, 1.78685867e-01, 0.027304397199999997], "temp_low": "ambient", "temp_high": 360.0, "p_low": 1.28250708e+05, "p_high": 1.37548728e+06, "optimize": "None", "setting": "initial", 'props': "REFPROP", }, } heat_exchanger_instance = StaticHeatExchanger.from_dict(input_hex)