HVAC-03 / data /ashrae_tablesxx.py
mabuseif's picture
Rename data/ashrae_tables.py to data/ashrae_tablesxx.py
f9df166 verified
"""
ASHRAE tables module for HVAC Load Calculator.
Integrates CLTD, SCL, CLF tables, cooling load calculations, climatic corrections, and visualization.
Combines data from original ashrae_tables.py and enhanced versions with ashrae_tables (3).py.
"""
from typing import Dict, List, Any, Optional, Tuple
import pandas as pd
import numpy as np
import os
import matplotlib.pyplot as plt
from enum import Enum
# Define paths
DATA_DIR = os.path.dirname(os.path.abspath(__file__))
class WallGroup(Enum):
"""Enumeration for ASHRAE wall groups."""
A = "A" # Light construction
B = "B"
C = "C"
D = "D"
E = "E"
F = "F"
G = "G"
H = "H" # Heavy construction
class RoofGroup(Enum):
"""Enumeration for ASHRAE roof groups."""
A = "A" # Light construction
B = "B"
C = "C"
D = "D"
E = "E"
F = "F"
G = "G" # Heavy construction
class Orientation(Enum):
"""Enumeration for building component orientations."""
N = "North"
NE = "Northeast"
E = "East"
SE = "Southeast"
S = "South"
SW = "Southwest"
W = "West"
NW = "Northwest"
HOR = "Horizontal" # For roofs and floors
class ASHRAETables:
"""Class for managing ASHRAE tables for load calculations."""
def __init__(self):
"""Initialize ASHRAE tables."""
# Load tables
self.cltd_wall = self._load_cltd_wall_table()
self.cltd_roof = self._load_cltd_roof_table()
self.scl = self._load_scl_table()
self.clf_lights = self._load_clf_lights_table()
self.clf_people = self._load_clf_people_table()
self.clf_equipment = self._load_clf_equipment_table()
self.heat_gain = self._load_heat_gain_table()
# Load correction factors
self.latitude_correction = self._load_latitude_correction()
self.color_correction = self._load_color_correction()
self.month_correction = self._load_month_correction()
# Load thermal properties and roof classifications
self.thermal_properties = self._load_thermal_properties()
self.roof_classifications = self._load_roof_classifications()
def _validate_cltd_inputs(self, group: str, orientation: str, hour: int, latitude: str, month: str, solar_absorptivity: float, is_wall: bool = True) -> Tuple[bool, str]:
"""Validate inputs for CLTD calculations."""
valid_groups = [e.value for e in WallGroup] if is_wall else [e.value for e in RoofGroup]
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 group not in valid_groups:
return False, f"Invalid {'wall' if is_wall else 'roof'} group: {group}. Valid groups: {valid_groups}"
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 numeric latitude values and ensure comprehensive mapping
if latitude not in valid_latitudes:
# Try to convert numeric latitude to standard format
try:
# First, handle string representations that might contain direction indicators
if isinstance(latitude, str):
# Extract numeric part, removing 'N' or 'S'
lat_str = latitude.upper().strip()
num_part = ''.join(c for c in lat_str if c.isdigit() or c == '.')
lat_val = float(num_part)
# Adjust for southern hemisphere if needed
if 'S' in lat_str:
lat_val = -lat_val
else:
# Handle direct numeric input
lat_val = float(latitude)
# Take absolute value for mapping purposes
abs_lat = abs(lat_val)
# Map to the closest standard latitude
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'
# Use the mapped latitude for validation
latitude = mapped_latitude
except (ValueError, TypeError):
return False, f"Invalid latitude: {latitude}. Valid latitudes: {valid_latitudes}"
if latitude not in valid_latitudes:
return False, f"Invalid latitude: {latitude}. Valid latitudes: {valid_latitudes}"
if month not in valid_months:
return False, f"Invalid month: {month}. Valid months: {valid_months}"
if not 0.0 <= solar_absorptivity <= 1.0:
return False, f"Invalid solar absorptivity: {solar_absorptivity}. Must be between 0.0 and 1.0."
return True, "Valid inputs."
def _load_cltd_wall_table(self) -> Dict[str, pd.DataFrame]:
"""
Load CLTD tables for walls at 24°N (July).
Returns: Dictionary of DataFrames with CLTD values for each wall group.
"""
hours = list(range(24))
# CLTD data for wall types 1-12 mapped to groups A-H
wall_data = {
"A": { # Type 1: Lightest construction
'N': [1, 0, -1, -2, -3, -2, 5, 13, 17, 18, 19, 22, 26, 28, 30, 32, 34, 34, 27, 17, 11, 7, 5, 3],
'NE': [1, 0, -1, -2, -3, 0, 17, 39, 51, 53, 48, 39, 32, 30, 30, 30, 30, 28, 24, 18, 13, 10, 7, 5],
'E': [1, 0, -1, -2, -3, 0, 18, 44, 59, 63, 59, 48, 36, 32, 31, 30, 32, 32, 29, 24, 19, 13, 10, 7],
'SE': [1, 0, -1, -2, -3, -2, 8, 25, 38, 44, 45, 42, 35, 32, 31, 30, 32, 32, 27, 24, 18, 13, 10, 7],
'S': [1, 0, -1, -2, -3, -3, -1, 3, 8, 12, 18, 24, 29, 31, 31, 30, 32, 32, 27, 23, 18, 13, 9, 7],
'SW': [1, 0, 1, 2, 3, 3, 1, 3, 8, 13, 17, 22, 27, 42, 59, 73, 30, 32, 27, 23, 18, 20, 12, 8],
'W': [2, 0, 2, 2, 3, 1, 3, 8, 13, 17, 22, 27, 42, 59, 73, 30, 32, 27, 23, 18, 20, 12, 8, 5],
'NW': [2, 0, 1, 2, 2, 3, 1, 3, 8, 13, 17, 22, 27, 42, 59, 73, 30, 32, 27, 23, 18, 20, 12, 8]
},
"B": { # Type 2
'N': [2, 1, 0, -1, -2, -1, 6, 14, 18, 19, 20, 23, 27, 29, 31, 33, 35, 35, 28, 18, 12, 8, 6, 4],
'NE': [2, 1, 0, -1, -2, 1, 18, 40, 52, 54, 49, 40, 33, 31, 31, 31, 31, 29, 25, 19, 14, 11, 8, 6],
'E': [2, 1, 0, -1, -2, 1, 19, 45, 60, 64, 60, 49, 37, 33, 32, 31, 33, 33, 30, 25, 20, 14, 11, 8],
'SE': [2, 1, 0, -1, -2, -1, 9, 26, 39, 45, 46, 43, 36, 33, 32, 31, 33, 33, 28, 25, 19, 14, 11, 8],
'S': [2, 1, 0, -1, -2, -2, 0, 4, 9, 13, 19, 25, 30, 32, 32, 31, 33, 33, 28, 24, 19, 14, 10, 8],
'SW': [2, 1, 2, 3, 4, 4, 2, 4, 9, 14, 18, 23, 28, 43, 60, 74, 31, 33, 28, 24, 19, 21, 13, 9],
'W': [3, 1, 3, 3, 4, 2, 4, 9, 14, 18, 23, 28, 43, 60, 74, 31, 33, 28, 24, 19, 21, 13, 9, 6],
'NW': [3, 1, 2, 3, 3, 4, 2, 4, 9, 14, 18, 23, 28, 43, 60, 74, 31, 33, 28, 24, 19, 21, 13, 9]
},
"C": { # Type 3
'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]
},
"D": { # Type 4
'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]
},
"E": { # Type 5
'N': [13, 11, 9, 7, 5, 3, 2, 3, 5, 7, 10, 12, 14, 16, 19, 21, 23, 25, 27, 27, 25, 22, 20, 16],
'NE': [13, 11, 8, 7, 5, 3, 3, 6, 12, 20, 26, 31, 33, 33, 32, 32, 32, 33, 31, 29, 27, 24, 21, 18],
'E': [14, 11, 9, 7, 5, 4, 3, 6, 13, 22, 31, 36, 39, 39, 39, 39, 39, 31, 31, 29, 26, 22, 19, 18],
'SE': [13, 10, 8, 6, 5, 3, 2, 4, 8, 14, 20, 25, 28, 30, 30, 30, 30, 30, 28, 26, 24, 21, 18, 16],
'S': [11, 9, 7, 6, 4, 3, 2, 1, 1, 3, 5, 7, 11, 14, 16, 20, 22, 23, 23, 23, 20, 18, 16, 14],
'SW': [18, 15, 12, 9, 7, 5, 3, 3, 3, 4, 5, 8, 11, 14, 16, 20, 26, 32, 33, 31, 41, 40, 36, 31],
'W': [23, 19, 15, 12, 9, 7, 5, 4, 4, 4, 6, 8, 11, 14, 16, 20, 28, 37, 35, 31, 51, 41, 41, 41],
'NW': [21, 17, 14, 11, 8, 6, 4, 3, 3, 4, 6, 8, 11, 14, 16, 20, 28, 37, 35, 31, 41, 41, 41, 41]
},
"F": { # Type 6
'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]
},
"G": { # Type 7
'N': [7, 5, 3, 1, -1, 0, 0, 1, 3, 5, 8, 10, 12, 14, 17, 19, 21, 23, 25, 25, 23, 20, 18, 14],
'NE': [7, 5, 3, 1, -1, 1, 1, 4, 10, 18, 24, 29, 31, 31, 30, 30, 30, 31, 29, 27, 25, 22, 19, 16],
'E': [8, 5, 3, 1, -1, 2, 1, 4, 11, 20, 29, 34, 37, 37, 37, 37, 37, 29, 29, 27, 24, 20, 17, 16],
'SE': [7, 4, 2, 0, -1, 1, 0, 2, 6, 12, 18, 23, 26, 28, 28, 28, 28, 28, 26, 24, 22, 19, 16, 14],
'S': [5, 3, 1, 0, -2, 1, 0, -1, -1, 1, 3, 5, 9, 12, 14, 18, 20, 21, 21, 21, 18, 16, 14, 12],
'SW': [12, 9, 6, 3, 1, 2, 1, 1, 1, 2, 3, 6, 9, 12, 14, 18, 24, 30, 31, 29, 39, 38, 34, 29],
'W': [17, 13, 9, 6, 3, 2, 2, 2, 2, 2, 4, 6, 9, 12, 14, 18, 26, 35, 33, 29, 49, 39, 39, 39],
'NW': [15, 11, 8, 5, 2, 3, 2, 1, 1, 2, 4, 6, 9, 12, 14, 18, 26, 35, 33, 29, 39, 39, 39, 39]
},
"H": { # Interpolated from types 8-12: Heaviest construction
'N': [4, 2, 0, -2, -4, -1, -1, 0, 2, 4, 7, 9, 11, 13, 16, 18, 20, 22, 24, 24, 22, 19, 17, 13],
'NE': [4, 2, 0, -2, -4, 0, 0, 3, 9, 17, 23, 28, 30, 30, 29, 29, 29, 30, 28, 26, 24, 21, 18, 15],
'E': [5, 2, 0, -2, -4, 1, 0, 3, 10, 19, 28, 33, 36, 36, 36, 36, 36, 28, 28, 26, 23, 19, 16, 15],
'SE': [4, 1, -1, -3, -4, 0, -1, 1, 5, 11, 17, 22, 25, 27, 27, 27, 27, 27, 25, 23, 21, 18, 15, 13],
'S': [2, 0, -2, -3, -5, 0, -1, -2, -2, 0, 2, 4, 8, 11, 13, 17, 19, 20, 20, 20, 17, 15, 13, 11],
'SW': [9, 6, 3, 0, -2, 1, 0, 0, 0, 1, 2, 5, 8, 11, 13, 17, 23, 29, 30, 28, 38, 37, 33, 28],
'W': [14, 10, 6, 3, 0, 1, 1, 1, 1, 1, 3, 5, 8, 11, 13, 17, 25, 34, 32, 28, 48, 38, 38, 38],
'NW': [12, 8, 5, 2, -1, 2, 1, 0, 0, 1, 3, 5, 8, 11, 13, 17, 25, 34, 32, 28, 38, 38, 38, 38]
}
}
wall_groups = {group: pd.DataFrame(data, index=hours) for group, data in wall_data.items()}
return wall_groups
def _load_cltd_roof_table(self) -> Dict[str, pd.DataFrame]:
"""
Load CLTD tables for roofs at 24°N, 36°N, 48°N (July).
Returns: Dictionary of DataFrames with CLTD values for each roof group and latitude.
"""
hours = list(range(24))
# CLTD data for roof types mapped to groups A-G across latitudes
roof_data = {
"24N": {
"A": [0, 4, 5, 6, 6, 3, 9, 16, 44, 62, 76, 87, 92, 92, 86, 74, 58, 39, 23, 14, 8, 4, 2, 0], # Type 1
"B": [12, 8, 5, 2, 0, -2, -2, 3, 11, 22, 35, 47, 59, 68, 74, 77, 74, 68, 58, 47, 37, 29, 22, 16], # Type 3
"C": [21, 16, 12, 8, 5, 3, 1, 1, 1, 10, 19, 20, 22, 23, 49, 49, 54, 58, 58, 56, 52, 47, 42, 37], # Type 5
"D": [31, 25, 20, 16, 12, 9, 6, 4, 3, 5, 10, 17, 26, 36, 46, 54, 61, 65, 66, 63, 58, 51, 44, 47], # Type 9
"E": [34, 31, 28, 25, 22, 20, 17, 16, 15, 19, 23, 28, 29, 32, 38, 38, 43, 43, 49, 49, 49, 46, 43, 40], # Type 13
"F": [35, 32, 30, 28, 25, 23, 21, 19, 20, 22, 23, 23, 24, 25, 39, 39, 40, 40, 40, 45, 46, 46, 44, 42], # Type 14
"G": [36, 33, 31, 29, 27, 25, 23, 21, 20, 22, 24, 25, 26, 27, 40, 41, 42, 42, 42, 47, 48, 48, 45, 43] # Interpolated
},
"36N": {
"A": [0, 2, 4, 5, 6, 6, 12, 28, 45, 61, 75, 84, 90, 90, 84, 79, 71, 62, 66, 59, 50, 42, 47, 0], # Type 1
"B": [12, 8, 5, 2, 0, -2, -1, 14, 13, 24, 25, 26, 27, 28, 38, 39, 40, 40, 43, 45, 46, 46, 43, 40], # Type 3
"C": [21, 16, 12, 8, 5, 3, 1, 12, 15, 12, 21, 22, 23, 32, 39, 40, 40, 40, 40, 45, 46, 46, 43, 40], # Type 5
"D": [32, 26, 21, 16, 13, 10, 8, 14, 17, 19, 20, 22, 23, 24, 39, 40, 40, 40, 40, 45, 46, 46, 43, 40], # Type 9
"E": [34, 31, 28, 25, 23, 20, 18, 16, 16, 20, 22, 22, 23, 24, 39, 39, 40, 40, 40, 45, 46, 46, 44, 42], # Type 13
"F": [35, 32, 30, 28, 25, 23, 21, 19, 20, 22, 23, 23, 24, 25, 39, 39, 40, 40, 40, 45, 46, 46, 44, 42], # Type 14
"G": [36, 33, 31, 29, 27, 25, 23, 21, 20, 22, 24, 25, 26, 27, 40, 41, 42, 42, 42, 47, 48, 48, 45, 43] # Interpolated
},
"48N": {
"A": [0, 2, 4, 5, 6, 5, 3, 15, 29, 44, 58, 69, 78, 83, 83, 79, 71, 59, 44, 49, 49, 49, 5, 2], # Type 1
"B": [12, 8, 5, 2, 0, -1, 1, 16, 16, 20, 22, 23, 24, 25, 39, 39, 40, 40, 40, 45, 46, 46, 43, 40], # Type 3
"C": [21, 16, 12, 8, 5, 3, 2, 16, 19, 20, 22, 23, 24, 25, 39, 39, 40, 40, 40, 45, 46, 46, 43, 40], # Type 5
"D": [31, 26, 21, 16, 12, 9, 6, 5, 5, 20, 22, 23, 24, 25, 39, 39, 40, 40, 40, 45, 46, 46, 43, 40], # Type 9
"E": [33, 30, 27, 25, 22, 20, 17, 16, 16, 20, 22, 23, 24, 25, 39, 39, 40, 40, 40, 47, 48, 47, 45, 40], # Type 13
"F": [34, 32, 29, 27, 25, 23, 21, 20, 19, 20, 22, 23, 24, 25, 39, 39, 40, 40, 40, 48, 48, 48, 43, 40], # Type 14
"G": [35, 33, 31, 29, 27, 25, 23, 21, 20, 22, 24, 25, 26, 27, 40, 41, 42, 42, 42, 48, 49, 49, 45, 43] # Interpolated
}
}
roof_groups = {}
for lat, groups in roof_data.items():
for group, data in groups.items():
roof_groups[f"{group}_{lat}"] = pd.DataFrame({"HOR": data}, index=hours)
return roof_groups
def _load_scl_table(self) -> Dict[str, pd.DataFrame]:
"""
Load SCL (Solar Cooling Load) tables for windows.
Returns: Dictionary of DataFrames with SCL values for each latitude/month.
"""
hours = list(range(24))
# Base SCL data for 40°N (July)
scl_40n_jul = {
"N": [11, 8, 6, 6, 6, 9, 13, 16, 19, 21, 22, 23, 23, 22, 20, 17, 14, 11, 11, 11, 11, 11, 11, 11],
"NE": [11, 8, 6, 6, 6, 19, 75, 113, 121, 103, 75, 40, 31, 27, 23, 19, 14, 11, 11, 11, 11, 11, 11, 11],
"E": [11, 8, 6, 6, 6, 13, 55, 159, 232, 251, 222, 157, 82, 43, 32, 24, 17, 11, 11, 11, 11, 11, 11, 11],
"SE": [11, 8, 6, 6, 6, 10, 33, 98, 187, 251, 276, 264, 214, 139, 74, 37, 21, 11, 11, 11, 11, 11, 11, 11],
"S": [11, 8, 6, 6, 6, 8, 14, 27, 66, 139, 209, 254, 268, 251, 203, 139, 66, 27, 14, 11, 11, 11, 11, 11],
"SW": [11, 8, 6, 6, 6, 8, 14, 19, 24, 37, 74, 139, 214, 264, 276, 251, 187, 98, 33, 14, 11, 11, 11, 11],
"W": [11, 8, 6, 6, 6, 8, 14, 19, 24, 32, 43, 82, 157, 222, 251, 232, 159, 55, 13, 11, 11, 11, 11, 11],
"NW": [11, 8, 6, 6, 6, 8, 14, 19, 24, 27, 31, 40, 75, 103, 121, 113, 75, 19, 11, 11, 11, 11, 11, 11],
"HOR": [11, 8, 6, 6, 6, 19, 69, 135, 201, 254, 290, 308, 308, 290, 254, 201, 135, 69, 19, 11, 11, 11, 11, 11]
}
scl_tables = {"40N_Jul": pd.DataFrame(scl_40n_jul, index=hours)}
latitudes = ["24N", "32N", "40N", "48N", "56N"]
months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
for lat in latitudes:
for month in months:
key = f"{lat}_{month}"
if key == "40N_Jul":
continue
lat_factor = (40 - float(lat[:-1])) / 40
month_idx = months.index(month)
month_factor = 1 + (month_idx - 6) / 24
scl_data = {}
for orient in scl_40n_jul:
base_scl = scl_40n_jul[orient]
scl_data[orient] = [max(6, round(v * (1 - lat_factor * 0.2) * month_factor)) for v in base_scl]
scl_tables[key] = pd.DataFrame(scl_data, index=hours)
return scl_tables
def _load_clf_lights_table(self) -> pd.DataFrame:
"""
Load CLF (Cooling Load Factor) table for lights.
Returns: DataFrame with CLF values for lights by zone type and hours.
"""
hours = list(range(24))
clf_lights_data = {
"A_8h": [0.85, 0.92, 0.95, 0.95, 0.97, 0.97, 0.98, 0.13, 0.06, 0.04, 0.03, 0.02, 0.02, 0.02, 0.02, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01],
"A_10h": [0.85, 0.93, 0.95, 0.97, 0.97, 0.98, 0.98, 0.98, 0.98, 0.98, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02],
"A_12h": [0.86, 0.93, 0.96, 0.97, 0.97, 0.98, 0.98, 0.98, 0.98, 0.98, 0.98, 0.98, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02],
"B_8h": [0.75, 0.85, 0.90, 0.93, 0.94, 0.95, 0.95, 0.95, 0.12, 0.08, 0.05, 0.04, 0.04, 0.03, 0.03, 0.03, 0.03, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02],
"B_10h": [0.75, 0.86, 0.91, 0.93, 0.94, 0.95, 0.95, 0.95, 0.96, 0.97, 0.24, 0.13, 0.08, 0.06, 0.05, 0.04, 0.04, 0.03, 0.03, 0.03, 0.03, 0.03, 0.02, 0.02],
"B_12h": [0.76, 0.86, 0.91, 0.93, 0.95, 0.95, 0.95, 0.95, 0.97, 0.97, 0.97, 0.97, 0.03, 0.03, 0.03, 0.03, 0.03, 0.03, 0.03, 0.03, 0.03, 0.03, 0.03, 0.03],
"C_8h": [0.70, 0.80, 0.85, 0.88, 0.90, 0.92, 0.93, 0.94, 0.10, 0.07, 0.04, 0.03, 0.03, 0.02, 0.02, 0.02, 0.02, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01],
"C_10h": [0.70, 0.81, 0.86, 0.89, 0.91, 0.92, 0.93, 0.94, 0.95, 0.96, 0.20, 0.11, 0.07, 0.05, 0.04, 0.03, 0.03, 0.02, 0.02, 0.02, 0.02, 0.02, 0.01, 0.01],
"C_12h": [0.71, 0.82, 0.87, 0.90, 0.92, 0.93, 0.94, 0.95, 0.96, 0.96, 0.96, 0.96, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02],
"D_8h": [0.65, 0.75, 0.80, 0.83, 0.85, 0.87, 0.88, 0.89, 0.08, 0.06, 0.03, 0.02, 0.02, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01],
"D_10h": [0.65, 0.76, 0.81, 0.84, 0.86, 0.88, 0.89, 0.90, 0.91, 0.92, 0.16, 0.09, 0.06, 0.04, 0.03, 0.02, 0.02, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01],
"D_12h": [0.66, 0.77, 0.82, 0.85, 0.87, 0.89, 0.90, 0.91, 0.92, 0.92, 0.92, 0.92, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01]
}
return pd.DataFrame(clf_lights_data, index=hours)
def _load_clf_people_table(self) -> pd.DataFrame:
"""
Load CLF (Cooling Load Factor) table for people.
Returns: DataFrame with CLF values for people by zone type and hours.
"""
hours = list(range(24))
clf_people_data = {
"A_2h": [0.75, 0.88, 0.18, 0.08, 0.04, 0.02, 0.01, 0.01, 0.01, 0.01, 0.01, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00],
"A_4h": [0.75, 0.88, 0.93, 0.95, 0.97, 0.10, 0.05, 0.03, 0.02, 0.02, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00],
"A_6h": [0.75, 0.88, 0.93, 0.95, 0.97, 0.97, 0.33, 0.11, 0.06, 0.04, 0.03, 0.02, 0.02, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.00, 0.00, 0.00, 0.00, 0.00],
"B_2h": [0.65, 0.75, 0.81, 0.85, 0.89, 0.91, 0.93, 0.95, 0.96, 0.97, 0.98, 0.98, 0.99, 0.99, 0.99, 0.99, 0.99, 0.99, 0.99, 0.02, 0.02, 0.02, 0.02, 0.02],
"B_4h": [0.65, 0.75, 0.82, 0.87, 0.90, 0.92, 0.94, 0.95, 0.96, 0.97, 0.98, 0.98, 0.99, 0.99, 0.99, 0.99, 0.99, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02],
"B_6h": [0.65, 0.75, 0.82, 0.87, 0.90, 0.92, 0.94, 0.95, 0.96, 0.97, 0.98, 0.98, 0.99, 0.99, 0.99, 0.99, 0.99, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02],
"C_2h": [0.60, 0.70, 0.76, 0.80, 0.84, 0.86, 0.88, 0.90, 0.91, 0.92, 0.93, 0.93, 0.94, 0.94, 0.94, 0.94, 0.94, 0.94, 0.94, 0.01, 0.01, 0.01, 0.01, 0.01],
"C_4h": [0.60, 0.70, 0.77, 0.82, 0.85, 0.87, 0.89, 0.90, 0.91, 0.92, 0.93, 0.93, 0.94, 0.94, 0.94, 0.94, 0.94, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01],
"C_6h": [0.60, 0.70, 0.77, 0.82, 0.85, 0.87, 0.89, 0.90, 0.91, 0.92, 0.93, 0.93, 0.94, 0.94, 0.94, 0.94, 0.94, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01],
"D_2h": [0.55, 0.65, 0.71, 0.75, 0.79, 0.81, 0.83, 0.85, 0.86, 0.87, 0.88, 0.88, 0.89, 0.89, 0.89, 0.89, 0.89, 0.89, 0.89, 0.00, 0.00, 0.00, 0.00, 0.00],
"D_4h": [0.55, 0.65, 0.72, 0.77, 0.80, 0.82, 0.84, 0.85, 0.86, 0.87, 0.88, 0.88, 0.89, 0.89, 0.89, 0.89, 0.89, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00],
"D_6h": [0.55, 0.65, 0.72, 0.77, 0.80, 0.82, 0.84, 0.85, 0.86, 0.87, 0.88, 0.88, 0.89, 0.89, 0.89, 0.89, 0.89, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00]
}
return pd.DataFrame(clf_people_data, index=hours)
def _load_clf_equipment_table(self) -> pd.DataFrame:
"""
Load CLF (Cooling Load Factor) table for equipment.
Returns: DataFrame with CLF values for equipment by zone type and hours.
"""
hours = list(range(24))
clf_equipment_data = {
"A_2h": [0.54, 0.83, 0.26, 0.11, 0.05, 0.03, 0.01, 0.01, 0.01, 0.01, 0.01, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00],
"A_4h": [0.64, 0.83, 0.90, 0.93, 0.31, 0.14, 0.07, 0.04, 0.03, 0.03, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00],
"A_6h": [0.64, 0.83, 0.90, 0.93, 0.95, 0.95, 0.33, 0.11, 0.06, 0.04, 0.03, 0.02, 0.02, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.00, 0.00, 0.00, 0.00, 0.00],
"B_2h": [0.50, 0.75, 0.81, 0.85, 0.89, 0.91, 0.93, 0.95, 0.96, 0.97, 0.98, 0.98, 0.99, 0.99, 0.99, 0.99, 0.99, 0.99, 0.99, 0.02, 0.02, 0.02, 0.02, 0.02],
"B_4h": [0.50, 0.75, 0.82, 0.87, 0.90, 0.92, 0.94, 0.95, 0.96, 0.97, 0.98, 0.98, 0.99, 0.99, 0.99, 0.99, 0.99, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02],
"B_6h": [0.50, 0.75, 0.82, 0.87, 0.90, 0.92, 0.94, 0.95, 0.96, 0.97, 0.98, 0.98, 0.99, 0.99, 0.99, 0.99, 0.99, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02],
"C_2h": [0.46, 0.70, 0.76, 0.80, 0.84, 0.86, 0.88, 0.90, 0.91, 0.92, 0.93, 0.93, 0.94, 0.94, 0.94, 0.94, 0.94, 0.94, 0.94, 0.01, 0.01, 0.01, 0.01, 0.01],
"C_4h": [0.46, 0.70, 0.77, 0.82, 0.85, 0.87, 0.89, 0.90, 0.91, 0.92, 0.93, 0.93, 0.94, 0.94, 0.94, 0.94, 0.94, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01],
"C_6h": [0.46, 0.70, 0.77, 0.82, 0.85, 0.87, 0.89, 0.90, 0.91, 0.92, 0.93, 0.93, 0.94, 0.94, 0.94, 0.94, 0.94, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01],
"D_2h": [0.42, 0.65, 0.71, 0.75, 0.79, 0.81, 0.83, 0.85, 0.86, 0.87, 0.88, 0.88, 0.89, 0.89, 0.89, 0.89, 0.89, 0.89, 0.89, 0.00, 0.00, 0.00, 0.00, 0.00],
"D_4h": [0.42, 0.65, 0.72, 0.77, 0.80, 0.82, 0.84, 0.85, 0.86, 0.87, 0.88, 0.88, 0.89, 0.89, 0.89, 0.89, 0.89, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00],
"D_6h": [0.42, 0.65, 0.72, 0.77, 0.80, 0.82, 0.84, 0.85, 0.86, 0.87, 0.88, 0.88, 0.89, 0.89, 0.89, 0.89, 0.89, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00]
}
return pd.DataFrame(clf_equipment_data, index=hours)
def _load_heat_gain_table(self) -> pd.DataFrame:
"""
Load heat gain table for internal sources.
Returns: DataFrame with heat gain values (Btu/h or Btu/h-ft²).
"""
data = {
"source": ["people_sensible", "people_latent", "lights", "equipment"],
"gain": [250, 200, 3.4, 500]
}
return pd.DataFrame(data)
def _load_thermal_properties(self) -> pd.DataFrame:
"""
Load thermal properties for building materials.
Returns: DataFrame with U-values, R-values, and density.
"""
data = {
"material": [
"Brick_4in", "Brick_8in", "Concrete_6in", "Concrete_12in",
"Wood_1in", "Wood_2in", "Insulation_1in", "Insulation_2in",
"Gypsum_0.5in", "Steel_1in"
],
"U_value": [0.45, 0.32, 0.51, 0.48, 0.12, 0.08, 0.03, 0.015, 0.32, 0.65], # Btu/h-ft²-°F
"R_value": [2.22, 3.13, 1.96, 2.08, 8.33, 12.5, 33.33, 66.67, 3.13, 1.54], # ft²-°F-h/Btu
"density": [120, 120, 140, 140, 35, 35, 1.5, 1.5, 40, 490] # lb/ft³
}
return pd.DataFrame(data)
def _load_roof_classifications(self) -> pd.DataFrame:
"""
Load roof classification data.
Returns: DataFrame with roof type descriptions and properties.
"""
data = {
"type": [1, 2, 3, 4, 5, 8, 9, 10, 13, 14],
"description": [
"Light roof, no insulation", "Light roof, minimal insulation",
"Medium roof, R-10 insulation", "Medium roof, R-15 insulation",
"Heavy roof, R-20 insulation", "Heavy roof, R-25 insulation",
"Concrete slab, R-15 insulation", "Concrete slab, R-20 insulation",
"Metal deck, R-30 insulation", "Metal deck, R-35 insulation"
],
"U_value": [0.5, 0.4, 0.3, 0.25, 0.2, 0.15, 0.18, 0.14, 0.1, 0.08],
"mass": [10, 15, 50, 60, 100, 120, 150, 160, 80, 90] # lb/ft²
}
return pd.DataFrame(data)
def _load_latitude_correction(self) -> Dict[str, Dict[str, float]]:
"""
Load latitude correction factors for CLTD/SCL values.
Returns: Dictionary of correction factors for different latitudes and months.
"""
return {
"24N": {"Jan": -5.0, "Feb": -3.5, "Mar": -1.0, "Apr": 2.0, "May": 4.0, "Jun": 5.0, "Jul": 4.5, "Aug": 3.0, "Sep": 1.0, "Oct": -1.5, "Nov": -4.0, "Dec": -5.5},
"32N": {"Jan": -4.0, "Feb": -2.5, "Mar": 0.0, "Apr": 2.5, "May": 4.5, "Jun": 5.5, "Jul": 5.0, "Aug": 3.5, "Sep": 1.5, "Oct": -0.5, "Nov": -3.0, "Dec": -4.5},
"40N": {"Jan": -3.0, "Feb": -1.5, "Mar": 1.0, "Apr": 3.0, "May": 5.0, "Jun": 6.0, "Jul": 5.5, "Aug": 4.0, "Sep": 2.0, "Oct": 0.0, "Nov": -2.0, "Dec": -3.5},
"48N": {"Jan": -2.0, "Feb": -0.5, "Mar": 2.0, "Apr": 4.0, "May": 6.0, "Jun": 7.0, "Jul": 6.5, "Aug": 5.0, "Sep": 3.0, "Oct": 1.0, "Nov": -1.0, "Dec": -2.5},
"56N": {"Jan": -1.0, "Feb": 0.5, "Mar": 3.0, "Apr": 5.0, "May": 7.0, "Jun": 8.0, "Jul": 7.5, "Aug": 6.0, "Sep": 4.0, "Oct": 2.0, "Nov": 0.0, "Dec": -1.5}
}
def _load_color_correction(self) -> Dict[str, float]:
"""
Load solar absorptivity correction factors based on ASHRAE Handbook—Fundamentals (2017).
Returns: Dictionary mapping solar absorptivity values to correction factors.
"""
return {
0.3: 0.85, # Light surfaces
0.45: 0.925, # Light to Medium surfaces
0.6: 1.00, # Medium surfaces
0.75: 1.075, # Medium to Dark surfaces
0.9: 1.15 # Dark surfaces
}
def _load_month_correction(self) -> Dict[str, float]:
"""
Load month correction factors for CLTD values.
Returns: Dictionary of correction factors for different months.
"""
return {
"Jan": -6.0, "Feb": -5.0, "Mar": -3.0, "Apr": -1.0, "May": 1.0,
"Jun": 2.0, "Jul": 2.0, "Aug": 2.0, "Sep": 1.0, "Oct": -1.0,
"Nov": -3.0, "Dec": -5.0
}
def _apply_climatic_corrections(self, cltd: float, latitude: str, month: str, color: str, outdoor_temp: float, indoor_temp: float) -> float:
"""
Apply climatic corrections to CLTD values based on latitude, month, color, and temperature.
Args:
cltd (float): Base CLTD value.
latitude (str): Latitude (e.g., '32N').
month (str): Month (e.g., 'Jul').
color (str): Surface color ('Dark', 'Medium', 'Light').
outdoor_temp (float): Outdoor design temperature (°C).
indoor_temp (float): Indoor design temperature (°C).
Returns:
float: Corrected CLTD value (°C).
"""
try:
# Convert temperatures to °F for ASHRAE corrections
outdoor_temp_f = outdoor_temp * 9/5 + 32
indoor_temp_f = indoor_temp * 9/5 + 32
# Get correction factors
lat_corr = self.latitude_correction.get(latitude, {}).get(month, 0.0)
month_corr = self.month_correction.get(month, 0.0)
color_corr = self.color_correction.get(color, 0.0)
# Apply temperature difference correction (ASHRAE CLTD correction formula)
temp_diff = outdoor_temp_f - indoor_temp_f
design_temp_diff = 85 - 78 # ASHRAE base conditions: 85°F outdoor, 78°F indoor
temp_corr = (temp_diff - design_temp_diff) * 0.5556 # Convert °F to °C
# Total correction
corrected_cltd = cltd + lat_corr + month_corr + color_corr + temp_corr
# Ensure non-negative CLTD
return max(0.0, corrected_cltd)
except Exception as e:
raise ValueError(f"Error applying climatic corrections: {str(e)}")
def get_cltd_wall(self, wall_group: str, orientation: str, hour: int) -> float:
"""Get CLTD value for a wall."""
if wall_group not in self.cltd_wall:
raise ValueError(f"Invalid wall group: {wall_group}")
orientation_map = {e.value: e.name for e in Orientation}
orientation_abbr = orientation_map.get(orientation, orientation)
if orientation_abbr not in self.cltd_wall[wall_group].columns:
raise ValueError(f"Invalid orientation: {orientation}")
if hour not in self.cltd_wall[wall_group].index:
raise ValueError(f"Invalid hour: {hour}")
return float(self.cltd_wall[wall_group].loc[hour, orientation_abbr])
def get_cltd_roof(self, roof_group: str, latitude: str, hour: int) -> float:
"""Get CLTD value for a roof."""
# Map latitude to standard format before forming the key
valid_latitudes = ['24N', '36N', '48N']
# Handle numeric or non-standard latitude values
if latitude not in valid_latitudes:
# Try to convert to standard format
try:
# First, handle string representations that might contain direction indicators
if isinstance(latitude, str):
# Extract numeric part, removing 'N' or 'S'
lat_str = latitude.upper().strip()
num_part = ''.join(c for c in lat_str if c.isdigit() or c == '.')
lat_val = float(num_part)
# Adjust for southern hemisphere if needed
if 'S' in lat_str:
lat_val = -lat_val
else:
# Handle direct numeric input
lat_val = float(latitude)
# Take absolute value for mapping purposes
abs_lat = abs(lat_val)
# Map to the closest standard latitude for roof data
if abs_lat < 30:
latitude = '24N'
elif abs_lat < 42:
latitude = '36N'
else:
latitude = '48N'
except (ValueError, TypeError):
raise ValueError(f"Invalid latitude format: {latitude}")
key = f"{roof_group}_{latitude}"
if key not in self.cltd_roof:
raise ValueError(f"Invalid roof group or latitude: {key}")
if hour not in self.cltd_roof[key].index:
raise ValueError(f"Invalid hour: {hour}")
return float(self.cltd_roof[key].loc[hour, "HOR"])
def get_scl(self, latitude: str, month: str, orientation: str, hour: int) -> float:
"""Get SCL value for a window."""
# Map latitude to standard format before forming the key
valid_latitudes = ['24N', '32N', '40N', '48N', '56N']
# Handle numeric or non-standard latitude values
if latitude not in valid_latitudes:
# Try to convert to standard format
try:
# First, handle string representations that might contain direction indicators
if isinstance(latitude, str):
# Extract numeric part, removing 'N' or 'S'
lat_str = latitude.upper().strip()
num_part = ''.join(c for c in lat_str if c.isdigit() or c == '.')
lat_val = float(num_part)
# Adjust for southern hemisphere if needed
if 'S' in lat_str:
lat_val = -lat_val
else:
# Handle direct numeric input
lat_val = float(latitude)
# Take absolute value for mapping purposes
abs_lat = abs(lat_val)
# Map to the closest standard latitude for SCL data
if abs_lat < 28:
latitude = '24N'
elif abs_lat < 36:
latitude = '32N'
elif abs_lat < 44:
latitude = '40N'
elif abs_lat < 52:
latitude = '48N'
else:
latitude = '56N'
except (ValueError, TypeError):
raise ValueError(f"Invalid latitude format: {latitude}")
key = f"{latitude}_{month}"
if key not in self.scl:
raise ValueError(f"Invalid latitude or month: {key}")
orientation_map = {e.value: e.name for e in Orientation}
orientation_abbr = orientation_map.get(orientation, orientation)
if orientation_abbr not in self.scl[key].columns:
raise ValueError(f"Invalid orientation: {orientation}")
if hour not in self.scl[key].index:
raise ValueError(f"Invalid hour: {hour}")
return float(self.scl[key].loc[hour, orientation_abbr])
def get_clf_lights(self, zone_type: str, hours_on: str, hour: int) -> float:
"""Get CLF value for lights."""
key = f"{zone_type}_{hours_on}"
if key not in self.clf_lights.columns:
raise ValueError(f"Invalid zone type or hours: {key}")
if hour not in self.clf_lights.index:
raise ValueError(f"Invalid hour: {hour}")
return float(self.clf_lights.loc[hour, key])
def get_clf_people(self, zone_type: str, hours_occupied: str, hour: int) -> float:
"""Get CLF value for people."""
key = f"{zone_type}_{hours_occupied}"
if key not in self.clf_people.columns:
raise ValueError(f"Invalid zone type or hours: {key}")
if hour not in self.clf_people.index:
raise ValueError(f"Invalid hour: {hour}")
return float(self.clf_people.loc[hour, key])
def get_clf_equipment(self, zone_type: str, hours_operated: str, hour: int) -> float:
"""Get CLF value for equipment."""
key = f"{zone_type}_{hours_operated}"
if key not in self.clf_equipment.columns:
raise ValueError(f"Invalid zone type or hours: {key}")
if hour not in self.clf_equipment.index:
raise ValueError(f"Invalid hour: {hour}")
return float(self.clf_equipment.loc[hour, key])
def get_thermal_property(self, material: str, property_type: str) -> float:
"""
Get thermal property for a material.
Args:
material (str): Material name (e.g., 'Brick_4in').
property_type (str): Property to retrieve ('U_value', 'R_value', 'density').
Returns:
float: Value of the specified thermal property.
Raises:
ValueError: If material or property_type is invalid.
"""
if material not in self.thermal_properties['material'].values:
raise ValueError(f"Invalid material: {material}")
if property_type not in ['U_value', 'R_value', 'density']:
raise ValueError(f"Invalid property type: {property_type}")
return float(self.thermal_properties.loc[self.thermal_properties['material'] == material, property_type].iloc[0])
def get_heat_gain(self, source: str) -> float:
"""
Get heat gain value for an internal source.
Args:
source (str): Source type ('people_sensible', 'people_latent', 'lights', 'equipment').
Returns:
float: Heat gain value (Btu/h or Btu/h-ft²).
Raises:
ValueError: If source is invalid.
"""
if source not in self.heat_gain['source'].values:
raise ValueError(f"Invalid source: {source}")
return float(self.heat_gain.loc[self.heat_gain['source'] == source, 'gain'].iloc[0])
def plot_cooling_load(self, cooling_loads: List[float], title: str = "Cooling Load Profile", filename: str = "cooling_load.png") -> None:
"""
Plot the cooling load profile over 24 hours.
Args:
cooling_loads (List[float]): List of cooling load values for each hour.
title (str): Plot title.
filename (str): Output filename for the plot.
"""
if len(cooling_loads) != 24:
raise ValueError("Cooling loads must contain 24 hourly values")
plt.figure(figsize=(10, 6))
hours = list(range(24))
plt.plot(hours, cooling_loads, marker='o', linestyle='-', color='b')
plt.title(title)
plt.xlabel("Hour of Day")
plt.ylabel("Cooling Load (Btu/h)")
plt.grid(True)
plt.xticks(hours)
plt.savefig(filename)
plt.close()
def calculate_corrected_cltd_wall(self, wall_group: str, orientation: str, hour: int, latitude: str, month: str, color: str, outdoor_temp: float, indoor_temp: float) -> float:
"""
Calculate corrected CLTD for a wall with climatic corrections.
Args:
wall_group (str): Wall group (e.g., 'A', 'B', ..., 'H').
orientation (str): Wall orientation (e.g., 'North', 'East', etc.).
hour (int): Hour of the day (0-23).
latitude (str): Latitude (e.g., '32N').
month (str): Month (e.g., 'Jul').
color (str): Surface color ('Dark', 'Medium', 'Light').
outdoor_temp (float): Outdoor design temperature (°C).
indoor_temp (float): Indoor design temperature (°C).
Returns:
float: Corrected CLTD value (°C).
Raises:
ValueError: If inputs are invalid or correction fails.
"""
valid, message = self._validate_cltd_inputs(wall_group, orientation, hour, latitude, month, color, is_wall=True)
if not valid:
raise ValueError(message)
try:
# Get base CLTD
base_cltd = self.get_cltd_wall(wall_group, orientation, hour)
# Apply climatic corrections
corrected_cltd = self._apply_climatic_corrections(base_cltd, latitude, month, color, outdoor_temp, indoor_temp)
return corrected_cltd
except Exception as e:
raise ValueError(f"Error calculating corrected CLTD for wall: {str(e)}")
def calculate_corrected_cltd_roof(self, roof_group: str, latitude: str, hour: int, month: str, color: str, outdoor_temp: float, indoor_temp: float) -> float:
"""
Calculate corrected CLTD for a roof with climatic corrections.
Args:
roof_group (str): Roof group (e.g., 'A', 'B', ..., 'G').
latitude (str): Latitude (e.g., '24N', '36N', '48N').
hour (int): Hour of the day (0-23).
month (str): Month (e.g., 'Jul').
color (str): Surface color ('Dark', 'Medium', 'Light').
outdoor_temp (float): Outdoor design temperature (°C).
indoor_temp (float): Indoor design temperature (°C).
Returns:
float: Corrected CLTD value (°C).
Raises:
ValueError: If inputs are invalid or correction fails.
"""
valid, message = self._validate_cltd_inputs(roof_group, 'Horizontal', hour, latitude, month, color, is_wall=False)
if not valid:
raise ValueError(message)
try:
# Get base CLTD
base_cltd = self.get_cltd_roof(roof_group, latitude, hour)
# Apply climatic corrections
corrected_cltd = self._apply_climatic_corrections(base_cltd, latitude, month, color, outdoor_temp, indoor_temp)
return corrected_cltd
except Exception as e:
raise ValueError(f"Error calculating corrected CLTD for roof: {str(e)}")