|
|
""" |
|
|
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 |
|
|
|
|
|
|
|
|
from utils.psychrometrics import Psychrometrics |
|
|
|
|
|
|
|
|
class PsychrometricVisualization: |
|
|
"""Class for psychrometric visualization.""" |
|
|
|
|
|
def __init__(self): |
|
|
"""Initialize psychrometric visualization.""" |
|
|
self.psychrometrics = Psychrometrics() |
|
|
|
|
|
|
|
|
self.temp_min = -10 |
|
|
self.temp_max = 50 |
|
|
self.w_min = 0 |
|
|
self.w_max = 0.030 |
|
|
|
|
|
|
|
|
self.pressure = 101325 |
|
|
|
|
|
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 |
|
|
""" |
|
|
|
|
|
fig = go.Figure() |
|
|
|
|
|
|
|
|
temp_range = np.linspace(self.temp_min, self.temp_max, 100) |
|
|
w_range = np.linspace(self.w_min, self.w_max, 100) |
|
|
|
|
|
|
|
|
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] |
|
|
|
|
|
|
|
|
fig.add_trace(go.Scatter( |
|
|
x=sat_temps, |
|
|
y=sat_w, |
|
|
mode="lines", |
|
|
line=dict(color="blue", width=2), |
|
|
name="Saturation Curve" |
|
|
)) |
|
|
|
|
|
|
|
|
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] |
|
|
|
|
|
|
|
|
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" |
|
|
)) |
|
|
|
|
|
|
|
|
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: |
|
|
|
|
|
w_low = 0 |
|
|
w_high = self.psychrometrics.humidity_ratio(t, 100, self.pressure) |
|
|
|
|
|
for _ in range(10): |
|
|
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" |
|
|
)) |
|
|
|
|
|
|
|
|
h_values = np.arange(0, 100, 10) * 1000 |
|
|
|
|
|
for h in h_values: |
|
|
h_temps = np.linspace(self.temp_min, self.temp_max, 50) |
|
|
h_points = [] |
|
|
|
|
|
for t in h_temps: |
|
|
|
|
|
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" |
|
|
)) |
|
|
|
|
|
|
|
|
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: |
|
|
|
|
|
w_low = 0 |
|
|
w_high = self.psychrometrics.humidity_ratio(t, 100, self.pressure) |
|
|
|
|
|
for _ in range(10): |
|
|
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" |
|
|
)) |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
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" |
|
|
)) |
|
|
|
|
|
|
|
|
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 |
|
|
)) |
|
|
|
|
|
|
|
|
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), |
|
|
name=name |
|
|
)) |
|
|
|
|
|
|
|
|
fig.update_layout( |
|
|
title="Psychrometric Chart", |
|
|
xaxis_title="Dry-Bulb Temperature (°C)", |
|
|
yaxis_title="Humidity Ratio (kg/kg)", |
|
|
legend_title="Legend", |
|
|
height=700, |
|
|
margin=dict(l=50, r=50, t=50, b=50), |
|
|
plot_bgcolor="white", |
|
|
paper_bgcolor="white", |
|
|
font=dict(size=12) |
|
|
) |
|
|
|
|
|
|
|
|
fig.update_xaxes(range=[self.temp_min, self.temp_max], gridcolor="lightgray") |
|
|
fig.update_yaxes(range=[self.w_min, self.w_max], gridcolor="lightgray") |
|
|
|
|
|
return fig |
|
|
|
|
|
def display_psychrometric_chart(self, calculation_results: Dict[str, Any], design_conditions: Dict[str, Any]) -> None: |
|
|
""" |
|
|
Display psychrometric chart with calculation results. |
|
|
|
|
|
Args: |
|
|
calculation_results: Dictionary containing calculation results |
|
|
design_conditions: Dictionary containing design conditions |
|
|
""" |
|
|
|
|
|
summer_outdoor_db = design_conditions.get("summer_outdoor_db", 35) |
|
|
summer_outdoor_wb = design_conditions.get("summer_outdoor_wb", 25) |
|
|
summer_indoor_db = design_conditions.get("summer_indoor_db", 24) |
|
|
summer_indoor_rh = design_conditions.get("summer_indoor_rh", 50) |
|
|
|
|
|
winter_outdoor_db = design_conditions.get("winter_outdoor_db", 0) |
|
|
winter_outdoor_rh = design_conditions.get("winter_outdoor_rh", 80) |
|
|
winter_indoor_db = design_conditions.get("winter_indoor_db", 22) |
|
|
winter_indoor_rh = design_conditions.get("winter_indoor_rh", 40) |
|
|
|
|
|
|
|
|
summer_outdoor_w = self.psychrometrics.humidity_ratio_from_wb(summer_outdoor_db, summer_outdoor_wb, self.pressure) |
|
|
summer_indoor_w = self.psychrometrics.humidity_ratio(summer_indoor_db, summer_indoor_rh, self.pressure) |
|
|
winter_outdoor_w = self.psychrometrics.humidity_ratio(winter_outdoor_db, winter_outdoor_rh, self.pressure) |
|
|
winter_indoor_w = self.psychrometrics.humidity_ratio(winter_indoor_db, winter_indoor_rh, self.pressure) |
|
|
|
|
|
|
|
|
points = [ |
|
|
{ |
|
|
"temp": summer_outdoor_db, |
|
|
"w": summer_outdoor_w, |
|
|
"name": "Summer Outdoor", |
|
|
"color": "red" |
|
|
}, |
|
|
{ |
|
|
"temp": summer_indoor_db, |
|
|
"w": summer_indoor_w, |
|
|
"name": "Summer Indoor", |
|
|
"color": "blue" |
|
|
}, |
|
|
{ |
|
|
"temp": winter_outdoor_db, |
|
|
"w": winter_outdoor_w, |
|
|
"name": "Winter Outdoor", |
|
|
"color": "purple" |
|
|
}, |
|
|
{ |
|
|
"temp": winter_indoor_db, |
|
|
"w": winter_indoor_w, |
|
|
"name": "Winter Indoor", |
|
|
"color": "green" |
|
|
} |
|
|
] |
|
|
|
|
|
|
|
|
processes = [ |
|
|
{ |
|
|
"start": {"temp": summer_outdoor_db, "w": summer_outdoor_w}, |
|
|
"end": {"temp": summer_indoor_db, "w": summer_indoor_w}, |
|
|
"name": "Cooling Process", |
|
|
"color": "blue" |
|
|
}, |
|
|
{ |
|
|
"start": {"temp": winter_outdoor_db, "w": winter_outdoor_w}, |
|
|
"end": {"temp": winter_indoor_db, "w": winter_indoor_w}, |
|
|
"name": "Heating Process", |
|
|
"color": "red" |
|
|
} |
|
|
] |
|
|
|
|
|
|
|
|
comfort_zone = { |
|
|
"temp_min": 20, |
|
|
"temp_max": 26, |
|
|
"rh_min": 30, |
|
|
"rh_max": 60 |
|
|
} |
|
|
|
|
|
|
|
|
fig = self.create_psychrometric_chart(points, processes, comfort_zone) |
|
|
|
|
|
|
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
|
|
|
|
|
st.subheader("Psychrometric Properties") |
|
|
|
|
|
|
|
|
properties = [] |
|
|
|
|
|
|
|
|
summer_outdoor_rh = self.psychrometrics.relative_humidity_from_wb(summer_outdoor_db, summer_outdoor_wb, self.pressure) |
|
|
summer_outdoor_dp = self.psychrometrics.dew_point(summer_outdoor_db, summer_outdoor_rh, self.pressure) |
|
|
summer_outdoor_h = self.psychrometrics.enthalpy(summer_outdoor_db, summer_outdoor_w) |
|
|
summer_outdoor_v = self.psychrometrics.specific_volume(summer_outdoor_db, summer_outdoor_w, self.pressure) |
|
|
|
|
|
properties.append({ |
|
|
"Point": "Summer Outdoor", |
|
|
"Dry-Bulb (°C)": f"{summer_outdoor_db:.1f}", |
|
|
"Wet-Bulb (°C)": f"{summer_outdoor_wb:.1f}", |
|
|
"Relative Humidity (%)": f"{summer_outdoor_rh:.1f}", |
|
|
"Humidity Ratio (g/kg)": f"{summer_outdoor_w*1000:.1f}", |
|
|
"Dew Point (°C)": f"{summer_outdoor_dp:.1f}", |
|
|
"Enthalpy (kJ/kg)": f"{summer_outdoor_h/1000:.1f}", |
|
|
"Specific Volume (m³/kg)": f"{summer_outdoor_v:.3f}" |
|
|
}) |
|
|
|
|
|
|
|
|
summer_indoor_wb = self.psychrometrics.wet_bulb_temperature(summer_indoor_db, summer_indoor_rh, self.pressure) |
|
|
summer_indoor_dp = self.psychrometrics.dew_point(summer_indoor_db, summer_indoor_rh, self.pressure) |
|
|
summer_indoor_h = self.psychrometrics.enthalpy(summer_indoor_db, summer_indoor_w) |
|
|
summer_indoor_v = self.psychrometrics.specific_volume(summer_indoor_db, summer_indoor_w, self.pressure) |
|
|
|
|
|
properties.append({ |
|
|
"Point": "Summer Indoor", |
|
|
"Dry-Bulb (°C)": f"{summer_indoor_db:.1f}", |
|
|
"Wet-Bulb (°C)": f"{summer_indoor_wb:.1f}", |
|
|
"Relative Humidity (%)": f"{summer_indoor_rh:.1f}", |
|
|
"Humidity Ratio (g/kg)": f"{summer_indoor_w*1000:.1f}", |
|
|
"Dew Point (°C)": f"{summer_indoor_dp:.1f}", |
|
|
"Enthalpy (kJ/kg)": f"{summer_indoor_h/1000:.1f}", |
|
|
"Specific Volume (m³/kg)": f"{summer_indoor_v:.3f}" |
|
|
}) |
|
|
|
|
|
|
|
|
winter_outdoor_wb = self.psychrometrics.wet_bulb_temperature(winter_outdoor_db, winter_outdoor_rh, self.pressure) |
|
|
winter_outdoor_dp = self.psychrometrics.dew_point(winter_outdoor_db, winter_outdoor_rh, self.pressure) |
|
|
winter_outdoor_h = self.psychrometrics.enthalpy(winter_outdoor_db, winter_outdoor_w) |
|
|
winter_outdoor_v = self.psychrometrics.specific_volume(winter_outdoor_db, winter_outdoor_w, self.pressure) |
|
|
|
|
|
properties.append({ |
|
|
"Point": "Winter Outdoor", |
|
|
"Dry-Bulb (°C)": f"{winter_outdoor_db:.1f}", |
|
|
"Wet-Bulb (°C)": f"{winter_outdoor_wb:.1f}", |
|
|
"Relative Humidity (%)": f"{winter_outdoor_rh:.1f}", |
|
|
"Humidity Ratio (g/kg)": f"{winter_outdoor_w*1000:.1f}", |
|
|
"Dew Point (°C)": f"{winter_outdoor_dp:.1f}", |
|
|
"Enthalpy (kJ/kg)": f"{winter_outdoor_h/1000:.1f}", |
|
|
"Specific Volume (m³/kg)": f"{winter_outdoor_v:.3f}" |
|
|
}) |
|
|
|
|
|
|
|
|
winter_indoor_wb = self.psychrometrics.wet_bulb_temperature(winter_indoor_db, winter_indoor_rh, self.pressure) |
|
|
winter_indoor_dp = self.psychrometrics.dew_point(winter_indoor_db, winter_indoor_rh, self.pressure) |
|
|
winter_indoor_h = self.psychrometrics.enthalpy(winter_indoor_db, winter_indoor_w) |
|
|
winter_indoor_v = self.psychrometrics.specific_volume(winter_indoor_db, winter_indoor_w, self.pressure) |
|
|
|
|
|
properties.append({ |
|
|
"Point": "Winter Indoor", |
|
|
"Dry-Bulb (°C)": f"{winter_indoor_db:.1f}", |
|
|
"Wet-Bulb (°C)": f"{winter_indoor_wb:.1f}", |
|
|
"Relative Humidity (%)": f"{winter_indoor_rh:.1f}", |
|
|
"Humidity Ratio (g/kg)": f"{winter_indoor_w*1000:.1f}", |
|
|
"Dew Point (°C)": f"{winter_indoor_dp:.1f}", |
|
|
"Enthalpy (kJ/kg)": f"{winter_indoor_h/1000:.1f}", |
|
|
"Specific Volume (m³/kg)": f"{winter_indoor_v:.3f}" |
|
|
}) |
|
|
|
|
|
|
|
|
st.dataframe(pd.DataFrame(properties), use_container_width=True) |
|
|
|