mabuseif commited on
Commit
a9c8feb
·
verified ·
1 Parent(s): 7223797

Upload ctf_calculations.py

Browse files
Files changed (1) hide show
  1. utils/ctf_calculations.py +230 -0
utils/ctf_calculations.py ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
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
9
+ """
10
+
11
+ import numpy as np
12
+ import scipy.sparse as sparse
13
+ import scipy.sparse.linalg as sparse_linalg
14
+ import hashlib
15
+ import logging
16
+ import threading
17
+ from typing import List
18
+ from data.material_library import Construction
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')
24
+ logger = logging.getLogger(__name__)
25
+
26
+ class ComponentType(Enum):
27
+ WALL = "Wall"
28
+ ROOF = "Roof"
29
+ FLOOR = "Floor"
30
+ WINDOW = "Window"
31
+ DOOR = "Door"
32
+ SKYLIGHT = "Skylight"
33
+
34
+ class CTFCoefficients(NamedTuple):
35
+ X: List[float] # Exterior temperature coefficients
36
+ Y: List[float] # Cross coefficients
37
+ Z: List[float] # Interior temperature coefficients
38
+ F: List[float] # Flux history coefficients
39
+
40
+ class CTFCalculator:
41
+ """Class to calculate and cache CTF coefficients for building components."""
42
+
43
+ # Cache for CTF coefficients based on construction properties
44
+ _ctf_cache = {}
45
+ _cache_lock = threading.Lock() # Thread-safe lock for cache access
46
+
47
+ @staticmethod
48
+ def _hash_construction(construction: Construction) -> str:
49
+ """Generate a unique hash for a construction based on its properties.
50
+
51
+ Args:
52
+ construction: Construction object containing material layers.
53
+
54
+ Returns:
55
+ str: SHA-256 hash of the construction properties.
56
+ """
57
+ hash_input = f"{construction.name}"
58
+ for layer in construction.layers:
59
+ material = layer["material"]
60
+ hash_input += f"{material.name}{material.conductivity}{material.density}{material.specific_heat}{layer['thickness']}"
61
+ return hashlib.sha256(hash_input.encode()).hexdigest()
62
+
63
+ @classmethod
64
+ def calculate_ctf_coefficients(cls, component) -> CTFCoefficients:
65
+ """Calculate CTF coefficients using implicit Finite Difference Method.
66
+
67
+ Note: Per ASHRAE, CTF calculations are skipped for WINDOW, DOOR, and SKYLIGHT components,
68
+ as they use typical material properties. CTF tables for these components will be added later.
69
+
70
+ Args:
71
+ component: Building component with construction properties.
72
+
73
+ Returns:
74
+ CTFCoefficients: Named tuple containing X, Y, Z, and F coefficients.
75
+ """
76
+ # Skip CTF for WINDOW, DOOR, SKYLIGHT as per ASHRAE; return zero coefficients
77
+ if component.component_type in [ComponentType.WINDOW, ComponentType.DOOR, ComponentType.SKYLIGHT]:
78
+ logger.info(f"Skipping CTF calculation for {component.component_type.value} component '{component.name}'. Using zero coefficients until CTF tables are implemented.")
79
+ return CTFCoefficients(X=[0.0], Y=[0.0], Z=[0.0], F=[0.0])
80
+
81
+ # Check if construction exists and has layers
82
+ construction = component.construction
83
+ if not construction or not construction.layers:
84
+ logger.warning(f"No valid construction or layers for component '{component.name}' ({component.component_type.value}). Returning zero CTFs.")
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 {construction.name}")
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
111
+ node_positions = []
112
+ node_idx = 0
113
+ for i, t in enumerate(thicknesses):
114
+ for j in range(nodes_per_layer):
115
+ node_positions.append((i, j, node_idx)) # (layer_idx, node_in_layer, global_node_idx)
116
+ node_idx += 1
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} ({materials[i].name}). Adjusting node spacing.")
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
126
+ Fo = a * dt / (dx[i] ** 2)
127
+ logger.info(f"Adjusted node spacing for layer {i}: dx={dx[i]:.4f} m, Fo={Fo:.3f}")
128
+
129
+ # Build system matrices
130
+ A = sparse.lil_matrix((total_nodes, total_nodes))
131
+ b = np.zeros(total_nodes)
132
+ node_to_layer = [i for i, _, _ in node_positions]
133
+
134
+ for idx, (layer_idx, node_j, global_idx) in enumerate(node_positions):
135
+ k_i = k[layer_idx]
136
+ rho_i = rho[layer_idx]
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)
143
+ b[idx] = dt / (rho_i * c_i * dx_i * R_out) # Outdoor temp contribution
144
+ elif node_j == nodes_per_layer - 1 and layer_idx == len(thicknesses) - 1: # Indoor surface node
145
+ 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)
146
+ A[idx, idx - 1] = -2 * dt * k_i / (dx_i * rho_i * c_i * dx_i)
147
+ b[idx] = dt / (rho_i * c_i * dx_i * R_in) # Indoor temp contribution
148
+ elif node_j == nodes_per_layer - 1 and layer_idx < len(thicknesses) - 1: # Interface between layers
149
+ k_next = k[layer_idx + 1]
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))
156
+ elif node_j == 0 and layer_idx > 0: # Interface from previous layer
157
+ k_prev = k[layer_idx - 1]
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))
164
+ else: # Internal node
165
+ A[idx, idx] = 1.0 + 2 * dt * k_i / (dx_i * rho_i * c_i * dx_i)
166
+ A[idx, idx - 1] = -dt * k_i / (dx_i * rho_i * c_i * dx_i)
167
+ A[idx, idx + 1] = -dt * k_i / (dx_i * rho_i * c_i * dx_i)
168
+
169
+ A = A.tocsr() # Convert to CSR for efficient solving
170
+
171
+ # Calculate CTF coefficients (X, Y, Z, F)
172
+ num_ctf = 12 # Standard number of coefficients
173
+ X = [0.0] * num_ctf # Exterior temp response
174
+ Y = [0.0] * num_ctf # Cross response
175
+ Z = [0.0] * num_ctf # Interior temp response
176
+ F = [0.0] * num_ctf # Flux history
177
+ T_prev = np.zeros(total_nodes) # Previous temperatures
178
+
179
+ # Impulse response for exterior temperature (X, Y)
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
187
+ q_out = (0.0 - T[0]) / R_out # Outdoor heat flux
188
+ X[t] = q_out
189
+ T_prev = T.copy()
190
+
191
+ # Reset for interior temperature (Z)
192
+ T_prev = np.zeros(total_nodes)
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
200
+ T_prev = T.copy()
201
+
202
+ # Flux history coefficients (F)
203
+ T_prev = np.zeros(total_nodes)
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
211
+ T_prev = T.copy()
212
+
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 {construction.name}")
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, doors, and skylights.
222
+
223
+ Args:
224
+ component: Building component with construction properties.
225
+
226
+ Returns:
227
+ CTFCoefficients: Placeholder zero coefficients until implementation.
228
+ """
229
+ logger.info(f"CTF table calculation for {component.component_type.value} component '{component.name}' not yet implemented. Returning zero coefficients.")
230
+ return CTFCoefficients(X=[0.0], Y=[0.0], Z=[0.0], F=[0.0])