Spaces:
Sleeping
Sleeping
| """ | |
| 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() |