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 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()