modnn 2.0.0__tar.gz → 2.0.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {modnn-2.0.0 → modnn-2.0.2}/PKG-INFO +1 -1
- {modnn-2.0.0 → modnn-2.0.2}/modnn/Config.py +3 -1
- modnn-2.0.2/modnn/Models/ModNN_phy.py +381 -0
- {modnn-2.0.0 → modnn-2.0.2}/modnn/Play.py +4 -1
- {modnn-2.0.0 → modnn-2.0.2}/modnn/utils.py +11 -4
- {modnn-2.0.0 → modnn-2.0.2}/modnn.egg-info/PKG-INFO +1 -1
- {modnn-2.0.0 → modnn-2.0.2}/modnn.egg-info/SOURCES.txt +2 -2
- {modnn-2.0.0 → modnn-2.0.2}/pyproject.toml +1 -1
- modnn-2.0.0/modnn/Models/ModNN.py +0 -402
- {modnn-2.0.0 → modnn-2.0.2}/LICENSE +0 -0
- {modnn-2.0.0 → modnn-2.0.2}/README.md +0 -0
- {modnn-2.0.0 → modnn-2.0.2}/modnn/Dataset.py +0 -0
- {modnn-2.0.0 → modnn-2.0.2}/modnn/Models/BaseNN.py +0 -0
- /modnn-2.0.0/modnn/Models/Modback.py → /modnn-2.0.2/modnn/Models/ModNN_data.py +0 -0
- {modnn-2.0.0 → modnn-2.0.2}/modnn/Models/__init__.py +0 -0
- {modnn-2.0.0 → modnn-2.0.2}/modnn/__init__.py +0 -0
- {modnn-2.0.0 → modnn-2.0.2}/modnn/run.py +0 -0
- {modnn-2.0.0 → modnn-2.0.2}/modnn.egg-info/dependency_links.txt +0 -0
- {modnn-2.0.0 → modnn-2.0.2}/modnn.egg-info/requires.txt +0 -0
- {modnn-2.0.0 → modnn-2.0.2}/modnn.egg-info/top_level.txt +0 -0
- {modnn-2.0.0 → modnn-2.0.2}/setup.cfg +0 -0
|
@@ -66,7 +66,7 @@ def _args(**kwargs):
|
|
|
66
66
|
"envelop": _envelops(**envelop_overrides),
|
|
67
67
|
# Paths and device
|
|
68
68
|
"datapath": "/home/zjiang19/Documents/GitHub/Eplus_ModNN_Compare/dataset/Eplus/EPlus_train_AC_off_2month.csv", #"../Dataset/EPlus.csv",
|
|
69
|
-
"device": "cuda:
|
|
69
|
+
"device": "cuda:0",
|
|
70
70
|
"save_name": "Mid",
|
|
71
71
|
|
|
72
72
|
# Data settings
|
|
@@ -77,6 +77,8 @@ def _args(**kwargs):
|
|
|
77
77
|
"trainday": 180, # Training data selection
|
|
78
78
|
"testday": 1, # Testing data selection
|
|
79
79
|
"training_batch": 1024*1,
|
|
80
|
+
"envelop_mdl": "physics", # We provide "physics" and "datadriven"
|
|
81
|
+
# "physics" rely on heatbalance equation, "datadriven" is a blackbox
|
|
80
82
|
"ext_mdl": "LSTM", # We provide LSTM and RNN module, for RNN, we can apply positive constraint easily
|
|
81
83
|
# But LSTM has Hadamard product, making this constraint hard to integrate
|
|
82
84
|
# However, disturbance variables always have similiar distribution, in other word, is this constraint really necessary?
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
import torch.nn as nn
|
|
3
|
+
import torch.nn.functional as F
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# --- Solar Radiation Module ---
|
|
7
|
+
class SolarRadiation(nn.Module):
|
|
8
|
+
def __init__(self, envelop, name="Solar"):
|
|
9
|
+
super().__init__()
|
|
10
|
+
# Initialization
|
|
11
|
+
self.envelop = envelop
|
|
12
|
+
|
|
13
|
+
# Load transmission and absorption coefficient
|
|
14
|
+
self.direct_gain = nn.Parameter(torch.tensor(0.1, dtype=torch.float32))
|
|
15
|
+
self.abs_wall = nn.Parameter(torch.tensor(0.1, dtype=torch.float32))
|
|
16
|
+
self.abs_roof = nn.Parameter(torch.tensor(0.1, dtype=torch.float32))
|
|
17
|
+
self.name = name
|
|
18
|
+
|
|
19
|
+
def forward(self, radiation, n_wall, n_roof):
|
|
20
|
+
"""
|
|
21
|
+
Calculate solar heat gains [transmission and absorption] in a vectorized way.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
radiation: Solar radiation input [batch, time, 1] or [batch, time, directions]
|
|
25
|
+
(e.g., [batch, time, 4] for E,S,W,N directions)
|
|
26
|
+
n_wall: Number of wall components
|
|
27
|
+
n_roof: Number of roof components
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
direct_gain: Heat directly entering zone
|
|
31
|
+
wall_gains: List of absorbed heat for each wall [n_wall elements]
|
|
32
|
+
roof_gains: List of absorbed heat for each roof [n_roof elements]
|
|
33
|
+
"""
|
|
34
|
+
# Apply softplus to ensure positive coefficients
|
|
35
|
+
direct_coef = F.softplus(self.direct_gain) * self.envelop["direct_coef"]
|
|
36
|
+
abs_wall_coef = F.softplus(self.abs_wall) * self.envelop["abs_wall_coef"]
|
|
37
|
+
abs_roof_coef = F.softplus(self.abs_roof) * self.envelop["abs_roof_coef"]
|
|
38
|
+
|
|
39
|
+
# Transmission solar gain
|
|
40
|
+
direct_gain = direct_coef * radiation
|
|
41
|
+
|
|
42
|
+
# Absorption solar gain
|
|
43
|
+
# Wall
|
|
44
|
+
batch_size, time_steps = radiation.shape[0], radiation.shape[1]
|
|
45
|
+
if radiation.shape[-1] > 1: # TODO: add solar angle model to distribute solar radiations
|
|
46
|
+
# Handle multi-directional radiation
|
|
47
|
+
wall_gains = []
|
|
48
|
+
for i in range(n_wall):
|
|
49
|
+
dir_idx = min(i, radiation.shape[-1] - 1)
|
|
50
|
+
wall_gain = abs_wall_coef * radiation[:, :, dir_idx:dir_idx + 1]
|
|
51
|
+
wall_gains.append(wall_gain)
|
|
52
|
+
else:
|
|
53
|
+
wall_gain = abs_wall_coef * radiation
|
|
54
|
+
wall_gains = [wall_gain] * n_wall
|
|
55
|
+
|
|
56
|
+
# Roof
|
|
57
|
+
roof_gain = abs_roof_coef * radiation
|
|
58
|
+
roof_gains = [roof_gain] * n_roof
|
|
59
|
+
|
|
60
|
+
return direct_gain, wall_gains, roof_gains
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# --- Envelope Module [RC, R] ---
|
|
64
|
+
class EnvelopeRC(nn.Module):
|
|
65
|
+
def __init__(self, envelop, n_wall, n_roof, n_window):
|
|
66
|
+
super().__init__()
|
|
67
|
+
self.n_wall = n_wall
|
|
68
|
+
self.n_roof = n_roof
|
|
69
|
+
self.n_window = n_window
|
|
70
|
+
self.n_rc = n_wall + n_roof
|
|
71
|
+
self.envelop = envelop
|
|
72
|
+
|
|
73
|
+
# Initialization
|
|
74
|
+
wall_r_init = torch.tensor([1.0 / (10.0 + i * 1.0) for i in range(n_wall)], dtype=torch.float32)
|
|
75
|
+
roof_r_init = torch.tensor([1.0 / (20.0 + i * 2.0) for i in range(n_roof)], dtype=torch.float32)
|
|
76
|
+
self.rc_R_inv = nn.Parameter(torch.cat([wall_r_init, roof_r_init]))
|
|
77
|
+
|
|
78
|
+
wall_c_init = torch.tensor([1.0 / (80.0 + i * 10.0) for i in range(n_wall)], dtype=torch.float32)
|
|
79
|
+
roof_c_init = torch.tensor([1.0 / (60.0 + i * 10.0) for i in range(n_roof)], dtype=torch.float32)
|
|
80
|
+
self.rc_C_inv = nn.Parameter(torch.cat([wall_c_init, roof_c_init]))
|
|
81
|
+
|
|
82
|
+
if n_window > 0:
|
|
83
|
+
window_r_init = torch.tensor([1.0 / (1.0 + i * 0.1) for i in range(n_window)], dtype=torch.float32)
|
|
84
|
+
self.window_R_inv = nn.Parameter(window_r_init)
|
|
85
|
+
|
|
86
|
+
def forward(self, T_amb, Tmid_list, Tzone, solar_gains_wall, solar_gains_roof):
|
|
87
|
+
"""
|
|
88
|
+
Envelope heat transfer calculation
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
T_amb: Ambient temperature [batch, time, 1]
|
|
92
|
+
Tmid_list: List of envelope temperatures [n_rc elements each [batch, time, 1]]
|
|
93
|
+
Tzone: Zone temperature [batch, time, 1]
|
|
94
|
+
solar_gains_wall: List of wall solar gains [n_wall elements each [batch, time, 1]]
|
|
95
|
+
solar_gains_roof: List of roof solar gains [n_roof elements each [batch, time, 1]]
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
dTmid_dt_list: List of RC temperature derivatives
|
|
99
|
+
q_env_total: Total heat flow to zone
|
|
100
|
+
"""
|
|
101
|
+
batch_size = T_amb.shape[0]
|
|
102
|
+
|
|
103
|
+
# Stack all envelope temperatures into one tensor [batch, time, n_rc]
|
|
104
|
+
Tmid_stacked = torch.cat(Tmid_list, dim=2)
|
|
105
|
+
|
|
106
|
+
# Expand ambient and zone temperatures for broadcasting
|
|
107
|
+
T_amb_expanded = T_amb.expand(-1, -1, self.n_rc)
|
|
108
|
+
Tzone_expanded = Tzone.expand(-1, -1, self.n_rc)
|
|
109
|
+
|
|
110
|
+
# Apply softplus to parameters and expand for broadcasting
|
|
111
|
+
r_inv_bounded = F.softplus(self.rc_R_inv) * (1.0 / self.envelop["r_opaque_coef"])
|
|
112
|
+
c_inv_bounded = F.softplus(self.rc_C_inv) * (1.0 / self.envelop["c_opaque_coef"])
|
|
113
|
+
r_inv_expanded = r_inv_bounded.view(1, 1, -1).expand(batch_size, T_amb.shape[1], -1)
|
|
114
|
+
c_inv_expanded = c_inv_bounded.view(1, 1, -1).expand(batch_size, T_amb.shape[1], -1)
|
|
115
|
+
|
|
116
|
+
# Energy balance constraints
|
|
117
|
+
q_in = (T_amb_expanded - Tmid_stacked) * r_inv_expanded
|
|
118
|
+
q_out = (Tmid_stacked - Tzone_expanded) * r_inv_expanded
|
|
119
|
+
|
|
120
|
+
# Combine solar gains into single tensor with same shape as Tmid_stacked
|
|
121
|
+
# Add solar radiation to the first capacitance node
|
|
122
|
+
solar_gains = []
|
|
123
|
+
for i in range(self.n_wall):
|
|
124
|
+
solar_gains.append(solar_gains_wall[i])
|
|
125
|
+
for i in range(self.n_roof):
|
|
126
|
+
solar_gains.append(solar_gains_roof[i])
|
|
127
|
+
|
|
128
|
+
solar_stacked = torch.cat(solar_gains, dim=2) if solar_gains else torch.zeros_like(Tmid_stacked)
|
|
129
|
+
|
|
130
|
+
# Calculate temperature derivatives
|
|
131
|
+
dTmid_dt_stacked = (q_in - q_out + solar_stacked) * c_inv_expanded
|
|
132
|
+
|
|
133
|
+
# Window heat transfer (Assume no storage capacity)
|
|
134
|
+
q_window_total = 0
|
|
135
|
+
if self.n_window > 0:
|
|
136
|
+
window_r_inv_bounded = F.softplus(self.window_R_inv) * (1.0 / self.envelop["r_transparent_coef"])
|
|
137
|
+
window_r_inv_expanded = window_r_inv_bounded.view(1, 1, -1).expand(batch_size, T_amb.shape[1], -1)
|
|
138
|
+
T_amb_window = T_amb.expand(-1, -1, self.n_window)
|
|
139
|
+
Tzone_window = Tzone.expand(-1, -1, self.n_window)
|
|
140
|
+
q_window = (T_amb_window - Tzone_window) * window_r_inv_expanded
|
|
141
|
+
q_window_total = torch.sum(q_window, dim=2, keepdim=True)
|
|
142
|
+
|
|
143
|
+
# Total flow calculation
|
|
144
|
+
q_rc_total = torch.sum(q_out, dim=2, keepdim=True)
|
|
145
|
+
q_env_total = q_rc_total + q_window_total
|
|
146
|
+
|
|
147
|
+
# Split to each state node
|
|
148
|
+
dTmid_dt_list = torch.split(dTmid_dt_stacked, 1, dim=2)
|
|
149
|
+
|
|
150
|
+
return dTmid_dt_list, q_env_total
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class ModNN(nn.Module):
|
|
154
|
+
def __init__(self, args):
|
|
155
|
+
super().__init__()
|
|
156
|
+
para = args["para"]
|
|
157
|
+
envelop = args["envelop"]
|
|
158
|
+
self.enc_len = args["enLen"]
|
|
159
|
+
self.device = args["device"]
|
|
160
|
+
self.delta_t = 900 # 15 minutes in seconds
|
|
161
|
+
|
|
162
|
+
# Model configuration
|
|
163
|
+
self.n_wall = envelop.get("n_wall", 0)
|
|
164
|
+
self.n_roof = envelop.get("n_roof", 0)
|
|
165
|
+
self.n_window = envelop.get("n_window", 0)
|
|
166
|
+
|
|
167
|
+
print(f"Initializing Vectorized ModNN with "
|
|
168
|
+
f"{self.n_wall} walls, "
|
|
169
|
+
f"{self.n_roof} roofs, "
|
|
170
|
+
f"{self.n_window} windows")
|
|
171
|
+
|
|
172
|
+
# Modules Assemble
|
|
173
|
+
self.Int = Internal(para["Int_in"], para["Int_h"], para["Int_out"])
|
|
174
|
+
self.HVAC = HVAC()
|
|
175
|
+
self.solar = SolarRadiation(envelop=envelop)
|
|
176
|
+
self.envelopes = EnvelopeRC(envelop, self.n_wall, self.n_roof, self.n_window)
|
|
177
|
+
self.zone_update = ZoneUpdate(envelop=envelop)
|
|
178
|
+
|
|
179
|
+
def forward(self, input_X):
|
|
180
|
+
"""
|
|
181
|
+
input_X order: [T_zone, T_ambient, solar, day_sin, day_cos, occ, phvac]
|
|
182
|
+
Shape: [batch_size, time_steps, features]
|
|
183
|
+
"""
|
|
184
|
+
batch_size = input_X.shape[0]
|
|
185
|
+
time_steps = input_X.shape[1]
|
|
186
|
+
|
|
187
|
+
# Extract input data
|
|
188
|
+
T_amb = input_X[:, :, [1]] # Ambient air temperature
|
|
189
|
+
Solar_X = input_X[:, :, [2]] # Solar radiation
|
|
190
|
+
Int_X = input_X[:, :, [3, 4, 5]] # Time_sin/cos, Occ
|
|
191
|
+
HVAC_X = input_X[:, :, [6]] # pHVAC
|
|
192
|
+
Tzone_gt = input_X[:, :, [0]] # Zone air temperature groundtruth
|
|
193
|
+
|
|
194
|
+
# Pre-allocate results arrays
|
|
195
|
+
Tzone_pred = torch.zeros(batch_size, time_steps, 1, device=input_X.device)
|
|
196
|
+
q_int_pred = torch.zeros(batch_size, time_steps, 1, device=input_X.device)
|
|
197
|
+
q_hvac_pred = torch.zeros(batch_size, time_steps, 1, device=input_X.device)
|
|
198
|
+
q_env_pred = torch.zeros(batch_size, time_steps, 1, device=input_X.device)
|
|
199
|
+
q_solar_direct_pred = torch.zeros(batch_size, time_steps, 1, device=input_X.device)
|
|
200
|
+
|
|
201
|
+
# Pre-compute internal and HVAC
|
|
202
|
+
q_int_pred = self.Int(Int_X)
|
|
203
|
+
q_hvac_pred = self.HVAC(HVAC_X)
|
|
204
|
+
|
|
205
|
+
# Initialize zone temperature
|
|
206
|
+
Tzone = Tzone_gt[:, 0:1, :]
|
|
207
|
+
|
|
208
|
+
# Initialize envelope temperatures
|
|
209
|
+
Tmid = []
|
|
210
|
+
for _ in range(self.n_wall + self.n_roof):
|
|
211
|
+
tmid_init = 0.7 * Tzone + 0.3 * T_amb[:, 0:1, :]
|
|
212
|
+
Tmid.append(tmid_init)
|
|
213
|
+
|
|
214
|
+
# Pre-allocate envelope temperature histories
|
|
215
|
+
Tmid_hist = [torch.zeros(batch_size, time_steps, 1, device=input_X.device)
|
|
216
|
+
for _ in range(self.n_wall + self.n_roof)]
|
|
217
|
+
|
|
218
|
+
# --- Encoder phase ---
|
|
219
|
+
for i in range(self.enc_len):
|
|
220
|
+
# The purpose is to stablize the initial envelop temperatures so we use ground truth temperature
|
|
221
|
+
Tzone_pred[:, i:i + 1, :] = Tzone_gt[:, i:i + 1, :]
|
|
222
|
+
Tzone_i = Tzone_gt[:, i:i + 1, :]
|
|
223
|
+
|
|
224
|
+
# Get inputs for this timestep
|
|
225
|
+
T_amb_i = T_amb[:, i:i + 1, :]
|
|
226
|
+
Solar_i = Solar_X[:, i:i + 1, :]
|
|
227
|
+
|
|
228
|
+
# Calculate solar distribution
|
|
229
|
+
q_solar_direct, q_solar_wall, q_solar_roof = self.solar(
|
|
230
|
+
Solar_i, self.n_wall, self.n_roof
|
|
231
|
+
)
|
|
232
|
+
q_solar_direct_pred[:, i:i + 1, :] = q_solar_direct
|
|
233
|
+
|
|
234
|
+
# Envelope heat transfer calculation
|
|
235
|
+
dTmid_dt_list, q_env_total = self.envelopes(
|
|
236
|
+
T_amb_i, Tmid, Tzone_i, q_solar_wall, q_solar_roof
|
|
237
|
+
)
|
|
238
|
+
q_env_pred[:, i:i + 1, :] = q_env_total
|
|
239
|
+
|
|
240
|
+
# Envelope temperatures update
|
|
241
|
+
# Use RK4 if want higher accuracy
|
|
242
|
+
for idx, dTmid_dt in enumerate(dTmid_dt_list):
|
|
243
|
+
# Clamp derivatives for stability
|
|
244
|
+
dTmid_dt_clamped = torch.clamp(dTmid_dt, -3.0 / self.delta_t, 3.0 / self.delta_t)
|
|
245
|
+
delta = torch.clamp(self.delta_t * dTmid_dt_clamped, -3.0, 3.0)
|
|
246
|
+
Tmid[idx] = Tmid[idx] + delta
|
|
247
|
+
Tmid_hist[idx][:, i:i + 1, :] = Tmid[idx]
|
|
248
|
+
|
|
249
|
+
# --- Decoder phase (this is the actual prediction stage) ---
|
|
250
|
+
Tzone = Tzone_gt[:, self.enc_len - 1:self.enc_len, :]
|
|
251
|
+
|
|
252
|
+
for i in range(time_steps - self.enc_len):
|
|
253
|
+
idx = self.enc_len + i
|
|
254
|
+
|
|
255
|
+
# Initialize
|
|
256
|
+
T_amb_i = T_amb[:, idx:idx + 1, :]
|
|
257
|
+
Solar_i = Solar_X[:, idx:idx + 1, :]
|
|
258
|
+
q_int_i = q_int_pred[:, idx:idx + 1, :]
|
|
259
|
+
q_hvac_i = q_hvac_pred[:, idx:idx + 1, :]
|
|
260
|
+
|
|
261
|
+
# Calculate solar distribution
|
|
262
|
+
q_solar_direct, q_solar_wall, q_solar_roof = self.solar(
|
|
263
|
+
Solar_i, self.n_wall, self.n_roof
|
|
264
|
+
)
|
|
265
|
+
q_solar_direct_pred[:, idx:idx + 1, :] = q_solar_direct
|
|
266
|
+
|
|
267
|
+
# Envelope heat transfer calculation
|
|
268
|
+
dTmid_dt_list, q_env_total = self.envelopes(
|
|
269
|
+
T_amb_i, Tmid, Tzone, q_solar_wall, q_solar_roof
|
|
270
|
+
)
|
|
271
|
+
q_env_pred[:, idx:idx + 1, :] = q_env_total
|
|
272
|
+
|
|
273
|
+
# Envelope temperatures update
|
|
274
|
+
for j, dTmid_dt in enumerate(dTmid_dt_list):
|
|
275
|
+
# Clamp derivatives for stability
|
|
276
|
+
dTmid_dt_clamped = torch.clamp(dTmid_dt, -3.0 / self.delta_t, 3.0 / self.delta_t)
|
|
277
|
+
delta = torch.clamp(self.delta_t * dTmid_dt_clamped, -3.0, 3.0)
|
|
278
|
+
Tmid[j] = Tmid[j] + delta
|
|
279
|
+
Tmid_hist[j][:, idx:idx + 1, :] = Tmid[j]
|
|
280
|
+
|
|
281
|
+
# Calculate zone temperature derivative
|
|
282
|
+
dTzone_dt = self.zone_update(q_env_total, q_int_i, q_hvac_i, q_solar_direct)
|
|
283
|
+
# TODO: consider to integrated with Neural ODE
|
|
284
|
+
|
|
285
|
+
# Update zone temperature with stability limits
|
|
286
|
+
dTzone_dt_clamped = torch.clamp(dTzone_dt, -2.0 / self.delta_t, 2.0 / self.delta_t)
|
|
287
|
+
delta = torch.clamp(self.delta_t * dTzone_dt_clamped, -2.0, 2.0)
|
|
288
|
+
Tzone = Tzone + delta
|
|
289
|
+
Tzone_pred[:, idx:idx + 1, :] = Tzone
|
|
290
|
+
|
|
291
|
+
return Tzone_pred, Tzone_gt, (q_int_pred, q_hvac_pred, q_env_pred, q_solar_direct_pred)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
# --- Runge-Kutta Integration ---
|
|
295
|
+
def rk4_step(f, x, dt, max_delta=3.0):
|
|
296
|
+
k1 = f(x)
|
|
297
|
+
k1 = torch.clamp(k1, -max_delta / dt, max_delta / dt)
|
|
298
|
+
|
|
299
|
+
k2 = f(x + 0.5 * dt * k1)
|
|
300
|
+
k2 = torch.clamp(k2, -max_delta / dt, max_delta / dt)
|
|
301
|
+
|
|
302
|
+
k3 = f(x + 0.5 * dt * k2)
|
|
303
|
+
k3 = torch.clamp(k3, -max_delta / dt, max_delta / dt)
|
|
304
|
+
|
|
305
|
+
k4 = f(x + dt * k3)
|
|
306
|
+
k4 = torch.clamp(k4, -max_delta / dt, max_delta / dt)
|
|
307
|
+
|
|
308
|
+
dx = (dt / 6.0) * (k1 + 2 * k2 + 2 * k3 + k4)
|
|
309
|
+
return x + torch.clamp(dx, -max_delta, max_delta)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
# --- Internal Heat Gain Module ---
|
|
313
|
+
class Internal(nn.Module):
|
|
314
|
+
"""
|
|
315
|
+
A simple MLP is used to predict internal heat gain (convection ONLY)
|
|
316
|
+
#TODO: add a RNN/MLP with lookback window to consider internal radiation
|
|
317
|
+
|
|
318
|
+
In general, the internal heat gain comes from Lighting, Occupant, Appliance, etc,.
|
|
319
|
+
But they can be represented by a factor (alpha) multiply with a "schedule" (sch), for example:
|
|
320
|
+
q_light = alpha_light * sch_light
|
|
321
|
+
q_human = alpha_human * sch_human
|
|
322
|
+
q_cooking = alpha_cooking * sch_cooking
|
|
323
|
+
|
|
324
|
+
If detailed information is available, we can replace it by Physics Equations
|
|
325
|
+
For example, 80~100W * Number_of_people
|
|
326
|
+
Otherwise, we learn form periodic features
|
|
327
|
+
"""
|
|
328
|
+
|
|
329
|
+
def __init__(self, input_size, hidden_size, output_size):
|
|
330
|
+
super().__init__()
|
|
331
|
+
self.FC1 = nn.Linear(input_size - 1, hidden_size)
|
|
332
|
+
self.FC2 = nn.Linear(hidden_size, output_size)
|
|
333
|
+
self.gain = nn.Parameter(torch.tensor(0.1, dtype=torch.float32))
|
|
334
|
+
|
|
335
|
+
# Initialize weights
|
|
336
|
+
nn.init.xavier_uniform_(self.FC1.weight, gain=0.1)
|
|
337
|
+
nn.init.zeros_(self.FC1.bias)
|
|
338
|
+
nn.init.xavier_uniform_(self.FC2.weight, gain=0.1)
|
|
339
|
+
nn.init.zeros_(self.FC2.bias)
|
|
340
|
+
|
|
341
|
+
def forward(self, x):
|
|
342
|
+
x1 = F.relu(self.FC1(x[:, :, :-1]))
|
|
343
|
+
sch = torch.sigmoid(self.FC2(x1)) + x[:, :, -1:]
|
|
344
|
+
return F.softplus(self.gain) * 0.1 * sch
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
# --- HVAC Module ---
|
|
348
|
+
class HVAC(nn.Module):
|
|
349
|
+
"""
|
|
350
|
+
A simplified linear module is used here for AIR SIDE SYSTEM ONLY
|
|
351
|
+
Change it to any type of RNN or add look back window for RADIATION SYSTEM
|
|
352
|
+
The input is pre-calculated pHVAC (thermal load)
|
|
353
|
+
|
|
354
|
+
But if the raw data is HVAC energy, or supply air flow/temperature, fan speed, coil load, etc
|
|
355
|
+
#TODO: Add More Types of HVAC Modules
|
|
356
|
+
|
|
357
|
+
"""
|
|
358
|
+
|
|
359
|
+
def __init__(self):
|
|
360
|
+
super().__init__()
|
|
361
|
+
self.gain = nn.Parameter(torch.tensor(0.1, dtype=torch.float32))
|
|
362
|
+
|
|
363
|
+
def forward(self, x):
|
|
364
|
+
return (F.softplus(self.gain) * 0.1 * x)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
# --- Zone Heat Balance Module ---
|
|
368
|
+
class ZoneUpdate(nn.Module):
|
|
369
|
+
def __init__(self, envelop, c_init=10000.0):
|
|
370
|
+
super().__init__()
|
|
371
|
+
self.C_inv = nn.Parameter(torch.tensor(1.0 / c_init, dtype=torch.float32))
|
|
372
|
+
self.envelop = envelop
|
|
373
|
+
|
|
374
|
+
def forward(self, q_env_total, q_int, q_hvac, q_solar_direct=None):
|
|
375
|
+
c_inv_bounded = F.softplus(self.C_inv) * (1.0 / self.envelop["c_zone"])
|
|
376
|
+
|
|
377
|
+
# Add direct solar gain if provided
|
|
378
|
+
if q_solar_direct is not None:
|
|
379
|
+
return (q_env_total + q_int + q_hvac + q_solar_direct) * c_inv_bounded
|
|
380
|
+
else:
|
|
381
|
+
return (q_env_total + q_int + q_hvac) * c_inv_bounded
|
|
@@ -73,7 +73,7 @@ class EarlyStopping:
|
|
|
73
73
|
|
|
74
74
|
def train_model(model, train_loader, valid_loader, test_loader, lr, epochs,
|
|
75
75
|
patience, tempscal, fluxscal, enlen, delen,
|
|
76
|
-
rawdf, plott, modeltype, scale, device, ext_mdl):
|
|
76
|
+
rawdf, plott, modeltype, scale, device, ext_mdl, envelop_mdl):
|
|
77
77
|
num_update = 0
|
|
78
78
|
total_time = 0
|
|
79
79
|
time_every_update = {}
|
|
@@ -97,6 +97,8 @@ def train_model(model, train_loader, valid_loader, test_loader, lr, epochs,
|
|
|
97
97
|
"PI-modnn|L": (0, 1),
|
|
98
98
|
"PI-modnn|LC": (0, 0)
|
|
99
99
|
}.get(modeltype, (None, None))
|
|
100
|
+
if envelop_mdl=="physics":
|
|
101
|
+
Phy_loss, Phy_cons = 0, 0
|
|
100
102
|
if Phy_loss is None:
|
|
101
103
|
raise ValueError("Unknown modeltype")
|
|
102
104
|
|
|
@@ -213,6 +215,7 @@ def train_model(model, train_loader, valid_loader, test_loader, lr, epochs,
|
|
|
213
215
|
hvac_out_denorm.append(
|
|
214
216
|
fluxscal.inverse_transform(hvac_out[[idx], enlen:, :].reshape(-1, 1)))
|
|
215
217
|
|
|
218
|
+
#TODO: collect different heat fluxes, need match with different model, leave it later
|
|
216
219
|
# inter_out_denorm.append(
|
|
217
220
|
# fluxscal.inverse_transform(inter_out[[idx], enlen:, :].reshape(-1, 1)))
|
|
218
221
|
# external_out_denorm.append(
|
|
@@ -8,7 +8,7 @@ import numpy as np
|
|
|
8
8
|
import torch
|
|
9
9
|
import matplotlib.dates as dates
|
|
10
10
|
import matplotlib.pyplot as plt
|
|
11
|
-
from modnn.Models import
|
|
11
|
+
from modnn.Models import ModNN_phy, BaseNN, ModNN_data
|
|
12
12
|
from modnn.Play import train_model, test_model, check_model, dynamic_check_model, grad_model
|
|
13
13
|
import matplotlib
|
|
14
14
|
import seaborn as sns
|
|
@@ -37,7 +37,10 @@ class Mod:
|
|
|
37
37
|
|
|
38
38
|
def train(self):
|
|
39
39
|
if "modnn" in self.args["modeltype"]:
|
|
40
|
-
|
|
40
|
+
if self.args["envelop_mdl"] == "physics":
|
|
41
|
+
model = ModNN_phy.ModNN(self.args).to(self.device)
|
|
42
|
+
else:
|
|
43
|
+
model = ModNN_data.ModNN(self.args).to(self.device)
|
|
41
44
|
if self.args["modeltype"] == "LSTM":
|
|
42
45
|
model = BaseNN.Baseline(self.args).to(self.device)
|
|
43
46
|
start_time = time.time()
|
|
@@ -58,7 +61,8 @@ class Mod:
|
|
|
58
61
|
modeltype = self.args["modeltype"],
|
|
59
62
|
scale = self.args["scale"],
|
|
60
63
|
device=self.device,
|
|
61
|
-
ext_mdl = self.args["ext_mdl"]
|
|
64
|
+
ext_mdl = self.args["ext_mdl"],
|
|
65
|
+
envelop_mdl = self.args["envelop_mdl"],)
|
|
62
66
|
print("--- %s seconds ---" % (time.time() - start_time))
|
|
63
67
|
|
|
64
68
|
folder_name = ("../Saved/{}/Trained_mdl".format(self.args['save_name']) +
|
|
@@ -138,7 +142,10 @@ class Mod:
|
|
|
138
142
|
start_time = time.time()
|
|
139
143
|
print("Loading model")
|
|
140
144
|
if "modnn" in self.args["modeltype"]:
|
|
141
|
-
|
|
145
|
+
if self.args["envelop_mdl"] == "physics":
|
|
146
|
+
model = ModNN_phy.ModNN(self.args).to(self.device)
|
|
147
|
+
else:
|
|
148
|
+
model = ModNN_data.ModNN(self.args).to(self.device)
|
|
142
149
|
if self.args["modeltype"] == "LSTM":
|
|
143
150
|
model = BaseNN.Baseline(self.args).to(self.device)
|
|
144
151
|
|
|
@@ -13,6 +13,6 @@ modnn.egg-info/dependency_links.txt
|
|
|
13
13
|
modnn.egg-info/requires.txt
|
|
14
14
|
modnn.egg-info/top_level.txt
|
|
15
15
|
modnn/Models/BaseNN.py
|
|
16
|
-
modnn/Models/
|
|
17
|
-
modnn/Models/
|
|
16
|
+
modnn/Models/ModNN_data.py
|
|
17
|
+
modnn/Models/ModNN_phy.py
|
|
18
18
|
modnn/Models/__init__.py
|
|
@@ -1,402 +0,0 @@
|
|
|
1
|
-
import torch
|
|
2
|
-
import torch.nn as nn
|
|
3
|
-
import torch.nn.functional as F
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
# --- Solar Radiation Module ---
|
|
7
|
-
class SolarRadiation(nn.Module):
|
|
8
|
-
def __init__(self, envelop, name="Solar"):
|
|
9
|
-
super().__init__()
|
|
10
|
-
# Initialization
|
|
11
|
-
self.envelop = envelop
|
|
12
|
-
|
|
13
|
-
# Solar transmission coefficient
|
|
14
|
-
self.direct_gain = nn.Parameter(torch.tensor(0.1, dtype=torch.float32))
|
|
15
|
-
|
|
16
|
-
# Solar absorption coefficient
|
|
17
|
-
self.abs_wall = nn.Parameter(torch.tensor(0.1, dtype=torch.float32))
|
|
18
|
-
self.abs_roof = nn.Parameter(torch.tensor(0.1, dtype=torch.float32))
|
|
19
|
-
self.name = name
|
|
20
|
-
|
|
21
|
-
def forward(self, radiation, n_wall, n_roof):
|
|
22
|
-
"""
|
|
23
|
-
Calculate solar heat gains [transmission and absorption]
|
|
24
|
-
|
|
25
|
-
Args:
|
|
26
|
-
radiation: Solar radiation input [batch, time, 1] or [batch, time, directions]
|
|
27
|
-
(e.g., [batch, time, 4] for E,S,W,N directions)
|
|
28
|
-
n_wall: Number of wall components
|
|
29
|
-
n_roof: Number of roof components
|
|
30
|
-
|
|
31
|
-
Returns:
|
|
32
|
-
direct_gain: Heat directly entering zone
|
|
33
|
-
wall_gains: List of absorbed heat for each wall [n_wall elements]
|
|
34
|
-
roof_gains: List of absorbed heat for each roof [n_roof elements]
|
|
35
|
-
"""
|
|
36
|
-
# Apply softplus to ensure positive coefficients
|
|
37
|
-
direct_coef = F.softplus(self.direct_gain) * self.envelop["direct_coef"]
|
|
38
|
-
abs_wall_coef = F.softplus(self.abs_wall) * self.envelop["abs_wall_coef"]
|
|
39
|
-
abs_roof_coef = F.softplus(self.abs_roof) * self.envelop["abs_roof_coef"]
|
|
40
|
-
|
|
41
|
-
# Calculate direct solar gain to zone
|
|
42
|
-
direct_gain = direct_coef * radiation
|
|
43
|
-
# TODO: Distribute transmission solar radiation to each surfaces
|
|
44
|
-
|
|
45
|
-
# Calculate absorption by walls
|
|
46
|
-
wall_gains = []
|
|
47
|
-
for i in range(n_wall):
|
|
48
|
-
# Different walls can have different orientations
|
|
49
|
-
# TODO: ASHRAE Clear-Sky Model
|
|
50
|
-
if radiation.shape[-1] > 1 and i < radiation.shape[-1]:
|
|
51
|
-
# Use specific direction for this wall
|
|
52
|
-
wall_gain = abs_wall_coef * radiation[:, :, i % radiation.shape[-1]]
|
|
53
|
-
wall_gains.append(wall_gain.unsqueeze(-1))
|
|
54
|
-
else:
|
|
55
|
-
# Use total radiation
|
|
56
|
-
wall_gain = abs_wall_coef * radiation
|
|
57
|
-
wall_gains.append(wall_gain)
|
|
58
|
-
|
|
59
|
-
# Calculate absorption by roof (assume horizontal)
|
|
60
|
-
roof_gains = []
|
|
61
|
-
for i in range(n_roof):
|
|
62
|
-
roof_gain = abs_roof_coef * radiation
|
|
63
|
-
roof_gains.append(roof_gain)
|
|
64
|
-
|
|
65
|
-
return direct_gain, wall_gains, roof_gains
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
# --- Envelope Module [RC] ---
|
|
69
|
-
class EnvelopeRC(nn.Module):
|
|
70
|
-
def __init__(self, envelop, name="RC", r_init=10.0, c_init=100.0):
|
|
71
|
-
super().__init__()
|
|
72
|
-
self.R_inv = nn.Parameter(torch.tensor(1.0 / r_init, dtype=torch.float32))
|
|
73
|
-
self.C_inv = nn.Parameter(torch.tensor(1.0 / c_init, dtype=torch.float32))
|
|
74
|
-
self.name = name
|
|
75
|
-
self.envelop = envelop
|
|
76
|
-
|
|
77
|
-
def forward(self, Tamb, Tmid, Tzone, solar_gain=None):
|
|
78
|
-
# Bounded parameters for stability
|
|
79
|
-
r_inv_bounded = F.softplus(self.R_inv) * (1.0 / self.envelop["r_opaque_coef"])
|
|
80
|
-
c_inv_bounded = F.softplus(self.C_inv) * (1.0 / self.envelop["c_opaque_coef"])
|
|
81
|
-
|
|
82
|
-
# Calculate heat flows
|
|
83
|
-
q_in = (Tamb - Tmid) * r_inv_bounded
|
|
84
|
-
q_out = (Tmid - Tzone) * r_inv_bounded
|
|
85
|
-
|
|
86
|
-
# Add solar gain if provided
|
|
87
|
-
if solar_gain is not None:
|
|
88
|
-
# Add solar radiation to the first capacitance node
|
|
89
|
-
dTmid_dt = (q_in - q_out + solar_gain) * c_inv_bounded
|
|
90
|
-
else:
|
|
91
|
-
dTmid_dt = (q_in - q_out) * c_inv_bounded
|
|
92
|
-
|
|
93
|
-
return dTmid_dt, q_out
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
# --- Envelope Module [R] ---
|
|
97
|
-
class EnvelopeROnly(nn.Module):
|
|
98
|
-
def __init__(self, envelop, name="R-only", r_init=0.1):
|
|
99
|
-
super().__init__()
|
|
100
|
-
self.R_inv = nn.Parameter(torch.tensor(1/r_init, dtype=torch.float32))
|
|
101
|
-
self.name = name
|
|
102
|
-
self.envelop = envelop
|
|
103
|
-
|
|
104
|
-
def forward(self, Tamb, Tzone):
|
|
105
|
-
r_inv_bounded = F.softplus(self.R_inv) * (1.0 / self.envelop["r_transparent_coef"])
|
|
106
|
-
|
|
107
|
-
return (Tamb - Tzone) * r_inv_bounded
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
# --- Internal Heat Gain Module ---
|
|
111
|
-
class Internal(nn.Module):
|
|
112
|
-
"""
|
|
113
|
-
A simple MLP is used to predict internal heat gain (convection ONLY)
|
|
114
|
-
#TODO: add a RNN/MLP with lookback window to consider internal radiation
|
|
115
|
-
|
|
116
|
-
In general, the internal heat gain comes from Lighting, Occupant, Appliance, etc,.
|
|
117
|
-
But they can be represented by a factor (alpha) multiply with a "schedule" (sch), for example:
|
|
118
|
-
q_light = alpha_light * sch_light
|
|
119
|
-
q_human = alpha_human * sch_human
|
|
120
|
-
q_cooking = alpha_cooking * sch_cooking
|
|
121
|
-
|
|
122
|
-
If detailed information is available, we can replace it by Physics Equations
|
|
123
|
-
For example, 80~100W * Number_of_people
|
|
124
|
-
Otherwise, we learn form periodic features
|
|
125
|
-
"""
|
|
126
|
-
def __init__(self, input_size, hidden_size, output_size):
|
|
127
|
-
super().__init__()
|
|
128
|
-
self.FC1 = nn.Linear(input_size - 1, hidden_size)
|
|
129
|
-
self.FC2 = nn.Linear(hidden_size, output_size)
|
|
130
|
-
self.gain = nn.Parameter(torch.tensor(0.1, dtype=torch.float32))
|
|
131
|
-
|
|
132
|
-
# Initialize weights
|
|
133
|
-
nn.init.xavier_uniform_(self.FC1.weight, gain=0.1)
|
|
134
|
-
nn.init.zeros_(self.FC1.bias)
|
|
135
|
-
nn.init.xavier_uniform_(self.FC2.weight, gain=0.1)
|
|
136
|
-
nn.init.zeros_(self.FC2.bias)
|
|
137
|
-
|
|
138
|
-
def forward(self, x):
|
|
139
|
-
x1 = F.relu(self.FC1(x[:, :, :-1]))
|
|
140
|
-
sch = torch.sigmoid(self.FC2(x1)) + x[:, :, -1:]
|
|
141
|
-
return F.softplus(self.gain) * 0.1 * sch
|
|
142
|
-
|
|
143
|
-
class HVAC(nn.Module):
|
|
144
|
-
"""
|
|
145
|
-
A simplified linear module is used here for AIR SIDE SYSTEM ONLY
|
|
146
|
-
Change it to any type of RNN or add look back window for RADIATION SYSTEM
|
|
147
|
-
The input is pre-calculated pHVAC (thermal load)
|
|
148
|
-
|
|
149
|
-
But if the raw data is HVAC energy, or supply air flow/temperature, fan speed, coil load, etc
|
|
150
|
-
#TODO: Add More Types of HVAC Modules
|
|
151
|
-
|
|
152
|
-
"""
|
|
153
|
-
def __init__(self):
|
|
154
|
-
super().__init__()
|
|
155
|
-
self.gain = nn.Parameter(torch.tensor(0.1, dtype=torch.float32))
|
|
156
|
-
|
|
157
|
-
def forward(self, x):
|
|
158
|
-
return (F.softplus(self.gain) * 0.1 * x)
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
class ZoneUpdate(nn.Module):
|
|
162
|
-
def __init__(self, envelop, c_init=10000.0):
|
|
163
|
-
super().__init__()
|
|
164
|
-
self.C_inv = nn.Parameter(torch.tensor(1.0 / c_init, dtype=torch.float32))
|
|
165
|
-
self.envelop = envelop
|
|
166
|
-
|
|
167
|
-
def forward(self, q_env_total, q_int, q_hvac, q_solar_direct=None):
|
|
168
|
-
# Bounded parameter
|
|
169
|
-
c_inv_bounded = F.softplus(self.C_inv) * (1.0 / self.envelop["c_zone"])
|
|
170
|
-
|
|
171
|
-
# Add direct solar gain if provided
|
|
172
|
-
if q_solar_direct is not None:
|
|
173
|
-
return (q_env_total + q_int + q_hvac + q_solar_direct) * c_inv_bounded
|
|
174
|
-
else:
|
|
175
|
-
return (q_env_total + q_int + q_hvac) * c_inv_bounded
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
# --- Runge-Kutta Integration ---
|
|
179
|
-
def rk4_step(f, x, dt, max_delta=3.0):
|
|
180
|
-
k1 = f(x)
|
|
181
|
-
k1 = torch.clamp(k1, -max_delta / dt, max_delta / dt)
|
|
182
|
-
|
|
183
|
-
k2 = f(x + 0.5 * dt * k1)
|
|
184
|
-
k2 = torch.clamp(k2, -max_delta / dt, max_delta / dt)
|
|
185
|
-
|
|
186
|
-
k3 = f(x + 0.5 * dt * k2)
|
|
187
|
-
k3 = torch.clamp(k3, -max_delta / dt, max_delta / dt)
|
|
188
|
-
|
|
189
|
-
k4 = f(x + dt * k3)
|
|
190
|
-
k4 = torch.clamp(k4, -max_delta / dt, max_delta / dt)
|
|
191
|
-
|
|
192
|
-
dx = (dt / 6.0) * (k1 + 2 * k2 + 2 * k3 + k4)
|
|
193
|
-
return x + torch.clamp(dx, -max_delta, max_delta)
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
class ModNN(nn.Module):
|
|
197
|
-
def __init__(self, args):
|
|
198
|
-
super().__init__()
|
|
199
|
-
para = args["para"]
|
|
200
|
-
envelop = args["envelop"]
|
|
201
|
-
self.enc_len = args["enLen"]
|
|
202
|
-
self.device = args["device"]
|
|
203
|
-
self.delta_t = 900 # 15 minutes in seconds
|
|
204
|
-
|
|
205
|
-
# Model configuration
|
|
206
|
-
self.n_wall = envelop.get("n_wall", 0)
|
|
207
|
-
self.n_roof = envelop.get("n_roof", 0)
|
|
208
|
-
self.n_window = envelop.get("n_window", 0)
|
|
209
|
-
|
|
210
|
-
print(f"Initializing ModNN with {self.n_wall} walls, {self.n_roof} roofs, {self.n_window} windows")
|
|
211
|
-
|
|
212
|
-
# Assemble modules
|
|
213
|
-
# Internal heat gain module
|
|
214
|
-
self.Int = Internal(para["Int_in"], para["Int_h"], para["Int_out"])
|
|
215
|
-
# HVAC module
|
|
216
|
-
self.HVAC = HVAC()
|
|
217
|
-
# Envelope module
|
|
218
|
-
self.solar = SolarRadiation(envelop=envelop)
|
|
219
|
-
self.rc_envelopes = nn.ModuleList([
|
|
220
|
-
EnvelopeRC(envelop=envelop,
|
|
221
|
-
name=f"wall_{i}",
|
|
222
|
-
r_init=10.0 + i * 1.0,
|
|
223
|
-
c_init=80.0 + i * 10.0)
|
|
224
|
-
for i in range(self.n_wall)
|
|
225
|
-
] + [
|
|
226
|
-
EnvelopeRC(envelop=envelop,
|
|
227
|
-
name=f"roof_{i}",
|
|
228
|
-
r_init=20.0 + i * 2.0,
|
|
229
|
-
c_init=60.0 + i * 10.0)
|
|
230
|
-
for i in range(self.n_roof)
|
|
231
|
-
])
|
|
232
|
-
|
|
233
|
-
self.r_only_envelopes = nn.ModuleList([
|
|
234
|
-
EnvelopeROnly(envelop=envelop,
|
|
235
|
-
name=f"window_{i}",
|
|
236
|
-
r_init=1 + i * 0.1)
|
|
237
|
-
for i in range(self.n_window)
|
|
238
|
-
])
|
|
239
|
-
|
|
240
|
-
self.zone_update = ZoneUpdate(envelop=envelop)
|
|
241
|
-
|
|
242
|
-
def forward(self, input_X):
|
|
243
|
-
"""
|
|
244
|
-
input_X order: [T_zone, T_ambient, solar, day_sin, day_cos, occ, phvac]
|
|
245
|
-
Shape: [batch_size, time_steps, features]
|
|
246
|
-
"""
|
|
247
|
-
batch_size = input_X.shape[0]
|
|
248
|
-
time_steps = input_X.shape[1]
|
|
249
|
-
|
|
250
|
-
# Extract input data
|
|
251
|
-
T_amb = input_X[:, :, [1]] # Ambient air temperature
|
|
252
|
-
Solar_X = input_X[:, :, [2]] # Solar radiation
|
|
253
|
-
Int_X = input_X[:, :, [3, 4, 5]] # Time_sin/cos, Occ
|
|
254
|
-
HVAC_X = input_X[:, :, [6]] # pHVAC
|
|
255
|
-
Tzone_gt = input_X[:, :, [0]] # Zone air temperature groundtruth
|
|
256
|
-
|
|
257
|
-
# Pre-allocate results arrays - more efficient than appending
|
|
258
|
-
Tzone_pred = torch.zeros(batch_size, time_steps, 1, device=input_X.device)
|
|
259
|
-
q_int_pred = torch.zeros(batch_size, time_steps, 1, device=input_X.device)
|
|
260
|
-
q_hvac_pred = torch.zeros(batch_size, time_steps, 1, device=input_X.device)
|
|
261
|
-
q_env_pred = torch.zeros(batch_size, time_steps, 1, device=input_X.device)
|
|
262
|
-
q_solar_direct_pred = torch.zeros(batch_size, time_steps, 1, device=input_X.device)
|
|
263
|
-
|
|
264
|
-
# Pre-allocate envelope temperature histories
|
|
265
|
-
Tmid_hist = [torch.zeros(batch_size, time_steps, 1, device=input_X.device)
|
|
266
|
-
for _ in range(len(self.rc_envelopes))]
|
|
267
|
-
|
|
268
|
-
# Initialize zone temperature for first step
|
|
269
|
-
Tzone = Tzone_gt[:, 0:1, :]
|
|
270
|
-
|
|
271
|
-
# Initialize envelope temperatures (only once)
|
|
272
|
-
Tmid = []
|
|
273
|
-
for _ in range(len(self.rc_envelopes)):
|
|
274
|
-
tmid_init = 0.7 * Tzone + 0.3 * T_amb[:, 0:1, :]
|
|
275
|
-
Tmid.append(tmid_init)
|
|
276
|
-
|
|
277
|
-
# --- Encoder phase (optimization: only calculate what's needed) ---
|
|
278
|
-
for i in range(self.enc_len):
|
|
279
|
-
# Simply record the ground truth temperature
|
|
280
|
-
Tzone_pred[:, i:i + 1, :] = Tzone_gt[:, i:i + 1, :]
|
|
281
|
-
|
|
282
|
-
# For envelope temperatures, we need to calculate and track these
|
|
283
|
-
# even during encoder phase as they're not directly observed
|
|
284
|
-
T_amb_i = T_amb[:, i:i + 1, :]
|
|
285
|
-
Solar_i = Solar_X[:, i:i + 1, :]
|
|
286
|
-
Tzone_i = Tzone_gt[:, i:i + 1, :]
|
|
287
|
-
|
|
288
|
-
# Calculate solar distribution (needed for envelope temperatures)
|
|
289
|
-
q_solar_direct, q_solar_wall, q_solar_roof = self.solar(
|
|
290
|
-
Solar_i, self.n_wall, self.n_roof
|
|
291
|
-
)
|
|
292
|
-
q_solar_direct_pred[:, i:i + 1, :] = q_solar_direct
|
|
293
|
-
|
|
294
|
-
# Update envelope temperatures
|
|
295
|
-
for idx, env in enumerate(self.rc_envelopes):
|
|
296
|
-
# Assign solar gains
|
|
297
|
-
if idx < self.n_wall and q_solar_wall:
|
|
298
|
-
solar_gain = q_solar_wall[idx]
|
|
299
|
-
elif idx >= self.n_wall and q_solar_roof:
|
|
300
|
-
solar_gain = q_solar_roof[idx - self.n_wall]
|
|
301
|
-
else:
|
|
302
|
-
solar_gain = None
|
|
303
|
-
|
|
304
|
-
# Evolution function for rk4
|
|
305
|
-
def dTmid(Tmid_local):
|
|
306
|
-
dTdt, _ = env(T_amb_i, Tmid_local, Tzone_i, solar_gain)
|
|
307
|
-
return dTdt
|
|
308
|
-
|
|
309
|
-
# Update envelope temperature using RK4
|
|
310
|
-
Tmid[idx] = rk4_step(dTmid, Tmid[idx], self.delta_t)
|
|
311
|
-
Tmid_hist[idx][:, i:i + 1, :] = Tmid[idx]
|
|
312
|
-
|
|
313
|
-
# Only calculate these if needed for analysis, otherwise leave as zeros
|
|
314
|
-
# in encoder phase since they don't affect the state evolution
|
|
315
|
-
Int_i = Int_X[:, i:i + 1, :]
|
|
316
|
-
HVAC_i = HVAC_X[:, i:i + 1, :]
|
|
317
|
-
q_int_pred[:, i:i + 1, :] = self.Int(Int_i)
|
|
318
|
-
q_hvac_pred[:, i:i + 1, :] = self.HVAC(HVAC_i)
|
|
319
|
-
|
|
320
|
-
# Optional: Calculate envelope heat transfer contributions
|
|
321
|
-
# if needed for analysis. If not, can skip this.
|
|
322
|
-
q_env_total = torch.zeros_like(Tzone)
|
|
323
|
-
for idx, env in enumerate(self.rc_envelopes):
|
|
324
|
-
if idx < self.n_wall and q_solar_wall:
|
|
325
|
-
solar_gain = q_solar_wall[idx]
|
|
326
|
-
elif idx >= self.n_wall and q_solar_roof:
|
|
327
|
-
solar_gain = q_solar_roof[idx - self.n_wall]
|
|
328
|
-
else:
|
|
329
|
-
solar_gain = None
|
|
330
|
-
_, q_env = env(T_amb_i, Tmid[idx], Tzone_i, solar_gain)
|
|
331
|
-
q_env_total += q_env
|
|
332
|
-
|
|
333
|
-
for env in self.r_only_envelopes:
|
|
334
|
-
q_env_r = env(T_amb_i, Tzone_i)
|
|
335
|
-
q_env_total += q_env_r
|
|
336
|
-
|
|
337
|
-
q_env_pred[:, i:i + 1, :] = q_env_total
|
|
338
|
-
|
|
339
|
-
# --- Decoder phase (predicting future temps) ---
|
|
340
|
-
# Starting from the last encoder step
|
|
341
|
-
Tzone = Tzone_gt[:, self.enc_len - 1:self.enc_len, :]
|
|
342
|
-
|
|
343
|
-
for i in range(time_steps - self.enc_len):
|
|
344
|
-
idx = self.enc_len + i
|
|
345
|
-
T_amb_i = T_amb[:, idx:idx + 1, :]
|
|
346
|
-
Int_i = Int_X[:, idx:idx + 1, :]
|
|
347
|
-
HVAC_i = HVAC_X[:, idx:idx + 1, :]
|
|
348
|
-
Solar_i = Solar_X[:, idx:idx + 1, :]
|
|
349
|
-
|
|
350
|
-
# Process internal gains and HVAC - only calculate once
|
|
351
|
-
q_int = self.Int(Int_i)
|
|
352
|
-
q_hvac = self.HVAC(HVAC_i)
|
|
353
|
-
q_int_pred[:, idx:idx + 1, :] = q_int
|
|
354
|
-
q_hvac_pred[:, idx:idx + 1, :] = q_hvac
|
|
355
|
-
|
|
356
|
-
# Distribute solar radiation
|
|
357
|
-
q_solar_direct, q_solar_wall, q_solar_roof = self.solar(
|
|
358
|
-
Solar_i, self.n_wall, self.n_roof
|
|
359
|
-
)
|
|
360
|
-
q_solar_direct_pred[:, idx:idx + 1, :] = q_solar_direct
|
|
361
|
-
|
|
362
|
-
# Prepare for zone temperature update
|
|
363
|
-
q_env_total = torch.zeros_like(Tzone)
|
|
364
|
-
|
|
365
|
-
# Update envelope temperatures and calculate heat flow
|
|
366
|
-
for j, env in enumerate(self.rc_envelopes):
|
|
367
|
-
# Assign solar gains
|
|
368
|
-
if j < self.n_wall and q_solar_wall:
|
|
369
|
-
solar_gain = q_solar_wall[j]
|
|
370
|
-
elif j >= self.n_wall and q_solar_roof:
|
|
371
|
-
solar_gain = q_solar_roof[j - self.n_wall]
|
|
372
|
-
else:
|
|
373
|
-
solar_gain = None
|
|
374
|
-
|
|
375
|
-
def dTmid(Tmid_local):
|
|
376
|
-
dTdt, _ = env(T_amb_i, Tmid_local, Tzone, solar_gain)
|
|
377
|
-
return dTdt
|
|
378
|
-
|
|
379
|
-
# Update envelope temperature
|
|
380
|
-
Tmid[j] = rk4_step(dTmid, Tmid[j], self.delta_t)
|
|
381
|
-
Tmid_hist[j][:, idx:idx + 1, :] = Tmid[j]
|
|
382
|
-
|
|
383
|
-
# Calculate heat flow contribution (calculate once)
|
|
384
|
-
_, q_env = env(T_amb_i, Tmid[j], Tzone, solar_gain)
|
|
385
|
-
q_env_total += q_env
|
|
386
|
-
|
|
387
|
-
# Add window heat flow
|
|
388
|
-
for env in self.r_only_envelopes:
|
|
389
|
-
q_env_r = env(T_amb_i, Tzone)
|
|
390
|
-
q_env_total += q_env_r
|
|
391
|
-
|
|
392
|
-
q_env_pred[:, idx:idx + 1, :] = q_env_total
|
|
393
|
-
|
|
394
|
-
# Update zone temperature using RK4
|
|
395
|
-
def dTzone(Tzone_local):
|
|
396
|
-
return self.zone_update(q_env_total, q_int, q_hvac, q_solar_direct)
|
|
397
|
-
|
|
398
|
-
# Update zone temperature with stability limits
|
|
399
|
-
Tzone = rk4_step(dTzone, Tzone, self.delta_t, max_delta=2.0)
|
|
400
|
-
Tzone_pred[:, idx:idx + 1, :] = Tzone
|
|
401
|
-
|
|
402
|
-
return Tzone_pred, Tzone_gt, (q_int_pred, q_hvac_pred, q_env_pred, q_solar_direct_pred)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|