include("Yfind.jl")
include("is_it_taylor.jl")

H_test(u)=( (u[3]-u[1])^2+(u[2]-u[4])^2 )*( (u[3]+u[1])^2+(u[2]+u[4])^2 )/ ((u[2]^4+2*u[2]^2*(u[1]^2-1)+(1+u[1]^2)^2 )*(u[3]^4+2*u[3]^2*(u[4]^2+1)+(u[4]^2-1)^2 ))
ODE1(z,w)=conj(  im * w.*( 1 ./(z.^2-w.^2)+1 ./(1+w.^2) ))
ODE2(z,w)=conj(  im * z.*( 1 ./(w.^2-z.^2)+1 ./(1+z.^2) ))
function Eq_of_M!(t,u,du)
    du[1]=real(ODE1(u[1]+im*u[2],u[3]+im*u[4]))
    du[2]=imag(ODE1(u[1]+im*u[2],u[3]+im*u[4]))
    du[3]=real(ODE2(u[1]+im*u[2],u[3]+im*u[4]))
    du[4]=imag(ODE2(u[1]+im*u[2],u[3]+im*u[4]))
    return nothing
end
function g(t,u,du)
    px_ = constant_term(u[4])
 # if px > 0...
 if px_ > zero(px_)
     # ...return x
     return u[1]
 else
     #otherwise, discard the crossing
     return nothing
 end
end

function PSS_function_taylor(Q,P, Energy,  t_end)
    Q_0=0
    P_0=0

    Q_1=0
    P_1=0

    Q_2=0
    P_2=0

    Q_3=0
    P_3=0

    tol_dist=1e-2
    H=(2*Energy)^2

    Tabs_Q=zeros(0)
    Tabs_P=zeros(0)
    Y=Yfind(Q,P,H)
    if ~isempty(Y) && Y>0
        u0=zeros(4)
        u0[1]=0 #X
        u0[2]=P #P
        u0[3]=Q #Q
        u0[4]=Y #Y
        # counter variable
        #are the initial conditions right?
        # prob = ODEProblem(Eq_of_M,u0,(0., t_end))
        tv_i, xv_i, tvS_i, xvS_i, gvS_i=taylorinteg(Eq_of_M!, g, u0, 0., t_end, 12, 1e-12,maxsteps=300000, newtoniter=30 )
        if is_it_taylor(xv_i,H,tol_dist)
            if isempty(xvS_i)
                Q_0=Q
                P_0=P
            else
                uf=zeros(5)
                uf[1]=xvS_i[end,1] #X
                uf[2]=xvS_i[end,2] #P
                uf[3]=xvS_i[end,3] #Q
                uf[4]=xvS_i[end,4] #Y
                dH=abs(H_test(uf)-H)
                if dH<1e-5
                    Tabs_Q=xvS_i[:,3]
                    Tabs_P=xvS_i[:,2]
                    if length(xvS_i[:1])<5
                        if length(Tabs_Q)==1
                            #never hits, exits
                            Q_0=Tabs_Q[end]
                            P_0=Tabs_P[end]
                        elseif length(Tabs_Q)==2
                            #hits once, exits
                            Q_0=Tabs_Q[end]
                            P_0=Tabs_P[end]
                            Q_1=Tabs_Q[end-1]
                            P_1=Tabs_P[end-1]
                        elseif length(Tabs_Q)==3
                            #hits twice, exits
                            Q_0=Tabs_Q[end]
                            P_0=Tabs_P[end]
                            Q_1=Tabs_Q[end-1]
                            P_1=Tabs_P[end-1]
                            Q_2=Tabs_Q[end-2]
                            P_2=Tabs_P[end-2]
                        elseif  length(Tabs_Q)==4
                            Q_0=Tabs_Q[end]
                            P_0=Tabs_P[end]
                            Q_1=Tabs_Q[end-1]
                            P_1=Tabs_P[end-1]
                            Q_2=Tabs_Q[end-2]
                            P_2=Tabs_P[end-2]
                            Q_3=Tabs_Q[end-3]
                            P_3=Tabs_P[end-3]
                        end
                    else
                    Q_0=Tabs_Q[end]
                    P_0=Tabs_P[end]
                    Q_1=Tabs_Q[end-1]
                    P_1=Tabs_P[end-1]
                    Q_2=Tabs_Q[end-2]
                    P_2=Tabs_P[end-2]
                    Q_3=Tabs_Q[end-3]
                    P_3=Tabs_P[end-3]
                    end
                end
            end
        end
    end
return     Q_0,P_0,Q_1,P_1,Q_2,P_2,Q_3,P_3
end


using DiffEqOperators

n = 100
x=0.0:0.005:2π
xprime = x[2:(end-1)]
dx=diff(x)
y = exp.(π*x)
y_im = exp.(π*im*x)
yim_ = y_im[2:(end-1)]
y_ = y[2:(end-1)]

@test_broken for dor in 1:6, aor in 1:6

    D1 = CenteredDifference(dor,aor,dx,length(x))

    #take derivative
    yprime1 = D1*y

    #test result
    @test_broken yprime1 ≈ (π^dor)*y_ # test operator with known derivative of exp(kx)

    #take derivatives
    y_imprime1 = D1*y_im

    #test result
    @test_broken y_imprime1 ≈ ((pi*im)^dor)*yim_ # test operator with known derivative of exp(jkx)

    #TODO: implement specific tests for the left and right boundary regions, waiting until after update
end


using DataStructures

function solve()
    m = 998244353
    n, k = [parse(Int, x) for x in split(readline())]
    segments = zeros(Int, k, 2)
    for s=1:k
        segments[s, 1], segments[s, 2] = [parse(Int, x) for x in split(readline())]
    end
    ft = FenwickTree{Int}(n)
    inc!(ft, 1, 1)
    for i=2:n
        ways = 0
        for s=1:k
            lindex = i - segments[s, 2] - 1
            rindex = i - segments[s, 1]
            l = lindex > 0 ? prefixsum(ft, lindex) % m : 0
            r = rindex > 0 ? prefixsum(ft, rindex) % m : 0
            ways += ((r - l) + m) % m
            ways %= m
        end
        inc!(ft, i, ways)
    end
    (prefixsum(ft, n) - prefixsum(ft, n-1) + m) % m
end

println(solve())


using GAIO
using ForwardDiff

# domain (-40,40)^n, 3^n roots in domain
g(x) = 100*x + x .^ 2 - x .^ 3 .- sum(x)
Dg = x -> ForwardDiff.jacobian(g, x)

dim = 3
center, radius = zeros(dim), 40*ones(dim)
P = BoxPartition(Box(center, radius))

R = cover_roots(g, Dg, P[:], steps=dim*8)

plot(R)


using CompScienceMeshes, BEAST
Γ = readmesh(joinpath(@__DIR__,"sphere2.in"))

X = raviartthomas(Γ)
Y = buffachristiansen(Γ)

Δt, Nt = 0.6, 200
T = timebasisshiftedlagrange(Δt, Nt, 2)
δ = timebasisdelta(Δt, Nt)

V = X ⊗ T
W = Y ⊗ δ

duration = 20 * Δt
delay = 1.5 * duration
amplitude = 1.0
gaussian = creategaussian(duration, delay, amplitude)

direction, polarisation = ẑ, x̂
E = BEAST.planewave(polarisation, direction, gaussian, 1.0)
H = direction × E

K = TDMaxwell3D.doublelayer(speedoflight=1.0)
I = Identity()
N = NCross()

@hilbertspace k
@hilbertspace j
mfie = @discretise (0.5(N⊗I) + 1.0K)[k,j] == -1.0H[k] k∈W j∈V
xmfie = solve(mfie)

Xmfie, Δω, ω0 = fouriertransform(xmfie, Δt, 0.0, 2)
ω = collect(ω0 .+ (0:Nt-1)*Δω)
_, i1 = findmin(abs.(ω .- 1.0))

ω1 = ω[i1]
um = Xmfie[:,i1] / fouriertransform(gaussian)(ω1)


using Test
using DataFrames

using JMcDM

@testset "Euclidean distance" begin
    @test euclidean([0.0, 1.0, 2.0], [0.0, 1.0, 2.0]) == 0.0
    @test euclidean([0.0, 0.0, 0.0]) == 0.0
    @test euclidean([0.0, 0.0, 1.0]) == 1.0
    @test euclidean([0.0, 0.0, 1.0], [0.0, 0.0, 2.0]) == 1.0
end

@testset "Normalization" begin
    tol = 0.00001
    nz = normalize([1.0, 2.0, 3.0, -1.0, 0.0])
    @test isapprox(nz[1], 0.2581989, atol=tol)
    @test isapprox(nz[2], 0.5163978, atol=tol)
    @test isapprox(nz[3], 0.7745967, atol=tol)
    @test isapprox(nz[4], -0.2581989, atol=tol)
    @test isapprox(nz[5], 0.0000000, atol=tol)
end

@testset "Column min and max vectors" begin
    df = DataFrame()
    df[:,:x] = [0.0, 1.0, 10.0]
    df[:,:y] = [0.0, -1.0, -10.0]
    @test colmins(df) == [0.0, -10.0]
    @test colmaxs(df) == [10.0, 0.0]
end

@testset "Unitize vector" begin
    x = [1.0, 2.0, 3.0, 4.0, 5.0]
    result = x |> unitize |> sum
    @test result == 1.0
end

@testset "Product weights with DataFrame" begin
    df = DataFrame()
    df[:, :x] = [1.0, 2.0, 4.0, 8.0]
    df[:, :y] = [10.0, 20.0, 30.0, 40.0]
    w = [0.60, 0.40]
    result = w * df

    @test result[:, :x] == [0.6, 1.2, 2.4, 4.8]
    @test result[:, :y] == [4.0, 8.0, 12.0, 16.0]
end

@testset "Make Decision Matrix" begin
    m = rand(5, 10)
    df = makeDecisionMatrix(m)

    @test isa(df, DataFrame)
    @test size(df) == (5, 10)
end

@testset "TOPSIS" begin
    tol = 0.00001
    df = DataFrame()
    df[:, :x] = Float64[9, 8, 7]
    df[:, :y] = Float64[7, 7, 8]
    df[:, :z] = Float64[6, 9, 6]
    df[:, :q] = Float64[7, 6, 6]
    w = Float64[4, 2, 6, 8]

    fns = makeminmax([maximum, maximum, maximum, maximum])

    result = topsis(df, w, fns)

    @test isa(result, TopsisResult)
    @test result.bestIndex == 2
    @test isapprox(result.scores, [0.3876870, 0.6503238, 0.0834767], atol=tol)
end

@testset "VIKOR" begin
    tol = 0.00001
    w =  [0.110, 0.035, 0.379, 0.384, 0.002, 0.002, 0.010, 0.077]
    Amat = [
      100 92 10 2 80 70 95 80 ;
      80  70 8  4 100 80 80 90 ;
      90 85 5 0 75 95 70 70 ;
      70 88 20 18 60 90 95 85
    ]
    dmat = makeDecisionMatrix(Amat)
    result = vikor(dmat, w)

    @test isa(result, VikorResult)
    @test result.bestIndex == 4

    @test isapprox(result.scores[1], 0.1975012087551764, atol=tol)
    @test isapprox(result.scores[2], 0.2194064473270817, atol=tol)
    @test isapprox(result.scores[3], 0.3507643203516215, atol=tol)
    @test isapprox(result.scores[4], -0.16727341435277993, atol=tol)
end

@testset "ELECTRE" begin
    tol = 0.00001
    w =  [0.110, 0.035, 0.379, 0.384, 0.002, 0.002, 0.010, 0.077]
    Amat = [
      100 92 10 2 80 70 95 80 ;
      80  70 8  4 100 80 80 90 ;
      90 85 5 0 75 95 70 70 ;
      70 88 20 18 60 90 95 85
    ]
    dmat = makeDecisionMatrix(Amat)
    result = electre(dmat, w)

    @test isa(result, ElectreResult)
    @test isa(result.bestIndex, Tuple)
    @test result.bestIndex[1] == 4

    @test isapprox(result.C, [0.36936937,  0.01501502, -2.47347347,  2.08908909], atol=tol)
    @test isapprox(result.D, [0.1914244, -0.1903929,  2.8843076, -2.8853391], atol=tol)
end

@testset "MOORA" begin
    tol = 0.00001
    w =  [0.110, 0.035, 0.379, 0.384, 0.002, 0.002, 0.010, 0.077]
    Amat = [
      100 92 10 2 80 70 95 80 ;
      80  70 8  4 100 80 80 90 ;
      90 85 5 0 75 95 70 70 ;
      70 88 20 18 60 90 95 85
    ]
    dmat = makeDecisionMatrix(Amat)
    result = moora(dmat, w)

    @test isa(result, MooraResult)
    @test isa(result.bestIndex, Int64)
    @test result.bestIndex == 4

    @test isapprox(result.scores, [0.33159387, 0.29014464, 0.37304311, 0.01926526], atol=tol)
end

@testset "DEMATEL" begin
    tol = 0.00001
    K = [
        0 3 0 2 0 0 0 0 3 0;
        3 0 0 0 0 0 0 0 0 2;
        4 1 0 2 1 3 1 2 3 2;
        4 1 4 0 1 2 0 1 0 0;
        3 2 3 1 0 3 0 2 0 0;
        4 1 4 4 0 0 0 1 1 3;
        3 0 0 0 0 2 0 0 0 0;
        3 0 4 3 2 3 1 0 0 0;
        4 3 2 0 0 1 0 0 0 2;
        2 1 0 0 0 0 0 0 3 0
    ]

    dmat = makeDecisionMatrix(K)

    result = dematel(dmat)

    @test isapprox(result.threshold, 0.062945, atol=tol)

    @test isapprox(result.c, [0.3991458, 0.2261648, 1.0204318, 0.7538625,
                           0.8096760, 0.9780926, 0.2717874,
                           0.9455390, 0.5960514, 0.2937537], atol=tol)

    @test isapprox(result.r, [1.5527024, 0.7251791, 0.8551461, 0.6895615,
                           0.2059141, 0.6790404, 0.1057168,
                           0.3163574, 0.6484014, 0.5164858], atol=tol)

    @test isapprox(result.influenceMatrix,
            [ 0.0  1.0  0.0  1.0  0.0  0.0  0.0  0.0  1.0  0.0;
            1.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0  1.0;
            1.0  1.0  0.0  1.0  0.0  1.0  0.0  1.0  1.0  1.0;
            1.0  1.0  1.0  0.0  0.0  1.0  0.0  0.0  0.0  0.0;
            1.0  1.0  1.0  1.0  0.0  1.0  0.0  1.0  0.0  0.0;
            1.0  1.0  1.0  1.0  0.0  0.0  0.0  0.0  1.0  1.0;
            1.0  0.0  0.0  0.0  0.0  1.0  0.0  0.0  0.0  0.0;
            1.0  0.0  1.0  1.0  1.0  1.0  0.0  0.0  0.0  0.0;
            1.0  1.0  1.0  0.0  0.0  0.0  0.0  0.0  0.0  1.0;
            1.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0  1.0  0.0], atol=tol)

    @test isapprox(result.weights,
        [0.1686568559124561,
        0.07991375718719543,
        0.14006200243438863,
        0.10748052790517183,
        0.08789022388276985,
        0.12526272598854982,
        0.03067915023486491,
        0.10489168834828348,
        0.092654758940811,
        0.06250830916550884
    ], atol=tol)

end

@testset "AHP - Consistency" begin

    tol = 0.00001

    K = [
        1 7 (1 / 5) (1 / 8) (1 / 2) (1 / 3) (1 / 5) 1;
        (1 / 7) 1 (1 / 8) (1 / 9) (1 / 4) (1 / 5) (1 / 9) (1 / 8);
        5 8 1 (1 / 3) 4 2 1 1;
        8 9 3 1 7 5 3 3;
        2 4 (1 / 4) (1 / 7) 1 (1 / 2) (1 / 5) (1 / 5);
        3 5 (1 / 2) (1 / 5) 2 1 (1 / 3) (1 / 3);
        5 9 1 (1 / 3) 5 3 1 1;
        1 8 1 (1 / 3) 5 3 1 1
    ]

    dmat = makeDecisionMatrix(K)
    result::AHPConsistencyResult = ahp_consistency(dmat)

    @test isa(result, AHPConsistencyResult)

    @test result.isConsistent

    @test isapprox(result.CR, 0.07359957154133831, atol=tol)

    @test isapprox(result.CI, 0.10377539587328702, atol=tol)

    @test isapprox(result.lambda_max, 8.72642777111301, atol=tol)

    @test isapprox(result.pc, [
        8.40982182534543
        8.227923797501001
        8.95201017080425
        8.848075127995905
        8.860427038870013
        8.941497805932498
        8.946071053879708
        8.62559534857526
    ], atol=tol)
end

@testset "AHP" begin

    tol = 0.00001

    K = [
    1 7 1 / 5 1 / 8 1 / 2 1 / 3 1 / 5 1;
    1 / 7 1 1 / 8 1 / 9 1 / 4 1 / 5 1 / 9 1 / 8;
    5 8 1 1 / 3 4 2 1 1;
    8 9 3 1 7 5 3 3;
    2 4 1 / 4 1 / 7 1 1 / 2 1 / 5 1 / 5;
    3 5 1 / 2 1 / 5 2 1 1 / 3 1 / 3;
    5 9 1 1 / 3 5 3 1 1;
    1 8 1 1 / 3 5 3 1 1
]

    A1 = [1 3 1 / 5 2;
                  1 / 3 1 1 / 7 1 / 3;
                  5 7 1 4;
                  1 / 2 3 1 / 4 1]
    A2 = [1 1 / 2 4 5;
                  2 1 6 7;
                  1 / 4 1 / 6 1 3;
                  1 / 5 1 / 7 1 / 3 1]
    A3 = [1 1 / 2 1 / 6 3;
                  2 1 1 / 4 5;
                  6 4 1 9;
                  1 / 3 1 / 5 1 / 9 1]
    A4 = [1 7 1 / 4 2;
                  1 / 7 1 1 / 9 1 / 5;
                  4 9 1 5;
                  1 / 2 5 1 / 5 1]
    A5 = [1 6 2 3;
                  1 / 6 1 1 / 4 1 / 3;
                  1 / 2 4 1 2;
                  1 / 3 3 1 / 2 1]
    A6 = [1 1 / 4 1 / 2 1 / 7;
                  4 1 2 1 / 3;
                  2 1 / 2 1 1 / 5;
                  7 3 5 1]
    A7 = [1 3 7 1;
                  1 / 3 1 4 1 / 3;
                  1 / 7 1 / 4 1 1 / 7;
                  1 3 7 1]
    A8 = [1 2 5 8;
                  1 / 2 1 3 6;
                  1 / 5 1 / 3 1 3;
                  1 / 8 1 / 6 1 / 3 1];

    km = makeDecisionMatrix(K)
    as = map(makeDecisionMatrix, [A1, A2, A3, A4, A5, A6, A7, A8])

    result = ahp(as, km)

    @test isa(result, AHPResult)

    @test result.bestIndex == 3

    @test isapprox(result.scores, [0.2801050, 0.1482273, 0.3813036, 0.1903641], atol=tol)
end

@testset "NDS" begin
    cases = [
        1.0 2.0 3.0;
        2.0 1.0 3.0;
        1.0 3.0 2.0;
        4.0 5.0 6.0
    ]

    nd = makeDecisionMatrix(cases)

    result = nds(nd)

    @test isa(result, NDSResult)

    @test result.bestIndex == 4

    @test result.ranks == [0, 0, 0, 3]

end

@testset "Laplace" begin

    tol = 0.00001

    mat = [
        3000 2750 2500 2250;
        1500 4750 8000 7750;
        2000 5250 8500 11750
    ]

    dm = makeDecisionMatrix(mat)

    result = laplace(dm)

    @test isa(result, LaplaceResult)

    @test isapprox(result.expected_values, [2625.0, 5500, 6875], atol=tol)

    @test result.bestIndex == 3
end

@testset "Maximin" begin

    tol = 0.00001

    mat = [
        26 26 18 22;
        22 34 30 18;
        28 24 34 26;
        22 30 28 20
    ]

    dm = makeDecisionMatrix(mat)

    result = maximin(dm)

    @test isa(result, MaximinResult)

    @test isapprox(result.rowmins, [18, 18, 24, 20], atol=tol)

    @test result.bestIndex == 3
end

@testset "Maximax" begin

    tol = 0.00001

    mat = [
        26 26 18 22;
        22 34 30 18;
        28 24 34 26;
        22 30 28 20
    ]

    dm = makeDecisionMatrix(mat)

    result = maximax(dm)

    @test isa(result, MaximaxResult)

    @test isapprox(result.rowmaxs, [26, 34, 34, 30], atol=tol)

    @test result.bestIndex in [2, 3]
end

@testset "Minimax" begin

    tol = 0.00001

    mat = [
        26 26 18 22;
        22 34 30 18;
        28 24 34 26;
        22 30 28 20
    ]

    dm = makeDecisionMatrix(mat)

    result = minimax(dm)

    @test isa(result, MinimaxResult)

    @test isapprox(result.rowmaxs, [26, 34, 34, 30], atol=tol)

    @test result.bestIndex == 1
end

@testset "Minimin" begin

    tol = 0.00001

    mat = [
        26 26 18 22;
        22 34 30 18;
        28 24 34 26;
        22 30 28 20
    ]

    dm = makeDecisionMatrix(mat)

    result = minimin(dm)

    @test isa(result, MiniminResult)

    @test isapprox(result.rowmins, [18, 18,24,20], atol=tol)

    @test result.bestIndex in [1, 2]
end

@testset "Savage" begin

    tol = 0.00001

    mat = [
        26 26 18 22;
        22 34 30 18;
        28 24 34 26;
        22 30 28 20
    ]

    dm = makeDecisionMatrix(mat)

    result = savage(dm)

    @test isa(result, SavageResult)

    @test result.bestIndex == 4

end

@testset "Hurwicz" begin

    tol = 0.00001

    mat = [
        26 26 18 22;
        22 34 30 18;
        28 24 34 26;
        22 30 28 20
    ]

    dm = makeDecisionMatrix(mat)

    result = hurwicz(dm)

    @test isa(result, HurwiczResult)

    @test result.bestIndex == 3

end

@testset "Maximum likelihood for single criterion" begin

    tol = 0.00001

    mat = [
        26 26 18 22;
        22 34 30 18;
        28 24 34 26;
        22 30 28 20
    ]

    weights = [0.2, 0.5, 0.2, 0.1]

    dm = makeDecisionMatrix(mat)

    result = mle(dm, weights)

    @test isa(result, MLEResult)

    @test result.bestIndex == 2

    @test isapprox(result.scores, [24, 29.2, 27, 27], atol=tol)
end

@testset "Expected Regret" begin

    tol = 0.00001

    mat = [
        26 26 18 22;
        22 34 30 18;
        28 24 34 26;
        22 30 28 20
    ]

    weights = [0.2, 0.5, 0.2, 0.1]

    dm = makeDecisionMatrix(mat)

    result = expectedregret(dm, weights)

    @test isa(result, ExpectedRegretResult)

    @test result.bestIndex == 2

    @test isapprox(result.scores, [8, 2.8, 5, 5], atol=tol)
end

@testset "Game" begin

    tol = 0.00001

    mat = [-2 6 3;
        3 -4 7;
        -1 2 4]

    dm = makeDecisionMatrix(mat)
    result = game(dm)

    @test isa(result, GameResult)

    @test result.value == 0.6666666666666661

    @test isapprox(result.row_player_probabilities, [0.4666667, 0.5333333, 0.0000000], atol=tol)

end

@testset "Game - Rock & Paper & Scissors" begin

    tol = 0.00001

    mat = [0 -1 1;
      1 0 -1;
       -1 1 0]

    dm = makeDecisionMatrix(mat)
    result = game(dm)

    @test isa(result, GameResult)

    @test result.value == 0.0

    @test isapprox(result.row_player_probabilities, [0.333333, 0.333333, 0.33333], atol=tol)

end

@testset "Data Envelopment" begin

    tol = 0.00001

    x1 = [96.0, 84, 90, 81, 102, 83, 108, 99, 95]
    x2 = [300.0, 282, 273, 270, 309, 285, 294, 288, 306]

    out = [166.0, 150, 140, 136, 171, 144, 172, 170, 165]
    inp = hcat(x1, x2)

    result::DataEnvelopResult = dataenvelop(inp, out)

    @test isa(result, DataEnvelopResult)

    @test result.orderedcases == [:Case8, :Case2, :Case7, :Case1, :Case9, :Case6, :Case5, :Case4, :Case3]

    @test isapprox(result.efficiencies,
        [0.9879815986198964, 0.9999999999999999,
        0.8959653733189055, 0.9421686746987951,
        0.9659435120753173, 0.9715662650602411,
        0.9911164465786314, 1.0, 0.9841048789857857], atol=tol)

   @test isapprox(result.references[:, :Case1],
        [0.0, 0.544106, 0.0, 0.0, 0.0,
        0.0, 0.0, 0.496377, 0.0], atol=tol)

   @test isapprox(result.references[:, :Case2],
        [0.0, 1, 0, 0, 0, 0, 0, 0, 0], atol=tol)

    @test isapprox(result.references[:, :Case3],
        [0.0, 0.266193, 0, 0, 0, 0, 0, 0.588654, 0], atol=tol)

    @test isapprox(result.references[:, :Case4],
        [0.0, 0.860241, 0, 0, 0, 0, 0, 0.0409639, 0], atol=tol)

    @test isapprox(result.references[:, :Case5],
        [0.0, 0.314982, 0, 0, 0, 0, 0, 0.727957, 0], atol=tol)

    @test isapprox(result.references[:, :Case6],
        [0.0, 0.96 , 0, 0, 0, 0, 0, 0, 0], atol=tol)

    @test isapprox(result.references[:, :Case7],
        [0.0, 0 , 0, 0, 0, 0, 0, 1.01176, 0], atol=tol)

    @test isapprox(result.references[:, :Case8],
        [0.0, 0 , 0, 0, 0, 0, 0, 1.0, 0], atol=tol)

    @test isapprox(result.references[:, :Case9],
        [0.0, 0.774923 , 0, 0, 0, 0, 0, 0.286833, 0], atol=tol)

end

@testset "Grey Relational Analysis" begin

    tol = 0.0001

    df = DataFrame(
        :K1 => [105000.0, 120000, 150000, 115000, 135000],
        :K2 => [105.0, 110, 120, 105, 115],
        :K3 => [10.0, 15, 12, 20, 15],
        :K4 => [4.0, 4, 3, 4, 5],
        :K5 => [300.0, 500, 550, 600, 400],
        :K6 => [10.0, 8, 12, 9, 9]
    )
    functionlist = [minimum, maximum, minimum, maximum, maximum, minimum]

    w = [0.05, 0.20, 0.10, 0.15, 0.10, 0.40]

    result = grey(df, w, functionlist)

    @test isa(result, GreyResult)

    @test isapprox(result.scores, [0.525, 0.7007142857142857,
    0.5464285714285715, 0.5762820512820512, 0.650952380952381], atol=tol)

    @test result.bestIndex == 2
end

@testset "SAW" begin

    @testset "Example 1: 4 criteria × 4 alternatives" begin
        tol = 0.0001
        df = DataFrame(
            :c1 => [25.0, 21, 19, 22],
            :c2 => [65.0, 78, 53, 25],
            :c3 => [7.0, 6, 5, 2],
            :c4 => [20.0, 24, 33, 31]
        )
        weights = [0.25, 0.25, 0.25, 0.25]
        fns = [maximum, maximum, minimum, maximum]
        result = saw(df, weights, fns)

        @test result isa SawResult

        @test isapprox(result.scores, [0.681277, 0.725151, 0.709871, 0.784976], atol=tol)
    end

    @testset "Example 2: 7 criteria × 5 alternatives " begin
        tol = 0.0001
        decmat = [4.0  7  3  2  2  2  2;
                  4.0  4  6  4  4  3  7;
                  7.0  6  4  2  5  5  3;
                  3.0  2  5  3  3  2  5;
                  4.0  2  2  5  5  3  6]

        df = makeDecisionMatrix(decmat)
        weights = [0.283, 0.162, 0.162, 0.07, 0.085, 0.162, 0.076]
        fns = makeminmax([maximum for i in 1:7])
        result = saw(df, weights, fns)

        @test result isa SawResult
        @test isapprox(result.scores, [0.553228, 0.713485, 0.837428, 0.514657, 0.579342], atol=tol)
        @test result.bestIndex == 3
        @test result.ranking == [3, 2, 5, 1, 4]
    end
end

@testset "ARAS Additive Ratio Assessment" begin

    tol = 0.0001

    df = DataFrame(
        :K1 => [105000.0, 120000, 150000, 115000, 135000],
        :K2 => [105.0, 110, 120, 105, 115],
        :K3 => [10.0, 15, 12, 20, 15],
        :K4 => [4.0, 4, 3, 4, 5],
        :K5 => [300.0, 500, 550, 600, 400],
        :K6 => [10.0, 8, 12, 9, 9]
    )
    functionlist = [minimum, maximum, minimum, maximum, maximum, minimum]

    w = [0.05, 0.20, 0.10, 0.15, 0.10, 0.40]

    result = aras(df, w, functionlist)

    @test isa(result, ARASResult)

    @test isapprox(result.scores, [0.81424068, 0.89288620, 0.76415790, 0.84225462, 0.86540635], atol=tol)

    @test result.bestIndex == 2
end

@testset "WPM" begin
    tol = 0.0001
    decmat = [3	12.5	2	120	14	3;
                5	15	3	110	38	4;
                3	13	2	120	19	3;
                4	14	2	100	31	4;
                3	15	1.5	125	40	4]

    df = makeDecisionMatrix(decmat)

    weights = [0.221, 0.159, 0.175, 0.127, 0.117, 0.201]

    fns = [maximum, minimum, minimum, maximum, minimum, maximum]

    result = wpm(df, weights, fns)

    @test result isa WPMResult

    @test isapprox(result.scores, [0.7975224331331, 0.7532541470585, 0.7647463553356, 0.7873956894791, 0.7674278741782], atol=tol)

end

@testset "WASPAS" begin
    tol = 0.0001
    decmat = [3	12.5	2	120	14	3;
                5	15	3	110	38	4;
                3	13	2	120	19	3;
                4	14	2	100	31	4;
                3	15	1.5	125	40	4]

    df = makeDecisionMatrix(decmat)

    weights = [0.221, 0.159, 0.175, 0.127, 0.117, 0.201]

    fns = [maximum, minimum, minimum, maximum, minimum, maximum]

    result = waspas(df, weights, fns)

    @test result isa WASPASResult

    @test isapprox(result.scores, [0.805021, 0.775060, 0.770181, 0.796424, 0.788239], atol=tol)

end

@testset "EDAS" begin
    tol = 0.0001
    decmat = [5000 5 5300 450;
                4500 5 5000 400;
                4500 4 4700 400;
                4000 4 4200 400;
                5000 4 7100 500;
                5000 5 5400 450;
                5500 5 6200 500;
                5000 4 5800 450]

    df = makeDecisionMatrix(decmat)

    weights = [0.25, 0.25, 0.25, 0.25];

    fns = [maximum, maximum, minimum, minimum];

    result = edas(df, weights, fns)

    @test result isa EDASResult

    @test isapprox(result.scores, [0.759594, 0.886016, 0.697472, 0.739658, 0.059083, 0.731833, 0.641691, 0.385194], atol=tol)

end

@testset "MARCOS" begin

    tol = 0.0001
    decmat = [8.675 8.433 8.000 7.800 8.025 8.043;
    8.825 8.600 7.420 7.463 7.825 8.229;
    8.325 7.600 8.040 7.700 7.925 7.600;
    8.525 8.667 7.180 7.375 7.750 8.071]

    df = makeDecisionMatrix(decmat)

    weights = [0.19019, 0.15915, 0.19819, 0.19019, 0.15115, 0.11111];

    fns = [maximum, maximum, maximum, maximum, maximum, maximum];

    Fns = convert(Array{Function,1}, fns)

    result = marcos(df, weights, Fns)

    @test result isa MARCOSResult

    @test isapprox(result.scores, [0.684865943528, 0.672767106696, 0.662596906139, 0.661103207660], atol=tol)

end

@testset "MABAC" begin

    tol = 0.0001
    decmat = [2 1 4 7 6 6 7 3000;
    4 1 5 6 7 7 6 3500;
    3 2 6 6 5 6 8 4000;
    5 1 5 7 6 7 7 3000;
    4 2 5 6 7 7 6 3000;
    3 2 6 6 6 6 6 3500]

    df = makeDecisionMatrix(decmat)

    weights = [0.293, 0.427, 0.067, 0.027, 0.053, 0.027, 0.053, 0.053]

    fns = [maximum, maximum, maximum, maximum, maximum, maximum, maximum, minimum];

    result = mabac(df, weights, fns)

    @test result isa MABACResult

    @test isapprox(result.scores, [-0.31132, -0.10898, 0.20035, 0.04218, 0.34452, 0.20035], atol=tol)

end

@testset "Reverse minimum & maximum array" begin

    fns = [minimum, maximum, maximum, minimum, maximum]
    revfns = [maximum, minimum, minimum, maximum, minimum]

    @test reverseminmax(fns) == revfns

    @test reverseminmax(revfns) == fns
end

@testset "Make Array of minimum and maximum" begin
    result1 = makeminmax([maximum, maximum, maximum, maximum])

    @test typeof(result1) == Array{Function,1}

    @test typeof(result1[1]([1.0, 2.0, 3.0])) == Float64

    @test result1[1]([1.0, 2.0, 3.0]) == 3.0

end

@testset "MAIRCA" begin

    tol = 0.0001
    decmat = [6.952 8.000 6.649 7.268 8.000 7.652 6.316;
    7.319 7.319 6.604 7.319 8.000 7.652 5.313;
    7.000 7.319 7.652 6.952 7.652 6.952 4.642;
    7.319 6.952 6.649 7.319 7.652 6.649 5.000]

    df = makeDecisionMatrix(decmat)

    weights = [0.172, 0.165, 0.159, 0.129, 0.112, 0.122, 0.140];

    fns = [maximum, maximum, maximum, maximum, maximum, maximum, minimum];

    result = mairca(df, weights, fns)

    @test result isa MAIRCAResult

    @test isapprox(result.scores, [0.1206454, 0.0806646, 0.1458627, 0.1454237], atol=tol)

end

@testset "COPRAS" begin

    tol = 0.0001
    decmat = [2.50 240 57 45 1.10 0.333333;
    2.50 285 60 75 4.00 0.428571;
    4.50 320 100 65 7.50 1.111111;
    4.50 365 100 90 7.50 1.111111;
    5.00 400 100 90 11.00 1.111111;
    2.50 225 60 45 1.10 0.333333;
    2.50 270 57 60 4.00 0.428571;
    4.50 330 100 70 7.50 1.111111;
    4.50 365 100 80 7.50 1.111111;
    5.00 380 110 65 8.00 1.111111;
    2.50 285 65 80 4.00 0.400000;
    4.00 280 75 65 4.00 0.400000;
    4.50 365 102 95 7.50 1.111111;
    4.50 400 102 95 7.50 1.111111;
    6.00 450 110 95 11.00 1.176471;
    6.00 510 110 105 11.00 1.176471;
    6.00 330 140 110 18.50 1.395349;
    2.50 240 65 80 4.00 0.400000;
    4.00 280 75 75 4.00 0.400000;
    4.50 355 102 95 7.50 1.111111;
    4.50 385 102 90 7.50 1.111111;
    5.00 385 114 95 7.50 1.000000;
    6.00 400 110 90 11.00 1.000000;
    6.00 480 110 95 15.00 1.000000;
    6.00 440 140 100 18.50 1.200000;
    6.00 500 140 100 18.50 1.200000;
    5.00 450 125 100 15.00 1.714286;
    6.00 500 150 125 18.50 1.714286;
    6.00 515 180 140 22.00 2.307692;
    7.00 550 200 150 30.00 2.307692;
    6.00 500 180 140 15.00 2.307692;
    6.00 500 180 140 18.50 2.307692;
    6.00 500 180 140 22.00 2.307692;
    7.00 500 180 140 30.00 2.307692;
    7.00 500 200 140 37.00 2.307692;
    7.00 500 200 140 45.00 2.307692;
    7.00 500 200 140 55.00 2.307692;
    7.00 500 200 140 75.00 2.307692]

    df = makeDecisionMatrix(decmat)

    weights = [0.1667, 0.1667, 0.1667, 0.1667, 0.1667, 0.1667];

    fns = [maximum, maximum, maximum, maximum, maximum, minimum];

    result = copras(df, weights, fns)

    @test result isa COPRASResult

    @test isapprox(result.scores, [0.44194, 0.44395, 0.41042, 0.44403, 0.48177, 0.44074, 0.42430, 0.41737, 0.43474, 0.44382, 0.46625, 0.48602, 0.45019, 0.45825, 0.51953, 0.54265, 0.56134, 0.45588, 0.49532, 0.44788, 0.45014, 0.48126, 0.51586, 0.56243, 0.58709, 0.60091, 0.51850, 0.61085, 0.65888, 0.75650, 0.61430, 0.63486, 0.65542, 0.72065, 0.77680, 0.82379, 0.88253, 1.00000] , atol=tol)

end

@testset "PROMETHEE" begin
    tol = 0.005
    decmat = [42.0 35 43 51;
              89 72 92 85;
              14 85 17 40;
              57 60 45 80;
              48 32 43 40;
              71 45 60 85;
              69 40 72 55;
              64 35 70 60]
    df = makeDecisionMatrix(decmat)
    qs = [49, nothing, 45, 30]
    ps = [100, 98, 95, 80]
    weights = [0.25, 0.35, 0.22, 0.18]
    fns = makeminmax([maximum, maximum, maximum, maximum])
    prefs = convert(Array{Function,1}, [prometLinear, prometVShape, prometLinear, prometLinear])

    result = promethee(df, prefs, weights, fns, qs, ps)

    @test result isa PrometheeResult

    @test isapprox(result.scores, [0.07, -0.15, -0.06, -0.05, 0.10, 0.0, 0.03, 0.06], atol=tol)

    @test result.bestIndex == 2
end


@testset "Vector Spherical Harmonic mode indices" begin
    lmax = 10
    N = lmax + 1
    M = 2 * N - 1

    for v in 1:2
        mode_seen = falses(lmax + 1, 2 * lmax + 1)
        for l in 1:lmax, m in (-l):l
            lm = sphv_mode(l, m, v)
            @test !mode_seen[lm]
            mode_seen[lm] = true
        end
        @test all(mode_seen .≤ sphvones(Bool, N, M))
    end
end

@testset "Vector Spherical Harmonics for $T" for T in
                                                 [Float64, Complex{Float64}]
    # lmax = 100
    lmax = 4
    atol = rtol = 4 / lmax^2

    N = lmax + 1
    Θ, Φ = sph_points(N)
    @test length(Θ) == N
    M = length(Φ)

    # Test duality
    F = randn(T, N, M)
    C = sphv_transform(F)
    @test size(C) == size(F)
    @test eltype(C) == eltype(F)
    F′ = sphv_evaluate(C)
    @test size(F′) == size(F)
    @test eltype(F′) == eltype(F)
    C′ = sphv_transform(F′)
    @test C′ ≈ C
    F″ = sphv_evaluate(C′)
    @test F″ ≈ F′

    # Test gradient field, l=1, m=0
    Fθ = T[grad_Y_1_0_θ(θ, ϕ) for θ in Θ, ϕ in Φ]
    Fϕ = T[grad_Y_1_0_ϕ(θ, ϕ) for θ in Θ, ϕ in Φ]
    @test size(Fθ) == (N, M)
    @test size(Fϕ) == (N, M)
    @test isapprox(integrate(Fθ, Fϕ, Fθ, Fϕ), 1 * 2; atol=atol, rtol=rtol)
    Cθ = sphv_transform(Fθ)
    Cϕ = sphv_transform(Fϕ)
    @test Cθ[sphv_mode(1, 0, 1)] ≈ sqrt(2)
    @test sum(abs2.(Cθ)) ≈ 2
    @test sum(abs2.(Cϕ)) ≈ 0
    Cgrad = Cθ                  # l=0

    # Test curl field, l=1, m=0
    Fθ = T[curl_Y_1_0_θ(θ, ϕ) for θ in Θ, ϕ in Φ]
    Fϕ = T[curl_Y_1_0_ϕ(θ, ϕ) for θ in Θ, ϕ in Φ]
    @test isapprox(integrate(Fθ, Fϕ, Fθ, Fϕ), 1 * 2; atol=atol, rtol=rtol)
    Cθ = sphv_transform(Fθ)
    Cϕ = sphv_transform(Fϕ)
    @test sum(abs2.(Cθ)) ≈ 0
    @test Cϕ[sphv_mode(1, 0, 2)] ≈ -sqrt(2)
    @test sum(abs2.(Cϕ)) ≈ 2
    Ccurl = Cϕ                  # l=0

    # Test gradient field, l=1, m=1
    Fθ = T[grad_Y_1p1_θ(θ, ϕ) for θ in Θ, ϕ in Φ]
    Fϕ = T[grad_Y_1p1_ϕ(θ, ϕ) for θ in Θ, ϕ in Φ]
    @test isapprox(integrate(Fθ, Fϕ, Fθ, Fϕ), 1 * 2; atol=atol, rtol=rtol)
    Cθ = sphv_transform(Fθ)
    Cϕ = sphv_transform(Fϕ)
    @test Cθ[sphv_mode(1, 1, 1)] ≈ sqrt(1 / 2)
    @test Cϕ[sphv_mode(1, 1, 2)] ≈ sqrt(3 / 2)
    @test sum(abs2.(Cθ)) ≈ 1 / 2
    @test sum(abs2.(Cϕ)) ≈ 3 / 2

    # Test curl field, l=1, m=1
    Fθ = T[curl_Y_1p1_θ(θ, ϕ) for θ in Θ, ϕ in Φ]
    Fϕ = T[curl_Y_1p1_ϕ(θ, ϕ) for θ in Θ, ϕ in Φ]
    @test isapprox(integrate(Fθ, Fϕ, Fθ, Fϕ), 1 * 2; atol=atol, rtol=rtol)
    Cθ = sphv_transform(Fθ)
    Cϕ = sphv_transform(Fϕ)
    # TODO: Using converse `v` value
    @test Cθ[sphv_mode(1, 1, 2)] ≈ sqrt(3 / 2)
    @test Cϕ[sphv_mode(1, 1, 1)] ≈ -sqrt(1 / 2)
    @test sum(abs2.(Cθ)) ≈ 3 / 2
    @test sum(abs2.(Cϕ)) ≈ 1 / 2

    # Test gradient field, l=1, m=-1
    Fθ = T[grad_Y_1m1_θ(θ, ϕ) for θ in Θ, ϕ in Φ]
    Fϕ = T[grad_Y_1m1_ϕ(θ, ϕ) for θ in Θ, ϕ in Φ]
    @test isapprox(integrate(Fθ, Fϕ, Fθ, Fϕ), 1 * 2; atol=atol, rtol=rtol)
    Cθ = sphv_transform(Fθ)
    Cϕ = sphv_transform(Fϕ)
    @test Cθ[sphv_mode(1, -1, 1)] ≈ sqrt(1 / 2)
    @test Cϕ[sphv_mode(1, -1, 2)] ≈ sqrt(3 / 2)
    @test sum(abs2.(Cθ)) ≈ 1 / 2
    @test sum(abs2.(Cϕ)) ≈ 3 / 2

    # Test curl field, l=1, m=-1
    Fθ = T[curl_Y_1m1_θ(θ, ϕ) for θ in Θ, ϕ in Φ]
    Fϕ = T[curl_Y_1m1_ϕ(θ, ϕ) for θ in Θ, ϕ in Φ]
    @test isapprox(integrate(Fθ, Fϕ, Fθ, Fϕ), 1 * 2; atol=atol, rtol=rtol)
    Cθ = sphv_transform(Fθ)
    Cϕ = sphv_transform(Fϕ)
    # TODO: Using converse `v` value
    @test Cθ[sphv_mode(1, -1, 2)] ≈ sqrt(3 / 2)
    @test Cϕ[sphv_mode(1, -1, 1)] ≈ -sqrt(1 / 2)
    @test sum(abs2.(Cθ)) ≈ 3 / 2
    @test sum(abs2.(Cϕ)) ≈ 1 / 2

    # Test gradient field, l=2, m=0
    Fθ = T[grad_Y_2_0_θ(θ, ϕ) for θ in Θ, ϕ in Φ]
    Fϕ = T[grad_Y_2_0_ϕ(θ, ϕ) for θ in Θ, ϕ in Φ]
    @test isapprox(integrate(Fθ, Fϕ, Fθ, Fϕ), 2 * 3; atol=atol, rtol=rtol)
    Cθ = sphv_transform(Fθ)
    Cϕ = sphv_transform(Fϕ)
    @test Cθ[sphv_mode(2, 0, 1)] ≈ sqrt(6)
    @test sum(abs2.(Cθ)) ≈ 6
    @test sum(abs2.(Cϕ)) ≈ 0

    # Test gradient field, l=2, m=1
    Fθ = T[grad_Y_2p1_θ(θ, ϕ) for θ in Θ, ϕ in Φ]
    Fϕ = T[grad_Y_2p1_ϕ(θ, ϕ) for θ in Θ, ϕ in Φ]
    @test isapprox(integrate(Fθ, Fϕ, Fθ, Fϕ), 2 * 3; atol=atol, rtol=rtol)
    Cθ = sphv_transform(Fθ)
    Cϕ = sphv_transform(Fϕ)
    @test Cθ[sphv_mode(2, 1, 1)] ≈ sqrt(8 / 3)
    @test Cϕ[sphv_mode(2, 1, 2)] ≈ sqrt(10 / 4)
    @show chop.(Cθ)
    @show chop.(Cϕ)
    @test sum(abs2.(Cθ)) ≈ 8 / 3
    @test sum(abs2.(Cϕ)) ≈ 10 / 4
    @assert false

    # Test gradient field, l=3, m=0
    Fθ = T[grad_Y_3_0_θ(θ, ϕ) for θ in Θ, ϕ in Φ]
    Fϕ = T[grad_Y_3_0_ϕ(θ, ϕ) for θ in Θ, ϕ in Φ]
    @test isapprox(integrate(Fθ, Fϕ, Fθ, Fϕ), 3 * 4; atol=atol, rtol=rtol)
    Cθ = sphv_transform(Fθ)
    Cϕ = sphv_transform(Fϕ)
    @test Cθ[sphv_mode(3, 0, 1)] ≈ sqrt(12)
    @test sum(abs2.(Cθ)) ≈ 12
    @test sum(abs2.(Cϕ)) ≈ 0

    # Test gradient field, l=3, m=1
    Fθ = T[grad_Y_3p1_θ(θ, ϕ) for θ in Θ, ϕ in Φ]
    Fϕ = T[grad_Y_3p1_ϕ(θ, ϕ) for θ in Θ, ϕ in Φ]
    @test isapprox(integrate(Fθ, Fϕ, Fθ, Fϕ), 3 * 4; atol=atol, rtol=rtol)
    Cθ = sphv_transform(Fθ)
    Cϕ = sphv_transform(Fϕ)
    @test Cθ[sphv_mode(3, 1, 1)] ≈ sqrt(27 / 4)
    @test sum(abs2.(Cθ)) ≈ 27 / 4
    @test sum(abs2.(Cϕ)) ≈ 0

    # Test orthogonality
    lmax_test = 4
    for gc in 1:2, l in 1:lmax_test, m in (-l):l
        Cθ = zeros(T, N, M)
        Cϕ = zeros(T, N, M)
        if gc == 1
            Cθ[sphv_mode(l, m, 1)] = sqrt(1 / 2)
            Cϕ[sphv_mode(l, m, 2)] = sqrt(3 / 2)
        else
            Cθ[sphv_mode(l, m, 2)] = sqrt(3 / 2)
            Cϕ[sphv_mode(l, m, 1)] = -sqrt(1 / 2)
        end
        Fθ = sphv_evaluate(Cθ)
        Fϕ = sphv_evaluate(Cϕ)
        for gc′ in 1:2, l′ in 1:lmax_test, m′ in (-l′):l′
            gc′ == gc || continue
            Cθ′ = zeros(T, N, M)
            Cϕ′ = zeros(T, N, M)
            if gc′ == 1
                Cθ′[sphv_mode(l′, m′, 1)] = sqrt(1 / 2)
                Cϕ′[sphv_mode(l′, m′, 2)] = sqrt(3 / 2)
            else
                Cθ′[sphv_mode(l′, m′, 2)] = sqrt(3 / 2)
                Cϕ′[sphv_mode(l′, m′, 1)] = -sqrt(1 / 2)
            end
            Fθ′ = sphv_evaluate(Cθ′)
            Fϕ′ = sphv_evaluate(Cϕ′)
            int = integrate(Fθ, Fϕ, Fθ′, Fϕ′)
            δ = l * (l + 1) * (gc == gc′ && l == l′ && m == m′)
            if !isapprox(int, δ; atol=atol, rtol=rtol)
                @show gc gc′ l l′ m m′ int δ
                @assert false
            end
            @test isapprox(int, δ; atol=atol, rtol=rtol)
        end
    end
end


ϕ_vortex_33 =  generate_complex_potential_vortex(; pos = complex(0.0, 1.0)m, vorticity = 1.0m²/s / 2π)
ϕ_source_33 = generate_complex_potential_source(; pos = complex(3.0, 0.0)m, massflowout = 1.0m²/s)
ϕ_sink_33 = generate_complex_potential_source(; pos = -complex(3.0, 0.0)m, massflowout = -1.0m²/s)
ϕ_33(p) = ϕ_vortex_33(p) + ϕ_source_33(p) + ϕ_sink_33(p)


struct POMDP
    γ   # discount factor
    𝒮   # state space
    𝒜   # action space
    𝒪   # observation space
    T   # transition function
    R   # reward function
    O   # observation function
    TRO # sample transition, reward, and observation
end


#    Bessel functions of the second kind of order zero and one
#                       bessely0, bessely1
#
#    Calculation of bessely0 is done in three branches using polynomial approximations
#
#    Branch 1: x <= 5.0
#              bessely0 = R(x^2) + 2*log(x)*besselj0(x) / pi
#    where r1 and r2 are zeros of J0
#    and P3 and Q8 are a 3 and 8 degree polynomial respectively
#    Polynomial coefficients are from [1] which is based on [2].
#    For tiny arugments the power series expansion is used.
#
#    Branch 2: 5.0 < x < 25.0
#              bessely0 = sqrt(2/(pi*x))*(sin(x - pi/4)*R7(x) - cos(x - pi/4)*R8(x))
#    Hankel's asymptotic expansion is used
#    where R7 and R8 are rational functions (Pn(x)/Qn(x)) of degree 7 and 8 respectively
#    See section 4 of [3] for more details and [1] for coefficients of polynomials
#
#   Branch 3: x >= 25.0
#              bessely0 = sqrt(2/(pi*x))*beta(x)*(sin(x - pi/4 - alpha(x))
#   See modified expansions given in [3]. Exact coefficients are used.
#
#   Calculation of bessely1 is done in a similar way as bessely0.
#   See [3] for details on similarities.
#
# [1] https://github.com/deepmind/torch-cephes
# [2] Cephes Math Library Release 2.8:  June, 2000 by Stephen L. Moshier
# [3] Harrison, John. "Fast and accurate Bessel function computation."
#     2009 19th IEEE Symposium on Computer Arithmetic. IEEE, 2009.
#

"""
    bessely0(x::T) where T <: Union{Float32, Float64}

Bessel function of the second kind of order zero, ``Y_0(x)``.
"""
function bessely0(x::T) where T <: Union{Float32, Float64}
    if x <= zero(x)
        if iszero(x)
            return T(-Inf)
        else
            return throw(DomainError(x, "NaN result for non-NaN input."))
        end
    elseif isinf(x)
        return zero(x)
    end
    return _bessely0_compute(x)
end
function _bessely0_compute(x::Float64)
    T = Float64
    if x <= 5.0
        z = x * x
        w = evalpoly(z, YP_y0(T)) / evalpoly(z, YQ_y0(T))
        w += TWOOPI(T) * log(x) * besselj0(x)
        return w
    elseif x < 25.0
        w = 5.0 / x
        z = w * w
        p = evalpoly(z, PP_y0(T)) / evalpoly(z, PQ_y0(T))
        q = evalpoly(z, QP_y0(T)) / evalpoly(z, QQ_y0(T))
        xn = x - PIO4(T)
        sc = sincos(xn)
        p = p * sc[1] + w * q * sc[2]
        return p * SQ2OPI(T) / sqrt(x)
    else
        if x < 120.0
            p = (one(T), -1/16, 53/512, -4447/8192, 3066403/524288, -896631415/8388608, 796754802993/268435456, -500528959023471/4294967296)
            q = (-1/8, 25/384, -1073/5120, 375733/229376, -55384775/2359296, 24713030909/46137344, -7780757249041/436207616)
        else
            p = (one(T), -1/16, 53/512, -4447/8192)
            q = (-1/8, 25/384, -1073/5120, 375733/229376)
        end
        xinv = inv(x)
        x2 = xinv*xinv

        p = evalpoly(x2, p)
        a = SQ2OPI(T) * sqrt(xinv) * p
        xn = muladd(xinv, evalpoly(x2, q), - PIO4(T))

        # the following computes b = sin(x + xn) more accurately
        # see src/misc.jl
        b = sin_sum(x, xn)
        return a * b
    end
end
function _bessely0_compute(x::Float32)
    T = Float32
    if x <= 2.0f0
        z = x * x
        YZ1 =  0.43221455686510834878f0
        w = (z - YZ1) * evalpoly(z, YP_y1(T))
        w += TWOOPI(T) * log(x) * besselj0(x)
        return w
    else
        q = 1.0f0 / x
        w = sqrt(q)
        p = w * evalpoly(q, MO_y1(T))
        w = q * q
        xn = q * evalpoly(w, PH_y1(T)) - PIO4(T)
        p = p * sin(xn + x)
        return p
    end
end

"""
    bessely1(x::T) where T <: Union{Float32, Float64}

Bessel function of the second kind of order one, ``Y_1(x)``.
"""
function bessely1(x::T) where T <: Union{Float32, Float64}
    if x <= zero(x)
        if iszero(x)
            return T(-Inf)
        else
            return throw(DomainError(x, "NaN result for non-NaN input."))
        end
    elseif isinf(x)
        return zero(x)
    end
    return _bessely1_compute(x)
end
function _bessely1_compute(x::Float64)
    T = Float64
    if x <= 5
        z = x * x
        w = x * (evalpoly(z, YP_y1(T)) / evalpoly(z, YQ_y1(T)))
        w += TWOOPI(T) * (besselj1(x) * log(x) - inv(x))
        return w
    elseif x < 25.0
        w = 5.0 / x
        z = w * w
        p = evalpoly(z, PP_j1(T)) / evalpoly(z, PQ_j1(T))
        q = evalpoly(z, QP_j1(T)) / evalpoly(z, QQ_j1(T))
        xn = x - THPIO4(T)
        sc = sincos(xn)
        p = p * sc[1] + w * q * sc[2]
        return p * SQ2OPI(T) / sqrt(x)
    else
        if x < 120.0
            p = (one(T), 3/16, -99/512, 6597/8192, -4057965/524288, 1113686901/8388608, -951148335159/268435456, 581513783771781/4294967296)
            q = (3/8, -21/128, 1899/5120, -543483/229376, 8027901/262144, -30413055339/46137344, 9228545313147/436207616)
        else
            p = (one(T), 3/16, -99/512, 6597/8192)
            q = (3/8, -21/128, 1899/5120, -543483/229376)
        end
        xinv = inv(x)
        x2 = xinv*xinv

        p = evalpoly(x2, p)
        a = SQ2OPI(T) * sqrt(xinv) * p
        xn = muladd(xinv, evalpoly(x2, q), - 3 * PIO4(T))

        # the following computes b = sin(x + xn) more accurately
        # see src/misc.jl
        b = sin_sum(x, xn)
        return a * b
    end
end
function _bessely1_compute(x::Float32)
    T = Float32
    if x <= 2.0f0
        z = x * x
        YO1 = 4.66539330185668857532f0
        w = (z - YO1) * x * evalpoly(z, YP32)
        w += TWOOPI(Float32) * (besselj1(x) * log(x) - inv(x))
        return w
    else
        q = inv(x)
        w = sqrt(q)
        p = w * evalpoly(q, MO132)
        w = q * q
        xn = q * evalpoly(w, PH132) - THPIO4(Float32)
        p = p * sin(xn + x)
        return p
    end
end


module ApproxTools

using LinearAlgebra
using SparseArrays
using FFTW
using IterTools
using Statistics

include("tucker.jl")
export tucker

include("grideval.jl")
export grideval, @gridfun

include("LogNumbers.jl")

include("ArgumentWrappers.jl")

include("LinearCombinations.jl")
export LinearCombination, coeffs, basis, @evaluate

include("approximate.jl")
export approximate

include("inner_products.jl")
export WeightedL2, LegendreInnerProduct, JacobiInnerProduct, evaluate, evalpoints

include("points/ChebyshevPoints.jl")
export ChebyshevPoints

include("points/TrapezoidalPoints.jl")
export TrapezoidalPoints

include("points/Midpoints.jl")
export Midpoints

include("bases/Basis.jl")
export Basis

include("bases/Monomials.jl")
export Monomials

include("bases/NegativeMonomials.jl")
export NegativeMonomials

include("bases/Chebyshev.jl")
export Chebyshev

include("bases/Combined.jl")
export Combined

include("bases/Poles.jl")
export Poles

include("bases/Weighted.jl")
export Weighted

include("bases/Mapped.jl")
export Mapped

include("bases/Barycentric.jl")
export Barycentric

include("bases/LanczosBasis.jl")
export LanczosBasis

end # module


const ROOT2OVER2 = 0.7071067811865476
const SKEW_2D = 0.366025403784439
const UNSKEW_2D = -0.21132486540518713

const RSQUARED_2D = 2.0f0 / 3.0f0

export ImproveX
struct ImproveX <: NoiseType end

"""
2D OpenSimplex2S/SuperSimplex noise, standard lattice orientation
"""
function noise(seed::Int64, x, y)
    # Get points for A2* lattice
    s = SKEW_2D * (x + y)
    _noise(seed, x + s, y + s)
end

"""
2D OpenSimplex2S/SuperSimplex noise, with Y pointing down the main diagonal.
Might be better for a 2D sandbox style game, where Y is vertical.
Probably slightly less optimal for heightmaps or continent maps,
unless your map is centered around an equator. It's a slight
difference, but the option is here to make it easy.
"""
function noise(::Type{ImproveX}, seed::Int64, x, y)
    # Skew transform and rotation baked into one.
    xx = x * ROOT2OVER2
    yy = y * (ROOT2OVER2 * (1 + 2 * SKEW_2D))
    _noise(seed, yy + xx, yy - xx)
end

dxy(x, y) = RSQUARED_2D - x * x - y * y

"""
2D  OpenSimplex2S/SuperSimplex noise base.
"""
function _noise(seed::Int64, xs, ys)

    # Get base points and offsets.
    xsb = fastFloor(xs)
    ysb = fastFloor(ys)
    xi = Float32(xs - xsb)
    yi = Float32(ys - ysb)

    # Prime pre-multiplication for hash.
    xsbp = (xsb * PRIME_X)%Int64
    ysbp = (ysb * PRIME_Y)%Int64

    # Unskew.
    t = (xi + yi) * Float32(UNSKEW_2D)
    dx0 = xi + t
    dy0 = yi + t

    UNSKEW_2D_2X = (1 + 2 * UNSKEW_2D)

    # First vertex.
    a0 = dxy(dx0, dy0)
    value = grad(a0, seed, xsbp, ysbp, dx0, dy0)

    # Second vertex.
    a = Float32(2 * UNSKEW_2D_2X * (1 / UNSKEW_2D + 2)) * t +
        (Float32(-2 * UNSKEW_2D_2X * UNSKEW_2D_2X) + a0)
    value += grad(a, seed, xsbp + PRIME_X, ysbp + PRIME_Y,
                  dx0 - Float32(UNSKEW_2D_2X), dy0 - Float32(UNSKEW_2D_2X))

    # Third and fourth vertices.
    # Nested conditionals were faster than compact bit logic/arithmetic.
    xmyi = xi - yi
    if t < UNSKEW_2D
        if xi + xmyi > 1
            dx = dx0 - Float32(3 * UNSKEW_2D + 2)
            dy = dy0 - Float32(3 * UNSKEW_2D + 1)
            a = dxy(dx, dy)
            if a > 0
                value += grad(a, seed, xsbp + (PRIME_X << 1), ysbp + PRIME_Y, dx, dy)
            end
        else
            dx = dx0 - Float32(UNSKEW_2D)
            dy = dy0 - Float32(UNSKEW_2D + 1)
            a = dxy(dx, dy)
            if a > 0
                value += grad(a, seed, xsbp, ysbp + PRIME_Y, dx, dy)
            end
        end

        if yi - xmyi > 1
            dx = dx0 - Float32(3 * UNSKEW_2D + 1)
            dy = dy0 - Float32(3 * UNSKEW_2D + 2)
            a = dxy(dx, dy)
            if a > 0
                value += grad(a, seed, xsbp + PRIME_X, ysbp + (PRIME_Y << 1), dx, dy)
            end
        else
            dx = dx0 - Float32(UNSKEW_2D + 1)
            dy = dy0 - Float32(UNSKEW_2D)
            a = dxy(dx, dy)
            if a > 0
                value += grad(a, seed, xsbp + PRIME_X, ysbp, dx, dy)
            end
        end
    else
        if xi + xmyi < 0
            dx = dx0 + Float32(1 + UNSKEW_2D)
            dy = dy0 + Float32(UNSKEW_2D)
            a = dxy(dx, dy)
            if a > 0
                value += grad(a, seed, xsbp - PRIME_X, ysbp, dx, dy)
            end
        else
            dx = dx0 - Float32(UNSKEW_2D + 1)
            dy = dy0 - Float32(UNSKEW_2D)
            a = dxy(dx, dy)
            if a > 0
                value += grad(a, seed, xsbp + PRIME_X, ysbp, dx, dy)
            end
        end

        if yi < xmyi
            dx = dx0 + Float32(UNSKEW_2D)
            dy = dy0 + Float32(UNSKEW_2D + 1)
            a = dxy(dx, dy)
            if a > 0
                value += grad(a, seed, xsbp, ysbp - PRIME_Y, dx, dy)
            end
        else
            dx = dx0 - Float32(UNSKEW_2D)
            dy = dy0 - Float32(UNSKEW_2D + 1)
            a = dxy(dx, dy)
            if a > 0
                value += grad(a, seed, xsbp, ysbp + PRIME_Y, dx, dy)
            end
        end
    end

    return Float32(value)
end


@testset "GMM" begin
    Random.seed!(28374544);

    # generate dataset
    d = 2
    theta = [0,3,5,7] * (pi/4)
    k = length(theta)
    X = hcat([MultivariateMixtures.clusters(t, 250, d) for t in theta]...)

    G1 = fit_mle(MvNormal, X[:,1:250])
    GMM = fit_mm(FullNormal, X, k, tol=1e-10)
    @test Distributions.component_type(GMM) <: FullNormal
    @test ncomponents(GMM) == k
    @test probs(GMM) ≈ fill(1/4, k) atol=1e-2
    G2 = component(GMM,2)
    @test mean(G1) ≈ mean(G2) atol=1e-2
    @test cov(G1) ≈ cov(G2) atol=1e-2
    @test length(GMM) == d
    @test size(GMM) == (d,)
    @test mean(GMM) ≈ mean(X, dims=2) atol=1e-2
    @test cov(GMM) ≈ cov(X, dims=2) atol=1e-1

    GMM = fit_mm(FullNormal, X, k, tol=1e-5, homoscedastic=true)
    G3 = component(GMM,1)
    @test mean(G1) ≈ mean(G3) atol=1e-2
    @test cov(G1) ≈ cov(G3) atol=0.5
    @test cov(G3) == cov(component(GMM,1))

    GMM = fit_mm(DiagNormal, X, k, tol=1e-4)
    @test Distributions.component_type(GMM) <: DiagNormal
    @test ncomponents(GMM) == k
    @test probs(GMM) ≈ fill(1/4, k) atol=1e-2

    GMM = fit_mm(IsoNormal, X, k, tol=1e-4)
    @test Distributions.component_type(GMM) <: IsoNormal

    X = hcat([MultivariateMixtures.clusters(t, 250, 5) for t in theta]...)
    GMM = fit_mm(FullNormal, X, k, tol=1e-3)
    @test length(GMM) == 5

end


using AbstractGPs
using ConjugateComputationVI
using Distributions
using Optim
using ParameterHandling
using Plots
using Random
using RDatasets
using StatsFuns
using Stheno
using Zygote

using ConjugateComputationVI: GaussHermiteQuadrature, UnivariateFactorisedLikelihood

# Specify a model.
θ_init = (
    f1 = (
        scale=positive(1.9),
        stretch=positive(0.8),
    ),
    f2 = (
        scale=positive(1.0),
        stretch=positive(0.3),
    ),
);

θ_init_flat, unflatten = ParameterHandling.flatten(θ_init);

function build_latent_gp(θ::AbstractVector{<:Real})
    return build_latent_gp(ParameterHandling.value(unflatten(θ)))
end
function build_latent_gp(θ::NamedTuple)
    gp = @gppp let
        f1 = θ.f1.scale * stretch(GP(SEKernel()), θ.f1.stretch)
        f2 = θ.f2.scale * stretch(GP(SEKernel()), θ.f2.stretch)
        f3 = f1 + f2
    end
    lik = UnivariateFactorisedLikelihood(f -> Exponential(exp(f)))
    return LatentGP(gp, lik, 1e-3)
end

# Generate some synthetic data.
x_f2 = GPPPInput(:f2, range(-5.0, 5.0; length=100));
x_f3 = GPPPInput(:f3, range(-5.0, 5.0; length=150));
x = BlockData(x_f2, x_f3);
y = rand(build_latent_gp(θ_init_flat)(x)).y;
y_f2, y_f3 = split(x, y);

# Add some noise to the initialisation to make this more interesting.
f_approx_post, results_summary = ConjugateComputationVI.optimize_elbo(
    build_latent_gp,
    GaussHermiteQuadrature(10),
    x,
    y,
    θ_init_flat + randn(length(θ_init_flat)),
    BFGS(
        alphaguess = Optim.LineSearches.InitialStatic(scaled=true),
        linesearch = Optim.LineSearches.BackTracking(),
    ),
    Optim.Options(
        show_trace = true,
        iterations=50,
        f_calls_limit=50,
    ),
);

function approx_post_marginal_samples(x::AbstractVector, N::Int)
    return map(marginals(f_approx_post(x, 1e-6))) do latent_marginal
        f = rand(latent_marginal, N)
        return rand.(Exponential.(exp.(f)))
    end
end

function approx_post_95_CI(x::AbstractVector, N::Int)
    samples = approx_post_marginal_samples(x, N)
    return quantile.(samples, Ref((0.025, 0.5, 0.975)))
end

θ_opt = results_summary.training_results.minimizer;
x_pr_raw = range(-6.0, 6.0; length=250);
x_pr_f1 = GPPPInput(:f1, x_pr_raw);
x_pr_f2 = GPPPInput(:f2, x_pr_raw);
x_pr_f3 = GPPPInput(:f3, x_pr_raw);
x_pr = BlockData(x_pr_f1, x_pr_f2, x_pr_f3);

# Plot the predictions.
_, qs_f2, qs_f3 = split(x_pr, approx_post_95_CI(x_pr, 10_000));
p1 = plot(
    x_pr_raw, getindex.(qs_f2, 1);
    linealpha=0,
    fillrange=getindex.(qs_f2, 3),
    label="95% CI",
    fillalpha=0.3,
);
scatter!(p1, x_f2.x, y_f2; markersize=2, label="Observations f2");

p2 = plot(
    x_pr_raw, getindex.(qs_f3, 1);
    linealpha=0,
    fillrange=getindex.(qs_f3, 3),
    label="95% CI",
    fillalpha=0.3,
);
scatter!(p2, x_f3.x, y_f3; markersize=2, label="Observations of f3")

p3 = plot();
plot!(p3, x_pr_raw, f_approx_post(x_pr_f1, 1e-6); ribbon_scale=3, color=:blue, label="f1");
sampleplot!(p3, x_pr_raw, f_approx_post(x_pr_f1, 1e-6); samples=3, color=:blue);
plot!(p3, x_pr_raw, f_approx_post(x_pr_f2, 1e-6); ribbon_scale=3, color=:red, label="f2");
sampleplot!(p3, x_pr_raw, f_approx_post(x_pr_f2, 1e-6); samples=3, color=:red);
plot!(p3, x_pr_raw, f_approx_post(x_pr_f3, 1e-6); ribbon_scale=3, color=:green, label="f3");
sampleplot!(p3, x_pr_raw, f_approx_post(x_pr_f3, 1e-6); samples=3, color=:green);

plot(p1, p2, p3; layout=(3, 1))


function getBestMove(M,player,score,moves,movePossible,makeMove!,gameOver)
    # uses minimax algorithm to find best move

    # initialise algorithm values
    bestVal = -1000
    bestMove = -1
    bestDepth = 1000
    moveVal = 0

    for i in moves # search over all candidate moves
        if movePossible(M,i)
            N = deepcopy(M)
            N = makeMove!(N,i,player) # play the move
            moveVal, depth = minimax(N,0,false,3-player,moves,score,movePossible,makeMove!,gameOver) # evaluate it

            print("$i: $moveVal $depth\n")
            # if the move is better than the current best, keep it
            # if it's equal, sometimes keep it
            if (moveVal > bestVal) || (moveVal==bestVal && depth<=bestDepth && rand()<0.75)
                # or if it's equivalent, sometimes keep it
                bestVal = moveVal
                bestMove = i
                bestDepth = depth
            end
        end
    end
    print("Choosing $bestMove\n")
    return bestMove # we are left with one of the optimal moves
end

function minimax(M,depth,isMax,player,moves,score,movePossible,makeMove!,gameOver)
    # minimax step
    depthOut = depth
    if gameOver(M)
        return isMax ? score(M,player) : -score(M,player), depthOut
    else
        best = isMax ? -1000 : 1000
        for i in moves # serach over candidate moves
            if movePossible(M,i)
                N = deepcopy(M)
                N = makeMove!(N,i,player) # play the move

                # pass on to the minimiser
                value, depthOut = minimax(N,depth+1,!isMax,3-player,moves,score,movePossible,makeMove!,gameOver)

                # did this move do better?
                best = isMax ? max(best, value) : min(best, value)
            end
        end
        return best, depthOut
    end
end

function getBestMoveAB(M,player,score,moves,movePossible,makeMove!,gameOver,maxDepth)
    # uses minimax algorithm to find best move

    # initialise algorithm values
    alpha = -1000
    beta = 1000
    bestVal = -1000
    bestMove = [-1]
    bestDepth = [1000]
    moveVal = 0

    for i in moves # search over all candidate moves
        if movePossible(M,i)
            N = deepcopy(M)
            N = makeMove!(N,i,player) # play the move
            moveVal, depth = minimaxAB(alpha,beta,N,0,false,3-player,
                moves,score,movePossible,makeMove!,gameOver,maxDepth) # evaluate it

            print("$i: $moveVal $depth ($bestVal)\n")
            # if the move is better than the current best, keep it
            # if it's equal, sometimes keep it

            if moveVal > bestVal
                bestVal = moveVal
                bestMove = [i]
                bestDepth = [depth]
            elseif moveVal==bestVal
                push!(bestMove,i)
                push!(bestDepth,depth)
            end
        end
    end
    if bestVal<0
        goodIdxs = bestDepth.==maximum(bestDepth)
    else
        goodIdxs = bestDepth.==minimum(bestDepth)
    end
    bestMoveOptions = bestMove[goodIdxs]
    bestMove = length(bestMoveOptions)>1 ? rand(bestMoveOptions) : bestMoveOptions[1]
    print("From $bestMoveOptions, choosing $bestMove\n")
    return bestMove # we are left with one of the optimal moves
end

function minimaxAB(alpha,beta,M,depth,isMax,player,moves,score,
        movePossible,makeMove!,gameOver,maxDepth)
    # minimax step
    depthOut = depth
    if depth>maxDepth
        return score(M,player), depth
    elseif gameOver(M)
        return isMax ? score(M,player) : -score(M,player), depth
    else
        bestValue = isMax ? -1000 : 1000
        for i in moves # serach over candidate moves
            if movePossible(M,i)
                N = deepcopy(M)
                N = makeMove!(N,i,player) # play the move
                valueTemp, depthOut = minimaxAB(alpha,beta,N,depth+1,!isMax,
                    3-player,moves,score,movePossible,makeMove!,gameOver,maxDepth)

                if isMax
                    bestValue = max(bestValue,valueTemp)
                    alpha = max(alpha,bestValue)
                else
                    bestValue = min(bestValue,valueTemp)
                    beta = min(beta,bestValue)
                end
                if alpha>=beta
                    return bestValue, depthOut
                end
            end
        end

        return bestValue, depthOut
    end
end


function demoSumProdStandardiseVariables()
    #DEMOSUMPROD Sum-Product algorithm test :
    # Variable order is arbitary
    r=randperm(10)
    variables=r[1:5] # In this code, the variables do not need to be numbered 1:end
    a, b, c, d, e=variables
    nstates=round(Int64,3*rand(1,5)+2) # random number of states for each variable

    pot=Array(PotArray,5)
    pot[1]=PotArray([a b],rand(nstates[memberinds([a b],variables)]...))
    pot[2]=PotArray([b c d],rand(nstates[memberinds([b c d],variables)]...));
    pot[3]=PotArray([c],rand(nstates[memberinds([c],variables)]...));
    pot[4]=PotArray([e d],rand(nstates[memberinds([e d],variables)]...));
    pot[5]=PotArray([d],rand(nstates[memberinds([d],variables)]...));

    newpot,originalvariables=standardisevariables(pot) # translate the variables into 1:end form
    A = FactorGraph(newpot);
    newmarg, mess, normconst=sumprod(newpot,A)

    marg=returnvariables(newmarg,originalvariables) # translate back to orginal variables
    # check if this is correct:
    jointpot =prod(pot); V=length(pot);
    for i=variables
        mtable=[condpot(marg[whichpot(marg,i)]).content condpot(jointpot,i).content]
        println("marginal of variable $i:\n Factor Graph\t\t Raw summation");
        println(mtable)
    end
    tmp=sum(prod(pot))
    println("Normalisation constant:\nFactorGraph\t=$(normconst.content)\nRaw summation\t=$(tmp.content)")

end


"""
Maximize sharp coefficient allocation.
"""
function max_sharpe(Σ, r̄, rf)
    one = ones(size(r̄, 1))
    invΣ = pinv(Σ, 1E-25)
    v = invΣ * (r̄ .- one * rf)
    return v
end

"""
Equal weights.
"""
function equal_weights(numA)
    return fill(1 / numA, numA)
end


# steady-state solution Kienle 2 layer 1998

@. D = 1/3musp

function fluence_spatial_freq(z, s, μa, μsp, l)

    D = @. 1/3*μsp
    α = @. sqrt((D*s^2 + μa)/D)
    A = 1
    zb = 2*A*D[1]
    z0 = 1/(μsp[1] + μa[1])

    ϕ = BigFloat(0.0)

    if z < l && z >= 0

        ϕ = 1/(D[1]*α[1]*cosh(α[1]*(l + zb)) + D[2]*α[2]*sinh(α[1]*(l + zb)))

        ϕ = ϕ*sinh(α[1]*(zb + z0))/(D[1]* α[1])
        ϕ = ϕ*(D[1]*α[1]*cosh(α[1]*(l - z)) + D[2]*α[2]*sinh(α[1]*(l - z)))

        if z < z0
            ϕ  = ϕ - sinh(α[1]*(z0 - z))/(D[1]*α[1])
        end

    else
        ϕ = sinh(α[1]*(zb + z0))*exp(α[2]*(l - z))
        ϕ = ϕ/(D[1]*α[1]*cosh(α[1]*(l + zb)) + D[2]*α[2]*sinh(α[1]*(l + zb)))
    end

    return ϕ
end

function fluence_steadystate(ρ, z, μa, μsp, l)

   ϕ = quadgk((s) -> compute_integral(z, s, μa, μsp, l, ρ), 0, 400)
   return ϕ[1]/2π

end

function compute_integral(z, s, μa, μsp, l, ρ)
    return fluence_spatial_freq(z, s, μa, μsp, l)*s*besselj0(ρ*s)
end

function fluence_spatial_freq(z, s, μa, μsp, l)
    # keep l under 10 because of bounds error
    D = @. 1/3μsp
    α = @. sqrt((D*s^2 + μa)/D)
    A = 1
    zb = 2*A*D[1]
    z0 = 1/(μsp[1] + μa[1])

    if 2*cosh(α[1]*(l + zb)) == Inf # cosh(715) returns inf
        ϕ = fluence_spatial_freq_large(z, α, z0, l, D)
    else
        ϕ = fluence_spatial_freq_small(z, α, zb, z0, l, D)
    end

    return ϕ
end

function fluence_spatial_freq_small(z, α, zb, z0, l, D)

    if z < l

        ϕ = (cosh(α[1]*(l - z))/(D[2]*α[2]) + sinh(α[1]*(l - z))/(D[1]*α[1]))
        ϕ = ϕ/(cosh(α[1]*(l + zb))/(D[2]*α[2]) + sinh(α[1]*(l + zb))/(D[1]*α[1]))
        ϕ = ϕ*sinh(α[1]*(zb + z0))/(D[1]*α[1])

        if z < z0
            ϕ  = ϕ - sinh(α[1]*(z0 - z))/(D[1]*α[1])
        end

    else
        ϕ = sinh(α[1]*(zb + z0))/(D[1]*α[1]*D[2]*α[2])
        ϕ = ϕ/(cosh(α[1]*(l + zb))/(D[2]*α[2]) + sinh(α[1]*(l + zb))/(D[1]*α[1]))
        ϕ = ϕ*exp(α[2]*(l - z))
    end

    if isinf(ϕ)
        return NaN
    end

    return ϕ
end

function fluence_spatial_freq_large(z, α, z0, l, D)

    if z < z0
        ϕ = 0.0

    elseif z < l && z >= z0
        ϕ = exp(α[1]*z0 - α[1]*z)/(2*D[1]*α[1])

    else
        ϕ = exp(α[1]*z0 + α[2]*l - α[2]*z - α[1]*l)/(D[1]*α[1] + D[2]*α[2])
    end

end

function ss_compare(ρ, z, μa, μsp)

    D = 1/3μsp
    A = 1
    z0 = 1/(μsp + μa)
    zb = 2*A*D
    μe = sqrt(3*μa*(μa + μsp))

    ϕ = exp(-μe*sqrt((z - z0)^2 + ρ^2))/sqrt((z - z0)^2 + ρ^2)
    ϕ = ϕ - exp(-μe*sqrt((z + z0 + 2*zb)^2 + ρ^2))/sqrt((z + z0 + 2*zb)^2 + ρ^2)
    ϕ = ϕ/(4*π*D)
end

function fluence_spatial_freq1(z, s, μa, μsp, l)

    D = @. 1/3μsp
    α = @. sqrt((D*s^2 + μa)/D)
    A = 1
    zb = 2*A*D[1]
    z0 = 1/(μsp[1] + μa[1])

    if z < l

        ϕ = D[1]*α[1]*(1 + exp(-2*α[1]*(l - z))) +  D[2]*α[2]*(1 - exp(-2*α[1]*(l - z)))
        ϕ = ϕ/(D[1]*α[1]*(1 + exp(-2*α[1]*(l + zb))) +  D[2]*α[2]*(1 - exp(-2*α[1]*(l + zb))))
        ϕ = ϕ*(1 + exp(-2*α[1]*(zb + z0)))/(2*D[1]*α[1])
        ϕ = ϕ*exp(α[1]*z0 - α[1]*z)

        if z < z0
            ϕ = ϕ - exp(α[1]*(z0 - z))*(1 - exp(-2*α[1]*(z0 - z)))/(2*D[1]*α[1])
        end

    else

        ϕ = exp(α[1]*(z0 - l) + α[2]*(l - z))
        ϕ = ϕ*(1 - exp(-2*α[1]*(zb + z0)))
        ϕ = ϕ/(D[1]*α[1]*(1 + exp(-2*α[1]*(l + zb))) + D[2]*α[2]*(1 - exp(-2*α[1]*(l + zb))))
    end

    return ϕ
end

ϕ = ϕ - exp(α[1]*(z0 - z))*(1 - exp(-2*α[1]*(z0 - z)))/(2*D[1]*α[1])


function buildptdf(branches::Array{T}, nodes::Array{Bus}, dist_slack::Array{Float64}=[0.1] ) where {T<:Branch}

    buscount = length(nodes)
    linecount = length(branches)
    line_axis = [branch.name for branch in branches]
    num_bus = Dict{Int,Int}()

    for (ix,b) in enumerate(nodes)
        if b.number < -1
            @error "buses must be numbered consecutively in the bus/node matrix" # TODO: raise error here?
        end
        num_bus[b.number] = ix
    end

    A = zeros(Float64,buscount,linecount);
    inv_X = zeros(Float64,linecount,linecount);

   #build incidence matrix
   #incidence_matrix = A

    for (ix,b) in enumerate(branches)

        isa(b,DCLine) ? continue : true

        A[num_bus[b.connectionpoints.from.number], ix] =  1;

        A[num_bus[b.connectionpoints.to.number], ix] = -1;

        if isa(b,Transformer2W)
            inv_X[ix,ix] = 1/b.x;

        elseif isa(b,TapTransformer)
            inv_X[ix,ix] = 1/(b.x*b.tap);

        elseif isa(b, Line)
            inv_X[ix,ix] = 1/b.x;

        elseif isa(b,PhaseShiftingTransformer)
            y = 1 / (b.r + b.x * 1im)
            y_a = y / (b.tap * exp(b.α * 1im * (π / 180)))
            inv_X[ix,ix] = 1/imag(y_a)
        end
    end
    slacks = [num_bus[n.number] for n in nodes if n.bustype == "SF"]
    slack_position = slacks[1]
    bus_axis = [bus.name for bus in nodes]

    B = gemm('N','T', gemm('N','N',A[setdiff(1:end, slack_position),1:end] ,inv_X), A[setdiff(1:end, slack_position),1:end])

    if dist_slack[1] == 0.1 && length(dist_slack) ==1
        (B, bipiv, binfo) = getrf!(B)
        S_ = gemm('N','N', gemm('N','T', inv_X, A[setdiff(1:end, slack_position), :]), getri!(B, bipiv) )
        S = hcat(S_[:,1:slack_position-1],zeros(linecount,),S_[:,slack_position:end])

    elseif dist_slack[1] != 0.1 && length(dist_slack)  == buscount
        @info "Distributed bus"
        (B, bipiv, binfo) = getrf!(B)
        S_ = gemm('N','N', gemm('N','T', inv_X, A[setdiff(1:end, slack_position), :]), getri!(B, bipiv) )
        S = hcat(S_[:,1:slack_position-1],zeros(linecount,),S_[:,slack_position:end])
        slack_array =dist_slack/sum(dist_slack)
        slack_array = reshape(slack_array,buscount,1)
        S = S - gemm('N','N',gemm('N','N',S,slack_array),ones(1,buscount))

    elseif length(slack_position) == 0
        @warn "Slack bus not identified in the Bus/Nodes list, can't build PTDF"
        S = Array{Float64,2}(undef,linecount,buscount)
    end

    S_ax = AxisArrays.AxisArray(S, AxisArrays.Axis{:branches}(line_axis), AxisArrays.Axis{:buses}(bus_axis))
    A_ax = AxisArrays.AxisArray(A, AxisArrays.Axis{:buses}(bus_axis), AxisArrays.Axis{:lines}(line_axis))

    return  S_ax , A_ax

end


# Computes the fidelity with an EPR pair

export entFidelity, eprFidelity

""" `F = entFidelity(rho)`

Returns the fidelity with a maximally entangled state of equal dimension.

"""

function entFidelity(rho::AbstractMatrix)

	@assert isQuantumState(rho) "Input is not a valid quantum state."

	# Construct the maximally entangled state of equal local dimension
	(d1, d2) = size(rho);
	ent = maxEnt(convert(Int,sqrt(d1)));

	# Compute the fidelity with the pure state ent
	out = trace(ent * rho);

	return out;

end

""" `F = eprFidelity(rho)`

For locally two-dimensional states, returns the fidelity with the closest bell state.

"""

function eprFidelity(rho::AbstractMatrix)

	(d, db) = size(rho);
	@assert isQuantumState(rho) "Input is not a valid quantum state."
    	@assert d == 4 "The state is not locally two-dimensional"

    	M = toBellBasis(rho)

    	# Compute the fidelity with the closest bell state
    	out = max(diag(M)...);

    	return out;
end


struct InverseGamma <: Distribution{Float64} end

"""
    inv_gamma(shape::Real, scale::Real)

Sample a `Float64` from a inverse gamma distribution.
"""
const inv_gamma = InverseGamma()

function logpdf(::InverseGamma, x::Real, shape::Real, scale::Real)
    if x > 0.
        shape * log(scale) - (shape + 1) * log(x) - loggamma(shape) - (scale / x)
    else
        -Inf
    end
end

function logpdf_grad(::InverseGamma, x::Real, shape::Real, scale::Real)
    error("Not Implemented")
    (nothing, nothing, nothing)
end

function random(::InverseGamma, shape::Real, scale::Real)
    rand(Distributions.InverseGamma(shape, scale))
end

is_discrete(::InverseGamma) = false

(::InverseGamma)(shape, scale) = random(InverseGamma(), shape, scale)

has_output_grad(::InverseGamma) = false
has_argument_grads(::InverseGamma) = (false, false)

export inv_gamma


# Conways game of life
#
# includes an RLE decoder
# UnicodePlots is used to display the animation in the terminal

using UnicodePlots

function count_neighbors(m::Matrix{Bool}, i::CartesianIndex)
    c = 0
    for j in CartesianIndex.((-1:1)', -1:1)
        j == CartesianIndex(0,0) && continue

        if checkbounds(Bool, m, i+j)
            c += m[i+j]
        end
    end
    return c
end

function gameoflife_step(m::Matrix{Bool}; live, appear)
    nextm = similar(m)
    for i in CartesianIndices(m)
        if m[i]
            nextm[i] = count_neighbors(m, i) ∈ live
        else
            nextm[i] = count_neighbors(m, i) ∈ appear
        end
    end
    return nextm
end

function animate(m::Matrix{Bool}, dt, steps; live=2:3, appear=3)
    print(repr(spy(m; labels=false, title="", canvas=DotCanvas)))
    height = div(size(m, 1), 2, RoundUp) + 1
    t = time()
    for _ in 1:steps
        m = gameoflife_step(m; live=live, appear=appear)
        while time() - t < dt end
        t = time()
        print("\r\u1b[K\u1b[A"^height) # remove previous graph
        print(repr(spy(m; labels=false, title="", canvas=DotCanvas)))
    end
    println()
end

function RLE_decoder(RLE; x, y)
    m = zeros(Bool, y, x)
    i = CartesianIndex(1,1)
    n = 0
    for j in collect(RLE)
        nn = n == 0 ? 1 : n
        if isspace(j)
            continue
        elseif j == '!' # end
            break
        elseif j == 'b' # dead
            i += CartesianIndex(0, nn)
            n = 0
        elseif j == '$' # new line
            i = CartesianIndex(i[1]+nn, 1)
            n = 0
        elseif j ∈ '0':'9'
            n = 10*n + j - '0'
        else # alive
            for _ in 1:nn
                m[i] = true
                i += CartesianIndex(0, 1)
            end
            n = 0
        end
    end
    return m
end

m = RLE_decoder(raw"16bo30b$16bobo16bo11b$16b2o17bobo9b$obo10bo21b2o10b$b2o11b2o31b$bo11b
2o32b3$10b2o20b2o13b$11b2o19bobo9b3o$10bo21bo11bo2b$27bo17bob$27b2o18b$26bobo!",
x = 47,
y = 14)

# add margins
T = 0 # top
B = 10 # bottom
R = 0 # right
L = 0 # left

m = Bool[
    zeros(T, L)         zeros(T, size(m,2)) zeros(T, R)
    zeros(size(m,1), L) m                   zeros(size(m,1), R)
    zeros(B, L)         zeros(B, size(m,2)) zeros(B, R)
]

animate(m, 0.01, 200)


"""
    NelsonAalen

An immutable type containing cumulative hazard function estimates computed
using the Nelson-Aalen method.
The type has the following fields:

* `times`: Distinct event times
* `nevents`: Number of observed events at each time
* `ncensor`: Number of right censored events at each time
* `natrisk`: Size of the risk set at each time
* `chaz`: Estimate of the cumulative hazard at each time
* `stderr`: Standard error of the cumulative hazard
Use `fit(NelsonAalen, ...)` to compute the estimates and construct
this type.
"""
struct NelsonAalen{T<:Real} <: NonparametricEstimator
    times::Vector{T}
    nevents::Vector{Int}
    ncensor::Vector{Int}
    natrisk::Vector{Int}
    chaz::Vector{Float64}
    stderr::Vector{Float64}
end

estimator_start(::Type{NelsonAalen}) = 0.0  # Estimator starting point
stderr_start(::Type{NelsonAalen}) = 0.0 # StdErr starting point

estimator_update(::Type{NelsonAalen}, es, dᵢ, nᵢ) = es + dᵢ / nᵢ # Estimator update rule
stderr_update(::Type{NelsonAalen}, gw, dᵢ, nᵢ) = gw + dᵢ * (nᵢ - dᵢ) / (nᵢ^3) # StdErr update rule

"""
    confint(na::NelsonAalen, α=0.05)

Compute the pointwise confidence intervals for the cumulative hazard
function as a vector of tuples.
"""
function StatsBase.confint(na::NelsonAalen, α::Float64=0.05)
    q = quantile(Normal(), 1 - α/2)
    return map(na.chaz, na.stderr) do srv, se
        srv - q * se, srv + q * se
    end
end

"""
    fit(NelsonAalen, times, status) -> NelsonAalen

Given a vector of times to events and a corresponding vector of indicators that
dictate whether each time is an observed event or is right censored, compute the
Nelson-Aalen estimate of the cumulative hazard rate function.
"""
StatsBase.fit(::Type{NelsonAalen}, times, status)


# # FFTW plans

# Here we test planning the FFTW and run some simple benchmarks.

# Here are the packages we need.

using FFTW
using Plots
using LinearAlgebra: mul!
using Test
using Random
using BenchmarkTools

@info "Threads: $(FFTW.nthreads())"

# ## Testing setup

# ### The spatial domain and discretization

L = 2π
κ₀ = 2π/L
N = 144 # 2^4 * 3^2
x = y = (L/N):(L/N):L
nothing

# ### Vorticity field for the tests

# We randomly excite a certain number of modes.
rng = Xoshiro(123)
num_modes = 8
vort = sum(
    [
        2κ₀^2 * (kx^2 + ky^2) * (
            ar * cos.(κ₀ * (kx * one.(y) * x' + ky * y * one.(x)'))
            - ai * sin.(κ₀ * (kx * one.(y) * x' + ky * y * one.(x)'))
        )
        for (kx, ky, ar, ai) in zip(
            rand(rng, 1:div(N,4), num_modes),
            rand(rng, 1:div(N,4), num_modes),
            10*rand(rng, num_modes),
            10*rand(rng, num_modes)
        )
    ]
)

vort_hat = rfft(vort)
nothing

# Visualizing the vorticity field

heatmap(x, y, vort, xlabel="x", ylabel="y", title="Vorticity field", titlefont=12)

# ### Planning the FFTWs

#
plan_estimate = plan_rfft(vort); # default is FFTW.ESTIMATE
plan_inv_estimate = plan_irfft(vort_hat, N);
plan_measure = plan_rfft(vort, flags=FFTW.MEASURE);
plan_inv_measure = plan_irfft(vort_hat, N, flags=FFTW.MEASURE);
plan_patient = plan_rfft(vort, flags=FFTW.PATIENT);
plan_inv_patient = plan_irfft(vort_hat, N, flags=FFTW.PATIENT);
plan_exhaustive = plan_rfft(vort, flags=FFTW.EXHAUSTIVE);
plan_inv_exhaustive = plan_irfft(vort_hat, N, flags=FFTW.EXHAUSTIVE);

# ## Sanity tests of the different plans

@testset "Planned FFTW with ESTIMATE" begin
    w_hat = plan_estimate * vort
    w_hat_org = copy(w_hat)
    @test w_hat ≈ rfft(vort)
    @test plan_inv_estimate * w_hat ≈ vort
    @test plan_inv_estimate * w_hat ≈ irfft(rfft(vort), N)
    w_hat_mul = similar(w_hat)
    mul!(w_hat_mul, plan_estimate, vort)
    @test w_hat ≈ w_hat_mul
    vort_back = similar(vort)
    mul!(vort_back, plan_inv_estimate, w_hat) # careful, inverse with mul! may mutate w_hat as well
    @test vort_back ≈ vort
end
nothing

#

@testset "Planned FFTW with MEASURE" begin
    w_hat = plan_measure * vort
    @test w_hat ≈ rfft(vort)
    @test plan_inv_measure * w_hat ≈ vort
    @test plan_inv_measure * w_hat ≈ irfft(rfft(vort), N)
    w_hat_back = similar(w_hat)
    mul!(w_hat_back, plan_measure, vort)
    @test w_hat ≈ w_hat_back
    vort_back = similar(vort)
    mul!(vort_back, plan_inv_measure, w_hat)  # careful, inverse with mul! may mutate w_hat as well
    @test vort_back ≈ vort
end
nothing

#

@testset "Planned FFTW with PATIENT" begin
    w_hat = plan_patient * vort
    @test w_hat ≈ rfft(vort)
    @test plan_inv_patient * w_hat ≈ vort
    @test plan_inv_patient * w_hat ≈ irfft(rfft(vort), N)
    w_hat_back = similar(w_hat)
    mul!(w_hat_back, plan_patient, vort)
    @test w_hat ≈ w_hat_back
    vort_back = similar(vort)
    mul!(vort_back, plan_inv_patient, w_hat)  # careful, inverse with mul! may mutate w_hat as well
    @test vort_back ≈ vort
end
nothing

#

@testset "Planned FFTW with EXHAUSTIVE" begin
    w_hat = plan_exhaustive * vort
    @test w_hat ≈ rfft(vort)
    @test plan_inv_exhaustive * w_hat ≈ vort
    @test plan_inv_exhaustive * w_hat ≈ irfft(rfft(vort), N)
    w_hat_back = similar(w_hat)
    mul!(w_hat_back, plan_exhaustive, vort)
    @test w_hat ≈ w_hat_back
    vort_back = similar(vort)
    mul!(vort_back, plan_inv_exhaustive, w_hat) # careful, inverse with mul! may mutate w_hat as well
    @test vort_back ≈ vort
end
nothing

# ## Timings and allocations

# Let us look at it with `@btime`. **Notice that `mul!` does not allocate and is slightly faster.**

@info "FFTW no plan"
@btime rfft(w) setup = (w = copy(vort));

@info "FFTW plan with ESTIMATE"
@btime p * w setup = (p = $plan_estimate; w = copy($vort));
@info "FFTW plan with ESTIMATE and mul!"
@btime mul!(w_hat, p, w) setup = (
    w_hat = copy($vort_hat);
    p = $plan_estimate;
    w = copy($vort)
);

@info "FFTW plan with MEASURE"
@btime p * w setup = (p = $plan_measure; w = copy($vort));
@info "FFTW plan with MEASURE and mul!"
@btime mul!(w_hat, p, w) setup = (
    w_hat = copy($vort_hat);
    p = $plan_measure;
    w = copy($vort)
);

@info "FFTW plan with PATIENT"
@btime p * w setup = (p = $plan_patient; w = copy($vort));
@info "FFTW plan with PATIENT and mul!"
@btime mul!(w_hat, p, w) setup = (
    w_hat = copy($vort_hat);
    p = $plan_patient;
    w = copy($vort)
);

@info "FFTW plan with EXHAUSTIVE"
@btime p * w setup = (p = $plan_exhaustive; w = copy($vort));
@info "FFTW plan with EXHAUSTIVE and mul!"
@btime mul!(w_hat, p, w) setup = (
    w_hat = copy($vort_hat);
    p = $plan_exhaustive;
    w = copy($vort)
);

# ## Direct and inverse transforms

# Here we compare the timings between direct and inverse transforms. We restrict this to using `mul!`, since this is what we will use at the end. **Notice the direct transform is slightly faster than the inverse transform.**

# Only one note of **warning:** when using `mul!` with an inverse transform plan, the last argument may mutate. Type `@edit mul!(vort, plan_inv_estimate.p, vort_hat)` to see where this happens.

@info "Direct and inverse FFTW plan with ESTIMATE and mul!"
@btime mul!(w_hat, p, w) setup = (
    w_hat = similar($vort_hat);
    p = $plan_estimate;
    w = copy($vort)
);
@btime mul!(w, p, w_hat) setup = (
    w = similar($vort);
    p = $plan_inv_estimate;
    w_hat = copy($vort_hat)
);

@info "Direct and inverse FFTW plan with MEASURE and mul!"
@btime mul!(w_hat, p, w) setup = (
    w_hat = similar($vort_hat);
    p = $plan_measure;
    w = copy($vort)
);
@btime mul!(w, p, w_hat) setup = (
    w = similar($vort);
    p = $plan_inv_measure;
    w_hat = copy($vort_hat)
);

@info "Direct and inverse FFTW plan with PATIENT and mul!"
@btime mul!(w_hat, p, w) setup = (
    w_hat = similar($vort_hat);
    p = $plan_patient;
    w = copy($vort)
);
@btime mul!(w, p, w_hat) setup = (
    w = similar($vort);
    p = $plan_inv_patient;
    w_hat = copy($vort_hat)
);

@info "Direct and inverse FFTW plan with EXHAUSTIVE and mul!"
@btime mul!(w_hat, p, w) setup = (
    w_hat = similar($vort_hat);
    p = $plan_exhaustive;
    w = copy($vort)
);
@btime mul!(w, p, w_hat) setup = (
    w = similar($vort);
    p = $plan_inv_exhaustive;
    w_hat = copy($vort_hat)
);


### Hastings & Powell 1991
## Examining behavior when varying b2 value
using LaTeXStrings
### Figure S1 (Bifurcation diagram)

## Wrap in function
# b1val : value of b1
# b2low : lower limit of b2 values to examine
# b2up : upper limit of b2 values to examine
# out : desired output plot, either "maxloc" for local maxima only (default) or "all" for all values
function b2_behavior(;b1val::Float64, b2low::Float64=1.5, b2up::Float64=3.2,out::String="maxloc")
  ## Define initial parameters
  p = a1, a2, b1, b2, d1, d2 = [5.0, 0.1, b1val, b2low, 0.4, 0.01]
  u0 = [1.0, 1.0, 1.0]
  tspan = (0.0, 10000.0)
  # b1 values interval
  bs = collect(b2low:0.01:b2up)
  # number of values to collect
  N = 1000

  ## Loop to select all values of z for all b1 values
  Z = zeros(Float64, (N, length(bs)))
  Zmax = zeros(Float64, length(bs))
  Zmin = zeros(Float64, length(bs))
  errors = zeros(Float64, length(bs))
  for j in 1:length(bs)
    # Change b1 value
    p[4] = b2 = bs[j]
    # Solve for given b1 value
    prob = ODEProblem(parameterized_model!, u0, tspan, p)
    sol = solve(prob)
    # Save values
    if length(sol[3,:]) > N
      Z[:,j] = sol[3, end-(N-1):end]
      Zmax[j] = maximum(Z[:,j])
      Zmin[j] = minimum(Z[:,j])
    else
      # Safeguard for when true solution is unstable
      errors[j] = j
    end
  end
  Z
  Zmax
  Zmin
  errors[errors .> 0]

  ## Select only local maxima
  Zmaxloc = zeros(Float64, size(Z))
  for i in 2:(N-1), j in 1:length(bs)
    # Select maxima values(greater than neighbors)
    if Z[i, j] > Z[i-1, j] && Z[i, j] > Z[i+1, j]
      # Select first local maxima only with arbitrary interval
      Zrange = Zmax[j] - Zmin[j]
      if Z[i, j] > (Zmin[j] + 0.66*(Zrange))
         Zmaxloc[i, j] = Z[i, j]
      end
    end
  end
  Zmaxloc

  ## Visualize results
  # Vector of b1 values repeated N times
  B = repeat(bs, inner = N)
  # Match local maxima with respective b1 value
  pts_all = unique(hcat(B, vec(Z)); dims = 1)
  pts_maxloc = unique(hcat(B, vec(Zmaxloc)); dims = 1)
  # Remove null values
  pts_all = pts_all[pts_all[:, 2] .> 0.00, :]
  pts_maxloc = pts_maxloc[pts_maxloc[:,2] .> 0.00, :]
  # Select desired output (out parameter value)
  if out == "maxloc" # default
    results = pts_maxloc
  end
  if out == "all" # default
    results = pts_all
  end
  # Plot results (bifurcation diagram)
  scatter(results[:,1], results[:,2],
          markersize = 1, c = :black,
          xlabel = L"b_2", xlim = (b2low,b2up), xticks = b2low:0.25:b2up,
          ylabel = L"z_{max}", yticks = (0.0:2.0:maximum(results[:, 2]), string.(0.0:2.0:maximum(results[:, 2]))),
          legend = false, grid = :none, framestyle = :box,
          guidefontsize = 10)
end

# Examine behavior when changing b2 at b1 = 3.0
figS1_maxloc = b2_behavior(b1val = 3.0, b2low = 1.5, b2up = 3.2,
                           out = "maxloc") # default parameters
figS1_all = b2_behavior(b1val = 3.0, out = "all")

## Export figures
savefig(figS1_maxloc, joinpath("..", "article", "figures", "figS1.pdf"))

# Additionnal examinations
b2_behavior(b1val = 2.0)
b2_behavior(b1val = 4.0)
b2_behavior(b1val = 5.0)
b2_behavior(b1val = 6.0)
b2_behavior(b1val = 1.5)


let
    v = collect(1:10)
    t = 5.5
    idx,Δt = linear_interp(v,t)
    @test idx == 5

end
let
    v = collect(1:10)
    t1 = 2.5
    t2 = 5.5
    idx1,Δt1 = linear_interp(v,t1)
    idx2,Δt2 = linear_interp(v,t2)
    vp = prune_sorted_array(v,idx1,idx2,Δt1,Δt2)
    @test length(vp) == 5
end


export meanerror, classerror

"""
`meanerror(net::FFNNet,inputs::Vector{Vector{Float64}},
   outputs::Vector{Vector{Float64}}; cost::Function = quaderror)`

Mean error of the network in this sample (consisting of `inputs` and `outputs`). The error is measured using `cost` function.

### Arguments
* `net` (`FFNNet`): Feedforward Neural Network
* `inputs` (`Vector{Vector{Float64}}`): Vector containing input examples (each one is a vector). This have K elements, K being the number of examples in this dataset. Each input vector must have I elements, where I is the input size of `net`.
* `outputs` (`Vector{Vector{Float64}}`): Vector containing input examples (each one is a vector). This must also have K elements, K being the number of examples in this dataset. Each output vector must have O elements, where O is the size of the output layer of `net`.

### Keyword Arguments
* `cost` (`Function`, `quaderror` by default): Cost function to be used as error measure.
"""
function meanerror(net::FFNNet,
                   inputs::Vector{Vector{Float64}},
                   outputs::Vector{Vector{Float64}};
                   cost::Function = quaderror)

    total_error = 0.0
    for ex in eachindex(inputs)
        total_error += cost(propagate(net, inputs[ex]), outputs[ex])
    end
    return total_error/length(inputs)
end

"""
`classerror(net::FFNNet,
                 inputs::Vector{Vector{Float64}},
                 outputs::Vector{Vector{Float64}})`

Classification error of the network in this sample (consisting of `inputs` and
`outputs`). The error is measured counting the ammount of misclassified inputs.

### Arguments
* `net` (`FFNNet`): Feedforward Neural Network
* `inputs` (`Vector{Vector{Float64}}`): Vector containing input examples (each one is a vector). This have K elements, K being the number of examples in this dataset. Each input vector must have I elements, where I is the input size of `net`.
* `outputs` (`Vector{Vector{Float64}}`): Vector containing input examples (each one is a vector). This must also have K elements, K being the number of examples in this dataset. Each output vector must have O elements, where O is the size of the output layer of `net`.

### Details
This function assumes that a classification of a vector is the index of the maximum value inside that vector. For example, the vector `[0,1,0]` is classified as 2. This function takes the classification of the output of the network, and compares to the classification of the example's output, counting the number of mismatches.

Therefore, it's natural to use `softmax` activation on the last layer of `net` and *one-hot* encoding on the examples' outputs, but it's not mandatory.
"""
function classerror(net::FFNNet,
                    inputs::Vector{Vector{Float64}},
                    outputs::Vector{Vector{Float64}})

    return sum([indmax(outputs[i]) != indmax(propagate(net, inputs[i])) for i in 1:length(inputs)])
end


# -*- coding: utf-8 -*-
# ---
# jupyter:
#   jupytext:
#     formats: ipynb,jl:light
#     text_representation:
#       extension: .jl
#       format_name: light
#       format_version: '1.4'
#       jupytext_version: 1.2.4
#   kernelspec:
#     display_name: Julia 1.3.1
#     language: julia
#     name: julia-1.3
# ---

# # Particle Cloud Simulation Example
#
#

#
#
#

#
# Here we simulate the trajectory of a particle cloud in a randomly generated flow field which is a crude representation for an ocean eddy field.
#
# As an exercise left to the user, directions are provided e.g. to modify the size of the domain or the initial position of the particle cloud.
#
# The demonstrated methods that are generally applicable for research purposes (e.g. with flow field derived from models or data). The notebook also demoes simple methods to create plots and movies.
#
# - 1. setup the software and initialize example
# - 2. simulate trajectories & plot results
# - 3. exercises (user)

# + {"slideshow": {"slide_type": "subslide"}, "cell_type": "markdown"}
# ### For More Documentation
#
# On Julia :
# - <https://julialang.org>
# - <https://docs.julialang.org>
#
# On this notebook :
# - <https://docs.juliadiffeq.org/latest>
# - <https://en.wikipedia.org/wiki/Displacement_(vector)>
# - <https://juliaclimate.github.io/IndividualDisplacements.jl/dev>
# - <https://juliaclimate.github.io/MeshArrays.jl/dev>

# + {"slideshow": {"slide_type": "slide"}, "cell_type": "markdown"}
# ## 1.1 Import Software
# -

using OrdinaryDiffEq, Plots, ColorSchemes
using IndividualDisplacements, MeshArrays
using Statistics, Random, DataFrames
p=dirname(pathof(MeshArrays)); include(joinpath(p,"../examples/Demos.jl"))
include("helper_functions.jl")

# + {"slideshow": {"slide_type": "subslide"}, "cell_type": "markdown"}
# ## 1.2 Setup Problem
# -

# Put grid variables in a dictionary.

np=24
Γ=SetupPeriodicDomain(np);

# + {"slideshow": {"slide_type": "subslide"}, "cell_type": "markdown"}
# Derive flow field (`𝑃["u0"]` etc) from randomly generated ϕ streamfunction
# -

𝑃,ϕ=SetupRandomFlow(Γ);
#𝑃["t1"]=100.0

# + {"slideshow": {"slide_type": "slide"}, "cell_type": "markdown"}
# ## 1.3 Initial Positions
#
# Set the initial positions to a region of the gridded domain. These will provide the initial condition `u0` for the differential equation solver.
#
# _Skipped at first in presentation mode_

# + {"slideshow": {"slide_type": "skip"}}
x0=np*(0.25:0.02:0.75)
y0=np*(0.25:0.02:0.75)
#x0=np*(0.2:0.005:0.25)
#y0=np*(0.6:0.005:0.65)

#reformat data into u0 initial condition
x0=vec(x0)*ones(1,length(y0))
y0=ones(size(x0,1),1)*transpose(vec(y0))
u0=transpose([x0[:] y0[:] ones(size(x0[:]))]);

# + {"slideshow": {"slide_type": "subslide"}}
x=vec(Γ["XC"][1][:,1])
y=vec(Γ["YC"][1][1,:])
χ=transpose(ϕ[1])

plt=contourf(x,y,χ,linewidth = 0.1,clims=(-0.4,0.4),
             title="Flow Stream (color) & Initial Positions (dots)")
scatter!(plt,u0[1,:],u0[2,:],c=:white,markersize=3.0,leg=false)

#pth=tempdir()*"/"
#savefig(plt,pth*"RandomFlow.png")

# + {"slideshow": {"slide_type": "slide"}, "cell_type": "markdown"}
# ## 2.1 Compute Trajectories
#
# As done in the `SolidBodyRotation` example earlier, we simply use the `OrdinaryDiffEq` package to solve for the particle trajectories.
# -

𝑇 = (𝑃["t0"],𝑃["t1"])
prob = ODEProblem(⬡!,u0,𝑇,𝑃)
sol = solve(prob,Tsit5(),reltol=1e-5,abstol=1e-5)
size(sol)

# + {"slideshow": {"slide_type": "subslide"}, "cell_type": "markdown"}
# ## 2.2 Post-Process Output
#
# Reformat the `sol` output from the `solve` function into a `DataFrame` for practical use, plotting, etc.
#
# _Code cell is skipped at first in presentation mode_

# + {"slideshow": {"slide_type": "skip"}}
#x,y axes etc
x=sol[1,:,:]
y=sol[2,:,:]
fIndex=sol[3,:,:]
ID=collect(1:size(sol,2))*ones(1,size(sol,3))

#put together as a DataFrame
df = DataFrame(ID=Int.(ID[:]), x=mod.(x[:],Ref(np)), y=mod.(y[:],Ref(np)), fIndex=fIndex[:]);

# + {"slideshow": {"slide_type": "skip"}}
#add a time axis
nf=size(u0,2)
nt=size(df,1)/nf
t=[ceil(i/nf)-1 for i in 1:nt*nf]

df[!,:t]=(𝑃["t1"]-𝑃["t0"])/t[end].*t;

#df

# + {"slideshow": {"slide_type": "slide"}, "cell_type": "markdown"}
# ## 2.3 Plot Results
#
# - define `scatter_subset` convenience function
# - generate animation using `scatter_subset`

# + {"slideshow": {"slide_type": "subslide"}}
function scatter_subset(df,t)
    dt=5.0
    df_t = df[ (df.t.>t-dt).&(df.t.<=t) , :]

    x=vec(Γ["XC"][1][:,1])
    y=vec(Γ["YC"][1][1,:])
    χ=transpose(ϕ[1])
    contourf(x,y,χ,c = :blues,linewidth = 0.1,clims=(-0.4,0.4),colorbar=false)

    scatter!(df_t.x,df_t.y,markersize=2.0,c=:red,
    xlims=(0,np),ylims=(0,np),leg=:none,marker = (:circle, stroke(0)))
end
# + {"slideshow": {"slide_type": "subslide"}}
#@gif for t in 0:2.0:maximum(df[!,:t])
#   scatter_subset(df,t)
#end

anim = @animate for t in 0:2.0:maximum(df[!,:t])
   scatter_subset(df,t)
end
pth=tempdir()*"/"
gif(anim, pth*"RandomFlow.gif", fps = 15)
# -

# ## 3. Exercises
#
#
# ### Try uncomment some of these lines:
#
# - `#df` (see the [package docs](https://juliadata.github.io/DataFrames.jl/stable/))
# - the three lines below at once, or one at a time
#     - `#x0=np*(0.2:0.005:0.25)`
#     - `#y0=np*(0.6:0.005:0.65)`
#     - `𝑃["t1"]=100`
#
# _Tip: select `run all below` from the `Cell` drop down menu to avoid replacing the random flow field every time._
#
# ### Try the following exercise (after uncommenting the above lines):
#
# Can you find a choice of `x0` & `y0` that reduces convergence in one direction? Or increases convergence in one direction?
#
# - more `convergence` ~ less relative `separation` ~ reduced xx,yy below
# - less `convergence` ~ more relative `separation` ~ increased xx,yy below
#
# _See code & plot below_

# + {"slideshow": {"slide_type": "skip"}}
#Same DataFrame as eefore but with original x,y
df2=deepcopy(df)
df2.x[:]=sol[1,:,:]
df2.y[:]=sol[2,:,:];

# +
#Compute relative separation estimates
tt=0:2.0:maximum(df[!,:t])
xx=zeros(length(tt)); yy=similar(xx)
dt=5.0
for t in 1:length(tt)
    df_t = df2[ (df2.t.>tt[t]-dt).&(df2.t.<=tt[t]) , :]
    xx[t]=std(df_t.x)
    yy[t]=std(df_t.y)
end

#Plot relative separation estimates
plot(tt,xx,label="std(x)",title="relative separation estimates")
plot!(tt,yy,label="std(y)")
# -


export logsumexp3!, logsumexp_and_softmax3!, softmax3!, logsumexp_and_softmax!, logsumexp_and_softmax

add_1_dim(x::AbstractArray) = reshape(x, size(x)..., 1)

"""
    logsumexp_and_softmax!(r, x)

Set `r` = softmax(x) and return `logsumexp(x)`.
"""
function logsumexp_and_softmax!(r::AbstractArray{T}, x::AbstractArray{T}) where {T}
    n = length(x)
    length(r) == n || throw(DimensionMismatch("Inconsistent array lengths."))
    isempty(x) && return -T(Inf)

    u = maximum(x)                                       # max value used to re-center
    abs(u) == Inf && return any(isnan, x) ? T(NaN) : u   # check for non-finite values

    s = zero(T)
    @inbounds @simd for i in 1:n
        s += ( r[i] = exp(x[i] - u) )
    end
    invs = one(T)/s
    r .*= invs
    return log(s) + u
end

logsumexp_and_softmax!(x::AbstractArray) = logsumexp_and_softmax!(x, x)

function logsumexp3!(lse::AbstractMatrix, tmp::AbstractMatrix, x::AbstractArray3)
    nz, nψ, nd = size(x)
    (nz, nψ) ==  size(lse) == size(tmp) || throw(DimensionMismatch())

    maximum!(add_1_dim(tmp), x)
    fill!(lse, zero(eltype(lse)))
    @inbounds for k in 1:nd
        @views lse .+= exp.(x[:,:,k] .- tmp)
    end
    lse .= log.(lse) .+ tmp
end

function softmax3!(q::AbstractArray3, lse::AbstractMatrix, tmp::AbstractMatrix, x::AbstractArray3)
    nz, nψ, nd = size(x)
    (nz, nψ, nd) == size(q) || throw(DimensionMismatch())
    (nz, nψ) ==  size(lse) == size(tmp) || throw(DimensionMismatch())

    maximum!(add_1_dim(tmp), x)
    fill!(lse, zero(eltype(lse)))
    @inbounds for k in 1:size(x,3)
        @views lse .+= (q[:,:,k] .= exp.(x[:,:,k] .- tmp))
    end
    q ./= lse
end

# update q as Pr(x[:,:,1])
function softmax3!(q::AbstractMatrix, lse::AbstractMatrix, tmp::AbstractMatrix, x::AbstractArray3)
    nz, nψ, nd = size(x)
    (nz, nψ) ==  size(lse) == size(tmp) == size(q) || throw(DimensionMismatch())

    maximum!(add_1_dim(tmp), x)
    @views lse .= (q .= exp.(x[:,:,1] .- tmp))
    @inbounds for k in 2:nd
        @views lse .+= exp.(x[:,:,k] .- tmp)
    end
    q ./= lse
end

# updates updates x to Pr(x)
softmax3!(x::AbstractArray3, lse::AbstractMatrix, tmp::AbstractMatrix) = softmax3!(x, lse, tmp, x)

# does not change x
function logsumexp_and_softmax3!(lse::AbstractMatrix, q::AbstractArray, tmp::AbstractMatrix, x::AbstractArray3)
    softmax3!(q, lse, tmp, x)
    lse .= log.(lse) .+ tmp
end

# updates x to Pr(x)
function logsumexp_and_softmax3!(lse::AbstractMatrix, x::AbstractArray3, tmp::AbstractMatrix)
    logsumexp_and_softmax3!(lse, x, tmp, x)
end


# QuObj Tests
using Schrodinger
using Compat, Compat.Test, Compat.LinearAlgebra, Compat.SparseArrays
println("Testing Math...")

@testset "QuObj Basic Math" begin
# Build a few different variable for testing
g = basis(2,0)
e1 = dense(basis(2,1))
ρ = maxmixed(4)
α = rand(ComplexF64)
ψα = coherent(15,α)
β = rand(ComplexF64)
ψβ = coherent(15,β)
σ = dense(create(4))
a4 = destroy(4)
a2 = destroy(2)
adag2 = create(2)
plus = normalize!(g+1.0im*basis(2,1))

@testset "QuObj/Number Algebra" begin
    # Test algebra between QuObjects and numbers
    @test +g == g
    @test -g == 0-g
    @test +e1 == e1
    @test -e1 == 0-e1
    @test +(g') == g'
    @test -(g') == 0-g'
    @test +(e1') == e1'
    @test -(e1') == 0-e1'
    @test +ρ == ρ
    @test -ρ == 0-ρ
    @test +σ == σ
    @test -σ == 0-σ
    @test g+1 == 1+g == Ket([2,1])
    @test e1-1 == Ket([-1,0])
    @test 1-e1 == Ket([1,0])
    @test g'+1 == 1+g' == Bra([2,1])
    @test e1'-1 == Bra([-1,0])
    @test 1-e1' == Bra([1,0])
    @test ρ+1 == 1+ρ == ρ+qeye(4)
    @test ρ-0.25 == 0.25-ρ == qzero(4)
    @test σ+2 == 2+σ == σ+2*qeye(4)
    @test σ-2 == -(2-σ) == σ-2*qeye(4)
    @test 2*g == g*2 == Ket(sparse([2,0]))
    @test 2*e1 == e1*2 == Ket([0,2])
    @test 2*(g') == (g')*2 == Bra(sparse([2,0]))
    @test 2*(e1') == (e1')*2 == Bra([0,2])
    @test 4*ρ == ρ*4 == qeye(4)
    @test 2σ == σ*2 == σ+σ
    @test g/2 == Ket(sparse([0.5,0]))
    @test e1/1 == e1
    @test (g')/2 == Bra(sparse([0.5,0]))
    @test (e1')/1 == e1'
    @test_throws ArgumentError 2/g
    @test_throws ArgumentError 2/e1
    @test_throws ArgumentError 2/(g')
    @test_throws ArgumentError 2/(e1')
    if VERSION > v"0.7.0-"
        @test (x=(g/0);isnan(x[2])&&isinf(x[1])) # see julia PR #22715
    end
    @test (x=(e1/0);isnan(x[1])&&isinf(x[2]))
    @test ρ/2 == qeye(4)/8
    @test (x=ρ/0;isnan(x[2,1])&&isinf(x[1,1]))
    @test (x=σ/0;isnan(x[1,1])&&isinf(x[2,1]))
    @test_throws ArgumentError 3/ρ
    @test_throws ArgumentError 3/σ
    @test_throws ArgumentError plus^3
    @test_throws ArgumentError plus^3.3
    @test ρ^3 == 0.015625*qeye(4)
    @test σx^2 == σy^2 == σz^2 == σ0
    if VERSION < v"0.7.0-"
        @test_throws MethodError data(ρ^2.5)
    else
        @test data(ρ^2.5) == data(ρ)^2.5
    end
    @test data(dense(ρ)^2.5) == full(ρ)^2.5
    @test (σ^2)[4,2] == √(2)*√(3)
    @test_broken data(σ^2.5) == data(σ)^2.5
end

@testset "QuObj/QuObj Algebra" begin
    @test g+g == 2g
    @test g-g == Ket(sparse([0,0]))
    @test e1+e1+e1 == 3*e1
    @test g+e1 == Ket([1,1])
    @test g-e1 == Ket([1,-1])
    @test g'+g' == 2g'
    @test e1'+e1'+e1' == 3*e1'
    @test g'+e1' == Bra([1,1])
    @test g'-e1' == Bra([1,-1])
    @test ρ+ρ == 2ρ
    @test σ+σ+σ == 3σ
    @test σ-σ == qzero(4)
    @test ρ-ρ == qzero(4)
    @test data(ρ+σ) == data(ρ)+data(σ)
    @test data(ρ-σ) == data(ρ)-data(σ)
    @test_throws ArgumentError g+σ
    @test_throws ArgumentError g-σ
    @test ρ*ρ == ρ^2
    @test σ*σ == σ^2
    @test σ*a4 ≈ numberop(4)
    @test a4'*a4 ≈ numberop(4)
    @test σ*σ' ≈ numberop(4)
    @test a4'*σ' ≈ numberop(4)
    @test adag2*g == adag2*Bra(g)'== e1
    @test adag2'*e1 == adag2'*Bra(e1)' == g
    @test g'*a2 == Bra(g)*a2 == e1'
    @test g'*adag2' == Bra(g)*adag2' == e1'
    @test g'*g == Bra(e1)*e1 == 1
    @test dot(plus,plus) ≈ 1
    @test dot(ψα,ψβ) ≈ exp(-(abs2(α)+abs2(β))/2+α'*β)
    @test plus*plus' == plus*Bra(plus) ≈ normalize!(σ0 + σy)
    @test_throws ArgumentError g*e1
    @test_throws ArgumentError g/e1
end

@testset "Operator Inner Product" begin
    @test inner(σx,σx) == 2
    @test inner(σx,σy) == 0
    @test inner(σx,σz) == 0
    @test inner(ρ*projectorop(size(ρ,1),1:2),ρ) == 2/size(ρ,1)^2
    @test inner(Operator(ψα),Operator(ψβ)) ≈ exp(-abs2(α-β))
    @test_throws DimensionMismatch inner(Operator(ψα),ρ)
end

@testset "Tensor Product" begin
    @test g ⊗ g   == Ket(Bra(g)  ⊗ Bra(g)) == Ket(sparse([1,0,0,0]),(2,2))
    @test g ⊗ e1  == Ket(Bra(g)  ⊗ Bra(e1)) == Ket(sparse([0,1,0,0]),(2,2))
    @test e1 ⊗ g  == Ket(Bra(e1) ⊗ Bra(g)) == Ket(sparse([0,0,1,0]),(2,2))
    @test e1 ⊗ e1 == Ket(Bra(e1) ⊗ Bra(e1)) == Ket([0,0,0,1],(2,2))
    @test data(Operator(g) ⊗ σ) == [data(σ) zeros(4,4); zeros(4,8)]
    @test data(ρ ⊗ Operator(g)) == (A=Diagonal(fill(0.25,8));A.diag[2:2:8].=0;A)
    @test_throws ArgumentError e1 ⊗ ρ
    @test_throws ArgumentError Bra(e1) ⊗ σ
end

@testset "Transposition" begin
    @test g' == Bra([1,0])
    @test (g+1im*e1)' == Bra([1,-1im])
    @test conj((g+1im*e1)) == Ket([1,-1im])
    @test Bra(g)' == Ket([1,0])
    @test Bra([1,1im])' == Ket([1,-1im])
    @test conj(Bra([1,1im])) == Bra([1,-1im])
    @test ρ' == transpose(ρ) == ρ
    @test σ' == transpose(σ)
end

@testset "Math Functions" begin
    for f in [sqrt,log,exp], A in [Operator(coherent(4,1.2+3im)),ρ,create(4)+destroy(4)]
        @test data(f(A)) == f(full(A))
    end
end

@testset "Misc Functions" begin
    obj = [g,e1,plus,Bra(plus),σ,ρ,Operator(coherent(4,1.2+3im))]
    for A in obj
        for f in [real,imag,abs,abs2]
            @test f(A) == f.(data(A))
        end
        @static if VERSION > v"0.7.0-"
            @test data(round(A,digits=4)) == round.(data(A),digits=4)
        else
            @test data(round(A,4)) == round.(data(A),4)
        end
    end
end

@testset "Eigen" begin
    E = 3.3; Δ = 0.25
    O = dense(E*σz+Δ*σx)
    @test eigvals(O) == [-√(E^2+Δ^2), √(E^2+Δ^2)]
    @test eigen(O)[2][1] ≈ normalize!(Ket([-(E-√(E^2+Δ^2))/Δ,-1]))
    a = destroy(20) ⊗ qeye(2)
    s = qeye(20) ⊗ σ₋
    fc = 5; fa = 6; g = 0.025;
    H = fc*(a'a+0.5) + fa*(s's-0.5) + g*(a's + a*s')
    function En(l)
        n,q,Δ = (l-1)÷2, 1-2*(l%2), fa-fc
        return l==0 ? -Δ/2 : (n+1)*fc + 0.5q*√(4g^2*(n+1)+Δ^2)
    end
    θ(n) = 0.5*atan(2g*√(n+1)/(fa-fc))
    D,V = eigs(H,nev=10)
    @test D ≈ En.(0:9)
    @test abs(inner(V[6], cos(θ(2))*ket((3,0),(20,2)) - sin(θ(2))*ket((2,1),(20,2)))) ≈ 1
end

end