using
    Wavelets,
    WaveletsExt,
    LinearAlgebra,
    Random,
    DataFrames,
    Statistics,
    CSV

function addnoise(x::AbstractArray{<:Number,1}, s::Real=0.1)
    ϵ = randn(length(x))
    ϵ = ϵ/norm(ϵ)
    y = x + ϵ * s * norm(x)
    return y
end

# compute metrics
function computemetrics(x::Array{T}, x₀::Array{T}) where T<:Number
    # PSNR
    r_psnr = [psnr(x[:,i], x₀[:,i]) for i in axes(x₀,2)]
    # SSIM
    r_ssim = [ssim(x[:,i], x₀[:,i]) for i in axes(x₀,2)]
    return mean([r_psnr r_ssim], dims = 1)
end

# single comparison
function singlecomparison(x::Vector{T}, wt::DiscreteWavelet,
        noise::AbstractFloat, shifts::Integer, samples::Integer) where T<:Number

    n = size(x,1)
    result = Array{AbstractFloat, 2}(undef, (109,3))
    # generate signals
    X₀ = duplicatesignals(x, samples, shifts)
    X = hcat([addnoise(X₀[:,i], noise) for i in 1:samples]...)

    # NOISY
    result[1,:] = [0 computemetrics(X, X₀)]

    # DWT
    L = maxtransformlevels(n)
    y = dwtall(X, wt, L)
    σ₁ = [noisest(y[:,i], false) for i in 1:samples]
    σ₂ = [relerrorthreshold(y[:,i], false) for i in 1:samples]
    ## visushrink & bestTH = individual
    X̂, time = @timed denoiseall(y, :dwt, wt, L=L, dnt=VisuShrink(n), estnoise=σ₁, bestTH=nothing)
    result[2,:] = [time computemetrics(X̂, X₀)]
    ## visushrink & bestTH = mean
    X̂, time = @timed denoiseall(y, :dwt, wt, L=L, dnt=VisuShrink(n), estnoise=σ₁, bestTH=mean)
    result[3,:] = [time computemetrics(X̂, X₀)]
    ## visushrink & bestTH = median
    X̂, time = @timed denoiseall(y, :dwt, wt, L=L, dnt=VisuShrink(n), estnoise=σ₁, bestTH=median)
    result[4,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = individual
    X̂, time = @timed denoiseall(y, :dwt, wt, L=L, dnt=RelErrorShrink(), estnoise=σ₂, bestTH=nothing)
    result[5,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = mean
    X̂, time = @timed denoiseall(y, :dwt, wt, L=L, dnt=RelErrorShrink(), estnoise=σ₂, bestTH=mean)
    result[6,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = median
    X̂, time = @timed denoiseall(y, :dwt, wt, L=L, dnt=RelErrorShrink(), estnoise=σ₂, bestTH=median)
    result[7,:] = [time computemetrics(X̂, X₀)]

    # WPT-L4
    tree = maketree(n, 4, :full)
    y = wptall(X, wt, 4)
    σ₁ = [noisest(y[:,i], false, tree) for i in 1:samples]
    σ₂ = [relerrorthreshold(y[:,i], false, tree) for i in 1:samples]
    ## visushrink & bestTH = individual
    X̂, time = @timed denoiseall(y, :wpt, wt, tree=tree, dnt=VisuShrink(n), estnoise=σ₁, bestTH=nothing)
    result[8,:] = [time computemetrics(X̂, X₀)]
    ## visushrink & bestTH = mean
    X̂, time = @timed denoiseall(y, :wpt, wt, tree=tree, dnt=VisuShrink(n), estnoise=σ₁, bestTH=mean)
    result[9,:] = [time computemetrics(X̂, X₀)]
    ## visushrink & bestTH = median
    X̂, time = @timed denoiseall(y, :wpt, wt, tree=tree, dnt=VisuShrink(n), estnoise=σ₁, bestTH=median)
    result[10,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = individual
    X̂, time = @timed denoiseall(y, :wpt, wt, tree=tree, dnt=RelErrorShrink(), estnoise=σ₂, bestTH=nothing)
    result[11,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = mean
    X̂, time = @timed denoiseall(y, :wpt, wt, tree=tree, dnt=RelErrorShrink(), estnoise=σ₂, bestTH=mean)
    result[12,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = median
    X̂, time = @timed denoiseall(y, :wpt, wt, tree=tree, dnt=RelErrorShrink(), estnoise=σ₂, bestTH=median)
    result[13,:] = [time computemetrics(X̂, X₀)]

    # WPT-L8
    tree = maketree(n, 8, :full)
    y = wptall(X, wt, 8)
    σ₁ = [noisest(y[:,i], false, tree) for i in 1:samples]
    σ₂ = [relerrorthreshold(y[:,i], false, tree) for i in 1:samples]
    ## visushrink & bestTH = individual
    X̂, time = @timed denoiseall(y, :wpt, wt, tree=tree, dnt=VisuShrink(n), estnoise=σ₁, bestTH=nothing)
    result[14,:] = [time computemetrics(X̂, X₀)]
    ## visushrink & bestTH = mean
    X̂, time = @timed denoiseall(y, :wpt, wt, tree=tree, dnt=VisuShrink(n), estnoise=σ₁, bestTH=mean)
    result[15,:] = [time computemetrics(X̂, X₀)]
    ## visushrink & bestTH = median
    X̂, time = @timed denoiseall(y, :wpt, wt, tree=tree, dnt=VisuShrink(n), estnoise=σ₁, bestTH=median)
    result[16,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = individual
    X̂, time = @timed denoiseall(y, :wpt, wt, tree=tree, dnt=RelErrorShrink(), estnoise=σ₂, bestTH=nothing)
    result[17,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = mean
    X̂, time = @timed denoiseall(y, :wpt, wt, tree=tree, dnt=RelErrorShrink(), estnoise=σ₂, bestTH=mean)
    result[18,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = median
    X̂, time = @timed denoiseall(y, :wpt, wt, tree=tree, dnt=RelErrorShrink(), estnoise=σ₂, bestTH=median)
    result[19,:] = [time computemetrics(X̂, X₀)]

    # WPT-BT
    Xw = wpdall(X, wt)
    tree = bestbasistreeall(Xw, BB())
    y = getbasiscoefall(Xw, tree)
    σ₁ = [noisest(y[:,i], false, tree[:,i]) for i in 1:samples]
    σ₂ = [relerrorthreshold(y[:,i], false, tree[:,i]) for i in 1:samples]
    ## visushrink & bestTH = individual
    X̂, time = @timed hcat([
        denoise(y[:,i], :wpt, wt, tree=tree[:,i], dnt=VisuShrink(n), estnoise=σ₁[i]) for i in 1:samples
    ]...)
    result[20,:] = [time computemetrics(X̂, X₀)]
    ## visushrink & bestTH = mean
    X̂, time = @timed hcat([
        denoise(y[:,i], :wpt, wt, tree=tree[:,i], dnt=VisuShrink(n), estnoise=mean(σ₁)) for i in 1:samples
    ]...)
    result[21,:] = [time computemetrics(X̂, X₀)]
    ## visushrink & bestTH = median
    X̂, time = @timed hcat([
        denoise(y[:,i], :wpt, wt, tree=tree[:,i], dnt=VisuShrink(n), estnoise=median(σ₁)) for i in 1:samples
    ]...)
    result[22,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = individual
    X̂, time = @timed hcat([
        denoise(y[:,i], :wpt, wt, tree=tree[:,i], dnt=RelErrorShrink(), estnoise=σ₂[i]) for i in 1:samples
    ]...)
    result[23,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = mean
    X̂, time = @timed hcat([
        denoise(y[:,i], :wpt, wt, tree=tree[:,i], dnt=RelErrorShrink(), estnoise=mean(σ₂)) for i in 1:samples
    ]...)
    result[24,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = median
    X̂, time = @timed hcat([
        denoise(y[:,i], :wpt, wt, tree=tree[:,i], dnt=RelErrorShrink(), estnoise=median(σ₂)) for i in 1:samples
    ]...)
    result[25,:] = [time computemetrics(X̂, X₀)]

    # JBB
    tree = bestbasistree(Xw, JBB())
    y = getbasiscoefall(Xw, tree)
    σ₁ = [noisest(y[:,i], false, tree) for i in 1:samples]
    σ₂ = [relerrorthreshold(y[:,i], false, tree) for i in 1:samples]
    ## visushrink & bestTH = individual
    X̂, time = @timed denoiseall(y, :wpt, wt, tree=tree, dnt=VisuShrink(n), estnoise=σ₁, bestTH=nothing)
    result[26,:] = [time computemetrics(X̂, X₀)]
    ## visushrink & bestTH = mean
    X̂, time = @timed denoiseall(y, :wpt, wt, tree=tree, dnt=VisuShrink(n), estnoise=σ₁, bestTH=mean)
    result[27,:] = [time computemetrics(X̂, X₀)]
    ## visushrink & bestTH = median
    X̂, time = @timed denoiseall(y, :wpt, wt, tree=tree, dnt=VisuShrink(n), estnoise=σ₁, bestTH=median)
    result[28,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = individual
    X̂, time = @timed denoiseall(y, :wpt, wt, tree=tree, dnt=RelErrorShrink(), estnoise=σ₂, bestTH=nothing)
    result[29,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = mean
    X̂, time = @timed denoiseall(y, :wpt, wt, tree=tree, dnt=RelErrorShrink(), estnoise=σ₂, bestTH=mean)
    result[30,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = median
    X̂, time = @timed denoiseall(y, :wpt, wt, tree=tree, dnt=RelErrorShrink(), estnoise=σ₂, bestTH=median)
    result[31,:] = [time computemetrics(X̂, X₀)]

    # LSDB
    tree = bestbasistree(Xw, LSDB())
    y = getbasiscoefall(Xw, tree)
    σ₁ = [noisest(y[:,i], false, tree) for i in 1:samples]
    σ₂ = [relerrorthreshold(y[:,i], false, tree) for i in 1:samples]
    ## visushrink & bestTH = individual
    X̂, time = @timed denoiseall(y, :wpt, wt, tree=tree, dnt=VisuShrink(n), estnoise=σ₁, bestTH=nothing)
    result[32,:] = [time computemetrics(X̂, X₀)]
    ## visushrink & bestTH = mean
    X̂, time = @timed denoiseall(y, :wpt, wt, tree=tree, dnt=VisuShrink(n), estnoise=σ₁, bestTH=mean)
    result[33,:] = [time computemetrics(X̂, X₀)]
    ## visushrink & bestTH = median
    X̂, time = @timed denoiseall(y, :wpt, wt, tree=tree, dnt=VisuShrink(n), estnoise=σ₁, bestTH=median)
    result[34,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = individual
    X̂, time = @timed denoiseall(y, :wpt, wt, tree=tree, dnt=RelErrorShrink(), estnoise=σ₂, bestTH=nothing)
    result[35,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = mean
    X̂, time = @timed denoiseall(y, :wpt, wt, tree=tree, dnt=RelErrorShrink(), estnoise=σ₂, bestTH=mean)
    result[36,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = median
    X̂, time = @timed denoiseall(y, :wpt, wt, tree=tree, dnt=RelErrorShrink(), estnoise=σ₂, bestTH=median)
    result[37,:] = [time computemetrics(X̂, X₀)]

    # ACWT
    L = maxtransformlevels(n)
    y = acdwtall(X, wt, L)
    σ₁ = [noisest(y[:,:,i], true) for i in 1:samples]
    σ₂ = [relerrorthreshold(y[:,:,i], true, nothing) for i in 1:samples]
    ## visushrink & bestTH = individual
    X̂, time = @timed denoiseall(y, :acwt, wt, L=L, dnt=VisuShrink(n), estnoise=σ₁, bestTH=nothing)
    result[38,:] = [time computemetrics(X̂, X₀)]
    ## visushrink & bestTH = mean
    X̂, time = @timed denoiseall(y, :acwt, wt, L=L, dnt=VisuShrink(n), estnoise=σ₁, bestTH=mean)
    result[39,:] = [time computemetrics(X̂, X₀)]
    ## visushrink & bestTH = median
    X̂, time = @timed denoiseall(y, :acwt, wt, L=L, dnt=VisuShrink(n), estnoise=σ₁, bestTH=median)
    result[40,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = individual
    X̂, time = @timed denoiseall(y, :acwt, wt, L=L, dnt=RelErrorShrink(), estnoise=σ₂, bestTH=nothing)
    result[41,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = mean
    X̂, time = @timed denoiseall(y, :acwt, wt, L=L, dnt=RelErrorShrink(), estnoise=σ₂, bestTH=mean)
    result[42,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = median
    X̂, time = @timed denoiseall(y, :acwt, wt, L=L, dnt=RelErrorShrink(), estnoise=σ₂, bestTH=median)
    result[43,:] = [time computemetrics(X̂, X₀)]

    # ACWPT-L4
    tree = maketree(n, 4, :full)
    y = acwpdall(X, wt, 8)
    σ₁ = [noisest(y[:,:,i], true, tree) for i in 1:samples]
    σ₂ = [relerrorthreshold(y[:,:,i], true, tree) for i in 1:samples]
    ## visushrink & bestTH = individual
    X̂, time = @timed denoiseall(y, :acwpt, wt, tree=tree, dnt=VisuShrink(n), estnoise=σ₁, bestTH=nothing)
    result[44,:] = [time computemetrics(X̂, X₀)]
    ## visushrink & bestTH = mean
    X̂, time = @timed denoiseall(y, :acwpt, wt, tree=tree, dnt=VisuShrink(n), estnoise=σ₁, bestTH=mean)
    result[45,:] = [time computemetrics(X̂, X₀)]
    ## visushrink & bestTH = median
    X̂, time = @timed denoiseall(y, :acwpt, wt, tree=tree, dnt=VisuShrink(n), estnoise=σ₁, bestTH=median)
    result[46,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = individual
    X̂, time = @timed denoiseall(y, :acwpt, wt, tree=tree, dnt=RelErrorShrink(), estnoise=σ₂, bestTH=nothing)
    result[47,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = mean
    X̂, time = @timed denoiseall(y, :acwpt, wt, tree=tree, dnt=RelErrorShrink(), estnoise=σ₂, bestTH=mean)
    result[48,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = median
    X̂, time = @timed denoiseall(y, :acwpt, wt, tree=tree, dnt=RelErrorShrink(), estnoise=σ₂, bestTH=median)
    result[49,:] = [time computemetrics(X̂, X₀)]

    # ACWPT-L8
    tree = maketree(n, 8, :full)
    σ₁ = [noisest(y[:,:,i], true, tree) for i in 1:samples]
    σ₂ = [relerrorthreshold(y[:,:,i], true, tree) for i in 1:samples]
    ## visushrink & bestTH = individual
    X̂, time = @timed denoiseall(y, :acwpt, wt, tree=tree, dnt=VisuShrink(n), estnoise=σ₁, bestTH=nothing)
    result[50,:] = [time computemetrics(X̂, X₀)]
    ## visushrink & bestTH = mean
    X̂, time = @timed denoiseall(y, :acwpt, wt, tree=tree, dnt=VisuShrink(n), estnoise=σ₁, bestTH=mean)
    result[51,:] = [time computemetrics(X̂, X₀)]
    ## visushrink & bestTH = median
    X̂, time = @timed denoiseall(y, :acwpt, wt, tree=tree, dnt=VisuShrink(n), estnoise=σ₁, bestTH=median)
    result[52,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = individual
    X̂, time = @timed denoiseall(y, :acwpt, wt, tree=tree, dnt=RelErrorShrink(), estnoise=σ₂, bestTH=nothing)
    result[53,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = mean
    X̂, time = @timed denoiseall(y, :acwpt, wt, tree=tree, dnt=RelErrorShrink(), estnoise=σ₂, bestTH=mean)
    result[54,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = median
    X̂, time = @timed denoiseall(y, :acwpt, wt, tree=tree, dnt=RelErrorShrink(), estnoise=σ₂, bestTH=median)
    result[55,:] = [time computemetrics(X̂, X₀)]

    # ACWPT-BT
    tree = bestbasistreeall(y, BB(redundant=true))
    σ₁ = [noisest(y[:,:,i], true, tree[:,i]) for i in 1:samples]
    σ₂ = [relerrorthreshold(y[:,:,i], true, tree[:,i]) for i in 1:samples]
    ## visushrink & bestTH = individual
    X̂, time = @timed hcat([
        denoise(y[:,:,i], :acwpt, wt, tree=tree[:,i], dnt=VisuShrink(n), estnoise=σ₁[i]) for i in 1:samples
    ]...)
    result[56,:] = [time computemetrics(X̂, X₀)]
    ## visushrink & bestTH = mean
    X̂, time = @timed hcat([
        denoise(y[:,:,i], :acwpt, wt, tree=tree[:,i], dnt=VisuShrink(n), estnoise=mean(σ₁)) for i in 1:samples
    ]...)
    result[57,:] = [time computemetrics(X̂, X₀)]
    ## visushrink & bestTH = median
    X̂, time = @timed hcat([
        denoise(y[:,:,i], :acwpt, wt, tree=tree[:,i], dnt=VisuShrink(n), estnoise=median(σ₁)) for i in 1:samples
    ]...)
    result[58,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = individual
    X̂, time = @timed hcat([
        denoise(y[:,:,i], :acwpt, wt, tree=tree[:,i], dnt=RelErrorShrink(), estnoise=σ₂[i]) for i in 1:samples
    ]...)
    result[59,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = mean
    X̂, time = @timed hcat([
        denoise(y[:,:,i], :acwpt, wt, tree=tree[:,i], dnt=RelErrorShrink(), estnoise=mean(σ₂)) for i in 1:samples
    ]...)
    result[60,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = median
    X̂, time = @timed hcat([
        denoise(y[:,:,i], :acwpt, wt, tree=tree[:,i], dnt=RelErrorShrink(), estnoise=median(σ₂)) for i in 1:samples
    ]...)
    result[61,:] = [time computemetrics(X̂, X₀)]

    # ACWPT-JBB
    tree = bestbasistree(y, JBB(redundant=true))
    σ₁ = [noisest(y[:,:,i], true, tree) for i in 1:samples]
    σ₂ = [relerrorthreshold(y[:,:,i], true, tree) for i in 1:samples]
    ## visushrink & bestTH = individual
    X̂, time = @timed denoiseall(y, :acwpt, wt, tree=tree, dnt=VisuShrink(n), estnoise=σ₁, bestTH=nothing)
    result[62,:] = [time computemetrics(X̂, X₀)]
    ## visushrink & bestTH = mean
    X̂, time = @timed denoiseall(y, :acwpt, wt, tree=tree, dnt=VisuShrink(n), estnoise=σ₁, bestTH=mean)
    result[63,:] = [time computemetrics(X̂, X₀)]
    ## visushrink & bestTH = median
    X̂, time = @timed denoiseall(y, :acwpt, wt, tree=tree, dnt=VisuShrink(n), estnoise=σ₁, bestTH=median)
    result[64,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = individual
    X̂, time = @timed denoiseall(y, :acwpt, wt, tree=tree, dnt=RelErrorShrink(), estnoise=σ₂, bestTH=nothing)
    result[65,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = mean
    X̂, time = @timed denoiseall(y, :acwpt, wt, tree=tree, dnt=RelErrorShrink(), estnoise=σ₂, bestTH=mean)
    result[66,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = median
    X̂, time = @timed denoiseall(y, :acwpt, wt, tree=tree, dnt=RelErrorShrink(), estnoise=σ₂, bestTH=median)
    result[67,:] = [time computemetrics(X̂, X₀)]

    # ACWPT-LSDB
    tree = bestbasistree(y, LSDB(redundant=true))
    σ₁ = [noisest(y[:,:,i], true, tree) for i in 1:samples]
    σ₂ = [relerrorthreshold(y[:,:,i], true, tree) for i in 1:samples]
    ## visushrink & bestTH = individual
    X̂, time = @timed denoiseall(y, :acwpt, wt, tree=tree, dnt=VisuShrink(n), estnoise=σ₁, bestTH=nothing)
    result[68,:] = [time computemetrics(X̂, X₀)]
    ## visushrink & bestTH = mean
    X̂, time = @timed denoiseall(y, :acwpt, wt, tree=tree, dnt=VisuShrink(n), estnoise=σ₁, bestTH=mean)
    result[69,:] = [time computemetrics(X̂, X₀)]
    ## visushrink & bestTH = median
    X̂, time = @timed denoiseall(y, :acwpt, wt, tree=tree, dnt=VisuShrink(n), estnoise=σ₁, bestTH=median)
    result[70,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = individual
    X̂, time = @timed denoiseall(y, :acwpt, wt, tree=tree, dnt=RelErrorShrink(), estnoise=σ₂, bestTH=nothing)
    result[71,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = mean
    X̂, time = @timed denoiseall(y, :acwpt, wt, tree=tree, dnt=RelErrorShrink(), estnoise=σ₂, bestTH=mean)
    result[72,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = median
    X̂, time = @timed denoiseall(y, :acwpt, wt, tree=tree, dnt=RelErrorShrink(), estnoise=σ₂, bestTH=median)
    result[73,:] = [time computemetrics(X̂, X₀)]

    # SDWT
    L = maxtransformlevels(n)
    y = sdwtall(X, wt, L)
    σ₁ = [noisest(y[:,:,i], true) for i in 1:samples]
    σ₂ = [relerrorthreshold(y[:,:,i], true, nothing) for i in 1:samples]
    ## visushrink & bestTH = individual
    X̂, time = @timed denoiseall(y, :sdwt, wt, L=L, dnt=VisuShrink(n), estnoise=σ₁, bestTH=nothing)
    result[74,:] = [time computemetrics(X̂, X₀)]
    ## visushrink & bestTH = mean
    X̂, time = @timed denoiseall(y, :sdwt, wt, L=L, dnt=VisuShrink(n), estnoise=σ₁, bestTH=mean)
    result[75,:] = [time computemetrics(X̂, X₀)]
    ## visushrink & bestTH = median
    X̂, time = @timed denoiseall(y, :sdwt, wt, L=L, dnt=VisuShrink(n), estnoise=σ₁, bestTH=median)
    result[76,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = individual
    X̂, time = @timed denoiseall(y, :sdwt, wt, L=L, dnt=RelErrorShrink(), estnoise=σ₂, bestTH=nothing)
    result[77,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = mean
    X̂, time = @timed denoiseall(y, :sdwt, wt, L=L, dnt=RelErrorShrink(), estnoise=σ₂, bestTH=mean)
    result[78,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = median
    X̂, time = @timed denoiseall(y, :sdwt, wt, L=L, dnt=RelErrorShrink(), estnoise=σ₂, bestTH=median)
    result[79,:] = [time computemetrics(X̂, X₀)]

    # SWPT-L4
    tree = maketree(n, 4, :full)
    y = swpdall(X, wt, 8)
    σ₁ = [noisest(y[:,:,i], true, tree) for i in 1:samples]
    σ₂ = [relerrorthreshold(y[:,:,i], true, tree) for i in 1:samples]
    ## visushrink & bestTH = individual
    X̂, time = @timed denoiseall(y, :swpd, wt, tree=tree, dnt=VisuShrink(n), estnoise=σ₁, bestTH=nothing)
    result[80,:] = [time computemetrics(X̂, X₀)]
    ## visushrink & bestTH = mean
    X̂, time = @timed denoiseall(y, :swpd, wt, tree=tree, dnt=VisuShrink(n), estnoise=σ₁, bestTH=mean)
    result[81,:] = [time computemetrics(X̂, X₀)]
    ## visushrink & bestTH = median
    X̂, time = @timed denoiseall(y, :swpd, wt, tree=tree, dnt=VisuShrink(n), estnoise=σ₁, bestTH=median)
    result[82,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = individual
    X̂, time = @timed denoiseall(y, :swpd, wt, tree=tree, dnt=RelErrorShrink(), estnoise=σ₂, bestTH=nothing)
    result[83,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = mean
    X̂, time = @timed denoiseall(y, :swpd, wt, tree=tree, dnt=RelErrorShrink(), estnoise=σ₂, bestTH=mean)
    result[84,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = median
    X̂, time = @timed denoiseall(y, :swpd, wt, tree=tree, dnt=RelErrorShrink(), estnoise=σ₂, bestTH=median)
    result[85,:] = [time computemetrics(X̂, X₀)]

    # SWPT-L8
    tree = maketree(n, 8, :full)
    σ₁ = [noisest(y[:,:,i], true, tree) for i in 1:samples]
    σ₂ = [relerrorthreshold(y[:,:,i], true, tree) for i in 1:samples]
    ## visushrink & bestTH = individual
    X̂, time = @timed denoiseall(y, :swpd, wt, tree=tree, dnt=VisuShrink(n), estnoise=σ₁, bestTH=nothing)
    result[86,:] = [time computemetrics(X̂, X₀)]
    ## visushrink & bestTH = mean
    X̂, time = @timed denoiseall(y, :swpd, wt, tree=tree, dnt=VisuShrink(n), estnoise=σ₁, bestTH=mean)
    result[87,:] = [time computemetrics(X̂, X₀)]
    ## visushrink & bestTH = median
    X̂, time = @timed denoiseall(y, :swpd, wt, tree=tree, dnt=VisuShrink(n), estnoise=σ₁, bestTH=median)
    result[88,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = individual
    X̂, time = @timed denoiseall(y, :swpd, wt, tree=tree, dnt=RelErrorShrink(), estnoise=σ₂, bestTH=nothing)
    result[89,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = mean
    X̂, time = @timed denoiseall(y, :swpd, wt, tree=tree, dnt=RelErrorShrink(), estnoise=σ₂, bestTH=mean)
    result[90,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = median
    X̂, time = @timed denoiseall(y, :swpd, wt, tree=tree, dnt=RelErrorShrink(), estnoise=σ₂, bestTH=median)
    result[91,:] = [time computemetrics(X̂, X₀)]

    # SWPT-BT
    tree = bestbasistreeall(y, BB(redundant=true))
    σ₁ = [noisest(y[:,:,i], true, tree[:,i]) for i in 1:samples]
    σ₂ = [relerrorthreshold(y[:,:,i], true, tree[:,i]) for i in 1:samples]
    ## visushrink & bestTH = individual
    X̂, time = @timed hcat([
        denoise(y[:,:,i], :swpd, wt, tree=tree[:,i], dnt=VisuShrink(n), estnoise=σ₁[i]) for i in 1:samples
    ]...)
    result[92,:] = [time computemetrics(X̂, X₀)]
    ## visushrink & bestTH = mean
    X̂, time = @timed hcat([
        denoise(y[:,:,i], :swpd, wt, tree=tree[:,i], dnt=VisuShrink(n), estnoise=mean(σ₁)) for i in 1:samples
    ]...)
    result[93,:] = [time computemetrics(X̂, X₀)]
    ## visushrink & bestTH = median
    X̂, time = @timed hcat([
        denoise(y[:,:,i], :swpd, wt, tree=tree[:,i], dnt=VisuShrink(n), estnoise=median(σ₁)) for i in 1:samples
    ]...)
    result[94,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = individual
    X̂, time = @timed hcat([
        denoise(y[:,:,i], :swpd, wt, tree=tree[:,i], dnt=RelErrorShrink(), estnoise=σ₂[i]) for i in 1:samples
    ]...)
    result[95,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = mean
    X̂, time = @timed hcat([
        denoise(y[:,:,i], :swpd, wt, tree=tree[:,i], dnt=RelErrorShrink(), estnoise=mean(σ₂)) for i in 1:samples
    ]...)
    result[96,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = median
    X̂, time = @timed hcat([
        denoise(y[:,:,i], :swpd, wt, tree=tree[:,i], dnt=RelErrorShrink(), estnoise=median(σ₂)) for i in 1:samples
    ]...)
    result[97,:] = [time computemetrics(X̂, X₀)]

    # SJBB
    tree = bestbasistree(y, JBB(redundant=true))
    σ₁ = [noisest(y[:,:,i], true, tree) for i in 1:samples]
    σ₂ = [relerrorthreshold(y[:,:,i], true, tree) for i in 1:samples]
    ## visushrink & bestTH = individual
    X̂, time = @timed denoiseall(y, :swpd, wt, tree=tree, dnt=VisuShrink(n), estnoise=σ₁, bestTH=nothing)
    result[98,:] = [time computemetrics(X̂, X₀)]
    ## visushrink & bestTH = mean
    X̂, time = @timed denoiseall(y, :swpd, wt, tree=tree, dnt=VisuShrink(n), estnoise=σ₁, bestTH=mean)
    result[99,:] = [time computemetrics(X̂, X₀)]
    ## visushrink & bestTH = median
    X̂, time = @timed denoiseall(y, :swpd, wt, tree=tree, dnt=VisuShrink(n), estnoise=σ₁, bestTH=median)
    result[100,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = individual
    X̂, time = @timed denoiseall(y, :swpd, wt, tree=tree, dnt=RelErrorShrink(), estnoise=σ₂, bestTH=nothing)
    result[101,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = mean
    X̂, time = @timed denoiseall(y, :swpd, wt, tree=tree, dnt=RelErrorShrink(), estnoise=σ₂, bestTH=mean)
    result[102,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = median
    X̂, time = @timed denoiseall(y, :swpd, wt, tree=tree, dnt=RelErrorShrink(), estnoise=σ₂, bestTH=median)
    result[103,:] = [time computemetrics(X̂, X₀)]

    # SLSDB
    tree = bestbasistree(y, LSDB(redundant=true))
    σ₁ = [noisest(y[:,:,i], true, tree) for i in 1:samples]
    σ₂ = [relerrorthreshold(y[:,:,i], true, tree) for i in 1:samples]
    ## visushrink & bestTH = individual
    X̂, time = @timed denoiseall(y, :swpd, wt, tree=tree, dnt=VisuShrink(n), estnoise=σ₁, bestTH=nothing)
    result[104,:] = [time computemetrics(X̂, X₀)]
    ## visushrink & bestTH = mean
    X̂, time = @timed denoiseall(y, :swpd, wt, tree=tree, dnt=VisuShrink(n), estnoise=σ₁, bestTH=mean)
    result[105,:] = [time computemetrics(X̂, X₀)]
    ## visushrink & bestTH = median
    X̂, time = @timed denoiseall(y, :swpd, wt, tree=tree, dnt=VisuShrink(n), estnoise=σ₁, bestTH=median)
    result[106,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = individual
    X̂, time = @timed denoiseall(y, :swpd, wt, tree=tree, dnt=RelErrorShrink(), estnoise=σ₂, bestTH=nothing)
    result[107,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = mean
    X̂, time = @timed denoiseall(y, :swpd, wt, tree=tree, dnt=RelErrorShrink(), estnoise=σ₂, bestTH=mean)
    result[108,:] = [time computemetrics(X̂, X₀)]
    ## relerrorshrink & bestTH = median
    X̂, time = @timed denoiseall(y, :swpd, wt, tree=tree, dnt=RelErrorShrink(), estnoise=σ₂, bestTH=median)
    result[109,:] = [time computemetrics(X̂, X₀)]

    return result
end

# repeated comparisons
function repeatedcomparisons(x::Vector{T}, wt::DiscreteWavelet,
        noise::AbstractFloat, shifts::Integer, samples::Integer,
        repeats::Integer) where T<:Number

    results =
        [singlecomparison(x, wt, noise, shifts, samples) for _ in 1:repeats] |>
        y -> cat(y..., dims = 3) |>
        y -> mean(y, dims = 3) |>
        y -> reshape(y, :, 3) |>
        y -> DataFrame(y, [:Time, :PSNR, :SSIM])

    params = DataFrame(
        transform = ["None";
            repeat(["DWT", "WPT-L4", "WPT-L8", "WPT-BT", "JBB", "LSDB"], inner=6);
            repeat(["ACWT", "ACWPT-L4", "ACWPT-L8", "ACWPT-BT", "ACJBB", "ACLSDB"], inner=6);
            repeat(["SDWT", "SWPT-L4", "SWPT-L8", "SWPT-BT", "SJBB", "SLSDB"], inner=6)],
        method = ["None";
            repeat(["VisuShrink", "RelErrorShrink"], inner=3, outer=18)],
        THType = ["None"; repeat(["Hard"], inner=108)],
        bestTH = ["None";
            repeat(["Individual", "Average", "Median"], outer=36)]
    )
    return [params results]
end

wt = wavelet(WT.db4)
L = 8

# compute and save results
samples = 100
repeats = 50
results = Dict{String, DataFrame}()
results["blocks"] = repeatedcomparisons(generatesignals(:blocks,L), wt, 0.5, 2, samples, repeats)
CSV.write("./results/blocks.csv", results["blocks"])
results["bumps"] = repeatedcomparisons(generatesignals(:bumps,L), wt, 0.5, 2, samples, repeats)
CSV.write("./results/bumps.csv", results["bumps"])
results["heavisine"] = repeatedcomparisons(generatesignals(:heavisine,L), wt, 0.5, 2, samples, repeats)
CSV.write("./results/heavisine.csv", results["heavisine"])
results["doppler"] = repeatedcomparisons(generatesignals(:doppler,L), wt, 0.5, 2, samples, repeats)
CSV.write("./results/doppler.csv", results["doppler"])
results["quadchirp"] = repeatedcomparisons(generatesignals(:quadchirp,L), wt, 0.5, 2, samples, repeats)
CSV.write("./results/quadchirp.csv", results["quadchirp"])
results["mishmash"] = repeatedcomparisons(generatesignals(:mishmash,L), wt, 0.5, 2, samples, repeats)
CSV.write("./results/mishmash.csv", results["mishmash"])

# display results
vscodedisplay(results["blocks"])
vscodedisplay(results["bumps"])
vscodedisplay(results["heavysine"])
vscodedisplay(results["doppler"])
vscodedisplay(results["quadchirp"])
vscodedisplay(results["mishmash"])


"""
Struct for LogNormalMixture

		lnmModel=LogNormalMixture(η::Array{num1},λ::Array{num2}) where {num1,num2<: Number}

Where:\n
		η  =	Array of volatilities.
		λ  = 	Array of weights.
"""
mutable struct LogNormalMixture{num <: Number, num2 <: Number, abstrUnderlying <: AbstractUnderlying, numtype <: Number} <: ItoProcess{numtype}
    η::Array{num, 1}
    λ::Array{num2, 1}
    underlying::abstrUnderlying
    function LogNormalMixture(η::Array{num, 1}, λ::Array{num2, 1}, underlying::abstrUnderlying) where {num <: Number, num2 <: Number, abstrUnderlying <: AbstractUnderlying}
        if minimum(η) <= 0.0
            error("Volatilities must be positive")
        elseif minimum(λ) <= 0.0
            error("weights must be positive")
        elseif sum(λ) > 1.0
            error("λs must be weights")
        elseif length(λ) != length(η) - 1
            error("Check vector lengths")
        else
            zero_typed = zero(num) + zero(num2)
            return new{num, num2, abstrUnderlying, typeof(zero_typed)}(η, λ, underlying)
        end
    end
end

export LogNormalMixture;

function simulate!(S, mcProcess::LogNormalMixture, rfCurve::AbstractZeroRateCurve, mcBaseData::AbstractMonteCarloConfiguration, T::Number)
    @assert T > 0.0
    r = rfCurve.r
    S0 = mcProcess.underlying.S0
    d = dividend(mcProcess)
    η_gbm = mcProcess.η
    λ_gmb = mcProcess.λ
    push!(λ_gmb, 1.0 - sum(mcProcess.λ))
    mu_gbm = r - d
    simulate!(S, GeometricBrownianMotion(η_gbm[1], mu_gbm, S0), mcBaseData, T)
    S .*= λ_gmb[1]
    tmp_ = similar(S)
    for (η_gbm_, λ_gmb_) in zip(η_gbm[2:end], λ_gmb[2:end])
        simulate!(tmp_, GeometricBrownianMotion(η_gbm_, mu_gbm, S0), mcBaseData, T)
        S .+= λ_gmb_ * tmp_
    end
    return nothing
end


# What would the battery range be? The local start-up competition
# considers a battery powered torpedo pulling a hang-glider!
#
# Run line by line in the REPL, or ctrl + enter in VSCode

using MechanicalUnits
@import_expand ~W
kWh = kW*h

begin
    "Battery capability"
    E_battery = 200kWh
    "Diameter torpedo"
    D_torpedo = 0.5m
    "Area, hangglider"
    A_hangglider = 20m²
    "Glide ratio, hangglider"
    Cl_per_Cd = 15
    "Mass, hangglider and pilot"
    m_t = 22kg + 70kg
    "Efficiency, torpedo"
    η = 0.3
    "Drag coefficient, torpedo"
    C_d = 0.2
    "Density, water"
    ρ_w = 1026kg/m³
    "Density, air"
    ρ_a = 1.25kg/m³
    "Velocity, torpedo and hangglider"
    v = 50km/h

    "
    resistance(C, ρ::Density, v::Velocity, A::Area)
    -> ::Force
    "
    resistance(C, ρ::Density, v::Velocity, A::Area) = (1/2) * ρ * C  * A * v^2 |> N
end

lift = m_t * g

drag_hangglider = lift / Cl_per_Cd |> N

drag_torpedo = resistance(C_d, ρ_w, v, (π / 4) * D_torpedo^2 )

range_torpedo_hangglider = E_battery * η / (drag_torpedo + drag_hangglider) |> km


function nuts(θ0, δ, L, ∇L, L∇L=θ->(L(θ),∇L(θ)), M=100, Madapt=M, verbose=true, Δ_max=1000)
  doc"""
    - θ0      : initial model parameter
    - δ       : desirable average accept rate
    - L       : likelihood function
    - ∇L      : gradient of L
    - L∇L     : function to compute both L and ∇L, ideally in one pass
    - M       : sample number
    - Madapt  : number of samples for step size adaptation
    - verbose : whether to show log
  """

  function leapfrog(θ, r, ϵ)
    doc"""
      - θ : model parameter
      - r : momentum variable
      - ϵ : leapfrog step size
    """
    r̃ = r + 0.5 * ϵ * ∇L(θ)
    θ̃ = θ + ϵ * r̃
    r̃ = r̃ + 0.5 * ϵ * ∇L(θ̃)
    return θ̃, r̃
  end

  function find_reasonable_ϵ(θ)
    ϵ, r = 1, randn(length(θ))
    θ′, r′ = leapfrog(θ, r, ϵ)

    # This trick prevents the log-joint or its graident from being infinte
    # Ref: code start from Line 111 in https://github.com/mfouesneau/NUTS/blob/master/nuts.py
    # QUES: will this lead to some bias of the sampler?
    (l,g) = L∇L(θ′)
    while isinf(l) || any(isinf.(g))
      ϵ = ϵ * 0.5
      θ′, r′ = leapfrog(θ, r, ϵ)
      (l,g) = L∇L(θ′)
    end

    a = 2 * (exp(L(θ′) - 0.5 * dot(r′, r′)) / exp(L(θ) - 0.5 * dot(r, r)) > 0.5) - 1
    while (exp(L(θ′) - 0.5 * dot(r′, r′)) / exp(L(θ) - 0.5 * dot(r, r)))^float(a) > 2^float(-a)
      ϵ = 2^float(a) * ϵ
      θ′, r′ = leapfrog(θ, r, ϵ)
    end
    return ϵ
  end

  function build_tree(θ, r, u, v, j, ϵ, θ0, r0)
    doc"""
      - θ   : model parameter
      - r   : momentum variable
      - u   : slice variable
      - v   : direction ∈ {-1, 1}
      - j   : depth of tree
      - ϵ   : leapfrog step size
      - θ0  : initial model parameter
      - r0  : initial mometum variable
    """
    if j == 0
      # Base case - take one leapfrog step in the direction v.
      θ′, r′ = leapfrog(θ, r, v * ϵ)
      # NOTE: this trick prevents the log-joint or its graident from being infinte
      (l,g) = L∇L(θ′)
      while isinf(l) || any(isinf.(g))
        ϵ = ϵ * 0.5
        θ′, r′ = leapfrog(θ, r, ϵ)
        (l,g) = L∇L(θ′)
      end

      n′ = u <= exp(L(θ′) - 0.5 * dot(r′, r′))
      s′ = u < exp(Δ_max + L(θ′) - 0.5 * dot(r′, r′))
      return θ′, r′, θ′, r′, θ′, n′, s′, min(1, exp(L(θ′) - 0.5 * dot(r′, r′) - L(θ0) + 0.5 * dot(r0, r0))), 1
    else
      # Recursion - build the left and right subtrees.
      θm, rm, θp, rp, θ′, n′, s′, α′, n′_α = build_tree(θ, r, u, v, j - 1, ϵ, θ0, r0)
      if s′ == 1
        if v == -1
          θm, rm, _, _, θ′′, n′′, s′′, α′′, n′′_α = build_tree(θm, rm, u, v, j - 1, ϵ, θ0, r0)
        else
          _, _, θp, rp, θ′′, n′′, s′′, α′′, n′′_α = build_tree(θp, rp, u, v, j - 1, ϵ, θ0, r0)
        end
        if rand() < n′′ / (n′ + n′′)
          θ′ = θ′′
        end
        α′ = α′ + α′′
        n′_α = n′_α + n′′_α
        s′ = s′′ & (dot(θp - θm, rm) >= 0) & (dot(θp - θm, rp) >= 0)
        n′ = n′ + n′′
      end
      return θm, rm, θp, rp, θ′, n′, s′, α′, n′_α
    end
  end

  θs = Array{Array}(M + 1)

  θs[1], ϵ = θ0, find_reasonable_ϵ(θ0)
  μ, γ, t_0, κ = log(10 * ϵ), 0.05, 10, 0.75
  ϵ̄, H̄ = 1, 0

  if verbose println("[NUTS] start sampling for $M samples with inital ϵ=$ϵ") end

  for m = 1:M
    if verbose print('.') end
    r0 = randn(length(θ0))
    u = rand() * exp(L(θs[m]) - 0.5 * dot(r0, r0)) # Note: θ^{m-1} in the paper corresponds to
                                                   #       `θs[m]` in the code
    θm, θp, rm, rp, j, θs[m + 1], n, s = θs[m], θs[m], r0, r0, 0, θs[m], 1, 1
    α, n_α = NaN, NaN
    while s == 1
      v = rand([-1, 1])
      if v == -1
        θm, rm, _, _, θ′, n′, s′, α, n_α = build_tree(θm, rm, u, v, j, ϵ, θs[m], r0)
      else
        _, _, θp, rp, θ′, n′, s′, α, n_α = build_tree(θp, rp, u, v, j, ϵ, θs[m], r0)
      end
      if s′ == 1
        if rand() < min(1, n′ / n)
          θs[m + 1] = θ′
        end
      end
      n = n + n′
      s = s′ & (dot(θp - θm, rm) >= 0) & (dot(θp - θm, rp) >= 0)
      j = j + 1
    end
    if m + 1 <= Madapt + 1
      # NOTE: H̄ goes to negative when δ - α / n_α < 0
      H̄ = (1 - 1 / (m + t_0)) * H̄ + 1 / (m + t_0) * (δ - α / n_α)
      ϵ = exp(μ - sqrt(m) / γ * H̄)
      ϵ̄ = exp(m^float(-κ) * log(ϵ) + (1 - m^float(-κ)) * log(ϵ̄))
    else
      ϵ = ϵ̄
    end
  end

  if verbose println() end
  if verbose println("[NUTS] sampling complete with final apated ϵ = $ϵ") end

  return θs
end


import MittagLefflerFunctions.H
import MittagLefflerFunctions.Fplus
using PyPlot

α = 0.7
β = 0.95
tol = 1e-10

x = range(0.5, 1.5, length=201)
z = complex.(x)
errH(z) = (z^α-1) - (z-1)*(α+(z-1)*H(α, z, tol))

y = real(errH.(z))
figure(1)
plot(x, y)
grid(true)

G(z) = z^(α-β) * (z-1) / (z^α-1)
errG(z) = G(z) - z^(α-β) / (α+(z-1)*H(α,z,tol))
y = real(errG.(z))
figure(1)
figure(2)
plot(x, y)
grid(true)

y = real(Fplus.(α, β, z, tol))
figure(3)
lo = 1 - 0.1
hi = 1 + 0.1
plot(x, y)
v = axis()
plot([lo, lo, NaN, hi, hi], [v[3], v[4], NaN, v[3], v[4]], ":")
grid(true)


### lucy.jl ---  Lucy deconvolution
#
# Copyright (C) 2019 Jakub Wronowski.
#
# Maintainer: Mosè Giordano <mose AT gnu DOT org>
# Keywords: richadson, lucy, deconvolution, signal processing
#
# This file is a part of Deconvolution.jl.
#
# License is MIT "Expat".
#
### Commentary:
#
# This file provides the `lucy' function to perform Richardson-Lucy deconvolution.
#
### Code:

export lucy

function lucy(observed::AbstractArray, psf::AbstractArray; iterations::Integer = 1)
    @assert size(observed) == size(psf)
    @assert iterations >= 0

    psf_ft = fft(psf)
    psf_ft_conj = conj.(psf_ft)

    function lucystep(e)
        return e .* real(ifft(fft(observed ./ ifft(fft(e) .* psf_ft)) .* psf_ft_conj))
    end

    estimated = observed
    for i in 1:iterations
        estimated = lucystep(estimated)
    end

    return estimated
end

"""
    lucy(observed, psf[, iterations])

Returns the Richardson-Lucy deconvolution of `observed`, using the point spread
function `psf`.

All arguments must be arrays in the time/space domain and all of the same size,
they will be converted into the frequency domain internally using the `fft`
function.
"""
lucy


# Model m4.1s.jl

using Pkg, DrWatson

begin
    using StanQuap
    using StatisticalRethinking
end

begin
    df = CSV.read(sr_datadir("Howell1.csv"), DataFrame; delim=';')
    df = filter(row -> row[:age] >= 18, df);
end;

stan4_1 = "
data {
  int N;
  real<lower=0> height[N];
}
parameters {
  real<lower=0> sigma;
  real<lower=0,upper=250> mu;
}
model {
  // Priors for mu and sigma
  mu ~ normal(178, 20);
  sigma ~ uniform(0 , 50);

  // Observed heights
  height ~ normal(mu, sigma);
}
";

data = (N = length(df.height), height = df.height)
init = (mu = 180.0, sigma = 10.0)
q4_1s, m4_1s, o4_1s = stan_quap("m4.1s", stan4_1; data, init);

if q4_1s.converged
  quap4_1s_df = sample(q4_1s)          # DataFrame with samples
  precis(quap4_1s_df)
  post4_1s = read_samples(m4_1s)
  post4_1s |> display
end

# End of m4.1s.jl


using Test

using LinearAlgebra
using ProximalAlgorithms
using ProximalAlgorithms: NesterovExtrapolation, initialize, SimpleNesterovSequence, FixedNesterovSequence, AdaptiveNesterovSequence

for sequence_type in [SimpleNesterovSequence, FixedNesterovSequence]
    for R in [Float32, Float64]
        @testset "Nesterov accel. ($sequence_type, $R)" begin
            H = R[
                0.63287    0.330934   -0.156908   -0.294776    0.10761;
                0.330934   0.673201    0.0459778   0.231011   -0.235265;
               -0.156908   0.0459778   0.635812   -0.232261   -0.388775;
               -0.294776   0.231011   -0.232261    0.726854   -0.0691783;
                0.10761   -0.235265   -0.388775   -0.0691783   0.336262;
            ]
            l = R[1.0, 2.0, 3.0, 4.0, 5.0]

            n = length(l)

            f(x) = dot(x, H * x) / 2 + dot(x, l)
            grad_f(x) = H * x + l

            x_star = -H \ l
            f_star = f(x_star)

            Lip = opnorm(H)
            gamma = 1 / Lip
            x = zeros(R, n)
            y = x

            norm_initial_error = norm(x_star - x)

            @inferred initialize(NesterovExtrapolation(sequence_type), x)

            seq = sequence_type{R}()
            @test eltype(seq) == R

            @inferred Iterators.peel(seq)

            for (it, coeff) in Iterators.take(enumerate(seq), 100)
                if it == 1
                    @test iszero(coeff)
                end

                grad_f_y = grad_f(y)
                x_prev = x
                x = y - gamma * grad_f_y
                y = x + coeff * (x - x_prev)

                # test that iterates satisfy Thm 4.4 from Beck, Teboulle (2009)
                @test f(x) - f_star <= 2 / (gamma * (it + 1)^2) * norm_initial_error^2
            end
        end
    end
end

@testset "Nesterov adaptive accel." begin
    R = Float64
    fixed_gamma = R(1.7)

    adaptive_seq = AdaptiveNesterovSequence(R(0))
    fixed_seq = FixedNesterovSequence{R}()
    for el in Iterators.take(fixed_seq, 20)
        @test isapprox(el, ProximalAlgorithms.next!(adaptive_seq, fixed_gamma))
    end

    m = R(1)
    adaptive_seq = AdaptiveNesterovSequence(m)
    for _ in 1:20
        @test isapprox(
            (1 - sqrt(m * fixed_gamma)) / (1 + sqrt(m * fixed_gamma)),
            ProximalAlgorithms.next!(adaptive_seq, fixed_gamma)
        )
    end
end


using Pardiso

export newton, L₁

L₁(V::Array) = sum(abs.(V)) / length(V)
L₂(V::Array, xs::Array) = trapz(xs, V.^2)

function newton(V₀::Array, rhs::Function, jac::Function, p::Dict, N::Int, Δx::Real)
    max_iter = 100
    tol = 1e-8

    V = copy(V₀)

    ps = MKLPardisoSolver()

    for i in 1:max_iter
        Y = rhs(V, p, N, Δx)
        error = L₁(Y)

        println("Running iteration n $i, error is $error")
        if error < tol
            break
        end

        J = jac(V, p, N, Δx)
        Nx, Ny = size(J)
        Nv = length(V)
        V += solve(ps, J, -Y)
    end
    V
end


@testset " Classic model forecasting " begin

    import DifferentialEquations: ODEProblem, solve, Tsit5

    σ = 10.0
    ρ = 28.0
    β = 8.0 / 3

    dt_integration = 0.01
    dt_states = 1
    dt_obs = 8
    parameters = [σ, ρ, β]
    var_obs = [1]
    nb_loop_train = 10^2
    nb_loop_test = 10
    sigma2_catalog = 0.0
    sigma2_obs = 2.0

    ssm = StateSpaceModel(
        lorenz63,
        dt_integration,
        dt_states,
        dt_obs,
        parameters,
        var_obs,
        nb_loop_train,
        nb_loop_test,
        sigma2_catalog,
        sigma2_obs,
    )

    # compute u0 at time = 5 to be in the attractor space

    u0 = [8.0; 0.0; 30.0]
    tspan = (0.0, 5.0)
    prob = ODEProblem(lorenz63, u0, tspan, parameters)
    u0 = last(solve(prob, Tsit5(), reltol = 1e-6, save_everystep = false))

    xt, yo, catalog = generate_data(ssm, u0)

    DA = DataAssimilation(ssm, xt)

    x̂ = forecast(DA, yo, AnEnKS(100), progress = false)

    rmse = RMSE(xt, x̂)

    println(" model forecasting RMSE : $rmse ")

    @test rmse < 1.0

end


##### Packaged loglikelihood
using LinearAlgebra
#using Turing
using Random
using AdvancedHMC
using ForwardDiff
include("../../src/faberschauder.jl")
include("../../src/asvar.jl")
include("../../src/pdmp.jl")
# up to constant of proportionality
# π(x) = exp(-ϕ)*C
function ϕ(ξ, L, T; p = T/(1<<L)) # formula (17)
        r = 0.0
        s = p*0.5
        for i in 1:(1<<L)
            x = dotψ(ξ, s, L,  T)
            r += 0.5*p*(b(x)^2 + b′(x))
            s += p
        end
        @assert  T+p*0.5 - 0.0001 < s < T+p*0.5 + 0.0001
    return r + 0.5*norm(ξ)^2
end

function ∇ϕ(ξ, i, L, T; p = T/(1<<L)) # formula (17)
    if i == (2 << L) + 1    # final point
        error("fixed final point")
    elseif i == 1   # initial point
        error("fixed initial point")
    else
        l = lvl(i, L)
        k = (i - 1) ÷ (2 << l)
        # println("index ($l, $k) out of $L level")
        δ = T/(1 << (L-l)) # T/(2^(L-l))
        r = 0.0
        s = δ*k + p*0.5
        # println("T = $T")
        for i in 1:l+1
            # println("integrating at the point $s")
            x = dotψ(ξ, s, L,  T)
            r += 0.5*p*Λ(s, L-l, T)*(2b(x)*b′(x) + b″(x))
            s += p
        end
        @assert δ*(k + 1) + p*0.5 - 0.001 < s < δ*(k + 1) + p*0.5 + 0.001
        return r + ξ[i]
    end
end

# Drift
b(x) = α * sin(x)
# First derivative
b′(x) = α * cos(x)
# Second derivative
b″(x) = -α * sin(x)

# Drift
#α = 1.5
L = 6
T = 50.0
n = (2 << L) + 1
u, v = 0.0, 0.0  # initial and fianl point
D = n - 2

# loglikelihood
function logdensity_f(q)
    ξ = [u/sqrt(T); q;  v/sqrt(T)]
    return -ϕ(ξ, L, T)
end
# gradient of loglikelihood
function grad_f(q)
    ξ = [u / sqrt(T); q; v / sqrt(T)]
    return (-ϕ(ξ, L, T),[-∇ϕ(ξ, i, L, T) for i in 2:(2 << L)])
end

# reshape output from vector of vector to matrix
function vecvec_to_matrix(vecvec)
    dim1 = length(vecvec)
    dim2 = length(vecvec[1])
    my_array = zeros(Float64, dim2, dim1)
    for i in 1:dim1
        for j in 1:dim2
            my_array[j,i] = vecvec[i][j]
        end
    end
    return my_array
end

function run_hmc(df)
    q0 = randn(D)
    n_samples, n_adapts, target = 3_000, 2_000, 0.8
    metric = DiagEuclideanMetric(D)
    # h = Hamiltonian(metric, logdensity_f, grad_f)
    h = Hamiltonian(metric, logdensity_f, ForwardDiff)
    eps_init = find_good_stepsize(h, q0)
    int = Leapfrog(eps_init)
    traj = AdvancedHMC.NUTS{MultinomialTS,GeneralisedNoUTurn}(int)
    adaptor = AdvancedHMC.StanHMCAdaptor(
        n_adapts, MassMatrixAdaptor(metric), NesterovDualAveraging(target, eps_init)
        )
    #u, v = 0.0/sqrt(T), 0.0/ sqrt(T)  # initial and fianl point
    hmc_time = @elapsed ((samples, stats) = sample(h, traj, q0, n_samples, adaptor, n_adapts))
    burn1 = 100
     # xT_2 = sqrt(T)/2*(samples[Int((length(q0)+1)/2), burn1+1:niter])
    xT_2 = sqrt(T)/2 .*(getindex.(samples, Int((length(q0)+1)/2)))
    nn = length(xT_2)
    ave = cumsum(xT_2)./(Float64.(1:nn))
    for batches in [50]
        #reshape a vector of vectors in a matrix
        ess = ESS(vecvec_to_matrix(samples)[:,burn1:end], n_batches = batches)
        push!(df, Dict(:sampler => "HMC", :alpha => α, :T => n_samples, :nbatches => batches,
            :stat => "ess_mean", :y => sum(ess[2:end-1])/(length(ess)-2), :runtime =>  hmc_time))
        push!(df, Dict(:sampler => "HMC", :alpha => α, :T => n_samples, :nbatches => batches,
            :stat => "ess_median", :y => median(ess[2:end-1]), :runtime =>  hmc_time))
        push!(df, Dict(:sampler => "HMC", :alpha => α, :T => n_samples, :nbatches => batches,
            :stat => "ess_min", :y => minimum(ess[2:end-1]), :runtime => hmc_time))
        push!(df, Dict(:sampler => "HMC", :alpha => α, :T => n_samples, :nbatches => batches,
            :stat => "ess_x_T2", :y => ess[Int((length(q0)+1)/2)], :runtime => hmc_time))
    end
    return df, ave
end

using DataFrames, Random
function data_collection_hmc(df)
    Random.seed!(0)
    for α′ in [0.1, 0.3, 0.5]
        global α = α′
        println("iteration ")
        df, x = run_hmc(df)
    end
    return df
end

df = DataFrame(sampler = String[], alpha = Float64[], T = Float64[],
    nbatches = Int64[], stat = String[], y = Float64[], runtime = Float64[])
df = data_collection_hmc(df)
using CSV, JLD2
CSV.write("./scripts/compare/benchamrk_hmc.csv", df)


# test_multigrid.jl : test multigrid implementation

for (d,d_name) in zip(2:3,["₂","₃"])
    @testset "Default Multigrid methods for $(d)D anisotropic problems" begin
        for (problem, problem_name) in zip(["p₁","p₂","p₃","p₄"],["     LAPLACE $(d)D      ","     ELLIPTIC $(d)D     ","   ANISOTROPIC $(d)D    ","ROTATED ANISTROPIC $(d)D"])
            if !( d == 3 && problem == "p₄" )
                println("*************************")
                println("* $(problem_name) *")
                println("*************************")
                for (method,method_name) in zip([V_cycle, W_cycle, F_cycle],["V-CYCLE", "W-CYCLE", "  FMG  "])
                    println("+-----------------------+")
                    println("|         $(method_name)       |")
                    println("+-----------------------+")
                    for n in 2 .^(1:(12-2d))
                        print("n = $(n)...")
                        A = method(eval(Symbol(problem,d_name))(n),ntuple(i->n,d))
                        b = ones(prod([n-1 for i in 1:d]))
                        x = A\b
                        @test A.resnorm[end] < 1/n^d
                        println("done ($(length(A.resnorm)-1) iterations)")
                    end
                end
            end
        end
    end
end


# using these functions
using DelimitedFiles, Polynomials

# function definitions

# get user input for left baseline points
function get_baseline_points_l()
	println("Enter desired points for left baseline correction, separated by spaces:")
	string = readline()
	splitstring = split(string, " ")
	return [parse(Int, i) for i in splitstring]
end

# get user input for right baseline points
function get_baseline_points_r()
	println("Enter desired points for right baseline correction, separated by spaces:")
	string = readline()
	splitstring = split(string, " ")
	return [parse(Int, i) for i in splitstring]
end

# get order of polynomial for fitting
function get_polynomial_order()
	println("Enter polynomial order:")
	return parse(Int, readline())
end

# get rid of first 250 ps and return array of remaining spectra
function droptimes()
	println("getting input file")
	arr = readdlm("all_procesd_spectra_.csv", ',')
	pixels = @view arr[:, 1]
	longdelays = @view arr[:, 42:end]
	smallarr = hcat(pixels, longdelays)
	return smallarr
end

# stitch all times back together
function stitch(corrected)
	println("compiling output")
	original = readdlm("all_procesd_spectra_.csv", ',')
	shortdelays = @view original[2:end, 1:41]
	delays = @view original[1, :]
	arrout = vcat(transpose(delays), hcat(shortdelays, corrected))
	return arrout
end

# get left y-values for polynomial fitting
function ysforpolyfitl(arrin)
	spectral = @view arrin[2:129, 2:end]
	indexl = @view arrin[2:129, 1]
	yindiciesl = [findfirst(isequal(n), indexl) for n in fitpointsl]

	println("generate y-values for polynomial fit (left)")
	ys = [spectral[k, 1] for k in yindiciesl]
	for i in 2:length(spectral[1, : ])
		temp =  [spectral[j, i] for j in yindiciesl]
		ys = hcat(ys, temp)
	end
	return ys
end

# get right y-values for polynomial fitting
function ysforpolyfitr(arrin)
	spectrar = @view arrin[130:end, 2:end]
	indexr = @view arrin[130:end, 1]
	yindiciesr = [findfirst(isequal(n), indexr) for n in fitpointsr]

	println("generate y-values for polynomial fit (right)")
	ys = [spectrar[k, 1] for k in yindiciesr]
	for i in 2:length(spectrar[1, : ])
		temp =  [spectrar[j, i] for j in yindiciesr]
		ys = hcat(ys, temp)
	end
	return ys
end

# generate array of baseline subtractions for left detector
function genbasesubsl(arrin, polys)
	indexl = @view arrin[2:129, 1]
	println("generating baseline subtractions (left)")
	subslist = [polyval(polys[i], indexl) for i in 1:length(polys)]
	subs = subslist[1]
	for i in 2:length(subslist)
		subs = hcat(subs, subslist[i])
	end
	return subs
end

# generate array of baseline subtractions for right detector
function genbasesubsr(arrin, polys)
	indexr = @view arrin[130:end, 1]
	println("generating baseline subtractions (right)")
	subslist = [polyval(polys[i], indexr) for i in 1:length(polys)]
	subs = subslist[1]
	for i in 2:length(subslist)
		subs = hcat(subs, subslist[i])
	end
	return subs
end

# get array of left detector polynomials
function polynomialsl(yvalues)
	println("fitting polynomials (left)")
	polys = [polyfit(fitpointsl, yvalues[:, i], order) for i in 1:length(yvalues[1, : ])]
	return polys
end

# get array of right detector polynomials
function polynomialsr(yvalues)
	println("fitting polynomials (right)")
	polys = [polyfit(fitpointsr, yvalues[:, i], order) for i in 1:length(yvalues[1, : ])]
	return polys
end

# get array of corrected baseline corrected spectra
function subtractbase(arrin, subs)
	spectra = @view arrin[2:end, 2:end]
	println("subtracting baselines")
	corrected = spectra .- subs
	return corrected
end

# code below here
const fitpointsl = get_baseline_points_l()
const fitpointsr = get_baseline_points_r()
const order = get_polynomial_order()

const arrin = droptimes()
const subs = vcat(genbasesubsl(arrin, polynomialsl(ysforpolyfitl(arrin))), genbasesubsr(arrin, polynomialsr(ysforpolyfitr(arrin))))
const corrected = subtractbase(arrin, subs)

const outarr = stitch(corrected)

writedlm("testset.csv", outarr, ',')
println("complete")
exit()


"""
    struct FermionSpace{N::Int, B <: BasisSet} <: BasisSet

Represents an `N`-particle fermion space, constructed out as a `N`-fold antisymmetrized
tensor product of the single-particle basis set `B`.
"""
struct FermionSpace{N,B <: BasisSet} <: BasisSet
    b :: B

    function FermionSpace(b :: BasisSet, N :: Integer)
        length(b) >= N || error("N=$N too large for basis length(b)=$(length(b)).")
        N = convert(Int, N)
        new{N,typeof(b)}(b)
    end
end
Base.length(fs::FermionSpace{N}) where N = binomial(length(fs.b), N)
nparticles(::FermionSpace{N}) where N = N

struct Fermion1POperator{N, B <: BasisSet, O <: LinearOperator{B}} <: LinearOperator{FermionSpace{N,B}}
    b :: FermionSpace{N,B}
    op :: O
    function Fermion1POperator(fs::FermionSpace{N,B}, op::O) where {N, B <: BasisSet, O <: LinearOperator{B}}
        new{N,B,O}(fs, op)
    end
end
basis(op::Fermion1POperator) = op.b

function apply(i::Integer, op::Fermion1POperator, j::Integer) :: ComplexF64
    N = nparticles(basis(op))
    if i == j
        is = combinationunrank(N, i)
        sum(apply(i, op.op, i) for i = is)
    else
        is, js =  combinationunrank(N, i),  combinationunrank(N, j)
        scd = findcombinationdiff(is, js)
        if isnothing(scd)
            0.0
        else
            idx1, idx2 = scd
            apply(is[idx1], op.op, js[idx2]) * (-1)^(idx1 + idx2)
        end
    end
end


# Test @assume and macros on a model with conditioning.

using Turing
using Test

@model test() = begin
  z ~ Normal(0,1)
  x ~ Bernoulli(1)
  1 ~ Bernoulli(x / 2)
  0 ~ Bernoulli(x / 2)
  x
end

is  = IS(10000)
smc = SMC(10000)
pg  = PG(100,10)

res = sample(test(), is)

@test reduce(&, res[:x]) == 1  #c heck that x is always 1
@test res[:logevidence] ≈ 2 * log(0.5)

res = sample(test(), smc)

@test reduce(&, res[:x]) == 1  #c heck that x is always 1
@test res[:logevidence] ≈ 2 * log(0.5)

res = sample(test(), pg)

@test reduce(&, res[:x]) == 1  # check that x is always 1
# PG does not provide logevidence estimate


using Jchemo

n = 50 ; p = 7
Xtrain = rand(n, p) ; ytrain = rand(n)
m = 3
Xtest = rand(m, p) ; ytest = rand(m)

fm = svmr(Xtrain, ytrain) ;
#fm = svmr(Xtrain, ytrain; cost = .1, gamma = 10) ;
#fm = svmr(Xtrain, ytrain; kern = "kpol", degree = 2) ;
res = Jchemo.predict(fm, Xtest)
res.pred
rmsep(res.pred, ytest)


using DONEs
using Random
using LinearAlgebra

Random.seed!(1)

n = 3
rfe = RFE(20,n,0.1)

lb = -ones(n)
ub = ones(n)
σs = 0.1 # surrogate exploration
σf = 0.1 # function exploration
done = DONE(rfe, lb, ub, σs, σf)

f(x) = dot(x.-0.1,x.-0.1) + 0.05*randn() # simple quadratic function
N = 40
for i in 1:N
    xi = new_input(done)
    yi = f(xi)
    add_measurement!(done, xi, yi)
    update_optimal_input!(done)
end

println("Estimated minimum:")
evaluateRFE(rfe, done.current_optimal_x) |> display
println("Estimated minimizer")
done.current_optimal_x |> display


using ReservoirComputing

const approx_res_size = 12
const alpha = 1.0
const beta = 1*10^(-5)
const extended_states = false
const nla_type = NLADefault()
const activation = tanh

const input_weight = 0.1
const cyrcle_weight = 0.99
const jump_weight = 0.1
const jumps = 12

const memory_size = 16

input = ones(Float64, 10, 2)
output = ones(Float64, 10, 2)

W = CRJ(approx_res_size, cyrcle_weight, jump_weight, jumps)
W_in = irrational_sign_input(approx_res_size, size(input, 2), input_weight)

rmm = RMM(W, input, output, W_in, memory_size, beta,
        extended_states = extended_states,
        nla_type = nla_type,
        activation = activation,
        alpha = alpha)

@test isequal(approx_res_size, rmm.res_size)
@test isequal(rmm.in_size, size(input, 2))
@test isequal(rmm.out_size, size(output, 2))
@test isequal(input, rmm.train_data)
@test isequal(alpha, rmm.alpha)
@test isequal(nla_type, rmm.nla_type)
@test isequal(activation, rmm.activation)
@test size(rmm.W) == (rmm.res_size, rmm.res_size)
@test size(rmm.W_in) == (rmm.res_size, rmm.in_size)
@test isequal(rmm.extended_states, extended_states)
@test size(rmm.W_out) == (rmm.out_size, rmm.res_size+rmm.in_size)
#test direct predict
rmmout = RMMdirect_predict(rmm, input)
@test size(rmmout) == size(input)


module TrustPilotCalc

export getTrustScore, playBack

using Dates
using JuliaDB

# references
# https://support.trustpilot.com/hc/en-us/articles/201748946
# http://www.ebc.cat/2015/01/05/how-to-rank-restaurants/
# https://www.evanmiller.org/bayesian-average-ratings.html
# https://medium.com/district-data-labs/computing-a-bayesian-estimate-of-star-rating-means-651496a890ab

"""
TrustPilot review structure
"""
struct TPReview
    author_id::AbstractString
    stars::Float64
    created_at::DateTime
end

"""
TrustPilot score structure
"""
struct TPScore
    score::Float64
    stars::Float64
    date::DateTime
end

"""
  getReviewWeightStatic(review_date, last_review_date)

static variant of review weighting function assuming no decay.
"""
function getReviewWeightStatic(reviewed_at::DateTime, last_reviewed_at::DateTime)
    milli_month = 2592000000
    delta = round((last_reviewed_at - reviewed_at).value / milli_month)
    return if delta < 1 1
        elseif delta < 3 .8
        elseif delta < 6 .5
        elseif delta < 12 .3
        else .1
        end
end

"""
  show(result)

print values of result
"""
function show(result::TPScore)
    printstyled(result.score, " | ", result.stars, " | ", result.date, "\n", color = :blue)
end

"""
  getReviewWeightHL(review_date, last_review_date)

exponential decay (half-life) variant of the `getReviewWeight` function
"""
function getReviewWeightHL(reviewed_at::DateTime, last_reviewed_at::DateTime)
    milli_month = 2592000000 # a month in millis
    initial_weight = 1 # starting weight
    hl = 0.5 # half life
    hlt = 6 # half life t
    delta = round((last_reviewed_at - reviewed_at).value / milli_month)
    weight = initial_weight * exp((log(hl) / hlt) * delta)
    return weight >= 1 ? 1 : weight
end

"""
  getTrustScore(reviews_array)

calculates TrustPilot score given an array of reviews.

returns a `TPScore` object
"""
function getTrustScore(reviews::Array{TPReview})
    first_reviewed_at = mapreduce(x -> x.created_at, min, reviews)
    last_reviewed_at = mapreduce(x -> x.created_at, max, reviews)
    base_reviews = fill(TPReview("seed", 3.5, first_reviewed_at), 7) # avg seed
    all_reviews = [base_reviews; reviews]
    all_weights = mapreduce(x -> getReviewWeightHL(x.created_at, last_reviewed_at), +, all_reviews)
    R = mapreduce(x -> (x.stars * getReviewWeightHL(x.created_at, last_reviewed_at)), +, all_reviews) / all_weights # average user rating for this product
    C = 3.5 # average user rating for all products (seed) aka 3.5
    v = length(reviews)
    m = length(base_reviews)
    w = 1 #v/(v + m) # weight  for R... v/(v + m) v = n rating for this product, m = ... all products aka 7

    S = w * R + (1 - w) * C
    return TPScore(round(S, digits=1), scoreToStars(S), last_reviewed_at)
end

"""
  scoreToStars(trustPilot_score)

rounds TrustPilot score to its equivalent stars value.
"""
function scoreToStars(score::Float64)
    score = round(score, digits=1)
    rem = round(score % .5, digits = 1)
    stars = rem <= .2 ? score - rem : score + (.5 - rem)
    return stars < 1 ? 1 : stars
end

"""
  playBack(csv_file)

imports a TrustPilot reviews CSV export and calculates a TrustPilot score.
"""
function playBack(filename::String; max_yearmon = round(Dates.now(), Dates.Month), schema = (:review_author_id, :review_created_at_utc, :review_star))
    raw_data = loadtable(filename; type_detect_rows = 500)
    working_data = select(raw_data, schema)
    working_data = filter(x -> x.review_created_at_utc < max_yearmon, working_data)
    sort!(working_data, :review_created_at_utc)
    all_reviews = map(x -> TPReview(x.review_author_id, x.review_star, x.review_created_at_utc), working_data)
    return getTrustScore(all_reviews)
end

function playBack(dataset::IndexedTable; max_yearmon = round(Dates.now(), Dates.Month), schema = (:review_author_id, :review_created_at_utc, :review_star))
    raw_data = dataset
    working_data = select(raw_data, schema)
    working_data = filter(x -> x.review_created_at_utc < max_yearmon, working_data)
    sort!(working_data, :review_created_at_utc)
    all_reviews = map(x -> TPReview(x.review_author_id, x.review_star, x.review_created_at_utc), working_data)
    return getTrustScore(all_reviews)
end

end # module


export pwc_density, pwc_densities, pwc_densities!

"""
    pwc_density(x::AbstractVector)

Returns the piecewise constant probability density from the quantile particle positions.

Let the quantile particles be at positions `x₀, x₁, …, xₙ` (remember that in Julia
they are actually `x[1], x[2], ..., x[n], x[n+1]`).
The returned array `R` is such that `R[1] = R[n+2] = 0` and `R[i] = 1 / (N * (x[i] - x[i-1]))`
for the intermediate indices (this formula holds also for the first and last entry
if we assume `x[0] = -∞` and `x[n+2] = ∞`).

# Examples

```jldoctest
julia> pwc_density([0, 1, 3])
4-element Vector{Float64}:
 0.0
 0.5
 0.25
 0.0
```
"""
function pwc_density(x::AbstractVector{<:Real})
    len = length(x)
    R = Array{float(eltype(x))}(undef, len+1)
    R[1] = R[end] = 0
    d = 1 / (len-1)
    for i in 2:len
        R[i] = d / (x[i] - x[i-1])
    end
    R
end

"""
    pwc_density(x::AbstractVector, y::AbstractVector)

The returned `x_dens` is indexed as `x_dens[x_or_y, left_or_right, i]`.
"""
function pwc_density(x::AbstractVector{<:Real}, y::AbstractVector{<:Real})
    x_len, y_len = length(x), length(y)
    T = promote_type(float(eltype(x)), float(eltype(y)))
    x_dens = Array{T, 3}(undef, (2, 2, x_len))
    y_dens = Array{T, 3}(undef, (2, 2, y_len))
    # indices of current particles being examined
    x_i::Int, y_i::Int = 1, 1
    # current densities
    x_d::T, y_d::T = zero(T), zero(T)
    # normalization factors
    x_f::T, y_f::T = 1 / (x_len - 1), 1 / (y_len - 1)
    # @debug "Entering loop" x_len y_len
    @inbounds while x_i <= x_len || y_i <= y_len
        if y_i > y_len || x_i <= x_len && x[x_i] < y[y_i]
            # @debug "Iteration x < y" x_i y_i
            x_dens[1, 1, x_i] = x_d
            x_dens[2, 1, x_i] = y_d
            if x_i < x_len
                x_d = x_f / (x[x_i + 1] - x[x_i])
            else
                x_d = zero(T)
            end
            x_dens[1, 2, x_i] = x_d
            x_dens[2, 2, x_i] = y_d
            x_i += 1
        elseif x_i > x_len || y_i <= y_len && y[y_i] < x[x_i]
            # @debug "Iteration y < x" x_i y_i
            y_dens[1, 1, y_i] = x_d
            y_dens[2, 1, y_i] = y_d
            if y_i < y_len
                y_d = y_f / (y[y_i + 1] - y[y_i])
            else
                y_d = zero(T)
            end
            y_dens[1, 2, y_i] = x_d
            y_dens[2, 2, y_i] = y_d
            y_i += 1
        else
            # @debug "Iteration x == y" x_i y_i
            x_dens[1, 1, x_i] = x_d
            x_dens[2, 1, x_i] = y_d
            y_dens[1, 1, y_i] = x_d
            y_dens[2, 1, y_i] = y_d
            if x_i < x_len
                x_d = x_f / (x[x_i + 1] - x[x_i])
            else
                x_d = zero(T)
            end
            if y_i < y_len
                y_d = y_f / (y[y_i + 1] - y[y_i])
            else
                y_d = zero(T)
            end
            x_dens[1, 2, x_i] = x_d
            x_dens[2, 2, x_i] = y_d
            y_dens[1, 2, y_i] = x_d
            y_dens[2, 2, y_i] = y_d
            x_i += 1
            y_i += 1
        end
    end
    (x_dens, y_dens)
end

"""
    pwc_densities(xs::AbstractVector{<:Real}...)
    pwc_densities(xs::Tuple{Vararg{AbstractVector{<:Real}}})

Given a list of `n` families of particles of lengths `l₁, …, lₙ`,
returns an `n`-tuple whose `s`-th entry is an `n×2×lₛ` array `densₛ`.

This array contains the information about the densities of all the species
around the particles of the `s`-th species.
This array is indexed as `[o,side,i]`, where `o∈{1, …, n}` is the index of the other
species whose density we want to know, `side` is either `1` if we want to know
the left density or `2` for the right density, and `i∈{1, …, lₛ}` is the index
of the particle.

In other words, `densₛ[o,side,i]` is the density of the `o`-th species on the left (`side=1`)
or right (`side=2`) of the `i`-th particle of the `s`-th species.

See also [`pwc_densities!`](@ref) for an inplace version.

# Examples

```jldoctest
julia> pwc_densities([0,1,2], [0,2,3])
([0.0 0.5; 0.0 0.25]

[0.5 0.5; 0.25 0.25]

[0.5 0.0; 0.25 0.5], [0.0 0.5; 0.0 0.25]

[0.5 0.0; 0.25 0.5]

[0.0 0.0; 0.5 0.0])
```
"""
function pwc_densities end

pwc_densities(xs::AbstractVector{<:Real}...) = pwc_densities(xs)

function pwc_densities(xs::Tuple{Vararg{AbstractVector{<:Real}}})
    N = nfields(xs)
    T = promote_type(float.(eltype.(xs))...)
    len = length.(xs)
    dens = map(x -> new_undef_densities(T, N, length(x)), xs)::NTuple{N, Array{T, 3}}
    pwc_densities!(dens, xs)
end

"""
    pwc_densities!(dens, xs::AbstractVector{<:Real}...)
    pwc_densities!(dens, xs::Tuple{Vararg{AbstractVector{<:Real}}})

This is an inplace version of [`pwc_densities`](@ref) that writes the result in `dens`.

See the documentation of [`pwc_densities`](@ref) for an explanation.
"""
function pwc_densities! end

pwc_densities!(dens, xs::AbstractVector{<:Real}...) = pwc_densities!(dens, xs)

function pwc_densities!(dens, xs::Tuple{Vararg{AbstractVector{<:Real}}})
    N = nfields(xs)
    T = promote_type(float.(eltype.(xs))...)
    len = length.(xs)
    # indices of current particles being examined
    ind = ones(Int, N)
    # current densities
    ds = zeros(T, N)
    # normalization factors
    fs::NTuple{N, T} = 1 ./ (len .- 1)
    # @debug "Entering loop" T len ds fs
    i_min = Int[0]
    @inbounds while true
        n_mins::Int = 0
        for i in 1:N
            if ind[i] <= len[i]
                if n_mins == 0 || xs[i][ind[i]] < xs[i_min[1]][ind[i_min[1]]]
                    i_min[1] = i
                    n_mins = 1
                elseif xs[i][ind[i]] <= xs[i_min[1]][ind[i_min[1]]]
                    n_mins += 1
                    if n_mins <= length(i_min)
                        i_min[n_mins] = i
                    else
                        push!(i_min, i)
                    end
                end
            end
        end
        if n_mins == 0
            break
        end
        i_mins = @view i_min[1:n_mins]
        # let ind = (ind...,), i_min = (i_min...,), i_mins = (i_mins...,)
        #     @debug "Iteration" ind i_min n_mins i_mins
        # end
        for i in i_mins
            # @debug "Assigning left" i ind[i] ds
            # dens[i][:, 1, ind[i]] = ds
            for s in 1:N
                dens[i][s, 1, ind[i]] = ds[s]
            end
        end
        for i in i_mins
            if ind[i] < length(xs[i])
                ds[i] = fs[i] / (xs[i][ind[i]+1] - xs[i][ind[i]])
            else
                ds[i] = zero(T)
            end
            # ds[i] = right_local_density(xs[i], ind[i], fs[i])
        end
        for i in i_mins
            # @debug "Assigning right" i ind[i] ds
            # dens[i][:, 2, ind[i]] = ds
            for s in 1:N
                dens[i][s, 2, ind[i]] = ds[s]
            end
        end
        # ind[i_mins] .+= 1
        for i in i_mins
            ind[i] += 1
        end
    end
    dens
end

new_undef_densities(T::Type, N::Int, len::Int) = Array{T, 3}(undef, (N, 2, len))


include("helpers.jl")
include("algo_util.jl")

abstract type ZerothOrder end

function solve(M::ZerothOrder, f, x, max_iters; num_eval_termination=true)
    init!(M, x)
    x_hist = [x]
    y, terminate = f(x), false
    while !terminate
        x, y, terminate = step!(M, f, x, y)
        push!(x_hist, x)
        if num_eval_termination && (count(f) >= max_iters - M.evals_per_iter)
            break
        end
    end
    return x, x_hist
end

# Coordinate Descent
Base.@kwdef mutable struct CoordinateDescentAcceleration <: ZerothOrder
    ϵ = 1e-4
    approx_line_search = backtracking_line_search
    n = nothing
end
function init!(M::CoordinateDescentAcceleration, x)
    M.n = length(x)
end
function step!(M::CoordinateDescentAcceleration, f, ∇f, x)
    ϵ, n, approx_line_search = M.ϵ, M.n, M.approx_line_search
    terminate = false

    x_old = copy(x)
    for i in 1:n
        d = basis(i,n)
        α = backtracking_line_search(f, ∇f, x, d, 1e-2)
        x += α * d
    end
    α = backtracking_line_search(f, ∇f, x, x - x_old, 1e-2)
    x += α * (x - x_old)

    if abs(norm(x - x_old)) <= ϵ
        terminate = true
    end
    return x, terminate
end

function solve(M::CoordinateDescentAcceleration, f, ∇f, x, max_iters; num_eval_termination=true)
    init!(M, x)
    x_hist = [x]
    terminate = false
    while !terminate
        x, terminate = step!(M, f, ∇f, x)
        push!(x_hist, x)
        if num_eval_termination && (count(f) >= max_iters - M.evals_per_iter)
            break
        end
    end
    return x, x_hist
end

# Hooke Jeeves
Base.@kwdef mutable struct HookeJeeves <: ZerothOrder
    α = 1e-2
    ϵ = 1e-4
    γ = 0.5
    n = nothing
    evals_per_iter = nothing
end
function init!(M::HookeJeeves, x)
    M.n = length(x)
    M.evals_per_iter = 2 * M.n
end
function step!(M::HookeJeeves, f, x, y)
    α, ϵ, γ, n = M.α, M.ϵ, M.γ, M.n
    improved, terminate = false, false

    x_best, y_best = x, y
    xs_new = [x + sgn * α * basis(i,n) for i in 1:n for sgn in (-1, +1)]
    ys_new = [f(x_new) for x_new in xs_new]
    y_best_iter, y_best_iter_idx = findmin(ys_new)

    if y_best_iter < y_best
        x_best = xs_new[y_best_iter_idx]
        y_best = y_best_iter
        improved = true
    end

    M.α *= (!improved ? γ : 1)
    terminate = (M.α <= ϵ ? true : false)
    return x_best, y_best, terminate
end

# Hooke Jeeves Dynamic w/ Eager execution
Base.@kwdef mutable struct HookeJeevesDynamic <: ZerothOrder
    α = 1e-2
    ϵ = 1e-4
    γ = 0.5
    n = nothing
    evals_per_iter = nothing
    D = nothing # directions to search in, need to store it for permutating order
end
function init!(M::HookeJeevesDynamic, x)
    M.n = length(x)
    M.evals_per_iter = 2 * M.n
    M.D = [sgn * basis(i, M.n) for i in 1:M.n for sgn in (-1, +1)]
end
function step!(M::HookeJeevesDynamic, f, x, y, idx_best_prev)
    α, ϵ, γ = M.α, M.ϵ, M.γ
    improved, terminate, idx_best = false, false, 1

    x_best, y_best = x, y
    d_best_prev = M.D[idx_best_prev]
    M.D = pushfirst!(deleteat!(M.D, idx_best_prev), d_best_prev)
    xs_new = [x + α * d for d in M.D]

    for (idx, x_new) in enumerate(xs_new)
        y_new = f(x_new)
        if y_new < y_best
            x_best, y_best = x_new, y_new
            improved = true
            idx_best = idx
            break
        end
    end

    M.α *= (!improved ? γ : 1)
    terminate = (M.α <= ϵ ? true : false)
    return x_best, y_best, terminate, idx_best
end

function solve(M::HookeJeevesDynamic, f, x, max_iters; num_eval_termination=true)
    init!(M, x)
    x_hist = [x]
    y, terminate, idx_best = f(x), false, 1

    while !terminate
        x, y, terminate, idx_best = step!(M, f, x, y, idx_best)
        push!(x_hist, x)

        if num_eval_termination && (count(f) >= max_iters - M.evals_per_iter)
            break
        end
    end
    return x, x_hist
end

# one-off function to generate cool plot. do not actually use it.
function solve_diag(M::HookeJeevesDynamic, f, x, max_iters; num_eval_termination=true)
    init!(M, x)
    M.D = [[√2, √2], [-√2, -√2], [-√2, √2], [√2, -√2]]
    x_hist = [x]
    y, terminate, idx_best = f(x), false, 1

    while !terminate
        x, y, terminate, idx_best = step!(M, f, x, y, idx_best)
        push!(x_hist, x)

        if num_eval_termination && (count(f) >= max_iters - M.evals_per_iter)
            break
        end
    end
    return x, x_hist
end


# Stationary voltage density (and stationary rate) for a single LIF neuron
# without refractoriness!

push!(LOAD_PATH, abspath(@__DIR__,".."))
using LinearAlgebra,Statistics,StatsBase
using Plots,NamedColors ; theme(:dark)
using SparseArrays
using SpikingRNNs; const global S = SpikingRNNs
using ProgressMeter

function onesparsemat(w::Real)
  mat=Matrix{Float64}(undef,1,1) ; mat[1,1]=w
  return sparse(mat)
end

## Time parameters
dt = 1E-4
Ttot = 30.

## start with LIF model

# LIF neuron parameters
myτ = 0.5
vth = 10.
v_r = -5.0
τrefr = 0.5 # refractoriness
τpcd = 0.2 # post synaptic current decay
myinput = 20.0
mynt = S.NTLIF(myτ,vth,v_r,τrefr,τpcd)
myps = S.PSLIF(mynt,1)

## one static input above threshold
hw_in=20.0
in_state = S.PSSimpleInput(S.InputSimpleOffset(hw_in))
mypop = S.Population(myps,(S.FakeConnection(),),(in_state,))

## that's it, let's make the network
myntw = S.RecurrentNetwork(dt,(mypop,))

## Record spike number and internal potential
myrec1 = S.RecStateNow(myps,10,dt,Ttot)
myrec2 = S.RecCountSpikes(myps,dt)

## Run the simulation
times = (0:myntw.dt:Ttot)
nt = length(times)
# initial conditions
myps.state_now[1] = v_r

S.reset!.([myrec1,myrec2])

for (k,t) in enumerate(times)
  S.dynamics_step!(t,myntw)
  myrec1(t,k,myntw)
  myrec2(t,k,myntw)
end
r0 = S.get_mean_rates(myrec2,dt,Ttot)[1]
## show internal membrane potential
_ = let tshow = (0.,5.)
  ts = myrec1.times
  y = myrec1.state_now[1,:]
  idx_keep = findall(t->tshow[1]<= t <= tshow[2],ts)
  plot(ts[idx_keep],y[idx_keep];leg=false,linewidth=2)
end
##

# now, use Fokker-Planck theory and Richardson method
# it is hardcoded for now... maybe later I will convert it to
# a function
dv = 1E-3
v_lb = v_r - 0.1
ndv = round(Int64,(vth - v_lb)/dv)
k_re = round(Int64,(v_r - v_lb)/dv)
ks = collect(0:ndv)
Vks = @. v_lb + ks * dv
js = fill(NaN,ndv+1)
ps = fill(NaN,ndv+1)

ksrev = collect(ndv+1:-1:2)

js[ndv+1]=1.0
ps[ndv+1]=0.0

for k in ksrev
  krond = k==k_re ? 1.0 : 0.0
  js[k-1] = js[k] - krond
  ps[k-1] = js[k-1] * mynt.τ / (myinput-Vks[k])
end

r0 = inv(dv*sum(ps))

##
_ = let plt = plot(;xlabel="membrane potential",ylabel="prob. density",
    leg=:topleft)
  histogram!(plt,myrec1.state_now[:];nbins=100,normalize=true,label="simulation")
  plot!(plt,Vks,r0.*ps;color=:white,linewidth=2,label="Fokker-Plank")
end

## Let's do an f-I curve with this

function f_lif_num(h_in;dt=1E-4,Ttot=10.0)
  in_state = S.PSSimpleInput(S.InputSimpleOffset(h_in))
  mypop = S.Population(myps,(S.FakeConnection(),),(in_state,))
  myntw = S.RecurrentNetwork(dt,(mypop,))
  rec = S.RecCountSpikes(myps,dt)
  ## Run!
  times = (0:myntw.dt:Ttot)
  # initial conditions
  myps.state_now[1] = v_r
  S.reset!(rec)
  for (k,t) in enumerate(times)
    S.dynamics_step!(t,myntw)
    rec(t,k,myntw)
  end
  r0 = S.get_mean_rates(rec,dt,Ttot)[1]
  return r0
end

function f_lif_semianalitic(h_in;dv=1E-3)
  v_lb = v_r - 0.1
  ndv = round(Int64,(vth - v_lb)/dv)
  k_re = round(Int64,(v_r - v_lb)/dv)
  ks = collect(0:ndv)
  Vks = @. v_lb + ks * dv
  js = fill(NaN,ndv+1)
  ps = fill(NaN,ndv+1)
  ksrev = collect(ndv+1:-1:2)

  js[ndv+1]=1.0
  ps[ndv+1]=0.0
  for k in ksrev
    krond = k==k_re ? 1.0 : 0.0
    js[k-1] = js[k] - krond
    ps[k-1] = js[k-1] * mynt.τ / (h_in-Vks[k])
  end
  return inv(dv*sum(ps))
end

##
_ = let Ttot = 50.0
  nsim = 40
  nfp = 200
  inputs_sim = range(vth+0.01,80.0;length=nsim)
  inputs_fp = range(vth+0.01,80.0;length=nfp)
  y1 = @showprogress map(in->f_lif_num(in;Ttot=Ttot),inputs_sim)
  y2 = @showprogress map(in->f_lif_semianalitic(in),inputs_fp)
  plt = plot(leg=:bottomright,xlabel="input current",ylabel="stationary rate")
  scatter!(plt,inputs_sim,y1;label="simulation")
  plot!(plt,inputs_fp,y2; label="Fokker-Planck",linewidth=2)
end

#####################################
#####################################
## now a LIF neuron with a gaussian noisy input
# Time parameters
dt = 1E-4
Ttot = 500.
# LIF neuron parameters
myτ = 0.5
v_th = 10.
v_r = -5.0
τrefr = 0.0 # refractoriness
τpcd = 0.2 # post synaptic current decay
myinput = 7.
myσ = 2.0
mynt = S.NTLIF(myτ,v_th,v_r,τrefr,τpcd)
myps = S.PSLIF(mynt,1)

## inputs here
# one static input
in_state = S.PSSimpleInput(S.InputSimpleOffset(myinput))
# ... plus noise
in_state_noise = S.PSSimpleInput(S.InputIndependentNormal(myσ))
##
mypop = S.Population(myps,(S.FakeConnection(),S.FakeConnection()),
    (in_state,in_state_noise))

## that's it, let's make the network
myntw = S.RecurrentNetwork(dt,(mypop,))

## Recorder
rec1_twup=50.0 # further away from initial conditions
myrec1 = S.RecStateNow(myps,10,dt,Ttot;t_warmup=rec1_twup)
myrec2 = S.RecCountSpikes(myps,dt)

## Run!
times = (0:myntw.dt:Ttot)
nt = length(times)
# initial conditions
myps.state_now[1] = v_r

S.reset!.([myrec1,myrec2])

for (k,t) in enumerate(times)
  S.dynamics_step!(t,myntw)
  myrec1(t,k,myntw)
  myrec2(t,k,myntw)
end
r0 = S.get_mean_rates(myrec2,dt,Ttot)[1]
@info mean_and_std(myrec1.state_now[:])
##
_ = let tshow = (0.,8.)
  tshow = tshow .+ rec1_twup
  ts = myrec1.times
  y = myrec1.state_now[1,:]
  idx_keep = findall(t->tshow[1]<= t <= tshow[2],ts)
  plot(ts[idx_keep],y[idx_keep];leg=false,linewidth=2,xlabel="time",
    ylabel="membrane potential")
end

## now, use Fokker-Planck theory and Richardson method
# it is hardcoded for now... maybe later I will convert it to
# a function
dv = 1E-3
v_lb = v_r - 2.0
ndv = round(Int64,(v_th - v_lb)/dv)
k_re = round(Int64,(v_r - v_lb)/dv)
ks = collect(0:ndv)
Vks = @. v_lb + ks * dv
js = fill(NaN,ndv+1)
ps = fill(NaN,ndv+1)

ksrev = collect(ndv+1:-1:2)

js[ndv+1]=1.0
ps[ndv+1]=0.0

for k in ksrev
  krond = k==k_re ? 1.0 : 0.0
  js[k-1] = js[k] - krond
  Gk = (Vks[k]-myinput)/(myσ^2)
  Hk = myτ*js[k]/(myσ^2)
  Ak = exp(dv*Gk)
  Bk = Gk==0 ? dv*Hk : Hk/Gk*(Ak-1)
  ps[k-1] = ps[k]*Ak + Bk
end

r0 = inv(dv*sum(ps))

_ = let plt = plot(;xlabel="membrane potential",ylabel="prob. density",
    leg=:topleft)
  histogram!(plt,myrec1.state_now[:];nbins=100,normalize=true,label="simulation")
  plot!(plt,Vks,r0.*ps;color=:white,linewidth=2,label="Fokker-Plank")
end


#=

  From BLOG examples/mixture-of-gaussian-infinite.blog
  """
  Gaussian mixture model with infinite number of components

  @author leili
  @date 2014-07-10
  """

  Note: The BLOG model remove numComponent == 0 via a condition.
        Here we remove it using Truncated(Poisson(2),0,Inf)

  Summary Statistics
    parameters      mean       std   naive_se       mcse         ess       rhat   ess_per_sec
        Symbol   Float64   Float64    Float64   Float64?    Float64?   Float64?      Float64?

  numComponent    2.2097    1.2555     0.0126     0.0179   4752.1622     0.9999     1866.5209
          z[1]    1.6089    0.9319     0.0093     0.0121   4800.4385     0.9999     1885.4825
          z[2]    1.6034    0.9092     0.0091     0.0128   5013.1564     0.9999     1969.0324
          z[3]    1.6042    0.9161     0.0092     0.0134   5163.1008     0.9999     2027.9265
          z[4]    1.5902    0.8956     0.0090     0.0136   4864.7405     1.0001     1910.7386
         mu[1]    0.2789    0.4861     0.0049     0.0064   5152.2290     1.0000     2023.6563
         mu[2]    0.2106    0.5170     0.0064     0.0090     missing    missing       missing
         mu[3]    0.1611    0.5371     0.0093     0.0134     missing    missing       missing
         mu[4]    0.1342    0.5243     0.0138     0.0170     missing    missing       missing
     x_post[1]    0.3065    1.1066     0.0111     0.0121   8080.3308     1.0000     3173.7356
     x_post[2]    0.3533    1.1055     0.0111     0.0116   8837.3254     1.0001     3471.0626
     x_post[3]    0.3147    1.0961     0.0110     0.0105   9327.8597     0.9999     3663.7312
     x_post[4]    0.3229    1.0999     0.0110     0.0120   8585.7202     0.9999     3372.2389
         mu[5]    0.0978    0.5476     0.0237     0.0348     missing    missing       missing
         mu[6]    0.0579    0.5407     0.0401    missing     missing    missing       missing
         mu[7]    0.0565    0.4821     0.0568    missing     missing    missing       missing
         mu[8]    0.3198    0.4958     0.0891    missing     missing    missing       missing
         mu[9]    0.1723    0.4504     0.2014    missing     missing    missing       missing

  Distributions of variable numComponent (num:0)
  1.00000 =>    3517  (0.351700)
  2.00000 =>    3139  (0.313900)
  3.00000 =>    1900  (0.190000)
  4.00000 =>     908  (0.090800)
  5.00000 =>     354  (0.035400)
  6.00000 =>     110  (0.011000)
  7.00000 =>      41  (0.004100)
  8.00000 =>      26  (0.002600)
  9.00000 =>       5  (0.000500)

  Cf ~/webppl/mixture_of_gaussian_infinite.wppl

=#

using Turing, StatsPlots
include("jl_utils.jl")

@model function mixture_of_gaussian_infinite(x=[0.2,1.0,0.5,0.6])
    n = length(x)

    # Note: We have to set 0..Inf (not 1..Inf), otherwise
    # the value 1 will not be drawn.
    numComponent ~ truncated(Poisson(2),0,Inf)
    z ~ filldist(DiscreteUniform(1,numComponent),n)
    mu ~ filldist(Uniform(-1,1), numComponent)

    for i in 1:n
        x[i] ~ Normal(mu[z[i]],1.0)
    end

    x_post = tzeros(n) # Vector{Real}(undef, n)
    for i in 1:n
        x_post[i] ~ Normal(mu[z[i]],1.0)
    end

end

x=[0.2,1.0,0.5,0.6]
model = mixture_of_gaussian_infinite(x)

# chns = sample(model, Prior(), 10_000)
# chns = sample(model, MH(), 10_000)
# chns = sample(model, PG(15), 10_000)
chns = sample(model, SMC(), 10_000)
# chns = sample(model, IS(), 10_000)

# chns = sample(model, NUTS(), 10_000)

display(chns)
# display(plot(chns))

show_var_dist_pct(chns, :numComponent)