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 | |
} |