ravimohan19 commited on
Commit
814f98a
·
verified ·
1 Parent(s): 058f8a4

Upload models/gp_model.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. models/gp_model.py +269 -0
models/gp_model.py ADDED
@@ -0,0 +1,269 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """GPyTorch-based Gaussian Process models with physics-informed priors."""
2
+
3
+ from typing import Callable, Optional, Tuple
4
+
5
+ import torch
6
+ from torch import Tensor
7
+ import gpytorch
8
+ from gpytorch.models import ExactGP
9
+ from gpytorch.means import ConstantMean, ZeroMean
10
+ from gpytorch.kernels import ScaleKernel, RBFKernel, MaternKernel
11
+ from gpytorch.likelihoods import GaussianLikelihood
12
+ from gpytorch.distributions import MultivariateNormal
13
+ from gpytorch.mlls import ExactMarginalLogLikelihood
14
+
15
+ from botorch.models.gpytorch import GPyTorchModel
16
+ from botorch.posteriors.gpytorch import GPyTorchPosterior
17
+ from botorch.models.transforms.input import Normalize
18
+ from botorch.models.transforms.outcome import Standardize
19
+
20
+ from physics_informed_bo.models.base import SurrogateModel
21
+ from physics_informed_bo.models.physics_model import PhysicsMeanFunction
22
+
23
+
24
+ class _ExactGPModel(ExactGP, GPyTorchModel):
25
+ """Core GPyTorch ExactGP model with BoTorch compatibility."""
26
+
27
+ _num_outputs = 1
28
+
29
+ def __init__(
30
+ self,
31
+ train_X: Tensor,
32
+ train_y: Tensor,
33
+ likelihood: GaussianLikelihood,
34
+ mean_module: Optional[gpytorch.means.Mean] = None,
35
+ kernel: str = "matern",
36
+ ard_num_dims: Optional[int] = None,
37
+ ):
38
+ super().__init__(train_X, train_y.squeeze(-1), likelihood)
39
+
40
+ self.mean_module = mean_module or ConstantMean()
41
+
42
+ if kernel == "rbf":
43
+ base_kernel = RBFKernel(ard_num_dims=ard_num_dims)
44
+ elif kernel == "matern":
45
+ base_kernel = MaternKernel(nu=2.5, ard_num_dims=ard_num_dims)
46
+ else:
47
+ raise ValueError(f"Unknown kernel: {kernel}. Use 'rbf' or 'matern'.")
48
+
49
+ self.covar_module = ScaleKernel(base_kernel)
50
+
51
+ def forward(self, X: Tensor) -> MultivariateNormal:
52
+ mean = self.mean_module(X)
53
+ covar = self.covar_module(X)
54
+ return MultivariateNormal(mean, covar)
55
+
56
+
57
+ class StandardGP(SurrogateModel):
58
+ """Standard Gaussian Process model (no physics, pure data-driven).
59
+
60
+ Uses GPyTorch for the GP and is BoTorch-compatible for optimization.
61
+ """
62
+
63
+ def __init__(
64
+ self,
65
+ kernel: str = "matern",
66
+ noise_variance: float = 0.01,
67
+ learn_noise: bool = True,
68
+ normalize_inputs: bool = True,
69
+ standardize_outputs: bool = True,
70
+ device: str = "cpu",
71
+ dtype: torch.dtype = torch.float64,
72
+ ):
73
+ self.kernel = kernel
74
+ self.noise_variance = noise_variance
75
+ self.learn_noise = learn_noise
76
+ self.normalize_inputs = normalize_inputs
77
+ self.standardize_outputs = standardize_outputs
78
+ self.device = torch.device(device)
79
+ self.dtype = dtype
80
+ self._model = None
81
+ self._likelihood = None
82
+
83
+ def fit(
84
+ self,
85
+ X: Tensor,
86
+ y: Tensor,
87
+ training_iterations: int = 100,
88
+ lr: float = 0.1,
89
+ ) -> None:
90
+ """Fit the GP model by optimizing the marginal log likelihood."""
91
+ X = X.to(device=self.device, dtype=self.dtype)
92
+ y = y.to(device=self.device, dtype=self.dtype)
93
+
94
+ if y.dim() == 1:
95
+ y = y.unsqueeze(-1)
96
+
97
+ self._likelihood = GaussianLikelihood()
98
+ if not self.learn_noise:
99
+ self._likelihood.noise = self.noise_variance
100
+ self._likelihood.noise_covar.raw_noise.requires_grad_(False)
101
+
102
+ self._model = _ExactGPModel(
103
+ train_X=X,
104
+ train_y=y,
105
+ likelihood=self._likelihood,
106
+ kernel=self.kernel,
107
+ ard_num_dims=X.shape[-1],
108
+ ).to(device=self.device, dtype=self.dtype)
109
+
110
+ self._optimize_hyperparameters(X, y, training_iterations, lr)
111
+
112
+ def _optimize_hyperparameters(
113
+ self, X: Tensor, y: Tensor, n_iter: int, lr: float
114
+ ) -> None:
115
+ """Optimize GP hyperparameters via type-II MLE."""
116
+ self._model.train()
117
+ self._likelihood.train()
118
+
119
+ optimizer = torch.optim.Adam(self._model.parameters(), lr=lr)
120
+ mll = ExactMarginalLogLikelihood(self._likelihood, self._model)
121
+
122
+ for _ in range(n_iter):
123
+ optimizer.zero_grad()
124
+ output = self._model(X)
125
+ loss = -mll(output, y.squeeze(-1))
126
+ loss.backward()
127
+ optimizer.step()
128
+
129
+ self._model.eval()
130
+ self._likelihood.eval()
131
+
132
+ def predict(self, X: Tensor) -> Tuple[Tensor, Tensor]:
133
+ X = X.to(device=self.device, dtype=self.dtype)
134
+ self._model.eval()
135
+ self._likelihood.eval()
136
+
137
+ with torch.no_grad(), gpytorch.settings.fast_pred_var():
138
+ posterior = self._likelihood(self._model(X))
139
+ mean = posterior.mean.unsqueeze(-1)
140
+ variance = posterior.variance.unsqueeze(-1)
141
+
142
+ return mean, variance
143
+
144
+ def posterior(self, X: Tensor):
145
+ self._model.eval()
146
+ self._likelihood.eval()
147
+ return self._model.posterior(X)
148
+
149
+ @property
150
+ def model(self):
151
+ """Access the underlying BoTorch-compatible GP model."""
152
+ return self._model
153
+
154
+
155
+ class PhysicsInformedGP(SurrogateModel):
156
+ """GP with a physics model as the mean function.
157
+
158
+ The GP prior mean is set to the physics model predictions, so the GP
159
+ learns the residual (discrepancy) between the physics model and reality.
160
+ This is the core model of the platform.
161
+
162
+ Architecture:
163
+ f(x) = physics(x) + GP_residual(x)
164
+ where GP_residual ~ GP(0, k(x, x'))
165
+ """
166
+
167
+ def __init__(
168
+ self,
169
+ physics_fn: Callable[[Tensor], Tensor],
170
+ kernel: str = "matern",
171
+ physics_output_scale: float = 1.0,
172
+ learnable_physics_scale: bool = True,
173
+ noise_variance: float = 0.01,
174
+ learn_noise: bool = True,
175
+ device: str = "cpu",
176
+ dtype: torch.dtype = torch.float64,
177
+ ):
178
+ self.physics_fn = physics_fn
179
+ self.kernel = kernel
180
+ self.physics_output_scale = physics_output_scale
181
+ self.learnable_physics_scale = learnable_physics_scale
182
+ self.noise_variance = noise_variance
183
+ self.learn_noise = learn_noise
184
+ self.device = torch.device(device)
185
+ self.dtype = dtype
186
+ self._model = None
187
+ self._likelihood = None
188
+
189
+ def fit(
190
+ self,
191
+ X: Tensor,
192
+ y: Tensor,
193
+ training_iterations: int = 200,
194
+ lr: float = 0.05,
195
+ ) -> None:
196
+ """Fit the physics-informed GP model."""
197
+ X = X.to(device=self.device, dtype=self.dtype)
198
+ y = y.to(device=self.device, dtype=self.dtype)
199
+
200
+ if y.dim() == 1:
201
+ y = y.unsqueeze(-1)
202
+
203
+ self._likelihood = GaussianLikelihood()
204
+ if not self.learn_noise:
205
+ self._likelihood.noise = self.noise_variance
206
+ self._likelihood.noise_covar.raw_noise.requires_grad_(False)
207
+
208
+ physics_mean = PhysicsMeanFunction(
209
+ physics_fn=self.physics_fn,
210
+ output_scale=self.physics_output_scale,
211
+ learnable_scale=self.learnable_physics_scale,
212
+ )
213
+
214
+ self._model = _ExactGPModel(
215
+ train_X=X,
216
+ train_y=y,
217
+ likelihood=self._likelihood,
218
+ mean_module=physics_mean,
219
+ kernel=self.kernel,
220
+ ard_num_dims=X.shape[-1],
221
+ ).to(device=self.device, dtype=self.dtype)
222
+
223
+ self._optimize_hyperparameters(X, y, training_iterations, lr)
224
+
225
+ def _optimize_hyperparameters(
226
+ self, X: Tensor, y: Tensor, n_iter: int, lr: float
227
+ ) -> None:
228
+ self._model.train()
229
+ self._likelihood.train()
230
+
231
+ optimizer = torch.optim.Adam(self._model.parameters(), lr=lr)
232
+ mll = ExactMarginalLogLikelihood(self._likelihood, self._model)
233
+
234
+ for _ in range(n_iter):
235
+ optimizer.zero_grad()
236
+ output = self._model(X)
237
+ loss = -mll(output, y.squeeze(-1))
238
+ loss.backward()
239
+ optimizer.step()
240
+
241
+ self._model.eval()
242
+ self._likelihood.eval()
243
+
244
+ def predict(self, X: Tensor) -> Tuple[Tensor, Tensor]:
245
+ X = X.to(device=self.device, dtype=self.dtype)
246
+ self._model.eval()
247
+ self._likelihood.eval()
248
+
249
+ with torch.no_grad(), gpytorch.settings.fast_pred_var():
250
+ posterior = self._likelihood(self._model(X))
251
+ mean = posterior.mean.unsqueeze(-1)
252
+ variance = posterior.variance.unsqueeze(-1)
253
+
254
+ return mean, variance
255
+
256
+ def posterior(self, X: Tensor):
257
+ self._model.eval()
258
+ self._likelihood.eval()
259
+ return self._model.posterior(X)
260
+
261
+ @property
262
+ def model(self):
263
+ return self._model
264
+
265
+ def get_residuals(self, X: Tensor, y: Tensor) -> Tensor:
266
+ """Compute residuals between physics predictions and observations."""
267
+ with torch.no_grad():
268
+ physics_pred = self.physics_fn(X)
269
+ return y.squeeze() - physics_pred