#####
##### Tuning MCTS parameters
#####

using AlphaZero
using AlphaZero: Log, Util
using Formatting
using Base: @kwdef

const NUM_GAMES = 1000
const RESET_MCTS_EVERY = 10
const LOGGER = Log.Logger()

#####
##### MCTS parameters to tune
#####

@kwdef struct Config
  n :: Int      # num simulations
  w :: Int      # num workers
  c :: Float64  # cpuct
  τ :: Float64
  ϵ :: Float64
  α :: Float64
  p :: Float64 # prior temperature
end

Util.generate_update_constructor(Config) |> eval

function Base.string(conf::Config)
  c = fmt(".1f", conf.c)
  τ = fmt(".2f", conf.τ)
  ϵ = fmt(".2f", conf.ϵ)
  α = fmt(".2f", conf.α)
  p = fmt(".2f", conf.p)
  return "$(conf.n) simulations, $(conf.w) workers; " *
    "c=$c, τ=$τ, ϵ=$ϵ, α=$α, p=$p"
end

#####
##### Experiment Report
#####

@kwdef struct Report
  reward :: Float64
  time_per_game :: Float64
  inference_time_ratio :: Float64
  average_exploration_depth :: Float64
end

function print_report(report, legend)
  wr   = Log.ColType(10, x -> format("{:.2f}", x))
  time = Log.ColType(10, x -> format("{:.1f} min", x * 100 / 60))
  itr  = Log.ColType(5,  x -> format("{}%", round(Int, x * 100)))
  expd = Log.ColType(5,  x -> format("{:.1f}", x))
  tab  = Log.Table([
    ("AVGR", wr,   r -> r.reward),
    ("T100", time, r -> r.time_per_game),
    ("ITR",  itr,  r -> r.inference_time_ratio),
    ("EXPD", expd, r -> r.average_exploration_depth)])
  Log.table_row(LOGGER, tab, report, [legend])
end

#####
##### Measurement Code
#####

using Juno

function evaluate_player(network, params, baseline, n)
  player = AlphaZero.MctsPlayer(network, params)
  Rtot = 0.0
  time = @elapsed Juno.@progress for i in 1:n
    if i % RESET_MCTS_EVERY == 0
      MCTS.reset!(player.mcts)
    end
    Rtot += AlphaZero.play_game(player, baseline)
  end
  return Report(
    reward=(Rtot / n),
    time_per_game=(time / n),
    inference_time_ratio=MCTS.inference_time_ratio(player.mcts),
    average_exploration_depth=MCTS.average_exploration_depth(player.mcts))
end

function run_experiments(network, baseline, configs)
  for config in configs
    params = MctsParams(
      use_gpu=true,
      num_workers=config.w,
      num_iters_per_turn=config.n,
      cpuct=config.c,
      temperature=ConstSchedule(config.τ),
      dirichlet_noise_ϵ=config.ϵ,
      dirichlet_noise_α=config.α,
      prior_temperature=config.p)
    evaluate_player(network, params, baseline, 1) # Compilation
    rep = evaluate_player(network, params, baseline, NUM_GAMES)
    print_report(rep, string(config))
  end
end

#####
##### Experiments
#####

include("../games.jl")
const GAME = "connect-four"
const SelectedGame = GAME_MODULE[GAME]
using .SelectedGame: Game, Training

const SESSION_DIR = "sessions/connect-four"

session = Session(
  Game, Training.Network{Game},
  Training.params, Training.netparams, benchmark=Training.benchmark,
  dir=SESSION_DIR, autosave=false, save_intermediate=false)

configs = [
  Config(n=n, w=w, c=c, τ=τ, ϵ=ϵ, α=α, p=p)
  for τ in [1.0, 0.5, 0.1]
  for n in [400, 600]
  for w in [8, 16, 32]
  for ϵ in [0.1, 0.2]
  for c in [3.0]
  for α in [1.0]
  for p in [1.0]
]

network = session.env.bestnn
baseline = MinMax.Player{Game}(;depth=5, τ=0.2)
run_experiments(network, baseline, configs)


function load(model::JuMP.Model, d::Data)
    d.state = Loading
    for variable in d.variables
        load(model, variable)
    end
    for (index, constraint) in d.constraints
        cref = load(model, constraint)
        if cref !== nothing
            d.transformed_constraints[index] = cref
        end
    end
    if d.objective !== nothing
        load(model, d.objective)
    end
    d.state = Modeling
end

# set as `optimize_hook` to JuMP Model in `data` so it is called at
# `JuMP.optimize!` if at least one set variable is created
function optimize_hook(model::JuMP.AbstractModel)
    d = data(model)
    set_space(d, model)
    # In case `optimize!` is called then the problem is modified and then it is
    # called again we need to clear first the space that might be wrong
    synchronize_perspective(d)
    clear_spaces(d)
    create_spaces(d)
    load(model, d)
    JuMP.optimize!(model, ignore_optimize_hook = true)
end


############################################################
# judiJacobianExQ ###############################################
############################################################

# Authors: Philipp Witte (pwitte@eos.ubc.ca), Henryk Modzelewski (hmodzelewski@eos.ubc.ca)
# Date: January 2017

export judiJacobianExQ, judiJacobianExQException, subsample

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

# Type for linear operator representing  Pr*A(m)^-1*Ps,
# i.e. it includes source and receiver projections
struct judiJacobianExQ{DDT<:Number,RDT<:Number} <: judiAbstractJacobian{DDT,RDT}
    name::String
    m::Integer
    n::Integer
    info::Info
    model::Model
    recGeometry::Geometry
    wavelet
    weights
    options::Options
    fop::Function              # forward
    fop_T::Union{Function, Nothing}  # transpose
end

mutable struct judiJacobianExQException <: Exception
    msg :: String
end

judiJacobian(J::judiJacobianExQ{DDT,RDT}; name=J.name, m=J.m, n=J.n, info=J.info, model=J.model, wavelet=J.wavelet,
     weights=J.weights, geom=J.recGeometry, opt=J.options, fop=J.fop, fop_T=J.fop_T) where {DDT, RDT} =
        judiJacobianExQ{DDT,RDT}(name, m, n, info, model, geom, wavelet, weights, opt, fop, fop_T)

############################################################
## Constructor
"""
    judiJacobianExQ(F,q)
Create a linearized modeling operator from the non-linear modeling operator `F` and \\
the source `q`. `F` is a full modeling operator including source/receiver projections.
Examples
========
1) `F` is a modeling operator without source/receiver projections:
    J = judiJacobianExQ(Pr*F*Ps',q)
2) `F` is the combined operator `Pr*F*Ps'`:
    J = judiJacobianExQ(F,q)
"""
function judiJacobian(F::judiPDEextended, weights::Union{judiWeights, Array}; DDT::DataType=Float32, RDT::DataType=DDT)
# JOLI wrapper for nonlinear forward modeling w/ extended source

    (DDT == Float32 && RDT == Float32) || throw(judiJacobianExQException("Domain and range types not supported"))
    m = n_samples(F.recGeometry, F.info)
    n = F.info.n

    weights = process_input_data(weights, F.model, F.info)  # extract cell array

    return J = judiJacobianExQ{Float32,Float32}("linearized wave equation", m, n, F.info, F.model, F.recGeometry, F.wavelet, weights, F.options,
                                                bornop, adjbornop)
end

############################################################
## Additional overloaded functions

# Subsample Jacobian
function subsample(J::judiJacobianExQ{ADDT,ARDT}, srcnum) where {ADDT,ARDT}
    recGeometry = subsample(J.recGeometry,srcnum)
    nsrc = typeof(srcnum) <: Int ? 1 : length(srcnum)
    info = Info(J.info.n, nsrc, J.info.nt[srcnum])
    m = n_samples(recGeometry, info)

    opt = subsample(J.options, srcnum)
    nsrc == 1 && (srcnum = srcnum:srcnum)
    return judiJacobian(J; m=m, info=info, weights=J.weights[srcnum], wavelet=J.wavelet[srcnum],
                        geom=recGeometry, opt=opt)
end

# *(num,judiJacobian)
*(a::Number,A::judiJacobianExQ{ADDT,ARDT}) where {ADDT,ARDT} =  judiJacobian(A; wavelet=a*A.wavelet)
# -(judiJacobian)
-(A::judiJacobianExQ{DDT,RDT}) where {DDT,RDT} = judiJacobian(A; wavelet=-A.wavelet)

############################################################
## Forward/adjoint function to avoid unecessary extra declaration

function bornop(J::judiJacobianExQ, v)
    srcnum = 1:J.info.nsrc
    return extended_source_modeling(J.model, J.wavelet, J.recGeometry, nothing, J.weights, v, srcnum, 'J', 1, J.options)
end

function adjbornop(J::judiJacobianExQ, w)
    srcnum = 1:J.info.nsrc
    return extended_source_modeling(J.model, J.wavelet, J.recGeometry, process_input_data(w, J.info),
                                    J.weights, nothing, srcnum, 'J', -1, J.options)
end


using NeXLMatrixCorrection, NeXLSpectrum
using PrettyTables

det = BasicEDS(4096, 0.0, 5.0, 132.0, 1, Dict(KShell => n"He", LShell => n"K", MShell => n"Cs", NShell => n"Pu"))

open(joinpath(homedir(), "Desktop", "suitable_references.tex"),"w") do io
    for el in elements[5:92]
        println(io,"\\subsection{Element: $(name(el))}")
        pretty_table(io,
            suitability(el, det, minC=0.01, latex=true),
            nosubheader=true, backend=:latex,
            label="The suitability of references for $(name(el))",
            wrap_table=false, table_type=:longtable )
        println(io)
    end
end


using DrWatson
@quickactivate "AccuracyAtTop_DeepTopPush"

include(srcdir("utilities.jl"))

# ------------------------------------------------------------------------------------------
# Basic datasets
# ------------------------------------------------------------------------------------------
Dataset_Settings = Dict(
    :dataset => [FashionMNIST, CIFAR100, SVHN2Full, Molecules],
    :posclass => [0, 1, 1, 1],
)

Train_Settings = Dict(
    :batchsize => 32,
    :epochs => 200,
    :optimiser => Descent,
    :steplength => 0.0001,
    :seed => collect(1:10),
)

Model_Settings = Dict(
    :type => [BaseLine, DeepTopPush, PatMatNP],
    :arg => [missing, missing, 0.01],
    :surrogate => [missing, quadratic, quadratic],
    :reg => 1e-3,
    :buffer => [missing, true, missing],
)

run_simulations(Dataset_Settings, Train_Settings, Model_Settings; nepochs_save = 50)
run_evaluation(Dataset_Settings)

# ------------------------------------------------------------------------------------------
# APPerf
# ------------------------------------------------------------------------------------------
Dataset_Settings = Dict(
    :dataset => [FashionMNIST, CIFAR100, Molecules],
    :posclass => [0, 1, 1],
)

Train_Settings = Dict(
    :batchsize => 32,
    :epochs => 200,
    :optimiser => Descent,
    :steplength => 0.0001,
    :seed => 1,
)

Model_Settings = Dict(
    :type => [APPerf],
    :arg => [0.01],
    :surrogate => missing,
    :reg => 1e-3,
    :buffer => missing,
)

run_simulations(Dataset_Settings, Train_Settings, Model_Settings)
run_evaluation(Dataset_Settings)

# ------------------------------------------------------------------------------------------
# Imagenet
# ------------------------------------------------------------------------------------------
Dataset_Settings = Dict(
    :dataset => [ImageNet],
    :posclass => [33:37], # turtles
)

Train_Settings = Dict(
    :batchsize => 32,
    :epochs => 10,
    :optimiser => Descent,
    :steplength => 0.0001,
    :seed => 1,
)

Model_Settings = Dict(
    :type => [DeepTopPush, PatMatNP, PatMatNP],
    :arg => [missing, 0.01, 0.05],
    :surrogate => [hinge, hinge, hinge],
    :reg => 1e-3,
    :buffer => [false, missing, missing],
)

run_simulations(Dataset_Settings, Train_Settings, Model_Settings; nepochs_save = 10)
run_evaluation(Dataset_Settings)

@info "All experiments finished"


module TrackerFlux

using Flux, Tracker

track(m) = fmap(x -> x isa AbstractArray ? Tracker.param(x) : x, m)
untrack(m) = fmap(Tracker.data, m)

function Flux.Optimise.update!(opt, xs::Tracker.Params, gs)
    for x in xs
        Flux.Optimise.update!(opt, x, gs[x])
    end
end
function Flux.Optimise.update!(opt, x, x̄)
    Tracker.update!(x, -Flux.Optimise.apply!(opt, Tracker.data(x), Tracker.data(x̄)))
end

_truncate(x::AbstractArray) = Tracker.data(x)
_truncate(x::Tuple) = _truncate.(x)
truncate!(m::Flux.Recur) = (m.state = _truncate(m.state))
truncate!(m) = foreach(truncate!, Flux.functor(m)[1])

function overload_gradient()
    @eval Flux.gradient(f, args...) = Tracker.gradient(f, args...)
end

function __init__()
    @eval Flux truncate! = $truncate!
end

end


function _get_organism_in_front(model::GebModel, organism::GebOrganism)
    _get_organism_in_direction(model, organism.coordinates, organism.direction)
end
function _get_organism_behind(model::GebModel, organism::GebOrganism)
    _get_organism_in_direction(model, organism.coordinates, 360 - organism.direction)
end

function _get_organism_in_direction(model::GebModel, coordinates, direction)
    x_target, y_target = _get_cell_in_direction(model, coordinates, direction)
    target = model.grid[x_target, y_target]
    return target
end

function _get_cell_in_direction(model, coordinates, direction)
    x_organism, y_organism = _get_grid_coordinates(coordinates)

    if 45 <= direction < 135
        x_target, y_target = x_organism, y_organism + 1
    elseif 135 <= direction < 225
        x_target, y_target = x_organism - 1, y_organism
    elseif 225 <= direction < 315
        x_target, y_target = x_organism, y_organism - 1
    elseif 315 <= direction || direction < 45
        x_target, y_target = x_organism + 1, y_organism
    end

    x_target, y_target = mod_1(x_target, model.size), mod_1(y_target, model.size)

    return x_target, y_target
end


@testset "defaulttrajectorystate.jl" begin

    @testset "DefaultTrajectoryState" begin
        graph = Matrix(adjacency_matrix(random_regular_graph(6,3)))
        ts1 = SeaPearl.DefaultTrajectoryState(
            GeometricFlux.FeaturedGraph(graph; nf=rand(3,6)),
            2,
            collect(3:6)
        )
        ts2 = SeaPearl.DefaultTrajectoryState(
            GeometricFlux.FeaturedGraph(graph; nf=rand(3,6)),
            2
        )

        batched1 = ts1 |> cpu
        batched2 = ts2 |> cpu
        @test isa(batched1, SeaPearl.BatchedDefaultTrajectoryState{Float32})

        @test all(ts1.fg.graph .== batched1.fg.graph[:, :, 1])
        @test all(abs.(ts1.fg.nf .- batched1.fg.nf[:,:,1]) .< 1e-5)
        @test ts1.variableIdx == batched1.variableIdx[1]
        @test all(ts1.allValuesIdx .==  batched1.allValuesIdx[:,1])

        @test isnothing(batched2.allValuesIdx)
        @test all(ts2.fg.graph .== batched2.fg.graph[:, :, 1])
        @test all(abs.(ts2.fg.nf .- batched2.fg.nf[:,:,1]) .< 1e-5)
        @test ts2.variableIdx == batched2.variableIdx[1]
    end

    @testset "BatchedFeaturedGraph" begin
        graph = cat(Matrix.(adjacency_matrix.([random_regular_graph(6,3) for i =1:3]))...; dims=3)
        nf = rand(4, 6, 3)
        ef = rand(2, 9, 3)
        gf = rand(2, 3)

        batched1 = SeaPearl.BatchedFeaturedGraph(graph, nf, ef, gf)

        @test isa(batched1, SeaPearl.BatchedFeaturedGraph{Float32})

        batched2 = SeaPearl.BatchedFeaturedGraph{Float32}(graph)

        @test size(batched2.graph) == (6,6,3)
        @test size(batched2.nf) == (0,6,3)
        @test size(batched2.ef) == (0,9,3)
        @test size(batched2.gf) == (0,3)
    end

    @testset "BatchedDefaultTrajectoryState" begin
        graph = cat(Matrix.(adjacency_matrix.([random_regular_graph(6, i) for i = 2:4]))...; dims=3)
        bfg = SeaPearl.BatchedFeaturedGraph{Float32}(graph)
        var = collect(1:3)
        val = rand(1:6, 2, 3)

        batched = SeaPearl.BatchedDefaultTrajectoryState(bfg, var, val)

        @test isa(batched, SeaPearl.BatchedDefaultTrajectoryState{Float32})

        batched = SeaPearl.BatchedDefaultTrajectoryState{Float32}(fg=bfg, variableIdx=var)

        @test isnothing(batched.allValuesIdx)
    end

    # TODO test Flux.functor(::Type{Vector{DefaultTrajectoryState}}, v)
    @testset "Flux.functor(::Type{Vector{DefaultTrajectoryState}}, v)" begin

    end
end


using Tinker
using Gtk.ShortNames, GtkReactive, Graphics, Colors, Images, IntervalSets

# Opens a second window displaying the current value of rectview for the
# selection. Makes it easier to check if the view is correct.

ctx = value(Tinker.active_context)

win = Window("rectview", size(ctx.image,2), size(ctx.image,1));
c = canvas(UserUnit);
push!(win, c);

redraw = draw(c, ctx.rectview, ctx.zr) do cnvs, rv, zr
    #fill!(cnvs, colorant"white")
    set_coordinates(cnvs, zr)
    bg = fill(colorant"white", (size(ctx.image)))
    bview = view(bg,1:size(rv,1),1:size(rv,2))
    copy!(bview,rv)
    copy!(bg, bview)
    copy!(cnvs,bg) # make this not fill the whole canvas
end

showall(win);

nothing


############################################################################################
"""
$(TYPEDEF)
Diagnostics fields for NUTS sampler.

# Fields
$(TYPEDFIELDS)
"""
struct DiagnosticsNUTS{T<:AbstractFloat} <: MCMCKernelDiagnostics
    "Log density (negative energy)."
    ℓH::T
    "Depth of the tree."
    depth::Int64
    "Reason for termination. See [`InvalidTree`](@ref) and [`REACHED_MAX_DEPTH`](@ref)."
    termination::InvalidTree
    "Acceptance rate statistic."
    acceptance_rate::T
    "Discretization size"
    ϵ::T
    "Number of leapfrog steps evaluated."
    steps::Int64
    "Directions for tree doubling (useful for debugging)."
    directions::Directions
    function DiagnosticsNUTS(
        ℓH::T,
        depth::Int64,
        termination::InvalidTree,
        acceptance_rate::T,
        ϵ::T,
        steps::Int64,
        directions::Directions,
    ) where {T<:AbstractFloat}
        @argcheck ϵ > 0.0 "Discretization size has to be positive"
        return new{T}(ℓH, depth, termination, acceptance_rate, ϵ, steps, directions)
    end
end

"""
$(SIGNATURES)
Summarize results of Vector of diagnostics.

# Examples
```julia
```

"""
function results(
    diagnostics::AbstractVector{DiagnosticsNUTS{F}}, Ndigits::Integer, quantiles::Vector{T}
) where {F<:AbstractFloat,T<:Real}
    println(
        "NUTS sampler had ",
        sum(is_divergent(diagnostics[iter].termination) for iter in eachindex(diagnostics)),
        " divergent transitions. Average (final) stepsize of ",
        round.(
            mean(diagnostics[iter].ϵ for iter in eachindex(diagnostics)); digits=Ndigits
        ),
        " (",
        round.(diagnostics[end].ϵ; digits=Ndigits),
        ") with ",
        round.(
            mean(diagnostics[iter].steps for iter in eachindex(diagnostics)); digits=Ndigits
        ),
        " (",
        diagnostics[end].steps,
        ") number of steps and depth of ",
        round.(
            mean(diagnostics[iter].depth for iter in eachindex(diagnostics)); digits=Ndigits
        ),
        " (",
        diagnostics[end].depth,
        ").",
    )
    return nothing
end

############################################################################################
"""
$(SIGNATURES)
Show relevant diagnostic results.

# Examples
```julia
```

"""
function generate_showvalues(diagnostics::D) where {D<:DiagnosticsNUTS}
    return function showvalues()
        return (:NUTS, "diagnostics"),
        (:ϵ, diagnostics.ϵ), (:steps, diagnostics.steps),
        (:ℓH, diagnostics.ℓH)
    end
end

############################################################################################
# Export
export DiagnosticsNUTS, generate_showvalues


"""
    MetaGraph{T<:Integer,Label,Graph,VertexData,EdgeData,GraphData,WeightFunction,U<:Real} <: AbstractGraph{T}

A graph type with custom vertex labels containing vertex-, edge- and graph-level metadata.

Vertex labels have type `Label`, while vertex (resp. edge, resp. graph) metadata has type `VertexData` (resp. `EdgeData`, resp. `GraphData`).
It is recommended not to set `Label` to an integer type, so as to avoid confusion between vertex labels and vertex codes (which have type `T<:Integer`).

# Fields
- `g::Graph`: underlying, data-less graph with vertex indices of type `T`
- `vertex_labels::Dict{T,Label}`: dictionary mapping vertex codes to vertex labels
- `vertex_properties::Dict{Label,Tuple{T,VertexData}}`: dictionary mapping vertex labels to vertex codes & data
- `edge_data::Dict{Tuple{Label,Label},EdgeData}`: dictionary mapping edge labels such as `(label_u, label_v)` to edge metadata
- `graph_data::GraphData`: graph metadata
- `weight_function::WeightFunction`: function defining edge weight from edge metadata
- `default_weight::U`: default weight for the edges
"""
struct MetaGraph{
    T<:Integer,Label,Graph,VertexData,EdgeData,GraphData,WeightFunction,U<:Real
} <: AbstractGraph{T}
    graph::Graph
    vertex_labels::Dict{T,Label}
    vertex_properties::Dict{Label,Tuple{T,VertexData}}
    edge_data::Dict{Tuple{Label,Label},EdgeData}
    graph_data::GraphData
    weight_function::WeightFunction
    default_weight::U
end

"""
    MetaGraph(
        g;
        Label = Symbol,
        VertexData = Nothing,
        EdgeData = Nothing,
        graph_data = nothing,
        weight_function = edge_data -> 1.0,
        default_weight = 1.0
    )

Construct an empty `MetaGraph` with the given metadata types and weights.
"""
function MetaGraph(
    graph::AbstractGraph{T};
    Label=Symbol,
    VertexData=Nothing,
    EdgeData=Nothing,
    graph_data=nothing,
    weight_function=edata -> 1.0,
    default_weight=1.0,
) where {T}
    if Label <: Integer
        @warn "Constructing a MetaGraph with integer labels is not advised."
    elseif nv(graph) > 0
        @warn "Constructing a MetaGraph with a nonempty underlying graph is not advised."
    end
    return MetaGraph(
        graph,
        Dict{T,Label}(),
        Dict{Label,Tuple{T,VertexData}}(),
        Dict{Tuple{Label,Label},EdgeData}(),
        graph_data,
        weight_function,
        default_weight,
    )
end

"""
    arrange(g, label_1, label_2)

Sort two vertex labels in a default order (useful to uniquely express undirected edges).
"""
function arrange end

function Base.zero(
    g::MetaGraph{T,Label,Graph,VertexData,EdgeData}
) where {T,Label,Graph,VertexData,EdgeData}
    return MetaGraph(
        Graph();
        Label=Label,
        VertexData=VertexData,
        EdgeData=EdgeData,
        graph_data=g.graph_data,
        weight_function=g.weight_function,
        default_weight=g.default_weight,
    )
end

Base.:(==)(x::MetaGraph, y::MetaGraph) = x.graph == y.graph

Base.copy(g::MetaGraph) = deepcopy(g)

function Base.show(
    io::IO, g::MetaGraph{<:Any,Label,<:Any,VertexData,EdgeData}
) where {Label,VertexData,EdgeData}
    return print(
        io,
        "Meta graph based on a $(g.graph) with vertex labels of type $Label, vertex metadata of type $VertexData, edge metadata of type $EdgeData, graph metadata given by $(repr(g.graph_data)), and default weight $(g.default_weight)",
    )
end


# Plot helpers

"Axis unit type. Currently supported: `SI`, `RE`."
@enum AxisUnit SI RE
"Color scales type for 2D plots. Currently supported: `Log`, `Linear`, `SymLog`."
@enum ColorScale Log Linear SymLog

"Plotting arguments."
struct PlotArgs
   "Unit of spatial axis"
   axisunit::AxisUnit
   "data array size"
   sizes::Vector{Int}
   "plotting data range"
   plotrange::Vector{Float32}
   "cell IDs in the cut plane"
   idlist::Vector{Int}
   "mapping from original cell order to cut plane"
   indexlist::Vector{Int}
   "title"
   str_title::String
   "xlabel"
   strx::String
   "ylabel"
   stry::String
   "colorbar title"
   cb_title::String
end

"Set plot-related arguments of `var` in `axisunit`."
function set_args(meta::MetaVLSV, var, axisunit::AxisUnit; normal::Symbol=:none, origin=0.0)
   @unpack ncells, coordmin, coordmax = meta

   if normal == :x
      seq = @SVector [2,3]
      dir = 1
   elseif normal == :y || (ncells[2] == 1 && ncells[3] != 1) # polar
      seq = @SVector [1,3]
      dir = 2
   elseif normal == :z || (ncells[3] == 1 && ncells[2] != 1) # ecliptic
      seq = @SVector [1,2]
      dir = 3
   else
      throw(ArgumentError("1D data detected. Please use 1D plot functions."))
   end

   sizes = ncells[seq]
   plotrange = [coordmin[seq[1]], coordmax[seq[1]], coordmin[seq[2]], coordmax[seq[2]]]
   axislabels = ['X', 'Y', 'Z'][seq]

   if normal == :none
      idlist, indexlist = Int[], Int[]
   else
      idlist, indexlist = let sliceoffset = origin - coordmin[dir]
         getslicecell(meta, sliceoffset, dir, coordmin[dir], coordmax[dir])
      end
   end

   # Scale the sizes to the highest refinement level
   sizes *= 2^meta.maxamr # data needs to be refined later

   unitstr = axisunit == RE ? L"$R_E$" : L"$m$"
   strx = axislabels[1]*"["*unitstr*"]"
   stry = axislabels[2]*"["*unitstr*"]"

   str_title = @sprintf "t= %4.1fs" meta.time

   datainfo = readvariablemeta(meta, var)

   cb_title = !isempty(datainfo.variableLaTeX) ?
      datainfo.variableLaTeX * " ["*datainfo.unitLaTeX*"]" : ""

   PlotArgs(axisunit, sizes, plotrange, idlist, indexlist, str_title, strx, stry, cb_title)
end

"Set colormap limits for `data`."
function set_lim(vmin, vmax, data, colorscale::ColorScale=Linear)
   if colorscale in (Linear, SymLog)
      v1 = isinf(vmin) ? minimum(x->isnan(x) ? +Inf : x, data) : vmin
      v2 = isinf(vmax) ? maximum(x->isnan(x) ? -Inf : x, data) : vmax
   else # logarithmic
      datapositive = data[data .> 0.0]
      v1 = isinf(vmin) ? minimum(datapositive) : vmin
      v2 = isinf(vmax) ? maximum(x->isnan(x) ? -Inf : x, data) : vmax
   end

   v1, v2
end

"Return x and y ranges for 2D."
function get_axis(axisunit::AxisUnit, plotrange, sizes)
   if axisunit == RE
      x = LinRange(plotrange[1], plotrange[2], sizes[1]) ./ Vlasiator.Re
      y = LinRange(plotrange[3], plotrange[4], sizes[2]) ./ Vlasiator.Re
   else
      x = LinRange(plotrange[1], plotrange[2], sizes[1])
      y = LinRange(plotrange[3], plotrange[4], sizes[2])
   end
   x, y
end

get_axis(pArgs::PlotArgs) = get_axis(pArgs.axisunit, pArgs.plotrange, pArgs.sizes)

"Obtain data from `meta` of `var` for 2D plotting. Use `op` to select vector components."
function prep2d(meta::MetaVLSV, var, op=:none)
   dataRaw = Vlasiator.getdata2d(meta, var)

   data =
      if ndims(dataRaw) == 3
         if op in (:x, :1)
            @view dataRaw[1,:,:]
         elseif op in (:y, :2)
            @view dataRaw[2,:,:]
         elseif op in (:z, :3)
            @view dataRaw[3,:,:]
         elseif op == :mag
            @views hypot.(dataRaw[1,:,:], dataRaw[2,:,:], dataRaw[3,:,:])
         end
      else
         dataRaw
      end
   data
end

"Return `data` of `var` on a uniform 2D mesh on the finest AMR level."
function prep2dslice(meta::MetaVLSV, var, normal, op, pArgs::PlotArgs)
   @unpack idlist, indexlist = pArgs

   data3D = readvariable(meta, var)

   if startswith(var, "fg_") # field quantities, fsgrid
      throw(ArgumentError("FS grid variable $var plotting in cut currently not supported!"))
   else # moments, dccrg grid
      # vlasov grid, AMR
      if ndims(data3D) == 1
         data2D = data3D[indexlist]

         data = refineslice(meta, idlist, data2D, normal)
      elseif ndims(data3D) == 2
         data2D = data3D[:,indexlist]

         if op in (:x, :y, :z, :1, :2, :3)
            if op in (:x, :1)
               slice = @view data2D[1,:]
            elseif op in (:y, :2)
               slice = @view data2D[2,:]
            elseif op in (:z, :3)
               slice = @view data2D[3,:]
            end
            data = refineslice(meta, idlist, slice, normal)
         elseif op == :mag
            datax = @views refineslice(meta, idlist, data2D[1,:], normal)
            datay = @views refineslice(meta, idlist, data2D[2,:], normal)
            dataz = @views refineslice(meta, idlist, data2D[3,:], normal)
            data = hypot.(datax, datay, dataz)
         end
      end
   end
   data
end

"""
    prep_vdf(meta::MetaVLSV, location; kwargs...)

Return the cell velocities `v1, v2`, bin ranges `r1, r2`, cell VDF values `fweight`,
and strings of labels and titles for VDF plots.

# Optional arguments
- `unit::AxisUnit`: location unit in `SI`, `RE`.
- `unitv::String`: velocity unit in ("km/s", "m/s").
- `limits::Vector{Real}`: velocity space range given in [xmin, xmax, ymin, ymax].
- `slicetype`: symbol for choosing the slice type from :xy, :xz, :yz, :bperp, :bpar, :bpar1.
- `center`: symbol for setting the reference frame from :bulk, :peak.
- `vslicethick`: setting the velocity space slice thickness in the normal direction. If set
to 0, the whole distribution along the normal direction is projected onto a plane. Currently
this is only meaningful when `center` is set such that a range near the bulk/peak normal
velocity is selected!
- `weight::Symbol`: choosing distribution weights from phase space density or particle flux
between `:particle` and `:flux`.
- `flimit`: minimum VDF threshold for plotting.
- `verbose`: display the selection process.
"""
function prep_vdf(meta::MetaVLSV, location;
   species="proton", unit::AxisUnit=SI, unitv="km/s", slicetype=:nothing, vslicethick=0.0,
   center=:nothing, weight=:particle, flimit=-1.0, verbose=false)

   ncells = meta.ncells

   if haskey(meta.meshes, species)
      vmesh = meta.meshes[species]
   else
      throw(ArgumentError("Unable to detect population $species"))
   end

   unit == RE && (location .*= Re)

   # Set unit conversion factor
   unitvfactor = unitv == "km/s" ? 1f3 : 1.0f0

   # Get closest cell ID from input coordinates
   cidReq = getcell(meta, location)
   cidNearest = getnearestcellwithvdf(meta, cidReq)

   # Set normal direction
   if slicetype == :nothing
      slicetype =
         if ncells[2] == 1 && ncells[3] == 1 # 1D, select xz
            :xz
         elseif ncells[2] == 1 # polar
            :xz
         elseif ncells[3] == 1 # ecliptic
            :xy
         end
   end

   if slicetype == :xy
      dir1, dir2, dir3 = 1, 2, 3
      sliceNormal = [0., 0., 1.]
   elseif slicetype == :xz
      dir1, dir2, dir3 = 1, 3, 2
      sliceNormal = [0., 1., 0.]
   elseif slicetype == :yz
      dir1, dir2, dir3 = 2, 3, 1
      sliceNormal = [1., 0., 0.]
   elseif slicetype in (:bperp, :bpar, :bpar1)
      if hasvariable(meta, "B_vol")
         B = readvariable(meta, "B_vol", cidNearest)
      elseif hasvariable(meta, "vg_b_vol")
         B = readvariable(meta, "vg_b_vol", cidNearest)
      end
      BxV = B × Vbulk
      if slicetype == :bperp # slice in b_perp1/b_perp2
         sliceNormal = B ./ norm(B)
      elseif slicetype == :bpar1 # slice in b_parallel/b_perp1 plane
         sliceNormal = B × BxV
         sliceNormal ./= norm(sliceNormal)
      else # slice in b_parallel/b_perp2 plane
         sliceNormal = BxV ./ norm(BxV)
      end
   end

   v1size = vmesh.vblocks[dir1] * vmesh.vblock_size[dir1]
   v2size = vmesh.vblocks[dir2] * vmesh.vblock_size[dir2]

   v1min, v1max = vmesh.vmin[dir1], vmesh.vmax[dir1]
   v2min, v2max = vmesh.vmin[dir2], vmesh.vmax[dir2]

   @assert (v1max - v1min) / v1size ≈ (v2max - v2min) / v2size "Noncubic vgrid detected!"
   cellsize = (v1max - v1min) / v1size

   vcellids, vcellf = readvcells(meta, cidNearest; species)

   V = getvcellcoordinates(meta, vcellids; species)

   if center == :bulk # centered with bulk velocity
      Vbulk =
         if hasvariable(meta, "moments")
            # From a restart file
            readvariable(meta, "restart_V", cidNearest)
         elseif hasvariable(meta, species*"/vg_v")
            # Vlasiator 5
            readvariable(meta, species*"/vg_v", cidNearest)
         elseif hasvariable(meta, species*"/V")
            readvariable(meta, species*"/V", cidNearest)
         else
            readvariable(meta, "V", cidNearest)
         end

      for i in eachindex(V)
         V[i] = V[i] .- Vbulk
      end
   elseif center == :peak # centered on highest VDF-value
      Vpeak = maximum(vcellf)
      for i in eachindex(V)
         V[i] = V[i] .- Vpeak
      end
   end

   # Set sparsity threshold
   if flimit < 0
      flimit =
         if hasvariable(meta, species*"/vg_effectivesparsitythreshold")
            readvariable(meta, species*"/vg_effectivesparsitythreshold", cidNearest)
         elseif hasvariable(meta, species*"/EffectiveSparsityThreshold")
            readvariable(meta, species*"/EffectiveSparsityThreshold", cidNearest)
         else
            1e-16
         end
   end

   # Drop velocity cells which are below the sparsity threshold
   findex_ = vcellf .≥ flimit
   fselect = vcellf[findex_]
   Vselect = V[findex_]

   if slicetype ∈ (:xy, :yz, :xz)
      v1select = [v[dir1] for v in Vselect]
      v2select = [v[dir2] for v in Vselect]
      vnormal = [v[dir3] for v in Vselect]

      strx, stry = getindex(["vx", "vy", "vz"], [dir1, dir2])
   elseif slicetype ∈ (:Bperp, :Bpar, :Bpar1)
      #TODO: NOT working yet!
      if slicetype == :Bperp
         v1select = Vrot2[1,:] # the X axis of the slice is BcrossV=perp1
         v2select = Vrot2[2,:] # the Y axis of the slice is Bcross(BcrossV)=perp2
         vnormal = Vrot2[3,:] # the Z axis of the slice is B
         strx = L"$v_{B \times V}$"
         stry = L"$v_{B \times (B \times V)}$"
      elseif slicetype == :Bpar
         v1select = Vrot2[3,:] # the X axis of the slice is B
         v2select = Vrot2[2,:] # the Y axis of the slice is Bcross(BcrossV)=perp2
         vnormal = Vrot2[1,:] # the Z axis of the slice is -BcrossV=perp1
         strx = L"$v_{B}$"
         stry = L"$v_{B \times V}$"
      elseif slicetype == :Bpar1
         v1select = Vrot2[3,:] # the X axis of the slice is B
         v2select = Vrot2[1,:] # the Y axis of the slice is BcrossV=perp1
         vnormal = Vrot2[2,:] # the Z axis of the slice is Bcross(BcrossV)=perp2
         strx = L"$v_{B}$"
         stry = L"$v_{B \times (B \times V)}$"
      end
   end

   let str = " [$unitv]"
      strx *= str
      stry *= str
   end

   if vslicethick < 0 # Set a proper thickness
      vslicethick =
         if any(sliceNormal .== 1.0) # Assure that the slice cut through at least 1 vcell
            cellsize
         else # Assume cubic vspace grid, add extra space
            cellsize*(√3+0.05)
      end
   end

   # Weights using particle flux or phase-space density
   fweight = weight == :flux ? fselect*norm([v1select, v2select, vnormal]) : fselect

   # Select cells within the slice area
   if vslicethick > 0.0
      v1, v2, fweight = let ind_ = @. (abs(vnormal) ≤ 0.5*vslicethick)
         v1select[ind_], v2select[ind_], fweight[ind_]
      end
   else
      v1, v2 = v1select, v2select
   end

   v1 ./= unitvfactor
   v2 ./= unitvfactor

   r1 = LinRange(v1min, v1max, v1size+1) / unitvfactor
   r2 = LinRange(v2min, v2max, v2size+1) / unitvfactor

   str_title = @sprintf "t = %4.1fs" meta.time

   if verbose
      @info "Original coordinates : $location"
      @info "Original cell        : $(getcellcoordinates(meta, cidReq))"
      @info "Nearest cell with VDF: $(getcellcoordinates(meta, cidNearest))"
      let
         x, y, z = getcellcoordinates(meta, cidNearest)
         @info "cellid $cidNearest, x = $x, y = $y, z = $z"
      end

      if center == :bulk
         @info "Transforming to plasma frame, travelling at speed $Vbulk"
      elseif center == :peak
         @info "Transforming to peak f-value frame, travelling at speed $Vpeak"
      end

      @info "Using VDF threshold value of $flimit."

      if vslicethick > 0
         @info "Performing slice with a counting thickness of $vslicethick"
      else
         @info "Projecting total VDF to a single plane"
      end
   end

   v1, v2, r1, r2, fweight, strx, stry, str_title
end


export GraphNodeFeaturization, featurize!
export encodable_elements, decode, chunk_vec

"""
    GraphNodeFeaturization(encoded_atom_features::Vector{AtomFeatureDescriptor})
    GraphNodeFeaturization(feature_names, lookup_table, nbins, logspaced, categorical)

A featurization for AtomGraph objects that encodes features associated with each node. Contains a collection of `AtomFeatureDescriptor` objects, and can be initialized by passing those, or by passing parameters for constructing them.

## Notes

## Required Arguments
- `feature_names::Vector{String}`: Names of each feature
- `lookup_table::DataFrame` (optional): source of data for features, if not included in built-in `atom_data_df`

## Keyword Arguments
- `nbins::Union{Vector{Integer},Integer}`: Number of bins to use for one-hot encoding of continuous-valued features. Will be ignored for categorical features.
- `logspaced::Union{Vector{Bool},Bool}`: Whether to logarithmically space the bins
- `categorical::Union{Vector{Bool},Bool}`: Whether each feature is categorical or continous-valued.
"""
struct GraphNodeFeaturization <: AbstractFeaturization
    features::Vector{<:AbstractAtomFeatureDescriptor} # TODO: this might need another name
end

function GraphNodeFeaturization(
    feature_names::Vector{String},
    lookup_table::Union{DataFrame,Nothing} = nothing;
    nbins::Union{Vector{Integer},Integer,Nothing} = nothing,
    logspaced::Union{Vector{Bool},Bool,Nothing} = nothing,
    categorical::Union{Vector{Bool},Bool,Nothing} = nothing,
)
    num_features = length(feature_names)
    local lookup_table_here , logspaced_here , categorical_here , nbins_here
    if isnothing(lookup_table)
        lookup_table_here = atom_data_df
    else
        # need to merge them in case some data is in one place and some the other
        lookup_table_here = outerjoin(atom_data_df, lookup_table, on = :Symbol)
    end

    if isnothing(logspaced)
        logspaced_here = map(fn -> default_log(fn, lookup_table_here), feature_names)
    else
        logspaced_here = get_param_vec(logspaced, num_features)
    end

    if isnothing(categorical)
        categorical_here =
            map(fn -> default_categorical(fn, lookup_table_here), feature_names)
    else
        categorical_here = get_param_vec(categorical, num_features)
    end

    if isnothing(nbins)
        nbins_here = [default_nbins for i = 1:num_features]
    else
        nbins_here = get_param_vec(nbins, num_features, pad_val = default_nbins)
    end

    afs = map(zip(feature_names, nbins_here, logspaced_here, categorical_here)) do args
        ElementFeatureDescriptor(
            args[1],
            lookup_table_here,
            nbins = args[2],
            logspaced = args[3],
            categorical = args[4],
        )
    end
    GraphNodeFeaturization(afs)
end

# TODO: function to compute total vector length?

encodable_elements(fzn::GraphNodeFeaturization) =
    intersect([encodable_elements(f) for f in fzn.features]...)

"""
    chunk_vec(vec, nbins)

Helper function that divides up an already-constructed feature vector into "chunks" (one for each feature) of lengths specified by the vector nbins.

Sum of nbins should be equal to the length of vec.

# Examples
```jldoctest
julia> chunk_vec([1,0,0,1,0], [3,2])
2-element Array{Array{Bool,1},1}:
 [1, 0, 0]
 [1, 0]
 ```
"""
function chunk_vec(vec::Vector{<:Real}, nbins::Vector{<:Integer})
    chunks = fill(Bool[], size(nbins, 1))
    @assert length(vec) == sum(nbins) "Total number of bins doesn't match length of feature vector."
    for i = 1:size(nbins, 1)
        if i == 1
            start_ind = 1
        else
            start_ind = sum(nbins[1:i-1]) + 1
        end
        chunks[i] = vec[start_ind:start_ind+nbins[i]-1]
    end
    return chunks
end

function featurize!(ag::AtomGraph, fzn::GraphNodeFeaturization)
    encoded = reduce(vcat, map((x) -> x(ag), fzn.features))
    ag.encoded_features = encoded
    ag.featurization = fzn
end

function decode(fzn::GraphNodeFeaturization, encoded::Matrix{<:Real})
    num_atoms = size(encoded, 2)
    nbins = [f.length for f in fzn.features]
    local decoded = Dict{Integer,Dict{String,Any}}()
    for i = 1:num_atoms
        #println("atom $i")
        chunks = chunk_vec(encoded[:, i], nbins)
        decoded[i] = Dict{String,Any}()
        for (chunk, f) in zip(chunks, fzn.features)
            #println("    $(f.name): $(decode(f, chunk))")
            decoded[i][f.name] = decode(f, chunk)
        end
    end
    return decoded
end

function decode(ag::AtomGraph)
    @assert !(any(isnothing.([ag.featurization, ag.encoded_features])))
    decoded = decode(ag.featurization, ag.encoded_features)
    for (k,v) in decoded
        v["Symbol"] = ag.elements[k]
    end
    return decoded
end


using Knet
libknet8handle = Libdl.dlopen(Knet.libknet8)

SIZE = 100000
ITER = 100000
x32 = KnetArray(rand(Float32,SIZE))
y32 = KnetArray(rand(Float32,SIZE))
z32 = similar(x32)
x64 = KnetArray(rand(Float64,SIZE))
y64 = KnetArray(rand(Float64,SIZE))
z64 = similar(x64)

function cuda11test(fname, jname=fname, o...)
    println(fname)
    fcpu = eval(parse(jname))
    f32 = Libdl.dlsym(libknet8handle, fname*"_32_11")
    @time cuda11rep(f32,x32,y32,z32)
    isapprox(Array(z32),fcpu(Array(x32),Array(y32))) || warn("$fname 32")
    f64 = Libdl.dlsym(libknet8handle, fname*"_64_11")
    @time cuda11rep(f64,x64,y64,z64)
    isapprox(Array(z64),fcpu(Array(x64),Array(y64))) || warn("$fname 64")
end

function cuda11rep{T}(f,x::KnetArray{T},y::KnetArray{T},z::KnetArray{T})
    n = Cint(length(z))
    for i=1:ITER
        ccall(f,Void,(Cint,Ptr{T},Ptr{T},Ptr{T}),n,x,y,z)
    end
    Knet.@cuda(cudart,cudaDeviceSynchronize,())
    Knet.@cuda(cudart,cudaGetLastError,())
end

for f in Knet.cuda11
    isa(f,Tuple) || (f=(f,))
    cuda11test(f...)
end


function elements2df(els::Vector{Element}; rtod=180.0/pi)
  DataFrame(
    length  = [els[i].length for i in 1:length(els)],
    id      = [els[i].id for i in 1:length(els)],
    od      = [els[i].od for i in 1:length(els)],
    radius  = [els[i].radius for i in 1:length(els)],
    inclination    = [els[i].inclination*rtod for i in 1:length(els)],
    heading = [els[i].heading*rtod for i in 1:length(els)],
    fc      = [els[i].fc for i in 1:length(els)],
    mass    = [els[i].mass for i in 1:length(els)],
    ea      = [els[i].ea for i in 1:length(els)],
    er      = [els[i].er for i in 1:length(els)],
    gj      = [els[i].gj for i in 1:length(els)]
  )
end

function nodes2df(nodes::Vector{Node}; rtod=180.0/pi)
  yvals = [nodes[i].y for i in 1:length(nodes)]
  for i in 1:length(yvals)
    if abs(yvals[i]) < 1.0e-13
      yvals[i] = 0.0
    end
  end
  DataFrame(
    x  = [nodes[i].x for i in 1:length(nodes)],
    y  = yvals,
    z  = [nodes[i].z for i in 1:length(nodes)],
    tvd  = [nodes[i].tvd for i in 1:length(nodes)],
    xvert  = [nodes[i].xvert for i in 1:length(nodes)],
    dispew  = [nodes[i].dispew for i in 1:length(nodes)],
    dispns  = [nodes[i].dispns for i in 1:length(nodes)],
    inclination  = [nodes[i].inclination*rtod for i in 1:length(nodes)],
    dip  = [nodes[i].dip*rtod for i in 1:length(nodes)],
    heading  = [nodes[i].heading*rtod for i in 1:length(nodes)],
    holeradius  = [nodes[i].holeradius for i in 1:length(nodes)],
    stringradius  = [nodes[i].stringradius for i in 1:length(nodes)],
    clearance  = [nodes[i].clearance for i in 1:length(nodes)],
    fc  = [nodes[i].fc for i in 1:length(nodes)],
    weight  = [nodes[i].weight for i in 1:length(nodes)],
    mass  = [nodes[i].mass for i in 1:length(nodes)],
    massinertia  = [nodes[i].massinertia for i in 1:length(nodes)],
    axialinertia  = [nodes[i].axialinertia for i in 1:length(nodes)],
    radialinertia  = [nodes[i].radialinertia for i in 1:length(nodes)],
    deltainclination  = [nodes[i].deltainclination*rtod for i in 1:length(nodes)],
    deltaheading  = [nodes[i].deltaheading*rtod for i in 1:length(nodes)],
    externalxforce  = [nodes[i].externalxforce for i in 1:length(nodes)],
    externalyforce  = [nodes[i].externalyforce for i in 1:length(nodes)],
    fixedxcoord  = [nodes[i].fixedxcoord for i in 1:length(nodes)],
    fixedycoord  = [nodes[i].fixedycoord for i in 1:length(nodes)],
    fixedxtheta  = [nodes[i].fixedxtheta for i in 1:length(nodes)],
    fixedytheta  = [nodes[i].fixedytheta for i in 1:length(nodes)],
  )
end

function segments2df(data::Dict{Symbol, Any})
  syms = [:segment_type, :material, :length, :id, :od, :fc, :angle, :fx, :fy]
  df = DataFrame(Any[DataType[], Symbol[], Float64[], Float64[],
    Float64[], Float64[], Float64[], Float64[], Float64[]], syms)
  for seg in data[:segments]
    if typeof(seg) in [Bit, Stabilizer]
      push!(df, [typeof(seg) seg.material 0.0 seg.id seg.od seg.fc 0.0 0.0 0.0])
    elseif typeof(seg) in [Collar, Pipe]
      push!(df, [typeof(seg) seg.material seg.length seg.id seg.od seg.fc 0.0 0.0 0.0])
    elseif typeof(seg) in [Rig]
      push!(df, [typeof(seg) seg.material 0.0 seg.id seg.od 0.0 0.0 0.0 0.0])
    elseif typeof(seg) in [Bentsub]
      push!(df, [typeof(seg) :none 0.0 0.0 0.0 0.0 seg.angle 0.0 0.0])
    elseif typeof(seg) in [ExtForce]
      push!(df, [typeof(seg) :none 0.0 0.0 0.0 0.0 0.0 seg.fx seg.fy])
    else
      println("Unknown segment_type found.")
    end
  end
  df
end

function sol2df(fname::AbstractString)
  tmp_df = DataFrames.readtable(fname)

	syms = [:depth, :dtb, :radius, :incl, :hdng, :x, :y, :delx, :dely, :fx, :fy, :fz]
	df = DataFrame(Any[Float64[], Float64[], Float64[], Float64[], Float64[], Float64[],
    Float64[], Float64[], Float64[], Float64[], Float64[], Float64[]], syms
	)

  (rows, colums) = size(tmp_df)
  maxdepth = maximum(tmp_df[:depth])

  for i in 1:rows
  	push!(df, [
      tmp_df[:depth][i], tmp_df[:distanceToBit][i],
      tmp_df[:holeRadius][i], tmp_df[:inclination][i], tmp_df[:heading][i],
      round(tmp_df[:x][i], digits=4),
      round(tmp_df[:y][i], digits=4),
      round(tmp_df[:delX][i], digits=4),
      round(tmp_df[:delY][i], digits=5),
      round(tmp_df[:fX][i], digits=5),
      round(tmp_df[:fY][i], digits=5),
      round(tmp_df[:fZ][i], digits=5)
      ]
    )
  end
	df
end

function dyn2df(params::BHAtp.Parameters)
	syms = [:depth, :xampl, :xphase, :yampl, :yphase, :fxamp, :fxphase, :fyampl, :fyphase]
	df = DataFrame(Any[Float64[], Float64[], Float64[], Float64[], Float64[], Float64[],
    Float64[], Float64[], Float64[]], syms
	)
  for i in 1:params.noofnodes
    xampl = round(sqrt(params.del[1,2,i]^2 + params.del[1,3,i]^2), digits=4)
    xphase = xampl > 1.0e-20 ? atan2(params.del[1,2,i], params.del[1,3,i])/params.dtor : 0.0
    yampl = round(sqrt(params.del[2,2,i]^2 + params.del[2,3,i]^2), digits=4)
    yphase = yampl > 1.0e-20 ? atan2(-params.del[2,2,i], -params.del[2,3,i])/params.dtor : 0.0
    fxampl = round(sqrt(params.bf[1,2,i]^2 + params.bf[1,3,i]^2), digits=4)
    fxphase = fxampl > 1.0e-20 ? atan2(params.bf[1,2,i], params.bf[1,3,i])/params.dtor : 0.0
    fyampl = round(sqrt(params.bf[2,2,i]^2 + params.bf[2,3,i]^2), digits=4)
    fyphase = fyampl > 1.0e-20 ? atan2(-params.bf[2,2,i], -params.bf[2,3,i])/params.dtor : 0.0
  	push!(df, [
      params.nodes[i].z/12.0,
      xampl,  xphase,
      yampl,  yphase,
      fxampl,  fxphase,
      fyampl,  fyphase
      ]
    )
  end
	df
end

function tp2df(tptable)
  syms = [:wob, :incl, :tp, :fbit, :dtft, :ft]
	df = DataFrame(Any[Float64[], Float64[], Float64[], Float64[], Float64[], Float64[]],
    syms)
  for i in 1:length(tptable)
    wob = round(tptable[i].wob, digits=4)/1000.0
    inclination = round(round(tptable[i].inclination, digits=4), digits=4)
    dalpha = -round(tptable[i].dalpha, digits=4)
    fbit = round(round(tptable[i].fbit, digits=4), digits=4)
    firsttouch = round(round(tptable[i].firsttouch, digits=4), digits=4)
    f_firsttouch = round(round(tptable[i].f_firsttouch, digits=4), digits=4)
  	push!(df, [wob, inclination, dalpha, fbit,  firsttouch, f_firsttouch])
  end
	df
end

function sol2df(params::BHAtp.Parameters)
	syms = [:depth, :dtb, :radius, :od, :incl, :hdng, :x, :y, :dx, :dy, :dz,
    :fx, :fy, :fz, :friction, :fn, :fs]
	df = DataFrame(Any[Float64[], Float64[], Float64[], Float64[], Float64[],
    Float64[], Float64[], Float64[], Float64[], Float64[], Float64[], Float64[],
    Float64[], Float64[], Float64[], Float64[], Float64[]], syms
	)
  nnodes = params.noofnodes
  maxdepth = maximum(params.nodes[nnodes].z)
  for i in 1:nnodes
  	push!(df, [
      (maxdepth - params.nodes[i].z)/12.0, params.nodes[i].z/12.0,
      params.nodes[i].holeradius, params.nodes[i].stringradius,
      (params.nodes[i].inclination/params.dtor),
      (params.nodes[i].heading/params.dtor),
      params.nodes[i].x, params.nodes[i].y,
      round((params.del[1,1,i] - params.nodes[i].x), digits=4),
      round((params.del[2,1,i] - params.nodes[i].y), digits=4),
      round(params.del[3,1,i], digits=4),
      round(params.f[1, 1, i], digits=5),
      round(params.f[2, 1, i], digits=5),
      round(params.f[3, 1, i], digits=5),
      round(params.friction[i], digits=5),
      round(params.fn[i], digits=5),
      round(params.fs[i], digits=5)
      ]
    )
  end
	df
end


function surf3(gr::Graph, v::cmgl.Float, a::Array{cmgl.Float, 3}; stl::String="", opt::String="")
    cmgl.surf3_val(gr.ptr, v, Data(a).ptr, stl, opt)
end

function surf3(gr::Graph, v::cmgl.Float, x::Array{cmgl.Float}, y::Array{cmgl.Float}, z::Array{cmgl.Float}, a::Array{cmgl.Float, 3}; stl::String="", opt::String="")
    cmgl.surf3_xyz_val(gr.ptr, v, Data(x).ptr, Data(y).ptr, Data(z).ptr, Data(a).ptr, stl, opt)
end

function surf3(gr::Graph, v::Array{cmgl.Float, 1}, a::Array{cmgl.Float, 3}; stl::String="", opt::String="")
    for val in v
        cmgl.surf3_val(gr.ptr, val, Data(a).ptr, stl, opt)
    end
end

function surf3(gr::Graph, v::Array{cmgl.Float, 1}, x::Array{cmgl.Float}, y::Array{cmgl.Float}, z::Array{cmgl.Float}, a::Array{cmgl.Float, 3}; stl::String="", opt::String="")
    for val in v
        cmgl.surf3_xyz_val(gr.ptr, val, Data(x).ptr, Data(y).ptr, Data(z).ptr, Data(a).ptr, stl, opt)
    end
end

function surf3(gr::Graph, a::Array{cmgl.Float, 3}; stl::String="", opt::String="")
    cmgl.surf3(gr.ptr, Data(a).ptr, stl, opt)
end

function surf3(gr::Graph, x::Array{cmgl.Float}, y::Array{cmgl.Float}, z::Array{cmgl.Float}, a::Array{cmgl.Float, 3}; stl::String="", opt::String="")
    cmgl.surf3_xyz(gr.ptr, Data(x).ptr, Data(y).ptr, Data(z).ptr, Data(a).ptr, stl, opt)
end

function cloud(gr::Graph, a::Array{cmgl.Float, 3}; stl::String="", opt::String="")
    cmgl.cloud(gr.ptr, Data(a).ptr, stl, opt)
end

function cloud(gr::Graph, x::Array{cmgl.Float}, y::Array{cmgl.Float}, z::Array{cmgl.Float}, a::Array{cmgl.Float, 3}; stl::String="", opt::String="")
    cmgl.cloud_xyz(gr.ptr, Data(x).ptr, Data(y).ptr, Data(z).ptr, Data(a).ptr, stl, opt)
end

function dens3(gr::Graph, a::Array{cmgl.Float, 3}; sval::cmgl.Float=-1, stl::String="", opt::String="")
    cmgl.dens3(gr.ptr, Data(a).ptr, stl, sval, opt)
end

function dens3(gr::Graph, x::Array{cmgl.Float}, y::Array{cmgl.Float}, z::Array{cmgl.Float}, a::Array{cmgl.Float, 3}; sval::cmgl.Float=-1, stl::String="", opt::String="")
    cmgl.dens3_xyz(gr.ptr, Data(x).ptr, Data(y).ptr, Data(z).ptr, Data(a).ptr, stl, sval, opt)
end

function cont3(gr::Graph, v::Array{cmgl.Float, 1}, a::Array{cmgl.Float, 3}; sval::cmgl.Float=-1, stl::String="", opt::String="")
    cmgl.cont3_val(gr.ptr, Data(v).ptr, Data(a).ptr, stl, sval, opt)
end

function cont3(gr::Graph, v::Array{cmgl.Float, 1}, x::Array{cmgl.Float}, y::Array{cmgl.Float}, z::Array{cmgl.Float}, a::Array{cmgl.Float, 3}; sval::cmgl.Float=-1, stl::String="", opt::String="")
    cmgl.cont3_xyz_val(gr.ptr, Data(v).ptr, Data(x).ptr, Data(y).ptr, Data(z).ptr, Data(a).ptr, stl, sval, opt)
end

function cont3(gr::Graph, v::cmgl.Float, a::Array{cmgl.Float, 3}; sval::cmgl.Float=-1, stl::String="", opt::String="")
    cmgl.cont3_val(gr.ptr, Data(cmgl.Float[v]).ptr, Data(a).ptr, stl, sval, opt)
end

function cont3(gr::Graph, v::cmgl.Float, x::Array{cmgl.Float}, y::Array{cmgl.Float}, z::Array{cmgl.Float}, a::Array{cmgl.Float, 3}; sval::cmgl.Float=-1, stl::String="", opt::String="")
    cmgl.cont3_xyz_val(gr.ptr, Data(cmgl.Float[v]).ptr, Data(x).ptr, Data(y).ptr, Data(z).ptr, Data(a).ptr, stl, sval, opt)
end

function cont3(gr::Graph, a::Array{cmgl.Float, 3}; sval::cmgl.Float=-1, stl::String="", opt::String="")
    cmgl.cont3(gr.ptr, Data(a).ptr, stl, sval, opt)
end

function cont3(gr::Graph, x::Array{cmgl.Float}, y::Array{cmgl.Float}, z::Array{cmgl.Float}, a::Array{cmgl.Float, 3}; sval::cmgl.Float=-1, stl::String="", opt::String="")
    cmgl.cont3_xyz(gr.ptr, Data(x).ptr, Data(y).ptr, Data(z).ptr, Data(a).ptr, stl, sval, opt)
end

function contf3(gr::Graph, v::Array{cmgl.Float, 1}, a::Array{cmgl.Float, 3}; sval::cmgl.Float=-1, stl::String="", opt::String="")
    cmgl.contf3_val(gr.ptr, Data(v).ptr, Data(a).ptr, stl, sval, opt)
end

function contf3(gr::Graph, v::Array{cmgl.Float, 1}, x::Array{cmgl.Float}, y::Array{cmgl.Float}, z::Array{cmgl.Float}, a::Array{cmgl.Float, 3}; sval::cmgl.Float=-1, stl::String="", opt::String="")
    cmgl.contf3_xyz_val(gr.ptr, Data(v).ptr, Data(x).ptr, Data(y).ptr, Data(z).ptr, Data(a).ptr, stl, sval, opt)
end

function contf3(gr::Graph, v::cmgl.Float, a::Array{cmgl.Float, 3}; sval::cmgl.Float=-1, stl::String="", opt::String="")
    cmgl.contf3_val(gr.ptr, Data(cmgl.Float[v]).ptr, Data(a).ptr, stl, sval, opt)
end

function contf3(gr::Graph, v::cmgl.Float, x::Array{cmgl.Float}, y::Array{cmgl.Float}, z::Array{cmgl.Float}, a::Array{cmgl.Float, 3}; sval::cmgl.Float=-1, stl::String="", opt::String="")
    cmgl.contf3_xyz_val(gr.ptr, Data(cmgl.Float[v]).ptr, Data(x).ptr, Data(y).ptr, Data(z).ptr, Data(a).ptr, stl, sval, opt)
end

function contf3(gr::Graph, a::Array{cmgl.Float, 3}; sval::cmgl.Float=-1, stl::String="", opt::String="")
    cmgl.contf3(gr.ptr, Data(a).ptr, stl, sval, opt)
end

function contf3(gr::Graph, x::Array{cmgl.Float}, y::Array{cmgl.Float}, z::Array{cmgl.Float}, a::Array{cmgl.Float, 3}; sval::cmgl.Float=-1, stl::String="", opt::String="")
    cmgl.contf3_xyz(gr.ptr, Data(x).ptr, Data(y).ptr, Data(z).ptr, Data(a).ptr, stl, sval, opt)
end

function grid3(gr::Graph, a::Array{cmgl.Float, 3}; sval::cmgl.Float=-1, stl::String="", opt::String="")
    cmgl.grid3(gr.ptr, Data(a).ptr, stl, sval, opt)
end

function grid3(gr::Graph, x::Array{cmgl.Float}, y::Array{cmgl.Float}, z::Array{cmgl.Float}, a::Array{cmgl.Float, 3}; sval::cmgl.Float=-1, stl::String="", opt::String="")
    cmgl.grid3_xyz(gr.ptr, Data(x).ptr, Data(y).ptr, Data(z).ptr, Data(a).ptr, stl, sval, opt)
end

function beam(gr::Graph, tr::Array{cmgl.Float}, g1::Array{cmgl.Float}, g2::Array{cmgl.Float}, a::Array{cmgl.Float, 3}, r::Array{cmgl.Float}; stl::String="", flag::Int=0, num::Int=3)
    cmgl.beam(gr.ptr, Data(tr).ptr, Data(g1).ptr, Data(g2).ptr, Data(a).ptr, Data(r).ptr, stl, flag, num)
end

function beam(gr::Graph, v::Array{cmgl.Float, 1}, tr::Array{cmgl.Float}, g1::Array{cmgl.Float}, g2::Array{cmgl.Float}, a::Array{cmgl.Float, 3}, r::Array{cmgl.Float}; stl::String="", flag::Int=0)
    cmgl.beam_val(gr.ptr, Data(v).ptr, Data(tr).ptr, Data(g1).ptr, Data(g2).ptr, Data(a).ptr, Data(r).ptr, stl, flag)
end


# using Base.Test
# using MIToS.MSA

print("""

Tests for Raw Multiple Sequence Alignment a.k.a Matrix{Residue}
================================================================
""")

print("""

Parse Raw
---------
""")

let raw = read(joinpath(pwd(), "data", "gaps.txt"), Raw),
  raw_string = """THAYQAIHQV
       THAYQAIHQ-
       THAYQAIH--
       THAYQAI---
       THAYQA----
       THAYQ-----
       THAY------
       THA-------
       TH--------
       T---------
       """

  @test asciisequence(raw, 1)  == "THAYQAIHQV"
  @test asciisequence(raw, 10) == "T---------"

  @test parse(raw_string, Raw) == raw

  print("""

Print Raw
---------
""")

  let io = IOBuffer()
    print(io, raw, Raw)
    @test takebuf_string(io) == raw_string
  end

  print("""

Test %
------
""")

  @test gapfraction(raw) == 0.45
  @test gapfraction(raw, 1) == [0.0,0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9]
  @test gapfraction(raw, 2) == [0.0,0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9]

  @test residuefraction(raw) == 0.55
  @test residuefraction(raw, 1) == [1.0,0.9,0.8,0.7,0.6,0.5,0.4,0.3,0.2,0.1]
  @test residuefraction(raw, 2) == [1.0,0.9,0.8,0.7,0.6,0.5,0.4,0.3,0.2,0.1]

  @test coverage(raw) == [1.0,0.9,0.8,0.7,0.6,0.5,0.4,0.3,0.2,0.1]

  @test columngapfraction(raw) == [0.0,0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9]

  print("""

Reference and Gapstrip
----------------------
""")

  let gs = gapstrip(raw,  coveragelimit=0.5, gaplimit=0.5)
    @test getsequence(gs, 1) == res"THAYQAIH"
    @test ncolumns(gs) == 8
    @test nsequences(gs) == 6
  end

  let ref = setreference!(copy(raw), 2)
    @test getsequence(ref, 1) == res"THAYQAIHQ-"
    ref = adjustreference(ref)
    @test getsequence(ref, 1) == res"THAYQAIHQ"
  end

end


function getAgesAtTime( mpSim::MPsim, timePoint::Float64,
    subpopulations::Vector{Subpopulation}, ageType::Symbol )

    # Get the ids in each subpopulation at the time point.
    result = Vector{Vector{Float64}}( undef, length( subpopulations ) )
    idsInSubpop = getSubpopulationAtTime( mpSim, timePoint, subpopulations )

    # ! No longer necessary after overhaul.
    if ageType === :timeInNode
        queryPrefix = string( "SELECT ", timePoint,
            " - max( timeIndex ) `time in node` FROM `", mpSim.transDBname,
            "` WHERE",
            "\n    timeIndex <= ", timePoint, " AND ",
            "\n    `", mpSim.idKey, "` IN ( '" )
        querySuffix = string( "' )",
            "\n    GROUP BY `", mpSim.idKey, "`" )

        for ii in eachindex( subpopulations )
            if isempty( idsInSubpop[ii] )
                result[ii] = Vector{Float64}()
            else
                queryCmd = string( queryPrefix, join( idsInSubpop[ii],
                    "', '" ), querySuffix )
                result[ii] = DataFrame( DBInterface.execute( mpSim.simDB,
                    queryCmd ) )[:, 1]
            end  # if isempty( idsInSubpop )[ii]
        end  # for ii in eachindex( subpopulations )

        return result
    end  # if ageType === :timeInNode

    queryCmdTmp = string( "SELECT ", timePoint, " - timeEntered",
        ageType === :age ? " + ageAtRecruitment age" : " tenure",
        " FROM `", mpSim.persDBname, "` WHERE ",
        "\n    `", mpSim.idKey, "` IN ( '" )

    for ii in eachindex( subpopulations )
        if isempty( idsInSubpop[ii] )
            result[ii] = Vector{Float64}()
        else
            queryCmd = string( queryCmdTmp, join( idsInSubpop[ii], "', '" ),
                "' )" )
            result[ii] = DataFrame( DBInterface.execute( mpSim.simDB,
                queryCmd ) )[:, 1]
        end  # if isempty( idsInSubpop )[ii]
    end  # for ii in eachindex( subpopulations )

    return result

end  # getAgesAtTime( mpSim, timePoint, subpopulationulations, ageType )

function processSubpopulationAges( personnelAges::Array{Vector{Float64},2},
    subpopulations::Vector{Subpopulation}, timeGrid::Vector{Float64},
    ageRes::Float64 )

    result = Dict{String, DataFrame}()
    baseNames = [:timePoint, :mean, :median, :stdev, :min, :max]

    for ii in eachindex( subpopulations )
        subpopName = subpopulations[ii].name
        agesOfSubpop = personnelAges[ii, :]
        ageSummary = zeros( Union{Float64, Missing}, length( timeGrid ), 6 )
        ageSummary[:, 1] = timeGrid
        emptySubpop = isempty.( agesOfSubpop )

        if all( emptySubpop )
            ageSummary[:, 2:end] .= missing
            result[subpopName] = DataFrame( ageSummary, baseNames )
        else
            minAge = minimum( minimum.( agesOfSubpop[.!emptySubpop] ) )
            minAge = floor( minAge / ageRes ) * ageRes
            maxAge = maximum( maximum.( agesOfSubpop[.!emptySubpop] ) )
            maxAge = floor( maxAge / ageRes ) * ageRes
            ageGrid = collect( minAge:ageRes:maxAge )

            counts = zeros( Int, length( timeGrid ), length( ageGrid ) )

            for jj in eachindex( timeGrid )
                subpopAgesAtTime = agesOfSubpop[jj]

                if isempty( subpopAgesAtTime )
                    ageSummary[jj, 2:6] .= missing
                else
                    ageSummary[jj, 2:6] = [mean( subpopAgesAtTime ),
                        median( subpopAgesAtTime ),
                        length( subpopAgesAtTime ) == 1 ? missing :
                            std( subpopAgesAtTime ),
                        minimum( subpopAgesAtTime ),
                        maximum( subpopAgesAtTime )]
                end  # if isempty( subpopAgesAtTime )

                tmpCounts = map( age -> count( subpopAgesAtTime .>= age ),
                    ageGrid )
                tmpCounts -= vcat( tmpCounts[2:end], 0 )
                counts[jj, :] = tmpCounts
            end  # for jj in eachindex( timeGrid )

            result[subpopName] = DataFrame( hcat( ageSummary, counts ),
                vcat( baseNames, Symbol.( ageGrid ) ) )
        end  # if all( emptySubpop )
    end  # for ii in eachindex( subpopulations )

    return result

end  # processSubpopulationAges( personnelAges,subpopulations, timeGrid,
     #   ageRes )


# These functions are still WIP

function noise_model(freq_hz, fknee_hz, rms, alpha)
    rms * (1 + fknee_hz / freq_hz)^alpha
end

function baseline_spectrum(freq_hz, baseline_period_s)
    while false

    end
end


const MANTISSA_MASK = 0x000fffffffffffff
const EXP_MASK = 0x00000000000007ff

memcpy(d, doff, s, soff, n) = ccall(:memcpy, Cvoid, (Ptr{UInt8}, Ptr{UInt8}, Int), d + doff - 1, s + soff - 1, n)
memmove(d, doff, s, soff, n) = ccall(:memmove, Cvoid, (Ptr{UInt8}, Ptr{UInt8}, Int), d + doff - 1, s + soff - 1, n)

uint(x::Float16) = Core.bitcast(UInt16, x)
uint(x::Float32) = Core.bitcast(UInt32, x)
uint(x::Float64) = Core.bitcast(UInt64, x)

mantissabits(::Type{Float16}) = 10
mantissabits(::Type{Float32}) = 23
mantissabits(::Type{Float64}) = 52

exponentbits(::Type{Float16}) = 5
exponentbits(::Type{Float32}) = 8
exponentbits(::Type{Float64}) = 11

bias(::Type{Float16}) = 15
bias(::Type{Float32}) = 127
bias(::Type{Float64}) = 1023

pow5_bitcount(::Type{Float16}) = 30
pow5_bitcount(::Type{Float32}) = 61
pow5_bitcount(::Type{Float64}) = 121

pow5_inv_bitcount(::Type{Float16}) = 30
pow5_inv_bitcount(::Type{Float32}) = 59
pow5_inv_bitcount(::Type{Float64}) = 122

qinvbound(::Type{Float16}) = 4
qinvbound(::Type{Float32}) = 9
qinvbound(::Type{Float64}) = 21

qbound(::Type{Float16}) = 15
qbound(::Type{Float32}) = 31
qbound(::Type{Float64}) = 63

log10pow2(e) = (e * 78913) >> 18
log10pow5(e) = (e * 732923) >> 20
pow5bits(e) = ((e * 1217359) >> 19) + 1
@inline mulshift(m::UInt64, mula, mulb, j) = ((((UInt128(m) * mula) >> 64) + UInt128(m) * mulb) >> (j - 64)) % UInt64
@inline mulshift(m::UInt32, mul, j) = ((((UInt64(m) * (mul % UInt32)) >> 32) + (UInt64(m) * (mul >> 32))) >> (j - 32)) % UInt32
@inline mulshift(m::UInt16, mul, j) = ((((UInt32(m) * (mul % UInt16)) >> 16) + (UInt32(m) * (mul >> 16))) >> (j - 16))
indexforexp(e) = div(e + 15, 16)
pow10bitsforindex(idx) = 16 * idx + 120
lengthforindex(idx) = div(((Int64(16 * idx) * 1292913986) >> 32) + 1 + 16 + 8, 9)

@inline function pow5(x, p)
    count = 0
    while true
        q = div(x, 5)
        r = x - 5 * q
        r != 0 && return count >= p
        x = q
        count += 1
    end
end

pow2(x, p) = (x & ((Int64(1) << p) - 1)) == 0

@inline function decimallength(v)
    v >= 10000000000000000 && return 17
    v >= 1000000000000000 && return 16
    v >= 100000000000000 && return 15
    v >= 10000000000000 && return 14
    v >= 1000000000000 && return 13
    v >= 100000000000 && return 12
    v >= 10000000000 && return 11
    v >= 1000000000 && return 10
    v >= 100000000 && return 9
    v >= 10000000 && return 8
    v >= 1000000 && return 7
    v >= 100000 && return 6
    v >= 10000 && return 5
    v >= 1000 && return 4
    v >= 100 && return 3
    v >= 10 && return 2
    return 1
end

@inline function decimallength(v::UInt32)
    v >= 100000000 && return 9
    v >= 10000000 && return 8
    v >= 1000000 && return 7
    v >= 100000 && return 6
    v >= 10000 && return 5
    v >= 1000 && return 4
    v >= 100 && return 3
    v >= 10 && return 2
    return 1
end

@inline function decimallength(v::UInt16)
    v >= 10000 && return 5
    v >= 1000 && return 4
    v >= 100 && return 3
    v >= 10 && return 2
    return 1
end

@inline function mulshiftinvsplit(::Type{Float64}, mv, mp, mm, i, j)
    @inbounds mula, mulb = DOUBLE_POW5_INV_SPLIT[i + 1]
    vr = mulshift(mv, mula, mulb, j)
    vp = mulshift(mp, mula, mulb, j)
    vm = mulshift(mm, mula, mulb, j)
    return vr, vp, vm
end

@inline function mulshiftinvsplit(::Type{Float32}, mv, mp, mm, i, j)
    @inbounds mul = FLOAT_POW5_INV_SPLIT[i + 1]
    vr = mulshift(mv, mul, j)
    vp = mulshift(mp, mul, j)
    vm = mulshift(mm, mul, j)
    return vr, vp, vm
end

@inline function mulshiftinvsplit(::Type{Float16}, mv, mp, mm, i, j)
    @inbounds mul = HALF_POW5_INV_SPLIT[i + 1]
    vr = mulshift(mv, mul, j)
    vp = mulshift(mp, mul, j)
    vm = mulshift(mm, mul, j)
    return vr, vp, vm
end

@inline function mulshiftsplit(::Type{Float64}, mv, mp, mm, i, j)
    @inbounds mula, mulb = DOUBLE_POW5_SPLIT[i + 1]
    vr = mulshift(mv, mula, mulb, j)
    vp = mulshift(mp, mula, mulb, j)
    vm = mulshift(mm, mula, mulb, j)
    return vr, vp, vm
end

@inline function mulshiftsplit(::Type{Float32}, mv, mp, mm, i, j)
    @inbounds mul = FLOAT_POW5_SPLIT[i + 1]
    vr = mulshift(mv, mul, j)
    vp = mulshift(mp, mul, j)
    vm = mulshift(mm, mul, j)
    return vr, vp, vm
end

@inline function mulshiftsplit(::Type{Float16}, mv, mp, mm, i, j)
    @inbounds mul = HALF_POW5_SPLIT[i + 1]
    vr = mulshift(mv, mul, j)
    vp = mulshift(mp, mul, j)
    vm = mulshift(mm, mul, j)
    return vr, vp, vm
end

@inline function pow5invsplit(::Type{Float64}, i)
    pow = big(5)^i
    inv = div(big(1) << (bitlength(pow) - 1 + pow5_inv_bitcount(Float64)), pow) + 1
    return (UInt64(inv & ((big(1) << 64) - 1)), UInt64(inv >> 64))
end

@inline function pow5invsplit(::Type{Float32}, i)
    pow = big(5)^i
    inv = div(big(1) << (bitlength(pow) - 1 + pow5_inv_bitcount(Float32)), pow) + 1
    return UInt64(inv)
end

@inline function pow5invsplit(::Type{Float16}, i)
    pow = big(5)^i
    inv = div(big(1) << (bitlength(pow) - 1 + pow5_inv_bitcount(Float16)), pow) + 1
    return UInt32(inv)
end

@inline function pow5split(::Type{Float64}, i)
    pow = big(5)^i
    j = bitlength(pow) - pow5_bitcount(Float64)
    return (UInt64((pow >> j) & ((big(1) << 64) - 1)), UInt64(pow >> (j + 64)))
end

@inline function pow5split(::Type{Float32}, i)
    pow = big(5)^i
    return UInt64(pow >> (bitlength(pow) - pow5_bitcount(Float32)))
end

@inline function pow5split(::Type{Float16}, i)
    pow = big(5)^i
    return UInt32(pow >> (bitlength(pow) - pow5_bitcount(Float16)))
end

const DOUBLE_POW5_INV_SPLIT = map(i->pow5invsplit(Float64, i), 0:291)
const FLOAT_POW5_INV_SPLIT = map(i->pow5invsplit(Float32, i), 0:30)
const HALF_POW5_INV_SPLIT = map(i->pow5invsplit(Float16, i), 0:17)

const DOUBLE_POW5_SPLIT = map(i->pow5split(Float64, i), 0:325)
const FLOAT_POW5_SPLIT = map(i->pow5split(Float32, i), 0:46)
const HALF_POW5_SPLIT = map(i->pow5split(Float16, i), 0:23)

const DIGIT_TABLE = UInt8[
  '0','0','0','1','0','2','0','3','0','4','0','5','0','6','0','7','0','8','0','9',
  '1','0','1','1','1','2','1','3','1','4','1','5','1','6','1','7','1','8','1','9',
  '2','0','2','1','2','2','2','3','2','4','2','5','2','6','2','7','2','8','2','9',
  '3','0','3','1','3','2','3','3','3','4','3','5','3','6','3','7','3','8','3','9',
  '4','0','4','1','4','2','4','3','4','4','4','5','4','6','4','7','4','8','4','9',
  '5','0','5','1','5','2','5','3','5','4','5','5','5','6','5','7','5','8','5','9',
  '6','0','6','1','6','2','6','3','6','4','6','5','6','6','6','7','6','8','6','9',
  '7','0','7','1','7','2','7','3','7','4','7','5','7','6','7','7','7','8','7','9',
  '8','0','8','1','8','2','8','3','8','4','8','5','8','6','8','7','8','8','8','9',
  '9','0','9','1','9','2','9','3','9','4','9','5','9','6','9','7','9','8','9','9'
]

neededdigits(::Type{Float64}) = 309 + 17
neededdigits(::Type{Float32}) = 39 + 9
neededdigits(::Type{Float16}) = 9 + 5

@inline function writeshortest(buf::Vector{UInt8}, pos, x::T,
    plus=false, space=false, hash=true,
    precision=-1, expchar=UInt8('e'), padexp=false, decchar=UInt8('.')) where {T}
    neg = signbit(x)
    # special cases
    if x == 0
        if neg
            buf[pos] = UInt8('-')
            pos += 1
        elseif plus
            buf[pos] = UInt8('+')
            pos += 1
        elseif space
            buf[pos] = UInt8(' ')
            pos += 1
        end
        buf[pos] = UInt8('0')
        pos += 1
        if hash
            buf[pos] = decchar
            pos += 1
        end
        if precision == -1
            buf[pos] = UInt8('0')
            return pos + 1
        end
        while precision > 1
            buf[pos] = UInt8('0')
            pos += 1
            precision -= 1
        end
        return pos
    elseif isnan(x)
        buf[pos] = UInt8('N')
        buf[pos + 1] = UInt8('a')
        buf[pos + 2] = UInt8('N')
        return pos + 3
    elseif !isfinite(x)
        if neg
            buf[pos] = UInt8('-')
        end
        buf[pos + neg] = UInt8('I')
        buf[pos + neg + 1] = UInt8('n')
        buf[pos + neg + 2] = UInt8('f')
        return pos + neg + 3
    end

    bits = uint(x)
    mant = bits & (oftype(bits, 1) << mantissabits(T) - oftype(bits, 1))
    exp = Int((bits >> mantissabits(T)) & ((Int64(1) << exponentbits(T)) - 1))
    m2 = oftype(bits, Int64(1) << mantissabits(T)) | mant
    e2 = exp - bias(T) - mantissabits(T)
    fraction = m2 & ((oftype(bits, 1) << -e2) - 1)
    if e2 > 0 || e2 < -52 || fraction != 0
        if exp == 0
            e2 = 1 - bias(T) - mantissabits(T) - 2
            m2 = mant
        else
            e2 -= 2
        end
        even = (m2 & 1) == 0
        mv = oftype(m2, 4 * m2)
        mp = oftype(m2, mv + 2)
        mmShift = mant != 0 || exp <= 1
        mm = oftype(m2, mv - 1 - mmShift)
        vmIsTrailingZeros = false
        vrIsTrailingZeros = false
        lastRemovedDigit = 0x00
        if e2 >= 0
            q = log10pow2(e2) - (T == Float64 ? (e2 > 3) : 0)
            e10 = q
            k = pow5_inv_bitcount(T) + pow5bits(q) - 1
            i = -e2 + q + k
            vr, vp, vm = mulshiftinvsplit(T, mv, mp, mm, q, i)
            if T == Float32 || T == Float16
                if q != 0 && div(vp - 1, 10) <= div(vm, 10)
                    l = pow5_inv_bitcount(T) + pow5bits(q - 1) - 1
                    mul = T == Float32 ? FLOAT_POW5_INV_SPLIT[q] : HALF_POW5_INV_SPLIT[q]
                    lastRemovedDigit = (mulshift(mv, mul, -e2 + q - 1 + l) % 10) % UInt8
                end
            end
            if q <= qinvbound(T)
                if ((mv % UInt32) - 5 * div(mv, 5)) == 0
                    vrIsTrailingZeros = pow5(mv, q)
                elseif even
                    vmIsTrailingZeros = pow5(mm, q)
                else
                    vp -= pow5(mp, q)
                end
            end
        else
            q = log10pow5(-e2) - (T == Float64 ? (-e2 > 1) : 0)
            e10 = q + e2
            i = -e2 - q
            k = pow5bits(i) - pow5_bitcount(T)
            j = q - k
            vr, vp, vm = mulshiftsplit(T, mv, mp, mm, i, j)
            if T == Float32 || T == Float16
                if q != 0 && div(vp - 1, 10) <= div(vm, 10)
                    j = q - 1 - (pow5bits(i + 1) - pow5_bitcount(T))
                    mul = T == Float32 ? FLOAT_POW5_SPLIT[i + 2] : HALF_POW5_SPLIT[i + 2]
                    lastRemovedDigit = (mulshift(mv, mul, j) % 10) % UInt8
                end
            end
            if q <= 1
                vrIsTrailingZeros = true
                if even
                    vmIsTrailingZeros = mmShift
                else
                    vp -= 1
                end
            elseif q < qbound(T)
                vrIsTrailingZeros = pow2(mv, q - (T != Float64))
            end
        end
        removed = 0
        if vmIsTrailingZeros || vrIsTrailingZeros
            while true
                vpDiv10 = div(vp, 10)
                vmDiv10 = div(vm, 10)
                vpDiv10 <= vmDiv10 && break
                vmMod10 = (vm % UInt32) - UInt32(10) * (vmDiv10 % UInt32)
                vrDiv10 = div(vr, 10)
                vrMod10 = (vr % UInt32) - UInt32(10) * (vrDiv10 % UInt32)
                vmIsTrailingZeros &= vmMod10 == 0
                vrIsTrailingZeros &= lastRemovedDigit == 0
                lastRemovedDigit = vrMod10 % UInt8
                vr = vrDiv10
                vp = vpDiv10
                vm = vmDiv10
                removed += 1
            end
            if vmIsTrailingZeros
                while true
                    vmDiv10 = div(vm, 10)
                    vmMod10 = (vm % UInt32) - UInt32(10) * (vmDiv10 % UInt32)
                    vmMod10 != 0 && break
                    vpDiv10 = div(vp, 10)
                    vrDiv10 = div(vr, 10)
                    vrMod10 = (vr % UInt32) - UInt32(10) * (vrDiv10 % UInt32)
                    vrIsTrailingZeros &= lastRemovedDigit == 0
                    lastRemovedDigit = vrMod10 % UInt8
                    vr = vrDiv10
                    vp = vpDiv10
                    vm = vmDiv10
                    removed += 1
                end
            end
            if vrIsTrailingZeros && lastRemovedDigit == 5 && vr % 2 == 0
                lastRemovedDigit = UInt8(4)
            end
            output = vr + ((vr == vm && (!even || !vmIsTrailingZeros)) || lastRemovedDigit >= 5)
        else
            roundUp = false
            vpDiv100 = div(vp, 100)
            vmDiv100 = div(vm, 100)
            if vpDiv100 > vmDiv100
                vrDiv100 = div(vr, 100)
                vrMod100 = (vr % UInt32) - UInt32(100) * (vrDiv100 % UInt32)
                roundUp = vrMod100 >= 50
                vr = vrDiv100
                vp = vpDiv100
                vm = vmDiv100
                removed += 2
            end
            while true
                vpDiv10 = div(vp, 10)
                vmDiv10 = div(vm, 10)
                vpDiv10 <= vmDiv10 && break
                vrDiv10 = div(vr, 10)
                vrMod10 = (vr % UInt32) - UInt32(10) * (vrDiv10 % UInt32)
                roundUp = vrMod10 >= 5
                vr = vrDiv10
                vp = vpDiv10
                vm = vmDiv10
                removed += 1
            end
            output = vr + (vr == vm || roundUp || lastRemovedDigit >= 5)
        end
        nexp = e10 + removed
    else
        output = m2 >> -e2
        nexp = 0
        while true
            q = div(output, 10)
            r = (output % UInt32) - UInt32(10) * (q % UInt32)
            r != 0 && break
            output = q
            nexp += 1
        end
    end

    if neg
        buf[pos] = UInt8('-')
        pos += 1
    elseif plus
        buf[pos] = UInt8('+')
        pos += 1
    elseif space
        buf[pos] = UInt8(' ')
        pos += 1
    end

    olength = decimallength(output)
    exp_form = true
    pt = nexp + olength
    if -4 < pt <= (precision == -1 ? (T == Float16 ? 5 : 6) : precision) #&& !(pt >= olength && abs(mod(x + 0.05, 10^(pt - olength)) - 0.05) > 0.05)
        exp_form = false
        if pt <= 0
            buf[pos] = UInt8('0')
            pos += 1
            buf[pos] = decchar
            pos += 1
            for _ = 1:abs(pt)
                buf[pos] = UInt8('0')
                pos += 1
            end
        # elseif pt >= olength
            # nothing to do at this point
        # else
            # nothing to do at this point
        end
    else
        pos += 1
    end
    i = 0
    ptr = pointer(buf)
    ptr2 = pointer(DIGIT_TABLE)
    if (output >> 32) != 0
        q = output ÷ 100000000
        output2 = (output % UInt32) - UInt32(100000000) * (q % UInt32)
        output = q

        c = output2 % UInt32(10000)
        output2 = div(output2, UInt32(10000))
        d = output2 % UInt32(10000)
        c0 = (c % 100) << 1
        c1 = (c ÷ 100) << 1
        d0 = (d % 100) << 1
        d1 = (d ÷ 100) << 1
        memcpy(ptr, pos + olength - 2, ptr2, c0 + 1, 2)
        memcpy(ptr, pos + olength - 4, ptr2, c1 + 1, 2)
        memcpy(ptr, pos + olength - 6, ptr2, d0 + 1, 2)
        memcpy(ptr, pos + olength - 8, ptr2, d1 + 1, 2)
        i += 8
    end
    output2 = output % UInt32
    while output2 >= 10000
        c = output2 % UInt32(10000)
        output2 = div(output2, UInt32(10000))
        c0 = (c % 100) << 1
        c1 = (c ÷ 100) << 1
        memcpy(ptr, pos + olength - i - 2, ptr2, c0 + 1, 2)
        memcpy(ptr, pos + olength - i - 4, ptr2, c1 + 1, 2)
        i += 4
    end
    if output2 >= 100
        c = (output2 % UInt32(100)) << 1
        output2 = div(output2, UInt32(100))
        memcpy(ptr, pos + olength - i - 2, ptr2, c + 1, 2)
        i += 2
    end
    if output2 >= 10
        c = output2 << 1
        buf[pos + 1] = DIGIT_TABLE[c + 2]
        buf[pos - exp_form] = DIGIT_TABLE[c + 1]
    else
        buf[pos - exp_form] = UInt8('0') + (output2 % UInt8)
    end

    if !exp_form
        if pt <= 0
            pos += olength
            precision -= olength
            while hash && precision > 0
                buf[pos] = UInt8('0')
                pos += 1
                precision -= 1
            end
        elseif pt >= olength
            pos += olength
            precision -= olength
            for _ = 1:nexp
                buf[pos] = UInt8('0')
                pos += 1
                precision -= 1
            end
            if hash
                buf[pos] = decchar
                pos += 1
                if precision < 0
                    buf[pos] = UInt8('0')
                    pos += 1
                end
                while precision > 0
                    buf[pos] = UInt8('0')
                    pos += 1
                    precision -= 1
                end
            end
        else
            pointoff = olength - abs(nexp)
            memmove(ptr, pos + pointoff + 1, ptr, pos + pointoff, olength - pointoff + 1)
            buf[pos + pointoff] = decchar
            pos += olength + 1
            precision -= olength
            while hash && precision > 0
                buf[pos] = UInt8('0')
                pos += 1
                precision -= 1
            end
        end
    else
        if olength > 1 || hash
            buf[pos] = decchar
            pos += olength
            precision -= olength
        end
        if hash && olength == 1
            buf[pos] = UInt8('0')
            pos += 1
        end
        while hash && precision > 0
            buf[pos] = UInt8('0')
            pos += 1
            precision -= 1
        end

        buf[pos] = expchar
        pos += 1
        exp2 = nexp + olength - 1
        if exp2 < 0
            buf[pos] = UInt8('-')
            pos += 1
            exp2 = -exp2
        elseif padexp
            buf[pos] = UInt8('+')
            pos += 1
        end

        if exp2 >= 100
            c = exp2 % 10
            memcpy(ptr, pos, ptr2, 2 * div(exp2, 10) + 1, 2)
            buf[pos + 2] = UInt8('0') + (c % UInt8)
            pos += 3
        elseif exp2 >= 10
            memcpy(ptr, pos, ptr2, 2 * exp2 + 1, 2)
            pos += 2
        else
            if padexp
                buf[pos] = UInt8('0')
                pos += 1
            end
            buf[pos] = UInt8('0') + (exp2 % UInt8)
            pos += 1
        end
    end

    return pos
end

function writeshortest(x::T,
        plus::Bool=false,
        space::Bool=false,
        hash::Bool=true,
        precision::Integer=-1,
        expchar::UInt8=UInt8('e'),
        padexp::Bool=false,
        decchar::UInt8=UInt8('.')) where {T <: Base.IEEEFloat}
    buf = Base.StringVector(neededdigits(T))
    pos = writeshortest(buf, 1, x)
    @assert pos - 1 <= length(buf)
    return String(resize!(buf, pos - 1))
end


@impl begin
    struct DistMakeFactors
        numpartitions::Int64 = 100
    end

    function make_factors(sf::Dist{T},
            range::Vector{T},
            parranges::NTuple{N,Vector},
            id,
            parids::Tuple)::Tuple{Vector{<:Scruff.Utils.Factor}, Vector{<:Scruff.Utils.Factor}} where {T,N}

        (lowers, uppers) = bounded_probs(sf, range, ())
        keys = (id,)
        dims = (length(range),)
        return ([Factor(dims, keys, lowers)], [Factor(dims, keys, uppers)])
    end
end

@impl begin
    struct DistSendLambda end

    function send_lambda(sf::Dist{T},
                       lambda::Score{<:T},
                       range::__OptVec{<:T},
                       parranges::NTuple{N,Vector},
                       incoming_pis::Tuple,
                       parent_idx::Integer)::Score where {N,T}

        SoftScore(Float64[], Float64[])
    end
end


include("bomex_model.jl")

function main()
    # add a command line argument to specify the kind of surface flux
    # TODO: this will move to the future namelist functionality
    bomex_args = ArgParseSettings(autofix_names = true)
    add_arg_group!(bomex_args, "BOMEX")
    @add_arg_table! bomex_args begin
        "--surface-flux"
        help = "specify surface flux for energy and moisture"
        metavar = "prescribed|bulk"
        arg_type = String
        default = "prescribed"
        "--moisture-model"
        help = "specify cloud condensate model"
        metavar = "equilibrium|nonequilibrium"
        arg_type = String
        default = "equilibrium"
    end

    cl_args =
        ClimateMachine.init(parse_clargs = true, custom_clargs = bomex_args)

    surface_flux = cl_args["surface_flux"]
    moisture_model = cl_args["moisture_model"]

    FT = Float64
    config_type = AtmosLESConfigType

    # DG polynomial order
    N = 4
    # Domain resolution and size
    Δh = FT(100)
    Δv = FT(40)

    resolution = (Δh, Δh, Δv)

    # Prescribe domain parameters
    xmax = FT(6400)
    ymax = FT(6400)
    zmax = FT(3000)

    t0 = FT(0)

    # For a full-run, please set the timeend to 3600*6 seconds
    # and change the values in ConservationCheck
    # For the test we set this to == 20 minutes
    timeend = FT(1200)
    #timeend = FT(3600 * 6)
    CFLmax = FT(0.35)

    # Choose default IMEX solver
    ode_solver_type = ClimateMachine.IMEXSolverType()

    model = bomex_model(
        FT,
        config_type,
        zmax,
        surface_flux,
        moisture_model = moisture_model,
    )
    ics = model.problem.init_state_prognostic
    # Assemble configuration
    driver_config = ClimateMachine.AtmosLESConfiguration(
        "BOMEX",
        N,
        resolution,
        xmax,
        ymax,
        zmax,
        param_set,
        ics;
        solver_type = ode_solver_type,
        model = model,
    )

    solver_config = ClimateMachine.SolverConfiguration(
        t0,
        timeend,
        driver_config,
        init_on_cpu = true,
        Courant_number = CFLmax,
        CFL_direction = HorizontalDirection(),
    )
    dgn_config = config_diagnostics(driver_config)

    if moisture_model == "equilibrium"
        filter_vars = ("moisture.ρq_tot",)
    elseif moisture_model == "nonequilibrium"
        filter_vars = ("moisture.ρq_tot", "moisture.ρq_liq", "moisture.ρq_ice")
    end

    cbtmarfilter = GenericCallbacks.EveryXSimulationSteps(1) do
        Filters.apply!(
            solver_config.Q,
            filter_vars,
            solver_config.dg.grid,
            TMARFilter(),
        )
        nothing
    end

    check_cons = (
        ClimateMachine.ConservationCheck("ρ", "3000steps", FT(0.0001)),
        ClimateMachine.ConservationCheck("ρe", "3000steps", FT(0.0025)),
    )

    result = ClimateMachine.invoke!(
        solver_config;
        user_callbacks = (cbtmarfilter,),
        diagnostics_config = dgn_config,
        check_cons = check_cons,
        check_euclidean_distance = true,
    )
end

main()


type GbGlb
    # REMEMBER -- all multi-dim arrays are column-indexing!  [i,j] => [j*N+i]
    particles::Array{Float64,1} # [Ndim x Ndens]      // means of selected particles
    variance::Array{Float64,1}  # [Ndim x Ndens]      //   variance of selected particles
    p::Array{Float64,1}         # [Np]                // probability of ith kernel
    ind::Array{Int64,1}                    # current indexes of MCMC step
    Malmost::Array{Float64,1}
    Calmost::Array{Float64,1}   #[Ndim x 1]   // Means & Cov. of all indices but j^th
    # Random number callback
    randU::Array{Float64,1}     # [Npoints * Ndens * (Niter+1)] uniformly distrib'd random variables
    randN::Array{Float64,1}     # [Ndim * Npoints] normally distrib'd random variables
    Ndim::Int64
    Ndens::Int64
    Nlevels::Int64
    dNp::Int64
    newPoints::Array{Float64,1}
    newWeights::Array{Float64,1}
    newIndices::Array{Int64,1}
    trees::Array{BallTreeDensity,1}
    levelList::Array{Int64,2}
    levelListNew::Array{Int64,2}
    dNpts::Array{Int64,1}
    ruptr::Int64
    rnptr::Int64
end

function makeEmptyGbGlb()
   return GbGlb(zeros(0),
                zeros(0),
                zeros(0),
                zeros(Int64,0),
                zeros(0),
                zeros(0),
                zeros(0),
                zeros(0),
                0,0,0,0,
                zeros(0),
                zeros(0),
                ones(Int64,0),
                Array(BallTreeDensity,1),
                ones(Int64,1,0),
                ones(Int64,1,0),
                zeros(Int64,0),
                0, 0)
end

type MSCompOpt
  pT::Float64
  tmpC::Float64
  tmpM::Float64
end

function levelInit!(glb::GbGlb)
  for j in 1:glb.Ndens
    glb.dNpts[j] = 1
    glb.levelList[j,1] = root(glb.trees[j])
  end
end

# this is a weird function -- not tested at all at this point
function initIndices!(glb::GbGlb)
  count=1
  for j in 1:glb.Ndens
    dNp = glb.dNpts[j]
    zz=glb.levelList[j,1]
    z = 1
    while z <= dNp # ++z, used to be a for loop
      glb.p[z] = weight(glb.trees[j], zz);  # init by sampling from weights
        z+=1
        if z<=dNp zz=glb.levelList[j,z]; end
    end

    for z in 2:dNp
        glb.p[z] += glb.p[z-1];
    end
    zz=glb.levelList[j,1]
    z = 1
    while z <= (dNp-1)
      if (glb.randU[glb.ruptr] <= glb.p[z]) break; end #count
        z+=1
        if z<dNp zz=glb.levelList[j,z]; end
    end
    glb.ind[j] = zz;
    count+=1
    glb.ruptr += 1
  end
  #glb.randU = glb.randU[count:end]
end

function calcIndices!(glb::GbGlb)
  #println("oooooooooooooooooooooooooooooooo")
  @fastmath @inbounds begin
    for j in 1:glb.Ndens
      for z in 1:glb.Ndim
        #@show j, z, glb.ind[j]
        #@show mean(glb.trees[j],glb.ind[j])
        glb.particles[z+glb.Ndim*(j-1)] = mean(glb.trees[j],glb.ind[j], z)#[z];
        glb.variance[z+glb.Ndim*(j-1)]  = bw(glb.trees[j],glb.ind[j], z);
      end
    end
  end
  #println("pppppppppppppppppppppppppppppppp")
end

function samplePoint!(X::Array{Float64,1}, glb::GbGlb, frm::Int64)
  #counter = 1
  for j in 1:glb.Ndim
    mn=0.0; vn=0.0;
    for z in 1:glb.Ndens                             # Compute mean and variances of
      vn += 1.0/glb.variance[j+glb.Ndim*(z-1)]       #   product of selected particles
      #@show j, z, j+glb.Ndim*(z-1)
      #@show round([mn, glb.particles[j+glb.Ndim*(z-1)], glb.variance[j+glb.Ndim*(z-1)]],3)
      mn += glb.particles[j+glb.Ndim*(z-1)]/glb.variance[j+glb.Ndim*(z-1)]
    end
    vn = 1.0/vn; mn *= vn;
    glb.rnptr += 1
    X[j+frm] = mn + sqrt(vn) * glb.randN[glb.rnptr] #counter       # then draw a sample from it
    #counter+=1
  end
  #glb.randN = glb.randN[counter:end]   # TODO ugh
  return Union{}
end

function levelDown!(glb::GbGlb)
  for j in 1:glb.Ndens
    z = 1
    for y in 1:(glb.dNpts[j])
      #@show "left", y, j, left(glb.trees[j], glb.levelList[j,y])
      if validIndex(glb.trees[j], left(glb.trees[j], glb.levelList[j,y]))
        #@show "a", y, j, z, left(glb.trees[j], glb.levelList[j,y])
        glb.levelListNew[j,z] = left(glb.trees[j], glb.levelList[j,y])
        z+=1
      end
      #@show "right", y, j, right(glb.trees[j], glb.levelList[j,y])
      if validIndex(glb.trees[j], right(glb.trees[j], glb.levelList[j,y]))
        #@show "b", y, j, z, right(glb.trees[j], glb.levelList[j,y])
        glb.levelListNew[j,z] = right(glb.trees[j], glb.levelList[j,y]);
        z+=1
      end
      #@show "last", glb.ind[j], glb.levelList[j,y]
      if (glb.ind[j] == glb.levelList[j,y])                      # make sure ind points to
        #@show "c", y, j, z, glb.levelListNew[j,z-1]
        glb.ind[j] = glb.levelListNew[j,z-1]                     #  a child of the old ind
      end
    end
    glb.dNpts[j] = z-1
    #println("levelDown! -- glb.dNpts[j=$j]=$(glb.dNpts[j])")
  end
  tmp = glb.levelList                            # make new list the current
  glb.levelList = glb.levelListNew              #   list and recycle the old
  glb.levelListNew=tmp

  #println("A, levelList $(glb.levelList[1,:]), $(glb.levelList[2,:])")
  #println("A, levelListNew $(glb.levelListNew[1,:]), $(glb.levelListNew[2,:])")
  #println("A, ind $(glb.ind)")
end

function sampleIndices!(X::Array{Float64,1}, cmoi::MSCompOpt, glb::GbGlb, frm::Int64)#pT::Array{Float64,1}
  counter=1
  zz=0
  #z = 1
  for j in 1:glb.Ndens
    dNp = glb.dNpts[j]    #trees[j].Npts();
    cmoi.pT = 0.0

    ##z = 1
    zz=glb.levelList[j,1]#z
    #println("start zz=glb.levelList[$(j),$(z)]=$(zz)")
    for z in 1:dNp ##while <=
      #println("starting first z=$(z), dNp=$(dNp), zz=$(zz)")
      glb.p[z] = 0.0
      for i in 1:glb.Ndim
        tmp = X[i+frm] - mean(glb.trees[j], zz, i)#[i]
        glb.p[z] += (tmp*tmp) / bw(glb.trees[j], zz, i)#[i]
        glb.p[z] += Base.Math.JuliaLibm.log(bw(glb.trees[j], zz, i))
      end
      glb.p[z] = exp( -0.5 * glb.p[z] ) * weight(glb.trees[j], zz)
      cmoi.pT += glb.p[z]
      #println("end ++z zz=glb.levelList[$(j),$(z)]=$(zz)")
      ##z+=1
      #println("1 ++z=$(z)")
      zz = z<dNp ? glb.levelList[j,z+1] : zz
    end
    #println("after first zz=$(zz)")
    @simd for z in 1:dNp
        glb.p[z] /= cmoi.pT
    end
    @simd for z in 2:dNp
        glb.p[z] += glb.p[z-1]              # construct CDF and sample a
    end

    z=1
    zz=glb.levelList[j,z]
    #println("mid zz=glb.levelList[$(j),$(z)]=$(zz)")
    while z<=(dNp-1)
      #println("second zz=glb.levelList[$(j),$(z)]=$(zz)")
      if (glb.randU[glb.ruptr] <= glb.p[z]) # counter
        break;                              # new kernel from jth density
      end
      z+=1
      #println("2 ++z=$(z)")
      if z<=dNp
        zz=glb.levelList[j,z]
      else
        error("This should never happend due to -1 MSGibbs01.jl")
      end
    end
    glb.ind[j] = zz                          #  using those weights
    #println("sampleIndices -- glb.ind[j=$(j)]=$(glb.ind[j])")
    counter+=1
    glb.ruptr += 1
  end
  #glb.randU = glb.randU[counter:end]
  calcIndices!(glb);                         # recompute particles, variance
end

## SLOWEST PIECE OF THE COMPUTATION -- TODO
# easy PARALLELs overhead here is much slower, already tried -- rather search for BLAS optimizations...
function makeFasterSampleIndex!(j::Int64, cmo::MSCompOpt, glb::GbGlb)
  #pT::Array{Float64,1}
  cmo.tmpC = 0.0
  cmo.tmpM = 0.0

  zz=glb.levelList[j,1]
  #z=1
  for z in 1:(glb.dNpts[j])#dNp
    glb.p[z] = 0.0
    for i in 1:glb.Ndim
      cmo.tmpC = bw(glb.trees[j], zz, i) + glb.Calmost[i]
      cmo.tmpM = mean(glb.trees[j], zz, i) - glb.Malmost[i]
      glb.p[z] += abs2(cmo.tmpM)/cmo.tmpC + log(cmo.tmpC) # This is the slowest piece
    end
    glb.p[z] = exp( -0.5 * glb.p[z] ) * weight(glb.trees[j].bt, zz) # slowest piece
    z < glb.dNpts[j] ? zz = glb.levelList[j,(z+1)] : nothing
  end

  # incorrect! remember zz
  #@simd for z in 1:glb.dNpts[j]
  #  glb.p[z] = exp( -0.5 * glb.p[z] ) * weight(glb.trees[j], zz)
  #end
  nothing
end

function sampleIndex(j::Int64, cmo::MSCompOpt, glb::GbGlb)
#pT::Array{Float64,1}
  #dNp = glb.dNpts[j];  #trees[j].Npts();
  cmo.pT = 0.0

  # determine product of selected particles from all but jth density
  for i in 1:glb.Ndim
    iCalmost = 0.0; iMalmost = 0.0;
    for k in 1:glb.Ndens
      if (k!=j) iCalmost += 1.0/glb.variance[i+glb.Ndim*(k-1)]; end
      if (k!=j) iMalmost += glb.particles[i+glb.Ndim*(k-1)]/glb.variance[i+glb.Ndim*(k-1)]; end
    end
    glb.Calmost[i] = 1/iCalmost;
    glb.Malmost[i] = iMalmost * glb.Calmost[i];
  end

  makeFasterSampleIndex!(j, cmo, glb)

  @inbounds @simd for k in 1:glb.dNpts[j]
    cmo.pT += glb.p[k]
  end

  @inbounds @simd for z in 1:glb.dNpts[j]#dNp
    glb.p[z] /= cmo.pT            # normalize weights# normalize weights
  end
  @inbounds @simd for z in 2:glb.dNpts[j]#dNp
    glb.p[z] += glb.p[z-1]    # construct CDF and sample
  end
  zz=glb.levelList[j,1]
  z=1
  while z<=(glb.dNpts[j]-1)#dNp
    if (glb.randU[glb.ruptr] <= glb.p[z]) break;  end   #1  #   a new kernel from the jth
    z+=1
    if z<=glb.dNpts[j]#dNp
      zz=glb.levelList[j,z]
      #println("sampleIndex 2++z, , zz=glb.levelList[$(j),$(z)]=$(zz)")
    end
  end
  glb.ind[j] = zz;                                          #   density using these weights
  #glb.randU = glb.randU[2:end]
  glb.ruptr += 1

  for i in 1:glb.Ndim
    #@show round(glb.trees[j].means, 3)
    #@show i, j, glb.ind[j], mean(glb.trees[j], glb.ind[j])
    glb.particles[i+glb.Ndim*(j-1)] = mean(glb.trees[j], glb.ind[j], i)#[i]
    glb.variance[i+glb.Ndim*(j-1)]  = bw(glb.trees[j], glb.ind[j], i)#[i];
  end
end

function printGlbs(g::GbGlb, tag=Union{})
    if tag==Union{}
        println("=========================================================================")
    else
        println(string(tag,"================================================================"))
    end
    println("Ndim=$(g.Ndim), Ndens=$(g.Ndens), Nlevels=$(g.Nlevels), dNp=$(g.dNp), dNpts=$(g.dNpts)")
    @show g.ind
    @show round(g.particles,2)
    @show round(g.variance,2)
    @show round(g.p,2)
    @show round(g.Malmost,2)
    @show round(g.Calmost,2)
    @show g.levelList
    @show g.levelListNew
    @show round(g.newPoints,4)
    @show g.newIndices
end

function gibbs1(Ndens::Int64, trees::Array{BallTreeDensity,1},
                Np::Int64, Niter::Int64,
                pts::Array{Float64,1}, ind::Array{Int64,1},
                randU::Array{Float64,1}, randN::Array{Float64,1})

    glbs = makeEmptyGbGlb()
    glbs.Ndens = Ndens
    glbs.trees = trees
    glbs.newPoints = pts
    glbs.newIndices = ind
    glbs.randU = randU
    glbs.randN = randN
    glbs.Ndim = trees[1].bt.dims

    maxNp = 0                         # largest # of particles we deal with
    for tree in trees
        if (maxNp < Npts(tree))
            maxNp = Npts(tree)
        end
    end

    glbs.ind = ones(Int64,Ndens)
    glbs.p = zeros(maxNp)
    glbs.Malmost = zeros(glbs.Ndim)
    glbs.Calmost = zeros(glbs.Ndim)
    glbs.Nlevels = floor(Int64,((log(maxNp)/log(2))+1))
    glbs.particles = zeros(glbs.Ndim*Ndens)
    glbs.variance  = zeros(glbs.Ndim*Ndens)
    glbs.dNpts = zeros(Int64,Ndens)
    glbs.levelList = ones(Int64,Ndens,maxNp)
    glbs.levelListNew = ones(Int64,Ndens,maxNp)
    cmo = MSCompOpt(0.0, 0.0, 0.0)
    cmoi = MSCompOpt(0.0, 0.0, 0.0)

    ##@show glbs.ruptr, size(glbs.randU)
    for s in 1:Np   #   (for each sample:)
          #println("+++++++++++++++++++++++++++++NEW POINT+++++++++++++++++++++++++++++++++++++++")
        frm = ((s-1)*glbs.Ndim)

        levelInit!(glbs)
        initIndices!(glbs)
        calcIndices!(glbs)

        #printGlbs(glbs, string("s=",s-1.5))

        for l in 1:glbs.Nlevels
          samplePoint!(glbs.newPoints, glbs, frm)
          levelDown!(glbs);
          sampleIndices!(glbs.newPoints, cmoi, glbs, frm);

          #printGlbs(glbs, string("s=",s-1.5+.1*l))

          for i in 1:Niter         #   perform Gibbs sampling
            for j in 1:glbs.Ndens
              @fastmath @inbounds sampleIndex(j, cmo, glbs);
            end
          end
        end

        for j in 1:glbs.Ndens                       #/ save and
          glbs.newIndices[(s-1)*glbs.Ndens+j] = getIndexOf(glbs.trees[j], glbs.ind[j])+1;  # return particle label
        end

        samplePoint!(glbs.newPoints, glbs, frm);                            # draw a sample from that label
               #glbs.newIndices = glbs.newIndices[glbs.Ndens:end]  # move pointers to next sample
               #glbs.newPoints  = glbs.newPoints[glbs.Ndim:end]
        #printGlbs(glbs, string("s=",s-1))
    end
    ##@show glbs.ruptr, size(glbs.randU), size(glbs.randU,1)+glbs.ruptr
    # deref the glbs data and let gc() remove it later, or force with gc()
    glbs = 0
    #@show @elapsed gc()
    #error("gibbs1 -- not implemented yet")
    nothing
end

# function remoteProdAppxMSGibbsS(npd0::BallTreeDensity,
#                           npds::Array{BallTreeDensity,1}, anFcns, anParams,
#                           Niter::Int64=5)
#
#   len = length(npds)
#   arr = Array{Array{Float64,2},1}(len)
#
#   d = npds[1].bt.dims
#   N = npds[1].bt.num_points
#
#
#   for i in 1:len
#     arr[i] = reshape(npds[i].bt.centers[(N*d+1):end],d,N)
#   end
#
#   pts, bw = remoteProd(arr)
#
#   return pts, -1
# end

function prodAppxMSGibbsS(npd0::BallTreeDensity,
                          npds::Array{BallTreeDensity,1}, anFcns, anParams,
                          Niter::Int64=5)
    # See  Ihler,Sudderth,Freeman,&Willsky, "Efficient multiscale sampling from products
    #         of Gaussian mixtures", in Proc. Neural Information Processing Systems 2003

    Ndens = length(npds)              # of densities
    Ndim  = npds[1].bt.dims           # of dimensions
    Np    = Npts(npd0)                # of points to sample

    # skipping analytic functions for now TODO ??

    UseAn = false
    #??pointsM = zeros(Ndim, Np)
    points = zeros(Ndim*Np)
    #??plhs[1] = mxCreateNumericMatrix(Ndens, Np, mxUINT32_CLASS, mxREAL);
    indices=ones(Int64,Ndens*Np)
    maxNp = Np                        # largest # of particles we deal with
    for tree in npds
        if (maxNp < Npts(tree))
            maxNp = Npts(tree)
        end
    end
    Nlevels = floor(Int64,(log(Float64(maxNp))/log(2.0))+1.0)  # how many levels to a balanced binary tree?

    # Generate enough random numbers to get us through the rest of this
    if true
      randU = rand(Int64(Np*Ndens*(Niter+2)*Nlevels))
      randN = randn(Int64(Ndim*Np*(Nlevels+1)))
    else
        randU = vec(readdlm("randU.csv"))
        randN = vec(readdlm("randN.csv"))
    end

    #@show size(randU), size(randN)
    gibbs1(Ndens, npds, Np, Niter, points, indices, randU, randN);

    return reshape(points, Ndim, Np), reshape(indices, Ndens, Np)
end

function *(p1::BallTreeDensity, p2::BallTreeDensity)
  numpts = round(Int,(Npts(p1)+Npts(p2))/2)
  d = Ndim(p1)
  d != Ndim(p2) ? error("kdes must have same dimension") : nothing
  dummy = kde!(rand(d,numpts),[1.0]);
  pGM, = prodAppxMSGibbsS(dummy, [p1;p2], Union{}, Union{}, 5)
  return kde!(pGM)
end


# 这是一个风洞配平程序
# 配平结果要求旋翼没有侧向力
# ——————要求旋翼没有俯仰和滚装力矩
# ——————要求旋翼的桨尖平面与旋翼轴垂直
# 配平结果不要求配平旋翼拉力
# 最后根据一般性的风洞培平要求，决定只通过挥舞量配平操纵量

# 程序首次编辑时间为11/11/2017

function wttrim()
  jacobi = [A0/A_a1 0 A_B1/A_a1;
              γ_B*F0 -B_A1/B_b1  γ_B*F_B1]
  soljaco = inv(jacobi)
  # delthe_lon = 0.1  #纵向周期变距步进
  # delthe_lat = 0.1  #横向周期变距步进

  while abs(Fhub[2])>1e-3 | abs(Mx)>1e-3 | abs(My)>1e-3
    the_lon = the_lon+delthe_lon
    the_lat = the_lat+delthe_lat
    aoaget(v_all,ψ,the_lon,the_lat)
  end

  return the_lon,the_lat

end


include("_models.jl")
include("ensembles.jl")