Spaces:
Sleeping
Sleeping
Update utils/ctf_calculations.py
Browse files- utils/ctf_calculations.py +121 -38
utils/ctf_calculations.py
CHANGED
@@ -14,10 +14,9 @@ import scipy.sparse.linalg as sparse_linalg
|
|
14 |
import hashlib
|
15 |
import logging
|
16 |
import threading
|
17 |
-
from typing import List
|
18 |
-
|
19 |
from enum import Enum
|
20 |
-
from typing import Dict, List, Optional, NamedTuple
|
21 |
|
22 |
# Configure logging
|
23 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
@@ -28,7 +27,6 @@ class ComponentType(Enum):
|
|
28 |
ROOF = "Roof"
|
29 |
FLOOR = "Floor"
|
30 |
WINDOW = "Window"
|
31 |
-
DOOR = "Door"
|
32 |
SKYLIGHT = "Skylight"
|
33 |
|
34 |
class CTFCoefficients(NamedTuple):
|
@@ -45,66 +43,138 @@ class CTFCalculator:
|
|
45 |
_cache_lock = threading.Lock() # Thread-safe lock for cache access
|
46 |
|
47 |
@staticmethod
|
48 |
-
def _hash_construction(construction:
|
49 |
"""Generate a unique hash for a construction based on its properties.
|
50 |
|
51 |
Args:
|
52 |
-
construction:
|
53 |
|
54 |
Returns:
|
55 |
str: SHA-256 hash of the construction properties.
|
56 |
"""
|
57 |
-
hash_input = f"{construction.name}"
|
58 |
-
|
59 |
-
|
60 |
-
|
|
|
|
|
61 |
return hashlib.sha256(hash_input.encode()).hexdigest()
|
62 |
|
63 |
@classmethod
|
64 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
65 |
"""Calculate CTF coefficients using implicit Finite Difference Method.
|
66 |
|
67 |
-
Note: Per ASHRAE, CTF calculations are skipped for WINDOW
|
68 |
as they use typical material properties. CTF tables for these components will be added later.
|
69 |
|
70 |
Args:
|
71 |
-
component:
|
72 |
|
73 |
Returns:
|
74 |
CTFCoefficients: Named tuple containing X, Y, Z, and F coefficients.
|
75 |
"""
|
76 |
-
#
|
77 |
-
|
78 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
79 |
return CTFCoefficients(X=[0.0], Y=[0.0], Z=[0.0], F=[0.0])
|
80 |
|
81 |
-
#
|
82 |
-
|
83 |
-
if not
|
84 |
-
logger.warning(f"No
|
|
|
|
|
|
|
|
|
|
|
|
|
85 |
return CTFCoefficients(X=[0.0], Y=[0.0], Z=[0.0], F=[0.0])
|
86 |
|
87 |
# Check cache with thread-safe access
|
88 |
construction_hash = cls._hash_construction(construction)
|
89 |
with cls._cache_lock:
|
90 |
if construction_hash in cls._ctf_cache:
|
91 |
-
logger.info(f"Using cached CTF coefficients for construction {
|
92 |
return cls._ctf_cache[construction_hash]
|
93 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
94 |
# Discretization parameters
|
95 |
dt = 3600 # 1-hour time step (s)
|
96 |
nodes_per_layer = 3 # 2–4 nodes per layer for balance
|
97 |
R_out = 0.04 # Outdoor surface resistance (m²·K/W, ASHRAE)
|
98 |
R_in = 0.12 # Indoor surface resistance (m²·K/W, ASHRAE)
|
99 |
|
100 |
-
# Collect layer properties
|
101 |
-
thicknesses = [layer["thickness"] for layer in construction.layers]
|
102 |
-
materials = [layer["material"] for layer in construction.layers]
|
103 |
-
k = [m.conductivity for m in materials] # W/m·K
|
104 |
-
rho = [m.density for m in materials] # kg/m³
|
105 |
-
c = [m.specific_heat for m in materials] # J/kg·K
|
106 |
-
alpha = [k_i / (rho_i * c_i) for k_i, rho_i, c_i in zip(k, rho, c)] # Thermal diffusivity (m²/s)
|
107 |
-
|
108 |
# Calculate node spacing and check stability
|
109 |
total_nodes = sum(nodes_per_layer for _ in thicknesses)
|
110 |
dx = [t / nodes_per_layer for t in thicknesses] # Node spacing per layer
|
@@ -117,9 +187,12 @@ class CTFCalculator:
|
|
117 |
|
118 |
# Stability check: Fourier number
|
119 |
for i, (a, d) in enumerate(zip(alpha, dx)):
|
|
|
|
|
|
|
120 |
Fo = a * dt / (d ** 2)
|
121 |
if Fo < 0.33:
|
122 |
-
logger.warning(f"Fourier number {Fo:.3f} < 0.33 for layer {i} ({
|
123 |
dx[i] = np.sqrt(a * dt / 0.33)
|
124 |
nodes_per_layer = max(2, int(np.ceil(thicknesses[i] / dx[i])))
|
125 |
dx[i] = thicknesses[i] / nodes_per_layer
|
@@ -137,6 +210,10 @@ class CTFCalculator:
|
|
137 |
c_i = c[layer_idx]
|
138 |
dx_i = dx[layer_idx]
|
139 |
|
|
|
|
|
|
|
|
|
140 |
if node_j == 0 and layer_idx == 0: # Outdoor surface node
|
141 |
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)
|
142 |
A[idx, idx + 1] = -2 * dt * k_i / (dx_i * rho_i * c_i * dx_i)
|
@@ -150,6 +227,9 @@ class CTFCalculator:
|
|
150 |
dx_next = dx[layer_idx + 1]
|
151 |
rho_next = rho[layer_idx + 1]
|
152 |
c_next = c[layer_idx + 1]
|
|
|
|
|
|
|
153 |
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))
|
154 |
A[idx, idx - 1] = -dt * k_i / (dx_i * 0.5 * (rho_i * c_i * dx_i + rho_next * c_next * dx_next))
|
155 |
A[idx, idx + 1] = -dt * k_next / (dx_next * 0.5 * (rho_i * c_i * dx_i + rho_next * c_next * dx_next))
|
@@ -158,6 +238,9 @@ class CTFCalculator:
|
|
158 |
dx_prev = dx[layer_idx - 1]
|
159 |
rho_prev = rho[layer_idx - 1]
|
160 |
c_prev = c[layer_idx - 1]
|
|
|
|
|
|
|
161 |
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))
|
162 |
A[idx, idx - 1] = -dt * k_prev / (dx_prev * 0.5 * (rho_prev * c_prev * dx_prev + rho_i * c_i * dx_i))
|
163 |
A[idx, idx + 1] = -dt * k_i / (dx_i * 0.5 * (rho_prev * c_prev * dx_prev + rho_i * c_i * dx_i))
|
@@ -180,7 +263,7 @@ class CTFCalculator:
|
|
180 |
for t in range(num_ctf):
|
181 |
b_out = b.copy()
|
182 |
if t == 0:
|
183 |
-
b_out[0] = dt / (rho[0] * c[0] * dx[0] * R_out) # Unit outdoor temp impulse
|
184 |
T = sparse_linalg.spsolve(A, b_out + T_prev)
|
185 |
q_in = (T[-1] - 0.0) / R_in # Indoor heat flux (W/m²)
|
186 |
Y[t] = q_in
|
@@ -193,7 +276,7 @@ class CTFCalculator:
|
|
193 |
for t in range(num_ctf):
|
194 |
b_in = b.copy()
|
195 |
if t == 0:
|
196 |
-
b_in[-1] = dt / (rho[-1] * c[-1] * dx[-1] * R_in) # Unit indoor temp impulse
|
197 |
T = sparse_linalg.spsolve(A, b_in + T_prev)
|
198 |
q_in = (T[-1] - 0.0) / R_in
|
199 |
Z[t] = q_in
|
@@ -204,7 +287,7 @@ class CTFCalculator:
|
|
204 |
for t in range(num_ctf):
|
205 |
b_flux = np.zeros(total_nodes)
|
206 |
if t == 0:
|
207 |
-
b_flux[-1] = -1.0 / (rho[-1] * c[-1] * dx[-1]) # Unit flux impulse
|
208 |
T = sparse_linalg.spsolve(A, b_flux + T_prev)
|
209 |
q_in = (T[-1] - 0.0) / R_in
|
210 |
F[t] = q_in
|
@@ -213,18 +296,18 @@ class CTFCalculator:
|
|
213 |
ctf = CTFCoefficients(X=X, Y=Y, Z=Z, F=F)
|
214 |
with cls._cache_lock:
|
215 |
cls._ctf_cache[construction_hash] = ctf
|
216 |
-
logger.info(f"Calculated CTF coefficients for construction {
|
217 |
return ctf
|
218 |
|
219 |
@classmethod
|
220 |
-
def calculate_ctf_tables(cls, component) -> CTFCoefficients:
|
221 |
-
"""Placeholder for future implementation of CTF table lookups for windows
|
222 |
|
223 |
Args:
|
224 |
-
component:
|
225 |
|
226 |
Returns:
|
227 |
CTFCoefficients: Placeholder zero coefficients until implementation.
|
228 |
"""
|
229 |
-
logger.info(f"CTF table calculation for {component.
|
230 |
return CTFCoefficients(X=[0.0], Y=[0.0], Z=[0.0], F=[0.0])
|
|
|
14 |
import hashlib
|
15 |
import logging
|
16 |
import threading
|
17 |
+
from typing import List, Dict, Any, NamedTuple
|
18 |
+
import streamlit as st
|
19 |
from enum import Enum
|
|
|
20 |
|
21 |
# Configure logging
|
22 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
|
|
27 |
ROOF = "Roof"
|
28 |
FLOOR = "Floor"
|
29 |
WINDOW = "Window"
|
|
|
30 |
SKYLIGHT = "Skylight"
|
31 |
|
32 |
class CTFCoefficients(NamedTuple):
|
|
|
43 |
_cache_lock = threading.Lock() # Thread-safe lock for cache access
|
44 |
|
45 |
@staticmethod
|
46 |
+
def _hash_construction(construction: Dict[str, Any]) -> str:
|
47 |
"""Generate a unique hash for a construction based on its properties.
|
48 |
|
49 |
Args:
|
50 |
+
construction: Dictionary containing construction properties (name, layers).
|
51 |
|
52 |
Returns:
|
53 |
str: SHA-256 hash of the construction properties.
|
54 |
"""
|
55 |
+
hash_input = f"{construction.get('name', '')}"
|
56 |
+
layers = construction.get('layers', [])
|
57 |
+
for layer in layers:
|
58 |
+
material_name = layer.get('material', '')
|
59 |
+
thickness = layer.get('thickness', 0.0)
|
60 |
+
hash_input += f"{material_name}{thickness}"
|
61 |
return hashlib.sha256(hash_input.encode()).hexdigest()
|
62 |
|
63 |
@classmethod
|
64 |
+
def _get_material_properties(cls, material_name: str) -> Dict[str, float]:
|
65 |
+
"""Retrieve material properties from session state.
|
66 |
+
|
67 |
+
Args:
|
68 |
+
material_name: Name of the material.
|
69 |
+
|
70 |
+
Returns:
|
71 |
+
Dict containing conductivity, density, specific_heat, absorptivity, emissivity.
|
72 |
+
Returns empty dict if material not found.
|
73 |
+
"""
|
74 |
+
try:
|
75 |
+
materials = st.session_state.project_data.get('materials', {})
|
76 |
+
material = materials.get('library', {}).get(material_name, materials.get('project', {}).get(material_name))
|
77 |
+
if not material:
|
78 |
+
logger.error(f"Material '{material_name}' not found in library or project materials.")
|
79 |
+
return {}
|
80 |
+
|
81 |
+
# Extract required properties
|
82 |
+
thermal_props = material.get('thermal_properties', {})
|
83 |
+
return {
|
84 |
+
'name': material_name,
|
85 |
+
'conductivity': thermal_props.get('conductivity', 0.0),
|
86 |
+
'density': thermal_props.get('density', 0.0),
|
87 |
+
'specific_heat': thermal_props.get('specific_heat', 0.0),
|
88 |
+
'absorptivity': material.get('absorptivity', 0.6),
|
89 |
+
'emissivity': material.get('emissivity', 0.9)
|
90 |
+
}
|
91 |
+
except Exception as e:
|
92 |
+
logger.error(f"Error retrieving material '{material_name}' properties: {str(e)}")
|
93 |
+
return {}
|
94 |
+
|
95 |
+
@classmethod
|
96 |
+
def calculate_ctf_coefficients(cls, component: Dict[str, Any]) -> CTFCoefficients:
|
97 |
"""Calculate CTF coefficients using implicit Finite Difference Method.
|
98 |
|
99 |
+
Note: Per ASHRAE, CTF calculations are skipped for WINDOW and SKYLIGHT components,
|
100 |
as they use typical material properties. CTF tables for these components will be added later.
|
101 |
|
102 |
Args:
|
103 |
+
component: Dictionary containing component properties from st.session_state.project_data["components"].
|
104 |
|
105 |
Returns:
|
106 |
CTFCoefficients: Named tuple containing X, Y, Z, and F coefficients.
|
107 |
"""
|
108 |
+
# Determine component type
|
109 |
+
comp_type_str = component.get('type', '').lower() # Expected from component dictionary key (e.g., 'walls')
|
110 |
+
comp_type_map = {
|
111 |
+
'walls': ComponentType.WALL,
|
112 |
+
'roofs': ComponentType.ROOF,
|
113 |
+
'floors': ComponentType.FLOOR,
|
114 |
+
'windows': ComponentType.WINDOW,
|
115 |
+
'skylights': ComponentType.SKYLIGHT
|
116 |
+
}
|
117 |
+
component_type = comp_type_map.get(comp_type_str, None)
|
118 |
+
if not component_type:
|
119 |
+
logger.warning(f"Invalid component type '{comp_type_str}' for component '{component.get('name', 'Unknown')}'. Returning zero CTFs.")
|
120 |
+
return CTFCoefficients(X=[0.0], Y=[0.0], Z=[0.0], F=[0.0])
|
121 |
+
|
122 |
+
# Skip CTF for WINDOW, SKYLIGHT as per ASHRAE; return zero coefficients
|
123 |
+
if component_type in [ComponentType.WINDOW, ComponentType.SKYLIGHT]:
|
124 |
+
logger.info(f"Skipping CTF calculation for {component_type.value} component '{component.get('name', 'Unknown')}'. Using zero coefficients until CTF tables are implemented.")
|
125 |
return CTFCoefficients(X=[0.0], Y=[0.0], Z=[0.0], F=[0.0])
|
126 |
|
127 |
+
# Retrieve construction
|
128 |
+
construction_name = component.get('construction', '')
|
129 |
+
if not construction_name:
|
130 |
+
logger.warning(f"No construction specified for component '{component.get('name', 'Unknown')}' ({component_type.value}). Returning zero CTFs.")
|
131 |
+
return CTFCoefficients(X=[0.0], Y=[0.0], Z=[0.0], F=[0.0])
|
132 |
+
|
133 |
+
constructions = st.session_state.project_data.get('constructions', {})
|
134 |
+
construction = constructions.get('library', {}).get(construction_name, constructions.get('project', {}).get(construction_name))
|
135 |
+
if not construction or not construction.get('layers'):
|
136 |
+
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.")
|
137 |
return CTFCoefficients(X=[0.0], Y=[0.0], Z=[0.0], F=[0.0])
|
138 |
|
139 |
# Check cache with thread-safe access
|
140 |
construction_hash = cls._hash_construction(construction)
|
141 |
with cls._cache_lock:
|
142 |
if construction_hash in cls._ctf_cache:
|
143 |
+
logger.info(f"Using cached CTF coefficients for construction '{construction_name}'")
|
144 |
return cls._ctf_cache[construction_hash]
|
145 |
|
146 |
+
# Collect layer properties
|
147 |
+
thicknesses = []
|
148 |
+
material_props = []
|
149 |
+
for layer in construction.get('layers', []):
|
150 |
+
material_name = layer.get('material', '')
|
151 |
+
thickness = layer.get('thickness', 0.0)
|
152 |
+
if thickness <= 0.0:
|
153 |
+
logger.warning(f"Invalid thickness {thickness} for material '{material_name}' in construction '{construction_name}'. Skipping layer.")
|
154 |
+
continue
|
155 |
+
material = cls._get_material_properties(material_name)
|
156 |
+
if not material:
|
157 |
+
logger.warning(f"Skipping layer with material '{material_name}' in construction '{construction_name}' due to missing properties.")
|
158 |
+
continue
|
159 |
+
thicknesses.append(thickness)
|
160 |
+
material_props.append(material)
|
161 |
+
|
162 |
+
if not thicknesses or not material_props:
|
163 |
+
logger.warning(f"No valid layers with material properties for construction '{construction_name}' in component '{component.get('name', 'Unknown')}'. Returning zero CTFs.")
|
164 |
+
return CTFCoefficients(X=[0.0], Y=[0.0], Z=[0.0], F=[0.0])
|
165 |
+
|
166 |
+
# Extract material properties
|
167 |
+
k = [m['conductivity'] for m in material_props] # W/m·K
|
168 |
+
rho = [m['density'] for m in material_props] # kg/m³
|
169 |
+
c = [m['specific_heat'] for m in material_props] # J/kg·K
|
170 |
+
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)
|
171 |
+
|
172 |
# Discretization parameters
|
173 |
dt = 3600 # 1-hour time step (s)
|
174 |
nodes_per_layer = 3 # 2–4 nodes per layer for balance
|
175 |
R_out = 0.04 # Outdoor surface resistance (m²·K/W, ASHRAE)
|
176 |
R_in = 0.12 # Indoor surface resistance (m²·K/W, ASHRAE)
|
177 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
178 |
# Calculate node spacing and check stability
|
179 |
total_nodes = sum(nodes_per_layer for _ in thicknesses)
|
180 |
dx = [t / nodes_per_layer for t in thicknesses] # Node spacing per layer
|
|
|
187 |
|
188 |
# Stability check: Fourier number
|
189 |
for i, (a, d) in enumerate(zip(alpha, dx)):
|
190 |
+
if a == 0 or d == 0:
|
191 |
+
logger.warning(f"Invalid thermal diffusivity or node spacing for layer {i} in construction '{construction_name}'. Skipping stability adjustment.")
|
192 |
+
continue
|
193 |
Fo = a * dt / (d ** 2)
|
194 |
if Fo < 0.33:
|
195 |
+
logger.warning(f"Fourier number {Fo:.3f} < 0.33 for layer {i} ({material_props[i]['name']}). Adjusting node spacing.")
|
196 |
dx[i] = np.sqrt(a * dt / 0.33)
|
197 |
nodes_per_layer = max(2, int(np.ceil(thicknesses[i] / dx[i])))
|
198 |
dx[i] = thicknesses[i] / nodes_per_layer
|
|
|
210 |
c_i = c[layer_idx]
|
211 |
dx_i = dx[layer_idx]
|
212 |
|
213 |
+
if k_i == 0 or rho_i == 0 or c_i == 0 or dx_i == 0:
|
214 |
+
logger.warning(f"Invalid material properties for layer {layer_idx} ({material_props[layer_idx]['name']}) in construction '{construction_name}'. Using default values.")
|
215 |
+
continue
|
216 |
+
|
217 |
if node_j == 0 and layer_idx == 0: # Outdoor surface node
|
218 |
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)
|
219 |
A[idx, idx + 1] = -2 * dt * k_i / (dx_i * rho_i * c_i * dx_i)
|
|
|
227 |
dx_next = dx[layer_idx + 1]
|
228 |
rho_next = rho[layer_idx + 1]
|
229 |
c_next = c[layer_idx + 1]
|
230 |
+
if k_next == 0 or dx_next == 0 or rho_next == 0 or c_next == 0:
|
231 |
+
logger.warning(f"Invalid material properties for layer {layer_idx + 1} ({material_props[layer_idx + 1]['name']}) in construction '{construction_name}'. Skipping interface.")
|
232 |
+
continue
|
233 |
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))
|
234 |
A[idx, idx - 1] = -dt * k_i / (dx_i * 0.5 * (rho_i * c_i * dx_i + rho_next * c_next * dx_next))
|
235 |
A[idx, idx + 1] = -dt * k_next / (dx_next * 0.5 * (rho_i * c_i * dx_i + rho_next * c_next * dx_next))
|
|
|
238 |
dx_prev = dx[layer_idx - 1]
|
239 |
rho_prev = rho[layer_idx - 1]
|
240 |
c_prev = c[layer_idx - 1]
|
241 |
+
if k_prev == 0 or dx_prev == 0 or rho_prev == 0 or c_prev == 0:
|
242 |
+
logger.warning(f"Invalid material properties for layer {layer_idx - 1} ({material_props[layer_idx - 1]['name']}) in construction '{construction_name}'. Skipping interface.")
|
243 |
+
continue
|
244 |
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))
|
245 |
A[idx, idx - 1] = -dt * k_prev / (dx_prev * 0.5 * (rho_prev * c_prev * dx_prev + rho_i * c_i * dx_i))
|
246 |
A[idx, idx + 1] = -dt * k_i / (dx_i * 0.5 * (rho_prev * c_prev * dx_prev + rho_i * c_i * dx_i))
|
|
|
263 |
for t in range(num_ctf):
|
264 |
b_out = b.copy()
|
265 |
if t == 0:
|
266 |
+
b_out[0] = dt / (rho[0] * c[0] * dx[0] * R_out) if rho[0] * c[0] * dx[0] != 0 else 0.0 # Unit outdoor temp impulse
|
267 |
T = sparse_linalg.spsolve(A, b_out + T_prev)
|
268 |
q_in = (T[-1] - 0.0) / R_in # Indoor heat flux (W/m²)
|
269 |
Y[t] = q_in
|
|
|
276 |
for t in range(num_ctf):
|
277 |
b_in = b.copy()
|
278 |
if t == 0:
|
279 |
+
b_in[-1] = dt / (rho[-1] * c[-1] * dx[-1] * R_in) if rho[-1] * c[-1] * dx[-1] != 0 else 0.0 # Unit indoor temp impulse
|
280 |
T = sparse_linalg.spsolve(A, b_in + T_prev)
|
281 |
q_in = (T[-1] - 0.0) / R_in
|
282 |
Z[t] = q_in
|
|
|
287 |
for t in range(num_ctf):
|
288 |
b_flux = np.zeros(total_nodes)
|
289 |
if t == 0:
|
290 |
+
b_flux[-1] = -1.0 / (rho[-1] * c[-1] * dx[-1]) if rho[-1] * c[-1] * dx[-1] != 0 else 0.0 # Unit flux impulse
|
291 |
T = sparse_linalg.spsolve(A, b_flux + T_prev)
|
292 |
q_in = (T[-1] - 0.0) / R_in
|
293 |
F[t] = q_in
|
|
|
296 |
ctf = CTFCoefficients(X=X, Y=Y, Z=Z, F=F)
|
297 |
with cls._cache_lock:
|
298 |
cls._ctf_cache[construction_hash] = ctf
|
299 |
+
logger.info(f"Calculated CTF coefficients for construction '{construction_name}' in component '{component.get('name', 'Unknown')}'")
|
300 |
return ctf
|
301 |
|
302 |
@classmethod
|
303 |
+
def calculate_ctf_tables(cls, component: Dict[str, Any]) -> CTFCoefficients:
|
304 |
+
"""Placeholder for future implementation of CTF table lookups for windows and skylights.
|
305 |
|
306 |
Args:
|
307 |
+
component: Dictionary containing component properties.
|
308 |
|
309 |
Returns:
|
310 |
CTFCoefficients: Placeholder zero coefficients until implementation.
|
311 |
"""
|
312 |
+
logger.info(f"CTF table calculation for {component.get('type', 'Unknown')} component '{component.get('name', 'Unknown')}' not yet implemented. Returning zero coefficients.")
|
313 |
return CTFCoefficients(X=[0.0], Y=[0.0], Z=[0.0], F=[0.0])
|