mabuseif commited on
Commit
69a8f51
·
verified ·
1 Parent(s): 827ae4b

Create Drapery.py

Browse files
Files changed (1) hide show
  1. Drapery.py +941 -0
Drapery.py ADDED
@@ -0,0 +1,941 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Enhanced Drapery module for HVAC Load Calculator with comprehensive CLTD implementation and SCL integration.
3
+ This module provides classes and functions for handling drapery properties
4
+ and calculating their effects on window heat transfer using detailed ASHRAE CLTD/SCL methods.
5
+
6
+ Includes comprehensive CLTD tables for windows (SingleClear, DoubleTinted, LowE, Reflective)
7
+ at multiple latitudes (24°N, 40°N, 48°N) and all orientations, as well as detailed
8
+ climatic corrections and door CLTD calculations.
9
+
10
+ Enhanced to map UI shading coefficients to drapery properties (openness, color, fullness)
11
+ and apply conduction reduction (5-15%) based on openness per ASHRAE guidelines.
12
+ """
13
+
14
+ from typing import Dict, Any, Optional, Tuple, List, Union
15
+ from enum import Enum
16
+ import math
17
+ import pandas as pd
18
+ from data.ashrae_tables import ASHRAETables
19
+
20
+
21
+ class DraperyOpenness(Enum):
22
+ """Enum for drapery openness classification."""
23
+ OPEN = "Open (>25%)"
24
+ SEMI_OPEN = "Semi-open (7-25%)"
25
+ CLOSED = "Closed (0-7%)"
26
+
27
+
28
+ class DraperyColor(Enum):
29
+ """Enum for drapery color/reflectance classification."""
30
+ DARK = "Dark (0-25%)"
31
+ MEDIUM = "Medium (25-50%)"
32
+ LIGHT = "Light (>50%)"
33
+
34
+
35
+ class GlazingType(Enum):
36
+ """Enum for glazing types."""
37
+ SINGLE_CLEAR = "Single Clear"
38
+ SINGLE_TINTED = "Single Tinted"
39
+ DOUBLE_CLEAR = "Double Clear"
40
+ DOUBLE_TINTED = "Double Tinted"
41
+ LOW_E = "Low-E"
42
+ REFLECTIVE = "Reflective"
43
+
44
+
45
+ class FrameType(Enum):
46
+ """Enum for window frame types."""
47
+ ALUMINUM = "Aluminum without Thermal Break"
48
+ ALUMINUM_THERMAL_BREAK = "Aluminum with Thermal Break"
49
+ VINYL = "Vinyl/Fiberglass"
50
+ WOOD = "Wood/Vinyl-Clad Wood"
51
+ INSULATED = "Insulated"
52
+
53
+
54
+ class SurfaceColor(Enum):
55
+ """Enum for surface color classification."""
56
+ DARK = "Dark"
57
+ MEDIUM = "Medium"
58
+ LIGHT = "Light"
59
+
60
+
61
+ class Latitude(Enum):
62
+ """Enum for latitude ranges."""
63
+ LAT_24N = "24N"
64
+ LAT_40N = "40N"
65
+ LAT_48N = "48N"
66
+
67
+
68
+ # U-Factors for various fenestration products (Table 9-1) in SI units (W/m²K)
69
+ # Format: {(glazing_type, frame_type): u_factor}
70
+ WINDOW_U_FACTORS = {
71
+ # Single Clear Glass
72
+ (GlazingType.SINGLE_CLEAR, FrameType.ALUMINUM): 7.22,
73
+ (GlazingType.SINGLE_CLEAR, FrameType.ALUMINUM_THERMAL_BREAK): 6.14,
74
+ (GlazingType.SINGLE_CLEAR, FrameType.VINYL): 5.11,
75
+ (GlazingType.SINGLE_CLEAR, FrameType.WOOD): 5.06,
76
+ (GlazingType.SINGLE_CLEAR, FrameType.INSULATED): 4.60,
77
+
78
+ # Single Tinted Glass
79
+ (GlazingType.SINGLE_TINTED, FrameType.ALUMINUM): 7.22,
80
+ (GlazingType.SINGLE_TINTED, FrameType.ALUMINUM_THERMAL_BREAK): 6.14,
81
+ (GlazingType.SINGLE_TINTED, FrameType.VINYL): 5.11,
82
+ (GlazingType.SINGLE_TINTED, FrameType.WOOD): 5.06,
83
+ (GlazingType.SINGLE_TINTED, FrameType.INSULATED): 4.60,
84
+
85
+ # Double Clear Glass
86
+ (GlazingType.DOUBLE_CLEAR, FrameType.ALUMINUM): 4.60,
87
+ (GlazingType.DOUBLE_CLEAR, FrameType.ALUMINUM_THERMAL_BREAK): 3.41,
88
+ (GlazingType.DOUBLE_CLEAR, FrameType.VINYL): 3.01,
89
+ (GlazingType.DOUBLE_CLEAR, FrameType.WOOD): 2.90,
90
+ (GlazingType.DOUBLE_CLEAR, FrameType.INSULATED): 2.50,
91
+
92
+ # Double Tinted Glass
93
+ (GlazingType.DOUBLE_TINTED, FrameType.ALUMINUM): 4.60,
94
+ (GlazingType.DOUBLE_TINTED, FrameType.ALUMINUM_THERMAL_BREAK): 3.41,
95
+ (GlazingType.DOUBLE_TINTED, FrameType.VINYL): 3.01,
96
+ (GlazingType.DOUBLE_TINTED, FrameType.WOOD): 2.90,
97
+ (GlazingType.DOUBLE_TINTED, FrameType.INSULATED): 2.50,
98
+
99
+ # Low-E Glass
100
+ (GlazingType.LOW_E, FrameType.ALUMINUM): 3.41,
101
+ (GlazingType.LOW_E, FrameType.ALUMINUM_THERMAL_BREAK): 2.67,
102
+ (GlazingType.LOW_E, FrameType.VINYL): 2.33,
103
+ (GlazingType.LOW_E, FrameType.WOOD): 2.22,
104
+ (GlazingType.LOW_E, FrameType.INSULATED): 1.87,
105
+
106
+ # Reflective Glass
107
+ (GlazingType.REFLECTIVE, FrameType.ALUMINUM): 3.41,
108
+ (GlazingType.REFLECTIVE, FrameType.ALUMINUM_THERMAL_BREAK): 2.67,
109
+ (GlazingType.REFLECTIVE, FrameType.VINYL): 2.33,
110
+ (GlazingType.REFLECTIVE, FrameType.WOOD): 2.22,
111
+ (GlazingType.REFLECTIVE, FrameType.INSULATED): 1.87,
112
+ }
113
+
114
+ # SHGC values for various glazing types (Table 9-3)
115
+ # Format: {(glazing_type, frame_type): shgc}
116
+ WINDOW_SHGC = {
117
+ # Single Clear Glass
118
+ (GlazingType.SINGLE_CLEAR, FrameType.ALUMINUM): 0.78,
119
+ (GlazingType.SINGLE_CLEAR, FrameType.ALUMINUM_THERMAL_BREAK): 0.75,
120
+ (GlazingType.SINGLE_CLEAR, FrameType.VINYL): 0.67,
121
+ (GlazingType.SINGLE_CLEAR, FrameType.WOOD): 0.65,
122
+ (GlazingType.SINGLE_CLEAR, FrameType.INSULATED): 0.63,
123
+
124
+ # Single Tinted Glass
125
+ (GlazingType.SINGLE_TINTED, FrameType.ALUMINUM): 0.65,
126
+ (GlazingType.SINGLE_TINTED, FrameType.ALUMINUM_THERMAL_BREAK): 0.62,
127
+ (GlazingType.SINGLE_TINTED, FrameType.VINYL): 0.55,
128
+ (GlazingType.SINGLE_TINTED, FrameType.WOOD): 0.53,
129
+ (GlazingType.SINGLE_TINTED, FrameType.INSULATED): 0.52,
130
+
131
+ # Double Clear Glass
132
+ (GlazingType.DOUBLE_CLEAR, FrameType.ALUMINUM): 0.65,
133
+ (GlazingType.DOUBLE_CLEAR, FrameType.ALUMINUM_THERMAL_BREAK): 0.61,
134
+ (GlazingType.DOUBLE_CLEAR, FrameType.VINYL): 0.53,
135
+ (GlazingType.DOUBLE_CLEAR, FrameType.WOOD): 0.51,
136
+ (GlazingType.DOUBLE_CLEAR, FrameType.INSULATED): 0.49,
137
+
138
+ # Double Tinted Glass
139
+ (GlazingType.DOUBLE_TINTED, FrameType.ALUMINUM): 0.53,
140
+ (GlazingType.DOUBLE_TINTED, FrameType.ALUMINUM_THERMAL_BREAK): 0.50,
141
+ (GlazingType.DOUBLE_TINTED, FrameType.VINYL): 0.42,
142
+ (GlazingType.DOUBLE_TINTED, FrameType.WOOD): 0.40,
143
+ (GlazingType.DOUBLE_TINTED, FrameType.INSULATED): 0.38,
144
+
145
+ # Low-E Glass
146
+ (GlazingType.LOW_E, FrameType.ALUMINUM): 0.46,
147
+ (GlazingType.LOW_E, FrameType.ALUMINUM_THERMAL_BREAK): 0.44,
148
+ (GlazingType.LOW_E, FrameType.VINYL): 0.38,
149
+ (GlazingType.LOW_E, FrameType.WOOD): 0.36,
150
+ (GlazingType.LOW_E, FrameType.INSULATED): 0.34,
151
+
152
+ # Reflective Glass
153
+ (GlazingType.REFLECTIVE, FrameType.ALUMINUM): 0.33,
154
+ (GlazingType.REFLECTIVE, FrameType.ALUMINUM_THERMAL_BREAK): 0.31,
155
+ (GlazingType.REFLECTIVE, FrameType.VINYL): 0.27,
156
+ (GlazingType.REFLECTIVE, FrameType.WOOD): 0.25,
157
+ (GlazingType.REFLECTIVE, FrameType.INSULATED): 0.24,
158
+ }
159
+
160
+ # Door U-Factors in SI units (W/m²K)
161
+ # Format: {door_type: u_factor}
162
+ DOOR_U_FACTORS = {
163
+ "WoodSolid": 3.35, # Approximated from Group D walls
164
+ "MetalInsulated": 2.61, # Approximated from Group F walls
165
+ "GlassDoor": 7.22, # Same as single clear glass with aluminum frame
166
+ "InsulatedMetal": 2.15, # Insulated metal door
167
+ "InsulatedWood": 1.93, # Insulated wood door
168
+ "Custom": 3.00, # Default for custom doors
169
+ }
170
+
171
+ # Skylight U-Factors in SI units (W/m²K)
172
+ # Format: {(glazing_type, frame_type): u_factor}
173
+ SKYLIGHT_U_FACTORS = {
174
+ # Single Clear Glass
175
+ (GlazingType.SINGLE_CLEAR, FrameType.ALUMINUM): 7.79,
176
+ (GlazingType.SINGLE_CLEAR, FrameType.ALUMINUM_THERMAL_BREAK): 6.71,
177
+ (GlazingType.SINGLE_CLEAR, FrameType.VINYL): 5.68,
178
+ (GlazingType.SINGLE_CLEAR, FrameType.WOOD): 5.63,
179
+ (GlazingType.SINGLE_CLEAR, FrameType.INSULATED): 5.17,
180
+
181
+ # Single Tinted Glass
182
+ (GlazingType.SINGLE_TINTED, FrameType.ALUMINUM): 7.79,
183
+ (GlazingType.SINGLE_TINTED, FrameType.ALUMINUM_THERMAL_BREAK): 6.71,
184
+ (GlazingType.SINGLE_TINTED, FrameType.VINYL): 5.68,
185
+ (GlazingType.SINGLE_TINTED, FrameType.WOOD): 5.63,
186
+ (GlazingType.SINGLE_TINTED, FrameType.INSULATED): 5.17,
187
+
188
+ # Double Clear Glass
189
+ (GlazingType.DOUBLE_CLEAR, FrameType.ALUMINUM): 5.17,
190
+ (GlazingType.DOUBLE_CLEAR, FrameType.ALUMINUM_THERMAL_BREAK): 3.98,
191
+ (GlazingType.DOUBLE_CLEAR, FrameType.VINYL): 3.58,
192
+ (GlazingType.DOUBLE_CLEAR, FrameType.WOOD): 3.47,
193
+ (GlazingType.DOUBLE_CLEAR, FrameType.INSULATED): 3.07,
194
+
195
+ # Double Tinted Glass
196
+ (GlazingType.DOUBLE_TINTED, FrameType.ALUMINUM): 5.17,
197
+ (GlazingType.DOUBLE_TINTED, FrameType.ALUMINUM_THERMAL_BREAK): 3.98,
198
+ (GlazingType.DOUBLE_TINTED, FrameType.VINYL): 3.58,
199
+ (GlazingType.DOUBLE_TINTED, FrameType.WOOD): 3.47,
200
+ (GlazingType.DOUBLE_TINTED, FrameType.INSULATED): 3.07,
201
+
202
+ # Low-E Glass
203
+ (GlazingType.LOW_E, FrameType.ALUMINUM): 3.98,
204
+ (GlazingType.LOW_E, FrameType.ALUMINUM_THERMAL_BREAK): 3.24,
205
+ (GlazingType.LOW_E, FrameType.VINYL): 2.90,
206
+ (GlazingType.LOW_E, FrameType.WOOD): 2.78,
207
+ (GlazingType.LOW_E, FrameType.INSULATED): 2.44,
208
+
209
+ # Reflective Glass
210
+ (GlazingType.REFLECTIVE, FrameType.ALUMINUM): 3.98,
211
+ (GlazingType.REFLECTIVE, FrameType.ALUMINUM_THERMAL_BREAK): 3.24,
212
+ (GlazingType.REFLECTIVE, FrameType.VINYL): 2.90,
213
+ (GlazingType.REFLECTIVE, FrameType.WOOD): 2.78,
214
+ (GlazingType.REFLECTIVE, FrameType.INSULATED): 2.44,
215
+ }
216
+
217
+ # Skylight SHGC values
218
+ # Format: {(glazing_type, frame_type): shgc}
219
+ SKYLIGHT_SHGC = {
220
+ # Single Clear Glass
221
+ (GlazingType.SINGLE_CLEAR, FrameType.ALUMINUM): 0.83,
222
+ (GlazingType.SINGLE_CLEAR, FrameType.ALUMINUM_THERMAL_BREAK): 0.80,
223
+ (GlazingType.SINGLE_CLEAR, FrameType.VINYL): 0.72,
224
+ (GlazingType.SINGLE_CLEAR, FrameType.WOOD): 0.70,
225
+ (GlazingType.SINGLE_CLEAR, FrameType.INSULATED): 0.68,
226
+
227
+ # Single Tinted Glass
228
+ (GlazingType.SINGLE_TINTED, FrameType.ALUMINUM): 0.70,
229
+ (GlazingType.SINGLE_TINTED, FrameType.ALUMINUM_THERMAL_BREAK): 0.67,
230
+ (GlazingType.SINGLE_TINTED, FrameType.VINYL): 0.60,
231
+ (GlazingType.SINGLE_TINTED, FrameType.WOOD): 0.58,
232
+ (GlazingType.SINGLE_TINTED, FrameType.INSULATED): 0.57,
233
+
234
+ # Double Clear Glass
235
+ (GlazingType.DOUBLE_CLEAR, FrameType.ALUMINUM): 0.70,
236
+ (GlazingType.DOUBLE_CLEAR, FrameType.ALUMINUM_THERMAL_BREAK): 0.66,
237
+ (GlazingType.DOUBLE_CLEAR, FrameType.VINYL): 0.58,
238
+ (GlazingType.DOUBLE_CLEAR, FrameType.WOOD): 0.56,
239
+ (GlazingType.DOUBLE_CLEAR, FrameType.INSULATED): 0.54,
240
+
241
+ # Double Tinted Glass
242
+ (GlazingType.DOUBLE_TINTED, FrameType.ALUMINUM): 0.58,
243
+ (GlazingType.DOUBLE_TINTED, FrameType.ALUMINUM_THERMAL_BREAK): 0.55,
244
+ (GlazingType.DOUBLE_TINTED, FrameType.VINYL): 0.47,
245
+ (GlazingType.DOUBLE_TINTED, FrameType.WOOD): 0.45,
246
+ (GlazingType.DOUBLE_TINTED, FrameType.INSULATED): 0.43,
247
+
248
+ # Low-E Glass
249
+ (GlazingType.LOW_E, FrameType.ALUMINUM): 0.51,
250
+ (GlazingType.LOW_E, FrameType.ALUMINUM_THERMAL_BREAK): 0.49,
251
+ (GlazingType.LOW_E, FrameType.VINYL): 0.43,
252
+ (GlazingType.LOW_E, FrameType.WOOD): 0.41,
253
+ (GlazingType.LOW_E, FrameType.INSULATED): 0.39,
254
+
255
+ # Reflective Glass
256
+ (GlazingType.REFLECTIVE, FrameType.ALUMINUM): 0.38,
257
+ (GlazingType.REFLECTIVE, FrameType.ALUMINUM_THERMAL_BREAK): 0.36,
258
+ (GlazingType.REFLECTIVE, FrameType.VINYL): 0.32,
259
+ (GlazingType.REFLECTIVE, FrameType.WOOD): 0.30,
260
+ (GlazingType.REFLECTIVE, FrameType.INSULATED): 0.29,
261
+ }
262
+
263
+
264
+ class Drapery:
265
+ """Class for handling drapery properties and effects on window heat transfer."""
266
+
267
+ def __init__(self, openness: str = "Semi-Open", color: str = "Medium",
268
+ fullness: float = 1.5, enabled: bool = True, shading_device: str = "Drapes"):
269
+ """
270
+ Initialize drapery properties with UI-compatible inputs.
271
+
272
+ Args:
273
+ openness: Drapery openness category ("Closed", "Semi-Open", "Open")
274
+ color: Drapery color category ("Light", "Medium", "Dark")
275
+ fullness: Fullness factor (1.0 for flat, 1.0-2.0 for pleated)
276
+ enabled: Whether drapery is enabled
277
+ shading_device: Type of shading device ("Venetian Blinds", "Drapes", etc.)
278
+ """
279
+ self.openness = openness
280
+ self.color = color
281
+ self.fullness = fullness
282
+ self.enabled = enabled
283
+ self.shading_device = shading_device
284
+
285
+ def get_openness_category(self) -> str:
286
+ """Get openness category as string."""
287
+ return self.openness
288
+
289
+ def get_color_category(self) -> str:
290
+ """Get color category as string."""
291
+ return self.color
292
+
293
+ def get_shading_coefficient(self, shgc: float = 0.5) -> float:
294
+ """
295
+ Calculate shading coefficient for drapery based on UI inputs.
296
+
297
+ Args:
298
+ shgc: Solar Heat Gain Coefficient of window (default 0.5)
299
+
300
+ Returns:
301
+ Shading coefficient (0.0-1.0)
302
+ """
303
+ if not self.enabled:
304
+ return 1.0
305
+
306
+ # Mapping of UI shading devices to properties
307
+ mapping = {
308
+ ("Venetian Blinds", "Light"): {"openness": "Semi-Open", "color": "Light", "fullness": 1.0, "sc": 0.6},
309
+ ("Venetian Blinds", "Medium"): {"openness": "Semi-Open", "color": "Medium", "fullness": 1.0, "sc": 0.65},
310
+ ("Venetian Blinds", "Dark"): {"openness": "Semi-Open", "color": "Dark", "fullness": 1.0, "sc": 0.7},
311
+ ("Drapes", "Light"): {"openness": "Closed", "color": "Light", "fullness": 1.5, "sc": 0.59},
312
+ ("Drapes", "Medium"): {"openness": "Closed", "color": "Medium", "fullness": 1.5, "sc": 0.74},
313
+ ("Drapes", "Dark"): {"openness": "Closed", "color": "Dark", "fullness": 1.5, "sc": 0.87},
314
+ ("Roller Shades", "Light"): {"openness": "Open", "color": "Light", "fullness": 1.0, "sc": 0.8},
315
+ ("Roller Shades", "Medium"): {"openness": "Open", "color": "Medium", "fullness": 1.0, "sc": 0.88},
316
+ ("Roller Shades", "Dark"): {"openness": "Open", "color": "Dark", "fullness": 1.0, "sc": 0.94},
317
+ }
318
+
319
+ # Get shading coefficient from mapping or default to table-based value
320
+ properties = mapping.get((self.shading_device, self.color), {
321
+ "openness": self.openness,
322
+ "color": self.color,
323
+ "fullness": self.fullness,
324
+ "sc": 0.85
325
+ })
326
+ base_sc = properties["sc"]
327
+
328
+ # Adjust for fullness if different from mapped value
329
+ if self.fullness != properties["fullness"]:
330
+ fullness_factor = 1.0 - 0.05 * (self.fullness - 1.0)
331
+ base_sc *= fullness_factor
332
+
333
+ return base_sc
334
+
335
+ def get_conduction_reduction(self) -> float:
336
+ """
337
+ Get conduction reduction factor based on openness.
338
+
339
+ Returns:
340
+ Reduction factor (0.05-0.15)
341
+ """
342
+ reductions = {
343
+ "Closed": 0.15, # 15% reduction
344
+ "Semi-Open": 0.10, # 10% reduction
345
+ "Open": 0.05 # 5% reduction
346
+ }
347
+ return reductions.get(self.openness, 0.10)
348
+
349
+
350
+ class CLTDCalculator:
351
+ """Class for calculating Cooling Load Temperature Difference (CLTD) values."""
352
+
353
+ def __init__(self, indoor_temp: float = 25.6, outdoor_max_temp: float = 35.0,
354
+ outdoor_daily_range: float = 11.7, latitude: Latitude = Latitude.LAT_40N,
355
+ month: int = 7):
356
+ """
357
+ Initialize CLTD calculator.
358
+
359
+ Args:
360
+ indoor_temp: Indoor design temperature (°C)
361
+ outdoor_max_temp: Outdoor maximum temperature (°C)
362
+ outdoor_daily_range: Daily temperature range (°C)
363
+ latitude: Latitude category (24°N, 40°N, 48°N)
364
+ month: Month (1-12)
365
+ """
366
+ self.indoor_temp = indoor_temp # °C
367
+ self.outdoor_max_temp = outdoor_max_temp # ��C
368
+ self.outdoor_daily_range = outdoor_daily_range # °C
369
+ self.latitude = latitude
370
+ self.month = month
371
+ self.outdoor_avg_temp = outdoor_max_temp - outdoor_daily_range / 2
372
+
373
+ # Initialize ASHRAE tables for SCL data
374
+ self.ashrae_tables = ASHRAETables()
375
+
376
+ # Load CLTD tables
377
+ self.cltd_window_tables = self._load_cltd_window_table()
378
+ self.cltd_door_tables = self._load_cltd_door_table()
379
+ self.cltd_skylight_tables = self._load_cltd_skylight_table()
380
+
381
+ # Load correction factors
382
+ self.latitude_corrections = self._load_latitude_correction()
383
+ self.month_corrections = self._load_month_correction()
384
+
385
+ def _load_cltd_window_table(self) -> Dict[str, Dict[str, pd.DataFrame]]:
386
+ """
387
+ Load CLTD tables for windows at multiple latitudes (July).
388
+
389
+ Returns:
390
+ Dictionary of DataFrames with CLTD values indexed by hour (0-23)
391
+ and columns for orientations (N, NE, E, SE, S, SW, W, NW)
392
+ """
393
+ hours = list(range(24))
394
+
395
+ # Comprehensive window CLTD data for different latitudes, glazing types, and orientations
396
+ window_cltd_data = {
397
+ "24N": {
398
+ "SingleClear": {
399
+ "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],
400
+ "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],
401
+ "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],
402
+ "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],
403
+ "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],
404
+ "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],
405
+ "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],
406
+ "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]
407
+ },
408
+ "DoubleTinted": {
409
+ "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],
410
+ "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],
411
+ "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],
412
+ "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],
413
+ "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],
414
+ "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],
415
+ "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],
416
+ "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]
417
+ },
418
+ "LowE": {
419
+ "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],
420
+ "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],
421
+ "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],
422
+ "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],
423
+ "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],
424
+ "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],
425
+ "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],
426
+ "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]
427
+ },
428
+ "Reflective": {
429
+ "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],
430
+ "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],
431
+ "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],
432
+ "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],
433
+ "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],
434
+ "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],
435
+ "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],
436
+ "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]
437
+ }
438
+ },
439
+ "40N": {
440
+ "SingleClear": {
441
+ "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],
442
+ "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],
443
+ "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],
444
+ "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],
445
+ "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],
446
+ "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],
447
+ "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],
448
+ "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]
449
+ },
450
+ "DoubleTinted": {
451
+ "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],
452
+ "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],
453
+ "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],
454
+ "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],
455
+ "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],
456
+ "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],
457
+ "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],
458
+ "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]
459
+ },
460
+ "LowE": {
461
+ "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],
462
+ "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],
463
+ "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],
464
+ "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],
465
+ "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],
466
+ "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],
467
+ "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],
468
+ "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]
469
+ },
470
+ "Reflective": {
471
+ "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],
472
+ "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],
473
+ "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],
474
+ "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],
475
+ "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],
476
+ "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],
477
+ "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],
478
+ "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]
479
+ }
480
+ },
481
+ "48N": {
482
+ "SingleClear": {
483
+ "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],
484
+ "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],
485
+ "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],
486
+ "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],
487
+ "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],
488
+ "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],
489
+ "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],
490
+ "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]
491
+ },
492
+ "DoubleTinted": {
493
+ "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],
494
+ "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],
495
+ "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],
496
+ "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],
497
+ "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],
498
+ "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],
499
+ "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],
500
+ "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]
501
+ },
502
+ "LowE": {
503
+ "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],
504
+ "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],
505
+ "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],
506
+ "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],
507
+ "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],
508
+ "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],
509
+ "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],
510
+ "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]
511
+ },
512
+ "Reflective": {
513
+ "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],
514
+ "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],
515
+ "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],
516
+ "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],
517
+ "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],
518
+ "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],
519
+ "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],
520
+ "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]
521
+ }
522
+ }
523
+ }
524
+
525
+ # Convert to DataFrames
526
+ window_cltd_tables = {}
527
+ for latitude, glazing_data in window_cltd_data.items():
528
+ window_cltd_tables[latitude] = {}
529
+ for glazing_type, orientation_data in glazing_data.items():
530
+ window_cltd_tables[latitude][glazing_type] = pd.DataFrame(orientation_data, index=hours)
531
+
532
+ return window_cltd_tables
533
+
534
+ def _load_cltd_door_table(self) -> Dict[str, pd.DataFrame]:
535
+ """
536
+ Load CLTD tables for doors.
537
+
538
+ Returns:
539
+ Dictionary of DataFrames with CLTD values indexed by hour (0-23)
540
+ """
541
+ hours = list(range(24))
542
+
543
+ # Door CLTD data approximated from wall groups
544
+ door_cltd_data = {
545
+ "WoodSolid": { # Approximated from Group D walls
546
+ '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],
547
+ '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],
548
+ '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],
549
+ '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],
550
+ '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],
551
+ '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],
552
+ '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],
553
+ '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]
554
+ },
555
+ "MetalInsulated": { # Approximated from Group F walls
556
+ '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],
557
+ '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],
558
+ '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],
559
+ '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],
560
+ '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],
561
+ '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],
562
+ '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],
563
+ '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]
564
+ },
565
+ "GlassDoor": { # Same as single clear glass
566
+ '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],
567
+ '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],
568
+ '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],
569
+ '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],
570
+ '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],
571
+ '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],
572
+ '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],
573
+ '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]
574
+ },
575
+ "InsulatedMetal": { # Enhanced insulated metal door
576
+ '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],
577
+ '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],
578
+ '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],
579
+ '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],
580
+ '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],
581
+ '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],
582
+ '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],
583
+ '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]
584
+ },
585
+ "InsulatedWood": { # Enhanced insulated wood door
586
+ '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],
587
+ '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],
588
+ '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],
589
+ '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],
590
+ '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],
591
+ '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],
592
+ '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],
593
+ '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]
594
+ },
595
+ "Custom": { # Default for custom doors
596
+ '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],
597
+ '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],
598
+ '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],
599
+ '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],
600
+ '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],
601
+ '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],
602
+ '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],
603
+ '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]
604
+ }
605
+ }
606
+
607
+ # Convert to DataFrames
608
+ door_cltd_tables = {}
609
+ for door_type, orientation_data in door_cltd_data.items():
610
+ door_cltd_tables[door_type] = pd.DataFrame(orientation_data, index=hours)
611
+
612
+ return door_cltd_tables
613
+
614
+ def _load_cltd_skylight_table(self) -> Dict[str, pd.DataFrame]:
615
+ """
616
+ Load CLTD tables for skylights (flat, 0° slope).
617
+
618
+ Returns:
619
+ Dictionary of DataFrames with CLTD values indexed by hour (0-23)
620
+ """
621
+ hours = list(range(24))
622
+
623
+ # Skylight CLTD data for 40°N latitude, July
624
+ skylight_cltd_data = {
625
+ "SingleClear": {
626
+ '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]
627
+ },
628
+ "DoubleTinted": {
629
+ '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]
630
+ },
631
+ "LowE": {
632
+ '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]
633
+ },
634
+ "Reflective": {
635
+ '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]
636
+ }
637
+ }
638
+
639
+ # Convert to DataFrames
640
+ skylight_cltd_tables = {}
641
+ for glazing_type, orientation_data in skylight_cltd_data.items():
642
+ skylight_cltd_tables[glazing_type] = pd.DataFrame(orientation_data, index=hours)
643
+
644
+ return skylight_cltd_tables
645
+
646
+ def _load_latitude_correction(self) -> Dict[str, float]:
647
+ """
648
+ Load latitude correction factors for CLTD.
649
+
650
+ Returns:
651
+ Dictionary of correction factors by latitude
652
+ """
653
+ return {
654
+ "24N": 0.95,
655
+ "40N": 1.00,
656
+ "48N": 1.05
657
+ }
658
+
659
+ def _load_month_correction(self) -> Dict[int, float]:
660
+ """
661
+ Load month correction factors for CLTD.
662
+
663
+ Returns:
664
+ Dictionary of correction factors by month
665
+ """
666
+ return {
667
+ 1: 0.85, 2: 0.90, 3: 0.95, 4: 0.98, 5: 1.00,
668
+ 6: 1.02, 7: 1.00, 8: 0.98, 9: 0.95, 10: 0.90,
669
+ 11: 0.85, 12: 0.80
670
+ }
671
+
672
+ def get_cltd_window(self, glazing_type: str, orientation: str, hour: int) -> float:
673
+ """
674
+ Get CLTD for a window with corrections.
675
+
676
+ Args:
677
+ glazing_type: Type of glazing ("SingleClear", "DoubleTinted", etc.)
678
+ orientation: Orientation ("N", "NE", etc.)
679
+ hour: Hour of day (0-23)
680
+
681
+ Returns:
682
+ Corrected CLTD value (°C)
683
+ """
684
+ try:
685
+ base_cltd = self.cltd_window_tables[self.latitude.value][glazing_type][orientation][hour]
686
+ except KeyError:
687
+ base_cltd = 0.0
688
+
689
+ # Apply corrections
690
+ latitude_factor = self.latitude_corrections.get(self.latitude.value, 1.0)
691
+ month_factor = self.month_corrections.get(self.month, 1.0)
692
+ temp_correction = (self.outdoor_avg_temp - 29.4) + (self.indoor_temp - 24.0)
693
+
694
+ corrected_cltd = base_cltd * latitude_factor * month_factor + temp_correction
695
+ return max(0.0, corrected_cltd)
696
+
697
+ def get_cltd_door(self, door_type: str, orientation: str, hour: int) -> float:
698
+ """
699
+ Get CLTD for a door with corrections.
700
+
701
+ Args:
702
+ door_type: Type of door ("WoodSolid", "MetalInsulated", etc.)
703
+ orientation: Orientation ("N", "NE", etc.)
704
+ hour: Hour of day (0-23)
705
+
706
+ Returns:
707
+ Corrected CLTD value (°C)
708
+ """
709
+ try:
710
+ base_cltd = self.cltd_door_tables[door_type][orientation][hour]
711
+ except KeyError:
712
+ base_cltd = 0.0
713
+
714
+ # Apply corrections
715
+ latitude_factor = self.latitude_corrections.get(self.latitude.value, 1.0)
716
+ month_factor = self.month_corrections.get(self.month, 1.0)
717
+ temp_correction = (self.outdoor_avg_temp - 29.4) + (self.indoor_temp - 24.0)
718
+
719
+ corrected_cltd = base_cltd * latitude_factor * month_factor + temp_correction
720
+ return max(0.0, corrected_cltd)
721
+
722
+ def get_cltd_skylight(self, glazing_type: str, hour: int) -> float:
723
+ """
724
+ Get CLTD for a skylight with corrections.
725
+
726
+ Args:
727
+ glazing_type: Type of glazing ("SingleClear", "DoubleTinted", etc.)
728
+ hour: Hour of day (0-23)
729
+
730
+ Returns:
731
+ Corrected CLTD value (°C)
732
+ """
733
+ try:
734
+ base_cltd = self.cltd_skylight_tables[glazing_type]['Horizontal'][hour]
735
+ except KeyError:
736
+ base_cltd = 0.0
737
+
738
+ # Apply corrections
739
+ latitude_factor = self.latitude_corrections.get(self.latitude.value, 1.0)
740
+ month_factor = self.month_corrections.get(self.month, 1.0)
741
+ temp_correction = (self.outdoor_avg_temp - 29.4) + (self.indoor_temp - 24.0)
742
+
743
+ corrected_cltd = base_cltd * latitude_factor * month_factor + temp_correction
744
+ return max(0.0, corrected_cltd)
745
+
746
+
747
+ class WindowHeatGainCalculator:
748
+ """Class for calculating window heat gain using CLTD/SCL method."""
749
+
750
+ def __init__(self, cltd_calculator: CLTDCalculator):
751
+ """
752
+ Initialize window heat gain calculator.
753
+
754
+ Args:
755
+ cltd_calculator: Instance of CLTDCalculator
756
+ """
757
+ self.cltd_calculator = cltd_calculator
758
+
759
+ def calculate_window_heat_gain(self, area: float, glazing_type: GlazingType,
760
+ frame_type: FrameType, orientation: str, hour: int,
761
+ drapery: Optional[Drapery] = None) -> Tuple[float, float]:
762
+ """
763
+ Calculate window heat gain (conduction and solar).
764
+
765
+ Args:
766
+ area: Window area (m²)
767
+ glazing_type: Type of glazing
768
+ frame_type: Type of frame
769
+ orientation: Orientation ("N", "NE", etc.)
770
+ hour: Hour of day (0-23)
771
+ drapery: Drapery object (optional)
772
+
773
+ Returns:
774
+ Tuple of (conduction_heat_gain, solar_heat_gain) in Watts
775
+ """
776
+ # Get U-factor
777
+ u_factor = WINDOW_U_FACTORS.get((glazing_type, frame_type), 7.22)
778
+
779
+ # Get SHGC
780
+ shgc = WINDOW_SHGC.get((glazing_type, frame_type), 0.78)
781
+
782
+ # Get CLTD
783
+ cltd = self.cltd_calculator.get_cltd_window(glazing_type.value, orientation, hour)
784
+
785
+ # Calculate conduction heat gain
786
+ conduction_reduction = drapery.get_conduction_reduction() if drapery and drapery.enabled else 0.0
787
+ conduction_heat_gain = area * u_factor * cltd * (1.0 - conduction_reduction)
788
+
789
+ # Get SCL from ASHRAE tables
790
+ scl = self.cltd_calculator.ashrae_tables.get_scl(
791
+ latitude=self.cltd_calculator.latitude.value,
792
+ orientation=orientation,
793
+ hour=hour,
794
+ month=self.cltd_calculator.month
795
+ )
796
+
797
+ # Apply drapery shading coefficient
798
+ shading_coefficient = drapery.get_shading_coefficient(shgc) if drapery and drapery.enabled else 1.0
799
+ solar_heat_gain = area * shgc * scl * shading_coefficient
800
+
801
+ return conduction_heat_gain, solar_heat_gain
802
+
803
+ def calculate_skylight_heat_gain(self, area: float, glazing_type: GlazingType,
804
+ frame_type: FrameType, hour: int,
805
+ drapery: Optional[Drapery] = None) -> Tuple[float, float]:
806
+ """
807
+ Calculate skylight heat gain (conduction and solar).
808
+
809
+ Args:
810
+ area: Skylight area (m²)
811
+ glazing_type: Type of glazing
812
+ frame_type: Type of frame
813
+ hour: Hour of day (0-23)
814
+ drapery: Drapery object (optional)
815
+
816
+ Returns:
817
+ Tuple of (conduction_heat_gain, solar_heat_gain) in Watts
818
+ """
819
+ # Get U-factor
820
+ u_factor = SKYLIGHT_U_FACTORS.get((glazing_type, frame_type), 7.79)
821
+
822
+ # Get SHGC
823
+ shgc = SKYLIGHT_SHGC.get((glazing_type, frame_type), 0.83)
824
+
825
+ # Get CLTD
826
+ cltd = self.cltd_calculator.get_cltd_skylight(glazing_type.value, hour)
827
+
828
+ # Calculate conduction heat gain
829
+ conduction_reduction = drapery.get_conduction_reduction() if drapery and drapery.enabled else 0.0
830
+ conduction_heat_gain = area * u_factor * cltd * (1.0 - conduction_reduction)
831
+
832
+ # Get SCL for skylight (horizontal)
833
+ scl = self.cltd_calculator.ashrae_tables.get_scl(
834
+ latitude=self.cltd_calculator.latitude.value,
835
+ orientation='Horizontal',
836
+ hour=hour,
837
+ month=self.cltd_calculator.month
838
+ )
839
+
840
+ # Apply drapery shading coefficient
841
+ shading_coefficient = drapery.get_shading_coefficient(shgc) if drapery and drapery.enabled else 1.0
842
+ solar_heat_gain = area * shgc * scl * shading_coefficient
843
+
844
+ return conduction_heat_gain, solar_heat_gain
845
+
846
+
847
+ class DoorHeatGainCalculator:
848
+ """Class for calculating door heat gain using CLTD method."""
849
+
850
+ def __init__(self, cltd_calculator: CLTDCalculator):
851
+ """
852
+ Initialize door heat gain calculator.
853
+
854
+ Args:
855
+ cltd_calculator: Instance of CLTDCalculator
856
+ """
857
+ self.cltd_calculator = cltd_calculator
858
+
859
+ def calculate_door_heat_gain(self, area: float, door_type: str, orientation: str,
860
+ hour: int) -> float:
861
+ """
862
+ Calculate door heat gain (conduction only).
863
+
864
+ Args:
865
+ area: Door area (m²)
866
+ door_type: Type of door ("WoodSolid", "MetalInsulated", etc.)
867
+ orientation: Orientation ("N", "NE", etc.)
868
+ hour: Hour of day (0-23)
869
+
870
+ Returns:
871
+ Conduction heat gain in Watts
872
+ """
873
+ # Get U-factor
874
+ u_factor = DOOR_U_FACTORS.get(door_type, 3.00)
875
+
876
+ # Get CLTD
877
+ cltd = self.cltd_calculator.get_cltd_door(door_type, orientation, hour)
878
+
879
+ # Calculate conduction heat gain
880
+ conduction_heat_gain = area * u_factor * cltd
881
+
882
+ return conduction_heat_gain
883
+
884
+
885
+ def calculate_total_heat_gain(window_area: float, glazing_type: GlazingType,
886
+ frame_type: FrameType, orientation: str, hour: int,
887
+ drapery: Optional[Drapery] = None,
888
+ door_area: float = 0.0, door_type: str = "WoodSolid",
889
+ skylight_area: float = 0.0) -> Dict[str, float]:
890
+ """
891
+ Calculate total heat gain for a fenestration system.
892
+
893
+ Args:
894
+ window_area: Window area (m²)
895
+ glazing_type: Type of glazing
896
+ frame_type: Type of frame
897
+ orientation: Orientation ("N", "NE", etc.)
898
+ hour: Hour of day (0-23)
899
+ drapery: Drapery object (optional)
900
+ door_area: Door area (m²)
901
+ door_type: Type of door
902
+ skylight_area: Skylight area (m²)
903
+
904
+ Returns:
905
+ Dictionary with conduction and solar heat gains (Watts)
906
+ """
907
+ cltd_calculator = CLTDCalculator()
908
+ window_calculator = WindowHeatGainCalculator(cltd_calculator)
909
+ door_calculator = DoorHeatGainCalculator(cltd_calculator)
910
+
911
+ total_conduction = 0.0
912
+ total_solar = 0.0
913
+
914
+ # Calculate window heat gain
915
+ if window_area > 0:
916
+ conduction, solar = window_calculator.calculate_window_heat_gain(
917
+ window_area, glazing_type, frame_type, orientation, hour, drapery
918
+ )
919
+ total_conduction += conduction
920
+ total_solar += solar
921
+
922
+ # Calculate skylight heat gain
923
+ if skylight_area > 0:
924
+ conduction, solar = window_calculator.calculate_skylight_heat_gain(
925
+ skylight_area, glazing_type, frame_type, hour, drapery
926
+ )
927
+ total_conduction += conduction
928
+ total_solar += solar
929
+
930
+ # Calculate door heat gain
931
+ if door_area > 0:
932
+ conduction = door_calculator.calculate_door_heat_gain(
933
+ door_area, door_type, orientation, hour
934
+ )
935
+ total_conduction += conduction
936
+
937
+ return {
938
+ "conduction_heat_gain": total_conduction,
939
+ "solar_heat_gain": total_solar,
940
+ "total_heat_gain": total_conduction + total_solar
941
+ }