function make_grids_for_chunklist_timeseries(clt::ACLT; oversample_factor::Real = 1 ) where { ACLT<:AbstractChunkListTimeseries }
    map(i->RvSpectMLBase.make_grid_for_chunk(clt,i,oversample_factor=oversample_factor), 1:num_chunks(clt))
end

# Wrapper code to deal with weird data structures
function pack_chunk_list_timeseries_to_matrix(timeseries::ACLT, chunk_grids::Union{AR,AAV}; alg::Symbol = :TemporalGP,
    oversample_factor::Real = 1, smooth_factor::Real=1, verbose::Bool = false ) where {
        ACLT<:AbstractChunkListTimeseries, RT<:AbstractRange, AR<:AbstractArray{RT,1}, T<:Real, AV<:AbstractVector{T}, AAV<:AbstractArray{AV,1} }
    @assert alg == :Linear || alg == :GP  || alg == :Sinc || alg == :TemporalGP # TODO: Eventually move to traits-based system?
    num_obs = length(timeseries)
    num_λ = sum(length.(chunk_grids))
    flux_matrix = Array{Float64,2}(undef,num_λ,num_obs)
    var_matrix = Array{Float64,2}(undef,num_λ,num_obs)
    λ_vec = Array{Float64,1}(undef,num_λ)
    chunk_map = Array{UnitRange{Int64}}(undef,length(chunk_grids))
    mean_flux = zeros(num_λ)
    mean_var = zeros(num_λ)
    dfluxdlnλ = zeros(num_λ)
    d2fluxdlnλ2 = zeros(num_λ)
    if (alg == :GP) && (maximum(length.(chunk_grids))>1024)
        @error "To use a GP with more than 1024 points in a chunk, use a more efficient GP package, e.g., :TemporalGP."
    end
    if alg == :Sinc    # Setup workspace for Sync.  TODO: Put into functor
        filter_size=23
        kaiserB=13
        sinc_filter = SincInterpolation.create_filter_curve(filter_size*21; filter_size=filter_size, kaiserB=kaiserB)
    end

    for t in 1:num_obs
       idx_start = 0
       for c in 1:length(chunk_grids)
           idx = (idx_start+1):(idx_start+length(chunk_grids[c]))
           if verbose && !issorted(timeseries.chunk_list[t].data[c].λ)
               flush(stdout);  println("t= ",t, " c= ",c," idx= ", idx, " size(flux)= ",size(flux_matrix), " issorted(λ)=", issorted(timeseries.chunk_list[t].data[c].λ))
           end
           if t == 1
               λ_vec[idx] .= chunk_grids[c]
               chunk_map[c] = idx
           end
           mean_flux_in_chunk = mean(timeseries.chunk_list[t].data[c].flux)
           if mean_flux_in_chunk > 0
               if alg == :Linear
                   LinearInterpolation.interp_chunk_to_grid_linear!(view(flux_matrix,idx,t), view(var_matrix,idx,t), timeseries.chunk_list[t].data[c], chunk_grids[c])
               elseif alg == :Sinc
                   SincInterpolation.interp_chunk_to_grid_sinc!(view(flux_matrix,idx,t), view(var_matrix,idx,t), timeseries.chunk_list[t].data[c], chunk_grids[c], Filter=sinc_filter)
               elseif alg == :GP
                   GPInterpolation.interp_chunk_to_grid_gp!(view(flux_matrix,idx,t), view(var_matrix,idx,t), timeseries.chunk_list[t].data[c], chunk_grids[c])
               elseif alg == :TemporalGP
                   RvSpectML.TemporalGPInterpolation.interp_chunk_to_grid_gp_temporal!(view(flux_matrix,idx,t), view(var_matrix,idx,t), timeseries.chunk_list[t].data[c], chunk_grids[c] , use_logx=true, use_logy=false, smooth_factor=smooth_factor)
               end
               mean_flux[idx] .+= flux_matrix[idx,t]
               mean_var[idx]  .+= var_matrix[idx,t]
               dfluxdlnλ[idx] .+= calc_dfluxdlnlambda(view(flux_matrix,idx,t),view(λ_vec,idx))
               d2fluxdlnλ2[idx] .+= calc_d2fluxdlnlambda2(view(flux_matrix,idx,t),view(λ_vec,idx))

               flux_matrix[idx,t] ./= mean_flux_in_chunk
               var_matrix[idx,t]  ./= mean_flux_in_chunk^2
           else  # no flux in chunk?!?
               @warn "No flux in chunk " * string(c) * " at time " * string(t) * "."
           end
           idx_start += length(chunk_grids[c])
       end # for c
   end # for t

    # TODO Think if can propagate uncertainties better
    var_matrix .*= sqrt(oversample_factor)

    #flush(stdout)
    #println("# mean flux pre-normalize = ", mean(mean_flux))
    idx_start = 0
    for c in 1:length(chunk_grids)
        idx = (idx_start+1):(idx_start+length(chunk_grids[c]))
        mean_flux_in_chunk = mean(mean_flux[idx])
        #println("Normalizing chunk ", c, " (",idx,") by ", mean_flux_in_chunk)
        #dmeanfluxdlnλ[idx] .= calc_dfluxdlnlambda(vec(sum(view(flux_matrix,idx,:),dims=2)),vec(sum(view(var_matrix,idx,:), dims=2)) ) ./mean_flux_in_chunk
        mean_flux[idx] ./= mean_flux_in_chunk
        mean_var[idx] ./= mean_flux_in_chunk^2
        #dfluxdlnλ[idx] ./= mean_flux_in_chunk
        #d2fluxdlnλ2[idx] ./= mean_flux_in_chunk
        dfluxdlnλ[idx] .= calc_dfluxdlnlambda(mean_flux[idx],view(λ_vec,idx))
        d2fluxdlnλ2[idx] .= calc_d2fluxdlnlambda2(mean_flux[idx],view(λ_vec,idx))

        #dmeanfluxdlnλ[idx] .= calc_dfluxdlnlambda(vec(sum(view(flux_matrix,idx,:),dims=2)),view(λ_vec,idx))  ./mean_flux_in_chunk
        idx_start += length(chunk_grids[c])
    end
    #println("# mean flux post-normalize = ", mean(mean_flux))
    #flush(stdout)
    #mean_flux /= num_obs
    mean_var .*= sqrt(oversample_factor)
    #dmeanfluxdlnλ ./= num_obs
    return ( matrix=SpectralTimeSeriesCommonWavelengths(λ_vec,flux_matrix,var_matrix,chunk_map, TheoreticalInstrument1D() ), mean_flux=mean_flux, mean_var=mean_var, deriv=dfluxdlnλ, deriv2=d2fluxdlnλ2 )
end

# Wrapper code to deal with weird data structures
function pack_shifted_chunk_list_timeseries_to_matrix(timeseries::ACLT, chunk_grids::Union{AR,AAV}; alg::Symbol = :TemporalGP,
    oversample_factor::Real = 1, smooth_factor::Real=1, remove_rv_est::Bool = true, verbose::Bool = false ) where {
        ACLT<:AbstractChunkListTimeseries, RT<:AbstractRange, AR<:AbstractArray{RT,1}, T<:Real, AV<:AbstractVector{T}, AAV<:AbstractArray{AV,1} }
    @assert alg == :Linear || alg == :GP  || alg == :Sinc || alg == :TemporalGP # TODO: Eventually move to traits-based system?
    num_obs = length(timeseries)
    num_λ = sum(length.(chunk_grids))
    flux_matrix = Array{Float64,2}(undef,num_λ,num_obs)
    var_matrix = Array{Float64,2}(undef,num_λ,num_obs)
    λ_vec = Array{Float64,1}(undef,num_λ)
    chunk_map = Array{UnitRange{Int64}}(undef,length(chunk_grids))
    mean_flux = zeros(num_λ)
    mean_var = zeros(num_λ)
    dfluxdlnλ = zeros(num_λ)
    d2fluxdlnλ2 = zeros(num_λ)
    if remove_rv_est
        @assert haskey(first(timeseries.metadata),:rv_est)
    end
    if (alg == :GP) && (maximum(length.(chunk_grids))>1024)
        @error "To use a GP with more than 1024 points in a chunk, use a more efficient GP package, e.g., :TemporalGP."
    end
    if alg == :Sinc    # Setup workspace for Sync.  TODO: Put into functor
        filter_size=23
        kaiserB=13
        sinc_filter = SincInterpolation.create_filter_curve(filter_size*21; filter_size=filter_size, kaiserB=kaiserB)
    end
    for t in 1:num_obs
       idx_start = 0
       boost_factor = remove_rv_est ? calc_doppler_factor(timeseries.metadata[t][:rv_est]) : 1.0
       for c in 1:length(chunk_grids)
           idx = (idx_start+1):(idx_start+length(chunk_grids[c]))
           if verbose
               flush(stdout);  println("t= ",t, " c= ",c," idx= ", idx, " size(flux)= ",size(flux_matrix))
           end
           if t == 1
               λ_vec[idx] .= chunk_grids[c]
               chunk_map[c] = idx
           end
           mean_flux_in_chunk = mean(timeseries.chunk_list[t].data[c].flux)
           if mean_flux_in_chunk > 0
               #println(" mean flux in chunk ", c, " at time ", t, " = ", mean_flux_in_chunk)
               if alg == :Linear
                   LinearInterpolation.interp_chunk_to_shifted_grid_linear!(view(flux_matrix,idx,t), view(var_matrix,idx,t), timeseries.chunk_list[t].data[c], chunk_grids[c], boost_factor )
                   view(flux_matrix,idx,t) .= calc_running_mean_weighted(view(flux_matrix,idx,t))
                   view(var_matrix,idx,t) .= calc_running_mean_weighted(view(var_matrix,idx,t))
               elseif alg == :Sinc
                   SincInterpolation.interp_chunk_to_shifted_grid_sinc!(view(flux_matrix,idx,t), view(var_matrix,idx,t), timeseries.chunk_list[t].data[c], chunk_grids[c], boost_factor, Filter=sinc_filter )
                   view(flux_matrix,idx,t) .= calc_running_mean_weighted(view(flux_matrix,idx,t))
                   view(var_matrix,idx,t) .= calc_running_mean_weighted(view(var_matrix,idx,t))
               elseif alg == :GP
                   GPInterpolation.interp_chunk_to_shifted_grid_gp!(view(flux_matrix,idx,t), view(var_matrix,idx,t), timeseries.chunk_list[t].data[c], chunk_grids[c], boost_factor)
               elseif alg == :TemporalGP
                   #TemporalGPsInterpolation.
                   interp_chunk_to_shifted_grid_gp_temporal!(view(flux_matrix,idx,t), view(var_matrix,idx,t), timeseries.chunk_list[t].data[c], chunk_grids[c], boost_factor, use_logx=true, use_logy=false, smooth_factor=smooth_factor)
               end
               if t == 1
                  λ_vec[idx] .= chunk_grids[c]
                  chunk_map[c] = idx
               end
            else  # no flux in chunk?!?
                @warn "No flux in chunk " * string(c) * " at time " * string(t) * "."
            end

           mean_flux[idx] .+= view(flux_matrix,idx,t)
           mean_var[idx]  .+= view(var_matrix,idx,t)
           #dfluxdlnλ[idx] .+= calc_dfluxdlnlambda(view(flux_matrix,idx,t),view(λ_vec,idx))
           #d2fluxdlnλ2[idx] .+= calc_d2fluxdlnlambda2(view(flux_matrix,idx,t),view(λ_vec,idx))

           flux_matrix[idx,t] ./= mean_flux_in_chunk
           var_matrix[idx,t]  ./= mean_flux_in_chunk^2

           idx_start += length(chunk_grids[c])
       end
    end

    # TODO Think if can propagate uncertainties better
    var_matrix .*= sqrt(oversample_factor)

    #flush(stdout)
    #println("# mean flux pre-normalize = ", mean(mean_flux))
    idx_start = 0
    for c in 1:length(chunk_grids)
        idx = (idx_start+1):(idx_start+length(chunk_grids[c]))
        mean_flux_in_chunk = mean(mean_flux[idx])
        #println("Normalizing chunk ", c, " (",idx,") by ", mean_flux_in_chunk)
        #dmeanfluxdlnλ[idx] .= calc_dfluxdlnlambda(vec(sum(view(flux_matrix,idx,:),dims=2)),vec(sum(view(var_matrix,idx,:), dims=2)) ) ./mean_flux_in_chunk
        #sum_in_chunk = vec(sum(view(flux_matrix,idx,:),dims=2))
        mean_flux[idx] ./= mean_flux_in_chunk
        mean_var[idx] ./= mean_flux_in_chunk^2
        #dfluxdlnλ[idx] ./= mean_flux_in_chunk
        #d2fluxdlnλ2[idx] ./= mean_flux_in_chunk
        if length(idx) >= 2
            dfluxdlnλ[idx] .= calc_dfluxdlnlambda(mean_flux[idx],view(λ_vec,idx))
        else
            dfluxdlnλ[idx] .= 0.0
        end
        if length(idx) >= 3
            d2fluxdlnλ2[idx] .= calc_d2fluxdlnlambda2(mean_flux[idx],view(λ_vec,idx))
        else
            d2fluxdlnλ2[idx] .= 0.0
        end
        idx_start += length(chunk_grids[c])
    end

    #println("# mean flux post-normalize = ", mean(mean_flux))
    #flush(stdout)
    #mean_flux /= num_obs
    mean_var .*= sqrt(oversample_factor)
    #dmeanfluxdlnλ ./= num_obs
    return ( matrix=SpectralTimeSeriesCommonWavelengths(λ_vec,flux_matrix,var_matrix,chunk_map, TheoreticalInstrument1D() ), mean_flux=mean_flux, mean_var=mean_var, deriv=dfluxdlnλ, deriv2=d2fluxdlnλ2 )
end

"""   repack_flux_vector_to_chunk_matrix(λ, flux, var, chunk_map, λc; alg )
Warning:  This doesn't work yet
"""
function repack_flux_vector_to_chunk_matrix(λ::AA1, flux::AA2, var::AA3, chunk_map::CMT, λc::AA4; alg::Symbol = :TemporalGP,
    oversample_factor::Real = 1, smooth_factor::Real=1, verbose::Bool = false ) where {
        T1<:Real, AA1<:AbstractVector{T1}, T2<:Real, AA2<:AbstractVector{T2}, T3<:Real, AA3<:AbstractVector{T3}, T4<:Real, AA4<:AbstractVector{T4},
        CMT<:AbstractArray{UnitRange{Int64}} } #RT<:AbstractRange, AR<:AbstractArray{RT,1}, T<:Real, AV<:AbstractVector{T}, AAV<:AbstractArray{AV,1} }
    @assert alg == :Linear || alg == :GP  || alg == :Sinc || alg == :TemporalGP # TODO: Eventually move to traits-based system?
	@warn "This doesn't work yet"
    num_chunks = length(chunk_map)
    num_λ_per_chunk = minimum(length.(chunk_map))
    flux_matrix = zeros(num_λ_per_chunk,num_chunks)
    var_matrix = zeros(num_λ_per_chunk,num_chunks)
    if (alg == :GP) && (num_λ_per_chunk>1024)
        @error "To use a GP with more than 1024 points in a chunk, use a more efficient GP package, e.g., :TemporalGP."
    end
    if alg == :Sinc    # Setup workspace for Sync.  TODO: Put into functor
        filter_size=23
        kaiserB=13
        sinc_filter = SincInterpolation.create_filter_curve(filter_size*21; filter_size=filter_size, kaiserB=kaiserB)
    end

   idx_start = 0
   for c in 1:length(chunk_map)
       idx_in = (idx_start+1):(idx_start+length(chunk_map[c]))
       idx_out_begin = floor(Int,(idx_in[1]+idx_in[end])//2)-floor(Int,num_λ_per_chunk//2)
       idx_out_end = idx_out_begin+num_λ_per_chunk-1
       idx_out = idx_out_begin:idx_out_end
       Δλ_in_chunk = λ[idx_out].-λc[c]
       mean_flux_in_chunk = mean(flux[idx_out])
       chunk_of_spectrum = ChunkOfSpectrum{eltype(λ),eltype(flux),eltype(var)}(view(λ,idx_out), view(flux,idx_out), view(var,idx_out))
       Δv = 400  # TODO: Figure out good choice for Δv
       Δλ = Δv /RvSpectML.speed_of_light_mps*λc[c]
       if mean_flux_in_chunk > 0
           if alg == :Linear
               LinearInterpolation.interp_chunk_to_grid_linear!(view(flux_matrix,:,c), view(var_matrix,:,c), chunk_of_spectrum, chunk_grid)
               view(flux_matrix,idx,:,c) .= calc_running_mean_weighted(view(flux_matrix,idx,:,c))
               view(var_matrix,idx,:,c) .= calc_running_mean_weighted(view(var_matrix,idx,:,c))
           elseif alg == :Sinc
               chunk_grid = range(λc[c]-(num_λ_per_chunk-1)//2*Δλ, stop=λc[c]+(num_λ_per_chunk-1)//2*Δλ, length=num_λ_per_chunk )
               SincInterpolation.interp_chunk_to_grid_sinc!(view(flux_matrix,:,c), view(var_matrix,:,c), chunk_of_spectrum, chunk_grid, Filter=sinc_filter)
               view(flux_matrix,idx,:,c) .= calc_running_mean_weighted(view(flux_matrix,idx,:,c))
               view(var_matrix,idx,:,c) .= calc_running_mean_weighted(view(var_matrix,idx,:,c))
           elseif alg == :GP
               GPInterpolation.interp_chunk_to_grid_gp!(view(flux_matrix,:,c), view(var_matrix,:,c), chunk_of_spectrum, chunk_grid)
           elseif alg == :TemporalGP
               TemporalGPsInterpolation.interp_chunk_to_grid_gp_temporal!(view(flux_matrix,:,c), view(var_matrix,:,c), chunk_of_spectrum, chunk_grid , use_logx=true, use_logy=false, smooth_factor=smooth_factor)
           end
       else  # no flux in chunk?!?
           @warn "No flux in chunk " * string(c) * " at time " * string(t) * "."
       end
       idx_start += length(chunk_map[c])
   end # for c
   return flux_matrix
    #flush(stdout)
    #println("# mean flux pre-normalize = ", mean(mean_flux))
    #=
    idx_start = 0
    for c in 1:length(chunk_grids)
        idx = (idx_start+1):(idx_start+length(chunk_grids[c]))
        mean_flux_in_chunk = mean(mean_flux[idx])
        #println("Normalizing chunk ", c, " (",idx,") by ", mean_flux_in_chunk)
        #dmeanfluxdlnλ[idx] .= calc_dfluxdlnlambda(vec(sum(view(flux_matrix,idx,:),dims=2)),vec(sum(view(var_matrix,idx,:), dims=2)) ) ./mean_flux_in_chunk
        mean_flux[idx] ./= mean_flux_in_chunk
        mean_var[idx] ./= mean_flux_in_chunk^2
        #dfluxdlnλ[idx] ./= mean_flux_in_chunk
        #d2fluxdlnλ2[idx] ./= mean_flux_in_chunk
        dfluxdlnλ[idx] .= calc_dfluxdlnlambda(mean_flux[idx],view(λ_vec,idx))
        d2fluxdlnλ2[idx] .= calc_d2fluxdlnlambda2(mean_flux[idx],view(λ_vec,idx))

        #dmeanfluxdlnλ[idx] .= calc_dfluxdlnlambda(vec(sum(view(flux_matrix,idx,:),dims=2)),view(λ_vec,idx))  ./mean_flux_in_chunk
        idx_start += length(chunk_grids[c])
    end
    =#
    #println("# mean flux post-normalize = ", mean(mean_flux))
    #flush(stdout)
    #mean_flux /= num_obs
    #mean_var .*= sqrt(oversample_factor)
    #dmeanfluxdlnλ ./= num_obs
    #return flux ( matrix=SpectralTimeSeriesCommonWavelengths(λ_vec,flux_matrix,var_matrix,chunk_map, TheoreticalInstrument1D() ), mean_flux=mean_flux, mean_var=mean_var, deriv=dfluxdlnλ, deriv2=d2fluxdlnλ2 )
end


"""
    Before calling this function one should check that `startpos` is inside `det`: `in(startpos, det`

"""
function drift_charge!(
                            drift_path::Vector{CartesianPoint{T}},
                            det::SolidStateDetector{T, :cartesian},
                            point_types::PointTypes{T, 3, :cartesian},
                            grid::Grid{T, 3, :cartesian},
                            startpos::CartesianPoint{T},
                            Δt::T,
                            velocity_field::Interpolations.Extrapolation{<:StaticVector{3}, 3};
                            verbose::Bool = true
                        )::Nothing where {T <: SSDFloat}
    done::Bool = false
    drift_path[1] = startpos
    null_step::CartesianVector{T} = CartesianVector{T}(0, 0, 0)
    @inbounds for istep in eachindex(drift_path)[2:end] #end]
        if done == false
            current_pos::CartesianPoint{T} = drift_path[istep - 1]
            stepvector::CartesianVector{T} = get_velocity_vector(velocity_field, current_pos) * Δt
            if stepvector == null_step done = true end
            next_pos::CartesianPoint{T} = current_pos + stepvector
            if next_pos in point_types
                drift_path[istep] = next_pos
            elseif next_pos in det
                drift_path[istep] = next_pos
            else
                crossing_pos::CartesianPoint{T}, cd_point_type::UInt8, boundary_index::Int, surface_normal::CartesianVector{T} = get_crossing_pos(det, grid, current_pos, next_pos)
                if cd_point_type == CD_ELECTRODE
                    done = true
                    drift_path[istep] = crossing_pos
                elseif cd_point_type == CD_FLOATING_BOUNDARY
                    projected_vector = project_to_plane(stepvector, surface_normal )
                    next_pos = current_pos + projected_vector
                    # ToDo: We actually need a time array as well to do this properly...
                    small_projected_vector = projected_vector * T(0.001)
                    i::Int = 0
                    while i < 1000 && !(next_pos in det)
                        next_pos -= small_projected_vector
                        i += 1
                    end
                    if i == 1000 && verbose @warn("Handling of charge at floating boundary did not work as intended. Start Position (Cart): $startpos") end
                    drift_path[istep] = next_pos
                elseif cd_point_type == CD_BULK
                    if verbose @warn ("Internal error for charge starting at $startpos") end
                    drift_path[istep] = current_pos
                    done = true
                else # elseif cd_point_type == CD_OUTSIDE
                    if verbose @warn ("Internal error for charge starting at $startpos") end
                    drift_path[istep] = current_pos
                    done = true
                end
            end
        elseif done == true
            drift_path[istep] = drift_path[istep-1]
        end
    end
    return nothing
end

# Point types for charge drift: Defined in DetectorGeometries/DetectorGeometries.jl
# const CD_ELECTRODE = 0x00
# const CD_OUTSIDE = 0x01
# const CD_BULK = 0x02
# const CD_FLOATING_BOUNDARY = 0x04 # not 0x03, so that one could use bit operations here...

function get_crossing_pos(  detector::SolidStateDetector{T, :cartesian}, grid::Grid{T, 3}, point_in::CartesianPoint{T}, point_out::CartesianPoint{T};
                            max_n_iter::Int = 2000)::Tuple{CartesianPoint{T}, UInt8, Int, CartesianVector{T}} where {T <: SSDFloat}
    point_mid::CartesianPoint{T} = T(0.5) * (point_in + point_out)
    cd_point_type::UInt8, contact_idx::Int, surface_normal::CartesianVector{T} = point_type(detector, grid, point_mid)
    for i in 1:max_n_iter
        if cd_point_type == CD_BULK
            point_in = point_mid
        elseif cd_point_type == CD_OUTSIDE
            point_out = point_mid
        elseif cd_point_type == CD_ELECTRODE
            break
        else #elseif cd_point_type == CD_FLOATING_BOUNDARY
            break
        end
        point_mid = T(0.5) * (point_in + point_out)
        cd_point_type, contact_idx, surface_normal = point_type(detector, grid, point_mid)
    end
    return point_mid, cd_point_type, contact_idx, surface_normal
end


"""
    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

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 input dimensions
    ni_high    = 50,              # Number of local 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.

    #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.
    #

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

    root = 0 # global node

    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_lrho = [ADAM(1. * 0.0075) for ii in 1:len]     #     opt  = ADAM(0.0075, (0.5, 0.8))
        lopt_lkap = [ADAM(1. * 0.0075) for ii in 1:len]      #     opt  = ADAM(0.0075, (0.5, 0.8))
        lopt_lsig = [ADAM(1. * 0.0075) for ii in 1:len]      #     opt  = ADAM(0.0075, (0.5, 0.8))
        loptm     = [ADAM(1. * 0.01)   for ii in 1:len]        #     optm = ADAM(0.02, (0.5, 0.8))
        loptS     = [ADAM(1. * 0.002)  for ii in 1:len]       #     optS = ADAM(0.002, (0.5, 0.8))
        loptx     = [ADAM(1. * 0.01)   for ii in 1:len]        #     optx = ADAM(0.01, (0.5, 0.8))

    end # myrank != root

    if myrank == root

        # 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_lrho = ADAM(1. * 0.0075)     #     opt  = ADAM(0.0075, (0.5, 0.8))
        opt_lkap = ADAM(1. * 0.0075)     #     opt  = ADAM(0.0075, (0.5, 0.8))
        opt_lsig = ADAM(1. * 0.0075)     #     opt  = ADAM(0.0075, (0.5, 0.8))
        optm     = ADAM(1. * 0.01)       #     optm = ADAM(0.02, (0.5, 0.8))
        optS     = ADAM(1. * 0.002)      #     optS = ADAM(0.002, (0.5, 0.8))
        optx     = ADAM(1. * 0.01)       #     optx = ADAM(0.01, (0.5, 0.8))

        # 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(gp -> svgp_elbo_global(xi_low, yi_low, gp, global_svgp), global_svgp.params)[1][]

            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)
            # println("global ELBO optimized")

            # Sending prior_mean, prior_cov to local process ###################
            send_prior_to_local( pred_vgp(xi_low, global_svgp), recv_id, comm) ### TODO: IMPORTANT! Need to communicate which partition is being recieved
            # println("prior_mean, prior_cov sent to local")

        end # for loop

    end # myrank == root

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

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

    for iter= 1:n_iters
        if mod(iter, 10) == 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
                for sub_iter = 1:n_subcycle_ELBO_local
                    inds   = rand(1:size(local_svgps[ii].data.y)[1], batch_size)

                    grads  = gradient(gp -> svgp_elbo_local(
                            local_svgps[ii].data.x[inds,:],
                            local_svgps[ii].data.y[inds],
                            gp,
                            local_svgps[ii],
                            (prior_mean, prior_sd)
                            ), local_svgps[ii].params)[1][]

                    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)
                    if sub_iter == n_subcycle_ELBO_local
                        # 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
                end
                # println("local ELBO optimized")

                # Step 6
                # Sending inducing point data to global
                send_inducing_points_to_global(local_svgps[ii].params.inducing_locs, local_svgps[ii].params.inducing_mean, root, comm)
            end

        end # myrank != root

        if myrank == root
            step_elbo = 0.
            for part = (root+1):n_parts
                status  = MPI.Probe(MPI.MPI_ANY_SOURCE, MPI.MPI_ANY_TAG, comm)
                recv_id = status.source

                xi_low  = zeros(ni_low, n_dims)
                rmsg    = MPI.Recv!(xi_low, recv_id, 0, comm)
                yi_low  = zeros(ni_low)
                rmsg    = MPI.Recv!(yi_low, recv_id, 1, comm)s

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

                    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
                # println("global ELBO optimized")

                # Update step_elbo
                step_elbo += svgp_elbo(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), recv_id, comm) # MAKE SURE COMMUNICATION INDICATES WHICH PARTITION IN A PROCESS THIS COMM IS FOR
                # println("prior_mean, prior_cov sent to local")

            end # for loop

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

        MPI.Barrier(comm)
    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


## Dense matrix interface

dense_matrix_rows(s::CDenseMatrix) = ccall((:dense_matrix_rows, libsymengine), Int, (Ptr{Cvoid}, ), s.ptr)
dense_matrix_cols(s::CDenseMatrix) = ccall((:dense_matrix_cols, libsymengine), Int, (Ptr{Cvoid}, ), s.ptr)
function dense_matrix_rows_cols(mat::CDenseMatrix, r::UInt, c::UInt)
    ccall((:dense_matrix_rows_cols, libsymengine), Int, (Ptr{Cvoid}, UInt, UInt), s.ptr, r, c)
end

function dense_matrix_get_basic(s::CDenseMatrix, r::Int, c::Int)
    result = Basic()
    ccall((:dense_matrix_get_basic, libsymengine), Nothing, (Ref{Basic}, Ptr{Cvoid}, UInt, UInt), result, s.ptr, UInt(r), UInt(c))
    result
end

function dense_matrix_set_basic(s::CDenseMatrix, val, r::Int, c::Int)
    value = Basic(val)
    ccall((:dense_matrix_set_basic, libsymengine), Nothing, (Ptr{Cvoid}, UInt, UInt, Ref{Basic}), s.ptr, UInt(r), UInt(c), value)
    value
end

## Basic operations det, inv, transpose
function dense_matrix_det(s::CDenseMatrix)
    result = Basic()
    ccall((:dense_matrix_det, libsymengine), Nothing, (Ref{Basic}, Ptr{Cvoid}), result, s.ptr)
    result
end
function dense_matrix_inv(s::CDenseMatrix)
    result = CDenseMatrix()
    ccall((:dense_matrix_inv, libsymengine), Nothing, (Ptr{Cvoid}, Ptr{Cvoid}), result.ptr, s.ptr)
    result
end

function dense_matrix_transpose(s::CDenseMatrix)
    result = CDenseMatrix()
    ccall((:dense_matrix_transpose, libsymengine), Nothing, (Ptr{Cvoid}, Ptr{Cvoid}), result.ptr, s.ptr)
    result
end

function dense_matrix_submatrix(mat::CDenseMatrix, r1::Int, c1::Int, r2::Int, c2::Int, r::Int, c::Int)
    s = CDenseMatrix()
    ccall((:dense_matrix_inv, libsymengine), Nothing, (Ptr{Cvoid}, Ptr{Cvoid}, UInt, UInt, UInt, UInt, UInt, UInt),
          s.ptr, mat.ptr, UInt(r1), UInt(c1), UInt(r2), UInt(c2), UInt(r), UInt(c))
    s
end

## some matrix arithmetic methods
## These are unncecessary, as +,-,*,^ are inhertied by the AbstractArray Interface. Those return Array{Basic,2}. These
## return CDenseMatrix objects.
function dense_matrix_add_matrix(a::CDenseMatrix, b::CDenseMatrix)
    result = CDenseMatrix()
    ccall((:dense_matrix_add_matrix, libsymengine), Nothing, (Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cvoid}), result.ptr, a.ptr, b.ptr)
    result
end

function dense_matrix_mul_matrix(a::CDenseMatrix, b::CDenseMatrix)
    result = CDenseMatrix()
    ccall((:dense_matrix_mul_matrix, libsymengine), Nothing, (Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cvoid}), result.ptr, a.ptr, b.ptr)
    result
end

function dense_matrix_add_scalar(a::CDenseMatrix, b::Basic)
    result = CDenseMatrix()
    ccall((:dense_matrix_add_scalar, libsymengine), Nothing, (Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cvoid}), result.ptr, a.ptr, b.ptr)
    result
end

function dense_matrix_mul_scalar(a::CDenseMatrix, b::Basic)
    result = CDenseMatrix()
    ccall((:dense_matrix_mul_scalar, libsymengine), Nothing, (Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cvoid}), result.ptr, a.ptr, b.ptr)
    result
end

## Factorizations ##################################################
function dense_matrix_LU(mat::CDenseMatrix)
    ## need square?
    L = CDenseMatrix()
    U = CDenseMatrix()
    ccall((:dense_matrix_LU, libsymengine), Nothing, (Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cvoid}), L.ptr, U.ptr,  mat.ptr)
    (L, U)
end

## LDL decomposition
function dense_matrix_LDL(mat::CDenseMatrix)
    L = CDenseMatrix()
    D = CDenseMatrix()
    ccall((:dense_matrix_LDL, libsymengine), Nothing, (Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cvoid}), L.ptr, D.ptr,  mat.ptr)
    (L, D)
end

##  Fraction free LU factorization
function dense_matrix_FFLU(mat::CDenseMatrix)
    LU = CDenseMatrix()
    ccall((:dense_matrix_FFLU, libsymengine), Nothing, (Ptr{Cvoid}, Ptr{Cvoid}), LU.ptr, mat.ptr)
    LU
end

## Fraction free LDU factorization
function dense_matrix_FFLDU(mat::CDenseMatrix)
    L = CDenseMatrix()
    D = CDenseMatrix()
    U = CDenseMatrix()

    ccall((:dense_matrix_FFLDU, libsymengine), Nothing, (Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cvoid}), L.ptr, D.ptr, U.ptr, mat.ptr)
    (L, D, U)
end

function dense_matrix_LU_solve(A::CDenseMatrix, b::CDenseMatrix)
    x = CDenseMatrix()
    ccall((:dense_matrix_LU_solve, libsymengine), Nothing, (Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cvoid}), x.ptr, A.ptr, b.ptr)
    x
end

function dense_matrix_zeros(::Type{CDenseMatrix}, r::Int, c::Int)
    result = CDenseMatrix()
    ccall((:dense_matrix_zeros, libsymengine), Nothing, (Ptr{Cvoid}, UInt, UInt), result.ptr, UInt(r), UInt(c))
    result
end

function dense_matrix_ones(r::Int, c::Int)
    result = CDenseMatrix()
    ccall((:dense_matrix_ones, libsymengine), Nothing, (Ptr{Cvoid}, UInt, UInt), result.ptr, UInt(r), UInt(c))
    result
end

## dense_matrix_diag XXX don't have CVecBasic constructor

function dense_matrix_eye(N::Int, M::Int, k::Int=0)
    s = CDenseMatrix()
    ccall((:dense_matrix_eye, libsymengine), Nothing, (Ptr{Cvoid}, UInt, UInt, Int), s.ptr, UInt(N), UInt(M), k)
    s
end

dense_matrix_eq(a::CDenseMatrix, b::CDenseMatrix) = ccall((:dense_matrix_eq, libsymengine), Int, (Ptr{Cvoid},Ptr{Cvoid}), a.ptr, b.ptr)

## Plug into Julia's interface ##################################################

import Base: ==
==(a::CDenseMatrix, b::CDenseMatrix)  = dense_matrix_eq(a, b) == 1

## Abstract Array Interface
Base.size(s::CDenseMatrix) = (dense_matrix_rows(s), dense_matrix_cols(s))
Base.getindex(s::CDenseMatrix, r::Int, c::Int) = dense_matrix_get_basic(s, r-1, c-1)
Base.setindex!(s::CDenseMatrix, val, r::Int, c::Int) = dense_matrix_set_basic(s, val, r-1, c-1)

## special matrices
Base.zeros(::Type{CDenseMatrix}, r::Int, c::Int) = dense_matrix_zeros(s, r-1, c-1)
Base.ones(::Type{CDenseMatrix}, r::Int, c::Int) = dense_matrix_ones(r-1, c-1)

## basic functions
LinearAlgebra.det(s::CDenseMatrix) = dense_matrix_det(s)
Base.inv(s::CDenseMatrix) = dense_matrix_inv(s)
Base.transpose(s::CDenseMatrix) = dense_matrix_transpose(s)

LinearAlgebra.factorize(M::CDenseMatrix) = factorize(convert(Matrix, M))

"""
LU decomposition for CDenseMatrix, dense matrices of symbolic values

Also: lufact(a, val{:false}) for non-pivoting lu factorization
"""
function LinearAlgebra.lu(a::CDenseMatrix)
    l, u = dense_matrix_LU(a)
    convert(Matrix, l), convert(Matrix, u), Matrix{Basic}(LinearAlgebra.I, size(l)[1], size(l)[1])
end

# solve using LU_solve
import Base: \
\(A::CDenseMatrix, b::CDenseMatrix) = dense_matrix_LU_solve(A, b)
\(A::CDenseMatrix, b::Vector) = A \ convert(CDenseMatrix, [convert(Basic,u) for u in b])


"""
    setup_problem(N, Npairs, pair_order)

Create the list of objects necessary for the PAPA reconstruction.
"""
function setup_problem(N::Int,Npairs::Int,pair_order::Array{Int,2})

    # Dimensions and parameter numbers
    dim = 2^N
    Choi_dim = dim^2
    dim2q = 16
    Nel = convert(Int,dim2q*(dim2q+1)/2)

    # Two-qubit bases
    Bases = zeros(Npairs,dim2q,dim,dim)
    B2 = zeros(1,16,4,4)

    bases_create!(Bases,N,Npairs,pair_order)
    Bases = convert(Array{ComplexF64,4},Bases)

    p_temp = zeros(Int,1,2)
    p_temp[1,:] = [1 2]
    # p_temp = convert(Array{Int,2},p_temp)
    bases_create!(B2,2,1,p_temp)
    B2 = convert(Array{ComplexF64,4},B2)

    # Maximally entangled states
    mrho = zeros(dim^2,dim^2)
    mrho2 = zeros(16,16)
    max_ent_state!(dim;mrho=mrho)
    max_ent_state!(4,mrho=mrho2)

    # Identities
    IN = iden(dim)
    I4 = iden(4)

    # Choi and chi matrices
    Choi = zeros(ComplexF64,Npairs,Choi_dim,Choi_dim)
    Choi2q = zeros(ComplexF64,dim2q,dim2q)
    chitemp = zeros(ComplexF64,dim2q,dim2q)
    rhotemp = zeros(ComplexF64,dim,dim)
    tempE = zeros(ComplexF64,16)
    tempF = zeros(ComplexF64,Nel)

    # Counters and qubit labels
    start = 0
    stop = 0
    qstemp = ones(Int,N)

    return (Bases,B2,N,Npairs,pair_order,mrho,mrho2,IN,I4,Choi_dim,dim2q,Nel,Choi,Choi2q,chitemp,rhotemp,tempE,tempF,start,stop,qstemp)
end

"""
Functionalized version of the full process solver
"""
function process_solver(chilist::Vector{Float64},sigmaVec::Vector{Float64},setup_tuple::Tuple,CP_penalty::Float64)
    return process_solver_full(chilist,sigmaVec,CP_penalty,setup_tuple...)
end

function process_solver!(F::Vector{Float64},chilist::Vector{Float64},sigmaVec::Vector{Float64},setup_tuple::Tuple,CP_penalty::Float64)
    F[:] = process_solver_full(chilist,sigmaVec,CP_penalty,setup_tuple...)
    nothing
end

"""
Full process solver that implements the PAPA_BEAR bootstrapping
"""
function process_solver_full(chilist::Vector{Float64},sigmaVec::Vector{Float64},CP_penalty::Float64,Bases::Array{ComplexF64,4},B2::Array{ComplexF64,4},N::Int,Npairs::Int,pair_order::Array{Int,2},mrho::Array{Float64,2},mrho2::Array{Float64,2},IN::Array{ComplexF64,2},I4::Array{ComplexF64,2},Choi_dim::Int,dim2q::Int,Nel::Int,Choi::Array{ComplexF64,3},Choi2q::Array{ComplexF64,2},chitemp::Array{ComplexF64,2},rhotemp::Array{ComplexF64,2},tempE::Array{ComplexF64,1},tempF::Array{ComplexF64,1},start::Int,stop::Int,qstemp::Vector{Int})::Vector{Float64}

    F = []

    fill!(Choi,0.)

    for nn = 1:1:Npairs
        fill!(chitemp,0.)
        start = 1 + (nn-1)*Nel
        stop = nn*Nel;
        chitemp[triu(trues(dim2q,dim2q))] = chilist[start:1:stop] + 1im*chilist[(start+Npairs*Nel):1:(stop+Npairs*Nel)]
        chitemp[:,:] = chitemp+chitemp'
        chitemp[:,:] = chitemp - diagm(0 => diag(chitemp))/2

        fill!(Choi2q,0.)
        for aa = 1:1:dim2q
            for bb = 1:1:dim2q
                # Choi state for the 2-qubit process on all N qubits
                Choi[nn,:,:] = Choi[nn,:,:] + chitemp[aa,bb]*kron(IN,Bases[nn,aa,:,:])*mrho*kron(IN,Bases[nn,bb,:,:]')

                # Choi state for the 2-qubit process on the 2-qubits only (for constraints use)
                Choi2q[:,:] = Choi2q[:,:] + chitemp[aa,bb]*kron(I4,B2[1,aa,:,:])*mrho2*kron(I4,B2[1,bb,:,:]')
            end
        end

        tempE = eigvals(Choi2q)

        F = vcat(F,[real(4*partial_trace(Choi2q,[0,1],[4,4],2)[:]-I4[:]);imag(4*partial_trace(Choi2q,[0,1],[4,4],2)[:]-I4[:]);CP_penalty*abs(sum(tempE[tempE.<0]))])

    end

    for pp = 1:1:Npairs
        fill!(chitemp,0.)  #ignore the name, used as a temp array

        fill!(qstemp,1)
        qstemp[pair_order[pp,1]] = 0
        qstemp[pair_order[pp,2]] = 0

        # Calculate the reduced process
        for dd = 1:1:dim2q
            fill!(rhotemp,0.) #ignore the name, used as a temp array
            full_process(rhotemp,Bases[pp,dd,:,:],Choi,IN)
            chitemp[:,:] = chitemp[:,:] + kron(B2[1,dd,:,:],partial_trace(rhotemp,qstemp,2*ones(Int,N),N));
        end
        chitemp[:,:] = chitemp[:,:]/2^N

        # Compare to input data
        start = 1 + (pp-1)*Nel
        stop = pp*Nel
        tempF[:] = chitemp[triu(trues(dim2q,dim2q))]

        F = vcat(F,[real(tempF)-sigmaVec[start:1:stop];imag(tempF)-sigmaVec[(start+Npairs*Nel):1:(stop+Npairs*Nel)]])
    end

    return abs.(F)

end


abstract type Preset end

export Glider, BHeptomino, GosperGun

struct Glider <: Preset
    data::Array{Int,2}
    size::Tuple{Int,Int}
end
Glider() = Glider([
    0 1 0
    0 0 1
    1 1 1
], (3,3))

struct BHeptomino <: Preset
    data::Array{Int,2}
    size::Tuple{Int,Int}
end
BHeptomino() = BHeptomino([
    1 0 1 1
    1 1 1 0
    0 1 0 0
], (3,4))

struct GosperGun <: Preset
    data::Array{Int,2}
    size::Tuple{Int,Int}
end
GosperGun() = GosperGun([
    0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0
    0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0
    0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 1 1
    0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 1 1
    1 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0
    1 1 0 0 0 0 0 0 0 0 1 0 0 0 1 0 1 1 0 0 0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0
    0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0
    0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
    0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
], (9, 36)
)


"""
$(DocStringExtensions.TYPEDSIGNATURES)

A short description of the function

```jldoctest
julia> 1 + 1
2
```
"""
function determine_edge_probabilities(graph, strand)
    kmers = graph.gprops[:kmers]
    outgoing_edge_probabilities = SparseArrays.spzeros(length(kmers), length(kmers))

    for (kmer_index, kmer) in enumerate(kmers)
        if !strand
            kmer = BioSequences.reverse_complement(kmer)
        end

        downstream_neighbor_indices = Int[]
        for neighbor in BioSequences.neighbors(kmer)
            index = get_kmer_index(kmers, BioSequences.canonical(neighbor))
            # kmer must be in our dataset and there must be a connecting edge
            if !isnothing(index) && Graphs.has_edge(graph, ordered_edge(kmer_index, index))
                push!(downstream_neighbor_indices, index)
            end
        end
        sort!(unique!(downstream_neighbor_indices))

        downstream_edge_weights = Int[
            length(get(graph.edge_evidence, ordered_edge(kmer_index, neighbor_index), EdgeEvidence[])) for neighbor_index in downstream_neighbor_indices
        ]

        non_zero_indices = downstream_edge_weights .> 0
        downstream_neighbor_indices = downstream_neighbor_indices[non_zero_indices]
        downstream_edge_weights = downstream_edge_weights[non_zero_indices]

        downstream_edge_likelihoods = downstream_edge_weights ./ sum(downstream_edge_weights)

        for (neighbor_index, likelihood) in zip(downstream_neighbor_indices, downstream_edge_likelihoods)
            outgoing_edge_probabilities[kmer_index, neighbor_index] = likelihood
        end
    end
    return outgoing_edge_probabilities
end

"""
$(DocStringExtensions.TYPEDSIGNATURES)

A short description of the function

```jldoctest
julia> 1 + 1
2
```
"""
function determine_edge_probabilities(graph)
    outgoing_edge_probabilities = determine_edge_probabilities(graph, true)
    incoming_edge_probabilities = determine_edge_probabilities(graph, false)
    return outgoing_edge_probabilities, incoming_edge_probabilities
end

"""
$(DocStringExtensions.TYPEDSIGNATURES)

Get dna (db = "nuccore") or protein (db = "protein") sequences from NCBI
or get fasta directly from FTP site

```jldoctest
julia> 1 + 1
2
```
"""
function oriented_unbranching_walk(kmer_graph, vertex, orientation)
    walk = []
    viable_neighbors = find_unbranched_neighbors(kmer_graph, vertex, orientation)
    while length(viable_neighbors) == 1
#         @show "found a viable neighbor!!"
        viable_neighbor = first(viable_neighbors)
        edge = Graphs.Edge(vertex, viable_neighbor)
        push!(walk, edge)
        vertex = edge.dst
        viable_neighbors = Set{Int}()
        destination_orientations = [o.destination_orientation for o in kmer_graph.eprops[edge][:orientations]]
        for destination_orientation in destination_orientations
            union!(viable_neighbors, find_unbranched_neighbors(kmer_graph, vertex, destination_orientation))
        end
    end
    return walk
end

"""
$(DocStringExtensions.TYPEDSIGNATURES)

Get dna (db = "nuccore") or protein (db = "protein") sequences from NCBI
or get fasta directly from FTP site

```jldoctest
julia> 1 + 1
2
```
"""
function find_unbranched_neighbors(kmer_graph, vertex, orientation)
    downstream_vertices = find_downstream_vertices(kmer_graph, vertex, orientation)
#     backtrack_vertices
#     @show downstream_vertices
    if length(downstream_vertices) == 1
        downstream_vertex = first(downstream_vertices)
#         @show downstream_vertex
        edge = Graphs.Edge(vertex, downstream_vertex)
#         @show edge
        destination_orientations = [o.destination_orientation for o in kmer_graph.eprops[edge][:orientations]]
#         @show destination_orientations
        for destination_orientation in destination_orientations
            backtrack_vertices = find_downstream_vertices(kmer_graph, downstream_vertex, !destination_orientation)
#             @show backtrack_vertices
            # if the only backtrack is the vertex we're on, then we can simplify
            if backtrack_vertices == Set([vertex])
                return downstream_vertices
            end
        end
    end
    return Int[]
end

"""
$(DocStringExtensions.TYPEDSIGNATURES)

Get dna (db = "nuccore") or protein (db = "protein") sequences from NCBI
or get fasta directly from FTP site

```jldoctest
julia> 1 + 1
2
```
"""
function find_downstream_vertices(kmer_graph, vertex, orientation)
    viable_neighbors = Set{Int}()
    for neighbor in Graphs.neighbors(kmer_graph, vertex)
        not_same_vertex = vertex != neighbor
        candidate_edge = Graphs.Edge(vertex, neighbor)
        # palindromes can have multiple viable orientations
        # check each viable orientation individually
        edge_src_orientations = [e.source_orientation for e in kmer_graph.eprops[candidate_edge][:orientations]]
        for edge_src_orientation in edge_src_orientations
#             edge_src_orientation = kmer_graph.eprops[candidate_edge][:orientations].source_orientation
            viable_orientation = edge_src_orientation == orientation
            if not_same_vertex && viable_orientation
                push!(viable_neighbors, neighbor)
            end
        end
    end
    return viable_neighbors
end

# """
# $(DocStringExtensions.TYPEDSIGNATURES)

# A short description of the function

# ```jldoctest
# julia> 1 + 1
# 2
# ```
# """
# function reverse_oriented_path(oriented_path)
#     reversed_path = copy(oriented_path)
#     for (index, state) in enumerate(oriented_path)
#         reversed_path[index] = Mycelia.OrientedKmer(index = state.index, orientation = !state.orientation)
#     end
#     return reverse!(reversed_path)
# end

# """
# $(DocStringExtensions.TYPEDSIGNATURES)

# A short description of the function

# ```jldoctest
# julia> 1 + 1
# 2
# ```
# """
# function maximum_likelihood_walk(graph, connected_component)
#     max_count = maximum(graph.counts[connected_component])
#     max_count_indices = findall(count -> count == max_count, graph.counts[connected_component])
#     initial_node_index = rand(max_count_indices)
#     initial_node = connected_component[initial_node_index]
#     outgoing_edge_probabilities, incoming_edge_probabilities = Mycelia.determine_edge_probabilities(graph)
#     forward_walk = maximum_likelihood_walk(graph, [Mycelia.OrientedKmer(index = initial_node, orientation = true)], outgoing_edge_probabilities, incoming_edge_probabilities)
#     reverse_walk = maximum_likelihood_walk(graph, [Mycelia.OrientedKmer(index = initial_node, orientation = false)], outgoing_edge_probabilities, incoming_edge_probabilities)
#     reversed_reverse_walk = reverse!(
#         [
#             Mycelia.OrientedKmer(index = oriented_kmer.index, orientation = oriented_kmer.orientation)
#             for oriented_kmer in reverse_walk[2:end]
#         ]
#         )
#     full_path = [reversed_reverse_walk..., forward_walk...]
# end

# """
# $(DocStringExtensions.TYPEDSIGNATURES)

# A short description of the function

# ```jldoctest
# julia> 1 + 1
# 2
# ```
# """
# function maximum_likelihood_walk(graph, path::Vector{Mycelia.OrientedKmer}, outgoing_edge_probabilities, incoming_edge_probabilities)
#     done = false
#     while !done
#         maximum_path_likelihood = 0.0
#         maximum_likelihood_path = Vector{Mycelia.OrientedKmer}()
#         for neighbor in Graphs.neighbors(graph.graph, last(path).index)
#             this_path = [last(path).index, neighbor]
#             this_oriented_path, this_path_likelihood =
#                 Mycelia.assess_path(this_path,
#                     graph.kmers,
#                     graph.counts,
#                     last(path).orientation,
#                     outgoing_edge_probabilities,
#                     incoming_edge_probabilities)
#             if this_path_likelihood > maximum_path_likelihood
#                 maximum_path_likelihood = this_path_likelihood
#                 maximum_likelihood_path = this_oriented_path
#             end
#         end
#         if isempty(maximum_likelihood_path) && (maximum_path_likelihood == 0.0)
#             done = true
#         else
#             append!(path, maximum_likelihood_path[2:end])
#         end
#     end
#     return path
# end

# """
# $(DocStringExtensions.TYPEDSIGNATURES)

# A short description of the function

# ```jldoctest
# julia> 1 + 1
# 2
# ```
# """
# function take_a_walk(graph, connected_component)
#     max_count = maximum(graph.counts[connected_component])
#     max_count_indices = findall(count -> count == max_count, graph.counts[connected_component])
#     initial_node_index = rand(max_count_indices)
#     initial_node = connected_component[initial_node_index]
#     outgoing_edge_probabilities, incoming_edge_probabilities = Mycelia.determine_edge_probabilities(graph)

#     # walk forwards from the initial starting node
#     forward_walk = take_a_walk(graph, [Mycelia.OrientedKmer(index = initial_node, orientation = true)], outgoing_edge_probabilities, incoming_edge_probabilities)

#     # walk backwards from the initial starting node
#     reverse_walk = take_a_walk(graph, [Mycelia.OrientedKmer(index = initial_node, orientation = false)], outgoing_edge_probabilities, incoming_edge_probabilities)

#     # we need to reverse everything to re-orient against the forward walk
#     reverse_walk = reverse_oriented_path(reverse_walk)

#     # also need to drop the last node, which is equivalent to the first node of the
#     @assert last(reverse_walk) == first(forward_walk)
#     full_path = [reverse_walk[1:end-1]..., forward_walk...]
# #     @show full_path
# end

# """
# $(DocStringExtensions.TYPEDSIGNATURES)

# A short description of the function

# ```jldoctest
# julia> 1 + 1
# 2
# ```
# """
# function take_a_walk(graph, path::Vector{Mycelia.OrientedKmer}, outgoing_edge_probabilities, incoming_edge_probabilities)
#     done = false
#     while !done
#         maximum_path_likelihood = 0.0
#         maximum_likelihood_path = Vector{Mycelia.OrientedKmer}()
#         for neighbor in Graphs.neighbors(graph.graph, last(path).index)
#             this_path = [last(path).index, neighbor]
#             this_oriented_path, this_path_likelihood =
#                 Mycelia.assess_path(this_path,
#                     graph.kmers,
#                     graph.counts,
#                     last(path).orientation,
#                     outgoing_edge_probabilities,
#                     incoming_edge_probabilities)
#             if this_path_likelihood > maximum_path_likelihood
#                 maximum_path_likelihood = this_path_likelihood
#                 maximum_likelihood_path = this_oriented_path
#             end
#         end
#         if isempty(maximum_likelihood_path) && (maximum_path_likelihood == 0.0)
#             done = true
#         else
#             append!(path, maximum_likelihood_path[2:end])
#         end
#     end
#     return path
# end

# use default cost 1 for breadth first seach
# use default cost 0 for depth first search
function dijkstra_step!(graph, distances, arrival_paths, queue; default_cost = 1)
    current_kmer, cost = DataStructures.dequeue_pair!(queue)
    current_orientation = BioSequences.iscanonical(current_kmer)
    current_canonical_kmer = BioSequences.canonical(current_kmer)
    current_index = graph[current_canonical_kmer, :kmer]

    present_neighbors = Vector{typeof(current_kmer)}()
    for neighbor in BioSequences.neighbors(current_kmer)
        try
            graph[BioSequences.canonical(neighbor), :kmer]
            push!(present_neighbors, neighbor)
        catch
            continue
        end
    end

    true_neighbors =
        filter(neighbor ->
            Graphs.has_edge(
                graph,
                graph[BioSequences.canonical(current_kmer), :kmer],
                graph[BioSequences.canonical(neighbor), :kmer]),
            present_neighbors)

    if !isempty(true_neighbors)
        canonical_true_neighbors = BioSequences.canonical.(true_neighbors)
        true_neighbor_is_canonical = BioSequences.iscanonical.(true_neighbors)
        neighbor_indices = map(canonical_neighbor -> graph[canonical_neighbor, :kmer], canonical_true_neighbors)
        neighbor_weights = map(neighbor_i -> MetaGraphs.get_prop(graph, current_index, neighbor_i, :weight), neighbor_indices)

        # inverse probability!!
        neighbor_costs = 1 .- (neighbor_weights ./ sum(neighbor_weights))
        neighbor_costs .+= default_cost
        # afterwards, 100% likelihood edges cost 1
        # 50% likelihood costs 1.5
        # 0% likelihood is impossible and not included

        for (neighbor, cost) in zip(true_neighbors, neighbor_costs)
            candidate_path = vcat(arrival_paths[current_kmer], current_kmer)
            candidate_path_cost = distances[current_kmer] + cost
            if distances[neighbor] > candidate_path_cost
                arrival_paths[neighbor] = candidate_path
                DataStructures.enqueue!(queue, neighbor => cost)
                distances[neighbor] = candidate_path_cost
            end
        end
    end
    return distances, arrival_paths, queue
end

function evaluate_hits(hits, forward_distances, reverse_distances, forward_arrival_paths, reverse_arrival_paths)

    lowest_cost = Inf
    optimal_path = Int[]

    for hit in hits
        reverse_complement_hit = BioSequences.reverse_complement(hit)
        total_cost = forward_distances[hit] + reverse_distances[reverse_complement_hit]
        if total_cost < lowest_cost
            forward_path = vcat(forward_arrival_paths[hit], hit)
            reverse_path = vcat(reverse_arrival_paths[reverse_complement_hit], reverse_complement_hit)
            reverse_path = reverse!(BioSequences.reverse_complement.(reverse_path))
            full_path = vcat(forward_path, reverse_path[2:end])

            lowest_cost = total_cost
            optimal_path = full_path
        end
    end

    return lowest_cost, optimal_path
end

function bidirectional_dijkstra(graph, a::T, b::T) where T <: BioSequences.AbstractMer
    forward_distances = DataStructures.DefaultDict{T, Float64}(Inf)
    forward_distances[a] = 0

    forward_arrival_paths = Dict{T, Vector{T}}()
    forward_arrival_paths[a] = Int[]

    forward_queue = DataStructures.PriorityQueue{T, Float64}()
    DataStructures.enqueue!(forward_queue, a, 0)

    reverse_b = BioSequences.reverse_complement(b)
    reverse_distances = DataStructures.DefaultDict{T, Float64}(Inf)
    reverse_distances[reverse_b] = 0

    reverse_arrival_paths = Dict{T, Vector{T}}()
    reverse_arrival_paths[reverse_b] = Int[]

    reverse_queue = DataStructures.PriorityQueue{T, Float64}()
    DataStructures.enqueue!(reverse_queue, reverse_b, 0)

    hits = Set{T}()

    while isempty(hits)
        dijkstra_step!(graph, forward_distances, forward_arrival_paths, forward_queue)
        dijkstra_step!(graph, reverse_distances, reverse_arrival_paths, reverse_queue)
        hits = intersect(
                    keys(forward_arrival_paths),
                    BioSequences.reverse_complement.(keys(reverse_arrival_paths)))
    end
    lowest_cost, optimal_path = evaluate_hits(hits, forward_distances, reverse_distances, forward_arrival_paths, reverse_arrival_paths)
    return lowest_cost, optimal_path
end

# # simple point to point
# function dijkstra(graph, a::T, b::T) where {T <: BioSequences.AbstractMer}
#     distances = DataStructures.DefaultDict{T, Float64}(Inf)
#     distances[a] = 0

#     arrival_paths = Dict{T, Vector{T}}()
#     arrival_paths[a] = Int[]

#     queue = DataStructures.PriorityQueue{T, Float64}()
#     DataStructures.enqueue!(queue, a, 0)

#     current_kmer, cost = a, Inf
#     while current_kmer != b
#         distances, arrival_paths, queue = dijkstra_step!(graph, distances, arrival_paths, queue)
#     end
#     shortest_path = vcat(arrival_paths[b], b)
#     return shortest_path, distances[b]
# end

function dijkstra(graph, a::T, targets::Set{T}; search_strategy::Union{Symbol, Missing}=missing) where {T <: BioSequences.AbstractMer}
    if ismissing(search_strategy)
        # local breadth first search to find the single target
        # a-b shortest path
        if length(targets) == 1
            search_strategy = :BFS
        # a - no targets - go find the ends
        # a - series of any target
        else
            search_strategy = :DFS
        end
    end
    if search_strategy == :BFS
        default_cost = 1
    elseif search_strategy == :DFS
        default_cost = 0
    else
        error("please specify either :BFS (breadth-first search) or :DFS (depth-first search)")
    end

    distances = DataStructures.DefaultDict{T, Float64}(Inf)
    distances[a] = 0

    arrival_paths = Dict{T, Vector{T}}()
    arrival_paths[a] = Int[]

    queue = DataStructures.PriorityQueue{T, Float64}()
    DataStructures.enqueue!(queue, a, 0)

    while !isempty(queue) && !(first(DataStructures.peek(queue)) in targets)
        distances, arrival_paths, queue =
            Mycelia.dijkstra_step!(graph, distances, arrival_paths, queue, default_cost = default_cost)
    end
    if !isempty(queue)
        discovered_destination = DataStructures.dequeue!(queue)
        @assert discovered_destination in targets
        shortest_path = vcat(arrival_paths[discovered_destination], discovered_destination)
        return shortest_path, distances[discovered_destination]
    else
        longest_path = Int[]
        distance = Inf
        for (destination, arrival_path) in arrival_paths
            this_distance = distances[destination]
            this_path = vcat(arrival_path, destination)
            if (length(this_path) > length(longest_path)) && (this_distance <= distance)
                longest_path = this_path
                distance = this_distance
            end

        end
        return longest_path, distance
    end
end

function update_remaining_targets(current_walk::AbstractVector{T}, remaining_targets::AbstractSet{T}) where T <: BioSequences.AbstractMer
    # assess whether targets have been hit in the canonical space
    remaining_targets = setdiff(BioSequences.canonical.(remaining_targets), BioSequences.canonical.(current_walk))
    # blow back out into forward and reverse_complement space
    remaining_targets = Set{T}(vcat(remaining_targets, BioSequences.reverse_complement.(remaining_targets)))
    return remaining_targets
end

function assess_downstream_weight(graph, kmer)
    # here we look to see if walking forward or backward from the initial node gets us to heavier weight options
    score = 0
    for neighbor in BioSequences.neighbors(kmer)
        try
            score += MetaGraphs.get_prop(graph, graph[BioSequences.canonical(neighbor), :kmer], :count)
        catch
            continue
        end
    end
    return score
end

# vertices should either be entire graph (by default) or a connected component
# if people want to work on just the connected component, let them induce a subgraph
function find_graph_core(graph; seed=rand(Int))

    Random.seed!(seed)

    T = typeof(MetaGraphs.get_prop(graph, 1, :kmer))

    # targets = [MetaGraphs.get_prop(graph, v, :kmer) for v in Graphs.vertices(graph)]
    # sortperm

    # targets = [
    #         MetaGraphs.get_prop(graph, v_index, :count) for v_index in
    #         sortperm([MetaGraphs.get_prop(graph, v, :count) for v in Graphs.vertices(graph)], rev=true)[1:Int(ceil(Graphs.nv(graph)/10))]]

    targets = [MetaGraphs.get_prop(graph, v, :kmer) for v in Graphs.vertices(graph) if MetaGraphs.get_prop(graph, v, :count) > 1]
            # sortperm([MetaGraphs.get_prop(graph, v, :count) for v in Graphs.vertices(graph)], rev=true)[1:Int(ceil(Graphs.nv(graph)/10))]]

    @show length(targets)
    starting_kmer = first(targets)
    max_degree = 0
    for node in targets
        node_degree = Graphs.degree(graph, graph[node, :kmer])
        if node_degree > max_degree
            max_degree = node_degree
            starting_kmer = node
        end
    end

    current_walk = [starting_kmer]
    prior_walk_length = length(current_walk)
    remaining_targets = update_remaining_targets(current_walk, Set(targets))
    done = isempty(remaining_targets)

    while !done
        # here we look to see if walking forward or backward from the current ends gets us to heavier weight options
        # we want to prioritize walks toward higher coverage nodes
        forward_score = assess_downstream_weight(graph, last(current_walk))
        reverse_score = assess_downstream_weight(graph, BioSequences.reverse_complement(first(current_walk)))
        if reverse_score > forward_score
            current_walk = reverse(BioSequences.reverse_complement.(current_walk))
        end

        forward_source = last(current_walk)
        forward_walk, forward_distance = Mycelia.dijkstra(graph, forward_source, remaining_targets, search_strategy=:DFS)
        current_walk = vcat(current_walk, forward_walk[2:end])
        remaining_targets = update_remaining_targets(current_walk, remaining_targets)
        if isempty(remaining_targets)
            done = true
        else
            reverse_source = BioSequences.reverse_complement(first(current_walk))
            reverse_walk, reverse_distance = Mycelia.dijkstra(graph, reverse_source, remaining_targets, search_strategy=:DFS)
            current_walk = vcat(reverse(BioSequences.reverse_complement.(reverse_walk))[1:end-1], current_walk)
            remaining_targets = update_remaining_targets(current_walk, remaining_targets)
        end
        @show length(current_walk)
        failed_this_expansion = length(current_walk) == prior_walk_length
        prior_walk_length = length(current_walk)
        if isempty(remaining_targets)
            done = true
        elseif failed_this_expansion
            done = true
        end
    end
    return current_walk
end


abstract type DataSet{DS} end

getproperty(ds::DS, k::Symbol) where {DS<:DataSet{<:DataSet}} =
    hasfield(DS, k) ? getfield(ds, k) : getproperty(getfield(ds, :_super), k)
setproperty!(ds::DS, k::Symbol, v) where {DS<:DataSet{<:DataSet}} =
    hasfield(DS, k) ? setfield!(ds, k, v) : setproperty!(getfield(ds, :_super), k, v)
propertynames(ds::DS) where {DS′<:DataSet, DS<:DataSet{DS′}} =
    union(fieldnames(DS), fieldnames(DS′))

function new_dataset(::Type{DS}; kwargs...) where {DS′<:DataSet, DS<:DataSet{DS′}}
    kw  = filter(((k,_),)->  k in fieldnames(DS),  kwargs)
    kw′ = filter(((k,_),)->!(k in fieldnames(DS)), kwargs)
    DS(_super=DS′(;kw′...); kw...)
end

copy(ds::DS) where {DS<:DataSet} =
    DS(((k==:_super ? copy(v) : v) for (k,v) in pairs(fields(ds)))...)

# needed until fix to https://github.com/FluxML/Zygote.jl/issues/685
Zygote.grad_mut(ds::DataSet) = Ref{Any}((;(propertynames(ds) .=> nothing)...))

# Stores variables needed to construct the posterior
@kwdef mutable struct BaseDataSet <: DataSet{Nothing}
    d                # data
    Cϕ               # ϕ covariance
    Cf               # unlensed field covariance
    Cf̃ = nothing     # lensed field covariance (not always needed)
    Cn               # noise covariance
    Cn̂ = Cn          # approximate noise covariance, diagonal in same basis as Cf
    M  = 1           # user mask
    M̂  = M           # approximate user mask, diagonal in same basis as Cf
    B  = 1           # beam and instrumental transfer functions
    B̂  = B           # approximate beam and instrumental transfer functions, diagonal in same basis as Cf
    D  = 1           # mixing matrix for mixed parametrization
    G  = 1           # reparametrization for ϕ
    P  = 1           # pixelization operator (if estimating field on higher res than data)
    L  = LenseFlow   # lensing operator, possibly cached for memory reuse
end

function subblock(ds::DS, block) where {DS<:DataSet}
    DS(map(collect(pairs(fields(ds)))) do (k,v)
        @match (k,v) begin
            ((:Cϕ || :G || :L), v)              => v
            (_, L::Union{Nothing,FuncOp,Real})  => L
            (_, L)                              => getindex(L,block)
        end
    end...)
end

function (ds::DataSet)(θ::NamedTuple)
    DS = typeof(ds)
    DS(map(fieldvalues(ds)) do v
        (v isa Union{ParamDependentOp,DataSet}) ? v(θ) : v
    end...)
end
(ds::DataSet)(;θ...) = ds((;θ...))

adapt_structure(to, ds::DS) where {DS <: DataSet} = DS(adapt(to, fieldvalues(ds))...)

@doc doc"""
    resimulate(ds::DataSet; [f, ϕ, n])

Make a new DataSet with the data replaced by a simulation, potentially given a
fixed f, ϕ, or n, if any are provided.

Returns a named tuple of `(ds, f, ϕ, n, f̃)`
"""
resimulate(ds::DataSet; kwargs...) = resimulate!(copy(ds); kwargs...)

@doc doc"""
    resimulate!(ds::DataSet; [f, ϕ, n])

Replace the data in this DataSet in-place with a simulation, potentially given a
fixed f, ϕ, or n, if any are provided.

Returns a named tuple of `(ds, f, ϕ, n, f̃)`
"""
function resimulate!(
    ds::DataSet;
    f=nothing, ϕ=nothing, n=nothing, f̃=nothing,
    rng=global_rng_for(ds.d), seed=nothing
)

    @unpack M,P,B,L,Cϕ,Cf,Cn,d = ds()
    D = batchsize(d)

    if (f̃ == nothing)
        if (ϕ == nothing)
            ϕ = simulate(batch(Cϕ,D), rng=rng, seed=seed)
        end
        if (f == nothing)
            f = simulate(batch(Cf,D), rng=rng, seed=(seed==nothing ? nothing : seed+1))
        end
        f̃ = L(ϕ)*f
    else
        f = ϕ = nothing
    end
    if (n == nothing)
        n = simulate(batch(Cn,D), rng=rng, seed=(seed==nothing ? nothing : seed+2))
    end

    ds.d = d = M*P*B*f̃ + n

    @namedtuple(ds,f,ϕ,n,f̃,d)

end

@doc doc"""
    load_sim

Create a `BaseDataSet` object with some simulated data, returing the DataSet
and simulated truths. E.g.

```julia
@unpack f,ϕ,ds = load_sim(;
    θpix  = 2,
    Nside = 128,
    pol   = :I,
    T     = Float32
);
```
For rectangular maps, Nside expects (Ny, Nx), i.e. (Nrow, Ncol).
If Nside isa Int, the code interprets it as a square map.
"""
function load_sim(;

    # basic configuration
    θpix,
    θpix_data = θpix,
    Nside,
    pol,
    T = Float32,
    storage = Array,
    Nbatch = nothing,

    # noise parameters, or set Cℓn or even Cn directly
    μKarcminT = 3,
    ℓknee = 100,
    αknee = 3,
    Cℓn = nothing,
    Cn = nothing,

    # beam parameters, or set B directly
    beamFWHM = 0,
    B = nothing, B̂ = nothing,

    # mask parameters, or set M directly
    pixel_mask_kwargs = nothing,
    bandpass_mask = LowPass(3000),
    M = nothing, M̂ = nothing,

    # theory
    Cℓ = nothing,
    fiducial_θ = NamedTuple(),
    rfid = nothing,

    rng = global_rng_for(storage),
    seed = nothing,
    D = nothing,
    G = nothing,
    Nϕ_fac = 2,
    L = LenseFlow,
    ∂mode = fourier∂

)

    # the biggest ℓ on the 2D fourier grid
    ℓmax = round(Int,ceil(√2*fieldinfo(Flat(θpix=θpix,Nside=Nside)).nyq)+1)

    # CMB Cℓs
    if (rfid != nothing)
        @warn "`rfid` will be removed in a future version. Use `fiducial_θ=(r=...,)` instead."
        fiducial_θ = merge(fiducial_θ,(r=rfid,))
    end
    Aϕ₀ = get(fiducial_θ, :Aϕ, 1)
    fiducial_θ = Base.structdiff(fiducial_θ, NamedTuple{(:Aϕ,)}) # remove Aϕ key if present
    if (Cℓ == nothing)
        Cℓ = camb(;fiducial_θ..., ℓmax=ℓmax)
    else
        if !isempty(fiducial_θ)
            error("Can't pass both `Cℓ` and `fiducial_θ` parameters which affect `Cℓ`, choose one or the other.")
        elseif maximum(Cℓ.total.TT.ℓ) < ℓmax
            error("ℓmax of `Cℓ` argument should be higher than $ℓmax for this configuration.")
        end
    end
    r₀ = Cℓ.params.r

    # noise Cℓs (these are non-debeamed, hence beamFWHM=0 below; the beam comes in via the B operator)
    if (Cℓn == nothing)
        Cℓn = noiseCℓs(μKarcminT=μKarcminT, beamFWHM=0, ℓknee=ℓknee, αknee=αknee, ℓmax=ℓmax)
    end

    # some things which depend on whether we chose :I, :P, or :IP
    pol = Symbol(pol)
    S,ks,F,F̂,nF = @match pol begin
        :I  => (S0,  (:TT,),            FlatMap,    FlatFourier,    1)
        :P  => (S2,  (:EE,:BB),         FlatQUMap,  FlatEBFourier,  2)
        :IP => (S02, (:TT,:EE,:BB,:TE), FlatIQUMap, FlatIEBFourier, 3)
        _   => throw(ArgumentError("`pol` should be one of :I, :P, or :IP"))
    end

    # pixelization
    Pix = Flat(Nside=Nside, θpix=θpix, ∂mode=∂mode)
    if (θpix_data == θpix)
        Pix_data = Pix
        P = 1
    else
        Pix_data = Flat(Nside=Nside.÷(θpix_data÷θpix), θpix=θpix_data, ∂mode=∂mode)
        P = FuncOp(
            op  = f -> ud_grade(f, θpix_data, deconv_pixwin=false, anti_aliasing=false),
            opᴴ = f -> ud_grade(f, θpix,      deconv_pixwin=false, anti_aliasing=false)
        )
    end

    # covariances
    Cϕ₀ = adapt(storage, Cℓ_to_Cov(Pix,      T, S0, (Cℓ.total.ϕϕ)))
    Cfs = adapt(storage, Cℓ_to_Cov(Pix,      T, S,  (Cℓ.unlensed_scalar[k] for k in ks)...))
    Cft = adapt(storage, Cℓ_to_Cov(Pix,      T, S,  (Cℓ.tensor[k]          for k in ks)...))
    Cf̃  = adapt(storage, Cℓ_to_Cov(Pix,      T, S,  (Cℓ.total[k]           for k in ks)...))
    Cn̂  = adapt(storage, Cℓ_to_Cov(Pix_data, T, S,  (Cℓn[k]                for k in ks)...))
    if (Cn == nothing); Cn = Cn̂; end
    Cf = ParamDependentOp((;r=r₀,   _...)->(Cfs + T(r/r₀)*Cft))
    Cϕ = ParamDependentOp((;Aϕ=Aϕ₀, _...)->(T(Aϕ) * Cϕ₀))

    # data mask
    if (M == nothing)
        Mfourier = adapt(storage, Cℓ_to_Cov(Pix_data, T, S, ((k==:TE ? 0 : 1) * bandpass_mask.diag.Wℓ for k in ks)...; units=1))
        if (pixel_mask_kwargs != nothing)
            Mpix = adapt(storage, Diagonal(F{Pix_data}(repeated(T.(make_mask(Nside.÷(θpix_data÷θpix),θpix_data; pixel_mask_kwargs...).Ix),nF)...)))
        else
            Mpix = 1
        end
        M = Mfourier * Mpix
        if (M̂ == nothing)
            M̂ = Mfourier
        end
    else
        if (M̂ == nothing)
            M̂ = M
        end
    end
    if (M̂ isa DiagOp{<:BandPass})
        M̂ = Diagonal(M̂ * one(diag(Cf)))
    end

    # beam
    if (B == nothing)
        B = adapt(storage, Cℓ_to_Cov(Pix, T, S, ((k==:TE ? 0 : 1) * sqrt(beamCℓs(beamFWHM=beamFWHM)) for k=ks)..., units=1))
    end
    if (B̂ == nothing)
        B̂ = B
    end

    # creating lensing operator cache
    Lϕ = alloc_cache(L(diag(Cϕ)),diag(Cf))

    # put everything in DataSet
    ds = BaseDataSet(;@namedtuple(d=nothing, Cn, Cn̂, Cf, Cf̃, Cϕ, M, M̂, B, B̂, D, P, L=Lϕ)...)

    # simulate data
    @unpack ds,f,f̃,ϕ,n = resimulate(ds, rng=rng, seed=seed)

    # with the DataSet created, we now more conveniently create the mixing matrices D and G
    if (G == nothing)
        Nϕ = quadratic_estimate(ds,(pol in (:P,:IP) ? :EB : :TT)).Nϕ / Nϕ_fac
        G₀ = @. nan2zero(sqrt(1 + 2/($Cϕ()/Nϕ)))
        ds.G = ParamDependentOp((;Aϕ=Aϕ₀, _...)->(pinv(G₀) * sqrt(I + 2 * Nϕ * pinv(Cϕ(Aϕ=Aϕ)))))
    end
    if (D == nothing)
        σ²len = T(deg2rad(5/60)^2)
        ds.D = ParamDependentOp(
            function (;r=r₀, _...)
                Cfr = Cf(;r=r)
                sqrt(Diagonal(diag(Cfr) .+ σ²len .+ 2*diag(Cn̂)) * pinv(Cfr))
            end,
        )
    end

    if Nbatch != nothing
        ds.d = identity.(batch(ds.d, Nbatch))
        ds.L = alloc_cache(L(identity.(batch(zero(diag(Cϕ)), Nbatch))), ds.d)
    end

    return adapt(storage, @namedtuple(f, f̃, ϕ, n, ds, ds₀=ds(), T, P=Pix, Cℓ, L))

end

@deprecate load_sim_dataset(args...; kwargs...) load_sim(args...; kwargs...)


function checkrepeat(OptCpnt)
    Opt = Dict(
    "V"=>["Vx","Vz"],
    "Tii"=>["Txx","Tzz"])

    for Cpnt in ["V","Tii"]
        if Cpnt in OptCpnt
            filter!(x->!(x in Opt[Cpnt]),OptCpnt)
        end
    end
    return OptCpnt
end

#=== elastic ===#
#=== write Recordings into bin file ===#
function wDB(
    fid::IOStream,
    wf::elwf2d,
    BDnloc::Array{Int64,2},
    nwf::nelwf2d,
    OptCpnt::Array{String})

    RecLocLib = Dict(
    "P"=>(BDnloc[2,:]-1).*nwf.BDntpp[1] + BDnloc[1,:],
    "Vx"=>(BDnloc[2,:]-1).*nwf.BDnvx[1] + BDnloc[1,:],
    "Vz"=>(BDnloc[2,:]-1).*nwf.BDnvz[1] + BDnloc[1,:],
    "Txx"=>(BDnloc[2,:]-1).*nwf.BDntpp[1] + BDnloc[1,:],
    "Tzz"=>(BDnloc[2,:]-1).*nwf.BDntpp[1] + BDnloc[1,:],
    "Txz"=>(BDnloc[2,:]-1).*nwf.BDntxz[1] + BDnloc[1,:])
    #"Wy"=>(BDnloc[2,:]-1).*nwf.N_BDWy[1] + BDnloc[1,:])

    RecLib = Dict(
    "P"=>vec((wf.txx+wf.tzz)/2)[RecLocLib["P"]],
    "Vx"=>vec(wf.vx)[RecLocLib["Vx"]],
    "Vz"=>vec(wf.vz)[RecLocLib["Vz"]],
    "Txx"=>vec(wf.txx)[RecLocLib["Txx"]],
    "Txz"=>vec(wf.txz)[RecLocLib["Txz"]],
    "Tzz"=>vec(wf.tzz)[RecLocLib["Tzz"]],
    "V"=>[vec(wf.vx)[RecLocLib["Vx"]];
    vec(wf.vz)[RecLocLib["Vz"]]],
    "Tii"=>[vec(wf.txx)[RecLocLib["Txx"]];
    vec(wf.tzz)[RecLocLib["Tzz"]]])
    #"Wy"=>wf.VecBDWy)

    for OptC in OptCpnt
        write(fid,RecLib[OptC])
    end
end

#=== write Wavefields into bin file ===#
function wDB(
    fid::IOStream,
    wf::elwf2d,
    OptCpnt::Array{String},
    it::Int64,
    Slices::Union{UnitRange{Int64},StepRange{Int64,Int64},Array{Int64,1},Int64})

    WFLib = Dict(
    "P"=>vec((wf.txx+wf.tzz)/2),
    "Vx"=>vec(wf.vx),
    "Vz"=>vec(wf.vz),
    "Txx"=>vec(wf.txx),
    "Tzz"=>vec(wf.tzz),
    "Txz"=>vec(wf.txz),
    "V"=>[vec(wf.vx);vec(wf.vz)],
    "Tii"=>[vec(wf.txx);vec(wf.tzz)])
    #"Wy"=>wf.VecBDWy)
    if it in Slices
        for OptC in OptCpnt
            write(fid,WFLib[OptC])
        end
    end
end

#=== write both Recordings and Wavefields into bin file ===#
function wDB(
    fidRec::IOStream,
    fidWF::IOStream,
    wf::elwf2d,
    BDnloc::Array{Int64,2},
    nwf::nelwf2d,
    OptCpnt::Array{String},
    it::Int64,
    Slices::Union{UnitRange{Int64},StepRange{Int64,Int64},Array{Int64,1},Int64})

    RecLocLib = Dict(
    "P"=>(BDnloc[2,:]-1).*nwf.BDntpp[1] + BDnloc[1,:],
    "Vx"=>(BDnloc[2,:]-1).*nwf.BDnvx[1] + BDnloc[1,:],
    "Vz"=>(BDnloc[2,:]-1).*nwf.BDnvz[1] + BDnloc[1,:],
    "Txx"=>(BDnloc[2,:]-1).*nwf.BDntpp[1] + BDnloc[1,:],
    "Tzz"=>(BDnloc[2,:]-1).*nwf.BDntpp[1] + BDnloc[1,:],
    "Txz"=>(BDnloc[2,:]-1).*nwf.BDntxz[1] + BDnloc[1,:])
    #"Wy"=>(BDnloc[2,:]-1).*nwf.N_BDWy[1] + BDnloc[1,:])

    RecLib = Dict(
    "P"=>vec((wf.txx+wf.tzz)/2)[RecLocLib["P"]],
    "Vx"=>vec(wf.vx)[RecLocLib["Vx"]],
    "Vz"=>vec(wf.vz)[RecLocLib["Vz"]],
    "Txx"=>vec(wf.txx)[RecLocLib["Txx"]],
    "Txz"=>vec(wf.txz)[RecLocLib["Txz"]],
    "Tzz"=>vec(wf.tzz)[RecLocLib["Tzz"]],
    "V"=>[vec(wf.vx)[RecLocLib["Vx"]];
    vec(wf.vz)[RecLocLib["Vz"]]],
    "Tii"=>[vec(wf.txx)[RecLocLib["Txx"]];
    vec(wf.tzz)[RecLocLib["Tzz"]]])
    #"Wy"=>wf.VecBDWy)

    WFLib = Dict(
    "P"=>vec((wf.txx+wf.tzz)/2),
    "Vx"=>vec(wf.vx),
    "Vz"=>vec(wf.vz),
    "Txx"=>vec(wf.txx),
    "Tzz"=>vec(wf.tzz),
    "Txz"=>vec(wf.txz),
    "V"=>[vec(vx);vec(wf.vz)],
    "Tii"=>[vec(txx);vec(tzz)])
    #"Wy"=>wf.VecBDWy)

    for OptC in OptCpnt
        write(fidRec,RecLib[OptC])
        if it in Slices
            write(fid,WFLib[OptC])
        end
    end

end

#=== acoustic ===#
#=== write Recordings into bin file ===#
function wDB(
    fid::IOStream,
    wf::acwf2d,
    BDnloc::Array{Int64,2},
    nwf::nacwf2d,
    OptCpnt::Array{String})

    RecLocLib = Dict(
    "P"=>(BDnloc[2,:]-1).*nwf.BDntpp[1] + BDnloc[1,:],
    "Vx"=>(BDnloc[2,:]-1).*nwf.BDnvx[1] + BDnloc[1,:],
    "Vz"=>(BDnloc[2,:]-1).*nwf.BDnvz[1] + BDnloc[1,:],
    "Txx"=>(BDnloc[2,:]-1).*nwf.BDntpp[1] + BDnloc[1,:],
    "Tzz"=>(BDnloc[2,:]-1).*nwf.BDntpp[1] + BDnloc[1,:])
    #"Wy"=>(BDnloc[2,:]-1).*nwf.N_BDWy[1] + BDnloc[1,:])

    RecLib = Dict(
    "P"=>vec((wf.txx+wf.tzz)/2)[RecLocLib["P"]],
    "Vx"=>vec(wf.vx)[RecLocLib["Vx"]],
    "Vz"=>vec(wf.vz)[RecLocLib["Vz"]],
    "Txx"=>vec(wf.txx)[RecLocLib["Txx"]],
    "Tzz"=>vec(wf.tzz)[RecLocLib["Tzz"]],
    "V"=>[vec(wf.vx)[RecLocLib["Vx"]];
    vec(wf.vz)[RecLocLib["Vz"]]],
    "Tii"=>[vec(wf.txx)[RecLocLib["Txx"]];
    vec(wf.tzz)[RecLocLib["Tzz"]]])
    #"Wy"=>wf.VecBDWy)

    for OptC in OptCpnt
        write(fid,RecLib[OptC])
    end
end

#=== write Wavefields into bin file ===#
function wDB(
    fid::IOStream,
    wf::acwf2d,
    OptCpnt::Array{String},
    it::Int64,
    Slices::Union{UnitRange{Int64},StepRange{Int64,Int64},Array{Int64,1},Int64})

    WFLib = Dict(
    "P"=>vec((wf.txx+wf.tzz)/2),
    "Vx"=>vec(wf.vx),
    "Vz"=>vec(wf.vz),
    "Txx"=>vec(wf.txx),
    "Tzz"=>vec(wf.tzz),
    "V"=>[vec(wf.vx);vec(wf.vz)],
    "Tii"=>[vec(wf.txx);vec(wf.tzz)])
    #"Wy"=>wf.VecBDWy)
    if it in Slices
        for OptC in OptCpnt
            write(fid,WFLib[OptC])
        end
    end
end

#=== write both Recordings and Wavefields into bin file ===#
function wDB(
    fidRec::IOStream,
    fidWF::IOStream,
    wf::acwf2d,
    BDnloc::Array{Int64,2},
    nwf::nacwf2d,
    OptCpnt::Array{String},
    it::Int64,
    Slices::Union{UnitRange{Int64},StepRange{Int64,Int64},Array{Int64,1},Int64})

    RecLocLib = Dict(
    "P"=>(BDnloc[2,:]-1).*nwf.BDntpp[1] + BDnloc[1,:],
    "Vx"=>(BDnloc[2,:]-1).*nwf.BDnvx[1] + BDnloc[1,:],
    "Vz"=>(BDnloc[2,:]-1).*nwf.BDnvz[1] + BDnloc[1,:],
    "Txx"=>(BDnloc[2,:]-1).*nwf.BDntpp[1] + BDnloc[1,:],
    "Tzz"=>(BDnloc[2,:]-1).*nwf.BDntpp[1] + BDnloc[1,:])
    #"Wy"=>(BDnloc[2,:]-1).*nwf.N_BDWy[1] + BDnloc[1,:])

    RecLib = Dict(
    "P"=>vec((wf.txx+wf.tzz)/2)[RecLocLib["P"]],
    "Vx"=>vec(wf.vx)[RecLocLib["Vx"]],
    "Vz"=>vec(wf.vz)[RecLocLib["Vz"]],
    "Txx"=>vec(wf.txx)[RecLocLib["Txx"]],
    "Tzz"=>vec(wf.tzz)[RecLocLib["Tzz"]],
    "V"=>[vec(wf.vx)[RecLocLib["Vx"]];
    vec(wf.vz)[RecLocLib["Vz"]]],
    "Tii"=>[vec(wf.txx)[RecLocLib["Txx"]];
    vec(wf.tzz)[RecLocLib["Tzz"]]])
    #"Wy"=>wf.VecBDWy)

    WFLib = Dict(
    "P"=>vec((wf.txx+wf.tzz)/2),
    "Vx"=>vec(wf.vx),
    "Vz"=>vec(wf.vz),
    "Txx"=>vec(wf.txx),
    "Tzz"=>vec(wf.tzz),
    "V"=>[vec(vx);vec(wf.vz)],
    "Tii"=>[vec(txx);vec(tzz)])
    #"Wy"=>wf.VecBDWy)

    for OptC in OptCpnt
        write(fidRec,RecLib[OptC])
        if it in Slices
            write(fid,WFLib[OptC])
        end
    end

end