Building-science/HVAC
0
1"""2Enhanced Drapery module for HVAC Load Calculator with comprehensive CLTD implementation and SCL integration.3This module provides classes and functions for handling drapery properties4and calculating their effects on window heat transfer using detailed ASHRAE CLTD/SCL methods.5 6Includes comprehensive CLTD tables for windows (SingleClear, DoubleTinted, LowE, Reflective)7at multiple latitudes (24°N, 40°N, 48°N) and all orientations, as well as detailed8climatic corrections and door CLTD calculations.9 10Enhanced to map UI shading coefficients to drapery properties (openness, color, fullness)11and apply conduction reduction (5-15%) based on openness per ASHRAE guidelines.12"""13 14from typing import Dict, Any, Optional, Tuple, List, Union15from enum import Enum16import math17import pandas as pd18from data.ashrae_tables import ASHRAETables19 20 21class DraperyOpenness(Enum):22 """Enum for drapery openness classification."""23 OPEN = "Open (>25%)"24 SEMI_OPEN = "Semi-open (7-25%)"25 CLOSED = "Closed (0-7%)"26 27 28class DraperyColor(Enum):29 """Enum for drapery color/reflectance classification."""30 DARK = "Dark (0-25%)"31 MEDIUM = "Medium (25-50%)"32 LIGHT = "Light (>50%)"33 34 35class GlazingType(Enum):36 """Enum for glazing types."""37 SINGLE_CLEAR = "Single Clear"38 SINGLE_TINTED = "Single Tinted"39 DOUBLE_CLEAR = "Double Clear"40 DOUBLE_TINTED = "Double Tinted"41 LOW_E = "Low-E"42 REFLECTIVE = "Reflective"43 44 45class FrameType(Enum):46 """Enum for window frame types."""47 ALUMINUM = "Aluminum without Thermal Break"48 ALUMINUM_THERMAL_BREAK = "Aluminum with Thermal Break"49 VINYL = "Vinyl/Fiberglass"50 WOOD = "Wood/Vinyl-Clad Wood"51 INSULATED = "Insulated"52 53 54class SurfaceColor(Enum):55 """Enum for surface color classification."""56 DARK = "Dark"57 MEDIUM = "Medium"58 LIGHT = "Light"59 60 61class Latitude(Enum):62 """Enum for latitude ranges."""63 LAT_24N = "24N"64 LAT_40N = "40N"65 LAT_48N = "48N"66 67 68# U-Factors for various fenestration products (Table 9-1) in SI units (W/m²K)69# Format: {(glazing_type, frame_type): u_factor}70WINDOW_U_FACTORS = {71 # Single Clear Glass72 (GlazingType.SINGLE_CLEAR, FrameType.ALUMINUM): 7.22,73 (GlazingType.SINGLE_CLEAR, FrameType.ALUMINUM_THERMAL_BREAK): 6.14,74 (GlazingType.SINGLE_CLEAR, FrameType.VINYL): 5.11,75 (GlazingType.SINGLE_CLEAR, FrameType.WOOD): 5.06,76 (GlazingType.SINGLE_CLEAR, FrameType.INSULATED): 4.60,77 78 # Single Tinted Glass79 (GlazingType.SINGLE_TINTED, FrameType.ALUMINUM): 7.22,80 (GlazingType.SINGLE_TINTED, FrameType.ALUMINUM_THERMAL_BREAK): 6.14,81 (GlazingType.SINGLE_TINTED, FrameType.VINYL): 5.11,82 (GlazingType.SINGLE_TINTED, FrameType.WOOD): 5.06,83 (GlazingType.SINGLE_TINTED, FrameType.INSULATED): 4.60,84 85 # Double Clear Glass86 (GlazingType.DOUBLE_CLEAR, FrameType.ALUMINUM): 4.60,87 (GlazingType.DOUBLE_CLEAR, FrameType.ALUMINUM_THERMAL_BREAK): 3.41,88 (GlazingType.DOUBLE_CLEAR, FrameType.VINYL): 3.01,89 (GlazingType.DOUBLE_CLEAR, FrameType.WOOD): 2.90,90 (GlazingType.DOUBLE_CLEAR, FrameType.INSULATED): 2.50,91 92 # Double Tinted Glass93 (GlazingType.DOUBLE_TINTED, FrameType.ALUMINUM): 4.60,94 (GlazingType.DOUBLE_TINTED, FrameType.ALUMINUM_THERMAL_BREAK): 3.41,95 (GlazingType.DOUBLE_TINTED, FrameType.VINYL): 3.01,96 (GlazingType.DOUBLE_TINTED, FrameType.WOOD): 2.90,97 (GlazingType.DOUBLE_TINTED, FrameType.INSULATED): 2.50,98 99 # Low-E Glass100 (GlazingType.LOW_E, FrameType.ALUMINUM): 3.41,101 (GlazingType.LOW_E, FrameType.ALUMINUM_THERMAL_BREAK): 2.67,102 (GlazingType.LOW_E, FrameType.VINYL): 2.33,103 (GlazingType.LOW_E, FrameType.WOOD): 2.22,104 (GlazingType.LOW_E, FrameType.INSULATED): 1.87,105 106 # Reflective Glass107 (GlazingType.REFLECTIVE, FrameType.ALUMINUM): 3.41,108 (GlazingType.REFLECTIVE, FrameType.ALUMINUM_THERMAL_BREAK): 2.67,109 (GlazingType.REFLECTIVE, FrameType.VINYL): 2.33,110 (GlazingType.REFLECTIVE, FrameType.WOOD): 2.22,111 (GlazingType.REFLECTIVE, FrameType.INSULATED): 1.87,112}113 114# SHGC values for various glazing types (Table 9-3)115# Format: {(glazing_type, frame_type): shgc}116WINDOW_SHGC = {117 # Single Clear Glass118 (GlazingType.SINGLE_CLEAR, FrameType.ALUMINUM): 0.78,119 (GlazingType.SINGLE_CLEAR, FrameType.ALUMINUM_THERMAL_BREAK): 0.75,120 (GlazingType.SINGLE_CLEAR, FrameType.VINYL): 0.67,121 (GlazingType.SINGLE_CLEAR, FrameType.WOOD): 0.65,122 (GlazingType.SINGLE_CLEAR, FrameType.INSULATED): 0.63,123 124 # Single Tinted Glass125 (GlazingType.SINGLE_TINTED, FrameType.ALUMINUM): 0.65,126 (GlazingType.SINGLE_TINTED, FrameType.ALUMINUM_THERMAL_BREAK): 0.62,127 (GlazingType.SINGLE_TINTED, FrameType.VINYL): 0.55,128 (GlazingType.SINGLE_TINTED, FrameType.WOOD): 0.53,129 (GlazingType.SINGLE_TINTED, FrameType.INSULATED): 0.52,130 131 # Double Clear Glass132 (GlazingType.DOUBLE_CLEAR, FrameType.ALUMINUM): 0.65,133 (GlazingType.DOUBLE_CLEAR, FrameType.ALUMINUM_THERMAL_BREAK): 0.61,134 (GlazingType.DOUBLE_CLEAR, FrameType.VINYL): 0.53,135 (GlazingType.DOUBLE_CLEAR, FrameType.WOOD): 0.51,136 (GlazingType.DOUBLE_CLEAR, FrameType.INSULATED): 0.49,137 138 # Double Tinted Glass139 (GlazingType.DOUBLE_TINTED, FrameType.ALUMINUM): 0.53,140 (GlazingType.DOUBLE_TINTED, FrameType.ALUMINUM_THERMAL_BREAK): 0.50,141 (GlazingType.DOUBLE_TINTED, FrameType.VINYL): 0.42,142 (GlazingType.DOUBLE_TINTED, FrameType.WOOD): 0.40,143 (GlazingType.DOUBLE_TINTED, FrameType.INSULATED): 0.38,144 145 # Low-E Glass146 (GlazingType.LOW_E, FrameType.ALUMINUM): 0.46,147 (GlazingType.LOW_E, FrameType.ALUMINUM_THERMAL_BREAK): 0.44,148 (GlazingType.LOW_E, FrameType.VINYL): 0.38,149 (GlazingType.LOW_E, FrameType.WOOD): 0.36,150 (GlazingType.LOW_E, FrameType.INSULATED): 0.34,151 152 # Reflective Glass153 (GlazingType.REFLECTIVE, FrameType.ALUMINUM): 0.33,154 (GlazingType.REFLECTIVE, FrameType.ALUMINUM_THERMAL_BREAK): 0.31,155 (GlazingType.REFLECTIVE, FrameType.VINYL): 0.27,156 (GlazingType.REFLECTIVE, FrameType.WOOD): 0.25,157 (GlazingType.REFLECTIVE, FrameType.INSULATED): 0.24,158}159 160# Door U-Factors in SI units (W/m²K)161# Format: {door_type: u_factor}162DOOR_U_FACTORS = {163 "WoodSolid": 3.35, # Approximated from Group D walls164 "MetalInsulated": 2.61, # Approximated from Group F walls165 "GlassDoor": 7.22, # Same as single clear glass with aluminum frame166 "InsulatedMetal": 2.15, # Insulated metal door167 "InsulatedWood": 1.93, # Insulated wood door168 "Custom": 3.00, # Default for custom doors169}170 171# Skylight U-Factors in SI units (W/m²K)172# Format: {(glazing_type, frame_type): u_factor}173SKYLIGHT_U_FACTORS = {174 # Single Clear Glass175 (GlazingType.SINGLE_CLEAR, FrameType.ALUMINUM): 7.79,176 (GlazingType.SINGLE_CLEAR, FrameType.ALUMINUM_THERMAL_BREAK): 6.71,177 (GlazingType.SINGLE_CLEAR, FrameType.VINYL): 5.68,178 (GlazingType.SINGLE_CLEAR, FrameType.WOOD): 5.63,179 (GlazingType.SINGLE_CLEAR, FrameType.INSULATED): 5.17,180 181 # Single Tinted Glass182 (GlazingType.SINGLE_TINTED, FrameType.ALUMINUM): 7.79,183 (GlazingType.SINGLE_TINTED, FrameType.ALUMINUM_THERMAL_BREAK): 6.71,184 (GlazingType.SINGLE_TINTED, FrameType.VINYL): 5.68,185 (GlazingType.SINGLE_TINTED, FrameType.WOOD): 5.63,186 (GlazingType.SINGLE_TINTED, FrameType.INSULATED): 5.17,187 188 # Double Clear Glass189 (GlazingType.DOUBLE_CLEAR, FrameType.ALUMINUM): 5.17,190 (GlazingType.DOUBLE_CLEAR, FrameType.ALUMINUM_THERMAL_BREAK): 3.98,191 (GlazingType.DOUBLE_CLEAR, FrameType.VINYL): 3.58,192 (GlazingType.DOUBLE_CLEAR, FrameType.WOOD): 3.47,193 (GlazingType.DOUBLE_CLEAR, FrameType.INSULATED): 3.07,194 195 # Double Tinted Glass196 (GlazingType.DOUBLE_TINTED, FrameType.ALUMINUM): 5.17,197 (GlazingType.DOUBLE_TINTED, FrameType.ALUMINUM_THERMAL_BREAK): 3.98,198 (GlazingType.DOUBLE_TINTED, FrameType.VINYL): 3.58,199 (GlazingType.DOUBLE_TINTED, FrameType.WOOD): 3.47,200 (GlazingType.DOUBLE_TINTED, FrameType.INSULATED): 3.07,201 202 # Low-E Glass203 (GlazingType.LOW_E, FrameType.ALUMINUM): 3.98,204 (GlazingType.LOW_E, FrameType.ALUMINUM_THERMAL_BREAK): 3.24,205 (GlazingType.LOW_E, FrameType.VINYL): 2.90,206 (GlazingType.LOW_E, FrameType.WOOD): 2.78,207 (GlazingType.LOW_E, FrameType.INSULATED): 2.44,208 209 # Reflective Glass210 (GlazingType.REFLECTIVE, FrameType.ALUMINUM): 3.98,211 (GlazingType.REFLECTIVE, FrameType.ALUMINUM_THERMAL_BREAK): 3.24,212 (GlazingType.REFLECTIVE, FrameType.VINYL): 2.90,213 (GlazingType.REFLECTIVE, FrameType.WOOD): 2.78,214 (GlazingType.REFLECTIVE, FrameType.INSULATED): 2.44,215}216 217# Skylight SHGC values218# Format: {(glazing_type, frame_type): shgc}219SKYLIGHT_SHGC = {220 # Single Clear Glass221 (GlazingType.SINGLE_CLEAR, FrameType.ALUMINUM): 0.83,222 (GlazingType.SINGLE_CLEAR, FrameType.ALUMINUM_THERMAL_BREAK): 0.80,223 (GlazingType.SINGLE_CLEAR, FrameType.VINYL): 0.72,224 (GlazingType.SINGLE_CLEAR, FrameType.WOOD): 0.70,225 (GlazingType.SINGLE_CLEAR, FrameType.INSULATED): 0.68,226 227 # Single Tinted Glass228 (GlazingType.SINGLE_TINTED, FrameType.ALUMINUM): 0.70,229 (GlazingType.SINGLE_TINTED, FrameType.ALUMINUM_THERMAL_BREAK): 0.67,230 (GlazingType.SINGLE_TINTED, FrameType.VINYL): 0.60,231 (GlazingType.SINGLE_TINTED, FrameType.WOOD): 0.58,232 (GlazingType.SINGLE_TINTED, FrameType.INSULATED): 0.57,233 234 # Double Clear Glass235 (GlazingType.DOUBLE_CLEAR, FrameType.ALUMINUM): 0.70,236 (GlazingType.DOUBLE_CLEAR, FrameType.ALUMINUM_THERMAL_BREAK): 0.66,237 (GlazingType.DOUBLE_CLEAR, FrameType.VINYL): 0.58,238 (GlazingType.DOUBLE_CLEAR, FrameType.WOOD): 0.56,239 (GlazingType.DOUBLE_CLEAR, FrameType.INSULATED): 0.54,240 241 # Double Tinted Glass242 (GlazingType.DOUBLE_TINTED, FrameType.ALUMINUM): 0.58,243 (GlazingType.DOUBLE_TINTED, FrameType.ALUMINUM_THERMAL_BREAK): 0.55,244 (GlazingType.DOUBLE_TINTED, FrameType.VINYL): 0.47,245 (GlazingType.DOUBLE_TINTED, FrameType.WOOD): 0.45,246 (GlazingType.DOUBLE_TINTED, FrameType.INSULATED): 0.43,247 248 # Low-E Glass249 (GlazingType.LOW_E, FrameType.ALUMINUM): 0.51,250 (GlazingType.LOW_E, FrameType.ALUMINUM_THERMAL_BREAK): 0.49,251 (GlazingType.LOW_E, FrameType.VINYL): 0.43,252 (GlazingType.LOW_E, FrameType.WOOD): 0.41,253 (GlazingType.LOW_E, FrameType.INSULATED): 0.39,254 255 # Reflective Glass256 (GlazingType.REFLECTIVE, FrameType.ALUMINUM): 0.38,257 (GlazingType.REFLECTIVE, FrameType.ALUMINUM_THERMAL_BREAK): 0.36,258 (GlazingType.REFLECTIVE, FrameType.VINYL): 0.32,259 (GlazingType.REFLECTIVE, FrameType.WOOD): 0.30,260 (GlazingType.REFLECTIVE, FrameType.INSULATED): 0.29,261}262 263 264class Drapery:265 """Class for handling drapery properties and effects on window heat transfer."""266 267 def __init__(self, openness: str = "Semi-Open", color: str = "Medium", 268 fullness: float = 1.5, enabled: bool = True, shading_device: str = "Drapes"):269 """270 Initialize drapery properties with UI-compatible inputs.271 272 Args:273 openness: Drapery openness category ("Closed", "Semi-Open", "Open")274 color: Drapery color category ("Light", "Medium", "Dark")275 fullness: Fullness factor (1.0 for flat, 1.0-2.0 for pleated)276 enabled: Whether drapery is enabled277 shading_device: Type of shading device ("Venetian Blinds", "Drapes", etc.)278 """279 self.openness = openness280 self.color = color281 self.fullness = fullness282 self.enabled = enabled283 self.shading_device = shading_device284 285 def get_openness_category(self) -> str:286 """Get openness category as string."""287 return self.openness288 289 def get_color_category(self) -> str:290 """Get color category as string."""291 return self.color292 293 def get_shading_coefficient(self, shgc: float = 0.5) -> float:294 """295 Calculate shading coefficient for drapery based on UI inputs.296 297 Args:298 shgc: Solar Heat Gain Coefficient of window (default 0.5)299 300 Returns:301 Shading coefficient (0.0-1.0)302 """303 if not self.enabled:304 return 1.0305 306 # Mapping of UI shading devices to properties307 mapping = {308 ("Venetian Blinds", "Light"): {"openness": "Semi-Open", "color": "Light", "fullness": 1.0, "sc": 0.6},309 ("Venetian Blinds", "Medium"): {"openness": "Semi-Open", "color": "Medium", "fullness": 1.0, "sc": 0.65},310 ("Venetian Blinds", "Dark"): {"openness": "Semi-Open", "color": "Dark", "fullness": 1.0, "sc": 0.7},311 ("Drapes", "Light"): {"openness": "Closed", "color": "Light", "fullness": 1.5, "sc": 0.59},312 ("Drapes", "Medium"): {"openness": "Closed", "color": "Medium", "fullness": 1.5, "sc": 0.74},313 ("Drapes", "Dark"): {"openness": "Closed", "color": "Dark", "fullness": 1.5, "sc": 0.87},314 ("Roller Shades", "Light"): {"openness": "Open", "color": "Light", "fullness": 1.0, "sc": 0.8},315 ("Roller Shades", "Medium"): {"openness": "Open", "color": "Medium", "fullness": 1.0, "sc": 0.88},316 ("Roller Shades", "Dark"): {"openness": "Open", "color": "Dark", "fullness": 1.0, "sc": 0.94},317 }318 319 # Get shading coefficient from mapping or default to table-based value320 properties = mapping.get((self.shading_device, self.color), {321 "openness": self.openness,322 "color": self.color,323 "fullness": self.fullness,324 "sc": 0.85325 })326 base_sc = properties["sc"]327 328 # Adjust for fullness if different from mapped value329 if self.fullness != properties["fullness"]:330 fullness_factor = 1.0 - 0.05 * (self.fullness - 1.0)331 base_sc *= fullness_factor332 333 return base_sc334 335 def get_conduction_reduction(self) -> float:336 """337 Get conduction reduction factor based on openness.338 339 Returns:340 Reduction factor (0.05-0.15)341 """342 reductions = {343 "Closed": 0.15, # 15% reduction344 "Semi-Open": 0.10, # 10% reduction345 "Open": 0.05 # 5% reduction346 }347 return reductions.get(self.openness, 0.10)348 349 350class CLTDCalculator:351 """Class for calculating Cooling Load Temperature Difference (CLTD) values."""352 353 def __init__(self, indoor_temp: float = 25.6, outdoor_max_temp: float = 35.0, 354 outdoor_daily_range: float = 11.7, latitude: Latitude = Latitude.LAT_40N, 355 month: int = 7):356 """357 Initialize CLTD calculator.358 359 Args:360 indoor_temp: Indoor design temperature (°C)361 outdoor_max_temp: Outdoor maximum temperature (°C)362 outdoor_daily_range: Daily temperature range (°C)363 latitude: Latitude category (24°N, 40°N, 48°N)364 month: Month (1-12)365 """366 self.indoor_temp = indoor_temp # °C367 self.outdoor_max_temp = outdoor_max_temp # °C368 self.outdoor_daily_range = outdoor_daily_range # °C369 self.latitude = latitude370 self.month = month371 self.outdoor_avg_temp = outdoor_max_temp - outdoor_daily_range / 2372 373 # Initialize ASHRAE tables for SCL data374 self.ashrae_tables = ASHRAETables()375 376 # Load CLTD tables377 self.cltd_window_tables = self._load_cltd_window_table()378 self.cltd_door_tables = self._load_cltd_door_table()379 self.cltd_skylight_tables = self._load_cltd_skylight_table()380 381 # Load correction factors382 self.latitude_corrections = self._load_latitude_correction()383 self.month_corrections = self._load_month_correction()384 385 def _load_cltd_window_table(self) -> Dict[str, Dict[str, pd.DataFrame]]:386 """387 Load CLTD tables for windows at multiple latitudes (July).388 389 Returns:390 Dictionary of DataFrames with CLTD values indexed by hour (0-23)391 and columns for orientations (N, NE, E, SE, S, SW, W, NW)392 """393 hours = list(range(24))394 395 # Comprehensive window CLTD data for different latitudes, glazing types, and orientations396 window_cltd_data = {397 "24N": {398 "SingleClear": {399 "N": [3, 2, 1, 1, 1, 2, 3, 4, 5, 6, 7, 8, 8, 7, 6, 5, 4, 3, 3, 3, 3, 3, 3, 3],400 "NE": [3, 2, 1, 1, 1, 3, 6, 9, 11, 10, 9, 7, 6, 5, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3],401 "E": [3, 2, 1, 1, 1, 3, 7, 11, 13, 13, 11, 9, 7, 6, 5, 4, 3, 3, 3, 3, 3, 3, 3, 3],402 "SE": [3, 2, 1, 1, 1, 2, 4, 6, 8, 10, 11, 11, 10, 9, 7, 5, 4, 3, 3, 3, 3, 3, 3, 3],403 "S": [3, 2, 1, 1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 9, 8, 7, 6, 5, 4, 3, 3, 3, 3, 3],404 "SW": [3, 2, 1, 1, 1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 11, 10, 8, 6, 5, 4, 3, 3, 3, 3],405 "W": [3, 2, 1, 1, 1, 2, 3, 4, 6, 8, 10, 11, 11, 11, 10, 9, 8, 7, 6, 5, 4, 3, 3, 3],406 "NW": [3, 2, 1, 1, 1, 2, 3, 5, 7, 9, 10, 10, 9, 8, 7, 6, 5, 4, 3, 3, 3, 3, 3, 3]407 },408 "DoubleTinted": {409 "N": [2, 1, 0, 0, 0, 1, 2, 3, 4, 5, 5, 6, 6, 5, 4, 3, 2, 2, 2, 2, 2, 2, 2, 2],410 "NE": [2, 1, 0, 0, 0, 2, 5, 7, 9, 8, 7, 5, 4, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],411 "E": [2, 1, 0, 0, 0, 2, 5, 9, 10, 10, 9, 7, 5, 4, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2],412 "SE": [2, 1, 0, 0, 0, 1, 3, 5, 6, 8, 9, 9, 8, 7, 5, 3, 2, 2, 2, 2, 2, 2, 2, 2],413 "S": [2, 1, 0, 0, 0, 1, 2, 3, 4, 5, 5, 6, 7, 7, 6, 5, 4, 3, 2, 2, 2, 2, 2, 2],414 "SW": [2, 1, 0, 0, 0, 1, 2, 3, 4, 5, 5, 7, 8, 9, 9, 8, 6, 4, 3, 2, 2, 2, 2, 2],415 "W": [2, 1, 0, 0, 0, 1, 2, 3, 5, 6, 8, 9, 9, 9, 8, 7, 6, 5, 4, 3, 2, 2, 2, 2],416 "NW": [2, 1, 0, 0, 0, 1, 2, 4, 5, 7, 8, 8, 7, 6, 5, 4, 3, 2, 2, 2, 2, 2, 2, 2]417 },418 "LowE": {419 "N": [1, 0, 0, 0, 0, 0, 1, 2, 3, 4, 4, 5, 5, 4, 3, 2, 2, 1, 1, 1, 1, 1, 1, 1],420 "NE": [1, 0, 0, 0, 0, 1, 4, 6, 8, 7, 6, 4, 3, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1],421 "E": [1, 0, 0, 0, 0, 1, 4, 8, 9, 9, 8, 6, 4, 3, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1],422 "SE": [1, 0, 0, 0, 0, 0, 2, 4, 5, 7, 8, 8, 7, 6, 4, 3, 2, 1, 1, 1, 1, 1, 1, 1],423 "S": [1, 0, 0, 0, 0, 0, 1, 2, 3, 4, 4, 5, 6, 6, 5, 4, 3, 2, 2, 1, 1, 1, 1, 1],424 "SW": [1, 0, 0, 0, 0, 0, 1, 2, 3, 4, 4, 6, 7, 8, 8, 7, 5, 3, 2, 2, 1, 1, 1, 1],425 "W": [1, 0, 0, 0, 0, 0, 1, 2, 4, 5, 7, 8, 8, 8, 7, 6, 5, 4, 3, 2, 2, 1, 1, 1],426 "NW": [1, 0, 0, 0, 0, 0, 1, 3, 4, 6, 7, 7, 6, 5, 4, 3, 2, 2, 1, 1, 1, 1, 1, 1]427 },428 "Reflective": {429 "N": [0, 0, 0, 0, 0, 0, 1, 1, 2, 3, 3, 4, 4, 3, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1],430 "NE": [0, 0, 0, 0, 0, 1, 3, 5, 6, 5, 4, 3, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],431 "E": [0, 0, 0, 0, 0, 1, 3, 6, 7, 7, 6, 5, 3, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1],432 "SE": [0, 0, 0, 0, 0, 0, 1, 3, 4, 5, 6, 6, 5, 4, 3, 2, 1, 1, 1, 1, 1, 1, 1, 1],433 "S": [0, 0, 0, 0, 0, 0, 1, 1, 2, 3, 3, 4, 5, 5, 4, 3, 2, 2, 1, 1, 1, 1, 1, 1],434 "SW": [0, 0, 0, 0, 0, 0, 1, 1, 2, 3, 3, 5, 5, 6, 6, 5, 4, 2, 2, 1, 1, 1, 1, 1],435 "W": [0, 0, 0, 0, 0, 0, 1, 1, 3, 4, 5, 6, 6, 6, 5, 4, 4, 3, 2, 2, 1, 1, 1, 1],436 "NW": [0, 0, 0, 0, 0, 0, 1, 2, 3, 5, 5, 5, 4, 4, 3, 2, 2, 1, 1, 1, 1, 1, 1, 1]437 }438 },439 "40N": {440 "SingleClear": {441 "N": [2, 1, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 7, 6, 5, 4, 3, 2, 2, 2, 2, 2, 2, 2],442 "NE": [2, 1, 0, 0, 0, 2, 5, 8, 10, 9, 8, 6, 5, 4, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2],443 "E": [2, 1, 0, 0, 0, 2, 6, 10, 12, 12, 10, 8, 6, 5, 4, 3, 2, 2, 2, 2, 2, 2, 2, 2],444 "SE": [2, 1, 0, 0, 0, 1, 3, 5, 7, 9, 10, 10, 9, 8, 6, 4, 3, 2, 2, 2, 2, 2, 2, 2],445 "S": [2, 1, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 8, 7, 6, 5, 4, 3, 2, 2, 2, 2, 2],446 "SW": [2, 1, 0, 0, 0, 1, 2, 3, 4, 5, 6, 8, 9, 10, 10, 9, 7, 5, 4, 3, 2, 2, 2, 2],447 "W": [2, 1, 0, 0, 0, 1, 2, 3, 5, 7, 9, 10, 10, 10, 9, 8, 7, 6, 5, 4, 3, 2, 2, 2],448 "NW": [2, 1, 0, 0, 0, 1, 2, 4, 6, 8, 9, 9, 8, 7, 6, 5, 4, 3, 2, 2, 2, 2, 2, 2]449 },450 "DoubleTinted": {451 "N": [1, 0, 0, 0, 0, 0, 1, 2, 3, 4, 4, 5, 5, 4, 3, 2, 1, 1, 1, 1, 1, 1, 1, 1],452 "NE": [1, 0, 0, 0, 0, 1, 4, 6, 8, 7, 6, 4, 3, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],453 "E": [1, 0, 0, 0, 0, 1, 4, 8, 9, 9, 8, 6, 4, 3, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1],454 "SE": [1, 0, 0, 0, 0, 0, 2, 4, 5, 7, 8, 8, 7, 6, 4, 2, 1, 1, 1, 1, 1, 1, 1, 1],455 "S": [1, 0, 0, 0, 0, 0, 1, 2, 3, 4, 4, 5, 6, 6, 5, 4, 3, 2, 1, 1, 1, 1, 1, 1],456 "SW": [1, 0, 0, 0, 0, 0, 1, 2, 3, 4, 4, 6, 7, 8, 8, 7, 5, 3, 2, 1, 1, 1, 1, 1],457 "W": [1, 0, 0, 0, 0, 0, 1, 2, 4, 5, 7, 8, 8, 8, 7, 6, 5, 4, 3, 2, 1, 1, 1, 1],458 "NW": [1, 0, 0, 0, 0, 0, 1, 3, 4, 6, 7, 7, 6, 5, 4, 3, 2, 1, 1, 1, 1, 1, 1, 1]459 },460 "LowE": {461 "N": [0, 0, 0, 0, 0, 0, 1, 1, 2, 3, 3, 4, 4, 3, 2, 1, 1, 0, 0, 0, 0, 0, 0, 0],462 "NE": [0, 0, 0, 0, 0, 1, 3, 5, 7, 6, 5, 3, 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],463 "E": [0, 0, 0, 0, 0, 1, 3, 7, 8, 8, 7, 5, 3, 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0],464 "SE": [0, 0, 0, 0, 0, 0, 1, 3, 4, 6, 7, 7, 6, 5, 3, 2, 1, 0, 0, 0, 0, 0, 0, 0],465 "S": [0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 3, 4, 5, 5, 4, 3, 2, 1, 1, 0, 0, 0, 0, 0],466 "SW": [0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 3, 5, 6, 7, 7, 6, 4, 2, 1, 1, 0, 0, 0, 0],467 "W": [0, 0, 0, 0, 0, 0, 0, 1, 3, 4, 6, 7, 7, 7, 6, 5, 4, 3, 2, 1, 1, 0, 0, 0],468 "NW": [0, 0, 0, 0, 0, 0, 0, 2, 3, 5, 6, 6, 5, 4, 3, 2, 1, 1, 0, 0, 0, 0, 0, 0]469 },470 "Reflective": {471 "N": [0, 0, 0, 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0],472 "NE": [0, 0, 0, 0, 0, 0, 2, 4, 5, 4, 3, 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],473 "E": [0, 0, 0, 0, 0, 0, 2, 5, 6, 6, 5, 4, 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],474 "SE": [0, 0, 0, 0, 0, 0, 0, 2, 3, 4, 5, 5, 4, 3, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0],475 "S": [0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 2, 3, 4, 4, 3, 2, 1, 1, 0, 0, 0, 0, 0, 0],476 "SW": [0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 2, 4, 4, 5, 5, 4, 3, 1, 1, 0, 0, 0, 0, 0],477 "W": [0, 0, 0, 0, 0, 0, 0, 0, 2, 3, 4, 5, 5, 5, 4, 3, 3, 2, 1, 1, 0, 0, 0, 0],478 "NW": [0, 0, 0, 0, 0, 0, 0, 1, 2, 4, 4, 4, 3, 3, 2, 1, 1, 0, 0, 0, 0, 0, 0, 0]479 }480 },481 "48N": {482 "SingleClear": {483 "N": [1, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 6, 5, 4, 3, 2, 1, 1, 1, 1, 1, 1, 1],484 "NE": [1, 0, 0, 0, 0, 1, 4, 7, 9, 8, 7, 5, 4, 3, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1],485 "E": [1, 0, 0, 0, 0, 1, 5, 9, 11, 11, 9, 7, 5, 4, 3, 2, 1, 1, 1, 1, 1, 1, 1, 1],486 "SE": [1, 0, 0, 0, 0, 0, 2, 4, 6, 8, 9, 9, 8, 7, 5, 3, 2, 1, 1, 1, 1, 1, 1, 1],487 "S": [1, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 7, 6, 5, 4, 3, 2, 1, 1, 1, 1, 1],488 "SW": [1, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 7, 8, 9, 9, 8, 6, 4, 3, 2, 1, 1, 1, 1],489 "W": [1, 0, 0, 0, 0, 0, 1, 2, 4, 6, 8, 9, 9, 9, 8, 7, 6, 5, 4, 3, 2, 1, 1, 1],490 "NW": [1, 0, 0, 0, 0, 0, 1, 3, 5, 7, 8, 8, 7, 6, 5, 4, 3, 2, 1, 1, 1, 1, 1, 1]491 },492 "DoubleTinted": {493 "N": [0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 3, 4, 4, 3, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0],494 "NE": [0, 0, 0, 0, 0, 0, 3, 5, 7, 6, 5, 3, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],495 "E": [0, 0, 0, 0, 0, 0, 3, 7, 8, 8, 7, 5, 3, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],496 "SE": [0, 0, 0, 0, 0, 0, 1, 3, 4, 6, 7, 7, 6, 5, 3, 1, 0, 0, 0, 0, 0, 0, 0, 0],497 "S": [0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 3, 4, 5, 5, 4, 3, 2, 1, 0, 0, 0, 0, 0, 0],498 "SW": [0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 3, 5, 6, 7, 7, 6, 4, 2, 1, 0, 0, 0, 0, 0],499 "W": [0, 0, 0, 0, 0, 0, 0, 1, 3, 4, 6, 7, 7, 7, 6, 5, 4, 3, 2, 1, 0, 0, 0, 0],500 "NW": [0, 0, 0, 0, 0, 0, 0, 2, 3, 5, 6, 6, 5, 4, 3, 2, 1, 0, 0, 0, 0, 0, 0, 0]501 },502 "LowE": {503 "N": [0, 0, 0, 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],504 "NE": [0, 0, 0, 0, 0, 0, 2, 4, 6, 5, 4, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],505 "E": [0, 0, 0, 0, 0, 0, 2, 6, 7, 7, 6, 4, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],506 "SE": [0, 0, 0, 0, 0, 0, 0, 2, 3, 5, 6, 6, 5, 4, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0],507 "S": [0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 2, 3, 4, 4, 3, 2, 1, 0, 0, 0, 0, 0, 0, 0],508 "SW": [0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 2, 4, 5, 6, 6, 5, 3, 1, 0, 0, 0, 0, 0, 0],509 "W": [0, 0, 0, 0, 0, 0, 0, 0, 2, 3, 5, 6, 6, 6, 5, 4, 3, 2, 1, 0, 0, 0, 0, 0],510 "NW": [0, 0, 0, 0, 0, 0, 0, 1, 2, 4, 5, 5, 4, 3, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0]511 },512 "Reflective": {513 "N": [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],514 "NE": [0, 0, 0, 0, 0, 0, 1, 3, 4, 3, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],515 "E": [0, 0, 0, 0, 0, 0, 1, 4, 5, 5, 4, 3, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],516 "SE": [0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 4, 3, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],517 "S": [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 2, 3, 3, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0],518 "SW": [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 3, 3, 4, 4, 3, 2, 0, 0, 0, 0, 0, 0, 0],519 "W": [0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 4, 4, 3, 2, 2, 1, 0, 0, 0, 0, 0, 0],520 "NW": [0, 0, 0, 0, 0, 0, 0, 0, 1, 3, 3, 3, 2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]521 }522 }523 }524 525 # Convert to DataFrames526 window_cltd_tables = {}527 for latitude, glazing_data in window_cltd_data.items():528 window_cltd_tables[latitude] = {}529 for glazing_type, orientation_data in glazing_data.items():530 window_cltd_tables[latitude][glazing_type] = pd.DataFrame(orientation_data, index=hours)531 532 return window_cltd_tables533 534 def _load_cltd_door_table(self) -> Dict[str, pd.DataFrame]:535 """536 Load CLTD tables for doors.537 538 Returns:539 Dictionary of DataFrames with CLTD values indexed by hour (0-23)540 """541 hours = list(range(24))542 543 # Door CLTD data approximated from wall groups544 door_cltd_data = {545 "WoodSolid": { # Approximated from Group D walls546 '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],547 '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],548 '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],549 '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],550 '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],551 '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],552 '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],553 '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]554 },555 "MetalInsulated": { # Approximated from Group F walls556 '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],557 '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],558 '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],559 '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],560 '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],561 '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],562 '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],563 '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]564 },565 "GlassDoor": { # Same as single clear glass566 'N': [3, 2, 1, 1, 1, 2, 3, 4, 5, 6, 7, 8, 8, 7, 6, 5, 4, 3, 3, 3, 3, 3, 3, 3],567 'NE': [3, 2, 1, 1, 1, 3, 6, 9, 11, 10, 9, 7, 6, 5, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3],568 'E': [3, 2, 1, 1, 1, 3, 7, 11, 13, 13, 11, 9, 7, 6, 5, 4, 3, 3, 3, 3, 3, 3, 3, 3],569 'SE': [3, 2, 1, 1, 1, 2, 4, 6, 8, 10, 11, 11, 10, 9, 7, 5, 4, 3, 3, 3, 3, 3, 3, 3],570 'S': [3, 2, 1, 1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 9, 8, 7, 6, 5, 4, 3, 3, 3, 3, 3],571 'SW': [3, 2, 1, 1, 1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 11, 10, 8, 6, 5, 4, 3, 3, 3, 3],572 'W': [3, 2, 1, 1, 1, 2, 3, 4, 6, 8, 10, 11, 11, 11, 10, 9, 8, 7, 6, 5, 4, 3, 3, 3],573 'NW': [3, 2, 1, 1, 1, 2, 3, 5, 7, 9, 10, 10, 9, 8, 7, 6, 5, 4, 3, 3, 3, 3, 3, 3]574 },575 "InsulatedMetal": { # Enhanced insulated metal door576 'N': [8, 6, 4, 2, 0, 0, 0, 1, 3, 5, 7, 9, 11, 13, 16, 18, 20, 22, 24, 24, 22, 19, 17, 13],577 'NE': [8, 6, 4, 2, 0, 1, 1, 4, 10, 18, 24, 29, 31, 31, 30, 30, 30, 31, 29, 27, 25, 22, 19, 16],578 'E': [9, 6, 4, 2, 0, 2, 1, 4, 11, 20, 29, 34, 37, 37, 37, 37, 37, 29, 29, 27, 24, 20, 17, 16],579 'SE': [8, 5, 3, 1, 0, 1, 0, 2, 6, 12, 18, 23, 26, 28, 28, 28, 28, 28, 26, 24, 22, 19, 16, 14],580 'S': [6, 4, 2, 1, -1, 1, 0, 0, 0, 1, 3, 5, 9, 12, 14, 18, 20, 21, 21, 21, 18, 16, 14, 12],581 'SW': [13, 10, 7, 4, 2, 2, 1, 1, 1, 2, 3, 6, 9, 12, 14, 18, 24, 30, 31, 29, 39, 38, 34, 29],582 'W': [18, 14, 10, 7, 4, 3, 2, 2, 2, 2, 4, 6, 9, 12, 14, 18, 26, 35, 33, 29, 49, 39, 39, 39],583 'NW': [16, 12, 9, 6, 3, 3, 2, 1, 1, 2, 4, 6, 9, 12, 14, 18, 26, 35, 33, 29, 39, 39, 39, 39]584 },585 "InsulatedWood": { # Enhanced insulated wood door586 '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],587 '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],588 '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],589 '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],590 '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],591 '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],592 '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],593 '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]594 },595 "Custom": { # Default for custom doors596 '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],597 '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],598 '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],599 '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],600 '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],601 '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],602 '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],603 '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]604 }605 }606 607 # Convert to DataFrames608 door_cltd_tables = {}609 for door_type, orientation_data in door_cltd_data.items():610 door_cltd_tables[door_type] = pd.DataFrame(orientation_data, index=hours)611 612 return door_cltd_tables613 614 def _load_cltd_skylight_table(self) -> Dict[str, pd.DataFrame]:615 """616 Load CLTD tables for skylights (flat, 0° slope).617 618 Returns:619 Dictionary of DataFrames with CLTD values indexed by hour (0-23)620 """621 hours = list(range(24))622 623 # Skylight CLTD data for 40°N latitude, July624 skylight_cltd_data = {625 "SingleClear": {626 'Horizontal': [3, 2, 1, 1, 1, 2, 4, 6, 9, 12, 15, 18, 20, 21, 20, 18, 15, 12, 9, 7, 5, 4, 3, 3]627 },628 "DoubleTinted": {629 'Horizontal': [2, 1, 0, 0, 0, 1, 3, 5, 7, 10, 12, 15, 17, 18, 17, 15, 12, 9, 7, 5, 3, 2, 2, 2]630 },631 "LowE": {632 'Horizontal': [1, 0, 0, 0, 0, 0, 2, 4, 6, 8, 10, 12, 14, 15, 14, 12, 10, 7, 5, 3, 2, 1, 1, 1]633 },634 "Reflective": {635 'Horizontal': [0, 0, 0, 0, 0, 0, 1, 2, 4, 6, 8, 10, 11, 12, 11, 10, 8, 6, 4, 2, 1, 0, 0, 0]636 }637 }638 639 # Convert to DataFrames640 skylight_cltd_tables = {}641 for glazing_type, orientation_data in skylight_cltd_data.items():642 skylight_cltd_tables[glazing_type] = pd.DataFrame(orientation_data, index=hours)643 644 return skylight_cltd_tables645 646 def _load_latitude_correction(self) -> Dict[str, float]:647 """648 Load latitude correction factors for CLTD.649 650 Returns:651 Dictionary of correction factors by latitude652 """653 return {654 "24N": 0.95,655 "40N": 1.00,656 "48N": 1.05657 }658 659 def _load_month_correction(self) -> Dict[int, float]:660 """661 Load month correction factors for CLTD.662 663 Returns:664 Dictionary of correction factors by month665 """666 return {667 1: 0.85, 2: 0.90, 3: 0.95, 4: 0.98, 5: 1.00,668 6: 1.02, 7: 1.00, 8: 0.98, 9: 0.95, 10: 0.90,669 11: 0.85, 12: 0.80670 }671 672 def get_cltd_window(self, glazing_type: str, orientation: str, hour: int) -> float:673 """674 Get CLTD for a window with corrections.675 676 Args:677 glazing_type: Type of glazing ("SingleClear", "DoubleTinted", etc.)678 orientation: Orientation ("N", "NE", etc.)679 hour: Hour of day (0-23)680 681 Returns:682 Corrected CLTD value (°C)683 """684 try:685 base_cltd = self.cltd_window_tables[self.latitude.value][glazing_type][orientation][hour]686 except KeyError:687 base_cltd = 0.0688 689 # Apply corrections690 latitude_factor = self.latitude_corrections.get(self.latitude.value, 1.0)691 month_factor = self.month_corrections.get(self.month, 1.0)692 temp_correction = (self.outdoor_avg_temp - 29.4) + (self.indoor_temp - 24.0)693 694 corrected_cltd = base_cltd * latitude_factor * month_factor + temp_correction695 return max(0.0, corrected_cltd)696 697 def get_cltd_door(self, door_type: str, orientation: str, hour: int) -> float:698 """699 Get CLTD for a door with corrections.700 701 Args:702 door_type: Type of door ("WoodSolid", "MetalInsulated", etc.)703 orientation: Orientation ("N", "NE", etc.)704 hour: Hour of day (0-23)705 706 Returns:707 Corrected CLTD value (°C)708 """709 try:710 base_cltd = self.cltd_door_tables[door_type][orientation][hour]711 except KeyError:712 base_cltd = 0.0713 714 # Apply corrections715 latitude_factor = self.latitude_corrections.get(self.latitude.value, 1.0)716 month_factor = self.month_corrections.get(self.month, 1.0)717 temp_correction = (self.outdoor_avg_temp - 29.4) + (self.indoor_temp - 24.0)718 719 corrected_cltd = base_cltd * latitude_factor * month_factor + temp_correction720 return max(0.0, corrected_cltd)721 722 def get_cltd_skylight(self, glazing_type: str, hour: int) -> float:723 """724 Get CLTD for a skylight with corrections.725 726 Args:727 glazing_type: Type of glazing ("SingleClear", "DoubleTinted", etc.)728 hour: Hour of day (0-23)729 730 Returns:731 Corrected CLTD value (°C)732 """733 try:734 base_cltd = self.cltd_skylight_tables[glazing_type]['Horizontal'][hour]735 except KeyError:736 base_cltd = 0.0737 738 # Apply corrections739 latitude_factor = self.latitude_corrections.get(self.latitude.value, 1.0)740 month_factor = self.month_corrections.get(self.month, 1.0)741 temp_correction = (self.outdoor_avg_temp - 29.4) + (self.indoor_temp - 24.0)742 743 corrected_cltd = base_cltd * latitude_factor * month_factor + temp_correction744 return max(0.0, corrected_cltd)745 746 747class WindowHeatGainCalculator:748 """Class for calculating window heat gain using CLTD/SCL method."""749 750 def __init__(self, cltd_calculator: CLTDCalculator):751 """752 Initialize window heat gain calculator.753 754 Args:755 cltd_calculator: Instance of CLTDCalculator756 """757 self.cltd_calculator = cltd_calculator758 759 def calculate_window_heat_gain(self, area: float, glazing_type: GlazingType, 760 frame_type: FrameType, orientation: str, hour: int, 761 drapery: Optional[Drapery] = None) -> Tuple[float, float]:762 """763 Calculate window heat gain (conduction and solar).764 765 Args:766 area: Window area (m²)767 glazing_type: Type of glazing768 frame_type: Type of frame769 orientation: Orientation ("N", "NE", etc.)770 hour: Hour of day (0-23)771 drapery: Drapery object (optional)772 773 Returns:774 Tuple of (conduction_heat_gain, solar_heat_gain) in Watts775 """776 # Get U-factor777 u_factor = WINDOW_U_FACTORS.get((glazing_type, frame_type), 7.22)778 779 # Get SHGC780 shgc = WINDOW_SHGC.get((glazing_type, frame_type), 0.78)781 782 # Get CLTD783 cltd = self.cltd_calculator.get_cltd_window(glazing_type.value, orientation, hour)784 785 # Calculate conduction heat gain786 conduction_reduction = drapery.get_conduction_reduction() if drapery and drapery.enabled else 0.0787 conduction_heat_gain = area * u_factor * cltd * (1.0 - conduction_reduction)788 789 # Get SCL from ASHRAE tables790 scl = self.cltd_calculator.ashrae_tables.get_scl(791 latitude=self.cltd_calculator.latitude.value,792 orientation=orientation,793 hour=hour,794 month=self.cltd_calculator.month795 )796 797 # Apply drapery shading coefficient798 shading_coefficient = drapery.get_shading_coefficient(shgc) if drapery and drapery.enabled else 1.0799 solar_heat_gain = area * shgc * scl * shading_coefficient800 801 return conduction_heat_gain, solar_heat_gain802 803 def calculate_skylight_heat_gain(self, area: float, glazing_type: GlazingType, 804 frame_type: FrameType, hour: int, 805 drapery: Optional[Drapery] = None) -> Tuple[float, float]:806 """807 Calculate skylight heat gain (conduction and solar).808 809 Args:810 area: Skylight area (m²)811 glazing_type: Type of glazing812 frame_type: Type of frame813 hour: Hour of day (0-23)814 drapery: Drapery object (optional)815 816 Returns:817 Tuple of (conduction_heat_gain, solar_heat_gain) in Watts818 """819 # Get U-factor820 u_factor = SKYLIGHT_U_FACTORS.get((glazing_type, frame_type), 7.79)821 822 # Get SHGC823 shgc = SKYLIGHT_SHGC.get((glazing_type, frame_type), 0.83)824 825 # Get CLTD826 cltd = self.cltd_calculator.get_cltd_skylight(glazing_type.value, hour)827 828 # Calculate conduction heat gain829 conduction_reduction = drapery.get_conduction_reduction() if drapery and drapery.enabled else 0.0830 conduction_heat_gain = area * u_factor * cltd * (1.0 - conduction_reduction)831 832 # Get SCL for skylight (horizontal)833 scl = self.cltd_calculator.ashrae_tables.get_scl(834 latitude=self.cltd_calculator.latitude.value,835 orientation='Horizontal',836 hour=hour,837 month=self.cltd_calculator.month838 )839 840 # Apply drapery shading coefficient841 shading_coefficient = drapery.get_shading_coefficient(shgc) if drapery and drapery.enabled else 1.0842 solar_heat_gain = area * shgc * scl * shading_coefficient843 844 return conduction_heat_gain, solar_heat_gain845 846 847class DoorHeatGainCalculator:848 """Class for calculating door heat gain using CLTD method."""849 850 def __init__(self, cltd_calculator: CLTDCalculator):851 """852 Initialize door heat gain calculator.853 854 Args:855 cltd_calculator: Instance of CLTDCalculator856 """857 self.cltd_calculator = cltd_calculator858 859 def calculate_door_heat_gain(self, area: float, door_type: str, orientation: str, 860 hour: int) -> float:861 """862 Calculate door heat gain (conduction only).863 864 Args:865 area: Door area (m²)866 door_type: Type of door ("WoodSolid", "MetalInsulated", etc.)867 orientation: Orientation ("N", "NE", etc.)868 hour: Hour of day (0-23)869 870 Returns:871 Conduction heat gain in Watts872 """873 # Get U-factor874 u_factor = DOOR_U_FACTORS.get(door_type, 3.00)875 876 # Get CLTD877 cltd = self.cltd_calculator.get_cltd_door(door_type, orientation, hour)878 879 # Calculate conduction heat gain880 conduction_heat_gain = area * u_factor * cltd881 882 return conduction_heat_gain883 884 885def calculate_total_heat_gain(window_area: float, glazing_type: GlazingType, 886 frame_type: FrameType, orientation: str, hour: int, 887 drapery: Optional[Drapery] = None, 888 door_area: float = 0.0, door_type: str = "WoodSolid", 889 skylight_area: float = 0.0) -> Dict[str, float]:890 """891 Calculate total heat gain for a fenestration system.892 893 Args:894 window_area: Window area (m²)895 glazing_type: Type of glazing896 frame_type: Type of frame897 orientation: Orientation ("N", "NE", etc.)898 hour: Hour of day (0-23)899 drapery: Drapery object (optional)900 door_area: Door area (m²)901 door_type: Type of door902 skylight_area: Skylight area (m²)903 904 Returns:905 Dictionary with conduction and solar heat gains (Watts)906 """907 cltd_calculator = CLTDCalculator()908 window_calculator = WindowHeatGainCalculator(cltd_calculator)909 door_calculator = DoorHeatGainCalculator(cltd_calculator)910 911 total_conduction = 0.0912 total_solar = 0.0913 914 # Calculate window heat gain915 if window_area > 0:916 conduction, solar = window_calculator.calculate_window_heat_gain(917 window_area, glazing_type, frame_type, orientation, hour, drapery918 )919 total_conduction += conduction920 total_solar += solar921 922 # Calculate skylight heat gain923 if skylight_area > 0:924 conduction, solar = window_calculator.calculate_skylight_heat_gain(925 skylight_area, glazing_type, frame_type, hour, drapery926 )927 total_conduction += conduction928 total_solar += solar929 930 # Calculate door heat gain931 if door_area > 0:932 conduction = door_calculator.calculate_door_heat_gain(933 door_area, door_type, orientation, hour934 )935 total_conduction += conduction936 937 return {938 "conduction_heat_gain": total_conduction,939 "solar_heat_gain": total_solar,940 "total_heat_gain": total_conduction + total_solar941 }