OneScience commited on
Commit
6c3f19f
·
verified ·
1 Parent(s): 7997e02

Upload folder using huggingface_hub

Browse files
README.md ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: unknown
3
+ language:
4
+ - en
5
+ tags:
6
+ - OneScience
7
+ - fluid dynamics
8
+ - topology optimization
9
+ - Gaussian processes
10
+ - physics-informed learning
11
+ frameworks: PyTorch
12
+ ---
13
+
14
+ <p align="center">
15
+ <strong>
16
+ <span style="font-size: 30px;">GP_for_TO</span>
17
+ </strong>
18
+ </p>
19
+
20
+ # Model Overview
21
+
22
+ GP_for_TO (Physics-informed GP-TO) is a physics-informed Gaussian-process framework for topology optimization developed by researchers at Northwestern University. It jointly optimizes material distributions and physical state variables in complex design domains without requiring an explicit mesh discretization.
23
+
24
+ Paper: Simultaneous and Meshfree Topology Optimization with Physics-informed Gaussian Processes
25
+ https://arxiv.org/abs/2408.03490
26
+
27
+ # Model Description
28
+
29
+ GP_for_TO uses a Gaussian-process architecture whose mean function is parameterized by a deep neural network. It provides a mesh-free approach to fluid topology optimization problems such as minimizing dissipated power in Stokes flow.
30
+
31
+ # Usage
32
+
33
+ ## 1. OneCode
34
+
35
+ Use the online OneCode environment for an intelligent, one-click AI for Science (AI4S) programming experience:
36
+
37
+ [Launch OneCode for one-click AI4S programming](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
38
+
39
+ ## 2. Manual Setup
40
+
41
+ **Hardware Requirements**
42
+
43
+ - A GPU or DCU is recommended.
44
+ - A CPU can be used for import checks and small-scale pipeline validation, but full training and inference will be slow.
45
+ - DCU users must install DTK in advance. DTK 25.04.2 or later, or the OneScience-recommended version for the target cluster, is recommended.
46
+
47
+
48
+ ### Download the Model Package
49
+
50
+ ```bash
51
+ modelscope download --model OneScience/GP_for_TO --local_dir ./GP_for_TO
52
+ cd GP_for_TO
53
+ ```
54
+
55
+ ### Set Up the Runtime Environment
56
+
57
+
58
+ **DCU Environment**
59
+
60
+ ```bash
61
+ # Activate DTK and Conda first
62
+ conda create -n onescience311 python=3.11 -y
63
+ conda activate onescience311
64
+ # Installation with uv is also supported
65
+ pip install onescience[cfd-dcu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
66
+ ```
67
+
68
+ **GPU Environment**
69
+ ```bash
70
+ # Activate Conda first
71
+ conda create -n onescience311 python=3.11 -y libstdcxx-ng=12 libgcc-ng=12 gcc_linux-64=12 gxx_linux-64=12
72
+ conda activate onescience311
73
+ # Installation with uv is also supported
74
+ pip install onescience[cfd-gpu] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
75
+ ```
76
+
77
+
78
+ ### Training Data
79
+ The method does not rely on pregenerated topology structures or labeled physical fields. Instead, it samples spatial points within the design domain and constructs physics-informed training signals from the governing Stokes-flow equations, boundary conditions, dissipated-power objective, and material volume-fraction constraint. The paper evaluates four fluid topology optimization cases—Rugby, Pipe Bend, Diffuser, and Double Pipe—and compares the results against COMSOL solutions obtained with the SIMP method.
80
+
81
+ ### Training
82
+
83
+ The default training configuration retains the original settings: `N_col_domain=10000`, `N_train_per_BC=25`, `num_iter=50000`, and `diff_method=Numerical`.
84
+
85
+ ```bash
86
+ python scripts/train.py
87
+ ```
88
+ ### Model Weights
89
+ This repository will provide GP_for_TO model weights in the `weights/` directory. The weights will be uploaded soon.
90
+
91
+ ## Inference
92
+
93
+ ```bash
94
+ python scripts/inference.py
95
+ ```
96
+
97
+ By default, inference loads `weight/gp_for_to.pt` and produces:
98
+
99
+ ```text
100
+ result/inference/predictions.npz
101
+ result/inference/inference_summary.json
102
+ ```
103
+
104
+ `predictions.npz` contains five arrays: `x`, `u`, `v`, `p`, and `ro`.
105
+
106
+ ## Result Visualization
107
+
108
+ ```bash
109
+ python scripts/result.py
110
+ ```
111
+
112
+
113
+ # Official OneScience Resources
114
+
115
+ | Platform | OneScience Repository | Skills Repository |
116
+ | --- | --- | --- |
117
+ | Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
118
+ | GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
119
+
120
+ # Citations and License
121
+
122
+ - Original GP_for_TO paper: [Simultaneous and Meshfree Topology Optimization with Physics-informed Gaussian Processes](https://arxiv.org/abs/2408.03490).
123
+ - This repository retains the relevant source and attribution notices. Follow all applicable license requirements when using, modifying, or distributing its contents.
conf/config.yaml ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ root:
2
+ problem: "doublepipe"
3
+ seed: 11
4
+ output_names: ["u", "v", "p", "ro"]
5
+
6
+ runtime:
7
+ onescience_src: null
8
+
9
+ data:
10
+ n_train_per_bc: 25
11
+ n_col_domain: 10000
12
+
13
+ model:
14
+ mean_function: "neural_network"
15
+ nn_layers_base: [64, 64, 64, 64, 64]
16
+ dtype: "float32"
17
+
18
+ training:
19
+ device: "auto"
20
+ gpu: 0
21
+ num_iter: 50000
22
+ lr_default: 0.001
23
+ diff_method: "Numerical"
24
+ checkpoint_path: "./weight/gp_for_to.pt"
25
+ output_dir: "./result/training"
26
+ plot_outputs: true
27
+ checkpoints: [1000, 10000, 20000, 30000, 40000, 50000]
28
+
29
+ inference:
30
+ device: "auto"
31
+ gpu: 0
32
+ checkpoint_path: "./weight/gp_for_to.pt"
33
+ output_dir: "./result/inference"
34
+ n_col_domain: 10000
35
+
36
+ fake_data:
37
+ problem: "doublepipe"
38
+ n_train_per_bc: 25
39
+ n_col_domain: 10000
40
+ output_dir: "./data/fake"
configuration.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"framework":"Pytorch","task":"physics-informed-topology-optimization"}
model/__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ from .gpregression import GPR
2
+ from .gpplus_model import GPPLUS
3
+ from .horseshoe import LogHalfHorseshoePrior
4
+ from .mollified_uniform import MollifiedUniformPrior
model/gpplus_model.py ADDED
@@ -0,0 +1,778 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import torch
3
+ import math
4
+ import gpytorch
5
+ from gpytorch.constraints import Positive
6
+ from gpytorch.priors import NormalPrior
7
+ from gpytorch.distributions import MultivariateNormal
8
+ from onescience.utils.GP_TO.plot_latenth import plot_sep
9
+ from .gpregression import GPR
10
+ import gpytorch.kernels as kernels
11
+ from .mollified_uniform import MollifiedUniformPrior
12
+ from pandas import DataFrame
13
+ import matplotlib.pyplot as plt
14
+ from torch.nn.parameter import Parameter
15
+ import torch.nn.functional as F
16
+
17
+ def setlevels(X, qual_index = None, return_label = False):
18
+ labels = []
19
+ if qual_index == []:
20
+ return X
21
+ if qual_index is None:
22
+ qual_index = list(range(X.shape[-1]))
23
+ # if type(X) == np.ndarray:
24
+ # temp = torch.from_numpy(X).detach().clone()
25
+ temp = np.copy(X)
26
+ if type(X) == torch.Tensor:
27
+ temp = X.clone()
28
+ if temp.ndim > 1:
29
+ for j in qual_index:
30
+ l = np.sort(np.unique(temp[..., j])).tolist()
31
+ labels.append(l)
32
+ #l = torch.unique(temp[..., j], sorted = True).tolist()
33
+ temp[..., j] = torch.tensor([*map(lambda m: l.index(m),temp[..., j])])
34
+ else:
35
+ l = torch.unique(temp, sorted = True)
36
+ temp = torch.tensor([*map(lambda m: l.tolist().index(m), temp)])
37
+
38
+
39
+ if temp.dtype == object:
40
+ temp = temp.astype(float)
41
+ if type(X) == np.ndarray:
42
+ temp = torch.from_numpy(temp)
43
+
44
+ if return_label:
45
+ return temp, labels
46
+ else:
47
+ return temp
48
+ else:
49
+ if type(X) == np.ndarray:
50
+ temp = torch.from_numpy(temp)
51
+ if return_label:
52
+ return temp, labels
53
+ else:
54
+ return temp
55
+
56
+ class GPPLUS(GPR):
57
+ """The latent Map GP regression model (LMGP) which extends GPs to handle categorical inputs.
58
+
59
+ :note: Binary categorical variables should not be treated as qualitative inputs. There is no
60
+ benefit from applying a latent variable treatment for such variables. Instead, treat them
61
+ as numerical inputs.
62
+
63
+ :param train_x: The training inputs (size N x d). Qualitative inputs needed to be encoded as
64
+ integers 0,...,L-1 where L is the number of levels. For best performance, scale the
65
+ numerical variables to the unit hypercube.
66
+ """
67
+ def __init__(
68
+ self,
69
+ #transformation_of_A_parameters:str,
70
+ train_x:torch.Tensor,
71
+ train_y:torch.Tensor,
72
+ collocation_x:torch.Tensor,
73
+ qual_ind_lev = {},
74
+ multiple_noise = False,
75
+ lv_dim:int=2,
76
+ quant_correlation_class:str='Rough_RBF',
77
+ noise:float=5e-8,
78
+ fix_noise:bool=True,
79
+ lb_noise:float=1e-8,
80
+ NN_layers:list = [],
81
+ name_output:str='u',
82
+ encoding_type = 'one-hot',
83
+ uniform_encoding_columns = 2,
84
+ lv_columns = [] ,
85
+ basis='neural_network',
86
+ NN_layers_base=[4,4],
87
+ basis_function_size=None,
88
+ device="cpu",
89
+ dtype= torch.float32
90
+ ) -> None:
91
+
92
+ tkwargs = {} # or dict()
93
+ tkwargs['dtype'] = dtype
94
+ tkwargs['device'] =device
95
+
96
+ qual_index = list(qual_ind_lev.keys())
97
+ all_index = set(range(train_x.shape[-1]))
98
+ quant_index = list(all_index.difference(qual_index))
99
+ num_levels_per_var = list(qual_ind_lev.values())
100
+ #------------------- lm columns --------------------------
101
+ lm_columns = list(set(qual_index).difference(lv_columns))
102
+ if len(lm_columns) > 0:
103
+ qual_kernel_columns = [*lv_columns, lm_columns]
104
+ else:
105
+ qual_kernel_columns = lv_columns
106
+
107
+ #########################
108
+ if len(qual_index) > 0:
109
+ train_x = setlevels(train_x, qual_index=qual_index)
110
+ #
111
+ if multiple_noise:
112
+ noise_indices = list(range(0,num_levels_per_var[0]))
113
+ else:
114
+ noise_indices = []
115
+
116
+ if len(qual_index) == 1 and num_levels_per_var[0] < 2:
117
+ temp = quant_index.copy()
118
+ temp.append(qual_index[0])
119
+ quant_index = temp.copy()
120
+ qual_index = []
121
+ lv_dim = 0
122
+ elif len(qual_index) == 0:
123
+ lv_dim = 0
124
+
125
+ quant_correlation_class_name = quant_correlation_class
126
+
127
+ if len(qual_index) == 0:
128
+ lv_dim = 0
129
+
130
+ if quant_correlation_class_name == 'Rough_RBF':
131
+ quant_correlation_class = 'RBFKernel'
132
+
133
+ if quant_correlation_class_name == 'Matern32Kernel':
134
+ quant_correlation_class = 'Matern32Kernel'
135
+
136
+ if quant_correlation_class_name == 'Matern52Kernel':
137
+ quant_correlation_class = 'Matern52Kernel'
138
+
139
+ if quant_correlation_class_name == 'Matern12Kernel':
140
+ quant_correlation_class = 'Matern12Kernel'
141
+
142
+ if len(qual_index) > 0:
143
+ ####################### Defined multiple kernels for seperate variables ###################
144
+ qual_kernels = []
145
+ for i in range(len(qual_kernel_columns)):
146
+ qual_kernels.append(kernels.RBFKernel(
147
+ active_dims=torch.arange(lv_dim) + lv_dim * i) )
148
+ qual_kernels[i].initialize(**{'lengthscale':1.0})
149
+ qual_kernels[i].raw_lengthscale.requires_grad_(False)
150
+
151
+ if len(quant_index) == 0:
152
+ correlation_kernel = qual_kernels[0]
153
+ for i in range(1, len(qual_kernels)):
154
+ correlation_kernel *= qual_kernels[i]
155
+ else:
156
+ try:
157
+ quant_correlation_class = getattr(kernels,quant_correlation_class)
158
+ except:
159
+ raise RuntimeError(
160
+ "%s not an allowed kernel" % quant_correlation_class
161
+ )
162
+
163
+ if quant_correlation_class_name == 'RBFKernel':
164
+ quant_kernel = quant_correlation_class(
165
+ ard_num_dims=len(quant_index),
166
+ active_dims=len(qual_kernel_columns) * lv_dim+torch.arange(len(quant_index)),
167
+ lengthscale_constraint= Positive(transform= torch.exp,inv_transform= torch.log)
168
+ )
169
+ elif quant_correlation_class_name == 'Rough_RBF':
170
+ quant_kernel = quant_correlation_class(
171
+ ard_num_dims=len(quant_index),
172
+ active_dims=len(qual_kernel_columns)*lv_dim+torch.arange(len(quant_index)),
173
+ lengthscale_constraint= Positive(transform = lambda x: 2.0**(-0.5) * torch.pow(10,-x/2),inv_transform= lambda x: -2.0*torch.log10(x/2.0))
174
+ )
175
+
176
+
177
+ elif quant_correlation_class_name == 'Matern12Kernel':
178
+ quant_kernel = quant_correlation_class(
179
+ ard_num_dims=len(quant_index),
180
+ active_dims=len(qual_kernel_columns)*lv_dim+torch.arange(len(quant_index)),
181
+ lengthscale_constraint= Positive(transform= lambda x: 2.0**(-0.5) * torch.pow(10,-x/2),inv_transform= lambda x: -2.0*torch.log10(x/2.0))
182
+ )
183
+
184
+ elif quant_correlation_class_name == 'Matern32Kernel':
185
+ quant_kernel = quant_correlation_class(
186
+ ard_num_dims=len(quant_index),
187
+ active_dims=len(qual_kernel_columns)*lv_dim+torch.arange(len(quant_index)),
188
+ #lengthscale_constraint= Positive(transform= torch.exp,inv_transform= torch.log)
189
+ lengthscale_constraint= Positive(transform= lambda x: 2.0**(-0.5) * torch.pow(10,-x/2),inv_transform= lambda x: -2.0*torch.log10(x/2.0))
190
+ )
191
+
192
+ elif quant_correlation_class_name == 'Matern52Kernel':
193
+ quant_kernel = quant_correlation_class(
194
+ ard_num_dims=len(quant_index),
195
+ active_dims=len(qual_kernel_columns)*lv_dim+torch.arange(len(quant_index)),
196
+ #lengthscale_constraint= Positive(transform= torch.exp,inv_transform= torch.log)
197
+ lengthscale_constraint= Positive(transform= lambda x: 2.0**(-0.5) * torch.pow(10,-x/2),inv_transform= lambda x: -2.0*torch.log10(x/2.0))
198
+ )
199
+
200
+ if quant_correlation_class_name == 'RBFKernel':
201
+
202
+ quant_kernel.register_prior(
203
+ 'lengthscale_prior', MollifiedUniformPrior(math.log(0.1),math.log(10)),'raw_lengthscale'
204
+ )
205
+
206
+ elif quant_correlation_class_name == 'Rough_RBF':
207
+ quant_kernel.register_prior(
208
+ 'lengthscale_prior',NormalPrior(-3.0,3.0),'raw_lengthscale'
209
+ )
210
+
211
+ elif quant_correlation_class_name == 'Matern12Kernel':
212
+ quant_kernel.register_prior(
213
+ #'lengthscale_prior', MollifiedUniformPrior(math.log(0.1),math.log(10)),'raw_lengthscale'
214
+ 'lengthscale_prior',NormalPrior(-3.0,3.0),'raw_lengthscale'
215
+ )
216
+
217
+ elif quant_correlation_class_name == 'Matern32Kernel':
218
+ quant_kernel.register_prior(
219
+ #'lengthscale_prior', MollifiedUniformPrior(math.log(0.1),math.log(10)),'raw_lengthscale'
220
+ 'lengthscale_prior',NormalPrior(-3.0,3.0),'raw_lengthscale'
221
+ )
222
+
223
+ elif quant_correlation_class_name == 'Matern52Kernel':
224
+ quant_kernel.register_prior(
225
+ #'lengthscale_prior', MollifiedUniformPrior(math.log(0.1),math.log(10)),'raw_lengthscale'
226
+ 'lengthscale_prior',NormalPrior(-3.0,3.0),'raw_lengthscale'
227
+ )
228
+
229
+ if len(qual_index) > 0:
230
+ temp = qual_kernels[0]
231
+ for i in range(1, len(qual_kernels)):
232
+ temp *= qual_kernels[i]
233
+ correlation_kernel = temp*quant_kernel #+ qual_kernel + quant_kernel
234
+ else:
235
+ correlation_kernel = quant_kernel
236
+
237
+ super(GPPLUS,self).__init__(
238
+ train_x=train_x,train_y=train_y,noise_indices=noise_indices,
239
+ correlation_kernel=correlation_kernel,
240
+ noise=noise,fix_noise=fix_noise,lb_noise=lb_noise
241
+ )
242
+
243
+ # register index and transforms
244
+ self.register_buffer('quant_index',torch.tensor(quant_index))
245
+ self.register_buffer('qual_index',torch.tensor(qual_index))
246
+
247
+
248
+ self.num_levels_per_var = num_levels_per_var
249
+ self.lv_dim = lv_dim
250
+ self.uniform_encoding_columns = uniform_encoding_columns
251
+ self.encoding_type = encoding_type
252
+ self.perm =[]
253
+ self.zeta = []
254
+ self.perm_dict = []
255
+ self.A_matrix = []
256
+ self.collocation_x = collocation_x ####### ADDED
257
+ self.alpha = 1.0
258
+ self.beta = 20.0
259
+ self.covar_inv = None
260
+ self.omega = 3 #3.2
261
+ self.name_output = name_output
262
+ self.chol_decomp = None
263
+ self.g_uvp = None
264
+ self.k_xX = None
265
+
266
+ if len(qual_kernel_columns) > 0:
267
+ for i in range(len(qual_kernel_columns)):
268
+ if type(qual_kernel_columns[i]) == int:
269
+ num = self.num_levels_per_var[qual_index.index(qual_kernel_columns[i])]
270
+ cat = [num]
271
+ else:
272
+ cat = [self.num_levels_per_var[qual_index.index(k)] for k in qual_kernel_columns[i]]
273
+ num = sum(cat)
274
+
275
+ zeta, perm, perm_dict = self.zeta_matrix(num_levels=cat, lv_dim = self.lv_dim)
276
+ self.zeta.append(zeta.to(**tkwargs))
277
+ self.perm.append(perm.to(**tkwargs))
278
+ self.perm_dict.append(perm_dict)
279
+
280
+ model_temp = FFNN(self, input_size= num, num_classes=lv_dim,
281
+ layers = NN_layers, name = str(qual_kernel_columns[i])).to(**tkwargs)
282
+ self.A_matrix.append(model_temp.to(**tkwargs))
283
+
284
+ self.basis=basis
285
+ i=0
286
+ if self.basis=='single':
287
+ self.mean_module = gpytorch.means.ConstantMean(prior=NormalPrior(0.,1.))
288
+ self.mean_module.constant.data = torch.tensor([0.0]) # Set the desired value
289
+ self.mean_module.constant.requires_grad = False # Fix the hyperparameter
290
+ elif self.basis=='multiple_constant':
291
+ if basis_function_size is None:
292
+ basis_function_size=train_x.shape[1]-1
293
+ self.num_sources=int(torch.max(train_x[:,-1]))
294
+ for i in range(self.num_sources +1):
295
+ if i==0:
296
+ setattr(self,'mean_module_'+str(i), gpytorch.means.ZeroMean())
297
+
298
+ else:
299
+ #Constant
300
+ setattr(self,'mean_module_'+str(i), gpytorch.means.ConstantMean(prior=NormalPrior(0.,.3)))
301
+
302
+ elif self.basis=='multiple_polynomial':
303
+ if basis_function_size is None:
304
+ basis_function_size=train_x.shape[1]-1
305
+ self.num_sources=int(torch.max(train_x[:,-1]))
306
+ for i in range(self.num_sources +1):
307
+ if i==0:
308
+ setattr(self,'mean_module_'+str(i), gpytorch.means.ZeroMean())
309
+ else:
310
+ setattr(self,'mean_module_'+str(i), LinearMean_with_prior(input_size=basis_function_size, batch_shape=torch.Size([]), bias=True))
311
+ elif self.basis=='neural_network':
312
+ ############################################### One NN for ALL
313
+ if len(qual_index) == 0:
314
+ setattr(self,'mean_module_NN_All', FFNN_for_Mean(self, input_size= train_x.shape[1], num_classes=4,layers =NN_layers_base, name = str('mean_module_'+str(i)+'_')))
315
+ else:
316
+ setattr(self,'mean_module_NN_All', FFNN_for_Mean(self, input_size= train_x.shape[1]-len(qual_index)+2, num_classes=1, layers =NN_layers_base, name = str('mean_module_'+str(i)+'_')))
317
+ elif self.basis=='M3':
318
+ setattr(self,'mean_module_NN_All', NetworkM4(input_dim = train_x.shape[1], output_dim=3, layers = NN_layers_base))
319
+
320
+
321
+ # Fix the hyperparameter value
322
+ self.covar_module.base_kernel.raw_lengthscale.data = torch.tensor([self.omega, self.omega], dtype=torch.float32) # Set the desired value
323
+ self.covar_module.base_kernel.raw_lengthscale.requires_grad = False # Fix the hyperparameter
324
+
325
+ self.covar_module.raw_outputscale.data = torch.tensor(0.541) # Set the desired value
326
+ self.covar_module.raw_outputscale.requires_grad = False # Fix the hyperparameter
327
+
328
+ def forward(self,x:torch.Tensor) -> MultivariateNormal:
329
+ x_forward_raw=x.clone()
330
+ nd_flag = 0
331
+ if x.dim() > 2:
332
+ xsize = x.shape
333
+ x = x.reshape(-1, x.shape[-1])
334
+ nd_flag = 1
335
+
336
+ if len(self.qual_kernel_columns) > 0:
337
+ embeddings = []
338
+ for i in range(len(self.qual_kernel_columns)):
339
+ temp= self.transform_categorical(x=x[:,self.qual_kernel_columns[i]].clone().type(torch.int64),
340
+ perm_dict = self.perm_dict[i], zeta = self.zeta[i])
341
+ embeddings.append(self.A_matrix[i](temp))
342
+
343
+ x= torch.cat([embeddings[0],x[...,self.quant_index]],dim=-1)
344
+
345
+ if nd_flag == 1:
346
+ x = x.reshape(*xsize[:-1], -1)
347
+
348
+ #################### Multiple bases (General Case) ####################################
349
+ def multi_mean(x,x_forward_raw):
350
+ mean_x=torch.zeros_like(x[:,-1])
351
+ if self.basis=='single':
352
+ mean_x=self.mean_module(x)
353
+ elif self.basis=='multiple_constant':
354
+ for i in range(len(mean_x)):
355
+ qq=int(x_forward_raw[i,-1])
356
+ mean_x[i]=getattr(self,'mean_module_'+str(qq))(torch.tensor(x[i,-1].clone()).reshape(-1,1))
357
+ elif self.basis=='multiple_polynomial':
358
+ for i in range(len(mean_x)):
359
+ qq=int(x_forward_raw[i,-1])
360
+ mean_x[i]=getattr(self,'mean_module_'+str(qq))(torch.cat((torch.tensor((x[i,-1].clone().double().reshape(-1,1))**2),torch.tensor(x[i,-1].clone().double()).reshape(-1,1)),1))
361
+
362
+ elif self.basis=='neural_network':
363
+ mean_x = getattr(self,'mean_module_NN_All')(x.clone())#.reshape(-1) #### FOR MULTIOUTPUT DELETE RESHAPE
364
+
365
+ elif self.basis=='M3':
366
+ if hasattr(self, 'name_output'):
367
+ mean_x = getattr(self,'mean_module_NN_All')(x.clone())
368
+ else:
369
+ mean_x = getattr(self,'mean_module_NN_All')(x.clone()).reshape(-1)
370
+
371
+ return mean_x
372
+ ##########################################################################################
373
+ if self.name_output == 'u':
374
+ mean_x = multi_mean(x,x_forward_raw)[:,0].reshape(-1)
375
+ if self.name_output == 'v':
376
+ mean_x = multi_mean(x,x_forward_raw)[:,1].reshape(-1)
377
+ if self.name_output == 'p':
378
+ mean_x = multi_mean(x,x_forward_raw)[:,2].reshape(-1)
379
+ if self.name_output == 'ro':
380
+ mean_x = multi_mean(x,x_forward_raw)[:,3].reshape(-1)
381
+ covar_x = self.covar_module(x)
382
+
383
+ return MultivariateNormal(mean_x,covar_x)
384
+
385
+ def predict(self, Xtest,return_std=True, include_noise = True):
386
+ with torch.no_grad():
387
+ return super().predict(Xtest, return_std = return_std, include_noise= include_noise)
388
+
389
+ def predict_with_grad(self, Xtest,return_std=True, include_noise = True):
390
+ return super().predict(Xtest, return_std = return_std, include_noise= include_noise)
391
+
392
+ def noise_value(self):
393
+ noise = self.likelihood.noise_covar.noise.detach() * self.y_std**2
394
+ return noise
395
+
396
+ def visualize_latent(self, suptitle = None):
397
+ if len(self.qual_kernel_columns) > 0:
398
+ for i in range(len(self.qual_kernel_columns)):
399
+ zeta = self.zeta[i]
400
+ A = self.A_matrix[i]
401
+
402
+ positions = A(zeta)
403
+ level = torch.max(self.perm[i], axis = 0)[0].tolist()
404
+ perm = self.perm[i]
405
+ plot_sep(positions = positions, levels = level, perm = perm, constraints_flag=True, )
406
+
407
+
408
+ def visualize_latent_position(self,lv_columns=None):
409
+ if len(self.qual_kernel_columns) > 0:
410
+ for i in range(len(self.qual_kernel_columns)):
411
+ zeta = self.zeta[i]
412
+ A = self.A_matrix[i]
413
+ positions = A(zeta)
414
+ if self.qual_kernel_columns[i]==lv_columns[0]:
415
+ return positions
416
+
417
+
418
+ def visualize_latent_position_simple(self, suptitle = None):
419
+ if len(self.qual_kernel_columns) > 0:
420
+ for i in range(len(self.qual_kernel_columns)):
421
+ zeta = self.zeta[i]
422
+ A = self.A_matrix[i]
423
+ positions = A(zeta)
424
+ return positions
425
+
426
+
427
+ @classmethod
428
+ def show(cls):
429
+ plt.show()
430
+
431
+ def get_params(self, name = None):
432
+ params = {}
433
+ print('###################Parameters###########################')
434
+ for n, value in self.named_parameters():
435
+ params[n] = value
436
+ if name is None:
437
+ print(params)
438
+ return params
439
+ else:
440
+ if name == 'Mean':
441
+ key = 'mean_module.constant'
442
+ elif name == 'Sigma':
443
+ key = 'covar_module.raw_outputscale'
444
+ elif name == 'Noise':
445
+ key = 'likelihood.noise_covar.raw_noise'
446
+ elif name == 'Omega':
447
+ for n in params.keys():
448
+ if 'raw_lengthscale' in n and params[n].numel() > 1:
449
+ key = n
450
+ print(params[key])
451
+ return params[key]
452
+
453
+ def get_latent_space(self):
454
+ if len(self.qual_index) > 0:
455
+ zeta = torch.tensor(self.zeta)
456
+ positions = self.nn_model(zeta)
457
+ return positions.detach()
458
+ else:
459
+ print('No categorical Variable, No latent positions')
460
+ return None
461
+
462
+ def zeta_matrix(self,
463
+ num_levels:int,
464
+ lv_dim:int,
465
+ batch_shape=torch.Size()
466
+ ) -> None:
467
+
468
+ if any([i == 1 for i in num_levels]):
469
+ raise ValueError('Categorical variable has only one level!')
470
+
471
+ if lv_dim == 1:
472
+ raise RuntimeWarning('1D latent variables are difficult to optimize!')
473
+
474
+ for level in num_levels:
475
+ if lv_dim > level - 0:
476
+ lv_dim = min(lv_dim, level-1)
477
+ raise RuntimeWarning(
478
+ 'The LV dimension can atmost be num_levels-1. '
479
+ 'Setting it to %s in place of %s' %(level-1,lv_dim)
480
+ )
481
+
482
+ from itertools import product
483
+ levels = []
484
+ for l in num_levels:
485
+ levels.append(torch.arange(l))
486
+
487
+ perm = list(product(*levels))
488
+ perm = torch.tensor(perm, dtype=torch.int64)
489
+
490
+ #-------------Mapping-------------------------
491
+ perm_dic = {}
492
+ for i, row in enumerate(perm):
493
+ temp = str(row.tolist())
494
+ if temp not in perm_dic.keys():
495
+ perm_dic[temp] = i
496
+
497
+ #-------------One_hot_encoding------------------
498
+ for ii in range(perm.shape[-1]):
499
+ if perm[...,ii].min() != 0:
500
+ perm[...,ii] -= perm[...,ii].min()
501
+
502
+ perm_one_hot = []
503
+ for i in range(perm.size()[1]):
504
+ perm_one_hot.append( torch.nn.functional.one_hot(perm[:,i]) )
505
+
506
+ perm_one_hot = torch.concat(perm_one_hot, axis=1)
507
+
508
+ return perm_one_hot, perm, perm_dic
509
+
510
+
511
+ def transform_categorical(self, x:torch.Tensor,perm_dict = [], zeta = []) -> None:
512
+ if x.dim() == 1:
513
+ x = x.reshape(-1,1)
514
+ # categorical should start from 0
515
+ if self.training == False:
516
+ x = setlevels(x.cpu())
517
+ if self.encoding_type == 'one-hot':
518
+ index = [perm_dict[str(row.tolist())] for row in x]
519
+
520
+ if x.dim() == 1:
521
+ x = x.reshape(len(x),)
522
+
523
+ return zeta[index,:]
524
+
525
+ elif self.encoding_type == 'uniform':
526
+
527
+ temp2=np.random.uniform(0,1,(len(self.perm), self.uniform_encoding_columns))
528
+ dict={}
529
+ dict2={}
530
+
531
+ for i in range(0,self.perm.shape[0]):
532
+ dict[tuple((self.perm[i,:]).numpy())]=temp2[i,:]
533
+
534
+ for i in range(0,x.shape[0]):
535
+ dict2[i]=dict[tuple((x[i]).numpy())]
536
+
537
+ x_one_hot= torch.from_numpy(np.array(list(dict2.values())))
538
+ else:
539
+ raise ValueError ('Invalid type')
540
+
541
+
542
+ return x_one_hot
543
+
544
+
545
+ ##########################################################################################################################################################
546
+
547
+ class LinearMean_with_prior(gpytorch.means.Mean):
548
+ def __init__(self, input_size, batch_shape=torch.Size(), bias=True):
549
+ super().__init__()
550
+ self.register_parameter(name="weights", parameter=torch.nn.Parameter(torch.randn(*batch_shape, input_size, 1)))
551
+ self.register_prior(name = 'weights_prior', prior=gpytorch.priors.NormalPrior(0.,1.), param_or_closure='weights')
552
+
553
+ if bias:
554
+ self.register_parameter(name="bias", parameter=torch.nn.Parameter(torch.randn(*batch_shape, 1)))
555
+ self.register_prior(name = 'bias_prior', prior=gpytorch.priors.NormalPrior(0.,1.), param_or_closure='bias')
556
+
557
+ else:
558
+ self.bias = None
559
+
560
+ def forward(self, x):
561
+ res = x.matmul(self.weights).squeeze(-1)
562
+ if self.bias is not None:
563
+ res = res + self.bias
564
+ return res
565
+
566
+
567
+ ###############################################################################################################################################################################################################################################################
568
+ class FFNN(torch.nn.Module):
569
+ def __init__(self, lmgp, input_size, num_classes, layers,name):
570
+ super(FFNN, self).__init__()
571
+ # Our first linear layer take input_size, in this case 784 nodes to 50
572
+ # and our second linear layer takes 50 to the num_classes we have, in
573
+ # this case 10.
574
+ self.hidden_num = len(layers)
575
+ if self.hidden_num > 0:
576
+ self.fci = torch.nn.Linear(input_size, layers[0], bias=False)
577
+ lmgp.register_parameter('fci', self.fci.weight)
578
+ lmgp.register_prior(name = 'latent_prior_fci', prior=gpytorch.priors.NormalPrior(0.,3.), param_or_closure='fci')
579
+
580
+ for i in range(1,self.hidden_num):
581
+ #self.h = nn.Linear(neuran[i-1], neuran[i])
582
+ setattr(self, 'h' + str(i), torch.nn.Linear(layers[i-1], layers[i], bias=False))
583
+ lmgp.register_parameter('h'+str(i), getattr(self, 'h' + str(i)).weight )
584
+ lmgp.register_prior(name = 'latent_prior'+str(i), prior=gpytorch.priors.NormalPrior(0.,3.), param_or_closure='h'+str(i))
585
+
586
+ self.fce = torch.nn.Linear(layers[-1], num_classes, bias= False)
587
+ lmgp.register_parameter('fce', self.fce.weight)
588
+ lmgp.register_prior(name = 'latent_prior_fce', prior=gpytorch.priors.NormalPrior(0.,3.), param_or_closure='fce')
589
+ else:
590
+ self.fci = Linear_MAP(input_size, num_classes, bias = False)
591
+ lmgp.register_parameter(name, self.fci.weight)
592
+ lmgp.register_prior(name = 'latent_prior_'+name, prior=gpytorch.priors.NormalPrior(0,3) , param_or_closure=name)
593
+
594
+
595
+
596
+ def forward(self, x, transform = lambda x: x):
597
+ """
598
+ x here is the mnist images and we run it through fc1, fc2 that we created above.
599
+ we also add a ReLU activation function in between and for that (since it has no parameters)
600
+ I recommend using nn.functional (F)
601
+ """
602
+ if self.hidden_num > 0:
603
+ x = torch.tanh(self.fci(x))
604
+ for i in range(1,self.hidden_num):
605
+ x = torch.tanh( getattr(self, 'h' + str(i))(x) )
606
+
607
+ x = self.fce(x)
608
+ else:
609
+ x = self.fci(x, transform)
610
+ return x
611
+
612
+ class FFNN_for_Mean(gpytorch.Module):
613
+ def __init__(self, lmgp, input_size, num_classes, layers, name):
614
+ super(FFNN_for_Mean, self).__init__()
615
+ self.dropout = torch.nn.Dropout(0.0)
616
+ # Our first linear layer take input_size, in this case 784 nodes to 50
617
+ # and our second linear layer takes 50 to the num_classes we have, in
618
+ # this case 10.
619
+ self.hidden_num = len(layers)
620
+ if self.hidden_num > 0:
621
+ self.fci = Linear_new(input_size, layers[0], bias=True, name='fci')
622
+ for i in range(1,self.hidden_num):
623
+ setattr(self, 'h' + str(i), Linear_new(layers[i-1], layers[i], bias=True,name='h' + str(i)))
624
+
625
+ self.fce = Linear_new(layers[-1], num_classes, bias=True,name='fce')
626
+ else:
627
+ self.fci = Linear_new(input_size, num_classes, bias=True,name='fci') #Linear_MAP(input_size, num_classes, bias = True)
628
+
629
+ def forward(self, x, transform = lambda x: x):
630
+ """
631
+ x here is the mnist images and we run it through fc1, fc2 that we created above.
632
+ we also add a ReLU activation function in between and for that (since it has no parameters)
633
+ I recommend using nn.functional (F)
634
+ """
635
+ if self.hidden_num > 0:
636
+
637
+ x = torch.tanh(self.fci(x))
638
+ # x = self.dropout(x)
639
+ # x = self.fci(x)
640
+ for i in range(1,self.hidden_num):
641
+ x = torch.tanh( getattr(self, 'h' + str(i))(x) )
642
+
643
+ x = self.fce(x)
644
+ #x = torch.cat([x[...,:3] , torch.sigmoid(x[...,3]).unsqueeze(-1)] , dim = -1)
645
+ else:
646
+ x = self.fci(x)
647
+ #x = torch.cat([x[...,:3] , torch.tanh(x[...,3]).unsqueeze(-1)] , dim = -1)
648
+
649
+ return x
650
+
651
+
652
+
653
+ class NetworkM4(torch.nn.Module):
654
+ def __init__(self, input_dim = 2, output_dim = 1, layers = [40, 40, 40, 40], activation = 'tanh', collocation_x = []) -> None:
655
+ super(NetworkM4, self).__init__()
656
+ activation_list = {'tanh':torch.nn.Tanh(), 'Silu':torch.nn.SiLU(), 'Sigmoid':torch.nn.Sigmoid()}
657
+ activation = activation_list[activation]
658
+ self.dim = layers[0]
659
+
660
+ self.U = torch.nn.Linear(input_dim, self.dim).to('cuda')
661
+ self.V = torch.nn.Linear(input_dim, self.dim).to('cuda')
662
+ self.H1 = torch.nn.Linear(input_dim, self.dim).to('cuda')
663
+ self.last= torch.nn.Linear(self.dim, output_dim).to('cuda')
664
+
665
+ self.collocation_x = collocation_x
666
+ self.alpha = 1.0
667
+ self.beta = 1.0
668
+
669
+ l = torch.nn.ModuleList()
670
+ for _ in range(len(layers)):
671
+ l.append(torch.nn.Linear(self.dim, self.dim))
672
+ l.append(activation)
673
+ self.layers = torch.nn.Sequential(*l).to('cuda')
674
+
675
+ def forward(self, input):
676
+ U = torch.nn.Tanh()(self.U(input))
677
+ V = torch.nn.Tanh()(self.V(input))
678
+ H = torch.nn.Tanh()(self.H1(input))
679
+
680
+ for layer in self.layers:
681
+ Z = layer(H)
682
+ H = (1-Z)*U + Z*V
683
+
684
+ out = self.last(H)
685
+ return out
686
+
687
+ class Linear_new(gpytorch.means.Mean):
688
+ r"""Applies a linear transformation to the incoming data: :math:`y = xA^T + b`
689
+
690
+ This module supports :ref:`TensorFloat32<tf32_on_ampere>`.
691
+
692
+ On certain ROCm devices, when using float16 inputs this module will use :ref:`different precision<fp16_on_mi200>` for backward.
693
+
694
+ Args:
695
+ in_features: size of each input sample
696
+ out_features: size of each output sample
697
+ bias: If set to ``False``, the layer will not learn an additive bias.
698
+ Default: ``True``
699
+
700
+ Shape:
701
+ - Input: :math:`(*, H_{in})` where :math:`*` means any number of
702
+ dimensions including none and :math:`H_{in} = \text{in\_features}`.
703
+ - Output: :math:`(*, H_{out})` where all but the last dimension
704
+ are the same shape as the input and :math:`H_{out} = \text{out\_features}`.
705
+
706
+ Attributes:
707
+ weight: the learnable weights of the module of shape
708
+ :math:`(\text{out\_features}, \text{in\_features})`. The values are
709
+ initialized from :math:`\mathcal{U}(-\sqrt{k}, \sqrt{k})`, where
710
+ :math:`k = \frac{1}{\text{in\_features}}`
711
+ bias: the learnable bias of the module of shape :math:`(\text{out\_features})`.
712
+ If :attr:`bias` is ``True``, the values are initialized from
713
+ :math:`\mathcal{U}(-\sqrt{k}, \sqrt{k})` where
714
+ :math:`k = \frac{1}{\text{in\_features}}`
715
+
716
+ Examples::
717
+
718
+ >>> m = nn.Linear(20, 30)
719
+ >>> input = torch.randn(128, 20)
720
+ >>> output = m(input)
721
+ >>> print(output.size())
722
+ torch.Size([128, 30])
723
+ """
724
+ __constants__ = ['in_features', 'out_features']
725
+ in_features: int
726
+ out_features: int
727
+ weight: torch.Tensor
728
+
729
+ def __init__(self, in_features: int, out_features: int, bias: bool = True, name=None,
730
+ device=None, dtype=None) -> None:
731
+ # factory_kwargs = {'device': device, 'dtype': dtype}
732
+ # factory_kwargs=tkwargs
733
+ super(Linear_new, self).__init__()
734
+ self.in_features = in_features
735
+ self.out_features = out_features
736
+
737
+ self.name=str(name)
738
+ self.register_parameter(name=str(self.name)+'weight', parameter= Parameter(torch.empty((out_features, in_features))))
739
+ self.register_prior(name =str(self.name)+ 'prior_m_weight_fci', prior=gpytorch.priors.NormalPrior(0.,1.), param_or_closure=str(self.name)+'weight')
740
+
741
+ if bias:
742
+ self.register_parameter(name=str(self.name)+'bias', parameter=Parameter(torch.empty(out_features)))
743
+ self.register_prior(name= str(self.name)+'prior_m_bias_fci', prior=gpytorch.priors.NormalPrior(0.,1.), param_or_closure=str(self.name)+'bias')
744
+ else:
745
+ self.register_parameter('bias', None)
746
+ self.reset_parameters()
747
+
748
+ def reset_parameters(self) -> None:
749
+ # Setting a=sqrt(5) in kaiming_uniform is the same as initializing with
750
+ # uniform(-1/sqrt(in_features), 1/sqrt(in_features)). For details, see
751
+ # https://github.com/pytorch/pytorch/issues/57109
752
+ torch.nn.init.kaiming_uniform_( getattr(self,str(self.name)+'weight'), a=math.sqrt(5))
753
+ if getattr(self,str(self.name)+'bias') is not None:
754
+ fan_in, _ = torch.nn.init._calculate_fan_in_and_fan_out(getattr(self,str(self.name)+'weight'))
755
+ bound = 1 / math.sqrt(fan_in) if fan_in > 0 else 0
756
+ torch.nn.init.uniform_(getattr(self,str(self.name)+'bias'), -bound, bound)
757
+
758
+ def forward(self, input) -> torch.Tensor:
759
+ # return F.linear(input, self.weight, self.bias)
760
+ # print(getattr(self,str(self.name)+'weight'))
761
+
762
+ # return F.linear(input, getattr(self,str(self.name)+'weight').double(), getattr(self,str(self.name)+'bias').double()) ### Forced to Add .double() for NN in mean function
763
+ return F.linear(input, getattr(self,str(self.name)+'weight'), getattr(self,str(self.name)+'bias')) ### Forced to Add .double() for NN in mean function
764
+
765
+ def extra_repr(self) -> str:
766
+ return 'in_features={}, out_features={}, bias={}'.format(
767
+ self.in_features, self.out_features, self.bias is not None
768
+ )
769
+
770
+ class Linear_MAP(torch.nn.Linear):
771
+ def __init__(self, in_features: int, out_features: int, bias: bool = True, device=None, dtype=None) -> None:
772
+ super().__init__(in_features, out_features, bias, device, dtype)
773
+
774
+ def forward(self, input, transform = lambda x: x):
775
+ return F.linear(input,transform(self.weight), self.bias)
776
+
777
+
778
+
model/gpregression.py ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import gpytorch
3
+ import math
4
+ from gpytorch.models import ExactGP
5
+ from gpytorch import settings as gptsettings
6
+ from gpytorch.constraints import GreaterThan,Positive
7
+ from gpytorch.distributions import MultivariateNormal
8
+ import gpytorch.kernels as kernels
9
+ from .horseshoe import LogHalfHorseshoePrior
10
+ from .mollified_uniform import MollifiedUniformPrior
11
+ from gpytorch.priors import NormalPrior,LogNormalPrior
12
+ from onescience.utils.GP_TO.transforms import softplus,inv_softplus
13
+
14
+ from typing import List,Tuple,Union
15
+
16
+ from typing import List, Union
17
+
18
+ class GPR(ExactGP):
19
+ """Standard GP regression module for numerical inputs
20
+
21
+ :param train_x: The training inputs (size N x d). All input variables are expected
22
+ to be numerical. For best performance, scale the variables to the unit hypercube.
23
+ :type train_x: torch.Tensor
24
+ :param train_y: The training targets (size N)
25
+ :type train_y: torch.Tensor
26
+ :param correlation_kernel: Either a `gpytorch.kernels.Kernel` instance or one of the
27
+ following strings - 'RBFKernel' (radial basis kernel), 'Matern52Kernel' (twice
28
+ differentiable Matern kernel), 'Matern32Kernel' (first order differentiable Matern
29
+ kernel). If the former is specified, any hyperparameters to be estimated need to have
30
+ associated priors for multi-start optimization. If the latter is specified, then
31
+ the kernel uses a separate lengthscale for each input variable.
32
+ :type correlation_kernel: Union[gpytorch.kernels.Kernel,str]
33
+ :param noise: The (initial) noise variance.
34
+ :type noise: float, optional
35
+ :param fix_noise: Fixes the noise variance at the current level if `True` is specifed.
36
+ Defaults to `False`
37
+ :type fix_noise: bool, optional
38
+ :param lb_noise: Lower bound on the noise variance. Setting a higher value results in
39
+ more stable computations, when optimizing noise variance, but might reduce
40
+ prediction quality. Defaults to 1e-6
41
+ :type lb_noise: float, optional
42
+ """
43
+ def __init__(
44
+ self,
45
+ train_x:torch.Tensor,
46
+ train_y:torch.Tensor,
47
+ correlation_kernel,
48
+ noise_indices:List[int],
49
+ noise:float=1e-4,
50
+ fix_noise:bool=False,
51
+ lb_noise:float=1e-12,
52
+ ) -> None:
53
+ # check inputs
54
+ if not torch.is_tensor(train_x):
55
+ raise RuntimeError("'train_x' must be a tensor")
56
+ if not torch.is_tensor(train_y):
57
+ raise RuntimeError("'train_y' must be a tensor")
58
+
59
+ if train_x.shape[0] != train_y.shape[0]:
60
+ raise RuntimeError("Inputs and output have different number of observations")
61
+
62
+ # initializing likelihood
63
+ noise_constraint=GreaterThan(lb_noise,transform=torch.exp,inv_transform=torch.log)
64
+
65
+ if len(noise_indices) == 0:
66
+ likelihood = gpytorch.likelihoods.GaussianLikelihood(noise_constraint=noise_constraint)
67
+
68
+ y_mean = torch.tensor(0.0)
69
+ y_std = torch.tensor(1.0)
70
+ train_y_sc = (train_y-y_mean)/y_std
71
+
72
+ ExactGP.__init__(self, train_x,train_y_sc, likelihood)
73
+
74
+ # registering mean and std of the raw response
75
+ self.register_buffer('y_mean',y_mean)
76
+ self.register_buffer('y_std',y_std)
77
+ self.register_buffer('y_scaled',train_y_sc)
78
+
79
+ self._num_outputs = 1
80
+
81
+ # initializing and fixing noise
82
+ if noise is not None:
83
+ self.likelihood.initialize(noise=noise)
84
+
85
+ self.likelihood.register_prior('noise_prior',LogHalfHorseshoePrior(0.01,lb_noise),'raw_noise')
86
+ if fix_noise:
87
+ self.likelihood.raw_noise.requires_grad_(False)
88
+
89
+ if isinstance(correlation_kernel,str):
90
+ try:
91
+ correlation_kernel_class = getattr(kernels,correlation_kernel)
92
+ correlation_kernel = correlation_kernel_class(
93
+ ard_num_dims = self.train_inputs[0].size(1),
94
+ lengthscale_constraint=Positive(transform=torch.exp,inv_transform=torch.log),
95
+ )
96
+ correlation_kernel.register_prior(
97
+ 'lengthscale_prior',MollifiedUniformPrior(math.log(0.1),math.log(10)),'raw_lengthscale'
98
+ )
99
+ except:
100
+ raise RuntimeError(
101
+ "%s not an allowed kernel" % correlation_kernel
102
+ )
103
+ elif not isinstance(correlation_kernel,gpytorch.kernels.Kernel):
104
+ raise RuntimeError(
105
+ "specified correlation kernel is not a `gpytorch.kernels.Kernel` instance"
106
+ )
107
+
108
+ self.covar_module = kernels.ScaleKernel(
109
+ base_kernel = correlation_kernel,
110
+ outputscale_constraint=Positive(transform=softplus,inv_transform=inv_softplus),
111
+ )
112
+ # register priors
113
+ self.covar_module.register_prior(
114
+ 'outputscale_prior',LogNormalPrior(1e-6,1.),'outputscale'
115
+ )
116
+
117
+
118
+
119
+ def forward(self,x:torch.Tensor)->MultivariateNormal:
120
+ mean_x = self.mean_module(x)
121
+ covar_x = self.covar_module(x)
122
+ return MultivariateNormal(mean_x,covar_x)
123
+
124
+ def predict(
125
+ self,x:torch.Tensor,return_std:bool=False,include_noise:bool=False
126
+ )-> Union[torch.Tensor,Tuple[torch.Tensor]]:
127
+ """Returns the predictive mean, and optionally the standard deviation at the given points
128
+
129
+ :param x: The input variables at which the predictions are sought.
130
+ :type x: torch.Tensor
131
+ :param return_std: Standard deviation is returned along the predictions if `True`.
132
+ Defaults to `False`.
133
+ :type return_std: bool, optional
134
+ :param include_noise: Noise variance is included in the standard deviation if `True`.
135
+ Defaults to `False`.
136
+ :type include_noise: bool
137
+ """
138
+ self.eval()
139
+ with gptsettings.fast_computations(log_prob=False):
140
+ # determine if batched or not
141
+ ndim = self.train_targets.ndim
142
+ if ndim == 1:
143
+ output = self(x)
144
+ else:
145
+ # for batched GPs
146
+ num_samples = self.train_targets.shape[0]
147
+ output = self(x.unsqueeze(0).repeat(num_samples,1,1))
148
+
149
+ if return_std and include_noise:
150
+ output = self.likelihood(output)
151
+
152
+ out_mean = self.y_mean + self.y_std*output.mean
153
+
154
+ # standard deviation may not always be needed
155
+ if return_std:
156
+ out_std = output.variance.sqrt()*self.y_std
157
+ return out_mean,out_std
158
+
159
+ return out_mean
model/horseshoe.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ import torch
3
+ from gpytorch.priors import Prior
4
+ from torch.distributions import HalfCauchy,HalfNormal,constraints
5
+ from torch.distributions.utils import broadcast_all
6
+ from numbers import Number
7
+
8
+ class LogHalfHorseshoePrior(Prior):
9
+ """Prior for the log-noise variance hyperparameter for GPs.
10
+
11
+ This is parameterized by `scale` and `lb`. `lb` is the lower bound on the noise variance.
12
+ The `scale` parameter is more important. The default value for `scale` - 0.01 - works well
13
+ for deterministic and low-noise situations. A larger value may be need in noisy sitations
14
+ and small training datasets. A larger scale implies more noisy data as the prior.
15
+
16
+ To change the scale for this prior for a model to say 0.1,
17
+ >>> model.likelihood.register(
18
+ >>> 'noise_prior',LogHalfHorseshoePrior(0.1,model.likelihood.noise_prior.lb),
19
+ >>> 'raw_noise'
20
+ >>> )
21
+
22
+ .. note::
23
+ The `log_prob` method is only approximate and unnormalized. There is no closed form
24
+ expression for the underlying horseshoe distribution. The lower and upper bounds on
25
+ its' density are, however, known. Here, we use the same approximate density value that
26
+ the spearmint package uses.
27
+
28
+ :param scale: scale parameter of the Horseshoe distribution
29
+ :type scale: float or torch.Tensor
30
+
31
+ :param lb: lower bound on the original scale. Defaults to 1e-6
32
+ :type lb: float or torch.Tensor, optional
33
+ """
34
+ arg_constraints = {"scale": constraints.positive,"lb":constraints.positive}
35
+ support = constraints.real
36
+ def __init__(self, scale, lb=1e-6,validate_args=None):
37
+ self.scale,self.lb = broadcast_all(scale,lb)
38
+ if isinstance(scale,Number):
39
+ batch_shape = torch.Size()
40
+ else:
41
+ batch_shape = self.scale.size()
42
+ super().__init__(batch_shape,validate_args=validate_args)
43
+
44
+ def transform(self, x):
45
+ return self.lb + torch.exp(x)
46
+
47
+ def log_prob(self, X):
48
+ # first term is the density in the original scale
49
+ # the second term is for the transformation
50
+ return torch.log(torch.log(1+3*(self.scale / self.transform(X)) ** 2))+ X
51
+
52
+ def rsample(self, sample_shape=torch.Size([])):
53
+ local_shrinkage = HalfCauchy(1).rsample(self.scale.shape).to(self.lb)
54
+ param_sample = HalfNormal(local_shrinkage * self.scale).rsample(sample_shape).to(self.lb)
55
+ if len(self.lb) > 1:
56
+ param_sample[param_sample<self.lb[0]] = self.lb[0]
57
+ else:
58
+ param_sample[param_sample<self.lb] = self.lb
59
+ return param_sample.log()
60
+
61
+ def expand(self,expand_shape, _instance=None):
62
+ batch_shape = torch.Size(expand_shape)
63
+ return LogHalfHorseshoePrior(self.scale.expand(batch_shape))
model/mollified_uniform.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ import torch
3
+ from gpytorch.priors import Prior
4
+ from torch.distributions import constraints,Uniform,Normal
5
+ from torch.distributions.utils import broadcast_all
6
+ from numbers import Number
7
+
8
+ class MollifiedUniformPrior(Prior):
9
+ r"""Uniform distribution that is differentiable everywhere
10
+
11
+ This is an approximation to the Uniform distribution which maintains differentiability by placing a
12
+ Gaussian distribution over points away from the original support. The density for a single dimension is
13
+
14
+ .. math::
15
+
16
+ p(x) &= \frac{M}{\sqrt{2\pi\sigma^2}}\exp\left[-\frac{d(x)^2}{2\sigma^2}\right], \\
17
+ d(x) &= \begin{cases}
18
+ a-x & x < a \\
19
+ 0 & a\leq x < b\\
20
+ x-b & x \geq b
21
+ \end{cases}
22
+
23
+ :param a: lower range (inclusive)
24
+ :type a: float or torch.Tensor
25
+
26
+ :param b: upper range (exclusive)
27
+ :type b: float or torch.Tensor
28
+
29
+ :param tail_sigma: Standard deviation of the Gaussian distributions on the tails. Lower values make the
30
+ approximation closer to Uniform, but may increase the optimization effort. Defaults to 0.1
31
+ :type tail_sigma: float or torch.Tensor, optional
32
+
33
+ .. note::
34
+ The `rsample` method for this distribution returns uniformly distributed samples from the interval `[a,b)`,
35
+ and **not** from the Mollified distribution. The `log_prob` method, however, returns the correct probability.
36
+ The purpose of the priors in this package is to generate initial starting points for optimizing
37
+ hyperparameters and for MAP estimation.
38
+ """
39
+ arg_constraints = {'a':constraints.real,'b':constraints.real,'tail_sigma':constraints.positive}
40
+ support = constraints.real
41
+ def __init__(self,a,b,tail_sigma=0.1):
42
+ self.a,self.b,self.tail_sigma = broadcast_all(a,b,tail_sigma)
43
+
44
+ if isinstance(a,Number) or isinstance(b,Number):
45
+ batch_shape = torch.Size()
46
+ else:
47
+ batch_shape = self.a.size()
48
+
49
+ super().__init__(batch_shape)
50
+
51
+ @property
52
+ def mean(self):
53
+ return (self.a+self.b)/2
54
+
55
+ @property
56
+ def _half_range(self):
57
+ return (self.b-self.a)/2
58
+
59
+ @property
60
+ def _log_normalization_constant(self):
61
+ return -torch.log(1+(self.b-self.a)/(math.sqrt(2*math.pi)*self.tail_sigma))
62
+
63
+ def log_prob(self,X):
64
+ # expression preserving gradients under automatic differentiation
65
+ tail_dist = ((X-self.mean).abs()-self._half_range).clamp(min=0)
66
+ return Normal(loc=torch.zeros_like(self.a),scale=self.tail_sigma).log_prob(tail_dist)+self._log_normalization_constant
67
+
68
+ def rsample(self,sample_shape=torch.Size([])):
69
+ return Uniform(self.a,self.b).rsample(sample_shape).to(self.a)
70
+
71
+ def expand(self,expand_shape):
72
+ batch_shape = torch.Size(expand_shape)
73
+ return MollifiedUniformPrior(
74
+ self.a.expand(batch_shape),
75
+ self.b.expand(batch_shape),
76
+ self.tail_sigma.expand(batch_shape)
77
+ )
scripts/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+
scripts/common.py ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ import torch
7
+ import yaml
8
+
9
+
10
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
11
+ CONFIG_PATH = PROJECT_ROOT / "conf" / "config.yaml"
12
+ PROBLEMS = ("doublepipe", "diffuser", "rugby", "pipebend")
13
+
14
+
15
+ def ensure_onescience_path(explicit_src=None):
16
+ candidates = []
17
+ if explicit_src:
18
+ candidates.append(Path(explicit_src).expanduser())
19
+ if os.environ.get("ONESCIENCE_SRC"):
20
+ candidates.append(Path(os.environ["ONESCIENCE_SRC"]).expanduser())
21
+ for parent in (PROJECT_ROOT, *PROJECT_ROOT.parents):
22
+ candidates.append(parent / "refactor" / "onescience" / "src")
23
+ candidates.append(parent / "onescience" / "src")
24
+
25
+ for candidate in candidates:
26
+ if (candidate / "onescience").is_dir():
27
+ path = str(candidate.resolve())
28
+ if path not in sys.path:
29
+ sys.path.insert(0, path)
30
+ return candidate.resolve()
31
+ return None
32
+
33
+
34
+ def load_config():
35
+ with CONFIG_PATH.open("r", encoding="utf-8") as f:
36
+ return yaml.safe_load(f)["root"]
37
+
38
+
39
+ def resolve_path(path_value):
40
+ path = Path(path_value)
41
+ return path if path.is_absolute() else PROJECT_ROOT / path
42
+
43
+
44
+ def select_device(section):
45
+ requested = section.get("device", "auto")
46
+ gpu = int(section.get("gpu", 0))
47
+ if requested == "auto":
48
+ return torch.device(f"cuda:{gpu}" if torch.cuda.is_available() else "cpu")
49
+ if requested.startswith("cuda") and not torch.cuda.is_available():
50
+ print(f"Requested {requested}, but CUDA is not available. Falling back to CPU.")
51
+ return torch.device("cpu")
52
+ return torch.device(requested)
53
+
54
+
55
+ def dtype_from_config(name):
56
+ mapping = {
57
+ "float32": torch.float32,
58
+ "float": torch.float32,
59
+ "float64": torch.float64,
60
+ "double": torch.float64,
61
+ }
62
+ if name not in mapping:
63
+ raise ValueError(f"Unsupported dtype: {name}")
64
+ return mapping[name]
65
+
66
+
67
+ def set_nested(cfg, section, key, value):
68
+ if value is not None:
69
+ cfg[section][key] = value
70
+
71
+
72
+ def build_models(cfg, device, *, n_col_domain=None, n_train_per_bc=None, problem=None):
73
+ ensure_onescience_path(cfg.get("runtime", {}).get("onescience_src"))
74
+ from model import GPPLUS
75
+ from onescience.utils.GP_TO import get_data_fluid, set_seed
76
+
77
+ problem = problem or cfg["problem"]
78
+ n_col_domain = int(n_col_domain or cfg["data"]["n_col_domain"])
79
+ n_train_per_bc = int(n_train_per_bc or cfg["data"]["n_train_per_bc"])
80
+ dtype = dtype_from_config(cfg["model"].get("dtype", "float32"))
81
+
82
+ set_seed(int(cfg["seed"]))
83
+ x_col, x_train, sol_train = get_data_fluid(
84
+ problem=problem,
85
+ N_col_domain=n_col_domain,
86
+ N_train=n_train_per_bc,
87
+ )
88
+ collocation_x = x_col.to(device=device, dtype=dtype).clone().requires_grad_(True)
89
+
90
+ models = []
91
+ for i, name in enumerate(cfg["output_names"]):
92
+ model = GPPLUS(
93
+ train_x=x_train[i].type(dtype),
94
+ train_y=sol_train[i].type(dtype),
95
+ collocation_x=collocation_x,
96
+ basis=cfg["model"]["mean_function"],
97
+ NN_layers_base=cfg["model"]["nn_layers_base"],
98
+ name_output=name,
99
+ device=device,
100
+ dtype=dtype,
101
+ ).to(device=device, dtype=dtype)
102
+ models.append(model)
103
+
104
+ return models, {
105
+ "problem": problem,
106
+ "n_col_domain": n_col_domain,
107
+ "n_train_per_bc": n_train_per_bc,
108
+ "x_col_shape": tuple(x_col.shape),
109
+ "x_train_shapes": [tuple(x.shape) for x in x_train],
110
+ "sol_train_shapes": [tuple(y.shape) for y in sol_train],
111
+ }
112
+
113
+
114
+ def save_checkpoint(path, model_list, cfg, metadata, loss_history):
115
+ path = resolve_path(path)
116
+ path.parent.mkdir(parents=True, exist_ok=True)
117
+ torch.save(
118
+ {
119
+ "model_state_dicts": [model.state_dict() for model in model_list],
120
+ "config": cfg,
121
+ "metadata": metadata,
122
+ "loss_history": loss_history,
123
+ },
124
+ path,
125
+ )
126
+ return path
127
+
128
+
129
+ def load_checkpoint(path, model_list, device):
130
+ path = resolve_path(path)
131
+ if not path.is_file():
132
+ raise FileNotFoundError(f"Missing checkpoint: {path}. Run scripts/train.py first.")
133
+ try:
134
+ checkpoint = torch.load(path, map_location=device, weights_only=False)
135
+ except TypeError:
136
+ checkpoint = torch.load(path, map_location=device)
137
+
138
+ state_dicts = checkpoint.get("model_state_dicts")
139
+ if state_dicts is None:
140
+ raise KeyError(f"Checkpoint does not contain model_state_dicts: {path}")
141
+ for model, state_dict in zip(model_list, state_dicts):
142
+ model.load_state_dict(state_dict)
143
+ return checkpoint
144
+
145
+
146
+ def dump_json(data, path):
147
+ path = resolve_path(path)
148
+ path.parent.mkdir(parents=True, exist_ok=True)
149
+ with path.open("w", encoding="utf-8") as f:
150
+ json.dump(data, f, indent=2)
151
+ return path
152
+
153
+
154
+ def tensor_to_numpy_dict(fields):
155
+ return {key: value.detach().cpu().numpy() for key, value in fields.items()}
scripts/fake_data.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import json
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ import numpy as np
7
+
8
+
9
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
10
+ sys.path.insert(0, str(PROJECT_ROOT))
11
+
12
+ from scripts.common import PROBLEMS, ensure_onescience_path, load_config, resolve_path
13
+
14
+
15
+ def parse_args():
16
+ parser = argparse.ArgumentParser(description="Generate GP_for_TO runtime sample tensors.")
17
+ parser.add_argument("--problem", choices=PROBLEMS, default=None)
18
+ parser.add_argument("--n-col-domain", type=int, default=None)
19
+ parser.add_argument("--n-train-per-bc", type=int, default=None)
20
+ parser.add_argument("--output-dir", default=None)
21
+ return parser.parse_args()
22
+
23
+
24
+ def main():
25
+ args = parse_args()
26
+ cfg = load_config()
27
+ ensure_onescience_path(cfg.get("runtime", {}).get("onescience_src"))
28
+ from onescience.utils.GP_TO import get_data_fluid, set_seed
29
+
30
+ problem = args.problem or cfg["fake_data"]["problem"]
31
+ n_col_domain = args.n_col_domain or cfg["fake_data"]["n_col_domain"]
32
+ n_train_per_bc = args.n_train_per_bc or cfg["fake_data"]["n_train_per_bc"]
33
+ output_dir = resolve_path(args.output_dir or cfg["fake_data"]["output_dir"])
34
+ output_dir.mkdir(parents=True, exist_ok=True)
35
+
36
+ set_seed(int(cfg["seed"]))
37
+ x_col, x_train, sol_train = get_data_fluid(
38
+ problem=problem,
39
+ N_col_domain=n_col_domain,
40
+ N_train=n_train_per_bc,
41
+ )
42
+
43
+ npz_path = output_dir / f"{problem}_samples.npz"
44
+ arrays = {"x_col": x_col.cpu().numpy()}
45
+ for i, name in enumerate(cfg["output_names"]):
46
+ arrays[f"x_train_{name}"] = x_train[i].cpu().numpy()
47
+ arrays[f"target_{name}"] = sol_train[i].cpu().numpy()
48
+ np.savez(npz_path, **arrays)
49
+
50
+ metadata = {
51
+ "problem": problem,
52
+ "n_col_domain_requested": int(n_col_domain),
53
+ "n_train_per_bc": int(n_train_per_bc),
54
+ "x_col_shape": list(x_col.shape),
55
+ "x_train_shapes": [list(x.shape) for x in x_train],
56
+ "target_shapes": [list(y.shape) for y in sol_train],
57
+ }
58
+ metadata_path = output_dir / f"{problem}_metadata.json"
59
+ metadata_path.write_text(json.dumps(metadata, indent=2), encoding="utf-8")
60
+ print(f"Fake GP_for_TO tensors written to {npz_path}")
61
+ print(json.dumps(metadata, indent=2))
62
+
63
+
64
+ if __name__ == "__main__":
65
+ main()
scripts/inference.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import json
3
+ import os
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ import numpy as np
8
+ import torch
9
+
10
+
11
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
12
+ sys.path.insert(0, str(PROJECT_ROOT))
13
+
14
+ from scripts.common import (
15
+ PROBLEMS,
16
+ build_models,
17
+ ensure_onescience_path,
18
+ load_checkpoint,
19
+ load_config,
20
+ resolve_path,
21
+ select_device,
22
+ tensor_to_numpy_dict,
23
+ )
24
+
25
+ ensure_onescience_path()
26
+ from scripts.topology_optimization import clear_cached_kernels, predict_fields, share_mean_module
27
+
28
+
29
+ def parse_args():
30
+ parser = argparse.ArgumentParser(description="Run GP_for_TO field inference from a checkpoint.")
31
+ parser.add_argument("--problem", choices=PROBLEMS, default=None)
32
+ parser.add_argument("--gpu", type=int, default=None)
33
+ parser.add_argument("--device", default=None)
34
+ parser.add_argument("--n-col-domain", type=int, default=None)
35
+ parser.add_argument("--n-train-per-bc", type=int, default=None)
36
+ parser.add_argument("--checkpoint-path", default=None)
37
+ parser.add_argument("--output-dir", default=None)
38
+ return parser.parse_args()
39
+
40
+
41
+ def main():
42
+ args = parse_args()
43
+ cfg = load_config()
44
+ if args.problem:
45
+ cfg["problem"] = args.problem
46
+ if args.gpu is not None:
47
+ cfg["inference"]["gpu"] = args.gpu
48
+ if args.device:
49
+ cfg["inference"]["device"] = args.device
50
+ if args.n_col_domain is not None:
51
+ cfg["inference"]["n_col_domain"] = args.n_col_domain
52
+ if args.n_train_per_bc is not None:
53
+ cfg["data"]["n_train_per_bc"] = args.n_train_per_bc
54
+ if args.checkpoint_path:
55
+ cfg["inference"]["checkpoint_path"] = args.checkpoint_path
56
+ if args.output_dir:
57
+ cfg["inference"]["output_dir"] = args.output_dir
58
+
59
+ os.chdir(PROJECT_ROOT)
60
+ device = select_device(cfg["inference"])
61
+ models, metadata = build_models(
62
+ cfg,
63
+ device,
64
+ n_col_domain=cfg["inference"].get("n_col_domain", cfg["data"]["n_col_domain"]),
65
+ n_train_per_bc=cfg["data"]["n_train_per_bc"],
66
+ problem=cfg["problem"],
67
+ )
68
+ checkpoint = load_checkpoint(cfg["inference"]["checkpoint_path"], models, device)
69
+ share_mean_module(models)
70
+ for model in models:
71
+ model.eval()
72
+ clear_cached_kernels(models)
73
+
74
+ with torch.no_grad():
75
+ fields = predict_fields(models)
76
+
77
+ output_dir = resolve_path(cfg["inference"]["output_dir"])
78
+ output_dir.mkdir(parents=True, exist_ok=True)
79
+ npz_path = output_dir / "predictions.npz"
80
+ np.savez(npz_path, **tensor_to_numpy_dict(fields))
81
+
82
+ summary = {
83
+ **metadata,
84
+ "checkpoint_metadata": checkpoint.get("metadata", {}),
85
+ "output_file": str(npz_path),
86
+ "field_shapes": {key: list(value.shape) for key, value in fields.items()},
87
+ }
88
+ (output_dir / "inference_summary.json").write_text(json.dumps(summary, indent=2), encoding="utf-8")
89
+ print(f"Saved predictions to {npz_path}")
90
+ print(json.dumps(summary["field_shapes"], indent=2))
91
+
92
+
93
+ if __name__ == "__main__":
94
+ main()
scripts/result.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import json
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ import matplotlib.pyplot as plt
7
+ import numpy as np
8
+
9
+
10
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
11
+ sys.path.insert(0, str(PROJECT_ROOT))
12
+
13
+ from scripts.common import load_config, resolve_path
14
+
15
+
16
+ def parse_args():
17
+ parser = argparse.ArgumentParser(description="Summarize GP_for_TO inference outputs.")
18
+ parser.add_argument("--output-dir", default=None)
19
+ parser.add_argument("--no-plot", action="store_true")
20
+ return parser.parse_args()
21
+
22
+
23
+ def save_plot(data, output_dir):
24
+ x = data["x"]
25
+ fields = [("u", data["u"]), ("v", data["v"]), ("p", data["p"]), ("ro", data["ro"])]
26
+ fig, axes = plt.subplots(2, 2, figsize=(9, 7))
27
+ for ax, (name, values) in zip(axes.reshape(-1), fields):
28
+ im = ax.tricontourf(x[:, 0], x[:, 1], values, levels=32, cmap="viridis")
29
+ ax.set_title(name)
30
+ ax.set_xlabel("x")
31
+ ax.set_ylabel("y")
32
+ fig.colorbar(im, ax=ax)
33
+ fig.tight_layout()
34
+ path = output_dir / "field_summary.png"
35
+ fig.savefig(path, dpi=160)
36
+ plt.close(fig)
37
+ return path
38
+
39
+
40
+ def main():
41
+ args = parse_args()
42
+ cfg = load_config()
43
+ output_dir = resolve_path(args.output_dir or cfg["inference"]["output_dir"])
44
+ pred_path = output_dir / "predictions.npz"
45
+ summary_path = output_dir / "inference_summary.json"
46
+ if not pred_path.is_file():
47
+ raise FileNotFoundError(f"Missing inference output: {pred_path}")
48
+
49
+ data = np.load(pred_path)
50
+ print(f"Prediction file: {pred_path}")
51
+ for name in ("x", "u", "v", "p", "ro"):
52
+ arr = data[name]
53
+ print(
54
+ f"{name}: shape={arr.shape}, dtype={arr.dtype}, "
55
+ f"min={float(arr.min()):.6e}, max={float(arr.max()):.6e}, mean={float(arr.mean()):.6e}"
56
+ )
57
+
58
+ if summary_path.is_file():
59
+ summary = json.loads(summary_path.read_text(encoding="utf-8"))
60
+ print(f"Problem: {summary.get('problem')}, checkpoint source: {summary.get('checkpoint_metadata', {}).get('problem')}")
61
+
62
+ if not args.no_plot:
63
+ plot_path = save_plot(data, output_dir)
64
+ print(f"Plot: {plot_path}")
65
+
66
+
67
+ if __name__ == "__main__":
68
+ main()
scripts/topology_optimization.py ADDED
@@ -0,0 +1,427 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import math
3
+ import numpy as np
4
+ from gpytorch.settings import cholesky_jitter
5
+ import matplotlib.pyplot as plt
6
+ import torch.optim
7
+ import matplotlib.pyplot as plt
8
+ from tqdm import tqdm
9
+ from onescience.utils.GP_TO import plot_predictions_and_residuals,plot_loss_history,plot_density_and_velocity_fields
10
+ plt.rcParams['font.family'] = 'DejaVu Sans'
11
+ plt.rcParams['font.size'] = 15
12
+ plt.rcParams['figure.dpi'] = 150
13
+
14
+
15
+ checkpoints=[1000,10000,20000,30000,40000,50000]
16
+ lambdaa = 0.1
17
+
18
+ # Determine volume loss based on title
19
+ gamma_values = {
20
+ 'rugby': 0.9,
21
+ 'pipebend': 0.08 * torch.pi,
22
+ 'doublepipe': 1 / 3,
23
+ 'diffuser': 0.5
24
+ }
25
+
26
+ def modified_sigmoid(x, alpha=12.0):
27
+ """Compute the modified sigmoid function."""
28
+ return 1 / (1 + torch.exp(-alpha * (x - 0.5)))
29
+
30
+
31
+ def infer_square_grid(num_points):
32
+ side = int(math.sqrt(num_points))
33
+ if side * side != num_points:
34
+ raise ValueError(
35
+ "Numerical differentiation expects a square collocation grid. "
36
+ f"Got {num_points} points."
37
+ )
38
+ return side, side
39
+
40
+
41
+ def share_mean_module(model_list):
42
+ model_ref = model_list[0]
43
+ for model in model_list:
44
+ model.mean_module_NN_All = model_ref.mean_module_NN_All
45
+ return model_ref
46
+
47
+
48
+ def clear_cached_kernels(model_list):
49
+ for model in model_list:
50
+ model.k_xX = None
51
+ model.chol_decomp = None
52
+
53
+
54
+ def predict_fields(model_list):
55
+ model_ref = share_mean_module(model_list)
56
+ collocation_x = model_ref.collocation_x
57
+ m_col = model_ref.mean_module_NN_All(collocation_x)
58
+
59
+ if model_ref.k_xX is None:
60
+ for model in model_list:
61
+ model.k_xX = model.covar_module(model.train_inputs[0], collocation_x).evaluate()
62
+
63
+ if model_ref.chol_decomp is None:
64
+ with cholesky_jitter(1e-7):
65
+ for model in model_list:
66
+ model.chol_decomp = model.covar_module(model.train_inputs[0]).cholesky()
67
+
68
+ offsets = []
69
+ for i, model in enumerate(model_list):
70
+ target_offset = model.train_targets.unsqueeze(-1) - model.mean_module_NN_All(model.train_inputs[0])[:, i].unsqueeze(-1)
71
+ offsets.append(model.chol_decomp._cholesky_solve(target_offset))
72
+
73
+ u = (m_col[:, 0].unsqueeze(-1) + model_list[0].k_xX.t() @ offsets[0]).squeeze(-1)
74
+ v = (m_col[:, 1].unsqueeze(-1) + model_list[1].k_xX.t() @ offsets[1]).squeeze(-1)
75
+ p = (m_col[:, 2].unsqueeze(-1) + model_list[2].k_xX.t() @ offsets[2]).squeeze(-1)
76
+ ro_tensor = (m_col[:, 3].unsqueeze(-1) + model_list[3].k_xX.t() @ offsets[3]).squeeze(-1)
77
+ ro = modified_sigmoid(ro_tensor)
78
+ return {"x": collocation_x, "u": u, "v": v, "p": p, "ro": ro}
79
+
80
+
81
+ def compute_dynamic_weights_ic(pde_loss, bc_loss, ic_loss, model):
82
+ """
83
+ Compute dynamic weights for loss functions based on gradients.
84
+ """
85
+ params_to_update = [param for param in model.parameters() if param.requires_grad]
86
+
87
+ def compute_gradients(loss, scaling_factor):
88
+ gradients = torch.autograd.grad(scaling_factor * loss, params_to_update, retain_graph=True, allow_unused=True)
89
+ values = [p.reshape(-1).cpu().tolist() for p in gradients if p is not None]
90
+ return torch.abs(torch.tensor([v for val in values for v in val]))
91
+
92
+ delta_pde = compute_gradients(pde_loss, 1.0)
93
+ delta_bc = compute_gradients(bc_loss, model.alpha)
94
+ delta_ic = compute_gradients(ic_loss, model.beta)
95
+
96
+ temp_bc = torch.max(delta_pde) / torch.mean(delta_bc)
97
+ temp_ic = torch.max(delta_pde) / torch.mean(delta_ic)
98
+
99
+ return (
100
+ (1.0 - lambdaa) * model.alpha + lambdaa * temp_bc,
101
+ (1.0 - lambdaa) * model.beta + lambdaa * temp_ic
102
+ )
103
+
104
+ def compute_fvm_residuals_higher_order(u, v, p, f_x, f_y, nu, dx, dy):
105
+ # Check the device of the input tensors and move other tensors to the same device
106
+ device = u.device
107
+
108
+ # Extend the fields with ghost cells to handle boundary conditions
109
+ u_ext = torch.zeros((u.shape[0] + 4, u.shape[1] + 4), device=device)
110
+ v_ext = torch.zeros((v.shape[0] + 4, v.shape[1] + 4), device=device)
111
+ p_ext = torch.zeros((p.shape[0] + 4, p.shape[1] + 4), device=device)
112
+
113
+ # Copy the interior
114
+ u_ext[2:-2, 2:-2] = u
115
+ v_ext[2:-2, 2:-2] = v
116
+ p_ext[2:-2, 2:-2] = p
117
+
118
+ # Apply ghost cells for no-slip boundary conditions
119
+ u_ext[:2, :] = u_ext[2:4, :]
120
+ u_ext[-2:, :] = u_ext[-4:-2, :]
121
+ u_ext[:, :2] = u_ext[:, 2:4]
122
+ u_ext[:, -2:] = u_ext[:, -4:-2]
123
+
124
+ v_ext[:2, :] = v_ext[2:4, :]
125
+ v_ext[-2:, :] = v_ext[-4:-2, :]
126
+ v_ext[:, :2] = v_ext[:, 2:4]
127
+ v_ext[:, -2:] = v_ext[:, -4:-2]
128
+
129
+ # Laplacian of u and v (fourth-order central differences for second derivatives)
130
+ u_xx = (-u_ext[4:, 2:-2] + 16 * u_ext[3:-1, 2:-2] - 30 * u_ext[2:-2, 2:-2] + 16 * u_ext[1:-3, 2:-2] - u_ext[:-4, 2:-2]) / (12 * dx**2)
131
+ u_yy = (-u_ext[2:-2, 4:] + 16 * u_ext[2:-2, 3:-1] - 30 * u_ext[2:-2, 2:-2] + 16 * u_ext[2:-2, 1:-3] - u_ext[2:-2, :-4]) / (12 * dy**2)
132
+ laplacian_u = u_xx + u_yy
133
+
134
+ v_xx = (-v_ext[4:, 2:-2] + 16 * v_ext[3:-1, 2:-2] - 30 * v_ext[2:-2, 2:-2] + 16 * v_ext[1:-3, 2:-2] - v_ext[:-4, 2:-2]) / (12 * dx**2)
135
+ v_yy = (-v_ext[2:-2, 4:] + 16 * v_ext[2:-2, 3:-1] - 30 * v_ext[2:-2, 2:-2] + 16 * v_ext[2:-2, 1:-3] - v_ext[2:-2, :-4]) / (12 * dy**2)
136
+ laplacian_v = v_xx + v_yy
137
+
138
+ # Gradients of p using fourth-order central difference
139
+ grad_p_x = (-p_ext[4:, 2:-2] + 8 * p_ext[3:-1, 2:-2] - 8 * p_ext[1:-3, 2:-2] + p_ext[:-4, 2:-2]) / (12 * dx)
140
+ grad_p_y = (-p_ext[2:-2, 4:] + 8 * p_ext[2:-2, 3:-1] - 8 * p_ext[2:-2, 1:-3] + p_ext[2:-2, :-4]) / (12 * dy)
141
+
142
+ # Momentum equation residuals
143
+ residual_u = -nu * laplacian_u + grad_p_x + f_x
144
+ residual_v = -nu * laplacian_v + grad_p_y + f_y
145
+
146
+ # Mass conservation (divergence of velocity) using central differences
147
+ div_u = (u_ext[2:-2, 2:-2] - u_ext[1:-3, 2:-2]) / dx
148
+ div_v = (v_ext[2:-2, 2:-2] - v_ext[2:-2, 1:-3]) / dy
149
+
150
+ u_y = (u_ext[2:-2, 2:-2] - u_ext[2:-2, 1:-3]) / dy
151
+ v_x = (v_ext[2:-2, 2:-2] - v_ext[1:-3, 2:-2]) / dx
152
+ residual_mass = div_u + div_v
153
+
154
+ return residual_u.reshape(-1), residual_v.reshape(-1), residual_mass.reshape(-1), div_u.reshape(-1), div_v.reshape(-1), u_y.reshape(-1), v_x.reshape(-1)
155
+
156
+
157
+
158
+ def compute_autograd_derivatives(model, z_column, collocation_x, grad_order=1):
159
+ """
160
+ Compute first and second-order derivatives using PyTorch autograd.
161
+ :param model: Model to train
162
+ :param z_column: Column of `z_all` for differentiation
163
+ :param collocation_x: Input coordinates
164
+ :param grad_order: Order of gradients (1 for first, 2 for second)
165
+ :return: First and second-order derivatives as tensors
166
+ """
167
+ model.train()
168
+ grad_1 = torch.autograd.grad(z_column, collocation_x, torch.ones_like(z_column), create_graph=True)[0]
169
+ if grad_order > 1:
170
+ grad_2_x = torch.autograd.grad(grad_1[:, 0], collocation_x, torch.ones_like(grad_1[:, 0]), create_graph=True)[0][:, 0]
171
+ grad_2_y = torch.autograd.grad(grad_1[:, 1], collocation_x, torch.ones_like(grad_1[:, 1]), create_graph=True)[0][:, 1]
172
+ return grad_1[:, 0], grad_1[:, 1], grad_2_x, grad_2_y
173
+ return grad_1[:, 0], grad_1[:, 1], None, None
174
+
175
+ w_1,w_2,w_3, w_4, w_5 =0.01,0.01,1e2,1e5,1e4
176
+
177
+ def loss_volume(y, gamma=0.5):
178
+ """
179
+ Compute the volume loss as the squared difference between the mean of y and gamma.
180
+ """
181
+ mean_y = torch.mean(y)
182
+ return torch.square(mean_y - gamma)
183
+
184
+
185
+ def dissipated_power(u, u_x, v_y, u_y, v_x):
186
+ """
187
+ Compute the total dissipated power based on the input velocity gradients and displacements.
188
+ """
189
+ # First part of the dissipated power
190
+ p1 = (u_x**2 + v_y**2 + u_y**2 + v_x**2).sum(dim=1, keepdim=True)
191
+
192
+ # Second part of the dissipated power
193
+ u2 = (u[:, :2]**2).sum(dim=1, keepdim=True)
194
+ p2 = alpha(u[:, 3:]) * u2
195
+
196
+ return 0.5 * (p1 + p2)
197
+
198
+
199
+ def alpha(rho):
200
+ """
201
+ Compute the alpha parameter based on rho.
202
+ """
203
+ alpha_max = 2.5e4
204
+ alpha_min = 2.5e-4
205
+ q = 0.1
206
+ return alpha_max + (alpha_min - alpha_max) * rho * (1 + q) / (rho + q)
207
+
208
+ def calculate_loss_multioutput(
209
+ model_list,
210
+ iteration,
211
+ diff_method='Numerical',
212
+ title="default",
213
+ problem="doublepipe",
214
+ checkpoint_steps=None,
215
+ plot_outputs=True,
216
+ ):
217
+ """
218
+ Calculate loss for multi-output models based on PDE, BC, and IC residuals.
219
+ """
220
+ # Clone collocation points
221
+ collocation_x = model_list[0].collocation_x.clone()
222
+
223
+ # Compute mean values at collocation points
224
+ m_col = model_list[0].mean_module_NN_All(collocation_x)
225
+
226
+ # Evaluate covariance matrices if not already done
227
+ if model_list[0].k_xX is None:
228
+ for i, model in enumerate(model_list):
229
+ model.k_xX = model.covar_module(model.train_inputs[0], collocation_x).evaluate()
230
+
231
+ # Perform Cholesky decomposition if not already done
232
+ if model_list[0].chol_decomp is None:
233
+ with cholesky_jitter(1e-7):
234
+ for model in model_list:
235
+ model.chol_decomp = model.covar_module(model.train_inputs[0]).cholesky()
236
+
237
+ # Solve for offsets using Cholesky decomposition
238
+ offsets = []
239
+ for i, model in enumerate(model_list):
240
+ target_offset = model.train_targets.unsqueeze(-1) - model.mean_module_NN_All(model.train_inputs[0])[:, i].unsqueeze(-1)
241
+ offsets.append(model.chol_decomp._cholesky_solve(target_offset))
242
+
243
+ # Compute values for velocity, pressure, and density
244
+ ro_tensor = (m_col[:, 3].unsqueeze(-1) + model_list[3].k_xX.t() @ offsets[3]).squeeze(-1)
245
+ ro = modified_sigmoid(ro_tensor)
246
+
247
+ u = (m_col[:, 0].unsqueeze(-1) + model_list[0].k_xX.t() @ offsets[0]).squeeze(-1)
248
+ v = (m_col[:, 1].unsqueeze(-1) + model_list[1].k_xX.t() @ offsets[1]).squeeze(-1)
249
+ p = (m_col[:, 2].unsqueeze(-1) + model_list[2].k_xX.t() @ offsets[2]).squeeze(-1)
250
+
251
+ z_all = torch.cat((u.unsqueeze(1), v.unsqueeze(1), p.unsqueeze(1), ro.unsqueeze(1)), dim=1)
252
+ f = alpha(z_all[:, 3:]) * z_all[:, :2]
253
+ fx, fy = f[:, :1], f[:, 1:]
254
+
255
+ # Compute residuals
256
+ if diff_method == 'Numerical':
257
+ nx, ny = infer_square_grid(collocation_x.shape[0])
258
+ dx, dy = 1.0 / nx, 1.0 / ny
259
+ u, v, p = [arr.reshape(nx, ny) for arr in (u, v, p)]
260
+ fx, fy = [arr.reshape(nx, ny) for arr in (fx, fy)]
261
+ residuals = compute_fvm_residuals_higher_order(u, v, p, fx, fy, nu=1, dx=dx, dy=dy)
262
+ residual_pde1, residual_pde2, residual_pde3, u_x, v_y, u_y, v_x = residuals
263
+ residual_pde1, residual_pde2, residual_pde3 = [res * w for res, w in zip(residuals[:3], (w_1, w_2, w_3))]
264
+ u, v, p = [arr.reshape(-1) for arr in (u, v, p)]
265
+ fx, fy = [arr.reshape(-1) for arr in (fx, fy)]
266
+ else:
267
+ u_x, u_y, u_xx, u_yy = compute_autograd_derivatives(model_list[0], z_all[:, 0], collocation_x, grad_order=2)
268
+ v_x, v_y, v_xx, v_yy = compute_autograd_derivatives(model_list[1], z_all[:, 1], collocation_x, grad_order=2)
269
+ p_x, p_y, _, _ = compute_autograd_derivatives(model_list[2], z_all[:, 2], collocation_x, grad_order=1)
270
+ residual_pde1 = (- (u_xx + u_yy) + p_x + fx[:, 0]) * w_1
271
+ residual_pde2 = (- (v_xx + v_yy) + p_y + fy[:, 0]) * w_2
272
+ residual_pde3 = (u_x + v_y) * w_3
273
+
274
+ # Plot fields at checkpoints
275
+ active_checkpoints = checkpoints if checkpoint_steps is None else checkpoint_steps
276
+ if plot_outputs and iteration in active_checkpoints:
277
+ plot_density_and_velocity_fields(u, v, p, ro, collocation_x, iteration, problem)
278
+ plot_predictions_and_residuals(u, v, p, ro, collocation_x, iteration, residual_pde1, residual_pde2, residual_pde3, 1, 1, 1, problem)
279
+ # Calculate losses
280
+ loss_pde1 = torch.mean(residual_pde1**2)
281
+ loss_pde2 = torch.mean(residual_pde2**2)
282
+ loss_pde3 = torch.mean(residual_pde3**2)
283
+ dp_loss = dissipated_power(z_all, u_x.reshape(-1, 1), v_y.reshape(-1, 1), u_y.reshape(-1, 1), v_x.reshape(-1, 1))
284
+
285
+ gamma = next((val for key, val in gamma_values.items() if key in title), 0.5)
286
+ vol_loss = loss_volume(z_all[:, 3:], gamma=gamma)* w_4
287
+
288
+ # Return final losses
289
+ return loss_pde1, loss_pde2, loss_pde3, torch.sum(dp_loss), vol_loss
290
+
291
+
292
+
293
+
294
+ def find_TO(
295
+ model_list,
296
+ lr_default: float = 0.01,
297
+ num_iter: int = 500,
298
+ title: str = 'default',
299
+ problem: str = 'doublepipe', # 设置默认值
300
+ diff_method: str = 'Numerical',
301
+ checkpoint_steps=None,
302
+ plot_outputs=True,
303
+ ) -> float:
304
+ """
305
+ Train models to minimize total loss using dynamic weights and record progress.
306
+
307
+ Args:
308
+ model_list: List of models to be trained.
309
+ lr_default: Default learning rate for the optimizer.
310
+ num_iter: Number of training iterations.
311
+ title: Title for specific settings.
312
+ diff_method: Differentiation method ('Numerical' or 'Autograd').
313
+
314
+ Returns:
315
+ loss history
316
+ """
317
+ # Use the first model as a reference for shared NN
318
+ model_ref = share_mean_module(model_list)
319
+ for model in model_list:
320
+ model.train()
321
+
322
+ # Initialize variables
323
+ loss_total, loss_hist, loss_hist_total = [], [], []
324
+ loss_pde1_hist, loss_pde2_hist, loss_pde3_hist = [], [], []
325
+ loss_dp_hist, vol_loss_hist = [], []
326
+ scaled_loss_pde1_hist, scaled_loss_pde2_hist = [], []
327
+ scaled_loss_pde3_hist, scaled_loss_dp_hist, scaled_vol_loss_hist = [], [], []
328
+ weights = {'alpha': [], 'beta': [], 'mu_p': []}
329
+ result_loss_thetas = {'largest_eigval_hist': [], 'condition_number_hist': [], 'Hessian': [], 'grads': []}
330
+ GP_NN_hist, NN_hist, time_hist = [], [], []
331
+ dynamic_weights = True
332
+ sigma_1, beta_1, alph_1 = 10, 10, 0.1
333
+ f_inc, mu_F = math.inf, 1
334
+
335
+ # Initialize model-specific parameters
336
+ model1 = model_list[0]
337
+ model1.alpha, model1.beta, model1.theta = 1, 1, 1
338
+
339
+ # Set up optimizer and scheduler
340
+ optimizer = torch.optim.Adam(model_ref.parameters(), lr=lr_default)
341
+ milestones = sorted({int(x) for x in np.linspace(0, max(num_iter, 1), 4).tolist() if int(x) > 0})
342
+ scheduler = torch.optim.lr_scheduler.MultiStepLR(
343
+ optimizer,
344
+ milestones=milestones,
345
+ gamma=0.75
346
+ )
347
+
348
+ # Training loop
349
+ with tqdm(range(num_iter + 1), desc='Epoch', position=0, leave=True) as pbar:
350
+ for j in pbar:
351
+ optimizer.zero_grad()
352
+
353
+ # Calculate losses
354
+ loss_pde1, loss_pde2, loss_pde3, loss_dp, vol_loss = calculate_loss_multioutput(
355
+ model_list,
356
+ j,
357
+ diff_method=diff_method,
358
+ title=title,
359
+ problem=problem,
360
+ checkpoint_steps=checkpoint_steps,
361
+ plot_outputs=plot_outputs,
362
+ )
363
+ loss_pde = loss_pde1 + loss_pde3
364
+
365
+ # Dynamic weight adjustments
366
+ if dynamic_weights:
367
+ alpha, beta = compute_dynamic_weights_ic(
368
+ loss_pde1 + loss_pde2, loss_pde3, vol_loss, model1
369
+ )
370
+ if all(torch.is_tensor(val) and not (torch.isnan(val).any() or torch.isinf(val).any()) for val in [alpha, beta]):
371
+ model1.alpha, model1.beta = alpha, beta
372
+
373
+ weights['alpha'].append(alpha.detach().cpu().item())
374
+ weights['beta'].append(beta.detach().cpu().item())
375
+ weights['mu_p'].append(mu_F)
376
+
377
+ loss = loss_dp + mu_F * (
378
+ loss_pde1 + loss_pde2 + model1.alpha * loss_pde3 + model1.beta * vol_loss
379
+ )
380
+
381
+ if (j + 1) % 50 == 0:
382
+ mu_F = min(mu_F * 1.05, 5e2)
383
+ else:
384
+ alph_1 = 1e-6 / (j + 1)
385
+ beta_1, sigma_1 = 2 * (j + 1), 5 * (j + 1)
386
+ loss = sigma_1 * loss_pde + alph_1 * loss_dp + beta_1 * vol_loss
387
+
388
+ # Record loss history
389
+ loss_total.append(loss.item())
390
+ loss_pde1_hist.append((1/w_1) * loss_pde1.item())
391
+ loss_pde2_hist.append((1/w_2) * loss_pde2.item())
392
+ loss_pde3_hist.append((1/w_3) * loss_pde3.item())
393
+ loss_dp_hist.append((1/w_5) * loss_dp.item())
394
+ vol_loss_hist.append((1/w_4) * vol_loss.item())
395
+ scaled_loss_pde1_hist.append(loss_pde1.item())
396
+ scaled_loss_pde2_hist.append(loss_pde2.item())
397
+ scaled_loss_pde3_hist.append(model1.alpha * loss_pde3.item())
398
+ scaled_loss_dp_hist.append(loss_dp.item())
399
+ scaled_vol_loss_hist.append(model1.beta * vol_loss.item())
400
+
401
+ # Plot loss history at checkpoints
402
+ active_checkpoints = checkpoints if checkpoint_steps is None else checkpoint_steps
403
+ if plot_outputs and j in active_checkpoints:
404
+ plot_loss_history(
405
+ loss_total,
406
+ [loss_pde1_hist, scaled_loss_pde1_hist],
407
+ [loss_pde2_hist, scaled_loss_pde2_hist],
408
+ [loss_pde3_hist, scaled_loss_pde3_hist],
409
+ [loss_dp_hist, scaled_loss_dp_hist],
410
+ [vol_loss_hist, scaled_vol_loss_hist],
411
+ j,
412
+ problem
413
+ )
414
+
415
+ # Backpropagation and optimizer step
416
+ loss.backward(retain_graph=True)
417
+ optimizer.step()
418
+ scheduler.step()
419
+
420
+ # Update progress description
421
+ pbar.set_postfix(loss=f"{loss.item():.6f}")
422
+
423
+ loss_hist.append(loss.item())
424
+
425
+ loss_hist_total = loss_hist
426
+
427
+ return loss_hist_total
scripts/train.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import os
3
+ import sys
4
+ import time
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+
8
+ import numpy as np
9
+
10
+
11
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
12
+ sys.path.insert(0, str(PROJECT_ROOT))
13
+
14
+ from scripts.common import (
15
+ PROBLEMS,
16
+ build_models,
17
+ dump_json,
18
+ ensure_onescience_path,
19
+ load_config,
20
+ resolve_path,
21
+ save_checkpoint,
22
+ select_device,
23
+ )
24
+
25
+ ensure_onescience_path()
26
+ from scripts.topology_optimization import find_TO
27
+
28
+
29
+ def parse_args():
30
+ parser = argparse.ArgumentParser(description="Train GP_for_TO topology optimization models.")
31
+ parser.add_argument("--problem", choices=PROBLEMS, default=None)
32
+ parser.add_argument("--gpu", type=int, default=None)
33
+ parser.add_argument("--device", default=None)
34
+ parser.add_argument("--num-iter", type=int, default=None)
35
+ parser.add_argument("--n-col-domain", type=int, default=None)
36
+ parser.add_argument("--n-train-per-bc", type=int, default=None)
37
+ parser.add_argument("--diff-method", choices=("Numerical", "Autograd"), default=None)
38
+ parser.add_argument("--lr", type=float, default=None)
39
+ parser.add_argument("--checkpoint-path", default=None)
40
+ parser.add_argument("--output-dir", default=None)
41
+ parser.add_argument("--no-plot", action="store_true")
42
+ return parser.parse_args()
43
+
44
+
45
+ def main():
46
+ args = parse_args()
47
+ cfg = load_config()
48
+
49
+ if args.problem:
50
+ cfg["problem"] = args.problem
51
+ if args.gpu is not None:
52
+ cfg["training"]["gpu"] = args.gpu
53
+ if args.device:
54
+ cfg["training"]["device"] = args.device
55
+ if args.num_iter is not None:
56
+ cfg["training"]["num_iter"] = args.num_iter
57
+ if args.n_col_domain is not None:
58
+ cfg["data"]["n_col_domain"] = args.n_col_domain
59
+ if args.n_train_per_bc is not None:
60
+ cfg["data"]["n_train_per_bc"] = args.n_train_per_bc
61
+ if args.diff_method:
62
+ cfg["training"]["diff_method"] = args.diff_method
63
+ if args.lr is not None:
64
+ cfg["training"]["lr_default"] = args.lr
65
+ if args.checkpoint_path:
66
+ cfg["training"]["checkpoint_path"] = args.checkpoint_path
67
+ if args.output_dir:
68
+ cfg["training"]["output_dir"] = args.output_dir
69
+ if args.no_plot:
70
+ cfg["training"]["plot_outputs"] = False
71
+
72
+ os.chdir(PROJECT_ROOT)
73
+ device = select_device(cfg["training"])
74
+ models, metadata = build_models(
75
+ cfg,
76
+ device,
77
+ n_col_domain=cfg["data"]["n_col_domain"],
78
+ n_train_per_bc=cfg["data"]["n_train_per_bc"],
79
+ problem=cfg["problem"],
80
+ )
81
+
82
+ title = f"seed{cfg['seed']}_{cfg['problem']}_{datetime.now().strftime('%B%d_%H-%M')}"
83
+ start_time = time.time()
84
+ loss_history = find_TO(
85
+ model_list=models,
86
+ num_iter=int(cfg["training"]["num_iter"]),
87
+ lr_default=float(cfg["training"]["lr_default"]),
88
+ title=title,
89
+ problem=cfg["problem"],
90
+ diff_method=cfg["training"]["diff_method"],
91
+ checkpoint_steps=list(cfg["training"].get("checkpoints", [])),
92
+ plot_outputs=bool(cfg["training"].get("plot_outputs", True)),
93
+ )
94
+ elapsed = time.time() - start_time
95
+
96
+ output_dir = resolve_path(cfg["training"]["output_dir"])
97
+ output_dir.mkdir(parents=True, exist_ok=True)
98
+ np.save(output_dir / "loss_history.npy", np.asarray(loss_history, dtype=np.float64))
99
+ summary = {
100
+ **metadata,
101
+ "device": str(device),
102
+ "num_iter": int(cfg["training"]["num_iter"]),
103
+ "diff_method": cfg["training"]["diff_method"],
104
+ "elapsed_seconds": elapsed,
105
+ "final_loss": float(loss_history[-1]) if loss_history else None,
106
+ }
107
+ dump_json(summary, output_dir / "training_summary.json")
108
+ ckpt_path = save_checkpoint(cfg["training"]["checkpoint_path"], models, cfg, summary, loss_history)
109
+
110
+ print(f"Training finished in {elapsed:.2f}s")
111
+ print(f"Loss history: {output_dir / 'loss_history.npy'}")
112
+ print(f"Checkpoint: {ckpt_path}")
113
+
114
+
115
+ if __name__ == "__main__":
116
+ main()
weight/.gitkeep ADDED
@@ -0,0 +1 @@
 
 
1
+