""" Cooling load calculation module for HVAC Load Calculator. Based on ASHRAE steady-state calculation methods. Author: Dr Majed Abuseif Date: March 2025 Version: 1.0.0 """ from typing import Dict, List, Any, Optional, Tuple import numpy as np from datetime import datetime from data.ashrae_tables import ASHRAETables from utils.heat_transfer import HeatTransferCalculations from app.component_selection import Wall, Roof, Window, Door, Orientation 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(1, 25)) 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 self.hours}, 'roofs': {h: 0.0 for h in self.hours}, 'windows_conduction': {h: 0.0 for h in self.hours}, 'windows_solar': {h: 0.0 for h in self.hours}, 'doors': {h: 0.0 for h in self.hours}, 'people_sensible': {h: 0.0 for h in self.hours}, 'people_latent': {h: 0.0 for h in self.hours}, 'lights': {h: 0.0 for h in self.hours}, 'equipment_sensible': {h: 0.0 for h in self.hours}, 'equipment_latent': {h: 0.0 for h in self.hours}, 'infiltration_sensible': {h: 0.0 for h in self.hours}, 'infiltration_latent': {h: 0.0 for h in self.hours}, 'ventilation_sensible': {h: 0.0 for h in self.hours}, 'ventilation_latent': {h: 0.0 for h in self.hours} } try: # Calculate loads for walls for wall in building_components.get('walls', []): for hour in self.hours: load = self.calculate_wall_cooling_load( wall=wall, outdoor_temp=outdoor_conditions['temperature'], indoor_temp=indoor_conditions['temperature'], month=outdoor_conditions['month'], hour=hour, latitude=outdoor_conditions['latitude'] ) hourly_loads['walls'][hour] += load # Calculate loads for roofs for roof in building_components.get('roofs', []): for hour in self.hours: load = self.calculate_roof_cooling_load( roof=roof, outdoor_temp=outdoor_conditions['temperature'], indoor_temp=indoor_conditions['temperature'], month=outdoor_conditions['month'], hour=hour, latitude=outdoor_conditions['latitude'] ) hourly_loads['roofs'][hour] += load # Calculate loads for windows for window in building_components.get('windows', []): for hour in self.hours: load_dict = self.calculate_window_cooling_load( window=window, outdoor_temp=outdoor_conditions['temperature'], indoor_temp=indoor_conditions['temperature'], month=outdoor_conditions['month'], hour=hour, latitude=outdoor_conditions['latitude'], shading_coefficient=window.shading_coefficient ) hourly_loads['windows_conduction'][hour] += load_dict['conduction'] hourly_loads['windows_solar'][hour] += load_dict['solar'] # Calculate loads for doors for door in building_components.get('doors', []): for hour in self.hours: load = self.calculate_door_cooling_load( door=door, outdoor_temp=outdoor_conditions['temperature'], indoor_temp=indoor_conditions['temperature'] ) hourly_loads['doors'][hour] += load # Calculate internal loads for hour in self.hours: # 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] += people_load['sensible'] hourly_loads['people_latent'][hour] += 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] += 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] += equipment_load['sensible'] hourly_loads['equipment_latent'][hour] += 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] += infiltration_load['sensible'] hourly_loads['infiltration_latent'][hour] += 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] += ventilation_load['sensible'] hourly_loads['ventilation_latent'][hour] += ventilation_load['latent'] return hourly_loads except Exception as 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 self.hours: 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 = self.hours[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: 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: 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., '40N') Returns: Cooling load in Watts """ try: cltd = self.ashrae_tables.calculate_corrected_cltd_wall( wall_group=wall.wall_group, orientation=wall.orientation.value, hour=hour, color='Dark', month=month, latitude=latitude, indoor_temp=indoor_temp, outdoor_temp=outdoor_temp ) load = wall.u_value * wall.area * cltd return max(load, 0.0) except Exception as 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., '40N') Returns: Cooling load in Watts """ try: cltd = self.ashrae_tables.calculate_corrected_cltd_roof( roof_group=roof.roof_group, hour=hour, color='Dark', month=month, latitude=latitude, indoor_temp=indoor_temp, outdoor_temp=outdoor_temp ) load = roof.u_value * roof.area * cltd return max(load, 0.0) except Exception as 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., '40N') shading_coefficient: Shading coefficient for drapery Returns: Dictionary with conduction and solar loads in Watts """ try: # Conduction load delta_t = outdoor_temp - indoor_temp conduction_load = window.u_value * window.area * delta_t # Solar load solar_altitude = self.heat_transfer.solar.solar_altitude scl_latitude = f"{float(latitude[:-1])}_{month.upper()}" scl = self.ashrae_tables.get_scl( orientation=window.orientation.value, hour=hour, latitude=scl_latitude ) solar_load = window.area * window.shgc * scl * shading_coefficient return { 'conduction': max(conduction_load, 0.0), 'solar': max(solar_load, 0.0), 'total': max(conduction_load + solar_load, 0.0) } except Exception as 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: delta_t = outdoor_temp - indoor_temp load = door.u_value * door.area * delta_t return max(load, 0.0) except Exception as 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 hour: Hour of the day Returns: Dictionary with sensible and latent loads in Watts """ try: sensible_gain = { 'Seated/Resting': 70, 'Light Work': 100, 'Moderate Work': 150, 'Heavy Work': 200 }.get(activity_level, 70) latent_gain = { 'Seated/Resting': 45, 'Light Work': 75, 'Moderate Work': 120, 'Heavy Work': 180 }.get(activity_level, 45) clf = self.ashrae_tables.get_clf_people(hour, '8h') sensible_load = num_people * sensible_gain * clf latent_load = num_people * latent_gain return { 'sensible': max(sensible_load, 0.0), 'latent': max(latent_load, 0.0), 'total': max(sensible_load + latent_load, 0.0) } except Exception as 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: Lighting power (W) use_factor: Usage factor special_allowance: Special allowance factor hour: Hour of the day Returns: Cooling load in Watts """ try: clf = self.ashrae_tables.get_clf_lights(hour, '8h') load = power * use_factor * special_allowance * clf return max(load, 0.0) except Exception as 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: Equipment power (W) use_factor: Usage factor radiation_factor: Radiation factor hour: Hour of the day Returns: Dictionary with sensible and latent loads in Watts """ try: clf = self.ashrae_tables.get_clf_equipment(hour, '8h') sensible_load = power * use_factor * radiation_factor * clf latent_load = power * use_factor * (1 - radiation_factor) return { 'sensible': max(sensible_load, 0.0), 'latent': max(latent_load, 0.0), 'total': max(sensible_load + latent_load, 0.0) } except Exception as 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: air_changes_per_hour = (flow_rate * 3600) / building_volume sensible_load = 1.2 * flow_rate * 1000 * (outdoor_temp - indoor_temp) # Calculate humidity ratio difference outdoor_w = self.heat_transfer.psychrometrics.calculate_humidity_ratio(outdoor_temp, outdoor_rh) indoor_w = self.heat_transfer.psychrometrics.calculate_humidity_ratio(indoor_temp, indoor_rh) latent_load = 2501 * flow_rate * 1000 * (outdoor_w - indoor_w) return { 'sensible': max(sensible_load, 0.0), 'latent': max(latent_load, 0.0), 'total': max(sensible_load + latent_load, 0.0) } except Exception as 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: sensible_load = 1.2 * flow_rate * 1000 * (outdoor_temp - indoor_temp) # Calculate humidity ratio difference outdoor_w = self.heat_transfer.psychrometrics.calculate_humidity_ratio(outdoor_temp, outdoor_rh) indoor_w = self.heat_transfer.psychrometrics.calculate_humidity_ratio(indoor_temp, indoor_rh) latent_load = 2501 * flow_rate * 1000 * (outdoor_w - indoor_w) return { 'sensible': max(sensible_load, 0.0), 'latent': max(latent_load, 0.0), 'total': max(sensible_load + latent_load, 0.0) } except Exception as e: raise Exception(f"Error in calculate_ventilation_cooling_load: {str(e)}") if __name__ == "__main__": # Example usage for testing calculator = CoolingLoadCalculator() # Dummy inputs building_components = { 'walls': [Wall( name="North Wall", orientation=Orientation.NORTH, area=20.0, u_value=0.5, wall_group="A" )], 'roofs': [Roof( name="Main Roof", orientation=Orientation.HORIZONTAL, area=100.0, u_value=0.3, roof_group="1" )], 'windows': [Window( name="South Window", orientation=Orientation.SOUTH, area=10.0, u_value=2.8, shgc=0.7, shading_device="Medium drapery", shading_coefficient=0.8 )], 'doors': [Door( name="Main Door", orientation=Orientation.NORTH, area=2.0, u_value=2.0 )] } outdoor_conditions = { 'temperature': 35.0, 'relative_humidity': 50.0, 'ground_temperature': 20.0, 'month': 'Jul', 'latitude': '31.973N', 'wind_speed': 4.0, 'day_of_year': 204 } indoor_conditions = { 'temperature': 24.0, 'relative_humidity': 50.0 } internal_loads = { 'people': { 'number': 10, 'activity_level': 'Seated/Resting', 'operating_hours': '8:00-18:00' }, 'lights': { 'power': 1000.0, 'use_factor': 0.8, 'special_allowance': 0.1, 'hours_operation': '8h' }, 'equipment': { 'power': 500.0, 'use_factor': 0.7, 'radiation_factor': 0.3, 'hours_operation': '8h' }, 'infiltration': { 'flow_rate': 0.05, 'height': 3.0, 'crack_length': 10.0 }, 'ventilation': { 'flow_rate': 0.1 }, 'operating_hours': '8:00-18:00' } building_volume = 300.0 # Calculate loads hourly_loads = calculator.calculate_hourly_cooling_loads( building_components=building_components, outdoor_conditions=outdoor_conditions, indoor_conditions=indoor_conditions, internal_loads=internal_loads, building_volume=building_volume ) design_loads = calculator.calculate_design_cooling_load(hourly_loads) summary = calculator.calculate_cooling_load_summary(design_loads) print("Cooling Load Summary:", summary)