CoolFace
Apppublic

Building-science/HVAC

sourceHugging Faceupdated 1y agoView on Hugging Face
0likes
Drapery.py941 linesDownload Raw Back to root
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    }