1
+ import numpy as np
2
+ np .random .seed (206 )
3
+
4
+ import yaml
5
+ import scipy
6
+ import pandas as pd
7
+ import matplotlib .pyplot as plt
8
+ from sklearn .cluster import KMeans
9
+
10
+ import theano
11
+ import theano .tensor as tt
12
+ import pymc3 as pm
13
+ from pymc3 .gp .cov import Covariance
14
+ import scipy .stats as st
15
+ import random
16
+
17
+ class MultiMarginal (pm .gp .gp .Base ):
18
+ R"""
19
+ MultiMarginal Gaussian process.
20
+ The `MultiMarginal` class is an implementation of the sum of a GP
21
+ prior and additive noise. It has `marginal_likelihood`, `conditional`
22
+ and `predict` methods. This GP implementation can be used to
23
+ implement regression on data that is normally distributed. For more
24
+ information on the `prior` and `conditional` methods, see their docstrings.
25
+ Parameters
26
+ ----------
27
+ cov_func: None, 2D array, or instance of Covariance
28
+ The covariance function. Defaults to zero.
29
+ mean_func: None, instance of Mean
30
+ The mean function. Defaults to zero.
31
+ Examples
32
+ --------
33
+ .. code:: python
34
+ # A one dimensional column vector of inputs.
35
+ X = np.linspace(0, 1, 10)[:, None]
36
+ with pm.Model() as model:
37
+ # Specify the covariance function.
38
+ cov_func = pm.gp.cov.ExpQuad(1, ls=0.1)
39
+ # Specify the GP. The default mean function is `Zero`.
40
+ gp = pm.gp.Marginal(cov_func=cov_func)
41
+ # Place a GP prior over the function f.
42
+ sigma = pm.HalfCauchy("sigma", beta=3)
43
+ y_ = gp.marginal_likelihood("y", X=X, y=y, noise=sigma)
44
+ ...
45
+ # After fitting or sampling, specify the distribution
46
+ # at new points with .conditional
47
+ Xnew = np.linspace(-1, 2, 50)[:, None]
48
+ with model:
49
+ fcond = gp.conditional("fcond", Xnew=Xnew)
50
+ """
51
+
52
+ def _build_marginal_likelihood (self , X , y , noise ):
53
+ mu = tt .zeros_like (y ) # self.mean_func(X)
54
+ Kxx = self .cov_func (X )
55
+ Knx = noise (X )
56
+ cov = Kxx + Knx
57
+ return mu , cov
58
+
59
+ def marginal_likelihood (self , name , X , y , colchol , noise , matrix_shape , is_observed = True , ** kwargs ):
60
+ R"""
61
+ Returns the marginal likelihood distribution, given the input
62
+ locations `X` and the data `y`.
63
+ This is integral over the product of the GP prior and a normal likelihood.
64
+ .. math::
65
+ y \mid X,\theta \sim \int p(y \mid f,\, X,\, \theta) \, p(f \mid X,\, \theta) \, df
66
+ Parameters
67
+ ----------
68
+ name: string
69
+ Name of the random variable
70
+ X: array-like
71
+ Function input values. If one-dimensional, must be a column
72
+ vector with shape `(n, 1)`.
73
+ y: array-like
74
+ Data that is the sum of the function with the GP prior and Gaussian
75
+ noise. Must have shape `(n, )`.
76
+ noise: scalar, Variable, or Covariance
77
+ Standard deviation of the Gaussian noise. Can also be a Covariance for
78
+ non-white noise.
79
+ is_observed: bool
80
+ Whether to set `y` as an `observed` variable in the `model`.
81
+ Default is `True`.
82
+ **kwargs
83
+ Extra keyword arguments that are passed to `MvNormal` distribution
84
+ constructor.
85
+ """
86
+
87
+ if not isinstance (noise , Covariance ):
88
+ noise = pm .gp .cov .WhiteNoise (noise )
89
+ mu , cov = self ._build_marginal_likelihood (X , y , noise )
90
+ self .X = X
91
+ self .y = y
92
+ self .noise = noise
93
+
94
+ # Warning: the shape of y is hardcode
95
+
96
+ if is_observed :
97
+ return pm .MatrixNormal (name , mu = mu , colchol = colchol , rowcov = cov , observed = y , shape = (matrix_shape [0 ],matrix_shape [1 ]), ** kwargs )
98
+ else :
99
+ shape = infer_shape (X , kwargs .pop ("shape" , None ))
100
+ return pm .MvNormal (name , mu = mu , cov = cov , shape = shape , ** kwargs )
101
+
102
+ def _get_given_vals (self , given ):
103
+ if given is None :
104
+ given = {}
105
+
106
+ if 'gp' in given :
107
+ cov_total = given ['gp' ].cov_func
108
+ mean_total = given ['gp' ].mean_func
109
+ else :
110
+ cov_total = self .cov_func
111
+ mean_total = self .mean_func
112
+ if all (val in given for val in ['X' , 'y' , 'noise' ]):
113
+ X , y , noise = given ['X' ], given ['y' ], given ['noise' ]
114
+ if not isinstance (noise , Covariance ):
115
+ noise = pm .gp .cov .WhiteNoise (noise )
116
+ else :
117
+ X , y , noise = self .X , self .y , self .noise
118
+ return X , y , noise , cov_total , mean_total
119
+
120
+ def _build_conditional (self , Xnew , pred_noise , diag , X , y , noise ,
121
+ cov_total , mean_total ):
122
+ Kxx = cov_total (X )
123
+ Kxs = self .cov_func (X , Xnew )
124
+ Knx = noise (X )
125
+ rxx = y - mean_total (X )
126
+ L = cholesky (stabilize (Kxx ) + Knx )
127
+ A = solve_lower (L , Kxs )
128
+ v = solve_lower (L , rxx )
129
+ mu = self .mean_func (Xnew ) + tt .dot (tt .transpose (A ), v )
130
+ if diag :
131
+ Kss = self .cov_func (Xnew , diag = True )
132
+ var = Kss - tt .sum (tt .square (A ), 0 )
133
+ if pred_noise :
134
+ var += noise (Xnew , diag = True )
135
+ return mu , var
136
+ else :
137
+ Kss = self .cov_func (Xnew )
138
+ cov = Kss - tt .dot (tt .transpose (A ), A )
139
+ if pred_noise :
140
+ cov += noise (Xnew )
141
+ return mu , cov if pred_noise else stabilize (cov )
142
+
143
+ def conditional (self , name , Xnew , pred_noise = False , given = None , ** kwargs ):
144
+ R"""
145
+ Returns the conditional distribution evaluated over new input
146
+ locations `Xnew`.
147
+ Given a set of function values `f` that the GP prior was over, the
148
+ conditional distribution over a set of new points, `f_*` is:
149
+ .. math::
150
+ f_* \mid f, X, X_* \sim \mathcal{GP}\left(
151
+ K(X_*, X) [K(X, X) + K_{n}(X, X)]^{-1} f \,,
152
+ K(X_*, X_*) - K(X_*, X) [K(X, X) + K_{n}(X, X)]^{-1} K(X, X_*) \right)
153
+ Parameters
154
+ ----------
155
+ name: string
156
+ Name of the random variable
157
+ Xnew: array-like
158
+ Function input values. If one-dimensional, must be a column
159
+ vector with shape `(n, 1)`.
160
+ pred_noise: bool
161
+ Whether or not observation noise is included in the conditional.
162
+ Default is `False`.
163
+ given: dict
164
+ Can optionally take as key value pairs: `X`, `y`, `noise`,
165
+ and `gp`. See the section in the documentation on additive GP
166
+ models in PyMC3 for more information.
167
+ **kwargs
168
+ Extra keyword arguments that are passed to `MvNormal` distribution
169
+ constructor.
170
+ """
171
+
172
+ givens = self ._get_given_vals (given )
173
+ mu , cov = self ._build_conditional (Xnew , pred_noise , False , * givens )
174
+ shape = infer_shape (Xnew , kwargs .pop ("shape" , None ))
175
+ return pm .MvNormal (name , mu = mu , cov = cov , shape = shape , ** kwargs )
176
+
177
+ def predict (self , Xnew , point = None , diag = False , pred_noise = False , given = None ):
178
+ R"""
179
+ Return the mean vector and covariance matrix of the conditional
180
+ distribution as numpy arrays, given a `point`, such as the MAP
181
+ estimate or a sample from a `trace`.
182
+ Parameters
183
+ ----------
184
+ Xnew: array-like
185
+ Function input values. If one-dimensional, must be a column
186
+ vector with shape `(n, 1)`.
187
+ point: pymc3.model.Point
188
+ A specific point to condition on.
189
+ diag: bool
190
+ If `True`, return the diagonal instead of the full covariance
191
+ matrix. Default is `False`.
192
+ pred_noise: bool
193
+ Whether or not observation noise is included in the conditional.
194
+ Default is `False`.
195
+ given: dict
196
+ Same as `conditional` method.
197
+ """
198
+ if given is None :
199
+ given = {}
200
+
201
+ mu , cov = self .predictt (Xnew , diag , pred_noise , given )
202
+ return draw_values ([mu , cov ], point = point )
203
+
204
+ def predictt (self , Xnew , diag = False , pred_noise = False , given = None ):
205
+ R"""
206
+ Return the mean vector and covariance matrix of the conditional
207
+ distribution as symbolic variables.
208
+ Parameters
209
+ ----------
210
+ Xnew: array-like
211
+ Function input values. If one-dimensional, must be a column
212
+ vector with shape `(n, 1)`.
213
+ diag: bool
214
+ If `True`, return the diagonal instead of the full covariance
215
+ matrix. Default is `False`.
216
+ pred_noise: bool
217
+ Whether or not observation noise is included in the conditional.
218
+ Default is `False`.
219
+ given: dict
220
+ Same as `conditional` method.
221
+ """
222
+ givens = self ._get_given_vals (given )
223
+ mu , cov = self ._build_conditional (Xnew , pred_noise , diag , * givens )
224
+ return mu , cov
225
+
226
+
227
+ # Function for Bayesian calibration
228
+ def MultiOutput_Bayesian_Calibration (n_y ,DataComp ,DataField ,DataPred ,output_folder ):
229
+ # Data preprocessing
230
+ n = np .shape (DataField )[0 ] # number of measured data
231
+ m = np .shape (DataComp )[0 ] # number of simulation data
232
+
233
+ p = np .shape (DataField )[1 ] - n_y # number of input x
234
+ q = np .shape (DataComp )[1 ] - p - n_y # number of calibration parameters t
235
+
236
+ xc = DataComp [:,n_y :] # simulation input x + calibration parameters t
237
+ xf = DataField [:,n_y :] # observed input
238
+
239
+ yc = DataComp [:,:n_y ] # simulation output
240
+ yf = DataField [:,:n_y ] # observed output
241
+
242
+ x_pred = DataPred [:,n_y :] # designed points for predictions
243
+ y_true = DataPred [:,:n_y ] # true values for designed points for predictions
244
+ n_pred = np .shape (x_pred )[0 ] # number of predictions
245
+ N = n + m + n_pred
246
+
247
+ # Put points xc, xf, and x_pred on [0,1]
248
+ for i in range (p ):
249
+ x_min = min (min (xc [:,i ]),min (xf [:,i ]))
250
+ x_max = max (max (xc [:,i ]),max (xf [:,i ]))
251
+ xc [:,i ] = (xc [:,i ]- x_min )/ (x_max - x_min )
252
+ xf [:,i ] = (xf [:,i ]- x_min )/ (x_max - x_min )
253
+ x_pred [:,i ] = (x_pred [:,i ]- x_min )/ (x_max - x_min )
254
+
255
+ # Put calibration parameters t on domain [0,1]
256
+ for i in range (p ,(p + q )):
257
+ t_min = min (xc [:,i ])
258
+ t_max = max (xc [:,i ])
259
+ xc [:,i ] = (xc [:,i ]- t_min )/ (t_max - t_min )
260
+
261
+ # Store mean and std of yc for future use of scaling back
262
+ yc_mean = np .zeros (n_y )
263
+ yc_sd = np .zeros (n_y )
264
+
265
+ # Standardization of output yf and yc
266
+ for i in range (n_y ):
267
+ yc_mean [i ] = np .mean (yc [:,i ])
268
+ yc_sd [i ] = np .std (yc [:,i ])
269
+ yc [:,i ] = (yc [:,i ]- yc_mean [i ])/ yc_sd [i ]
270
+ yf [:,i ] = (yf [:,i ]- yc_mean [i ])/ yc_sd [i ]
271
+
272
+ # PyMC3 modelling
273
+ with pm .Model () as model :
274
+ # Set priors
275
+ eta1 = pm .HalfCauchy ("eta1" , beta = 5 ) # Set eta of gaussian process
276
+ lengthscale = pm .Gamma ("lengthscale" , alpha = 2 , beta = 1 , shape = (p + q )) # Set lengthscale of gaussian process
277
+ tf = pm .Beta ("tf" , alpha = 2 , beta = 2 , shape = q ) # Set for calibration parameters
278
+ sigma1 = pm .HalfCauchy ('sigma1' , beta = 5 ) # Set for noise
279
+ y_pred = pm .Normal ('y_pred' , 0 , 1.5 , shape = (n_pred ,n_y )) # y prediction
280
+
281
+ # Setup prior of right cholesky matrix
282
+ sd_dist = pm .HalfCauchy .dist (beta = 2.5 , shape = n_y )
283
+ colchol_packed = pm .LKJCholeskyCov ('colcholpacked' , n = n_y , eta = 2 ,sd_dist = sd_dist )
284
+ colchol = pm .expand_packed_triangular (n_y , colchol_packed )
285
+
286
+ # Concatenate data into a big matrix[[xf tf], [xc tc], [x_pred tf]]
287
+ xf1 = tt .concatenate ([xf , tt .fill (tt .zeros ([n ,q ]), tf )], axis = 1 )
288
+ x_pred1 = tt .concatenate ([x_pred , tt .fill (tt .zeros ([n_pred ,q ]), tf )], axis = 1 )
289
+ X = tt .concatenate ([xf1 , xc , x_pred1 ], axis = 0 )
290
+ # Concate data into a big matrix[[yf], [yc], [y_pred]]
291
+ y = tt .concatenate ([yf , yc , y_pred ], axis = 0 )
292
+
293
+ # Covariance funciton of gaussian process
294
+ cov_z = eta1 ** 2 * pm .gp .cov .ExpQuad ((p + q ), ls = lengthscale )
295
+ # Gaussian process with covariance funciton of cov_z
296
+ gp = MultiMarginal (cov_func = cov_z )
297
+
298
+ # Bayesian inference
299
+ matrix_shape = [n + m + n_pred ,n_y ]
300
+ outcome = gp .marginal_likelihood ("outcome" , X = X , y = y , colchol = colchol , noise = sigma1 , matrix_shape = matrix_shape )
301
+ trace = pm .sample (n_sample ,cores = 1 ) # Put the number of samples you designed to n_sample such as 250 or 500, etc.
302
+
303
+ # Data collection and visualization
304
+ pm .summary (trace ).to_csv (output_folder + '/trace_summary_{}.csv' .format (case_name ))
305
+ pd .DataFrame (np .array (trace ['tf' ])).to_csv (output_folder + '/tf__{}.csv' .format (case_name ))
306
+
307
+ name_columns = []
308
+ n_columns = n_pred
309
+ for i in range (n_columns ):
310
+ for j in range (n_y ):
311
+ name_columns .append ('y' + str (j + 1 )+ '_pred' + str (i + 1 ))
312
+ y_prediction = pd .DataFrame (np .array (trace ['y_pred' ]).reshape (n_trace ,n_pred * n_y ),columns = name_columns ) # Put the number of trace to n_trace based on the above trace results
313
+
314
+ # Store predictions
315
+ for i in range (n_y ):
316
+ index = list (range (0 + i ,n_pred * n_y + i ,n_y ))
317
+ y_prediction1 = pd .DataFrame (y_prediction .iloc [:,index ])
318
+ y_prediction1 = y_prediction1 * yc_sd [i ]+ yc_mean [i ] # Scale y_prediction back
319
+ y_prediction1 .to_csv (output_folder + '/_case_{}_y_pred' .format (case_name )+ str (i + 1 )+ '.csv' ) # Store y_prediction
320
+
321
+
322
+ # Run the model if the file is being executed as the main program
323
+ if __name__ == '__main__' :
324
+ # Load data
325
+ case_name = 'your_case_name' # Put your case name here
326
+ DataComp = np .asarray (pd .read_csv ("Path of DataComp + {}" .format (case_name ))) # Put the path of simulated data here
327
+ DataField = np .asarray (pd .read_csv ("Path of DataField + {}" .format (case_name )))[:12 ,:] # Put the path of field data here
328
+ DataPred = np .asarray (pd .read_csv ("Path of DataField + {}" .format (case_name )))
329
+ output_folder = folder
330
+
331
+ conf = yaml .load (open ("config path + {}" .format (case_name )), Loader = yaml .FullLoader )
332
+
333
+ # Indicate the number of output
334
+ n_y = len (conf ['vc_keys' ])+ len (conf ['yc_keys' ])
335
+
336
+ print (f'The case you are running is { case_name } ' ) # Indicate the case you are running
337
+
338
+ MultiOutput_Bayesian_Calibration (n_y ,DataComp ,DataField ,DataPred ,output_folder )
0 commit comments