@testset "LSA" begin
    # Documents
    doc1 = StringDocument("This is a text about an apple. There are many texts about apples.")
    doc2 = StringDocument("Pears and apples are good but not exotic. An apple a day keeps the doctor away.")
    doc3 = StringDocument("Fruits are good for you.")
    doc4 = StringDocument("This phrase has nothing to do with the others...")
    doc5 = StringDocument("Simple text, little info inside")
    # Corpus
    crps = Corpus(AbstractDocument[doc1, doc2, doc3, doc4, doc5])
    prepare!(crps, strip_punctuation)
    ngram_complexity = 2
    lex = create_lexicon(crps, ngram_complexity)
    lex_keys = collect(keys(lex))
    n = length(crps)
    # Retrieval
    query = StringDocument("Apples and an exotic fruit.")
    for k in [1, 6]
        for stats in [:count, :tf, :tfidf, :bm25]
            for T in [Float32, Float64]
                dtm = DocumentTermMatrix{T}(crps, lex_keys, ngram_complexity=ngram_complexity)
                model = lsa(dtm, k=k, stats=stats, ngram_complexity=ngram_complexity)
                @test model isa LSAModel{String, T, Matrix{T}, Int}
                @test length(model.Σinv) == min(k, n)
                idxs, corrs = cosine(model, dtm, query)
                @test length(idxs) == length(corrs) == length(crps)
                sim = similarity(model, crps[rand(1:n)], query)
                @test -1.0 <= sim <= 1.0
            end
        end
    end

    # Tests for the rest of the functions
    K = 2
    T = Float32
    # Vocabulary
    model = lsa(crps, T, k=K, ngram_complexity=ngram_complexity)
    @test model isa LSAModel{String, T, Matrix{T}, Int}
    @test all(in_vocabulary(model, word) for word in lex_keys)
    @test vocabulary(model) == lex_keys
    @test size(model) == (length(lex_keys), K)
    # Document, corpus embedding
    dtm = DocumentTermMatrix{T}(crps, lex_keys, ngram_complexity=ngram_complexity)
    V = Matrix(embed_document(model, crps))
    @test eltype(V) == T
    @test eltype(embed_document(model, crps[1])) == T
    for i in 1:n
        @test all(V[:,i] .≈ embed_document(model, crps[i]))
    end
    # Index, Similarity
    idx = 2
    word = model.vocab[idx]
    @test index(model, word) == model.vocab_hash[word]
    @test get_vector(model, word) == model.Uᵀ[:, idx]
    @test similarity(model, crps[1], crps[2]) isa T
    @test similarity(model, crps[1], crps[2]) == similarity(model, crps[2], crps[1])
    @test_throws ErrorException LSAModel(DocumentTermMatrix{Int}(crps), k=K)
    @test_throws ErrorException StringAnalysis.embed_word(model, "word")
    # Test saving and loading an LSA model
    T = Float32
    vocab = split("a random string")
    vocab_hash = OrderedDict("a"=>1, "random"=>2, "string"=>3)
    k=2
    stats = :tf
    κ = 2
    β = 0.71
    tol = 1e-5
    model = lsa(crps, T, k=k, stats=stats, tol=T(tol), κ=κ, β=β)
    file = "./_lsa_model.txt"
    save_lsa_model(model, file)
    # Model 1
    loaded_model_1 = load_lsa_model(file, Float64, sparse=true)
    @test loaded_model_1 isa LSAModel{String, Float64, SparseMatrixCSC{Float64, Int}, Int}
    @test all(loaded_model_1.Uᵀ .≈ sparse(model.Uᵀ))
    @test all(loaded_model_1.Σinv .≈ sparse(model.Σinv))
    @test loaded_model_1.vocab == model.vocab
    @test loaded_model_1.vocab_hash == model.vocab_hash
    @test loaded_model_1.stats == model.stats
    @test all(loaded_model_1.idf .≈ model.idf)
    @test loaded_model_1.nwords ≈ model.nwords
    @test loaded_model_1.κ == model.κ
    @test loaded_model_1.β == model.β
    @test loaded_model_1.tol ≈ model.tol
    # Model 2
    loaded_model_2 = load_lsa_model(file, Float32, sparse=false)
    @test loaded_model_2 isa LSAModel{String, Float32, Matrix{Float32}, Int}
    @test all(loaded_model_2.Uᵀ .≈ model.Uᵀ)
    @test all(loaded_model_2.Σinv .≈ model.Σinv)
    @test loaded_model_2.vocab == model.vocab
    @test loaded_model_2.vocab_hash == model.vocab_hash
    @test loaded_model_2.stats == model.stats
    @test all(loaded_model_2.idf .≈ model.idf)
    @test loaded_model_2.nwords ≈ model.nwords
    @test loaded_model_2.κ == model.κ
    @test loaded_model_2.β == model.β
    @test loaded_model_2.tol ≈ model.tol
    rm(file)
end


# =========================================================================== #
# Version mise à jour du DM1 afin de pouvoir faire le DM2
#
# =========================================================================== #

#asked question:
#Mettre en place une heuristique de recherche locale (descente ou plus profonde descente) fondée sur deux voisinages (exemple : type “k-p exchange”).

#taking an array of lenght m
#returning another array of length m
include("DM1_1.jl")

# Notre structure de Solution
mutable struct Solution
    #""""état actuel de notre Solution"""
    x::Array{Bool,1}
    #"""fonction objectif de notre Solution"""
    objectif::Integer

    Solution(x,objectif) = new(x,objectif)
end

"""
    Calcule la fonction objectif d'une Solution
"""
function calculz(x,costs,m)
    objectif = 0
    for i in 1:m
        #checkbounds(x,m)
        objectif += x[i]*costs[i]
    end

    return objectif
end

"""
    fonction qui renvoies "vrai" si la variable j peu être entrée dans la Solution
"""

function select(crts,matrix,m,j)
    selec = true # juste une initialisation
    for l in 1:m
        selec = selec && (( crts[l] * matrix[l,j]) == 0)
    end
    return selec
end

"""
    fonction de kp-exchange avec k=1 et p=1
    fonction lourde : à optimiser
"""
function exchange1_1(solution,n,m,couts,crts,matrix)
    x_best = Solution(solution,calculz(solution,couts,n))
    # notre boucle k
    for i in 1:n
        if  x_best.x[i]  == 1
            x_prime = deepcopy(x_best)
            x_prime.x[i] = 0
            x_prime.objectif -= couts[i]
            # notre boucle p
            for j = 1:n
                if x_prime.x[j] == 0 && j != i && select(crts,matrix,m,j)# j != i car ça sers à rien de remettre une variable enlevée
                    x_seconde = deepcopy(x_prime)
                    x_seconde.x[j] = 1
                    x_seconde.objectif = couts[j]

                    #on met à jour notre meilleur Solution
                    if x_seconde.objectif > x_best.objectif
                        x_best = deepcopy(x_seconde)
                    end
                end
            end
        end
    end
    #x,objectif= Solution.x
#    println(x_best.objectif, "\n")
    return(x_best.x,x_best.objectif)
end

"""
    fonction de kp-exchange avec k=1 et p=1
    fonction lourde : à optimiser
"""
function exchange1_2(solution,n,m,couts,crts,matrix)
    x_best = Solution(solution,calculz(solution,couts,n))
    # notre boucle k
    for i in 1:n
        if  x_best.x[i]  == 1
            x_prime = deepcopy(x_best)
            x_prime.x[i] = 0
            x_prime.objectif -= couts[i]
            # notre boucle p
            for j = 1:n
                if x_prime.x[j] == 0 && j != i && select(crts,matrix,m,j) # j != i car ça sers à rien de remettre une variable enlevée
                    x_seconde = deepcopy(x_prime)
                    x_seconde.x[j] = 1
                    x_seconde.objectif += couts[j]
                    for p in j:n
                        if  x_seconde.x[p] == 0 && p != i && select(crts,matrix,m,p)
                            x_tierce = deepcopy(x_seconde)
                            x_tierce.x[p] = 1
                            x_tierce.objectif += couts[p]

                            if x_tierce.objectif > x_best.objectif
                                x_best = deepcopy(x_tierce)
                            end
                        end
                    end
                    #on met à jour notre meilleur Solution

                end
            end
        end
    end
    #x,objectif= Solution.x
#    println(x_best.objectif, "\n")
    return(x_best.x,x_best.objectif)
end

function fast1_2(solution,n,m,couts,crts,matrix)
    x_best = Solution(solution,calculz(solution,couts,n))
    for i in 1:n
        if  x_best.x[i]  == 1
            x_prime = deepcopy(x_best)
            x_prime.x[i] = 0
            x_prime.objectif -= couts[i]
            for j = 1:n
                if x_prime.x[j] == 0 && j != i && select(crts,matrix,m,j)
                    x_seconde = deepcopy(x_prime)
                    x_seconde.x[j] = 1
                    x_seconde.objectif += couts[j]
                    for p in j:n
                        if  x_seconde.x[p] == 0 && p != i && select(crts,matrix,m,p)
                            x_tierce = deepcopy(x_seconde)
                            x_tierce.x[p] = 1
                            x_tierce.objectif += couts[p]

                            if x_tierce.objectif > x_best.objectif
                                x_best = deepcopy(x_tierce)
                                return(x_best.x,x_best.objectif)
                            end
                        end
                    end
                end
            end
        end
    end
    return(x_best.x,x_best.objectif)
end

function fast1_2opti(solution,n,m,couts,crts,matrix)
    x_best = Solution(solution,calculz(solution,couts,n))
    trouvé = false
    i = 0
    x_best = Solution(solution,calculz(solution,couts,n))
    while i < n && !trouvé
        i+=1
        if  x_best.x[i]  == 1
            x_prime = deepcopy(x_best)
            x_prime.x[i] = 0
            x_prime.objectif -= couts[i]
            j = 0
            while j < n && !trouvé
                j+=1
                if x_prime.x[j] == 0 && j != i && select(crts,matrix,m,j)
                    x_seconde = deepcopy(x_prime)
                    x_seconde.x[j] = 1
                    x_seconde.objectif += couts[j]
                    p = 0
                    while p < n && !trouvé
                        p+=1
                        if  x_seconde.x[p] == 0 && p != i && select(crts,matrix,m,p)
                            x_tierce = deepcopy(x_seconde)
                            x_tierce.x[p] = 1
                            x_tierce.objectif = couts[p]

                            if x_tierce.objectif > x_best.objectif
                                x_best = deepcopy(x_tierce)
                                trouvé = true
                            end
                        end
                    end
                end
            end
        end
    end
    return(x_best.x,x_best.objectif)
end

function ite1_2(solution,n,m,couts,crts,matrix)
	x = (solution, calculz(solution))
	arret = false
	while ! arret
		x_prime = fast1_2opti(x[1],n,m,couts,crts,matrix)
		 arrêt = x[2] == x_prime[2]
		 x = x_prime
	end
end


# Data for the simulation

using Parameters

  @with_kw struct Data
   N :: Int64 = 10_000

   eps :: Float64 = 5.0
   sig :: Float64 = 0.5

   cutoff :: Float64 = 2.0
   side :: Float64 = 100

   sig6 :: Float64 = (sig^2)^3
   eps4 :: Float64 = 4*eps
   sig12 :: Float64 = (sig^2)^6

end


function corner_coordinates(tess::Tessellation)
    # Each cell needs the number of points + line from last to first point + NaN
    n_points = mapreduce(x -> length(x) + 2, +, tess.Cells)
    p = Vector{GeometryBasics.Point2{Float64}}(undef, n_points)

    count = 0
    for cell in tess.Cells
        for corner in cell
            count += 1
            p[count] = GeometryBasics.Point2(getx(corner), gety(corner))
        end

        count += 1
        p[count] = GeometryBasics.Point2(getx(cell[1]), gety(cell[1]))

        count += 1
        p[count] = GeometryBasics.Point2(NaN, NaN)
    end

    return p
end

@recipe function plot(tess::Tessellation)
    @series begin
        xlims --> (left(tess.EnclosingRectangle), right(tess.EnclosingRectangle))
        ylims --> (lower(tess.EnclosingRectangle), upper(tess.EnclosingRectangle))
        seriestype --> :path
        label --> "Voronoi cells"

        corner_coordinates(tess)
    end
end


# TODO: uncomment vexity checks once SDP on vars/constraints changes vexity of problem
@add_problem sdp function sdp_sdp_variables(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    y = Variable((2, 2), :Semidefinite)
    p = minimize(y[1, 1]; numeric_type = T)

    # @fact vexity(p) --> ConvexVexity()
    handle_problem!(p)
    if test
        @test p.optval ≈ 0 atol = atol rtol = rtol
    end

    y = Variable((3, 3), :Semidefinite)
    p = minimize(y[1, 1], y[2, 2] == 1; numeric_type = T)

    # @fact vexity(p) --> ConvexVexity()
    handle_problem!(p)
    if test
        @test p.optval ≈ 0 atol = atol rtol = rtol
    end

    # Solution is obtained as y[2,2] -> infinity
    # This test fails on Mosek. See
    # https://github.com/JuliaOpt/Mosek.jl/issues/29
    # y = Variable((2, 2), :Semidefinite)
    # p = minimize(y[1, 1], y[1, 2] == 1; numeric_type = T)

    # # @fact vexity(p) --> ConvexVexity()
    # handle_problem!(p)
    # @fact p.optval --> roughly(0, atol)

    y = Semidefinite(3)
    p = minimize(sum(diag(y)), y[1, 1] == 1; numeric_type = T)

    # @fact vexity(p) --> ConvexVexity()
    handle_problem!(p)
    if test
        @test p.optval ≈ 1 atol = atol rtol = rtol
    end

    y = Variable((3, 3), :Semidefinite)
    p = minimize(tr(y), y[2, 1] <= 4, y[2, 2] >= 3; numeric_type = T)

    # @fact vexity(p) --> ConvexVexity()
    handle_problem!(p)
    if test
        @test p.optval ≈ 3 atol = atol rtol = rtol
    end

    x = Variable(Positive())
    y = Semidefinite(3)
    p = minimize(y[1, 2], y[2, 1] == 1; numeric_type = T)

    # @fact vexity(p) --> ConvexVexity()
    handle_problem!(p)
    if test
        @test p.optval ≈ 1 atol = atol rtol = rtol
    end
end

@add_problem sdp function sdp_sdp_constraints(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    x = Variable(Positive())
    y = Variable((3, 3))
    p = minimize(
        x + y[1, 1],
        isposdef(y),
        x >= 1,
        y[2, 1] == 1;
        numeric_type = T,
    )

    # @fact vexity(p) --> ConvexVexity()
    handle_problem!(p)
    if test
        @test p.optval ≈ 1 atol = atol rtol = rtol
    end
end

@add_problem sdp function sdp_nuclear_norm_atom(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    y = Semidefinite(3)
    p = minimize(
        nuclearnorm(y),
        y[2, 1] <= 4,
        y[2, 2] >= 3,
        y[3, 3] <= 2;
        numeric_type = T,
    )

    if test
        @test vexity(p) == ConvexVexity()
    end
    handle_problem!(p)
    if test
        @test p.optval ≈ 3 atol = atol rtol = rtol
        @test evaluate(nuclearnorm(y)) ≈ 3 atol = atol rtol = rtol
    end
end

@add_problem sdp function sdp_nuclear_norm_atom_complex(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    A = [1 2im 3 4; 4im 3im 2 1; 4 5 6 7]
    y = ComplexVariable(3, 4)
    p = minimize(nuclearnorm(y), y == A; numeric_type = T)

    if test
        @test vexity(p) == ConvexVexity()
    end
    handle_problem!(p)
    if test
        @test p.optval ≈ sum(svdvals(A)) atol = atol rtol = rtol
        @test evaluate(nuclearnorm(y)) ≈ sum(svdvals(A)) atol = atol rtol = rtol
    end
end

@add_problem sdp function sdp_operator_norm_atom(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    y = Variable((3, 3))
    p = minimize(
        opnorm(y),
        y[2, 1] <= 4,
        y[2, 2] >= 3,
        sum(y) >= 12;
        numeric_type = T,
    )

    if test
        @test vexity(p) == ConvexVexity()
    end
    handle_problem!(p)
    if test
        @test p.optval ≈ 4 atol = atol rtol = rtol
        @test evaluate(opnorm(y)) ≈ 4 atol = atol rtol = rtol
    end
end

@add_problem sdp function sdp_operator_norm_atom_complex(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    A = [1 2im 3 4; 4im 3im 2 1; 4 5 6 7]
    y = ComplexVariable(3, 4)
    p = minimize(opnorm(y), y == A; numeric_type = T)

    if test
        @test vexity(p) == ConvexVexity()
    end
    handle_problem!(p)
    if test
        @test p.optval ≈ maximum(svdvals(A)) atol = atol rtol = rtol
        @test evaluate(opnorm(y)) ≈ maximum(svdvals(A)) atol = atol rtol = rtol
    end
end

@add_problem sdp function sdp_sigma_max_atom(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    y = Variable((3, 3))
    p = minimize(
        sigmamax(y),
        y[2, 1] <= 4,
        y[2, 2] >= 3,
        sum(y) >= 12;
        numeric_type = T,
    )

    if test
        @test vexity(p) == ConvexVexity()
    end
    handle_problem!(p)
    if test
        @test p.optval ≈ 4 atol = atol rtol = rtol
        @test evaluate(sigmamax(y)) ≈ 4 atol = atol rtol = rtol
    end
end

@add_problem sdp function sdp_dual_lambda_max_atom(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    y = Semidefinite(3)
    p = minimize(eigmax(y), y[1, 1] >= 4; numeric_type = T)

    if test
        @test vexity(p) == ConvexVexity()
    end
    handle_problem!(p)
    if test
        @test p.optval ≈ 4 atol = atol rtol = rtol
        @test evaluate(eigmax(y)) ≈ 4 atol = atol rtol = rtol
    end

    # https://github.com/jump-dev/Convex.jl/issues/337
    x = ComplexVariable(2, 2)
    p = minimize(
        eigmax(x),
        [x[1, 2] == im, x[2, 2] == 1.0, x ⪰ -eye(2)];
        numeric_type = T,
    )
    handle_problem!(p)
    if test
        @test p.optval ≈ 1.5 atol = atol rtol = rtol
        @test p.constraints[1].dual ≈ im atol = atol rtol = rtol
        @test p.constraints[2].dual ≈ 0.75 atol = atol rtol = rtol
    end
end

@add_problem sdp function sdp_lambda_min_atom(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    y = Semidefinite(3)
    p = maximize(eigmin(y), tr(y) <= 6; numeric_type = T)

    if test
        @test vexity(p) == ConvexVexity()
    end
    handle_problem!(p)
    if test
        @test p.optval ≈ 2 atol = atol rtol = rtol
        @test evaluate(eigmin(y)) ≈ 2 atol = atol rtol = rtol
    end
end

@add_problem sdp function sdp_matrix_frac_atom(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    x = [1, 2, 3]
    P = Variable(3, 3)
    p = minimize(
        matrixfrac(x, P),
        P <= 2 * eye(3),
        P >= 0.5 * eye(3);
        numeric_type = T,
    )

    if test
        @test vexity(p) == ConvexVexity()
    end
    handle_problem!(p)
    if test
        @test p.optval ≈ 7 atol = atol rtol = rtol
        @test (evaluate(matrixfrac(x, P)))[1] ≈ 7 atol = atol rtol = rtol
    end
end

@add_problem sdp function sdp_matrix_frac_atom_both_arguments_variable(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    x = Variable(3)
    P = Variable(3, 3)
    p = minimize(matrixfrac(x, P), eigmax(P) <= 2, x[1] >= 1; numeric_type = T)

    if test
        @test vexity(p) == ConvexVexity()
    end
    handle_problem!(p)
    if test
        @test p.optval ≈ 0.5 atol = atol rtol = rtol
        @test (evaluate(matrixfrac(x, P)))[1] ≈ 0.5 atol = atol rtol = rtol
    end
end

@add_problem sdp function sdp_sum_largest_eigs(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    x = Semidefinite(3)
    p = minimize(sumlargesteigs(x, 2), x >= 1; numeric_type = T)

    handle_problem!(p)

    if test
        @test p.optval ≈ 3 atol = atol rtol = rtol
        @test evaluate(x) ≈ ones(3, 3) atol = atol rtol = rtol
    end

    x = Semidefinite(3)
    p = minimize(
        sumlargesteigs(x, 2) + sumlargesteigs(x, 0),
        [x[i, :] >= i for i in 1:3]...;
        numeric_type = T,
    )

    handle_problem!(p)

    if test
        @test p.optval ≈ 8.4853 atol = atol rtol = rtol
    end

    A = [
        1 -2im 3 4
        2im 7 3im 5
        3 -3im 2 9
        4 5 9 4
    ]

    x = ComplexVariable(4, 4)
    p = minimize(sumlargesteigs(x, 3), x == A; numeric_type = T)

    handle_problem!(p)

    if test
        @test p.optval ≈ sum(eigvals(A)[2:end]) atol = atol rtol = rtol
    end

    x1 = Semidefinite(3)
    p1 = minimize(eigmax(x1), x1[1, 1] >= 4; numeric_type = T)

    handle_problem!(p1)

    x2 = Semidefinite(3)
    p2 = minimize(sumlargesteigs(x2, 1), x2[1, 1] >= 4; numeric_type = T)

    handle_problem!(p2)

    if test
        @test p1.optval ≈ p2.optval atol = atol rtol = rtol
    end

    x1 = Semidefinite(3)
    p1 = minimize(eigmax(x1), [x1[i, :] >= i for i in 1:3]...; numeric_type = T)

    handle_problem!(p1)

    x2 = Semidefinite(3)
    p2 = minimize(
        sumlargesteigs(x2, 1),
        [x2[i, :] >= i for i in 1:3]...;
        numeric_type = T,
    )

    handle_problem!(p2)

    if test
        @test p1.optval ≈ p2.optval atol = atol rtol = rtol
    end
end

@add_problem sdp function sdp_kron_atom(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    id = eye(4)
    X = Semidefinite(4)
    W = kron(id, X)
    p = maximize(tr(W), tr(X) ≤ 1; numeric_type = T)

    if test
        @test vexity(p) == AffineVexity()
    end
    handle_problem!(p)
    if test
        @test p.optval ≈ 4 atol = atol rtol = rtol
    end
end

@add_problem sdp function sdp_Partial_trace(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    A = Semidefinite(2)
    B = [1 0; 0 0]
    ρ = kron(B, A)
    constraints = [
        partialtrace(ρ, 1, [2; 2]) ==
        [0.09942819 0.29923607; 0.29923607 0.90057181],
        ρ in :SDP,
    ]
    p = satisfy(constraints; numeric_type = T)

    handle_problem!(p)
    if test
        @test evaluate(ρ) ≈ [
            0.09942819 0.29923607 0 0
            0.299237 0.900572 0 0
            0 0 0 0
            0 0 0 0
        ] atol = atol rtol = rtol
        @test evaluate(partialtrace(ρ, 1, [2; 2])) ≈
              [0.09942819 0.29923607; 0.29923607 0.90057181] atol = atol rtol =
            rtol
    end

    function rand_normalized(n)
        A = 5 * randn(n, n) + im * 5 * randn(n, n)
        return A / tr(A)
    end

    As = [rand_normalized(3) for _ in 1:5]
    Bs = [rand_normalized(2) for _ in 1:5]
    p = rand(5)

    AB = sum(i -> p[i] * kron(As[i], Bs[i]), 1:5)
    if test
        @test partialtrace(AB, 2, [3, 2]) ≈ sum(p .* As) atol = atol rtol = rtol
        @test partialtrace(AB, 1, [3, 2]) ≈ sum(p .* Bs) atol = atol rtol = rtol
    end

    A, B, C = rand(5, 5), rand(4, 4), rand(3, 3)
    ABC = kron(kron(A, B), C)
    if test
        @test kron(A, B) * tr(C) ≈ partialtrace(ABC, 3, [5, 4, 3]) atol = atol rtol =
            rtol
    end

    # Test 281
    A = rand(6, 6)
    expr = partialtrace(Constant(A), 1, [2, 3])
    if test
        @test size(expr) == size(evaluate(expr))

        @test_throws ArgumentError partialtrace(rand(6, 6), 3, [2, 3])
        @test_throws ArgumentError partialtrace(rand(6, 6), 1, [2, 4])
        @test_throws ArgumentError partialtrace(rand(3, 4), 1, [2, 3])
    end
end

@add_problem sdp function sdp_Real_Variables_with_complex_equality_constraints(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    n = 10 # variable dimension (parameter)
    m = 5 # number of constraints (parameter)
    xo = rand(n)
    A = randn(m, n) + im * randn(m, n)
    b = A * xo
    x = Variable(n)
    p1 = minimize(sum(x), A * x == b, x >= 0; numeric_type = T)

    handle_problem!(p1)

    if test
        x1 = copy(evaluate(x))
    end

    p2 = minimize(
        sum(x),
        real(A) * x == real(b),
        imag(A) * x == imag(b),
        x >= 0;
        numeric_type = T,
    )

    handle_problem!(p2)
    if test
        x2 = evaluate(x)
        @test x1 == x2
    end
end

@add_problem sdp function sdp_Complex_Variable_with_complex_equality_constraints(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    n = 10 # variable dimension (parameter)
    m = 5 # number of constraints (parameter)
    xo = rand(n) + im * rand(n)
    A = randn(m, n) + im * randn(m, n)
    b = A * xo
    x = ComplexVariable(n)
    p1 = minimize(
        real(sum(x)),
        A * x == b,
        real(x) >= 0,
        imag(x) >= 0;
        numeric_type = T,
    )

    handle_problem!(p1)

    if test
        x1 = evaluate(x)
    end

    xr = Variable(n)
    xi = Variable(n)
    p2 = minimize(
        sum(xr),
        real(A) * xr - imag(A) * xi == real(b),
        imag(A) * xr + real(A) * xi == imag(b),
        xr >= 0,
        xi >= 0;
        numeric_type = T,
    )

    handle_problem!(p2)

    if test
        real_diff = real(x1) - evaluate(xr)
        @test real_diff ≈ zeros(10, 1) atol = atol rtol = rtol

        imag_diff = imag(x1) - evaluate(xi)
        @test imag_diff ≈ zeros(10, 1) atol = atol rtol = rtol
    end
end

@add_problem sdp function sdp_Issue_198(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    ρ = HermitianSemidefinite(2)
    constraints = [ρ == [1.0 0.0; 0.0 1.0]]
    p = satisfy(constraints; numeric_type = T)

    handle_problem!(p)
    if test
        @test p.status == MOI.OPTIMAL
        @test evaluate(ρ) ≈ [1.0 0.0; 0.0 1.0] atol = atol rtol = rtol
        @test p.optval ≈ 0 atol = atol rtol = rtol
    end
end

@add_problem sdp function sdp_socp_norm2_atom(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    a = 2 + 4im
    x = ComplexVariable()
    objective = norm2(a - x)
    c1 = real(x) >= 0
    p = minimize(objective, c1; numeric_type = T)

    handle_problem!(p)
    if test
        @test p.optval ≈ 0 atol = atol rtol = rtol
        @test evaluate(objective) ≈ 0 atol = atol rtol = rtol

        real_diff = real(evaluate(x)) - real(a)
        imag_diff = imag(evaluate(x)) - imag(a)
        @test real_diff ≈ 0 atol = atol rtol = rtol
        @test imag_diff ≈ 0 atol = atol rtol = rtol
    end
end

@add_problem sdp function sdp_socp_sumsquares_atom(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    a = [2 + 4im; 4 + 6im]
    x = ComplexVariable(2)
    objective = sumsquares(a - x)
    c1 = real(x) >= 0
    p = minimize(objective, c1; numeric_type = T)

    handle_problem!(p)
    if test
        @test p.optval ≈ 0 atol = atol rtol = rtol
        @test evaluate(objective) ≈ 0.0 atol = atol rtol = rtol

        real_diff = real.(evaluate(x)) - real.(a)
        imag_diff = imag.(evaluate(x)) - imag.(a)
        @test real_diff ≈ zeros(2, 1) atol = atol rtol = rtol
        @test imag_diff ≈ zeros(2, 1) atol = atol rtol = rtol
    end
end

@add_problem sdp function sdp_socp_abs_atom(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    a = 5 - 4im
    x = ComplexVariable()
    objective = abs(a - x)
    c1 = real(x) >= 0
    p = minimize(objective, c1; numeric_type = T)

    handle_problem!(p)
    if test
        @test p.optval ≈ 0 atol = atol rtol = rtol
        @test evaluate(objective) ≈ 0.0 atol = atol rtol = rtol

        real_diff = real(evaluate(x)) .- real(a)
        imag_diff = imag(evaluate(x)) .- imag(a)
        @test real_diff ≈ 0.0 atol = atol rtol = rtol
        @test imag_diff ≈ 0.0 atol = atol rtol = rtol
    end
end

@add_problem sdp function sdp_Complex_Semidefinite_constraint(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    n = 10
    A = rand(n, n) + im * rand(n, n)
    A = A + A' # now A is hermitian
    x = ComplexVariable(n, n)
    objective = sumsquares(A - x)
    c1 = x in :SDP
    p = minimize(objective, c1; numeric_type = T)

    handle_problem!(p)
    # test that X is approximately equal to posA:
    l, v = eigen(A)
    posA = v * Diagonal(max.(l, 0)) * v'

    if test
        real_diff = real.(evaluate(x)) - real.(posA)
        imag_diff = imag.(evaluate(x)) - imag.(posA)
        @test real_diff ≈ zeros(n, n) atol = atol rtol = rtol
        @test imag_diff ≈ zeros(n, n) atol = atol rtol = rtol
    end
end

@add_problem sdp function sdp_geom_mean_hypocone_argcheck(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    if test
        @test_throws DimensionMismatch GeomMeanHypoCone(
            zeros(2, 3),
            zeros(2, 3),
            1 // 2,
        )
        @test_throws DimensionMismatch GeomMeanHypoCone(
            zeros(2, 3),
            Variable(2, 3),
            1 // 2,
        )
        @test_throws DimensionMismatch GeomMeanHypoCone(
            Variable(2, 3),
            zeros(2, 3),
            1 // 2,
        )
        @test_throws DimensionMismatch GeomMeanHypoCone(
            Variable(2, 3),
            Variable(2, 3),
            1 // 2,
        )

        @test_throws DimensionMismatch GeomMeanHypoCone(
            zeros(2, 2),
            zeros(3, 3),
            1 // 2,
        )
        @test_throws DimensionMismatch GeomMeanHypoCone(
            zeros(2, 2),
            Variable(3, 3),
            1 // 2,
        )
        @test_throws DimensionMismatch GeomMeanHypoCone(
            Variable(2, 2),
            zeros(3, 3),
            1 // 2,
        )
        @test_throws DimensionMismatch GeomMeanHypoCone(
            Variable(2, 2),
            Variable(2, 3),
            1 // 2,
        )

        @test_throws DimensionMismatch Variable(2, 2) in GeomMeanHypoCone(
            Variable(3, 3),
            Variable(3, 3),
            1 // 2,
        )

        @test_throws DomainError GeomMeanHypoCone(
            Variable(3, 3),
            Variable(3, 3),
            -1 // 2,
        )
        @test_throws DomainError GeomMeanHypoCone(
            Variable(3, 3),
            Variable(3, 3),
            3 // 2,
        )
    end
end

@add_problem sdp function sdp_geom_mean_epicone_argcheck(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    if test
        @test_throws DimensionMismatch GeomMeanEpiCone(
            zeros(2, 3),
            zeros(2, 3),
            -1 // 2,
        )
        @test_throws DimensionMismatch GeomMeanEpiCone(
            zeros(2, 3),
            Variable(2, 3),
            -1 // 2,
        )
        @test_throws DimensionMismatch GeomMeanEpiCone(
            Variable(2, 3),
            zeros(2, 3),
            -1 // 2,
        )
        @test_throws DimensionMismatch GeomMeanEpiCone(
            Variable(2, 3),
            Variable(2, 3),
            -1 // 2,
        )

        @test_throws DimensionMismatch GeomMeanEpiCone(
            zeros(2, 2),
            zeros(3, 3),
            -1 // 2,
        )
        @test_throws DimensionMismatch GeomMeanEpiCone(
            zeros(2, 2),
            Variable(3, 3),
            -1 // 2,
        )
        @test_throws DimensionMismatch GeomMeanEpiCone(
            Variable(2, 2),
            zeros(3, 3),
            -1 // 2,
        )
        @test_throws DimensionMismatch GeomMeanEpiCone(
            Variable(2, 2),
            Variable(2, 3),
            -1 // 2,
        )

        @test_throws DimensionMismatch Variable(2, 2) in GeomMeanEpiCone(
            Variable(3, 3),
            Variable(3, 3),
            -1 // 2,
        )
        @test_throws DomainError GeomMeanEpiCone(
            Variable(3, 3),
            Variable(3, 3),
            -3 // 2,
        )
        @test_throws DomainError GeomMeanEpiCone(
            Variable(3, 3),
            Variable(3, 3),
            1 // 2,
        )
        @test_throws DomainError GeomMeanEpiCone(
            Variable(3, 3),
            Variable(3, 3),
            5 // 2,
        )
    end
end

@add_problem sdp function sdp_relative_entropy_argcheck(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    if test
        @test_throws DimensionMismatch RelativeEntropyEpiCone(
            zeros(2, 3),
            zeros(2, 3),
            3,
            3,
            eye(2),
        )
        @test_throws DimensionMismatch RelativeEntropyEpiCone(
            zeros(2, 3),
            Variable(2, 3),
            3,
            3,
            eye(2),
        )
        @test_throws DimensionMismatch RelativeEntropyEpiCone(
            Variable(2, 3),
            zeros(2, 3),
            3,
            3,
            eye(2),
        )
        @test_throws DimensionMismatch RelativeEntropyEpiCone(
            Variable(2, 3),
            Variable(2, 3),
            3,
            3,
            eye(2),
        )

        @test_throws DimensionMismatch RelativeEntropyEpiCone(
            zeros(2, 2),
            zeros(3, 3),
            3,
            3,
        )
        @test_throws DimensionMismatch RelativeEntropyEpiCone(
            zeros(2, 2),
            Variable(3, 3),
            3,
            3,
        )
        @test_throws DimensionMismatch RelativeEntropyEpiCone(
            Variable(2, 2),
            zeros(3, 3),
            3,
            3,
        )
        @test_throws DimensionMismatch RelativeEntropyEpiCone(
            Variable(2, 2),
            Variable(2, 3),
            3,
            3,
        )

        @test_throws DimensionMismatch RelativeEntropyEpiCone(
            zeros(3, 3),
            zeros(3, 3),
            3,
            3,
            zeros(2, 2),
        )
        @test_throws DimensionMismatch RelativeEntropyEpiCone(
            zeros(3, 3),
            Variable(3, 3),
            3,
            3,
            zeros(2, 2),
        )
        @test_throws DimensionMismatch RelativeEntropyEpiCone(
            Variable(3, 3),
            zeros(3, 3),
            3,
            3,
            zeros(2, 2),
        )
        @test_throws DimensionMismatch RelativeEntropyEpiCone(
            Variable(3, 3),
            Variable(2, 3),
            3,
            3,
            zeros(2, 2),
        )

        @test_throws DimensionMismatch RelativeEntropyEpiCone(
            zeros(3, 3),
            zeros(3, 3),
            3,
            3,
            zeros(2, 3),
        )
        @test_throws DimensionMismatch RelativeEntropyEpiCone(
            zeros(3, 3),
            Variable(3, 3),
            3,
            3,
            zeros(2, 3),
        )
        @test_throws DimensionMismatch RelativeEntropyEpiCone(
            Variable(3, 3),
            zeros(3, 3),
            3,
            3,
            zeros(2, 3),
        )
        @test_throws DimensionMismatch RelativeEntropyEpiCone(
            Variable(3, 3),
            Variable(2, 3),
            3,
            3,
            zeros(2, 3),
        )

        @test_throws DimensionMismatch RelativeEntropyEpiCone(
            zeros(3, 3),
            zeros(3, 3),
            3,
            3,
            zeros(2),
        )
        @test_throws DimensionMismatch RelativeEntropyEpiCone(
            zeros(3, 3),
            Variable(3, 3),
            3,
            3,
            zeros(2),
        )
        @test_throws DimensionMismatch RelativeEntropyEpiCone(
            Variable(3, 3),
            zeros(3, 3),
            3,
            3,
            zeros(2),
        )
        @test_throws DimensionMismatch RelativeEntropyEpiCone(
            Variable(3, 3),
            Variable(2, 3),
            3,
            3,
            zeros(2),
        )

        @test_throws DimensionMismatch Variable(2, 2) in RelativeEntropyEpiCone(
            Variable(3, 3),
            Variable(3, 3),
            3,
            3,
        )
    end
end

@add_problem sdp function sdp_relative_entropy(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    for cplx in [false, true]
        n = 4
        X = eye(n)
        if cplx
            Y = ComplexVariable(n, n)
        else
            Y = Variable(n, n)
        end

        c1 = eye(n) in RelativeEntropyEpiCone(X, Y)
        objective = real(tr(Y))
        p = minimize(objective, c1; numeric_type = T)

        handle_problem!(p)

        if test
            @test Y.value ≈ eye(n) * exp(-1) atol = atol rtol = rtol
        end
    end
end

@add_problem sdp function sdp_geom_mean_hypocone_real_0(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    n = 4
    A = Variable(n, n)
    B = randn(n, n)
    B = B * B' # now A is positive semidefinite
    B += 0.2 * I # prevent numerical instability

    c1 = eye(n) in GeomMeanHypoCone(A, B, 0)
    objective = tr(A)
    p = minimize(objective, c1; numeric_type = T)

    handle_problem!(p)

    if test
        @test A.value ≈ eye(n) atol = atol rtol = rtol
    end
end

@add_problem sdp function sdp_geom_mean_hypocone_real_1(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    n = 4
    A = Variable(n, n)
    B = randn(n, n)
    B = B * B' # now A is positive semidefinite
    B += 0.2 * I # prevent numerical instability

    c1 = eye(n) in GeomMeanHypoCone(B, A, 1)
    objective = tr(A)
    p = minimize(objective, c1; numeric_type = T)

    handle_problem!(p)

    if test
        @test A.value ≈ eye(n) atol = atol rtol = rtol
    end
end

@add_problem sdp function sdp_geom_mean_hypocone_real_1_2(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    n = 4
    A = randn(n, n)
    A = A * A' # now A is positive semidefinite
    A += 0.2 * I # prevent numerical instability
    B = Variable(n, n)

    c1 = eye(n) in GeomMeanHypoCone(A, B, 1 // 2)
    objective = tr(B)
    p = minimize(objective, c1; numeric_type = T)

    handle_problem!(p)

    if test
        @test B.value ≈ A^-1 atol = atol rtol = rtol
    end
end

@add_problem sdp function sdp_geom_mean_hypocone_real_3_8(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    n = 4
    A = randn(n, n)
    A = A * A' # now A is positive semidefinite
    A += 0.2 * I # prevent numerical instability
    B = Variable(n, n)

    c1 = eye(n) in GeomMeanHypoCone(A, B, 3 // 8)
    objective = tr(B)
    p = minimize(objective, c1; numeric_type = T)

    handle_problem!(p)

    # A #_t B = I  =>  B = A^(1-1/t)
    if test
        @test B.value ≈ A^(-5 // 3) atol = atol rtol = rtol
    end
end

@add_problem sdp function sdp_geom_mean_hypocone_real_3_5(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    n = 4
    A = randn(n, n)
    A = A * A' # now A is positive semidefinite
    A += 0.2 * I # prevent numerical instability
    B = Variable(n, n)

    c1 = eye(n) in GeomMeanHypoCone(A, B, 3 // 5)
    objective = tr(B)
    p = minimize(objective, c1; numeric_type = T)

    handle_problem!(p)

    # A #_t B = I  =>  B = A^(1-1/t)
    if test
        @test B.value ≈ A^(-2 // 3) atol = atol rtol = rtol
    end
end

@add_problem sdp function sdp_geom_mean_hypocone_cplx_1_2(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    n = 4
    A = randn(ComplexF64, n, n)
    A = A * A' # now A is positive semidefinite
    A += 0.2 * I # prevent numerical instability
    B = ComplexVariable(n, n)

    c1 = eye(n) in GeomMeanHypoCone(A, B, 1 // 2)
    objective = real(tr(B))
    p = minimize(objective, c1; numeric_type = T)

    handle_problem!(p)

    if test
        @test real.(B.value) ≈ real.(A^-1) atol = atol rtol = rtol
        @test imag.(B.value) ≈ imag.(A^-1) atol = atol rtol = rtol
    end
end

@add_problem sdp function sdp_geom_mean_hypocone_cplx_3_8(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    n = 4
    A = randn(ComplexF64, n, n)
    A = A * A' # now A is positive semidefinite
    A += 0.2 * I # prevent numerical instability
    B = ComplexVariable(n, n)

    c1 = eye(n) in GeomMeanHypoCone(A, B, 3 // 8)
    objective = real(tr(B))
    p = minimize(objective, c1; numeric_type = T)

    handle_problem!(p)

    # A #_t B = I  =>  B = A^(1-1/t)
    if test
        @test real.(B.value) ≈ real.(A^(-5 // 3)) atol = atol rtol = rtol
        @test imag.(B.value) ≈ imag.(A^(-5 // 3)) atol = atol rtol = rtol
    end
end

@add_problem sdp function sdp_geom_mean_hypocone_cplx_3_5(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    n = 4
    A = randn(ComplexF64, n, n)
    A = A * A' # now A is positive semidefinite
    A += 0.2 * I # prevent numerical instability
    B = ComplexVariable(n, n)

    c1 = eye(n) in GeomMeanHypoCone(A, B, 3 // 5)
    objective = real(tr(B))
    p = minimize(objective, c1; numeric_type = T)

    handle_problem!(p)

    # A #_t B = I  =>  B = A^(1-1/t)
    if test
        @test real.(B.value) ≈ real.(A^(-2 // 3)) atol = atol rtol = rtol
        @test imag.(B.value) ≈ imag.(A^(-2 // 3)) atol = atol rtol = rtol
    end
end

@add_problem sdp function sdp_geom_mean_hypocone_fullhyp(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    # These matrices satisfy A^(1/2) ⪰ B and so (A,eye(2),B) \in hyp_{1/2}
    # A and B however do not satisfy A ⪰ B^2 and so [A B; B eye(2)] not
    # psd, and using fullhyp=false will give an infeasible SDP.
    A = [6.25 0; 0 16]
    B = [2 1; 1 2]
    S = Semidefinite(2)

    p = minimize(
        0,
        [B in GeomMeanHypoCone(A, eye(2), 1 // 2, false)];
        numeric_type = T,
    )
    handle_problem!(p)
    if test
        @test p.status == MOI.INFEASIBLE
    end

    p = minimize(
        0,
        [B in GeomMeanHypoCone(A, eye(2), 1 // 2)];
        numeric_type = T,
    )
    handle_problem!(p)
    if test
        @test p.status == MOI.OPTIMAL
    end
end

@add_problem sdp function sdp_geom_mean_epicone_real_neg1_optA(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    n = 3
    B = randn(n, n)
    B = B * B' # now B is positive semidefinite
    B += 0.2 * I # prevent numerical instability
    A = Variable(n, n)

    c1 = eye(n) in GeomMeanEpiCone(A, B, -1)
    objective = tr(A)
    p = maximize(objective, c1; numeric_type = T)

    handle_problem!(p)

    # A #_t B = I  =>  B = A^(1-1/t)
    if test
        @test B ≈ A.value^2 atol = atol rtol = rtol
    end
end

@add_problem sdp function sdp_geom_mean_epicone_real_neg1_optB(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    n = 3
    A = randn(n, n)
    A = A * A' # now A is positive semidefinite
    A += 0.2 * I # prevent numerical instability
    A /= tr(A) # solver has problems if B is large
    B = Variable(n, n)

    c1 = eye(n) in GeomMeanEpiCone(A, B, -1)
    objective = tr(B)
    p = minimize(objective, c1; numeric_type = T)

    handle_problem!(p)

    # A #_t B = I  =>  B = A^(1-1/t)
    if test
        @test B.value ≈ A^2 atol = atol rtol = rtol
    end
end

@add_problem sdp function sdp_geom_mean_epicone_real_neg3_5_optA(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    n = 3
    B = randn(n, n)
    B = B * B' # now B is positive semidefinite
    B += 0.2 * I # prevent numerical instability
    A = Variable(n, n)

    c1 = eye(n) in GeomMeanEpiCone(A, B, -3 // 5)
    objective = tr(A)
    p = maximize(objective, c1; numeric_type = T)

    handle_problem!(p)

    # A #_t B = I  =>  B = A^(1-1/t)
    if test
        @test B ≈ A.value^(8 // 3) atol = atol rtol = rtol
    end
end

@add_problem sdp function sdp_geom_mean_epicone_real_neg3_5_optB(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    n = 3
    A = randn(n, n)
    A = A * A' # now A is positive semidefinite
    A += 0.2 * I # prevent numerical instability
    A /= tr(A) # solver has problems if B is large
    B = Variable(n, n)

    c1 = eye(n) in GeomMeanEpiCone(A, B, -3 // 5)
    objective = tr(B)
    p = minimize(objective, c1; numeric_type = T)

    handle_problem!(p)

    # A #_t B = I  =>  B = A^(1-1/t)
    if test
        @test B.value ≈ A^(8 // 3) atol = atol rtol = rtol
    end
end

@add_problem sdp function sdp_geom_mean_epicone_real_8_5_optA(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    n = 3
    B = randn(n, n)
    B = B * B' # now B is positive semidefinite
    B += 0.2 * I # prevent numerical instability
    B /= tr(B) # solver has problems if B is large
    A = Variable(n, n)

    c1 = eye(n) in GeomMeanEpiCone(A, B, 8 // 5)
    objective = tr(A)
    p = minimize(objective, c1; numeric_type = T)

    handle_problem!(p)

    # A #_t B = I  =>  B = A^(1-1/t)
    if test
        @test B ≈ A.value^(3 // 8) atol = atol rtol = rtol
    end
end

@add_problem sdp function sdp_geom_mean_epicone_real_8_5_optB(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    n = 3
    A = randn(n, n)
    A = A * A' # now A is positive semidefinite
    A += 0.2 * I # prevent numerical instability
    B = Variable(n, n)

    c1 = eye(n) in GeomMeanEpiCone(A, B, 8 // 5)
    objective = tr(B)
    p = maximize(objective, c1; numeric_type = T)

    handle_problem!(p)

    # A #_t B = I  =>  B = A^(1-1/t)
    if test
        @test B.value ≈ A^(3 // 8) atol = atol rtol = rtol
    end
end

@add_problem sdp function sdp_quantum_relative_entropy_argcheck(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    if test
        @test_throws DimensionMismatch quantum_relative_entropy(
            zeros(2, 3),
            zeros(2, 3),
        )
        @test_throws DimensionMismatch quantum_relative_entropy(
            zeros(2, 3),
            Variable(2, 3),
        )
        @test_throws DimensionMismatch quantum_relative_entropy(
            Variable(2, 3),
            zeros(2, 3),
        )
        @test_throws DimensionMismatch quantum_relative_entropy(
            Variable(2, 3),
            Variable(2, 3),
        )

        @test_throws DimensionMismatch quantum_relative_entropy(
            zeros(2, 2),
            zeros(3, 3),
        )
        @test_throws DimensionMismatch quantum_relative_entropy(
            zeros(2, 2),
            Variable(3, 3),
        )
        @test_throws DimensionMismatch quantum_relative_entropy(
            Variable(2, 2),
            zeros(3, 3),
        )
        @test_throws DimensionMismatch quantum_relative_entropy(
            Variable(2, 2),
            Variable(2, 3),
        )

        z = zeros(2, 2)
        v = Variable(2, 2)
        nh = [1 1; 0 1] # not hermitian
        np = [1 2; 2 1] # not positive semidefinite
        @test_throws DomainError quantum_relative_entropy(z, nh)
        @test_throws DomainError quantum_relative_entropy(z, np)
        @test_throws DomainError quantum_relative_entropy(nh, z)
        @test_throws DomainError quantum_relative_entropy(np, z)
        @test_throws DomainError quantum_relative_entropy(v, nh)
        @test_throws DomainError quantum_relative_entropy(v, np)
        @test_throws DomainError quantum_relative_entropy(nh, v)
        @test_throws DomainError quantum_relative_entropy(np, v)
    end
end

function sdp_quantum_relative_entropy_impl(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
    lowrank::Bool,
    mode::Integer,
) where {T,test}
    # Too slow for n>3.  Anything that can be done to speed it up?
    n = 3
    X = randn(ComplexF64, n, n)
    if lowrank
        X[:, 1] .= 0
    end
    X = X * X' # now X is positive semidefinite
    X /= tr(X) # make it a quantum state

    A = ComplexVariable(n, n)
    B = ComplexVariable(n, n)
    constraints = Array{Constraint,1}([A == X])
    if mode != 5
        push!(constraints, tr(B) == 1)
    end

    if mode == 1
        objective = quantum_relative_entropy(A, B)
    elseif mode == 2
        objective = quantum_relative_entropy(X, B)
    elseif mode == 3
        objective = quantum_relative_entropy(B, A)
    elseif mode == 4
        objective = quantum_relative_entropy(B, X)
    elseif mode == 5
        objective = quantum_relative_entropy(X, X)
    end

    p = minimize(objective, constraints; numeric_type = T)
    handle_problem!(p)

    if test
        @test real.(evaluate(A)) ≈ real.(X) atol = atol rtol = rtol
        @test imag.(evaluate(A)) ≈ imag.(X) atol = atol rtol = rtol
        if mode != 5
            @test real.(evaluate(B)) ≈ real.(X) atol = atol rtol = rtol
            @test imag.(evaluate(B)) ≈ imag.(X) atol = atol rtol = rtol
        end
        @test p.optval ≈ 0 atol = atol rtol = rtol
        if mode == 1
            @test p.optval ≈ evaluate(quantum_relative_entropy(A, B)) atol =
                atol rtol = rtol
        elseif mode == 2
            @test p.optval ≈ evaluate(quantum_relative_entropy(X, B)) atol =
                atol rtol = rtol
        elseif mode == 3
            @test p.optval ≈ evaluate(quantum_relative_entropy(B, A)) atol =
                atol rtol = rtol
        elseif mode == 4
            @test p.optval ≈ evaluate(quantum_relative_entropy(B, X)) atol =
                atol rtol = rtol
        elseif mode == 5
            @test p.optval ≈ evaluate(quantum_relative_entropy(X, X)) atol =
                atol rtol = rtol
        end
    end
end

@add_problem sdp function sdp_quantum_relative_entropy1_fullrank(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    return sdp_quantum_relative_entropy_impl(
        handle_problem!,
        Val(test),
        atol,
        rtol,
        T,
        false,
        1,
    )
end

@add_problem sdp function sdp_quantum_relative_entropy2_fullrank(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    return sdp_quantum_relative_entropy_impl(
        handle_problem!,
        Val(test),
        atol,
        rtol,
        T,
        false,
        2,
    )
end

@add_problem sdp function sdp_quantum_relative_entropy3_fullrank(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    return sdp_quantum_relative_entropy_impl(
        handle_problem!,
        Val(test),
        atol,
        rtol,
        T,
        false,
        3,
    )
end

@add_problem sdp function sdp_quantum_relative_entropy4_fullrank(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    return sdp_quantum_relative_entropy_impl(
        handle_problem!,
        Val(test),
        atol,
        rtol,
        T,
        false,
        4,
    )
end

@add_problem sdp function sdp_quantum_relative_entropy5_fullrank(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    return sdp_quantum_relative_entropy_impl(
        handle_problem!,
        Val(test),
        atol,
        rtol,
        T,
        false,
        5,
    )
end

@add_problem sdp function sdp_quantum_relative_entropy1_lowrank(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    return sdp_quantum_relative_entropy_impl(
        handle_problem!,
        Val(test),
        atol,
        rtol,
        T,
        true,
        1,
    )
end

@add_problem sdp function sdp_quantum_relative_entropy2_lowrank(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    return sdp_quantum_relative_entropy_impl(
        handle_problem!,
        Val(test),
        atol,
        rtol,
        T,
        true,
        2,
    )
end

@add_problem sdp function sdp_quantum_relative_entropy3_lowrank(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    return sdp_quantum_relative_entropy_impl(
        handle_problem!,
        Val(test),
        atol,
        rtol,
        T,
        true,
        3,
    )
end

@add_problem sdp function sdp_quantum_relative_entropy4_lowrank(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    return sdp_quantum_relative_entropy_impl(
        handle_problem!,
        Val(test),
        atol,
        rtol,
        T,
        true,
        4,
    )
end

@add_problem sdp function sdp_quantum_relative_entropy5_lowrank(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    return sdp_quantum_relative_entropy_impl(
        handle_problem!,
        Val(test),
        atol,
        rtol,
        T,
        true,
        5,
    )
end

@add_problem sdp function sdp_quantum_relative_entropy_const(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    if test
        @test quantum_relative_entropy(
            diagm(0 => [0.5, 0.5]),
            diagm(0 => [0.5, 0.5]),
        ) ≈ 0 atol = atol rtol = rtol
        @test quantum_relative_entropy(
            diagm(0 => [1.0, 0.0]),
            diagm(0 => [1.0, 0.0]),
        ) ≈ 0 atol = atol rtol = rtol
        @test quantum_relative_entropy(
            diagm(0 => [1.0, 0.0]),
            diagm(0 => [0.5, 0.5]),
        ) ≈ log(2) atol = atol rtol = rtol
        @test isinf(
            quantum_relative_entropy(
                diagm(0 => [0.5, 0.5]),
                diagm(0 => [1.0, 0.0]),
            ),
        )
    end
end

@add_problem sdp function sdp_quantum_channel_capacity(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    # Capacity of classical-quantum channel.  See chapter 3 of arxiv:1705.06671,
    # Fawzi, Fawzi "Efficient optimization of the quantum relative entropy".
    # For channels with two symbols, both pure states, we have an exact formula to compare against.

    n = 3

    for cplx in [false, true]
        if cplx
            ψ1 = normalize(randn(ComplexF64, n))
            ψ2 = normalize(randn(ComplexF64, n))
        else
            ψ1 = normalize(randn(Float64, n))
            ψ2 = normalize(randn(Float64, n))
        end
        ρ1 = ψ1 * ψ1'
        ρ2 = ψ2 * ψ2'

        p1 = Variable()
        p2 = Variable()

        objective =
            (
                quantum_entropy(p1 * ρ1 + p2 * ρ2) - p1 * quantum_entropy(ρ1) -
                p2 * quantum_entropy(ρ2)
            ) / log(2)
        constraints = [p1 >= 0, p2 >= 0, p1 + p2 == 1]
        p = maximize(objective, constraints; numeric_type = T)
        handle_problem!(p)

        ϵ = abs(ψ1' * ψ2)
        q = (1 + ϵ) / 2
        r = [q, 1 - q]
        v = -r' * log2.(r)

        if test
            @test p.optval ≈ v atol = atol rtol = rtol
            @test evaluate(objective) ≈ v atol = atol rtol = rtol

            @test_throws DimensionMismatch quantum_entropy(Variable(2, 3))
        end
    end
end

@add_problem sdp function sdp_trace_mpower_argcheck(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    if test
        @test_throws DimensionMismatch trace_mpower(
            zeros(2, 3),
            1 // 2,
            zeros(2, 3),
        )
        @test_throws DimensionMismatch trace_mpower(
            Variable(2, 3),
            1 // 2,
            zeros(2, 3),
        )

        @test_throws DimensionMismatch trace_mpower(
            zeros(2, 2),
            1 // 2,
            zeros(3, 3),
        )
        @test_throws DimensionMismatch trace_mpower(
            Variable(2, 2),
            1 // 2,
            zeros(3, 3),
        )

        @test_throws DomainError trace_mpower(
            Variable(3, 3),
            5 // 2,
            zeros(3, 3),
        )

        nh = [1 1; 0 1] # not hermitian
        np = [1 2; 2 1] # not positive semidefinite
        @test_throws DomainError trace_mpower(Variable(2, 2), 1 // 2, nh)
        @test_throws DomainError trace_mpower(Variable(2, 2), 1 // 2, np)
    end
end

@add_problem sdp function sdp_trace_mpower_real_2_3(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    # Too slow for n>3.  Anything that can be done to speed it up?
    n = 3
    A = randn(n, n)
    A = A * A' # now A is positive semidefinite
    C = randn(n, n)
    C = C * C' # now C is positive semidefinite
    t = 2 // 3

    B = Semidefinite(n, n)

    constraints = [B ⪯ A]
    objective = trace_mpower(B, t, C)
    p = maximize(objective, constraints; numeric_type = T)

    handle_problem!(p)

    if test
        @test real.(B.value) ≈ real.(A) atol = atol rtol = rtol
        @test imag.(B.value) ≈ imag.(A) atol = atol rtol = rtol
        @test p.optval ≈ tr(C * A^t) atol = atol rtol = rtol
        @test p.optval ≈ trace_mpower(A, t, C) atol = atol rtol = rtol
        @test p.optval ≈ evaluate(trace_mpower(B, t, C)) atol = atol rtol = rtol
    end
end

@add_problem sdp function sdp_trace_mpower_cplx_2_3(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    # Too slow for n>3.  Anything that can be done to speed it up?
    n = 3
    A = randn(ComplexF64, n, n)
    A = A * A' # now A is positive semidefinite
    C = randn(ComplexF64, n, n)
    C = C * C' # now C is positive semidefinite
    t = 2 // 3

    B = HermitianSemidefinite(n, n)

    constraints = [B ⪯ A]
    objective = trace_mpower(B, t, C)
    p = maximize(objective, constraints; numeric_type = T)

    handle_problem!(p)

    if test
        @test real.(B.value) ≈ real.(A) atol = atol rtol = rtol
        @test imag.(B.value) ≈ imag.(A) atol = atol rtol = rtol
        @test p.optval ≈ tr(C * A^t) atol = atol rtol = rtol
        @test p.optval ≈ trace_mpower(A, t, C) atol = atol rtol = rtol
        @test p.optval ≈ evaluate(trace_mpower(B, t, C)) atol = atol rtol = rtol
    end
end

@add_problem sdp function sdp_trace_mpower_real_5_4(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    # Too slow for n>3.  Anything that can be done to speed it up?
    n = 3
    A = randn(n, n)
    A = A * A' # now A is positive semidefinite
    C = randn(n, n)
    C = C * C' # now C is positive semidefinite
    t = 5 // 4

    B = Semidefinite(n, n)

    constraints = [
        # note: B ⪰ A doesn't work for this unit test because trace_mpower is not operator monotone
        # unless t ∈ [0,1].
        B == A,
    ]
    objective = trace_mpower(B, t, C)
    p = minimize(objective, constraints; numeric_type = T)

    handle_problem!(p)

    if test
        @test real.(B.value) ≈ real.(A) atol = atol rtol = rtol
        @test imag.(B.value) ≈ imag.(A) atol = atol rtol = rtol
        @test p.optval ≈ tr(C * A^t) atol = atol rtol = rtol
        @test p.optval ≈ trace_mpower(A, t, C) atol = atol rtol = rtol
        @test p.optval ≈ evaluate(trace_mpower(B, t, C)) atol = atol rtol = rtol
    end
end

@add_problem sdp function sdp_trace_mpower_cplx_5_4(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    # Too slow for n>3.  Anything that can be done to speed it up?
    n = 3
    A = randn(ComplexF64, n, n)
    A = A * A' # now A is positive semidefinite
    C = randn(ComplexF64, n, n)
    C = C * C' # now C is positive semidefinite
    t = 5 // 4

    B = HermitianSemidefinite(n, n)

    constraints = [
        # note: B ⪰ A doesn't work for this unit test because trace_mpower is not operator monotone
        # unless t ∈ [0,1].
        B == A,
    ]
    objective = trace_mpower(B, t, C)
    p = minimize(objective, constraints; numeric_type = T)

    handle_problem!(p)

    if test
        @test real.(B.value) ≈ real.(A) atol = atol rtol = rtol
        @test imag.(B.value) ≈ imag.(A) atol = atol rtol = rtol
        @test p.optval ≈ tr(C * A^t) atol = atol rtol = rtol
        @test p.optval ≈ trace_mpower(A, t, C) atol = atol rtol = rtol
        @test p.optval ≈ evaluate(trace_mpower(B, t, C)) atol = atol rtol = rtol
    end
end

@add_problem sdp function sdp_trace_mpower_real_neg1_4(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    # Too slow for n>3.  Anything that can be done to speed it up?
    n = 3
    A = randn(n, n)
    A = A * A' # now A is positive semidefinite
    A += 0.2 * I # prevent numerical instability
    C = randn(n, n)
    C = C * C' # now C is positive semidefinite
    t = -1 // 4

    B = Semidefinite(n, n)

    constraints = [
        # -trace_mpower is operator monotone for t ∈ [-1, 0].
        B ⪯ A,
    ]
    objective = trace_mpower(B, t, C)
    p = minimize(objective, constraints; numeric_type = T)

    handle_problem!(p)

    if test
        @test real.(B.value) ≈ real.(A) atol = atol rtol = rtol
        @test imag.(B.value) ≈ imag.(A) atol = atol rtol = rtol
        @test p.optval ≈ tr(C * A^t) atol = atol rtol = rtol
        @test p.optval ≈ trace_mpower(A, t, C) atol = atol rtol = rtol
        @test p.optval ≈ evaluate(trace_mpower(B, t, C)) atol = atol rtol = rtol
    end
end

@add_problem sdp function sdp_trace_mpower_cplx_neg1_4(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    # Too slow for n>3.  Anything that can be done to speed it up?
    n = 3
    A = randn(ComplexF64, n, n)
    A = A * A' # now A is positive semidefinite
    A += 0.2 * I # prevent numerical instability
    C = randn(ComplexF64, n, n)
    C = C * C' # now C is positive semidefinite
    t = -1 // 4

    B = HermitianSemidefinite(n, n)

    constraints = [
        # -trace_mpower is operator monotone for t ∈ [-1, 0].
        B ⪯ A,
    ]
    objective = trace_mpower(B, t, C)
    p = minimize(objective, constraints; numeric_type = T)

    handle_problem!(p)

    if test
        @test real.(B.value) ≈ real.(A) atol = atol rtol = rtol
        @test imag.(B.value) ≈ imag.(A) atol = atol rtol = rtol
        @test p.optval ≈ tr(C * A^t) atol = atol rtol = rtol
        @test p.optval ≈ trace_mpower(A, t, C) atol = atol rtol = rtol
        @test p.optval ≈ evaluate(trace_mpower(B, t, C)) atol = atol rtol = rtol
    end
end

@add_problem sdp function sdp_trace_logm_argcheck(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    if test
        @test_throws DimensionMismatch trace_logm(zeros(2, 3), zeros(2, 3))
        @test_throws DimensionMismatch trace_logm(Variable(2, 3), zeros(2, 3))

        @test_throws DimensionMismatch trace_logm(zeros(2, 2), zeros(3, 3))
        @test_throws DimensionMismatch trace_logm(Variable(2, 2), zeros(3, 3))

        nh = [1 1; 0 1] # not hermitian
        np = [1 2; 2 1] # not positive semidefinite
        @test_throws DomainError trace_logm(Variable(2, 2), nh)
        @test_throws DomainError trace_logm(Variable(2, 2), np)
    end
end

@add_problem sdp function sdp_trace_logm_real(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    n = 3
    C = randn(Float64, n, n)
    C = C * C' # now C is positive semidefinite

    X = Variable(n, n)

    constraints = [X ⪯ eye(n)]
    objective = trace_logm(X, C)
    p = maximize(objective, constraints; numeric_type = T)

    handle_problem!(p)

    if test
        @test real.(X.value) ≈ real.(eye(n)) atol = atol rtol = rtol
        @test imag.(X.value) ≈ imag.(eye(n)) atol = atol rtol = rtol
        @test p.optval ≈ tr(C * log(evaluate(X))) atol = atol rtol = rtol
        @test p.optval ≈ trace_logm(evaluate(X), C) atol = atol rtol = rtol
        @test p.optval ≈ evaluate(trace_logm(X, C)) atol = atol rtol = rtol
    end
end

@add_problem sdp function sdp_trace_logm_cplx(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    n = 3
    C = randn(ComplexF64, n, n)
    C = C * C' # now C is positive semidefinite

    X = ComplexVariable(n, n)

    constraints = [X ⪯ eye(n)]
    objective = trace_logm(X, C)
    p = maximize(objective, constraints; numeric_type = T)

    handle_problem!(p)

    if test
        @test real.(X.value) ≈ real.(eye(n)) atol = atol rtol = rtol
        @test imag.(X.value) ≈ imag.(eye(n)) atol = atol rtol = rtol
        @test p.optval ≈ tr(C * log(evaluate(X))) atol = atol rtol = rtol
        @test p.optval ≈ trace_logm(evaluate(X), C) atol = atol rtol = rtol
        @test p.optval ≈ evaluate(trace_logm(X, C)) atol = atol rtol = rtol
    end
end

@add_problem sdp function sdp_lieb_ando(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    if test
        @test_throws DomainError lieb_ando(
            zeros(3, 3),
            zeros(3, 3),
            eye(3),
            -3 // 2,
        )
        @test_throws DomainError lieb_ando(
            zeros(3, 3),
            zeros(3, 3),
            eye(3),
            5 // 2,
        )
        @test_throws DomainError lieb_ando(
            zeros(3, 3),
            Variable(3, 3),
            eye(3),
            -3 // 2,
        )
        @test_throws DomainError lieb_ando(
            zeros(3, 3),
            Variable(3, 3),
            eye(3),
            5 // 2,
        )
        @test_throws DomainError lieb_ando(
            Variable(3, 3),
            zeros(3, 3),
            eye(3),
            -3 // 2,
        )
        @test_throws DomainError lieb_ando(
            Variable(3, 3),
            zeros(3, 3),
            eye(3),
            5 // 2,
        )
        @test_throws DomainError lieb_ando(
            Variable(3, 3),
            Variable(3, 3),
            eye(3),
            -3 // 2,
        )
        @test_throws DomainError lieb_ando(
            Variable(3, 3),
            Variable(3, 3),
            eye(3),
            5 // 2,
        )
    end

    for n in [2, 3]
        for t in [1 // 2, 1 // 4, 3 // 4, 1 // 8, 3 // 2, 5 // 4]
            for cplx in [false, true]
                #@show n,t,cplx

                if cplx
                    A = randn(ComplexF64, n, n)
                    B = randn(ComplexF64, n, n)
                    X = ComplexVariable(n, n)
                    Y = ComplexVariable(n, n)
                else
                    A = randn(n, n)
                    B = randn(n, n)
                    X = Variable(n, n)
                    Y = Variable(n, n)
                end
                A = A * A'
                B = B * B'
                A = A / tr(A)
                B = B / tr(B)

                QtAB = lieb_ando(A, B, eye(n), t)

                objective = lieb_ando(X, B, eye(n), t)
                if t >= 0 && t <= 1
                    p = maximize(objective, [X == A]; numeric_type = T)
                else
                    p = minimize(objective, [X == A]; numeric_type = T)
                end
                handle_problem!(p)
                if test
                    @test p.optval ≈ QtAB atol = atol * 5 rtol = rtol
                end

                objective = lieb_ando(A, Y, eye(n), t)
                if t >= 0 && t <= 1
                    p = maximize(objective, [Y == B]; numeric_type = T)
                else
                    p = minimize(objective, [Y == B]; numeric_type = T)
                end
                handle_problem!(p)
                if test
                    @test p.optval ≈ QtAB atol = atol * 5 rtol = rtol
                end

                objective = lieb_ando(X, Y, eye(n), t)
                if t >= 0 && t <= 1
                    p = maximize(objective, [X == A, Y == B]; numeric_type = T)
                else
                    p = minimize(objective, [X == A, Y == B]; numeric_type = T)
                end
                handle_problem!(p)
                if test
                    @test p.optval ≈ QtAB atol = atol * 5 rtol = rtol
                end
            end
        end
    end
end

@add_problem sdp function sdp_min_maxeig_canon_lmi(
    handle_problem!,
    ::Val{test},
    atol,
    rtol,
    ::Type{T},
) where {T,test}
    # Minimize the maximum eigenvalue of an affine matrix function of a vector argument. Formulated
    # as a linear optimization subject to a canonical LMI (SDP) constraint:
    #
    # minimize      λ
    # subject to    A(x) ⪯ λ*I
    # where x is a vector in Rⁿ and the matrix function A(x) = A₀ + A₁x₁ + A₂x₂ + … + Aₙxₙ.
    #
    # Besides serving as a benchmark, it also tests the solution of the Issue
    # https://github.com/jump-dev/Convex.jl/issues/447
    # (the issues boils down to  evaluation of `A₁x₁`, because `x₁` was a matrix of size (1,1)).

    A₀ = [1.0 2.0; 2.0 3.0]
    A₁ = [1.0 0.0; 0.0 -1.0]
    A₂ = [4.0 5.0; 5.0 -6.0]

    x = Variable(2)
    λ = Variable()

    A = A₀ + A₁ * x[1] + A₂ * x[2]

    p = minimize(λ, A ⪯ λ * eye(2); numeric_type = T)

    handle_problem!(p)

    if test
        @test λ.value ≈ 2.400000051025101 atol = atol rtol = rtol
        @test x.value ≈ [3.0000000535867315, -0.4000000018585541] atol = atol rtol =
            rtol
        @test evaluate(A) ≈ [
            2.400000046152515 -9.292770553059881e-9
            -9.292770553059881e-9 2.399999957564593
        ] atol = atol rtol = rtol
    end
end


abstract type DiscreteCountingProcess <: Sampleable{Multivariate, Discrete} end

struct BernouliProcess<:DiscreteCountingProcess
    n::Int64
    p::Float64
    # domain::Tuple

    inter_arrival_distribution::Distribution
    success_distribution::Distribution

    function BernoulliProcess(n::Int64, p::Float64)
        new(n,p, Geometric(p), Bernoulli(p))
    end
end

rand(bp::BernouliProcess) = rand(bp.inter_arrival_distribution)
rand(bp::BernouliProcess, t::Int64) = rand(bp.success_distribution)
params(bp::BernouliProcess) = (bp.n, bp.p)


module Performance

using StandardizedMatrices, BenchmarkTools, StatsBase

n, p = 1000, 1000
x = randn(n, p)
x2 = zscore(x, 1)
z = StandardizedMatrix(x)
β = randn(p)
isapprox(x2 * β, z * β) ?
	info("Dense Matrix-Vector multiplication correct") :
	warn("Dense Matrix-Vector multiplication incorrect")
x = sprandn(n, p, .01)
x2 = zscore(x, 1)
z = StandardizedMatrix(x)
isapprox(x2 * β, z * β) ?
	info("Sparse Matrix-Vector multiplication correct") :
	warn("Sparse Matrix-Vector multiplication incorrect")

info("Dense A_mul_B! timing ratio")
b1 = @benchmark(
	A_mul_B!(storage, x, β),
	setup = (storage = zeros(n); x = StandardizedMatrix(randn(n, p)); β = randn(p))
)
b2 = @benchmark(
	A_mul_B!(storage, x, β),
	setup = (storage = zeros(n); x = randn(n, p); β = randn(p))
)
@show ratio(minimum(b1), minimum(b2))

info("Sparse A_mul_B! timing ratio")
b1 = @benchmark(
	A_mul_B!(storage, x, β),
	setup = (storage = zeros(n); x = StandardizedMatrix(sprandn(n, p, .05)); β = randn(p))
)
b2 = @benchmark(
	A_mul_B!(storage, x, β),
	setup = (storage = zeros(n); x = zscore(sprandn(n, p, .05), 1); β = randn(p))
)
@show ratio(minimum(b1), minimum(b2))

end


import Base: rand,
             ∈,
             isempty

export Line,
       an_element

"""
    Line{N<:Real, VN<:AbstractVector{N}} <: AbstractPolyhedron{N}

Type that represents a line in 2D of the form ``a⋅x = b`` (i.e., a special case
of a `Hyperplane`).

### Fields

- `a` -- normal direction (non-zero)
- `b` -- constraint

### Examples

The line ``y = -x + 1``:

```jldoctest
julia> Line([1., 1.], 1.)
Line{Float64,Array{Float64,1}}([1.0, 1.0], 1.0)
```
"""
struct Line{N<:Real, VN<:AbstractVector{N}} <: AbstractPolyhedron{N}
    a::VN
    b::N

    # default constructor with length constraint
    function Line(a::VN, b::N) where {N<:Real, VN<:AbstractVector{N}}
        @assert length(a) == 2 "lines must be two-dimensional"
        @assert !iszero(a) "a line needs a non-zero normal vector"
        return new{N, VN}(a, b)
    end
end

isoperationtype(::Type{<:Line}) = false
isconvextype(::Type{<:Line}) = true

# constructor from a LinearConstraint
Line(c::LinearConstraint{N}) where {N<:Real} = Line(c.a, c.b)

"""
    Line(p::AbstractVector{N}, q::AbstractVector{N}) where {N<:Real}

Constructor a line give two points.

### Input

- `p` -- point in 2D
- `q` -- another point in 2D

### Output

The line which passes through `p` and `q`.

### Algorithm

Given two points ``p = (x₁, y₁)`` and ``q = (x₂, y₂)`` the line that passes through these
two points is

```math
ℓ:~~y - y₁ = \\dfrac{(y₂ - y₁)}{(x₂ - x₁)} ⋅ (x-x₁).
```
The particular case ``x₂ = x₁`` defines a line parallel to the ``y``-axis (vertical line).
"""
function Line(p::AbstractVector{N}, q::AbstractVector{N}) where {N<:Real}
    x₁, y₁ = p[1], p[2]
    x₂, y₂ = q[1], q[2]

    if x₁ == x₂  # line is vertical
        a = [one(N), zero(N)]
        b = x₁
        return LazySets.Line(a, b)
    end

    k = (y₁ - y₂)/(x₂ - x₁)
    a = [k, one(N)]
    b = y₁ + k * x₁
    return Line(a, b)
end

# --- polyhedron interface functions ---

"""
    constraints_list(L::Line{N}) where {N<:Real}

Return the list of constraints of a line.

### Input

- `L` -- line

### Output

A list containing two half-spaces.
"""
function constraints_list(L::Line{N}) where {N<:Real}
    return _constraints_list_hyperplane(L.a, L.b)
end

# --- LazySet interface functions ---

"""
    dim(L::Line)

Return the ambient dimension of a line.

### Input

- `L` -- line

### Output

The ambient dimension of the line, which is 2.
"""
function dim(L::Line)
    return 2
end

"""
    σ(d::AbstractVector{N}, L::Line{N}) where {N<:Real}

Return the support vector of a line in a given direction.

### Input

- `d` -- direction
- `L` -- line

### Output

The support vector in the given direction, which is defined the same way as for
the more general `Hyperplane`.
"""
function σ(d::AbstractVector{N}, L::Line{N}) where {N<:Real}
    return σ(d, Hyperplane(L.a, L.b))
end

"""
    isbounded(L::Line)

Determine whether a line is bounded.

### Input

- `L` -- line

### Output

`false`.
"""
function isbounded(::Line)
    return false
end

"""
    isuniversal(L::Line{N}, [witness]::Bool=false) where {N<:Real}

Check whether a line is universal.

### Input

- `P`       -- line
- `witness` -- (optional, default: `false`) compute a witness if activated

### Output

* If `witness` option is deactivated: `false`
* If `witness` option is activated: `(false, v)` where ``v ∉ P``

### Algorithm

Witness production falls back to `isuniversal(::Hyperplane)`.
"""
function isuniversal(L::Line{N}, witness::Bool=false) where {N<:Real}
    if witness
        return isuniversal(Hyperplane(L.a, L.b), true)
    else
        return false
    end
end

"""
    an_element(L::Line{N}) where {N<:Real}

Return some element of a line.

### Input

- `L` -- line

### Output

An element on the line.

### Algorithm

If the ``b`` value of the line is zero, the result is the origin.
Otherwise the result is some ``x = [x1, x2]`` such that ``a·[x1, x2] = b``.
We first find out in which dimension ``a`` is nonzero, say, dimension 1, and
then choose ``x1 = 1`` and accordingly ``x2 = \\frac{b - a1}{a2}``.
"""
function an_element(L::Line{N}) where {N<:Real}
    if L.b == zero(N)
        return zeros(N, 2)
    end
    i = L.a[1] == zero(N) ? 2 : 1
    x = Vector{N}(undef, 2)
    x[3-i] = one(N)
    x[i] = (L.b - L.a[3-i]) / L.a[i]
    return x
end

"""
    ∈(x::AbstractVector{N}, L::Line{N}) where {N<:Real}

Check whether a given point is contained in a line.

### Input

- `x` -- point/vector
- `L` -- line

### Output

`true` iff `x ∈ L`.

### Algorithm

The point ``x`` belongs to the line if and only if ``a⋅x = b`` holds.
"""
function ∈(x::AbstractVector{N}, L::Line{N}) where {N<:Real}
    @assert length(x) == dim(L)
    return dot(L.a, x) == L.b
end

"""
    rand(::Type{Line}; [N]::Type{<:Real}=Float64, [dim]::Int=2,
         [rng]::AbstractRNG=GLOBAL_RNG, [seed]::Union{Int, Nothing}=nothing)

Create a random line.

### Input

- `Line` -- type for dispatch
- `N`    -- (optional, default: `Float64`) numeric type
- `dim`  -- (optional, default: 2) dimension
- `rng`  -- (optional, default: `GLOBAL_RNG`) random number generator
- `seed` -- (optional, default: `nothing`) seed for reseeding

### Output

A random line.

### Algorithm

All numbers are normally distributed with mean 0 and standard deviation 1.
Additionally, the constraint `a` is nonzero.
"""
function rand(::Type{Line};
              N::Type{<:Real}=Float64,
              dim::Int=2,
              rng::AbstractRNG=GLOBAL_RNG,
              seed::Union{Int, Nothing}=nothing)
    @assert dim == 2 "cannot create a random Line of dimension $dim"
    rng = reseed(rng, seed)
    a = randn(rng, N, dim)
    while iszero(a)
        a = randn(rng, N, dim)
    end
    b = randn(rng, N)
    return Line(a, b)
end

"""
    isempty(L::Line)

Return if a line is empty or not.

### Input

- `L` -- line

### Output

`false`.
"""
function isempty(L::Line)
    return false
end

"""
    constrained_dimensions(L::Line{N}) where {N<:Real}

Return the indices in which a line is constrained.

### Input

- `L` -- line

### Output

A vector of ascending indices `i` such that the line is constrained in dimension
`i`.

### Examples

A line with constraint ``x1 = 0`` is constrained in dimension 1 only.
"""
function constrained_dimensions(L::Line{N}) where {N<:Real}
    return nonzero_indices(L.a)
end

function _linear_map_hrep_helper(M::AbstractMatrix{N}, P::Line{N},
                                 algo::AbstractLinearMapAlgorithm) where {N<:Real}
    constraints = _linear_map_hrep(M, P, algo)
    if length(constraints) == 2
        # assuming these constraints define a line
        c = first(constraints)
        return Line(c.a, c.b)
    elseif isempty(constraints)
        return Universe{N}(size(M, 1))
    else
        error("unexpected number of $(length(constraints)) constraints")
    end
end

"""
    translate(L::Line{N}, v::AbstractVector{N}; share::Bool=false
             ) where {N<:Real}

Translate (i.e., shift) a line by a given vector.

### Input

- `L`     -- line
- `v`     -- translation vector
- `share` -- (optional, default: `false`) flag for sharing unmodified parts of
             the original set representation

### Output

A translated line.

### Notes

The normal vector of the line (vector `a` in `a⋅x = b`) is shared with the
original line if `share == true`.

### Algorithm

A line ``a⋅x = b`` is transformed to the line ``a⋅x = b + a⋅v``.
In other words, we add the dot product ``a⋅v`` to ``b``.
"""
function translate(L::Line{N}, v::AbstractVector{N}; share::Bool=false
                  ) where {N<:Real}
    @assert length(v) == dim(L) "cannot translate a $(dim(L))-dimensional " *
                                "set by a $(length(v))-dimensional vector"
    a = share ? L.a : copy(L.a)
    b = L.b + dot(L.a, v)
    return Line(a, b)
end


cd(@__DIR__); include("setups/rgc100.jl");

##
ϵ = 0.3
pair_inds, 𝛟1 = find_pairinds(W; ϵ = ϵ)

## (a) fiedler vector on RGC100
gr(dpi = 200)
plt = gplot(W, X; width = 1)
    scatter_gplot!(X; marker = 𝛟1, plotOrder = :l2s)
    plot!(xlims = [-180, 220], xlabel = "x(μm)", ylabel = "y(μm)", frame = :box,
    size = (520, 500), right_margin = 5mm)
savefig(plt, "../figs/RGC100_fiedler.png")

## (b) fiedler embedding
pyplot(dpi = 200)
plt = plot(size = (520, 150))
    plot!(-0.055:0.11:0.055, zeros(2), c = :black, lw = 2, yticks = false,
    xlab = latexstring("\\phi^{rw}_1(v)"))
    scatter!(𝛟1, 0:0, ylim = [-0.2, 0.2], grid = false,
    ms = 4, mswidth = 0, legend = false, frame = :box, c = :black)
savefig(plt, "../figs/RGC100_fiedler_embedding.png")


__precompile__()

module InvariantCausal

    export causalSearch, screen_lasso, screen_HOLP, two_sample_chow, sukhatme_fisher_test, conditional_inv_test_chow, conditional_inv_test_logistic,
           GaussianSEM, simulate, causes, cov, random_gaussian_SEM, random_noise_intervened_SEM

    include("conditionalInvTests.jl")
    include("causalSearch.jl")
    include("SEM.jl")
    include("screening.jl")

    function _test_full()
        include(joinpath(@__DIR__, "..", "test", "test_full.jl"))
    end

end


### A Pluto.jl notebook ###
# v0.12.10

using Markdown
using InteractiveUtils

# ╔═╡ 42cb734c-2b47-11eb-12de-6d2181461d88
begin
	using LinearAlgebra
	using BioSequences
	using Plots
	using Random
	using Statistics
end

# ╔═╡ 52fdcf9e-2b47-11eb-3710-6f1ef98c4e51
const KERNEL = let
	equations = []

	# Constrain one: All scaled frequencies sum to zero
	push!(equations, ones(Int, 256))

	# Constrain two: sum(xABC) == sum(ABCx)
	for base in 0:63
		equation = zeros(Int, 256)
		for affix in 0:3
			equation[base << 2 + affix + 1] += 1
			equation[affix << 6 + base + 1] -= 1
		end
		push!(equations, equation)
	end

	# Return the nullspace
	nullspace(Matrix(transpose(reduce(hcat, equations))))
end;

# ╔═╡ fea5f9c0-2b47-11eb-268b-03414fc4887e
"Use the kernel to project kmer freq matrix to orthonormal matrix"
function project_kmer_freq(freq::AbstractMatrix{<:Number})
	mean = sum(freq, dims=2) / size(freq, 2)
	shifted = freq .- mean
	return shifted * KERNEL
end;

# ╔═╡ 7e06120e-2b48-11eb-10fa-5765d37c680a
"Generate kmer spectrum from random nucleotides"
function random_kmer_spectrum(len::Integer, N::Integer)
	seq = LongSequence{DNAAlphabet{2}}(len)
	factor = 1 / (length(seq) - 3)
	counts = zeros(Int, 256)
	result = zeros(Float64, (N, 256))

	for row in 1:N
		rand!(seq.data)
		fill!(counts, zero(eltype(counts)))

		@inbounds for mer in each(DNAMer{4}, seq)
			index = reinterpret(Int, mer.fw) + 1
			counts[index] += 1
		end

		@inbounds for i in eachindex(counts)
			result[row, i] = counts[i] * factor
		end
	end

	return result
end

# ╔═╡ 9609e20c-2b4a-11eb-0f01-016bc09728d0
"Obs x features matrix"
function auto_correlation(matrix::AbstractMatrix)
	correlation = cor(matrix, dims=1)

	# Zero out correlation with a feature and itself, so as not
	# to saturate the heatplots below needlessly
	for i in 1:size(correlation, 1)
		correlation[i, i] = 0
	end
	correlation
end

# ╔═╡ 605618d0-2b4d-11eb-1bd3-ad2a83c52183
begin
	spectrum = random_kmer_spectrum(2_000, 10_000)
	orthonormal = project_kmer_freq(spectrum)
	corr_spectrum = auto_correlation(spectrum)
	corr_orthonormal = auto_correlation(orthonormal)

	mn = min(minimum(corr_spectrum), minimum(corr_orthonormal))
	mx = max(maximum(corr_spectrum), maximum(corr_orthonormal))
	clim = (mn, mx)
end;

# ╔═╡ d424aef0-2b4a-11eb-1e20-d93067c6038a
heatmap(corr_spectrum, clim=clim)

# ╔═╡ 70b600e0-2b49-11eb-1229-5fc87c84fc1e
heatmap(corr_orthonormal, clim=clim)

# ╔═╡ Cell order:
# ╠═42cb734c-2b47-11eb-12de-6d2181461d88
# ╠═52fdcf9e-2b47-11eb-3710-6f1ef98c4e51
# ╠═fea5f9c0-2b47-11eb-268b-03414fc4887e
# ╠═7e06120e-2b48-11eb-10fa-5765d37c680a
# ╠═9609e20c-2b4a-11eb-0f01-016bc09728d0
# ╠═605618d0-2b4d-11eb-1bd3-ad2a83c52183
# ╠═d424aef0-2b4a-11eb-1e20-d93067c6038a
# ╠═70b600e0-2b49-11eb-1229-5fc87c84fc1e


module SingularSpectrumAnalysis

using LinearAlgebra, Statistics, RecipesBase, Requires

export hankel, hankelize, elementary, reconstruct, hsvd, autogroup, analyze

export sigmaplot, pairplot

export PredictionData, trendorder, fit_trend, pred, trend, seasons

@recipe function sigmaplot(USV::SVD; cumulative=false)
    seriestype := :scatter
    title --> "Normalized Singular Value Plot"
    ylabel --> "\$\\sigma / \\sum\\sigma_i\$"
    S = USV.S./sum(USV.S)
    cumulative ? cumsum(S) : S
end

@userplot Pairplot
"""
pairplot(USV, groupings)
Usage:
```julia
USV = hsvd(data,L)
seasonal_groupings = [1:2, 4:5]
pairplot(USV,seasonal_groupings)
```
"""
pairplot

@recipe function pairplot(h::Pairplot)
    USV, groupings = h.args[1:2]
    M = length(groupings)
    layout := M
    legend --> false
    for m = 1:M
        i = groupings[m]
        @assert length(i) == 2 "pairplot: All groupings have to be pairs"
        elements = USV.U[:,i].*sqrt.(USV.S[i])'
        subplot := m
        @series elements[:,1], elements[:,2]
    end
    nothing
end

"""
X = hankel(x,window_size)
Form the trajectory matrix `X` of size KxL, K = N-L+1
x can be a vector or a matrix For multivariate SSA
"""
function hankel(x,L)
    N = size(x,1)
    D = isa(x,AbstractVector) ? 1 : size(x,2)
    @assert L <= N/2 "L has to be less than N/2 = $(N/2)"
    K = N-L+1
    X = zeros(K,L*D)
    for d = 1:D
        for j = 1:L, i = 1:K
            k = i+j-1
            X[i,j+(d-1)*L] = x[k,d]
        end
    end
    X
end

"""
Ui = elementary(USV,I)
Computes the sum Uᵢ*Sᵢ*Vᵢ' for all i in I
If I is 1:L, the returned matrix is identical to U*S*V'
"""
function elementary(USV,I)
    sum(USV.U[:,i]*USV.S[i]*USV.Vt[i,:]' for i in I)
end

"""
means = hankelize(X)
Computes a timeseries from an approximate Hankel matrix by diagoal averaging (Hankelization). Note that the returned value is a vector and not a hankel matrix. A hankel matrix is easily obtainable by `hankel(hankelize(X), size(X,2))`
"""
function hankelize(X)
    K,L = size(X)
    @assert K >= L "The input matrix must be a tall matrix"
    N = K+L-1
    means = zeros(N)
    for n = 1:N
        if n <= K
            rangei = n:-1:max(1,n-L+1)
            rangej = 1:min(n,L)
        else
            rangei = K:-1:max(n-L+1,n-K+2)
            rangej = n-K+1:L
        end
        means[n] = mean(X[rangei[k],rangej[k]] for k in eachindex(rangei))
    end
    means
end
"""
USV = hsvd(y,L)
Form a trajectory hankel matrix from data `y` and compute svd on this Hankelmatrix
"""
function hsvd(y,L)
    X = hankel(y,L) # Form trajectory matrix
    USV = svd(X)
end

"""
yrt,yrs = reconstruct(USV::SVD, trends, seasonal::AbstractArray)
yr      = reconstruct(USV::SVD, groupings::AbstractArray)

Compute a reconstruction of the time-series based on an SVD object obtained from `hsvd` and user selected groupings. See also `?SingularSpectrumAnalysis`
"""
function reconstruct(USV, trends, seasonal::AbstractArray)
    if isa(trends,Number)
        trends = [trends]
    end
    yrt = reconstruct(USV, trends)
    yrs = reconstruct(USV, seasonal)
    size(yrt,2) == 1 && (yrt = vec(yrt))
    yrt, yrs
end

function reconstruct(USV, groupings::AbstractArray)
    M = length(groupings)
    K,L = size(USV.U)
    N = K+L-1
    yr = zeros(N,M)
    for m = 1:M
        X = elementary(USV,groupings[m])
        yr[:,m] = hankelize(X)
    end
    yr
end

"""
trend, seasonal_groupings = autogroup(USV, th = 0.95)
Try to automatically group singular values. `th ∈ (0,1)` determins the percentage of variance to explain.
"""
function autogroup(USV, th = 0.95)
    nS = USV.S .- minimum(USV.S)
    nS ./= sum(nS)
    cs = cumsum(nS)
    ind = findfirst(x->x > th, cs)
    iseven(ind) && (ind -= 1)
    t = findtrend(nS[1:ind])
    seasonal_groupings = UnitRange[]
    for i = [1:2:t-1; t+1:2:ind]
        push!(seasonal_groupings, i:i+1)
    end
    t, seasonal_groupings
end

function findtrend(S)
    function eval_trend(ind)
        iseven(ind) && return Inf
        cost = 0.0
        for i = [1:2:ind-1; ind+1:2:length(S)]
            cost += S[i] - S[i+1]
        end
        cost
    end
    costs = eval_trend.(1:length(S))
    _,ind = findmin(costs)
    ind
end

function analyze(y,L)
    USV = hsvd(y,L)
    trend, seasonal_groupings = autogroup(USV)
    reconstruct(USV, trend, seasonal_groupings)
end

mutable struct PredictionData
    trend_parameters
    trend_regressor
    seasonal_models
    seasonal_predictions
end
trendorder(pd) = length(pd.trend_parameters)-1

trend(pd::PredictionData) = pd.trend_regressor*pd.trend_parameters
trend(pd::PredictionData, i) = pd.trend_regressor[i,:]'*pd.trend_parameters
seasons(pd::PredictionData) = pd.seasonal_predictions
seasons(pd::PredictionData,i) = getindex.(pd.seasonal_predictions,i)
Base.getindex(pd::PredictionData) = trend(pd,i), seasons(pd,i)

@recipe function plotpd(pd::PredictionData)
    layout := 1+length(pd.seasonal_models)
    legend --> false
    @series begin
        subplot := 1
        title --> "Estimated trend"
        pd.trend_regressor*pd.trend_parameters
    end
    for (i,s) in enumerate(pd.seasonal_predictions)
        @series begin
            subplot := 1+i
            title --> "Seasonal predictions $i"
            s
        end
    end
end

"""
A,x = fit_trend(yt::AbstractVector, order=1)
Fit an n:th order polynomial to the trend signal `yt`
Returns the regressor matrix `A` and the coefficients `x` with the first coefficient being the 0:th order component (mean).
"""
function fit_trend(yt::AbstractVector, order=1)
    N = length(yt)
    A = (1:N).^(0:order)'
    x = A\yt
    A,x
end

function __init__()
    @require ControlSystemIdentification="3abffc1c-5106-53b7-b354-a47bfc086282" begin
    CSI = ControlSystemIdentification

    function PredictionData(yt,ys; trend_order=1, ar_order=2)
        A,x = fit_trend(yt, trend_order)  # Fits a polynomial of order 1 (line)
        ns = size(ys,2) # number of seasonal components
        models = [CSI.ar(1, ys[:,i], ar_order)[1] for i = 1:ns] # Fit one model per component
        # We can use these models to for one-step predictions
        seasonal_predictions = [CSI.predict(models[i], ys[:,i])  for i = 1:ns]
        # for s in seasonal_predictions
        #     for i = 1:ar_order insert!(s,1,0.) end
        # end
        PredictionData(x,A,models,seasonal_predictions)
    end

    function _pred(pd::PredictionData)
        A,x,ys,models = pd.trend_regressor, pd.trend_parameters, pd.seasonal_predictions, pd.seasonal_models
        @assert size(A,1) >= 3
        @assert (ys isa AbstractVecOrMat{<:Number} && size(ys,1) >= 3) || size(ys[1],1) >= 3
        ns = length(models)
        ysh = [CSI.predict(models[i], ys[i][end-2:end])  for i = 1:ns]
        ysh = reduce(hcat, ysh)
        yth = A[end,:]'*x
        yth, ysh
    end

    """
    pd = pred(pd::PredictionData, n=1)
    Form `n` step prediction. This requires you to have constructed a `PredictionData` object according to
    ```julia
    yt, ys = analyze(yn, L) # trend and seasons
    pd = fit_trend(yt, nt)  # Fits a polynomial of order nt (nt = 1 -> line)
    na = 2 # Autoregressive order
    # Next line fits one AR model for each seasonal component
    ns = size(ys,2) # number of seasonal components
    using ControlSystemIdentification
    models = [ar(1, ys[:,i], na)[1] for i = 1:ns] # Fit one model per component
    pd.seasonal_models = models
    seasonal_predictions = [predict(models[i], ys[:,i])  for i = 1:ns]
    pd.seasonal_predictions = seasonal_predictions
    ```
    """
    function pred(pd::PredictionData, n=1)
        n == 0 && (return pd)
        A,x,ys,models = pd.trend_regressor, pd.trend_parameters, pd.seasonal_predictions, pd.seasonal_models
        yth, ysh = _pred(pd)
        pd.trend_regressor = [A; (size(A,1)+1).^(0:trendorder(pd))']
        for i = eachindex(ys)
            pd.seasonal_predictions[i] = [ys[i]; ysh[i]]
        end
        pred(pd,n-1)
    end
end
end
end


mutable struct Mesh
    t
    p
    e
end

function rect_grid2(xmin, xmax, ymin, ymax, nx, ny)
    nt = 2*nx*ny
    np = (nx+1)*(ny+1)

    mesh = Mesh(0, 0, 0)
    mesh.t = zeros(nt, 3)
    mesh.p = zeros(np, 2)

    nxp1 = nx + 1
    nyp1 = ny + 1

    nt  = 0
    for ix = 1:nx
       for iy = 1:ny

          iv  = (ix-1)*nyp1 + iy
          iv1 = iv + nyp1

          nt = nt + 1
          mesh.t[nt, 1] = iv
          mesh.t[nt, 2] = iv1
          mesh.t[nt, 3] = iv1+1

          nt = nt + 1
          mesh.t[nt, 1] = iv
          mesh.t[nt, 2] = iv1+1
          mesh.t[nt, 3] = iv+1
      end
    end

    hx   = (xmax-xmin)/nx
    hy   = (ymax-ymin)/ny
    x    = xmin

    for ix = 1:nx

      i1 = (ix-1)*(ny+1)+1
      i2 = ix*(ny+1)
      mesh.p[i1:i2, 1] = x*ones(nyp1, 1)
      mesh.p[i1:i2, 2] = collect(ymin:hy:ymax)

      x = x + hx
    end

    i1 = nx*(ny+1)+1
    i2 = (nx+1)*(ny+1)
    mesh.p[i1:i2, 1] = xmax*ones(nyp1,1)
    mesh.p[i1:i2, 2] = collect(ymin:hy:ymax)

    mesh.e = ones(2*(nx+ny), 3)

    mesh.e[1:ny, 1] = collect(1:ny)
    mesh.e[1:ny, 2] = collect(2:ny+1)

    mesh.e[ny+1:nx+ny, 1] = collect(ny+1:ny+1:np-1)
    mesh.e[ny+1:nx+ny, 2] = collect(2*(ny+1):ny+1:np)

    mesh.e[nx+ny+1:nx+2*ny, 1] = collect(np-ny:np-1)
    mesh.e[nx+ny+1:nx+2*ny, 2] = collect(np-ny+1:np)

    mesh.e[nx+2*ny+1:2*(nx+ny), 1] = collect(1:ny+1:np-2*ny-1)
    mesh.e[nx+2*ny+1:2*(nx+ny), 2] = collect(ny+2:ny+1:np-ny)
    return mesh
end

function Stiff_Mass(mesh)

    mesh.e[:, 3] .= 1

    N::Int = maximum(maximum(mesh.t[:, 1:3]))

    dirichlet = mesh.e[findall(x->x==1, mesh.e[:, 3]), 1:2]
    dirichlet = unique(sort(dirichlet[:]))
    FreeNodes = setdiff(1:N, dirichlet)

    (Dphi, area, _) = gradbasis(mesh.p, mesh.t)

    NT = size(mesh.t, 1)
    Mt = zeros(NT, 3, 3)
    At = zeros(NT, 3, 3)
    for i = 1:3
        for j = 1:3
            At[:, i, j] = (Dphi[:, 1, i].*Dphi[:, 1, j] + Dphi[:, 2, i].*Dphi[:, 2, j]).*area
            Mt[:, i, j] = area*((i==j)+1)/12
        end
    end

    #%% Assemble the mass matrix in Omega
    M = spzeros(N, N)
    A = spzeros(N, N)
    for i = 1:3
        krow = mesh.t[:, i]
        for j = 1:3
            kcol = mesh.t[:, j]
            M = M + sparse(Int64.(krow), Int64.(kcol), Mt[:, i, j], N, N)
            A = A + sparse(Int64.(krow), Int64.(kcol), At[:, i, j], N, N)
        end
    end
    return A, M, FreeNodes, dirichlet
end
#=
a=rect_grid2(1, 2, 1, 2, 10, 10)
Stiff_Mass(a)
=#

function gradbasis(node, elem)
    NT = size(elem, 1)
    elem = Int64.(elem)

    ve1 = node[elem[:, 3], :]-node[elem[:, 2], :]
    ve2 = node[elem[:, 1], :]-node[elem[:, 3], :]
    ve3 = node[elem[:, 2], :]-node[elem[:, 1], :]
    area = 0.5*(-ve3[:, 1].*ve2[:, 2] + ve3[:, 2].*ve2[:, 1])
    Dlambda = zeros(NT, 2, 3)
    Dlambda[1:NT, :, 3] = [-ve3[:, 2]./(2*area) ve3[:, 1]./(2*area)]
    Dlambda[1:NT, :, 1] = [-ve1[:, 2]./(2*area) ve1[:, 1]./(2*area)]
    Dlambda[1:NT, :, 2] = [-ve2[:, 2]./(2*area) ve2[:, 1]./(2*area)]

    idx = findall(x->x<0, area)
    #idx = (area<0)
    area[idx, :] = -area[idx, :]
    elemSign = ones(NT, 1)
    elemSign[idx] .= -1
    return Dlambda, area, elemSign
end

function f(p)
    f = (sin.(2*pi*p[:, 1]).*sin.(2*pi*p[:, 2]))
    return f
end
function exactu(p, s)
    u = (8*pi^2)^(-s)*sin.(2*pi*p[:, 1]).*sin.(2*pi*p[:, 2])
    return u
end
#s=0.2

function solve()

    for ind = 1:5
        mesh = rect_grid2(0, 1, 0, 1, 2^(ind+1), 2^(ind+1))

        (A, M, FreeNodes, dirichlet) = Stiff_Mass(mesh)

        N = size(A, 1)

        rhs = M*f(mesh.p)

        global u = zeros(N, 1)

        h = N^(-1/2)

        k = 1/log(1/h)

        Nplus  = ceil(pi^2/(4*s*k^2))
        Nminus = ceil(pi^2/(4*(1-s)*k^2))

        for ell=-Nminus:1:Nplus
            y = k*ell;
            u[FreeNodes] = u[FreeNodes] + exp((1-s)*y) * ( (exp(y)*M[FreeNodes, FreeNodes]+A[FreeNodes, FreeNodes]) \ rhs[FreeNodes])
        end

        u[FreeNodes] = (sin(s*pi)/pi) * k * u[FreeNodes]

        uD = zeros(N, 1)
        dirichlet = Int.(dirichlet)
        uD[dirichlet] = exactu(mesh.p[dirichlet, :], s)
        v = uD
        rhs = -A*uD
        v[FreeNodes] = A[FreeNodes, FreeNodes] \ rhs[FreeNodes]

        u = u+v

        #errL2[ind, 1] = sqrt((u-exactu(mesh.p, s))'*M*(u-exactu(mesh.p, s)))

        #hmax[ind, 1] = h
    end
    return u
end
#=
m = rect_grid2(0, 1, 0, 1, 2^(5+1), 2^(5+1))

using Plots
gr()
#pyplot()
#plotlyjs()
#pyplot()
plot3d(m.p[:, 1], m.p[:, 2], u)
=#


# ------------------------------------------------------------------------------------------------ #
#          Run a statistical test to compare the distributions of features in two matrices         #
# ------------------------------------------------------------------------------------------------ #
function statfeaturefilter(F₁, F₂; tail=:firstlarger, test=TailedTestOfVariance, alpha=0.05)

    p = testFeatureDistributions(F₁, F₂, test; tail=tail)
    issig = p .< alpha
    strs = deepcopy(p)
    strs[issig] = 10.0.^(ceil.(log10.(p[issig])))
    strs[.!issig] = round.(strs[.!issig], sigdigits=2)
    strs = string.(strs)
    strs[issig] = "<".*strs[issig]
    ann = [text("$(strs[i])", [:crimson, :black][issig[i]+1], 8, rotation=90, valign=:bottom) for i ∈ 1:length(p)] # :forestgreen
    plt = featureviolin(F₁, F₂, rightannotations=ann)
end