function create_first_global_cluster(hyperparams::model_hyper_params, groups_dict::Dict, cluster_index::Int64)
    pts = Vector{AbstractArray{Float64,2}}()
    sub_labels = Vector{AbstractArray{Int64,2}}()
    pts_to_groups = Vector{AbstractArray{Int64,1}}()
    count = 0
    for (k,v) in groups_dict
        push!(pts,(@view v.points[1:hyperparams.local_dim-1,:]))
        push!(sub_labels,v.labels_subcluster)
        push!(pts_to_groups,ones(Int64,(size(pts[end],2)))*k)
        for c in v.local_clusters
            if c.globalCluster == cluster_index
                count +=1
            end
        end
    end
    suff = create_sufficient_statistics(hyperparams.global_hyper_params, [])
    # suff = nothing
    post = hyperparams.global_hyper_params
    dist = sample_distribution(post)
    cp = cluster_parameters(hyperparams.global_hyper_params, dist, suff, post)
    cpl = deepcopy(cp)
    cpr = deepcopy(cp)
    splittable = splittable_cluster_params(cp,cpl,cpr,[0.5,0.5], false, ones(20).*-Inf)
    all_pts = reduce(hcat,pts)
    all_sub_labels = reduce(vcat,sub_labels)[:]
    all_pts_to_group = reduce(vcat,pts_to_groups)[:]
    update_splittable_cluster_params!(splittable,all_pts[1:hyperparams.local_dim-1,:],
        all_sub_labels,
         true, all_pts_to_group)
    # println(splittable)
    # println(count)
    cluster = global_cluster(splittable, hyperparams.total_dim, hyperparams.local_dim,splittable.cluster_params.suff_statistics.N,count,[1,1])
    return cluster
end

function get_p_for_point(x)
    x = log.(exp.(x .- maximum(x)) ./ exp(sum(x .- maximum(x))))
    x ./= sum(x)
    return log.(x)
end

function sample_group_cluster_labels(group_num::Int64, weights::AbstractArray{Float64, 1},final::Bool)
    group = groups_dict[group_num]
    points = group.points
    parr = zeros(length(group.labels), length(global_clusters_vector))
    for (k,v) in enumerate(global_clusters_vector)
        local_dim = v.local_dim
        log_likelihood!((@view parr[:,k]), points[1 : local_dim-1,:],v.cluster_params.cluster_params.distribution, group_num)
    end
    clusters_parr = zeros(length(group.local_clusters),length(global_clusters_vector))
    labels = zeros(Int64,length(group.local_clusters),1)
    for (i,c) in enumerate(group.local_clusters)
        relevant_arr = parr[(@view group.labels[:]) .== i,:]
        clusters_parr[i,:] .= sum(relevant_arr, dims = 1)[:] + log.(weights)
        sum_arr_trick = exp.(clusters_parr[i,:])
        testarr = clusters_parr[i,:] .- maximum(clusters_parr[i,:])
        testarr = exp.(testarr)
        if final
            labels[i,1] = argmax(clusters_parr[i,:][:])
        else
            labels[i,1] = sample(1:length(global_clusters_vector), ProbabilityWeights(testarr[:]))
        end
    end
    return labels
end

function sample_cluster_label(group::local_group, cluster::local_cluster ,i, weights::AbstractArray{Float64, 1},final::Bool)
    points = group.points[1 : cluster.local_dim - 1, @view (group.labels .== i)[:]]
    parr = zeros(size(points,2), length(global_clusters_vector))
    for (k,v) in enumerate(global_clusters_vector)
        log_likelihood!((@view parr[:,k]),points,v.cluster_params.cluster_params.distribution,group.group_num)
    end
    weights = reshape(weights,1,:)
    sum_arr = sum(parr,dims = 1)
    sum_arr .+= log.(weights)
    sum_arr_trick = exp.(sum_arr)
    testarr = sum_arr .- maximum(sum_arr)
    testarr = exp.(testarr)

    if final
        cluster.globalCluster = argmax(sum_arr[:])
    else
        cluster.globalCluster = sample(1:length(global_clusters_vector), ProbabilityWeights(testarr[:]))
    end
    # println("sum arr: " *string(sum_arr) * " parr:" * string(sum_arr_trick) * " testarr:" * string(testarr) * " choosen: " * string(cluster.globalCluster))
    return cluster.globalCluster
end

function sample_clusters_labels!(model::hdp_shared_features, final::Bool)
    labels_dict = Vector{Array}(undef,length(model.groups_dict))
    groups_count = zeros(length(global_clusters_vector))
    wvector = model.weights
    if mp
        @sync for (k,v) in model.groups_dict
            @async labels_dict[k] = @spawnat ((k % nworkers())+2) sample_group_cluster_labels(k, wvector, final)
            # labels_dict[k] = Dict()
            # for (i,c) in enumerate(v.local_clusters)
            #     # labels_dict[k][i] = @spawn sample_cluster_label(c, v.points[1 : v.model_hyperparams.local_dim - 1, (@view (v.labels .== i)[:])], model.weights, final)
            #     labels_dict[k][i] = sample_cluster_label(v,c, i, model.weights, final)
            # end
        end
    else
        Threads.@threads for k=1:length(model.groups_dict)
            labels_dict[k] = sample_group_cluster_labels(k, wvector, final)
        end
    end
    #at ((k % num_of_workers)+1)
    for (k,v) in model.groups_dict
        fetched_labels = fetch(labels_dict[k])
        for (i,c) in enumerate(v.local_clusters)
            c.globalCluster = fetched_labels[i,1]
            groups_count[c.globalCluster] += 1
        end
    end
    for (i,v) in enumerate(groups_count)
        global_clusters_vector[i].clusters_count = v
    end
end

function sample_sub_clusters!(model::hdp_shared_features)
    labels_dict = Dict()
    for v in global_clusters_vector
        v.clusters_sub_counts = [0,0]
    end
    for (k,v) in model.groups_dict
        labels_dict[k] = Vector{Array}(undef,length(model.groups_dict))
        if mp
            for (i,c) in enumerate(v.local_clusters)
                labels_dict[k][i] = @spawnat ((k % num_of_workers)+1) sample_cluster_sub_label(c, v.points[1 : v.model_hyperparams.local_dim - 1, (@view (v.labels .== i)[:])])
            end
        else
            Threads.@threads for i=1:length(v.local_clusters)
                c=local_clusters[i]
                labels_dict[k][i] = sample_cluster_sub_label(c, v.points[1 : v.model_hyperparams.local_dim - 1, (@view (v.labels .== i)[:])])
            end
        end
    end
    for (k,v) in model.groups_dict
        for (i,c) in enumerate(v.local_clusters)
            c.globalCluster_subcluster = fetch(labels_dict[k][i])
            global_clusters_vector[c.globalCluster].clusters_sub_counts[c.globalCluster_subcluster] += 1
        end
    end
end

function split_cluster!(model::hdp_shared_features, index::Int64, new_index::Int64)
    cluster = global_clusters_vector[index]
    new_cluster = deepcopy(cluster)
    new_cluster.cluster_params = create_splittable_from_params(cluster.cluster_params.cluster_params_r, model.model_hyperparams.γ)
    cluster.cluster_params = create_splittable_from_params(cluster.cluster_params.cluster_params_l, model.model_hyperparams.γ)
    new_cluster.points_count = new_cluster.cluster_params.cluster_params.suff_statistics.N
    cluster.points_count = cluster.cluster_params.cluster_params.suff_statistics.N
    global_clusters_vector[new_index] = new_cluster
end

function merge_clusters!(model::hdp_shared_features,index_l::Int64, index_r::Int64)
    new_splittable_cluster = merge_clusters_to_splittable(global_clusters_vector[index_l].cluster_params.cluster_params, global_clusters_vector[index_r].cluster_params.cluster_params, model.model_hyperparams.α)
    global_clusters_vector[index_l].cluster_params = new_splittable_cluster
    global_clusters_vector[index_l].clusters_count += global_clusters_vector[index_r].clusters_count
    global_clusters_vector[index_l].cluster_params.splittable = true
    global_clusters_vector[index_r].cluster_params.cluster_params.suff_statistics.N = 0
    global_clusters_vector[index_r].cluster_params.splittable = true
    global_clusters_vector[index_r].clusters_count = 0
end

function should_split!(should_split::AbstractArray{Float64,1},
        cluster_params::splittable_cluster_params,
        groups_dict::Dict,
        α::Float64,
        γ::Float64,
        c_count::Int64,
        lr_count::AbstractArray{Int64,1},
        index::Int64,
        glob_weight::Float64,
        final::Bool)
    cpl = cluster_params.cluster_params_l
    cpr = cluster_params.cluster_params_r
    cp = cluster_params.cluster_params
    if final || cpl.suff_statistics.N == 0 ||cpr.suff_statistics.N == 0 #||lr_count[1] == 0 || lr_count[2] == 0
        should_split .= 0
        return
    end
    log_likihood_l = log_marginal_likelihood(cpl.hyperparams, cpl. posterior_hyperparams, cpl.suff_statistics)
    log_likihood_r = log_marginal_likelihood(cpr.hyperparams, cpr. posterior_hyperparams, cpr.suff_statistics)
    log_likihood = log_marginal_likelihood(cp.hyperparams, cp. posterior_hyperparams, cp.suff_statistics)

    log_HR = log(γ) + logabsgamma(cpl.suff_statistics.N)[1] + log_likihood_l + logabsgamma(cpr.suff_statistics.N)[1] + log_likihood_r -(logabsgamma(cp.suff_statistics.N)[1] + log_likihood) +
        cp.suff_statistics.N*log(glob_weight)-cpl.suff_statistics.N*log(glob_weight*cluster_params.lr_weights[1])-cpr.suff_statistics.N*log(glob_weight*cluster_params.lr_weights[2])
    log_HR += get_groups_split_log_likelihood(groups_dict,
        index,
        cluster_params.lr_weights[1],
        cluster_params.lr_weights[2],
        glob_weight,
        α)

    if log_HR > log(rand())
        should_split .= 1
    end
end

function should_merge!(should_merge::AbstractArray{Float64,1},
        cpl::cluster_parameters,
        cpr::cluster_parameters,
        groups_dict::Dict,
        α::Float64,
        c1_count::Int64,
        c2_count::Int64,
        i::Int64,
        j::Int64,
        wi::Float64,
        wj::Float64,
        final::Bool)
    new_suff = aggregate_suff_stats(cpl.suff_statistics, cpr.suff_statistics)
    cp = cluster_parameters(cpl.hyperparams, cpl.distribution, new_suff,cpl.posterior_hyperparams)
    cp.posterior_hyperparams = calc_posterior(cp.hyperparams, cp.suff_statistics)
    log_likihood_l = log_marginal_likelihood(cpl.hyperparams, cpl.posterior_hyperparams, cpl.suff_statistics)
    log_likihood_r = log_marginal_likelihood(cpr.hyperparams, cpr.posterior_hyperparams, cpr.suff_statistics)
    log_likihood = log_marginal_likelihood(cp.hyperparams, cp.posterior_hyperparams, cp.suff_statistics)
    log_HR = -log(α) + logabsgamma(α)[1] -logabsgamma(wi*α)[1] -logabsgamma(wj*α)[1] + logabsgamma(cp.suff_statistics.N)[1] -logabsgamma(cp.suff_statistics.N + α)[1] + logabsgamma(cpl.suff_statistics.N + wi*α)[1]-logabsgamma(cpl.suff_statistics.N)[1]  -
        logabsgamma(cpr.suff_statistics.N)[1] + logabsgamma(cpr.suff_statistics.N + wj*α)[1]+
         log_likihood- log_likihood_l- log_likihood_r

    if (log_HR > log(rand())) || (final && log_HR > log(0.5))
        should_merge .= 1
    end
end

function get_groups_split_log_likelihood(groups_dict::Dict,
        global_cluster_index::Int64,
        lweight::Float64,
        rweight::Float64,
        glob_weight::Float64,
        γ::Float64)
    total_likelihood = 0.0
    for (k,group) in groups_dict
        lcount = 0.0
        rcount = 0.0
        for c in group.local_clusters
            if c.globalCluster == global_cluster_index
                lcount = c.global_suff_stats[1]
                rcount = c.global_suff_stats[2]
                if is_tp == false
                    total_likelihood += (logabsgamma(γ * glob_weight)[1] - logabsgamma(γ * glob_weight + lcount+ rcount)[1] +
                        logabsgamma(γ * glob_weight * lweight + lcount)[1] - logabsgamma(γ * glob_weight * lweight)[1] +
                        logabsgamma(γ * glob_weight * rweight + rcount)[1] - logabsgamma(γ * glob_weight * rweight)[1])
                else
                    total_likelihood += (logabsgamma(γ * glob_weight)[1] - logabsgamma( glob_weight + lcount+ rcount)[1] +
                        logabsgamma(γ * glob_weight * lweight + lcount)[1] - logabsgamma(glob_weight * lweight)[1] +
                        logabsgamma(γ * glob_weight * rweight + rcount)[1] - logabsgamma(glob_weight * rweight)[1])
                end
            end
        end

    end
    # println(total_likelihood)
    return total_likelihood
end

function get_groups_merge_log_likelihood(groups_dict::Dict,
        global_cluster_i::Int64,
        global_cluster_j::Int64,
        weight_i::Float64,
        weight_j::Float64,
        γ::Float64)
    total_likelihood = 0.0
    for (k,group) in groups_dict
        lcount = 0.0
        rcount = 0.0
        for c in group.local_clusters
            if c.globalCluster == global_cluster_i
                lcount += c.cluster_params.cluster_params_l.suff_statistics.N
            end
            if c.globalCluster == global_cluster_j
                rcount += c.cluster_params.cluster_params_r.suff_statistics.N
            end
        end
        total_likelihood += logabsgamma(γ * (weight_i+weight_j))[1] - logabsgamma(γ * (weight_i+weight_j)[1] + lcount+ rcount)[1] +
            logabsgamma(γ * weight_i + lcount)[1] - logabsgamma(γ * weight_i)[1] +
            logabsgamma(γ * weight_j + rcount)[1] - logabsgamma(γ * weight_j)[1]
    end
    return -total_likelihood
end

function check_and_split!(model::hdp_shared_features, final::Bool)
    split_arr= zeros(length(global_clusters_vector))
    for (index,cluster) in enumerate(global_clusters_vector)
        # println("index: " * string(index) * " splittable: " * string(cluster.cluster_params.splittable))
        if cluster.cluster_params.splittable == true
            should_split!((@view split_arr[index,:]),
                cluster.cluster_params,
                model.groups_dict,
                model.model_hyperparams.α,
                model.model_hyperparams.γ,
                cluster.clusters_count,
                cluster.clusters_sub_counts,
                index,
                model.weights[index],
                final)
            if  split_arr[index,:] == 1
                break
            end
        end
    end
    new_index = length(global_clusters_vector) + 1
    resize!(global_clusters_vector,Int64(length(global_clusters_vector) + sum(split_arr)))
    indices = Vector{Int64}()
    for i=1:length(split_arr)
        if split_arr[i,1] == 1
            split_cluster!(model,i, new_index)
            push!(indices,i)
            push!(indices,new_index)
            new_index += 1
        end
    end
    return indices
end

function check_and_merge!(model::hdp_shared_features, final::Bool)
    mergable = zeros(1)
    indices = Vector{Int64}()
    distance_matrix = zeros(length(global_clusters_vector),length(global_clusters_vector))
    if (@isdefined use_mean_for_merge) && use_mean_for_merge == true
        for i=1:length(global_clusters_vector)-1
            for j=i+1:length(global_clusters_vector)
                distance_matrix[i,j] = norm(global_clusters_vector[i].cluster_params.cluster_params.distribution.μ - global_clusters_vector[j].cluster_params.cluster_params.distribution.μ)
            end
            indice_to_check = argmin(distance_matrix[i,i+1:end])
            if (global_clusters_vector[i].cluster_params.splittable == true && global_clusters_vector[indice_to_check].cluster_params.splittable == true) || final
                should_merge!(mergable, global_clusters_vector[i].cluster_params.cluster_params,
                    global_clusters_vector[indice_to_check].cluster_params.cluster_params,model.groups_dict,
                    model.model_hyperparams.γ,global_clusters_vector[i].clusters_count,
                    global_clusters_vector[indice_to_check].clusters_count,i,indice_to_check,model.weights[i], model.weights[indice_to_check], final)
            end
            if mergable[1] == 1
                merge_clusters!(model, i, indice_to_check)
                push!(indices,i)
                push!(indices,indice_to_check)
                break
            end
        end
    else
        for i=1:length(global_clusters_vector)
            for j=i+1:length(global_clusters_vector)
                if (global_clusters_vector[i].cluster_params.splittable == true && global_clusters_vector[j].cluster_params.splittable == true) || final
                    should_merge!(mergable, global_clusters_vector[i].cluster_params.cluster_params,
                        global_clusters_vector[j].cluster_params.cluster_params,model.groups_dict,
                        model.model_hyperparams.γ,global_clusters_vector[i].clusters_count,
                        global_clusters_vector[j].clusters_count,i,j,model.weights[i], model.weights[j], final)
                end
                if mergable[1] == 1
                    merge_clusters!(model, i, j)
                    push!(indices,i)
                    push!(indices,j)
                    break
                end
            end
            if mergable[1] == 1
                break
            end
        end
    end
    return indices
end

function update_suff_stats_posterior!(model::hdp_shared_features, indices::AbstractArray{Int64,1})
    local_dim = model.model_hyperparams.local_dim
    pts_vector_dict = Dict()
    sub_labels_vector_dict = Dict()
    clusters_count_dict = Dict()
    pts_to_groups = Dict()
    for i=1:length(global_clusters_vector)
        if i in indices
            pts_vector_dict[i] = Vector{AbstractArray{Float64,2}}()
            sub_labels_vector_dict[i] = Vector{AbstractArray{Int64,1}}()
            pts_to_groups[i] = Vector{AbstractArray{Int64,1}}()
            clusters_count_dict[i] = 0
        end
    end
    for (k,v) in model.groups_dict
        for (i,c) in enumerate(v.local_clusters)
            if c.globalCluster in indices
                push!(pts_vector_dict[c.globalCluster], (@view v.points[1:local_dim-1,(@view (v.labels .== i)[:])]))
                sub_labels = (@view v.labels_subcluster[(@view (v.labels .== i)[:])])
                push!(sub_labels_vector_dict[c.globalCluster], sub_labels)
                push!(pts_to_groups[c.globalCluster],ones(Int64,size(sub_labels,1))*k)
            end
        end
    end
    cluster_params_dict = Dict()
    begin
         @sync for (index,cluster) in enumerate(global_clusters_vector)
            if index in indices
                if size(pts_vector_dict[index],1) > 0
                    #pts = reshape(CatView(Tuple(pts_vector_dict[index])),local_dim - 1,:)
                    cluster.clusters_sub_counts = [0,0]
                    pts = reduce(hcat,pts_vector_dict[index])
                    for sublabel in sub_labels_vector_dict[index]
                        if sum(sublabel .<= 2) >= sum(sublabel .> 2)
                            cluster.clusters_sub_counts[1] += 1
                        else
                            cluster.clusters_sub_counts[2] += 1
                        end
                    end
                    sublabels = reduce(vcat,sub_labels_vector_dict[index])
                    pts_group= reduce(vcat,pts_to_groups[index])
                    #sublabels = CatView(Tuple(sub_labels_vector_dict[index]))
                    # println(cluster.clusters_sub_counts)
                    cluster_params_dict[index] =  update_splittable_cluster_params(cluster.cluster_params, pts , sublabels, true, pts_group)
                end
            end
        end
        for (index,cluster) in enumerate(global_clusters_vector)
            if index in indices
                if size(pts_vector_dict[index],1) > 0
                    cluster.cluster_params = fetch(cluster_params_dict[index])
                    #println(cluster.cluster_params)
                end
            end
        end
    end
end

function sample_global_clusters_params!(model::hdp_shared_features)
    points_count = Vector{Float64}()
    for cluster in global_clusters_vector
        #push!(points_count, cluster.clusters_count)
        push!(points_count, cluster.cluster_params.cluster_params.suff_statistics.N)
        sample_cluster_params!(cluster.cluster_params, model.model_hyperparams.γ, cluster.clusters_sub_counts)
    end
    push!(points_count, model.model_hyperparams.γ)
    model.weights = rand(Dirichlet(points_count))[1:end-1]
    # println("Weights:" * string(model.weights))
    # println("Samples: " * string([x.cluster_params.cluster_params.distribution for x in global_clusters_vector]))
end

function remove_empty_clusters!(model::hdp_shared_features)
    new_vec = Vector{global_cluster}()
    to_remove = []
    for (index,cluster) in enumerate(global_clusters_vector)
        if cluster.clusters_count == 0
            push!(to_remove, index)
        else
            push!(new_vec,cluster)
        end
    end
    if length(to_remove) > 0
        for (k,v) in model.groups_dict
            for (i,c) in enumerate(v.local_clusters)
                c.globalCluster -= sum(to_remove .< c.globalCluster)
            end
        end
        global global_clusters_vector = new_vec
    end
end

function local_split!(model::hdp_shared_features, index::Int64, new_index::Int64)
    cluster = global_clusters_vector[index]
    new_cluster = deepcopy(cluster)
    new_cluster.clusters_count = 0
    new_cluster.cluster_params = create_splittable_from_params(cluster.cluster_params.cluster_params_r, model.model_hyperparams.γ)
    cluster.cluster_params = create_splittable_from_params(cluster.cluster_params.cluster_params_l, model.model_hyperparams.γ)
    new_cluster.points_count = new_cluster.cluster_params.cluster_params.suff_statistics.N
    cluster.points_count = cluster.cluster_params.cluster_params.suff_statistics.N
    global_clusters_vector[new_index] = new_cluster
end

function split_new_clusters_from_local!(model::hdp_shared_features)
    cur_count = length(global_clusters_vector)
    new_clusters_to_global = Dict() #keys are global clusters to split
    for (i,group) in model.groups_dict
        for c in group.local_clusters
            new_global = c.globalCluster
            if new_global > cur_count #new cluster
                # println(new_global)
                if haskey(new_clusters_to_global, new_global - cur_count)
                    push!(new_clusters_to_global[new_global - cur_count], c)                     #The last2 clusters are in the new global
                else
                    new_clusters_to_global[new_global - cur_count] = [c]
                end
            end
        end
    end
    new_index =  Int64(length(global_clusters_vector)) +1
    # println(keys(new_clusters_to_global))
    resize!(global_clusters_vector,Int64(length(global_clusters_vector) + length(new_clusters_to_global)))
    indicies_to_re_evaluate = Vector{Int64}()
    for (k,v) in new_clusters_to_global
        local_split!(model,k,new_index)
        # global_clusters_vector[k].clusters_count += length(v) / 2
        # global_clusters_vector[new_index].clusters_count += length(v)
        for group in v
            group.globalCluster = new_index
        end
        push!(indicies_to_re_evaluate,k)
        push!(indicies_to_re_evaluate,new_index)
        new_index += 1
    end
    if length(indicies_to_re_evaluate) > 0
        update_pts_count!(model)
        remove_empty_clusters!(model)
        update_weights!(model)
    end
    return indicies_to_re_evaluate
end

function update_pts_count!(model::hdp_shared_features)
    counts = zeros(length(global_clusters_vector))
    for (k,v) in model.groups_dict
        for c in v.local_clusters
            counts[c.globalCluster] += 1
        end
    end
    # println(counts)
    for (k,v) in enumerate(global_clusters_vector)
        v.clusters_count = counts[k]
    end
end

function update_weights!(model::hdp_shared_features)
    counts = Vector{Float64}()
    for c in global_clusters_vector
        push!(counts,c.clusters_count)
    end
    push!(counts, model.model_hyperparams.γ)
    model.weights = rand(Dirichlet(counts))[1:end-1]
end

function update_partial_pts_count!(model::hdp_shared_features)
    counts = zeros(length(global_clusters_vector))
    for (k,v) in model.groups_dict
        for c in v.local_clusters
            if c.globalCluster < length(counts)
                counts[c.globalCluster] += 1
            end
        end
    end
    for (k,v) in enumerate(global_clusters_vector)
        v.clusters_count = counts[k]
    end
end


function _precompile_()
    ccall(:jl_generating_output, Cint, ()) == 1 || return nothing
    Base.precompile(Tuple{typeof(materialize),Broadcasted{DefaultArrayStyle{1}, Nothing, typeof(/), Tuple{Broadcasted{DefaultArrayStyle{1}, Nothing, typeof(/), Tuple{Vector{Float64}, Vector{Int64}}}, Float64}}})   # time: 0.0245907
    Base.precompile(Tuple{typeof(materialize),Broadcasted{DefaultArrayStyle{1}, Nothing, typeof(/), Tuple{Vector{Float64}, Float64}}})   # time: 0.0153087
    Base.precompile(Tuple{typeof(materialize!),Vector{Float64},Broadcasted{DefaultArrayStyle{1}, Nothing, typeof(/), Tuple{Vector{Float64}, Float64}}})   # time: 0.0049648
    Base.precompile(Tuple{typeof(broadcasted),Function,Vector{Float64},Float64})   # time: 0.0036101
    Base.precompile(Tuple{typeof(broadcasted),Function,Broadcasted{DefaultArrayStyle{1}, Nothing, typeof(/), Tuple{Vector{Float64}, Vector{Int64}}},Float64})   # time: 0.0034639
end


# parameters
mass_bin_name = ARGS[1]
tslice = "t1"
path_wavelist = "src"
path_to_working_folder = "data"
list_of_files = ARGS[2:end]

println("Starting with ARGS = ", ARGS)
######################################################
using DelimitedFiles
using LinearAlgebra

push!(LOAD_PATH,"src")
using SDMHelper
using FittingPWALikelihood
using amplitudes_compass
using PWAHelper

BmatMC = read_cmatrix(
    joinpath(path_to_working_folder,"integrmat_$(mass_bin_name)_$(tslice)_mc.txt"));

# read wavelist throw waves below threshol
wavelist = get_wavelist(joinpath(path_wavelist,"wavelist_formated.txt");
     path_to_thresholds=joinpath(path_wavelist,"thresholds_formated.txt"),
     M3pi=Meta.parse(mass_bin_name[1:4])/1000)

const noϵ =  [1] # flat wave
const posϵ = [i for (i,ϵ) in enumerate(wavelist[:,6]) if ϵ=="+"]
const negϵ = [i for (i,ϵ) in enumerate(wavelist[:,6]) if ϵ=="-"]

# Model description
const ModelBlocks = [noϵ, posϵ, negϵ, negϵ]

# load precalculated data array
const PsiRD = read_precalc_basis(
    joinpath(path_to_working_folder,"functions_$(mass_bin_name)_$(tslice)_rd.bin"));

# get functions to calculate LLH, derivative and hessian
LLH, GRAD, LLH_and_GRAD!, HES = createLLHandGRAD(PsiRD, BmatMC, ModelBlocks);

println("---> Start calculating LLHs")
llhs = [LLH(vcat(readdlm(path_and_filename)...)) for path_and_filename in list_of_files]
sorted_list = sort(collect(zip(list_of_files, llhs)), by=x->x[2])
writedlm(joinpath(path_to_working_folder,"llh_attmpts_$(mass_bin_name)_$(tslice).txt"), sorted_list)

###########################################################################
println("---> Trying to improve minimum")
# fit the best minimum again
## normalize B
Bscale = [BmatMC[i,i] for i in 1:size(BmatMC,1)];
for i=1:size(BmatMC,1), j=1:size(BmatMC,2)
    BmatMC[i,j] *= 1/sqrt(Bscale[i]*Bscale[j])
end
## normalize Psi
for i in 1:size(PsiRD,2)
    PsiRD[:,i] .*= 1.0/sqrt(Bscale[i])
end
## get functions to calculate LLH, derivative and hessian
LLH, GRAD, LLH_and_GRAD!, HES = createLLHandGRAD(PsiRD, BmatMC, ModelBlocks);
## load the best parameters
best_parameters = vcat(readdlm(sorted_list[1][1])...)
parscale = abs.(extnd(sqrt.(Bscale), get_parameter_map(ModelBlocks, size(BmatMC,1))))
best_parameters_scalled = best_parameters .* parscale
## fit
@time minpars = minimize(LLH, LLH_and_GRAD!;
    algorithm = :LD_LBFGS, verbose=1, starting_pars=best_parameters_scalled,
    llhtolerance = 1e-6)

max_reldiff_of_parameters = max(abs.(1 .- minpars ./ best_parameters_scalled)...)
@show max_reldiff_of_parameters
(max_reldiff_of_parameters > 0.1) && warn("Too big improvement in the fit while fine tuning!")

@time minpars2 = minimize(LLH, LLH_and_GRAD!;
    algorithm = :LD_SLSQP, verbose=1, starting_pars=minpars,
    llhtolerance = 1e-6)

max_reldiff_of_parameters2 = max(abs.(1 .- minpars2 ./ minpars)...)
@show max_reldiff_of_parameters2
(max_reldiff_of_parameters2 > 0.1) && warn("Too big improvement in the fit while fine tuning!")

###########################################################################
println("---> Calculating Hessian")
# get derivative
hes = HES(minpars2)
singular = (det(hes) ≈ 0.0); singular && warn("Hessian matrix is singular!")
invhes = !(singular) ? inv(hes) : fill(0.0,size(hes)...)
invhes_scaled_back = invhes ./ transpose(parscale) ./ parscale
writedlm(joinpath(path_to_working_folder,"invhes_$(splitdir(sorted_list[1][1])[end])"), invhes_scaled_back)
println("Done")


type Distribution
    tG::Tuple{Vararg{Array{Float64}}}
    td::Array{Float64}
    T::SparseMatrixCSC
    d::Array{Float64}
end

type temp
    error::Array{Float64,2}
    J::Array{Float64,2}
    foc::Expr
    ploc::Vector{Int}
    updateD::Function
    f2::Expr
end

type Model
    G::NGrid
    S::Array{Float64,2}
    SP::Array{Float64,2}
    X::Array{Float64,2}
    XP::Array{Float64,2}
    ProbWeights::Array{Float64,2}
    variables::Vector{ModVar}
    F::Function
    J::Function
    distribution::Distribution
    temp::temp
end

(M::Model)(s::Symbol,x::Array{Float64,2}) = M.G(M[s,0],x)

function display(M::Model)
    [println("\t",x) for x in M.temp.foc.args]
    println("S: ",names(State(M.variables)))
    println("P: ",names(Policy(M.variables)))
end

function Model(foc,states,policy,vars,params)
    slist,G,S,SP,ProbWeights    = parsestate(states)
    vlist,Dlist,dlist,plist     = parsevars(slist,policy,vars,params)
    f1,f2,j2,expects = parsefoc(foc,vlist,dlist,plist)

    vlist = vcat(vlist,Dlist)
    vlist = vcat(vlist,Future(f2,slist,vlist))

    X = hcat([((length(v.def.args)==4 && vtype(v)==Policy) ? S[:,find(slist,v.def.args[3])]*v.init : ones(length(G))*v.init[1]) for v in Future(vlist,false)]...)
    XP = zeros(length(G)*size(ProbWeights,2),length(Future(vlist)))

    tG = ndgrid(Vector{Float64}[s.val.x for s in slist]...)
    td = zeros(size(tG[1]))+1/length(tG[1])
    T   = spzeros(length(td),length(td))
    d  = zeros(length(G))
    D = Distribution(tG,td,T,d)

    vlist = vcat(slist,vlist)

    return Model(G,S,SP,X,XP,ProbWeights,vlist,
        eval(:( $(gensym(:F))(M::Model) = @fastmath $(buildfunc(addpweights!(inserthardloc(f2,vlist),size(ProbWeights,2)),:(M.temp.error))))),
        eval(:( $(gensym(:J))(M::Model,i::Int) = @fastmath $(buildJ(vec(addpweights!(inserthardloc(j2,vlist),size(ProbWeights,2))))))),
        D,
        temp(ones(length(G),length(Policy(vlist)))
            ,zeros(length(Policy(vlist)),length(Policy(vlist)))
            ,foc,
            Int[findfirst(names(State(vlist,false)),p)+size(S,2) for p in names(Policy(vlist))],
            eval(:($(gensym(:updateD))(M) = $(buildfunc(inserthardloc(addindex(subs(Expr(:vect,[v.def for v in Dlist]...),plist)),vlist),:(M.X),length(vlist)-length(slist)-length(Dlist)-length(Future(vlist)))))),
            :( $(gensym(:F))(M) = @fastmath $(buildfunc(addpweights!(inserthardloc(f2,vlist),size(ProbWeights,2)),:(M.temp.error))))
            )
        )
end

function getindex(M::Model,x::Symbol)
    if in(x,names(State(M.variables)))
        return M.variables[findfirst(names(State(M.variables)),x)].val
    else
        error("$x not found")
    end
end

function getindex(M::Model,x::Symbol,t::Int)
    ns = length(State(M.variables))
    for i ∈ 1:length((M.variables))
        if  t==-1 && vtype(M.variables[i])==Endogenous && M.variables[i].name==x
            return M.S[:,i]
        elseif vtype(M.variables[i])==Aggregate && M.variables[i].name==x && M.variables[i].val.target[2]==t
            return M.X[:,i-ns]
        elseif  t==0 && vtype(M.variables[i]) <: Stochastic && M.variables[i].name==x
           return  M.S[:,i]
        elseif  t==0 && vtype(M.variables[i]) <: Union{Policy,Static}  && M.variables[i].name==x
           return  M.X[:,i-ns]
        end
   end
end

function setindex!(M::Model,X::Vector{Float64},x::Symbol,t::Int)
    ns = length(State(M.variables))
    for i ∈ 1:length((M.variables))
        if  t==-1 && vtype(M.variables[i])==Endogenous && M.variables[i].name==x
            return M.S[:,i] = X
        elseif vtype(M.variables[i])==Aggregate && M.variables[i].name==x && M.variables[i].val.target[2]==t
            return M.X[:,i-ns] = X
        elseif  t==0 && vtype(M.variables[i]) <: Stochastic && M.variables[i].name==x
           return  M.S[:,i] = X
        elseif  t==0 && vtype(M.variables[i]) <: Union{Policy,Static}  && M.variables[i].name==x
           return  M.X[:,i-ns] = X
        end
   end
end


function gpu_track(
    signal,
    state::GNSSBenchmarks.gpuTrackingState{S, C, CALF, COLF, CN, DS},
    prn::Integer,
    sampling_frequency;
    post_corr_filter = get_default_post_corr_filter(gpu_get_correlator(state)),
    intermediate_frequency = 0.0Hz,
    max_integration_time::typeof(1ms) = 1ms,
    min_integration_time::typeof(1.0ms) = 0.75ms,
    early_late_sample_shift = get_early_late_sample_shift(S,
        gpu_get_correlator(state), sampling_frequency, 0.5),
    carrier_loop_filter_bandwidth = 18Hz,
    code_loop_filter_bandwidth = 1Hz,
    velocity_aiding = 0Hz,
    carrier_amplitude_power::Val{N} = Val(5)
) where {
S <: AbstractGNSSSystem,
C <: AbstractCorrelator,
CALF <: AbstractLoopFilter,
COLF <: AbstractLoopFilter,
CN <: AbstractCN0Estimator,
DS <: CuArray,
N
}
correlator = gpu_get_correlator(state)
downconverted_signal = resize!(gpu_get_downconverted_signal(state), size(signal, 1))
carrier_replica = resize!(gpu_get_carrier(state), size(signal, 1))
code_replica = resize!(gpu_get_code(state), size(signal, 1) + 2 * maximum(early_late_sample_shift))
init_carrier_doppler = GNSSBenchmarks.gpu_get_init_carrier_doppler(state)
init_code_doppler = GNSSBenchmarks.gpu_get_init_code_doppler(state)
carrier_doppler = GNSSBenchmarks.gpu_get_carrier_doppler(state)
code_doppler = GNSSBenchmarks.gpu_get_code_doppler(state)
carrier_phase = GNSSBenchmarks.gpu_get_carrier_phase(state)
code_phase = GNSSBenchmarks.gpu_get_code_phase(state)
sc_bit_detector = GNSSBenchmarks.gpu_get_sc_bit_detector(state)
carrier_loop_filter = GNSSBenchmarks.gpu_get_carrier_loop_filter(state)
code_loop_filter = GNSSBenchmarks.gpu_get_code_loop_filter(state)
prompt_accumulator = GNSSBenchmarks.gpu_get_prompt_accumulator(state)
integrated_samples = GNSSBenchmarks.gpu_get_integrated_samples(state)
cn0_estimator = GNSSBenchmarks.gpu_get_cn0_estimator(state)
signal_start_sample = 1
bit_buffer = Tracking.BitBuffer()
valid_correlator = zero(correlator)
got_correlator = false
while true
    num_samples_left_to_integrate = get_num_samples_left_to_integrate(
        S,
        max_integration_time,
        sampling_frequency,
        code_doppler,
        code_phase,
        found(sc_bit_detector)
    )
    signal_samples_left = get_num_samples(signal) - signal_start_sample + 1
    num_samples_left = min(num_samples_left_to_integrate, signal_samples_left)
    carrier_frequency = get_current_carrier_frequency(
        intermediate_frequency,
        carrier_doppler
    )
    code_frequency = get_current_code_frequency(S, code_doppler)
    code_replica = GNSSBenchmarks.gpu_gen_code_replica!(
        code_replica,
        Tracking.GPSL1,
        code_frequency,
        sampling_frequency,
        code_phase,
        signal_start_sample,
        num_samples_left,
        early_late_sample_shift,
        prn
    )
    carrier_replica = GNSSBenchmarks.gpu_gen_carrier_replica!(
        carrier_replica,
        carrier_frequency,
        sampling_frequency,
        carrier_phase,
        carrier_amplitude_power,
        signal_start_sample,
        num_samples_left
    )
    downconverted_signal = GNSSBenchmarks.gpu_downconvert!(
        downconverted_signal,
        signal,
        carrier_replica,
        signal_start_sample,
        num_samples_left
    )
    correlator = GNSSBenchmarks.gpu_correlate(
        correlator,
        downconverted_signal,
        code_replica,
        early_late_sample_shift,
        signal_start_sample,
        num_samples_left,
        1,
        2,
        Val(7)
    )
    integrated_samples += num_samples_left
    carrier_phase = Tracking.update_carrier_phase(
        num_samples_left,
        carrier_frequency,
        sampling_frequency,
        carrier_phase,
        carrier_amplitude_power
    )
    prev_code_phase = code_phase
    code_phase = Tracking.update_code_phase(
        S,
        num_samples_left,
        code_frequency,
        sampling_frequency,
        code_phase,
        found(sc_bit_detector)
    )
    integration_time = integrated_samples / sampling_frequency
    if num_samples_left == num_samples_left_to_integrate &&
            integration_time >= min_integration_time
        got_correlator = true

        correlator = Tracking.normalize(correlator, integrated_samples)
        valid_correlator = correlator
        filtered_correlator = Tracking.filter(post_corr_filter, correlator)
        pll_discriminator = Tracking.pll_disc(S, filtered_correlator)
        dll_discriminator = Tracking.dll_disc(
            S,
            filtered_correlator,
            early_late_sample_shift,
            code_frequency / sampling_frequency
        )
        carrier_freq_update, carrier_loop_filter = filter_loop(
            carrier_loop_filter,
            pll_discriminator,
            integration_time,
            carrier_loop_filter_bandwidth
        )
        code_freq_update, code_loop_filter = filter_loop(
            code_loop_filter,
            dll_discriminator,
            integration_time,
            code_loop_filter_bandwidth
        )
        carrier_doppler, code_doppler = Tracking.aid_dopplers(
            S,
            init_carrier_doppler,
            init_code_doppler,
            carrier_freq_update,
            code_freq_update,
            velocity_aiding
        )
        cn0_estimator = Tracking.update(cn0_estimator, Tracking.get_prompt(filtered_correlator))
        bit_buffer, prompt_accumulator = Tracking.buffer(
            S,
            bit_buffer,
            prompt_accumulator,
            found(sc_bit_detector),
            prev_code_phase,
            code_phase,
            max_integration_time,
            Tracking.get_prompt(filtered_correlator)
        )
        sc_bit_detector = Tracking.find(S, sc_bit_detector, Tracking.get_prompt(filtered_correlator))
        correlator = zero(correlator)
        integrated_samples = 0
    end

    num_samples_left == signal_samples_left && break
    signal_start_sample += num_samples_left
end
next_state = GNSSBenchmarks.gpuTrackingState{S, C, CALF, COLF, CN, DS}(
    init_carrier_doppler,
    init_code_doppler,
    carrier_doppler,
    code_doppler,
    carrier_phase,
    code_phase,
    correlator,
    carrier_loop_filter,
    code_loop_filter,
    sc_bit_detector,
    integrated_samples,
    prompt_accumulator,
    cn0_estimator,
    downconverted_signal,
    carrier_replica,
    code_replica
)
estimated_cn0 = Tracking.estimate_cn0(cn0_estimator, max_integration_time)
gpuTrackingResults(next_state, valid_correlator, got_correlator, bit_buffer, estimated_cn0)
end


for F in (:typemax, :typemin, :zero, :one)
  @eval begin
    @pure function $F(::Type{M}) where {M<:MarkableInteger}
        Unmarked($F(itype(M)))
    end
    @pure function $F(x::M) where {M<:MarkableInteger}
        Unmarked($F(itype(M)))
    end
  end
end

for F in (:iszero, :isone, :iseven, :isodd)
  @eval begin
    function $F(x::M) where {M<:MarkableInteger}
        xx = itype(x)
        return $F(xx)
    end
  end
end

for F in (:leading_zeros, :leading_ones, :trailing_zeros, :trailing_ones,
          :count_zeros, :count_ones, :abs, :(-))
  @eval begin
    function $F(x::M) where {M<:MarkableInteger}
        xx = itype(x) >> 1
        zz = $F(xx)
        z = Unmarked(zz)
        z |= any_lsbits(x)
        return z
    end
  end
end

for F in (:(|), :(&), :(⊻))
  @eval begin
    function $F(x::M, y::M) where {M<:MarkableInteger}
        xx = ityped(x) >> 1
        yy = ityped(y) >> 1
        zz = $F(xx, yy)
        z = zz << 1
        !(zz === z >> 1) && throw(DomainError("$x, $y"))
        z |= any_lsbits(x, y)
        return reinterpret(M, z)
    end
    $F(x::M1, y::M2) where {M1<:MarkableInteger, M2<:MarkableInteger} =
         $F(promote(x,y)...,)
    $F(x::M, y::I) where {M<:MarkableInteger, I<:Union{Signed,Unsigned}} =
         $F(promote(x,y)...,)
    $F(x::I, y::M) where {M<:MarkableInteger, I<:Union{Signed,Unsigned}} =
         $F(promote(x,y)...,)
  end
end

for F in(:flipsign, :copysign,
         :(+), :(-), :(*), :(^), :div, :cld, :fld, :rem, :mod,
         :checked_add, :checked_cld, :checked_div, :checked_fld,
         :checked_mod, :checked_mul, :checked_rem, :checked_sub)
  @eval begin
    function $F(x::M, y::M) where {M<:MarkableInteger}
        xx = ityped(x) >> 1
        yy = ityped(y) >> 1
        zz = $F(xx, yy)
        z = zz << 1
        !(zz === z >> 1) && throw(DomainError("$x, $y"))
        z |= any_lsbits(x, y)
        return reinterpret(M, z)
    end
    $F(x::M1, y::M2) where {M1<:MarkableInteger, M2<:MarkableInteger} =
         $F(promote(x,y)...,)
    $F(x::M, y::I) where {M<:MarkableInteger, I<:Union{Signed,Unsigned}} =
         $F(promote(x,y)...,)
    $F(x::I, y::M) where {M<:MarkableInteger, I<:Union{Signed,Unsigned}} =
         $F(promote(x,y)...,)
  end
end

string(x::T) where {T<:MarkableSigned} = string(ityped(x) >> 1)
string(x::T) where {T<:MarkableUnsigned} = string(ityped(x) >> 1)

show(io::IO, x::T) where {T<:MarkableInteger} = print(io, string(x))
show(x::T) where {T<:MarkableInteger} = show(StdOut, x)


module LibCEED

using StaticArrays, UnsafeArrays, Requires, Preferences
using Base: RefValue

# import low-level C interface
include("C.jl")
import .C

export @interior_qf,
    @witharray,
    @witharray_read,
    Abscissa,
    AbscissaAndWeights,
    Basis,
    BasisCollocated,
    COPY_VALUES,
    Ceed,
    CeedDim,
    CeedInt,
    CeedScalar,
    CeedVector,
    CeedVectorActive,
    CeedVectorNone,
    Context,
    CopyMode,
    EVAL_CURL,
    EVAL_DIV,
    EVAL_GRAD,
    EVAL_INTERP,
    EVAL_NONE,
    EVAL_WEIGHT,
    ElemRestriction,
    ElemRestrictionNone,
    EvalMode,
    GAUSS,
    GAUSS_LOBATTO,
    HEX,
    LINE,
    MEM_DEVICE,
    MEM_HOST,
    MemType,
    NORM_1,
    NORM_2,
    NORM_MAX,
    NOTRANSPOSE,
    NormType,
    OWN_POINTER,
    Operator,
    PRISM,
    PYRAMID,
    QFunction,
    QFunctionNone,
    QUAD,
    QuadMode,
    RequestImmediate,
    RequestOrdered,
    STRIDES_BACKEND,
    TET,
    TRANSPOSE,
    TRIANGLE,
    Topology,
    TransposeMode,
    USE_POINTER,
    UserQFunction,
    add_input!,
    add_output!,
    apply!,
    apply_add!,
    apply,
    assemble,
    assemble_add_diagonal!,
    assemble_diagonal!,
    axpy!,
    ceedversion,
    create_composite_operator,
    create_elem_restriction,
    create_elem_restriction_strided,
    create_evector,
    create_h1_basis,
    create_identity_qfunction,
    create_interior_qfunction,
    create_lvector,
    create_tensor_h1_basis,
    create_tensor_h1_lagrange_basis,
    create_vectors,
    det,
    extract_array,
    extract_context,
    gauss_quadrature,
    get_libceed_path,
    get_preferred_memtype,
    getcompstride,
    getnumelements,
    getelementsize,
    getlvectorsize,
    getmultiplicity!,
    getmultiplicity,
    getdimension,
    getgrad,
    getgrad1d,
    getinterp,
    getinterp1d,
    getnumcomponents,
    getnumnodes,
    getnumnodes1d,
    getnumqpts,
    getnumqpts1d,
    getqref,
    getqweights,
    getresource,
    gettopology,
    getvoigt!,
    getvoigt,
    iscuda,
    isdeterministic,
    isrelease,
    lobatto_quadrature,
    norm,
    pointwisemult!,
    reciprocal!,
    scale!,
    set_context!,
    set_cufunction!,
    set_data!,
    set_field!,
    set_libceed_path!,
    setarray!,
    setvalue!,
    setvoigt!,
    setvoigt,
    syncarray!,
    takearray!,
    use_prebuilt_libceed!,
    witharray,
    witharray_read

include("Globals.jl")
include("Ceed.jl")
include("CeedVector.jl")
include("Basis.jl")
include("ElemRestriction.jl")
include("Quadrature.jl")
include("Context.jl")
include("UserQFunction.jl")
include("QFunction.jl")
include("Request.jl")
include("Operator.jl")
include("Misc.jl")

const minimum_libceed_version = v"0.8.0"

function __init__()
    libceed_version = ceedversion()
    if minimum_libceed_version > libceed_version
        @warn("""
              Incompatible libCEED version.
              LibCEED.jl requires libCEED version at least $minimum_libceed_version.
              The version of the libCEED library is $libceed_version."
              """)
    end
    @require CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" include("Cuda.jl")
    set_globals()
end

"""
    ceedversion()

Returns a `VersionNumber` corresponding to the version of the libCEED library currently used.
"""
function ceedversion()
    major = Ref{Cint}()
    minor = Ref{Cint}()
    patch = Ref{Cint}()
    release = Ref{Bool}()
    C.CeedGetVersion(major, minor, patch, release)
    return VersionNumber(major[], minor[], patch[])
end

"""
    isrelease()

Returns true if the libCEED library is a release build, false otherwise.
"""
function isrelease()
    major = Ref{Cint}()
    minor = Ref{Cint}()
    patch = Ref{Cint}()
    release = Ref{Bool}()
    C.CeedGetVersion(major, minor, patch, release)
    return release[]
end

"""
    set_libceed_path!(path::AbstractString)
    set_libceed_path!(:prebuilt)
    set_libceed_path!(:default)

Sets the path of the libCEED dynamic library. `path` should be the absolute path to the library
file.

`set_libceed_path!(:prebuilt)` indicates to LibCEED.jl to use the prebuilt version of the libCEED
library (bundled with libCEED\\_jll).

`set_libceed_path!(:default)` indicates to LibCEED.jl to use the default library. This usually has
the same effect as `set_libceed_path!(:prebuilt)`, unless a different path has been specified in the
depot-wide preferences or using
[Overrides.toml](https://pkgdocs.julialang.org/dev/artifacts/#Overriding-artifact-locations).

This function sets the library path as a _preference_ associated with the currently active
environment. Changes will take effect after restarting the Julia session. See the [Preferences.jl
documentation](https://github.com/JuliaPackaging/Preferences.jl) for more information.
"""
function set_libceed_path!(path::AbstractString)
    set_preferences!(C.libCEED_jll, "libceed_path" => path; force=true)
    @info("""
          Setting the libCEED library path to $path.
          Restart the Julia session for changes to take effect.
          """)
end

function set_libceed_path!(sym::Symbol)
    if sym == :prebuilt
        set_preferences!(C.libCEED_jll, "libceed_path" => nothing; force=true)
        @info("""
              Using the prebuilt libCEED binary.
              Restart the Julia session for changes to take effect.
              """)
    elseif sym == :default
        delete_preferences!(Preferences.get_uuid(C.libCEED_jll), "libceed_path"; force=true)
        @info("""
              Deleting preference for libCEED library path.
              Restart the Julia session for changes to take effect.
              """)
    else
        error("set_libceed_path(::Symbol) must be called with :prebuilt or :default.")
    end
end

"""
    use_prebuilt_libceed!()

Indicates that the prebuilt version of the libCEED library (bundled with libCEED\\_jll) should be
used.

Equivalent to `set_libceed_path!(:prebuilt)`.
"""
use_prebuilt_libceed!() = set_libceed_path!(:prebuilt)

"""
    get_libceed_path()

Returns the path to the currently used libCEED library. A different libCEED library can be used by
calling [`set_libceed_path!`](@ref) or by using a depot-wide Overrides.toml file.
"""
get_libceed_path() = C.libCEED_jll.libceed_path

end # module


@inline function update!(
    pcs::PotentialCalculationSetup{T},
    kernel,
    ndrange;
    use_nthreads::Int = Base.Threads.nthreads(),
    depletion_handling::Val{depletion_handling_enabled} = Val{false}(),
    is_weighting_potential::Val{_is_weighting_potential} = Val{false}(),
    only2d::Val{only_2d} = Val{false}()
)::Nothing where {T, only_2d, depletion_handling_enabled, _is_weighting_potential}
    update_even_points = true
    wait(kernel(
        pcs.potential, pcs.imp_scale, pcs.point_types, pcs.volume_weights, pcs.q_eff_imp, pcs.q_eff_fix, pcs.ϵ_r,
        pcs.geom_weights, pcs.sor_const, update_even_points, depletion_handling_enabled, _is_weighting_potential, only_2d,
        ndrange = ndrange
    ))
    apply_boundary_conditions!(pcs, Val(update_even_points), only2d)
    update_even_points = false
    wait(kernel(
        pcs.potential, pcs.imp_scale, pcs.point_types, pcs.volume_weights, pcs.q_eff_imp, pcs.q_eff_fix, pcs.ϵ_r,
        pcs.geom_weights, pcs.sor_const, update_even_points, depletion_handling_enabled, _is_weighting_potential, only_2d,
        ndrange = ndrange
    ))
    apply_boundary_conditions!(pcs, Val(update_even_points), only2d)
    return nothing
end


using VertexSafeGraphs

using Test
import Random

import LightGraphs
const LG = LightGraphs

@testset "Graph construction and basic interface" begin
    nv = 20
    g1 = VSafeGraph(nv)
    @test LG.nv(g1) == nv
    @test LG.nv(g1.g) == nv

    g2_inner = LG.CompleteGraph(nv)
    g2 = VSafeGraph(g2_inner)
    @test LG.nv(g2) == LG.nv(g2_inner)
    @test LG.ne(g2) == LG.ne(g2_inner)

    @test all(sort(collect(LG.vertices(g2))) .== sort(collect(LG.vertices(g2_inner))))

    g3 = VSafeGraph(LG.CompleteDiGraph(30))
    @test LG.is_directed(g3)
    @test !LG.is_directed(g2)
end

@testset "Vertex deletion" begin
    Random.seed!(33)
    nv = 45
    inner = LG.CompleteGraph(nv)
    g = VSafeGraph(inner)
    @test LG.ne(inner) == LG.ne(g)
    @test LG.nv(inner) == LG.nv(g)
    nrm = 0
    for _ in 1:15
        removed_ok = LG.rem_vertex!(g, rand(1:nv))
        if !removed_ok
            continue
        end
        nrm += 1
        @test LG.nv(inner) == nv
        @test LG.nv(g) == nv - nrm
        @test length(g.deleted_vertices) == nrm

        @test LG.ne(inner) == LG.ne(g)
    end
end

@testset "Generic LG algorithms work" begin
    nv = 45
    inner = LG.CompleteGraph(nv)
    g = VSafeGraph(inner)
    removed_ok = LG.rem_vertex!(g, rand(1:nv))
    @test removed_ok
    # LG broken here
    @test_throws BoundsError LG.pagerank(g)
    @test_throws BoundsError LG.kruskal_mst(g)
end


include("EchoAlgorithm.jl")

struct ElectionIDMessage{T <: Integer} <: MessageContent
    id::T
end

struct Swallow <: StopContent end

mutable struct ElectionAlgorithmWinner{T <: Integer}
    echo_algorithm_instance::Union{EchoAlgorithm,Nothing}
    winner_process_id::T
    ElectionAlgorithmWinner{T}() where T <: Integer = new(nothing, 0)

end

struct ElectionAlgorithm{T <: Integer}
    graph::AbstractGraph{T}
    process_ids::AbstractArray{T} # unique identity p
    strongest_ids::AbstractArray{T} # local variable M_p per process
    strongest_id_callback::Function
    initiators::AbstractArray{T} # process id of initiators
    initiator_id_nodes_map::Dict{T,T}
    echo_algorithms::AbstractArray{EchoAlgorithm}
    winner::ElectionAlgorithmWinner{T}

    function ElectionAlgorithm{T}(graph::AbstractGraph{T}, process_ids::AbstractArray{T}, initiators::AbstractArray{T}) where T <: Integer
        1 ≤ length(initiators) ≤ length(process_ids) || throw(error("There must not be more initiators than processes and at least one initiator!"))
        length(process_ids) == nv(graph) || throw(error("Number of processes and vertices of the graph must be equal!"))
        allunique(process_ids) || throw(error("There must be no duplicate process ids!"))
        is_connected(graph) || throw(error("Graph must be connected!"))
        initiator_id_nodes_map = Dict{T,T}()
        for (i, id) ∈ enumerate(process_ids)
            if id ∈ initiators
                initiator_id_nodes_map[id] =  i
            end
        end
        length(keys(initiator_id_nodes_map)) == length(initiators) || throw(error("All initiators must have a process id in the array!"))
        echo_algorithms = map(i -> EchoAlgorithm{T}(graph, initiator_id_nodes_map[i]), initiators)
        strongest_ids = zeros(T, length(process_ids))
        strongest_id_callback = _elec_algorithm_echo_callback_gen(strongest_ids)
        new(graph, process_ids, strongest_ids, strongest_id_callback, initiators, initiator_id_nodes_map, echo_algorithms, ElectionAlgorithmWinner{T}())
    end
end
function _elec_algorithm_echo_callback_gen(strongest_ids::AbstractArray{T}) where {T <: Integer}
    swallow = Swallow()
    return function elec_content_callback(letter::Letter{T,ElectionIDMessage{T}}) where {T <: Integer}
        recipient_node, content = dst(letter.e), letter.content
        if strongest_ids[recipient_node] <= content.id
            strongest_ids[recipient_node] = content.id
            return content
        end
        return swallow
    end
end

function ElectionAlgorithm{T}(graph::AbstractGraph{T}) where {T <: Integer}
    processes = collect(T, vertices(graph))
    ElectionAlgorithm{T}(graph, processes, processes)
end

has_winner(elec_alg::ElectionAlgorithm) = elec_alg.winner.echo_algorithm_instance !== nothing
has_terminated(elec_alg::ElectionAlgorithm) = has_winner(elec_alg) && has_terminated(elec_alg.winner.echo_algorithm_instance) # TODO implement last round
is_dead(elec_alg::ElectionAlgorithm) = !isempty(elec_alg.echo_algorithms) && all(is_dead.(elec_alg.echo_algorithms))

function init_election_algorithm!(elec_alg::ElectionAlgorithm)
    for initiator ∈ elec_alg.initiators # set M_p to p
        elec_alg.strongest_ids[elec_alg.initiator_id_nodes_map[initiator]] = initiator
    end
    init_echo_algorithm!.(elec_alg.echo_algorithms, [ElectionIDMessage(id) for id in elec_alg.initiators])
    return elec_alg
end

function election_algorithm_step!(elec_alg::ElectionAlgorithm)
    if has_winner(elec_alg)
        return _winner_step!(elec_alg)
    else
        dead_echo_instances = Vector{Int64}()
        for (i, echo_alg) in enumerate(elec_alg.echo_algorithms)
            if has_terminated(echo_alg)
                return _establish_winner!(elec_alg, echo_alg)
            elseif is_dead(echo_alg)
                push!(dead_echo_instances, i)
            else
                echo_algorithm_step!(echo_alg, elec_alg.strongest_id_callback)
            end
        end
        deleteat!(elec_alg.echo_algorithms, dead_echo_instances)
        return elec_alg
    end
end

function _establish_winner!(elec_alg::ElectionAlgorithm, winner_echo_instance::EchoAlgorithm)
    elec_alg.winner.echo_algorithm_instance = winner_echo_instance
    elec_alg.winner.winner_process_id = elec_alg.process_ids[winner_echo_instance.initiator]
    empty!(elec_alg.echo_algorithms)
    return elec_alg
end

function _winner_step!(elec_alg::ElectionAlgorithm) # TODO implement last distribution of winner
    return elec_alg
end


include("Init.jl")
include("ProjectInvestment.jl")

##########################################################################################################
#Investment decision making algorithm"
#GEP = "cost-minimizing future investments" price projection method
#EMLab = "myopic agents" price projection method
#EMCAS = "exogenous scenarios for future investments" price projection method
investmentModelSelectionList = [:GEP,:EMLab,:EMCAS]
#Selecting the look-ahead horizon
genCoHorizonList = [5,10,15,20]

#Store the simulation history
evaluationHistory_List = Array{Any,2}(undef,length(investmentModelSelectionList),9)
systemHistory_List = Array{Any,2}(undef,length(investmentModelSelectionList),9)
allGenCos_List = Array{Any,2}(undef,length(investmentModelSelectionList),9)

#Looping through the cases tested in the study
for modelCount in 1:length(investmentModelSelectionList)
    systemSettings.simulationSettings.investmentModelSelection = investmentModelSelectionList[modelCount]
    #For "exogenous scenarios for future investments" method,
        #we test different scenario tree settings
    if modelCount == 3
        #First layer of the scenario tree
        newExpansionList = [0.95,0.9,0.85]
        #Second layer of the scenario tree
        newDivisionList = [(0.2,0.5,0.3),(0.3,0.2,0.5),(0.5,0.3,0.2)]
        caseCount = 0
        #Looping through different scenario tree settings
        #Outer loop: first layer
        for newExpansion in newExpansionList
            #Inner layer: second layer
            for newDivision in newDivisionList
                caseCount += 1
                #Create a new case and serve as input to the "Init.jl" function where GenCos are initialized
                #The function "NewCase" can be found in the file "ScenarioTree.jl"
                gapCoverPercentage,divisionTypeShare = NewCase(newExpansion,newDivision)
                #Initialize the system
                #MO = Market Operator
                #allGenCos = all Generation Companies
                #systemLoad = load data
                #allPowerFlows = all power flows (connection between market operator and generators)
                #technologyPool = all candidate technologies
                MO,allGenCos,systemLoad,allPowerFlows,technologyPool,systemState,systemHistory = InitSystem(systemSettings,genCoHorizon;gapCoverPercentage = gapCoverPercentage,divisionTypeShare = divisionTypeShare)
                #Run the simulation
                SimulateInvestment(systemSettings,MO,allGenCos,systemLoad,allPowerFlows,technologyPool,systemState,systemHistory)
                #Record the history
                systemHistory_List[modelCount,caseCount] = systemHistory
                allGenCos_List[modelCount,caseCount] = allGenCos
            end
        end
    else
        #For "cost-minimizing future investments" and "myopic agents" price projection method, we test the sensitivity of look-ahead horizon
        for genCoHorizonCount in 1:length(genCoHorizonList)
            #Similar operation as above
            MO,allGenCos,systemLoad,allPowerFlows,technologyPool,systemState,systemHistory = InitSystem(systemSettings,genCoHorizonList[genCoHorizonCount])
            SimulateInvestment(systemSettings,MO,allGenCos,systemLoad,allPowerFlows,technologyPool,systemState,systemHistory)
            systemHistory_List[modelCount,genCoHorizonCount] = systemHistory
            allGenCos_List[modelCount,genCoHorizonCount] = allGenCos
        end
    end
end


mutable struct EulerNet{Tv,Tt,Tw,Tdelay,Tref,TΔt,Tstrace,Tparams,Trecorder,Tplasticity,Tnoise} <: AbstractNet{Tplasticity}
    v::Tv
    n_of_neurons::Int64
    t::Tt
    posts::Array{Array{Tuple{Int64,Int64}, 1}, 1}
    pres::Array{Array{Tuple{Int64,Int64}, 1}, 1}
    weights::Tw
    spiketrace::Tstrace
    spikecandidates::Array{Int64, 1}
    Δt::TΔt
    indelay::Array{Int64, 1}
    inref::Array{Int64, 1}
    delay::Tdelay
    refractory_period::Tref
    parameters::Tparams
    recorder::Trecorder
    plasticityrule::Tplasticity
    noise::Tnoise
end
function EulerNet(net; Δt = .1f0)
    if minimum(net.parameters.delay) == 0
        error("Cannot integrate nets with zero spike transmission delay.
              delay >= Δt")
    end
    EulerNet(net.v, net.n_of_neurons,
             net.t, net.posts, net.pres, net.weights, net.spiketrace,
             Int64[],
             Δt, -ones(Int64, net.n_of_neurons), -ones(Int64, net.n_of_neurons),
             round.(Int64, net.parameters.delay/Δt),
             round.(Int64, net.parameters.refractory_period/Δt),
             net.parameters, net.recorder, net.plasticityrule, net.noise)
end
export EulerNet

"""
    deepcopynet(net;
                recorder = net.recorder,
                plasticityrule = net.plasticityrule)

Copy an EulerNet and optionally replace the `recorder` or the `plasticityrule`.
"""
function deepcopynet(net::EulerNet;
                     recorder = deepcopy(net.recorder),
                     plasticityrule = deepcopy(net.plasticityrule),
                     parameters = deepcopy(net.parameters))
    EulerNet([field == :recorder ? recorder :
         field == :plasticityrule ? plasticityrule :
         field == :parameters ? parameters :
         deepcopy(getfield(net, field)) for field in fieldnames(typeof(net))]...)
end
export deepcopynet

function addspike!(net::EulerNet, t, i)
    net.indelay[i] = getdelay(net.delay, i)
    net.inref[i] = getrefractory_period(net.refractory_period, i)
    (typeof(net.noise) == RectLinEscapeNoise) &&
    (net.noise.endofrefrperiod[i] = t + getrefractory_period(net.parameters.refractory_period, i))
    addspikepostprocess!(net, t, i)
end
@inline function eulerspike!(net::EulerNet, noise::NoNoise, i)
  if abovethreshold(net.v, net.parameters.threshold, i)
      push!(net.spikecandidates, i)
  end
end
@inline getrectlinhazardrate(threshold::Number, v, β, i) = max(0, β * (v[i] - threshold))
@inline getrectlinhazardrate(threshold::AbstractArray, v, β, i) = max(0, β * (v[i] - threshold[i]))
@inline function eulerspike!(net::EulerNet, noise::RectLinEscapeNoise, i)
  if rand() <= net.Δt*getrectlinhazardrate(net.parameters.threshold, net.v, noise.β, i)
    push!(net.spikecandidates, i)
  end
end
function updatenet!(net::EulerNet, input, tmax)
    for eventtime in net.t:net.Δt:tmax
        updatevars!(net, eventtime, input)
        net.t = eventtime
        @inbounds for i in 1:net.n_of_neurons
            eulerspike!(net, net.noise, i)
            if net.indelay[i] == 1
                broadcastspike!(net, i)
                updateweights!(net, i)
            end
            if net.indelay[i] >= 1 net.indelay[i] -= 1 end
            if net.inref[i] == 0
                net.v[i] = getreset(net.parameters.reset, i)
            end
            if net.inref[i] >= 0 net.inref[i] -= 1 end
        end
        processspikecandidates!(net)
    end
    return false
end


struct RKParam <: EoSParam
    a::PairParam{Float64}
    b::PairParam{Float64}
    Tc::SingleParam{Float64}
    Pc::SingleParam{Float64}
    Mw::SingleParam{Float64}
end

abstract type RKModel <: ABCubicModel end

struct RK{T <: IdealModel,α,M} <: RKModel
    components::Array{String,1}
    icomponents::UnitRange{Int}
    alpha::α
    mixing::M
    params::RKParam
    idealmodel::T
    absolutetolerance::Float64
    references::Array{String,1}
end

has_sites(::Type{<:RKModel}) = false
has_groups(::Type{<:RKModel}) = false
built_by_macro(::Type{<:RKModel}) = false

function Base.show(io::IO, mime::MIME"text/plain", model::RK)
    return eosshow(io, mime, model)
end

function Base.show(io::IO, model::RK)
    return eosshow(io, model)
end

Base.length(model::RK) = Base.length(model.icomponents)

molecular_weight(model::RK,z=SA[1.0]) = comp_molecular_weight(mw(model),z)

export RK
function RK(components::Vector{String}; idealmodel=BasicIdeal,
    alpha = RKAlpha,
    mixing = vdW1fRule,
    activity = nothing,
    userlocations=String[],
    ideal_userlocations=String[],
    alpha_userlocations = String[],
    mixing_userlocations = String[],
    activity_userlocations = String[],
     verbose=false)
    params = getparams(components, ["properties/critical.csv", "properties/molarmass.csv","SAFT/PCSAFT/PCSAFT_unlike.csv"]; userlocations=userlocations, verbose=verbose)
    k  = params["k"]
    _pc = params["pc"]
    pc = _pc.values
    Mw = params["Mw"]
    _Tc = params["Tc"]
    Tc = _Tc.values
    #T̄c = sum(sqrt.(Tc*Tc')) #is this term correctly calculated? sqrt(Tc*Tc') is a matrix sqrt
    Ωa, Ωb = ab_consts(RK)
    a = epsilon_LorentzBerthelot(SingleParam(params["pc"], @. Ωa*R̄^2*Tc^2/pc), k)
    b = sigma_LorentzBerthelot(SingleParam(params["pc"], @. Ωb*R̄*Tc/pc))

    init_idealmodel = init_model(idealmodel,components,ideal_userlocations,verbose)
    init_alpha = init_model(alpha,components,alpha_userlocations,verbose)
    init_mixing = init_model(mixing,components,activity,mixing_userlocations,activity_userlocations,verbose)
    icomponents = 1:length(components)
    packagedparams = RKParam(a, b, params["Tc"],_pc,Mw)
    references = String[]
    model = RK(components,icomponents,init_alpha,init_mixing,packagedparams,init_idealmodel,1e-12,references)
    return model
end

function ab_consts(::Type{<:RKModel})
    Ωa =  1/(9*(2^(1/3)-1))
    Ωb = (2^(1/3)-1)/3
    return Ωa,Ωb
end

function cubic_abp(model::RKModel, V, T, z)
    n = sum(z)
    a,b = cubic_ab(model,V,T,z,n)
    v = V/n
    p =  R̄*T/(v-b) - a/((v+b)*v)
    return a,b,p
end

function cubic_poly(model::RKModel,p,T,z)
    n = sum(z)
    a,b = cubic_ab(model,p,T,z,n)
    RT⁻¹ = 1/(R̄*T)
    A = a*p* RT⁻¹* RT⁻¹
    B = b*p* RT⁻¹
    _1 = one(a)
    return [-A*B, -B*(B+_1) + A, -_1, _1]
end

function a_res(model::RKModel, V, T, z)
    n = sum(z)
    ā,b̄ = cubic_ab(model,V,T,z,n)
    ρ = n/V
    RT⁻¹ = 1/(R̄*T)
    return -log(1-b̄*ρ) - ā*RT⁻¹*log(b̄*ρ+1)/b̄
    #return -log(V-n*b̄) - ā/(R̄*T*b̄*√(T/T̄c))*log(1+n*b̄/V)
end

cubic_zc(::RKModel) = 1/3

# include("variants/SRK.jl")


using Test
using Statistics
using DelimitedFiles

using CLIMAParameters
struct EarthParameterSet <: AbstractEarthParameterSet end
const param_set = EarthParameterSet()

using ClimateMachine
using ClimateMachine.Land
using ClimateMachine.Land.SurfaceFlow
using ClimateMachine.Land.SoilWaterParameterizations
using ClimateMachine.Mesh.Topologies
using ClimateMachine.Mesh.Grids
using ClimateMachine.DGMethods
using ClimateMachine.DGMethods.NumericalFluxes
using ClimateMachine.DGMethods: BalanceLaw, LocalGeometry
using ClimateMachine.GenericCallbacks
using ClimateMachine.ODESolvers
using ClimateMachine.VariableTemplates
using ClimateMachine.SingleStackUtils
using ClimateMachine.BalanceLaws:
    BalanceLaw, Prognostic, Auxiliary, Gradient, GradientFlux, vars_state
using ClimateMachine.ArtifactWrappers

@testset "V Catchment Maxwell River Model" begin
    tv_dataset = ArtifactWrapper(
        @__DIR__,
        isempty(get(ENV, "CI", "")),
        "tiltedv",
        ArtifactFile[ArtifactFile(
            url = "https://caltech.box.com/shared/static/qi2gftjw2vu2j66b0tyfef427xxj3ug7.csv",
            filename = "TiltedVOutput.csv",
        ),],
    )
    tv_dataset_path = get_data_folder(tv_dataset)

    ClimateMachine.init()
    FT = Float64

    soil_water_model = PrescribedWaterModel()
    soil_heat_model = PrescribedTemperatureModel()
    soil_param_functions = nothing

    m_soil = SoilModel(soil_param_functions, soil_water_model, soil_heat_model)

    function x_slope(x, y)
        MFT = eltype(x)
        if x < MFT(800)
            MFT(-0.05)
        elseif x <= MFT(820)
            MFT(0)
        else
            MFT(0.05)
        end
    end

    function y_slope(x, y)
        MFT = eltype(x)
        MFT(-0.02)
    end

    function channel_mannings(x, y)
        MFT = eltype(x)
        return x >= MFT(800) && x <= MFT(820) ? MFT(2.5 * 60 * 10^-3) :
               MFT(2.5 * 60 * 10^-4)
    end

    m_surface = OverlandFlowModel(x_slope, y_slope; mannings = channel_mannings)

    bc = LandDomainBC(
        miny_bc = LandComponentBC(
            surface = Dirichlet((aux, t) -> eltype(aux)(0)),
        ),
        minx_bc = LandComponentBC(
            surface = Dirichlet((aux, t) -> eltype(aux)(0)),
        ),
        maxx_bc = LandComponentBC(
            surface = Dirichlet((aux, t) -> eltype(aux)(0)),
        ),
    )

    function init_land_model!(land, state, aux, localgeo, time)
        state.surface.height = eltype(state)(0)
    end

    # units in m / s
    precip(x, y, t) = t < eltype(t)(90 * 60) ? eltype(t)(3e-6) : eltype(t)(0.0)

    sources = (Precip{FT}(precip),)

    m = LandModel(
        param_set,
        m_soil;
        surface = m_surface,
        boundary_conditions = bc,
        source = sources,
        init_state_prognostic = init_land_model!,
    )

    N_poly_hori = 1
    N_poly_vert = 1
    xres = FT(20)
    yres = FT(20)
    zres = FT(1)
    # Specify the domain boundaries.
    zmax = FT(1)
    zmin = FT(0)
    xmax = FT(1620)
    ymax = FT(1000)

    driver_config = ClimateMachine.MultiColumnLandModel(
        "LandModel",
        (N_poly_hori, N_poly_vert),
        (xres, yres, zres),
        xmax,
        ymax,
        zmax,
        param_set,
        m;
        zmin = zmin,
        numerical_flux_first_order = RusanovNumericalFlux(),
    )

    t0 = FT(0)
    timeend = FT(180 * 60)
    dt = FT(0.5)

    solver_config = ClimateMachine.SolverConfiguration(
        t0,
        timeend,
        driver_config,
        ode_dt = dt,
    )
    mygrid = solver_config.dg.grid
    Q = solver_config.Q

    h_index = varsindex(vars_state(m, Prognostic(), FT), :surface, :height)
    n_outputs = 60

    every_x_simulation_time = ceil(Int, timeend / n_outputs)

    dons = Dict([k => Dict() for k in 1:n_outputs]...)

    iostep = [1]
    callback = GenericCallbacks.EveryXSimulationTime(
        every_x_simulation_time,
    ) do (init = false)
        t = ODESolvers.gettime(solver_config.solver)
        h = Q[:, h_index, :]
        all_vars = Dict{String, Array}("t" => [t], "h" => h)
        dons[iostep[1]] = all_vars
        iostep[1] += 1
        return
    end

    ClimateMachine.invoke!(solver_config; user_callbacks = (callback,))

    aux = solver_config.dg.state_auxiliary
    x = Array(aux[:, 1, :])
    y = Array(aux[:, 2, :])
    z = Array(aux[:, 3, :])
    # Get points at outlet (y = ymax)
    mask2 = (Float64.(y .== 1000.0)) .== 1
    n_outputs = length(dons)
    function compute_Q(h, xv)
        height = max.(h, 0.0)
        v = calculate_velocity(m_surface, xv, 1000.0, height)# put in y = 1000.0
        speed = sqrt(v[1]^2.0 + v[2]^2.0 + v[3]^2.0)
        Q_outlet = speed .* height .* 60.0 # multiply by 60 so it is per minute, not per second
        return Q_outlet
    end
    # We divide by 4 because we have 4 nodal points with the same value at each x (z = 0, 1)
    # Multiply by xres because the solution at each point roughly represents that the solution for that range in x
    Q =
        [
            sum(compute_Q.(Array(dons[k]["h"])[:][mask2[:]], x[mask2[:]]))
            for k in 1:n_outputs
        ] ./ 4.0 .* xres
    data = joinpath(tv_dataset_path, "TiltedVOutput.csv")
    ds_tv = readdlm(data, ',')
    error = sqrt(mean(Q .- ds_tv))
    @test error < 5e-7
end


# AdaBoost ensemble module
module AdaBoost

	using j4pr.ClassifierCombiner
	using j4pr: countapp, ohenc_integer
	using Distances: colwise, Euclidean
	using MLDataPattern: nobs, getobs, datasubset, undersample, targets
	using StatsBase: sample!,weights
	using StatsFuns: softmax!
	using UnicodePlots: lineplot
	export AdaBoostEnsemble, adaboost_train, adaboost_exec

	abstract type BoostType end
	struct AdaBoostM1 <: BoostType end
	struct AdaBoostM2 <: BoostType end

	"""
	AdaBoost ensemble object (it describes the ensemble).

	# Fields
	 * `weights::Vector{Float64}` is a vector weights associated to each ensemble member
	 * `f_train::Function` is the training function
	 * `f_exec::Function` is the execution function
	 * `members::Vector` is a vector containing the trained classifiers
	 * `boost_type::AdaBoost.BoostType` indicates the type of AdaBoost approach used
	 * `combiner::ClassifierCombiner.AbstractCombiner` the means by which ensemble outputs are combined
	"""
	struct AdaBoostEnsemble{F, T<:Vector{S} where S, B<:BoostType, U<:AbstractCombiner}
		weights::Vector{Float64}	# vector of ensemble weights
		f_train::Function		# training function
		f_exec::F			# execution function
		members::T			# ensemble members (trained)
		boost_type::B			# the type of boosting
		combiner::U			# combiner
	end

	# Aliases
	const AdaBoostEnsembleM1Cell{F<:Void, T, B<:AdaBoostM1, U} = AdaBoostEnsemble{F,T,B,U}			# M1 boost, no execution function
	const AdaBoostEnsembleM1Classic{F<:Function, T, B<:AdaBoostM1, U} = AdaBoostEnsemble{F,T,B,U}		# M1 boost, with execution function
	const AdaBoostEnsembleM2Cell{F<:Void, T, B<:AdaBoostM2, U} = AdaBoostEnsemble{F,T,B,U}			# M2 boost, no execution function
	const AdaBoostEnsembleM2Classic{F<:Function, T, B<:AdaBoostM2, U} = AdaBoostEnsemble{F,T,B,U}		# M2 boost, with execution function

	# Printers
	Base.show(io::IO, m::AdaBoostEnsembleM1Cell) = print(io, "AdaBoost M1 ensemble, $(length(m.weights)) members, Cell version")
	Base.show(io::IO, m::AdaBoostEnsembleM2Cell) = print(io, "AdaBoost M2 ensemble, $(length(m.weights)) members, Cell version")
	Base.show(io::IO, m::AdaBoostEnsembleM1Classic) = print(io, "AdaBoost M1 ensemble, $(length(m.weights)) members")
	Base.show(io::IO, m::AdaBoostEnsembleM2Classic) = print(io, "AdaBoost M2 ensemble, $(length(m.weights)) members")

	"""
		adaboost_train(X, y, L, f_train, f_exec, boost_type)

	Trains an AdaBoost classifier. The function returns a `Adaboost` object.

	# Arguments
	 * `X::AbstractMatrix` is the training data; columns are assumed to be samples, lines variables
	 * `y::AvstractVector` are the labels
	 * `L::Int` is the ensemble size
	 * `f_train::Function` is the training function for the ensemble members
	 * `f_exec::Function` is the execution function for the ensemble members
	 * `boost_type::AdaBoost.BoostType` specifies the training algorithm to use. Supported: `AdaBoostM1` and `AdaBoostM2`;

	It is important to note that the training method calls internally `x->f_train((X,y))` where `X` is the input data.
	Considering a generic training function `g` desired to be used in the ensemble, of the following forms:
	 * `g(X, y, arg1, arg2)`, one should provide as argument something as `f_train = (x)->g_train(x[1],y[2], arg1, arg2) # uses labels`
	 * `g(X, arg1, arg2)`, one should provide as argument something as `f_train = (x)->g_train(x[1],arg1, arg2) # ignores labels`
	 * `g(X, arg1, arg2)` where `X==(data,labels)`, one should provide as argument something as `f_train = (x)->g_train(x, arg1, arg2) # MLDataUtils container case`
	 * `g(args...)::CellFunU`, one can simply provide `g.f` (i.e. the training function of the untrained function cell) since it already contains the training arguments.

	During the training, errors are calculated by calling `f_exec(clf,X)` for a given trained ensemble member `clf`.
	"""
	adaboost_train(X::T where T<:AbstractMatrix, y::S where S<:AbstractVector, L::Int, f_train::Function, f_exec::Union{Function,Void}, ::AdaBoostM1) =
		adaboost_train_m1(X, y, L, f_train, f_exec)
	adaboost_train(X::T where T<:AbstractMatrix, y::S where S<:AbstractVector, L::Int, f_train::Function, f_exec::Union{Function,Void}, ::AdaBoostM2) =
		adaboost_train_m2(X, y, L, f_train, f_exec)

	function adaboost_train_m1(X::T where T<:AbstractMatrix, y::S where S<:AbstractVector, L::Int, f_train::Function, f_exec::Union{Function,Void})

		# Initializations
		n = nobs(X)			# number of observations
		yu = sort(unique(y))		# unique labels
		C = length(yu)			# number of classes
		w = fill(1/n, n) 		# sample weights
		beta = fill(0.0,L)		# classifier weight

		k = 1
		it = 0
		maxit = max(L, 10_000)
		mismatches = zeros(n)
		members = Vector{typeof(f_train(getobs(undersample((X,y)))))}(L)
		idxs = Vector{Int}(n)								# sampled observation indexes
		idx = collect(1:n)								# observation indexes
		while k <= L
			it += 1
			sample!(idx, weights(w), idxs)						# get sampling indices

			# Check for missing labels, if any don't go further, resample
			if isempty(setdiff(yu, unique(y[idxs])))

				members[k] = f_train(getobs(datasubset((X,y), idxs)));		# train classifier

				# Split execution between the cases where the execution function is implicit (for FunctionCells) or specified (generic)
				if f_exec isa Void
					mismatches = float(yu[targets(indmax, members[k](X))] .!= y)	# find mismatches (FunctionCell classifier)
				else
					mismatches = float(f_exec(members[k],X) .!= y)		# find mismatches
				end

				err = sum(mismatches .* w)					# calculate error for member 'k'
				if err == 0.0
					fill!(w, 1.0/n)						# re-initialize weights
					k += 1
				elseif err >= (1.0-1/C)
					fill!(w,1.0/n)						# reinitialize weights but do not change iteration
				else
					beta[k] = err/(1-err)					# beta is directly proportional with the error
					w = w .* (beta[k].^(1.0.-mismatches)) 			# if mismatch keep weight, if not, decrease weight
					w = w./sum(w)						# normalize
					k+=1
				end
			end

			if it == maxit && k<L							# Break if maximum number of iterations reached
				warn("[adaboost] Maximum number of $maxit iterations reached. Training interrupted.")
				members = members[1:k]
				break
			end
		end
		return AdaBoostEnsemble(log.(1 ./(beta.+eps())), f_train, f_exec, members, AdaBoostM1(), WeightedVoteCombiner(L, C, log.(1 ./(beta.+eps()))))
	end

	function adaboost_train_m2(X::T where T<:AbstractMatrix, y::S where S<:AbstractVector{V}, L::Int, f_train::Function, f_exec::Union{Function,Void}) where {V}

		# Labels need to be a matrix of Floats encoded OneOfK to easily compute distances
		# between their representation and the probabilities return by the classifiers (ensemble members)
		yu::Vector{V} = sort(unique(y))
		yenc::Vector{Int} = Int.(ohenc_integer(y,yu))

		# Initializations
		n = nobs(X)			# number of observations
		C = length(yu)			# number of classes
		beta = fill(0.0,L)		# classifier weight

		k = 1
		it = 0
		maxit = max(L, 10_000)
		members = Vector{typeof(f_train(getobs(undersample((X,y)))))}(L)

		# Initialize weight vector (mislabel distribution)
		# (The function does not seem to bring any advantage...)
		function _init_weights_!(w::Vector{Float64}, l::Vector{Int})
			ul::Vector{Int} = unique(l)
			counts::Vector{Float64} = length(l) - countapp(l, ul)
			for i in eachindex(l)
				@inbounds w[i] = 1/(length(l)-counts[find(ul.==l[i])][1])
			end
		end
		w = Vector{Float64}(n)
		fill!(w, 1.0/n)
		#_init_weights_!(w, yenc)

		idxs = Vector{Int}(n)								# sampled observation indexes
		idx = collect(1:n)								# observation indexes
		p = zeros(Float64, C, n) 							# ensemble outputs

		while k <= L
			it += 1
			sample!(idx, weights(w), idxs)						# get sampling indices

			# Check for missing labels, if any don't go further, resample
			if isempty(setdiff(yu, unique(yenc[idxs])))

				members[k] = f_train(getobs(datasubset((X,yenc), idxs)));	# train classifier

				# Split execution between the cases where the execution function is implicit (for FunctionCells) or specified (generic)
				if f_exec isa Void
					p = members[k](X)
				else
					p = f_exec(members[k],X)				# 'f_exec' must return a matrix of class-wise probabilities sized C x n
				end

				# Calculate the pseudo-loss
				err=0.0
				@simd for i in 1:n
					@inbounds err+=w[i]*(1-p[yenc[i],i])
				end

				if err == 0.0
					#_init_weights_!(w,yenc)				# re-initialize weights
					fill!(w, 1.0/n)
					k += 1
				elseif err >= (1-1/C)
					#_init_weights_!(w,yenc)				# reinitialize weights but do not change iteration
					fill!(w, 1.0/n)
				else
					beta[k] = err/(1-err)					# beta is directly proportional with the error
					@simd for i in 1:n
						@inbounds w[i] = w[i]*beta[k]^p[yenc[i],i]
					end
					w = w./sum(w)						# normalize
					k+=1
				end
			end

			if it == maxit && k<L							# Break if maximum number of iterations reached
				warn("[adaboost] Maximum number of $maxit iterations reached. Training interrupted.")
				members = members[1:k]
				break
			end
		end
		return AdaBoostEnsemble(log.(1 ./(beta.+eps())), f_train, f_exec, members, AdaBoostM2(), WeightedMeanCombiner(L,C,1.0,log.(1 ./(beta.+eps()))))
	end

	"""
		adaboost_exec(ensemble, X)

	Executes the AdaBoost ensemble.

	# Arguments
	 * `ensemble::AdaBoost.AdaBoostEnsemble` the AdaBoost ensemble
	 * `X::AbstractMatrix` is the data; columns are assumed to be samples, lines variables
	"""
	function adaboost_exec(ensemble::AdaBoostEnsembleM1Classic, X::T where T<:AbstractMatrix)
		out = [ensemble.f_exec(ensemble.members[i], getobs(X)) for i in eachindex(ensemble.members)]
		return combiner_exec(ensemble.combiner, out)

		# If results have to be of matrix form:
		#return float(MLLabelUtils.convertlabel(MLLabelUtils.LabelEnc.OneOfK(Val{ensemble.combiner.C}), combiner_exec(out, ensemble.combiner)))
	end

	function adaboost_exec(ensemble::AdaBoostEnsembleM2Classic, X::T where T<:AbstractMatrix)
		out = combiner_exec(ensemble.combiner,
			 		[ensemble.f_exec(ensemble.members[i], getobs(X)) for i in eachindex(ensemble.members)])
		for j in 1:size(out,2)
			@inbounds softmax!(view(out,:,j))
		end
		return out
	end

	# j4pr case: no execution function, the ensemble members contain it already
	function adaboost_exec(ensemble::AdaBoostEnsembleM1Cell, X::T where T<:AbstractMatrix)
		out = [targets(indmax, ensemble.members[i](getobs(X))) for i in eachindex(ensemble.members)]  # the 'targets' function is used to get the labels
		return combiner_exec(ensemble.combiner, out)

		# If results have to be of matrix form:
		#return float(MLLabelUtils.convertlabel(MLLabelUtils.LabelEnc.OneOfK(Val{ensemble.combiner.C}), combiner_exec(out, ensemble.combiner)))
	end

	function adaboost_exec(ensemble::AdaBoostEnsembleM2Cell, X::T where T<:AbstractMatrix)
		out = combiner_exec(ensemble.combiner,
		     			[ensemble.members[i](getobs(X)) for i in eachindex(ensemble.members)])
		for j in 1:size(out,2)
			@inbounds softmax!(view(out,:,j))
		end
		return out
	end

end

##########################
# FunctionCell Interface #
##########################
"""
	adaboost(f, L; boost_type=AdaBoostM2())

Creates an untrained AdaBoost ensemble.

# Arguments
 * `f::CellFunU` is a untrained cell corresponding to the classifer/transform that the ensemble will contain
 * `L::Int` is the size of the ensemble

# Keyword arguments
 * `boost_type::AdaBoost.BoostType` specifies whether to use the discrete `AdaBoostM1()` or continuous `AdaBoostM2()` approaches in AdaBoost (default `AdaBoostM1()`)

For more information:
	[1] J. Friedman, T. Hastie, R. Tibshirani "Additive Logistic Regression: A statistical view of boosting" 2000 The Annals of Statistics 28(2): 337-407.

Try `?j4pr.AdaBoost.adaboost_train` and `?j4pr.AdaBoost.adaboost_exec` and for more details.

# Examples
```
julia> using j4pr
```
"""
adaboost(f::F where F<:CellFunU, L::Int; boost_type::AdaBoost.BoostType=AdaBoost.AdaBoostM2()) =
	FunctionCell(adaboost, (f, L), ModelProperties(), "Adaboost ensemble ($(f.tinfo), $L members)"; boost_type=boost_type) # untrained function cell

############################
# DataCell/Array Interface #
############################
"""
	adaboost(x, f, L; boost_type=AdaBoostM2())

Trains an AdaBoost ensemble using the data `x`.
"""
# Training
adaboost(x::T where T<:CellDataL, f::F where F<:CellFunU, L::Int; boost_type::AdaBoost.BoostType=AdaBoost.AdaBoostM2()) =
	adaboost((getx!(x), gety(x)), f, L; boost_type=boost_type)

adaboost(x::Tuple{T,S} where T<:AbstractVector where S<:AbstractVector, f::F where F<:CellFunU, L::Int; boost_type::AdaBoost.BoostType=AdaBoost.AdaBoostM2()) =
	adaboost((mat(x[1], LearnBase.ObsDim.Constant{2}()), x[2]), f, L; boost_type=boost_type)

adaboost(x::Tuple{T,S} where T<:AbstractMatrix where S<:AbstractVector, f::F where F<:CellFunU, L::Int; boost_type::AdaBoost.BoostType=AdaBoost.AdaBoostM2()) =
begin

	@assert nobs(x[1]) == nobs(x[2]) "[adaboost] Expected $(nobs(x[1])) labels/values, got $(nobs(x[2]))."

	# Transform labels first
	enc = labelencn(x[2])
	yenc = label2ind.(x[2],enc)

	# Train model
	ensembledata = AdaBoost.adaboost_train(x[1], yenc, L, f.f, nothing, boost_type)

	# Build model properties
	modelprops = ModelProperties(nvars(x[1]), length(enc.label), enc)

	FunctionCell(adaboost, Model(ensembledata, modelprops), "Adaboost ensemble ($(f.tinfo), $L members)")
end

# Execution
adaboost(x::T where T<:CellData, model::Model{<:AdaBoost.AdaBoostEnsemble}) =
	datacell(adaboost(getx!(x), model), gety(x))
adaboost(x::T where T<:AbstractVector, model::Model{<:AdaBoost.AdaBoostEnsemble}) =
	adaboost(mat(x, LearnBase.ObsDim.Constant{2}()), model)
adaboost(x::T where T<:AbstractMatrix, model::Model{<:AdaBoost.AdaBoostEnsemble}) =
	AdaBoost.adaboost_exec(model.data,x)


module IndirectArrays

export IndirectArray

"""
    IndirectArray(index, values)

creates an array `A` where the values are looked up in the value table,
`values`, using the `index`.  Concretely, `A[i,j] =
values[index[i,j]]`.
"""
struct IndirectArray{T,N,A<:AbstractArray{<:Integer,N},V<:AbstractVector{T}} <: AbstractArray{T,N}
    index::A
    values::V

    @inline function IndirectArray{T,N,A,V}(index, values) where {T,N,A,V}
        # The typical logic for testing bounds and then using
        # @inbounds will not check whether index is inbounds for
        # values. So we had better check this on construction.
        @boundscheck checkbounds(values, index)
        new{T,N,A,V}(index, values)
    end
end
Base.@propagate_inbounds IndirectArray(index::AbstractArray{<:Integer,N}, values::AbstractVector{T}) where {T,N} =
    IndirectArray{T,N,typeof(index),typeof(values)}(index, values)
function IndirectArray{T}(A::AbstractArray) where {T}
    values = unique(A)
    index = convert(Array{T}, indexin(A, values))
    return IndirectArray(index, values)
end
IndirectArray(A::AbstractArray) = IndirectArray{UInt8}(A)

Base.size(A::IndirectArray) = size(A.index)
Base.axes(A::IndirectArray) = axes(A.index)
Base.IndexStyle(::Type{IndirectArray{T,N,A,V}}) where {T,N,A,V} = IndexStyle(A)

Base.copy(A::IndirectArray) = IndirectArray(copy(A.index), copy(A.values))

@inline function Base.getindex(A::IndirectArray, i::Int)
    @boundscheck checkbounds(A.index, i)
    @inbounds idx = A.index[i]
    @boundscheck checkbounds(A.values, idx)
    @inbounds ret = A.values[idx]
    ret
end

@inline function Base.getindex(A::IndirectArray{T,N}, I::Vararg{Int,N}) where {T,N}
    @boundscheck checkbounds(A.index, I...)
    @inbounds idx = A.index[I...]
    @boundscheck checkbounds(A.values, idx)
    @inbounds ret = A.values[idx]
    ret
end

@inline function Base.setindex!(A::IndirectArray, x, i::Int)
    @boundscheck checkbounds(A.index, i)
    idx = findfirst(isequal(x), A.values)
    if idx == nothing
        push!(A.values, x)
        A.index[i] = length(A.values)
    else
        A.index[i] = idx
    end
    return A
end

@inline function Base.push!(A::IndirectArray{T,1} where T, x)
    idx = findfirst(isequal(x), A.values)
    if idx == nothing
        push!(A.values, x)
        push!(A.index, length(A.values))
    else
        push!(A.index, idx)
    end
    return A
end

function Base.append!(A::IndirectArray{T,1}, B::IndirectArray{T,1}) where T
    if A.values == B.values
        append!(A.index, B.index)
    else # pretty inefficient but let's get something going
        for b in B
            push!(A, b)
        end
    end
    return A
end

function Base.append!(A::IndirectArray{<:Any,1}, B::AbstractVector)
    for b in B
        push!(A, b)
    end
    return A
end

end # module


using Pkg
Pkg.activate(".")
Pkg.add(PackageSpec(url="https://github.com/AlgebraicJulia/Decapods.jl.git"))
Pkg.instantiate()


# this is just to test locally, since we don't want
# to impose Gadfly dependency

p = plot(x=[1])
Image0 = Image(12, 16, p)

c = Code("""
h = 1/2
F' = sin(x)
""")

l = document([
    DocumentClass("article", ["11pt", "a4paper"]),
    Title("Title"),
    Section("Section",[
        Figure("Figure",Image0),
    ]),
])

openpdf(l)