"""
    mpifit_hsvgp(get_data, n_parts, n_dims, bounds_low, bounds_high;
    ni_low     = 5,
    ni_high    = 50,
    batch_size = 20,
    n_iters    = 200,
    n_subcycle_ELBO_local  = 20,
    n_subcycle_ELBO_global = 20)

Fit HSVGP to data for distributed computation

    get_data:                function to define how a local partition should read in its data
    ni_low:                  number of local inducing points
    ni_high:                 number of global inducing points
    batch_size:              size of batches for stochastic optimizer
    n_iters:                 number of optimization steps
    n_subcycle_ELBO_local:   subcycles for local  ELBO before MPI communication
    n_subcycle_ELBO_global:  subcycles for global ELBO before MPI communication
    n_batch:                 number of partitions in a local to global communication batch

WARNING: Currently writes out several jld2 files containing local and global GPss
"""
function mpifit_hsvgp(get_data, n_parts, n_dims, bounds_low, bounds_high;
    ni_low     = 5,               # Number of local inducing points
    ni_high    = 50,              # Number of global inducing points
    batch_size = 20,              # Number of points used in updating local ELBO
    n_iters    = 200,             # Total updates in whole cycle
    n_subcycle_ELBO_local  = 20,  # subcycles for local ELBO before comm.
    n_subcycle_ELBO_global = 20,  # subcycles for global ELBO before comm.
    n_batch    = 5)               # Number of partitions selected at each iteration

    #TODO:
    #     Need to clean up saving out of local and global objects at the end.
    #     It's messy now and will get worse with many MPI processes.
    #
    #     Would be nice to write a function for prediction to identify which
    #     partition a new value of X would be in, then direct the prediction to
    #     that local svgp
    #
    #     Need to generalize the interface here. Should have a way to pass in
    #     the structure of data to be worked with. Maybe pass in a get_data
    #     function that returns the x,y data for each partition.
    #
    #     Would like to clean up some of those arguments also. I dont think
    #     we should need to have to pass all that stuff in, but not sure the
    #     best way to give global bounds information without simply specifying
    #     it for cases where all the data cannot be stored on one node.
    #

    grad_clip = 1.e-3

    MPI.Init()
    comm = MPI.COMM_WORLD
    myrank = MPI.Comm_rank(comm)
    mysize = MPI.Comm_size(comm)

    root = 0 # global node

    # Fixing this for Gaussian case for now.
    #     Need to generalize this for the future but that will come in part of a large generalization of the code
    # n_latent = 1

    total_inducing = n_parts * ni_low

    part_split = [[ii] for ii in 1:(mysize-1)]

    if n_parts > mysize - 1
        for ii = mysize:n_parts
            append!(part_split[1 + ((ii-1) % (mysize - 1))], ii)
        end
    end

    ######################## DATA SPLIT ########################################
    # This part need not be done separately. Data will already be stored on nodes

    # Root process is the global node and rest of the processes are local processes
    # datarow goes from 2 to 48601
    # numrowscsv = 48600
    # numrowseachrank = Int(numrowscsv/(mysize-1))

    if myrank != root

        data_array = [get_data(part) for part in part_split[myrank]]
        len        = length(part_split[myrank])

    end # rank != root

    # Initializing "trace" array to store ELBO values
    if myrank == root
        trace_h       = zeros(n_iters)          # To collect the global ELBO value
        store_trace_l = zeros(n_iters,n_parts) # Store trace_l from all proesses
    end
    if myrank != root
        trace_l = [zeros(n_iters) for ii in 1:len] # To collect the local ELBO value
    end

    # Now that each local node has seen the data, it will send xi_low, cov_low,
    # mean_low to global node.
    # Once the global node gets data from local nodes, it will evaluate xi_high,
    # cov_high, mean_high and theta_high
    # Following that, global node will calculate prior_mean and prior_cov and
    # send it back to local nodes.

    ###################### INITIALIZATION ######################################

    if myrank != root
        # Step 1
        # Build local SVGPs ###################
        local_svgps = [SVGP_obj(data_array[ii][1], float(data_array[ii][2]), ni_low) for ii in 1:len]

        for ii in 1:len
            # Step 2
            # Sending mean_factor, xi_low, cov_low, mean_low and N to global #######
            send_inducing_points_to_global(local_svgps[ii].params.inducing_locs, local_svgps[ii].params.inducing_mean, root, comm)
        end

        # Step 3
        # Initialize local optimizers
        lopt_cmean = [Flux.Optimiser(Flux.ClipValue(grad_clip), ADAM(1. * 0.0075)) for ii in 1:len]
        lopt_lrho  = [Flux.Optimiser(Flux.ClipValue(grad_clip), ADAM(1. * 0.0075)) for ii in 1:len]
        lopt_lkap  = [Flux.Optimiser(Flux.ClipValue(grad_clip), ADAM(1. * 0.0075)) for ii in 1:len]
        lopt_lsig  = [Flux.Optimiser(Flux.ClipValue(grad_clip), ADAM(1. * 0.0075)) for ii in 1:len]
        loptm      = [Flux.Optimiser(Flux.ClipValue(grad_clip), ADAM(1. * 0.01)  ) for ii in 1:len]
        loptS      = [Flux.Optimiser(Flux.ClipValue(grad_clip), ADAM(1. * 0.002) ) for ii in 1:len]
        loptx      = [Flux.Optimiser(Flux.ClipValue(grad_clip), ADAM(1. * 0.01)  ) for ii in 1:len]

    end # myrank != root

    if myrank == root
        wts = StatsBase.AnalyticWeights(ones(n_parts));

        # Step 3
            # Initialize global svgp and optimizers
            # Root process will receive inducing point data
            # Root process will update global parameters
            # Root process will calculate prior_mean prior_cov for local nodes
            # Root process will send prior_mean prior_cov back to local processes

        # Initialize global optimizers
        opt_cmean = Flux.Optimiser(Flux.ClipValue(grad_clip), ADAM(1. * 0.0075))
        opt_lrho  = Flux.Optimiser(Flux.ClipValue(grad_clip), ADAM(1. * 0.0075))
        opt_lkap  = Flux.Optimiser(Flux.ClipValue(grad_clip), ADAM(1. * 0.0075))
        opt_lsig  = Flux.Optimiser(Flux.ClipValue(grad_clip), ADAM(1. * 0.0075))
        optm      = Flux.Optimiser(Flux.ClipValue(grad_clip), ADAM(1. * 0.01)  )
        optS      = Flux.Optimiser(Flux.ClipValue(grad_clip), ADAM(1. * 0.002) )
        optx      = Flux.Optimiser(Flux.ClipValue(grad_clip), ADAM(1. * 0.01)  )

        # Initialize global svgp
        # TODO:
        #     Need bounds on input space and some idea of scale for output
        #         to set initial GP variance parameter and
        #         Latin Hypercube for inducing point locations
        #
        #     Assuming we will work in the case that we can't send all inducing point
        #         data to global. Instead should probably get bounds as arguments to original
        #         function or otherwise.
        fake_data_to_init = SVGP_data(
            vcat(bounds_low', bounds_high'),
            randn(4)*10 # sampling 4 points with something like the variance I was to initialize at
        )
        global_svgp = SVGP_obj(ni_high, SVGP_data("global"), SVGP_params(fake_data_to_init, ni_high), "gaussian")

        for part = (root+1):n_parts
            recv_id = 1 + (part - 1) % (mysize - 1)
            # TODO: Would prefer to not do this sequentially by rank number but as communications arrive
            #

            # println("In Process(1): ",recv_id,"\n")

            # Receiving xi_low and yi_low #
            xi_low, yi_low = receive_inducing_points_in_global(ni_low, n_dims, recv_id, comm) ### TODO: IMPORTANT! Need to communicate which partition is being recieved

            # Update global parameters from local data #
            grads  = gradient(gps -> svgp_elbo_global(
                    xi_low,
                    yi_low,
                    gps,
                    global_svgp
                    ), global_svgp.params)[1][]

            Flux.Optimise.update!(opt_cmean, global_svgp.params.const_mean,    -grads.const_mean)
            Flux.Optimise.update!(opt_lrho,  global_svgp.params.log_rho,       -grads.log_rho)
            Flux.Optimise.update!(opt_lkap,  global_svgp.params.log_kappa,     -grads.log_kappa)
            Flux.Optimise.update!(opt_lsig,  global_svgp.params.log_sigma,     -grads.log_sigma)
            Flux.Optimise.update!(optm,      global_svgp.params.inducing_mean, -grads.inducing_mean)
            Flux.Optimise.update!(optS,      global_svgp.params.inducing_L,    -LowerTriangular(grads.inducing_L) )
            Flux.Optimise.update!(optx,      global_svgp.params.inducing_locs, -grads.inducing_locs)
            # Sending prior_mean, prior_cov to local process ###################
            send_prior_to_local( pred_vgp(xi_low, global_svgp.params), recv_id, comm)

        end # for loop
    end

    MPI.Barrier(comm)
    if myrank != root
        #Get prior information from globa
        prior_mean, prior_sd = receive_prior_in_local(
                                                        ni_low,
                                                        root,
                                                        comm)
    end # myrank == root

    ###################### END OF INITIALIZATION ###############################

    ##################### OUTER LOOP ###########################################

    for iter= 1:n_iters
        if mod(iter, 2000) == 0
            print(iter, " ")
        end

        if myrank != root

            for ii in 1:len ## TODO: Clean this up with Kelin to process non-sequentially
                            ## TODO: ENSURE THE MPI PROPERLY ASSOCIATED DATA COMMUNICATED TO CORRECT PARTITION
                # Step 4
                # Each local process will receive prior_mean and prior_cov

                # Receiving prior_mean, prior_cov to local process #################
                # prior_mean, prior_sd = receive_prior_in_local(
                #                                        ni_low,
                #                                        root,
                #                                        comm)
                # println("prior_mean, prior_cov received in local")

                # Step 5
                # Update local svgp using prior_mean prior_cov from global
                nsteps_curr  = rand(Distributions.Poisson(n_subcycle_ELBO_local)) + 3;
                for sub_iter = 1:nsteps_curr
                    inds   = rand(1:size(local_svgps[ii].data.y)[1], batch_size)

                    # Update global parameters from local data #
                    grads  = gradient(gps -> svgp_elbo_local(
                            local_svgps[ii].data.x[inds,:],
                            local_svgps[ii].data.y[inds],
                            gps,
                            local_svgps[ii],
                            (prior_mean, prior_sd)
                            ), local_svgps[ii].params)[1][]

                    Flux.Optimise.update!(lopt_cmean[ii], local_svgps[ii].params.const_mean,    -grads.const_mean)
                    Flux.Optimise.update!(lopt_lrho[ii],  local_svgps[ii].params.log_rho,       -grads.log_rho)
                    Flux.Optimise.update!(lopt_lkap[ii],  local_svgps[ii].params.log_kappa,     -grads.log_kappa)
                    Flux.Optimise.update!(lopt_lsig[ii],  local_svgps[ii].params.log_sigma,     -grads.log_sigma)
                    Flux.Optimise.update!(loptm[ii],      local_svgps[ii].params.inducing_mean, -grads.inducing_mean)
                    Flux.Optimise.update!(loptS[ii],      local_svgps[ii].params.inducing_L,    -LowerTriangular(grads.inducing_L) )
                    Flux.Optimise.update!(loptx[ii],      local_svgps[ii].params.inducing_locs, -grads.inducing_locs)
                    # # Sending prior_mean, prior_cov to local process ###################
                    # send_prior_to_local( pred_vgp(xi_low, global_svgp.params[jj]), recv_id, comm)

                    if sub_iter == nsteps_curr
                        # Collecting local ELBO in an array ###########################
                        trace_l[ii][iter] = svgp_elbo_local(
                            local_svgps[ii].data.x[inds,:],
                            local_svgps[ii].data.y[inds],
                            local_svgps[ii].params,
                            local_svgps[ii],
                            (prior_mean, prior_sd)
                        )
                    end # if sub_iter
                end # for sub_iter
                # println("local ELBO optimized")

                # THIS IS NOW DONE OUTSIDE THE ii LOOP
                # Step 6
                # Sending inducing point data to global
                # for jj in 1:n_latent
                #     send_inducing_points_to_global(local_svgps[ii].params[jj].inducing_locs, local_svgps[ii].params[jj].inducing_mean, root, comm)
                # end
            end # for ii

            # Kelin block for sending inducing point data to global only if you were selected
            is_message, status = MPI.Iprobe(0, 8, comm)
            is_message, status = MPI.Iprobe(0, 8, comm) # Kelin: Silly solution, but try this for now

            msg  = [1];
            msg2 = ones(1);

            if is_message
                MPI.Recv!(msg, 0, 8, comm);
                global msg = convert(Vector{Int64}, msg)
                global msg2 = convert(Vector{Int64}, ones(msg[1]));
                #println("process ", myrank, "recieved first message ", msg);
                MPI.Recv!(msg2, 0, 10+myrank, comm);
                global msg2 = msg2;
                #println("process ", myrank, "recieved second message ", msg2);

                indx = findall(in(part_split[myrank]), msg2)
                for ii in indx
                    send_inducing_points_to_global(local_svgps[ii].params.inducing_locs, local_svgps[ii].params.inducing_mean, root, comm)
                end
               #println("process ", myrank, "sent inducing points")

               #Get prior information from global
               prior_mean, prior_sd = receive_prior_in_local(
                                                        ni_low,
                                                        root,
                                                        comm)
            end # if is_message
        end # if myrank != root

        if myrank == root
            #Select random local partition for batch
            aa = collect(1:n_parts);
            selection = convert(Vector{Int64}, zeros(n_batch));
            StatsBase.efraimidis_ares_wsample_norep!(aa, wts, selection)
            part_split_curr = [intersect(part_split[ii], selection) for ii in 1:(mysize-1)];
            wts[selection] .= 0;
            wts .+= 1;

            for ii in 1:(mysize-1)
               msg_to_send = part_split_curr[ii]
               if length(msg_to_send) > 0
                  MPI.Isend([length(msg_to_send)], ii, 8, comm)
                  MPI.Isend(msg_to_send, ii, 10+ii, comm);
                  #println("sending message ", msg_to_send, "to process ", ii)
               end
            end

            step_elbo = 0.
            for part = 1:n_batch
                status  = MPI.Probe(MPI.MPI_ANY_SOURCE, MPI.MPI_ANY_TAG, comm)
                xi_low  = zeros(ni_low, n_dims)

                rmsg    = MPI.Recv!(xi_low, status.source, 0, comm)
                yi_low  = zeros(ni_low)

                rmsg    = MPI.Recv!(yi_low, status.source, 1, comm)
                recv_id = status.source

                # Update global parameters from local data #
                for sub_iter = 1:n_subcycle_ELBO_global
                    grads  = gradient(gps -> svgp_elbo_global(
                            xi_low,
                            yi_low,
                            gps,
                            global_svgp
                            ), global_svgp.params)[1][]

                    Flux.Optimise.update!(opt_cmean, global_svgp.params.const_mean,    -grads.const_mean)
                    Flux.Optimise.update!(opt_lrho,  global_svgp.params.log_rho,       -grads.log_rho)
                    Flux.Optimise.update!(opt_lkap,  global_svgp.params.log_kappa,     -grads.log_kappa)
                    Flux.Optimise.update!(opt_lsig,  global_svgp.params.log_sigma,     -grads.log_sigma)
                    Flux.Optimise.update!(optm,      global_svgp.params.inducing_mean, -grads.inducing_mean)
                    Flux.Optimise.update!(optS,      global_svgp.params.inducing_L,    -LowerTriangular(grads.inducing_L) )
                    Flux.Optimise.update!(optx,      global_svgp.params.inducing_locs, -grads.inducing_locs)
                end # for sub_iter
                # println("global ELBO optimized")

                # Update step_elbo
                step_elbo += svgp_elbo_global(xi_low, yi_low, global_svgp.params, global_svgp)

                # Sending prior_mean, prior_cov to local process ###################
                send_prior_to_local( pred_vgp(xi_low, global_svgp.params), recv_id, comm) # MAKE SURE COMM. INDICATES WHICH PARTITION IN A PROCESS THIS COMM IS FOR
            end # for part

            trace_h[iter] = step_elbo
        end # myrank == root

        barrier_req = MPI.Ibarrier(comm);
        barrier_flag, barrier_status = MPI.Test!(barrier_req);
        while !barrier_flag
            is_message, status = MPI.Iprobe(0, 8, comm)
            is_message, status = MPI.Iprobe(0, 8, comm) #Silly solution, but try this for now
            msg  = [1];
            msg2 = ones(1);
            #println("process ", myrank, "message recieved? ", is_message)
            if is_message
                MPI.Recv!(msg, 0, 8, comm);
                global msg = convert(Vector{Int64}, msg);
                global msg2 = convert(Vector{Int64}, ones(msg[1]));
                #println("process ", myrank, "recieved first message ", msg);
                MPI.Recv!(msg2, 0, 10+myrank, comm);
                global msg2 = msg2;
                #println("process ", myrank, "recieved second message ", msg2);

                indx = findall(in(part_split[myrank]), msg2)
                #println("process ", myrank, "indx = ", indx);
                for ii in indx
                    send_inducing_points_to_global(local_svgps[ii].params.inducing_locs, local_svgps[ii].params.inducing_mean, root, comm)
                end
                #println("process ", myrank, "sent inducing points")

                #Get prior information from global
                prior_mean, prior_sd = receive_prior_in_local(ni_low, root, comm)
            end
            barrier_flag, barrier_status = MPI.Test!(barrier_req);
        end # while !barrier_flag
    end # outeriter

    ############################################################################
    # Returning trace_h and trace_l to main function
    if myrank != root

        for ii in 1:len
            send_trace(trace_l[ii], root ,comm)
        end

    end #myrank != root

    if myrank == root

        for part = (root+1):n_parts
            recv_id = 1 + (part - 1) % (mysize - 1)
            trace_l = receive_trace(n_iters, recv_id, comm)
            store_trace_l[:,part] = trace_l
        end # recv_id

        return_trace = hcat(trace_h, store_trace_l)
        # println(return_trace)

        # Write trace to a csv file
        df_return_trace = DataFrame(return_trace)
        CSV.write("./return_trace.csv", df_return_trace)

    end # myrank == root
    ############################################################################

    ############################################################################
    # Save svgp_params
    if myrank != root

        # send_svgp_params(local_svgp.params, root ,comm)
        for ii in 1:len
            save(string("local_fitted_parameters",part_split[myrank][ii],"_",mysize-1, ".jld2"), string("local_svgp",part_split[myrank][ii]), local_svgps[ii])
        end

    end #myrank != root

    if myrank == root

        # local_svgp_params = [receive_svgp_params(recv_id, comm) for recv_id = 1:mysize-1]

        save(string("global_fitted_svgp_",mysize-1,".jld2"), "global_svgp", global_svgp)

    end # myrank == root
    ############################################################################

    MPI.Finalize()

end     # mpifit_HSVGP


"""
A file with structs and functions for sampling hops and updating hopping rates
"""

# TODO to simplify the design can do two things:
# 1. make an abstract type HopRatesGridOptim (grid-optimized) and dispatch on it. Or maybe dispatch on the type of hop_const_cumulative_sums instead (Array{F} vs Array{Vector{F}})
# 2. make an abstract type HopRatesGraphDsi, which has hop rates uniform wrt neighbors, and treat things not of that type as having hop_const_cumulative_sums field

### spatial hop rates ###
abstract type AbstractHopRates end

HopRates(hopping_constants::Matrix{F}, spatial_system) where F <: Number = HopRatesGraphDsi(hopping_constants)
HopRates(hopping_constants::Matrix{F}, grid::Union{CartesianGridRej, CartesianGridIter}) where F <: Number = HopRatesGraphDsi(hopping_constants)

HopRates(hopping_constants::Matrix{Vector{F}}, spatial_system) where F <: Number = HopRatesGraphDsij(hopping_constants)
HopRates(hopping_constants::Matrix{Vector{F}}, grid::Union{CartesianGridRej, CartesianGridIter}) where F <: Number = HopRatesGridDsij(hopping_constants, grid)

HopRates(p::Pair{SpecHop, SiteHop}, spatial_system) where {SpecHop <: Vector{F}, SiteHop <: Vector{Vector{F}}} where F <: Number = HopRatesGraphDsLij(p...)
HopRates(p::Pair{SpecHop, SiteHop}, grid::Union{CartesianGridRej, CartesianGridIter}) where {SpecHop <: Vector{F}, SiteHop <: Vector{Vector{F}}} where F <: Number = HopRatesGridDsLij(p..., grid)

HopRates(p::Pair{SpecHop, SiteHop}, spatial_system) where {SpecHop <: Matrix{F}, SiteHop <: Vector{Vector{F}}} where F <: Number = HopRatesGraphDsiLij(p...)
HopRates(p::Pair{SpecHop, SiteHop}, grid::Union{CartesianGridRej, CartesianGridIter}) where {SpecHop <: Matrix{F}, SiteHop <: Vector{Vector{F}}} where F <: Number = HopRatesGridDsiLij(p..., grid)

"""
    update_hop_rates!(hop_rates::AbstractHopRates, species::AbstractArray, u, site, spatial_system)

update rates of all specs in species at site
"""
function update_hop_rates!(hop_rates::AbstractHopRates, species::AbstractArray, u, site, spatial_system)
    @inbounds for spec in species
        update_hop_rate!(hop_rates, spec, u, site, spatial_system)
    end
end

"""
    update_hop_rate!(hop_rates::HopRatesGraphDsi, species, u, site, spatial_system)

update rates of single species at site
"""
function update_hop_rate!(hop_rates::AbstractHopRates, species, u, site, spatial_system)
    rates = hop_rates.rates
    @inbounds old_rate = rates[species, site]
    @inbounds rates[species, site] = evalhoprate(hop_rates, u, species, site, spatial_system)
    @inbounds hop_rates.sum_rates[site] += rates[species, site] - old_rate
    old_rate
end

"""
    total_site_hop_rate(hop_rates::AbstractHopRates, site)

return total hopping rate out of site
"""
total_site_hop_rate(hop_rates::AbstractHopRates, site) = @inbounds hop_rates.sum_rates[site]

"""
    reset!(hop_rates::AbstractHopRates)

make all rates zero
"""
function reset!(hop_rates::AbstractHopRates)
    hop_rates.rates .= zero(eltype(hop_rates.rates))
    hop_rates.sum_rates .= zero(eltype(hop_rates.sum_rates))
    nothing
end

"""
    sample_species(hop_rates::AbstractHopRates, site, rng)

sample species to hop from site
"""
sample_species(hop_rates::AbstractHopRates, site, rng) = @inbounds linear_search((@view hop_rates.rates[:,site]), rand(rng) * total_site_hop_rate(hop_rates, site))

"""
    sample_hop_at_site(hop_rates::AbstractHopRates, site, rng, spatial_system)

sample a reaction at site, return (species, target_site)
"""
function sample_hop_at_site(hop_rates::AbstractHopRates, site, rng, spatial_system)
    species = sample_species(hop_rates, site, rng)
    return species, sample_target_site(hop_rates, site, species, rng, spatial_system)
end

############## hopping rates of form D_{s,i} ################
struct HopRatesGraphDsi{F} <: AbstractHopRates
    "hopping_constants[i,j] is the hop constant of species i at site j"
    hopping_constants::Matrix{F}

    "rates[i,j] is total hopping rate of species i at site j"
    rates::Matrix{F}

    "sum_rates[j] is the sum of hopping rates at site j"
    sum_rates::Vector{F}
end

function Base.show(io::IO, ::MIME"text/plain", hop_rates::HopRatesGraphDsi)
    num_specs, num_sites = size(hop_rates.rates)
    println(io, "HopRates with $num_specs species and $num_sites sites. \nHopping constants of form D_{s,i} where s is species, and i is source.")
end

"""
    HopRatesGraphDsi(hopping_constants::Matrix{F}) where F <: Number

initializes HopRatesGraphDsi with zero rates
"""
function HopRatesGraphDsi(hopping_constants::Matrix{F}) where F <: Number
    rates = zeros(F, size(hopping_constants))
    HopRatesGraphDsi{F}(hopping_constants, rates, zeros(F, size(rates, 2)))
end

sample_target_site(hop_rates::HopRatesGraphDsi, site, species, rng, spatial_system) = rand_nbr(rng, spatial_system, site)

"""
return hopping rate of species at site
"""
function evalhoprate(hop_rates::HopRatesGraphDsi, u, species, site, spatial_system)
    @inbounds u[species,site]*hop_rates.hopping_constants[species,site]*outdegree(spatial_system, site)
end

############## hopping rates of form D_{s,i,j} ################
struct HopRatesGraphDsij{F} <: AbstractHopRates
    "hop_const_cumulative_sums[s,i] is the vector of cumulative sums of hopping constants of species s at site i"
    hop_const_cumulative_sums::Matrix{Vector{F}}

    "rates[s,i] is the total hopping rate of species s at site i"
    rates::Matrix{F}

    "sum_rates[i] is the total hopping rate out of site i"
    sum_rates::Vector{F}
end

function Base.show(io::IO, ::MIME"text/plain", hop_rates::HopRatesGraphDsij)
    num_specs, num_sites = size(hop_rates.rates)
    println(io, "HopRates with $num_specs species and $num_sites sites. \nHopping constants of form D_{s,i,j} where s is species, i is source and j is destination.")
end

"""
    HopRatesGraphDsij(hopping_constants::Matrix{Vector{F}}) where F <: Number

initializes HopRates with zero rates
"""
function HopRatesGraphDsij(hopping_constants::Matrix{Vector{F}}; do_cumsum = true) where F <: Number
    do_cumsum && (hopping_constants = map(cumsum, hopping_constants))
    rates = zeros(F, size(hopping_constants))
    sum_rates = zeros(F, size(rates, 2))
    HopRatesGraphDsij{F}(hopping_constants, rates, sum_rates)
end

function sample_target_site(hop_rates::HopRatesGraphDsij, site, species, rng, spatial_system)
    @inbounds cumulative_hop_constants = hop_rates.hop_const_cumulative_sums[species, site]
    @inbounds n = searchsortedfirst(cumulative_hop_constants, rand(rng) * cumulative_hop_constants[end])
    return nth_nbr(spatial_system, site, n)
end

function evalhoprate(hop_rates::HopRatesGraphDsij, u, species, site, spatial_system)
    @inbounds u[species, site] * hop_rates.hop_const_cumulative_sums[species, site][end]
end

#################  hopping rates of form L_{s,i,j} optimized for cartesian grid  ######################
"""
Analogue of HopRatesGraphDsij, optimized for CartesianGrid
"""
struct HopRatesGridDsij{F} <: AbstractHopRates
    "hop_const_cumulative_sums[:,s,i] is the vector of cumulative sums of hopping constants of species s at site i"
    hop_const_cumulative_sums::Array{F, 3}

    "rates[s,i] is the total hopping rate of species s at site i"
    rates::Matrix{F}

    "sum_rates[i] is the total hopping rate out of site i"
    sum_rates::Vector{F}
end

function Base.show(io::IO, ::MIME"text/plain", hop_rates::HopRatesGridDsij)
    num_specs, num_sites = size(hop_rates.rates)
    println(io, "HopRates with $num_specs species and $num_sites sites, optimized for CartesianGrid. \nHopping constants of form L_{s,i,j} where s is species, i is source and j is destination.")
end

"""
    HopRatesGridDsij(hopping_constants::Matrix{Vector{F}}) where F <: Number

initializes HopRates with zero rates
"""
function HopRatesGridDsij(hopping_constants::Array{F, 3}; do_cumsum = true) where F <: Number
    do_cumsum && (hopping_constants = mapslices(cumsum, hopping_constants, dims = 1))
    rates = zeros(F, size(hopping_constants)[2:3])
    sum_rates = zeros(F, size(rates, 2))
    HopRatesGridDsij{F}(hopping_constants, rates, sum_rates)
end

function HopRatesGridDsij(hopping_constants::Matrix{Vector{F}}, grid) where F <: Number
    new_hopping_constants = Array{F, 3}(undef, 2*dimension(grid), size(hopping_constants)...)
    for ci in CartesianIndices(hopping_constants)
        species, site = Tuple(ci)
        nb_constants = @view new_hopping_constants[:, species, site]
        pad_hop_vec!(nb_constants, grid, site, hopping_constants[ci])
        cumsum!(nb_constants, nb_constants)
    end
    HopRatesGridDsij(new_hopping_constants, do_cumsum = false)
end

function sample_target_site(hop_rates::HopRatesGridDsij, site, species, rng, grid)
    @inbounds cumulative_hop_constants = @view hop_rates.hop_const_cumulative_sums[:,species, site]
    @inbounds n = searchsortedfirst(cumulative_hop_constants, rand(rng) * cumulative_hop_constants[end])
    return nth_potential_nbr(grid, site, n)
end

function evalhoprate(hop_rates::HopRatesGridDsij, u, species, site, spatial_system)
    @inbounds u[species, site] * hop_rates.hop_const_cumulative_sums[end, species, site]
end

############## hopping rates of form D_s * L_{i,j} ################
struct HopRatesGraphDsLij{F} <: AbstractHopRates
    "hopping constants of species -- D_s"
    species_hop_constants::Vector{F}

    "nbs_cumulative[i] is the vector of cumulative sums of hopping constants from site i to its neighbors"
    hop_const_cumulative_sums::Vector{Vector{F}}

    "rates[s,i] is the total hopping rate of species s at site i"
    rates::Matrix{F}

    "sum_rates[i] is the total hopping rate out of site i"
    sum_rates::Vector{F}
end

function Base.show(io::IO, ::MIME"text/plain", hop_rates::HopRatesGraphDsLij)
    num_specs, num_sites = length(hop_rates.species_hop_constants), length(hop_rates.hop_const_cumulative_sums)
    println(io, "HopRates with $num_specs species and $num_sites sites. \nHopping constants of form D_s * L_{i,j} where s is species, i is source and j is destination.")
end

"""
    HopRatesGraphDsLij(species_hop_constants::Vector{F}, site_hop_constants::Vector{Vector{F}}) where F <: Number

initializes HopRates with zero rates
"""
function HopRatesGraphDsLij(species_hop_constants::Vector{F}, site_hop_constants::Vector{Vector{F}}; do_cumsum = true) where F <: Number
    do_cumsum && (site_hop_constants = map(cumsum, site_hop_constants))
    rates = zeros(F, length(species_hop_constants), length(site_hop_constants))
    sum_rates = zeros(size(rates, 2))
    HopRatesGraphDsLij{F}(species_hop_constants, site_hop_constants, rates, sum_rates)
end

function sample_target_site(hop_rates::HopRatesGraphDsLij, site, species, rng, spatial_system)
    @inbounds cumulative_hop_constants = hop_rates.hop_const_cumulative_sums[site]
    @inbounds n = searchsortedfirst(cumulative_hop_constants, rand(rng) * cumulative_hop_constants[end])
    return nth_nbr(spatial_system, site, n)
end

function evalhoprate(hop_rates::HopRatesGraphDsLij, u, species, site, spatial_system)
    @inbounds u[species, site] * hop_rates.species_hop_constants[species] * hop_rates.hop_const_cumulative_sums[site][end]
end

############## hopping rates of form D_s * L_{i,j} optimized for cartesian grid ################
struct HopRatesGridDsLij{F} <: AbstractHopRates
    "hopping constants of species -- D_s"
    species_hop_constants::Vector{F}

    "nbs_cumulative[i] is the vector of cumulative sums of hopping constants from site i to its neighbors"
    hop_const_cumulative_sums::Matrix{F}

    "rates[s,i] is the total hopping rate of species s at site i"
    rates::Matrix{F}

    "sum_rates[i] is the total hopping rate out of site i"
    sum_rates::Vector{F}
end

function Base.show(io::IO, ::MIME"text/plain", hop_rates::HopRatesGridDsLij)
    num_specs, num_sites = length(hop_rates.species_hop_constants), size(hop_rates.hop_const_cumulative_sums, 2)
    println(io, "HopRates with $num_specs species and $num_sites sites, optimized for CartesianGrid. \nHopping constants of form D_s * L_{i,j} where s is species, i is source and j is destination.")
end

function HopRatesGridDsLij(species_hop_constants::Vector{F}, site_hop_constants::Matrix{F}; do_cumsum = true) where F <: Number
    do_cumsum && (site_hop_constants = mapslices(cumsum, site_hop_constants, dims = 1))
    rates = zeros(F, length(species_hop_constants), size(site_hop_constants, 2))
    sum_rates = zeros(size(rates, 2))
    HopRatesGridDsLij{F}(species_hop_constants, site_hop_constants, rates, sum_rates)
end

function HopRatesGridDsLij(species_hop_constants::Vector{F}, site_hop_constants::Vector{Vector{F}}, grid) where F <: Number
    new_hopping_constants = Matrix{F}(undef, 2*dimension(grid), length(site_hop_constants))
    for site in 1:length(site_hop_constants)
        nb_constants = @view new_hopping_constants[:, site]
        pad_hop_vec!(nb_constants, grid, site, site_hop_constants[site])
        cumsum!(nb_constants, nb_constants)
    end
    HopRatesGridDsLij(species_hop_constants, new_hopping_constants, do_cumsum = false)
end

function sample_target_site(hop_rates::HopRatesGridDsLij, site, species, rng, grid)
    @inbounds cumulative_hop_constants = @view hop_rates.hop_const_cumulative_sums[:, site]
    @inbounds n = searchsortedfirst(cumulative_hop_constants, rand(rng) * cumulative_hop_constants[end])
    return nth_potential_nbr(grid, site, n)
end

function evalhoprate(hop_rates::HopRatesGridDsLij, u, species, site, grid)
    @inbounds u[species, site] * hop_rates.species_hop_constants[species] * hop_rates.hop_const_cumulative_sums[end, site]
end

############## hopping rates of form D_{s,i} * L_{i,j} ################
struct HopRatesGraphDsiLij{F} <: AbstractHopRates
    "hopping constants of species -- D_s"
    species_hop_constants::Matrix{F}

    "nbs_cumulative[i] is the vector of cumulative sums of hopping constants from site i to its neighbors"
    hop_const_cumulative_sums::Vector{Vector{F}}

    "rates[s,i] is the total hopping rate of species s at site i"
    rates::Matrix{F}

    "sum_rates[i] is the total hopping rate out of site i"
    sum_rates::Vector{F}
end

function Base.show(io::IO, ::MIME"text/plain", hop_rates::HopRatesGraphDsiLij)
    num_specs, num_sites = size(hop_rates.species_hop_constants)
    println(io, "HopRates with $num_specs species and $num_sites sites. \nHopping constants of form D_{s,i} * L_{i,j} where s is species, i is source and j is destination.")
end

function HopRatesGraphDsiLij(species_hop_constants::Matrix{F}, site_hop_constants::Vector{Vector{F}}; do_cumsum = true) where F <: Number
    @assert size(species_hop_constants, 2) == length(site_hop_constants)
    do_cumsum && (site_hop_constants = map(cumsum, site_hop_constants))
    rates = zeros(F, length(species_hop_constants), length(site_hop_constants))
    sum_rates = zeros(size(rates, 2))
    HopRatesGraphDsiLij{F}(species_hop_constants, site_hop_constants, rates, sum_rates)
end

function sample_target_site(hop_rates::HopRatesGraphDsiLij, site, species, rng, spatial_system)
    @inbounds cumulative_hop_constants = hop_rates.hop_const_cumulative_sums[site]
    @inbounds n = searchsortedfirst(cumulative_hop_constants, rand(rng) * cumulative_hop_constants[end])
    return nth_nbr(spatial_system, site, n)
end

function evalhoprate(hop_rates::HopRatesGraphDsiLij, u, species, site, spatial_system)
    @inbounds u[species, site] * hop_rates.species_hop_constants[species, site] * hop_rates.hop_const_cumulative_sums[site][end]
end

############## hopping rates of form D_{s,i} * L_{i,j} optimized for cartesian grid ################
struct HopRatesGridDsiLij{F} <: AbstractHopRates
    "hopping constants of species -- D_s"
    species_hop_constants::Matrix{F}

    "nbs_cumulative[i] is the vector of cumulative sums of hopping constants from site i to its neighbors"
    hop_const_cumulative_sums::Matrix{F}

    "rates[s,i] is the total hopping rate of species s at site i"
    rates::Matrix{F}

    "sum_rates[i] is the total hopping rate out of site i"
    sum_rates::Vector{F}
end

function Base.show(io::IO, ::MIME"text/plain", hop_rates::HopRatesGridDsiLij)
    num_specs, num_sites = length(hop_rates.species_hop_constants), size(hop_rates.hop_const_cumulative_sums, 2)
    println(io, "HopRates with $num_specs species and $num_sites sites, optimized for CartesianGrid. \nHopping constants of form D_{s,i} * L_{i,j} where s is species, i is source and j is destination.")
end

function HopRatesGridDsiLij(species_hop_constants::Matrix{F}, site_hop_constants::Matrix{F}; do_cumsum = true) where F <: Number
    @assert size(species_hop_constants, 2) == size(site_hop_constants, 2)
    do_cumsum && (site_hop_constants = mapslices(cumsum, site_hop_constants, dims = 1))
    rates = zeros(F, size(species_hop_constants))
    sum_rates = zeros(size(rates, 2))
    HopRatesGridDsiLij{F}(species_hop_constants, site_hop_constants, rates, sum_rates)
end

function HopRatesGridDsiLij(species_hop_constants::Matrix{F}, site_hop_constants::Vector{Vector{F}}, grid) where F <: Number
    new_hopping_constants = Matrix{F}(undef, 2*dimension(grid), length(site_hop_constants))
    for site in 1:length(site_hop_constants)
        nb_constants = @view new_hopping_constants[:, site]
        pad_hop_vec!(nb_constants, grid, site, site_hop_constants[site])
        cumsum!(nb_constants, nb_constants)
    end
    HopRatesGridDsiLij(species_hop_constants, new_hopping_constants, do_cumsum = false)
end

function sample_target_site(hop_rates::HopRatesGridDsiLij, site, species, rng, grid)
    @inbounds cumulative_hop_constants = @view hop_rates.hop_const_cumulative_sums[:, site]
    @inbounds n = searchsortedfirst(cumulative_hop_constants, rand(rng) * cumulative_hop_constants[end])
    return nth_potential_nbr(grid, site, n)
end

function evalhoprate(hop_rates::HopRatesGridDsiLij, u, species, site, grid)
    @inbounds u[species, site] * hop_rates.species_hop_constants[species, site] * hop_rates.hop_const_cumulative_sums[end, site]
end


# -----------------------------------------------------------------------------
# Implementation of Atomistic MolecularDynamicsResult API
# -----------------------------------------------------------------------------

"""
    NBSResult <: MolecularDynamicsResult

The result generated from running an `NBSimulator`.

**Field descriptions**
- `result::SimulationResult` the standard simulation result from `NBodySimulator`
"""
struct NBSResult <: MolecularDynamicsResult
    result::SimulationResult
end

function get_system(result::NBSResult, t::Integer = 0)
    sr = result.result
    simulation = sr.simulation
    system = simulation.system
    time = sr.solution.t[t > 0 ? t : end]
    positions = get_position(sr, time)
    velocities = get_velocity(sr, time)
    masses = get_masses(system)
    symbols = get_atomic_symbols(system)
    numbers = get_atomic_numbers(system)
    particles = [ElementMassBody(SVector{3}(positions[:, i]), SVector{3}(velocities[:, i]), masses[i], symbols[i], numbers[i]) for i ∈ 1:length(system.bodies)]
    DynamicSystem(FlexibleSystem(particles, simulation.boundary_conditions), time * TIME_UNIT)
end

function get_time_range(result::NBSResult)
    result.result.solution.t * TIME_UNIT
end

function temperature(result::NBSResult, t::Integer = 0)
    time = result.result.solution.t[t > 0 ? t : end]
    NBodySimulator.temperature(result.result, time) * TEMPERATURE_UNIT
end

function reference_temperature(result::NBSResult)
    thermostat = result.result.simulation.thermostat
    thermostat isa NullThermostat ? missing : thermostat.T * TEMPERATURE_UNIT
end

function kinetic_energy(result::NBSResult, t::Integer = 0)
    time = result.result.solution.t[t > 0 ? t : end]
    NBodySimulator.kinetic_energy(result.result, time) * ENERGY_UNIT
end

function potential_energy(result::NBSResult, t::Integer = 0)
    potentials = result.result.simulation.system.potentials
    # https://github.com/SciML/NBodySimulator.jl/issues/44
    if :custom ∈ keys(potentials)
        return InteratomicPotentials.potential_energy(get_system(result, t), potentials[:custom].potential) * ENERGY_UNIT
    end
    time = result.result.solution.t[t > 0 ? t : end]
    NBodySimulator.potential_energy(result.result, time) * ENERGY_UNIT
end

function rdf(result::NBSResult, sample_fraction::Float64 = 1.0)
    @assert 0 < sample_fraction ≤ 1
    sr = result.result
    n = length(sr.simulation.system.bodies)
    pbc = sr.simulation.boundary_conditions
    trange = sr.solution.t[end-floor(Int, length(sr.solution.t) * sample_fraction)+1:end]

    maxbin = 1000
    dr = pbc.L / 2 / maxbin
    hist = zeros(maxbin)
    for t ∈ trange
        cc = get_position(sr, t)
        for i ∈ 1:n
            ri = SVector{3}(cc[:, i])
            for j ∈ i+1:n
                rj = SVector{3}(cc[:, j])
                # TODO: potential for major performance improvements here
                (rij, r, r2) = NBodySimulator.get_interparticle_distance(ri, rj, pbc)
                if r2 < (0.5 * pbc.L)^2
                    bin = ceil(Int, r / dr)
                    if bin > 1 && bin <= maxbin
                        hist[bin] += 2
                    end
                end
            end
        end
    end

    c = 4 / 3 * π * n / pbc.L^3
    gr = zeros(maxbin)
    rs = zeros(maxbin)
    tlen = length(trange)
    for bin ∈ 1:maxbin
        rlower = (bin - 1) * dr
        rupper = rlower + dr
        nideal = c * (rupper^3 - rlower^3)
        gr[bin] = (hist[bin] / (tlen * n)) / nideal
        rs[bin] = rlower + dr / 2
    end

    rs, gr
end


function partitionnementBiObjectif(instance)
    V, A, P = AEPbiFiltrage(instance)
    graphiqueBis = plt.figure()
    plotPointsNonDomines(V, A, graphiqueBis)
    choix = choixBinaire("\n --> Souhaitez-vous calculer les points non-dominés supportés (o/n) ? ")
    if choix == "o"
        calculPointsSupportes(V, A, graphiqueBis)
    end
    solution = choixSolution(V, A)
    close(graphiqueBis)
    backtracking(instance, V, A, P, solution)
end

function backtracking(instance, V, A, P, solution)
    operationMinIJ = Array{Int64,2}(undef, length(instance.solution.ordreVisiteCamion), length(instance.solution.ordreVisiteCamion))
    for i in 1:length(instance.solution.ordreVisiteCamion)
        for j in 1:length(instance.solution.ordreVisiteCamion)
            operationMinIJ[i,j] = -100
        end
    end

    temp = []
    pred = P[end][solution]
    push!(temp, length(instance.solution.ordreVisiteCamion))
    push!(temp, pred[1])
    operationMinIJ[temp[end], temp[end-1]] = pred[3]
    while pred[1] != 1
        pred = P[pred[1]][pred[2]]
        push!(temp, pred[1])
        operationMinIJ[temp[end], temp[end-1]] = pred[3]
    end
    temp = reverse(temp)

    ordreVisiteCamion = []
    operationsDrone = []
    push!(ordreVisiteCamion, 1)
    for i in 1:length(temp)-1
        operation = operationMinIJ[temp[i],temp[i+1]]
        if operation == -1 # Trajet direct
            noeudCombine = instance.solution.ordreVisiteCamion[temp[i+1]]
            push!(ordreVisiteCamion, noeudCombine)
        else # Opération drone
            noeudDrone = instance.solution.ordreVisiteCamion[operation]
            noeudDepart = instance.solution.ordreVisiteCamion[temp[i]]
            noeudFin = instance.solution.ordreVisiteCamion[temp[i+1]]
            push!(operationsDrone, [noeudDepart, noeudDrone, noeudFin])
            for l in temp[i]+1:temp[i+1]-1
                noeudActu = instance.solution.ordreVisiteCamion[l]
                if noeudActu != noeudDrone # Noeud camion
                    push!(ordreVisiteCamion, noeudActu)
                end
            end
            push!(ordreVisiteCamion, noeudFin)
        end
    end

    instance.solution.tempsParcours = V[end][solution]
    instance.solution.tempsAttente = A[end][solution]
    instance.solution.ordreVisiteCamion = ordreVisiteCamion
    instance.solution.operationsDrone = operationsDrone
    instance.codeResolution = 5
end

function choixSolution(V, A) # On choisit la solution à afficher (avec laquelle on effectue le backtracking)
    println("\n Liste des points non-dominés :")
    println(" ------------------------------")
    for i in 1:length(V[end])
        println(" --> $i) ($(V[end][i]),$(A[end][i]))")
        text(V[end][i]+1, A[end][i]+1, i)
    end
    choix = choixListe(length(V[end]))
    return parse(Int64, choix)
end

function plotPointsNonDomines(V, A, graphiqueBis)
    ax = graphiqueBis.gca()
    ax.plot(V[end], A[end], marker="x", linestyle="", color="C0", label="Non-dominated points")
    title(L"#$Y_N =$ "*string(length(V[end])))
    grid(true)
    xlabel(L"$z_1$")
    ylabel(L"$z_2$")
    legend(loc="best")
end

function calculPointsSupportes(V, A, graphiqueBis)
    ax = graphiqueBis.gca()
    listeDesPoints = [tuple(V[end][i],A[end][i]) for i in 1:length(V[end])]
    hull = convex_hull_graham_scan!(listeDesPoints) # On doit enlever les points non-supportés qui sont sur les bords de l'enveloppe convexe

    # On cherche les deux points extrêmes
    maxY = -Inf32
    maxIndice = -1
    minY = Inf32
    minIndice = -1
    for i in 1:length(hull)
        if hull[i][2] < minY
            minY = hull[i][2]
            minIndice = i
        end
        if hull[i][2] > maxY
            maxY = hull[i][2]
            maxIndice = i
        end
    end

    # On cherche les points qui sont au-dessus (et donc à enlever)
    vecteurMinMax = [hull[minIndice][1] - hull[maxIndice][1], hull[minIndice][2] - hull[maxIndice][2]]
    hullNew = [hull[minIndice]]
    for i in 1:length(hull)
        if i != minIndice && i != maxIndice
            vecteur = [hull[i][1] - hull[maxIndice][1], hull[i][2] - hull[maxIndice][2]]
            produitVectoriel = (vecteurMinMax[1]*vecteur[2])-(vecteurMinMax[2]*vecteur[1])
            if produitVectoriel < 0 # ok : point supporté
                push!(hullNew, hull[i])
            end
        end
    end
    push!(hullNew, hull[maxIndice])
    hull = hullNew

    YSN = length(hull)
    ax.plot(hull[1][1], hull[1][2], marker="x", color="red", linestyle="", label="Supported non-dominated points")
    for i in 1:length(hull)
        #println(cvh_graham[i])
        ax.plot(hull[i][1], hull[i][2], marker="x", color="red", linestyle="")
        if i != length(hull)
            ax.plot([hull[i][1],hull[i+1][1]], [hull[i][2],hull[i+1][2]], color="red", linestyle="--", linewidth=0.7)
        end
    end

    YN = length(V[end])
    YSN = length(hull)
    YNN = YN - YSN
    title(L"#$Y_{N} =$ "*string(YN)*L"; #$Y_{SN} =$ "*string(YSN)*L"; #$Y_{NN} =$ "*string(YNN))
    legend(loc="best")
end

#################### Bi-objectif ####################

function tempsOperationBi(instance, i,j,k)
    solutionTSP = instance.solution.ordreVisiteCamion # Solution TSP : ordre de visite  du camion
    # Debug
    #println(" Calcul du temps de l'opération : (i=$i, j=$j, k=$k)")
    #println(" Solution TSP : R_tsp = $solutionTSP")
    #print(" r_$i = $(solutionTSP[i]);")
    #print(" r_$j = $(solutionTSP[j]);")
    #if k != -1
    #    println(" r_$k = $(solutionTSP[k])")
    #end
    # Calcul distance parcourue par le drone
    if k == -1 # Pas de noeud drone
        distanceDrone = 0
        ri = solutionTSP[i]
        rj = solutionTSP[j]
        distanceCamion = instance.D[ri,rj]
        tempsAttente = 0.0
    else
        ri = solutionTSP[i]
        rj = solutionTSP[j]
        rk = solutionTSP[k]
        distanceDrone = instance.D[ri,rk] + instance.D[rk,rj]
        distanceCamion = 0
        for l in i:(k-2)
            r1 = solutionTSP[l]
            r2 = solutionTSP[l+1]
            distanceCamion += instance.D[r1,r2]
        end
        rkAvant = solutionTSP[k-1]
        rkApres = solutionTSP[k+1]
        distanceCamion += instance.D[rkAvant,rkApres]
        for l in (k+1):(j-1)
            r1 = solutionTSP[l]
            r2 = solutionTSP[l+1]
            distanceCamion += instance.D[r1,r2]
        end
        tempsAttente = abs((distanceCamion/instance.vitesseCamion) - (distanceDrone/instance.vitesseDrone))
    end
    tempsParcours = max(distanceCamion/instance.vitesseCamion, distanceDrone/instance.vitesseDrone)
    return tempsParcours, tempsAttente
    #println(" Distance parcourue par le drone : $distanceDrone")
    #println(" Distance parcourue par le camion : $distanceCamion")
end

function calculMeilleuresOperationsBi(instance)
    solutionTSP = instance.solution.ordreVisiteCamion
    tempsMinIJ = Array{Vector{Float64},2}(undef, length(solutionTSP), length(solutionTSP))
    attenteIJ = Array{Vector{Float64},2}(undef, length(solutionTSP), length(solutionTSP))
    operationMinIJ = Array{Vector{Int64},2}(undef, length(solutionTSP), length(solutionTSP))
    for i in 1:length(solutionTSP)
        for j in 1:length(solutionTSP)
            tempsMinIJ[i,j] = Float64[]
            attenteIJ[i,j] = Float64[]
            operationMinIJ[i,j] = Int64[]
        end
    end
    for i in 1:length(solutionTSP)-1
        for j in i+1:length(solutionTSP)
            if j == i+1 # k = -1
                Tij, Aij = tempsOperationBi(instance, i, j, -1)
                operation = -1
                push!(tempsMinIJ[i, j], Tij)
                push!(attenteIJ[i, j], Aij)
                push!(operationMinIJ[i, j], operation)
            else
                # k > 0
                for k in (i+1):(j-1)
                    Tij, Aij = tempsOperationBi(instance, i, j, k)
                    operation = k
                    push!(tempsMinIJ[i, j], Tij)
                    push!(attenteIJ[i, j], Aij)
                    push!(operationMinIJ[i, j], operation)
                end
            end
        end
    end
    return tempsMinIJ, operationMinIJ, attenteIJ
end

########## Filtrage pendant (iterative dominance filtering) ##########

function AEPbiFiltrage(instance)
    tempsMinIJ, operationMinIJ, attenteIJ = calculMeilleuresOperationsBi(instance)
    tempsMinIJ, attenteIJ, operationMinIJ = filtrageOperations(tempsMinIJ, operationMinIJ, attenteIJ, instance.solution.ordreVisiteCamion) # Filtrage par dominance des opérations T(i,j)

    V = []
    A = []
    P = []
    for i in 1:length(instance.solution.ordreVisiteCamion)
        push!(V, Float64[])
        push!(A, Float64[])
        push!(P, [])
    end

    push!(V[1], 0.0)
    push!(A[1], 0.0)
    push!(P[1], [-1,-1,-1]) # A changer
    for i in 2:length(V)
        #println(" $(i-1) : $(length(V[i-1]))")
        for k in 1:(i-1)
            for y in 1:length(V[k])
                for z in 1:length(tempsMinIJ[k,i])
                    temps = V[k][y] + (tempsMinIJ[k,i])[z]
                    attente = A[k][y] + (attenteIJ[k,i])[z]
                    push!(V[i], temps)
                    push!(A[i], attente)
                    push!(P[i], [k,y, trunc(Int64, (operationMinIJ[k,i])[z]) ]) # On vient de V[k,m] et on a pris l'opération T(k,i,n)
                end
            end
        end
        Vtemp = []
        Atemp = []
        Ptemp = []
        for a in 1:length(V[i]) # On veut tester si le point i est dominé par un point j
            estDomine = false
            for b in 1:length(V[i])
                if b != a
                    if round(V[i][b], digits=5) == round(V[i][a], digits=5) && round(A[i][b], digits=5) == round(A[i][a], digits=5)
                        println(" Egalité V[i]")
                    end
                    if round(V[i][b], digits=5) <= round(V[i][a], digits=5) && round(A[i][b], digits=5) <= round(A[i][a], digits=5)
                        estDomine = true
                        break
                    end
                end
            end
            if estDomine == false
                push!(Vtemp, V[i][a])
                push!(Atemp, A[i][a])
                push!(Ptemp, P[i][a])
            end
        end
        V[i] = Vtemp
        A[i] = Atemp
        P[i] = Ptemp
    end
    #println(" V = $V")
    #println(" A = $A")
    #println(" P = $P")
    return V, A, P
end

########## Filtrage par dominance des T(i,j) ##########

function filtrageOperations(tempsMinIJ, operationMinIJ, attenteIJ, solutionTSP)
    tempsMinIJBis = Array{Vector{Float64},2}(undef, length(solutionTSP), length(solutionTSP))
    attenteIJBis = Array{Vector{Float64},2}(undef, length(solutionTSP), length(solutionTSP))
    operationMinIJBis = Array{Vector{Float64},2}(undef, length(solutionTSP), length(solutionTSP))
    for i in 1:length(solutionTSP)
        for j in 1:length(solutionTSP)
            tempsMinIJBis[i,j] = Float64[]
            attenteIJBis[i,j] = Float64[]
            operationMinIJBis[i,j] = Int64[]
        end
    end

    for i in 1:length(solutionTSP)
        for j in i+1:length(solutionTSP)
            for l1 in 1:length(tempsMinIJ[i,j]) # Pour toutes les opérations dans T(i,j)
                estDomine = false # Test du point (i,j,l1)
                for l2 in 1:length(tempsMinIJ[i,j]) # Pour toutes les opérations dans T(i,j)
                    if l1 != l2
                        if round(tempsMinIJ[i,j][l2], digits=5) == round(tempsMinIJ[i,j][l1], digits=5) && round(attenteIJ[i,j][l2], digits=5) == round(attenteIJ[i,j][l1], digits=5) # Cas d'égalité
                            println(" Egalité operation")
                        end
                        if round(tempsMinIJ[i,j][l2], digits=5) <= round(tempsMinIJ[i,j][l1], digits=5) && round(attenteIJ[i,j][l2], digits=5) <= round(attenteIJ[i,j][l1], digits=5)
                            estDomine = true
                            break
                        end
                    end
                end
                if estDomine == false
                    push!(tempsMinIJBis[i,j], tempsMinIJ[i,j][l1])
                    push!(attenteIJBis[i,j], attenteIJ[i,j][l1])
                    push!(operationMinIJBis[i,j], operationMinIJ[i,j][l1])
                end
            end
        end
    end
    return tempsMinIJBis, attenteIJBis, operationMinIJBis
end

########## Graham ##########

function orient(p,q,r)
    pr_x = r[1] - p[1]
    pr_y = r[2] - p[2]
    rq_x = q[1] - r[1]
    rq_y = q[2] - r[2]
    sign(pr_x * rq_y - pr_y * rq_x)
end

function convex_hull_graham_scan!(points)
    @assert length(points) >= 3 "Too few points"
    sort!(points)  # in lexical order

    function build_convex_hull(points)
        n = length(points)
        hull = points[1:2]
        sizehint!(hull, n)

        for i in 3:n
            while true
                if orient(hull[end - 1], hull[end], points[i]) > 0
                    break
                else
                    pop!(hull)
                    if length(hull) < 2
                        break
                    end
                end
            end
            push!(hull, points[i])
        end

        hull
    end

    upper_hull = build_convex_hull(points)
    pop!(upper_hull)
    lower_hull = build_convex_hull(@view points[end:-1:1])
    pop!(lower_hull)

    [upper_hull; lower_hull]
end

function convex_hull_length(hull)
    hull2 = circshift(hull, -1)
    map((x, y) -> hypot((x .- y)...), hull2, hull) |> sum
end


""" Verify that the data is self-consistent and consistent with the likelihood """
function check_data!(X::AbstractArray{T1,N1},y::AbstractArray{T2,N2},likelihood::Likelihood) where {T1<:Real,T2,N1,N2}
    @assert (size(y,1)==size(X,1)) "There is not the same number of samples in X and y";
    @assert N1 <= 2 "The input matrix X can only be a vector or a matrix"
    if N1 == 1 #Reshape a X vector as a matrix
        X = reshape(X,length(X),1);
    end
    y,nLatent,likelihood = treat_labels!(y,likelihood)
    return X,y,nLatent,likelihood
end

""" Verify that the likelihood and inference are compatible (are implemented) """
function check_implementation(model::Symbol,likelihood::L,inference::I) where {I<:Inference,L<:Likelihood}
    if isa(likelihood,GaussianLikelihood)
        if model == :GP && inference isa Analytic
            return true
        elseif model == :SVGP && inference isa AnalyticVI
            return true
        else
            return false
        end
    elseif likelihood isa StudentTLikelihood
        if inference isa AnalyticVI
            return true
        elseif model == :VGP && inference isa GibbsSampling
            return true
        else
            return false
        end
    elseif likelihood isa LaplaceLikelihood
        if inference isa AnalyticVI
            return true
        else
            return false
        end
    elseif likelihood isa LogisticLikelihood
        if inference isa AnalyticVI
            return true
        elseif model == :VGP && inference isa GibbsSampling
            return true
        else
            return false
        end
    elseif likelihood isa BayesianSVM
        if inference isa AnalyticVI
            return true
        else
            return false
        end
    elseif likelihood isa SoftMaxLikelihood
        if inference isa MCIntegrationVI
            return true
        else
            return false
        end
    elseif likelihood isa LogisticSoftMaxLikelihood
        if inference isa AnalyticVI
            return true
        elseif model == :VGP && inference isa GibbsSampling
            return true
        elseif inference isa MCIntegrationVI
            return true
        else
            return false
        end
    elseif likelihood isa PoissonLikelihood
        if inference isa AnalyticVI
            return true
        else
            return false
        end
    else
        return false
    end
end


###############################################################################
#
# Update carbon and water fluxes
#
# TODO: work more to make this function more customized
#
###############################################################################
"""
    layer_fluxes!(
                node::SPACMono{FT};
                updating::Bool = false
    ) where {FT<:AbstractFloat}

Run carbon, water, energy, and SIF fluxes for all canopy layers, given
- `node` [`SPACMono`](@ref) type struct
- `updating` If true, update cavitation history
"""
function layer_fluxes!(
            node::SPACMono{FT};
            updating::Bool = false
) where {FT<:AbstractFloat}
    # 0.1 unpack data
    @unpack angles, envirs, f_SL, ga, in_rad, leaves_rt, n_canopy, plant_ps, rt_con, rt_dim, soil_opt, stomata_model, wl_set = node;
    canopy_rt = node.canopy_rt;
    can_rad = node.can_rad;
    can_opt = node.can_opt;
    plant_hs = node.plant_hs;
    nSL = canopy_rt.nAzi * canopy_rt.nIncl;

    canopy_geometry!(canopy_rt, angles, can_opt, rt_con);
    canopy_matrices!(leaves_rt, can_opt);
    short_wave!(canopy_rt, can_opt, can_rad, in_rad, soil_opt, rt_con);
    canopy_fluxes!(canopy_rt, can_opt, can_rad, in_rad, soil_opt, leaves_rt,
                   wl_set, rt_con);

    #
    f_H₂O = 0;
    f_GPP = 0;
    f_NPP = 0;
    for i_can in 1:n_canopy
        iEN = envirs[i_can];
        iHS = plant_hs.leaves[i_can];
        iPS = plant_ps[i_can];
        iRT = n_canopy + 1 - i_can;

        iPS.T = can_rad.T_sun[i_can];
        update_leaf_TP!(iPS, iHS, iEN);
        temperature_effects!(iHS, iPS.T);

        # calculate the fraction of sunlit and shaded leaves
        f_view = (can_opt.Ps[iRT] + can_opt.Ps[iRT+1]) / 2;
        for iLF in 1:nSL
            iPS.APAR[iLF] = can_rad.absPAR_sunCab[(iRT-1)*nSL+iLF] * FT(1e6);
            iPS.LAIx[iLF] = f_view * f_SL[iLF];
        end
        iPS.APAR[end] = can_rad.absPAR_shadeCab[iRT] * FT(1e6);
        iPS.LAIx[end] = 1 - f_view;

        # iterate for N times until sum(iPS.Ag) does not change any more
        sum_ag_last = sum(iPS.Ag);
        count       = 0;
        while true
            # calculate the photosynthetic rates
            count += 1;
            gas_exchange!(iPS, iEN, GswDrive());
            if typeof(stomata_model) <: EmpiricalStomatalModel
                #
                #
                #
                #
                # also need to determine which β function is used
                #     to tune g1
                #     to tune Vcmax
                #
                #
                #
                #
                prognostic_gsw!(iPS, iEN, stomata_model, FT(1), FT(120));
            else
                prognostic_gsw!(iPS, iHS, iEN, stomata_model, FT(120));
            end

            # update flow and pressure profile (except for history)
            # TODO move this part to PlantHydraulics.jl
            gsw_control!(iPS, iEN);
            for i_can in 1:n_canopy
                iEN = envirs[i_can];
                iLF = plant_hs.leaves[i_can];
                iPS = plant_ps[i_can];
                iLF.flow = sum(iPS.g_lw .* iPS.LAIx) * (iPS.p_sat - iEN.p_H₂O) / iEN.P_AIR;
            end
            flow_profile!(plant_hs);
            pressure_profile!(plant_hs, SteadyStateMode(); update=false);

            # TODO make sure this is done in future SPAC module
            # update canopy layer p_ups, which will be passed to each leaf
            for _i_can in 1:n_canopy
                _iHS = plant_hs.leaves[_i_can];
                _iPS = plant_ps[_i_can];
                _iPS.p_ups = _iHS.p_ups;
            end;

            sum_ag_curr = sum(iPS.Ag);
            if (abs(sum_ag_curr - sum_ag_last) < 1e-4)
                break
            elseif count > 100000
                @info tinfo("layer_fluxes!: iterations exceed 100000 times");
                break
            else
                sum_ag_last = sum_ag_curr;
            end
        end

        # update the fluorescence quantum yield from leaf level calculation
        can_rad.ϕ_sun[:,:,iRT] .= reshape(view(iPS.φs,1:nSL), canopy_rt.nIncl,
                                          canopy_rt.nAzi);
        can_rad.ϕ_shade[iRT] = iPS.φs[end];

        # update the flow rates
        for iLF in 1:(nSL+1)
            f_GPP += iPS.Ag[iLF] * iPS.LAIx[iLF] * iPS.LA;
            f_NPP += iPS.An[iLF] * iPS.LAIx[iLF] * iPS.LA;
            f_H₂O += iPS.g_lw[iLF] * (iPS.p_sat - iEN.p_H₂O) / iEN.P_AIR * iPS.LAIx[iLF] * iPS.LA;
        end
    end

    # do SIF simulation
    SIF_fluxes!(leaves_rt, can_opt, can_rad, canopy_rt, soil_opt, wl_set,
                rt_con, rt_dim);

    # update flow profile and pressure history along the tree
    if updating
        for i_can in 1:n_canopy
            iEN = envirs[i_can];
            iLF = plant_hs.leaves[i_can];
            iPS = plant_ps[i_can];
            iLF.flow = sum(iPS.g_lw .* iPS.LAIx) * (iPS.p_sat - iEN.p_H₂O) / iEN.P_AIR;
        end
        flow_profile!(plant_hs);
        pressure_profile!(plant_hs, SteadyStateMode(); update=true);
    end

    # update the flows in SPACMono
    node.f_gpp = f_GPP / ga;
    node.f_npp = f_NPP / ga;
    node.f_H₂O = f_H₂O / ga;

    return nothing
end

"""
    layer_fluxes!(
                node::SPACMono{FT},
                Δt::FT;
                updating::Bool = false
    ) where {FT<:AbstractFloat}

Run carbon, water, energy, and SIF fluxes for all canopy layers, given
- `node` [`SPACMono`](@ref) type struct
- `Δt` Time step to forward in time
- `updating` If true, update cavitation history
"""
function layer_fluxes!(
            node::SPACMono{FT},
            Δt::FT;
            updating::Bool = false
) where {FT<:AbstractFloat}
    # 0.1 unpack data
    @unpack angles, envirs, f_SL, ga, in_rad, leaves_rt, n_canopy, plant_ps, rt_con, rt_dim, soil_opt, stomata_model, wl_set = node;
    canopy_rt = node.canopy_rt;
    can_rad = node.can_rad;
    can_opt = node.can_opt;
    plant_hs = node.plant_hs;
    nSL = canopy_rt.nAzi * canopy_rt.nIncl;

    canopy_geometry!(canopy_rt, angles, can_opt, rt_con);
    canopy_matrices!(leaves_rt, can_opt);
    short_wave!(canopy_rt, can_opt, can_rad, in_rad, soil_opt, rt_con);
    canopy_fluxes!(canopy_rt, can_opt, can_rad, in_rad, soil_opt, leaves_rt,
                   wl_set, rt_con);

    #
    f_H₂O = 0;
    f_GPP = 0;
    f_NPP = 0;
    for i_can in 1:n_canopy
        iEN = envirs[i_can];
        iHS = plant_hs.leaves[i_can];
        iPS = plant_ps[i_can];
        iRT = n_canopy + 1 - i_can;

        iPS.T = can_rad.T_sun[i_can];
        update_leaf_TP!(iPS, iHS, iEN);
        temperature_effects!(iHS, iPS.T);

        # calculate the fraction of sunlit and shaded leaves
        f_view = (can_opt.Ps[iRT] + can_opt.Ps[iRT+1]) / 2;
        for iLF in 1:nSL
            iPS.APAR[iLF] = can_rad.absPAR_sunCab[(iRT-1)*nSL+iLF] * FT(1e6);
            iPS.LAIx[iLF] = f_view * f_SL[iLF];
        end
        iPS.APAR[end] = can_rad.absPAR_shadeCab[iRT] * FT(1e6);
        iPS.LAIx[end] = 1 - f_view;

        # step forward with Δt
        gas_exchange!(iPS, iEN, GswDrive());
        if typeof(stomata_model) <: EmpiricalStomatalModel
            #
            #
            #
            #
            # also need to determine which β function is used
            #     to tune g1
            #     to tune Vcmax
            #
            #
            #
            #
            prognostic_gsw!(iPS, iEN, stomata_model, FT(1), Δt);
        else
            prognostic_gsw!(iPS, iHS, iEN, stomata_model, Δt);
        end

        # update flow and pressure profile (except for history)
        # TODO move this part to PlantHydraulics.jl
        gsw_control!(iPS, iEN);
        for i_can in 1:n_canopy
            iEN = envirs[i_can];
            iLF = plant_hs.leaves[i_can];
            iPS = plant_ps[i_can];
            iLF.flow = sum(iPS.g_lw .* iPS.LAIx) * (iPS.p_sat - iEN.p_H₂O) / iEN.P_AIR;
        end
        flow_profile!(plant_hs);
        pressure_profile!(plant_hs, SteadyStateMode(); update=false);

        # TODO make sure this is done in future SPAC module
        # update canopy layer p_ups, which will be passed to each leaf
        for _i_can in 1:n_canopy
            _iHS = plant_hs.leaves[_i_can];
            _iPS = plant_ps[_i_can];
            _iPS.p_ups = _iHS.p_ups;
        end;

        # update the fluorescence quantum yield from leaf level calculation
        can_rad.ϕ_sun[:,:,iRT] .= reshape(view(iPS.φs,1:nSL), canopy_rt.nIncl, canopy_rt.nAzi);
        can_rad.ϕ_shade[iRT] = iPS.φs[end];

        # update the flow rates
        for iLF in 1:(nSL+1)
            f_GPP += iPS.Ag[iLF] * iPS.LAIx[iLF] * iPS.LA;
            f_NPP += iPS.An[iLF] * iPS.LAIx[iLF] * iPS.LA;
            f_H₂O += iPS.g_lw[iLF] * (iPS.p_sat - iEN.p_H₂O) / iEN.P_AIR * iPS.LAIx[iLF] * iPS.LA;
        end
    end

    # do SIF simulation
    SIF_fluxes!(leaves_rt, can_opt, can_rad, canopy_rt, soil_opt, wl_set, rt_con, rt_dim);

    # update flow profile and pressure history along the tree
    if updating
        for i_can in 1:n_canopy
            iEN = envirs[i_can];
            iLF = plant_hs.leaves[i_can];
            iPS = plant_ps[i_can];
            iLF.flow = sum(iPS.g_lw .* iPS.LAIx) * (iPS.p_sat - iEN.p_H₂O) / iEN.P_AIR;
        end
        flow_profile!(plant_hs);
        pressure_profile!(plant_hs, SteadyStateMode(); update=true);
    end

    # update the flows in SPACMono
    node.f_gpp = f_GPP / ga;
    node.f_npp = f_NPP / ga;
    node.f_H₂O = f_H₂O / ga;

    return nothing
end