PODImodels 0.0.3__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.
- PODImodels/PODImodels.py +1033 -0
- PODImodels/PODdata.py +522 -0
- PODImodels/__init__.py +61 -0
- PODImodels/podImodelabstract.py +840 -0
- podimodels-0.0.3.dist-info/METADATA +211 -0
- podimodels-0.0.3.dist-info/RECORD +9 -0
- podimodels-0.0.3.dist-info/WHEEL +5 -0
- podimodels-0.0.3.dist-info/licenses/LICENSE +21 -0
- podimodels-0.0.3.dist-info/top_level.txt +1 -0
PODImodels/PODImodels.py
ADDED
|
@@ -0,0 +1,1033 @@
|
|
|
1
|
+
"""
|
|
2
|
+
POD-based Interpolation Models Implementation
|
|
3
|
+
=============================================
|
|
4
|
+
|
|
5
|
+
This module contains concrete implementations of POD-based interpolation models
|
|
6
|
+
that inherit from PODImodelAbstract. These models combine Proper Orthogonal
|
|
7
|
+
Decomposition with various machine learning techniques for reduced-order modeling
|
|
8
|
+
of high-dimensional field data.
|
|
9
|
+
|
|
10
|
+
The models are categorized into two types:
|
|
11
|
+
1. Direct field models (fields*): Learn direct mapping from parameters to field values
|
|
12
|
+
2. POD coefficient models (POD*): Learn mapping from parameters to POD coefficients
|
|
13
|
+
|
|
14
|
+
Available Models
|
|
15
|
+
----------------
|
|
16
|
+
Linear Regression Models:
|
|
17
|
+
- fieldsLinear: Direct field prediction using linear regression
|
|
18
|
+
- PODLinear: POD coefficient prediction using linear regression
|
|
19
|
+
- fieldsRidge: Direct field prediction using Ridge regression
|
|
20
|
+
- PODRidge: POD coefficient prediction using Ridge regression
|
|
21
|
+
|
|
22
|
+
Gaussian Process Regression Models:
|
|
23
|
+
- fieldsGPR: Direct field prediction using Gaussian Process Regression
|
|
24
|
+
- PODGPR: POD coefficient prediction using Gaussian Process Regression
|
|
25
|
+
- fieldsRidgeGPR: Field prediction with Ridge regularization and GPR
|
|
26
|
+
- PODRidgeGPR: POD coefficient prediction with Ridge regularization and GPR
|
|
27
|
+
|
|
28
|
+
Radial Basis Function Models:
|
|
29
|
+
- fieldsRBF: Direct field prediction using Radial Basis Function interpolation
|
|
30
|
+
- PODRBF: POD coefficient prediction using Radial Basis Function interpolation
|
|
31
|
+
- fieldsRidgeRBF: Field prediction with Ridge regularization and RBF
|
|
32
|
+
- PODRidgeRBF: POD coefficient prediction with Ridge regularization and RBF
|
|
33
|
+
|
|
34
|
+
Neural Network Models:
|
|
35
|
+
- PODANN: POD coefficient prediction using Artificial Neural Networks
|
|
36
|
+
|
|
37
|
+
Examples
|
|
38
|
+
--------
|
|
39
|
+
>>> # Gaussian Process Regression for POD coefficients
|
|
40
|
+
>>> model = PODGPR(rank=15, with_scaler_x=True, with_scaler_y=True)
|
|
41
|
+
>>> model.fit(parameters, field_snapshots)
|
|
42
|
+
>>> predictions = model.predict(new_parameters)
|
|
43
|
+
|
|
44
|
+
>>> # Direct field prediction with RBF
|
|
45
|
+
>>> model = fieldsRBF(kernel='thin_plate_spline', degree=2)
|
|
46
|
+
>>> model.fit(parameters, field_snapshots)
|
|
47
|
+
>>> field_prediction = model.predict(test_parameters)
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
import numpy as np
|
|
51
|
+
from sklearn.gaussian_process import GaussianProcessRegressor
|
|
52
|
+
from sklearn.gaussian_process.kernels import RBF, ConstantKernel
|
|
53
|
+
from typing import Optional, List, Tuple, Union, Any
|
|
54
|
+
from sklearn.gaussian_process.kernels import Kernel
|
|
55
|
+
from sklearn.linear_model import Ridge
|
|
56
|
+
from sklearn.linear_model import LinearRegression
|
|
57
|
+
from scipy.linalg import svd
|
|
58
|
+
from scipy.interpolate import RBFInterpolator
|
|
59
|
+
from sklearn.model_selection import train_test_split
|
|
60
|
+
from sklearn.preprocessing import MinMaxScaler
|
|
61
|
+
from .podImodelabstract import PODImodelAbstract
|
|
62
|
+
import torch
|
|
63
|
+
import torch.nn as nn
|
|
64
|
+
import torch.optim as optim
|
|
65
|
+
import random
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class fieldsLinear(PODImodelAbstract):
|
|
69
|
+
"""
|
|
70
|
+
Linear regression model for direct field prediction.
|
|
71
|
+
|
|
72
|
+
This model learns a direct linear mapping from input parameters to field values
|
|
73
|
+
without dimensionality reduction. It's suitable for problems where the full
|
|
74
|
+
field can be effectively approximated by linear combinations of the input parameters.
|
|
75
|
+
|
|
76
|
+
Parameters
|
|
77
|
+
----------
|
|
78
|
+
**kwargs
|
|
79
|
+
Additional keyword arguments passed to the parent PODImodelAbstract class.
|
|
80
|
+
Common parameters include:
|
|
81
|
+
- rank : int, Number of POD modes (not used for direct field models)
|
|
82
|
+
- with_scaler_x : bool, Whether to scale input features
|
|
83
|
+
- with_scaler_y : bool, Whether to scale target values
|
|
84
|
+
|
|
85
|
+
Attributes
|
|
86
|
+
----------
|
|
87
|
+
lin : LinearRegression
|
|
88
|
+
The underlying scikit-learn LinearRegression model.
|
|
89
|
+
|
|
90
|
+
Notes
|
|
91
|
+
-----
|
|
92
|
+
This model does not perform POD decomposition since it predicts fields directly.
|
|
93
|
+
It uses ordinary least squares to find the linear mapping: y = Xβ + ε.
|
|
94
|
+
|
|
95
|
+
Examples
|
|
96
|
+
--------
|
|
97
|
+
>>> model = fieldsLinear(with_scaler_x=True, with_scaler_y=True)
|
|
98
|
+
>>> model.fit(parameters, field_data)
|
|
99
|
+
>>> predictions = model.predict(new_parameters)
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
def __init__(self, **kwargs):
|
|
103
|
+
"""
|
|
104
|
+
Initialize the fieldsLinear model.
|
|
105
|
+
|
|
106
|
+
Parameters
|
|
107
|
+
----------
|
|
108
|
+
**kwargs
|
|
109
|
+
Keyword arguments passed to parent PODImodelAbstract class.
|
|
110
|
+
"""
|
|
111
|
+
super().__init__(**kwargs)
|
|
112
|
+
|
|
113
|
+
self.lin: LinearRegression = LinearRegression()
|
|
114
|
+
|
|
115
|
+
def fit_tmp(self, x: np.ndarray, y: np.ndarray) -> None:
|
|
116
|
+
"""
|
|
117
|
+
Fit the linear regression model to the training data.
|
|
118
|
+
|
|
119
|
+
Parameters
|
|
120
|
+
----------
|
|
121
|
+
x : np.ndarray
|
|
122
|
+
Preprocessed input features of shape (n_samples, n_features).
|
|
123
|
+
y : np.ndarray
|
|
124
|
+
Preprocessed target values of shape (n_samples, n_targets).
|
|
125
|
+
"""
|
|
126
|
+
self.lin.fit(x, y)
|
|
127
|
+
|
|
128
|
+
def predict_tmp(self, x: np.ndarray) -> np.ndarray:
|
|
129
|
+
"""
|
|
130
|
+
Make predictions using the trained linear model.
|
|
131
|
+
|
|
132
|
+
Parameters
|
|
133
|
+
----------
|
|
134
|
+
x : np.ndarray
|
|
135
|
+
Input features for prediction.
|
|
136
|
+
|
|
137
|
+
Returns
|
|
138
|
+
-------
|
|
139
|
+
np.ndarray
|
|
140
|
+
Predicted field values with appropriate scaling applied.
|
|
141
|
+
"""
|
|
142
|
+
if self.with_scaler_x:
|
|
143
|
+
x = self.scalar_X.transform(x)
|
|
144
|
+
x = self.check_input(x)
|
|
145
|
+
if self.with_scaler_y:
|
|
146
|
+
return self.scalar_Y.inverse_transform(self.lin.predict(x))
|
|
147
|
+
else:
|
|
148
|
+
return self.lin.predict(x)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class PODLinear(PODImodelAbstract):
|
|
152
|
+
"""
|
|
153
|
+
Linear regression model for POD coefficient prediction.
|
|
154
|
+
|
|
155
|
+
This model performs POD decomposition to reduce the dimensionality of field data,
|
|
156
|
+
then learns a linear mapping from input parameters to POD coefficients. The final
|
|
157
|
+
field predictions are reconstructed by combining the predicted coefficients with
|
|
158
|
+
the POD modes.
|
|
159
|
+
|
|
160
|
+
Parameters
|
|
161
|
+
----------
|
|
162
|
+
**kwargs
|
|
163
|
+
Additional keyword arguments passed to the parent PODImodelAbstract class.
|
|
164
|
+
Important parameters include:
|
|
165
|
+
- rank : int, Number of POD modes to retain
|
|
166
|
+
- with_scaler_x : bool, Whether to scale input features
|
|
167
|
+
- with_scaler_y : bool, Whether to scale POD coefficients
|
|
168
|
+
- POD_algo : str, POD algorithm ('svd' or 'eigen')
|
|
169
|
+
|
|
170
|
+
Attributes
|
|
171
|
+
----------
|
|
172
|
+
lin : LinearRegression
|
|
173
|
+
The underlying scikit-learn LinearRegression model.
|
|
174
|
+
|
|
175
|
+
Notes
|
|
176
|
+
-----
|
|
177
|
+
This model first applies POD to reduce field data to coefficients, then uses
|
|
178
|
+
linear regression to learn the parameter-coefficient mapping. Field reconstruction
|
|
179
|
+
follows: y_pred = coeffs_pred @ modes, where modes are the POD basis functions.
|
|
180
|
+
|
|
181
|
+
Examples
|
|
182
|
+
--------
|
|
183
|
+
>>> model = PODLinear(rank=20, POD_algo='svd')
|
|
184
|
+
>>> model.fit(parameters, field_snapshots)
|
|
185
|
+
>>> field_predictions = model.predict(new_parameters)
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
def __init__(self, **kwargs):
|
|
189
|
+
"""
|
|
190
|
+
Initialize the PODLinear model.
|
|
191
|
+
|
|
192
|
+
Parameters
|
|
193
|
+
----------
|
|
194
|
+
**kwargs
|
|
195
|
+
Keyword arguments passed to parent PODImodelAbstract class.
|
|
196
|
+
"""
|
|
197
|
+
super().__init__(**kwargs)
|
|
198
|
+
|
|
199
|
+
self.lin: LinearRegression = LinearRegression()
|
|
200
|
+
|
|
201
|
+
def fit_tmp(self, x: np.ndarray, y: np.ndarray) -> None:
|
|
202
|
+
"""
|
|
203
|
+
Fit the linear regression model to POD coefficients.
|
|
204
|
+
|
|
205
|
+
Parameters
|
|
206
|
+
----------
|
|
207
|
+
x : np.ndarray
|
|
208
|
+
Preprocessed input features of shape (n_samples, n_features).
|
|
209
|
+
y : np.ndarray
|
|
210
|
+
Preprocessed POD coefficients of shape (n_samples, rank).
|
|
211
|
+
"""
|
|
212
|
+
self.lin.fit(x, y)
|
|
213
|
+
|
|
214
|
+
def predict_tmp(self, x: np.ndarray) -> np.ndarray:
|
|
215
|
+
"""
|
|
216
|
+
Predict field values by first predicting POD coefficients then reconstructing.
|
|
217
|
+
|
|
218
|
+
Parameters
|
|
219
|
+
----------
|
|
220
|
+
x : np.ndarray
|
|
221
|
+
Input features for prediction.
|
|
222
|
+
|
|
223
|
+
Returns
|
|
224
|
+
-------
|
|
225
|
+
np.ndarray
|
|
226
|
+
Reconstructed field values with appropriate scaling applied.
|
|
227
|
+
"""
|
|
228
|
+
if self.with_scaler_x:
|
|
229
|
+
x = self.scalar_X.transform(x)
|
|
230
|
+
x = self.check_input(x)
|
|
231
|
+
if self.with_scaler_y:
|
|
232
|
+
return self.scalar_Y.inverse_transform(self.lin.predict(x)) @ self.v
|
|
233
|
+
else:
|
|
234
|
+
return self.lin.predict(x) @ self.v
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class fieldsRidge(PODImodelAbstract):
|
|
238
|
+
"""A Ridge regression model for fields."""
|
|
239
|
+
|
|
240
|
+
def __init__(self, **kwargs):
|
|
241
|
+
"""
|
|
242
|
+
Initialize the fieldsRidge model.
|
|
243
|
+
"""
|
|
244
|
+
super().__init__(**kwargs)
|
|
245
|
+
|
|
246
|
+
self.lin: Ridge = Ridge()
|
|
247
|
+
|
|
248
|
+
def fit_tmp(self, x: np.ndarray, y: np.ndarray) -> None:
|
|
249
|
+
self.lin.fit(x, y)
|
|
250
|
+
|
|
251
|
+
def predict_tmp(self, x: np.ndarray) -> np.ndarray:
|
|
252
|
+
if self.with_scaler_x:
|
|
253
|
+
x = self.scalar_X.transform(x)
|
|
254
|
+
x = self.check_input(x)
|
|
255
|
+
if self.with_scaler_y:
|
|
256
|
+
return self.scalar_Y.inverse_transform(self.lin.predict(x))
|
|
257
|
+
else:
|
|
258
|
+
return self.lin.predict(x)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class PODRidge(PODImodelAbstract):
|
|
262
|
+
"""A Ridge regression model for POD coefficients."""
|
|
263
|
+
|
|
264
|
+
def __init__(self, **kwargs):
|
|
265
|
+
"""
|
|
266
|
+
Initialize the PODRidge model.
|
|
267
|
+
"""
|
|
268
|
+
super().__init__(**kwargs)
|
|
269
|
+
|
|
270
|
+
self.lin: Ridge = Ridge()
|
|
271
|
+
|
|
272
|
+
def fit_tmp(self, x: np.ndarray, y: np.ndarray) -> None:
|
|
273
|
+
self.lin.fit(x, y)
|
|
274
|
+
|
|
275
|
+
def predict_tmp(self, x: np.ndarray) -> np.ndarray:
|
|
276
|
+
if self.with_scaler_x:
|
|
277
|
+
x = self.scalar_X.transform(x)
|
|
278
|
+
x = self.check_input(x)
|
|
279
|
+
if self.with_scaler_y:
|
|
280
|
+
return self.scalar_Y.inverse_transform(self.lin.predict(x)) @ self.v
|
|
281
|
+
else:
|
|
282
|
+
return self.lin.predict(x) @ self.v
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class fieldsGPR(PODImodelAbstract):
|
|
286
|
+
"""
|
|
287
|
+
Gaussian Process Regression model for direct field prediction.
|
|
288
|
+
|
|
289
|
+
This model applies Gaussian Process Regression directly to field data without
|
|
290
|
+
dimensionality reduction. It's particularly effective for problems with smooth
|
|
291
|
+
parameter-field relationships and when uncertainty quantification is important.
|
|
292
|
+
|
|
293
|
+
Parameters
|
|
294
|
+
----------
|
|
295
|
+
kernel : sklearn.gaussian_process.kernels.Kernel, optional
|
|
296
|
+
The kernel specifying the covariance function for the GP. If None,
|
|
297
|
+
uses RBF kernel with fixed length scale. Default is None.
|
|
298
|
+
alpha : float, optional
|
|
299
|
+
Value added to the diagonal of the kernel matrix during fitting for
|
|
300
|
+
numerical stability. Represents the expected amount of noise in the
|
|
301
|
+
observations. Default is 1e-10.
|
|
302
|
+
**kwargs
|
|
303
|
+
Additional keyword arguments passed to the parent PODImodelAbstract class.
|
|
304
|
+
|
|
305
|
+
Attributes
|
|
306
|
+
----------
|
|
307
|
+
kernel : sklearn.gaussian_process.kernels.Kernel
|
|
308
|
+
The covariance kernel for the Gaussian Process.
|
|
309
|
+
alpha : float
|
|
310
|
+
The noise regularization parameter.
|
|
311
|
+
gpr : GaussianProcessRegressor
|
|
312
|
+
The underlying scikit-learn Gaussian Process Regressor.
|
|
313
|
+
|
|
314
|
+
Notes
|
|
315
|
+
-----
|
|
316
|
+
GPR provides probabilistic predictions and can quantify uncertainty in
|
|
317
|
+
predictions. The computational complexity scales as O(n³) where n is the
|
|
318
|
+
number of training samples, making it more suitable for smaller datasets.
|
|
319
|
+
|
|
320
|
+
Examples
|
|
321
|
+
--------
|
|
322
|
+
>>> from sklearn.gaussian_process.kernels import RBF, Matern
|
|
323
|
+
>>> kernel = RBF(length_scale=1.0) + Matern(length_scale=2.0)
|
|
324
|
+
>>> model = fieldsGPR(kernel=kernel, alpha=1e-6)
|
|
325
|
+
>>> model.fit(parameters, field_data)
|
|
326
|
+
>>> predictions, std = model.gpr.predict(new_parameters, return_std=True)
|
|
327
|
+
"""
|
|
328
|
+
|
|
329
|
+
def __init__(
|
|
330
|
+
self, kernel: Optional[Kernel] = None, alpha: float = 1.0e-10, **kwargs
|
|
331
|
+
):
|
|
332
|
+
"""
|
|
333
|
+
Initialize the fieldsGPR model.
|
|
334
|
+
|
|
335
|
+
Parameters
|
|
336
|
+
----------
|
|
337
|
+
kernel : sklearn.gaussian_process.kernels.Kernel, optional
|
|
338
|
+
The covariance kernel for the Gaussian Process. If None, uses
|
|
339
|
+
RBF kernel with fixed length scale.
|
|
340
|
+
alpha : float, optional
|
|
341
|
+
Noise regularization parameter. Default is 1e-10.
|
|
342
|
+
**kwargs
|
|
343
|
+
Additional arguments passed to parent class.
|
|
344
|
+
"""
|
|
345
|
+
super().__init__(**kwargs)
|
|
346
|
+
|
|
347
|
+
if kernel is None:
|
|
348
|
+
self.kernel: Kernel = RBF(length_scale=1.0e0, length_scale_bounds="fixed")
|
|
349
|
+
else:
|
|
350
|
+
self.kernel: Kernel = kernel
|
|
351
|
+
|
|
352
|
+
self.alpha: float = alpha
|
|
353
|
+
|
|
354
|
+
def fit_tmp(self, x: np.ndarray, y: np.ndarray) -> None:
|
|
355
|
+
"""
|
|
356
|
+
Fit the Gaussian Process Regression model to the training data.
|
|
357
|
+
|
|
358
|
+
Parameters
|
|
359
|
+
----------
|
|
360
|
+
x : np.ndarray
|
|
361
|
+
Preprocessed input features of shape (n_samples, n_features).
|
|
362
|
+
y : np.ndarray
|
|
363
|
+
Preprocessed target values of shape (n_samples, n_targets).
|
|
364
|
+
"""
|
|
365
|
+
self.gpr: GaussianProcessRegressor = GaussianProcessRegressor(
|
|
366
|
+
kernel=self.kernel, alpha=self.alpha
|
|
367
|
+
)
|
|
368
|
+
self.gpr.fit(x, y)
|
|
369
|
+
|
|
370
|
+
def predict_tmp(self, x: np.ndarray) -> np.ndarray:
|
|
371
|
+
"""
|
|
372
|
+
Make predictions using the trained Gaussian Process model.
|
|
373
|
+
|
|
374
|
+
Parameters
|
|
375
|
+
----------
|
|
376
|
+
x : np.ndarray
|
|
377
|
+
Input features for prediction.
|
|
378
|
+
|
|
379
|
+
Returns
|
|
380
|
+
-------
|
|
381
|
+
np.ndarray
|
|
382
|
+
Predicted field values with appropriate scaling applied.
|
|
383
|
+
|
|
384
|
+
Notes
|
|
385
|
+
-----
|
|
386
|
+
For uncertainty quantification, use self.gpr.predict(x, return_std=True)
|
|
387
|
+
directly after fitting the model.
|
|
388
|
+
"""
|
|
389
|
+
if self.with_scaler_x:
|
|
390
|
+
x = self.scalar_X.transform(x)
|
|
391
|
+
x = self.check_input(x)
|
|
392
|
+
if self.with_scaler_y:
|
|
393
|
+
return self.scalar_Y.inverse_transform(self.gpr.predict(x))
|
|
394
|
+
else:
|
|
395
|
+
return self.gpr.predict(x)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
class PODGPR(PODImodelAbstract):
|
|
399
|
+
"""
|
|
400
|
+
Gaussian Process Regression model for POD coefficient prediction.
|
|
401
|
+
|
|
402
|
+
This model combines POD dimensionality reduction with Gaussian Process Regression.
|
|
403
|
+
It first applies POD to reduce field data to coefficients, then uses GPR to learn
|
|
404
|
+
the parameter-coefficient mapping. This approach is more computationally efficient
|
|
405
|
+
than direct field GPR for high-dimensional problems while preserving the
|
|
406
|
+
probabilistic nature of GP predictions.
|
|
407
|
+
|
|
408
|
+
Parameters
|
|
409
|
+
----------
|
|
410
|
+
kernel : sklearn.gaussian_process.kernels.Kernel, optional
|
|
411
|
+
The covariance kernel for the Gaussian Process. If None, uses RBF kernel
|
|
412
|
+
with length scale bounds from 1e-3 to 1e3. Default is None.
|
|
413
|
+
alpha : float, optional
|
|
414
|
+
Noise regularization parameter for the GP. Default is 1e-10.
|
|
415
|
+
**kwargs
|
|
416
|
+
Additional keyword arguments passed to the parent PODImodelAbstract class.
|
|
417
|
+
Important parameters include:
|
|
418
|
+
- rank : int, Number of POD modes to retain
|
|
419
|
+
- POD_algo : str, POD algorithm ('svd' or 'eigen')
|
|
420
|
+
|
|
421
|
+
Attributes
|
|
422
|
+
----------
|
|
423
|
+
kernel : sklearn.gaussian_process.kernels.Kernel
|
|
424
|
+
The covariance kernel for the Gaussian Process.
|
|
425
|
+
alpha : float
|
|
426
|
+
The noise regularization parameter.
|
|
427
|
+
gpr : GaussianProcessRegressor
|
|
428
|
+
The underlying scikit-learn Gaussian Process Regressor.
|
|
429
|
+
|
|
430
|
+
Notes
|
|
431
|
+
-----
|
|
432
|
+
This model offers several advantages:
|
|
433
|
+
- Computational efficiency through dimensionality reduction
|
|
434
|
+
- Uncertainty quantification for predictions
|
|
435
|
+
- Automatic hyperparameter optimization via marginal likelihood
|
|
436
|
+
- Suitable for nonlinear parameter-field relationships
|
|
437
|
+
|
|
438
|
+
The computational complexity scales as O(n³r) where n is the number of training
|
|
439
|
+
samples and r is the POD rank, making it more efficient than direct field GPR.
|
|
440
|
+
|
|
441
|
+
Examples
|
|
442
|
+
--------
|
|
443
|
+
>>> from sklearn.gaussian_process.kernels import RBF, WhiteKernel
|
|
444
|
+
>>> kernel = RBF(length_scale=1.0) + WhiteKernel(noise_level=1e-5)
|
|
445
|
+
>>> model = PODGPR(rank=15, kernel=kernel, alpha=1e-8)
|
|
446
|
+
>>> model.fit(parameters, field_snapshots)
|
|
447
|
+
>>> predictions = model.predict(new_parameters)
|
|
448
|
+
>>> # Get uncertainty estimates
|
|
449
|
+
>>> coeffs_pred, coeffs_std = model.gpr.predict(new_parameters, return_std=True)
|
|
450
|
+
"""
|
|
451
|
+
|
|
452
|
+
def __init__(
|
|
453
|
+
self, kernel: Optional[Kernel] = None, alpha: float = 1.0e-10, **kwargs
|
|
454
|
+
):
|
|
455
|
+
"""
|
|
456
|
+
Initialize the PODGPR model.
|
|
457
|
+
|
|
458
|
+
Args:
|
|
459
|
+
kernel (Optional[Kernel]): The kernel to use for the GPR.
|
|
460
|
+
alpha (float): The noise level for the GPR.
|
|
461
|
+
"""
|
|
462
|
+
super().__init__(**kwargs)
|
|
463
|
+
|
|
464
|
+
if kernel is None:
|
|
465
|
+
self.kernel: Kernel = RBF(length_scale=1.0e0, length_scale_bounds="fixed")
|
|
466
|
+
else:
|
|
467
|
+
self.kernel: Kernel = kernel
|
|
468
|
+
self.alpha: float = alpha
|
|
469
|
+
|
|
470
|
+
def fit_tmp(self, x: np.ndarray, y: np.ndarray) -> None:
|
|
471
|
+
self.gpr: GaussianProcessRegressor = GaussianProcessRegressor(
|
|
472
|
+
kernel=self.kernel, alpha=self.alpha
|
|
473
|
+
)
|
|
474
|
+
self.gpr.fit(x, y)
|
|
475
|
+
|
|
476
|
+
def predict_tmp(self, x: np.ndarray) -> np.ndarray:
|
|
477
|
+
if self.with_scaler_x:
|
|
478
|
+
x = self.scalar_X.transform(x)
|
|
479
|
+
x = self.check_input(x)
|
|
480
|
+
if self.with_scaler_y:
|
|
481
|
+
return self.scalar_Y.inverse_transform(self.gpr.predict(x)) @ self.v
|
|
482
|
+
else:
|
|
483
|
+
return self.gpr.predict(x) @ self.v
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
class fieldsRidgeGPR(PODImodelAbstract):
|
|
487
|
+
def __init__(
|
|
488
|
+
self, kernel: Optional[Kernel] = None, alpha: float = 1.0e-10, **kwargs
|
|
489
|
+
):
|
|
490
|
+
"""
|
|
491
|
+
Initialize the fieldsRidgeGPR model.
|
|
492
|
+
|
|
493
|
+
Args:
|
|
494
|
+
kernel (Optional[Kernel]): The kernel to use for the GPR.
|
|
495
|
+
alpha (float): The noise level for the GPR.
|
|
496
|
+
"""
|
|
497
|
+
super().__init__(**kwargs)
|
|
498
|
+
|
|
499
|
+
if kernel is None:
|
|
500
|
+
self.kernel: Kernel = RBF(length_scale=1.0e0, length_scale_bounds="fixed")
|
|
501
|
+
else:
|
|
502
|
+
self.kernel: Kernel = kernel
|
|
503
|
+
|
|
504
|
+
self.alpha: float = alpha
|
|
505
|
+
|
|
506
|
+
def fit_tmp(self, x: np.ndarray, y: np.ndarray) -> None:
|
|
507
|
+
self.gpr: GaussianProcessRegressor = GaussianProcessRegressor(
|
|
508
|
+
kernel=self.kernel, alpha=self.alpha
|
|
509
|
+
)
|
|
510
|
+
self.lin: Ridge = Ridge()
|
|
511
|
+
|
|
512
|
+
self.lin.fit(x, y)
|
|
513
|
+
self.gpr.fit(x, y - self.lin.predict(x))
|
|
514
|
+
|
|
515
|
+
def predict_tmp(self, x: np.ndarray) -> np.ndarray:
|
|
516
|
+
if self.with_scaler_x:
|
|
517
|
+
x = self.scalar_X.transform(x)
|
|
518
|
+
x = self.check_input(x)
|
|
519
|
+
if self.with_scaler_y:
|
|
520
|
+
return self.scalar_Y.inverse_transform(
|
|
521
|
+
self.gpr.predict(x) + self.lin.predict(x)
|
|
522
|
+
)
|
|
523
|
+
else:
|
|
524
|
+
return self.gpr.predict(x) + self.lin.predict(x)
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
class PODRidgeGPR(PODImodelAbstract):
|
|
528
|
+
def __init__(
|
|
529
|
+
self, kernel: Optional[Kernel] = None, alpha: float = 1.0e-10, **kwargs
|
|
530
|
+
):
|
|
531
|
+
"""
|
|
532
|
+
Initialize the PODRidgeGPR model.
|
|
533
|
+
|
|
534
|
+
Args:
|
|
535
|
+
kernel (Optional[Kernel]): The kernel to use for the GPR.
|
|
536
|
+
alpha (float): The noise level for the GPR.
|
|
537
|
+
"""
|
|
538
|
+
super().__init__(**kwargs)
|
|
539
|
+
|
|
540
|
+
if kernel is None:
|
|
541
|
+
self.kernel: Kernel = RBF(length_scale=1.0e0, length_scale_bounds="fixed")
|
|
542
|
+
else:
|
|
543
|
+
self.kernel: Kernel = kernel
|
|
544
|
+
self.alpha: float = alpha
|
|
545
|
+
|
|
546
|
+
def fit_tmp(self, x: np.ndarray, y: np.ndarray) -> None:
|
|
547
|
+
self.gpr: GaussianProcessRegressor = GaussianProcessRegressor(
|
|
548
|
+
kernel=self.kernel, alpha=self.alpha
|
|
549
|
+
)
|
|
550
|
+
self.lin: Ridge = Ridge()
|
|
551
|
+
|
|
552
|
+
self.lin.fit(x, y)
|
|
553
|
+
self.gpr.fit(x, y - self.lin.predict(x))
|
|
554
|
+
|
|
555
|
+
def predict_tmp(self, x: np.ndarray) -> np.ndarray:
|
|
556
|
+
if self.with_scaler_x:
|
|
557
|
+
x = self.scalar_X.transform(x)
|
|
558
|
+
x = self.check_input(x)
|
|
559
|
+
if self.with_scaler_y:
|
|
560
|
+
tmp = self.scalar_Y.inverse_transform(
|
|
561
|
+
self.lin.predict(x) + self.gpr.predict(x)
|
|
562
|
+
)
|
|
563
|
+
return tmp @ self.v
|
|
564
|
+
else:
|
|
565
|
+
return (self.lin.predict(x) + self.gpr.predict(x)) @ self.v
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
class fieldsRBF(PODImodelAbstract):
|
|
569
|
+
def __init__(
|
|
570
|
+
self,
|
|
571
|
+
kernel: str = "linear",
|
|
572
|
+
epsilon: float = 1.0,
|
|
573
|
+
neighbors: Optional[int] = None,
|
|
574
|
+
**kwargs,
|
|
575
|
+
):
|
|
576
|
+
"""
|
|
577
|
+
Initialize the fieldsRBF model.
|
|
578
|
+
Args:
|
|
579
|
+
kernel (str): The kernel to use for the RBF interpolator.
|
|
580
|
+
epsilon (float): The epsilon parameter for the RBF interpolator.
|
|
581
|
+
neighbors (int): The number of neighbors for the RBF interpolator.
|
|
582
|
+
"""
|
|
583
|
+
super().__init__(**kwargs)
|
|
584
|
+
|
|
585
|
+
self.kernel: str = kernel
|
|
586
|
+
self.epsilon: float = epsilon
|
|
587
|
+
self.neighbors: Optional[int] = neighbors
|
|
588
|
+
|
|
589
|
+
def fit_tmp(self, x: np.ndarray, y: np.ndarray) -> None:
|
|
590
|
+
self.rbf: RBFInterpolator = RBFInterpolator(
|
|
591
|
+
x, y, kernel=self.kernel, epsilon=self.epsilon, neighbors=self.neighbors
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
def predict_tmp(self, x: np.ndarray) -> np.ndarray:
|
|
595
|
+
if self.with_scaler_x:
|
|
596
|
+
x = self.scalar_X.transform(x)
|
|
597
|
+
x = self.check_input(x)
|
|
598
|
+
if self.with_scaler_y:
|
|
599
|
+
return self.scalar_Y.inverse_transform(self.rbf(x))
|
|
600
|
+
else:
|
|
601
|
+
return self.rbf(x)
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
class PODRBF(PODImodelAbstract):
|
|
605
|
+
def __init__(
|
|
606
|
+
self,
|
|
607
|
+
kernel: str = "linear",
|
|
608
|
+
epsilon: float = 1.0,
|
|
609
|
+
neighbors: Optional[int] = None,
|
|
610
|
+
**kwargs,
|
|
611
|
+
):
|
|
612
|
+
"""
|
|
613
|
+
Initialize the PODRBF model.
|
|
614
|
+
|
|
615
|
+
Args:
|
|
616
|
+
kernel (str): The kernel to use for the RBF interpolator.
|
|
617
|
+
epsilon (float): The epsilon parameter for the RBF interpolator.
|
|
618
|
+
neighbors (int): The number of neighbors for the RBF interpolator.
|
|
619
|
+
"""
|
|
620
|
+
super().__init__(**kwargs)
|
|
621
|
+
|
|
622
|
+
self.kernel: str = kernel
|
|
623
|
+
self.epsilon: float = epsilon
|
|
624
|
+
self.neighbors: Optional[int] = neighbors
|
|
625
|
+
|
|
626
|
+
def fit_tmp(self, x: np.ndarray, y: np.ndarray) -> None:
|
|
627
|
+
self.rbf: RBFInterpolator = RBFInterpolator(
|
|
628
|
+
x, y, kernel=self.kernel, epsilon=self.epsilon, neighbors=self.neighbors
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
def predict_tmp(self, x: np.ndarray) -> np.ndarray:
|
|
632
|
+
if self.with_scaler_x:
|
|
633
|
+
x = self.scalar_X.transform(x)
|
|
634
|
+
x = self.check_input(x)
|
|
635
|
+
if self.with_scaler_y:
|
|
636
|
+
tmp = self.scalar_Y.inverse_transform(self.rbf(x))
|
|
637
|
+
return tmp @ self.v
|
|
638
|
+
else:
|
|
639
|
+
return self.rbf(x) @ self.v
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
class fieldsRidgeRBF(PODImodelAbstract):
|
|
643
|
+
def __init__(
|
|
644
|
+
self,
|
|
645
|
+
kernel: str = "linear",
|
|
646
|
+
epsilon: float = 1.0,
|
|
647
|
+
neighbors: Optional[int] = None,
|
|
648
|
+
**kwargs,
|
|
649
|
+
):
|
|
650
|
+
"""
|
|
651
|
+
Initialize the fieldsRidgeRBF model.
|
|
652
|
+
|
|
653
|
+
Args:
|
|
654
|
+
kernel (str): The kernel to use for the RBF interpolator.
|
|
655
|
+
epsilon (float): The epsilon parameter for the RBF interpolator.
|
|
656
|
+
neighbors (int): The number of neighbors for the RBF interpolator.
|
|
657
|
+
"""
|
|
658
|
+
super().__init__(**kwargs)
|
|
659
|
+
|
|
660
|
+
self.kernel: str = kernel
|
|
661
|
+
self.epsilon: float = epsilon
|
|
662
|
+
self.lin: Ridge = Ridge()
|
|
663
|
+
self.neighbors: Optional[int] = neighbors
|
|
664
|
+
|
|
665
|
+
def fit_tmp(self, x: np.ndarray, y: np.ndarray) -> None:
|
|
666
|
+
self.lin.fit(x, y)
|
|
667
|
+
self.rbf: RBFInterpolator = RBFInterpolator(
|
|
668
|
+
x,
|
|
669
|
+
y - self.lin.predict(x),
|
|
670
|
+
kernel=self.kernel,
|
|
671
|
+
epsilon=self.epsilon,
|
|
672
|
+
neighbors=self.neighbors,
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
def predict_tmp(self, x: np.ndarray) -> np.ndarray:
|
|
676
|
+
if self.with_scaler_x:
|
|
677
|
+
x = self.scalar_X.transform(x)
|
|
678
|
+
x = self.check_input(x)
|
|
679
|
+
if self.with_scaler_y:
|
|
680
|
+
tmp = self.scalar_Y.inverse_transform(self.rbf(x) + self.lin.predict(x))
|
|
681
|
+
return tmp
|
|
682
|
+
else:
|
|
683
|
+
return self.rbf(x) + self.lin.predict(x)
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
class PODRidgeRBF(PODImodelAbstract):
|
|
687
|
+
def __init__(
|
|
688
|
+
self,
|
|
689
|
+
kernel: str = "linear",
|
|
690
|
+
epsilon: float = 1.0,
|
|
691
|
+
neighbors: Optional[int] = None,
|
|
692
|
+
**kwargs,
|
|
693
|
+
):
|
|
694
|
+
"""
|
|
695
|
+
Initialize the PODRidgeRBF model.
|
|
696
|
+
|
|
697
|
+
Args:
|
|
698
|
+
kernel (str): The kernel to use for the RBF interpolator.
|
|
699
|
+
epsilon (float): The epsilon parameter for the RBF interpolator.
|
|
700
|
+
neighbors (int): The number of neighbors for the RBF interpolator.
|
|
701
|
+
"""
|
|
702
|
+
super().__init__(**kwargs)
|
|
703
|
+
|
|
704
|
+
self.kernel: str = kernel
|
|
705
|
+
self.epsilon: float = epsilon
|
|
706
|
+
self.lin: Ridge = Ridge()
|
|
707
|
+
self.neighbors: Optional[int] = neighbors
|
|
708
|
+
|
|
709
|
+
def fit_tmp(self, x: np.ndarray, y: np.ndarray) -> None:
|
|
710
|
+
self.lin.fit(x, y)
|
|
711
|
+
self.rbf: RBFInterpolator = RBFInterpolator(
|
|
712
|
+
x,
|
|
713
|
+
y - self.lin.predict(x),
|
|
714
|
+
kernel=self.kernel,
|
|
715
|
+
epsilon=self.epsilon,
|
|
716
|
+
neighbors=self.neighbors,
|
|
717
|
+
)
|
|
718
|
+
|
|
719
|
+
def predict_tmp(self, x: np.ndarray) -> np.ndarray:
|
|
720
|
+
if self.with_scaler_x:
|
|
721
|
+
x = self.scalar_X.transform(x)
|
|
722
|
+
x = self.check_input(x)
|
|
723
|
+
if self.with_scaler_y:
|
|
724
|
+
tmp = self.scalar_Y.inverse_transform(self.lin.predict(x) + self.rbf(x))
|
|
725
|
+
return tmp @ self.v
|
|
726
|
+
else:
|
|
727
|
+
return (self.lin.predict(x) + self.rbf(x)) @ self.v
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
class PODANN(PODImodelAbstract):
|
|
731
|
+
"""
|
|
732
|
+
A PyTorch-based Artificial Neural Network for interpolating POD coefficients.
|
|
733
|
+
|
|
734
|
+
This class builds, trains, and uses a fully connected neural network
|
|
735
|
+
to map input parameters (e.g., CFD boundary conditions) to POD coefficients.
|
|
736
|
+
It handles data normalization, various activation functions, loss functions,
|
|
737
|
+
and optimizers.
|
|
738
|
+
"""
|
|
739
|
+
|
|
740
|
+
def __init__(
|
|
741
|
+
self,
|
|
742
|
+
hidden_layer_sizes: Optional[List[int]] = None,
|
|
743
|
+
activation_function_name: str = "relu",
|
|
744
|
+
activation_function: Optional[nn.Module] = None,
|
|
745
|
+
learning_rate: float = 0.001,
|
|
746
|
+
loss_function_name: str = "mse",
|
|
747
|
+
optimizer_name: str = "adam",
|
|
748
|
+
num_epochs: int = 1000,
|
|
749
|
+
stop_threshold: float = 1e-4,
|
|
750
|
+
random_seed: int = 42,
|
|
751
|
+
with_weight: bool = True,
|
|
752
|
+
**kwargs,
|
|
753
|
+
):
|
|
754
|
+
"""
|
|
755
|
+
Initializes the PODANN.
|
|
756
|
+
|
|
757
|
+
Args:
|
|
758
|
+
hidden_layer_sizes (list): A list where each element is the number
|
|
759
|
+
of neurons in a corresponding hidden layer.
|
|
760
|
+
Example: [32, 16] for two hidden layers
|
|
761
|
+
with 32 and 16 neurons respectively.
|
|
762
|
+
Defaults to [32, 16] if None.
|
|
763
|
+
activation_function_name (str): Name of the activation function to use
|
|
764
|
+
in hidden layers ('relu', 'sigmoid', 'tanh').
|
|
765
|
+
Defaults to 'relu'.
|
|
766
|
+
learning_rate (float): The learning rate for the optimizer. Defaults to 0.001.
|
|
767
|
+
loss_function_name (str): Name of the loss function ('mse' for Mean Squared Error,
|
|
768
|
+
'l1' for Mean Absolute Error). Defaults to 'mse'.
|
|
769
|
+
optimizer_name (str): Name of the optimizer ('adam', 'sgd'). Defaults to 'adam'.
|
|
770
|
+
num_epochs (int): The number of training epochs. Defaults to 1000.
|
|
771
|
+
stop_threshold (float): Threshold for early stopping based on loss value.
|
|
772
|
+
Defaults to 1e-6.
|
|
773
|
+
random_seed (int): Random seed for reproducibility. Defaults to 42.
|
|
774
|
+
with_weight (bool): Whether to use weights in the loss function.
|
|
775
|
+
Defaults to True.
|
|
776
|
+
"""
|
|
777
|
+
super().__init__(**kwargs)
|
|
778
|
+
|
|
779
|
+
self.hidden_layer_sizes: Optional[List[int]] = (
|
|
780
|
+
hidden_layer_sizes if hidden_layer_sizes is not None else [32, 16]
|
|
781
|
+
)
|
|
782
|
+
self.activation_function_name: str = activation_function_name.lower()
|
|
783
|
+
self.activation_function: Optional[nn.Module] = activation_function
|
|
784
|
+
self.learning_rate: float = learning_rate
|
|
785
|
+
self.loss_function_name: str = loss_function_name.lower()
|
|
786
|
+
self.optimizer_name: str = optimizer_name.lower()
|
|
787
|
+
self.num_epochs: int = num_epochs
|
|
788
|
+
self.stop_threshold: float = stop_threshold
|
|
789
|
+
self.random_seed: int = random_seed
|
|
790
|
+
self.with_weight: bool = with_weight
|
|
791
|
+
|
|
792
|
+
# Determine the device to use (GPU if available, otherwise CPU)
|
|
793
|
+
self.device: torch.device = torch.device(
|
|
794
|
+
"cuda" if torch.cuda.is_available() else "cpu"
|
|
795
|
+
)
|
|
796
|
+
print(f"Using device: {self.device}")
|
|
797
|
+
if self.device.type == "cuda":
|
|
798
|
+
print(f"GPU Info: {torch.cuda.get_device_name(0)}")
|
|
799
|
+
else:
|
|
800
|
+
print(f"CPU Info: {torch.get_num_threads()} threads")
|
|
801
|
+
|
|
802
|
+
# Initialize model and scalers to None, they will be set during fit
|
|
803
|
+
self.model: Optional[nn.Sequential] = None
|
|
804
|
+
|
|
805
|
+
# Set random seed for reproducibility
|
|
806
|
+
self._set_all_seeds()
|
|
807
|
+
|
|
808
|
+
def _get_activation_function(self) -> nn.Module:
|
|
809
|
+
if (
|
|
810
|
+
self.activation_function is not None
|
|
811
|
+
and self.activation_function_name is not None
|
|
812
|
+
):
|
|
813
|
+
raise ValueError(
|
|
814
|
+
"Both activation_function and activation_function_name are set. "
|
|
815
|
+
"Please use only one."
|
|
816
|
+
)
|
|
817
|
+
elif self.activation_function is not None:
|
|
818
|
+
return self.activation_function
|
|
819
|
+
elif self.activation_function_name is not None:
|
|
820
|
+
if self.activation_function_name == "relu":
|
|
821
|
+
return nn.ReLU()
|
|
822
|
+
elif self.activation_function_name == "sigmoid":
|
|
823
|
+
return nn.Sigmoid()
|
|
824
|
+
elif self.activation_function_name == "tanh":
|
|
825
|
+
return nn.Tanh()
|
|
826
|
+
elif self.activation_function_name == "leaky_relu":
|
|
827
|
+
return nn.LeakyReLU()
|
|
828
|
+
elif self.activation_function_name == "elu":
|
|
829
|
+
return nn.ELU()
|
|
830
|
+
elif self.activation_function_name == "softmax":
|
|
831
|
+
return nn.Softmax(dim=1)
|
|
832
|
+
elif self.activation_function_name == "softplus":
|
|
833
|
+
return nn.Softplus()
|
|
834
|
+
else:
|
|
835
|
+
raise ValueError(
|
|
836
|
+
f"Unsupported activation function: {self.activation_function_name}"
|
|
837
|
+
)
|
|
838
|
+
|
|
839
|
+
def _get_loss_function(self) -> nn.Module:
|
|
840
|
+
"""Returns the PyTorch loss function module based on its name."""
|
|
841
|
+
if self.loss_function_name == "mse":
|
|
842
|
+
if self.with_weight:
|
|
843
|
+
return nn.MSELoss(reduction="none")
|
|
844
|
+
else:
|
|
845
|
+
return nn.MSELoss()
|
|
846
|
+
elif self.loss_function_name == "l1":
|
|
847
|
+
return nn.L1Loss()
|
|
848
|
+
else:
|
|
849
|
+
raise ValueError(f"Unsupported loss function: {self.loss_function_name}")
|
|
850
|
+
|
|
851
|
+
def _get_optimizer(self, model_parameters) -> optim.Optimizer:
|
|
852
|
+
"""Returns the PyTorch optimizer based on its name and model parameters."""
|
|
853
|
+
if self.optimizer_name == "adam":
|
|
854
|
+
return optim.Adam(model_parameters, lr=self.learning_rate)
|
|
855
|
+
elif self.optimizer_name == "sgd":
|
|
856
|
+
return optim.SGD(model_parameters, lr=self.learning_rate)
|
|
857
|
+
else:
|
|
858
|
+
raise ValueError(f"Unsupported optimizer: {self.optimizer_name}")
|
|
859
|
+
|
|
860
|
+
def _build_model(self, input_dim: int, output_dim: int) -> None:
|
|
861
|
+
"""
|
|
862
|
+
Builds the neural network model dynamically based on specified hidden layers.
|
|
863
|
+
|
|
864
|
+
Args:
|
|
865
|
+
input_dim (int): The number of input features.
|
|
866
|
+
output_dim (int): The number of output features (POD coefficients).
|
|
867
|
+
"""
|
|
868
|
+
layers = []
|
|
869
|
+
current_dim = input_dim
|
|
870
|
+
activation = self._get_activation_function()
|
|
871
|
+
|
|
872
|
+
for h_size in self.hidden_layer_sizes:
|
|
873
|
+
layers.append(nn.Linear(current_dim, h_size))
|
|
874
|
+
layers.append(activation)
|
|
875
|
+
current_dim = h_size
|
|
876
|
+
|
|
877
|
+
# Output layer with linear activation (regression problem)
|
|
878
|
+
layers.append(nn.Linear(current_dim, output_dim))
|
|
879
|
+
|
|
880
|
+
self.model = nn.Sequential(*layers).to(self.device)
|
|
881
|
+
print("\n--- Model Architecture ---")
|
|
882
|
+
print(self.model)
|
|
883
|
+
print("--------------------------\n")
|
|
884
|
+
|
|
885
|
+
def _set_all_seeds(self) -> None:
|
|
886
|
+
"""
|
|
887
|
+
Sets the random seed for reproducibility across different libraries.
|
|
888
|
+
"""
|
|
889
|
+
seed = self.random_seed
|
|
890
|
+
np.random.seed(seed) # NumPy seed
|
|
891
|
+
random.seed(seed) # Python's built-in random module seed
|
|
892
|
+
torch.manual_seed(seed) # PyTorch CPU seed
|
|
893
|
+
if torch.cuda.is_available():
|
|
894
|
+
torch.cuda.manual_seed(seed) # PyTorch GPU seed
|
|
895
|
+
torch.cuda.manual_seed_all(seed) # PyTorch Multi-GPU seed
|
|
896
|
+
# Optional: For deterministic CUDA operations, but can slow down training
|
|
897
|
+
# If you encounter issues, you might need to comment these out.
|
|
898
|
+
torch.backends.cudnn.deterministic = True
|
|
899
|
+
torch.backends.cudnn.benchmark = False
|
|
900
|
+
print(f"CUDA deterministic set to {torch.backends.cudnn.deterministic}")
|
|
901
|
+
|
|
902
|
+
def fit_tmp(self, x: np.ndarray, y: np.ndarray) -> None:
|
|
903
|
+
"""
|
|
904
|
+
Trains the neural network model.
|
|
905
|
+
|
|
906
|
+
Args:
|
|
907
|
+
X_train (np.ndarray): Training input data (parameters).
|
|
908
|
+
Shape: (num_samples, input_dim).
|
|
909
|
+
y_train (np.ndarray): Training output data (high fidelity data).
|
|
910
|
+
Shape: (num_samples, output_dim).
|
|
911
|
+
"""
|
|
912
|
+
input_dim = x.shape[1]
|
|
913
|
+
output_dim = y.shape[1]
|
|
914
|
+
|
|
915
|
+
# Convert numpy arrays to PyTorch tensors
|
|
916
|
+
X_train_tensor = torch.tensor(x, dtype=torch.float32).to(self.device)
|
|
917
|
+
y_train_tensor = torch.tensor(y, dtype=torch.float32).to(self.device)
|
|
918
|
+
|
|
919
|
+
# 2. Build the model
|
|
920
|
+
self._build_model(input_dim, output_dim)
|
|
921
|
+
|
|
922
|
+
# 3. Define loss function and optimizer
|
|
923
|
+
criterion = self._get_loss_function()
|
|
924
|
+
optimizer = self._get_optimizer(self.model.parameters())
|
|
925
|
+
|
|
926
|
+
# Compute loss
|
|
927
|
+
if self.with_weight:
|
|
928
|
+
# set coefficient_weights for weighted loss as the singular values from POD
|
|
929
|
+
self.coefficient_weights: np.ndarray = self.s
|
|
930
|
+
if len(self.coefficient_weights) != output_dim:
|
|
931
|
+
raise ValueError(
|
|
932
|
+
f"Length of coefficient_weights ({len(self.coefficient_weights)}) must match output_dim ({output_dim})."
|
|
933
|
+
)
|
|
934
|
+
weights_tensor: torch.Tensor = torch.tensor(
|
|
935
|
+
self.coefficient_weights, dtype=torch.float32
|
|
936
|
+
).to(self.device)
|
|
937
|
+
# Ensure weights are positive
|
|
938
|
+
if (weights_tensor < 0).any():
|
|
939
|
+
raise ValueError("Coefficient weights must be non-negative.")
|
|
940
|
+
|
|
941
|
+
# 4. Training loop
|
|
942
|
+
print("Starting model training...")
|
|
943
|
+
for epoch in range(self.num_epochs):
|
|
944
|
+
# Set model to training mode
|
|
945
|
+
self.model.train()
|
|
946
|
+
|
|
947
|
+
# Forward pass
|
|
948
|
+
outputs = self.model(X_train_tensor)
|
|
949
|
+
|
|
950
|
+
# Ensure weighting is only applied if loss is MSE
|
|
951
|
+
if self.with_weight:
|
|
952
|
+
if self.loss_function_name == "mse":
|
|
953
|
+
# Calculate squared error for each element
|
|
954
|
+
loss_per_element = criterion(
|
|
955
|
+
outputs, y_train_tensor
|
|
956
|
+
) # Use targets from batch (unscaled)
|
|
957
|
+
# Apply weights
|
|
958
|
+
weighted_loss_per_element = loss_per_element * weights_tensor
|
|
959
|
+
# Take the mean of the weighted squared error over all elements
|
|
960
|
+
loss = weighted_loss_per_element.mean()
|
|
961
|
+
else:
|
|
962
|
+
# Fallback for other loss functions if implemented without specific weighting logic
|
|
963
|
+
print(
|
|
964
|
+
"Warning: Coefficient weights are set but loss function is not MSE. Using unweighted loss."
|
|
965
|
+
)
|
|
966
|
+
loss = criterion(outputs, y_train_tensor)
|
|
967
|
+
else:
|
|
968
|
+
# Calculate loss without weights
|
|
969
|
+
loss = criterion(outputs, y_train_tensor)
|
|
970
|
+
|
|
971
|
+
# Backward and optimize
|
|
972
|
+
optimizer.zero_grad() # Clear gradients
|
|
973
|
+
loss.backward() # Compute gradients
|
|
974
|
+
optimizer.step() # Update weights
|
|
975
|
+
|
|
976
|
+
if (epoch + 1) % 1000 == 0 or epoch == 0:
|
|
977
|
+
# Relative L2 norm loss (optional)
|
|
978
|
+
relative_loss = torch.sqrt(loss) / torch.norm(y_train_tensor)
|
|
979
|
+
print(
|
|
980
|
+
f"Epoch [{epoch+1}/{self.num_epochs}], Loss: {loss.item():.6f}, Relative L2 norm loss: {relative_loss.item():.6f}"
|
|
981
|
+
)
|
|
982
|
+
|
|
983
|
+
# Early stopping condition (optional)
|
|
984
|
+
if (
|
|
985
|
+
relative_loss < self.stop_threshold
|
|
986
|
+
): # Arbitrary threshold for early stopping
|
|
987
|
+
print("Early stopping triggered.")
|
|
988
|
+
break
|
|
989
|
+
|
|
990
|
+
print("Model training finished.")
|
|
991
|
+
|
|
992
|
+
def predict_tmp(self, x: np.ndarray) -> np.ndarray:
|
|
993
|
+
"""
|
|
994
|
+
Predicts POD coefficients for new input data using the trained model.
|
|
995
|
+
|
|
996
|
+
Args:
|
|
997
|
+
x (np.ndarray): Test input data (parameters).
|
|
998
|
+
Shape: (num_samples, input_dim).
|
|
999
|
+
|
|
1000
|
+
Returns:
|
|
1001
|
+
np.ndarray: Predicted POD coefficients.
|
|
1002
|
+
Shape: (num_samples, output_dim).
|
|
1003
|
+
"""
|
|
1004
|
+
|
|
1005
|
+
if self.model is None:
|
|
1006
|
+
raise RuntimeError("Model has not been trained. Call .fit() first.")
|
|
1007
|
+
if x.ndim != 2:
|
|
1008
|
+
raise ValueError("Test input data must be a 2D numpy array.")
|
|
1009
|
+
|
|
1010
|
+
# Set model to evaluation mode (important for layers like Dropout if they were used)
|
|
1011
|
+
self.model.eval()
|
|
1012
|
+
|
|
1013
|
+
# Normalize test input data
|
|
1014
|
+
if self.with_scaler_x:
|
|
1015
|
+
x = self.scalar_X.transform(x)
|
|
1016
|
+
x = self.check_input(x)
|
|
1017
|
+
|
|
1018
|
+
# Convert to PyTorch tensor
|
|
1019
|
+
X_test_tensor: torch.Tensor = torch.tensor(x, dtype=torch.float32).to(
|
|
1020
|
+
self.device
|
|
1021
|
+
)
|
|
1022
|
+
|
|
1023
|
+
with torch.no_grad(): # Disable gradient calculation during inference
|
|
1024
|
+
predictions = self.model(X_test_tensor).cpu().numpy()
|
|
1025
|
+
|
|
1026
|
+
# De-normalize the predictions
|
|
1027
|
+
if self.with_scaler_y:
|
|
1028
|
+
predictions = self.scalar_Y.inverse_transform(predictions)
|
|
1029
|
+
else:
|
|
1030
|
+
predictions = predictions
|
|
1031
|
+
|
|
1032
|
+
# return high fidelity predictions
|
|
1033
|
+
return predictions @ self.v
|