function instantiate(pat::PatVar, sub::Sub, rule::Rule)
    if haskey(sub, pat)
        (eclass, lit) = sub[pat]
        return (lit !== nothing ? lit : eclass)
    else
        error("unbound pattern variable $pat in rule $rule")
    end
end

function instantiate(pat::PatLiteral{T}, sub::Sub, rule::Rule) where T
    pat.val
end

function instantiate(pat::PatTypeAssertion, sub::Sub, rule::Rule)
    instantiate(pat.var, sub, rule)
end

function instantiate(pat::PatTerm, sub::Sub, rule::Rule)
    # TODO support custom types here!
    # similarterm ? ask Shashi
    meta = pat.metadata
    if meta !== nothing && meta.iscall
        Expr(:call, pat.head, map(x -> instantiate(x, sub, rule), pat.args)...)
    else
        Expr(pat.head, map(x -> instantiate(x, sub, rule), pat.args)...)
    end
end

function apply_rule!(g::EGraph, rule::UnequalRule,
        match::Match, matches::MatchesBuf, rep::Report, mod::Module)
    (_, pat, sub, id) = match
    lc = id
    rinst = instantiate(pat, sub, rule)
    rc = addexpr!(g, rinst).id
    # delete!(matches, match)
    if find(g, lc) == find(g, rc)
        @log "Contradiction!" rule
        rep.reason = Contradiction()
        return (false, rep)
    end
    return (true, nothing)
end

function apply_rule!(g::EGraph, rule::SymbolicRule,
        match::Match, matches::MatchesBuf, rep::Report,  mod::Module)
    (_, pat, sub, id) = match
    rinst = instantiate(pat, sub, rule)
    rc = addexpr!(g, rinst)
    if canprune(typeof(rule)) && rule.prune
        prune!(g, id, rc)
    else
        merge!(g, id, rc.id)
    end
    return (true, nothing)
end

function apply_rule!(g::EGraph, rule::DynamicRule,
        match::Match, matches::MatchesBuf, rep::Report,  mod::Module)
    (_, pat, sub, id) = match
    f = Rules.getrhsfun(rule, mod)
    actual_params = map(rule.patvars) do x
        (eclass, literal) = sub[x]
        literal !== nothing ? literal : eclass
    end
    r = f(geteclass(g, id), g, actual_params...)
    rc = addexpr!(g, r)

    if canprune(typeof(rule)) && rule.prune
        prune!(g, id, rc)
    else
        merge!(g, id, rc.id)
    end
    return (true, nothing)
end

using .ReportReasons
function eqsat_apply!(g::EGraph, matches::MatchesBuf,
        scheduler::AbstractScheduler, rep::Report,
        mod::Module, params::SaturationParams)::Report
    i = 0
    for match ∈ matches
        i += 1

        # if i % 300 == 0
        if params.eclasslimit > 0 && g.numclasses > params.eclasslimit
            @log "E-GRAPH SIZEOUT"
            rep.reason = EClassLimit(params.eclasslimit)
            return rep
        end

        #     if params.stopwhen()
        #         @log "Halting requirement satisfied"
        #         rep.reason = ConditionSatisfied()
        #         return rep
        #     end
        # end

        rule=match[1]
        writestep!(scheduler, rule)
        if find(g, match[4]) ∈ g.pruned
            return rep
        end
        (ok, nrep) = apply_rule!(g, rule, match, matches, rep, mod)
        if !ok
            return nrep
        end
        # println(rule)
        # println(sub)
        # println(l); println(r)
        # display(egraph.classes); println()
    end
    return rep
end


using Test

import Functional
using Functional: or, default, when
const F = Functional

struct TestStruct
    x::Int
    s::String
end

mutable struct TestMutableStruct
    x::Int
    s::String
end

join_test() = (["a", "b", "c"] |> F.join(",")) == "a,b,c"
sum_test() = ([1,2,3,4] |> F.sum) == 10
enumerate_test() = (["a", "b", "c"] |> F.enumerate |> collect) == [(1, "a"), (2, "b"), (3, "c")]

take_test() = ([1,2,3] |> F.take(2) |> collect) == [1,2]
takewhile_test() = ([1,2,3,4] |> F.takewhile(x -> x < 3) |> collect) == [1,2]

skip_test() = ([1,2,3] |> F.skip(2) |> collect) == [3]
skipwhile_test() = ([1,2,3,4] |> F.skipwhile(x -> x < 3) |> collect) == [3,4]

map_test() = ([1,2,3] |> F.map(x -> 2 * x) |> collect) == [2,4,6]
zip_test() = (([1,2], ["a", "b"]) |> x -> F.zip(x) |> collect) == [(1, "a"), (2, "b")]
reverse_test() = ([1,2,3,4] |> F.reverse |> collect) == [4,3,2,1]

all_test() = ([1,2,3,4] |> F.all(x -> x != 3)) == false
any_test() = ([1,2,3,4] |> F.any(x -> x == 3)) == true

groupby_test() = begin
    a = TestStruct(10, "a")
    b = TestStruct(10, "b")
    c = TestStruct(11, "c")
    d = TestStruct(11, "d")
    ([a,b,c,d] |> F.groupby(e -> e.x) |> collect) == [[a,b], [c,d]]
end

apply_test() = ([TestMutableStruct(1, "ciao"),
                TestMutableStruct(2, "abaco")] |> F.apply(x -> x.x = 2)) === nothing

@testset "FunctionTests" begin
    @test join_test()
    @test sum_test()
    @test enumerate_test()

    @test take_test()
    @test takewhile_test()

    @test skip_test()
    @test skipwhile_test()

    @test map_test()
    @test zip_test()
    @test reverse_test()

    @test all_test()
    @test any_test()

    @test groupby_test()

    @test apply_test()
end

matching_test() = 50 |> F.match(
    10 => 10,
    20 => 20,
    50 => 50
) == 50

matching_or_test() = 50 |> F.match(
    or(10, 20) => 10,
    or(20, 50) => 20
) == 20

matching_when_test() = 50 |> F.match(
    (50 |> when(x -> x < 50)) => 10,
    (50 |> when(x -> x >= 50)) => 20
) == 20

matching_with_default() = 50 |> F.match(
    20 => 20,
    default() => 10
)  == 10

@testset "MatchingTests" begin
    @test matching_test()
    @test matching_or_test()
    @test matching_when_test()
    @test matching_with_default()
end


#args
dataset = "3dmnist"
root = "./$(dataset)/"
K = 10 # k nearest-neighbors
batch_size = 32
num_classes = 10 #possible values {10,40}
npoints = 1024
epochs = 50


export CompositeFactor, @composite

abstract type CompositeFactor <: FactorNode end

"""
The `@composite` macro allows for defining custom (composite) nodes. Composite
nodes allow for implementating of custom update rules that may be computationally
more efficient or convenient. A composite node can be defined with or without an
internal model. For detailed usage instructions we refer to the `composite_nodes` demo.
"""
macro composite(name::Symbol, exposed_vars::Expr, model::Expr)
    (exposed_vars.head == :tuple) || error("Exposed variables should be passed as Tuple")

    exposed_var_arguments = join(["$varname::Variable" for varname in exposed_vars.args], ", ")
    n_vars = length(exposed_vars.args)

    # Code for constructing interfaces
    interface_definitions = ""
    for idx = 1:n_vars
        varname = exposed_vars.args[idx]
        interface_definitions *= "self.i[:$varname] = self.interfaces[$idx] = ForneyLab.associate!(Interface(self), $varname)\n"
    end

    # Code for constructing exposed variables in inner graph
    exposed_var_definitions = ""
    terminal_definitions = ""
    for idx = 1:n_vars
        varname = exposed_vars.args[idx]
        exposed_var_definitions *= "local $varname = Variable(id=:$varname)\n"
        terminal_definitions *= "push!(self.terminals, Terminal($varname, self.interfaces[$idx], id=:$varname))\n"
        terminal_definitions *= "self.interface2terminal[self.interfaces[$idx]] = self.terminals[$idx]\n"
    end

    expr = parse("""
    mutable struct $name <: CompositeFactor
        id::Symbol
        interfaces::Vector{Interface}
        i::Dict{Symbol, Interface}
        inner_graph::FactorGraph
        terminals::Vector{Terminal}
        interface2terminal::Dict{Interface,Terminal}

        function $name($exposed_var_arguments; id=ForneyLab.generateId($name))
            self = new(id, Array{Interface}(undef, $n_vars), Dict{Symbol,Interface}())
            outer_graph = currentGraph()
            ForneyLab.addNode!(outer_graph, self)
            $interface_definitions

            # Build internal graph
            self.inner_graph = FactorGraph()
            self.terminals = Terminal[]
            self.interface2terminal = Dict{Interface,Terminal}()
            let
                $exposed_var_definitions
                $model
                $terminal_definitions
            end
            setCurrentGraph(outer_graph)

            return self
        end
    end
    """)

    return esc(expr)
end


function get_refraction(obj::AtmosphericRefractionModel, arg0::jdouble)
    return jcall(obj, "getRefraction", jdouble, (jdouble,), arg0)
end


function AdamsMoultonIntegratorBuilder(arg0::jint, arg1::jdouble, arg2::jdouble, arg3::jdouble)
    return AdamsMoultonIntegratorBuilder((jint, jdouble, jdouble, jdouble), arg0, arg1, arg2, arg3)
end

function build_integrator(obj::AdamsMoultonIntegratorBuilder, arg0::Orbit, arg1::OrbitType)
    return jcall(obj, "buildIntegrator", AbstractIntegrator, (Orbit, OrbitType), arg0, arg1)
end


function Base.length(x::NASM)
  return length(fieldnames(typeof(x)))
end

function Base.getindex(x::NASM, i::Int64)
  return getproperty(x, fieldnames(typeof(x))[i])
end

@recipe function f(iter,
                   x::Vector{RiskParameters{T}}) where {T <: DiseaseStateSequence}
  xguide --> "Iteration"
  yguide --> "Value"
  legend --> :none
  seriestype := :line
  # label --> permutedims(_parameters(x[1]))
  iter, convert(Array{Float64, 2}, x[iter])
end

@recipe function f(iter, x::Vector{T}) where {T <: NASM}
  xguide --> "Iteration"
  yguide --> "Value"
  legend --> :none
  seriestype := :line
  # label --> permutedims(_parameters(x[1]))
  iter, convert(Array{Float64, 2}, x[iter])
end

@recipe function f(iter, p::Int64, x::MCMC{S, M}) where {S <: DiseaseStateSequence, M <: ILM}
  np = length(x.markov_chains[1].risk_parameters[1])
  sp = 0
  if M == PhyloILM
    sp += length(x.markov_chains[1].substitution_model[1])
  end
  xguide --> "Iteration"
  yguide --> "Value"
  guidefontsize --> 8
  seriestype --> :line
  if 0 <= p <= np
    for i in eachindex(x.markov_chains)
      @series begin
        label --> "Markov chain $i"
        iter, getindex.(x.markov_chains[i].risk_parameters[iter], p)
      end
    end
  elseif np < p <= (np + sp)
    for i in eachindex(x.markov_chains)
      @series begin
        label --> "Markov chain $i"
        iter, getindex.(x.markov_chains[i].substitution_model[iter], p-np)
      end
    end
  else
    error("Invalid parameter index")
  end
end

@recipe function f(iter, x::MCMC{S, M}) where {S <: DiseaseStateSequence, M <: ILM}
  np = length(x.markov_chains[1].risk_parameters[1])
  if M == PhyloILM
    np += length(x.markov_chains[1].substitution_model[1])
  end
  layout --> (np, 1)
  legend --> :none
  link --> :y
  for i = 1:np
    @series begin
      subplot := i
      iter, i, x
    end
  end
end


function char2Str(charTuple::Tuple{Vararg{UInt8}})
    cs = collect(charTuple)
    idx = 1
    for i in eachindex(cs)
        if cs[i] == 0
            break
        end
        idx = i
    end
    cs = Char.(cs[1:idx])

    return String(cs)
end

function rotsFromTuple(rotsTuple::Array{NTuple{9,Int32},1}, nop::Integer)
    r = Array{Int64,3}(undef, 3, 3, nop)
    for i in 1:nop
        r[:,:,i] = reshape([Base.convert(Int64, e) for e in rotsTuple[i]], 3, 3)
    end
    return r
end

function transFromTuple(transTuple::Array{NTuple{3,Float64}}, nop::Integer)
    t = Array{Float64,2}(undef, 3, nop)
    for i in 1:nop
        t[:,i] = [e for e in transTuple[i]]
    end
    return t
end


using HorizonSideRobots
export HorizonSide, HorizonSideRobots, Nord, Ost, Robot, Sud, West, isborder, ismarker, move!, putmarker!, save, show, show!, sitcreate, sitedit, sitedit!, temperature
next(side::HorizonSide) = HorizonSide(mod(Int(side)+1, 4))
inverse(side::HorizonSide) = HorizonSide(mod(Int(side)+2, 4)) ###Разварот
function putmarkers!(r::Robot, side::HorizonSide)   ###МаркеровОчка пока нет стеки
    while isborder(r,side)==false
        move!(r,side)
        putmarker!(r)
    end
end
function moves!(r::Robot, side::HorizonSide) ###счетчик
    num_steps=0
    while isborder(r,side)==false
        move!(r,side)
        num_steps+=1
    end
    return num_steps
end
function moves!(r::Robot,side::HorizonSide,num_steps::Int) #Обратно
    for _ in 1:numsteps # символ "_" заменяет фактически не используемую переменную
        move!(r,side)
    end
end
function movements!(r::Robot,side::HorizonSide,num_steps::Int) #Обратно2
    for _ in 1:num_steps
        move!(r,side)
    end
end

function movements!(r::Robot,side::HorizonSide) #Движение до стенки
    while isborder(r,side) == false
        move!(r,side)
    end
end
function moves!(r::Robot,side::HorizonSide)
    while move_if_possible!(r,side)

    end
end

movements!(r::Robot, side::HorizonSide) = while isborder(r,side)==false move!(r,side) end

function get_num_movements!(r::Robot, side::HorizonSide)
    num_steps = 0
    while isborder(r,side)==false
        move!(r,side)
        num_steps += 1
    end
    return num_steps
end

function through_rectangles_into_angle(r,angle::NTuple{2,HorizonSide})
    num_steps=[]
    while isborder(r,angle[1])==false || isborder(r,angle[2]) # Робот - не в юго-западном углу
        push!(num_steps, movements!(r, angle[2]))
        push!(num_steps, movements!(r, angle[1]))
    end
    return num_steps
end

function movements!(r,sides,num_steps::Vector{Int})
    for (i,n) in enumerate(num_steps)
        movements!(r, sides[mod(i-1, length(sides))+1], n)
    end
end

function through_rectangles_into_angle(r,angle::NTuple{2,HorizonSide})
    num_steps=[]
    while isborder(r,angle[1])==false || isborder(r,angle[2]) # Робот - не в юго-западном углу
        push!(num_steps, movements!(r, angle[2]))
        push!(num_steps, movements!(r, angle[1]))
    end
    return num_steps
end
function move_if_possible!(r::Robot, direct_side::HorizonSide)::Bool
    orthogonal_side = next(direct_side)
    reverse_side = reverse(orthogonal_side)
    num_steps=0
    while isborder(r,direct_side) == true
        if isborder(r, orthogonal_side) == false
            move(r, orthogonal_side)
            num_steps += 1
        else
            break
        end
    end
    #УТВ: Робот или уперся в угол внешней рамки поля, или готов сделать шаг (или несколько) в направлении
    # direct_side
    if isborder(r,direct_side) == false
        while isborder(r,reverse_side) == true
            move!(r,direct_side)
        end
        result = true
    else
        result = false
    end
    move!(r,reverse_side)
    return result
end
function moves!(r::Robot,side::HorizonSide)
    possible, _ = move_if_possible!(r,side)
    while possible
        possible, _ = move_if_possible!(r,side)
    end
end

function move_if_possible!(r::Robot, direct_side::HorizonSide)::Bool
    orthogonal_side = next(direct_side)
    reverse_side = inverse(orthogonal_side)
    num_steps=0
    while isborder(r, direct_side) == true
        if !isborder(r, orthogonal_side)
            move!(r, orthogonal_side)
            num_steps += 1
        else
            break
        end
    end
    #УТВ: Робот или уперся в угол внешней рамки поля, или готов сделать шаг (или несколько) в направлении
    # direct_side
    if isborder(r,direct_side) == false
        move!(r,direct_side)
        if num_steps > 0
            while isborder(r,reverse_side) == true
                move!(r,direct_side)
            end
        end
        result = true
    else
        result = false
    end

    for _ in 1:num_steps
        move!(r, reverse_side)
    end

    return result
end


using Profile, Random, Test, TrackingHeaps

function many_issorted_tests(
  Vs = [Int64, Float64, BigInt],
  Ks = [UInt16, Int64],
  Ns = [2, 3, 4, 7, 16],
  Os = [MinHeapOrder, MaxHeapOrder],
  Ss = [SafeFromYourself, NoTrainingWheels]
)
  for V in Vs, K in Ks, N in Ns, O in Os, S in Ss
    issorted_test(V, K, N, O, S)
  end
end

function issorted_test(
  ::Type{V}, ::Type{K}, N, ::Type{O}, ::Type{S}
) where {K, V, O, S}
  curr_lt(x, y) = TrackingHeaps.is_higher_than(O, x, y)
  @testset "track! and pop! can be used to (reverse) sort" begin
    mt0 = Random.MersenneTwister(0)
    if V == BigInt
      minbgv = parse(BigInt, "-10000000000000000000")
      maxbgv = parse(BigInt,  "10000000000000000000")
      values = rand(mt0, big.(minbgv:maxbgv), 1000)
    else
      values = rand(mt0, V, 1000)
    end
    heap = TrackingHeap(V; K = K, N = N, O = O, S = S)

    @test isempty(heap)

    size = 0
    for value in values
      track!(heap, value)
      size += 1
      @test length(heap) == size
    end

    sorted_values = empty(values)
    sizehint!(sorted_values, length(values))

    for i = 1:length(values)
      _, value = pop!(heap)
      size -= 1
      @test length(heap) == size
      push!(sorted_values, value)
    end

    @test isempty(heap)
    @test issorted(sorted_values; lt = curr_lt)
  end
end

# TODO: ideally we should test a method that uses other methods after
# we already tested the individual parts, but this is not done here
function runtests()
  @testset "basic example usage" begin
    heap = TrackingHeap(Float64, K = UInt8)
    @test isempty(heap)
    @test iszero(length(heap))
    @test isa(length(heap), UInt8)
    expected_first_trck = next_tracker(heap)
    first_trck = track!(heap, 50.0)
    @test first_trck == expected_first_trck
    @test !isempty(heap)

    expected_second_trck = next_tracker(heap)
    second_trck = track!(heap, 25.0)
    @test second_trck == expected_second_trck
    @test length(heap) === UInt8(2)
    @test top(heap) == Pair(second_trck, 25.0)

    update!(heap, first_trck, 10.0)
    @test heap[first_trck] == 10.0
    @test top(heap) == Pair(first_trck, 10.0)

    heap[first_trck] = 5.0
    @test heap[first_trck] == 5.0
    @test top(heap) == Pair(first_trck, 5.0)

    heap[UInt8(10)] = 1.0
    @test heap[UInt8(10)] == 1.0
    @test top(heap) == Pair(UInt8(10), 1.0)

    renew_tracker!(heap, UInt8(10), UInt8(5))
    @test heap[UInt8(5)] == 1.0
    @test top(heap) == Pair(UInt8(5), 1.0)

    renew_tracker!(heap, UInt8(5)) # new trck will be largest already used + 1
    @test heap[UInt8(11)] == 1.0
    @test top(heap) == Pair(UInt8(11), 1.0)

    empty!(heap)
    @test isempty(heap)
    @test next_tracker(heap) === UInt8(12)
    first_trck = track!(heap, 7.0)
    @test first_trck === UInt8(12)
    @test heap[first_trck] == 7.0
    @test !isempty(heap)

    empty!!(heap)
    @test isempty(heap)
    @test next_tracker(heap) === UInt8(1)
    first_trck = track!(heap, 8.0)
    @test first_trck === UInt8(1)
    @test heap[first_trck] == 8.0
    @test !isempty(heap)

    vals_to_store = (1.0, 0.5, 3.5, 11.0, 77.7)
    popped_pair, trcks = pop_once_and_track_many!(heap, vals_to_store)
    @test popped_pair == Pair(Int8(1), 8.0)
    @test top(heap) == Pair(trcks[2], 0.5)
    @test length(heap) == UInt8(5)
    @test all(x -> x in heap, map(Pair, trcks, vals_to_store))
    @test all(x -> x in keys(heap), trcks)
    @test all(x -> x in values(heap), vals_to_store)

    ex_sval = extract_and_push!(heap, trcks[2], UInt8(12) => 0.8)
    @test ex_sval == 0.5
    @test top(heap) == Pair(UInt8(12), 0.8)
    @test length(heap) == UInt8(5)
    ex_sval = extract_and_push!(heap, trcks[5], UInt8(12) => 10.0)
    @test ex_sval == 77.7
    @test top(heap) == Pair(trcks[1], 1.0)
    @test heap[UInt8(12)] == 10.0
    @test length(heap) == UInt8(4) # 4 because 77.7 and 0.8 were removed

    iterated_pairs = [Pair(k, v) for (k, v) in heap]
    @test iterated_pairs == collect(pairs(heap))
    @test eltype(keys(heap)) == keytype(heap)
    @test eltype(values(heap)) == valtype(heap)

    popped_pair = pop!(heap)
    @test popped_pair == Pair(trcks[1], 1.0)
    @test top(heap) == Pair(trcks[3], 3.5)
    @test length(heap) == UInt8(3)

    delete!(heap, trcks[4])
    @test top(heap) == Pair(trcks[3], 3.5)
    @test length(heap) == UInt8(2)

    @test firstindex(heap) == trcks[3]
    @test lastindex(heap) == UInt8(12)
    @test heap[end] == 10.0

    other_heap = TrackingHeap(
      Float64, K = UInt8, init_pairs = [
        UInt8(12) => -9.0,
        UInt8(20) => 5.0,
        UInt8(100) => 15.0
      ]
    )
    merged_heap = merge(+, heap, other_heap)
    @test length(heap) == UInt8(2)
    @test length(other_heap) == UInt8(3)
    @test top(merged_heap) == Pair(UInt8(12), 1.0)
    @test length(merged_heap) == UInt8(4)

    other_heap = TrackingHeap(UInt8, K = Int64, init_val_coll = [0x00, 0x0A])
    renew_tracker!(other_heap, 1, 20)
    renew_tracker!(other_heap, 2, 100)
    merged_heap = merge(heap, other_heap)
    @test length(heap) == UInt8(2)
    @test length(other_heap) == UInt8(2)
    @test top(merged_heap) == Pair(20, UInt8(0))
    @test length(merged_heap) == UInt8(4)

    expected_next_trck = next_tracker(merged_heap)
    expected_return = (UInt8(0), expected_next_trck)
    @test expected_return == extract_and_track!(merged_heap, 20, UInt8(1))
    @test length(merged_heap) == 4
    @test top(merged_heap) == Pair(expected_next_trck, UInt8(1))

    heap = TrackingHeap(Float64, K = UInt8, init_val_heap = Float64[200.0])
    @test (Pair(UInt8(1), 200.0), UInt8(2)) == pop_and_track!(heap, 300.0)
    @test length(heap) == UInt8(1)
    @test top(heap) == Pair(UInt8(2), 300.0)

    @test get(heap, UInt8(1), 0.0) == 0.0
    @test length(heap) == UInt8(1)
    expected_next_trck = next_tracker(heap)
    @test get!(heap, UInt8(1), 0.0) == 0.0
    @test length(heap) == UInt8(2)
    @test top(heap) == Pair(UInt8(1), 0.0)
    @test getkey(heap, UInt8(1), UInt8(10)) == UInt8(1)
    @test getkey(heap, expected_next_trck, UInt8(20)) == UInt8(20)
    @test get(heap, UInt8(2), 0.0) == 300.0
    expected_next_trck = next_tracker(heap)
    @test get!(heap, UInt8(2), 0.0) == 300.0
    @test length(heap) == UInt8(2)

    @test get(() -> error("test wrong"), heap, UInt8(2)) == 300.0
    @test get!(() -> error("test wrong"), heap, UInt8(2)) == 300.0
    @test length(heap) == UInt8(2)

    @test get(() -> 7.0, heap, UInt8(20)) == 7.0
    @test length(heap) == UInt8(2)
    expected_next_trck = next_tracker(heap)
    @test get!(() -> 7.0, heap, UInt8(20)) == 7.0
    @test length(heap) == UInt8(3)
    @test heap[UInt8(20)] == 7.0
    @test !haskey(heap, expected_next_trck)

    @test eltype(heap) == Pair{keytype(heap), valtype(heap)}
  end

  many_issorted_tests()

  @testset "pop_and_track! also works" begin
    mt0 = Random.MersenneTwister(0)
    elems = rand(mt0, 1000)
    sorted_elems = sort(elems)
    threshold = 9 * div(length(elems), 10)
    best = sorted_elems[1:threshold]
    worst = sorted_elems[(threshold+1):length(elems)]
    elems_sorted_by_heap = Vector{eltype(elems)}()
    sizehint!(elems_sorted_by_heap, length(elems))

    heap = TrackingHeap(eltype(elems))
    for b_elem in best
      track!(heap, b_elem)
    end
    for w_elem in worst
      (_, e), _ = pop_and_track!(heap, w_elem)
      push!(elems_sorted_by_heap, e)
    end
    for _ = 1:length(best)
      _, e = pop!(heap)
      push!(elems_sorted_by_heap, e)
    end
    @test isempty(heap)
    @test elems_sorted_by_heap == sorted_elems
  end
end

# ugly workaround to get better track-allocation data (yes, run tests twice)
# the first time it compiles everything needed, and mess up the memory
# allocation as it counts allocation from the compilation process, then we
# reset the counters and run it again
for trial = 1:2
  Profile.clear_malloc_data()
  runtests()
end


export MNIST

"""
The MNIST database of handwritten digits
- Authors: Yann LeCun, Corinna Cortes, Christopher J.C. Burges
- Website: http://yann.lecun.com/exdb/mnist/
MNIST is a classic image-classification dataset that is often
used in small-scale machine learning experiments. It contains
70,000 images of handwritten digits. Each observation is a 28x28
pixel gray-scale image that depicts a handwritten version of 1 of
the 10 possible digits (0-9).
# Interface
- [`MNIST.traintensor`](@ref), [`MNIST.trainlabels`](@ref), [`MNIST.traindata`](@ref)
- [`MNIST.testtensor`](@ref), [`MNIST.testlabels`](@ref), [`MNIST.testdata`](@ref)
# Utilities
- [`MNIST.download`](@ref)
- [`MNIST.convert2image`](@ref)
"""
module MNIST
    using DataDeps
    using ColorTypes
    using FixedPointNumbers

    bytes_to_type(::Type{UInt8}, A::Array{UInt8}) = A
    bytes_to_type(::Type{N0f8}, A::Array{UInt8}) = reinterpret(N0f8, A)
    bytes_to_type(::Type{T}, A::Array{UInt8}) where T<:Integer = convert(Array{T}, A)
    bytes_to_type(::Type{T}, A::Array{UInt8}) where T<:AbstractFloat = A ./ T(255)
    bytes_to_type(::Type{T}, A::Array{UInt8}) where T<:Number  = convert(Array{T}, reinterpret(N0f8, A))

    function with_accept(f, manual_overwrite)
        auto_accept = if manual_overwrite == nothing
            get(ENV, "DATADEPS_ALWAYS_ACCEPT", false)
        else
            manual_overwrite
        end
        withenv(f, "DATADEPS_ALWAYS_ACCEPT" => string(auto_accept))
    end

    function datadir(depname, dir = nothing; i_accept_the_terms_of_use = nothing)
        with_accept(i_accept_the_terms_of_use) do
            if dir == nothing
                # use DataDeps defaults
                @datadep_str depname
            else
                # use user-provided dir
                if isdir(dir)
                    dir
                else
                    DataDeps.env_bool("DATADEPS_DISABLE_DOWNLOAD") && error("DATADEPS_DISABLE_DOWNLOAD enviroment variable set. Can not trigger download.")
                    DataDeps.download(DataDeps.registry[depname], dir)
                    dir
                end
            end
        end::String
    end

    function datafile(depname, filename, dir = nothing; recurse = true, kw...)
        path = joinpath(datadir(depname, dir; kw...), filename)
        if !isfile(path)
            @warn "The file \"$path\" does not exist, even though the dataset-specific folder does. This is an unusual situation that may have been caused by a manual creation of an empty folder, or manual deletion of the given file \"$filename\"."
            if dir === nothing
                @info "Retriggering DataDeps.jl for \"$depname\""
                download_dep(depname; kw...)
            else
                @info "Retriggering DataDeps.jl for \"$depname\" to \"$dir\"."
                download_dep(depname, dir; kw...)
            end
            if recurse
                datafile(depname, filename, dir; recurse = false, kw...)
            else
                error("The file \"$path\" still does not exist. One possible explaination could be a spelling error in the name of the requested file.")
            end
        else
            path
        end::String
    end

    function download_dep(depname, dir = DataDeps.determine_save_path(depname); kw...)
        DataDeps.download(DataDeps.registry[depname], dir; kw...)
    end

    function download_docstring(modname, depname)
        """
        The corresponding resource file(s) of the dataset is/are
        expected to be located in the specified directory `dir`. If
        `dir` is omitted the directories in
        `DataDeps.default_loadpath` will be searched for an existing
        `$(depname)` subfolder. In case no such subfolder is found,
        `dir` will default to `~/.julia/datadeps/$(depname)`. In the
        case that `dir` does not yet exist, a download prompt will be
        triggered. You can also use `$(modname).download([dir])`
        explicitly for pre-downloading (or re-downloading) the
        dataset. Please take a look at the documentation of the
        package DataDeps.jl for more detail and configuration
        options.
        """
    end

    function _colorview(::Type{T}, array::AbstractArray{<:Number}) where T <: Color
        __images_supported__ ||
            error("Converting to image requires `using ImageCore`")
        ImageCore.colorview(T, array)
    end

    export

        traintensor,
        testtensor,

        trainlabels,
        testlabels,

        traindata,
        testdata,

        convert2image,

        download

    @deprecate convert2features reshape

    const DEPNAME = "MNIST"
    const TRAINIMAGES = "train-images-idx3-ubyte.gz"
    const TRAINLABELS = "train-labels-idx1-ubyte.gz"
    const TESTIMAGES  = "t10k-images-idx3-ubyte.gz"
    const TESTLABELS  = "t10k-labels-idx1-ubyte.gz"

    """
        download([dir]; [i_accept_the_terms_of_use])
    Trigger the (interactive) download of the full dataset into
    "`dir`". If no `dir` is provided the dataset will be
    downloaded into "~/.julia/datadeps/$DEPNAME".
    This function will display an interactive dialog unless
    either the keyword parameter `i_accept_the_terms_of_use` or
    the environment variable `DATADEPS_ALWAYS_ACCEPT` is set to
    `true`. Note that using the data responsibly and respecting
    copyright/terms-of-use remains your responsibility.
    """
    download(args...; kw...) = download_dep(DEPNAME, args...; kw...)

    include(joinpath("Reader","Reader.jl"))
    include("interface.jl")
    include("utils.jl")

    function __init__()
        register(DataDep(
            DEPNAME,
            """
            Dataset: THE MNIST DATABASE of handwritten digits
            Authors: Yann LeCun, Corinna Cortes, Christopher J.C. Burges
            Website: http://yann.lecun.com/exdb/mnist/
            [LeCun et al., 1998a]
                Y. LeCun, L. Bottou, Y. Bengio, and P. Haffner.
                "Gradient-based learning applied to document recognition."
                Proceedings of the IEEE, 86(11):2278-2324, November 1998
            The files are available for download at the offical
            website linked above. Note that using the data
            responsibly and respecting copyright remains your
            responsibility. The authors of MNIST aren't really
            explicit about any terms of use, so please read the
            website to make sure you want to download the
            dataset.
            """,
            "https://ossci-datasets.s3.amazonaws.com/mnist/" .* [TRAINIMAGES, TRAINLABELS, TESTIMAGES, TESTLABELS],
            "0bb1d5775d852fc5bb32c76ca15a7eb4e9a3b1514a2493f7edfcf49b639d7975",
        ))
    end
end


#/usr/bin/env julia

using Wut
using Base.Test

# Run tests

tic()

@time @test include("ScalarEncoderTests.jl")
@time @test include("RandomDistributedScalarEncoderTests.jl")

toc()


Base.inv(x::T) where T <: CasadiSymbolicObject = pycall(casadi.inv, T, x)
Base.sqrt(x::T) where T <: CasadiSymbolicObject = pycall(casadi.sqrt, T, x)
Base.sin(x::T) where T <: CasadiSymbolicObject = pycall(casadi.sin, T, x)
Base.cos(x::T) where T <: CasadiSymbolicObject = pycall(casadi.cos, T, x)
Base.vec(x::T) where T <: CasadiSymbolicObject = pycall(casadi.vec, T, x)
Base.transpose(x::T) where T <: CasadiSymbolicObject = pycall(casadi.transpose, T, x)

Base.size(x::CasadiSymbolicObject) = x.size()
Base.reshape(x::T, t::Tuple{Int, Int}) where T <: CasadiSymbolicObject = pycall(casadi.reshape, T, x, t)


export
    PopulationManager,
    release!,
    createCandidate,
    randomize!,
    chooseParentIndex,
    keepElites!,
    set!

type PopulationManager
    population::Population
    populationSize::Int
    dnaSize::Int
    best::BestEntity
end

PopulationManager(ctx, populationSize::Int, dnaSize::Int, comparer::AbstractComparer, decode::Function) =
    PopulationManager(Population(ctx, comparer, decode), populationSize, dnaSize, BestEntity(comparer))

release!(popMan::PopulationManager) = (release!(popMan.population); release!(popMan.best))

createCandidate(popMan::PopulationManager) =
    Population(popMan.population.ctx, popMan.population.comparer, popMan.population.decode)

function randomize!(popMan::PopulationManager)
    set!(popMan.best, randomize!(popMan.population, popMan.populationSize, popMan.dnaSize))
end

function chooseParentIndices(popMan::PopulationManager, stdDev::Float32)
    idx1 = chooseParentIndex(popMan, stdDev)
    idx2 = chooseParentIndex(popMan, stdDev)
    while idx1 == idx2
        idx2 = chooseParentIndex(popMan, stdDev)
    end
    idx1, idx2
end

chooseParentIndex(popMan::PopulationManager, stdDev::Float32) =
    chooseParentIndex(stdDev, length(popMan.population))

function chooseParentIndex(stdDev::Float32, size::Int)
    v = abs(randn() * stdDev)
    v = v - floor(v)
    Int(floor(v * size)) + 1
end

function keepElites!(popMan::PopulationManager, candidatePop::Population, rate::Float32)
    keep = Int(rate < 1.0 ? round(length(popMan.populationSize) * rate) : round(rate))
    for i in 1:keep
        push!(candidatePop, copy(popMan.population[i]))
    end
end

function set!(popMan::PopulationManager, candidatePop::Population, noSort = false)
    if noSort == false # sort
        update!(popMan.best, sort!(candidatePop))
    else
        bestOf = candidatePop[1]
        for entity in candidatePop.entities # TODO: support iteration in Population
            if entity != bestOf && popMan.population.comparer(entity.body, bestOf.body)
                bestOf = entity
            end
        end
        update!(popMan.best, bestOf)
    end
    release!(popMan.population)
    popMan.population = candidatePop
end


export Cell

mutable struct Cell
    color::Float64
    obj::Float64
end

function Cell(color::String, obj::String)
    Cell(color_to_float(color), type_to_float(obj))
end

WallCell = Cell("black", "wall")

function Cell()
    Cell("white", "empty")
end

function object(c::Cell)
    float_to_type(c.obj)
end

function color(c::Cell)
    float_to_color(c.color)
end


#
# This file is part of the SlowActors.jl Julia package,
# MIT license, part of https://github.com/JuliaActors
#

# This tests if actors generated with SlowActors and Actors
# can communicate.
#
using SlowActors, Test

raise(a, b) = a^b

query(act, a, b) = request!(act, a, b)

A = Actors.spawn(Func(raise))
B = spawn(Func(query, A))
@test request!(A, 2, 2) == 4
@test request!(B, 2, 2) == 4

A = spawn(Func(raise))
B = Actors.spawn(Func(query, A))
@test request!(A, 2, 2) == 4
@test request!(B, 2, 2) == 4


# Parameterize by T so that way it can be Vector{Expression} which is defined after
struct Operation <: AbstractOperation
    op::Function
    args::Vector{Expression}
end

# Recursive ==
function Base.:(==)(x::Operation,y::Operation)
    x.op == y.op && length(x.args) == length(y.args) && all(isequal.(x.args,y.args))
end
Base.:(==)(x::Operation,y::Number) = false
Base.:(==)(x::Number,y::Operation) = false
Base.:(==)(x::Operation,y::Void) = false
Base.:(==)(x::Void,y::Operation) = false
Base.:(==)(x::Variable,y::Operation) = y == Operation(identity,Expression[x])
Base.:(==)(x::Operation,y::Variable) = x == Operation(identity,Expression[y])

# Don't recurse inversion for Jacobians
Base.inv(x::Operation) = x

function Base.Expr(O::Operation)
    Expr(:call, Symbol(O.op), Expr.(O.args)...)
end

Base.show(io::IO,O::Operation) = print(io,string(Expr(O)))

# Bigger printing
# Is there a way to just not have this as the default?
function Base.parse(::Type{Operation},ex::Expr)
    f = ex.args[1]
    operands = ex.args[2:end]
    if ex.head == :call && any(x -> x isa Expr, ex.args)
        args = Expression[parse(Operation,o) for o in operands]
        parse(Operation, f, args)
    else
        parse(Operation, f, Expression[parse(Operation,o) for o in operands])
    end
end
Base.parse(::Type{Operation},x::Expression) = x
Base.parse(::Type{Operation},sym::Symbol,args) = Operation(eval(sym), args)
Base.parse(::Type{Operation},x::Union{Symbol, Number}) = x

"""
find_replace(O::Operation,x::Variable,y::Expression)

Finds the variable `x` in Operation `O` and replaces it with the Expression `y`
"""
function find_replace!(O::Operation,x::Variable,y::Expression)
    for i in eachindex(O.args)
        if isequal(O.args[i],x)
            O.args[i] = y
        elseif typeof(O.args[i]) <: Operation
            find_replace!(O.args[i],x,y)
        end
    end
end

# For inv
Base.convert(::Type{Operation},x::Int) = Operation(identity,Expression[Constant(x)])
Base.convert(::Type{Operation},x::Variable) = Operation(identity,Expression[x])


# const getf_master = Libdl.dlsym(polo_lib, :getf_master)
# const getx_master = Libdl.dlsym(polo_lib, :getx_master)

struct Master <: ParameterServer
    fval::Float64
    x::Vector{Float64}
    psopts::ParameterServerOptions

    function (::Type{Master})(; kw...)
        master =  new(0., Vector{Float64}(), ParameterServerOptions(; kw...))
    end
end

function initialize!(master::Master, x₀::AbstractVector)
    resize!(master.x,length(x₀))
    master.x .= x₀
end
getx(master::Master) = master.x

paramserver_handle(::Master) = POLO.proxgradient_master
execution_handle(::Master) = POLO.run_master
delete_handle(::Master) = POLO.delete_proxgradient_master
# getf_handle(::Master) = getf_master
# getx_handle(::Master) = getx_master


function psd_vec_to_square(v::Vector{Float64}, a::AuxiliaryData, cones::ConicSets, sqrt_2::Float64 = sqrt(2))
    # Build symmetric matrix(es) X
    @timeit "reshape1" begin
        cont = 1
        @inbounds for (idx, sdp) in enumerate(cones.sdpcone), j in 1:sdp.sq_side, i in 1:j#j:sdp.sq_side
            if i != j
                a.m[idx].data[i,j] = v[cont] / sqrt_2
            else
                a.m[idx].data[i,j] = v[cont]
            end
            # a.m[idx].data[i,j] = ifelse(i != j, v[cont] / sqrt_2, v[cont])
            cont += 1
        end
    end
    return cont
end
function psd_square_to_vec(v::Vector{Float64}, a::AuxiliaryData, cones::ConicSets, sqrt_2::Float64 = sqrt(2))
    # Build symmetric matrix(es) X
    @timeit "reshape2" begin
        cont = 1
        @inbounds for (idx, sdp) in enumerate(cones.sdpcone), j in 1:sdp.sq_side, i in 1:j#j:sdp.sq_side
            if i != j
                v[cont] = a.m[idx].data[i, j] * sqrt_2
            else
                v[cont] = a.m[idx].data[i, j]
            end
            cont += 1
        end
    end
    return cont
end

function psd_projection!(v::Vector{Float64}, a::AuxiliaryData, cones::ConicSets, opt::Options, p::Params, arc_list, iter::Int64)::Nothing

    p.min_eig, current_rank, sqrt_2 = zeros(length(cones.sdpcone)), 0, sqrt(2.)

    psd_vec_to_square(v, a, cones)

    # Project onto the p.s.d. cone
    for (idx, sdp) in enumerate(cones.sdpcone)
        p.current_rank[idx] = 0

        if sdp.sq_side == 1
            a.m[idx][1] = max(0., a.m[idx][1])
            p.min_eig[idx] = a.m[idx][1]
        elseif !opt.full_eig_decomp &&
                p.target_rank[idx] <= opt.max_target_rank_krylov_eigs &&
                sdp.sq_side > opt.min_size_krylov_eigs &&
                mod(p.iter, opt.full_eig_freq) > opt.full_eig_len # for full from time to time
            @timeit "eigs" if opt.eigsolver == 1
                arpack_eig!(arc_list[idx], a, idx, opt, p)
            else
                krylovkit_eig!(arc_list[idx], a, idx, opt, p)
            end
            if !hasconverged(arc_list[idx])
                @timeit "eigfact" full_eig!(a, idx, opt, p)
            end
        else
            @timeit "eigfact" full_eig!(a, idx, opt, p)
        end
    end

    psd_square_to_vec(v, a, cones)

    return nothing
end

function arpack_eig!(solver::EigSolverAlloc, a::AuxiliaryData, idx::Int, opt::Options, p::Params)::Nothing
    arpack_eig!(solver, a.m[idx], p.target_rank[idx], opt)
    if hasconverged(solver)
        fill!(a.m[idx].data, 0.)
        # TODO: how to measure this when convergen eigs is less than target?
        p.min_eig[idx] = minimum(arpack_getvalues(solver))
        # if solver.converged_eigs < p.target_rank[idx] && p.min_eig[idx] > 0.0
        #     p.min_eig[idx] = -Inf
        # end
        for i in 1:p.target_rank[idx]
            val = arpack_getvalues(solver)[i]
            if val > 0.
                p.current_rank[idx] += 1
                vec = view(arpack_getvectors(solver), :, i)
                LinearAlgebra.BLAS.gemm!('N', 'T', val, vec, vec, 1., a.m[idx].data)
            end
        end
    end
    return nothing
end

function krylovkit_eig!(solver::EigSolverAlloc, a::AuxiliaryData, idx::Int, opt::Options, p::Params)::Nothing
    krylovkit_eig!(solver, a.m[idx], p.target_rank[idx], opt)
    if hasconverged(solver)
        fill!(a.m[idx].data, 0.)
        # TODO: how to measure this when convergen eigs is less than target?
        # ? min_eig is just checking if the rank projection is going far enough to reach zeros and negatives
        p.min_eig[idx] = minimum(krylovkit_getvalues(solver))
        # if solver.converged_eigs < p.target_rank[idx] #&& p.min_eig[idx] > 0.0
        #     p.min_eig[idx] = -Inf
        # end
        for i in 1:min(p.target_rank[idx], solver.converged_eigs)
            val = krylovkit_getvalues(solver)[i]
            if val > 0.
                p.current_rank[idx] += 1
                vec = krylovkit_getvector(solver, i)
                LinearAlgebra.BLAS.gemm!('N', 'T', val, vec, vec, 1., a.m[idx].data)
            end
        end
    end
    return nothing
end

function full_eig!(a::AuxiliaryData, idx::Int, opt::Options, p::Params)::Nothing
    p.current_rank[idx] = 0
    fact = eigen!(a.m[idx])
    p.min_eig[idx] = 0.0 #minimum(fact.values)
    fill!(a.m[idx].data, 0.)
    for i in 1:length(fact.values)
        if fact.values[i] > 0.
            v = view(fact.vectors, :, i)
            LinearAlgebra.BLAS.gemm!('N', 'T', fact.values[i], v, v, 1., a.m[idx].data)
            if fact.values[i] > opt.tol_psd
                p.current_rank[idx] += 1
            end
        end
    end
    return nothing
end

function min_eig(a::AuxiliaryData, idx::Int, p::Params)
    # if p.min_eig[idx] == -Inf
    #     @timeit "bad min eig" begin
    #         fact = eigen!(a.m[idx])
    #         p.min_eig[idx] = minimum(fact.values)
    #     end
    # end
    return p.min_eig[idx]
end

function soc_projection!(v::Vector{Float64}, a::AuxiliaryData, cones::ConicSets, opt::Options, p::Params)::Nothing
    for (idx, soc) in enumerate(cones.socone)
        soc_projection!(a.soc_v[idx], a.soc_s[idx])
    end
    return nothing
end

function soc_projection!(v::ViewVector, s::ViewScalar)::Nothing
    nv = norm(v, 2)
    if nv <= -s[]
        s[] = 0.
        v .= 0.
    elseif nv <= s[]
        #do nothing
    else
        val = .5 * (1. + s[] / nv)
        v .*= val
        s[] = val * nv
    end
    return nothing
end

function box_projection!(v::Array{Float64,1}, aff::AffineSets, step::Float64)::Nothing
    # Projection onto = b
    @simd for i in 1:length(aff.b)
        @inbounds v[i] = aff.b[i]
    end
    # Projection onto <= h
    @simd for i in 1:length(aff.h)
        @inbounds v[aff.p+i] = min(v[aff.p+i] / step, aff.h[i])
    end
    return nothing
end


# GPU - apply along the features axis
function hist_kernel!(h::CuDeviceArray{T,3}, x::CuDeviceMatrix{T}, id, 𝑖, 𝑗, K) where {T<:AbstractFloat}
    i = threadIdx().x + (blockIdx().x - 1) * blockDim().x
    j = threadIdx().y + (blockIdx().y - 1) * blockDim().y
    if i <= length(𝑖) && j <= length(𝑗)
        for k in 1:K
            @inbounds pt = Base._to_linear_index(h, id[𝑖[i], 𝑗[j]], k, 𝑗[j])
            @inbounds CUDA.atomic_add!(pointer(h, pt), x[𝑖[i],k])
        end
    end
    return
end

# for 2D input: 𝑤
function hist_kernel!(h::CuDeviceMatrix{T}, x::CuDeviceVector{T}, id, 𝑖, 𝑗) where {T<:AbstractFloat}
    i = threadIdx().x + (blockIdx().x - 1) * blockDim().x
    j = threadIdx().y + (blockIdx().y - 1) * blockDim().y
    if i <= length(𝑖) && j <= length(𝑗)
        @inbounds pt = Base._to_linear_index(h, id[𝑖[i], 𝑗[j]], 𝑗[j])
        @inbounds CUDA.atomic_add!(pointer(h, pt), x[𝑖[i]])
    end
    return
end

# base approach - block built along the cols first, the rows (limit collisions)
function update_hist_gpu!(hδ::CuArray{T,3}, hδ²::CuArray{T,3}, h𝑤::CuMatrix{T},
    δ::CuMatrix{T}, δ²::CuMatrix{T}, 𝑤::CuVector{T},
    X_bin::CuMatrix{Int}, 𝑖::CuVector{Int}, 𝑗::CuVector{Int}, K; MAX_THREADS=1024) where {T<:AbstractFloat}

    hδ .*= 0.0
    hδ² .*= 0.0
    h𝑤 .*= 0.0

    thread_j = min(MAX_THREADS, length(𝑗))
    thread_i = min(MAX_THREADS ÷ thread_j, length(𝑖))
    threads = (thread_i, thread_j)
    blocks = ceil.(Int, (length(𝑖), length(𝑗)) ./ threads)
    @cuda blocks=blocks threads=threads hist_kernel!(hδ, δ, X_bin, 𝑖, 𝑗, K)
    @cuda blocks=blocks threads=threads hist_kernel!(hδ², δ², X_bin, 𝑖, 𝑗, K)
    @cuda blocks=blocks threads=threads hist_kernel!(h𝑤, 𝑤, X_bin, 𝑖, 𝑗)

    return
end

function find_split_gpu!(hist_δ::AbstractMatrix{T}, hist_δ²::AbstractMatrix{T}, hist_𝑤::AbstractVector{T},
    params::EvoTypes, node::TrainNode_gpu{T,S}, info::SplitInfo_gpu{T,S}, edges::Vector{T}) where {T,S}

    # initialize tracking
    ∑δL = copy(node.∑δ) .* 0
    ∑δ²L = copy(node.∑δ²) .* 0
    ∑𝑤L = node.∑𝑤 * 0
    ∑δR = copy(node.∑δ)
    ∑δ²R = copy(node.∑δ²)
    ∑𝑤R = node.∑𝑤

    # println("∑δ²L: ", ∑δ²L, " ∑δ²R:", ∑δ²R)
    # println("hist_δ²: ", hist_δ²)

    @inbounds for bin in 1:(length(hist_δ)-1)
        @views ∑δL .+= hist_δ[bin,:]
        @views ∑δ²L .+= hist_δ²[bin,:]
        ∑𝑤L += hist_𝑤[bin]
        @views ∑δR .-= hist_δ[bin,:]
        @views ∑δ²R .-= hist_δ²[bin,:]
        ∑𝑤R -= hist_𝑤[bin]

        # println("∑δ²L: ", ∑δ²L, " | ∑δ²R:", ∑δ²R, " | hist_δ²[bin,:]: ", hist_δ²[bin,:])

        gainL, gainR = get_gain(params.loss, ∑δL, ∑δ²L, ∑𝑤L, params.λ), get_gain(params.loss, ∑δR, ∑δ²R, ∑𝑤R, params.λ)
        gain = gainL + gainR

        if gain > info.gain && ∑𝑤L >= params.min_weight + 1e-12 && ∑𝑤R >= params.min_weight + 1e-12
            info.gain = gain
            info.gainL = gainL
            info.gainR = gainR
            @views info.∑δL .= ∑δL
            @views info.∑δ²L .= ∑δ²L
            info.∑𝑤L = ∑𝑤L
            @views info.∑δR .= ∑δR
            @views info.∑δ²R .= ∑δ²R
            info.∑𝑤R = ∑𝑤R
            info.cond = edges[bin]
            info.𝑖 = bin
        end # info update if gain
    end # loop on bins
end


export TimeSolver

abstract type TimeSolver end

show(io::IO, s::TimeSolver) =
    try print(io,s.info)
    catch
        print(io,"Time solver: $(s.label)")
    end

Base.:(==)(a::TimeSolver, b::TimeSolver) = begin

    init = Random((N=10, L=10))

    Na = try size(a.U1,1)
    catch
        2
    end
    ma = Airy((μ=1, N=Na, L=10))
    Ua = ma.mapto(init)

    Nb = try size(b.U1,1)
    catch
        Complex.(rand(2,2))
    end
    mb = Airy((μ=1, N=Nb, L=10))
    Ub = mb.mapto(init)

    step!(a, ma, Ua, 1)
    step!(b, mb, Ub, 1)

    a.label == b.label && Ua == Ub

end


module bfga

    import Base.isless

    include("Types.jl")
    using .Types

    #using GeneticAlgorithms
    #include("GeneticAlgorithms.jl")
    #using .GeneticAlgorithms

    include("Bf.jl")
    import .BfInterpreter

    mutable struct bfMonster <: Entity
        dna::Array
        fitness
        bonus :: Float64
        age :: Int
        m_length :: Int
        program :: String

        # = new(create_entity(1, nbGenes).dna, nothing, 1, nbGenes)
        bfMonster(dna :: Array) = new(dna, 0, 0,  1, length(dna), join(genesToBfInstructions(dna), ""))
        bfMonster(dna :: Array, age :: Int) = new(dna, 0, 0, age, length(dna), join(genesToBfInstructions(dna), ""))
        #bfMonster(age :: Int) = create_entity(age,nbGenes)
    end

    #nbGenes = 150

    function isless(lhs::bfMonster, rhs::bfMonster)
        (lhs.fitness, rhs.bonus) < (rhs.fitness , lhs.bonus) # abs(lhs.fitness) > abs(rhs.fitness)
    end

    function create_entity(num, nbgenes)
        babybf = [rand() for i in 1:nbgenes]
        bfMonster(babybf, num)
    end

    #Constants
global instructions = ['<', '>', '+', '-', '.', ',', '[', ']'] #, '$', '!', '*']

    global instructions2 = ['<', '>', '+', '-', '.', ',', '[', ']','$', '!', '*' , '#' , '@', '/' , '%']

    function floatToBfInstruction( number ::Float64 )
        @assert (number >= 0.0 && number <= 1.0)
        memoire = number*length(instructions)
        ent = trunc(Int, memoire)
        if ent == memoire
            return instructions[ent]
        else
            return instructions[ent+1]
        end
    end

    function entityToBfInstructions!( ent )
        ent.program = join(genesToBfInstructions(ent.dna), "")
    end

    function genesToBfInstructions( genes :: Array{Float64,1} )
        instructions = []
        for num in genes
            push!(instructions,floatToBfInstruction(num))
        end
        instructions
    end

    function getBfCode(ent)
        ent.program
    end

    #=function group_entities!(pop)
        println("BEST: ", pop[1].fitness)
        @show pop[1].dna
        println("SECOND: ", pop[2].fitness)

        if pop[1].fitness == 0
            return
        end

        liste = []
        # simple naive groupings that pair the best entitiy with every other
        for i in 1:length(pop)
            #produce([1, i])
            prems = 1
            if rand() > 0.3
                prems = rand(2:10)
                #=if prems < 0
                    prems *= -1
                end
                prems = (prems % length(pop)) +1 =#
            end
            push!(liste , [prems,i])
        end
        liste
    end=#

    function getInstructionsSet()
        mem = BfInterpreter.getInstructionsDict()
        @show mem
        mem
    end

    function crossover(group)
        parent1 = group[1]
        parent2 = group[2]
        n1 = length(parent1.dna)
        n2 = length(parent2.dna)
        pos = trunc(Int, rand() * n1)
        minN = min(n1,n2)

        child1 = create_entity(parent1.age, minN)
        child2 = create_entity(parent1.age, minN)
        child1.age = parent1.age + 1
        child2.age = parent1.age + 1

        for i in 1:minN
            if i < pos
                child1.dna[i] = parent2.dna[i]
                child2.dna[i] = parent1.dna[i]
            else
                child2.dna[i] = parent2.dna[i]
                child1.dna[i] = parent1.dna[i]
            end
        end
        mutate(child1)
        mutate(child2)
        #clearCode!(child1)
        #clearCode!(child2)
        child1.m_length = minN #length(child1.dna)
        child2.m_length = minN #length(child2.dna)
        child1, child2
    end

    function mutate(ent)
        m_length = length(ent.dna)
        # Go through each bit.
        for pos in 1:m_length

            # Should this bit mutate?
            if (rand() < 0.06)
                # Select a mutation type.
                r = rand()
                if (r <= 0.25)
                    # Insertion mutation.
                    # Get shift index.
                    mutationIndex = pos;

                    # Make a copy of the current bit before we mutate it.
                    shiftBit = ent.dna[mutationIndex];

                    # Set random bit at mutation index.
                    ent.dna[mutationIndex] = rand();

                    # Bump bits up or down by 1.
                    up = rand() >= 0.5;
                    if up
                        # Bump bits up by 1.
                        for i in ((mutationIndex + 1 ):m_length)
                            nextShiftBit = ent.dna[i];

                            ent.dna[i] = shiftBit;

                            shiftBit = nextShiftBit;
                        end
                    else
                        # Bump bits down by 1.
                        for i in mutationIndex:-1:1
                            nextShiftBit = ent.dna[i];

                            ent.dna[i] = shiftBit;

                            shiftBit = nextShiftBit;
                        end
                    end
                elseif (r <= 0.5)
                    # Deletion mutation.
                    # Get deletion index.
                    mutationIndex = pos;

                    # Bump bits up or down by 1.
                    up = rand() >= 0.5;
                    if (up)
                        # Bump bits up by 1.
                        for i in mutationIndex:-1:2
                            ent.dna[i] = ent.dna[i - 1];
                        end

                        # Add a new mutation bit at front of genome to replace the deleted one.
                        ent.dna[1] = rand();
                    else
                        # Bump bits down by 1.
                        for i in mutationIndex:(m_length - 1)
                            ent.dna[i] = ent.dna[i + 1];
                        end

                        # Add a new mutation bit at end of genome to replace the deleted one.
                        ent.dna[m_length] = rand();
                    end
                elseif (r <= 0.75)
                    # Shift/rotation mutation.
                    # Bump bits up or down by 1.
                    up = rand() >= 0.5;
                    if (up)
                        # Bump bits up by 1. 1, 2, 3 => 3, 1, 2
                        shiftBit = ent.dna[1];

                        for i in 1:m_length
                            if (i > 1)
                                # Make a copy of the current bit.
                                temp = ent.dna[i];

                                # Set the current bit to the previous one.
                                ent.dna[i] = shiftBit;

                                # Select the next bit to be copied.
                                shiftBit = temp;
                            else
                                # Wrap last bit to front.
                                ent.dna[i] = ent.dna[m_length];
                            end
                        end
                    else
                        # Bump bits down by 1. 1, 2, 3 => 2, 3, 1
                        shiftBit = ent.dna[m_length ];

                        for i in m_length:-1:1
                            if (i < m_length)
                                # Make a copy of the current bit.
                                temp = ent.dna[i];

                                # Set the current bit to the previous one.
                                ent.dna[i] = shiftBit;

                                # Select the next bit to be copied.
                                shiftBit = temp;
                            else
                                # Wrap first bit to end.
                                ent.dna[i] = ent.dna[1];
                            end
                        end
                    end
                else
                    # Replacement mutation.
                    # Mutate bits.
                    mutation = rand();
                    ent.dna[pos] = mutation;
                end
            end
        end
    end

    function expand(ent , size :: Int)
        #println("EXPAND CAAALLLED")
        originalSize = length(ent.dna);
        difference = size - originalSize;

        # Resize the genome array.
        newGenes = []

        if (difference > 0)
            if (rand() < 0.5)
                # Extend at front.
                newGenes = [ [rand() for i in 1:difference]; deepcopy(ent.dna) ]
            else
                # Extend at back.
                newGenes = [ deepcopy(ent.dna); [rand() for i in 1:difference] ]
            end
            ent.dna = newGenes;
        else
            ent.dna = ent.dna[1:size]
            #Array.Resize(ref ent.dna, size);
        end
        ent.m_length = size;
    end

    function clearCode!(ent)
        n = ent.m_length
        bfCodeArray = [] #Array{Float64,1}
        #age = ent.age

        #=bfcode = join((genesToBfInstructions(bfgenes)), "")
        replace(bfcode, r"[^\+\-\<\>\.\,\[\]]" => s"") =#
        i= 1
        while i < n
            if floatToBfInstruction(ent.dna[i]) == '[' && floatToBfInstruction(ent.dna[i+1]) == ']'
                i += 1
            else
                append!(bfCodeArray, [ent.dna[i] ])
            end
            i+=1
        end
        # has to be changed, ugly convertion
        ent.dna = [ i::Float64 for i in bfCodeArray] :: Array{Float64,1}
        ent.m_length = length(bfCodeArray)
    end

end


# The license for this code is available at https://github.com/avik-pal/FastStyleTransfer.jl/blob/master/LICENSE.md

#------------------------Utilities to Train new models----------------------------

"""
Train a model for style transfer.
"""
function train(train_data_path, batch_size, η, style_image_path, epochs, model_save_path, content_weight, style_weight, model = TransformerNet; images = 10000)
    train_dataset = load_dataset(train_data_path, batch_size, images)
    @info "Loading initial model weights"
    try
        BSON.@load model_save_path transformer
    catch
        transformer = model()
    end
    @info "Model weights to GPU"
    transformer = transformer |> gpu
    optimizer = Flux.ADAM(η)
    @info "Loading style image"
    style = load_image(style_image_path, size_img = 224)
    @info "Style image to GPU"
    style = repeat(reshape(style, size(style)..., 1), outer = (1,1,1,batch_size)) |> gpu
    im_mean2 = reshape([0.485, 0.458, 0.408], (1,1,3,1)) * 255 |> gpu # Reinitialize to avoid gpu error

    @info "Model to GPU"
    vgg = vgg19() |> gpu
    features_style = vgg(style)
    @info "Calculating gram matrix"
    gram_style = [gram_matrix(y) for y in features_style]

    @info "Defining loss function"
    function loss_function(x)
        y = transformer(x)

        y = y .- im_mean2 # No need to do for x as it is already normalized

        features_y = vgg(y)
        features_x = vgg(x)

        # TODO: Train models for other depths by changing the index number
        content_loss = content_weight * Flux.mse(features_y[2], features_x[2])

        style_loss = 0.0
        for i in 1:size(features_style, 1)
            gram_sty = gram_style[i]
            gram_y = gram_matrix(features_y[i])
            style_loss = style_loss + Flux.mse(gram_y, gram_sty)
        end
        style_loss = style_loss * style_weight

        total_loss = content_loss + style_loss
        @info "Content Loss : $(content_loss) || Style Loss : $(style_loss) || Total Loss : $(total_loss)"

        total_loss
    end
    #=
    for (x,y) in train_data
  gs = Flux.gradient(ps) do
    loss(x,y)
  end
  Flux.Optimise.update!(opt, ps, gs)

    =#
    @info "Beginning training cycle"
    ecount = 1
    Flux.@epochs epochs begin
        @info "Epoch $ecount..."
        dcount = 1
        for d in train_dataset
            @info "Data section $dcount..."
            size(d, 4) != batch_size && continue
            gs = Flux.gradient(params(transformer)) do
                loss_function(d |> gpu)
            end
            Flux.Optimise.update!(optimizer, params(model), gs)
        end

        transformer = transformer |> cpu
        BSON.@save model_save_path transformer
        ecount += 1
    end
end

#----------------------------------Utilities to Stylize Images--------------------------------

"""
Stylize an image based on a style model.
"""
function stylize(image_path, model_path = "../models/trained_network_1.bson"; save_path = "", display_img::Bool = true)
    info("Starting to Load Model")
    BSON.@load model_path transformer
    transformer = transformer |> gpu
    Flux.testmode!(transformer)
    @info "Model has been Loaded Successfully"
    img = load_image(image_path)
    img = reshape(img, size(img)..., 1) |> gpu
    @info "Image Loaded Successfully"
    a = time()
    stylized_img = transformer(img)
    @info "Image has been Stylized in $(time()-a) seconds"
    if save_path == ""
        path = rsplit(image_path, ".", limit = 2)
        save_path = "$(path[1])_stylized.$(path[2])"
    end
    stylized_img = stylized_img |> cpu
    save_image(save_path, data(stylized_img), display_img)
    @info "The image has been saved successfully"
end


using HDF5,JLD,KUparser,KUnet

@date net = loadnet("foo11nnet3.jld")
#@date d = load("acl11.trn.jld4")
@date d = load("acl11.dev.jld4")
@date co = d["corpus"][1:100]
@show (ndeps,nbeam,nbatch,ncpu) = (length(d["deprel"]),64,5,nworkers())
@show ft = Flist.zn11cpv
@show pt = ArcEager13

# @date Main.restartmachines()
# @date Main.restartcuda()
# @everywhere using KUparser

@show (pt, length(co), ndeps, length(ft), length(net), nbeam, nbatch, ncpu)
@date p1 = bparse(pt, co, ndeps, ft, net, nbeam, nbatch, ncpu; xy=true)

eqparse(a,b)=(isequal(a[1],b[1]) && isequal(sortcols(vcat(a[2],a[3])), sortcols(vcat(b[2],b[3]))))
@date p2 = bparse(pt, co, ndeps, ft, net, nbeam, nbatch; xy=true)
@show eqparse(p1,p2)

# @date (p0,x0,y0) = bparse(pt, corpus[1:10], ndeps, ft, net, nbeam; xy=true)
# @date (p1,x1,y1) = bparse(pt, corpus, ndeps, ft, net, nbeam; xy=true)
# @date (p2,x2,y2) = bparse(pt, corpus, ndeps, ft, net, nbeam, nbatch; xy=true)
# @date (p3,x3,y3) = bparse(pt, corpus, ndeps, ft, net, nbeam, nbatch, ncpu; xy=true)

# @date bparse(pt, co, ndeps, ft, net, nbeam, 128; xy=true)
# @date bparse(pt, co, ndeps, ft, net, nbeam, 64; xy=true)
# @date bparse(pt, co, ndeps, ft, net, nbeam, 32; xy=true)
# @date bparse(pt, co, ndeps, ft, net, nbeam, 16; xy=true)
# @date bparse(pt, co, ndeps, ft, net, nbeam, 8; xy=true)
# @date bparse(pt, co, ndeps, ft, net, nbeam, 4; xy=true)
# @date bparse(pt, co, ndeps, ft, net, nbeam, 2; xy=true)
# @date bparse(pt, co, ndeps, ft, net, nbeam; xy=true)

:ok


export Loss, Context, getContext, setContext, resetContext, hasgpu, is_on_gpu, KorA

const ϵ = 10e-8

"""
A meter is reponsible for presenting metric values. This can be
printing it to the console output, showing it on TensorBoard of storing it
in a database.
"""
abstract type Meter end

abstract type MetricStore end
abstract type Layer end
abstract type Metric end
abstract type Optimizer end

"""
Abstract type for the loss functions. However Photon accepts
any function as a loss function as long as it is callable and
returns the loss value as a scalar type.
"""
abstract type Loss end

const Shape = Tuple{Vararg{Int}}

const Tensor{T,N} = Union{
						AbstractArray{T,N},
						Knet.KnetArray{T,N},
						Knet.Value{Array{T,N}},
						Knet.Value{Knet.KnetArray{T,N}}}

const Tensors = Union{Tensor, Tuple{Tensor}}

"""
Context is used by various parts of Photon to determine what the device and
datatype should be for data. It allows for quickly switches between GPU and CPU
based environments.

# Attributes
- device::Symbol the type of device. For now supported :cpu and :gpu
- deviceId::Int the id of the device, useful for example if you multiple GPU's
- dtype::Type the type of data you want to use. Most common are Float32, Float16 or Float64.
"""
mutable struct Context
	device::Symbol
	deviceId::Int
	dtype::Type

	function Context()
		device = Knet.gpu() >= 0 ? :gpu : :cpu
		new(device, 0, Float32)
	end
end

global ctx = Context()

function is_on_gpu()::Bool
	ctx.device == :gpu
end

hasgpu() = Knet.gpu() >= 0

function getContext()::Context
  ctx
end

function setContext(;device=ctx.device, deviceId=ctx.deviceId, dtype=ctx.dtype)
  ctx.device = device
  ctx.deviceId = deviceId
  ctx.dtype= dtype
  getContext()
end

function resetContext()
	global ctx
	ctx = Context()
end

"""
KorA makes it easy to move an array to the GPU or the other way around
"""
KorA(arr::Array) = (ctx.device == :gpu) ? Knet.KnetArray(arr) : arr
KorA(arr::Knet.KnetArray)= (ctx.device == :cpu) ? Array(arr) : arr
KorA(arr::Tuple)= (KorA(elem) for elem in arr)

# TODO: Make more generic
toFloat32(arr::Array) = convert(Array{Float32}, arr)
toFloat32(arr::Knet.KnetArray) = convert(Knet.KnetArray{Float32}, arr)
toFloat32(arr::Tuple) = (toFloat32(elem) for elem in arr)

addlast(x) = reshape(x, (size(x)...,1))
droplast(x) = reshape(x, (size(x)[1:end-1]...))

"""
autoConvertor converts data to the right format for a model.
It uses the context to determine the device (cpu or gpu) and datatype
that the data needs to be.

It supports Tuples, Arrays and KnetArrays and a combination of those.
"""
function autoConvertor(arr::Array)
	arr = convert(Array{ctx.dtype},arr)
	ctx.device == :gpu ? Knet.KnetArray(arr) : arr
end

function autoConvertor(arr::Knet.KnetArray)
	arr = convert(Knet.KnetArray{ctx.dtype}, arr)
	ctx.device == :gpu ? arr : Array(arr)
end

autoConvertor(arr::Tuple)= (autoConvertor(elem) for elem in arr)

# small util
makeArray(x::AbstractArray) = x
makeArray(x) = Vector(x)


"""
    shiftlocus(locus::Locus, x)

Shift the index of `x` from the current locus to the new locus.

We only shift `Samped`, `Regular` or `Explicit`, `Intervals`.
"""
function shiftlocus(locus::Locus, lookup::LookupArray)
    samp = sampling(lookup)
    samp isa Intervals || error("Cannot shift locus of $(nameof(typeof(samp)))")
    newindex = _shiftindexlocus(locus, lookup)
    newlookup = rebuild(lookup; data=newindex)
    return set(newlookup, locus)
end

# Fallback - no shifting
_shiftindexlocus(locus::Locus, lookup::LookupArray) = index(lookup)
# Sampled
function _shiftindexlocus(locus::Locus, lookup::AbstractSampled)
    _shiftindexlocus(locus, span(lookup), sampling(lookup), lookup)
end
# TODO:
_shiftindexlocus(locus::Locus, span::Irregular, sampling::Sampling, lookup::LookupArray) = index(lookup)
# Sampled Regular
function _shiftindexlocus(destlocus::Center, span::Regular, sampling::Intervals, dim::LookupArray)
    if destlocus === locus(sampling)
        return index(dim)
    else
        offset = _offset(locus(sampling), destlocus)
        shift = ((index(dim) .+ abs(step(span))) .- index(dim)) .* offset
        return index(dim) .+ shift
    end
end
function _shiftindexlocus(destlocus::Locus, span::Regular, sampling::Intervals, lookup::LookupArray)
    index(lookup) .+ (abs(step(span)) * _offset(locus(sampling), destlocus))
end
# Sampled Explicit
_shiftindexlocus(::Start, span::Explicit, sampling::Intervals, lookup::LookupArray) = val(span)[1, :]
_shiftindexlocus(::End, span::Explicit, sampling::Intervals, lookup::LookupArray) = val(span)[2, :]
function _shiftindexlocus(destlocus::Center, span::Explicit, sampling::Intervals, lookup::LookupArray)
    _shiftindexlocus(destlocus, locus(lookup), span, sampling, lookup)
end
_shiftindexlocus(::Center, ::Center, span::Explicit, sampling::Intervals, lookup::LookupArray) = index(lookup)
function _shiftindexlocus(::Center, ::Locus, span::Explicit, sampling::Intervals, lookup::LookupArray)
    # A little complicated so that DateTime works
    (view(val(span), 2, :)  .- view(val(span), 1, :)) ./ 2 .+ view(val(span), 1, :)
end

_offset(::Start, ::Center) = 0.5
_offset(::Start, ::End) = 1
_offset(::Center, ::Start) = -0.5
_offset(::Center, ::End) = 0.5
_offset(::End, ::Start) = -1
_offset(::End, ::Center) = -0.5
_offset(::T, ::T) where T<:Locus = 0

maybeshiftlocus(locus::Locus, l::LookupArray) = _maybeshiftlocus(locus, sampling(l), l)

_maybeshiftlocus(locus::Locus, sampling::Intervals, l::LookupArray) = shiftlocus(locus, l)
_maybeshiftlocus(locus::Locus, sampling::Sampling, l::LookupArray) = l

"""
    basetypeof(x) => Type

Get the "base" type of an object - the minimum required to
define the object without it's fields. By default this is the full
`UnionAll` for the type. But custom `basetypeof` methods can be
defined for types with free type parameters.

In DimensionalData this is primariliy used for comparing `Dimension`s,
where `Dim{:x}` is different from `Dim{:y}`.
"""
@inline basetypeof(x::T) where T = basetypeof(T)
@generated function basetypeof(::Type{T}) where T
    if T isa Union
        T
    else
        getfield(parentmodule(T), nameof(T))
    end
end

# unwrap
# Unwrap Val and Type{Val}, or return unchanged
unwrap(::Val{X}) where X = X
unwrap(::Type{Val{X}}) where X = X
unwrap(x) = x

# orderof
# Detect the order of an abstract array
orderof(A::AbstractUnitRange) = ForwardOrdered()
orderof(A::AbstractRange) = _order(A)
function orderof(A::AbstractArray)
    local sorted, indord
    # This is awful. But we don't know if we can
    # call `issorted` on the contents of `A`.
    # This may be resolved by: https://github.com/JuliaLang/julia/pull/37239
    try
        indord = _order(A)
        sorted = issorted(A; rev=LookupArrays.isrev(indord))
    catch
        sorted = false
    end
    return sorted ? indord : Unordered()
end

_order(A) = first(A) <= last(A) ? ForwardOrdered() : ReverseOrdered()


"""
    Molecule

Photophysical properties of a molecule.

This is the most general type of luminecent or scattering single molecule.
Inherited types will defines the properties of a class of molecules.

"""
abstract type Molecule end

"""
    GenericFluor

Defines a fluorophore

# Fields
- γ: photon emission rate in Hz, Default: 1e3
- q: state transision matrix. Default: q=[1.0]
"""
mutable struct GenericFluor <: Molecule
    γ::AbstractFloat
    q::Array{<:AbstractFloat}
end
function GenericFluor(;
    γ::AbstractFloat=1e5,
    q::Array{<:AbstractFloat}=[1]
    )
    return GenericFluor(γ,q)
end


using JuLIP, ASE, StaticArrays, NeighbourLists
using Test

X_Ti = vecs([0.0 5.19374 2.59687 3.8953 1.29843 6.49217 7.7906 12.9843 10.3875 11.6859 9.08904 14.2828; 0.0 0.918131 1.83626 -1.11022e-16 0.918131 1.83626 -2.22045e-16 0.918131 1.83626 0.0 0.918131 1.83626; 0.0 0.0 0.0 2.24895 2.24895 2.24895 0.0 0.0 0.0 2.24895 2.24895 2.24895])
C_Ti = (@SMatrix [15.5812 2.47895 0.0; 0.0 2.75439 0.0; 0.0 0.0 4.49791])

# -------------- MatSciPy NeighbourList Patch -------------
using PyCall
import NeighbourLists
matscipy_neighbours = pyimport("matscipy.neighbours")
function asenlist(at::Atoms, rcut)
   pyat = ASEAtoms(at).po
   return matscipy_neighbours[:neighbour_list]("ijdD", pyat, rcut)
end

function matscipy_nlist(at::Atoms{T}, rcut::T; recompute=false, kwargs...) where T <: AbstractFloat
   i, j, r, R = asenlist(at, rcut)
   i = copy(i) .+ 1
   j = copy(j) .+ 1
   r = copy(r)
   R = collect(vecs(copy(R')))
   first = NeighbourLists.get_first(i, length(at))
   NeighbourLists.sort_neigs!(j, r, R, first)
   return NeighbourLists.PairList(positions(at), rcut, i, j, r, R, first)
end
# --------------------------------------------------------

pynlist(at, cutoff) = matscipy_nlist(at, cutoff)
jnlist(at, cutoff) = PairList(positions(at), cutoff, cell(at), pbc(at))

function test_nlist_julip(at, cutoff)
   nlist = jnlist(at, cutoff)
   py_nlist = pynlist(at, cutoff)
   return ( (nlist.i == py_nlist.i) &&
            (nlist.j == py_nlist.j) &&
            (nlist.r ≈ py_nlist.r) &&
            (nlist.R ≈ py_nlist.R) )
end

test_configs = [
   #
   ( "si, cubic, cluster, short",
    set_pbc!(bulk(:Si, cubic=true) * 3, false),
    1.1 * rnn(:Si) ),
   #
   ( "si, cubic, cluster, med",
    set_pbc!(bulk(:Si, cubic=true) * 3, false),
    2.1 * rnn(:Si) ),
   #
   ( "si, non-cubic cell, cluster, med",
    set_pbc!(bulk(:Si) * 5, false),
    2.1 * rnn(:Si) ),
   #
   ( "si, non-cubic cell, pbc",
    set_pbc!(bulk(:Si) * 5, false),
    2.1 * rnn(:Si) ),
   #
   ( "si, non-cubic cell, mixed bc",
    set_pbc!(bulk(:Si) * 5, false),
    2.1 * rnn(:Si) ),
    #
   ("Ti, non-symmetric elongated cell, pbc",
    set_pbc!( set_cell!( Atoms(:Ti, X_Ti), C_Ti ), true ),
    1.45 * rnn(:Ti) ),
   #
   ("Ti (hcp?) canonical cell, pbc",
    set_pbc!( bulk(:Ti) * 4, true ),
    2.3 * rnn(:Ti) ),
   ]

# ----------- A FEW MORE COMPLEX TESTS THAT FAILED AT SOME POINT ---------------

# [1] a left-handed cell orientation
#     the test that failed during Cas' experiments

X = [ 0.00000000e+00  0.00000000e+00  0.00000000e+00
      1.92333044e+00  6.63816518e-17 -1.36000000e+00
      1.92333044e+00  1.92333044e+00 -2.72000000e+00
      3.84666089e+00  1.92333044e+00 -4.08000000e+00 ]'
C = [ 3.84666089   0.           0.
      0.           3.84666089   0.
      0.           0.          -5.44 ]'

# X = [ 0.00000000e+00  0.00000000e+00  0.00000000e+00
#       1.92333044e+00  6.63816518e-17  1.36000000e+00
#       1.92333044e+00  1.92333044e+00  2.72000000e+00
#       3.84666089e+00  1.92333044e+00  4.08000000e+00 ]'
# C = [ 3.84666089   0.           0.
#       0.           3.84666089   0.
#       0.           0.           5.44 ]'
at = Atoms(:Si, collect(vecs(X)))
set_cell!(at, C)
set_pbc!(at, (true,true,true))
atlge = at * (1,1,10)
rcut = 2.3*rnn(:Si)
push!(test_configs, ("Si left-oriented", at, rcut))
push!(test_configs, ("Si left-oriented, large", atlge, rcut))

test_nlist_julip(at, rcut)
test_nlist_julip(atlge, rcut)

# # [2] vacancy in bulk Si
#
# using PyCall
# at = bulk(:Si, cubic=true)
# @pyimport ase.lattice.cubic as cubic
# at2py = cubic.Diamond(symbol = "Si", latticeconstant = 5.43)
# at2py[:get_positions]()
# @test at2py[:get_cell]() ≈ cell(at1)
# @test mat(positions(at1))' ≈ at2py[:get_positions]()[:,[3,2,1]]
#
# at = bulk(:Si, cubic=true)
# at1 = at * 3
# X = mat(positions(at1))
# at1 = deleteat!(at1, 1)
# at2 = deleteat!(set_positions!(at * 3, X[[3,2,1],:]), 1)
# rcut = 2.3 * rnn(:Si)
# test_nlist_julip(at1, rcut)
# test_nlist_julip(at2, rcut)

# [3] Two Ti configuration that seems to be causing problems
C1 = @SMatrix [5.71757 -1.81834e-15 9.74255e-41; -2.85879 4.95156 4.93924e-25; 4.56368e-40 9.05692e-25 9.05629]
X1 = vecs([0.00533847 2.85879 -1.42939 1.42939 0.0 2.85879 -1.42939 1.42939 -1.43e-6 2.85878 -1.42939 1.42939 -1.43e-6 2.85878 -1.42939 1.42939;
          -0.0 -0.0 2.47578 2.47578 0.0 -0.0 2.47578 2.47578 1.65052 1.65052 4.1263 4.1263 1.65052 1.65052 4.1263 4.1263;
          0.00845581 0.0 0.0 0.0 4.52815 4.52815 4.52815 4.52815 2.26407 2.26407 2.26407 2.26407 6.79222 6.79222 6.79222 6.79222]) |> collect
C2 = @SMatrix [5.71757 0.0 0.0; -2.85879 4.95156 0.0; 0.0 0.0 9.05629]
X2 = vecs([0.00534021 2.85879 -1.42939 1.42939 0.0 2.85879 -1.42939 1.42939 0.0 2.85879 -1.4294 1.4294 0.0 2.85879 -1.4294 1.4294;
           0.0 0.0 2.47578 2.47578 0.0 0.0 2.47578 2.47578 1.65052 1.65052 4.1263 4.1263 1.65052 1.65052 4.1263 4.1263;
           0.00845858 0.0 0.0 0.0 4.52815 4.52815 4.52815 4.52815 2.26407 2.26407 2.26407 2.26407 6.79222 6.79222 6.79222 6.79222])  |> collect
at1 = set_cell!(Atoms(:Ti, X1), C1)
at2 = set_cell!(Atoms(:Ti, X2), C2)
rcut = 2.5 * rnn(:Ti)
test_nlist_julip(at1, rcut)
test_nlist_julip(at2, rcut)

# --------------- ACTUALLY RUNNING THE TESTS ------------------

println("JuLIP Configuration tests:")
for (i, (descr, at, cutoff)) in enumerate(test_configs)
  print("TEST $i: $descr => ")
  println(@test test_nlist_julip(at, cutoff))
end


using BenchmarkTools, RData, MixedModels, StatsModels

const SUITE = BenchmarkGroup()

const dat = Dict(Symbol(k)=>v for (k,v) in load(joinpath(dirname(pathof(MixedModels)), "..", "test", "dat.rda")));

const mods = Dict{Symbol,Vector{Expr}}(
    :Alfalfa => [:(1+A*B+(1|G)), :(1+A+B+(1|G))],
    :Animal => [:(1+(1|G)+(1|H))],
    :Arabidopsis => [],             # glmm and rename variables
    :Assay => [:(1+A+B*C+(1|G)+(1|H))],
    :AvgDailyGain => [:(1+A*U+(1|G)), :(1+A+U+(1|G))],
    :BIB => [:(1+A*U+(1|G)), :(1+A+U+(1|G))],
    :Bond => [:(1+A+(1|G))],
    :Chem97 => [:(1+(1|G)+(1|H)),:(1+U+(1|G)+(1|H))],
    :Contraception => [],           # glmm and rename variables
    :Cultivation => [:(1+A*B+(1|G)), :(1+A+B+(1|G)), :(1+A+(1|G))],
    :Demand => [:(1+U+V+W+X+(1|G)+(1|H))],
    :Dyestuff => [:(1+(1|G))],
    :Dyestuff2 => [:(1+(1|G))],
    :Early => [:(1+U+U&A+(1+U|G))],
    :Exam => [:(1+A*U+B+(1|G)), :(1+A+B+U+(1|G))],
    :Gasoline => [:(1+U+(1|G))],
    :Gcsemv => [:(1+A+(1|G))],      # variables must be renamed
    :Genetics => [:(1+A+(1|G)+(1|H))],
    :HR => [:(1+A*U+V+(1+U|G))],
    :Hsb82 => [:(1+A+B+C+U+(1|G))],
    :IncBlk => [:(1+A+U+V+W+Z+(1|G))],
    :InstEval => [:(1+A+(1|G)+(1|H)+(1|I)),:(1+A*I+(1|G)+(1|H))],
    :KKL => [],                    # variables must be renamed
    :KWDYZ => [],                  # variables must be renamed
    :Mississippi => [:(1+A+(1|G))],
    :Mmmec => [],                  # glmm (and offset) and variables renamed
    :Multilocation => [:(1+A+(0+A|G)+(1|H))],
    :Oxboys => [:(1+U+(1+U|G))],
    :PBIB => [:(1+A+(1|G))],
    :Pastes => [:(1+(1|G)+(1|H))],
    :Penicillin => [:(1+(1|G)+(1|H))],
    :Pixel => [:(1+U+V+(1+U|G)+(1|H))],  # variables must be renamed
    :Poems => [:(1+U+V+W+(1|G)+(1|H)+(1|I))],
    :Rail => [:(1+(1|G))],
    :SIMS => [:(1+U+(1+U|G))],
    :ScotsSec => [:(1+A+U+V+(1|G)+(1|H))],
    :Semi2 => [:(1+A+(1|G)+(1|H))],
    :Semiconductor => [:(1+A*B+(1|G))],
    :Socatt => [],                 # variables must be renamed - binomial glmm?
    :TeachingII => [:(1+A+T+U+V+W+X+Z+(1|G))],
    :VerbAgg => [:(1+A+B+C+U+(1|G)+(1|H))], # Bernoulli glmm and rename variables
    :Weights => [:(1+A*U+(1+U|G))],
    :WWheat => [:(1+U+(1+U|G))],
    :bdf => [],                   # rename variables and look up model
    :bs10 => [:(1+U+V+W+((1+U+V+W)|G)+((1+U+V+W)|H))],
    :cake => [:(1+A*B+(1|G))],
    :cbpp => [:(1+A+(1|G))],      # Binomial glmm, create and rename variables
    :d3 => [:(1+U+(1|G)+(1|H)+(1|I)), :(1+U+(1+U|G)+(1+U|H)+(1+U|I))],
    :dialectNL => [:(1+A+T+U+V+W+X+(1|G)+(1|H)+(1|I))],
    :egsingle => [:(1+A+U+V+(1|G)+(1|H))],
    :epilepsy => [],              # unknown origin
    :ergoStool => [:(1+A+(1|G))],
    :gb12 => [:(1+S+T+U+V+W+X+Z+((1+S+U+W)|G)+((1+S+T+V)|H))],
    :grouseticks => [],           # rename variables, glmm needs formula
    :guImmun => [],               # rename variables, glmm needs formula
    :guPrenat => [],              # rename variables, glmm needs formula
    :kb07 => [:(1+S+T+U+V+W+X+Z+((1+S+T+U+V+W+X+Z)|G)+((1+S+T+U+V+W+X+Z)|H)),
              :(1+S+T+U+V+W+X+Z+(1|G)+((0+S)|G)+((0+T)|G)+((0+U)|G)+((0+V)|G)+((0+W)|G)+
              ((0+X)|G)+((0+Z)|G)+(1|H)+((0+S)|H)+((0+T)|H)+((0+U)|H)+((0+V)|H)+
              ((0+W)|H)+((0+X)|H)+((0+Z)|H))],
    :ml1m => [:(1+(1|G)+(1|H))],
    :paulsim => [:(1+S+T+U+(1|H)+(1|G))],  # names of H and G should be reversed
    :sleepstudy => [:(1+U+(1+U|G)), :(1+U+(1|G)+(0+U|G))],
    :s3bbx => [],                 # probably drop this one
    :star => []                   # not sure it is worthwhile working with these data
    );

fitbobyqa(rhs::Expr, dsname::Symbol) = fit(LinearMixedModel, @eval(@formula(Y ~ $rhs)), dat[dsname])
compactstr(ds,rhs) = replace(string(ds, ':', rhs), ' ' => "")

SUITE["simplescalar"] = BenchmarkGroup(["single", "simple", "scalar"])
for ds in [:Alfalfa, :AvgDailyGain, :BIB, :Bond, :cake, :Cultivation, :Dyestuff,
    :Dyestuff2, :ergoStool, :Exam, :Gasoline, :Hsb82, :IncBlk, :Mississippi,
    :PBIB, :Rail, :Semiconductor, :TeachingII]
    for rhs in mods[ds]
        SUITE["simplescalar"][compactstr(ds, rhs)] = @benchmarkable fitbobyqa($(QuoteNode(rhs)), $(QuoteNode(ds)))
    end
end

SUITE["singlevector"] = BenchmarkGroup(["single", "vector"])
for ds in [:Early, :HR, :Oxboys, :SIMS, :sleepstudy, :Weights, :WWheat]
    for rhs in mods[ds]
        SUITE["singlevector"][compactstr(ds, rhs)] = @benchmarkable fitbobyqa($(QuoteNode(rhs)), $(QuoteNode(ds)))
    end
end

SUITE["nested"] = BenchmarkGroup(["multiple", "nested", "scalar"])
for ds in [:Animal, :Chem97, :Genetics, :Pastes, :Semi2]
    for rhs in mods[ds]
        SUITE["nested"][compactstr(ds, rhs)] = @benchmarkable fitbobyqa($(QuoteNode(rhs)), $(QuoteNode(ds)))
    end
end

SUITE["crossed"] = BenchmarkGroup(["multiple", "crossed", "scalar"])
for ds in [:Assay, :Demand, :InstEval, :Penicillin, :ScotsSec,
           :dialectNL, :egsingle, :ml1m, :paulsim]
    for rhs in mods[ds]
        SUITE["crossed"][compactstr(ds, rhs)] = @benchmarkable fitbobyqa($(QuoteNode(rhs)), $(QuoteNode(ds)))
    end
end

SUITE["crossedvector"] = BenchmarkGroup(["multiple", "crossed", "vector"])
for ds in [:bs10, :d3, :gb12, :kb07]
    for rhs in mods[ds]
        SUITE["crossedvector"][compactstr(ds, rhs)] = @benchmarkable fitbobyqa($(QuoteNode(rhs)), $(QuoteNode(ds)))
    end
end


module Chemistry

using JuLIP.FIO: load_dict
import JuLIP.FIO: read_dict, write_dict

export atomic_number,
       chemical_symbol,
       atomic_mass,
       symmetry,
       lattice_constant,
       rnn,
       AtomicNumber

"""
`struct AtomicNumber` : wrapper type to encode atomic numbers. Makes it
easier to dispatch on `AtomicNumber` and not confuse with a species index in
a list e.g.
"""
struct AtomicNumber <: Number
   z::Int16
end

AtomicNumber(z::AtomicNumber) = z
AtomicNumber(s::Symbol) = atomic_number(s)
(TI::Type{<: Integer})(z::AtomicNumber) = TI(z.z)
Base.convert(::Type{AtomicNumber}, z::Integer) = AtomicNumber(z)
Base.convert(::Type{AtomicNumber}, z::AtomicNumber) = z
Base.convert(TI::Type{<: Integer}, z::AtomicNumber) = convert(TI, z.z)
Base.hash(z::AtomicNumber, h::UInt) = hash(z.z, h)
Base.show(io::IO, z::AtomicNumber) = print(io, "<$(z.z)>")
Base.promote(x::AtomicNumber, y::Number) = promote(x.z, y)
Base.promote(x::Number, y::AtomicNumber) = promote(x, y.z)
Base.isless(x::AtomicNumber, y::AtomicNumber) = (x.z < y.z)
Base.isequal(x::AtomicNumber, y::AtomicNumber) = (x.z == y.z)

write_dict(z::AtomicNumber) =
      Dict("__id__" => "JuLIP_AtomicNumber", "z" => z.z)

read_dict(::Val{:JuLIP_AtomicNumber}, D::Dict) =
      AtomicNumber(D["z"])

data = load_dict(@__DIR__()[1:end-3] * "data/asedata.json")

const _rnn = [Float64(d) for d in data["rnn"]]
const _masses = [Float64(m) for m in data["masses"]]
const _refstates = Dict{String, Any}[ d == nothing ? Dict{String, Any}() : d
                                      for d in data["refstates"]]

# const _symbols = [Symbol(s) for s in data["symbols"]]
const _symbols = Dict{Int16, Symbol}()
const _numbers = Dict{Symbol, Int16}()
for (n, sym) in enumerate(Symbol.(data["symbols"]))
   _numbers[sym] = Int16(n - 1)
   _symbols[n-1] = sym
end

# special symbols
# :X -> 0 is already included in data["symbols"]
const __zAny__ = Int16(1000)
_numbers[:Any] = __zAny__
_symbols[__zAny__] = :Any

atomic_number(sym::Symbol) = AtomicNumber(_numbers[sym])

chemical_symbol(z::AtomicNumber) = _symbols[z.z]

atomic_mass(z::AtomicNumber) = _masses[z.z+1]
atomic_mass(sym::Symbol) = atomic_mass(atomic_number(sym))

element_name(z::AtomicNumber) = _names[z.z+1]
element_name(sym::Symbol) = element_name(atomic_number(sym))

rnn(z::AtomicNumber) = _rnn[z.z+1]
rnn(sym::Symbol) = rnn(atomic_number(sym))

symmetry(z::AtomicNumber) = Symbol(_refstates[z.z+1]["symmetry"])
symmetry(sym::Symbol) = symmetry(atomic_number(sym))

lattice_constant(z::AtomicNumber) = Float64( _refstates[z.z+1]["a"] )
lattice_constant(sym::Symbol) = lattice_constant(atomic_number(sym))

refstate(z::AtomicNumber) = _refstates[z.z+1]
refstate(sym::Symbol) = refstate(atomic_number(sym))

end


# default labels and starting values for simulation
const labels = [L"V" L"N" L"Ca" L"Ca_{er}" L"Ca_m" "ADP" "F6P" "FBP"];
const y0 = [-60; 0; 0.1; 185; 100; 780; 60; 40];
# y0_stat produces a stationary solution
const y0_stat = [ -60.486843436763024
                    0.0001367295835481462
                    0.06376844044216665
                  127.60067923018624
                   25.57114498828348
                  809.2663464858219
                    9.141692725854208
                    4.432631767186038e-6 ];

# defines a struct with default values
# A new system can be instancated with keyword arguments
#
# Example
# System(t = 120) -> Struct with default parameters but 120 min length
#
@with_kw struct System
    ode_func :: Function = sys
    y0 :: AbstractVecOrMat{Float64} = y0
    time :: Integer = 60
    plot_args :: Dict = Dict()
    params :: Dict{String,Any} = deepcopy(params)
    plot_params :: AbstractVector{Integer} = [1,2,3,4,5,6,7,8]
    change_params :: Dict{String,Number} = Dict()
    meds :: Vector{Meds} = []
end

# This function attempts to solve the ode system.
# If an DomainError exception occures it retries with
# increased accuracy. If an unexpted exception is thrown
# it is printed to stdout
# Refere to the DifferentialEquations.jl docs
# https://diffeq.sciml.ai/stable/tutorials/ode_example/
#trysolve(system :: System, callback, iteration) = trysolve(to_named_tuple(trysolve), callback, iteration)
function trysolve(system, callback, iteration)
    tol = 1e-1^(iteration*6)
    backup = deepcopy(system) # Create copy for further iterations.
			      # This is necessary because the Med and ExpMed objects are destroyed
    problem = ODEProblem(
        system.ode_func,
        system.y0,
        (0.0, (
            system.time < 1000)
                ? system.time * 6000
                : system.time),
        system.params)
    if tol <= 1e-19 # maximum tolerance
        return nothing
    end
    try
	    # the following callback is called every 100 timesteps and checks for medications to apply
        cb = (callback) ? PeriodicCallback(parse_medications, 100) : nothing
	    # https://diffeq.sciml.ai/stable/basics/common_solver_opts/
        return solve(problem,
                Tsit5(),
                saveat   = 1,
                maxiters = 1e6,
                reltol   = tol,
                abstol   = tol,
                callback = cb
        )
    catch err
        if isa(err, DomainError)
            return trysolve(backup, callback, iteration+1)
        else
            println(err)
        end
    end
end

function add_meds_to_plot(p, meds :: Vector{Union{Activa,ExpMed,Med}}, matrix)
    plot_max = findmax(matrix)[1][1]
    p
    println("ok: " * string(plot_max))
    for (i, m) ∈ enumerate(meds)
            plot!(
                [m.time, m.time + m.duration],
                [plot_max + 20 * i, plot_max + 20 * i],
                label = m.name,
                linestyle = :dash
                )
            annotate!(m.time + m.duration / 2, plot_max + 20 * (i + 0.4), string(m.dose))
            #println("med annotations cannot be added with legacy Med or ExpMed structs")
    end
    return p
end

function gen_solution_plot(solution :: ODESolution, system :: System)
    gr()
    # apply scaling factors to fit all parameters in one plot,
    # since they are numerically unreliable anyways
    matrix = scale_solution_columns(solution)
    p = plot(
        solution.t/6000,
        matrix[system.plot_params];
        label = hcat(labels[system.plot_params]...),
        format=:png,
        fontfamily = "Computer Modern",
        titlefontsize  = 14,
        guidefontsize  = 14,
        tickfontsize   = 14,
        legendfontsize = 13,
        framestyle = :default,
        width=1.2,
        size = (900,600),
        palette = :tab10,
        grid = false,
        legend = (1.07,0.8),
        rightmargin = 42mm,
        system.plot_args...
        )

    print(p)
    p = add_meds_to_plot(p, system.meds, matrix)
    return p
end

# Takes a settings object (NamedTuple) and changes to parameters for the simulation
# accordingly. It returns DiffEq Solution object, the time series for the 8 values
# and a plot fo the solution.

function simulate(system :: System; iteration = 1)
    for (key,val) ∈ system.change_params
        system.params[key] = val;
    end
    system.params["meds"] = system.meds

    @time solution = trysolve(system, system.meds |> length > 0, iteration)

    (solution === nothing) ? nothing : (
        solution,
        rowtocolumn(solution.u),
        gen_solution_plot(solution,system)
    )
end

# maintina legacy compatablity with old named tuple args
simulate(system) = simulate(System(system...))

# can be called to put multiple vales for one parameter
# and produce one solution for each
# If multiple threads are available the simulations
# will be run in parallel

function loopvals(key::String, vals::Array, system)
    sols = Dict()
    sols_mat = Dict()
    plots = Dict()

    @threads for val ∈ vals
        local s = deepcopy(system)
        s.params[key] = val
        s.plot_args[:title] = key * " = " * string(val)
        try
            sol, mat, p = simulate(s)
            sols[val] = sol
            sols_mat[val] = mat
            plots[val] = p
        catch
            println("simulate " * key * " = " * string(val) * " failed")
        end
    end
    acc_plot = plot(
        sortedvalues(plots)...,
        dpi=300,
        label = hcat(labels[system.plot_params]...),
    )
    return (sols, sols_mat, plots, acc_plot)
end

# same as above but for two chaning variables
function loopvals(key1::String, vals1::Vector, key2::String, vals2::Vector, system)
    local sols::Dict{Any, Dict}     = Dict()
    local sols_mat::Dict{Any, Dict} = Dict()
    local plots::Dict{Any, Dict}    = Dict()

    @threads for val1 ∈ vals1
    #for val1 ∈ vals1
            sols[val1] = Dict()
            sols_mat[val1] = Dict()
            plots[val1] = Dict()
        @threads for val2 ∈ vals2
        #for val2 ∈ vals2
            local s = deepcopy(system)
            s.params[key1] = val1
            s.params[key2] = val2
            s.plot_args[:title] = key1 * " = " * string(val1) * ", " * key2 * " = " * string(val2)
            try
                sol, mat, p = simulate(s)
                sols[val1][val2] = sol
                sols_mat[val1][val2] = mat
                plots[val1][val2] = p
            catch
                println("simulate " * key1 * " = " * string(val1) * " " * key2 * " = " * string(val2) *" failed")
            end
        end
    end
    acc_plot = plot(
        sortedvalues(plots)...,
        dpi=300,
        label = hcat(labels[system.plot_params]...),
    )

    return (sols, sols_mat, plots, acc_plot)
end

# use multiple dispatch to avoid calls to loopvals
simulate(system :: System, key1::String, vals1::Vector, key2::String, vals2::Vector) = loopvals(key1, vals1, key2, vals2, system)
simulate(system :: System, key1::String, vals1::Vector) = loopvals(key1, vals1, system)


#supplybounce.jl: Generate plots of supply bounce
#-------------------------------------------------------------------------------

c2rgba(c::Colorant) = convert(Colors.RGBA{Colors.FixedPointNumbers.N0f8}, c)

#Perform color mix:
function cmix(c1::T, c2::T, w1::Float64) where T <: Colors.RGBA{Colors.FixedPointNumbers.N0f8}
	w2 = 1-w1
	return T(c1.r*w1+c2.r*w2, c1.g*w1+c2.g*w2, c1.b*w1+c2.b*w2, c1.alpha*w1+c2.alpha*w2)
end

cmix(c1::Colorant, c2::Colorant, w1::Float64) =
	cmix(c2rgba(c1), c2rgba(c2), w1)

#==Constants
===============================================================================#
line_sup = cons(:a, line = set(style=:solid, color=:red, width=3))
line_gnd = cons(:a, line = set(style=:solid, color=:blue, width=3))
line_sig = cons(:a, line = set(style=:solid, color=:green, width=3))

line_idealsup = cons(:a, line = set(style=:dash, color=cmix(colorant"red", colorant"white", .5), width=2))
line_idealgnd = cons(:a, line = set(style=:dash, color=cmix(colorant"blue", colorant"white", .5), width=2))

#==
===============================================================================#
function plot_supplybounce(plotname::String="supplybounce";
		datapath=joinpath(@__DIR__, "..", "..", "data", "SupplyTransients.csv")
	)
	data = readdlm(datapath, ',', comments=true)
	#Time, VDD, VSS, VSIG

	t = data[:, 1]
	VDD = DataF1(t, data[:, 2])
	VSS = DataF1(t, data[:, 3])
	Vload = DataF1(t, data[:, 4])
	VDD_diff = VDD-VSS
	simgnd = 0*VDD
	VDD_ideal = simgnd+1

	#NOTE: Using 1.25 to avoid bug displaying 1000e-3V
	plotattr = cons(:a,
		ystrip1 = set(scale=:lin, axislabel=LBLAX_VOLT, min=-0.25, max=1.25),
		ystrip2 = set(scale=:lin, axislabel=LBLAX_VOLT, min=-0.25, max=1.25),
		ystrip3 = set(scale=:lin, axislabel=LBLAX_VOLT, min=-0.25, max=1.25),
#		xaxis = set(max = 10e-9),
	)

	tmid = 10e-9
	midpos = (tmid,0.5)
	siglist=[
		(id="(single Lpkg)", wfrm=VDD_ideal, line=line_idealsup, idpos=midpos, posalign=:cc, strip=2),
		(id="(LVDD=LVSS=Lpkg/2)", wfrm=VDD_ideal, line=line_idealsup, idpos=midpos, posalign=:cc, strip=3),
		(id="", wfrm=simgnd, line=line_idealgnd, idpos=(0,0), posalign=:bl, strip=2),
		(id="", wfrm=simgnd, line=line_idealgnd, idpos=(0,0), posalign=:bl, strip=3),

		(id="Vload", wfrm=Vload, line=line_sig, idpos=midpos, posalign=:cl, strip=1),
		(id="VDD", wfrm=VDD_diff, line=line_sup, idpos=(tmid, 0.9), posalign=:tc, strip=2),
		(id="VDD", wfrm=VDD, line=line_sup, idpos=(tmid,0.95), posalign=:tc, strip=3),
		(id="VSS", wfrm=VSS, line=line_gnd, idpos=(tmid,0.05), posalign=:bc, strip=3),
	]

	data = (
		nstrips = 3,
		plotattr,
		siglist=siglist,
	)

	plot = EasyPlot.build(plot_builders[:tran], data)
	plotgui = display(plotdisplay[:dflt], plot)
	EasyPlot._write(:png, "$plotname.png", optnoleg, plotgui)
	EasyPlot._write(:eps, "$plotname.eps", optnoleg, plotgui)
	return plotgui
end

#last line


module OutlierDetection
    using OutlierDetectionInterface

    import MLJBase
    const MLJ = MLJBase

    # re-export from OutlierDetectionInterface
    export CLASS_NORMAL, CLASS_OUTLIER, DEFAULT_THRESHOLD

    export ScoreTransformer,
           ClassTransformer,
           scale_minmax,
           scale_unify,
           combine_mean,
           combine_median,
           combine_max,
           classify_percentile,
           to_categorical,
           to_univariate_finite,
           ProbabilisticDetector,
           DeterministicDetector

    # utilities
    include("normalization.jl")
    include("classification.jl")
    include("combination.jl")

    # extension
    include("mlj_helpers.jl")
    include("mlj_transformers.jl")
    include("mlj_wrappers.jl")
end


##########################################################################
# Copyright 2017 Samuel Ridler.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
##########################################################################

# return true if ambulance can be dispatched to a call
function isAmbDispatchable(sim::Simulation, ambulance::Ambulance, call::Call)
	status = ambulance.status
	if isFree(status)
		return true
	elseif in(status, (ambMobilising, ambGoingToCall))
		return isAmbRedispatchable(sim, ambulance, sim.calls[ambulance.callIndex], call)
	end
	return false
end

# return true if ambulance may be redispatched from its current call to a different call, false otherwise.
function isAmbRedispatchable(sim::Simulation, ambulance::Ambulance, fromCall::Call, toCall::Call)
	@assert(in(ambulance.status, (ambMobilising, ambGoingToCall)))
	@assert(ambulance.callIndex == fromCall.index)
	sim.redispatch.allow || return false
	return sim.redispatch.conditions[Int(fromCall.priority), Int(toCall.priority)]
end

# return the index of the nearest dispatchable ambulance for a call
function findNearestDispatchableAmb!(sim::Simulation, call::Call)

	# nearest node to call, this is independent of chosen ambulance
	(node2, dist2) = (call.nearestNodeIndex, call.nearestNodeDist)

	# for ambulances that can be dispatched, find the one with shortest travel time to call
	ambIndex = nullIndex # nearest free ambulance
	minDuration = Inf
	for amb in sim.ambulances
		if isAmbDispatchable(sim, amb, call)
			# get time that ambulance will mobilise
			mobilisationTime = sim.time
			if sim.mobilisationDelay.use
				if amb.status == ambIdleAtStation
					mobilisationTime += sim.mobilisationDelay.expectedDuration
				elseif amb.status == ambMobilising
					# dispatcher does not have perfect knowledge of actual dispatch duration
					mobilisationTime += max(0, sim.mobilisationDelay.expectedDuration - (sim.time - amb.dispatchTime))
				end
			end
			responseDuration = mobilisationTime - sim.time # will add travel time next

			# duration on network
			travelMode = getTravelMode!(sim.travel, sim.responseTravelPriorities[call.priority], sim.time; startTime = mobilisationTime)
			(node1, time1) = getRouteNextNode!(sim, amb.route, travelMode.index, sim.time) # next/nearest node in ambulance route
			responseDuration += shortestPathTravelTime(sim.net, travelMode.index, node1, node2) # time spent on network
			time2 = offRoadTravelTime(travelMode, dist2) # time to reach nearest node
			responseDuration += time1 + time2 # add time to get on and off network

			if minDuration > responseDuration
				ambIndex = amb.index
				minDuration = responseDuration
			end
		end
	end

	return ambIndex
end
findNearestFreeAmbToCall!(sim::Simulation, call::Call) = findNearestDispatchableAmb!(sim, call) # compat