Spaces:
Sleeping
Sleeping
| """ | |
| 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 | |
| } |