Spaces:
Sleeping
Sleeping
| """ | |
| 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 | |
| # 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 | |
| ) -> 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 | |
| hour: Hour of the day | |
| latitude: Latitude (e.g., '24N') | |
| shading_coefficient: Default shading coefficient | |
| adjusted_shgc: Adjusted SHGC from external drapery calculation (optional) | |
| 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 self.debug_mode: | |
| logger.debug(f"calculate_window_cooling_load: latitude={latitude}, month={month}, hour={hour}, orientation={window.orientation.value}") | |
| # Conduction load | |
| cltd = outdoor_temp - indoor_temp | |
| conduction_load = window.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(window.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 | |
| try: | |
| scl = self.ashrae_tables.get_scl( | |
| latitude=latitude, | |
| month=month, | |
| orientation=window.orientation.value, | |
| hour=hour | |
| ) | |
| except Exception as e: | |
| if self.debug_mode: | |
| logger.error(f"get_scl failed for latitude={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 * 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}, 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 | |
| ) -> 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 | |
| hour: Hour of the day | |
| latitude: Latitude (e.g., '24N') | |
| shading_coefficient: Default shading coefficient | |
| adjusted_shgc: Adjusted SHGC from external drapery calculation (optional) | |
| 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 self.debug_mode: | |
| logger.debug(f"calculate_skylight_cooling_load: latitude={latitude}, month={month}, hour={hour}, orientation=Horizontal") | |
| # Conduction load | |
| cltd = outdoor_temp - indoor_temp | |
| conduction_load = skylight.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(skylight.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 | |
| try: | |
| scl = self.ashrae_tables.get_scl( | |
| latitude=latitude, | |
| month=month, | |
| orientation='Horizontal', | |
| hour=hour | |
| ) | |
| except Exception as e: | |
| if self.debug_mode: | |
| logger.error(f"get_scl failed for latitude={latitude}, month={month}, orientation=Horizontal: {str(e)}") | |
| logger.warning("Using default SCL=100 W/m²") | |
| scl = 100.0 | |
| solar_load = skylight.area * skylight.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}, 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}") |