Skip to content

Commit a873d36

Browse files
authored
Merge pull request #14 from climate-machine/mb/Add_CES_code
Add Melanie's CES code - EKI, GPEmulator, MCMC, and Truth
2 parents 1d884e3 + d9b7a64 commit a873d36

File tree

4 files changed

+852
-0
lines changed

4 files changed

+852
-0
lines changed

src/EKI_mb.jl

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
module EKI
2+
3+
"""
4+
Module: EKI
5+
-------------------------------------
6+
Packages required: LinearAlgebra,
7+
Statistics,
8+
Distributions
9+
LinearAlgebra
10+
DifferentialeEquations
11+
Random
12+
Sundials
13+
-------------------------------------
14+
Idea: To construct an object to perform Ensemble Kalman updates
15+
It also measures errors to the truth to assess convergence
16+
-------------------------------------
17+
Exports: EKIObject
18+
update_ensemble!
19+
compute_error
20+
construct_initial_ensemble
21+
run_cloudy
22+
run_cloudy_ensemble
23+
-------------------------------------
24+
"""
25+
26+
# Import Cloudy modules
27+
using Cloudy.PDistributions
28+
using Cloudy.Sources
29+
using Cloudy.KernelTensors
30+
31+
# packages
32+
using Random
33+
using Statistics
34+
using Sundials # CVODE_BDF() solver for ODE
35+
using Distributions
36+
using LinearAlgebra
37+
using DifferentialEquations
38+
39+
# exports
40+
export EKIObj
41+
export construct_initial_ensemble
42+
export compute_error
43+
export run_cloudy
44+
export run_cloudy_ensemble
45+
export update_ensemble!
46+
47+
48+
#####
49+
##### Structure definitions
50+
#####
51+
52+
# structure to organize data
53+
struct EKIObj
54+
u::Vector{Array{Float64, 2}}
55+
unames::Vector{String}
56+
g_t::Vector{Float64}
57+
cov::Array{Float64, 2}
58+
N_ens::Int64
59+
g::Vector{Array{Float64, 2}}
60+
error::Vector{Float64}
61+
end
62+
63+
64+
#####
65+
##### Function definitions
66+
#####
67+
68+
# outer constructors
69+
function EKIObj(parameters::Array{Float64, 2}, parameter_names::Vector{String},
70+
t_mean, t_cov::Array{Float64, 2})
71+
72+
# ensemble size
73+
N_ens = size(parameters)[1]
74+
# parameters
75+
u = Array{Float64, 2}[] # array of Matrix{Float64}'s
76+
push!(u, parameters) # insert parameters at end of array (in this case just 1st entry)
77+
# observations
78+
g = Vector{Float64}[]
79+
# error store
80+
error = []
81+
82+
EKIObj(u, parameter_names, t_mean, t_cov, N_ens, g, error)
83+
end
84+
85+
86+
"""
87+
construct_initial_ensemble(priors, N_ens)
88+
89+
Constructs the initial parameters, by sampling N_ens samples from specified
90+
prior distributions.
91+
"""
92+
function construct_initial_ensemble(N_ens::Int64, priors; rng_seed=42)
93+
N_params = length(priors)
94+
params = zeros(N_ens, N_params)
95+
# Ensuring reproducibility of the sampled parameter values
96+
Random.seed!(rng_seed)
97+
for i in 1:N_params
98+
prior_i = priors[i]
99+
params[:, i] = rand(prior_i, N_ens)
100+
end
101+
102+
return params
103+
end # function construct_initial ensemble
104+
105+
function compute_error(eki)
106+
meang = dropdims(mean(eki.g[end], dims=1), dims=1)
107+
diff = eki.g_t - meang
108+
X = eki.cov \ diff # diff: column vector
109+
newerr = dot(diff, X)
110+
push!(eki.error, newerr)
111+
end # function compute_error
112+
113+
114+
function run_cloudy_ensemble(kernel::KernelTensor{Float64},
115+
dist::PDistribution{Float64},
116+
params::Array{Float64, 2},
117+
moments::Array{Float64, 1},
118+
tspan::Tuple{Float64, Float64}; rng_seed=42)
119+
120+
N_ens = size(params, 1) # params is N_ens x N_params
121+
n_moments = length(moments)
122+
g_ens = zeros(N_ens, n_moments)
123+
124+
Random.seed!(rng_seed)
125+
for i in 1:N_ens
126+
# run cloudy with the current parameters, i.e., map θ to G(θ)
127+
g_ens[i, :] = run_cloudy(params[i, :], kernel, dist, moments, tspan)
128+
end
129+
return g_ens
130+
end # function run_cloudy_ensemble
131+
132+
133+
"""
134+
run_cloudy(kernel, dist, moments, tspan)
135+
136+
- `kernel` - is the collision-coalescence kernel that determines the evolution
137+
of the droplet size distribution
138+
- `dist` - is a mass distribution function
139+
- `moments` - is an array defining the moments of dist Cloudy will compute
140+
over time (e.g, [0.0, 1.0, 2.0])
141+
- `tspan` - is a tuple definint the time interval over which cloudy is run
142+
"""
143+
function run_cloudy(params::Array{Float64, 1}, kernel::KernelTensor{Float64},
144+
dist::PDistributions.PDistribution{Float64},
145+
moments::Array{Float64, 1},
146+
tspan=Tuple{Float64, Float64})
147+
148+
# generate the initial distribution
149+
dist = PDistributions.update_params(dist, params)
150+
151+
# Numerical parameters
152+
tol = 1e-7
153+
154+
# Make sure moments are up to date. mom0 is the initial condition for the
155+
# ODE problem
156+
moments_init = fill(NaN, length(moments))
157+
for (i, mom) in enumerate(moments)
158+
moments_init[i] = PDistributions.moment(dist, convert(Float64, mom))
159+
end
160+
161+
# Set up ODE problem: dM/dt = f(M,p,t)
162+
rhs(M, p, t) = get_src_coalescence(M, dist, kernel)
163+
prob = ODEProblem(rhs, moments_init, tspan)
164+
# Solve the ODE
165+
sol = solve(prob, CVODE_BDF(), alg_hints=[:stiff], reltol=tol, abstol=tol)
166+
# Return moments at last time step
167+
moments_final = vcat(sol.u'...)[end, :]
168+
time = tspan[2]
169+
170+
return moments_final
171+
end # function run_cloudy
172+
173+
174+
function update_ensemble!(eki, g)
175+
# u: N_ens x N_params
176+
u = eki.u[end]
177+
178+
u_bar = fill(0.0, size(u)[2])
179+
# g: N_ens x N_data
180+
g_bar = fill(0.0, size(g)[2])
181+
182+
cov_ug = fill(0.0, size(u)[2], size(g)[2])
183+
cov_gg = fill(0.0, size(g)[2], size(g)[2])
184+
185+
# update means/covs with new param/observation pairs u, g
186+
for j = 1:eki.N_ens
187+
188+
u_ens = u[j, :]
189+
g_ens = g[j, :]
190+
191+
# add to mean
192+
u_bar += u_ens
193+
g_bar += g_ens
194+
195+
#add to cov
196+
cov_ug += u_ens * g_ens' # cov_ug is N_params x N_data
197+
cov_gg += g_ens * g_ens'
198+
end
199+
200+
u_bar = u_bar / eki.N_ens
201+
g_bar = g_bar / eki.N_ens
202+
cov_ug = cov_ug / eki.N_ens - u_bar * g_bar'
203+
cov_gg = cov_gg / eki.N_ens - g_bar * g_bar'
204+
205+
# update the parameters (with additive noise too)
206+
noise = rand(MvNormal(zeros(size(g)[2]), eki.cov), eki.N_ens) # N_data * N_ens
207+
y = (eki.g_t .+ noise)' # add g_t (N_data) to each column of noise (N_data x N_ens), then transp. into N_ens x N_data
208+
tmp = (cov_gg + eki.cov) \ (y - g)' # N_data x N_data \ [N_ens x N_data - N_ens x N_data]' --> tmp is N_data x N_ens
209+
u += (cov_ug * tmp)' # N_ens x N_params
210+
211+
# store new parameters (and observations)
212+
push!(eki.u, u) # N_ens x N_params
213+
push!(eki.g, g) # N_ens x N_data
214+
215+
compute_error(eki)
216+
217+
end # function update_ensemble!
218+
219+
end # module EKI

0 commit comments

Comments
 (0)