HVAC-03 / data /drapery.py
mabuseif's picture
Update data/drapery.py
dd71ab7 verified
"""
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
}