Spaces:
Sleeping
Sleeping
| """ | |
| HVAC Calculator Code Documentation | |
| Updated 2025-04-27: Enhanced climate ID generation, input validation, debug mode, and error handling. | |
| Updated 2025-04-28: Added activity-level-based internal gains, ground temperature validation, ASHRAE 62.1 ventilation rates, negative load prevention, and improved usability. | |
| """ | |
| 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 application modules | |
| from app.building_info_form import BuildingInfoForm | |
| from app.component_selection import ComponentSelectionInterface, Orientation, ComponentType, Wall, Roof, Floor, Window, Door | |
| 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 | |
| # 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 | |
| # 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} | |
| } | |
| 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': [] | |
| } | |
| 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() | |
| # 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.1\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']): | |
| return False, "At least one wall, roof, or window must be defined." | |
| # NEW: Validate climate data using climate_data.py | |
| 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 components | |
| for component_type in ['walls', 'roofs', 'windows', 'doors', 'floors']: | |
| 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}" | |
| # NEW: Validate ground temperature for floors | |
| 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" | |
| # NEW: Validate perimeter | |
| if getattr(comp, 'perimeter', 0) < 0: | |
| return False, f"Perimeter for {comp.name} cannot be negative" | |
| # NEW: 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" | |
| 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." | |
| # Check for duplicates based on key attributes | |
| 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']): | |
| 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 display_internal_loads(self): | |
| st.title("Internal Loads") | |
| # Reset button for all 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"]) # NEW: Added Ventilation tab | |
| 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", | |
| ["Office", "Classroom", "Retail", "Residential"], | |
| help="Select zone type for occupancy characteristics" | |
| ) | |
| hours_in_operation = st.number_input( | |
| "Hours in Operation", | |
| min_value=0.0, | |
| max_value=24.0, | |
| value=8.0, | |
| step=0.5, | |
| help="Daily hours of occupancy" | |
| ) | |
| 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 | |
| } | |
| 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", | |
| ["Office", "Classroom", "Retail", "Residential"], | |
| help="Select zone type for lighting characteristics" | |
| ) | |
| hours_in_operation = st.number_input( | |
| "Hours in Operation", | |
| min_value=0.0, | |
| max_value=24.0, | |
| value=8.0, | |
| step=0.5, | |
| help="Daily hours of lighting operation" | |
| ) | |
| 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", | |
| ["Office", "Classroom", "Retail", "Residential"], | |
| help="Select zone type for equipment characteristics" | |
| ) | |
| hours_in_operation = st.number_input( | |
| "Hours in Operation", | |
| min_value=0.0, | |
| max_value=24.0, | |
| value=8.0, | |
| step=0.5, | |
| help="Daily hours of equipment operation" | |
| ) | |
| 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]: # NEW: Ventilation tab | |
| 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)" | |
| ) | |
| 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)") | |
| 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) | |
| ventilation_rate = ( | |
| (total_people * people_rate + floor_area * area_rate) / 1000 # Convert L/s to m³/s | |
| ) | |
| if ventilation_method == 'Demand-Controlled': | |
| ventilation_rate *= 0.75 # Reduce by 25% for DCV | |
| st.session_state.building_info.update({ | |
| 'zone_type': zone_type, | |
| 'ventilation_method': ventilation_method, | |
| 'ventilation_rate': ventilation_rate | |
| }) | |
| st.success(f"Ventilation settings saved! Total rate: {ventilation_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.", {} | |
| # Format conditions | |
| outdoor_conditions = { | |
| 'temperature': location['summer_design_temp_db'], | |
| 'relative_humidity': location['monthly_humidity'].get('Jul', 50.0), | |
| 'ground_temperature': location['monthly_temps'].get('Jul', 20.0), | |
| 'month': 'Jul', | |
| 'latitude': location['latitude'], # Pass raw latitude value, validation will happen in cooling_load.py | |
| 'wind_speed': building_info.get('wind_speed', 4.0), | |
| 'day_of_year': 204 # Approx. July 23 | |
| } | |
| 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': f"{internal_loads.get('people', [{}])[0].get('hours_in_operation', 8)}:00-{internal_loads.get('people', [{}])[0].get('hours_in_operation', 8)+10}:00" | |
| }, | |
| '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': f"{internal_loads.get('lighting', [{}])[0].get('hours_in_operation', 8)}h" | |
| }, | |
| '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': f"{internal_loads.get('equipment', [{}])[0].get('hours_in_operation', 8)}h" | |
| }, | |
| '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) | |
| }, | |
| '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: | |
| # Calculate total if missing | |
| if 'total_sensible' in summary and 'total_latent' in summary: | |
| summary['total'] = summary['total_sensible'] + summary['total_latent'] | |
| else: | |
| # Fallback to sum of design loads if needed | |
| total_load = sum(value for key, value in design_loads.items() if key != 'design_hour') | |
| summary = { | |
| 'total_sensible': total_load * 0.7, # Approximate sensible ratio | |
| 'total_latent': total_load * 0.3, # Approximate latent ratio | |
| '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, | |
| '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': [], | |
| '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', []): | |
| 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'] | |
| ) | |
| results['detailed_loads']['walls'].append({ | |
| 'name': wall.name, | |
| 'orientation': wall.orientation.value, | |
| 'area': wall.area, | |
| 'u_value': wall.u_value, | |
| 'cltd': self.cooling_calculator.ashrae_tables.calculate_corrected_cltd_wall( | |
| wall_group=wall.wall_group, | |
| orientation=wall.orientation.value, | |
| hour=design_loads['design_hour'], | |
| color='Dark', | |
| month=outdoor_conditions['month'], | |
| latitude=outdoor_conditions['latitude'], | |
| indoor_temp=indoor_conditions['temperature'], | |
| outdoor_temp=outdoor_conditions['temperature'] | |
| ), | |
| 'load': load / 1000 | |
| }) | |
| for roof in building_components.get('roofs', []): | |
| 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'] | |
| ) | |
| results['detailed_loads']['roofs'].append({ | |
| 'name': roof.name, | |
| 'orientation': roof.orientation.value, | |
| 'area': roof.area, | |
| 'u_value': roof.u_value, | |
| 'cltd': self.cooling_calculator.ashrae_tables.calculate_corrected_cltd_roof( | |
| roof_group=roof.roof_group, | |
| hour=design_loads['design_hour'], | |
| color='Dark', | |
| month=outdoor_conditions['month'], | |
| latitude=outdoor_conditions['latitude'], | |
| indoor_temp=indoor_conditions['temperature'], | |
| outdoor_temp=outdoor_conditions['temperature'] | |
| ), | |
| 'load': load / 1000 | |
| }) | |
| for window in building_components.get('windows', []): | |
| 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 | |
| ) | |
| # Ensure load_dict has a 'total' key | |
| 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']) | |
| # Pass latitude directly to get_scl method which has its own validation | |
| results['detailed_loads']['windows'].append({ | |
| 'name': window.name, | |
| 'orientation': window.orientation.value, | |
| 'area': window.area, | |
| 'u_value': window.u_value, | |
| 'shgc': window.shgc, | |
| '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'].title(), | |
| orientation=window.orientation.value, | |
| hour=design_loads['design_hour'] | |
| ), | |
| '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 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'] | |
| ) | |
| # Ensure load_dict has a 'total' key | |
| 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'] | |
| ) | |
| # Ensure load_dict has a 'total' key | |
| 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='A', | |
| hours_occupied='6h', # Using valid '6h' instead of dynamic value that might not exist | |
| 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.", {} | |
| # NEW: Calculate ground temperature from floors or fallback to climate data | |
| 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", {} | |
| # NEW: Skip heating calculation if outdoor temp exceeds indoor temp | |
| indoor_temp = building_info.get('indoor_temp', 21.0) | |
| outdoor_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, | |
| 'infiltration': 0.0, | |
| 'ventilation': 0.0 | |
| }, | |
| 'detailed_loads': { | |
| 'walls': [], | |
| 'roofs': [], | |
| 'floors': [], | |
| 'windows': [], | |
| 'doors': [], | |
| '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': location['winter_design_temp'], | |
| 'design_relative_humidity': 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 | |
| }) | |
| # NEW: 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 | |
| ), | |
| 'operating_hours': f"{internal_loads.get('people', [{}])[0].get('hours_in_operation', 8)}:00-{internal_loads.get('people', [{}])[0].get('hours_in_operation', 8)+10}:00" | |
| }, | |
| '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': f"{internal_loads.get('lighting', [{}])[0].get('hours_in_operation', 8)}h" | |
| }, | |
| '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': f"{internal_loads.get('equipment', [{}])[0].get('hours_in_operation', 8)}h" | |
| }, | |
| '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, | |
| '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': [], | |
| '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, | |
| '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': roof.u_value, | |
| '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'] | |
| ) | |
| results['detailed_loads']['windows'].append({ | |
| 'name': window.name, | |
| 'orientation': window.orientation.value, | |
| 'area': window.area, | |
| 'u_value': window.u_value, | |
| '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 | |
| }) | |
| 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() |