mabuseif commited on
Commit
02d1d87
·
verified ·
1 Parent(s): 5fb27c0

Update utils/cooling_load.py

Browse files
Files changed (1) hide show
  1. utils/cooling_load.py +119 -30
utils/cooling_load.py CHANGED
@@ -29,24 +29,76 @@ class CoolingLoadCalculator:
29
  self.hours = list(range(24))
30
  self.valid_latitudes = ['24N', '36N', '48N']
31
  self.valid_months = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC']
 
 
32
 
33
- def validate_latitude(self, latitude: str) -> str:
34
- """Validate and return a valid latitude."""
35
- if not isinstance(latitude, str) or '_' in latitude:
36
- logger.warning(f"Invalid latitude format: {latitude}. Defaulting to '24N'")
37
- return '24N'
38
- if latitude not in self.valid_latitudes:
39
- logger.warning(f"Latitude {latitude} not in {self.valid_latitudes}. Defaulting to '24N'")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  return '24N'
41
- return latitude
42
 
43
- def validate_month(self, month: str) -> str:
44
- """Validate and return a valid month in uppercase."""
45
- month_upper = month.upper()
46
- if month_upper not in self.valid_months:
47
- logger.warning(f"Invalid month: {month}. Defaulting to 'JUL'")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  return 'JUL'
49
- return month_upper
50
 
51
  def calculate_hourly_cooling_loads(
52
  self,
@@ -327,10 +379,16 @@ class CoolingLoadCalculator:
327
  try:
328
  latitude = self.validate_latitude(latitude)
329
  month = self.validate_month(month)
330
- logger.debug(f"calculate_wall_cooling_load: latitude={latitude}, month={month}")
 
 
 
 
 
 
331
 
332
  cltd = self.ashrae_tables.calculate_corrected_cltd_wall(
333
- wall_group=wall.wall_group,
334
  orientation=wall.orientation.value,
335
  hour=hour,
336
  color='Dark',
@@ -341,9 +399,11 @@ class CoolingLoadCalculator:
341
  )
342
 
343
  load = wall.u_value * wall.area * cltd
 
344
  return max(load, 0.0)
345
 
346
  except Exception as e:
 
347
  raise Exception(f"Error in calculate_wall_cooling_load: {str(e)}")
348
 
349
  def calculate_roof_cooling_load(
@@ -370,19 +430,15 @@ class CoolingLoadCalculator:
370
  Cooling load in Watts
371
  """
372
  try:
373
- # Validate inputs
374
  latitude = self.validate_latitude(latitude)
375
  month = self.validate_month(month)
376
- logger.debug(f"calculate_roof_cooling_load: latitude={latitude}, month={month}")
377
 
378
  # Validate and map roof_group
379
- valid_roof_groups = ['A', 'B', 'C', 'D', 'E', 'F', 'G']
380
- roof_group = roof.roof_group
381
- if roof_group not in valid_roof_groups:
382
- if roof_group == '1': # Map invalid '1' to 'A'
383
- roof_group = 'A'
384
- else:
385
- raise ValueError(f"Invalid roof group: {roof_group}. Must be one of {valid_roof_groups}")
386
 
387
  cltd = self.ashrae_tables.calculate_corrected_cltd_roof(
388
  roof_group=roof_group,
@@ -395,9 +451,11 @@ class CoolingLoadCalculator:
395
  )
396
 
397
  load = roof.u_value * roof.area * cltd
 
398
  return max(load, 0.0)
399
 
400
  except Exception as e:
 
401
  raise Exception(f"Error in calculate_roof_cooling_load: {str(e)}")
402
 
403
  def calculate_window_cooling_load(
@@ -426,10 +484,9 @@ class CoolingLoadCalculator:
426
  Dictionary with conduction and solar loads in Watts
427
  """
428
  try:
429
- # Validate inputs
430
  scl_latitude = self.validate_latitude(latitude)
431
  month_upper = self.validate_month(month)
432
- logger.debug(f"calculate_window_cooling_load: latitude={scl_latitude}, month={month_upper}")
433
 
434
  # Conduction load
435
  delta_t = outdoor_temp - indoor_temp
@@ -474,6 +531,7 @@ class CoolingLoadCalculator:
474
 
475
  solar_load = window.area * window.shgc * scl * shading_coefficient
476
 
 
477
  return {
478
  'conduction': max(conduction_load, 0.0),
479
  'solar': max(solar_load, 0.0),
@@ -481,6 +539,7 @@ class CoolingLoadCalculator:
481
  }
482
 
483
  except Exception as e:
 
484
  raise Exception(f"Error in calculate_window_cooling_load: {str(e)}")
485
 
486
  def calculate_door_cooling_load(
@@ -503,9 +562,11 @@ class CoolingLoadCalculator:
503
  try:
504
  delta_t = outdoor_temp - indoor_temp
505
  load = door.u_value * door.area * delta_t
 
506
  return max(load, 0.0)
507
 
508
  except Exception as e:
 
509
  raise Exception(f"Error in calculate_door_cooling_load: {str(e)}")
510
 
511
  def calculate_people_cooling_load(
@@ -526,19 +587,24 @@ class CoolingLoadCalculator:
526
  Dictionary with sensible and latent loads in Watts
527
  """
528
  try:
 
 
 
 
 
529
  sensible_gain = {
530
  'Seated/Resting': 70,
531
  'Light Work': 100,
532
  'Moderate Work': 150,
533
  'Heavy Work': 200
534
- }.get(activity_level, 70)
535
 
536
  latent_gain = {
537
  'Seated/Resting': 45,
538
  'Light Work': 75,
539
  'Moderate Work': 120,
540
  'Heavy Work': 180
541
- }.get(activity_level, 45)
542
 
543
  logger.debug(f"Calling get_clf_people with zone_type='A', hours_occupied='6h', hour={hour}")
544
  try:
@@ -551,6 +617,7 @@ class CoolingLoadCalculator:
551
  sensible_load = num_people * sensible_gain * clf
552
  latent_load = num_people * latent_gain
553
 
 
554
  return {
555
  'sensible': max(sensible_load, 0.0),
556
  'latent': max(latent_load, 0.0),
@@ -558,6 +625,7 @@ class CoolingLoadCalculator:
558
  }
559
 
560
  except Exception as e:
 
561
  raise Exception(f"Error in calculate_people_cooling_load: {str(e)}")
562
 
563
  def calculate_lights_cooling_load(
@@ -588,9 +656,11 @@ class CoolingLoadCalculator:
588
  logger.warning("Using default CLF=1.0 for lights")
589
  clf = 1.0
590
  load = power * use_factor * special_allowance * clf
 
591
  return max(load, 0.0)
592
 
593
  except Exception as e:
 
594
  raise Exception(f"Error in calculate_lights_cooling_load: {str(e)}")
595
 
596
  def calculate_equipment_cooling_load(
@@ -623,6 +693,7 @@ class CoolingLoadCalculator:
623
  sensible_load = power * use_factor * radiation_factor * clf
624
  latent_load = power * use_factor * (1 - radiation_factor)
625
 
 
626
  return {
627
  'sensible': max(sensible_load, 0.0),
628
  'latent': max(latent_load, 0.0),
@@ -630,6 +701,7 @@ class CoolingLoadCalculator:
630
  }
631
 
632
  except Exception as e:
 
633
  raise Exception(f"Error in calculate_equipment_cooling_load: {str(e)}")
634
 
635
  def calculate_infiltration_cooling_load(
@@ -656,12 +728,17 @@ class CoolingLoadCalculator:
656
  Dictionary with sensible and latent loads in Watts
657
  """
658
  try:
 
 
 
 
659
  air_changes_per_hour = (flow_rate * 3600) / building_volume
660
  sensible_load = 1.2 * flow_rate * 1000 * (outdoor_temp - indoor_temp)
661
 
662
  # Calculate humidity ratio difference
663
  logger.debug(f"Calculating outdoor humidity ratio: temp={outdoor_temp}°C, rh={outdoor_rh}%")
664
  try:
 
665
  outdoor_w = self.heat_transfer.psychrometrics.humidity_ratio(outdoor_temp, outdoor_rh)
666
  except Exception as e:
667
  logger.error(f"Failed to calculate outdoor humidity ratio: {str(e)}")
@@ -670,6 +747,7 @@ class CoolingLoadCalculator:
670
 
671
  logger.debug(f"Calculating indoor humidity ratio: temp={indoor_temp}°C, rh={indoor_rh}%")
672
  try:
 
673
  indoor_w = self.heat_transfer.psychrometrics.humidity_ratio(indoor_temp, indoor_rh)
674
  except Exception as e:
675
  logger.error(f"Failed to calculate indoor humidity ratio: {str(e)}")
@@ -678,6 +756,7 @@ class CoolingLoadCalculator:
678
 
679
  latent_load = 2501 * flow_rate * 1000 * (outdoor_w - indoor_w)
680
 
 
681
  return {
682
  'sensible': max(sensible_load, 0.0),
683
  'latent': max(latent_load, 0.0),
@@ -685,6 +764,7 @@ class CoolingLoadCalculator:
685
  }
686
 
687
  except Exception as e:
 
688
  raise Exception(f"Error in calculate_infiltration_cooling_load: {str(e)}")
689
 
690
  def calculate_ventilation_cooling_load(
@@ -709,11 +789,16 @@ class CoolingLoadCalculator:
709
  Dictionary with sensible and latent loads in Watts
710
  """
711
  try:
 
 
 
 
712
  sensible_load = 1.2 * flow_rate * 1000 * (outdoor_temp - indoor_temp)
713
 
714
  # Calculate humidity ratio difference
715
  logger.debug(f"Calculating outdoor humidity ratio: temp={outdoor_temp}°C, rh={outdoor_rh}%")
716
  try:
 
717
  outdoor_w = self.heat_transfer.psychrometrics.humidity_ratio(outdoor_temp, outdoor_rh)
718
  except Exception as e:
719
  logger.error(f"Failed to calculate outdoor humidity ratio: {str(e)}")
@@ -722,6 +807,7 @@ class CoolingLoadCalculator:
722
 
723
  logger.debug(f"Calculating indoor humidity ratio: temp={indoor_temp}°C, rh={indoor_rh}%")
724
  try:
 
725
  indoor_w = self.heat_transfer.psychrometrics.humidity_ratio(indoor_temp, indoor_rh)
726
  except Exception as e:
727
  logger.error(f"Failed to calculate indoor humidity ratio: {str(e)}")
@@ -730,6 +816,7 @@ class CoolingLoadCalculator:
730
 
731
  latent_load = 2501 * flow_rate * 1000 * (outdoor_w - indoor_w)
732
 
 
733
  return {
734
  'sensible': max(sensible_load, 0.0),
735
  'latent': max(latent_load, 0.0),
@@ -737,6 +824,7 @@ class CoolingLoadCalculator:
737
  }
738
 
739
  except Exception as e:
 
740
  raise Exception(f"Error in calculate_ventilation_cooling_load: {str(e)}")
741
 
742
 
@@ -751,7 +839,7 @@ if __name__ == "__main__":
751
  orientation=Orientation.NORTH,
752
  area=20.0,
753
  u_value=0.5,
754
- wall_group="A"
755
  )],
756
  'roofs': [Roof(
757
  name="Main Roof",
@@ -806,6 +894,7 @@ if __name__ == "__main__":
806
  {'latitude': '24N', 'month': 'Jul'},
807
  {'latitude': '36N', 'month': 'Jul'},
808
  {'latitude': '48N', 'month': 'Jan'},
 
809
  {'latitude': 'invalid', 'month': 'invalid'} # Test invalid inputs
810
  ]
811
 
 
29
  self.hours = list(range(24))
30
  self.valid_latitudes = ['24N', '36N', '48N']
31
  self.valid_months = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC']
32
+ self.valid_wall_groups = [str(i) for i in range(1, 41)] # ASHRAE wall groups 1-40
33
+ self.valid_roof_groups = ['A', 'B', 'C', 'D', 'E', 'F', 'G']
34
 
35
+ def validate_latitude(self, latitude: Any) -> str:
36
+ """
37
+ Validate and normalize latitude input.
38
+
39
+ Args:
40
+ latitude: Latitude input (str, float, or other)
41
+
42
+ Returns:
43
+ Valid latitude string ('24N', '36N', or '48N')
44
+ """
45
+ try:
46
+ if not isinstance(latitude, str):
47
+ latitude = str(latitude)
48
+
49
+ # Remove whitespace and convert to uppercase
50
+ latitude = latitude.strip().upper()
51
+
52
+ # Handle formats like '31.973N', '1_31.973N'
53
+ if '_' in latitude or '.' in latitude:
54
+ # Extract numeric part
55
+ num_part = ''.join(c for c in latitude if c.isdigit() or c == '.')
56
+ try:
57
+ lat_val = float(num_part)
58
+ # Map to closest valid latitude
59
+ if lat_val <= 30:
60
+ return '24N'
61
+ elif lat_val <= 42:
62
+ return '36N'
63
+ else:
64
+ return '48N'
65
+ except ValueError:
66
+ logger.warning(f"Cannot parse latitude: {latitude}. Defaulting to '24N'")
67
+ return '24N'
68
+
69
+ if latitude not in self.valid_latitudes:
70
+ logger.warning(f"Invalid latitude: {latitude}. Defaulting to '24N'")
71
+ return '24N'
72
+
73
+ return latitude
74
+
75
+ except Exception as e:
76
+ logger.error(f"Error validating latitude {latitude}: {str(e)}")
77
  return '24N'
 
78
 
79
+ def validate_month(self, month: Any) -> str:
80
+ """
81
+ Validate and normalize month input.
82
+
83
+ Args:
84
+ month: Month input (str or other)
85
+
86
+ Returns:
87
+ Valid month string in uppercase
88
+ """
89
+ try:
90
+ if not isinstance(month, str):
91
+ month = str(month)
92
+
93
+ month_upper = month.strip().upper()
94
+ if month_upper not in self.valid_months:
95
+ logger.warning(f"Invalid month: {month}. Defaulting to 'JUL'")
96
+ return 'JUL'
97
+ return month_upper
98
+
99
+ except Exception as e:
100
+ logger.error(f"Error validating month {month}: {str(e)}")
101
  return 'JUL'
 
102
 
103
  def calculate_hourly_cooling_loads(
104
  self,
 
379
  try:
380
  latitude = self.validate_latitude(latitude)
381
  month = self.validate_month(month)
382
+ logger.debug(f"calculate_wall_cooling_load: latitude={latitude}, month={month}, wall_group={wall.wall_group}, orientation={wall.orientation.value}")
383
+
384
+ # Validate wall_group
385
+ wall_group = str(wall.wall_group)
386
+ if wall_group not in self.valid_wall_groups:
387
+ logger.warning(f"Invalid wall group: {wall_group}. Defaulting to '1'")
388
+ wall_group = '1'
389
 
390
  cltd = self.ashrae_tables.calculate_corrected_cltd_wall(
391
+ wall_group=wall_group,
392
  orientation=wall.orientation.value,
393
  hour=hour,
394
  color='Dark',
 
399
  )
400
 
401
  load = wall.u_value * wall.area * cltd
402
+ logger.debug(f"Wall load: u_value={wall.u_value}, area={wall.area}, cltd={cltd}, load={load}")
403
  return max(load, 0.0)
404
 
405
  except Exception as e:
406
+ logger.error(f"Error in calculate_wall_cooling_load: {str(e)}")
407
  raise Exception(f"Error in calculate_wall_cooling_load: {str(e)}")
408
 
409
  def calculate_roof_cooling_load(
 
430
  Cooling load in Watts
431
  """
432
  try:
 
433
  latitude = self.validate_latitude(latitude)
434
  month = self.validate_month(month)
435
+ logger.debug(f"calculate_roof_cooling_load: latitude={latitude}, month={month}, roof_group={roof.roof_group}")
436
 
437
  # Validate and map roof_group
438
+ roof_group = str(roof.roof_group).upper()
439
+ if roof_group not in self.valid_roof_groups:
440
+ logger.warning(f"Invalid roof group: {roof_group}. Defaulting to 'A'")
441
+ roof_group = 'A'
 
 
 
442
 
443
  cltd = self.ashrae_tables.calculate_corrected_cltd_roof(
444
  roof_group=roof_group,
 
451
  )
452
 
453
  load = roof.u_value * roof.area * cltd
454
+ logger.debug(f"Roof load: u_value={roof.u_value}, area={roof.area}, cltd={cltd}, load={load}")
455
  return max(load, 0.0)
456
 
457
  except Exception as e:
458
+ logger.error(f"Error in calculate_roof_cooling_load: {str(e)}")
459
  raise Exception(f"Error in calculate_roof_cooling_load: {str(e)}")
460
 
461
  def calculate_window_cooling_load(
 
484
  Dictionary with conduction and solar loads in Watts
485
  """
486
  try:
 
487
  scl_latitude = self.validate_latitude(latitude)
488
  month_upper = self.validate_month(month)
489
+ logger.debug(f"calculate_window_cooling_load: latitude={scl_latitude}, month={month_upper}, orientation={window.orientation.value}")
490
 
491
  # Conduction load
492
  delta_t = outdoor_temp - indoor_temp
 
531
 
532
  solar_load = window.area * window.shgc * scl * shading_coefficient
533
 
534
+ logger.debug(f"Window load: conduction={conduction_load}, solar={solar_load}")
535
  return {
536
  'conduction': max(conduction_load, 0.0),
537
  'solar': max(solar_load, 0.0),
 
539
  }
540
 
541
  except Exception as e:
542
+ logger.error(f"Error in calculate_window_cooling_load: {str(e)}")
543
  raise Exception(f"Error in calculate_window_cooling_load: {str(e)}")
544
 
545
  def calculate_door_cooling_load(
 
562
  try:
563
  delta_t = outdoor_temp - indoor_temp
564
  load = door.u_value * door.area * delta_t
565
+ logger.debug(f"Door load: u_value={door.u_value}, area={door.area}, delta_t={delta_t}, load={load}")
566
  return max(load, 0.0)
567
 
568
  except Exception as e:
569
+ logger.error(f"Error in calculate_door_cooling_load: {str(e)}")
570
  raise Exception(f"Error in calculate_door_cooling_load: {str(e)}")
571
 
572
  def calculate_people_cooling_load(
 
587
  Dictionary with sensible and latent loads in Watts
588
  """
589
  try:
590
+ valid_activities = ['Seated/Resting', 'Light Work', 'Moderate Work', 'Heavy Work']
591
+ if activity_level not in valid_activities:
592
+ logger.warning(f"Invalid activity_level: {activity_level}. Defaulting to 'Seated/Resting'")
593
+ activity_level = 'Seated/Resting'
594
+
595
  sensible_gain = {
596
  'Seated/Resting': 70,
597
  'Light Work': 100,
598
  'Moderate Work': 150,
599
  'Heavy Work': 200
600
+ }[activity_level]
601
 
602
  latent_gain = {
603
  'Seated/Resting': 45,
604
  'Light Work': 75,
605
  'Moderate Work': 120,
606
  'Heavy Work': 180
607
+ }[activity_level]
608
 
609
  logger.debug(f"Calling get_clf_people with zone_type='A', hours_occupied='6h', hour={hour}")
610
  try:
 
617
  sensible_load = num_people * sensible_gain * clf
618
  latent_load = num_people * latent_gain
619
 
620
+ logger.debug(f"People load: num_people={num_people}, sensible={sensible_load}, latent={latent_load}")
621
  return {
622
  'sensible': max(sensible_load, 0.0),
623
  'latent': max(latent_load, 0.0),
 
625
  }
626
 
627
  except Exception as e:
628
+ logger.error(f"Error in calculate_people_cooling_load: {str(e)}")
629
  raise Exception(f"Error in calculate_people_cooling_load: {str(e)}")
630
 
631
  def calculate_lights_cooling_load(
 
656
  logger.warning("Using default CLF=1.0 for lights")
657
  clf = 1.0
658
  load = power * use_factor * special_allowance * clf
659
+ logger.debug(f"Lights load: power={power}, use_factor={use_factor}, special_allowance={special_allowance}, clf={clf}, load={load}")
660
  return max(load, 0.0)
661
 
662
  except Exception as e:
663
+ logger.error(f"Error in calculate_lights_cooling_load: {str(e)}")
664
  raise Exception(f"Error in calculate_lights_cooling_load: {str(e)}")
665
 
666
  def calculate_equipment_cooling_load(
 
693
  sensible_load = power * use_factor * radiation_factor * clf
694
  latent_load = power * use_factor * (1 - radiation_factor)
695
 
696
+ logger.debug(f"Equipment load: power={power}, use_factor={use_factor}, radiation_factor={radiation_factor}, clf={clf}, sensible={sensible_load}, latent={latent_load}")
697
  return {
698
  'sensible': max(sensible_load, 0.0),
699
  'latent': max(latent_load, 0.0),
 
701
  }
702
 
703
  except Exception as e:
704
+ logger.error(f"Error in calculate_equipment_cooling_load: {str(e)}")
705
  raise Exception(f"Error in calculate_equipment_cooling_load: {str(e)}")
706
 
707
  def calculate_infiltration_cooling_load(
 
728
  Dictionary with sensible and latent loads in Watts
729
  """
730
  try:
731
+ if flow_rate < 0 or building_volume <= 0:
732
+ logger.warning(f"Invalid inputs: flow_rate={flow_rate}, building_volume={building_volume}. Returning zero loads")
733
+ return {'sensible': 0.0, 'latent': 0.0, 'total': 0.0}
734
+
735
  air_changes_per_hour = (flow_rate * 3600) / building_volume
736
  sensible_load = 1.2 * flow_rate * 1000 * (outdoor_temp - indoor_temp)
737
 
738
  # Calculate humidity ratio difference
739
  logger.debug(f"Calculating outdoor humidity ratio: temp={outdoor_temp}°C, rh={outdoor_rh}%")
740
  try:
741
+ outdoor_rh = min(max(outdoor_rh, 0), 100) # Clamp RH to 0-100
742
  outdoor_w = self.heat_transfer.psychrometrics.humidity_ratio(outdoor_temp, outdoor_rh)
743
  except Exception as e:
744
  logger.error(f"Failed to calculate outdoor humidity ratio: {str(e)}")
 
747
 
748
  logger.debug(f"Calculating indoor humidity ratio: temp={indoor_temp}°C, rh={indoor_rh}%")
749
  try:
750
+ indoor_rh = min(max(indoor_rh, 0), 100) # Clamp RH to 0-100
751
  indoor_w = self.heat_transfer.psychrometrics.humidity_ratio(indoor_temp, indoor_rh)
752
  except Exception as e:
753
  logger.error(f"Failed to calculate indoor humidity ratio: {str(e)}")
 
756
 
757
  latent_load = 2501 * flow_rate * 1000 * (outdoor_w - indoor_w)
758
 
759
+ logger.debug(f"Infiltration load: sensible={sensible_load}, latent={latent_load}")
760
  return {
761
  'sensible': max(sensible_load, 0.0),
762
  'latent': max(latent_load, 0.0),
 
764
  }
765
 
766
  except Exception as e:
767
+ logger.error(f"Error in calculate_infiltration_cooling_load: {str(e)}")
768
  raise Exception(f"Error in calculate_infiltration_cooling_load: {str(e)}")
769
 
770
  def calculate_ventilation_cooling_load(
 
789
  Dictionary with sensible and latent loads in Watts
790
  """
791
  try:
792
+ if flow_rate < 0:
793
+ logger.warning(f"Invalid flow_rate={flow_rate}. Returning zero loads")
794
+ return {'sensible': 0.0, 'latent': 0.0, 'total': 0.0}
795
+
796
  sensible_load = 1.2 * flow_rate * 1000 * (outdoor_temp - indoor_temp)
797
 
798
  # Calculate humidity ratio difference
799
  logger.debug(f"Calculating outdoor humidity ratio: temp={outdoor_temp}°C, rh={outdoor_rh}%")
800
  try:
801
+ outdoor_rh = min(max(outdoor_rh, 0), 100) # Clamp RH to 0-100
802
  outdoor_w = self.heat_transfer.psychrometrics.humidity_ratio(outdoor_temp, outdoor_rh)
803
  except Exception as e:
804
  logger.error(f"Failed to calculate outdoor humidity ratio: {str(e)}")
 
807
 
808
  logger.debug(f"Calculating indoor humidity ratio: temp={indoor_temp}°C, rh={indoor_rh}%")
809
  try:
810
+ indoor_rh = min(max(indoor_rh, 0), 100) # Clamp RH to 0-100
811
  indoor_w = self.heat_transfer.psychrometrics.humidity_ratio(indoor_temp, indoor_rh)
812
  except Exception as e:
813
  logger.error(f"Failed to calculate indoor humidity ratio: {str(e)}")
 
816
 
817
  latent_load = 2501 * flow_rate * 1000 * (outdoor_w - indoor_w)
818
 
819
+ logger.debug(f"Ventilation load: sensible={sensible_load}, latent={latent_load}")
820
  return {
821
  'sensible': max(sensible_load, 0.0),
822
  'latent': max(latent_load, 0.0),
 
824
  }
825
 
826
  except Exception as e:
827
+ logger.error(f"Error in calculate_ventilation_cooling_load: {str(e)}")
828
  raise Exception(f"Error in calculate_ventilation_cooling_load: {str(e)}")
829
 
830
 
 
839
  orientation=Orientation.NORTH,
840
  area=20.0,
841
  u_value=0.5,
842
+ wall_group="1"
843
  )],
844
  'roofs': [Roof(
845
  name="Main Roof",
 
894
  {'latitude': '24N', 'month': 'Jul'},
895
  {'latitude': '36N', 'month': 'Jul'},
896
  {'latitude': '48N', 'month': 'Jan'},
897
+ {'latitude': '1_31.973N', 'month': 'Jul'}, # Test invalid latitude
898
  {'latitude': 'invalid', 'month': 'invalid'} # Test invalid inputs
899
  ]
900