""" HVAC Calculator Code Documentation. Updated 2025-05-02: Integrated skylights, surface color, glazing type, frame type, and drapery adjustments from main_new.py. Updated 2025-05-02: Enhanced per Plan.txt to include winter design temperature, humidity, building height, ventilation rate, internal load enhancements, and calculation parameters. Updated 2025-05-09: Fixed latitude parsing to return string (e.g., "24N") to match ASHRAE table keys and added group validation. Updated 2025-05-09: Corrected group validation to use alphabetical groups (A-H for walls, A-G for roofs) and enhanced stale component handling. Updated 2025-05-10: Aligned latitude parsing with cooling_load.py's validate_latitude and updated wall groups to A-H to match cooling_load.py. """ import streamlit as st import pandas as pd import numpy as np import plotly.express as px import json import pycountry import os import sys from typing import Dict, List, Any, Optional, Tuple import uuid # Import application modules from app.building_info_form import BuildingInfoForm from app.component_selection import ComponentSelectionInterface, Orientation, ComponentType, Wall, Roof, Floor, Window, Door, Skylight, GlazingType, FrameType from app.results_display import ResultsDisplay from app.data_validation import DataValidation from app.data_persistence import DataPersistence from app.data_export import DataExport # Import data modules from data.reference_data import ReferenceData from data.climate_data import ClimateData, ClimateLocation from data.ashrae_tables import ASHRAETables from data.building_components import Wall as WallModel, Roof as RoofModel, Skylight as SkylightModel # Import utility modules from utils.u_value_calculator import UValueCalculator from utils.shading_system import ShadingSystem from utils.area_calculation_system import AreaCalculationSystem from utils.psychrometrics import Psychrometrics from utils.heat_transfer import HeatTransferCalculations from utils.cooling_load import CoolingLoadCalculator from utils.heating_load import HeatingLoadCalculator from utils.component_visualization import ComponentVisualization from utils.scenario_comparison import ScenarioComparisonVisualization from utils.psychrometric_visualization import PsychrometricVisualization from utils.time_based_visualization import TimeBasedVisualization from data.drapery import Drapery # NEW: ASHRAE 62.1 Ventilation Rates (Table 6.1) VENTILATION_RATES = { "Office": {"people_rate": 2.5, "area_rate": 0.3}, # L/s/person, L/s/m² "Classroom": {"people_rate": 5.0, "area_rate": 0.9}, "Retail": {"people_rate": 3.8, "area_rate": 0.9}, "Restaurant": {"people_rate": 5.0, "area_rate": 1.8}, "Custom": {"people_rate": 0.0, "area_rate": 0.0} } # Valid wall and roof groups for ASHRAE CLTD tables (aligned with cooling_load.py) VALID_WALL_GROUPS = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'] VALID_ROOF_GROUPS = ['A', 'B', 'C', 'D', 'E', 'F', 'G'] class HVACCalculator: def __init__(self): st.set_page_config( page_title="HVAC Load Calculator", page_icon="🌡️", layout="wide", initial_sidebar_state="expanded" ) # Initialize session state if 'page' not in st.session_state: st.session_state.page = 'Building Information' if 'building_info' not in st.session_state: st.session_state.building_info = {"project_name": ""} if 'components' not in st.session_state: st.session_state.components = { 'walls': [], 'roofs': [], 'floors': [], 'windows': [], 'doors': [], 'skylights': [] } if 'internal_loads' not in st.session_state: st.session_state.internal_loads = { 'people': [], 'lighting': [], 'equipment': [] } if 'calculation_results' not in st.session_state: st.session_state.calculation_results = { 'cooling': {}, 'heating': {} } if 'saved_scenarios' not in st.session_state: st.session_state.saved_scenarios = {} if 'climate_data' not in st.session_state: st.session_state.climate_data = {} if 'debug_mode' not in st.session_state: st.session_state.debug_mode = False # Initialize modules self.building_info_form = BuildingInfoForm() self.component_selection = ComponentSelectionInterface() self.results_display = ResultsDisplay() self.data_validation = DataValidation() self.data_persistence = DataPersistence() self.data_export = DataExport() self.cooling_calculator = CoolingLoadCalculator() self.heating_calculator = HeatingLoadCalculator() # Initialize Drapery with UI inputs from session state self.drapery = Drapery( openness=st.session_state.get('drapery_openness', 'Semi-Open'), color=st.session_state.get('drapery_color', 'Medium'), fullness=st.session_state.get('drapery_fullness', 1.5), enabled=st.session_state.get('drapery_enabled', True), shading_device=st.session_state.get('shading_device', 'Drapes') ) # Persist ClimateData in session_state if 'climate_data_obj' not in st.session_state: st.session_state.climate_data_obj = ClimateData() self.climate_data = st.session_state.climate_data_obj # Load default climate data if locations are empty try: if not self.climate_data.locations: self.climate_data = ClimateData.from_json("/home/user/app/climate_data.json") st.session_state.climate_data_obj = self.climate_data except FileNotFoundError: st.warning("Default climate data file not found. Please enter climate data manually.") self.setup_layout() def setup_layout(self): st.sidebar.title("HVAC Load Calculator") st.sidebar.markdown("---") st.sidebar.subheader("Navigation") pages = [ "Building Information", "Climate Data", "Building Components", "Internal Loads", "Calculation Results", "Export Data" ] selected_page = st.sidebar.radio("Go to", pages, index=pages.index(st.session_state.page)) if selected_page != st.session_state.page: st.session_state.page = selected_page self.display_page(st.session_state.page) st.sidebar.markdown("---") st.sidebar.info( "HVAC Load Calculator v1.0.2\n\n" "Based on ASHRAE steady-state calculation methods\n\n" "Developed by: Dr Majed Abuseif\n\n" "School of Architecture and Built Environment\n\n" "Deakin University\n\n" "© 2025" ) def display_page(self, page: str): if page == "Building Information": self.building_info_form.display_building_info_form(st.session_state) elif page == "Climate Data": self.climate_data.display_climate_input(st.session_state) elif page == "Building Components": self.component_selection.display_component_selection(st.session_state) elif page == "Internal Loads": self.display_internal_loads() elif page == "Calculation Results": self.display_calculation_results() elif page == "Export Data": self.data_export.display() def generate_climate_id(self, country: str, city: str) -> str: """Generate a climate ID from country and city names.""" try: country = country.strip().title() city = city.strip().title() if len(country) < 2 or len(city) < 3: raise ValueError("Country and city names must be at least 2 and 3 characters long, respectively.") return f"{country[:2].upper()}-{city[:3].upper()}" except Exception as e: raise ValueError(f"Invalid country or city name: {str(e)}") def validate_calculation_inputs(self) -> Tuple[bool, str]: """Validate inputs for cooling and heating calculations.""" building_info = st.session_state.get('building_info', {}) components = st.session_state.get('components', {}) climate_data = st.session_state.get('climate_data', {}) # Check building info if not building_info.get('floor_area', 0) > 0: return False, "Floor area must be positive." if not any(components.get(key, []) for key in ['walls', 'roofs', 'windows', 'skylights']): return False, "At least one wall, roof, window, or skylight must be defined." # Validate climate data if not climate_data: return False, "Climate data is missing." if not self.climate_data.validate_climate_data(climate_data): return False, "Invalid climate data format or values." # Validate and fix component groups for component_type in ['walls', 'roofs']: for comp in components.get(component_type, []): if component_type == 'walls': wall_group = str(getattr(comp, 'wall_group', 'A')).upper() if wall_group not in VALID_WALL_GROUPS: st.warning(f"Invalid wall group '{wall_group}' for {comp.name}. Setting to 'A'.") comp.wall_group = 'A' if st.session_state.get('debug_mode', False): st.write(f"Debug: Wall {comp.name} group set to {comp.wall_group}") if component_type == 'roofs': roof_group = str(getattr(comp, 'roof_group', 'A')).upper() if roof_group not in VALID_ROOF_GROUPS: st.warning(f"Invalid roof group '{roof_group}' for {comp.name}. Setting to 'A'.") comp.roof_group = 'A' if st.session_state.get('debug_mode', False): st.write(f"Debug: Roof {comp.name} group set to {comp.roof_group}") # Validate components for component_type in ['walls', 'roofs', 'windows', 'doors', 'floors', 'skylights']: for comp in components.get(component_type, []): if comp.area <= 0: return False, f"Invalid area for {component_type}: {comp.name}" if comp.u_value <= 0: return False, f"Invalid U-value for {component_type}: {comp.name}" if component_type == 'floors' and getattr(comp, 'ground_contact', False): if not -10 <= comp.ground_temperature_c <= 40: return False, f"Ground temperature for {comp.name} must be between -10°C and 40°C" if getattr(comp, 'perimeter', 0) < 0: return False, f"Perimeter for {comp.name} cannot be negative" if component_type in ['walls', 'roofs']: if not 0.1 <= getattr(comp, 'solar_absorptivity', 0.6) <= 1.0: return False, f"Invalid solar absorptivity for {component_type}: {comp.name} (must be 0.1-1.0)" if component_type in ['windows', 'skylights']: if getattr(comp, 'shgc', 0) <= 0: return False, f"Invalid SHGC for {component_type}: {comp.name}" if getattr(comp, 'glazing_type', None) is None: return False, f"Glazing type missing for {component_type}: {comp.name}" if getattr(comp, 'frame_type', None) is None: return False, f"Frame type missing for {component_type}: {comp.name}" # Validate ventilation rate if building_info.get('ventilation_rate', 0) < 0: return False, "Ventilation rate cannot be negative" if building_info.get('zone_type', '') == 'Custom' and building_info.get('ventilation_rate', 0) == 0: return False, "Custom ventilation rate must be specified" # Validate new inputs if not -50 <= building_info.get('winter_temp', -10) <= 20: return False, "Winter design temperature must be -50 to 20°C" if not 0 <= building_info.get('outdoor_rh', 50) <= 100: return False, "Outdoor relative humidity must be 0-100%" if not 0 <= building_info.get('indoor_rh', 50) <= 100: return False, "Indoor relative humidity must be 0-100%" if not 0 <= building_info.get('building_height', 3) <= 100: return False, "Building height must be 0-100m" return True, "Inputs valid." def validate_internal_load(self, load_type: str, new_load: Dict) -> Tuple[bool, str]: """Validate if a new internal load is unique and within limits.""" loads = st.session_state.internal_loads.get(load_type, []) max_loads = 50 if len(loads) >= max_loads: return False, f"Maximum of {max_loads} {load_type} loads reached." for existing_load in loads: if load_type == 'people': if (existing_load['name'] == new_load['name'] and existing_load['num_people'] == new_load['num_people'] and existing_load['activity_level'] == new_load['activity_level'] and existing_load['zone_type'] == new_load['zone_type'] and existing_load['hours_in_operation'] == new_load['hours_in_operation'] and existing_load['latent_gain'] == new_load['latent_gain']): return False, f"Duplicate people load '{new_load['name']}' already exists." elif load_type == 'lighting': if (existing_load['name'] == new_load['name'] and existing_load['power'] == new_load['power'] and existing_load['usage_factor'] == new_load['usage_factor'] and existing_load['zone_type'] == new_load['zone_type'] and existing_load['hours_in_operation'] == new_load['hours_in_operation']): return False, f"Duplicate lighting load '{new_load['name']}' already exists." elif load_type == 'equipment': if (existing_load['name'] == new_load['name'] and existing_load['power'] == new_load['power'] and existing_load['usage_factor'] == new_load['usage_factor'] and existing_load['radiation_fraction'] == new_load['radiation_fraction'] and existing_load['zone_type'] == new_load['zone_type'] and existing_load['hours_in_operation'] == new_load['hours_in_operation']): return False, f"Duplicate equipment load '{new_load['name']}' already exists." return True, "Valid load." def parse_latitude(self, latitude: Any) -> str: """Parse latitude from string or number to ASHRAE table format (e.g., '24N').""" try: # Use cooling_calculator's validate_latitude for consistency return self.cooling_calculator.validate_latitude(latitude) except Exception as e: st.error(f"Invalid latitude: {latitude}. Using default 32N.") return "32N" def display_internal_loads(self): st.title("Internal Loads") if st.button("Reset All Internal Loads"): st.session_state.internal_loads = {'people': [], 'lighting': [], 'equipment': []} st.success("All internal loads reset!") st.rerun() tabs = st.tabs(["People", "Lighting", "Equipment", "Ventilation"]) with tabs[0]: st.subheader("People") with st.form("people_form"): num_people = st.number_input( "Number of People", min_value=0, value=0, step=1, help="Total number of occupants in the building" ) activity_level = st.selectbox( "Activity Level", ["Seated/Resting", "Light Work", "Moderate Work", "Heavy Work"], help="Select typical activity level (affects internal heat gains per ASHRAE)" ) zone_type = st.selectbox( "Zone Type", ["A", "B", "C", "D"], help="Select zone type for CLF accuracy per ASHRAE" ) hours_in_operation = st.selectbox( "Hours Occupied", ["2h", "4h", "6h"], help="Select hours of occupancy for CLF calculations" ) latent_gain = st.number_input( "Latent Gain per Person (Btu/h)", min_value=0.0, max_value=500.0, value=200.0, step=10.0, help="Latent heat gain per person per ASHRAE" ) people_name = st.text_input("Name", value="Occupants") if st.form_submit_button("Add People Load"): people_load = { "id": f"people_{len(st.session_state.internal_loads['people'])}", "name": people_name, "num_people": num_people, "activity_level": activity_level, "zone_type": zone_type, "hours_in_operation": hours_in_operation, "latent_gain": latent_gain } is_valid, message = self.validate_internal_load('people', people_load) if is_valid: st.session_state.internal_loads['people'].append(people_load) st.success("People load added!") st.rerun() else: st.error(message) if st.session_state.internal_loads['people']: people_df = pd.DataFrame(st.session_state.internal_loads['people']) st.dataframe(people_df, use_container_width=True) selected_people = st.multiselect( "Select People Loads to Delete", [load['id'] for load in st.session_state.internal_loads['people']] ) if st.button("Delete Selected People Loads"): st.session_state.internal_loads['people'] = [ load for load in st.session_state.internal_loads['people'] if load['id'] not in selected_people ] st.success("Selected people loads deleted!") st.rerun() with tabs[1]: st.subheader("Lighting") with st.form("lighting_form"): power = st.number_input( "Power (W)", min_value=0.0, value=1000.0, step=100.0, help="Total lighting power consumption" ) usage_factor = st.number_input( "Usage Factor", min_value=0.0, max_value=1.0, value=0.8, step=0.1, help="Fraction of time lighting is in use (0 to 1)" ) zone_type = st.selectbox( "Zone Type", ["A", "B", "C", "D"], help="Select zone type for CLF accuracy per ASHRAE" ) hours_in_operation = st.selectbox( "Hours On", ["8h", "10h", "12h"], help="Select hours of lighting operation for CLF calculations" ) lighting_name = st.text_input("Name", value="General Lighting") if st.form_submit_button("Add Lighting Load"): lighting_load = { "id": f"lighting_{len(st.session_state.internal_loads['lighting'])}", "name": lighting_name, "power": power, "usage_factor": usage_factor, "zone_type": zone_type, "hours_in_operation": hours_in_operation } is_valid, message = self.validate_internal_load('lighting', lighting_load) if is_valid: st.session_state.internal_loads['lighting'].append(lighting_load) st.success("Lighting load added!") st.rerun() else: st.error(message) if st.session_state.internal_loads['lighting']: lighting_df = pd.DataFrame(st.session_state.internal_loads['lighting']) st.dataframe(lighting_df, use_container_width=True) selected_lighting = st.multiselect( "Select Lighting Loads to Delete", [load['id'] for load in st.session_state.internal_loads['lighting']] ) if st.button("Delete Selected Lighting Loads"): st.session_state.internal_loads['lighting'] = [ load for load in st.session_state.internal_loads['lighting'] if load['id'] not in selected_lighting ] st.success("Selected lighting loads deleted!") st.rerun() with tabs[2]: st.subheader("Equipment") with st.form("equipment_form"): power = st.number_input( "Power (W)", min_value=0.0, value=500.0, step=100.0, help="Total equipment power consumption" ) usage_factor = st.number_input( "Usage Factor", min_value=0.0, max_value=1.0, value=0.7, step=0.1, help="Fraction of time equipment is in use (0 to 1)" ) radiation_fraction = st.number_input( "Radiation Fraction", min_value=0.0, max_value=1.0, value=0.3, step=0.1, help="Fraction of heat gain radiated to surroundings" ) zone_type = st.selectbox( "Zone Type", ["A", "B", "C", "D"], help="Select zone type for CLF accuracy per ASHRAE" ) hours_in_operation = st.selectbox( "Hours Operated", ["2h", "4h", "6h"], help="Select hours of equipment operation for CLF calculations" ) equipment_name = st.text_input("Name", value="Office Equipment") if st.form_submit_button("Add Equipment Load"): equipment_load = { "id": f"equipment_{len(st.session_state.internal_loads['equipment'])}", "name": equipment_name, "power": power, "usage_factor": usage_factor, "radiation_fraction": radiation_fraction, "zone_type": zone_type, "hours_in_operation": hours_in_operation } is_valid, message = self.validate_internal_load('equipment', equipment_load) if is_valid: st.session_state.internal_loads['equipment'].append(equipment_load) st.success("Equipment load added!") st.rerun() else: st.error(message) if st.session_state.internal_loads['equipment']: equipment_df = pd.DataFrame(st.session_state.internal_loads['equipment']) st.dataframe(equipment_df, use_container_width=True) selected_equipment = st.multiselect( "Select Equipment Loads to Delete", [load['id'] for load in st.session_state.internal_loads['equipment']] ) if st.button("Delete Selected Equipment Loads"): st.session_state.internal_loads['equipment'] = [ load for load in st.session_state.internal_loads['equipment'] if load['id'] not in selected_equipment ] st.success("Selected equipment loads deleted!") st.rerun() with tabs[3]: st.subheader("Ventilation Requirements (ASHRAE 62.1)") with st.form("ventilation_form"): col1, col2 = st.columns(2) with col1: zone_type = st.selectbox( "Zone Type", ["Office", "Classroom", "Retail", "Restaurant", "Custom"], help="Select building zone type for ASHRAE 62.1 ventilation rates" ) ventilation_method = st.selectbox( "Ventilation Method", ["Constant Volume", "Demand-Controlled"], help="Constant Volume uses fixed rate; Demand-Controlled adjusts based on occupancy" ) with col2: if zone_type == "Custom": people_rate = st.number_input( "Ventilation Rate per Person (L/s/person)", min_value=0.0, value=2.5, step=0.1, help="Custom ventilation rate per person (ASHRAE 62.1)" ) area_rate = st.number_input( "Ventilation Rate per Area (L/s/m²)", min_value=0.0, value=0.3, step=0.1, help="Custom ventilation rate per floor area (ASHRAE 62.1)" ) ventilation_rate = st.number_input( "Ventilation Rate (m³/s)", min_value=0.0, max_value=10.0, value=0.0, step=0.1, help="Total ventilation rate for custom zone type" ) else: people_rate = VENTILATION_RATES[zone_type]["people_rate"] area_rate = VENTILATION_RATES[zone_type]["area_rate"] st.write(f"People Rate: {people_rate} L/s/person (ASHRAE 62.1)") st.write(f"Area Rate: {area_rate} L/s/m² (ASHRAE 62.1)") ventilation_rate = st.number_input( "Ventilation Rate (m³/s)", min_value=0.0, max_value=10.0, value=0.0, step=0.1, help="Total ventilation rate (override ASHRAE defaults if needed)" ) if st.form_submit_button("Save Ventilation Settings"): total_people = sum(load['num_people'] for load in st.session_state.internal_loads.get('people', [])) floor_area = st.session_state.building_info.get('floor_area', 100.0) calculated_rate = ( (total_people * people_rate + floor_area * area_rate) / 1000 # Convert L/s to m³/s ) final_rate = ventilation_rate if ventilation_rate > 0 else calculated_rate if ventilation_method == 'Demand-Controlled': final_rate *= 0.75 # Reduce by 25% for DCV st.session_state.building_info.update({ 'zone_type': zone_type, 'ventilation_method': ventilation_method, 'ventilation_rate': final_rate }) st.success(f"Ventilation settings saved! Total rate: {final_rate:.3f} m³/s") col1, col2 = st.columns(2) with col1: st.button( "Back to Building Components", on_click=lambda: setattr(st.session_state, "page", "Building Components") ) with col2: st.button( "Continue to Calculation Results", on_click=lambda: setattr(st.session_state, "page", "Calculation Results") ) def calculate_cooling(self) -> Tuple[bool, str, Dict]: """ Calculate cooling loads using CoolingLoadCalculator. Returns: (success, message, results) """ try: # Validate inputs valid, message = self.validate_calculation_inputs() if not valid: return False, message, {} # Gather inputs building_components = st.session_state.get('components', {}) internal_loads = st.session_state.get('internal_loads', {}) building_info = st.session_state.get('building_info', {}) # Check climate data if "climate_data" not in st.session_state or not st.session_state["climate_data"]: return False, "Please enter climate data in the 'Climate Data' page.", {} # Extract climate data country = building_info.get('country', '').strip().title() city = building_info.get('city', '').strip().title() if not country or not city: return False, "Country and city must be set in Building Information.", {} climate_id = self.generate_climate_id(country, city) location = self.climate_data.get_location_by_id(climate_id, st.session_state) if not location: available_locations = list(self.climate_data.locations.keys())[:5] return False, f"No climate data for {climate_id}. Available locations: {', '.join(available_locations)}...", {} # Validate climate data if not all(k in location for k in ['summer_design_temp_db', 'summer_design_temp_wb', 'monthly_temps', 'latitude']): return False, f"Invalid climate data for {climate_id}. Missing required fields.", {} # NEW: Month-to-day mapping per Plan.txt month_to_day = { "Jan": 15, "Feb": 45, "Mar": 74, "Apr": 105, "May": 135, "Jun": 166, "Jul": 196, "Aug": 227, "Sep": 258, "Oct": 288, "Nov": 319, "Dec": 350 } # Validate latitude using cooling_calculator raw_latitude = location.get('latitude', '32N') latitude = self.cooling_calculator.validate_latitude(raw_latitude) if st.session_state.get('debug_mode', False): st.write(f"Debug: Raw latitude: {raw_latitude}, Validated latitude: {latitude}") # Format conditions outdoor_conditions = { 'temperature': location['summer_design_temp_db'], 'relative_humidity': building_info.get('outdoor_rh', location['monthly_humidity'].get('Jul', 50.0)), 'ground_temperature': location['monthly_temps'].get('Jul', 20.0), 'month': 'Jul', 'latitude': latitude, 'wind_speed': building_info.get('wind_speed', 4.0), 'day_of_year': month_to_day.get('Jul', 182) } indoor_conditions = { 'temperature': building_info.get('indoor_temp', 24.0), 'relative_humidity': building_info.get('indoor_rh', 50.0) } if st.session_state.get('debug_mode', False): st.write("Debug: Cooling Input State", { 'climate_id': climate_id, 'outdoor_conditions': outdoor_conditions, 'indoor_conditions': indoor_conditions, 'components': {k: len(v) for k, v in building_components.items()}, 'internal_loads': { 'people': len(internal_loads.get('people', [])), 'lighting': len(internal_loads.get('lighting', [])), 'equipment': len(internal_loads.get('equipment', [])) }, 'building_info': building_info }) # Format internal loads formatted_internal_loads = { 'people': { 'number': sum(load['num_people'] for load in internal_loads.get('people', [])), 'activity_level': internal_loads.get('people', [{}])[0].get('activity_level', 'Seated/Resting'), 'operating_hours': internal_loads.get('people', [{}])[0].get('hours_in_operation', '8h'), 'zone_type': internal_loads.get('people', [{}])[0].get('zone_type', 'A'), 'latent_gain': internal_loads.get('people', [{}])[0].get('latent_gain', 200.0) }, 'lights': { 'power': sum(load['power'] for load in internal_loads.get('lighting', [])), 'use_factor': internal_loads.get('lighting', [{}])[0].get('usage_factor', 0.8), 'special_allowance': 0.1, 'hours_operation': internal_loads.get('lighting', [{}])[0].get('hours_in_operation', '8h'), 'zone_type': internal_loads.get('lighting', [{}])[0].get('zone_type', 'A') }, 'equipment': { 'power': sum(load['power'] for load in internal_loads.get('equipment', [])), 'use_factor': internal_loads.get('equipment', [{}])[0].get('usage_factor', 0.7), 'radiation_factor': internal_loads.get('equipment', [{}])[0].get('radiation_fraction', 0.3), 'hours_operation': internal_loads.get('equipment', [{}])[0].get('hours_in_operation', '8h'), 'zone_type': internal_loads.get('equipment', [{}])[0].get('zone_type', 'A') }, 'infiltration': { 'flow_rate': building_info.get('infiltration_rate', 0.5), 'height': building_info.get('building_height', 3.0), 'crack_length': building_info.get('crack_length', 10.0) }, 'ventilation': { 'flow_rate': building_info.get('ventilation_rate', 0.1) }, 'operating_hours': building_info.get('operating_hours', '8:00-18:00') } # Calculate hourly loads hourly_loads = self.cooling_calculator.calculate_hourly_cooling_loads( building_components=building_components, outdoor_conditions=outdoor_conditions, indoor_conditions=indoor_conditions, internal_loads=formatted_internal_loads, building_volume=building_info.get('floor_area', 100.0) * building_info.get('building_height', 3.0) ) if not hourly_loads: return False, "Cooling hourly loads calculation failed. Check input data.", {} # Get design loads design_loads = self.cooling_calculator.calculate_design_cooling_load(hourly_loads) if not design_loads: return False, "Cooling design loads calculation failed. Check input data.", {} # Get summary summary = self.cooling_calculator.calculate_cooling_load_summary(design_loads) if not summary: return False, "Cooling load summary calculation failed. Check input data.", {} # Ensure summary has all required keys if 'total' not in summary: if 'total_sensible' in summary and 'total_latent' in summary: summary['total'] = summary['total_sensible'] + summary['total_latent'] else: total_load = sum(value for key, value in design_loads.items() if key != 'design_hour') summary = { 'total_sensible': total_load * 0.7, 'total_latent': total_load * 0.3, 'total': total_load } # Format results for results_display.py floor_area = building_info.get('floor_area', 100.0) or 100.0 results = { 'total_load': summary['total'] / 1000, # kW 'sensible_load': summary['total_sensible'] / 1000, # kW 'latent_load': summary['total_latent'] / 1000, # kW 'load_per_area': summary['total'] / floor_area, # W/m² 'component_loads': { 'walls': design_loads['walls'] / 1000, 'roof': design_loads['roofs'] / 1000, 'windows': (design_loads['windows_conduction'] + design_loads['windows_solar']) / 1000, 'doors': design_loads['doors'] / 1000, 'skylights': design_loads.get('skylights', 0) / 1000, 'people': (design_loads['people_sensible'] + design_loads['people_latent']) / 1000, 'lighting': design_loads['lights'] / 1000, 'equipment': (design_loads['equipment_sensible'] + design_loads['equipment_latent']) / 1000, 'infiltration': (design_loads['infiltration_sensible'] + design_loads['infiltration_latent']) / 1000, 'ventilation': (design_loads['ventilation_sensible'] + design_loads['ventilation_latent']) / 1000 }, 'detailed_loads': { 'walls': [], 'roofs': [], 'windows': [], 'doors': [], 'skylights': [], 'internal': [], 'infiltration': { 'air_flow': formatted_internal_loads['infiltration']['flow_rate'], 'sensible_load': design_loads['infiltration_sensible'] / 1000, 'latent_load': design_loads['infiltration_latent'] / 1000, 'total_load': (design_loads['infiltration_sensible'] + design_loads['infiltration_latent']) / 1000 }, 'ventilation': { 'air_flow': formatted_internal_loads['ventilation']['flow_rate'], 'sensible_load': design_loads['ventilation_sensible'] / 1000, 'latent_load': design_loads['infiltration_latent'] / 1000, 'total_load': (design_loads['ventilation_sensible'] + design_loads['ventilation_latent']) / 1000 } }, 'building_info': building_info } # Populate detailed loads for wall in building_components.get('walls', []): try: load = self.cooling_calculator.calculate_wall_cooling_load( wall=wall, outdoor_temp=outdoor_conditions['temperature'], indoor_temp=indoor_conditions['temperature'], month=outdoor_conditions['month'], hour=design_loads['design_hour'], latitude=outdoor_conditions['latitude'], solar_absorptivity=wall.solar_absorptivity ) if st.session_state.get('debug_mode', False): st.write("Debug: Wall CLTD Inputs", { 'wall_name': wall.name, 'element_type': 'wall', 'group': wall.wall_group, 'orientation': wall.orientation.value, 'hour': design_loads['design_hour'], 'latitude': outdoor_conditions['latitude'], 'solar_absorptivity': wall.solar_absorptivity }) try: lat_value = float(outdoor_conditions['latitude'].replace('N', '')) if st.session_state.get('debug_mode', False): st.write(f"Debug: Converted wall latitude {outdoor_conditions['latitude']} to {lat_value} for get_cltd") except ValueError: lat_value = 32.0 if st.session_state.get('debug_mode', False): st.error(f"Invalid latitude format in wall load: {outdoor_conditions['latitude']}. Defaulting to 32.0") results['detailed_loads']['walls'].append({ 'name': wall.name, 'orientation': wall.orientation.value, 'area': wall.area, 'u_value': wall.u_value, 'solar_absorptivity': wall.solar_absorptivity, 'cltd': self.cooling_calculator.ashrae_tables.get_cltd( element_type='wall', group=wall.wall_group, orientation=wall.orientation.value, hour=design_loads['design_hour'], latitude=lat_value, solar_absorptivity=wall.solar_absorptivity ), 'load': load / 1000 }) except TypeError as te: st.error(f"Type error in wall CLTD calculation for {wall.name}: {str(te)}") return False, f"Type error in wall CLTD calculation: {str(te)}", {} except Exception as e: if st.session_state.get('debug_mode', False): st.error(f"Error in wall CLTD calculation for {wall.name}: {str(e)}") return False, f"Error in wall CLTD calculation: {str(e)}", {} for roof in building_components.get('roofs', []): try: load = self.cooling_calculator.calculate_roof_cooling_load( roof=roof, outdoor_temp=outdoor_conditions['temperature'], indoor_temp=indoor_conditions['temperature'], month=outdoor_conditions['month'], hour=design_loads['design_hour'], latitude=outdoor_conditions['latitude'], solar_absorptivity=roof.solar_absorptivity ) if st.session_state.get('debug_mode', False): st.write("Debug: Roof CLTD Inputs", { 'roof_name': roof.name, 'element_type': 'roof', 'group': roof.roof_group, 'orientation': roof.orientation.value, 'hour': design_loads['design_hour'], 'latitude': outdoor_conditions['latitude'], 'solar_absorptivity': roof.solar_absorptivity }) try: lat_value = float(outdoor_conditions['latitude'].replace('N', '')) if st.session_state.get('debug_mode', False): st.write(f"Debug: Converted roof latitude {outdoor_conditions['latitude']} to {lat_value} for get_cltd") except ValueError: lat_value = 32.0 if st.session_state.get('debug_mode', False): st.error(f"Invalid latitude format in roof load: {outdoor_conditions['latitude']}. Defaulting to 32.0") results['detailed_loads']['roofs'].append({ 'name': roof.name, 'orientation': roof.orientation.value, 'area': roof.area, 'u_value': roof.u_value, 'solar_absorptivity': roof.solar_absorptivity, 'cltd': self.cooling_calculator.ashrae_tables.get_cltd( element_type='roof', group=roof.roof_group, orientation=roof.orientation.value, hour=design_loads['design_hour'], latitude=lat_value, solar_absorptivity=roof.solar_absorptivity ), 'load': load / 1000 }) except TypeError as te: st.error(f"Type error in roof CLTD calculation for {roof.name}: {str(te)}") return False, f"Type error in roof CLTD calculation: {str(te)}", {} except Exception as e: if st.session_state.get('debug_mode', False): st.error(f"Error in roof CLTD calculation for {roof.name}: {str(e)}") return False, f"Error in roof CLTD calculation: {str(e)}", {} for window in building_components.get('windows', []): adjusted_shgc = window.shgc # Default to base SHGC if hasattr(window, 'drapery_type') and window.drapery_type and self.drapery.enabled: try: adjusted_shgc = self.drapery.get_shading_coefficient(window.shgc) if st.session_state.get('debug_mode', False): st.write(f"Debug: Window {window.name} adjusted SHGC: {adjusted_shgc}") except Exception as e: if st.session_state.get('debug_mode', False): st.error(f"Error adjusting SHGC for window {window.name}: {str(e)}") adjusted_shgc = window.shgc load_dict = self.cooling_calculator.calculate_window_cooling_load( window=window, outdoor_temp=outdoor_conditions['temperature'], indoor_temp=indoor_conditions['temperature'], month=outdoor_conditions['month'], hour=design_loads['design_hour'], latitude=outdoor_conditions['latitude'], shading_coefficient=window.shading_coefficient, adjusted_shgc=adjusted_shgc, glazing_type=window.glazing_type, frame_type=window.frame_type ) if 'total' not in load_dict: if 'conduction' in load_dict and 'solar' in load_dict: load_dict['total'] = load_dict['conduction'] + load_dict['solar'] else: load_dict['total'] = window.u_value * window.area * (outdoor_conditions['temperature'] - indoor_conditions['temperature']) results['detailed_loads']['windows'].append({ 'name': window.name, 'orientation': window.orientation.value, 'area': window.area, 'u_value': window.u_value, 'shgc': window.shgc, 'adjusted_shgc': adjusted_shgc, 'glazing_type': window.glazing_type, 'frame_type': window.frame_type, 'drapery_type': window.drapery_type if hasattr(window, 'drapery_type') else 'None', 'shading_device': window.shading_device, 'shading_coefficient': window.shading_coefficient, 'scl': self.cooling_calculator.ashrae_tables.get_scl( latitude=outdoor_conditions['latitude'], month=outdoor_conditions['month'].lower(), orientation=window.orientation.value, hour=design_loads['design_hour'] ) * 3.15459, # Convert Btu/h-ft² to W/m² 'load': load_dict['total'] / 1000 }) for door in building_components.get('doors', []): load = self.cooling_calculator.calculate_door_cooling_load( door=door, outdoor_temp=outdoor_conditions['temperature'], indoor_temp=indoor_conditions['temperature'] ) results['detailed_loads']['doors'].append({ 'name': door.name, 'orientation': door.orientation.value, 'area': door.area, 'u_value': door.u_value, 'cltd': outdoor_conditions['temperature'] - indoor_conditions['temperature'], 'load': load / 1000 }) for skylight in building_components.get('skylights', []): adjusted_shgc = skylight.shgc # Default to base SHGC if hasattr(skylight, 'drapery_type') and skylight.drapery_type and self.drapery.enabled: try: adjusted_shgc = self.drapery.get_shading_coefficient(skylight.shgc) if st.session_state.get('debug_mode', False): st.write(f"Debug: Skylight {skylight.name} adjusted SHGC: {adjusted_shgc}") except Exception as e: if st.session_state.get('debug_mode', False): st.error(f"Error adjusting SHGC for skylight {skylight.name}: {str(e)}") adjusted_shgc = skylight.shgc load_dict = self.cooling_calculator.calculate_skylight_cooling_load( skylight=skylight, outdoor_temp=outdoor_conditions['temperature'], indoor_temp=indoor_conditions['temperature'], month=outdoor_conditions['month'], hour=design_loads['design_hour'], latitude=outdoor_conditions['latitude'], shading_coefficient=skylight.shading_coefficient, adjusted_shgc=adjusted_shgc, glazing_type=skylight.glazing_type, frame_type=skylight.frame_type ) if 'total' not in load_dict: if 'conduction' in load_dict and 'solar' in load_dict: load_dict['total'] = load_dict['conduction'] + load_dict['solar'] else: load_dict['total'] = skylight.u_value * skylight.area * (outdoor_conditions['temperature'] - indoor_conditions['temperature']) results['detailed_loads']['skylights'].append({ 'name': skylight.name, 'area': skylight.area, 'u_value': skylight.u_value, 'shgc': skylight.shgc, 'adjusted_shgc': adjusted_shgc, 'glazing_type': skylight.glazing_type, 'frame_type': skylight.frame_type, 'drapery_type': skylight.drapery_type if hasattr(skylight, 'drapery_type') else 'None', 'shading_coefficient': skylight.shading_coefficient, 'scl': self.cooling_calculator.ashrae_tables.get_scl( latitude=outdoor_conditions['latitude'], month=outdoor_conditions['month'].lower(), orientation='Horizontal', hour=design_loads['design_hour'] ) * 3.15459, # Convert Btu/h-ft² to W/m² 'load': load_dict['total'] / 1000 }) for load_type, key in [('people', 'people'), ('lighting', 'lights'), ('equipment', 'equipment')]: for load in internal_loads.get(key, []): if load_type == 'people': load_dict = self.cooling_calculator.calculate_people_cooling_load( num_people=load['num_people'], activity_level=load['activity_level'], hour=design_loads['design_hour'], latent_gain=load.get('latent_gain', 200.0) ) if 'total' not in load_dict and ('sensible' in load_dict or 'latent' in load_dict): load_dict['total'] = load_dict.get('sensible', 0) + load_dict.get('latent', 0) elif load_type == 'lighting': light_load = self.cooling_calculator.calculate_lights_cooling_load( power=load['power'], use_factor=load['usage_factor'], special_allowance=0.1, hour=design_loads['design_hour'] ) load_dict = {'total': light_load if light_load is not None else 0} else: load_dict = self.cooling_calculator.calculate_equipment_cooling_load( power=load['power'], use_factor=load['usage_factor'], radiation_factor=load['radiation_fraction'], hour=design_loads['design_hour'] ) if 'total' not in load_dict and ('sensible' in load_dict or 'latent' in load_dict): load_dict['total'] = load_dict.get('sensible', 0) + load_dict.get('latent', 0) results['detailed_loads']['internal'].append({ 'type': load_type.capitalize(), 'name': load['name'], 'quantity': load.get('num_people', load.get('power', 1)), 'heat_gain': load_dict.get('sensible', load_dict.get('total', 0)), 'clf': self.cooling_calculator.ashrae_tables.get_clf_people( zone_type=load.get('zone_type', 'A'), hours_occupied=load.get('hours_in_operation', '6h'), hour=design_loads['design_hour'] ) if load_type == 'people' else 1.0, 'load': load_dict.get('total', 0) / 1000 }) if st.session_state.get('debug_mode', False): st.write("Debug: Cooling Results", { 'total_load': results.get('total_load', 'N/A'), 'component_loads': results.get('component_loads', 'N/A'), 'detailed_loads': {k: len(v) if isinstance(v, list) else v for k, v in results.get('detailed_loads', {}).items()} }) return True, "Cooling calculation completed.", results except ValueError as ve: st.error(f"Input error in cooling calculation: {str(ve)}") return False, f"Input error: {str(ve)}", {} except KeyError as ke: st.error(f"Missing data in cooling calculation: {str(ke)}") return False, f"Missing data: {str(ke)}", {} except Exception as e: st.error(f"Unexpected error in cooling calculation: {str(e)}") return False, f"Unexpected error: {str(e)}", {} def calculate_heating(self) -> Tuple[bool, str, Dict]: """ Calculate heating loads using HeatingLoadCalculator. Returns: (success, message, results) """ try: # Validate inputs valid, message = self.validate_calculation_inputs() if not valid: return False, message, {} # Gather inputs building_components = st.session_state.get('components', {}) internal_loads = st.session_state.get('internal_loads', {}) building_info = st.session_state.get('building_info', {}) # Check climate data if "climate_data" not in st.session_state or not st.session_state["climate_data"]: return False, "Please enter climate data in the 'Climate Data' page.", {} # Extract climate data country = building_info.get('country', '').strip().title() city = building_info.get('city', '').strip().title() if not country or not city: return False, "Country and city must be set in Building Information.", {} climate_id = self.generate_climate_id(country, city) location = self.climate_data.get_location_by_id(climate_id, st.session_state) if not location: available_locations = list(self.climate_data.locations.keys())[:5] return False, f"No climate data for {climate_id}. Available locations: {', '.join(available_locations)}...", {} # Validate climate data if not all(k in location for k in ['winter_design_temp', 'monthly_temps', 'monthly_humidity']): return False, f"Invalid climate data for {climate_id}. Missing required fields.", {} # Calculate ground temperature ground_contact_floors = [f for f in building_components.get('floors', []) if getattr(f, 'ground_contact', False)] ground_temperature = ( sum(f.ground_temperature_c for f in ground_contact_floors) / len(ground_contact_floors) if ground_contact_floors else location['monthly_temps'].get('Jan', 10.0) ) if not -10 <= ground_temperature <= 40: return False, f"Invalid ground temperature: {ground_temperature}°C", {} # Skip heating calculation if outdoor temp exceeds indoor temp indoor_temp = building_info.get('indoor_temp', 21.0) outdoor_temp = building_info.get('winter_temp', location['winter_design_temp']) if outdoor_temp >= indoor_temp: results = { 'total_load': 0.0, 'load_per_area': 0.0, 'design_heat_loss': 0.0, 'safety_factor': 115.0, 'component_loads': { 'walls': 0.0, 'roof': 0.0, 'floor': 0.0, 'windows': 0.0, 'doors': 0.0, 'skylights': 0.0, 'infiltration': 0.0, 'ventilation': 0.0 }, 'detailed_loads': { 'walls': [], 'roofs': [], 'floors': [], 'windows': [], 'doors': [], 'skylights': [], 'infiltration': {'air_flow': 0.0, 'delta_t': 0.0, 'load': 0.0}, 'ventilation': {'air_flow': 0.0, 'delta_t': 0.0, 'load': 0.0} }, 'building_info': building_info } return True, "No heating required (outdoor temp exceeds indoor temp).", results # Format conditions outdoor_conditions = { 'design_temperature': outdoor_temp, 'design_relative_humidity': building_info.get('outdoor_rh', location['monthly_humidity'].get('Jan', 80.0)), 'ground_temperature': ground_temperature, 'wind_speed': building_info.get('wind_speed', 4.0) } indoor_conditions = { 'temperature': indoor_temp, 'relative_humidity': building_info.get('indoor_rh', 40.0) } if st.session_state.get('debug_mode', False): st.write("Debug: Heating Input State", { 'climate_id': climate_id, 'outdoor_conditions': outdoor_conditions, 'indoor_conditions': indoor_conditions, 'components': {k: len(v) for k, v in building_components.items()}, 'internal_loads': { 'people': len(internal_loads.get('people', [])), 'lighting': len(internal_loads.get('lighting', [])), 'equipment': len(internal_loads.get('equipment', [])) }, 'building_info': building_info }) # Activity-level-based sensible gains ACTIVITY_GAINS = { 'Seated/Resting': 70.0, # W/person 'Light Work': 85.0, 'Moderate Work': 100.0, 'Heavy Work': 150.0 } # Format internal loads formatted_internal_loads = { 'people': { 'number': sum(load['num_people'] for load in internal_loads.get('people', [])), 'sensible_gain': ACTIVITY_GAINS.get( internal_loads.get('people', [{}])[0].get('activity_level', 'Seated/Resting'), 70.0 ), 'latent_gain': internal_loads.get('people', [{}])[0].get('latent_gain', 200.0), 'operating_hours': internal_loads.get('people', [{}])[0].get('hours_in_operation', '8h'), 'zone_type': internal_loads.get('people', [{}])[0].get('zone_type', 'A') }, 'lights': { 'power': sum(load['power'] for load in internal_loads.get('lighting', [])), 'use_factor': internal_loads.get('lighting', [{}])[0].get('usage_factor', 0.8), 'hours_operation': internal_loads.get('lighting', [{}])[0].get('hours_in_operation', '8h'), 'zone_type': internal_loads.get('lighting', [{}])[0].get('zone_type', 'A') }, 'equipment': { 'power': sum(load['power'] for load in internal_loads.get('equipment', [])), 'use_factor': internal_loads.get('equipment', [{}])[0].get('usage_factor', 0.7), 'hours_operation': internal_loads.get('equipment', [{}])[0].get('hours_in_operation', '8h'), 'zone_type': internal_loads.get('equipment', [{}])[0].get('zone_type', 'A') }, 'infiltration': { 'flow_rate': building_info.get('infiltration_rate', 0.05), 'height': building_info.get('building_height', 3.0), 'crack_length': building_info.get('crack_length', 10.0) }, 'ventilation': { 'flow_rate': building_info.get('ventilation_rate', 0.1) }, 'usage_factor': 0.7, 'operating_hours': building_info.get('operating_hours', '8:00-18:00') } # Calculate design loads design_loads = self.heating_calculator.calculate_design_heating_load( building_components=building_components, outdoor_conditions=outdoor_conditions, indoor_conditions=indoor_conditions, internal_loads=formatted_internal_loads ) if not design_loads: return False, "Heating design loads calculation failed. Check input data.", {} # Get summary summary = self.heating_calculator.calculate_heating_load_summary(design_loads) if not summary: return False, "Heating load summary calculation failed. Check input data.", {} # Format results floor_area = building_info.get('floor_area', 100.0) or 100.0 results = { 'total_load': summary['total'] / 1000, # kW 'load_per_area': summary['total'] / floor_area, # W/m² 'design_heat_loss': summary['subtotal'] / 1000, # kW 'safety_factor': summary['safety_factor'] * 100, # % 'component_loads': { 'walls': design_loads['walls'] / 1000, 'roof': design_loads['roofs'] / 1000, 'floor': design_loads['floors'] / 1000, 'windows': design_loads['windows'] / 1000, 'doors': design_loads['doors'] / 1000, 'skylights': design_loads.get('skylights', 0) / 1000, 'infiltration': (design_loads['infiltration_sensible'] + design_loads['infiltration_latent']) / 1000, 'ventilation': (design_loads['ventilation_sensible'] + design_loads['ventilation_latent']) / 1000 }, 'detailed_loads': { 'walls': [], 'roofs': [], 'floors': [], 'windows': [], 'doors': [], 'skylights': [], 'infiltration': { 'air_flow': formatted_internal_loads['infiltration']['flow_rate'], 'delta_t': indoor_conditions['temperature'] - outdoor_conditions['design_temperature'], 'load': (design_loads['infiltration_sensible'] + design_loads['infiltration_latent']) / 1000 }, 'ventilation': { 'air_flow': formatted_internal_loads['ventilation']['flow_rate'], 'delta_t': indoor_conditions['temperature'] - outdoor_conditions['design_temperature'], 'load': (design_loads['ventilation_sensible'] + design_loads['ventilation_latent']) / 1000 } }, 'building_info': building_info } # Populate detailed loads delta_t = indoor_conditions['temperature'] - outdoor_conditions['design_temperature'] for wall in building_components.get('walls', []): load = self.heating_calculator.calculate_wall_heating_load( wall=wall, outdoor_temp=outdoor_conditions['design_temperature'], indoor_temp=indoor_conditions['temperature'] ) results['detailed_loads']['walls'].append({ 'name': wall.name, 'orientation': wall.orientation.value, 'area': wall.area, 'u_value': wall.u_value, 'solar_absorptivity': wall.solar_absorptivity, 'delta_t': delta_t, 'load': load / 1000 }) for roof in building_components.get('roofs', []): load = self.heating_calculator.calculate_roof_heating_load( roof=roof, outdoor_temp=outdoor_conditions['design_temperature'], indoor_temp=indoor_conditions['temperature'] ) results['detailed_loads']['roofs'].append({ 'name': roof.name, 'orientation': roof.orientation.value, 'area': roof.area, 'u_value': wall.u_value, 'solar_absorptivity': roof.solar_absorptivity, 'delta_t': delta_t, 'load': load / 1000 }) for floor in building_components.get('floors', []): load = self.heating_calculator.calculate_floor_heating_load( floor=floor, ground_temp=outdoor_conditions['ground_temperature'], indoor_temp=indoor_conditions['temperature'] ) results['detailed_loads']['floors'].append({ 'name': floor.name, 'area': floor.area, 'u_value': floor.u_value, 'delta_t': indoor_conditions['temperature'] - outdoor_conditions['ground_temperature'], 'load': load / 1000 }) for window in building_components.get('windows', []): load = self.heating_calculator.calculate_window_heating_load( window=window, outdoor_temp=outdoor_conditions['design_temperature'], indoor_temp=indoor_conditions['temperature'], frame_type=window.frame_type ) results['detailed_loads']['windows'].append({ 'name': window.name, 'orientation': wall.orientation.value, 'area': window.area, 'u_value': window.u_value, 'glazing_type': window.glazing_type, 'frame_type': window.frame_type, 'delta_t': delta_t, 'load': load / 1000 }) for door in building_components.get('doors', []): load = self.heating_calculator.calculate_door_heating_load( door=door, outdoor_temp=outdoor_conditions['design_temperature'], indoor_temp=indoor_conditions['temperature'] ) results['detailed_loads']['doors'].append({ 'name': door.name, 'orientation': door.orientation.value, 'area': door.area, 'u_value': door.u_value, 'delta_t': delta_t, 'load': load / 1000 }) for skylight in building_components.get('skylights', []): load = self.heating_calculator.calculate_skylight_heating_load( skylight=skylight, outdoor_temp=outdoor_conditions['design_temperature'], indoor_temp=indoor_conditions['temperature'], frame_type=skylight.frame_type ) results['detailed_loads']['skylights'].append({ 'name': skylight.name, 'area': skylight.area, 'u_value': skylight.u_value, 'glazing_type': skylight.glazing_type, 'frame_type': skylight.frame_type, 'delta_t': delta_t, 'load': load / 1000 }) if st.session_state.get('debug_mode', False): st.write("Debug: Heating Results", { 'total_load': results.get('total_load', 'N/A'), 'component_loads': results.get('component_loads', 'N/A'), 'detailed_loads': {k: len(v) if isinstance(v, list) else v for k, v in results.get('detailed_loads', {}).items()} }) return True, "Heating calculation completed.", results except ValueError as ve: st.error(f"Input error in heating calculation: {str(ve)}") return False, f"Input error: {str(ve)}", {} except KeyError as ke: st.error(f"Missing data in heating calculation: {str(ke)}") return False, f"Missing data: {str(ke)}", {} except Exception as e: st.error(f"Unexpected error in heating calculation: {str(e)}") return False, f"Unexpected error: {str(e)}", {} def display_calculation_results(self): st.title("Calculation Results") col1, col2 = st.columns(2) with col1: calculate_button = st.button("Calculate Loads") with col2: st.session_state.debug_mode = st.checkbox("Debug Mode", value=st.session_state.get('debug_mode', False)) if calculate_button: # Reset results st.session_state.calculation_results = {'cooling': {}, 'heating': {}} with st.spinner("Calculating loads..."): # Calculate cooling load cooling_success, cooling_message, cooling_results = self.calculate_cooling() if cooling_success: st.session_state.calculation_results['cooling'] = cooling_results st.success(cooling_message) else: st.error(cooling_message) # Calculate heating load heating_success, heating_message, heating_results = self.calculate_heating() if heating_success: st.session_state.calculation_results['heating'] = heating_results st.success(heating_message) else: st.error(heating_message) # Display results self.results_display.display_results(st.session_state) # Navigation col1, col2 = st.columns(2) with col1: st.button( "Back to Internal Loads", on_click=lambda: setattr(st.session_state, "page", "Internal Loads") ) with col2: st.button( "Continue to Export Data", on_click=lambda: setattr(st.session_state, "page", "Export Data") ) if __name__ == "__main__": app = HVACCalculator()