from refDLL import RefProp, RegDllCall import numpy as np import matplotlib.pyplot as plt from plotly import graph_objs as go import pandas as pd import altair as alt alt.data_transformers.disable_max_rows() # Setting default font sizes for plots SMALL_SIZE = 10 MEDIUM_SIZE = 22 BIGGER_SIZE = 28 class Diagram_PH: """Class to define and plot PH diagrams for a specified refrigerant.""" def __init__(self, REFRIG): """ Initialize the Diagram_PH class with a specific refrigerant. Args: REFRIG (str): The name of the refrigerant. """ self.Refname = REFRIG # Name of the refrigerant self.callref = RegDllCall(self.Refname) # Register DLL call with the refrigerant name self.Hsl, self.Hsv, self.Psat, self.Tsat = self.get_psat_values() # Get saturation values self.Tmax, self.Tmin, self.T_lst, self.P, self.IsoT_lst = self.get_IsoT_values() # Get isothermal values self.extra_points = [] # List for additional points to be plotted self.extra_points_order = [] # List to store the order of the extra points self.extra_dict = {} # Dictionary to store extra points by order self.nodes = [] # List for node annotations in Plotly plots def clearAllExtraPoint(self): """Clear all extra points previously added to the diagram.""" self.extra_points = [] self.extra_points_order = [] self.extra_dict = {} def get_psat_values(self): """ Calculate the psat values for the refrigerant. Returns: tuple: The Hsl, Hsv, Psat, and Tsat values. """ Hsl, Hsv, Psat, Tsat = [], [], [], [] # Calculate values for different pressures in the range of the refrigerant's pressure for p in np.arange(self.callref.refrig.p_begin(), self.callref.refrig.p_end(), 0.5e5): # Calculate and append the liquid enthalpy for the given pressure Hsl.append(self.callref.refrig.hsl_px(p, 0) / 1e3) # Calculate and append the vapor enthalpy for the given pressure Hsv.append(self.callref.refrig.hsv_px(p, 1) / 1e3) # Append the pressure Psat.append(p / 1e5) # Calculate and append the saturation temperature for the given pressure Tsat.append(self.callref.refrig.T_px(p, 0.5)) # Stop calculation if the liquid enthalpy doesn't change anymore if len(Hsl) > 2 and Hsl[-1] == Hsl[-2]: break return Hsl, Hsv, Psat, Tsat def add_points_common(self, refppt, points, is_ordered=False): """ Add extra points to the diagram. Args: refppt (int): The property pair identifier. points (dict): The points to be added. is_ordered (bool, optional): Whether the points are ordered. Defaults to False. """ # Mapping for the h calculation functions h_calc_funcs = { RefProp.PX: lambda p, x: self.callref.refrig.h_px(p, x), RefProp.PT: self.callref.H_pT, RefProp.TSX: lambda t, x: self.callref.h_px(self.callref.p_Tx(t, round(x)), x), RefProp.TSSH: lambda t, x: self.callref.h_px(self.callref.p_Tx(t, round(x)), x), } # Mapping for the p calculation functions p_calc_funcs = { RefProp.PX: lambda p, _: p, RefProp.PT: lambda p, _: p, RefProp.TSX: lambda t, x: self.callref.p_Tx(t, round(x)), RefProp.TSSH: lambda t, x: self.callref.p_Tx(t, round(x)), } # Iterate over points extra_dict = {} for _, i in enumerate(points): point = points[i] # Calculate h and p values using the corresponding function h = h_calc_funcs[refppt](*point) p = p_calc_funcs[refppt](*point) if is_ordered: extra_dict[i] = (h * 1e-3, p * 1e-5) # Use index as order else: # If the points are not ordered, simply append them to the list self.extra_points.append([h * 1e-3, p * 1e-5]) # If the points are ordered, store them in the dictionary using the index as the order if is_ordered: self.extra_dict.update(extra_dict) def add_points(self, data): """ Add extra points to the diagram. Args: data (dict): The points to be added. """ for refppt, points in data.items(): self.add_points_common(refppt, points, False) def add_points_order(self, data): """ Add extra ordered points to the diagram. Args: data (dict): The points to be added. """ for refppt, points in data.items(): self.add_points_common(refppt, points, True) self.extra_points_order = sorted(self.extra_dict.items(), key=lambda item: item[0]) def get_IsoT_values(self): """ Calculate the isothermal values for the refrigerant. Returns: tuple: The Tmax, Tmin, T_lst, P, and IsoT_lst values. """ # Calculate the temperatures for different pressures in the range of the refrigerant's pressure T = [self.callref.refrig.T_px(p, 0.5) - 273.15 for p in np.arange(self.callref.refrig.p_begin(), self.callref.refrig.p_end(), 50e5)] # Find the maximum and minimum saturation temperatures Tmax, Tmin = max(self.Tsat) - 273.15 - 1, min(self.Tsat) - 273.15 # Find the list of temperatures to use for isothermal calculations T_lst = self.callref.findwhole10number(Tmin, Tmax) # Generate pressures to use for isothermal calculations P = np.arange(self.callref.refrig.p_begin(), self.callref.refrig.p_end(), 0.05e5) # Calculate isothermal values for each temperature in the list IsoT_lst = [[self.callref.refrig.h_pT(p, temp + 273.15) / 1e3 for p in P] for temp in T_lst] data = { 'Temperature': T_lst.repeat(len(P)), 'Pressure': np.tile(P, len(T_lst)), 'Enthalpy': np.concatenate(IsoT_lst) } df = pd.DataFrame(data) # Save the dataframe to a CSV file for later analysis # df.to_csv(r'C:\Users\serameza\impact\EMEA_MBD_GitHub\CheckLabdata\IsothermalData.csv', index=False) return Tmax, Tmin, T_lst, P, IsoT_lst def add_points_df(self, df): """ Add points to the diagram from a DataFrame, considering groups and including the node index. Args: df (DataFrame): DataFrame containing the data points to be added. """ df_sorted = df.sort_values(by='Order') for idx, row in df_sorted.iterrows(): # Include 'Node' from the DataFrame index in the extra_dict self.extra_dict[row['Order']] = { 'Enthalpy': row['Enthalpy'] / 1e3, # Convert to kJ/kg if originally in J/kg 'Pressure': row['Pressure'] / 1e5, # Convert to bar if originally in Pa 'Group': row.get('Group'), 'Node': idx # Assuming 'idx' is the node index from the original DataFrame } # Sort the extra points by their order for later plotting self.extra_points_order = sorted(self.extra_dict.items(), key=lambda item: item[0]) def plot_diagram(self): """Plot the PH diagram using Matplotlib.""" plt.rc('font', size=SMALL_SIZE) # Controls default text sizes plt.rc('axes', titlesize=SMALL_SIZE) # Font size of the axes title plt.rc('axes', labelsize=MEDIUM_SIZE) # Font size of the x and y labels plt.rc('xtick', labelsize=SMALL_SIZE) # Font size of the tick labels plt.rc('ytick', labelsize=SMALL_SIZE) # Font size of the tick labels plt.rc('legend', fontsize=SMALL_SIZE) # Legend font size plt.rc('figure', titlesize=BIGGER_SIZE) # Font size of the figure title plt.figure(figsize=[15, 10]) # Plot saturation lines plt.plot(self.Hsl, self.Psat, 'k-', label='Liquid Saturation') plt.plot(self.Hsv, self.Psat, 'k-', label='Vapor Saturation') # Plot isotherms for Th_lst, temp in zip(self.IsoT_lst, self.T_lst): plt.plot(Th_lst, self.P / 1e5, 'g--', label=f'{temp}°C Isotherm', alpha=0.5) plt.annotate('{:.0f}°C'.format(temp), (self.callref.refrig.h_px(self.callref.refrig.p_Tx(temp + 273.15, 0.5), 0.1) / 1e3, self.callref.refrig.p_Tx(temp + 273.15, 0.5) / 1e5), ha='center', backgroundcolor="white") plt.yscale('log') # Plot additional points, grouped and connected by lines if applicable if self.extra_points_order: # Extract the groups and points for plotting df = pd.DataFrame(self.extra_points_order, columns=['Order', 'Data']) df['Enthalpy'] = df['Data'].apply(lambda x: x['Enthalpy']) df['Pressure'] = df['Data'].apply(lambda x: x['Pressure']) df['Group'] = df['Data'].apply(lambda x: x.get('Group', 'Unspecified')) plt.plot(df['Enthalpy'], df['Pressure'], '-o', zorder=10) # Plot points by group and connect them for group, group_df in df.groupby('Group'): if group != 'Unspecified': # Only plot specified groups with lines group_df = group_df.sort_values(by='Pressure') plt.plot(group_df['Enthalpy'], group_df['Pressure'], '-o', zorder=10) plt.xlabel('Enthalpy [kJ/kg]') plt.ylabel('Pressure [bar]') plt.title(f'PH Diagram for {self.Refname}') plt.grid(True, which='both', linestyle='--') plt.tight_layout() return plt def plot_diagram_plotly(self): """Plot the PH diagram interactively using Plotly, with points connected by group.""" fig = go.Figure() # Saturation lines fig.add_trace(go.Scatter(x=self.Hsl, y=self.Psat, mode='lines', name='Liquid Saturation', line=dict(color='black'))) fig.add_trace(go.Scatter(x=self.Hsv, y=self.Psat, mode='lines', name='Vapor Saturation', line=dict(color='black'))) # Isotherms for Th_lst, temp in zip(self.IsoT_lst, self.T_lst): fig.add_trace(go.Scatter(x=Th_lst, y=self.P / 1e5, mode='lines', name=f'{temp}°C Isotherm', line=dict(color='green', dash='dash', width=0.5))) # Add annotation for each isotherm enthalpy_at_mid_pressure = self.callref.refrig.h_px(self.callref.refrig.p_Tx(temp + 273.15, 0.5), 0.1) / 1e3 pressure_at_mid_point = self.callref.refrig.p_Tx(temp + 273.15, 0.5) / 1e5 pressure_at_mid_point = np.log10(pressure_at_mid_point) fig.add_annotation( x=enthalpy_at_mid_pressure, y=pressure_at_mid_point, text=f'{temp:.0f}°C', showarrow=False, bgcolor="white" ) if self.extra_points_order: # Prepare a DataFrame for easier handling df = pd.DataFrame([(order, data) for order, data in self.extra_points_order], columns=['Order', 'Data']) df['Enthalpy'] = df['Data'].apply(lambda x: x['Enthalpy']) df['Pressure'] = df['Data'].apply(lambda x: x['Pressure']) df['Group'] = df['Data'].apply(lambda x: x.get('Group', 'Unspecified')) df['Node'] = df['Data'].apply(lambda x: x['Node']) # Assuming 'self.nodes' are in the same order as 'self.extra_points_order' fig.add_trace(go.Scatter(x=df['Enthalpy'], y=df['Pressure'], mode='markers+lines', name='Ordered Points', line=dict(color='red'), hoverinfo='text', text=df['Node'])) # Plot points by group for _, group_df in df.groupby('Group'): fig.add_trace(go.Scatter( x=group_df['Enthalpy'], y=group_df['Pressure'], line=dict(color='red'), hoverinfo='text', text=group_df['Node'], mode='markers+lines' )) # Update layout for readability fig.update_layout( xaxis=dict( title='Enthalpie [kJ/kg]', # Title of x-axis showgrid=True, # Show grid gridcolor='LightPink', # Grid color linecolor='black', # Axis line color linewidth=0.3, # Axis line width mirror=True, # Mirror axis lines ), yaxis=dict( title='Pression [bar]', # Title of y-axis type='log', # Use logarithmic scale showgrid=True, # Show grid gridcolor='LightBlue', # Grid color linecolor='black', # Axis line color linewidth=0.3, # Axis line width mirror=True, # Mirror axis lines ), showlegend=False, # Hide legend autosize=True, width=1000, height=800, margin=dict(l=100, r=50, b=100, t=100, pad=4), plot_bgcolor="white", ) return fig def plot_diagram_altair(self): """Plot the PH diagram using Altair.""" # Convert lists to DataFrame for Altair data_saturationL = pd.DataFrame({ 'Enthalpy': self.Hsl, 'Pressure': self.Psat, 'Type': ['Liquid Saturation'] * len(self.Hsl) }) data_saturationV = pd.DataFrame({ 'Enthalpy': self.Hsv, 'Pressure': self.Psat, 'Type': ['Liquid Saturation'] * len(self.Hsv) }) # Isotherms and annotations data_isotherms = pd.DataFrame({ 'Enthalpy': np.concatenate(self.IsoT_lst), 'Pressure': np.tile(self.P / 1e5, len(self.T_lst)), 'Temperature': np.repeat(self.T_lst, len(self.P)) }) df_extra = pd.DataFrame() # Additional points, if present if self.extra_points_order: # Prepare a DataFrame for easier handling df = pd.DataFrame([(order, data) for order, data in self.extra_points_order], columns=['Order', 'Data']) df_extra['Enthalpy'] = df['Data'].apply(lambda x: x['Enthalpy']) df_extra['Pressure'] = df['Data'].apply(lambda x: x['Pressure']) df_extra['Group'] = df['Data'].apply(lambda x: x.get('Group', 'Unspecified')) df_extra['Node'] = df['Data'].apply(lambda x: x['Node']) # Assuming 'self.nodes' are in the same order as 'self.extra_points_order' df_extra['Order'] = df.index.to_list() else: df_extra = pd.DataFrame(columns=['Enthalpy', 'Pressure', 'Node']) # Create the base chart base = alt.Chart().encode( x=alt.X('Enthalpy:Q', title='Enthalpie [kJ/kg]'), y=alt.Y('Pressure:Q', title='Pression [bar]', scale=alt.Scale(type='log')) ) # Liquid saturation chart chart_saturationL = alt.Chart(data_saturationL).mark_line().encode( x='Enthalpy:Q', y=alt.Y('Pressure:Q', scale=alt.Scale(type='log')), ) # Vapor saturation chart median_pressure = data_saturationV['Pressure'].median() # Split DataFrame into two parts data_upper = data_saturationV[data_saturationV['Pressure'] > median_pressure] data_lower = data_saturationV[data_saturationV['Pressure'] <= median_pressure] # Create and combine charts chart_upper = alt.Chart(data_upper).mark_point(filled=True, size=2).encode( x='Enthalpy:Q', y=alt.Y('Pressure:Q', scale=alt.Scale(type='log')) ) chart_lower = alt.Chart(data_lower).mark_line().encode( x='Enthalpy:Q', y=alt.Y('Pressure:Q', scale=alt.Scale(type='log')) ) chart_saturationV = chart_upper + chart_lower data_isotherms.sort_values(by=['Enthalpy'], inplace=True, ascending=[False]) data_isotherms['Order'] = data_isotherms.index.to_list() # Isotherms chart chart_isotherms = alt.Chart(data_isotherms).mark_line(opacity=0.5).encode( x='Enthalpy:Q', y='Pressure:Q', order='Order:N', color=alt.Color('Temperature:Q', legend=alt.Legend(title="Temperature (°C)")) ) # Add annotations for isotherms (Altair does not handle annotations directly like Plotly) df_extra['Group'] = df_extra['Group'].fillna(-2000).astype(str) # Additional points grouped = df_extra[df_extra['Group'] != -2000] grouped['Type'] = grouped['Order'] brush = alt.selection_interval() # Safety check before using the DataFrame if isinstance(grouped, pd.DataFrame) and 'Group' in grouped.columns: group_chart = alt.Chart(grouped).mark_line(point=True).encode( x='Enthalpy:Q', y='Pressure:Q', detail='Group:N', color=alt.value('red'), tooltip=['Node:N', 'Enthalpy:Q', 'Pressure:Q'] ).add_params( brush ) else: print("Error: 'grouped' is not a DataFrame or missing necessary columns") # Similar check for 'df_extra' if isinstance(df_extra, pd.DataFrame) and {'Enthalpy', 'Pressure', 'Node', 'Order'}.issubset(df_extra.columns): ungroup_chart = alt.Chart(df_extra).mark_line(point=True).encode( x='Enthalpy:Q', y=alt.Y('Pressure:Q', scale=alt.Scale(type='log')), tooltip=['Node:N', 'Enthalpy:Q', 'Pressure:Q'], order='Order:O' ).add_params( brush ) else: print("Error: 'df_extra' is not a DataFrame or missing necessary columns") # Combine charts only if both are defined if group_chart and ungroup_chart: chart_extra_points = group_chart + ungroup_chart else: chart_extra_points = group_chart if group_chart else ungroup_chart # Combine all charts final_chart = chart_saturationL + chart_saturationV + chart_isotherms if chart_extra_points is not None: final_chart += chart_extra_points # Final configuration and display final_chart = final_chart.properties( width=800, height=600 ).configure_axis( grid=True ).configure_view( strokeWidth=0 ) interactive_scatter = final_chart.encode().brush( alt.selection_interval() ).interactive() return interactive_scatter