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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modnn
3
- Version: 2.0.0
3
+ Version: 2.0.2
4
4
  Summary: Physics-Informed Modularized Neural Network for Building Energy Modeling
5
5
  Author-email: Zixin Jiang <zjiang19@syr.edu>
6
6
  License-Expression: MIT
@@ -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:1",
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 ModNN, BaseNN
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
- model = ModNN.ModNN(self.args).to(self.device)
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
- model = ModNN.ModNN(self.args).to(self.device)
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modnn
3
- Version: 2.0.0
3
+ Version: 2.0.2
4
4
  Summary: Physics-Informed Modularized Neural Network for Building Energy Modeling
5
5
  Author-email: Zixin Jiang <zjiang19@syr.edu>
6
6
  License-Expression: MIT
@@ -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/ModNN.py
17
- modnn/Models/Modback.py
16
+ modnn/Models/ModNN_data.py
17
+ modnn/Models/ModNN_phy.py
18
18
  modnn/Models/__init__.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "modnn"
7
- version = "2.0.0"
7
+ version = "2.0.2"
8
8
  description = "Physics-Informed Modularized Neural Network for Building Energy Modeling"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.7"
@@ -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