pinnse 0.0.4__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.
- pinnse/PINNs.py +274 -0
- pinnse/__init__.py +20 -0
- pinnse/data.py +317 -0
- pinnse/plots.py +530 -0
- pinnse/train.py +587 -0
- pinnse/utils.py +738 -0
- pinnse-0.0.4.dist-info/METADATA +383 -0
- pinnse-0.0.4.dist-info/RECORD +11 -0
- pinnse-0.0.4.dist-info/WHEEL +5 -0
- pinnse-0.0.4.dist-info/licenses/LICENSE +21 -0
- pinnse-0.0.4.dist-info/top_level.txt +1 -0
pinnse/PINNs.py
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import torch.nn as nn
|
|
2
|
+
import torch
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Neural network architectures for PINN model.
|
|
6
|
+
|
|
7
|
+
This module defines a collection of feedforward neural network architectures
|
|
8
|
+
that can be used within the proposed framework. The provided
|
|
9
|
+
models include:
|
|
10
|
+
|
|
11
|
+
- ANN : standard fully connected feedforward neural network
|
|
12
|
+
- BranchedANN : shared-trunk network with multiple output heads
|
|
13
|
+
- Fourier_ANN : feedforward network with Fourier-feature augmentation
|
|
14
|
+
|
|
15
|
+
These architectures are designed to support flexible experimentation with
|
|
16
|
+
different network structures while maintaining a common PyTorch-based interface.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ANN(nn.Module):
|
|
21
|
+
"""
|
|
22
|
+
Standard fully connected feedforward neural network.
|
|
23
|
+
|
|
24
|
+
Inputs
|
|
25
|
+
------
|
|
26
|
+
layer_size : list[int]
|
|
27
|
+
List specifying the size of each layer, including input and output
|
|
28
|
+
dimensions. For example, [dim_in, 64, 64, dim_out].
|
|
29
|
+
|
|
30
|
+
activation : type[nn.Module]
|
|
31
|
+
PyTorch activation class used after each hidden linear layer,
|
|
32
|
+
e.g. `nn.Tanh`, `nn.ReLU`, or `nn.Sigmoid`.
|
|
33
|
+
|
|
34
|
+
Returns
|
|
35
|
+
-------
|
|
36
|
+
torch.Tensor
|
|
37
|
+
Predicted output tensor of shape `(batch_size, dim_out)`.
|
|
38
|
+
|
|
39
|
+
Notes
|
|
40
|
+
-----
|
|
41
|
+
- All linear-layer weights are initialized using Xavier uniform
|
|
42
|
+
initialization, and biases are initialized to zero.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, layer_size: list[int], activation: type[nn.Module]):
|
|
46
|
+
super().__init__()
|
|
47
|
+
|
|
48
|
+
layers = []
|
|
49
|
+
|
|
50
|
+
for i in range(len(layer_size) - 2):
|
|
51
|
+
layers.append(nn.Linear(layer_size[i], layer_size[i + 1]))
|
|
52
|
+
layers.append(activation())
|
|
53
|
+
|
|
54
|
+
layers.append(nn.Linear(layer_size[-2], layer_size[-1]))
|
|
55
|
+
self.net = nn.Sequential(*layers)
|
|
56
|
+
|
|
57
|
+
for m in self.modules():
|
|
58
|
+
if isinstance(m, nn.Linear):
|
|
59
|
+
nn.init.xavier_uniform_(m.weight)
|
|
60
|
+
nn.init.zeros_(m.bias)
|
|
61
|
+
|
|
62
|
+
def forward(self, x):
|
|
63
|
+
return self.net(x)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class SANN(nn.Module):
|
|
67
|
+
"""
|
|
68
|
+
Standard fully connected feedforward neural network with softplus output activation.
|
|
69
|
+
|
|
70
|
+
Inputs
|
|
71
|
+
------
|
|
72
|
+
layer_size : list[int]
|
|
73
|
+
List specifying the size of each layer, including input and output
|
|
74
|
+
dimensions. For example, [dim_in, 64, 64, dim_out].
|
|
75
|
+
|
|
76
|
+
activation : type[nn.Module]
|
|
77
|
+
PyTorch activation class used after each hidden linear layer,
|
|
78
|
+
e.g. `nn.Tanh`, `nn.ReLU`, or `nn.Sigmoid`.
|
|
79
|
+
|
|
80
|
+
Returns
|
|
81
|
+
-------
|
|
82
|
+
torch.Tensor
|
|
83
|
+
Predicted output tensor of shape `(batch_size, dim_out)`.
|
|
84
|
+
|
|
85
|
+
Notes
|
|
86
|
+
-----
|
|
87
|
+
- All linear-layer weights are initialized using Xavier uniform
|
|
88
|
+
initialization, and biases are initialized to zero.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
def __init__(self, layer_size: list[int], activation: type[nn.Module]):
|
|
92
|
+
super().__init__()
|
|
93
|
+
|
|
94
|
+
layers = []
|
|
95
|
+
|
|
96
|
+
for i in range(len(layer_size) - 2):
|
|
97
|
+
layers.append(nn.Linear(layer_size[i], layer_size[i + 1]))
|
|
98
|
+
layers.append(activation())
|
|
99
|
+
|
|
100
|
+
layers.append(nn.Linear(layer_size[-2], layer_size[-1]))
|
|
101
|
+
self.net = nn.Sequential(*layers)
|
|
102
|
+
self.softplus = nn.Softplus()
|
|
103
|
+
|
|
104
|
+
for m in self.modules():
|
|
105
|
+
if isinstance(m, nn.Linear):
|
|
106
|
+
nn.init.xavier_uniform_(m.weight)
|
|
107
|
+
nn.init.zeros_(m.bias)
|
|
108
|
+
|
|
109
|
+
def forward(self, x):
|
|
110
|
+
raw_output = self.net(x)
|
|
111
|
+
return self.softplus(raw_output)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class BranchedANN(nn.Module):
|
|
115
|
+
"""
|
|
116
|
+
Branched feedforward neural network with a shared trunk and multiple heads.
|
|
117
|
+
|
|
118
|
+
Inputs
|
|
119
|
+
------
|
|
120
|
+
in_dim : int
|
|
121
|
+
Input dimension of the network.
|
|
122
|
+
|
|
123
|
+
trunk_layers : list[int]
|
|
124
|
+
List specifying the hidden-layer sizes of the shared trunk network.
|
|
125
|
+
|
|
126
|
+
head_dims : dict[str, int]
|
|
127
|
+
Dictionary specifying the output heads, where each key is the head
|
|
128
|
+
name and each value is the corresponding output dimension.
|
|
129
|
+
|
|
130
|
+
activation : type[nn.Module]
|
|
131
|
+
PyTorch activation class used after each hidden linear layer.
|
|
132
|
+
|
|
133
|
+
Returns
|
|
134
|
+
-------
|
|
135
|
+
dict[str, torch.Tensor]
|
|
136
|
+
Dictionary mapping each head name to its predicted output tensor.
|
|
137
|
+
|
|
138
|
+
Notes
|
|
139
|
+
-----
|
|
140
|
+
- The trunk learns a shared latent representation from the input.
|
|
141
|
+
- Each output head receives the shared trunk representation and predicts
|
|
142
|
+
a separate output block.
|
|
143
|
+
- This architecture is useful when multiple outputs have related but
|
|
144
|
+
distinct physical behavior.
|
|
145
|
+
- All linear-layer weights are initialized using Xavier uniform
|
|
146
|
+
initialization, and biases are initialized to zero.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
def __init__(
|
|
150
|
+
self,
|
|
151
|
+
in_dim: int,
|
|
152
|
+
trunk_layers: list[int],
|
|
153
|
+
head_dims: dict[str, int],
|
|
154
|
+
activation: type[nn.Module],
|
|
155
|
+
):
|
|
156
|
+
super().__init__()
|
|
157
|
+
|
|
158
|
+
trunk = []
|
|
159
|
+
dims = [in_dim] + trunk_layers
|
|
160
|
+
for i in range(len(dims) - 1):
|
|
161
|
+
trunk.append(nn.Linear(dims[i], dims[i + 1]))
|
|
162
|
+
trunk.append(activation())
|
|
163
|
+
self.trunk = nn.Sequential(*trunk)
|
|
164
|
+
|
|
165
|
+
hidden_dim = trunk_layers[-1]
|
|
166
|
+
|
|
167
|
+
self.heads = nn.ModuleDict(
|
|
168
|
+
{
|
|
169
|
+
name: nn.Sequential(
|
|
170
|
+
nn.Linear(hidden_dim, hidden_dim),
|
|
171
|
+
activation(),
|
|
172
|
+
nn.Linear(hidden_dim, out_dim),
|
|
173
|
+
)
|
|
174
|
+
for name, out_dim in head_dims.items()
|
|
175
|
+
}
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
for m in self.modules():
|
|
179
|
+
if isinstance(m, nn.Linear):
|
|
180
|
+
nn.init.xavier_uniform_(m.weight)
|
|
181
|
+
nn.init.zeros_(m.bias)
|
|
182
|
+
|
|
183
|
+
def forward(self, x):
|
|
184
|
+
h = self.trunk(x)
|
|
185
|
+
outputs = {name: head(h) for name, head in self.heads.items()}
|
|
186
|
+
return outputs
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class Fourier_ANN(nn.Module):
|
|
190
|
+
"""
|
|
191
|
+
Feedforward neural network with Fourier-feature augmentation.
|
|
192
|
+
|
|
193
|
+
Inputs
|
|
194
|
+
------
|
|
195
|
+
layer_size : list[int]
|
|
196
|
+
List specifying the size of each layer before Fourier augmentation,
|
|
197
|
+
including input and output dimensions.
|
|
198
|
+
|
|
199
|
+
activation : type[nn.Module], optional, default=nn.Tanh
|
|
200
|
+
PyTorch activation class used after each hidden linear layer.
|
|
201
|
+
|
|
202
|
+
fourier_levels : int, optional, default=6
|
|
203
|
+
Number of Fourier frequency levels used to augment the last input
|
|
204
|
+
coordinate.
|
|
205
|
+
|
|
206
|
+
Returns
|
|
207
|
+
-------
|
|
208
|
+
torch.Tensor
|
|
209
|
+
Predicted output tensor of shape `(batch_size, dim_out)`.
|
|
210
|
+
|
|
211
|
+
Notes
|
|
212
|
+
-----
|
|
213
|
+
- The last input coordinate is mapped to sinusoidal Fourier features
|
|
214
|
+
using sine and cosine functions.
|
|
215
|
+
- If `fourier_levels > 0`, the original last input variable is replaced
|
|
216
|
+
by its Fourier-feature representation.
|
|
217
|
+
- The augmented feature vector is then passed through a standard
|
|
218
|
+
feedforward neural network.
|
|
219
|
+
- A Softplus activation is applied to the final network output to enforce
|
|
220
|
+
nonnegative predictions.
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
def __init__(
|
|
224
|
+
self,
|
|
225
|
+
layer_size: list[int],
|
|
226
|
+
activation: type[nn.Module] = nn.Tanh,
|
|
227
|
+
fourier_levels: int = 6,
|
|
228
|
+
positive_output: bool = False,
|
|
229
|
+
):
|
|
230
|
+
super().__init__()
|
|
231
|
+
|
|
232
|
+
self.fourier_levels = fourier_levels
|
|
233
|
+
self.positive_output = positive_output
|
|
234
|
+
|
|
235
|
+
d_in = layer_size[0]
|
|
236
|
+
net_layer_size = layer_size.copy()
|
|
237
|
+
|
|
238
|
+
if self.fourier_levels > 0:
|
|
239
|
+
extra = 2 * self.fourier_levels
|
|
240
|
+
net_layer_size[0] = (d_in - 1) + extra
|
|
241
|
+
|
|
242
|
+
layers = []
|
|
243
|
+
for i in range(len(net_layer_size) - 2):
|
|
244
|
+
layers.append(nn.Linear(net_layer_size[i], net_layer_size[i + 1]))
|
|
245
|
+
layers.append(activation())
|
|
246
|
+
layers.append(nn.Linear(net_layer_size[-2], net_layer_size[-1]))
|
|
247
|
+
|
|
248
|
+
self.net = nn.Sequential(*layers)
|
|
249
|
+
self.softplus = nn.Softplus()
|
|
250
|
+
|
|
251
|
+
for m in self.modules():
|
|
252
|
+
if isinstance(m, nn.Linear):
|
|
253
|
+
nn.init.xavier_uniform_(m.weight)
|
|
254
|
+
nn.init.zeros_(m.bias)
|
|
255
|
+
|
|
256
|
+
def forward(self, x):
|
|
257
|
+
if self.fourier_levels > 0:
|
|
258
|
+
V = x[:, -1:].contiguous()
|
|
259
|
+
x_base = x[:, :-1]
|
|
260
|
+
|
|
261
|
+
device = x.device
|
|
262
|
+
dtype = x.dtype
|
|
263
|
+
|
|
264
|
+
omegas = (
|
|
265
|
+
(2.0 ** torch.arange(self.fourier_levels, device=device, dtype=dtype))
|
|
266
|
+
* torch.pi
|
|
267
|
+
).unsqueeze(0)
|
|
268
|
+
|
|
269
|
+
args = V * omegas
|
|
270
|
+
fourier_feats = torch.cat([torch.sin(args), torch.cos(args)], dim=1)
|
|
271
|
+
x = torch.cat([x_base, fourier_feats], dim=1)
|
|
272
|
+
|
|
273
|
+
raw = self.net(x)
|
|
274
|
+
return self.softplus(raw) if self.positive_output else raw
|
pinnse/__init__.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from .PINNs import ANN, SANN, BranchedANN, Fourier_ANN
|
|
2
|
+
from .data import DataModule, DataLoader
|
|
3
|
+
from .train import Training
|
|
4
|
+
from .plots import Plotter
|
|
5
|
+
from .utils import Normalization, Denormalization, Save, Analyze
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"ANN",
|
|
9
|
+
"SANN",
|
|
10
|
+
"BranchedANN",
|
|
11
|
+
"Fourier_ANN",
|
|
12
|
+
"DataModule",
|
|
13
|
+
"DataLoader",
|
|
14
|
+
"Training",
|
|
15
|
+
"Plotter",
|
|
16
|
+
"Normalization",
|
|
17
|
+
"Denormalization",
|
|
18
|
+
"Save",
|
|
19
|
+
"Analyze",
|
|
20
|
+
]
|
pinnse/data.py
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import numpy as np
|
|
3
|
+
import torch
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from torch.utils.data import TensorDataset, DataLoader
|
|
6
|
+
from sklearn.model_selection import train_test_split
|
|
7
|
+
from scipy.stats import qmc
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DataModule:
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
I_S_data: pd.DataFrame,
|
|
14
|
+
D_S_data: pd.DataFrame,
|
|
15
|
+
labeled_data_batch_size: int,
|
|
16
|
+
physics_coll_data_size: Optional[int] = None,
|
|
17
|
+
physics_coll_batch_size: Optional[int] = None,
|
|
18
|
+
boundary_coll_data_size: Optional[int] = None,
|
|
19
|
+
boundry_coll_batch_size: Optional[int] = None,
|
|
20
|
+
test_frac: Optional[float] = None,
|
|
21
|
+
val_frac: Optional[float] = None,
|
|
22
|
+
random_state: Optional[int] = 42,
|
|
23
|
+
):
|
|
24
|
+
self.I_S_data = I_S_data
|
|
25
|
+
self.D_S_data = D_S_data
|
|
26
|
+
self.labeled_data_batch_size = labeled_data_batch_size
|
|
27
|
+
self.physics_coll_data_size = physics_coll_data_size
|
|
28
|
+
self.physics_coll_batch_size = physics_coll_batch_size
|
|
29
|
+
self.boundry_coll_data_size = boundary_coll_data_size
|
|
30
|
+
self.boundry_coll_batch_size = boundry_coll_batch_size
|
|
31
|
+
self.test_frac = test_frac
|
|
32
|
+
self.val_frac = val_frac
|
|
33
|
+
self.random_state = random_state
|
|
34
|
+
|
|
35
|
+
def labeled_data_loader(self):
|
|
36
|
+
"""
|
|
37
|
+
Construct labeled DataLoaders for training, validation, and test datasets.
|
|
38
|
+
|
|
39
|
+
Inputs
|
|
40
|
+
------
|
|
41
|
+
None
|
|
42
|
+
Uses I_S as the input dataset and D_S as the output dataset.
|
|
43
|
+
|
|
44
|
+
Returns
|
|
45
|
+
-------
|
|
46
|
+
train_loader : torch.utils.data.DataLoader
|
|
47
|
+
DataLoader containing the training subset.
|
|
48
|
+
|
|
49
|
+
val_loader : torch.utils.data.DataLoader
|
|
50
|
+
DataLoader containing the validation subset.
|
|
51
|
+
|
|
52
|
+
test_loader : torch.utils.data.DataLoader
|
|
53
|
+
DataLoader containing the test subset.
|
|
54
|
+
"""
|
|
55
|
+
X = self.I_S_data.to_numpy(dtype=np.float32)
|
|
56
|
+
Y = self.D_S_data.to_numpy(dtype=np.float32)
|
|
57
|
+
|
|
58
|
+
self.lower_bnd = X.min(axis=0)
|
|
59
|
+
self.upper_bnd = X.max(axis=0)
|
|
60
|
+
|
|
61
|
+
X_tv, X_test, Y_tv, Y_test = train_test_split(
|
|
62
|
+
X, Y, test_size=self.test_frac, random_state=self.random_state
|
|
63
|
+
)
|
|
64
|
+
X_train, X_val, Y_train, Y_val = train_test_split(
|
|
65
|
+
X_tv, Y_tv, test_size=self.val_frac, random_state=self.random_state
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def make_loader(
|
|
69
|
+
X: np.ndarray,
|
|
70
|
+
Y: np.ndarray,
|
|
71
|
+
shuffle: bool,
|
|
72
|
+
drop_last: bool = False,
|
|
73
|
+
):
|
|
74
|
+
X_t = torch.from_numpy(X)
|
|
75
|
+
Y_t = torch.from_numpy(Y)
|
|
76
|
+
dataset = TensorDataset(X_t, Y_t)
|
|
77
|
+
return DataLoader(
|
|
78
|
+
dataset=dataset,
|
|
79
|
+
batch_size=self.labeled_data_batch_size,
|
|
80
|
+
shuffle=shuffle,
|
|
81
|
+
drop_last=drop_last,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
train_loader = make_loader(X_train, Y_train, shuffle=True)
|
|
85
|
+
val_loader = make_loader(X_val, Y_val, shuffle=False)
|
|
86
|
+
test_loader = make_loader(X_test, Y_test, shuffle=False)
|
|
87
|
+
|
|
88
|
+
return train_loader, val_loader, test_loader
|
|
89
|
+
|
|
90
|
+
def phys_colloc_loader(
|
|
91
|
+
self,
|
|
92
|
+
shuffle: Optional[bool] = True,
|
|
93
|
+
alpha: Optional[float] = 1.0,
|
|
94
|
+
drop_last: bool = False,
|
|
95
|
+
):
|
|
96
|
+
"""
|
|
97
|
+
Generate a physics-collocation DataLoader by sampling the surrogate input space.
|
|
98
|
+
|
|
99
|
+
Inputs
|
|
100
|
+
------
|
|
101
|
+
shuffle : bool, optional, default=True
|
|
102
|
+
Whether to shuffle the collocation samples in the returned DataLoader.
|
|
103
|
+
|
|
104
|
+
alpha : float, optional, default=1.0
|
|
105
|
+
Dirichlet concentration parameter used for sampling molar-fraction
|
|
106
|
+
variables whose names start with 'Z_', 'X_', or 'Y_'.
|
|
107
|
+
A value of 1.0 gives a uniform distribution over the simplex.
|
|
108
|
+
|
|
109
|
+
drop_last : bool, optional, default=False
|
|
110
|
+
Whether to drop the last incomplete batch in the DataLoader.
|
|
111
|
+
|
|
112
|
+
Returns
|
|
113
|
+
-------
|
|
114
|
+
DataLoader
|
|
115
|
+
PyTorch DataLoader containing collocation inputs only.
|
|
116
|
+
Each batch has the form (X_coll,), where X_coll is a tensor of shape
|
|
117
|
+
(batch_size, n_input_features).
|
|
118
|
+
|
|
119
|
+
Notes
|
|
120
|
+
-----
|
|
121
|
+
- Variables with prefixes 'Z_', 'X_', and 'Y_' are sampled using
|
|
122
|
+
Dirichlet distributions so that each group sums to 1.
|
|
123
|
+
- All other input variables are sampled using Latin Hypercube Sampling
|
|
124
|
+
"""
|
|
125
|
+
rng = np.random.default_rng(self.random_state)
|
|
126
|
+
N_colloc = (
|
|
127
|
+
self.physics_coll_data_size
|
|
128
|
+
if self.physics_coll_data_size is not None
|
|
129
|
+
else 0
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
in_cols = list(self.I_S_data.columns)
|
|
133
|
+
simplex_prefixes = ("Z_", "X_", "Y_")
|
|
134
|
+
|
|
135
|
+
box_cols = [col for col in in_cols if not col.startswith(simplex_prefixes)]
|
|
136
|
+
dirichlet_groups = {
|
|
137
|
+
prefix: [col for col in in_cols if col.startswith(prefix)]
|
|
138
|
+
for prefix in simplex_prefixes
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
parts = {}
|
|
142
|
+
|
|
143
|
+
# Latin Hypercube sampling for ordinary bounded variables
|
|
144
|
+
if box_cols:
|
|
145
|
+
box_idx = [in_cols.index(col) for col in box_cols]
|
|
146
|
+
lb = self.lower_bnd[box_idx].astype(np.float32)
|
|
147
|
+
ub = self.upper_bnd[box_idx].astype(np.float32)
|
|
148
|
+
|
|
149
|
+
sampler = qmc.LatinHypercube(d=len(box_cols), rng=rng)
|
|
150
|
+
unit = sampler.random(N_colloc)
|
|
151
|
+
X_box = qmc.scale(unit, lb, ub).astype(np.float32)
|
|
152
|
+
for j, col in enumerate(box_cols):
|
|
153
|
+
parts[col] = X_box[:, j : j + 1]
|
|
154
|
+
|
|
155
|
+
# Dirichlet sampling for molar fraction variables
|
|
156
|
+
for prefix, cols in dirichlet_groups.items():
|
|
157
|
+
if cols:
|
|
158
|
+
X_dir = rng.dirichlet(
|
|
159
|
+
alpha=np.full(len(cols), alpha, dtype=np.float32), size=N_colloc
|
|
160
|
+
).astype(np.float32)
|
|
161
|
+
|
|
162
|
+
for j, col in enumerate(cols):
|
|
163
|
+
parts[col] = X_dir[:, j : j + 1]
|
|
164
|
+
|
|
165
|
+
# Reassemble in original I_S column order
|
|
166
|
+
X_coll = np.hstack([parts[col] for col in in_cols]).astype(np.float32)
|
|
167
|
+
X_coll = torch.from_numpy(X_coll)
|
|
168
|
+
|
|
169
|
+
dataset = TensorDataset(X_coll)
|
|
170
|
+
return DataLoader(
|
|
171
|
+
dataset=dataset,
|
|
172
|
+
batch_size=self.physics_coll_batch_size,
|
|
173
|
+
shuffle=shuffle,
|
|
174
|
+
drop_last=drop_last,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def bnd_colloc_loader(
|
|
178
|
+
self, shuffle: bool = True, drop_last: bool = False, bnd_value: float = -1.0
|
|
179
|
+
):
|
|
180
|
+
"""
|
|
181
|
+
Construct a boundary-collocation DataLoader by fixing the last input column
|
|
182
|
+
to a prescribed boundary value and sampling all preceding input columns
|
|
183
|
+
within their admissible bounds.
|
|
184
|
+
|
|
185
|
+
Inputs
|
|
186
|
+
------
|
|
187
|
+
shuffle : bool, optional, default=True
|
|
188
|
+
Whether to shuffle the collocation samples.
|
|
189
|
+
|
|
190
|
+
drop_last : bool, optional, default=False
|
|
191
|
+
Whether to drop the last incomplete batch.
|
|
192
|
+
|
|
193
|
+
boundary_value : float, optional, default=-1.0
|
|
194
|
+
Boundary value imposed on the last input column (normalized).
|
|
195
|
+
|
|
196
|
+
Returns
|
|
197
|
+
-------
|
|
198
|
+
DataLoader
|
|
199
|
+
DataLoader containing boundary-collocation inputs only.
|
|
200
|
+
|
|
201
|
+
Notes
|
|
202
|
+
-----
|
|
203
|
+
- The last column of `I_S` is treated as the boundary variable.
|
|
204
|
+
- All other input variables are sampled using Latin Hypercube Sampling
|
|
205
|
+
over their admissible bounds.
|
|
206
|
+
"""
|
|
207
|
+
rng = np.random.default_rng(self.random_state)
|
|
208
|
+
dim_in = self.I_S_data.shape[1]
|
|
209
|
+
|
|
210
|
+
N_bc = (
|
|
211
|
+
self.boundry_coll_data_size
|
|
212
|
+
if self.boundry_coll_data_size is not None
|
|
213
|
+
else 0
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
low = self.lower_bnd[:-1].astype(np.float32)
|
|
217
|
+
up = self.upper_bnd[:-1].astype(np.float32)
|
|
218
|
+
|
|
219
|
+
sampler = qmc.LatinHypercube(d=dim_in - 1, rng=rng)
|
|
220
|
+
unit = sampler.random(N_bc)
|
|
221
|
+
X_free = qmc.scale(unit, low, up).astype(np.float32)
|
|
222
|
+
|
|
223
|
+
X_bc = np.hstack([X_free, np.full((N_bc, 1), bnd_value, dtype=np.float32)])
|
|
224
|
+
X_bc = torch.from_numpy(X_bc)
|
|
225
|
+
dataset = TensorDataset(X_bc)
|
|
226
|
+
return DataLoader(
|
|
227
|
+
dataset=dataset,
|
|
228
|
+
batch_size=self.boundry_coll_batch_size,
|
|
229
|
+
shuffle=shuffle,
|
|
230
|
+
drop_last=drop_last,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
def inspect_loader(self, name: str, loader: torch.utils.data.DataLoader):
|
|
234
|
+
"""
|
|
235
|
+
Inspect a constructed DataLoader.
|
|
236
|
+
|
|
237
|
+
Inputs
|
|
238
|
+
------
|
|
239
|
+
name : str
|
|
240
|
+
Name of the loader to be displayed in the printed summary.
|
|
241
|
+
|
|
242
|
+
loader : torch.utils.data.DataLoader
|
|
243
|
+
DataLoader to inspect. The loader may contain either:
|
|
244
|
+
- labeled batches of the form (X, Y), or
|
|
245
|
+
- unlabeled batches of the form (X,).
|
|
246
|
+
|
|
247
|
+
Returns
|
|
248
|
+
-------
|
|
249
|
+
None
|
|
250
|
+
Prints the shape of the batch tensors and the number of batches
|
|
251
|
+
in the loader.
|
|
252
|
+
|
|
253
|
+
Notes
|
|
254
|
+
-----
|
|
255
|
+
- For labeled loaders, prints the shapes of both input and output tensors.
|
|
256
|
+
- For unlabeled (collocattion) loaders, prints only the shape of the input tensor.
|
|
257
|
+
"""
|
|
258
|
+
batch = next(iter(loader))
|
|
259
|
+
if isinstance(batch, (list, tuple)) and len(batch) == 2:
|
|
260
|
+
X, Y = batch
|
|
261
|
+
print(
|
|
262
|
+
f"{name}_loader -> X: {X.shape}, Y: {Y.shape}, batches: {len(loader)}"
|
|
263
|
+
)
|
|
264
|
+
else:
|
|
265
|
+
X = batch[0] if isinstance(batch, (list, tuple)) else batch
|
|
266
|
+
print(f"{name}_loader -> X: {X.shape}, batches: {len(loader)}")
|
|
267
|
+
return
|
|
268
|
+
|
|
269
|
+
def save_loaders(self, loader, filename):
|
|
270
|
+
"""
|
|
271
|
+
Save the contents of a DataLoader to an Excel file.
|
|
272
|
+
|
|
273
|
+
Inputs
|
|
274
|
+
------
|
|
275
|
+
loader : torch.utils.data.DataLoader
|
|
276
|
+
DataLoader to save. The loader may contain either:
|
|
277
|
+
- labeled batches of the form (X, Y), or
|
|
278
|
+
- unlabeled batches of the form (X,).
|
|
279
|
+
|
|
280
|
+
filename : str
|
|
281
|
+
Name or path of the output Excel file.
|
|
282
|
+
|
|
283
|
+
Returns
|
|
284
|
+
-------
|
|
285
|
+
|
|
286
|
+
Notes
|
|
287
|
+
-----
|
|
288
|
+
- For labeled loaders, the saved file contains both input and output
|
|
289
|
+
columns concatenated side by side.
|
|
290
|
+
- For unlabeled loaders, only the input columns are saved.
|
|
291
|
+
"""
|
|
292
|
+
X_all, Y_all = [], []
|
|
293
|
+
|
|
294
|
+
for batch in loader:
|
|
295
|
+
if isinstance(batch, (list, tuple)) and len(batch) == 2:
|
|
296
|
+
X, Y = batch
|
|
297
|
+
X_all.append(X.detach().cpu().numpy())
|
|
298
|
+
Y_all.append(Y.detach().cpu().numpy())
|
|
299
|
+
else:
|
|
300
|
+
X = batch[0] if isinstance(batch, (list, tuple)) else batch
|
|
301
|
+
X_all.append(X.detach().cpu().numpy())
|
|
302
|
+
|
|
303
|
+
if not X_all:
|
|
304
|
+
raise ValueError("The provided DataLoader is empty.")
|
|
305
|
+
|
|
306
|
+
X_all = np.vstack(X_all)
|
|
307
|
+
df_X = pd.DataFrame(X_all, columns=self.I_S_data.columns)
|
|
308
|
+
|
|
309
|
+
if Y_all:
|
|
310
|
+
Y_all = np.vstack(Y_all)
|
|
311
|
+
df_Y = pd.DataFrame(Y_all, columns=self.D_S_data.columns)
|
|
312
|
+
df = pd.concat([df_X, df_Y], axis=1)
|
|
313
|
+
else:
|
|
314
|
+
df = df_X
|
|
315
|
+
|
|
316
|
+
df.to_excel(filename, index=False)
|
|
317
|
+
print(f"Saved {len(df)} samples to {filename}")
|