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 +7 -0
- modnn-1.0.0/PKG-INFO +65 -0
- modnn-1.0.0/README.md +47 -0
- modnn-1.0.0/modnn/Config.py +109 -0
- modnn-1.0.0/modnn/Dataset.py +175 -0
- modnn-1.0.0/modnn/Play.py +484 -0
- modnn-1.0.0/modnn/__init__.py +0 -0
- modnn-1.0.0/modnn/run.py +22 -0
- modnn-1.0.0/modnn/utils.py +541 -0
- modnn-1.0.0/modnn.egg-info/PKG-INFO +65 -0
- modnn-1.0.0/modnn.egg-info/SOURCES.txt +14 -0
- modnn-1.0.0/modnn.egg-info/dependency_links.txt +1 -0
- modnn-1.0.0/modnn.egg-info/requires.txt +7 -0
- modnn-1.0.0/modnn.egg-info/top_level.txt +1 -0
- modnn-1.0.0/pyproject.toml +27 -0
- modnn-1.0.0/setup.cfg +4 -0
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()
|