mabuseif commited on
Commit
63eff5d
·
verified ·
1 Parent(s): 2b08ecf

Update utils/heat_transfer.py

Browse files
Files changed (1) hide show
  1. utils/heat_transfer.py +324 -372
utils/heat_transfer.py CHANGED
@@ -1,548 +1,500 @@
1
  """
2
- Shared calculation functions module for HVAC Load Calculator.
3
- This module implements common heat transfer calculations used in both cooling and heating load calculations.
 
4
  """
5
 
6
- from typing import Dict, List, Any, Optional, Tuple
7
  import math
8
  import numpy as np
9
- import pandas as pd
10
- import os
11
 
12
- # Import data models and utilities
13
- from data.building_components import Wall, Roof, Floor, Window, Door, Orientation
14
- from utils.psychrometrics import Psychrometrics
15
 
16
- # Define constants
17
- STEFAN_BOLTZMANN_CONSTANT = 5.67e-8 # W/(m²·K⁴)
18
- SOLAR_CONSTANT = 1367 # W/m²
19
- EARTH_TILT_ANGLE = 23.45 # degrees
20
-
21
-
22
- class HeatTransferCalculations:
23
- """Class for shared heat transfer calculations."""
24
 
25
- @staticmethod
26
- def conduction_heat_transfer(u_value: float, area: float, delta_t: float) -> float:
27
- """
28
- Calculate conduction heat transfer through a building component.
29
-
30
- Args:
31
- u_value: U-value of the component in W/(m²·K)
32
- area: Area of the component in m²
33
- delta_t: Temperature difference across the component in K (or °C)
34
-
35
- Returns:
36
- Heat transfer rate in W
37
- """
38
- return u_value * area * delta_t
39
 
40
- @staticmethod
41
- def convection_heat_transfer(h_c: float, area: float, delta_t: float) -> float:
42
  """
43
- Calculate convection heat transfer.
44
 
45
  Args:
46
- h_c: Convection heat transfer coefficient in W/(m²·K)
47
- area: Surface area in
48
- delta_t: Temperature difference between surface and fluid in K (or °C)
 
49
 
50
- Returns:
51
- Heat transfer rate in W
52
  """
53
- return h_c * area * delta_t
 
54
 
55
- @staticmethod
56
- def radiation_heat_transfer(emissivity: float, area: float, t_surface: float, t_surroundings: float) -> float:
57
  """
58
- Calculate radiation heat transfer.
59
 
60
  Args:
61
- emissivity: Surface emissivity (0-1)
62
- area: Surface area in m²
63
- t_surface: Surface temperature in K
64
- t_surroundings: Surroundings temperature in K
65
 
66
  Returns:
67
- Heat transfer rate in W
68
  """
69
- return emissivity * STEFAN_BOLTZMANN_CONSTANT * area * (t_surface**4 - t_surroundings**4)
 
 
 
 
 
 
 
 
70
 
71
- @staticmethod
72
- def infiltration_heat_transfer(flow_rate: float, delta_t: float, density: float = 1.2, specific_heat: float = 1006) -> float:
73
  """
74
- Calculate sensible heat transfer due to infiltration or ventilation.
75
 
76
  Args:
77
- flow_rate: Volumetric flow rate in m³/s
78
- delta_t: Temperature difference between indoor and outdoor air in K (or °C)
79
- density: Air density in kg/m³ (default: 1.2 kg/m³)
80
- specific_heat: Specific heat capacity of air in J/(kg·K) (default: 1006 J/(kg·K))
81
 
82
  Returns:
83
- Heat transfer rate in W
84
  """
85
- return flow_rate * density * specific_heat * delta_t
 
 
86
 
87
- @staticmethod
88
- def infiltration_latent_heat_transfer(flow_rate: float, delta_w: float, density: float = 1.2, latent_heat: float = 2501000) -> float:
89
  """
90
- Calculate latent heat transfer due to infiltration or ventilation.
91
 
92
  Args:
93
- flow_rate: Volumetric flow rate in m³/s
94
- delta_w: Humidity ratio difference between indoor and outdoor air in kg/kg
95
- density: Air density in kg/m³ (default: 1.2 kg/m³)
96
- latent_heat: Latent heat of vaporization in J/kg (default: 2501000 J/kg)
97
 
98
  Returns:
99
- Heat transfer rate in W
100
  """
101
- return flow_rate * density * latent_heat * delta_w
 
 
 
 
 
 
 
 
 
 
102
 
103
- @staticmethod
104
- def air_exchange_rate_to_flow_rate(ach: float, volume: float) -> float:
105
  """
106
- Convert air changes per hour to volumetric flow rate.
107
 
108
  Args:
109
- ach: Air changes per hour (1/h)
110
- volume: Room or building volume in
 
 
111
 
112
  Returns:
113
- Volumetric flow rate in m³/s
114
  """
115
- return ach * volume / 3600
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
 
117
- @staticmethod
118
- def flow_rate_to_air_exchange_rate(flow_rate: float, volume: float) -> float:
119
  """
120
- Convert volumetric flow rate to air changes per hour.
121
 
122
  Args:
123
- flow_rate: Volumetric flow rate in m³/s
124
- volume: Room or building volume in
 
 
125
 
126
  Returns:
127
- Air changes per hour (1/h)
128
  """
129
- return flow_rate * 3600 / volume
 
 
 
 
 
 
 
 
 
 
 
 
130
 
131
- @staticmethod
132
- def crack_method_infiltration(crack_length: float, coefficient: float, pressure_difference: float, exponent: float = 0.65) -> float:
133
  """
134
- Calculate infiltration using the crack method.
135
 
136
  Args:
137
- crack_length: Length of cracks in m
138
- coefficient: Flow coefficient in m³/(s·m·Pa^n)
139
- pressure_difference: Pressure difference in Pa
140
- exponent: Flow exponent (default: 0.65)
141
 
142
  Returns:
143
- Infiltration flow rate in /s
144
  """
145
- return coefficient * crack_length * pressure_difference**exponent
 
 
 
 
 
146
 
147
- @staticmethod
148
- def wind_pressure_difference(wind_speed: float, wind_coefficient: float, density: float = 1.2) -> float:
149
  """
150
- Calculate pressure difference due to wind.
151
 
152
  Args:
153
- wind_speed: Wind speed in m/s
154
- wind_coefficient: Wind pressure coefficient (dimensionless)
155
- density: Air density in kg/m³ (default: 1.2 kg/m³)
156
 
157
  Returns:
158
- Pressure difference in Pa
159
  """
160
- return 0.5 * density * wind_speed**2 * wind_coefficient
 
 
 
161
 
162
- @staticmethod
163
- def stack_pressure_difference(height: float, indoor_temp: float, outdoor_temp: float,
164
- neutral_plane_height: float = None, gravity: float = 9.81) -> float:
165
  """
166
- Calculate pressure difference due to stack effect.
167
 
168
  Args:
169
- height: Height from reference level in m
170
- indoor_temp: Indoor temperature in K
171
- outdoor_temp: Outdoor temperature in K
172
- neutral_plane_height: Height of neutral pressure plane in m (default: half of height)
173
- gravity: Acceleration due to gravity in m/s² (default: 9.81 m/s²)
174
 
175
  Returns:
176
- Pressure difference in Pa
177
  """
178
- if neutral_plane_height is None:
179
- neutral_plane_height = height / 2
 
 
180
 
181
- # Calculate pressure difference
182
- return gravity * (height - neutral_plane_height) * (outdoor_temp - indoor_temp) / outdoor_temp
 
 
 
 
 
183
 
184
- @staticmethod
185
- def combined_pressure_difference(wind_pd: float, stack_pd: float) -> float:
186
- """
187
- Calculate combined pressure difference from wind and stack effects.
188
-
189
- Args:
190
- wind_pd: Pressure difference due to wind in Pa
191
- stack_pd: Pressure difference due to stack effect in Pa
192
-
193
- Returns:
194
- Combined pressure difference in Pa
195
- """
196
- # Simple quadrature combination
197
- return math.sqrt(wind_pd**2 + stack_pd**2)
198
 
199
- @staticmethod
200
- def solar_declination(day_of_year: int) -> float:
201
  """
202
- Calculate solar declination angle.
203
 
204
  Args:
205
- day_of_year: Day of the year (1-365)
 
 
206
 
207
- Returns:
208
- Solar declination angle in degrees
209
- """
210
- return EARTH_TILT_ANGLE * math.sin(2 * math.pi * (day_of_year - 81) / 365)
 
 
 
 
 
211
 
212
- @staticmethod
213
- def solar_hour_angle(solar_time: float) -> float:
214
  """
215
- Calculate solar hour angle.
216
 
217
  Args:
218
- solar_time: Solar time in hours (0-24)
 
 
219
 
220
  Returns:
221
- Solar hour angle in degrees
222
  """
223
- return 15 * (solar_time - 12)
 
 
 
224
 
225
- @staticmethod
226
- def solar_altitude(latitude: float, declination: float, hour_angle: float) -> float:
227
  """
228
- Calculate solar altitude angle.
229
 
230
  Args:
231
- latitude: Latitude in degrees
232
- declination: Solar declination angle in degrees
233
- hour_angle: Solar hour angle in degrees
234
 
235
  Returns:
236
- Solar altitude angle in degrees
237
  """
238
- # Convert angles to radians
239
- lat_rad = math.radians(latitude)
240
- decl_rad = math.radians(declination)
241
- hour_rad = math.radians(hour_angle)
242
-
243
- # Calculate solar altitude
244
- sin_altitude = (math.sin(lat_rad) * math.sin(decl_rad) +
245
- math.cos(lat_rad) * math.cos(decl_rad) * math.cos(hour_rad))
246
-
247
- return math.degrees(math.asin(sin_altitude))
248
 
249
- @staticmethod
250
- def solar_azimuth(latitude: float, declination: float, hour_angle: float, altitude: float) -> float:
251
  """
252
- Calculate solar azimuth angle.
253
 
254
  Args:
255
- latitude: Latitude in degrees
256
- declination: Solar declination angle in degrees
257
- hour_angle: Solar hour angle in degrees
258
- altitude: Solar altitude angle in degrees
259
 
260
  Returns:
261
- Solar azimuth angle in degrees (0° = South, positive westward)
262
  """
263
- # Convert angles to radians
264
- lat_rad = math.radians(latitude)
265
- decl_rad = math.radians(declination)
266
- hour_rad = math.radians(hour_angle)
267
- alt_rad = math.radians(altitude)
268
-
269
- # Calculate solar azimuth
270
- cos_azimuth = ((math.sin(decl_rad) * math.cos(lat_rad) -
271
- math.cos(decl_rad) * math.sin(lat_rad) * math.cos(hour_rad)) /
272
- math.cos(alt_rad))
273
-
274
- # Constrain to [-1, 1] to avoid domain errors
275
- cos_azimuth = max(-1, min(1, cos_azimuth))
276
-
277
- # Calculate azimuth angle
278
- azimuth = math.degrees(math.acos(cos_azimuth))
279
-
280
- # Adjust for morning hours (negative hour angle)
281
- if hour_angle < 0:
282
- azimuth = -azimuth
283
 
284
- return azimuth
 
 
 
285
 
286
- @staticmethod
287
- def incident_angle(surface_tilt: float, surface_azimuth: float,
288
- solar_altitude: float, solar_azimuth: float) -> float:
289
  """
290
- Calculate angle of incidence on a surface.
291
 
292
  Args:
293
- surface_tilt: Surface tilt angle from horizontal in degrees (0° = horizontal, 90° = vertical)
294
- surface_azimuth: Surface azimuth angle in degrees (0° = South, positive westward)
295
- solar_altitude: Solar altitude angle in degrees
296
- solar_azimuth: Solar azimuth angle in degrees
297
 
298
  Returns:
299
- Incident angle in degrees
300
  """
301
- # Convert angles to radians
302
- surf_tilt_rad = math.radians(surface_tilt)
303
- surf_azim_rad = math.radians(surface_azimuth)
304
- solar_alt_rad = math.radians(solar_altitude)
305
- solar_azim_rad = math.radians(solar_azimuth)
306
-
307
- # Calculate incident angle
308
- cos_incident = (math.sin(solar_alt_rad) * math.cos(surf_tilt_rad) +
309
- math.cos(solar_alt_rad) * math.sin(surf_tilt_rad) *
310
- math.cos(solar_azim_rad - surf_azim_rad))
311
 
312
- # Constrain to [-1, 1] to avoid domain errors
313
- cos_incident = max(-1, min(1, cos_incident))
314
-
315
- return math.degrees(math.acos(cos_incident))
316
 
317
- @staticmethod
318
- def direct_normal_irradiance(altitude: float, atmospheric_clearness: float = 1.0) -> float:
319
  """
320
- Calculate direct normal irradiance.
321
 
322
  Args:
323
- altitude: Solar altitude angle in degrees
324
- atmospheric_clearness: Atmospheric clearness factor (0-1)
325
 
326
  Returns:
327
- Direct normal irradiance in W/m²
328
  """
329
- if altitude <= 0:
330
- return 0
331
-
332
- # Simple model based on air mass
333
- air_mass = 1 / math.sin(math.radians(altitude))
334
-
335
- # Limit air mass to reasonable values
336
- air_mass = min(air_mass, 38)
337
-
338
- # Calculate direct normal irradiance
339
- dni = SOLAR_CONSTANT * atmospheric_clearness**air_mass
340
-
341
- return dni
342
 
343
- @staticmethod
344
- def diffuse_horizontal_irradiance(dni: float, altitude: float, clearness: float = 0.2) -> float:
345
  """
346
- Calculate diffuse horizontal irradiance.
347
 
348
  Args:
349
- dni: Direct normal irradiance in W/m²
350
- altitude: Solar altitude angle in degrees
351
- clearness: Sky clearness factor (0-1)
352
 
353
  Returns:
354
- Diffuse horizontal irradiance in W/m²
355
  """
356
- if altitude <= 0:
357
- return 0
358
-
359
- # Simple model for diffuse irradiance
360
- return dni * clearness * math.sin(math.radians(altitude))
361
 
362
- @staticmethod
363
- def global_horizontal_irradiance(dni: float, dhi: float, altitude: float) -> float:
364
  """
365
- Calculate global horizontal irradiance.
366
 
367
  Args:
368
- dni: Direct normal irradiance in W/m²
369
- dhi: Diffuse horizontal irradiance in W/m²
370
- altitude: Solar altitude angle in degrees
371
 
372
  Returns:
373
- Global horizontal irradiance in W/m²
374
  """
375
- if altitude <= 0:
376
- return 0
 
 
377
 
378
- # Calculate direct horizontal component
379
- direct_horizontal = dni * math.sin(math.radians(altitude))
380
-
381
- # Calculate global horizontal irradiance
382
- return direct_horizontal + dhi
383
 
384
- @staticmethod
385
- def irradiance_on_surface(dni: float, dhi: float, incident_angle: float,
386
- surface_tilt: float, ground_reflectance: float = 0.2) -> float:
387
  """
388
- Calculate total irradiance on a surface.
389
 
390
  Args:
391
- dni: Direct normal irradiance in W/m²
392
- dhi: Diffuse horizontal irradiance in W/m²
393
- incident_angle: Incident angle in degrees
394
- surface_tilt: Surface tilt angle from horizontal in degrees
395
- ground_reflectance: Ground reflectance (albedo) (0-1)
396
 
397
  Returns:
398
- Total irradiance on the surface in W/m²
399
  """
400
- # Convert angles to radians
401
- incident_rad = math.radians(incident_angle)
402
- tilt_rad = math.radians(surface_tilt)
403
-
404
- # Calculate direct component
405
- if incident_angle < 90:
406
- direct = dni * math.cos(incident_rad)
407
- else:
408
- direct = 0
409
 
410
- # Calculate diffuse component (simple isotropic model)
411
- diffuse = dhi * (1 + math.cos(tilt_rad)) / 2
412
-
413
- # Calculate ground-reflected component
414
- reflected = (dni * math.sin(math.radians(incident_angle)) + dhi) * ground_reflectance * (1 - math.cos(tilt_rad)) / 2
415
-
416
- # Calculate total irradiance
417
- return direct + diffuse + reflected
418
 
419
- @staticmethod
420
- def solar_heat_gain(irradiance: float, area: float, shgc: float,
421
- shading_coefficient: float = 1.0, frame_factor: float = 0.85) -> float:
422
  """
423
- Calculate solar heat gain through a window.
424
 
425
  Args:
426
- irradiance: Total irradiance on the window in W/m²
427
- area: Window area in
428
- shgc: Solar Heat Gain Coefficient (0-1)
429
- shading_coefficient: External shading coefficient (0-1)
430
- frame_factor: Ratio of glazing area to total window area (0-1)
431
 
432
  Returns:
433
- Solar heat gain in W
434
  """
435
- return irradiance * area * shgc * shading_coefficient * frame_factor
 
 
436
 
437
- @staticmethod
438
- def internal_gains(occupants: int, lights_power: float, equipment_power: float,
439
- occupant_sensible_gain: float = 70, occupant_latent_gain: float = 45) -> Dict[str, float]:
440
  """
441
- Calculate internal heat gains.
442
 
443
  Args:
444
- occupants: Number of occupants
445
- lights_power: Lighting power in W
446
- equipment_power: Equipment power in W
447
- occupant_sensible_gain: Sensible heat gain per occupant in W (default: 70 W)
448
- occupant_latent_gain: Latent heat gain per occupant in W (default: 45 W)
449
 
450
  Returns:
451
- Dictionary with sensible, latent, and total heat gains in W
452
- """
453
- # Calculate occupant gains
454
- occupant_sensible = occupants * occupant_sensible_gain
455
- occupant_latent = occupants * occupant_latent_gain
456
-
457
- # Calculate total sensible and latent gains
458
- sensible_gain = occupant_sensible + lights_power + equipment_power
459
- latent_gain = occupant_latent
460
-
461
- return {
462
- "sensible": sensible_gain,
463
- "latent": latent_gain,
464
- "total": sensible_gain + latent_gain
465
- }
466
-
467
- @staticmethod
468
- def thermal_mass_effect(mass: float, specific_heat: float, delta_t: float) -> float:
469
  """
470
- Calculate heat storage in thermal mass.
 
 
 
 
 
471
 
472
- Args:
473
- mass: Mass of the material in kg
474
- specific_heat: Specific heat capacity in J/(kg·K)
475
- delta_t: Temperature change in K (or °C)
476
-
477
- Returns:
478
- Heat stored in J
479
- """
480
- return mass * specific_heat * delta_t
481
 
482
- @staticmethod
483
- def thermal_lag_factor(thermal_mass: float, time_constant: float, time_step: float) -> float:
484
  """
485
- Calculate thermal lag factor for dynamic heat transfer.
486
 
487
  Args:
488
- thermal_mass: Thermal mass in J/K
489
- time_constant: Time constant in hours
490
- time_step: Time step in hours
 
491
 
492
  Returns:
493
- Thermal lag factor (0-1)
494
  """
495
- return 1 - math.exp(-time_step / time_constant)
 
 
 
 
 
 
 
 
 
 
496
 
497
- @staticmethod
498
- def temperature_swing(heat_gain: float, thermal_mass: float) -> float:
499
  """
500
- Calculate temperature swing due to heat gain and thermal mass.
501
 
502
  Args:
503
- heat_gain: Heat gain in J
504
- thermal_mass: Thermal mass in J/K
 
 
505
 
506
  Returns:
507
- Temperature swing in K (or °C)
508
- """
509
- return heat_gain / thermal_mass
510
-
511
- @staticmethod
512
- def sol_air_temperature(outdoor_temp: float, solar_irradiance: float,
513
- surface_absorptivity: float, surface_resistance: float) -> float:
514
  """
515
- Calculate sol-air temperature.
 
 
 
 
 
 
516
 
517
- Args:
518
- outdoor_temp: Outdoor air temperature in °C
519
- solar_irradiance: Solar irradiance on the surface in W/m²
520
- surface_absorptivity: Surface solar absorptivity (0-1)
521
- surface_resistance: Surface heat transfer resistance in m²·K/W
522
-
523
- Returns:
524
- Sol-air temperature in °C
525
- """
526
- return outdoor_temp + solar_irradiance * surface_absorptivity * surface_resistance
527
 
528
 
529
  # Create a singleton instance
530
- heat_transfer = HeatTransferCalculations()
531
 
532
  # Example usage
533
  if __name__ == "__main__":
534
- # Calculate conduction heat transfer
535
- q_cond = heat_transfer.conduction_heat_transfer(u_value=0.5, area=10, delta_t=20)
536
- print(f"Conduction heat transfer: {q_cond:.2f} W")
 
537
 
538
- # Calculate infiltration heat transfer
539
- q_inf = heat_transfer.infiltration_heat_transfer(flow_rate=0.1, delta_t=20)
540
- print(f"Infiltration heat transfer: {q_inf:.2f} W")
 
541
 
542
- # Calculate solar heat gain
543
- q_solar = heat_transfer.solar_heat_gain(irradiance=500, area=5, shgc=0.7)
544
- print(f"Solar heat gain: {q_solar:.2f} W")
 
545
 
546
- # Calculate internal gains
547
- gains = heat_transfer.internal_gains(occupants=3, lights_power=200, equipment_power=500)
548
- print(f"Internal gains - Sensible: {gains['sensible']:.2f} W, Latent: {gains['latent']:.2f} W, Total: {gains['total']:.2f} W")
 
 
 
 
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
  """
6
 
7
+ from typing import Optional, Tuple
8
  import math
9
  import numpy as np
 
 
10
 
 
 
 
11
 
12
+ class SolarCalculations:
13
+ """Class for solar geometry and irradiance calculations."""
 
 
 
 
 
 
14
 
15
+ def __init__(self):
16
+ """Initialize solar calculations with cached values."""
17
+ self._declination_cache = {} # Cache for declination by day of year
 
 
 
 
 
 
 
 
 
 
 
18
 
19
+ def validate_angle(self, angle: float, name: str, min_val: float, max_val: float) -> None:
 
20
  """
21
+ Validate an angle input.
22
 
23
  Args:
24
+ angle: Angle in degrees
25
+ name: Name of the angle for error messages
26
+ min_val: Minimum allowed value
27
+ max_val: Maximum allowed value
28
 
29
+ Raises:
30
+ ValueError: If angle is out of range
31
  """
32
+ if not min_val <= angle <= max_val:
33
+ raise ValueError(f"{name} {angle}° is outside valid range ({min_val} to {max_val}°)")
34
 
35
+ def solar_declination(self, day_of_year: int) -> float:
 
36
  """
37
+ Calculate solar declination angle for a given day of the year.
38
 
39
  Args:
40
+ day_of_year: Day of the year (1-365)
 
 
 
41
 
42
  Returns:
43
+ Solar declination angle in degrees
44
  """
45
+ if not 1 <= day_of_year <= 365:
46
+ raise ValueError(f"Day of year {day_of_year} must be between 1 and 365")
47
+
48
+ if day_of_year in self._declination_cache:
49
+ return self._declination_cache[day_of_year]
50
+
51
+ declination = 23.45 * math.sin(math.radians(360 * (284 + day_of_year) / 365))
52
+ self._declination_cache[day_of_year] = declination
53
+ return declination
54
 
55
+ def solar_hour_angle(self, hour: float) -> float:
 
56
  """
57
+ Calculate solar hour angle for a given hour of the day.
58
 
59
  Args:
60
+ hour: Hour of the day (0-23)
 
 
 
61
 
62
  Returns:
63
+ Solar hour angle in degrees
64
  """
65
+ if not 0 <= hour <= 23:
66
+ raise ValueError(f"Hour {hour} must be between 0 and 23")
67
+ return 15 * (hour - 12)
68
 
69
+ def solar_altitude(self, latitude: float, declination: float, hour_angle: float) -> float:
 
70
  """
71
+ Calculate solar altitude angle.
72
 
73
  Args:
74
+ latitude: Latitude in degrees
75
+ declination: Solar declination angle in degrees
76
+ hour_angle: Solar hour angle in degrees
 
77
 
78
  Returns:
79
+ Solar altitude angle in degrees
80
  """
81
+ self.validate_angle(latitude, "Latitude", -90, 90)
82
+ self.validate_angle(declination, "Declination", -90, 90)
83
+ self.validate_angle(hour_angle, "Hour angle", -180, 180)
84
+
85
+ lat_rad = math.radians(latitude)
86
+ dec_rad = math.radians(declination)
87
+ ha_rad = math.radians(hour_angle)
88
+
89
+ sin_alt = math.sin(lat_rad) * math.sin(dec_rad) + math.cos(lat_rad) * math.cos(dec_rad) * math.cos(ha_rad)
90
+ altitude = math.degrees(math.asin(sin_alt))
91
+ return max(0, altitude)
92
 
93
+ def solar_azimuth(self, latitude: float, declination: float, hour_angle: float, altitude: float) -> float:
 
94
  """
95
+ Calculate solar azimuth angle.
96
 
97
  Args:
98
+ latitude: Latitude in degrees
99
+ declination: Solar declination angle in degrees
100
+ hour_angle: Solar hour angle in degrees
101
+ altitude: Solar altitude angle in degrees
102
 
103
  Returns:
104
+ Solar azimuth angle in degrees
105
  """
106
+ self.validate_angle(latitude, "Latitude", -90, 90)
107
+ self.validate_angle(declination, "Declination", -90, 90)
108
+ self.validate_angle(hour_angle, "Hour angle", -180, 180)
109
+ self.validate_angle(altitude, "Altitude", 0, 90)
110
+
111
+ lat_rad = math.radians(latitude)
112
+ dec_rad = math.radians(declination)
113
+ ha_rad = math.radians(hour_angle)
114
+ alt_rad = math.radians(altitude)
115
+
116
+ cos_az = (math.sin(alt_rad) * math.sin(lat_rad) - math.sin(dec_rad)) / (math.cos(alt_rad) * math.cos(lat_rad))
117
+ cos_az = max(-1, min(1, cos_az))
118
+ azimuth = math.degrees(math.acos(cos_az))
119
+
120
+ if hour_angle > 0:
121
+ azimuth = 360 - azimuth
122
+ return azimuth
123
 
124
+ def incident_angle(self, surface_tilt: float, surface_azimuth: float,
125
+ solar_altitude: float, solar_azimuth: float) -> float:
126
  """
127
+ Calculate angle of incidence for a surface.
128
 
129
  Args:
130
+ surface_tilt: Surface tilt angle in degrees (0=horizontal, 90=vertical)
131
+ surface_azimuth: Surface azimuth angle in degrees
132
+ solar_altitude: Solar altitude angle in degrees
133
+ solar_azimuth: Solar azimuth angle in degrees
134
 
135
  Returns:
136
+ Angle of incidence in degrees
137
  """
138
+ self.validate_angle(surface_tilt, "Surface tilt", 0, 180)
139
+ self.validate_angle(surface_azimuth, "Surface azimuth", 0, 360)
140
+ self.validate_angle(solar_altitude, "Solar altitude", 0, 90)
141
+ self.validate_angle(solar_azimuth, "Solar azimuth", 0, 360)
142
+
143
+ tilt_rad = math.radians(surface_tilt)
144
+ az_diff_rad = math.radians(solar_azimuth - surface_azimuth)
145
+ alt_rad = math.radians(solar_altitude)
146
+
147
+ cos_theta = (math.sin(alt_rad) * math.cos(tilt_rad) +
148
+ math.cos(alt_rad) * math.sin(tilt_rad) * math.cos(az_diff_rad))
149
+ cos_theta = max(0, min(1, cos_theta))
150
+ return math.degrees(math.acos(cos_theta))
151
 
152
+ def direct_normal_irradiance(self, solar_altitude: float) -> float:
 
153
  """
154
+ Calculate direct normal irradiance.
155
 
156
  Args:
157
+ solar_altitude: Solar altitude angle in degrees
 
 
 
158
 
159
  Returns:
160
+ Direct normal irradiance in W/
161
  """
162
+ self.validate_angle(solar_altitude, "Solar altitude", 0, 90)
163
+ if solar_altitude <= 0:
164
+ return 0
165
+ air_mass = 1 / math.cos(math.radians(90 - solar_altitude))
166
+ dni = 1367 * (1 - 0.14 * air_mass) # Simplified model
167
+ return max(0, dni)
168
 
169
+ def diffuse_horizontal_irradiance(self, dni: float, solar_altitude: float) -> float:
 
170
  """
171
+ Calculate diffuse horizontal irradiance.
172
 
173
  Args:
174
+ dni: Direct normal irradiance in W/
175
+ solar_altitude: Solar altitude angle in degrees
 
176
 
177
  Returns:
178
+ Diffuse horizontal irradiance in W/m²
179
  """
180
+ self.validate_angle(solar_altitude, "Solar altitude", 0, 90)
181
+ if solar_altitude <= 0:
182
+ return 0
183
+ return 0.1 * dni # Simplified model
184
 
185
+ def irradiance_on_surface(self, dni: float, dhi: float, incident_angle: float, surface_tilt: float) -> float:
 
 
186
  """
187
+ Calculate total irradiance on a tilted surface.
188
 
189
  Args:
190
+ dni: Direct normal irradiance in W/m²
191
+ dhi: Diffuse horizontal irradiance in W/m²
192
+ incident_angle: Angle of incidence in degrees
193
+ surface_tilt: Surface tilt angle in degrees
 
194
 
195
  Returns:
196
+ Total irradiance in W/m²
197
  """
198
+ self.validate_angle(incident_angle, "Incident angle", 0, 90)
199
+ self.validate_angle(surface_tilt, "Surface tilt", 0, 180)
200
+ if dni < 0 or dhi < 0:
201
+ raise ValueError("Irradiance values cannot be negative")
202
 
203
+ direct = dni * math.cos(math.radians(incident_angle))
204
+ diffuse = dhi * (1 + math.cos(math.radians(surface_tilt))) / 2
205
+ return max(0, direct + diffuse)
206
+
207
+
208
+ class HeatTransferCalculations:
209
+ """Class for heat transfer calculations."""
210
 
211
+ def __init__(self):
212
+ """Initialize heat transfer calculations with solar calculations."""
213
+ self.solar = SolarCalculations()
 
 
 
 
 
 
 
 
 
 
 
214
 
215
+ def validate_inputs(self, temp: float, area: float = 0.0, flow_rate: float = 0.0) -> None:
 
216
  """
217
+ Validate input parameters for heat transfer calculations.
218
 
219
  Args:
220
+ temp: Temperature in °C
221
+ area: Area in m²
222
+ flow_rate: Flow rate in m³/s
223
 
224
+ Raises:
225
+ ValueError: If inputs are out of acceptable ranges
226
+ """
227
+ if not -50 <= temp <= 60:
228
+ raise ValueError(f"Temperature {temp}°C is outside valid range (-50 to 60°C)")
229
+ if area < 0:
230
+ raise ValueError(f"Area {area}m² cannot be negative")
231
+ if flow_rate < 0:
232
+ raise ValueError(f"Flow rate {flow_rate}m³/s cannot be negative")
233
 
234
+ def conduction_heat_transfer(self, u_value: float, area: float, delta_t: float) -> float:
 
235
  """
236
+ Calculate heat transfer by conduction.
237
 
238
  Args:
239
+ u_value: Overall heat transfer coefficient in W/(m²·K)
240
+ area: Surface area in m²
241
+ delta_t: Temperature difference in °C
242
 
243
  Returns:
244
+ Heat transfer rate in W
245
  """
246
+ if u_value < 0:
247
+ raise ValueError(f"U-value {u_value} W/(m²·K) cannot be negative")
248
+ self.validate_inputs(delta_t, area)
249
+ return u_value * area * delta_t
250
 
251
+ def convection_heat_transfer(self, h: float, area: float, delta_t: float) -> float:
 
252
  """
253
+ Calculate heat transfer by convection.
254
 
255
  Args:
256
+ h: Convective heat transfer coefficient in W/(m²·K)
257
+ area: Surface area in
258
+ delta_t: Temperature difference in °C
259
 
260
  Returns:
261
+ Heat transfer rate in W
262
  """
263
+ if h < 0:
264
+ raise ValueError(f"Convective coefficient {h} W/(m²·K) cannot be negative")
265
+ self.validate_inputs(delta_t, area)
266
+ return h * area * delta_t
 
 
 
 
 
 
267
 
268
+ def radiation_heat_transfer(self, emissivity: float, area: float, t_surface: float, t_surroundings: float) -> float:
 
269
  """
270
+ Calculate heat transfer by radiation using Stefan-Boltzmann law.
271
 
272
  Args:
273
+ emissivity: Surface emissivity (0-1)
274
+ area: Surface area in
275
+ t_surface: Surface temperature in °C
276
+ t_surroundings: Surroundings temperature in °C
277
 
278
  Returns:
279
+ Heat transfer rate in W
280
  """
281
+ if not 0 <= emissivity <= 1:
282
+ raise ValueError(f"Emissivity {emissivity} must be between 0 and 1")
283
+ self.validate_inputs(t_surface, area)
284
+ self.validate_inputs(t_surroundings)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
285
 
286
+ sigma = 5.67e-8 # Stefan-Boltzmann constant in W/(m²·K⁴)
287
+ t_s = t_surface + 273.15
288
+ t_sur = t_surroundings + 273.15
289
+ return emissivity * sigma * area * (t_s**4 - t_sur**4)
290
 
291
+ def thermal_lag_factor(self, thermal_mass: float, time_constant: float, time_step: float) -> float:
 
 
292
  """
293
+ Calculate thermal lag factor for transient heat transfer.
294
 
295
  Args:
296
+ thermal_mass: Thermal mass in J/K
297
+ time_constant: Time constant in hours
298
+ time_step: Time step in hours
 
299
 
300
  Returns:
301
+ Thermal lag factor (0-1)
302
  """
303
+ if thermal_mass < 0:
304
+ raise ValueError(f"Thermal mass {thermal_mass} J/K cannot be negative")
305
+ if time_constant <= 0:
306
+ raise ValueError(f"Time constant {time_constant} hours must be positive")
307
+ if time_step < 0:
308
+ raise ValueError(f"Time step {time_step} hours cannot be negative")
 
 
 
 
309
 
310
+ return math.exp(-time_step / time_constant)
 
 
 
311
 
312
+ def infiltration_heat_transfer(self, flow_rate: float, delta_t: float) -> float:
 
313
  """
314
+ Calculate sensible heat transfer due to infiltration or ventilation.
315
 
316
  Args:
317
+ flow_rate: Air flow rate in m³/s
318
+ delta_t: Temperature difference in °C
319
 
320
  Returns:
321
+ Sensible heat transfer rate in W
322
  """
323
+ self.validate_inputs(delta_t, flow_rate=flow_rate)
324
+ rho = 1.2 # Air density in kg/m³
325
+ cp = 1005 # Specific heat of air in J/(kg·K)
326
+ return flow_rate * rho * cp * delta_t
 
 
 
 
 
 
 
 
 
327
 
328
+ def infiltration_latent_heat_transfer(self, flow_rate: float, delta_w: float) -> float:
 
329
  """
330
+ Calculate latent heat transfer due to infiltration or ventilation.
331
 
332
  Args:
333
+ flow_rate: Air flow rate in m³/s
334
+ delta_w: Humidity ratio difference in kg/kg
 
335
 
336
  Returns:
337
+ Latent heat transfer rate in W
338
  """
339
+ self.validate_inputs(0, flow_rate=flow_rate)
340
+ rho = 1.2 # Air density in kg/m³
341
+ h_fg = 2501000 # Latent heat of vaporization in J/kg
342
+ return flow_rate * rho * h_fg * delta_w
 
343
 
344
+ def wind_pressure_difference(self, wind_speed: float, wind_coefficient: float = 0.4) -> float:
 
345
  """
346
+ Calculate pressure difference due to wind.
347
 
348
  Args:
349
+ wind_speed: Wind speed in m/s
350
+ wind_coefficient: Wind pressure coefficient
 
351
 
352
  Returns:
353
+ Pressure difference in Pa
354
  """
355
+ if wind_speed < 0:
356
+ raise ValueError(f"Wind speed {wind_speed} m/s cannot be negative")
357
+ if not 0 <= wind_coefficient <= 1:
358
+ raise ValueError(f"Wind coefficient {wind_coefficient} must be between 0 and 1")
359
 
360
+ rho = 1.2 # Air density in kg/m³
361
+ return 0.5 * wind_coefficient * rho * wind_speed**2
 
 
 
362
 
363
+ def stack_pressure_difference(self, height: float, indoor_temp: float, outdoor_temp: float) -> float:
 
 
364
  """
365
+ Calculate pressure difference due to stack effect.
366
 
367
  Args:
368
+ height: Height difference in m
369
+ indoor_temp: Indoor temperature in K
370
+ outdoor_temp: Outdoor temperature in K
 
 
371
 
372
  Returns:
373
+ Pressure difference in Pa
374
  """
375
+ if height < 0:
376
+ raise ValueError(f"Height {height} m cannot be negative")
377
+ if indoor_temp <= 0 or outdoor_temp <= 0:
378
+ raise ValueError("Temperatures must be positive in Kelvin")
 
 
 
 
 
379
 
380
+ g = 9.81 # Gravitational acceleration in m/s²
381
+ rho = 1.2 # Air density in kg/m³
382
+ delta_t = abs(indoor_temp - outdoor_temp)
383
+ t_avg = (indoor_temp + outdoor_temp) / 2
384
+ return rho * g * height * delta_t / t_avg
 
 
 
385
 
386
+ def combined_pressure_difference(self, wind_pd: float, stack_pd: float) -> float:
 
 
387
  """
388
+ Calculate combined pressure difference from wind and stack effects.
389
 
390
  Args:
391
+ wind_pd: Wind pressure difference in Pa
392
+ stack_pd: Stack pressure difference in Pa
 
 
 
393
 
394
  Returns:
395
+ Combined pressure difference in Pa
396
  """
397
+ if wind_pd < 0 or stack_pd < 0:
398
+ raise ValueError("Pressure differences cannot be negative")
399
+ return math.sqrt(wind_pd**2 + stack_pd**2)
400
 
401
+ def crack_method_infiltration(self, crack_length: float, coefficient: float,
402
+ pressure_difference: float) -> float:
 
403
  """
404
+ Calculate infiltration flow rate using crack method.
405
 
406
  Args:
407
+ crack_length: Total crack length in m
408
+ coefficient: Flow coefficient in m³/(s·m·Pa^n)
409
+ pressure_difference: Pressure difference in Pa
 
 
410
 
411
  Returns:
412
+ Infiltration flow rate in m³/s
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
413
  """
414
+ if crack_length < 0:
415
+ raise ValueError(f"Crack length {crack_length} m cannot be negative")
416
+ if coefficient < 0:
417
+ raise ValueError(f"Coefficient {coefficient} cannot be negative")
418
+ if pressure_difference < 0:
419
+ raise ValueError(f"Pressure difference {pressure_difference} Pa cannot be negative")
420
 
421
+ n = 0.65 # Flow exponent
422
+ return coefficient * crack_length * pressure_difference**n
 
 
 
 
 
 
 
423
 
424
+ def sol_air_temperature(self, outdoor_temp: float, solar_irradiance: float,
425
+ surface_absorptivity: float, surface_resistance: float) -> float:
426
  """
427
+ Calculate sol-air temperature for a surface.
428
 
429
  Args:
430
+ outdoor_temp: Outdoor air temperature in °C
431
+ solar_irradiance: Solar irradiance on surface in W/m²
432
+ surface_absorptivity: Surface absorptivity (0-1)
433
+ surface_resistance: Surface resistance in m²·K/W
434
 
435
  Returns:
436
+ Sol-air temperature in °C
437
  """
438
+ self.validate_inputs(outdoor_temp)
439
+ if solar_irradiance < 0:
440
+ raise ValueError(f"Solar irradiance {solar_irradiance} W/m² cannot be negative")
441
+ if not 0 <= surface_absorptivity <= 1:
442
+ raise ValueError(f"Surface absorptivity {surface_absorptivity} must be between 0 and 1")
443
+ if surface_resistance < 0:
444
+ raise ValueError(f"Surface resistance {surface_resistance} m²·K/W cannot be negative")
445
+
446
+ h_ext = 1 / surface_resistance # External convective coefficient
447
+ delta_t_rad = surface_absorptivity * solar_irradiance / h_ext
448
+ return outdoor_temp + delta_t_rad
449
 
450
+ def solar_heat_gain(self, irradiance: float, area: float, shgc: float,
451
+ shading_coefficient: float = 1.0) -> float:
452
  """
453
+ Calculate solar heat gain through a surface.
454
 
455
  Args:
456
+ irradiance: Solar irradiance on surface in W/m²
457
+ area: Surface area in
458
+ shgc: Solar heat gain coefficient (0-1)
459
+ shading_coefficient: Shading coefficient (0-1)
460
 
461
  Returns:
462
+ Solar heat gain in W
 
 
 
 
 
 
463
  """
464
+ self.validate_inputs(0, area)
465
+ if irradiance < 0:
466
+ raise ValueError(f"Irradiance {irradiance} W/m² cannot be negative")
467
+ if not 0 <= shgc <= 1:
468
+ raise ValueError(f"SHGC {shgc} must be between 0 and 1")
469
+ if not 0 <= shading_coefficient <= 1:
470
+ raise ValueError(f"Shading coefficient {shading_coefficient} must be between 0 and 1")
471
 
472
+ return irradiance * area * shgc * shading_coefficient
 
 
 
 
 
 
 
 
 
473
 
474
 
475
  # Create a singleton instance
476
+ heat_transfer_calculator = HeatTransferCalculations()
477
 
478
  # Example usage
479
  if __name__ == "__main__":
480
+ # Example solar calculations
481
+ latitude = 40.0
482
+ day_of_year = 204
483
+ hour = 12.0
484
 
485
+ declination = heat_transfer_calculator.solar.solar_declination(day_of_year)
486
+ hour_angle = heat_transfer_calculator.solar.solar_hour_angle(hour)
487
+ altitude = heat_transfer_calculator.solar.solar_altitude(latitude, declination, hour_angle)
488
+ azimuth = heat_transfer_calculator.solar.solar_azimuth(latitude, declination, hour_angle, altitude)
489
 
490
+ print(f"Solar Declination: {declination:.2f}°")
491
+ print(f"Solar Hour Angle: {hour_angle:.2f}°")
492
+ print(f"Solar Altitude: {altitude:.2f}°")
493
+ print(f"Solar Azimuth: {azimuth:.2f}°")
494
 
495
+ # Example heat transfer calculation
496
+ u_value = 0.5 # W/(m²·K)
497
+ area = 20.0 #
498
+ delta_t = 10.0 # °C
499
+ conduction = heat_transfer_calculator.conduction_heat_transfer(u_value, area, delta_t)
500
+ print(f"Conduction Heat Transfer: {conduction:.2f} W")