""" Heat transfer calculation module for HVAC Load Calculator. This module implements heat transfer calculations for conduction, infiltration, and solar effects. Reference: ASHRAE Handbook—Fundamentals (2017), Chapters 16 and 18. """ from typing import Dict, List, Any, Optional, Tuple import math import numpy as np import logging from dataclasses import dataclass # Configure logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # Import utility modules from utils.psychrometrics import Psychrometrics # Import data modules from data.building_components import Orientation class SolarCalculations: """Class for solar geometry and radiation calculations.""" def validate_angle(self, angle: float, name: str, min_val: float, max_val: float) -> None: """ Validate angle inputs for solar calculations. Args: angle: Angle in degrees name: Name of the angle min_val: Minimum allowed value max_val: Maximum allowed value Raises: ValueError: If angle is out of range """ if not min_val <= angle <= max_val: raise ValueError(f"{name} {angle}° must be between {min_val}° and {max_val}°") def solar_declination(self, day_of_year: int) -> float: """ Calculate solar declination angle. Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 14, Equation 14.6. Args: day_of_year: Day of the year (1-365) Returns: Declination angle in degrees """ if not 1 <= day_of_year <= 365: raise ValueError("Day of year must be between 1 and 365") declination = 23.45 * math.sin(math.radians(360 * (284 + day_of_year) / 365)) self.validate_angle(declination, "Declination angle", -23.45, 23.45) return declination def solar_hour_angle(self, hour: float) -> float: """ Calculate solar hour angle. Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 14, Equation 14.7. Args: hour: Hour of the day (0-23) Returns: Hour angle in degrees """ if not 0 <= hour <= 24: raise ValueError("Hour must be between 0 and 24") hour_angle = (hour - 12) * 15 self.validate_angle(hour_angle, "Hour angle", -180, 180) return hour_angle def solar_altitude(self, latitude: float, declination: float, hour_angle: float) -> float: """ Calculate solar altitude angle. Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 14, Equation 14.8. Args: latitude: Latitude in degrees declination: Declination angle in degrees hour_angle: Hour angle in degrees Returns: Altitude angle in degrees """ self.validate_angle(latitude, "Latitude", -90, 90) self.validate_angle(declination, "Declination", -23.45, 23.45) self.validate_angle(hour_angle, "Hour angle", -180, 180) sin_beta = (math.sin(math.radians(latitude)) * math.sin(math.radians(declination)) + math.cos(math.radians(latitude)) * math.cos(math.radians(declination)) * math.cos(math.radians(hour_angle))) beta = math.degrees(math.asin(sin_beta)) self.validate_angle(beta, "Altitude angle", 0, 90) return beta def solar_azimuth(self, latitude: float, declination: float, hour_angle: float, altitude: float) -> float: """ Calculate solar azimuth angle. Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 14, Equation 14.9. Args: latitude: Latitude in degrees declination: Declination angle in degrees hour_angle: Hour angle in degrees altitude: Altitude angle in degrees Returns: Azimuth angle in degrees """ self.validate_angle(latitude, "Latitude", -90, 90) self.validate_angle(declination, "Declination", -23.45, 23.45) self.validate_angle(hour_angle, "Hour angle", -180, 180) self.validate_angle(altitude, "Altitude", 0, 90) sin_phi = (math.cos(math.radians(declination)) * math.sin(math.radians(hour_angle)) / math.cos(math.radians(altitude))) phi = math.degrees(math.asin(sin_phi)) if hour_angle > 0: phi = 180 - phi elif hour_angle < 0: phi = -180 - phi self.validate_angle(phi, "Azimuth angle", -180, 180) return phi class HeatTransferCalculations: """Class for heat transfer calculations.""" def __init__(self): """ Initialize heat transfer calculations with psychrometrics and solar calculations. Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 16. """ self.psychrometrics = Psychrometrics() self.solar = SolarCalculations() self.debug_mode = False def conduction_heat_transfer(self, u_value: float, area: float, delta_t: float) -> float: """ Calculate heat transfer via conduction. Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 18, Equation 18.1. Args: u_value: U-value of the component in W/(m²·K) area: Area of the component in m² delta_t: Temperature difference in °C Returns: Heat transfer rate in W """ if u_value < 0 or area < 0: raise ValueError("U-value and area must be non-negative") q = u_value * area * delta_t return q def infiltration_heat_transfer(self, flow_rate: float, delta_t: float, t_db: float, rh: float, p_atm: float = 101325) -> float: """ Calculate sensible heat transfer due to infiltration or ventilation. Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 18, Equation 18.5. Args: flow_rate: Air flow rate in m³/s delta_t: Temperature difference in °C t_db: Dry-bulb temperature for air properties in °C rh: Relative humidity in % (0-100) p_atm: Atmospheric pressure in Pa Returns: Sensible heat transfer rate in W """ if flow_rate < 0: raise ValueError("Flow rate cannot be negative") # Calculate air density and specific heat using psychrometrics w = self.psychrometrics.humidity_ratio(t_db, rh, p_atm) rho = self.psychrometrics.density(t_db, w, p_atm) c_p = 1006 + 1860 * w # Specific heat of moist air in J/(kg·K) q = flow_rate * rho * c_p * delta_t return q def infiltration_latent_heat_transfer(self, flow_rate: float, delta_w: float, t_db: float, rh: float, p_atm: float = 101325) -> float: """ Calculate latent heat transfer due to infiltration or ventilation. Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 18, Equation 18.6. Args: flow_rate: Air flow rate in m³/s delta_w: Humidity ratio difference in kg/kg t_db: Dry-bulb temperature for air properties in °C rh: Relative humidity in % (0-100) p_atm: Atmospheric pressure in Pa Returns: Latent heat transfer rate in W """ if flow_rate < 0 or delta_w < 0: raise ValueError("Flow rate and humidity ratio difference cannot be negative") # Calculate air density and latent heat w = self.psychrometrics.humidity_ratio(t_db, rh, p_atm) rho = self.psychrometrics.density(t_db, w, p_atm) h_fg = 2501000 + 1840 * t_db # Latent heat of vaporization in J/kg q = flow_rate * rho * h_fg * delta_w return q def wind_pressure_difference(self, wind_speed: float) -> float: """ Calculate pressure difference due to wind. Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 16, Equation 16.3. Args: wind_speed: Wind speed in m/s Returns: Pressure difference in Pa """ if wind_speed < 0: raise ValueError("Wind speed cannot be negative") c_p = 0.6 # Wind pressure coefficient rho_air = 1.2 # Air density at standard conditions in kg/m³ delta_p = 0.5 * c_p * rho_air * wind_speed**2 return delta_p def stack_pressure_difference(self, height: float, t_inside: float, t_outside: float) -> float: """ Calculate pressure difference due to stack effect. Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 16, Equation 16.4. Args: height: Height of the building in m t_inside: Inside temperature in K t_outside: Outside temperature in K Returns: Pressure difference in Pa """ if height < 0 or t_inside <= 0 or t_outside <= 0: raise ValueError("Height and temperatures must be positive") g = 9.81 # Gravitational acceleration in m/s² rho_air = 1.2 # Air density at standard conditions in kg/m³ delta_p = rho_air * g * height * (1 / t_outside - 1 / t_inside) return delta_p def combined_pressure_difference(self, wind_pd: float, stack_pd: float) -> float: """ Calculate combined pressure difference from wind and stack effects. Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 16, Section 16.2. Args: wind_pd: Wind pressure difference in Pa stack_pd: Stack pressure difference in Pa Returns: Combined pressure difference in Pa """ delta_p = math.sqrt(wind_pd**2 + stack_pd**2) return delta_p def crack_method_infiltration(self, crack_length: float, crack_width: float, delta_p: float) -> float: """ Calculate infiltration flow rate using crack method. Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 16, Equation 16.5. Args: crack_length: Length of cracks in m crack_width: Width of cracks in m delta_p: Pressure difference across cracks in Pa Returns: Infiltration flow rate in m³/s """ if crack_length < 0 or crack_width < 0 or delta_p < 0: raise ValueError("Crack dimensions and pressure difference cannot be negative") c_d = 0.65 # Discharge coefficient area = crack_length * crack_width rho_air = 1.2 # Air density at standard conditions in kg/m³ q = c_d * area * math.sqrt(2 * delta_p / rho_air) return q # Example usage if __name__ == "__main__": heat_transfer = HeatTransferCalculations() heat_transfer.debug_mode = True # Example conduction calculation u_value = 0.5 # W/(m²·K) area = 20.0 # m² delta_t = 26.0 # °C q_conduction = heat_transfer.conduction_heat_transfer(u_value, area, delta_t) logger.info(f"Conduction heat transfer: {q_conduction:.2f} W") # Example infiltration calculation flow_rate = 0.05 # m³/s delta_t = 26.0 # °C t_db = 21.0 # °C rh = 40.0 # % p_atm = 101325 # Pa q_infiltration = heat_transfer.infiltration_heat_transfer(flow_rate, delta_t, t_db, rh, p_atm) logger.info(f"Infiltration sensible heat transfer: {q_infiltration:.2f} W") # Example solar calculation latitude = 40.0 # degrees day_of_year = 172 # June 21 hour = 12.0 # Noon declination = heat_transfer.solar.solar_declination(day_of_year) hour_angle = heat_transfer.solar.solar_hour_angle(hour) altitude = heat_transfer.solar.solar_altitude(latitude, declination, hour_angle) azimuth = heat_transfer.solar.solar_azimuth(latitude, declination, hour_angle, altitude) logger.info(f"Solar altitude: {altitude:.2f}°, Azimuth: {azimuth:.2f}°")