Skip to content

Commit f46498e

Browse files
committed
multiple-output Bayesian calibration
1 parent 397ba7e commit f46498e

File tree

1 file changed

+338
-0
lines changed

1 file changed

+338
-0
lines changed
+338
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
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

Comments
 (0)