modnn 1.0.0__py3-none-any.whl
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/Config.py +109 -0
- modnn/Dataset.py +175 -0
- modnn/Play.py +484 -0
- modnn/__init__.py +0 -0
- modnn/run.py +22 -0
- modnn/utils.py +541 -0
- modnn-1.0.0.dist-info/METADATA +65 -0
- modnn-1.0.0.dist-info/RECORD +11 -0
- modnn-1.0.0.dist-info/WHEEL +5 -0
- modnn-1.0.0.dist-info/licenses/LICENSE +7 -0
- modnn-1.0.0.dist-info/top_level.txt +1 -0
modnn/Config.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
def _paras(**kwargs):
|
|
2
|
+
"""
|
|
3
|
+
Return hyperparameters for training.
|
|
4
|
+
Accepts keyword arguments to override default values.
|
|
5
|
+
"""
|
|
6
|
+
para = {
|
|
7
|
+
# Internal gain module
|
|
8
|
+
"Int_in": 3, "Int_h": 18, "Int_out": 1,
|
|
9
|
+
|
|
10
|
+
# External disturbance module
|
|
11
|
+
"Ext_in": 5, "Ext_h": 22, "Ext_out": 1,
|
|
12
|
+
|
|
13
|
+
# Zone module
|
|
14
|
+
"Zone_in": 1, "Zone_out": 1,
|
|
15
|
+
|
|
16
|
+
# HVAC module
|
|
17
|
+
"HVAC_in": 1, "HVAC_out": 1,
|
|
18
|
+
|
|
19
|
+
# Look back window
|
|
20
|
+
"window" : 1,
|
|
21
|
+
|
|
22
|
+
# Training hyperparameters
|
|
23
|
+
"lr": 0.01,
|
|
24
|
+
"epochs": 500,
|
|
25
|
+
"patience": 10, #Early stop
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# Allow override from kwargs
|
|
29
|
+
para.update(kwargs)
|
|
30
|
+
|
|
31
|
+
return para
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _args(**kwargs):
|
|
35
|
+
"""
|
|
36
|
+
Returns model configurations.
|
|
37
|
+
Allows keyword-based overrides.
|
|
38
|
+
"""
|
|
39
|
+
para_overrides = kwargs.pop("para", {})
|
|
40
|
+
args = {
|
|
41
|
+
"para": _paras(**para_overrides),
|
|
42
|
+
|
|
43
|
+
# Paths and device
|
|
44
|
+
"datapath": "../Dataset/EPlus.csv",
|
|
45
|
+
"device": "cuda:1",
|
|
46
|
+
|
|
47
|
+
# Data settings
|
|
48
|
+
"resolution": 15, # 15 minutes data
|
|
49
|
+
"enLen": 48, # "Kind of warm-up"
|
|
50
|
+
"deLen": 96, # Prediction horizon, 96 is for 24 hours
|
|
51
|
+
"startday": 60, # Training data selection
|
|
52
|
+
"trainday": 90, # Training data selection
|
|
53
|
+
"testday": 1, # Testing data selection
|
|
54
|
+
"training_batch": 1024,
|
|
55
|
+
"plott": 'all', # all: If want to see how model response to max heating/cooling; else: only Tzone prediction
|
|
56
|
+
"modeltype": 'PI-modnn', # We also have "LSTM", "PI-modnn", "PI-modnn|C", "PI-modnn|L", "PI-modnn|LC" for fun
|
|
57
|
+
# LSTM is the baseline, |C means no constraints, |L means no loss adjustment
|
|
58
|
+
"scale": 1, # scaling factor for HVAC power
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
args.update(kwargs)
|
|
62
|
+
|
|
63
|
+
return args
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_config(overrides=None):
|
|
67
|
+
"""
|
|
68
|
+
Adjust parameters as needed.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
overrides (dict): override config like:
|
|
72
|
+
{
|
|
73
|
+
"datapath": "your_path.csv",
|
|
74
|
+
"para": {"epochs": 200, "lr": 0.01},
|
|
75
|
+
"device": "cuda:1",
|
|
76
|
+
...
|
|
77
|
+
}
|
|
78
|
+
Returns:
|
|
79
|
+
dict: Final configuration dictionary
|
|
80
|
+
"""
|
|
81
|
+
return _args(**(overrides or {}))
|
|
82
|
+
|
|
83
|
+
def print_help():
|
|
84
|
+
print("\n🔧 Adjustable Parameters:\n")
|
|
85
|
+
print("General Args:")
|
|
86
|
+
print(" datapath (str) : Path to the dataset CSV")
|
|
87
|
+
print(" device (str) : Device to run the model on (e.g., 'cuda:0', 'cuda:1', 'cpu')")
|
|
88
|
+
print(" resolution (int) : Data resolution in minutes")
|
|
89
|
+
print(" enLen (int) : Encoder sequence length (timesteps), 1 step is 15 minutes")
|
|
90
|
+
print(" deLen (int) : Decoder sequence length, it is also prediction horizon (timesteps), 1 step is 15 minutes")
|
|
91
|
+
print(" startday (int) : Start day of the dataset for training")
|
|
92
|
+
print(" trainday (int) : Number of training days")
|
|
93
|
+
print(" testday (int) : Number of test days")
|
|
94
|
+
print(" training_batch (int) : Batch size for training")
|
|
95
|
+
print(" plott (str) : 'all' to plot prediction results and checking results, 'others' to plot prediction results only")
|
|
96
|
+
print(" modeltype (str) : LSTM, PI-modnn, PI-modnn|C, PI-modnn|L, PI-modnn|LC where LSTM is the baseline, |C means no constraints, |L means no loss adjustment")
|
|
97
|
+
print(" scale (float): Scaling factor for HVAC power")
|
|
98
|
+
print("\nHyperparameters (args['para']):")
|
|
99
|
+
print(" Int_in, Int_h, Int_out : Internal module input/hidden/output size")
|
|
100
|
+
print(" Ext_in, Ext_h, Ext_out : External module input/hidden/output size")
|
|
101
|
+
print(" Zone_in, Zone_out : Zone module input/output size")
|
|
102
|
+
print(" HVAC_in, HVAC_out : HVAC module input/output size")
|
|
103
|
+
print(" window : Look up window size")
|
|
104
|
+
print(" lr : Learning rate")
|
|
105
|
+
print(" epochs : Max training epochs")
|
|
106
|
+
print(" patience : Early stopping patience")
|
|
107
|
+
print("\n📝 Use `get_config(overrides)` to modify these settings.\n")
|
|
108
|
+
|
|
109
|
+
|
modnn/Dataset.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
from sklearn.preprocessing import MinMaxScaler
|
|
2
|
+
from torch.utils.data import DataLoader, Dataset, random_split
|
|
3
|
+
import pandas as pd
|
|
4
|
+
import numpy as np
|
|
5
|
+
import os
|
|
6
|
+
import torch
|
|
7
|
+
import pickle
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _get_ModNN_input(start_time, timestep_minutes, occupancy=None, hvac=None,
|
|
11
|
+
temp_amb=None, solar=None, temp_room=None, path_to_save=None):
|
|
12
|
+
"""
|
|
13
|
+
Generate dataframe that can be used for modnn.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
start_time (str): Start timestamp (e.g., '2023-07-01 00:00')
|
|
17
|
+
timestep_minutes (int): Time resolution (e.g., 15 for 15-minute steps)
|
|
18
|
+
occupancy (list or np.array): Occupancy schedule
|
|
19
|
+
hvac (list or np.array): HVAC power values (in W)
|
|
20
|
+
temp_amb (list or np.array): Ambient temperature [°F]
|
|
21
|
+
solar (list or np.array): Solar radiation [W/m²]
|
|
22
|
+
temp_room (list or np.array): Room temp [°F]
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
pd.DataFrame: Formatted and time-indexed input DataFrame
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
n = len(hvac)
|
|
29
|
+
index = pd.date_range(start=start_time, periods=n, freq=f"{timestep_minutes}min")
|
|
30
|
+
|
|
31
|
+
def fill_or_default(arr, default):
|
|
32
|
+
return arr if arr is not None else np.full(n, default)
|
|
33
|
+
|
|
34
|
+
df = pd.DataFrame({
|
|
35
|
+
"temp_room": fill_or_default(temp_room, 72),
|
|
36
|
+
"temp_amb": fill_or_default(temp_amb, 85),
|
|
37
|
+
"solar": fill_or_default(solar, 0),
|
|
38
|
+
"occ": fill_or_default(occupancy, 0),
|
|
39
|
+
"phvac": hvac
|
|
40
|
+
}, index=index)
|
|
41
|
+
df=df.resample('15T').mean()
|
|
42
|
+
df.to_csv(path_to_save)
|
|
43
|
+
|
|
44
|
+
return df
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class MyData(Dataset):
|
|
48
|
+
"""
|
|
49
|
+
Generate sequence-to-sequence pairs for learning.
|
|
50
|
+
"""
|
|
51
|
+
def __init__(self, X_seq, y_seq):
|
|
52
|
+
self.X = np.array(X_seq)
|
|
53
|
+
self.y = np.array(y_seq)[:, :, [0]] # Index 0 is Tzone
|
|
54
|
+
|
|
55
|
+
def __len__(self):
|
|
56
|
+
return len(self.X)
|
|
57
|
+
|
|
58
|
+
def __getitem__(self, index):
|
|
59
|
+
return torch.tensor(self.X[index], dtype=torch.float32), \
|
|
60
|
+
torch.tensor(self.y[index], dtype=torch.float32)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class DataCook:
|
|
64
|
+
"""
|
|
65
|
+
Full data pipeline for modnn:
|
|
66
|
+
- Load CSV
|
|
67
|
+
- Add time-based features
|
|
68
|
+
- Normalize inputs
|
|
69
|
+
- Slice into training/testing sequences
|
|
70
|
+
- Create PyTorch DataLoaders
|
|
71
|
+
"""
|
|
72
|
+
def __init__(self, args):
|
|
73
|
+
self.args = args
|
|
74
|
+
self.df = None
|
|
75
|
+
self.processed_data = None
|
|
76
|
+
|
|
77
|
+
def load_data(self):
|
|
78
|
+
"""Load raw CSV data and preprocess it."""
|
|
79
|
+
self.df = pd.read_csv(self.args["datapath"], index_col=[0])
|
|
80
|
+
self._parse_time_index()
|
|
81
|
+
self._generate_time_features()
|
|
82
|
+
self._scale_features()
|
|
83
|
+
|
|
84
|
+
def _parse_time_index(self):
|
|
85
|
+
"""Convert index to datetime format (auto-detect)."""
|
|
86
|
+
try:
|
|
87
|
+
self.df.index = pd.to_datetime(self.df.index, format="%m/%d/%Y %H:%M")
|
|
88
|
+
except:
|
|
89
|
+
try:
|
|
90
|
+
self.df.index = pd.to_datetime(self.df.index, format="%Y-%m-%d %H:%M:%S")
|
|
91
|
+
except:
|
|
92
|
+
raise ValueError("Unsupported datetime format in index.")
|
|
93
|
+
|
|
94
|
+
def _generate_time_features(self):
|
|
95
|
+
"""Add time-of-day features (sin and cos)."""
|
|
96
|
+
if "day_sin" not in self.df.columns:
|
|
97
|
+
time_hours = self.df.index.hour + self.df.index.minute / 60
|
|
98
|
+
self.df["day_sin"] = np.sin(2 * np.pi * time_hours / 24)
|
|
99
|
+
self.df["day_cos"] = np.cos(2 * np.pi * time_hours / 24)
|
|
100
|
+
|
|
101
|
+
def _scale_features(self):
|
|
102
|
+
"""Apply MinMax scaling to each feature and save scalers."""
|
|
103
|
+
features = ["temp_room", "temp_amb", "solar", "occ", "phvac"]
|
|
104
|
+
scalers = {f: MinMaxScaler(feature_range=(-1, 1)) for f in features}
|
|
105
|
+
scalers["phvac"] = MinMaxScaler(feature_range=(-1*self.args["scale"], 1*self.args["scale"]))
|
|
106
|
+
scaled_data = [scalers[f].fit_transform(self.df[[f]]) for f in features]
|
|
107
|
+
|
|
108
|
+
# Save scalers for inference
|
|
109
|
+
os.makedirs("../Scaler", exist_ok=True)
|
|
110
|
+
with open("ModNN_scaler.pkl", "wb") as f:
|
|
111
|
+
pickle.dump(scalers, f)
|
|
112
|
+
|
|
113
|
+
self.scalers = scalers
|
|
114
|
+
self.processed_data = np.hstack([
|
|
115
|
+
scaled_data[0], # temp_room
|
|
116
|
+
scaled_data[1], # temp_amb
|
|
117
|
+
scaled_data[2], # solar
|
|
118
|
+
self.df[["day_sin", "day_cos"]].values, # keep as-is (since it is already within -1 to 1)
|
|
119
|
+
scaled_data[3], # occ
|
|
120
|
+
scaled_data[4], # phvac
|
|
121
|
+
])
|
|
122
|
+
|
|
123
|
+
def prepare_data_splits(self):
|
|
124
|
+
"""Split into training and testing datasets."""
|
|
125
|
+
res = int(1440 / self.args["resolution"])
|
|
126
|
+
start, train, test = self.args["startday"], self.args["trainday"], self.args["testday"]
|
|
127
|
+
en_len, de_len = self.args["enLen"], self.args["deLen"]
|
|
128
|
+
|
|
129
|
+
self.trainingdf = self.processed_data[res * start : res * (start + train)]
|
|
130
|
+
# offset a little bit, since the prediction needs to start from 12:00
|
|
131
|
+
self.testingdf = self.processed_data[res * (start + train) - en_len :
|
|
132
|
+
res * (start + train + test) + de_len]
|
|
133
|
+
|
|
134
|
+
self.test_raw_df = self.df.iloc[res * (start + train): res * (start + train + test + 1)]
|
|
135
|
+
self.test_start = self.test_raw_df.index[0].strftime("%m-%d")
|
|
136
|
+
self.test_end = self.test_raw_df.index[-1].strftime("%m-%d")
|
|
137
|
+
|
|
138
|
+
def create_dataloaders(self):
|
|
139
|
+
"""Generate PyTorch dataloaders."""
|
|
140
|
+
self.TrainLoader, self.ValidLoader = self._create_dataloader(self.trainingdf,
|
|
141
|
+
self.args["training_batch"],
|
|
142
|
+
shuffle=True, split=0.3)
|
|
143
|
+
self.TestLoader = self._create_dataloader(self.testingdf, batch_size=len(self.testingdf), shuffle=False)
|
|
144
|
+
|
|
145
|
+
def _create_dataloader(self, data, batch_size, shuffle, split=None):
|
|
146
|
+
"""Internal function to create dataset + optional split."""
|
|
147
|
+
X, y = self._generate_sequences(data)
|
|
148
|
+
dataset = MyData(X, y)
|
|
149
|
+
|
|
150
|
+
if split:
|
|
151
|
+
train_size = int((1 - split) * len(dataset))
|
|
152
|
+
valid_size = len(dataset) - train_size
|
|
153
|
+
train_ds, valid_ds = random_split(dataset, [train_size, valid_size])
|
|
154
|
+
return DataLoader(train_ds, batch_size=batch_size, shuffle=shuffle), \
|
|
155
|
+
DataLoader(valid_ds, batch_size=batch_size, shuffle=shuffle)
|
|
156
|
+
|
|
157
|
+
return DataLoader(dataset, batch_size=batch_size, shuffle=shuffle)
|
|
158
|
+
|
|
159
|
+
def _generate_sequences(self, data):
|
|
160
|
+
"""Slice data into overlapping sequences of encoder+decoder length."""
|
|
161
|
+
en_len, de_len = self.args["enLen"], self.args["deLen"]
|
|
162
|
+
X, y = [], []
|
|
163
|
+
for i in range(len(data) - (en_len + de_len)):
|
|
164
|
+
seq = data[i: i + en_len + de_len]
|
|
165
|
+
# Don't be surprise why X and Y are suing same index
|
|
166
|
+
# The offset was considered in model itself
|
|
167
|
+
X.append(seq)
|
|
168
|
+
y.append(seq)
|
|
169
|
+
return X, y
|
|
170
|
+
|
|
171
|
+
def cook(self):
|
|
172
|
+
"""Run the full pipeline."""
|
|
173
|
+
self.load_data()
|
|
174
|
+
self.prepare_data_splits()
|
|
175
|
+
self.create_dataloaders()
|