module TestNAArrayOperators

using FactCheck
using DataCubes
using DataCubes: AbstractArrayWrapper,FloatNAArray,wrap_array

facts("NAArrayOperators tests") do
  context("AbstractArrayWrapper tests") do
    @fact broadcast(+, 1, AbstractArrayWrapper(nalift([1,2,3]))) --> AbstractArrayWrapper(nalift([2,3,4]))
    @fact broadcast(-, AbstractArrayWrapper(nalift([1,2,3])), 1) --> AbstractArrayWrapper(nalift([0,1,2]))
    @fact broadcast(*, AbstractArrayWrapper(nalift([1,2,3])),
            AbstractArrayWrapper(nalift([5.0,6.0,7.0]))) --> AbstractArrayWrapper(nalift([5.0,12.0,21.0]))
    @fact map(x->Nullable(x.value*2), AbstractArrayWrapper(nalift([1,2,3]))) --> AbstractArrayWrapper(nalift([2,4,6]))
    @fact (x=AbstractArrayWrapper([5,1,3,2,4]);sort!(x);x) --> AbstractArrayWrapper([1,2,3,4,5])
    @fact sort(AbstractArrayWrapper([5 1 3 2 4]),2) --> AbstractArrayWrapper([1 2 3 4 5])
    @fact sort(AbstractArrayWrapper([5,1,3,2,4])) --> AbstractArrayWrapper([1,2,3,4,5])
    @fact map((x,y)->DataCubes.naop_plus(x,y), nalift([1,2,3,4,5]), @nalift([1,2,NA,4,5])) --> @nalift([2,4,NA,8,10])
    @fact broadcast(>, @nalift([1,2,NA]), 1) --> @nalift([false, true, NA])
    @fact broadcast(<, 1, @nalift([1,2,NA])) --> @nalift([false, true, NA])
    @fact broadcast(<, @nalift([1,2,3,NA]), @nalift([NA,3,1,2])) --> @nalift([NA,true,false,NA])
    @fact broadcast(-, @nalift([1.0,2.0,NA]), 1) --> @nalift([0.0,1.0,NA])
    @fact broadcast(-, 1, @nalift([1.0,2.0,NA])) --> @nalift([0.0,-1.0,NA])
    @fact broadcast(*, @nalift([1.0,2.0,NA]), @nalift([NA,2.0,3.0])) --> @nalift([NA,4.0,NA])
    @fact -@nalift([1.0,2.0,NA]) --> @nalift([-1.0,-2.0,NA])
    @fact ~@nalift([1,2,NA]) --> @nalift([~1,~2,NA])
    @fact broadcast(>, AbstractArrayWrapper([1,2,3]), 1) --> AbstractArrayWrapper([false,true,true])
    @fact broadcast(>, AbstractArrayWrapper([1,2,3]), 1.5) --> AbstractArrayWrapper([false,true,true])
    @fact broadcast(<, 1, AbstractArrayWrapper([1,2,3])) --> AbstractArrayWrapper([false,true,true])
    @fact broadcast(>, AbstractArrayWrapper([1,2,3]), Nullable(1)) --> nalift([false,true,true])
    @fact AbstractArrayWrapper([1,2,3]) + 1.0 --> AbstractArrayWrapper([2.0,3.0,4.0])
    @fact AbstractArrayWrapper([1,2,3]) + Nullable(1.0) --> nalift([2.0,3.0,4.0])
    @fact broadcast(<, Nullable(1), AbstractArrayWrapper([1,2,3])) --> nalift([false,true,true])
    @fact broadcast(<, AbstractArrayWrapper([3.0,2.0,1.0]), AbstractArrayWrapper([1.0,2.0,3.0])) --> AbstractArrayWrapper([false,false,true])
    @fact broadcast(<, AbstractArrayWrapper([3,2,1]), nalift([1,2,3])) --> nalift([false,false,true])
    @fact broadcast(<, @nalift([3,NA,1]), AbstractArrayWrapper([1.0,2.0,3.0])) --> @nalift([false,NA,true])
    @fact broadcast(<, nalift([3,2,1]), nalift([1.0,2.0,3.0])) --> nalift([false,false,true])
    @fact @nalift([NA,2,1]) + @nalift([1.0,NA,3.0]) --> @nalift([NA,NA,4.0])
    @fact nalift([3,2,1]) + AbstractArrayWrapper([1.0,2.0,3.0]) --> nalift([4.0,4.0,4.0])
    @fact AbstractArrayWrapper([3,2,1]) + AbstractArrayWrapper([1.0,2.0,3.0]) --> AbstractArrayWrapper([4.0,4.0,4.0])
    @fact AbstractArrayWrapper([3,2,1]) + 5.0 --> AbstractArrayWrapper([8.0,7.0,6.0])
    @fact AbstractArrayWrapper([3,2,1]) + Nullable(5.0) --> nalift([8.0,7.0,6.0])
    @fact -AbstractArrayWrapper([3,2,1]) --> AbstractArrayWrapper([-3,-2,-1])
    @fact -AbstractArrayWrapper([3.0,2.0,1.0]) --> AbstractArrayWrapper([-3.0,-2.0,-1.0])
    @fact -nalift([3.0,2.0,1.0]) --> nalift([-3.0,-2.0,-1.0])
    @fact -darr(a=[3.0,2.0,1.0]) --> darr(a=[-3.0,-2.0,-1.0])
    @fact -larr(a=[3.0,2.0,1.0], axis1=[:x,:y,:z]) --> larr(a=[-3.0,-2.0,-1.0], axis1=[:x,:y,:z])
    @fact nalift([1.0,2.0,3.0]) + nalift([1,2,3]) --> nalift([2.0,4.0,6.0])
    @fact nalift([1.0,2.0,3.0]) + nalift([1.0,2.0,3.0]) --> nalift([2.0,4.0,6.0])
    @fact AbstractArrayWrapper(map(Nullable, [1.0,2.0,3.0])) + nalift([1.0,2.0,3.0]) --> nalift([2.0,4.0,6.0])
    @fact AbstractArrayWrapper(map(Nullable, [1.0,2.0,3.0])) + @nalift([1.0,2.0,NA]) --> @nalift([2.0,4.0,NA])
    @fact @nalift([1.0,2.0,NA]) + AbstractArrayWrapper([Nullable(1.0),Nullable{Float64}(), Nullable(3.0)]) --> @nalift([2.0,NA,NA])
    @fact AbstractArrayWrapper(map(Nullable, [1.0,2.0,3.0])) + nalift([1,2,3]) --> nalift([2.0,4.0,6.0])
    @fact AbstractArrayWrapper([Nullable(1.0),Nullable(2.0),Nullable{Float64}()]) + AbstractArrayWrapper([1,2,3]) --> @nalift([2.0,4.0,NA])
    @fact AbstractArrayWrapper([Nullable(1), Nullable(2), Nullable{Int}()]) + AbstractArrayWrapper([1.0,2.0,3.0]) --> @nalift([2.0,4.0,NA])
    @fact AbstractArrayWrapper([1.0,2.0,3.0]) + AbstractArrayWrapper([Nullable(1), Nullable(2), Nullable{Int}()]) --> @nalift([2.0,4.0,NA])
    @fact AbstractArrayWrapper([1.0,2.0,3.0]) + AbstractArrayWrapper([1,2,3]) --> [2.0,4.0,6.0]
    @fact broadcast(/, AbstractArrayWrapper([1,2,3]), AbstractArrayWrapper([1,2,3])) --> [1.0,1.0,1.0]
    @fact wrap_array([true,true,false]) & wrap_array([false,true,true]) --> wrap_array([false,true,false])
    @fact wrap_array([true,true,false]) & true --> wrap_array([true,true,false])
    @fact wrap_array([true,true,false]) & false --> wrap_array([false,false,false])
    @fact (2+3im) * @nalift([1,2,NA]) --> @nalift([2+3im,4+6im,NA])
    @fact @nalift([1,2,NA]) * (2+3im)--> @nalift([2+3im,4+6im,NA])
    @fact Nullable(2+3im) * @nalift([1,2,NA]) --> @nalift([2+3im,4+6im,NA])
    @fact @nalift([1,2,NA]) * Nullable(2+3im)--> @nalift([2+3im,4+6im,NA])
    @fact Nullable(im) / @nalift([1,2,NA]) --> @nalift([im,0.5im,NA])
    @fact @nalift([1,2,NA]) / Nullable(im)--> @nalift([-im,-2im,NA])
    @fact Nullable(e) / @nalift([1,2,NA]) --> @nalift([convert(Float64,e),convert(Float64,e/2),NA])
    @fact @nalift([1,2,NA]) / Nullable(e)--> @nalift([convert(Float64,1/e),convert(Float64,2/e),NA])
    @fact e / @nalift([1,2,NA]) --> @nalift([convert(Float64,e),convert(Float64,e/2),NA])
    @fact @nalift([1,2,NA]) / e--> @nalift([convert(Float64,1/e),convert(Float64,2/e),NA])
    @fact nalift([1.0,2.0,3.0]) + 1.0 --> nalift([2.0,3.0,4.0])
    @fact 1.0 + nalift([1.0,2.0,3.0]) --> nalift([2.0,3.0,4.0])
    @fact nalift([1.0,2.0,3.0]) + 1 --> nalift([2.0,3.0,4.0])
    @fact 1 + nalift([1.0,2.0,3.0]) --> nalift([2.0,3.0,4.0])
    @fact 1.0 + nalift([1,2,3]) --> nalift([2.0,3.0,4.0])
    @fact nalift([1,2,3]) + 1.0 --> nalift([2.0,3.0,4.0])
    @fact nalift([1.0,2.0,3.0]) + Nullable(1.0) --> nalift([2.0,3.0,4.0])
    @fact Nullable(1.0) + nalift([1.0,2.0,3.0]) --> nalift([2.0,3.0,4.0])
    @fact nalift([1.0,2.0,3.0]) + Nullable(1) --> nalift([2.0,3.0,4.0])
    @fact Nullable(1) + nalift([1.0,2.0,3.0]) --> nalift([2.0,3.0,4.0])
    @fact Nullable(1.0) + nalift([1,2,3]) --> nalift([2.0,3.0,4.0])
    @fact nalift([1,2,3]) + Nullable(1.0) --> nalift([2.0,3.0,4.0])
    @fact nalift([1.0,2.0,3.0]) * 1.0 --> nalift([2.0,3.0,4.0]) - 1
    @fact 1.0 * nalift([1.0,2.0,3.0]) --> nalift([2.0,3.0,4.0]) - 1
    @fact nalift([1.0,2.0,3.0]) * 1 --> nalift([2.0,3.0,4.0]) - 1
    @fact 1 * nalift([1.0,2.0,3.0]) --> nalift([2.0,3.0,4.0]) - 1
    @fact 1.0 * nalift([1,2,3]) --> nalift([2.0,3.0,4.0]) - 1
    @fact nalift([1,2,3]) * 1.0 --> nalift([2.0,3.0,4.0]) - 1
    @fact nalift([1.0,2.0,3.0]) * Nullable(1.0) --> nalift([2.0,3.0,4.0]) - 1
    @fact Nullable(1.0) * nalift([1.0,2.0,3.0]) --> nalift([2.0,3.0,4.0]) - 1
    @fact @nalift([1.0,2.0,NA]) * Nullable(1) --> @nalift([2.0,3.0,NA]) - 1
    @fact Nullable(1) * nalift([1.0,2.0,3.0]) --> nalift([2.0,3.0,4.0]) - 1
    @fact Nullable(1.0) * @nalift([NA,2,3]) --> @nalift([NA,3.0,4.0]) - 1
    @fact nalift([1,2,3]) * Nullable(1.0) --> nalift([2.0,3.0,4.0]) - 1
    @fact nalift([1,2,3]) * Nullable{Int}() --> nalift([Nullable{Int}(), Nullable{Int}(), Nullable{Int}()])
    @fact broadcast(/, @nalift([1,2,3,NA,NA,5]), @nalift([2,1,2,3,NA,NA])) --> @nalift([0.5,2.0,1.5,NA,NA,NA])
    @fact broadcast(/, @nalift([1,2,3,NA,NA,5]), @nalift([2,1,2,3,NA,NA])) --> @nalift([0.5,2.0,1.5,NA,NA,NA])
    @fact broadcast(/, @nalift([1,2,3,NA,NA,5]), DataCubes.wrap_array([2,1,2,3,2,5])) --> @nalift([0.5,2.0,1.5,NA,NA,1.0])
    @fact broadcast(/, DataCubes.wrap_array([1,2,3,4,5,6]), @nalift([2,1,2,2,NA,NA])) --> @nalift([0.5,2.0,1.5,2.0,NA,NA])
    @fact broadcast(/, @nalift([1,2,3,NA,NA,5]), 2) --> @nalift([0.5,1.0,1.5,NA,NA,2.5])
    @fact broadcast(/, @nalift([1,2,3,NA,NA,5]), 2.0) --> @nalift([0.5,1.0,1.5,NA,NA,2.5])
    @fact @nalift([1,2,3,NA,NA,5]) / 2 --> @nalift([0.5,1.0,1.5,NA,NA,2.5])
    @fact @nalift([1,2,3,NA,NA,5]) / 2.0 --> @nalift([0.5,1.0,1.5,NA,NA,2.5])
    @fact 1/@nalift([1,2,NA]) --> @nalift([1.0,0.5,NA])
    @fact 1.0/@nalift([1,2,NA]) --> @nalift([1.0,0.5,NA])
    @fact broadcast(/, 1, @nalift([1,2,NA])) --> @nalift([1.0,0.5,NA])
    @fact broadcast(/, 1.0, @nalift([1,2,NA])) --> @nalift([1.0,0.5,NA])
    @fact broadcast(^, e, @nalift([1,2,NA])) --> map(x -> isnull(x) ? Nullable{Float64}() : Nullable(e^x.value), @nalift([1,2,NA]))
    @fact broadcast(^, e, DataCubes.wrap_array([1.0,2.0])) --> DataCubes.wrap_array(map(x->e^x, [1.0,2.0]))
    @fact broadcast(^, e, DataCubes.wrap_array(Real[1.0,2])) --> DataCubes.wrap_array(map(x->e^x, Real[1.0,2]))
    @fact @nalift([1,3,NA]) == @nalift([1,NA,3]) --> false
    @fact @nalift([1,NA,3]) == @nalift([1,3,NA]) --> false
    @fact @nalift([1,NA,3]) == @nalift([1,NA,3]) --> true
    @fact @nalift([1,NA,3]) == @nalift([1,NA,2]) --> false
    @fact @nalift([1.0,3.0,NA]) == @nalift([1.0,NA,3.0]) --> false
    @fact @nalift([1.0,NA,3.0]) == @nalift([1.0,3.0,NA]) --> false
    @fact @nalift([1.0,NA,3.0]) == @nalift([1.0,NA,3.0]) --> true
    @fact @nalift([1.0,NA,3.0]) == @nalift([1.0,NA,2.0]) --> false
    @fact larr(a=[1,2,3]) + Nullable(1) --> larr(a=[2,3,4])
    @fact larr(a=1.0*[1,2,3]) + Nullable(1) --> larr(a=1.0*[2,3,4])
    @fact larr(a=1.0*[1,2,3]) + Nullable(1.0) --> larr(a=1.0*[2,3,4])
    @fact broadcast(+, larr(a=1.0*[1,2,3]), Nullable(1.0)) --> larr(a=1.0*[2,3,4])
    @fact broadcast(+, larr(a=1.0*[1,2,3]), Nullable{Int}()) --> larr(a=fill(Nullable{Float64}(),3))
    @fact broadcast(*, larr(a=1.0*[1,2,3]), Nullable(2.0)) --> larr(a=2.0*[1,2,3])
    @fact larr(a=1.0*[1,2,3]) * Nullable(2.0) --> larr(a=2.0*[1,2,3])
    @fact Nullable(2.0) * larr(a=1.0*[1,2,3]) --> larr(a=2.0*[1,2,3])
    @fact broadcast(*, Nullable(2), larr(a=1.0*[1,2,3])) --> larr(a=2.0*[1,2,3])
    @fact broadcast(*, Nullable{Int}(), larr(a=1.0*[1,2,3])) --> larr(a=fill(Nullable{Float64}(),3))
    @fact larr(a=[1,2,3]) + 1 --> larr(a=[2,3,4])
    @fact larr(a=1.0*[1,2,3]) + 1 --> larr(a=1.0*[2,3,4])
    @fact larr(a=1.0*[1,2,3]) * im --> larr(a=im*1.0*[1,2,3])
    @fact larr(a=1.0*[1,2,3]) / im --> larr(a=-im*1.0*[1,2,3])
    @fact larr(a=1.0*[1,2,3]) * Nullable(im) --> larr(a=1.0im*[1,2,3])
    @fact larr(a=1.0*[1,2,3]) / Nullable(im) --> larr(a=-1.0im*[1,2,3])
    @fact im * larr(a=1.0*[1,2,3]) --> larr(a=im*[1,2,3]*1.0)
    @fact im / larr(a=1.0*[1,2,3]) --> larr(a=im./(1.0*[1,2,3]))
    @fact Nullable(im) * larr(a=1.0*[1,2,3]) --> larr(a=im.*[1,2,3])
    @fact Nullable(im) / larr(a=1.0*[1,2,3]) --> larr(a=im./(1.0*[1,2,3]))
    @fact im * nalift(1.0*[1,2,3]) --> nalift(im*[1,2,3]*1.0)
    @fact im / nalift(1.0*[1,2,3]) --> nalift(im./(1.0*[1,2,3]))
    @fact Nullable(im) * nalift(1.0*[1,2,3]) --> nalift(im.*[1,2,3])
    @fact Nullable(im) / nalift(1.0*[1,2,3]) --> nalift(im./(1.0*[1,2,3]))
    @fact broadcast(*, nalift(1.0*[1,2,3]), im) --> nalift(im*1.0*[1,2,3])
    @fact broadcast(/, nalift(1.0*[1,2,3]), im) --> nalift(-im*1.0*[1,2,3])
    @fact nalift(1.0*[1,2,3]) * im --> nalift(im*1.0*[1,2,3])
    @fact nalift(1.0*[1,2,3]) / im --> nalift(-im*1.0*[1,2,3])
    @fact broadcast(*, wrap_array(1.0*[1,2,3]), im) --> wrap_array(im*1.0*[1,2,3])
    @fact broadcast(/, wrap_array(1.0*[1,2,3]), im) --> wrap_array(-im*1.0*[1,2,3])
    @fact larr(a=1.0*[1,2,3]) + 1.0 --> larr(a=1.0*[2,3,4])
    @fact broadcast(+, larr(a=1.0*[1,2,3]), 1.0) --> larr(a=1.0*[2,3,4])
    @fact broadcast(*, larr(a=1.0*[1,2,3]), 2.0) --> larr(a=2.0*[1,2,3])
    @fact larr(a=1.0*[1,2,3]) * 2.0 --> larr(a=2.0*[1,2,3])
    @fact 2.0 * larr(a=1.0*[1,2,3]) --> larr(a=2.0*[1,2,3])
    @fact broadcast(*, 2, larr(a=1.0*[1,2,3])) --> larr(a=2.0*[1,2,3])
    @fact darr(a=[1,2,3]) + 1 --> darr(a=[2,3,4])
    @fact darr(a=1.0*[1,2,3]) + 1 --> darr(a=1.0*[2,3,4])
    @fact darr(a=1.0*[1,2,3]) + 1.0 --> darr(a=1.0*[2,3,4])
    @fact broadcast(+, darr(a=1.0*[1,2,3]), 1.0) --> darr(a=1.0*[2,3,4])
    @fact broadcast(*, darr(a=1.0*[1,2,3]), 2.0) --> darr(a=2.0*[1,2,3])
    @fact darr(a=1.0*[1,2,3]) * 2.0 --> darr(a=2.0*[1,2,3])
    @fact 2.0 * darr(a=1.0*[1,2,3]) --> darr(a=2.0*[1,2,3])
    @fact broadcast(*, 2, darr(a=1.0*[1,2,3])) --> darr(a=2.0*[1,2,3])
    @fact darr(a=[1,2,3]) / 1 --> darr(a=[1,2,3])
    @fact darr(a=1.0*[1,2,3]) / 2 --> darr(a=[1,2,3]/2.0)
    @fact darr(a=1.0*[1,2,3]) / Nullable(2) --> darr(a=[1,2,3]/2.0)
    @fact broadcast(/, darr(a=1.0*[1,2,3]), 2) --> darr(a=[1,2,3]/2.0)
    @fact broadcast(/, darr(a=1.0*[1,2,3]), Nullable(2)) --> darr(a=[1,2,3]/2.0)
    @fact darr(a=1.0*[1,2,3]) / 3.0 --> darr(a=[1,2,3]/3.0)
    @fact broadcast(/, darr(a=1.0*[1,2,3]), 3.0) --> darr(a=[1,2,3]/3.0)
    @fact broadcast(/, darr(a=1.0*[1,2,3]), 2.0) --> darr(a=[1,2,3]/2.0)
    @fact darr(a=1.0*[1,2,3]) / 2.0 --> darr(a=[1,2,3]/2.0)
    @fact broadcast(/, darr(a=1.0*[1,2,3]), Nullable(2.0)) --> darr(a=[1,2,3]/2.0)
    @fact darr(a=1.0*[1,2,3]) / Nullable(2.0) --> darr(a=[1,2,3]/2.0)
    @fact broadcast(/, darr(a=[1,2,3]), darr(a=[3,2,1])) --> darr(a=[1,2,3]./[3,2,1])
    @fact 2.0 / darr(a=1.0*[1,2,3]) --> darr(a=2.0./[1,2,3])
    @fact broadcast(/, 2.0, darr(a=1.0*[1,2,3])) --> darr(a=2.0./[1,2,3])
    @fact Nullable(2.0) / darr(a=1.0*[1,2,3]) --> darr(a=2.0./[1,2,3])
    @fact broadcast(/, Nullable(2.0), darr(a=1.0*[1,2,3])) --> darr(a=2.0./[1,2,3])
    @fact darr(a=[1 2 3;4 5 6],b=1.0*[1 2 3;4 5 6])+darr(b=[1 2 3;4 5 6],a=[11 12 13;14 15 16]) --> darr(a=[12 14 16;18 20 22],b=2.0*[1 2 3;4 5 6])
    @fact darr(a=[1 2 3;4 5 6],b=1.0*[1 2 3;4 5 6])+larr(b=[1 2 3;4 5 6],a=[11 12 13;14 15 16],axis1=[:X,:Y]) --> larr(a=[12 14 16;18 20 22],b=2.0*[1 2 3;4 5 6],axis1=[:X,:Y])
    @fact darr(a=[1 2 3;4 5 6],b=1.0*[1 2 3;4 5 6])+larr(b=[1 2 3;4 5 6],a=[11 12 13;14 15 16],axis1=[:X,:Y]) --> larr(a=[12 14 16;18 20 22],b=2.0*[1 2 3;4 5 6],axis1=[:X,:Y])
    @fact larr(b=[1 2 3;4 5 6],a=[11 12 13;14 15 16],axis1=[:X,:Y]) + darr(a=[1 2 3;4 5 6],b=1.0*[1 2 3;4 5 6]) --> larr(b=2.0*[1 2 3;4 5 6],a=[12 14 16;18 20 22],axis1=[:X,:Y])
    @fact_throws larr(b=[1 2 3;4 5 6],a=[11 12 13;14 15 16],axis1=[:X,:Y]) + larr(a=[1 2 3;4 5 6],b=1.0*[1 2 3;4 5 6])
    @fact darr(a=[1.0 2.0]) * darr(a=[2.0,3.0]) --> darr(a=[8.0])
    @fact darr(a=[1.0 2.0;3.0 4.0]) / darr(a=[2.0 3.0]) --> darr(a=[1.0 2.0;3.0 4.0]/[2.0 3.0])
    @fact nalift([1.0 2.0]) * nalift([2.0,3.0]) --> nalift([8.0])
    @fact nalift([1.0 2.0;3.0 4.0]) / nalift([2.0 3.0]) --> nalift([1.0 2.0;3.0 4.0]/[2.0 3.0])
    @fact wrap_array(FloatNAArray([1.0 2.0]) * FloatNAArray([2.0,3.0])) --> wrap_array(FloatNAArray([8.0]))
    @fact wrap_array(FloatNAArray([1.0 2.0;3.0 4.0]) / FloatNAArray([2.0 3.0])) --> wrap_array(FloatNAArray([1.0 2.0;3.0 4.0]/[2.0 3.0]))
    @fact vcat(nalift([1 2 3]), nalift([4 5 6])) --> nalift([1 2 3;4 5 6])
    @fact hcat(nalift([1 2 3]), nalift([4 5 6])) --> nalift([1 2 3 4 5 6])
    @fact broadcast(/, nalift([1.0,2.0,3.0]), wrap_array([1,2,3])) --> nalift([1.0,1.0,1.0])
    @fact broadcast(/, wrap_array([1,2,3]), nalift([1.0,2.0,3.0]))  --> nalift([1.0,1.0,1.0])
    @fact broadcast(/, wrap_array([1,2,3]), wrap_array([1.0,2.0,3.0]))  --> wrap_array([1.0,1.0,1.0])
    @fact 3 * wrap_array([1,2,3]) --> wrap_array([3,6,9])
    @fact wrap_array([1,2,3]) * 3 --> wrap_array([3,6,9])
    # currently a matrix multiplication between non floating arrays are not supported.
    @fact larr(a=1.0*[1 2;3 4]) * (1.0 * larr(a=[5 6;7 8])) --> larr(a=[19.0 22.0;43.0 50.0])
    @fact_throws larr(a=[1 2;3 4]) * 1.0 * larr(a=[5 6;7 8])
    @fact_throws larr(a=[1 2;3 4]) * larr(a=[5 6;7 8])
    @fact_throws larr(rand(2,3),axis=[:a,:b],axis=['x','y','z']) * larr(rand(3,4),axis=[:m,:n,:p],axis=[10,11,12,13])
    @fact size(larr(rand(2,3),axis=[:a,:b],axis=['x','y','z']) * larr(rand(3,4),axis=['x','y','z'],axis=[10,11,12,13])) --> (2,4)
    @fact size(larr(rand(2,3),axis=[:a,:b],axis=['x','y','z']) / larr(rand(4,3),axis=[10,11,12,13],axis=['x','y','z'])) --> (2,4)
    @fact_throws size(larr(rand(2,3),axis=[:a,:b],axis=['x','y','z']) / larr(rand(4,3),axis=[10,11,12,13]))
  end
  context("ifelse tests") do
    @fact ifelse(@nalift([true,false,NA]), 3, 5) --> @nalift([3,5,NA])
    @fact ifelse(@nalift([true,false,NA]), Nullable(3), Nullable(5)) --> @nalift([3,5,NA])
    @fact ifelse(@nalift([true,false,NA]), Nullable(3), 5) --> @nalift([3,5,NA])
    @fact ifelse(@nalift([true,false,NA]), 3, Nullable(5)) --> @nalift([3,5,NA])
    @fact ifelse(@nalift([true,true,false,NA]), @nalift([1,2,3,4]), 5) --> @nalift([1,2,5,NA])
    @fact ifelse(@nalift([true,true,false,NA]), 5, @nalift([1,2,3,4])) --> @nalift([5,5,3,NA])
    @fact ifelse(@nalift([true,true,false,NA]), 1.0*nalift([1,2,3,4]), 5) --> 1.0*@nalift([1,2,5,NA])
    @fact ifelse(@nalift([true,true,false,NA]), 5, 1.0*nalift([1,2,3,4])) --> 1.0*@nalift([5,5,3,NA])
    @fact ifelse(@nalift([true,true,false,NA]), @nalift([1,2,3,4]), 5) --> @nalift([1,2,5,NA])
    @fact ifelse(@nalift([true,true,false,NA]), Nullable(5), @nalift([1,2,3,4])) --> @nalift([5,5,3,NA])
    @fact ifelse(@nalift([true,true,false,NA]), 1.0*nalift([1,2,3,4]), Nullable(5)) --> 1.0*@nalift([1,2,5,NA])
    @fact ifelse(@nalift([true,true,false,NA]), Nullable(5), 1.0*nalift([1,2,3,4])) --> 1.0*@nalift([5,5,3,NA])
    @fact ifelse(@nalift([true,true,false,NA]), Nullable(5), 1.0*[1,2,3,4]) --> 1.0*@nalift([5,5,3,NA])
    @fact ifelse(@nalift([true,true,false,NA]), [1,2,3,4], Nullable(5)) --> @nalift([1,2,5,NA])
    @fact ifelse(@nalift([true,true,false,NA]), 5, [1,2,3,4]) --> @nalift([5,5,3,NA])
    @fact ifelse(@nalift([true,true,false,NA]), 1.0*[1,2,3,4], 5) --> 1.0*@nalift([1,2,5,NA])
    @fact ifelse(@nalift([true,true,false,NA]), 1.0*AbstractArrayWrapper([1,2,3,4]), 5) --> 1.0*@nalift([1,2,5,NA])
    @fact ifelse(@nalift([true,true,false,NA]), 5, 1.0*[1,2,3,4]) --> 1.0*@nalift([5,5,3,NA])
    @fact ifelse(@nalift([true,true,false,NA]), 5, 1.0*AbstractArrayWrapper([1,2,3,4])) --> 1.0*@nalift([5,5,3,NA])
    @fact ifelse(@nalift([true,true,false,NA]), [1,2,3,4], Nullable(5)) --> @nalift([1,2,5,NA])
    @fact ifelse(@nalift([true,true,false,NA]), Nullable(5), [1,2,3,4]) --> @nalift([5,5,3,NA])
    @fact ifelse(@nalift([true,true,false,NA]), 1.0*[1,2,3,4], Nullable(5)) --> 1.0*@nalift([1,2,5,NA])
    @fact ifelse(@nalift([true,true,false,NA]), 1.0*AbstractArrayWrapper([1,2,3,4]), Nullable(5)) --> 1.0*@nalift([1,2,5,NA])
    @fact ifelse(@nalift([true,true,false,NA]), Nullable(5), 1.0*[1,2,3,4]) --> 1.0*@nalift([5,5,3,NA])
    @fact ifelse(@nalift([true,true,false,NA]), Nullable(5), 1.0*AbstractArrayWrapper([1,2,3,4])) --> 1.0*@nalift([5,5,3,NA])
    @fact ifelse(@nalift([true true false NA]), AbstractArrayWrapper([1 2 3 4]), AbstractArrayWrapper([11 12 13 14])*1.0) --> 1.0*@nalift([1 2 13 NA])
    @fact ifelse(@nalift([true true false NA]), nalift([1 2 3 4]), [11 12 13 14]*1.0) --> 1.0*@nalift([1 2 13 NA])
    @fact ifelse(@nalift([true true false NA]), nalift([1 2 3 4]), AbstractArrayWrapper([11 12 13 14])*1.0) --> 1.0*@nalift([1 2 13 NA])
    @fact ifelse(@nalift([true true false NA]), [1 2 3 4], nalift([11 12 13 14])*1.0) --> 1.0*@nalift([1 2 13 NA])
    @fact ifelse(@nalift([true true false NA]), AbstractArrayWrapper([1 2 3 4]), nalift([11 12 13 14])*1.0) --> 1.0*@nalift([1 2 13 NA])
    @fact ifelse(@nalift([true true false NA]), nalift([1 2 3 4]), nalift([11 12 13 14])*1.0) --> 1.0*@nalift([1 2 13 NA])

    @fact ifelse(DataCubes.AbstractArrayWrapper([true,false,false]), 3, 5) --> @nalift([3,5,5])
    @fact ifelse(DataCubes.AbstractArrayWrapper([true,false,false]), Nullable(3), 5) --> @nalift([3,5,5])
    @fact ifelse(DataCubes.AbstractArrayWrapper([true,false,false]), 3, Nullable(5)) --> @nalift([3,5,5])
    @fact ifelse(DataCubes.AbstractArrayWrapper([true,false,false]), Nullable(3), Nullable(5)) --> @nalift([3,5,5])
    @fact ifelse(DataCubes.AbstractArrayWrapper([true,true,false,false]), @nalift([1,2,3,4]), 5) --> @nalift([1,2,5,5])
    @fact ifelse(DataCubes.AbstractArrayWrapper([true,true,false,false]), [1,2,3,4], 5) --> @nalift([1,2,5,5])
    @fact ifelse(DataCubes.AbstractArrayWrapper([true,true,false,false]), AbstractArrayWrapper([1,2,3,4]), 5) --> @nalift([1,2,5,5])
    @fact ifelse(DataCubes.AbstractArrayWrapper([true,true,false,false]), 5, @nalift([1,2,3,4])) --> @nalift([5,5,3,4])
    @fact ifelse(DataCubes.AbstractArrayWrapper([true,true,false,false]), 5, [1,2,3,4]) --> @nalift([5,5,3,4])
    @fact ifelse(DataCubes.AbstractArrayWrapper([true,true,false,false]), 5, AbstractArrayWrapper([1,2,3,4])) --> @nalift([5,5,3,4])
    @fact ifelse(DataCubes.AbstractArrayWrapper([true,true,false,false]), 1.0*nalift([1,2,3,4]), 5) --> 1.0*nalift([1,2,5,5])
    @fact ifelse(DataCubes.AbstractArrayWrapper([true,true,false,false]), 5, 1.0*nalift([1,2,3,4])) --> 1.0*nalift([5,5,3,4])
    @fact ifelse(DataCubes.AbstractArrayWrapper([true,true,false,false]), @nalift([1,2,3,4]), 5) --> @nalift([1,2,5,5])
    @fact ifelse(DataCubes.AbstractArrayWrapper([true,true,false,false]), Nullable(5), @nalift([1,2,3,4])) --> @nalift([5,5,3,4])
    @fact ifelse(DataCubes.AbstractArrayWrapper([true,true,false,false]), Nullable(5), [1,2,3,4]) --> @nalift([5,5,3,4])
    @fact ifelse(DataCubes.AbstractArrayWrapper([true,true,false,false]), Nullable(5), AbstractArrayWrapper([1,2,3,4])) --> @nalift([5,5,3,4])
    @fact ifelse(DataCubes.AbstractArrayWrapper([true,true,false,false]), 1.0*nalift([1,2,3,4]), Nullable(5)) --> 1.0*nalift([1,2,5,5])
    @fact ifelse(DataCubes.AbstractArrayWrapper([true,true,false,false]), 1.0*[1,2,3,4], Nullable(5)) --> 1.0*nalift([1,2,5,5])
    @fact ifelse(DataCubes.AbstractArrayWrapper([true,true,false,false]), 1.0*AbstractArrayWrapper([1,2,3,4]), Nullable(5)) --> 1.0*nalift([1,2,5,5])
    @fact ifelse(DataCubes.AbstractArrayWrapper([true,true,false,false]), Nullable(5), 1.0*nalift([1,2,3,4])) --> 1.0*nalift([5,5,3,4])
    @fact ifelse(DataCubes.AbstractArrayWrapper([true,true,false,false]), [1,2,3,4], Nullable(5)) --> @nalift([1,2,5,5])
    @fact ifelse(DataCubes.AbstractArrayWrapper([true,true,false,false]), 5, [1,2,3,4]) --> @nalift([5,5,3,4])
    @fact ifelse(DataCubes.AbstractArrayWrapper([true,true,false,false]), 1.0*[1,2,3,4], 5) --> 1.0*nalift([1,2,5,5])
    @fact ifelse(DataCubes.AbstractArrayWrapper([true,true,false,false]), 5, 1.0*[1,2,3,4]) --> 1.0*nalift([5,5,3,4])
    @fact ifelse(DataCubes.AbstractArrayWrapper([true,true,false,false]), [1,2,3,4], Nullable(5)) --> @nalift([1,2,5,5])
    @fact ifelse(DataCubes.AbstractArrayWrapper([true,true,false,false]), Nullable(5), [1,2,3,4]) --> @nalift([5,5,3,4])
    @fact ifelse(DataCubes.AbstractArrayWrapper([true,true,false,false]), 1.0*[1,2,3,4], Nullable(5)) --> 1.0*nalift([1,2,5,5])
    @fact ifelse(DataCubes.AbstractArrayWrapper([true,true,false,false]), Nullable(5), 1.0*[1,2,3,4]) --> 1.0*nalift([5,5,3,4])

    @fact ifelse(true, @nalift([1,2,3,4]), 5) --> @nalift([1,2,3,4])
    @fact ifelse(true, AbstractArrayWrapper([1,2,3,4]), 5) --> @nalift([1,2,3,4])
    @fact ifelse(Nullable(true), @nalift([1,2,3,4]), 5) --> @nalift([1,2,3,4])
    @fact ifelse(false, @nalift([1,2,3,4]), 5) --> @nalift([5,5,5,5])
    @fact ifelse(Nullable(false), @nalift([1,2,3,4]), 5) --> @nalift([5,5,5,5])
    @fact ifelse(Nullable(false), AbstractArrayWrapper([1,2,3,4]), 5) --> @nalift([5,5,5,5])
    @fact ifelse(Nullable{Bool}(), @nalift([1,2,3,4]), 5) --> @nalift(fill(Nullable{Int}(),4))
    @fact ifelse(true, 5, @nalift([1,2,3,4])) --> @nalift([5,5,5,5])
    @fact ifelse(Nullable(true), 5, @nalift([1,2,3,4])) --> @nalift([5,5,5,5])
    @fact ifelse(false, 5, @nalift([1,2,3,4])) --> @nalift([1,2,3,4])
    @fact ifelse(false, 5, wrap_array([1,2,3,4])) --> @nalift([1,2,3,4])
    @fact ifelse(Nullable(false), 5, @nalift([1,2,3,4])) --> @nalift([1,2,3,4])
    @fact ifelse(Nullable(false), 5, wrap_array([1,2,3,4])) --> @nalift([1,2,3,4])
    @fact ifelse(Nullable{Bool}(), 5, @nalift([1,2,3,4])) --> @nalift(fill(Nullable{Int}(),4))
    @fact ifelse(true, 1.0*nalift([1,2,3,4]), 5) --> 1.0*@nalift([1,2,3,4])
    @fact ifelse(Nullable(true), 1.0*nalift([1,2,3,4]), 5) --> 1.0*@nalift([1,2,3,4])
    @fact ifelse(false, 1.0*nalift([1,2,3,4]), 5) --> @nalift([5,5,5,5])
    @fact ifelse(Nullable(false), 1.0*nalift([1,2,3,4]), 5) --> @nalift([5,5,5,5])
    @fact ifelse(Nullable{Bool}(), 1.0*nalift([1,2,3,4]), 5) --> @nalift(fill(Nullable{Float64}(),4))
    @fact ifelse(true, 5, 1.0*nalift([1,2,3,4])) --> @nalift([5,5,5,5])
    @fact ifelse(Nullable(true), 5, 1.0*nalift([1,2,3,4])) --> @nalift([5,5,5,5])
    @fact ifelse(false, 5, 1.0*nalift([1,2,3,4])) --> 1.0*nalift([1,2,3,4])
    @fact ifelse(Nullable(false), 5, 1.0*nalift([1,2,3,4])) --> 1.0*nalift([1,2,3,4])
    @fact ifelse(Nullable{Bool}(), 5, 1.0*nalift([1,2,3,4])) --> 1.0*nalift(fill(Nullable{Int}(), 4))
    @fact ifelse(true, Nullable(5), @nalift([1,2,3,4])) --> @nalift([5,5,5,5])
    @fact ifelse(Nullable(true), Nullable(5), @nalift([1,2,3,4])) --> @nalift([5,5,5,5])
    @fact ifelse(false, Nullable(5), @nalift([1,2,3,4])) --> @nalift([1,2,3,4])
    @fact ifelse(Nullable(false), Nullable(5), @nalift([1,2,3,4])) --> @nalift([1,2,3,4])
    @fact ifelse(Nullable{Bool}(), Nullable(5), @nalift([1,2,3,4])) --> @nalift(fill(Nullable{Int}(),4))
    @fact ifelse(true, 1.0*nalift([1,2,3,4]), Nullable(5)) --> 1.0*nalift([1,2,3,4])
    @fact ifelse(Nullable(true), 1.0*nalift([1,2,3,4]), Nullable(5)) --> 1.0*nalift([1,2,3,4])
    @fact ifelse(false, 1.0*nalift([1,2,3,4]), Nullable(5)) --> @nalift([5,5,5,5])
    @fact ifelse(Nullable(false), 1.0*nalift([1,2,3,4]), Nullable(5)) --> @nalift([5,5,5,5])
    @fact ifelse(Nullable{Bool}(), 1.0*nalift([1,2,3,4]), Nullable(5)) --> 1.0*@nalift(fill(Nullable{Int}(),4))
    @fact ifelse(true, Nullable(5), 1.0*nalift([1,2,3,4])) --> @nalift([5,5,5,5])
    @fact ifelse(Nullable(true), Nullable(5), 1.0*nalift([1,2,3,4])) --> @nalift([5,5,5,5])
    @fact ifelse(false, Nullable(5), 1.0*nalift([1,2,3,4])) --> 1.0*nalift([1,2,3,4])
    @fact ifelse(Nullable(false), Nullable(5), 1.0*nalift([1,2,3,4])) --> 1.0*nalift([1,2,3,4])
    @fact ifelse(Nullable{Bool}(), Nullable(5), 1.0*nalift([1,2,3,4])) --> 1.0*nalift(fill(Nullable{Int}(),4))
    @fact ifelse(true, 5, DataCubes.AbstractArrayWrapper([1,2,3,4])) --> @nalift([5,5,5,5])
    @fact ifelse(Nullable(true), 5, DataCubes.AbstractArrayWrapper([1,2,3,4])) --> @nalift([5,5,5,5])
    @fact ifelse(false, 5, DataCubes.AbstractArrayWrapper([1,2,3,4])) --> @nalift([1,2,3,4])
    @fact ifelse(Nullable(false), 5, DataCubes.AbstractArrayWrapper([1,2,3,4])) --> @nalift([1,2,3,4])
    @fact ifelse(Nullable{Bool}(), 5, DataCubes.AbstractArrayWrapper([1,2,3,4])) --> @nalift(fill(Nullable{Int}(),4))
    @fact ifelse(true, DataCubes.AbstractArrayWrapper([1,2,3,4]), Nullable(5)) --> @nalift([1,2,3,4])
    @fact ifelse(Nullable(true), DataCubes.AbstractArrayWrapper([1,2,3,4]), Nullable(5)) --> @nalift([1,2,3,4])
    @fact ifelse(false, Nullable(5), DataCubes.AbstractArrayWrapper([1,2,3,4])) --> @nalift([1,2,3,4])
    @fact ifelse(Nullable(false), Nullable(5), DataCubes.AbstractArrayWrapper([1,2,3,4])) --> @nalift([1,2,3,4])
    @fact ifelse(Nullable{Bool}(), Nullable(5), DataCubes.AbstractArrayWrapper([1,2,3,4])) --> @nalift(fill(Nullable{Int}(),4))
    @fact ifelse(true, DataCubes.AbstractArrayWrapper(1.0*[1,2,3,4]), Nullable(5)) --> 1.0*nalift([1,2,3,4])
    @fact ifelse(Nullable(true), DataCubes.AbstractArrayWrapper(1.0*[1,2,3,4]), Nullable(5)) --> 1.0*nalift([1,2,3,4])
    @fact ifelse(false, DataCubes.AbstractArrayWrapper(1.0*[1,2,3,4]), Nullable(5)) --> @nalift([5,5,5,5])
    @fact ifelse(Nullable(false), DataCubes.AbstractArrayWrapper(1.0*[1,2,3,4]), Nullable(5)) --> @nalift([5,5,5,5])
    @fact ifelse(Nullable{Bool}(), DataCubes.AbstractArrayWrapper(1.0*[1,2,3,4]), Nullable(5)) --> @nalift(fill(Nullable{Int}(),4))

    @fact ifelse(true, @nalift([1,2,NA,NA,5]), @nalift([10,NA,8,NA,6])) --> @nalift([1,2,NA,NA,5])
    @fact ifelse(Nullable(true), @nalift([1,2,NA,NA,5]), @nalift([10,NA,8,NA,6])) --> @nalift([1,2,NA,NA,5])
    @fact ifelse(false, @nalift([1,2,NA,NA,5]), @nalift([10,NA,8,NA,6])) --> @nalift([10,NA,8,NA,6])
    @fact ifelse(Nullable(false), @nalift([1,2,NA,NA,5]), @nalift([10,NA,8,NA,6])) --> @nalift([10,NA,8,NA,6])
    @fact ifelse(Nullable{Bool}(), @nalift([1,2,NA,NA,5]), @nalift([10,NA,8,NA,6])) --> @nalift(fill(Nullable{Int}(),5))
    @fact ifelse(DataCubes.AbstractArrayWrapper([true,false,true,false,true]), @nalift([1,2,NA,NA,5]), @nalift([10,NA,8,NA,6])) --> @nalift([1,NA,NA,NA,5])
    @fact ifelse(@nalift([true,NA,true,false,NA]), @nalift([1,2,NA,NA,5]), @nalift([10,NA,8,NA,6])) --> @nalift([1,NA,NA,NA,NA])

    @fact ifelse(true, @nalift([1,2,NA,NA,5]), AbstractArrayWrapper([10,9,8,7,6])) --> @nalift([1,2,NA,NA,5])
    @fact ifelse(true, @nalift([1,2,NA,NA,5]), [10,9,8,7,6]) --> @nalift([1,2,NA,NA,5])
    @fact ifelse(Nullable(true), @nalift([1,2,NA,NA,5]), AbstractArrayWrapper([10,9,8,7,6])) --> @nalift([1,2,NA,NA,5])
    @fact ifelse(Nullable(true), @nalift([1,2,NA,NA,5]), [10,9,8,7,6]) --> @nalift([1,2,NA,NA,5])
    @fact ifelse(false, @nalift([1,2,NA,NA,5]), AbstractArrayWrapper([10,9,8,7,6])) --> @nalift([10,9,8,7,6])
    @fact ifelse(Nullable(false), @nalift([1,2,NA,NA,5]), AbstractArrayWrapper([10,9,8,7,6])) --> @nalift([10,9,8,7,6])
    @fact ifelse(Nullable{Bool}(), @nalift([1,2,NA,NA,5]), AbstractArrayWrapper([10,9,8,7,6])) --> @nalift(fill(Nullable{Int}(),4))
    @fact ifelse(AbstractArrayWrapper([true,false,true,false,true]), @nalift([1,2,NA,NA,5]), AbstractArrayWrapper([10,9,8,7,6])) --> @nalift([1,9,NA,7,5])
    @fact ifelse(@nalift([true,NA,true,false,true]), @nalift([1,2,NA,NA,5]), AbstractArrayWrapper([10,9,8,7,6])) --> @nalift([1,NA,NA,7,5])

    @fact ifelse(true, AbstractArrayWrapper([1,2,3,4,5]), @nalift([10,NA,8,NA,6])) --> @nalift([1,2,3,4,5])
    @fact ifelse(true, [1,2,3,4,5], @nalift([10,NA,8,NA,6])) --> @nalift([1,2,3,4,5])
    @fact ifelse(Nullable(true), AbstractArrayWrapper([1,2,3,4,5]), @nalift([10,NA,8,NA,6])) --> @nalift([1,2,3,4,5])
    @fact ifelse(Nullable(true), [1,2,3,4,5], @nalift([10,NA,8,NA,6])) --> @nalift([1,2,3,4,5])
    @fact ifelse(false, AbstractArrayWrapper([1,2,3,4,5]), @nalift([10,NA,8,NA,6])) --> @nalift([10,NA,8,NA,6])
    @fact ifelse(Nullable(false), AbstractArrayWrapper([1,2,3,4,5]), @nalift([10,NA,8,NA,6])) --> @nalift([10,NA,8,NA,6])
    @fact ifelse(Nullable{Bool}(), AbstractArrayWrapper([1,2,3,4,5]), @nalift([10,NA,8,NA,6])) --> @nalift(fill(Nullable{Int}(),4))
    @fact ifelse(AbstractArrayWrapper([true,false,true,false,true]), AbstractArrayWrapper([1,2,3,4,5]), @nalift([10,NA,8,NA,6])) --> @nalift([1,NA,3,NA,5])
    @fact ifelse(AbstractArrayWrapper([true,false,true,false,true]), AbstractArrayWrapper([1,2,3,4,5]), [10,9,8,7,6]) --> @nalift([1,9,3,7,5])
    @fact ifelse(AbstractArrayWrapper([true,false,true,false,true]), [10,9,8,7,6], AbstractArrayWrapper([1,2,3,4,5])) --> @nalift([10,2,8,4,6])
    @fact ifelse(AbstractArrayWrapper([true,false,true,false,true]), [10,9,8,7,6], nalift([1,2,3,4,5])) --> @nalift([10,2,8,4,6])
    @fact ifelse(AbstractArrayWrapper([true,false,true,false,true]), [10,9,8,7,6], [1,2,3,4,5]) --> @nalift([10,2,8,4,6])
    @fact ifelse(AbstractArrayWrapper([true,false,true,false,true]), nalift([10,9,8,7,6]), [1,2,3,4,5]) --> @nalift([10,2,8,4,6])
    @fact ifelse(@nalift([NA,NA,true,false,true]), AbstractArrayWrapper([1,2,3,4,5]), @nalift([10,NA,8,NA,6])) --> @nalift([NA,NA,3,NA,5])

    @fact ifelse(true, AbstractArrayWrapper([1,2,3,4,5]), AbstractArrayWrapper([10,9,8,7,6])) --> @nalift([1,2,3,4,5])
    @fact ifelse(true, AbstractArrayWrapper([1,2,3,4,5]), [10,9,8,7,6]) --> @nalift([1,2,3,4,5])
    @fact ifelse(true, [1,2,3,4,5], AbstractArrayWrapper([10,9,8,7,6])) --> @nalift([1,2,3,4,5])
    @fact ifelse(Nullable(true), AbstractArrayWrapper([1,2,3,4,5]), AbstractArrayWrapper([10,9,8,7,6])) --> @nalift([1,2,3,4,5])
    @fact ifelse(false, AbstractArrayWrapper([1,2,3,4,5]), AbstractArrayWrapper([10,9,8,7,6])) --> @nalift([10,9,8,7,6])
    @fact ifelse(Nullable(false), AbstractArrayWrapper([1,2,3,4,5]), AbstractArrayWrapper([10,9,8,7,6])) --> @nalift([10,9,8,7,6])
    @fact ifelse(Nullable{Bool}(), AbstractArrayWrapper([1,2,3,4,5]), AbstractArrayWrapper([10,9,8,7,6])) --> @nalift(fill(Nullable{Int}(),4))
    @fact ifelse(AbstractArrayWrapper([true,false,true,false,true]), AbstractArrayWrapper([1,2,3,4,5]), AbstractArrayWrapper([10,9,8,7,6])) --> @nalift([1,9,3,7,5])
    @fact ifelse(@nalift([true,NA,true,false,true]), AbstractArrayWrapper([1,2,3,4,5]), AbstractArrayWrapper([10,9,8,7,6])) --> @nalift([1,NA,3,7,5])
  end
end

end


using Keras
using Flux
using PyCall
@pyimpory numpy

X = numpy.load("X.npy")
Y = numpy.load("Y.npy")

model = Keras.load("structure.json", "weights.h5")

examples/sentiment analysis using glove embeddings/main.jl


# AtmosLESDefaultPerturbations
#
# Computes perturbations from the horizontal averages for various
# fields.

using ..Atmos
using ..Mesh.Topologies
using ..Mesh.Grids
using Thermodynamics

"""
    setup_atmos_default_perturbations(
        ::AtmosLESConfigType,
        interval::String,
        out_prefix::String;
        writer::AbstractWriter,
        interpol = nothing,
    )

Create the "AtmosLESDefaultPerturbations" `DiagnosticsGroup` which
contains the following diagnostic variables, all of which are
perturbations from the horizontal average of that variable.

- u_prime
- v_prime
- w_prime
- avg_rho_prime
- temp_prime
- pres_prime
- thd_prime
- et_prime
- ei_prime
- ht_prime
- hi_prime

When an `EquilMoist` moisture model is used, the following additional
diagnostic variables are also output:

- qt_prime
- ql_prime
- qv_prime
- thv_prime
- thl_prime

The perturbations are output on `x`, `y`, `z` dimensions of an
interpolated grid (`interpol` _must_ be specified) and a (unlimited)
`time` dimension at the specified `interval`.
"""
function setup_atmos_default_perturbations(
    ::AtmosLESConfigType,
    interval::String,
    out_prefix::String;
    writer = NetCDFWriter(),
    interpol = nothing,
)
    # TODO: remove this
    @assert !isnothing(interpol)

    return DiagnosticsGroup(
        "AtmosLESDefaultPerturbations",
        Diagnostics.atmos_les_default_perturbations_init,
        Diagnostics.atmos_les_default_perturbations_fini,
        Diagnostics.atmos_les_default_perturbations_collect,
        interval,
        out_prefix,
        writer,
        interpol,
    )
end

# Compute sums for density-averaged horizontal averages
#
# These are trimmed down versions of `atmos_les_default_simple_sums!()`:
# - operate on interpolated grid (no `MH` scaling)
# - skip `w_ht_sgs` and `w_qt_sgs`
function atmos_les_default_perturbations_sums!(
    atmos::AtmosModel,
    state,
    thermo,
    sums,
)
    sums.u += state.ρu[1]
    sums.v += state.ρu[2]
    sums.w += state.ρu[3]
    sums.avg_rho += state.ρ
    sums.rho += state.ρ * state.ρ
    sums.temp += thermo.temp * state.ρ
    sums.pres += thermo.pres * state.ρ
    sums.thd += thermo.θ_dry * state.ρ
    sums.et += state.energy.ρe
    sums.ei += thermo.e_int * state.ρ
    sums.ht += thermo.h_tot * state.ρ
    sums.hi += thermo.h_int * state.ρ

    atmos_les_default_perturbations_sums!(
        moisture_model(atmos),
        state,
        thermo,
        sums,
    )

    return nothing
end
function atmos_les_default_perturbations_sums!(
    ::AbstractMoistureModel,
    state,
    thermo,
    sums,
)
    return nothing
end
function atmos_les_default_perturbations_sums!(
    moist::EquilMoist,
    state,
    thermo,
    sums,
)
    sums.moisture.qt += state.moisture.ρq_tot
    sums.moisture.ql += thermo.moisture.q_liq * state.ρ
    sums.moisture.qv += thermo.moisture.q_vap * state.ρ
    sums.moisture.thv += thermo.moisture.θ_vir * state.ρ
    sums.moisture.thl += thermo.moisture.θ_liq_ice * state.ρ

    return nothing
end

# Perturbations from horizontal averages
function vars_atmos_les_default_perturbations(m::AtmosModel, FT)
    @vars begin
        u_prime::FT
        v_prime::FT
        w_prime::FT
        avg_rho_prime::FT             # ρ
        temp_prime::FT
        pres_prime::FT
        thd_prime::FT                 # θ_dry
        et_prime::FT                  # e_tot
        ei_prime::FT                  # e_int
        ht_prime::FT
        hi_prime::FT

        moisture::vars_atmos_les_default_perturbations(moisture_model(m), FT)
    end
end
vars_atmos_les_default_perturbations(::AbstractMoistureModel, FT) = @vars()
function vars_atmos_les_default_perturbations(m::EquilMoist, FT)
    @vars begin
        qt_prime::FT                  # q_tot
        ql_prime::FT                  # q_liq
        qv_prime::FT                  # q_vap
        thv_prime::FT                 # θ_vir
        thl_prime::FT                 # θ_liq
    end
end
num_atmos_les_default_perturbation_vars(m, FT) =
    varsize(vars_atmos_les_default_perturbations(m, FT))
atmos_les_default_perturbation_vars(m, array) =
    Vars{vars_atmos_les_default_perturbations(m, eltype(array))}(array)

# Compute the perturbations from horizontal averages
function atmos_les_default_perturbations!(
    atmos::AtmosModel,
    state,
    thermo,
    ha,
    vars,
)
    u = state.ρu[1] / state.ρ
    vars.u_prime = u - ha.u
    v = state.ρu[2] / state.ρ
    vars.v_prime = v - ha.v
    w = state.ρu[3] / state.ρ
    vars.w_prime = w - ha.w
    vars.avg_rho_prime = state.ρ - ha.avg_rho
    vars.temp_prime = thermo.temp - ha.temp
    vars.pres_prime = thermo.pres - ha.pres
    vars.thd_prime = thermo.θ_dry - ha.thd
    et = state.energy.ρe / state.ρ
    vars.et_prime = et - ha.et
    vars.ei_prime = thermo.e_int - ha.ei
    vars.ht_prime = thermo.h_tot - ha.ht
    vars.hi_prime = thermo.h_int - ha.hi

    atmos_les_default_perturbations!(
        moisture_model(atmos),
        atmos,
        state,
        thermo,
        ha,
        vars,
    )

    return nothing
end
function atmos_les_default_perturbations!(
    ::AbstractMoistureModel,
    ::AtmosModel,
    state,
    thermo,
    ha,
    vars,
)
    return nothing
end
function atmos_les_default_perturbations!(
    m::EquilMoist,
    atmos::AtmosModel,
    state,
    thermo,
    ha,
    vars,
)
    qt = state.moisture.ρq_tot / state.ρ
    vars.moisture.qt_prime = qt - ha.moisture.qt
    vars.moisture.ql_prime = thermo.moisture.q_liq - ha.moisture.ql
    vars.moisture.qv_prime = thermo.moisture.q_vap - ha.moisture.qv
    vars.moisture.thv_prime = thermo.moisture.θ_vir - ha.moisture.thv
    vars.moisture.thl_prime = thermo.moisture.θ_liq_ice - ha.moisture.thl

    return nothing
end

function atmos_les_default_perturbations_init(
    dgngrp::DiagnosticsGroup,
    currtime,
)
    FT = eltype(Settings.Q)
    atmos = Settings.dg.balance_law
    mpicomm = Settings.mpicomm
    mpirank = MPI.Comm_rank(mpicomm)

    if !(dgngrp.interpol isa InterpolationBrick)
        @warn """
            Diagnostics $(dgngrp.name): requires `InterpolationBrick`!
            """
        return nothing
    end

    if mpirank == 0
        # get dimensions for the interpolated grid
        dims = dimensions(dgngrp.interpol)

        # set up the variables we're going to be writing
        vars = OrderedDict()
        varnames = map(
            s -> startswith(s, "moisture.") ? s[10:end] : s,
            flattenednames(vars_atmos_les_default_perturbations(atmos, FT)),
        )
        for varname in varnames
            vars[varname] = (tuple(collect(keys(dims))...), FT, OrderedDict())
        end

        # create the output file
        dprefix = @sprintf("%s_%s", dgngrp.out_prefix, dgngrp.name)
        dfilename = joinpath(Settings.output_dir, dprefix)
        noov = Settings.no_overwrite
        init_data(dgngrp.writer, dfilename, noov, dims, vars)
    end

    return nothing
end

"""
    atmos_les_default_perturbations_collect(dgngrp, currtime)

Perform a global grid traversal to compute various diagnostics.
"""
function atmos_les_default_perturbations_collect(
    dgngrp::DiagnosticsGroup,
    currtime,
)
    interpol = dgngrp.interpol
    if !(interpol isa InterpolationBrick)
        @warn """
            Diagnostics $(dgngrp.name): requires `InterpolationBrick`!
            """
        return nothing
    end

    mpicomm = Settings.mpicomm
    dg = Settings.dg
    Q = Settings.Q
    mpirank = MPI.Comm_rank(mpicomm)
    atmos = dg.balance_law
    grid = dg.grid
    grid_info = basic_grid_info(dg)
    topl_info = basic_topology_info(grid.topology)
    Nqk = grid_info.Nqk
    Nqh = grid_info.Nqh
    npoints = prod(grid_info.Nq)
    nrealelem = topl_info.nrealelem
    nvertelem = topl_info.nvertelem
    nhorzelem = topl_info.nhorzrealelem

    # get needed arrays onto the CPU
    if array_device(Q) isa CPU
        ArrayType = Array
        state_data = Q.realdata
        aux_data = dg.state_auxiliary.realdata
    else
        ArrayType = CuArray
        state_data = Array(Q.realdata)
        aux_data = Array(dg.state_auxiliary.realdata)
    end
    FT = eltype(state_data)

    # Compute thermo variables
    thermo_array = Array{FT}(undef, npoints, num_thermo(atmos, FT), nrealelem)
    @traverse_dg_grid grid_info topl_info begin
        state = extract_state(dg, state_data, ijk, e, Prognostic())
        aux = extract_state(dg, aux_data, ijk, e, Auxiliary())

        thermo = thermo_vars(atmos, view(thermo_array, ijk, :, e))
        compute_thermo!(atmos, state, aux, thermo)
    end

    # Interpolate the state and thermo variables.
    interpol = dgngrp.interpol
    istate =
        ArrayType{FT}(undef, interpol.Npl, number_states(atmos, Prognostic()))
    interpolate_local!(interpol, Q.realdata, istate)

    ithermo = ArrayType{FT}(undef, interpol.Npl, num_thermo(atmos, FT))
    interpolate_local!(interpol, ArrayType(thermo_array), ithermo)

    # FIXME: accumulating to rank 0 is not scalable
    all_state_data = accumulate_interpolated_data(mpicomm, interpol, istate)
    all_thermo_data = accumulate_interpolated_data(mpicomm, interpol, ithermo)

    if mpirank == 0
        # get dimensions for the interpolated grid
        dims = dimensions(interpol)

        nx = length(dims["x"][1])
        ny = length(dims["y"][1])
        nz = length(dims["z"][1])

        # collect horizontal sums
        simple_sums = [
            zeros(FT, num_atmos_les_default_simple_vars(atmos, FT))
            for _ in 1:nz
        ]
        @traverse_interpolated_grid nx ny nz begin
            statei = Vars{vars_state(atmos, Prognostic(), FT)}(view(
                all_state_data,
                lo,
                la,
                le,
                :,
            ))
            thermoi = thermo_vars(atmos, view(all_thermo_data, lo, la, le, :))
            simple = atmos_les_default_simple_vars(atmos, simple_sums[le])
            atmos_les_default_perturbations_sums!(
                atmos,
                statei,
                thermoi,
                simple,
            )
        end

        # compute horizontal averages
        simple_avgs = [
            zeros(FT, num_atmos_les_default_simple_vars(atmos, FT))
            for _ in 1:nz
        ]
        for le in 1:nz
            simple_avgs[le] .= simple_sums[le] ./ (nx * ny)
        end

        # complete density averaging
        simple_varnames = map(
            s -> startswith(s, "moisture.") ? s[10:end] : s,
            flattenednames(vars_atmos_les_default_simple(atmos, FT)),
        )
        for vari in 1:length(simple_varnames)
            for le in 1:nz
                ha = atmos_les_default_simple_vars(atmos, simple_avgs[le])
                avg_rho = ha.avg_rho
                if simple_varnames[vari] != "avg_rho"
                    simple_avgs[le][vari] /= avg_rho
                end
            end
        end

        # now compute the perturbations from the horizontal averages
        perturbations_array = Array{FT}(
            undef,
            nx,
            ny,
            nz,
            num_atmos_les_default_perturbation_vars(atmos, FT),
        )
        @traverse_interpolated_grid nx ny nz begin
            statei = Vars{vars_state(atmos, Prognostic(), FT)}(view(
                all_state_data,
                lo,
                la,
                le,
                :,
            ))
            thermoi = thermo_vars(atmos, view(all_thermo_data, lo, la, le, :))
            ha = atmos_les_default_simple_vars(atmos, simple_avgs[le])
            perturbations = atmos_les_default_perturbation_vars(
                atmos,
                view(perturbations_array, lo, la, le, :),
            )
            atmos_les_default_perturbations!(
                atmos,
                statei,
                thermoi,
                ha,
                perturbations,
            )
        end

        # prepare and write out the perturbations
        varvals = OrderedDict()
        varnames = map(
            s -> startswith(s, "moisture.") ? s[10:end] : s,
            flattenednames(vars_atmos_les_default_perturbations(atmos, FT)),
        )
        for (vari, varname) in enumerate(varnames)
            varvals[varname] = perturbations_array[:, :, :, vari]
        end

        # write output
        append_data(dgngrp.writer, varvals, currtime)
    end

    MPI.Barrier(mpicomm)
    return nothing
end # function collect

function atmos_les_default_perturbations_fini(
    dgngrp::DiagnosticsGroup,
    currtime,
) end


# Julia interface when not defined in source files

Base.eltype(x::DistMultiVec{T}) where {T} = T

function Base.size(A::ElementalMatrix, i::Integer)
    if i < 1
        error("dimension out of range")
    elseif i == 1
        return Int(height(A))
    elseif i == 2
        return Int(width(A))
    else
        return 1
    end
end

Base.size(A::ElementalMatrix) = (size(A, 1), size(A, 2))

Base.copyto!(A::T, B::T) where {T<:ElementalMatrix} = _copy!(B, A)
# copy(A::ElementalMatrix) = copy!(similar(A), A)
Base.length(A::ElementalMatrix) = prod(size(A))

## Current mutating Julia multiplication API
LinearAlgebra.mul!(C::T, A::T, B::T) where {T<:ElementalMatrix} = gemm(NORMAL, NORMAL, one(eltype(T)), A, B, zero(eltype(T)), C)
LinearAlgebra.mul!(C::T, adjA::Adjoint{<:Any,T}, B::T) where {T<:ElementalMatrix} = gemm(ADJOINT, NORMAL, one(eltype(T)), parent(adjA), B, zero(eltype(T)), C)
LinearAlgebra.mul!(C::T, trA::Transpose{<:Any,T}, B::T) where {T<:ElementalMatrix} = gemm(TRANSPOSE, NORMAL, one(eltype(T)), parent(trA), B, zero(eltype(T)), C)
LinearAlgebra.mul!(C::T, A::T, adjB::Adjoint{<:Any,T}) where {T<:ElementalMatrix} = gemm(NORMAL, ADJOINT, one(eltype(T)), A, parent(adjB), zero(eltype(T)), C)
LinearAlgebra.mul!(C::T, A::T, trB::Transpose{<:Any,T}) where {T<:ElementalMatrix} = gemm(NORMAL, TRANSPOSE, one(eltype(T)), A, parent(trB), zero(eltype(T)), C)

## BLAS like multiplication API (i.e. with α and β)
LinearAlgebra.mul!(C::S, A::S, B::S, α::Number, β::Number) where {S<:ElementalMatrix{T}} where {T} =
    gemm(NORMAL, NORMAL, T(α), A, B, T(β), C)
LinearAlgebra.mul!(C::S, adjA::Adjoint{<:Any,S}, B::S, α::Number, β::Number) where {S<:ElementalMatrix{T}} where {T} =
    gemm(ADJOINT, NORMAL, T(α), parent(adjA), B, T(β), C)
LinearAlgebra.mul!(C::S, trA::Transpose{<:Any,S}, B::S, α::Number, β::Number) where {S<:ElementalMatrix{T}} where {T} =
    gemm(TRANSPOSE, NORMAL, T(α), parent(trA), B, T(β), C)
LinearAlgebra.mul!(C::S, A::S, adjB::Adjoint{<:Any,S}, α::Number, β::Number) where {S<:ElementalMatrix{T}} where {T} =
    gemm(NORMAL, ADJOINT, T(α), A, parent(adjB), T(β), C)
LinearAlgebra.mul!(C::S, A::S, trB::Transpose{<:Any,S}, α::Number, β::Number) where {S<:ElementalMatrix{T}} where {T} =
    gemm(NORMAL, TRANSPOSE, T(α), A, parent(trB), T(β), C)

## Linear solve API
LinearAlgebra.ldiv!(A::LowerTriangular{T,S}, B::S)  where {T,S<:ElementalMatrix} =
    trsm(LEFT, LOWER, NORMAL   , NON_UNIT, one(T), A.data, B)
LinearAlgebra.ldiv!(adjA::Adjoint{T,LowerTriangular{T,S}}, B::S) where {T,S<:ElementalMatrix} =
    trsm(LEFT, LOWER, ADJOINT  , NON_UNIT, one(T), parent(adjA).data, B)
LinearAlgebra.ldiv!(trA::Transpose{T,LowerTriangular{T,S}}, B::S) where {T,S<:ElementalMatrix} =
    trsm(LEFT, LOWER, TRANSPOSE, NON_UNIT, one(T), parent(trA).data, B)
LinearAlgebra.ldiv!(A::UpperTriangular{T,S}, B::S)  where {T,S<:ElementalMatrix} =
    trsm(LEFT, UPPER, NORMAL   , NON_UNIT, one(T), A.data, B)
LinearAlgebra.ldiv!(adjA::Adjoint{T,UpperTriangular{T,S}}, B::S) where {T,S<:ElementalMatrix} =
    trsm(LEFT, UPPER, ADJOINT  , NON_UNIT, one(T), parent(adjA).data, B)
LinearAlgebra.ldiv!(trA::Transpose{T,UpperTriangular{T,S}}, B::S) where {T,S<:ElementalMatrix} =
    trsm(LEFT, UPPER, TRANSPOSE, NON_UNIT, one(T), parent(trA).data, B)

LinearAlgebra.rdiv!(A::S, B::LowerTriangular{T,S})  where {T,S<:ElementalMatrix} =
    trsm(RIGHT, LOWER, NORMAL   , NON_UNIT, one(T), B.data, A)
LinearAlgebra.rdiv!(A::S, adjB::Adjoint{T,LowerTriangular{T,S}}) where {T,S<:ElementalMatrix} =
    trsm(RIGHT, LOWER, ADJOINT  , NON_UNIT, one(T), parent(adjB).data, A)
LinearAlgebra.rdiv!(A::S, trB::Transpose{T,LowerTriangular{T,S}}) where {T,S<:ElementalMatrix} =
    trsm(RIGHT, LOWER, TRANSPOSE, NON_UNIT, one(T), B.data, A)
LinearAlgebra.rdiv!(A::S, B::UpperTriangular{T,S})  where {T,S<:ElementalMatrix} =
    trsm(RIGHT, UPPER, NORMAL   , NON_UNIT, one(T), B.data, A)
LinearAlgebra.rdiv!(A::S, adjB::Adjoint{T,UpperTriangular{T,S}}) where {T,S<:ElementalMatrix} =
    trsm(RIGHT, UPPER, ADJOINT  , NON_UNIT, one(T), parent(adjB).data, A)
LinearAlgebra.rdiv!(A::S, trB::Transpose{T,UpperTriangular{T,S}}) where {T,S<:ElementalMatrix} =
    trsm(RIGHT, UPPER, TRANSPOSE, NON_UNIT, one(T), parent(trB).data, A)

# Spectral
LinearAlgebra.svd(A::ElementalMatrix) = svd!(copy(A))
LinearAlgebra.svd(A::ElementalMatrix, ctrl::SVDCtrl) = svd!(copy(A), ctrl)
LinearAlgebra.svdvals(A::ElementalMatrix, ctrl::SVDCtrl) = svdvals!(copy(A), ctrl)

# conversions to and from julia arrays

# function copy!(dest::Matrix{T}, src::Base.VecOrMat{T}) where {T}
#     m, n = size(src, 1), size(src, 2)
#     resize!(dest, m, n)
#     Base.unsafe_copy!(pointer(dest), pointer(src), m*n)
#     return dest
# end
# function copy!(dest::Base.Matrix{T}, src::Matrix{T}) where {T}
#     m, n = size(dest)
#     if m != size(src, 1) || n != size(src, 2)
#         throw(DimensionMisMatch("source and destination must have same shape"))
#     end
#     Base.unsafe_copy!(pointer(dest), pointer(src), m*n)
#     return dest
# end

function Base.copyto!(dest::DistMatrix{T}, src::Base.AbstractVecOrMat) where {T}
    m, n = size(src, 1), size(src, 2)
    zeros!(dest, m, n)
    if MPI.commRank(comm(dest)) == 0
        for j = 1:n
            for i = 1:m
                queueUpdate(dest, i, j, src[i,j])
            end
        end
    end
    processQueues(dest)
    return dest
end

Base.copyto!(dest::DistMatrix, src::ElementalMatrix) = _copy!(src, dest)

function Base.copyto!(dest::Base.VecOrMat, src::DistMatrix{T}) where {T}
    m, n = size(src, 1), size(src, 2)
    # if MPI.commRank(comm(src)) == 0
        for j = 1:n
            for i = 1:m
                queuePull(src, i, j)
            end
        end
    # end
    dest_mat = ndims(dest) == 1 ? reshape(dest, :, 1) : dest
    processPullQueue(src, dest_mat)
    return dest
end

function Base.convert(::Type{Matrix{T}}, A::Base.VecOrMat{T}) where {T}
    m, n = size(A, 1), size(A, 2)
    B = Matrix(T)
    resize!(B, m, n)
    Base.unsafe_copyto!(pointer(B), pointer(A), m*n)
    return B
end
function Base.convert(::Type{Base.Matrix{T}}, A::Matrix{T}) where {T}
    m, n = size(A)
    B = Base.Matrix{T}(undef, m, n)
    Base.unsafe_copyto!(pointer(B), pointer(A), m*n)
    return B
end

function Base.convert(::Type{DistMatrix{T}}, A::Base.VecOrMat{T}) where {T}
    m, n = size(A, 1), size(A, 2)
    B = DistMatrix(T)
    zeros!(B, m, n)
    if MPI.commRank(comm(B)) == 0
        for j = 1:n
            for i = 1:m
                queueUpdate(B, i, j, A[i,j])
            end
        end
    end
    processQueues(B)
    return B
end

function Base.convert(::Type{DistMultiVec{T}}, A::Base.VecOrMat{T}) where {T}
    m, n = size(A, 1), size(A, 2)
    B = DistMultiVec(T)
    zeros!(B, m, n)
    if MPI.commRank(comm(B)) == 0
        for j = 1:n
            for i = 1:m
                queueUpdate(B, i, j, A[i,j])
            end
        end
    end
    processQueues(B)
    return B
end

function Base.convert(::Type{DistMatrix{T}}, A::DistMultiVec{T}) where {T}
    m, n = size(A)
    B = DistMatrix(T)
    zeros!(B, m, n)
    for j = 1:n
        for i = 1:localHeight(A)
            xij = getLocal(A, i, j)
            queueUpdate(B, globalRow(A, i), j, xij)
        end
    end
    processQueues(B)
    return B
end

Base.convert(::Type{Array}, xd::DistMatrix{T}) where {T} =
    copyto!(Base.zeros(T, size(xd)), xd)

Base.Array(xd::DistMatrix) = convert(Array, xd)

LinearAlgebra.norm(x::ElementalMatrix) = nrm2(x)
# function LinearAlgebra.norm(x::ElementalMatrix)
#     if size(x, 2) == 1
#         return nrm2(x)
#     else
#         return twoNorm(x)
#     end
# end

LinearAlgebra.cholesky!(A::Hermitian{<:Union{Real,Complex},<:ElementalMatrix}) = LinearAlgebra.Cholesky(_cholesky!(A.uplo == 'U' ? UPPER : LOWER, A.data), A.uplo, 0)
LinearAlgebra.cholesky(A::Hermitian{<:Union{Real,Complex},<:ElementalMatrix}) = cholesky!(copy(A))

LinearAlgebra.lu(A::ElementalMatrix) = _lu!(copy(A))

# Mixed multiplication with Julia Arrays
(*)(A::DistMatrix{T}, B::StridedVecOrMat{T}) where {T} = A*convert(DistMatrix{T}, B)
(*)(A::DistMultiVec{T}, B::StridedVecOrMat{T}) where {T} = convert(DistMatrix{T}, A)*convert(DistMatrix{T}, B)
(*)(A::DistSparseMatrix{T}, B::StridedVecOrMat{T}) where {T} = A*convert(DistMultiVec{T}, B)
(*)(A::Adjoint{T,DistMatrix{T}}, B::StridedVecOrMat{T}) where {T} = A*convert(DistMatrix{T}, B)
(*)(A::Adjoint{T,DistMultiVec{T}}, B::Base.VecOrMat{T}) where {T} = convert(DistMatrix{T}, parent(A))'*convert(DistMatrix{T}, B)
(*)(A::Adjoint{T,DistSparseMatrix{T}}, B::Base.VecOrMat{T}) where {T} = A*convert(DistMultiVec{T}, B)


# API demo
# This is a stripped-down version for the layout here: http://postimage.org/image/s6pskvknf/
# See scatter_image.jl for a more comprehensive implementation. This one
# is here to provide a relatively simple test-case for early development.
# It cuts several corners:
#    hard-codes the minimum space needed for the axis labels
#    uses pixels for all measurements

require("Layout")
using Layout

# Create a fake window
win = Window(0, 0, 800, 600)

# Create the overall layout inside win. This is specified in terms of named tabstops.
xtabs = [:scatterL, :scatterR, :margeEL, :margeER, :imageL, :imageR]
ytabs = [:margeNT, :margeNB, :scatterT, :scatterB, :buttonsT]
l = LayoutLP(win, xtabs, ytabs)

# Create a sub-layout for the buttons
# After the parent, the next two arguments refer to parent coordinates.
# The last two arguments introduce tab stops in the child
# lbuttons = LayoutLP(l, [:W, :E], [:buttonsT, :S], [:cancelL, :cancelR, :doneL], [])

# Specify image dimensions
imsz = [384,512]
imheight = () -> imsz[1]
imwidth = () -> imsz[2]
# These are only nominally hard-coding the image size; you could change
# imsz[1] or imsz[2] and it would update to the new dimensions.

# Now specify the geometry. This is done by adding constraints and penalties. Note that a "subplot" function would take care of many of these for you, but here we're going low-level because this GUI has a lot of custom requirements.

# First we require that the tabstops are in increasing order. One can pick out
# a subset of the tab stops and do something different for the rest
# (partial ordering)
increasing(l, xtabs)
increasing(l, ytabs)

# Now add particular constraints
addconstraints(l,
        # Ensure the proper aspect ratio for the image
        :($imheight()*(imageR-imageL) == $imwidth()*(scatterB-scatterT)),
        # Leave enough room for scatterplot y labels
        :(scatterL-W > 40),
        # Scatterplot bottom label
        :(buttonsT-scatterB > 40),
        # Ensure a small gap between east marginal axis and the image axis
        :(imageL > margeER + 10)
    )
# addconstraints(lbuttons, :(S-N > 25))
addconstraints(l, :(S - buttonsT > 25))

# Yet more constraints
addconstraints(l,
        # we want lots of space for the primary displays
        :(soft(imageR-imageL>0.3*(E-W), 1.0)),
        :(soft(scatterR-scatterL>0.4*(E-W), 1.0)),
        # we'd like the marginal axes to be substantially thinner than the scatterplot
        :(soft(margeER-margeEL == (scatterR-scatterL)/3, 1.0)),
        :(soft(margeNB-margeNT == (scatterB-scatterT)/3, 1.0)),
        # small gaps for the marginal axes
        :(margeEL-scatterR>5),
        :(scatterT-margeNB>5)
    )

# Reward leaving as much vertical space as possible
# addobjective(l, :(100*(scatterB-scatterT)))

lpp, stuffer = lpparse(l)
lpsolve(lpp, stuffer)

@show l[xtabs]
@show l[ytabs]


const GEO_FILE = "cities1000"
const GEO_SOURCE = "http://download.geonames.org/export/dump"
const DATA_DIR = joinpath(dirname(dirname(pathof(ReverseGeocode))),"data")

"""
Column names in the geonames dumpfile (from http://download.geonames.org/export/dump):
"""
const COLUMNS = [:geonameid, :name, :asciiname, :alternatenames, :latitude, :longitude,
                :feature_class, :feature_code, :country_code, :cc2, :admin1_code, :admin2_code,
                :admin3_code, :admin4_code, :population, :elevation, :dem, :timezone, :modification_date]

"""
    Geocoder(;data_dir="./data", geo_file="cities1000"))

Geocoder structure that holds the reference points and their labels (city name and country code).
"""
struct Geocoder
    tree::NNTree
    info::Array{NamedTuple{(:city, :country_code),Tuple{String, String}}}
    country_codes::Dict{String, String}

    function Geocoder(;data_dir::String=DATA_DIR, geo_file::String=GEO_FILE)
        if ! isfile(joinpath(data_dir,"$geo_file.csv"))
            download_data(;data_dir=data_dir, geo_file=geo_file)
        end
        points, info = read_data(;data_dir=data_dir, geo_file=geo_file)

        tree = KDTree(points)
        country_codes = Dict(CSV.File(joinpath(data_dir,"country_codes.csv"); delim="\t", header=false))

        new(tree, info, country_codes)
    end
end

"""
    read_data(;data_dir="./data", geo_file="cities1000")

Load coordinates, country codes and city names from the `.csv` saved export of the geonames file.
Make sure to call `download_data()` before `read_data()`.
"""
function read_data(;data_dir::String=DATA_DIR, geo_file::String=GEO_FILE)
    data = CSV.File(joinpath(data_dir,"$geo_file.csv"); delim="\t", header=true, types=[String, Float64, Float64, String])

    n = length(data)
    points = Array{Float64}(undef, 2, n)
    info = Array{NamedTuple{(:city, :country_code),Tuple{String, String}}}(undef, n)
    for (i,row) in enumerate(data)
        points[:,i] .= row.latitude, row.longitude
        info[i] = (city = row.name, country_code = row.country_code)
    end
    points, info
end

"""
    download_data(;data_dir="./data", geo_file="cities1000", header=COLUMNS)

Download dump from [geonames.org](http://download.geonames.org/export/dump/). This function
fetches a file of cities with a population > 1000 (and seats of administrations of ceratain country subdivisions,
other options are population 500, 5000, 15000, see geonames.org for details).
The dump is unpacked and city name, coordinates and country code are saved
in a `.csv` file for use in the Geocoder.
"""
function download_data(;data_dir::String=DATA_DIR, geo_file::String=GEO_FILE, header=COLUMNS)
    # Download the source file
    download("$GEO_SOURCE/$geo_file.zip", joinpath(data_dir,"$geo_file.zip"))
    # extract the csv and drop unnecessary columns
    r = ZipFile.Reader(joinpath(data_dir,"$geo_file.zip"))
    data = CSV.File(read(r.files[1]); delim="\t", header=header, select=[:name,:latitude,:longitude,:country_code])
    close(r)
    # save needed data as csv
    CSV.write(joinpath(data_dir,"$geo_file.csv"), data; delim="\t")
    # clean up
    rm(joinpath(data_dir,"$geo_file.zip"))

    @info "Reference dataset sucessfuly saved in $data_dir."
end

"""
Download and resave the country codes csv from geonames.
Country codes are part of the package so this function does not usually need to run during install.
"""
function download_country_codes(;data_dir::String=DATA_DIR)
    download("http://download.geonames.org/export/dump/countryInfo.txt", "$data_dir/countryInfo.txt")
    country_info = CSV.File(joinpath(data_dir,"countryInfo.txt"); delim="\t", header=false, select=[1,5], datarow=51)
    country_codes = Dict([(c.Column1, c.Column5) for c in country_info])
    CSV.write(joinpath(data_dir,"country_codes.csv"), country_codes, delim="\t", header=false)
end

"""
    decode(gc::Geocoder, points) => Array{NamedTuple{(:country, :country_code, :city), Tuple{String, String, String}}}

Return country name and city for collection of points. Points should be either an array of staticaly sized arrays
(e.g. StaticArrays) or a Matrix (see NearestNeighbors.jl documentation for details).

The country and city is determined by the nearest neighbor search in the labelled list of city locations from geonames.org.
As such, the results may not be exactly accurate (e.g searches for points close to borders or in the middle of nowhere).

Nearest neighbor search uses the euclidian metric in the space of lat/lon coordinates.

```
# Example
julia> gc = Geocoder();
julia> ReverseGeocode.decode(gc, [SA[49.5863897, 17.2627342], SA[63.3342550, 12.0280064]])
2-element Array{Tuple{String,String},1}:
 (country="Czechia", country_code="CZ", city="Olomouc")
 (country="Norway", country_code="NO", city="Meråker")
```
"""
function decode(gc::Geocoder, points::Union{AbstractVector{<:AbstractVector{<:Real}}, AbstractMatrix{<:Real}})::Vector{NamedTuple{(:country, :country_code, :city), Tuple{String, String, String}}}
    idxs, dist = nn(gc.tree, points)
    infos = [gc.info[idx] for idx in idxs]
    tags = [(country=gc.country_codes[i.country_code], country_code=i.country_code, city=i.city) for i in infos]
end

"""
    decode(gc::Geocoder, point) => NamedTuple{(:country, :country_code, :city), Tuple{String, String, String}}

Decode for single point. If processing many points, preferably use `decode(gc, points)` instead of using this method in a loop.
"""
function decode(gc::Geocoder, point::AbstractVector{<:Real})
    decode(gc, [point])[1]
end


###############################################################################
#
# General data struct
#
###############################################################################
"""
    struct GriddedDataset{FT<:AbstractFloat}

A general struct to store data

# Fields
$(FIELDS)
"""
Base.@kwdef struct GriddedDataset{FT<:AbstractFloat}
    "Gridded dataset"
    data::Array{FT,3} = zeros(360,180,1);
    "Realistic range"
    lims::Array{FT,1} = FT[-100,100]
    "Latitude resolution `[°]`"
    res_lat::FT = 180 / size(data,2)
    "Longitude resolution `[°]`"
    res_lon::FT = 360 / size(data,1)
    "Time resolution: D-M-Y-C: day-month-year-century"
    res_time::String = "1Y"
    "Variable name"
    var_name::String = "ZEROS"
    "Variable attribute"
    var_attr::Dict{String,String} = Dict("longname" => "ZEROS",
                                         "units"    => "-")
end


# License for this file: MIT (expat)
# Copyright 2017-2018, DLR Institute of System Dynamics and Control
#
# This file is part of module
#   ModiaMath.SimulationEngine(ModiaMath/SimulationEngine/_module.jl)
#
import DASSL
using DASSL

mutable struct IntegratorData
    model::ModiaMath.AbstractSimulationModel    # Model specific data structure
    simulationState::ModiaMath.SimulationState
    statistics::ModiaMath.SimulationStatistics   # Simulation statistics
    z::Vector{Float64}          # Vector of event indicators (zero crossings). If one of z[i] passes
                                # zero, that is beforeEvent(z[i])*z[i] < 0, an event is triggered
                                # (allocated during instanciation according to nz).
    zDir::Vector{Int32}          # zDir[i] =  0: Root is reported for both crossing directions
                                #         =  1: Root is reported when crossing from negative to positive direction
    eweight::Vector{Float64}    # Weight vector
    last_t::Float64              # Time instant of the last call to idasol_f (residue function)
    last_norm_y::Float64         # Norm of y of the last call to idasol_f (residue function)
    hcur::MVector{1,Float64}     # Current step size argument for IDAGetCurrentStep
    order::MVector{1,Int32}       # Current order for IDAGetCurrentOrder
    r::Vector{Float64} # Residues vector used in sol_f function

    y::Vector{Float64}    # Julia vector of y wrapping y_N_Vector vector
    yp::Vector{Float64}   # Julia vector of yp wrapping yp_N_Vector vector
    fulljac::Union{Matrix{Float64}, Nothing}  # Julia matrix of fulljac wrapping fulljac_N_xxx

    function IntegratorData(model::ModiaMath.AbstractSimulationModel, simulationState::ModiaMath.SimulationState)
        ny = simulationState.nx
        nz = simulationState.nz
        new(model, simulationState, simulationState.statistics,
          ones(nz), fill(0, 0), zeros(ny), typemin(Float64), 0.0,
          MVector{1,Float64}(0.0), MVector{1,Int32}(0), zeros(ny))
    end
end

include("UserData.jl")
#=
function updateStatistics!(sol_mem, stat::ModiaMath.SimulationStatistics)
    info = 0
    info = sol_mem.steps
    stat.nSteps        += info

    info = sol_mem.jac_evals
    stat.nJac          += info

    info = sol_mem.fails
    stat.nErrTestFails += info

end
=#

"""
    result = closeTimePoints(t1,t2,epsilon)

The function returns true if the two time points t1 and t2 are close together (with respect to epsilon)
"""
closeTimePoints(t1::Float64, t2::Float64, epsilon::Float64) = abs(t1 - t2) / max(abs(t2), 10 * epsilon) <= 100 * epsilon

"""
    ModiaMath.simulate!(simulationModel; log=false, startTime=NaN, stopTime=NaN,
                                                    tolerance=NaN, interval=NaN)

Simulates a DAE `simulationModel` that is defined with package `Modia`, package `Modia3D` or
with the `ModiaMath.@component` macro. The DAE is mathematically described as
implicit, index 1 DAE with events (containing an ODE or a semi-explicit
index 1 DAE as special cases). For details see [`ModiaMath.StructureOfDAE`](@ref).

During continuous integration, the DAE is solved with the
[Sundials](https://computation.llnl.gov/projects/sundials) IDA solver
(accessed via the Julia [Sundials](https://github.com/JuliaDiffEq/Sundials.jl) interface package).

Input arguments

- `simulationModel::ModiaMath.AbstractSimulationModel`: Model struct (generated with Modia, Modia3D or `ModiaMath.@component`).
- `log::Bool`: = true, if logging is enabled, otherwise it is disabled.
- `startTime::Float64`: Start time of the simulation in [s].
                        If startTime=NaN, the default startTime is used that is defined by the `simulationModel`.
- `stopTime::Float64`: Stop time of the simulation in [s].
                       If stopTime=NaN, the default stopTime is used that is defined by the `simulationModel`.
- `tolerance::Float64`: The relative tolerance for the integration.
  The absolute tolerance is computed as `0.1*tolerance*nominal(variable)` where
  `nominal(variable)` is the nominal value of the variable.
   If tolerance=NaN, the default tolerance is used that is defined by the `simulationModel`.
- `interval::Float64`: Output interval for results in [s]. If events occur, the event time instants are
  additionally added to the result.
  If interval=NaN, the default interval is used that is defined by the `simulationModel`.
"""
function simulate!(model::ModiaMath.AbstractSimulationModel;
                   tolerance=NaN,
                   startTime=NaN,
                   stopTime=NaN,
                   interval=NaN,
                   log::Bool=false,
                   KLUorderingChoice::Int=1)

    use_fulljac = false

    sim           = model.simulationState
    sim.model     = model
    sim.tolerance = isnan(tolerance) ? sim.defaultTolerance : convert(Float64, tolerance)
    sim.startTime = isnan(startTime) ? sim.defaultStartTime : convert(Float64, startTime)
    sim.stopTime  = isnan(stopTime)  ? sim.defaultStopTime  : convert(Float64, stopTime)
    sim.interval  = isnan(interval)  ? (isnan(sim.defaultInterval) ? (sim.stopTime - sim.startTime)/500.0 : sim.defaultInterval)  : convert(Float64, interval)
    sim.time      = sim.startTime
    @assert(sim.stopTime >= sim.startTime)
    @assert(sim.interval >= 0.0)
    @assert(sim.tolerance > 0.0)

    stopTime2::Float64  = sim.stopTime
    startTime2::Float64 = sim.startTime
    tolerance::Float64  = sim.tolerance
    interval::Float64   = sim.interval
    logger              = sim.logger
    ModiaMath.setLog!(logger, log)

    if ModiaMath.isLogInfos(logger)
        println("... ModiaMath.simulate! (version ", ModiaMath.Version, " ", ModiaMath.Date, ") to simulate model: ", sim.name)
    end

    # Start timing measure
    cpuStart::UInt64 = time_ns()
    cpuLast::UInt64  = cpuStart
    cpuStartIntegration::UInt64 = cpuStart

    # Init variables
    nt::Int           = round(Int64, 1 + (stopTime2 - startTime2) / interval)
    t0::Float64       = startTime2
    tEnd::Float64     = stopTime2
    tReached::Float64 = t0
    epsilon::Float64  = eps()

    # Initialize simulation model and store result after initialization
    statistics = sim.statistics
    ModiaMath.reInitializeStatistics!(statistics, t0, stopTime2, interval, tolerance)
    statistics.h0 = interval
    statistics.hMin = Inf
    statistics.hMax = 0
    if ModiaMath.isLogInfos(logger)
        println("      Initialization at time = ", t0, " s")
    end

    init = DAE.initialize!(model, sim, t0, nt, tolerance)
    @assert(length(init.y0)  == sim.nx)
    @assert(length(init.yp0) == sim.nx)
    @assert(tolerance > 0.0)

    if init.terminate || tEnd <= t0
        statistics.h0   = 0.0
        statistics.hMin = 0.0
        @goto TerminateSimulation
    end

    if ModiaMath.isLogInfos(logger)
        println("      Simulation started")
    end

    maxTime::Float64          = init.maxTime
    nextEventTime::Float64    = init.nextEventTime
    integrateToEvent::Bool = init.integrateToEvent

    if nextEventTime < t0
        nextEventTime = typemax(Float64)
    end

    # Initialize auxiliary variables
    simModel = IntegratorData(model, sim)
    simModel.hcur[1] = statistics.h0
    stateEvent::Bool = false
    timeEvent::Bool  = false
    flag = false
    ny = sim.nx
    nz = sim.nz

    y  = copy(init.y0)
    yp = copy(init.yp0)
    simModel.y  = y
    simModel.yp = yp
    tolAbs = tolerance * 5e-4 #magic number passes tests
    if use_fulljac
        fulljac = zeros(sim.nx, sim.nx)
        simModel.fulljac = fulljac
    else
        simModel.fulljac = nothing
    end

    eventInfo = DAE.EventInfo()
    tret    = [0.0]
    jc::Int = 0  # communication point counter
    if ModiaMath.isLogInfos(logger) && nz > 0
        rootInfo = fill(0, nz)
    end

    #will be used later to let solver operate with vectors of more presice than Float64 reals
    y_Vector  = copy(y)
    yp_Vector = copy(yp)
    relTol =  0.0001 * tolerance * init.y_nominal
    for i in 1:ny
        if !init.y_errorControl[i]
            tolAbs[i] = 1e5 * init.y_nominal[i]   # switch tolerance control off
        end
    end
    # Run solution
    # Initialize solvers
    tReached = t0
    solver = dassl()
    #solver = IDA()

    y0 = y_Vector
    yp0 = yp_Vector
    if use_fulljac
        # lsJacFn
        jac_fun = sol_fulljac
    end

    # Sundials.IDASetId(mem, y_states_nvector)
    #if sim.sparse
    #   nnz_jac = nnz(sim.jac)
    #   Sundials.IDAKLU(mem, ny, nnz_jac)
    #   Sundials.IDAKLUSetOrdering(mem, KLUorderingChoice)
    #   Sundials.IDASlsSetSparseJacFn(mem);
    #else
    #A  = Sundials.SUNDenseMatrix(ny, ny)
    #LS = Sundials.SUNDenseLinearSolver(y, A)
    #Sundials.IDADlsSetLinearSolver(mem, LS, A)
    #end

    # Initialize zero crossing function, if required
    hasZeroCross = nz > 0
    if hasZeroCross
        ModiaMath.DAE.getEventIndicators!(simModel.model, sim, t0, y0, yp0, simModel.z)
        simModel.zDir = zeros(nz)
        sim.zDir = simModel.zDir
    end

    # Initialize event variables
    tStop::Float64 = tEnd
    tNext = t0
    tc    = t0
    nTimeEvents = 0
    restart     = ModiaMath.NoRestart

    # Check for event at time = t0. If yes, trigger event and store result after event
    if closeTimePoints(t0, nextEventTime, epsilon)
        tReached = t0
        if ModiaMath.isLogEvents(logger)
            println("\n      Time event at time = $tReached s")
        end

        DAE.reset!(eventInfo)
        DAE.processEvent!(model, sim, tReached, y_Vector, yp_Vector, eventInfo)
        restart          = eventInfo.restart
        maxTime          = eventInfo.maxTime
        nextEventTime    = eventInfo.nextEventTime
        integrateToEvent = eventInfo.integrateToEvent

        statistics.nTimeEvents += 1
        if ModiaMath.isLogEvents(logger)
            println("        restart = ", restart)
        end

        # Reinitialize DAE if required by model
        if restart == ModiaMath.Restart
            statistics.nRestartEvents += 1
            simModel.y = y
            simModel.yp = yp
            DAE.reinitialize!(simModel.model, sim, tReached)
        end
    end

    # Loop over communication points
    cpuStartIntegration = time_ns()

    flag = false

    while tReached < tEnd && restart != ModiaMath.Terminate  #------------------ while --------------
        #println("entered")
        if tReached >= tc
            # Last communication point reached -> increment communication point
            jc += 1
            tc = startTime2 + jc * interval
        end

        if closeTimePoints(nextEventTime, tc, epsilon)
            nextEventTime = tc
        end

        if nextEventTime <= tc # next tStop is a time event
            event = true
            tNext = nextEventTime
            tStop = integrateToEvent ? nextEventTime : min(tEnd, maxTime)
        else        # next tStop is a communication point (no event)
            event = false
            tNext = tc
            tStop = min(tEnd, maxTime)
        end

        # Integrate in direction of tStop
        #println("... tReached = ", tReached, ", tNext = ", tNext, ", tStop = ", tStop,
        #        ", y = ", y[1], ", yp = ", yp[1])
        #initialize derivatives
        r =  simModel.r
        tspan = (tReached, tNext)
        resprob!(r, du, u, p, t) = sol_f!(simModel, sim, t, u, du, r, simModel.hcur[1])
        differential_vars = [true,true,false]
        prob = DAEProblem{true}(resprob!, yp, y, tspan, differential_vars=differential_vars)
        sol = Vector{Any}
        try #some problems require higher tolerance than passed, some fail with this hish tolerance
            sol = solve(prob, solver, reltol = relTol, abstol = tolAbs, initstep = simModel.hcur[1] )
        catch
            tAbs = tolAbs * 100
            rTol = relTol * 100
            sol = solve(prob, solver, reltol = rTol, abstol = tAbs, initstep = simModel.hcur[1] )
        end

        endInd = length(sol.t)
        simModel.yp = sol.du[end]
        if hasZeroCross

            len = length(sol.t)
            for el in 2:len
                ModiaMath.DAE.getEventIndicators!(simModel.model, sim, sol.t[el-1], sol.u[el-1], sol.du[el-1], simModel.z)
                old_z = copy(simModel.z)
                #println("old_z = $old_z")
                ModiaMath.DAE.getEventIndicators!(simModel.model, sim, sol.t[el], sol.u[el], sol.du[el], simModel.z)
                zs = old_z.*(simModel.z)
                z = copy(simModel.z)
                #println("new_z = $z, zs = $zs")
                #println("Event ind = $z")
                if (tReached!=t0)
                    flag = any(x->x<0, zs) && !flag
                    #println("flag = $flag")
                end
                if flag
                endInd = el
                #println("before event at all: y =", sol.u, ", \n yp = ", sol.du)
                    break
                end
            end
        end

        #update statistics
        nSteps = length(sol.t)-1
        if nSteps>0
            steps = zeros(nSteps)
            for it in 1:nSteps
                step = sol.t[it+1] - sol.t[it]
                steps[it] = step
            end
            simModel.hcur[1] = steps[end]
            statistics.h0 = steps[end]

            sim.time             = tReached
            simModel.last_t      = tReached
            simModel.last_norm_y = norm(simModel.y, Inf)
            #println("stat = ", statistics.h0)
            statistics.hMin = min(statistics.hMin, minimum(steps))
            statistics.hMax = max(statistics.hMax, maximum(steps))
        end

        if flag
            #binary search
            ind_l = endInd-1
            ind_r = endInd
            #println("indices are ind_l = $ind_l and ind_r = $ind_r")
            yMin = sol.u[ind_l]
            ypMin = sol.du[ind_l]
            tMin = sol.t[ind_l]
            yMax = sol.u[ind_r]
            ypMax = sol.du[ind_r]
            tMax = sol.t[ind_r]
            z = copy(simModel.z)
            while !any(x->abs(x)<tolAbs, z) #binsearch by values approx
            #while  false #binsearch by values approx
                tspan = (tMin, tMax)
                yMid = (yMax + yMin)/2
                ypMid = (ypMax + ypMin)/2
                tMid = (tMax + tMin)/2
                ModiaMath.DAE.getEventIndicators!(simModel.model, sim, tMid, yMid, ypMid, simModel.z)
                z = copy(simModel.z)
                if any(x->x<0, old_z.*(simModel.z))
                    yMax = yMid
                    ypMax = ypMid
                    tMax = tMid
                else
                    yMin = yMid
                    ypMin = ypMid
                    tMin = tMid
                end
                #println("y internal = $yMid, yp = $ypMid,  z = $z, t = $tMid")
            end  #end binsearch by values approx
            y = yMax
            tReached = tMax
            yp = ypMax
            simModel.yp = yp
        else #-- if not flag
            y = sol.u[end]
            yp = sol.du[end]
            tReached = sol.t[end]
        end #-- end if flag --

        #println("y new = $y, yp = $yp,  tol = $tolerance, t = $tReached")
        simModel.y = y
        simModel.yp = yp

        sim.time             = tReached
        simModel.last_t      = tReached
        simModel.last_norm_y = norm(simModel.y, Inf)

        stateEvent = flag
        timeEvent  = event && closeTimePoints(tNext, tReached, epsilon)
        isEvent    = timeEvent || stateEvent
        # Store result point at tReached
        DAE.computeAndStoreResult!(model, sim, tReached, y, yp)

        # If event at tReached, handle the event
        if isEvent
            if ModiaMath.isLogEvents(logger)
                if timeEvent && stateEvent
                    print("\n      Time and state (zero-crossing) event at time = $tReached s")
                elseif timeEvent
                    println("\n      Time event at time = $tReached s")
                elseif stateEvent
                    print("\n      State event (zero-crossing) at time = $tReached s")
                end
                #println("\n y = $y, yp=$yp")
                if stateEvent
                   # Print information about the root
                    ModiaMath.DAE.getEventIndicators!(simModel.model, sim, tReached, y, yp, simModel.z)
                    rootInfo = simModel.z
                    print(" (")
                    firstRoot = true
                    for iroot in eachindex(rootInfo)
                        if rootInfo[iroot] != 0
                            if firstRoot
                                firstRoot = false
                            else
                                print(", ")
                            end
                            print("z[", iroot, "]", rootInfo[iroot] > 0 ? " > 0" : " < 0")
                        end
                    end
                    print(")\n")
                end
            end
            old_z = simModel.z
            # Trigger event
            DAE.reset!(eventInfo)
            DAE.processEvent!(model, sim, tReached, y, yp, eventInfo)
            #println("\n  event = $eventInfo")
            #println("\n after event y = $y, yp=$yp, t = $tReached")

            restart          = eventInfo.restart
            maxTime          = eventInfo.maxTime
            nextEventTime    = eventInfo.nextEventTime
            integrateToEvent = eventInfo.integrateToEvent

            if timeEvent
                statistics.nTimeEvents += 1
            end

            if stateEvent
                statistics.nStateEvents += 1
            end

            if restart == ModiaMath.Restart || restart == ModiaMath.FullRestart
                statistics.nRestartEvents += 1
            end

            if ModiaMath.isLogEvents(logger)
                println("        restart = ", restart)
            end

            # Handle restart flag
            if restart == ModiaMath.Restart
                # Restart (dimensions do not change)
                simModel.y = y
                simModel.yp = yp
                DAE.reinitialize!(simModel.model, sim, tReached)
                #y = simModel.y
                #yp = simModel.yp
                ##Sundials.__IDAReInit(mem, tReached, y_N_Vector, yp_N_Vector);
            elseif restart == ModiaMath.FullRestart
                error("FullRestart not yet supported")
            end
        end

        # print every 5 s the time in order that the user sees the progress
        if ModiaMath.isLogProgress(logger)
            cpuNew = time_ns()
            if (cpuNew - cpuLast) * 1e-9 > 5.0
                cpuLast = cpuNew
                @printf("      progress: integrated up to time = %.2g s\n", tReached)
            end
        end
    end #---------------------- end while ------------------------------------

    # Finalize statistics for simulation run

    @label TerminateSimulation
    if ModiaMath.isLogInfos(logger)
        println("\n      Simulation is terminated at time = ", tEnd, " s")
    end

    result = DAE.terminate!(model, sim, tReached, y, yp)
    statistics.cpuTimeInitialization = (cpuStartIntegration - cpuStart) * 1e-9
    statistics.cpuTimeIntegration    = (time_ns() - cpuStartIntegration) * 1e-9

    if ModiaMath.isLogStatistics(logger)
        println("\n      Statistics (get help with ?ModiaMath.SimulationStatistics):")
        display(statistics)
    end
    return result
end


using Multilane
using MCTS
using RobustMCTS
using POMCP
using GenerativeModels
using POMDPToolbox
using JLD
using POMDPs
using DataFrames
using Plots

N=500

nb_lanes = 4
desired_lane_reward = 10.
rmodel = NoCrashRewardModel(desired_lane_reward*10., desired_lane_reward,2.5,nb_lanes)

pp = PhysicalParam(nb_lanes,lane_length=100.)

normal_behavior = IDMMOBILBehavior("normal", pp.v_med, pp.l_car, 1)
behaviors=IDMMOBILBehavior[
                    normal_behavior,
                    IDMMOBILBehavior("cautious", pp.v_slow+0.5, pp.l_car, 2),
                    IDMMOBILBehavior("aggressive", pp.v_fast, pp.l_car, 3)
                    ]

_discount = 1.0
nb_cars = 10
dmodel = NoCrashIDMMOBILModel(nb_cars, pp,
                              behaviors=behaviors,
                              behavior_probabilities=WeightVec([0.2, 0.4, 0.4]),
                              lane_terminate=false)

base_problem = NoCrashMDP(dmodel, rmodel, _discount)

problems = Dict{String, Any}()
key_rng = MersenneTwister(123)

is_rng = MersenneTwister(123)
init_phys_states = [MLPhysicalState(initial_state(base_problem, is_rng)) for i in 1:N]
initial_states = Dict{String, Any}()
state_lists = Dict{String, Any}()

params = Dict{Symbol, Any}()
params[:lambda] = Float64[0.1, 1., 10., 17.78, 31.62, 56.23, 100., 1000.]
# params[:lambda] = Float64[0.1, 1.]
params[:p_normal] = Float64[0.2, 1.0]

initial_relevant = [:p_normal]

param_table = DataFrame()
for p in params
    if isempty(param_table)
        param_table = DataFrame(Dict(p))
    else
        param_table = join(param_table, DataFrame(Dict(p)), kind = :cross)
    end
end

param_table[:problem_key] = DataArray(String, nrow(param_table))
param_table[:state_list_key] = DataArray(String, nrow(param_table))

for row in eachrow(param_table)
    key = randstring(key_rng)
    problem = deepcopy(base_problem)
    # lambda
    problem.rmodel.cost_dangerous_brake = row[:lambda]*problem.rmodel.reward_in_desired_lane
    # p_normal
    nb_behaviors = length(problem.dmodel.behaviors)
    probs = ones(nb_behaviors) * (1.0 - row[:p_normal])/(nb_behaviors-1)
    probs[1] = row[:p_normal]
    problem.dmodel.behavior_probabilities = WeightVec(probs)

    problems[key] = problem
    row[:problem_key] = key
end

for g in groupby(param_table, initial_relevant)
    key = randstring(key_rng)
    pk = g[1,:problem_key]
    p = problems[pk]
    these_states = assign_keys([initial_state(p, init_phys_states[i], is_rng) for i in 1:N])
    merge!(initial_states, these_states)
    g[:,:state_list_key] = key
    state_lists[key] = collect(keys(these_states))
end

objects = Dict{String, Any}()
objects["problems"] = problems
objects["param_table"] = param_table
objects["initial_states"] = initial_states
objects["state_lists"] = state_lists

# Just Figure 1 with 5 simulations

dpws = DPWSolver(depth=20,
                 n_iterations=300,
                 exploration_constant=50.0,
                 k_state=4.0,
                 alpha_state=1/8,
                 rollout_solver=SimpleSolver())

rsolver = RobustMCTSSolver(
    depth=20,
    c=50.0,
    n_iterations=1500,
    k_state=4.0,
    alpha_state=1/8,
    k_nature=4.0,
    alpha_nature=1/8,
    rollout_solver=SimpleSolver(),
    rollout_nature=StochasticBehaviorNoCrashMDP(collect(values(problems))[1]))

pomcps = POMCPDPWSolver(
    eps=0.001,
    max_depth=20,
    c=50.0,
    tree_queries=1500,
    k_observation=4.0,
    alpha_observation=1/8,
    rollout_solver=SimpleSolver())

# for 20% and 100 percent, we need the upper bound, the heuristic and random
point_solvers = Dict{String, Solver}(
    "random" => RandomSolver(),
    "heuristic" => SimpleSolver()
)

objects["solvers"] = deepcopy(point_solvers)
objects["solvers"]["upper_bound"] = dpws
objects["solvers"]["pomcp"] = MLPOMDPSolver(pomcps, BehaviorRootUpdaterStub(0.1))
objects["solvers"]["robust"] = RobustMLSolver(rsolver)

results = objects

problems_100 = param_table[param_table[:p_normal].==1.0, :problem_key]
problems_20 = param_table[param_table[:p_normal].==0.2, :problem_key]
solver_pairs = [("upper_bound", problems_100),
                ("pomcp", problems_20),
                ("robust", problems_20)]

for p_normal in unique(param_table[:p_normal])
    list = param_table[param_table[:p_normal].==p_normal, :state_list_key][1]
    inits = state_lists[list]
    problems = param_table[param_table[:p_normal].==p_normal, :problem_key]

    r = evaluate([problems[1]], inits, collect(keys(point_solvers)), objects, N=N, desc="$p_normal Point: ")
    merge_results!(results, r)

    r = evaluate(problems, inits, ["upper_bound"], objects, N=N, desc="$p_normal Upper: ")
    merge_results!(results, r)

    for sp in solver_pairs
        r = evaluate(problems, inits, [sp[1]], objects, soln_problem_keys=sp[2], N=N, desc="$p_normal $(sp[1]): ")
        merge_results!(results, r)
    end
end

#=
# Calculate single points
point_solvers = Dict{String, Solver}(
    "random" => RandomSolver(),
    "heuristic" => SimpleSolver()
)

point_results = evaluate(representative_problem, initial_states, point_solvers, parallel=true, N=N)

dpws = DPWSolver(depth=20,
                 n_iterations=300,
                 exploration_constant=50.0,
                 k_state=4.0,
                 alpha_state=1/8,
                 rollout_solver=SimpleSolver())

rsolver = RobustMCTSSolver(
    depth=20,
    c=50.0,
    n_iterations=1500,
    k_state=4.0,
    alpha_state=1/8,
    k_nature=4.0,
    alpha_nature=1/8,
    rollout_solver=SimpleSolver(),
    rollout_nature=StochasticBehaviorNoCrashMDP(collect(values(problems))[1]))

pomcps = POMCPDPWSolver(
    eps=0.001,
    max_depth=20,
    c=50.0,
    tree_queries=1500,
    k_observation=4.0,
    alpha_observation=1/8,
    rollout_solver=SimpleSolver()
)

curve_solvers = Dict{String, Solver}(
    "upper_bound" => dpws,
    "robust" => RobustMLSolver(rsolver),
    "single_behavior" =>
        SingleBehaviorSolver(dpws,
                 IDMMOBILBehavior("normal", 30.0, 10.0, 1)),
    "pomcp" => MLPOMDPSolver(pomcps, BehaviorRootUpdaterStub(0.1))
)

curve_results = evaluate(problems, initial_states, curve_solvers, parallel=true, N=N)

results = merge_results!(curve_results, point_results)
=#

println(results["stats"])

filename = string("results_", Dates.format(Dates.now(),"u_d_HH_MM"), ".jld")
results["histories"] = nothing
save(filename, results)
println("results saved to $filename")

#=
stats = results["stats"]
mean_performance = by(stats, :solver_key) do df
    by(df, :lambda) do df
        DataFrame(steps_in_lane=mean(df[:steps_in_lane]),
                  nb_brakes=mean(df[:nb_brakes]),
                  time_to_lane=mean(df[:time_to_lane]),
                  steps=mean(df[:steps]),
                  )
    end
end

unicodeplots()
for g in groupby(mean_performance, :solver_key)
    if size(g,1) > 1
        # plot!(g, :steps_in_lane, :nb_brakes, group=:solver_key)
        plot!(g, :time_to_lane, :nb_brakes, group=:solver_key)
    else
        # scatter!(g, :steps_in_lane, :nb_brakes, group=:solver_key)
        scatter!(g, :time_to_lane, :nb_brakes, group=:solver_key)
    end
end
gui()
=#


export getblock, nbit_used, nbit_simulated, nrepeat, expand_circuit, QuantumMPS
export state_exact, fidelity_exact
export gensample

"""
    QuantumMPS{RT}

Members:
    `nbit_measure`, number of qubits measured in a single iteration, or physical qubits.
    `nbit_virtual`, number of virtual qubits to represent the virtual bond dimension in quantum MPS.
    `circuit`, the circuit structure (measurements are not included).
    `initial_reg`, the initial state (GPUReg or a regular one), always prepaired in |0>.
    `nbit_ancilla`, number of ancilla qubits.
"""
struct QuantumMPS{RT}
    nbit_measure::Int
    nbit_virtual::Int
    nbit_ancilla::Int

    circuit::AbstractBlock
    initial_reg::RT

    input_state::Vector{Int}
end

getblock(chem::QuantumMPS, i::Int) = chem.circuit[i]
nrepeat(chem::QuantumMPS) = length(chem.circuit)
nbit_used(chem::QuantumMPS) = nqubits(chem.circuit[1])
nbit_simulated(chem::QuantumMPS) = chem.nbit_measure*nrepeat(chem) + chem.nbit_virtual

"""convert a chem circuit to a circuit with no reuse"""
function expand_circuit(tnchem)
    nbit = nbit_simulated(tnchem) + tnchem.nbit_ancilla
    nm = tnchem.nbit_measure
    nv = tnchem.nbit_virtual + tnchem.nbit_ancilla
    c = chain(nbit)
    for (i, blk) in enumerate(tnchem.circuit)
        push!(c, concentrate(nbit, blk, [(i-1)*nm+1:i*nm..., nbit-nv+1:nbit...]))
    end
    c
end

function state_exact(chem::QuantumMPS)
    circuit = expand_circuit(chem)
    if chem.nbit_ancilla == 0
        return product_state(nqubits(circuit), chem.input_state|>packbits) |> circuit
    else
        nbit = nqubits(circuit)
        product_state(nbit, chem.input_state|>packbits) |> circuit |> focus!((1:nbit-chem.nbit_ancilla)...) |> remove_env!
    end
end

function remove_env!(reg::DefaultRegister)
    reg.state = dropdims(sum(reg |> rank3, dims=2), dims=2)
    reg
end

function fidelity_exact(chem::QuantumMPS, ground_state::AbstractRegister)
    fidelity(ground_state, state_exact(chem))
end

function gensample(chem::QuantumMPS, pauli::PauliGate)
    input_state = chem.input_state
    reg = chem.initial_reg |> copy
    nv = chem.nbit_virtual + chem.nbit_ancilla
    nrep = nrepeat(chem)
    T = datatype(chem.initial_reg)

    op = eigen!(pauli |> mat |>Matrix)
    G = matblock(T.(op.vectors' |> Matrix))
    rotor = put(nv+1, 1=>G)
    local res = similar(reg |> state, Int, nbatch(reg), nbit_simulated(chem))
    for i = nrep+1:nrep+nv
        input_state[i] == 1 && apply!(reg, put(nv+1, (i-nrep+1)=>X))
    end
    input_state[1] == 1 && apply!(reg, put(nv+1, 1=>X))
    for i=1:nrep
        reg |> getblock(chem, i)
        if i!=nrep
            reg |> rotor
            @inbounds res[:,i] = 2 .* measure_collapseto!(reg, 1; config=input_state[i+1]) .- 1
        end
    end
    for i=1:nv+1-chem.nbit_ancilla
        reg |> put(nqubits(reg), 1=>G)
        @inbounds res[:,i+nrep-1] = 2 .* measure_remove!(reg, 1) .- 1
    end
    res
end


mutable struct MapClassGrp <: Map{GrpAbFinGen, NfOrdIdlSet, HeckeMap, MapClassGrp}
  header::MapHeader{GrpAbFinGen, NfOrdIdlSet}

  quo::Int
  princ_gens::Vector{Tuple{FacElem{NfOrdIdl,NfOrdIdlSet}, FacElem{nf_elem, AnticNumberField}}}
  small_gens::Vector{NfOrdIdl}
  function MapClassGrp()
    mp = new()
    mp.quo = -1
    return mp
  end
end

mutable struct SmallLLLRelationsCtx{T}
  A::NfOrdIdl
  b::Vector{T}
  bd::Int
  cnt::Int
  elt::T
  function SmallLLLRelationsCtx(a::T) where {T}
    n = new{T}()
    n.bd = 1
    n.cnt = 0
    return n
  end

  function SmallLLLRelationsCtx(::Type{T}) where T
    n = new{T}()
    n.bd = 1
    n.cnt = 0
    return n
  end
end

mutable struct NormCtx_split <: NormCtx
  O::NfAbsOrd{AnticNumberField, nf_elem}
  lp::Vector{Int}  #the primes
  lR::Vector{GaloisField} #the local (finite field) splitting field
  nb::Int #bound on the number of bits the norm is allowed to have
  lC::Vector{gfp_mat} # for each p in lp, the conjugates of the basis of O
  mp::Vector{gfp_mat} # for each p in lp, the conjugates of the basis of O
  np::Vector{gfp_mat} # for each p in lp, the conjugates of the basis of O
  e::crt_env
  function NormCtx_split(O::NfAbsOrd, nb::Int)
    p = p_start
    NC = new()
    NC.O = O
    NC.nb = nb
    NC.lp = Vector{Int}()
    NC.lR = Vector{GaloisField}()
    NC.lC = Vector{gfp_mat}()
    NC.mp = Vector{gfp_mat}()
    NC.np = Vector{gfp_mat}()
    B = basis(O)
    n = degree(O)
    while (true)
      p = next_prime(p)
      lP = prime_decomposition(O, p)
      if length(lP) < degree(O)
        continue
      end
      push!(NC.lp, p)
      R = GF(p, cached = false)
      push!(NC.lR, R)
      push!(NC.mp, zero_matrix(R, 1, degree(O)))
      push!(NC.np, zero_matrix(R, 1, degree(O)))
      M = Vector{Nemo.elem_type(R)}()
      for P = lP
        F, mF = ResidueFieldSmall(O, P[1])
        for x = B
          xp = mF(x)
          push!(M, R(coeff(xp, 0)))
        end
      end
      push!(NC.lC, matrix(R, n, n, M)')
      nb -= nbits(p)
      if nb <= 0
        NC.e = crt_env(fmpz[p for p = NC.lp])
        return NC
      end
    end
  end
end

mutable struct NormCtx_gen <: NormCtx
  nb::Int
  O::NfAbsOrd
  function NormCtx_gen(O::NfAbsOrd, nb::Int)
    NC = new()
    NC.nb = nb
    NC.O = O
    return NC
  end
end

mutable struct MapUnitGrp{T} <: Map{GrpAbFinGen, T, HeckeMap, MapUnitGrp}
  header::Hecke.MapHeader{GrpAbFinGen, T}

  # Only for non-maximal orders:
  OO_mod_F_mod_O_mod_F::GrpAbFinGenToAbsOrdQuoRingMultMap # a map from (OO/F*OO)^\times/(O/F)^\times to OO where OO is a maximal order and F the conductor

  function MapUnitGrp{T}() where {T}
    return new{T}()
  end
end

mutable struct MapSUnitModUnitGrpFacElem <: Map{GrpAbFinGen, FacElemMon{AnticNumberField}, HeckeMap, MapSUnitModUnitGrpFacElem}
  header::MapHeader{GrpAbFinGen, FacElemMon{AnticNumberField}}
  idl::Vector{NfOrdIdl}
  valuations::Vector{SRow{fmpz}}

  function MapSUnitModUnitGrpFacElem()
    return new()
  end
end

mutable struct MapSUnitGrpFacElem <: Map{GrpAbFinGen, FacElemMon{AnticNumberField}, HeckeMap, MapSUnitGrpFacElem}
  header::MapHeader{GrpAbFinGen, FacElemMon{AnticNumberField}}
  idl::Vector{NfOrdIdl}
  valuations::Vector{SRow{fmpz}}
  isquotientmap::Int

  function MapSUnitGrpFacElem()
    z = new()
    z.isquotientmap = -1
    return z
  end
end

mutable struct MapSUnitGrp <: Map{GrpAbFinGen, AnticNumberField, HeckeMap, MapSUnitGrp}
  header::MapHeader{GrpAbFinGen, AnticNumberField}
  idl::Vector{NfOrdIdl}

  function MapSUnitGrp()
    return new()
  end
end


using ...Network
import ...Network: act, state_dict, accept
using ...Signals

struct DelayerAgent{T <: AbstractFloat, U <: Unsigned, V <: Signal}
    delay::T # delay time
    w::AbstractFloat
    stack::Vector{Tuple{T, V}} # (time left, signal)
    donor::Address{U}
    acceptor::Address{U}
end

function DelayerAgent(
    delay::T,
    w::T,
    donor::Address{U},
    acceptor::Address{U},
    signal_name::String,
    network::Dict{String, Population{U}}
) where {T <: AbstractFloat, U <: Unsigned, V <: ForwardSignal}
    donor_check = can_add_acceptor(get_agent(network, donor), signal_name)
    acceptor_check = can_add_donor(get_agent(network, acceptor), signal_name)
    if donor_check && acceptor_check
        DelayerAgent{T, U, V}(delay, w, [], donor, acceptor)
    else
        error(string("DelayerAgent cannot be created for $(signal_name) on: ",
                     donor_check ? "" : "donor $(donor)",
                     ! donor_check && ! acceptor_check ? "and" : "",
                     acceptor_check ? "" : "acceptor $(acceptor)",
                     "."))
    end
end

function update(agent::DelayerAgent{T, U, V},
                updates::Vector{Tuple{T, V}}) where {T <: AbstractFloat, U <: Signal, V <: ForwardSignal}
    agent.stack = updates
end

## state_dict is for recording states, not for network-serialization. temporarily commented.
# function state_dict(agent::DelayerAgent{T}) where {T <: AbstractFloat, U <: ForwardSignal}
#     Dict(["stack" => map(state_dict, agent.stack)])
# end

function act(address::Address{U},
             agent::DelayerAgent{T, V},
             dt::T,
             trigger,
             push_signal) where {T <: AbstractFloat, U <: Unsigned, V <: ForwardSignal}

    new_stack, due_stack = reduce(
        agent.stack;
        Init=(Vector{(T, V)}(undef, 0), Vector{V}(undef, 0))
    ) do (acc, pair)
        new_t = pair[1] - dt
        if new_t >= 0
            ([acc[1]..., (new_t, pair[2])], acc[2])
        else
            (acc[1], [acc[2]..., amplify(pair[2], agent.w)])
        end
    end

    foreach(s -> push_signal(agent.acceptor, s), due_stack)
    agent.stack - new_stack

    if length(new_stack) > 0
        trigger(address)
    end

end

function accept(agent::DelayerAgent{T, U, V}, signal::U) where {T <: AbstractFloat, U <: Unsigned, V <: ForwardSignal}
    push!(agent.stack, (agent.delay, signal))
end

function can_add_donor(agent::DelayerAgent{T, U, V},
                       signal_name::String) where {T <: AbstractFloat, U <: Unsigned, V <: ForwardSignal}
    signal_name == name_dc_instruction
end

function accept(agent::DelayerAgent{T, U, V},
                signal::BackSpike) where {T <: AbstractFloat, U <: Unsigned, V <: ForwardSignal}
    # STDP
end