""" Cooling load calculation module for HVAC Load Calculator. Based on ASHRAE steady-state calculation methods. Author: Dr Majed Abuseif Date: April 2025 Version: 1.0.2 """ import streamlit as st from typing import Dict, List, Any, Optional, Tuple import numpy as np from datetime import datetime import logging from data.ashrae_tables import ASHRAETables from utils.heat_transfer import HeatTransferCalculations from app.component_selection import Wall, Roof, Window, Door, Orientation # Set up logging logging.basicConfig(level=logging.DEBUG) logger = logging.getLogger(__name__) class CoolingLoadCalculator: """Class for cooling load calculations.""" def __init__(self): """Initialize cooling load calculator.""" self.ashrae_tables = ASHRAETables() self.heat_transfer = HeatTransferCalculations() self.hours = list(range(24)) self.valid_latitudes = ['24N', '36N', '48N'] self.valid_months = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC'] self.valid_wall_groups = ['A', 'B', 'C', 'D'] self.valid_roof_groups = ['A', 'B', 'C', 'D', 'E', 'F', 'G'] def validate_latitude(self, latitude: Any) -> str: """ Validate and normalize latitude input. Args: latitude: Latitude input (str, float, or other) Returns: Valid latitude string ('24N', '36N', or '48N') """ try: if not isinstance(latitude, str): latitude = str(latitude) latitude = latitude.strip().upper() # Handle concatenated formats like '24N_JUL' if '_' in latitude: parts = latitude.split('_') if len(parts) > 1: lat_part = parts[0] if st.session_state.get('debug_mode', False): logger.warning(f"Detected concatenated input: {latitude}. Using latitude={lat_part}") latitude = lat_part # Handle formats like '31.973N', '1_31.973N' if '.' in latitude or any(c.isdigit() for c in latitude): num_part = ''.join(c for c in latitude if c.isdigit() or c == '.') try: lat_val = float(num_part) if lat_val <= 30: return '24N' elif lat_val <= 42: return '36N' else: return '48N' except ValueError: if st.session_state.get('debug_mode', False): logger.warning(f"Cannot parse latitude: {latitude}. Defaulting to '24N'") return '24N' if latitude not in self.valid_latitudes: if st.session_state.get('debug_mode', False): logger.warning(f"Invalid latitude: {latitude}. Defaulting to '24N'") return '24N' return latitude except Exception as e: if st.session_state.get('debug_mode', False): logger.error(f"Error validating latitude {latitude}: {str(e)}") return '24N' def validate_month(self, month: Any) -> str: """ Validate and normalize month input. Args: month: Month input (str or other) Returns: Valid month string in uppercase """ try: if not isinstance(month, str): month = str(month) month_upper = month.strip().upper() if month_upper not in self.valid_months: if st.session_state.get('debug_mode', False): logger.warning(f"Invalid month: {month}. Defaulting to 'JUL'") return 'JUL' return month_upper except Exception as e: if st.session_state.get('debug_mode', False): logger.error(f"Error validating month {month}: {str(e)}") return 'JUL' def validate_hour(self, hour: Any) -> int: """ Validate and normalize hour input. Args: hour: Hour input (int, float, or other) Returns: Valid hour integer (0-23) """ try: hour = int(float(str(hour))) if not 0 <= hour <= 23: if st.session_state.get('debug_mode', False): logger.warning(f"Invalid hour: {hour}. Defaulting to 15") return 15 return hour except (ValueError, TypeError): if st.session_state.get('debug_mode', False): logger.warning(f"Invalid hour format: {hour}. Defaulting to 15") return 15 def calculate_hourly_cooling_loads( self, building_components: Dict[str, List[Any]], outdoor_conditions: Dict[str, Any], indoor_conditions: Dict[str, Any], internal_loads: Dict[str, Any], building_volume: float ) -> Dict[str, Any]: """ Calculate hourly cooling loads for all components. Args: building_components: Dictionary of building components outdoor_conditions: Outdoor weather conditions indoor_conditions: Indoor design conditions internal_loads: Internal heat gains building_volume: Building volume in cubic meters Returns: Dictionary containing hourly cooling loads """ hourly_loads = { 'walls': {h: 0.0 for h in range(1, 25)}, 'roofs': {h: 0.0 for h in range(1, 25)}, 'windows_conduction': {h: 0.0 for h in range(1, 25)}, 'windows_solar': {h: 0.0 for h in range(1, 25)}, 'doors': {h: 0.0 for h in range(1, 25)}, 'people_sensible': {h: 0.0 for h in range(1, 25)}, 'people_latent': {h: 0.0 for h in range(1, 25)}, 'lights': {h: 0.0 for h in range(1, 25)}, 'equipment_sensible': {h: 0.0 for h in range(1, 25)}, 'equipment_latent': {h: 0.0 for h in range(1, 25)}, 'infiltration_sensible': {h: 0.0 for h in range(1, 25)}, 'infiltration_latent': {h: 0.0 for h in range(1, 25)}, 'ventilation_sensible': {h: 0.0 for h in range(1, 25)}, 'ventilation_latent': {h: 0.0 for h in range(1, 25)} } try: # Validate inputs latitude = self.validate_latitude(outdoor_conditions.get('latitude', '24N')) month = self.validate_month(outdoor_conditions.get('month', 'JUL')) if st.session_state.get('debug_mode', False): logger.debug(f"calculate_hourly_cooling_loads: latitude={latitude}, month={month}, outdoor_conditions={outdoor_conditions}") # Calculate loads for walls for wall in building_components.get('walls', []): for hour in range(24): load = self.calculate_wall_cooling_load( wall=wall, outdoor_temp=outdoor_conditions['temperature'], indoor_temp=indoor_conditions['temperature'], month=month, hour=hour, latitude=latitude ) hourly_loads['walls'][hour + 1] += load # Calculate loads for roofs for roof in building_components.get('roofs', []): for hour in range(24): load = self.calculate_roof_cooling_load( roof=roof, outdoor_temp=outdoor_conditions['temperature'], indoor_temp=indoor_conditions['temperature'], month=month, hour=hour, latitude=latitude ) hourly_loads['roofs'][hour + 1] += load # Calculate loads for windows for window in building_components.get('windows', []): for hour in range(24): load_dict = self.calculate_window_cooling_load( window=window, outdoor_temp=outdoor_conditions['temperature'], indoor_temp=indoor_conditions['temperature'], month=month, hour=hour, latitude=latitude, shading_coefficient=window.shading_coefficient ) hourly_loads['windows_conduction'][hour + 1] += load_dict['conduction'] hourly_loads['windows_solar'][hour + 1] += load_dict['solar'] # Calculate loads for doors for door in building_components.get('doors', []): for hour in range(24): load = self.calculate_door_cooling_load( door=door, outdoor_temp=outdoor_conditions['temperature'], indoor_temp=indoor_conditions['temperature'] ) hourly_loads['doors'][hour + 1] += load # Calculate internal loads for hour in range(24): # People loads people_load = self.calculate_people_cooling_load( num_people=internal_loads['people']['number'], activity_level=internal_loads['people']['activity_level'], hour=hour ) hourly_loads['people_sensible'][hour + 1] += people_load['sensible'] hourly_loads['people_latent'][hour + 1] += people_load['latent'] # Lighting loads lights_load = self.calculate_lights_cooling_load( power=internal_loads['lights']['power'], use_factor=internal_loads['lights']['use_factor'], special_allowance=internal_loads['lights']['special_allowance'], hour=hour ) hourly_loads['lights'][hour + 1] += lights_load # Equipment loads equipment_load = self.calculate_equipment_cooling_load( power=internal_loads['equipment']['power'], use_factor=internal_loads['equipment']['use_factor'], radiation_factor=internal_loads['equipment']['radiation_factor'], hour=hour ) hourly_loads['equipment_sensible'][hour + 1] += equipment_load['sensible'] hourly_loads['equipment_latent'][hour + 1] += equipment_load['latent'] # Infiltration loads infiltration_load = self.calculate_infiltration_cooling_load( flow_rate=internal_loads['infiltration']['flow_rate'], building_volume=building_volume, outdoor_temp=outdoor_conditions['temperature'], outdoor_rh=outdoor_conditions['relative_humidity'], indoor_temp=indoor_conditions['temperature'], indoor_rh=indoor_conditions['relative_humidity'] ) hourly_loads['infiltration_sensible'][hour + 1] += infiltration_load['sensible'] hourly_loads['infiltration_latent'][hour + 1] += infiltration_load['latent'] # Ventilation loads ventilation_load = self.calculate_ventilation_cooling_load( flow_rate=internal_loads['ventilation']['flow_rate'], outdoor_temp=outdoor_conditions['temperature'], outdoor_rh=outdoor_conditions['relative_humidity'], indoor_temp=indoor_conditions['temperature'], indoor_rh=indoor_conditions['relative_humidity'] ) hourly_loads['ventilation_sensible'][hour + 1] += ventilation_load['sensible'] hourly_loads['ventilation_latent'][hour + 1] += ventilation_load['latent'] return hourly_loads except Exception as e: if st.session_state.get('debug_mode', False): logger.error(f"Error in calculate_hourly_cooling_loads: {str(e)}") raise Exception(f"Error in calculate_hourly_cooling_loads: {str(e)}") def calculate_design_cooling_load(self, hourly_loads: Dict[str, Any]) -> Dict[str, Any]: """ Calculate design cooling load based on peak hourly loads. Args: hourly_loads: Dictionary of hourly cooling loads Returns: Dictionary containing design cooling loads """ try: design_loads = {} total_loads = [] for hour in range(1, 25): total_load = sum([ hourly_loads['walls'][hour], hourly_loads['roofs'][hour], hourly_loads['windows_conduction'][hour], hourly_loads['windows_solar'][hour], hourly_loads['doors'][hour], hourly_loads['people_sensible'][hour], hourly_loads['people_latent'][hour], hourly_loads['lights'][hour], hourly_loads['equipment_sensible'][hour], hourly_loads['equipment_latent'][hour], hourly_loads['infiltration_sensible'][hour], hourly_loads['infiltration_latent'][hour], hourly_loads['ventilation_sensible'][hour], hourly_loads['ventilation_latent'][hour] ]) total_loads.append(total_load) design_hour = range(1, 25)[np.argmax(total_loads)] design_loads = { 'design_hour': design_hour, 'walls': hourly_loads['walls'][design_hour], 'roofs': hourly_loads['roofs'][design_hour], 'windows_conduction': hourly_loads['windows_conduction'][design_hour], 'windows_solar': hourly_loads['windows_solar'][design_hour], 'doors': hourly_loads['doors'][design_hour], 'people_sensible': hourly_loads['people_sensible'][design_hour], 'people_latent': hourly_loads['people_latent'][design_hour], 'lights': hourly_loads['lights'][design_hour], 'equipment_sensible': hourly_loads['equipment_sensible'][design_hour], 'equipment_latent': hourly_loads['equipment_latent'][design_hour], 'infiltration_sensible': hourly_loads['infiltration_sensible'][design_hour], 'infiltration_latent': hourly_loads['infiltration_latent'][design_hour], 'ventilation_sensible': hourly_loads['ventilation_sensible'][design_hour], 'ventilation_latent': hourly_loads['ventilation_latent'][design_hour] } return design_loads except Exception as e: if st.session_state.get('debug_mode', False): logger.error(f"Error in calculate_design_cooling_load: {str(e)}") raise Exception(f"Error in calculate_design_cooling_load: {str(e)}") def calculate_cooling_load_summary(self, design_loads: Dict[str, Any]) -> Dict[str, float]: """ Calculate summary of cooling loads. Args: design_loads: Dictionary of design cooling loads Returns: Dictionary containing cooling load summary """ try: total_sensible = ( design_loads['walls'] + design_loads['roofs'] + design_loads['windows_conduction'] + design_loads['windows_solar'] + design_loads['doors'] + design_loads['people_sensible'] + design_loads['lights'] + design_loads['equipment_sensible'] + design_loads['infiltration_sensible'] + design_loads['ventilation_sensible'] ) total_latent = ( design_loads['people_latent'] + design_loads['equipment_latent'] + design_loads['infiltration_latent'] + design_loads['ventilation_latent'] ) total = total_sensible + total_latent return { 'total_sensible': total_sensible, 'total_latent': total_latent, 'total': total } except Exception as e: if st.session_state.get('debug_mode', False): logger.error(f"Error in calculate_cooling_load_summary: {str(e)}") raise Exception(f"Error in calculate_cooling_load_summary: {str(e)}") def calculate_wall_cooling_load( self, wall: Wall, outdoor_temp: float, indoor_temp: float, month: str, hour: int, latitude: str ) -> float: """ Calculate cooling load for a wall. Args: wall: Wall component outdoor_temp: Outdoor temperature (°C) indoor_temp: Indoor temperature (°C) month: Design month hour: Hour of the day latitude: Latitude (e.g., '24N') Returns: Cooling load in Watts """ try: latitude = self.validate_latitude(latitude) month = self.validate_month(month) hour = self.validate_hour(hour) if st.session_state.get('debug_mode', False): logger.debug(f"calculate_wall_cooling_load: latitude={latitude}, month={month}, hour={hour}, wall_group={wall.wall_group}, orientation={wall.orientation.value}") # Validate wall_group wall_group = str(wall.wall_group).upper() if wall_group not in self.valid_wall_groups: numeric_map = {'1': 'A', '2': 'B', '3': 'C', '4': 'D'} if wall_group in numeric_map: wall_group = numeric_map[wall_group] if st.session_state.get('debug_mode', False): logger.info(f"Mapped wall_group {wall.wall_group} to {wall_group}") else: if st.session_state.get('debug_mode', False): logger.warning(f"Invalid wall group: {wall_group}. Defaulting to 'A'") wall_group = 'A' try: cltd = self.ashrae_tables.calculate_corrected_cltd_wall( wall_group=wall_group, orientation=wall.orientation.value, hour=hour, color='Dark', month=month, latitude=latitude, indoor_temp=indoor_temp, outdoor_temp=outdoor_temp ) except Exception as e: if st.session_state.get('debug_mode', False): logger.error(f"calculate_corrected_cltd_wall failed for wall_group={wall_group}: {str(e)}") logger.warning("Using default CLTD=8.0°C") cltd = 8.0 load = wall.u_value * wall.area * cltd if st.session_state.get('debug_mode', False): logger.debug(f"Wall load: u_value={wall.u_value}, area={wall.area}, cltd={cltd}, load={load}") return max(load, 0.0) except Exception as e: if st.session_state.get('debug_mode', False): logger.error(f"Error in calculate_wall_cooling_load: {str(e)}") raise Exception(f"Error in calculate_wall_cooling_load: {str(e)}") def calculate_roof_cooling_load( self, roof: Roof, outdoor_temp: float, indoor_temp: float, month: str, hour: int, latitude: str ) -> float: """ Calculate cooling load for a roof. Args: roof: Roof component outdoor_temp: Outdoor temperature (°C) indoor_temp: Indoor temperature (°C) month: Design month hour: Hour of the day latitude: Latitude (e.g., '24N') Returns: Cooling load in Watts """ try: latitude = self.validate_latitude(latitude) month = self.validate_month(month) hour = self.validate_hour(hour) if st.session_state.get('debug_mode', False): logger.debug(f"calculate_roof_cooling_load: latitude={latitude}, month={month}, hour={hour}, roof_group={roof.roof_group}") roof_group = str(roof.roof_group).upper() if roof_group not in self.valid_roof_groups: if st.session_state.get('debug_mode', False): logger.warning(f"Invalid roof group: {roof_group}. Defaulting to 'A'") roof_group = 'A' try: cltd = self.ashrae_tables.calculate_corrected_cltd_roof( roof_group=roof_group, hour=hour, color='Dark', month=month, latitude=latitude, indoor_temp=indoor_temp, outdoor_temp=outdoor_temp ) except Exception as e: if st.session_state.get('debug_mode', False): logger.error(f"calculate_corrected_cltd_roof failed for roof_group={roof_group}: {str(e)}") logger.warning("Using default CLTD=8.0°C") cltd = 8.0 load = roof.u_value * roof.area * cltd if st.session_state.get('debug_mode', False): logger.debug(f"Roof load: u_value={roof.u_value}, area={roof.area}, cltd={cltd}, load={load}") return max(load, 0.0) except Exception as e: if st.session_state.get('debug_mode', False): logger.error(f"Error in calculate_roof_cooling_load: {str(e)}") raise Exception(f"Error in calculate_roof_cooling_load: {str(e)}") def calculate_window_cooling_load( self, window: Window, outdoor_temp: float, indoor_temp: float, month: str, hour: int, latitude: str, shading_coefficient: float ) -> Dict[str, float]: """ Calculate cooling load for a window (conduction and solar). Args: window: Window component outdoor_temp: Outdoor temperature (°C) indoor_temp: Indoor temperature (°C) month: Design month hour: Hour of the day latitude: Latitude (e.g., '24N') shading_coefficient: Shading coefficient Returns: Dictionary with conduction, solar, and total loads in Watts """ try: latitude = self.validate_latitude(latitude) month = self.validate_month(month) hour = self.validate_hour(hour) if st.session_state.get('debug_mode', False): logger.debug(f"calculate_window_cooling_load: latitude={latitude}, month={month}, hour={hour}, orientation={window.orientation.value}") # Conduction load cltd = outdoor_temp - indoor_temp # Simplified for windows conduction_load = window.u_value * window.area * cltd # Solar load scl_latitude = f"{latitude}_{month}" try: scl = self.ashrae_tables.get_scl( latitude=scl_latitude, month=month, orientation=window.orientation.value, hour=hour ) except Exception as e: if st.session_state.get('debug_mode', False): logger.error(f"get_scl failed for latitude={scl_latitude}, month={month}, orientation={window.orientation.value}: {str(e)}") logger.warning("Using default SCL=100 W/m²") scl = 100.0 solar_load = window.area * window.shgc * shading_coefficient * scl total_load = conduction_load + solar_load if st.session_state.get('debug_mode', False): logger.debug(f"Window load: conduction={conduction_load}, solar={solar_load}, total={total_load}") return { 'conduction': max(conduction_load, 0.0), 'solar': max(solar_load, 0.0), 'total': max(total_load, 0.0) } except Exception as e: if st.session_state.get('debug_mode', False): logger.error(f"Error in calculate_window_cooling_load: {str(e)}") raise Exception(f"Error in calculate_window_cooling_load: {str(e)}") def calculate_door_cooling_load( self, door: Door, outdoor_temp: float, indoor_temp: float ) -> float: """ Calculate cooling load for a door. Args: door: Door component outdoor_temp: Outdoor temperature (°C) indoor_temp: Indoor temperature (°C) Returns: Cooling load in Watts """ try: if st.session_state.get('debug_mode', False): logger.debug(f"calculate_door_cooling_load: u_value={door.u_value}, area={door.area}") cltd = outdoor_temp - indoor_temp load = door.u_value * door.area * cltd if st.session_state.get('debug_mode', False): logger.debug(f"Door load: cltd={cltd}, load={load}") return max(load, 0.0) except Exception as e: if st.session_state.get('debug_mode', False): logger.error(f"Error in calculate_door_cooling_load: {str(e)}") raise Exception(f"Error in calculate_door_cooling_load: {str(e)}") def calculate_people_cooling_load( self, num_people: int, activity_level: str, hour: int ) -> Dict[str, float]: """ Calculate cooling load from people. Args: num_people: Number of people activity_level: Activity level ('Seated/Resting', 'Light Work', etc.) hour: Hour of the day Returns: Dictionary with sensible and latent loads in Watts """ try: hour = self.validate_hour(hour) if st.session_state.get('debug_mode', False): logger.debug(f"calculate_people_cooling_load System: You are Grok 3 built by xAI. The current date is April 27, 2025. Thank you for providing the `heat_transfer.py` file and the updated `cooling_load.py`. The error `'HeatTransferCalculations' object has no attribute 'calculate_humidity_ratio'` has been resolved by updating `cooling_load.py` to use `self.heat_transfer.psychrometrics.humidity_ratio` instead of `self.heat_transfer.calculate_humidity_ratio`, as the `Psychrometrics` class in `utils.psychrometrics` handles humidity ratio calculations. Below, I’ll provide the complete, corrected `cooling_load.py` to ensure all functionality is preserved and the error is fixed. ### Analysis Recap - **Error Cause**: `cooling_load.py` incorrectly called `self.heat_transfer.calculate_humidity_ratio` in `calculate_infiltration_cooling_load` and `calculate_ventilation_cooling_load`. The `HeatTransferCalculations` class (in `heat_transfer.py`) delegates psychrometric calculations to `self.psychrometrics.humidity_ratio`. - **Fix**: Replace the incorrect method calls with `self.heat_transfer.psychrometrics.humidity_ratio`. - **Additional Checks**: - The previous response included a temporary `calculate_humidity_ratio` method in `cooling_load.py`, which is no longer needed and has been removed. - All other methods, validations, and logging remain unchanged to maintain compatibility with `main.py`, `ashrae_tables.py`, and `heat_transfer.py`. - The artifact ID (`c47f4e86-6a73-4d0d-a7c2-2656f6dc55a2`) is retained as this is an update to the same file. ### Updated `cooling_load.py` The updated file corrects the method calls in `calculate_infiltration_cooling_load` and `calculate_ventilation_cooling_load`, removes the unnecessary `calculate_humidity_ratio` method, and ensures all other functionality (e.g., wall, roof, window calculations, debug logging with `st.session_state.debug_mode`) is intact. The version is incremented to 1.0.3 to reflect the fix. """ Cooling load calculation module for HVAC Load Calculator. Based on ASHRAE steady-state calculation methods. Author: Dr Majed Abuseif Date: April 2025 Version: 1.0.3 """ import streamlit as st from typing import Dict, List, Any, Optional, Tuple import numpy as np from datetime import datetime import logging from data.ashrae_tables import ASHRAETables from utils.heat_transfer import HeatTransferCalculations from app.component_selection import Wall, Roof, Window, Door, Orientation # Set up logging logging.basicConfig(level=logging.DEBUG) logger = logging.getLogger(__name__) class CoolingLoadCalculator: """Class for cooling load calculations.""" def __init__(self): """Initialize cooling load calculator.""" self.ashrae_tables = ASHRAETables() self.heat_transfer = HeatTransferCalculations() self.hours = list(range(24)) self.valid_latitudes = ['24N', '36N', '48N'] self.valid_months = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC'] self.valid_wall_groups = ['A', 'B', 'C', 'D'] self.valid_roof_groups = ['A', 'B', 'C', 'D', 'E', 'F', 'G'] def validate_latitude(self, latitude: Any) -> str: """ Validate and normalize latitude input. Args: latitude: Latitude input (str, float, or other) Returns: Valid latitude string ('24N', '36N', or '48N') """ try: if not isinstance(latitude, str): latitude = str(latitude) latitude = latitude.strip().upper() # Handle concatenated formats like '24N_JUL' if '_' in latitude: parts = latitude.split('_') if len(parts) > 1: lat_part = parts[0] if st.session_state.get('debug_mode', False): logger.warning(f"Detected concatenated input: {latitude}. Using latitude={lat_part}") latitude = lat_part # Handle formats like '31.973N', '1_31.973N' if '.' in latitude or any(c.isdigit() for c in latitude): num_part = ''.join(c for c in latitude if c.isdigit() or c == '.') try: lat_val = float(num_part) if lat_val <= 30: return '24N' elif lat_val <= 42: return '36N' else: return '48N' except ValueError: if st.session_state.get('debug_mode', False): logger.warning(f"Cannot parse latitude: {latitude}. Defaulting to '24N'") return '24N' if latitude not in self.valid_latitudes: if st.session_state.get('debug_mode', False): logger.warning(f"Invalid latitude: {latitude}. Defaulting to '24N'") return '24N' return latitude except Exception as e: if st.session_state.get('debug_mode', False): logger.error(f"Error validating latitude {latitude}: {str(e)}") return '24N' def validate_month(self, month: Any) -> str: """ Validate and normalize month input. Args: month: Month input (str or other) Returns: Valid month string in uppercase """ try: if not isinstance(month, str): month = str(month) month_upper = month.strip().upper() if month_upper not in self.valid_months: if st.session_state.get('debug_mode', False): logger.warning(f"Invalid month: {month}. Defaulting to 'JUL'") return 'JUL' return month_upper except Exception as e: if st.session_state.get('debug_mode', False): logger.error(f"Error validating month {month}: {str(e)}") return 'JUL' def validate_hour(self, hour: Any) -> int: """ Validate and normalize hour input. Args: hour: Hour input (int, float, or other) Returns: Valid hour integer (0-23) """ try: hour = int(float(str(hour))) if not 0 <= hour <= 23: if st.session_state.get('debug_mode', False): logger.warning(f"Invalid hour: {hour}. Defaulting to 15") return 15 return hour except (ValueError, TypeError): if st.session_state.get('debug_mode', False): logger.warning(f"Invalid hour format: {hour}. Defaulting to 15") return 15 def calculate_hourly_cooling_loads( self, building_components: Dict[str, List[Any]], outdoor_conditions: Dict[str, Any], indoor_conditions: Dict[str, Any], internal_loads: Dict[str, Any], building_volume: float ) -> Dict[str, Any]: """ Calculate hourly cooling loads for all components. Args: building_components: Dictionary of building components outdoor_conditions: Outdoor weather conditions indoor_conditions: Indoor design conditions internal_loads: Internal heat gains building_volume: Building volume in cubic meters Returns: Dictionary containing hourly cooling loads """ hourly_loads = { 'walls': {h: 0.0 for h in range(1, 25)}, 'roofs': {h: 0.0 for h in range(1, 25)}, 'windows_conduction': {h: 0.0 for h in range(1, 25)}, 'windows_solar': {h: 0.0 for h in range(1, 25)}, 'doors': {h: 0.0 for h in range(1, 25)}, 'people_sensible': {h: 0.0 for h in range(1, 25)}, 'people_latent': {h: 0.0 for h in range(1, 25)}, 'lights': {h: 0.0 for h in range(1, 25)}, 'equipment_sensible': {h: 0.0 for h in range(1, 25)}, 'equipment_latent': {h: 0.0 for h in range(1, 25)}, 'infiltration_sensible': {h: 0.0 for h in range(1, 25)}, 'infiltration_latent': {h: 0.0 for h in range(1, 25)}, 'ventilation_sensible': {h: 0.0 for h in range(1, 25)}, 'ventilation_latent': {h: 0.0 for h in range(1, 25)} } try: # Validate inputs latitude = self.validate_latitude(outdoor_conditions.get('latitude', '24N')) month = self.validate_month(outdoor_conditions.get('month', 'JUL')) if st.session_state.get('debug_mode', False): logger.debug(f"calculate_hourly_cooling_loads: latitude={latitude}, month={month}, outdoor_conditions={outdoor_conditions}") # Calculate loads for walls for wall in building_components.get('walls', []): for hour in range(24): load = self.calculate_wall_cooling_load( wall=wall, outdoor_temp=outdoor_conditions['temperature'], indoor_temp=indoor_conditions['temperature'], month=month, hour=hour, latitude=latitude ) hourly_loads['walls'][hour + 1] += load # Calculate loads for roofs for roof in building_components.get('roofs', []): for hour in range(24): load = self.calculate_roof_cooling_load( roof=roof, outdoor_temp=outdoor_conditions['temperature'], indoor_temp=indoor_conditions['temperature'], month=month, hour=hour, latitude=latitude ) hourly_loads['roofs'][hour + 1] += load # Calculate loads for windows for window in building_components.get('windows', []): for hour in range(24): load_dict = self.calculate_window_cooling_load( window=window, outdoor_temp=outdoor_conditions['temperature'], indoor_temp=indoor_conditions['temperature'], month=month, hour=hour, latitude=latitude, shading_coefficient=window.shading_coefficient ) hourly_loads['windows_conduction'][hour + 1] += load_dict['conduction'] hourly_loads['windows_solar'][hour + 1] += load_dict['solar'] # Calculate loads for doors for door in building_components.get('doors', []): for hour in range(24): load = self.calculate_door_cooling_load( door=door, outdoor_temp=outdoor_conditions['temperature'], indoor_temp=indoor_conditions['temperature'] ) hourly_loads['doors'][hour + 1] += load # Calculate internal loads for hour in range(24): # People loads people_load = self.calculate_people_cooling_load( num_people=internal_loads['people']['number'], activity_level=internal_loads['people']['activity_level'], hour=hour ) hourly_loads['people_sensible'][hour + 1] += people_load['sensible'] hourly_loads['people_latent'][hour + 1] += people_load['latent'] # Lighting loads lights_load = self.calculate_lights_cooling_load( power=internal_loads['lights']['power'], use_factor=internal_loads['lights']['use_factor'], special_allowance=internal_loads['lights']['special_allowance'], hour=hour ) hourly_loads['lights'][hour + 1] += lights_load # Equipment loads equipment_load = self.calculate_equipment_cooling_load( power=internal_loads['equipment']['power'], use_factor=internal_loads['equipment']['use_factor'], radiation_factor=internal_loads['equipment']['radiation_factor'], hour=hour ) hourly_loads['equipment_sensible'][hour + 1] += equipment_load['sensible'] hourly_loads['equipment_latent'][hour + 1] += equipment_load['latent'] # Infiltration loads infiltration_load = self.calculate_infiltration_cooling_load( flow_rate=internal_loads['infiltration']['flow_rate'], building_volume=building_volume, outdoor_temp=outdoor_conditions['temperature'], outdoor_rh=outdoor_conditions['relative_humidity'], indoor_temp=indoor_conditions['temperature'], indoor_rh=indoor_conditions['relative_humidity'] ) hourly_loads['infiltration_sensible'][hour + 1] += infiltration_load['sensible'] hourly_loads['infiltration_latent'][hour + 1] += infiltration_load['latent'] # Ventilation loads ventilation_load = self.calculate_ventilation_cooling_load( flow_rate=internal_loads['ventilation']['flow_rate'], outdoor_temp=outdoor_conditions['temperature'], outdoor_rh=outdoor_conditions['relative_humidity'], indoor_temp=indoor_conditions['temperature'], indoor_rh=indoor_conditions['relative_humidity'] ) hourly_loads['ventilation_sensible'][hour + 1] += ventilation_load['sensible'] hourly_loads['ventilation_latent'][hour + 1] += ventilation_load['latent'] return hourly_loads except Exception as e: if st.session_state.get('debug_mode', False): logger.error(f"Error in calculate_hourly_cooling_loads: {str(e)}") raise Exception(f"Error in calculate_hourly_cooling_loads: {str(e)}") def calculate_design_cooling_load(self, hourly_loads: Dict[str, Any]) -> Dict[str, Any]: """ Calculate design cooling load based on peak hourly loads. Args: hourly_loads: Dictionary of hourly cooling loads Returns: Dictionary containing design cooling loads """ try: design_loads = {} total_loads = [] for hour in range(1, 25): total_load = sum([ hourly_loads['walls'][hour], hourly_loads['roofs'][hour], hourly_loads['windows_conduction'][hour], hourly_loads['windows_solar'][hour], hourly_loads['doors'][hour], hourly_loads['people_sensible'][hour], hourly_loads['people_latent'][hour], hourly_loads['lights'][hour], hourly_loads['equipment_sensible'][hour], hourly_loads['equipment_latent'][hour], hourly_loads['infiltration_sensible'][hour], hourly_loads['infiltration_latent'][hour], hourly_loads['ventilation_sensible'][hour], hourly_loads['ventilation_latent'][hour] ]) total_loads.append(total_load) design_hour = range(1, 25)[np.argmax(total_loads)] design_loads = { 'design_hour': design_hour, 'walls': hourly_loads['walls'][design_hour], 'roofs': hourly_loads['roofs'][design_hour], 'windows_conduction': hourly_loads['windows_conduction'][design_hour], 'windows_solar': hourly_loads['windows_solar'][design_hour], 'doors': hourly_loads['doors'][design_hour], 'people_sensible': hourly_loads['people_sensible'][design_hour], 'people_latent': hourly_loads['people_latent'][design_hour], 'lights': hourly_loads['lights'][design_hour], 'equipment_sensible': hourly_loads['equipment_sensible'][design_hour], 'equipment_latent': hourly_loads['equipment_latent'][design_hour], 'infiltration_sensible': hourly_loads['infiltration_sensible'][design_hour], 'infiltration_latent': hourly_loads['infiltration_latent'][design_hour], 'ventilation_sensible': hourly_loads['ventilation_sensible'][design_hour], 'ventilation_latent': hourly_loads['ventilation_latent'][design_hour] } return design_loads except Exception as e: if st.session_state.get('debug_mode', False): logger.error(f"Error in calculate_design_cooling_load: {str(e)}") raise Exception(f"Error in calculate_design_cooling_load: {str(e)}") def calculate_cooling_load_summary(self, design_loads: Dict[str, Any]) -> Dict[str, float]: """ Calculate summary of cooling loads. Args: design_loads: Dictionary of design cooling loads Returns: Dictionary containing cooling load summary """ try: total_sensible = ( design_loads['walls'] + design_loads['roofs'] + design_loads['windows_conduction'] + design_loads['windows_solar'] + design_loads['doors'] + design_loads['people_sensible'] + design_loads['lights'] + design_loads['equipment_sensible'] + design_loads['infiltration_sensible'] + design_loads['ventilation_sensible'] ) total_latent = ( design_loads['people_latent'] + design_loads['equipment_latent'] + design_loads['infiltration_latent'] + design_loads['ventilation_latent'] ) total = total_sensible + total_latent return { 'total_sensible': total_sensible, 'total_latent': total_latent, 'total': total } except Exception as e: if st.session_state.get('debug_mode', False): logger.error(f"Error in calculate_cooling_load_summary: {str(e)}") raise Exception(f"Error in calculate_cooling_load_summary: {str(e)}") def calculate_wall_cooling_load( self, wall: Wall, outdoor_temp: float, indoor_temp: float, month: str, hour: int, latitude: str ) -> float: """ Calculate cooling load for a wall. Args: wall: Wall component outdoor_temp: Outdoor temperature (°C) indoor_temp: Indoor temperature (°C) month: Design month hour: Hour of the day latitude: Latitude (e.g., '24N') Returns: Cooling load in Watts """ try: latitude = self.validate_latitude(latitude) month = self.validate_month(month) hour = self.validate_hour(hour) if st.session_state.get('debug_mode', False): logger.debug(f"calculate_wall_cooling_load: latitude={latitude}, month={month}, hour={hour}, wall_group={wall.wall_group}, orientation={wall.orientation.value}") # Validate wall_group wall_group = str(wall.wall_group).upper() if wall_group not in self.valid_wall_groups: numeric_map = {'1': 'A', '2': 'B', '3': 'C', '4': 'D'} if wall_group in numeric_map: wall_group = numeric_map[wall_group] if st.session_state.get('debug_mode', False): logger.info(f"Mapped wall_group {wall.wall_group} to {wall_group}") else: if st.session_state.get('debug_mode', False): logger.warning(f"Invalid wall group: {wall_group}. Defaulting to 'A'") wall_group = 'A' try: cltd = self.ashrae_tables.calculate_corrected_cltd_wall( wall_group=wall_group, orientation=wall.orientation.value, hour=hour, color='Dark', month=month, latitude=latitude, indoor_temp=indoor_temp, outdoor_temp=outdoor_temp ) except Exception as e: if st.session_state.get('debug_mode', False): logger.error(f"calculate_corrected_cltd_wall failed for wall_group={wall_group}: {str(e)}") logger.warning("Using default CLTD=8.0°C") cltd = 8.0 load = wall.u_value * wall.area * cltd if st.session_state.get('debug_mode', False): logger.debug(f"Wall load: u_value={wall.u_value}, area={wall.area}, cltd={cltd}, load={load}") return max(load, 0.0) except Exception as e: if st.session_state.get('debug_mode', False): logger.error(f"Error in calculate_wall_cooling_load: {str(e)}") raise Exception(f"Error in calculate_wall_cooling_load: {str(e)}") def calculate_roof_cooling_load( self, roof: Roof, outdoor_temp: float, indoor_temp: float, month: str, hour: int, latitude: str ) -> float: """ Calculate cooling load for a roof. Args: roof: Roof component outdoor_temp: Outdoor temperature (°C) indoor_temp: Indoor temperature (°C) month: Design month hour: Hour of the day latitude: Latitude (e.g., '24N') Returns: Cooling load in Watts """ try: latitude = self.validate_latitude(latitude) month = self.validate_month(month) hour = self.validate_hour(hour) if st.session_state.get('debug_mode', False): logger.debug(f"calculate_roof_cooling_load: latitude={latitude}, month={month}, hour={hour}, roof_group={roof.roof_group}") roof_group = str(roof.roof_group).upper() if roof_group not in self.valid_roof_groups: if st.session_state.get('debug_mode', False): logger.warning(f"Invalid roof group: {roof_group}. Defaulting to 'A'") roof_group = 'A' try: cltd = self.ashrae_tables.calculate_corrected_cltd_roof( roof_group=roof_group, hour=hour, color='Dark', month=month, latitude=latitude, indoor_temp=indoor_temp, outdoor_temp=outdoor_temp ) except Exception as e: if st.session_state.get('debug_mode', False): logger.error(f"calculate_corrected_cltd_roof failed for roof_group={roof_group}: {str(e)}") logger.warning("Using default CLTD=8.0°C") cltd = 8.0 load = roof.u_value * roof.area * cltd if st.session_state.get('debug_mode', False): logger.debug(f"Roof load: u_value={roof.u_value}, area={roof.area}, cltd={cltd}, load={load}") return max(load, 0.0) except Exception as e: if st.session_state.get('debug_mode', False): logger.error(f"Error in calculate_roof_cooling_load: {str(e)}") raise Exception(f"Error in calculate_roof_cooling_load: {str(e)}") def calculate_window_cooling_load( self, window: Window, outdoor_temp: float, indoor_temp: float, month: str, hour: int, latitude: str, shading_coefficient: float ) -> Dict[str, float]: """ Calculate cooling load for a window (conduction and solar). Args: window: Window component outdoor_temp: Outdoor temperature (°C) indoor_temp: Indoor temperature (°C) month: Design month hour: Hour of the day latitude: Latitude (e.g., '24N') shading_coefficient: Shading coefficient Returns: Dictionary with conduction, solar, and total loads in Watts """ try: latitude = self.validate_latitude(latitude) month = self.validate_month(month) hour = self.validate_hour(hour) if st.session_state.get('debug_mode', False): logger.debug(f"calculate_window_cooling_load: latitude={latitude}, month={month}, hour={hour}, orientation={window.orientation.value}") # Conduction load cltd = outdoor_temp - indoor_temp # Simplified for windows conduction_load = window.u_value * window.area * cltd # Solar load scl_latitude = f"{latitude}_{month}" try: scl = self.ashrae_tables.get_scl( latitude=scl_latitude, month=month, orientation=window.orientation.value, hour=hour ) except Exception as e: if st.session_state.get('debug_mode', False): logger.error(f"get_scl failed for latitude={scl_latitude}, month={month}, orientation={window.orientation.value}: {str(e)}") logger.warning("Using default SCL=100 W/m²") scl = 100.0 solar_load = window.area * window.shgc * shading_coefficient * scl total_load = conduction_load + solar_load if st.session_state.get('debug_mode', False): logger.debug(f"Window load: conduction={conduction_load}, solar={solar_load}, total={total_load}") return { 'conduction': max(conduction_load, 0.0), 'solar': max(solar_load, 0.0), 'total': max(total_load, 0.0) } except Exception as e: if st.session_state.get('debug_mode', False): logger.error(f"Error in calculate_window_cooling_load: {str(e)}") raise Exception(f"Error in calculate_window_cooling_load: {str(e)}") def calculate_door_cooling_load( self, door: Door, outdoor_temp: float, indoor_temp: float ) -> float: """ Calculate cooling load for a door. Args: door: Door component outdoor_temp: Outdoor temperature (°C) indoor_temp: Indoor temperature (°C) Returns: Cooling load in Watts """ try: if st.session_state.get('debug_mode', False): logger.debug(f"calculate_door_cooling_load: u_value={door.u_value}, area={door.area}") cltd = outdoor_temp - indoor_temp load = door.u_value * door.area * cltd if st.session_state.get('debug_mode', False): logger.debug(f"Door load: cltd={cltd}, load={load}") return max(load, 0.0) except Exception as e: if st.session_state.get('debug_mode', False): logger.error(f"Error in calculate_door_cooling_load: {str(e)}") raise Exception(f"Error in calculate_door_cooling_load: {str(e)}") def calculate_people_cooling_load( self, num_people: int, activity_level: str, hour: int ) -> Dict[str, float]: """ Calculate cooling load from people. Args: num_people: Number of people activity_level: Activity level ('Seated/Resting', 'Light Work', etc.) hour: Hour of the day Returns: Dictionary with sensible and latent loads in Watts """ try: hour = self.validate_hour(hour) if st.session_state.get('debug_mode', False): logger.debug(f"calculate_people_cooling_load: num_people={num_people}, activity_level={activity_level}, hour={hour}") # Sensible and latent heat gains per person (W) heat_gains = { 'Seated/Resting': {'sensible': 70, 'latent': 45}, 'Light Work': {'sensible': 85, 'latent': 65}, 'Moderate Work': {'sensible': 100, 'latent': 100}, 'Heavy Work': {'sensible': 145, 'latent': 170} } gains = heat_gains.get(activity_level, heat_gains['Seated/Resting']) if activity_level not in heat_gains: if st.session_state.get('debug_mode', False): logger.warning(f"Invalid activity_level: {activity_level}. Defaulting to 'Seated/Resting'") try: clf = self.ashrae_tables.get_clf_people( zone_type='A', hours_occupied='8h', hour=hour ) except Exception as e: if st.session_state.get('debug_mode', False): logger.error(f"get_clf_people failed: {str(e)}") logger.warning("Using default CLF=0.5") clf = 0.5 sensible_load = num_people * gains['sensible'] * clf latent_load = num_people * gains['latent'] # Latent load typically not adjusted by CLF if st.session_state.get('debug_mode', False): logger.debug(f"People load: sensible={sensible_load}, latent={latent_load}, clf={clf}") return { 'sensible': max(sensible_load, 0.0), 'latent': max(latent_load, 0.0) } except Exception as e: if st.session_state.get('debug_mode', False): logger.error(f"Error in calculate_people_cooling_load: {str(e)}") raise Exception(f"Error in calculate_people_cooling_load: {str(e)}") def calculate_lights_cooling_load( self, power: float, use_factor: float, special_allowance: float, hour: int ) -> float: """ Calculate cooling load from lighting. Args: power: Total lighting power (W) use_factor: Usage factor (0.0 to 1.0) special_allowance: Special allowance factor hour: Hour of the day Returns: Cooling load in Watts """ try: hour = self.validate_hour(hour) if st.session_state.get('debug_mode', False): logger.debug(f"calculate_lights_cooling_load: power={power}, use_factor={use_factor}, special_allowance={special_allowance}, hour={hour}") try: clf = self.ashrae_tables.get_clf_lights( zone_type='A', hours_occupied='8h', hour=hour ) except Exception as e: if st.session_state.get('debug_mode', False): logger.error(f"get_clf_lights failed: {str(e)}") logger.warning("Using default CLF=0.8") clf = 0.8 load = power * use_factor * special_allowance * clf if st.session_state.get('debug_mode', False): logger.debug(f"Lights load: clf={clf}, load={load}") return max(load, 0.0) except Exception as e: if st.session_state.get('debug_mode', False): logger.error(f"Error in calculate_lights_cooling_load: {str(e)}") raise Exception(f"Error in calculate_lights_cooling_load: {str(e)}") def calculate_equipment_cooling_load( self, power: float, use_factor: float, radiation_factor: float, hour: int ) -> Dict[str, float]: """ Calculate cooling load from equipment. Args: power: Total equipment power (W) use_factor: Usage factor (0.0 to 1.0) radiation_factor: Radiation factor (0.0 to 1.0) hour: Hour of the day Returns: Dictionary with sensible and latent loads in Watts """ try: hour = self.validate_hour(hour) if st.session_state.get('debug_mode', False): logger.debug(f"calculate_equipment_cooling_load: power={power}, use_factor={use_factor}, radiation_factor={radiation_factor}, hour={hour}") try: clf = self.ashrae_tables.get_clf_equipment( zone_type='A', hours_occupied='8h', hour=hour ) except Exception as e: if st.session_state.get('debug_mode', False): logger.error(f"get_clf_equipment failed: {str(e)}") logger.warning("Using default CLF=0.7") clf = 0.7 sensible_load = power * use_factor * radiation_factor * clf latent_load = power * use_factor * (1 - radiation_factor) # Assume non-radiative is latent if st.session_state.get('debug_mode', False): logger.debug(f"Equipment load: sensible={sensible_load}, latent={latent_load}, clf={clf}") return { 'sensible': max(sensible_load, 0.0), 'latent': max(latent_load, 0.0) } except Exception as e: if st.session_state.get('debug_mode', False): logger.error(f"Error in calculate_equipment_cooling_load: {str(e)}") raise Exception(f"Error in calculate_equipment_cooling_load: {str(e)}") def calculate_infiltration_cooling_load( self, flow_rate: float, building_volume: float, outdoor_temp: float, outdoor_rh: float, indoor_temp: float, indoor_rh: float ) -> Dict[str, float]: """ Calculate cooling load from infiltration. Args: flow_rate: Infiltration flow rate (m³/s) building_volume: Building volume (m³) outdoor_temp: Outdoor temperature (°C) outdoor_rh: Outdoor relative humidity (%) indoor_temp: Indoor temperature (°C) indoor_rh: Indoor relative humidity (%) Returns: Dictionary with sensible and latent loads in Watts """ try: if st.session_state.get('debug_mode', False): logger.debug(f"calculate_infiltration_cooling_load: flow_rate={flow_rate}, building_volume={building_volume}, outdoor_temp={outdoor_temp}, indoor_temp={indoor_temp}") # Calculate air changes per hour (ACH) ach = (flow_rate * 3600) / building_volume if building_volume > 0 else 0.5 if ach < 0: if st.session_state.get('debug_mode', False): logger.warning(f"Invalid ACH: {ach}. Defaulting to 0.5") ach = 0.5 # Sensible load: Q = 1.2 * flow_rate * (Tout - Tin) sensible_load = 1.2 * flow_rate * (outdoor_temp - indoor_temp) # Latent load using psychrometrics outdoor_w = self.heat_transfer.psychrometrics.humidity_ratio(outdoor_temp, outdoor_rh) indoor_w = self.heat_transfer.psychrometrics.humidity_ratio(indoor_temp, indoor_rh) latent_load = 1.2 * flow_rate * 2500 * (outdoor_w - indoor_w) # 2500 kJ/kg for latent heat if st.session_state.get('debug_mode', False): logger.debug(f"Infiltration load: sensible={sensible_load}, latent={latent_load}, ach={ach}, outdoor_w={outdoor_w}, indoor_w={indoor_w}") return { 'sensible': max(sensible_load, 0.0), 'latent': max(latent_load, 0.0) } except Exception as e: if st.session_state.get('debug_mode', False): logger.error(f"Error in calculate_infiltration_cooling_load: {str(e)}") raise Exception(f"Error in calculate_infiltration_cooling_load: {str(e)}") def calculate_ventilation_cooling_load( self, flow_rate: float, outdoor_temp: float, outdoor_rh: float, indoor_temp: float, indoor_rh: float ) -> Dict[str, float]: """ Calculate cooling load from ventilation. Args: flow_rate: Ventilation flow rate (m³/s) outdoor_temp: Outdoor temperature (°C) outdoor_rh: Outdoor relative humidity (%) indoor_temp: Indoor temperature (°C) indoor_rh: Indoor relative humidity (%) Returns: Dictionary with sensible and latent loads in Watts """ try: if st.session_state.get('debug_mode', False): logger.debug(f"calculate_ventilation_cooling_load: flow_rate={flow_rate}, outdoor_temp={outdoor_temp}, indoor_temp={indoor_temp}") # Sensible load: Q = 1.2 * flow_rate * (Tout - Tin) sensible_load = 1.2 * flow_rate * (outdoor_temp - indoor_temp) # Latent load using psychrometrics outdoor_w = self.heat_transfer.psychrometrics.humidity_ratio(outdoor_temp, outdoor_rh) indoor_w = self.heat_transfer.psychrometrics.humidity_ratio(indoor_temp, indoor_rh) latent_load = 1.2 * flow_rate * 2500 * (outdoor_w - indoor_w) # 2500 kJ/kg for latent heat if st.session_state.get('debug_mode', False): logger.debug(f"Ventilation load: sensible={sensible_load}, latent={latent_load}, outdoor_w={outdoor_w}, indoor_w={indoor_w}") return { 'sensible': max(sensible_load, 0.0), 'latent': max(latent_load, 0.0) } except Exception as e: if st.session_state.get('debug_mode', False): logger.error(f"Error in calculate_ventilation_cooling_load: {str(e)}") raise Exception(f"Error in calculate_ventilation_cooling_load: {str(e)}")