HVAC-03 / utils /cooling_load.py
mabuseif's picture
Update utils/cooling_load.py
6a3b4aa verified
"""
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}")