mabuseif commited on
Commit
625c9cc
·
verified ·
1 Parent(s): 02ba5be

Upload heat_transfer.py

Browse files
Files changed (1) hide show
  1. utils/heat_transfer.py +538 -211
utils/heat_transfer.py CHANGED
@@ -1,10 +1,16 @@
1
  """
2
  Heat transfer calculation module for HVAC Load Calculator.
3
- This module implements heat transfer calculations for conduction, infiltration, and solar effects.
4
- Reference: ASHRAE Handbook—Fundamentals (2017), Chapters 16 and 18.
 
 
 
 
 
 
5
  """
6
 
7
- from typing import Dict, List, Any, Optional, Tuple
8
  import math
9
  import numpy as np
10
  import logging
@@ -14,314 +20,635 @@ from dataclasses import dataclass
14
  logging.basicConfig(level=logging.INFO)
15
  logger = logging.getLogger(__name__)
16
 
17
- # Import utility modules
18
- from utils.psychrometrics import Psychrometrics
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
- # Import data modules
21
- from data.building_components import Orientation
 
 
 
 
 
 
22
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
  class SolarCalculations:
25
- """Class for solar geometry and radiation calculations."""
26
-
27
- def validate_angle(self, angle: float, name: str, min_val: float, max_val: float) -> None:
 
 
 
 
 
 
 
 
 
28
  """
29
- Validate angle inputs for solar calculations.
30
-
31
  Args:
32
- angle: Angle in degrees
33
- name: Name of the angle
34
- min_val: Minimum allowed value
35
- max_val: Maximum allowed value
36
-
37
- Raises:
38
- ValueError: If angle is out of range
39
- """
40
- if not min_val <= angle <= max_val:
41
- raise ValueError(f"{name} {angle}° must be between {min_val}° and {max_val}°")
42
 
43
- def solar_declination(self, day_of_year: int) -> float:
 
 
 
 
 
 
44
  """
45
  Calculate solar declination angle.
46
- Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 14, Equation 14.6.
47
-
48
  Args:
49
- day_of_year: Day of the year (1-365)
50
-
51
  Returns:
52
- Declination angle in degrees
53
  """
54
- if not 1 <= day_of_year <= 365:
55
- raise ValueError("Day of year must be between 1 and 365")
56
-
57
- declination = 23.45 * math.sin(math.radians(360 * (284 + day_of_year) / 365))
58
- self.validate_angle(declination, "Declination angle", -23.45, 23.45)
 
 
 
 
 
 
 
 
59
  return declination
60
 
61
- def solar_hour_angle(self, hour: float) -> float:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  """
63
- Calculate solar hour angle.
64
- Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 14, Equation 14.7.
65
-
66
  Args:
67
- hour: Hour of the day (0-23)
68
-
69
  Returns:
70
- Hour angle in degrees
71
  """
72
- if not 0 <= hour <= 24:
73
- raise ValueError("Hour must be between 0 and 24")
74
-
75
- hour_angle = (hour - 12) * 15
76
- self.validate_angle(hour_angle, "Hour angle", -180, 180)
77
  return hour_angle
78
 
79
- def solar_altitude(self, latitude: float, declination: float, hour_angle: float) -> float:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  """
81
- Calculate solar altitude angle.
82
- Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 14, Equation 14.8.
83
-
84
  Args:
85
- latitude: Latitude in degrees
86
- declination: Declination angle in degrees
87
- hour_angle: Hour angle in degrees
88
-
89
  Returns:
90
- Altitude angle in degrees
91
- """
92
- self.validate_angle(latitude, "Latitude", -90, 90)
93
- self.validate_angle(declination, "Declination", -23.45, 23.45)
94
- self.validate_angle(hour_angle, "Hour angle", -180, 180)
95
-
96
- sin_beta = (math.sin(math.radians(latitude)) * math.sin(math.radians(declination)) +
97
- math.cos(math.radians(latitude)) * math.cos(math.radians(declination)) *
98
- math.cos(math.radians(hour_angle)))
99
- beta = math.degrees(math.asin(sin_beta))
100
- self.validate_angle(beta, "Altitude angle", 0, 90)
101
- return beta
102
-
103
- def solar_azimuth(self, latitude: float, declination: float, hour_angle: float, altitude: float) -> float:
104
- """
105
- Calculate solar azimuth angle.
106
- Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 14, Equation 14.9.
107
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  Args:
109
- latitude: Latitude in degrees
110
- declination: Declination angle in degrees
111
- hour_angle: Hour angle in degrees
112
- altitude: Altitude angle in degrees
113
-
114
  Returns:
115
- Azimuth angle in degrees
116
- """
117
- self.validate_angle(latitude, "Latitude", -90, 90)
118
- self.validate_angle(declination, "Declination", -23.45, 23.45)
119
- self.validate_angle(hour_angle, "Hour angle", -180, 180)
120
- self.validate_angle(altitude, "Altitude", 0, 90)
121
-
122
- sin_phi = (math.cos(math.radians(declination)) * math.sin(math.radians(hour_angle)) /
123
- math.cos(math.radians(altitude)))
124
- phi = math.degrees(math.asin(sin_phi))
125
-
126
- if hour_angle > 0:
127
- phi = 180 - phi
128
- elif hour_angle < 0:
129
- phi = -180 - phi
130
-
131
- self.validate_angle(phi, "Azimuth angle", -180, 180)
132
- return phi
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
 
 
134
 
135
  class HeatTransferCalculations:
136
  """Class for heat transfer calculations."""
137
-
138
- def __init__(self):
139
  """
140
  Initialize heat transfer calculations with psychrometrics and solar calculations.
141
  Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 16.
 
 
142
  """
143
  self.psychrometrics = Psychrometrics()
144
  self.solar = SolarCalculations()
145
- self.debug_mode = False
146
-
147
- def conduction_heat_transfer(self, u_value: float, area: float, delta_t: float) -> float:
 
 
 
148
  """
149
  Calculate heat transfer via conduction.
150
  Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 18, Equation 18.1.
151
-
152
  Args:
153
- u_value: U-value of the component in W/(m²·K)
154
- area: Area of the component in m²
155
- delta_t: Temperature difference in °C
156
-
157
  Returns:
158
- Heat transfer rate in W
159
  """
160
- if u_value < 0 or area < 0:
 
 
 
 
 
 
161
  raise ValueError("U-value and area must be non-negative")
162
-
163
  q = u_value * area * delta_t
164
  return q
165
 
166
- def infiltration_heat_transfer(self, flow_rate: float, delta_t: float,
167
- t_db: float, rh: float, p_atm: float = 101325) -> float:
 
 
168
  """
169
  Calculate sensible heat transfer due to infiltration or ventilation.
170
  Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 18, Equation 18.5.
171
-
172
  Args:
173
- flow_rate: Air flow rate in m³/s
174
- delta_t: Temperature difference in °C
175
- t_db: Dry-bulb temperature for air properties in °C
176
- rh: Relative humidity in % (0-100)
177
- p_atm: Atmospheric pressure in Pa
178
-
179
  Returns:
180
- Sensible heat transfer rate in W
181
  """
182
- if flow_rate < 0:
 
 
 
 
 
 
 
 
 
 
183
  raise ValueError("Flow rate cannot be negative")
184
-
185
- # Calculate air density and specific heat using psychrometrics
186
  w = self.psychrometrics.humidity_ratio(t_db, rh, p_atm)
187
  rho = self.psychrometrics.density(t_db, w, p_atm)
188
  c_p = 1006 + 1860 * w # Specific heat of moist air in J/(kg·K)
189
-
190
  q = flow_rate * rho * c_p * delta_t
191
  return q
192
 
193
- def infiltration_latent_heat_transfer(self, flow_rate: float, delta_w: float,
194
- t_db: float, rh: float, p_atm: float = 101325) -> float:
 
 
195
  """
196
  Calculate latent heat transfer due to infiltration or ventilation.
197
  Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 18, Equation 18.6.
198
-
199
  Args:
200
- flow_rate: Air flow rate in m³/s
201
- delta_w: Humidity ratio difference in kg/kg
202
- t_db: Dry-bulb temperature for air properties in °C
203
- rh: Relative humidity in % (0-100)
204
- p_atm: Atmospheric pressure in Pa
205
-
206
  Returns:
207
- Latent heat transfer rate in W
208
  """
209
- if flow_rate < 0 or delta_w < 0:
210
- raise ValueError("Flow rate and humidity ratio difference cannot be negative")
211
-
 
 
 
 
 
 
 
 
 
212
  # Calculate air density and latent heat
213
- w = self.psychrometrics.humidity_ratio(t_db, rh, p_atm)
214
- rho = self.psychrometrics.density(t_db, w, p_atm)
215
- h_fg = 2501000 + 1840 * t_db # Latent heat of vaporization in J/kg
216
-
217
  q = flow_rate * rho * h_fg * delta_w
218
  return q
219
 
220
- def wind_pressure_difference(self, wind_speed: float) -> float:
 
221
  """
222
  Calculate pressure difference due to wind.
223
  Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 16, Equation 16.3.
224
-
225
  Args:
226
- wind_speed: Wind speed in m/s
227
-
 
228
  Returns:
229
- Pressure difference in Pa
230
  """
231
  if wind_speed < 0:
232
  raise ValueError("Wind speed cannot be negative")
233
-
234
- c_p = 0.6 # Wind pressure coefficient
235
- rho_air = 1.2 # Air density at standard conditions in kg/m³
236
- delta_p = 0.5 * c_p * rho_air * wind_speed**2
237
  return delta_p
238
 
239
- def stack_pressure_difference(self, height: float, t_inside: float, t_outside: float) -> float:
 
240
  """
241
  Calculate pressure difference due to stack effect.
242
  Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 16, Equation 16.4.
243
-
244
  Args:
245
- height: Height of the building in m
246
- t_inside: Inside temperature in K
247
- t_outside: Outside temperature in K
248
-
249
  Returns:
250
- Pressure difference in Pa
251
- """
252
- if height < 0 or t_inside <= 0 or t_outside <= 0:
253
- raise ValueError("Height and temperatures must be positive")
254
-
255
- g = 9.81 # Gravitational acceleration in m/s²
256
- rho_air = 1.2 # Air density at standard conditions in kg/m³
257
- delta_p = rho_air * g * height * (1 / t_outside - 1 / t_inside)
 
 
 
 
 
 
258
  return delta_p
259
 
260
  def combined_pressure_difference(self, wind_pd: float, stack_pd: float) -> float:
261
- """
262
- Calculate combined pressure difference from wind and stack effects.
263
- Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 16, Section 16.2.
264
-
265
- Args:
266
- wind_pd: Wind pressure difference in Pa
267
- stack_pd: Stack pressure difference in Pa
268
-
269
- Returns:
270
- Combined pressure difference in Pa
271
- """
272
  delta_p = math.sqrt(wind_pd**2 + stack_pd**2)
273
  return delta_p
274
 
275
- def crack_method_infiltration(self, crack_length: float, crack_width: float, delta_p: float) -> float:
 
276
  """
277
- Calculate infiltration flow rate using crack method.
278
  Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 16, Equation 16.5.
279
-
280
  Args:
281
- crack_length: Length of cracks in m
282
- crack_width: Width of cracks in m
283
- delta_p: Pressure difference across cracks in Pa
284
-
285
  Returns:
286
- Infiltration flow rate in m³/s
287
  """
288
  if crack_length < 0 or crack_width < 0 or delta_p < 0:
289
- raise ValueError("Crack dimensions and pressure difference cannot be negative")
290
-
291
- c_d = 0.65 # Discharge coefficient
 
 
292
  area = crack_length * crack_width
293
- rho_air = 1.2 # Air density at standard conditions in kg/m³
294
- q = c_d * area * math.sqrt(2 * delta_p / rho_air)
295
  return q
296
 
297
-
298
  # Example usage
299
  if __name__ == "__main__":
300
- heat_transfer = HeatTransferCalculations()
301
- heat_transfer.debug_mode = True
302
-
303
  # Example conduction calculation
304
- u_value = 0.5 # W/(m²·K)
305
- area = 20.0 # m²
306
- delta_t = 26.0 # °C
307
  q_conduction = heat_transfer.conduction_heat_transfer(u_value, area, delta_t)
308
  logger.info(f"Conduction heat transfer: {q_conduction:.2f} W")
309
-
310
- # Example infiltration calculation
311
- flow_rate = 0.05 # m³/s
312
- delta_t = 26.0 # °C
313
- t_db = 21.0 # °C
314
- rh = 40.0 # %
315
- p_atm = 101325 # Pa
316
- q_infiltration = heat_transfer.infiltration_heat_transfer(flow_rate, delta_t, t_db, rh, p_atm)
317
- logger.info(f"Infiltration sensible heat transfer: {q_infiltration:.2f} W")
318
-
319
- # Example solar calculation
320
- latitude = 40.0 # degrees
321
- day_of_year = 172 # June 21
322
- hour = 12.0 # Noon
323
- declination = heat_transfer.solar.solar_declination(day_of_year)
324
- hour_angle = heat_transfer.solar.solar_hour_angle(hour)
325
- altitude = heat_transfer.solar.solar_altitude(latitude, declination, hour_angle)
326
- azimuth = heat_transfer.solar.solar_azimuth(latitude, declination, hour_angle, altitude)
327
- logger.info(f"Solar altitude: {altitude:.2f}°, Azimuth: {azimuth:.2f}°")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """
2
  Heat transfer calculation module for HVAC Load Calculator.
3
+ This module implements heat transfer calculations for conduction, infiltration,
4
+ and enhanced solar radiation modeling, including vectorization support.
5
+ Reference: ASHRAE Handbook—Fundamentals (2017), Chapters 14, 16, 18.
6
+ Duffie & Beckman, Solar Engineering of Thermal Processes (4th Ed.).
7
+
8
+ Author: Dr Majed Abuseif
9
+ Date: May 2025 (Enhanced based on plan, preserving original features)
10
+ Version: 1.3.0
11
  """
12
 
13
+ from typing import Dict, List, Any, Optional, Tuple, Union
14
  import math
15
  import numpy as np
16
  import logging
 
20
  logging.basicConfig(level=logging.INFO)
21
  logger = logging.getLogger(__name__)
22
 
23
+ # Import utility modules (ensure these exist and are correct)
24
+ try:
25
+ from utils.psychrometrics import Psychrometrics
26
+ except ImportError:
27
+ print("Warning: Could not import Psychrometrics. Using placeholder.")
28
+ class Psychrometrics:
29
+ def humidity_ratio(self, tdb, rh, p): return 0.01
30
+ def density(self, tdb, w, p): return 1.2
31
+ def pressure_at_altitude(self, alt): return 101325
32
+ def latent_heat_of_vaporization(self, tdb): return 2501000
33
+ # Add other methods if needed by HeatTransferCalculations
34
+
35
+ # Import data modules (ensure these exist and are correct)
36
+ try:
37
+ # Assuming Orientation enum is defined elsewhere, e.g., in component selection
38
+ from app.component_selection import Orientation
39
+ except ImportError:
40
+ print("Warning: Could not import Orientation enum. Using placeholder.")
41
+ from enum import Enum
42
+ class Orientation(Enum): NORTH="N"; NORTHEAST="NE"; EAST="E"; SOUTHEAST="SE"; SOUTH="S"; SOUTHWEST="SW"; WEST="W"; NORTHWEST="NW"; HORIZONTAL="H"
43
+
44
+ # Constants
45
+ STEFAN_BOLTZMANN = 5.67e-8 # W/(m²·K⁴)
46
+ SOLAR_CONSTANT = 1367 # W/m² (Average extraterrestrial solar irradiance)
47
+ ATMOSPHERIC_PRESSURE = 101325 # Pa
48
+ GAS_CONSTANT_DRY_AIR = 287.058 # J/(kg·K)
49
 
50
+ @dataclass
51
+ class SolarAngles:
52
+ """Holds calculated solar angles."""
53
+ declination: float # degrees
54
+ hour_angle: float # degrees
55
+ altitude: float # degrees
56
+ azimuth: float # degrees
57
+ incidence_angle: Optional[float] = None # degrees, for a specific surface
58
 
59
+ @dataclass
60
+ class SolarRadiation:
61
+ """Holds calculated solar radiation components."""
62
+ extraterrestrial_normal: float # W/m²
63
+ direct_normal: float # W/m² (DNI)
64
+ diffuse_horizontal: float # W/m² (DHI)
65
+ global_horizontal: float # W/m² (GHI = DNI*cos(zenith) + DHI)
66
+ total_on_surface: Optional[float] = None # W/m², for a specific surface
67
+ direct_on_surface: Optional[float] = None # W/m²
68
+ diffuse_on_surface: Optional[float] = None # W/m²
69
+ reflected_on_surface: Optional[float] = None # W/m²
70
 
71
  class SolarCalculations:
72
+ """Class for enhanced solar geometry and radiation calculations."""
73
+
74
+ def validate_angle(self, angle: Union[float, np.ndarray], name: str, min_val: float, max_val: float) -> None:
75
+ """ Validate angle inputs for solar calculations (handles scalars and numpy arrays). """
76
+ if isinstance(angle, np.ndarray):
77
+ if np.any(angle < min_val) or np.any(angle > max_val):
78
+ logger.warning(f"{name} contains values outside range [{min_val}, {max_val}]")
79
+ # Optionally clamp values: angle = np.clip(angle, min_val, max_val)
80
+ elif not min_val <= angle <= max_val:
81
+ raise ValueError(f"{name} {angle}° must be between {min_val}° and {max_val}°")
82
+
83
+ def equation_of_time(self, day_of_year: Union[int, np.ndarray]) -> Union[float, np.ndarray]:
84
  """
85
+ Calculate the Equation of Time (EoT) in minutes.
86
+ Reference: Duffie & Beckman (4th Ed.), Eq 1.5.3a.
87
  Args:
88
+ day_of_year: Day of the year (1-365 or 1-366).
89
+ Returns:
90
+ Equation of Time in minutes.
91
+ """
92
+ if isinstance(day_of_year, np.ndarray):
93
+ if np.any(day_of_year < 1) or np.any(day_of_year > 366):
94
+ raise ValueError("Day of year must be between 1 and 366")
95
+ elif not 1 <= day_of_year <= 366:
96
+ raise ValueError("Day of year must be between 1 and 366")
 
97
 
98
+ B = (day_of_year - 1) * 360 / 365 # degrees (approximate)
99
+ B_rad = np.radians(B)
100
+ eot = 229.18 * (0.000075 + 0.001868 * np.cos(B_rad) - 0.032077 * np.sin(B_rad) \
101
+ - 0.014615 * np.cos(2 * B_rad) - 0.04089 * np.sin(2 * B_rad))
102
+ return eot
103
+
104
+ def solar_declination(self, day_of_year: Union[int, np.ndarray]) -> Union[float, np.ndarray]:
105
  """
106
  Calculate solar declination angle.
107
+ Reference: Duffie & Beckman (4th Ed.), Eq 1.6.1a (more accurate than ASHRAE approx).
 
108
  Args:
109
+ day_of_year: Day of the year (1-365 or 1-366).
 
110
  Returns:
111
+ Declination angle in degrees.
112
  """
113
+ if isinstance(day_of_year, np.ndarray):
114
+ if np.any(day_of_year < 1) or np.any(day_of_year > 366):
115
+ raise ValueError("Day of year must be between 1 and 366")
116
+ elif not 1 <= day_of_year <= 366:
117
+ raise ValueError("Day of year must be between 1 and 366")
118
+
119
+ # Using Spencer's formula (1971) as cited in Duffie & Beckman
120
+ gamma_rad = (2 * np.pi / 365) * (day_of_year - 1)
121
+ declination_rad = (0.006918 - 0.399912 * np.cos(gamma_rad) + 0.070257 * np.sin(gamma_rad)
122
+ - 0.006758 * np.cos(2 * gamma_rad) + 0.000907 * np.sin(2 * gamma_rad)
123
+ - 0.002697 * np.cos(3 * gamma_rad) + 0.00148 * np.sin(3 * gamma_rad))
124
+ declination = np.degrees(declination_rad)
125
+ # self.validate_angle(declination, "Declination angle", -23.45, 23.45)
126
  return declination
127
 
128
+ def solar_time(self, local_standard_time_hour: Union[float, np.ndarray], eot_minutes: Union[float, np.ndarray],
129
+ longitude_deg: float, standard_meridian_deg: float) -> Union[float, np.ndarray]:
130
+ """
131
+ Calculate solar time (Local Apparent Time, LAT).
132
+ Reference: Duffie & Beckman (4th Ed.), Eq 1.5.2.
133
+ Args:
134
+ local_standard_time_hour: Local standard time (clock time) in hours (0-24).
135
+ eot_minutes: Equation of Time in minutes.
136
+ longitude_deg: Local longitude in degrees (East positive, West negative).
137
+ standard_meridian_deg: Standard meridian for the local time zone in degrees.
138
+ Returns:
139
+ Solar time in hours (0-24).
140
+ """
141
+ # Time correction for longitude difference from standard meridian
142
+ longitude_correction_minutes = 4 * (standard_meridian_deg - longitude_deg)
143
+ solar_time_hour = local_standard_time_hour + (eot_minutes + longitude_correction_minutes) / 60.0
144
+ return solar_time_hour
145
+
146
+ def solar_hour_angle(self, solar_time_hour: Union[float, np.ndarray]) -> Union[float, np.ndarray]:
147
  """
148
+ Calculate solar hour angle from solar time.
149
+ Reference: Duffie & Beckman (4th Ed.), Section 1.6.
 
150
  Args:
151
+ solar_time_hour: Solar time in hours (0-24).
 
152
  Returns:
153
+ Hour angle in degrees (-180 to 180, zero at solar noon).
154
  """
155
+ # Hour angle is 15 degrees per hour from solar noon (12)
156
+ hour_angle = (solar_time_hour - 12) * 15
157
+ # self.validate_angle(hour_angle, "Hour angle", -180, 180)
 
 
158
  return hour_angle
159
 
160
+ def solar_zenith_altitude(self, latitude_deg: float, declination_deg: Union[float, np.ndarray],
161
+ hour_angle_deg: Union[float, np.ndarray]) -> Tuple[Union[float, np.ndarray], Union[float, np.ndarray]]:
162
+ """
163
+ Calculate solar zenith and altitude angles.
164
+ Reference: Duffie & Beckman (4th Ed.), Eq 1.6.5.
165
+ Args:
166
+ latitude_deg: Latitude in degrees.
167
+ declination_deg: Declination angle in degrees.
168
+ hour_angle_deg: Hour angle in degrees.
169
+ Returns:
170
+ Tuple: (zenith angle in degrees [0-180], altitude angle in degrees [-90 to 90]).
171
+ Altitude is 90 - zenith.
172
+ """
173
+ self.validate_angle(latitude_deg, "Latitude", -90, 90)
174
+ # self.validate_angle(declination_deg, "Declination", -23.45, 23.45)
175
+ # self.validate_angle(hour_angle_deg, "Hour angle", -180, 180)
176
+
177
+ lat_rad = np.radians(latitude_deg)
178
+ dec_rad = np.radians(declination_deg)
179
+ ha_rad = np.radians(hour_angle_deg)
180
+
181
+ cos_zenith = (np.sin(lat_rad) * np.sin(dec_rad) +
182
+ np.cos(lat_rad) * np.cos(dec_rad) * np.cos(ha_rad))
183
+ # Clamp cos_zenith to [-1, 1] due to potential floating point inaccuracies
184
+ cos_zenith = np.clip(cos_zenith, -1.0, 1.0)
185
+
186
+ zenith_rad = np.arccos(cos_zenith)
187
+ zenith_deg = np.degrees(zenith_rad)
188
+ altitude_deg = 90.0 - zenith_deg
189
+
190
+ # self.validate_angle(zenith_deg, "Zenith angle", 0, 180)
191
+ # self.validate_angle(altitude_deg, "Altitude angle", -90, 90)
192
+ return zenith_deg, altitude_deg
193
+
194
+ def solar_azimuth(self, latitude_deg: float, declination_deg: Union[float, np.ndarray],
195
+ hour_angle_deg: Union[float, np.ndarray], zenith_deg: Union[float, np.ndarray]) -> Union[float, np.ndarray]:
196
+ """
197
+ Calculate solar azimuth angle (measured clockwise from North = 0°).
198
+ Reference: Duffie & Beckman (4th Ed.), Eq 1.6.6 (modified for N=0, E=90, S=180, W=270).
199
+ Args:
200
+ latitude_deg: Latitude in degrees.
201
+ declination_deg: Declination angle in degrees.
202
+ hour_angle_deg: Hour angle in degrees.
203
+ zenith_deg: Zenith angle in degrees.
204
+ Returns:
205
+ Azimuth angle in degrees (0-360, clockwise from North).
206
+ """
207
+ lat_rad = np.radians(latitude_deg)
208
+ dec_rad = np.radians(declination_deg)
209
+ ha_rad = np.radians(hour_angle_deg)
210
+ zenith_rad = np.radians(zenith_deg)
211
+
212
+ # Avoid division by zero if zenith is 0 (sun directly overhead)
213
+ sin_zenith = np.sin(zenith_rad)
214
+ azimuth_deg = np.zeros_like(zenith_deg) # Initialize with zeros
215
+
216
+ # Calculate only where sin_zenith is significantly greater than zero
217
+ valid_mask = sin_zenith > 1e-6
218
+ if isinstance(valid_mask, bool): # Handle scalar case
219
+ if valid_mask:
220
+ cos_azimuth_term = (np.sin(dec_rad) * np.cos(lat_rad) - np.cos(dec_rad) * np.sin(lat_rad) * np.cos(ha_rad)) / sin_zenith
221
+ cos_azimuth_term = np.clip(cos_azimuth_term, -1.0, 1.0)
222
+ azimuth_rad_provisional = np.arccos(cos_azimuth_term)
223
+ azimuth_deg_provisional = np.degrees(azimuth_rad_provisional)
224
+
225
+ # Adjust based on hour angle
226
+ if isinstance(hour_angle_deg, np.ndarray):
227
+ azimuth_deg = np.where(hour_angle_deg > 0, 360.0 - azimuth_deg_provisional, azimuth_deg_provisional)
228
+ else:
229
+ azimuth_deg = 360.0 - azimuth_deg_provisional if hour_angle_deg > 0 else azimuth_deg_provisional
230
+ else:
231
+ azimuth_deg = 0.0 # Undefined when sun is at zenith, assign 0
232
+ else: # Handle array case
233
+ cos_azimuth_term = np.full_like(zenith_deg, 0.0)
234
+ # Calculate term only for valid entries
235
+ cos_azimuth_term[valid_mask] = (np.sin(dec_rad[valid_mask]) * np.cos(lat_rad) - np.cos(dec_rad[valid_mask]) * np.sin(lat_rad) * np.cos(ha_rad[valid_mask])) / sin_zenith[valid_mask]
236
+ cos_azimuth_term = np.clip(cos_azimuth_term, -1.0, 1.0)
237
+
238
+ azimuth_rad_provisional = np.arccos(cos_azimuth_term)
239
+ azimuth_deg_provisional = np.degrees(azimuth_rad_provisional)
240
+
241
+ # Adjust based on hour angle (vectorized)
242
+ azimuth_deg = np.where(hour_angle_deg > 0, 360.0 - azimuth_deg_provisional, azimuth_deg_provisional)
243
+ # Ensure invalid entries remain 0
244
+ azimuth_deg[~valid_mask] = 0.0
245
+
246
+ # self.validate_angle(azimuth_deg, "Azimuth angle", 0, 360)
247
+ return azimuth_deg
248
+
249
+ def extraterrestrial_radiation_normal(self, day_of_year: Union[int, np.ndarray]) -> Union[float, np.ndarray]:
250
+ """
251
+ Calculate extraterrestrial solar radiation normal to the sun's rays.
252
+ Reference: Duffie & Beckman (4th Ed.), Eq 1.4.1b.
253
+ Args:
254
+ day_of_year: Day of the year (1-365 or 1-366).
255
+ Returns:
256
+ Extraterrestrial normal irradiance (G_on) in W/m².
257
+ """
258
+ B = (day_of_year - 1) * 360 / 365 # degrees
259
+ B_rad = np.radians(B)
260
+ G_on = SOLAR_CONSTANT * (1.000110 + 0.034221 * np.cos(B_rad) + 0.001280 * np.sin(B_rad)
261
+ + 0.000719 * np.cos(2 * B_rad) + 0.000077 * np.sin(2 * B_rad))
262
+ return G_on
263
+
264
+ def angle_of_incidence(self, latitude_deg: float, declination_deg: float, hour_angle_deg: float,
265
+ surface_tilt_deg: float, surface_azimuth_deg: float) -> float:
266
+ """
267
+ Calculate the angle of incidence of beam radiation on a tilted surface.
268
+ Reference: Duffie & Beckman (4th Ed.), Eq 1.6.2.
269
+ Args:
270
+ latitude_deg: Latitude.
271
+ declination_deg: Solar declination.
272
+ hour_angle_deg: Solar hour angle.
273
+ surface_tilt_deg: Surface tilt angle from horizontal (0=horizontal, 90=vertical).
274
+ surface_azimuth_deg: Surface azimuth angle (0=N, 90=E, 180=S, 270=W).
275
+ Returns:
276
+ Angle of incidence in degrees (0-90). Returns > 90 if sun is behind surface.
277
+ """
278
+ lat_rad = np.radians(latitude_deg)
279
+ dec_rad = np.radians(declination_deg)
280
+ ha_rad = np.radians(hour_angle_deg)
281
+ tilt_rad = np.radians(surface_tilt_deg)
282
+ surf_azim_rad = np.radians(surface_azimuth_deg)
283
+
284
+ # Convert surface azimuth from N=0 to S=0 convention used in formula
285
+ gamma_rad = surf_azim_rad - np.pi # S=0, E=pi/2, W=-pi/2
286
+
287
+ cos_theta = (np.sin(lat_rad) * np.sin(dec_rad) * np.cos(tilt_rad)
288
+ - np.cos(lat_rad) * np.sin(dec_rad) * np.sin(tilt_rad) * np.cos(gamma_rad)
289
+ + np.cos(lat_rad) * np.cos(dec_rad) * np.cos(ha_rad) * np.cos(tilt_rad)
290
+ + np.sin(lat_rad) * np.cos(dec_rad) * np.cos(ha_rad) * np.sin(tilt_rad) * np.cos(gamma_rad)
291
+ + np.cos(dec_rad) * np.sin(ha_rad) * np.sin(tilt_rad) * np.sin(gamma_rad))
292
+
293
+ # Clamp cos_theta to [-1, 1]
294
+ cos_theta = np.clip(cos_theta, -1.0, 1.0)
295
+ theta_rad = np.arccos(cos_theta)
296
+ theta_deg = np.degrees(theta_rad)
297
+
298
+ return theta_deg
299
+
300
+ def ashrae_clear_sky_radiation(self, day_of_year: int, hour: float, latitude_deg: float,
301
+ longitude_deg: float, standard_meridian_deg: float,
302
+ altitude_m: float = 0)
303
+ -> Tuple[float, float, float]:
304
  """
305
+ Estimate DNI and DHI using ASHRAE Clear Sky model.
306
+ Reference: ASHRAE HOF 2017, Ch. 14, Eq. 14.18-14.21 (Simplified version)
 
307
  Args:
308
+ day_of_year, hour, latitude_deg, longitude_deg, standard_meridian_deg: Location/time info.
309
+ altitude_m: Site altitude in meters.
 
 
310
  Returns:
311
+ Tuple: (DNI in W/m², DHI in W/m², GHI in W/m²)
312
+ """
313
+ # 1. Calculate Solar Angles
314
+ eot = self.equation_of_time(day_of_year)
315
+ solar_time = self.solar_time(hour, eot, longitude_deg, standard_meridian_deg)
316
+ ha = self.solar_hour_angle(solar_time)
317
+ dec = self.solar_declination(day_of_year)
318
+ zenith, alt = self.solar_zenith_altitude(latitude_deg, dec, ha)
319
+
320
+ if alt <= 0: # Sun below horizon
321
+ return 0.0, 0.0, 0.0
322
+
323
+ # 2. Extraterrestrial Radiation
324
+ G_on = self.extraterrestrial_radiation_normal(day_of_year)
325
+ G_oh = G_on * np.cos(np.radians(zenith)) # On horizontal surface
326
+
327
+ # 3. ASHRAE Clear Sky Model Parameters (simplified)
328
+ # These depend on atmospheric conditions (clearness, water vapor, etc.)
329
+ # Using typical clear day values for illustration
330
+ A = 1160 + 75 * np.sin(np.radians(360 * (day_of_year - 275) / 365)) # Apparent extraterrestrial irradiance
331
+ k = 0.174 + 0.035 * np.sin(np.radians(360 * (day_of_year - 100) / 365)) # Optical depth
332
+ C = 0.095 + 0.04 * np.sin(np.radians(360 * (day_of_year - 100) / 365)) # Sky diffuse factor
333
+
334
+ # Air mass (simplified Kasten and Young, 1989)
335
+ m = 1 / (np.cos(np.radians(zenith)) + 0.50572 * (96.07995 - zenith)**(-1.6364))
336
+ # Altitude correction for air mass (approximate)
337
+ pressure_ratio = (Psychrometrics().pressure_at_altitude(altitude_m) / ATMOSPHERIC_PRESSURE)
338
+ m *= pressure_ratio
339
+
340
+ # 4. Calculate DNI and DHI
341
+ dni = A * np.exp(-k * m)
342
+ dhi = C * dni
343
+ ghi = dni * np.cos(np.radians(zenith)) + dhi
344
+
345
+ # Ensure non-negative
346
+ dni = max(0.0, dni)
347
+ dhi = max(0.0, dhi)
348
+ ghi = max(0.0, ghi)
349
+
350
+ return dni, dhi, ghi
351
+
352
+ def total_radiation_on_surface(self, dni: float, dhi: float, ghi: float,
353
+ zenith_deg: float, solar_azimuth_deg: float,
354
+ surface_tilt_deg: float, surface_azimuth_deg: float,
355
+ ground_reflectance: float = 0.2)
356
+ -> Tuple[float, float, float, float]:
357
+ """
358
+ Calculate total solar radiation incident on a tilted surface.
359
+ Uses isotropic sky model for diffuse radiation.
360
+ Reference: Duffie & Beckman (4th Ed.), Section 2.15, 2.16.
361
  Args:
362
+ dni, dhi, ghi: Direct Normal, Diffuse Horizontal, Global Horizontal Irradiance (W/m²).
363
+ zenith_deg, solar_azimuth_deg: Solar position angles.
364
+ surface_tilt_deg, surface_azimuth_deg: Surface orientation angles.
365
+ ground_reflectance: Albedo of the ground (0-1).
 
366
  Returns:
367
+ Tuple: (Total G_t, Direct G_b,t, Diffuse G_d,t, Reflected G_r,t) on surface (W/m²).
368
+ """
369
+ if zenith_deg >= 90: # Sun below horizon
370
+ return 0.0, 0.0, 0.0, 0.0
371
+
372
+ # 1. Angle of Incidence (theta)
373
+ # Need latitude, declination, hour angle for precise calculation, OR use zenith/azimuth
374
+ # Using zenith/azimuth method (Duffie & Beckman Eq 1.6.3)
375
+ cos_theta = (np.cos(np.radians(zenith_deg)) * np.cos(np.radians(surface_tilt_deg)) +
376
+ np.sin(np.radians(zenith_deg)) * np.sin(np.radians(surface_tilt_deg)) *
377
+ np.cos(np.radians(solar_azimuth_deg - surface_azimuth_deg)))
378
+ cos_theta = np.clip(cos_theta, -1.0, 1.0)
379
+ theta_deg = np.degrees(np.arccos(cos_theta))
380
+
381
+ # 2. Direct Beam component on tilted surface (G_b,t)
382
+ # Only if sun is in front of the surface (theta <= 90)
383
+ G_b_t = dni * cos_theta if theta_deg <= 90.0 else 0.0
384
+ G_b_t = max(0.0, G_b_t)
385
+
386
+ # 3. Diffuse component on tilted surface (G_d,t) - Isotropic Sky Model
387
+ # View factor from surface to sky
388
+ F_sky = (1 + np.cos(np.radians(surface_tilt_deg))) / 2
389
+ G_d_t = dhi * F_sky
390
+ G_d_t = max(0.0, G_d_t)
391
+
392
+ # 4. Ground Reflected component on tilted surface (G_r,t)
393
+ # View factor from surface to ground
394
+ F_ground = (1 - np.cos(np.radians(surface_tilt_deg))) / 2
395
+ G_r_t = ghi * ground_reflectance * F_ground
396
+ G_r_t = max(0.0, G_r_t)
397
+
398
+ # 5. Total radiation on tilted surface
399
+ G_t = G_b_t + G_d_t + G_r_t
400
 
401
+ return G_t, G_b_t, G_d_t, G_r_t
402
 
403
  class HeatTransferCalculations:
404
  """Class for heat transfer calculations."""
405
+
406
+ def __init__(self, debug_mode: bool = False):
407
  """
408
  Initialize heat transfer calculations with psychrometrics and solar calculations.
409
  Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 16.
410
+ Args:
411
+ debug_mode: Enable debug logging if True.
412
  """
413
  self.psychrometrics = Psychrometrics()
414
  self.solar = SolarCalculations()
415
+ self.debug_mode = debug_mode
416
+ if debug_mode:
417
+ logger.setLevel(logging.DEBUG)
418
+
419
+ def conduction_heat_transfer(self, u_value: Union[float, np.ndarray], area: Union[float, np.ndarray],
420
+ delta_t: Union[float, np.ndarray]) -> Union[float, np.ndarray]:
421
  """
422
  Calculate heat transfer via conduction.
423
  Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 18, Equation 18.1.
 
424
  Args:
425
+ u_value: U-value(s) of the component(s) in W/(m²·K)
426
+ area: Area(s) of the component(s) in m²
427
+ delta_t: Temperature difference(s) in °C or K
 
428
  Returns:
429
+ Heat transfer rate(s) in W
430
  """
431
+ if isinstance(u_value, np.ndarray) or isinstance(area, np.ndarray) or isinstance(delta_t, np.ndarray):
432
+ u_value = np.asarray(u_value)
433
+ area = np.asarray(area)
434
+ delta_t = np.asarray(delta_t)
435
+ if np.any(u_value < 0) or np.any(area < 0):
436
+ raise ValueError("U-value and area must be non-negative")
437
+ elif u_value < 0 or area < 0:
438
  raise ValueError("U-value and area must be non-negative")
439
+
440
  q = u_value * area * delta_t
441
  return q
442
 
443
+ def infiltration_heat_transfer(self, flow_rate: Union[float, np.ndarray], delta_t: Union[float, np.ndarray],
444
+ t_db: Union[float, np.ndarray], rh: Union[float, np.ndarray],
445
+ p_atm: Union[float, np.ndarray] = ATMOSPHERIC_PRESSURE)
446
+ -> Union[float, np.ndarray]:
447
  """
448
  Calculate sensible heat transfer due to infiltration or ventilation.
449
  Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 18, Equation 18.5.
 
450
  Args:
451
+ flow_rate: Air flow rate(s) in m³/s
452
+ delta_t: Temperature difference(s) in °C or K
453
+ t_db: Dry-bulb temperature(s) for air properties in °C
454
+ rh: Relative humidity(ies) in % (0-100)
455
+ p_atm: Atmospheric pressure(s) in Pa
 
456
  Returns:
457
+ Sensible heat transfer rate(s) in W
458
  """
459
+ # Convert inputs to numpy arrays if any input is an array
460
+ is_array = any(isinstance(arg, np.ndarray) for arg in [flow_rate, delta_t, t_db, rh, p_atm])
461
+ if is_array:
462
+ flow_rate = np.asarray(flow_rate)
463
+ delta_t = np.asarray(delta_t)
464
+ t_db = np.asarray(t_db)
465
+ rh = np.asarray(rh)
466
+ p_atm = np.asarray(p_atm)
467
+ if np.any(flow_rate < 0):
468
+ raise ValueError("Flow rate cannot be negative")
469
+ elif flow_rate < 0:
470
  raise ValueError("Flow rate cannot be negative")
471
+
472
+ # Calculate air density and specific heat using psychrometrics (vectorized if needed)
473
  w = self.psychrometrics.humidity_ratio(t_db, rh, p_atm)
474
  rho = self.psychrometrics.density(t_db, w, p_atm)
475
  c_p = 1006 + 1860 * w # Specific heat of moist air in J/(kg·K)
476
+
477
  q = flow_rate * rho * c_p * delta_t
478
  return q
479
 
480
+ def infiltration_latent_heat_transfer(self, flow_rate: Union[float, np.ndarray], delta_w: Union[float, np.ndarray],
481
+ t_db: Union[float, np.ndarray], # Temp for density/hfg calc
482
+ p_atm: Union[float, np.ndarray] = ATMOSPHERIC_PRESSURE)
483
+ -> Union[float, np.ndarray]:
484
  """
485
  Calculate latent heat transfer due to infiltration or ventilation.
486
  Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 18, Equation 18.6.
 
487
  Args:
488
+ flow_rate: Air flow rate(s) in m³/s
489
+ delta_w: Humidity ratio difference(s) in kg/kg
490
+ t_db: Dry-bulb temperature(s) for air properties in °C
491
+ p_atm: Atmospheric pressure(s) in Pa
 
 
492
  Returns:
493
+ Latent heat transfer rate(s) in W
494
  """
495
+ is_array = any(isinstance(arg, np.ndarray) for arg in [flow_rate, delta_w, t_db, p_atm])
496
+ if is_array:
497
+ flow_rate = np.asarray(flow_rate)
498
+ delta_w = np.asarray(delta_w)
499
+ t_db = np.asarray(t_db)
500
+ p_atm = np.asarray(p_atm)
501
+ if np.any(flow_rate < 0):
502
+ raise ValueError("Flow rate cannot be negative")
503
+ # Delta_w can be negative (humidification)
504
+ elif flow_rate < 0:
505
+ raise ValueError("Flow rate cannot be negative")
506
+
507
  # Calculate air density and latent heat
508
+ rho = self.psychrometrics.density(t_db, w=0.008, p_atm=p_atm) # Approximate density
509
+ h_fg = self.psychrometrics.latent_heat_of_vaporization(t_db) # J/kg
510
+
 
511
  q = flow_rate * rho * h_fg * delta_w
512
  return q
513
 
514
+ def wind_pressure_difference(self, wind_speed: float, wind_pressure_coeff: float = 0.6,
515
+ air_density: float = 1.2) -> float:
516
  """
517
  Calculate pressure difference due to wind.
518
  Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 16, Equation 16.3.
 
519
  Args:
520
+ wind_speed: Wind speed in m/s.
521
+ wind_pressure_coeff: Wind pressure coefficient (depends on building shape, location).
522
+ air_density: Air density in kg/m³.
523
  Returns:
524
+ Pressure difference in Pa.
525
  """
526
  if wind_speed < 0:
527
  raise ValueError("Wind speed cannot be negative")
528
+ delta_p = 0.5 * wind_pressure_coeff * air_density * wind_speed**2
 
 
 
529
  return delta_p
530
 
531
+ def stack_pressure_difference(self, height: float, t_inside_k: float, t_outside_k: float,
532
+ p_atm: float = ATMOSPHERIC_PRESSURE) -> float:
533
  """
534
  Calculate pressure difference due to stack effect.
535
  Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 16, Equation 16.4.
 
536
  Args:
537
+ height: Height difference in m (e.g., NPL to opening).
538
+ t_inside_k: Inside absolute temperature in K.
539
+ t_outside_k: Outside absolute temperature in K.
540
+ p_atm: Atmospheric pressure in Pa.
541
  Returns:
542
+ Pressure difference in Pa.
543
+ """
544
+ if height < 0 or t_inside_k <= 0 or t_outside_k <= 0:
545
+ raise ValueError("Height and absolute temperatures must be positive")
546
+
547
+ g = 9.80665 # Gravitational acceleration in m/s²
548
+ r_da = GAS_CONSTANT_DRY_AIR
549
+
550
+ # Calculate Density inside and outside (approximating as dry air for simplicity)
551
+ rho_inside = p_atm / (r_da * t_inside_k)
552
+ rho_outside = p_atm / (r_da * t_outside_k)
553
+
554
+ # Pressure difference = g * height * (rho_outside - rho_inside)
555
+ delta_p = g * height * (rho_outside - rho_inside)
556
  return delta_p
557
 
558
  def combined_pressure_difference(self, wind_pd: float, stack_pd: float) -> float:
559
+ """ Calculate combined pressure difference from wind and stack effects (simple superposition). """
 
 
 
 
 
 
 
 
 
 
560
  delta_p = math.sqrt(wind_pd**2 + stack_pd**2)
561
  return delta_p
562
 
563
+ def crack_method_infiltration(self, crack_length: float, crack_width: float, delta_p: float,
564
+ flow_exponent: float = 0.65) -> float:
565
  """
566
+ Calculate infiltration flow rate using the power law (crack) method.
567
  Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 16, Equation 16.5.
568
+ Formula: Q = flow_coeff * area * (delta_p / 0.001)^n
569
  Args:
570
+ crack_length: Length of cracks in m.
571
+ crack_width: Width of cracks in m.
572
+ delta_p: Pressure difference across cracks in Pa.
573
+ flow_exponent: Flow exponent (n), typically 0.5 to 1.0 (default 0.65).
574
  Returns:
575
+ Infiltration flow rate (Q) in m³/s.
576
  """
577
  if crack_length < 0 or crack_width < 0 or delta_p < 0:
578
+ raise ValueError("Crack length, crack width, and pressure difference must be non-negative")
579
+ if not 0.5 <= flow_exponent <= 1.0:
580
+ logger.warning(f"Flow exponent {flow_exponent} is outside the typical range [0.5, 1.0]")
581
+
582
+ flow_coeff = 0.65 # ASHRAE default flow coefficient
583
  area = crack_length * crack_width
584
+ q = flow_coeff * area * ((delta_p / 0.001) ** flow_exponent)
 
585
  return q
586
 
 
587
  # Example usage
588
  if __name__ == "__main__":
589
+ heat_transfer = HeatTransferCalculations(debug_mode=True)
590
+
 
591
  # Example conduction calculation
592
+ u_value = 0.5
593
+ area = 20.0
594
+ delta_t = 10.0
595
  q_conduction = heat_transfer.conduction_heat_transfer(u_value, area, delta_t)
596
  logger.info(f"Conduction heat transfer: {q_conduction:.2f} W")
597
+
598
+ # Example infiltration calculation (Sensible)
599
+ flow_rate = 0.05
600
+ delta_t_inf = 10.0
601
+ t_db_inf = 25.0
602
+ rh_inf = 50.0
603
+ q_inf_sens = heat_transfer.infiltration_heat_transfer(flow_rate, delta_t_inf, t_db_inf, rh_inf)
604
+ logger.info(f"Infiltration sensible heat transfer: {q_inf_sens:.2f} W")
605
+
606
+ # Example infiltration calculation (Latent)
607
+ delta_w_inf = 0.002 # kg/kg
608
+ q_inf_lat = heat_transfer.infiltration_latent_heat_transfer(flow_rate, delta_w_inf, t_db_inf)
609
+ logger.info(f"Infiltration latent heat transfer: {q_inf_lat:.2f} W")
610
+
611
+ # Example Solar Calculation
612
+ logger.info("--- Solar Calculation Example ---")
613
+ latitude = 34.0 # Los Angeles
614
+ longitude = -118.0
615
+ std_meridian = -120.0 # PST
616
+ day_of_year = 80 # Around March 21
617
+ hour = 13.5 # Local standard time
618
+ altitude_m = 100
619
+
620
+ # Get clear sky radiation
621
+ dni, dhi, ghi = heat_transfer.solar.ashrae_clear_sky_radiation(
622
+ day_of_year, hour, latitude, longitude, std_meridian, altitude_m
623
+ )
624
+ logger.info(f"Clear Sky Radiation (W/m²): DNI={dni:.1f}, DHI={dhi:.1f}, GHI={ghi:.1f}")
625
+
626
+ # Calculate radiation on a south-facing vertical wall
627
+ eot = heat_transfer.solar.equation_of_time(day_of_year)
628
+ solar_time = heat_transfer.solar.solar_time(hour, eot, longitude, std_meridian)
629
+ ha = heat_transfer.solar.solar_hour_angle(solar_time)
630
+ dec = heat_transfer.solar.solar_declination(day_of_year)
631
+ zenith, alt = heat_transfer.solar.solar_zenith_altitude(latitude, dec, ha)
632
+ sol_azimuth = heat_transfer.solar.solar_azimuth(latitude, dec, ha, zenith)
633
+
634
+ surface_tilt = 90.0
635
+ surface_azimuth = 180.0 # South facing
636
+ ground_reflectance = 0.2
637
+
638
+ if alt > 0:
639
+ G_t, G_b_t, G_d_t, G_r_t = heat_transfer.solar.total_radiation_on_surface(
640
+ dni, dhi, ghi, zenith, sol_azimuth, surface_tilt, surface_azimuth, ground_reflectance
641
+ )
642
+ logger.info(f"Radiation on South Vertical Wall (W/m²): Total={G_t:.1f}, Beam={G_b_t:.1f}, Diffuse={G_d_t:.1f}, Reflected={G_r_t:.1f}")
643
+ theta = heat_transfer.solar.angle_of_incidence(latitude, dec, ha, surface_tilt, surface_azimuth)
644
+ logger.info(f"Incidence Angle: {theta:.1f} degrees")
645
+ else:
646
+ logger.info("Sun is below horizon.")
647
+
648
+ # Vectorized example (e.g., hourly conduction)
649
+ logger.info("--- Vectorized Conduction Example ---")
650
+ hours_array = np.arange(24)
651
+ delta_t_hourly = 10 * np.sin(np.pi * (hours_array - 8) / 12) + 5 # Example hourly delta T
652
+ delta_t_hourly[delta_t_hourly < 0] = 0 # Only positive delta T
653
+ q_conduction_hourly = heat_transfer.conduction_heat_transfer(u_value, area, delta_t_hourly)
654
+ logger.info(f"Peak Hourly Conduction: {np.max(q_conduction_hourly):.2f} W")