modnn 1.0.0__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-1.0.0/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright 2024-2025 Zixin
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the β€œSoftware”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED β€œAS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
modnn-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,65 @@
1
+ Metadata-Version: 2.4
2
+ Name: modnn
3
+ Version: 1.0.0
4
+ Summary: Physics-Informed Modularized Neural Network for Building Energy Modeling
5
+ Author-email: Zixin Jiang <zjiang19@syr.edu>
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.7
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: numpy
11
+ Requires-Dist: torch
12
+ Requires-Dist: pandas
13
+ Requires-Dist: matplotlib
14
+ Requires-Dist: seaborn
15
+ Requires-Dist: scikit-learn
16
+ Requires-Dist: tqdm
17
+ Dynamic: license-file
18
+
19
+ # ModNN
20
+
21
+ **ModNN** is a Modularized Physics-Informed Neural Network for building energy modeling.
22
+
23
+ It incorporates with physics-informed model structure, loss function, and model constraints.
24
+
25
+ ---
26
+
27
+ ## πŸš€ Installation
28
+
29
+ You can install the package using pip very easy:
30
+
31
+ pip install modnn
32
+
33
+
34
+
35
+ ## 🧠 Example
36
+ Please find the online Jupyter notebook for a step-by-step instruction
37
+
38
+
39
+ ## πŸ§ͺ Requirements
40
+
41
+ Python 3.7+
42
+
43
+ PyTorch
44
+
45
+ NumPy
46
+
47
+ Pandas
48
+
49
+ Matplotlib
50
+
51
+ Seaborn
52
+
53
+ scikit-learn
54
+
55
+ tqdm
56
+
57
+ ---
58
+ πŸ“¬ License
59
+
60
+ MIT License
61
+
62
+ πŸ™‹β€β™‚οΈ Author
63
+
64
+ Zixin Jiang:
65
+ zjiang19@syr.edu
modnn-1.0.0/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # ModNN
2
+
3
+ **ModNN** is a Modularized Physics-Informed Neural Network for building energy modeling.
4
+
5
+ It incorporates with physics-informed model structure, loss function, and model constraints.
6
+
7
+ ---
8
+
9
+ ## πŸš€ Installation
10
+
11
+ You can install the package using pip very easy:
12
+
13
+ pip install modnn
14
+
15
+
16
+
17
+ ## 🧠 Example
18
+ Please find the online Jupyter notebook for a step-by-step instruction
19
+
20
+
21
+ ## πŸ§ͺ Requirements
22
+
23
+ Python 3.7+
24
+
25
+ PyTorch
26
+
27
+ NumPy
28
+
29
+ Pandas
30
+
31
+ Matplotlib
32
+
33
+ Seaborn
34
+
35
+ scikit-learn
36
+
37
+ tqdm
38
+
39
+ ---
40
+ πŸ“¬ License
41
+
42
+ MIT License
43
+
44
+ πŸ™‹β€β™‚οΈ Author
45
+
46
+ Zixin Jiang:
47
+ zjiang19@syr.edu
@@ -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
+
@@ -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()