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 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}")