|
|
""" |
|
|
Shared calculation functions module for HVAC Load Calculator. |
|
|
This module implements common heat transfer calculations used in both cooling and heating load calculations. |
|
|
""" |
|
|
|
|
|
from typing import Dict, List, Any, Optional, Tuple |
|
|
import math |
|
|
import numpy as np |
|
|
import pandas as pd |
|
|
import os |
|
|
|
|
|
|
|
|
from data.building_components import Wall, Roof, Floor, Window, Door, Orientation |
|
|
from utils.psychrometrics import Psychrometrics |
|
|
|
|
|
|
|
|
STEFAN_BOLTZMANN_CONSTANT = 5.67e-8 |
|
|
SOLAR_CONSTANT = 1367 |
|
|
EARTH_TILT_ANGLE = 23.45 |
|
|
|
|
|
|
|
|
class HeatTransfer: |
|
|
"""Class for shared heat transfer calculations.""" |
|
|
|
|
|
@staticmethod |
|
|
def conduction_heat_transfer(u_value: float, area: float, delta_t: float) -> float: |
|
|
""" |
|
|
Calculate conduction heat transfer through a building component. |
|
|
|
|
|
Args: |
|
|
u_value: U-value of the component in W/(m²·K) |
|
|
area: Area of the component in m² |
|
|
delta_t: Temperature difference across the component in K (or °C) |
|
|
|
|
|
Returns: |
|
|
Heat transfer rate in W |
|
|
""" |
|
|
return u_value * area * delta_t |
|
|
|
|
|
@staticmethod |
|
|
def convection_heat_transfer(h_c: float, area: float, delta_t: float) -> float: |
|
|
""" |
|
|
Calculate convection heat transfer. |
|
|
|
|
|
Args: |
|
|
h_c: Convection heat transfer coefficient in W/(m²·K) |
|
|
area: Surface area in m² |
|
|
delta_t: Temperature difference between surface and fluid in K (or °C) |
|
|
|
|
|
Returns: |
|
|
Heat transfer rate in W |
|
|
""" |
|
|
return h_c * area * delta_t |
|
|
|
|
|
@staticmethod |
|
|
def radiation_heat_transfer(emissivity: float, area: float, t_surface: float, t_surroundings: float) -> float: |
|
|
""" |
|
|
Calculate radiation heat transfer. |
|
|
|
|
|
Args: |
|
|
emissivity: Surface emissivity (0-1) |
|
|
area: Surface area in m² |
|
|
t_surface: Surface temperature in K |
|
|
t_surroundings: Surroundings temperature in K |
|
|
|
|
|
Returns: |
|
|
Heat transfer rate in W |
|
|
""" |
|
|
return emissivity * STEFAN_BOLTZMANN_CONSTANT * area * (t_surface**4 - t_surroundings**4) |
|
|
|
|
|
@staticmethod |
|
|
def infiltration_heat_transfer(flow_rate: float, delta_t: float, density: float = 1.2, specific_heat: float = 1006) -> float: |
|
|
""" |
|
|
Calculate sensible heat transfer due to infiltration or ventilation. |
|
|
|
|
|
Args: |
|
|
flow_rate: Volumetric flow rate in m³/s |
|
|
delta_t: Temperature difference between indoor and outdoor air in K (or °C) |
|
|
density: Air density in kg/m³ (default: 1.2 kg/m³) |
|
|
specific_heat: Specific heat capacity of air in J/(kg·K) (default: 1006 J/(kg·K)) |
|
|
|
|
|
Returns: |
|
|
Heat transfer rate in W |
|
|
""" |
|
|
return flow_rate * density * specific_heat * delta_t |
|
|
|
|
|
@staticmethod |
|
|
def infiltration_latent_heat_transfer(flow_rate: float, delta_w: float, density: float = 1.2, latent_heat: float = 2501000) -> float: |
|
|
""" |
|
|
Calculate latent heat transfer due to infiltration or ventilation. |
|
|
|
|
|
Args: |
|
|
flow_rate: Volumetric flow rate in m³/s |
|
|
delta_w: Humidity ratio difference between indoor and outdoor air in kg/kg |
|
|
density: Air density in kg/m³ (default: 1.2 kg/m³) |
|
|
latent_heat: Latent heat of vaporization in J/kg (default: 2501000 J/kg) |
|
|
|
|
|
Returns: |
|
|
Heat transfer rate in W |
|
|
""" |
|
|
return flow_rate * density * latent_heat * delta_w |
|
|
|
|
|
@staticmethod |
|
|
def air_exchange_rate_to_flow_rate(ach: float, volume: float) -> float: |
|
|
""" |
|
|
Convert air changes per hour to volumetric flow rate. |
|
|
|
|
|
Args: |
|
|
ach: Air changes per hour (1/h) |
|
|
volume: Room or building volume in m³ |
|
|
|
|
|
Returns: |
|
|
Volumetric flow rate in m³/s |
|
|
""" |
|
|
return ach * volume / 3600 |
|
|
|
|
|
@staticmethod |
|
|
def flow_rate_to_air_exchange_rate(flow_rate: float, volume: float) -> float: |
|
|
""" |
|
|
Convert volumetric flow rate to air changes per hour. |
|
|
|
|
|
Args: |
|
|
flow_rate: Volumetric flow rate in m³/s |
|
|
volume: Room or building volume in m³ |
|
|
|
|
|
Returns: |
|
|
Air changes per hour (1/h) |
|
|
""" |
|
|
return flow_rate * 3600 / volume |
|
|
|
|
|
@staticmethod |
|
|
def crack_method_infiltration(crack_length: float, coefficient: float, pressure_difference: float, exponent: float = 0.65) -> float: |
|
|
""" |
|
|
Calculate infiltration using the crack method. |
|
|
|
|
|
Args: |
|
|
crack_length: Length of cracks in m |
|
|
coefficient: Flow coefficient in m³/(s·m·Pa^n) |
|
|
pressure_difference: Pressure difference in Pa |
|
|
exponent: Flow exponent (default: 0.65) |
|
|
|
|
|
Returns: |
|
|
Infiltration flow rate in m³/s |
|
|
""" |
|
|
return coefficient * crack_length * pressure_difference**exponent |
|
|
|
|
|
@staticmethod |
|
|
def wind_pressure_difference(wind_speed: float, wind_coefficient: float, density: float = 1.2) -> float: |
|
|
""" |
|
|
Calculate pressure difference due to wind. |
|
|
|
|
|
Args: |
|
|
wind_speed: Wind speed in m/s |
|
|
wind_coefficient: Wind pressure coefficient (dimensionless) |
|
|
density: Air density in kg/m³ (default: 1.2 kg/m³) |
|
|
|
|
|
Returns: |
|
|
Pressure difference in Pa |
|
|
""" |
|
|
return 0.5 * density * wind_speed**2 * wind_coefficient |
|
|
|
|
|
@staticmethod |
|
|
def stack_pressure_difference(height: float, indoor_temp: float, outdoor_temp: float, |
|
|
neutral_plane_height: float = None, gravity: float = 9.81) -> float: |
|
|
""" |
|
|
Calculate pressure difference due to stack effect. |
|
|
|
|
|
Args: |
|
|
height: Height from reference level in m |
|
|
indoor_temp: Indoor temperature in K |
|
|
outdoor_temp: Outdoor temperature in K |
|
|
neutral_plane_height: Height of neutral pressure plane in m (default: half of height) |
|
|
gravity: Acceleration due to gravity in m/s² (default: 9.81 m/s²) |
|
|
|
|
|
Returns: |
|
|
Pressure difference in Pa |
|
|
""" |
|
|
if neutral_plane_height is None: |
|
|
neutral_plane_height = height / 2 |
|
|
|
|
|
|
|
|
return gravity * (height - neutral_plane_height) * (outdoor_temp - indoor_temp) / outdoor_temp |
|
|
|
|
|
@staticmethod |
|
|
def combined_pressure_difference(wind_pd: float, stack_pd: float) -> float: |
|
|
""" |
|
|
Calculate combined pressure difference from wind and stack effects. |
|
|
|
|
|
Args: |
|
|
wind_pd: Pressure difference due to wind in Pa |
|
|
stack_pd: Pressure difference due to stack effect in Pa |
|
|
|
|
|
Returns: |
|
|
Combined pressure difference in Pa |
|
|
""" |
|
|
|
|
|
return math.sqrt(wind_pd**2 + stack_pd**2) |
|
|
|
|
|
@staticmethod |
|
|
def solar_declination(day_of_year: int) -> float: |
|
|
""" |
|
|
Calculate solar declination angle. |
|
|
|
|
|
Args: |
|
|
day_of_year: Day of the year (1-365) |
|
|
|
|
|
Returns: |
|
|
Solar declination angle in degrees |
|
|
""" |
|
|
return EARTH_TILT_ANGLE * math.sin(2 * math.pi * (day_of_year - 81) / 365) |
|
|
|
|
|
@staticmethod |
|
|
def solar_hour_angle(solar_time: float) -> float: |
|
|
""" |
|
|
Calculate solar hour angle. |
|
|
|
|
|
Args: |
|
|
solar_time: Solar time in hours (0-24) |
|
|
|
|
|
Returns: |
|
|
Solar hour angle in degrees |
|
|
""" |
|
|
return 15 * (solar_time - 12) |
|
|
|
|
|
@staticmethod |
|
|
def solar_altitude(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 |
|
|
""" |
|
|
|
|
|
lat_rad = math.radians(latitude) |
|
|
decl_rad = math.radians(declination) |
|
|
hour_rad = math.radians(hour_angle) |
|
|
|
|
|
|
|
|
sin_altitude = (math.sin(lat_rad) * math.sin(decl_rad) + |
|
|
math.cos(lat_rad) * math.cos(decl_rad) * math.cos(hour_rad)) |
|
|
|
|
|
return math.degrees(math.asin(sin_altitude)) |
|
|
|
|
|
@staticmethod |
|
|
def solar_azimuth(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 (0° = South, positive westward) |
|
|
""" |
|
|
|
|
|
lat_rad = math.radians(latitude) |
|
|
decl_rad = math.radians(declination) |
|
|
hour_rad = math.radians(hour_angle) |
|
|
alt_rad = math.radians(altitude) |
|
|
|
|
|
|
|
|
cos_azimuth = ((math.sin(decl_rad) * math.cos(lat_rad) - |
|
|
math.cos(decl_rad) * math.sin(lat_rad) * math.cos(hour_rad)) / |
|
|
math.cos(alt_rad)) |
|
|
|
|
|
|
|
|
cos_azimuth = max(-1.0, min(1.0, cos_azimuth)) |
|
|
|
|
|
|
|
|
azimuth = math.degrees(math.acos(cos_azimuth)) |
|
|
|
|
|
|
|
|
if hour_angle < 0: |
|
|
azimuth = -azimuth |
|
|
|
|
|
|
|
|
return azimuth |
|
|
|
|
|
@staticmethod |
|
|
def incident_angle(surface_tilt: float, surface_azimuth: float, |
|
|
solar_altitude: float, solar_azimuth: float) -> float: |
|
|
""" |
|
|
Calculate angle of incidence on a surface. |
|
|
|
|
|
Args: |
|
|
surface_tilt: Surface tilt angle from horizontal in degrees (0° = horizontal, 90° = vertical) |
|
|
surface_azimuth: Surface azimuth angle in degrees (0° = South, positive westward) |
|
|
solar_altitude: Solar altitude angle in degrees |
|
|
solar_azimuth: Solar azimuth angle in degrees (0° = South, positive westward) |
|
|
|
|
|
Returns: |
|
|
Angle of incidence in degrees |
|
|
""" |
|
|
|
|
|
surf_tilt_rad = math.radians(surface_tilt) |
|
|
surf_azim_rad = math.radians(surface_azimuth) |
|
|
solar_alt_rad = math.radians(solar_altitude) |
|
|
solar_azim_rad = math.radians(solar_azimuth) |
|
|
|
|
|
|
|
|
cos_incident = (math.cos(solar_alt_rad) * math.cos(solar_azim_rad - surf_azim_rad) * |
|
|
math.sin(surf_tilt_rad) + math.sin(solar_alt_rad) * math.cos(surf_tilt_rad)) |
|
|
|
|
|
|
|
|
cos_incident = max(-1.0, min(1.0, cos_incident)) |
|
|
|
|
|
|
|
|
incident_angle = math.degrees(math.acos(cos_incident)) |
|
|
|
|
|
return incident_angle |
|
|
|
|
|
@staticmethod |
|
|
def direct_normal_irradiance(altitude: float, atmospheric_extinction: float = 0.14) -> float: |
|
|
""" |
|
|
Calculate direct normal irradiance. |
|
|
|
|
|
Args: |
|
|
altitude: Solar altitude angle in degrees |
|
|
atmospheric_extinction: Atmospheric extinction coefficient (default: 0.14) |
|
|
|
|
|
Returns: |
|
|
Direct normal irradiance in W/m² |
|
|
""" |
|
|
if altitude <= 0: |
|
|
return 0 |
|
|
|
|
|
|
|
|
air_mass = 1 / math.sin(math.radians(altitude)) |
|
|
|
|
|
|
|
|
return SOLAR_CONSTANT * math.exp(-atmospheric_extinction * air_mass) |
|
|
|
|
|
@staticmethod |
|
|
def diffuse_horizontal_irradiance(direct_normal: float, altitude: float, |
|
|
clearness_factor: float = 1.0) -> float: |
|
|
""" |
|
|
Calculate diffuse horizontal irradiance. |
|
|
|
|
|
Args: |
|
|
direct_normal: Direct normal irradiance in W/m² |
|
|
altitude: Solar altitude angle in degrees |
|
|
clearness_factor: Sky clearness factor (default: 1.0) |
|
|
|
|
|
Returns: |
|
|
Diffuse horizontal irradiance in W/m² |
|
|
""" |
|
|
if altitude <= 0: |
|
|
return 0 |
|
|
|
|
|
|
|
|
c = 0.095 + 0.04 * math.sin(math.radians(altitude)) |
|
|
return c * direct_normal * clearness_factor |
|
|
|
|
|
@staticmethod |
|
|
def global_horizontal_irradiance(direct_normal: float, diffuse_horizontal: float, |
|
|
altitude: float) -> float: |
|
|
""" |
|
|
Calculate global horizontal irradiance. |
|
|
|
|
|
Args: |
|
|
direct_normal: Direct normal irradiance in W/m² |
|
|
diffuse_horizontal: Diffuse horizontal irradiance in W/m² |
|
|
altitude: Solar altitude angle in degrees |
|
|
|
|
|
Returns: |
|
|
Global horizontal irradiance in W/m² |
|
|
""" |
|
|
if altitude <= 0: |
|
|
return 0 |
|
|
|
|
|
|
|
|
return direct_normal * math.sin(math.radians(altitude)) + diffuse_horizontal |
|
|
|
|
|
@staticmethod |
|
|
def irradiance_on_tilted_surface(direct_normal: float, diffuse_horizontal: float, |
|
|
global_horizontal: float, incident_angle: float, |
|
|
surface_tilt: float, ground_reflectance: float = 0.2) -> float: |
|
|
""" |
|
|
Calculate solar irradiance on a tilted surface. |
|
|
|
|
|
Args: |
|
|
direct_normal: Direct normal irradiance in W/m² |
|
|
diffuse_horizontal: Diffuse horizontal irradiance in W/m² |
|
|
global_horizontal: Global horizontal irradiance in W/m² |
|
|
incident_angle: Angle of incidence in degrees |
|
|
surface_tilt: Surface tilt angle from horizontal in degrees |
|
|
ground_reflectance: Ground reflectance (albedo) (default: 0.2) |
|
|
|
|
|
Returns: |
|
|
Total irradiance on tilted surface in W/m² |
|
|
""" |
|
|
|
|
|
incident_rad = math.radians(incident_angle) |
|
|
tilt_rad = math.radians(surface_tilt) |
|
|
|
|
|
|
|
|
if incident_angle < 90: |
|
|
direct_component = direct_normal * math.cos(incident_rad) |
|
|
else: |
|
|
direct_component = 0 |
|
|
|
|
|
|
|
|
diffuse_component = diffuse_horizontal * (1 + math.cos(tilt_rad)) / 2 |
|
|
|
|
|
|
|
|
reflected_component = global_horizontal * ground_reflectance * (1 - math.cos(tilt_rad)) / 2 |
|
|
|
|
|
|
|
|
return direct_component + diffuse_component + reflected_component |
|
|
|
|
|
@staticmethod |
|
|
def solar_heat_gain(irradiance: float, area: float, shgc: float, |
|
|
incident_angle_modifier: float = 1.0) -> float: |
|
|
""" |
|
|
Calculate solar heat gain through a window. |
|
|
|
|
|
Args: |
|
|
irradiance: Solar irradiance on window surface in W/m² |
|
|
area: Window area in m² |
|
|
shgc: Solar heat gain coefficient at normal incidence |
|
|
incident_angle_modifier: Incident angle modifier (default: 1.0) |
|
|
|
|
|
Returns: |
|
|
Solar heat gain in W |
|
|
""" |
|
|
return irradiance * area * shgc * incident_angle_modifier |
|
|
|
|
|
@staticmethod |
|
|
def incident_angle_modifier(incident_angle: float, glazing_layers: int = 1) -> float: |
|
|
""" |
|
|
Calculate incident angle modifier for windows. |
|
|
|
|
|
Args: |
|
|
incident_angle: Angle of incidence in degrees |
|
|
glazing_layers: Number of glazing layers (default: 1) |
|
|
|
|
|
Returns: |
|
|
Incident angle modifier (dimensionless) |
|
|
""" |
|
|
if incident_angle >= 90: |
|
|
return 0 |
|
|
|
|
|
|
|
|
if glazing_layers == 1: |
|
|
|
|
|
return 1 - 0.0018 * incident_angle |
|
|
else: |
|
|
|
|
|
return 1 - 0.00259 * incident_angle |
|
|
|
|
|
@staticmethod |
|
|
def shading_coefficient_to_shgc(sc: float) -> float: |
|
|
""" |
|
|
Convert shading coefficient to solar heat gain coefficient. |
|
|
|
|
|
Args: |
|
|
sc: Shading coefficient |
|
|
|
|
|
Returns: |
|
|
Solar heat gain coefficient |
|
|
""" |
|
|
return 0.87 * sc |
|
|
|
|
|
@staticmethod |
|
|
def shgc_to_shading_coefficient(shgc: float) -> float: |
|
|
""" |
|
|
Convert solar heat gain coefficient to shading coefficient. |
|
|
|
|
|
Args: |
|
|
shgc: Solar heat gain coefficient |
|
|
|
|
|
Returns: |
|
|
Shading coefficient |
|
|
""" |
|
|
return shgc / 0.87 |
|
|
|