mabuseif commited on
Commit
54945c1
·
verified ·
1 Parent(s): 83e8a76

Update utils/ctf_calculations.py

Browse files
Files changed (1) hide show
  1. utils/ctf_calculations.py +121 -38
utils/ctf_calculations.py CHANGED
@@ -14,10 +14,9 @@ import scipy.sparse.linalg as sparse_linalg
14
  import hashlib
15
  import logging
16
  import threading
17
- from typing import List
18
- from data.material_library import Construction
19
  from enum import Enum
20
- from typing import Dict, List, Optional, NamedTuple
21
 
22
  # Configure logging
23
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
@@ -28,7 +27,6 @@ class ComponentType(Enum):
28
  ROOF = "Roof"
29
  FLOOR = "Floor"
30
  WINDOW = "Window"
31
- DOOR = "Door"
32
  SKYLIGHT = "Skylight"
33
 
34
  class CTFCoefficients(NamedTuple):
@@ -45,66 +43,138 @@ class CTFCalculator:
45
  _cache_lock = threading.Lock() # Thread-safe lock for cache access
46
 
47
  @staticmethod
48
- def _hash_construction(construction: Construction) -> str:
49
  """Generate a unique hash for a construction based on its properties.
50
 
51
  Args:
52
- construction: Construction object containing material layers.
53
 
54
  Returns:
55
  str: SHA-256 hash of the construction properties.
56
  """
57
- hash_input = f"{construction.name}"
58
- for layer in construction.layers:
59
- material = layer["material"]
60
- hash_input += f"{material.name}{material.conductivity}{material.density}{material.specific_heat}{layer['thickness']}"
 
 
61
  return hashlib.sha256(hash_input.encode()).hexdigest()
62
 
63
  @classmethod
64
- def calculate_ctf_coefficients(cls, component) -> CTFCoefficients:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  """Calculate CTF coefficients using implicit Finite Difference Method.
66
 
67
- Note: Per ASHRAE, CTF calculations are skipped for WINDOW, DOOR, and SKYLIGHT components,
68
  as they use typical material properties. CTF tables for these components will be added later.
69
 
70
  Args:
71
- component: Building component with construction properties.
72
 
73
  Returns:
74
  CTFCoefficients: Named tuple containing X, Y, Z, and F coefficients.
75
  """
76
- # Skip CTF for WINDOW, DOOR, SKYLIGHT as per ASHRAE; return zero coefficients
77
- if component.component_type in [ComponentType.WINDOW, ComponentType.DOOR, ComponentType.SKYLIGHT]:
78
- logger.info(f"Skipping CTF calculation for {component.component_type.value} component '{component.name}'. Using zero coefficients until CTF tables are implemented.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  return CTFCoefficients(X=[0.0], Y=[0.0], Z=[0.0], F=[0.0])
80
 
81
- # Check if construction exists and has layers
82
- construction = component.construction
83
- if not construction or not construction.layers:
84
- logger.warning(f"No valid construction or layers for component '{component.name}' ({component.component_type.value}). Returning zero CTFs.")
 
 
 
 
 
 
85
  return CTFCoefficients(X=[0.0], Y=[0.0], Z=[0.0], F=[0.0])
86
 
87
  # Check cache with thread-safe access
88
  construction_hash = cls._hash_construction(construction)
89
  with cls._cache_lock:
90
  if construction_hash in cls._ctf_cache:
91
- logger.info(f"Using cached CTF coefficients for construction {construction.name}")
92
  return cls._ctf_cache[construction_hash]
93
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  # Discretization parameters
95
  dt = 3600 # 1-hour time step (s)
96
  nodes_per_layer = 3 # 2–4 nodes per layer for balance
97
  R_out = 0.04 # Outdoor surface resistance (m²·K/W, ASHRAE)
98
  R_in = 0.12 # Indoor surface resistance (m²·K/W, ASHRAE)
99
 
100
- # Collect layer properties
101
- thicknesses = [layer["thickness"] for layer in construction.layers]
102
- materials = [layer["material"] for layer in construction.layers]
103
- k = [m.conductivity for m in materials] # W/m·K
104
- rho = [m.density for m in materials] # kg/m³
105
- c = [m.specific_heat for m in materials] # J/kg·K
106
- alpha = [k_i / (rho_i * c_i) for k_i, rho_i, c_i in zip(k, rho, c)] # Thermal diffusivity (m²/s)
107
-
108
  # Calculate node spacing and check stability
109
  total_nodes = sum(nodes_per_layer for _ in thicknesses)
110
  dx = [t / nodes_per_layer for t in thicknesses] # Node spacing per layer
@@ -117,9 +187,12 @@ class CTFCalculator:
117
 
118
  # Stability check: Fourier number
119
  for i, (a, d) in enumerate(zip(alpha, dx)):
 
 
 
120
  Fo = a * dt / (d ** 2)
121
  if Fo < 0.33:
122
- logger.warning(f"Fourier number {Fo:.3f} < 0.33 for layer {i} ({materials[i].name}). Adjusting node spacing.")
123
  dx[i] = np.sqrt(a * dt / 0.33)
124
  nodes_per_layer = max(2, int(np.ceil(thicknesses[i] / dx[i])))
125
  dx[i] = thicknesses[i] / nodes_per_layer
@@ -137,6 +210,10 @@ class CTFCalculator:
137
  c_i = c[layer_idx]
138
  dx_i = dx[layer_idx]
139
 
 
 
 
 
140
  if node_j == 0 and layer_idx == 0: # Outdoor surface node
141
  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)
142
  A[idx, idx + 1] = -2 * dt * k_i / (dx_i * rho_i * c_i * dx_i)
@@ -150,6 +227,9 @@ class CTFCalculator:
150
  dx_next = dx[layer_idx + 1]
151
  rho_next = rho[layer_idx + 1]
152
  c_next = c[layer_idx + 1]
 
 
 
153
  A[idx, idx] = 1.0 + dt * (k_i / dx_i + k_next / dx_next) / (0.5 * (rho_i * c_i * dx_i + rho_next * c_next * dx_next))
154
  A[idx, idx - 1] = -dt * k_i / (dx_i * 0.5 * (rho_i * c_i * dx_i + rho_next * c_next * dx_next))
155
  A[idx, idx + 1] = -dt * k_next / (dx_next * 0.5 * (rho_i * c_i * dx_i + rho_next * c_next * dx_next))
@@ -158,6 +238,9 @@ class CTFCalculator:
158
  dx_prev = dx[layer_idx - 1]
159
  rho_prev = rho[layer_idx - 1]
160
  c_prev = c[layer_idx - 1]
 
 
 
161
  A[idx, idx] = 1.0 + dt * (k_prev / dx_prev + k_i / dx_i) / (0.5 * (rho_prev * c_prev * dx_prev + rho_i * c_i * dx_i))
162
  A[idx, idx - 1] = -dt * k_prev / (dx_prev * 0.5 * (rho_prev * c_prev * dx_prev + rho_i * c_i * dx_i))
163
  A[idx, idx + 1] = -dt * k_i / (dx_i * 0.5 * (rho_prev * c_prev * dx_prev + rho_i * c_i * dx_i))
@@ -180,7 +263,7 @@ class CTFCalculator:
180
  for t in range(num_ctf):
181
  b_out = b.copy()
182
  if t == 0:
183
- b_out[0] = dt / (rho[0] * c[0] * dx[0] * R_out) # Unit outdoor temp impulse
184
  T = sparse_linalg.spsolve(A, b_out + T_prev)
185
  q_in = (T[-1] - 0.0) / R_in # Indoor heat flux (W/m²)
186
  Y[t] = q_in
@@ -193,7 +276,7 @@ class CTFCalculator:
193
  for t in range(num_ctf):
194
  b_in = b.copy()
195
  if t == 0:
196
- b_in[-1] = dt / (rho[-1] * c[-1] * dx[-1] * R_in) # Unit indoor temp impulse
197
  T = sparse_linalg.spsolve(A, b_in + T_prev)
198
  q_in = (T[-1] - 0.0) / R_in
199
  Z[t] = q_in
@@ -204,7 +287,7 @@ class CTFCalculator:
204
  for t in range(num_ctf):
205
  b_flux = np.zeros(total_nodes)
206
  if t == 0:
207
- b_flux[-1] = -1.0 / (rho[-1] * c[-1] * dx[-1]) # Unit flux impulse
208
  T = sparse_linalg.spsolve(A, b_flux + T_prev)
209
  q_in = (T[-1] - 0.0) / R_in
210
  F[t] = q_in
@@ -213,18 +296,18 @@ class CTFCalculator:
213
  ctf = CTFCoefficients(X=X, Y=Y, Z=Z, F=F)
214
  with cls._cache_lock:
215
  cls._ctf_cache[construction_hash] = ctf
216
- logger.info(f"Calculated CTF coefficients for construction {construction.name}")
217
  return ctf
218
 
219
  @classmethod
220
- def calculate_ctf_tables(cls, component) -> CTFCoefficients:
221
- """Placeholder for future implementation of CTF table lookups for windows, doors, and skylights.
222
 
223
  Args:
224
- component: Building component with construction properties.
225
 
226
  Returns:
227
  CTFCoefficients: Placeholder zero coefficients until implementation.
228
  """
229
- logger.info(f"CTF table calculation for {component.component_type.value} component '{component.name}' not yet implemented. Returning zero coefficients.")
230
  return CTFCoefficients(X=[0.0], Y=[0.0], Z=[0.0], F=[0.0])
 
14
  import hashlib
15
  import logging
16
  import threading
17
+ from typing import List, Dict, Any, NamedTuple
18
+ import streamlit as st
19
  from enum import Enum
 
20
 
21
  # Configure logging
22
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
 
27
  ROOF = "Roof"
28
  FLOOR = "Floor"
29
  WINDOW = "Window"
 
30
  SKYLIGHT = "Skylight"
31
 
32
  class CTFCoefficients(NamedTuple):
 
43
  _cache_lock = threading.Lock() # Thread-safe lock for cache access
44
 
45
  @staticmethod
46
+ def _hash_construction(construction: Dict[str, Any]) -> str:
47
  """Generate a unique hash for a construction based on its properties.
48
 
49
  Args:
50
+ construction: Dictionary containing construction properties (name, layers).
51
 
52
  Returns:
53
  str: SHA-256 hash of the construction properties.
54
  """
55
+ hash_input = f"{construction.get('name', '')}"
56
+ layers = construction.get('layers', [])
57
+ for layer in layers:
58
+ material_name = layer.get('material', '')
59
+ thickness = layer.get('thickness', 0.0)
60
+ hash_input += f"{material_name}{thickness}"
61
  return hashlib.sha256(hash_input.encode()).hexdigest()
62
 
63
  @classmethod
64
+ def _get_material_properties(cls, material_name: str) -> Dict[str, float]:
65
+ """Retrieve material properties from session state.
66
+
67
+ Args:
68
+ material_name: Name of the material.
69
+
70
+ Returns:
71
+ Dict containing conductivity, density, specific_heat, absorptivity, emissivity.
72
+ Returns empty dict if material not found.
73
+ """
74
+ try:
75
+ materials = st.session_state.project_data.get('materials', {})
76
+ material = materials.get('library', {}).get(material_name, materials.get('project', {}).get(material_name))
77
+ if not material:
78
+ logger.error(f"Material '{material_name}' not found in library or project materials.")
79
+ return {}
80
+
81
+ # Extract required properties
82
+ thermal_props = material.get('thermal_properties', {})
83
+ return {
84
+ 'name': material_name,
85
+ 'conductivity': thermal_props.get('conductivity', 0.0),
86
+ 'density': thermal_props.get('density', 0.0),
87
+ 'specific_heat': thermal_props.get('specific_heat', 0.0),
88
+ 'absorptivity': material.get('absorptivity', 0.6),
89
+ 'emissivity': material.get('emissivity', 0.9)
90
+ }
91
+ except Exception as e:
92
+ logger.error(f"Error retrieving material '{material_name}' properties: {str(e)}")
93
+ return {}
94
+
95
+ @classmethod
96
+ def calculate_ctf_coefficients(cls, component: Dict[str, Any]) -> CTFCoefficients:
97
  """Calculate CTF coefficients using implicit Finite Difference Method.
98
 
99
+ Note: Per ASHRAE, CTF calculations are skipped for WINDOW and SKYLIGHT components,
100
  as they use typical material properties. CTF tables for these components will be added later.
101
 
102
  Args:
103
+ component: Dictionary containing component properties from st.session_state.project_data["components"].
104
 
105
  Returns:
106
  CTFCoefficients: Named tuple containing X, Y, Z, and F coefficients.
107
  """
108
+ # Determine component type
109
+ comp_type_str = component.get('type', '').lower() # Expected from component dictionary key (e.g., 'walls')
110
+ comp_type_map = {
111
+ 'walls': ComponentType.WALL,
112
+ 'roofs': ComponentType.ROOF,
113
+ 'floors': ComponentType.FLOOR,
114
+ 'windows': ComponentType.WINDOW,
115
+ 'skylights': ComponentType.SKYLIGHT
116
+ }
117
+ component_type = comp_type_map.get(comp_type_str, None)
118
+ if not component_type:
119
+ logger.warning(f"Invalid component type '{comp_type_str}' for component '{component.get('name', 'Unknown')}'. Returning zero CTFs.")
120
+ return CTFCoefficients(X=[0.0], Y=[0.0], Z=[0.0], F=[0.0])
121
+
122
+ # Skip CTF for WINDOW, SKYLIGHT as per ASHRAE; return zero coefficients
123
+ if component_type in [ComponentType.WINDOW, ComponentType.SKYLIGHT]:
124
+ logger.info(f"Skipping CTF calculation for {component_type.value} component '{component.get('name', 'Unknown')}'. Using zero coefficients until CTF tables are implemented.")
125
  return CTFCoefficients(X=[0.0], Y=[0.0], Z=[0.0], F=[0.0])
126
 
127
+ # Retrieve construction
128
+ construction_name = component.get('construction', '')
129
+ if not construction_name:
130
+ logger.warning(f"No construction specified for component '{component.get('name', 'Unknown')}' ({component_type.value}). Returning zero CTFs.")
131
+ return CTFCoefficients(X=[0.0], Y=[0.0], Z=[0.0], F=[0.0])
132
+
133
+ constructions = st.session_state.project_data.get('constructions', {})
134
+ construction = constructions.get('library', {}).get(construction_name, constructions.get('project', {}).get(construction_name))
135
+ if not construction or not construction.get('layers'):
136
+ logger.warning(f"No valid construction or layers found for construction '{construction_name}' in component '{component.get('name', 'Unknown')}' ({component_type.value}). Returning zero CTFs.")
137
  return CTFCoefficients(X=[0.0], Y=[0.0], Z=[0.0], F=[0.0])
138
 
139
  # Check cache with thread-safe access
140
  construction_hash = cls._hash_construction(construction)
141
  with cls._cache_lock:
142
  if construction_hash in cls._ctf_cache:
143
+ logger.info(f"Using cached CTF coefficients for construction '{construction_name}'")
144
  return cls._ctf_cache[construction_hash]
145
 
146
+ # Collect layer properties
147
+ thicknesses = []
148
+ material_props = []
149
+ for layer in construction.get('layers', []):
150
+ material_name = layer.get('material', '')
151
+ thickness = layer.get('thickness', 0.0)
152
+ if thickness <= 0.0:
153
+ logger.warning(f"Invalid thickness {thickness} for material '{material_name}' in construction '{construction_name}'. Skipping layer.")
154
+ continue
155
+ material = cls._get_material_properties(material_name)
156
+ if not material:
157
+ logger.warning(f"Skipping layer with material '{material_name}' in construction '{construction_name}' due to missing properties.")
158
+ continue
159
+ thicknesses.append(thickness)
160
+ material_props.append(material)
161
+
162
+ if not thicknesses or not material_props:
163
+ logger.warning(f"No valid layers with material properties for construction '{construction_name}' in component '{component.get('name', 'Unknown')}'. Returning zero CTFs.")
164
+ return CTFCoefficients(X=[0.0], Y=[0.0], Z=[0.0], F=[0.0])
165
+
166
+ # Extract material properties
167
+ k = [m['conductivity'] for m in material_props] # W/m·K
168
+ rho = [m['density'] for m in material_props] # kg/m³
169
+ c = [m['specific_heat'] for m in material_props] # J/kg·K
170
+ 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)
171
+
172
  # Discretization parameters
173
  dt = 3600 # 1-hour time step (s)
174
  nodes_per_layer = 3 # 2–4 nodes per layer for balance
175
  R_out = 0.04 # Outdoor surface resistance (m²·K/W, ASHRAE)
176
  R_in = 0.12 # Indoor surface resistance (m²·K/W, ASHRAE)
177
 
 
 
 
 
 
 
 
 
178
  # Calculate node spacing and check stability
179
  total_nodes = sum(nodes_per_layer for _ in thicknesses)
180
  dx = [t / nodes_per_layer for t in thicknesses] # Node spacing per layer
 
187
 
188
  # Stability check: Fourier number
189
  for i, (a, d) in enumerate(zip(alpha, dx)):
190
+ if a == 0 or d == 0:
191
+ logger.warning(f"Invalid thermal diffusivity or node spacing for layer {i} in construction '{construction_name}'. Skipping stability adjustment.")
192
+ continue
193
  Fo = a * dt / (d ** 2)
194
  if Fo < 0.33:
195
+ logger.warning(f"Fourier number {Fo:.3f} < 0.33 for layer {i} ({material_props[i]['name']}). Adjusting node spacing.")
196
  dx[i] = np.sqrt(a * dt / 0.33)
197
  nodes_per_layer = max(2, int(np.ceil(thicknesses[i] / dx[i])))
198
  dx[i] = thicknesses[i] / nodes_per_layer
 
210
  c_i = c[layer_idx]
211
  dx_i = dx[layer_idx]
212
 
213
+ if k_i == 0 or rho_i == 0 or c_i == 0 or dx_i == 0:
214
+ logger.warning(f"Invalid material properties for layer {layer_idx} ({material_props[layer_idx]['name']}) in construction '{construction_name}'. Using default values.")
215
+ continue
216
+
217
  if node_j == 0 and layer_idx == 0: # Outdoor surface node
218
  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)
219
  A[idx, idx + 1] = -2 * dt * k_i / (dx_i * rho_i * c_i * dx_i)
 
227
  dx_next = dx[layer_idx + 1]
228
  rho_next = rho[layer_idx + 1]
229
  c_next = c[layer_idx + 1]
230
+ if k_next == 0 or dx_next == 0 or rho_next == 0 or c_next == 0:
231
+ logger.warning(f"Invalid material properties for layer {layer_idx + 1} ({material_props[layer_idx + 1]['name']}) in construction '{construction_name}'. Skipping interface.")
232
+ continue
233
  A[idx, idx] = 1.0 + dt * (k_i / dx_i + k_next / dx_next) / (0.5 * (rho_i * c_i * dx_i + rho_next * c_next * dx_next))
234
  A[idx, idx - 1] = -dt * k_i / (dx_i * 0.5 * (rho_i * c_i * dx_i + rho_next * c_next * dx_next))
235
  A[idx, idx + 1] = -dt * k_next / (dx_next * 0.5 * (rho_i * c_i * dx_i + rho_next * c_next * dx_next))
 
238
  dx_prev = dx[layer_idx - 1]
239
  rho_prev = rho[layer_idx - 1]
240
  c_prev = c[layer_idx - 1]
241
+ if k_prev == 0 or dx_prev == 0 or rho_prev == 0 or c_prev == 0:
242
+ logger.warning(f"Invalid material properties for layer {layer_idx - 1} ({material_props[layer_idx - 1]['name']}) in construction '{construction_name}'. Skipping interface.")
243
+ continue
244
  A[idx, idx] = 1.0 + dt * (k_prev / dx_prev + k_i / dx_i) / (0.5 * (rho_prev * c_prev * dx_prev + rho_i * c_i * dx_i))
245
  A[idx, idx - 1] = -dt * k_prev / (dx_prev * 0.5 * (rho_prev * c_prev * dx_prev + rho_i * c_i * dx_i))
246
  A[idx, idx + 1] = -dt * k_i / (dx_i * 0.5 * (rho_prev * c_prev * dx_prev + rho_i * c_i * dx_i))
 
263
  for t in range(num_ctf):
264
  b_out = b.copy()
265
  if t == 0:
266
+ 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
267
  T = sparse_linalg.spsolve(A, b_out + T_prev)
268
  q_in = (T[-1] - 0.0) / R_in # Indoor heat flux (W/m²)
269
  Y[t] = q_in
 
276
  for t in range(num_ctf):
277
  b_in = b.copy()
278
  if t == 0:
279
+ b_in[-1] = dt / (rho[-1] * c[-1] * dx[-1] * R_in) if rho[-1] * c[-1] * dx[-1] != 0 else 0.0 # Unit indoor temp impulse
280
  T = sparse_linalg.spsolve(A, b_in + T_prev)
281
  q_in = (T[-1] - 0.0) / R_in
282
  Z[t] = q_in
 
287
  for t in range(num_ctf):
288
  b_flux = np.zeros(total_nodes)
289
  if t == 0:
290
+ b_flux[-1] = -1.0 / (rho[-1] * c[-1] * dx[-1]) if rho[-1] * c[-1] * dx[-1] != 0 else 0.0 # Unit flux impulse
291
  T = sparse_linalg.spsolve(A, b_flux + T_prev)
292
  q_in = (T[-1] - 0.0) / R_in
293
  F[t] = q_in
 
296
  ctf = CTFCoefficients(X=X, Y=Y, Z=Z, F=F)
297
  with cls._cache_lock:
298
  cls._ctf_cache[construction_hash] = ctf
299
+ logger.info(f"Calculated CTF coefficients for construction '{construction_name}' in component '{component.get('name', 'Unknown')}'")
300
  return ctf
301
 
302
  @classmethod
303
+ def calculate_ctf_tables(cls, component: Dict[str, Any]) -> CTFCoefficients:
304
+ """Placeholder for future implementation of CTF table lookups for windows and skylights.
305
 
306
  Args:
307
+ component: Dictionary containing component properties.
308
 
309
  Returns:
310
  CTFCoefficients: Placeholder zero coefficients until implementation.
311
  """
312
+ logger.info(f"CTF table calculation for {component.get('type', 'Unknown')} component '{component.get('name', 'Unknown')}' not yet implemented. Returning zero coefficients.")
313
  return CTFCoefficients(X=[0.0], Y=[0.0], Z=[0.0], F=[0.0])