""" Cooling load calculation module for HVAC Load Calculator. Implements ASHRAE steady-state methods with Cooling Load Temperature Difference (CLTD). Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 18, Section 18.5. Author: Dr Majed Abuseif Date: April 2025 Version: 1.0.7 """ from typing import Dict, List, Any, Optional, Tuple import numpy as np import logging from data.ashrae_tables import ASHRAETables from utils.heat_transfer import HeatTransferCalculations from utils.psychrometrics import Psychrometrics from app.component_selection import Wall, Roof, Window, Door, Skylight, Orientation from data.drapery import Drapery, GlazingType, FrameType, WINDOW_U_FACTORS, WINDOW_SHGC, SKYLIGHT_U_FACTORS, SKYLIGHT_SHGC, CLTDCalculator # Set up logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) class CoolingLoadCalculator: """Class for cooling load calculations based on ASHRAE steady-state methods.""" def __init__(self, debug_mode: bool = False): """ Initialize cooling load calculator. Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 18, Section 18.5. Args: debug_mode: Enable debug logging if True """ self.ashrae_tables = ASHRAETables() self.heat_transfer = HeatTransferCalculations() self.psychrometrics = Psychrometrics() self.hours = list(range(24)) self.valid_latitudes = ['24N', '32N', '40N', '48N', '56N'] self.valid_months = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC'] self.valid_wall_groups = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'] self.valid_roof_groups = ['A', 'B', 'C', 'D', 'E', 'F', 'G'] self.debug_mode = debug_mode if debug_mode: logger.setLevel(logging.DEBUG) def validate_latitude(self, latitude: Any) -> str: """ Validate and normalize latitude input. Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 14, Section 14.2. Args: latitude: Latitude input (str, float, or other) Returns: Valid latitude string ('24N', '32N', '40N', '48N', '56N') """ try: if not isinstance(latitude, str): try: lat_val = float(latitude) if lat_val <= 28: return '24N' elif lat_val <= 36: return '32N' elif lat_val <= 44: return '40N' elif lat_val <= 52: return '48N' else: return '56N' except (ValueError, TypeError): latitude = str(latitude) latitude = latitude.strip().upper() if self.debug_mode: logger.debug(f"Validating latitude: {latitude}") if '_' in latitude: parts = latitude.split('_') if len(parts) > 1: lat_part = parts[0] if self.debug_mode: logger.warning(f"Detected concatenated input: {latitude}. Using latitude={lat_part}") latitude = lat_part 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 <= 28: mapped_latitude = '24N' elif lat_val <= 36: mapped_latitude = '32N' elif lat_val <= 44: mapped_latitude = '40N' elif lat_val <= 52: mapped_latitude = '48N' else: mapped_latitude = '56N' if self.debug_mode: logger.debug(f"Mapped numerical latitude {lat_val} to {mapped_latitude}") return mapped_latitude except ValueError: if self.debug_mode: logger.warning(f"Cannot parse numerical latitude: {latitude}. Defaulting to '32N'") return '32N' if latitude in self.valid_latitudes: return latitude if self.debug_mode: logger.warning(f"Invalid latitude: {latitude}. Defaulting to '32N'") return '32N' except Exception as e: if self.debug_mode: logger.error(f"Error validating latitude {latitude}: {str(e)}") return '32N' def validate_month(self, month: Any) -> str: """ Validate and normalize month input. Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 14, Section 14.2. 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 self.debug_mode: logger.warning(f"Invalid month: {month}. Defaulting to 'JUL'") return 'JUL' return month_upper except Exception as e: if self.debug_mode: logger.error(f"Error validating month {month}: {str(e)}") return 'JUL' def validate_hour(self, hour: Any) -> int: """ Validate and normalize hour input. Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 14, Section 14.2. 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 self.debug_mode: logger.warning(f"Invalid hour: {hour}. Defaulting to 15") return 15 return hour except (ValueError, TypeError): if self.debug_mode: logger.warning(f"Invalid hour format: {hour}. Defaulting to 15") return 15 def validate_conditions(self, outdoor_temp: float, indoor_temp: float, outdoor_rh: float, indoor_rh: float) -> None: """ Validate temperature and relative humidity inputs. Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 1, Section 1.2. Args: outdoor_temp: Outdoor temperature in °C indoor_temp: Indoor temperature in °C outdoor_rh: Outdoor relative humidity in % indoor_rh: Indoor relative humidity in % Raises: ValueError: If inputs are invalid """ if not -50 <= outdoor_temp <= 60 or not -50 <= indoor_temp <= 60: raise ValueError("Temperatures must be between -50°C and 60°C") if not 0 <= outdoor_rh <= 100 or not 0 <= indoor_rh <= 100: raise ValueError("Relative humidities must be between 0 and 100%") if outdoor_temp - indoor_temp < 1: raise ValueError("Outdoor temperature must be at least 1°C above indoor temperature for cooling") 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, p_atm: float = 101325 ) -> Dict[str, Any]: """ Calculate hourly cooling loads for all components. Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 18, Section 18.5. Args: building_components: Dictionary of building components outdoor_conditions: Outdoor weather conditions (temperature, relative_humidity, latitude, month) indoor_conditions: Indoor design conditions (temperature, relative_humidity) internal_loads: Internal heat gains (people, lights, equipment, infiltration, ventilation) building_volume: Building volume in cubic meters p_atm: Atmospheric pressure in Pa (default: 101325 Pa) 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)}, 'skylights_conduction': {h: 0.0 for h in range(1, 25)}, 'skylights_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 conditions self.validate_conditions( outdoor_conditions['temperature'], indoor_conditions['temperature'], outdoor_conditions.get('relative_humidity', 50.0), indoor_conditions.get('relative_humidity', 50.0) ) latitude = self.validate_latitude(outdoor_conditions.get('latitude', '32N')) month = self.validate_month(outdoor_conditions.get('month', 'JUL')) if self.debug_mode: 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, solar_absorptivity=wall.solar_absorptivity ) 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, solar_absorptivity=roof.solar_absorptivity ) hourly_loads['roofs'][hour + 1] += load # Calculate loads for windows for window in building_components.get('windows', []): for hour in range(24): adjusted_shgc = getattr(window, 'adjusted_shgc', None) 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, adjusted_shgc=adjusted_shgc ) hourly_loads['windows_conduction'][hour + 1] += load_dict['conduction'] hourly_loads['windows_solar'][hour + 1] += load_dict['solar'] # Calculate loads for skylights for skylight in building_components.get('skylights', []): for hour in range(24): adjusted_shgc = getattr(skylight, 'adjusted_shgc', None) load_dict = self.calculate_skylight_cooling_load( skylight=skylight, outdoor_temp=outdoor_conditions['temperature'], indoor_temp=indoor_conditions['temperature'], month=month, hour=hour, latitude=latitude, shading_coefficient=skylight.shading_coefficient, adjusted_shgc=adjusted_shgc ) hourly_loads['skylights_conduction'][hour + 1] += load_dict['conduction'] hourly_loads['skylights_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'], p_atm=p_atm ) 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'], p_atm=p_atm ) 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 self.debug_mode: 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. Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 18, Section 18.5. 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['skylights_conduction'][hour], hourly_loads['skylights_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], 'skylights_conduction': hourly_loads['skylights_conduction'][design_hour], 'skylights_solar': hourly_loads['skylights_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 self.debug_mode: 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. Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 18, Section 18.5. 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['skylights_conduction'] + design_loads['skylights_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 self.debug_mode: 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, solar_absorptivity: float ) -> float: """ Calculate cooling load for a wall using CLTD method. Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 18, Equation 18.10. 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') solar_absorptivity: Solar absorptivity of the wall surface (0.0 to 1.0) Returns: Cooling load in Watts """ try: latitude = self.validate_latitude(latitude) month = self.validate_month(month) hour = self.validate_hour(hour) wall_group = str(wall.wall_group).upper() if hasattr(wall, 'wall_group') else 'A' numeric_map = {'1': 'A', '2': 'B', '3': 'C', '4': 'D', '5': 'E', '6': 'F', '7': 'G', '8': 'H'} if wall_group in numeric_map: wall_group = numeric_map[wall_group] if self.debug_mode: logger.info(f"Mapped wall_group {wall.wall_group} to {wall_group}") elif wall_group not in self.valid_wall_groups: if self.debug_mode: logger.warning(f"Invalid wall group: {wall_group}. Defaulting to 'A'") wall_group = 'A' try: lat_value = float(latitude.replace('N', '')) if self.debug_mode: logger.debug(f"Converted latitude {latitude} to {lat_value} for wall CLTD") except ValueError: if self.debug_mode: logger.error(f"Invalid latitude format: {latitude}. Defaulting to 32.0") lat_value = 32.0 if self.debug_mode: logger.debug(f"Calling get_cltd for wall: group={wall_group}, orientation={wall.orientation.value}, hour={hour}, latitude={lat_value}, solar_absorptivity={solar_absorptivity}") try: cltd_f = self.ashrae_tables.get_cltd( element_type='wall', group=wall_group, orientation=wall.orientation.value, hour=hour, latitude=lat_value, solar_absorptivity=solar_absorptivity ) cltd = (cltd_f - 32) * 5 / 9 # Convert °F to °C except Exception as e: if self.debug_mode: logger.error(f"get_cltd failed for wall_group={wall_group}, latitude={lat_value}: {str(e)}") logger.warning("Using default CLTD=8.0°C") cltd = 8.0 load = wall.u_value * wall.area * cltd if self.debug_mode: 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 self.debug_mode: 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, solar_absorptivity: float ) -> float: """ Calculate cooling load for a roof using CLTD method. Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 18, Equation 18.10. 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') solar_absorptivity: Solar absorptivity of the roof surface (0.0 to 1.0) Returns: Cooling load in Watts """ try: latitude = self.validate_latitude(latitude) month = self.validate_month(month) hour = self.validate_hour(hour) roof_group = str(roof.roof_group).upper() if hasattr(roof, 'roof_group') else 'A' numeric_map = {'1': 'A', '2': 'B', '3': 'C', '4': 'D', '5': 'E', '6': 'F', '7': 'G', '8': 'G'} if roof_group in numeric_map: roof_group = numeric_map[roof_group] if self.debug_mode: logger.info(f"Mapped roof_group {roof.roof_group} to {roof_group}") elif roof_group not in self.valid_roof_groups: if self.debug_mode: logger.warning(f"Invalid roof group: {roof_group}. Defaulting to 'A'") roof_group = 'A' try: lat_value = float(latitude.replace('N', '')) if self.debug_mode: logger.debug(f"Converted latitude {latitude} to {lat_value} for roof CLTD") except ValueError: if self.debug_mode: logger.error(f"Invalid latitude format: {latitude}. Defaulting to 32.0") lat_value = 32.0 if self.debug_mode: logger.debug(f"Calling get_cltd for roof: group={roof_group}, orientation={roof.orientation.value}, hour={hour}, latitude={lat_value}, solar_absorptivity={solar_absorptivity}") try: cltd_f = self.ashrae_tables.get_cltd( element_type='roof', group=roof_group, orientation=roof.orientation.value, hour=hour, latitude=lat_value, solar_absorptivity=solar_absorptivity ) cltd = (cltd_f - 32) * 5 / 9 # Convert °F to °C except Exception as e: if self.debug_mode: logger.error(f"get_cltd failed for roof_group={roof_group}, latitude={lat_value}: {str(e)}") logger.warning("Using default CLTD=8.0°C") cltd = 8.0 load = roof.u_value * roof.area * cltd if self.debug_mode: 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 self.debug_mode: 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, adjusted_shgc: Optional[float] = None, glazing_type: Optional[str] = None, frame_type: Optional[str] = None ) -> Dict[str, float]: """ Calculate cooling load for a window (conduction and solar). Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 18, Equations 18.12-18.13. Args: window: Window component outdoor_temp: Outdoor temperature (°C) indoor_temp: Indoor temperature (°C) month: Design month (e.g., 'JUL') hour: Hour of the day latitude: Latitude (e.g., '40N') shading_coefficient: Default shading coefficient adjusted_shgc: Adjusted SHGC from external drapery calculation (optional) glazing_type: Glazing type (e.g., 'Single Clear') (optional) frame_type: Frame type (e.g., 'Aluminum without Thermal Break') (optional) Returns: Dictionary with conduction, solar, and total loads in Watts """ try: # Validate inputs latitude = self.validate_latitude(latitude) month = self.validate_month(month) hour = self.validate_hour(hour) if self.debug_mode: logger.debug(f"calculate_window_cooling_load: latitude={latitude}, month={month}, hour={hour}, orientation={window.orientation.value}, glazing_type={glazing_type}, frame_type={frame_type}") # Convert month string to integer for CLTDCalculator month_map = {'JAN': 1, 'FEB': 2, 'MAR': 3, 'APR': 4, 'MAY': 5, 'JUN': 6, 'JUL': 7, 'AUG': 8, 'SEP': 9, 'OCT': 10, 'NOV': 11, 'DEC': 12} month_int = month_map.get(month.upper(), 7) # Default to July if self.debug_mode: logger.debug(f"Month converted: {month} -> {month_int}") # Convert string latitude to numerical for SCL interpolation only try: lat_value = float(latitude.replace('N', '')) except ValueError: if self.debug_mode: logger.error(f"Invalid latitude format: {latitude}. Defaulting to 32.0") lat_value = 32.0 # Initialize CLTDCalculator with string latitude cltd_calculator = CLTDCalculator( indoor_temp=indoor_temp, outdoor_max_temp=outdoor_temp, outdoor_daily_range=11.7, # Default from drapery.py latitude=latitude, # Use string latitude month=month_int ) # Determine U-factor and SHGC u_value = window.u_value shgc = window.shgc if glazing_type and frame_type: try: glazing_enum = next(g for g in GlazingType if g.value == glazing_type) frame_enum = next(f for f in FrameType if f.value == frame_type) u_value = WINDOW_U_FACTORS.get((glazing_enum, frame_enum), window.u_value) shgc = WINDOW_SHGC.get((glazing_enum, frame_enum), window.shgc) if self.debug_mode: logger.debug(f"Using table values: u_value={u_value}, shgc={shgc} for glazing_type={glazing_type}, frame_type={frame_type}") except StopIteration: if self.debug_mode: logger.warning(f"Invalid glazing_type={glazing_type} or frame_type={frame_type}. Using default u_value={u_value}, shgc={shgc}") # Conduction load using CLTD try: glazing_key = glazing_type if glazing_type in ['Single Clear', 'Double Tinted', 'Low-E', 'Reflective'] else 'SingleClear' cltd = cltd_calculator.get_cltd_window( glazing_type=glazing_key, orientation=window.orientation.value, hour=hour ) if self.debug_mode: logger.debug(f"CLTD from CLTDCalculator: {cltd} for glazing_type={glazing_key}, latitude={latitude}") except Exception as e: if self.debug_mode: logger.error(f"get_cltd_window failed for glazing_type={glazing_key}, latitude={latitude}: {str(e)}") logger.warning("Using default CLTD=8.0°C") cltd = 8.0 conduction_load = u_value * window.area * cltd # Determine shading coefficient effective_shading_coefficient = adjusted_shgc if adjusted_shgc is not None else shading_coefficient if adjusted_shgc is None and hasattr(window, 'drapery') and window.drapery and window.drapery.enabled: try: effective_shading_coefficient = window.drapery.get_shading_coefficient(shgc) if self.debug_mode: logger.debug(f"Using drapery shading coefficient: {effective_shading_coefficient}") except Exception as e: if self.debug_mode: logger.warning(f"Error getting drapery shading coefficient: {str(e)}. Using default shading_coefficient={shading_coefficient}") else: if self.debug_mode: logger.debug(f"Using shading coefficient: {effective_shading_coefficient} (adjusted_shgc={adjusted_shgc}, drapery={'enabled' if hasattr(window, 'drapery') and window.drapery and window.drapery.enabled else 'disabled'})") # Solar load with latitude Interpolation try: latitudes = [24, 32, 40, 48, 56] lat1 = max([lat for lat in latitudes if lat <= lat_value], default=24) lat2 = min([lat for lat in latitudes if lat >= lat_value], default=56) scl1 = cltd_calculator.ashrae_tables.get_scl( latitude=float(lat1), orientation=window.orientation.value, hour=hour, month=month_int ) scl2 = cltd_calculator.ashrae_tables.get_scl( latitude=float(lat2), orientation=window.orientation.value, hour=hour, month=month_int ) # Interpolate SCL if lat1 == lat2: scl = scl1 else: weight = (lat_value - lat1) / (lat2 - lat1) scl = scl1 + weight * (scl2 - scl1) if self.debug_mode: logger.debug(f"SCL interpolated: scl1={scl1}, scl2={scl2}, lat1={lat1}, lat2={lat2}, weight={weight}, scl={scl}") except Exception as e: if self.debug_mode: logger.error(f"get_scl failed for latitude={lat_value}, month={month}, orientation={window.orientation.value}: {str(e)}") logger.warning("Using default SCL=100 W/m²") scl = 100.0 solar_load = window.area * shgc * effective_shading_coefficient * scl total_load = conduction_load + solar_load if self.debug_mode: logger.debug(f"Window load: conduction={conduction_load}, solar={solar_load}, total={total_load}, u_value={u_value}, shgc={shgc}, cltd={cltd}, effective_shading_coefficient={effective_shading_coefficient}") return { 'conduction': max(conduction_load, 0.0), 'solar': max(solar_load, 0.0), 'total': max(total_load, 0.0) } except Exception as e: if self.debug_mode: logger.error(f"Error in calculate_window_cooling_load: {str(e)}") raise Exception(f"Error in calculate_window_cooling_load: {str(e)}") def calculate_skylight_cooling_load( self, skylight: Skylight, outdoor_temp: float, indoor_temp: float, month: str, hour: int, latitude: str, shading_coefficient: float, adjusted_shgc: Optional[float] = None, glazing_type: Optional[str] = None, frame_type: Optional[str] = None ) -> Dict[str, float]: """ Calculate cooling load for a skylight (conduction and solar). Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 18, Equations 18.12-18.13. Args: skylight: Skylight component outdoor_temp: Outdoor temperature (°C) indoor_temp: Indoor temperature (°C) month: Design month (e.g., 'JUL') hour: Hour of the day latitude: Latitude (e.g., '40N') shading_coefficient: Default shading coefficient adjusted_shgc: Adjusted SHGC from external drapery calculation (optional) glazing_type: Glazing type (e.g., 'Single Clear') (optional) frame_type: Frame type (e.g., 'Aluminum without Thermal Break') (optional) Returns: Dictionary with conduction, solar, and total loads in Watts """ try: # Validate inputs latitude = self.validate_latitude(latitude) month = self.validate_month(month) hour = self.validate_hour(hour) if self.debug_mode: logger.debug(f"calculate_skylight_cooling_load: latitude={latitude}, month={month}, hour={hour}, orientation=Horizontal, glazing_type={glazing_type}, frame_type={frame_type}") # Convert month string to integer for CLTDCalculator month_map = {'JAN': 1, 'FEB': 2, 'MAR': 3, 'APR': 4, 'MAY': 5, 'JUN': 6, 'JUL': 7, 'AUG': 8, 'SEP': 9, 'OCT': 10, 'NOV': 11, 'DEC': 12} month_int = month_map.get(month.upper(), 7) # Default to July if self.debug_mode: logger.debug(f"Month converted: {month} -> {month_int}") # Convert string latitude to numerical for SCL interpolation only try: lat_value = float(latitude.replace('N', '')) except ValueError: if self.debug_mode: logger.error(f"Invalid latitude format: {latitude}. Defaulting to 32.0") lat_value = 32.0 # Initialize CLTDCalculator with string latitude cltd_calculator = CLTDCalculator( indoor_temp=indoor_temp, outdoor_max_temp=outdoor_temp, outdoor_daily_range=11.7, # Default from drapery.py latitude=latitude, # Use string latitude month=month_int ) # Determine U-factor and SHGC u_value = skylight.u_value shgc = skylight.shgc if glazing_type and frame_type: try: glazing_enum = next(g for g in GlazingType if g.value == glazing_type) frame_enum = next(f for f in FrameType if f.value == frame_type) u_value = SKYLIGHT_U_FACTORS.get((glazing_enum, frame_enum), skylight.u_value) shgc = SKYLIGHT_SHGC.get((glazing_enum, frame_enum), skylight.shgc) if self.debug_mode: logger.debug(f"Using table values: u_value={u_value}, shgc={shgc} for glazing_type={glazing_type}, frame_type={frame_type}") except StopIteration: if self.debug_mode: logger.warning(f"Invalid glazing_type={glazing_type} or frame_type={frame_type}. Using default u_value={u_value}, shgc={shgc}") # Conduction load using CLTD try: glazing_key = glazing_type if glazing_type in ['Single Clear', 'Double Tinted', 'Low-E', 'Reflective'] else 'SingleClear' cltd = cltd_calculator.get_cltd_skylight( glazing_type=glazing_key, hour=hour ) if self.debug_mode: logger.debug(f"CLTD from CLTDCalculator: {cltd} for glazing_type={glazing_key}, latitude={latitude}") except Exception as e: if self.debug_mode: logger.error(f"get_cltd_skylight failed for glazing_type={glazing_key}, latitude={latitude}: {str(e)}") logger.warning("Using default CLTD=8.0°C") cltd = 8.0 conduction_load = u_value * skylight.area * cltd # Determine shading coefficient effective_shading_coefficient = adjusted_shgc if adjusted_shgc is not None else shading_coefficient if adjusted_shgc is None and hasattr(skylight, 'drapery') and skylight.drapery and skylight.drapery.enabled: try: effective_shading_coefficient = skylight.drapery.get_shading_coefficient(shgc) if self.debug_mode: logger.debug(f"Using drapery shading coefficient: {effective_shading_coefficient}") except Exception as e: if self.debug_mode: logger.warning(f"Error getting drapery shading coefficient: {str(e)}. Using default shading_coefficient={shading_coefficient}") else: if self.debug_mode: logger.debug(f"Using shading coefficient: {effective_shading_coefficient} (adjusted_shgc={adjusted_shgc}, drapery={'enabled' if hasattr(skylight, 'drapery') and skylight.drapery and skylight.drapery.enabled else 'disabled'})") # Solar load with latitude interpolation try: latitudes = [24, 32, 40, 48, 56] lat1 = max([lat for lat in latitudes if lat <= lat_value], default=24) lat2 = min([lat for lat in latitudes if lat >= lat_value], default=56) scl1 = cltd_calculator.ashrae_tables.get_scl( latitude=float(lat1), orientation='Horizontal', hour=hour, month=month_int ) scl2 = cltd_calculator.ashrae_tables.get_scl( latitude=float(lat2), orientation='Horizontal', hour=hour, month=month_int ) # Interpolate SCL if lat1 == lat2: scl = scl1 else: weight = (lat_value - lat1) / (lat2 - lat1) scl = scl1 + weight * (scl2 - scl1) if self.debug_mode: logger.debug(f"SCL interpolated: scl1={scl1}, scl2={scl2}, lat1={lat1}, lat2={lat2}, weight={weight}, scl={scl}") except Exception as e: if self.debug_mode: logger.error(f"get_scl failed for latitude={lat_value}, month={month}, orientation=Horizontal: {str(e)}") logger.warning("Using default SCL=100 W/m²") scl = 100.0 solar_load = skylight.area * shgc * effective_shading_coefficient * scl total_load = conduction_load + solar_load if self.debug_mode: logger.debug(f"Skylight load: conduction={conduction_load}, solar={solar_load}, total={total_load}, u_value={u_value}, shgc={shgc}, cltd={cltd}, effective_shading_coefficient={effective_shading_coefficient}") return { 'conduction': max(conduction_load, 0.0), 'solar': max(solar_load, 0.0), 'total': max(total_load, 0.0) } except Exception as e: if self.debug_mode: logger.error(f"Error in calculate_skylight_cooling_load: {str(e)}") raise Exception(f"Error in calculate_skylight_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. Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 18, Equation 18.1. Args: door: Door component outdoor_temp: Outdoor temperature (°C) indoor_temp: Indoor temperature (°C) Returns: Cooling load in Watts """ try: if self.debug_mode: 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 self.debug_mode: logger.debug(f"Door load: cltd={cltd}, load={load}") return max(load, 0.0) except Exception as e: if self.debug_mode: 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. Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 18, Table 18.4. 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 self.debug_mode: logger.debug(f"calculate_people_cooling_load: num_people={num_people}, activity_level={activity_level}, hour={hour}") 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 self.debug_mode: 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='6h', hour=hour ) except Exception as e: if self.debug_mode: 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'] if self.debug_mode: 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 self.debug_mode: 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. Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 18, Table 18.5. 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 self.debug_mode: 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='6h', hour=hour ) except Exception as e: if self.debug_mode: 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 self.debug_mode: logger.debug(f"Lights load: clf={clf}, load={load}") return max(load, 0.0) except Exception as e: if self.debug_mode: 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. Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 18, Table 18.6. 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 self.debug_mode: 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_operated='6h', hour=hour ) except Exception as e: if self.debug_mode: 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) if self.debug_mode: 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 self.debug_mode: 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, p_atm: float = 101325 ) -> Dict[str, float]: """ Calculate cooling load from infiltration. Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 18, Equations 18.5-18.6. 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 (%) p_atm: Atmospheric pressure in Pa (default: 101325 Pa) Returns: Dictionary with sensible and latent loads in Watts """ try: if self.debug_mode: logger.debug(f"calculate_infiltration_cooling_load: flow_rate={flow_rate}, building_volume={building_volume}, outdoor_temp={outdoor_temp}, indoor_temp={indoor_temp}") self.validate_conditions(outdoor_temp, indoor_temp, outdoor_rh, indoor_rh) if flow_rate < 0 or building_volume <= 0: raise ValueError("Flow rate cannot be negative and building volume must be positive") # Calculate air changes per hour (ACH) ach = (flow_rate * 3600) / building_volume if building_volume > 0 else 0.5 if ach < 0: if self.debug_mode: logger.warning(f"Invalid ACH: {ach}. Defaulting to 0.5") ach = 0.5 # Calculate humidity ratio difference outdoor_w = self.heat_transfer.psychrometrics.humidity_ratio(outdoor_temp, outdoor_rh, p_atm) indoor_w = self.heat_transfer.psychrometrics.humidity_ratio(indoor_temp, indoor_rh, p_atm) delta_w = max(0, outdoor_w - indoor_w) # Calculate sensible and latent loads using heat_transfer methods sensible_load = self.heat_transfer.infiltration_heat_transfer( flow_rate, outdoor_temp - indoor_temp, indoor_temp, indoor_rh, p_atm ) latent_load = self.heat_transfer.infiltration_latent_heat_transfer( flow_rate, delta_w, indoor_temp, indoor_rh, p_atm ) if self.debug_mode: 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 self.debug_mode: 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, p_atm: float = 101325 ) -> Dict[str, float]: """ Calculate cooling load from ventilation. Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 18, Equations 18.5-18.6. 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 (%) p_atm: Atmospheric pressure in Pa (default: 101325 Pa) Returns: Dictionary with sensible and latent loads in Watts """ try: if self.debug_mode: logger.debug(f"calculate_ventilation_cooling_load: flow_rate={flow_rate}, outdoor_temp={outdoor_temp}, indoor_temp={indoor_temp}") self.validate_conditions(outdoor_temp, indoor_temp, outdoor_rh, indoor_rh) if flow_rate < 0: raise ValueError("Flow rate cannot be negative") # Calculate humidity ratio difference outdoor_w = self.heat_transfer.psychrometrics.humidity_ratio(outdoor_temp, outdoor_rh, p_atm) indoor_w = self.heat_transfer.psychrometrics.humidity_ratio(indoor_temp, indoor_rh, p_atm) delta_w = max(0, outdoor_w - indoor_w) # Calculate sensible and latent loads using heat_transfer methods sensible_load = self.heat_transfer.infiltration_heat_transfer( flow_rate, outdoor_temp - indoor_temp, indoor_temp, indoor_rh, p_atm ) latent_load = self.heat_transfer.infiltration_latent_heat_transfer( flow_rate, delta_w, indoor_temp, indoor_rh, p_atm ) if self.debug_mode: 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 self.debug_mode: logger.error(f"Error in calculate_ventilation_cooling_load: {str(e)}") raise Exception(f"Error in calculate_ventilation_cooling_load: {str(e)}") # Example usage if __name__ == "__main__": calculator = CoolingLoadCalculator(debug_mode=True) # Example inputs components = { 'walls': [Wall(id="w1", name="North Wall", area=20.0, u_value=0.5, orientation=Orientation.NORTH, wall_group='A', solar_absorptivity=0.6)], 'roofs': [Roof(id="r1", name="Main Roof", area=100.0, u_value=0.3, orientation=Orientation.HORIZONTAL, roof_group='A', solar_absorptivity=0.6)], 'windows': [Window(id="win1", name="South Window", area=10.0, u_value=2.8, orientation=Orientation.SOUTH, shgc=0.7, shading_coefficient=0.8)], 'doors': [Door(id="d1", name="Main Door", area=2.0, u_value=2.0, orientation=Orientation.NORTH)] } outdoor_conditions = { 'temperature': 35.0, 'relative_humidity': 60.0, 'latitude': '32N', 'month': 'JUL' } indoor_conditions = { 'temperature': 24.0, 'relative_humidity': 50.0 } internal_loads = { 'people': {'number': 10, 'activity_level': 'Seated/Resting'}, 'lights': {'power': 1000.0, 'use_factor': 0.8, 'special_allowance': 1.0}, 'equipment': {'power': 500.0, 'use_factor': 0.7, 'radiation_factor': 0.5}, 'infiltration': {'flow_rate': 0.05}, 'ventilation': {'flow_rate': 0.1} } building_volume = 300.0 # Calculate hourly loads hourly_loads = calculator.calculate_hourly_cooling_loads( components, outdoor_conditions, indoor_conditions, internal_loads, building_volume ) design_loads = calculator.calculate_design_cooling_load(hourly_loads) summary = calculator.calculate_cooling_load_summary(design_loads) logger.info(f"Design Cooling Load Summary: {summary}")