mabuseif commited on
Commit
7e62d7c
·
verified ·
1 Parent(s): 0a44424

Update app/hvac_loads.py

Browse files
Files changed (1) hide show
  1. app/hvac_loads.py +198 -124
app/hvac_loads.py CHANGED
@@ -671,134 +671,208 @@ class TFMCalculations:
671
  temp = adaptive_setpoints.get(key, 24.0) if adaptive_setpoints else 24.0
672
  return {"temperature": temp, "rh": 50.0}
673
 
674
- @staticmethod
675
- def calculate_tfm_loads(components: Dict, hourly_data: List[Dict], indoor_conditions: Dict, internal_loads: Dict, building_info: Dict, sim_period: Dict, hvac_settings: Dict) -> List[Dict]:
676
- """Calculate TFM loads for heating and cooling with user-defined filters and temperature threshold."""
677
- # Access climate_data for ground temperatures
678
- climate_data = st.session_state.project_data["climate_data"]
679
- ground_temperatures = climate_data.get("ground_temperatures", {})
680
- logger.debug(f"Ground temperatures available: {ground_temperatures.keys()}")
681
-
682
- filtered_data = TFMCalculations.filter_hourly_data(hourly_data, sim_period, climate_data)
683
- temp_loads = []
684
- building_orientation = building_info.get("orientation_angle", 0.0)
685
- operating_periods = hvac_settings.get("operating_hours", [{"start": 8, "end": 18}])
686
- area = building_info.get("floor_area", 100.0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
687
 
688
- if "material_library" not in st.session_state:
689
- from app.materials_library import MaterialLibrary
690
- st.session_state.material_library = MaterialLibrary()
691
- logger.info("Initialized MaterialLibrary in session_state for solar calculations")
692
-
693
- if indoor_conditions["type"] == "ASHRAE 55 Adaptive Comfort":
694
- acceptability = indoor_conditions.get("adaptive_acceptability", "90")
695
- adaptive_setpoints = AdaptiveComfortModel.generate_adaptive_setpoints(hourly_data, acceptability)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
696
  else:
697
- adaptive_setpoints = None
698
-
699
- for comp_list in components.values():
700
- for comp in comp_list:
701
- comp['ctf'] = CTFCalculator.calculate_ctf_coefficients(comp)
702
- logger.debug(f"Stored CTF coefficients for component {comp.get('name', 'Unknown')}")
703
-
704
- for hour_data in filtered_data:
705
- hour = hour_data["hour"]
706
- outdoor_temp = hour_data["dry_bulb"]
707
- month = hour_data["month"]
708
- day = hour_data["day"]
709
- # For future enhancement: Retrieve ground temperature for the current month
710
- ground_temp = ground_temperatures.get("0.5", [20.0]*12)[month-1] if ground_temperatures else 20.0
711
- logger.debug(f"Ground temperature for month {month}: {ground_temp:.1f}°C")
 
 
712
 
713
- indoor_cond = TFMCalculations.get_indoor_conditions(indoor_conditions, hour, outdoor_temp, month, day, adaptive_setpoints)
714
- indoor_temp = indoor_cond["temperature"]
715
- conduction_cooling = conduction_heating = solar = internal = ventilation_cooling = ventilation_heating = infiltration_cooling = infiltration_heating = 0
716
- solar_by_orientation = defaultdict(float)
717
- conduction_by_orientation = defaultdict(float)
718
- is_operating = False
719
- for period in operating_periods:
720
- start_hour = period.get("start", 8)
721
- end_hour = period.get("end", 18)
722
- if start_hour <= hour % 24 <= end_hour:
723
- is_operating = True
724
- break
725
- mode = "none" if abs(outdoor_temp - 18) < 0.01 else "cooling" if outdoor_temp > 18 else "heating"
726
- if is_operating and mode == "cooling":
727
- for comp_list in components.values():
728
- for comp in comp_list:
729
- cool_load, _ = TFMCalculations.calculate_conduction_load(comp, outdoor_temp, indoor_temp, hour, mode="cooling")
730
- component_solar_load = TFMCalculations.calculate_solar_load(comp, hour_data, hour, building_orientation, mode="cooling")
731
- orientation = classify_azimuth(comp.get("surface_azimuth", 0))
732
- element = TFMCalculations.get_component_type(comp).name
733
- key = orientation if element in ["WALL", "WINDOW"] else element
734
- conduction_by_orientation[key] += cool_load
735
- solar_by_orientation[key] += component_solar_load
736
- conduction_cooling += cool_load
737
- solar += component_solar_load
738
- logger.info(f"Component {comp.get('name', 'Unknown')} ({TFMCalculations.get_component_type(comp).value}) solar load: {component_solar_load:.3f} kW, accumulated solar: {solar:.3f} kW")
739
-
740
- internal = TFMCalculations.calculate_internal_load(internal_loads, hour, max([p["end"] - p["start"] for p in operating_periods]), area)
741
- ventilation_cooling, _ = TFMCalculations.calculate_ventilation_load(internal_loads, outdoor_temp, indoor_temp, area, building_info, hour, mode="cooling")
742
- infiltration_cooling, _ = TFMCalculations.calculate_infiltration_load(internal_loads, outdoor_temp, indoor_temp, area, building_info, hour, mode="cooling")
743
- elif is_operating and mode == "heating":
744
- for comp_list in components.values():
745
- for comp in comp_list:
746
- _, heat_load = TFMCalculations.calculate_conduction_load(comp, outdoor_temp, indoor_temp, hour, mode="heating")
747
- orientation = classify_azimuth(comp.get("surface_azimuth", 0))
748
- element = TFMCalculations.get_component_type(comp).name
749
- key = orientation if element in ["WALL", "WINDOW"] else element
750
- conduction_by_orientation[key] += heat_load
751
- conduction_heating += heat_load
752
- internal = TFMCalculations.calculate_internal_load(internal_loads, hour, max([p["end"] - p["start"] for p in operating_periods]), area)
753
- _, ventilation_heating = TFMCalculations.calculate_ventilation_load(internal_loads, outdoor_temp, indoor_temp, area, building_info, hour, mode="heating")
754
- _, infiltration_heating = TFMCalculations.calculate_infiltration_load(internal_loads, outdoor_temp, indoor_temp, area, building_info, hour, mode="heating")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
755
  else:
756
- internal = 0
757
-
758
- logger.info(f"Hour {hour} total loads - conduction: {conduction_cooling:.3f} kW, solar: {solar:.3f} kW, internal: {internal:.3f} kW")
759
-
760
- total_cooling = conduction_cooling + solar + internal + ventilation_cooling + infiltration_cooling
761
- total_heating = max(conduction_heating + ventilation_heating + infiltration_heating - internal, 0)
762
- if mode == "cooling":
763
- total_heating = 0
764
- elif mode == "heating":
765
- total_cooling = 0
766
- temp_loads.append({
767
- "hour": hour,
768
- "month": month,
769
- "day": day,
770
- "conduction_cooling": conduction_cooling,
771
- "conduction_heating": conduction_heating,
772
- "solar": solar,
773
- "internal": internal,
774
- "ventilation_cooling": ventilation_cooling,
775
- "ventilation_heating": ventilation_heating,
776
- "infiltration_cooling": infiltration_cooling,
777
- "infiltration_heating": infiltration_heating,
778
- "total_cooling": total_cooling,
779
- "total_heating": total_heating,
780
- "ground_temperature": ground_temp,
781
- "solar_by_orientation": dict(solar_by_orientation),
782
- "conduction_by_orientation": dict(conduction_by_orientation)
783
- })
784
- loads_by_day = defaultdict(list)
785
- for load in temp_loads:
786
- day_key = (load["month"], load["day"])
787
- loads_by_day[day_key].append(load)
788
- final_loads = []
789
- for day_key, day_loads in loads_by_day.items():
790
- cooling_hours = sum(1 for load in day_loads if load["total_cooling"] > 0)
791
- heating_hours = sum(1 for load in day_loads if load["total_heating"] > 0)
792
- for load in day_loads:
793
- if cooling_hours > heating_hours:
794
- load["total_heating"] = 0
795
- elif heating_hours > cooling_hours:
796
- load["total_cooling"] = 0
797
- else:
798
- load["total_cooling"] = 0
799
- load["total_heating"] = 0
800
- final_loads.append(load)
801
- return final_loads
802
 
803
  def make_pie(data: Dict[str, float], title: str) -> px.pie:
804
  """Create a Plotly pie chart from a dictionary of values."""
 
671
  temp = adaptive_setpoints.get(key, 24.0) if adaptive_setpoints else 24.0
672
  return {"temperature": temp, "rh": 50.0}
673
 
674
+ @staticmethod
675
+ def calculate_tfm_loads(components: Dict, hourly_data: List[Dict], indoor_conditions: Dict, internal_loads: Dict, building_info: Dict, sim_period: Dict, hvac_settings: Dict) -> List[Dict]:
676
+ """Calculate TFM loads for heating and cooling with user-defined filters and temperature threshold."""
677
+ # Access climate_data for ground temperatures
678
+ climate_data = st.session_state.project_data["climate_data"]
679
+ ground_temperatures = climate_data.get("ground_temperatures", {})
680
+ logger.debug(f"Ground temperatures available: {ground_temperatures.keys()}")
681
+
682
+ filtered_data = TFMCalculations.filter_hourly_data(hourly_data, sim_period, climate_data)
683
+ temp_loads = []
684
+ building_orientation = building_info.get("orientation_angle", 0.0)
685
+ operating_periods = hvac_settings.get("operating_hours", [{"start": 8, "end": 18}])
686
+ area = building_info.get("floor_area", 100.0)
687
+
688
+ if "material_library" not in st.session_state:
689
+ from app.materials_library import MaterialLibrary
690
+ st.session_state.material_library = MaterialLibrary()
691
+ logger.info("Initialized MaterialLibrary in session_state for solar calculations")
692
+
693
+ if indoor_conditions["type"] == "ASHRAE 55 Adaptive Comfort":
694
+ acceptability = indoor_conditions.get("adaptive_acceptability", "90")
695
+ adaptive_setpoints = AdaptiveComfortModel.generate_adaptive_setpoints(hourly_data, acceptability)
696
+ else:
697
+ adaptive_setpoints = None
698
+
699
+ for comp_list in components.values():
700
+ for comp in comp_list:
701
+ comp['ctf'] = CTFCalculator.calculate_ctf_coefficients(comp)
702
+ logger.debug(f"Stored CTF coefficients for component {comp.get('name', 'Unknown')}")
703
+
704
+ # Cache total surface area for opaque components (walls, roofs, floors)
705
+ total_surface_area = 0.0
706
+ opaque_components = []
707
+ for comp_list in components.values():
708
+ for comp in comp_list:
709
+ comp_type = TFMCalculations.get_component_type(comp)
710
+ if comp_type in [ComponentType.WALL, ComponentType.ROOF, ComponentType.FLOOR]:
711
+ comp_area = comp.get('area', 0.0)
712
+ if comp_area > 0:
713
+ total_surface_area += comp_area
714
+ opaque_components.append(comp)
715
+ logger.debug(f"Total surface area for opaque components: {total_surface_area:.2f} m²")
716
+
717
+ for hour_data in filtered_data:
718
+ hour = hour_data["hour"]
719
+ outdoor_temp = hour_data["dry_bulb"]
720
+ month = hour_data["month"]
721
+ day = hour_data["day"]
722
+ # For future enhancement: Retrieve ground temperature for the current month
723
+ ground_temp = ground_temperatures.get("0.5", [20.0]*12)[month-1] if ground_temperatures else 20.0
724
+ logger.debug(f"Ground temperature for month {month}: {ground_temp:.1f}°C")
725
 
726
+ indoor_cond = TFMCalculations.get_indoor_conditions(indoor_conditions, hour, outdoor_temp, month, day, adaptive_setpoints)
727
+ indoor_temp = indoor_cond["temperature"]
728
+ conduction_cooling = conduction_heating = solar = internal = ventilation_cooling = ventilation_heating = infiltration_cooling = infiltration_heating = 0
729
+ solar_by_orientation = defaultdict(float)
730
+ conduction_by_orientation = defaultdict(float)
731
+ is_operating = False
732
+ for period in operating_periods:
733
+ start_hour = period.get("start", 8)
734
+ end_hour = period.get("end", 18)
735
+ if start_hour <= hour % 24 <= end_hour:
736
+ is_operating = True
737
+ break
738
+ mode = "none" if abs(outdoor_temp - 18) < 0.01 else "cooling" if outdoor_temp > 18 else "heating"
739
+
740
+ # Calculate radiant loads from internal sources
741
+ total_radiant_load = 0.0
742
+ internal_loads_conditions = st.session_state.project_data.get("internal_loads_conditions", {
743
+ "air_velocity": 0.1,
744
+ "lighting_convective_fraction": 0.5,
745
+ "lighting_radiative_fraction": 0.5,
746
+ "equipment_convective_fraction": 0.5,
747
+ "equipment_radiative_fraction": 0.5
748
+ })
749
+ is_weekend = False # Simplified; determine from date in practice
750
+
751
+ # People radiant load
752
+ air_velocity = internal_loads_conditions.get("air_velocity", 0.1)
753
+ if air_velocity < 0.0 or air_velocity > 2.0:
754
+ logger.warning(f"Air velocity {air_velocity} out of range [0.0, 2.0] for hour {hour}. Clamping to nearest bound.")
755
+ air_velocity = max(0.0, min(2.0, air_velocity))
756
+ people_convective_fraction = min(max(0.5 + 0.31 * air_velocity, 0.0), 1.0)
757
+ people_radiative_fraction = 1.0 - people_convective_fraction
758
+ for group in internal_loads.get("people", []):
759
+ schedule_name = group.get("schedule", "Continuous")
760
+ fraction = TFMCalculations.get_schedule_fraction(schedule_name, hour, is_weekend)
761
+ sensible = group.get("total_sensible_heat", 0.0)
762
+ radiant_load = sensible * people_radiative_fraction * fraction / 1000 # kW, exclude latent heat
763
+ total_radiant_load += radiant_load
764
+ logger.debug(f"People group '{group.get('name', 'unknown')}': sensible={sensible:.2f} W, radiative_fraction={people_radiative_fraction:.2f}, fraction={fraction:.2f}, radiant_load={radiant_load:.3f} kW")
765
+
766
+ # Lighting radiant load
767
+ lighting_radiative_fraction = internal_loads_conditions.get("lighting_radiative_fraction", 0.5)
768
+ for light in internal_loads.get("lighting", []):
769
+ schedule_name = light.get("schedule", "Continuous")
770
+ fraction = TFMCalculations.get_schedule_fraction(schedule_name, hour, is_weekend)
771
+ total_power = light.get("total_power", 0.0)
772
+ radiant_load = total_power * lighting_radiative_fraction * fraction / 1000 # kW
773
+ total_radiant_load += radiant_load
774
+ logger.debug(f"Lighting system '{light.get('name', 'unknown')}': total_power={total_power:.2f} W, radiative_fraction={lighting_radiative_fraction:.2f}, fraction={fraction:.2f}, radiant_load={radiant_load:.3f} kW")
775
+
776
+ # Equipment radiant load
777
+ equipment_radiative_fraction = internal_loads_conditions.get("equipment_radiative_fraction", 0.5)
778
+ for equip in internal_loads.get("equipment", []):
779
+ schedule_name = equip.get("schedule", "Continuous")
780
+ fraction = TFMCalculations.get_schedule_fraction(schedule_name, hour, is_weekend)
781
+ sensible = equip.get("total_sensible_power", 0.0)
782
+ radiant_load = sensible * equipment_radiative_fraction * fraction / 1000 # kW, exclude latent heat
783
+ total_radiant_load += radiant_load
784
+ logger.debug(f"Equipment '{equip.get('name', 'unknown')}': sensible={sensible:.2f} W, radiative_fraction={equipment_radiative_fraction:.2f}, fraction={fraction:.2f}, radiant_load={radiant_load:.3f} kW")
785
+
786
+ logger.debug(f"Total radiant load for hour {hour}: {total_radiant_load:.3f} kW")
787
+
788
+ # Distribute radiant load to opaque surfaces
789
+ if total_surface_area > 0:
790
+ for comp in opaque_components:
791
+ comp_area = comp.get('area', 0.0)
792
+ comp['radiant_load'] = total_radiant_load * (comp_area / total_surface_area)
793
+ logger.debug(f"Component '{comp.get('name', 'Unknown')}': area={comp_area:.2f} m², radiant_load={comp['radiant_load']:.3f} kW")
794
  else:
795
+ logger.warning(f"No valid surface area for hour {hour}. Skipping radiant load distribution.")
796
+ for comp in opaque_components:
797
+ comp['radiant_load'] = 0.0
798
+
799
+ if is_operating and mode == "cooling":
800
+ for comp_list in components.values():
801
+ for comp in comp_list:
802
+ cool_load, _ = TFMCalculations.calculate_conduction_load(comp, outdoor_temp, indoor_temp, hour, mode="cooling")
803
+ component_solar_load = TFMCalculations.calculate_solar_load(comp, hour_data, hour, building_orientation, mode="cooling")
804
+ orientation = classify_azimuth(comp.get("surface_azimuth", 0))
805
+ element = TFMCalculations.get_component_type(comp).name
806
+ key = orientation if element in ["WALL", "WINDOW"] else element
807
+ conduction_by_orientation[key] += cool_load
808
+ solar_by_orientation[key] += component_solar_load
809
+ conduction_cooling += cool_load
810
+ solar += component_solar_load
811
+ logger.info(f"Component {comp.get('name', 'Unknown')} ({TFMCalculations.get_component_type(comp).value}) solar load: {component_solar_load:.3f} kW, accumulated solar: {solar:.3f} kW")
812
 
813
+ internal = TFMCalculations.calculate_internal_load(internal_loads, hour, max([p["end"] - p["start"] for p in operating_periods]), area)
814
+ ventilation_cooling, _ = TFMCalculations.calculate_ventilation_load(internal_loads, outdoor_temp, indoor_temp, area, building_info, hour, mode="cooling")
815
+ infiltration_cooling, _ = TFMCalculations.calculate_infiltration_load(internal_loads, outdoor_temp, indoor_temp, area, building_info, hour, mode="cooling")
816
+ elif is_operating and mode == "heating":
817
+ for comp_list in components.values():
818
+ for comp in comp_list:
819
+ _, heat_load = TFMCalculations.calculate_conduction_load(comp, outdoor_temp, indoor_temp, hour, mode="heating")
820
+ orientation = classify_azimuth(comp.get("surface_azimuth", 0))
821
+ element = TFMCalculations.get_component_type(comp).name
822
+ key = orientation if element in ["WALL", "WINDOW"] else element
823
+ conduction_by_orientation[key] += heat_load
824
+ conduction_heating += heat_load
825
+ internal = TFMCalculations.calculate_internal_load(internal_loads, hour, max([p["end"] - p["start"] for p in operating_periods]), area)
826
+ _, ventilation_heating = TFMCalculations.calculate_ventilation_load(internal_loads, outdoor_temp, indoor_temp, area, building_info, hour, mode="heating")
827
+ _, infiltration_heating = TFMCalculations.calculate_infiltration_load(internal_loads, outdoor_temp, indoor_temp, area, building_info, hour, mode="heating")
828
+ else:
829
+ internal = 0
830
+
831
+ logger.info(f"Hour {hour} total loads - conduction: {conduction_cooling:.3f} kW, solar: {solar:.3f} kW, internal: {internal:.3f} kW")
832
+
833
+ total_cooling = conduction_cooling + solar + internal + ventilation_cooling + infiltration_cooling
834
+ total_heating = max(conduction_heating + ventilation_heating + infiltration_heating - internal, 0)
835
+ if mode == "cooling":
836
+ total_heating = 0
837
+ elif mode == "heating":
838
+ total_cooling = 0
839
+ temp_loads.append({
840
+ "hour": hour,
841
+ "month": month,
842
+ "day": day,
843
+ "conduction_cooling": conduction_cooling,
844
+ "conduction_heating": conduction_heating,
845
+ "solar": solar,
846
+ "internal": internal,
847
+ "ventilation_cooling": ventilation_cooling,
848
+ "ventilation_heating": ventilation_heating,
849
+ "infiltration_cooling": infiltration_cooling,
850
+ "infiltration_heating": infiltration_heating,
851
+ "total_cooling": total_cooling,
852
+ "total_heating": total_heating,
853
+ "ground_temperature": ground_temp,
854
+ "solar_by_orientation": dict(solar_by_orientation),
855
+ "conduction_by_orientation": dict(conduction_by_orientation)
856
+ })
857
+
858
+ loads_by_day = defaultdict(list)
859
+ for load in temp_loads:
860
+ day_key = (load["month"], load["day"])
861
+ loads_by_day[day_key].append(load)
862
+ final_loads = []
863
+ for day_key, day_loads in loads_by_day.items():
864
+ cooling_hours = sum(1 for load in day_loads if load["total_cooling"] > 0)
865
+ heating_hours = sum(1 for load in day_loads if load["total_heating"] > 0)
866
+ for load in day_loads:
867
+ if cooling_hours > heating_hours:
868
+ load["total_heating"] = 0
869
+ elif heating_hours > cooling_hours:
870
+ load["total_cooling"] = 0
871
  else:
872
+ load["total_cooling"] = 0
873
+ load["total_heating"] = 0
874
+ final_loads.append(load)
875
+ return final_loads
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
876
 
877
  def make_pie(data: Dict[str, float], title: str) -> px.pie:
878
  """Create a Plotly pie chart from a dictionary of values."""