mabuseif commited on
Commit
0db3a37
·
verified ·
1 Parent(s): 2a32bd2

Upload cooling_load.py

Browse files
Files changed (1) hide show
  1. cooling_load.py +1180 -0
cooling_load.py ADDED
@@ -0,0 +1,1180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Cooling load calculation module for HVAC Load Calculator.
3
+ Implements ASHRAE steady-state methods with Cooling Load Temperature Difference (CLTD).
4
+ Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 18, Section 18.5.
5
+
6
+ Author: Dr Majed Abuseif
7
+ Date: April 2025
8
+ Version: 1.0.7
9
+ """
10
+
11
+ from typing import Dict, List, Any, Optional, Tuple
12
+ import numpy as np
13
+ import logging
14
+ from data.ashrae_tables import ASHRAETables
15
+ from utils.heat_transfer import HeatTransferCalculations
16
+ from utils.psychrometrics import Psychrometrics
17
+ from app.component_selection import Wall, Roof, Window, Door, Skylight, Orientation
18
+ from data.drapery import Drapery
19
+
20
+ # Set up logging
21
+ logging.basicConfig(level=logging.INFO)
22
+ logger = logging.getLogger(__name__)
23
+
24
+ class CoolingLoadCalculator:
25
+ """Class for cooling load calculations based on ASHRAE steady-state methods."""
26
+
27
+ def __init__(self, debug_mode: bool = False):
28
+ """
29
+ Initialize cooling load calculator.
30
+ Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 18, Section 18.5.
31
+
32
+ Args:
33
+ debug_mode: Enable debug logging if True
34
+ """
35
+ self.ashrae_tables = ASHRAETables()
36
+ self.heat_transfer = HeatTransferCalculations()
37
+ self.psychrometrics = Psychrometrics()
38
+ self.hours = list(range(24))
39
+ self.valid_latitudes = ['24N', '32N', '40N', '48N', '56N']
40
+ self.valid_months = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC']
41
+ self.valid_wall_groups = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']
42
+ self.valid_roof_groups = ['A', 'B', 'C', 'D', 'E', 'F', 'G']
43
+ self.debug_mode = debug_mode
44
+ if debug_mode:
45
+ logger.setLevel(logging.DEBUG)
46
+
47
+ def validate_latitude(self, latitude: Any) -> str:
48
+ """
49
+ Validate and normalize latitude input.
50
+ Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 14, Section 14.2.
51
+
52
+ Args:
53
+ latitude: Latitude input (str, float, or other)
54
+
55
+ Returns:
56
+ Valid latitude string ('24N', '32N', '40N', '48N', '56N')
57
+ """
58
+ try:
59
+ if not isinstance(latitude, str):
60
+ try:
61
+ lat_val = float(latitude)
62
+ if lat_val <= 28:
63
+ return '24N'
64
+ elif lat_val <= 36:
65
+ return '32N'
66
+ elif lat_val <= 44:
67
+ return '40N'
68
+ elif lat_val <= 52:
69
+ return '48N'
70
+ else:
71
+ return '56N'
72
+ except (ValueError, TypeError):
73
+ latitude = str(latitude)
74
+
75
+ latitude = latitude.strip().upper()
76
+ if self.debug_mode:
77
+ logger.debug(f"Validating latitude: {latitude}")
78
+
79
+ if '_' in latitude:
80
+ parts = latitude.split('_')
81
+ if len(parts) > 1:
82
+ lat_part = parts[0]
83
+ if self.debug_mode:
84
+ logger.warning(f"Detected concatenated input: {latitude}. Using latitude={lat_part}")
85
+ latitude = lat_part
86
+
87
+ if '.' in latitude or any(c.isdigit() for c in latitude):
88
+ num_part = ''.join(c for c in latitude if c.isdigit() or c == '.')
89
+ try:
90
+ lat_val = float(num_part)
91
+ if lat_val <= 28:
92
+ mapped_latitude = '24N'
93
+ elif lat_val <= 36:
94
+ mapped_latitude = '32N'
95
+ elif lat_val <= 44:
96
+ mapped_latitude = '40N'
97
+ elif lat_val <= 52:
98
+ mapped_latitude = '48N'
99
+ else:
100
+ mapped_latitude = '56N'
101
+ if self.debug_mode:
102
+ logger.debug(f"Mapped numerical latitude {lat_val} to {mapped_latitude}")
103
+ return mapped_latitude
104
+ except ValueError:
105
+ if self.debug_mode:
106
+ logger.warning(f"Cannot parse numerical latitude: {latitude}. Defaulting to '32N'")
107
+ return '32N'
108
+
109
+ if latitude in self.valid_latitudes:
110
+ return latitude
111
+
112
+ if self.debug_mode:
113
+ logger.warning(f"Invalid latitude: {latitude}. Defaulting to '32N'")
114
+ return '32N'
115
+
116
+ except Exception as e:
117
+ if self.debug_mode:
118
+ logger.error(f"Error validating latitude {latitude}: {str(e)}")
119
+ return '32N'
120
+
121
+ def validate_month(self, month: Any) -> str:
122
+ """
123
+ Validate and normalize month input.
124
+ Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 14, Section 14.2.
125
+
126
+ Args:
127
+ month: Month input (str or other)
128
+
129
+ Returns:
130
+ Valid month string in uppercase
131
+ """
132
+ try:
133
+ if not isinstance(month, str):
134
+ month = str(month)
135
+
136
+ month_upper = month.strip().upper()
137
+ if month_upper not in self.valid_months:
138
+ if self.debug_mode:
139
+ logger.warning(f"Invalid month: {month}. Defaulting to 'JUL'")
140
+ return 'JUL'
141
+ return month_upper
142
+
143
+ except Exception as e:
144
+ if self.debug_mode:
145
+ logger.error(f"Error validating month {month}: {str(e)}")
146
+ return 'JUL'
147
+
148
+ def validate_hour(self, hour: Any) -> int:
149
+ """
150
+ Validate and normalize hour input.
151
+ Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 14, Section 14.2.
152
+
153
+ Args:
154
+ hour: Hour input (int, float, or other)
155
+
156
+ Returns:
157
+ Valid hour integer (0-23)
158
+ """
159
+ try:
160
+ hour = int(float(str(hour)))
161
+ if not 0 <= hour <= 23:
162
+ if self.debug_mode:
163
+ logger.warning(f"Invalid hour: {hour}. Defaulting to 15")
164
+ return 15
165
+ return hour
166
+ except (ValueError, TypeError):
167
+ if self.debug_mode:
168
+ logger.warning(f"Invalid hour format: {hour}. Defaulting to 15")
169
+ return 15
170
+
171
+ def validate_conditions(self, outdoor_temp: float, indoor_temp: float,
172
+ outdoor_rh: float, indoor_rh: float) -> None:
173
+ """
174
+ Validate temperature and relative humidity inputs.
175
+ Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 1, Section 1.2.
176
+
177
+ Args:
178
+ outdoor_temp: Outdoor temperature in °C
179
+ indoor_temp: Indoor temperature in °C
180
+ outdoor_rh: Outdoor relative humidity in %
181
+ indoor_rh: Indoor relative humidity in %
182
+
183
+ Raises:
184
+ ValueError: If inputs are invalid
185
+ """
186
+ if not -50 <= outdoor_temp <= 60 or not -50 <= indoor_temp <= 60:
187
+ raise ValueError("Temperatures must be between -50°C and 60°C")
188
+ if not 0 <= outdoor_rh <= 100 or not 0 <= indoor_rh <= 100:
189
+ raise ValueError("Relative humidities must be between 0 and 100%")
190
+ if outdoor_temp - indoor_temp < 1:
191
+ raise ValueError("Outdoor temperature must be at least 1°C above indoor temperature for cooling")
192
+
193
+ def calculate_hourly_cooling_loads(
194
+ self,
195
+ building_components: Dict[str, List[Any]],
196
+ outdoor_conditions: Dict[str, Any],
197
+ indoor_conditions: Dict[str, Any],
198
+ internal_loads: Dict[str, Any],
199
+ building_volume: float,
200
+ p_atm: float = 101325
201
+ ) -> Dict[str, Any]:
202
+ """
203
+ Calculate hourly cooling loads for all components.
204
+ Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 18, Section 18.5.
205
+
206
+ Args:
207
+ building_components: Dictionary of building components
208
+ outdoor_conditions: Outdoor weather conditions (temperature, relative_humidity, latitude, month)
209
+ indoor_conditions: Indoor design conditions (temperature, relative_humidity)
210
+ internal_loads: Internal heat gains (people, lights, equipment, infiltration, ventilation)
211
+ building_volume: Building volume in cubic meters
212
+ p_atm: Atmospheric pressure in Pa (default: 101325 Pa)
213
+
214
+ Returns:
215
+ Dictionary containing hourly cooling loads
216
+ """
217
+ hourly_loads = {
218
+ 'walls': {h: 0.0 for h in range(1, 25)},
219
+ 'roofs': {h: 0.0 for h in range(1, 25)},
220
+ 'windows_conduction': {h: 0.0 for h in range(1, 25)},
221
+ 'windows_solar': {h: 0.0 for h in range(1, 25)},
222
+ 'skylights_conduction': {h: 0.0 for h in range(1, 25)},
223
+ 'skylights_solar': {h: 0.0 for h in range(1, 25)},
224
+ 'doors': {h: 0.0 for h in range(1, 25)},
225
+ 'people_sensible': {h: 0.0 for h in range(1, 25)},
226
+ 'people_latent': {h: 0.0 for h in range(1, 25)},
227
+ 'lights': {h: 0.0 for h in range(1, 25)},
228
+ 'equipment_sensible': {h: 0.0 for h in range(1, 25)},
229
+ 'equipment_latent': {h: 0.0 for h in range(1, 25)},
230
+ 'infiltration_sensible': {h: 0.0 for h in range(1, 25)},
231
+ 'infiltration_latent': {h: 0.0 for h in range(1, 25)},
232
+ 'ventilation_sensible': {h: 0.0 for h in range(1, 25)},
233
+ 'ventilation_latent': {h: 0.0 for h in range(1, 25)}
234
+ }
235
+
236
+ try:
237
+ # Validate conditions
238
+ self.validate_conditions(
239
+ outdoor_conditions['temperature'],
240
+ indoor_conditions['temperature'],
241
+ outdoor_conditions.get('relative_humidity', 50.0),
242
+ indoor_conditions.get('relative_humidity', 50.0)
243
+ )
244
+
245
+ latitude = self.validate_latitude(outdoor_conditions.get('latitude', '32N'))
246
+ month = self.validate_month(outdoor_conditions.get('month', 'JUL'))
247
+ if self.debug_mode:
248
+ logger.debug(f"calculate_hourly_cooling_loads: latitude={latitude}, month={month}, outdoor_conditions={outdoor_conditions}")
249
+
250
+ # Calculate loads for walls
251
+ for wall in building_components.get('walls', []):
252
+ for hour in range(24):
253
+ load = self.calculate_wall_cooling_load(
254
+ wall=wall,
255
+ outdoor_temp=outdoor_conditions['temperature'],
256
+ indoor_temp=indoor_conditions['temperature'],
257
+ month=month,
258
+ hour=hour,
259
+ latitude=latitude,
260
+ solar_absorptivity=wall.solar_absorptivity
261
+ )
262
+ hourly_loads['walls'][hour + 1] += load
263
+
264
+ # Calculate loads for roofs
265
+ for roof in building_components.get('roofs', []):
266
+ for hour in range(24):
267
+ load = self.calculate_roof_cooling_load(
268
+ roof=roof,
269
+ outdoor_temp=outdoor_conditions['temperature'],
270
+ indoor_temp=indoor_conditions['temperature'],
271
+ month=month,
272
+ hour=hour,
273
+ latitude=latitude,
274
+ solar_absorptivity=roof.solar_absorptivity
275
+ )
276
+ hourly_loads['roofs'][hour + 1] += load
277
+
278
+ # Calculate loads for windows
279
+ for window in building_components.get('windows', []):
280
+ for hour in range(24):
281
+ adjusted_shgc = getattr(window, 'adjusted_shgc', None)
282
+ load_dict = self.calculate_window_cooling_load(
283
+ window=window,
284
+ outdoor_temp=outdoor_conditions['temperature'],
285
+ indoor_temp=indoor_conditions['temperature'],
286
+ month=month,
287
+ hour=hour,
288
+ latitude=latitude,
289
+ shading_coefficient=window.shading_coefficient,
290
+ adjusted_shgc=adjusted_shgc
291
+ )
292
+ hourly_loads['windows_conduction'][hour + 1] += load_dict['conduction']
293
+ hourly_loads['windows_solar'][hour + 1] += load_dict['solar']
294
+
295
+ # Calculate loads for skylights
296
+ for skylight in building_components.get('skylights', []):
297
+ for hour in range(24):
298
+ adjusted_shgc = getattr(skylight, 'adjusted_shgc', None)
299
+ load_dict = self.calculate_skylight_cooling_load(
300
+ skylight=skylight,
301
+ outdoor_temp=outdoor_conditions['temperature'],
302
+ indoor_temp=indoor_conditions['temperature'],
303
+ month=month,
304
+ hour=hour,
305
+ latitude=latitude,
306
+ shading_coefficient=skylight.shading_coefficient,
307
+ adjusted_shgc=adjusted_shgc
308
+ )
309
+ hourly_loads['skylights_conduction'][hour + 1] += load_dict['conduction']
310
+ hourly_loads['skylights_solar'][hour + 1] += load_dict['solar']
311
+
312
+ # Calculate loads for doors
313
+ for door in building_components.get('doors', []):
314
+ for hour in range(24):
315
+ load = self.calculate_door_cooling_load(
316
+ door=door,
317
+ outdoor_temp=outdoor_conditions['temperature'],
318
+ indoor_temp=indoor_conditions['temperature']
319
+ )
320
+ hourly_loads['doors'][hour + 1] += load
321
+
322
+ # Calculate internal loads
323
+ for hour in range(24):
324
+ # People loads
325
+ people_load = self.calculate_people_cooling_load(
326
+ num_people=internal_loads['people']['number'],
327
+ activity_level=internal_loads['people']['activity_level'],
328
+ hour=hour
329
+ )
330
+ hourly_loads['people_sensible'][hour + 1] += people_load['sensible']
331
+ hourly_loads['people_latent'][hour + 1] += people_load['latent']
332
+
333
+ # Lighting loads
334
+ lights_load = self.calculate_lights_cooling_load(
335
+ power=internal_loads['lights']['power'],
336
+ use_factor=internal_loads['lights']['use_factor'],
337
+ special_allowance=internal_loads['lights']['special_allowance'],
338
+ hour=hour
339
+ )
340
+ hourly_loads['lights'][hour + 1] += lights_load
341
+
342
+ # Equipment loads
343
+ equipment_load = self.calculate_equipment_cooling_load(
344
+ power=internal_loads['equipment']['power'],
345
+ use_factor=internal_loads['equipment']['use_factor'],
346
+ radiation_factor=internal_loads['equipment']['radiation_factor'],
347
+ hour=hour
348
+ )
349
+ hourly_loads['equipment_sensible'][hour + 1] += equipment_load['sensible']
350
+ hourly_loads['equipment_latent'][hour + 1] += equipment_load['latent']
351
+
352
+ # Infiltration loads
353
+ infiltration_load = self.calculate_infiltration_cooling_load(
354
+ flow_rate=internal_loads['infiltration']['flow_rate'],
355
+ building_volume=building_volume,
356
+ outdoor_temp=outdoor_conditions['temperature'],
357
+ outdoor_rh=outdoor_conditions['relative_humidity'],
358
+ indoor_temp=indoor_conditions['temperature'],
359
+ indoor_rh=indoor_conditions['relative_humidity'],
360
+ p_atm=p_atm
361
+ )
362
+ hourly_loads['infiltration_sensible'][hour + 1] += infiltration_load['sensible']
363
+ hourly_loads['infiltration_latent'][hour + 1] += infiltration_load['latent']
364
+
365
+ # Ventilation loads
366
+ ventilation_load = self.calculate_ventilation_cooling_load(
367
+ flow_rate=internal_loads['ventilation']['flow_rate'],
368
+ outdoor_temp=outdoor_conditions['temperature'],
369
+ outdoor_rh=outdoor_conditions['relative_humidity'],
370
+ indoor_temp=indoor_conditions['temperature'],
371
+ indoor_rh=indoor_conditions['relative_humidity'],
372
+ p_atm=p_atm
373
+ )
374
+ hourly_loads['ventilation_sensible'][hour + 1] += ventilation_load['sensible']
375
+ hourly_loads['ventilation_latent'][hour + 1] += ventilation_load['latent']
376
+
377
+ return hourly_loads
378
+
379
+ except Exception as e:
380
+ if self.debug_mode:
381
+ logger.error(f"Error in calculate_hourly_cooling_loads: {str(e)}")
382
+ raise Exception(f"Error in calculate_hourly_cooling_loads: {str(e)}")
383
+
384
+ def calculate_design_cooling_load(self, hourly_loads: Dict[str, Any]) -> Dict[str, Any]:
385
+ """
386
+ Calculate design cooling load based on peak hourly loads.
387
+ Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 18, Section 18.5.
388
+
389
+ Args:
390
+ hourly_loads: Dictionary of hourly cooling loads
391
+
392
+ Returns:
393
+ Dictionary containing design cooling loads
394
+ """
395
+ try:
396
+ design_loads = {}
397
+ total_loads = []
398
+
399
+ for hour in range(1, 25):
400
+ total_load = sum([
401
+ hourly_loads['walls'][hour],
402
+ hourly_loads['roofs'][hour],
403
+ hourly_loads['windows_conduction'][hour],
404
+ hourly_loads['windows_solar'][hour],
405
+ hourly_loads['skylights_conduction'][hour],
406
+ hourly_loads['skylights_solar'][hour],
407
+ hourly_loads['doors'][hour],
408
+ hourly_loads['people_sensible'][hour],
409
+ hourly_loads['people_latent'][hour],
410
+ hourly_loads['lights'][hour],
411
+ hourly_loads['equipment_sensible'][hour],
412
+ hourly_loads['equipment_latent'][hour],
413
+ hourly_loads['infiltration_sensible'][hour],
414
+ hourly_loads['infiltration_latent'][hour],
415
+ hourly_loads['ventilation_sensible'][hour],
416
+ hourly_loads['ventilation_latent'][hour]
417
+ ])
418
+ total_loads.append(total_load)
419
+
420
+ design_hour = range(1, 25)[np.argmax(total_loads)]
421
+
422
+ design_loads = {
423
+ 'design_hour': design_hour,
424
+ 'walls': hourly_loads['walls'][design_hour],
425
+ 'roofs': hourly_loads['roofs'][design_hour],
426
+ 'windows_conduction': hourly_loads['windows_conduction'][design_hour],
427
+ 'windows_solar': hourly_loads['windows_solar'][design_hour],
428
+ 'skylights_conduction': hourly_loads['skylights_conduction'][design_hour],
429
+ 'skylights_solar': hourly_loads['skylights_solar'][design_hour],
430
+ 'doors': hourly_loads['doors'][design_hour],
431
+ 'people_sensible': hourly_loads['people_sensible'][design_hour],
432
+ 'people_latent': hourly_loads['people_latent'][design_hour],
433
+ 'lights': hourly_loads['lights'][design_hour],
434
+ 'equipment_sensible': hourly_loads['equipment_sensible'][design_hour],
435
+ 'equipment_latent': hourly_loads['equipment_latent'][design_hour],
436
+ 'infiltration_sensible': hourly_loads['infiltration_sensible'][design_hour],
437
+ 'infiltration_latent': hourly_loads['infiltration_latent'][design_hour],
438
+ 'ventilation_sensible': hourly_loads['ventilation_sensible'][design_hour],
439
+ 'ventilation_latent': hourly_loads['ventilation_latent'][design_hour]
440
+ }
441
+
442
+ return design_loads
443
+
444
+ except Exception as e:
445
+ if self.debug_mode:
446
+ logger.error(f"Error in calculate_design_cooling_load: {str(e)}")
447
+ raise Exception(f"Error in calculate_design_cooling_load: {str(e)}")
448
+
449
+ def calculate_cooling_load_summary(self, design_loads: Dict[str, Any]) -> Dict[str, float]:
450
+ """
451
+ Calculate summary of cooling loads.
452
+ Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 18, Section 18.5.
453
+
454
+ Args:
455
+ design_loads: Dictionary of design cooling loads
456
+
457
+ Returns:
458
+ Dictionary containing cooling load summary
459
+ """
460
+ try:
461
+ total_sensible = (
462
+ design_loads['walls'] +
463
+ design_loads['roofs'] +
464
+ design_loads['windows_conduction'] +
465
+ design_loads['windows_solar'] +
466
+ design_loads['skylights_conduction'] +
467
+ design_loads['skylights_solar'] +
468
+ design_loads['doors'] +
469
+ design_loads['people_sensible'] +
470
+ design_loads['lights'] +
471
+ design_loads['equipment_sensible'] +
472
+ design_loads['infiltration_sensible'] +
473
+ design_loads['ventilation_sensible']
474
+ )
475
+
476
+ total_latent = (
477
+ design_loads['people_latent'] +
478
+ design_loads['equipment_latent'] +
479
+ design_loads['infiltration_latent'] +
480
+ design_loads['ventilation_latent']
481
+ )
482
+
483
+ total = total_sensible + total_latent
484
+
485
+ return {
486
+ 'total_sensible': total_sensible,
487
+ 'total_latent': total_latent,
488
+ 'total': total
489
+ }
490
+
491
+ except Exception as e:
492
+ if self.debug_mode:
493
+ logger.error(f"Error in calculate_cooling_load_summary: {str(e)}")
494
+ raise Exception(f"Error in calculate_cooling_load_summary: {str(e)}")
495
+
496
+ def calculate_wall_cooling_load(
497
+ self,
498
+ wall: Wall,
499
+ outdoor_temp: float,
500
+ indoor_temp: float,
501
+ month: str,
502
+ hour: int,
503
+ latitude: str,
504
+ solar_absorptivity: float
505
+ ) -> float:
506
+ """
507
+ Calculate cooling load for a wall using CLTD method.
508
+ Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 18, Equation 18.10.
509
+
510
+ Args:
511
+ wall: Wall component
512
+ outdoor_temp: Outdoor temperature (°C)
513
+ indoor_temp: Indoor temperature (°C)
514
+ month: Design month
515
+ hour: Hour of the day
516
+ latitude: Latitude (e.g., '24N')
517
+ solar_absorptivity: Solar absorptivity of the wall surface (0.0 to 1.0)
518
+
519
+ Returns:
520
+ Cooling load in Watts
521
+ """
522
+ try:
523
+ latitude = self.validate_latitude(latitude)
524
+ month = self.validate_month(month)
525
+ hour = self.validate_hour(hour)
526
+ wall_group = str(wall.wall_group).upper() if hasattr(wall, 'wall_group') else 'A'
527
+ numeric_map = {'1': 'A', '2': 'B', '3': 'C', '4': 'D', '5': 'E', '6': 'F', '7': 'G', '8': 'H'}
528
+
529
+ if wall_group in numeric_map:
530
+ wall_group = numeric_map[wall_group]
531
+ if self.debug_mode:
532
+ logger.info(f"Mapped wall_group {wall.wall_group} to {wall_group}")
533
+ elif wall_group not in self.valid_wall_groups:
534
+ if self.debug_mode:
535
+ logger.warning(f"Invalid wall group: {wall_group}. Defaulting to 'A'")
536
+ wall_group = 'A'
537
+
538
+ try:
539
+ lat_value = float(latitude.replace('N', ''))
540
+ if self.debug_mode:
541
+ logger.debug(f"Converted latitude {latitude} to {lat_value} for wall CLTD")
542
+ except ValueError:
543
+ if self.debug_mode:
544
+ logger.error(f"Invalid latitude format: {latitude}. Defaulting to 32.0")
545
+ lat_value = 32.0
546
+
547
+ if self.debug_mode:
548
+ logger.debug(f"Calling get_cltd for wall: group={wall_group}, orientation={wall.orientation.value}, hour={hour}, latitude={lat_value}, solar_absorptivity={solar_absorptivity}")
549
+
550
+ try:
551
+ cltd_f = self.ashrae_tables.get_cltd(
552
+ element_type='wall',
553
+ group=wall_group,
554
+ orientation=wall.orientation.value,
555
+ hour=hour,
556
+ latitude=lat_value,
557
+ solar_absorptivity=solar_absorptivity
558
+ )
559
+ cltd = (cltd_f - 32) * 5 / 9 # Convert °F to °C
560
+ except Exception as e:
561
+ if self.debug_mode:
562
+ logger.error(f"get_cltd failed for wall_group={wall_group}, latitude={lat_value}: {str(e)}")
563
+ logger.warning("Using default CLTD=8.0°C")
564
+ cltd = 8.0
565
+
566
+ load = wall.u_value * wall.area * cltd
567
+ if self.debug_mode:
568
+ logger.debug(f"Wall load: u_value={wall.u_value}, area={wall.area}, cltd={cltd}, load={load}")
569
+ return max(load, 0.0)
570
+
571
+ except Exception as e:
572
+ if self.debug_mode:
573
+ logger.error(f"Error in calculate_wall_cooling_load: {str(e)}")
574
+ raise Exception(f"Error in calculate_wall_cooling_load: {str(e)}")
575
+
576
+ def calculate_roof_cooling_load(
577
+ self,
578
+ roof: Roof,
579
+ outdoor_temp: float,
580
+ indoor_temp: float,
581
+ month: str,
582
+ hour: int,
583
+ latitude: str,
584
+ solar_absorptivity: float
585
+ ) -> float:
586
+ """
587
+ Calculate cooling load for a roof using CLTD method.
588
+ Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 18, Equation 18.10.
589
+
590
+ Args:
591
+ roof: Roof component
592
+ outdoor_temp: Outdoor temperature (°C)
593
+ indoor_temp: Indoor temperature (°C)
594
+ month: Design month
595
+ hour: Hour of the day
596
+ latitude: Latitude (e.g., '24N')
597
+ solar_absorptivity: Solar absorptivity of the roof surface (0.0 to 1.0)
598
+
599
+ Returns:
600
+ Cooling load in Watts
601
+ """
602
+ try:
603
+ latitude = self.validate_latitude(latitude)
604
+ month = self.validate_month(month)
605
+ hour = self.validate_hour(hour)
606
+ roof_group = str(roof.roof_group).upper() if hasattr(roof, 'roof_group') else 'A'
607
+ numeric_map = {'1': 'A', '2': 'B', '3': 'C', '4': 'D', '5': 'E', '6': 'F', '7': 'G', '8': 'G'}
608
+
609
+ if roof_group in numeric_map:
610
+ roof_group = numeric_map[roof_group]
611
+ if self.debug_mode:
612
+ logger.info(f"Mapped roof_group {roof.roof_group} to {roof_group}")
613
+ elif roof_group not in self.valid_roof_groups:
614
+ if self.debug_mode:
615
+ logger.warning(f"Invalid roof group: {roof_group}. Defaulting to 'A'")
616
+ roof_group = 'A'
617
+
618
+ try:
619
+ lat_value = float(latitude.replace('N', ''))
620
+ if self.debug_mode:
621
+ logger.debug(f"Converted latitude {latitude} to {lat_value} for roof CLTD")
622
+ except ValueError:
623
+ if self.debug_mode:
624
+ logger.error(f"Invalid latitude format: {latitude}. Defaulting to 32.0")
625
+ lat_value = 32.0
626
+
627
+ if self.debug_mode:
628
+ logger.debug(f"Calling get_cltd for roof: group={roof_group}, orientation={roof.orientation.value}, hour={hour}, latitude={lat_value}, solar_absorptivity={solar_absorptivity}")
629
+
630
+ try:
631
+ cltd_f = self.ashrae_tables.get_cltd(
632
+ element_type='roof',
633
+ group=roof_group,
634
+ orientation=roof.orientation.value,
635
+ hour=hour,
636
+ latitude=lat_value,
637
+ solar_absorptivity=solar_absorptivity
638
+ )
639
+ cltd = (cltd_f - 32) * 5 / 9 # Convert °F to °C
640
+ except Exception as e:
641
+ if self.debug_mode:
642
+ logger.error(f"get_cltd failed for roof_group={roof_group}, latitude={lat_value}: {str(e)}")
643
+ logger.warning("Using default CLTD=8.0°C")
644
+ cltd = 8.0
645
+
646
+ load = roof.u_value * roof.area * cltd
647
+ if self.debug_mode:
648
+ logger.debug(f"Roof load: u_value={roof.u_value}, area={roof.area}, cltd={cltd}, load={load}")
649
+ return max(load, 0.0)
650
+
651
+ except Exception as e:
652
+ if self.debug_mode:
653
+ logger.error(f"Error in calculate_roof_cooling_load: {str(e)}")
654
+ raise Exception(f"Error in calculate_roof_cooling_load: {str(e)}")
655
+
656
+ def calculate_window_cooling_load(
657
+ self,
658
+ window: Window,
659
+ outdoor_temp: float,
660
+ indoor_temp: float,
661
+ month: str,
662
+ hour: int,
663
+ latitude: str,
664
+ shading_coefficient: float,
665
+ adjusted_shgc: Optional[float] = None
666
+ ) -> Dict[str, float]:
667
+ """
668
+ Calculate cooling load for a window (conduction and solar).
669
+ Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 18, Equations 18.12-18.13.
670
+
671
+ Args:
672
+ window: Window component
673
+ outdoor_temp: Outdoor temperature (°C)
674
+ indoor_temp: Indoor temperature (°C)
675
+ month: Design month
676
+ hour: Hour of the day
677
+ latitude: Latitude (e.g., '24N')
678
+ shading_coefficient: Default shading coefficient
679
+ adjusted_shgc: Adjusted SHGC from external drapery calculation (optional)
680
+
681
+ Returns:
682
+ Dictionary with conduction, solar, and total loads in Watts
683
+ """
684
+ try:
685
+ latitude = self.validate_latitude(latitude)
686
+ month = self.validate_month(month)
687
+ hour = self.validate_hour(hour)
688
+ if self.debug_mode:
689
+ logger.debug(f"calculate_window_cooling_load: latitude={latitude}, month={month}, hour={hour}, orientation={window.orientation.value}")
690
+
691
+ # Conduction load
692
+ cltd = outdoor_temp - indoor_temp
693
+ conduction_load = window.u_value * window.area * cltd
694
+
695
+ # Determine shading coefficient
696
+ effective_shading_coefficient = adjusted_shgc if adjusted_shgc is not None else shading_coefficient
697
+ if adjusted_shgc is None and hasattr(window, 'drapery') and window.drapery and window.drapery.enabled:
698
+ try:
699
+ effective_shading_coefficient = window.drapery.get_shading_coefficient(window.shgc)
700
+ if self.debug_mode:
701
+ logger.debug(f"Using drapery shading coefficient: {effective_shading_coefficient}")
702
+ except Exception as e:
703
+ if self.debug_mode:
704
+ logger.warning(f"Error getting drapery shading coefficient: {str(e)}. Using default shading_coefficient={shading_coefficient}")
705
+ else:
706
+ if self.debug_mode:
707
+ logger.debug(f"Using shading coefficient: {effective_shading_coefficient} (adjusted_shgc={adjusted_shgc}, drapery={'enabled' if hasattr(window, 'drapery') and window.drapery and window.drapery.enabled else 'disabled'})")
708
+
709
+ # Solar load
710
+ try:
711
+ scl = self.ashrae_tables.get_scl(
712
+ latitude=latitude,
713
+ month=month,
714
+ orientation=window.orientation.value,
715
+ hour=hour
716
+ )
717
+ except Exception as e:
718
+ if self.debug_mode:
719
+ logger.error(f"get_scl failed for latitude={latitude}, month={month}, orientation={window.orientation.value}: {str(e)}")
720
+ logger.warning("Using default SCL=100 W/m²")
721
+ scl = 100.0
722
+
723
+ solar_load = window.area * window.shgc * effective_shading_coefficient * scl
724
+
725
+ total_load = conduction_load + solar_load
726
+ if self.debug_mode:
727
+ logger.debug(f"Window load: conduction={conduction_load}, solar={solar_load}, total={total_load}, effective_shading_coefficient={effective_shading_coefficient}")
728
+
729
+ return {
730
+ 'conduction': max(conduction_load, 0.0),
731
+ 'solar': max(solar_load, 0.0),
732
+ 'total': max(total_load, 0.0)
733
+ }
734
+
735
+ except Exception as e:
736
+ if self.debug_mode:
737
+ logger.error(f"Error in calculate_window_cooling_load: {str(e)}")
738
+ raise Exception(f"Error in calculate_window_cooling_load: {str(e)}")
739
+
740
+ def calculate_skylight_cooling_load(
741
+ self,
742
+ skylight: Skylight,
743
+ outdoor_temp: float,
744
+ indoor_temp: float,
745
+ month: str,
746
+ hour: int,
747
+ latitude: str,
748
+ shading_coefficient: float,
749
+ adjusted_shgc: Optional[float] = None
750
+ ) -> Dict[str, float]:
751
+ """
752
+ Calculate cooling load for a skylight (conduction and solar).
753
+ Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 18, Equations 18.12-18.13.
754
+
755
+ Args:
756
+ skylight: Skylight component
757
+ outdoor_temp: Outdoor temperature (°C)
758
+ indoor_temp: Indoor temperature (°C)
759
+ month: Design month
760
+ hour: Hour of the day
761
+ latitude: Latitude (e.g., '24N')
762
+ shading_coefficient: Default shading coefficient
763
+ adjusted_shgc: Adjusted SHGC from external drapery calculation (optional)
764
+
765
+ Returns:
766
+ Dictionary with conduction, solar, and total loads in Watts
767
+ """
768
+ try:
769
+ latitude = self.validate_latitude(latitude)
770
+ month = self.validate_month(month)
771
+ hour = self.validate_hour(hour)
772
+ if self.debug_mode:
773
+ logger.debug(f"calculate_skylight_cooling_load: latitude={latitude}, month={month}, hour={hour}, orientation=Horizontal")
774
+
775
+ # Conduction load
776
+ cltd = outdoor_temp - indoor_temp
777
+ conduction_load = skylight.u_value * skylight.area * cltd
778
+
779
+ # Determine shading coefficient
780
+ effective_shading_coefficient = adjusted_shgc if adjusted_shgc is not None else shading_coefficient
781
+ if adjusted_shgc is None and hasattr(skylight, 'drapery') and skylight.drapery and skylight.drapery.enabled:
782
+ try:
783
+ effective_shading_coefficient = skylight.drapery.get_shading_coefficient(skylight.shgc)
784
+ if self.debug_mode:
785
+ logger.debug(f"Using drapery shading coefficient: {effective_shading_coefficient}")
786
+ except Exception as e:
787
+ if self.debug_mode:
788
+ logger.warning(f"Error getting drapery shading coefficient: {str(e)}. Using default shading_coefficient={shading_coefficient}")
789
+ else:
790
+ if self.debug_mode:
791
+ logger.debug(f"Using shading coefficient: {effective_shading_coefficient} (adjusted_shgc={adjusted_shgc}, drapery={'enabled' if hasattr(skylight, 'drapery') and skylight.drapery and skylight.drapery.enabled else 'disabled'})")
792
+
793
+ # Solar load
794
+ try:
795
+ scl = self.ashrae_tables.get_scl(
796
+ latitude=latitude,
797
+ month=month,
798
+ orientation='Horizontal',
799
+ hour=hour
800
+ )
801
+ except Exception as e:
802
+ if self.debug_mode:
803
+ logger.error(f"get_scl failed for latitude={latitude}, month={month}, orientation=Horizontal: {str(e)}")
804
+ logger.warning("Using default SCL=100 W/m²")
805
+ scl = 100.0
806
+
807
+ solar_load = skylight.area * skylight.shgc * effective_shading_coefficient * scl
808
+
809
+ total_load = conduction_load + solar_load
810
+ if self.debug_mode:
811
+ logger.debug(f"Skylight load: conduction={conduction_load}, solar={solar_load}, total={total_load}, effective_shading_coefficient={effective_shading_coefficient}")
812
+
813
+ return {
814
+ 'conduction': max(conduction_load, 0.0),
815
+ 'solar': max(solar_load, 0.0),
816
+ 'total': max(total_load, 0.0)
817
+ }
818
+
819
+ except Exception as e:
820
+ if self.debug_mode:
821
+ logger.error(f"Error in calculate_skylight_cooling_load: {str(e)}")
822
+ raise Exception(f"Error in calculate_skylight_cooling_load: {str(e)}")
823
+
824
+ def calculate_door_cooling_load(
825
+ self,
826
+ door: Door,
827
+ outdoor_temp: float,
828
+ indoor_temp: float
829
+ ) -> float:
830
+ """
831
+ Calculate cooling load for a door.
832
+ Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 18, Equation 18.1.
833
+
834
+ Args:
835
+ door: Door component
836
+ outdoor_temp: Outdoor temperature (°C)
837
+ indoor_temp: Indoor temperature (°C)
838
+
839
+ Returns:
840
+ Cooling load in Watts
841
+ """
842
+ try:
843
+ if self.debug_mode:
844
+ logger.debug(f"calculate_door_cooling_load: u_value={door.u_value}, area={door.area}")
845
+
846
+ cltd = outdoor_temp - indoor_temp
847
+ load = door.u_value * door.area * cltd
848
+ if self.debug_mode:
849
+ logger.debug(f"Door load: cltd={cltd}, load={load}")
850
+ return max(load, 0.0)
851
+
852
+ except Exception as e:
853
+ if self.debug_mode:
854
+ logger.error(f"Error in calculate_door_cooling_load: {str(e)}")
855
+ raise Exception(f"Error in calculate_door_cooling_load: {str(e)}")
856
+
857
+ def calculate_people_cooling_load(
858
+ self,
859
+ num_people: int,
860
+ activity_level: str,
861
+ hour: int
862
+ ) -> Dict[str, float]:
863
+ """
864
+ Calculate cooling load from people.
865
+ Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 18, Table 18.4.
866
+
867
+ Args:
868
+ num_people: Number of people
869
+ activity_level: Activity level ('Seated/Resting', 'Light Work', etc.)
870
+ hour: Hour of the day
871
+
872
+ Returns:
873
+ Dictionary with sensible and latent loads in Watts
874
+ """
875
+ try:
876
+ hour = self.validate_hour(hour)
877
+ if self.debug_mode:
878
+ logger.debug(f"calculate_people_cooling_load: num_people={num_people}, activity_level={activity_level}, hour={hour}")
879
+
880
+ heat_gains = {
881
+ 'Seated/Resting': {'sensible': 70, 'latent': 45},
882
+ 'Light Work': {'sensible': 85, 'latent': 65},
883
+ 'Moderate Work': {'sensible': 100, 'latent': 100},
884
+ 'Heavy Work': {'sensible': 145, 'latent': 170}
885
+ }
886
+
887
+ gains = heat_gains.get(activity_level, heat_gains['Seated/Resting'])
888
+ if activity_level not in heat_gains:
889
+ if self.debug_mode:
890
+ logger.warning(f"Invalid activity_level: {activity_level}. Defaulting to 'Seated/Resting'")
891
+
892
+ try:
893
+ clf = self.ashrae_tables.get_clf_people(
894
+ zone_type='A',
895
+ hours_occupied='6h',
896
+ hour=hour
897
+ )
898
+ except Exception as e:
899
+ if self.debug_mode:
900
+ logger.error(f"get_clf_people failed: {str(e)}")
901
+ logger.warning("Using default CLF=0.5")
902
+ clf = 0.5
903
+
904
+ sensible_load = num_people * gains['sensible'] * clf
905
+ latent_load = num_people * gains['latent']
906
+ if self.debug_mode:
907
+ logger.debug(f"People load: sensible={sensible_load}, latent={latent_load}, clf={clf}")
908
+
909
+ return {
910
+ 'sensible': max(sensible_load, 0.0),
911
+ 'latent': max(latent_load, 0.0)
912
+ }
913
+
914
+ except Exception as e:
915
+ if self.debug_mode:
916
+ logger.error(f"Error in calculate_people_cooling_load: {str(e)}")
917
+ raise Exception(f"Error in calculate_people_cooling_load: {str(e)}")
918
+
919
+ def calculate_lights_cooling_load(
920
+ self,
921
+ power: float,
922
+ use_factor: float,
923
+ special_allowance: float,
924
+ hour: int
925
+ ) -> float:
926
+ """
927
+ Calculate cooling load from lighting.
928
+ Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 18, Table 18.5.
929
+
930
+ Args:
931
+ power: Total lighting power (W)
932
+ use_factor: Usage factor (0.0 to 1.0)
933
+ special_allowance: Special allowance factor
934
+ hour: Hour of the day
935
+
936
+ Returns:
937
+ Cooling load in Watts
938
+ """
939
+ try:
940
+ hour = self.validate_hour(hour)
941
+ if self.debug_mode:
942
+ logger.debug(f"calculate_lights_cooling_load: power={power}, use_factor={use_factor}, special_allowance={special_allowance}, hour={hour}")
943
+
944
+ try:
945
+ clf = self.ashrae_tables.get_clf_lights(
946
+ zone_type='A',
947
+ hours_occupied='6h',
948
+ hour=hour
949
+ )
950
+ except Exception as e:
951
+ if self.debug_mode:
952
+ logger.error(f"get_clf_lights failed: {str(e)}")
953
+ logger.warning("Using default CLF=0.8")
954
+ clf = 0.8
955
+
956
+ load = power * use_factor * special_allowance * clf
957
+ if self.debug_mode:
958
+ logger.debug(f"Lights load: clf={clf}, load={load}")
959
+ return max(load, 0.0)
960
+
961
+ except Exception as e:
962
+ if self.debug_mode:
963
+ logger.error(f"Error in calculate_lights_cooling_load: {str(e)}")
964
+ raise Exception(f"Error in calculate_lights_cooling_load: {str(e)}")
965
+
966
+ def calculate_equipment_cooling_load(
967
+ self,
968
+ power: float,
969
+ use_factor: float,
970
+ radiation_factor: float,
971
+ hour: int
972
+ ) -> Dict[str, float]:
973
+ """
974
+ Calculate cooling load from equipment.
975
+ Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 18, Table 18.6.
976
+
977
+ Args:
978
+ power: Total equipment power (W)
979
+ use_factor: Usage factor (0.0 to 1.0)
980
+ radiation_factor: Radiation factor (0.0 to 1.0)
981
+ hour: Hour of the day
982
+
983
+ Returns:
984
+ Dictionary with sensible and latent loads in Watts
985
+ """
986
+ try:
987
+ hour = self.validate_hour(hour)
988
+ if self.debug_mode:
989
+ logger.debug(f"calculate_equipment_cooling_load: power={power}, use_factor={use_factor}, radiation_factor={radiation_factor}, hour={hour}")
990
+
991
+ try:
992
+ clf = self.ashrae_tables.get_clf_equipment(
993
+ zone_type='A',
994
+ hours_operated='6h',
995
+ hour=hour
996
+ )
997
+ except Exception as e:
998
+ if self.debug_mode:
999
+ logger.error(f"get_clf_equipment failed: {str(e)}")
1000
+ logger.warning("Using default CLF=0.7")
1001
+ clf = 0.7
1002
+
1003
+ sensible_load = power * use_factor * radiation_factor * clf
1004
+ latent_load = power * use_factor * (1 - radiation_factor)
1005
+ if self.debug_mode:
1006
+ logger.debug(f"Equipment load: sensible={sensible_load}, latent={latent_load}, clf={clf}")
1007
+
1008
+ return {
1009
+ 'sensible': max(sensible_load, 0.0),
1010
+ 'latent': max(latent_load, 0.0)
1011
+ }
1012
+
1013
+ except Exception as e:
1014
+ if self.debug_mode:
1015
+ logger.error(f"Error in calculate_equipment_cooling_load: {str(e)}")
1016
+ raise Exception(f"Error in calculate_equipment_cooling_load: {str(e)}")
1017
+
1018
+ def calculate_infiltration_cooling_load(
1019
+ self,
1020
+ flow_rate: float,
1021
+ building_volume: float,
1022
+ outdoor_temp: float,
1023
+ outdoor_rh: float,
1024
+ indoor_temp: float,
1025
+ indoor_rh: float,
1026
+ p_atm: float = 101325
1027
+ ) -> Dict[str, float]:
1028
+ """
1029
+ Calculate cooling load from infiltration.
1030
+ Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 18, Equations 18.5-18.6.
1031
+
1032
+ Args:
1033
+ flow_rate: Infiltration flow rate (m³/s)
1034
+ building_volume: Building volume (m³)
1035
+ outdoor_temp: Outdoor temperature (°C)
1036
+ outdoor_rh: Outdoor relative humidity (%)
1037
+ indoor_temp: Indoor temperature (°C)
1038
+ indoor_rh: Indoor relative humidity (%)
1039
+ p_atm: Atmospheric pressure in Pa (default: 101325 Pa)
1040
+
1041
+ Returns:
1042
+ Dictionary with sensible and latent loads in Watts
1043
+ """
1044
+ try:
1045
+ if self.debug_mode:
1046
+ logger.debug(f"calculate_infiltration_cooling_load: flow_rate={flow_rate}, building_volume={building_volume}, outdoor_temp={outdoor_temp}, indoor_temp={indoor_temp}")
1047
+
1048
+ self.validate_conditions(outdoor_temp, indoor_temp, outdoor_rh, indoor_rh)
1049
+ if flow_rate < 0 or building_volume <= 0:
1050
+ raise ValueError("Flow rate cannot be negative and building volume must be positive")
1051
+
1052
+ # Calculate air changes per hour (ACH)
1053
+ ach = (flow_rate * 3600) / building_volume if building_volume > 0 else 0.5
1054
+ if ach < 0:
1055
+ if self.debug_mode:
1056
+ logger.warning(f"Invalid ACH: {ach}. Defaulting to 0.5")
1057
+ ach = 0.5
1058
+
1059
+ # Calculate humidity ratio difference
1060
+ outdoor_w = self.heat_transfer.psychrometrics.humidity_ratio(outdoor_temp, outdoor_rh, p_atm)
1061
+ indoor_w = self.heat_transfer.psychrometrics.humidity_ratio(indoor_temp, indoor_rh, p_atm)
1062
+ delta_w = max(0, outdoor_w - indoor_w)
1063
+
1064
+ # Calculate sensible and latent loads using heat_transfer methods
1065
+ sensible_load = self.heat_transfer.infiltration_heat_transfer(
1066
+ flow_rate, outdoor_temp - indoor_temp, indoor_temp, indoor_rh, p_atm
1067
+ )
1068
+ latent_load = self.heat_transfer.infiltration_latent_heat_transfer(
1069
+ flow_rate, delta_w, indoor_temp, indoor_rh, p_atm
1070
+ )
1071
+
1072
+ if self.debug_mode:
1073
+ logger.debug(f"Infiltration load: sensible={sensible_load}, latent={latent_load}, ach={ach}, outdoor_w={outdoor_w}, indoor_w={indoor_w}")
1074
+
1075
+ return {
1076
+ 'sensible': max(sensible_load, 0.0),
1077
+ 'latent': max(latent_load, 0.0)
1078
+ }
1079
+
1080
+ except Exception as e:
1081
+ if self.debug_mode:
1082
+ logger.error(f"Error in calculate_infiltration_cooling_load: {str(e)}")
1083
+ raise Exception(f"Error in calculate_infiltration_cooling_load: {str(e)}")
1084
+
1085
+ def calculate_ventilation_cooling_load(
1086
+ self,
1087
+ flow_rate: float,
1088
+ outdoor_temp: float,
1089
+ outdoor_rh: float,
1090
+ indoor_temp: float,
1091
+ indoor_rh: float,
1092
+ p_atm: float = 101325
1093
+ ) -> Dict[str, float]:
1094
+ """
1095
+ Calculate cooling load from ventilation.
1096
+ Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 18, Equations 18.5-18.6.
1097
+
1098
+ Args:
1099
+ flow_rate: Ventilation flow rate (m³/s)
1100
+ outdoor_temp: Outdoor temperature (°C)
1101
+ outdoor_rh: Outdoor relative humidity (%)
1102
+ indoor_temp: Indoor temperature (°C)
1103
+ indoor_rh: Indoor relative humidity (%)
1104
+ p_atm: Atmospheric pressure in Pa (default: 101325 Pa)
1105
+
1106
+ Returns:
1107
+ Dictionary with sensible and latent loads in Watts
1108
+ """
1109
+ try:
1110
+ if self.debug_mode:
1111
+ logger.debug(f"calculate_ventilation_cooling_load: flow_rate={flow_rate}, outdoor_temp={outdoor_temp}, indoor_temp={indoor_temp}")
1112
+
1113
+ self.validate_conditions(outdoor_temp, indoor_temp, outdoor_rh, indoor_rh)
1114
+ if flow_rate < 0:
1115
+ raise ValueError("Flow rate cannot be negative")
1116
+
1117
+ # Calculate humidity ratio difference
1118
+ outdoor_w = self.heat_transfer.psychrometrics.humidity_ratio(outdoor_temp, outdoor_rh, p_atm)
1119
+ indoor_w = self.heat_transfer.psychrometrics.humidity_ratio(indoor_temp, indoor_rh, p_atm)
1120
+ delta_w = max(0, outdoor_w - indoor_w)
1121
+
1122
+ # Calculate sensible and latent loads using heat_transfer methods
1123
+ sensible_load = self.heat_transfer.infiltration_heat_transfer(
1124
+ flow_rate, outdoor_temp - indoor_temp, indoor_temp, indoor_rh, p_atm
1125
+ )
1126
+ latent_load = self.heat_transfer.infiltration_latent_heat_transfer(
1127
+ flow_rate, delta_w, indoor_temp, indoor_rh, p_atm
1128
+ )
1129
+
1130
+ if self.debug_mode:
1131
+ logger.debug(f"Ventilation load: sensible={sensible_load}, latent={latent_load}, outdoor_w={outdoor_w}, indoor_w={indoor_w}")
1132
+
1133
+ return {
1134
+ 'sensible': max(sensible_load, 0.0),
1135
+ 'latent': max(latent_load, 0.0)
1136
+ }
1137
+
1138
+ except Exception as e:
1139
+ if self.debug_mode:
1140
+ logger.error(f"Error in calculate_ventilation_cooling_load: {str(e)}")
1141
+ raise Exception(f"Error in calculate_ventilation_cooling_load: {str(e)}")
1142
+
1143
+ # Example usage
1144
+ if __name__ == "__main__":
1145
+ calculator = CoolingLoadCalculator(debug_mode=True)
1146
+
1147
+ # Example inputs
1148
+ components = {
1149
+ 'walls': [Wall(id="w1", name="North Wall", area=20.0, u_value=0.5, orientation=Orientation.NORTH, wall_group='A', solar_absorptivity=0.6)],
1150
+ 'roofs': [Roof(id="r1", name="Main Roof", area=100.0, u_value=0.3, orientation=Orientation.HORIZONTAL, roof_group='A', solar_absorptivity=0.6)],
1151
+ 'windows': [Window(id="win1", name="South Window", area=10.0, u_value=2.8, orientation=Orientation.SOUTH, shgc=0.7, shading_coefficient=0.8)],
1152
+ 'doors': [Door(id="d1", name="Main Door", area=2.0, u_value=2.0, orientation=Orientation.NORTH)]
1153
+ }
1154
+ outdoor_conditions = {
1155
+ 'temperature': 35.0,
1156
+ 'relative_humidity': 60.0,
1157
+ 'latitude': '32N',
1158
+ 'month': 'JUL'
1159
+ }
1160
+ indoor_conditions = {
1161
+ 'temperature': 24.0,
1162
+ 'relative_humidity': 50.0
1163
+ }
1164
+ internal_loads = {
1165
+ 'people': {'number': 10, 'activity_level': 'Seated/Resting'},
1166
+ 'lights': {'power': 1000.0, 'use_factor': 0.8, 'special_allowance': 1.0},
1167
+ 'equipment': {'power': 500.0, 'use_factor': 0.7, 'radiation_factor': 0.5},
1168
+ 'infiltration': {'flow_rate': 0.05},
1169
+ 'ventilation': {'flow_rate': 0.1}
1170
+ }
1171
+ building_volume = 300.0
1172
+
1173
+ # Calculate hourly loads
1174
+ hourly_loads = calculator.calculate_hourly_cooling_loads(
1175
+ components, outdoor_conditions, indoor_conditions, internal_loads, building_volume
1176
+ )
1177
+ design_loads = calculator.calculate_design_cooling_load(hourly_loads)
1178
+ summary = calculator.calculate_cooling_load_summary(design_loads)
1179
+
1180
+ logger.info(f"Design Cooling Load Summary: {summary}")