""" Enhanced Drapery module for HVAC Load Calculator with comprehensive CLTD implementation and SCL integration. This module provides classes and functions for handling drapery properties and calculating their effects on window heat transfer using detailed ASHRAE CLTD/SCL methods. Includes comprehensive CLTD tables for windows (SingleClear, DoubleTinted, LowE, Reflective) at multiple latitudes (24°N, 40°N, 48°N) and all orientations, as well as detailed climatic corrections and door CLTD calculations. Enhanced to map UI shading coefficients to drapery properties (openness, color, fullness) and apply conduction reduction (5-15%) based on openness per ASHRAE guidelines. """ from typing import Dict, Any, Optional, Tuple, List, Union from enum import Enum import math import pandas as pd from data.ashrae_tables import ASHRAETables class DraperyOpenness(Enum): """Enum for drapery openness classification.""" OPEN = "Open (>25%)" SEMI_OPEN = "Semi-open (7-25%)" CLOSED = "Closed (0-7%)" class DraperyColor(Enum): """Enum for drapery color/reflectance classification.""" DARK = "Dark (0-25%)" MEDIUM = "Medium (25-50%)" LIGHT = "Light (>50%)" class GlazingType(Enum): """Enum for glazing types.""" SINGLE_CLEAR = "Single Clear" SINGLE_TINTED = "Single Tinted" DOUBLE_CLEAR = "Double Clear" DOUBLE_TINTED = "Double Tinted" LOW_E = "Low-E" REFLECTIVE = "Reflective" class FrameType(Enum): """Enum for window frame types.""" ALUMINUM = "Aluminum without Thermal Break" ALUMINUM_THERMAL_BREAK = "Aluminum with Thermal Break" VINYL = "Vinyl/Fiberglass" WOOD = "Wood/Vinyl-Clad Wood" INSULATED = "Insulated" class SurfaceColor(Enum): """Enum for surface color classification.""" DARK = "Dark" MEDIUM = "Medium" LIGHT = "Light" class Latitude(Enum): """Enum for latitude ranges.""" LAT_24N = "24N" LAT_40N = "40N" LAT_48N = "48N" # U-Factors for various fenestration products (Table 9-1) in SI units (W/m²K) # Format: {(glazing_type, frame_type): u_factor} WINDOW_U_FACTORS = { # Single Clear Glass (GlazingType.SINGLE_CLEAR, FrameType.ALUMINUM): 7.22, (GlazingType.SINGLE_CLEAR, FrameType.ALUMINUM_THERMAL_BREAK): 6.14, (GlazingType.SINGLE_CLEAR, FrameType.VINYL): 5.11, (GlazingType.SINGLE_CLEAR, FrameType.WOOD): 5.06, (GlazingType.SINGLE_CLEAR, FrameType.INSULATED): 4.60, # Single Tinted Glass (GlazingType.SINGLE_TINTED, FrameType.ALUMINUM): 7.22, (GlazingType.SINGLE_TINTED, FrameType.ALUMINUM_THERMAL_BREAK): 6.14, (GlazingType.SINGLE_TINTED, FrameType.VINYL): 5.11, (GlazingType.SINGLE_TINTED, FrameType.WOOD): 5.06, (GlazingType.SINGLE_TINTED, FrameType.INSULATED): 4.60, # Double Clear Glass (GlazingType.DOUBLE_CLEAR, FrameType.ALUMINUM): 4.60, (GlazingType.DOUBLE_CLEAR, FrameType.ALUMINUM_THERMAL_BREAK): 3.41, (GlazingType.DOUBLE_CLEAR, FrameType.VINYL): 3.01, (GlazingType.DOUBLE_CLEAR, FrameType.WOOD): 2.90, (GlazingType.DOUBLE_CLEAR, FrameType.INSULATED): 2.50, # Double Tinted Glass (GlazingType.DOUBLE_TINTED, FrameType.ALUMINUM): 4.60, (GlazingType.DOUBLE_TINTED, FrameType.ALUMINUM_THERMAL_BREAK): 3.41, (GlazingType.DOUBLE_TINTED, FrameType.VINYL): 3.01, (GlazingType.DOUBLE_TINTED, FrameType.WOOD): 2.90, (GlazingType.DOUBLE_TINTED, FrameType.INSULATED): 2.50, # Low-E Glass (GlazingType.LOW_E, FrameType.ALUMINUM): 3.41, (GlazingType.LOW_E, FrameType.ALUMINUM_THERMAL_BREAK): 2.67, (GlazingType.LOW_E, FrameType.VINYL): 2.33, (GlazingType.LOW_E, FrameType.WOOD): 2.22, (GlazingType.LOW_E, FrameType.INSULATED): 1.87, # Reflective Glass (GlazingType.REFLECTIVE, FrameType.ALUMINUM): 3.41, (GlazingType.REFLECTIVE, FrameType.ALUMINUM_THERMAL_BREAK): 2.67, (GlazingType.REFLECTIVE, FrameType.VINYL): 2.33, (GlazingType.REFLECTIVE, FrameType.WOOD): 2.22, (GlazingType.REFLECTIVE, FrameType.INSULATED): 1.87, } # SHGC values for various glazing types (Table 9-3) # Format: {(glazing_type, frame_type): shgc} WINDOW_SHGC = { # Single Clear Glass (GlazingType.SINGLE_CLEAR, FrameType.ALUMINUM): 0.78, (GlazingType.SINGLE_CLEAR, FrameType.ALUMINUM_THERMAL_BREAK): 0.75, (GlazingType.SINGLE_CLEAR, FrameType.VINYL): 0.67, (GlazingType.SINGLE_CLEAR, FrameType.WOOD): 0.65, (GlazingType.SINGLE_CLEAR, FrameType.INSULATED): 0.63, # Single Tinted Glass (GlazingType.SINGLE_TINTED, FrameType.ALUMINUM): 0.65, (GlazingType.SINGLE_TINTED, FrameType.ALUMINUM_THERMAL_BREAK): 0.62, (GlazingType.SINGLE_TINTED, FrameType.VINYL): 0.55, (GlazingType.SINGLE_TINTED, FrameType.WOOD): 0.53, (GlazingType.SINGLE_TINTED, FrameType.INSULATED): 0.52, # Double Clear Glass (GlazingType.DOUBLE_CLEAR, FrameType.ALUMINUM): 0.65, (GlazingType.DOUBLE_CLEAR, FrameType.ALUMINUM_THERMAL_BREAK): 0.61, (GlazingType.DOUBLE_CLEAR, FrameType.VINYL): 0.53, (GlazingType.DOUBLE_CLEAR, FrameType.WOOD): 0.51, (GlazingType.DOUBLE_CLEAR, FrameType.INSULATED): 0.49, # Double Tinted Glass (GlazingType.DOUBLE_TINTED, FrameType.ALUMINUM): 0.53, (GlazingType.DOUBLE_TINTED, FrameType.ALUMINUM_THERMAL_BREAK): 0.50, (GlazingType.DOUBLE_TINTED, FrameType.VINYL): 0.42, (GlazingType.DOUBLE_TINTED, FrameType.WOOD): 0.40, (GlazingType.DOUBLE_TINTED, FrameType.INSULATED): 0.38, # Low-E Glass (GlazingType.LOW_E, FrameType.ALUMINUM): 0.46, (GlazingType.LOW_E, FrameType.ALUMINUM_THERMAL_BREAK): 0.44, (GlazingType.LOW_E, FrameType.VINYL): 0.38, (GlazingType.LOW_E, FrameType.WOOD): 0.36, (GlazingType.LOW_E, FrameType.INSULATED): 0.34, # Reflective Glass (GlazingType.REFLECTIVE, FrameType.ALUMINUM): 0.33, (GlazingType.REFLECTIVE, FrameType.ALUMINUM_THERMAL_BREAK): 0.31, (GlazingType.REFLECTIVE, FrameType.VINYL): 0.27, (GlazingType.REFLECTIVE, FrameType.WOOD): 0.25, (GlazingType.REFLECTIVE, FrameType.INSULATED): 0.24, } # Door U-Factors in SI units (W/m²K) # Format: {door_type: u_factor} DOOR_U_FACTORS = { "WoodSolid": 3.35, # Approximated from Group D walls "MetalInsulated": 2.61, # Approximated from Group F walls "GlassDoor": 7.22, # Same as single clear glass with aluminum frame "InsulatedMetal": 2.15, # Insulated metal door "InsulatedWood": 1.93, # Insulated wood door "Custom": 3.00, # Default for custom doors } # Skylight U-Factors in SI units (W/m²K) # Format: {(glazing_type, frame_type): u_factor} SKYLIGHT_U_FACTORS = { # Single Clear Glass (GlazingType.SINGLE_CLEAR, FrameType.ALUMINUM): 7.79, (GlazingType.SINGLE_CLEAR, FrameType.ALUMINUM_THERMAL_BREAK): 6.71, (GlazingType.SINGLE_CLEAR, FrameType.VINYL): 5.68, (GlazingType.SINGLE_CLEAR, FrameType.WOOD): 5.63, (GlazingType.SINGLE_CLEAR, FrameType.INSULATED): 5.17, # Single Tinted Glass (GlazingType.SINGLE_TINTED, FrameType.ALUMINUM): 7.79, (GlazingType.SINGLE_TINTED, FrameType.ALUMINUM_THERMAL_BREAK): 6.71, (GlazingType.SINGLE_TINTED, FrameType.VINYL): 5.68, (GlazingType.SINGLE_TINTED, FrameType.WOOD): 5.63, (GlazingType.SINGLE_TINTED, FrameType.INSULATED): 5.17, # Double Clear Glass (GlazingType.DOUBLE_CLEAR, FrameType.ALUMINUM): 5.17, (GlazingType.DOUBLE_CLEAR, FrameType.ALUMINUM_THERMAL_BREAK): 3.98, (GlazingType.DOUBLE_CLEAR, FrameType.VINYL): 3.58, (GlazingType.DOUBLE_CLEAR, FrameType.WOOD): 3.47, (GlazingType.DOUBLE_CLEAR, FrameType.INSULATED): 3.07, # Double Tinted Glass (GlazingType.DOUBLE_TINTED, FrameType.ALUMINUM): 5.17, (GlazingType.DOUBLE_TINTED, FrameType.ALUMINUM_THERMAL_BREAK): 3.98, (GlazingType.DOUBLE_TINTED, FrameType.VINYL): 3.58, (GlazingType.DOUBLE_TINTED, FrameType.WOOD): 3.47, (GlazingType.DOUBLE_TINTED, FrameType.INSULATED): 3.07, # Low-E Glass (GlazingType.LOW_E, FrameType.ALUMINUM): 3.98, (GlazingType.LOW_E, FrameType.ALUMINUM_THERMAL_BREAK): 3.24, (GlazingType.LOW_E, FrameType.VINYL): 2.90, (GlazingType.LOW_E, FrameType.WOOD): 2.78, (GlazingType.LOW_E, FrameType.INSULATED): 2.44, # Reflective Glass (GlazingType.REFLECTIVE, FrameType.ALUMINUM): 3.98, (GlazingType.REFLECTIVE, FrameType.ALUMINUM_THERMAL_BREAK): 3.24, (GlazingType.REFLECTIVE, FrameType.VINYL): 2.90, (GlazingType.REFLECTIVE, FrameType.WOOD): 2.78, (GlazingType.REFLECTIVE, FrameType.INSULATED): 2.44, } # Skylight SHGC values # Format: {(glazing_type, frame_type): shgc} SKYLIGHT_SHGC = { # Single Clear Glass (GlazingType.SINGLE_CLEAR, FrameType.ALUMINUM): 0.83, (GlazingType.SINGLE_CLEAR, FrameType.ALUMINUM_THERMAL_BREAK): 0.80, (GlazingType.SINGLE_CLEAR, FrameType.VINYL): 0.72, (GlazingType.SINGLE_CLEAR, FrameType.WOOD): 0.70, (GlazingType.SINGLE_CLEAR, FrameType.INSULATED): 0.68, # Single Tinted Glass (GlazingType.SINGLE_TINTED, FrameType.ALUMINUM): 0.70, (GlazingType.SINGLE_TINTED, FrameType.ALUMINUM_THERMAL_BREAK): 0.67, (GlazingType.SINGLE_TINTED, FrameType.VINYL): 0.60, (GlazingType.SINGLE_TINTED, FrameType.WOOD): 0.58, (GlazingType.SINGLE_TINTED, FrameType.INSULATED): 0.57, # Double Clear Glass (GlazingType.DOUBLE_CLEAR, FrameType.ALUMINUM): 0.70, (GlazingType.DOUBLE_CLEAR, FrameType.ALUMINUM_THERMAL_BREAK): 0.66, (GlazingType.DOUBLE_CLEAR, FrameType.VINYL): 0.58, (GlazingType.DOUBLE_CLEAR, FrameType.WOOD): 0.56, (GlazingType.DOUBLE_CLEAR, FrameType.INSULATED): 0.54, # Double Tinted Glass (GlazingType.DOUBLE_TINTED, FrameType.ALUMINUM): 0.58, (GlazingType.DOUBLE_TINTED, FrameType.ALUMINUM_THERMAL_BREAK): 0.55, (GlazingType.DOUBLE_TINTED, FrameType.VINYL): 0.47, (GlazingType.DOUBLE_TINTED, FrameType.WOOD): 0.45, (GlazingType.DOUBLE_TINTED, FrameType.INSULATED): 0.43, # Low-E Glass (GlazingType.LOW_E, FrameType.ALUMINUM): 0.51, (GlazingType.LOW_E, FrameType.ALUMINUM_THERMAL_BREAK): 0.49, (GlazingType.LOW_E, FrameType.VINYL): 0.43, (GlazingType.LOW_E, FrameType.WOOD): 0.41, (GlazingType.LOW_E, FrameType.INSULATED): 0.39, # Reflective Glass (GlazingType.REFLECTIVE, FrameType.ALUMINUM): 0.38, (GlazingType.REFLECTIVE, FrameType.ALUMINUM_THERMAL_BREAK): 0.36, (GlazingType.REFLECTIVE, FrameType.VINYL): 0.32, (GlazingType.REFLECTIVE, FrameType.WOOD): 0.30, (GlazingType.REFLECTIVE, FrameType.INSULATED): 0.29, } class Drapery: """Class for handling drapery properties and effects on window heat transfer.""" def __init__(self, openness: str = "Semi-Open", color: str = "Medium", fullness: float = 1.5, enabled: bool = True, shading_device: str = "Drapes"): """ Initialize drapery properties with UI-compatible inputs. Args: openness: Drapery openness category ("Closed", "Semi-Open", "Open") color: Drapery color category ("Light", "Medium", "Dark") fullness: Fullness factor (1.0 for flat, 1.0-2.0 for pleated) enabled: Whether drapery is enabled shading_device: Type of shading device ("Venetian Blinds", "Drapes", etc.) """ self.openness = openness self.color = color self.fullness = fullness self.enabled = enabled self.shading_device = shading_device def get_openness_category(self) -> str: """Get openness category as string.""" return self.openness def get_color_category(self) -> str: """Get color category as string.""" return self.color def get_shading_coefficient(self, shgc: float = 0.5) -> float: """ Calculate shading coefficient for drapery based on UI inputs. Args: shgc: Solar Heat Gain Coefficient of window (default 0.5) Returns: Shading coefficient (0.0-1.0) """ if not self.enabled: return 1.0 # Mapping of UI shading devices to properties mapping = { ("Venetian Blinds", "Light"): {"openness": "Semi-Open", "color": "Light", "fullness": 1.0, "sc": 0.6}, ("Venetian Blinds", "Medium"): {"openness": "Semi-Open", "color": "Medium", "fullness": 1.0, "sc": 0.65}, ("Venetian Blinds", "Dark"): {"openness": "Semi-Open", "color": "Dark", "fullness": 1.0, "sc": 0.7}, ("Drapes", "Light"): {"openness": "Closed", "color": "Light", "fullness": 1.5, "sc": 0.59}, ("Drapes", "Medium"): {"openness": "Closed", "color": "Medium", "fullness": 1.5, "sc": 0.74}, ("Drapes", "Dark"): {"openness": "Closed", "color": "Dark", "fullness": 1.5, "sc": 0.87}, ("Roller Shades", "Light"): {"openness": "Open", "color": "Light", "fullness": 1.0, "sc": 0.8}, ("Roller Shades", "Medium"): {"openness": "Open", "color": "Medium", "fullness": 1.0, "sc": 0.88}, ("Roller Shades", "Dark"): {"openness": "Open", "color": "Dark", "fullness": 1.0, "sc": 0.94}, } # Get shading coefficient from mapping or default to table-based value properties = mapping.get((self.shading_device, self.color), { "openness": self.openness, "color": self.color, "fullness": self.fullness, "sc": 0.85 }) base_sc = properties["sc"] # Adjust for fullness if different from mapped value if self.fullness != properties["fullness"]: fullness_factor = 1.0 - 0.05 * (self.fullness - 1.0) base_sc *= fullness_factor return base_sc def get_conduction_reduction(self) -> float: """ Get conduction reduction factor based on openness. Returns: Reduction factor (0.05-0.15) """ reductions = { "Closed": 0.15, # 15% reduction "Semi-Open": 0.10, # 10% reduction "Open": 0.05 # 5% reduction } return reductions.get(self.openness, 0.10) class CLTDCalculator: """Class for calculating Cooling Load Temperature Difference (CLTD) values.""" def __init__(self, indoor_temp: float = 25.6, outdoor_max_temp: float = 35.0, outdoor_daily_range: float = 11.7, latitude: Latitude = Latitude.LAT_40N, month: int = 7): """ Initialize CLTD calculator. Args: indoor_temp: Indoor design temperature (°C) outdoor_max_temp: Outdoor maximum temperature (°C) outdoor_daily_range: Daily temperature range (°C) latitude: Latitude category (24°N, 40°N, 48°N) month: Month (1-12) """ self.indoor_temp = indoor_temp # °C self.outdoor_max_temp = outdoor_max_temp # °C self.outdoor_daily_range = outdoor_daily_range # °C self.latitude = latitude self.month = month self.outdoor_avg_temp = outdoor_max_temp - outdoor_daily_range / 2 # Initialize ASHRAE tables for SCL data self.ashrae_tables = ASHRAETables() # Load CLTD tables self.cltd_window_tables = self._load_cltd_window_table() self.cltd_door_tables = self._load_cltd_door_table() self.cltd_skylight_tables = self._load_cltd_skylight_table() # Load correction factors self.latitude_corrections = self._load_latitude_correction() self.month_corrections = self._load_month_correction() def _load_cltd_window_table(self) -> Dict[str, Dict[str, pd.DataFrame]]: """ Load CLTD tables for windows at multiple latitudes (July). Returns: Dictionary of DataFrames with CLTD values indexed by hour (0-23) and columns for orientations (N, NE, E, SE, S, SW, W, NW) """ hours = list(range(24)) # Comprehensive window CLTD data for different latitudes, glazing types, and orientations window_cltd_data = { "24N": { "SingleClear": { "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], "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], "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], "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], "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], "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], "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], "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] }, "DoubleTinted": { "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], "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], "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], "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], "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], "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], "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], "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] }, "LowE": { "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], "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], "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], "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], "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], "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], "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], "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] }, "Reflective": { "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], "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], "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], "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], "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], "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], "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], "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] } }, "40N": { "SingleClear": { "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], "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], "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], "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], "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], "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], "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], "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] }, "DoubleTinted": { "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], "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], "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], "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], "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], "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], "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], "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] }, "LowE": { "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], "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], "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], "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], "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], "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], "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], "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] }, "Reflective": { "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], "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], "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], "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], "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], "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], "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], "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] } }, "48N": { "SingleClear": { "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], "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], "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], "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], "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], "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], "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], "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] }, "DoubleTinted": { "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], "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], "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], "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], "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], "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], "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], "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] }, "LowE": { "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], "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], "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], "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], "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], "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], "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], "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] }, "Reflective": { "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], "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], "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], "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], "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], "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], "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], "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] } } } # Convert to DataFrames window_cltd_tables = {} for latitude, glazing_data in window_cltd_data.items(): window_cltd_tables[latitude] = {} for glazing_type, orientation_data in glazing_data.items(): window_cltd_tables[latitude][glazing_type] = pd.DataFrame(orientation_data, index=hours) return window_cltd_tables def _load_cltd_door_table(self) -> Dict[str, pd.DataFrame]: """ Load CLTD tables for doors. Returns: Dictionary of DataFrames with CLTD values indexed by hour (0-23) """ hours = list(range(24)) # Door CLTD data approximated from wall groups door_cltd_data = { "WoodSolid": { # Approximated from Group D walls '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] }, "MetalInsulated": { # Approximated from Group F walls '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] }, "GlassDoor": { # Same as single clear glass '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], '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], '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], '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], '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], '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], '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], '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] }, "InsulatedMetal": { # Enhanced insulated metal door '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], '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], '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], '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], '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], '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], '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], '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] }, "InsulatedWood": { # Enhanced insulated wood door '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] }, "Custom": { # Default for custom doors '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] } } # Convert to DataFrames door_cltd_tables = {} for door_type, orientation_data in door_cltd_data.items(): door_cltd_tables[door_type] = pd.DataFrame(orientation_data, index=hours) return door_cltd_tables def _load_cltd_skylight_table(self) -> Dict[str, pd.DataFrame]: """ Load CLTD tables for skylights (flat, 0° slope). Returns: Dictionary of DataFrames with CLTD values indexed by hour (0-23) """ hours = list(range(24)) # Skylight CLTD data for 40°N latitude, July skylight_cltd_data = { "SingleClear": { '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] }, "DoubleTinted": { '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] }, "LowE": { '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] }, "Reflective": { '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] } } # Convert to DataFrames skylight_cltd_tables = {} for glazing_type, orientation_data in skylight_cltd_data.items(): skylight_cltd_tables[glazing_type] = pd.DataFrame(orientation_data, index=hours) return skylight_cltd_tables def _load_latitude_correction(self) -> Dict[str, float]: """ Load latitude correction factors for CLTD. Returns: Dictionary of correction factors by latitude """ return { "24N": 0.95, "40N": 1.00, "48N": 1.05 } def _load_month_correction(self) -> Dict[int, float]: """ Load month correction factors for CLTD. Returns: Dictionary of correction factors by month """ return { 1: 0.85, 2: 0.90, 3: 0.95, 4: 0.98, 5: 1.00, 6: 1.02, 7: 1.00, 8: 0.98, 9: 0.95, 10: 0.90, 11: 0.85, 12: 0.80 } def get_cltd_window(self, glazing_type: str, orientation: str, hour: int) -> float: """ Get CLTD for a window with corrections. Args: glazing_type: Type of glazing ("SingleClear", "DoubleTinted", etc.) orientation: Orientation ("N", "NE", etc.) hour: Hour of day (0-23) Returns: Corrected CLTD value (°C) """ try: base_cltd = self.cltd_window_tables[self.latitude.value][glazing_type][orientation][hour] except KeyError: base_cltd = 0.0 # Apply corrections latitude_factor = self.latitude_corrections.get(self.latitude.value, 1.0) month_factor = self.month_corrections.get(self.month, 1.0) temp_correction = (self.outdoor_avg_temp - 29.4) + (self.indoor_temp - 24.0) corrected_cltd = base_cltd * latitude_factor * month_factor + temp_correction return max(0.0, corrected_cltd) def get_cltd_door(self, door_type: str, orientation: str, hour: int) -> float: """ Get CLTD for a door with corrections. Args: door_type: Type of door ("WoodSolid", "MetalInsulated", etc.) orientation: Orientation ("N", "NE", etc.) hour: Hour of day (0-23) Returns: Corrected CLTD value (°C) """ try: base_cltd = self.cltd_door_tables[door_type][orientation][hour] except KeyError: base_cltd = 0.0 # Apply corrections latitude_factor = self.latitude_corrections.get(self.latitude.value, 1.0) month_factor = self.month_corrections.get(self.month, 1.0) temp_correction = (self.outdoor_avg_temp - 29.4) + (self.indoor_temp - 24.0) corrected_cltd = base_cltd * latitude_factor * month_factor + temp_correction return max(0.0, corrected_cltd) def get_cltd_skylight(self, glazing_type: str, hour: int) -> float: """ Get CLTD for a skylight with corrections. Args: glazing_type: Type of glazing ("SingleClear", "DoubleTinted", etc.) hour: Hour of day (0-23) Returns: Corrected CLTD value (°C) """ try: base_cltd = self.cltd_skylight_tables[glazing_type]['Horizontal'][hour] except KeyError: base_cltd = 0.0 # Apply corrections latitude_factor = self.latitude_corrections.get(self.latitude.value, 1.0) month_factor = self.month_corrections.get(self.month, 1.0) temp_correction = (self.outdoor_avg_temp - 29.4) + (self.indoor_temp - 24.0) corrected_cltd = base_cltd * latitude_factor * month_factor + temp_correction return max(0.0, corrected_cltd) class WindowHeatGainCalculator: """Class for calculating window heat gain using CLTD/SCL method.""" def __init__(self, cltd_calculator: CLTDCalculator): """ Initialize window heat gain calculator. Args: cltd_calculator: Instance of CLTDCalculator """ self.cltd_calculator = cltd_calculator def calculate_window_heat_gain(self, area: float, glazing_type: GlazingType, frame_type: FrameType, orientation: str, hour: int, drapery: Optional[Drapery] = None) -> Tuple[float, float]: """ Calculate window heat gain (conduction and solar). Args: area: Window area (m²) glazing_type: Type of glazing frame_type: Type of frame orientation: Orientation ("N", "NE", etc.) hour: Hour of day (0-23) drapery: Drapery object (optional) Returns: Tuple of (conduction_heat_gain, solar_heat_gain) in Watts """ # Get U-factor u_factor = WINDOW_U_FACTORS.get((glazing_type, frame_type), 7.22) # Get SHGC shgc = WINDOW_SHGC.get((glazing_type, frame_type), 0.78) # Get CLTD cltd = self.cltd_calculator.get_cltd_window(glazing_type.value, orientation, hour) # Calculate conduction heat gain conduction_reduction = drapery.get_conduction_reduction() if drapery and drapery.enabled else 0.0 conduction_heat_gain = area * u_factor * cltd * (1.0 - conduction_reduction) # Get SCL from ASHRAE tables scl = self.cltd_calculator.ashrae_tables.get_scl( latitude=self.cltd_calculator.latitude.value, orientation=orientation, hour=hour, month=self.cltd_calculator.month ) # Apply drapery shading coefficient shading_coefficient = drapery.get_shading_coefficient(shgc) if drapery and drapery.enabled else 1.0 solar_heat_gain = area * shgc * scl * shading_coefficient return conduction_heat_gain, solar_heat_gain def calculate_skylight_heat_gain(self, area: float, glazing_type: GlazingType, frame_type: FrameType, hour: int, drapery: Optional[Drapery] = None) -> Tuple[float, float]: """ Calculate skylight heat gain (conduction and solar). Args: area: Skylight area (m²) glazing_type: Type of glazing frame_type: Type of frame hour: Hour of day (0-23) drapery: Drapery object (optional) Returns: Tuple of (conduction_heat_gain, solar_heat_gain) in Watts """ # Get U-factor u_factor = SKYLIGHT_U_FACTORS.get((glazing_type, frame_type), 7.79) # Get SHGC shgc = SKYLIGHT_SHGC.get((glazing_type, frame_type), 0.83) # Get CLTD cltd = self.cltd_calculator.get_cltd_skylight(glazing_type.value, hour) # Calculate conduction heat gain conduction_reduction = drapery.get_conduction_reduction() if drapery and drapery.enabled else 0.0 conduction_heat_gain = area * u_factor * cltd * (1.0 - conduction_reduction) # Get SCL for skylight (horizontal) scl = self.cltd_calculator.ashrae_tables.get_scl( latitude=self.cltd_calculator.latitude.value, orientation='Horizontal', hour=hour, month=self.cltd_calculator.month ) # Apply drapery shading coefficient shading_coefficient = drapery.get_shading_coefficient(shgc) if drapery and drapery.enabled else 1.0 solar_heat_gain = area * shgc * scl * shading_coefficient return conduction_heat_gain, solar_heat_gain class DoorHeatGainCalculator: """Class for calculating door heat gain using CLTD method.""" def __init__(self, cltd_calculator: CLTDCalculator): """ Initialize door heat gain calculator. Args: cltd_calculator: Instance of CLTDCalculator """ self.cltd_calculator = cltd_calculator def calculate_door_heat_gain(self, area: float, door_type: str, orientation: str, hour: int) -> float: """ Calculate door heat gain (conduction only). Args: area: Door area (m²) door_type: Type of door ("WoodSolid", "MetalInsulated", etc.) orientation: Orientation ("N", "NE", etc.) hour: Hour of day (0-23) Returns: Conduction heat gain in Watts """ # Get U-factor u_factor = DOOR_U_FACTORS.get(door_type, 3.00) # Get CLTD cltd = self.cltd_calculator.get_cltd_door(door_type, orientation, hour) # Calculate conduction heat gain conduction_heat_gain = area * u_factor * cltd return conduction_heat_gain def calculate_total_heat_gain(window_area: float, glazing_type: GlazingType, frame_type: FrameType, orientation: str, hour: int, drapery: Optional[Drapery] = None, door_area: float = 0.0, door_type: str = "WoodSolid", skylight_area: float = 0.0) -> Dict[str, float]: """ Calculate total heat gain for a fenestration system. Args: window_area: Window area (m²) glazing_type: Type of glazing frame_type: Type of frame orientation: Orientation ("N", "NE", etc.) hour: Hour of day (0-23) drapery: Drapery object (optional) door_area: Door area (m²) door_type: Type of door skylight_area: Skylight area (m²) Returns: Dictionary with conduction and solar heat gains (Watts) """ cltd_calculator = CLTDCalculator() window_calculator = WindowHeatGainCalculator(cltd_calculator) door_calculator = DoorHeatGainCalculator(cltd_calculator) total_conduction = 0.0 total_solar = 0.0 # Calculate window heat gain if window_area > 0: conduction, solar = window_calculator.calculate_window_heat_gain( window_area, glazing_type, frame_type, orientation, hour, drapery ) total_conduction += conduction total_solar += solar # Calculate skylight heat gain if skylight_area > 0: conduction, solar = window_calculator.calculate_skylight_heat_gain( skylight_area, glazing_type, frame_type, hour, drapery ) total_conduction += conduction total_solar += solar # Calculate door heat gain if door_area > 0: conduction = door_calculator.calculate_door_heat_gain( door_area, door_type, orientation, hour ) total_conduction += conduction return { "conduction_heat_gain": total_conduction, "solar_heat_gain": total_solar, "total_heat_gain": total_conduction + total_solar }