""" Main HVAC Calculator Script with Streamlit Interface Developed by: Dr Majed Abuseif, Deakin University © 2025 """ import streamlit as st import pandas as pd import plotly.graph_objects as go import logging from enum import Enum from typing import Dict, List from data.material_library import GlazingMaterial from data.calculation import TFMCalculations from utils.solar import SolarCalculations # Configure logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) class ComponentType(Enum): WALL = "Wall" ROOF = "Roof" FLOOR = "Floor" WINDOW = "Window" DOOR = "Door" SKYLIGHT = "Skylight" class HVACCalculatorApp: def __init__(self): self.tfm = TFMCalculations() # Initialize session state if 'window_action' not in st.session_state: st.session_state.glazing_action = {"action": None, "id": None} if 'components' not in st.session_state: st.session_state.components = { "windows": [ GlazingMaterial( id="window_1", name="Single Clear 6mm", component_type=ComponentType.WINDOW, area=10.0, # m² u_value=5.8, # W/m²·K shgc=0.7, facade="A" ) ], "skylights": [], "walls": [], "roofs": [], "floors": [], "doors": [], "_building_info": { "A": {"azimuth": 0.0, "tilt_angle": 0.0} } } if 'climate_data' not in st.session_state: st.session_state.climate_data = { "hourly_data": [ { "month": 7, "day": 1, "hour": h, "dry_bulb": 30.0, "global_horizontal_radiation": 800.0 if 8 <= h <= 16 else 0.0, # W/m² "direct_normal_radiation": 560.0 if 8 <= h <= 16 else 0.0, "diffuse_horizontal_radiation": 240.0 if 8 <= h <= 16 else 0.0 } for h in range(24) ], "typical_extreme_periods": { "summer_extreme": {"start": {"month": 7, "day": 1}, "end": {"month": 7, "day": 7}}, "summer_typical": {"start": {"month": 7, "day": 1}, "end": {"month": 7, "day": 7}}, "winter_extreme": {"start": {"month": 1, "day": 1}, "end": {"month": 1, "day": 7}}, "winter_typical": {"start": {"month": 1, "day": 1}, "end": {"month": 1, "day": 7}} } } if 'building_info' not in st.session_state: st.session_state.building_info = { "latitude": 37.8, "longitude": -122.4, "timezone": -8.0, "ground_reflectivity": 0.2, "floor_area": 100.0, "indoor_design_temp": 24.0, "indoor_design_rh": 50.0 } if 'internal_loads' not in st.session_state: st.session_state.internal_loads = { "people": [{"num_people": 10, "activity_data": {"sensible_min_w": 70, "sensible_max_w": 100, "latent_min_w": 40, "latent_max_w": 60}, "diversity_factor": 1.0}], "lighting": [{"lpd": 10.0, "operating_hours": 10}], "equipment": {"total_power_density": 5.0}, "ventilation": {"space_rate": 0.3, "people_rate": 2.5}, "infiltration": {"method": "ACH", "settings": {"rate": 0.5}} } if 'calculation_results' not in st.session_state: st.session_state.calculation_results = {"cooling": [], "heating": []} def display_calculation_results(self): st.title("Calculation Results") st.write("Configure simulation settings and view cooling and heating load calculations based on ASHRAE CTF/TFM methods.") if not st.session_state.get("climate_data"): st.error("Please upload climate data in the Climate Data section.") st.button("Go to Climate Data", key="results_to_climate", on_click=lambda: setattr(st.session_state, "page", "Climate Data and Design Requirements")) return if not any(st.session_state.components.values()): st.error("Please define building components in the Building Components section.") st.button("Go to Building Components", key="results_to_components", on_click=lambda: setattr(st.session_state, "page", "Building Components")) return # Debug SolarCalculations for 4 hours st.subheader("Debugging Solar Calculations") debug_hours = [8, 9, 10, 11] # July 1, 8 AM–12 PM debug_data = [d for d in st.session_state.climate_data["hourly_data"] if d["month"] == 7 and d["day"] == 1 and d["hour"] in debug_hours] if not debug_data: st.warning("No climate data for debug hours (July 1, 8 AM–12 PM).") else: logger.info("Debugging SolarCalculations Inputs:") logger.info(f"Components (windows): {[{'id': c.id, 'area': c.area, 'shgc': c.shgc, 'facade': c.facade} for c in st.session_state.components.get('windows', [])]}") logger.info(f"Components (skylights): {[{'id': c.id, 'area': c.area, 'shgc': c.shgc, 'facade': c.facade} for c in st.session_state.components.get('skylights', [])]}") logger.info(f"Hourly Data: {[{'month': d['month'], 'day': d['day'], 'hour': d['hour'], 'GHI': d['global_horizontal_radiation'], 'DNI': d.get('direct_normal_radiation', 0), 'DHI': d.get('diffuse_horizontal_radiation', 0), 'dry_bulb': d['dry_bulb']} for d in debug_data]}") logger.info(f"Building Info: {{'latitude': {st.session_state.building_info.get('latitude', 0.0)}, 'longitude': {st.session_state.building_info.get('longitude', 0.0)}, 'timezone': {st.session_state.building_info.get('timezone', 0.0)}, 'ground_reflectivity': {st.session_state.building_info.get('ground_reflectivity', 0.2)}}}") try: solar_results = SolarCalculations.calculate_solar_parameters( hourly_data=debug_data, latitude=st.session_state.building_info.get("latitude", 0.0), longitude=st.session_state.building_info.get("longitude", 0.0), timezone=st.session_state.building_info.get("timezone", 0.0), ground_reflectivity=st.session_state.building_info.get("ground_reflectivity", 0.2), components=st.session_state.components ) st.write("Solar Calculations Debug Output:") for result in solar_results: month, day, hour = result["month"], result["day"], result["hour"] logger.info(f"Solar Results for {month}/{day}/{hour}:") logger.info(f" Solar Altitude: {result['altitude']:.2f}°, Azimuth: {result['azimuth']:.2f}°") for comp_result in result["component_results"]: if "solar_heat_gain" in comp_result: logger.info(f" Component {comp_result['component_id']}: Solar Heat Gain = {comp_result['solar_heat_gain']:.2f} kW") st.write(f"Month {month}, Day {day}, Hour {hour}, Component {comp_result['component_id']}: Solar Heat Gain = {comp_result['solar_heat_gain']:.2f} kW") else: logger.info(f" Component {comp_result['component_id']}: No solar heat gain (not a window/skylight)") if not any("solar_heat_gain" in cr for cr in result["component_results"]): st.write(f"Month {month}, Day {day}, Hour {hour}: No solar heat gains calculated.") except Exception as e: logger.error(f"Error in SolarCalculations: {str(e)}") st.error(f"Error in Solar Calculations: {str(e)}") # Initialize session state for simulation settings if 'sim_period' not in st.session_state: st.session_state.sim_period = {"type": "Full Year"} if 'indoor_conditions' not in st.session_state: st.session_state.indoor_conditions = { "type": "Fixed", "cooling_setpoint": { "temperature": st.session_state.building_info["indoor_design_temp"], "rh": st.session_state.building_info["indoor_design_rh"] }, "heating_setpoint": { "temperature": st.session_state.building_info["indoor_design_temp"] - 2.0, "rh": st.session_state.building_info["indoor_design_rh"] } } if 'hvac_settings' not in st.session_state: st.session_state.hvac_settings = { "cop": 3.5, "operating_hours": [{"start": 8, "end": 18}] } # Simulation Period Controls st.subheader("Simulation Period") typical_extreme_periods = st.session_state.climate_data.get("typical_extreme_periods", {}) sim_type_options = [ "Full Year", "From Date to Date", "Heating Only", "Cooling Only", "Summer Extreme", "Summer Typical", "Winter Extreme", "Winter Typical" ] sim_type = st.selectbox( "Simulation Type", sim_type_options, index=sim_type_options.index(st.session_state.sim_period["type"]) if st.session_state.sim_period["type"] in sim_type_options else 0, key="sim_type" ) sim_period = {"type": sim_type} if sim_type == "From Date to Date": col1, col2 = st.columns(2) with col1: start_date = st.date_input("Start Date", value=pd.to_datetime("2025-01-01"), key="start_date") with col2: end_date = st.date_input("End Date", value=pd.to_datetime("2025-12-31"), key="end_date") sim_period["start_date"] = start_date sim_period["end_date"] = end_date elif sim_type in ["Summer Extreme", "Summer Typical", "Winter Extreme", "Winter Typical"]: period_key = sim_type.lower().replace(" ", "_") if period_key in typical_extreme_periods: period = typical_extreme_periods[period_key] start_date = pd.to_datetime(f"2025-{period['start']['month']}-{period['start']['day']}") end_date = pd.to_datetime(f"2025-{period['end']['month']}-{period['end']['day']}") st.write(f"Period: {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}") sim_period["start_date"] = start_date sim_period["end_date"] = end_date else: st.warning(f"No data available for {sim_type}. Please check climate data.") sim_period["start_date"] = pd.to_datetime("2025-01-01") sim_period["end_date"] = pd.to_datetime("2025-12-31") # Indoor Conditions Controls st.subheader("Indoor Conditions") indoor_type = st.selectbox( "Indoor Conditions Type", ["Fixed", "Time-varying", "Adaptive"], index=["Fixed", "Time-varying", "Adaptive"].index(st.session_state.indoor_conditions["type"]) if st.session_state.indoor_conditions["type"] in ["Fixed", "Time-varying", "Adaptive"] else 0, key="indoor_type" ) indoor_conditions = {"type": indoor_type} if indoor_type == "Fixed": st.write("Cooling Setpoint") col1, col2 = st.columns(2) with col1: cooling_temp = st.number_input( "Cooling Indoor Temperature (°C)", min_value=15.0, max_value=30.0, value=st.session_state.indoor_conditions.get("cooling_setpoint", {}).get("temperature", 24.0), key="cooling_fixed_temp" ) with col2: cooling_rh = st.number_input( "Cooling Indoor Relative Humidity (%)", min_value=0.0, max_value=100.0, value=st.session_state.indoor_conditions.get("cooling_setpoint", {}).get("rh", 50.0), key="cooling_fixed_rh" ) st.write("Heating Setpoint") col3, col4 = st.columns(2) with col3: heating_temp = st.number_input( "Heating Indoor Temperature (°C)", min_value=15.0, max_value=30.0, value=st.session_state.indoor_conditions.get("heating_setpoint", {}).get("temperature", 22.0), key="heating_fixed_temp" ) with col4: heating_rh = st.number_input( "Heating Indoor Relative Humidity (%)", min_value=0.0, max_value=100.0, value=st.session_state.indoor_conditions.get("heating_setpoint", {}).get("rh", 50.0), key="heating_fixed_rh" ) indoor_conditions["cooling_setpoint"] = {"temperature": cooling_temp, "rh": cooling_rh} indoor_conditions["heating_setpoint"] = {"temperature": heating_temp, "rh": heating_rh} elif indoor_type == "Time-varying": st.write("Define hourly schedule (0-23 hours) for Cooling and Heating Setpoints") cooling_schedule = [] heating_schedule = [] for hour in range(24): with st.expander(f"Hour {hour}", expanded=False): st.write("Cooling Setpoint") col1, col2 = st.columns(2) with col1: cooling_temp = st.number_input( f"Cooling Temperature (°C) at Hour {hour}", min_value=15.0, max_value=30.0, value=24.0, key=f"cooling_schedule_temp_{hour}" ) with col2: cooling_rh = st.number_input( f"Cooling RH (%) at Hour {hour}", min_value=0.0, max_value=100.0, value=50.0, key=f"cooling_schedule_rh_{hour}" ) st.write("Heating Setpoint") col3, col4 = st.columns(2) with col3: heating_temp = st.number_input( f"Heating Temperature (°C) at Hour {hour}", min_value=15.0, max_value=30.0, value=22.0, key=f"heating_schedule_temp_{hour}" ) with col4: heating_rh = st.number_input( f"Heating RH (%) at Hour {hour}", min_value=0.0, max_value=100.0, value=50.0, key=f"heating_schedule_rh_{hour}" ) cooling_schedule.append({"hour": hour, "temperature": cooling_temp, "rh": cooling_rh}) heating_schedule.append({"hour": hour, "temperature": heating_temp, "rh": heating_rh}) indoor_conditions["cooling_schedule"] = cooling_schedule indoor_conditions["heating_schedule"] = heating_schedule else: # Adaptive st.write("Adaptive comfort model (ASHRAE 55) will be used, adjusting temperature based on outdoor conditions.") indoor_conditions["rh"] = 50.0 # HVAC System Controls st.subheader("HVAC System Settings") col1, col2 = st.columns(2) with col1: cop = st.number_input( "Coefficient of Performance (COP)", min_value=1.0, max_value=6.0, value=st.session_state.hvac_settings.get("cop", 3.5), step=0.1, key="hvac_cop" ) with col2: num_periods = st.number_input( "Number of Operating Periods", min_value=1, max_value=5, value=len(st.session_state.hvac_settings.get("operating_hours", [{"start": 8, "end": 18}])), step=1, key="num_operating_periods" ) operating_hours = [] for i in range(int(num_periods)): st.write(f"Operating Period {i+1}") col1, col2 = st.columns(2) with col1: start_hour = st.slider( f"Start Hour (Period {i+1})", min_value=0, max_value=23, value=st.session_state.hvac_settings["operating_hours"][i]["start"] if i < len(st.session_state.hvac_settings["operating_hours"]) else 8, key=f"start_hour_{i}" ) with col2: end_hour = st.slider( f"End Hour (Period {i+1})", min_value=start_hour, max_value=23, value=st.session_state.hvac_settings["operating_hours"][i]["end"] if i < len(st.session_state.hvac_settings["operating_hours"]) else 18, key=f"end_hour_{i}" ) operating_hours.append({"start": start_hour, "end": end_hour}) # Save settings to session state st.session_state.sim_period = sim_period st.session_state.indoor_conditions = indoor_conditions st.session_state.hvac_settings = { "cop": cop, "operating_hours": operating_hours } # Run Simulation Button if st.button("Run Simulation", key="run_simulation"): climate_data = st.session_state.climate_data.get("hourly_data", []) if not climate_data: st.error("No valid climate data available.") return with st.spinner("Running simulation..."): loads = self.tfm.calculate_tfm_loads( st.session_state.components, climate_data, st.session_state.indoor_conditions, st.session_state.internal_loads, st.session_state.building_info, st.session_state.sim_period, st.session_state.hvac_settings ) st.session_state.calculation_results["cooling"] = loads st.session_state.calculation_results["heating"] = loads # Debug TFMCalculations solar loads for the same hours st.write("TFMCalculations Solar Load Debug Output:") for load in loads: if load["month"] == 7 and load["day"] == 1 and load["hour"] in debug_hours: logger.info(f"TFM Solar Load for {load['month']}/{load['day']}/{load['hour']}: {load['solar']:.2f} kW") st.write(f"Month {load['month']}, Day {load['day']}, Hour {load['hour']}: Solar Load = {load['solar']:.2f} kW") st.success("Simulation completed!") # Display Results if st.session_state.calculation_results.get("cooling") and st.session_state.calculation_results.get("heating"): df = pd.DataFrame(st.session_state.calculation_results["cooling"]) if df.empty: st.error("No load calculations available.") return # Equipment Sizing st.subheader("Equipment Sizing") peak_cooling_load = df["total_cooling"].max() if "total_cooling" in df else 0.0 peak_heating_load = df["total_heating"].max() if "total_heating" in df else 0.0 col1, col2 = st.columns(2) with col1: st.metric("Cooling Equipment Size", f"{peak_cooling_load:.2f} kW", help="Peak hourly cooling load") with col2: st.metric("Heating Equipment Size", f"{peak_heating_load:.2f} kW", help="Peak hourly heating load") # Pie Charts for Load Breakdown st.subheader("Load Breakdown") cooling_totals = { "Conduction": df["conduction_cooling"].sum(), "Solar": df["solar"].sum(), "Internal": df["internal"].sum(), "Ventilation": df["ventilation_cooling"].sum(), "Infiltration": df["infiltration_cooling"].sum() } heating_totals = { "Conduction": df["conduction_heating"].sum(), "Ventilation": df["ventilation_heating"].sum(), "Infiltration": df["infiltration_heating"].sum() } col1, col2 = st.columns(2) with col1: fig_cooling = go.Figure(data=[ go.Pie(labels=list(cooling_totals.keys()), values=list(cooling_totals.values())) ]) fig_cooling.update_layout(title="Cooling Load Breakdown (kWh)", width=400, height=400) st.plotly_chart(fig_cooling, use_container_width=True) with col2: fig_heating = go.Figure(data=[ go.Pie(labels=list(heating_totals.keys()), values=list(heating_totals.values())) ]) fig_heating.update_layout(title="Heating Load Breakdown (kWh)", width=400, height=400) st.plotly_chart(fig_heating, use_container_width=True) # Monthly Loads Bar Chart st.subheader("Monthly Heating and Cooling Loads") df["month_name"] = df["month"].map({ 1: "Jan", 2: "Feb", 3: "Mar", 4: "Apr", 5: "May", 6: "Jun", 7: "Jul", 8: "Aug", 9: "Sep", 10: "Oct", 11: "Nov", 12: "Dec" }) monthly_loads = df.groupby("month_name")[["total_cooling", "total_heating"]].sum().reindex( ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] ) fig_monthly = go.Figure(data=[ go.Bar(name="Cooling Load (kWh)", x=monthly_loads.index, y=monthly_loads["total_cooling"]), go.Bar(name="Heating Load (kWh)", x=monthly_loads.index, y=monthly_loads["total_heating"]) ]) fig_monthly.update_layout( title="Monthly Heating and Cooling Loads", xaxis_title="Month", yaxis_title="Load (kWh)", barmode="group", width=800, height=400 ) st.plotly_chart(fig_monthly, use_container_width=True) # Detailed Load Summary Table st.subheader("Load Summary") summary_row = { "hour": "Total", "month_name": "", "conduction_cooling": df["conduction_cooling"].sum(), "conduction_heating": df["conduction_heating"].sum(), "solar": df["solar"].sum(), "internal": df["internal"].sum(), "ventilation_cooling": df["ventilation_cooling"].sum(), "ventilation_heating": df["ventilation_heating"].sum(), "infiltration_cooling": df["infiltration_cooling"].sum(), "infiltration_heating": df["infiltration_heating"].sum(), "total_cooling": df["total_cooling"].sum(), "total_heating": df["total_heating"].sum() } display_df = df[["hour", "month_name", "conduction_cooling", "conduction_heating", "solar", "internal", "ventilation_cooling", "ventilation_heating", "infiltration_cooling", "infiltration_heating", "total_cooling", "total_heating"]] display_df = pd.concat([display_df, pd.DataFrame([summary_row])], ignore_index=True) st.dataframe(display_df.rename(columns={"month_name": "Month"}), use_container_width=True) col1, col2 = st.columns(2) with col1: st.button("Back to Internal Loads", key="results_back_to_internal", on_click=lambda: setattr(st.session_state, "page", "Internal Loads")) with col2: st.button("Continue to Export Data", key="results_to_export", on_click=lambda: setattr(st.session_state, "page", "Export Data")) def main(): app = HVACCalculatorApp() app.display_calculation_results() if __name__ == "__main__": main()