Spaces:
Sleeping
Sleeping
Update utils/ctf_calculations.py
Browse files- utils/ctf_calculations.py +101 -95
utils/ctf_calculations.py
CHANGED
@@ -2,7 +2,9 @@
|
|
2 |
CTF Calculations Module
|
3 |
|
4 |
This module contains the CTFCalculator class for calculating Conduction Transfer Function (CTF)
|
5 |
-
coefficients for HVAC load calculations using the implicit Finite Difference Method
|
|
|
|
|
6 |
|
7 |
Developed by: Dr Majed Abuseif, Deakin University
|
8 |
© 2025
|
@@ -16,13 +18,19 @@ import logging
|
|
16 |
import threading
|
17 |
from typing import List, Dict, Any, NamedTuple
|
18 |
import streamlit as st
|
19 |
-
from
|
20 |
-
from utils.solar import SolarCalculations
|
21 |
|
22 |
# Configure logging
|
23 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
24 |
logger = logging.getLogger(__name__)
|
25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
26 |
class CTFCoefficients(NamedTuple):
|
27 |
X: List[float] # Exterior temperature coefficients
|
28 |
Y: List[float] # Cross coefficients
|
@@ -36,6 +44,74 @@ class CTFCalculator:
|
|
36 |
_ctf_cache = {}
|
37 |
_cache_lock = threading.Lock() # Thread-safe lock for cache access
|
38 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
39 |
@staticmethod
|
40 |
def _hash_construction(construction: Dict[str, Any]) -> str:
|
41 |
"""Generate a unique hash for a construction based on its properties.
|
@@ -87,14 +163,15 @@ class CTFCalculator:
|
|
87 |
return {}
|
88 |
|
89 |
@classmethod
|
90 |
-
def calculate_ctf_coefficients(cls, component: Dict[str, Any]) -> CTFCoefficients:
|
91 |
-
"""Calculate CTF coefficients using implicit Finite Difference Method.
|
92 |
|
93 |
Note: Per ASHRAE, CTF calculations are skipped for WINDOW and SKYLIGHT components,
|
94 |
as they use typical material properties. CTF tables for these components will be added later.
|
95 |
|
96 |
Args:
|
97 |
component: Dictionary containing component properties from st.session_state.project_data["components"].
|
|
|
98 |
|
99 |
Returns:
|
100 |
CTFCoefficients: Named tuple containing X, Y, Z, and F coefficients.
|
@@ -162,13 +239,28 @@ class CTFCalculator:
|
|
162 |
rho = [m['density'] for m in material_props] # kg/m³
|
163 |
c = [m['specific_heat'] for m in material_props] # J/kg·K
|
164 |
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)
|
|
|
|
|
165 |
|
166 |
# Discretization parameters
|
167 |
dt = 3600 # 1-hour time step (s)
|
168 |
nodes_per_layer = 3 # 2–4 nodes per layer for balance
|
169 |
-
R_out = 0.04 # Outdoor surface resistance (m²·K/W, ASHRAE)
|
170 |
R_in = 0.12 # Indoor surface resistance (m²·K/W, ASHRAE)
|
171 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
172 |
# Calculate node spacing and check stability
|
173 |
total_nodes = sum(nodes_per_layer for _ in thicknesses)
|
174 |
dx = [t / nodes_per_layer for t in thicknesses] # Node spacing per layer
|
@@ -211,7 +303,7 @@ class CTFCalculator:
|
|
211 |
if node_j == 0 and layer_idx == 0: # Outdoor surface node
|
212 |
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)
|
213 |
A[idx, idx + 1] = -2 * dt * k_i / (dx_i * rho_i * c_i * dx_i)
|
214 |
-
b[idx] = dt / (rho_i * c_i * dx_i * R_out) #
|
215 |
elif node_j == nodes_per_layer - 1 and layer_idx == len(thicknesses) - 1: # Indoor surface node
|
216 |
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)
|
217 |
A[idx, idx - 1] = -2 * dt * k_i / (dx_i * rho_i * c_i * dx_i)
|
@@ -257,7 +349,7 @@ class CTFCalculator:
|
|
257 |
for t in range(num_ctf):
|
258 |
b_out = b.copy()
|
259 |
if t == 0:
|
260 |
-
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
|
261 |
T = sparse_linalg.spsolve(A, b_out + T_prev)
|
262 |
q_in = (T[-1] - 0.0) / R_in # Indoor heat flux (W/m²)
|
263 |
Y[t] = q_in
|
@@ -304,90 +396,4 @@ class CTFCalculator:
|
|
304 |
CTFCoefficients: Placeholder zero coefficients until implementation.
|
305 |
"""
|
306 |
logger.info(f"CTF table calculation for {component.get('type', 'Unknown')} component '{component.get('name', 'Unknown')}' not yet implemented. Returning zero coefficients.")
|
307 |
-
return CTFCoefficients(X=[0.0], Y=[0.0], Z=[0.0], F=[0.0])
|
308 |
-
|
309 |
-
@classmethod
|
310 |
-
def calculate_heat_flux(cls, component: Dict[str, Any], hourly_data: Dict[str, Any],
|
311 |
-
solar_calc: SolarCalculations, building_info: Dict[str, Any]) -> float:
|
312 |
-
"""Calculate hourly heat flux using CTF coefficients and sol-air temperature.
|
313 |
-
|
314 |
-
Args:
|
315 |
-
component: Dictionary containing component properties.
|
316 |
-
hourly_data: Dictionary with month, day, hour, dry_bulb, dew_point, wind_speed,
|
317 |
-
total_sky_cover, global_horizontal_radiation, direct_normal_radiation,
|
318 |
-
diffuse_horizontal_radiation.
|
319 |
-
solar_calc: SolarCalculations instance for sol-air temperature.
|
320 |
-
building_info: Building information dictionary.
|
321 |
-
|
322 |
-
Returns:
|
323 |
-
float: Heat flux through the component (W/m²).
|
324 |
-
|
325 |
-
References:
|
326 |
-
ASHRAE Handbook—Fundamentals, Chapter 26.
|
327 |
-
"""
|
328 |
-
# Get component type
|
329 |
-
comp_type_str = component.get('type', '').lower()
|
330 |
-
comp_type_map = {
|
331 |
-
'walls': ComponentType.WALL,
|
332 |
-
'roofs': ComponentType.ROOF,
|
333 |
-
'floors': ComponentType.FLOOR,
|
334 |
-
'windows': ComponentType.WINDOW,
|
335 |
-
'skylights': ComponentType.SKYLIGHT
|
336 |
-
}
|
337 |
-
component_type = comp_type_map.get(comp_type_str, None)
|
338 |
-
if not component_type:
|
339 |
-
logger.warning(f"Invalid component type '{comp_type_str}' for component '{component.get('name', 'Unknown')}'. Returning zero flux.")
|
340 |
-
return 0.0
|
341 |
-
|
342 |
-
# Skip for WINDOW, SKYLIGHT (handled via SHGC)
|
343 |
-
if component_type in [ComponentType.WINDOW, ComponentType.SKYLIGHT]:
|
344 |
-
logger.info(f"Skipping heat flux calculation for {component_type.value} component '{component.get('name', 'Unknown')}'.")
|
345 |
-
return 0.0
|
346 |
-
|
347 |
-
# Get CTF coefficients
|
348 |
-
ctf = cls.calculate_ctf_coefficients(component)
|
349 |
-
if not any(ctf.X + ctf.Y + ctf.Z + ctf.F):
|
350 |
-
logger.warning(f"Zero CTF coefficients for component '{component.get('name', 'Unknown')}'. Returning zero flux.")
|
351 |
-
return 0.0
|
352 |
-
|
353 |
-
# Extract hourly data
|
354 |
-
T_out = hourly_data.get('dry_bulb', 25.0)
|
355 |
-
dew_point = hourly_data.get('dew_point', T_out - 5.0)
|
356 |
-
wind_speed = hourly_data.get('wind_speed', 4.0)
|
357 |
-
total_sky_cover = hourly_data.get('total_sky_cover', 0.5)
|
358 |
-
T_in = 24.0 # Assume constant indoor temperature (adjust as needed)
|
359 |
-
|
360 |
-
# Get surface parameters and total incident radiation
|
361 |
-
surface_tilt, surface_azimuth, h_o, emissivity, absorptivity = solar_calc.get_surface_parameters(
|
362 |
-
component, building_info, wind_speed)
|
363 |
-
|
364 |
-
# Calculate total incident radiation (simplified, reuse solar_calc logic)
|
365 |
-
ghi = hourly_data.get('global_horizontal_radiation', 0.0)
|
366 |
-
dni = hourly_data.get('direct_normal_radiation', ghi * 0.7)
|
367 |
-
dhi = hourly_data.get('diffuse_horizontal_radiation', ghi * 0.3)
|
368 |
-
if ghi <= 0:
|
369 |
-
I_t = 0.0
|
370 |
-
else:
|
371 |
-
# Simplified radiation calculation (full calc in solar.py)
|
372 |
-
view_factor = (1 - math.cos(math.radians(surface_tilt))) / 2
|
373 |
-
ground_reflectivity = 0.2
|
374 |
-
cos_theta = max(math.cos(math.radians(surface_tilt)), 0.0) # Simplified for CTF
|
375 |
-
I_t = dni * cos_theta + dhi + ground_reflectivity * ghi * view_factor
|
376 |
-
|
377 |
-
# Calculate sol-air temperature
|
378 |
-
T_sol_air = solar_calc.calculate_sol_air_temperature(
|
379 |
-
T_out, I_t, absorptivity, emissivity, h_o, dew_point, total_sky_cover)
|
380 |
-
|
381 |
-
# Calculate heat flux using CTF (q_t = Σ X_i * T_sol_air(t-i) + Σ Z_i * T_in(t-i) - Σ F_i * q(t-i))
|
382 |
-
num_ctf = len(ctf.X)
|
383 |
-
q_t = 0.0
|
384 |
-
for i in range(num_ctf):
|
385 |
-
# Assume T_sol_air and T_in are constant for history (simplified, extend with history buffer if needed)
|
386 |
-
q_t += ctf.X[i] * T_sol_air + ctf.Z[i] * T_in
|
387 |
-
# Flux history (F) requires previous q values; assume zero for first calculation
|
388 |
-
q_t -= ctf.F[i] * 0.0 # Placeholder, implement history buffer for accurate F terms
|
389 |
-
|
390 |
-
logger.info(f"Calculated heat flux for component '{component.get('name', 'Unknown')}' at "
|
391 |
-
f"{hourly_data.get('month')}/{hourly_data.get('day')}/{hourly_data.get('hour')}: "
|
392 |
-
f"{q_t:.2f} W/m² (T_sol_air={T_sol_air:.2f}°C)")
|
393 |
-
return q_t
|
|
|
2 |
CTF Calculations Module
|
3 |
|
4 |
This module contains the CTFCalculator class for calculating Conduction Transfer Function (CTF)
|
5 |
+
coefficients for HVAC load calculations using the implicit Finite Difference Method, enhanced
|
6 |
+
with sol-air temperature calculations accounting for solar radiation, longwave radiation, and
|
7 |
+
dynamic outdoor heat transfer coefficient.
|
8 |
|
9 |
Developed by: Dr Majed Abuseif, Deakin University
|
10 |
© 2025
|
|
|
18 |
import threading
|
19 |
from typing import List, Dict, Any, NamedTuple
|
20 |
import streamlit as st
|
21 |
+
from enum import Enum
|
|
|
22 |
|
23 |
# Configure logging
|
24 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
25 |
logger = logging.getLogger(__name__)
|
26 |
|
27 |
+
class ComponentType(Enum):
|
28 |
+
WALL = "Wall"
|
29 |
+
ROOF = "Roof"
|
30 |
+
FLOOR = "Floor"
|
31 |
+
WINDOW = "Window"
|
32 |
+
SKYLIGHT = "Skylight"
|
33 |
+
|
34 |
class CTFCoefficients(NamedTuple):
|
35 |
X: List[float] # Exterior temperature coefficients
|
36 |
Y: List[float] # Cross coefficients
|
|
|
44 |
_ctf_cache = {}
|
45 |
_cache_lock = threading.Lock() # Thread-safe lock for cache access
|
46 |
|
47 |
+
@staticmethod
|
48 |
+
def calculate_sky_temperature(T_out: float, dew_point: float, total_sky_cover: float = 0.5) -> float:
|
49 |
+
"""Calculate sky temperature using cloud-cover-dependent model.
|
50 |
+
|
51 |
+
Args:
|
52 |
+
T_out (float): Outdoor dry-bulb temperature (°C).
|
53 |
+
dew_point (float): Dew point temperature (°C).
|
54 |
+
total_sky_cover (float): Sky cover fraction (0 to 1).
|
55 |
+
|
56 |
+
Returns:
|
57 |
+
float: Sky temperature (°C), bounded by dew point.
|
58 |
+
|
59 |
+
References:
|
60 |
+
ASHRAE Handbook—Fundamentals (2021), Chapter 26.
|
61 |
+
"""
|
62 |
+
epsilon_sky = 0.9 + 0.04 * total_sky_cover
|
63 |
+
T_sky = (epsilon_sky * (T_out + 273.15)**4)**0.25 - 273.15
|
64 |
+
return T_sky if dew_point <= T_out else dew_point
|
65 |
+
|
66 |
+
@staticmethod
|
67 |
+
def calculate_h_o(wind_speed: float, surface_type: ComponentType) -> float:
|
68 |
+
"""Calculate dynamic outdoor heat transfer coefficient based on wind speed and surface type.
|
69 |
+
|
70 |
+
Args:
|
71 |
+
wind_speed (float): Wind speed (m/s).
|
72 |
+
surface_type (ComponentType): Type of surface (WALL, ROOF, FLOOR, WINDOW, SKYLIGHT).
|
73 |
+
|
74 |
+
Returns:
|
75 |
+
float: Outdoor heat transfer coefficient (W/m²·K).
|
76 |
+
|
77 |
+
References:
|
78 |
+
ASHRAE Handbook—Fundamentals (2021), Chapter 26.
|
79 |
+
"""
|
80 |
+
from app.m_c_data import DEFAULT_WINDOW_PROPERTIES # Delayed import to avoid circular dependency
|
81 |
+
wind_speed = max(min(wind_speed, 20.0), 0.0) # Bound for stability
|
82 |
+
if surface_type in [ComponentType.WALL, ComponentType.FLOOR]:
|
83 |
+
h_o = 8.3 + 4.0 * (wind_speed ** 0.6) # ASHRAE Ch. 26
|
84 |
+
elif surface_type == ComponentType.ROOF:
|
85 |
+
h_o = 9.1 + 2.8 * wind_speed # ASHRAE Ch. 26
|
86 |
+
else: # WINDOW, SKYLIGHT
|
87 |
+
h_o = DEFAULT_WINDOW_PROPERTIES["h_o"]
|
88 |
+
return max(h_o, 5.0) # Minimum for stability
|
89 |
+
|
90 |
+
@staticmethod
|
91 |
+
def calculate_sol_air_temperature(T_out: float, I_t: float, absorptivity: float, emissivity: float,
|
92 |
+
h_o: float, dew_point: float, total_sky_cover: float = 0.5) -> float:
|
93 |
+
"""Calculate sol-air temperature for a surface.
|
94 |
+
|
95 |
+
Args:
|
96 |
+
T_out (float): Outdoor dry-bulb temperature (°C).
|
97 |
+
I_t (float): Total incident solar radiation (W/m²).
|
98 |
+
absorptivity (float): Surface absorptivity.
|
99 |
+
emissivity (float): Surface emissivity.
|
100 |
+
h_o (float): Outdoor heat transfer coefficient (W/m²·K).
|
101 |
+
dew_point (float): Dew point temperature (°C).
|
102 |
+
total_sky_cover (float): Sky cover fraction (0 to 1).
|
103 |
+
|
104 |
+
Returns:
|
105 |
+
float: Sol-air temperature (°C).
|
106 |
+
|
107 |
+
References:
|
108 |
+
ASHRAE Handbook—Fundamentals (2021), Chapter 26.
|
109 |
+
"""
|
110 |
+
sigma = 5.67e-8 # Stefan-Boltzmann constant (W/m²·K⁴)
|
111 |
+
T_sky = CTFCalculator.calculate_sky_temperature(T_out, dew_point, total_sky_cover)
|
112 |
+
T_sol_air = T_out + (absorptivity * I_t - emissivity * sigma * ((T_out + 273.15)**4 - (T_sky + 273.15)**4)) / h_o
|
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.
|
|
|
163 |
return {}
|
164 |
|
165 |
@classmethod
|
166 |
+
def calculate_ctf_coefficients(cls, component: Dict[str, Any], hourly_data: Dict[str, Any] = None) -> CTFCoefficients:
|
167 |
+
"""Calculate CTF coefficients using implicit Finite Difference Method with sol-air temperature.
|
168 |
|
169 |
Note: Per ASHRAE, CTF calculations are skipped for WINDOW and SKYLIGHT components,
|
170 |
as they use typical material properties. CTF tables for these components will be added later.
|
171 |
|
172 |
Args:
|
173 |
component: Dictionary containing component properties from st.session_state.project_data["components"].
|
174 |
+
hourly_data: Dictionary containing hourly weather data (T_out, dew_point, wind_speed, total_sky_cover, I_t).
|
175 |
|
176 |
Returns:
|
177 |
CTFCoefficients: Named tuple containing X, Y, Z, and F coefficients.
|
|
|
239 |
rho = [m['density'] for m in material_props] # kg/m³
|
240 |
c = [m['specific_heat'] for m in material_props] # J/kg·K
|
241 |
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)
|
242 |
+
absorptivity = material_props[0].get('absorptivity', 0.6) # Use first layer's absorptivity
|
243 |
+
emissivity = material_props[0].get('emissivity', 0.9) # Use first layer's emissivity
|
244 |
|
245 |
# Discretization parameters
|
246 |
dt = 3600 # 1-hour time step (s)
|
247 |
nodes_per_layer = 3 # 2–4 nodes per layer for balance
|
|
|
248 |
R_in = 0.12 # Indoor surface resistance (m²·K/W, ASHRAE)
|
249 |
|
250 |
+
# Get weather data for sol-air temperature
|
251 |
+
T_out = hourly_data.get('dry_bulb', 25.0) if hourly_data else 25.0
|
252 |
+
dew_point = hourly_data.get('dew_point', T_out - 5.0) if hourly_data else T_out - 5.0
|
253 |
+
wind_speed = hourly_data.get('wind_speed', 4.0) if hourly_data else 4.0
|
254 |
+
total_sky_cover = hourly_data.get('total_sky_cover', 0.5) if hourly_data else 0.5
|
255 |
+
I_t = hourly_data.get('total_incident_radiation', 0.0) if hourly_data else 0.0
|
256 |
+
|
257 |
+
# Calculate dynamic h_o and sol-air temperature
|
258 |
+
h_o = cls.calculate_h_o(wind_speed, component_type)
|
259 |
+
T_sol_air = cls.calculate_sol_air_temperature(T_out, I_t, absorptivity, emissivity, h_o, dew_point, total_sky_cover)
|
260 |
+
R_out = 1.0 / h_o # Outdoor surface resistance based on dynamic h_o
|
261 |
+
|
262 |
+
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')}'")
|
263 |
+
|
264 |
# Calculate node spacing and check stability
|
265 |
total_nodes = sum(nodes_per_layer for _ in thicknesses)
|
266 |
dx = [t / nodes_per_layer for t in thicknesses] # Node spacing per layer
|
|
|
303 |
if node_j == 0 and layer_idx == 0: # Outdoor surface node
|
304 |
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)
|
305 |
A[idx, idx + 1] = -2 * dt * k_i / (dx_i * rho_i * c_i * dx_i)
|
306 |
+
b[idx] = dt / (rho_i * c_i * dx_i * R_out) * T_sol_air # Use sol-air temperature
|
307 |
elif node_j == nodes_per_layer - 1 and layer_idx == len(thicknesses) - 1: # Indoor surface node
|
308 |
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)
|
309 |
A[idx, idx - 1] = -2 * dt * k_i / (dx_i * rho_i * c_i * dx_i)
|
|
|
349 |
for t in range(num_ctf):
|
350 |
b_out = b.copy()
|
351 |
if t == 0:
|
352 |
+
b_out[0] = dt / (rho[0] * c[0] * dx[0] * R_out) * T_sol_air if rho[0] * c[0] * dx[0] != 0 else 0.0 # Unit outdoor temp impulse with sol-air
|
353 |
T = sparse_linalg.spsolve(A, b_out + T_prev)
|
354 |
q_in = (T[-1] - 0.0) / R_in # Indoor heat flux (W/m²)
|
355 |
Y[t] = q_in
|
|
|
396 |
CTFCoefficients: Placeholder zero coefficients until implementation.
|
397 |
"""
|
398 |
logger.info(f"CTF table calculation for {component.get('type', 'Unknown')} component '{component.get('name', 'Unknown')}' not yet implemented. Returning zero coefficients.")
|
399 |
+
return CTFCoefficients(X=[0.0], Y=[0.0], Z=[0.0], F=[0.0])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|