############################################################################################
# Constraints

############################################################################################
@testset "Constraints - Bijector" begin
    for output in outputtypes
        for flattentype in flattentypes
            flatdefault = FlattenDefault(; output = output, flattentype = flattentype)
            val = 2.
            constraint = Bijectors.bijector(Gamma(2,2))
            ReConstructor(constraint, val)
            reconstruct = ReConstructor(flatdefault, constraint, val)
# Flatten
            x_flat = flatten(reconstruct, val)
            @test x_flat isa AbstractVector
            @test eltype(x_flat) == output
# Flatten AD
            x_flatAD = flattenAD(reconstruct, val)
            @test x_flatAD isa AbstractVector
            @test eltype(x_flatAD) == eltype(val)
# Unflatten
            x_unflat = unflatten(reconstruct, x_flat)
            @test x_unflat isa typeof(val)
# Unflatten AD
            x_unflatAD = unflattenAD(reconstruct, x_flat)
            @test x_unflatAD isa eltype(x_flat)
            x_unflatAD2 = unflattenAD(reconstruct, x_flatAD)
            @test x_unflatAD2 isa eltype(x_flatAD)
################################################################################
# Transforms
            _transform, _inversetransform = construct_transform(constraint, val)
            x_transformed = _transform(val)
            x_inversetransformed = _inversetransform(x_transformed)

            transformer = TransformConstructor(constraint, val)
            val_uncon = unconstrain(transformer, val)
            val_con = constrain(transformer, val_uncon)

            @test transformer.unconstrain == _transform
            @test transformer.constrain == _inversetransform
            @test x_transformed == val_uncon
            @test x_inversetransformed == val_con

            @test x_inversetransformed isa typeof(val)
            val_unconstrained = unconstrain(_transform, val)
            val_constrained = constrain(_inversetransform, val_unconstrained)
            @test val_constrained isa typeof(val)
            @test val_constrained ≈ val atol = _TOL
            logabs = log_abs_det_jac(_transform, val)
            @test logabs isa AbstractFloat
            @test _check(_RNG, constraint, val)
        end
    end
end

############################################################################################
@testset "Constraints - Distribution" begin
    for output in outputtypes
        for flattentype in flattentypes
            flatdefault = FlattenDefault(; output = output, flattentype = flattentype)
            val = 2.
            constraint = Gamma(2,2)
            ReConstructor(constraint, val)
            reconstruct = ReConstructor(flatdefault, constraint, val)
# Flatten
            x_flat = flatten(reconstruct, val)
            @test x_flat isa AbstractVector
            @test eltype(x_flat) == output
# Flatten AD
            x_flatAD = flattenAD(reconstruct, val)
            @test x_flatAD isa AbstractVector
            @test eltype(x_flatAD) == eltype(val)
# Unflatten
            x_unflat = unflatten(reconstruct, x_flat)
            @test x_unflat isa typeof(val)
# Unflatten AD
            x_unflatAD = unflattenAD(reconstruct, x_flat)
            @test x_unflatAD isa eltype(x_flat)
            x_unflatAD2 = unflattenAD(reconstruct, x_flatAD)
            @test x_unflatAD2 isa eltype(x_flatAD)
################################################################################
# Transforms
            _transform, _inversetransform = construct_transform(constraint, val)
            x_transformed = _transform(val)
            x_inversetransformed = _inversetransform(x_transformed)

            transformer = TransformConstructor(constraint, val)
            val_uncon = unconstrain(transformer, val)
            val_con = constrain(transformer, val_uncon)

            @test transformer.unconstrain == _transform
            @test transformer.constrain == _inversetransform
            @test x_transformed == val_uncon
            @test x_inversetransformed == val_con

            @test x_inversetransformed isa typeof(val)
            val_unconstrained = unconstrain(_transform, val)
            val_constrained = constrain(_inversetransform, val_unconstrained)
            @test val_constrained isa typeof(val)
            @test val_constrained ≈ val atol = _TOL
            logabs = log_abs_det_jac(_transform, val)
            @test logabs isa AbstractFloat
            @test _check(_RNG, constraint, val)
        end
    end
end

############################################################################################
@testset "Constraints - Constrained" begin
    for output in outputtypes
        for flattentype in flattentypes
            flatdefault = FlattenDefault(; output = output, flattentype = flattentype)
            val = 2.
            constraint = Constrained(1.,3.)
            ReConstructor(constraint, val)
            reconstruct = ReConstructor(flatdefault, constraint, val)
# Flatten
            x_flat = flatten(reconstruct, val)
            @test x_flat isa AbstractVector
            @test eltype(x_flat) == output
# Flatten AD
            x_flatAD = flattenAD(reconstruct, val)
            @test x_flatAD isa AbstractVector
            @test eltype(x_flatAD) == eltype(val)
# Unflatten
            x_unflat = unflatten(reconstruct, x_flat)
            @test x_unflat isa typeof(val)
# Unflatten AD
            x_unflatAD = unflattenAD(reconstruct, x_flat)
            @test x_unflatAD isa eltype(x_flat)
            x_unflatAD2 = unflattenAD(reconstruct, x_flatAD)
            @test x_unflatAD2 isa eltype(x_flatAD)
################################################################################
# Transforms
            _transform, _inversetransform = construct_transform(constraint, val)
            x_transformed = _transform(val)
            x_inversetransformed = _inversetransform(x_transformed)

            transformer = TransformConstructor(constraint, val)
            val_uncon = unconstrain(transformer, val)
            val_con = constrain(transformer, val_uncon)

            @test transformer.unconstrain == _transform
            @test transformer.constrain == _inversetransform
            @test x_transformed == val_uncon
            @test x_inversetransformed == val_con

            @test x_inversetransformed isa typeof(val)
            val_unconstrained = unconstrain(_transform, val)
            val_constrained = constrain(_inversetransform, val_unconstrained)
            @test val_constrained isa typeof(val)
            @test val_constrained ≈ val atol = _TOL
            logabs = log_abs_det_jac(_transform, val)
            @test logabs isa AbstractFloat
            @test _check(_RNG, constraint, val)
        end
    end
end

############################################################################################
@testset "Constraints - Unconstrained" begin
    for output in outputtypes
        for flattentype in flattentypes
            flatdefault = FlattenDefault(; output = output, flattentype = flattentype)
            val = 2.
            constraint = Unconstrained()
            ReConstructor(constraint, val)
            reconstruct = ReConstructor(flatdefault, constraint, val)
# Flatten
            x_flat = flatten(reconstruct, val)
            @test x_flat isa AbstractVector
            @test eltype(x_flat) == output
# Flatten AD
            x_flatAD = flattenAD(reconstruct, val)
            @test x_flatAD isa AbstractVector
            @test eltype(x_flatAD) == eltype(val)
# Unflatten
            x_unflat = unflatten(reconstruct, x_flat)
            @test x_unflat isa typeof(val)
# Unflatten AD
            x_unflatAD = unflattenAD(reconstruct, x_flat)
            @test x_unflatAD isa eltype(x_flat)
            x_unflatAD2 = unflattenAD(reconstruct, x_flatAD)
            @test x_unflatAD2 isa eltype(x_flatAD)
################################################################################
# Transforms
            _transform, _inversetransform = construct_transform(constraint, val)
            x_transformed = _transform(val)
            x_inversetransformed = _inversetransform(x_transformed)

            transformer = TransformConstructor(constraint, val)
            val_uncon = unconstrain(transformer, val)
            val_con = constrain(transformer, val_uncon)

            @test transformer.unconstrain == _transform
            @test transformer.constrain == _inversetransform
            @test x_transformed == val_uncon
            @test x_inversetransformed == val_con

            @test x_inversetransformed isa typeof(val)
            val_unconstrained = unconstrain(_transform, val)
            val_constrained = constrain(_inversetransform, val_unconstrained)
            @test val_constrained isa typeof(val)
            @test val_constrained ≈ val atol = _TOL
            logabs = log_abs_det_jac(_transform, val)
            @test logabs isa AbstractFloat
            @test _check(_RNG, constraint, val)
        end
    end
end

############################################################################################
@testset "Constraints - Fixed" begin
    for output in outputtypes
        for flattentype in flattentypes
            flatdefault = FlattenDefault(; output = output, flattentype = flattentype)
            val = zeros(2,3,4)
            constraint = Fixed()
            ReConstructor(constraint, val)
            reconstruct = ReConstructor(flatdefault, constraint, val)
# Flatten
            x_flat = flatten(reconstruct, val)
            @test x_flat isa AbstractVector
            @test eltype(x_flat) == output
# Flatten AD
            x_flatAD = flattenAD(reconstruct, val)
            @test x_flatAD isa AbstractVector
#            @test eltype(x_flatAD) == eltype(val)
# Unflatten
            x_unflat = unflatten(reconstruct, x_flat)
            @test x_unflat isa typeof(val)
# Unflatten AD
            x_unflatAD = unflattenAD(reconstruct, x_flat)
#            @test x_unflatAD isa eltype(x_flat)
            x_unflatAD2 = unflattenAD(reconstruct, x_flatAD)
#            @test x_unflatAD2 isa eltype(x_flatAD)
################################################################################
# Transforms
            _transform, _inversetransform = construct_transform(constraint, val)
            x_transformed = _transform(val)
            x_inversetransformed = _inversetransform(x_transformed)

            transformer = TransformConstructor(constraint, val)
            val_uncon = unconstrain(transformer, val)
            val_con = constrain(transformer, val_uncon)

            @test transformer.unconstrain == _transform
            @test transformer.constrain == _inversetransform
            @test x_transformed == val_uncon
            @test x_inversetransformed == val_con

            @test x_inversetransformed isa typeof(val)
            val_unconstrained = unconstrain(_transform, val)
            val_constrained = constrain(_inversetransform, val_unconstrained)
            @test val_constrained isa typeof(val)
            @test val_constrained ≈ val atol = _TOL
            logabs = log_abs_det_jac(_transform, val)
            @test logabs isa AbstractFloat
            @test _check(_RNG, constraint, val)
        end
    end
end

############################################################################################
@testset "Constraints - CorrelationMatrix" begin
    for output in outputtypes
        for flattentype in flattentypes
            flatdefault = FlattenDefault(; output = output, flattentype = flattentype)
            val = zeros(3,3)
            val[1,1] = val[2,2] = val[3,3] = 1.0
            val
            val[2,1] = val[1, 2] = 0.12
            val[3,1] = val[1, 3] = 0.13
            val[3,2] = val[2, 3] = 0.14
            val
            constraint = CorrelationMatrix()
            ReConstructor(constraint, val)
            reconstruct = ReConstructor(flatdefault, constraint, val)
# Flatten
            x_flat = flatten(reconstruct, val)
            @test x_flat isa AbstractVector
            @test eltype(x_flat) == output
            @test length(x_flat) == 3
            @test x_flat == output.([0.12, 0.13, 0.14])
# Flatten AD
            x_flatAD = flattenAD(reconstruct, val)
            @test x_flatAD isa AbstractVector
            @test eltype(x_flatAD) == eltype(val)
# Unflatten
            x_unflat = unflatten(reconstruct, x_flat)
            @test x_unflat isa typeof(val)
# Unflatten AD
            x_unflatAD = unflattenAD(reconstruct, x_flat)
            @test eltype(x_unflatAD) == eltype(x_flat)
            x_unflatAD2 = unflattenAD(reconstruct, x_flatAD)
            @test eltype(x_unflatAD2) == eltype(x_flatAD)

################################################################################
# Check if errors if flattened parameter not correct size
            @test_throws ArgumentError flatten(reconstruct, zeros(4,4))
            @test_throws ArgumentError flatten(reconstruct, zeros(2,2))
            @test_throws ArgumentError flattenAD(reconstruct, zeros(4,4))
            @test_throws ArgumentError flattenAD(reconstruct,  zeros(2,3))
            @test_throws ArgumentError unflatten(reconstruct, zeros(length(x_flat)+1))
            @test_throws ArgumentError unflatten(reconstruct, zeros(length(x_flat)-1))
            @test_throws ArgumentError unflattenAD(reconstruct, zeros(length(x_flat)+1))
            @test_throws ArgumentError unflattenAD(reconstruct, zeros(length(x_flat)-1))

################################################################################
# Transforms
            _transform, _inversetransform = construct_transform(constraint, val)
            x_transformed = _transform(val)
            x_inversetransformed = _inversetransform(x_transformed)

            transformer = TransformConstructor(constraint, val)
            val_uncon = unconstrain(transformer, val)
            val_con = constrain(transformer, val_uncon)

            @test transformer.unconstrain == _transform
            @test transformer.constrain == _inversetransform
            @test x_transformed == val_uncon
            @test x_inversetransformed == val_con

            val_unconstrained = unconstrain(_transform, val)
            #Upper triangular
            @test val_unconstrained[3,1] == 0.0
            val_constrained = constrain(_inversetransform, val_unconstrained)
            @test val_constrained isa typeof(val)
            @test val_constrained ≈ val atol = _TOL
            logabs = log_abs_det_jac(_transform, val)
            @test logabs isa AbstractFloat
            @test _check(_RNG, constraint, val)
        end
    end
end

############################################################################################
@testset "Constraints - CovarianceMatrix" begin
    for output in outputtypes
        for flattentype in flattentypes
            flatdefault = FlattenDefault(; output = output, flattentype = flattentype)
            val = zeros(3,3)
            val[1,1] = 1.5
            val[2,2] = 1.6
            val[3,3] = 1.7
            val
            val[2,1] = val[1, 2] = 0.12
            val[3,1] = val[1, 3] = 0.13
            val[3,2] = val[2, 3] = 0.14
            val
            constraint = CovarianceMatrix()
            ReConstructor(constraint, val)
            reconstruct = ReConstructor(flatdefault, constraint, val)
# Flatten
            x_flat = flatten(reconstruct, val)
            @test x_flat isa AbstractVector
            @test eltype(x_flat) == output
            @test length(x_flat) == 6
            @test x_flat == output.([1.50, 0.12, 0.13, 1.60, 0.14, 1.70])
# Flatten AD
            x_flatAD = flattenAD(reconstruct, val)
            @test x_flatAD isa AbstractVector
            @test eltype(x_flatAD) == eltype(val)
# Unflatten
            x_unflat = unflatten(reconstruct, x_flat)
            @test x_unflat isa typeof(val)
# Unflatten AD
            x_unflatAD = unflattenAD(reconstruct, x_flat)
            @test eltype(x_unflatAD) == eltype(x_flat)
            x_unflatAD2 = unflattenAD(reconstruct, x_flatAD)
            @test eltype(x_unflatAD2) == eltype(x_flatAD)

################################################################################
# Check if errors if flattened parameter not correct size
            @test_throws ArgumentError flatten(reconstruct, zeros(4,4))
            @test_throws ArgumentError flatten(reconstruct, zeros(2,2))
            @test_throws ArgumentError flattenAD(reconstruct, zeros(4,4))
            @test_throws ArgumentError flattenAD(reconstruct,  zeros(2,3))
            @test_throws ArgumentError unflatten(reconstruct, zeros(length(x_flat)+1))
            @test_throws ArgumentError unflatten(reconstruct, zeros(length(x_flat)-1))
            @test_throws ArgumentError unflattenAD(reconstruct, zeros(length(x_flat)+1))
            @test_throws ArgumentError unflattenAD(reconstruct, zeros(length(x_flat)-1))

################################################################################
# Transforms
            _transform, _inversetransform = construct_transform(constraint, val)
            x_transformed = _transform(val)
            x_inversetransformed = _inversetransform(x_transformed)

            transformer = TransformConstructor(constraint, val)
            val_uncon = unconstrain(transformer, val)
            val_con = constrain(transformer, val_uncon)

            @test transformer.unconstrain == _transform
            @test transformer.constrain == _inversetransform
            @test x_transformed == val_uncon
            @test x_inversetransformed == val_con

            val_unconstrained = unconstrain(_transform, val)
            #Lower triangular
            @test val_unconstrained[1,3] == 0.0
            val_constrained = constrain(_inversetransform, val_unconstrained)
            @test val_constrained isa typeof(val)
            @test val_constrained ≈ val atol = _TOL
            logabs = log_abs_det_jac(_transform, val)
            @test logabs isa AbstractFloat
            @test _check(_RNG, constraint, val)
################################################################################
# Custom Matrix flattening
            _tag = tag(val, false, true)
            _vals4 = flatten_Symmetric(val, _tag)
            @test length(_vals4) == 6
            @test all(Symmetric_from_flatten(_vals4, _tag) .≈ val)
        end
    end
end

############################################################################################
@testset "Constraints - Simplex" begin
    for output in outputtypes
        for flattentype in flattentypes
            flatdefault = FlattenDefault(; output = output, flattentype = flattentype)
            val = [.1, .2, .7]
            constraint = Simplex(val)
            ReConstructor(constraint, val)
            reconstruct = ReConstructor(flatdefault, constraint, val)
# Flatten
            x_flat = flatten(reconstruct, val)
            @test x_flat isa AbstractVector
            @test eltype(x_flat) == output
            @test length(x_flat) == 2
            @test x_flat == output.([.1, .2])
# Flatten AD
            x_flatAD = flattenAD(reconstruct, val)
            @test x_flatAD isa AbstractVector
            @test eltype(x_flatAD) == eltype(val)
# Unflatten
            x_unflat = unflatten(reconstruct, x_flat)
            @test x_unflat isa typeof(val)
# Unflatten AD
            x_unflatAD = unflattenAD(reconstruct, x_flat)
            @test eltype(x_unflatAD) == eltype(x_flat)
            x_unflatAD2 = unflattenAD(reconstruct, x_flatAD)
            @test eltype(x_unflatAD2) == eltype(x_flatAD)

################################################################################
# Check if errors if flattened parameter not correct size
            @test_throws ArgumentError flatten(reconstruct, zeros(4))
            @test_throws ArgumentError flatten(reconstruct, zeros(2))
            @test_throws ArgumentError flattenAD(reconstruct, zeros(4))
            @test_throws ArgumentError flattenAD(reconstruct,  zeros(2))
            @test_throws ArgumentError unflatten(reconstruct, zeros(length(x_flat)+1))
            @test_throws ArgumentError unflatten(reconstruct, zeros(length(x_flat)-1))
            @test_throws ArgumentError unflattenAD(reconstruct, zeros(length(x_flat)+1))
            @test_throws ArgumentError unflattenAD(reconstruct, zeros(length(x_flat)-1))

################################################################################
# Transforms
            _transform, _inversetransform = construct_transform(constraint, val)
            x_transformed = _transform(val)
            x_inversetransformed = _inversetransform(x_transformed)

            transformer = TransformConstructor(constraint, val)
            val_uncon = unconstrain(transformer, val)
            val_con = constrain(transformer, val_uncon)

            @test transformer.unconstrain == _transform
            @test transformer.constrain == _inversetransform
            @test x_transformed == val_uncon
            @test x_inversetransformed == val_con

            @test x_inversetransformed isa typeof(val)
            val_unconstrained = unconstrain(_transform, val)
            val_constrained = constrain(_inversetransform, val_unconstrained)
            @test val_constrained isa typeof(val)
            @test sum(val_constrained .≈ val) == 3
            @test sum(val_constrained) ≈ 1.0
            logabs = log_abs_det_jac(_transform, val)
            @test logabs isa AbstractFloat
            @test _check(_RNG, constraint, val)
################################################################################
# Custom Matrix flattening
            _vals4 = flatten_Simplex(val)
            @test length(_vals4) == length(val)-1
            _vals5 = Simplex_from_flatten!(zeros(length(val)), _vals4)
            _vals6 = Simplex_from_flatten(_vals4)
            @test sum(_vals5) ≈ 1.0
            @test sum(_vals6) ≈ 1.0
            @test all(output.(val) .≈ _vals5)
            @test all(output.(val) .≈ _vals6)
        end
    end
end


#=
# 1D mixed FE/FV example
=#

### If the Finch package has already been added, use this line #########
using Finch # Note: to add the package, first do: ]add "https://github.com/paralab/Finch.git"

### If not, use these four lines (working from the examples directory) ###
# if !@isdefined(Finch)
#     include("../Finch.jl");
#     using .Finch
# end
##########################################################################

init_finch("fefv1d");

useLog("fefv1dlog")

# Configuration setup
domain(1)
solverType([CG,FV])

# Select one of these time stepper setups. First type is for FE, second for FV. They are same if only one is given.
# timeStepper(EULER_EXPLICIT)
# timeStepper([EULER_IMPLICIT, EULER_EXPLICIT])
# timeStepper([RK4, EULER_EXPLICIT])
# timeStepper([CRANK_NICHOLSON, EULER_EXPLICIT])
timeStepper([LSRK4, LSRK4])

# Mesh
n = 40 # number of elements
mesh(LINEMESH, elsperdim=n)

# Variables and BCs
u = variable("u", method=CG)
testSymbol("v")
boundary(u, 1, DIRICHLET, 0)

w = variable("w", location=CELL, method=FV)
boundary(w, 1, DIRICHLET, 0)

# Time interval and initial condition
T = 1;
timeInterval(T)
initial(u, "x<0.5 ? sin(2*pi*x)^4 : 0")
initial(w, "x>0.5 ? sin(2*pi*x)^4 : 0")

coefficient("D", 0.001) # Diffusion rate
coefficient("f", "-0.5*sin(pi*x)^2")

# The flux and source terms of the conservation equation
# F and S in the following equation:
# Dt(int(u dx)) = int(S dx) - int(F ds)
flux(w, "-D*dot(grad(w),normal())") # in 1D -> -D * dx(u) * normal
source(w, "u")

# The weak form expression
weakForm(u, "Dt(u*v) + D * dot(grad(u),grad(v)) - w*v")

solve([u,w])

finalize_finch()

##### Uncomment below to plot
# xc = Finch.fv_info.cellCenters[:];
# xn = Finch.grid_data.allnodes[1,:];
# nnodes = length(xn); # The initial condition
# u0 = zeros(nnodes);
# for i=1:nnodes
#     u0[i] = sin(2*pi*xn[i])^4
# end

# using Plots
# pyplot();
# display(plot([xn xn], [u0 u.values[:]], markershape=:circle, label=["initial" "u, t="*string(T)]))
# plot!(xc, w.values[:], markershape=:square, label="w, t="*string(T))


include("prop_logic_theory.jl")
include("prover.jl")

ex = rewrite(:(((p => q) ∧ (r => s) ∧ (p ∨ r)) => (q ∨ s)), impl)
@test prove(t, ex, 3, 7)

@test @areequal t true ((¬p == p) == false)
@test @areequal t true ((¬p == ¬p) == true)
@test @areequal t true ((¬p ∨ ¬p) == ¬p) (¬p ∨ p) ¬(¬p ∧ p)
@test @areequal t true ((p => (p ∨ p)))
@test @areequal t true ((p => (p ∨ p)) == ((¬(p) ∧ q) => q)) == true

# Frege's theorem
@test @areequal t true (p => (q => r)) => ((p => q) => (p => r))

# Demorgan's
@test @areequal t true (¬(p ∨ q) == (¬p ∧ ¬q))

# Consensus theorem
@test @areequal t ((x ∧ y) ∨ (¬x ∧ z) ∨ (y ∧ z))   ((x ∧ y) ∨ (¬x ∧ z))

# @timev areequal(t, :((x ∧ y) ∨ (¬x ∧ z) ∨ (y ∧ z)), :((x ∧ y) ∨ (¬x ∧ z)))
# @timev areequal(t, :((x ∧ y) ∨ (¬x ∧ z) ∨ (y ∧ z)), :((x ∧ y) ∨ (¬babo ∧ z)))
# @timev areequalmagic(t, :((x ∧ y) ∨ (¬x ∧ z) ∨ (y ∧ z)),   :((x ∧ y) ∨ (¬x ∧ z)))
# @timev areequalmagic(t, :((x ∧ y) ∨ (¬x ∧ z) ∨ (y ∧ z)),   :((babo ∧ y) ∨ (¬x ∧ z)))

# TODO proof strategies?
# Constructive Dilemma

# @test @areequal (t ∪ [@rule :p => true]) true (((p => q) ∧ (r => s)) ∧ (p ∨ r)) => (q ∨ s)

# @test areequal(t, true, :(¬(((¬p ∨ q) ∧ (¬r ∨ s)) ∧ (p ∨ r)) ∨ (q ∨ s)))

# using Metatheory.EGraphs.Schedulers
# Metatheory.options.verbose = true
# Metatheory.options.printiter = true
# ex = rewrite(:(((p => q) ∧ (r => s) ∧ (p ∨ r)) => (q ∨ s)), impl)
# g = EGraph(ex)
# params = SaturationParams(timeout=10, eclasslimit=5000, scheduler=SimpleScheduler)#, scheduler=ScoredScheduler)
# @profview saturate!(g, t, params)

# ex = :((p ∧ q ∨ (¬q ∧ (p => r) ∧ q)) => (s ∧ q))
# prove(t, ex, 3)

# extran = addanalysis!(g, ExtractionAnalysis, astsize)
# ex = extract!(g, extran)
# println(ex)
# #
# #
# ex = rewrite(:(((p => q) ∧ (r => s) ∧ (p ∨ r)) => (q ∨ s)), impl)
# g = EGraph(ex)
# @timev areequal(g, t, ex, true; timeout=10, eclasslimit=5000, scheduler=ScoredScheduler, schedulerparams=(32, 1))
#

# @profiler saturate!(g, t; timeout=8, eclasslimit=5000)
# exit(0)

# ex = rewrite(:(((p => p) ∧ (r => z) ∧ (p ∨ r)) => (q ∨ s)), impl)
# @test false == prove(t, ex, 4)

# g = EGraph(:(((p => q) ∧ (r => s) ∧ (p ∨ r)) => (q ∨ s)))
# @time saturate!(g, t; timeout=30, eclasslimit=Inf)
#
# in_same_set(g.uf, g.root, addexpr!(g, true).id) |> println
#
# struct LogicAnalysis <: AbstractAnalysis
#     egraph::EGraph
#     logic_egraph::EGraph
# end


"""
    wnorm(x, smooth=0.0; by="invprior")

`x` is each category's number of samples. `by` option provides two methods:\n
+ `prior`: let biger be more biger, e.g. [1,4] -> [0.25,1.0]\n
+ `invprior`: let smaller be biger, e.g. [1,4] -> [1.0,0.25]\n
to return each category's weight. `help?> prior` and `help?> invprior` for details.
As `p(X|S) = P(S|X)P(X)/P(S) ∝  P(S|X)/P(S)`, `P(S)` can be considered as weight
when we're dealing with imbalanced category samples with a neural network. `P(S)`
can be estimated by the proportion of each category. i.e. ⤦\n
           (# samples from S)
    P(S) = ────────────────── ∝  (# samples from S)
            (# all samples)
"""
function wnorm(x, smooth=0.0; by="invprior")
    @assert smooth>=0.0 "smooth >= 0 , but got $smooth"
    by=="invprior" && return invprior(x, smooth)
    by=="prior"    && return prior(x, smooth)
end

"""
    prior(x, smooth=0.0) -> z

let biger be more biger! ⤦

    y = (x .+ smooth*maximum(x))
    z = y ./ sum(y)
    z = z ./ maximum(z)
# Example
    julia> x = [1,100,1000];
    julia> prior(x,0.5)'
    0.192618  0.230681  0.576701
"""
function prior(x, smooth=0.0)
    n = maximum(x) * smooth
    y = x .+ n
    s = sum(y)
    z = y ./ sum(y)
    return z ./ maximum(z)
end

"""
    invprior(x, smooth=0.0) -> z

let smaller be biger! ⤦

    y = 1 ./ (x .+ smooth*maximum(x))
    z = y ./ sum(y)
    z = z ./ maximum(z)
# Example
    julia> x = [1,100,1000];
    julia> invprior(x,0.5)'
     1.0  0.835  0.334
"""
function invprior(x, smooth=0.0)
    n = maximum(x) * smooth
    y = 1 ./ (x .+ n)
    s = sum(y)
    z = y ./ sum(y)
    return z ./ maximum(z)
end


import Distributions
using Statistics

include(joinpath(@__DIR__, "simulation_multi_target_localization.jl"))

function getMetricHistory(s_history)
    # Evaluation Metric: Worst Entropy
    MetricArray = Float64[];
    for t = 1:length(s_history)
        bVec = Distributions.MvNormal.(s_history[t].b)
        push!(MetricArray,maximum(Distributions.entropy.(bVec)));
    end
    return MetricArray
end

function evaluate()
    data_dir = normpath(joinpath(@__DIR__, "../data",
                                 "multi_target_localization"));
    # For ergodic control, the ErgodicControl.jl package is not compatible with Julia v1.3
    # Therefore, we use previously collected data with Julia v0.6 for the WAFR paper.
    ergodic_data = FileIO.load(joinpath(data_dir, "statistical_data_localization_openloop_ergodic.jld2"));

    safe_rngnums = ergodic_data["safe_rngnums"]

    num_successful_cases = 0;

    cost_greedy = Array{Float64}[];
    tcalc_greedy = Array{Float64}[];
    cost_sacbp = Array{Float64}[];
    tcalc_sacbp = Array{Float64}[];
    cost_mcts = Array{Float64}[];
    tcalc_mcts = Array{Float64}[];

    time_idx_array = [];

    for rngnum in safe_rngnums
        println("------- RNG Seed: $(rngnum) & #Cases Tested: $(num_successful_cases) -------")
        println("1. Simulation with Greedy Controller")
        #try
        y_history,tcalc_true_history,s_history,u_history,U_pool =
        simulate_main("greedy",rng_num=rngnum, animate=false, plotfig=false, verbose=false);
        push!(cost_greedy,getMetricHistory(s_history));
        push!(tcalc_greedy,tcalc_true_history);
        if isempty(time_idx_array)
            push!(time_idx_array, unique([s.t for s in s_history]))
        end
        #catch
        #    rngnum += 1
        #    continue
        #end
        println("2. Simulation with SACBP Controller")
        #try
        y_history,tcalc_true_history,s_history,u_history,U_pool =
        simulate_main("sacbp",rng_num=rngnum, animate=false, plotfig=false, verbose=false);
        push!(cost_sacbp,getMetricHistory(s_history));
        push!(tcalc_sacbp,tcalc_true_history);
        if isempty(time_idx_array)
            push!(time_idx_array, unique([s.t for s in s_history]))
        end
        #catch
        #    pop!(cost_greedy)
        #    pop!(tcalc_greedy)
        #    rngnum += 1
        #    continue
        #end
        println("3. Simulation with MCTS Controller")
        #try
        y_history,tcalc_true_history,s_history,u_history,U_pool =
        simulate_main("mcts",rng_num=rngnum, animate=false, plotfig=false, verbose=false);
        push!(cost_mcts,getMetricHistory(s_history));
        push!(tcalc_mcts,tcalc_true_history);
        if isempty(time_idx_array)
            push!(time_idx_array, unique([s.t for s in s_history]))
        end
        #catch
        #    pop!(cost_greedy)
        #    pop!(tcalc_greedy)
        #    pop!(cost_sacbp)
        #    pop!(tcalc_sacbp)
        #    rngnum += 1
        #    continue
        #end
        #append!(safe_rngnums,rngnum)
        #rngnum += 1;
        num_successful_cases += 1;
    end

    tcalc_greedy = vcat(tcalc_greedy...);
    tcalc_sacbp = vcat(tcalc_sacbp...);
    tcalc_mcts = vcat(tcalc_mcts...);

    cost_ergodic, tcalc_ergodic = ergodic_data["cost_ergodic"], ergodic_data["tcalc_ergodic"]

    FileIO.save(joinpath(data_dir, "statistical_data_localization_openloop.jld2"),
                "cost_greedy",cost_greedy,"tcalc_greedy",tcalc_greedy,
                "cost_sacbp",cost_sacbp,"tcalc_sacbp",tcalc_sacbp,
                "cost_mcts",cost_mcts,"tcalc_mcts",tcalc_mcts,
                "cost_ergodic",cost_ergodic,"tcalc_ergodic",tcalc_ergodic)

    # Plot Stats Data
    cost_std_mcts = [std([cost[ii] for cost in cost_mcts]) for ii = 1:length(cost_mcts[1])]
    cost_std_sacbp = [std([cost[ii] for cost in cost_sacbp]) for ii = 1:length(cost_sacbp[1])]
    cost_std_greedy = [std([cost[ii] for cost in cost_greedy]) for ii = 1:length(cost_greedy[1])];
    cost_std_ergodic = [std([cost[ii] for cost in cost_ergodic]) for ii = 1:length(cost_ergodic[1])];

    MetricHistoryGreedy = mean(cost_greedy)
    plot(time_idx_array,MetricHistoryGreedy[1:end-1],xlabel="Time [s]",ribbon=cost_std_greedy,fillalpha=0.3,ylabel="Worst Entropy Value",label="Greedy",color=:dimgrey,linewidth=2.,linestyle=:dot)
    MetricHistoryMCTS = mean(cost_mcts)
    plot!(time_idx_array,MetricHistoryMCTS[1:end-1],label="MCTS-DPW",ribbon=cost_std_mcts,color=:lightcoral,fillalpha=0.3,linewidth=2.,linestyle=:dash)
    MetricHistoryErgodic = mean(cost_ergodic)
    plot!(time_idx_array,MetricHistoryErgodic[1:end-1],label="Ergodic",ribbon=cost_std_ergodic,color=:darkorange,fillalpha=0.3,linewidth=2.,linestyle=:dashdot)
    MetricHistorySACBP = mean(cost_sacbp)
    plot!(time_idx_array,MetricHistorySACBP[1:end-1],label="SACBP",ribbon=cost_std_sacbp,fillalpha=0.3,color=:darkblue,linewidth=2.,size=(400,400))
    savefig(joinpath(data_dir, "tracking_costs.pdf"))

    # Plot Computation Time Comparison
    tcalcTrueArray = [tcalc_greedy,tcalc_mcts,tcalc_ergodic,tcalc_sacbp]
    meanTcalcTrueArray = mean.(tcalcTrueArray);
    stdTcalcTrueArray = std.(tcalcTrueArray);
    bar([1,2,3,4],meanTcalcTrueArray,yerror=stdTcalcTrueArray,linecolor=:black,marker=stroke(4.0,:black, :dash),c=[:dimgrey,:lightcoral,:darkorange,:darkblue],label="",size=(400,400));
    xticks!([1,2,3,4],["Greedy","MCTS-DPW","Ergodic","SACBP"]);
    plot!([0.5,4.5],[3/4*0.2,3/4*0.2],width=2,label="Targeted",color=:darkred,ylim=(-0.03,0.4))
    ylabel!("Control Computation Time [s]")
    savefig(joinpath(data_dir, "tracking_time.pdf"))
end

if abspath(PROGRAM_FILE) == @__FILE__
    evaluate();
end


"""
In addition to storing a `DataSet`, a `DataModel` also contains a function `model(x,θ)` and its derivative `dmodel(x,θ)` where `x` denotes the x-value of the data and `θ` is a vector of parameters on which the model depends.
Crucially, `dmodel` contains the derivatives of the model with respect to the parameters `θ`, not the x-values.
For example
```julia
DS = DataSet([1,2,3,4], [4,5,6.5,7.8], [0.5,0.45,0.6,0.8])
model(x::Number, θ::AbstractVector{<:Number}) = θ[1] * x + θ[2]
DM = DataModel(DS, model)
```
In cases where the output of the model has more than one component (i.e. `ydim > 1`), it is advisable to define the model function in such a way that it outputs static vectors using **StaticArrays.jl** for increased performance.
For `ydim = 1`, **InformationGeometry.jl** expects the model to output a number instead of a vector with one component. In contrast, the parameter configuration `θ` must always be supplied as a vector (even if it only has a single component).

An initial guess for the maximum likelihood parameters can optionally be passed to the `DataModel` as a vector via
```julia
DM = DataModel(DS, model, [1.0,2.5])
```
During the construction of a `DataModel` process which includes the search for the maximum likelihood estimate ``\\theta_\\text{MLE}``, multiple tests are run. If necessary, these tests can be skipped by appending `true` as the last argument in the constructor:
```julia
DM = DataModel(DS, model, [-Inf,π,1], true)
```

If a `DataModel` is constructed as shown in the above examples, the gradient of the model with respect to the parameters `θ` (i.e. its "Jacobian") will be calculated using automatic differentiation. Alternatively, an explicit analytic expression for the Jacobian can be specified by hand:
```julia
using StaticArrays
function dmodel(x::Number, θ::AbstractVector{<:Number})
   @SMatrix [x  1.]     # ∂(model)/∂θ₁ and ∂(model)/∂θ₂
end
DM = DataModel(DS, model, dmodel)
```
The output of the Jacobian must be a matrix whose columns correspond to the partial derivatives with respect to different components of `θ` and whose rows correspond to evaluations at different components of `x`.
Again, although it is not strictly required, outputting the Jacobian in form of a static matrix is typically beneficial for the overall performance.

It is also possible to specify a (logarithmized) prior distribution on the parameter space to the `DataModel` constructor after the initial guess for the MLE. For example:
```julia
using Distributions
Dist = MvNormal(ones(2), [1 0; 0 3.])
LogPriorFn(θ) = logpdf(Dist, θ)
DM = DataModel(DS, model, [1.0,2.5], LogPriorFn)
```

The `DataSet` contained in a `DataModel` named `DM` can be accessed via `Data(DM)`, whereas the model and its Jacobian can be used via `Predictor(DM)` and `dPredictor(DM)` respectively. The MLE and the value of the log-likelihood at the MLE are accessible via `MLE(DM)` and `LogLikeMLE(DM)`. The logarithmized prior can be accessed via `LogPrior(DM)`.
"""
struct DataModel <: AbstractDataModel
    Data::AbstractDataSet
    model::ModelOrFunction
    dmodel::ModelOrFunction
    MLE::AbstractVector{<:Number}
    LogLikeMLE::Real
    LogPrior::Union{Function,Nothing}
    DataModel(DF::DataFrame, args...; kwargs...) = DataModel(DataSet(DF), args...; kwargs...)
    DataModel(DS::AbstractDataSet,model::ModelOrFunction,SkipTests::Bool=false; kwargs...) = DataModel(DS,model,DetermineDmodel(DS,model; kwargs...), SkipTests)
    DataModel(DS::AbstractDataSet,model::ModelOrFunction,mle::AbstractVector,SkipTests::Bool=false; kwargs...) = DataModel(DS,model,DetermineDmodel(DS,model; kwargs...),mle,SkipTests)
    function DataModel(DS::AbstractDataSet,model::ModelOrFunction,mle::AbstractVector,LogPriorFn::Union{Function,Nothing},SkipTests::Bool=false; kwargs...)
        DataModel(DS, model, DetermineDmodel(DS, model; kwargs...), mle, LogPriorFn, SkipTests)
    end
    function DataModel(DS::AbstractDataSet, model::ModelOrFunction, dmodel::ModelOrFunction, SkipTests::Bool=false)
        SkipTests ? DataModel(DS, model, dmodel, [-Inf,-Inf], true) : DataModel(DS, model, dmodel, FindMLE(DS,model,dmodel))
    end
    function DataModel(DS::AbstractDataSet,model::ModelOrFunction,dmodel::ModelOrFunction,mle::AbstractVector{<:Number},SkipTests::Bool=false)
        DataModel(DS, model, dmodel, mle, nothing, SkipTests)
    end
    function DataModel(DS::AbstractDataSet,model::ModelOrFunction,dmodel::ModelOrFunction,mle::AbstractVector{<:Number},logPriorFn::Union{Function,Nothing},SkipTests::Bool=false)
        LogPriorFn = Prior(logPriorFn, mle)
        SkipTests && return DataModel(DS, model, dmodel, mle, (try loglikelihood(DS, model, mle, LogPriorFn) catch; -Inf end), true)
        MLE = FindMLE(DS, model, dmodel, mle, LogPriorFn);        LogLikeMLE = loglikelihood(DS, model, MLE, LogPriorFn)
        DataModel(DS, model, dmodel, MLE, LogLikeMLE, LogPriorFn, SkipTests)
    end
    function DataModel(DS::AbstractDataSet,model::ModelOrFunction,dmodel::ModelOrFunction,MLE::AbstractVector{<:Number},LogLikeMLE::Real,SkipTests::Bool=false)
        DataModel(DS, model, dmodel, MLE, LogLikeMLE, nothing, SkipTests)
    end
    function DataModel(DS::AbstractDataSet,model::ModelOrFunction,dmodel::ModelOrFunction,MLE::AbstractVector{<:Number},LogLikeMLE::Real,LogPriorFn::Union{Function,Nothing},SkipTests::Bool=false)
        !SkipTests && TestDataModel(DS, model, dmodel, MLE, LogLikeMLE, LogPriorFn)
        new(DS, model, dmodel, MLE, LogLikeMLE, Prior(LogPriorFn,MLE))
    end
end

function TestDataModel(DS::AbstractDataSet,model::ModelOrFunction,dmodel::ModelOrFunction,MLE::AbstractVector{<:Number},LogLikeMLE::Real,LogPriorFn::Union{Function,Nothing}=nothing)
    CheckModelHealth(DS, model)
    if model isa ModelMap
        !IsInDomain(model, MLE) && @warn "Supposed MLE $MLE not inside valid parameter domain specified for ModelMap. Consider specifying an appropriate intial parameter configuration."
    end
    if LogPriorFn isa Function
        @assert LogPriorFn(MLE) isa Real && LogPriorFn(MLE) ≤ 0.0
        !all(x->x ≤ 0.0, eigvals(EvalLogPriorHess(LogPriorFn, MLE))) && @warn "Hessian of specified LogPrior does not appear to be negative-semidefinite at MLE."
    end
    S = Score(DS, model, dmodel, MLE, LogPriorFn)
    norm(S) > sqrt(length(MLE)*1e-5) && @warn "Norm of gradient of log-likelihood at supposed MLE $MLE comparatively large: $(norm(S))."
    g = FisherMetric(DS, dmodel, MLE, LogPriorFn)
    det(g) == 0. && @warn "Model appears to contain superfluous parameters since it is not structurally identifiable at supposed MLE $MLE."
    !isposdef(Symmetric(g)) && @warn "Hessian of likelihood at supposed MLE $MLE not negative-definite: Consider passing an appropriate initial parameter configuration 'init' for the estimation of the MLE to DataModel e.g. via DataModel(DS,model,init)."
end

# For SciMLBase.remake
DataModel(;
Data::AbstractDataSet=DataSet([0.], [0.], [1.]),
model::ModelOrFunction=(x,p)->-Inf,
dmodel::ModelOrFunction=(x,p)->[-Inf],
MLE::AbstractVector{<:Number}=[-Inf],
LogLikeMLE::Real=-Inf,
LogPrior::Union{Function,Nothing}=nothing) = DataModel(Data, model, dmodel, MLE, LogLikeMLE, LogPrior)

# Specialized methods for DataModel

Data(DM::DataModel) = DM.Data
Predictor(DM::DataModel) = DM.model
dPredictor(DM::DataModel) = DM.dmodel
LogPrior(DM::DataModel) = DM.LogPrior

"""
    MLE(DM::DataModel) -> Vector
Returns the parameter configuration ``\\theta_\\text{MLE} \\in \\mathcal{M}`` which is estimated to have the highest likelihood of producing the observed data (under the assumption that the specified model captures the true relationship present in the data).
For performance reasons, the maximum likelihood estimate is stored as a part of the `DataModel` type.
"""
MLE(DM::DataModel) = DM.MLE

"""
    LogLikeMLE(DM::DataModel) -> Real
Returns the value of the log-likelihood ``\\ell`` when evaluated at the maximum likelihood estimate, i.e. ``\\ell(\\mathrm{data} \\, | \\, \\theta_\\text{MLE})``.
For performance reasons, this value is stored as a part of the `DataModel` type.
"""
LogLikeMLE(DM::DataModel) = DM.LogLikeMLE

pdim(DM::DataModel) = length(MLE(DM))

import Base: BigFloat
BigFloat(DM::DataModel) = DataModel(Data(DM), Predictor(DM), dPredictor(DM), BigFloat.(MLE(DM)))

InformNames(DM::AbstractDataModel, xnames::AbstractVector{String}, ynames::AbstractVector{String}) = DataModel(InformNames(Data(DM), xnames, ynames), Predictor(DM), dPredictor(DM), MLE(DM), LogLikeMLE(DM), LogPrior(DM), true)

# Dot not create Prior object when there is no prior.
Prior(Func::Nothing, args...; kwargs...) = nothing
# DFunction uses :Symbolic by default which can lead to problems
Prior(args...; ADmode::Union{Symbol,Val}=Val(:ForwardDiff), kwargs...) = DFunction(args...; ADmode=ADmode, kwargs...)
Prior(D::DFunction, args...; kwargs...) = D

EvalLogPrior(P, θ::AbstractVector{<:Number}; kwargs...) = EvalF(P, θ; kwargs...)
EvalLogPriorGrad(P, θ::AbstractVector{<:Number}; kwargs...) = EvaldF(P, θ; kwargs...)
EvalLogPriorHess(P, θ::AbstractVector{<:Number}; kwargs...) = EvalddF(P, θ; kwargs...)

EvalLogPrior(D::Nothing, x; kwargs...) = zero(suff(x))
EvalLogPriorGrad(D::Nothing, x; kwargs...) = zeros(suff(x),length(x))
EvalLogPriorHess(D::Nothing, x; kwargs...) = zeros(suff(x),length(x),length(x))


import ThresholdStability: unstatedep, unconstrained

Σ = [randn(2, 2) for _ in 1:4]
G = LightAutomaton(4); add_transition!(G,1,1,1); add_transition!(G,1,2,1); add_transition!(G,2,3,2); add_transition!(G, 3, 4, 3); add_transition!(G, 4, 1, 4)
X = [1,2,3,4]
s = discreteswitchedsystem(Σ, G, X)

@testset "Switched system type conversion" begin
    t = unstatedep(s)
    @test typeof(t) <: ConstrainedDiscreteSwitchedLinearSystem
    @test typeof(unconstrained(s)) <: StateDepDiscreteSwitchedLinearSystem
    @test typeof(unconstrained(t)) <: DiscreteSwitchedLinearSystem
end


using Test
using tdpe

@testset "Prime Factors" begin
   @test begin
        channel = Channel(c -> tdpe.get_prime_factors(c,6))
        f0,n0 = take!(channel)
        f1,n1 = take!(channel)
        f0,n0,f1,n1
   end == (2,1,3,1)

   @test begin
        channel = Channel(c -> tdpe.get_prime_factors(c,13))
        f0,n0 = take!(channel)
        f0,n0
   end == (13,1)

end

@testset "Primes" begin
     @test tdpe.get_nth_prime(1) == 2
     @test tdpe.get_nth_prime(2) == 3
     @test tdpe.get_nth_prime(3) == 5
     @test tdpe.get_nth_prime(245) == 1553
end

@testset "Project Euler" begin
    #include individual tests
    include("001.jl")
    include("002.jl")
    include("003.jl")
    include("004.jl")
    include("005.jl")
    include("006.jl")
    include("007.jl")
    include("008.jl")
    include("009.jl")
    include("010.jl")
    #end includes
end


"""
    StraightRoadway
A simple type representing a one lane, one dimensional straight roadway
# Fields
- `length::Float64`
"""
struct StraightRoadway
    length::Float64
end

"""
    mod_position_to_roadway(s::Float64, roadway::StraightRoadway)
performs a modulo of the position `s` with the length of `roadway`
"""
function mod_position_to_roadway(s::Float64, roadway::StraightRoadway)
    while s > roadway.length
        s -= roadway.length
    end
    while s < 0.0
        s += roadway.length
    end
    return s
end

"""
    get_headway(s_rear::Float64, s_fore::Float64, roadway::StraightRoadway)
returns a positive distance between s_rear and s_fore.
"""
function get_headway(s_rear::Float64, s_fore::Float64, roadway::StraightRoadway)
    while s_fore < s_rear
        s_fore += roadway.length
    end
    while s_fore > s_rear + roadway.length
        s_fore -= roadway.length
    end
    return s_fore - s_rear # positive distance
end


export GeneralizedEuclideanAlgorithm

"""
    abstract type AbstractUnivariateGCDAlgorithm end

Algorithm computing the greatest common divisor of univariate polynomials.
See [`GeneralizedEuclideanAlgorithm`](@ref) and [`SubresultantAlgorithm`](@ref).
"""
abstract type AbstractUnivariateGCDAlgorithm end

"""
    struct GeneralizedEuclideanAlgorithm <: AbstractUnivariateGCDAlgorithm
        primitive_rem::Bool
        skip_last::Bool
    end

Algorithm computing the greatest common divisor of univariate polynomials using
the Euclidean algorithm generalized for polynomials with coefficients over a
a unique factorization domain, see [Knu14, Algorithm E, p. 426-427].

[Knu14] Knuth, D.E., 2014.
*Art of computer programming, volume 2: Seminumerical algorithms.*
Addison-Wesley Professional. Third edition.
"""
struct GeneralizedEuclideanAlgorithm <: AbstractUnivariateGCDAlgorithm
    primitive_rem::Bool
    skip_last::Bool
    function GeneralizedEuclideanAlgorithm(
        primitive_rem::Bool=false,
        skip_last::Bool=false,
    )
        return new(primitive_rem, skip_last)
    end
end

"""
    struct SubresultantAlgorithm <: AbstractUnivariateGCDAlgorithm end

Algorithm computing the greatest common divisor of univariate polynomials using
the Subresultant algorithm, see [Knu14, Algorithm C, p. 428-429].

[Knu14] Knuth, D.E., 2014.
*Art of computer programming, volume 2: Seminumerical algorithms.*
Addison-Wesley Professional. Third edition.
"""
struct SubresultantAlgorithm <: AbstractUnivariateGCDAlgorithm end

Base.lcm(p::APL, q::APL, algo::AbstractUnivariateGCDAlgorithm=GeneralizedEuclideanAlgorithm()) = p * div(q, gcd(p, q, algo))
Base.gcd(α, p::APL, algo::AbstractUnivariateGCDAlgorithm=GeneralizedEuclideanAlgorithm()) = gcd(α, content(p, algo))
Base.gcd(p::APL, α, algo::AbstractUnivariateGCDAlgorithm=GeneralizedEuclideanAlgorithm()) = gcd(content(p, algo), α)

#function MA.promote_operation(::typeof(gcd), P::Type{<:Number}, Q::Type{<:Number})
#    return typeof(gcd(one(P), one(Q)))
#end
function MA.promote_operation(::typeof(gcd), P::Type{<:APL}, Q::Type{<:APL})
    return MA.promote_operation(pseudo_rem, P, Q)
end

"""
    function gcd(p1::AbstractPolynomialLike{T}, p2::AbstractPolynomialLike{S}) where {T, S}

Returns a greatest common divisor of `p1` and `p2`. Note that it does not make
sense, in general, to speak of "the" greatest common divisor of u and v; there
is a set of greatest common divisors, each one being a unit multiple of the
others [Knu14, p. 424].

# Implementation notes

The classical algorithm for computing the `gcd`, commonly referred to as the
Euclidean Algorithm is to use a recursion with the base case `gcd(p, 0) = p`
and the relation `gcd(p1, p2) = gcd(p2, rem(p1, p2))`.
The relation comes from the Euclidean division:
`p1 = q * p2 + r`,
if `g` divides `p1` and `p2` then it divides `r` and
if `g` divides `r` and `p2` then it divides `p1`.

For multivariate polynomials, you may have `rem(p1, p2) = p1` hence this will not
terminate.
To ensure we make progress, we can pick a given variable `xi` and try to find
`q1` and `q2` such that
`q2 * p1 = q1 * p2 + r`
and the degree of `r` in `xi` is strictly smaller than the degree of `p1` in `xi`.
Note that if `g` divides `p1` and `p2` then it divides `r` but
if `g` divides `r` and `p2` then it might divide `q2` and not `p1`.
So what do we do ?
Let `dj` be the degree of `pj` in `xi`.
Suppose we pick `qj` to be the coefficient of `pj` in `xi^dj`.
If `g` divides `q2` then it means that the degree of `g` in `xi` is zero.
Therefore, if it divides `p2` then it also divides the coefficients
of `p2` in `xi^k` for `k = 0, 1, ..., d2`.
This means that if we ensure that these are relatively prime then we won't have
any issue.
So we start by computing a `gcd` `gj` of the coefficients in each degree of
`xi` of `pj`, this is called the [`content`](@ref) of `pj`.
And then we compute `_gcd(p1 / g1, p2 / g2) * gcd(g1, g2)` where we can use the
recursion `_gcd(p1, p2) = _gcd(p2, q2 * p1 - q1 * p2)` where `q1, q2` are as
defined above.
This is the [`GeneralizedEuclideanAlgorithm`](@ref).

[Knu14] Knuth, D.E., 2014.
*Art of computer programming, volume 2: Seminumerical algorithms.*
Addison-Wesley Professional. Third edition.
"""
function Base.gcd(p1::APL{T}, p2::APL{S}, algo::AbstractUnivariateGCDAlgorithm=GeneralizedEuclideanAlgorithm()) where {T, S}
    # If one of these is zero, `shift` should be infinite
    # for this method to work so we exclude these cases.
    if isapproxzero(p1)
        return convert(MA.promote_operation(gcd, typeof(p1), typeof(p2)), p2)
    end
    if isapproxzero(p2)
        return convert(MA.promote_operation(gcd, typeof(p1), typeof(p2)), p1)
    end
    shift1, defl1 = deflation(p1)
    shift2, defl2 = deflation(p2)
    shift = gcd(shift1, shift2)
    defl = mapexponents(gcd, defl1, defl2)
    # We factor out `x.^shift1` from `p1` and
    # `x.^shift2` from `p2`. The `gcd` of these
    # monomials is `x.^shift`.
    # Then, we subsitute `y[i] = x[i]^defl[i]`.
    q1 = deflate(p1, shift1, defl)
    q2 = deflate(p2, shift2, defl)
    g = deflated_gcd(q1, q2, algo)
    return inflate(g, shift, defl)::MA.promote_operation(gcd, typeof(p1), typeof(p2))
end

# Inspired from to `AbstractAlgebra.deflation`
function deflation(p::AbstractPolynomialLike)
    if iszero(p)
        return constantmonomial(p), constantmonomial(p)
    end
    shift = fill(-1, nvariables(p))
    defl = zeros(Int, nvariables(p))
    for mono in monomials(p)
        exps = exponents(mono)
        for i in eachindex(exps)
            exp = exps[i]
            @assert exp >= 0
            if shift[i] == -1
                shift[i] = exp
            elseif exp < shift[i]
                # There are two cases:
                # 1) If `defl[i]` is zero then it means all previous monomials
                #    had degree `shift[i]` so we just set `defl[i]` to
                #    `shift[i] - exp` or equivalently `gcd(0, shift[i] - exp)`.
                # 2) If  `defl[i]` is positive then we have some monomials with
                #    degree `shift[i]` and some with degree
                #    `shift[i] + k * defl[i]` for some `k > 0`. We have
                #    `gcd(shift[i] - exp, shift[i] + k1 * defl[i] - exp, shift[i] + k2 * defl[i] - exp, ...) =`
                #    `gcd(shift[i] - exp, k1 * defl[i], k2 * defl[i], ...)`
                #    Since `gcd(k1, k2, ...) = 1`, this is equal to
                #    `gcd(shift[i] - exp, defl[i])`
                defl[i] = gcd(defl[i], shift[i] - exp)
                shift[i] = exp
            else
                defl[i] = gcd(defl[i], exp - shift[i])
            end
        end
    end
    @assert all(d -> d >= 0, shift)
    @assert all(d -> d >= 0, defl)
    s = prod(variables(p).^shift)::monomialtype(p)
    d = prod(variables(p).^defl)::monomialtype(p)
    return s, d
end

function _zero_to_one_exp(defl::AbstractMonomial)
    # TODO Make it faster by calling something like `mapexponents`.
    return prod(variables(defl).^map(d -> iszero(d) ? one(d) : d, exponents(defl)))
end
function deflate(p::AbstractPolynomialLike, shift, defl)
    if isconstant(shift) && all(d -> isone(d) || iszero(d), exponents(defl))
        return p
    end
    q = MA.operate(deflate, p, shift, _zero_to_one_exp(defl))
    return q
end
function inflate(α, shift, defl)
    return inflate(convert(polynomialtype(shift, typeof(α)), α), shift, defl)
end
function inflate(p::AbstractPolynomialLike, shift, defl)
    if isconstant(shift) && all(d -> isone(d) || iszero(d), exponents(defl))
        return p
    end
    q = MA.operate(inflate, p, shift, _zero_to_one_exp(defl))
    return q
end

function MA.operate(::typeof(deflate), mono::AbstractMonomial, shift, defl)
    mutable_mono = mapexponents(-, mono, shift)
    return mapexponents!(div, mutable_mono, defl)
end
function MA.operate(::typeof(inflate), mono::AbstractMonomial, shift, defl)
    mutable_mono = mapexponents(*, mono, defl)
    return mapexponents!(+, mutable_mono, shift)
end

# Inspired from to `AbstractAlgebra.deflate`
function MA.operate(op::Union{typeof(deflate), typeof(inflate)}, p::AbstractPolynomialLike, shift, defl)
    return polynomial(map(terms(p)) do t
        return term(coefficient(t), MA.operate(op, monomial(t), shift, defl))
    end)
end

function deflated_gcd(p1::APL{T}, p2::APL{S}, algo) where {T, S}
    i1, i2, num_common = _extracted_variable(p1, p2)
    if iszero(i1)
        if iszero(i2)
            return univariate_gcd(p1, p2, algo)
        else
            if isapproxzero(p1)
                return convert(MA.promote_operation(gcd, typeof(p1), typeof(p2)), p2)
            end
            v2 = variables(p2)[i2]
            q2 = isolate_variable(p2, v2)
            g = content(q2, algo)
            return gcd(p1, g, algo)
        end
    else
        if iszero(i2)
            if isapproxzero(p2)
                return convert(MA.promote_operation(gcd, typeof(p1), typeof(p2)), p1)
            end
            v1 = variables(p1)[i1]
            q1 = isolate_variable(p1, v1)
            g = content(q1, algo)
            return gcd(g, p2, algo)
        else
            if num_common > 1
                @assert i1 == i2
                v1 = variables(p1)[i1]
                return multivariate_gcd(p1, p2, v1, algo)
            else
                return univariate_gcd(p1, p2, algo)
            end
        end
    end
end

function Base.gcdx(p1::APL{T}, p2::APL{S}, algo::AbstractUnivariateGCDAlgorithm=GeneralizedEuclideanAlgorithm()) where {T, S}
    i1, i2, num_common = _extracted_variable(p1, p2)
    R = MA.promote_operation(gcd, typeof(p1), typeof(p2))
    if iszero(i1)
        if iszero(i2)
            return univariate_gcdx(p1, p2, algo)
        else
            if isapproxzero(p1)
                return zero(R), one(R), convert(R, p2)
            end
            error("Not implemented yet")
        end
    else
        if iszero(i2)
            if isapproxzero(p2)
                return one(R), zero(R), convert(R, p1)
            end
            error("Not implemented yet")
        else
            if num_common > 1
                @assert i1 == i2
                error("Not implemented yet")
            else
                return univariate_gcdx(p1, p2, algo)
            end
        end
    end
end

# Returns first element in the union of two decreasing vectors
function _extracted_variable(p1, p2)
    v1 = variables(p1)
    v2 = variables(p2)
    i1 = i2 = 1
    best = nothing
    best_var1 = 0
    best_var2 = 0
    num_common = 0
    while i1 <= length(v1) || i2 <= length(v2)
        if i2 > length(v2) || (i1 <= length(v1) && v1[i1] > v2[i2])
            if !iszero(maxdegree(p1, v1[i1]))
                return i1, 0, num_common
            end
            i1 += 1
        elseif i1 > length(v1) || v2[i2] > v1[i1]
            if !iszero(maxdegree(p2, v2[i2]))
                return 0, i2, num_common
            end
            i2 += 1
        else
            @assert v1[i1] == v2[i2]
            v = v1[i1]
            i1 += 1
            i2 += 1
            d1, n1 = deg_num_leading_terms(p1, v)
            d2, n2 = deg_num_leading_terms(p2, v)
            if iszero(d1)
                if iszero(d2)
                    continue
                else
                    return 0, i2-1, num_common
                end
            else
                if iszero(d2)
                    return i1-1, 0, num_common
                end
            end
            if d1 < d2
                d1, d2 = d2, d1
                n1, n2 = n2, n1
            end
            # Heuristic used in `AbstractAlgebra`:
            # https://github.com/Nemocas/AbstractAlgebra.jl/blob/4c6b0a366e550df3db84a665de186111bc3cf8ed/src/generic/MPoly.jl#L4347
            # FIXME what is this based on ? Is there any analysis somewhere comparing different heuristics ?
            cur = max(log(n2) * d1 * d2, log(2) * d2)
            if best === nothing || best > cur
                best = cur
                best_var1 = i1-1
                best_var2 = i2-1
            end
            num_common += 1
        end
    end
    return best_var1, best_var2, num_common
end

function multivariate_gcd(p1::APL, p2::APL, var, algo)
    q = univariate_gcd(isolate_variable(p1, var), isolate_variable(p2, var), algo)
    return sum(coefficient(t) * monomial(t) for t in terms(q))::MA.promote_operation(gcd, typeof(p1), typeof(p2))
end

function isolate_variable(poly::APL, var::AbstractVariable)
    T = typeof(substitute(Subs(), zeroterm(poly), (var,) => (1,)))
    dict = Dict{Int,Vector{T}}()
    for t in terms(poly)
        d = degree(t, var)
        dict_ts = get(dict, d, nothing)
        ts = dict_ts === nothing ? T[] : dict_ts
        push!(ts, subs(t, (var,) => (1,)))
        if dict_ts === nothing
            dict[d] = ts
        end
    end
    return polynomial([
        term(polynomial(ts), var^d) for (d, ts) in dict
    ])
end

"""
    primitive_univariate_gcd(p::APL, q::APL, algo::AbstractUnivariateGCDAlgorithm)

Returns the `gcd` of primitive polynomials `p` and `q` using algorithm `algo`
which is a subtype of [`AbstractUnivariateGCDAlgorithm`](@ref).
"""
function primitive_univariate_gcd end

function not_divided_error(u, v)
    error(
        "Polynomial `$v` of degree `$(maxdegree(v))` and effective",
        " variables `$(effective_variables(v))` does not divide",
        " polynomial `$u` of degree `$(maxdegree(u))` and effective",
        " variables `$(effective_variables(u))`. Did you call",
        " `univariate_gcd` with polynomials with more than one",
        " variable in common ? If yes, call `gcd` instead, otherwise,",
        " please report this.",
    )
end

# If `p` and `q` do not have the same time then the local variables `p` and `q`
# won't be type stable.
function primitive_univariate_gcd(p::APL, q::APL, algo::GeneralizedEuclideanAlgorithm)
    if maxdegree(p) < maxdegree(q)
        return primitive_univariate_gcd(q, p, algo)
    end
    R = MA.promote_operation(gcd, typeof(p), typeof(q))
    u = convert(R, p)
    v = convert(R, q)
    while true
        if isapproxzero(v)
            return u
        elseif isconstant(v)
            # `p` and `q` are primitive so if one of them is constant, it cannot
            # divide the content of the other one.
            return one(R)
        end
        divided, r = pseudo_rem(u, v, algo)
        if !divided
            not_divided_error(u, v)
        end
        if !algo.primitive_rem
            r = primitive_part(r, algo)::R
        end
        u, v = v, r::R
    end
end

function primitive_univariate_gcdx(u0::APL, v0::APL, algo::GeneralizedEuclideanAlgorithm)
    if maxdegree(u0) < maxdegree(v0)
        a, b, g = primitive_univariate_gcdx(v0, u0, algo)
        return b, a, g
    end
    R = MA.promote_operation(gcd, typeof(u0), typeof(v0))
    u = convert(R, u0)
    v = convert(R, v0)
    if isapproxzero(v)
        return one(R), zero(R), u
    elseif isconstant(v)
        # `p` and `q` are primitive so if one of them is constant, it cannot
        # divide the content of the other one.
        return zero(R), one(R), v
    end
    # p * u = q * v + r
    p, q, r = pseudo_divrem(u, v, algo)
    if iszero(q)
        not_divided_error(u, v)
    end
    if iszero(r)
        # Shortcut, does not change the output
        return zero(R), one(R), v
    end
    # TODO
    #if !algo.primitive_rem
    #    r = primitive_part(r, algo)::R
    #end
    # a * v + b * r = g
    # a * v + b * (p * u - q * v) = g
    # b * p * u + (a - b * q) * v = g
    a, b, g = primitive_univariate_gcdx(v, r, algo)
    return p * b, (a - b * q), g
end

function primitive_univariate_gcd(p::APL, q::APL, ::SubresultantAlgorithm)
    error("Not implemented yet")
end

"""
    univariate_gcd(p1::AbstractPolynomialLike, p2::AbstractPolynomialLike, algo::AbstractUnivariateGCDAlgorithm)

Return the *greatest common divisor* of the polynomials `p1` and `p2` that have
at most one variable in common and for which the coefficients are either
`AbstractFloat` or part of a unique factorization domain, e.g., rational numbers,
integers or multivariate polynomials. So `p1` and `p2` should have at most one
variable in common but their coefficients can be multivariate polynomials that
share arbitrarily many variables.

If the coefficients are not `AbstractFloat`, this
1. separates `p1` and `p2` in their [`content`](@ref) and
   [`primitive_part`](@ref) using [`primitive_part_content`](@ref); see
   [Knu14, Algorithm E: E1, p. 426] or [Knu14, Algorithm C: C1, p. 428].
2. Computes the [`gcd`](@ref) of the contents and primitive parts, using
   [`primitive_univariate_gcd`](@ref) for primitive parts.
3. Return the product of these two `gcd`; see
   [Knu14, Algorithm E: E4, p. 427] or [Knu14, Algorithm C: C4, p. 429].

[Knu14] Knuth, D.E., 2014.
*Art of computer programming, volume 2: Seminumerical algorithms.*
Addison-Wesley Professional. Third edition.
"""
function univariate_gcd(p1::APL{S}, p2::APL{T}, algo::AbstractUnivariateGCDAlgorithm) where {S,T}
    return univariate_gcd(algebraic_structure(S, T), p1, p2, algo)
end
function univariate_gcd(::UFD, p1::APL, p2::APL, algo::AbstractUnivariateGCDAlgorithm)
    f1, g1 = primitive_part_content(p1, algo)
    f2, g2 = primitive_part_content(p2, algo)
    pp = primitive_univariate_gcd(f1, f2, algo)
    gg = _gcd(g1, g2, algo)#::MA.promote_operation(gcd, typeof(g1), typeof(g2))
    # Multiply each coefficient by the gcd of the contents.
    return mapcoefficientsnz(Base.Fix1(*, gg), pp)
end

function univariate_gcd(::Field, p1::APL, p2::APL, algo::AbstractUnivariateGCDAlgorithm)
    return primitive_univariate_gcd(p1, p2, algo)
end

function univariate_gcdx(p1::APL{S}, p2::APL{T}, algo::AbstractUnivariateGCDAlgorithm) where {S,T}
    return univariate_gcdx(algebraic_structure(S, T), p1, p2, algo)
end
function univariate_gcdx(::UFD, p1::APL, p2::APL, algo::AbstractUnivariateGCDAlgorithm)
    f1, g1 = primitive_part_content(p1, algo)
    f2, g2 = primitive_part_content(p2, algo)
    a, b, pp = primitive_univariate_gcdx(f1, f2, algo)
    gg = _gcd(g1, g2, algo)#::MA.promote_operation(gcd, typeof(g1), typeof(g2))
    # Multiply each coefficient by the gcd of the contents.
    return g2 * a, g1 * b, g1 * g2 * mapcoefficientsnz(Base.Fix1(*, gg), pp)
end
function univariate_gcdx(::Field, p1::APL, p2::APL, algo::AbstractUnivariateGCDAlgorithm)
    return primitive_univariate_gcdx(p1, p2, algo)
end

_gcd(a::APL, b::APL, algo) = gcd(a, b, algo)
_gcd(a, b::APL, algo) = gcd(a, b, algo)
_gcd(a::APL, b, algo) = gcd(a, b, algo)
_gcd(a, b, algo) = gcd(a, b)

_simplifier(a::APL, b::APL, algo) = gcd(a, b, algo)
_simplifier(a, b, algo) = _gcd(a, b, algo)
# Before Julia v1.4, it is not defined.
# After Julia v1.4, it is defined as `gcd of num / lcm of den`.
# We prefer `gcd of den`, otherwise,
# `1/a0 + 1/a1 x + 1/a2 x^2 + ... + 1/an x^n`
# will be transformed into
# `a1*a2*...*an + a0*a2*...*an x + ...`
# which makes the size of the `BigInt`s grow significantly which slows things down.
_simplifier(a::Rational, b::Rational, algo) = gcd(a.num, b.num) // gcd(a.den, b.den)

"""
    content(poly::AbstractPolynomialLike{T}, algo::AbstractUnivariateGCDAlgorithm) where {T}

Return the *content* of the polynomial `poly` over a unique factorization
domain `S` as defined in [Knu14, (3) p. 423].
That is, return the `gcd` of the coefficients of `poly`.
See also [`primitive_part_content`](@ref).

[Knu14] Knuth, D.E., 2014.
*Art of computer programming, volume 2: Seminumerical algorithms.*
Addison-Wesley Professional. Third edition.
"""
function content(poly::APL{T}, algo::AbstractUnivariateGCDAlgorithm) where {T}
    P = MA.promote_operation(gcd, T, T)
    # This is tricky to infer a `content` calls `gcd` which calls `content`, etc...
    # To help Julia break the loop, we annotate the result here.
    return reduce(
        (a, b) -> _simplifier(a, b, algo),
        coefficients(poly),
        init = zero(P),
    )::P
end

"""
    primitive_part_content(poly::AbstractPolynomialLike{T}, algo::AbstractUnivariateGCDAlgorithm) where {T}

Return the *primitive part* of the polynomial `poly` over a unique
factorization domain `S` as defined in [Knu14, (3) p. 423].
That is, return the exact division of `poly` by its [`content`](@ref).
If the content is also needed, call [`primitive_part_content`](@ref)
instead.

[Knu14] Knuth, D.E., 2014.
*Art of computer programming, volume 2: Seminumerical algorithms.*
Addison-Wesley Professional. Third edition.
"""
primitive_part(p::APL, algo::AbstractUnivariateGCDAlgorithm) = primitive_part_content(p, algo)[1]
primitive_part(p::APL{<:AbstractFloat}, algo::AbstractUnivariateGCDAlgorithm) = p

"""
    primitive_part_content(poly::AbstractPolynomialLike{T}, algo::AbstractUnivariateGCDAlgorithm) where {T}

Return the *primitive part* and *content* of the polynomial `poly` over a unique
factorization domain `S` as defined in [Knu14, (3) p. 423]. This is more
efficient to call this function rather than calling [`primitive_part`](@ref) and
[`content`](@ref) separately since computing the primitive part requires
computing the content first and this function avoid computing the content twice.

[Knu14] Knuth, D.E., 2014.
*Art of computer programming, volume 2: Seminumerical algorithms.*
Addison-Wesley Professional. Third edition.
"""
function primitive_part_content(p, algo::AbstractUnivariateGCDAlgorithm)
    g = content(p, algo)
    return mapcoefficientsnz(Base.Fix2(_div, g), p), g
end


using RDatasets, KernelDensity
mlmf = dataset("mlmRev", "Gcsemv");

df = mlmf[complete_cases(mlmf[[:Written, :Course]]), :];

dc = array(df[:Course]);
dcc = zeros(0);
for i in 1:length(dc)
  if !isnan(dc[i])
    push!(dcc,dc[i])
  end
end
kdc = kde(dcc);

dw = array(df[:Written]);
dww = zeros(0);
for i in 1:length(dw)
  if !isnan(dw[i])
    push!(dww,dw[i])
  end
end
kdw = kde(dww);

using Winston
kdc = kde(dcc); kdw = kde(dww);
plot(kdc.x, kdc.density, "r--", kdw.x, kdw.density, "b;")


# Invertible CNN layer from Putzky & Welling (2019)
# Author: Philipp Witte, pwitte3@gatech.edu
# Date: January 2020

using InvertibleNetworks, LinearAlgebra, Test

# Input
nx = 28
ny = 28
n_in = 8
n_hidden = 8
batchsize = 2

# Input images
X = randn(Float32, nx, ny, n_in, batchsize)
X0 = randn(Float32, nx, ny, n_in, batchsize)
dX = X - X0

# Invertible layers
L = CouplingLayerIRIM(nx, ny, n_in, n_hidden, batchsize)
L01 = CouplingLayerIRIM(nx, ny, n_in, n_hidden, batchsize)
L02 = CouplingLayerIRIM(nx, ny, n_in, n_hidden, batchsize)

###################################################################################################
# Test invertibility

X_ = L.inverse(L.forward(X))
@test isapprox(norm(X - X_)/norm(X), 0f0; atol=1e-2)

X_ = L.forward(L.inverse(X))
@test isapprox(norm(X - X_)/norm(X), 0f0; atol=1e-2)

###################################################################################################
# Gradient tests

# Loss Function
function loss(L, X, Y)
    Y_ = L.forward(X)
    ΔY = Y_ - Y

    f = .5f0*norm(ΔY)^2
    ΔX = L.backward(ΔY, Y_)[1]

    # Pass back gradients w.r.t. input X and from the residual block and 1x1 conv. layer
    return f, ΔX, L.C.v1.grad, L.C.v2.grad, L.C.v3.grad, L.RB.W1.grad, L.RB.W2.grad, L.RB.W3.grad
end

# Gradient test w.r.t. input X0
Y = L.forward(X)
f0, ΔX = loss(L, X0, Y)[1:2]
h = 0.1f0
maxiter = 5
err1 = zeros(Float32, maxiter)
err2 = zeros(Float32, maxiter)

print("\nGradient test invertible layer\n")
for j=1:maxiter
    f = loss(L, X0 + h*dX, Y)[1]
    err1[j] = abs(f - f0)
    err2[j] = abs(f - f0 - h*dot(dX, ΔX))
    print(err1[j], "; ", err2[j], "\n")
    global h = h/2f0
end

@test isapprox(err1[end] / (err1[1]/2^(maxiter-1)), 1f0; atol=1f1)
@test isapprox(err2[end] / (err2[1]/4^(maxiter-1)), 1f0; atol=1f1)

# Gradient test w.r.t. weights of residual block
Y = L.forward(X)
Lini = deepcopy(L02)
dW1 = L.RB.W1.data - L02.RB.W1.data
dW2 = L.RB.W2.data - L02.RB.W2.data
dW3 = L.RB.W3.data - L02.RB.W3.data

f0, ΔX, Δv1, Δv2, Δv3, ΔW1, ΔW2, ΔW3 = loss(L02, X, Y)
h = 0.1f0
maxiter = 5
err3 = zeros(Float32, maxiter)
err4 = zeros(Float32, maxiter)

print("\nGradient test invertible layer\n")
for j=1:maxiter
    L02.RB.W1.data = Lini.RB.W1.data + h*dW1
    L02.RB.W2.data = Lini.RB.W2.data + h*dW2
    L02.RB.W3.data = Lini.RB.W3.data + h*dW3
    f = loss(L02, X, Y)[1]
    err3[j] = abs(f - f0)
    err4[j] = abs(f - f0 - h*dot(dW1, ΔW1) - h*dot(dW2, ΔW2) - h*dot(dW3, ΔW3))
    print(err3[j], "; ", err4[j], "\n")
    global h = h/2f0
end

@test isapprox(err3[end] / (err3[1]/2^(maxiter-1)), 1f0; atol=1f1)
@test isapprox(err4[end] / (err4[1]/4^(maxiter-1)), 1f0; atol=1f1)

# Gradient test w.r.t. 1x1 conv weights
Y = L.forward(X)
Lini = deepcopy(L01)
dv1 = L.C.v1.data - L01.C.v1.data
dv2 = L.C.v2.data - L01.C.v2.data
dv3 = L.C.v3.data - L01.C.v3.data

f0, ΔX, Δv1, Δv2, Δv3, ΔW1, ΔW2, ΔW3 = loss(L01, X, Y)
h = 0.1f0
maxiter = 4
err5 = zeros(Float32, maxiter)
err6 = zeros(Float32, maxiter)

print("\nGradient test invertible layer\n")
for j=1:maxiter
    L01.C.v1.data = Lini.C.v1.data + h*dv1
    L01.C.v2.data = Lini.C.v2.data + h*dv2
    L01.C.v3.data = Lini.C.v3.data + h*dv3
    f = loss(L01, X, Y)[1]
    err5[j] = abs(f - f0)
    err6[j] = abs(f - f0 - h*dot(dv1, Δv1) - h*dot(dv2, Δv2) - h*dot(dv3, Δv3))
    print(err5[j], "; ", err6[j], "\n")
    global h = h/2f0
end

@test isapprox(err5[end] / (err5[1]/2^(maxiter-1)), 1f0; atol=1f1)
@test isapprox(err6[end] / (err6[1]/4^(maxiter-1)), 1f0; atol=1f1)


# Script to run a test case for understanding how the probability of success scales with active learning #

# Add paths
loc_gm_package = "../src/"
if !(loc_gm_package in LOAD_PATH)
    push!(LOAD_PATH, "../src/")
end

# Required packages
using GraphicalModelLearning
using LightGraphs
using Cairo
using Junet

# Function definitions
include("beta_scaling.jl")

# parameters
FLAG_create_struct_gm = true
FLAG_regular_random_gm = false
FLAG_lattice_gm = true
FLAG_weak_impurity = false
sampling_regime = M_regime()
learning_algo = CD(1e-12)

if FLAG_lattice_gm
    # text file to save adjacency matrix in
    file_adj_matrix_gm = "adj_matrix_ferro_lattice_gm_A_ME.txt"
    # name of picture to save graphical model in
    file_plot_gm = "ferro_lattice_gm_A_ME.eps"
    # File to save final results
    file_M_opt_gm = "M_opt_Ferro_Lattice_A_ME.txt"
elseif FLAG_regular_random_gm
    # text file to save adjacency matrix in
    file_adj_matrix_gm = "adj_matrix_spin_glass_gm.txt"
    # name of picture to save graphical model in
    file_plot_gm = "spin_glass_gm3.eps"
    # File to save final results
    file_M_opt_gm = "M_opt_M_Regime_Regular_Random_B.txt"
end

# Simple test
N = 16
d = 3
α = 0.4
β_array = [0.4+0.1*i for i=0:46]

# Create and plot the initial graphical graphical model
β = copy(β_array[4])
if FLAG_create_struct_gm
    if FLAG_regular_random_gm
        adj_matrix, struct_adj_matrix = spin_glass_random_regular_model(N,d,α,β)
        gm = Junet.Graph(struct_adj_matrix,directed=false)

        h = plot(gm, size=(300,300), layout=layout_fruchterman_reingold(gm), node_color="white", node_border_color="black",
            node_border_width=2, edge_color=[edge_color_gm(i, adj_matrix, α, β) for i = 1:edgecount(gm)],
            edge_width=2, format=:eps, file=file_plot_gm)
    elseif FLAG_lattice_gm
        adj_matrix, struct_adj_matrix = ferro_lattice(N,α,β,FLAG_weak_impurity)
        m = n = Int(sqrt(N))
        draw_periodic_lattice(adj_matrix,m,n,α,β,file_plot_gm)
        open(file_adj_matrix_gm, "w") do io
            writedlm(io, adj_matrix)
        end
    end
else
    adj_matrix = readdlm(file_adj_matrix_gm)
end

## Start the complexity studies
τ = α/2
L = 45
M_guess = 1000
M_factor = 0.05

M_opt = Array{Int64,1}(length(β_array))

if FLAG_lattice_gm
    for i = 1:length(β_array)
        # define β
        β = copy(β_array[i])
        println(β)

        # Create the adjacency matrix
        adj_matrix, struct_adj_matrix = ferro_lattice(N,α,β,FLAG_weak_impurity)

        # Get the optimum number of samples
        M_opt[i] = get_M_opt_glauber_dynamics(adj_matrix, RISE(), learning_algo, sampling_regime, τ, L, M_guess, M_factor)

        M_guess = Int(floor(0.8*copy(M_opt[i])))
    end
end

writedlm(file_M_opt_gm,M_opt)


export between, P, O, X, Y, xs, ys, magnitude, normalize, signed_angle_to

X(x::Real) = P(x, 0)
Y(y::Real) = P(0, y)
const O = P(0, 0)

xs(ps::Array{Point}) = [p.x for p in ps]
ys(ps::Array{Point}) = [p.y for p in ps]

Point(ang::Angle) = Point(cos(ang.rad), sin(ang.rad))
Point(ang::Angle, length::Real) = Point(cos(ang.rad) * length, sin(ang.rad) * length)

Base.convert(::Type{Point}, t::Tuple{S, T}) where {S<:Real,T<:Real} = Point(t[1], t[2])

Base.show(io::IO, p::Point) = print(io, "Point($(p.xy[1]), $(p.xy[2]))")

function needed_attributes(::Type{Point})
    (Visible, Fill, Stroke, Linestyle, Linewidth, Markersize, Marker)
end

magnitude(p::Point) = sqrt(sum(p.xy .^ 2))
normalize(p::Point) = p / magnitude(p)

function Base.getproperty(p::Point, sym::Symbol)
    if sym == :x
        return p.xy[1]
    elseif sym == :y
        return p.xy[2]
    else
        getfield(p, sym)
    end
end

Base.convert(::Type{SVector{2, Float64}}, p::Point) = p.xy

Base.:+(p1::Point, p2::Point) = Point(p1.xy + p2.xy)
Base.:-(p1::Point, p2::Point) = Point(p1.xy - p2.xy)
Base.:-(p::Point) = Point(-p.xy)
Base.:*(p::Point, factor::Real) = Point(p.xy .* factor)
Base.:*(factor::Real, p::Point) = p * factor
Base.:/(p::Point, r::Real) = Point(p.xy ./ r)
Base.:(==)(p1::Point, p2::Point) = p1.xy == p2.xy
Base.:(!=)(p1::Point, p2::Point) = p1.xy != p2.xy
from_to(p1::Point, p2::Point) = p2 - p1
const → = from_to
between(p1::Point, p2::Point, fraction::Real) = p1 + (p2 - p1) * fraction
cross(p1::Point, p2::Point) = p1.x * p2.y - p1.y * p2.x
dot(p1::Point, p2::Point) = p1.x * p2.x + p1.y * p2.y

Base.isapprox(p1::Point, p2::Point) = Base.isapprox(magnitude(p1 → p2), 0; atol=6)

function Base.angle(p::Point)
    Angle(atan(p.y, p.x))
end

function signed_angle_to(p1::Point, p2::Point)
    Angle(atan(cross(p1, p2), dot(p1, p2)))
end

function _rotation_matrix(angle::Angle)
    c = cos(angle)
    s = sin(angle)
    SMatrix{2, 2, Float64}(c, s, -s, c)
end

function rotate(p::Point, angle::Angle; around::Point=Point(0, 0))
    vector = from_to(around, p)
    rotated_vector = Point(_rotation_matrix(angle) * vector.xy)
    rotated_vector + around
end


type BurgerModel <: NonlinearSpectralModel
  d :: Float64
end

type BurgerData <: SpectralDataField
  n :: Int64
  x :: Array{Float64,2}
end

export BurgerModel, BurgerData
include("trunes.jl")

include("timegradient.jl")

function +(x:: BurgerData, y :: BurgerData)
  BurgerData(x.n,x.x+y.x)
end
function *(x:: BurgerData, y :: Float64)
  BurgerData(x.n,x.x*y)
end
function *(y :: Float64,x:: BurgerData )
  BurgerData(x.n,x.x*y)
end


using OrdinaryDiffEq, DiffEqDevTools, Test, LinearAlgebra, Random
using DiffEqProblemLibrary.ODEProblemLibrary: importodeproblems; importodeproblems()
using OrdinaryDiffEq: maxeig!
import DiffEqProblemLibrary.ODEProblemLibrary: prob_ode_linear, prob_ode_2Dlinear
probArr = Vector{ODEProblem}(undef, 2)
probArr[1] = prob_ode_linear
probArr[2] = prob_ode_2Dlinear

@testset "Power Iteration of Runge-Kutta-Chebyshev Tests" begin
  Random.seed!(123)
  for iip in [true, false], alg in [ROCK2(), ROCK4(), RKC(), SERK2(), ESERK4(), ESERK5()]
    println(typeof(alg))
    A = randn(20,20)
    test_f(u,p,t) = A*u
    test_f(du,u,p,t) = mul!(du, A, u)
    prob = ODEProblem{iip}(test_f, randn(20), (0,1.))
    integrator = init(prob, alg)
    eigm = maximum(abs.(eigvals(A)))
    maxeig!(integrator, integrator.cache)
    eigest = integrator.eigen_est
    @test eigest ≈ eigm rtol=0.1eigm

    A = A - 1e2I
    test_stiff(u,p,t) = A*u
    test_stiff(du,u,p,t) = mul!(du, A, u)
    prob = ODEProblem{iip}(test_stiff, ones(20), (0,1.))
    @test_nowarn solve(prob, alg)
  end

  Random.seed!(123)
  for iip in [true, false], alg in [IRKC()]
    println(typeof(alg))
    A = randn(20,20)
    B = randn(20,20)
    test_f1 = !iip ? (u,p,t) -> A*u : (du,u,p,t) -> mul!(du, A, u)
    test_f2 = !iip ? (u,p,t) -> B*u : (du,u,p,t) -> mul!(du, B, u)
    ff_split = SplitFunction{iip}(test_f1,test_f2)
    prob = SplitODEProblem{iip}(ff_split, randn(20,1), (0.0,1.))
    integrator = init(prob, alg)
    eigm = maximum(abs.(eigvals(A)))
    maxeig!(integrator, integrator.cache)
    eigest = integrator.eigen_est
    @test eigest ≈ eigm rtol=0.1eigm

    A = A - 1e2I
    test_f1 = !iip ? (u,p,t) -> A*u : (du,u,p,t) -> mul!(du, A, u)
    prob = SplitODEProblem{iip}(SplitFunction{iip}(test_f1,test_f2), ones(20), (0.0, 1.0))
    @test_nowarn solve(prob, alg)
  end
end

@testset "Runge-Kutta-Chebyshev Convergence Tests" begin
  dts = 1 .//2 .^(8:-1:4)
  testTol = 0.1
  for prob in probArr
    println("ROCK2")
    #default ROCK2
    sim = test_convergence(dts,prob,ROCK2())
    @test sim.𝒪est[:l∞] ≈ 2 atol=testTol
    #testing ROCK2 for different minimum stages to insure that the constatns are right
    sim = test_convergence(dts,prob,ROCK2(min_stages=5))
    @test sim.𝒪est[:l∞] ≈ 2 atol=testTol
    sim = test_convergence(dts,prob,ROCK2(min_stages=10))
    @test sim.𝒪est[:l∞] ≈ 2 atol=testTol
    sim = test_convergence(dts,prob,ROCK2(min_stages=21))
    @test sim.𝒪est[:l∞] ≈ 2 atol=testTol
    #default ROCK4
    println("ROCK4")
    sim = test_convergence(dts,prob,ROCK4())
    @test sim.𝒪est[:l∞] ≈ 4 atol=testTol
    #testing ROCK4 for different minimum stages to insure that the constatns are right
    sim = test_convergence(dts,prob,ROCK4(min_stages=6))
    @test sim.𝒪est[:l∞] ≈ 4 atol=testTol
    sim = test_convergence(dts,prob,ROCK4(min_stages=10))
    @test sim.𝒪est[:l∞] ≈ 4 atol=testTol
    sim = test_convergence(dts,prob,ROCK4(min_stages=21))
    @test sim.𝒪est[:l∞] ≈ 4 atol=testTol

    println("ROCKC")
    sim = test_convergence(dts,prob,RKC())
    @test sim.𝒪est[:l∞] ≈ 2 atol=testTol
    sim = test_convergence(dts,prob,SERK2())
    @test sim.𝒪est[:l∞] ≈ 2 atol=testTol
    sim = test_convergence(dts,prob,ESERK4())
    @test sim.𝒪est[:l∞] ≈ 4 atol=testTol
  end
  dts = 1 .//2 .^(6:-1:2)
  for prob in probArr
    sim = test_convergence(dts,prob,ESERK5())
    @test sim.𝒪est[:l∞] ≈ 5 atol=testTol
  end
end


@testset "app" begin

@testset "exit" begin
	app = MiniApp()
	put!(app.fromGUI, :exit=>[])
	process_thread(app.jg, app.fromGUI, app.toGUI; verbosityLevel=2)
	didExit = false
	while true
		@test isready(app.toGUI)
		msg = take!(app.toGUI)
		msg.first == :exited && (didExit=true; break)
	end
	@test didExit
end

@testset "exit2" begin
	app = MiniApp()
	init(app)
	lastSchedulerTime = Ref{UInt64}(0)
	put!(app.fromGUI, :exit=>[])
	@test !runall(app)
end

@testset "data.tsv" begin
	filepath = joinpath(@__DIR__, "data/data.tsv")

	varIds = [Symbol("V$i") for i=1:40]
	sampleIds = [string('S',lpad(i,2,'0')) for i=1:20]
	groupAnnot = repeat(["A","B"],inner=10)
	timeAnnot = vcat(0.01:0.01:0.1, 0.01:0.01:0.1)

	app = MiniApp()
	init(app)

	# Open file
	setvalue(app, Any["samplefilepath", filepath])
	setvalue(app, Any["lastsampleannot", "Time"])
	put!(app.fromGUI, :loadsample=>[])
	@test runall(app)
	dfSample = app.jg.scheduler.jobs[app.jg.loadSampleID].result
	@test propertynames(dfSample) == vcat([:SampleId, :Group, :Time], varIds)
	@test dfSample.SampleId == sampleIds
	@test dfSample.Group == groupAnnot
	@test dfSample.Time == timeAnnot
	@test size(dfSample)==(20,3+40)

	X = Matrix(Matrix{Float64}(dfSample[:,4:end])')
	normalizemean!(X)

	# PMA (groups)
	setvalue(app, Any["samplesimplexmethod", "SA"])
	setvalue(app, Any["sampleannot", "Group"])
	put!(app.fromGUI, :dimreduction=>[])
	@test runall(app)
	reduced = app.jg.scheduler.jobs[app.jg.dimreductionID].result
	@test reduced.sa == dfSample[!,1:3]
	@test reduced.va[!,1] == varIds
	factorizationcmp(pma(X, groupsimplices(groupAnnot); nsv=10), reduced.F)

	# PMA (time series)
	setvalue(app, Any["samplesimplexmethod", "Time"])
	setvalue(app, Any["sampleannot", "Group"])
	setvalue(app, Any["timeannot", "Time"])
	put!(app.fromGUI, :dimreduction=>[])
	@test runall(app)
	reduced = app.jg.scheduler.jobs[app.jg.dimreductionID].result
	@test reduced.sa == dfSample[!,1:3]
	@test reduced.va[!,1] == varIds
	#factorizationcmp(pma(X, timeseriessimplices(timeAnnot, groupby=groupAnnot); nsv=10), reduced.F)
	G = timeseriessimplices(timeAnnot, groupby=groupAnnot)
	factorizationcmp(pma(X, G; nsv=10), reduced.F)

	# PMA (NN)
	setvalue(app, Any["samplesimplexmethod", "NN"])
	setvalue(app, Any["knearestneighbors", "2"])
	setvalue(app, Any["distnearestneighbors", "0.5"])
	put!(app.fromGUI, :dimreduction=>[])
	@test runall(app)
	reduced = app.jg.scheduler.jobs[app.jg.dimreductionID].result
	@test reduced.sa == dfSample[!,1:3]
	@test reduced.va[!,1] == varIds
	factorizationcmp(pma(X, neighborsimplices(X,k=2,r=0.5,dim=50), nsv=10), reduced.F)

	# PMA (NN withing groups)
	setvalue(app, Any["samplesimplexmethod", "NNSA"])
	setvalue(app, Any["sampleannot", "Group"])
	setvalue(app, Any["knearestneighbors", "2"])
	setvalue(app, Any["distnearestneighbors", "0.5"])
	put!(app.fromGUI, :dimreduction=>[])
	@test runall(app)
	reduced = app.jg.scheduler.jobs[app.jg.dimreductionID].result
	@test reduced.sa == dfSample[!,1:3]
	@test reduced.va[!,1] == varIds
	F = pma(X, neighborsimplices(X,k=2,r=0.5,dim=50,groupby=groupAnnot), nsv=10)
	factorizationcmp(F, reduced.F)

	# Exports
	mktempdir() do temppath
		for (dim,order,mode) in zip((1,3,7,10), ("Abs","Descending","Ascending","Original"), ("Samples","Variables","Samples","Variables"))
			setvalue(app, Any["exportsingledim", "$dim"])
			setvalue(app, Any["exportsinglesort", order])
			setvalue(app, Any["exportmode", mode])
			filepath = joinpath(temppath,"PMA$(dim)_$(mode)_$order.tsv")
			setvalue(app, Any["exportsinglepath", filepath])
			put!(app.fromGUI, :exportsingle=>[])
			@test runall(app)
			result = CSV.read(filepath, DataFrame; delim='\t', ntasks=1)
			colName = Symbol(:PMA,dim)
			resultPMA = result[:,colName]

			if mode=="Samples"
				idCol = :SampleId
				ids = sampleIds
				PMA = reduced.F.V[:,dim]
			else
				idCol = :VariableId
				ids = string.(varIds)
				PMA = reduced.F.U[:,dim]
			end

			perm = collect(1:length(PMA))
			if order=="Abs"
				perm = sortperm(PMA, by=abs, rev=true)
			elseif order=="Descending"
				perm = sortperm(PMA, rev=true)
			elseif order=="Ascending"
				perm = sortperm(PMA)
			end

			@test propertynames(result)==[idCol,colName]
			@test result[!,idCol] == ids[perm]
			@test resultPMA ≈ PMA[perm]
		end

		for (dim,mode) in zip((4,5), ("Samples","Variables"))
			setvalue(app, Any["exportmultipledim", "$dim"])
			setvalue(app, Any["exportmode", mode])
			filepath = joinpath(temppath,"PMA$(dim)_$(mode).csv")
			setvalue(app, Any["exportmultiplepath", filepath])
			put!(app.fromGUI, :exportmultiple=>[])
			@test runall(app)
			result = CSV.read(filepath, DataFrame; delim=',', ntasks=false)

			colNames = Symbol.(:PMA,1:dim)

			if mode=="Samples"
				@test propertynames(result)==vcat(:SampleId, colNames)
				@test result.SampleId == sampleIds
				@test Matrix(result[:,colNames]) ≈ reduced.F.V[:,1:dim]
			else
				@test propertynames(result)==vcat(:VariableId, colNames)
				@test result.VariableId == string.(varIds)
				@test Matrix(result[:,colNames]) ≈ reduced.F.U[:,1:dim]
			end
		end
	end

	# Exit
	put!(app.fromGUI, :exit=>[])
	@test !runall(app)
end

@testset "MissingValueReconstruction" begin
	reconstructed = Float64[0.06 0.26 0.0 0.0 0.28 0.03 0.04 0.12 0.05 0.0 0.02 0.31 0.0 0.85 0.41 0.06 0.04 0.92 0.32 0.12; 0.01 0.13 0.48 0.64 0.23 0.01 0.0 0.18 0.03 0.14 0.26 0.0 0.09 0.27 0.1 0.0 0.48 0.06 0.07 0.56; 0.0 0.22 0.02 0.0 0.02 0.06 0.05 0.59 0.64 0.81 0.98 0.24 0.08 0.24 0.41 0.0 0.65 0.15 0.11 0.0; 0.01 0.01 0.31 0.23 0.02 0.0 0.01 0.15 0.03 0.64 0.21 0.16 0.0 0.02 0.12 0.84 0.06 0.0 0.0 0.2; 0.03 0.55 0.0 0.06 0.01 0.01 0.16 0.01 0.42 0.73 0.01 0.15 0.27 0.02 0.02 0.37 0.16 0.19 0.04 0.0; 0.36 0.97 0.54 0.98 0.03 0.52 0.1 0.06 0.14 0.0 0.46 0.38 0.29 0.93 0.06 0.07 0.05 0.45 0.0 0.01; 0.12 0.97 0.3 0.07 0.11 0.28 0.31 0.07 0.0 0.16 0.0 0.29 0.33 0.01 0.01 0.02 0.17 0.0 0.67 0.45; 0.0 0.08 0.49 0.34 0.07 0.01 0.03 0.1 0.05 0.16 0.05 0.36 0.0 0.38 0.41 0.0 0.09 0.67 0.61 1.0; 0.01 0.3 0.19 0.0 0.0 0.0 0.32 0.72 0.0 0.04 0.7 1.0 0.55 0.12 0.03 0.19 0.52 0.04 0.04 0.8; 0.12 0.0 0.04 0.02 0.19 0.09 0.01 0.66 0.25 0.21 0.01 0.02 0.38 0.62 0.3 0.99 0.38 0.1 0.54 0.07; 0.08 0.0 0.16 0.01 0.69 0.23 0.81 0.29 0.13 0.4 0.04 0.15 0.17 0.64 0.03 0.32 0.78 0.28 0.12 0.36; 0.89 0.02 0.0 0.02 0.26 0.06 0.01 0.01 0.48 0.58 0.25 0.29 0.51 0.31 0.22 0.0 0.27 0.01 0.04 0.05; 0.55 0.04 0.47 0.0 0.24 0.07 0.64 0.01 0.02 0.0 0.42 0.0 0.36 0.22 0.06 0.95 0.66 0.27 0.09 0.05; 0.46 0.19 0.1 0.01 0.13 0.15 0.06 0.84 0.02 0.0 0.09 0.62 0.01 0.0 0.01 0.0 0.01 0.33 0.47 0.58; 0.04 0.28 0.27 0.69 0.0 0.78 0.12 0.01 0.13 0.0 0.0 0.09 0.11 0.53 0.0 0.29 0.39 0.67 0.42 0.29; 0.01 0.09 0.0 0.49 0.18 0.0 0.64 0.67 0.32 0.37 0.08 0.0 0.04 0.17 0.0 0.06 0.01 0.0 0.43 0.22; 0.27 0.02 0.05 0.88 0.02 0.16 0.09 0.0 0.46 0.05 0.18 0.11 0.02 0.37 0.88 0.47 0.92 0.41 0.96 0.0; 0.0 0.99 0.98 0.31 0.05 0.02 0.06 0.03 0.34 0.37 0.17 0.0 0.33 0.78 0.47 0.75 0.34 0.04 0.69 0.0; 0.69 0.34 0.15 0.12 0.11 0.0 0.91 0.35 0.05 0.0 0.26 0.01 0.62 0.82 0.48 0.4 0.08 0.0 0.32 0.0; 0.08 0.01 0.85 0.82 0.0 0.0 0.99 0.0 0.12 0.05 0.04 0.53 0.0 0.89 0.26 0.01 0.17 0.39 0.05 0.0; 0.01 0.01 0.72 0.2647368421052631 0.0 0.6 0.01 0.08 0.18 0.42 0.48 0.02 0.34 0.59 0.02 0.6 0.02 0.76 0.03 0.14; 0.17555555555555552 0.13 0.47 0.01 0.08 0.26 0.25 0.17555555555555552 0.0 0.0 0.01 0.82 0.0 0.0 0.03 0.0 0.0 0.3 0.07 0.73; 0.0 0.3664705882352941 0.39 0.99 0.03 0.5 0.88 0.13 0.8 0.0 0.0 0.0 0.3664705882352941 0.3664705882352941 0.43 0.7 0.72 0.22 0.04 0.4; 0.0 0.5 0.5 0.97 0.02 0.08 0.35 0.238125 0.02 0.26 0.02 0.238125 0.1 0.0 0.0 0.92 0.0 0.238125 0.238125 0.07; 0.09 0.0 0.09733333333333334 0.09733333333333334 0.56 0.0 0.09733333333333334 0.09733333333333334 0.0 0.14 0.01 0.0 0.02 0.08 0.12 0.0 0.09733333333333334 0.09 0.1 0.25; 0.34500000000000003 0.34500000000000003 0.13 0.63 0.34500000000000003 0.03 0.0 0.06 0.56 0.55 0.15 0.34500000000000003 0.87 0.9 0.79 0.04 0.34500000000000003 0.34500000000000003 0.09 0.03; 0.85 0.79 0.32769230769230767 0.32769230769230767 0.35 0.32769230769230767 0.32769230769230767 0.21 0.03 0.01 0.36 0.44 0.52 0.01 0.32769230769230767 0.66 0.32769230769230767 0.03 0.0 0.32769230769230767; 0.86 0.24499999999999997 0.24499999999999997 0.24 0.01 0.24499999999999997 0.84 0.24499999999999997 0.24499999999999997 0.15 0.24499999999999997 0.24499999999999997 0.24499999999999997 0.1 0.13 0.3 0.03 0.04 0.24 0.0; 0.3427272727272727 0.92 0.3427272727272727 0.3427272727272727 0.09 0.3427272727272727 0.9 0.0 0.3427272727272727 0.3427272727272727 0.02 0.3427272727272727 0.63 0.05 0.19 0.09 0.0 0.88 0.3427272727272727 0.3427272727272727; 0.6 0.271 0.271 0.271 0.271 0.271 0.16 0.271 0.271 0.271 0.271 0.271 0.02 0.4 0.04 0.0 0.18 0.34 0.01 0.96; 0.0 0.1688888888888889 0.24 0.0 0.1688888888888889 0.2 0.02 0.1688888888888889 0.33 0.1688888888888889 0.1688888888888889 0.1688888888888889 0.17 0.1688888888888889 0.06 0.1688888888888889 0.1688888888888889 0.1688888888888889 0.1688888888888889 0.5; 0.26375000000000004 0.26375000000000004 0.84 0.26375000000000004 0.26375000000000004 0.42 0.26375000000000004 0.26375000000000004 0.26375000000000004 0.26375000000000004 0.54 0.26375000000000004 0.26375000000000004 0.0 0.13 0.01 0.04 0.26375000000000004 0.26375000000000004 0.13; 0.21142857142857144 0.21142857142857144 0.21142857142857144 0.21142857142857144 0.27 0.03 0.21142857142857144 0.11 0.21142857142857144 0.5 0.21142857142857144 0.21142857142857144 0.21142857142857144 0.21142857142857144 0.21142857142857144 0.21142857142857144 0.21142857142857144 0.15 0.36 0.06; 0.22833333333333336 0.22833333333333336 0.22833333333333336 0.22833333333333336 0.1 0.22833333333333336 0.22833333333333336 0.01 0.02 0.22833333333333336 0.01 0.22833333333333336 0.22833333333333336 0.22833333333333336 0.22833333333333336 0.64 0.22833333333333336 0.59 0.22833333333333336 0.22833333333333336; 0.08800000000000001 0.08800000000000001 0.08800000000000001 0.04 0.08800000000000001 0.08800000000000001 0.08 0.15 0.17 0.08800000000000001 0.08800000000000001 0.08800000000000001 0.08800000000000001 0.08800000000000001 0.08800000000000001 0.0 0.08800000000000001 0.08800000000000001 0.08800000000000001 0.08800000000000001; 0.52 0.62 0.30250000000000005 0.07 0.30250000000000005 0.30250000000000005 0.30250000000000005 0.30250000000000005 0.30250000000000005 0.30250000000000005 0.30250000000000005 0.30250000000000005 0.30250000000000005 0.30250000000000005 0.0 0.30250000000000005 0.30250000000000005 0.30250000000000005 0.30250000000000005 0.30250000000000005; 0.043333333333333335 0.08 0.043333333333333335 0.043333333333333335 0.043333333333333335 0.043333333333333335 0.0 0.043333333333333335 0.043333333333333335 0.043333333333333335 0.043333333333333335 0.043333333333333335 0.043333333333333335 0.05 0.043333333333333335 0.043333333333333335 0.043333333333333335 0.043333333333333335 0.043333333333333335 0.043333333333333335; 0.34 0.34 0.34 0.34 0.34 0.34 0.34 0.68 0.34 0.0 0.34 0.34 0.34 0.34 0.34 0.34 0.34 0.34 0.34 0.34; 0.04 0.04 0.04 0.04 0.04 0.04 0.04 0.04 0.04 0.04 0.04 0.04 0.04 0.04 0.04 0.04 0.04 0.04 0.04 0.04; 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0]
	logReconstructed = Float64[-0.6666 -0.2688 -0.811 -0.811 -0.2345 -0.737 -0.7131 -0.5353 -0.6897 -0.811 -0.7612 -0.1844 -0.811 0.5059 -0.02915 -0.6666 -0.7131 0.5753 -0.1681 -0.5353; -0.7859 -0.5146 0.07039 0.275 -0.3219 -0.7859 -0.811 -0.415 -0.737 -0.4941 -0.2688 -0.811 -0.5995 -0.2515 -0.5778 -0.811 0.07039 -0.6666 -0.6439 0.1763; -0.811 -0.3401 -0.7612 -0.811 -0.7612 -0.6666 -0.6897 0.2141 0.275 0.4647 0.6323 -0.304 -0.6215 -0.304 -0.02915 -0.811 0.2869 -0.4739 -0.5564 -0.811; -0.7859 -0.7859 -0.1844 -0.3219 -0.7612 -0.811 -0.7859 -0.4739 -0.737 0.275 -0.3585 -0.454 -0.811 -0.7612 -0.5353 0.4957 -0.6666 -0.811 -0.811 -0.3771; -0.737 0.1635 -0.811 -0.6666 -0.7859 -0.7859 -0.454 -0.7859 -0.0145 0.3785 -0.7859 -0.4739 -0.2515 -0.7612 -0.7612 -0.08927 -0.454 -0.3959 -0.7131 -0.811; -0.1047 0.6229 0.1506 0.6323 -0.737 0.1243 -0.5778 -0.6666 -0.4941 -0.811 0.04264 -0.074 -0.2176 0.585 -0.6666 -0.6439 -0.6897 0.02857 -0.811 -0.7859; -0.5353 0.6229 -0.2009 -0.6439 -0.5564 -0.2345 -0.1844 -0.6439 -0.811 -0.454 -0.811 -0.2176 -0.152 -0.7859 -0.7859 -0.7612 -0.4344 -0.811 0.3103 0.02857; -0.811 -0.6215 0.08406 -0.1361 -0.6439 -0.7859 -0.737 -0.5778 -0.6897 -0.454 -0.6897 -0.1047 -0.811 -0.074 -0.02915 -0.811 -0.5995 0.3103 0.2388 0.6508; -0.7859 -0.2009 -0.3959 -0.811 -0.811 -0.811 -0.1681 0.3674 -0.811 -0.7131 0.3448 0.6508 0.1635 -0.5353 -0.737 -0.3959 0.1243 -0.7131 -0.7131 0.4542; -0.5353 -0.811 -0.7131 -0.7612 -0.3959 -0.5995 -0.7859 0.2987 -0.2863 -0.3585 -0.7859 -0.7612 -0.074 0.251 -0.2009 0.6415 -0.074 -0.5778 0.1506 -0.6439; -0.6215 -0.811 -0.454 -0.7859 0.3334 -0.3219 0.4647 -0.2176 -0.5146 -0.04394 -0.7131 -0.4739 -0.4344 0.275 -0.737 -0.1681 0.433 -0.2345 -0.5353 -0.1047; 0.546 -0.7612 -0.811 -0.7612 -0.2688 -0.6666 -0.7859 -0.7859 0.07039 0.2016 -0.2863 -0.2176 0.111 -0.1844 -0.3401 -0.811 -0.2515 -0.7859 -0.7131 -0.6897; 0.1635 -0.7131 0.05658 -0.811 -0.304 -0.6439 0.275 -0.7859 -0.7612 -0.811 -0.0145 -0.811 -0.1047 -0.3401 -0.6666 0.6041 0.2987 -0.2515 -0.5995 -0.6897; 0.04264 -0.3959 -0.5778 -0.7859 -0.5146 -0.4739 -0.6666 0.4957 -0.7612 -0.811 -0.5995 0.251 -0.7859 -0.811 -0.7859 -0.811 -0.7859 -0.152 0.05658 0.2016; -0.7131 -0.2345 -0.2515 0.3334 -0.811 0.433 -0.5353 -0.7859 -0.5146 -0.811 -0.811 -0.5995 -0.5564 0.1375 -0.811 -0.2176 -0.05889 0.3103 -0.0145 -0.2176; -0.7859 -0.5995 -0.811 0.08406 -0.415 -0.811 0.275 0.3103 -0.1681 -0.08927 -0.6215 -0.811 -0.7131 -0.4344 -0.811 -0.6666 -0.7859 -0.811 0.0 -0.3401; -0.2515 -0.7612 -0.6897 0.5361 -0.7612 -0.454 -0.5995 -0.811 0.04264 -0.6897 -0.415 -0.5564 -0.7612 -0.08927 0.5361 0.05658 0.5753 -0.02915 0.6135 -0.811; -0.811 0.6415 0.6323 -0.1844 -0.6897 -0.7612 -0.6666 -0.737 -0.1361 -0.08927 -0.4344 -0.811 -0.152 0.433 0.05658 0.4005 -0.1361 -0.7131 0.3334 -0.811; 0.3334 -0.1361 -0.4739 -0.5353 -0.5564 -0.811 0.5656 -0.1203 -0.6897 -0.811 -0.2688 -0.7859 0.251 0.4751 0.07039 -0.04394 -0.6215 -0.811 -0.1681 -0.811; -0.6215 -0.7859 0.5059 0.4751 -0.811 -0.811 0.6415 -0.811 -0.5353 -0.6897 -0.7131 0.1375 -0.811 0.546 -0.2688 -0.7859 -0.4344 -0.05889 -0.6897 -0.811; -0.7859 -0.7859 0.3674 -0.3344 -0.811 0.2265 -0.7859 -0.6215 -0.415 -0.0145 0.07039 -0.7612 -0.1361 0.2141 -0.7612 0.2265 -0.7612 0.4114 -0.737 -0.4941; -0.4889 -0.5146 0.05658 -0.7859 -0.6215 -0.2688 -0.2863 -0.4889 -0.811 -0.811 -0.7859 0.4751 -0.811 -0.811 -0.737 -0.811 -0.811 -0.2009 -0.6439 0.3785; -0.811 -0.1888 -0.05889 0.6415 -0.737 0.09761 0.5361 -0.5146 0.4542 -0.811 -0.811 -0.811 -0.1888 -0.1888 0.0 0.3448 0.3674 -0.3401 -0.7131 -0.04394; -0.811 0.09761 0.09761 0.6229 -0.7612 -0.6215 -0.1203 -0.3979 -0.7612 -0.2688 -0.7612 -0.3979 -0.5778 -0.811 -0.811 0.5753 -0.811 -0.3979 -0.3979 -0.6439; -0.5995 -0.811 -0.6093 -0.6093 0.1763 -0.811 -0.6093 -0.6093 -0.811 -0.4941 -0.7859 -0.811 -0.7612 -0.6215 -0.5353 -0.811 -0.6093 -0.5995 -0.5778 -0.2863; -0.2232 -0.2232 -0.5146 0.263 -0.2232 -0.737 -0.811 -0.6666 0.1763 0.1635 -0.4739 -0.2232 0.5261 0.5558 0.4436 -0.7131 -0.2232 -0.2232 -0.5995 -0.737; 0.5059 0.4436 -0.2349 -0.2349 -0.1203 -0.2349 -0.2349 -0.3585 -0.737 -0.7859 -0.1047 0.01436 0.1243 -0.7859 -0.2349 0.2987 -0.2349 -0.737 -0.811 -0.2349; 0.516 -0.3675 -0.3675 -0.304 -0.7859 -0.3675 0.4957 -0.3675 -0.3675 -0.4739 -0.3675 -0.3675 -0.3675 -0.5778 -0.5146 -0.2009 -0.737 -0.7131 -0.304 -0.811; -0.2489 0.5753 -0.2489 -0.2489 -0.5995 -0.2489 0.5558 -0.811 -0.2489 -0.2489 -0.7612 -0.2489 0.263 -0.6897 -0.3959 -0.5995 -0.811 0.5361 -0.2489 -0.2489; 0.2265 -0.328 -0.328 -0.328 -0.328 -0.328 -0.454 -0.328 -0.328 -0.328 -0.328 -0.328 -0.7612 -0.04394 -0.7131 -0.811 -0.415 -0.1361 -0.7859 0.6135; -0.811 -0.4688 -0.304 -0.811 -0.4688 -0.3771 -0.7612 -0.4688 -0.152 -0.4688 -0.4688 -0.4688 -0.4344 -0.4688 -0.6666 -0.4688 -0.4688 -0.4688 -0.4688 0.09761; -0.3384 -0.3384 0.4957 -0.3384 -0.3384 -0.0145 -0.3384 -0.3384 -0.3384 -0.3384 0.1506 -0.3384 -0.3384 -0.811 -0.5146 -0.7859 -0.7131 -0.3384 -0.3384 -0.5146; -0.3846 -0.3846 -0.3846 -0.3846 -0.2515 -0.737 -0.3846 -0.5564 -0.3846 0.09761 -0.3846 -0.3846 -0.3846 -0.3846 -0.3846 -0.3846 -0.3846 -0.4739 -0.1047 -0.6666; -0.4036 -0.4036 -0.4036 -0.4036 -0.5778 -0.4036 -0.4036 -0.7859 -0.7612 -0.4036 -0.7859 -0.4036 -0.4036 -0.4036 -0.4036 0.275 -0.4036 0.2141 -0.4036 -0.4036; -0.6108 -0.6108 -0.6108 -0.7131 -0.6108 -0.6108 -0.6215 -0.4739 -0.4344 -0.6108 -0.6108 -0.6108 -0.6108 -0.6108 -0.6108 -0.811 -0.6108 -0.6108 -0.6108 -0.6108; 0.1243 0.251 -0.2699 -0.6439 -0.2699 -0.2699 -0.2699 -0.2699 -0.2699 -0.2699 -0.2699 -0.2699 -0.2699 -0.2699 -0.811 -0.2699 -0.2699 -0.2699 -0.2699 -0.2699; -0.7074 -0.6215 -0.7074 -0.7074 -0.7074 -0.7074 -0.811 -0.7074 -0.7074 -0.7074 -0.7074 -0.7074 -0.7074 -0.6897 -0.7074 -0.7074 -0.7074 -0.7074 -0.7074 -0.7074; -0.2445 -0.2445 -0.2445 -0.2445 -0.2445 -0.2445 -0.2445 0.3219 -0.2445 -0.811 -0.2445 -0.2445 -0.2445 -0.2445 -0.2445 -0.2445 -0.2445 -0.2445 -0.2445 -0.2445; -0.7131 -0.7131 -0.7131 -0.7131 -0.7131 -0.7131 -0.7131 -0.7131 -0.7131 -0.7131 -0.7131 -0.7131 -0.7131 -0.7131 -0.7131 -0.7131 -0.7131 -0.7131 -0.7131 -0.7131; 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0]

	filepath = joinpath(@__DIR__, "data/data_missing.tsv")

	varIds = [Symbol("V$i") for i=1:40]
	sampleIds = [string('S',lpad(i,2,'0')) for i=1:20]
	groupAnnot = repeat(["A","B"],inner=10)
	timeAnnot = vcat(0.01:0.01:0.1, 0.01:0.01:0.1)

	app = MiniApp()
	init(app)

	# Open file
	setvalue(app, Any["samplefilepath", filepath])
	setvalue(app, Any["lastsampleannot", "Time"])
	setvalue(app, Any["normalizemethod", "None"])
	put!(app.fromGUI, :normalize=>[])
	@test runall(app)
	sampleData = app.jg.scheduler.jobs[app.jg.normalizeID].result
	@test sampleData.data ≈ reconstructed

	setvalue(app, Any["logtransform", "true"])
	setvalue(app, Any["logtransformoffset", "0.57"])
	put!(app.fromGUI, :normalize=>[])
	@test runall(app)
	sampleData = app.jg.scheduler.jobs[app.jg.normalizeID].result
	@test sampleData.data ≈ logReconstructed atol=1e-3
end

end