Spaces:
Sleeping
Sleeping
Update app/hvac_loads.py
Browse files- 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 |
-
|
675 |
-
|
676 |
-
|
677 |
-
|
678 |
-
|
679 |
-
|
680 |
-
|
681 |
-
|
682 |
-
|
683 |
-
|
684 |
-
|
685 |
-
|
686 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
687 |
|
688 |
-
|
689 |
-
|
690 |
-
|
691 |
-
|
692 |
-
|
693 |
-
|
694 |
-
|
695 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
696 |
else:
|
697 |
-
|
698 |
-
|
699 |
-
|
700 |
-
|
701 |
-
|
702 |
-
|
703 |
-
|
704 |
-
|
705 |
-
|
706 |
-
|
707 |
-
|
708 |
-
|
709 |
-
|
710 |
-
|
711 |
-
|
|
|
|
|
712 |
|
713 |
-
|
714 |
-
indoor_temp =
|
715 |
-
|
716 |
-
|
717 |
-
|
718 |
-
|
719 |
-
|
720 |
-
|
721 |
-
|
722 |
-
|
723 |
-
|
724 |
-
|
725 |
-
|
726 |
-
|
727 |
-
|
728 |
-
|
729 |
-
|
730 |
-
|
731 |
-
|
732 |
-
|
733 |
-
|
734 |
-
|
735 |
-
|
736 |
-
|
737 |
-
|
738 |
-
|
739 |
-
|
740 |
-
|
741 |
-
|
742 |
-
|
743 |
-
|
744 |
-
|
745 |
-
|
746 |
-
|
747 |
-
|
748 |
-
|
749 |
-
|
750 |
-
|
751 |
-
|
752 |
-
|
753 |
-
|
754 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
755 |
else:
|
756 |
-
|
757 |
-
|
758 |
-
|
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."""
|