mabuseif commited on
Commit
32bccf6
·
verified ·
1 Parent(s): 2de4402

Update utils/cooling_load.py

Browse files
Files changed (1) hide show
  1. utils/cooling_load.py +677 -9
utils/cooling_load.py CHANGED
@@ -4,7 +4,675 @@ Based on ASHRAE steady-state calculation methods.
4
 
5
  Author: Dr Majed Abuseif
6
  Date: April 2025
7
- Version: 1.0.1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  """
9
 
10
  import streamlit as st
@@ -822,13 +1490,13 @@ class CoolingLoadCalculator:
822
  # Sensible load: Q = 1.2 * flow_rate * (Tout - Tin)
823
  sensible_load = 1.2 * flow_rate * (outdoor_temp - indoor_temp)
824
 
825
- # Latent load using simplified psychrometrics
826
- outdoor_w = self.heat_transfer.calculate_humidity_ratio(outdoor_temp, outdoor_rh)
827
- indoor_w = self.heat_transfer.calculate_humidity_ratio(indoor_temp, indoor_rh)
828
  latent_load = 1.2 * flow_rate * 2500 * (outdoor_w - indoor_w) # 2500 kJ/kg for latent heat
829
 
830
  if st.session_state.get('debug_mode', False):
831
- logger.debug(f"Infiltration load: sensible={sensible_load}, latent={latent_load}, ach={ach}")
832
 
833
  return {
834
  'sensible': max(sensible_load, 0.0),
@@ -868,13 +1536,13 @@ class CoolingLoadCalculator:
868
  # Sensible load: Q = 1.2 * flow_rate * (Tout - Tin)
869
  sensible_load = 1.2 * flow_rate * (outdoor_temp - indoor_temp)
870
 
871
- # Latent load using simplified psychrometrics
872
- outdoor_w = self.heat_transfer.calculate_humidity_ratio(outdoor_temp, outdoor_rh)
873
- indoor_w = self.heat_transfer.calculate_humidity_ratio(indoor_temp, indoor_rh)
874
  latent_load = 1.2 * flow_rate * 2500 * (outdoor_w - indoor_w) # 2500 kJ/kg for latent heat
875
 
876
  if st.session_state.get('debug_mode', False):
877
- logger.debug(f"Ventilation load: sensible={sensible_load}, latent={latent_load}")
878
 
879
  return {
880
  'sensible': max(sensible_load, 0.0),
 
4
 
5
  Author: Dr Majed Abuseif
6
  Date: April 2025
7
+ Version: 1.0.2
8
+ """
9
+
10
+ import streamlit as st
11
+ from typing import Dict, List, Any, Optional, Tuple
12
+ import numpy as np
13
+ from datetime import datetime
14
+ import logging
15
+ from data.ashrae_tables import ASHRAETables
16
+ from utils.heat_transfer import HeatTransferCalculations
17
+ from app.component_selection import Wall, Roof, Window, Door, Orientation
18
+
19
+ # Set up logging
20
+ logging.basicConfig(level=logging.DEBUG)
21
+ logger = logging.getLogger(__name__)
22
+
23
+ class CoolingLoadCalculator:
24
+ """Class for cooling load calculations."""
25
+
26
+ def __init__(self):
27
+ """Initialize cooling load calculator."""
28
+ self.ashrae_tables = ASHRAETables()
29
+ self.heat_transfer = HeatTransferCalculations()
30
+ self.hours = list(range(24))
31
+ self.valid_latitudes = ['24N', '36N', '48N']
32
+ self.valid_months = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC']
33
+ self.valid_wall_groups = ['A', 'B', 'C', 'D']
34
+ self.valid_roof_groups = ['A', 'B', 'C', 'D', 'E', 'F', 'G']
35
+
36
+ def validate_latitude(self, latitude: Any) -> str:
37
+ """
38
+ Validate and normalize latitude input.
39
+
40
+ Args:
41
+ latitude: Latitude input (str, float, or other)
42
+
43
+ Returns:
44
+ Valid latitude string ('24N', '36N', or '48N')
45
+ """
46
+ try:
47
+ if not isinstance(latitude, str):
48
+ latitude = str(latitude)
49
+
50
+ latitude = latitude.strip().upper()
51
+
52
+ # Handle concatenated formats like '24N_JUL'
53
+ if '_' in latitude:
54
+ parts = latitude.split('_')
55
+ if len(parts) > 1:
56
+ lat_part = parts[0]
57
+ if st.session_state.get('debug_mode', False):
58
+ logger.warning(f"Detected concatenated input: {latitude}. Using latitude={lat_part}")
59
+ latitude = lat_part
60
+
61
+ # Handle formats like '31.973N', '1_31.973N'
62
+ if '.' in latitude or any(c.isdigit() for c in latitude):
63
+ num_part = ''.join(c for c in latitude if c.isdigit() or c == '.')
64
+ try:
65
+ lat_val = float(num_part)
66
+ if lat_val <= 30:
67
+ return '24N'
68
+ elif lat_val <= 42:
69
+ return '36N'
70
+ else:
71
+ return '48N'
72
+ except ValueError:
73
+ if st.session_state.get('debug_mode', False):
74
+ logger.warning(f"Cannot parse latitude: {latitude}. Defaulting to '24N'")
75
+ return '24N'
76
+
77
+ if latitude not in self.valid_latitudes:
78
+ if st.session_state.get('debug_mode', False):
79
+ logger.warning(f"Invalid latitude: {latitude}. Defaulting to '24N'")
80
+ return '24N'
81
+
82
+ return latitude
83
+
84
+ except Exception as e:
85
+ if st.session_state.get('debug_mode', False):
86
+ logger.error(f"Error validating latitude {latitude}: {str(e)}")
87
+ return '24N'
88
+
89
+ def validate_month(self, month: Any) -> str:
90
+ """
91
+ Validate and normalize month input.
92
+
93
+ Args:
94
+ month: Month input (str or other)
95
+
96
+ Returns:
97
+ Valid month string in uppercase
98
+ """
99
+ try:
100
+ if not isinstance(month, str):
101
+ month = str(month)
102
+
103
+ month_upper = month.strip().upper()
104
+ if month_upper not in self.valid_months:
105
+ if st.session_state.get('debug_mode', False):
106
+ logger.warning(f"Invalid month: {month}. Defaulting to 'JUL'")
107
+ return 'JUL'
108
+ return month_upper
109
+
110
+ except Exception as e:
111
+ if st.session_state.get('debug_mode', False):
112
+ logger.error(f"Error validating month {month}: {str(e)}")
113
+ return 'JUL'
114
+
115
+ def validate_hour(self, hour: Any) -> int:
116
+ """
117
+ Validate and normalize hour input.
118
+
119
+ Args:
120
+ hour: Hour input (int, float, or other)
121
+
122
+ Returns:
123
+ Valid hour integer (0-23)
124
+ """
125
+ try:
126
+ hour = int(float(str(hour)))
127
+ if not 0 <= hour <= 23:
128
+ if st.session_state.get('debug_mode', False):
129
+ logger.warning(f"Invalid hour: {hour}. Defaulting to 15")
130
+ return 15
131
+ return hour
132
+ except (ValueError, TypeError):
133
+ if st.session_state.get('debug_mode', False):
134
+ logger.warning(f"Invalid hour format: {hour}. Defaulting to 15")
135
+ return 15
136
+
137
+ def calculate_hourly_cooling_loads(
138
+ self,
139
+ building_components: Dict[str, List[Any]],
140
+ outdoor_conditions: Dict[str, Any],
141
+ indoor_conditions: Dict[str, Any],
142
+ internal_loads: Dict[str, Any],
143
+ building_volume: float
144
+ ) -> Dict[str, Any]:
145
+ """
146
+ Calculate hourly cooling loads for all components.
147
+
148
+ Args:
149
+ building_components: Dictionary of building components
150
+ outdoor_conditions: Outdoor weather conditions
151
+ indoor_conditions: Indoor design conditions
152
+ internal_loads: Internal heat gains
153
+ building_volume: Building volume in cubic meters
154
+
155
+ Returns:
156
+ Dictionary containing hourly cooling loads
157
+ """
158
+ hourly_loads = {
159
+ 'walls': {h: 0.0 for h in range(1, 25)},
160
+ 'roofs': {h: 0.0 for h in range(1, 25)},
161
+ 'windows_conduction': {h: 0.0 for h in range(1, 25)},
162
+ 'windows_solar': {h: 0.0 for h in range(1, 25)},
163
+ 'doors': {h: 0.0 for h in range(1, 25)},
164
+ 'people_sensible': {h: 0.0 for h in range(1, 25)},
165
+ 'people_latent': {h: 0.0 for h in range(1, 25)},
166
+ 'lights': {h: 0.0 for h in range(1, 25)},
167
+ 'equipment_sensible': {h: 0.0 for h in range(1, 25)},
168
+ 'equipment_latent': {h: 0.0 for h in range(1, 25)},
169
+ 'infiltration_sensible': {h: 0.0 for h in range(1, 25)},
170
+ 'infiltration_latent': {h: 0.0 for h in range(1, 25)},
171
+ 'ventilation_sensible': {h: 0.0 for h in range(1, 25)},
172
+ 'ventilation_latent': {h: 0.0 for h in range(1, 25)}
173
+ }
174
+
175
+ try:
176
+ # Validate inputs
177
+ latitude = self.validate_latitude(outdoor_conditions.get('latitude', '24N'))
178
+ month = self.validate_month(outdoor_conditions.get('month', 'JUL'))
179
+ if st.session_state.get('debug_mode', False):
180
+ logger.debug(f"calculate_hourly_cooling_loads: latitude={latitude}, month={month}, outdoor_conditions={outdoor_conditions}")
181
+
182
+ # Calculate loads for walls
183
+ for wall in building_components.get('walls', []):
184
+ for hour in range(24):
185
+ load = self.calculate_wall_cooling_load(
186
+ wall=wall,
187
+ outdoor_temp=outdoor_conditions['temperature'],
188
+ indoor_temp=indoor_conditions['temperature'],
189
+ month=month,
190
+ hour=hour,
191
+ latitude=latitude
192
+ )
193
+ hourly_loads['walls'][hour + 1] += load
194
+
195
+ # Calculate loads for roofs
196
+ for roof in building_components.get('roofs', []):
197
+ for hour in range(24):
198
+ load = self.calculate_roof_cooling_load(
199
+ roof=roof,
200
+ outdoor_temp=outdoor_conditions['temperature'],
201
+ indoor_temp=indoor_conditions['temperature'],
202
+ month=month,
203
+ hour=hour,
204
+ latitude=latitude
205
+ )
206
+ hourly_loads['roofs'][hour + 1] += load
207
+
208
+ # Calculate loads for windows
209
+ for window in building_components.get('windows', []):
210
+ for hour in range(24):
211
+ load_dict = self.calculate_window_cooling_load(
212
+ window=window,
213
+ outdoor_temp=outdoor_conditions['temperature'],
214
+ indoor_temp=indoor_conditions['temperature'],
215
+ month=month,
216
+ hour=hour,
217
+ latitude=latitude,
218
+ shading_coefficient=window.shading_coefficient
219
+ )
220
+ hourly_loads['windows_conduction'][hour + 1] += load_dict['conduction']
221
+ hourly_loads['windows_solar'][hour + 1] += load_dict['solar']
222
+
223
+ # Calculate loads for doors
224
+ for door in building_components.get('doors', []):
225
+ for hour in range(24):
226
+ load = self.calculate_door_cooling_load(
227
+ door=door,
228
+ outdoor_temp=outdoor_conditions['temperature'],
229
+ indoor_temp=indoor_conditions['temperature']
230
+ )
231
+ hourly_loads['doors'][hour + 1] += load
232
+
233
+ # Calculate internal loads
234
+ for hour in range(24):
235
+ # People loads
236
+ people_load = self.calculate_people_cooling_load(
237
+ num_people=internal_loads['people']['number'],
238
+ activity_level=internal_loads['people']['activity_level'],
239
+ hour=hour
240
+ )
241
+ hourly_loads['people_sensible'][hour + 1] += people_load['sensible']
242
+ hourly_loads['people_latent'][hour + 1] += people_load['latent']
243
+
244
+ # Lighting loads
245
+ lights_load = self.calculate_lights_cooling_load(
246
+ power=internal_loads['lights']['power'],
247
+ use_factor=internal_loads['lights']['use_factor'],
248
+ special_allowance=internal_loads['lights']['special_allowance'],
249
+ hour=hour
250
+ )
251
+ hourly_loads['lights'][hour + 1] += lights_load
252
+
253
+ # Equipment loads
254
+ equipment_load = self.calculate_equipment_cooling_load(
255
+ power=internal_loads['equipment']['power'],
256
+ use_factor=internal_loads['equipment']['use_factor'],
257
+ radiation_factor=internal_loads['equipment']['radiation_factor'],
258
+ hour=hour
259
+ )
260
+ hourly_loads['equipment_sensible'][hour + 1] += equipment_load['sensible']
261
+ hourly_loads['equipment_latent'][hour + 1] += equipment_load['latent']
262
+
263
+ # Infiltration loads
264
+ infiltration_load = self.calculate_infiltration_cooling_load(
265
+ flow_rate=internal_loads['infiltration']['flow_rate'],
266
+ building_volume=building_volume,
267
+ outdoor_temp=outdoor_conditions['temperature'],
268
+ outdoor_rh=outdoor_conditions['relative_humidity'],
269
+ indoor_temp=indoor_conditions['temperature'],
270
+ indoor_rh=indoor_conditions['relative_humidity']
271
+ )
272
+ hourly_loads['infiltration_sensible'][hour + 1] += infiltration_load['sensible']
273
+ hourly_loads['infiltration_latent'][hour + 1] += infiltration_load['latent']
274
+
275
+ # Ventilation loads
276
+ ventilation_load = self.calculate_ventilation_cooling_load(
277
+ flow_rate=internal_loads['ventilation']['flow_rate'],
278
+ outdoor_temp=outdoor_conditions['temperature'],
279
+ outdoor_rh=outdoor_conditions['relative_humidity'],
280
+ indoor_temp=indoor_conditions['temperature'],
281
+ indoor_rh=indoor_conditions['relative_humidity']
282
+ )
283
+ hourly_loads['ventilation_sensible'][hour + 1] += ventilation_load['sensible']
284
+ hourly_loads['ventilation_latent'][hour + 1] += ventilation_load['latent']
285
+
286
+ return hourly_loads
287
+
288
+ except Exception as e:
289
+ if st.session_state.get('debug_mode', False):
290
+ logger.error(f"Error in calculate_hourly_cooling_loads: {str(e)}")
291
+ raise Exception(f"Error in calculate_hourly_cooling_loads: {str(e)}")
292
+
293
+ def calculate_design_cooling_load(self, hourly_loads: Dict[str, Any]) -> Dict[str, Any]:
294
+ """
295
+ Calculate design cooling load based on peak hourly loads.
296
+
297
+ Args:
298
+ hourly_loads: Dictionary of hourly cooling loads
299
+
300
+ Returns:
301
+ Dictionary containing design cooling loads
302
+ """
303
+ try:
304
+ design_loads = {}
305
+ total_loads = []
306
+
307
+ for hour in range(1, 25):
308
+ total_load = sum([
309
+ hourly_loads['walls'][hour],
310
+ hourly_loads['roofs'][hour],
311
+ hourly_loads['windows_conduction'][hour],
312
+ hourly_loads['windows_solar'][hour],
313
+ hourly_loads['doors'][hour],
314
+ hourly_loads['people_sensible'][hour],
315
+ hourly_loads['people_latent'][hour],
316
+ hourly_loads['lights'][hour],
317
+ hourly_loads['equipment_sensible'][hour],
318
+ hourly_loads['equipment_latent'][hour],
319
+ hourly_loads['infiltration_sensible'][hour],
320
+ hourly_loads['infiltration_latent'][hour],
321
+ hourly_loads['ventilation_sensible'][hour],
322
+ hourly_loads['ventilation_latent'][hour]
323
+ ])
324
+ total_loads.append(total_load)
325
+
326
+ design_hour = range(1, 25)[np.argmax(total_loads)]
327
+
328
+ design_loads = {
329
+ 'design_hour': design_hour,
330
+ 'walls': hourly_loads['walls'][design_hour],
331
+ 'roofs': hourly_loads['roofs'][design_hour],
332
+ 'windows_conduction': hourly_loads['windows_conduction'][design_hour],
333
+ 'windows_solar': hourly_loads['windows_solar'][design_hour],
334
+ 'doors': hourly_loads['doors'][design_hour],
335
+ 'people_sensible': hourly_loads['people_sensible'][design_hour],
336
+ 'people_latent': hourly_loads['people_latent'][design_hour],
337
+ 'lights': hourly_loads['lights'][design_hour],
338
+ 'equipment_sensible': hourly_loads['equipment_sensible'][design_hour],
339
+ 'equipment_latent': hourly_loads['equipment_latent'][design_hour],
340
+ 'infiltration_sensible': hourly_loads['infiltration_sensible'][design_hour],
341
+ 'infiltration_latent': hourly_loads['infiltration_latent'][design_hour],
342
+ 'ventilation_sensible': hourly_loads['ventilation_sensible'][design_hour],
343
+ 'ventilation_latent': hourly_loads['ventilation_latent'][design_hour]
344
+ }
345
+
346
+ return design_loads
347
+
348
+ except Exception as e:
349
+ if st.session_state.get('debug_mode', False):
350
+ logger.error(f"Error in calculate_design_cooling_load: {str(e)}")
351
+ raise Exception(f"Error in calculate_design_cooling_load: {str(e)}")
352
+
353
+ def calculate_cooling_load_summary(self, design_loads: Dict[str, Any]) -> Dict[str, float]:
354
+ """
355
+ Calculate summary of cooling loads.
356
+
357
+ Args:
358
+ design_loads: Dictionary of design cooling loads
359
+
360
+ Returns:
361
+ Dictionary containing cooling load summary
362
+ """
363
+ try:
364
+ total_sensible = (
365
+ design_loads['walls'] +
366
+ design_loads['roofs'] +
367
+ design_loads['windows_conduction'] +
368
+ design_loads['windows_solar'] +
369
+ design_loads['doors'] +
370
+ design_loads['people_sensible'] +
371
+ design_loads['lights'] +
372
+ design_loads['equipment_sensible'] +
373
+ design_loads['infiltration_sensible'] +
374
+ design_loads['ventilation_sensible']
375
+ )
376
+
377
+ total_latent = (
378
+ design_loads['people_latent'] +
379
+ design_loads['equipment_latent'] +
380
+ design_loads['infiltration_latent'] +
381
+ design_loads['ventilation_latent']
382
+ )
383
+
384
+ total = total_sensible + total_latent
385
+
386
+ return {
387
+ 'total_sensible': total_sensible,
388
+ 'total_latent': total_latent,
389
+ 'total': total
390
+ }
391
+
392
+ except Exception as e:
393
+ if st.session_state.get('debug_mode', False):
394
+ logger.error(f"Error in calculate_cooling_load_summary: {str(e)}")
395
+ raise Exception(f"Error in calculate_cooling_load_summary: {str(e)}")
396
+
397
+ def calculate_wall_cooling_load(
398
+ self,
399
+ wall: Wall,
400
+ outdoor_temp: float,
401
+ indoor_temp: float,
402
+ month: str,
403
+ hour: int,
404
+ latitude: str
405
+ ) -> float:
406
+ """
407
+ Calculate cooling load for a wall.
408
+
409
+ Args:
410
+ wall: Wall component
411
+ outdoor_temp: Outdoor temperature (°C)
412
+ indoor_temp: Indoor temperature (°C)
413
+ month: Design month
414
+ hour: Hour of the day
415
+ latitude: Latitude (e.g., '24N')
416
+
417
+ Returns:
418
+ Cooling load in Watts
419
+ """
420
+ try:
421
+ latitude = self.validate_latitude(latitude)
422
+ month = self.validate_month(month)
423
+ hour = self.validate_hour(hour)
424
+ if st.session_state.get('debug_mode', False):
425
+ logger.debug(f"calculate_wall_cooling_load: latitude={latitude}, month={month}, hour={hour}, wall_group={wall.wall_group}, orientation={wall.orientation.value}")
426
+
427
+ # Validate wall_group
428
+ wall_group = str(wall.wall_group).upper()
429
+ if wall_group not in self.valid_wall_groups:
430
+ numeric_map = {'1': 'A', '2': 'B', '3': 'C', '4': 'D'}
431
+ if wall_group in numeric_map:
432
+ wall_group = numeric_map[wall_group]
433
+ if st.session_state.get('debug_mode', False):
434
+ logger.info(f"Mapped wall_group {wall.wall_group} to {wall_group}")
435
+ else:
436
+ if st.session_state.get('debug_mode', False):
437
+ logger.warning(f"Invalid wall group: {wall_group}. Defaulting to 'A'")
438
+ wall_group = 'A'
439
+
440
+ try:
441
+ cltd = self.ashrae_tables.calculate_corrected_cltd_wall(
442
+ wall_group=wall_group,
443
+ orientation=wall.orientation.value,
444
+ hour=hour,
445
+ color='Dark',
446
+ month=month,
447
+ latitude=latitude,
448
+ indoor_temp=indoor_temp,
449
+ outdoor_temp=outdoor_temp
450
+ )
451
+ except Exception as e:
452
+ if st.session_state.get('debug_mode', False):
453
+ logger.error(f"calculate_corrected_cltd_wall failed for wall_group={wall_group}: {str(e)}")
454
+ logger.warning("Using default CLTD=8.0°C")
455
+ cltd = 8.0
456
+
457
+ load = wall.u_value * wall.area * cltd
458
+ if st.session_state.get('debug_mode', False):
459
+ logger.debug(f"Wall load: u_value={wall.u_value}, area={wall.area}, cltd={cltd}, load={load}")
460
+ return max(load, 0.0)
461
+
462
+ except Exception as e:
463
+ if st.session_state.get('debug_mode', False):
464
+ logger.error(f"Error in calculate_wall_cooling_load: {str(e)}")
465
+ raise Exception(f"Error in calculate_wall_cooling_load: {str(e)}")
466
+
467
+ def calculate_roof_cooling_load(
468
+ self,
469
+ roof: Roof,
470
+ outdoor_temp: float,
471
+ indoor_temp: float,
472
+ month: str,
473
+ hour: int,
474
+ latitude: str
475
+ ) -> float:
476
+ """
477
+ Calculate cooling load for a roof.
478
+
479
+ Args:
480
+ roof: Roof component
481
+ outdoor_temp: Outdoor temperature (°C)
482
+ indoor_temp: Indoor temperature (°C)
483
+ month: Design month
484
+ hour: Hour of the day
485
+ latitude: Latitude (e.g., '24N')
486
+
487
+ Returns:
488
+ Cooling load in Watts
489
+ """
490
+ try:
491
+ latitude = self.validate_latitude(latitude)
492
+ month = self.validate_month(month)
493
+ hour = self.validate_hour(hour)
494
+ if st.session_state.get('debug_mode', False):
495
+ logger.debug(f"calculate_roof_cooling_load: latitude={latitude}, month={month}, hour={hour}, roof_group={roof.roof_group}")
496
+
497
+ roof_group = str(roof.roof_group).upper()
498
+ if roof_group not in self.valid_roof_groups:
499
+ if st.session_state.get('debug_mode', False):
500
+ logger.warning(f"Invalid roof group: {roof_group}. Defaulting to 'A'")
501
+ roof_group = 'A'
502
+
503
+ try:
504
+ cltd = self.ashrae_tables.calculate_corrected_cltd_roof(
505
+ roof_group=roof_group,
506
+ hour=hour,
507
+ color='Dark',
508
+ month=month,
509
+ latitude=latitude,
510
+ indoor_temp=indoor_temp,
511
+ outdoor_temp=outdoor_temp
512
+ )
513
+ except Exception as e:
514
+ if st.session_state.get('debug_mode', False):
515
+ logger.error(f"calculate_corrected_cltd_roof failed for roof_group={roof_group}: {str(e)}")
516
+ logger.warning("Using default CLTD=8.0°C")
517
+ cltd = 8.0
518
+
519
+ load = roof.u_value * roof.area * cltd
520
+ if st.session_state.get('debug_mode', False):
521
+ logger.debug(f"Roof load: u_value={roof.u_value}, area={roof.area}, cltd={cltd}, load={load}")
522
+ return max(load, 0.0)
523
+
524
+ except Exception as e:
525
+ if st.session_state.get('debug_mode', False):
526
+ logger.error(f"Error in calculate_roof_cooling_load: {str(e)}")
527
+ raise Exception(f"Error in calculate_roof_cooling_load: {str(e)}")
528
+
529
+ def calculate_window_cooling_load(
530
+ self,
531
+ window: Window,
532
+ outdoor_temp: float,
533
+ indoor_temp: float,
534
+ month: str,
535
+ hour: int,
536
+ latitude: str,
537
+ shading_coefficient: float
538
+ ) -> Dict[str, float]:
539
+ """
540
+ Calculate cooling load for a window (conduction and solar).
541
+
542
+ Args:
543
+ window: Window component
544
+ outdoor_temp: Outdoor temperature (°C)
545
+ indoor_temp: Indoor temperature (°C)
546
+ month: Design month
547
+ hour: Hour of the day
548
+ latitude: Latitude (e.g., '24N')
549
+ shading_coefficient: Shading coefficient
550
+
551
+ Returns:
552
+ Dictionary with conduction, solar, and total loads in Watts
553
+ """
554
+ try:
555
+ latitude = self.validate_latitude(latitude)
556
+ month = self.validate_month(month)
557
+ hour = self.validate_hour(hour)
558
+ if st.session_state.get('debug_mode', False):
559
+ logger.debug(f"calculate_window_cooling_load: latitude={latitude}, month={month}, hour={hour}, orientation={window.orientation.value}")
560
+
561
+ # Conduction load
562
+ cltd = outdoor_temp - indoor_temp # Simplified for windows
563
+ conduction_load = window.u_value * window.area * cltd
564
+
565
+ # Solar load
566
+ scl_latitude = f"{latitude}_{month}"
567
+ try:
568
+ scl = self.ashrae_tables.get_scl(
569
+ latitude=scl_latitude,
570
+ month=month,
571
+ orientation=window.orientation.value,
572
+ hour=hour
573
+ )
574
+ except Exception as e:
575
+ if st.session_state.get('debug_mode', False):
576
+ logger.error(f"get_scl failed for latitude={scl_latitude}, month={month}, orientation={window.orientation.value}: {str(e)}")
577
+ logger.warning("Using default SCL=100 W/m²")
578
+ scl = 100.0
579
+
580
+ solar_load = window.area * window.shgc * shading_coefficient * scl
581
+
582
+ total_load = conduction_load + solar_load
583
+ if st.session_state.get('debug_mode', False):
584
+ logger.debug(f"Window load: conduction={conduction_load}, solar={solar_load}, total={total_load}")
585
+
586
+ return {
587
+ 'conduction': max(conduction_load, 0.0),
588
+ 'solar': max(solar_load, 0.0),
589
+ 'total': max(total_load, 0.0)
590
+ }
591
+
592
+ except Exception as e:
593
+ if st.session_state.get('debug_mode', False):
594
+ logger.error(f"Error in calculate_window_cooling_load: {str(e)}")
595
+ raise Exception(f"Error in calculate_window_cooling_load: {str(e)}")
596
+
597
+ def calculate_door_cooling_load(
598
+ self,
599
+ door: Door,
600
+ outdoor_temp: float,
601
+ indoor_temp: float
602
+ ) -> float:
603
+ """
604
+ Calculate cooling load for a door.
605
+
606
+ Args:
607
+ door: Door component
608
+ outdoor_temp: Outdoor temperature (°C)
609
+ indoor_temp: Indoor temperature (°C)
610
+
611
+ Returns:
612
+ Cooling load in Watts
613
+ """
614
+ try:
615
+ if st.session_state.get('debug_mode', False):
616
+ logger.debug(f"calculate_door_cooling_load: u_value={door.u_value}, area={door.area}")
617
+
618
+ cltd = outdoor_temp - indoor_temp
619
+ load = door.u_value * door.area * cltd
620
+ if st.session_state.get('debug_mode', False):
621
+ logger.debug(f"Door load: cltd={cltd}, load={load}")
622
+ return max(load, 0.0)
623
+
624
+ except Exception as e:
625
+ if st.session_state.get('debug_mode', False):
626
+ logger.error(f"Error in calculate_door_cooling_load: {str(e)}")
627
+ raise Exception(f"Error in calculate_door_cooling_load: {str(e)}")
628
+
629
+ def calculate_people_cooling_load(
630
+ self,
631
+ num_people: int,
632
+ activity_level: str,
633
+ hour: int
634
+ ) -> Dict[str, float]:
635
+ """
636
+ Calculate cooling load from people.
637
+
638
+ Args:
639
+ num_people: Number of people
640
+ activity_level: Activity level ('Seated/Resting', 'Light Work', etc.)
641
+ hour: Hour of the day
642
+
643
+ Returns:
644
+ Dictionary with sensible and latent loads in Watts
645
+ """
646
+ try:
647
+ hour = self.validate_hour(hour)
648
+ if st.session_state.get('debug_mode', False):
649
+ logger.debug(f"calculate_people_cooling_load
650
+
651
+ System: You are Grok 3 built by xAI.
652
+
653
+ The current date is April 27, 2025.
654
+
655
+ Thank you for providing the `heat_transfer.py` file and the updated `cooling_load.py`. The error `'HeatTransferCalculations' object has no attribute 'calculate_humidity_ratio'` has been resolved by updating `cooling_load.py` to use `self.heat_transfer.psychrometrics.humidity_ratio` instead of `self.heat_transfer.calculate_humidity_ratio`, as the `Psychrometrics` class in `utils.psychrometrics` handles humidity ratio calculations. Below, I’ll provide the complete, corrected `cooling_load.py` to ensure all functionality is preserved and the error is fixed.
656
+
657
+ ### Analysis Recap
658
+ - **Error Cause**: `cooling_load.py` incorrectly called `self.heat_transfer.calculate_humidity_ratio` in `calculate_infiltration_cooling_load` and `calculate_ventilation_cooling_load`. The `HeatTransferCalculations` class (in `heat_transfer.py`) delegates psychrometric calculations to `self.psychrometrics.humidity_ratio`.
659
+ - **Fix**: Replace the incorrect method calls with `self.heat_transfer.psychrometrics.humidity_ratio`.
660
+ - **Additional Checks**:
661
+ - The previous response included a temporary `calculate_humidity_ratio` method in `cooling_load.py`, which is no longer needed and has been removed.
662
+ - All other methods, validations, and logging remain unchanged to maintain compatibility with `main.py`, `ashrae_tables.py`, and `heat_transfer.py`.
663
+ - The artifact ID (`c47f4e86-6a73-4d0d-a7c2-2656f6dc55a2`) is retained as this is an update to the same file.
664
+
665
+ ### Updated `cooling_load.py`
666
+ The updated file corrects the method calls in `calculate_infiltration_cooling_load` and `calculate_ventilation_cooling_load`, removes the unnecessary `calculate_humidity_ratio` method, and ensures all other functionality (e.g., wall, roof, window calculations, debug logging with `st.session_state.debug_mode`) is intact. The version is incremented to 1.0.3 to reflect the fix.
667
+
668
+ <xaiArtifact artifact_id="c47f4e86-6a73-4d0d-a7c2-2656f6dc55a2" artifact_version_id="430e5082-16c0-4456-a8c3-b766fb73f165" title="cooling_load.py" contentType="text/python">
669
+ """
670
+ Cooling load calculation module for HVAC Load Calculator.
671
+ Based on ASHRAE steady-state calculation methods.
672
+
673
+ Author: Dr Majed Abuseif
674
+ Date: April 2025
675
+ Version: 1.0.3
676
  """
677
 
678
  import streamlit as st
 
1490
  # Sensible load: Q = 1.2 * flow_rate * (Tout - Tin)
1491
  sensible_load = 1.2 * flow_rate * (outdoor_temp - indoor_temp)
1492
 
1493
+ # Latent load using psychrometrics
1494
+ outdoor_w = self.heat_transfer.psychrometrics.humidity_ratio(outdoor_temp, outdoor_rh)
1495
+ indoor_w = self.heat_transfer.psychrometrics.humidity_ratio(indoor_temp, indoor_rh)
1496
  latent_load = 1.2 * flow_rate * 2500 * (outdoor_w - indoor_w) # 2500 kJ/kg for latent heat
1497
 
1498
  if st.session_state.get('debug_mode', False):
1499
+ logger.debug(f"Infiltration load: sensible={sensible_load}, latent={latent_load}, ach={ach}, outdoor_w={outdoor_w}, indoor_w={indoor_w}")
1500
 
1501
  return {
1502
  'sensible': max(sensible_load, 0.0),
 
1536
  # Sensible load: Q = 1.2 * flow_rate * (Tout - Tin)
1537
  sensible_load = 1.2 * flow_rate * (outdoor_temp - indoor_temp)
1538
 
1539
+ # Latent load using psychrometrics
1540
+ outdoor_w = self.heat_transfer.psychrometrics.humidity_ratio(outdoor_temp, outdoor_rh)
1541
+ indoor_w = self.heat_transfer.psychrometrics.humidity_ratio(indoor_temp, indoor_rh)
1542
  latent_load = 1.2 * flow_rate * 2500 * (outdoor_w - indoor_w) # 2500 kJ/kg for latent heat
1543
 
1544
  if st.session_state.get('debug_mode', False):
1545
+ logger.debug(f"Ventilation load: sensible={sensible_load}, latent={latent_load}, outdoor_w={outdoor_w}, indoor_w={indoor_w}")
1546
 
1547
  return {
1548
  'sensible': max(sensible_load, 0.0),