mabuseif commited on
Commit
a186076
·
verified ·
1 Parent(s): 7e62d7c

Update utils/ctf_calculations.py

Browse files
Files changed (1) hide show
  1. utils/ctf_calculations.py +34 -27
utils/ctf_calculations.py CHANGED
@@ -165,14 +165,14 @@ class CTFCalculator:
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.
178
  """
@@ -189,31 +189,31 @@ class CTFCalculator:
189
  if not component_type:
190
  logger.warning(f"Invalid component type '{comp_type_str}' for component '{component.get('name', 'Unknown')}'. Returning zero CTFs.")
191
  return CTFCoefficients(X=[0.0], Y=[0.0], Z=[0.0], F=[0.0])
192
-
193
  # Skip CTF for WINDOW, SKYLIGHT as per ASHRAE; return zero coefficients
194
  if component_type in [ComponentType.WINDOW, ComponentType.SKYLIGHT]:
195
  logger.info(f"Skipping CTF calculation for {component_type.value} component '{component.get('name', 'Unknown')}'. Using zero coefficients until CTF tables are implemented.")
196
  return CTFCoefficients(X=[0.0], Y=[0.0], Z=[0.0], F=[0.0])
197
-
198
  # Retrieve construction
199
  construction_name = component.get('construction', '')
200
  if not construction_name:
201
  logger.warning(f"No construction specified for component '{component.get('name', 'Unknown')}' ({component_type.value}). Returning zero CTFs.")
202
  return CTFCoefficients(X=[0.0], Y=[0.0], Z=[0.0], F=[0.0])
203
-
204
  constructions = st.session_state.project_data.get('constructions', {})
205
  construction = constructions.get('library', {}).get(construction_name, constructions.get('project', {}).get(construction_name))
206
  if not construction or not construction.get('layers'):
207
  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.")
208
  return CTFCoefficients(X=[0.0], Y=[0.0], Z=[0.0], F=[0.0])
209
-
210
  # Check cache with thread-safe access
211
  construction_hash = cls._hash_construction(construction)
212
  with cls._cache_lock:
213
  if construction_hash in cls._ctf_cache:
214
  logger.info(f"Using cached CTF coefficients for construction '{construction_name}'")
215
  return cls._ctf_cache[construction_hash]
216
-
217
  # Collect layer properties
218
  thicknesses = []
219
  material_props = []
@@ -229,11 +229,11 @@ class CTFCalculator:
229
  continue
230
  thicknesses.append(thickness)
231
  material_props.append(material)
232
-
233
  if not thicknesses or not material_props:
234
  logger.warning(f"No valid layers with material properties for construction '{construction_name}' in component '{component.get('name', 'Unknown')}'. Returning zero CTFs.")
235
  return CTFCoefficients(X=[0.0], Y=[0.0], Z=[0.0], F=[0.0])
236
-
237
  # Extract material properties
238
  k = [m['conductivity'] for m in material_props] # W/m·K
239
  rho = [m['density'] for m in material_props] # kg/m³
@@ -241,26 +241,26 @@ class CTFCalculator:
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
@@ -270,7 +270,7 @@ class CTFCalculator:
270
  for j in range(nodes_per_layer):
271
  node_positions.append((i, j, node_idx)) # (layer_idx, node_in_layer, global_node_idx)
272
  node_idx += 1
273
-
274
  # Stability check: Fourier number
275
  for i, (a, d) in enumerate(zip(alpha, dx)):
276
  if a == 0 or d == 0:
@@ -284,22 +284,22 @@ class CTFCalculator:
284
  dx[i] = thicknesses[i] / nodes_per_layer
285
  Fo = a * dt / (dx[i] ** 2)
286
  logger.info(f"Adjusted node spacing for layer {i}: dx={dx[i]:.4f} m, Fo={Fo:.3f}")
287
-
288
  # Build system matrices
289
  A = sparse.lil_matrix((total_nodes, total_nodes))
290
  b = np.zeros(total_nodes)
291
  node_to_layer = [i for i, _, _ in node_positions]
292
-
293
  for idx, (layer_idx, node_j, global_idx) in enumerate(node_positions):
294
  k_i = k[layer_idx]
295
  rho_i = rho[layer_idx]
296
  c_i = c[layer_idx]
297
  dx_i = dx[layer_idx]
298
-
299
  if k_i == 0 or rho_i == 0 or c_i == 0 or dx_i == 0:
300
  logger.warning(f"Invalid material properties for layer {layer_idx} ({material_props[layer_idx]['name']}) in construction '{construction_name}'. Using default values.")
301
  continue
302
-
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)
@@ -308,6 +308,13 @@ class CTFCalculator:
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)
310
  b[idx] = dt / (rho_i * c_i * dx_i * R_in) # Indoor temp contribution
 
 
 
 
 
 
 
311
  elif node_j == nodes_per_layer - 1 and layer_idx < len(thicknesses) - 1: # Interface between layers
312
  k_next = k[layer_idx + 1]
313
  dx_next = dx[layer_idx + 1]
@@ -334,9 +341,9 @@ class CTFCalculator:
334
  A[idx, idx] = 1.0 + 2 * dt * k_i / (dx_i * rho_i * c_i * dx_i)
335
  A[idx, idx - 1] = -dt * k_i / (dx_i * rho_i * c_i * dx_i)
336
  A[idx, idx + 1] = -dt * k_i / (dx_i * rho_i * c_i * dx_i)
337
-
338
  A = A.tocsr() # Convert to CSR for efficient solving
339
-
340
  # Calculate CTF coefficients (X, Y, Z, F)
341
  num_ctf = 12 # Standard number of coefficients
342
  X = [0.0] * num_ctf # Exterior temp response
@@ -344,7 +351,7 @@ class CTFCalculator:
344
  Z = [0.0] * num_ctf # Interior temp response
345
  F = [0.0] * num_ctf # Flux history
346
  T_prev = np.zeros(total_nodes) # Previous temperatures
347
-
348
  # Impulse response for exterior temperature (X, Y)
349
  for t in range(num_ctf):
350
  b_out = b.copy()
@@ -356,7 +363,7 @@ class CTFCalculator:
356
  q_out = (0.0 - T[0]) / R_out # Outdoor heat flux
357
  X[t] = q_out
358
  T_prev = T.copy()
359
-
360
  # Reset for interior temperature (Z)
361
  T_prev = np.zeros(total_nodes)
362
  for t in range(num_ctf):
@@ -367,7 +374,7 @@ class CTFCalculator:
367
  q_in = (T[-1] - 0.0) / R_in
368
  Z[t] = q_in
369
  T_prev = T.copy()
370
-
371
  # Flux history coefficients (F)
372
  T_prev = np.zeros(total_nodes)
373
  for t in range(num_ctf):
@@ -378,7 +385,7 @@ class CTFCalculator:
378
  q_in = (T[-1] - 0.0) / R_in
379
  F[t] = q_in
380
  T_prev = T.copy()
381
-
382
  ctf = CTFCoefficients(X=X, Y=Y, Z=Z, F=F)
383
  with cls._cache_lock:
384
  cls._ctf_cache[construction_hash] = ctf
 
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.
178
  """
 
189
  if not component_type:
190
  logger.warning(f"Invalid component type '{comp_type_str}' for component '{component.get('name', 'Unknown')}'. Returning zero CTFs.")
191
  return CTFCoefficients(X=[0.0], Y=[0.0], Z=[0.0], F=[0.0])
192
+
193
  # Skip CTF for WINDOW, SKYLIGHT as per ASHRAE; return zero coefficients
194
  if component_type in [ComponentType.WINDOW, ComponentType.SKYLIGHT]:
195
  logger.info(f"Skipping CTF calculation for {component_type.value} component '{component.get('name', 'Unknown')}'. Using zero coefficients until CTF tables are implemented.")
196
  return CTFCoefficients(X=[0.0], Y=[0.0], Z=[0.0], F=[0.0])
197
+
198
  # Retrieve construction
199
  construction_name = component.get('construction', '')
200
  if not construction_name:
201
  logger.warning(f"No construction specified for component '{component.get('name', 'Unknown')}' ({component_type.value}). Returning zero CTFs.")
202
  return CTFCoefficients(X=[0.0], Y=[0.0], Z=[0.0], F=[0.0])
203
+
204
  constructions = st.session_state.project_data.get('constructions', {})
205
  construction = constructions.get('library', {}).get(construction_name, constructions.get('project', {}).get(construction_name))
206
  if not construction or not construction.get('layers'):
207
  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.")
208
  return CTFCoefficients(X=[0.0], Y=[0.0], Z=[0.0], F=[0.0])
209
+
210
  # Check cache with thread-safe access
211
  construction_hash = cls._hash_construction(construction)
212
  with cls._cache_lock:
213
  if construction_hash in cls._ctf_cache:
214
  logger.info(f"Using cached CTF coefficients for construction '{construction_name}'")
215
  return cls._ctf_cache[construction_hash]
216
+
217
  # Collect layer properties
218
  thicknesses = []
219
  material_props = []
 
229
  continue
230
  thicknesses.append(thickness)
231
  material_props.append(material)
232
+
233
  if not thicknesses or not material_props:
234
  logger.warning(f"No valid layers with material properties for construction '{construction_name}' in component '{component.get('name', 'Unknown')}'. Returning zero CTFs.")
235
  return CTFCoefficients(X=[0.0], Y=[0.0], Z=[0.0], F=[0.0])
236
+
237
  # Extract material properties
238
  k = [m['conductivity'] for m in material_props] # W/m·K
239
  rho = [m['density'] for m in material_props] # kg/m³
 
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.0
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
 
270
  for j in range(nodes_per_layer):
271
  node_positions.append((i, j, node_idx)) # (layer_idx, node_in_layer, global_node_idx)
272
  node_idx += 1
273
+
274
  # Stability check: Fourier number
275
  for i, (a, d) in enumerate(zip(alpha, dx)):
276
  if a == 0 or d == 0:
 
284
  dx[i] = thicknesses[i] / nodes_per_layer
285
  Fo = a * dt / (dx[i] ** 2)
286
  logger.info(f"Adjusted node spacing for layer {i}: dx={dx[i]:.4f} m, Fo={Fo:.3f}")
287
+
288
  # Build system matrices
289
  A = sparse.lil_matrix((total_nodes, total_nodes))
290
  b = np.zeros(total_nodes)
291
  node_to_layer = [i for i, _, _ in node_positions]
292
+
293
  for idx, (layer_idx, node_j, global_idx) in enumerate(node_positions):
294
  k_i = k[layer_idx]
295
  rho_i = rho[layer_idx]
296
  c_i = c[layer_idx]
297
  dx_i = dx[layer_idx]
298
+
299
  if k_i == 0 or rho_i == 0 or c_i == 0 or dx_i == 0:
300
  logger.warning(f"Invalid material properties for layer {layer_idx} ({material_props[layer_idx]['name']}) in construction '{construction_name}'. Using default values.")
301
  continue
302
+
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)
 
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)
310
  b[idx] = dt / (rho_i * c_i * dx_i * R_in) # Indoor temp contribution
311
+ # Add radiant load to indoor surface node (convert kW to W)
312
+ radiant_load = component.get("radiant_load", 0.0) * 1000 # kW to W
313
+ if radiant_load != 0 and rho_i * c_i * dx_i != 0:
314
+ b[idx] += dt / (rho_i * c_i * dx_i) * radiant_load
315
+ logger.debug(f"Added radiant load {radiant_load:.2f} W to indoor node for component '{component.get('name', 'Unknown')}'")
316
+ elif radiant_load != 0:
317
+ logger.warning(f"Invalid material properties (rho={rho_i}, c={c_i}, dx={dx_i}) for radiant load in component '{component.get('name', 'Unknown')}'. Skipping.")
318
  elif node_j == nodes_per_layer - 1 and layer_idx < len(thicknesses) - 1: # Interface between layers
319
  k_next = k[layer_idx + 1]
320
  dx_next = dx[layer_idx + 1]
 
341
  A[idx, idx] = 1.0 + 2 * dt * k_i / (dx_i * rho_i * c_i * dx_i)
342
  A[idx, idx - 1] = -dt * k_i / (dx_i * rho_i * c_i * dx_i)
343
  A[idx, idx + 1] = -dt * k_i / (dx_i * rho_i * c_i * dx_i)
344
+
345
  A = A.tocsr() # Convert to CSR for efficient solving
346
+
347
  # Calculate CTF coefficients (X, Y, Z, F)
348
  num_ctf = 12 # Standard number of coefficients
349
  X = [0.0] * num_ctf # Exterior temp response
 
351
  Z = [0.0] * num_ctf # Interior temp response
352
  F = [0.0] * num_ctf # Flux history
353
  T_prev = np.zeros(total_nodes) # Previous temperatures
354
+
355
  # Impulse response for exterior temperature (X, Y)
356
  for t in range(num_ctf):
357
  b_out = b.copy()
 
363
  q_out = (0.0 - T[0]) / R_out # Outdoor heat flux
364
  X[t] = q_out
365
  T_prev = T.copy()
366
+
367
  # Reset for interior temperature (Z)
368
  T_prev = np.zeros(total_nodes)
369
  for t in range(num_ctf):
 
374
  q_in = (T[-1] - 0.0) / R_in
375
  Z[t] = q_in
376
  T_prev = T.copy()
377
+
378
  # Flux history coefficients (F)
379
  T_prev = np.zeros(total_nodes)
380
  for t in range(num_ctf):
 
385
  q_in = (T[-1] - 0.0) / R_in
386
  F[t] = q_in
387
  T_prev = T.copy()
388
+
389
  ctf = CTFCoefficients(X=X, Y=Y, Z=Z, F=F)
390
  with cls._cache_lock:
391
  cls._ctf_cache[construction_hash] = ctf