Spaces:
Sleeping
Sleeping
Update utils/psychrometrics.py
Browse files- utils/psychrometrics.py +89 -10
utils/psychrometrics.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
"""
|
| 2 |
Psychrometric module for HVAC Load Calculator.
|
| 3 |
This module implements psychrometric calculations for air properties.
|
|
|
|
| 4 |
"""
|
| 5 |
|
| 6 |
from typing import Dict, List, Any, Optional, Tuple
|
|
@@ -19,10 +20,31 @@ GAS_CONSTANT_WATER_VAPOR = UNIVERSAL_GAS_CONSTANT / WATER_MOLECULAR_WEIGHT # J/
|
|
| 19 |
class Psychrometrics:
|
| 20 |
"""Class for psychrometric calculations."""
|
| 21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
@staticmethod
|
| 23 |
def saturation_pressure(t_db: float) -> float:
|
| 24 |
"""
|
| 25 |
Calculate saturation pressure of water vapor.
|
|
|
|
| 26 |
|
| 27 |
Args:
|
| 28 |
t_db: Dry-bulb temperature in °C
|
|
@@ -30,6 +52,8 @@ class Psychrometrics:
|
|
| 30 |
Returns:
|
| 31 |
Saturation pressure in Pa
|
| 32 |
"""
|
|
|
|
|
|
|
| 33 |
# Convert temperature to Kelvin
|
| 34 |
t_k = t_db + 273.15
|
| 35 |
|
|
@@ -67,6 +91,7 @@ class Psychrometrics:
|
|
| 67 |
def humidity_ratio(t_db: float, rh: float, p_atm: float = ATMOSPHERIC_PRESSURE) -> float:
|
| 68 |
"""
|
| 69 |
Calculate humidity ratio (mass of water vapor per unit mass of dry air).
|
|
|
|
| 70 |
|
| 71 |
Args:
|
| 72 |
t_db: Dry-bulb temperature in °C
|
|
@@ -76,6 +101,8 @@ class Psychrometrics:
|
|
| 76 |
Returns:
|
| 77 |
Humidity ratio in kg water vapor / kg dry air
|
| 78 |
"""
|
|
|
|
|
|
|
| 79 |
# Convert relative humidity to decimal
|
| 80 |
rh_decimal = rh / 100.0
|
| 81 |
|
|
@@ -85,8 +112,10 @@ class Psychrometrics:
|
|
| 85 |
# Calculate partial pressure of water vapor
|
| 86 |
p_w = rh_decimal * p_ws
|
| 87 |
|
|
|
|
|
|
|
|
|
|
| 88 |
# Calculate humidity ratio
|
| 89 |
-
# ASHRAE Fundamentals 2017 Chapter 1, Equation 20
|
| 90 |
w = 0.621945 * p_w / (p_atm - p_w)
|
| 91 |
|
| 92 |
return w
|
|
@@ -95,6 +124,7 @@ class Psychrometrics:
|
|
| 95 |
def relative_humidity(t_db: float, w: float, p_atm: float = ATMOSPHERIC_PRESSURE) -> float:
|
| 96 |
"""
|
| 97 |
Calculate relative humidity from humidity ratio.
|
|
|
|
| 98 |
|
| 99 |
Args:
|
| 100 |
t_db: Dry-bulb temperature in °C
|
|
@@ -104,11 +134,14 @@ class Psychrometrics:
|
|
| 104 |
Returns:
|
| 105 |
Relative humidity (0-100)
|
| 106 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
# Calculate saturation pressure
|
| 108 |
p_ws = Psychrometrics.saturation_pressure(t_db)
|
| 109 |
|
| 110 |
# Calculate partial pressure of water vapor
|
| 111 |
-
# Rearranged from ASHRAE Fundamentals 2017 Chapter 1, Equation 20
|
| 112 |
p_w = p_atm * w / (0.621945 + w)
|
| 113 |
|
| 114 |
# Calculate relative humidity
|
|
@@ -120,6 +153,7 @@ class Psychrometrics:
|
|
| 120 |
def wet_bulb_temperature(t_db: float, rh: float, p_atm: float = ATMOSPHERIC_PRESSURE) -> float:
|
| 121 |
"""
|
| 122 |
Calculate wet-bulb temperature using iterative method.
|
|
|
|
| 123 |
|
| 124 |
Args:
|
| 125 |
t_db: Dry-bulb temperature in °C
|
|
@@ -129,6 +163,8 @@ class Psychrometrics:
|
|
| 129 |
Returns:
|
| 130 |
Wet-bulb temperature in °C
|
| 131 |
"""
|
|
|
|
|
|
|
| 132 |
# Calculate humidity ratio at given conditions
|
| 133 |
w = Psychrometrics.humidity_ratio(t_db, rh, p_atm)
|
| 134 |
|
|
@@ -140,6 +176,9 @@ class Psychrometrics:
|
|
| 140 |
tolerance = 0.001 # °C
|
| 141 |
|
| 142 |
for i in range(max_iterations):
|
|
|
|
|
|
|
|
|
|
| 143 |
# Calculate saturation pressure at wet-bulb temperature
|
| 144 |
p_ws_wb = Psychrometrics.saturation_pressure(t_wb)
|
| 145 |
|
|
@@ -147,7 +186,6 @@ class Psychrometrics:
|
|
| 147 |
w_s_wb = 0.621945 * p_ws_wb / (p_atm - p_ws_wb)
|
| 148 |
|
| 149 |
# Calculate humidity ratio from wet-bulb temperature
|
| 150 |
-
# ASHRAE Fundamentals 2017 Chapter 1, Equation 35
|
| 151 |
h_fg = 2501000 + 1840 * t_wb # Latent heat of vaporization at t_wb in J/kg
|
| 152 |
c_pa = 1006 # Specific heat of dry air in J/(kg·K)
|
| 153 |
c_pw = 1860 # Specific heat of water vapor in J/(kg·K)
|
|
@@ -170,6 +208,7 @@ class Psychrometrics:
|
|
| 170 |
def dew_point_temperature(t_db: float, rh: float) -> float:
|
| 171 |
"""
|
| 172 |
Calculate dew point temperature.
|
|
|
|
| 173 |
|
| 174 |
Args:
|
| 175 |
t_db: Dry-bulb temperature in °C
|
|
@@ -178,6 +217,8 @@ class Psychrometrics:
|
|
| 178 |
Returns:
|
| 179 |
Dew point temperature in °C
|
| 180 |
"""
|
|
|
|
|
|
|
| 181 |
# Convert relative humidity to decimal
|
| 182 |
rh_decimal = rh / 100.0
|
| 183 |
|
|
@@ -188,7 +229,6 @@ class Psychrometrics:
|
|
| 188 |
p_w = rh_decimal * p_ws
|
| 189 |
|
| 190 |
# Calculate dew point temperature
|
| 191 |
-
# ASHRAE Fundamentals 2017 Chapter 1, Equation 39 and 40
|
| 192 |
alpha = math.log(p_w / 1000.0) # Convert to kPa for the formula
|
| 193 |
|
| 194 |
if t_db >= 0:
|
|
@@ -214,6 +254,7 @@ class Psychrometrics:
|
|
| 214 |
def enthalpy(t_db: float, w: float) -> float:
|
| 215 |
"""
|
| 216 |
Calculate specific enthalpy of moist air.
|
|
|
|
| 217 |
|
| 218 |
Args:
|
| 219 |
t_db: Dry-bulb temperature in °C
|
|
@@ -222,7 +263,10 @@ class Psychrometrics:
|
|
| 222 |
Returns:
|
| 223 |
Specific enthalpy in J/kg dry air
|
| 224 |
"""
|
| 225 |
-
|
|
|
|
|
|
|
|
|
|
| 226 |
c_pa = 1006 # Specific heat of dry air in J/(kg·K)
|
| 227 |
h_fg = 2501000 # Latent heat of vaporization at 0°C in J/kg
|
| 228 |
c_pw = 1860 # Specific heat of water vapor in J/(kg·K)
|
|
@@ -235,6 +279,7 @@ class Psychrometrics:
|
|
| 235 |
def specific_volume(t_db: float, w: float, p_atm: float = ATMOSPHERIC_PRESSURE) -> float:
|
| 236 |
"""
|
| 237 |
Calculate specific volume of moist air.
|
|
|
|
| 238 |
|
| 239 |
Args:
|
| 240 |
t_db: Dry-bulb temperature in °C
|
|
@@ -244,10 +289,13 @@ class Psychrometrics:
|
|
| 244 |
Returns:
|
| 245 |
Specific volume in m³/kg dry air
|
| 246 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
# Convert temperature to Kelvin
|
| 248 |
t_k = t_db + 273.15
|
| 249 |
|
| 250 |
-
# ASHRAE Fundamentals 2017 Chapter 1, Equation 28
|
| 251 |
r_da = GAS_CONSTANT_DRY_AIR # Gas constant for dry air in J/(kg·K)
|
| 252 |
|
| 253 |
v = r_da * t_k * (1 + 1.607858 * w) / p_atm
|
|
@@ -258,6 +306,7 @@ class Psychrometrics:
|
|
| 258 |
def density(t_db: float, w: float, p_atm: float = ATMOSPHERIC_PRESSURE) -> float:
|
| 259 |
"""
|
| 260 |
Calculate density of moist air.
|
|
|
|
| 261 |
|
| 262 |
Args:
|
| 263 |
t_db: Dry-bulb temperature in °C
|
|
@@ -267,6 +316,10 @@ class Psychrometrics:
|
|
| 267 |
Returns:
|
| 268 |
Density in kg/m³
|
| 269 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 270 |
# Calculate specific volume
|
| 271 |
v = Psychrometrics.specific_volume(t_db, w, p_atm)
|
| 272 |
|
|
@@ -279,6 +332,7 @@ class Psychrometrics:
|
|
| 279 |
def moist_air_properties(t_db: float, rh: float, p_atm: float = ATMOSPHERIC_PRESSURE) -> Dict[str, float]:
|
| 280 |
"""
|
| 281 |
Calculate all psychrometric properties of moist air.
|
|
|
|
| 282 |
|
| 283 |
Args:
|
| 284 |
t_db: Dry-bulb temperature in °C
|
|
@@ -288,6 +342,8 @@ class Psychrometrics:
|
|
| 288 |
Returns:
|
| 289 |
Dictionary with all psychrometric properties
|
| 290 |
"""
|
|
|
|
|
|
|
| 291 |
# Calculate humidity ratio
|
| 292 |
w = Psychrometrics.humidity_ratio(t_db, rh, p_atm)
|
| 293 |
|
|
@@ -331,6 +387,7 @@ class Psychrometrics:
|
|
| 331 |
def find_humidity_ratio_for_enthalpy(t_db: float, h: float) -> float:
|
| 332 |
"""
|
| 333 |
Find humidity ratio for a given dry-bulb temperature and enthalpy.
|
|
|
|
| 334 |
|
| 335 |
Args:
|
| 336 |
t_db: Dry-bulb temperature in °C
|
|
@@ -339,19 +396,23 @@ class Psychrometrics:
|
|
| 339 |
Returns:
|
| 340 |
Humidity ratio in kg water vapor / kg dry air
|
| 341 |
"""
|
| 342 |
-
|
|
|
|
|
|
|
|
|
|
| 343 |
c_pa = 1006 # Specific heat of dry air in J/(kg·K)
|
| 344 |
h_fg = 2501000 # Latent heat of vaporization at 0°C in J/kg
|
| 345 |
c_pw = 1860 # Specific heat of water vapor in J/(kg·K)
|
| 346 |
|
| 347 |
w = (h - c_pa * t_db) / (h_fg + c_pw * t_db)
|
| 348 |
|
| 349 |
-
return max(0, w)
|
| 350 |
|
| 351 |
@staticmethod
|
| 352 |
def find_temperature_for_enthalpy(w: float, h: float) -> float:
|
| 353 |
"""
|
| 354 |
Find dry-bulb temperature for a given humidity ratio and enthalpy.
|
|
|
|
| 355 |
|
| 356 |
Args:
|
| 357 |
w: Humidity ratio in kg water vapor / kg dry air
|
|
@@ -360,19 +421,25 @@ class Psychrometrics:
|
|
| 360 |
Returns:
|
| 361 |
Dry-bulb temperature in °C
|
| 362 |
"""
|
| 363 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 364 |
c_pa = 1006 # Specific heat of dry air in J/(kg·K)
|
| 365 |
h_fg = 2501000 # Latent heat of vaporization at 0°C in J/kg
|
| 366 |
c_pw = 1860 # Specific heat of water vapor in J/(kg·K)
|
| 367 |
|
| 368 |
t_db = (h - w * h_fg) / (c_pa + w * c_pw)
|
| 369 |
|
|
|
|
| 370 |
return t_db
|
| 371 |
|
| 372 |
@staticmethod
|
| 373 |
def sensible_heat_ratio(q_sensible: float, q_total: float) -> float:
|
| 374 |
"""
|
| 375 |
Calculate sensible heat ratio.
|
|
|
|
| 376 |
|
| 377 |
Args:
|
| 378 |
q_sensible: Sensible heat load in W
|
|
@@ -383,6 +450,8 @@ class Psychrometrics:
|
|
| 383 |
"""
|
| 384 |
if q_total == 0:
|
| 385 |
return 1.0
|
|
|
|
|
|
|
| 386 |
|
| 387 |
return q_sensible / q_total
|
| 388 |
|
|
@@ -391,6 +460,7 @@ class Psychrometrics:
|
|
| 391 |
rh_return: float = 50.0, p_atm: float = ATMOSPHERIC_PRESSURE) -> Dict[str, float]:
|
| 392 |
"""
|
| 393 |
Calculate required air flow rate for a given sensible load.
|
|
|
|
| 394 |
|
| 395 |
Args:
|
| 396 |
q_sensible: Sensible heat load in W
|
|
@@ -402,6 +472,9 @@ class Psychrometrics:
|
|
| 402 |
Returns:
|
| 403 |
Dictionary with air flow rate in different units
|
| 404 |
"""
|
|
|
|
|
|
|
|
|
|
| 405 |
# Calculate return air properties
|
| 406 |
w_return = Psychrometrics.humidity_ratio(t_return, rh_return, p_atm)
|
| 407 |
rho_return = Psychrometrics.density(t_return, w_return, p_atm)
|
|
@@ -441,6 +514,7 @@ class Psychrometrics:
|
|
| 441 |
p_atm: float = ATMOSPHERIC_PRESSURE) -> Dict[str, float]:
|
| 442 |
"""
|
| 443 |
Calculate properties of mixed airstreams.
|
|
|
|
| 444 |
|
| 445 |
Args:
|
| 446 |
m1: Mass flow rate of airstream 1 in kg/s
|
|
@@ -454,6 +528,11 @@ class Psychrometrics:
|
|
| 454 |
Returns:
|
| 455 |
Dictionary with mixed air properties
|
| 456 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 457 |
# Calculate humidity ratios
|
| 458 |
w1 = Psychrometrics.humidity_ratio(t_db1, rh1, p_atm)
|
| 459 |
w2 = Psychrometrics.humidity_ratio(t_db2, rh2, p_atm)
|
|
@@ -499,4 +578,4 @@ if __name__ == "__main__":
|
|
| 499 |
print(f"Specific volume: {properties['specific_volume']:.4f} m³/kg")
|
| 500 |
print(f"Density: {properties['density']:.4f} kg/m³")
|
| 501 |
print(f"Saturation pressure: {properties['saturation_pressure']/1000:.2f} kPa")
|
| 502 |
-
print(f"Partial pressure: {properties['partial_pressure']/1000:.2f} kPa")
|
|
|
|
| 1 |
"""
|
| 2 |
Psychrometric module for HVAC Load Calculator.
|
| 3 |
This module implements psychrometric calculations for air properties.
|
| 4 |
+
Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 1.
|
| 5 |
"""
|
| 6 |
|
| 7 |
from typing import Dict, List, Any, Optional, Tuple
|
|
|
|
| 20 |
class Psychrometrics:
|
| 21 |
"""Class for psychrometric calculations."""
|
| 22 |
|
| 23 |
+
@staticmethod
|
| 24 |
+
def validate_inputs(t_db: float, rh: Optional[float] = None, p_atm: Optional[float] = None) -> None:
|
| 25 |
+
"""
|
| 26 |
+
Validate input parameters for psychrometric calculations.
|
| 27 |
+
|
| 28 |
+
Args:
|
| 29 |
+
t_db: Dry-bulb temperature in °C
|
| 30 |
+
rh: Relative humidity in % (0-100), optional
|
| 31 |
+
p_atm: Atmospheric pressure in Pa, optional
|
| 32 |
+
|
| 33 |
+
Raises:
|
| 34 |
+
ValueError: If inputs are invalid
|
| 35 |
+
"""
|
| 36 |
+
if not -50 <= t_db <= 60:
|
| 37 |
+
raise ValueError(f"Temperature {t_db}°C must be between -50°C and 60°C")
|
| 38 |
+
if rh is not None and not 0 <= rh <= 100:
|
| 39 |
+
raise ValueError(f"Relative humidity {rh}% must be between 0 and 100%")
|
| 40 |
+
if p_atm is not None and p_atm <= 0:
|
| 41 |
+
raise ValueError(f"Atmospheric pressure {p_atm} Pa must be positive")
|
| 42 |
+
|
| 43 |
@staticmethod
|
| 44 |
def saturation_pressure(t_db: float) -> float:
|
| 45 |
"""
|
| 46 |
Calculate saturation pressure of water vapor.
|
| 47 |
+
Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 1, Equations 5 and 6.
|
| 48 |
|
| 49 |
Args:
|
| 50 |
t_db: Dry-bulb temperature in °C
|
|
|
|
| 52 |
Returns:
|
| 53 |
Saturation pressure in Pa
|
| 54 |
"""
|
| 55 |
+
Psychrometrics.validate_inputs(t_db)
|
| 56 |
+
|
| 57 |
# Convert temperature to Kelvin
|
| 58 |
t_k = t_db + 273.15
|
| 59 |
|
|
|
|
| 91 |
def humidity_ratio(t_db: float, rh: float, p_atm: float = ATMOSPHERIC_PRESSURE) -> float:
|
| 92 |
"""
|
| 93 |
Calculate humidity ratio (mass of water vapor per unit mass of dry air).
|
| 94 |
+
Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 1, Equation 20.
|
| 95 |
|
| 96 |
Args:
|
| 97 |
t_db: Dry-bulb temperature in °C
|
|
|
|
| 101 |
Returns:
|
| 102 |
Humidity ratio in kg water vapor / kg dry air
|
| 103 |
"""
|
| 104 |
+
Psychrometrics.validate_inputs(t_db, rh, p_atm)
|
| 105 |
+
|
| 106 |
# Convert relative humidity to decimal
|
| 107 |
rh_decimal = rh / 100.0
|
| 108 |
|
|
|
|
| 112 |
# Calculate partial pressure of water vapor
|
| 113 |
p_w = rh_decimal * p_ws
|
| 114 |
|
| 115 |
+
if p_w >= p_atm:
|
| 116 |
+
raise ValueError("Partial pressure of water vapor exceeds atmospheric pressure")
|
| 117 |
+
|
| 118 |
# Calculate humidity ratio
|
|
|
|
| 119 |
w = 0.621945 * p_w / (p_atm - p_w)
|
| 120 |
|
| 121 |
return w
|
|
|
|
| 124 |
def relative_humidity(t_db: float, w: float, p_atm: float = ATMOSPHERIC_PRESSURE) -> float:
|
| 125 |
"""
|
| 126 |
Calculate relative humidity from humidity ratio.
|
| 127 |
+
Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 1, Equation 20 (rearranged).
|
| 128 |
|
| 129 |
Args:
|
| 130 |
t_db: Dry-bulb temperature in °C
|
|
|
|
| 134 |
Returns:
|
| 135 |
Relative humidity (0-100)
|
| 136 |
"""
|
| 137 |
+
Psychrometrics.validate_inputs(t_db, p_atm=p_atm)
|
| 138 |
+
if w < 0:
|
| 139 |
+
raise ValueError("Humidity ratio cannot be negative")
|
| 140 |
+
|
| 141 |
# Calculate saturation pressure
|
| 142 |
p_ws = Psychrometrics.saturation_pressure(t_db)
|
| 143 |
|
| 144 |
# Calculate partial pressure of water vapor
|
|
|
|
| 145 |
p_w = p_atm * w / (0.621945 + w)
|
| 146 |
|
| 147 |
# Calculate relative humidity
|
|
|
|
| 153 |
def wet_bulb_temperature(t_db: float, rh: float, p_atm: float = ATMOSPHERIC_PRESSURE) -> float:
|
| 154 |
"""
|
| 155 |
Calculate wet-bulb temperature using iterative method.
|
| 156 |
+
Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 1, Equation 35.
|
| 157 |
|
| 158 |
Args:
|
| 159 |
t_db: Dry-bulb temperature in °C
|
|
|
|
| 163 |
Returns:
|
| 164 |
Wet-bulb temperature in °C
|
| 165 |
"""
|
| 166 |
+
Psychrometrics.validate_inputs(t_db, rh, p_atm)
|
| 167 |
+
|
| 168 |
# Calculate humidity ratio at given conditions
|
| 169 |
w = Psychrometrics.humidity_ratio(t_db, rh, p_atm)
|
| 170 |
|
|
|
|
| 176 |
tolerance = 0.001 # °C
|
| 177 |
|
| 178 |
for i in range(max_iterations):
|
| 179 |
+
# Validate wet-bulb temperature
|
| 180 |
+
Psychrometrics.validate_inputs(t_wb)
|
| 181 |
+
|
| 182 |
# Calculate saturation pressure at wet-bulb temperature
|
| 183 |
p_ws_wb = Psychrometrics.saturation_pressure(t_wb)
|
| 184 |
|
|
|
|
| 186 |
w_s_wb = 0.621945 * p_ws_wb / (p_atm - p_ws_wb)
|
| 187 |
|
| 188 |
# Calculate humidity ratio from wet-bulb temperature
|
|
|
|
| 189 |
h_fg = 2501000 + 1840 * t_wb # Latent heat of vaporization at t_wb in J/kg
|
| 190 |
c_pa = 1006 # Specific heat of dry air in J/(kg·K)
|
| 191 |
c_pw = 1860 # Specific heat of water vapor in J/(kg·K)
|
|
|
|
| 208 |
def dew_point_temperature(t_db: float, rh: float) -> float:
|
| 209 |
"""
|
| 210 |
Calculate dew point temperature.
|
| 211 |
+
Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 1, Equations 39 and 40.
|
| 212 |
|
| 213 |
Args:
|
| 214 |
t_db: Dry-bulb temperature in °C
|
|
|
|
| 217 |
Returns:
|
| 218 |
Dew point temperature in °C
|
| 219 |
"""
|
| 220 |
+
Psychrometrics.validate_inputs(t_db, rh)
|
| 221 |
+
|
| 222 |
# Convert relative humidity to decimal
|
| 223 |
rh_decimal = rh / 100.0
|
| 224 |
|
|
|
|
| 229 |
p_w = rh_decimal * p_ws
|
| 230 |
|
| 231 |
# Calculate dew point temperature
|
|
|
|
| 232 |
alpha = math.log(p_w / 1000.0) # Convert to kPa for the formula
|
| 233 |
|
| 234 |
if t_db >= 0:
|
|
|
|
| 254 |
def enthalpy(t_db: float, w: float) -> float:
|
| 255 |
"""
|
| 256 |
Calculate specific enthalpy of moist air.
|
| 257 |
+
Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 1, Equation 30.
|
| 258 |
|
| 259 |
Args:
|
| 260 |
t_db: Dry-bulb temperature in °C
|
|
|
|
| 263 |
Returns:
|
| 264 |
Specific enthalpy in J/kg dry air
|
| 265 |
"""
|
| 266 |
+
Psychrometrics.validate_inputs(t_db)
|
| 267 |
+
if w < 0:
|
| 268 |
+
raise ValueError("Humidity ratio cannot be negative")
|
| 269 |
+
|
| 270 |
c_pa = 1006 # Specific heat of dry air in J/(kg·K)
|
| 271 |
h_fg = 2501000 # Latent heat of vaporization at 0°C in J/kg
|
| 272 |
c_pw = 1860 # Specific heat of water vapor in J/(kg·K)
|
|
|
|
| 279 |
def specific_volume(t_db: float, w: float, p_atm: float = ATMOSPHERIC_PRESSURE) -> float:
|
| 280 |
"""
|
| 281 |
Calculate specific volume of moist air.
|
| 282 |
+
Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 1, Equation 28.
|
| 283 |
|
| 284 |
Args:
|
| 285 |
t_db: Dry-bulb temperature in °C
|
|
|
|
| 289 |
Returns:
|
| 290 |
Specific volume in m³/kg dry air
|
| 291 |
"""
|
| 292 |
+
Psychrometrics.validate_inputs(t_db, p_atm=p_atm)
|
| 293 |
+
if w < 0:
|
| 294 |
+
raise ValueError("Humidity ratio cannot be negative")
|
| 295 |
+
|
| 296 |
# Convert temperature to Kelvin
|
| 297 |
t_k = t_db + 273.15
|
| 298 |
|
|
|
|
| 299 |
r_da = GAS_CONSTANT_DRY_AIR # Gas constant for dry air in J/(kg·K)
|
| 300 |
|
| 301 |
v = r_da * t_k * (1 + 1.607858 * w) / p_atm
|
|
|
|
| 306 |
def density(t_db: float, w: float, p_atm: float = ATMOSPHERIC_PRESSURE) -> float:
|
| 307 |
"""
|
| 308 |
Calculate density of moist air.
|
| 309 |
+
Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 1, derived from Equation 28.
|
| 310 |
|
| 311 |
Args:
|
| 312 |
t_db: Dry-bulb temperature in °C
|
|
|
|
| 316 |
Returns:
|
| 317 |
Density in kg/m³
|
| 318 |
"""
|
| 319 |
+
Psychrometrics.validate_inputs(t_db, p_atm=p_atm)
|
| 320 |
+
if w < 0:
|
| 321 |
+
raise ValueError("Humidity ratio cannot be negative")
|
| 322 |
+
|
| 323 |
# Calculate specific volume
|
| 324 |
v = Psychrometrics.specific_volume(t_db, w, p_atm)
|
| 325 |
|
|
|
|
| 332 |
def moist_air_properties(t_db: float, rh: float, p_atm: float = ATMOSPHERIC_PRESSURE) -> Dict[str, float]:
|
| 333 |
"""
|
| 334 |
Calculate all psychrometric properties of moist air.
|
| 335 |
+
Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 1.
|
| 336 |
|
| 337 |
Args:
|
| 338 |
t_db: Dry-bulb temperature in °C
|
|
|
|
| 342 |
Returns:
|
| 343 |
Dictionary with all psychrometric properties
|
| 344 |
"""
|
| 345 |
+
Psychrometrics.validate_inputs(t_db, rh, p_atm)
|
| 346 |
+
|
| 347 |
# Calculate humidity ratio
|
| 348 |
w = Psychrometrics.humidity_ratio(t_db, rh, p_atm)
|
| 349 |
|
|
|
|
| 387 |
def find_humidity_ratio_for_enthalpy(t_db: float, h: float) -> float:
|
| 388 |
"""
|
| 389 |
Find humidity ratio for a given dry-bulb temperature and enthalpy.
|
| 390 |
+
Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 1, Equation 30 (rearranged).
|
| 391 |
|
| 392 |
Args:
|
| 393 |
t_db: Dry-bulb temperature in °C
|
|
|
|
| 396 |
Returns:
|
| 397 |
Humidity ratio in kg water vapor / kg dry air
|
| 398 |
"""
|
| 399 |
+
Psychrometrics.validate_inputs(t_db)
|
| 400 |
+
if h < 0:
|
| 401 |
+
raise ValueError("Enthalpy cannot be negative")
|
| 402 |
+
|
| 403 |
c_pa = 1006 # Specific heat of dry air in J/(kg·K)
|
| 404 |
h_fg = 2501000 # Latent heat of vaporization at 0°C in J/kg
|
| 405 |
c_pw = 1860 # Specific heat of water vapor in J/(kg·K)
|
| 406 |
|
| 407 |
w = (h - c_pa * t_db) / (h_fg + c_pw * t_db)
|
| 408 |
|
| 409 |
+
return max(0, w)
|
| 410 |
|
| 411 |
@staticmethod
|
| 412 |
def find_temperature_for_enthalpy(w: float, h: float) -> float:
|
| 413 |
"""
|
| 414 |
Find dry-bulb temperature for a given humidity ratio and enthalpy.
|
| 415 |
+
Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 1, Equation 30 (rearranged).
|
| 416 |
|
| 417 |
Args:
|
| 418 |
w: Humidity ratio in kg water vapor / kg dry air
|
|
|
|
| 421 |
Returns:
|
| 422 |
Dry-bulb temperature in °C
|
| 423 |
"""
|
| 424 |
+
if w < 0:
|
| 425 |
+
raise ValueError("Humidity ratio cannot be negative")
|
| 426 |
+
if h < 0:
|
| 427 |
+
raise ValueError("Enthalpy cannot be negative")
|
| 428 |
+
|
| 429 |
c_pa = 1006 # Specific heat of dry air in J/(kg·K)
|
| 430 |
h_fg = 2501000 # Latent heat of vaporization at 0°C in J/kg
|
| 431 |
c_pw = 1860 # Specific heat of water vapor in J/(kg·K)
|
| 432 |
|
| 433 |
t_db = (h - w * h_fg) / (c_pa + w * c_pw)
|
| 434 |
|
| 435 |
+
Psychrometrics.validate_inputs(t_db)
|
| 436 |
return t_db
|
| 437 |
|
| 438 |
@staticmethod
|
| 439 |
def sensible_heat_ratio(q_sensible: float, q_total: float) -> float:
|
| 440 |
"""
|
| 441 |
Calculate sensible heat ratio.
|
| 442 |
+
Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 1, Section 1.5.
|
| 443 |
|
| 444 |
Args:
|
| 445 |
q_sensible: Sensible heat load in W
|
|
|
|
| 450 |
"""
|
| 451 |
if q_total == 0:
|
| 452 |
return 1.0
|
| 453 |
+
if q_sensible < 0 or q_total < 0:
|
| 454 |
+
raise ValueError("Heat loads cannot be negative")
|
| 455 |
|
| 456 |
return q_sensible / q_total
|
| 457 |
|
|
|
|
| 460 |
rh_return: float = 50.0, p_atm: float = ATMOSPHERIC_PRESSURE) -> Dict[str, float]:
|
| 461 |
"""
|
| 462 |
Calculate required air flow rate for a given sensible load.
|
| 463 |
+
Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 1, Section 1.6.
|
| 464 |
|
| 465 |
Args:
|
| 466 |
q_sensible: Sensible heat load in W
|
|
|
|
| 472 |
Returns:
|
| 473 |
Dictionary with air flow rate in different units
|
| 474 |
"""
|
| 475 |
+
Psychrometrics.validate_inputs(t_return, rh_return, p_atm)
|
| 476 |
+
Psychrometrics.validate_inputs(t_supply)
|
| 477 |
+
|
| 478 |
# Calculate return air properties
|
| 479 |
w_return = Psychrometrics.humidity_ratio(t_return, rh_return, p_atm)
|
| 480 |
rho_return = Psychrometrics.density(t_return, w_return, p_atm)
|
|
|
|
| 514 |
p_atm: float = ATMOSPHERIC_PRESSURE) -> Dict[str, float]:
|
| 515 |
"""
|
| 516 |
Calculate properties of mixed airstreams.
|
| 517 |
+
Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 1, Section 1.7.
|
| 518 |
|
| 519 |
Args:
|
| 520 |
m1: Mass flow rate of airstream 1 in kg/s
|
|
|
|
| 528 |
Returns:
|
| 529 |
Dictionary with mixed air properties
|
| 530 |
"""
|
| 531 |
+
Psychrometrics.validate_inputs(t_db1, rh1, p_atm)
|
| 532 |
+
Psychrometrics.validate_inputs(t_db2, rh2, p_atm)
|
| 533 |
+
if m1 < 0 or m2 < 0:
|
| 534 |
+
raise ValueError("Mass flow rates cannot be negative")
|
| 535 |
+
|
| 536 |
# Calculate humidity ratios
|
| 537 |
w1 = Psychrometrics.humidity_ratio(t_db1, rh1, p_atm)
|
| 538 |
w2 = Psychrometrics.humidity_ratio(t_db2, rh2, p_atm)
|
|
|
|
| 578 |
print(f"Specific volume: {properties['specific_volume']:.4f} m³/kg")
|
| 579 |
print(f"Density: {properties['density']:.4f} kg/m³")
|
| 580 |
print(f"Saturation pressure: {properties['saturation_pressure']/1000:.2f} kPa")
|
| 581 |
+
print(f"Partial pressure: {properties['partial_pressure']/1000:.2f} kPa")
|