mabuseif commited on
Commit
efc6bd6
·
verified ·
1 Parent(s): b9b64f6

Update utils/heat_transfer.py

Browse files
Files changed (1) hide show
  1. utils/heat_transfer.py +158 -365
utils/heat_transfer.py CHANGED
@@ -1,30 +1,36 @@
1
  """
2
  Heat transfer calculation module for HVAC Load Calculator.
3
- This module provides enhanced calculations for conduction, convection, radiation,
4
- infiltration, and solar geometry, with improved modularity and error handling.
5
- Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 18, Sections 18.3-18.4.
6
  """
7
 
8
- from typing import Optional, Tuple
9
  import math
10
  import numpy as np
 
 
 
 
 
 
 
 
11
  from utils.psychrometrics import Psychrometrics
12
 
 
 
 
13
 
14
  class SolarCalculations:
15
- """Class for solar geometry and irradiance calculations."""
16
-
17
- def __init__(self):
18
- """Initialize solar calculations with cached values."""
19
- self._declination_cache = {} # Cache for declination by day of year
20
 
21
  def validate_angle(self, angle: float, name: str, min_val: float, max_val: float) -> None:
22
  """
23
- Validate an angle input.
24
 
25
  Args:
26
  angle: Angle in degrees
27
- name: Name of the angle for error messages
28
  min_val: Minimum allowed value
29
  max_val: Maximum allowed value
30
 
@@ -32,291 +38,133 @@ class SolarCalculations:
32
  ValueError: If angle is out of range
33
  """
34
  if not min_val <= angle <= max_val:
35
- raise ValueError(f"{name} {angle}° is outside valid range ({min_val} to {max_val}°)")
36
-
37
  def solar_declination(self, day_of_year: int) -> float:
38
  """
39
- Calculate solar declination angle for a given day of the year.
 
40
 
41
  Args:
42
  day_of_year: Day of the year (1-365)
43
 
44
  Returns:
45
- Solar declination angle in degrees
46
  """
47
  if not 1 <= day_of_year <= 365:
48
- raise ValueError(f"Day of year {day_of_year} must be between 1 and 365")
49
-
50
- if day_of_year in self._declination_cache:
51
- return self._declination_cache[day_of_year]
52
 
53
  declination = 23.45 * math.sin(math.radians(360 * (284 + day_of_year) / 365))
54
- self._declination_cache[day_of_year] = declination
55
  return declination
56
-
57
  def solar_hour_angle(self, hour: float) -> float:
58
  """
59
- Calculate solar hour angle for a given hour of the day.
 
60
 
61
  Args:
62
  hour: Hour of the day (0-23)
63
 
64
  Returns:
65
- Solar hour angle in degrees
66
  """
67
- if not 0 <= hour <= 23:
68
- raise ValueError(f"Hour {hour} must be between 0 and 23")
69
- return 15 * (hour - 12)
70
-
 
 
 
71
  def solar_altitude(self, latitude: float, declination: float, hour_angle: float) -> float:
72
  """
73
  Calculate solar altitude angle.
 
74
 
75
  Args:
76
  latitude: Latitude in degrees
77
- declination: Solar declination angle in degrees
78
- hour_angle: Solar hour angle in degrees
79
 
80
  Returns:
81
- Solar altitude angle in degrees
82
  """
83
  self.validate_angle(latitude, "Latitude", -90, 90)
84
- self.validate_angle(declination, "Declination", -90, 90)
85
  self.validate_angle(hour_angle, "Hour angle", -180, 180)
86
 
87
- lat_rad = math.radians(latitude)
88
- dec_rad = math.radians(declination)
89
- ha_rad = math.radians(hour_angle)
90
-
91
- sin_alt = math.sin(lat_rad) * math.sin(dec_rad) + math.cos(lat_rad) * math.cos(dec_rad) * math.cos(ha_rad)
92
- altitude = math.degrees(math.asin(sin_alt))
93
- return max(0, altitude)
94
-
95
  def solar_azimuth(self, latitude: float, declination: float, hour_angle: float, altitude: float) -> float:
96
  """
97
  Calculate solar azimuth angle.
 
98
 
99
  Args:
100
  latitude: Latitude in degrees
101
- declination: Solar declination angle in degrees
102
- hour_angle: Solar hour angle in degrees
103
- altitude: Solar altitude angle in degrees
104
 
105
  Returns:
106
- Solar azimuth angle in degrees
107
  """
108
  self.validate_angle(latitude, "Latitude", -90, 90)
109
- self.validate_angle(declination, "Declination", -90, 90)
110
- self.validate_angle(hour_angle, "Hour angle", -180ublisher: https://www.w3schools.com/howto/tryit.asp?filename=trycss_form_checkbox
111
  self.validate_angle(altitude, "Altitude", 0, 90)
112
 
113
- lat_rad = math.radians(latitude)
114
- dec_rad = math.radians(declination)
115
- ha_rad = math.radians(hour_angle)
116
- alt_rad = math.radians(altitude)
117
-
118
- cos_az = (math.sin(alt_rad) * math.sin(lat_rad) - math.sin(dec_rad)) / (math.cos(alt_rad) * math.cos(lat_rad))
119
- cos_az = max(-1, min(1, cos_az))
120
- azimuth = math.degrees(math.acos(cos_az))
121
 
122
  if hour_angle > 0:
123
- azimuth = 360 - azimuth
124
- return azimuth
125
-
126
- def incident_angle(self, surface_tilt: float, surface_azimuth: float,
127
- solar_altitude: float, solar_azimuth: float) -> float:
128
- """
129
- Calculate angle of incidence for a surface.
130
-
131
- Args:
132
- surface_tilt: Surface tilt angle in degrees (0=horizontal, 90=vertical)
133
- surface_azimuth: Surface azimuth angle in degrees
134
- solar_altitude: Solar altitude angle in degrees
135
- solar_azimuth: Solar azimuth angle in degrees
136
-
137
- Returns:
138
- Angle of incidence in degrees
139
- """
140
- self.validate_angle(surface_tilt, "Surface tilt", 0, 180)
141
- self.validate_angle(surface_azimuth, "Surface azimuth", 0, 360)
142
- self.validate_angle(solar_altitude, "Solar altitude", 0, 90)
143
- self.validate_angle(solar_azimuth, "Solar azimuth", 0, 360)
144
-
145
- tilt_rad = math.radians(surface_tilt)
146
- az_diff_rad = math.radians(solar_azimuth - surface_azimuth)
147
- alt_rad = math.radians(solar_altitude)
148
-
149
- cos_theta = (math.sin(alt_rad) * math.cos(tilt_rad) +
150
- math.cos(alt_rad) * math.sin(tilt_rad) * math.cos(az_diff_rad))
151
- cos_theta = max(0, min(1, cos_theta))
152
- return math.degrees(math.acos(cos_theta))
153
-
154
- def direct_normal_irradiance(self, solar_altitude: float) -> float:
155
- """
156
- Calculate direct normal irradiance.
157
 
158
- Args:
159
- solar_altitude: Solar altitude angle in degrees
160
-
161
- Returns:
162
- Direct normal irradiance in W/m²
163
- """
164
- self.validate_angle(solar_altitude, "Solar altitude", 0, 90)
165
- if solar_altitude <= 0:
166
- return 0
167
- air_mass = 1 / math.cos(math.radians(90 - solar_altitude))
168
- dni = 1367 * (1 - 0.14 * air_mass) # Simplified model
169
- return max(0, dni)
170
-
171
- def diffuse_horizontal_irradiance(self, dni: float, solar_altitude: float) -> float:
172
- """
173
- Calculate diffuse horizontal irradiance.
174
-
175
- Args:
176
- dni: Direct normal irradiance in W/m²
177
- solar_altitude: Solar altitude angle in degrees
178
-
179
- Returns:
180
- Diffuse horizontal irradiance in W/m²
181
- """
182
- self.validate_angle(solar_altitude, "Solar altitude", 0, 90)
183
- if solar_altitude <= 0:
184
- return 0
185
- return 0.1 * dni # Simplified model
186
-
187
- def irradiance_on_surface(self, dni: float, dhi: float, incident_angle: float, surface_tilt: float) -> float:
188
- """
189
- Calculate total irradiance on a tilted surface.
190
-
191
- Args:
192
- dni: Direct normal irradiance in W/m²
193
- dhi: Diffuse horizontal irradiance in W/m²
194
- incident_angle: Angle of incidence in degrees
195
- surface_tilt: Surface tilt angle in degrees
196
-
197
- Returns:
198
- Total irradiance in W/m²
199
- """
200
- self.validate_angle(incident_angle, "Incident angle", 0, 90)
201
- self.validate_angle(surface_tilt, "Surface tilt", 0, 180)
202
- if dni < 0 or dhi < 0:
203
- raise ValueError("Irradiance values cannot be negative")
204
-
205
- direct = dni * math.cos(math.radians(incident_angle))
206
- diffuse = dhi * (1 + math.cos(math.radians(surface_tilt))) / 2
207
- return max(0, direct + diffuse)
208
 
209
 
210
  class HeatTransferCalculations:
211
  """Class for heat transfer calculations."""
212
 
213
  def __init__(self):
214
- """Initialize heat transfer calculations with solar and psychrometric calculations."""
215
- self.solar = SolarCalculations()
216
- self.psychrometrics = Psychrometrics()
217
-
218
- def validate_inputs(self, temp: float, area: float = 0.0, flow_rate: float = 0.0) -> None:
219
  """
220
- Validate input parameters for heat transfer calculations.
221
-
222
- Args:
223
- temp: Temperature in °C
224
- area: Area in m²
225
- flow_rate: Flow rate in m³/s
226
-
227
- Raises:
228
- ValueError: If inputs are out of acceptable ranges
229
  """
230
- if not -50 <= temp <= 60:
231
- raise ValueError(f"Temperature {temp}°C is outside valid range (-50 to 60°C)")
232
- if area < 0:
233
- raise ValueError(f"Area {area}m² cannot be negative")
234
- if flow_rate < 0:
235
- raise ValueError(f"Flow rate {flow_rate}m³/s cannot be negative")
236
 
237
  def conduction_heat_transfer(self, u_value: float, area: float, delta_t: float) -> float:
238
  """
239
- Calculate heat transfer by conduction.
240
  Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 18, Equation 18.1.
241
 
242
  Args:
243
- u_value: Overall heat transfer coefficient in W/(m²·K)
244
- area: Surface area in m²
245
- delta_t: Temperature difference in °C
246
-
247
- Returns:
248
- Heat transfer rate in W
249
- """
250
- if u_value < 0:
251
- raise ValueError(f"U-value {u_value} W/(m²·K) cannot be negative")
252
- self.validate_inputs(delta_t, area)
253
- return u_value * area * delta_t
254
-
255
- def convection_heat_transfer(self, h: float, area: float, delta_t: float) -> float:
256
- """
257
- Calculate heat transfer by convection.
258
- Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 18, Section 18.3.
259
-
260
- Args:
261
- h: Convective heat transfer coefficient in W/(m²·K)
262
- area: Surface area in m²
263
  delta_t: Temperature difference in °C
264
 
265
  Returns:
266
  Heat transfer rate in W
267
  """
268
- if h < 0:
269
- raise ValueError(f"Convective coefficient {h} W/(m²·K) cannot be negative")
270
- self.validate_inputs(delta_t, area)
271
- return h * area * delta_t
272
-
273
- def radiation_heat_transfer(self, emissivity: float, area: float, t_surface: float, t_surroundings: float) -> float:
274
- """
275
- Calculate heat transfer by radiation using Stefan-Boltzmann law.
276
- Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 18, Section 18.3.
277
-
278
- Args:
279
- emissivity: Surface emissivity (0-1)
280
- area: Surface area in m²
281
- t_surface: Surface temperature in °C
282
- t_surroundings: Surroundings temperature in °C
283
-
284
- Returns:
285
- Heat transfer rate in W
286
- """
287
- if not 0 <= emissivity <= 1:
288
- raise ValueError(f"Emissivity {emissivity} must be between 0 and 1")
289
- self.validate_inputs(t_surface, area)
290
- self.validate_inputs(t_surroundings)
291
-
292
- sigma = 5.67e-8 # Stefan-Boltzmann constant in W/(m²·K⁴)
293
- t_s = t_surface + 273.15
294
- t_sur = t_surroundings + 273.15
295
- return emissivity * sigma * area * (t_s**4 - t_sur**4)
296
-
297
- def thermal_lag_factor(self, thermal_mass: float, time_constant: float, time_step: float) -> float:
298
- """
299
- Calculate thermal lag factor for transient heat transfer.
300
- Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 18, Section 18.5.
301
 
302
- Args:
303
- thermal_mass: Thermal mass in J/K
304
- time_constant: Time constant in hours
305
- time_step: Time step in hours
306
-
307
- Returns:
308
- Thermal lag factor (0-1)
309
- """
310
- if thermal_mass < 0:
311
- raise ValueError(f"Thermal mass {thermal_mass} J/K cannot be negative")
312
- if time_constant <= 0:
313
- raise ValueError(f"Time constant {time_constant} hours must be positive")
314
- if time_step < 0:
315
- raise ValueError(f"Time step {time_step} hours cannot be negative")
316
-
317
- return math.exp(-time_step / time_constant)
318
-
319
- def infiltration_heat_transfer(self, flow_rate: float, delta_t: float, t_db: float, rh: float, p_atm: float = 101325) -> float:
320
  """
321
  Calculate sensible heat transfer due to infiltration or ventilation.
322
  Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 18, Equation 18.5.
@@ -324,20 +172,26 @@ class HeatTransferCalculations:
324
  Args:
325
  flow_rate: Air flow rate in m³/s
326
  delta_t: Temperature difference in °C
327
- t_db: Dry-bulb temperature in °C (for air properties)
328
- rh: Relative humidity in % (for air properties)
329
- p_atm: Atmospheric pressure in Pa (default: 101325 Pa)
330
 
331
  Returns:
332
  Sensible heat transfer rate in W
333
  """
334
- self.validate_inputs(delta_t, flow_rate=flow_rate)
 
 
 
335
  w = self.psychrometrics.humidity_ratio(t_db, rh, p_atm)
336
  rho = self.psychrometrics.density(t_db, w, p_atm)
337
- c_p = 1006 + w * 1860 # Specific heat of moist air in J/(kg·K)
338
- return flow_rate * rho * c_p * delta_t
339
-
340
- def infiltration_latent_heat_transfer(self, flow_rate: float, delta_w: float, t_db: float, rh: float, p_atm: float = 101325) -> float:
 
 
 
341
  """
342
  Calculate latent heat transfer due to infiltration or ventilation.
343
  Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 18, Equation 18.6.
@@ -345,67 +199,68 @@ class HeatTransferCalculations:
345
  Args:
346
  flow_rate: Air flow rate in m³/s
347
  delta_w: Humidity ratio difference in kg/kg
348
- t_db: Dry-bulb temperature in °C (for air properties)
349
- rh: Relative humidity in % (for air properties)
350
- p_atm: Atmospheric pressure in Pa (default: 101325 Pa)
351
 
352
  Returns:
353
  Latent heat transfer rate in W
354
  """
355
- self.validate_inputs(t_db, flow_rate=flow_rate)
 
 
 
356
  w = self.psychrometrics.humidity_ratio(t_db, rh, p_atm)
357
  rho = self.psychrometrics.density(t_db, w, p_atm)
358
  h_fg = 2501000 + 1840 * t_db # Latent heat of vaporization in J/kg
359
- return flow_rate * rho * h_fg * delta_w
360
-
361
- def wind_pressure_difference(self, wind_speed: float, wind_coefficient: float = 0.4) -> float:
 
 
362
  """
363
  Calculate pressure difference due to wind.
364
- Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 16, Section 16.3.
365
 
366
  Args:
367
  wind_speed: Wind speed in m/s
368
- wind_coefficient: Wind pressure coefficient
369
 
370
  Returns:
371
  Pressure difference in Pa
372
  """
373
  if wind_speed < 0:
374
- raise ValueError(f"Wind speed {wind_speed} m/s cannot be negative")
375
- if not 0 <= wind_coefficient <= 1:
376
- raise ValueError(f"Wind coefficient {wind_coefficient} must be between 0 and 1")
377
 
378
- rho = 1.2 # Air density in kg/m³ (simplified for wind calculations)
379
- return 0.5 * wind_coefficient * rho * wind_speed**2
380
-
381
- def stack_pressure_difference(self, height: float, indoor_temp: float, outdoor_temp: float) -> float:
 
 
382
  """
383
  Calculate pressure difference due to stack effect.
384
- Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 16, Section 16.3.
385
 
386
  Args:
387
- height: Height difference in m
388
- indoor_temp: Indoor temperature in K
389
- outdoor_temp: Outdoor temperature in K
390
 
391
  Returns:
392
  Pressure difference in Pa
393
  """
394
- if height < 0:
395
- raise ValueError(f"Height {height} m cannot be negative")
396
- if indoor_temp <= 0 or outdoor_temp <= 0:
397
- raise ValueError("Temperatures must be positive in Kelvin")
398
 
399
  g = 9.81 # Gravitational acceleration in m/s²
400
- rho = 1.2 # Air density in kg/m³ (simplified for stack calculations)
401
- delta_t = abs(indoor_temp - outdoor_temp)
402
- t_avg = (indoor_temp + outdoor_temp) / 2
403
- return rho * g * height * delta_t / t_avg
404
-
405
  def combined_pressure_difference(self, wind_pd: float, stack_pd: float) -> float:
406
  """
407
  Calculate combined pressure difference from wind and stack effects.
408
- Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 16, Section 16.3.
409
 
410
  Args:
411
  wind_pd: Wind pressure difference in Pa
@@ -414,121 +269,59 @@ class HeatTransferCalculations:
414
  Returns:
415
  Combined pressure difference in Pa
416
  """
417
- if wind_pd < 0 or stack_pd < 0:
418
- raise ValueError("Pressure differences cannot be negative")
419
- return math.sqrt(wind_pd**2 + stack_pd**2)
420
-
421
- def crack_method_infiltration(self, crack_length: float, coefficient: float,
422
- pressure_difference: float) -> float:
423
  """
424
  Calculate infiltration flow rate using crack method.
425
- Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 16, Section 16.4.
426
 
427
  Args:
428
- crack_length: Total crack length in m
429
- coefficient: Flow coefficient in m³/(s·m·Pa^n)
430
- pressure_difference: Pressure difference in Pa
431
 
432
  Returns:
433
  Infiltration flow rate in m³/s
434
  """
435
- if crack_length < 0:
436
- raise ValueError(f"Crack length {crack_length} m cannot be negative")
437
- if coefficient < 0:
438
- raise ValueError(f"Coefficient {coefficient} cannot be negative")
439
- if pressure_difference < 0:
440
- raise ValueError(f"Pressure difference {pressure_difference} Pa cannot be negative")
441
-
442
- n = 0.65 # Flow exponent
443
- return coefficient * crack_length * pressure_difference**n
444
-
445
- def sol_air_temperature(self, outdoor_temp: float, solar_irradiance: float,
446
- surface_absorptivity: float, surface_resistance: float) -> float:
447
- """
448
- Calculate sol-air temperature for a surface.
449
- Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 18, Section 18.4.
450
-
451
- Args:
452
- outdoor_temp: Outdoor air temperature in °C
453
- solar_irradiance: Solar irradiance on surface in W/m²
454
- surface_absorptivity: Surface absorptivity (0-1)
455
- surface_resistance: Surface resistance in m²·K/W
456
-
457
- Returns:
458
- Sol-air temperature in °C
459
- """
460
- self.validate_inputs(outdoor_temp)
461
- if solar_irradiance < 0:
462
- raise ValueError(f"Solar irradiance {solar_irradiance} W/m² cannot be negative")
463
- if not 0 <= surface_absorptivity <= 1:
464
- raise ValueError(f"Surface absorptivity {surface_absorptivity} must be between 0 and 1")
465
- if surface_resistance < 0:
466
- raise ValueError(f"Surface resistance {surface_resistance} m²·K/W cannot be negative")
467
 
468
- h_ext = 1 / surface_resistance # External convective coefficient
469
- delta_t_rad = surface_absorptivity * solar_irradiance / h_ext
470
- return outdoor_temp + delta_t_rad
471
-
472
- def solar_heat_gain(self, irradiance: float, area: float, shgc: float,
473
- shading_coefficient: float = 1.0) -> float:
474
- """
475
- Calculate solar heat gain through a surface.
476
- Reference: ASHRAE Handbook—Fundamentals (2017), Chapter 18, Section 18.4.
477
-
478
- Args:
479
- irradiance: Solar irradiance on surface in W/m²
480
- area: Surface area in m²
481
- shgc: Solar heat gain coefficient (0-1)
482
- shading_coefficient: Shading coefficient (0-1)
483
-
484
- Returns:
485
- Solar heat gain in W
486
- """
487
- self.validate_inputs(0, area)
488
- if irradiance < 0:
489
- raise ValueError(f"Irradiance {irradiance} W/m² cannot be negative")
490
- if not 0 <= shgc <= 1:
491
- raise ValueError(f"SHGC {shgc} must be between 0 and 1")
492
- if not 0 <= shading_coefficient <= 1:
493
- raise ValueError(f"Shading coefficient {shading_coefficient} must be between 0 and 1")
494
-
495
- return irradiance * area * shgc * shading_coefficient
496
 
497
 
498
- # Create a singleton instance
499
- heat_transfer_calculator = HeatTransferCalculations()
500
-
501
  # Example usage
502
  if __name__ == "__main__":
503
- # Example solar calculations
504
- latitude = 40.0
505
- day_of_year = 204
506
- hour = 12.0
507
-
508
- declination = heat_transfer_calculator.solar.solar_declination(day_of_year)
509
- hour_angle = heat_transfer_calculator.solar.solar_hour_angle(hour)
510
- altitude = heat_transfer_calculator.solar.solar_altitude(latitude, declination, hour_angle)
511
- azimuth = heat_transfer_calculator.solar.solar_azimuth(latitude, declination, hour_angle, altitude)
512
-
513
- print(f"Solar Declination: {declination:.2f}°")
514
- print(f"Solar Hour Angle: {hour_angle:.2f}°")
515
- print(f"Solar Altitude: {altitude:.2f}°")
516
- print(f"Solar Azimuth: {azimuth:.2f}°")
517
 
518
- # Example heat transfer calculation
519
  u_value = 0.5 # W/(m²·K)
520
  area = 20.0 # m²
521
- delta_t = 10.0 # °C
522
- t_db = 25.0 # °C
523
- rh = 50.0 # %
524
- conduction = heat_transfer_calculator.conduction_heat_transfer(u_value, area, delta_t)
525
- print(f"Conduction Heat Transfer: {conduction:.2f} W")
526
 
527
  # Example infiltration calculation
528
  flow_rate = 0.05 # m³/s
529
- infiltration = heat_transfer_calculator.infiltration_heat_transfer(flow_rate, delta_t, t_db, rh)
530
- print(f"Infiltration Heat Transfer: {infiltration:.2f} W")
 
 
 
 
531
 
532
- # Example psychrometric calculation
533
- humidity_ratio = heat_transfer_calculator.psychrometrics.humidity_ratio(t_db, rh)
534
- print(f"Humidity Ratio: {humidity_ratio:.6f} kg/kg")
 
 
 
 
 
 
 
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
11
+ from dataclasses import dataclass
12
+
13
+ # Configure logging
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
 
 
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.
 
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.
 
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
 
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}°")