mabuseif commited on
Commit
07614b6
·
verified ·
1 Parent(s): e63e763

Update app/hvac_loads.py

Browse files
Files changed (1) hide show
  1. app/hvac_loads.py +206 -89
app/hvac_loads.py CHANGED
@@ -19,6 +19,7 @@ import math
19
  from utils.ctf_calculations import CTFCalculator, ComponentType, CTFCoefficients
20
  from utils.solar import SolarCalculations # Import SolarCalculations for SHGC data
21
  from app.m_c_data import SAMPLE_MATERIALS, SAMPLE_FENESTRATIONS, DEFAULT_MATERIAL_PROPERTIES, DEFAULT_WINDOW_PROPERTIES
 
22
 
23
  # Configure logging
24
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
@@ -68,6 +69,27 @@ class AdaptiveComfortModel:
68
  setpoints[key] = (t_min + t_max) / 2
69
  return setpoints
70
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  class TFMCalculations:
72
  @staticmethod
73
  def get_component_type(component: Dict[str, Any]) -> ComponentType:
@@ -689,6 +711,8 @@ class TFMCalculations:
689
  indoor_cond = TFMCalculations.get_indoor_conditions(indoor_conditions, hour, outdoor_temp, month, day, adaptive_setpoints)
690
  indoor_temp = indoor_cond["temperature"]
691
  conduction_cooling = conduction_heating = solar = internal = ventilation_cooling = ventilation_heating = infiltration_cooling = infiltration_heating = 0
 
 
692
  is_operating = False
693
  for period in operating_periods:
694
  start_hour = period.get("start", 8)
@@ -701,8 +725,13 @@ class TFMCalculations:
701
  for comp_list in components.values():
702
  for comp in comp_list:
703
  cool_load, _ = TFMCalculations.calculate_conduction_load(comp, outdoor_temp, indoor_temp, hour, mode="cooling")
704
- conduction_cooling += cool_load
705
  component_solar_load = TFMCalculations.calculate_solar_load(comp, hour_data, hour, building_orientation, mode="cooling")
 
 
 
 
 
 
706
  solar += component_solar_load
707
  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")
708
 
@@ -713,6 +742,10 @@ class TFMCalculations:
713
  for comp_list in components.values():
714
  for comp in comp_list:
715
  _, heat_load = TFMCalculations.calculate_conduction_load(comp, outdoor_temp, indoor_temp, hour, mode="heating")
 
 
 
 
716
  conduction_heating += heat_load
717
  internal = TFMCalculations.calculate_internal_load(internal_loads, hour, max([p["end"] - p["start"] for p in operating_periods]), area)
718
  _, ventilation_heating = TFMCalculations.calculate_ventilation_load(internal_loads, outdoor_temp, indoor_temp, area, building_info, hour, mode="heating")
@@ -742,7 +775,9 @@ class TFMCalculations:
742
  "infiltration_heating": infiltration_heating,
743
  "total_cooling": total_cooling,
744
  "total_heating": total_heating,
745
- "ground_temperature": ground_temp # Included for future enhancement
 
 
746
  })
747
  loads_by_day = defaultdict(list)
748
  for load in temp_loads:
@@ -763,6 +798,116 @@ class TFMCalculations:
763
  final_loads.append(load)
764
  return final_loads
765
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
766
  def display_hvac_loads_page():
767
  """
768
  Display the HVAC Loads page in the Streamlit application.
@@ -966,93 +1111,65 @@ def display_hvac_loads_page():
966
  # Calculate HVAC Loads
967
  if st.button("Calculate HVAC Loads"):
968
  try:
969
- components = st.session_state.project_data["components"]
970
- hourly_data = st.session_state.project_data["climate_data"]["hourly_data"]
971
- indoor_conditions = st.session_state.project_data["indoor_conditions"]
972
- internal_loads = st.session_state.project_data["internal_loads"]
973
- building_info = st.session_state.project_data["building_info"]
974
- sim_period = st.session_state.project_data["sim_period"]
975
- hvac_settings = st.session_state.project_data["hvac_settings"]
976
-
977
- if not hourly_data:
978
- st.error("No climate data available. Please configure climate data first.")
979
- logger.error("HVAC calculation failed: No climate data available")
980
- return
981
- elif not any(comp_list for comp_list in components.values()):
982
- st.error("No building components defined. Please configure components first.")
983
- logger.error("HVAC calculation failed: No building components defined")
984
- return
985
- else:
986
- loads = TFMCalculations.calculate_tfm_loads(
987
- components=components,
988
- hourly_data=hourly_data,
989
- indoor_conditions=indoor_conditions,
990
- internal_loads=internal_loads,
991
- building_info=building_info,
992
- sim_period=sim_period,
993
- hvac_settings=hvac_settings
994
- )
995
-
996
- # Update session state with results
997
- cooling_loads = [load for load in loads if load["total_cooling"] > 0]
998
- heating_loads = [load for load in loads if load["total_heating"] > 0]
999
- st.session_state.project_data["hvac_loads"]["cooling"]["hourly"] = cooling_loads
1000
- st.session_state.project_data["hvac_loads"]["heating"]["hourly"] = heating_loads
1001
- st.session_state.project_data["hvac_loads"]["cooling"]["peak"] = max([load["total_cooling"] for load in cooling_loads], default=0)
1002
- st.session_state.project_data["hvac_loads"]["heating"]["peak"] = max([load["total_heating"] for load in heating_loads], default=0)
1003
-
1004
- # Store breakdown
1005
- cooling_breakdown = {
1006
- "conduction": sum(load["conduction_cooling"] for load in cooling_loads),
1007
- "solar": sum(load["solar"] for load in cooling_loads if load["total_cooling"] > 0),
1008
- "internal": sum(load["internal"] for load in cooling_loads if load["total_cooling"] > 0),
1009
- "ventilation": sum(load["ventilation_cooling"] for load in cooling_loads),
1010
- "infiltration": sum(load["infiltration_cooling"] for load in cooling_loads)
1011
- }
1012
- heating_breakdown = {
1013
- "conduction": sum(load["conduction_heating"] for load in heating_loads),
1014
- "ventilation": sum(load["ventilation_heating"] for load in heating_loads),
1015
- "infiltration": sum(load["infiltration_heating"] for load in heating_loads)
1016
- }
1017
- st.session_state.project_data["hvac_loads"]["cooling"]["breakdown"] = cooling_breakdown
1018
- st.session_state.project_data["hvac_loads"]["heating"]["breakdown"] = heating_breakdown
1019
-
1020
- # Display Results
1021
- st.subheader("HVAC Load Results")
1022
- st.write(f"Peak Cooling Load: {st.session_state.project_data['hvac_loads']['cooling']['peak']:.2f} kW")
1023
- st.write(f"Peak Heating Load: {st.session_state.project_data['hvac_loads']['heating']['peak']:.2f} kW")
1024
-
1025
- # Display Breakdown
1026
- st.subheader("Cooling Load Breakdown")
1027
- for key, value in cooling_breakdown.items():
1028
- st.write(f"{key.capitalize()}: {value:.2f} kW")
1029
- st.subheader("Heating Load Breakdown")
1030
- for key, value in heating_breakdown.items():
1031
- st.write(f"{key.capitalize()}: {value:.2f} kW")
1032
-
1033
- # Optional: Display Hourly Loads Table in Debug Mode
1034
- if st.session_state.debug_mode:
1035
- st.subheader("Hourly Loads (Debug Mode)")
1036
- hourly_df = []
1037
- for load in loads:
1038
- hourly_df.append({
1039
- "Month": load["month"],
1040
- "Day": load["day"],
1041
- "Hour": load["hour"],
1042
- "Total Cooling (kW)": load["total_cooling"],
1043
- "Total Heating (kW)": load["total_heating"],
1044
- "Conduction Cooling (kW)": load["conduction_cooling"],
1045
- "Solar (kW)": load["solar"],
1046
- "Internal (kW)": load["internal"],
1047
- "Ventilation Cooling (kW)": load["ventilation_cooling"],
1048
- "Infiltration Cooling (kW)": load["infiltration_cooling"],
1049
- "Ventilation Heating (kW)": load["ventilation_heating"],
1050
- "Infiltration Heating (kW)": load["infiltration_heating"]
1051
- })
1052
- st.dataframe(hourly_df)
1053
-
1054
- st.success("HVAC loads calculated successfully.")
1055
- logger.info("HVAC loads calculated and stored in session state")
1056
 
1057
  except Exception as e:
1058
  st.error(f"Error calculating HVAC loads: {str(e)}")
 
19
  from utils.ctf_calculations import CTFCalculator, ComponentType, CTFCoefficients
20
  from utils.solar import SolarCalculations # Import SolarCalculations for SHGC data
21
  from app.m_c_data import SAMPLE_MATERIALS, SAMPLE_FENESTRATIONS, DEFAULT_MATERIAL_PROPERTIES, DEFAULT_WINDOW_PROPERTIES
22
+ import plotly.express as px
23
 
24
  # Configure logging
25
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
 
69
  setpoints[key] = (t_min + t_max) / 2
70
  return setpoints
71
 
72
+ def classify_azimuth(azimuth: float) -> str:
73
+ """Classify azimuth angle into cardinal directions."""
74
+ azimuth = azimuth % 360
75
+ if 337.5 <= azimuth or azimuth < 22.5:
76
+ return "N"
77
+ elif 22.5 <= azimuth < 67.5:
78
+ return "NE"
79
+ elif 67.5 <= azimuth < 112.5:
80
+ return "E"
81
+ elif 112.5 <= azimuth < 157.5:
82
+ return "SE"
83
+ elif 157.5 <= azimuth < 202.5:
84
+ return "S"
85
+ elif 202.5 <= azimuth < 247.5:
86
+ return "SW"
87
+ elif 247.5 <= azimuth < 292.5:
88
+ return "W"
89
+ elif 292.5 <= azimuth < 337.5:
90
+ return "NW"
91
+ return "N"
92
+
93
  class TFMCalculations:
94
  @staticmethod
95
  def get_component_type(component: Dict[str, Any]) -> ComponentType:
 
711
  indoor_cond = TFMCalculations.get_indoor_conditions(indoor_conditions, hour, outdoor_temp, month, day, adaptive_setpoints)
712
  indoor_temp = indoor_cond["temperature"]
713
  conduction_cooling = conduction_heating = solar = internal = ventilation_cooling = ventilation_heating = infiltration_cooling = infiltration_heating = 0
714
+ solar_by_orientation = defaultdict(float)
715
+ conduction_by_orientation = defaultdict(float)
716
  is_operating = False
717
  for period in operating_periods:
718
  start_hour = period.get("start", 8)
 
725
  for comp_list in components.values():
726
  for comp in comp_list:
727
  cool_load, _ = TFMCalculations.calculate_conduction_load(comp, outdoor_temp, indoor_temp, hour, mode="cooling")
 
728
  component_solar_load = TFMCalculations.calculate_solar_load(comp, hour_data, hour, building_orientation, mode="cooling")
729
+ orientation = classify_azimuth(comp.get("surface_azimuth", 0))
730
+ element = TFMCalculations.get_component_type(comp).name
731
+ key = orientation if element in ["WALL", "WINDOW"] else element
732
+ conduction_by_orientation[key] += cool_load
733
+ solar_by_orientation[key] += component_solar_load
734
+ conduction_cooling += cool_load
735
  solar += component_solar_load
736
  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")
737
 
 
742
  for comp_list in components.values():
743
  for comp in comp_list:
744
  _, heat_load = TFMCalculations.calculate_conduction_load(comp, outdoor_temp, indoor_temp, hour, mode="heating")
745
+ orientation = classify_azimuth(comp.get("surface_azimuth", 0))
746
+ element = TFMCalculations.get_component_type(comp).name
747
+ key = orientation if element in ["WALL", "WINDOW"] else element
748
+ conduction_by_orientation[key] += heat_load
749
  conduction_heating += heat_load
750
  internal = TFMCalculations.calculate_internal_load(internal_loads, hour, max([p["end"] - p["start"] for p in operating_periods]), area)
751
  _, ventilation_heating = TFMCalculations.calculate_ventilation_load(internal_loads, outdoor_temp, indoor_temp, area, building_info, hour, mode="heating")
 
775
  "infiltration_heating": infiltration_heating,
776
  "total_cooling": total_cooling,
777
  "total_heating": total_heating,
778
+ "ground_temperature": ground_temp,
779
+ "solar_by_orientation": dict(solar_by_orientation),
780
+ "conduction_by_orientation": dict(conduction_by_orientation)
781
  })
782
  loads_by_day = defaultdict(list)
783
  for load in temp_loads:
 
798
  final_loads.append(load)
799
  return final_loads
800
 
801
+ def make_pie(data: Dict[str, float], title: str) -> px.pie:
802
+ """Create a Plotly pie chart from a dictionary of values."""
803
+ fig = px.pie(
804
+ names=list(data.keys()),
805
+ values=list(data.values()),
806
+ title=title,
807
+ hole=0.4
808
+ )
809
+ fig.update_traces(textinfo='percent+label', hoverinfo='label+percent+value')
810
+ return fig
811
+
812
+ def display_hvac_results_ui(loads: List[Dict[str, Any]]):
813
+ """Display HVAC load results with enhanced UI elements."""
814
+ st.subheader("HVAC Load Results")
815
+
816
+ # Monthly Load Graph
817
+ st.subheader("Monthly Heating and Cooling Load")
818
+ monthly_df = pd.DataFrame(loads).groupby("month").agg({
819
+ "total_cooling": "sum",
820
+ "total_heating": "sum"
821
+ }).reset_index()
822
+ monthly_df = monthly_df.rename(columns={"total_cooling": "Cooling", "total_heating": "Heating"})
823
+ fig = px.line(monthly_df, x="month", y=["Cooling", "Heating"], markers=True, title="Monthly Load Summary")
824
+ fig.update_xaxes(title="Month", tickvals=list(range(1, 13)))
825
+ fig.update_yaxes(title="Load (kW)")
826
+ st.plotly_chart(fig)
827
+ st.session_state.project_data["hvac_loads"]["monthly_summary"] = monthly_df.to_dict()
828
+
829
+ # Two-Column Layout for Equipment Sizing and Pie Charts
830
+ col1, col2 = st.columns(2)
831
+ with col1:
832
+ st.subheader("Equipment Sizing")
833
+ cooling_loads = [load for load in loads if load["total_cooling"] > 0]
834
+ heating_loads = [load for load in loads if load["total_heating"] > 0]
835
+ peak_cooling = max(cooling_loads, key=lambda x: x["total_cooling"]) if cooling_loads else None
836
+ peak_heating = max(heating_loads, key=lambda x: x["total_heating"]) if heating_loads else None
837
+ if peak_cooling:
838
+ st.write(f"**Peak Cooling Load**: {peak_cooling['total_cooling']:.2f} kW")
839
+ st.write(f"Occurred on: {peak_cooling['month']}/{peak_cooling['day']} at {peak_cooling['hour']}:00")
840
+ else:
841
+ st.write("**Peak Cooling Load**: 0.00 kW")
842
+ if peak_heating:
843
+ st.write(f"**Peak Heating Load**: {peak_heating['total_heating']:.2f} kW")
844
+ st.write(f"Occurred on: {peak_heating['month']}/{peak_heating['day']} at {peak_heating['hour']}:00")
845
+ else:
846
+ st.write("**Peak Heating Load**: 0.00 kW")
847
+
848
+ with col2:
849
+ st.subheader("Cooling Load Breakdown")
850
+ cooling_breakdown = {
851
+ "Conduction": sum(load["conduction_cooling"] for load in cooling_loads),
852
+ "Solar Gains": sum(load["solar"] for load in cooling_loads),
853
+ "Internal": sum(load["internal"] for load in cooling_loads),
854
+ "Ventilation": sum(load["ventilation_cooling"] for load in cooling_loads),
855
+ "Infiltration": sum(load["infiltration_cooling"] for load in cooling_loads)
856
+ }
857
+ st.markdown("**Conduction**", help="Heat transfer through building envelope materials.")
858
+ st.markdown("**Solar Gains**", help="Radiative heat gains from sun through surfaces and windows.")
859
+ st.markdown("**Ventilation**", help="Loads due to outdoor air entering through ventilation systems.")
860
+ st.markdown("**Infiltration**", help="Loads due to unintended air leakage through the building envelope.")
861
+ st.markdown("**Internal**", help="Heat gains from occupants, lighting, and equipment inside the building.")
862
+ cooling_pie = make_pie({k: v for k, v in cooling_breakdown.items() if v > 0}, "Cooling Load Components")
863
+ st.plotly_chart(cooling_pie)
864
+ st.session_state.project_data["hvac_loads"]["cooling"]["charts"]["pie_by_component"] = cooling_breakdown
865
+
866
+ # Second Row for Heating and Orientation Breakdown
867
+ col3, col4 = st.columns(2)
868
+ with col3:
869
+ st.subheader("Heating Load Breakdown")
870
+ heating_breakdown = {
871
+ "Conduction": sum(load["conduction_heating"] for load in heating_loads),
872
+ "Ventilation": sum(load["ventilation_heating"] for load in heating_loads),
873
+ "Infiltration": sum(load["infiltration_heating"] for load in heating_loads)
874
+ }
875
+ heating_pie = make_pie({k: v for k, v in heating_breakdown.items() if v > 0}, "Heating Load Components")
876
+ st.plotly_chart(heating_pie)
877
+ st.session_state.project_data["hvac_loads"]["heating"]["charts"]["pie_by_component"] = heating_breakdown
878
+
879
+ with col4:
880
+ st.subheader("Cooling Load by Orientation")
881
+ orientation_solar = defaultdict(float)
882
+ orientation_conduction = defaultdict(float)
883
+ for load in cooling_loads:
884
+ for key, value in load["solar_by_orientation"].items():
885
+ orientation_solar[key] += value
886
+ for key, value in load["conduction_by_orientation"].items():
887
+ orientation_conduction[key] += value
888
+ orientation_breakdown = {k: orientation_solar[k] + orientation_conduction[k] for k in set(orientation_solar) | set(orientation_conduction)}
889
+ orientation_pie = make_pie({k: v for k, v in orientation_breakdown.items() if v > 0}, "Cooling Load by Orientation/Component")
890
+ st.plotly_chart(orientation_pie)
891
+ st.session_state.project_data["hvac_loads"]["cooling"]["charts"]["pie_by_orientation"] = orientation_breakdown
892
+
893
+ # Interactive Filtering
894
+ st.subheader("Explore Hourly Loads")
895
+ df = pd.DataFrame(loads)
896
+ col5, col6 = st.columns(2)
897
+ with col5:
898
+ selected_month = st.selectbox("Select Month", sorted(df["month"].unique()), key="filter_month")
899
+ with col6:
900
+ selected_day = st.selectbox("Select Day", sorted(df[df["month"] == selected_month]["day"].unique()), key="filter_day")
901
+ filtered_df = df[(df["month"] == selected_month) & (df["day"] == selected_day)]
902
+ st.dataframe(filtered_df[[
903
+ "hour", "total_cooling", "total_heating", "conduction_cooling", "solar", "internal",
904
+ "ventilation_cooling", "infiltration_cooling", "ventilation_heating", "infiltration_heating"
905
+ ]])
906
+
907
+ # CSV Export
908
+ csv = filtered_df.to_csv(index=False)
909
+ st.download_button("Download Hourly Summary as CSV", data=csv, file_name=f"hourly_loads_{selected_month}_{selected_day}.csv")
910
+
911
  def display_hvac_loads_page():
912
  """
913
  Display the HVAC Loads page in the Streamlit application.
 
1111
  # Calculate HVAC Loads
1112
  if st.button("Calculate HVAC Loads"):
1113
  try:
1114
+ with st.spinner("Running simulation... this may take up to a minute depending on data size."):
1115
+ components = st.session_state.project_data["components"]
1116
+ hourly_data = st.session_state.project_data["climate_data"]["hourly_data"]
1117
+ indoor_conditions = st.session_state.project_data["indoor_conditions"]
1118
+ internal_loads = st.session_state.project_data["internal_loads"]
1119
+ building_info = st.session_state.project_data["building_info"]
1120
+ sim_period = st.session_state.project_data["sim_period"]
1121
+ hvac_settings = st.session_state.project_data["hvac_settings"]
1122
+
1123
+ if not hourly_data:
1124
+ st.error("No climate data available. Please configure climate data first.")
1125
+ logger.error("HVAC calculation failed: No climate data available")
1126
+ return
1127
+ elif not any(comp_list for comp_list in components.values()):
1128
+ st.error("No building components defined. Please configure components first.")
1129
+ logger.error("HVAC calculation failed: No building components defined")
1130
+ return
1131
+ else:
1132
+ loads = TFMCalculations.calculate_tfm_loads(
1133
+ components=components,
1134
+ hourly_data=hourly_data,
1135
+ indoor_conditions=indoor_conditions,
1136
+ internal_loads=internal_loads,
1137
+ building_info=building_info,
1138
+ sim_period=sim_period,
1139
+ hvac_settings=hvac_settings
1140
+ )
1141
+
1142
+ # Update session state with results
1143
+ cooling_loads = [load for load in loads if load["total_cooling"] > 0]
1144
+ heating_loads = [load for load in loads if load["total_heating"] > 0]
1145
+ st.session_state.project_data["hvac_loads"]["cooling"]["hourly"] = cooling_loads
1146
+ st.session_state.project_data["hvac_loads"]["heating"]["hourly"] = heating_loads
1147
+ st.session_state.project_data["hvac_loads"]["cooling"]["peak"] = max([load["total_cooling"] for load in cooling_loads], default=0)
1148
+ st.session_state.project_data["hvac_loads"]["heating"]["peak"] = max([load["total_heating"] for load in heating_loads], default=0)
1149
+ st.session_state.project_data["hvac_loads"]["cooling"]["charts"] = {}
1150
+ st.session_state.project_data["hvac_loads"]["heating"]["charts"] = {}
1151
+
1152
+ # Store breakdown
1153
+ cooling_breakdown = {
1154
+ "conduction": sum(load["conduction_cooling"] for load in cooling_loads),
1155
+ "solar": sum(load["solar"] for load in cooling_loads if load["total_cooling"] > 0),
1156
+ "internal": sum(load["internal"] for load in cooling_loads if load["total_cooling"] > 0),
1157
+ "ventilation": sum(load["ventilation_cooling"] for load in cooling_loads),
1158
+ "infiltration": sum(load["infiltration_cooling"] for load in cooling_loads)
1159
+ }
1160
+ heating_breakdown = {
1161
+ "conduction": sum(load["conduction_heating"] for load in heating_loads),
1162
+ "ventilation": sum(load["ventilation_heating"] for load in heating_loads),
1163
+ "infiltration": sum(load["infiltration_heating"] for load in heating_loads)
1164
+ }
1165
+ st.session_state.project_data["hvac_loads"]["cooling"]["breakdown"] = cooling_breakdown
1166
+ st.session_state.project_data["hvac_loads"]["heating"]["breakdown"] = heating_breakdown
1167
+
1168
+ # Display Results UI
1169
+ display_hvac_results_ui(loads)
1170
+
1171
+ st.success("HVAC loads calculated successfully.")
1172
+ logger.info("HVAC loads calculated and stored in session state")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1173
 
1174
  except Exception as e:
1175
  st.error(f"Error calculating HVAC loads: {str(e)}")