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, 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}") |