mabuseif commited on
Commit
9a63b4c
·
verified ·
1 Parent(s): 82ed6c9

Update app/main.py

Browse files
Files changed (1) hide show
  1. app/main.py +133 -101
app/main.py CHANGED
@@ -1,5 +1,6 @@
1
  """
2
  HVAC Calculator Code Documentation
 
3
  """
4
 
5
  import streamlit as st
@@ -83,6 +84,9 @@ class HVACCalculator:
83
  if 'climate_data' not in st.session_state:
84
  st.session_state.climate_data = {}
85
 
 
 
 
86
  # Initialize modules
87
  self.building_info_form = BuildingInfoForm()
88
  self.component_selection = ComponentSelectionInterface()
@@ -153,6 +157,35 @@ class HVACCalculator:
153
  elif page == "Export Data":
154
  self.data_export.display()
155
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
  def validate_internal_load(self, load_type: str, new_load: Dict) -> Tuple[bool, str]:
157
  """Validate if a new internal load is unique and within limits."""
158
  loads = st.session_state.internal_loads.get(load_type, [])
@@ -384,39 +417,30 @@ class HVACCalculator:
384
  Returns: (success, message, results)
385
  """
386
  try:
 
 
 
 
 
387
  # Gather inputs
388
  building_components = st.session_state.get('components', {})
389
  internal_loads = st.session_state.get('internal_loads', {})
390
  building_info = st.session_state.get('building_info', {})
391
 
392
- # Validate inputs
393
- if not building_info.get('floor_area'):
394
- return False, "Floor area is missing in building information.", {}
395
- if not building_components.get('walls') and not building_components.get('roofs') and not building_components.get('windows'):
396
- return False, "No building components defined. Please add walls, roofs, or windows.", {}
397
-
398
  # Check climate data
399
  if "climate_data" not in st.session_state or not st.session_state["climate_data"]:
400
  return False, "Please enter climate data in the 'Climate Data' page.", {}
401
 
402
- # Extract climate data using ClimateData.get_location_by_id
403
  country = building_info.get('country', '').strip().title()
404
  city = building_info.get('city', '').strip().title()
405
  if not country or not city:
406
  return False, "Country and city must be set in Building Information.", {}
407
- climate_id = f"{country[:2].upper()}-{city[:3].upper()}"
408
-
409
- # Debug
410
- st.write("Debug: Climate Data Retrieval", {
411
- 'climate_id': climate_id,
412
- 'building_info': building_info,
413
- 'session_state_climate_data': st.session_state.get('climate_data', 'Not set'),
414
- 'climate_locations': list(self.climate_data.locations.keys())
415
- })
416
-
417
  location = self.climate_data.get_location_by_id(climate_id, st.session_state)
418
  if not location:
419
- return False, f"No climate data found for {climate_id}. Please provide valid climate data.", {}
 
420
 
421
  # Validate climate data
422
  if not all(k in location for k in ['summer_design_temp_db', 'summer_design_temp_wb', 'monthly_temps', 'latitude']):
@@ -429,7 +453,7 @@ class HVACCalculator:
429
  'ground_temperature': location['monthly_temps'].get('Jul', 20.0),
430
  'month': 'Jul',
431
  'latitude': f"{location['latitude']}N" if location['latitude'] >= 0 else f"{abs(location['latitude'])}S",
432
- 'wind_speed': 4.0, # Default as not provided in climate data
433
  'day_of_year': 204 # Approx. July 23
434
  }
435
  indoor_conditions = {
@@ -437,19 +461,19 @@ class HVACCalculator:
437
  'relative_humidity': building_info.get('indoor_rh', 50.0)
438
  }
439
 
440
- # Debug: Log inputs
441
- st.write("Debug: Cooling Input State", {
442
- 'climate_id': climate_id,
443
- 'outdoor_conditions': outdoor_conditions,
444
- 'indoor_conditions': indoor_conditions,
445
- 'components': {k: len(v) for k, v in building_components.items()},
446
- 'internal_loads': {
447
- 'people': len(internal_loads.get('people', [])),
448
- 'lighting': len(internal_loads.get('lighting', [])),
449
- 'equipment': len(internal_loads.get('equipment', []))
450
- },
451
- 'building_info': building_info
452
- })
453
 
454
  # Format internal loads
455
  formatted_internal_loads = {
@@ -616,9 +640,10 @@ class HVACCalculator:
616
  'shading_device': window.shading_device,
617
  'shading_coefficient': window.shading_coefficient,
618
  'scl': self.cooling_calculator.ashrae_tables.get_scl(
619
- window.orientation.value,
620
- design_loads['design_hour'],
621
- scl_latitude
 
622
  ),
623
  'load': load_dict['total'] / 1000
624
  })
@@ -666,23 +691,31 @@ class HVACCalculator:
666
  'quantity': load.get('num_people', load.get('power', 1)),
667
  'heat_gain': load_dict.get('sensible', load_dict['total']),
668
  'clf': self.cooling_calculator.ashrae_tables.get_clf_people(
669
- design_loads['design_hour'],
670
- f"{load['hours_in_operation']}h"
 
671
  ) if load_type == 'people' else 1.0,
672
  'load': load_dict['total'] / 1000
673
  })
674
 
675
- # Debug: Log results
676
- st.write("Debug: Cooling Results", {
677
- 'total_load': results.get('total_load', 'N/A'),
678
- 'component_loads': results.get('component_loads', 'N/A'),
679
- 'detailed_loads': {k: len(v) if isinstance(v, list) else v for k, v in results.get('detailed_loads', {}).items()}
680
- })
681
 
682
  return True, "Cooling calculation completed.", results
 
 
 
 
 
 
 
683
  except Exception as e:
684
- st.error(f"Cooling calculation error: {str(e)}")
685
- return False, f"Cooling calculation error: {str(e)}", {}
686
 
687
  def calculate_heating(self) -> Tuple[bool, str, Dict]:
688
  """
@@ -690,39 +723,30 @@ class HVACCalculator:
690
  Returns: (success, message, results)
691
  """
692
  try:
 
 
 
 
 
693
  # Gather inputs
694
  building_components = st.session_state.get('components', {})
695
  internal_loads = st.session_state.get('internal_loads', {})
696
  building_info = st.session_state.get('building_info', {})
697
 
698
- # Validate inputs
699
- if not building_info.get('floor_area'):
700
- return False, "Floor area is missing in building information.", {}
701
- if not building_components.get('walls') and not building_components.get('roofs') and not building_components.get('windows'):
702
- return False, "No building components defined. Please add walls, roofs, or windows.", {}
703
-
704
  # Check climate data
705
  if "climate_data" not in st.session_state or not st.session_state["climate_data"]:
706
  return False, "Please enter climate data in the 'Climate Data' page.", {}
707
 
708
- # Extract climate data using ClimateData.get_location_by_id
709
  country = building_info.get('country', '').strip().title()
710
  city = building_info.get('city', '').strip().title()
711
  if not country or not city:
712
  return False, "Country and city must be set in Building Information.", {}
713
- climate_id = f"{country[:2].upper()}-{city[:3].upper()}"
714
-
715
- # Debug
716
- st.write("Debug: Climate Data Retrieval", {
717
- 'climate_id': climate_id,
718
- 'building_info': building_info,
719
- 'session_state_climate_data': st.session_state.get('climate_data', 'Not set'),
720
- 'climate_locations': list(self.climate_data.locations.keys())
721
- })
722
-
723
  location = self.climate_data.get_location_by_id(climate_id, st.session_state)
724
  if not location:
725
- return False, f"No climate data found for {climate_id}. Please provide valid climate data.", {}
 
726
 
727
  # Validate climate data
728
  if not all(k in location for k in ['winter_design_temp', 'monthly_temps', 'monthly_humidity']):
@@ -733,26 +757,26 @@ class HVACCalculator:
733
  'design_temperature': location['winter_design_temp'],
734
  'design_relative_humidity': location['monthly_humidity'].get('Jan', 80.0),
735
  'ground_temperature': location['monthly_temps'].get('Jan', 10.0),
736
- 'wind_speed': 4.0 # Default as not provided in climate data
737
  }
738
  indoor_conditions = {
739
  'temperature': building_info.get('indoor_temp', 21.0),
740
  'relative_humidity': building_info.get('indoor_rh', 40.0)
741
  }
742
 
743
- # Debug: Log inputs
744
- st.write("Debug: Heating Input State", {
745
- 'climate_id': climate_id,
746
- 'outdoor_conditions': outdoor_conditions,
747
- 'indoor_conditions': indoor_conditions,
748
- 'components': {k: len(v) for k, v in building_components.items()},
749
- 'internal_loads': {
750
- 'people': len(internal_loads.get('people', [])),
751
- 'lighting': len(internal_loads.get('lighting', [])),
752
- 'equipment': len(internal_loads.get('equipment', []))
753
- },
754
- 'building_info': building_info
755
- })
756
 
757
  # Format internal loads
758
  formatted_internal_loads = {
@@ -910,17 +934,24 @@ class HVACCalculator:
910
  'load': load / 1000
911
  })
912
 
913
- # Debug: Log results
914
- st.write("Debug: Heating Results", {
915
- 'total_load': results.get('total_load', 'N/A'),
916
- 'component_loads': results.get('component_loads', 'N/A'),
917
- 'detailed_loads': {k: len(v) if isinstance(v, list) else v for k, v in results.get('detailed_loads', {}).items()}
918
- })
919
 
920
  return True, "Heating calculation completed.", results
 
 
 
 
 
 
 
921
  except Exception as e:
922
- st.error(f"Heating calculation error: {str(e)}")
923
- return False, f"Heating calculation error: {str(e)}", {}
924
 
925
  def display_calculation_results(self):
926
  st.title("Calculation Results")
@@ -929,27 +960,28 @@ class HVACCalculator:
929
  with col1:
930
  calculate_button = st.button("Calculate Loads")
931
  with col2:
932
- debug = st.checkbox("Debug Mode", value=True) # Enable by default for now
933
 
934
  if calculate_button:
935
  # Reset results
936
  st.session_state.calculation_results = {'cooling': {}, 'heating': {}}
937
 
938
- # Calculate cooling load
939
- cooling_success, cooling_message, cooling_results = self.calculate_cooling()
940
- if cooling_success:
941
- st.session_state.calculation_results['cooling'] = cooling_results
942
- st.success(cooling_message)
943
- else:
944
- st.error(cooling_message)
945
-
946
- # Calculate heating load
947
- heating_success, heating_message, heating_results = self.calculate_heating()
948
- if heating_success:
949
- st.session_state.calculation_results['heating'] = heating_results
950
- st.success(heating_message)
951
- else:
952
- st.error(heating_message)
 
953
 
954
  # Display results
955
  self.results_display.display_results(st.session_state)
 
1
  """
2
  HVAC Calculator Code Documentation
3
+ Updated 2025-04-27: Enhanced climate ID generation, input validation, debug mode, and error handling.
4
  """
5
 
6
  import streamlit as st
 
84
  if 'climate_data' not in st.session_state:
85
  st.session_state.climate_data = {}
86
 
87
+ if 'debug_mode' not in st.session_state:
88
+ st.session_state.debug_mode = False
89
+
90
  # Initialize modules
91
  self.building_info_form = BuildingInfoForm()
92
  self.component_selection = ComponentSelectionInterface()
 
157
  elif page == "Export Data":
158
  self.data_export.display()
159
 
160
+ def generate_climate_id(self, country: str, city: str) -> str:
161
+ """Generate a climate ID from country and city names."""
162
+ try:
163
+ country = country.strip().title()
164
+ city = city.strip().title()
165
+ if len(country) < 2 or len(city) < 3:
166
+ raise ValueError("Country and city names must be at least 2 and 3 characters long, respectively.")
167
+ return f"{country[:2].upper()}-{city[:3].upper()}"
168
+ except Exception as e:
169
+ raise ValueError(f"Invalid country or city name: {str(e)}")
170
+
171
+ def validate_calculation_inputs(self) -> Tuple[bool, str]:
172
+ """Validate inputs for cooling and heating calculations."""
173
+ building_info = st.session_state.get('building_info', {})
174
+ components = st.session_state.get('components', {})
175
+ if not building_info.get('floor_area', 0) > 0:
176
+ return False, "Floor area must be positive."
177
+ if not any(components.get(key, []) for key in ['walls', 'roofs', 'windows']):
178
+ return False, "At least one wall, roof, or window must be defined."
179
+ if not st.session_state.get('climate_data'):
180
+ return False, "Climate data is missing."
181
+ for component_type in ['walls', 'roofs', 'windows', 'doors', 'floors']:
182
+ for comp in components.get(component_type, []):
183
+ if comp.area <= 0:
184
+ return False, f"Invalid area for {component_type}: {comp.name}"
185
+ if comp.u_value <= 0:
186
+ return False, f"Invalid U-value for {component_type}: {comp.name}"
187
+ return True, "Inputs valid."
188
+
189
  def validate_internal_load(self, load_type: str, new_load: Dict) -> Tuple[bool, str]:
190
  """Validate if a new internal load is unique and within limits."""
191
  loads = st.session_state.internal_loads.get(load_type, [])
 
417
  Returns: (success, message, results)
418
  """
419
  try:
420
+ # Validate inputs
421
+ valid, message = self.validate_calculation_inputs()
422
+ if not valid:
423
+ return False, message, {}
424
+
425
  # Gather inputs
426
  building_components = st.session_state.get('components', {})
427
  internal_loads = st.session_state.get('internal_loads', {})
428
  building_info = st.session_state.get('building_info', {})
429
 
 
 
 
 
 
 
430
  # Check climate data
431
  if "climate_data" not in st.session_state or not st.session_state["climate_data"]:
432
  return False, "Please enter climate data in the 'Climate Data' page.", {}
433
 
434
+ # Extract climate data
435
  country = building_info.get('country', '').strip().title()
436
  city = building_info.get('city', '').strip().title()
437
  if not country or not city:
438
  return False, "Country and city must be set in Building Information.", {}
439
+ climate_id = self.generate_climate_id(country, city)
 
 
 
 
 
 
 
 
 
440
  location = self.climate_data.get_location_by_id(climate_id, st.session_state)
441
  if not location:
442
+ available_locations = list(self.climate_data.locations.keys())[:5]
443
+ return False, f"No climate data for {climate_id}. Available locations: {', '.join(available_locations)}...", {}
444
 
445
  # Validate climate data
446
  if not all(k in location for k in ['summer_design_temp_db', 'summer_design_temp_wb', 'monthly_temps', 'latitude']):
 
453
  'ground_temperature': location['monthly_temps'].get('Jul', 20.0),
454
  'month': 'Jul',
455
  'latitude': f"{location['latitude']}N" if location['latitude'] >= 0 else f"{abs(location['latitude'])}S",
456
+ 'wind_speed': building_info.get('wind_speed', 4.0),
457
  'day_of_year': 204 # Approx. July 23
458
  }
459
  indoor_conditions = {
 
461
  'relative_humidity': building_info.get('indoor_rh', 50.0)
462
  }
463
 
464
+ if st.session_state.get('debug_mode', False):
465
+ st.write("Debug: Cooling Input State", {
466
+ 'climate_id': climate_id,
467
+ 'outdoor_conditions': outdoor_conditions,
468
+ 'indoor_conditions': indoor_conditions,
469
+ 'components': {k: len(v) for k, v in building_components.items()},
470
+ 'internal_loads': {
471
+ 'people': len(internal_loads.get('people', [])),
472
+ 'lighting': len(internal_loads.get('lighting', [])),
473
+ 'equipment': len(internal_loads.get('equipment', []))
474
+ },
475
+ 'building_info': building_info
476
+ })
477
 
478
  # Format internal loads
479
  formatted_internal_loads = {
 
640
  'shading_device': window.shading_device,
641
  'shading_coefficient': window.shading_coefficient,
642
  'scl': self.cooling_calculator.ashrae_tables.get_scl(
643
+ latitude=scl_latitude,
644
+ month=outdoor_conditions['month'].upper(),
645
+ orientation=window.orientation.value,
646
+ hour=design_loads['design_hour']
647
  ),
648
  'load': load_dict['total'] / 1000
649
  })
 
691
  'quantity': load.get('num_people', load.get('power', 1)),
692
  'heat_gain': load_dict.get('sensible', load_dict['total']),
693
  'clf': self.cooling_calculator.ashrae_tables.get_clf_people(
694
+ zone_type='A',
695
+ hours_occupied=f"{load['hours_in_operation']}h",
696
+ hour=design_loads['design_hour']
697
  ) if load_type == 'people' else 1.0,
698
  'load': load_dict['total'] / 1000
699
  })
700
 
701
+ if st.session_state.get('debug_mode', False):
702
+ st.write("Debug: Cooling Results", {
703
+ 'total_load': results.get('total_load', 'N/A'),
704
+ 'component_loads': results.get('component_loads', 'N/A'),
705
+ 'detailed_loads': {k: len(v) if isinstance(v, list) else v for k, v in results.get('detailed_loads', {}).items()}
706
+ })
707
 
708
  return True, "Cooling calculation completed.", results
709
+
710
+ except ValueError as ve:
711
+ st.error(f"Input error in cooling calculation: {str(ve)}")
712
+ return False, f"Input error: {str(ve)}", {}
713
+ except KeyError as ke:
714
+ st.error(f"Missing data in cooling calculation: {str(ke)}")
715
+ return False, f"Missing data: {str(ke)}", {}
716
  except Exception as e:
717
+ st.error(f"Unexpected error in cooling calculation: {str(e)}")
718
+ return False, f"Unexpected error: {str(e)}", {}
719
 
720
  def calculate_heating(self) -> Tuple[bool, str, Dict]:
721
  """
 
723
  Returns: (success, message, results)
724
  """
725
  try:
726
+ # Validate inputs
727
+ valid, message = self.validate_calculation_inputs()
728
+ if not valid:
729
+ return False, message, {}
730
+
731
  # Gather inputs
732
  building_components = st.session_state.get('components', {})
733
  internal_loads = st.session_state.get('internal_loads', {})
734
  building_info = st.session_state.get('building_info', {})
735
 
 
 
 
 
 
 
736
  # Check climate data
737
  if "climate_data" not in st.session_state or not st.session_state["climate_data"]:
738
  return False, "Please enter climate data in the 'Climate Data' page.", {}
739
 
740
+ # Extract climate data
741
  country = building_info.get('country', '').strip().title()
742
  city = building_info.get('city', '').strip().title()
743
  if not country or not city:
744
  return False, "Country and city must be set in Building Information.", {}
745
+ climate_id = self.generate_climate_id(country, city)
 
 
 
 
 
 
 
 
 
746
  location = self.climate_data.get_location_by_id(climate_id, st.session_state)
747
  if not location:
748
+ available_locations = list(self.climate_data.locations.keys())[:5]
749
+ return False, f"No climate data for {climate_id}. Available locations: {', '.join(available_locations)}...", {}
750
 
751
  # Validate climate data
752
  if not all(k in location for k in ['winter_design_temp', 'monthly_temps', 'monthly_humidity']):
 
757
  'design_temperature': location['winter_design_temp'],
758
  'design_relative_humidity': location['monthly_humidity'].get('Jan', 80.0),
759
  'ground_temperature': location['monthly_temps'].get('Jan', 10.0),
760
+ 'wind_speed': building_info.get('wind_speed', 4.0)
761
  }
762
  indoor_conditions = {
763
  'temperature': building_info.get('indoor_temp', 21.0),
764
  'relative_humidity': building_info.get('indoor_rh', 40.0)
765
  }
766
 
767
+ if st.session_state.get('debug_mode', False):
768
+ st.write("Debug: Heating Input State", {
769
+ 'climate_id': climate_id,
770
+ 'outdoor_conditions': outdoor_conditions,
771
+ 'indoor_conditions': indoor_conditions,
772
+ 'components': {k: len(v) for k, v in building_components.items()},
773
+ 'internal_loads': {
774
+ 'people': len(internal_loads.get('people', [])),
775
+ 'lighting': len(internal_loads.get('lighting', [])),
776
+ 'equipment': len(internal_loads.get('equipment', []))
777
+ },
778
+ 'building_info': building_info
779
+ })
780
 
781
  # Format internal loads
782
  formatted_internal_loads = {
 
934
  'load': load / 1000
935
  })
936
 
937
+ if st.session_state.get('debug_mode', False):
938
+ st.write("Debug: Heating Results", {
939
+ 'total_load': results.get('total_load', 'N/A'),
940
+ 'component_loads': results.get('component_loads', 'N/A'),
941
+ 'detailed_loads': {k: len(v) if isinstance(v, list) else v for k, v in results.get('detailed_loads', {}).items()}
942
+ })
943
 
944
  return True, "Heating calculation completed.", results
945
+
946
+ except ValueError as ve:
947
+ st.error(f"Input error in heating calculation: {str(ve)}")
948
+ return False, f"Input error: {str(ve)}", {}
949
+ except KeyError as ke:
950
+ st.error(f"Missing data in heating calculation: {str(ke)}")
951
+ return False, f"Missing data: {str(ke)}", {}
952
  except Exception as e:
953
+ st.error(f"Unexpected error in heating calculation: {str(e)}")
954
+ return False, f"Unexpected error: {str(e)}", {}
955
 
956
  def display_calculation_results(self):
957
  st.title("Calculation Results")
 
960
  with col1:
961
  calculate_button = st.button("Calculate Loads")
962
  with col2:
963
+ st.session_state.debug_mode = st.checkbox("Debug Mode", value=st.session_state.get('debug_mode', False))
964
 
965
  if calculate_button:
966
  # Reset results
967
  st.session_state.calculation_results = {'cooling': {}, 'heating': {}}
968
 
969
+ with st.spinner("Calculating loads..."):
970
+ # Calculate cooling load
971
+ cooling_success, cooling_message, cooling_results = self.calculate_cooling()
972
+ if cooling_success:
973
+ st.session_state.calculation_results['cooling'] = cooling_results
974
+ st.success(cooling_message)
975
+ else:
976
+ st.error(cooling_message)
977
+
978
+ # Calculate heating load
979
+ heating_success, heating_message, heating_results = self.calculate_heating()
980
+ if heating_success:
981
+ st.session_state.calculation_results['heating'] = heating_results
982
+ st.success(heating_message)
983
+ else:
984
+ st.error(heating_message)
985
 
986
  # Display results
987
  self.results_display.display_results(st.session_state)