Spaces:
Sleeping
Sleeping
""" | |
Psychrometric visualization module for HVAC Load Calculator. | |
This module provides visualization tools for psychrometric processes. | |
""" | |
import streamlit as st | |
import pandas as pd | |
import numpy as np | |
import plotly.graph_objects as go | |
import plotly.express as px | |
from typing import Dict, List, Any, Optional, Tuple | |
import math | |
# Import psychrometrics module | |
from utils.psychrometrics import Psychrometrics | |
class PsychrometricVisualization: | |
"""Class for psychrometric visualization.""" | |
def __init__(self): | |
"""Initialize psychrometric visualization.""" | |
self.psychrometrics = Psychrometrics() | |
# Define temperature and humidity ratio ranges for chart | |
self.temp_min = -10 | |
self.temp_max = 50 | |
self.w_min = 0 | |
self.w_max = 0.030 | |
# Define standard atmospheric pressure | |
self.pressure = 101325 # Pa | |
def create_psychrometric_chart(self, points: Optional[List[Dict[str, Any]]] = None, | |
processes: Optional[List[Dict[str, Any]]] = None, | |
comfort_zone: Optional[Dict[str, Any]] = None) -> go.Figure: | |
""" | |
Create an interactive psychrometric chart. | |
Args: | |
points: List of points to plot on the chart | |
processes: List of processes to plot on the chart | |
comfort_zone: Dictionary with comfort zone parameters | |
Returns: | |
Plotly figure with psychrometric chart | |
""" | |
# Create figure | |
fig = go.Figure() | |
# Generate temperature and humidity ratio grids | |
temp_range = np.linspace(self.temp_min, self.temp_max, 100) | |
w_range = np.linspace(self.w_min, self.w_max, 100) | |
# Generate saturation curve | |
sat_temps = np.linspace(self.temp_min, self.temp_max, 100) | |
sat_w = [self.psychrometrics.humidity_ratio(t, 100, self.pressure) for t in sat_temps] | |
# Plot saturation curve | |
fig.add_trace(go.Scatter( | |
x=sat_temps, | |
y=sat_w, | |
mode="lines", | |
line=dict(color="blue", width=2), | |
name="Saturation Curve" | |
)) | |
# Generate constant RH curves | |
rh_values = [10, 20, 30, 40, 50, 60, 70, 80, 90] | |
for rh in rh_values: | |
rh_temps = np.linspace(self.temp_min, self.temp_max, 50) | |
rh_w = [self.psychrometrics.humidity_ratio(t, rh, self.pressure) for t in rh_temps] | |
# Filter out values above saturation | |
valid_points = [(t, w) for t, w in zip(rh_temps, rh_w) if w <= self.psychrometrics.humidity_ratio(t, 100, self.pressure)] | |
if valid_points: | |
valid_temps, valid_w = zip(*valid_points) | |
fig.add_trace(go.Scatter( | |
x=valid_temps, | |
y=valid_w, | |
mode="lines", | |
line=dict(color="rgba(0, 0, 255, 0.3)", width=1, dash="dot"), | |
name=f"{rh}% RH", | |
hoverinfo="name" | |
)) | |
# Generate constant wet-bulb temperature lines | |
wb_values = np.arange(0, 35, 5) | |
for wb in wb_values: | |
wb_temps = np.linspace(wb, self.temp_max, 50) | |
wb_points = [] | |
for t in wb_temps: | |
# Binary search to find humidity ratio for this wet-bulb temperature | |
w_low = 0 | |
w_high = self.psychrometrics.humidity_ratio(t, 100, self.pressure) | |
for _ in range(10): # 10 iterations should be enough for good precision | |
w_mid = (w_low + w_high) / 2 | |
rh = self.psychrometrics.relative_humidity(t, w_mid, self.pressure) | |
t_wb_calc = self.psychrometrics.wet_bulb_temperature(t, rh, self.pressure) | |
if abs(t_wb_calc - wb) < 0.1: | |
wb_points.append((t, w_mid)) | |
break | |
elif t_wb_calc < wb: | |
w_low = w_mid | |
else: | |
w_high = w_mid | |
if wb_points: | |
wb_temps, wb_w = zip(*wb_points) | |
fig.add_trace(go.Scatter( | |
x=wb_temps, | |
y=wb_w, | |
mode="lines", | |
line=dict(color="rgba(0, 128, 0, 0.3)", width=1, dash="dash"), | |
name=f"{wb}°C WB", | |
hoverinfo="name" | |
)) | |
# Generate constant enthalpy lines | |
h_values = np.arange(0, 100, 10) * 1000 # kJ/kg to J/kg | |
for h in h_values: | |
h_temps = np.linspace(self.temp_min, self.temp_max, 50) | |
h_points = [] | |
for t in h_temps: | |
# Calculate humidity ratio for this enthalpy | |
w = self.psychrometrics.find_humidity_ratio_for_enthalpy(t, h) | |
if 0 <= w <= self.psychrometrics.humidity_ratio(t, 100, self.pressure): | |
h_points.append((t, w)) | |
if h_points: | |
h_temps, h_w = zip(*h_points) | |
fig.add_trace(go.Scatter( | |
x=h_temps, | |
y=h_w, | |
mode="lines", | |
line=dict(color="rgba(255, 0, 0, 0.3)", width=1, dash="dashdot"), | |
name=f"{h/1000:.0f} kJ/kg", | |
hoverinfo="name" | |
)) | |
# Generate constant specific volume lines | |
v_values = [0.8, 0.85, 0.9, 0.95, 1.0, 1.05] | |
for v in v_values: | |
v_temps = np.linspace(self.temp_min, self.temp_max, 50) | |
v_points = [] | |
for t in h_temps: | |
# Binary search to find humidity ratio for this specific volume | |
w_low = 0 | |
w_high = self.psychrometrics.humidity_ratio(t, 100, self.pressure) | |
for _ in range(10): # 10 iterations should be enough for good precision | |
w_mid = (w_low + w_high) / 2 | |
v_calc = self.psychrometrics.specific_volume(t, w_mid, self.pressure) | |
if abs(v_calc - v) < 0.01: | |
v_points.append((t, w_mid)) | |
break | |
elif v_calc < v: | |
w_low = w_mid | |
else: | |
w_high = w_mid | |
if v_points: | |
v_temps, v_w = zip(*v_points) | |
fig.add_trace(go.Scatter( | |
x=v_temps, | |
y=v_w, | |
mode="lines", | |
line=dict(color="rgba(128, 0, 128, 0.3)", width=1, dash="longdash"), | |
name=f"{v:.2f} m³/kg", | |
hoverinfo="name" | |
)) | |
# Add comfort zone if specified | |
if comfort_zone: | |
temp_min = comfort_zone.get("temp_min", 20) | |
temp_max = comfort_zone.get("temp_max", 26) | |
rh_min = comfort_zone.get("rh_min", 30) | |
rh_max = comfort_zone.get("rh_max", 60) | |
# Calculate humidity ratios at corners | |
w_bottom_left = self.psychrometrics.humidity_ratio(temp_min, rh_min, self.pressure) | |
w_bottom_right = self.psychrometrics.humidity_ratio(temp_max, rh_min, self.pressure) | |
w_top_right = self.psychrometrics.humidity_ratio(temp_max, rh_max, self.pressure) | |
w_top_left = self.psychrometrics.humidity_ratio(temp_min, rh_max, self.pressure) | |
# Add comfort zone as a filled polygon | |
fig.add_trace(go.Scatter( | |
x=[temp_min, temp_max, temp_max, temp_min, temp_min], | |
y=[w_bottom_left, w_bottom_right, w_top_right, w_top_left, w_bottom_left], | |
fill="toself", | |
fillcolor="rgba(0, 255, 0, 0.2)", | |
line=dict(color="green", width=2), | |
name="Comfort Zone" | |
)) | |
# Add points if specified | |
if points: | |
for i, point in enumerate(points): | |
temp = point.get("temp", 0) | |
rh = point.get("rh", 0) | |
w = point.get("w", self.psychrometrics.humidity_ratio(temp, rh, self.pressure)) | |
name = point.get("name", f"Point {i+1}") | |
color = point.get("color", "blue") | |
fig.add_trace(go.Scatter( | |
x=[temp], | |
y=[w], | |
mode="markers+text", | |
marker=dict(size=10, color=color), | |
text=[name], | |
textposition="top center", | |
name=name, | |
hovertemplate=( | |
f"<b>{name}</b><br>" + | |
"Temperature: %{x:.1f}°C<br>" + | |
"Humidity Ratio: %{y:.5f} kg/kg<br>" + | |
f"Relative Humidity: {rh:.1f}%<br>" | |
) | |
)) | |
# Add processes if specified | |
if processes: | |
for i, process in enumerate(processes): | |
start_point = process.get("start", {}) | |
end_point = process.get("end", {}) | |
start_temp = start_point.get("temp", 0) | |
start_rh = start_point.get("rh", 0) | |
start_w = start_point.get("w", self.psychrometrics.humidity_ratio(start_temp, start_rh, self.pressure)) | |
end_temp = end_point.get("temp", 0) | |
end_rh = end_point.get("rh", 0) | |
end_w = end_point.get("w", self.psychrometrics.humidity_ratio(end_temp, end_rh, self.pressure)) | |
name = process.get("name", f"Process {i+1}") | |
color = process.get("color", "red") | |
fig.add_trace(go.Scatter( | |
x=[start_temp, end_temp], | |
y=[start_w, end_w], | |
mode="lines+markers", | |
line=dict(color=color, width=2, dash="solid"), | |
marker=dict(size=8, color=color), | |
name=name | |
)) | |
# Add arrow to indicate direction | |
fig.add_annotation( | |
x=end_temp, | |
y=end_w, | |
ax=start_temp, | |
ay=start_w, | |
xref="x", | |
yref="y", | |
axref="x", | |
ayref="y", | |
showarrow=True, | |
arrowhead=2, | |
arrowsize=1, | |
arrowwidth=2, | |
arrowcolor=color | |
) | |
# Update layout | |
fig.update_layout( | |
title="Psychrometric Chart", | |
xaxis_title="Dry-Bulb Temperature (°C)", | |
yaxis_title="Humidity Ratio (kg/kg)", | |
xaxis=dict( | |
range=[self.temp_min, self.temp_max], | |
gridcolor="rgba(0, 0, 0, 0.1)", | |
showgrid=True | |
), | |
yaxis=dict( | |
range=[self.w_min, self.w_max], | |
gridcolor="rgba(0, 0, 0, 0.1)", | |
showgrid=True | |
), | |
height=700, | |
margin=dict(l=50, r=50, b=50, t=50), | |
legend=dict( | |
orientation="h", | |
yanchor="bottom", | |
y=1.02, | |
xanchor="right", | |
x=1 | |
), | |
hovermode="closest" | |
) | |
return fig | |
def create_process_visualization(self, process: Dict[str, Any]) -> go.Figure: | |
""" | |
Create a visualization of a psychrometric process. | |
Args: | |
process: Dictionary with process parameters | |
Returns: | |
Plotly figure with process visualization | |
""" | |
# Extract process parameters | |
start_point = process.get("start", {}) | |
end_point = process.get("end", {}) | |
start_temp = start_point.get("temp", 0) | |
start_rh = start_point.get("rh", 0) | |
end_temp = end_point.get("temp", 0) | |
end_rh = end_point.get("rh", 0) | |
# Calculate psychrometric properties | |
start_props = self.psychrometrics.moist_air_properties(start_temp, start_rh, self.pressure) | |
end_props = self.psychrometrics.moist_air_properties(end_temp, end_rh, self.pressure) | |
# Calculate process changes | |
delta_t = end_temp - start_temp | |
delta_w = end_props["humidity_ratio"] - start_props["humidity_ratio"] | |
delta_h = end_props["enthalpy"] - start_props["enthalpy"] | |
# Determine process type | |
process_type = "Unknown" | |
if abs(delta_w) < 0.0001: # Sensible heating/cooling | |
if delta_t > 0: | |
process_type = "Sensible Heating" | |
else: | |
process_type = "Sensible Cooling" | |
elif abs(delta_t) < 0.1: # Humidification/Dehumidification | |
if delta_w > 0: | |
process_type = "Humidification" | |
else: | |
process_type = "Dehumidification" | |
elif delta_t > 0 and delta_w > 0: | |
process_type = "Heating and Humidification" | |
elif delta_t < 0 and delta_w < 0: | |
process_type = "Cooling and Dehumidification" | |
elif delta_t > 0 and delta_w < 0: | |
process_type = "Heating and Dehumidification" | |
elif delta_t < 0 and delta_w > 0: | |
process_type = "Cooling and Humidification" | |
# Create figure | |
fig = go.Figure() | |
# Add process to psychrometric chart | |
chart_fig = self.create_psychrometric_chart( | |
points=[ | |
{"temp": start_temp, "rh": start_rh, "name": "Start", "color": "blue"}, | |
{"temp": end_temp, "rh": end_rh, "name": "End", "color": "red"} | |
], | |
processes=[ | |
{"start": {"temp": start_temp, "rh": start_rh}, | |
"end": {"temp": end_temp, "rh": end_rh}, | |
"name": process_type, | |
"color": "green"} | |
] | |
) | |
# Create process diagram | |
# Create data for process parameters | |
params = [ | |
"Dry-Bulb Temperature (°C)", | |
"Relative Humidity (%)", | |
"Humidity Ratio (g/kg)", | |
"Enthalpy (kJ/kg)", | |
"Wet-Bulb Temperature (°C)", | |
"Dew Point Temperature (°C)", | |
"Specific Volume (m³/kg)" | |
] | |
start_values = [ | |
start_props["dry_bulb_temperature"], | |
start_props["relative_humidity"], | |
start_props["humidity_ratio"] * 1000, # Convert to g/kg | |
start_props["enthalpy"] / 1000, # Convert to kJ/kg | |
start_props["wet_bulb_temperature"], | |
start_props["dew_point_temperature"], | |
start_props["specific_volume"] | |
] | |
end_values = [ | |
end_props["dry_bulb_temperature"], | |
end_props["relative_humidity"], | |
end_props["humidity_ratio"] * 1000, # Convert to g/kg | |
end_props["enthalpy"] / 1000, # Convert to kJ/kg | |
end_props["wet_bulb_temperature"], | |
end_props["dew_point_temperature"], | |
end_props["specific_volume"] | |
] | |
delta_values = [end - start for start, end in zip(start_values, end_values)] | |
# Create table | |
table_fig = go.Figure(data=[go.Table( | |
header=dict( | |
values=["Parameter", "Start", "End", "Change"], | |
fill_color="paleturquoise", | |
align="left", | |
font=dict(size=12) | |
), | |
cells=dict( | |
values=[ | |
params, | |
[f"{val:.2f}" for val in start_values], | |
[f"{val:.2f}" for val in end_values], | |
[f"{val:.2f}" for val in delta_values] | |
], | |
fill_color="lavender", | |
align="left", | |
font=dict(size=11) | |
) | |
)]) | |
table_fig.update_layout( | |
title=f"Process Parameters: {process_type}", | |
height=300, | |
margin=dict(l=0, r=0, b=0, t=30) | |
) | |
return chart_fig, table_fig | |
def display_psychrometric_visualization(self) -> None: | |
""" | |
Display psychrometric visualization in Streamlit. | |
""" | |
st.header("Psychrometric Visualization") | |
# Create tabs for different visualizations | |
tab1, tab2, tab3 = st.tabs([ | |
"Interactive Psychrometric Chart", | |
"Process Visualization", | |
"Comfort Zone Analysis" | |
]) | |
with tab1: | |
st.subheader("Interactive Psychrometric Chart") | |
# Add controls for points | |
st.write("Add points to the chart:") | |
col1, col2, col3 = st.columns(3) | |
with col1: | |
point1_temp = st.number_input("Point 1 Temperature (°C)", -10.0, 50.0, 20.0, key="point1_temp") | |
point1_rh = st.number_input("Point 1 RH (%)", 0.0, 100.0, 50.0, key="point1_rh") | |
with col2: | |
point2_temp = st.number_input("Point 2 Temperature (°C)", -10.0, 50.0, 30.0, key="point2_temp") | |
point2_rh = st.number_input("Point 2 RH (%)", 0.0, 100.0, 40.0, key="point2_rh") | |
with col3: | |
show_process = st.checkbox("Show Process Line", True, key="show_process") | |
process_name = st.text_input("Process Name", "Cooling Process", key="process_name") | |
# Create points | |
points = [ | |
{"temp": point1_temp, "rh": point1_rh, "name": "Point 1", "color": "blue"}, | |
{"temp": point2_temp, "rh": point2_rh, "name": "Point 2", "color": "red"} | |
] | |
# Create process if enabled | |
processes = [] | |
if show_process: | |
processes.append({ | |
"start": {"temp": point1_temp, "rh": point1_rh}, | |
"end": {"temp": point2_temp, "rh": point2_rh}, | |
"name": process_name, | |
"color": "green" | |
}) | |
# Create and display chart | |
fig = self.create_psychrometric_chart(points=points, processes=processes) | |
st.plotly_chart(fig, use_container_width=True) | |
# Display point properties | |
col1, col2 = st.columns(2) | |
with col1: | |
st.subheader("Point 1 Properties") | |
props1 = self.psychrometrics.moist_air_properties(point1_temp, point1_rh, self.pressure) | |
st.write(f"Dry-Bulb Temperature: {props1['dry_bulb_temperature']:.2f} °C") | |
st.write(f"Relative Humidity: {props1['relative_humidity']:.2f} %") | |
st.write(f"Humidity Ratio: {props1['humidity_ratio']*1000:.2f} g/kg") | |
st.write(f"Enthalpy: {props1['enthalpy']/1000:.2f} kJ/kg") | |
st.write(f"Wet-Bulb Temperature: {props1['wet_bulb_temperature']:.2f} °C") | |
st.write(f"Dew Point Temperature: {props1['dew_point_temperature']:.2f} °C") | |
with col2: | |
st.subheader("Point 2 Properties") | |
props2 = self.psychrometrics.moist_air_properties(point2_temp, point2_rh, self.pressure) | |
st.write(f"Dry-Bulb Temperature: {props2['dry_bulb_temperature']:.2f} °C") | |
st.write(f"Relative Humidity: {props2['relative_humidity']:.2f} %") | |
st.write(f"Humidity Ratio: {props2['humidity_ratio']*1000:.2f} g/kg") | |
st.write(f"Enthalpy: {props2['enthalpy']/1000:.2f} kJ/kg") | |
st.write(f"Wet-Bulb Temperature: {props2['wet_bulb_temperature']:.2f} °C") | |
st.write(f"Dew Point Temperature: {props2['dew_point_temperature']:.2f} °C") | |
with tab2: | |
st.subheader("Process Visualization") | |
# Add controls for process | |
st.write("Define a psychrometric process:") | |
col1, col2 = st.columns(2) | |
with col1: | |
st.write("Starting Point") | |
start_temp = st.number_input("Temperature (°C)", -10.0, 50.0, 24.0, key="start_temp") | |
start_rh = st.number_input("RH (%)", 0.0, 100.0, 50.0, key="start_rh") | |
with col2: | |
st.write("Ending Point") | |
end_temp = st.number_input("Temperature (°C)", -10.0, 50.0, 14.0, key="end_temp") | |
end_rh = st.number_input("RH (%)", 0.0, 100.0, 90.0, key="end_rh") | |
# Create process | |
process = { | |
"start": {"temp": start_temp, "rh": start_rh}, | |
"end": {"temp": end_temp, "rh": end_rh} | |
} | |
# Create and display process visualization | |
chart_fig, table_fig = self.create_process_visualization(process) | |
st.plotly_chart(chart_fig, use_container_width=True) | |
st.plotly_chart(table_fig, use_container_width=True) | |
# Calculate process energy requirements | |
start_props = self.psychrometrics.moist_air_properties(start_temp, start_rh, self.pressure) | |
end_props = self.psychrometrics.moist_air_properties(end_temp, end_rh, self.pressure) | |
delta_h = end_props["enthalpy"] - start_props["enthalpy"] # J/kg | |
st.subheader("Energy Calculations") | |
air_flow = st.number_input("Air Flow Rate (m³/s)", 0.1, 100.0, 1.0, key="air_flow") | |
# Calculate mass flow rate | |
density = start_props["density"] # kg/m³ | |
mass_flow = air_flow * density # kg/s | |
# Calculate energy rate | |
energy_rate = mass_flow * delta_h # W | |
st.write(f"Air Density: {density:.2f} kg/m³") | |
st.write(f"Mass Flow Rate: {mass_flow:.2f} kg/s") | |
st.write(f"Enthalpy Change: {delta_h/1000:.2f} kJ/kg") | |
st.write(f"Energy Rate: {energy_rate/1000:.2f} kW") | |
with tab3: | |
st.subheader("Comfort Zone Analysis") | |
# Add controls for comfort zone | |
st.write("Define comfort zone parameters:") | |
col1, col2 = st.columns(2) | |
with col1: | |
temp_min = st.number_input("Minimum Temperature (°C)", 10.0, 30.0, 20.0, key="temp_min") | |
temp_max = st.number_input("Maximum Temperature (°C)", 10.0, 30.0, 26.0, key="temp_max") | |
with col2: | |
rh_min = st.number_input("Minimum RH (%)", 0.0, 100.0, 30.0, key="rh_min") | |
rh_max = st.number_input("Maximum RH (%)", 0.0, 100.0, 60.0, key="rh_max") | |
# Create comfort zone | |
comfort_zone = { | |
"temp_min": temp_min, | |
"temp_max": temp_max, | |
"rh_min": rh_min, | |
"rh_max": rh_max | |
} | |
# Add point to check if it's in comfort zone | |
st.write("Check if a point is within the comfort zone:") | |
col1, col2 = st.columns(2) | |
with col1: | |
check_temp = st.number_input("Temperature (°C)", -10.0, 50.0, 22.0, key="check_temp") | |
check_rh = st.number_input("RH (%)", 0.0, 100.0, 45.0, key="check_rh") | |
# Check if point is in comfort zone | |
in_comfort_zone = ( | |
temp_min <= check_temp <= temp_max and | |
rh_min <= check_rh <= rh_max | |
) | |
with col2: | |
if in_comfort_zone: | |
st.success("✅ Point is within the comfort zone") | |
else: | |
st.error("❌ Point is outside the comfort zone") | |
# Calculate properties | |
check_props = self.psychrometrics.moist_air_properties(check_temp, check_rh, self.pressure) | |
st.write(f"Humidity Ratio: {check_props['humidity_ratio']*1000:.2f} g/kg") | |
st.write(f"Enthalpy: {check_props['enthalpy']/1000:.2f} kJ/kg") | |
st.write(f"Wet-Bulb Temperature: {check_props['wet_bulb_temperature']:.2f} °C") | |
# Create and display chart with comfort zone | |
fig = self.create_psychrometric_chart( | |
points=[{"temp": check_temp, "rh": check_rh, "name": "Test Point", "color": "purple"}], | |
comfort_zone=comfort_zone | |
) | |
st.plotly_chart(fig, use_container_width=True) | |
# Create a singleton instance | |
psychrometric_visualization = PsychrometricVisualization() | |
# Example usage | |
if __name__ == "__main__": | |
import streamlit as st | |
# Display psychrometric visualization | |
psychrometric_visualization.display_psychrometric_visualization() | |