""" ASHRAE tables module for HVAC Load Calculator. Integrates CLTD, SCL, CLF tables, cooling load calculations, climatic corrections, and visualization. Combines data from original ashrae_tables.py and enhanced versions with ashrae_tables (3).py. """ from typing import Dict, List, Any, Optional, Tuple import pandas as pd import numpy as np import os import matplotlib.pyplot as plt from enum import Enum # Define paths DATA_DIR = os.path.dirname(os.path.abspath(__file__)) class WallGroup(Enum): """Enumeration for ASHRAE wall groups.""" A = "A" # Light construction B = "B" C = "C" D = "D" E = "E" F = "F" G = "G" H = "H" # Heavy construction class RoofGroup(Enum): """Enumeration for ASHRAE roof groups.""" A = "A" # Light construction B = "B" C = "C" D = "D" E = "E" F = "F" G = "G" # Heavy construction class Orientation(Enum): """Enumeration for building component orientations.""" N = "North" NE = "Northeast" E = "East" SE = "Southeast" S = "South" SW = "Southwest" W = "West" NW = "Northwest" HOR = "Horizontal" # For roofs and floors class ASHRAETables: """Class for managing ASHRAE tables for load calculations.""" def __init__(self): """Initialize ASHRAE tables.""" # Load tables self.cltd_wall = self._load_cltd_wall_table() self.cltd_roof = self._load_cltd_roof_table() self.scl = self._load_scl_table() self.clf_lights = self._load_clf_lights_table() self.clf_people = self._load_clf_people_table() self.clf_equipment = self._load_clf_equipment_table() self.heat_gain = self._load_heat_gain_table() # Load correction factors self.latitude_correction = self._load_latitude_correction() self.color_correction = self._load_color_correction() self.month_correction = self._load_month_correction() # Load thermal properties and roof classifications self.thermal_properties = self._load_thermal_properties() self.roof_classifications = self._load_roof_classifications() def _validate_cltd_inputs(self, group: str, orientation: str, hour: int, latitude: str, month: str, color: str, is_wall: bool = True) -> Tuple[bool, str]: """Validate inputs for CLTD calculations.""" valid_groups = [e.value for e in WallGroup] if is_wall else [e.value for e in RoofGroup] valid_orientations = [e.value for e in Orientation] valid_latitudes = ['24N', '32N', '40N', '48N', '56N'] valid_months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] valid_colors = ['Dark', 'Medium', 'Light'] if group not in valid_groups: return False, f"Invalid {'wall' if is_wall else 'roof'} group: {group}. Valid groups: {valid_groups}" if orientation not in valid_orientations: return False, f"Invalid orientation: {orientation}. Valid orientations: {valid_orientations}" if hour not in range(24): return False, "Hour must be between 0 and 23." # Handle numeric latitude values and ensure comprehensive mapping if latitude not in valid_latitudes: # Try to convert numeric latitude to standard format try: # First, handle string representations that might contain direction indicators if isinstance(latitude, str): # Extract numeric part, removing 'N' or 'S' lat_str = latitude.upper().strip() num_part = ''.join(c for c in lat_str if c.isdigit() or c == '.') lat_val = float(num_part) # Adjust for southern hemisphere if needed if 'S' in lat_str: lat_val = -lat_val else: # Handle direct numeric input lat_val = float(latitude) # Take absolute value for mapping purposes abs_lat = abs(lat_val) # Map to the closest standard latitude if abs_lat < 28: mapped_latitude = '24N' elif abs_lat < 36: mapped_latitude = '32N' elif abs_lat < 44: mapped_latitude = '40N' elif abs_lat < 52: mapped_latitude = '48N' else: mapped_latitude = '56N' # Use the mapped latitude for validation latitude = mapped_latitude except (ValueError, TypeError): return False, f"Invalid latitude: {latitude}. Valid latitudes: {valid_latitudes}" if latitude not in valid_latitudes: return False, f"Invalid latitude: {latitude}. Valid latitudes: {valid_latitudes}" if month not in valid_months: return False, f"Invalid month: {month}. Valid months: {valid_months}" if color not in valid_colors: return False, f"Invalid color: {color}. Valid colors: {valid_colors}" return True, "Valid inputs." def _load_cltd_wall_table(self) -> Dict[str, pd.DataFrame]: """ Load CLTD tables for walls at 24°N (July). Returns: Dictionary of DataFrames with CLTD values for each wall group. """ hours = list(range(24)) # CLTD data for wall types 1-12 mapped to groups A-H wall_data = { "A": { # Type 1: Lightest construction 'N': [1, 0, -1, -2, -3, -2, 5, 13, 17, 18, 19, 22, 26, 28, 30, 32, 34, 34, 27, 17, 11, 7, 5, 3], 'NE': [1, 0, -1, -2, -3, 0, 17, 39, 51, 53, 48, 39, 32, 30, 30, 30, 30, 28, 24, 18, 13, 10, 7, 5], 'E': [1, 0, -1, -2, -3, 0, 18, 44, 59, 63, 59, 48, 36, 32, 31, 30, 32, 32, 29, 24, 19, 13, 10, 7], 'SE': [1, 0, -1, -2, -3, -2, 8, 25, 38, 44, 45, 42, 35, 32, 31, 30, 32, 32, 27, 24, 18, 13, 10, 7], 'S': [1, 0, -1, -2, -3, -3, -1, 3, 8, 12, 18, 24, 29, 31, 31, 30, 32, 32, 27, 23, 18, 13, 9, 7], 'SW': [1, 0, 1, 2, 3, 3, 1, 3, 8, 13, 17, 22, 27, 42, 59, 73, 30, 32, 27, 23, 18, 20, 12, 8], 'W': [2, 0, 2, 2, 3, 1, 3, 8, 13, 17, 22, 27, 42, 59, 73, 30, 32, 27, 23, 18, 20, 12, 8, 5], 'NW': [2, 0, 1, 2, 2, 3, 1, 3, 8, 13, 17, 22, 27, 42, 59, 73, 30, 32, 27, 23, 18, 20, 12, 8] }, "B": { # Type 2 'N': [2, 1, 0, -1, -2, -1, 6, 14, 18, 19, 20, 23, 27, 29, 31, 33, 35, 35, 28, 18, 12, 8, 6, 4], 'NE': [2, 1, 0, -1, -2, 1, 18, 40, 52, 54, 49, 40, 33, 31, 31, 31, 31, 29, 25, 19, 14, 11, 8, 6], 'E': [2, 1, 0, -1, -2, 1, 19, 45, 60, 64, 60, 49, 37, 33, 32, 31, 33, 33, 30, 25, 20, 14, 11, 8], 'SE': [2, 1, 0, -1, -2, -1, 9, 26, 39, 45, 46, 43, 36, 33, 32, 31, 33, 33, 28, 25, 19, 14, 11, 8], 'S': [2, 1, 0, -1, -2, -2, 0, 4, 9, 13, 19, 25, 30, 32, 32, 31, 33, 33, 28, 24, 19, 14, 10, 8], 'SW': [2, 1, 2, 3, 4, 4, 2, 4, 9, 14, 18, 23, 28, 43, 60, 74, 31, 33, 28, 24, 19, 21, 13, 9], 'W': [3, 1, 3, 3, 4, 2, 4, 9, 14, 18, 23, 28, 43, 60, 74, 31, 33, 28, 24, 19, 21, 13, 9, 6], 'NW': [3, 1, 2, 3, 3, 4, 2, 4, 9, 14, 18, 23, 28, 43, 60, 74, 31, 33, 28, 24, 19, 21, 13, 9] }, "C": { # Type 3 'N': [3, 2, 1, 0, -1, 0, 7, 15, 19, 20, 21, 24, 28, 30, 32, 34, 36, 36, 29, 19, 13, 9, 7, 5], 'NE': [3, 2, 1, 0, -1, 2, 19, 41, 53, 55, 50, 41, 34, 32, 32, 32, 32, 30, 26, 20, 15, 12, 9, 7], 'E': [3, 2, 1, 0, -1, 2, 20, 46, 61, 65, 61, 50, 38, 34, 33, 32, 34, 34, 31, 26, 21, 15, 12, 9], 'SE': [3, 2, 1, 0, -1, 0, 10, 27, 40, 46, 47, 44, 37, 34, 33, 32, 34, 34, 29, 26, 20, 15, 12, 9], 'S': [3, 2, 1, 0, -1, -1, 1, 5, 10, 14, 20, 26, 31, 33, 33, 32, 34, 34, 29, 25, 20, 15, 11, 9], 'SW': [3, 2, 3, 4, 5, 5, 3, 5, 10, 15, 19, 24, 29, 44, 61, 75, 32, 34, 29, 25, 20, 22, 14, 10], 'W': [4, 2, 4, 4, 5, 3, 5, 10, 15, 19, 24, 29, 44, 61, 75, 32, 34, 29, 25, 20, 22, 14, 10, 7], 'NW': [4, 2, 3, 4, 4, 5, 3, 5, 10, 15, 19, 24, 29, 44, 61, 75, 32, 34, 29, 25, 20, 22, 14, 10] }, "D": { # Type 4 'N': [4, 3, 2, 1, 0, 1, 8, 16, 20, 21, 22, 25, 29, 31, 33, 35, 37, 37, 30, 20, 14, 10, 8, 6], 'NE': [4, 3, 2, 1, 0, 3, 20, 42, 54, 56, 51, 42, 35, 33, 33, 33, 33, 31, 27, 21, 16, 13, 10, 8], 'E': [4, 3, 2, 1, 0, 3, 21, 47, 62, 66, 62, 51, 39, 35, 34, 33, 35, 35, 32, 27, 22, 16, 13, 10], 'SE': [4, 3, 2, 1, 0, 1, 11, 28, 41, 47, 48, 45, 38, 35, 34, 33, 35, 35, 30, 27, 21, 16, 13, 10], 'S': [4, 3, 2, 1, 0, 0, 2, 6, 11, 15, 21, 27, 32, 34, 34, 33, 35, 35, 30, 26, 21, 16, 12, 10], 'SW': [4, 3, 4, 5, 6, 6, 4, 6, 11, 16, 20, 25, 30, 45, 62, 76, 33, 35, 30, 26, 21, 23, 15, 11], 'W': [5, 3, 5, 5, 6, 4, 6, 11, 16, 20, 25, 30, 45, 62, 76, 33, 35, 30, 26, 21, 23, 15, 11, 8], 'NW': [5, 3, 4, 5, 5, 6, 4, 6, 11, 16, 20, 25, 30, 45, 62, 76, 33, 35, 30, 26, 21, 23, 15, 11] }, "E": { # Type 5 'N': [13, 11, 9, 7, 5, 3, 2, 3, 5, 7, 10, 12, 14, 16, 19, 21, 23, 25, 27, 27, 25, 22, 20, 16], 'NE': [13, 11, 8, 7, 5, 3, 3, 6, 12, 20, 26, 31, 33, 33, 32, 32, 32, 33, 31, 29, 27, 24, 21, 18], 'E': [14, 11, 9, 7, 5, 4, 3, 6, 13, 22, 31, 36, 39, 39, 39, 39, 39, 31, 31, 29, 26, 22, 19, 18], 'SE': [13, 10, 8, 6, 5, 3, 2, 4, 8, 14, 20, 25, 28, 30, 30, 30, 30, 30, 28, 26, 24, 21, 18, 16], 'S': [11, 9, 7, 6, 4, 3, 2, 1, 1, 3, 5, 7, 11, 14, 16, 20, 22, 23, 23, 23, 20, 18, 16, 14], 'SW': [18, 15, 12, 9, 7, 5, 3, 3, 3, 4, 5, 8, 11, 14, 16, 20, 26, 32, 33, 31, 41, 40, 36, 31], 'W': [23, 19, 15, 12, 9, 7, 5, 4, 4, 4, 6, 8, 11, 14, 16, 20, 28, 37, 35, 31, 51, 41, 41, 41], 'NW': [21, 17, 14, 11, 8, 6, 4, 3, 3, 4, 6, 8, 11, 14, 16, 20, 28, 37, 35, 31, 41, 41, 41, 41] }, "F": { # Type 6 'N': [10, 8, 6, 4, 2, 1, 1, 2, 4, 6, 9, 11, 13, 15, 18, 20, 22, 24, 26, 26, 24, 21, 19, 15], 'NE': [10, 8, 6, 4, 2, 2, 2, 5, 11, 19, 25, 30, 32, 32, 31, 31, 31, 32, 30, 28, 26, 23, 20, 17], 'E': [11, 8, 6, 4, 2, 3, 2, 5, 12, 21, 30, 35, 38, 38, 38, 38, 38, 30, 30, 28, 25, 21, 18, 17], 'SE': [10, 7, 5, 3, 2, 2, 1, 3, 7, 13, 19, 24, 27, 29, 29, 29, 29, 29, 27, 25, 23, 20, 17, 15], 'S': [8, 6, 4, 3, 1, 2, 1, 0, 0, 2, 4, 6, 10, 13, 15, 19, 21, 22, 22, 22, 19, 17, 15, 13], 'SW': [15, 12, 9, 6, 4, 3, 2, 2, 2, 3, 4, 7, 10, 13, 15, 19, 25, 31, 32, 30, 40, 39, 35, 30], 'W': [20, 16, 12, 9, 6, 4, 3, 3, 3, 3, 5, 7, 10, 13, 15, 19, 27, 36, 34, 30, 50, 40, 40, 40], 'NW': [18, 14, 11, 8, 5, 4, 3, 2, 2, 3, 5, 7, 10, 13, 15, 19, 27, 36, 34, 30, 40, 40, 40, 40] }, "G": { # Type 7 'N': [7, 5, 3, 1, -1, 0, 0, 1, 3, 5, 8, 10, 12, 14, 17, 19, 21, 23, 25, 25, 23, 20, 18, 14], 'NE': [7, 5, 3, 1, -1, 1, 1, 4, 10, 18, 24, 29, 31, 31, 30, 30, 30, 31, 29, 27, 25, 22, 19, 16], 'E': [8, 5, 3, 1, -1, 2, 1, 4, 11, 20, 29, 34, 37, 37, 37, 37, 37, 29, 29, 27, 24, 20, 17, 16], 'SE': [7, 4, 2, 0, -1, 1, 0, 2, 6, 12, 18, 23, 26, 28, 28, 28, 28, 28, 26, 24, 22, 19, 16, 14], 'S': [5, 3, 1, 0, -2, 1, 0, -1, -1, 1, 3, 5, 9, 12, 14, 18, 20, 21, 21, 21, 18, 16, 14, 12], 'SW': [12, 9, 6, 3, 1, 2, 1, 1, 1, 2, 3, 6, 9, 12, 14, 18, 24, 30, 31, 29, 39, 38, 34, 29], 'W': [17, 13, 9, 6, 3, 2, 2, 2, 2, 2, 4, 6, 9, 12, 14, 18, 26, 35, 33, 29, 49, 39, 39, 39], 'NW': [15, 11, 8, 5, 2, 3, 2, 1, 1, 2, 4, 6, 9, 12, 14, 18, 26, 35, 33, 29, 39, 39, 39, 39] }, "H": { # Interpolated from types 8-12: Heaviest construction 'N': [4, 2, 0, -2, -4, -1, -1, 0, 2, 4, 7, 9, 11, 13, 16, 18, 20, 22, 24, 24, 22, 19, 17, 13], 'NE': [4, 2, 0, -2, -4, 0, 0, 3, 9, 17, 23, 28, 30, 30, 29, 29, 29, 30, 28, 26, 24, 21, 18, 15], 'E': [5, 2, 0, -2, -4, 1, 0, 3, 10, 19, 28, 33, 36, 36, 36, 36, 36, 28, 28, 26, 23, 19, 16, 15], 'SE': [4, 1, -1, -3, -4, 0, -1, 1, 5, 11, 17, 22, 25, 27, 27, 27, 27, 27, 25, 23, 21, 18, 15, 13], 'S': [2, 0, -2, -3, -5, 0, -1, -2, -2, 0, 2, 4, 8, 11, 13, 17, 19, 20, 20, 20, 17, 15, 13, 11], 'SW': [9, 6, 3, 0, -2, 1, 0, 0, 0, 1, 2, 5, 8, 11, 13, 17, 23, 29, 30, 28, 38, 37, 33, 28], 'W': [14, 10, 6, 3, 0, 1, 1, 1, 1, 1, 3, 5, 8, 11, 13, 17, 25, 34, 32, 28, 48, 38, 38, 38], 'NW': [12, 8, 5, 2, -1, 2, 1, 0, 0, 1, 3, 5, 8, 11, 13, 17, 25, 34, 32, 28, 38, 38, 38, 38] } } wall_groups = {group: pd.DataFrame(data, index=hours) for group, data in wall_data.items()} return wall_groups def _load_cltd_roof_table(self) -> Dict[str, pd.DataFrame]: """ Load CLTD tables for roofs at 24°N, 36°N, 48°N (July). Returns: Dictionary of DataFrames with CLTD values for each roof group and latitude. """ hours = list(range(24)) # CLTD data for roof types mapped to groups A-G across latitudes roof_data = { "24N": { "A": [0, 4, 5, 6, 6, 3, 9, 16, 44, 62, 76, 87, 92, 92, 86, 74, 58, 39, 23, 14, 8, 4, 2, 0], # Type 1 "B": [12, 8, 5, 2, 0, -2, -2, 3, 11, 22, 35, 47, 59, 68, 74, 77, 74, 68, 58, 47, 37, 29, 22, 16], # Type 3 "C": [21, 16, 12, 8, 5, 3, 1, 1, 1, 10, 19, 20, 22, 23, 49, 49, 54, 58, 58, 56, 52, 47, 42, 37], # Type 5 "D": [31, 25, 20, 16, 12, 9, 6, 4, 3, 5, 10, 17, 26, 36, 46, 54, 61, 65, 66, 63, 58, 51, 44, 47], # Type 9 "E": [34, 31, 28, 25, 22, 20, 17, 16, 15, 19, 23, 28, 29, 32, 38, 38, 43, 43, 49, 49, 49, 46, 43, 40], # Type 13 "F": [35, 32, 30, 28, 25, 23, 21, 19, 20, 22, 23, 23, 24, 25, 39, 39, 40, 40, 40, 45, 46, 46, 44, 42], # Type 14 "G": [36, 33, 31, 29, 27, 25, 23, 21, 20, 22, 24, 25, 26, 27, 40, 41, 42, 42, 42, 47, 48, 48, 45, 43] # Interpolated }, "36N": { "A": [0, 2, 4, 5, 6, 6, 12, 28, 45, 61, 75, 84, 90, 90, 84, 79, 71, 62, 66, 59, 50, 42, 47, 0], # Type 1 "B": [12, 8, 5, 2, 0, -2, -1, 14, 13, 24, 25, 26, 27, 28, 38, 39, 40, 40, 43, 45, 46, 46, 43, 40], # Type 3 "C": [21, 16, 12, 8, 5, 3, 1, 12, 15, 12, 21, 22, 23, 32, 39, 40, 40, 40, 40, 45, 46, 46, 43, 40], # Type 5 "D": [32, 26, 21, 16, 13, 10, 8, 14, 17, 19, 20, 22, 23, 24, 39, 40, 40, 40, 40, 45, 46, 46, 43, 40], # Type 9 "E": [34, 31, 28, 25, 23, 20, 18, 16, 16, 20, 22, 22, 23, 24, 39, 39, 40, 40, 40, 45, 46, 46, 44, 42], # Type 13 "F": [35, 32, 30, 28, 25, 23, 21, 19, 20, 22, 23, 23, 24, 25, 39, 39, 40, 40, 40, 45, 46, 46, 44, 42], # Type 14 "G": [36, 33, 31, 29, 27, 25, 23, 21, 20, 22, 24, 25, 26, 27, 40, 41, 42, 42, 42, 47, 48, 48, 45, 43] # Interpolated }, "48N": { "A": [0, 2, 4, 5, 6, 5, 3, 15, 29, 44, 58, 69, 78, 83, 83, 79, 71, 59, 44, 49, 49, 49, 5, 2], # Type 1 "B": [12, 8, 5, 2, 0, -1, 1, 16, 16, 20, 22, 23, 24, 25, 39, 39, 40, 40, 40, 45, 46, 46, 43, 40], # Type 3 "C": [21, 16, 12, 8, 5, 3, 2, 16, 19, 20, 22, 23, 24, 25, 39, 39, 40, 40, 40, 45, 46, 46, 43, 40], # Type 5 "D": [31, 26, 21, 16, 12, 9, 6, 5, 5, 20, 22, 23, 24, 25, 39, 39, 40, 40, 40, 45, 46, 46, 43, 40], # Type 9 "E": [33, 30, 27, 25, 22, 20, 17, 16, 16, 20, 22, 23, 24, 25, 39, 39, 40, 40, 40, 47, 48, 47, 45, 40], # Type 13 "F": [34, 32, 29, 27, 25, 23, 21, 20, 19, 20, 22, 23, 24, 25, 39, 39, 40, 40, 40, 48, 48, 48, 43, 40], # Type 14 "G": [35, 33, 31, 29, 27, 25, 23, 21, 20, 22, 24, 25, 26, 27, 40, 41, 42, 42, 42, 48, 49, 49, 45, 43] # Interpolated } } roof_groups = {} for lat, groups in roof_data.items(): for group, data in groups.items(): roof_groups[f"{group}_{lat}"] = pd.DataFrame({"HOR": data}, index=hours) return roof_groups def _load_scl_table(self) -> Dict[str, pd.DataFrame]: """ Load SCL (Solar Cooling Load) tables for windows. Returns: Dictionary of DataFrames with SCL values for each latitude/month. """ hours = list(range(24)) # Base SCL data for 40°N (July) scl_40n_jul = { "N": [11, 8, 6, 6, 6, 9, 13, 16, 19, 21, 22, 23, 23, 22, 20, 17, 14, 11, 11, 11, 11, 11, 11, 11], "NE": [11, 8, 6, 6, 6, 19, 75, 113, 121, 103, 75, 40, 31, 27, 23, 19, 14, 11, 11, 11, 11, 11, 11, 11], "E": [11, 8, 6, 6, 6, 13, 55, 159, 232, 251, 222, 157, 82, 43, 32, 24, 17, 11, 11, 11, 11, 11, 11, 11], "SE": [11, 8, 6, 6, 6, 10, 33, 98, 187, 251, 276, 264, 214, 139, 74, 37, 21, 11, 11, 11, 11, 11, 11, 11], "S": [11, 8, 6, 6, 6, 8, 14, 27, 66, 139, 209, 254, 268, 251, 203, 139, 66, 27, 14, 11, 11, 11, 11, 11], "SW": [11, 8, 6, 6, 6, 8, 14, 19, 24, 37, 74, 139, 214, 264, 276, 251, 187, 98, 33, 14, 11, 11, 11, 11], "W": [11, 8, 6, 6, 6, 8, 14, 19, 24, 32, 43, 82, 157, 222, 251, 232, 159, 55, 13, 11, 11, 11, 11, 11], "NW": [11, 8, 6, 6, 6, 8, 14, 19, 24, 27, 31, 40, 75, 103, 121, 113, 75, 19, 11, 11, 11, 11, 11, 11], "HOR": [11, 8, 6, 6, 6, 19, 69, 135, 201, 254, 290, 308, 308, 290, 254, 201, 135, 69, 19, 11, 11, 11, 11, 11] } scl_tables = {"40N_Jul": pd.DataFrame(scl_40n_jul, index=hours)} latitudes = ["24N", "32N", "40N", "48N", "56N"] months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] for lat in latitudes: for month in months: key = f"{lat}_{month}" if key == "40N_Jul": continue lat_factor = (40 - float(lat[:-1])) / 40 month_idx = months.index(month) month_factor = 1 + (month_idx - 6) / 24 scl_data = {} for orient in scl_40n_jul: base_scl = scl_40n_jul[orient] scl_data[orient] = [max(6, round(v * (1 - lat_factor * 0.2) * month_factor)) for v in base_scl] scl_tables[key] = pd.DataFrame(scl_data, index=hours) return scl_tables def _load_clf_lights_table(self) -> pd.DataFrame: """ Load CLF (Cooling Load Factor) table for lights. Returns: DataFrame with CLF values for lights by zone type and hours. """ hours = list(range(24)) clf_lights_data = { "A_8h": [0.85, 0.92, 0.95, 0.95, 0.97, 0.97, 0.98, 0.13, 0.06, 0.04, 0.03, 0.02, 0.02, 0.02, 0.02, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01], "A_10h": [0.85, 0.93, 0.95, 0.97, 0.97, 0.98, 0.98, 0.98, 0.98, 0.98, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02], "A_12h": [0.86, 0.93, 0.96, 0.97, 0.97, 0.98, 0.98, 0.98, 0.98, 0.98, 0.98, 0.98, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02], "B_8h": [0.75, 0.85, 0.90, 0.93, 0.94, 0.95, 0.95, 0.95, 0.12, 0.08, 0.05, 0.04, 0.04, 0.03, 0.03, 0.03, 0.03, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02], "B_10h": [0.75, 0.86, 0.91, 0.93, 0.94, 0.95, 0.95, 0.95, 0.96, 0.97, 0.24, 0.13, 0.08, 0.06, 0.05, 0.04, 0.04, 0.03, 0.03, 0.03, 0.03, 0.03, 0.02, 0.02], "B_12h": [0.76, 0.86, 0.91, 0.93, 0.95, 0.95, 0.95, 0.95, 0.97, 0.97, 0.97, 0.97, 0.03, 0.03, 0.03, 0.03, 0.03, 0.03, 0.03, 0.03, 0.03, 0.03, 0.03, 0.03], "C_8h": [0.70, 0.80, 0.85, 0.88, 0.90, 0.92, 0.93, 0.94, 0.10, 0.07, 0.04, 0.03, 0.03, 0.02, 0.02, 0.02, 0.02, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01], "C_10h": [0.70, 0.81, 0.86, 0.89, 0.91, 0.92, 0.93, 0.94, 0.95, 0.96, 0.20, 0.11, 0.07, 0.05, 0.04, 0.03, 0.03, 0.02, 0.02, 0.02, 0.02, 0.02, 0.01, 0.01], "C_12h": [0.71, 0.82, 0.87, 0.90, 0.92, 0.93, 0.94, 0.95, 0.96, 0.96, 0.96, 0.96, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02], "D_8h": [0.65, 0.75, 0.80, 0.83, 0.85, 0.87, 0.88, 0.89, 0.08, 0.06, 0.03, 0.02, 0.02, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01], "D_10h": [0.65, 0.76, 0.81, 0.84, 0.86, 0.88, 0.89, 0.90, 0.91, 0.92, 0.16, 0.09, 0.06, 0.04, 0.03, 0.02, 0.02, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01], "D_12h": [0.66, 0.77, 0.82, 0.85, 0.87, 0.89, 0.90, 0.91, 0.92, 0.92, 0.92, 0.92, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01] } return pd.DataFrame(clf_lights_data, index=hours) def _load_clf_people_table(self) -> pd.DataFrame: """ Load CLF (Cooling Load Factor) table for people. Returns: DataFrame with CLF values for people by zone type and hours. """ hours = list(range(24)) clf_people_data = { "A_2h": [0.75, 0.88, 0.18, 0.08, 0.04, 0.02, 0.01, 0.01, 0.01, 0.01, 0.01, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00], "A_4h": [0.75, 0.88, 0.93, 0.95, 0.97, 0.10, 0.05, 0.03, 0.02, 0.02, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00], "A_6h": [0.75, 0.88, 0.93, 0.95, 0.97, 0.97, 0.33, 0.11, 0.06, 0.04, 0.03, 0.02, 0.02, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.00, 0.00, 0.00, 0.00, 0.00], "B_2h": [0.65, 0.75, 0.81, 0.85, 0.89, 0.91, 0.93, 0.95, 0.96, 0.97, 0.98, 0.98, 0.99, 0.99, 0.99, 0.99, 0.99, 0.99, 0.99, 0.02, 0.02, 0.02, 0.02, 0.02], "B_4h": [0.65, 0.75, 0.82, 0.87, 0.90, 0.92, 0.94, 0.95, 0.96, 0.97, 0.98, 0.98, 0.99, 0.99, 0.99, 0.99, 0.99, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02], "B_6h": [0.65, 0.75, 0.82, 0.87, 0.90, 0.92, 0.94, 0.95, 0.96, 0.97, 0.98, 0.98, 0.99, 0.99, 0.99, 0.99, 0.99, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02], "C_2h": [0.60, 0.70, 0.76, 0.80, 0.84, 0.86, 0.88, 0.90, 0.91, 0.92, 0.93, 0.93, 0.94, 0.94, 0.94, 0.94, 0.94, 0.94, 0.94, 0.01, 0.01, 0.01, 0.01, 0.01], "C_4h": [0.60, 0.70, 0.77, 0.82, 0.85, 0.87, 0.89, 0.90, 0.91, 0.92, 0.93, 0.93, 0.94, 0.94, 0.94, 0.94, 0.94, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01], "C_6h": [0.60, 0.70, 0.77, 0.82, 0.85, 0.87, 0.89, 0.90, 0.91, 0.92, 0.93, 0.93, 0.94, 0.94, 0.94, 0.94, 0.94, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01], "D_2h": [0.55, 0.65, 0.71, 0.75, 0.79, 0.81, 0.83, 0.85, 0.86, 0.87, 0.88, 0.88, 0.89, 0.89, 0.89, 0.89, 0.89, 0.89, 0.89, 0.00, 0.00, 0.00, 0.00, 0.00], "D_4h": [0.55, 0.65, 0.72, 0.77, 0.80, 0.82, 0.84, 0.85, 0.86, 0.87, 0.88, 0.88, 0.89, 0.89, 0.89, 0.89, 0.89, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00], "D_6h": [0.55, 0.65, 0.72, 0.77, 0.80, 0.82, 0.84, 0.85, 0.86, 0.87, 0.88, 0.88, 0.89, 0.89, 0.89, 0.89, 0.89, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00] } return pd.DataFrame(clf_people_data, index=hours) def _load_clf_equipment_table(self) -> pd.DataFrame: """ Load CLF (Cooling Load Factor) table for equipment. Returns: DataFrame with CLF values for equipment by zone type and hours. """ hours = list(range(24)) clf_equipment_data = { "A_2h": [0.54, 0.83, 0.26, 0.11, 0.05, 0.03, 0.01, 0.01, 0.01, 0.01, 0.01, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00], "A_4h": [0.64, 0.83, 0.90, 0.93, 0.31, 0.14, 0.07, 0.04, 0.03, 0.03, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00], "A_6h": [0.64, 0.83, 0.90, 0.93, 0.95, 0.95, 0.33, 0.11, 0.06, 0.04, 0.03, 0.02, 0.02, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.00, 0.00, 0.00, 0.00, 0.00], "B_2h": [0.50, 0.75, 0.81, 0.85, 0.89, 0.91, 0.93, 0.95, 0.96, 0.97, 0.98, 0.98, 0.99, 0.99, 0.99, 0.99, 0.99, 0.99, 0.99, 0.02, 0.02, 0.02, 0.02, 0.02], "B_4h": [0.50, 0.75, 0.82, 0.87, 0.90, 0.92, 0.94, 0.95, 0.96, 0.97, 0.98, 0.98, 0.99, 0.99, 0.99, 0.99, 0.99, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02], "B_6h": [0.50, 0.75, 0.82, 0.87, 0.90, 0.92, 0.94, 0.95, 0.96, 0.97, 0.98, 0.98, 0.99, 0.99, 0.99, 0.99, 0.99, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02], "C_2h": [0.46, 0.70, 0.76, 0.80, 0.84, 0.86, 0.88, 0.90, 0.91, 0.92, 0.93, 0.93, 0.94, 0.94, 0.94, 0.94, 0.94, 0.94, 0.94, 0.01, 0.01, 0.01, 0.01, 0.01], "C_4h": [0.46, 0.70, 0.77, 0.82, 0.85, 0.87, 0.89, 0.90, 0.91, 0.92, 0.93, 0.93, 0.94, 0.94, 0.94, 0.94, 0.94, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01], "C_6h": [0.46, 0.70, 0.77, 0.82, 0.85, 0.87, 0.89, 0.90, 0.91, 0.92, 0.93, 0.93, 0.94, 0.94, 0.94, 0.94, 0.94, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01], "D_2h": [0.42, 0.65, 0.71, 0.75, 0.79, 0.81, 0.83, 0.85, 0.86, 0.87, 0.88, 0.88, 0.89, 0.89, 0.89, 0.89, 0.89, 0.89, 0.89, 0.00, 0.00, 0.00, 0.00, 0.00], "D_4h": [0.42, 0.65, 0.72, 0.77, 0.80, 0.82, 0.84, 0.85, 0.86, 0.87, 0.88, 0.88, 0.89, 0.89, 0.89, 0.89, 0.89, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00], "D_6h": [0.42, 0.65, 0.72, 0.77, 0.80, 0.82, 0.84, 0.85, 0.86, 0.87, 0.88, 0.88, 0.89, 0.89, 0.89, 0.89, 0.89, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00] } return pd.DataFrame(clf_equipment_data, index=hours) def _load_heat_gain_table(self) -> pd.DataFrame: """ Load heat gain table for internal sources. Returns: DataFrame with heat gain values (Btu/h or Btu/h-ft²). """ data = { "source": ["people_sensible", "people_latent", "lights", "equipment"], "gain": [250, 200, 3.4, 500] } return pd.DataFrame(data) def _load_thermal_properties(self) -> pd.DataFrame: """ Load thermal properties for building materials. Returns: DataFrame with U-values, R-values, and density. """ data = { "material": [ "Brick_4in", "Brick_8in", "Concrete_6in", "Concrete_12in", "Wood_1in", "Wood_2in", "Insulation_1in", "Insulation_2in", "Gypsum_0.5in", "Steel_1in" ], "U_value": [0.45, 0.32, 0.51, 0.48, 0.12, 0.08, 0.03, 0.015, 0.32, 0.65], # Btu/h-ft²-°F "R_value": [2.22, 3.13, 1.96, 2.08, 8.33, 12.5, 33.33, 66.67, 3.13, 1.54], # ft²-°F-h/Btu "density": [120, 120, 140, 140, 35, 35, 1.5, 1.5, 40, 490] # lb/ft³ } return pd.DataFrame(data) def _load_roof_classifications(self) -> pd.DataFrame: """ Load roof classification data. Returns: DataFrame with roof type descriptions and properties. """ data = { "type": [1, 2, 3, 4, 5, 8, 9, 10, 13, 14], "description": [ "Light roof, no insulation", "Light roof, minimal insulation", "Medium roof, R-10 insulation", "Medium roof, R-15 insulation", "Heavy roof, R-20 insulation", "Heavy roof, R-25 insulation", "Concrete slab, R-15 insulation", "Concrete slab, R-20 insulation", "Metal deck, R-30 insulation", "Metal deck, R-35 insulation" ], "U_value": [0.5, 0.4, 0.3, 0.25, 0.2, 0.15, 0.18, 0.14, 0.1, 0.08], "mass": [10, 15, 50, 60, 100, 120, 150, 160, 80, 90] # lb/ft² } return pd.DataFrame(data) def _load_latitude_correction(self) -> Dict[str, Dict[str, float]]: """ Load latitude correction factors for CLTD/SCL values. Returns: Dictionary of correction factors for different latitudes and months. """ return { "24N": {"Jan": -5.0, "Feb": -3.5, "Mar": -1.0, "Apr": 2.0, "May": 4.0, "Jun": 5.0, "Jul": 4.5, "Aug": 3.0, "Sep": 1.0, "Oct": -1.5, "Nov": -4.0, "Dec": -5.5}, "32N": {"Jan": -4.0, "Feb": -2.5, "Mar": 0.0, "Apr": 2.5, "May": 4.5, "Jun": 5.5, "Jul": 5.0, "Aug": 3.5, "Sep": 1.5, "Oct": -0.5, "Nov": -3.0, "Dec": -4.5}, "40N": {"Jan": -3.0, "Feb": -1.5, "Mar": 1.0, "Apr": 3.0, "May": 5.0, "Jun": 6.0, "Jul": 5.5, "Aug": 4.0, "Sep": 2.0, "Oct": 0.0, "Nov": -2.0, "Dec": -3.5}, "48N": {"Jan": -2.0, "Feb": -0.5, "Mar": 2.0, "Apr": 4.0, "May": 6.0, "Jun": 7.0, "Jul": 6.5, "Aug": 5.0, "Sep": 3.0, "Oct": 1.0, "Nov": -1.0, "Dec": -2.5}, "56N": {"Jan": -1.0, "Feb": 0.5, "Mar": 3.0, "Apr": 5.0, "May": 7.0, "Jun": 8.0, "Jul": 7.5, "Aug": 6.0, "Sep": 4.0, "Oct": 2.0, "Nov": 0.0, "Dec": -1.5} } def _load_color_correction(self) -> Dict[str, float]: """ Load color correction factors for CLTD values. Returns: Dictionary of correction factors for different colors. """ return {"Dark": 0.0, "Medium": -1.0, "Light": -2.0} def _load_month_correction(self) -> Dict[str, float]: """ Load month correction factors for CLTD values. Returns: Dictionary of correction factors for different months. """ return { "Jan": -6.0, "Feb": -5.0, "Mar": -3.0, "Apr": -1.0, "May": 1.0, "Jun": 2.0, "Jul": 2.0, "Aug": 2.0, "Sep": 1.0, "Oct": -1.0, "Nov": -3.0, "Dec": -5.0 } def _apply_climatic_corrections(self, cltd: float, latitude: str, month: str, color: str, outdoor_temp: float, indoor_temp: float) -> float: """ Apply climatic corrections to CLTD values based on latitude, month, color, and temperature. Args: cltd (float): Base CLTD value. latitude (str): Latitude (e.g., '32N'). month (str): Month (e.g., 'Jul'). color (str): Surface color ('Dark', 'Medium', 'Light'). outdoor_temp (float): Outdoor design temperature (°C). indoor_temp (float): Indoor design temperature (°C). Returns: float: Corrected CLTD value (°C). """ try: # Convert temperatures to °F for ASHRAE corrections outdoor_temp_f = outdoor_temp * 9/5 + 32 indoor_temp_f = indoor_temp * 9/5 + 32 # Get correction factors lat_corr = self.latitude_correction.get(latitude, {}).get(month, 0.0) month_corr = self.month_correction.get(month, 0.0) color_corr = self.color_correction.get(color, 0.0) # Apply temperature difference correction (ASHRAE CLTD correction formula) temp_diff = outdoor_temp_f - indoor_temp_f design_temp_diff = 85 - 78 # ASHRAE base conditions: 85°F outdoor, 78°F indoor temp_corr = (temp_diff - design_temp_diff) * 0.5556 # Convert °F to °C # Total correction corrected_cltd = cltd + lat_corr + month_corr + color_corr + temp_corr # Ensure non-negative CLTD return max(0.0, corrected_cltd) except Exception as e: raise ValueError(f"Error applying climatic corrections: {str(e)}") def get_cltd_wall(self, wall_group: str, orientation: str, hour: int) -> float: """Get CLTD value for a wall.""" if wall_group not in self.cltd_wall: raise ValueError(f"Invalid wall group: {wall_group}") orientation_map = {e.value: e.name for e in Orientation} orientation_abbr = orientation_map.get(orientation, orientation) if orientation_abbr not in self.cltd_wall[wall_group].columns: raise ValueError(f"Invalid orientation: {orientation}") if hour not in self.cltd_wall[wall_group].index: raise ValueError(f"Invalid hour: {hour}") return float(self.cltd_wall[wall_group].loc[hour, orientation_abbr]) def get_cltd_roof(self, roof_group: str, latitude: str, hour: int) -> float: """Get CLTD value for a roof.""" # Map latitude to standard format before forming the key valid_latitudes = ['24N', '36N', '48N'] # Handle numeric or non-standard latitude values if latitude not in valid_latitudes: # Try to convert to standard format try: # First, handle string representations that might contain direction indicators if isinstance(latitude, str): # Extract numeric part, removing 'N' or 'S' lat_str = latitude.upper().strip() num_part = ''.join(c for c in lat_str if c.isdigit() or c == '.') lat_val = float(num_part) # Adjust for southern hemisphere if needed if 'S' in lat_str: lat_val = -lat_val else: # Handle direct numeric input lat_val = float(latitude) # Take absolute value for mapping purposes abs_lat = abs(lat_val) # Map to the closest standard latitude for roof data if abs_lat < 30: latitude = '24N' elif abs_lat < 42: latitude = '36N' else: latitude = '48N' except (ValueError, TypeError): raise ValueError(f"Invalid latitude format: {latitude}") key = f"{roof_group}_{latitude}" if key not in self.cltd_roof: raise ValueError(f"Invalid roof group or latitude: {key}") if hour not in self.cltd_roof[key].index: raise ValueError(f"Invalid hour: {hour}") return float(self.cltd_roof[key].loc[hour, "HOR"]) def get_scl(self, latitude: str, month: str, orientation: str, hour: int) -> float: """Get SCL value for a window.""" # Map latitude to standard format before forming the key valid_latitudes = ['24N', '32N', '40N', '48N', '56N'] # Handle numeric or non-standard latitude values if latitude not in valid_latitudes: # Try to convert to standard format try: # First, handle string representations that might contain direction indicators if isinstance(latitude, str): # Extract numeric part, removing 'N' or 'S' lat_str = latitude.upper().strip() num_part = ''.join(c for c in lat_str if c.isdigit() or c == '.') lat_val = float(num_part) # Adjust for southern hemisphere if needed if 'S' in lat_str: lat_val = -lat_val else: # Handle direct numeric input lat_val = float(latitude) # Take absolute value for mapping purposes abs_lat = abs(lat_val) # Map to the closest standard latitude for SCL data if abs_lat < 28: latitude = '24N' elif abs_lat < 36: latitude = '32N' elif abs_lat < 44: latitude = '40N' elif abs_lat < 52: latitude = '48N' else: latitude = '56N' except (ValueError, TypeError): raise ValueError(f"Invalid latitude format: {latitude}") key = f"{latitude}_{month}" if key not in self.scl: raise ValueError(f"Invalid latitude or month: {key}") orientation_map = {e.value: e.name for e in Orientation} orientation_abbr = orientation_map.get(orientation, orientation) if orientation_abbr not in self.scl[key].columns: raise ValueError(f"Invalid orientation: {orientation}") if hour not in self.scl[key].index: raise ValueError(f"Invalid hour: {hour}") return float(self.scl[key].loc[hour, orientation_abbr]) def get_clf_lights(self, zone_type: str, hours_on: str, hour: int) -> float: """Get CLF value for lights.""" key = f"{zone_type}_{hours_on}" if key not in self.clf_lights.columns: raise ValueError(f"Invalid zone type or hours: {key}") if hour not in self.clf_lights.index: raise ValueError(f"Invalid hour: {hour}") return float(self.clf_lights.loc[hour, key]) def get_clf_people(self, zone_type: str, hours_occupied: str, hour: int) -> float: """Get CLF value for people.""" key = f"{zone_type}_{hours_occupied}" if key not in self.clf_people.columns: raise ValueError(f"Invalid zone type or hours: {key}") if hour not in self.clf_people.index: raise ValueError(f"Invalid hour: {hour}") return float(self.clf_people.loc[hour, key]) def get_clf_equipment(self, zone_type: str, hours_operated: str, hour: int) -> float: """Get CLF value for equipment.""" key = f"{zone_type}_{hours_operated}" if key not in self.clf_equipment.columns: raise ValueError(f"Invalid zone type or hours: {key}") if hour not in self.clf_equipment.index: raise ValueError(f"Invalid hour: {hour}") return float(self.clf_equipment.loc[hour, key]) def get_thermal_property(self, material: str, property_type: str) -> float: """ Get thermal property for a material. Args: material (str): Material name (e.g., 'Brick_4in'). property_type (str): Property to retrieve ('U_value', 'R_value', 'density'). Returns: float: Value of the specified thermal property. Raises: ValueError: If material or property_type is invalid. """ if material not in self.thermal_properties['material'].values: raise ValueError(f"Invalid material: {material}") if property_type not in ['U_value', 'R_value', 'density']: raise ValueError(f"Invalid property type: {property_type}") return float(self.thermal_properties.loc[self.thermal_properties['material'] == material, property_type].iloc[0]) def get_heat_gain(self, source: str) -> float: """ Get heat gain value for an internal source. Args: source (str): Source type ('people_sensible', 'people_latent', 'lights', 'equipment'). Returns: float: Heat gain value (Btu/h or Btu/h-ft²). Raises: ValueError: If source is invalid. """ if source not in self.heat_gain['source'].values: raise ValueError(f"Invalid source: {source}") return float(self.heat_gain.loc[self.heat_gain['source'] == source, 'gain'].iloc[0]) def plot_cooling_load(self, cooling_loads: List[float], title: str = "Cooling Load Profile", filename: str = "cooling_load.png") -> None: """ Plot the cooling load profile over 24 hours. Args: cooling_loads (List[float]): List of cooling load values for each hour. title (str): Plot title. filename (str): Output filename for the plot. """ if len(cooling_loads) != 24: raise ValueError("Cooling loads must contain 24 hourly values") plt.figure(figsize=(10, 6)) hours = list(range(24)) plt.plot(hours, cooling_loads, marker='o', linestyle='-', color='b') plt.title(title) plt.xlabel("Hour of Day") plt.ylabel("Cooling Load (Btu/h)") plt.grid(True) plt.xticks(hours) plt.savefig(filename) plt.close() def calculate_corrected_cltd_wall(self, wall_group: str, orientation: str, hour: int, latitude: str, month: str, color: str, outdoor_temp: float, indoor_temp: float) -> float: """ Calculate corrected CLTD for a wall with climatic corrections. Args: wall_group (str): Wall group (e.g., 'A', 'B', ..., 'H'). orientation (str): Wall orientation (e.g., 'North', 'East', etc.). hour (int): Hour of the day (0-23). latitude (str): Latitude (e.g., '32N'). month (str): Month (e.g., 'Jul'). color (str): Surface color ('Dark', 'Medium', 'Light'). outdoor_temp (float): Outdoor design temperature (°C). indoor_temp (float): Indoor design temperature (°C). Returns: float: Corrected CLTD value (°C). Raises: ValueError: If inputs are invalid or correction fails. """ valid, message = self._validate_cltd_inputs(wall_group, orientation, hour, latitude, month, color, is_wall=True) if not valid: raise ValueError(message) try: # Get base CLTD base_cltd = self.get_cltd_wall(wall_group, orientation, hour) # Apply climatic corrections corrected_cltd = self._apply_climatic_corrections(base_cltd, latitude, month, color, outdoor_temp, indoor_temp) return corrected_cltd except Exception as e: raise ValueError(f"Error calculating corrected CLTD for wall: {str(e)}") def calculate_corrected_cltd_roof(self, roof_group: str, latitude: str, hour: int, month: str, color: str, outdoor_temp: float, indoor_temp: float) -> float: """ Calculate corrected CLTD for a roof with climatic corrections. Args: roof_group (str): Roof group (e.g., 'A', 'B', ..., 'G'). latitude (str): Latitude (e.g., '24N', '36N', '48N'). hour (int): Hour of the day (0-23). month (str): Month (e.g., 'Jul'). color (str): Surface color ('Dark', 'Medium', 'Light'). outdoor_temp (float): Outdoor design temperature (°C). indoor_temp (float): Indoor design temperature (°C). Returns: float: Corrected CLTD value (°C). Raises: ValueError: If inputs are invalid or correction fails. """ valid, message = self._validate_cltd_inputs(roof_group, 'Horizontal', hour, latitude, month, color, is_wall=False) if not valid: raise ValueError(message) try: # Get base CLTD base_cltd = self.get_cltd_roof(roof_group, latitude, hour) # Apply climatic corrections corrected_cltd = self._apply_climatic_corrections(base_cltd, latitude, month, color, outdoor_temp, indoor_temp) return corrected_cltd except Exception as e: raise ValueError(f"Error calculating corrected CLTD for roof: {str(e)}")