444 lines
19 KiB
Python
444 lines
19 KiB
Python
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
|
|
|
|
|
|
|