module FeatureSelectors

using DataFrames
using Random

using AutoMLPipeline.AbsTypes
using AutoMLPipeline.BaseFilters
using AutoMLPipeline.Utils

import AutoMLPipeline.AbsTypes: fit!, transform!
export fit!, transform!
export FeatureSelector, CatFeatureSelector, NumFeatureSelector, CatNumDiscriminator

export feature_test

# generic way to extract num/cat features by specifying their columns
mutable struct FeatureSelector <: Transformer
    name::String
    model::Dict
    args::Dict

    function FeatureSelector(args::Dict = Dict())
	default_args = Dict(
			    :name => "featureselector",
			    :columns => Int[],
			    )
	cargs=nested_dict_merge(default_args,args)
	cargs[:name] = cargs[:name]*"_"*randstring(3)
	new(cargs[:name],Dict(),cargs)
    end
end

function FeatureSelector(cols::Vector{Int})
    FeatureSelector(Dict(:columns => cols))
end

function fit!(ft::FeatureSelector, features::DataFrame, labels::Vector=[])
    if features == DataFrame()
	error("empty dataframe")
    end
    ft.model = ft.args
end

function transform!(ft::FeatureSelector, features::DataFrame)
    if features == DataFrame()
	error("empty dataframe")
    end
    return features[:,ft.model[:columns]]
end

# ----------
# automatically extracts cat features based on their inferred element non-number types
mutable struct CatFeatureSelector <: Transformer
    name::String
    model::Dict
    args::Dict

    function CatFeatureSelector(args::Dict = Dict())
	default_args = Dict(
			    :name => "catfeatsel",
			    )
	cargs=nested_dict_merge(default_args,args)
	cargs[:name] = cargs[:name]*"_"*randstring(3)
	new(cargs[:name],Dict(),cargs)
    end
end

function fit!(ft::CatFeatureSelector, features::DataFrame, labels::Vector=[])
    if features == DataFrame()
        error("empty dataframe")
    end
    catcols,_ = find_catnum_columns(features)

    # create model
    ft.model = Dict(
		    :nominal_columns => catcols
		    )
end

function transform!(ft::CatFeatureSelector, features::DataFrame)
    catcols = ft.model[:nominal_columns]
    return features[:,catcols]
end

# ---------
# automatically extracts numeric features based on their inferred element types
mutable struct NumFeatureSelector <: Transformer
    name::String
    model::Dict
    args::Dict

    function NumFeatureSelector(args::Dict = Dict())
	default_args = Dict(
			    :name => "numfeatsel"
			    )
	cargs=nested_dict_merge(default_args,args)
	cargs[:name] = cargs[:name]*"_"*randstring(3)
	new(cargs[:name],Dict(),cargs)
    end
end

function fit!(ft::NumFeatureSelector, features::DataFrame, labels::Vector=[])
    if features == DataFrame()
        error("empty dataframe")
    end
    _,realcols = find_catnum_columns(features)

    # create model
    ft.model = Dict(
		    :real_columns => realcols
		    )
end

function transform!(ft::NumFeatureSelector, features::DataFrame)
    realcols = ft.model[:real_columns]
    return features[:,realcols]
end

# ---------
# convert numeric categories to string based on count of unique elements
mutable struct CatNumDiscriminator <: Transformer
    name::String
    model::Dict
    args::Dict

    function CatNumDiscriminator(args::Dict = Dict())
	default_args = Dict(
			    :name => "catnumdisc",
			    # default max categories for numeric-encoded categories
			    :maxcategories => 24,
			    )
	cargs=nested_dict_merge(default_args,args)
	cargs[:name] = cargs[:name]*"_"*randstring(3)
	new(cargs[:name],Dict(),cargs)
    end
end

function CatNumDiscriminator(maxcat::Int)
    CatNumDiscriminator(Dict(:maxcategories=>maxcat))
end

function fit!(ft::CatNumDiscriminator, features::DataFrame, labels::Vector=[])
    if features == DataFrame()
        error("empty dataframe")
    end
    catcols,realcols = find_catnum_columns(features,ft.args[:maxcategories])

    # create model
    ft.model = Dict(
		    :real_columns => realcols,
		    :nominal_columns => catcols
		    )
end

function transform!(ft::CatNumDiscriminator, features::DataFrame)
    catcols = ft.model[:nominal_columns]
    features[!,catcols] .= features[!,catcols] .|> string
    return features
end

function feature_test()
    data = getiris()
    X = data[:,1:5]
    X[!,5] = X[!,5] .|> string
    catfeat = FeatureSelector([5])
    numfeat = FeatureSelector([1,2,3,4])
    autocat = CatFeatureSelector()
    autonum = NumFeatureSelector()
    @assert (fit_transform!(catfeat,X) .== X[:,5]) |> Matrix |> sum == 150
    @assert (fit_transform!(numfeat,X) .== X[:,1:4]) |> Matrix |> sum == 600
    @assert (fit_transform!(autocat,X) .== X[:,5]) |> Matrix |> sum == 150
    @assert (fit_transform!(autonum,X) .== X[:,1:4]) |> Matrix |> sum == 600
    catnumdata = hcat(X,repeat([1,2,3,4,5],30))
    catnum = CatNumDiscriminator()
    fit_transform!(catnum,catnumdata)
    @assert eltype(catnumdata[:,6]) <: String
    catnumdata = hcat(X,repeat([1,2,3,4,5],30))
    catnum = CatNumDiscriminator(0)
    fit_transform!(catnum,catnumdata)
    @assert eltype(catnumdata[:,6]) <: Int
end

end


module Simulate_FourBar

using  Modia3D
import Modia3D.ModiaMath

# Solids
groundMaterial = Modia3D.Material(color="DarkGreen", transparency=0.5)
massProperties = "Aluminium"
vmat1 = Modia3D.Material(color="LightBlue" , transparency=0.5)
vmat2 = Modia3D.Material(color="Red")

@assembly Bar(;Lx = 0.1, Ly=Lx/5, Lz=Ly) begin
   frame0 = Modia3D.Object3D(Modia3D.Solid(Modia3D.SolidBeam(Lx,Ly,Lz), nothing, vmat1))
   frame1 = Modia3D.Object3D(frame0; r=[-Lx/2, 0.0, 0.0])
   frame2 = Modia3D.Object3D(frame0; r=[ Lx/2, 0.0, 0.0])
   cyl    = Modia3D.Cylinder(Ly/2,1.2*Ly; material=vmat2)
   cyl1   = Modia3D.Object3D(frame1, cyl)
   cyl2   = Modia3D.Object3D(frame2, cyl)
end

@assembly Fourbar(;Lx = 0.1, Ly=Lx/5, Lz=Ly, groundWidth=Lx, groundHeight=0.1*Lx) begin
   world  = Modia3D.Object3D(Modia3D.CoordinateSystem(0.6))
   frame1 = Modia3D.Object3D(world ; r=[0.5*Lx, 0.0, groundWidth/2])
   frame2 = Modia3D.Object3D(frame1; r=[Lx    , 0.0, 0.0])
   ground = Modia3D.Object3D(world , Modia3D.Box(3*Lx, groundWidth, groundHeight; material=groundMaterial);
                                              r = [1.5*Lx, -groundHeight/2, groundWidth/2],
                                              R = ModiaMath.rot_nxy([-1,0,0], [0,0,1]))
   bar1   = Bar(Lx=Lx, Ly=Ly, Lz=Lz)
   bar2   = Bar(Lx=Lx, Ly=Ly, Lz=Lz)
   bar3   = Bar(Lx=Lx, Ly=Ly, Lz=Lz)

   rev1   = Modia3D.Revolute(frame1     , bar1.frame1; phi_start =  pi/2)
   rev2   = Modia3D.Revolute(bar1.frame2, bar2.frame1; phi_start = -pi/2)
   rev3   = Modia3D.Revolute(frame2     , bar3.frame1; phi_start =  pi/2)
   rev4   = Modia3D.Revolute(bar3.frame2, bar2.frame2; phi_start = -pi/2)
end

#hier wird eine scene erstellt
fourbar = Fourbar(Lx=1.0, sceneOptions=Modia3D.SceneOptions(visualizeFrames=true,
                                                    defaultFrameLength=0.3,
                                                    enableContactDetection=false))
#hier wird wieder eine scene erstellt
model = Modia3D.SimulationModel(fourbar)

#Modia3D.visualizeAssembly!( fourbar )

println("... success of Simulate_FourBar.jl!")
end


"""
    abstract type AbstractUInt16RNG <: AbstractRNG
Supertype for an `AbstractRNG` that is fed random UInt16s.
"""
abstract type AbstractUInt16RNG <: AbstractRNG end

"""
    getrand(r::AbstractUInt16RNG, T::Type, n::Integer)

Generate `n` random `T`s using UInt16s from `r`.
"""
function getrand(::AbstractUInt16RNG, ::Type{UInt16}, n::Integer)
    error("NotImplemented: getrand must be implemented for UInt16 as an AbstractUInt16RNG.")
end
getrand(r::AbstractUInt16RNG, ::Type{UInt32}, n::Integer) =
    Vector{UInt32}(getrand(r, UInt16, n)) .<< 16 .+ Vector{UInt32}(getrand(r, UInt16, n))
getrand(r::AbstractUInt16RNG, ::Type{UInt64}, n::Integer) =
    Vector{UInt64}(getrand(r, UInt32, n)) .<< 32 .+ Vector{UInt64}(getrand(r, UInt32, n))
getrand(r::AbstractUInt16RNG, ::Type{UInt128}, n::Integer) =
    Vector{UInt128}(getrand(r, UInt64, n)) .<< 64 .+ Vector{UInt128}(getrand(r, UInt64, n))

function getrand(r::AbstractUInt16RNG, ::Type{UInt8}, n::Integer)
    vals = Vector{UInt8}(undef, n)
    u16s = getrand(r, UInt16, ceil(Int, n / 2))
    for i in 1:length(u16s)-1
        vals[2i-1:2i] = digits(UInt8, u16s[i], base=256, pad=2)
    end
    if n % 2 == 0
        vals[end-1:end] = digits(UInt8, u16s[end], base=256, pad=2)
    else
        vals[end] = u16s[end] % UInt8
    end
    vals
end
function getrand(r::AbstractUInt16RNG, ::Type{Bool}, n::Integer)
    bools = Vector{Bool}(undef, n)
    uint16needed = ceil(Int, n / 16)
    uint16s = getrand(r, UInt16, uint16needed)
    for i in 1:(uint16needed-1)
        bools[16(i-1)+1:16i] = digits(Bool, uint16s[i], base=2, pad=16)
    end
    bools[16*(uint16needed-1)+1:n] = digits(Bool, uint16s[end], base=2, pad=16)[1:n%16]
    bools
end

function rand!(r::AbstractUInt16RNG, A::Array{Bool}, ::Random.SamplerType{Bool})
    A[:] = getrand(r, Bool, length(A))
    A
end

rand(r::AbstractUInt16RNG, ::Random.SamplerType{Bool}) = getrand(r, Bool, 1)[1]

const UInts = (UInt8, UInt16, UInt32, UInt64, UInt128)
const Ints = (Int8, Int16, Int32, Int64, Int128)

for T in UInts
    @eval function rand!(r::AbstractUInt16RNG, A::Array{$T}, ::Random.SamplerType{$T})
        A[:] = getrand(r, $T, length(A))
        A
    end
    @eval rand(r::AbstractUInt16RNG, ::Random.SamplerType{$T}) = getrand(r, $T, 1)[1]
end
for (T, U) in zip(Ints, UInts)
    @eval function rand!(r::AbstractUInt16RNG, A::Array{$T}, ::Random.SamplerType{$T})
        A[:] = reinterpret.($T, getrand(r, $U, length(A)))
        A
    end
    @eval rand(r::AbstractUInt16RNG, ::Random.SamplerType{$T}) = reinterpret.($T, getrand(r, $U, 1))[1]
end

Random.rng_native_52(::AbstractUInt16RNG) = Float64

const F64BITMASK1 = 0b0011111111111111111111111111111111111111111111111111111111111111
const F64BITMASK2 = 0b0011111111110000000000000000000000000000000000000000000000000000

const F32BITMASK1 = 0b00111111111111111111111111111111
const F32BITMASK2 = 0b00111111100000000000000000000000

function rand!(r::AbstractUInt16RNG, A::Array{Float64}, ::Random.SamplerTrivial{Random.CloseOpen01{Float64}, Float64})
    A[:] = reinterpret.(Float64, getrand(r, UInt64, length(A)) .& F64BITMASK1 .| F64BITMASK2) .- 1
    A
end
function rand(r::AbstractUInt16RNG, ::Random.CloseOpen12{Float64})
    reinterpret(Float64, getrand(r, UInt64, 1)[1] & F64BITMASK1 | F64BITMASK2)
end

function rand!(r::AbstractUInt16RNG, A::Array{Float32}, ::Random.SamplerTrivial{Random.CloseOpen01{Float32}, Float32})
    A[:] = reinterpret.(Float32, getrand(r, UInt32, length(A)) .& F32BITMASK1 .| F32BITMASK2) .- 1
    A
end

# Online and cached UInt16 sources

mutable struct OnlineUInt16RNG <: AbstractUInt16RNG
    label::String
    batchsize::Integer
    cache::Vector{UInt16}
    pointer::Integer
    fetchrand::Function
    prefillthreshold::Integer
    cacheprefill::Vector{UInt16}
    currentlyprefilling::Bool
    function OnlineUInt16RNG(label::String, fetchrand::Function, batchsize::Integer,
                             prefillthreshold::Integer; prefill=prefillthreshold > 0)
        rng = new(label, batchsize, UInt16[], 1, fetchrand,
                  prefillthreshold, UInt16[], false)
        if prefill
            Threads.@spawn prefillcache(rng)
        end
        rng
    end
end

const term256greentored = [119, 190, 220, 221, 215, 208, 202, 160, 124]
function show(io::IO, r::OnlineUInt16RNG)
    print(io, "$(r.label). ")
    unusedcached = length(r.cache) - r.pointer + 1
    color = if r.prefillthreshold == 0
        [:red, :yellow, :green][round(Int, 3*unusedcached/r.batchsize)]
    elseif unusedcached > r.prefillthreshold
        :green
    elseif unusedcached > 0.5r.prefillthreshold
        :yellow
    else
        :red
    end
    printstyled(io, unusedcached; color)
    if r.currentlyprefilling
        printstyled(io, '*'; color=:light_black)
    end
    printstyled(io, '/', r.batchsize; bold=true)
    print(io, " UInt16s cached.")
end

function getrand(r::OnlineUInt16RNG, ::Type{UInt16}, n::Integer)
    if r.batchsize ≤ 0
        r.fetchrand(n)
    else
        ncached = length(r.cache) - r.pointer + 1
        if ncached > n
            r.pointer += n
            if length(r.cacheprefill) == 0 &&
                length(r.cache) - r.pointer + 1 < r.prefillthreshold &&
                ! r.currentlyprefilling
                Threads.@spawn prefillcache(r)
            end
            r.cache[r.pointer-n:r.pointer-1]
        elseif ncached + length(r.cacheprefill) > n
            oldcache = r.cache[r.pointer:end]
            r.cache = r.cacheprefill
            r.cacheprefill = []
            r.pointer = 1
            [oldcache; getrand(r, UInt16, n - length(oldcache))]
        else
            rands = r.fetchrand(n - ncached + r.batchsize)
            res = [r.cache[r.pointer:end]; rands[r.batchsize+1:end]]
            r.cache = rands[1:r.batchsize]
            res
        end
    end
end

function prefillcache(r::OnlineUInt16RNG)
    r.currentlyprefilling = true
    if length(r.cache) == 0
        r.cache = r.fetchrand(r.batchsize)
    else
        r.cacheprefill = r.fetchrand(r.batchsize)
    end
    r.currentlyprefilling = false
end


##===----------------------------------------------------------------------===##
##                                   ACCELERATE.JL                            ##
##                                                                            ##
##===----------------------------------------------------------------------===##
##                                                                            ##
##  This file checks for the available frameworks and imports only the        ##
##  frameworks available; it also coordinates access to three frameworks      ##
##  through the unified Vectorize.f() syntax                                  ##
##                                                                            ##
##===----------------------------------------------------------------------===##
module Vectorize

using Libdl
using SpecialFunctions
using Statistics
export @vectorize, @replacebase

OS = Sys.KERNEL

## FUNCTION DICTIONARY
functions = Dict()

## Function for adding to function dictionary
function addfunction(d::Dict, k, v)
    if k in keys(d)
        append!(d[k], [v])
    else
        d[k] = []
        append!(d[k], [v])
    end
end

## START OSX
## On OS X, check if Accelerate can be found - future proofing
OS == :Darwin ? (ACCELERATE = true) : (ACCELERATE = false)
if ACCELERATE == true
    if isfile("/System/Library/Frameworks/Accelerate.framework/Accelerate")
        eval(:(include("Accelerate.jl")))
    else
        warn("Unable to locate the Apple Accelerate framework")
    end
end
## END OSX

## Find Yeppp library
libyeppp_ = Libdl.find_library(["libyeppp"])
if libyeppp_ != ""
    @eval const global libyeppp = libyeppp_
else
    currdir = @__FILE__
    @static if Sys.iswindows()
        bindir = currdir[1:end-16]*"deps\\src\\yeppp\\binaries\\"
    else
        bindir = currdir[1:end-16]*"deps/src/yeppp/binaries/"
    end

    if OS == :Darwin
        @eval const global libyeppp = bindir*"macosx/x86_64/libyeppp.dylib"
    elseif OS == :Linux
        @eval const global libyeppp = bindir*"linux/x86_64/libyeppp.so"
    elseif OS == :NT
        @eval const global libyeppp = bindir*"windows\\amd64\\yeppp.dll"
    end
end

if isfile(libyeppp)
    include("Yeppp.jl") # include Yeppp if present
end

## Find VML
const global libvml = Libdl.find_library(["libmkl_rt"], ["/opt/intel/mkl/lib"])
if libvml != ""
    include("VML.jl")
end

# vectorize macro
macro vectorize(ex)
    len = length(ex.args)
    if len == 2
        f = :(Vectorize.$(ex.args[1]))
        arg = ex.args[2]
        return esc(:($f($arg)))
    elseif len == 3
        f = :(Vectorize.$(ex.args[1]))
        arg1 = ex.args[2]
        arg2 = ex.args[3]
        return esc(:($f($arg1, $arg2)))
    else
        f = :(Vectorize.$(ex.args[1]))
        arg1 = ex.args[2]
        arg2 = ex.args[3]
        arg3 = ex.args[4]
        return esc(:($f($arg1, $arg2, $arg3)))
    end
end

# replacebase macro
"""
Replace all broadcasted base functions with benchmarked vectorized equivalents.
Alternatively, if the user may pass the functions to overload instead.
"""
macro replacebase(fs...)
    b = Expr(:block)
    for ((fvec, args), vectorized_f) in functions
        m, f = get_corresponding_f(fvec)
        if isempty(fs) || f in fs
            if m == :Base
                if ismutating(fvec)
                    Tdest = first(args)
                    Targs = makeargs(args[2:end])
                    e = quote
                        (Base.copyto!)(dest::$Tdest, bc::Base.Broadcast.Broadcasted{Style, Axes, typeof($m.$f), $Targs}) where {Style, Axes, N} = (Vectorize.$fvec)(dest, bc.args...)
                    end
                else
                    Targs = makeargs(args)
                    e = quote
                        (Base.copy)(bc::Base.Broadcast.Broadcasted{Style, Axes, typeof($m.$f), $Targs}) where {Style, Axes, N} = (Vectorize.$fvec)(bc.args...)
                    end
                end
                push!(b.args, e)
            end
        end
    end
    b
end

function get_corresponding_f(fvec)
    fstr = string(fvec)
    if ismutating(fvec)
        fstr = chop(fstr)
    end
    if fstr == "add"
        fstr = "+"
    elseif fstr == "sub"
        fstr = "-"
    elseif fstr == "mul"
        fstr = "*"
    elseif fstr == "div"
        fstr = "/"
    elseif fstr == "rec"
        fstr = "inv"
    elseif fstr == "pow"
        fstr = "^"
    end

    m = :Base
    if fstr in ("cdfnorminv", "erf", "erfc", "erfi", "erfinv", "erfcinv", "cdfnorm")
        m = :SpecialFunctions
    elseif fstr in ("mean", )
        m = :Statistics
    elseif fstr in ("dot", )
        m = :LinearAlgebra
    elseif fstr in ("pow3o2", "frac", "mulbyconj", "tanpi", "fdiv", "invsqrt",
                    "sqr", "pow2o3", "summag", "sumsqr", "invcbrt")
        m = :None
    end

    fsym = Meta.parse(fstr)
    return m, fsym
end
@inline ismutating(f) = last(string(f)) == '!'
function makeargs(Targs)
    arg_str = "Tuple{"
    for T in Targs
        arg_str *= "$T, "
    end
    arg_str = arg_str[1:end-2]
    arg_str *= "}"
    arg_str = replace(arg_str, " where N" => "")

    return Meta.parse(arg_str)
end

# Include optimized functions
include("Functions.jl")

end # module


@cell "Figure and Subplots" begin
    fig, _ = scatter(randn(100, 2), color = :red)
    scatter(fig[1, 2], randn(100, 2), color = :blue)
    scatter(fig[2, 1:2], randn(100, 2), color = :green)
    scatter(fig[1:2, 3][1:2, 1], randn(100, 2), color = :black)
    scatter(fig[1:2, 3][3, 1], randn(100, 2), color = :gray)
    fig
end

@cell "Figure with Layoutables" begin
    fig = Figure(resolution = (900, 900))
    ax, sc = scatter(fig[1, 1][1, 1], randn(100, 2), axis = (;title = "Random Dots", xlabel = "Time"))
    sc2 = scatter!(ax, randn(100, 2) .+ 2, color = :red)
    ll = fig[1, 1][1, 2] = Legend(fig, [sc, sc2], ["Scatter", "Other"])
    lines(fig[2, 1:2][1, 3][1, 1], 0..3, sin ∘ exp, axis = (;title = "Exponential Sine"))
    heatmap(fig[2, 1:2][1, 1], randn(30, 30))
    heatmap(fig[2, 1:2][1, 2], randn(30, 30), colormap = :grays)
    lines!(fig[2, 1:2][1, 2], cumsum(rand(30)), color = :red, linewidth = 10)
    surface(fig[1, 2], collect(1.0:40), collect(1.0:40), (x, y) -> 10 * cos(x) * sin(y))
    fig[2, 1:2][2, :] = Colorbar(fig, vertical = false,
        height = 20, ticklabelalign = (:center, :top), flipaxis = false)
    fig[3, :] = Menu(fig, options = ["A", "B", "C"])
    lt = fig[0, :] = Label(fig, "Figure Demo")
    fig[5, :] = Textbox(fig)
    fig
end


module Ifpack2
  using CxxWrap, MPI
  import ..libjltrilinos
  import ..Teuchos
  import ..Tpetra

  @wrapmodule(libjltrilinos, :register_ifpack2)

  function default_parameters(ifpack_prec = "ILUT")
    pl = Teuchos.ParameterList("Ifpack2")
    ifpack2 = Teuchos.ParameterList("Ifpack2")
    pl["Preconditioner Type"] = ifpack_prec
    pl[ifpack_prec] = Teuchos.ParameterList(ifpack_prec)
  end

end


import Future

dothreads = Threads.nthreads() > 1

function setthreads(p::Bool)
    global dothreads = p
end

macro threads(ex)
    if ex.head === :for
        ret = quote
            global dothreads
            if dothreads
                Threads.@threads($ex)
            else
                $ex
            end
        end
        esc(ret)
    else
        throw(ArgumentError("unrecognized argument to @threads"))
    end
end

"""
Creates initial population as well as auxiliary structures for GA.
"""
function initializepop(model::GAModel, npop::Integer,
    nelites::Integer, rng::MersenneTwister, sortpop::Bool=true)
    # each thread gets its own auxiliary scratch space
    # and each thread gets its own random number generator
    nthreads = Threads.nthreads()
    rngs = accumulate(Future.randjump, fill(big(10)^20, nthreads), init=rng)
    aux = map(i -> genauxga(model), 1:nthreads)
    if npop > 0
        # initialize population
        pop1 = randcreature(model, aux[1], rngs[1])
        pop = Vector{typeof(pop1)}(undef, npop)
        pop[1] = pop1
        @threads for i = 2:npop
            threadid = Threads.threadid()
            pop[i] = randcreature(model, aux[threadid], rngs[threadid])
        end
        if sortpop
            sort!(pop, by=fitness, rev=true, alg=PartialQuickSort(max(1,nelites)))
        end
    else
        pop = nothing
    end
    return pop,aux,rngs
end

# this holds the full state of the genetic algorithm
# so that it can be stored to file
mutable struct GAState{GAM <: GAModel}
    model::GAM
    # vector of the population
    pop::Vector
    # number of generations
    ngen::Int
    # fraction of population that goes to the next generation regardless
    elite_fraction::Real
    # parameters for mutation!, crossover!, selection, logiteration, etc.
    params::Dict
    # random number generator for replication purposes
    rng::AbstractRNG
    # current generation
    curgen::Int
end
function GAState(model::GAM;
    ngen=10, npop=100, elite_fraction=0.01,
    params=Dict(),
    rng=MersenneTwister(rand(UInt))) where {GAM <: GAModel}
    npop > 1 || error("population size needs to be more than 1")
    0 <= elite_fraction <= 1 || error("elite_fraction bounds")
    nelites = Int(floor(elite_fraction*npop))

    (pop, _, _) = initializepop(model, npop, nelites, rng)

    return GAState{GAM}(model, pop, ngen, elite_fraction,
        merge(DEFAULT_GASTATE_PARAMS, params), rng, 0)
end

struct GAIterable
    state :: GAState
    children :: Vector
    aux :: Vector
    rngs :: Vector
end

# Create space to store children
# as well as auxiliary space and rngs for each thread
function GAIterable(st::GAState)
    nelites = Int(floor(st.elite_fraction * length(st.pop)))
    nchildren = length(st.pop) - nelites
    (children, aux, rngs) = initializepop(st.model, nchildren, 0, st.rng, false)
    GAIterable(st, children, aux, rngs)
end

"""
    ga function
    x in each generation, the following is done
        - select parents from all creatures in population
        - create children using crossover
        - replace non-elites in population with children
        - mutate all creatures (both elites and children) in population
"""
function Base.iterate(it::GAIterable, iteration::Int=it.state.curgen)
    st = it.state
    if stopcondition(st)
        return nothing
    end
    st.curgen += 1

    nchildren = length(it.children)
    nelites = length(st.pop) - nchildren
    # main loop:
    # 1. select parents
    # 2. crossover parents & create children
    # 4. replace non-elites in current generation with children
    # 3. mutate population
    # 5. sort population
    parents = selection(st.pop, nchildren, st, it.rngs[1])
    @threads for i = 1:nchildren
        threadid = Threads.threadid()
        p1,p2 = parents[i]
        it.children[i] = crossover!(it.children[i], st.pop[p1], st.pop[p2], st,
            it.aux[threadid], it.rngs[threadid])
    end
    # moves children and elites to current pop
    for i = 1:nchildren
        j = nelites+i
        # need to swap here because each array element is a reference
        # and crossover! and mutation! overwrites the creatures
        st.pop[j], it.children[i] = it.children[i], st.pop[j]
    end

    # mutate pop, including elites (except for the most elite creature,
    # so that monotonocity of best fitness wrt generation number is preserved)
    @threads for i = 2:length(st.pop)
        threadid = Threads.threadid()
        st.pop[i] = mutation!(st.pop[i], st, it.aux[threadid], it.rngs[threadid])
    end
    sort!(st.pop, by=fitness, rev=true, alg=PartialQuickSort(max(1,nelites)))

    st.pop[1], iteration+1
end

function ga!(st::GAState)
    println("Running genetic algorithm with population size $(length(st.pop)), generation number $(st.ngen), elite fraction $(st.elite_fraction).")
    it = GAIterable(st)
    for _ in it
        logiteration(st)
    end
    st.pop[1]
end


# Formulas for representing and working with linear-model-type expressions
# Harlan D. Harris

# we can use Julia's parser and just write them as expressions

type Formula
    lhs::Vector
    rhs::Vector
end

function _dashtree_to_list(ex::Expr)
    # if the head of the expression is a --, return a list of recursive calls to the LHS and RHS
    # otherwise, return what we got as a cell array
    if ex.args[1] == :(--)
        append(_dashtree_to_list(ex.args[2]), _dashtree_to_list(ex.args[3]))
    else
        [ ex ]
    end
end
_dashtree_to_list(ss::Symbol) = [ ss ]

function Formula(ex::Expr)
    # confirm we've got a call to ~, then recursively build a list out of -- - separated subtrees
    if ex.args[1] != :(~)
        error("Formula must have a ~!")
    else
        lhs = _dashtree_to_list(ex.args[2])
        rhs = _dashtree_to_list(ex.args[3])
        Formula(lhs, rhs)
    end
end

function show(io::IO, f::Formula)
    print(io, strcat("Formula: ", join(f.lhs, ", "), " ~ ", join(f.rhs, " +")))
end

# a ModelFrame is just a wrapper around a DataFrame
# construct with mf::ModelFrame = model_frame(f::Formula, d::DataFrame)
# this unpacks the formula, extracts the columns from the DataFrame, and
# applies the ^, &, *, / operators, and evaluates any other expressions
# in the context of the df. Note that columns remain DataVectors, and NAs are
# preserved through this step.

# minimal first version: support y ~ x1 + x2 + log(x3)
type ModelFrame
    df::AbstractDataFrame
    y_indexes::Vector{Int}
    formula::Formula
end

# Obtain Array of Symbols used in an Expr
function unique_symbols(ex::Expr)
    [[unique_symbols(a) for a in ex.args[2:end]]...]
end
unique_symbols(ex::Array{Expr,1}) = [unique_symbols(ex[1])]
unique_symbols(ex::Symbol) = [ex]
unique_symbols(ex::Array{Symbol,1}) = [ex[1]]

# Create a DataFrame containing only variables required by the Formula
# Include grouped indexes present in original DataFrame that are in Formula
# TODO: Split GroupedDataFrame away from this function?
function model_frame(f::Formula, d::AbstractDataFrame)
    cols = {}
    col_names = Array(ASCIIString,0)
    gidx = Dict{Union(UTF8String,ASCIIString),Array{Union(UTF8String,ASCIIString),1}}()

    lhs = unique_symbols(f.lhs)
    rhs = unique_symbols(f.rhs)

    # so, foreach element in the lhs, evaluate it in the context of the df
    for ll = 1:length(lhs)
      s = lhs[ll]
      if is_group(d, string(s))
        for a = get_groups(d)[string(s)]
          push!(cols, with(d, symbol(a)))
          push!(col_names, a)
        end
        gidx[string(s)] = get_groups(d)[string(s)]
      else
        push!(cols, with(d, s))
        push!(col_names, string(s))
      end
    end
    y_indexes = [1:length(lhs)]

    # and foreach unique symbol in the rhs, do the same
    for rr = 1:length(rhs)
      s = rhs[rr]
      if is_group(d, string(s))
        for a = get_groups(d)[string(s)]
          push!(cols, with(d, symbol(a)))
          push!(col_names, a)
        end
        gidx[string(s)] = get_groups(d)[string(s)]
      else
        push!(cols, with(d, s))
        push!(col_names, string(s))
      end
    end
    # bind together and return
    df = DataFrame(cols, col_names)
    if length(gidx) > 0
        set_groups(df, gidx)
    end
    ModelFrame(df, y_indexes, f)
end

model_frame(ex::Expr, d::AbstractDataFrame) = model_frame(Formula(ex), d)

# a ModelMatrix is a wrapper around a matrix, with column names.
# construct with mm::ModelMatrix = model_matrix(mf::ModelFrame, ...)
# this converts any non-numeric types to contrasts, deals with NAs, etc.

# minimal first version: allow numbers and strings only, complete cases only

_parallel_or(a,b) = [(x[1] || x[2])::Bool for x in zip(a, b)]
_parallel_and(a,b) = [(x[1] && x[2])::Bool for x in zip(a, b)]
complete_cases(df::AbstractDataFrame) = reduce(_parallel_and, colwise(x->!isna(x), df))

type ModelMatrix{T}
    model::Array{Float64}
    response::Array{Float64}
    model_colnames::Array{T}
    response_colnames::Array{T}
end

# Expand dummy variables and equations
function model_matrix(mf::ModelFrame)
    ex = mf.formula.rhs[1]
    # BUG: complete_cases doesn't preserve grouped columns
    df = mf.df#[complete_cases(mf.df),1:ncol(mf.df)]
    rdf = df[mf.y_indexes]
    mdf = expand(ex, df)

    # TODO: Convert to Array{Float64} in a cleaner way
    rnames = colnames(rdf)
    mnames = colnames(mdf)
    r = Array(Float64,nrow(rdf),ncol(rdf))
    m = Array(Float64,nrow(mdf),ncol(mdf))
    for i = 1:nrow(rdf)
      for j = 1:ncol(rdf)
        r[i,j] = float(rdf[i,j])
      end
      for j = 1:ncol(mdf)
        m[i,j] = float(mdf[i,j])
      end
    end

    include_intercept = true
    if include_intercept
      m = hcat(ones(nrow(mdf)), m)
      unshift!(mnames, "(Intercept)")
    end

    ModelMatrix(m, r, mnames, rnames)
    ## mnames = {}
    ## rnames = {}
    ## for c in 1:ncol(rdf)
    ##   r = hcat(r, float(rdf[c]))
    ##   push!(rnames, colnames(rdf)[c])
    ## end
    ## for c in 1:ncol(mdf)
    ##   m = hcat(m, mdf[c])
    ##   push!(mnames, colnames(mdf)[c])
    ## end
end

model_matrix(f::Formula, d::AbstractDataFrame) = model_matrix(model_frame(f, d))
model_matrix(ex::Expr, d::AbstractDataFrame) = model_matrix(model_frame(Formula(ex), d))

# TODO: Make a more general version of these functions
# TODO: Be able to extract information about each column name
function interaction_design_matrix(a::AbstractDataFrame, b::AbstractDataFrame)
   cols = {}
   col_names = Array(ASCIIString,0)
   for i in 1:ncol(a)
       for j in 1:ncol(b)
          push!(cols, DataArray(a[:,i] .* b[:,j]))
          push!(col_names, strcat(colnames(a)[i],"&",colnames(b)[j]))
       end
   end
   DataFrame(cols, col_names)
end

function interaction_design_matrix(a::AbstractDataFrame, b::AbstractDataFrame, c::AbstractDataFrame)
   cols = {}
   col_names = Array(ASCIIString,0)
   for i in 1:ncol(a)
       for j in 1:ncol(b)
           for k in 1:ncol(b)
              push!(cols, DataArray(a[:,i] .* b[:,j] .* c[:,k]))
              push!(col_names, strcat(colnames(a)[i],"&",colnames(b)[j],"&",colnames(c)[k]))
           end
       end
   end
   DataFrame(cols, col_names)
end

# Temporary: Manually describe the interactions needed for DataFrame Array.
function all_interactions(dfs::Array{Any,1})
    d = DataFrame()
    if length(dfs) == 2
      combos = ([1,2],)
    elseif length(dfs) == 3
      combos = ([1,2], [1,3], [2,3], [1,2,3])
    else
      error("interactions with more than 3 terms not implemented (yet)")
    end
    for combo in combos
       if length(combo) == 2
         a = interaction_design_matrix(dfs[combo[1]],dfs[combo[2]])
       elseif length(combo) == 3
         a = interaction_design_matrix(dfs[combo[1]],dfs[combo[2]],dfs[combo[3]])
       end
       d = insert!(d, a)
    end
    return d
end

# string(Expr) now quotes, which we don't want. This hacks around that, stealing
# from print_to_string
function formula_string(ex::Expr)
    s = memio(0, false)
    Base.show_unquoted(s, ex)
    takebuf_string(s)
end

#
# The main expression to DataFrame expansion function.
# Returns a DataFrame.
#

function expand(ex::Expr, df::AbstractDataFrame)
    f = eval(ex.args[1])
    if method_exists(f, (FormulaExpander, Vector{Any}, DataFrame))
        # These are specialized expander functions (+, *, &, etc.)
        f(FormulaExpander(), ex.args[2:end], df)
    else
        # Everything else is called recursively:
        expand(with(df, ex), formula_string(ex), df)
    end
end

function expand(s::Symbol, df::AbstractDataFrame)
    expand(with(df, s), string(s), df)
end

# TODO: make this array{symbol}?
function expand(args::Array{Any}, df::AbstractDataFrame)
    [expand(x, df) for x in args]
end

function expand(x, name::ByteString, df::AbstractDataFrame)
    # If this happens to be a column group, then expand each and concatenate
    if is_group(df, name)
      preds = get_groups(df)[name]
      dfs = [expand(symbol(x), df) for x in preds]
      return cbind(dfs...)
    end
    # This is the default for expansion: put it right in to a DataFrame.
    DataFrame({x}, [name])
end

#
# Methods for expansion of specific data types
#

# Expand a PooledDataVector into a matrix of indicators for each dummy variable
# TODO: account for NAs?
function expand(poolcol::PooledDataVector, colname::ByteString, df::AbstractDataFrame)
    newcol = {DataArray([convert(Float64,x)::Float64 for x in (poolcol.refs .== i)]) for i in 2:length(poolcol.pool)}
    newcolname = [strcat(colname, ":", x) for x in poolcol.pool[2:length(poolcol.pool)]]
    DataFrame(newcol, convert(Vector{ByteString}, newcolname))
end

#
# Methods for Formula expansion
#
type FormulaExpander; end # This is an indictor type.

function +(::FormulaExpander, args::Vector{Any}, df::AbstractDataFrame)
    d = DataFrame()
    for a in args
        d = insert!(d, expand(a, df))
    end
    d
end
function (&)(::FormulaExpander, args::Vector{Any}, df::AbstractDataFrame)
    interaction_design_matrix(expand(args[1], df), expand(args[2], df))
end
function *(::FormulaExpander, args::Vector{Any}, df::AbstractDataFrame)
    d = +(FormulaExpander(), args, df)
    d = insert!(d, all_interactions(expand(args, df)))
    d
end


"""
    head_to_obj(filename::String)

Returns the header of a snapshot as a `Header` object.
"""
function head_to_obj(filename)

    h = Header()

    f = open(filename)
    blocksize = read(f, Int32)

    if blocksize == 8
        swap = false
        snap_format = 2
    elseif blocksize == 256
        swap = false
        snap_format = 1
    else
        blocksize = bswap(blocksize)
        if blocksize == 8
            swap = true
            snap_format = 2
        elseif blocksize == 256
            swap = true
            snap_format = 1
        else
            error("incorrect file format encountered when reading header of $filename")
        end
    end

    #println("Reading snapshot format: ", snap_format)

    if snap_format == 2
        seek(f, 16)
        skip_line = read(f, Int32)
    end

    h.npart = read!(f, Array{Int32,1}(undef,6))
    h.massarr = read!(f, Array{Float64,1}(undef,6))
    h.time = read(f, Float64)
    h.z = read(f, Float64)
    h.flag_sfr = read(f, Int32)
    h.flag_feedback = read(f, Int32)
    h.nall = read!(f, Array{UInt32,1}(undef,6))
    h.flag_cooling = read(f, Int32)
    h.num_files = read(f, Int32)
    h.boxsize = read(f, Float64)
    h.omega_0 = read(f, Float64)
    h.omega_l = read(f, Float64)
    h.h0 = read(f, Float64)
    h.flag_stellarage = read(f, Int32)
    h.flag_metals = read(f, Int32)
    h.npartTotalHighWord = read!(f, Array{UInt32,1}(undef,6))
    h.flag_entropy_instead_u = read(f, Int32)
    h.flag_doubleprecision = read(f, Int32)
    h.flag_ic_info = read(f, Int32)
    h.lpt_scalingfactor = read(f, Float32)

    close(f)

    return h
end

"""
    head_to_dict(filename::String)

Returns the header of a snapshot as a dictionary.
"""
function head_to_dict(filename::String)

        header = Dict()

        f = open(filename)
        blocksize = read(f, Int32)

        if blocksize[1] == 8
            swap = 0
            snap_format = 2
        elseif blocksize[1] == 256
            swap = 0
            snap_format = 1
        else
            blocksize[1] = bswap(blocksize[1])
            if blocksize[1] == 8
                swap = 1
                snap_format = 2
            elseif blocksize[1] == 256
                swap = 1
                snap_format = 1
            else
                println("incorrect file format encountered when reading header of", filename)
            end
        end

        if snap_format == 2
            seek(f, 16)
            skip_line = read(f, Int32)
        end

        header["snap_format"] = snap_format
        header["PartTypes"] = ["PartType0", "PartType1", "PartType2",
                               "PartType3", "PartType4", "PartType5"]
        header["npart"] = read!(f, Array{Int32,1}(undef,6))
        header["massarr"] = read!(f, Array{Float64,1}(undef,6))
        header["time"] = read(f, Float64)
        header["redshift"] = read(f, Float64)
        header["flag_sfr"] = read(f, Int32)
        header["flag_feedback"] = read(f, Int32)
        header["nall"] = read!(f, Array{UInt32,1}(undef,6))
        header["flag_cooling"] = read(f, Int32)
        header["num_files"] = read(f, Int32)
        header["boxsize"] = read(f, Float64)
        header["omega_m"] = read(f, Float64)
        header["omega_l"] = read(f, Float64)
        header["hubble"] = read(f, Float64)
        header["flag_stellarage"] = read(f, Int32)
        header["flag_metals"] = read(f, Int32)
        header["npartTotalHighWord"] = read!(f, Array{UInt32,1}(undef,6))
        header["flag_entropy_instead_u"] = read(f, Int32)
        header["flag_doubleprecision"] = read(f, Int32)
        header["flag_ic_info"] = read(f, Int32)
        header["lpt_scalingfactor"] = read(f, Float32)

        close(f)

        return header

end

"""
    read_header(filename::String)

Reads the header of a snapshot and returns a Header object.

See also: [`head_to_obj`](@ref)
"""
function read_header(filename::String)
    return head_to_obj(filename)
end


using AstroImages, FITSIO, Images, Random, Widgets, WCS, JLD

using Test, WCS
using SHA: sha256

import AstroImages: _float, render, _brightness_contrast, brightness_contrast

@testset "Conversion to float and fixed-point" begin
    @testset "Float" begin
        for T in (Float16, Float32, Float64)
            @test _float(T(-9.8)) === T(-9.8)
            @test _float(T(12.3)) === T(12.3)
        end
    end
    @testset "Integers" begin
        for (UIT, SIT) in ((UInt8,  Int8),
                           (UInt16, Int16),
                           (UInt32, Int32),
                           (UInt64, Int64))
            N = sizeof(UIT) * 8
            NT = Normed{UIT, N}
            maxint = UIT(big(2) ^ (N - 1))
            @test _float(typemin(UIT)) === _float(typemin(SIT)) === NT(0)
            @test _float(UIT(85)) === reinterpret(NT, UIT(85))
            @test _float(SIT(-85)) === _float(UIT(-85 + big(maxint)))
            @test _float(SIT(115)) === _float(UIT(115) + maxint)
            @test _float(typemax(UIT)) === _float(typemax(SIT)) === NT(1)
        end
    end
end

@testset "FITS and images" begin
    fname = tempname() * ".fits"
    for T in [UInt8, Int8, UInt16, Int16, UInt32, Int32, Int64,
              Float32, Float64]
        data = reshape(T[1:100;], 5, 20)
        FITS(fname, "w") do f
            write(f, data)
        end
        @test load(fname, 1)[1] == data
        @test load(fname, (1, 1))[1] == (data, data)
        img = AstroImage(fname)
        rendered_img = colorview(img)
        @test iszero(minimum(rendered_img))
    end
    rm(fname, force = true)
end

@testset "Control contrast" begin
    @test @inferred(_brightness_contrast(Gray, ones(Float32, 2, 2), 100, 100)) isa
          Array{Gray{Float32},2}
    Random.seed!(1)
    M = rand(2, 2)
    @test @inferred(_brightness_contrast(Gray, M, 100, 100)) ≈
        Gray.([0.48471895908315565  0.514787046406301;
               0.5280458879184159   0.39525854250610226]) rtol = 1e-12
    @test @inferred(_brightness_contrast(Gray, M, 0,   255)) == Gray.(M)
    @test @inferred(_brightness_contrast(Gray, M, 255,   0)) == Gray.(ones(size(M)))
    @test @inferred(_brightness_contrast(Gray, M,   0,   0)) == Gray.(zeros(size(M)))
    @test brightness_contrast(AstroImage(M)) isa Widgets.Widget{:manipulate,Any}
end

@testset "default handler" begin
    fname = tempname() * ".fits"
    @testset "less dimensions than 2" begin
        data = rand(2)
        FITS(fname, "w") do f
            write(f, data)
        end
        @test_throws ErrorException AstroImage(fname)
    end

    @testset "no ImageHDU" begin
        ## Binary table
        indata = Dict{String, Array}()
        i = length(indata) + 1
        indata["col$i"] = [randstring(10) for j=1:20]  # ASCIIString column
        i += 1
        indata["col$i"] = ones(Bool, 20)  # Bool column
        i += 1
        indata["col$i"] = reshape([1:40;], (2, 20))  # vector Int64 column
        i += 1
        indata["col$i"] = [randstring(5) for j=1:2, k=1:20]  # vector ASCIIString col
        indata["vcol"] = [randstring(j) for j=1:20]  # variable length column
        indata["VCOL"] = [collect(1.:j) for j=1.:20.] # variable length

        FITS(fname, "w") do f
            write(f, indata; varcols=["vcol", "VCOL"])
            @test_throws MethodError AstroImage(f)
        end
    end

    @testset "Opening AstroImage in different ways" begin
        data = rand(2,2)
        wcs = WCSTransform(2;)
        FITS(fname, "w") do f
            write(f, data)
        end
        f = FITS(fname)
        @test AstroImage(fname, 1) isa AstroImage
        @test AstroImage(Gray ,fname, 1) isa AstroImage
        @test AstroImage(Gray, f, 1) isa AstroImage
        @test AstroImage(data, wcs) isa AstroImage
        @test AstroImage((data,data), (wcs,wcs)) isa AstroImage
        @test AstroImage(Gray, data, wcs) isa AstroImage
        close(f)
    end

    @testset "Image HDU is not at 1st position" begin
        ## Binary table
        indata = Dict{String, Array}()
        i = length(indata) + 1
        indata["col$i"] = [randstring(10) for j=1:20]  # ASCIIString column
        i += 1
        indata["col$i"] = ones(Bool, 20)  # Bool column
        i += 1
        indata["col$i"] = reshape([1:40;], (2, 20))  # vector Int64 column
        i += 1
        indata["col$i"] = [randstring(5) for j=1:2, k=1:20]  # vector ASCIIString col
        indata["vcol"] = [randstring(j) for j=1:20]  # variable length column
        indata["VCOL"] = [collect(1.:j) for j=1.:20.] # variable length

        FITS(fname, "w") do f
            write(f, indata; varcols=["vcol", "VCOL"])
            write(f, rand(2, 2))
        end

        @test @test_logs (:info, "Image was loaded from HDU 3") AstroImage(fname) isa AstroImage
    end
    rm(fname, force = true)
end

@testset "Utility functions" begin
   @test size(AstroImage((rand(10,10), rand(10,10)))) == ((10,10), (10,10))
   @test length(AstroImage((rand(10,10), rand(10,10)))) == 2
end

@testset "multi image AstroImage" begin
    data1 = rand(10,10)
    data2 = rand(10,10)
    fname = tempname() * ".fits"
    FITS(fname, "w") do f
        write(f, data1)
        write(f, data2)
    end

    img = AstroImage(fname, (1,2))
    @test length(img.data) == 2
    @test img.data[1] == data1
    @test img.data[2] == data2

    f = FITS(fname)
    img = AstroImage(Gray, f, (1,2))
    @test length(img.data) == 2
    @test img.data[1] == data1
    @test img.data[2] == data2
    close(f)
end

@testset "multi wcs AstroImage" begin
    fname = tempname() * ".fits"
    f = FITS(fname, "w")
    inhdr = FITSHeader(["CTYPE1", "CTYPE2", "RADESYS", "FLTKEY", "INTKEY", "BOOLKEY", "STRKEY", "COMMENT",
                        "HISTORY"],
                       ["RA---TAN", "DEC--TAN", "UNK", 1.0, 1, true, "string value", nothing, nothing],
                       ["",
                        "",
                        "",
                        "floating point keyword",
                        "",
                        "boolean keyword",
                        "string value",
                        "this is a comment",
                        "this is a history"])

    indata = reshape(Float32[1:100;], 5, 20)
    write(f, indata; header=inhdr)
    write(f, indata; header=inhdr)
    close(f)

    img = AstroImage(fname, (1,2))
    f = FITS(fname)
    @test length(img.wcs) == 2
    @test WCS.to_header(img.wcs[1]) === WCS.to_header(WCS.from_header(read_header(f[1], String))[1])
    @test WCS.to_header(img.wcs[2]) === WCS.to_header(WCS.from_header(read_header(f[2], String))[1])

    img = AstroImage(Gray, f, (1,2))
    @test length(img.wcs) == 2
    @test WCS.to_header(img.wcs[1]) === WCS.to_header(WCS.from_header(read_header(f[1], String))[1])
    @test WCS.to_header(img.wcs[2]) === WCS.to_header(WCS.from_header(read_header(f[2], String))[1])
    close(f)
end

@testset "multi file AstroImage" begin
    fname1 = tempname() * ".fits"
    f = FITS(fname1, "w")
    inhdr = FITSHeader(["CTYPE1", "CTYPE2", "RADESYS", "FLTKEY", "INTKEY", "BOOLKEY", "STRKEY", "COMMENT",
                        "HISTORY"],
                    ["RA---TAN", "DEC--TAN", "UNK", 1.0, 1, true, "string value", nothing, nothing],
                    ["",
                        "",
                        "",
                        "floating point keyword",
                        "",
                        "boolean keyword",
                        "string value",
                        "this is a comment",
                        "this is a history"])

    indata1 = reshape(Int[1:100;], 5, 20)
    write(f, indata1; header=inhdr)
    close(f)

    fname2 = tempname() * ".fits"
    f = FITS(fname2, "w")
    indata2 = reshape(Int[1:100;], 5, 20)
    write(f, indata2; header=inhdr)
    close(f)

    fname3 = tempname() * ".fits"
    f = FITS(fname3, "w")
    indata3 = reshape(Int[1:100;], 5, 20)
    write(f, indata3; header=inhdr)
    close(f)

    img = AstroImage((fname1, fname2, fname3))
    f1 = FITS(fname1)
    f2 = FITS(fname2)
    f3 = FITS(fname3)

    @test length(img.data) == length(img.wcs) == 3
    @test img.data[1] == indata1
    @test img.data[2] == indata2
    @test img.data[3] == indata3
    @test WCS.to_header(img.wcs[1]) == WCS.to_header(img.wcs[2]) ==
        WCS.to_header(img.wcs[3]) == WCS.to_header(WCS.from_header(read_header(f1[1], String))[1])
    @test eltype(eltype(img.data)) == Int

    img = AstroImage(Gray, (f1, f2, f3), (1,1,1))
    @test length(img.data) == length(img.wcs) == 3
    @test img.data[1] == indata1
    @test img.data[2] == indata2
    @test img.data[3] == indata3
    @test WCS.to_header(img.wcs[1]) == WCS.to_header(img.wcs[2]) ==
        WCS.to_header(img.wcs[3]) == WCS.to_header(WCS.from_header(read_header(f1[1], String))[1])
    @test eltype(eltype(img.data)) == Int
    close(f1)
    close(f2)
    close(f3)
    rm(fname1, force = true)
    rm(fname2, force = true)
    rm(fname3, force = true)
end

include("plots.jl")
include("ccd2rgb.jl")


using POMDPs

POMDPs.add("MCTS")


using Test
include("ExtendableSparseTest.jl")

ExtendableSparseTest.benchmark()

@time begin

    @test ExtendableSparseTest.check(m=10,n=10,nnz=5)
    @test ExtendableSparseTest.check(m=100,n=100,nnz=500,nsplice=2)
    @test ExtendableSparseTest.check(m=1000,n=1000,nnz=5000,nsplice=3)

    @test ExtendableSparseTest.check(m=20,n=10,nnz=5)
    @test ExtendableSparseTest.check(m=200,n=100,nnz=500,nsplice=2)
    @test ExtendableSparseTest.check(m=2000,n=1000,nnz=5000,nsplice=3)

    @test ExtendableSparseTest.check(m=10,n=20,nnz=5)
    @test ExtendableSparseTest.check(m=100,n=200,nnz=500,nsplice=2)
    @test ExtendableSparseTest.check(m=1000,n=2000,nnz=5000,nsplice=3)

    for irun=1:10
        m=rand((1:10000))
        n=rand((1:10000))
        nnz=rand((1:10000))
        nsplice=rand((1:5))
        @test ExtendableSparseTest.check(m=m,n=n,nnz=nnz,nsplice=nsplice)
    end

end


# getindex/view/setindex! ======================================================

#### getindex/view ####

# Integer returns a single value, but not for view
@propagate_inbounds Base.getindex(A::AbstractDimArray, i1::Integer, i2::Integer, I::Integer...) =
    Base.getindex(parent(A), i1, i2, I...)
# No indices. These just prevent stack overflows
@propagate_inbounds Base.getindex(A::AbstractDimArray) = Base.getindex(parent(A))
@propagate_inbounds Base.view(A::AbstractDimArray) = rebuild(A, Base.view(parent(A)), ())

const SelectorOrStandard = Union{SelectorOrInterval,StandardIndices}

for f in (:getindex, :view, :dotview)
    @eval begin
        #### Array getindex/view ###
        @propagate_inbounds Base.$f(A::AbstractDimArray, i::Integer) = Base.$f(parent(A), i)
        @propagate_inbounds Base.$f(A::AbstractDimArray, i::CartesianIndex) = Base.$f(parent(A), i)
        # Linear indexing forwards to the parent array
        @propagate_inbounds Base.$f(A::AbstractDimArray, i::Union{Colon,AbstractVector{<:Integer}}) =
            Base.$f(parent(A), i)
        @propagate_inbounds Base.$f(A::AbstractDimArray, i::AbstractArray{<:Bool}) =
            Base.$f(parent(A), i)
        # Except 1D DimArrays
        @propagate_inbounds Base.$f(A::AbstractDimArray{<:Any,1}, i::Union{Colon,AbstractVector{<:Integer}}) =
            rebuildsliced(Base.$f, A, Base.$f(parent(A), i), (i,))
        # Selector/Interval indexing
        @propagate_inbounds Base.$f(A::AbstractDimArray, i1::SelectorOrStandard, I::SelectorOrStandard...) =
            Base.$f(A, dims2indices(A, (i1, I...))...)
        @propagate_inbounds Base.$f(A::AbstractDimArray, extent::Extents.Extent) =
            Base.$f(A, dims2indices(A, extent)...)
        # Dimension indexing. Allows indexing with A[somedim=At(25.0)] for Dim{:somedim}
        @propagate_inbounds Base.$f(A::AbstractDimArray, args::Dimension...; kw...) =
            Base.$f(A, dims2indices(A, (args..., kwdims(values(kw))...))...)
        # Everything else - such as custom indexing types from other packages
        # @propagate_inbounds Base.$f(A::AbstractDimArray, i1, I...) = Base.$f(parent(A), i1, I...)
    end
    # Standard indices
    if f == :view
        @eval @propagate_inbounds function Base.$f(A::AbstractDimArray, i1::StandardIndices, i2::StandardIndices, I::StandardIndices...)
            I = _unwrap_cartesian(i1, i2, I...)
            x = Base.$f(parent(A), I...)
            rebuildsliced(Base.$f, A, x, I)
        end
    else
        @eval @propagate_inbounds function Base.$f(A::AbstractDimArray, i1::StandardIndices, i2::StandardIndices, I::StandardIndices...)
            I = _unwrap_cartesian(i1, i2, I...)
            x = Base.$f(parent(A), I...)
            all(i -> i isa Integer, I) ? x : rebuildsliced(Base.$f, A, x, I)
        end
    end
end

@inline _unwrap_cartesian(i1::CartesianIndex, I...) = (Tuple(i1)..., _unwrap_cartesian(I...)...)
@inline _unwrap_cartesian(i1, I...) = (i1, _unwrap_cartesian(I...)...)
@inline _unwrap_cartesian() = ()

#### setindex ####

@propagate_inbounds Base.setindex!(A::AbstractDimArray, x) = setindex!(parent(A), x)
@propagate_inbounds Base.setindex!(A::AbstractDimArray, x, args::Dimension...; kw...) =
    setindex!(A, x, dims2indices(A, (args..., kwdims(values(kw))...))...)
@propagate_inbounds Base.setindex!(A::AbstractDimArray, x, i, I...) =
    setindex!(A, x, dims2indices(A, (i, I...))...)
@propagate_inbounds Base.setindex!(A::AbstractDimArray, x, i1::StandardIndices, I::StandardIndices...) =
    setindex!(parent(A), x, i1, I...)


using DelimitedFiles

# Run the CMD from the command function which will create Temp.wav file and extract the raw string information as a Tuple of vectors.

function feature(command)

    print("""Starting CMD "$command"\n""")
    # run command on CMD
    run(`cmd /k $command "&" exit`)

    # remove Temp.wav file
    rm(raw".\extraFiles\Temp.wav", force=true)

    # temporarily solution: use readdlm, remove commas, convert the strings to floats

    temp = DelimitedFiles.readdlm(raw".\extraFiles\Temp.features") # this contains strings, commas, and numbers

    for i in eachindex(temp)
           if(typeof(temp[i]) == SubString{String})
               temp[i] = parse(Float64,replace(temp[i], ","=>""))
           end
    end

    track = (t=temp[:,1],voicing=temp[:,2],f0=temp[:,3],f1=temp[:,4],f2=temp[:,5],f3=temp[:,6],f4=temp[:,7],f5=temp[:,8],f6=temp[:,9],f7=temp[:,10],f8=temp[:,11],f9=temp[:,12],f10=temp[:,13],f11=temp[:,14],f12=temp[:,15],f13=temp[:,16],f14=temp[:,17],f15=temp[:,18],Formants=temp[:,4:end])

end


# Tests for distances
function t_dist()

N = 10	# variables
M = 25  # samples

vectordata = rand(M)
matrixdata = rand(N,M)
labels = round.(2*rand(M))
multilabels = round.(2*rand(2,M))

A01 = j4pr.datacell(vectordata)
A02 = j4pr.datacell(vectordata, labels)
A03 = j4pr.datacell(vectordata, multilabels)
A04 = j4pr.datacell(matrixdata)
A05 = j4pr.datacell(matrixdata, labels)
A06 = j4pr.datacell(matrixdata, multilabels)

# Tests
D = [Distances.Euclidean(),
     Distances.Cityblock(),
     Distances.Chebyshev(),
     Distances.Jaccard()] # Add more just to be sure ...
for tr in [A01, A02, A03, vectordata]
	for ts in [A01, A02, A03, vectordata]
		for d in D
			Test.@test try
				ts |> (tr |> j4pr.dist(d));
				true
			catch
				false
			end
		end
	end
end

for tr in [A04, A05, A06, matrixdata]
	for ts in [A04, A05, A06, matrixdata]
		for d in D
			Test.@test try
				ts |> (tr |> j4pr.dist(d));
				true
			catch
				false
			end
		end
	end
end

# Test training with tuple and the generic dist(Array,Array)
Test.@test try
	w = j4pr.dist(j4pr.strip(A03),D[1])
	true
catch
	false
end

end


include("LineOfResponse.jl")

# Transform event to main plane
function transform_event(lor::LOR, plane::Plane)
    # Transform event so we can use the same code for different main planes.
    if plane == x_plane
        x1 = lor.P1.x
        x2 = lor.P2.x
        y1 = lor.P1.y
        y2 = lor.P2.y
        z1 = lor.P1.z
        z2 = lor.P2.z
    elseif plane == y_plane
        x1 = lor.P1.y
        x2 = lor.P2.y
        y1 = lor.P1.x
        y2 = lor.P2.x
        z1 = lor.P1.z
        z2 = lor.P2.z
    else
        x1 = lor.P1.z
        x2 = lor.P2.z
        y1 = lor.P1.y
        y2 = lor.P2.y
        z1 = lor.P1.x
        z2 = lor.P2.x
    end

    event_ret = Event(LOR(Point(x1, y1, z1), Point(x2, y2, z2), lor.timeDiff), plane)

    return event_ret
end

function read_3D(filename)
	fs = open("Data/" * filename,"r")
	array_length = Int(stat("Data/" * filename).size / (4*7))
	lor_type = Ref{LOR}()

	events = Array{Event}(undef, array_length)

	for i = 1:array_length
		lor = getindex(read!(fs, lor_type))

		if abs(lor.P2.x-lor.P1.x) > abs(lor.P2.y-lor.P1.y) && abs(lor.P2.x-lor.P1.x) > abs(lor.P2.z-lor.P1.z) # x-plane
			e = transform_event(lor, x_plane)
			events[i] = e
		elseif abs(lor.P2.y-lor.P1.y) > abs(lor.P2.x-lor.P1.x) && abs(lor.P2.y-lor.P1.y) > abs(lor.P2.z-lor.P1.z) # y-plane
			e = transform_event(lor, y_plane)
			events[i] = e
		elseif abs(lor.P2.z-lor.P1.z) > abs(lor.P2.x-lor.P1.x) && abs(lor.P2.z-lor.P1.z) > abs(lor.P2.y-lor.P1.y) # z-plane
			e = transform_event(lor, z_plane)
			events[i] = e
		else
			events[i] = Event(lor, undef_plane)
		end
	end

	println("Total number of events: ", array_length)

	return events, array_length
end


function lambda_arg_types(f::Function)
    [var for var in code_lowered(f)[1].specTypes.types[2:end]]
end

# Simple properties
function property(prop::Function, typs::Vector, ntests)
    arggens = [size -> generator(typ, size) for typ in typs]
    quantproperty(prop, typs, ntests, arggens...)
end
property(prop::Function, typs::Vector) = property(prop, typs, 100)
property(prop::Function, ntests) = property(prop, lambda_arg_types(prop), ntests)
property(prop::Function) = property(prop, 100)

# Conditional properties
function condproperty(prop::Function, typs::Vector, ntests, maxtests, argconds...)
    arggens = [size -> generator(typ, size) for typ in typs]
    check_property(prop, arggens, argconds, ntests, maxtests)
end
condproperty(prop::Function, args...) = condproperty(prop, lambda_arg_types(prop), args...)

# Quantified properties (custom generators)
function quantproperty(prop::Function, typs::Vector, ntests, arggens...)
    argconds = [(_...)->true for t in typs]
    check_property(prop, arggens, argconds, ntests, ntests)
end
quantproperty(prop::Function, args...) = quantproperty(prop, lambda_arg_types(prop), args...)

function check_property(prop::Function, arggens, argconds, ntests, maxtests)
    totalTests = 0
	counterexamples = false
    for i in 1:ntests
        goodargs = false
        args = []
        while !goodargs
            totalTests += 1
            if totalTests > maxtests
                println("Arguments exhausted after $i tests.")
                return
            end
            args = [arggen(div(i,2)+3) for arggen in arggens]
            goodargs = reduce(&,[cond(args...) for cond in argconds])
        end
        if !prop(args...)
		println("Falsifiable, after $i tests:\n$args")
		counterexamples = true
		return false
            #error("Falsifiable, after $i tests:\n$args")
        end
    end
	if !counterexamples
	    println("OK, passed $ntests tests.")
	return true
	end
end

function check_existence(prop::Function, arggens, argconds, ntests, maxtests)
    totalTests = 0
	findings = false
    for i in 1:ntests
        goodargs = false
        args = []
        while !goodargs
            totalTests += 1
            if totalTests > maxtests
                println("Arguments exhausted after $i tests.")
                return
            end
            args = [arggen(div(i,2)+3) for arggen in arggens]
            goodargs = reduce(&,[cond(args...) for cond in argconds])
        end
        if prop(args...)
            println("Found necessary value after $i tests:\n$args")
            findings = true
	     return true #break
        end
    end
     if !findings
		println("Could not prove existence after $ntests tests.")
		return false
	end
end

# Default generators for primitive types
generator{T<:Unsigned}(::Type{T}, size) = convert(T, rand(1:size))
generator{T<:Signed}(::Type{T}, size) = convert(T, rand(-size:size))
# Fima: replaced .* with * in the next line (was it typo?)
generator{T<:AbstractFloat}(::Type{T}, size) = convert(T, (rand()-0.5)*size)
# This won't generate interesting UTF-8, but doing that is a Hard Problem
generator{T<:AbstractString}(::Type{T}, size) = convert(T, randstring(size))

generator(::Type{Any}, size) = error("Property variables cannot by typed Any.")

# Generator for array types
function generator{T,n}(::Type{Array{T,n}}, size)
    dims = [rand(1:size) for i in 1:n]
    reshape([generator(T, size) for x in 1:prod(dims)], dims...)
end

# Generator for composite types
function generator{C}(::Type{C}, size)
    if C.types == ()
        error("No generator defined for type $C.")
    end
    C([generator(T, size) for T in C.types]...)
end

macro not(b)
	return :(!$b)
end


# DESCRIPTION
# Control input time history.
#
# ------------------------------------------------------------------------------

function finp(t)
    # amplitude of perturbation from trim controls
    amp=5
    # controls history
    if t < 1
        u = [0.0; 0.0; 0.0; 0.0]
    elseif t < 3
        u = [amp; 0.0; 0.0; 0.0]
    elseif t < 5
        u = [-amp; 0.0; 0.0; 0.0]
    else
        u = [0.0; 0.0; 0.0; 0.0]
    end
end


# instantiate project
#import Pkg
#Pkg.activate(@__DIR__)
#Pkg.instantiate()

const TARGET_FOLDER = "results"
const RESULTS_FILE = "results.csv"

function main()
    if !isdir(TARGET_FOLDER)
        mkdir(TARGET_FOLDER)
    end
    global io = open(joinpath(TARGET_FOLDER, RESULTS_FILE), "w")

    println("Running evaluation...")

    println(">>> Running Logistic model")
    include("evaluation/Logistic/Logistic.jl")

    println(">>> Running SEIR model")
    include("evaluation/SEIR/SEIR.jl")

    println(">>> Running Burgers model")
    include("evaluation/Burgers/Burgers.jl")

    println("Finished running evaluation. Results stored in $TARGET_FOLDER")
    close(io)
    nothing
end

main()


Base.@kwdef mutable struct GridStruct
    nz = nothing
    zP = nothing
    z = nothing
    dz = nothing
    NumFaces = nothing
    Faces = nothing
    NumEdges = nothing
    Edges = nothing
    NumNodes = nothing
    Nodes = nothing
    Form = nothing
    Type = nothing
    nBar = nothing
    Dim = nothing
    Rad = nothing
    NumEdgesI = nothing
    NumEdgesB = nothing
    vtkP = nothing
    ConnectivityList = nothing
    nBar3 = nothing
    KiteGrid = nothing
end


module RoughSurface

# Write your package code here.
using DelimitedFiles, JLD2, FileIO
using PyCall

export write_surf, read_surf, show_surf

```@meta
CurrentModule = RoughSurface
```

include(joinpath(@__DIR__, "RoughSurface_GUI.jl"))
include(joinpath(@__DIR__, "RoughSurface_Controller.jl"))

"""
    write_surf()

将表面数据写入文件, 默认txt文件的数据用制表符分割, csv文件的数据用``,''分割,
jld2则为直接保存二进制文件
"""
function write_surf(file_path, surf)
    # 利用splitext来得到文件类型
    file_ext = splitext(file_path)[2]
    if file_ext == ".txt"
        open(file_path, "w") do io
            writedlm(io, surf)
        end # do
    elseif file_ext == ".csv"
        open(file_path, "w") do io
            writedlm(io, surf, ',')
        end # do
    elseif file_ext == ".jld2"
        save(file_path, "surf", surf)
    else
        @warn("未知的文件类型, 保存为jld2格式")
        save(file_path*".jld2", "surf", surf)
    end

end # function

"""
    read_surf()

从文件读取表面数据, 与写入数据保持一致, 默认txt文件的数据用制表符分割, csv文件的数据用``,''分割,
jld2则为直接保存二进制文件.
"""
function read_surf(file_path)
    file_ext = splitext(file_path)[2]
    if file_ext == ".txt"
        surf = readdlm(file_path)
    elseif file_ext == ".csv"
        surf = readdlm(file_path, ',')
    elseif file_ext == ".jld2"
        surf = load(file_path,"surf")
    else
        @warn("未知的文件类型, 尝试jld2格式")
        surf = load(file_path*".jld2","surf")
    end
    return surf
end # function

"""
    show_surf()

利用python的pyvista包显示规则表面数据.
"""
show_surf(surf_mat) = py"show_surf"(surf_mat)

end  # module


using Flux: @treelike
import Flux
import Base

include("./vocab.jl")
include("./onehot.jl")
include("./gather.jl")
include("./etype.jl")
include("./embed.jl")
include("./position_embed.jl")


import Dates
using Dates: Second, Millisecond

warsaw = first(compile("Europe/Warsaw", tzdata["europe"]))
fixed = FixedTimeZone("Fixed", -7200, 3600)

# ZonedDateTime accessors
zdt = ZonedDateTime(DateTime(2014,6,12,23,59,58,57), fixed)
@test timezone(zdt) === fixed

@test TimeZones.days(zdt) == 735396
@test TimeZones.hour(zdt) == 23
@test TimeZones.minute(zdt) == 59
@test TimeZones.second(zdt) == 58
@test TimeZones.millisecond(zdt) == 57

@test eps(zdt) == Millisecond(1)

# Make sure that Dates accessors work with ZonedDateTime.
@test Dates.year(zdt) == 2014
@test Dates.month(zdt) == 6
@test Dates.week(zdt) == 24
@test Dates.day(zdt) == 12
@test Dates.dayofmonth(zdt) == 12
@test Dates.yearmonth(zdt) == (2014, 6)
@test Dates.monthday(zdt) == (6, 12)
@test Dates.yearmonthday(zdt) == (2014, 6, 12)

# Vectorized accessors
# Note: fill is used to test for size and equality.
n = 10
arr = fill(zdt, n)
@test TimeZones.hour.(arr) == fill(23, n)
@test TimeZones.minute.(arr) == fill(59, n)
@test TimeZones.second.(arr) == fill(58, n)
@test TimeZones.millisecond.(arr) == fill(57, n)

@test Dates.year.(arr) == fill(2014, n)
@test Dates.month.(arr) == fill(6, n)
@test Dates.day.(arr) == fill(12, n)
@test Dates.dayofmonth.(arr) == fill(12, n)
@test Dates.yearmonth.(arr) == fill((2014, 6), n)
@test Dates.monthday.(arr) == fill((6, 12), n)
@test Dates.yearmonthday.(arr) == fill((2014, 6, 12), n)


function equidistant_time_stepper(psi::WaveFunction, t0::Real, tend::Real,
                         dt::Real, es::EmbeddedScheme,
                         operator_sequence="AB")
    equidistant_time_stepper(psi, t0, tend, dt, es.scheme2,
                         operator_sequence)
end
function equidistant_time_stepper(psi::WaveFunction, t0::Real, tend::Real,
                         dt::Real, es::DefectBasedScheme,
                         operator_sequence="AB")
    equidistant_time_stepper(psi, t0, tend, dt, es.scheme,
                         operator_sequence)
end
function equidistant_time_stepper(psi::WaveFunction, t0::Real, tend::Real,
                         dt::Real, ps::PalindromicScheme,
                         operator_sequence="AB")
    equidistant_time_stepper(psi, t0, tend, dt, ps.scheme,
                         operator_sequence)
end
function step!(psi::WaveFunction, dt::Real, scheme::EmbeddedScheme, operator_sequence="AB")
    step!(psi,dt,scheme.scheme2,operator_sequence)
end
function step!(psi::WaveFunction, dt::Real, scheme::PalindromicScheme, operator_sequence="AB")
    step!(psi,dt,scheme.scheme,operator_sequence)
end
function step!(psi::WaveFunction, dt::Real, scheme::DefectBasedScheme, operator_sequence="AB")
    step!(psi,dt,scheme.scheme,operator_sequence)
end

function savefig!(scheme,schemes,mytime,steps_a,nsteps_a)
    figure(1)
    hold(false)
            plot(mytime[1:nsteps_a-1], steps_a[1:nsteps_a-1])
            xlabel("t")
            ylabel("stepsize")
            if scheme==schemes[1]
                savefig("Emb43AKp_$(tol)_step.png", bbox_inches="tight")
            end
            if scheme==schemes[2]
                savefig("PP34A$(tol)_step.png", bbox_inches="tight")
            end
            if scheme==schemes[3]
                savefig("PP56A_$(tol)_step.png", bbox_inches="tight")
            end
            if scheme==schemes[4]
                savefig("Emb43AKp_defect_$(tol)_step.png", bbox_inches="tight")
            end
            if scheme==schemes[5]
                savefig("PP34A_defect$(tol)_step.png", bbox_inches="tight")
            end
            if scheme==schemes[6]
                savefig("PP56A_defect_$(tol)_step.png", bbox_inches="tight")
            end
end
function calc_stepmin(psi_ref,psia,psi,scheme,soliton)
    h=1.0
    chg=h/2
    #@printf("||psi_a-psi_ref||=%5.3E ,h=%5.3E \n",distance(psia,psi_ref),h)
    set!(psi, soliton)
    while chg>1e-3 || distance(psia,psi_ref) < distance(psi,psi_ref)
        set!(psi, soliton)
        time = @elapsed for t in equidistant_time_stepper(psi, t0, tend, h, scheme, "AB")
        end
        #@printf("||psi-psi_ref||=%5.3E,  h=%5.3E \n",distance(psi,psi_ref),h)
        if distance(psia,psi_ref) < distance(psi,psi_ref)
            h=h-chg
        else
            h=h+chg
        end
        chg=chg/2
    end
    #@printf("||psi-psi_ref||=%5.3E,  h=%20.18E \n",distance(psi,psi_ref),h)
    return h
end
function latextable(scheme,schemes,tol,nsteps_a,nsteps,time_a,time)
            if scheme==schemes[1]
            @printf("\\texttt{Emb 4/3 AK p}, TOL = \$ 10^{%d} \$ & \$ %d \$ & \$ %d \$ & \$ %5.3f \$ & \$ %5.3f \$ \\\\ \n",tol,nsteps_a,nsteps,time_a,time)
        end
        if scheme==schemes[2]
            @printf(    "\\texttt{PP 3/4 A}, TOL = \$ 10^{%d} \$ & \$ %d \$ & \$ %d \$ & \$ %5.3f \$ & \$ %5.3f \$ \\\\ \n",tol,nsteps_a,nsteps,time_a,time)
        end
        if scheme==schemes[3]
            @printf(    "\\texttt{PP 5/6 A}, TOL = \$ 10^{%d} \$ & \$ %d \$ & \$ %d \$ & \$ %5.3f \$ & \$ %5.3f \$ \\\\ \\hline \n",tol,nsteps_a,nsteps,time_a,time)
        end
        if scheme==schemes[4]
            @printf("\\texttt{Emb 4/3 AK p (defect)}, TOL = \$ 10^{%d} \$ & \$ %d \$ & \$ %d \$ & \$ %5.3f \$ & \$ %5.3f \$ \\\\ \n",tol,nsteps_a,nsteps,time_a,time)
        end
        if scheme==schemes[5]
            @printf(    "\\texttt{PP 3/4 A (defect)}, TOL = \$ 10^{%d} \$ & \$ %d \$ & \$ %d \$ & \$ %5.3f \$ & \$ %5.3f \$ \\\\ \n",tol,nsteps_a,nsteps,time_a,time)
        end
        if scheme==schemes[6]
            @printf(    "\\texttt{PP 5/6 A (defect)}, TOL = \$ 10^{%d} \$ & \$ %d \$ & \$ %d \$ & \$ %5.3f \$ & \$ %5.3f \$ \\\\ \\hline \n",tol,nsteps_a,nsteps,time_a,time)
        end
end


macro intrinsic(ex)
    esc(intrinsic_m(ex))
end

macro intrinsic(n::Int, ex)
    esc(intrinsic_m(ex, n))
end

function intrinsic_m(ex, n::Int=1)
    if ex isa Symbol
        def = JLStruct(;name=Symbol(ex, :Gate), supertype=IntrinsicRoutine)
        binding_name = ex
        binding = :(Core.@__doc__ const $ex = $(def.name)())
    else
        name, args, kw, whereparams, rettype = split_function_head(ex)
        kw === nothing || error("cannot have kwargs in intrinsic operation")
        rettype === nothing || error("cannot have rettype in intrinsic operation")

        typevars=whereparams
        fields = JLField[]
        for each in args
            field = @match each begin
                :($fname::$type) => JLField(;name=fname, type)
                ::Symbol => JLField(;name=each)
                _ => error("cannot handle $each")
            end
            push!(fields, field)
        end
        def = JLStruct(;name, fields, supertype=IntrinsicRoutine, typevars)
        binding_name = name
        binding = nothing
    end

    quote
        export $(def.name)
        Core.@__doc__ $(codegen_ast(def))
        $(codegen_helpers(def, n, binding_name))
        $binding
    end
end

nqubits(::Type{T}) where {T <: Routine} = error("YaoHIR.nqubits is not defined for $T")
isintrinsic(::Type{T}) where T = false

function codegen_helpers(def::JLStruct, n::Int, name::Symbol)
    quote
        $YaoHIR.nqubits(::Type{<:$(def.name)}) = $n
        $YaoHIR.isintrinsic(::Type{<:$(def.name)}) = true
        $YaoHIR.routine_name(::Type{<:$(def.name)}) = $(QuoteNode(name))
        $MLStyle.@as_record $(def.name)
    end
end

@intrinsic X
@intrinsic Y
@intrinsic Z
@intrinsic H
@intrinsic S
@intrinsic T
@intrinsic SWAP
@intrinsic shift(θ::T) where {T}
@intrinsic Rx(θ::T) where {T}
@intrinsic Ry(θ::T) where {T}
@intrinsic Rz(θ::T) where {T}
@intrinsic UGate(α::T, β::T, γ::T) where {T}


using DataStructures, StringBuilders, Missings

const keyfilename = "keyfile.csv"

const OD = OrderedDict

categories = OD("Liability" =>
                ["Transfer", "Other"],
                "Expense" =>
                [OD("Food" =>
                    ["Cafe", "Groceries", "Restaurant"]),
                 OD("Alcohol" =>
                    ["Shop", "Bar"]),
                 OD("Health" =>
                    ["Sport", "Other"]),
                 OD("Transport" =>
                    ["Private", "Public"]),
                 OD("FixedCosts" =>
                    ["Rent", "Internet", "Electricity", "Phone", "Daycare", "CSN", "Insurance", "BankCard"]),
                 "Shopping", "Fun", "Travel", "Other"],
                "Revenue" =>
                ["ParentalSupport", "Salary", "ExternalTransfer", "Other"])

cats = String[]
sb = StringBuilder()
n = 0
for (k, vs) in categories
    append!(sb, k, "\n")
    for v in vs
        if v isa OD
            for (k2, v2s) in v
                append!(sb, "\t", k2, "\n")
                for v2 in v2s
                    n += 1
                    append!(sb, "\t\t$n) ", v2, "\n")
                    push!(cats, "$k:$k2:$v2")
                end
            end
        else
            n += 1
            append!(sb, "\t$n) ", v, "\n")
            push!(cats, "$k:$v")
        end
    end
end
msg = String(sb)
print(msg)

function ask(comment, amount, person, date)
    print_with_color(:blue, comment, ", ", amount, " SEK, ", person, ", ", date, '\n')
    run(`googler -c se --np -n 5 $comment`)
    txt = readline()
    if all(isnumber, txt)
        i = parse(Int, txt)
        if 1 ≤ i ≤ n
            return cats[i]
        end
    end
    print_with_color(:red, "must be an integer between 1 and $n\n")
    ask(comment, amount, person, date)
end

function getkey(keyfile)
    key = Dict{String, String}()
    open(keyfile, "r") do o
        for l in eachline(o)
            comment, category = split(l, ",")
            @assert !haskey(key, comment) "found duplicate comment: $comment"
            key[comment] = category
        end
    end
    key
end

function addcomment!(o, key, comment, amount, person, date)
    category = ask(comment, amount, person, date)
    println(o, comment, ",", category)
    key[comment] = category
end

function categorise!(table, folder)
    keyfile = joinpath(folder, keyfilename)
    key = getkey(keyfile)
    table[:category] = ""
    open(keyfile, "a") do o
        for row in DataFrames.eachrow(table)
            comment = row[:description]
            row[:category] = haskey(key, comment) ? key[comment] : addcomment!(o, key, comment, row[:amount], row[:person], row[:date])
        end
    end
    table
end


# Fast parallel reduction for Kepler hardware
# - uses shuffle and shared memory to reduce efficiently
# - support for large arrays
#
# Based on devblogs.nvidia.com/parallelforall/faster-parallel-reductions-kepler/

using CUDAdrv, CUDAnative

#
# Main implementation
#

# Reduce a value across a warp
@inline function reduce_warp{F<:Function,T}(op::F, val::T)::T
    offset = CUDAnative.warpsize() ÷ UInt32(2)
    # TODO: this can be unrolled if warpsize is known...
    while offset > 0
        val = op(val, shfl_down(val, offset))
        offset ÷= UInt32(2)
    end
    return val
end

# Reduce a value across a block, using shared memory for communication
@inline function reduce_block{F<:Function,T}(op::F, val::T)::T
    # shared mem for 32 partial sums
    shared = @cuStaticSharedMem(T, 32)

    # TODO: use fldmod1 (JuliaGPU/CUDAnative.jl#28)
    wid  = div(threadIdx().x-UInt32(1), CUDAnative.warpsize()) + UInt32(1)
    lane = rem(threadIdx().x-UInt32(1), CUDAnative.warpsize()) + UInt32(1)

    # each warp performs partial reduction
    val = reduce_warp(op, val)

    # write reduced value to shared memory
    if lane == 1
        @inbounds shared[wid] = val
    end

    # wait for all partial reductions
    sync_threads()

    # read from shared memory only if that warp existed
    @inbounds val = (threadIdx().x <= fld(blockDim().x, CUDAnative.warpsize())) ? shared[lane] : zero(T)

    # final reduce within first warp
    if wid == 1
        val = reduce_warp(op, val)
    end

    return val
end

# Reduce an array across a complete grid
function reduce_grid{F<:Function,T}(op::F, input::CuDeviceArray{T,1},
                                    output::CuDeviceArray{T,1}, len::Integer)

    # TODO: neutral element depends on the operator (see Base's 2 and 3 argument `reduce`)
    val = zero(T)

    # reduce multiple elements per thread (grid-stride loop)
    # TODO: step range (see JuliaGPU/CUDAnative.jl#12)
    i = (blockIdx().x-UInt32(1)) * blockDim().x + threadIdx().x
    step = blockDim().x * gridDim().x
    while i <= len
        @inbounds val = op(val, input[i])
        i += step
    end

    val = reduce_block(op, val)

    if threadIdx().x == UInt32(1)
        @inbounds output[blockIdx().x] = val
    end

    return
end

"""
Reduce a large array.

Kepler-specific implementation, ie. you need sm_30 or higher to run this code.
"""
function gpu_reduce{F<:Function,T}(op::F, input::CuArray{T,1}, output::CuArray{T,1})
    len = length(input)

    # TODO: these values are hardware-dependent, with recent GPUs supporting more threads
    threads = 512
    blocks = min((len + threads - 1) ÷ threads, 1024)

    # the output array must have a size equal to or larger than the number of thread blocks
    # in the grid because each block writes to a unique location within the array.
    if length(output) < blocks
        throw(ArgumentError("output array too small, should be at least $blocks elements"))
    end

    @cuda (blocks,threads) reduce_grid(op, input, output, Int32(len))
    @cuda (1,1024) reduce_grid(op, output, output, Int32(blocks))

    return
end

# FURTHER IMPROVEMENTS:
# - use atomic memory operations
# - dynamic block/grid size based on device capabilities
# - vectorized memory access
#   devblogs.nvidia.com/parallelforall/cuda-pro-tip-increase-performance-with-vectorized-memory-access/


export A2CLearner

"""
    A2CLearner(;kwargs...)

# Keyword arguments

- `approximator`::[`ActorCritic`](@ref)
- `γ::Float32`, reward discount rate.
- `actor_loss_weight::Float32`
- `critic_loss_weight::Float32`
- `entropy_loss_weight::Float32`
- `update_freq::Int`, usually set to the same with the length of trajectory.
"""
Base.@kwdef mutable struct A2CLearner{A<:ActorCritic} <: AbstractLearner
    approximator::A
    γ::Float32
    max_grad_norm::Union{Nothing,Float32} = nothing
    actor_loss_weight::Float32
    critic_loss_weight::Float32
    entropy_loss_weight::Float32
    update_freq::Int
    update_step::Int = 0
    # for logging
    actor_loss::Float32 = 0.0f0
    critic_loss::Float32 = 0.0f0
    entropy_loss::Float32 = 0.0f0
    loss::Float32 = 0.0f0
    norm::Float32 = 0.0f0
end

Flux.functor(x::A2CLearner) = (app = x.approximator,), y -> @set x.approximator = y.app

function (learner::A2CLearner)(env::MultiThreadEnv)
    learner.approximator.actor(send_to_device(device(learner), state(env))) |> send_to_host
end

function (learner::A2CLearner)(env)
    s = state(env)
    s = Flux.unsqueeze(s, ndims(s) + 1)
    s = send_to_device(device(learner), s)
    learner.approximator.actor(s) |> vec |> send_to_host
end

function RLBase.update!(learner::A2CLearner, t::CircularArraySARTTrajectory)
    length(t) == 0 && return  # in the first update, only state & action is inserted into trajectory
    learner.update_step += 1
    if learner.update_step % learner.update_freq == 0
        _update!(learner, t)
    end
end

function _update!(learner::A2CLearner, t::CircularArraySARTTrajectory)
    n = length(t)

    AC = learner.approximator
    γ = learner.γ
    w₁ = learner.actor_loss_weight
    w₂ = learner.critic_loss_weight
    w₃ = learner.entropy_loss_weight
    to_device = x -> send_to_device(device(AC), x)

    states = select_last_dim(t[:state], 1:n)
    states_flattened = flatten_batch(states) |> to_device # (state_size..., n_thread * update_freq)

    actions = select_last_dim(t[:action], 1:n)
    actions = flatten_batch(actions)
    actions = CartesianIndex.(actions, 1:length(actions))

    next_state_values =
        t[:state] |> select_last_frame |> Array |> to_device |> AC.critic |> send_to_host

    gains =
        discount_rewards(
            t[:reward],
            γ;
            dims = 2,
            init = send_to_host(next_state_values),
            terminal = t[:terminal],
        ) |> to_device

    ps = Flux.params(AC)
    gs = gradient(ps) do
        logits = AC.actor(states_flattened)
        probs = softmax(logits)
        log_probs = logsoftmax(logits)
        log_probs_select = log_probs[actions]
        values = AC.critic(states_flattened)
        advantage = vec(gains) .- vec(values)
        actor_loss = -mean(log_probs_select .* Zygote.dropgrad(advantage))
        critic_loss = mean(advantage .^ 2)
        entropy_loss = -sum(probs .* log_probs) * 1 // size(probs, 2)
        loss = w₁ * actor_loss + w₂ * critic_loss - w₃ * entropy_loss
        ignore() do
            learner.actor_loss = actor_loss
            learner.critic_loss = critic_loss
            learner.entropy_loss = entropy_loss
            learner.loss = loss
        end
        loss
    end
    if !isnothing(learner.max_grad_norm)
        learner.norm = clip_by_global_norm!(gs, ps, learner.max_grad_norm)
    end
    update!(AC, gs)
end

RLCore.check(::QBasedPolicy{<:A2CLearner}, ::MultiThreadEnv) = nothing


# ===========================================================================
# All constants needed by tests are here
const unicode_chars = String.(readdlm(path*"/SampleFiles/julia-unicode.csv", '\n')[:,1])
const n_unicode = length(unicode_chars)
const breaking_dict = Dict{String, Any}(
    "0" => rand(Char),
    "1" => randstring(2^rand(2:10)),
   "16" => rand(UInt8),
   "17" => rand(UInt16),
   "18" => rand(UInt32),
   "19" => rand(UInt64),
   "20" => rand(UInt128),
   "32" => rand(Int8),
   "33" => rand(Int16),
   "34" => rand(Int32),
   "35" => rand(Int64),
   "36" => rand(Int128),
   "48" => rand(Float16),
   "49" => rand(Float32),
   "50" => rand(Float64),
   "80" => rand(Complex{UInt8}),
   "81" => rand(Complex{UInt16}),
   "82" => rand(Complex{UInt32}),
   "83" => rand(Complex{UInt64}),
   "84" => rand(Complex{UInt128}),
   "96" => rand(Complex{Int8}),
   "97" => rand(Complex{Int16}),
   "98" => rand(Complex{Int32}),
   "99" => rand(Complex{Int64}),
  "100" => rand(Complex{Int128}),
  "112" => rand(Complex{Float16}),
  "113" => rand(Complex{Float32}),
  "114" => rand(Complex{Float64}),
  "128" => rand(Char, 2^rand(2:6)),
  "129" => [randstring(2^rand(3:8)) for i = 1:rand(4:24)],
  "144" => rand(UInt8, rand(4:24)),
  "145" => rand(UInt16, rand(4:24)),
  "146" => rand(UInt32, rand(4:24)),
  "147" => rand(UInt64, rand(4:24)),
  "148" => rand(UInt128, rand(4:24)),
  "160" => rand(Int8, rand(4:24)),
  "161" => rand(Int16, rand(4:24)),
  "162" => rand(Int32, rand(4:24)),
  "163" => rand(Int64, rand(4:24)),
  "164" => rand(Int128, rand(4:24)),
  "176" => rand(Float16, rand(4:24)),
  "177" => rand(Float32, rand(4:24)),
  "178" => rand(Float64, rand(4:24)),
  "208" => rand(Complex{UInt8}, rand(4:24)),
  "209" => rand(Complex{UInt16}, rand(4:24)),
  "210" => rand(Complex{UInt32}, rand(4:24)),
  "211" => rand(Complex{UInt64}, rand(4:24)),
  "212" => rand(Complex{UInt128}, rand(4:24)),
  "224" => rand(Complex{Int8}, rand(4:24)),
  "225" => rand(Complex{Int16}, rand(4:24)),
  "226" => rand(Complex{Int32}, rand(4:24)),
  "227" => rand(Complex{Int64}, rand(4:24)),
  "228" => rand(Complex{Int128}, rand(4:24)),
  "240" => rand(Complex{Float16}, rand(4:24)),
  "242" => rand(Complex{Float64}, rand(4:24)),
  "241" => rand(Complex{Float32}, rand(4:24))
  )
const NOOF = "
  HI ITS CLOVER LOL
          ,-'-,  `---..
         /             \\
         =,             .
  ______<3.  ` ,+,     ,\\`
 ( \\  + `-”.` .; `     `.\\
 (_/   \\    | ((         ) \\
  |_ ;  \"    \\   (        ,’ |\\
  \\    ,- '💦 (,\\_____,’   / “\\
   \\__---+ }._)              |\\
   / _\\__💧”)/                  +
  ( /    💧” \\                  ++_
   \\)    ,“  |)                ++  ++
   💧     “💧  (                 *    +***
"


module TurbulenceClosures

export
  IsotropicViscosity,
  ConstantIsotropicDiffusivity,
  ConstantAnisotropicDiffusivity,
  AnisotropicBiharmonicDiffusivity,
  TwoDimensionalLeith,
  ConstantSmagorinsky,
  SmagorinskyLilly,
  BlasiusSmagorinsky,
  AnisotropicMinimumDissipation,
  RozemaAnisotropicMinimumDissipation,
  VerstappenAnisotropicMinimumDissipation,

  TurbulentDiffusivities,
  calculate_diffusivities!,

  ∇_κ_∇c,
  ∇_κ_∇T,
  ∇_κ_∇S,
  ∂ⱼ_2ν_Σ₁ⱼ,
  ∂ⱼ_2ν_Σ₂ⱼ,
  ∂ⱼ_2ν_Σ₃ⱼ,

  cell_diffusion_timescale

using Oceananigans: @hascuda

@hascuda using CUDAdrv, CUDAnative

using
  Oceananigans,
  Oceananigans.Grids,
  Oceananigans.Operators,
  GPUifyLoops

import Oceananigans: with_tracers

using Oceananigans: AbstractArchitecture, AbstractGrid,
                    TracerFields, device, launch_config,
                    buoyancy_perturbation, buoyancy_frequency_squared, ∂x_b, ∂y_b, ∂z_b

####
#### Molecular viscosity and thermal diffusivity definitions
#### Approximate viscosities and thermal diffusivities for seawater at 20ᵒC and 35 psu,
#### according to Sharqawy et al., "Thermophysical properties of seawater: A review of
#### existing correlations and data" (2010).
####

const ν₀ = 1.05e-6
const κ₀ = 1.46e-7

####
#### Abstract types
####

"""
    TurbulenceClosure{FT}

Abstract supertype for turbulence closures with model parameters stored as properties of
type `FT`.
"""
abstract type TurbulenceClosure{FT} end

"""
    IsotropicViscosity{FT} <: TurbulenceClosure{FT}

Abstract supertype for turbulence closures that are defined by an isotropic viscosity
and isotropic diffusivities with model parameters stored as properties of type `FT`.
"""
abstract type IsotropicViscosity{FT} <: TurbulenceClosure{FT} end

"""
    TensorDiffusivity{FT} <: TurbulenceClosure{FT}

Abstract supertype for turbulence closures that are defined by a tensor viscosity and
tensor diffusivities with model parameters stored as properties of type `FT`.
"""
abstract type TensorDiffusivity{FT} <: TurbulenceClosure{FT} end

"""
    AbstractSmagorinsky{FT}

Abstract supertype for large eddy simulation models based off the model described
by Smagorinsky with model parameters stored as properties of type `FT`.
"""
abstract type AbstractSmagorinsky{FT} <: IsotropicViscosity{FT} end

"""
    AbstractAnisotropicMinimumDissipation{FT}

Abstract supertype for large eddy simulation models based on the anisotropic minimum
dissipation principle with model parameters stored as properties of type `FT`.
"""
abstract type AbstractAnisotropicMinimumDissipation{FT} <: IsotropicViscosity{FT} end

"""
    AbstractLeith{FT}

Abstract supertype for large eddy simulation models based on the Leith viscosity
principle with model parameters stored as properties of type `FT`.
"""
abstract type AbstractLeith{FT} <: IsotropicViscosity{FT} end

####
#### 'Tupled closure' implementation
####

for stress_div in (:∂ⱼ_2ν_Σ₁ⱼ, :∂ⱼ_2ν_Σ₂ⱼ, :∂ⱼ_2ν_Σ₃ⱼ)
    @eval begin
        @inline function $stress_div(i, j, k, grid::AbstractGrid{FT}, closure_tuple::Tuple, U,
                                     K_tuple, args...) where FT

            stress_div_ijk = zero(FT)

            ntuple(Val(length(closure_tuple))) do α
                @inbounds closure = closure_tuple[α]
                @inbounds K = K_tuple[α]
                stress_div_ijk += $stress_div(i, j, k, grid, closure, U, K, args...)
            end

            return stress_div_ijk
        end
    end
end

@inline function ∇_κ_∇c(i, j, k, grid::AbstractGrid{FT}, closure_tuple::Tuple,
                        c, tracer_index, K_tuple, args...) where FT
    flux_div_ijk = zero(FT)

    ntuple(Val(length(closure_tuple))) do α
        @inbounds closure = closure_tuple[α]
        @inbounds K = K_tuple[α]
        flux_div_ijk +=  ∇_κ_∇c(i, j, k, grid, closure, c, tracer_index, K, args...)
    end

    return flux_div_ijk
end

function calculate_diffusivities!(K_tuple::Tuple, arch, grid, closure_tuple::Tuple, args...)
    ntuple(Val(length(closure_tuple))) do α
        @inbounds closure = closure_tuple[α]
        @inbounds K = K_tuple[α]
        calculate_diffusivities!(K, arch, grid, closure, args...)
    end

    return nothing
end

TurbulentDiffusivities(arch::AbstractArchitecture, grid::AbstractGrid, tracers, closure_tuple::Tuple) =
    Tuple(TurbulentDiffusivities(arch, grid, tracers, closure) for closure in closure_tuple)

with_tracers(tracers, closure_tuple::Tuple) =
    Tuple(with_tracers(tracers, closure) for closure in closure_tuple)

####
#### Include module code
####

# Fallback constructor for diffusivity types without precomputed diffusivities:
TurbulentDiffusivities(arch::AbstractArchitecture, grid::AbstractGrid, args...) = nothing

include("turbulence_closure_utils.jl")
include("closure_operators.jl")
include("velocity_tracer_gradients.jl")

include("closure_tuples.jl")

include("turbulence_closure_implementations/constant_isotropic_diffusivity.jl")
include("turbulence_closure_implementations/constant_anisotropic_diffusivity.jl")
include("turbulence_closure_implementations/anisotropic_biharmonic_diffusivity.jl")
include("turbulence_closure_implementations/leith_enstrophy_diffusivity.jl")
include("turbulence_closure_implementations/smagorinsky_lilly.jl")
include("turbulence_closure_implementations/blasius_smagorinsky.jl")
include("turbulence_closure_implementations/verstappen_anisotropic_minimum_dissipation.jl")
include("turbulence_closure_implementations/rozema_anisotropic_minimum_dissipation.jl")

include("turbulence_closure_diagnostics.jl")

####
#### Some value judgements here
####

"""
    AnisotropicMinimumDissipation

An alias for `VerstappenAnisotropicMinimumDissipation`.
"""
const AnisotropicMinimumDissipation = VerstappenAnisotropicMinimumDissipation

"""
    ConstantSmagorinsky

An alias for `SmagorinskyLilly`.
"""
const ConstantSmagorinsky = SmagorinskyLilly

end # module


# Copyright © 2021, UChicago Argonne, LLC
# All Rights Reserved.

# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.

function get_fixed_nodes(ysol, fixlevel)
    if fixlevel < 0
        return nothing
    elseif fixlevel == 0
        return keys(ysol)
    else
        fixed_nodes = keys(ysol)
        for i in 1:fixlevel
            fixed_nodes = get_nonleaves(fixed_nodes)
        end
        return fixed_nodes
    end
end

function reset_variables(model, df_vars, bounds; unused_nodes=nothing, fixed_nodes=nothing)
    """

    """
    variables = JuMP.all_variables(model)

    # ##### (1) reset variables to original setting
    for row in eachrow(df_vars)
        var = variables[row.index]
        if JuMP.is_fixed(var)
            JuMP.unfix(var)
            if row.vartype in ["c"]
                JuMP.set_lower_bound(var, bounds["c_lb"])
                JuMP.set_lower_bound(var, bounds["c_ub"])
            end
        end
    end

    if isnothing(unused_nodes)
        unused_nodes = []
    end

    if isnothing(fixed_nodes)
        fixed_nodes = []
    end

    for row in eachrow(df_vars)
        var = variables[row.index]
        if row.vartype in ["y", "c"]
            if row.nodeid in unused_nodes
                JuMP.fix(var, 0.0, force=true)
            elseif row.nodeid in fixed_nodes
                JuMP.fix(var, row.sol, force=true)
            else
                if row.vartype in ["y"] && row.sol > 0.5
                    JuMP.set_start_value(var, 1.0)
                elseif row.vartype in ["y"] && row.sol <= 0.5
                    JuMP.set_start_value(var, 0.0)
                else
                    JuMP.set_start_value(var, row.sol)
                end
            end
        end
    end

    model
end

# function reset_variables_by_maxdist_fixlevel(model, df_vars, bounds, ysol, maxdist, fixlevel)
#     fixed_nodes = get_fixed_nodes(ysol, fixlevel)
#     nodes_ground = Set(1:maximum(df_vars.nodeid))
#     unused_nodes = get_unused_nodes(ysol, nodes_ground, maxdist)
#     reset_variables(model, df_vars, bounds; unused_nodes=nothing, fixed_nodes=fixed_nodes)
# end

function add_neighbor_constraints(model, df_vars, mindist, maxdist)
    variables = JuMP.all_variables(model)
    df_y_1 = filter(row -> row.vartype == "y" && row.sol > 0.5, df_vars)
    df_y_0 = filter(row -> row.vartype == "y" && row.sol <= 0.5, df_vars)
    active_nodes = unique(df_y_1.nodeid)
    @constraint(model, mindist <= sum(variables[row.index] for row in eachrow(df_y_0) if !(row.nodeid in active_nodes))
                + sum(1 - variables[i] for i in df_y_1.index) <= maxdist)

    return model
end

function set_restricted_MINLP(model, df_vars, bounds, ysol, mindist, maxdist, fixlevel)
    fixed_nodes = get_fixed_nodes(ysol, fixlevel)
    nodes_ground = Set(1:maximum(df_vars.nodeid))
    unused_nodes = get_unused_nodes(ysol, nodes_ground, maxdist)
        model = reset_variables(model, df_vars, bounds; unused_nodes=unused_nodes, fixed_nodes=fixed_nodes)
    model = add_neighbor_constraints(model, df_vars, mindist, maxdist)
    model
end

function initialize_STreCH_parameters(params)
    mindist = 0
    maxdist = params["INT_SOLU_DIST_INIT"]
    fixlevel = params["INT_FIXLEVEL_INIT"]
    nodelimit = params["INT_NODELIMIT_INIT"]
    mindist, maxdist, fixlevel, nodelimit
end

function update_STreCH_parameters(params, mindist, maxdist, fixlevel, nodelimit)
    if fixlevel < params["INT_FIXLEVEL_MAX"]
        fixlevel += 1
    else
        if maxdist < params["INT_SOLU_DIST_MAX"]
            fixlevel = params["INT_FIXLEVEL_INIT"]
            mindist = maxdist + 1
            maxdist = maxdist + params["INT_SOLU_DIST_INCR"]
        else
            if nodelimit < params["INT_NODELIMIT_MAX"]
                fixlevel = params["INT_FIXLEVEL_INIT"]
                mindist = 0
                maxdist = params["INT_SOLU_DIST_INIT"]
                nodelimit = 10 * nodelimit
            else
                return nothing, nothing, nothing, nothing
            end
        end
    end

    mindist, maxdist, fixlevel, nodelimit
end

function update_df_sol(df_sol, obs, df_vars, num_iter, time)
    ysol, csol = get_ysol_csol(df_vars)
    formula = get_formula(ysol, csol)[1]
    objval = compute_err_formula(obs, formula, "mse"; err2Inf=true)
    append!(df_sol, Dict("index"=>num_iter, "formula"=>formula, "objval"=>objval, "treesize"=>length(ysol), "time"=>time))
    df_sol
end

function STreCH(dir; datafile="data.txt", paramfile="params.txt")
    EPSILON = 1e-12
    params = read_parameters(dir * paramfile)
    df_sol = DataFrame(index=Int[], formula=String[], objval=Float64[], treesize=Int[], time=Float64[])

    io = open_logger(dir * "log.log", params["LOGLEVEL"])

    t_begin = time()
    num_iter = 1

    ### Parameters
    nodes = OrderedSet(1:(2^(params["INT_MAXDEPTH"]+1)-1))
    obs = DelimitedFiles.readdlm(dir * datafile)

    ### Solve an initial problem
    model, df_vars, bounds = get_MINLP_model(nodes, obs, params["OPERATORS"], params["FORMULATION"], params)
    model = reset_variables(model, df_vars, bounds, unused_nodes=8:1000)
    set_optimizer(model, SCIP.Optimizer)

    ### Set absgap, timelimit
    remaining_time = params["DBL_TIMELIMIT"] - (time() - t_begin)
    set_optimizer_attribute(model, "limits/time", remaining_time)
    set_optimizer_attribute(model, "display/verblevel", 2)

    ### Solve
    optimize!(model)

    ### Save the result
    objval_best, ysol_best, csol_best, df_vars_best = save_solution(dir, model, df_vars, obs, save_df_sol=false)
    df_sol = update_df_sol(df_sol, obs, df_vars_best, num_iter, time() - t_begin)
    CSV.write(dir * "df_sol.csv", df_sol)

    ### Improve
    mindist, maxdist, fixlevel, nodelimit = initialize_STreCH_parameters(params)
    while remaining_time > 0 && objval_best > EPSILON
        num_iter += 1
        @info "num_iter = $num_iter, fixlevel = $fixlevel, mindist=$mindist, maxdist=$maxdist, nodelimit=$nodelimit"
        model, df_vars, bounds = get_MINLP_model(nodes, obs, params["OPERATORS"], params["FORMULATION"], params)
        model = set_restricted_MINLP(model, df_vars_best, bounds, ysol_best, mindist, maxdist, fixlevel)

        ### Set the optimizer and its attributes
        set_optimizer(model, SCIP.Optimizer)
        remaining_time = params["DBL_TIMELIMIT"] - (time() - t_begin)
        absgap = objval_best < 1e-06 ? 0.0 : 0.99 * objval_best
        set_optimizer_attribute(model, "limits/time", remaining_time)
        set_optimizer_attribute(model, "limits/nodes", nodelimit)
        set_optimizer_attribute(model, "limits/absgap", absgap)
        set_optimizer_attribute(model, "display/verblevel", 2)

        ### Solve
        optimize!(model)
        termination_status = JuMP.termination_status(model)
        @info "num_iter = $num_iter , $termination_status"

        ### Read solution
        objval_new, ysol_new, csol_new, df_vars_new = save_solution(dir, model, df_vars, obs, save_df_sol=false)
        df_sol = update_df_sol(df_sol, obs, df_vars_new, num_iter, time() - t_begin)
        CSV.write(dir * "df_sol.csv", df_sol)

        ### Compare with the best solution and update paramters for the next iteration
        if objval_new < objval_best - EPSILON
            objval_best = objval_new
            ysol_best = ysol_new
            csol_best = csol_new
            df_vars_best = df_vars_new
            mindist, maxdist, fixlevel, nodelimit = initialize_STreCH_parameters(params)
        else
            """
            update parameters and keep the current solution
            """
            mindist, maxdist, fixlevel, nodelimit = update_STreCH_parameters(params, mindist, maxdist, fixlevel, nodelimit)
            if isnothing(mindist)
                break
            end
        end

        remaining_time = params["DBL_TIMELIMIT"] - (time() - t_begin)
    end

    # ### Remove files
    # Glob.glob(dir * "*.csv")
    # for file in Glob.glob(dir * "*.csv")
    #     if !occursin("df_sol.csv", file) && !occursin("df_vars.csv", file)
    #         rm(file)
    #     end
    # end

    close(io)
end


struct Snowflake
    n::UInt64
end

Snowflake(s::AbstractString) = Snowflake(parse(UInt64, s))
Base.show(io::IO, s::Snowflake) = print(io, string(s.n; base=10))
Base.:(==)(s::Snowflake, n::Integer) = s.n == n
Base.:(==)(n::Integer, s::Snowflake) = n == s.n
StructTypes.StructType(::Type{Snowflake}) = StructTypes.StringType()
HTTP.escapeuri(s::Snowflake) = escapeuri(s.n)

snowflake2datetime(s::Snowflake) = unix2datetime(((s.n >> 22) + 1420070400000) / 1000)
worker_id(s::Snowflake) = (s.n & 0x3e0000) >> 17
process_id(s::Snowflake) = (s.n & 0x1f000) >> 12
increment(s::Snowflake) = s.n & 0xfff


@testset "void types" begin
    @test scitype(nothing) == Nothing
    @test scitype(missing) == Missing
    @test scitype([nothing, nothing]) == AbstractVector{Nothing}
end

@testset "scitype" begin
    X = [1, 2, 3]
    @test scitype(X) == AbstractVector{Unknown}

    @test scitype(missing) == Missing
    @test scitype((5, 2)) == Tuple{Unknown,Unknown}
    anyv = Any[5]
    @test scitype(anyv[1]) == Unknown

    X = [missing, 1, 2, 3]
    @test scitype(X) == AbstractVector{Union{Missing, Unknown}}

    Xnm = X[2:end]
    @test scitype(Xnm) == AbstractVector{Union{Missing, Unknown}}
    @test scitype(Xnm, tight=true) == AbstractVector{Unknown}

    Xm = Any[missing, missing]
    @test scitype(Xm) == AbstractVector{Missing}

    @test scitype([missing, missing]) == AbstractVector{Missing}
end

@testset "scitype2" begin
    ScientificTypesBase.Scitype(::Type{<:Integer}, ::MockMLJ) = Count
    X = [1, 2, 3]
    @test scitype(X) == AbstractVector{Count}
    Xm = [missing, 1, 2, 3]
    @test scitype(Xm) == AbstractVector{Union{Missing,Count}}
    Xnm = Xm[2:end]
    @test scitype(Xnm) == AbstractVector{Union{Missing,Count}}
    @test scitype(Xnm; tight=true) == AbstractVector{Count}

    @test elscitype(X) == Count
    @test elscitype(Xm) == Union{Missing,Count}
    @test elscitype(Xnm) == Union{Missing,Count}
    @test elscitype(Xnm, tight=true) == Count
end

@testset "temporal types" begin
    @test ScientificDate <: ScientificTimeType
    @test ScientificDateTime <: ScientificTimeType
    @test ScientificTime <: ScientificTimeType
end

@testset "Empty array" begin
    set_convention(MockMLJ())
    ScientificTypesBase.Scitype(::Type{<:Integer}, ::MockMLJ) = Count
    ScientificTypesBase.Scitype(::Type{Missing}, ::MockMLJ) = Missing
    @test scitype(Int[]) == AbstractVector{Count}
    @test scitype(Any[]) == AbstractVector{Unknown}
    @test scitype(Missing[]) == AbstractVector{Missing}
    @test scitype(Vector{Union{Int,Missing}}()) == AbstractVector{Union{Missing,Count}}
end