immutable DBNModel
    BN            :: BayesNet
    statsvec      :: Vector{Matrix{Float64}} # each matrix is [r×q], nvar_instantiations × nparent_instantiations
    features      :: Vector{AbstractFeature}
    discretizers  :: Vector{AbstractDiscretizer}
    istarget      :: BitVector # whether a feature is a target feature
end

function Base.print(io::IO, model::DBNModel)

    target_lat = get_target_lat(model)
    target_lon = get_target_lon(model)

    println(io, "DBNModel")
    println(io, "\t", symbol(target_lat), " <- ", map(f->symbol(f), get_indicators_for_target(model, target_lat)))
    println(io, "\t", symbol(target_lon), " <- ", map(f->symbol(f), get_indicators_for_target(model, target_lon)))
end

function dbnmodel{R<:Real, D<:AbstractDiscretizer}(
    BN:: BayesNet,
    statsvec::Vector{Matrix{R}},
    discretizerdict::Dict{Symbol, D},
    targets::Vector{AbstractFeature},
    indicators::Vector{AbstractFeature}
    )

    features = AbstractFeature[symbol2feature(sym) for sym in names(BN)]
    discretizers = AbstractDiscretizer[discretizerdict[sym] for sym in names(BN)]
    istarget = falses(length(features))
    for (i,f) in enumerate(features)
        if in(f, targets)
            istarget[i] = true
        elseif !in(f, indicators)
            error("Feature not in targets or indicators")
        end
    end

    statsvec_float = convert(Vector{Matrix{Float64}}, statsvec)
    DBNModel(BN, statsvec_float, features, discretizers, istarget)
end
function dbnmodel{R<:Real, D<:AbstractDiscretizer}(
    BN::BayesNet,
    statsvec::Vector{Matrix{R}},
    discretizers::Vector{D},
    targets::Vector{AbstractFeature},
    indicators::Vector{AbstractFeature},
    )

    features = AbstractFeature[symbol2feature(sym) for sym in names(BN)]
    istarget = falses(length(features))
    for (i,f) in enumerate(features)
        if in(f, targets)
            istarget[i] = true
        elseif !in(f, indicators)
            error("Feature $(symbol(f)) neither in targets nor indicators")
        end
    end

    statsvec_float = convert(Vector{Matrix{Float64}}, statsvec)
    DBNModel(BN, statsvec_float, features, discretizers, istarget)
end
function dbnmodel{R<:Real, D<:AbstractDiscretizer}(
    adj::BitMatrix, # adj[i,j] = true means i → j
    statsvec::Vector{Matrix{R}},
    discretizerdict::Dict{Symbol, D},
    targets::Vector{AbstractFeature},
    indicators::Vector{AbstractFeature},
    )

    BN = build_bn(statsvec, targets, indicators, adj)
    dbnmodel(BN, statsvec, discretizerdict, targets, indicators)
end
function dbnmodel(modelstats::Dict{AbstractString, Any})

    discretizerdict = modelstats["binmaps"]
    targets    = modelstats["targets"]
    indicators = modelstats["indicators"]
    stats      = modelstats["statistics"]
    adj        = modelstats["adjacency"] # adj[i,j] = true means i → j

    dbnmodel(adj, stats, discretizerdict, convert(Vector{AbstractFeature}, targets),
                        convert(Vector{AbstractFeature}, indicators))
end
function dbnmodel(modelpstats_file::AbstractString)
    emstats = load(modelpstats_file)
    dbnmodel(emstats)
end

function build_bn{R<:Real}(
    statsvec   :: Vector{Matrix{R}},
    targets    :: Vector{AbstractFeature},
    indicators :: Vector{AbstractFeature}, # technically all of the non-target indicators
    adj        :: BitMatrix
    )

    @assert(size(adj,1) == size(adj,2)) # is square

    n_targets = length(targets)
    n_indicators = length(indicators)

    bnnames = Array(Symbol, n_targets + n_indicators)
    for (i,f) in enumerate(targets)
        bnnames[i] = symbol(f)
    end
    for (i,f) in enumerate(indicators)
        bnnames[i+n_targets] = symbol(f)
    end

    @assert(length(unique(bnnames)) == length(bnnames))
    n_nodes = length(bnnames)
    @assert(n_nodes == size(adj, 1) == size(adj, 2))

    BN = BayesNet(bnnames)

    r_arr = Array(Int, n_nodes)
    for (i,node_sym) in enumerate(bnnames)
        stats = statsvec[i]
        r, q = size(stats) # r = num node instantiations, q = num parental instantiations
        states = collect(1:r)
        BN.nodes[i].domain = BayesNets.CPDs.DiscreteDomain(states)
        r_arr[i] = r
    end

    for (node,node_sym) in enumerate(bnnames)

        stats = statsvec[node]
        r, q = size(stats)
        states = collect(1:r)

        stats .+= 1 # NOTE(tim): adding uniform prior
        probabilities = stats ./ sum(stats,1)

        # set any parents & populate probability table
        n_parents = sum(adj[:,node])
        if n_parents > 0
            bnparents = bnnames[adj[:,node]]
            for pa in bnparents
                add_edge!(BN, pa, node_sym)
            end

            # populate probability table
            assignments = BayesNets.assignment_dicts(BN, bnparents)
            # parameterFunction = BayesNets.discrete_parameter_function(assignments, vec(probabilities), r)
            # setCPD!(BN, node_sym, CPDs.Discrete(states, parameterFunction))

            parameterlookup = BayesNets.discrete_parameter_dict(assignments, vec(probabilities), r)
            set_CPD!(BN, node_sym, CPDs.DiscreteDict(states, parameterlookup))
        else
            # no parents
            # setCPD!(BN, node_sym, CPDs.Discrete(states, vec(probabilities)))

            set_CPD!(BN, node_sym, CPDs.DiscreteStatic(states, vec(probabilities)))
        end

    end

    return BN
end
# function build_bn{R<:Real}(
#     statsvec   :: Vector{Matrix{R}},
#     targets    :: Vector{AbstractFeature},
#     indicators :: Vector{AbstractFeature}, # technically all of the non-target indicators
#     adj        :: BitMatrix
#     )

#     @assert(size(adj,1) == size(adj,2)) # is square

#     n_targets = length(targets)
#     n_indicators = length(indicators)

#     bnnames = Array(Symbol, n_targets + n_indicators)
#     for (i,f) in enumerate(targets)
#         bnnames[i] = symbol(f)
#     end
#     for (i,f) in enumerate(indicators)
#         bnnames[i+n_targets] = symbol(f)
#     end

#     @assert(length(unique(bnnames)) == length(bnnames))
#     n_nodes = length(bnnames)
#     @assert(n_nodes == size(adj, 1) == size(adj, 2))

#     BN = BayesNet(bnnames)

#     r_arr = Array(Int, n_nodes)
#     for (node,node_sym) in enumerate(bnnames)
#         stats = statsvec[node]
#         r, q = size(stats) # r = num node instantiations, q = num parental instantiations
#         states = collect(1:r)
#         set_domain!(BN, node_sym, DiscreteDomain(states))
#         r_arr[node] = r
#     end

#     for (node,node_sym) in enumerate(bnnames)

#         stats = statsvec[node]
#         r, q = size(stats)
#         states = collect(1:r)

#         stats .+= 1 # NOTE(tim): adding uniform prior
#         probabilities = stats ./ sum(stats,1)

#         # set any parents & populate probability table
#         n_parents = sum(adj[:,node])
#         if n_parents > 0
#             bnparents = bnnames[adj[:,node]]
#             for pa in bnparents
#                 add_edge!(BN, pa, node_sym)
#             end

#             # populate probability table
#             assignments = BayesNets.assignment_dicts(BN, bnparents)
#             # parameterFunction = BayesNets.discrete_parameter_function(assignments, vec(probabilities), r)
#             # setCPD!(BN, node_sym, CPDs.Discrete(states, parameterFunction))

#             parameterlookup = BayesNets.discrete_parameter_dict(assignments, vec(probabilities), r)
#             setCPD!(BN, node_sym, CPDs.DiscreteDictCPD(states, parameterlookup))
#         else
#             # no parents
#             # setCPD!(BN, node_sym, CPDs.Discrete(states, vec(probabilities)))

#             setCPD!(BN, node_sym, CPDs.DiscreteStaticCPD(states, vec(probabilities)))
#         end

#     end

#     return BN
# end

indexof(f::Symbol, model::DBNModel) = model.BN.name_to_index[f]
indexof(f::AbstractFeature, model::DBNModel) = model.BN.name_to_index[symbol(f)]
is_parent(model::DBNModel, parent::Int, child::Int) = in(parent, in_neighbors(model.BN.dag, child))
is_parent(model::DBNModel, parent::Symbol, child::Symbol) = is_parent(model, model.BN.name_to_index[parent], model.BN.name_to_index[child])
function parent_indeces(varindex::Int, model::DBNModel)
    parent_names = BayesNets.parents(model.BN, names(model.BN)[varindex])
    retval = Array(Int, length(parent_names))
    for (i, name) in enumerate(parent_names)
        retval[i] = model.BN.name_to_index[name]
    end
    retval
end

get_targets(model::DBNModel) = model.features[model.istarget]
function get_target_lat(model::DBNModel, targets::Vector{AbstractFeature}=get_targets(model))
    ind = findfirst(f->isa(f, FeatureTargetLat), targets)
    targets[ind]
end
function get_target_lon(model::DBNModel, targets::Vector{AbstractFeature}=get_targets(model))
    ind = findfirst(f->isa(f, FeatureTargetLon), targets)
    targets[ind]
end

AutomotiveDrivingModels.get_indicators(model::DBNModel) = model.features[!model.istarget]
function get_indicators_for_target(model::DBNModel, i::Int)
    indicator_indeces = parent_indeces(i, model)
    model.features[indicator_indeces]
end
function get_indicators_for_target(model::DBNModel, target::AbstractFeature)
    i = indexof(target, model)
    get_indicators_for_target(model, i)
end
get_indicators_lat(model::DBNModel) = get_indicators_for_target(model, get_target_lat(model))
get_indicators_lon(model::DBNModel) = get_indicators_for_target(model, get_target_lon(model))

function find_target_indeces{F<:AbstractFeature}(targetset::ModelTargets, features::Vector{F})
    ind_lat = findfirst(f->f==targetset.lat, features)
    ind_lon = findfirst(f->f==targetset.lon, features)
    @assert(ind_lat  != 0)
    @assert(ind_lon  != 0)
    (ind_lat, ind_lon)
end

get_num_vertices(model::DBNModel) = nv(model.BN.dag)
get_num_edges(model::DBNModel) = ne(model.BN.dag)

get_total_sample_count(model::DBNModel) = sum(model.statsvec[1])
function get_bin_counts(model::DBNModel)
    n_nodes = get_num_vertices(model)
    r_arr = Array(Int, n_nodes)
    for i = 1 : n_nodes
        r_arr[i] = size(model.statsvec[i], 1)
    end
    r_arr
end
function get_marginal_probability(model::DBNModel, varindex::Int)
    binprobs = vec(sum(model.statsvec[varindex], 2))
    binprobs ./= sum(binprobs)
end
function get_counts_for_assignment(
    model::DBNModel,
    targetind::Int,
    parentindeces::Vector{Int},
    parentassignments::Vector{Int},
    bincounts::Vector{Int}
    )

    dims = tuple(bincounts[parentindeces]...)
    subs = tuple(parentassignments...)
    j = sub2ind(dims, subs...)
    copy(model.statsvec[targetind][:,j])
end
function get_counts_for_assignment(
    model::DBNModel,
    targetind::Int,
    assignments::Dict{Symbol, Int},
    bincounts::Vector{Int}
    )

    parentindeces = parent_indeces(targetind, model)
    nparents = length(parentindeces)
    parentassignments = Array(Int, nparents)
    for (i,ind) in enumerate(parentindeces)
        parentassignments[i] = assignments[model.BN.nodes[ind].name]
    end
    get_counts_for_assignment(model, targetind, parentindeces, parentassignments, bincounts)
end

function encode!(assignment::Dict{Symbol,Int}, model::DBNModel, observations::Dict{Symbol,Float64})
    # take each observation and bin it appropriately
    # returns a Dict{Symbol,Int}

    # TODO(tim): ensure order is correct
    for (i,istarget) in enumerate(model.istarget)
        if !istarget
            sym = model.BN.nodes[i].name
            val = observations[sym]
            try
                encode(model.discretizers[i], val)
            catch
                println("failed to encode ", sym, " of value ", val, "  ", model.discretizers[i])
            end
            assignment[sym] = encode(model.discretizers[i], val)
        end
    end
    assignment
end
function sample!(model::DBNModel, assignment::Dict{Symbol, Int}, ordering::Vector{Int}=topological_sort_by_dfs(model.BN.dag))
    #=
    Run through nodes in topological order, building the instantiation vector as we go
    We use nodes we already know to condition on the distribution for nodes we do not
    Modifies assignment to include newly sampled symbols
    =#

    # for name in model.BN.names[ordering]
    #     if !haskey(assignment, name)
    #         assignment[name] = BayesNets.rand(BayesNets.cpd(model.BN, name), assignment)
    #     end
    # end

    for node in model.BN.nodes[ordering]
        name = node.name
        cpd = BayesNets.cpd(model.BN, name)

        p = BayesNets.probvec(cpd, assignment)
        r = rand()
        i = 1
        p_tot = 0.0
        while p_tot + p[i] < r && i < length(p)
            p_tot += p[i]
            i += 1
        end
        assignment[name] = cpd.domain[i]
    end

    assignment
end
function sample_unset!(model::DBNModel, assignment::Dict{Symbol, Int}, ordering::Vector{Int}=topological_sort_by_dfs(model.BN.dag); missing_value::Int=-1)
    #=
    Run through nodes in topological order, building the instantiation vector as we go
    We use nodes we already know to condition on the distribution for nodes we do not
    Modifies assignment to include newly sampled symbols
    Only sample values set to missing_value
    =#

    for node in model.BN.nodes[ordering]

        name = node.name

        if get(assignment, name, missing_value) == missing_value

            cpd = BayesNets.cpd(model.BN, name)

            p = BayesNets.probvec(cpd, assignment)
            r = rand()
            i = 1
            p_tot = 0.0
            while p_tot + p[i] < r && i < length(p)
                p_tot += p[i]
                i += 1
            end
            assignment[name] = cpd.domain[i]
        end
    end

    assignment
end
function sample_and_logP!(
    model::DBNModel,
    assignment::Dict{Symbol, Int},
    logPs::Dict{Symbol, Float64},

    ordering::Vector{Int}=topological_sort_by_dfs(model.BN.dag),
    )

    for node in model.BN.nodes[ordering]
        name = node.name

        cpd = BayesNets.cpd(model.BN, name)

        p = BayesNets.probvec(cpd, assignment)

        r = rand()
        i = 1
        p_tot = 0.0
        while p_tot + p[i] < r && i < length(p)
            p_tot += p[i]
            i += 1
        end

        assignment[name] = cpd.domain[i]
        logPs[name] = log(p[i])
    end

    logPs
end
function calc_log_probability_of_assignment(model::DBNModel, assignment::Dict{Symbol, Int}, sym::Symbol)
    # returns the discrete log probability of the given bin assignment
    cpd = BayesNets.cpd(model.BN, sym)
    b = assignment[sym]
    p = cpd.parameterFunction(assignment)
    log(p[b])
end
function calc_probability_distribution_over_assignments!(
    dest::Vector{Float64},
    model::DBNModel,
    assignment::Dict{Symbol, Int},
    target::Symbol
    )

    # NOTE (tim): cpd.parameterFunction(assignment) returns the actual probability vector, not a copy
    cpd = BayesNets.cpd(model.BN, target)
    copy!(dest, BayesNets.probvec(cpd, assignment))
    @assert(abs(sum(dest)-1.0) < 0.0001) # probability sums to 1.0
    dest
end
function calc_probability_for_uniform_sample_from_bin(
    discrete_prob::Float64,
    ::CategoricalDiscretizer, # disc
    ::Int # bin
    )

    discrete_prob
end
function calc_probability_for_uniform_sample_from_bin(
    discrete_prob::Float64,
    disc::LinearDiscretizer,
    bin::Int
    )

    # widthtot = totalwidth(disc)
    widthbin = binwidth(disc, bin)

    discrete_prob / widthbin # * widthtot
end
function calc_probability_for_uniform_sample_from_bin(
    discrete_prob::Float64,
    disc::HybridDiscretizer,
    bin::Int
    )

    if bin ≤ disc.lin.nbins
        calc_probability_for_uniform_sample_from_bin(discrete_prob, disc.lin, bin)
    else
        discrete_prob
    end
end

function export_to_text(model::DBNModel, filename::AbstractString)
    # export a bayesian network to an encounter definition file

    n = model.n # the number of nodes

    open(filename, "w") do fout

        # Labels: the node names
        ###############################################
        println(fout, "# labels")
        for i = 1 : n
            @printf(fout, "\"%s\"", string(model.labels[i]))
            if i < n
                @printf(fout, ", ")
            else
                @printf(fout, "\n")
            end
        end

        # G: the graphical structure
        ##################################################
        println(fout, "# G: graphical structure")
        for i = 1 : n
            for j = 1 : n
                @printf(fout, "%c ", model.G[i,j] ? '1' : '0')
            end
            @printf(fout, "\n")
        end

        # r: number of bins for each variable
        ##################################################
        println(fout, "# r: number of bins")
        for i = 1 : n
            @printf(fout, "%d ", model.r[i])
        end
        @printf(fout, "\n")

        # N: the sufficient statistics, in integer form
        ##################################################
        println(fout, "# N: sufficient statistics")
        for i = 1 : n
            stats = model.N[i]
            r, q = size(stats)
            for b = 1:q, a = 1:r
                @printf(fout, "%d ", stats[a,b])
            end
        end
        @printf(fout, "\n")

        # discretizers, '*' if discrete
        ##################################################
        @printf(fout, "# discretizers\n")
        for i = 1 : n
            bmap = model.discretizers[i]

            if isa(bmap, CategoricalDiscretizer)
                @printf(fout, "*")
            elseif isa(bmap, LinearDiscretizer)
                for edge in bmap.binedges
                    @printf(fout, "%f ", edge)
                end
            elseif isa(bmap, HybridDiscretizer)
                for edge in bmap.lin.binedges
                    @printf(fout, "%f ", edge)
                end
            else
                error("invalid bmap type $(typeof(bmap))")
            end
            @printf(fout, "\n")
        end
    end
end
function print_structure(model::DBNModel)
    target = get_target_lat(model)
    println("target lat: ", symbol(target))
    for indicator in get_indicators_lat(model, target)
        println("\t", symbol(indicator))
    end

    target = get_target_lon(model)
    println("target lon: ", symbol(target))
    for indicator in get_indicators_lon(model, target)
        println("\t", symbol(indicator))
    end
end

function construct_model_adjacency_matrix(
    net_ind_lat :: Int,
    net_ind_lon :: Int,
    net_parent_indices_lat :: Vector{Int},
    net_parent_indices_lon :: Vector{Int},
    num_net_features :: Int
    )

    # [parent_index, child_index] = true for parent -> child edges

    adj = falses(num_net_features, num_net_features)

    adj[net_parent_indices_lat, net_ind_lat] = true
    adj[net_parent_indices_lon, net_ind_lon] = true

    adj
end


import Base: copy, copyto!, ==
import Random: rand, seed!
import RandomNumbers: AbstractRNG, gen_seed, seed_type, unsafe_copyto!, unsafe_compare

"""
```julia
AbstractXorshift1024 <: AbstractRNG{UInt64}
```

The base abstract type for `Xorshift1024`, `Xorshift1024Star` and `Xorshift1024Plus`.
"""
abstract type AbstractXorshift1024 <: AbstractRNG{UInt64} end

for (star, plus) in (
        (false, false),
        (false, true),
        (true, false),
    )
    rng_name = Symbol(string("Xorshift1024", star ? "Star" : plus ? "Plus" : ""))
    @eval begin
        mutable struct $rng_name <: AbstractXorshift1024
            s0::UInt64
            s1::UInt64
            s2::UInt64
            s3::UInt64
            s4::UInt64
            s5::UInt64
            s6::UInt64
            s7::UInt64
            s8::UInt64
            s9::UInt64
            s10::UInt64
            s11::UInt64
            s12::UInt64
            s13::UInt64
            s14::UInt64
            s15::UInt64
            p::Int
        end

        function $rng_name(seed::NTuple{16, UInt64}=gen_seed(UInt64, 16))
            o = zero(UInt64)
            r = $rng_name(
                o, o, o, o, o, o, o, o,
                o, o, o, o, o, o, o, o,
                zero(Int)
            )
            seed!(r, seed)
            r
        end

        function $rng_name(seed::Integer)
            $rng_name(init_seed(seed, UInt64, 16))
        end

        @inline function xorshift_next(r::$rng_name)
            ptr = Ptr{UInt64}(pointer_from_objref(r))
            p = r.p
            s0 = unsafe_load(ptr, p + 1)
            p = (p + 1) % 16
            s1 = unsafe_load(ptr, p + 1)
            s1 ⊻= s1 << 31
            s1 ⊻= s1 >> 11
            s1 ⊻= s0 >> 30
            s1 ⊻= s0
            unsafe_store!(ptr, s1, p + 1)
            r.p = p
            $(star ? :(s1 * 2685821657736338717) :
              plus ? :(s1 + s0) : :s1)
        end
    end
end

@inline seed_type(::Type{T}) where T <: AbstractXorshift1024 = NTuple{16, UInt64}

function copyto!(dest::T, src::T) where T <: AbstractXorshift1024
    unsafe_copyto!(dest, src, UInt64, 16)
    dest.p = src.p
    dest
end

copy(src::T) where T <: AbstractXorshift1024 = copyto!(T(), src)

==(r1::T, r2::T) where T <: AbstractXorshift1024 = unsafe_compare(r1, r2, UInt64, 16) && r1.p == r2.p

function seed!(r::AbstractXorshift1024, seed::NTuple{16, UInt64})
    all(==(0), seed) && error("0 cannot be the seed")
    ptr = Ptr{UInt64}(pointer_from_objref(r))
    @inbounds for i in 1:16
        unsafe_store!(ptr, seed[i], i)
    end
    r.p = 0
    for i in 1:16
        xorshift_next(r)
    end
    r
end

function seed!(r::AbstractXorshift1024, seed::Integer...)
    l = length(seed)
    @assert 0 ≤ l ≤ 16
    if l == 0
        return seed!(r, gen_seed(UInt64, 16))
    end
    seed = [x % UInt64 for x in seed]
    while length(seed) < 16
        push!(seed, splitmix64(seed[end]))
    end
    seed!(r, Tuple(seed))
end

@inline rand(r::AbstractXorshift1024, ::Type{UInt64}) = xorshift_next(r)


module LULESH

using CEnum
using CUDA
using MPI
using Parameters
using Printf
using Random

abstract type AbstractDomain end
# Device vector types
const VD{T} = Union{Vector{T}, CuVector{T}} where T

# Index type
const IndexT = Int
const MAX_FIELDS_PER_MPI_COMM = 6

struct LuleshProblem
    num_iters::Int
    structured::Bool
    nx::Int
    nr::Int
    balance::Int
    cost::Int
    col::Int
    row::Int
    plane::Int
    side::Int
    devicetype
    floattype
    comm::Union{MPI.Comm, Nothing}
    function LuleshProblem(num_iters, structured, nx, nr, balance, cost, devicetype, floattype, comm)
        !MPI.Initialized() && MPI.Init()
        col, row, plane, side = InitMeshDecomp(comm)
        return new(num_iters, structured, nx, nr, balance, cost, col, row, plane, side, devicetype, floattype, comm)
    end
end

include("bc.jl")
include("types.jl")
include("domain.jl")
include("mpi.jl")
include("utils.jl")

export printUsage, IndexT, Domain, LuleshProblem, getMyRank, getNumRanks, getWtime, lagrangeLeapFrog, comm_max

function InitMeshDecomp(comm)
    # Assume cube processor layout for now
    numRanks = getNumRanks(comm)
    myRank = getMyRank(comm)
    testProcs = floor(cbrt(numRanks+0.5))
    @show testProcs, numRanks
    if (testProcs*testProcs*testProcs != numRanks)
        error("Num processors must be a cube of an integer (1, 8, 27, ...)")
    end
    @show typeof(testProcs)
    # TODO This is not good for the padding
    if (MAX_FIELDS_PER_MPI_COMM > getCacheCoherencePad(testProcs))
        error("corner element comm buffers too small. MAX_FIELDS_PER_MPI_COMM > CACHE_COHERENCE_PAD_REAL ($MAX_FIELDS_PER_MPI_COMM > $(getCacheCoherencePad(testProcs))")
    end
    testProcs = convert(Int, testProcs)
    dx = convert(Int, testProcs)
    dy = convert(Int, testProcs)
    dz = convert(Int, testProcs)

    # temporary test
    if dx*dy*dz != numRanks
        error("error -- must have as many domains as procs\n") ;
    end
    remainder = (dx*dy*dz) % numRanks
    myDom = Int(0)
    if (myRank < remainder)
        myDom = myRank*( 1+ (div(dx*dy*dz, numRanks)))
    else
      myDom = remainder*( 1+ (div(dx*dy*dz, numRanks))) +
         (myRank - remainder)*(div(dx*dy*dz, numRanks))
    end
   @show typeof(myDom)
   col = myDom % dx
   row = div(myDom, dx) % dy
   plane = div(myDom, (dx*dy))
   side = testProcs

   return col, row, plane, side
end
export InitMeshDecomp
end # module


"""
Common functionality to simulate any physical model.
"""
module CPIMC
using StaticArrays
using DataStructures
using FixedPointNumbers
import LinearAlgebra: dot, norm
using OnlineStats
using Printf

export Group, Mean, Variance, Fixed

include("configuration.jl")
include("ensemble.jl")
include("model.jl")

include("Estimators.jl")
include("PlaneWaves.jl")
include("DefaultUpdates.jl")
include("UniformElectronGas.jl")

export UpdateCounter, sweep!, print_results, print_rates, Step, apply_step, apply_step!, apply_back_step!, update!, measure!

"""
Objects of this type are used to keep track of the acceptance ratio of a class of updates.

**Fields**
- `rejected` -- number of times updates have been rejected
- `accepted` -- number of times updates have been accepted
- `trivial`  -- number of times the changes proposed by updates of that class have been trivial, e.g. the Step returned by update has been empty

For convenience, an outer constructor with all counters set to zero is provided:

    UpdateCounter() = UpdateCounter(0,0,0)

"""
mutable struct UpdateCounter
    rejected :: Int64
    accepted :: Int64
    trivial  :: Int64
end

UpdateCounter() = UpdateCounter(0,0,0)

"""
    Base.:+(x::UpdateCounter, y::UpdateCounter)

Addition of counters for example to add counters from different Markov-chains.
"""
function Base.:+(x::UpdateCounter, y::UpdateCounter)
    UpdateCounter(x.rejected + y.rejected, x.accepted + y.accepted, x.trivial + y.trivial)
end

"""
    Diff = Union{Nothing,Tuple}

Type alias for Union{Nothing,Tuple}. This is used as type parameters for the fields of struct Step.
Mainly for convenient construction of any combination of ::Nothing, ::Tuple.
"""
const Diff = Union{Nothing,Tuple}
Diff(::Nothing) = nothing

"""
Represents a change which can be applied to a `Configuration`.

**Fields**

- `drop_orbs`  -- `Orbital`s which are to be removed from the initial occupation
- `drop_kinks` -- pairs of `ImgTime` and `Kink`s which are to be removed
- `add_orbs`   -- `Orbital`s which are added to the initial occupation
- `add_kinks`  -- pairs of `ImgTime` and `Kinks` which are added

"""
struct Step{T<:Diff, S<:Diff, R<:Diff, Q<:Diff}
    drop_orbs :: T
    drop_kinks :: S
    add_orbs :: R
    add_kinks :: Q
end

# outer constructor method for an empty Step
Step() = Step(nothing,nothing,nothing,nothing)
Step(a,b) = Step(a,nothing,b,nothing)

"""
    apply_step!(c::Configuration, step::Step)
    apply_step!(c::Configuration, steps)

Apply the changes given by the second argument to a `Configuration`.
"""
function apply_step!(c::Configuration, s::Step)
    drop_orbs!(c.occupations, s.drop_orbs)
    drop_kinks!(c.kinks, s.drop_kinks)
    add_orbs!(c.occupations, s.add_orbs)
    add_kinks!(c.kinks, s.add_kinks)
end

function apply_back_step!(c::Configuration, s::Step)
    drop_orbs!(c.occupations, s.add_orbs)
    drop_kinks!(c.kinks, s.add_kinks)
    add_orbs!(c.occupations, s.drop_orbs)
    add_kinks!(c.kinks, s.drop_kinks)
end

function apply_step!(c::Configuration, steps::Array)
    for step in steps
        apply_step!(c, step)
    end
end

function apply_back_step!(c::Configuration, steps::Array)
    for i in 0:(length(steps)-1)
        apply_back_step!(c, steps[length(steps)-i])
    end
end

"""
    apply_step(c::Configuration, step::Step)
    apply_step(c::Configuration, steps)

Return the result of applying the changes given by the second argument to a `Configuration`.
"""
function apply_step(c::Configuration, steps)
    c1 = deepcopy(c)
    apply_step!(c1,steps)
    return c1
end

"""
    update!(m::Model, e::Ensemble, c::Configuration, updates)

Obtain the next state of the Markov chain by randomly selecting an element of `updates`
and applying the proposed changes to `c` with the calculated probability. Returns a tuple
containing the index of the chosen function and `:accept`, `:reject` or `:trivial` if
the function yielded an empty `Step`.
"""
function update!(m::Model, e::Ensemble, c::Configuration, updates)
    @assert !isempty(updates)
    i = rand(1:length(updates))
    dv, Δ = updates[i](m, e, c)

    if Δ == Step()
        return (i, :trivial)
    end

    if rand() < dv
        return (i, :accept)
    else
        apply_back_step!(c, Δ)
        return (i, :reject)
    end
end

"""
    measure!(m::Model, e::Ensemble, c::Configuration, estimators)

Perform measurements on a `Configuration`. `estimators` needs to be a dictionary that contains tuples `(::OnlineStat, ::Function)`.
"""
function measure!(m, e, c, estimators)
    s = signum(m, c)
    for (key, (stat, obs)) in estimators
        if in(key, [:sign, :K, :T1c])
            fit!(stat, obs(m,e,c))
        else
            fit!(stat, obs(m,e,c)*s)
        end
    end
end

"""
    sweep!(m::Model, e::Ensemble, c::Configuration, updates, estimators, steps::Int, sampleEvery::Int, throwAway::Int)

Generate a markov chain of length `steps` using the Metropolis-Hastings algorithm with the updates given in `updates`.
After `throwAway` steps have been performed, the observables given in `estimators` are calculated every `sampleEvery` steps.

Returns a dictionary containing an `UpdateCounter` for every function given in `updates`.
"""
function sweep!(m::Model, e::Ensemble, c::Configuration, updates, estimators, steps::Int, sampleEvery::Int, throwAway::Int)
    counters = [ UpdateCounter() for u in updates ]

    if (Threads.threadid() == 1)
        println("\nstarting equilibration")
    end

    k = 1 # progress counter
    for i in 1:throwAway
        # print progress
        if i % (throwAway/100) == 0
            print("⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀    "^(Threads.threadid()-1), "eq:",Threads.threadid(), "T",lpad(k,3, ' '),"%","K:",lpad(length(c.kinks),4, ' '), "\r")
            k += 1
        end
        update!(m, e, c, updates)
    end
    if (Threads.threadid() == 1)
        println("\nstarting simulation")
    end

    i = 0
    k = 1 # progress counter

    while i < steps
        # print progress
        if i % (steps/100) == 0
            print("⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀    "^(Threads.threadid()-1),"   ",Threads.threadid(), "T",lpad(k,3, ' '),"%","K:",lpad(length(c.kinks),4, ' '), "\r")
            k+=1
        end

        " MC step "
        (up, res) = update!(m, e, c, updates)

        if res == :accept
            counters[up].accepted += 1
        elseif res == :reject
            counters[up].rejected += 1
        elseif res == :trivial
            counters[up].trivial += 1
        end

        " calculate estimators "
        if i % sampleEvery == 0
            measure!(m, e, c, estimators)
        end

        i += 1
    end
    println("\nthread ",Threads.threadid()," finished")
    update_counters_dict = Dict(map(u -> typeof(u).name.mt.name => UpdateCounter(), updates))
    for i in 1:length(updates)
        update_counters_dict[typeof(updates[i]).name.mt.name] += counters[i]
    end
    return update_counters_dict#Dict(zip(map(u -> typeof(u).name.mt.name, updates), counters))
end

"""
    print_results(measurements, e::Ensemble)

Print all measured observables, potentially taking the sign into account.
"""
function print_results(measurements, e::Ensemble)
    println("measurements:")
    println("=============")

    # calculate average sign
    if haskey(measurements, :sign)
        avg_sign = mean(first(measurements[:sign]))
    else
        avg_sign = 1.0
    end

    for (k,(f,m)) in measurements
        if typeof(f) == Variance{Float64,Float64,EqualWeight}
            if in(k,[:sign, :K])
                println(k, "\t", mean(f), " +/- ", std(f)/sqrt(f.n - 1))
            else
                if typeof(f) == Variance{Float64,Float64,EqualWeight}
                    println(k, "\t", mean(f)/avg_sign, " +/- ", std(f)/sqrt(f.n - 1)/avg_sign)
                elseif typeof(f).name == typeof(Group()).name
                    println(string(k))
                    println("-------------")
                    println("means : $(mean.(f.stats) ./ avg_sign)")
                    println("errors : $(std.(f.stats) ./ sqrt(f.n - 1) ./ avg_sign))")
                    println("-------------")
                end
            end
        end
    end
end

"""
    print_rates(dict::Dict{Any,UpdateCounter})

Calculate and print acceptance statistics.
"""
function print_rates(dict)
    long = 31

    println("\n")
    println(rpad("update", long), lpad("accepted", 11), lpad("rejected", 11), lpad("trivial", 11))
    println("================================================================")
    println("\ntotal:\n")

    for (up,cn) in dict

        println(rpad(up, long), lpad(cn.accepted, 11), lpad(cn.rejected, 11), lpad(cn.trivial, 11))
    end
    overall_counters = sum(values(dict))
    println(rpad("all", long), lpad(overall_counters.accepted, 11), lpad(overall_counters.rejected, 11), lpad(overall_counters.trivial, 11))
    println("\nrelative:\n")

    for (up,cn) in dict
        proposed = cn.accepted + cn.rejected + cn.trivial
        println(rpad(up, long),
                lpad(@sprintf("%.2f", 100*cn.accepted/proposed), 10), "%",
                lpad(@sprintf("%.2f", 100*cn.rejected/proposed), 10), "%",
                lpad(@sprintf("%.2f", 100*cn.trivial/proposed), 10), "%")
    end
    proposed = overall_counters.accepted + overall_counters.rejected + overall_counters.trivial
    println(rpad("all", long),
            lpad(@sprintf("%.2f", 100*overall_counters.accepted/proposed), 10), "%",
            lpad(@sprintf("%.2f", 100*overall_counters.rejected/proposed), 10), "%",
            lpad(@sprintf("%.2f", 100*overall_counters.trivial/proposed), 10), "%")
    println("\n\n")
end

end


"""
    common.jl

Description:
    Includes all of the unsupervised ARTMAP modules common code.
"""

# -------------------------------------------
# Methods
# -------------------------------------------

"""
    train!(art::ARTMAP, x::RealMatrix, y::IntegerVector, preprocessed::Bool=false)

Train the ARTMAP model on a batch of data 'x' with supervisory labels 'y.'

# Arguments
- `art::ARTMAP`: the supervised ARTMAP model to train.
- `x::RealMatrix`: the 2-D dataset containing columns of samples with rows of features.
- `y::IntegerVector`: labels for supervisory training.
- `preprocessed::Bool=false`: flag, if the data has already been complement coded or not.
"""
function train!(art::ARTMAP, x::RealMatrix, y::IntegerVector, preprocessed::Bool=false)
    # Show a message if display is on
    art.opts.display && @info "Training $(typeof(art))"

    # Data information and setup
    n_samples = length(y)

    # Run the batch initialization procedure
    x = init_train!(x, art, preprocessed)

    # Initialize the output vector
    y_hat = zeros(Int, n_samples)
    # Learn until the stopping conditions
    art.epoch = 0
    while true
        # Increment the epoch and get the iterator
        art.epoch += 1
        iter = get_iterator(art.opts, x)
        for i = iter
            # Update the iterator if necessary
            update_iter(art, iter, i)
            # Grab the sample slice
            # sample = get_sample(x, i)
            sample = x[:, i]
            label = y[i]
            # Train upon the sample and label
            y_hat[i] = train!(art, sample, label, preprocessed=true)
        end

        # Check stopping conditions
        if stopping_conditions(art)
            break
        end
    end
    return y_hat
end # train!(art::ARTMAP, x::RealMatrix, y::IntegerVector, preprocessed::Bool=false)

# -------------------------------------------
# Common Documentation
# -------------------------------------------

@doc raw"""
    train!(art::ARTMAP, x::RealVector, y::Integer ; preprocessed::Bool=false)

Train the supervised ARTMAP model on a single sample of features 'x' with supervisory label 'y'.

# Arguments
- `art::ARTMAP`: the supervised ART model to train.
- `x::RealVector`: the single sample feature vector to train upon.
- `y::Integer`: the label for supervisory training.
- `preprocessed::Bool=false`: optional, flag if the data has already been complement coded or not.
"""
train!(art::ARTMAP, x::RealVector, y::Integer ; preprocessed::Bool=false)


# http://www.neos-server.org/neos/NEOS-API.html
module NEOS

warn("Make sure you comply with the NEOS terms of use: http://www.neos-server.org/neos/termofuse.html")

using LightXML
using Requests
using Codecs
using Libz
using AmplNLWriter

importall MathProgBase.SolverInterface

include("NEOSServer.jl")
include("NEOSSolver.jl")
include("NEOSSolverInterface.jl")
include("MPSWriter.jl")
include("nl.jl")
include("mps.jl")

include("solvers/CPLEX_MPS.jl")
include("solvers/MOSEK_MPS.jl")
include("solvers/SYMPHONY_MPS.jl")
include("solvers/FICOXpress_MPS.jl")
include("solvers/CPLEX_NL.jl")

export NEOSServer, NEOSSolver,
	NEOSCPLEXSolver, NEOSMOSEKSolver, NEOSSYMPHONYSolver, NEOSXpressSolver,
	addparameter!, addemail!,

 	# NEOS API functions
	neosHelp, emailHelp, welcome, version, ping, printQueue,
	listAllSolvers, listCategories,
	getSolverTemplate,
	listSolversInCategory,
	submitJob,
	getJobStatus, killJob, getFinalResults, getFinalResultsNonBlocking,
	getJobInfo,
	getIntermediateResults, getIntermediateResultsNonBlocking
end


## Gradient arithmetic
import Zygote
import Base.+, Base.*, Base.-, Base./

# Add Grads together
function +(G1::Zygote.Grads, G2::Zygote.Grads)
    temp = G1
    for p in G1.params
        temp.grads[p] .+= G2.grads[p]
    end
    return temp
end

# Subtract Grads from each other
function -(G1::Zygote.Grads, G2::Zygote.Grads)
    temp = G1
    for p in G1.params
        temp.grads[p] .-= G2.grads[p]
    end
    return temp
end

# Scalar multiply Grads
function *(G::Zygote.Grads, n::Number)
    temp = G
    for p in G.params
        temp.grads[p] .*= n
    end
    return temp
end

# Scalar divide Grads
function /(G::Zygote.Grads, n::Number)
    temp = G
    for p in G.params
        temp.grads[p] ./= n
    end
    return temp
end


mutable struct LockedMinHeap{Heap,GetPriority,Priority}
    @const heap::Heap
    @const getpriority::GetPriority
    @atomic priority::Priority
    @atomic locked::Bool
end

lockedheap(
    ::Type{Eltype},
    ::Type{Priority} = Eltype;
    by = identity,
) where {Eltype,Priority} = LockedMinHeap(
    MutableBinaryHeap{Eltype}(Base.Order.By(by), EMPTY_VECTOR),
    by,
    typemax(Priority),
    false,
)

Base.eltype(h::LockedMinHeap) = eltype(typeof(h))
Base.eltype(::Type{LockedMinHeap{Heap}}) where {Heap} = eltype(Heap)

prioritytype(x) = prioritytype(typeof(x))
prioritytype(T::Type) = error("not defined for type ", T)
prioritytype(::Type{<:LockedMinHeap{<:Any,<:Any,Priority}}) where {Priority} = Priority

function Base.trylock(h::LockedMinHeap)
    if @atomic :monotonic h.locked
        return false
    end
    old = @atomicswap h.locked = true
    return !old
end

function Base.unlock(h::LockedMinHeap)
    @atomic h.locked = false
    return
end

function Base.push!(h::LockedMinHeap, x)
    @DBG @check h.getpriority(x) != typemax(prioritytype(h))
    push!(h.heap, x)
    @atomic :monotonic h.priority = h.getpriority(first(h.heap))
    return h
end

function Base.pop!(h::LockedMinHeap)
    x = pop!(h.heap)
    if isempty(h.heap)
        p = typemax(prioritytype(h))
    else
        p = h.getpriority(first(h.heap))
    end
    @atomic :monotonic h.priority = p
    return x
end

struct MultiQueue{Heap<:LockedMinHeap}
    heaps::Vector{Heap}
end

Base.eltype(q::MultiQueue) = eltype(typeof(q))
Base.eltype(::Type{MultiQueue{Heap}}) where {Heap} = eltype(Heap)

prioritytype(::Type{MultiQueue{Heap}}) where {Heap} = prioritytype(Heap)

function Base.push!(multiq::MultiQueue, x)
    nspins = 0
    while true
        nspins += 1
        h = rand(multiq.heaps)
        if trylock(h)
            try
                push!(h, x)
                return h
            finally
                unlock(h)
            end
        end
        if nspins > 1000
            errorwith(; multiq = multiq, x = x) do io, (; multiq, x)
                print(io, "Failed to push an element `", x, "` to ")
                show(io, MIME"text/plain"(), multiq)
            end
        end
    end
end

struct Contention end
struct LikelyEmpty end

function trypop!(multiq::MultiQueue)
    nempties = 0
    while true
        h1 = rand(multiq.heaps)
        h2 = rand(multiq.heaps)
        p1 = @atomic :monotonic h1.priority
        p2 = @atomic :monotonic h2.priority
        if p2 < p1
            p1, p2 = p2, p1
            h1, h2 = h2, h1
        end
        if p1 == typemax(prioritytype(multiq))
            nempties += 1
            if nempties > length(multiq.heaps)
                break
            else
                continue
            end
        end
        if trylock(h1)
            try
                p1 = @atomic :monotonic h1.priority
                if p1 == typemax(prioritytype(multiq))
                    continue
                end
                return Some{eltype(h1)}(pop!(h1))
            finally
                unlock(h1)
            end
        end
        # nempties = 0
    end

    lockfailed = false
    for h in multiq.heaps
        p = @atomic :monotonic h.priority
        if p == typemax(prioritytype(multiq))
            continue
        end
        if trylock(h)
            try
                p = @atomic :monotonic h.priority
                if p == typemax(prioritytype(multiq))
                    continue
                end
                return Some{eltype(h)}(pop!(h))
            finally
                unlock(h)
            end
        else
            lockfailed = true
        end
    end
    if lockfailed
        return Contention()
    else
        return LikelyEmpty()
    end
end


### Utility Functions needed for the construction of the Power System, mostly used for consistency checking ####

## Check that all the buses have a type defintion ##

function buscheck(buses)
    for b in buses
        if isnothing(b.bustype)
            @warn "Bus/Nodes data does not contain information to build an a network"
        end
    end
end

## Slack Bus Definition ##

function slack_bus_check(buses)
    slack = -9
    for b in buses
        if b.bustype == BusTypes.REF
            slack = b.number
            break
        end
    end
    if slack == -9
        @error "Model doesn't contain a slack bus"
    end
end

# TODO: Check for islanded Buses

# check for minimum timediff
function minimumtimestep(time_series::Array{T}) where {T <: TimeSeriesData}
    if length(time_series[1].data) > 1
        timeseries = time_series[1].data
        n = length(timeseries) - 1
        ts = []
        for i in 1:n
            push!(
                ts,
                TimeSeries.timestamp(timeseries)[n + 1] -
                TimeSeries.timestamp(timeseries)[n],
            )
        end
        return minimum(ts)
    else
        ts = Dates.Dates.Minute(1)
        return ts
    end
end

function critical_components_check(sys::System)
    critical_component_types = [Bus, Generator, ElectricLoad]
    for component_type in critical_component_types
        components = get_components(component_type, sys)
        if length(components) == 0
            @warn "There are no $(component_type) Components in the System"
        end
    end
end

"""
    adequacy_check(sys::System)

Checks the system for sum(generator ratings) >= sum(load ratings).

# Arguments
- `sys::System`: system
"""
function adequacy_check(sys::System)
    gen = total_capacity_rating(sys)
    load = total_load_rating(sys)
    load > gen && @warn "System peak load ($load) exceeds total capacity capability ($gen)."
end

"""
    total_load_rating(sys::System)

Checks the system for sum(generator ratings) >= sum(load ratings).

# Arguments
- `sys::System`: system
"""
function total_load_rating(sys::System)
    base_power = get_base_power(sys)
    controllable_loads = get_components(ControllableLoad, sys)
    cl =
        isempty(controllable_loads) ? 0.0 :
        sum(get_max_active_power.(controllable_loads)) * base_power
    @debug "System has $cl MW of ControllableLoad" _group = IS.LOG_GROUP_SYSTEM_CHECKS
    static_loads = get_components(StaticLoad, sys)
    sl = isempty(static_loads) ? 0.0 : sum(get_max_active_power.(static_loads)) * base_power
    @debug "System has $sl MW of StaticLoad" _group = IS.LOG_GROUP_SYSTEM_CHECKS
    # Total load calculation assumes  P = Real(V^2/Y) assuming V=1.0
    fa_loads = get_components(FixedAdmittance, sys)
    fa =
        isempty(fa_loads) ? 0.0 :
        sum(real.(get_base_voltage.(get_bus.(fa_loads)) .^ 2 ./ get_Y.(fa_loads)))
    @debug "System has $fa MW of FixedAdmittance assuming admittance values are in P.U." _group =
        IS.LOG_GROUP_SYSTEM_CHECKS
    total_load = cl + sl + fa
    @debug "Total System Load: $total_load" _group = IS.LOG_GROUP_SYSTEM_CHECKS
    return total_load
end

"""
    total_capacity_rating(sys::System)

Sum of system generator and storage ratings.

# Arguments
- `sys::System`: system
"""
function total_capacity_rating(sys::System)
    total = 0
    for component_type in (Generator, Storage)
        components = get_components(Generator, sys)
        if !isempty(components)
            component_total = sum(get_rating.(components)) * get_base_power(sys)
            @debug "total rating for $component_type = $component_total" _group =
                IS.LOG_GROUP_SYSTEM_CHECKS
            total += component_total
        end
    end

    @debug "Total System capacity: $total" _group = IS.LOG_GROUP_SYSTEM_CHECKS
    return total
end


# Performing and plotting measurements during the simulation
using Statistics: mean
using RollingFunctions: rollmean, rolling
using Printf: @sprintf
using LaTeXStrings
using StatsPlots: plot, plot!, density, RGB, ylims, ylims!, savefig
# using StatsBase: fit, Histogram
using Dates: DateFormat, format, now
using DataFrames: DataFrame
using CSV

# Dummy function
null(args...) = nothing

# Compute translational kinetic energy
function avg_kinetic_energy(velocities, m)
    return 0.5 * m * mean( sum(velocities .^2, dims = 1) )
end

# Get a time-save string
filetime_fmt = DateFormat("yyyy-mm-dd-HH-MM-SS")
filetime() = format(now(), filetime_fmt)

# TODO: More versatile / customisable sensing

# Measure global quantities
struct GlobalSensor
    time :: Vector{Float64}
    ke :: Vector{Float64} # Per atom
    pe :: Vector{Float64} # Per atom
    F :: Vector{Float64}
    Nt :: Vector{Int64}
    cand :: Vector{Int64} # Total
    coll :: Vector{Int64} # Total
    Vsim :: Vector{Float64}
    n0 :: Vector{Float64} # Peak density

    function GlobalSensor(args...)
        # Check that all arrays have the same length
        if all(a -> length(a) == length(args[1]), args)
            return new(args...)
        else
            throw(ArgumentError("Sensor arrays must be equally sized."))
        end
    end
end

# Blank sensor
GlobalSensor() = GlobalSensor([zeros(0) for _ in 1:9]...)

# TODO: GridSensor

# Overload measurement
function measurer(sensor::GlobalSensor, p = 0, ωx = 0, ωy = 0, ωz = 0, centre = [0,0,0])
    ω_x, ω_y, ω_z = time_parametrize(ωx, ωy, ωz)
    centre = time_parametrize(centre)
    return (args...) -> measure(sensor, args..., p = p,
        ωx = ω_x, ωy = ω_y, ωz = ω_z, centre = centre)
end

# Save to CSV
function savecsv(sensor::GlobalSensor, filename)
    CSV.write(filename, DataFrame(
        time = sensor.time,
        ke = sensor.ke,
        pe = sensor.pe,
        F = sensor.F,
        Nt = sensor.Nt,
        cand = sensor.cand,
        coll = sensor.coll,
        Vsim = sensor.Vsim,
        n0 = sensor.n0
    ))
    return nothing
end

# Load from CSV
function loadsensor(filename)
    df = CSV.read(filename, DataFrame)
    return GlobalSensor(df.time, df.ke, df.pe, df.F, df.Nt,
                        df.cand, df.coll, df.Vsim, df.n0)
end

function trapped_cloud(cloud, conditions, T, ωx, ωy, ωz, p, centre)

    positions = view(positions, :, 1:N)
    velocities = view(velocities, :, 1:N)

    return positions, velocities
end

# Perform measurements
function measure(sensor::GlobalSensor, cloud, conditions, cand_count, coll_count, t, n0, V;
                 p = 0, ωx = (t) -> 0, ωy = (t) -> 0, ωz = (t) -> 0, centre = (t) -> [0,0,0])

    Nt = cloud.Nt
    positions = view(cloud.positions, :, 1:Nt)
    velocities = view(cloud.velocities, :, 1:Nt)

    species = conditions.species
    m = species.m

    # Update cloud potentials
    conditions.potential(positions, species, t, cloud.potens)

    # Current temperature for ellipsoid sizing
    T = (length(sensor.ke) > 0) ? 2 * last(sensor.ke) / (3 * kB) : Inf
    N = 0
    bound = - 2 * log(p)

    σx² = kB * T / (m * ωx(t)^2)
    σy² = kB * T / (m * ωy(t)^2)
    σz² = kB * T / (m * ωz(t)^2)

    cx, cy, cz = centre(t)

    pe_tot = 0.0
    ke_tot = 0.0

    for atom in 1:Nt
        x = cloud.positions[1, atom] - cx
        y = cloud.positions[2, atom] - cy
        z = cloud.positions[3, atom] - cz
        if (x^2 / σx² + y^2 / σy² + z^2 / σz²) < bound
            N += 1
            vx, vy, vz = view(velocities, :, atom)
            ke_tot += 0.5 * m * (vx^2 + vy^2 + vz^2)
            pe_tot += cloud.potens[atom]
        end
    end

    push!(sensor.time, t)
    push!(sensor.ke, ke_tot/N)
    push!(sensor.pe, pe_tot/N)
    push!(sensor.F, cloud.F)
    push!(sensor.Nt, Nt)
    push!(sensor.cand, cand_count)
    push!(sensor.coll, coll_count)
    push!(sensor.n0, n0)
    push!(sensor.Vsim, V)
end

# PLOTTING + ANALYSIS

# Convert a window time to window size
function window_time_size(tseries, window_time)
    return ceil(Int64, length(tseries) * window_time / last(tseries))
end

function temperature_data(sensor, window_size)
    instant_temp = 2 * sensor.ke / (3 * kB)
    rolling_temp = rollmean(instant_temp, window_size)

    final_temp = last(rolling_temp)

    return final_temp, instant_temp, rolling_temp
end

# Average temperature over time
function plot_temperature(sensor, window_time = 1e-2)
    window_size = window_time_size(sensor.time, window_time)
    T_final, instant_temp, rolling_temp = temperature_data(sensor, window_size)
    rolling_time = rollmean(sensor.time, window_size)

    linecolor = RGB(0.1, 0.1, 0.7)

    # Plot instantaneous
    plt = plot(sensor.time, instant_temp,
        title = "Average temperature (Final: $(@sprintf("%.3g", T_final)) K)",
        xlabel = "Time (s)",
        ylabel = "Temperature (K)",
        label = false,
        ylims = (0, Inf),
        linealpha = 0.5,
        linecolor = linecolor,
        dpi = 300)
    # Plot rolling
    plot!(plt, rolling_time, rolling_temp, linecolor = linecolor, label = false)

    return plt, T_final
end

# Total number of atoms over time
function plot_number(sensor)
    Nt = sensor.Nt
    Np = Nt .* sensor.F
    plt = plot(sensor.time, Np,
        title = "Total number of atoms",
        xlabel = "Time (s)",
        ylabel = "Number",
        label = false,
        ylims = (0, Inf),
        dpi = 300)
    return plt
end

# Average kinetic, potential and total energy per atom
function plot_energy(sensor, window_time = 1e-2)
    window_size = window_time_size(sensor.time, window_time)
    rolling_time = rollmean(sensor.time, window_size)

    # Final temp
    T, _, _ = temperature_data(sensor, window_size)

    instant_ke = sensor.ke / (kB * T) # Units kT (final)
    instant_pe = sensor.pe / (kB * T)

    # Rolling and total energy
    rolling_ke, rolling_pe = rollmean.([instant_ke, instant_pe], window_size)
    instant_e, rolling_e = instant_ke + instant_pe, rolling_ke + rolling_pe

    E = last(rolling_e) * kB * T # Final energy (J/atom)

    linecolors = hcat(RGB(0.1059, 0.6196, 0.4667),
                      RGB(0.851, 0.373, 0),
                      RGB(0.459, 0.439, 0.702))

    plt = plot(sensor.time, [instant_ke, instant_pe, instant_e],
        title="Average energy per atom (Final: $(@sprintf("%.3e", E)) J)",
        xlabel="Time (s)",
        ylabel=L"\textrm{Energy\:\:}(k_B T_f)",
        label=false,
        #ylims = (0, Inf),
        linecolor=linecolors,
        linealpha=0.5,
        dpi = 300)
    plot!(rolling_time, [rolling_ke, rolling_pe, rolling_e],
        label=["Kinetic" "Potential" "Total"],
        linecolor=linecolors)

    return plt
end

# Get speeds
function speeds(cloud)
    Nt = cloud.Nt
    velocities = view(cloud.velocities, :, 1:Nt)
    speeds = vec(sqrt.(sum(velocities .* velocities, dims=1)))
    return speeds
end

# Speed distribution
function plot_speed(cloud)
    plt = density(speeds(cloud),
        title="Speed distribution",
        label="Simulation",
        xlabel=L"\textrm{Speed\:\:}(ms^{-1})",
        ylabel="Probability density",
        dpi = 300)

    return plt
end

# Collision rate per atom
function plot_collrate(sensor, window_time = 1e-2)
    window_size = window_time_size(sensor.time, window_time)
    # Collision rates
    timesteps = vcat(first(sensor.time), diff(sensor.time))

    colls_per_atom = 2 * sensor.coll ./ sensor.Nt
    instant_collrate = colls_per_atom ./ timesteps

    rolling_time = rollmean(sensor.time, window_size)

    rolling_timesteps = (
        sensor.time[window_size:end] - sensor.time[1:end - window_size + 1]
    )
    rolling_collrate = (
        rolling(sum, colls_per_atom, window_size) ./ rolling_timesteps
    )

    final_rate = last(rolling_collrate)
    linecolor = RGB(0.851, 0.373, 0)
    # Note: per-atom collision rates of test and real particles are equal.

    plt = plot(rolling_time, rolling_collrate,
        title="""Average collision rate per atom
        (Final: $(@sprintf("%.3g", final_rate)) Hz)""",
        xlabel="Time (s)",
        ylabel="Rate (Hz)",
        label = false,
        #yaxis = :log10,
        #yminorticks = 10,
        linecolor=linecolor,
        dpi = 300)
    plot!(sensor.time, instant_collrate, label = false,
          linecolor = linecolor, linealpha = 0.5)

    return plt
end

# Density
function plot_density(sensor, window_time = 1e-2)
    window_size = window_time_size(sensor.time, window_time)
    rolling_time = rollmean(sensor.time, window_size)

    #T, _, _ = temperature_data(sensor, window_size)

    instant_V = sensor.Vsim
    rolling_V = rollmean(instant_V, window_size)

    #V = last(rolling_V)
    instant_n0 = sensor.n0 .* sensor.F
    rolling_n0 = rollmean(instant_n0, window_size)

    rolling_F = rollmean(sensor.F, window_size)
    rolling_Nt = rollmean(sensor.Nt, window_size)

    instant_n_avg = sensor.F .* sensor.Nt ./ instant_V
    rolling_n_avg = rolling_F .* rolling_Nt ./ rolling_V

    linecolors = hcat(RGB(0.1059, 0.6196, 0.4667),
                      RGB(0.851, 0.373, 0))

    plt = plot(sensor.time, [instant_n0, instant_n_avg],
        title="Number density",
        xlabel="Time (s)",
        ylabel=L"n (# $m^{-3}$)",
        label=false,
        linecolor=linecolors,
        linealpha=0.5,
        dpi = 300)
    plot!(rolling_time, [rolling_n0, rolling_n_avg],
        label=["Peak" "Average"],
        linecolor=linecolors)

    return plt
end

# Phase space density (WIP)
function plot_psd(sensor, m, window_time = 1e-2)
    window_size = window_time_size(sensor.time, window_time)
    rolling_time = rollmean(sensor.time, window_size)

    _, _, rolling_T = temperature_data(sensor, window_size)
    rolling_F = rollmean(sensor.F, window_size)
    rolling_n0 = rolling_F .* rollmean(sensor.n0, window_size)
    hbar = 6.626e-34 / (2 * π)
    rolling_psd = rolling_n0 .* (sqrt.(2*π*hbar^2 ./ (m * kB .* rolling_T) ) .^3)

    linecolor = RGB(0.1059, 0.6196, 0.4667)

    plt = plot(rolling_time, rolling_psd,
        title="Phase space density",
        xlabel="Time (s)",
        ylabel=L"PSD",
        label=false,
        linecolor=linecolor,
        dpi = 300)

    return plt
end


function displayResults(resultados)
    numStructures =length(resultados)
    sn = resultados[1]
    sa = resultados[2]
    si = resultados[3]
    st = resultados[4]
    sm = resultados[5]
    sf = resultados[6]

    displayResults(sn, sa, si, st, sm, sf)
end

function displayResults(sn, sa, si, st, sm, sf)

    println("Características Generales de la Cabida Óptima:")
    println("----------------------------------------------")
    println("N° Deptos: ", sum(floor.(sa.numDeptosTipo)), " (Máx. Densidad = ", round(Int, sn.maxNumDeptos), ")")
    println("N° Deptos por Tipo: ", floor.(sa.numDeptosTipo)')
    println("Ocupacion: ", round(sa.ocupacion, digits = 2), " m2 (Máx. Ocupación = ", round(sn.maxOcupacion, digits = 2), " m2)")
    println("Constructibilidad: ", round(sa.constructibilidad, digits = 2), " m2 (Máx. Constructibilidad = ", round(sn.maxConstructibilidad, digits = 2), " m2)")
    println("N° Pisos: ", Int(floor(sa.numPisos)), " (Máx. Pisos = ", Int(floor(sn.maxPisos)), ")")
    println("Altura: ", round(sa.altura, digits = 2), " m2 (Máx. Altura = ", round(sn.maxAltura, digits = 2), " m2)")
    println("Superficie Útil SNT: ", round(sa.superficieUtilSNT, digits = 2), " m2")
    println("Superficie Común SNT: ", round(sa.superficieComunSNT, digits = 2), " m2")
    println("Superficie Edificada SNT: ", round(sa.superficieEdificadaSNT, digits = 2), " m2")
    println("Superficie por Piso: ", round(sa.superficiePorPiso, digits = 2))
    println("N° Estac. Vendibles: ", round(Int, sa.estacionamientosVendibles))
    println("N° Estac. Visita: ", round(Int, sa.estacionamientosVisita))
    println("N° Estac. Discapacitados: ", round(Int, sn.minEstacionamientosDiscapacitados))
    println("N° Estac. Totales: ", round(Int, sa.estacionamientosVendibles) + round(Int, sa.estacionamientosVisita) + round(Int, sn.minEstacionamientosDiscapacitados))
    println("N° Estac. Bicicletas: ", round(Int, sa.numBicicleteros))

    println("")
    println("Características del Terreno:")
    println("----------------------------")
    println("Superficie Terreno: ", round(st.superficieTerreno, digits = 2), " m2")
    println("Superficie Bruta: ", round(st.superficieBruta, digits = 2), " m2")
    println("Costo Terreno antes Corredor: UF ", round(st.costoTerrenoAntesCorr, digits = 2), " (", round(st.costoUnitTerrenoAntesCorr, digits = 2), " UF/m2)")
    println("Costo Corredor: UF ", round(st.CostoCorredor, digits = 2), (" (IVA Inc.)"))
    println("Costo Total Terreno: UF ", round(st.costoTerreno, digits = 2), " (", round(st.costoUnitTerreno, digits = 2), " UF/m2)")

    println("")
    println("Indicadores de Desempeño:")
    println("-------------------------")
    println("Ingresos por Ventas: UF ", round(si.IngresosVentas, digits = 2))
    println("Costo Total: UF ", round(si.CostoTotal, digits = 2))
    println("Margen antes de Impuesto: UF ", round(si.MargenAntesImpuesto, digits = 2))
    println("Rentabilidad Total Bruta: UF+ ", round(si.RentabilidadTotalBruta, digits = 2) * 100," %")
    println("TIR antes Impuestos: UF+ ", round(si.TirAntesImpuestos, digits = 2) * 100," %")
    println("Impuesto Renta: UF ", round(si.ImpuestoRenta, digits = 2))
    println("Utilidad después Impuesto: UF ", round(si.UtilidadDespuesImpuesto, digits = 2))
    println("Rentabilidad Total Neta: UF+ ", round(si.RentabilidadTotalNeta, digits = 2) * 100," %")
    println("Incidencia Terreno: ", round(si.IncidenciaTerreno, digits = 2))

end


###
### Particle Filtering and Particle MCMC Samplers.
###

#######################
# Particle Transition #
#######################

"""
    ParticleTransition{T, F<:AbstractFloat} <: AbstractTransition

Fields:
- `θ`: The parameters for any given sample.
- `lp`: The log pdf for the sample's parameters.
- `le`: The log evidence retrieved from the particle.
- `weight`: The weight of the particle the sample was retrieved from.
"""
struct ParticleTransition{T, F<:AbstractFloat} <: AbstractTransition
    θ::T
    lp::F
    le::F
    weight::F
end

transition_type(spl::Sampler{<:ParticleInference}) = ParticleTransition

function additional_parameters(::Type{<:ParticleTransition})
    return [:lp,:le, :weight]
end

####
#### Generic Sequential Monte Carlo sampler.
####

"""
    SMC()

Sequential Monte Carlo sampler.

Note that this method is particle-based, and arrays of variables
must be stored in a [`TArray`](@ref) object.

Fields:
- `resampler`: A function used to sample particles from the particle container.
  Defaults to `resample_systematic`.
- `resampler_threshold`: The threshold at which resampling terminates -- defaults to 0.5. If
  the `ess` <= `resampler_threshold` * `n_particles`, the resampling step is completed.

  Usage:

```julia
SMC()
```
"""
struct SMC{space, RT<:AbstractFloat} <: ParticleInference
    resampler             ::  Function
    resampler_threshold   ::  RT
end

alg_str(spl::Sampler{SMC}) = "SMC"
function SMC(
    resampler::Function,
    resampler_threshold::RT,
    space::Tuple
) where {RT<:AbstractFloat}
    return SMC{space, RT}(resampler, resampler_threshold)
end
SMC() = SMC(resample_systematic, 0.5, ())
SMC(::Tuple{}) = SMC()
function SMC(space::Symbol...)
    SMC(resample_systematic, 0.5, space)
end

mutable struct SMCState{V<:VarInfo, F<:AbstractFloat} <: AbstractSamplerState
    vi                   ::   V
    # The logevidence after aggregating all samples together.
    average_logevidence  ::   F
    particles            ::   ParticleContainer
end

function SMCState(
    model::M,
) where {
    M<:Model
}
    vi = VarInfo(model)
    particles = ParticleContainer{Trace}(model)

    return SMCState(vi, 0.0, particles)
end

function Sampler(alg::T, model::Model, s::Selector) where T<:SMC
    dict = Dict{Symbol, Any}()
    state = SMCState(model)
    return Sampler(alg, dict, s, state)
end

function sample_init!(
    ::AbstractRNG,
    model::Turing.Model,
    spl::Sampler{<:SMC},
    N::Integer;
    kwargs...
)
    # Set the parameters to a starting value.
    initialize_parameters!(spl; kwargs...)

    # Update the particle container now that the sampler type
    # is defined.
    spl.state.particles = ParticleContainer{Trace{typeof(spl),
        typeof(spl.state.vi), typeof(model)}}(model)

    spl.state.vi.num_produce = 0;  # Reset num_produce before new sweep\.
    set_retained_vns_del_by_spl!(spl.state.vi, spl)
    resetlogp!(spl.state.vi)

    push!(spl.state.particles, N, spl, empty!(spl.state.vi))

    while consume(spl.state.particles) != Val{:done}
        ess = effectiveSampleSize(spl.state.particles)
        if ess <= spl.alg.resampler_threshold * length(spl.state.particles)
            resample!(spl.state.particles, spl.alg.resampler)
        end
    end
end

function step!(
    ::AbstractRNG,
    model::Turing.Model,
    spl::Sampler{<:SMC},
    ::Integer;
    iteration=-1,
    kwargs...
)
    # Check that we received a real iteration number.
    @assert iteration >= 1 "step! needs to be called with an 'iteration' keyword argument."

    ## Grab the weights.
    Ws, _ = weights(spl.state.particles)

    # update the master vi.
    particle = spl.state.particles.vals[iteration]
    params = tonamedtuple(particle.vi)
    lp = getlogp(particle.vi)

    return ParticleTransition(params, lp, spl.state.particles.logE, Ws[iteration])
end

####
#### Particle Gibbs sampler.
####

"""
    PG(n_particles::Int)

Particle Gibbs sampler.

Note that this method is particle-based, and arrays of variables
must be stored in a [`TArray`](@ref) object.

Usage:

```julia
PG(100, 100)
```
"""
struct PG{space} <: ParticleInference
  n_particles           ::    Int         # number of particles used
  resampler             ::    Function    # function to resample
end
function PG(n_particles::Int, resampler::Function, space::Tuple)
    return PG{space}(n_particles, resampler)
end
PG(n1::Int, ::Tuple{}) = PG(n1)
function PG(n1::Int, space::Symbol...)
    PG(n1, resample_systematic, space)
end

alg_str(spl::Sampler{PG}) = "PG"

mutable struct PGState{V<:VarInfo, F<:AbstractFloat} <: AbstractSamplerState
    vi                   ::   V
    # The logevidence after aggregating all samples together.
    average_logevidence  ::   F
end

function PGState(model::M) where {M<:Model}
    vi = VarInfo(model)
    return PGState(vi, 0.0)
end

const CSMC = PG # type alias of PG as Conditional SMC

"""
    Sampler(alg::PG, model::Model, s::Selector)

Return a `Sampler` object for the PG algorithm.
"""
function Sampler(alg::T, model::Model, s::Selector) where T<:PG
    info = Dict{Symbol, Any}()
    state = PGState(model)
    return Sampler(alg, info, s, state)
end

function step!(
    ::AbstractRNG,
    model::Turing.Model,
    spl::Sampler{<:PG},
    ::Integer;
    kwargs...
)
    particles = ParticleContainer{Trace{typeof(spl), typeof(spl.state.vi), typeof(model)}}(model)

    spl.state.vi.num_produce = 0;  # Reset num_produce before new sweep.
    ref_particle = isempty(spl.state.vi) ?
              nothing :
              forkr(Trace(model, spl, spl.state.vi))

    set_retained_vns_del_by_spl!(spl.state.vi, spl)
    resetlogp!(spl.state.vi)

    if ref_particle == nothing
        push!(particles, spl.alg.n_particles, spl, spl.state.vi)
    else
        push!(particles, spl.alg.n_particles-1, spl, spl.state.vi)
        push!(particles, ref_particle)
    end

    while consume(particles) != Val{:done}
        resample!(particles, spl.alg.resampler, ref_particle)
    end

    ## pick a particle to be retained.
    Ws, _ = weights(particles)
    indx = randcat(Ws)

    # Extract the VarInfo from the retained particle.
    params = tonamedtuple(spl.state.vi)
    spl.state.vi = particles[indx].vi
    lp = getlogp(spl.state.vi)

    # update the master vi.
    return ParticleTransition(params, lp, particles.logE, 1.0)
end

function sample_end!(
    ::AbstractRNG,
    ::Model,
    spl::Sampler{<:ParticleInference},
    N::Integer,
    ts::Vector{ParticleTransition};
    kwargs...
)
    # Set the default for resuming the sampler.
    resume_from = get(kwargs, :resume_from, nothing)

    # Exponentiate the average log evidence.
    # loge = exp(mean([t.le for t in ts]))
    loge = mean(t.le for t in ts)

    # If we already had a chain, grab the logevidence.
    if resume_from != nothing   # concat samples
        @assert resume_from isa Chains "resume_from needs to be a Chains object."
        # pushfirst!(samples, resume_from.info[:samples]...)
        pre_loge = resume_from.logevidence
        # Calculate new log-evidence
        pre_n = length(resume_from)
        loge = (pre_loge * pre_n + loge * N) / (pre_n + N)
    end

    # Store the logevidence.
    spl.state.average_logevidence = loge
end

function assume(  spl::Sampler{T},
                  dist::Distribution,
                  vn::VarName,
                  _::VarInfo
                ) where T<:Union{PG,SMC}

    vi = current_trace().vi
    if isempty(getspace(spl.alg)) || vn.sym in getspace(spl.alg)
        if ~haskey(vi, vn)
            r = rand(dist)
            push!(vi, vn, r, dist, spl)
        elseif is_flagged(vi, vn, "del")
            unset_flag!(vi, vn, "del")
            r = rand(dist)
            vi[vn] = vectorize(dist, r)
            setgid!(vi, spl.selector, vn)
            setorder!(vi, vn, vi.num_produce)
        else
            updategid!(vi, vn, spl)
            r = vi[vn]
        end
    else # vn belongs to other sampler <=> conditionning on vn
        if haskey(vi, vn)
            r = vi[vn]
        else
            r = rand(dist)
            push!(vi, vn, r, dist, Selector(:invalid))
        end
        acclogp!(vi, logpdf_with_trans(dist, r, istrans(vi, vn)))
    end
    return r, zero(Real)
end

function assume(  spl::Sampler{A},
                  dists::Vector{D},
                  vn::VarName,
                  var::Any,
                  vi::VarInfo
                ) where {A<:Union{PG,SMC},D<:Distribution}
    error("[Turing] PG and SMC doesn't support vectorizing assume statement")
end

function observe(spl::Sampler{T}, dist::Distribution, value, vi) where T<:Union{PG,SMC}
    produce(logpdf(dist, value))
    return zero(Real)
end

function observe( spl::Sampler{A},
                  ds::Vector{D},
                  value::Any,
                  vi::VarInfo
                ) where {A<:Union{PG,SMC},D<:Distribution}
    error("[Turing] PG and SMC doesn't support vectorizing observe statement")
end

####
#### Resampling schemes for particle filters
####

# Some references
#  - http://arxiv.org/pdf/1301.4019.pdf
#  - http://people.isy.liu.se/rt/schon/Publications/HolSG2006.pdf
# Code adapted from: http://uk.mathworks.com/matlabcentral/fileexchange/24968-resampling-methods-for-particle-filtering

# Default resampling scheme
function resample(w::AbstractVector{<:Real}, num_particles::Integer=length(w))
    return resample_systematic(w, num_particles)
end

# More stable, faster version of rand(Categorical)
function randcat(p::AbstractVector{T}) where T<:Real
    r, s = rand(T), 1
    for j in eachindex(p)
        r -= p[j]
        if r <= zero(T)
            s = j
            break
        end
    end
    return s
end

function resample_multinomial(w::AbstractVector{<:Real}, num_particles::Integer)
    return rand(Distributions.sampler(Categorical(w)), num_particles)
end

function resample_residual(w::AbstractVector{<:Real}, num_particles::Integer)

    M = length(w)

    # "Repetition counts" (plus the random part, later on):
    Ns = floor.(length(w) .* w)

    # The "remainder" or "residual" count:
    R = Int(sum(Ns))

    # The number of particles which will be drawn stocastically:
    M_rdn = num_particles - R

    # The modified weights:
    Ws = (M .* w - floor.(M .* w)) / M_rdn

    # Draw the deterministic part:
    indx1, i = Array{Int}(undef, R), 1
    for j in 1:M
        for k in 1:Ns[j]
            indx1[i] = j
            i += 1
        end
    end

    # And now draw the stocastic (Multinomial) part:
    return append!(indx1, rand(Distributions.sampler(Categorical(w)), M_rdn))
end

function resample_stratified(w::AbstractVector{<:Real}, num_particles::Integer)

    Q, N = cumsum(w), num_particles

    T = Array{Float64}(undef, N + 1)
    for i=1:N,
        T[i] = rand() / N + (i - 1) / N
    end
    T[N+1] = 1

    indx, i, j = Array{Int}(undef, N), 1, 1
    while i <= N
        if T[i] < Q[j]
            indx[i] = j
            i += 1
        else
            j += 1
        end
    end
    return indx
end

function resample_systematic(w::AbstractVector{<:Real}, num_particles::Integer)

    Q, N = cumsum(w), num_particles

    T = collect(range(0, stop = maximum(Q)-1/N, length = N)) .+ rand()/N
    push!(T, 1)

    indx, i, j = Array{Int}(undef, N), 1, 1
    while i <= N
        if T[i] < Q[j]
            indx[i] = j
            i += 1
        else
            j += 1
        end
    end
    return indx
end


################################
# generative function wrappers #
################################

# generative function that takes a factor graph and the result of variable
# elimination and samples from the conditional distribution on the latent
# variables

struct SampleFactorGraphTrace <: Gen.Trace
    fg::FactorGraph
    args::Tuple{FactorGraph,VariableEliminationResult}
    choices::Gen.DynamicChoiceMap
    log_prob::Float64
end

struct SampleFactorGraph <: GenerativeFunction{Nothing,SampleFactorGraphTrace}
end

const sample_factor_graph = SampleFactorGraph()

function Gen.simulate(
        ::SampleFactorGraph,
        args::Tuple{FactorGraph{N},VariableEliminationResult}) where {N}
    (fg, elimination_result) = args
    choices = choicemap()
    values = Vector{Any}(undef, N)
    log_prob = 0.0
    for addr in reverse(elimination_result.elimination_order)
        var_idx = addr_to_idx(fg, addr)
        fg = elimination_result.intermediate_fgs[var_idx]
        dist = conditional_dist(fg, values, addr)
        idx = categorical(dist)
        value = idx_to_value(idx_to_var_node(fg, addr_to_idx(fg, addr)), idx)
        values[var_idx] = value
        choices[addr] = value
        log_prob += log(dist[idx])
    end
    return SampleFactorGraphTrace(fg, args, choices, log_prob)
end

function Gen.generate(
        ::SampleFactorGraph,
        args::Tuple{FactorGraph{N},VariableEliminationResult},
        choices::ChoiceMap) where {N}
    (fg, elimination_result) = args
    values = Vector{Any}(undef, N)
    for addr in elimination_result.elimination_order
        values[addr_to_idx(fg, addr)] = choices[addr]
    end
    log_prob = 0.0
    for addr in reverse(elimination_result.elimination_order)
        fg = elimination_result.intermediate_fgs[addr_to_idx(fg, addr)]
        dist = conditional_dist(fg, values, addr)
        idx = value_to_idx(idx_to_var_node(fg, addr_to_idx(fg, addr)), values[addr_to_idx(fg, addr)])
        log_prob += log(dist[idx])
    end
    trace = SampleFactorGraphTrace(fg, args, DynamicChoiceMap(choices), log_prob)
    return (trace, log_prob)
end

Gen.get_args(trace::SampleFactorGraphTrace) = trace.args
Gen.get_retval(trace::SampleFactorGraphTrace) = nothing
Gen.get_choices(trace::SampleFactorGraphTrace) = trace.choices
Gen.get_score(trace::SampleFactorGraphTrace) = trace.log_prob
Gen.get_gen_fn(trace::SampleFactorGraphTrace) = sample_factor_graph
Gen.project(trace::SampleFactorGraphTrace, ::EmptySelection) = 0.0
Gen.has_argument_grads(::SampleFactorGraph) = (false, false, false)
Gen.accepts_output_grad(::SampleFactorGraph) = false

# TODO: to use this as an MH proposal, it must take its first argument to be a trace
# ideally, we could wrap it in another generatiev function like this:

#@gen function mh_proposal(trace, fg, elimination_result)
    #{*} ~ sample_factor_graph(fg, elimination_result)
#end

# however, this is not supported...
# we could embed it in involutive MH, but unfortunately that requires some code
# in principle, it should be possible to do this without requiring any user code.

# generative function that takes a trace and compiles and samples from the
# conditional distribution on the latent variables

struct CompileAndSampleFactorGraphTrace <: Gen.Trace
    fg::FactorGraph
    args::Tuple{Gen.Trace,Dict{Any,Latent},Dict{Any,Observation},Any}
    choices::Gen.DynamicChoiceMap
    log_prob::Float64
end

struct CompileAndSampleFactorGraph <: GenerativeFunction{Nothing,CompileAndSampleFactorGraphTrace}
end

const compile_and_sample_factor_graph = CompileAndSampleFactorGraph()

function Gen.simulate(
        ::CompileAndSampleFactorGraph,
        args::Tuple{Gen.Trace,Dict{Any,Latent},Dict{Any,Observation},Any})
    (trace, latents, observations, elimination_order) = args
    fg = compile_trace_to_factor_graph(trace, latents, observations)
    elimination_result = variable_elimination(fg, elimination_order)
    trace = Gen.simulate(sample_factor_graph, (fg, elimination_result))
    return CompileAndSampleFactorGraphTrace(fg, args, get_choices(trace), get_score(trace))
end

function Gen.generate(
        ::CompileAndSampleFactorGraph,
        args::Tuple{Gen.Trace,Dict{Any,Latent},Dict{Any,Observation},Any},
        choices::ChoiceMap)
    (trace, latents, observations, elimination_order) = args
    fg = compile_trace_to_factor_graph(trace, latents, observations)
    elimination_result = variable_elimination(fg, elimination_order)
    trace, weight = Gen.generate(sample_factor_graph, (fg, elimination_result), choices)
    return (CompileAndSampleFactorGraphTrace(fg, args, get_choices(trace), get_score(trace)), weight)
end

Gen.get_args(trace::CompileAndSampleFactorGraphTrace) = trace.args
Gen.get_retval(trace::CompileAndSampleFactorGraphTrace) = nothing
Gen.get_choices(trace::CompileAndSampleFactorGraphTrace) = trace.choices
Gen.get_score(trace::CompileAndSampleFactorGraphTrace) = trace.log_prob
Gen.get_gen_fn(trace::CompileAndSampleFactorGraphTrace) = sample_factor_graph
Gen.project(trace::CompileAndSampleFactorGraphTrace, ::EmptySelection) = 0.0
Gen.has_argument_grads(::CompileAndSampleFactorGraph) = (false, false, false)
Gen.accepts_output_grad(::CompileAndSampleFactorGraph) = false


_nextpow2exp(n) = (8 * sizeof(n) - leading_zeros(n) - (count_ones(n) == 1))

_get_bit_depth(img::BitArray) = 8  # TODO: write 1 bit-depth images
_get_bit_depth(img::AbstractArray{C}) where {C<:Colorant} = __get_bit_depth(eltype(C))
_get_bit_depth(img::AbstractArray{T}) where {T<:Normed} = __get_bit_depth(T)
# __get_bit_depth(::Type{Normed{T,1}}) where T = 1  # TODO: write 1 bit-depth images
# __get_bit_depth(::Type{Normed{T,2}}) where T = 2  # TODO: write 2 bit-depth images
# __get_bit_depth(::Type{Normed{T,4}}) where T = 4  # TODO: write 4 bit-depth images
__get_bit_depth(::Type{Bool}) = 8  # TODO: write 1 bit-depth images
__get_bit_depth(::Type{Normed{T,8}}) where T = 8
__get_bit_depth(::Type{Normed{T,16}}) where T = 16
__get_bit_depth(::Type{Normed{T,N}}) where {T,N} = ifelse(N <= 8, 8, 16)
__get_bit_depth(::Type{<:AbstractFloat}) = 8
_get_bit_depth(img::AbstractArray{T}) where {T<:AbstractFloat} = 8
_get_bit_depth(img::AbstractArray{<:Bool}) = 8  # TODO: write 1 bit-depth images
_get_bit_depth(img::AbstractArray{<:UInt8}) = 8
_get_bit_depth(img::AbstractArray{<:UInt16}) = 16

function _inspect_png_read(fpath, gamma::Union{Nothing,Float64}=nothing)
    fp = open_png(fpath)
    png_ptr = create_read_struct()
    info_ptr = create_info_struct(png_ptr)
    png_init_io(png_ptr, fp)
    png_set_sig_bytes(png_ptr, PNG_BYTES_TO_CHECK)
    png_read_info(png_ptr, info_ptr)

    width = png_get_image_width(png_ptr, info_ptr)
    height = png_get_image_height(png_ptr, info_ptr)
    color_type_orig = png_get_color_type(png_ptr, info_ptr)
    color_type = color_type_orig
    bit_depth_orig = png_get_bit_depth(png_ptr, info_ptr)
    bit_depth = bit_depth_orig
    num_channels = png_get_channels(png_ptr, info_ptr)
    interlace_type = png_get_interlace_type(png_ptr, info_ptr)

    backgroundp = png_color_16p()
    if png_get_bKGD(png_ptr, info_ptr, Ref(backgroundp)[]) != 0
        png_set_background(png_ptr, backgroundp, PNG_BACKGROUND_GAMMA_FILE, 1, 1.0)
    end

    if color_type == PNG_COLOR_TYPE_PALETTE
        png_set_palette_to_rgb(png_ptr)
        color_type = PNG_COLOR_TYPE_RGB
    end

    if color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8
        png_set_expand_gray_1_2_4_to_8(png_ptr)
        png_set_packing(png_ptr)
        bit_depth = UInt8(8)
    end

    if png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS) != 0
        png_set_tRNS_to_alpha(png_ptr)
        if color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_RGB
            color_type |= PNG_COLOR_MASK_ALPHA
        end
    end

    screen_gamma = PNG_DEFAULT_sRGB
    image_gamma = Ref{Cdouble}(0.0)
    intent = Ref{Cint}(-1)
    white_x = Ref{Cdouble}(0.0)
    white_y = Ref{Cdouble}(0.0)
    red_x = Ref{Cdouble}(0.0)
    red_y = Ref{Cdouble}(0.0)
    green_x = Ref{Cdouble}(0.0)
    green_y = Ref{Cdouble}(0.0)
    blue_x = Ref{Cdouble}(0.0)
    blue_y = Ref{Cdouble}(0.0)
    if isnothing(gamma)
        if png_get_valid(png_ptr, info_ptr, PNG_INFO_sRGB) != 0
            if png_get_sRGB(png_ptr, info_ptr, intent) != 0
                png_set_gamma(png_ptr, screen_gamma, PNG_DEFAULT_sRGB);
            else
                if png_get_gAMA(png_ptr, info_ptr, image_gamma) != 0
                    png_set_gamma(png_ptr, screen_gamma, image_gamma[])
                else
                    image_gamma[] = 0.45455
                    png_set_gamma(png_ptr, screen_gamma, image_gamma[])
                end
            end
        elseif png_get_valid(png_ptr, info_ptr, PNG_INFO_gAMA) != 0
            if png_get_gAMA(png_ptr, info_ptr, image_gamma) != 0
                png_set_gamma(png_ptr, screen_gamma, image_gamma[])
            else
                image_gamma[] = 0.45455
                png_set_gamma(png_ptr, screen_gamma, image_gamma[])
            end
        end
    elseif gamma != 1
        image_gamma[] = gamma
        png_set_gamma(png_ptr, screen_gamma, image_gamma[])
    end

    buffer_eltype = _buffer_color_type(color_type, bit_depth)
    bit_depth == 16 && png_set_swap(png_ptr)
    n_passes = png_set_interlace_handling(png_ptr)
    png_read_update_info(png_ptr, info_ptr)
    chunk_info(chunk) = png_get_valid(png_ptr, info_ptr, chunk)

    @show(fpath,
        chunk_info(PNG_INFO_sRGB),
        chunk_info(PNG_INFO_iCCP),
        chunk_info(PNG_INFO_gAMA),
        chunk_info(PNG_INFO_tRNS),
        chunk_info(PNG_INFO_cHRM),
        chunk_info(PNG_INFO_PLTE),
        chunk_info(PNG_INFO_sPLT),
        chunk_info(PNG_INFO_hIST),
        chunk_info(PNG_INFO_sBIT),
        chunk_info(PNG_INFO_bKGD),
        chunk_info(PNG_INFO_pCAL),
        (height, width, num_channels),
        (color_type_orig, color_type),
        (bit_depth_orig, bit_depth),
        (image_gamma[], screen_gamma),
        intent[],
        (white_x[], white_y[], red_x[], red_y[], green_x[], green_y[], blue_x[], blue_y[]),
        (interlace_type, n_passes),
        buffer_eltype,
    )

    png_destroy_read_struct(Ref{Ptr{Cvoid}}(png_ptr), Ref{Ptr{Cvoid}}(info_ptr), C_NULL)
    close_png(fp)
end

mutable struct _png_color_16_struct
    index::png_byte
    red::png_uint_16
    green::png_uint_16
    blue::png_uint_16
    gray::png_uint_16
end
_png_color_16_struct(c::AbstractGray{<:AbstractFloat}) = _png_color_16_struct(0x00, gray(Gray{N0f8}(c)).i, gray(Gray{N0f8}(c)).i, gray(Gray{N0f8}(c)).i, gray(Gray{N0f8}(c)).i)
_png_color_16_struct(c::AbstractGray{<:Normed}) = _png_color_16_struct(0x00, gray(c).i, gray(c).i, gray(c).i, gray(c).i)
_png_color_16_struct(c::UInt8) = _png_color_16_struct(c, 0x0000, 0x0000, 0x0000, 0x0000)
function _png_color_16_struct(c::AbstractRGB{<:AbstractFloat})
    n = RGB{N0f8}(c)
    _png_color_16_struct(0x00, red(n).i, green(n).i, blue(n).i, 0x0000)
end
_png_color_16_struct(c::AbstractRGB{<:Normed}) =  _png_color_16_struct(0x00, red(c).i, green(c).i, blue(c).i, 0x0000)

function _png_get_bKGD(png_ptr, info_ptr, background)
    ccall((:png_get_bKGD, libpng), png_uint_32, (png_const_structrp, png_inforp, Ptr{Ptr{_png_color_16_struct}}), png_ptr, info_ptr, background)
end

function _png_set_background(png_ptr, background_color, background_gamma_code, need_expand, background_gamma)
    ccall((:png_set_background, libpng), Cvoid, (png_structrp, Ptr{_png_color_16_struct}, Cint, Cint, Cdouble), png_ptr, background_color, background_gamma_code, need_expand, background_gamma)
end

function process_background(png_ptr, info_ptr, background::Bool)
    if background
        bg = _png_color_16_struct(0, 0, 0, 0, 0)
        GC.@preserve bg begin
            bgpp = Ref(Ptr{_png_color_16_struct}(pointer_from_objref(bg)))
            if _png_get_bKGD(png_ptr, info_ptr, bgpp) != 0
                _png_set_background(png_ptr, bgpp[], PNG_BACKGROUND_GAMMA_FILE, 1, 1.0)
            end
        end
        return bg
    end
    return
end

function process_background(png_ptr, info_ptr, background::Union{AbstractRGB,AbstractGray})
    bg = _png_color_16_struct(background)
    GC.@preserve bg begin
        bgp = Ptr{_png_color_16_struct}(pointer_from_objref(bg))
        _png_set_background(png_ptr, bgp, PNG_BACKGROUND_GAMMA_FILE, 1, 1.0)
        # _png_set_background(png_ptr, bgp, PNG_BACKGROUND_GAMMA_SCREEN, 0, 1.0)
    end
    return bg
end

function process_background(png_ptr, info_ptr, background::UInt8)
    bg = _png_color_16_struct(background)
    GC.@preserve bg begin
        bgp = Ptr{_png_color_16_struct}(pointer_from_objref(bg))
        _png_set_background(png_ptr, bgp, PNG_BACKGROUND_GAMMA_FILE, 1, 1.0)
        # _png_set_background(png_ptr, bgp, PNG_BACKGROUND_GAMMA_SCREEN, 0, 1.0)
    end
    return bg
end

_check_background_load(background::Bool, is_gray, has_pallette, as_paletted) = background
_check_background_load(background::AbstractGray, is_gray, has_pallette, as_paletted) = true
function _check_background_load(background::UInt8, is_gray, has_pallette, as_paletted)
    has_pallette || throw(ArgumentError("Only paletted images can use a paletted index (background::UInt8)."))
    !as_paletted || throw(ArgumentError("In order to use a (non-default) palette index to specify background, the `expand_paletted` flag must be set."))
    return true
end
function _check_background_load(background::AbstractRGB, is_gray, has_pallette, as_paletted)
    is_gray && throw(ArgumentError("Gray Images cannot use RGB background."))
    return true
end

function _check_background_save(background::AbstractGray, color_type)
    color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_GRAY_ALPHA ||
        throw(ArgumentError("Only gray scale images can use a Gray for a background."))
end
function _check_background_save(background::AbstractRGB, color_type)
   (color_type & PNG_COLOR_MASK_COLOR) != 0 ||
        throw(ArgumentError("Only true color images can use a RGB for background."))
end
function _check_background_save(background::UInt8, color_type)
    (color_type & PNG_COLOR_MASK_PALETTE) != 0 ||
        throw(ArgumentError("Only paletted images can use a paletted index for background."))
end

function _png_set_bKGD(png_ptr, info_ptr, background)
    bg = _png_color_16_struct(background)
    GC.@preserve bg begin
        bgp = Ptr{_png_color_16_struct}(pointer_from_objref(bg))
        png_set_bKGD(png_ptr, info_ptr, bgp)
    end
    return
end

_adjust_background_bitdepth(x, bit_depth) = x
function _adjust_background_bitdepth(x::AbstractRGB{T}, bit_depth) where {T}
    __get_bit_depth(T) == bit_depth && return x
    return bit_depth == 8 ? RGB{N0f8}(x) : RGB{N0f16}(x)
end
function _adjust_background_bitdepth(x::AbstractGray{T}, bit_depth) where {T}
    __get_bit_depth(T) == bit_depth && return x
    return bit_depth == 8 ? Gray{N0f8}(x) : Gray{N0f16}(x)
end


const PPLC = Union{Path{Port},Path{Link},Path{Component}}

"""
Representation of the routing resources of a `TopLevel`.
"""
struct RoutingGraph
    """
    Adjacency information of routing resources, encode as a
    `LightGraphs.SimpleDiGraph`.
    """
    graph::SimpleDiGraph{Int64}

    """
    Translation information mapping elements on the parent `TopLevel` to indices
    in `graph`.

    Implemented as a `Dict{Path{<:Union{Port,Link,Component}}, Int64}` where the
    values in the dict are the vertex index in `graph` of the key.
    """
    map::Dict{PPLC,Int64}

    function RoutingGraph(graph::AbstractGraph, map::Dict{P,Int64}) where {P<:Path}

        #= Make sure that all of the values in the port map are valid. =#
        num_vertices = nv(graph)
        for v in values(map)
            if v > num_vertices
                error("Map value $v greater than number of vertices: $(nv(graph))")
            end
        end
        m = Dict{PPLC,Int}(map)
        return new(graph, m)
    end
end

"""
Return the `map` of a [`RoutingGraph`](@ref)

Method List
-----------
$(METHODLIST)
"""
getmap(graph::RoutingGraph) = graph.map

function translate_routes(r::RoutingGraph, graphs::Vector{<:SparseDiGraph})
    map_reversed = rev_dict(r.map)

    routes = map(graphs) do g
        route = SparseDiGraph{PPLC}()
        # Add vertices
        for v in vertices(g)
            path = map_reversed[v]
            add_vertex!(route, path)
        end
        # Add edges
        for src in vertices(g), dst in outneighbors(g, src)
            add_edge!(route, map_reversed[src], map_reversed[dst])
        end

        return route
    end

    return routes
end

# Make a vertex for each port and a single vertex in the middle.
# Connect all inputs with an edge input -> middle
#
# Connect all outputs with an edge middle -> output
function build_routing_mux(c::Component)
    # Assert that this component has not children or intra-component routing
    assert_no_children(c)
    assert_no_intrarouting(c)

    g = DiGraph(length(c.ports) + 1)
    # Create a path for node #1, the node in the middle of the mux
    m = Dict{Path,Int}(Path{Component}() => 1)

    for (i, (p, v)) in enumerate(c.ports)
        # Add 1 to i to account for the Path{Component}() we already added.
        node_index = i + 1
        m[Path{Port}(p)] = node_index

        if v.class == Output
            add_edge!(g, 1, node_index)
        elseif v.class == Input
            add_edge!(g, node_index, 1)
        else
            throw(KeyError(v.class))
        end
    end

    return RoutingGraph(g, m)
end

# Create a graph with vertices for each port and no edges.
function build_routing_blackbox(c::Component)
    # Get all of the ports for this component.
    ports = keys(c.ports)
    # Instantiate the number of vertices equal to the number of ports.
    g = DiGraph(length(ports))
    # Sequentially assign port mappings.
    if length(ports) == 0
        m = Dict{Path,Int}()
    else
        m = Dict{Path,Int}(Path{Port}(p) => i for (i, p) in enumerate(ports))
    end
    return RoutingGraph(g, m)
end

const __ROUTING_DICT = Dict("mux" => build_routing_mux)

# Constructor dispatch based on primitive type. Default to BlackBox
function routing_skeleton(c::Component)::RoutingGraph
    f = get(__ROUTING_DICT, c.primitive, build_routing_blackbox)
    return f(c)
end

function routing_skeleton(tl::TopLevel)::RoutingGraph
    # Return an empty graph
    g = DiGraph(0)
    m = Dict{Path,Int}()
    return RoutingGraph(g, m)
end

function record!(new_dict::Dict, old_dict::Dict, offset::Integer, extension)
    for (path, index) in old_dict
        # Update path
        new_path = catpath(extension, path)
        # Update index
        new_index = index + offset
        # Record the new item.
        haskey(new_dict, new_path) && error()
        new_dict[new_path] = new_index
    end
end

function add_subgraphs!(top::RoutingGraph, prefixes, subgraphs::Vector{<:RoutingGraph})
    # Iterate through the prefixes and subgraphs.
    for (prefix, subgraph) in zip(prefixes, subgraphs)
        # Get an offset calculated by the number of vertices in the current graph.
        offset = nv(top.graph)
        # Add the number of vertices in the subgraph to g.
        add_vertices!(top.graph, nv(subgraph.graph))
        # Create dictionary records for each of the new nodes.
        record!(top.map, subgraph.map, offset, prefix)
        # Record all the edges from the subgraph - account for offset.
        for edge in edges(subgraph.graph)
            add_edge!(top.graph, src(edge) + offset, dst(edge) + offset)
        end
    end
    return nothing
end

function add_links!(top, c::AbstractComponent)
    # Iterate through all links for the component.
    for (key, link) in c.links
        # Create a node for the link
        add_vertex!(top.graph)
        link_index = nv(top.graph)
        # Create an entry in the link table
        linkpath = Path{Link}(key)
        top.map[linkpath] = link_index
        # Connect the link to all its attached ports by looking through the
        # PortPaths in the link and getting the reference vertex in the graph
        # from the portmap
        for port in sources(link)
            src = top.map[port]
            add_edge!(top.graph, src, link_index)
        end
        for port in dests(link)
            dst = top.map[port]
            add_edge!(top.graph, link_index, dst)
        end
    end
    return nothing
end

function splicegraphs(component::AbstractComponent, topgraph::RoutingGraph, subgraphs)
    add_subgraphs!(topgraph, keys(component.children), subgraphs)
    add_links!(topgraph, component)
    return topgraph
end

function routing_graph(
    component::AbstractComponent, memoize = true, memo = Dict{String,RoutingGraph}()
)

    # Check if the graph for this component has been memoized and return the
    # memoized result if it has.
    if memoize && haskey(memo, component.name)
        return memo[component.name]
    end

    # If this component has not children, no need to route each of the children,
    # just build a routing skeleton for it.
    if length(component.children) == 0
        graph = routing_skeleton(component)

        # Recursively build graphs for each of the children of this component.
        # Once all the graphs for the children have been created, splice them into
        # a skeleton graph for this component.
    else
        subgraphs = [routing_graph(i, memoize, memo) for i in children(component)]
        topgraph = routing_skeleton(component)
        graph = splicegraphs(component, topgraph, subgraphs)
    end
    # Memoize this result
    memoize && memoize!(component, memo, component.name, graph)
    safety_check(component, graph)
    return graph
end

memoize!(::Component, md::Dict, key, value) = (md[key] = value)
memoize!(::TopLevel, md::Dict, key, value) = nothing

safety_check(::Component, g) = nothing
function safety_check(::TopLevel, g)
    maprev = rev_dict_safe(g.map)
    for (k, v) in maprev
        length(v) > 1 && throw(ErrorException("$k => $v"))
    end
    @debug routing_graph_info(g)
end

function routing_graph_info(g)
    return """
           Number of Vertices: $(nv(g.graph))
           Number of Edges: $(ne(g.graph))
           Entries in Map Dictionary: $(length(g.map))
           """
end


module CorrDCA

using Distributed: @distributed
using SharedArrays: SharedArray, sdata
using FastaIO: FastaReader

export read_fasta_alignment,
    remove_duplicate_sequences,
    compute_weights,
    compute_frequencies,
    add_pseudocount,
    covariance_matrix

include("readfasta.jl")
include("reweighting.jl")
include("frequencies.jl")
end