using CairoMakie
using Distributions
using LinearAlgebra
using OrdinaryDiffEq
using Random
using Statistics
set_theme!(
=18,
fontsize=(xgridvisible=false, ygridvisible=false,
Axis=false, rightspinevisible=false),
topspinevisible )
Metropolis-Algorithm for parameters estimation of ODEs
the script is adapted from the Bayesian Estimation of Differential Equations tutorial from Turing.jl, but instead of relying on the Nuts algorithm of Turing.jl, a simple Metroplis algorithm is coded here from scratch
1 Load packages and Makie theme
2 Define ODE-System
function lotka_volterra(du, u, p, t)
= p
α, β, γ, δ = u
x, y
1] = (α - β * y) * x
du[2] = (δ * x - γ) * y
du[
return nothing
end
lotka_volterra (generic function with 1 method)
3 Generate a test data set
function generate_data(rng; p)
= [1.0, 1.0]
u0 = (0.0, 10.0)
tspan = ODEProblem(lotka_volterra, u0, tspan, p)
prob
= solve(prob, Tsit5();
sol =0.1)
saveat= Array(sol) .+ rand(rng, Normal(0, 0.5), size(Array(sol)))
estim_dat
return estim_dat, sol.t
end
generate_data (generic function with 1 method)
4 Function to calculate the unnormalized posterior density
function unnormalized_posterior(θ, prior_dists, data, t)
= θ
σ, α, β, γ, δ = length(θ)
nparameter
## prior
if σ <= 0
return -Inf
end
= 0
prior for i in 1:nparameter
+= logpdf(prior_dists[i], θ[i])
prior end
if prior == -Inf
return -Inf
end
## likelihood
= [α, β, γ, δ]
p = [1.0, 1.0]
u0 = (0.0, 10.0)
tspan = ODEProblem(lotka_volterra, u0, tspan, p)
prob = solve(prob, Tsit5(); p=p, saveat=t)
predicted
= 0
likelihood for i in 1:length(predicted)
+= logpdf(MvNormal(predicted[i], σ^2 * I), data[:, i])
likelihood end
return prior + likelihood
end
unnormalized_posterior (generic function with 1 method)
5 Function to simulate the Markov chains
function run_chains(rng, data, t;
σ_prop,=5,
nchains=5_000)
nsamples
## priors
= truncated(InverseGamma(2, 3); lower=0, upper=1)
σ_prior = truncated(Normal(1.5, 0.5); lower=0.8, upper=2.5)
α_prior = truncated(Normal(1.2, 0.5); lower=0, upper=2)
β_prior = truncated(Normal(3.0, 0.5); lower=1, upper=4)
γ_prior = truncated(Normal(1.0, 0.5); lower=0, upper=2)
δ_prior = [σ_prior, α_prior, β_prior, γ_prior, δ_prior]
prior_dists
= 5
nparameter = zeros(nchains, nparameter, nsamples)
accepted_θ = zeros(nchains)
accepted = zeros(nchains, nparameter)
θ
Threads.@threads for n in 1:nchains
## start values for the parameters in the chain
## rough guesses are used here
## it would also possible to use the prior distributions as follows:
## for i in 1:nparameter
## θ[n, i] = rand(rng, prior_dists[i])
## end
:] = [0.7, 1.4, 0.9, 3.1, 1.1] .+ rand(rng, Normal(0, 0.1), 5)
θ[n, = unnormalized_posterior(θ[n, :], prior_dists, data, t)
post
for k in 1:nsamples
## new proposal
= MvNormal(θ[n, :], σ_prop)
proposal_dist = rand(rng, proposal_dist)
θstar
## evaluate prior + likelihood
= unnormalized_posterior(θstar, prior_dists, data, t)
poststar
## Metropolis ratio
= poststar - post
ratio
if log(rand(rng)) < min(ratio, 1)
+= 1
accepted[n] :] = θstar
θ[n, = poststar
post end
:, k] = θ[n, :]
accepted_θ[n, end
end
return accepted_θ, accepted / nsamples
end
run_chains (generic function with 1 method)
6 Trace plots and densities
function plot_trace_dens(; θ, burnin=nothing)
= Figure(size=(800, 800))
fig
= ["σ", "α", "β", "γ", "δ"]
titles = size(θ)
nchains, nparameter, nsamples = isnothing(burnin) ? max(Int(0.5*nsamples), 1) : burnin
burnin
for i in 1:nparameter
Axis(fig[i,1]; title = titles[i])
for n in 1:nchains
lines!((burnin:nsamples) .- burnin, θ[n, i, burnin:end];
=(Makie.wong_colors()[n], 0.5))
colorend
Axis(fig[i,2])
for n in 1:nchains
density!(θ[n, i, burnin:end];
=20,
bins= (Makie.wong_colors()[n], 0.05),
color= (Makie.wong_colors()[n], 1),
strokecolor = 2, strokearound = false)
strokewidth end
end
rowgap!(fig.layout, 1, 5)
return fig
end
plot_trace_dens (generic function with 1 method)
7 Posterior predictive check
function posterior_check(rng; θ, data, t, p, npost_samples=500, burnin=nothing)
= size(θ)
nchains, nparameter, nsamples = isnothing(burnin) ? max(Int(0.5*nsamples), 1) : burnin
burnin
= [1.0, 1.0]
u0 = (0.0, 10.0)
tspan
= Figure()
fig Axis(fig[1,1]; xlabel="Time", ylabel="Density")
## select posterior draws and plot solutions
= rand(rng, 1:nchains, npost_samples)
selected_chains = rand(rng, burnin:nsamples, npost_samples)
selected_samples for k in 1:npost_samples
= θ[selected_chains[k], :, selected_samples[k]]
θi = θi[2:5]
p_i
= ODEProblem(lotka_volterra, u0, tspan, p_i)
prob = solve(prob, Tsit5(); saveat=0.01)
sol
lines!(sol.t, sol[1, :], color=(Makie.wong_colors()[1], 0.05))
lines!(sol.t, sol[2, :], color=(Makie.wong_colors()[2], 0.05))
end
## true solution
= ODEProblem(lotka_volterra, u0, tspan, p)
prob = solve(prob, Tsit5(); p=p, saveat=0.01)
sol
lines!(sol.t, sol[1, :],
=:black,
color=2)
linewidthlines!(sol.t, sol[2, :],
=:black,
color=2)
linewidth
## measured data
scatter!(t, data[1, :])
scatter!(t, data[2, :])
return fig
end
posterior_check (generic function with 1 method)
8 Run everything
= MersenneTwister(123)
rng
## "true" parameter values
= [1.5, 1.0, 3.0, 1.0]
p = generate_data(rng; p)
data, t
## Simulate.
= Diagonal([0.001, 0.001, 0.001, 0.001, 0.001])
σ_prop = run_chains(rng, data, t;
θ, acceptance_rate
σ_prop,=200_000)
nsamples acceptance_rate
5-element Vector{Float64}:
0.03816
0.0378
0.03827
0.039195
0.037925
plot_trace_dens(; θ, burnin=50_000)
posterior_check(rng; θ, data, t, p, burnin=50_000)
the black line is generated with the parameters that are also used to produce the test data set, orange and blue lines are produced with posterior draws, the circles represent the test data set: