include("LatticeVars.jl")

function main()
    LatVecs = zeros(3,3)
    LatVecs[1,:] = [5.0, 5.0, 0.0]
    LatVecs[2,:] = [5.0, 0.0, 5.0]
    LatVecs[3,:] = [0.0, 6.0, 5.0]

    lattice_vars = LatticeVars( LatVecs )
    avec = lattice_vars.avec
    bvec = lattice_vars.bvec
    res = (avec*bvec')/(2*pi)
    display(res); println()

    println("Pass here")
end

main()


abstract type AbstractMap end
Line_Circle = Union{Line,Circle}

"""
	(type) Möbius
Representation of a Möbius or bilinear transformation.
"""
struct Möbius <: AbstractMap
	# interpreted as [a,b,c,d], where f(z)=(az+b)/(cz+d)
	coeff::SVector{4}
end

#
# Construction
#

const Mobius = Möbius   # for us lazy Americans
"""
	Möbius(a,b,c,d)
Construct the `Möbius` map ``z ↦ (az+b)/(cz+d)`` by giving its coefficients.
"""
Möbius(a::Number,b::Number,c::Number,d::Number) = Möbius(SVector(a,b,c,d))

"""
	Möbius(A::AbstractMatrix)
Construct the `Möbius` map ``z ↦ (az+b)/(cz+d)`` by giving a matrix `A==[a b;c d]`.
"""
Möbius(A::AbstractMatrix) = Möbius(SVector(A[1,1],A[1,2],A[2,1],A[2,2]))

"""
	Möbius(z::AbstractVector,w::AbstractVector)
Construct the `Möbius` map that transforms the points `z[k]` to `w[k]` for k=1,2,3.
Values of `Inf` are permitted in both vectors.
"""
function Möbius(source::AbstractVector,image::AbstractVector)
	# finds the coeffs of map from (0,1,Inf) to given points
	function standard(x,y,z)
		if isinf(x)
			return z,y-z,1,0
		elseif isinf(y)
			return -z,x,-1,1
		elseif isinf(z)
			return y-x,x,0,1
		else
			xy,yz = y-x,z-y
			return z*xy,x*yz,xy,yz
		end
	end

	za,zb,zc,zd = standard(source...)
	wa,wb,wc,wd = standard(image...)
	A = SMatrix{2,2}(wa,wc,wb,wd)*SMatrix{2,2}(zd,-zc,-zb,za)
	return Möbius(A[1,1],A[1,2],A[2,1],A[2,2])
end

"""
	Möbius(C1,C2)
Construct a `Möbius` map that transforms the curve `C1` to `C2`. Both curves must be either a `Line` or `Circle`. (These maps are not uniquely determined.)
"""
Möbius(c1::Line_Circle,c2::Line_Circle) = Möbius(point(c1,[0,0.25,0.5]),point(c2,[0,0.25,0.5]))

#
# Evaluation
#
"""
	f(z::Number)
Evaluate the `Möbius` map `f` at a real or complex value `z`.
"""
function (f::Möbius)(z::Number)
	a,b,c,d = f.coeff
	if isinf(z)
		num,den = a,c
	else
		num,den = a*z+b,c*z+d
	end
	# note: 1 over complex zero is NaN, not Inf
	iszero(den) ? complex(abs(num)/abs(den)) : num/den
end

"""
	f(C::Union{Circle,Line})
Find the image of the circle or line `C` under the `Möbius` map `f`. The result is also either a `Circle` or a `Line`.
"""
(f::Möbius)(C::Line_Circle) = Circle( f.(point(C,[0,0.25,0.5]))... )
"""
	f(C::Union{Arc,Segment})
Find the image of the arc or segment `C` under the `Möbius` map `f`. The result is also either an `Arc` or a `Segment`.
"""
(f::Möbius)(C::Union{Arc,Segment}) = Arc( f.(point(C,[0,0.5,1]))... )

"""
	f(R::Union{AbstractDisk,AbstractHalfplane})
If `R` is an `AbstractDisk` or an `AbstractHalfplane`, find its image under the `Möbius` map `f`. The result is also either an `AbstractDisk` or an `AbstractHalfplane`.
# Examples
```julia-repl
julia> f = Möbius(Line(-1,1),Circle(0,1))
Möbius transformation:

   (1.0 + 0.9999999999999999im) z + (3.666666666666666 - 1.666666666666667im)
   ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
   (1.0 + 0.9999999999999999im) z + (-1.666666666666666 + 3.6666666666666665im)

julia> f(upperhalfplane)
Disk interior to:
   Circle(-5.55112e-17+2.22045e-16im,1.0)

julia> isapprox(ans,unitdisk)
true
```
"""
(f::Möbius)(R::Union{AbstractDisk,AbstractHalfplane}) = interior(f(R.boundary))

"""
	inv(f::Möbius)
Find the inverse of a `Möbius` transformation. This is the functional inverse, not 1/f(z).
"""
inv(f::Möbius) = Möbius(f.coeff[4],-f.coeff[2],-f.coeff[3],f.coeff[1])

"""
	∘(f::Möbius,g::Möbius)
Compose two `Möbius` transformations.
"""
function ∘(f::Möbius,g::Möbius)
	A = SMatrix{2,2}(f.coeff[1],f.coeff[3],f.coeff[2],f.coeff[4])
	B = SMatrix{2,2}(g.coeff[1],g.coeff[3],g.coeff[2],g.coeff[4])
	C = A*B
	Möbius(C[1,1],C[1,2],C[2,1],C[2,2])
end

#
# Display
#

function show(io::IO,f::Möbius)
	a,b,c,d = f.coeff
	print(IOContext(io,:compact=>true),"Möbius map z --> (($a)*z + $b) / (($c)*z + $d)")
end

function show(io::IO,::MIME"text/plain",f::Möbius)
	a,b,c,d = [repr("text/plain",x) for x in f.coeff]
	numer = "("*a*") z + ("*b*")"
	denom = "("*c*") z + ("*d*")"
	hline = reduce(*,fill("–",max(length(numer),length(denom))))
	print(io,"Möbius transformation:\n\n   ",numer,"\n   ",hline,"\n   ",denom)
end


@testset "cdp.jl" begin

    @testset "Deterministic optimal growth" begin
        alpha = 0.65
        beta = 0.95
        f(s, x) = log(x)
        g(s, x, e) = s^alpha - x
        shocks = [1.]
        weights = [1.]
        x_lb(s) = 0
        x_ub(s) = s

        # Analytical solution
        ab = alpha * beta
        c1 = (log(1 - ab) + log(ab) * ab / (1 - ab)) / (1 - beta)
        c2 = alpha / (1 - ab)
        v_star(s) = c1 + c2 * log(s)
        x_star(s) = (1 - ab) * s^alpha

        # Analytical state path
        s_init = 0.1
        ts_length = 25
        s_path_star = Array{Float64}(undef, ts_length)
        s_path_star[1] = s_init
        shock = 1.  # Arbitrary
        for t in 1:ts_length-1
            s_path_star[t+1] = g(s_path_star[t], x_star(s_path_star[t]), shock)
        end

        # Bases
        bases = Basis[]

        # Chebyshev
        n = 30
        s_min, s_max = 0.1, 2.
        push!(bases, Basis(ChebParams(n, s_min, s_max)))

        # Spline
        k = 3
        m = 101
        breaks = m - (k-1)
        s_min, s_max = 0.1, 2.
        push!(bases, Basis(SplineParams(breaks, s_min, s_max, k)))

        methods = [PFI, VFI]

        for basis in bases
            cdp = ContinuousDP(f, g, beta, shocks, weights, x_lb, x_ub, basis)

            for method in methods
                # solve
                tol = sqrt(eps())
                max_iter = 500
                res = @inferred(solve(cdp, method, tol=tol, max_iter=max_iter))

                rtol = 1e-5
                @test isapprox(res.V, v_star.(cdp.interp.S); rtol=rtol)
                @test isapprox(res.X, x_star.(cdp.interp.S); rtol=rtol)

                # set_eval_nodes!
                grid_size = 200
                eval_nodes = collect(range(s_min, stop=s_max, length=grid_size))
                set_eval_nodes!(res, eval_nodes);

                # simulate
                s_path = @inferred(simulate(res, s_init, ts_length))
                atol = 1e-5
                @test isapprox(s_path[end], s_path_star[end]; atol=atol)
            end
        end

        @testset "Test warning" begin
            cdp = ContinuousDP(
                f, g, beta, shocks, weights, x_lb, x_ub, bases[1]
            )
            for max_iter in [0, 1]
                @test_logs (:warn, r".*max_iter.*")
                           solve(cdp, max_iter=max_iter)
           end
        end
    end

end


export DimRedux, GaussDimRedux, SparseDimRedux, SSRFTDimRedux, *

### Dimension reduction operators

"""DimRedux is an abstract dimension reduction map
over the field F, which is either the real or complex numbers"""
abstract type DimRedux{F<:Number} end

"""By default, a DimRedux map reduces a matrix of data via left or right multiplication by its internal variable Ξ.
We extend the multiplication method * to allow multiplication by dimension reduction maps"""
*{F<:Number}(dr::DimRedux{F}, M::Matrix{F}) = dr.Ξ * M
*{F<:Number}(M::Matrix{F}, dr::DimRedux{F}) = M * dr.Ξ' #' nb: different from paper

"""GaussDimRedux is a kind of dimension reduction map
It reduces data over the field F using the Gaussian random matrix Ξ"""
type GaussDimRedux{F<:Number}<:DimRedux{F}
    Ξ::Matrix{F}
end

"""Constructs a GaussDimRedux map which reduces data in F^n to F^k
By default, the field F is a real (floating point) number"""
function GaussDimRedux{F<:Number}(k::Int, n::Int, ::Type{F}=Float64)
    if F<:Real
        Ξ = randn(k,n)
    elseif F<:Complex
        Ξ = randn(k,n) + im*randn(k,n)
    else error("GaussDimRedux not defined over the field $F")
    end
    return GaussDimRedux(Ξ)
end

"""SparseDimRedux is a kind of dimension reduction map
It reduces data over the field F using the sparse random matrix Ξ"""
type SparseDimRedux{F<:Number}<:DimRedux{F}
    Ξ::SparseMatrixCSC{F}
end

"""Constructs a SparseDimRedux map which reduces data in F^n to F^k
By default, the field F is the real numbers
p is the sparsity level, with default level .1 (???)"""
function SparseDimRedux{F<:Number}(k::Int, n::Int, p::Float64=.1, ::Type{F}=Real)
    if F<:Real
        Ξ = sprandn(k,n,p) # nb --- not defined in paper
    elseif F<:Complex
        Ξ = sprandn(k,n,p) + im*sprandn(k,n,p) # nb --- not defined in paper
    else error("SparseDimRedux not defined over the field $F")
    end
    return SparseDimRedux(Ξ)
end

"""SSRFTDimRedux is a kind of dimension reduction map
It reduces data over the field F using a scrambled SRFT
Its internal variables are
the permutations Π1 and Π2, the random signs ϵ1 and ϵ2, and the coordinate restriction R"""
type SSRFTDimRedux{F<:Number}<:DimRedux{F}
    Π1::Array{Int,1} # first permutation
    ϵ1::Array{F,1} # first random signs
    Π2::Array{Int,1} # second permutation
    ϵ2::Array{F,1} # second random signs
    R::Array{Int,1} # restriction onto k coordinates
end

"""Constructs a SSRFTDimRedux map which reduces data in F^n to F^k
By default, the field F is the real numbers"""
function SSRFTDimRedux{F<:Number}(k::Int, n::Int, ::Type{F}=Float64)
    Π1 = randperm(n) # a random permutation of {1,...,n}
    Π2 = randperm(n) # a random permutation of {1,...,n}
    R = randperm(n)[1:k] # a random selection of k elements from {1,...,n}
    if F<:Real
        ϵ1 = one(F) * rand([-1, 1], n) # a random ±1 vector of length n in the field F
        ϵ2 = one(F) * rand([-1, 1], n) # a random ±1 vector of length n in the field F
    elseif F<:Complex
        ϵ1 = one(F) * rand([-1, 1], n) + im*one(F) * rand([-1, 1], n) # a random ±1±i vector of length n in the field F
        ϵ2 = one(F) * rand([-1, 1], n) + im*one(F) * rand([-1, 1], n) # a random ±1±i vector of length n in the field F
    else error("SSRFTDimRedux not defined over the field $F")
    end
    return SSRFTDimRedux(Π1,ϵ1,Π2,ϵ2,R)
end

"""The SSRFTDimRedux map uses an efficient specialized method to reduce a matrix of data,
multiplying it on the left by Ξ = R FFT ϵ2 Π2 FFT ϵ1 Π1"""
function *{F<:Number}(dr::SSRFTDimRedux{F}, M::Matrix{F})
    B = M[dr.Π1,:] # scramble rows
    B .= dr.ϵ1 .* B # scramble signs - equivalent to diagm(dr.ϵ1) * B
    if F<:Real
        B = dct(B)
    elseif F<:Complex
        B = fft(B)
    end
    B = B[dr.Π2,:] # scramble rows
    B .= dr.ϵ2 .* B # scramble signs - equivalent to diagm(dr.ϵ2) * B
    if F<:Real
        B = dct(B)
    elseif F<:Complex
        B = fft(B)
    end
    B = B[dr.R,:] # select rows
    return B
end
"""The SSRFTDimRedux map uses an efficient specialized method to reduce a matrix of data,
multiplying it on the right by Ξ' = (R FFT ϵ2 Π2 FFT ϵ1 Π1)'"""
function *{F<:Number}(M::Matrix{F}, dr::SSRFTDimRedux{F})
  B = M[:,dr.Π1] # scramble columns
  B .= B .* dr.ϵ1' # scramble signs - equivalent to B * diagm(dr.ϵ1)
  if F<:Real
      B = dct(B)
  elseif F<:Complex
      B = ifft(B)
  end
  B = M[:,dr.Π2] # scramble columns
  B .= B .* dr.ϵ2' # scramble signs - equivalent to B * diagm(dr.ϵ1)
  if F<:Real
      B = dct(B)
  elseif F<:Complex
      B = ifft(B)
  end
  B = B[:,dr.R] # select columns
  return B
end


export StraightLineHomotopy

"""
    StraightLineHomotopy(G::System, F::System; gamma = 1.0)
    StraightLineHomotopy(G::AbstractSystem, F::AbstractSystem; gamma = 1.0)

Constructs the straight line homotopy ``H(x, t) = γ t G(x) + (1-t) F(x)`` where
``γ`` is `gamma`.
"""
struct StraightLineHomotopy{S<:AbstractSystem,T<:AbstractSystem} <: AbstractHomotopy
    start::S
    target::T
    γ::ComplexF64

    u::Vector{ComplexF64}
    ū::Vector{ComplexDF64}
    v̄::Vector{ComplexDF64}
    U::Matrix{ComplexF64}

    dv_start::TaylorVector{4,ComplexF64}
    dv_target::TaylorVector{4,ComplexF64}
end

function StraightLineHomotopy(
    start::System,
    target::System;
    compile::Union{Bool,Symbol} = COMPILE_DEFAULT[],
    kwargs...,
)
    StraightLineHomotopy(
        fixed(start; compile = compile),
        fixed(target; compile = compile);
        kwargs...,
    )
end
function StraightLineHomotopy(
    start::AbstractSystem,
    target::AbstractSystem;
    γ = 1.0,
    gamma = γ,
)
    size(start) == size(target) || throw(
        ArgumentError(
            "Start and target do not have the same size, got $(size(start)) and $(size(target))",
        ),
    )

    m, n = size(start)
    u = zeros(ComplexF64, m)
    ū = zeros(ComplexDF64, m)
    v̄ = zeros(ComplexDF64, m)
    U = zeros(ComplexF64, m, n)

    dv_start = TaylorVector{4}(ComplexF64, m)
    dv_target = TaylorVector{4}(ComplexF64, m)

    StraightLineHomotopy(
        start,
        target,
        ComplexF64(gamma),
        u,
        ū,
        v̄,
        U,
        dv_start,
        dv_target,
    )
end

Base.size(H::StraightLineHomotopy) = size(H.start)

function Base.show(io::IO, mime::MIME"text/plain", H::StraightLineHomotopy)
    println(io, typeof(H), ":")
    println(io, "γ: ", H.γ)
    println(io, "\nG: ")
    show(io, mime, H.start)
    println(io, "\n\nF:")
    show(io, mime, H.target)
end

function ModelKit.evaluate!(
    u,
    H::StraightLineHomotopy,
    x::Vector{ComplexDF64},
    t,
    p = nothing,
) where {T}
    evaluate!(H.v̄, H.start, x)
    evaluate!(H.ū, H.target, x)
    ts, tt = H.γ .* t, 1 - t
    for i = 1:size(H, 1)
        @inbounds u[i] = ts * H.v̄[i] + tt * H.ū[i]
    end
end

function ModelKit.evaluate!(u, H::StraightLineHomotopy, x, t, p = nothing)
    evaluate!(u, H.start, x)
    evaluate!(H.u, H.target, x)
    ts, tt = H.γ .* t, 1 - t
    for i = 1:size(H, 1)
        @inbounds u[i] = ts * u[i] + tt * H.u[i]
    end
    u
end

function ModelKit.evaluate_and_jacobian!(
    u,
    U,
    H::StraightLineHomotopy,
    x::AbstractVector{T},
    t,
    p = nothing,
) where {T}
    evaluate_and_jacobian!(u, U, H.start, x)
    evaluate_and_jacobian!(H.u, H.U, H.target, x)
    ts, tt = H.γ .* t, 1 - t
    for i = 1:size(H, 1)
        @inbounds u[i] = ts * u[i] + tt * H.u[i]
    end
    for j = 1:size(H, 2), i = 1:size(H, 1)
        @inbounds U[i, j] = ts * U[i, j] + tt * H.U[i, j]
    end
    nothing
end
function ModelKit.taylor!(u, ::Val{1}, H::StraightLineHomotopy, x, t)
    evaluate!(u, H.start, x)
    evaluate!(H.u, H.target, x)
    for i = 1:size(H, 1)
        @inbounds u[i] = H.γ * u[i] - H.u[i]
    end
    u
end
function ModelKit.taylor!(
    u,
    v::Val{K},
    H::StraightLineHomotopy,
    tx::TaylorVector,
    t,
) where {K}
    taylor!(H.dv_start, v, H.start, tx)
    taylor!(H.dv_target, v, H.target, tx)

    for i = 1:size(H, 1)
        start = H.γ * (H.dv_start[i, K] + t * H.dv_start[i, K+1])
        target = (1 - t) * H.dv_target[i, K+1] - H.dv_target[i, K]
        u[i] = start + target
    end
    u
end


# ------------------------------------------------------------------
# Licensed under the MIT License. See LICENSE in the project root.
# ------------------------------------------------------------------

"""
    BallPartition(radius; metric=Euclidean())

A method for partitioning spatial objects into balls of a given
`radius` using a `metric`.
"""
struct BallPartition{T,M} <: SPredicatePartitionMethod
  radius::T
  metric::M
end

BallPartition(radius::T; metric::M=Euclidean()) where {T,M} =
  BallPartition{T,M}(radius, metric)

(p::BallPartition)(x, y) = evaluate(p.metric, x, y) < p.radius


@inline function abs_db_db(x::DoubleFloat{T}) where {T<:IEEEFloat}
    return DoubleFloat{T}(abs_dd_dd(HILO(x)))
end

@inline function neg_db_db(x::DoubleFloat{T}) where {T<:IEEEFloat}
    return DoubleFloat{T}(neg_dd_dd(HILO(x)))
end

@inline function negabs_db_db(x::DoubleFloat{T}) where {T<:IEEEFloat}
    return DoubleFloat{T}(negabs_dd_dd(HILO(x)))
end

@inline function inv_db_db(x::DoubleFloat{T}) where {T<:IEEEFloat}
    return DoubleFloat{T}(inv_dd_dd(HILO(x)))
end

@inline function square_db_db(x::DoubleFloat{T}) where {T<:IEEEFloat}
    return DoubleFloat{T}(square_dd_dd(HILO(x)))
end

@inline function cube_db_db(x::DoubleFloat{T}) where {T<:IEEEFloat}
    return DoubleFloat{T}(cube_dd_dd(HILO(x)))
end

@inline function sqrt_db_db(x::DoubleFloat{T}) where {T<:IEEEFloat}
    return DoubleFloat{T}(sqrt_dd_dd(HILO(x)))
end

@inline function cbrt_db_db(x::DoubleFloat{T}) where {T<:IEEEFloat}
    return DoubleFloat{T}(cbrt_dd_dd(HILO(x)))
end


#=
     sqrt(eps(Float64)) == ldexp(0.5, -25) ~ 1.5e-8
     isapprox here uses ldexp(0.5, -30) ~ 1.0e-9 ,   so the system default (sqrt(eps(T))) is ~32x less sensitive 5bits
                        ldexp(0.5, -31) ~ 2.3e-10,                                           ~64x less sensitive 6bits
                                                                                (32.5 sig bits must match)

=#
releps(::Type{Float64}) = ldexp(0.5, -31)
releps(::Type{Float32}) = ldexp(inv(sqrt(2.0f0)), -17)
releps(::Type{Double64}) = ldexp(0.5, -62)
releps(::Type{Double32}) = ldexp(inv(sqrt(2.0f0)), -34)

function Base.isapprox(x::DoubleFloat{T}, y::T; atol::Real=0.0, rtol::Real=atol>0 ? 0 : releps(T), nans::Bool=false, norm::Function=norm) where {T<:IEEEFloat}
    return isapprox(x, DoubleFloat{T}(y), atol=atol, rtol=rtol, nans=nans, norm=norm)
end
function Base.isapprox(x::T, y::DoubleFloat{T}; atol::Real=0.0, rtol::Real=atol>0 ? 0 : releps(T), nans::Bool=false, norm::Function=norm) where {T<:IEEEFloat}
    return isapprox(DoubleFloat{T}(x), y, atol=atol, rtol=rtol, nans=nans, norm=norm)
end
function Base.isapprox(x::DoubleFloat{T}, y::DoubleFloat{T}; atol::Real=0.0, rtol::Real=atol>0 ? 0 : releps(DoubleFloat{T}), nans::Bool=false, norm::Function=norm) where {T<:IEEEFloat}
    x == y || (isfinite(x) && isfinite(y) && abs(x-y) <= max(max(1.0e-32, atol), rtol*max(abs(x), abs(y)))) || (nans && isnan(x) && isnan(y))
end

Base.isapprox(x::DoubleFloat{T}, y::F; atol::Real=0.0, rtol::Real=atol>0.0 ? 0.0 : eps(max(abs(x), abs(y)))^(37/64), nans::Bool=false, norm::Function=norm) where {T<:IEEEFloat, F<:Real} =
    isapprox(promote(x, y)..., atol=atol, rtol=rtol, nans=nans, norm=norm)

Base.isapprox(x::F, y::DoubleFloat{T}; atol::Real=0.0, rtol::Real=atol>0.0 ? 0.0 : eps(max(abs(x), abs(y)))^(37/64), nans::Bool=false, norm::Function=norm) where {T<:IEEEFloat, F<:Real} =
    isapprox(promote(x, y)..., atol=atol, rtol=rtol, nans=nans, norm=norm)

function Base.lerpi(j::Integer, d::Integer, a::DoubleFloat{T}, b::DoubleFloat{T}) where {T}
    t = DoubleFloat{T}(j)/d
    a = fma(-t, a, a)
    return fma(t, b, a)
end

function Base.clamp(x::DoubleFloat{T}, lo::DoubleFloat{T}, hi::DoubleFloat{T}) where {T}
     lo <= x <= hi && return x
     lo <= x && return hi
     return lo
end
function Base.clamp(x::DoubleFloat{T}, lo::T, hi::T) where {T}
     lo <= x <= hi && return x
     lo <= x && return hi
     return lo
end
function Base.clamp(x::T, lo::DoubleFloat{T}, hi::DoubleFloat{T}) where {T}
     lo <= x <= hi && return x
     lo <= x && return hi
     return lo
end

abs2(x::DoubleFloat{T}) where {T} =
    let absx = abs(x)
        absx * absx
    end

# for compatibility with old or unrevised outside linalg functions
function Base.:(+)(v::Vector{DoubleFloat{T}}, x::T) where {T}
    return v .+ x
end
function Base.:(-)(v::Vector{DoubleFloat{T}}, x::T) where {T}
    return v .- x
end
function Base.:(+)(m::Matrix{DoubleFloat{T}}, x::T) where {T}
    return m .+ x
end
function Base.:(-)(m::Matrix{DoubleFloat{T}}, x::T) where {T}
    return m .- x
end


@testset "Boolean Decision Diagrams" begin
    l1 = pliteral(1)
    @show l1
    @show ¬l1
    @show apply(~,l1)
    @show nliteral(1)
    @show l1 ∧ (¬l1)
    @test value(l1 ∧ (¬l1)) == false
    @show l1 ∨ (¬l1)
    @test value(l1 ∨ (¬l1)) == true
    @info "Fig. 7 in Bryant's paper"
    l2, l3 = pliteral(2), pliteral(3)
    @show ¬(l1 ∧ l3) ∨ (l2 ∧ l3)
    println()
end # testset


function f(x::Array{Float64,2})
  x[1]^2 + x[2]^2
end
function gradient(x::Array{Float64,2})
  [2*x[1] 2*x[2]]
end

# Initial conditions
x0 = [0.0 0.0]

# Tuning parameters
epsilon = .001  # step size
L = 10          # number of steps

# Basic tests on regular interface
x,gx = hmc_sampler(x0, f, gradient, epsilon, L)

@assert isa(x,Array{Float64,2})
@assert isa(gx,Float64)

# Basic tests on Density interface
dd = DifferentiableDensity(f,gradient)
x,gx = hmc_sampler(x0, dd, epsilon, L)

@assert isa(x,Array{Float64,2})
@assert isa(gx,Float64)

# Example of using Options with an HMC sampler allowing bounds
opts = Options(:stepsize,0.01,
               :numsteps,int(50),
               :bounded,[1],          # list of dimensions that are bounded
               :lower_bounds,[0],  # lower bound for 1st dim
               :upper_bounds,[Inf])   # upper bound for 1st dim
x,gx = bounded_hmc_sampler(x0, dd, opts)

@assert isa(x,Array{Float64,2})
@assert isa(gx,Float64)
@assert x[1] > 0


function puzzle20(path::String=joinpath(@__DIR__, "..", "data/20.txt"))
    data = []
    for line in readlines(path)
        arr = []
        for m in eachmatch(r"-?[0-9]+", line)
            val = string(m.match)
            try
                val = parse(Int, val)
            catch
                val = Symbol(val)
            end
            append!(arr, val)
        end
        append!(data, [arr])
    end
    # data = map(x->parse.(x), matchall.(r"-?[0-9]+", readlines(path)))
    iter = 0
    closest = []
    collided = falses(length(data))
    positions = [x[1:3] for x in data]
    velocity = [x[4:6] for x in data]
    acceleration = [x[7:9] for x in data]
    while true
        # Positions are updated simultaneously
        velocity += acceleration
        positions += velocity
        distances = [sum(abs.(x)) for x in positions]
        append!(closest, findmin(distances)[2])
        iter += 1
        # Part 2: determine collisons
        # for each particle that has not yet collided
        # check that it does not match the position of any other particle
        # and that the other particle has not already been collided
        for i in findall(.!collided)
            # check if is collided with any other non collided
            for j in findall(.!collided)
                if i == j
                    continue
                elseif positions[i]==positions[j]
                    collided[i] = true
                    collided[j] = true
                end
            end
        end
        # Check if we should end the loop
        if iter > 1_000
            if closest[iter] == closest[iter-1]
                break
            end
        end
    end
    partone = last(closest)-1
    parttwo = sum(.!collided)
    return [partone, parttwo]
end


using PiecewiseLinearOpt
using Base.Test

using JuMP, Cbc

const solver = CbcSolver()
methods_1D = (:CC,:MC,:Logarithmic,:LogarithmicIB,:ZigZag,:ZigZagInteger,:SOS2,:GeneralizedCelaya,:SymmetricCelaya,:Incremental,:DisaggLogarithmic)
methods_2D = (:CC,:MC,:Logarithmic,:LogarithmicIB,:ZigZag,:ZigZagInteger,:SOS2,:GeneralizedCelaya,:SymmetricCelaya,:DisaggLogarithmic)

let d = linspace(1,2π,8), f = sin
    for method in methods_1D
        println("Method: $method")
        model = Model(solver=solver)
        @variable(model, x)
        z = piecewiselinear(model, x, d, sin, method=method)
        @objective(model, Max, z)
        @test solve(model) == :Optimal
        @test isapprox(getvalue(x), 1.75474, rtol=1e-4)
        @test isapprox(getvalue(z), 0.98313, rtol=1e-4)

        @constraint(model, x ≤ 1.5z)
        @test solve(model) == :Optimal
        @test isapprox(getvalue(x), 1.36495, rtol=1e-4)
        @test isapprox(getvalue(z), 0.90997, rtol=1e-4)
    end
end

let dˣ = linspace(0,1,8), dʸ = linspace(0,1,8), f = (x,y) -> 2*(x-1/3)^2 + 3*(y-4/7)^4
    for method in methods_2D
        println("Method: $method")
        model = Model(solver=solver)
        @variable(model, x)
        @variable(model, y)
        # z = piecewiselinear(model, x, y, dˣ, dʸ, f, method=method)
        z = piecewiselinear(model, x, y, BivariatePWLFunction(dˣ, dʸ, f, pattern=:UnionJack), method=method)
        @objective(model, Min, z)
        @test solve(model) == :Optimal
        @test isapprox(getvalue(x), 0.285714, rtol=1e-4)
        @test isapprox(getvalue(y), 0.571429, rtol=1e-4)
        @test isapprox(getvalue(z), 0.004535, atol=1e-4)

        @constraint(model, x ≥ 0.6)
        @test solve(model) == :Optimal
        @test isapprox(getvalue(x), 0.6, rtol=1e-4)
        @test isapprox(getvalue(y), 0.571428, rtol=1e-4)
        @test isapprox(getvalue(z), 0.148753, rtol=1e-4)
    end
end


using Factotum, Statistics, LinearAlgebra, Test
T, n, r = (200, 10, 6)
x = rand(T,n)                  # generate data
fm = Factotum.FactorModel(x, 6; scale = true)      # fit factor model

F = Factotum.factors(fm)
Λ = Factotum.loadings(fm)
σ = Factotum.sdev(fm)
## Check diagonality cov(F)

Σ = cov(F; corrected = false);

@test Σ ≈ diagm(0=>σ.^2)
@test Λ'Λ/n ≈ diagm(0=>ones(r))

## Replicate Ng and Bai (2002)
function simulate_factormodel(r, T, N)
    F = randn(T,r)
    Λ = randn(r,N)
    e = sqrt(r).*randn(T,N)
    F*Λ .+ e
end

X = simulate_factormodel(1, 251, 78)
fm = Factotum.FactorModel(X, 10; scale = true)
Factotum.informationcriteria((IC1, IC2), fm, 10)

X = simulate_factormodel(3, 100, 20)
fm = Factotum.FactorModel(X, 10; scale = true)
ic = Factotum.informationcriteria((IC1, IC2), fm, 10)

findmin(ic)
## Test the proportion of the variance explained


function cep_coeffs(n::Order,q::Number,N::Integer)
    iszero(q) && return q0_coeffs1(n,q,N)
    C1 = mat_C1(q,N) # Generate matrix
    Au = coeffs(C1,n)
    Au[1,:] .*= √0.5 # Renormalize eigenvectors
    return Au
end

function cea_coeffs(n::Order,q::Number,N::Integer)
    iszero(q) && return q0_coeffs2(n,q,N)
    C2 = mat_C2(q,N) # Generate matrix
    return coeffs(C2,n)
end

function sea_coeffs(n::Order,q::Number,N::Integer)
    iszero(q) && return q0_coeffs2(n,q,N)
    C3 = mat_C3(q,N) # Generate matrix
    return coeffs(C3,n)
end

function sep_coeffs(n::Order,q::Number,N::Integer)
    iszero(q) && return q0_coeffs2(n,q,N)
    C4 = mat_C4(q,N) # Generate matrix
    return coeffs(C4,n)
end

function coeffs(C::AbstractMatrix,n::Order)
    u = sort!(unique(n))
    au = eigvals(C)[u.+1] # Calculate characteristic values
    #au = LAPACK.stebz!('A','E',0.0,0.0,0,0,2*eps(C.ev[1]),C.dv,C.ev)[1][u+1]
    Au = eigvecs(C,au) # Calculate eigenvectors
    Au .*= signp.(sum(Au,dims=1)) # Make sure we have the correct sign
    return Au[:,indexin(checkvec(n),u)]
end

function q0_coeffs1(n::Order,q::Number,N::Integer)
    A = zeros(typeof(blasfloat(q)),N,length(n))
    for (i,n) in enumerate(n); A[n+1,i] = n==0 ? 1/√(eltype(A)(2)) : 1; end
    return A
end

function q0_coeffs2(n::Order,q::Number,N::Integer)
    A = zeros(typeof(blasfloat(q)),N,length(n))
    for (i,n) in enumerate(n); A[n+1,i] = one(eltype(A)); end
    return A
end

function per_coeffs(m::Order,q::Number,N::Integer)
    ie = map(iseven,m)
    all(ie)  && return cep_coeffs(div.(m,2),q,N)
    !any(ie) && return sep_coeffs(div.(m,2),q,N)
    P1 = cep_coeffs(div.(filter(iseven,m),2),q,N)
    P2 = sep_coeffs(div.(filter(isodd, m),2),q,N)
    P = similar(P1,N,length(m))
    P[:,ie] = P1
    P[:,.!ie] = P2
    return P
end


include("../utils.jl")
include("group_utils.jl")

@testset "Translation action" begin
    M = Euclidean(2,3)
    G = TranslationGroup(2,3)
    A = TranslationAction(Euclidean(2,3), G)

    @test repr(A) == "TranslationAction($(repr(M)), $(repr(G)), LeftAction())"
    @test repr(switch_direction(A)) == "TranslationAction($(repr(M)), $(repr(G)), RightAction())"

    types_a = [
        Matrix{Float64},
    ]

    types_m = [
        Matrix{Float64},
    ]

    @test g_manifold(A) == M
    @test base_group(A) == G
    @test base_manifold(G) == M

    for (T_A, T_M) in zip(types_a, types_m)
        a_pts = convert.(T_A, [[0.0 1.0 2.0; 2.0 4.0 1.0], [-1.0 0.0 2.0; -2.0 3.0 3.0], [1.0 1.0 2.0; 3.0 2.0 1.0]])
        m_pts = convert.(T_M, [[0.0 1.0 2.0; 2.0 4.0 1.0], [-1.0 0.0 2.0; -2.0 3.0 3.0], [1.0 1.0 2.0; 3.0 2.0 1.0]])
        v_pts = convert.(T_M, [[0.0 1.0 2.0; 2.0 4.0 1.0], [-1.0 0.0 2.0; -2.0 3.0 3.0], [1.0 1.0 2.0; 3.0 2.0 1.0]])

        atol = if eltype(T_M) == Float32
            1e-7
        else
            1e-15
        end
        @test apply_diff(A, a_pts[1], m_pts[1], v_pts[1]) === v_pts[1]
        @test inverse_apply_diff(A, a_pts[1], m_pts[1], v_pts[1]) === v_pts[1]
        test_action(A, a_pts, m_pts, v_pts;
            test_optimal_alignment = false,
            test_diff = true,
            atol = atol)
    end

end


# This code generates the 2l+l dimensional real irreducible representations of O(3).
#
# For a function defined on the sphere (f: S² → R) and represented in the basis of the
# real spherical harmonics, these matrix representations rotate/reflect the function
# about an axis in 3D space.
#
# The routines in this file are based upon:
#
#   Rotation Matrices for Real Spherical Harmonics, Direct Determination by Recursion
#   Joseph Ivanic and Klaus Ruedenberg
#   J. Phys. Chem., Vol. 100, No. 15, 1996
#
# The article contains numerous errors, and the "Additions and Corrections"
# — J. Phys. Chem. A, Vol. 102, No. 45, 1998 — also contains at least one error,
# for the matrices generated using Table 2 do not constitute an O(3) group
# homomorphism. A few empirical tests reveal that the term "sqrt(1 - δ(m,-1))" should
# probably be changed to "sqrt(1 + δ(m,-1))" in Table 2 for "V_mm'" and "m < 0".
#
# This file contains an optimized implementation of the (corrected) methods in
# the article. A reference implementation is provided in reference_irreps_O3.jl
#
# These methods are dependently typed on (and compile for) distinct values of l.
# Because of this, the first function call for a new value of l may be slow, but
# subsequent calls should be very fast.
#
# Compilation becomes rather slow beyond about l = 12, but if the goal is to rotate
# high bandwidth-limited functions on the sphere, then there exist better methods that
# do not involve explicitly constructing the entire rotation matrix.

δ(i::Int, j::Int) = i == j ? 1 : 0

function P(l::Int, i::Int, m::Int, n::Int)
    r = 2
    q = l

    if abs(n) < l
        :(R[$(r + i), $(r + 0)] * M[$(q + m), $(q + n)])
    elseif n == l
        :(R[$(r + i), $(r + 1)] * M[$(q + m), $(q + l - 1)] - R[$(r + i), $(r - 1)] * M[$(q + m), $(q - l + 1)])
    elseif n == -l
        :(R[$(r + i), $(r + 1)] * M[$(q + m), $(q - l + 1)] + R[$(r + i), $(r - 1)] * M[$(q + m), $(q + l - 1)])
    end
end

function irrep_O3_coeff(l::Int, m::Int, n::Int)
    nu = (l + m) * (l - m)
    nv = (1 + δ(m, 0)) * (l + abs(m) - 1) * (l + abs(m))
    nw = (l - abs(m) - 1) * (l - abs(m))
    d = abs(n) < l ? (l + n) * (l - n) : (2l) * (2l - 1)
    u = sqrt(nu / d)
    v = (1/2) * sqrt(nv / d) * (1 - 2δ(m, 0))
    w = (-1/2) * sqrt(nw / d) * (1 - δ(m, 0))

    U = P(l, 0, m, n)

    if m > 0
        V = :($(sqrt(1 + δ(m, 1))) * $(P(l, 1, m - 1, n)) - $(1 - δ(m, 1)) * $(P(l, -1, -m + 1, n)))
        W = :($(P(l, 1, m + 1, n)) + $(P(l, -1, -m - 1, n)))
    elseif m < 0
        V = :($(1 - δ(m, -1)) * $(P(l, 1, m + 1, n)) + $(sqrt(1 + δ(m, -1))) * $(P(l, -1, -m - 1, n)))
        W = :($(P(l, 1, m - 1, n)) - $(P(l, -1, -m + 1, n)))
    else
        V = :($(P(l, 1, 1, n)) + $(P(l, -1, -1, n)))
        W = :(0)
    end

    ex = :($(u == 0 ? 0 : :($u * ($U))) + $(v == 0 ? 0 : :($v * ($V))) + $(w == 0 ? 0 : :($w * ($W))))
    :(N[$(m + l + 1), $(n + l + 1)] = $(ex))
end

#@generated irrep_O3!{L}(N::AbstractMatrix, M::AbstractMatrix, R::AbstractMatrix, ::Type{Val{L}}) = Expr(:block, (irrep_O3_coeff(L, i, j) for i in -L:L, j in -L:L)..., :N)
@generated irrep_O3!{L}(N::AbstractMatrix, M::AbstractMatrix, R::AbstractMatrix, ::Type{Val{L}}) = Expr(:block, [irrep_O3_coeff(L, i, j) for i in -L:L, j in -L:L]..., :N)

"""
    irrep_O3!(N, M, R)

Generate an irrep of O(3) of dimension 2l+1 from irrep M of dimension 2l-1 and irrep R of dimension 3, and store it in matrix N.
"""
irrep_O3!{T}(N::AbstractMatrix{T}, M::AbstractMatrix{T}, R::AbstractMatrix{T}) = irrep_O3!(N, M, R, Val{fld(size(N, 1), 2)})

"""
    irrep_O3(M, R)

Generate an irrep of O(3) of dimension 2l+1 from irrep M of dimension 2l-1 and irrep R of dimension 3.
"""
irrep_O3{T}(M::AbstractMatrix{T}, R::AbstractMatrix{T}) = irrep_O3!(Matrix{T}(size(M, 1) + 2, size(M, 2) + 2), M, R)

"""
    irrep_O3(R, l)

Generate an irrep of O(3) of dimension 2l+1 from irrep R of dimension 3.
"""
irrep_O3{T}(R::AbstractMatrix{T}, l::Int) = l == 1 ? R : irrep_O3(irrep_O3(R, l - 1), R)


using SubstitutionTilings
using SubstitutionTilings.CoreDefs
using StructEquality
using LinearAlgebra

@def_structequal struct TM <: DGroupElem
    a::Int
end

@enum TMTile begin
    A=1
    B=2
end

function Base.:*(x :: TM, y :: TM)
    return TM(x.a + y.a)
end

function SubstitutionTilings.dilate(λ, x :: TM)
    return TM(λ*x.a)
end

function SubstitutionTilings.inv(x :: TM)
    return TM(-x.a)
end

function SubstitutionTilings.id(::Type{TM})
    return TM(0)
end

function SubstitutionTilings.is_interior(tiling :: Dict, t :: TM)
    return haskey(t.a, tiling) && haskey(t.a-1, tiling) && haskey(t.a+1, tiling)
end

function SubstitutionTilings.collar_in(tiling :: Dict, t :: TM)
    collar_shape = t*[TM(x) for x in [-2,0,2]]
    collar = []
    tiling_dict = tiling
    for s in collar_shape
        if !haskey(tiling_dict, s)
            throw(UnrecognizedCollar)
        end
        push!(collar, (s => tiling_dict[s]))
    end
    return Dict(collar)
end

thue_morse = SubSystem(Dict(A => [(TM(-1) => A), (TM(1) => B)], B => [(TM(-1) => B), (TM(1) => A)]),2)

substitute(thue_morse, [(TM(0) => A)], 4)

(collars, S) = total_collaring(thue_morse, Dict([(TM(-2) => A), (TM(0) => A), (TM(2) => B)]))
length(collars)


# # potentially create subtype of SimpleDiGraph that has fadjlist sorted

function sort_by_deg_or!(DG::SimpleDiGraph)
    for i in vertices(DG) # for each node
        # # order its out-neighbor list from lowest degree to highest degree
        sort!(DG.fadjlist[i], lt=(x, y) -> (degree(DG, x) < degree(DG, y)) ||(degree(DG, x) == degree(DG, y) && x < y))
    end
end

"""
    toDAG

Returns degree oriented DAG
"""
function toDAG(G::SimpleGraph)
    n = nv(G)

    # # SimpleDiGraphs contain both out- and in-degree information
    DG = SimpleDiGraph(n)

    for i in vertices(G) # for each node
        for j in neighbors(G, i) # for each neighbor

            # # if edge has already been seen, skip processing
            if i > j
                continue
            end

            degi = degree(G, i)
            degj = degree(G, j)

            # # conditions for adding a directed edge
            if (degi < degj) || (degi == degj && i < j)
                add_edge!(DG, i, j)
            else
                add_edge!(DG, j, i)
            end
        end
    end

    # for i in vertices(DG) # for each node
    #     # # order its out-neighbor list from lowest degree to highest degree
    #     sort!(DG.fadjlist[i], lt=(x, y) -> (degree(DG, x) < degree(DG, y)) ||(degree(DG, x) == degree(DG, y) && x < y))
    # end

    sort_by_deg_or!(DG)
    # println(DG.fadjlist)
    # println(DG.badjlist)

    return DG

end


using Test
using SymDiff

@test simplify(Val{:^}, :((2+0)^(1*a))) == :(2^a)


"""
    local_clustering_coefficient(g, v)
    local_clustering_coefficient(g, vs)

Return the [local clustering coefficient](https://en.wikipedia.org/wiki/Clustering_coefficient)
for node `v` in graph `g`. If a list of vertices `vs` is specified, return a vector
of coefficients for each node in the list.
"""
function local_clustering_coefficient(g::AbstractGraph, v::Integer)
    ntriang, nalltriang = local_clustering(g, v)
    return nalltriang == 0 ? 0. : ntriang * 1.0 / nalltriang
end

function local_clustering_coefficient(g::AbstractGraph, vs = vertices(g))
    ntriang, nalltriang = local_clustering(g, vs)
    return map(p -> p[2] == 0 ? 0. : p[1] * 1.0 / p[2], zip(ntriang, nalltriang))
end

function local_clustering!(storage::AbstractVector{Bool}, g::AbstractGraph, v::Integer)
    k = degree(g, v)
    k <= 1 && return (0, 0)
    neighs = neighbors(g, v)
    tcount = 0
    storage[neighs] .= true

    @inbounds for i in neighs
        @inbounds for j in neighbors(g, i)
            if (i != j) && storage[j]
                tcount += 1
            end
        end
    end
    return is_directed(g) ? (tcount, k * (k - 1)) : (div(tcount, 2), div(k * (k - 1), 2))
end

function local_clustering!(storage::AbstractVector{Bool},
                           ntriang::AbstractVector{Int},
                           nalltriang::AbstractVector{Int},
                           g::AbstractGraph,
                           vs)
    i = 0
    for (i, v) in enumerate(vs)
        ntriang[i], nalltriang[i] = local_clustering!(storage, g, v)
        storage[neighbors(g, v)] .= false
    end
    return ntriang, nalltriang
end

"""
    local_clustering(g, v)
    local_clustering(g, vs)

Return a tuple `(a, b)`, where `a` is the number of triangles in the neighborhood
of `v` and `b` is the maximum number of possible triangles. If a list of vertices
`vs` is specified, return two vectors representing the number of triangles and
the maximum number of possible triangles, respectively, for each node in the list.

This function is related to the local clustering coefficient `r` by ``r=\\frac{a}{b}``.
"""
function local_clustering(g::AbstractGraph, v::Integer)
    storage = zeros(Bool, nv(g))
    return local_clustering!(storage, g, v)
end

function local_clustering(g::AbstractGraph, vs = vertices(g))
    storage = zeros(Bool, nv(g))
    ntriang = zeros(Int, length(vs))
    nalltriang = zeros(Int, length(vs))
    return local_clustering!(storage, ntriang, nalltriang, g, vs)
end

"""
    triangles(g[, v])
    triangles(g, vs)

Return the number of triangles in the neighborhood of node `v` in graph `g`.
If a list of vertices `vs` is specified, return a vector of number of triangles
for each node in the list. If no vertices are specified, return the number
of triangles for each node in the graph.
"""
triangles(g::AbstractGraph, v::Integer) = local_clustering(g, v)[1]
triangles(g::AbstractGraph, vs = vertices(g)) = local_clustering(g, vs)[1]

"""
    global_clustering_coefficient(g)

Return the [global clustering coefficient](https://en.wikipedia.org/wiki/Clustering_coefficient)
of graph `g`.
"""
function global_clustering_coefficient(g::AbstractGraph)
    c = 0
    ntriangles = 0
    for v in vertices(g)
        neighs = neighbors(g, v)
        for i in neighs, j in neighs
            i == j && continue
            if has_edge(g, i, j)
                c += 1
            end
        end
        k = degree(g, v)
        ntriangles += k * (k - 1)
    end
    ntriangles == 0 && return 1.
    return c / ntriangles
end


### A Pluto.jl notebook ###
# v0.12.20

using Markdown
using InteractiveUtils

# ╔═╡ e637f8ae-7e0b-11eb-31d9-c5343065be69
md"""
The Heston volatility model follows

$dS_{t} = S_{t} = (r - q)dt + \sqrt{V_{t}} dW_{t}$
$dV_{t} = k(\nu - V_{t}) dt + \lambda \sqrt{\nu_{t}}dZ_{t}$
$Cov(W_{t}, Z_{t}) = \rho dt, \rho \in (-1, 1)$
+ Initial volatility : $\sigma_{0} > 0$
+ Equilibrium volatility : $\nu$
+ Speed of mean reversion $k$
+ vol of vol: $\lambda$ . If $2k\nu > \lambda^2$ variance cannot reach zero (Feller condition)
+ Variance $V_{t}$  is is non-centrally chi-squared distributed, \sqrt{V_{t}} is Rayleigh Distributed
+ Log-Price: $\phi(u, t) := \mathbb{E}[exp(iu \log(S_{t}) | S_{0}, \sigma_{0}^{2}]$

\begin{align}
\phi(u, t)  &=  exp(iu(logS0+ (r−q)t))×exp(ηκλ−2((κ−ρλiu+d)t−2 \\
            & \times log((1−g1edt)/(1−g1))))×exp(σ20λ−2(κ−ρλiu+d)(1−edt)/(1−g1edt))\\
\end{align}

Fast-Reversion Limit of the Heston Model

https://papers.ssrn.com/sol3/papers.cfm?abstract_id=2418631

"""

# ╔═╡ Cell order:
# ╠═e637f8ae-7e0b-11eb-31d9-c5343065be69


#############################################################################
# SCS.jl
# Wrapper around the SCS solver https://github.com/cvxgrp/scs
# See http://github.com/karanveerm/SCS.jl
#############################################################################
# test/mpb.jl
# Test the MathProgBase.jl interface for the SCS.jl solver wrapper
#############################################################################

using Base.Test
using MathProgBase
using SCS

for solver in [SCSSolver(), SCSSolver(linearsolver=SCS.Indirect)]
    objtol = 1e-4
    primaltol = 1e-4

    # min -x
    # s.t. 2x + y <= 1.5
    # x,y >= 0
    # solution is (0.75,0) with objval -0.75
    sol = linprog([-1,0],[2 1],'<',1.5,solver)
    @test sol.status == :Optimal
    @test isapprox(sol.objval, -0.75, atol=objtol)
    @test isapprox(norm(sol.sol - [0.75,0.0]), 0, atol=primaltol)

    sol = linprog([-1,0],sparse([2 1]),'<',1.5,solver)
    @test sol.status == :Optimal
    @test isapprox(sol.objval, -0.75, atol=objtol)
    @test isapprox(norm(sol.sol - [0.75,0.0]), 0, atol=primaltol)

    # test infeasible problem:
    # min x
    # s.t. 2x+y <= -1
    # x,y >= 0
    sol = linprog([1.0,0.0],[2.0 1.0],'<',-1.0,solver)
    @test sol.status == :Infeasible

    # test unbounded problem:
    # min -x-y
    # s.t. -x+2y <= 0
    # x,y >= 0
    sol = linprog([-1.,-1.],[-1. 2.],'<',[0.0],solver)
    @test sol.status == :Unbounded

    # More complicated through loadproblem!
    A = [1.0 1.0 0.0 0.0 0.0;
         0.0 1.0 0.0 0.0 1.0;
         0.0 0.0 1.0 1.0 1.0]
    collb = [0.0, 0.0, 0.0, 0.0, 0.0]
    colub = [Inf, Inf, Inf, Inf, Inf]
    obj   = [3.0, 4.0, 4.0, 9.0, 5.0]
    sense = :Max
    rowlb = [-Inf, -Inf, -Inf]
    rowub = [ 5.0,  3.0,  9.0]
    m = MathProgBase.LinearQuadraticModel(solver)
    MathProgBase.loadproblem!(m, A, collb, colub, obj, rowlb, rowub, sense)
    MathProgBase.optimize!(m)
    @test isapprox(MathProgBase.getobjval(m), 99.0, atol=1e-3)
    x = MathProgBase.getsolution(m)
    @test isapprox(x[1], 2.0, atol=1e-4)
    @test isapprox(x[2], 3.0, atol=1e-4)
    @test isapprox(x[3], 0.0, atol=1e-4)
    @test isapprox(x[4], 9.0, atol=1e-4)
    @test isapprox(x[5], 0.0, atol=1e-4)
end


const DEFAULT_ETA = 1e-3
const DEFAULT_STEPS = 3e3

abstract type Rescaler end

struct OrderGuess
    name::String
    testloss::Real
    trainloss::Real
    x::Array
    y::Array
    preds::Array
end

struct OrderEstimator
    name::String
    initparams::Array{Float64, 1}
    funform::Base.Callable
    scaler::Base.Callable
end

function OrderEstimator(name::String, initparams::Array{Float64, 1}, funform::Base.Callable)
    OrderEstimator(name, initparams, funform, PosScaler)
end

function getscale(a::Array)
    m = mean(a)
    s = std(a) |> (x -> x > 0 ? x : 1)
    return [m, s, minimum((a .- m) ./ s) - 1]
end

struct PosScaler <: Rescaler
    m::Real
    s::Real
    m2::Real

    PosScaler(a::Array) = new(getscale(a)...)
end

struct LogScaler <: Rescaler
    m::Real
    s::Real
    m2::Real

    LogScaler(a::Array) = new(getscale(log.(a))...)
end

transform(scaler::PosScaler, a::Array) = (a .- scaler.m) ./ scaler.s .- scaler.m2
function transform(scaler::LogScaler, a::Array)
    (log.(a) .- scaler.m) ./ scaler.s .- scaler.m2
end

invert(scaler::PosScaler, a::Array) = (a .+ scaler.m2) * scaler.s .+ scaler.m
invert(scaler::LogScaler, a::Array) = exp.((a .+ scaler.m2) * scaler.s .+ scaler.m)

function orders()
    return [
    OrderEstimator(
        "O(1)",
        [1.0],
        c -> (x -> c)
        ),
    OrderEstimator(
        "O(log n)",
        [0.0, 1.0, 2.0],
        (c, b, l) -> (x -> max(0.0, c) + max(1e-2, b) * log(max(1.01, l), x))
        ),
    OrderEstimator(
        "O(n)",
        [0.0, 1.0],
        (c, b) -> (x -> max(0.0,c) + max(0.001, b) * x)
        ),
    OrderEstimator(
        "O(n log n)",
        [0.0, 0.0, 1.0, 2.0],
        (c, b1, b2, l) -> (x -> max(0.0, c) + max(0, b1) * x + max(0.001, b2) * x * log(max(1.01, l), x))
        ),
    OrderEstimator(
        "O(n^p)",
        [0.0, 0.0, 1.0, 2.0],
        (c, b1, b2, p) -> (x -> max(0.0, c) + max(0, b1) * x + max(0.001, b2) * x ^ max(1.01, p))
        ),
    OrderEstimator(
        "O(k^n)",
        [0.0, 0.0, 1.0, 2.0],
        (c, b1, b2, k) -> (x -> max(0.0, c) + max(0, b1) * x + max(0.001, b2) * max(1.001, k) ^ x),
        LogScaler
        ),
    ]
end

function plot(guess::OrderGuess)
    plot(
        guess.x,
        hcat(guess.preds, guess.y),
        labels=["preds" "test"],
        legend=:topleft,
        title=guess.name
    )
end

function _optimize!(loss, parameters, eta, steps=1e5, breaklimit=1e-15)
    for _ in 1:steps
        l = loss(parameters...)
        l < breaklimit && break
        g = gradient(loss, parameters...) ./ l
        parameters .= parameters .- (g .* eta ./ sum(abs.(g)))
    end
end

function fit(order::OrderEstimator, x, y, eta=1e-3, steps=5e4)::Base.Callable
    scaler = order.scaler(y)

    prepped_y = transform(scaler, y)
    ps = order.initparams |> copy

    predscaler = order.scaler(order.funform(ps...).(x))

    model(ps) = x -> transform(predscaler, order.funform(ps...).(x))
    loss(ps...) = sum((model(ps)(x) .- prepped_y) .^ 2)

    ps[1] += mean(prepped_y .- model(ps)(x))
    _optimize!(loss, ps, eta, steps)

    return x -> invert(scaler, model(ps)(x))
end

function guessbigo(x, y, eta=DEFAULT_ETA, steps=DEFAULT_STEPS, trainrate=0.7)::Array{OrderGuess}

    te = (length(x) * trainrate) |> round |> Int
    train_x, train_y = (a -> a[1:te]).([x, y])
    test_x, test_y = (a -> a[te+1:end]).([x, y])

    recs = []
    for order in orders()
        estimator = fit(order, train_x, train_y, eta, steps)

        testerror = sum(abs.(estimator(test_x) .- test_y))
        trainerror = sum(abs.(estimator(train_x) .- train_y))
        push!(
            recs,
            OrderGuess(order.name, testerror, trainerror, x, y, estimator(x))
        )
    end
    return sort(recs, by=x-> x.testloss)
end
guessbigo(fun::Base.Callable, x=50:25:500, eta=DEFAULT_ETA, steps=DEFAULT_STEPS, trainrate=0.7) = guessbigo(x, fun.(x), eta, steps, trainrate)

best(guesses::Array{OrderGuess}) = sort(guesses, by=x-> x.testloss)[1].name

function bigos(report::RunReport, eta=DEFAULT_ETA, steps=DEFAULT_STEPS, trainrate=0.7)
    return Dict(
        [
            (
                report.names[i],
                guessbigo(
                    report.inputsizes,
                    report.times[:, i],
                    eta,
                    steps,
                    trainrate
                    ) |> best
            )
            for i in axes(report.names, 1)
        ]
    )
end


"""
    DataDistribution(X; kwargs...)

A probability distribution representing the frequency of codewords in a given
sample. The sample must be a `Matrix{Bool}` or a `BitMatrix` with columns
representing codewords. The number of bits = `size(X,1)` must be less than 64.

First sets `X = X[I,:]` then proceeds

"""
mutable struct DataDistribution <: AbstractBinaryVectorDistribution
    P::SparseVector{Float64}
    N::Int
    X::BitMatrix
    # I::Vector{Int} # moving this to metadata
    metadata::Dict{Any,Any}
    cache::Dict{Any,Any}

    function DataDistribution(X::AbstractMatrix{Bool}; kwargs...)
        (N,m) = size(X)
        if N >= 64
            error("Attempting to compute data distribution of data with >= 64 bit words")
        end
        P = sparsevec(_compute_Xtilde(X)[:], ones(m), 2^N)
        new(P/m, N, BitMatrix(X), Dict(kwargs), Dict(kwargs))
    end
end
_compute_Xtilde(X::AbstractMatrix{Bool}) = 1 .+ sum(X .* [2^i for i = 0:(size(X,1)-1)], 1)
_compute_Xtilde(X::BitMatrix) = [1 + Int(X[:,k].chunks[1]) for k = 1:size(X,2)]

n_bits(DD::DataDistribution) = DD.N

==(P1::DataDistribution, P2::DataDistribution) = P1.P == P2.P

# pdf(Pr::DataDistribution, x::Vector{Bool}) = length(x) == n_bits(Pr) ? Pr.P[1 + dot([2^i for i = 0:(Pr.N - 1)], x)] : error("DataDistribution pdf: out of domain error")
# pdf(Pr::DataDistribution, x::BitVector) = length(x) == n_bits(Pr) ? Pr.P[1 + Int(x.chunks[1])] : error("DataDistribution pdf: out of domain error")
pdf(Pr::DataDistribution, x) = length(x) == n_bits(Pr) ? Pr.P[1 + _binary_to_int(x)] : error("DataDistribution pdf: out of domain error")

get_pdf(DD::DataDistribution) = full(DD.P)

function show(io::IO, P::DataDistribution)
    println(io, "Data Distribution")
    println(io, "N_neurons: $(n_bits(P))")
    # println(io, "Indices:   $(P.I)")
    println(io, "N_samples: $(size(P.X,2))")
    show_metadata(io, P)
end

expectation_matrix(DD::DataDistribution) = DD.X * DD.X' / size(DD.X,2)

entropy(DD::DataDistribution) = -sum_kbn([p * log(p) for p in nonzeros(DD.P)])
entropy2(DD::DataDistribution) = -sum_kbn([p * log2(p) for p in nonzeros(DD.P)])

# random(DD::DataDistribution, n_samples::Int=1) = _random_exact(DD, n_samples)


# Continuous quadratic problems

function qp1test(model::MOI.ModelLike, config::TestConfig)
    atol = config.atol
    rtol = config.rtol
    @testset "QP1 - Quadratic objective" begin
        # simple quadratic objective
        # Min x^2 + xy + y^2 + yz + z^2
        # st  x + 2y + 3z >= 4 (c1)
        #     x +  y      >= 1 (c2)
        #     x,y \in R

        #@test MOI.supportsproblem(model, MOI.ScalarQuadraticFunction{Float64}, [(MOI.ScalarAffineFunction{Float64},MOI.GreaterThan{Float64})])

        MOI.empty!(model)
        @test MOI.isempty(model)

        MOI.canaddvariable(model)
        v = MOI.addvariables!(model, 3)
        @test MOI.get(model, MOI.NumberOfVariables()) == 3

        cf1 = MOI.ScalarAffineFunction(v, [1.0,2.0,3.0], 0.0)
        @test MOI.canaddconstraint(model, typeof(cf1), MOI.GreaterThan{Float64})
        c1 = MOI.addconstraint!(model, cf1, MOI.GreaterThan(4.0))
        @test MOI.get(model, MOI.NumberOfConstraints{MOI.ScalarAffineFunction{Float64}, MOI.GreaterThan{Float64}}()) == 1

        @test MOI.canaddconstraint(model, MOI.ScalarAffineFunction{Float64}, MOI.GreaterThan{Float64})
        c2 = MOI.addconstraint!(model, MOI.ScalarAffineFunction([v[1],v[2]], [1.0,1.0], 0.0), MOI.GreaterThan(1.0))
        @test MOI.get(model, MOI.NumberOfConstraints{MOI.ScalarAffineFunction{Float64}, MOI.GreaterThan{Float64}}()) == 2

        obj = MOI.ScalarQuadraticFunction(MOI.VariableIndex[], Float64[], v[[1,1,2,2,3]], v[[1,2,2,3,3]], [2.0, 1.0, 2.0, 1.0, 2.0], 0.0)
        @test MOI.canset(model, MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}())
        MOI.set!(model, MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}(), obj)
        @test MOI.canset(model, MOI.ObjectiveSense())
        MOI.set!(model, MOI.ObjectiveSense(), MOI.MinSense)
        @test MOI.get(model, MOI.ObjectiveSense()) == MOI.MinSense

        if config.query
            @test MOI.canget(model, MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}())
            @test obj ≈ MOI.get(model, MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}())

            @test MOI.canget(model, MOI.ConstraintFunction(), typeof(c1))
            @test cf1 ≈ MOI.get(model, MOI.ConstraintFunction(), c1)

            @test MOI.canget(model, MOI.ConstraintSet(), typeof(c1))
            @test MOI.GreaterThan(4.0) == MOI.get(model, MOI.ConstraintSet(), c1)
        end

        if config.solve
            MOI.optimize!(model)

            @test MOI.canget(model, MOI.TerminationStatus())
            @test MOI.get(model, MOI.TerminationStatus()) == MOI.Success

            @test MOI.canget(model, MOI.PrimalStatus())
            @test MOI.get(model, MOI.PrimalStatus()) == MOI.FeasiblePoint

            @test MOI.canget(model, MOI.ObjectiveValue())
            @test MOI.get(model, MOI.ObjectiveValue()) ≈ 130/70 atol=atol rtol=rtol

            @test MOI.canget(model, MOI.VariablePrimal(), MOI.VariableIndex)
            @test MOI.get(model, MOI.VariablePrimal(), v) ≈ [0.5714285714285715,0.4285714285714285,0.8571428571428572] atol=atol rtol=rtol
        end
    end
end

function qp2test(model::MOI.ModelLike, config::TestConfig)
    atol = config.atol
    rtol = config.rtol
    @testset "QP2" begin
        # same as QP0 but with duplicate terms
        # then change the objective and sense
        # simple quadratic objective
        # Min x^2 + xy + y^2 + yz + z^2
        # st  x + 2y + 3z >= 4 (c1)
        #     x +  y      >= 1 (c2)
        #     x,y \in R

        #@test MOI.supportsproblem(model, MOI.ScalarQuadraticFunction{Float64}, [(MOI.ScalarAffineFunction{Float64},MOI.GreaterThan{Float64})])

        MOI.empty!(model)
        @test MOI.isempty(model)

        MOI.canaddvariable(model)
        v = MOI.addvariables!(model, 3)
        @test MOI.get(model, MOI.NumberOfVariables()) == 3

        c1f = MOI.ScalarAffineFunction(v, [1.0,2.0,3.0], 0.0)
        @test MOI.canaddconstraint(model, typeof(c1f), MOI.GreaterThan{Float64})
        c1 = MOI.addconstraint!(model, c1f, MOI.GreaterThan(4.0))
        @test MOI.get(model, MOI.NumberOfConstraints{MOI.ScalarAffineFunction{Float64}, MOI.GreaterThan{Float64}}()) == 1

        @test MOI.canaddconstraint(model, MOI.ScalarAffineFunction{Float64}, MOI.GreaterThan{Float64})
        c2 = MOI.addconstraint!(model, MOI.ScalarAffineFunction([v[1],v[2]], [1.0,1.0], 0.0), MOI.GreaterThan(1.0))
        @test MOI.get(model, MOI.NumberOfConstraints{MOI.ScalarAffineFunction{Float64}, MOI.GreaterThan{Float64}}()) == 2

        obj = MOI.ScalarQuadraticFunction(v, [0.0,0.0,0.0],[v[1], v[1], v[1], v[2], v[2], v[3], v[3]], [v[1], v[2], v[2], v[2], v[3], v[3], v[3]], [2.0, 0.5, 0.5, 2.0, 1.0, 1.0, 1.0], 0.0)
        @test MOI.canset(model, MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}())
        MOI.set!(model, MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}(), obj)
        @test MOI.canset(model, MOI.ObjectiveSense())
        MOI.set!(model, MOI.ObjectiveSense(), MOI.MinSense)
        @test MOI.get(model, MOI.ObjectiveSense()) == MOI.MinSense

        if config.query
            @test MOI.canget(model, MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}())
            @test obj ≈ MOI.get(model, MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}())

            @test MOI.canget(model, MOI.ConstraintFunction(), typeof(c1))
            @test c1f ≈ MOI.get(model, MOI.ConstraintFunction(), c1)

            @test MOI.canget(model, MOI.ConstraintSet(), typeof(c1))
            @test MOI.GreaterThan(4.0) == MOI.get(model, MOI.ConstraintSet(), c1)
        end

        if config.solve
            MOI.optimize!(model)

            @test MOI.canget(model, MOI.TerminationStatus())
            @test MOI.get(model, MOI.TerminationStatus()) == MOI.Success

            @test MOI.canget(model, MOI.PrimalStatus())
            @test MOI.get(model, MOI.PrimalStatus()) == MOI.FeasiblePoint

            @test MOI.canget(model, MOI.ObjectiveValue())
            @test MOI.get(model, MOI.ObjectiveValue()) ≈ 130/70 atol=atol rtol=rtol

            @test MOI.canget(model, MOI.VariablePrimal(), MOI.VariableIndex)
            @test MOI.get(model, MOI.VariablePrimal(), v) ≈ [0.5714285714285715,0.4285714285714285,0.8571428571428572] atol=atol rtol=rtol
        end

        # change objective to Max -2(x^2 + xy + y^2 + yz + z^2)
        obj2 = MOI.ScalarQuadraticFunction(v, [0.0,0.0,0.0],[v[1], v[1], v[1], v[2], v[2], v[3], v[3]], [v[1], v[2], v[2], v[2], v[3], v[3], v[3]], [-4.0, -1.0, -1.0, -4.0, -2.0, -2.0, -2.0], 0.0)
        @test MOI.canset(model, MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}())
        MOI.set!(model, MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}(), obj2)
        @test MOI.canset(model, MOI.ObjectiveSense())
        MOI.set!(model, MOI.ObjectiveSense(), MOI.MaxSense)
        @test MOI.get(model, MOI.ObjectiveSense()) == MOI.MaxSense

        if config.query
            @test MOI.canget(model, MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}())
            @test obj2 ≈ MOI.get(model, MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}())
        end

        if config.solve
            MOI.optimize!(model)

            @test MOI.canget(model, MOI.TerminationStatus())
            @test MOI.get(model, MOI.TerminationStatus()) == MOI.Success

            @test MOI.canget(model, MOI.PrimalStatus())
            @test MOI.get(model, MOI.PrimalStatus()) == MOI.FeasiblePoint

            @test MOI.canget(model, MOI.ObjectiveValue())
            @test MOI.get(model, MOI.ObjectiveValue()) ≈ -2*130/70 atol=atol rtol=rtol

            @test MOI.canget(model, MOI.VariablePrimal(), MOI.VariableIndex)
            @test MOI.get(model, MOI.VariablePrimal(), v) ≈ [0.5714285714285715,0.4285714285714285,0.8571428571428572] atol=atol rtol=rtol
        end
    end
end

function qp3test(model::MOI.ModelLike, config::TestConfig)
    atol = config.atol
    rtol = config.rtol
    @testset "qp3test - Linear Quadratic objective" begin
        # simple quadratic objective
        #    minimize 2 x^2 + y^2 + xy + x + y + 1
        #       s.t.  x, y >= 0
        #             x + y = 1

        #@test MOI.supportsproblem(model, MOI.ScalarQuadraticFunction{Float64},
        #    [
        #        (MOI.SingleVariable,MOI.GreaterThan{Float64}),
        #        (MOI.ScalarAffineFunction{Float64},MOI.EqualTo{Float64})
        #    ]
        #)

        MOI.empty!(model)
        @test MOI.isempty(model)

        MOI.canaddvariable(model)
        x = MOI.addvariable!(model)
        MOI.canaddvariable(model)
        y = MOI.addvariable!(model)

        MOI.addconstraint!(model,
            MOI.ScalarAffineFunction([x,y], [1.0,1.0], 0.0),
            MOI.EqualTo(1.0)
        )

        @test MOI.canaddconstraint(model, MOI.SingleVariable, MOI.GreaterThan{Float64})
        MOI.addconstraint!(model, MOI.SingleVariable(x), MOI.GreaterThan(0.0))
        @test MOI.canaddconstraint(model, MOI.SingleVariable, MOI.GreaterThan{Float64})
        MOI.addconstraint!(model, MOI.SingleVariable(y), MOI.GreaterThan(0.0))

        obj = MOI.ScalarQuadraticFunction(
                [x,y], [1.0,1.0],
                [x,y,x], [x,y,y], [4.0, 2.0, 1.0],
                1.0
              )
        @test MOI.canset(model, MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}())
        MOI.set!(model, MOI.ObjectiveFunction{MOI.ScalarQuadraticFunction{Float64}}(), obj)
        @test MOI.canset(model, MOI.ObjectiveSense())
        MOI.set!(model, MOI.ObjectiveSense(), MOI.MinSense)

        if config.solve
            MOI.optimize!(model)

            @test MOI.canget(model, MOI.TerminationStatus())
            @test MOI.get(model, MOI.TerminationStatus()) == MOI.Success

            @test MOI.canget(model, MOI.PrimalStatus())
            @test MOI.get(model, MOI.PrimalStatus()) == MOI.FeasiblePoint

            @test MOI.canget(model, MOI.ObjectiveValue())
            @test MOI.get(model, MOI.ObjectiveValue()) ≈ 2.875 atol=atol rtol=rtol
            @test MOI.canget(model, MOI.VariablePrimal(), MOI.VariableIndex)
            @test MOI.get(model, MOI.VariablePrimal(), [x,y]) ≈ [0.25, 0.75] atol=atol rtol=rtol
        end

        # change back to linear
        #        max 2x + y + 1
        #       s.t.  x, y >= 0
        #             x + y = 1
        # (x,y) = (1,0), obj = 3
        objf = MOI.ScalarAffineFunction([x,y], [2.0,1.0], 1.0)
        @test MOI.canset(model, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}())
        MOI.set!(model, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), objf)
        @test MOI.canset(model, MOI.ObjectiveSense())
        MOI.set!(model, MOI.ObjectiveSense(), MOI.MaxSense)

        if config.solve
            MOI.optimize!(model)

            @test MOI.canget(model, MOI.TerminationStatus())
            @test MOI.get(model, MOI.TerminationStatus()) == MOI.Success

            @test MOI.canget(model, MOI.PrimalStatus())
            @test MOI.get(model, MOI.PrimalStatus()) == MOI.FeasiblePoint

            @test MOI.canget(model, MOI.ObjectiveValue())
            @test MOI.get(model, MOI.ObjectiveValue()) ≈ 3.0 atol=atol rtol=rtol
            @test MOI.canget(model, MOI.VariablePrimal(), MOI.VariableIndex)
            @test MOI.get(model, MOI.VariablePrimal(), [x,y]) ≈ [1.0, 0.0] atol=atol rtol=rtol
        end

    end
end

function qptests(model::MOI.ModelLike; atol=Base.rtoldefault(Float64), rtol=Base.rtoldefault(Float64))
    @testset "Quadratic Programs (quad. objective)" begin
        qp1test(model, atol=atol, rtol=rtol)
        qp2test(model, atol=atol, rtol=rtol)
        qp3test(model, atol=atol, rtol=rtol)
    end
end

#=
    Quadratically constrained programs
=#

function qcp1test(model::MOI.ModelLike, config::TestConfig)
    atol = config.atol
    rtol = config.rtol
    @testset "qcp1" begin
        # quadratic constraint
        # Max x + y
        # st  - x + y >= 0 (c1[1])
        #       x + y >= 0 (c1[2])
        #     0.5x^2 + y <= 2 (c2)

        #@test MOI.supportsproblem(model, MOI.ScalarAffineFunction{Float64}, [(MOI.VectorAffineFunction{Float64},MOI.Nonnegatives),(MOI.ScalarQuadraticFunction{Float64},MOI.LessThan{Float64})])

        MOI.empty!(model)
        @test MOI.isempty(model)

        MOI.canaddvariable(model)
        x = MOI.addvariable!(model)
        MOI.canaddvariable(model)
        y = MOI.addvariable!(model)
        @test MOI.get(model, MOI.NumberOfVariables()) == 2

        @test MOI.canaddconstraint(model, MOI.VectorAffineFunction{Float64}, MOI.Nonnegatives)
        c1 = MOI.addconstraint!(model, MOI.VectorAffineFunction([1,1,2,2], [x,y,x,y], [-1.0,1.0,1.0,1.0], [0.0,0.0]), MOI.Nonnegatives(2))
        @test MOI.get(model, MOI.NumberOfConstraints{MOI.VectorAffineFunction{Float64}, MOI.Nonnegatives}()) == 1

        c2f = MOI.ScalarQuadraticFunction([y],[1.0],[x],[x],[1.0], 0.0)
        @test MOI.canaddconstraint(model, typeof(c2f), MOI.LessThan{Float64})
        c2 = MOI.addconstraint!(model, c2f, MOI.LessThan(2.0))
        @test MOI.get(model, MOI.NumberOfConstraints{MOI.ScalarQuadraticFunction{Float64}, MOI.LessThan{Float64}}()) == 1

        @test MOI.canset(model, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}())
        MOI.set!(model, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), MOI.ScalarAffineFunction([x,y], [1.0,1.0], 0.0))
        @test MOI.canset(model, MOI.ObjectiveSense())
        MOI.set!(model, MOI.ObjectiveSense(), MOI.MaxSense)
        @test MOI.get(model, MOI.ObjectiveSense()) == MOI.MaxSense

        if config.query
            @test MOI.canget(model, MOI.ConstraintFunction(), typeof(c2))
            @test c2f ≈ MOI.get(model, MOI.ConstraintFunction(), c2)
        end

        if config.solve
            MOI.optimize!(model)

            @test MOI.canget(model, MOI.TerminationStatus())
            @test MOI.get(model, MOI.TerminationStatus()) == MOI.Success

            @test MOI.canget(model, MOI.PrimalStatus())
            @test MOI.get(model, MOI.PrimalStatus()) == MOI.FeasiblePoint

            @test MOI.canget(model, MOI.ObjectiveValue())
            @test MOI.get(model, MOI.ObjectiveValue()) ≈ 2.25 atol=atol rtol=rtol

            @test MOI.canget(model, MOI.VariablePrimal(), MOI.VariableIndex)
            @test MOI.get(model, MOI.VariablePrimal(), [x,y]) ≈ [0.5,1.75] atol=atol rtol=rtol
        end

        # try delete quadratic constraint and go back to linear

        # MOI.delete!(model, c2)
        #
        # MOI.optimize!(model)
        #
        # @test MOI.canget(model, MOI.TerminationStatus())
        # @test MOI.get(model, MOI.TerminationStatus()) == MOI.Success
        #
        # @test MOI.canget(model, MOI.PrimalStatus())
        # @test MOI.get(model, MOI.PrimalStatus()) == MOI.FeasiblePoint
        #
        # @test MOI.canget(model, MOI.ObjectiveValue())
        # @test MOI.get(model, MOI.ObjectiveValue()) ≈ 0.0 atol=atol rtol=rtol
    end
end

function qcp2test(model::MOI.ModelLike, config::TestConfig)
    atol = config.atol
    rtol = config.rtol
    @testset "qcp2" begin
        # Max x
        # s.t. x^2 <= 2 (c)

        #@test MOI.supportsproblem(model, MOI.ScalarAffineFunction{Float64}, [(MOI.ScalarQuadraticFunction{Float64},MOI.LessThan{Float64})])

        MOI.empty!(model)
        @test MOI.isempty(model)

        MOI.canaddvariable(model)
        x = MOI.addvariable!(model)
        @test MOI.get(model, MOI.NumberOfVariables()) == 1

        cf = MOI.ScalarQuadraticFunction(MOI.VariableIndex[x],Float64[0.0],[x],[x],[1.0], 0.0)
        @test MOI.canaddconstraint(model, typeof(cf), MOI.LessThan{Float64})
        c = MOI.addconstraint!(model, cf, MOI.LessThan(2.0))
        @test MOI.get(model, MOI.NumberOfConstraints{MOI.ScalarQuadraticFunction{Float64}, MOI.LessThan{Float64}}()) == 1

        @test MOI.canset(model, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}())
        MOI.set!(model, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), MOI.ScalarAffineFunction([x], [1.0], 0.0))
        @test MOI.canset(model, MOI.ObjectiveSense())
        MOI.set!(model, MOI.ObjectiveSense(), MOI.MaxSense)
        @test MOI.get(model, MOI.ObjectiveSense()) == MOI.MaxSense

        if config.query
            @test MOI.canget(model, MOI.ConstraintFunction(), typeof(c))
            @test cf ≈ MOI.get(model, MOI.ConstraintFunction(), c)
        end

        if config.solve
            MOI.optimize!(model)

            @test MOI.canget(model, MOI.TerminationStatus())
            @test MOI.get(model, MOI.TerminationStatus()) == MOI.Success

            @test MOI.canget(model, MOI.PrimalStatus())
            @test MOI.get(model, MOI.PrimalStatus()) == MOI.FeasiblePoint

            if config.duals
                @test MOI.canget(model, MOI.DualStatus())
                @test MOI.get(model, MOI.DualStatus()) == MOI.FeasiblePoint
            end

            @test MOI.canget(model, MOI.ObjectiveValue())
            @test MOI.get(model, MOI.ObjectiveValue()) ≈ sqrt(2) atol=atol rtol=rtol

            @test MOI.canget(model, MOI.VariablePrimal(), MOI.VariableIndex)
            @test MOI.get(model, MOI.VariablePrimal(), x) ≈ sqrt(2) atol=atol rtol=rtol

            # TODO - duals
            # @test MOI.canget(model, MOI.ConstraintDual(), typeof(c))
            # @test MOI.get(model, MOI.ConstraintDual(), c) ≈ 0.5/sqrt(2) atol=atol rtol=rtol
        end
    end
end

function qcp3test(model::MOI.ModelLike, config::TestConfig)
    atol = config.atol
    rtol = config.rtol
    @testset "qcp3" begin
        # Min -x
        # s.t. x^2 <= 2

        #@test MOI.supportsproblem(model, MOI.ScalarAffineFunction{Float64}, [(MOI.ScalarQuadraticFunction{Float64},MOI.LessThan{Float64})])

        MOI.empty!(model)
        @test MOI.isempty(model)

        MOI.canaddvariable(model)
        x = MOI.addvariable!(model)
        @test MOI.get(model, MOI.NumberOfVariables()) == 1

        cf = MOI.ScalarQuadraticFunction(MOI.VariableIndex[],Float64[],[x],[x],[1.0], 0.0)
        @test MOI.canaddconstraint(model, typeof(cf), MOI.LessThan{Float64})
        c = MOI.addconstraint!(model, cf, MOI.LessThan(2.0))
        @test MOI.get(model, MOI.NumberOfConstraints{MOI.ScalarQuadraticFunction{Float64}, MOI.LessThan{Float64}}()) == 1

        @test MOI.canset(model, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}())
        MOI.set!(model, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), MOI.ScalarAffineFunction([x], [-1.0], 0.0))
        @test MOI.canset(model, MOI.ObjectiveSense())
        MOI.set!(model, MOI.ObjectiveSense(), MOI.MinSense)
        @test MOI.get(model, MOI.ObjectiveSense()) == MOI.MinSense

        if config.query
            @test MOI.canget(model, MOI.ConstraintFunction(), typeof(c))
            @test cf ≈ MOI.get(model, MOI.ConstraintFunction(), c)
        end

        if config.solve
            MOI.optimize!(model)

            @test MOI.canget(model, MOI.TerminationStatus())
            @test MOI.get(model, MOI.TerminationStatus()) == MOI.Success

            @test MOI.canget(model, MOI.PrimalStatus())
            @test MOI.get(model, MOI.PrimalStatus()) == MOI.FeasiblePoint

            if config.duals
                @test MOI.canget(model, MOI.DualStatus())
                @test MOI.get(model, MOI.DualStatus()) == MOI.FeasiblePoint
            end

            @test MOI.canget(model, MOI.ObjectiveValue())
            @test MOI.get(model, MOI.ObjectiveValue()) ≈ -sqrt(2) atol=atol rtol=rtol

            @test MOI.canget(model, MOI.VariablePrimal(), MOI.VariableIndex)
            @test MOI.get(model, MOI.VariablePrimal(), x) ≈ sqrt(2) atol=atol rtol=rtol

            # TODO - duals
            # @test MOI.canget(model, MOI.ConstraintDual(), typeof(c))
            # @test MOI.get(model, MOI.ConstraintDual(), c) ≈ -0.5/sqrt(2) atol=atol rtol=rtol
        end
    end
end

function qcptests(model::MOI.ModelLike; atol=Base.rtoldefault(Float64), rtol=Base.rtoldefault(Float64))
    @testset "Quadratic Constrainted Programs (quad. constraints only)" begin
        qcp1test(model, atol=atol, rtol=rtol)
        qcp2test(model, atol=atol, rtol=rtol)
        qcp3test(model, atol=atol, rtol=rtol)
    end
end

#=
    SOCP
=#

function socp1test(model::MOI.ModelLike, config::TestConfig)
    atol = config.atol
    rtol = config.rtol
    @testset "socp1" begin
        # min t
        # s.t. x + y >= 1 (c1)
        #      x^2 + y^2 <= t^2 (c2)
        #      t >= 0 (bound)

        #@test MOI.supportsproblem(model, MOI.ScalarAffineFunction{Float64}, [(MOI.ScalarQuadraticFunction{Float64},MOI.LessThan{Float64}), (MOI.ScalarAffineFunction{Float64},MOI.GreaterThan{Float64}), (MOI.SingleVariable,MOI.GreaterThan{Float64})])

        MOI.empty!(model)
        @test MOI.isempty(model)

        MOI.canaddvariable(model)
        x = MOI.addvariable!(model)
        MOI.canaddvariable(model)
        y = MOI.addvariable!(model)
        MOI.canaddvariable(model)
        t = MOI.addvariable!(model)
        @test MOI.get(model, MOI.NumberOfVariables()) == 3

        c1f = MOI.ScalarAffineFunction([x,y],[1.0, 1.0], 0.0)
        @test MOI.canaddconstraint(model, typeof(c1f), MOI.GreaterThan{Float64})
        c1 = MOI.addconstraint!(model, c1f, MOI.GreaterThan(1.0))
        @test MOI.get(model, MOI.NumberOfConstraints{MOI.ScalarAffineFunction{Float64}, MOI.GreaterThan{Float64}}()) == 1

        c2f = MOI.ScalarQuadraticFunction(MOI.VariableIndex[],Float64[],[x,y,t],[x,y,t],[1.0,1.0,-1.0], 0.0)
        @test MOI.canaddconstraint(model, typeof(c2f), MOI.LessThan{Float64})
        c2 = MOI.addconstraint!(model, c2f, MOI.LessThan(0.0))
        @test MOI.get(model, MOI.NumberOfConstraints{MOI.ScalarQuadraticFunction{Float64}, MOI.LessThan{Float64}}()) == 1

        @test MOI.canaddconstraint(model, MOI.SingleVariable, MOI.GreaterThan{Float64})
        bound = MOI.addconstraint!(model, MOI.SingleVariable(t), MOI.GreaterThan(0.0))
        @test MOI.get(model, MOI.NumberOfConstraints{MOI.SingleVariable, MOI.GreaterThan{Float64}}()) == 1

        @test MOI.canset(model, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}())
        MOI.set!(model, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), MOI.ScalarAffineFunction([t], [1.0], 0.0))
        @test MOI.canset(model, MOI.ObjectiveSense())
        MOI.set!(model, MOI.ObjectiveSense(), MOI.MinSense)
        @test MOI.get(model, MOI.ObjectiveSense()) == MOI.MinSense

        if config.query
            @test MOI.canget(model, MOI.ConstraintFunction(), typeof(c1))
            @test c1f ≈ MOI.get(model, MOI.ConstraintFunction(), c1)

            @test MOI.canget(model, MOI.ConstraintFunction(), typeof(c2))
            @test c2f ≈ MOI.get(model, MOI.ConstraintFunction(), c2)
        end

        if config.solve
            MOI.optimize!(model)

            @test MOI.canget(model, MOI.TerminationStatus())
            @test MOI.get(model, MOI.TerminationStatus()) == MOI.Success

            @test MOI.canget(model, MOI.PrimalStatus())
            @test MOI.get(model, MOI.PrimalStatus()) == MOI.FeasiblePoint

            @test MOI.canget(model, MOI.ObjectiveValue())
            @test MOI.get(model, MOI.ObjectiveValue()) ≈ sqrt(1/2) atol=atol rtol=rtol

            @test MOI.canget(model, MOI.VariablePrimal(), MOI.VariableIndex)
            @test MOI.get(model, MOI.VariablePrimal(), [x,y,t]) ≈ [0.5,0.5,sqrt(1/2)] atol=atol rtol=rtol

            @test MOI.canget(model, MOI.VariablePrimal(), MOI.VariableIndex)
            @test MOI.get(model, MOI.VariablePrimal(), [t,x,y,t]) ≈ [sqrt(1/2),0.5,0.5,sqrt(1/2)] atol=atol rtol=rtol
        end
    end
end

function socptests(model::MOI.ModelLike; atol=Base.rtoldefault(Float64), rtol=Base.rtoldefault(Float64))
    @testset "Second Order Cone Programs" begin
        socp1test(model, atol=atol, rtol=rtol)
    end
end

const contquadratictests = Dict("quadratic1" => qp1test,
                                "quadratic2" => qp2test,
                                "quadratic3" => qp3test,
                                "quadratic4" => qcp1test,
                                "quadratic5" => qcp2test,
                                "quadratic6" => qcp3test,
                                "quadratic7" => socp1test)

@moitestset contquadratic


@testset "trainable" begin
    ν = 2.0; c = 3.0; d = 2.0; γ = 2.0; α = 2.5; h = 0.5; r = rand(3)

    function test_params(kernel, reference)
        params_kernel = params(kernel)
        params_reference = params(reference)

        @test length(params_kernel) == length(params_reference)
        @test all(p == q for (p, q) in zip(params_kernel, params_reference))
    end

    kc = ConstantKernel(c=c)
    test_params(kc, ([c],))

    kfbm = FBMKernel(h = h)
    test_params(kfbm, ([h],))

    kge = GammaExponentialKernel(γ=γ)
    test_params(kge, ([γ],))

    kgr = GammaRationalQuadraticKernel(γ=γ, α=α)
    test_params(kgr, ([α], [γ]))

    kl = LinearKernel(c=c)
    test_params(kl, ([c],))

    km = MaternKernel(ν=ν)
    test_params(km, ([ν],))

    kp = PolynomialKernel(c=c, d=d)
    test_params(kp, ([d], [c]))

    kpe = PeriodicKernel(r = r)
    test_params(kpe, (r,))

    kr = RationalQuadraticKernel(α=α)
    test_params(kr, ([α],))

    k = km + kc
    test_params(k, (k.weights, km, kc))

    k = km * kc
    test_params(k, (km, kc))

    s = 2.0
    k = transform(km, s)
    test_params(k, ([s], km))

    v = [2.0]
    k = transform(kc, v)
    test_params(k, (v, kc))

    P = rand(3, 2)
    k = transform(km, LinearTransform(P))
    test_params(k, (P, km))

    k = transform(km, LinearTransform(P) ∘ ScaleTransform(s))
    test_params(k, ([s], P, km))

    c = Chain(Dense(3, 2))
    k = transform(km, FunctionTransform(c))
    test_params(k, (c, km))
end


@test length(coalitions([1,2,3])) == 2^3
@test length(coalitions([1,2,3,4])) == 2^4
@test sum([w for (c,w) in coalitions([1,2,3])]) ≈ 1
@test sum([w for (c,w) in coalitions(collect(1:9))]) ≈ 1
@test sum([w for (c,w) in coalitions(collect(1:10))]) ≈ 1
it = ShapleyValues.statefuliterator(coalitions([1,2]))
@test next(it)[1] == Int64[]
@test next(it)[1] == [1,2]
@test next(it)[1] == [1]
@test next(it)[1] == [2]
@test done(it)
it = ShapleyValues.statefuliterator(coalitions([1,2,3]))
@test next(it)[1] == Int64[]
@test next(it)[1] == [1,2,3]
@test next(it)[1] == [1]
@test next(it)[1] == [2,3]
@test next(it)[1] == [2]
@test next(it)[1] == [1,3]
@test next(it)[1] == [3]
@test next(it)[1] == [1,2]
@test done(it)