""" 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, 32°N, 40°N, 48°N, 56°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 import logging logger = logging.getLogger(__name__) 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_32N = "32N" LAT_40N = "40N" LAT_48N = "48N" LAT_56N = "56N" # 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 _validate_inputs(self, drapery_type: str, orientation: str, hour: int, latitude: Any, month: str) -> Tuple[bool, str, str]: """Validate inputs for drapery shading coefficient calculations, following ASHRAE latitude handling.""" valid_drapery_types = list(self.shading_coefficients.keys()) valid_orientations = [e.value for e in Orientation] valid_latitudes = ['24N', '32N', '40N', '48N', '56N'] valid_months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] if drapery_type not in valid_drapery_types: return False, f"Invalid drapery type: {drapery_type}. Valid types: {valid_drapery_types}", "" if orientation not in valid_orientations: return False, f"Invalid orientation: {orientation}. Valid orientations: {valid_orientations}", "" if hour not in range(24): return False, "Hour must be between 0 and 23.", "" # Handle latitude input, following ASHRAE mapped_latitude = "" if latitude not in valid_latitudes: try: if isinstance(latitude, str): lat_str = latitude.upper().strip().replace('°', '').replace(' ', '') num_part = ''.join(c for c in lat_str if c.isdigit() or c == '.') lat_val = float(num_part) if 'S' in lat_str: lat_val = -lat_val else: lat_val = float(latitude) abs_lat = abs(lat_val) if abs_lat < 28: mapped_latitude = '24N' elif abs_lat < 36: mapped_latitude = '32N' elif abs_lat < 44: mapped_latitude = '40N' elif abs_lat < 52: mapped_latitude = '48N' else: mapped_latitude = '56N' except (ValueError, TypeError): return False, f"Invalid latitude: {latitude}. Valid latitudes: {valid_latitudes}", "" else: mapped_latitude = latitude if month not in valid_months: return False, f"Invalid month: {month}. Valid months: {valid_months}", "" return True, "Valid inputs.", mapped_latitude 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: Any = '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 (number, e.g., 40, or string, e.g., '40N') month: Month (1-12) """ self.indoor_temp = indoor_temp self.outdoor_max_temp = outdoor_max_temp self.outdoor_daily_range = outdoor_daily_range self.month = month self.outdoor_avg_temp = outdoor_max_temp - outdoor_daily_range / 2 # Validate and map latitude valid_latitudes = ['24N', '32N', '40N', '48N', '56N'] try: if isinstance(latitude, str): lat_str = latitude.upper().strip().replace('°', '').replace(' ', '') logger.debug(f"Processing latitude string: {lat_str}") num_part = ''.join(c for c in lat_str if c.isdigit() or c == '.') try: lat_val = float(num_part) except ValueError: logger.error(f"Failed to parse numerical part from latitude: {lat_str}") raise ValueError(f"Invalid latitude format: {latitude}. Expected format like '32N'") if 'S' in lat_str: lat_val = -lat_val else: lat_val = float(latitude) logger.debug(f"Processing numerical latitude: {lat_val}") abs_lat = abs(lat_val) if abs_lat < 28: mapped_latitude = '24N' elif abs_lat < 36: mapped_latitude = '32N' elif abs_lat < 44: mapped_latitude = '40N' elif abs_lat < 52: mapped_latitude = '48N' else: mapped_latitude = '56N' logger.debug(f"Mapped latitude: {lat_val} -> {mapped_latitude}") except (ValueError, TypeError) as e: logger.error(f"Invalid latitude: {latitude}. Defaulting to 40N. Error: {str(e)}") mapped_latitude = '40N' try: self.latitude = Latitude[mapped_latitude] logger.debug(f"Set latitude enum: {self.latitude}") except KeyError: logger.error(f"Latitude {mapped_latitude} not found in Latitude enum. Defaulting to LAT_40N") self.latitude = Latitude.LAT_40N # Initialize ASHRAE tables 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] } }, "32N": { "SingleClear": { "N": [3, 2, 1, 1, 1, 1, 2, 3, 4, 5, 6, 7, 7, 7, 6, 4, 3, 3, 3, 3, 3, 3, 3, 3], "NE": [3, 2, 1, 1, 1, 2, 5, 8, 10, 10, 8, 7, 5, 4, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3], "E": [3, 2, 1, 1, 1, 2, 6, 10, 12, 12, 10, 8, 6, 5, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3], "SE": [3, 2, 1, 1, 1, 1, 3, 5, 7, 9, 10, 10, 9, 8, 6, 4, 3, 3, 3, 3, 3, 3, 3, 3], "S": [3, 2, 1, 1, 1, 1, 2, 3, 4, 5, 6, 7, 8, 8, 7, 6, 5, 4, 3, 3, 3, 3, 3, 3], "SW": [3, 2, 1, 1, 1, 1, 2, 3, 4, 5, 6, 8, 9, 10, 10, 9, 7, 5, 4, 3, 3, 3, 3, 3], "W": [3, 2, 1, 1, 1, 1, 2, 3, 5, 7, 9, 10, 10, 10, 9, 8, 7, 6, 5, 4, 3, 3, 3, 3], "NW": [3, 2, 1, 1, 1, 1, 2, 4, 6, 8, 9, 9, 8, 7, 6, 5, 4, 3, 3, 3, 3, 3, 3, 3] }, "DoubleTinted": { "N": [2, 1, 0, 0, 0, 0, 1, 2, 3, 4, 4, 5, 5, 4, 3, 2, 1, 1, 1, 1, 1, 1, 1, 1], "NE": [2, 1, 0, 0, 0, 1, 4, 6, 8, 7, 6, 4, 3, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], "E": [2, 1, 0, 0, 0, 1, 4, 8, 9, 9, 8, 6, 4, 3, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1], "SE": [2, 1, 0, 0, 0, 0, 2, 4, 5, 7, 8, 8, 7, 6, 4, 2, 1, 1, 1, 1, 1, 1,1, 1], "S": [2, 1, 0, 0, 0, 0, 1, 2, 3, 4, 4, 5, 6, 6, 5, 4, 3, 2, 1, 1, 1, 1, 1, 1], "SW": [2, 1, 0, 0, 0, 0, 1, 2, 3, 4, 4, 6, 7, 8, 8, 7, 5, 3, 2, 1, 1, 1, 1, 1], "W": [2, 1, 0, 0, 0, 0, 1, 2, 4, 5, 7, 8, 8, 8, 7, 6, 5, 4, 3, 2, 1, 1, 1, 1], "NW": [2, 1, 0, 0, 0, 0, 1, 3, 4, 6, 7, 7, 6, 5, 4, 3, 2, 1, 1, 1, 1, 1, 1, 1] }, "LowE": { "N": [1, 0, 0, 0, 0, 0, 1, 1, 2, 3, 4, 4, 4, 4, 3, 2, 1, 1, 1, 1, 1, 1, 1, 1], "NE": [1, 0, 0, 0, 0, 1, 3, 5, 7, 6, 5, 4, 3, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], "E": [1, 0, 0, 0, 0, 1, 3, 7, 8, 8, 7, 5, 4, 3, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1], "SE": [1, 0, 0, 0, 0, 0, 1, 3, 4, 6, 7, 7, 6, 5, 4, 2, 1, 1, 1, 1, 1, 1, 1, 1], "S": [1, 0, 0, 0, 0, 0, 1, 1, 2, 3, 4, 5, 5, 5, 5, 4, 3, 2, 1, 1, 1, 1, 1, 1], "SW": [1, 0, 0, 0, 0, 0, 1, 1, 2, 3, 4, 5, 6, 7, 7, 6, 5, 3, 2, 1, 1, 1, 1, 1], "W": [1, 0, 0, 0, 0, 0, 1, 1, 3, 4, 6, 7, 7, 7, 6, 5, 4, 3, 2, 1, 1, 1, 1, 1], "NW": [1, 0, 0, 0, 0, 0, 1, 2, 4, 5, 6, 6, 6, 5, 4, 3, 2, 1, 1, 1, 1, 1, 1, 1] }, "Reflective": { "N": [0, 0, 0, 0, 0, 0, 0, 1, 1, 2, 3, 3, 3, 3, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1], "NE": [0, 0, 0, 0, 0, 0, 2, 4, 5, 5, 4, 3, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], "E": [0, 0, 0, 0, 0, 0, 2, 5, 6, 6, 5, 4, 3, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], "SE": [0, 0, 0, 0, 0, 0, 1, 2, 3, 5, 6, 6, 5, 4, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1], "S": [0, 0, 0, 0, 0, 0, 0, 1, 1, 2, 3, 3, 4, 4, 4, 3, 2, 1, 1, 1, 1, 1, 1, 1], "SW": [0, 0, 0, 0, 0, 0, 0, 1, 1, 2, 3, 4, 5, 6, 6, 5, 4, 2, 1, 1, 1, 1, 1, 1], "W": [0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 5, 6, 6, 6, 5, 4, 3, 2, 2, 1, 1, 1, 1, 1], "NW": [0, 0, 0, 0, 0, 0, 0, 1, 2, 4, 5, 5, 4, 4, 3, 2, 1, 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] } }, "56N": { "SingleClear": { "N": [0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 5, 4, 3, 2, 1, 0, 0, 0, 0, 0, 0, 0], "NE": [0, 0, 0, 0, 0, 0, 3, 6, 8, 7, 6, 4, 3, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0], "E": [0, 0, 0, 0, 0, 0, 4, 8, 10, 10, 8, 6, 4, 3, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0], "SE": [0, 0, 0, 0, 0, 0, 1, 3, 5, 7, 8, 8, 7, 6, 4, 2, 1, 0, 0, 0, 0, 0, 0, 0], "S": [0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 6, 5, 4, 3, 2, 1, 0, 0, 0, 0, 0], "SW": [0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 6, 7, 8, 8, 7, 5, 3, 2, 1, 0, 0, 0, 0], "W": [0, 0, 0, 0, 0, 0, 0, 1, 3, 5, 7, 8, 8, 8, 7, 6, 5, 4, 3, 2, 1, 0, 0, 0], "NW": [0, 0, 0, 0, 0, 0, 0, 2, 4, 6, 7, 7, 6, 5, 4, 3, 2, 1, 0, 0, 0, 0, 0, 0] }, "DoubleTinted": { "N": [0, 0, 0, 0, 0, 0, 0, 0, 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, 0, 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] }, "LowE": { "N": [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "NE": [0, 0, 0, 0, 0, 0, 1, 3, 5, 4, 3, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "E": [0, 0, 0, 0, 0, 0, 1, 5, 6, 6, 5, 3, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "SE": [0, 0, 0, 0, 0, 0, 0, 1, 2, 4, 5, 5, 4, 3, 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, 4, 5, 5, 4, 2, 0, 0, 0, 0, 0, 0, 0], "W": [0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 4, 5, 5, 5, 4, 3, 2, 1, 0, 0, 0, 0, 0, 0], "NW": [0, 0, 0, 0, 0, 0, 0, 0, 1, 3, 4, 4, 3, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0] }, "Reflective": { "N": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "NE": [0, 0, 0, 0, 0, 0, 0, 2, 3, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "E": [0, 0, 0, 0, 0, 0, 0, 3, 4, 4, 3, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "SE": [0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 3, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "S": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0], "SW": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 3, 3, 2, 1, 0, 0, 0, 0, 0, 0, 0], "W": [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 3, 3, 2, 1, 1, 0, 0, 0, 0, 0, 0, 0], "NW": [0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 1, 1, 0, 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) """ # Map glazing type to table keys glazing_key_map = { 'Single Clear': 'SingleClear', 'Double Tinted': 'DoubleTinted', 'Low-E': 'LowE', 'Reflective': 'Reflective' } glazing_key = glazing_key_map.get(glazing_type, glazing_type) logger.debug(f"get_cltd_window: glazing_type={glazing_type}, mapped_glazing_key={glazing_key}, orientation={orientation}, hour={hour}, latitude={self.latitude.value}") try: base_cltd = self.cltd_window_tables[self.latitude.value][glazing_key][orientation][hour] logger.debug(f"Base CLTD: {base_cltd}") except KeyError as e: logger.error(f"KeyError in cltd_window_tables: latitude={self.latitude.value}, glazing_key={glazing_key}, orientation={orientation}, hour={hour}. Error: {str(e)}") logger.warning("Using default CLTD=8.0°C") base_cltd = 8.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 logger.debug(f"Applied corrections: base_cltd={base_cltd}, latitude_factor={latitude_factor}, month_factor={month_factor}, temp_correction={temp_correction}, corrected_cltd={corrected_cltd}") 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 _validate_inputs(self, glazing_type: GlazingType, frame_type: FrameType, orientation: str, hour: int, latitude: Any, month: int) -> Tuple[bool, str, float]: """Validate inputs for window/skylight heat gain calculations, following ASHRAE.""" valid_orientations = ['North', 'Northeast', 'East', 'Southeast', 'South', 'Southwest', 'West', 'Northwest', 'Horizontal'] valid_latitudes = ['24N', '32N', '40N', '48N', '56N'] valid_months = list(range(1, 13)) valid_glazing_types = [e.value for e in GlazingType] valid_frame_types = [e.value for e in FrameType] if glazing_type.value not in valid_glazing_types: return False, f"Invalid glazing type: {glazing_type.value}. Valid types: {valid_glazing_types}", 0.0 if frame_type.value not in valid_frame_types: return False, f"Invalid frame type: {frame_type.value}. Valid types: {valid_frame_types}", 0.0 if orientation not in valid_orientations: return False, f"Invalid orientation: {orientation}. Valid orientations: {valid_orientations}", 0.0 if hour not in range(24): return False, "Hour must be between 0 and 23.", 0.0 if month not in valid_months: return False, f"Invalid month: {month}. Valid months: 1-12", 0.0 # Handle latitude input try: if isinstance(latitude, str): lat_str = latitude.upper().strip().replace('°', '').replace(' ', '') num_part = ''.join(c for c in lat_str if c.isdigit() or c == '.') lat_val = float(num_part) if 'S' in lat_str: lat_val = -lat_val else: lat_val = float(latitude) abs_lat = abs(lat_val) except (ValueError, TypeError): return False, f"Invalid latitude: {latitude}. Use number (e.g., 40) or string (e.g., '40N')", 0.0 return True, "Valid inputs.", abs_lat 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 """ # Validate inputs is_valid, error_msg, lat_val = self._validate_inputs( glazing_type, frame_type, orientation, hour, self.cltd_calculator.latitude.value, self.cltd_calculator.month ) if not is_valid: raise ValueError(error_msg) # 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) # Interpolate SCL for latitude latitudes = [24, 32, 40, 48, 56] lat1 = max([lat for lat in latitudes if lat <= lat_val], default=24) lat2 = min([lat for lat in latitudes if lat >= lat_val], default=56) scl1 = self.cltd_calculator.ashrae_tables.get_scl(f"{lat1}N", orientation, hour, self.cltd_calculator.month) scl2 = self.cltd_calculator.ashrae_tables.get_scl(f"{lat2}N", orientation, hour, self.cltd_calculator.month) if lat1 == lat2: scl = scl1 else: weight = (lat_val - lat1) / (lat2 - lat1) scl = scl1 + weight * (scl2 - scl1) # 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 """ # Validate inputs is_valid, error_msg, lat_val = self._validate_inputs( glazing_type, frame_type, 'Horizontal', hour, self.cltd_calculator.latitude.value, self.cltd_calculator.month ) if not is_valid: raise ValueError(error_msg) # 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) # Interpolate SCL for latitude latitudes = [24, 32, 40, 48, 56] lat1 = max([lat for lat in latitudes if lat <= lat_val], default=24) lat2 = min([lat for lat in latitudes if lat >= lat_val], default=56) scl1 = self.cltd_calculator.ashrae_tables.get_scl(f"{lat1}N", 'Horizontal', hour, self.cltd_calculator.month) scl2 = self.cltd_calculator.ashrae_tables.get_scl(f"{lat2}N", 'Horizontal', hour, self.cltd_calculator.month) if lat1 == lat2: scl = scl1 else: weight = (lat_val - lat1) / (lat2 - lat1) scl = scl1 + weight * (scl2 - scl1) # 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 }