abstract type NoiseModelFlag end
struct ErdosRenyiNoise  <: NoiseModelFlag end
struct DuplicationNoise <: NoiseModelFlag end

#=------------------------------------------------------------------------------
                        Local File Experiments Routines
------------------------------------------------------------------------------=#

#TODO: update to use kwargs properly
function self_alignment(ssten_files::Array{String,1};method="LambdaTAME")

    Best_alignment_ratio = Array{Float64}(undef,length(ssten_files))

    for f in ssten_files
        A = load(f;type="COOTen")

        #permute tensor to align against
        p = sort(1:A.cubical_dimension,by=i->rand())
        A_permuted = COOTen(A.indices,A.vals,A.cubical_dimension)
        permute_tensor!(A_permuted,p)

        best_score, _, _ = align_tensors(A,A_permuted;method=method)

        println("$f aligned with $best_score")
    end

end

function apply_Metis_permutation!(A::SparseMatrixCSC,k::Int=100)

	n = size(A,1)
	metis_partition = Metis.Metis.partition(A, 100)
	p = sort(1:n, by= i-> metis_partition[i])

	permute!(A,p,p)
end

#TODO: put in kwargs
function pairwise_alignment(dir)

    #align all .ssten files
    ssten_files = sort([f for f in readdir(dir) if occursin(".ssten",f)])

    Best_alignment_ratio = Array{Float64}(undef,length(ssten_files),length(ssten_files))

    for i in 1:length(ssten_files)
        for j in i+1:length(ssten_files)
            Best_alignment_ratio[i,j] = align_from_files(dir*ssten_files[i],dir*ssten_files[j])
            Best_alignment_ratio[i,j] = Best_alignment_ratio[j,i]
        end
    end

    return ssten_files, Best_alignment_ratio
end

#=
function distributed_pairwise_alignment(dir::String;kwargs...)

    #align all .ssten files
    ssten_files = sort([f for f in readdir(dir) if occursin(".ssten",f)])
    distributed_pairwise_alignment(ssten_files,dir;kwargs...)

end
=#

"""------------------------------------------------------------------------------
  This function runs alignemnt experiments in parallel using the @spawn macro of
  Distributed.jl. This function should be run in the src file, as the @everywhere
  macro will only work properly there. Any additional kwargs are passed to
  'align_matrices' or 'align_tensors' depending on the file extensions.

  Inputs
  ------
  * files - (Array{String,1}):
    List of the file names. Files may be either all SMAT or all STEN files.
    'align_tensors' or 'align_matrices' will be called depending on the extension.
  * dirpath - (String):
    The path to where the files exist.
  * method - (String):
    What type of alignment method to run, options are 'LambdaTAME', 'TAME',
    'LowRankTAME', 'EigenAlign', 'LowRankEigenAlign', 'Degree', or 'Random'.
  * profile - (Bool):
    Whether or not to profile the algorithm. Setting this flag to true will
    record different things depending on the algorithm, but will always return
    runtime information.
Output
------
  * 'exp_results' - (type omitted):
    The final array of results for the series of experiments, the size of each
    entry will differ depending on whether or not profile was True. At the very
    least, each entry will contain the the pair of file names aligned, the
    number of triangles matched in the alignemnt, and the maximum number of
    triangles possible to align (the minimum number of triangles in either
    network).

------------------------------------------------------------------------------"""
function distributed_pairwise_ssten_alignment(files::Array{String,1},dirpath::String;
                                        method::AlignmentMethod=ΛTAME_M(),profile=false,kwargs...)

    futures = []
	exp_results = []

    for i in 1:length(files)
        for j in i+1:length(files)
            future = @spawn align_tensors(dirpath*files[i],dirpath*files[j];profile=profile,method=method,kwargs...)
            push!(futures,((i,j),future))
        end
    end

    for ((i,j), future) in futures

        if method === ΛTAME_M() || method === LowRankTAME_M() || method === TAME_M()
            alignmentOutput = fetch(future)
            matched_tris = alignmentOutput.matchScore
            best_matching = alignmentOutput.matching
            A_tens, B_tens = alignmentOutput.motifCounts
            if profile
                profiling_results = alignmentOutput.profile
			end
		end

        data_to_save = Array{Any,1}(undef,0)
        push!(data_to_save,files[i])
        push!(data_to_save,files[j])
        push!(data_to_save,matched_tris)
        push!(data_to_save,best_matching)
        push!(data_to_save,A_tens)
        push!(data_to_save,B_tens)

        if profile
            push!(data_to_save,profiling_results)
        end

        push!(exp_results,data_to_save)
    end

    return exp_results
end

function distributed_pairwise_smat_alignment(files::Array{String,1},dirpath::String;
                                             method::AlignmentMethod=ΛTAME_M(),profile=false,kwargs...)

    futures = []
	exp_results = []

    for i in 1:length(files)
        for j in i+1:length(files)
            future = @spawn align_matrices(dirpath*files[i],dirpath*files[j];profile,method,kwargs...)
            push!(futures,((i,j),future))
        end
    end

    for ((i,j), future) in futures

        if method === ΛTAME_M() || method === LowRankTAME_M()
            if !haskey(kwargs,:postProcessing) || kwargs[:postProcessing] === noPostProcessing()
                alignmentOutput = fetch(future)
            else
                alignmentOutput, postProcessingOutput = fetch(future)
            end
            matched_tris = alignmentOutput.matchScore
            max_tris = min(alignmentOutput.motifCounts...)
            best_matching = alignmentOutput.matching
            A_tens, B_tens = alignmentOutput.motifCounts
            if profile
                profiling_results = alignmentOutput.profile
			end
        elseif method === TAME_M()
            alignmentOutput = fetch(future)
            matched_tris = alignmentOutput.matchScore
            max_tris = min(alignmentOutput.motifCounts...)
            best_matching = alignmentOutput.matching
            A_tens, B_tens = alignmentOutput.motifCounts
            if profile
                profiling_results = alignmentOutput.profile
			end
        elseif method === ΛTAME_MultiMotif_M() || method === LowRankTAME_MultiMotif_M() || method === TAME_MultiMotif_M()
            if !haskey(kwargs,:postProcessing) || kwargs[:postProcessing] === noPostProcessing()
                A_motifDistribution, B_motifDistribution,alignmentOutput = fetch(future)
            else
                A_motifDistribution, B_motifDistribution,alignmentOutput, postProcessingOutput = fetch(future)
            end

            if profile
                profiling_results = alignmentOutput.profile
			end
        elseif method === LowRankEigenAlign_M()

            if !haskey(kwargs,:postProcessing) || kwargs[:postProcessing] === noPostProcessing()
                alignmentOutput = fetch(future)
            else
                alignmentOutput,postProcessingOutput = fetch(future)
            end

            matched_tris = alignmentOutput.matchScore
            A_tens,B_tens = alignmentOutput.motifCounts
            max_tris = min(alignmentOutput.motifCounts...)
            best_matching = alignmentOutput.matching

            if profile
                profiling_results = alignmentOutput.profile
            end

        elseif method === EigenAlign_M() || method === Degree_M() || method === Random_M()
            A_tens, B_tens, matched_tris, best_matching, profiling_results = fetch(future)
            max_tris = min(A_tens, B_tens)
            profile = true
		end

        data_to_save = Array{Any,1}(undef,0)
        push!(data_to_save,files[i])
        push!(data_to_save,files[j])

        if method === ΛTAME_MultiMotif_M() || method === LowRankTAME_MultiMotif_M() || method === TAME_MultiMotif_M()
            push!(data_to_save,alignmentOutput.matchScore)
            push!(data_to_save,alignmentOutput.motifCounts[1])
            push!(data_to_save,alignmentOutput.motifCounts[2])
            push!(data_to_save,A_motifDistribution)
            push!(data_to_save,B_motifDistribution)
        else
            push!(data_to_save, matched_tris)
            push!(data_to_save, A_tens)
            push!(data_to_save, B_tens)
        end

        if profile
            push!(data_to_save,profiling_results)
        end

        if haskey(kwargs,:postProcessing)
            if typeof(kwargs[:postProcessing]) <: KlauAlgo
                push!(data_to_save,postProcessingOutput.original_edges_matched)
                push!(data_to_save,postProcessingOutput.klau_edges_matched)
                push!(data_to_save,postProcessingOutput.klau_tris_matched)
                push!(data_to_save,postProcessingOutput.matching)
                if profile
                    push!(data_to_save,postProcessingOutput.setup_rt)
                    push!(data_to_save,postProcessingOutput.klau_rt)
                end
                push!(data_to_save,postProcessingOutput.L_sparsity)
                push!(data_to_save,postProcessingOutput.f_status)
            elseif typeof(kwargs[:postProcessing]) <: SuccessiveKlauAlgo

                push!(data_to_save,postProcessingOutput.original_edges_matched)
                push!(data_to_save,postProcessingOutput.klau_edges_matched)
                push!(data_to_save,postProcessingOutput.klau_tris_matched)
                push!(data_to_save,postProcessingOutput.matching)
                if profile
                    push!(data_to_save,postProcessingOutput.profiling)
                end
            elseif typeof(kwargs[:postProcessing]) <: LocalSearch

                push!(data_to_save,postProcessingOutput.original_edges_matched)
                push!(data_to_save,postProcessingOutput.localsearch_edges_matched)
                push!(data_to_save,postProcessingOutput.localsearch_tris_matched)
                push!(data_to_save,postProcessingOutput.matching)
                if profile
                    push!(data_to_save,postProcessingOutput.profiling)
                    push!(data_to_save,postProcessingOutput.full_runtime)
                end

            end
        end

        push!(exp_results,data_to_save)
    end

    return exp_results
end

function distributed_pairwise_alignment(files::Array{String,1},args...;kwargs...)
     if all( [f[end-5:end] == ".ssten" for f in files]) #tensors
        subkwargs = Dict([(k,v) for (k,v) in kwargs if k != :postProcessing]) #ssten can't support post processing
        return distributed_pairwise_ssten_alignment(files,args...;subkwargs...)
     elseif all( [f[end-4:end] == ".smat" for f in files]) #matrices
         return distributed_pairwise_smat_alignment(files,args...;kwargs...)
     else
         throw(ArgumentError("all files must be the same file type, either all '.ssten' or '.smat'."))
     end
 end

function self_alignment(dir::String;kwargs...)

    #align all .ssten files
    ssten_files = sort([dir*"/"*f for f in readdir(dir) if occursin(".ssten",f)])
    return self_alignment(ssten_files;kwargs...)

end

#=------------------------------------------------------------------------------
                      Generated Graph Experiments Routines
------------------------------------------------------------------------------=#
"""------------------------------------------------------------------------------
  This function runs randomly generated experiments in parallel using the @spawn
  macro of Distributed.jl. This function should be run in the src file, as the
  @everywhere macro will only work properly there. Any additional kwargs are
  passed to 'random_graph_exp'.

  Inputs
  ------
  * 'trial_count' - (Int):
    The number of trials to run for each of the experiments conducted.
  * 'seed_exps' - (Bool):
    Indicates whether or not to seed the experiments. Seeds are generated by
    seeding Random.seed!(0), and then generating an array of UInts using the
    number of 'p_remove' and 'n_sizes' variables, and the 'trial_count'. Changing
    these may lead to different seeds.
  * 'n_sizes' - (Array{Int,1}):
    The different sizes of graphs to generate experiments for.
  * 'p_remove' - (Array{Float,1}):
    The different probability of edge removals to test in the ER noise models.
  * profile - (Bool):
    Whether or not to profile the algorithm. Setting this flag to true will
    record different things depending on the algorithm, but will always return
    runtime information.
Output
------
  * 'exp_results' - (type omitted):
    The final array of results for the series of experiments, the size of each
    entry will differ depending on whether or not profile was True. At the very
    least, the each entry will contain the seed used, the 'p_remove' value, the
    number of vertices in the graph, the ground truth accuracy of the alignment,
    the degree weighted accuracy of the alignment, the number of matched
    triangles, the number of triangles each graph, and the maximum number of
    triangles possible to align. Anything additional returned by the functions
    with the profile flag on, is appended at the end.
------------------------------------------------------------------------------"""
function distributed_random_trials(trial_count::Int,noise_model::ErdosRenyiNoise,seed_exps::Bool=false
                                   ;method::AlignmentMethod=ΛTAME_M(),graph::RandomGraphType=ErdosRenyi(),
                                   n_sizes = [100, 500, 1000, 2000,5000],p_remove = [.01,.05],
                                   profile=false,kwargs...)

	if seed_exps
		Random.seed!(0)
		seeds = rand(UInt64, length(p_remove),length(n_sizes), trial_count)
	end

	seed = nothing
	results = []
    p_index = 1

    futures = []

    for p in p_remove

        n_index = 1

        for n in n_sizes

            for trial = 1:trial_count

                if seed_exps
                    seed = seeds[p_index,n_index,trial]
                end
                future = @spawn random_graph_exp(n,p,graph; profile=profile,seed=seed,
                                                 method=method,noise_model=noise_model,
                                                 kwargs...)
                push!(futures,(seed,p,n,future))

            end

            n_index += 1
        end
        p_index += 1
    end

    #TODO: fix parsing of returned functions
    for (seed,p,n,future) in futures

        if method === ΛTAME_M() || method === LowRankTAME_M()

            if !haskey(kwargs,:postProcessing) || kwargs[:postProcessing] === noPostProcessing()

                d_A, d_B, perm, alignmentOutput = fetch(future)
            else
                d_A, d_B, perm, (alignmentOutput, postProcessingOutput) = fetch(future)
            end

            matched_tris = alignmentOutput.matchScore
            max_tris = min(alignmentOutput.motifCounts...)
            A_tris, B_tris = alignmentOutput.motifCounts
            best_matching = alignmentOutput.matching
            if profile
                profiling_results = alignmentOutput.profile
            end
        elseif method === TAME_M()
            d_A, d_B, perm, alignmentOutput = fetch(future)
            matched_tris = alignmentOutput.matchScore
            A_tris, B_tris = alignmentOutput.motifCounts
            max_tris = min(alignmentOutput.motifCounts...)
            best_matching = alignmentOutput.matching
            if profile
                profiling_results = alignmentOutput.profile
            end
        elseif method === ΛTAME_MultiMotif_M() || method === LowRankTAME_MultiMotif_M() || method === TAME_MultiMotif_M()
            if !haskey(kwargs,:postProcessing) || kwargs[:postProcessing] === noPostProcessing()
                d_A, d_B, perm, (A_motifDistribution, B_motifDistribution, alignmentOutput) = fetch(future)
            else
                d_A, d_B, perm, (A_motifDistribution, B_motifDistribution, alignmentOutput, postProcessingOutput) = fetch(future)
            end
            if profile
                profiling_results = alignmentOutput.profile
            end
            A_motifCounts, B_motifCounts = alignmentOutput.motifCounts
            best_matching_score = alignmentOutput.matchScore
            best_matching = alignmentOutput.matching
            A_tris = -1
            B_tris = -1
        elseif method === LowRankEigenAlign_M()

            if !haskey(kwargs,:postProcessing) || kwargs[:postProcessing] === noPostProcessing()
                d_A, d_B, perm, alignmentOutput = fetch(future)
            else
                d_A, d_B, perm, (alignmentOutput,postProcessingOutput) = fetch(future)
            end

            matched_tris = alignmentOutput.matchScore
            A_tris, B_tris = alignmentOutput.motifCounts
            max_tris = min(alignmentOutput.motifCounts...)
            best_matching = alignmentOutput.matching

            if profile
                profiling_results = alignmentOutput.profile
            end

        elseif method === EigenAlign_M() || method == Degree_M() || method === Random_M() || method === LowRankEigenAlignOnlyEdges_M()
            d_A, d_B, perm, (A_tris, B_tris, matched_tris, best_matching, _) = fetch(future)
            max_tris = minimum((A_tris,B_tris))
        end

        if length(alignmentOutput.matching) > 0
            accuracy = sum([1 for (i,j) in alignmentOutput.matching if get(perm,j,-1) == i])/n

            D_A = sum(d_A)
            D_B = sum(d_B)
            degree_weighted_accuracy = sum([(get(perm,j,-1) == i) ? ((d_A[i] + d_B[j])/(D_A+D_B)) : 0.0 for (i,j) in best_matching])
        else
            accuracy = -1.0
            degree_weighted_accuracy = -1.0
        end

        data_to_save = Array{Any,1}(undef,0)

        push!(data_to_save,seed)
        push!(data_to_save,p)
        push!(data_to_save,n)
        push!(data_to_save,accuracy)
        push!(data_to_save, degree_weighted_accuracy)
        if method === ΛTAME_MultiMotif_M() || method === LowRankTAME_MultiMotif_M() || method === TAME_MultiMotif_M()
            push!(data_to_save,best_matching_score)
            push!(data_to_save,A_motifCounts)
            push!(data_to_save,B_motifCounts)
            push!(data_to_save,A_motifDistribution)
            push!(data_to_save,B_motifDistribution)
        else
            push!(data_to_save, matched_tris)
            push!(data_to_save, A_tris)
            push!(data_to_save, B_tris)
            push!(data_to_save, max_tris)
        end

        if profile
            push!(data_to_save,profiling_results)
        end

        if haskey(kwargs,:postProcessing)
            if typeof(kwargs[:postProcessing]) <: KlauAlgo
                push!(data_to_save,postProcessingOutput.original_edges_matched)
                push!(data_to_save,postProcessingOutput.klau_edges_matched)
                push!(data_to_save,postProcessingOutput.klau_tris_matched)
                push!(data_to_save,sum([1 for (i,j) in postProcessingOutput.matching if get(perm,j,-1) == i])/n)
                push!(data_to_save,postProcessingOutput.matching)
                if profile
                    push!(data_to_save,postProcessingOutput.setup_rt)
                    push!(data_to_save,postProcessingOutput.klau_rt)
                end
                push!(data_to_save,postProcessingOutput.L_sparsity)
                push!(data_to_save,postProcessingOutput.f_status)
            elseif typeof(kwargs[:postProcessing]) <: SuccessiveKlauAlgo

                push!(data_to_save,postProcessingOutput.original_edges_matched)
                push!(data_to_save,postProcessingOutput.klau_edges_matched)
                push!(data_to_save,postProcessingOutput.klau_tris_matched)
                push!(data_to_save,sum([1 for (i,j) in postProcessingOutput.matching if get(perm,j,-1) == i])/n)
                push!(data_to_save,postProcessingOutput.matching)
                if profile
                    push!(data_to_save,postProcessingOutput.profiling)
                end
            elseif typeof(kwargs[:postProcessing]) <: LocalSearch

                push!(data_to_save,postProcessingOutput.original_edges_matched)
                push!(data_to_save,postProcessingOutput.localsearch_edges_matched)
                push!(data_to_save,postProcessingOutput.localsearch_tris_matched)
                push!(data_to_save,sum([1 for (i,j) in postProcessingOutput.matching if get(perm,j,-1) == i])/n)
                push!(data_to_save,postProcessingOutput.matching)
                if profile
                    push!(data_to_save,postProcessingOutput.profiling)
                    push!(data_to_save,postProcessingOutput.full_runtime)
                end

            end
        end

        push!(results,data_to_save)

    end

    return results

end

function distributed_random_trials(trial_count::Int,noise_model::DuplicationNoise,seed_exps::Bool=false;
                                    method::AlignmentMethod=ΛTAME_M(),graph::RandomGraphType=ErdosRenyi(),
                                    n_sizes = [100, 500, 1000, 2000,5000],
                                    edge_inclusion_p = [.5,1.0],
                                    step_percentage=[.1],profile=false,kwargs...)

    if seed_exps
        Random.seed!(0)
        seeds = rand(UInt64, length(step_percentage),length(edge_inclusion_p),length(n_sizes), trial_count)
    end

    seed = nothing
    results = []

    step_index = 1
    p_index = 1

    futures = []

    for sp in step_percentage

        p_index = 1
        for p in edge_inclusion_p

            n_index = 1

            for n in n_sizes

                for trial = 1:trial_count

                    if seed_exps
                        seed = seeds[step_index,p_index,n_index,trial]
                    end
                    future = @spawn random_graph_exp(n,p,graph; profile=profile,noise_model=noise_model,step_percent=sp,
                                                     seed=seed,method=method, kwargs...)
                    push!(futures,(seed,p,n,sp,future))
                end
                n_index += 1
            end
            p_index += 1
        end
        step_index += 1
    end

    #TODO: fix parsing of returned functions
    for (seed,p,n,sp,future) in futures
        if method === ΛTAME_M() || method === LowRankTAME_M()

            if !haskey(kwargs,:postProcessing) || kwargs[:postProcessing] === noPostProcessing()
                perm, dup_vertices, alignmentOutput = fetch(future)
            else
                perm, dup_vertices, (alignmentOutput, postProcessingOutput) = fetch(future)
            end

            matched_tris = alignmentOutput.matchScore
            A_tris,B_tris = alignmentOutput.motifCounts
            max_tris = min(alignmentOutput.motifCounts...)
            best_matching = alignmentOutput.matching
            if profile
                profiling_results = alignmentOutput.profile
            end

        elseif method === TAME_M()
            perm, dup_vertices, alignmentOutput = fetch(future)
            A_tris,B_tris = alignmentOutput.motifCounts
            matched_tris = alignmentOutput.matchScore
            max_tris = min(alignmentOutput.motifCounts...)
            best_matching = alignmentOutput.matching
            if profile
                profiling_results = alignmentOutput.profile
            end
        elseif method === ΛTAME_MultiMotif_M() || method === LowRankTAME_MultiMotif_M() || method === TAME_MultiMotif_M()
            if !haskey(kwargs,:postProcessing) || kwargs[:postProcessing] === noPostProcessing()
                perm, dup_vertices, (A_motifDistribution, B_motifDistribution, alignmentOutput) = fetch(future)
            else
                perm, dup_vertices, (A_motifDistribution, B_motifDistribution, alignmentOutput, postProcessingOutput) = fetch(future)
            end
            if profile
                profiling_results = alignmentOutput.profile
            end

            best_matching = alignmentOutput.matching
            A_tris = -1
            B_tris = -1
        elseif method === LowRankEigenAlign_M()

            if !haskey(kwargs,:postProcessing) || kwargs[:postProcessing] === noPostProcessing()
                perm, dup_vertices, alignmentOutput = fetch(future)
            else
                perm, dup_vertices, (alignmentOutput,postProcessingOutput) = fetch(future)
            end

            matched_tris = alignmentOutput.matchScore
            A_tris, B_tris = alignmentOutput.motifCounts
            max_tris = min(alignmentOutput.motifCounts...)
            best_matching = alignmentOutput.matching

            if profile
                profiling_results = alignmentOutput.profile
            end

        elseif method === EigenAlign_M() || method == Degree_M() || method === Random_M() || method === LowRankEigenAlignOnlyEdges_M()
            perm, dup_vertices, (A_tris, B_tris, matched_tris, best_matching, _) = fetch(future)
            max_tris = minimum((A_tris, B_tris))
        end

        if length(alignmentOutput.matching) > 0
            accuracy = sum([1 for (i,j) in alignmentOutput.matching if get(perm,i,-1) == j])/n
            #align_matrices is called with B,A since B > A

            dup_tolerant_perm = replaced_duplications_with_originals(perm,n, dup_vertices)
            dup_vertex_tolerant_accuracy = sum([1 for (j,i) in alignmentOutput.matching if get(dup_tolerant_perm,i,-1) == j])/n
        else
            accuracy = -1.0
            dup_vertex_tolerant_accuracy = -1.0
        end

        data_to_save = Array{Any,1}(undef,0)
        push!(data_to_save,seed)
        push!(data_to_save,p)
        push!(data_to_save,n)
        push!(data_to_save,sp)
        push!(data_to_save,accuracy)
        push!(data_to_save, dup_vertex_tolerant_accuracy)

        if method === ΛTAME_MultiMotif_M() || method === LowRankTAME_MultiMotif_M() || method === TAME_MultiMotif_M()
            push!(data_to_save,alignmentOutput.matchScore)
            push!(data_to_save,alignmentOutput.motifCounts[1])
            push!(data_to_save,alignmentOutput.motifCounts[2])
            push!(data_to_save,A_motifDistribution)
            push!(data_to_save,B_motifDistribution)
        else
            push!(data_to_save, matched_tris)
            push!(data_to_save, A_tris)
            push!(data_to_save, B_tris)
            push!(data_to_save, max_tris)
        end

        if profile
            push!(data_to_save,profiling_results)
        end
        if haskey(kwargs,:postProcessing)
            if typeof(kwargs[:postProcessing]) <: KlauAlgo
                push!(data_to_save,postProcessingOutput.original_edges_matched)
                push!(data_to_save,postProcessingOutput.klau_edges_matched)
                push!(data_to_save,postProcessingOutput.klau_tris_matched)
                push!(data_to_save,sum([1 for (j,i) in postProcessingOutput.matching if get(perm,j,-1) == i])/n)
                push!(data_to_save,postProcessingOutput.matching)
                if profile
                    push!(data_to_save,postProcessingOutput.setup_rt)
                    push!(data_to_save,postProcessingOutput.klau_rt)
                end
                push!(data_to_save,postProcessingOutput.L_sparsity)
                push!(data_to_save,postProcessingOutput.f_status)
            elseif typeof(kwargs[:postProcessing]) <: SuccessiveKlauAlgo

                push!(data_to_save,postProcessingOutput.original_edges_matched)
                push!(data_to_save,postProcessingOutput.klau_edges_matched)
                push!(data_to_save,postProcessingOutput.klau_tris_matched)
                push!(data_to_save,sum([1 for (j,i) in postProcessingOutput.matching if get(perm,j,-1) == i])/n)
                push!(data_to_save,postProcessingOutput.matching)
                if profile
                    push!(data_to_save,postProcessingOutput.profiling)
                end
            elseif typeof(kwargs[:postProcessing]) <: LocalSearch

                push!(data_to_save,postProcessingOutput.original_edges_matched)
                push!(data_to_save,postProcessingOutput.localsearch_edges_matched)
                push!(data_to_save,postProcessingOutput.localsearch_tris_matched)
                push!(data_to_save,sum([1 for (j,i) in postProcessingOutput.matching if get(perm,j,-1) == i])/n)
                push!(data_to_save,postProcessingOutput.matching)
                if profile
                    push!(data_to_save,postProcessingOutput.profiling)
                    push!(data_to_save,postProcessingOutput.full_runtime)
                end
            end
        end

        push!(results,data_to_save)
    end
    return results
end

#used as a helper function for mapping duplicated vertices back to vertices in original graph
function replaced_duplications_with_originals(perm,n, dup_vertices)
    dup_tolerant_perm = copy(perm)
    dup_vertex_mapping = Dict([(n+i,v) for (i,v) in enumerate(dup_vertices)])

    while maximum(dup_tolerant_perm) > n
        dup_tolerant_perm = [haskey(dup_vertex_mapping,i) ? dup_vertex_mapping[i] : i for i in dup_tolerant_perm]
    end

    return dup_tolerant_perm

end

"""-----------------------------------------------------------------------------
                        ΛTAME Matching Experiments
-----------------------------------------------------------------------------"""
function ΛTAME_matching_exp(A_file::String, B_file::String, args...)

    @assert A_file[end-5:end] == ".ssten"
    @assert B_file[end-5:end] == ".ssten"

    A = load_SymTensorUnweighted(A_file,'\t') #delimiter may need to be updated
    B = load_SymTensorUnweighted(B_file,'\t')

    return ΛTAME_matching_exp(A,B,args...)

end

function ΛTAME_matching_exp(A_file::String, B_file::String, output_file::String=nothing, args...)

    @assert A_file[end-5:end] == ".ssten"
    @assert B_file[end-5:end] == ".ssten"
    @assert output_file[end-4:end] == ".json"

    A = load_SymTensorUnweighted(A_file,'\t') #delimiter may need to be updated
    B = load_SymTensorUnweighted(B_file,'\t')

    open(output_file,"w") do f
        JSON.print(f,ΛTAME_matching_exp(A,B,args...))
    end

end

function ΛTAME_matching_exp(A::Union{ThirdOrderSymTensor,SymTensorUnweighted{S}},
                            B::Union{ThirdOrderSymTensor,SymTensorUnweighted{S}},
                            max_iter::Int,alphas::Array{Float64,1}=[.5,1.0],
                            betas::Array{Float64,1}=[0.0,1.0,10.0,100.0]) where {S <:Motif}

    tol = 1e-16 # we want to track how triangles change over iterations
    A_motifs= size(A.indices,2)
    B_motifs= size(B.indices,2)

    results = []
    for α in alphas
        for β in betas

            matching_counts = []
            U,V = ΛTAME(A, B, β,max_iter,tol,α)

            d = size(U,2)

            for i=1:d
                motifs_matched,_,_ = TAME_score(A,B,U[:,1:i]*V[:,1:i]')
                push!(matching_counts,motifs_matched)
            end

            push!(results,(α,β,matching_counts))
        end
    end

    A_motifs, B_motifs, results
end

"""-----------------------------------------------------------------------------
                              SSHOPM Experiments
-----------------------------------------------------------------------------"""
function distributed_SSHOPM_exps(tensor_files::Array{String,1},output_path::String,seed::Bool;kwargs...)

    #TODO: test
    seeds = Array{UInt,2}(undef,1,1)

    if seed
        seeds = rand(UInt,length(tensor_files),length(tensor_files))
    end

    futures = []
    for i =1:length(tensor_files)
        for j=i+1:length(tensor_files)

            future = @spawn SSHOPM_exp(tensor_files[i],tensor_files[j],output_path,seeds[i,j])
            push!(futures,(tensor_files[i],tensor_files[j],future))
        end
    end

    #TODO: join using a sigpoll equivilant
    for (ten_A, ten_B, future) in futures
        fetch(future)
        println("joined experiment $ten_A + $ten_B")
    end
end

function distributed_SSHOPM_exps(tensor_files::Array{Tuple{String,String},1},output_path::String,seed::Bool;kwargs...)

    #TODO: test
    seeds = Array{UInt,2}(undef,1,1)

    if seed
        seeds = rand(UInt,length(tensor_files),length(tensor_files))
    end

    futures = []
    for (tensor_A_file, tensor_B_file) in tensor_files

        future = @spawn SSHOPM_exp(tensor_A_file, tensor_B_file, output_path,seeds[i,j])
        push!(futures,(tensor_A_file, tensor_B_file,future))

    end

    #TODO: join using a sigpoll equivilant
    for (ten_A, ten_B, future) in futures
        fetch(future)
        println("joined experiment $ten_A + $ten_B")
    end
end

function SSHOPM_exp(tensor_A_file::String, tensor_B_file::String,output_path::String,seed::UInt)

    @assert tensor_A_file[end-5:end] == ".ssten"
    @assert tensor_B_file[end-5:end] == ".ssten"
    @assert output_path[end] == '/'

    A = load_ThirdOrderSymTensor(tensor_A_file)
    B = load_ThirdOrderSymTensor(tensor_B_file)

    A_root = split(split(tensor_A_file,"/")[end],".ssten")[1]
    B_root = split(split(tensor_B_file,"/")[end],".ssten")[1]

    tol = 1e-16
    max_iter = 30
    β = 0.0
    samples = 200

    #delimeter heirarchy:
    #    - > : > +
    exp_filename = output_path*"align:$(A_root)+$(B_root)-beta:$β-max_iter:$max_iter-samples:$samples-seed:$seed-tol:$tol-results.json"

    relative_λ_diff, extremal_idx, eig_vals, extremal_vecs = SSHOPM_exp(A,B,samples,tol,max_iter,β,seed)

    #TODO: add in json save
    open(exp_filename,"w") do f
        JSON.print(f,[relative_λ_diff, extremal_idx, eig_vals, extremal_vecs])
    end

end

function SSHOPM_exp(A::ThirdOrderSymTensor, B::ThirdOrderSymTensor, samples::Int, tol::Float64,max_iter::Int,β::Float64,seed::UInt=nothing)

    if seed !== nothing
        Random.seed!(seed)
    end

    #A_V,A_Λ = SSHOPM_sample(A,samples,β,max_iter,tol)
    #B_V,B_Λ = SSHOPM_sample(B,samples,β,max_iter,tol)
    #AB_V,AB_Λ = SSHOPM_sample(A,B,samples,β,max_iter,tol)

    A_argmax_vec,A_argmax_val,A_Λ = SSHOPM_sample(A,samples,β,max_iter,tol)
    B_argmax_vec,B_argmax_val,B_Λ = SSHOPM_sample(B,samples,β,max_iter,tol)
    AB_argmax_vec,AB_argmax_val,AB_Λ = SSHOPM_sample(A,B,samples,β,max_iter,tol)

    A_i  = argmax([abs(x) for x in A_Λ])
    B_i  = argmax([abs(x) for x in B_Λ])
    AB_i = argmax([abs(x) for x in AB_Λ])

    relative_λ_diff = abs(A_argmax_val*B_argmax_val - AB_argmax_val)/abs(AB_argmax_val)

    println(relative_λ_diff)

    #r = rank(AB_V[:,:,AB_i])
   #U,S,Vt = svd(AB_V[:,:,AB_i])

    # not assuming anything about the rank of argmax(AB_Λ),
    # error will trigger if not rank 1.
    #u_Avec_inner_prod = dot(U[:,1:r],A_V[:,A_i])
    #v_Bvec_inner_prod = dot(Vt[:,1:r],B_V[:,B_i])

    #  -- subspace angle  --  #
    #u_Av_subspaceAngle = acos(dot(U[:,1:r],A_V[:,A_i]))
    #v_Bv_subspaceAngle = acos(dot(Vt[:,1:r],B_V[:,B_i]))

    #  -- relative norm code  --  #
    #first_nnz_sign = x-> sign(x[findfirst(x .!= 0.0)])
    #relative_u_norm = norm( U[:,1:r]*Diagonal([first_nnz_sign( U[:,j]) for j in 1:r]) - first_nnz_sign(A_V[:,A_i])*A_V[:,A_i])/norm(A_V[:,A_i])
    #relative_v_norm = norm(Vt[:,1:r]*Diagonal([first_nnz_sign(Vt[:,j]) for j in 1:r]) - first_nnz_sign(B_V[:,B_i])*B_V[:,B_i])/norm(B_V[:,B_i])

    # -- group the variables to return from the experiment -- #
    #comparisons = (relative_λ_diff, u_Avec_inner_prod, v_Bvec_inner_prod)
    extremal_idx = (A_i, B_i, AB_i)
    eig_vals = (A_Λ, B_Λ, AB_Λ)
    extremal_vecs = (A_argmax_vec, B_argmax_vec, AB_argmax_vec)

    return relative_λ_diff, extremal_idx, eig_vals, extremal_vecs
end

function test_SSHOPM_exp_file(filename)

    relative_λ_diff, extremal_idx, eig_vals, extremal_vecs = open(filename,"r") do f
        JSON.parse(f)
    end

    subspace_angle = dot(extremal_vecs[1]*extremal_vecs[2]',hcat(extremal_vecs[3]...))
    λ_A  = eig_vals[1][extremal_idx[1]]
    λ_B  = eig_vals[2][extremal_idx[2]]
    λ_AB = eig_vals[3][extremal_idx[3]]

    println("A.n = $(length(extremal_vecs[1]))  B.n=$(length(extremal_vecs[2]))")
    println("λ_A: $λ_A  -- λ_B: $λ_B  -- λ_AB: $λ_AB")
    println("λ_Aλ_B - λ_AB = $(λ_A*λ_B - λ_AB)")
    println("<(v_A*v_B, v_AB) = $subspace_angle")
end

struct HyperKron <: RandomGraphType end
"""-----------------------------------------------------------------------------
  Runs an instance of a graph alignment problem using a random graph model. Once
  a graph is generated, we use the function 'ER_noise_model' to generate a
  second network to align against it. Once the second network is generated it is
  randomly permuted. All additional kwargs are passed to the 'align_matrices'
  function.

  #TODO: update docs

  Inputs
  ------
  * n - (Int):
    The node of nodes to use in each network.
  * 'p_remove' -(Float):
    The probability of removing an edge in the ER noise model. The probability
    of adding in an edge is computed using a function of this parameter at the
    density of the generated network.
  * 'graph_type' - (String):
    The type of graph used to generate the network. Options include 'ER' for an
    Erdos Renyi network, 'RandomGeometric' for a Random Geometric network from
    a 2D unit square, and 'HyperKron' which comes from the Triangle Generalized
    Preferential Attachment model.
  * seed - (Any):
    A seed used to replicate the results. Our experiments typically use UInts.
  * 'use_metis' - (Bool):
    Indicates whether or not to use metis clusters to permute the adjacency
    matrix. For large networks this can improve the runtime by improving the
    access patterns.
  * degreedist - (UnivariateDistribution):
    A random variable generator from Distributions.jl which is used to generate
    the degrees for random nodes. We primarily use
    Distributions.LogNormal(log(5,1)).
  * 'p_edges' - (func Int - Float):
    An anonymous function used for computing the probability of adding edges.
    Primarily used in the ER graphs for generating the probability of adding
    in edges from the number of nodes in the network. an example expected input
    would be 'n -> 2*log(n)/n'.

    Outputs
    -------
    * 'd_A','d_B' - (Array{Float,1}):
      The degrees of all the nodes in graphs A and B. The ith entry of each
      being the degree of node i in the respective networks.
    * perm - (Array{Int,1}):
      The permutation used to shuffle the nodes in graph B. Used for computing
      the precision of the methods.
    * All additional return variables are from 'align_matrices' function call.
-----------------------------------------------------------------------------"""
function random_graph_exp(n::Int, perturbation_p::Float64,graph::RandomGraphType;
                          noise_model::NoiseModelFlag,seed=nothing,step_percent=.1,
                          use_metis=false,degreedist=nothing,p_edges=nothing,kwargs...)

    if seed !== nothing
		Random.seed!(seed)
	end

    if typeof(graph) === ErdosRenyi
        if degreedist === nothing
            if p_edges === nothing
                p = 2*log(n)/n
            else
                p = p_edges(n)
            end
            A = erdos_renyi(n,p)
        else
            A = erdos_renyi(n;degreedist)
            p = nnz(A)/n^2
        end

	elseif typeof(graph) === RandomGeometric

		if degreedist === nothing
            k = 10
            if p_edges === nothing
                p = k/n
            end
			A = random_geometric_graph(n,k)
		else
			d = 2
			A = spatial_network(n, d;degreedist= degreedist)
			p = nnz(A)/n^2
		end

    elseif typeof(graph) == HyperKron
        if p_edges === nothing
            p = .4#2*log(n)/n
        end
		r = .4

		A = sparse(gpa_graph(n,p,r,5))

	else
		error("invalid graph type: $graph_type\n must be either 'ER','RandomGeometric' or 'HyperKron'")
	end

    if typeof(noise_model) === ErdosRenyiNoise
        p_add = (p*perturbation_p)/(1-p)
        #B = ER_noise_model(A,0.0,perturbation_p)#,p_add)
        B = ER_noise_model(A,perturbation_p,p_add)
    elseif  typeof(noise_model) === DuplicationNoise
        steps = Int(ceil(step_percent*size(A,1))) #adds in 10% new nodes by default
        B,dup_vertices = duplication_perturbation_noise_model(A,steps, perturbation_p)
    end

    perm = shuffle(1:size(B,1))
    B = B[perm,perm]

    #return A, B , perm

	if use_metis
		apply_Metis_permutation!(A,100)
		apply_Metis_permutation!(B,100)
	end

    d_A = A*ones(size(A,1))
    d_B = B*ones(size(B,1))

    if typeof(noise_model) === DuplicationNoise
        #  B is always larger than A
        perm,dup_vertices,align_matrices(B,A;kwargs...)
    else
        return d_A,d_B,perm,align_matrices(A,B;kwargs...)
    end
end

function random_graph_exp(file::String,perturbation_p::Float64;noise_model::NoiseModelFlag,seed=nothing,step_percent=.1,
                          use_metis=false,degreedist=nothing,p_edges=nothing,kwargs...)

    if seed !== nothing
        println("seeding with $seed")
		Random.seed!(seed)
	end

    A = MatrixNetworks.readSMAT(file)
    n = size(A,1)

    #=
	elseif typeof(graph) === RandomGeometric

		if degreedist === nothing
            k = 10
            if p_edges === nothing
                p = k/n
            end
			A = random_geometric_graph(n,k)
		else
			d = 2
			A = spatial_network(n, d;degreedist= degreedist)
			p = nnz(A)/n^2
		end

    elseif typeof(graph) == HyperKron
        if p_edges === nothing
            p = .4#2*log(n)/n
        end
		r = .4

		A = sparse(gpa_graph(n,p,r,5))

	else
		error("invalid graph type: $graph_type\n must be either 'ER','RandomGeometric' or 'HyperKron'")
	end
    =#

    if typeof(noise_model) === ErdosRenyiNoise
        p = nnz(A)/size(A,1)^2
        p_add = (p*perturbation_p)/(1-p)
        #B = ER_noise_model(A,0.0,perturbation_p)#,p_add)
        B = ER_noise_model(A,perturbation_p,p_add)
    elseif  typeof(noise_model) === DuplicationNoise
        steps = Int(ceil(step_percent*size(A,1))) #adds in 10% new nodes by default
        B,dup_vertices = duplication_perturbation_noise_model(A,steps, perturbation_p)
    end

    perm = shuffle(1:size(B,1))
    B = B[perm,perm]

    #return A, B , perm

	if use_metis
		apply_Metis_permutation!(A,100)
		apply_Metis_permutation!(B,100)
	end

    d_A = A*ones(size(A,1))
    d_B = B*ones(size(B,1))

    if typeof(noise_model) === DuplicationNoise
        #  B is always larger than A
        return perm,dup_vertices,align_matrices(B,A;kwargs...),A,B
    else
        return d_A,d_B,perm,align_matrices(A,B;kwargs...)
    end
end

function ER_noise_model(A,p_remove::F,p_add::F) where {F <: AbstractFloat}
    n = size(A,1)
    B = copy(A)

    is,js,_ = findnz(erdos_renyi(n,p_remove))
    for (i,j) in zip(is,js)
        if A[i,j] > 0
            B[i,j] = 0
            B[j,i] = 0
        end
    end

    is,js,_ = findnz(erdos_renyi(n,p_add))
    for (i,j) in zip(is,js)
        if A[i,j] == 0.0
            B[i,j] = 1
            B[j,i] = 1
        end
    end

    dropzeros!(B)
    return B
end

function duplication_perturbation_noise_model(A::SparseMatrixCSC{T,Int},steps::Integer, new_edge_p::Float64,seed::Union{UInt,Nothing}=nothing) where T

    is_undirected(A)                      || throw(ArgumentError("A must be undirected."))
    (new_edge_p >= 0 && new_edge_p <= 1)  || throw(ArgumentError("new_edge_p must be a probability."))
    steps >= 0                            || throw(ArgumentError("Must take a non-negative number of steps."))
    # let it steps equal 0 for testing purposes

    if seed !== nothing
        Random.seed!(seed)
    end

    duplicated_vertices = Array{Int,1}(undef, steps)

    n,_ = size(A) # n will be updated

    #store A as an edge list so it's fast to sample
    A_edge_list = Array{Array{Tuple{Int,T},1},1}(undef,n+steps)
    for i = 1:n
        A_edge_list[i] = collect(zip(findnz(A[i,:])...))
    end
    for i = n+1:n+steps
        A_edge_list[i] = Array{Tuple{Int,T},1}(undef,0)
    end

    for step in 1:steps

        dup_vertex = rand(1:n)
        duplicated_vertices[step] = dup_vertex
        for (neighbor,weight) in A_edge_list[dup_vertex]
            if rand() < new_edge_p
                push!(A_edge_list[n+1],(neighbor,weight))
                push!(A_edge_list[neighbor],(n+1,weight))
            end
        end
        n += 1
    end

    #convert edge list back into a MatrixNetwork
    total_edges = 0
    for i=1:n
        total_edges += length(A_edge_list[i])
    end

    Is = Array{Int,1}(undef,total_edges)
    Js = Array{Int,1}(undef,total_edges)
    Vs = Array{T,1}(undef,total_edges)

    edge_idx = 1
    for i=1:n
        for (n_j,weight) in A_edge_list[i]
            Is[edge_idx] = i
            Js[edge_idx] = n_j
            Vs[edge_idx] = weight
            edge_idx += 1
        end
    end

    return sparse(Js,Is,Vs,n,n), duplicated_vertices
end


function createtestvideo(filename; w = 300, h = 300, fps = 25, totalseconds = 10)
    totalframes = fps*totalseconds
    Δt = Millisecond(Second(1))/fps
    framenumber = Observable(1)
    msg = lift(framenumber) do fn
        t = Time(0) + (fn - 1)*Δt
        """
        Test video
        Frame #: $fn
        Time: $t"""
    end
    scene = Scene(resolution = (w, h))#, show_axis=false, backgroundcolor = :black)
    text!(scene, msg)#, color = :white)
    # scene
    record(scene, filename, 1:totalframes, framerate = fps) do i
        framenumber[] = i
    end
end


tests = [
    "transform_stats",
    "wavelet",
    "point_field"
]

println("Running tests:")

for t in tests
    println(" * $t.jl")
    include("$t.jl")
end

# TODO point field tests


using Test
using TestSetExtensions

include("../src/Groebner.jl")

using .Groebner
using .Groebner.AbstractAlgebra

# Taken from JuMP/test/solvers.jl
function try_import(name::Symbol)
    try
        @eval import $name
        return true
    catch e
        return false
    end
end

⊂(xs, ys) = all(in(ys), xs)
# set equality
≂(xs, ys) = ⊂(xs, ys) && ⊂(ys, xs)

@testset "All tests" begin

    @includetests ["commons", "io", "io_consistency",
                    "univariate_aa"]

    @includetests ["crt_reconstruction"] # "mod_reconstruction"]#,

    @includetests ["core_f4_reduce", "core_f4_stress",
                    "core_f4", "rational_f4", "groebner_certify"]

    @includetests ["core_isgroebner", "core_isgroebner_stress",
                    "isgroebner_certify"]

    @includetests ["core_normalform", "core_normalform_stress"]

    @includetests ["hard_problems_f4", "large_problems_f4"]

    @includetests ["probabilistic_linalg"]

    if try_import(:DynamicPolynomials)
        @includetests ["io_dynamic", "core_dynamic"]
    end

    @includetests ["onthefly_order_change"]

    @includetests ["core_fglm", "core_kbase"]

    @includetests ["benchmark_systems"]

end


using Test
using GraphIO.Graph6

@testset "Graph6" begin
    n1 = (30, UInt8.([93]))
    n2 = (12345, UInt8.([126; 66; 63; 120]))
    n3 = (460175067, UInt8.([126; 126; 63; 90; 90; 90; 90; 90]))
    ns = [n1; n2; n3]
    for n in ns
        @test Graph6._g6_N(n[1]) == n[2]
        @test Graph6._g6_Np(n[2])[1] == n[1]
    end

    for g in values(graphs)
        readback_test(Graph6Format(), g, "graph1")
    end

    f = gettempname()
    write_test(Graph6Format(), graphs, f; remove=false, silent=true)
    read_test_mult(Graph6Format(), graphs, f)
    rm(f)
end


using Base.Test

addprocs(3)

println("[TEST] with local Graphs.jl dictionary and arrays only (multicore)...")
include("fourdoortest.jl")
println("Success")
@test true

println("[TEST] plot functions...")
using Gadfly
# draw all beliefs
DOYTICKS = false
xx,ll = ls(fg)
msgPlots = drawHorBeliefsList(fg, xx, gt=gt,nhor=2);
evalstr = ""
for i in 1:length(msgPlots)
    evalstr = string(evalstr, ",msgPlots[$(i)]")
end
pl = eval(parse(string("vstack(",evalstr[2:end],")")));
@test true

print("[TEST] Ensure memory return is working properly...")
include("typeReturnMemRef.jl")
println("Success")

println("[TEST] packing converters work...")
# using fourdoortest data
topack = fg.f[4].attributes["data"]
dd = convert(FunctionNodeData{PackedOdo},topack)
upd = convert(FunctionNodeData{Odo}, dd)
@test topack.fnc.Zij[1] == upd.fnc.Zij[1]

# data structure conversion tests for protobuffing
println("Testing conversion to packed data structure and back")
dat = IncrementalInference.dlapi.getvertex(fg,1).attributes["data"]
pd = convert(PackedVariableNodeData, dat) #fg.v[1].attributes["data"]
# pd = convert(PackedVariableNodeData,fg.v[1].attributes["data"])
unpckd = convert(VariableNodeData, pd)

@test compare(IncrementalInference.dlapi.getvertex(fg,1).attributes["data"], unpckd)
@test IncrementalInference.dlapi.getvertex(fg,1).attributes["data"] == unpckd
println("Conversions and comparisons agree")

if false
  println("[TEST] with CloudGraphs data layer (multicore)...")
  include("fourdoortestcloudgraph.jl")
  println("[SUCESS]")
else
  warn("[NOT TESTING] CloudGraphs interface, which needs Neo4j installed")
end

println("[TEST] Pose3 evaluations...")
include("threeDimLinearProductTest.jl")
println("[SUCCESS]")


@testset "SimpleEdgeIter" begin
    ga = @inferred(SimpleGraph(10, 20; seed=1))
    gb = @inferred(SimpleGraph(10, 20; seed=1))
    dga = @inferred(SimpleDiGraph(10, 20; seed=1))
    dgb = @inferred(SimpleDiGraph(10, 20; seed=1))
    @testset "string representation" begin
        @test sprint(show, edges(ga)) == "SimpleEdgeIter 20"
    end

    @test length(collect(edges(Graph(0, 0)))) == 0

    @testset "collection operations" begin
        @test @inferred(edges(ga)) == edges(gb)
        @test @inferred(edges(ga)) == collect(Edge, edges(gb))
        @test edges(ga) != collect(Edge, Base.Iterators.take(edges(gb), 5))
        @test collect(Edge, edges(gb)) == edges(ga)
        @test Set{Edge}(collect(Edge, edges(gb))) == edges(ga)
        @test @inferred(edges(ga)) == Set{Edge}(collect(Edge, edges(gb)))

        @test @inferred(edges(dga)) == edges(dgb)
        @test @inferred(edges(dga)) == collect(SimpleEdge, edges(dgb))
        @test edges(dga) != collect(Edge, Base.Iterators.take(edges(dgb), 5))
        @test collect(SimpleEdge, edges(dgb)) == edges(dga)
        @test Set{Edge}(collect(SimpleEdge, edges(dgb))) == edges(dga)
        @test @inferred(edges(dga)) == Set{SimpleEdge}(collect(SimpleEdge, edges(dgb)))
    end
    @testset "eltype" begin
        @test @inferred(eltype(edges(ga))) == eltype(typeof(edges(ga))) == edgetype(ga)
        @test eltype(collect(edges(ga))) == edgetype(ga)
        @test eltype(collect(edges(dga))) == edgetype(dga)
        #
        # codecov for eltype(::Type{SimpleEdgeIter{SimpleDiGraph{T}}}) where {T} = SimpleDiGraphEdge{T}
        gd = SimpleDiGraph{UInt8}(10, 20)
        @test @inferred(eltype(edges(gd))) == eltype(typeof(edges(gd))) == edgetype(gd) == SimpleDiGraphEdge{UInt8}
    end

    ga = SimpleGraph(10)
    add_edge!(ga, 3, 2)
    add_edge!(ga, 3, 10)
    add_edge!(ga, 5, 10)
    add_edge!(ga, 10, 3)

    dga = SimpleDiGraph(10)
    add_edge!(dga, 3, 2)
    add_edge!(dga, 3, 10)
    add_edge!(dga, 5, 10)
    add_edge!(dga, 10, 3)

    e1 = Edge(3, 10)
    e2 = (3, 10)
    @testset "membership operations" begin
        @test e1 ∈ edges(ga)
        @test e2 ∈ edges(ga)
        @test (3, 9) ∉ edges(ga)

        for u in 1:12, v in 1:12
            b = has_edge(ga, u, v)
            @test b == @inferred (u, v) ∈ edges(ga)
            @test b == @inferred (u => v) ∈ edges(ga)
            @test b == @inferred Edge(u, v) ∈ edges(ga)

            db = has_edge(dga, u, v)
            @test db == @inferred (u, v) ∈ edges(dga)
            @test db == @inferred (u => v) ∈ edges(dga)
            @test db == @inferred Edge(u, v) ∈ edges(dga)
        end
    end

    @testset "iterator protocol" begin
        eit = edges(ga)
        # @inferred not valid for new interface anymore (return type is a Union)
        @test collect(eit) == [Edge(2, 3), Edge(3, 10), Edge(5, 10)]

        eit = @inferred(edges(dga))
        @test collect(eit) == [
            SimpleEdge(3, 2), SimpleEdge(3, 10),
            SimpleEdge(5, 10), SimpleEdge(10, 3)
        ]
    end

    @testset "graph modifications" begin
        gb = copy(ga)
        add_vertex!(gb)
        @test edges(ga) == edges(gb)
        @test edges(gb) == edges(ga)

        dgb = copy(dga)
        add_vertex!(dgb)
        @test edges(dga) == edges(dgb)
        @test edges(dgb) == edges(dga)
    end
end


"""
    AbstractPopulation

An abstract type for population-based uncertain values.
"""
abstract type AbstractPopulation <: AbstractUncertainValue end

export AbstractPopulation


#####################################################################################################
# CpG groups
#####################################################################################################
using FASTX
using CpelNano

# Single CpG site
CpelNano.get_grps_from_cgs([10],10)

# Two CpG sites
CpelNano.get_grps_from_cgs([10,15],10)
CpelNano.get_grps_from_cgs([10,25],10)

# Two groups
CpelNano.get_grps_from_cgs([10,30,50,70],10)
CpelNano.get_grps_from_cgs([10,18,26,50,58,66],10)

# Different min distances
CpelNano.get_grps_from_cgs([10,18,26,50,58,66],4)

# IO
dataDir = "$(dirname(dirname(pathof(CpelNano))))/examples/full_example/"
fasta = "$(dataDir)/reference/hg38_chr17_43023997_43145780.fa"
reg_data = CpelNano.RegStruct(); reg_data.chrst = 1; reg_data.chrend = 3000; reg_data.chr = "hg38_dna";

# Get FASTA record for chr
fa_reader = open(FASTA.Reader, fasta, index=fasta * ".fai")
fa_rec = fa_reader["hg38_dna"]
close(fa_reader)

# Get group info
CpelNano.get_grp_info!(reg_data,fa_rec,1)
reg_data.cpg_pos
reg_data.cpg_grps
reg_data.L

#####################################################################################################
# ANALYSIS REGIONS (aka SUBREGIONS)
#####################################################################################################
## Function used in data analysis

using CpelNano
config = CpelNano.CpelNanoConfig()
config.max_size_subreg = 121
rs = CpelNano.RegStruct()
rs.chrst = 1
rs.chrend = 1007
rs.cpg_pos = [50,100,112,150,250,600,700,900]
CpelNano.get_nls_reg_info(rs,config)

## Table from fasta
using CpelNano

# IO
fasta = "/Users/jordiabante/OneDrive - Johns Hopkins/CpelNano/Data/Simulations/reference/chr22.fa"

# Partition evenly spaced CpG sites
config = CpelNano.CpelNanoConfig()
config.out_dir = "/Users/jordiabante/Desktop/"
config.size_est_reg = 3000
config.max_size_subreg = 250

# Get histogram of size
CpelNano.subregion_table(fasta,config)

#####################################################################################################
# GENOME METRICS
#####################################################################################################
using CpelNano

# CpG density
dna_seq = "AACGTAACGAA"
cpg_pos = map(x -> getfield(x, :offset), eachmatch(r"[Cc][Gg]", dna_seq))
CpelNano.get_ρn(dna_seq,cpg_pos;width=3)

#####################################################################################################
# Produce genome info table
#####################################################################################################
using CpelNano

# IO
dataDir = "$(dirname(dirname(pathof(CpelNano))))/examples/full_example/reference/"
fasta = "$(dataDir)/hg38_chr17_43023997_43145780.fa"

# Partition evenly spaced CpG sites
config = CpelNano.CpelNanoConfig(); config.out_dir = "/Users/jordiabante/Desktop/"

# Get histogram of size
CpelNano.estimation_region_table(fasta,config)

#####################################################################################################
# Produce estimation region genomic info
#####################################################################################################
using CpelNano

# IO
dataDir = "$(dirname(dirname(pathof(CpelNano))))/examples/full_example/reference/"
fasta = "$(dataDir)/hg38_chr17_43023997_43145780.fa"

# Partition evenly spaced CpG sites
config = CpelNano.CpelNanoConfig(); config.out_dir = "/Users/jordiabante/Desktop/"

# Get histogram of size
CpelNano.get_estimation_region_info(fasta,"hg38_dna",1000:2000,config)

#####################################################################################################
# Produce histogram of group lengths
#####################################################################################################
using CpelNano

# IO
dataDir = "$(dirname(dirname(pathof(CpelNano))))/examples/full_example/reference/"
fasta = "$(dataDir)/hg38_chr17_43023997_43145780.fa"

# Partition evenly spaced CpG sites
config = CpelNano.CpelNanoConfig(); config.out_dir = "/Users/jordiabante/Desktop/"

# Get histogram of size
CpelNano.hist_group_length(fasta,config)

#####################################################################################################
# Produce histogram of group num of CG
#####################################################################################################
using CpelNano

# IO
dataDir = "$(dirname(dirname(pathof(CpelNano))))/examples/full_example/reference/"
fasta = "$(dataDir)/hg38_chr17_43023997_43145780.fa"

# Partition evenly spaced CpG sites
config = CpelNano.CpelNanoConfig(); config.out_dir = "/Users/jordiabante/Desktop/"

# Get histogram of size
CpelNano.hist_group_cpg_num(fasta,config)

#####################################################################################################
# Partition of chromosome (informME)
#####################################################################################################
using CpelNano

# IO
dataDir = "$(dirname(dirname(pathof(CpelNano))))/examples/full_example/reference/"
fasta = "$(dataDir)/hg38_chr17_43023997_43145780.fa"

# Partition evenly spaced CpG sites
config = CpelNano.CpelNanoConfig(); config.informme_mode = true;

# Get partition respecting CpG groups
chr_names, chr_sizes = CpelNano.get_genome_info(fasta)
chr_part = CpelNano.get_informME_chr_part(chr_sizes[1], config)

#####################################################################################################
# Partition of chromosome
#####################################################################################################
using CpelNano

# IO
dataDir = "$(dirname(dirname(pathof(CpelNano))))/examples/full_example/reference/"
fasta = "$(dataDir)/hg38_chr17_43023997_43145780.fa"

# Partition evenly spaced CpG sites
config = CpelNano.CpelNanoConfig(); config.min_grp_dist = 1; config.verbose = true;

# Get partition respecting CpG groups
chr = "hg38_dna"
chr_part = CpelNano.get_chr_part(chr, config, fasta)

#####################################################################################################
# Partition in targeted case
#####################################################################################################
using CpelNano

# IO
dataDir = "$(dirname(dirname(pathof(CpelNano))))/examples/targeted_example/"
fasta = "$(dataDir)/reference/hg38_chr17_42879379_43290399.fa"
bed = "$(dataDir)/regions_of_interest/brca1.bed"

# IO
config = CpelNano.CpelNanoConfig(); config.bed_reg = bed; config.min_grp_dist = 1;

# Get dictionary
targ_regs = CpelNano.read_bed_reg(config)

# Get chr partition
chr = "hg38_dna"
chr_part = CpelNano.get_chr_part(chr, config, fasta, targ_regs[chr])


# packages required for test
using Test
using LinearAlgebra
using Random
using ForwardDiff
using Zygote

# packages tested
using KFEstimate

@testset "KFEstimate.jl" begin
    # Write your tests here.
    testdir = joinpath(dirname(@__DIR__), "test")
    @time @testset "Test KF" begin
        include(joinpath(testdir, "test_kf.jl"))
    end
end


# This file is a part of JuliaFEM.
# License is MIT: see https://github.com/JuliaFEM/FEMSparse.jl/blob/master/LICENSE

type SparseVectorDOK{Tv,Ti<:Integer} <: AbstractSparseArray{Tv,Ti<:Integer,1}
    data :: Dict{Ti,Tv}
end

function SparseVectorDOK()
    return SparseVectorDOK(Dict{Int64, Float64}())
end

function SparseVectorDOK{Tv,Ti<:Integer}(b::SparseVector{Tv,Ti})
    I, V = findnz(b)
    c = SparseVectorDOK()
    add!(c, I, V)
    return c
end

function SparseVectorDOK{T}(b::Vector{T})
    return SparseVectorDOK(sparsevec(b))
end

function add!{Tv,Ti<:Integer}(b::SparseVectorDOK{Tv,Ti}, i::Ti, v::Tv)
    b.data[i] = get(b, i) + v
    return nothing
end

function add!{Tv,Ti<:Integer}(b::SparseVectorDOK{Tv,Ti}, dofs::Vector{Ti}, data::Vector{Tv})
    @assert length(dofs) == length(data)
    z = Tv(0)
    for i=1:length(dofs)
        @inbounds b.data[dofs[i]] = Base.get(b.data, i, z) + data[i]
    end
    return nothing
end

function get{Tv,Ti<:Integer}(b::SparseVectorDOK{Tv,Ti}, i::Ti)
    return Base.get(b.data, i, Tv(0))
end

function get!{Tv,Ti<:Integer}(b::SparseVectorDOK{Tv,Ti}, dofs::Vector{Ti}, data::Vector{Tv})
    for (i,j) in enumerate(dofs)
        data[i] = get(b, i)
    end
    return nothing
end


function doubleIt_unmemoized(x)
    println("Evaluating...")
    2x
end

empty!(ObjectIdDict())

doubleIt(x)::Any = begin
    if haskey(ObjectIdDict(), (x,))
        (ObjectIdDict())[(x,)]::Core.Inference.return_type(doubleIt_unmemoized, typeof((x,)))
    else
        (ObjectIdDict())[(x,)] = doubleIt_unmemoized(x)
    end
end


##############################################################################
# an array of strings with header info

import Statistics

mutable struct DFrame
    A
    names
    cols
end

# empty frame
function DFrame(numrows::Integer; estnumcols=0)
    A =  Array{Float64}(undef,numrows,estnumcols)
    names = Any[]
    return DFrame(A,names,0)
end

# frame with no names
function DFrame(A)
    d = size(A,2)
    names = Array{Any}(d)
    fill!(names, "")
    return DFrame(A,names,d)
end

function DFrame(A, names)
    return DFrame(A, names, size(A,2))
end

findcolumn(c::String, DF::DFrame) = findvalue(c, DF.names)
findcolumn(c::Number, DF::DFrame) = c

function addcolumn(DF::DFrame, u::Array{Float64,2})
    newcols = size(u,2)
    if DF.cols + newcols < size(DF.A,2)
        DF.A[:,DF.cols+1:DF.cols+newcols] = u
        DF.cols += newcols
        return
    end
    DF.A = [DF.A u]
    DF.cols += newcols
end

addcolumn(DF::DFrame, u::Array{Float64,1}) = addcolumn(DF, reshape(u, length(u),1))

import Base.size
function size(DF::DFrame, x::Number)
    if x == 1
        return size(DF.A,1)
    end
    return DF.cols
end

function size(DF::DFrame)
    return size(DF.A,1), DF.cols
end
function value(DF::DFrame)
    return DF.A[:,1:DF.cols]
end

##############################################################################

abstract type DataSource end

##############################################################################
# interface

mutable struct FrameSource<:DataSource
    Uf
    Vf
    Xmaps
    Ymaps
    count
    Xnames  # saved for unembedding
    Ynames  # saved for unembedding
end

function FrameSource(Uf::DFrame, Vf::DFrame)
    return FrameSource(Uf, Vf, Any[], Any[], [0], nothing, nothing)
end

function makeFrameSource(U, V, Unames, Vnames; kwargs...)
    if Unames != nothing
        Uf = DFrame(matrix(U), Unames)
    else
        Uf = DFrame(matrix(U))
    end
    if Vnames != nothing
        Vf = DFrame(matrix(V), Vnames)
    else
        Vf = DFrame(matrix(V))
    end
    return FrameSource(Uf, Vf)
end

function getXY(F::FrameSource; Uestnumcols=0, Vestnumcols=0, verbose=false)
    Xf = applyfmaplist(F.Xmaps, F.Uf; estnumcols = Uestnumcols, verbose=verbose)
    Yf = applyfmaplist(F.Ymaps, F.Vf; estnumcols = Vestnumcols, verbose=verbose)
    X = value(Xf)
    Y = value(Yf)
    F.Xnames = Xf.names
    F.Ynames = Yf.names
    return X, Y
end

getU(F::FrameSource) = value(F.Uf)
getV(F::FrameSource) = value(F.Vf)

# embed one data record
function embedU(F::FrameSource, u::Array{T,1}) where {T<:Any}
    if size(F.Uf,2) == 1
        # U has only one column, so we are embedding
        # many 1 dimensional records
        U = reshape(u, length(u),1)
    else
        # U has more than one column
        # so we must be embedding a single record
        # (and the records must have dimension equal to length(u)
        # otherwise this will fail.)
        U = reshape(u, 1, length(u))
    end
    embedU(F, U)
end

function embedU(F::FrameSource, U::Array{T,2}) where {T<:Any}
    Uf = DFrame(U, F.Uf.names)
    Xf = applyfmaplist(F.Xmaps, Uf)
    return value(Xf)
end

function embedV(F::FrameSource, U::Array{T,2}) where {T<:Any}
    Uf = DFrame(U, F.Vf.names)
    Xf = applyfmaplist(F.Ymaps, Uf)
    return value(Xf)
end
##############################################################################
# internal code

function findcolumn(c::Number, uvframe, xyframe)
    return uvframe, c
end

# by default c refers to the uvframe
# but can refer to the xyframe if
# c is only in the xyframe.names
function findcolumn(c::String, uvframe, xyframe)
    if c in uvframe.names
        return uvframe, findvalue(c, uvframe.names)
    end
    if c in xyframe.names
        return xyframe, findvalue(c, xyframe.names)
    end
    # we cannot resolve this column name
    # but return c so that it can be used in appendcol
    return xyframe, c
end

function columnvalues(DF::DFrame, col)
    j = findcolumn(col, DF)
    n = size(DF, 1)
    u = zeros(n)
    for i=1:n
        u[i] = number(DF.A[i,j])
    end
    return u
end

function appendcol(DF::DFrame, name::Int, u)
    DF.A[:,name] = u
end

# appends or replaces if the name exists already
function appendcol(DF::DFrame, name, u)
    if name != nothing && name in DF.names
        if size(u,2) > 1
            println("ERROR: Cannot specify existing destination name if using onehot")
        end
        j = findvalue(name, DF.names)
        DF.A[:,j] = u
    else
        addcolumn(DF, u)
        if size(u,2) == 1
            push!(DF.names, name)
        else
            for k=1:size(u,2)
                push!(DF.names, name * "_$k")
            end
        end
    end
end

# find all different values in column
# assign each a number 1...d
# return a dict mapping values to numbers
# and a list of values in order
function uniquecolumnvalues(DF::DFrame, col)
    j = findcolumn(col, DF)
    n = size(DF, 1)
    valtonum = Dict()
    vals = Any[]
    num = 1
    possiblevalues = sort(collect(Set(DF.A[:,j])))
    for val in possiblevalues
        valtonum[val] = num
        push!(vals, val)
        num += 1
    end
    return valtonum, vals
end

# map a column according to valtonum
function columntointegers(DF::DFrame, col, valtonum)
    j = findcolumn(col, DF)
    n = size(DF,1)
    u = zeros(n)
    for i=1:n
        u[i] = valtonum[DF.A[i,j]]
    end
    return u
end

# valtonum is a dict mapping strings to 1...d
function coltoonehot(DF::DFrame, col, valtonum)
    j = findcolumn(col, DF)
    n = size(DF,1)
    d = length(valtonum)
    u = zeros(n,d)
    for i=1:n
        u[i, valtonum[DF.A[i,j]] ] = 1
    end
    return u
end

#######################################################################

abstract type FeatureMap end

##############################

mutable struct AddColumnFmap<:FeatureMap
    src    # source column name or number
    dest   # destination name, may be nothing
end

status(FM::AddColumnFmap) = "Add column $(FM.src) as $(FM.dest)"

function applyfmap(FM::AddColumnFmap, uvframe, xyframe)
    srcframe, srccol =  findcolumn(FM.src, uvframe, xyframe)
    u  = columnvalues(srcframe, srccol)
    appendcol(xyframe, FM.dest, u)
end

# map from dest to src
function invertfmap(FM::AddColumnFmap, uvframe, xyframe)
    destcol = findcolumn(FM.dest, xyframe)
    x = columnvalues(xyframe, destcol)
    srcframe, srccol = findcolumn(FM.src, uvframe, xyframe)
    appendcol(srcframe, FM.src, x)
end

##############################

mutable struct OneHotFmap<:FeatureMap
    src  # source column name or number
    dest # destination name, may be nothing
    standardize  # true if standardized
    used # if been standardized before
    mean
    std
    valtonum  # dict mapping values to numbers
    vals  # list of the values in order (i.e. a list mapping numbers to values)
end

status(FM::OneHotFmap) = "Add one-hot column $(FM.src)"

OneHotFmap(c,n,s) = OneHotFmap(c,n,s,false,nothing,nothing,nothing,nothing)

# one hot does it's own standardization since at the moment
# each feature added only takes one input column
# we do not know at the time the feature is added to the list
# how many possible values its input column will take
# and so we don't know how many output columns will be generated
# and so we cannot generate standardizers for each of them
#

function applyfmap(FM::OneHotFmap, uvframe, xyframe)
    srcframe, srccol =  findcolumn(FM.src, uvframe, xyframe)
    valtonum, vals = uniquecolumnvalues(srcframe, srccol)
    FM.valtonum = valtonum
    FM.vals = vals
    u = coltoonehot(srcframe, srccol, valtonum)
    if FM.standardize
        if !FM.used
            FM.mean = Statistics.mean(u,dims=1)
            FM.std = Statistics.std(u,dims=1)
            FM.used = true
        end
        n = size(u,1)
        u = (u - repeat(FM.mean, n, 1))./repeat(FM.std,n,1)
    end
    appendcol(xyframe, FM.dest, u)
end

# map from dest to src
function invertfmap(FM::OneHotFmap, uvframe, xyframe)
    # K is number of classes
    K = length(FM.vals)
    n = size(xyframe, 1)
    x = Array{Any}(n)
    destcols = zeros(Int64, K)
    for k=1:K
        destcols[k] = findcolumn(FM.dest * "_$(k)", xyframe)
    end
    xi = zeros(K)
    for i=1:n
        for k=1:K
            xi[k] = xyframe.A[i,destcols[k]]
        end
        maxval, maxind = findmax(xi)
        x[i] = FM.vals[maxind]
    end
    srcframe, srccol = findcolumn(FM.src, uvframe, xyframe)
    appendcol(srcframe, FM.src, x)
end

##############################

mutable struct OrdinalFmap<:FeatureMap
    src   # source column name or number
    dest  # destination name, may be nothing
    categories
end

status(FM::OrdinalFmap) = "Add ordinal column $(FM.src) with categories $(FM.categories)"

function applyfmap(FM::OrdinalFmap, uvframe, xyframe)
    srcframe, srccol =  findcolumn(FM.src, uvframe, xyframe)
    valtonum = Dict()
    for i=1:length(FM.categories)
        valtonum[FM.categories[i]] = i
    end
    u = columntointegers(srcframe, srccol, valtonum)
    appendcol(xyframe, FM.dest, u)
end

# maps dest to src
# map back to categories by nearest neighbor
function invertfmap(FM::OrdinalFmap, uvframe, xyframe)
    destcol = findcolumn(FM.dest, xyframe)
    x = columnvalues(xyframe, destcol)
    n = length(x)
    u = zeros(n)
    for i=1:n
        closestcat = 0
        mindist = Inf
        for j=1:length(FM.categories)
            dist = abs(x[i]-j)
            if dist < mindist
                mindist = dist
                closestcat = j
            end
        end
        u[i]  = FM.categories[closestcat]
    end
    srcframe, srccol = findcolumn(FM.src, uvframe, xyframe)
    appendcol(srcframe, FM.src, u)
end

##############################

mutable struct FunctionFmap<:FeatureMap
    src   # source column name or number
    dest  # destination name, may be nothing
    f
    finv
end

status(FM::FunctionFmap) = "Add function column $(FM.src)"

function FunctionFmap(src, dest, f)
    if f == log
        finv = exp
    else
        finv = nothing
    end
    return FunctionFmap(src, dest, f, finv)
end

function applyfmap(FM::FunctionFmap, uvframe, xyframe)
    srcframe, srccol =  findcolumn(FM.src, uvframe, xyframe)
    u  = columnvalues(srcframe, srccol)
    unew = [ FM.f(x) for x in u]
    appendcol(xyframe, FM.dest, unew)
end

# maps dest to src
function invertfmap(FM::FunctionFmap, uvframe, xyframe)
    destcol = findcolumn(FM.dest, xyframe)
    x = columnvalues(xyframe, destcol)
    u = [ FM.finv(a) for a in x ]
    srcframe, srccol = findcolumn(FM.src, uvframe, xyframe)
    appendcol(srcframe, FM.src, u)
end

###########################################

mutable struct FunctionListFmap<:FeatureMap
    src  # list of source column names or numbers
    dest # destination name, may be nothing
    f
end

status(FM::FunctionListFmap) = "add function to columns: $(FM.src)"

function applyfmap(FM::FunctionListFmap, uvframe, xyframe)
    args = Any[]
    for srci in FM.src
        srcframe, srccol =  findcolumn(srci, uvframe, xyframe)
        u  = columnvalues(srcframe, srccol)
        push!(args, u)
    end
    n = length(args[1])
    unew = zeros(n)
    for i=1:n
        a2 = Any[]
        for j=1:length(args)
            push!(a2, args[j][i])
        end
        unew[i]  = FM.f(a2...)
    end
    appendcol(xyframe, FM.dest, unew)
end

########################################################
mutable struct UfunctionFmap<:FeatureMap
    dest # destination name, may be nothing
    f
end

status(FM::UfunctionFmap) = "apply function to U"

function applyfmap(FM::UfunctionFmap, uvframe, xyframe)
    n = size(uvframe, 1)
    unew  = FM.f(uvframe.A)
    appendcol(xyframe, FM.dest, unew)
end

########################################################
mutable struct AllFmap<:FeatureMap
    dest # destination name, may be nothing
    stand  # boolean
    addones #bool
    mean
    std
    used
end

AllFmap(dest, stand, addones) = AllFmap(dest, stand, addones,
                                        nothing, nothing, false)

status(FM::AllFmap) = "embed all at once"

function applyfmap(FM::AllFmap, uvframe, xyframe)
    U = uvframe.A
    if !FM.used
        if FM.stand
            FM.mean = Statistics.mean(U,dims=1)
            FM.std =Statistics.std(U,dims=1)
        else
            FM.mean = zeros(1,d)
            FM.std = ones(1,d)
        end
        FM.used = true
    end
    n,d = size(U)
    unew = (U - repeat(FM.mean,n,1))./repeat(FM.std,n,1)
    if FM.addones
        unew = [ones(n) unew]
    end
    appendcol(xyframe, FM.dest, unew)
end

##############################

mutable struct StandardizeFmap<:FeatureMap
    src  # source column name or number
    dest # destination name, may be nothing
    mean
    std
    used
end

status(FM::StandardizeFmap) = "standardize column $(FM.src)"

function applyfmap(FM::StandardizeFmap, uvframe, xyframe)
    srcframe, srccol =  findcolumn(FM.src, uvframe, xyframe)
    u  = columnvalues(srcframe, srccol)
    if !FM.used
        FM.mean = Statistics.mean(u)
        FM.std = Statistics.std(u)
        if FM.std<1e-2
            FM.std=1
        end
        FM.used = true
    end
    unew = (u .- FM.mean)/FM.std
    appendcol(xyframe, FM.dest, unew)
end

# map dest to src
function invertfmap(FM::StandardizeFmap, uvframe, xyframe)
    destcol = findcolumn(FM.dest, xyframe)
    x = columnvalues(xyframe, destcol)
    u = FM.std*x + FM.mean
    srcframe, srccol = findcolumn(FM.src, uvframe, xyframe)
    appendcol(srcframe, FM.src, u)
end

function StandardizeFmap(src, dest)
    return StandardizeFmap(src, dest, nothing, nothing, false)
end

##############################

mutable struct OneFmap<:FeatureMap
    dest # destination name, may be nothing
end

status(FM::OneFmap) = "add column of ones as $(FM.dest)"

function applyfmap(FM::OneFmap, uvframe, xyframe)
    n = size(uvframe, 1)
    u = ones(n)
    appendcol(xyframe, FM.dest, u)
end

##############################################################################

function getfeature(col; name = nothing, etype="number",
                    categories = nothing,
                    f = nothing, stand=true, addones=false, kwargs...)
    if etype == "number"
        return AddColumnFmap(col, name)
    elseif etype == "onehot"
        return OneHotFmap(col, name, false)
    elseif etype == "onehotstd"
        return OneHotFmap(col, name, true)
    elseif etype == "ordinal"
        return OrdinalFmap(col, name, categories)
    elseif etype == "function"
        return FunctionFmap(col, name, f)
    elseif etype == "standardize"
        return StandardizeFmap(col, name)
    elseif etype == "product"
        return FunctionListFmap(col, name, (x,y)-> x*y)
    elseif etype == "functionlist"
        return FunctionListFmap(col, name, f)
    elseif etype == "ufunction"
        return UfunctionFmap(name, f)
    elseif etype == "all"
        return AllFmap(name, stand, addones)
    elseif etype == "one"
        return OneFmap(name)
    end
end

function addfeaturex(fmaps, count, col;
                     stand=true, name=nothing, etype="number", kwargs...)
    # make sure every feature has a name
    if name == nothing
        name = "feature$(count[1])"
        count[1] += 1
    end
    fe = getfeature(col; name=name, etype=etype, kwargs...)
    push!(fmaps, fe)
    if stand && etype != "onehot" && etype != "one" && etype != "onehotstd" && etype != "ufunction" && etype != "all"
        # add a featurizer that replaces the feature with
        # with a standardized version that has the same name
        addfeaturex(fmaps, count, name;
                      etype="standardize", stand=false, name=name, kwargs...)
    end

end

function addfeatureU(F::FrameSource, col; kwargs...)
    addfeaturex(F.Xmaps, F.count, col; kwargs...)
end

function addfeatureV(F::FrameSource, col; kwargs...)
    addfeaturex(F.Ymaps, F.count, col; kwargs...)
end

function applyfmaplist(fmaps, uvframe; estnumcols=0, verbose=false)
    n = size(uvframe,1)
    xyframe = DFrame(n; estnumcols=estnumcols)
    ne = length(fmaps)
    for i=1:ne
        if verbose
            println("FrameSource: Applying feature map: ", status(fmaps[i]))
        end
        applyfmap(fmaps[i], uvframe, xyframe)
    end
    return xyframe
end

function unembedY(F::FrameSource, y::Array{Float64,1})
    Y = reshape(y, 1, length(y))
    unembedY(F, Y)
end

function unembedY(F::FrameSource, Y::Array{Float64,2})
    Yf = DFrame(Y, F.Ynames)
    d = size(F.Vf, 2)
    n = size(Y,1)
    Vf = DFrame(zeros(n,d), F.Vf.names)
    invertfmaplist(F.Ymaps, Yf, Vf)
    V = value(Vf)
    return V
end

function invertfmaplist(fmaps, xyframe, uvframe)
    n = size(xyframe, 1)
    ne = length(fmaps)
    for i=ne:-1:1
        invertfmap(fmaps[i], uvframe, xyframe)
    end
end


export  transFluxPlot

function transFluxPlot( mpSim::MPsim, timeGrid::Vector{T},
    transitions::MP.TransitionType...; showPlot::Bool=true,
    savePlot::Bool=false, filename::AbstractString="",
    timeFactor::Real=12.0 ) where T <: Real

    savePlot |= filename != ""

    # Don't do anything if the plot mustn't be shown or saved.
    if !( showPlot || savePlot )
        return
    end  # if !( showPlot || savePlot )

    # Generate the report.
    fluxReport = transitionFluxReport( mpSim, timeGrid, transitions... )

    if isempty( fluxReport )
        return
    end  # if isempty( fluxReport )

    generateTransFluxPlot( fluxReport, timeFactor, showPlot, savePlot,
        filename )

end  # transFluxPlot( mpSim, timeGrid, transitions, showPlot, savePlot,
     #   filename, timeFactor )

transFluxPlot( mpSim::MPsim, timeRes::Real,
    transitions::MP.TransitionType...; showPlot::Bool=true,
    savePlot::Bool=false, filename::AbstractString="",
    timeFactor::Real=12.0 ) =
    transFluxPlot( mpSim, MP.generateTimeGrid( mpSim, timeRes ),
    transitions..., showPlot=showPlot, savePlot=savePlot,
    filename=filename, timeFactor=timeFactor )

include( "private/transitionflux.jl" )


type Run{F <: AbstractFloat, I <: Integer, C <: Configuration}
    cost::Function
    cost_arguments::Dict{Symbol, Any}
    cost_evaluations::I
    cost_values::Array{F, 1}
    starting_point::C
    starting_cost::F
    report_after::I
    reporting_criterion::Function
    measurement_method::Function
    stopping_criterion::Function
    duration::I
    methods::Array{Any, 2}
    channel_size::I

    function call{F <: AbstractFloat, I <: Integer, C <: Configuration}(::Type{Run};
            cost::Function                    = (c(x) = 0),
            cost_arguments::Dict{Symbol, Any} = Dict{Symbol, Any}(),
            cost_evaluations::I               = 1,
            cost_values::Array{F, 1}          = [0.0],
            starting_point::C                 = Configuration("empty"),
            starting_cost::F                  = 0.0,
            report_after::I                   = 100,
            reporting_criterion::Function     = iterations_reporting_criterion,
            measurement_method::Function      = measure_mean!,
            stopping_criterion::Function      = iterations_criterion,
            duration::I                       = 1_000,
            methods::Array{Any, 2}            = [[:simulated_annealing 2];],
            channel_size::I                   = 4096)

        new{F, I, C}(cost,
                     cost_arguments,
                     cost_evaluations,
                     cost_values,
                     starting_point,
                     starting_cost,
                     report_after,
                     reporting_criterion,
                     measurement_method,
                     stopping_criterion,
                     duration,
                     methods,
                     channel_size)
    end
end


# This file describes how we decide which logger (e.g. LogText vs LogValue vs LogHistograms)
# to use for what data, and any preprocessing

"""
    preprocess(name, val, data)

This method takes a tag `name` and the value `val::T` pair. If type `T` can be
serialized to TensorBoard then the pair is pushed to `data`, otherwise it should
call `preprocess` recursively with some simpler types, until a serializable
type is finally hit.

For a struct, it calls preprocess on every field.
"""
function preprocess(name, val::T, data) where T
    if isstructtype(T)
        fn = logable_propertynames(val)
        for f=fn
            prop = getfield(val, f)
            preprocess(name*"/$f", prop, data)
        end
    else
        # If we do not know how to serialize a type, then
        # it will be simply logged as text
        push!(data, name=>val)
    end
    data
end

"""
    logable_propertynames(val::Any)

Returns a tuple with the name of the fields of the structure `val` that
should be logged to TensorBoard. This function should be overridden when
you want TensorBoard to ignore some fields in a structure when logging
it. The default behaviour is to return the  same result as `propertynames`.

See also: [`Base.propertynames`](@ref)
"""
logable_propertynames(val::Any) = propertynames(val)

## Default unpacking of key-value dictionaries
function preprocess(name, dict::AbstractDict, data)
    for (key, val) in dict
        # convert any key into a string, via interpolating it
        preprocess("$name/$key", val, data)
    end
    return data
end

## Default behaviours

########## For things going to LogImage ##############################
function preprocess(name,   img::AbstractArray{<:Colorant}, data)
    # If it has three dimensions (and we don't have 3D monitors) we log several
    # 2D slices under the same tag so that TB shows a slider along the z direction.
    dimensions = ndims(img)
    if dimensions == 3
        #3rd is channel dim as observed in testimages
        channels = size(img, 3)
        for c in 1:channels
            preprocess(name, convert(PngImage, img[:, :, c]), data)
        end
    else
        preprocess(name, convert(PngImage, img), data)
    end
    return data
end
preprocess(name, val::PngImage, data) = push!(data, name=>val)
summary_impl(name, value::PngImage) = image_summary(name, value)

########## For things going to LogText ##############################
preprocess(name, val::AbstractString, data) where T<:String = push!(data, name=>val)
summary_impl(name, value::Any) = text_summary(name, value)

########## For things going to LogHistograms ########################
# Only consider 1D histograms for histogram plotting
preprocess(name, hist::StatsBase.Histogram{T,1}, data) where T = push!(data, name=>hist)
summary_impl(name, hist::StatsBase.Histogram) = histogram_summary(name, hist)

# TODO: maybe deprecate? tuple means histogram (only if bins/weights match)
function preprocess(name,   (bins,weights)::Tuple{AbstractVector,AbstractVector}, data)
    # if ... this is an histogram
    if length(bins) == length(weights)+1
        return preprocess(name, Histogram(bins,weights), data)
    end
    preprocess(name*"/1", bins, data)
    preprocess(name*"/2", weights, data)
end

preprocess(name, val::AbstractArray{<:Real}, data) = push!(data, name=>val)
summary_impl(name, val::AbstractArray{<:Real}) = histogram_arr_summary(name, val)

# Split complex numbers into real/complex pairs
preprocess(name, val::AbstractArray{<:Complex}, data) = push!(data, name*"/re"=>real.(val), name*"/im"=>imag.(val))

########## For things going to LogValue #############################
preprocess(name, val::Real, data) = push!(data, name=>val)
summary_impl(name, value::Real) = scalar_summary(name, value)

# Split complex numbers into real/complex pairs
preprocess(name, val::Complex, data) = push!(data, name*"/re"=>real(val), name*"/im"=>imag(val))


module MetNets

    import MAT
    import SparseArrays
    import SparseArrays: AbstractSparseArray, sparse
    import Statistics: mean

    using ProgressMeter
    using Serialization

    # Types
    include("Types/Rxns.jl")
    include("Types/Mets.jl")
    include("Types/MetNet.jl")
    include("Types/MetStates.jl")

    # Utils
    include("Utils/iders.jl") # Need to be included first
    include("Utils/balance_str.jl")
    include("Utils/base.jl")
    include("Utils/check_dims.jl")
    include("Utils/clampfields.jl")
    include("Utils/compressed_model.jl")
    include("Utils/defaults.jl")
    include("Utils/del_blocked.jl")
    include("Utils/del_met.jl")
    include("Utils/del_rxn.jl")
    include("Utils/del_void_rxns_mets.jl")
    include("Utils/extended_model.jl")
    include("Utils/fixxing.jl")
    include("Utils/force_dims.jl")
    include("Utils/getter.jl")
    include("Utils/interchange_rxn.jl")
    include("Utils/invert_bkwds.jl")
    include("Utils/invert_rxn.jl")
    include("Utils/make_intake_info_compat.jl")
    include("Utils/queries.jl")
    include("Utils/read_mat.jl")
    include("Utils/rxn_str.jl")
    include("Utils/scale_polytope.jl")
    include("Utils/search.jl")
    include("Utils/set_met.jl")
    include("Utils/set_rxn.jl")
    include("Utils/setter.jl")
    include("Utils/similar_rxns.jl")
    include("Utils/sparse.jl")
    include("Utils/split_revs.jl")
    include("Utils/stoi_err.jl")
    include("Utils/summary.jl")
    include("Utils/TNmarginal.jl")
    include("Utils/update.jl")
    include("Utils/utils.jl")
    include("Utils/well_scaled_model.jl")

    # Test
    include("Test/ecoli_core.jl")
    include("Test/lineal_model.jl")
    include("Test/toy_model.jl")

end


# Previous Graff based visualization functions

# Internal transform functions
function projectPose2(renderObject, node::NodeDetailsResponse)::Nothing
    mapEst = node.properties["MAP_est"]
    rotZ = 0
    if "POSE" in node.labels
        rotZ = mapEst[3] # Pose2, otherwise it's a Point2
    end
    trans = Translation(mapEst[1],mapEst[2],0) ∘ LinearMap(RotZ(rotZ))
    settransform!(renderObject, trans)
    return nothing
end

function projectPose3(renderObject, node::NodeDetailsResponse)::Nothing
    mapEst = node.properties["MAP_est"]
     # one day when this changes to quaternions -- for now though Pose3 is using Euler angles during infinite product approximations (but convolutions are generally done on a proper rotation manifold)
     # yaw = convert(q).theta3
    trans = Translation(mapEst[1],mapEst[2],mapEst[3])
    settransform!(renderObject, LinearMap(RotZ(mapEst[6])) ∘ trans)
    return nothing
end

# Callbacks for pose transforms
# TODO -- MAKE OBSOLETE wishlist, use MultipleDispatch instead of global
global poseTransforms = Dict{String, Function}(
    "Pose2" => projectPose2,
    "Pose3" => projectPose3
)

"""
$(SIGNATURES)
Visualize a session using MeshCat.
Return: Nothing.

OBSOLETE, WORK IN PROGRESS ON new unified visualization functions
"""
function visualizeSession(vis::Visualizer,
                          robotId::String,
                          sessionId::String,
                          bigDataImageKey::String = "",
                          pointCloudKey::String = "",
                          dCamModel::Arena.CameraModel = Arena.CameraModel(640, 480, 387.205, [322.042, 238.544])  )::Nothing
    #
    global poseTransforms
    config = getGraffConfig()
    if config == nothing
        error("Graff config is not set, please call setGraffConfig with a valid configuration.")
    end

    # Create a new visualizer instance
    # vis = Visualizer()
    # open(vis)

    # Get the session info
    println("Get the session info for session '$sessionId'...")
    sessionInfo = getSession(robotId, sessionId)
    println("Looking if we have a pose transform for '$(sessionInfo.initialPoseType)'...")
    if isempty(sessionInfo.initialPoseType)
        error("The session doesn't have a specified pose type - please provide a pose type when creating the session with the parameter 'initialPoseType'")
    end
    if !haskey(poseTransforms, sessionInfo.initialPoseType)
        error("Need an explicit transform for '$(sessionInfo.initialPoseType)' to visualize it. Please edit VisualizationService.jl and add a new PoseTransform.")
    end
    println("Good stuff, using it!")
    pose2TransFunc = poseTransforms[sessionInfo.initialPoseType]

    # Retrieve all variables and render them.
    println("Retrieving all variables and rendering them...")
    nodesResponse = getNodes(robotId, sessionId)
    println(" -- Rendering $(length(nodesResponse.nodes)) nodes for session $sessionId for robot $robotId...")
    @showprogress for nSummary in nodesResponse.nodes
        node = getNode(robotId, sessionId, nSummary.id)
        label = node.label

        println(" - Rendering $(label)...")
        if haskey(node.properties, "MAP_est")
            mapEst = node.properties["MAP_est"]

            # Parent triad
            triad = Triad(0.5)
            setobject!(vis[label], triad)
            pose2TransFunc(vis[label], node)

            # Landmark
            if "LANDMARK" in node.labels
                sphere = HyperSphere(Point(0.,0,0), 0.1)
                setobject!(vis[label]["landmark"], sphere, MeshLambertMaterial(color=colorant"blue"))
            end
        else
            @warn "  - Node hasn't been solved, can't really render this one..."
        end
    end
    # Rendering the point clouds and images
    @showprogress for nSummary in nodesResponse.nodes
        node = getNode(robotId, sessionId, nSummary.id)
        label = node.label

        println(" - Rendering $(label)...")
        if haskey(node.properties, "MAP_est")
            mapEst = node.properties["MAP_est"]

            # # Stochastic point clouds
            # if haskey(node.packed, "val")
            #     println(" - Rendering stochastic measurements")
            #     # TODO: Make a lookup as well.
            #     points = map(p -> Point3f0(p[1], p[2], 0), node.packed["val"])
            #     # Make more fancy in future.
            #     # cols = reinterpret(RGB{Float32}, points); # use the xyz value as rgb color
            #     cols = map(p -> RGB{Float32}(1.0, 1.0, 1.0), points)
            #     # pointsMaterial = PointsMaterial(RGB(1., 1., 1.), 0.001, 2)
            #     pointCloud = PointCloud(points, cols)
            #     setobject!(vis[label]["statsPointCloud"], pointCloud)
            # end

            bigEntries = getDataEntries(node)
            # Making a material to set the size
        	material = PointsMaterial(color=RGBA(0,1.0,1.0,0.5),size=0.02)

            if pointCloudKey != "" # Get and render point clouds
                println(" - Rendering point cloud data for keys that have id = $bigDataImageKey...")
                for bigEntry in bigEntries
                    if bigEntry.id == pointCloudKey
                        dataFrame = GraffSDK.getData(node, bigEntry.id)
                        dData = base64decode(dataFrame.data)

                        # Testing: Get the sensor pose
                        sensor = JSON.parse(getRawData(node, "Sensor"))
                        kQi = map(a -> Float64(a), sensor["kQi"]) #Vector{Float64}
                        kTc = (SE3([0,0,0], Quaternion(kQi)))
                        trans = Translation([0,0,0])∘LinearMap(Quat(kTc.R.R))

                        # Form it up.
                        c = reinterpret(UInt16, dData)
                        depths = collect(reshape(c, (640, 480)))
                        pointCloud = cloudFromDepthImage(depths, dCamModel; trans=trans)

                        setobject!(vis[label][pointCloudKey], pointCloud, material)
                    end
                end
            end

            # Camera imagery
            if bigDataImageKey != "" # Get and render big data images and pointclouds
                println(" - Rendering image data for keys that have id = $bigDataImageKey...")
                for bigEntry in bigEntries
                    if bigEntry.id == bigDataImageKey
                        # HyperRectangle until we have sprites
                        box = HyperRectangle(Vec(0,0,0), Vec(0.01, 9.0/16.0/2.0, 16.0/9.0/2.0))
                        dataFrame = GraffSDK.getData(robotId, sessionId, nSummary.id, bigEntry.id)
                        image = PngImage(base64decode(dataFrame.data))

                        # Make an image and put it in the right place.
                        texture = Texture(image=image)
                        material = MeshBasicMaterial(map=texture)
                        trans = Translation(1.0,16.0/9.0/4.0,0) ∘ LinearMap(RotX(pi/2.0))
                        setobject!(vis[label]["camImage"], box, material)
                        settransform!(vis[label]["camImage"], trans)
                    end
                end
            end
        else
            @warn "  - Node hasn't been solved, can't really render this one..."
        end
    end
end

"""
$(SIGNATURES)
Visualize a session using MeshCat.
Return: Nothing.
"""
function visualizeSession(vis::Visualizer; dataImageKey::String = "", pointCloudKey::String = "", dCamModel::Arena.CameraModel = Arena.CameraModel(640, 480, 387.205, [322.042, 238.544]))::Nothing
    config = getGraffConfig()
    if config == nothing
        error("Graff config is not set, please call setGraffConfig with a valid configuration.")
    end

    if config.robotId == "" || config.sessionId == ""
        error("Your config doesn't have a robot or a session specified, please attach your config to a valid robot or session by setting the robotId and sessionId fields. Robot = $(config.robotId), Session = $(config.sessionId)")
    end

    visualizeSession(vis, config.robotId, config.sessionId, dataImageKey, pointCloudKey, dCamModel)
end


# Input binary video
# Output different ids for different sweat glands production
# Start from first frame,
# grow spots frame by frame,
# merge spots if they are too small (sometimes sweat jumps over some pixels)

@inline function kernelvote4(q, mask, output, x, y)
    q0 = @inbounds q[x, y];
    if (@inbounds mask[x, y] == 0) return 0 end

    if (q0 != 0)
        @inbounds output[x, y] = q0;
        return 0;
    end
    kernelvote4inner(q, mask, output, x, y)
end
function sort4(i1, i2, i3, i4)
    # Probably bitonic sort
    j1 = min(i1, i2);
    j2 = max(i1, i2);
    j3 = min(i3, i4);
    j4 = max(i3, i4);
    i2     = max(j1, j3);
    j1     = min(j1, j3);
    i3     = min(j2, j4);
    j4     = max(j2, j4);
    j2 = min(i2, i3);
    j3 = max(i2, i3);
    j1, j2, j3, j4
end
function kernelvote4inner(q, mask, output, x, y)
    w, h = size(q)
    i1 = x < w && mask[x + 1, y + 0] != 0 ? q[x + 1, y + 0] : 0
    i2 = x > 1 && mask[x - 1, y + 0] != 0 ? q[x - 1, y + 0] : 0
    i3 = y < h && mask[x + 0, y + 1] != 0 ? q[x + 0, y + 1] : 0
    i4 = y > 1 && mask[x + 0, y - 1] != 0 ? q[x + 0, y - 1] : 0

    j1, j2, j3, j4 = sort4(i1, i2, i3, i4)
    o = 0;
    if (j1 != 0 && j1 == j2) o = j2;
    elseif (j3 != 0) o = j3;
    elseif (j4 != 0) o = j4;
    end
    if (o != 0)
        output[x, y] = o;
        return 1
    end
    0
end

function _iter(A, mask, tmp)
    # amask = convert(Array, mask)
    # aA = convert(Array, A)
    b1 = A
    b2 = copy!(tmp, A)#copy(A)
    i = 0
    while i < 100
        changed = 0
        for y in axes(A, 2)
            for x in axes(A, 1)
                changed += kernelvote4(b1, mask, b2, x, y)
            end
        end

        if changed == 0
            break
        end
        i += 1
        b1, b2 = b2, b1
    end
    return i, b1
end

function segment_blobs(mask)
    f = Int32.(mask)
    tmp = Matrix{Int32}(undef, size(f, 1), size(f, 2))
    tmp2 = similar(tmp)
    _, clusterid = doclustering!(view(f, :,:,1))
    for frame in 2:size(f, 3)
        #println(frame)
        _, clusterid = doclustering!(view(f, :,:,frame)) #, clusterid)
        #return f[:,:,frame], f[:,:,frame-1]
        matchclusterids!(view(f, :,:,frame), view(f, :,:,frame-1), tmp, tmp2)
    end
    println()
    f
end

function matchclusterids_part1(f2, f1)
    matchclusterids_part1!(similar(f1), f2, f1)
end
function matchclusterids_part1!(f1c, f2, f1)
    #mask = @. Int32(f2 != 0)
    maxid = max(maximum(f2), maximum(f1))
    toosmall, new = computetoosmallandnew(f2, f1, maxid)
    # Remove small clusters from f1
    # Copy new clusters

    newids = Dict{Int, Int}()
    # f1c = similar(f1)
    for i in eachindex(f1c)
        a = f1[i]
        b = f2[i]
        if b != 0
            if new[b]
                c = get!(newids, b, maxid+1)
                if c == maxid+1
                    maxid += one(maxid)
                end
                f1c[i] = c
                # f1c[i] = get!(newids, b) do
                #     maxid += 1
                #     maxid
                # end
            elseif a == 0 || !toosmall[a]
                f1c[i] = a
            else
                f1c[i] = zero(eltype(f1))
            end
        else
            @assert a == 0
        end
    end
    f1c
end

function matchclusterids!(f2, f1, tmp, tmp2)
    f1c = matchclusterids_part1!(tmp, f2, f1)

    #println(sizesnew, sizesold)
    mask = f2
    i, B = _iter(f1c, mask, tmp2)
    print(i, ",") #, ":", count(!iszero, mask)
    copy!(f2, B)
end

# Count the number of pixels in each blob of A
#  # (And the number of pixels that are also in B)
# Id of the largest (too small) blob in B or 0 otherwise.
#  Too small = all but largest
function computetoosmallandnew(f2::AbstractArray{T}, f1::AbstractArray{T}, maxid::T) where T
    @assert maxid < 1_000_000
    toosmall = falses(maxid)
    new = falses(maxid)

    # Create a Dict with new cluster ids -> Dict(old cluster ids -> count)
    dict = [Dict{T, Int}() for i in 1:maxid]
    for (a,b) in zip(f1, f2)
        if a != 0 #&& b != 0
            d = dict[b]
            count = get!(d, a, 0)
            if count <= minimum_blob_size + 2# No need to count more than used in the next for loop
                d[a] = count+1
            end
        end
    end
    # The Vector{Dict} dict could be simplified to two Vector{Int}:s maybe:
    # testi = reduce(vcat, collect.(keys.(dict)))
    # @assert allunique(testi)
    for (i, d) in enumerate(dict)
        if length(d) == 0
            new[i] = true
        else
            count, key = maximum((count, k) for (k, count) in d)
            for (k, v) in d
                if v < minimum_blob_size && (v, k) < (count, key)
                    toosmall[k] = true
                end
            end
        end
    end
    toosmall, new
end