mabuseif commited on
Commit
28bd938
·
verified ·
1 Parent(s): 4944117

Update data/calculation.py

Browse files
Files changed (1) hide show
  1. data/calculation.py +5 -182
data/calculation.py CHANGED
@@ -11,193 +11,16 @@ from typing import Dict, List, Optional, NamedTuple
11
  from enum import Enum
12
  from data.material_library import Construction, GlazingMaterial, DoorMaterial
13
  from data.internal_loads import PEOPLE_ACTIVITY_LEVELS, DIVERSITY_FACTORS, LIGHTING_FIXTURE_TYPES, EQUIPMENT_HEAT_GAINS, VENTILATION_RATES, INFILTRATION_SETTINGS
14
- import scipy.linalg as linalg
15
- import scipy.sparse as sparse
16
- import scipy.sparse.linalg as sparse_linalg
17
  from datetime import datetime
18
  from collections import defaultdict
19
- import hashlib
20
  import logging
 
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 TFMCalculations:
41
- # Cache for CTF coefficients based on construction properties
42
- _ctf_cache = {}
43
-
44
- @staticmethod
45
- def _hash_construction(construction: Construction) -> str:
46
- """Generate a unique hash for a construction based on its properties."""
47
- hash_input = f"{construction.name}"
48
- for layer in construction.layers:
49
- material = layer["material"]
50
- hash_input += f"{material.name}{material.conductivity}{material.density}{material.specific_heat}{layer['thickness']}"
51
- return hashlib.sha256(hash_input.encode()).hexdigest()
52
-
53
- @staticmethod
54
- def calculate_ctf_coefficients(component) -> CTFCoefficients:
55
- """Calculate CTF coefficients using implicit Finite Difference Method.
56
-
57
- Note: Per ASHRAE, CTF calculations are skipped for WINDOW, DOOR, and SKYLIGHT components,
58
- as they use typical material properties. CTF tables for these components will be added later.
59
- """
60
- # Skip CTF for WINDOW, DOOR, SKYLIGHT as per ASHRAE; return zero coefficients
61
- if component.component_type in [ComponentType.WINDOW, ComponentType.DOOR, ComponentType.SKYLIGHT]:
62
- logger.info(f"Skipping CTF calculation for {component.component_type.value} component '{component.name}'. Using zero coefficients until CTF tables are implemented.")
63
- return CTFCoefficients(X=[0.0], Y=[0.0], Z=[0.0], F=[0.0])
64
-
65
- # Check if construction exists and has layers
66
- construction = component.construction
67
- if not construction or not construction.layers:
68
- logger.warning(f"No valid construction or layers for component '{component.name}' ({component.component_type.value}). Returning zero CTFs.")
69
- return CTFCoefficients(X=[0.0], Y=[0.0], Z=[0.0], F=[0.0])
70
-
71
- # Check cache
72
- construction_hash = TFMCalculations._hash_construction(construction)
73
- if construction_hash in TFMCalculations._ctf_cache:
74
- logger.info(f"Using cached CTF coefficients for construction {construction.name}")
75
- return TFMCalculations._ctf_cache[construction_hash]
76
-
77
- # Discretization parameters
78
- dt = 3600 # 1-hour time step (s)
79
- nodes_per_layer = 3 # 2–4 nodes per layer for balance
80
- R_out = 0.04 # Outdoor surface resistance (m²·K/W, ASHRAE)
81
- R_in = 0.12 # Indoor surface resistance (m²·K/W, ASHRAE)
82
-
83
- # Collect layer properties
84
- thicknesses = [layer["thickness"] for layer in construction.layers]
85
- materials = [layer["material"] for layer in construction.layers]
86
- k = [m.conductivity for m in materials] # W/m·K
87
- rho = [m.density for m in materials] # kg/m³
88
- c = [m.specific_heat for m in materials] # J/kg·K
89
- alpha = [k_i / (rho_i * c_i) for k_i, rho_i, c_i in zip(k, rho, c)] # Thermal diffusivity (m²/s)
90
-
91
- # Calculate node spacing and check stability
92
- total_nodes = sum(nodes_per_layer for _ in thicknesses)
93
- dx = [t / nodes_per_layer for t in thicknesses] # Node spacing per layer
94
- node_positions = []
95
- node_idx = 0
96
- for i, t in enumerate(thicknesses):
97
- for j in range(nodes_per_layer):
98
- node_positions.append((i, j, node_idx)) # (layer_idx, node_in_layer, global_node_idx)
99
- node_idx += 1
100
-
101
- # Stability check: Fourier number
102
- for i, (a, d) in enumerate(zip(alpha, dx)):
103
- Fo = a * dt / (d ** 2)
104
- if Fo < 0.33:
105
- logger.warning(f"Fourier number {Fo:.3f} < 0.33 for layer {i} ({materials[i].name}). Adjusting node spacing.")
106
- dx[i] = np.sqrt(a * dt / 0.33)
107
- nodes_per_layer = max(2, int(np.ceil(thicknesses[i] / dx[i])))
108
- dx[i] = thicknesses[i] / nodes_per_layer
109
- Fo = a * dt / (dx[i] ** 2)
110
- logger.info(f"Adjusted node spacing for layer {i}: dx={dx[i]:.4f} m, Fo={Fo:.3f}")
111
-
112
- # Build system matrices
113
- A = sparse.lil_matrix((total_nodes, total_nodes))
114
- b = np.zeros(total_nodes)
115
- node_to_layer = [i for i, _, _ in node_positions]
116
-
117
- for idx, (layer_idx, node_j, global_idx) in enumerate(node_positions):
118
- k_i = k[layer_idx]
119
- rho_i = rho[layer_idx]
120
- c_i = c[layer_idx]
121
- dx_i = dx[layer_idx]
122
-
123
- if node_j == 0 and layer_idx == 0: # Outdoor surface node
124
- 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)
125
- A[idx, idx + 1] = -2 * dt * k_i / (dx_i * rho_i * c_i * dx_i)
126
- b[idx] = dt / (rho_i * c_i * dx_i * R_out) # Outdoor temp contribution
127
- elif node_j == nodes_per_layer - 1 and layer_idx == len(thicknesses) - 1: # Indoor surface node
128
- 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)
129
- A[idx, idx - 1] = -2 * dt * k_i / (dx_i * rho_i * c_i * dx_i)
130
- b[idx] = dt / (rho_i * c_i * dx_i * R_in) # Indoor temp contribution
131
- elif node_j == nodes_per_layer - 1 and layer_idx < len(thicknesses) - 1: # Interface between layers
132
- k_next = k[layer_idx + 1]
133
- dx_next = dx[layer_idx + 1]
134
- rho_next = rho[layer_idx + 1]
135
- c_next = c[layer_idx + 1]
136
- 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))
137
- A[idx, idx - 1] = -dt * k_i / (dx_i * 0.5 * (rho_i * c_i * dx_i + rho_next * c_next * dx_next))
138
- A[idx, idx + 1] = -dt * k_next / (dx_next * 0.5 * (rho_i * c_i * dx_i + rho_next * c_next * dx_next))
139
- elif node_j == 0 and layer_idx > 0: # Interface from previous layer
140
- k_prev = k[layer_idx - 1]
141
- dx_prev = dx[layer_idx - 1]
142
- rho_prev = rho[layer_idx - 1]
143
- c_prev = c[layer_idx - 1]
144
- 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))
145
- A[idx, idx - 1] = -dt * k_prev / (dx_prev * 0.5 * (rho_prev * c_prev * dx_prev + rho_i * c_i * dx_i))
146
- A[idx, idx + 1] = -dt * k_i / (dx_i * 0.5 * (rho_prev * c_prev * dx_prev + rho_i * c_i * dx_i))
147
- else: # Internal node
148
- A[idx, idx] = 1.0 + 2 * dt * k_i / (dx_i * rho_i * c_i * dx_i)
149
- A[idx, idx - 1] = -dt * k_i / (dx_i * rho_i * c_i * dx_i)
150
- A[idx, idx + 1] = -dt * k_i / (dx_i * rho_i * c_i * dx_i)
151
-
152
- A = A.tocsr() # Convert to CSR for efficient solving
153
-
154
- # Calculate CTF coefficients (X, Y, Z, F)
155
- num_ctf = 12 # Standard number of coefficients
156
- X = [0.0] * num_ctf # Exterior temp response
157
- Y = [0.0] * num_ctf # Cross response
158
- Z = [0.0] * num_ctf # Interior temp response
159
- F = [0.0] * num_ctf # Flux history
160
- T_prev = np.zeros(total_nodes) # Previous temperatures
161
-
162
- # Impulse response for exterior temperature (X, Y)
163
- for t in range(num_ctf):
164
- b_out = b.copy()
165
- if t == 0:
166
- b_out[0] = dt / (rho[0] * c[0] * dx[0] * R_out) # Unit outdoor temp impulse
167
- T = sparse_linalg.spsolve(A, b_out + T_prev)
168
- q_in = (T[-1] - 0.0) / R_in # Indoor heat flux (W/m²)
169
- Y[t] = q_in
170
- q_out = (0.0 - T[0]) / R_out # Outdoor heat flux
171
- X[t] = q_out
172
- T_prev = T.copy()
173
-
174
- # Reset for interior temperature (Z)
175
- T_prev = np.zeros(total_nodes)
176
- for t in range(num_ctf):
177
- b_in = b.copy()
178
- if t == 0:
179
- b_in[-1] = dt / (rho[-1] * c[-1] * dx[-1] * R_in) # Unit indoor temp impulse
180
- T = sparse_linalg.spsolve(A, b_in + T_prev)
181
- q_in = (T[-1] - 0.0) / R_in
182
- Z[t] = q_in
183
- T_prev = T.copy()
184
-
185
- # Flux history coefficients (F)
186
- T_prev = np.zeros(total_nodes)
187
- for t in range(num_ctf):
188
- b_flux = np.zeros(total_nodes)
189
- if t == 0:
190
- b_flux[-1] = -1.0 / (rho[-1] * c[-1] * dx[-1]) # Unit flux impulse
191
- T = sparse_linalg.spsolve(A, b_flux + T_prev)
192
- q_in = (T[-1] - 0.0) / R_in
193
- F[t] = q_in
194
- T_prev = T.copy()
195
-
196
- ctf = CTFCoefficients(X=X, Y=Y, Z=Z, F=F)
197
- TFMCalculations._ctf_cache[construction_hash] = ctf
198
- logger.info(f"Calculated CTF coefficients for construction {construction.name}")
199
- return ctf
200
-
201
  @staticmethod
202
  def calculate_conduction_load(component, outdoor_temp: float, indoor_temp: float, hour: int, mode: str = "none") -> tuple[float, float]:
203
  """Calculate conduction load for heating and cooling in kW based on mode."""
@@ -209,8 +32,8 @@ class TFMCalculations:
209
  if mode == "heating" and delta_t >= 0:
210
  return 0, 0
211
 
212
- # Get CTF coefficients
213
- ctf = TFMCalculations.calculate_ctf_coefficients(component)
214
 
215
  # Initialize history terms (simplified: assume steady-state history for demonstration)
216
  # In practice, maintain temperature and flux histories
@@ -385,10 +208,10 @@ class TFMCalculations:
385
  operating_periods = hvac_settings.get("operating_hours", [{"start": 8, "end": 18}])
386
  area = building_info.get("floor_area", 100.0)
387
 
388
- # Pre-calculate CTF coefficients for all components
389
  for comp_list in components.values():
390
  for comp in comp_list:
391
- comp.ctf = TFMCalculations.calculate_ctf_coefficients(comp)
392
 
393
  for hour_data in filtered_data:
394
  hour = hour_data["hour"]
 
11
  from enum import Enum
12
  from data.material_library import Construction, GlazingMaterial, DoorMaterial
13
  from data.internal_loads import PEOPLE_ACTIVITY_LEVELS, DIVERSITY_FACTORS, LIGHTING_FIXTURE_TYPES, EQUIPMENT_HEAT_GAINS, VENTILATION_RATES, INFILTRATION_SETTINGS
 
 
 
14
  from datetime import datetime
15
  from collections import defaultdict
 
16
  import logging
17
+ from utils.ctf_calculations import CTFCalculator, ComponentType, CTFCoefficients
18
 
19
  # Configure logging
20
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
21
  logger = logging.getLogger(__name__)
22
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  class TFMCalculations:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  @staticmethod
25
  def calculate_conduction_load(component, outdoor_temp: float, indoor_temp: float, hour: int, mode: str = "none") -> tuple[float, float]:
26
  """Calculate conduction load for heating and cooling in kW based on mode."""
 
32
  if mode == "heating" and delta_t >= 0:
33
  return 0, 0
34
 
35
+ # Get CTF coefficients using CTFCalculator
36
+ ctf = CTFCalculator.calculate_ctf_coefficients(component)
37
 
38
  # Initialize history terms (simplified: assume steady-state history for demonstration)
39
  # In practice, maintain temperature and flux histories
 
208
  operating_periods = hvac_settings.get("operating_hours", [{"start": 8, "end": 18}])
209
  area = building_info.get("floor_area", 100.0)
210
 
211
+ # Pre-calculate CTF coefficients for all components using CTFCalculator
212
  for comp_list in components.values():
213
  for comp in comp_list:
214
+ comp.ctf = CTFCalculator.calculate_ctf_coefficients(comp)
215
 
216
  for hour_data in filtered_data:
217
  hour = hour_data["hour"]