Spaces:
Sleeping
Sleeping
Update app/hvac_loads.py
Browse files- app/hvac_loads.py +100 -10
app/hvac_loads.py
CHANGED
@@ -115,17 +115,48 @@ class TFMCalculations:
|
|
115 |
return component_type
|
116 |
|
117 |
@staticmethod
|
118 |
-
def calculate_conduction_load(component: Dict[str, Any], outdoor_temp: float, indoor_temp: float, hour: int, mode: str = "none") -> tuple[float, float]:
|
119 |
"""Calculate conduction load for heating and cooling in kW based on mode."""
|
120 |
if mode == "none":
|
121 |
return 0, 0
|
|
|
122 |
component_name = component.get('name', 'unnamed_component')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
123 |
delta_t = outdoor_temp - indoor_temp
|
124 |
if mode == "cooling" and delta_t <= 0:
|
125 |
return 0, 0
|
126 |
if mode == "heating" and delta_t >= 0:
|
127 |
return 0, 0
|
128 |
-
|
129 |
try:
|
130 |
# Get CTF coefficients, preferring stored value
|
131 |
ctf = component.get('ctf')
|
@@ -143,7 +174,7 @@ class TFMCalculations:
|
|
143 |
heating_load = -load / 1000 if mode == "heating" else 0
|
144 |
logger.info(f"Conduction load for {component_name} at hour {hour}: cooling={cooling_load:.3f} kW, heating={heating_load:.3f} kW")
|
145 |
return cooling_load, heating_load
|
146 |
-
|
147 |
except Exception as e:
|
148 |
logger.error(f"Error calculating conduction load for {component_name} at hour {hour}: {str(e)}")
|
149 |
return 0, 0
|
@@ -234,12 +265,14 @@ class TFMCalculations:
|
|
234 |
"""Calculate solar load in kW (cooling only) using ASHRAE-compliant solar calculations."""
|
235 |
if mode != "cooling":
|
236 |
return 0
|
237 |
-
|
238 |
component_type = TFMCalculations.get_component_type(component)
|
239 |
-
if component_type == ComponentType.FLOOR:
|
240 |
-
return 0
|
241 |
-
|
242 |
component_name = component.get('name', 'unnamed_component')
|
|
|
|
|
|
|
|
|
|
|
243 |
|
244 |
try:
|
245 |
material_library = st.session_state.get("material_library")
|
@@ -862,7 +895,7 @@ class TFMCalculations:
|
|
862 |
if is_operating and mode == "cooling":
|
863 |
for comp_list in components.values():
|
864 |
for comp in comp_list:
|
865 |
-
cool_load, _ = TFMCalculations.calculate_conduction_load(comp, outdoor_temp, indoor_temp, hour, mode="cooling")
|
866 |
component_solar_load = TFMCalculations.calculate_solar_load(comp, hour_data, hour, building_orientation, mode="cooling")
|
867 |
orientation = classify_azimuth(comp.get("surface_azimuth", 0))
|
868 |
element = TFMCalculations.get_component_type(comp).name
|
@@ -872,14 +905,14 @@ class TFMCalculations:
|
|
872 |
conduction_cooling += cool_load
|
873 |
solar += component_solar_load
|
874 |
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")
|
875 |
-
|
876 |
internal = TFMCalculations.calculate_internal_load(internal_loads, hour, max([p["end"] - p["start"] for p in operating_periods]), area)
|
877 |
ventilation_cooling, _ = TFMCalculations.calculate_ventilation_load(internal_loads, outdoor_temp, indoor_temp, area, building_info, hour, mode="cooling")
|
878 |
infiltration_cooling, _ = TFMCalculations.calculate_infiltration_load(internal_loads, outdoor_temp, indoor_temp, area, building_info, hour, mode="cooling")
|
879 |
elif is_operating and mode == "heating":
|
880 |
for comp_list in components.values():
|
881 |
for comp in comp_list:
|
882 |
-
_, heat_load = TFMCalculations.calculate_conduction_load(comp, outdoor_temp, indoor_temp, hour, mode="heating")
|
883 |
orientation = classify_azimuth(comp.get("surface_azimuth", 0))
|
884 |
element = TFMCalculations.get_component_type(comp).name
|
885 |
key = orientation if element in ["WALL", "WINDOW"] else element
|
@@ -1421,6 +1454,63 @@ def display_hvac_loads_page():
|
|
1421 |
st.success("Internal loads conditions saved successfully.")
|
1422 |
logger.info("Internal loads conditions updated in session state.")
|
1423 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1424 |
# Calculate HVAC Loads
|
1425 |
if st.button("Calculate HVAC Loads"):
|
1426 |
try:
|
|
|
115 |
return component_type
|
116 |
|
117 |
@staticmethod
|
118 |
+
def calculate_conduction_load(component: Dict[str, Any], outdoor_temp: float, indoor_temp: float, hour: int, month: int, mode: str = "none") -> tuple[float, float]:
|
119 |
"""Calculate conduction load for heating and cooling in kW based on mode."""
|
120 |
if mode == "none":
|
121 |
return 0, 0
|
122 |
+
|
123 |
component_name = component.get('name', 'unnamed_component')
|
124 |
+
component_type = TFMCalculations.get_component_type(component)
|
125 |
+
|
126 |
+
# Validate adiabatic and ground_contact mutual exclusivity
|
127 |
+
if component.get('adiabatic', False) and component.get('ground_contact', False):
|
128 |
+
logger.warning(f"Component {component_name} has both adiabatic and ground_contact set to True. Treating as adiabatic, setting ground_contact to False.")
|
129 |
+
component['ground_contact'] = False
|
130 |
+
|
131 |
+
# Skip for adiabatic components
|
132 |
+
if component.get('adiabatic', False):
|
133 |
+
logger.info(f"Skipping conduction load calculation for adiabatic component {component_name} at hour {hour}")
|
134 |
+
return 0, 0
|
135 |
+
|
136 |
+
# Handle ground-contact surfaces
|
137 |
+
if component.get('ground_contact', False):
|
138 |
+
valid_types = [ComponentType.WALL, ComponentType.ROOF, ComponentType.FLOOR]
|
139 |
+
if component_type not in valid_types:
|
140 |
+
logger.warning(f"Invalid ground-contact component type '{component_type.value}' for {component_name}. Using outdoor temperature {outdoor_temp:.2f}°C.")
|
141 |
+
else:
|
142 |
+
# Retrieve ground temperature
|
143 |
+
climate_data = st.session_state.project_data.get("climate_data", {})
|
144 |
+
ground_temperatures = climate_data.get("ground_temperatures", {})
|
145 |
+
depth = "2" # Default depth
|
146 |
+
default_temps = {"0.5": 20.0, "2": 18.0, "4": 16.0}
|
147 |
+
if depth not in ground_temperatures or not ground_temperatures[depth]:
|
148 |
+
logger.warning(f"No ground temperature data for depth {depth} m for month {month}. Using default {default_temps[depth]}°C.")
|
149 |
+
outdoor_temp = default_temps[depth]
|
150 |
+
else:
|
151 |
+
outdoor_temp = ground_temperatures[depth][month-1] if len(ground_temperatures[depth]) >= month else default_temps[depth]
|
152 |
+
logger.info(f"Ground-contact component {component_name} at hour {hour}, month {month}: using ground temperature {outdoor_temp:.2f}°C")
|
153 |
+
|
154 |
delta_t = outdoor_temp - indoor_temp
|
155 |
if mode == "cooling" and delta_t <= 0:
|
156 |
return 0, 0
|
157 |
if mode == "heating" and delta_t >= 0:
|
158 |
return 0, 0
|
159 |
+
|
160 |
try:
|
161 |
# Get CTF coefficients, preferring stored value
|
162 |
ctf = component.get('ctf')
|
|
|
174 |
heating_load = -load / 1000 if mode == "heating" else 0
|
175 |
logger.info(f"Conduction load for {component_name} at hour {hour}: cooling={cooling_load:.3f} kW, heating={heating_load:.3f} kW")
|
176 |
return cooling_load, heating_load
|
177 |
+
|
178 |
except Exception as e:
|
179 |
logger.error(f"Error calculating conduction load for {component_name} at hour {hour}: {str(e)}")
|
180 |
return 0, 0
|
|
|
265 |
"""Calculate solar load in kW (cooling only) using ASHRAE-compliant solar calculations."""
|
266 |
if mode != "cooling":
|
267 |
return 0
|
268 |
+
|
269 |
component_type = TFMCalculations.get_component_type(component)
|
|
|
|
|
|
|
270 |
component_name = component.get('name', 'unnamed_component')
|
271 |
+
|
272 |
+
# Skip for floors, adiabatic, or ground-contact components
|
273 |
+
if component_type == ComponentType.FLOOR or component.get('adiabatic', False) or component.get('ground_contact', False):
|
274 |
+
logger.info(f"Skipping solar load calculation for {component_name} at hour {hour} (type={component_type.value}, adiabatic={component.get('adiabatic', False)}, ground_contact={component.get('ground_contact', False)})")
|
275 |
+
return 0
|
276 |
|
277 |
try:
|
278 |
material_library = st.session_state.get("material_library")
|
|
|
895 |
if is_operating and mode == "cooling":
|
896 |
for comp_list in components.values():
|
897 |
for comp in comp_list:
|
898 |
+
cool_load, _ = TFMCalculations.calculate_conduction_load(comp, outdoor_temp, indoor_temp, hour, month, mode="cooling")
|
899 |
component_solar_load = TFMCalculations.calculate_solar_load(comp, hour_data, hour, building_orientation, mode="cooling")
|
900 |
orientation = classify_azimuth(comp.get("surface_azimuth", 0))
|
901 |
element = TFMCalculations.get_component_type(comp).name
|
|
|
905 |
conduction_cooling += cool_load
|
906 |
solar += component_solar_load
|
907 |
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")
|
908 |
+
|
909 |
internal = TFMCalculations.calculate_internal_load(internal_loads, hour, max([p["end"] - p["start"] for p in operating_periods]), area)
|
910 |
ventilation_cooling, _ = TFMCalculations.calculate_ventilation_load(internal_loads, outdoor_temp, indoor_temp, area, building_info, hour, mode="cooling")
|
911 |
infiltration_cooling, _ = TFMCalculations.calculate_infiltration_load(internal_loads, outdoor_temp, indoor_temp, area, building_info, hour, mode="cooling")
|
912 |
elif is_operating and mode == "heating":
|
913 |
for comp_list in components.values():
|
914 |
for comp in comp_list:
|
915 |
+
_, heat_load = TFMCalculations.calculate_conduction_load(comp, outdoor_temp, indoor_temp, hour, month, mode="heating")
|
916 |
orientation = classify_azimuth(comp.get("surface_azimuth", 0))
|
917 |
element = TFMCalculations.get_component_type(comp).name
|
918 |
key = orientation if element in ["WALL", "WINDOW"] else element
|
|
|
1454 |
st.success("Internal loads conditions saved successfully.")
|
1455 |
logger.info("Internal loads conditions updated in session state.")
|
1456 |
|
1457 |
+
# Ground Temperature Configuration
|
1458 |
+
st.subheader("Ground Temperature Configuration")
|
1459 |
+
has_ground_contact = any(
|
1460 |
+
comp.get('ground_contact', False)
|
1461 |
+
for comp_list in st.session_state.project_data["components"].values()
|
1462 |
+
for comp in comp_list
|
1463 |
+
)
|
1464 |
+
if has_ground_contact:
|
1465 |
+
st.markdown("Configure monthly ground temperatures for components in contact with the ground (e.g., floors, walls, roofs). Typical ranges are 10–20°C at 2 m depth (ASHRAE Fundamentals, Chapter 18).")
|
1466 |
+
|
1467 |
+
depth_options = ["0.5", "2", "4"]
|
1468 |
+
default_depth = "2"
|
1469 |
+
selected_depth = st.selectbox(
|
1470 |
+
"Ground Temperature Depth (m)",
|
1471 |
+
options=depth_options,
|
1472 |
+
index=depth_options.index(default_depth),
|
1473 |
+
key="ground_temp_depth"
|
1474 |
+
)
|
1475 |
+
|
1476 |
+
climate_data = st.session_state.project_data.get("climate_data", {})
|
1477 |
+
ground_temperatures = climate_data.get("ground_temperatures", {})
|
1478 |
+
default_temps = {"0.5": [20.0]*12, "2": [18.0]*12, "4": [16.0]*12}
|
1479 |
+
|
1480 |
+
if selected_depth not in ground_temperatures or not ground_temperatures[selected_depth]:
|
1481 |
+
st.warning(f"No ground temperature data available for depth {selected_depth} m. Using default temperatures: {default_temps[selected_depth][0]}°C.")
|
1482 |
+
monthly_temps = default_temps[selected_depth]
|
1483 |
+
else:
|
1484 |
+
monthly_temps = ground_temperatures[selected_depth]
|
1485 |
+
|
1486 |
+
st.write("Enter monthly ground temperatures (°C):")
|
1487 |
+
months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
|
1488 |
+
temp_inputs = []
|
1489 |
+
cols = st.columns(12)
|
1490 |
+
for i, month in enumerate(months):
|
1491 |
+
with cols[i]:
|
1492 |
+
temp = st.number_input(
|
1493 |
+
month,
|
1494 |
+
min_value=-20.0,
|
1495 |
+
max_value=40.0,
|
1496 |
+
value=monthly_temps[i],
|
1497 |
+
step=0.1,
|
1498 |
+
key=f"ground_temp_{selected_depth}_{month}"
|
1499 |
+
)
|
1500 |
+
temp_inputs.append(temp)
|
1501 |
+
|
1502 |
+
if st.button("Save Ground Temperatures"):
|
1503 |
+
if len(temp_inputs) != 12:
|
1504 |
+
st.error("Please provide temperatures for all 12 months.")
|
1505 |
+
elif any(not -20.0 <= t <= 40.0 for t in temp_inputs):
|
1506 |
+
st.error("All temperatures must be between -20°C and 40°C.")
|
1507 |
+
else:
|
1508 |
+
st.session_state.project_data["climate_data"]["ground_temperatures"][selected_depth] = temp_inputs
|
1509 |
+
st.success(f"Ground temperatures for depth {selected_depth} m saved successfully.")
|
1510 |
+
logger.info(f"Ground temperatures for depth {selected_depth} m updated in session state")
|
1511 |
+
else:
|
1512 |
+
st.info("No ground-contact components detected. Ground temperature configuration is not required.")
|
1513 |
+
|
1514 |
# Calculate HVAC Loads
|
1515 |
if st.button("Calculate HVAC Loads"):
|
1516 |
try:
|