mabuseif commited on
Commit
6a27fd5
·
verified ·
1 Parent(s): cd408c4

Update utils/ctf_calculations.py

Browse files
Files changed (1) hide show
  1. utils/ctf_calculations.py +110 -18
utils/ctf_calculations.py CHANGED
@@ -113,16 +113,17 @@ class CTFCalculator:
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.
118
 
119
  Args:
120
  construction: Dictionary containing construction properties (name, layers, adiabatic).
 
121
 
122
  Returns:
123
- str: SHA-256 hash of the construction properties.
124
  """
125
- hash_input = f"{construction.get('name', '')}{construction.get('adiabatic', False)}"
126
  layers = construction.get('layers', [])
127
  for layer in layers:
128
  material_name = layer.get('material', '')
@@ -217,13 +218,6 @@ class CTFCalculator:
217
  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.")
218
  return CTFCoefficients(X=[0.0], Y=[0.0], Z=[0.0], F=[0.0])
219
 
220
- # Check cache with thread-safe access
221
- construction_hash = cls._hash_construction(construction)
222
- with cls._cache_lock:
223
- if construction_hash in cls._ctf_cache:
224
- logger.info(f"Using cached CTF coefficients for construction '{construction_name}'")
225
- return cls._ctf_cache[construction_hash]
226
-
227
  # Collect layer properties
228
  thicknesses = []
229
  material_props = []
@@ -254,7 +248,7 @@ class CTFCalculator:
254
 
255
  # Discretization parameters
256
  dt = 3600 # 1-hour time step (s)
257
- nodes_per_layer = 3 # 2–4 nodes per layer for balance
258
  R_in = 0.12 # Indoor surface resistance (m²·K/W, ASHRAE)
259
 
260
  # Get weather data for sol-air temperature
@@ -271,9 +265,15 @@ class CTFCalculator:
271
 
272
  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')}'")
273
 
274
- # Calculate node spacing and check stability
275
- total_nodes = sum(nodes_per_layer for _ in thicknesses)
276
- dx = [t / nodes_per_layer for t in thicknesses] # Node spacing per layer
 
 
 
 
 
 
277
  node_positions = []
278
  node_idx = 0
279
  for i, t in enumerate(thicknesses):
@@ -287,15 +287,23 @@ class CTFCalculator:
287
  logger.warning(f"Invalid thermal diffusivity or node spacing for layer {i} in construction '{construction_name}'. Skipping stability adjustment.")
288
  continue
289
  Fo = a * dt / (d ** 2)
290
- if Fo < 0.33:
291
- logger.warning(f"Fourier number {Fo:.3f} < 0.33 for layer {i} ({material_props[i]['name']}). Adjusting node spacing.")
292
- dx[i] = np.sqrt(a * dt / 0.33)
293
  nodes_per_layer = max(2, int(np.ceil(thicknesses[i] / dx[i])))
294
  dx[i] = thicknesses[i] / nodes_per_layer
295
  Fo = a * dt / (dx[i] ** 2)
296
  logger.info(f"Adjusted node spacing for layer {i}: dx={dx[i]:.4f} m, Fo={Fo:.3f}")
297
 
 
 
 
 
 
 
 
298
  # Build system matrices
 
299
  A = sparse.lil_matrix((total_nodes, total_nodes))
300
  b = np.zeros(total_nodes)
301
  node_to_layer = [i for i, _, _ in node_positions]
@@ -354,6 +362,90 @@ class CTFCalculator:
354
 
355
  A = A.tocsr() # Convert to CSR for efficient solving
356
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
357
  # Calculate CTF coefficients (X, Y, Z, F)
358
  num_ctf = 12 # Standard number of coefficients
359
  X = [0.0] * num_ctf # Exterior temp response
 
113
  return T_sol_air
114
 
115
  @staticmethod
116
+ def _hash_construction(construction: Dict[str, Any], nodes_per_layer: int) -> str:
117
+ """Generate a unique hash for a construction based on its properties and node discretization.
118
 
119
  Args:
120
  construction: Dictionary containing construction properties (name, layers, adiabatic).
121
+ nodes_per_layer: Number of nodes per layer used in discretization.
122
 
123
  Returns:
124
+ str: SHA-256 hash of the construction properties and nodes_per_layer.
125
  """
126
+ hash_input = f"{construction.get('name', '')}{construction.get('adiabatic', False)}{nodes_per_layer}"
127
  layers = construction.get('layers', [])
128
  for layer in layers:
129
  material_name = layer.get('material', '')
 
218
  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.")
219
  return CTFCoefficients(X=[0.0], Y=[0.0], Z=[0.0], F=[0.0])
220
 
 
 
 
 
 
 
 
221
  # Collect layer properties
222
  thicknesses = []
223
  material_props = []
 
248
 
249
  # Discretization parameters
250
  dt = 3600 # 1-hour time step (s)
251
+ nodes_per_layer = 3 # Initial number of nodes per layer
252
  R_in = 0.12 # Indoor surface resistance (m²·K/W, ASHRAE)
253
 
254
  # Get weather data for sol-air temperature
 
265
 
266
  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')}'")
267
 
268
+ # Check cache with thread-safe access
269
+ construction_hash = cls._hash_construction(construction, nodes_per_layer)
270
+ with cls._cache_lock:
271
+ if construction_hash in cls._ctf_cache:
272
+ logger.info(f"Using cached CTF coefficients for construction '{construction_name}'")
273
+ return cls._ctf_cache[construction_hash]
274
+
275
+ # Calculate node spacing
276
+ dx = [t / nodes_per_layer for t in thicknesses] # Initial node spacing per layer
277
  node_positions = []
278
  node_idx = 0
279
  for i, t in enumerate(thicknesses):
 
287
  logger.warning(f"Invalid thermal diffusivity or node spacing for layer {i} in construction '{construction_name}'. Skipping stability adjustment.")
288
  continue
289
  Fo = a * dt / (d ** 2)
290
+ if Fo > 0.5 or Fo < 0.33:
291
+ logger.warning(f"Fourier number {Fo:.3f} out of stable range (0.33 ≤ Fo ≤ 0.5) for layer {i} ({material_props[i]['name']}). Adjusting dx.")
292
+ dx[i] = np.sqrt(a * dt / 0.4) # Target Fo = 0.4
293
  nodes_per_layer = max(2, int(np.ceil(thicknesses[i] / dx[i])))
294
  dx[i] = thicknesses[i] / nodes_per_layer
295
  Fo = a * dt / (dx[i] ** 2)
296
  logger.info(f"Adjusted node spacing for layer {i}: dx={dx[i]:.4f} m, Fo={Fo:.3f}")
297
 
298
+ # Update construction hash if nodes_per_layer changed
299
+ construction_hash = cls._hash_construction(construction, nodes_per_layer)
300
+ with cls._cache_lock:
301
+ if construction_hash in cls._ctf_cache:
302
+ logger.info(f"Using cached CTF coefficients for construction '{construction_name}' after node adjustment")
303
+ return cls._ctf_cache[construction_hash]
304
+
305
  # Build system matrices
306
+ total_nodes = sum(nodes_per_layer for _ in thicknesses)
307
  A = sparse.lil_matrix((total_nodes, total_nodes))
308
  b = np.zeros(total_nodes)
309
  node_to_layer = [i for i, _, _ in node_positions]
 
362
 
363
  A = A.tocsr() # Convert to CSR for efficient solving
364
 
365
+ # Check matrix conditioning
366
+ try:
367
+ from scipy.sparse.linalg import norm
368
+ A_csc = A.tocsc()
369
+ cond_number = norm(A, ord=2) * norm(sparse_linalg.inv(A_csc), ord=2)
370
+ if cond_number > 1e6:
371
+ logger.warning(f"Matrix A is ill-conditioned (condition number: {cond_number:.2e}). Adjusting node spacing.")
372
+ nodes_per_layer = min(nodes_per_layer + 1, 6)
373
+ dx = [t / nodes_per_layer for t in thicknesses]
374
+
375
+ # Recalculate node positions
376
+ node_positions = []
377
+ node_idx = 0
378
+ for i, t in enumerate(thicknesses):
379
+ for j in range(nodes_per_layer):
380
+ node_positions.append((i, j, node_idx))
381
+ node_idx += 1
382
+
383
+ # Rebuild system matrices
384
+ total_nodes = sum(nodes_per_layer for _ in thicknesses)
385
+ A = sparse.lil_matrix((total_nodes, total_nodes))
386
+ b = np.zeros(total_nodes)
387
+ node_to_layer = [i for i, _, _ in node_positions]
388
+
389
+ for idx, (layer_idx, node_j, global_idx) in enumerate(node_positions):
390
+ k_i = k[layer_idx]
391
+ rho_i = rho[layer_idx]
392
+ c_i = c[layer_idx]
393
+ dx_i = dx[layer_idx]
394
+
395
+ if k_i == 0 or rho_i == 0 or c_i == 0 or dx_i == 0:
396
+ logger.warning(f"Invalid material properties for layer {layer_idx} ({material_props[layer_idx]['name']}) in construction '{construction_name}'. Using default values.")
397
+ continue
398
+
399
+ if node_j == 0 and layer_idx == 0:
400
+ 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)
401
+ A[idx, idx + 1] = -2 * dt * k_i / (dx_i * rho_i * c_i * dx_i)
402
+ b[idx] = dt / (rho_i * c_i * dx_i * R_out) * T_sol_air
403
+ elif node_j == nodes_per_layer - 1 and layer_idx == len(thicknesses) - 1:
404
+ 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)
405
+ A[idx, idx - 1] = -2 * dt * k_i / (dx_i * rho_i * c_i * dx_i)
406
+ b[idx] = dt / (rho_i * c_i * dx_i * R_in)
407
+ radiant_load = component.get("radiant_load", 0.0) * 1000
408
+ if radiant_load != 0 and rho_i * c_i * dx_i != 0:
409
+ b[idx] += dt / (rho_i * c_i * dx_i) * radiant_load
410
+ logger.debug(f"Added radiant load {radiant_load:.2f} W to indoor node for component '{component.get('name', 'Unknown')}'")
411
+ elif radiant_load != 0:
412
+ 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.")
413
+ elif node_j == nodes_per_layer - 1 and layer_idx < len(thicknesses) - 1:
414
+ k_next = k[layer_idx + 1]
415
+ dx_next = dx[layer_idx + 1]
416
+ rho_next = rho[layer_idx + 1]
417
+ c_next = c[layer_idx + 1]
418
+ if k_next == 0 or dx_next == 0 or rho_next == 0 or c_next == 0:
419
+ logger.warning(f"Invalid material properties for layer {layer_idx + 1} ({material_props[layer_idx + 1]['name']}) in construction '{construction_name}'. Skipping interface.")
420
+ continue
421
+ 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))
422
+ A[idx, idx - 1] = -dt * k_i / (dx_i * 0.5 * (rho_i * c_i * dx_i + rho_next * c_next * dx_next))
423
+ A[idx, idx + 1] = -dt * k_next / (dx_next * 0.5 * (rho_i * c_i * dx_i + rho_next * c_next * dx_next))
424
+ elif node_j == 0 and layer_idx > 0:
425
+ k_prev = k[layer_idx - 1]
426
+ dx_prev = dx[layer_idx - 1]
427
+ rho_prev = rho[layer_idx - 1]
428
+ c_prev = c[layer_idx - 1]
429
+ if k_prev == 0 or dx_prev == 0 or rho_prev == 0 or c_prev == 0:
430
+ logger.warning(f"Invalid material properties for layer {layer_idx - 1} ({material_props[layer_idx - 1]['name']}) in construction '{construction_name}'. Skipping interface.")
431
+ continue
432
+ 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))
433
+ A[idx, idx - 1] = -dt * k_prev / (dx_prev * 0.5 * (rho_prev * c_prev * dx_prev + rho_i * c_i * dx_i))
434
+ A[idx, idx + 1] = -dt * k_i / (dx_i * 0.5 * (rho_prev * c_prev * dx_prev + rho_i * c_i * dx_i))
435
+ else:
436
+ A[idx, idx] = 1.0 + 2 * dt * k_i / (dx_i * rho_i * c_i * dx_i)
437
+ A[idx, idx - 1] = -dt * k_i / (dx_i * rho_i * c_i * dx_i)
438
+ A[idx, idx + 1] = -dt * k_i / (dx_i * rho_i * c_i * dx_i)
439
+
440
+ A = A.tocsr()
441
+ construction_hash = cls._hash_construction(construction, nodes_per_layer)
442
+ with cls._cache_lock:
443
+ if construction_hash in cls._ctf_cache:
444
+ logger.info(f"Using cached CTF coefficients for construction '{construction_name}' after matrix conditioning adjustment")
445
+ return cls._ctf_cache[construction_hash]
446
+ except Exception as e:
447
+ logger.warning(f"Failed to compute matrix condition number: {str(e)}. Proceeding with current discretization.")
448
+
449
  # Calculate CTF coefficients (X, Y, Z, F)
450
  num_ctf = 12 # Standard number of coefficients
451
  X = [0.0] * num_ctf # Exterior temp response