""" Cooling load calculation module for HVAC Load Calculator. This module implements the CLTD/CLF method for calculating cooling loads. """ from typing import Dict, List, Any, Optional, Tuple import math import numpy as np import pandas as pd import os from datetime import datetime, timedelta from enum import Enum # Import data models and utilities from data.building_components import Wall, Roof, Floor, Window, Door, Orientation, ComponentType from data.ashrae_tables import ashrae_tables from utils.psychrometrics import Psychrometrics from utils.heat_transfer import HeatTransfer # Define paths DATA_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) class CoolingLoad: """Class for calculating cooling loads using the CLTD/CLF method.""" def __init__(self): """Initialize cooling load calculator.""" self.heat_transfer = HeatTransfer() self.psychrometrics = Psychrometrics() self.ashrae_tables = ashrae_tables def calculate_wall_cooling_load(self, wall: Wall, outdoor_temp: float, indoor_temp: float, month: str, hour: int, latitude: str = "40N", color: str = "Dark") -> float: """ Calculate cooling load through a wall using the CLTD method. Args: wall: Wall object outdoor_temp: Outdoor temperature in °C indoor_temp: Indoor temperature in °C month: Month (Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec) hour: Hour of the day (0-23) latitude: Latitude (24N, 32N, 40N, 48N, 56N) color: Surface color (Dark, Medium, Light) Returns: Cooling load in W """ # Get wall properties u_value = wall.u_value area = wall.area orientation = wall.orientation.value wall_group = wall.wall_group # Calculate corrected CLTD cltd = self.ashrae_tables.calculate_corrected_cltd_wall( wall_group=wall_group, orientation=orientation, hour=hour, color=color, month=month, latitude=latitude, indoor_temp=indoor_temp, outdoor_temp=outdoor_temp ) # Calculate cooling load cooling_load = u_value * area * cltd return cooling_load def calculate_roof_cooling_load(self, roof: Roof, outdoor_temp: float, indoor_temp: float, month: str, hour: int, latitude: str = "40N", color: str = "Dark") -> float: """ Calculate cooling load through a roof using the CLTD method. Args: roof: Roof object outdoor_temp: Outdoor temperature in °C indoor_temp: Indoor temperature in °C month: Month (Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec) hour: Hour of the day (0-23) latitude: Latitude (24N, 32N, 40N, 48N, 56N) color: Surface color (Dark, Medium, Light) Returns: Cooling load in W """ # Get roof properties u_value = roof.u_value area = roof.area roof_group = roof.roof_group # Calculate corrected CLTD cltd = self.ashrae_tables.calculate_corrected_cltd_roof( roof_group=roof_group, hour=hour, color=color, month=month, latitude=latitude, indoor_temp=indoor_temp, outdoor_temp=outdoor_temp ) # Calculate cooling load cooling_load = u_value * area * cltd return cooling_load def calculate_window_cooling_load(self, window: Window, outdoor_temp: float, indoor_temp: float, month: str, hour: int, latitude: str = "40N_JUL", shading_coefficient: float = 1.0) -> Dict[str, float]: """ Calculate cooling load through a window using the CLTD/SCL method. Args: window: Window object outdoor_temp: Outdoor temperature in °C indoor_temp: Indoor temperature in °C month: Month (Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec) hour: Hour of the day (0-23) latitude: Latitude and month key (default: "40N_JUL") shading_coefficient: Shading coefficient (0-1) Returns: Dictionary with conduction, solar, and total cooling loads in W """ # Get window properties u_value = window.u_value area = window.area orientation = window.orientation.value shgc = window.shgc # Calculate conduction cooling load delta_t = outdoor_temp - indoor_temp conduction_load = u_value * area * delta_t # Calculate solar cooling load scl = self.ashrae_tables.get_scl(orientation, hour, latitude) solar_load = area * shgc * shading_coefficient * scl # Calculate total cooling load total_load = conduction_load + solar_load return { "conduction": conduction_load, "solar": solar_load, "total": total_load } def calculate_door_cooling_load(self, door: Door, outdoor_temp: float, indoor_temp: float) -> float: """ Calculate cooling load through a door using simple conduction. Args: door: Door object outdoor_temp: Outdoor temperature in °C indoor_temp: Indoor temperature in °C Returns: Cooling load in W """ # Get door properties u_value = door.u_value area = door.area # Calculate cooling load delta_t = outdoor_temp - indoor_temp cooling_load = u_value * area * delta_t return cooling_load def calculate_floor_cooling_load(self, floor: Floor, ground_temp: float, indoor_temp: float) -> float: """ Calculate cooling load through a floor. Args: floor: Floor object ground_temp: Ground or adjacent space temperature in °C indoor_temp: Indoor temperature in °C Returns: Cooling load in W """ # Get floor properties u_value = floor.u_value area = floor.area # Calculate cooling load delta_t = ground_temp - indoor_temp cooling_load = u_value * area * delta_t # Return positive value for heat gain, zero for heat loss return max(0, cooling_load) def calculate_infiltration_cooling_load(self, flow_rate: float, outdoor_temp: float, indoor_temp: float, outdoor_rh: float, indoor_rh: float) -> Dict[str, float]: """ Calculate sensible and latent cooling loads due to infiltration. Args: flow_rate: Infiltration flow rate in m³/s outdoor_temp: Outdoor temperature in °C indoor_temp: Indoor temperature in °C outdoor_rh: Outdoor relative humidity in % indoor_rh: Indoor relative humidity in % Returns: Dictionary with sensible, latent, and total cooling loads in W """ # Calculate sensible cooling load sensible_load = self.heat_transfer.infiltration_heat_transfer( flow_rate=flow_rate, delta_t=outdoor_temp - indoor_temp ) # Calculate humidity ratios outdoor_w = self.psychrometrics.relative_humidity_to_humidity_ratio( temperature=outdoor_temp, relative_humidity=outdoor_rh ) indoor_w = self.psychrometrics.relative_humidity_to_humidity_ratio( temperature=indoor_temp, relative_humidity=indoor_rh ) # Calculate latent cooling load latent_load = self.heat_transfer.infiltration_latent_heat_transfer( flow_rate=flow_rate, delta_w=outdoor_w - indoor_w ) # Calculate total cooling load total_load = sensible_load + latent_load return { "sensible": sensible_load, "latent": latent_load, "total": total_load } def calculate_ventilation_cooling_load(self, flow_rate: float, outdoor_temp: float, indoor_temp: float, outdoor_rh: float, indoor_rh: float) -> Dict[str, float]: """ Calculate sensible and latent cooling loads due to ventilation. Args: flow_rate: Ventilation flow rate in m³/s outdoor_temp: Outdoor temperature in °C indoor_temp: Indoor temperature in °C outdoor_rh: Outdoor relative humidity in % indoor_rh: Indoor relative humidity in % Returns: Dictionary with sensible, latent, and total cooling loads in W """ # Ventilation load calculation is similar to infiltration return self.calculate_infiltration_cooling_load( flow_rate=flow_rate, outdoor_temp=outdoor_temp, indoor_temp=indoor_temp, outdoor_rh=outdoor_rh, indoor_rh=indoor_rh ) def calculate_people_cooling_load(self, num_people: int, activity_level: str, hour: int) -> Dict[str, float]: """ Calculate sensible and latent cooling loads due to people. Args: num_people: Number of people activity_level: Activity level (Seated/Resting, Light Work, Medium Work, Heavy Work) hour: Hour of the day (0-23) Returns: Dictionary with sensible, latent, and total cooling loads in W """ # Get heat gain values based on activity level if activity_level == "Seated/Resting": sensible_gain_per_person = 70 # W latent_gain_per_person = 45 # W elif activity_level == "Light Work": sensible_gain_per_person = 75 # W latent_gain_per_person = 55 # W elif activity_level == "Medium Work": sensible_gain_per_person = 75 # W latent_gain_per_person = 115 # W elif activity_level == "Heavy Work": sensible_gain_per_person = 80 # W latent_gain_per_person = 175 # W else: # Default to light work sensible_gain_per_person = 75 # W latent_gain_per_person = 55 # W # Calculate instantaneous heat gains instantaneous_sensible = num_people * sensible_gain_per_person instantaneous_latent = num_people * latent_gain_per_person # Apply CLF for sensible load clf = self.ashrae_tables.get_clf_people(hour) sensible_load = instantaneous_sensible * clf # Latent load doesn't use CLF latent_load = instantaneous_latent # Calculate total cooling load total_load = sensible_load + latent_load return { "sensible": sensible_load, "latent": latent_load, "total": total_load } def calculate_lighting_cooling_load(self, power: float, usage_factor: float, special_allowance_factor: float, hour: int) -> float: """ Calculate cooling load due to lighting. Args: power: Lighting power in W usage_factor: Usage factor (0-1) special_allowance_factor: Special allowance factor for fixtures (1.0 for normal fixtures) hour: Hour of the day (0-23) Returns: Cooling load in W """ # Calculate instantaneous heat gain instantaneous_gain = power * usage_factor * special_allowance_factor # Apply CLF clf = self.ashrae_tables.get_clf_lights(hour) cooling_load = instantaneous_gain * clf return cooling_load def calculate_equipment_cooling_load(self, power: float, usage_factor: float, radiation_factor: float, hour: int) -> Dict[str, float]: """ Calculate sensible and latent cooling loads due to equipment. Args: power: Equipment power in W usage_factor: Usage factor (0-1) radiation_factor: Radiation factor (0-1) hour: Hour of the day (0-23) Returns: Dictionary with sensible, latent, and total cooling loads in W """ # Calculate instantaneous heat gain instantaneous_gain = power * usage_factor # Split into radiant and convective components radiant_gain = instantaneous_gain * radiation_factor convective_gain = instantaneous_gain * (1 - radiation_factor) # Apply CLF to radiant component clf = self.ashrae_tables.get_clf_equipment(hour) radiant_load = radiant_gain * clf # Convective component doesn't use CLF convective_load = convective_gain # Calculate total sensible load sensible_load = radiant_load + convective_load # Most equipment doesn't have latent load latent_load = 0 # Calculate total cooling load total_load = sensible_load + latent_load return { "sensible": sensible_load, "latent": latent_load, "total": total_load } def calculate_total_cooling_load(self, building_components: Dict[str, List[Any]], building_info: Dict[str, Any], climate_data: Dict[str, Any], internal_loads: Dict[str, Any], hour: int = 15) -> Dict[str, Any]: """ Calculate total cooling load for a building. Args: building_components: Dictionary with lists of building components building_info: Dictionary with building information climate_data: Dictionary with climate data internal_loads: Dictionary with internal loads information hour: Design hour (default: 15, which is 3 PM) Returns: Dictionary with detailed cooling load results """ # Initialize results dictionary results = { "walls": 0, "roofs": 0, "floors": 0, "windows_conduction": 0, "windows_solar": 0, "doors": 0, "infiltration_sensible": 0, "infiltration_latent": 0, "ventilation_sensible": 0, "ventilation_latent": 0, "people_sensible": 0, "people_latent": 0, "lights": 0, "equipment_sensible": 0, "equipment_latent": 0, "detailed_loads": { "walls": [], "roofs": [], "floors": [], "windows": [], "doors": [] } } # Get design conditions outdoor_temp = climate_data.get("design_cooling_temp", 35) indoor_temp = building_info.get("indoor_cooling_temp", 24) outdoor_rh = climate_data.get("design_cooling_rh", 50) indoor_rh = building_info.get("indoor_cooling_rh", 50) month = climate_data.get("design_cooling_month", "Jul") latitude = climate_data.get("latitude", "40N") # Calculate wall cooling loads for wall in building_components.get("walls", []): wall_load = self.calculate_wall_cooling_load( wall=wall, outdoor_temp=outdoor_temp, indoor_temp=indoor_temp, month=month, hour=hour, latitude=latitude ) results["walls"] += wall_load # Add detailed load results["detailed_loads"]["walls"].append({ "name": wall.name, "orientation": wall.orientation.value, "area": wall.area, "u_value": wall.u_value, "cltd": (wall_load / (wall.area * wall.u_value)) if (wall.area * wall.u_value) > 0 else 0, "load": wall_load / 1000 # Convert to kW }) # Calculate roof cooling loads for roof in building_components.get("roofs", []): roof_load = self.calculate_roof_cooling_load( roof=roof, outdoor_temp=outdoor_temp, indoor_temp=indoor_temp, month=month, hour=hour, latitude=latitude ) results["roofs"] += roof_load # Add detailed load results["detailed_loads"]["roofs"].append({ "name": roof.name, "orientation": "Horizontal", "area": roof.area, "u_value": roof.u_value, "cltd": (roof_load / (roof.area * roof.u_value)) if (roof.area * roof.u_value) > 0 else 0, "load": roof_load / 1000 # Convert to kW }) # Calculate floor cooling loads for floor in building_components.get("floors", []): ground_temp = climate_data.get("ground_temp", 15) floor_load = self.calculate_floor_cooling_load( floor=floor, ground_temp=ground_temp, indoor_temp=indoor_temp ) results["floors"] += floor_load # Add detailed load results["detailed_loads"]["floors"].append({ "name": floor.name, "area": floor.area, "u_value": floor.u_value, "delta_t": ground_temp - indoor_temp, "load": floor_load / 1000 # Convert to kW }) # Calculate window cooling loads for window in building_components.get("windows", []): window_load = self.calculate_window_cooling_load( window=window, outdoor_temp=outdoor_temp, indoor_temp=indoor_temp, month=month, hour=hour, latitude=f"{latitude}_{month.upper()[:3]}", shading_coefficient=window.shading_coefficient ) results["windows_conduction"] += window_load["conduction"] results["windows_solar"] += window_load["solar"] # Add detailed load results["detailed_loads"]["windows"].append({ "name": window.name, "orientation": window.orientation.value, "area": window.area, "u_value": window.u_value, "shgc": window.shgc, "scl": window_load["solar"] / (window.area * window.shgc * window.shading_coefficient) if (window.area * window.shgc * window.shading_coefficient) > 0 else 0, "conduction_load": window_load["conduction"] / 1000, # Convert to kW "solar_load": window_load["solar"] / 1000, # Convert to kW "load": window_load["total"] / 1000 # Convert to kW }) # Calculate door cooling loads for door in building_components.get("doors", []): door_load = self.calculate_door_cooling_load( door=door, outdoor_temp=outdoor_temp, indoor_temp=indoor_temp ) results["doors"] += door_load # Add detailed load results["detailed_loads"]["doors"].append({ "name": door.name, "orientation": door.orientation.value, "area": door.area, "u_value": door.u_value, "delta_t": outdoor_temp - indoor_temp, "load": door_load / 1000 # Convert to kW }) # Calculate infiltration cooling load if "infiltration" in building_info: infiltration_info = building_info["infiltration"] # Convert ACH to flow rate if "ach" in infiltration_info: volume = building_info.get("volume", building_info.get("floor_area", 100) * 3) # Assume 3m ceiling height if not specified flow_rate = self.heat_transfer.air_exchange_rate_to_flow_rate( ach=infiltration_info["ach"], volume=volume ) else: flow_rate = infiltration_info.get("flow_rate", 0.05) # Default to 0.05 m³/s infiltration_load = self.calculate_infiltration_cooling_load( flow_rate=flow_rate, outdoor_temp=outdoor_temp, indoor_temp=indoor_temp, outdoor_rh=outdoor_rh, indoor_rh=indoor_rh ) results["infiltration_sensible"] += infiltration_load["sensible"] results["infiltration_latent"] += infiltration_load["latent"] # Calculate ventilation cooling load if "ventilation" in building_info: ventilation_info = building_info["ventilation"] # Convert ACH to flow rate if "ach" in ventilation_info: volume = building_info.get("volume", building_info.get("floor_area", 100) * 3) # Assume 3m ceiling height if not specified flow_rate = self.heat_transfer.air_exchange_rate_to_flow_rate( ach=ventilation_info["ach"], volume=volume ) else: flow_rate = ventilation_info.get("flow_rate", 0.1) # Default to 0.1 m³/s ventilation_load = self.calculate_ventilation_cooling_load( flow_rate=flow_rate, outdoor_temp=outdoor_temp, indoor_temp=indoor_temp, outdoor_rh=outdoor_rh, indoor_rh=indoor_rh ) results["ventilation_sensible"] += ventilation_load["sensible"] results["ventilation_latent"] += ventilation_load["latent"] # Calculate people cooling load if "people" in internal_loads: people_info = internal_loads["people"] num_people = people_info.get("count", 1) activity_level = people_info.get("activity", "Light Work") people_load = self.calculate_people_cooling_load( num_people=num_people, activity_level=activity_level, hour=hour ) results["people_sensible"] += people_load["sensible"] results["people_latent"] += people_load["latent"] # Calculate lighting cooling load if "lighting" in internal_loads: lighting_info = internal_loads["lighting"] power = lighting_info.get("power", 0) usage_factor = lighting_info.get("usage_factor", 1.0) special_allowance_factor = lighting_info.get("special_allowance_factor", 1.0) lighting_load = self.calculate_lighting_cooling_load( power=power, usage_factor=usage_factor, special_allowance_factor=special_allowance_factor, hour=hour ) results["lights"] += lighting_load # Calculate equipment cooling load if "equipment" in internal_loads: equipment_info = internal_loads["equipment"] power = equipment_info.get("power", 0) usage_factor = equipment_info.get("usage_factor", 1.0) radiation_factor = equipment_info.get("radiation_factor", 0.3) equipment_load = self.calculate_equipment_cooling_load( power=power, usage_factor=usage_factor, radiation_factor=radiation_factor, hour=hour ) results["equipment_sensible"] += equipment_load["sensible"] results["equipment_latent"] += equipment_load["latent"] # Calculate subtotals envelope_load = ( results["walls"] + results["roofs"] + results["floors"] + results["windows_conduction"] + results["windows_solar"] + results["doors"] ) infiltration_load = results["infiltration_sensible"] + results["infiltration_latent"] ventilation_load = results["ventilation_sensible"] + results["ventilation_latent"] people_load = results["people_sensible"] + results["people_latent"] equipment_load = results["equipment_sensible"] + results["equipment_latent"] internal_load = people_load + results["lights"] + equipment_load # Calculate totals sensible_load = ( envelope_load + results["infiltration_sensible"] + results["ventilation_sensible"] + results["people_sensible"] + results["lights"] + results["equipment_sensible"] ) latent_load = ( results["infiltration_latent"] + results["ventilation_latent"] + results["people_latent"] + results["equipment_latent"] ) total_load = sensible_load + latent_load # Calculate sensible heat ratio sensible_heat_ratio = sensible_load / total_load if total_load > 0 else 1.0 # Add subtotals and totals to results results.update({ "envelope": envelope_load, "infiltration": infiltration_load, "ventilation": ventilation_load, "internal": internal_load, "sensible": sensible_load, "latent": latent_load, "total": total_load, "sensible_heat_ratio": sensible_heat_ratio }) # Add per area metrics floor_area = building_info.get("floor_area", 100) # Default to 100 m² results["load_per_area"] = total_load / floor_area if floor_area > 0 else 0 return results