""" 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"{name}
" + "Temperature: %{x:.1f}°C
" + "Humidity Ratio: %{y:.5f} kg/kg
" + f"Relative Humidity: {rh:.1f}%
" ) )) # 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()