Spaces:
Sleeping
Sleeping
Update utils/ctf_calculations.py
Browse files- utils/ctf_calculations.py +45 -161
utils/ctf_calculations.py
CHANGED
@@ -113,17 +113,16 @@ class CTFCalculator:
|
|
113 |
return T_sol_air
|
114 |
|
115 |
@staticmethod
|
116 |
-
def _hash_construction(construction: Dict[str, Any]
|
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 |
-
nodes_per_layer: List of number of nodes per layer used in discretization.
|
122 |
|
123 |
Returns:
|
124 |
-
str: SHA-256 hash of the construction properties
|
125 |
"""
|
126 |
-
hash_input = f"{construction.get('name', '')}{construction.get('adiabatic', False)}
|
127 |
layers = construction.get('layers', [])
|
128 |
for layer in layers:
|
129 |
material_name = layer.get('material', '')
|
@@ -133,14 +132,14 @@ class CTFCalculator:
|
|
133 |
|
134 |
@classmethod
|
135 |
def _get_material_properties(cls, material_name: str) -> Dict[str, float]:
|
136 |
-
"""Retrieve material properties from session state
|
137 |
|
138 |
Args:
|
139 |
material_name: Name of the material.
|
140 |
|
141 |
Returns:
|
142 |
Dict containing conductivity, density, specific_heat, absorptivity, emissivity.
|
143 |
-
Returns empty dict if material not found
|
144 |
"""
|
145 |
try:
|
146 |
materials = st.session_state.project_data.get('materials', {})
|
@@ -151,26 +150,11 @@ class CTFCalculator:
|
|
151 |
|
152 |
# Extract required properties
|
153 |
thermal_props = material.get('thermal_properties', {})
|
154 |
-
conductivity = thermal_props.get('conductivity', 0.0)
|
155 |
-
density = thermal_props.get('density', 0.0)
|
156 |
-
specific_heat = thermal_props.get('specific_heat', 0.0)
|
157 |
-
|
158 |
-
# Validate properties
|
159 |
-
if conductivity <= 0 or conductivity > 1000: # Reasonable bounds for conductivity (W/m·K)
|
160 |
-
logger.error(f"Invalid conductivity {conductivity} for material '{material_name}'. Must be between 0 and 1000 W/m·K.")
|
161 |
-
return {}
|
162 |
-
if density <= 0 or density > 10000: # Reasonable bounds for density (kg/m³)
|
163 |
-
logger.error(f"Invalid density {density} for material '{material_name}'. Must be between 0 and 10000 kg/m³.")
|
164 |
-
return {}
|
165 |
-
if specific_heat <= 0 or specific_heat > 5000: # Reasonable bounds for specific heat (J/kg·K)
|
166 |
-
logger.error(f"Invalid specific heat {specific_heat} for material '{material_name}'. Must be between 0 and 5000 J/kg·K.")
|
167 |
-
return {}
|
168 |
-
|
169 |
return {
|
170 |
'name': material_name,
|
171 |
-
'conductivity': conductivity,
|
172 |
-
'density': density,
|
173 |
-
'specific_heat': specific_heat,
|
174 |
'absorptivity': material.get('absorptivity', 0.6),
|
175 |
'emissivity': material.get('emissivity', 0.9)
|
176 |
}
|
@@ -233,6 +217,13 @@ class CTFCalculator:
|
|
233 |
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.")
|
234 |
return CTFCoefficients(X=[0.0], Y=[0.0], Z=[0.0], F=[0.0])
|
235 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
236 |
# Collect layer properties
|
237 |
thicknesses = []
|
238 |
material_props = []
|
@@ -244,7 +235,7 @@ class CTFCalculator:
|
|
244 |
continue
|
245 |
material = cls._get_material_properties(material_name)
|
246 |
if not material:
|
247 |
-
logger.warning(f"Skipping layer with material '{material_name}' in construction '{construction_name}' due to missing
|
248 |
continue
|
249 |
thicknesses.append(thickness)
|
250 |
material_props.append(material)
|
@@ -263,7 +254,7 @@ class CTFCalculator:
|
|
263 |
|
264 |
# Discretization parameters
|
265 |
dt = 3600 # 1-hour time step (s)
|
266 |
-
nodes_per_layer =
|
267 |
R_in = 0.12 # Indoor surface resistance (m²·K/W, ASHRAE)
|
268 |
|
269 |
# Get weather data for sol-air temperature
|
@@ -280,52 +271,31 @@ class CTFCalculator:
|
|
280 |
|
281 |
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')}'")
|
282 |
|
283 |
-
#
|
284 |
-
|
285 |
-
|
286 |
-
|
287 |
-
|
288 |
-
|
|
|
|
|
|
|
289 |
|
290 |
-
# Calculate node spacing
|
291 |
-
dx = [t / n for t, n in zip(thicknesses, nodes_per_layer)] # Initial node spacing per layer
|
292 |
-
|
293 |
# Stability check: Fourier number
|
294 |
-
for i, (a, d
|
295 |
if a == 0 or d == 0:
|
296 |
logger.warning(f"Invalid thermal diffusivity or node spacing for layer {i} in construction '{construction_name}'. Skipping stability adjustment.")
|
297 |
continue
|
298 |
Fo = a * dt / (d ** 2)
|
299 |
-
if Fo
|
300 |
-
logger.warning(f"Fourier number {Fo:.3f}
|
301 |
-
|
302 |
-
|
303 |
-
|
304 |
-
|
305 |
-
|
306 |
-
dx[i] = np.sqrt(a * dt / 0.4) # Target Fo = 0.4
|
307 |
-
nodes_per_layer[i] = max(2, int(np.ceil(t / dx[i])))
|
308 |
-
dx[i] = t / nodes_per_layer[i]
|
309 |
-
Fo = a * dt / (dx[i] ** 2) if dx[i] != 0 else 0.0
|
310 |
-
logger.info(f"Adjusted node spacing for layer {i}: dx={dx[i]:.4f} m, Fo={Fo:.3f}, nodes={nodes_per_layer[i]}")
|
311 |
-
|
312 |
-
# Update construction hash if nodes_per_layer changed
|
313 |
-
construction_hash = cls._hash_construction(construction, nodes_per_layer)
|
314 |
-
with cls._cache_lock:
|
315 |
-
if construction_hash in cls._ctf_cache:
|
316 |
-
logger.info(f"Using cached CTF coefficients for construction '{construction_name}' after node adjustment")
|
317 |
-
return cls._ctf_cache[construction_hash]
|
318 |
-
|
319 |
-
# Build node positions
|
320 |
-
node_positions = []
|
321 |
-
node_idx = 0
|
322 |
-
for i, n in enumerate(nodes_per_layer):
|
323 |
-
for j in range(n):
|
324 |
-
node_positions.append((i, j, node_idx)) # (layer_idx, node_in_layer, global_node_idx)
|
325 |
-
node_idx += 1
|
326 |
|
327 |
# Build system matrices
|
328 |
-
total_nodes = sum(nodes_per_layer)
|
329 |
A = sparse.lil_matrix((total_nodes, total_nodes))
|
330 |
b = np.zeros(total_nodes)
|
331 |
node_to_layer = [i for i, _, _ in node_positions]
|
@@ -335,7 +305,6 @@ class CTFCalculator:
|
|
335 |
rho_i = rho[layer_idx]
|
336 |
c_i = c[layer_idx]
|
337 |
dx_i = dx[layer_idx]
|
338 |
-
npl_i = nodes_per_layer[layer_idx]
|
339 |
|
340 |
if k_i == 0 or rho_i == 0 or c_i == 0 or dx_i == 0:
|
341 |
logger.warning(f"Invalid material properties for layer {layer_idx} ({material_props[layer_idx]['name']}) in construction '{construction_name}'. Using default values.")
|
@@ -343,11 +312,11 @@ class CTFCalculator:
|
|
343 |
|
344 |
if node_j == 0 and layer_idx == 0: # Outdoor surface node
|
345 |
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)
|
346 |
-
A[idx, idx + 1] = -2 * dt * k_i / (dx_i * rho_i * c_i * dx_i)
|
347 |
b[idx] = dt / (rho_i * c_i * dx_i * R_out) * T_sol_air # Use sol-air temperature
|
348 |
-
elif node_j ==
|
349 |
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)
|
350 |
-
A[idx, idx - 1] = -2 * dt * k_i / (dx_i * rho_i * c_i * dx_i)
|
351 |
b[idx] = dt / (rho_i * c_i * dx_i * R_in) # Indoor temp contribution
|
352 |
# Add radiant load to indoor surface node (convert kW to W)
|
353 |
radiant_load = component.get("radiant_load", 0.0) * 1000 # kW to W
|
@@ -356,7 +325,7 @@ class CTFCalculator:
|
|
356 |
logger.debug(f"Added radiant load {radiant_load:.2f} W to indoor node for component '{component.get('name', 'Unknown')}'")
|
357 |
elif radiant_load != 0:
|
358 |
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.")
|
359 |
-
elif node_j ==
|
360 |
k_next = k[layer_idx + 1]
|
361 |
dx_next = dx[layer_idx + 1]
|
362 |
rho_next = rho[layer_idx + 1]
|
@@ -365,8 +334,8 @@ class CTFCalculator:
|
|
365 |
logger.warning(f"Invalid material properties for layer {layer_idx + 1} ({material_props[layer_idx + 1]['name']}) in construction '{construction_name}'. Skipping interface.")
|
366 |
continue
|
367 |
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))
|
368 |
-
A[idx, idx - 1] = -dt * k_i / (dx_i * 0.5 * (rho_i * c_i * dx_i + rho_next * c_next * dx_next))
|
369 |
-
A[idx, idx + 1] = -dt * k_next / (dx_next * 0.5 * (rho_i * c_i * dx_i + rho_next * c_next * dx_next))
|
370 |
elif node_j == 0 and layer_idx > 0: # Interface from previous layer
|
371 |
k_prev = k[layer_idx - 1]
|
372 |
dx_prev = dx[layer_idx - 1]
|
@@ -376,100 +345,15 @@ class CTFCalculator:
|
|
376 |
logger.warning(f"Invalid material properties for layer {layer_idx - 1} ({material_props[layer_idx - 1]['name']}) in construction '{construction_name}'. Skipping interface.")
|
377 |
continue
|
378 |
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))
|
379 |
-
A[idx, idx - 1] = -dt * k_prev / (dx_prev * 0.5 * (rho_prev * c_prev * dx_prev + rho_i * c_i * dx_i))
|
380 |
-
A[idx, idx + 1] = -dt * k_i / (dx_i * 0.5 * (rho_prev * c_prev * dx_prev + rho_i * c_i * dx_i))
|
381 |
else: # Internal node
|
382 |
A[idx, idx] = 1.0 + 2 * dt * k_i / (dx_i * rho_i * c_i * dx_i)
|
383 |
-
A[idx, idx - 1] = -dt * k_i / (dx_i * rho_i * c_i * dx_i)
|
384 |
-
A[idx, idx + 1] = -dt * k_i / (dx_i * rho_i * c_i * dx_i)
|
385 |
|
386 |
A = A.tocsr() # Convert to CSR for efficient solving
|
387 |
|
388 |
-
# Check matrix conditioning
|
389 |
-
try:
|
390 |
-
from scipy.sparse.linalg import norm
|
391 |
-
A_csc = A.tocsc()
|
392 |
-
cond_number = norm(A, ord=2) * norm(sparse_linalg.inv(A_csc), ord=2)
|
393 |
-
if cond_number > 1e6:
|
394 |
-
logger.warning(f"Matrix A is ill-conditioned (condition number: {cond_number:.2e}). Adjusting node spacing.")
|
395 |
-
nodes_per_layer = [min(n + 1, 6) for n in nodes_per_layer] # Increase nodes, cap at 6
|
396 |
-
dx = [t / n for t, n in zip(thicknesses, nodes_per_layer)]
|
397 |
-
|
398 |
-
# Recalculate node positions
|
399 |
-
node_positions = []
|
400 |
-
node_idx = 0
|
401 |
-
for i, n in enumerate(nodes_per_layer):
|
402 |
-
for j in range(n):
|
403 |
-
node_positions.append((i, j, node_idx))
|
404 |
-
node_idx += 1
|
405 |
-
|
406 |
-
# Rebuild system matrices
|
407 |
-
total_nodes = sum(nodes_per_layer)
|
408 |
-
A = sparse.lil_matrix((total_nodes, total_nodes))
|
409 |
-
b = np.zeros(total_nodes)
|
410 |
-
node_to_layer = [i for i, _, _ in node_positions]
|
411 |
-
|
412 |
-
for idx, (layer_idx, node_j, global_idx) in enumerate(node_positions):
|
413 |
-
k_i = k[layer_idx]
|
414 |
-
rho_i = rho[layer_idx]
|
415 |
-
c_i = c[layer_idx]
|
416 |
-
dx_i = dx[layer_idx]
|
417 |
-
npl_i = nodes_per_layer[layer_idx]
|
418 |
-
|
419 |
-
if k_i == 0 or rho_i == 0 or c_i == 0 or dx_i == 0:
|
420 |
-
logger.warning(f"Invalid material properties for layer {layer_idx} ({material_props[layer_idx]['name']}) in construction '{construction_name}'. Using default values.")
|
421 |
-
continue
|
422 |
-
|
423 |
-
if node_j == 0 and layer_idx == 0:
|
424 |
-
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)
|
425 |
-
A[idx, idx + 1] = -2 * dt * k_i / (dx_i * rho_i * c_i * dx_i) if idx + 1 < total_nodes else 0.0
|
426 |
-
b[idx] = dt / (rho_i * c_i * dx_i * R_out) * T_sol_air
|
427 |
-
elif node_j == npl_i - 1 and layer_idx == len(thicknesses) - 1:
|
428 |
-
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)
|
429 |
-
A[idx, idx - 1] = -2 * dt * k_i / (dx_i * rho_i * c_i * dx_i) if idx - 1 >= 0 else 0.0
|
430 |
-
b[idx] = dt / (rho_i * c_i * dx_i * R_in)
|
431 |
-
radiant_load = component.get("radiant_load", 0.0) * 1000
|
432 |
-
if radiant_load != 0 and rho_i * c_i * dx_i != 0:
|
433 |
-
b[idx] += dt / (rho_i * c_i * dx_i) * radiant_load
|
434 |
-
logger.debug(f"Added radiant load {radiant_load:.2f} W to indoor node for component '{component.get('name', 'Unknown')}'")
|
435 |
-
elif radiant_load != 0:
|
436 |
-
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.")
|
437 |
-
elif node_j == npl_i - 1 and layer_idx < len(thicknesses) - 1:
|
438 |
-
k_next = k[layer_idx + 1]
|
439 |
-
dx_next = dx[layer_idx + 1]
|
440 |
-
rho_next = rho[layer_idx + 1]
|
441 |
-
c_next = c[layer_idx + 1]
|
442 |
-
if k_next == 0 or dx_next == 0 or rho_next == 0 or c_next == 0:
|
443 |
-
logger.warning(f"Invalid material properties for layer {layer_idx + 1} ({material_props[layer_idx + 1]['name']}) in construction '{construction_name}'. Skipping interface.")
|
444 |
-
continue
|
445 |
-
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))
|
446 |
-
A[idx, idx - 1] = -dt * k_i / (dx_i * 0.5 * (rho_i * c_i * dx_i + rho_next * c_next * dx_next)) if idx - 1 >= 0 else 0.0
|
447 |
-
A[idx, idx + 1] = -dt * k_next / (dx_next * 0.5 * (rho_i * c_i * dx_i + rho_next * c_next * dx_next)) if idx + 1 < total_nodes else 0.0
|
448 |
-
elif node_j == 0 and layer_idx > 0:
|
449 |
-
k_prev = k[layer_idx - 1]
|
450 |
-
dx_prev = dx[layer_idx - 1]
|
451 |
-
rho_prev = rho[layer_idx - 1]
|
452 |
-
c_prev = c[layer_idx - 1]
|
453 |
-
if k_prev == 0 or dx_prev == 0 or rho_prev == 0 or c_prev == 0:
|
454 |
-
logger.warning(f"Invalid material properties for layer {layer_idx - 1} ({material_props[layer_idx - 1]['name']}) in construction '{construction_name}'. Skipping interface.")
|
455 |
-
continue
|
456 |
-
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))
|
457 |
-
A[idx, idx - 1] = -dt * k_prev / (dx_prev * 0.5 * (rho_prev * c_prev * dx_prev + rho_i * c_i * dx_i)) if idx - 1 >= 0 else 0.0
|
458 |
-
A[idx, idx + 1] = -dt * k_i / (dx_i * 0.5 * (rho_prev * c_prev * dx_prev + rho_i * c_i * dx_i)) if idx + 1 < total_nodes else 0.0
|
459 |
-
else:
|
460 |
-
A[idx, idx] = 1.0 + 2 * dt * k_i / (dx_i * rho_i * c_i * dx_i)
|
461 |
-
A[idx, idx - 1] = -dt * k_i / (dx_i * rho_i * c_i * dx_i) if idx - 1 >= 0 else 0.0
|
462 |
-
A[idx, idx + 1] = -dt * k_i / (dx_i * rho_i * c_i * dx_i) if idx + 1 < total_nodes else 0.0
|
463 |
-
|
464 |
-
A = A.tocsr()
|
465 |
-
construction_hash = cls._hash_construction(construction, nodes_per_layer)
|
466 |
-
with cls._cache_lock:
|
467 |
-
if construction_hash in cls._ctf_cache:
|
468 |
-
logger.info(f"Using cached CTF coefficients for construction '{construction_name}' after matrix conditioning adjustment")
|
469 |
-
return cls._ctf_cache[construction_hash]
|
470 |
-
except Exception as e:
|
471 |
-
logger.warning(f"Failed to compute matrix condition number: {str(e)}. Proceeding with current discretization.")
|
472 |
-
|
473 |
# Calculate CTF coefficients (X, Y, Z, F)
|
474 |
num_ctf = 12 # Standard number of coefficients
|
475 |
X = [0.0] * num_ctf # Exterior temp response
|
|
|
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', '')
|
|
|
132 |
|
133 |
@classmethod
|
134 |
def _get_material_properties(cls, material_name: str) -> Dict[str, float]:
|
135 |
+
"""Retrieve material properties from session state.
|
136 |
|
137 |
Args:
|
138 |
material_name: Name of the material.
|
139 |
|
140 |
Returns:
|
141 |
Dict containing conductivity, density, specific_heat, absorptivity, emissivity.
|
142 |
+
Returns empty dict if material not found.
|
143 |
"""
|
144 |
try:
|
145 |
materials = st.session_state.project_data.get('materials', {})
|
|
|
150 |
|
151 |
# Extract required properties
|
152 |
thermal_props = material.get('thermal_properties', {})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
153 |
return {
|
154 |
'name': material_name,
|
155 |
+
'conductivity': thermal_props.get('conductivity', 0.0),
|
156 |
+
'density': thermal_props.get('density', 0.0),
|
157 |
+
'specific_heat': thermal_props.get('specific_heat', 0.0),
|
158 |
'absorptivity': material.get('absorptivity', 0.6),
|
159 |
'emissivity': material.get('emissivity', 0.9)
|
160 |
}
|
|
|
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 = []
|
|
|
235 |
continue
|
236 |
material = cls._get_material_properties(material_name)
|
237 |
if not material:
|
238 |
+
logger.warning(f"Skipping layer with material '{material_name}' in construction '{construction_name}' due to missing properties.")
|
239 |
continue
|
240 |
thicknesses.append(thickness)
|
241 |
material_props.append(material)
|
|
|
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 |
|
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):
|
280 |
+
for j in range(nodes_per_layer):
|
281 |
+
node_positions.append((i, j, node_idx)) # (layer_idx, node_in_layer, global_node_idx)
|
282 |
+
node_idx += 1
|
283 |
|
|
|
|
|
|
|
284 |
# Stability check: Fourier number
|
285 |
+
for i, (a, d) in enumerate(zip(alpha, dx)):
|
286 |
if a == 0 or d == 0:
|
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]
|
|
|
305 |
rho_i = rho[layer_idx]
|
306 |
c_i = c[layer_idx]
|
307 |
dx_i = dx[layer_idx]
|
|
|
308 |
|
309 |
if k_i == 0 or rho_i == 0 or c_i == 0 or dx_i == 0:
|
310 |
logger.warning(f"Invalid material properties for layer {layer_idx} ({material_props[layer_idx]['name']}) in construction '{construction_name}'. Using default values.")
|
|
|
312 |
|
313 |
if node_j == 0 and layer_idx == 0: # Outdoor surface node
|
314 |
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)
|
315 |
+
A[idx, idx + 1] = -2 * dt * k_i / (dx_i * rho_i * c_i * dx_i)
|
316 |
b[idx] = dt / (rho_i * c_i * dx_i * R_out) * T_sol_air # Use sol-air temperature
|
317 |
+
elif node_j == nodes_per_layer - 1 and layer_idx == len(thicknesses) - 1: # Indoor surface node
|
318 |
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)
|
319 |
+
A[idx, idx - 1] = -2 * dt * k_i / (dx_i * rho_i * c_i * dx_i)
|
320 |
b[idx] = dt / (rho_i * c_i * dx_i * R_in) # Indoor temp contribution
|
321 |
# Add radiant load to indoor surface node (convert kW to W)
|
322 |
radiant_load = component.get("radiant_load", 0.0) * 1000 # kW to W
|
|
|
325 |
logger.debug(f"Added radiant load {radiant_load:.2f} W to indoor node for component '{component.get('name', 'Unknown')}'")
|
326 |
elif radiant_load != 0:
|
327 |
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.")
|
328 |
+
elif node_j == nodes_per_layer - 1 and layer_idx < len(thicknesses) - 1: # Interface between layers
|
329 |
k_next = k[layer_idx + 1]
|
330 |
dx_next = dx[layer_idx + 1]
|
331 |
rho_next = rho[layer_idx + 1]
|
|
|
334 |
logger.warning(f"Invalid material properties for layer {layer_idx + 1} ({material_props[layer_idx + 1]['name']}) in construction '{construction_name}'. Skipping interface.")
|
335 |
continue
|
336 |
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))
|
337 |
+
A[idx, idx - 1] = -dt * k_i / (dx_i * 0.5 * (rho_i * c_i * dx_i + rho_next * c_next * dx_next))
|
338 |
+
A[idx, idx + 1] = -dt * k_next / (dx_next * 0.5 * (rho_i * c_i * dx_i + rho_next * c_next * dx_next))
|
339 |
elif node_j == 0 and layer_idx > 0: # Interface from previous layer
|
340 |
k_prev = k[layer_idx - 1]
|
341 |
dx_prev = dx[layer_idx - 1]
|
|
|
345 |
logger.warning(f"Invalid material properties for layer {layer_idx - 1} ({material_props[layer_idx - 1]['name']}) in construction '{construction_name}'. Skipping interface.")
|
346 |
continue
|
347 |
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))
|
348 |
+
A[idx, idx - 1] = -dt * k_prev / (dx_prev * 0.5 * (rho_prev * c_prev * dx_prev + rho_i * c_i * dx_i))
|
349 |
+
A[idx, idx + 1] = -dt * k_i / (dx_i * 0.5 * (rho_prev * c_prev * dx_prev + rho_i * c_i * dx_i))
|
350 |
else: # Internal node
|
351 |
A[idx, idx] = 1.0 + 2 * dt * k_i / (dx_i * rho_i * c_i * dx_i)
|
352 |
+
A[idx, idx - 1] = -dt * k_i / (dx_i * rho_i * c_i * dx_i)
|
353 |
+
A[idx, idx + 1] = -dt * k_i / (dx_i * rho_i * c_i * dx_i)
|
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
|