mabuseif commited on
Commit
ce95872
·
verified ·
1 Parent(s): a8554b0

Update utils/cooling_load.py

Browse files
Files changed (1) hide show
  1. utils/cooling_load.py +360 -228
utils/cooling_load.py CHANGED
@@ -1,6 +1,7 @@
1
  """
2
  Cooling load calculation module for HVAC Load Calculator.
3
- This module implements the CLTD/CLF method for calculating cooling loads.
 
4
  """
5
 
6
  from typing import Dict, List, Any, Optional, Tuple
@@ -22,39 +23,98 @@ DATA_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
22
 
23
 
24
  class CoolingLoadCalculator:
25
- """Class for calculating cooling loads using the CLTD/CLF method."""
26
 
27
  def __init__(self):
28
- """Initialize cooling load calculator."""
29
  self.heat_transfer = HeatTransferCalculations()
30
  self.psychrometrics = Psychrometrics()
31
  self.ashrae_tables = ASHRAETables()
32
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  def calculate_wall_cooling_load(self, wall: Wall, outdoor_temp: float, indoor_temp: float,
34
  month: str, hour: int, latitude: str = "40N",
35
- color: str = "Dark") -> float:
 
36
  """
37
- Calculate cooling load through a wall using the CLTD method.
38
 
39
  Args:
40
  wall: Wall object
41
  outdoor_temp: Outdoor temperature in °C
42
  indoor_temp: Indoor temperature in °C
43
- month: Month (Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec)
44
  hour: Hour of the day (0-23)
45
  latitude: Latitude (24N, 32N, 40N, 48N, 56N)
46
  color: Surface color (Dark, Medium, Light)
 
 
47
 
48
  Returns:
49
  Cooling load in W
50
  """
 
 
51
  # Get wall properties
52
  u_value = wall.u_value
53
  area = wall.area
54
  orientation = wall.orientation.value
55
  wall_group = wall.wall_group
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
 
57
- # Calculate corrected CLTD
58
  cltd = self.ashrae_tables.calculate_corrected_cltd_wall(
59
  wall_group=wall_group,
60
  orientation=orientation,
@@ -63,36 +123,79 @@ class CoolingLoadCalculator:
63
  month=month,
64
  latitude=latitude,
65
  indoor_temp=indoor_temp,
66
- outdoor_temp=outdoor_temp
67
  )
68
 
 
 
 
 
 
 
69
  # Calculate cooling load
70
- cooling_load = u_value * area * cltd
71
 
72
- return cooling_load
73
 
74
  def calculate_roof_cooling_load(self, roof: Roof, outdoor_temp: float, indoor_temp: float,
75
  month: str, hour: int, latitude: str = "40N",
76
- color: str = "Dark") -> float:
 
77
  """
78
- Calculate cooling load through a roof using the CLTD method.
79
 
80
  Args:
81
  roof: Roof object
82
  outdoor_temp: Outdoor temperature in °C
83
  indoor_temp: Indoor temperature in °C
84
- month: Month (Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec)
85
  hour: Hour of the day (0-23)
86
  latitude: Latitude (24N, 32N, 40N, 48N, 56N)
87
  color: Surface color (Dark, Medium, Light)
 
 
88
 
89
  Returns:
90
  Cooling load in W
91
  """
 
 
92
  # Get roof properties
93
  u_value = roof.u_value
94
  area = roof.area
95
  roof_group = roof.roof_group
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
 
97
  # Calculate corrected CLTD
98
  cltd = self.ashrae_tables.calculate_corrected_cltd_roof(
@@ -102,32 +205,41 @@ class CoolingLoadCalculator:
102
  month=month,
103
  latitude=latitude,
104
  indoor_temp=indoor_temp,
105
- outdoor_temp=outdoor_temp
106
  )
107
 
 
 
 
 
 
 
108
  # Calculate cooling load
109
- cooling_load = u_value * area * cltd
110
 
111
- return cooling_load
112
 
113
  def calculate_window_cooling_load(self, window: Window, outdoor_temp: float, indoor_temp: float,
114
- month: str, hour: int, latitude: str = "40N_JUL",
115
- shading_coefficient: float = 1.0) -> Dict[str, float]:
116
  """
117
- Calculate cooling load through a window using the CLTD/SCL method.
118
 
119
  Args:
120
  window: Window object
121
  outdoor_temp: Outdoor temperature in °C
122
  indoor_temp: Indoor temperature in °C
123
- month: Month (Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec)
124
  hour: Hour of the day (0-23)
125
- latitude: Latitude and month key (default: "40N_JUL")
126
  shading_coefficient: Shading coefficient (0-1)
 
127
 
128
  Returns:
129
  Dictionary with conduction, solar, and total cooling loads in W
130
  """
 
 
131
  # Get window properties
132
  u_value = window.u_value
133
  area = window.area
@@ -138,17 +250,42 @@ class CoolingLoadCalculator:
138
  delta_t = outdoor_temp - indoor_temp
139
  conduction_load = u_value * area * delta_t
140
 
141
- # Calculate solar cooling load
142
- scl = self.ashrae_tables.get_scl(orientation, hour, latitude)
143
- solar_load = area * shgc * shading_coefficient * scl
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
 
145
  # Calculate total cooling load
146
  total_load = conduction_load + solar_load
147
 
148
  return {
149
- "conduction": conduction_load,
150
- "solar": solar_load,
151
- "total": total_load
152
  }
153
 
154
  def calculate_door_cooling_load(self, door: Door, outdoor_temp: float, indoor_temp: float) -> float:
@@ -163,15 +300,14 @@ class CoolingLoadCalculator:
163
  Returns:
164
  Cooling load in W
165
  """
166
- # Get door properties
 
167
  u_value = door.u_value
168
  area = door.area
169
-
170
- # Calculate cooling load
171
  delta_t = outdoor_temp - indoor_temp
172
  cooling_load = u_value * area * delta_t
173
 
174
- return cooling_load
175
 
176
  def calculate_floor_cooling_load(self, floor: Floor, ground_temp: float, indoor_temp: float) -> float:
177
  """
@@ -185,32 +321,51 @@ class CoolingLoadCalculator:
185
  Returns:
186
  Cooling load in W
187
  """
188
- # Get floor properties
 
189
  u_value = floor.u_value
190
  area = floor.area
191
-
192
- # Calculate cooling load
193
  delta_t = ground_temp - indoor_temp
194
  cooling_load = u_value * area * delta_t
195
 
196
- # Return positive value for heat gain, zero for heat loss
197
  return max(0, cooling_load)
198
 
199
- def calculate_infiltration_cooling_load(self, flow_rate: float, outdoor_temp: float, indoor_temp: float,
200
- outdoor_rh: float, indoor_rh: float) -> Dict[str, float]:
 
 
201
  """
202
- Calculate sensible and latent cooling loads due to infiltration.
203
 
204
  Args:
205
- flow_rate: Infiltration flow rate in m³/s
206
  outdoor_temp: Outdoor temperature in °C
207
  indoor_temp: Indoor temperature in °C
208
  outdoor_rh: Outdoor relative humidity in %
209
  indoor_rh: Indoor relative humidity in %
 
 
 
210
 
211
  Returns:
212
  Dictionary with sensible, latent, and total cooling loads in W
213
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
  # Calculate sensible cooling load
215
  sensible_load = self.heat_transfer.infiltration_heat_transfer(
216
  flow_rate=flow_rate,
@@ -227,17 +382,16 @@ class CoolingLoadCalculator:
227
  delta_w=w_outdoor - w_indoor
228
  )
229
 
230
- # Calculate total cooling load
231
  total_load = sensible_load + latent_load
232
 
233
  return {
234
- "sensible": sensible_load,
235
- "latent": latent_load,
236
- "total": total_load
237
  }
238
 
239
  def calculate_ventilation_cooling_load(self, flow_rate: float, outdoor_temp: float, indoor_temp: float,
240
- outdoor_rh: float, indoor_rh: float) -> Dict[str, float]:
241
  """
242
  Calculate sensible and latent cooling loads due to ventilation.
243
 
@@ -251,30 +405,40 @@ class CoolingLoadCalculator:
251
  Returns:
252
  Dictionary with sensible, latent, and total cooling loads in W
253
  """
254
- # Ventilation load calculation is the same as infiltration
255
- return self.calculate_infiltration_cooling_load(
 
256
  flow_rate=flow_rate,
257
- outdoor_temp=outdoor_temp,
258
- indoor_temp=indoor_temp,
259
- outdoor_rh=outdoor_rh,
260
- indoor_rh=indoor_rh
261
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
262
 
263
  def calculate_people_cooling_load(self, num_people: int, activity_level: str,
264
- hours_occupancy: str, hour: int) -> Dict[str, float]:
265
  """
266
- Calculate sensible and latent cooling loads due to people.
267
 
268
  Args:
269
  num_people: Number of people
270
  activity_level: Activity level (Seated/Resting, Light work, Medium work, Heavy work)
271
- hours_occupancy: Hours of occupancy (8h, 10h, 12h, 14h, 16h, 18h, 24h)
272
  hour: Hour of the day (0-23)
 
273
 
274
  Returns:
275
  Dictionary with sensible, latent, and total cooling loads in W
276
  """
277
- # Define heat gains for different activity levels
278
  activity_gains = {
279
  "Seated/Resting": {"sensible": 70, "latent": 45},
280
  "Light work": {"sensible": 75, "latent": 55},
@@ -282,132 +446,127 @@ class CoolingLoadCalculator:
282
  "Heavy work": {"sensible": 95, "latent": 145}
283
  }
284
 
285
- # Get heat gains for the specified activity level
286
  if activity_level not in activity_gains:
287
  raise ValueError(f"Invalid activity level: {activity_level}")
288
 
289
  sensible_gain = activity_gains[activity_level]["sensible"]
290
  latent_gain = activity_gains[activity_level]["latent"]
291
 
292
- # Get CLF for the specified hour and occupancy
293
- clf = self.ashrae_tables.get_clf_people(hour, hours_occupancy)
 
 
294
 
295
- # Calculate cooling loads
296
- sensible_load = num_people * sensible_gain * clf
297
- latent_load = num_people * latent_gain # Latent load is not affected by CLF
298
  total_load = sensible_load + latent_load
299
 
300
  return {
301
- "sensible": sensible_load,
302
- "latent": latent_load,
303
- "total": total_load
304
  }
305
 
306
  def calculate_lights_cooling_load(self, power: float, use_factor: float,
307
- special_allowance: float, hours_operation: str,
308
- hour: int) -> float:
309
  """
310
- Calculate cooling load due to lights.
311
 
312
  Args:
313
  power: Installed lighting power in W
314
  use_factor: Usage factor (0-1)
315
  special_allowance: Special allowance factor for fixtures (0-1)
316
- hours_operation: Hours of operation (8h, 10h, 12h, 14h, 16h, 18h, 24h)
317
  hour: Hour of the day (0-23)
 
318
 
319
  Returns:
320
  Cooling load in W
321
  """
322
- # Get CLF for the specified hour and operation
323
- clf = self.ashrae_tables.get_clf_lights(hour, hours_operation)
 
324
 
325
- # Calculate cooling load
326
- cooling_load = power * use_factor * (1 + special_allowance) * clf
327
 
328
- return cooling_load
329
 
330
  def calculate_equipment_cooling_load(self, power: float, use_factor: float,
331
- radiation_factor: float, hours_operation: str,
332
- hour: int) -> Dict[str, float]:
333
  """
334
- Calculate sensible and latent cooling loads due to equipment.
335
 
336
  Args:
337
  power: Equipment power in W
338
  use_factor: Usage factor (0-1)
339
  radiation_factor: Radiation factor (0-1)
340
- hours_operation: Hours of operation (8h, 10h, 12h, 14h, 16h, 18h, 24h)
341
  hour: Hour of the day (0-23)
 
342
 
343
  Returns:
344
  Dictionary with sensible, latent, and total cooling loads in W
345
  """
346
- # Get CLF for the specified hour and operation
347
- clf = self.ashrae_tables.get_clf_equipment(hour, hours_operation)
348
-
349
- # Calculate sensible cooling load
350
- sensible_load = power * use_factor * radiation_factor * clf
351
-
352
- # Calculate latent cooling load (if any)
353
- latent_load = power * use_factor * (1 - radiation_factor)
354
 
355
- # Calculate total cooling load
 
356
  total_load = sensible_load + latent_load
357
 
358
  return {
359
- "sensible": sensible_load,
360
- "latent": latent_load,
361
- "total": total_load
362
  }
363
 
364
  def calculate_hourly_cooling_loads(self, building_components: Dict[str, List[Any]],
365
  outdoor_conditions: Dict[str, Any],
366
  indoor_conditions: Dict[str, Any],
367
- internal_loads: Dict[str, Any]) -> Dict[int, Dict[str, float]]:
 
368
  """
369
- Calculate hourly cooling loads for a building.
370
 
371
  Args:
372
  building_components: Dictionary with lists of building components
373
  outdoor_conditions: Dictionary with outdoor conditions
374
  indoor_conditions: Dictionary with indoor conditions
375
  internal_loads: Dictionary with internal loads
 
376
 
377
  Returns:
378
  Dictionary with hourly cooling loads
379
  """
380
- # Extract building components
381
  walls = building_components.get("walls", [])
382
  roofs = building_components.get("roofs", [])
383
  floors = building_components.get("floors", [])
384
  windows = building_components.get("windows", [])
385
  doors = building_components.get("doors", [])
386
 
387
- # Extract outdoor conditions
388
  outdoor_temp = outdoor_conditions.get("temperature", 35.0)
389
  outdoor_rh = outdoor_conditions.get("relative_humidity", 50.0)
390
  ground_temp = outdoor_conditions.get("ground_temperature", 20.0)
391
  month = outdoor_conditions.get("month", "Jul")
392
  latitude = outdoor_conditions.get("latitude", "40N")
 
 
393
 
394
- # Extract indoor conditions
395
  indoor_temp = indoor_conditions.get("temperature", 24.0)
396
  indoor_rh = indoor_conditions.get("relative_humidity", 50.0)
397
 
398
- # Extract internal loads
399
  people = internal_loads.get("people", {})
400
  lights = internal_loads.get("lights", {})
401
  equipment = internal_loads.get("equipment", {})
402
  infiltration = internal_loads.get("infiltration", {})
403
  ventilation = internal_loads.get("ventilation", {})
404
 
405
- # Initialize hourly cooling loads
406
  hourly_loads = {}
407
 
408
- # Calculate cooling loads for each hour
409
  for hour in range(24):
410
- # Initialize loads for this hour
411
  loads = {
412
  "walls": 0,
413
  "roofs": 0,
@@ -429,42 +588,43 @@ class CoolingLoadCalculator:
429
  "total": 0
430
  }
431
 
432
- # Calculate wall loads
433
  for wall in walls:
434
- wall_load = self.calculate_wall_cooling_load(
435
  wall=wall,
436
  outdoor_temp=outdoor_temp,
437
  indoor_temp=indoor_temp,
438
  month=month,
439
  hour=hour,
440
  latitude=latitude,
441
- color=wall.color if hasattr(wall, "color") else "Dark"
 
 
442
  )
443
- loads["walls"] += wall_load
444
 
445
- # Calculate roof loads
446
  for roof in roofs:
447
- roof_load = self.calculate_roof_cooling_load(
448
  roof=roof,
449
  outdoor_temp=outdoor_temp,
450
  indoor_temp=indoor_temp,
451
  month=month,
452
  hour=hour,
453
  latitude=latitude,
454
- color=roof.color if hasattr(roof, "color") else "Dark"
 
 
455
  )
456
- loads["roofs"] += roof_load
457
 
458
- # Calculate floor loads
459
  for floor in floors:
460
- floor_load = self.calculate_floor_cooling_load(
461
  floor=floor,
462
  ground_temp=ground_temp,
463
  indoor_temp=indoor_temp
464
  )
465
- loads["floors"] += floor_load
466
 
467
- # Calculate window loads
468
  for window in windows:
469
  window_loads = self.calculate_window_cooling_load(
470
  window=window,
@@ -472,35 +632,37 @@ class CoolingLoadCalculator:
472
  indoor_temp=indoor_temp,
473
  month=month,
474
  hour=hour,
475
- latitude=f"{latitude}_{month.upper()}",
476
- shading_coefficient=window.shading_coefficient if hasattr(window, "shading_coefficient") else 1.0
 
477
  )
478
  loads["windows_conduction"] += window_loads["conduction"]
479
  loads["windows_solar"] += window_loads["solar"]
480
 
481
- # Calculate door loads
482
  for door in doors:
483
- door_load = self.calculate_door_cooling_load(
484
  door=door,
485
  outdoor_temp=outdoor_temp,
486
  indoor_temp=indoor_temp
487
  )
488
- loads["doors"] += door_load
489
 
490
- # Calculate infiltration loads
491
  if infiltration:
492
- flow_rate = infiltration.get("flow_rate", 0.0)
493
  infiltration_loads = self.calculate_infiltration_cooling_load(
494
- flow_rate=flow_rate,
495
  outdoor_temp=outdoor_temp,
496
  indoor_temp=indoor_temp,
497
  outdoor_rh=outdoor_rh,
498
- indoor_rh=indoor_rh
 
 
 
499
  )
500
  loads["infiltration_sensible"] = infiltration_loads["sensible"]
501
  loads["infiltration_latent"] = infiltration_loads["latent"]
502
 
503
- # Calculate ventilation loads
504
  if ventilation:
505
  flow_rate = ventilation.get("flow_rate", 0.0)
506
  ventilation_loads = self.calculate_ventilation_cooling_load(
@@ -513,71 +675,56 @@ class CoolingLoadCalculator:
513
  loads["ventilation_sensible"] = ventilation_loads["sensible"]
514
  loads["ventilation_latent"] = ventilation_loads["latent"]
515
 
516
- # Calculate people loads
517
  if people:
518
- num_people = people.get("number", 0)
519
- activity_level = people.get("activity_level", "Seated/Resting")
520
- hours_occupancy = people.get("hours_occupancy", "8h")
521
-
522
- people_loads = self.calculate_people_cooling_load(
523
- num_people=num_people,
524
- activity_level=activity_level,
525
- hours_occupancy=hours_occupancy,
526
- hour=hour
527
- )
528
- loads["people_sensible"] = people_loads["sensible"]
529
- loads["people_latent"] = people_loads["latent"]
530
 
531
- # Calculate lights loads
532
  if lights:
533
- power = lights.get("power", 0.0)
534
- use_factor = lights.get("use_factor", 1.0)
535
- special_allowance = lights.get("special_allowance", 0.0)
536
- hours_operation = lights.get("hours_operation", "8h")
537
-
538
- lights_load = self.calculate_lights_cooling_load(
539
- power=power,
540
- use_factor=use_factor,
541
- special_allowance=special_allowance,
542
- hours_operation=hours_operation,
543
- hour=hour
544
  )
545
- loads["lights"] = lights_load
546
 
547
- # Calculate equipment loads
548
  if equipment:
549
- power = equipment.get("power", 0.0)
550
- use_factor = equipment.get("use_factor", 1.0)
551
- radiation_factor = equipment.get("radiation_factor", 0.7)
552
- hours_operation = equipment.get("hours_operation", "8h")
553
-
554
  equipment_loads = self.calculate_equipment_cooling_load(
555
- power=power,
556
- use_factor=use_factor,
557
- radiation_factor=radiation_factor,
558
- hours_operation=hours_operation,
559
- hour=hour
560
  )
561
  loads["equipment_sensible"] = equipment_loads["sensible"]
562
  loads["equipment_latent"] = equipment_loads["latent"]
563
 
564
- # Calculate total loads
565
- loads["total_sensible"] = (
566
- loads["walls"] + loads["roofs"] + loads["floors"] +
567
- loads["windows_conduction"] + loads["windows_solar"] +
568
- loads["doors"] + loads["infiltration_sensible"] +
569
- loads["ventilation_sensible"] + loads["people_sensible"] +
570
- loads["lights"] + loads["equipment_sensible"]
571
- )
572
-
573
- loads["total_latent"] = (
574
- loads["infiltration_latent"] + loads["ventilation_latent"] +
575
- loads["people_latent"] + loads["equipment_latent"]
576
- )
577
-
578
  loads["total"] = loads["total_sensible"] + loads["total_latent"]
579
 
580
- # Store loads for this hour
581
  hourly_loads[hour] = loads
582
 
583
  return hourly_loads
@@ -592,15 +739,9 @@ class CoolingLoadCalculator:
592
  Returns:
593
  Dictionary with design cooling loads
594
  """
595
- # Find hour with maximum total load
596
  max_hour = max(hourly_loads.keys(), key=lambda h: hourly_loads[h]["total"])
597
-
598
- # Get loads for the design hour
599
  design_loads = hourly_loads[max_hour].copy()
600
-
601
- # Add design hour information
602
  design_loads["design_hour"] = max_hour
603
-
604
  return design_loads
605
 
606
  def calculate_cooling_load_summary(self, design_loads: Dict[str, float]) -> Dict[str, float]:
@@ -613,28 +754,21 @@ class CoolingLoadCalculator:
613
  Returns:
614
  Dictionary with cooling load summary
615
  """
616
- # Calculate envelope loads
617
- envelope_loads = (
618
- design_loads["walls"] + design_loads["roofs"] + design_loads["floors"] +
619
- design_loads["windows_conduction"] + design_loads["windows_solar"] +
620
  design_loads["doors"]
621
- )
622
-
623
- # Calculate ventilation and infiltration loads
624
  ventilation_loads = design_loads["ventilation_sensible"] + design_loads["ventilation_latent"]
625
  infiltration_loads = design_loads["infiltration_sensible"] + design_loads["infiltration_latent"]
626
-
627
- # Calculate internal loads
628
- internal_loads = (
629
- design_loads["people_sensible"] + design_loads["people_latent"] +
630
- design_loads["lights"] + design_loads["equipment_sensible"] + design_loads["equipment_latent"]
631
- )
632
-
633
- # Calculate sensible heat ratio
634
  shr = design_loads["total_sensible"] / design_loads["total"] if design_loads["total"] > 0 else 1.0
635
 
636
- # Create summary
637
- summary = {
638
  "envelope_loads": envelope_loads,
639
  "ventilation_loads": ventilation_loads,
640
  "infiltration_loads": infiltration_loads,
@@ -645,8 +779,6 @@ class CoolingLoadCalculator:
645
  "sensible_heat_ratio": shr,
646
  "design_hour": design_loads["design_hour"]
647
  }
648
-
649
- return summary
650
 
651
 
652
  # Create a singleton instance
@@ -654,10 +786,9 @@ cooling_load_calculator = CoolingLoadCalculator()
654
 
655
  # Example usage
656
  if __name__ == "__main__":
657
- # Create sample building components
658
  from data.building_components import Wall, Roof, Window, Door, Orientation, ComponentType
659
 
660
- # Create a sample wall
661
  wall = Wall(
662
  id="wall1",
663
  name="Exterior Wall",
@@ -666,10 +797,11 @@ if __name__ == "__main__":
666
  area=20.0,
667
  orientation=Orientation.SOUTH,
668
  wall_type="Brick",
669
- wall_group="B"
 
 
 
670
  )
671
-
672
- # Create a sample roof
673
  roof = Roof(
674
  id="roof1",
675
  name="Flat Roof",
@@ -678,10 +810,11 @@ if __name__ == "__main__":
678
  area=50.0,
679
  orientation=Orientation.HORIZONTAL,
680
  roof_type="Concrete",
681
- roof_group="C"
 
 
 
682
  )
683
-
684
- # Create a sample window
685
  window = Window(
686
  id="window1",
687
  name="South Window",
@@ -694,10 +827,10 @@ if __name__ == "__main__":
694
  window_type="Double Glazed",
695
  glazing_layers=2,
696
  gas_fill="Air",
697
- low_e_coating=False
 
698
  )
699
 
700
- # Define building components
701
  building_components = {
702
  "walls": [wall],
703
  "roofs": [roof],
@@ -712,63 +845,62 @@ if __name__ == "__main__":
712
  "relative_humidity": 50.0,
713
  "ground_temperature": 20.0,
714
  "month": "Jul",
715
- "latitude": "40N"
 
 
716
  }
717
-
718
  indoor_conditions = {
719
  "temperature": 24.0,
720
  "relative_humidity": 50.0
721
  }
722
 
723
- # Define internal loads
 
 
 
 
724
  internal_loads = {
725
  "people": {
726
  "number": 3,
727
  "activity_level": "Seated/Resting",
728
- "hours_occupancy": "8h"
729
  },
730
  "lights": {
731
  "power": 500.0,
732
  "use_factor": 0.9,
733
  "special_allowance": 0.1,
734
- "hours_operation": "8h"
735
  },
736
  "equipment": {
737
  "power": 1000.0,
738
  "use_factor": 0.7,
739
  "radiation_factor": 0.7,
740
- "hours_operation": "8h"
741
  },
742
  "infiltration": {
743
- "flow_rate": 0.05
 
744
  },
745
  "ventilation": {
746
  "flow_rate": 0.1
747
  }
748
  }
749
 
750
- # Calculate hourly cooling loads
751
  hourly_loads = cooling_load_calculator.calculate_hourly_cooling_loads(
752
  building_components=building_components,
753
  outdoor_conditions=outdoor_conditions,
754
  indoor_conditions=indoor_conditions,
755
- internal_loads=internal_loads
 
756
  )
757
-
758
- # Calculate design cooling load
759
  design_loads = cooling_load_calculator.calculate_design_cooling_load(hourly_loads)
760
-
761
- # Calculate cooling load summary
762
  summary = cooling_load_calculator.calculate_cooling_load_summary(design_loads)
763
 
764
  # Print results
765
  print("Cooling Load Summary:")
766
- print(f"Envelope Loads: {summary['envelope_loads']:.2f} W")
767
- print(f"Ventilation Loads: {summary['ventilation_loads']:.2f} W")
768
- print(f"Infiltration Loads: {summary['infiltration_loads']:.2f} W")
769
- print(f"Internal Loads: {summary['internal_loads']:.2f} W")
770
- print(f"Total Sensible: {summary['total_sensible']:.2f} W")
771
- print(f"Total Latent: {summary['total_latent']:.2f} W")
772
- print(f"Total: {summary['total']:.2f} W")
773
- print(f"Sensible Heat Ratio: {summary['sensible_heat_ratio']:.2f}")
774
- print(f"Design Hour: {summary['design_hour']}")
 
1
  """
2
  Cooling load calculation module for HVAC Load Calculator.
3
+ This module implements an enhanced CLTD/CLF method with dynamic heat transfer,
4
+ detailed solar calculations, and pressure-driven infiltration.
5
  """
6
 
7
  from typing import Dict, List, Any, Optional, Tuple
 
23
 
24
 
25
  class CoolingLoadCalculator:
26
+ """Class for calculating cooling loads using an enhanced CLTD/CLF method."""
27
 
28
  def __init__(self):
29
+ """Initialize cooling load calculator with heat transfer and psychrometrics utilities."""
30
  self.heat_transfer = HeatTransferCalculations()
31
  self.psychrometrics = Psychrometrics()
32
  self.ashrae_tables = ASHRAETables()
33
 
34
+ def validate_inputs(self, temp: float, rh: float, area: float, u_value: float) -> None:
35
+ """
36
+ Validate input parameters for calculations.
37
+
38
+ Args:
39
+ temp: Temperature in °C
40
+ rh: Relative humidity in %
41
+ area: Area in m²
42
+ u_value: U-value in W/(m²·K)
43
+
44
+ Raises:
45
+ ValueError: If inputs are out of acceptable ranges
46
+ """
47
+ if not -50 <= temp <= 60:
48
+ raise ValueError(f"Temperature {temp}°C is outside valid range (-50 to 60°C)")
49
+ if not 0 <= rh <= 100:
50
+ raise ValueError(f"Relative humidity {rh}% is outside valid range (0 to 100%)")
51
+ if area < 0:
52
+ raise ValueError(f"Area {area}m² cannot be negative")
53
+ if u_value < 0:
54
+ raise ValueError(f"U-value {u_value} W/(m²·K) cannot be negative")
55
+
56
  def calculate_wall_cooling_load(self, wall: Wall, outdoor_temp: float, indoor_temp: float,
57
  month: str, hour: int, latitude: str = "40N",
58
+ color: str = "Dark", day_of_year: int = 204,
59
+ wind_speed: float = 4.0) -> float:
60
  """
61
+ Calculate cooling load through a wall using enhanced CLTD method with thermal mass.
62
 
63
  Args:
64
  wall: Wall object
65
  outdoor_temp: Outdoor temperature in °C
66
  indoor_temp: Indoor temperature in °C
67
+ month: Month (Jan, Feb, ..., Dec)
68
  hour: Hour of the day (0-23)
69
  latitude: Latitude (24N, 32N, 40N, 48N, 56N)
70
  color: Surface color (Dark, Medium, Light)
71
+ day_of_year: Day of year (1-365, default: 204 for mid-July)
72
+ wind_speed: Wind speed in m/s (default: 4.0 m/s)
73
 
74
  Returns:
75
  Cooling load in W
76
  """
77
+ self.validate_inputs(outdoor_temp, 50.0, wall.area, wall.u_value)
78
+
79
  # Get wall properties
80
  u_value = wall.u_value
81
  area = wall.area
82
  orientation = wall.orientation.value
83
  wall_group = wall.wall_group
84
+ surface_absorptivity = {"Dark": 0.9, "Medium": 0.6, "Light": 0.3}.get(color, 0.9)
85
+
86
+ # Calculate sol-air temperature
87
+ latitude_deg = float(latitude[:-1]) if latitude.endswith("N") else -float(latitude[:-1])
88
+ solar_altitude = self.heat_transfer.solar_altitude(
89
+ latitude=latitude_deg,
90
+ declination=self.heat_transfer.solar_declination(day_of_year),
91
+ hour_angle=self.heat_transfer.solar_hour_angle(hour)
92
+ )
93
+ dni = self.heat_transfer.direct_normal_irradiance(solar_altitude)
94
+ dhi = self.heat_transfer.diffuse_horizontal_irradiance(dni, solar_altitude)
95
+ irradiance = self.heat_transfer.irradiance_on_surface(
96
+ dni, dhi,
97
+ incident_angle=self.heat_transfer.incident_angle(
98
+ surface_tilt=90, # Vertical wall
99
+ surface_azimuth={"NORTH": 0, "EAST": 90, "SOUTH": 180, "WEST": 270}.get(orientation, 180),
100
+ solar_altitude=solar_altitude,
101
+ solar_azimuth=self.heat_transfer.solar_azimuth(
102
+ latitude_deg,
103
+ self.heat_transfer.solar_declination(day_of_year),
104
+ self.heat_transfer.solar_hour_angle(hour),
105
+ solar_altitude
106
+ )
107
+ ),
108
+ surface_tilt=90
109
+ )
110
+ sol_air_temp = self.heat_transfer.sol_air_temperature(
111
+ outdoor_temp=outdoor_temp,
112
+ solar_irradiance=irradiance,
113
+ surface_absorptivity=surface_absorptivity,
114
+ surface_resistance=0.04 # Typical for wind-exposed surface
115
+ )
116
 
117
+ # Calculate corrected CLTD with sol-air temperature
118
  cltd = self.ashrae_tables.calculate_corrected_cltd_wall(
119
  wall_group=wall_group,
120
  orientation=orientation,
 
123
  month=month,
124
  latitude=latitude,
125
  indoor_temp=indoor_temp,
126
+ outdoor_temp=sol_air_temp # Use sol-air temperature
127
  )
128
 
129
+ # Apply thermal mass effect
130
+ thermal_mass = getattr(wall, "thermal_mass", 100000) # J/K, default value
131
+ time_constant = getattr(wall, "time_constant", 2.0) # hours, default
132
+ lag_factor = self.heat_transfer.thermal_lag_factor(thermal_mass, time_constant, 1.0)
133
+ cltd_adjusted = cltd * lag_factor
134
+
135
  # Calculate cooling load
136
+ cooling_load = u_value * area * cltd_adjusted
137
 
138
+ return max(0, cooling_load)
139
 
140
  def calculate_roof_cooling_load(self, roof: Roof, outdoor_temp: float, indoor_temp: float,
141
  month: str, hour: int, latitude: str = "40N",
142
+ color: str = "Dark", day_of_year: int = 204,
143
+ wind_speed: float = 4.0) -> float:
144
  """
145
+ Calculate cooling load through a roof using enhanced CLTD method with thermal mass.
146
 
147
  Args:
148
  roof: Roof object
149
  outdoor_temp: Outdoor temperature in °C
150
  indoor_temp: Indoor temperature in °C
151
+ month: Month (Jan, Feb, ..., Dec)
152
  hour: Hour of the day (0-23)
153
  latitude: Latitude (24N, 32N, 40N, 48N, 56N)
154
  color: Surface color (Dark, Medium, Light)
155
+ day_of_year: Day of year (1-365, default: 204 for mid-July)
156
+ wind_speed: Wind speed in m/s (default: 4.0 m/s)
157
 
158
  Returns:
159
  Cooling load in W
160
  """
161
+ self.validate_inputs(outdoor_temp, 50.0, roof.area, roof.u_value)
162
+
163
  # Get roof properties
164
  u_value = roof.u_value
165
  area = roof.area
166
  roof_group = roof.roof_group
167
+ surface_absorptivity = {"Dark": 0.9, "Medium": 0.6, "Light": 0.3}.get(color, 0.9)
168
+
169
+ # Calculate sol-air temperature
170
+ latitude_deg = float(latitude[:-1]) if latitude.endswith("N") else -float(latitude[:-1])
171
+ solar_altitude = self.heat_transfer.solar_altitude(
172
+ latitude=latitude_deg,
173
+ declination=self.heat_transfer.solar_declination(day_of_year),
174
+ hour_angle=self.heat_transfer.solar_hour_angle(hour)
175
+ )
176
+ dni = self.heat_transfer.direct_normal_irradiance(solar_altitude)
177
+ dhi = self.heat_transfer.diffuse_horizontal_irradiance(dni, solar_altitude)
178
+ irradiance = self.heat_transfer.irradiance_on_surface(
179
+ dni, dhi,
180
+ incident_angle=self.heat_transfer.incident_angle(
181
+ surface_tilt=0, # Horizontal roof
182
+ surface_azimuth=0,
183
+ solar_altitude=solar_altitude,
184
+ solar_azimuth=self.heat_transfer.solar_azimuth(
185
+ latitude_deg,
186
+ self.heat_transfer.solar_declination(day_of_year),
187
+ self.heat_transfer.solar_hour_angle(hour),
188
+ solar_altitude
189
+ )
190
+ ),
191
+ surface_tilt=0
192
+ )
193
+ sol_air_temp = self.heat_transfer.sol_air_temperature(
194
+ outdoor_temp=outdoor_temp,
195
+ solar_irradiance=irradiance,
196
+ surface_absorptivity=surface_absorptivity,
197
+ surface_resistance=0.04
198
+ )
199
 
200
  # Calculate corrected CLTD
201
  cltd = self.ashrae_tables.calculate_corrected_cltd_roof(
 
205
  month=month,
206
  latitude=latitude,
207
  indoor_temp=indoor_temp,
208
+ outdoor_temp=sol_air_temp
209
  )
210
 
211
+ # Apply thermal mass effect
212
+ thermal_mass = getattr(roof, "thermal_mass", 200000) # J/K, default
213
+ time_constant = getattr(roof, "time_constant", 3.0) # hours, default
214
+ lag_factor = self.heat_transfer.thermal_lag_factor(thermal_mass, time_constant, 1.0)
215
+ cltd_adjusted = cltd * lag_factor
216
+
217
  # Calculate cooling load
218
+ cooling_load = u_value * area * cltd_adjusted
219
 
220
+ return max(0, cooling_load)
221
 
222
  def calculate_window_cooling_load(self, window: Window, outdoor_temp: float, indoor_temp: float,
223
+ month: str, hour: int, latitude: str = "40N",
224
+ shading_coefficient: float = 1.0, day_of_year: int = 204) -> Dict[str, float]:
225
  """
226
+ Calculate cooling load through a window using dynamic solar calculations.
227
 
228
  Args:
229
  window: Window object
230
  outdoor_temp: Outdoor temperature in °C
231
  indoor_temp: Indoor temperature in °C
232
+ month: Month (Jan, Feb, ..., Dec)
233
  hour: Hour of the day (0-23)
234
+ latitude: Latitude (24N, 32N, 40N, 48N, 56N)
235
  shading_coefficient: Shading coefficient (0-1)
236
+ day_of_year: Day of year (1-365, default: 204 for mid-July)
237
 
238
  Returns:
239
  Dictionary with conduction, solar, and total cooling loads in W
240
  """
241
+ self.validate_inputs(outdoor_temp, 50.0, window.area, window.u_value)
242
+
243
  # Get window properties
244
  u_value = window.u_value
245
  area = window.area
 
250
  delta_t = outdoor_temp - indoor_temp
251
  conduction_load = u_value * area * delta_t
252
 
253
+ # Calculate solar cooling load dynamically
254
+ latitude_deg = float(latitude[:-1]) if latitude.endswith("N") else -float(latitude[:-1])
255
+ solar_altitude = self.heat_transfer.solar_altitude(
256
+ latitude=latitude_deg,
257
+ declination=self.heat_transfer.solar_declination(day_of_year),
258
+ hour_angle=self.heat_transfer.solar_hour_angle(hour)
259
+ )
260
+ solar_azimuth = self.heat_transfer.solar_azimuth(
261
+ latitude=latitude_deg,
262
+ declination=self.heat_transfer.solar_declination(day_of_year),
263
+ hour_angle=self.heat_transfer.solar_hour_angle(hour),
264
+ altitude=solar_altitude
265
+ )
266
+ incident_angle = self.heat_transfer.incident_angle(
267
+ surface_tilt=90, # Vertical window
268
+ surface_azimuth={"NORTH": 0, "EAST": 90, "SOUTH": 180, "WEST": 270}.get(orientation, 180),
269
+ solar_altitude=solar_altitude,
270
+ solar_azimuth=solar_azimuth
271
+ )
272
+ dni = self.heat_transfer.direct_normal_irradiance(solar_altitude)
273
+ dhi = self.heat_transfer.diffuse_horizontal_irradiance(dni, solar_altitude)
274
+ irradiance = self.heat_transfer.irradiance_on_surface(dni, dhi, incident_angle, surface_tilt=90)
275
+ solar_load = self.heat_transfer.solar_heat_gain(
276
+ irradiance=irradiance,
277
+ area=area,
278
+ shgc=shgc,
279
+ shading_coefficient=shading_coefficient
280
+ )
281
 
282
  # Calculate total cooling load
283
  total_load = conduction_load + solar_load
284
 
285
  return {
286
+ "conduction": max(0, conduction_load),
287
+ "solar": max(0, solar_load),
288
+ "total": max(0, total_load)
289
  }
290
 
291
  def calculate_door_cooling_load(self, door: Door, outdoor_temp: float, indoor_temp: float) -> float:
 
300
  Returns:
301
  Cooling load in W
302
  """
303
+ self.validate_inputs(outdoor_temp, 50.0, door.area, door.u_value)
304
+
305
  u_value = door.u_value
306
  area = door.area
 
 
307
  delta_t = outdoor_temp - indoor_temp
308
  cooling_load = u_value * area * delta_t
309
 
310
+ return max(0, cooling_load)
311
 
312
  def calculate_floor_cooling_load(self, floor: Floor, ground_temp: float, indoor_temp: float) -> float:
313
  """
 
321
  Returns:
322
  Cooling load in W
323
  """
324
+ self.validate_inputs(ground_temp, 50.0, floor.area, floor.u_value)
325
+
326
  u_value = floor.u_value
327
  area = floor.area
 
 
328
  delta_t = ground_temp - indoor_temp
329
  cooling_load = u_value * area * delta_t
330
 
 
331
  return max(0, cooling_load)
332
 
333
+ def calculate_infiltration_cooling_load(self, building_volume: float, outdoor_temp: float,
334
+ indoor_temp: float, outdoor_rh: float, indoor_rh: float,
335
+ wind_speed: float = 4.0, height: float = 3.0,
336
+ crack_length: float = 10.0) -> Dict[str, float]:
337
  """
338
+ Calculate sensible and latent cooling loads due to pressure-driven infiltration.
339
 
340
  Args:
341
+ building_volume: Building volume in m³
342
  outdoor_temp: Outdoor temperature in °C
343
  indoor_temp: Indoor temperature in °C
344
  outdoor_rh: Outdoor relative humidity in %
345
  indoor_rh: Indoor relative humidity in %
346
+ wind_speed: Wind speed in m/s (default: 4.0 m/s)
347
+ height: Building height in m (default: 3.0 m)
348
+ crack_length: Total crack length in m (default: 10.0 m)
349
 
350
  Returns:
351
  Dictionary with sensible, latent, and total cooling loads in W
352
  """
353
+ self.validate_inputs(outdoor_temp, outdoor_rh, building_volume, 0.0)
354
+
355
+ # Calculate infiltration flow rate
356
+ wind_pd = self.heat_transfer.wind_pressure_difference(wind_speed, wind_coefficient=0.4)
357
+ stack_pd = self.heat_transfer.stack_pressure_difference(
358
+ height=height,
359
+ indoor_temp=indoor_temp + 273.15,
360
+ outdoor_temp=outdoor_temp + 273.15
361
+ )
362
+ total_pd = self.heat_transfer.combined_pressure_difference(wind_pd, stack_pd)
363
+ flow_rate = self.heat_transfer.crack_method_infiltration(
364
+ crack_length=crack_length,
365
+ coefficient=0.0001,
366
+ pressure_difference=total_pd
367
+ )
368
+
369
  # Calculate sensible cooling load
370
  sensible_load = self.heat_transfer.infiltration_heat_transfer(
371
  flow_rate=flow_rate,
 
382
  delta_w=w_outdoor - w_indoor
383
  )
384
 
 
385
  total_load = sensible_load + latent_load
386
 
387
  return {
388
+ "sensible": max(0, sensible_load),
389
+ "latent": max(0, latent_load),
390
+ "total": max(0, total_load)
391
  }
392
 
393
  def calculate_ventilation_cooling_load(self, flow_rate: float, outdoor_temp: float, indoor_temp: float,
394
+ outdoor_rh: float, indoor_rh: float) -> Dict[str, float]:
395
  """
396
  Calculate sensible and latent cooling loads due to ventilation.
397
 
 
405
  Returns:
406
  Dictionary with sensible, latent, and total cooling loads in W
407
  """
408
+ self.validate_inputs(outdoor_temp, outdoor_rh, 0.0, 0.0)
409
+
410
+ sensible_load = self.heat_transfer.infiltration_heat_transfer(
411
  flow_rate=flow_rate,
412
+ delta_t=outdoor_temp - indoor_temp
 
 
 
413
  )
414
+ w_outdoor = self.psychrometrics.humidity_ratio(outdoor_temp, outdoor_rh)
415
+ w_indoor = self.psychrometrics.humidity_ratio(indoor_temp, indoor_rh)
416
+ latent_load = self.heat_transfer.infiltration_latent_heat_transfer(
417
+ flow_rate=flow_rate,
418
+ delta_w=w_outdoor - w_indoor
419
+ )
420
+ total_load = sensible_load + latent_load
421
+
422
+ return {
423
+ "sensible": max(0, sensible_load),
424
+ "latent": max(0, latent_load),
425
+ "total": max(0, total_load)
426
+ }
427
 
428
  def calculate_people_cooling_load(self, num_people: int, activity_level: str,
429
+ hour: int, occupancy_schedule: Optional[List[float]] = None) -> Dict[str, float]:
430
  """
431
+ Calculate sensible and latent cooling loads due to people with schedule.
432
 
433
  Args:
434
  num_people: Number of people
435
  activity_level: Activity level (Seated/Resting, Light work, Medium work, Heavy work)
 
436
  hour: Hour of the day (0-23)
437
+ occupancy_schedule: List of 24 hourly occupancy factors (0-1, default: None)
438
 
439
  Returns:
440
  Dictionary with sensible, latent, and total cooling loads in W
441
  """
 
442
  activity_gains = {
443
  "Seated/Resting": {"sensible": 70, "latent": 45},
444
  "Light work": {"sensible": 75, "latent": 55},
 
446
  "Heavy work": {"sensible": 95, "latent": 145}
447
  }
448
 
 
449
  if activity_level not in activity_gains:
450
  raise ValueError(f"Invalid activity level: {activity_level}")
451
 
452
  sensible_gain = activity_gains[activity_level]["sensible"]
453
  latent_gain = activity_gains[activity_level]["latent"]
454
 
455
+ # Apply occupancy schedule
456
+ occupancy_factor = 1.0
457
+ if occupancy_schedule and len(occupancy_schedule) == 24:
458
+ occupancy_factor = occupancy_schedule[hour]
459
 
460
+ sensible_load = num_people * sensible_gain * occupancy_factor
461
+ latent_load = num_people * latent_gain * occupancy_factor
 
462
  total_load = sensible_load + latent_load
463
 
464
  return {
465
+ "sensible": max(0, sensible_load),
466
+ "latent": max(0, latent_load),
467
+ "total": max(0, total_load)
468
  }
469
 
470
  def calculate_lights_cooling_load(self, power: float, use_factor: float,
471
+ special_allowance: float, hour: int,
472
+ lighting_schedule: Optional[List[float]] = None) -> float:
473
  """
474
+ Calculate cooling load due to lights with schedule.
475
 
476
  Args:
477
  power: Installed lighting power in W
478
  use_factor: Usage factor (0-1)
479
  special_allowance: Special allowance factor for fixtures (0-1)
 
480
  hour: Hour of the day (0-23)
481
+ lighting_schedule: List of 24 hourly lighting factors (0-1, default: None)
482
 
483
  Returns:
484
  Cooling load in W
485
  """
486
+ schedule_factor = 1.0
487
+ if lighting_schedule and len(lighting_schedule) == 24:
488
+ schedule_factor = lighting_schedule[hour]
489
 
490
+ cooling_load = power * use_factor * (1 + special_allowance) * schedule_factor
 
491
 
492
+ return max(0, cooling_load)
493
 
494
  def calculate_equipment_cooling_load(self, power: float, use_factor: float,
495
+ radiation_factor: float, hour: int,
496
+ equipment_schedule: Optional[List[float]] = None) -> Dict[str, float]:
497
  """
498
+ Calculate sensible and latent cooling loads due to equipment with schedule.
499
 
500
  Args:
501
  power: Equipment power in W
502
  use_factor: Usage factor (0-1)
503
  radiation_factor: Radiation factor (0-1)
 
504
  hour: Hour of the day (0-23)
505
+ equipment_schedule: List of 24 hourly equipment factors (0-1, default: None)
506
 
507
  Returns:
508
  Dictionary with sensible, latent, and total cooling loads in W
509
  """
510
+ schedule_factor = 1.0
511
+ if equipment_schedule and len(equipment_schedule) == 24:
512
+ schedule_factor = equipment_schedule[hour]
 
 
 
 
 
513
 
514
+ sensible_load = power * use_factor * radiation_factor * schedule_factor
515
+ latent_load = power * use_factor * (1 - radiation_factor) * schedule_factor
516
  total_load = sensible_load + latent_load
517
 
518
  return {
519
+ "sensible": max(0, sensible_load),
520
+ "latent": max(0, latent_load),
521
+ "total": max(0, total_load)
522
  }
523
 
524
  def calculate_hourly_cooling_loads(self, building_components: Dict[str, List[Any]],
525
  outdoor_conditions: Dict[str, Any],
526
  indoor_conditions: Dict[str, Any],
527
+ internal_loads: Dict[str, Any],
528
+ building_volume: float = 300.0) -> Dict[int, Dict[str, float]]:
529
  """
530
+ Calculate hourly cooling loads for a building with enhanced calculations.
531
 
532
  Args:
533
  building_components: Dictionary with lists of building components
534
  outdoor_conditions: Dictionary with outdoor conditions
535
  indoor_conditions: Dictionary with indoor conditions
536
  internal_loads: Dictionary with internal loads
537
+ building_volume: Building volume in m³ (default: 300 m³)
538
 
539
  Returns:
540
  Dictionary with hourly cooling loads
541
  """
542
+ # Extract inputs
543
  walls = building_components.get("walls", [])
544
  roofs = building_components.get("roofs", [])
545
  floors = building_components.get("floors", [])
546
  windows = building_components.get("windows", [])
547
  doors = building_components.get("doors", [])
548
 
 
549
  outdoor_temp = outdoor_conditions.get("temperature", 35.0)
550
  outdoor_rh = outdoor_conditions.get("relative_humidity", 50.0)
551
  ground_temp = outdoor_conditions.get("ground_temperature", 20.0)
552
  month = outdoor_conditions.get("month", "Jul")
553
  latitude = outdoor_conditions.get("latitude", "40N")
554
+ wind_speed = outdoor_conditions.get("wind_speed", 4.0)
555
+ day_of_year = outdoor_conditions.get("day_of_year", 204)
556
 
 
557
  indoor_temp = indoor_conditions.get("temperature", 24.0)
558
  indoor_rh = indoor_conditions.get("relative_humidity", 50.0)
559
 
 
560
  people = internal_loads.get("people", {})
561
  lights = internal_loads.get("lights", {})
562
  equipment = internal_loads.get("equipment", {})
563
  infiltration = internal_loads.get("infiltration", {})
564
  ventilation = internal_loads.get("ventilation", {})
565
 
566
+ # Initialize hourly loads
567
  hourly_loads = {}
568
 
 
569
  for hour in range(24):
 
570
  loads = {
571
  "walls": 0,
572
  "roofs": 0,
 
588
  "total": 0
589
  }
590
 
591
+ # Wall loads
592
  for wall in walls:
593
+ loads["walls"] += self.calculate_wall_cooling_load(
594
  wall=wall,
595
  outdoor_temp=outdoor_temp,
596
  indoor_temp=indoor_temp,
597
  month=month,
598
  hour=hour,
599
  latitude=latitude,
600
+ color=wall.color if hasattr(wall, "color") else "Dark",
601
+ day_of_year=day_of_year,
602
+ wind_speed=wind_speed
603
  )
 
604
 
605
+ # Roof loads
606
  for roof in roofs:
607
+ loads["roofs"] += self.calculate_roof_cooling_load(
608
  roof=roof,
609
  outdoor_temp=outdoor_temp,
610
  indoor_temp=indoor_temp,
611
  month=month,
612
  hour=hour,
613
  latitude=latitude,
614
+ color=roof.color if hasattr(roof, "color") else "Dark",
615
+ day_of_year=day_of_year,
616
+ wind_speed=wind_speed
617
  )
 
618
 
619
+ # Floor loads
620
  for floor in floors:
621
+ loads["floors"] += self.calculate_floor_cooling_load(
622
  floor=floor,
623
  ground_temp=ground_temp,
624
  indoor_temp=indoor_temp
625
  )
 
626
 
627
+ # Window loads
628
  for window in windows:
629
  window_loads = self.calculate_window_cooling_load(
630
  window=window,
 
632
  indoor_temp=indoor_temp,
633
  month=month,
634
  hour=hour,
635
+ latitude=latitude,
636
+ shading_coefficient=window.shading_coefficient if hasattr(window, "shading_coefficient") else 1.0,
637
+ day_of_year=day_of_year
638
  )
639
  loads["windows_conduction"] += window_loads["conduction"]
640
  loads["windows_solar"] += window_loads["solar"]
641
 
642
+ # Door loads
643
  for door in doors:
644
+ loads["doors"] += self.calculate_door_cooling_load(
645
  door=door,
646
  outdoor_temp=outdoor_temp,
647
  indoor_temp=indoor_temp
648
  )
 
649
 
650
+ # Infiltration loads
651
  if infiltration:
 
652
  infiltration_loads = self.calculate_infiltration_cooling_load(
653
+ building_volume=building_volume,
654
  outdoor_temp=outdoor_temp,
655
  indoor_temp=indoor_temp,
656
  outdoor_rh=outdoor_rh,
657
+ indoor_rh=indoor_rh,
658
+ wind_speed=wind_speed,
659
+ height=infiltration.get("height", 3.0),
660
+ crack_length=infiltration.get("crack_length", 10.0)
661
  )
662
  loads["infiltration_sensible"] = infiltration_loads["sensible"]
663
  loads["infiltration_latent"] = infiltration_loads["latent"]
664
 
665
+ # Ventilation loads
666
  if ventilation:
667
  flow_rate = ventilation.get("flow_rate", 0.0)
668
  ventilation_loads = self.calculate_ventilation_cooling_load(
 
675
  loads["ventilation_sensible"] = ventilation_loads["sensible"]
676
  loads["ventilation_latent"] = ventilation_loads["latent"]
677
 
678
+ # People loads
679
  if people:
680
+ loads["people_sensible"], loads["people_latent"] = self.calculate_people_cooling_load(
681
+ num_people=people.get("number", 0),
682
+ activity_level=people.get("activity_level", "Seated/Resting"),
683
+ hour=hour,
684
+ occupancy_schedule=people.get("occupancy_schedule", None)
685
+ )["sensible"], self.calculate_people_cooling_load(
686
+ num_people=people.get("number", 0),
687
+ activity_level=people.get("activity_level", "Seated/Resting"),
688
+ hour=hour,
689
+ occupancy_schedule=people.get("occupancy_schedule", None)
690
+ )["latent"]
 
691
 
692
+ # Lights loads
693
  if lights:
694
+ loads["lights"] = self.calculate_lights_cooling_load(
695
+ power=lights.get("power", 0.0),
696
+ use_factor=lights.get("use_factor", 1.0),
697
+ special_allowance=lights.get("special_allowance", 0.0),
698
+ hour=hour,
699
+ lighting_schedule=lights.get("lighting_schedule", None)
 
 
 
 
 
700
  )
 
701
 
702
+ # Equipment loads
703
  if equipment:
 
 
 
 
 
704
  equipment_loads = self.calculate_equipment_cooling_load(
705
+ power=equipment.get("power", 0.0),
706
+ use_factor=equipment.get("use_factor", 1.0),
707
+ radiation_factor=equipment.get("radiation_factor", 0.7),
708
+ hour=hour,
709
+ equipment_schedule=equipment.get("equipment_schedule", None)
710
  )
711
  loads["equipment_sensible"] = equipment_loads["sensible"]
712
  loads["equipment_latent"] = equipment_loads["latent"]
713
 
714
+ # Calculate totals
715
+ loads["total_sensible"] = sum([
716
+ loads["walls"], loads["roofs"], loads["floors"],
717
+ loads["windows_conduction"], loads["windows_solar"],
718
+ loads["doors"], loads["infiltration_sensible"],
719
+ loads["ventilation_sensible"], loads["people_sensible"],
720
+ loads["lights"], loads["equipment_sensible"]
721
+ ])
722
+ loads["total_latent"] = sum([
723
+ loads["infiltration_latent"], loads["ventilation_latent"],
724
+ loads["people_latent"], loads["equipment_latent"]
725
+ ])
 
 
726
  loads["total"] = loads["total_sensible"] + loads["total_latent"]
727
 
 
728
  hourly_loads[hour] = loads
729
 
730
  return hourly_loads
 
739
  Returns:
740
  Dictionary with design cooling loads
741
  """
 
742
  max_hour = max(hourly_loads.keys(), key=lambda h: hourly_loads[h]["total"])
 
 
743
  design_loads = hourly_loads[max_hour].copy()
 
 
744
  design_loads["design_hour"] = max_hour
 
745
  return design_loads
746
 
747
  def calculate_cooling_load_summary(self, design_loads: Dict[str, float]) -> Dict[str, float]:
 
754
  Returns:
755
  Dictionary with cooling load summary
756
  """
757
+ envelope_loads = sum([
758
+ design_loads["walls"], design_loads["roofs"], design_loads["floors"],
759
+ design_loads["windows_conduction"], design_loads["windows_solar"],
 
760
  design_loads["doors"]
761
+ ])
 
 
762
  ventilation_loads = design_loads["ventilation_sensible"] + design_loads["ventilation_latent"]
763
  infiltration_loads = design_loads["infiltration_sensible"] + design_loads["infiltration_latent"]
764
+ internal_loads = sum([
765
+ design_loads["people_sensible"], design_loads["people_latent"],
766
+ design_loads["lights"], design_loads["equipment_sensible"],
767
+ design_loads["equipment_latent"]
768
+ ])
 
 
 
769
  shr = design_loads["total_sensible"] / design_loads["total"] if design_loads["total"] > 0 else 1.0
770
 
771
+ return {
 
772
  "envelope_loads": envelope_loads,
773
  "ventilation_loads": ventilation_loads,
774
  "infiltration_loads": infiltration_loads,
 
779
  "sensible_heat_ratio": shr,
780
  "design_hour": design_loads["design_hour"]
781
  }
 
 
782
 
783
 
784
  # Create a singleton instance
 
786
 
787
  # Example usage
788
  if __name__ == "__main__":
 
789
  from data.building_components import Wall, Roof, Window, Door, Orientation, ComponentType
790
 
791
+ # Sample building components
792
  wall = Wall(
793
  id="wall1",
794
  name="Exterior Wall",
 
797
  area=20.0,
798
  orientation=Orientation.SOUTH,
799
  wall_type="Brick",
800
+ wall_group="B",
801
+ thermal_mass=100000,
802
+ time_constant=2.0,
803
+ color="Dark"
804
  )
 
 
805
  roof = Roof(
806
  id="roof1",
807
  name="Flat Roof",
 
810
  area=50.0,
811
  orientation=Orientation.HORIZONTAL,
812
  roof_type="Concrete",
813
+ roof_group="C",
814
+ thermal_mass=200000,
815
+ time_constant=3.0,
816
+ color="Dark"
817
  )
 
 
818
  window = Window(
819
  id="window1",
820
  name="South Window",
 
827
  window_type="Double Glazed",
828
  glazing_layers=2,
829
  gas_fill="Air",
830
+ low_e_coating=False,
831
+ shading_coefficient=1.0
832
  )
833
 
 
834
  building_components = {
835
  "walls": [wall],
836
  "roofs": [roof],
 
845
  "relative_humidity": 50.0,
846
  "ground_temperature": 20.0,
847
  "month": "Jul",
848
+ "latitude": "40N",
849
+ "wind_speed": 4.0,
850
+ "day_of_year": 204
851
  }
 
852
  indoor_conditions = {
853
  "temperature": 24.0,
854
  "relative_humidity": 50.0
855
  }
856
 
857
+ # Define internal loads with schedules
858
+ occupancy_schedule = [0.0] * 8 + [1.0] * 8 + [0.5] * 8 # 8h off, 8h full, 8h half
859
+ lighting_schedule = [0.1] * 8 + [0.9] * 8 + [0.5] * 8
860
+ equipment_schedule = [0.2] * 8 + [0.8] * 8 + [0.4] * 8
861
+
862
  internal_loads = {
863
  "people": {
864
  "number": 3,
865
  "activity_level": "Seated/Resting",
866
+ "occupancy_schedule": occupancy_schedule
867
  },
868
  "lights": {
869
  "power": 500.0,
870
  "use_factor": 0.9,
871
  "special_allowance": 0.1,
872
+ "lighting_schedule": lighting_schedule
873
  },
874
  "equipment": {
875
  "power": 1000.0,
876
  "use_factor": 0.7,
877
  "radiation_factor": 0.7,
878
+ "equipment_schedule": equipment_schedule
879
  },
880
  "infiltration": {
881
+ "height": 3.0,
882
+ "crack_length": 10.0
883
  },
884
  "ventilation": {
885
  "flow_rate": 0.1
886
  }
887
  }
888
 
889
+ # Calculate loads
890
  hourly_loads = cooling_load_calculator.calculate_hourly_cooling_loads(
891
  building_components=building_components,
892
  outdoor_conditions=outdoor_conditions,
893
  indoor_conditions=indoor_conditions,
894
+ internal_loads=internal_loads,
895
+ building_volume=300.0
896
  )
 
 
897
  design_loads = cooling_load_calculator.calculate_design_cooling_load(hourly_loads)
 
 
898
  summary = cooling_load_calculator.calculate_cooling_load_summary(design_loads)
899
 
900
  # Print results
901
  print("Cooling Load Summary:")
902
+ for key, value in summary.items():
903
+ if isinstance(value, float):
904
+ print(f"{key.replace('_', ' ').title()}: {value:.2f} W")
905
+ else:
906
+ print(f"{key.replace('_', ' ').title()}: {value}")