Spaces:
Sleeping
Sleeping
| """ | |
| Heat transfer calculation module for HVAC Load Calculator. | |
| This module provides enhanced calculations for conduction, convection, radiation, | |
| infiltration, and solar geometry, with improved modularity and error handling. | |
| """ | |
| from typing import Optional, Tuple | |
| import math | |
| import numpy as np | |
| class SolarCalculations: | |
| """Class for solar geometry and irradiance calculations.""" | |
| def __init__(self): | |
| """Initialize solar calculations with cached values.""" | |
| self._declination_cache = {} # Cache for declination by day of year | |
| def validate_angle(self, angle: float, name: str, min_val: float, max_val: float) -> None: | |
| """ | |
| Validate an angle input. | |
| Args: | |
| angle: Angle in degrees | |
| name: Name of the angle for error messages | |
| 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}° is outside valid range ({min_val} to {max_val}°)") | |
| def solar_declination(self, day_of_year: int) -> float: | |
| """ | |
| Calculate solar declination angle for a given day of the year. | |
| Args: | |
| day_of_year: Day of the year (1-365) | |
| Returns: | |
| Solar declination angle in degrees | |
| """ | |
| if not 1 <= day_of_year <= 365: | |
| raise ValueError(f"Day of year {day_of_year} must be between 1 and 365") | |
| if day_of_year in self._declination_cache: | |
| return self._declination_cache[day_of_year] | |
| declination = 23.45 * math.sin(math.radians(360 * (284 + day_of_year) / 365)) | |
| self._declination_cache[day_of_year] = declination | |
| return declination | |
| def solar_hour_angle(self, hour: float) -> float: | |
| """ | |
| Calculate solar hour angle for a given hour of the day. | |
| Args: | |
| hour: Hour of the day (0-23) | |
| Returns: | |
| Solar hour angle in degrees | |
| """ | |
| if not 0 <= hour <= 23: | |
| raise ValueError(f"Hour {hour} must be between 0 and 23") | |
| return 15 * (hour - 12) | |
| def solar_altitude(self, latitude: float, declination: float, hour_angle: float) -> float: | |
| """ | |
| Calculate solar altitude angle. | |
| Args: | |
| latitude: Latitude in degrees | |
| declination: Solar declination angle in degrees | |
| hour_angle: Solar hour angle in degrees | |
| Returns: | |
| Solar altitude angle in degrees | |
| """ | |
| self.validate_angle(latitude, "Latitude", -90, 90) | |
| self.validate_angle(declination, "Declination", -90, 90) | |
| self.validate_angle(hour_angle, "Hour angle", -180, 180) | |
| lat_rad = math.radians(latitude) | |
| dec_rad = math.radians(declination) | |
| ha_rad = math.radians(hour_angle) | |
| sin_alt = math.sin(lat_rad) * math.sin(dec_rad) + math.cos(lat_rad) * math.cos(dec_rad) * math.cos(ha_rad) | |
| altitude = math.degrees(math.asin(sin_alt)) | |
| return max(0, altitude) | |
| def solar_azimuth(self, latitude: float, declination: float, hour_angle: float, altitude: float) -> float: | |
| """ | |
| Calculate solar azimuth angle. | |
| Args: | |
| latitude: Latitude in degrees | |
| declination: Solar declination angle in degrees | |
| hour_angle: Solar hour angle in degrees | |
| altitude: Solar altitude angle in degrees | |
| Returns: | |
| Solar azimuth angle in degrees | |
| """ | |
| self.validate_angle(latitude, "Latitude", -90, 90) | |
| self.validate_angle(declination, "Declination", -90, 90) | |
| self.validate_angle(hour_angle, "Hour angle", -180, 180) | |
| self.validate_angle(altitude, "Altitude", 0, 90) | |
| lat_rad = math.radians(latitude) | |
| dec_rad = math.radians(declination) | |
| ha_rad = math.radians(hour_angle) | |
| alt_rad = math.radians(altitude) | |
| cos_az = (math.sin(alt_rad) * math.sin(lat_rad) - math.sin(dec_rad)) / (math.cos(alt_rad) * math.cos(lat_rad)) | |
| cos_az = max(-1, min(1, cos_az)) | |
| azimuth = math.degrees(math.acos(cos_az)) | |
| if hour_angle > 0: | |
| azimuth = 360 - azimuth | |
| return azimuth | |
| def incident_angle(self, surface_tilt: float, surface_azimuth: float, | |
| solar_altitude: float, solar_azimuth: float) -> float: | |
| """ | |
| Calculate angle of incidence for a surface. | |
| Args: | |
| surface_tilt: Surface tilt angle in degrees (0=horizontal, 90=vertical) | |
| surface_azimuth: Surface azimuth angle in degrees | |
| solar_altitude: Solar altitude angle in degrees | |
| solar_azimuth: Solar azimuth angle in degrees | |
| Returns: | |
| Angle of incidence in degrees | |
| """ | |
| self.validate_angle(surface_tilt, "Surface tilt", 0, 180) | |
| self.validate_angle(surface_azimuth, "Surface azimuth", 0, 360) | |
| self.validate_angle(solar_altitude, "Solar altitude", 0, 90) | |
| self.validate_angle(solar_azimuth, "Solar azimuth", 0, 360) | |
| tilt_rad = math.radians(surface_tilt) | |
| az_diff_rad = math.radians(solar_azimuth - surface_azimuth) | |
| alt_rad = math.radians(solar_altitude) | |
| cos_theta = (math.sin(alt_rad) * math.cos(tilt_rad) + | |
| math.cos(alt_rad) * math.sin(tilt_rad) * math.cos(az_diff_rad)) | |
| cos_theta = max(0, min(1, cos_theta)) | |
| return math.degrees(math.acos(cos_theta)) | |
| def direct_normal_irradiance(self, solar_altitude: float) -> float: | |
| """ | |
| Calculate direct normal irradiance. | |
| Args: | |
| solar_altitude: Solar altitude angle in degrees | |
| Returns: | |
| Direct normal irradiance in W/m² | |
| """ | |
| self.validate_angle(solar_altitude, "Solar altitude", 0, 90) | |
| if solar_altitude <= 0: | |
| return 0 | |
| air_mass = 1 / math.cos(math.radians(90 - solar_altitude)) | |
| dni = 1367 * (1 - 0.14 * air_mass) # Simplified model | |
| return max(0, dni) | |
| def diffuse_horizontal_irradiance(self, dni: float, solar_altitude: float) -> float: | |
| """ | |
| Calculate diffuse horizontal irradiance. | |
| Args: | |
| dni: Direct normal irradiance in W/m² | |
| solar_altitude: Solar altitude angle in degrees | |
| Returns: | |
| Diffuse horizontal irradiance in W/m² | |
| """ | |
| self.validate_angle(solar_altitude, "Solar altitude", 0, 90) | |
| if solar_altitude <= 0: | |
| return 0 | |
| return 0.1 * dni # Simplified model | |
| def irradiance_on_surface(self, dni: float, dhi: float, incident_angle: float, surface_tilt: float) -> float: | |
| """ | |
| Calculate total irradiance on a tilted surface. | |
| Args: | |
| dni: Direct normal irradiance in W/m² | |
| dhi: Diffuse horizontal irradiance in W/m² | |
| incident_angle: Angle of incidence in degrees | |
| surface_tilt: Surface tilt angle in degrees | |
| Returns: | |
| Total irradiance in W/m² | |
| """ | |
| self.validate_angle(incident_angle, "Incident angle", 0, 90) | |
| self.validate_angle(surface_tilt, "Surface tilt", 0, 180) | |
| if dni < 0 or dhi < 0: | |
| raise ValueError("Irradiance values cannot be negative") | |
| direct = dni * math.cos(math.radians(incident_angle)) | |
| diffuse = dhi * (1 + math.cos(math.radians(surface_tilt))) / 2 | |
| return max(0, direct + diffuse) | |
| class HeatTransferCalculations: | |
| """Class for heat transfer calculations.""" | |
| def __init__(self): | |
| """Initialize heat transfer calculations with solar calculations.""" | |
| self.solar = SolarCalculations() | |
| def validate_inputs(self, temp: float, area: float = 0.0, flow_rate: float = 0.0) -> None: | |
| """ | |
| Validate input parameters for heat transfer calculations. | |
| Args: | |
| temp: Temperature in °C | |
| area: Area in m² | |
| flow_rate: Flow rate in m³/s | |
| Raises: | |
| ValueError: If inputs are out of acceptable ranges | |
| """ | |
| if not -50 <= temp <= 60: | |
| raise ValueError(f"Temperature {temp}°C is outside valid range (-50 to 60°C)") | |
| if area < 0: | |
| raise ValueError(f"Area {area}m² cannot be negative") | |
| if flow_rate < 0: | |
| raise ValueError(f"Flow rate {flow_rate}m³/s cannot be negative") | |
| def conduction_heat_transfer(self, u_value: float, area: float, delta_t: float) -> float: | |
| """ | |
| Calculate heat transfer by conduction. | |
| Args: | |
| u_value: Overall heat transfer coefficient in W/(m²·K) | |
| area: Surface area in m² | |
| delta_t: Temperature difference in °C | |
| Returns: | |
| Heat transfer rate in W | |
| """ | |
| if u_value < 0: | |
| raise ValueError(f"U-value {u_value} W/(m²·K) cannot be negative") | |
| self.validate_inputs(delta_t, area) | |
| return u_value * area * delta_t | |
| def convection_heat_transfer(self, h: float, area: float, delta_t: float) -> float: | |
| """ | |
| Calculate heat transfer by convection. | |
| Args: | |
| h: Convective heat transfer coefficient in W/(m²·K) | |
| area: Surface area in m² | |
| delta_t: Temperature difference in °C | |
| Returns: | |
| Heat transfer rate in W | |
| """ | |
| if h < 0: | |
| raise ValueError(f"Convective coefficient {h} W/(m²·K) cannot be negative") | |
| self.validate_inputs(delta_t, area) | |
| return h * area * delta_t | |
| def radiation_heat_transfer(self, emissivity: float, area: float, t_surface: float, t_surroundings: float) -> float: | |
| """ | |
| Calculate heat transfer by radiation using Stefan-Boltzmann law. | |
| Args: | |
| emissivity: Surface emissivity (0-1) | |
| area: Surface area in m² | |
| t_surface: Surface temperature in °C | |
| t_surroundings: Surroundings temperature in °C | |
| Returns: | |
| Heat transfer rate in W | |
| """ | |
| if not 0 <= emissivity <= 1: | |
| raise ValueError(f"Emissivity {emissivity} must be between 0 and 1") | |
| self.validate_inputs(t_surface, area) | |
| self.validate_inputs(t_surroundings) | |
| sigma = 5.67e-8 # Stefan-Boltzmann constant in W/(m²·K⁴) | |
| t_s = t_surface + 273.15 | |
| t_sur = t_surroundings + 273.15 | |
| return emissivity * sigma * area * (t_s**4 - t_sur**4) | |
| def thermal_lag_factor(self, thermal_mass: float, time_constant: float, time_step: float) -> float: | |
| """ | |
| Calculate thermal lag factor for transient heat transfer. | |
| Args: | |
| thermal_mass: Thermal mass in J/K | |
| time_constant: Time constant in hours | |
| time_step: Time step in hours | |
| Returns: | |
| Thermal lag factor (0-1) | |
| """ | |
| if thermal_mass < 0: | |
| raise ValueError(f"Thermal mass {thermal_mass} J/K cannot be negative") | |
| if time_constant <= 0: | |
| raise ValueError(f"Time constant {time_constant} hours must be positive") | |
| if time_step < 0: | |
| raise ValueError(f"Time step {time_step} hours cannot be negative") | |
| return math.exp(-time_step / time_constant) | |
| def infiltration_heat_transfer(self, flow_rate: float, delta_t: float) -> float: | |
| """ | |
| Calculate sensible heat transfer due to infiltration or ventilation. | |
| Args: | |
| flow_rate: Air flow rate in m³/s | |
| delta_t: Temperature difference in °C | |
| Returns: | |
| Sensible heat transfer rate in W | |
| """ | |
| self.validate_inputs(delta_t, flow_rate=flow_rate) | |
| rho = 1.2 # Air density in kg/m³ | |
| cp = 1005 # Specific heat of air in J/(kg·K) | |
| return flow_rate * rho * cp * delta_t | |
| def infiltration_latent_heat_transfer(self, flow_rate: float, delta_w: float) -> float: | |
| """ | |
| Calculate latent heat transfer due to infiltration or ventilation. | |
| Args: | |
| flow_rate: Air flow rate in m³/s | |
| delta_w: Humidity ratio difference in kg/kg | |
| Returns: | |
| Latent heat transfer rate in W | |
| """ | |
| self.validate_inputs(0, flow_rate=flow_rate) | |
| rho = 1.2 # Air density in kg/m³ | |
| h_fg = 2501000 # Latent heat of vaporization in J/kg | |
| return flow_rate * rho * h_fg * delta_w | |
| def wind_pressure_difference(self, wind_speed: float, wind_coefficient: float = 0.4) -> float: | |
| """ | |
| Calculate pressure difference due to wind. | |
| Args: | |
| wind_speed: Wind speed in m/s | |
| wind_coefficient: Wind pressure coefficient | |
| Returns: | |
| Pressure difference in Pa | |
| """ | |
| if wind_speed < 0: | |
| raise ValueError(f"Wind speed {wind_speed} m/s cannot be negative") | |
| if not 0 <= wind_coefficient <= 1: | |
| raise ValueError(f"Wind coefficient {wind_coefficient} must be between 0 and 1") | |
| rho = 1.2 # Air density in kg/m³ | |
| return 0.5 * wind_coefficient * rho * wind_speed**2 | |
| def stack_pressure_difference(self, height: float, indoor_temp: float, outdoor_temp: float) -> float: | |
| """ | |
| Calculate pressure difference due to stack effect. | |
| Args: | |
| height: Height difference in m | |
| indoor_temp: Indoor temperature in K | |
| outdoor_temp: Outdoor temperature in K | |
| Returns: | |
| Pressure difference in Pa | |
| """ | |
| if height < 0: | |
| raise ValueError(f"Height {height} m cannot be negative") | |
| if indoor_temp <= 0 or outdoor_temp <= 0: | |
| raise ValueError("Temperatures must be positive in Kelvin") | |
| g = 9.81 # Gravitational acceleration in m/s² | |
| rho = 1.2 # Air density in kg/m³ | |
| delta_t = abs(indoor_temp - outdoor_temp) | |
| t_avg = (indoor_temp + outdoor_temp) / 2 | |
| return rho * g * height * delta_t / t_avg | |
| def combined_pressure_difference(self, wind_pd: float, stack_pd: float) -> float: | |
| """ | |
| Calculate combined pressure difference from wind and stack effects. | |
| Args: | |
| wind_pd: Wind pressure difference in Pa | |
| stack_pd: Stack pressure difference in Pa | |
| Returns: | |
| Combined pressure difference in Pa | |
| """ | |
| if wind_pd < 0 or stack_pd < 0: | |
| raise ValueError("Pressure differences cannot be negative") | |
| return math.sqrt(wind_pd**2 + stack_pd**2) | |
| def crack_method_infiltration(self, crack_length: float, coefficient: float, | |
| pressure_difference: float) -> float: | |
| """ | |
| Calculate infiltration flow rate using crack method. | |
| Args: | |
| crack_length: Total crack length in m | |
| coefficient: Flow coefficient in m³/(s·m·Pa^n) | |
| pressure_difference: Pressure difference in Pa | |
| Returns: | |
| Infiltration flow rate in m³/s | |
| """ | |
| if crack_length < 0: | |
| raise ValueError(f"Crack length {crack_length} m cannot be negative") | |
| if coefficient < 0: | |
| raise ValueError(f"Coefficient {coefficient} cannot be negative") | |
| if pressure_difference < 0: | |
| raise ValueError(f"Pressure difference {pressure_difference} Pa cannot be negative") | |
| n = 0.65 # Flow exponent | |
| return coefficient * crack_length * pressure_difference**n | |
| def sol_air_temperature(self, outdoor_temp: float, solar_irradiance: float, | |
| surface_absorptivity: float, surface_resistance: float) -> float: | |
| """ | |
| Calculate sol-air temperature for a surface. | |
| Args: | |
| outdoor_temp: Outdoor air temperature in °C | |
| solar_irradiance: Solar irradiance on surface in W/m² | |
| surface_absorptivity: Surface absorptivity (0-1) | |
| surface_resistance: Surface resistance in m²·K/W | |
| Returns: | |
| Sol-air temperature in °C | |
| """ | |
| self.validate_inputs(outdoor_temp) | |
| if solar_irradiance < 0: | |
| raise ValueError(f"Solar irradiance {solar_irradiance} W/m² cannot be negative") | |
| if not 0 <= surface_absorptivity <= 1: | |
| raise ValueError(f"Surface absorptivity {surface_absorptivity} must be between 0 and 1") | |
| if surface_resistance < 0: | |
| raise ValueError(f"Surface resistance {surface_resistance} m²·K/W cannot be negative") | |
| h_ext = 1 / surface_resistance # External convective coefficient | |
| delta_t_rad = surface_absorptivity * solar_irradiance / h_ext | |
| return outdoor_temp + delta_t_rad | |
| def solar_heat_gain(self, irradiance: float, area: float, shgc: float, | |
| shading_coefficient: float = 1.0) -> float: | |
| """ | |
| Calculate solar heat gain through a surface. | |
| Args: | |
| irradiance: Solar irradiance on surface in W/m² | |
| area: Surface area in m² | |
| shgc: Solar heat gain coefficient (0-1) | |
| shading_coefficient: Shading coefficient (0-1) | |
| Returns: | |
| Solar heat gain in W | |
| """ | |
| self.validate_inputs(0, area) | |
| if irradiance < 0: | |
| raise ValueError(f"Irradiance {irradiance} W/m² cannot be negative") | |
| if not 0 <= shgc <= 1: | |
| raise ValueError(f"SHGC {shgc} must be between 0 and 1") | |
| if not 0 <= shading_coefficient <= 1: | |
| raise ValueError(f"Shading coefficient {shading_coefficient} must be between 0 and 1") | |
| return irradiance * area * shgc * shading_coefficient | |
| # Create a singleton instance | |
| heat_transfer_calculator = HeatTransferCalculations() | |
| # Example usage | |
| if __name__ == "__main__": | |
| # Example solar calculations | |
| latitude = 40.0 | |
| day_of_year = 204 | |
| hour = 12.0 | |
| declination = heat_transfer_calculator.solar.solar_declination(day_of_year) | |
| hour_angle = heat_transfer_calculator.solar.solar_hour_angle(hour) | |
| altitude = heat_transfer_calculator.solar.solar_altitude(latitude, declination, hour_angle) | |
| azimuth = heat_transfer_calculator.solar.solar_azimuth(latitude, declination, hour_angle, altitude) | |
| print(f"Solar Declination: {declination:.2f}°") | |
| print(f"Solar Hour Angle: {hour_angle:.2f}°") | |
| print(f"Solar Altitude: {altitude:.2f}°") | |
| print(f"Solar Azimuth: {azimuth:.2f}°") | |
| # Example heat transfer calculation | |
| u_value = 0.5 # W/(m²·K) | |
| area = 20.0 # m² | |
| delta_t = 10.0 # °C | |
| conduction = heat_transfer_calculator.conduction_heat_transfer(u_value, area, delta_t) | |
| print(f"Conduction Heat Transfer: {conduction:.2f} W") |