Spaces:
Sleeping
Sleeping
Update utils/ctf_calculations.py
Browse files- 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 #
|
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 |
-
#
|
275 |
-
|
276 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
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}
|
292 |
-
dx[i] = np.sqrt(a * dt / 0.
|
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
|