Spaces:
Sleeping
Sleeping
Update utils/ctf_calculations.py
Browse files- utils/ctf_calculations.py +74 -50
utils/ctf_calculations.py
CHANGED
@@ -113,17 +113,17 @@ class CTFCalculator:
|
|
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:
|
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', '')
|
@@ -133,14 +133,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,11 +151,26 @@ class CTFCalculator:
|
|
151 |
|
152 |
# Extract required properties
|
153 |
thermal_props = material.get('thermal_properties', {})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
154 |
return {
|
155 |
'name': material_name,
|
156 |
-
'conductivity':
|
157 |
-
'density':
|
158 |
-
'specific_heat':
|
159 |
'absorptivity': material.get('absorptivity', 0.6),
|
160 |
'emissivity': material.get('emissivity', 0.9)
|
161 |
}
|
@@ -229,7 +244,7 @@ class CTFCalculator:
|
|
229 |
continue
|
230 |
material = cls._get_material_properties(material_name)
|
231 |
if not material:
|
232 |
-
logger.warning(f"Skipping layer with material '{material_name}' in construction '{construction_name}' due to missing properties.")
|
233 |
continue
|
234 |
thicknesses.append(thickness)
|
235 |
material_props.append(material)
|
@@ -248,7 +263,7 @@ class CTFCalculator:
|
|
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
|
@@ -273,27 +288,26 @@ class CTFCalculator:
|
|
273 |
return cls._ctf_cache[construction_hash]
|
274 |
|
275 |
# Calculate node spacing
|
276 |
-
dx = [t /
|
277 |
-
|
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.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 |
-
|
293 |
-
|
294 |
-
|
295 |
-
|
296 |
-
|
|
|
|
|
|
|
|
|
|
|
297 |
|
298 |
# Update construction hash if nodes_per_layer changed
|
299 |
construction_hash = cls._hash_construction(construction, nodes_per_layer)
|
@@ -302,8 +316,16 @@ class CTFCalculator:
|
|
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
|
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]
|
@@ -313,6 +335,7 @@ class CTFCalculator:
|
|
313 |
rho_i = rho[layer_idx]
|
314 |
c_i = c[layer_idx]
|
315 |
dx_i = dx[layer_idx]
|
|
|
316 |
|
317 |
if k_i == 0 or rho_i == 0 or c_i == 0 or dx_i == 0:
|
318 |
logger.warning(f"Invalid material properties for layer {layer_idx} ({material_props[layer_idx]['name']}) in construction '{construction_name}'. Using default values.")
|
@@ -320,11 +343,11 @@ class CTFCalculator:
|
|
320 |
|
321 |
if node_j == 0 and layer_idx == 0: # Outdoor surface node
|
322 |
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)
|
323 |
-
A[idx, idx + 1] = -2 * dt * k_i / (dx_i * rho_i * c_i * dx_i)
|
324 |
b[idx] = dt / (rho_i * c_i * dx_i * R_out) * T_sol_air # Use sol-air temperature
|
325 |
-
elif node_j ==
|
326 |
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)
|
327 |
-
A[idx, idx - 1] = -2 * dt * k_i / (dx_i * rho_i * c_i * dx_i)
|
328 |
b[idx] = dt / (rho_i * c_i * dx_i * R_in) # Indoor temp contribution
|
329 |
# Add radiant load to indoor surface node (convert kW to W)
|
330 |
radiant_load = component.get("radiant_load", 0.0) * 1000 # kW to W
|
@@ -333,7 +356,7 @@ class CTFCalculator:
|
|
333 |
logger.debug(f"Added radiant load {radiant_load:.2f} W to indoor node for component '{component.get('name', 'Unknown')}'")
|
334 |
elif radiant_load != 0:
|
335 |
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.")
|
336 |
-
elif node_j ==
|
337 |
k_next = k[layer_idx + 1]
|
338 |
dx_next = dx[layer_idx + 1]
|
339 |
rho_next = rho[layer_idx + 1]
|
@@ -342,8 +365,8 @@ class CTFCalculator:
|
|
342 |
logger.warning(f"Invalid material properties for layer {layer_idx + 1} ({material_props[layer_idx + 1]['name']}) in construction '{construction_name}'. Skipping interface.")
|
343 |
continue
|
344 |
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))
|
345 |
-
A[idx, idx - 1] = -dt * k_i / (dx_i * 0.5 * (rho_i * c_i * dx_i + rho_next * c_next * dx_next))
|
346 |
-
A[idx, idx + 1] = -dt * k_next / (dx_next * 0.5 * (rho_i * c_i * dx_i + rho_next * c_next * dx_next))
|
347 |
elif node_j == 0 and layer_idx > 0: # Interface from previous layer
|
348 |
k_prev = k[layer_idx - 1]
|
349 |
dx_prev = dx[layer_idx - 1]
|
@@ -353,12 +376,12 @@ class CTFCalculator:
|
|
353 |
logger.warning(f"Invalid material properties for layer {layer_idx - 1} ({material_props[layer_idx - 1]['name']}) in construction '{construction_name}'. Skipping interface.")
|
354 |
continue
|
355 |
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))
|
356 |
-
A[idx, idx - 1] = -dt * k_prev / (dx_prev * 0.5 * (rho_prev * c_prev * dx_prev + rho_i * c_i * dx_i))
|
357 |
-
A[idx, idx + 1] = -dt * k_i / (dx_i * 0.5 * (rho_prev * c_prev * dx_prev + rho_i * c_i * dx_i))
|
358 |
else: # Internal node
|
359 |
A[idx, idx] = 1.0 + 2 * dt * k_i / (dx_i * rho_i * c_i * dx_i)
|
360 |
-
A[idx, idx - 1] = -dt * k_i / (dx_i * rho_i * c_i * dx_i)
|
361 |
-
A[idx, idx + 1] = -dt * k_i / (dx_i * rho_i * c_i * dx_i)
|
362 |
|
363 |
A = A.tocsr() # Convert to CSR for efficient solving
|
364 |
|
@@ -369,19 +392,19 @@ class CTFCalculator:
|
|
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(
|
373 |
-
dx = [t /
|
374 |
|
375 |
# Recalculate node positions
|
376 |
node_positions = []
|
377 |
node_idx = 0
|
378 |
-
for i,
|
379 |
-
for j in range(
|
380 |
node_positions.append((i, j, node_idx))
|
381 |
node_idx += 1
|
382 |
|
383 |
# Rebuild system matrices
|
384 |
-
total_nodes = sum(nodes_per_layer
|
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]
|
@@ -391,6 +414,7 @@ class CTFCalculator:
|
|
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.")
|
@@ -398,11 +422,11 @@ class CTFCalculator:
|
|
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 ==
|
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:
|
@@ -410,7 +434,7 @@ class CTFCalculator:
|
|
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 ==
|
414 |
k_next = k[layer_idx + 1]
|
415 |
dx_next = dx[layer_idx + 1]
|
416 |
rho_next = rho[layer_idx + 1]
|
@@ -419,8 +443,8 @@ class CTFCalculator:
|
|
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]
|
@@ -430,12 +454,12 @@ class CTFCalculator:
|
|
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)
|
|
|
113 |
return T_sol_air
|
114 |
|
115 |
@staticmethod
|
116 |
+
def _hash_construction(construction: Dict[str, Any], nodes_per_layer: List[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: List of 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)}{''.join(map(str, nodes_per_layer))}"
|
127 |
layers = construction.get('layers', [])
|
128 |
for layer in layers:
|
129 |
material_name = layer.get('material', '')
|
|
|
133 |
|
134 |
@classmethod
|
135 |
def _get_material_properties(cls, material_name: str) -> Dict[str, float]:
|
136 |
+
"""Retrieve material properties from session state with validation.
|
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 or properties are invalid.
|
144 |
"""
|
145 |
try:
|
146 |
materials = st.session_state.project_data.get('materials', {})
|
|
|
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 |
}
|
|
|
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 or invalid properties.")
|
248 |
continue
|
249 |
thicknesses.append(thickness)
|
250 |
material_props.append(material)
|
|
|
263 |
|
264 |
# Discretization parameters
|
265 |
dt = 3600 # 1-hour time step (s)
|
266 |
+
nodes_per_layer = [3] * len(thicknesses) # Initial number of nodes per layer
|
267 |
R_in = 0.12 # Indoor surface resistance (m²·K/W, ASHRAE)
|
268 |
|
269 |
# Get weather data for sol-air temperature
|
|
|
288 |
return cls._ctf_cache[construction_hash]
|
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, t) in enumerate(zip(alpha, dx, thicknesses)):
|
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 > 0.5 or Fo < 0.33:
|
300 |
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.")
|
301 |
+
if Fo > 1000: # Handle extreme cases (e.g., air spaces, thin metal layers)
|
302 |
+
logger.warning(f"Extreme Fourier number {Fo:.3f} detected. Using minimum dx for layer {i}.")
|
303 |
+
dx[i] = t / 2 # Minimum 2 nodes per layer
|
304 |
+
nodes_per_layer[i] = 2
|
305 |
+
else:
|
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)
|
|
|
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 |
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 |
|
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) if idx + 1 < total_nodes else 0.0
|
347 |
b[idx] = dt / (rho_i * c_i * dx_i * R_out) * T_sol_air # Use sol-air temperature
|
348 |
+
elif node_j == npl_i - 1 and layer_idx == len(thicknesses) - 1: # Indoor surface node
|
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) if idx - 1 >= 0 else 0.0
|
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 |
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 == npl_i - 1 and layer_idx < len(thicknesses) - 1: # Interface between layers
|
360 |
k_next = k[layer_idx + 1]
|
361 |
dx_next = dx[layer_idx + 1]
|
362 |
rho_next = rho[layer_idx + 1]
|
|
|
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)) if idx - 1 >= 0 else 0.0
|
369 |
+
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
|
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 |
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)) if idx - 1 >= 0 else 0.0
|
380 |
+
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
|
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) if idx - 1 >= 0 else 0.0
|
384 |
+
A[idx, idx + 1] = -dt * k_i / (dx_i * rho_i * c_i * dx_i) if idx + 1 < total_nodes else 0.0
|
385 |
|
386 |
A = A.tocsr() # Convert to CSR for efficient solving
|
387 |
|
|
|
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]
|
|
|
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.")
|
|
|
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:
|
|
|
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]
|
|
|
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]
|
|
|
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)
|