mabuseif commited on
Commit
16e2fd4
·
verified ·
1 Parent(s): 49d8002

Update utils/ctf_calculations.py

Browse files
Files changed (1) hide show
  1. utils/ctf_calculations.py +101 -95
utils/ctf_calculations.py CHANGED
@@ -2,7 +2,9 @@
2
  CTF Calculations Module
3
 
4
  This module contains the CTFCalculator class for calculating Conduction Transfer Function (CTF)
5
- coefficients for HVAC load calculations using the implicit Finite Difference Method.
 
 
6
 
7
  Developed by: Dr Majed Abuseif, Deakin University
8
  © 2025
@@ -16,13 +18,19 @@ import logging
16
  import threading
17
  from typing import List, Dict, Any, NamedTuple
18
  import streamlit as st
19
- from utils.enums import ComponentType
20
- from utils.solar import SolarCalculations
21
 
22
  # Configure logging
23
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
24
  logger = logging.getLogger(__name__)
25
 
 
 
 
 
 
 
 
26
  class CTFCoefficients(NamedTuple):
27
  X: List[float] # Exterior temperature coefficients
28
  Y: List[float] # Cross coefficients
@@ -36,6 +44,74 @@ class CTFCalculator:
36
  _ctf_cache = {}
37
  _cache_lock = threading.Lock() # Thread-safe lock for cache access
38
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  @staticmethod
40
  def _hash_construction(construction: Dict[str, Any]) -> str:
41
  """Generate a unique hash for a construction based on its properties.
@@ -87,14 +163,15 @@ class CTFCalculator:
87
  return {}
88
 
89
  @classmethod
90
- def calculate_ctf_coefficients(cls, component: Dict[str, Any]) -> CTFCoefficients:
91
- """Calculate CTF coefficients using implicit Finite Difference Method.
92
 
93
  Note: Per ASHRAE, CTF calculations are skipped for WINDOW and SKYLIGHT components,
94
  as they use typical material properties. CTF tables for these components will be added later.
95
 
96
  Args:
97
  component: Dictionary containing component properties from st.session_state.project_data["components"].
 
98
 
99
  Returns:
100
  CTFCoefficients: Named tuple containing X, Y, Z, and F coefficients.
@@ -162,13 +239,28 @@ class CTFCalculator:
162
  rho = [m['density'] for m in material_props] # kg/m³
163
  c = [m['specific_heat'] for m in material_props] # J/kg·K
164
  alpha = [k_i / (rho_i * c_i) if rho_i * c_i > 0 else 0.0 for k_i, rho_i, c_i in zip(k, rho, c)] # Thermal diffusivity (m²/s)
 
 
165
 
166
  # Discretization parameters
167
  dt = 3600 # 1-hour time step (s)
168
  nodes_per_layer = 3 # 2–4 nodes per layer for balance
169
- R_out = 0.04 # Outdoor surface resistance (m²·K/W, ASHRAE)
170
  R_in = 0.12 # Indoor surface resistance (m²·K/W, ASHRAE)
171
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  # Calculate node spacing and check stability
173
  total_nodes = sum(nodes_per_layer for _ in thicknesses)
174
  dx = [t / nodes_per_layer for t in thicknesses] # Node spacing per layer
@@ -211,7 +303,7 @@ class CTFCalculator:
211
  if node_j == 0 and layer_idx == 0: # Outdoor surface node
212
  A[idx, idx] = 1.0 + 2 * dt * k_i / (dx_i * rho_i * c_i * dx_i) + dt / (rho_i * c_i * dx_i * R_out)
213
  A[idx, idx + 1] = -2 * dt * k_i / (dx_i * rho_i * c_i * dx_i)
214
- b[idx] = dt / (rho_i * c_i * dx_i * R_out) # Outdoor temp contribution
215
  elif node_j == nodes_per_layer - 1 and layer_idx == len(thicknesses) - 1: # Indoor surface node
216
  A[idx, idx] = 1.0 + 2 * dt * k_i / (dx_i * rho_i * c_i * dx_i) + dt / (rho_i * c_i * dx_i * R_in)
217
  A[idx, idx - 1] = -2 * dt * k_i / (dx_i * rho_i * c_i * dx_i)
@@ -257,7 +349,7 @@ class CTFCalculator:
257
  for t in range(num_ctf):
258
  b_out = b.copy()
259
  if t == 0:
260
- b_out[0] = dt / (rho[0] * c[0] * dx[0] * R_out) if rho[0] * c[0] * dx[0] != 0 else 0.0 # Unit outdoor temp impulse
261
  T = sparse_linalg.spsolve(A, b_out + T_prev)
262
  q_in = (T[-1] - 0.0) / R_in # Indoor heat flux (W/m²)
263
  Y[t] = q_in
@@ -304,90 +396,4 @@ class CTFCalculator:
304
  CTFCoefficients: Placeholder zero coefficients until implementation.
305
  """
306
  logger.info(f"CTF table calculation for {component.get('type', 'Unknown')} component '{component.get('name', 'Unknown')}' not yet implemented. Returning zero coefficients.")
307
- return CTFCoefficients(X=[0.0], Y=[0.0], Z=[0.0], F=[0.0])
308
-
309
- @classmethod
310
- def calculate_heat_flux(cls, component: Dict[str, Any], hourly_data: Dict[str, Any],
311
- solar_calc: SolarCalculations, building_info: Dict[str, Any]) -> float:
312
- """Calculate hourly heat flux using CTF coefficients and sol-air temperature.
313
-
314
- Args:
315
- component: Dictionary containing component properties.
316
- hourly_data: Dictionary with month, day, hour, dry_bulb, dew_point, wind_speed,
317
- total_sky_cover, global_horizontal_radiation, direct_normal_radiation,
318
- diffuse_horizontal_radiation.
319
- solar_calc: SolarCalculations instance for sol-air temperature.
320
- building_info: Building information dictionary.
321
-
322
- Returns:
323
- float: Heat flux through the component (W/m²).
324
-
325
- References:
326
- ASHRAE Handbook—Fundamentals, Chapter 26.
327
- """
328
- # Get component type
329
- comp_type_str = component.get('type', '').lower()
330
- comp_type_map = {
331
- 'walls': ComponentType.WALL,
332
- 'roofs': ComponentType.ROOF,
333
- 'floors': ComponentType.FLOOR,
334
- 'windows': ComponentType.WINDOW,
335
- 'skylights': ComponentType.SKYLIGHT
336
- }
337
- component_type = comp_type_map.get(comp_type_str, None)
338
- if not component_type:
339
- logger.warning(f"Invalid component type '{comp_type_str}' for component '{component.get('name', 'Unknown')}'. Returning zero flux.")
340
- return 0.0
341
-
342
- # Skip for WINDOW, SKYLIGHT (handled via SHGC)
343
- if component_type in [ComponentType.WINDOW, ComponentType.SKYLIGHT]:
344
- logger.info(f"Skipping heat flux calculation for {component_type.value} component '{component.get('name', 'Unknown')}'.")
345
- return 0.0
346
-
347
- # Get CTF coefficients
348
- ctf = cls.calculate_ctf_coefficients(component)
349
- if not any(ctf.X + ctf.Y + ctf.Z + ctf.F):
350
- logger.warning(f"Zero CTF coefficients for component '{component.get('name', 'Unknown')}'. Returning zero flux.")
351
- return 0.0
352
-
353
- # Extract hourly data
354
- T_out = hourly_data.get('dry_bulb', 25.0)
355
- dew_point = hourly_data.get('dew_point', T_out - 5.0)
356
- wind_speed = hourly_data.get('wind_speed', 4.0)
357
- total_sky_cover = hourly_data.get('total_sky_cover', 0.5)
358
- T_in = 24.0 # Assume constant indoor temperature (adjust as needed)
359
-
360
- # Get surface parameters and total incident radiation
361
- surface_tilt, surface_azimuth, h_o, emissivity, absorptivity = solar_calc.get_surface_parameters(
362
- component, building_info, wind_speed)
363
-
364
- # Calculate total incident radiation (simplified, reuse solar_calc logic)
365
- ghi = hourly_data.get('global_horizontal_radiation', 0.0)
366
- dni = hourly_data.get('direct_normal_radiation', ghi * 0.7)
367
- dhi = hourly_data.get('diffuse_horizontal_radiation', ghi * 0.3)
368
- if ghi <= 0:
369
- I_t = 0.0
370
- else:
371
- # Simplified radiation calculation (full calc in solar.py)
372
- view_factor = (1 - math.cos(math.radians(surface_tilt))) / 2
373
- ground_reflectivity = 0.2
374
- cos_theta = max(math.cos(math.radians(surface_tilt)), 0.0) # Simplified for CTF
375
- I_t = dni * cos_theta + dhi + ground_reflectivity * ghi * view_factor
376
-
377
- # Calculate sol-air temperature
378
- T_sol_air = solar_calc.calculate_sol_air_temperature(
379
- T_out, I_t, absorptivity, emissivity, h_o, dew_point, total_sky_cover)
380
-
381
- # Calculate heat flux using CTF (q_t = Σ X_i * T_sol_air(t-i) + Σ Z_i * T_in(t-i) - Σ F_i * q(t-i))
382
- num_ctf = len(ctf.X)
383
- q_t = 0.0
384
- for i in range(num_ctf):
385
- # Assume T_sol_air and T_in are constant for history (simplified, extend with history buffer if needed)
386
- q_t += ctf.X[i] * T_sol_air + ctf.Z[i] * T_in
387
- # Flux history (F) requires previous q values; assume zero for first calculation
388
- q_t -= ctf.F[i] * 0.0 # Placeholder, implement history buffer for accurate F terms
389
-
390
- logger.info(f"Calculated heat flux for component '{component.get('name', 'Unknown')}' at "
391
- f"{hourly_data.get('month')}/{hourly_data.get('day')}/{hourly_data.get('hour')}: "
392
- f"{q_t:.2f} W/m² (T_sol_air={T_sol_air:.2f}°C)")
393
- return q_t
 
2
  CTF Calculations Module
3
 
4
  This module contains the CTFCalculator class for calculating Conduction Transfer Function (CTF)
5
+ coefficients for HVAC load calculations using the implicit Finite Difference Method, enhanced
6
+ with sol-air temperature calculations accounting for solar radiation, longwave radiation, and
7
+ dynamic outdoor heat transfer coefficient.
8
 
9
  Developed by: Dr Majed Abuseif, Deakin University
10
  © 2025
 
18
  import threading
19
  from typing import List, Dict, Any, NamedTuple
20
  import streamlit as st
21
+ from enum import Enum
 
22
 
23
  # Configure logging
24
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
25
  logger = logging.getLogger(__name__)
26
 
27
+ class ComponentType(Enum):
28
+ WALL = "Wall"
29
+ ROOF = "Roof"
30
+ FLOOR = "Floor"
31
+ WINDOW = "Window"
32
+ SKYLIGHT = "Skylight"
33
+
34
  class CTFCoefficients(NamedTuple):
35
  X: List[float] # Exterior temperature coefficients
36
  Y: List[float] # Cross coefficients
 
44
  _ctf_cache = {}
45
  _cache_lock = threading.Lock() # Thread-safe lock for cache access
46
 
47
+ @staticmethod
48
+ def calculate_sky_temperature(T_out: float, dew_point: float, total_sky_cover: float = 0.5) -> float:
49
+ """Calculate sky temperature using cloud-cover-dependent model.
50
+
51
+ Args:
52
+ T_out (float): Outdoor dry-bulb temperature (°C).
53
+ dew_point (float): Dew point temperature (°C).
54
+ total_sky_cover (float): Sky cover fraction (0 to 1).
55
+
56
+ Returns:
57
+ float: Sky temperature (°C), bounded by dew point.
58
+
59
+ References:
60
+ ASHRAE Handbook—Fundamentals (2021), Chapter 26.
61
+ """
62
+ epsilon_sky = 0.9 + 0.04 * total_sky_cover
63
+ T_sky = (epsilon_sky * (T_out + 273.15)**4)**0.25 - 273.15
64
+ return T_sky if dew_point <= T_out else dew_point
65
+
66
+ @staticmethod
67
+ def calculate_h_o(wind_speed: float, surface_type: ComponentType) -> float:
68
+ """Calculate dynamic outdoor heat transfer coefficient based on wind speed and surface type.
69
+
70
+ Args:
71
+ wind_speed (float): Wind speed (m/s).
72
+ surface_type (ComponentType): Type of surface (WALL, ROOF, FLOOR, WINDOW, SKYLIGHT).
73
+
74
+ Returns:
75
+ float: Outdoor heat transfer coefficient (W/m²·K).
76
+
77
+ References:
78
+ ASHRAE Handbook—Fundamentals (2021), Chapter 26.
79
+ """
80
+ from app.m_c_data import DEFAULT_WINDOW_PROPERTIES # Delayed import to avoid circular dependency
81
+ wind_speed = max(min(wind_speed, 20.0), 0.0) # Bound for stability
82
+ if surface_type in [ComponentType.WALL, ComponentType.FLOOR]:
83
+ h_o = 8.3 + 4.0 * (wind_speed ** 0.6) # ASHRAE Ch. 26
84
+ elif surface_type == ComponentType.ROOF:
85
+ h_o = 9.1 + 2.8 * wind_speed # ASHRAE Ch. 26
86
+ else: # WINDOW, SKYLIGHT
87
+ h_o = DEFAULT_WINDOW_PROPERTIES["h_o"]
88
+ return max(h_o, 5.0) # Minimum for stability
89
+
90
+ @staticmethod
91
+ def calculate_sol_air_temperature(T_out: float, I_t: float, absorptivity: float, emissivity: float,
92
+ h_o: float, dew_point: float, total_sky_cover: float = 0.5) -> float:
93
+ """Calculate sol-air temperature for a surface.
94
+
95
+ Args:
96
+ T_out (float): Outdoor dry-bulb temperature (°C).
97
+ I_t (float): Total incident solar radiation (W/m²).
98
+ absorptivity (float): Surface absorptivity.
99
+ emissivity (float): Surface emissivity.
100
+ h_o (float): Outdoor heat transfer coefficient (W/m²·K).
101
+ dew_point (float): Dew point temperature (°C).
102
+ total_sky_cover (float): Sky cover fraction (0 to 1).
103
+
104
+ Returns:
105
+ float: Sol-air temperature (°C).
106
+
107
+ References:
108
+ ASHRAE Handbook—Fundamentals (2021), Chapter 26.
109
+ """
110
+ sigma = 5.67e-8 # Stefan-Boltzmann constant (W/m²·K⁴)
111
+ T_sky = CTFCalculator.calculate_sky_temperature(T_out, dew_point, total_sky_cover)
112
+ T_sol_air = T_out + (absorptivity * I_t - emissivity * sigma * ((T_out + 273.15)**4 - (T_sky + 273.15)**4)) / h_o
113
+ return T_sol_air
114
+
115
  @staticmethod
116
  def _hash_construction(construction: Dict[str, Any]) -> str:
117
  """Generate a unique hash for a construction based on its properties.
 
163
  return {}
164
 
165
  @classmethod
166
+ def calculate_ctf_coefficients(cls, component: Dict[str, Any], hourly_data: Dict[str, Any] = None) -> CTFCoefficients:
167
+ """Calculate CTF coefficients using implicit Finite Difference Method with sol-air temperature.
168
 
169
  Note: Per ASHRAE, CTF calculations are skipped for WINDOW and SKYLIGHT components,
170
  as they use typical material properties. CTF tables for these components will be added later.
171
 
172
  Args:
173
  component: Dictionary containing component properties from st.session_state.project_data["components"].
174
+ hourly_data: Dictionary containing hourly weather data (T_out, dew_point, wind_speed, total_sky_cover, I_t).
175
 
176
  Returns:
177
  CTFCoefficients: Named tuple containing X, Y, Z, and F coefficients.
 
239
  rho = [m['density'] for m in material_props] # kg/m³
240
  c = [m['specific_heat'] for m in material_props] # J/kg·K
241
  alpha = [k_i / (rho_i * c_i) if rho_i * c_i > 0 else 0.0 for k_i, rho_i, c_i in zip(k, rho, c)] # Thermal diffusivity (m²/s)
242
+ absorptivity = material_props[0].get('absorptivity', 0.6) # Use first layer's absorptivity
243
+ emissivity = material_props[0].get('emissivity', 0.9) # Use first layer's emissivity
244
 
245
  # Discretization parameters
246
  dt = 3600 # 1-hour time step (s)
247
  nodes_per_layer = 3 # 2–4 nodes per layer for balance
 
248
  R_in = 0.12 # Indoor surface resistance (m²·K/W, ASHRAE)
249
 
250
+ # Get weather data for sol-air temperature
251
+ T_out = hourly_data.get('dry_bulb', 25.0) if hourly_data else 25.0
252
+ dew_point = hourly_data.get('dew_point', T_out - 5.0) if hourly_data else T_out - 5.0
253
+ wind_speed = hourly_data.get('wind_speed', 4.0) if hourly_data else 4.0
254
+ total_sky_cover = hourly_data.get('total_sky_cover', 0.5) if hourly_data else 0.5
255
+ I_t = hourly_data.get('total_incident_radiation', 0.0) if hourly_data else 0.0
256
+
257
+ # Calculate dynamic h_o and sol-air temperature
258
+ h_o = cls.calculate_h_o(wind_speed, component_type)
259
+ T_sol_air = cls.calculate_sol_air_temperature(T_out, I_t, absorptivity, emissivity, h_o, dew_point, total_sky_cover)
260
+ R_out = 1.0 / h_o # Outdoor surface resistance based on dynamic h_o
261
+
262
+ logger.info(f"Calculated h_o={h_o:.2f} W/m²·K, T_sol_air={T_sol_air:.2f}°C for component '{component.get('name', 'Unknown')}'")
263
+
264
  # Calculate node spacing and check stability
265
  total_nodes = sum(nodes_per_layer for _ in thicknesses)
266
  dx = [t / nodes_per_layer for t in thicknesses] # Node spacing per layer
 
303
  if node_j == 0 and layer_idx == 0: # Outdoor surface node
304
  A[idx, idx] = 1.0 + 2 * dt * k_i / (dx_i * rho_i * c_i * dx_i) + dt / (rho_i * c_i * dx_i * R_out)
305
  A[idx, idx + 1] = -2 * dt * k_i / (dx_i * rho_i * c_i * dx_i)
306
+ b[idx] = dt / (rho_i * c_i * dx_i * R_out) * T_sol_air # Use sol-air temperature
307
  elif node_j == nodes_per_layer - 1 and layer_idx == len(thicknesses) - 1: # Indoor surface node
308
  A[idx, idx] = 1.0 + 2 * dt * k_i / (dx_i * rho_i * c_i * dx_i) + dt / (rho_i * c_i * dx_i * R_in)
309
  A[idx, idx - 1] = -2 * dt * k_i / (dx_i * rho_i * c_i * dx_i)
 
349
  for t in range(num_ctf):
350
  b_out = b.copy()
351
  if t == 0:
352
+ b_out[0] = dt / (rho[0] * c[0] * dx[0] * R_out) * T_sol_air if rho[0] * c[0] * dx[0] != 0 else 0.0 # Unit outdoor temp impulse with sol-air
353
  T = sparse_linalg.spsolve(A, b_out + T_prev)
354
  q_in = (T[-1] - 0.0) / R_in # Indoor heat flux (W/m²)
355
  Y[t] = q_in
 
396
  CTFCoefficients: Placeholder zero coefficients until implementation.
397
  """
398
  logger.info(f"CTF table calculation for {component.get('type', 'Unknown')} component '{component.get('name', 'Unknown')}' not yet implemented. Returning zero coefficients.")
399
+ return CTFCoefficients(X=[0.0], Y=[0.0], Z=[0.0], F=[0.0])