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.
@@ -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