# Create example data
x = repeat(LinRange(-2, 2, 20), inner=10)
y = repeat(LinRange(0, pi, 10), outer=20)
z = sin.(x) .+ cos.(y)
u = 0.1ones(200)
v = zeros(200)
w = 0.5z
# Plot vectors
quiver3(x, y, z, u, v, w, "o", markersize=0.5)


# arnoldi.jl

mutable struct ArnoldiFactorization{T,S} <: KrylovFactorization{T,S}
    k::Int # current Krylov dimension
    V::OrthonormalBasis{T} # basis of length k
    H::Vector{S} # stores the Hessenberg matrix in packed form
    r::T # residual
end

Base.length(F::ArnoldiFactorization) = F.k
Base.sizehint!(F::ArnoldiFactorization, n) = begin
    sizehint!(F.V, n)
    sizehint!(F.H, (n*n + 3*n) >> 1)
    return F
end
Base.eltype(F::ArnoldiFactorization) = eltype(typeof(F))
Base.eltype(::Type{<:ArnoldiFactorization{<:Any,S}}) where {S} = S

basis(F::ArnoldiFactorization) = F.V
rayleighquotient(F::ArnoldiFactorization) = PackedHessenberg(F.H, F.k)
residual(F::ArnoldiFactorization) = F.r
@inbounds normres(F::ArnoldiFactorization) = abs(F.H[end])
rayleighextension(F::ArnoldiFactorization) = SimpleBasisVector(F.k, F.k)

# Arnoldi iteration for constructing the orthonormal basis of a Krylov subspace.
struct ArnoldiIterator{F,T,O<:Orthogonalizer} <: KrylovIterator{F,T}
    operator::F
    x₀::T
    orth::O
end
ArnoldiIterator(A, x₀) = ArnoldiIterator(A, x₀, KrylovDefaults.orth)
ArnoldiIterator(A::AbstractMatrix, x₀::AbstractVector, orth::Orthogonalizer =
                KrylovDefaults.orth) = ArnoldiIterator(x->A*x, x₀, orth)

Base.IteratorSize(::Type{<:ArnoldiIterator}) = Base.SizeUnknown()
Base.IteratorEltype(::Type{<:ArnoldiIterator}) = Base.EltypeUnknown()

function Base.iterate(iter::ArnoldiIterator)
    state = initialize(iter)
    return state, state
end
function Base.iterate(iter::ArnoldiIterator, state)
    nr = normres(state)
    if nr < eps(typeof(nr))
        return nothing
    else
        state = expand!(iter, deepcopy(state))
        return state, state
    end
end

function initialize(iter::ArnoldiIterator; verbosity::Int = 0)
    β₀ = norm(iter.x₀)
    β₀ == 0 && throw(ArgumentError("initial vector should not have norm zero"))
    invβ₀ = one(eltype(iter.x₀))/β₀
    T = typeof(invβ₀) # division might change eltype
    x₀ = mul!(similar(iter.x₀, T), iter.x₀, invβ₀)
    w = iter.operator(x₀) # applying the operator might change eltype
    v = eltype(x₀) == eltype(w) ? x₀ : copyto!(similar(w), x₀)
    r, α = orthogonalize!(w, v, iter.orth)
    β = norm(r)
    V = OrthonormalBasis([v])
    H = [α, β]
    if verbosity > 0
        @info "Arnoldi iteration step 1: normres = $β"
    end

    state = ArnoldiFactorization(1, V, H, r)
end
function initialize!(iter::ArnoldiIterator, state::ArnoldiFactorization; verbosity::Int = 0)
    x₀ = iter.x₀
    V = state.V
    while length(V) > 1
        pop!(V)
    end
    H = empty!(state.H)

    v = mul!(V[1], x₀, 1/norm(x₀))
    w = iter.operator(v)
    r, α = orthogonalize!(w, v, iter.orth)
    β = norm(r)
    state.k = 1
    push!(H, α, β)
    state.r = r
    if verbosity > 0
        @info "Arnoldi iteration step 1: normres = $β"
    end
    return state
end
function expand!(iter::ArnoldiIterator, state::ArnoldiFactorization; verbosity::Int = 0)
    state.k += 1
    k = state.k
    V = state.V
    H = state.H
    r = state.r
    β = normres(state)
    push!(V, rmul!(r, 1/β))
    m = length(H)
    resize!(H, m+k+1)
    r, β = arnoldirecurrence!(iter.operator, V, view(H, (m+1):(m+k)), iter.orth)
    H[m+k+1] = β
    state.r = r
    if verbosity > 0
        @info "Arnoldi iteration step $k: normres = $β"
    end
    return state
end
function shrink!(state::ArnoldiFactorization, k)
    length(state) <= k && return state
    V = state.V
    H = state.H
    while length(V) > k+1
        pop!(V)
    end
    r = pop!(V)
    resize!(H, (k*k + 3*k) >> 1)
    state.k = k
    state.r = rmul!(r, normres(state))
    return state
end

# Arnoldi recurrence: simply use provided orthonormalization routines
function arnoldirecurrence!(operator, V::OrthonormalBasis, h::AbstractVector,
                            orth::Orthogonalizer)
    w = operator(last(V))
    r, h = orthogonalize!(w, V, h, orth)
    return r, norm(r)
end


function fib(n)
    if n < 2
        return n
    end
    return fib(n-1) + fib(n-2)
end

print(fib(5))


using SparseArrays
using LinearAlgebra
using Random
using Clp
using MathOptInterface
const MOI = MathOptInterface

using TimerOutputs

struct RandomLP
    rows::Int
    cols::Int
    dens::Float64
end

function generate_moi_problem(model, At, b, c;
    var_bounds = false, scalar = true)
    cols, rows = size(At)
    x = MOI.add_variables(model, cols)
    A_cols = rowvals(At)
    A_vals = nonzeros(At)
    if var_bounds
        for col in 1:cols
            MOI.add_constraint(model, MOI.SingleVariable(x[col]),
                MOI.LessThan(10.0))
            MOI.add_constraint(model, MOI.SingleVariable(x[col]),
                MOI.GreaterThan(-10.0))
        end
    end
    if scalar
        for row in 1:rows
            MOI.add_constraint(model, MOI.ScalarAffineFunction(
                [MOI.ScalarAffineTerm(A_vals[i], x[A_cols[i]]) for i in nzrange(At, row)], 0.0),
                MOI.LessThan(b[row]))
        end
    else
        for row in 1:rows
            MOI.add_constraint(model, MOI.VectorAffineFunction(
                [MOI.VectorAffineTerm(1,
                    MOI.ScalarAffineTerm(A_vals[i], x[A_cols[i]])
                ) for i in nzrange(At, row)], [-b[row]]),
                MOI.Nonpositives(1))
        end
    end
    objective = MOI.ScalarAffineFunction(
        [MOI.ScalarAffineTerm(c[i], x[i]) for i in findall(!iszero, c)],
            0.0)
    MOI.set(model, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), objective)
    MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
    return x
end

function random_data(seed, data)

    rows = data.rows
    cols = data.cols
    density = data.dens

    p_neg_element = 0.0 # 0.25

    rng = Random.MersenneTwister(seed)

    f_A(r, n) = ifelse.(rand(r, n) .> p_neg_element, 1, -1) .* (15 .+ 30 .* rand(r, n))

    At = sprand(rng, cols, rows, density, f_A)
    b = 50 * rand(rng, rows)

    # not using signs now
    sign = ifelse.(rand(rng, rows) .> 0.2, 'L', 'G')

    f_c(r, n) = 20 .* 2 .* (rand(r, n) .- 0.5)
    c = sprand(rng, cols, 0.5, f_c)

    return At, b, c
end

function bridged_cache_and_solver()
    model = MOI.Bridges.full_bridge_optimizer(MOI.Utilities.CachingOptimizer(
        MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()),
        MOI.Utilities.MANUAL), Float64)
    clp = Clp.Optimizer()
    MOI.set(clp, MOI.Silent(), true)
    return model, clp
end
function cache_and_solver()
    model = MOI.Utilities.CachingOptimizer(
        MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()),
        MOI.Utilities.MANUAL)
    clp = Clp.Optimizer()
    MOI.set(clp, MOI.Silent(), true)
    return model, clp
end
function bridged_cached_solver()
    model = MOI.Bridges.full_bridge_optimizer(MOI.Utilities.CachingOptimizer(
        MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()),
        Clp.Optimizer()), Float64)
    MOI.set(model, MOI.Silent(), true)
    return model
end
function cached_solver()
    model = MOI.Utilities.CachingOptimizer(
        MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()),
        Clp.Optimizer())
    MOI.set(model, MOI.Silent(), true)
    return model
end

function time_build_and_solve(to_build, to_solve, At, b, c, scalar = true)
    @timeit "build" x = generate_moi_problem(to_build, At, b, c, scalar = scalar)
    if to_build !== to_solve
        @timeit "copy" MOI.copy_to(to_solve, to_build, copy_names = false)
    end
    @time @timeit "opt" MOI.optimize!(to_solve)
    MOI.get(to_solve, MOI.ObjectiveValue())
    val = MOI.get(to_solve, MOI.SolveTime())
    println(val)
end

function solve_clp(seed, data; time_limit_sec=Inf)

    reset_timer!()

    for i in 1:seed

        @timeit "data"  At, b, c = random_data(i, data)

        bridged_cache, pure_solver = bridged_cache_and_solver()
        @timeit "bc + s" time_build_and_solve(bridged_cache, pure_solver, At, b, c)

        cache, pure_solver2 = cache_and_solver()
        @timeit "c + s" time_build_and_solve(cache, pure_solver2, At, b, c)

        full_solver = bridged_cached_solver()
        @timeit "bcs" time_build_and_solve(full_solver, full_solver, At, b, c)

        full_solver = bridged_cached_solver()
        @timeit "bcs + v" time_build_and_solve(full_solver, full_solver, At, b, c, false)

        cache_solver = cached_solver()
        @timeit "cs" time_build_and_solve(cache_solver, cache_solver, At, b, c)

    end

    print_timer()

end

solve_clp(10, RandomLP(10000, 20000, 0.01); time_limit_sec=5)


@testset "a† and a" begin
    v3 = zeros(Float64, 5); v3[4] = 1
    v4 = zeros(Float64, 5); v4[5] = 1
    @test create(v3) ≈ √(3+1) * v4
    @test annihilate(v4) ≈ √4 * v3
    @test eltype(create(v3)) === Float64
    @test eltype(annihilate(v4)) === Float64

    ρ3 = zeros(Float64, 5, 5); ρ3[4, 4] = 1
    ρ4 = zeros(Float64, 5, 5); ρ4[5, 5] = 1
    @test create(ρ3) ≈ (3+1) * ρ4
    @test annihilate(ρ4) ≈ 4 * ρ3
    @test eltype(create(ρ3)) === Float64
    @test eltype(annihilate(ρ4)) === Float64

    v3 = zeros(Float32, 5); v3[4] = 1
    v4 = zeros(Float32, 5); v4[5] = 1
    @test create(v3) ≈ √(3+1) * v4
    @test annihilate(v4) ≈ √4 * v3
    @test eltype(create(v3)) === Float32
    @test eltype(annihilate(v4)) === Float32

    ρ3 = zeros(Float32, 5, 5); ρ3[4, 4] = 1
    ρ4 = zeros(Float32, 5, 5); ρ4[5, 5] = 1
    @test create(ρ3) ≈ (3+1) * ρ4
    @test annihilate(ρ4) ≈ 4 * ρ3
    @test eltype(create(ρ3)) === Float32
    @test eltype(annihilate(ρ4)) === Float32
end

@testset "α and ξ" begin
    @test repr(QSB.ComplexVec(2., π/4)) == "ComplexVec{Float64}(2.0exp(-$(π/4)im))"
    @test repr(QSB.ComplexVec(2, 3)) == "ComplexVec{Int64}(2exp(-3im))"
    @test QSB.z(QSB.ComplexVec(2., π/4)) ≈ 2 * exp(-im * π/4)
end

@testset "Displacement" begin
    T = Float64
    v0 = zeros(T, DIM); v0[1] = 1
    α = QSB.ComplexVec(2., π/4)
    @test displace(v0, 2, π/4) ≈
        exp(collect(QSB.z(α) * QSB.Creation(T, dim=DIM) - QSB.z(α)' * QSB.Annihilation(T, dim=DIM))) * v0
    @test eltype(displace(v0, 2, π/4)) == complex(T)

    T = Float32
    v0 = zeros(T, DIM); v0[1] = 1
    α = QSB.ComplexVec(2., π/4)
    @test displace(v0, 2, π/4) ≈
        exp(collect(QSB.z(α) * QSB.Creation(T, dim=DIM) - QSB.z(α)' * QSB.Annihilation(T, dim=DIM))) * v0
    @test eltype(displace(v0, 2, π/4)) == complex(T)
end

@testset "Squeezing" begin
    T = Float64
    v0 = zeros(T, DIM); v0[1] = 1
    ξ = QSB.ComplexVec(1., π/4)
    @test squeeze(v0, 1, π/4) ≈
        exp(collect(0.5QSB.z(ξ)' * QSB.Annihilation(T, dim=DIM)^2 - 0.5QSB.z(ξ) * QSB.Creation(T, dim=DIM)^2)) * v0
    @test eltype(squeeze(v0, 1, π/4)) == complex(T)

    T = Float32
    v0 = zeros(T, DIM); v0[1] = 1
    ξ = QSB.ComplexVec(1., π/4)
    @test squeeze(v0, 1, π/4) ≈
        exp(collect(0.5QSB.z(ξ)' * QSB.Annihilation(T, dim=DIM)^2 - 0.5QSB.z(ξ) * QSB.Creation(T, dim=DIM)^2)) * v0
    @test eltype(squeeze(v0, 1, π/4)) == complex(T)
end


using SkewDist, Distributions, Base.Test

println("Testing MvSkewTDist...")

srand(1)
N = 1000000 # Sample size

ξ = [0.0, -0.1]
α = [1.0, 0.5]
Ω = rand_pd(2)
df = 4.0

dist = MvSkewTDist(ξ, Ω, α, df)
rand(dist)

Z = rand(dist, N)

@test_approx_eq_eps mean(Z, 2) mean(dist) 5e-3
@test_approx_eq_eps var(Z, 2) var(dist) 1e-2
@test_approx_eq_eps Base.covm(Z, mean(Z, 2), 2) cov(dist) 1e-2


notenames = Dict(0=>"A",1=>"Bb",2=>"B",3=>"C",4=>"Db",5=>"D",
    6=>"Eb",7=>"E",8=>"F",9=>"Gb",10=>"G",11=>"Ab")

convlist = Dict("C#"=>"Db","D#"=>"Eb","F#"=>"Gb","G#"=>"Ab","A#"=>"Bb")

"""Calculate dBSPL of pressure x"""
dBSPL(x) = 20log10(abs(x/2.0e-5))

"""
Hz to Note name
    hztonote(f,pitch=440.0)
"""
function hztonote(f,pitch=440.0)
    c = 1200log2(f/pitch)
    n = Int(mod(c÷100,12)) # note number
    cc = c%100 # cents remainder
    if cc > 50
        cc = 100-cc
        n+=1
    elseif cc <= -50
        cc = 100+cc
        n-=1
        if n < 0
            n += 12
        end
    end
    octbase = pitch*2^(-95/120) # c'-50cent
    oc =Int(floor(log2(f/octbase)))
    nm = notenames[n]
    if oc > -2
        nm = lowercase(nm)
    end
    if oc > -1
        om = "'"^(oc+1)
    elseif oc < -2
        om = ","^(-2-oc)
    else
        om = ""
    end
    nm *= om
    return nm,cc
end

function notenameconv(name)
    if name in keys(convlist)
        nm = convlist[name]
    else
        nm = name
    end
    nm
end

function notenumber(s)
    ss = notenameconv(uppercasefirst(s))
    for p in notenames
        if p.second == ss
            return p.first
        end
    end
    return -1
end

"""
Convert note and cent to Hz
"""
function notetohz(name, cent, pitch = 440.0)
    if isuppercase(name[1])
        om = replace(name,r"[^,]"=>"")
        nm = replace(name,","=>"")
        oc = -3 - length(om)
    else
        om = replace(name,r"[^']"=>"")
        nm = replace(name,"'"=>"")
        oc = length(om)-2
    end
    nn = notenumber(nm)
    if nn < 3 # lower than c
        oc+=1
    end
    f = pitch * 2^((nn*100+cent)/1200+oc)
#     @show nm,nn,oc
    return f
end


"""
    rf = downscale_spaceonly(r,f,weight=1.;fglob=false, fsmooth=false )

Downscale precipitation field `r` using metagaussian spectral field `f`. An optional weights array can be specified. Precipitation can be conserved globally (`fglob`) or using convolution (`fsmooth`).
"""
function downscale_spaceonly(r,f,weight=1.;fglob=false, fsmooth=false )

(nas,nas)=size(r);
(ns,ns)=size(f);

# No negative rain
@compat ii=findall(r[:].<=0);
r[ii].=0;
rs=r;

# Gaussianize
# Alternative: rg=log(rs);
rg=gaussianize(rs);

# Prepare small scale random fields
g=metagauss(f);

# fix the amplitude of f
(pstg,pstgt)=fft3d(g);
pstg=pstg*((ns*ns)^2);
(pstrg,pstrgt)=fft3d(rg);
pstrg=pstrg*((nas*nas)^2);

varg=pstg[div(nas,2)];
varrg=pstrg[div(nas,2)];

c=varrg/varg;
g=g*sqrt(c);

gm=mergespec_spaceonly(rg,g,div(nas,2));

st=Statistics.std(gm);
if st==0.
   st=1.;
end
gm=gm/st;

fm=exp.(gm);

fm=fm.*weight;

# We want the aggregated field to be the same as pa
@compat ii=findall(isnan.(r)); fm[ii].=NaN;

if(fglob)
@compat  ii=findall(.~isnan.(fm));
    ri=interpola(r,ns,1);
    fm=fm*Statistics.mean(ri[ii])/Statistics.mean(fm[ii])
elseif(fsmooth)
@compat    ii=findall(isnan.(fm));
    fmi=interpola(agg(fm,nas,1),ns,1);
    fmi[ii].=NaN;
    fma=smoothconv(fmi,nas);
 # fma=smoothconv(fm,nas);
    ri=interpola(r,ns,1);
    ri[ii].=NaN;
    raa=smoothconv(ri,nas);
    fm=raa./fma.*fm; #pa=p aggregato a L0 e T0;
else
#fm=mergespec_spaceonly(fm,raa,div(nas,2));
    raa=agg(r,nas,1);
    fma=agg(fm,nas,1);
    ca=raa./fma; #pa=p aggregato a L0 e T0;
    cai=interpola(ca,ns,1);
    fm=cai.*fm;
end

return fm
end


# Given the log likelihood (LL) of each seed,
# calculate the log of the mean likelihood,
# i.e. LL = log (sum(LL_seeds) / nseeds)
# INSTEAD OF the mean of the log likelihoods
# i.e. sum (log (LL_seeds)) / nseeds
# Thanks to  Alireza Modirshanechi!
function get_meanLL_acrossseeds(LL_seeds)
    LL_max, seed_idx = findmax(LL_seeds)
    tempsum = 0.
    for i in 1:length(LL_seeds)
        tempsum += exp(LL_seeds[i] - LL_max)
    end
    LL = LL_max + log(tempsum) - log(length(LL_seeds))
end


using JuMP, Ipopt, Optim, LineSearches, LinearAlgebra, SparseArrays, Distributions, DataFrames, CSV, HTTP

function dense_hessian(hessian_sparsity, V, n)
    I = [i for (i,j) in hessian_sparsity]
    J = [j for (i,j) in hessian_sparsity]
    raw = sparse(I, J, V, n, n)
    return Matrix(raw + raw' - sparse(diagm(0=>diag(raw))))
end

function wrapper()
    # Let's read in the data from PS8
    url = "https://raw.githubusercontent.com/OU-PhD-Econometrics/fall-2020/master/ProblemSets/PS8-factor/nlsy.csv"
    df = CSV.read(HTTP.get(url).body)
    X = [df.black df.hispanic df.female df.schoolt df.gradHS df.grad4yr ones(size(df,1),1)]
    y = df.logwage

    # first let's do unconstrained optimization
    function reg_mle(θ, X, y)
        # first K elements are the coefficients of the outcome equation
        β = θ[1:end-1]
        # last element is the variance (stdev)
        σ = θ[end]
        # now build the likelihood
        loglike = -sum(-.5 .* ( log(2*π) .+ log(σ^2) .+ ( (y .- X*β)./σ ).^2 ) )
        # more intuitive way? (but JuMP can't use pdf's from Distributions.jl)
        #loglike = -sum( log(1 ./ sqrt(σ^2)) .+ logpdf.(Normal(0,1),(y .- X*β)./sqrt(σ^2)) )
        return loglike
    end

    # run the optimizer for MLE
    svals = vcat(X\y,.5);
    td = TwiceDifferentiable(th -> reg_mle(th, X, y), svals; autodiff = :forward)
    θ̂_optim_ad = optimize(td, svals, Newton(linesearch = BackTracking()), Optim.Options(g_tol = 1e-5, iterations=100_000, show_trace=true, show_every=1))
    θ̂_mle_optim_ad = θ̂_optim_ad.minimizer
    loglikeval = θ̂_optim_ad.minimum
    # evaluate the Hessian at the estimates
    H  = Optim.hessian!(td, θ̂_mle_optim_ad)
    θ̂_mle_optim_ad_se = sqrt.(diag(inv(H)))
    # store results in a data frame
    results = DataFrame(coef_mle = vcat(vec(θ̂_mle_optim_ad),-loglikeval), se_mle = vcat(vec(θ̂_mle_optim_ad_se),missing), coef_ols = vcat(X\y,missing,missing) )

    # now use JuMP (objective function + optimization call is all in one function here)
    function jump_mle(θ₀, X, y)
        # define the model
        model = Model(Ipopt.Optimizer)
        set_silent(model)
        @variable(model, β[j=1:size(X,2)], start = θ₀[j])
        @variable(model, σ, start = θ₀[end])
        @NLobjective(model, Max, sum(-.5 * ( log(2*π) + log(σ^2) + ((y[i] - sum(X[i,j]*β[j] for j in 1:size(X,2)) )/σ)^2 ) for i in 1:size(X,1) ) )
        # optimize the model
        JuMP.optimize!(model)
        # return parameter estimates
        coef_jump = vcat(JuMP.value.(β), JuMP.value(σ), JuMP.objective_value(model) )
        # return Hessian for SEs
        values = coef_jump[1:end-1]
        MOI = JuMP.MathOptInterface
        d = JuMP.NLPEvaluator(model)
        MOI.initialize(d, [:Hess])
        hessian_sparsity = MOI.hessian_lagrangian_structure(d)
        V = zeros(length(hessian_sparsity))
        MOI.eval_hessian_lagrangian(d, V, values, 1.0, Float64[])
        H = dense_hessian(hessian_sparsity, V, length(values))
        se_jump = sqrt.(diag(inv(-H)))
        return coef_jump, se_jump
    end
    jump_coefs,jump_se = jump_mle(svals, X, y)
    results.coef_jump = jump_coefs
    results.se_jump = vcat(jump_se,missing)

    # now let's use JuMP to estimate a constrained optimization
    function jump_cns_mle(θ₀, X, y)
        # define the model
        model = Model(Ipopt.Optimizer)
        set_silent(model)
        @variable(model, β[j=1:size(X,2)], start = θ₀[j])
        @variable(model, σ, start = θ₀[end])
        @constraint(model, β[2] == .16)
        @NLobjective(model, Max, sum(-.5 * ( log(2*π) + log(σ^2) + ((y[i] - sum(X[i,j]*β[j] for j in 1:size(X,2)) )/σ)^2 ) for i in 1:size(X,1) ) )
        # optimize the model
        JuMP.optimize!(model)
        # return parameter estimates
        coef_jump = vcat(JuMP.value.(β), JuMP.value(σ), JuMP.objective_value(model) )
        # return Hessian for SEs
        values = coef_jump[1:end-1]
        MOI = JuMP.MathOptInterface
        d = JuMP.NLPEvaluator(model)
        MOI.initialize(d, [:Hess])
        hessian_sparsity = MOI.hessian_lagrangian_structure(d)
        V = zeros(length(hessian_sparsity))
        MOI.eval_hessian_lagrangian(d, V, values, 1.0, Float64[])
        H = dense_hessian(hessian_sparsity, V, length(values))
        se_jump = sqrt.(diag(inv(-H)))
        return coef_jump, se_jump
    end
    jump_cns_coefs,jump_cns_se = jump_cns_mle(svals, X, y)
    results.coef_jump_cns = jump_cns_coefs
    results.se_jump_cns = vcat(jump_cns_se,missing)
    @show results

    # can we use optim for the same constrained optimization?
    function cns_reg_mle(θ, cns_mat, X, y)
        # first K elements are the coefficients of the outcome equation
        β = θ[1:end-1]
        # last element is the variance (stdev)
        σ = θ[end]
        # impose constraint
        for r in size(cns_mat,1)
            insert!(β,convert(Int64,cns_mat[r,1]),cns_mat[r,2])
        end
        # now build the likelihood
        loglike = -sum(-.5 .* ( log(2*π) .+ log(σ^2) .+ ( (y .- X*β)./σ ).^2 ) )
        # more intuitive way? (but JuMP can't use pdf's from Distributions.jl)
        #loglike = -sum( log(1 ./ sqrt(σ^2)) .+ logpdf.(Normal(0,1),(y .- X*β)./sqrt(σ^2)) )
        return loglike
    end

    # run the optimizer for MLE
    # first, set up constraints
    cns_mat = hcat(2,.16)
    svals = vcat(X\y,.5)
    # constraints are treated as data, so take them out of starting values (they get added back in inside the obj function)
    deleteat!(svals, convert(Int64,cns_mat[1,1]))
    td = TwiceDifferentiable(th -> cns_reg_mle(th, cns_mat, X, y), svals; autodiff = :forward)
    θ̂_optim_ad = optimize(td, svals, Newton(linesearch = BackTracking()), Optim.Options(g_tol = 1e-5, iterations=100_000, show_trace=true, show_every=1))
    θ̂_mle_optim_ad = θ̂_optim_ad.minimizer
    loglikeval = θ̂_optim_ad.minimum
    # evaluate the Hessian at the estimates
    H  = Optim.hessian!(td, θ̂_mle_optim_ad)
    θ̂_mle_optim_ad_se = sqrt.(diag(inv(H)))
    # add back in constraint in both estimates and SEs
    insert!(θ̂_mle_optim_ad   ,convert(Int64,cns_mat[1,1]),cns_mat[1,2])
    insert!(θ̂_mle_optim_ad_se,convert(Int64,cns_mat[1,1]),0)
    # store results in a data frame
    results.coef_optim_cns = vcat(θ̂_mle_optim_ad,-loglikeval)
    results.se_optim_cns   = vcat(θ̂_mle_optim_ad_se,missing)
    @show results

    # now let's use JuMP to estimate another constrained optimization
    function jump_cns2_mle(θ₀, X, y)
        # define the model
        model = Model(Ipopt.Optimizer)
        set_silent(model)
        @variable(model, β[j=1:size(X,2)], start = θ₀[j])
        @variable(model, σ, start = θ₀[end])
        @constraint(model, β[2] == .16)
        @constraint(model, β[4] == 1+2*β[3])
        @NLobjective(model, Max, sum(-.5 * ( log(2*π) + log(σ^2) + ((y[i] - sum(X[i,j]*β[j] for j in 1:size(X,2)) )/σ)^2 ) for i in 1:size(X,1) ) )
        # optimize the model
        JuMP.optimize!(model)
        # return parameter estimates
        coef_jump = vcat(JuMP.value.(β), JuMP.value(σ), JuMP.objective_value(model) )
        # return Hessian for SEs
        values = coef_jump[1:end-1]
        MOI = JuMP.MathOptInterface
        d = JuMP.NLPEvaluator(model)
        MOI.initialize(d, [:Hess])
        hessian_sparsity = MOI.hessian_lagrangian_structure(d)
        V = zeros(length(hessian_sparsity))
        MOI.eval_hessian_lagrangian(d, V, values, 1.0, Float64[])
        H = dense_hessian(hessian_sparsity, V, length(values))
        se_jump = sqrt.(diag(inv(-H)))
        return coef_jump, se_jump
    end
    jump_cns2_coefs,jump_cns2_se = jump_cns2_mle(svals, X, y)
    results.coef_jump_cns2 = jump_cns2_coefs
    results.se_jump_cns2 = vcat(jump_cns2_se,missing)
    @show results

    # can we use optim for the same constrained optimization?
    function cns2_reg_mle(θ, cns_mat, X, y)
        # first K elements are the coefficients of the outcome equation
        β = θ[1:end-1]
        # last element is the variance (stdev)
        σ = θ[end]
        # impose constraints
        for r in 1:size(cns_mat,1)
            idx1 = convert(Int64,cns_mat[r,1])
            idx2 = convert(Int64,cns_mat[r,2])
            if cns_mat[r,3]==0
                insert!(β,idx1,cns_mat[r,5])
            else
                insert!(β,idx1,cns_mat[r,5]+cns_mat[r,4]*β[idx2])
            end
        end
        # now build the likelihood
        loglike = -sum(-.5 .* ( log(2*π) .+ log(σ^2) .+ ( (y .- X*β)./σ ).^2 ) )
        # more intuitive way? (but JuMP can't use pdf's from Distributions.jl)
        #loglike = -sum( log(1 ./ sqrt(σ^2)) .+ logpdf.(Normal(0,1),(y .- X*β)./sqrt(σ^2)) )
        return loglike
    end

    # run the optimizer for MLE
    # now we need additional information: whether the constraint is "type 1" (set equal to fixed value) or "type 2" (set equal to another parameter)
    # first, set up constraints
    #     Type 1  Restricting one parameter ("parmA") to equal a fixed value
    #     Type 2  Restricting one parameter, parmA, to equal another ("parmB"),
    #             potentially multiplied by some real number q and addd to
    #             some constant m, e.g. parmA = m + q*parmB.
    #
    #   RESTRMAT follows a very specific format. It is an R-by-5 matrix,
    #   where R is the number of restrictions. The role of each of the four
    #   columns is as follows
    #
    #   Column 1  The index of parmA
    #   Column 2  The index of parmB (zero if type 1 restriction)
    #   Column 3  Binary vector w   here 0 indciates a type 1 restriction (parmA
    #               set equal to fixed value) and 1 indicates a type 2
    #               restriction (parmA set equal to parmB)
    #   Column 4  If a type 1 restriction, 0. If     a type 2 restriction, any
    #               real number q such that parmA = q*parmB.
    #   Column 5  If a type 1 restriction, the fixed value. If a type 2
    #                restriction, any real number m such that parmA = m+q*parmB.
    #   NOTE: parmA should always be a later index than parmB
    cns_mat2 = [2 0 0 0 .16;
                4 3 1 2 1]
    svals = vcat(X\y,.5)
    # constraints are treated as data, so take them out of starting values (they get added back in inside the obj function)
    for r=1:size(cns_mat2,1)
        deleteat!(svals, convert(Int64,cns_mat2[r,1]))
    end
    td = TwiceDifferentiable(th -> cns2_reg_mle(th, cns_mat2, X, y), svals; autodiff = :forward)
    θ̂_optim_ad = optimize(td, svals, Newton(linesearch = BackTracking()), Optim.Options(g_tol = 1e-5, iterations=100_000, show_trace=true, show_every=1))
    θ̂_mle_optim_ad = θ̂_optim_ad.minimizer
    loglikeval = θ̂_optim_ad.minimum
    # evaluate the Hessian at the estimates
    H  = Optim.hessian!(td, θ̂_mle_optim_ad)
    θ̂_mle_optim_ad_se = sqrt.(diag(inv(H)))
    println(θ̂_mle_optim_ad)
    # add back in constraint in both estimates and SEs
    for r in 1:size(cns_mat2,1)
        idx1 = convert(Int64,cns_mat2[r,1])
        idx2 = convert(Int64,cns_mat2[r,2])
        if cns_mat2[r,3]==0
            insert!(θ̂_mle_optim_ad,idx1,cns_mat2[r,5])
            insert!(θ̂_mle_optim_ad_se,idx1,0)
        else
            insert!(θ̂_mle_optim_ad,idx1,cns_mat2[r,5]+cns_mat2[r,4]*θ̂_mle_optim_ad[idx2])
            insert!(θ̂_mle_optim_ad_se,idx1,cns_mat2[r,5]+cns_mat2[r,4]*θ̂_mle_optim_ad_se[idx2]) # this is wrong
        end
        println(θ̂_mle_optim_ad)
    end
    # store results in a data frame
    results.coef_optim_cns2 = vcat(θ̂_mle_optim_ad,-loglikeval)
    results.se_optim_cns2   = vcat(θ̂_mle_optim_ad_se,missing)
    @show results

    # Analytical gradient
    @views @inline function asclogit(bstart::Vector,Y::Array,X::Array,Z::Array,J::Int64,baseAlt::Int64=J,W::Array=ones(length(Y)))

    ## error checking
    @assert ((!isempty(X) || !isempty(Z)) && !isempty(Y))    "You must supply data to the model"
    @assert (ndims(Y)==1 && size(Y,2)==1)                    "Y must be a 1-D Array"
    @assert (minimum(Y)==1 && maximum(Y)==J) "Y should contain integers numbered consecutively from 1 through J"
    if !isempty(X)
        @assert ndims(X)==2          "X must be a 2-dimensional matrix"
        @assert size(X,1)==size(Y,1) "The 1st dimension of X should equal the number of observations in Y"
    end
    if !isempty(Z)
        @assert ndims(Z)==3          "Z must be a 3-dimensional tensor"
        @assert size(Z,1)==size(Y,1) "The 1st dimension of Z should equal the number of observations in Y"
        @assert size(Z,3)==J         "The 3rd dimension of Z should equal the number of choice alternatives"
    end

    K1 = size(X,2)
    K2 = size(Z,2)
    jdx = setdiff(1:J,baseAlt)

    function f(b)
        T = promote_type(promote_type(promote_type(eltype(X),eltype(b)),eltype(Z)),eltype(W))
        num   = zeros(T,size(Y))
        dem   = zeros(T,size(Y))
        temp  = zeros(T,size(Y))
        numer =  ones(T,size(Y,1),J)
        P     = zeros(T,size(Y,1),J)
        ℓ     =  zero(T)
        b2 = b[K1*(J-1)+1:K1*(J-1)+K2]

        k = 1
        for j in 1:J
            if j != baseAlt
                temp       .= X*b[(k-1)*K1+1:k*K1] .+ (Z[:,:,j].-Z[:,:,baseAlt])*b2
                num        .= (Y.==j).*temp.+num
                dem        .+= exp.(temp)
                numer[:,j] .=  exp.(temp)
                k += 1
            end
        end
        dem.+=1
        P   .=numer./(1 .+ sum(numer;dims=2))

        ℓ = -W'*(num.-log.(dem))
    end

    function g!(G,b)
        T     = promote_type(promote_type(promote_type(eltype(X),eltype(b)),eltype(Z)),eltype(W))
        numer = zeros(T,size(Y,1),J)
        P     = zeros(T,size(Y,1),J)
        numg  = zeros(T,K2)
        demg  = zeros(T,K2)
        b2    = b[K1*(J-1)+1:K1*(J-1)+K2]

        G .= T(0)
        k = 1
        for j in 1:J
            if j != baseAlt
                numer[:,j] .= exp.( X*b[(k-1)*K1+1:k*K1] .+ (Z[:,:,j].-Z[:,:,baseAlt])*b2 )
                k += 1
            end
        end
        P   .=numer./(1 .+ sum(numer;dims=2))

        k = 1
        for j in 1:J
            if j != baseAlt
                G[(k-1)*K1+1:k*K1] .= -X'*(W.*((Y.==j).-P[:,j]))
                k += 1
            end
        end

        for j in 1:J
            if j != baseAlt
                numg .-= (Z[:,:,j].-Z[:,:,baseAlt])'*(W.*(Y.==j))
                demg .-= (Z[:,:,j].-Z[:,:,baseAlt])'*(W.*P[:,j])
            end
        end
        G[K1*(J-1)+1:K1*(J-1)+K2] .= numg.-demg
        return nothing
    end

    td = TwiceDifferentiable(f, g!, bstart, autodiff = :forwarddiff)
    rs = optimize(td, bstart, LBFGS(; linesearch = LineSearches.BackTracking()), Optim.Options(iterations=100_000,g_tol=1e-8,f_tol=1e-8,x_tol=1e-8,show_trace=true))
    β  = Optim.minimizer(rs)
    ℓ  = Optim.minimum(rs)*(-1)
    H  = Optim.hessian!(td, β)
    g  = Optim.gradient!(td, β)
    se = sqrt.(diag(inv(H)))

    return β,se,ℓ,g
    end

    url = "https://raw.githubusercontent.com/OU-PhD-Econometrics/fall-2020/master/ProblemSets/PS4-mixture/nlsw88t.csv"
    dff = CSV.read(HTTP.get(url).body)
    XX = [dff.age dff.white dff.collgrad]
    ZZ = cat(dff.elnwage1, dff.elnwage2, dff.elnwage3, dff.elnwage4,
             dff.elnwage5, dff.elnwage6, dff.elnwage7, dff.elnwage8; dims=3)
    yy = dff.occ_code
    J  = 8
    startvals = [2*rand(7*size(XX,2)).-1; .1]
    β,se,ℓ,g = asclogit(startvals,yy,XX,ZZ,J,J,ones(length(yy)))
    ans = [ .0403744; .2439942; -1.57132; .0433254; .1468556; -2.959103; .1020574; .7473086; -4.12005; .0375628; .6884899; -3.65577; .0204543; -.3584007; -4.376929; .1074636; -.5263738; -6.199197; .1168824; -.2870554; -5.322248; 1.307477]
    dfr = DataFrame(β=β,se=se,answer=ans)
    # time: about 15 seconds
    @show dfr

    @views @inline function mlogit_with_Z(theta, X, Z, y)
        alpha = theta[1:end-1]
        gamma = theta[end]
        K = size(X,2)
        J = length(unique(y))
        N = length(y)
        bigY = zeros(N,J)
        for j=1:J
            bigY[:,j] = y.==j
        end
        bigAlpha = [reshape(alpha,K,J-1) zeros(K)]

        T = promote_type(eltype(X),eltype(theta))
        num   = zeros(T,N,J)
        dem   = zeros(T,N)
        for j=1:J
            num[:,j] = exp.(X*bigAlpha[:,j] .+ (Z[:,j] .- Z[:,J])*gamma)
            dem .+= num[:,j]
        end

        P = num./repeat(dem,1,J)

        loglike = -sum( bigY.*log.(P) )

        return loglike
    end
    ZZ = cat(dff.elnwage1, dff.elnwage2, dff.elnwage3, dff.elnwage4,
             dff.elnwage5, dff.elnwage6, dff.elnwage7, dff.elnwage8; dims=2)
    td = TwiceDifferentiable(theta -> mlogit_with_Z(theta, XX, ZZ, yy), startvals; autodiff = :forward)
    # run the optimizer
    theta_hat_optim_ad = optimize(td, startvals, LBFGS(; linesearch = LineSearches.BackTracking()), Optim.Options(iterations=100_000,g_tol=1e-8,f_tol=1e-8,x_tol=1e-8,show_trace=true))
    theta_hat_mle_ad = theta_hat_optim_ad.minimizer
    # evaluate the Hessian at the estimates
    H  = Optim.hessian!(td, theta_hat_mle_ad)
    theta_hat_mle_ad_se = sqrt.(diag(inv(H)))
    results = DataFrame(coef = theta_hat_mle_ad, se = theta_hat_mle_ad_se)

    # time: about 70 seconds

    return nothing
end

wrapper()


# utility based risk measures --> build check for utility functions

"""
    spectral(states, prob, spec)

implements the spectral risk measure
```math
\\rho(Y) := \\int_0^1 spec(x) \\cdot F_Y^{-1}(\\alpha) d\\alpha,
```
where ``Y`` is the discrete random variable defined by `states` and `prob` and ``spec(\\cdot)`` denotes the spectral function which is

+ increasing
+ integrates to 1.
"""
function spectral(states, prob,spec)
    # spec(x) = x^2
    # spectral(spec, [1, 2], [0.2, 0.8])

    # ToDo: check that prob is probability vector

    #ToDo: check that spec is a spectral function
    if checkSpectral(spec) == false
        return(nothing)
    end

    # create distribution
    d = DiscreteNonParametric(states, prob)

    # approximate integral
    integral, err = quadgk(x -> quantile(d,x)*spec(x), 0, 1, rtol=1e-8)
	return(integral)
end

"""
    distortion(states, prob,dist)

implements the distortion risk measure
```math
\\rho(Y) := \\int_0^\\infty 1- dist(F_Y(x)) dx - \\int_{-\\infty}^0 dist(F_Y(x) dx,
```
where ``Y`` is the discrete random variable defined by `states` and `prob` and ``dist(\\cdot)`` denotes the distortion function which is
a right-continuous distribution function.
"""
function distortion(states, prob,dist)
    d = DiscreteNonParametric(states, prob)

    minState = minimum(states)-1; maxState = maximum(states)+1;

    integral1, err1 = quadgk(x -> dist(cdf(d,x)), minState, 0, rtol=1e-10)
    integral2, err2 = quadgk(x -> 1-dist(cdf(d,x)), 0, maxState, rtol=1e-10)
    return (integral2 - integral1)
end

"""
    GenCoherent(states, prob,conds)

implements a generic coherent risk measure based on the dual representation
```math
\\rho(Y) := \\sup \\left( \\mathbb{E} YZ \\colon \\mathbb{E}Z=1, Z\\geq 0, F(Z) \\leq c \\right),
```
where ``F(\\cdot)`` is a positively homegeneous and convex function and c is a constant.
"""
function GenCoherent(states, prob,conds)

# For testing :
 #states = [1 2 3]; prob = [0.3 0.4 0.3]
 #o1 = :( (Y)^2 *p)
 #o2 = :(sqrt)
 #oJoin = :+
 #conds = [o1 oJoin o2]

    n = length(prob); Zopt = ones(n);
    o1 = conds[1]; o2 = conds[3]
    oJoin = conds[2]

    EV = Model(with_optimizer(Ipopt.Optimizer, print_level=2))
    @variable(EV, 0 <= Z[1:n] )
    EV[:Z]

    o_tmp = :(0)
    for i=1:n
        global Y = Z[i]
        global p = prob[i]
        bla = eval(o1)
        o_tmp = math_expr(oJoin,o_tmp, bla)
    end
    o_tmp = math_expr(o2,o_tmp) # if no operation shall be carried out a unary plus o2 = :+ suffices

    add_NL_constraint(EV, :($(o_tmp) <= 2))
    @NLobjective(EV, Max, sum(states[i]*Z[i]*prob[i] for i in 1:n) )
    @constraint(EV, normalized, dot(prob,Z)==1)

    JuMP.optimize!(EV)
    EVaR = JuMP.objective_value(EV)
    ZOpt = JuMP.value.(Z)

    return(EVaR, ZOpt)

end

"""
    GenConvex(states, prob, conds, conjugate)

implements a generic coherent risk measure based on the dual representation
```math
\\rho(Y) := \\sup \\left( \\mathbb{E} YZ - conjugate(Z) \\colon \\mathbb{E}Z=1, Z\\geq 0 \\right),
```
where ``conjugate(\\cdot)`` is a and convex function and conds describes the domain of ``conjugate(\\cdot)``.
"""
function GenConvex(states, prob,conds,conjugate)
    # based on the dual representation a set of conditions can be set and
    # using IpOpt + JuMP the risk measure can be considered.

	n = length(prob); Zopt = ones(n);
    o1 = conds[1]; o2 = conds[3]
    oJoin = conds[2]

    EV = Model(with_optimizer(Ipopt.Optimizer, print_level=2))
    @variable(EV, 0 <= Z[1:n] )

    #register(EV, :conjugate,1,conjugate, autodiff=true)

    o_tmp = :(0)
    o_tmp2 = :(0)
    for i=1:n
        # First the set construction
        global Y = Z[i]
        global p = prob[i]
        bla = eval(o1)
        o_tmp = math_expr(oJoin,o_tmp, bla)

        #Now the convex conjugate
        Conj = eval(conjugate)
        o_tmp2 = math_expr(:+,o_tmp2, Conj)
    end
    o_tmp = math_expr(o2,o_tmp) # if no operation shall be carried out a unary plus o2 = :+ suffices

    #add_NL_constraint(EV, :($(o_tmp) <= 2))
    @NLobjective(EV, Max, sum(states[i]*Z[i]*prob[i] for i in 1:n) - 0.5*sum(Z[i]^2 for i in 1:n) ) # - conjugate(Z) should be added
    @constraint(EV, normalized, dot(prob,Z)==1)

    JuMP.optimize!(EV)
    EVaR = JuMP.objective_value(EV)
    ZOpt = JuMP.value.(Z)

    #return(EVaR, ZOpt)
    return(nothing)
end


using IterativeSolvers
using LinearMaps
using LinearAlgebra
using Test
using Random

# Already defined in another file
#=
include("laplace_matrix.jl")

struct JacobiPrec{TD}
    diagonal::TD
end

Base.ldiv!(y, P::JacobiPrec, x) = y .= x ./ P.diagonal
=#

function max_err(R)
    r = zeros(real(eltype(R)), size(R, 2))
    for j in 1:length(r)
        for i in 1:size(R, 1)
            r[j] += conj(R[i,j])*R[i,j]
        end
        r[j] = sqrt(r[j])
    end
    maximum(r)
end

@testset "Locally Optimal Block Preconditioned Conjugate Gradient" begin
    srand(1234323)
    @testset "Single eigenvalue" begin
        n = 10
        @testset "Small full system" begin
            @testset "Simple eigenvalue problem" begin
                @testset "Matrix{$T}" for T in (Float32, Float64, ComplexF32, ComplexF64)
                    @testset "largest = $largest" for largest in (true, false)
                        A = rand(T, n, n)
                        A = A' + A + 20I
                        b = rand(T, n, 1)
                        tol = IterativeSolvers.default_tolerance(T)
                        r = lobpcg(A, largest, b; tol=tol, maxiter=Inf, log=false)
                        λ, X = r.λ, r.X
                        @test norm(A*X - X*λ) ≤ tol

                        # If you start from the exact solution, you should converge immediately
                        r = lobpcg(A, largest, X; tol=10tol, log=true)
                        @test length(r.trace) == 1
                    end
                end
            end
            @testset "Generalized eigenvalue problem" begin
                @testset "Matrix{$T}" for T in (Float32, Float64, ComplexF32, ComplexF64)
                    @testset "largest = $largest" for largest in (true, false)
                        A = rand(T, n, n)
                        A = A' + A + 20I
                        B = rand(T, n, n)
                        B = B' + B + 20I
                        b = rand(T, n, 1)
                        tol = IterativeSolvers.default_tolerance(T)
                        r = lobpcg(A, B, largest, b; tol=tol, maxiter=Inf, log=true)
                        λ, X = r.λ, r.X
                        @test max_err(A*X - B*X*λ) ≤ tol

                        # If you start from the exact solution, you should converge immediately
                        r = lobpcg(A, B, largest, X; tol=10tol, log=true)
                        @test length(r.trace) == 1
                    end
                end
            end
        end
        @testset "Sparse Laplacian" begin
            A = laplace_matrix(Float64, 20, 2)
            rhs = randn(size(A, 2), 1)
            rmul!(rhs, inv(norm(rhs)))
            tol = IterativeSolvers.default_tolerance(Float64)
            @testset "Matrix" begin
                @testset "largest = $largest" for largest in (true, false)
                    r = lobpcg(A, largest, rhs; tol=tol, maxiter=Inf)
                    λ, X = r.λ, r.X
                    @test norm(A*X - X*λ) ≤ tol
                end
            end
        end
        @testset "Zero initial solution" begin
            @testset "Simple eigenvalue problem" begin
                @testset "Matrix{$T}" for T in (Float32, Float64, ComplexF32, ComplexF64)
                    @testset "largest = $largest" for largest in (true, false)
                        A = rand(T, n, n)
                        A = A' + A + 20I
                        b = zeros(T, n, 1)
                        tol = IterativeSolvers.default_tolerance(T)
                        r = lobpcg(A, largest, b; tol=tol, maxiter=Inf, log=false)
                        λ, X = r.λ, r.X
                        @test norm(A*X - X*λ) ≤ tol
                    end
                end
            end
            @testset "Generalized eigenvalue problem" begin
                @testset "Matrix{$T}" for T in (Float32, Float64, ComplexF32, ComplexF64)
                    @testset "largest = $largest" for largest in (true, false)
                        A = rand(T, n, n)
                        A = A' + A + 20I
                        B = rand(T, n, n)
                        B = B' + B + 20I
                        b = zeros(T, n, 1)
                        tol = IterativeSolvers.default_tolerance(T)

                        r = lobpcg(A, B, largest, b; tol=tol, maxiter=Inf, log=true)
                        λ, X = r.λ, r.X
                        @test max_err(A*X - B*X*λ) ≤ tol
                    end
                end
            end
        end
        @testset "No initial solution" begin
            @testset "Simple eigenvalue problem" begin
                @testset "Matrix{$T}" for T in (Float32, Float64, ComplexF32, ComplexF64)
                    @testset "largest = $largest" for largest in (true, false)
                        A = rand(T, n, n)
                        A = A' + A + 20I
                        tol = IterativeSolvers.default_tolerance(T)

                        r = lobpcg(A, largest, 1; tol=tol, maxiter=Inf, log=false)
                        λ, X = r.λ, r.X
                        @test norm(A*X - X*λ) ≤ tol
                    end
                end
            end
            @testset "Generalized eigenvalue problem" begin
                @testset "Matrix{$T}" for T in (Float32, Float64, ComplexF32, ComplexF64)
                    @testset "largest = $largest" for largest in (true, false)
                        A = rand(T, n, n)
                        A = A' + A + 20I
                        B = rand(T, n, n)
                        B = B' + B + 20I
                        tol = IterativeSolvers.default_tolerance(T)

                        r = lobpcg(A, B, largest, 1; tol=tol, maxiter=Inf, log=true)
                        λ, X = r.λ, r.X
                        @test max_err(A*X - B*X*λ) ≤ tol
                    end
                end
            end
        end
        @testset "Inplace" begin
            @testset "Simple eigenvalue problem" begin
                @testset "Matrix{$T}" for T in (Float32, Float64, ComplexF32, ComplexF64)
                    @testset "largest = $largest" for largest in (true, false)
                        A = rand(T, n, n)
                        A = A' + A + 20I
                        tol = IterativeSolvers.default_tolerance(T)
                        b = rand(T, n, 1)
                        itr = LOBPCGIterator(A, largest, b)

                        r = lobpcg!(itr; tol=tol, maxiter=Inf, log=false)
                        λ, X = r.λ, r.X
                        @test norm(A*X - X*λ) ≤ tol
                    end
                end
            end
            @testset "Generalized eigenvalue problem" begin
                @testset "Matrix{$T}" for T in (Float32, Float64, ComplexF32, ComplexF64)
                    @testset "largest = $largest" for largest in (true, false)
                        A = rand(T, n, n)
                        A = A' + A + 20I
                        B = rand(T, n, n)
                        B = B' + B + 20I
                        b = rand(T, n, 1)
                        tol = IterativeSolvers.default_tolerance(T)
                        itr = LOBPCGIterator(A, B, largest, b)

                        r = lobpcg!(itr; tol=tol, maxiter=Inf, log=true)
                        λ, X = r.λ, r.X
                        @test max_err(A*X - B*X*λ) ≤ tol
                    end
                end
            end
        end
        @testset "Jacobi preconditioner" begin
            @testset "Simple eigenvalue problem" begin
                @testset "Matrix{$T}" for T in (Float32, Float64, ComplexF32, ComplexF64)
                    @testset "largest = $largest" for largest in (true, false)
                        A = rand(T, n, n)
                        A = A' + A + 20I
                        tol = IterativeSolvers.default_tolerance(T)
                        P = JacobiPrec(diag(A))
                        r = lobpcg(A, largest, 1; P=P, tol=tol, maxiter=Inf, log=false)
                        λ, X = r.λ, r.X
                        @test norm(A*X - X*λ) ≤ tol
                    end
                end
            end
            @testset "Generalized eigenvalue problem" begin
                @testset "Matrix{$T}" for T in (Float32, Float64, ComplexF32, ComplexF64)
                    @testset "largest = $largest" for largest in (true, false)
                        A = rand(T, n, n)
                        A = A' + A + 20I
                        P = JacobiPrec(diag(A))
                        B = rand(T, n, n)
                        B = B' + B + 20I
                        tol = IterativeSolvers.default_tolerance(T)

                        r = lobpcg(A, B, largest, 1; P=P, tol=tol, maxiter=Inf, log=true)
                        λ, X = r.λ, r.X
                        @test max_err(A*X - B*X*λ) ≤ tol
                    end
                end
            end
        end
        @testset "Constraint" begin
            @testset "Simple eigenvalue problem" begin
                @testset "Matrix{$T}" for T in (Float32, Float64, ComplexF32, ComplexF64)
                    @testset "largest = $largest" for largest in (true, false)
                        A = rand(T, n, n)
                        A = A' + A + 20I
                        tol = IterativeSolvers.default_tolerance(T)
                        r = lobpcg(A, largest, 1; tol=tol, maxiter=Inf, log=false)
                        λ1, X1 = r.λ, r.X
                        r = lobpcg(A, largest, 1; C=copy(r.X), tol=tol, maxiter=Inf, log=false)
                        λ2, X2 = r.λ, r.X
                        @test norm(A*X2 - X2*λ2) ≤ tol
                        @test isapprox(real((adjoint(X1)*X2)[1,1]), 0, atol=2*n*tol)
                    end
                end
            end
            @testset "Generalized eigenvalue problem" begin
                @testset "Matrix{$T}" for T in (Float32, Float64, ComplexF32, ComplexF64)
                    @testset "largest = $largest" for largest in (true, false)
                        A = rand(T, n, n)
                        A = A' + A + 20I
                        B = rand(T, n, n)
                        B = B' + B + 20I
                        tol = IterativeSolvers.default_tolerance(T)
                        r = lobpcg(A, B, largest, 1; tol=tol, maxiter=Inf, log=false)
                        λ1, X1 = r.λ, r.X
                        r = lobpcg(A, B, largest, 1; C=copy(r.X), tol=tol, maxiter=Inf, log=false)
                        λ2, X2 = r.λ, r.X
                        @test norm(A*X2 - B*X2*λ2) ≤ tol
                        @test isapprox(real((adjoint(X1)*(B*X2))[1,1]), 0, atol=2*n*tol)
                    end
                end
            end
        end
    end
    @testset "Two eigenvalues" begin
        @testset "Small full system" begin
            n = 10
            @testset "Simple eigenvalue problem" begin
                @testset "Matrix{$T}" for T in (Float32, Float64, ComplexF32, ComplexF64)
                    @testset "largest = $largest" for largest in (true, false)
                        A = rand(T, n, n)
                        A = A' + A + 20I
                        b = rand(T, n, 2)
                        tol = IterativeSolvers.default_tolerance(T)

                        r  = lobpcg(A, largest, b; tol=tol, maxiter=Inf, log=false)
                        λ, X = r.λ, r.X
                        @test max_err(A*X - X*Matrix(Diagonal(λ))) ≤ tol

                        # If you start from the exact solution, you should converge immediately
                        r = lobpcg(A, largest, X; tol=10tol, log=true)
                        @test length(r.trace) == 1
                    end
                end
            end
            @testset "Generalized eigenvalue problem" begin
                @testset "Matrix{$T}" for T in (Float32, Float64, ComplexF32, ComplexF64)
                    @testset "largest = $largest" for largest in (true, false)
                        A = rand(T, n, n)
                        A = A' + A + 20I
                        B = rand(T, n, n)
                        B = B' + B + 20I
                        b = rand(T, n, 2)
                        tol = IterativeSolvers.default_tolerance(T)
                        r = lobpcg(A, B, largest, b; tol=tol, maxiter=Inf, log=true)
                        λ, X = r.λ, r.X
                        @test max_err(A*X - B*X*Matrix(Diagonal(λ))) ≤ tol

                        # If you start from the exact solution, you should converge immediately
                        r = lobpcg(A, B, largest, X; tol=10tol, log=true)
                        @test length(r.trace) == 1
                    end
                end
            end
        end
    end
    @testset "nev = 3, block size = $block_size" for block_size in (1, 2)
        n = 10
        @testset "Simple eigenvalue problem" begin
            @testset "Matrix{$T}" for T in (Float32, Float64, ComplexF32, ComplexF64)
                @testset "largest = $largest" for largest in (true, false)
                    A = rand(T, n, n)
                    A = A' + A + 20I
                    tol = IterativeSolvers.default_tolerance(T)
                    X0 = rand(T, n, block_size)
                    r = lobpcg(A, largest, X0, 3, tol=tol, maxiter=Inf, log=true)
                    λ, X = r.λ, r.X
                    @test max_err(A*X - X*Matrix(Diagonal(λ))) ≤ tol
                    @test all(isapprox.(adjoint(X)*X, Matrix{T}(I, 3, 3), atol=2*n*tol))
                end
            end
        end
        @testset "Generalized eigenvalue problem" begin
            @testset "Matrix{$T}" for T in (Float32, Float64, ComplexF32, ComplexF64)
                @testset "largest = $largest" for largest in (true, false)
                    A = rand(T, n, n)
                    A = A' + A + 20I
                    B = rand(T, n, n)
                    B = B' + B + 20I
                    tol = IterativeSolvers.default_tolerance(T)
                    X0 = rand(T, n, block_size)
                    r = lobpcg(A, B, largest, X0, 3, tol=tol, maxiter=Inf, log=true)
                    λ, X = r.λ, r.X
                    @test max_err(A*X - B*X*Matrix(Diagonal(λ))) ≤ tol
                    @test all(isapprox.(adjoint(X)*(B*X), Matrix{T}(I, 3, 3), atol=2*n*tol))
                end
            end
        end
        @testset "Constraint" begin
            @testset "Simple eigenvalue problem" begin
                @testset "Matrix{$T}" for T in (Float32, Float64, ComplexF32, ComplexF64)
                    @testset "largest = $largest" for largest in (true, false)
                        A = rand(T, n, n)
                        A = A' + A + 20I
                        tol = IterativeSolvers.default_tolerance(T)
                        r = lobpcg(A, largest, 1; tol=tol, maxiter=Inf, log=false)
                        λ1, X1 = r.λ, r.X

                        X0 = rand(T, n, block_size)
                        r = lobpcg(A, largest, X0, 3, C=copy(r.X), tol=tol, maxiter=Inf, log=true)
                        λ2, X2 = r.λ, r.X
                        @test max_err(A*X2 - X2*Matrix(Diagonal(λ2))) ≤ tol
                        @test all(isapprox.(adjoint(X2)*X2, Matrix{T}(I, 3, 3), atol=2*n*tol))
                        @test all(isapprox.(real(adjoint(X1)*X2), 0, atol=2*n*tol))
                    end
                end
            end
            @testset "Generalized eigenvalue problem" begin
                @testset "Matrix{$T}" for T in (Float32, Float64, ComplexF32, ComplexF64)
                    @testset "largest = $largest" for largest in (true, false)
                        A = rand(T, n, n)
                        A = A' + A + 20I
                        B = rand(T, n, n)
                        B = B' + B + 20I
                        tol = IterativeSolvers.default_tolerance(T)
                        r = lobpcg(A, B, largest, 1; tol=tol, maxiter=Inf, log=false)
                        λ1, X1 = r.λ, r.X

                        X0 = rand(T, n, block_size)
                        r = lobpcg(A, B, largest, X0, 2, C=copy(r.X), tol=tol, maxiter=Inf, log=true)
                        λ2, X2 = r.λ, r.X
                        @test max_err(A*X2 - B*X2*Matrix(Diagonal(λ2))) ≤ tol
                        @test all(isapprox.(adjoint(X2)*(B*X2), Matrix{T}(I, 2, 2), atol=2*n*tol))
                        @test all(isapprox.(real(adjoint(X1)*(B*X2)), 0, atol=2*n*tol))
                    end
                end
            end
        end
    end
end


@doc raw"""
    UCME(k, testpredictions, testtargets)

Estimator of the unnormalized calibration mean embedding (UCME) with kernel `k` and sets of
`testpredictions` and `testtargets`.

Kernel `k` on the product space of predictions and targets has to be a `Kernel` from the
Julia package
[KernelFunctions.jl](https://github.com/JuliaGaussianProcesses/KernelFunctions.jl)
that can be evaluated for inputs that are tuples of predictions and targets.

The number of test predictions and test targets must be the same and at least one.

# Details

The estimator is biased and guaranteed to be non-negative. Its sample complexity
is ``O(mn)``, where ``m`` is the number of test locations and ``n`` is the total number of
samples.

Let ``(T_i)_{i=1,\ldots,m}`` be the set of test locations, i.e., test predictions and
corresponding targets, and let ``(P_{X_j}, Y_j)_{j=1,\ldots,n}`` be a data set of
predictions and corresponding targets. The plug-in estimator of ``\mathrm{UCME}_{k,m}^2``
is defined as
```math
m^{-1} \sum_{i=1}^{m} {\bigg(n^{-1} \sum_{j=1}^n k\big(T_i, (P_{X_j}, Y_j)\big)
- \mathbb{E}_{Z \sim P_{X_j}} k\big(T_i, (P_{X_j}, Z)\big)\bigg)}^2.
```

# References

Widmann, D., Lindsten, F., & Zachariah, D. (2021).
[Calibration tests beyond classification](https://openreview.net/forum?id=-bxf89v3Nx).
To be presented at *ICLR 2021*.
"""
struct UCME{K<:Kernel,TP,TT} <: CalibrationErrorEstimator
    """Kernel."""
    kernel::K
    """Test predictions."""
    testpredictions::TP
    """Test targets."""
    testtargets::TT

    function UCME{K,TP,TT}(kernel::K, testpredictions::TP, testtargets::TT) where {K,TP,TT}
        check_nsamples(testpredictions, testtargets)
        return new{K,TP,TT}(kernel, testpredictions, testtargets)
    end
end

function UCME(kernel::Kernel, testpredictions, testtargets)
    return UCME{typeof(kernel),typeof(testpredictions),typeof(testtargets)}(
        kernel, testpredictions, testtargets
    )
end

function (estimator::UCME)(predictions::AbstractVector, targets::AbstractVector)
    @unpack kernel, testpredictions, testtargets = estimator

    # obtain number of samples
    nsamples = check_nsamples(predictions, targets)

    # compute average over test locations
    estimate = mean(zip(testpredictions, testtargets)) do (tp, ty)
        unsafe_ucme_eval_testlocation(kernel, predictions, targets, tp, ty)
    end

    return estimate
end

function unsafe_ucme_eval_testlocation(
    kernel::Kernel,
    predictions::AbstractVector,
    targets::AbstractVector,
    testprediction,
    testtarget,
)
    # compute average over predictions and targets for the given test location
    estimate = mean(zip(predictions, targets)) do (p, y)
        unsafe_ucme_eval(kernel, p, y, testprediction, testtarget)
    end

    return estimate^2
end

function unsafe_ucme_eval(
    kernel::Kernel,
    p::AbstractVector{<:Real},
    y::Integer,
    testp::AbstractVector{<:Real},
    testy::Integer,
)
    res = sum(((z == y) - pz) * kernel((p, z), (testp, testy)) for (z, pz) in enumerate(p))
    return res
end
function unsafe_ucme_eval(kernel::Kernel, p::Real, y::Bool, testp::Real, testy::Bool)
    noty = !y
    return (y - p) * kernel((p, y), (testp, testy)) +
           (noty - p) * kernel((p, noty), (testp, testy))
end

function unsafe_ucme_eval(kernel::KernelTensorProduct, p, y, testp, testy)
    κpredictions, κtargets = kernel.kernels
    return unsafe_ucme_eval_targets(κtargets, p, y, testp, testy) * κpredictions(p, testp)
end
# resolve method ambiguity
function unsafe_ucme_eval(
    kernel::KernelTensorProduct,
    p::AbstractVector{<:Real},
    y::Integer,
    testp::AbstractVector{<:Real},
    testy::Integer,
)
    κpredictions, κtargets = kernel.kernels
    return unsafe_ucme_eval_targets(κtargets, p, y, testp, testy) * κpredictions(p, testp)
end
function unsafe_ucme_eval(
    kernel::KernelTensorProduct, p::Real, y::Bool, testp::Real, testy::Bool
)
    κpredictions, κtargets = kernel.kernels
    return unsafe_ucme_eval_targets(κtargets, p, y, testp, testy) * κpredictions(p, testp)
end

function unsafe_ucme_eval_targets(
    kernel::Kernel,
    p::AbstractVector{<:Real},
    y::Integer,
    testp::AbstractVector{<:Real},
    testy::Integer,
)
    res = sum(((z == y) - pz) * kernel(z, testy) for (z, pz) in enumerate(p))
    return res
end
function unsafe_ucme_eval_targets(
    kernel::Kernel, p::Real, y::Bool, testp::Real, testy::Bool
)
    noty = !y
    res = (y - p) * kernel(y, testy) + (noty - p) * kernel(noty, testy)
    return res
end

function unsafe_ucme_eval_targets(
    κtargets::WhiteKernel,
    p::AbstractVector{<:Real},
    y::Integer,
    testp::AbstractVector{<:Real},
    testy::Integer,
)
    @inbounds res = (y == testy) - p[testy]
    return res
end
function unsafe_ucme_eval_targets(
    kernel::WhiteKernel, p::Real, y::Bool, testp::Real, testy::Bool
)
    return (2 * testy - 1) * (y - p)
end


#=
This file actually implements the user interface of the Psychro library.
=#

"""
    volume(DryAir, T, P[, out_unit=u"m^3/kg"])
    volume(Vapor, T[, out_unit=u"m^3/kg"])
    volume(MoistAir, T, HumidityType, hum, P[, out_unit="m^3/kg"])

Calculates the specific volume of a gas. In the case of moist air, parameters specifying the humidity shoud be added and the the property is based on the mass of dry air not mass of the gas (as is the case for `DryAir` or `Vapor`).

If `Unitful` units are used, all parameters of the function (except dimensionless) should have units associated. When no units are provided, all parameters should use SI units (and so does the return type). For further information, checkout the doc for `Psychro` module.

Temperature should be in the range `173.15 K < T < 473.15` and pressure should be below 5 MPa.

# Examples
```julia-repl
julia> volume(DryAir, 293.15, 101325.0)
0.8302353995092402

julia> volume(DryAir, 20.0u"°C", 1.0u"atm")
0.8302353995092402 kg^-1 m^3

julia> volume(Vapor, 293.15)
57.77492045936827

julia> volume(Vapor, 20.0u"°C")
57.77492045936827 kg^-1 m^3

julia> volume(MoistAir, 293.15, RelHum, 0.7, 101325.0)
0.843889817602806

julia> volume(MoistAir, 20.0u"°C", WetBulb, 18.0u"°C", 1.0u"atm")
0.8464079202783964 kg^-1 m^3

julia> volume(MoistAir, 20.0u"°C", WetBulb, 18.0u"°C", 1.0u"atm", u"inch^3/lb")
23428.490579267214 in^3 lb^-1
```
"""
function volume(::Type{DryAir}, Tk, P)
    @assert 173.1 < Tk < 473.2 "Out of range error: Temperature should be between 173.15 K and 473.15 K!"
    @assert 0 <= P < 5e6 "Out of range error: Pressure should be below 5×10⁶ Pa"
    volumeair(Tk, P)
end

"""
    volumem(DryAir, T, P[, out_unit=u"m^3/kg"])
    volumem(Vapor, T[, out_unit=u"m^3/kg"])
    volumem(MoistAir, T, HumidityType, hum, P[, out_unit="m^3/kg"])

Calculates the molar volume of a gas. In the case of moist air, parameters specifying the humidity shoud be added.

If `Unitful` units are used, all parameters of the function (except dimensionless) should have units associated. When no units are provided, all parameters should use SI units (and so does the return type). For further information, checkout the doc for `Psychro` module.

Temperature should be in the range `173.15 K < T < 473.15` and pressure should be below 5 MPa.

# Examples
```julia-repl
julia> volumem(DryAir, 293.15, 101325.0)
0.024046522993685877

julia> volumem(DryAir, 20.0u"°C", 1.0u"atm")
0.024046522993685877 m^3 mol^-1

julia> volumem(Vapor, 293.15)
1.040831369053248

julia> volumem(Vapor, 20.0u"°C")
1.040831369053248 m^3 mol^-1

julia> volumem(MoistAir, 293.15, RelHum, 0.7, 101325.0)
0.02404547233948706

julia> volumem(MoistAir, 20.0u"°C", WetBulb, 18.0u"°C", 1.0u"atm")
0.024045209859305458 m^3 mol^-1

julia> volumem(MoistAir, 20.0u"°C", WetBulb, 18.0u"°C", 1.0u"atm", u"inch^3/mol")
1467.32873315839 in^3 mol^-1
```
"""
function volumem(::Type{DryAir}, Tk, P)
    @assert 173.1 < Tk < 473.2 "Out of range error: Temperature should be between 173.15 K and 473.15 K!"
    @assert 0 <= P < 5e6 "Out of range error: Pressure should be below 5×10⁶ Pa"
    molarvolumeair(Tk, P)
end

"""
    density(DryAir, T, P[, out_unit=u"m^3/kg"])
    density(Vapor, T[, out_unit=u"m^3/kg"])
    density(MoistAir, T, HumidityType, hum, P[, out_unit="m^3/kg"])

Calculates the density volume of a gas. In the case of moist air, parameters specifying the humidity shoud be added.

If `Unitful` units are used, all parameters of the function (except dimensionless) should have units associated. When no units are provided, all parameters should use SI units (and so does the return type). For further information, checkout the doc for `Psychro` module.

Temperature should be in the range `173.15 K < T < 473.15` and pressure should be below 5 MPa.

# Examples
```julia-repl
julia> density(DryAir, 293.15, 101325.0)
1.2044776705391136

julia> density(DryAir, 20.0u"°C", 1.0u"atm")
1.2044776705391136 kg m^-3

julia> density(Vapor, 293.15)
0.017308548277505224

julia> density(Vapor, 20.0u"°C")
0.017308548277505224 kg m^-3

julia> density(MoistAir, 293.15, RelHum, 0.7, 101325.0)
1.1971436091840653

julia> density(MoistAir, 20.0u"°C", WetBulb, 18.0u"°C", 1.0u"atm")
1.195819185774941 kg m^-3

julia> density(MoistAir, 20.0u"°C", WetBulb, 18.0u"°C", 1.0u"atm", u"lb/inch^3")
4.3201708903793606e-5 in^-3 lb

```
"""
function density(::Type{DryAir}, Tk, P)
    @assert 173.1 < Tk < 473.2 "Out of range error: Temperature should be between 173.15 K and 473.15 K!"
    @assert 0 <= P < 5e6 "Out of range error: Pressure should be below 5×10⁶ Pa"
    1.0/volumeair(Tk, P)
end

"""
    enthalpy(DryAir, T, P[, out_unit=u"m^3/kg"])
    enthalpy(Vapor, T[, out_unit=u"m^3/kg"])
    enthalpy(MoistAir, T, HumidityType, hum, P[, out_unit="m^3/kg"])

Calculates the specific enthalpy of a gas. In the case of moist air, parameters specifying the humidity shoud be added and the the property is based on the mass of dry air not mass of the gas (as is the case for `DryAir` or `Vapor`).

If `Unitful` units are used, all parameters of the function (except dimensionless) should have units associated. When no units are provided, all parameters should use SI units (and so does the return type). For further information, checkout the doc for `Psychro` module.

Temperature should be in the range `173.15 K < T < 473.15` and pressure should be below 5 MPa.

# Examples
```julia-repl
julia> enthalpy(DryAir, 293.15, 101325.0)
20121.2722813185

julia> enthalpy(DryAir, 20.0u"°C", 1.0u"atm")
20121.2722813185 kg^-1 J

julia> enthalpy(Vapor, 293.15)
2.5374003352412493e6

julia> enthalpy(Vapor, 20.0u"°C")
2.5374003352412493e6 kg^-1 J

julia> enthalpy(MoistAir, 293.15, RelHum, 0.7, 101325.0)
46142.773129687484

julia> enthalpy(MoistAir, 20.0u"°C", WetBulb, 18.0u"°C", 1.0u"atm")
50944.84575377501 kg^-1 J

julia> enthalpy(MoistAir, 20.0u"°C", WetBulb, 18.0u"°C", 1.0u"atm", u"J/lb")
23108.193324739244 J lb^-1

```
"""
function enthalpy(::Type{DryAir}, Tk, P)
    @assert 173.1 < Tk < 473.2 "Out of range error: Temperature should be between 173.15 K and 473.15 K!"
    @assert 0 <= P < 5e6 "Out of range error: Pressure should be below 5×10⁶ Pa"
    enthalpyair(Tk, P)
end

"""
    enthalpym(DryAir, T, P[, out_unit=u"m^3/kg"])
    enthalpym(Vapor, T[, out_unit=u"m^3/kg"])
    enthalpym(MoistAir, T, HumidityType, hum, P[, out_unit="m^3/kg"])

Calculates the molar enthalpy of a gas. In the case of moist air, parameters specifying the humidity shoud be added.

If `Unitful` units are used, all parameters of the function (except dimensionless) should have units associated. When no units are provided, all parameters should use SI units (and so does the return type). For further information, checkout the doc for `Psychro` module.

Temperature should be in the range `173.15 K < T < 473.15` and pressure should be below 5 MPa.

# Examples
```julia-repl
julia> enthalpym(DryAir, 293.15, 101325.0)
582.7824697199684

julia> enthalpym(DryAir, 20.0u"°C", 1.0u"atm")
582.7824697199684 J mol^-1

julia> enthalpy(Vapor, 293.15)
2.5374003352412493e6

julia> enthalpym(Vapor, 20.0u"°C")
45711.977511464975 J mol^-1

julia> enthalpym(MoistAir, 293.15, RelHum, 0.7, 101325.0)
1314.7744549269437

julia> enthalpym(MoistAir, 20.0u"°C", WetBulb, 18.0u"°C", 1.0u"atm")
1447.2684837312868 J mol^-1

julia> enthalpym(MoistAir, 20.0u"°C", WetBulb, 18.0u"°C", 1.0u"atm", u"kJ/kmol")
1447.2684837312868 kJ kmol^-1

```
"""
function enthalpym(::Type{DryAir}, Tk, P)
    @assert 173.1 < Tk < 473.2 "Out of range error: Temperature should be between 173.15 K and 473.15 K!"
    @assert 0 <= P < 5e6 "Out of range error: Pressure should be below 5×10⁶ Pa"
    molarenthalpyair(Tk, P)
end

"""
    entropy(DryAir, T, P[, out_unit=u"m^3/kg"])
    entropy(Vapor, T[, out_unit=u"m^3/kg"])
    entropy(MoistAir, T, HumidityType, hum, P[, out_unit="m^3/kg"])

Calculates the specific entropy of a gas. In the case of moist air, parameters specifying the humidity shoud be added and the the property is based on the mass of dry air not mass of the gas (as is the case for `DryAir` or `Vapor`).

If `Unitful` units are used, all parameters of the function (except dimensionless) should have units associated. When no units are provided, all parameters should use SI units (and so does the return type). For further information, checkout the doc for `Psychro` module.

Temperature should be in the range `173.15 K < T < 473.15` and pressure should be below 5 MPa.

# Examples
```julia-repl
julia> entropy(DryAir, 293.15, 101325.0)
71.09262577195514

julia> entropy(DryAir, 20.0u"°C", 1.0u"atm")
71.09262577195514 kg^-1 J K^-1

julia> entropy(Vapor, 293.15)
8665.873003613997

julia> entropy(Vapor, 20.0u"°C")
8665.873003613997 kg^-1 J K^-1

julia> entropy(MoistAir, 293.15, RelHum, 0.7, 101325.0)
166.33538729355692

julia> entropy(MoistAir, 20.0u"°C", WetBulb, 18.0u"°C", 1.0u"atm")
182.97140931650105 kg^-1 J K^-1

julia> entropy(MoistAir, 20.0u"°C", WetBulb, 18.0u"°C", 1.0u"atm", u"kJ/lb/°F")
0.04610801955228433 °F^-1 kJ lb^-1

```
"""
function entropy(::Type{DryAir}, Tk, P)
    @assert 173.1 < Tk < 473.2 "Out of range error: Temperature should be between 173.15 K and 473.15 K!"
    @assert 0 <= P < 5e6 "Out of range error: Pressure should be below 5×10⁶ Pa"
    entropyair(Tk, P)
end

"""
    entropym(DryAir, T, P[, out_unit=u"m^3/kg"])
    entropym(Vapor, T[, out_unit=u"m^3/kg"])
    entropym(MoistAir, T, HumidityType, hum, P[, out_unit="m^3/kg"])

Calculates the molar entropy of a gas. In the case of moist air, parameters specifying the humidity shoud be added.

If `Unitful` units are used, all parameters of the function (except dimensionless) should have units associated. When no units are provided, all parameters should use SI units (and so does the return type). For further information, checkout the doc for `Psychro` module.

Temperature should be in the range `173.15 K < T < 473.15` and pressure should be below 5 MPa.

# Examples
```julia-repl
julia> entropym(DryAir, 293.15, 101325.0)
2.0590912665460226

julia> entropym(DryAir, 20.0u"°C", 1.0u"atm")
2.0590912665460226 J K^-1 mol^-1

julia> entropy(Vapor, 293.15)
8665.873003613997

julia> entropym(Vapor, 20.0u"°C")
156.11812860454717 J K^-1 mol^-1

julia> entropym(MoistAir, 293.15, RelHum, 0.7, 101325.0)
4.739496639035868

julia> entropym(MoistAir, 20.0u"°C", WetBulb, 18.0u"°C", 1.0u"atm")
5.197949865380578 J K^-1 mol^-1

julia> entropym(MoistAir, 20.0u"°C", WetBulb, 18.0u"°C", 1.0u"atm", u"kJ/kmol/°F")
2.8877499252114327 °F^-1 kJ kmol^-1

```
"""
function entropym(::Type{DryAir}, Tk, P)
    @assert 173.1 < Tk < 473.2 "Out of range error: Temperature should be between 173.15 K and 473.15 K!"
    @assert 0 <= P < 5e6 "Out of range error: Pressure should be below 5×10⁶ Pa"
    molarentropyair(Tk, P)
end

"""
    compressfactor(DryAir, T, P[, out_unit=u"m^3/kg"])
    compressfactor(Vapor, T[, out_unit=u"m^3/kg"])
    compressfactor(MoistAir, T, HumidityType, hum, P[, out_unit="m^3/kg"])

Calculates the compressibility factor Z of a gas. In the case of moist air, parameters specifying the humidity shoud be added.

If `Unitful` units are used, all parameters of the function (except dimensionless) should have units associated. When no units are provided, all parameters should use SI units (and so does the return type). For further information, checkout the doc for `Psychro` module.

Temperature should be in the range `173.15 K < T < 473.15` and pressure should be below 5 MPa.

# Examples
```julia-repl
julia> compressfactor(DryAir, 293.15, 101325.0)
1.2044776705391136

julia> compressfactor(DryAir, 20.0u"°C", 1.0u"atm")
1.2044776705391136 kg m^-3

julia> compressfactor(Vapor, 293.15)
0.017308548277505224

julia> compressfactor(Vapor, 20.0u"°C")
0.017308548277505224 kg m^-3

julia> compressfactor(MoistAir, 293.15, RelHum, 0.7, 101325.0)
1.1971436091840653

julia> compressfactor(MoistAir, 20.0u"°C", WetBulb, 18.0u"°C", 1.0u"atm")
1.195819185774941 kg m^-3

```
"""
function compressfactor(::Type{DryAir}, Tk, P)
    @assert 173.1 < Tk < 473.2 "Out of range error: Temperature should be between 173.15 K and 473.15 K!"
    @assert 0 <= P < 5e6 "Out of range error: Pressure should be below 5×10⁶ Pa"
    P*molarvolumeair(Tk, P) / (R*Tk)
end

function volume(::Type{Vapor}, Tk)
    @assert 173.1 < Tk < 473.2 "Out of range error: Temperature should be between 173.15 K and 473.15 K!"
    volumevapor(Tk)
end

function volumem(::Type{Vapor}, Tk)
    @assert 173.1 < Tk < 473.2 "Out of range error: Temperature should be between 173.15 K and 473.15 K!"
    molarvolvapor(Tk)
end

function density(::Type{Vapor}, Tk)
    @assert 173.1 < Tk < 473.2 "Out of range error: Temperature should be between 173.15 K and 473.15 K!"
    1/volumevapor(Tk)
end

function enthalpy(::Type{Vapor}, Tk)
    @assert 173.1 < Tk < 473.2 "Out of range error: Temperature should be between 173.15 K and 473.15 K!"
    enthalpyvapor(Tk)
end

function enthalpym(::Type{Vapor}, Tk)
    @assert 173.1 < Tk < 473.2 "Out of range error: Temperature should be between 173.15 K and 473.15 K!"
    enthalpyvapor(Tk)*Mv
end

function entropym(::Type{Vapor}, Tk)
    @assert 173.1 < Tk < 473.2 "Out of range error: Temperature should be between 173.15 K and 473.15 K!"
    molarentropyvapor(Tk)
end

function entropy(::Type{Vapor}, Tk)
    @assert 173.1 < Tk < 473.2 "Out of range error: Temperature should be between 173.15 K and 473.15 K!"
    entropyvapor(Tk)
end

function compressfactor(::Type{Vapor}, Tk)
    @assert 173.1 < Tk < 473.2 "Out of range error: Temperature should be between 173.15 K and 473.15 K!"
    Zvapor(Tk)
end

"""
    molarfrac(Tk, HumidityType, x, P)

Calculates the molarfraction of water vapor given a humidity parameter.
The humidity parameter can be one of the following:

 * `MolarFrac` - Identity function basically...
 * `RelHum` - Relative humidity in decimal notation (not percentage!)
 * `DewPoint`- Dew point temperature in K
 * `HumRat` - Humidity ration, kg of vapor / kg of dry air
 * `SpecHum` - Specific Humidity of water vapor = mv / (mv + ma)
 * `WetBulb` - Wet bulb temperature in K- estimated by the adiabatic saturation temperature.

As usual all calculations are performed using plain SI units.
The parameters used by this function are:

 * `Tk` - Temperature in K
 * Humidity type - a type listed above that characterizes the humidity
 * The value of the humidity parameter.
 * P - Pressure in Pa
"""
function molarfrac(Tk, ::Type{MolarFrac}, xv, P)
    xv
end

function molarfrac(Tk, ::Type{HumRat}, w, P)
    w  / (Mv/Ma + w)
end

function molarfrac(Tk, ::Type{SpecHum}, q, P)
    q * Ma / (Mv + r*(Ma - Mv))
end

function molarfrac(Tk, ::Type{RelHum}, rel, P)
    rel * efactor(Tk, P) * Pws(Tk) / P
end

function molarfrac(Tk, ::Type{DewPoint}, D, P)
    efactor(D,P) * Pws(D) / P
end

function molarfrac(Tk, ::Type{WetBulb}, B, P)
    w = calc_W_from_B(Tk, B, P)
    w / (Mv/Ma + w)
end

function volume(::Type{MoistAir}, Tk, ::Type{T}, y, P) where {T<:PsychroProperty}
    @assert 173.1 < Tk < 473.2 "Out of range error: Temperature should be between 173.15 K and 473.15! K"
    @assert 0 <= P < 5e6 "Out of range error: Pressure should be below 5×10⁶ Pa"
    xv = molarfrac(Tk, T, y, P)
    volumemoist(Tk, P, xv)
end

function volumem(::Type{MoistAir}, Tk, ::Type{T}, y, P) where {T<:PsychroProperty}
    @assert 173.1 < Tk < 473.2 "Out of range error: Temperature should be between 173.15 K and 473.15! K"
    @assert 0 <= P < 5e6 "Out of range error: Pressure should be below 5×10⁶ Pa"

    xv = molarfrac(Tk, T, y, P)
    molarvolumemoist(Tk, P, xv)
end

function density(::Type{MoistAir}, Tk, ::Type{T}, y, P) where {T<:PsychroProperty}
    @assert 173.1 < Tk < 473.2 "Out of range error: Temperature should be between 173.15 K and 473.15 K!"
    @assert 0 <= P < 5e6 "Out of range error: Pressure should be below 5×10⁶ Pa"

    xv = molarfrac(Tk, T, y, P)
    M = xv*Mv + (1-xv)*Ma
    vm = molarvolumemoist(Tk, P, xv)
    return M/vm
end

function enthalpy(::Type{MoistAir}, Tk, ::Type{T}, y, P) where {T<:PsychroProperty}
    @assert 173.1 < Tk < 473.2 "Out of range error: Temperature should be between 173.15 K and 473.15 K!"
    @assert 0 <= P < 5e6 "Out of range error: Pressure should be below 5×10⁶ Pa"

    xv = molarfrac(Tk, T, y, P)
    enthalpymoist(Tk, P, xv)
end

function enthalpym(::Type{MoistAir}, Tk, ::Type{T}, y, P) where {T<:PsychroProperty}
    @assert 173.1 < Tk < 473.2 "Out of range error: Temperature should be between 173.15 K and 473.15 K!"
    @assert 0 <= P < 5e6 "Out of range error: Pressure should be below 5×10⁶ Pa"

    xv = molarfrac(Tk, T, y, P)
    molarenthalpymoist(Tk, P, xv)
end

function entropy(::Type{MoistAir}, Tk, ::Type{T}, y, P) where {T<:PsychroProperty}
    @assert 173.1 < Tk < 473.2 "Out of range error: Temperature should be between 173.15 K and 473.15 K!"
    @assert 0 <= P < 5e6 "Out of range error: Pressure should be below 5×10⁶ Pa"

    xv = molarfrac(Tk, T, y, P)
    entropymoist(Tk, P, xv)
end

function entropym(::Type{MoistAir}, Tk, ::Type{T}, y, P) where {T<:PsychroProperty}
    @assert 173.1 < Tk < 473.2 "Out of range error: Temperature should be between 173.15 K and 473.15 K!"
    @assert 0 <= P < 5e6 "Out of range error: Pressure should be below 5×10⁶ Pa"

    xv = molarfrac(Tk, T, y, P)
    molarentropymoist(Tk, P, xv)
end

function compressfactor(::Type{MoistAir}, Tk, ::Type{T}, y, P) where {T<:PsychroProperty}
    @assert 173.1 < Tk < 473.2 "Out of range error: Temperature should be between 173.15 K and 473.15 K!"
    @assert 0 <= P < 5e6 "Out of range error: Pressure should be below 5×10⁶ Pa"

    xv = molarfrac(Tk, T, y, P)
    Zmoist(Tk, P, xv)
end

function calcdewpoint(Tk, P, xv, EPS=1e-9, MAXITER=100)
    # Use Ideal Gas to
    D = Tws(xv*P)
    Dnew = D
    err = 0.0
    niter = 1
    for i = 1:MAXITER
        niter = i
        f = efactor(D, P)
        Dnew = Tws(xv*P/f)
        err = abs(D-Dnew)
        if err < EPS
            return Dnew
        end
        D = Dnew
    end
    throw(ConvergenceError("Dew point calculation failed to converge!", D, niter, err))
    return Dnew
end

"""
# Thermodynamic properties of moist air, dry air and saturated water vapor.

The methods listed below calculate thermodynamic properties of moist air:

    volume(MoistAir, T, HumidityType, humidity, P[, outunit])
    volume(MoistAir, T, HumidityType, humidity, P[, outunit])
    density(MoistAir, T, HumidityType, humidity, P[, outunit])
    enthalpy(MoistAir, T, HumidityType, humidity, P[, outunit])
    enthalpym(MoistAir, T, HumidityType, humidity, P[, outunit])
    entropy(MoistAir, T, HumidityType, humidity, P[, outunit])
    entropym(MoistAir, T, HumidityType, humidity, P[, outunit])
    compressfactor(MoistAir, T, HumidityType, humidity, P[, outunit])
    dewpoint(MoistAir, T, HumidityType, humidity, P[, outunit])
    wetbulb(MoistAir, T, HumidityType, humidity, P[, outunit])
    humrat(MoistAir, T, HumidityType, humidity, P)
    relhum(MoistAir, T, HumidityType, humidity, P)
    humrat(MoistAir, T, HumidityType, humidity, P)
    spechum(MoistAir, T, HumidityType, humidity, P)
    molarfrac(MoistAir, T, HumidityType, humidity, P)

The methods listed above calculate the following thermodynamic properties of moist air:

 * `volume` Specific volume
 * `volumem` Molar volume
 * `density` Density
 * `enthalpy` Specific enthalpy
 * `enthalpym` Molar enthalpy
 * `entropy` Specific entropy
 * `entropym` Molar entropy
 * `compressfactor` Compressibility factor Z
 * `dewpoint` Dew point temperature
 * `wetbulb` Adiabatic saturation temperature
 * `humrat` Humidity ratio
 * `relhum` Relative humidity
 * `spechum` Specific humidity
 * `molarfrac` Molar fraction of water vapor

The humidity is specified using two parameters:

 * How the humidity is specified
 * The actual value of humidity

The following types are used to characterize the humidity.

 * `WetBulb` for wet bulb temperature, actually adiabatic saturation temperature
 * `DewPoint` Dew point temperature
 * `RelHum` Relative humidity
 * `HumRat` Humidity ratio (kg of vapor / kg of dry air)
 * `SpecHum` Specific humidity (kg of vapor / kg of moist air)
 * `MolarFrac` Molar fraction of water vapor.

# Examples
```julia-repl
julia> volume(MoistAir, 293.15, WetBulb, 291.15, 101325.0)
0.8464079202783964

julia> volume(MoistAir, 293.15, DewPoint, 291.15, 101325.0)
0.8475219875187474

julia> volume(MoistAir, 293.15, RelHum, 0.7, 101325.0)
0.843889817602806

julia> volume(MoistAir, 20.0u"°C", DewPoint, 60.0u"°F", 1.0u"atm")
0.8449934929585231 kg^-1 m^3

julia> volumem(MoistAir, 293.15, RelHum, 0.5, 93000.0)
0.026199080086890276

julia> volumem(MoistAir, 20.0u"°C", WetBulb, 17.0u"°C", 93u"kPa", u"inch^3/kmol")
1.598733210336603e6 in^3 kmol^-1

julia> density(MoistAir, 20.0u"°C", WetBulb, 17.0u"°C", 93u"kPa")
1.0976075893895811 kg m^-3

julia> density(MoistAir, 20.0u"°C", WetBulb, 17.0u"°C", 93u"kPa", u"lb/inch^3")
3.965358988338535e-5 in^-3 lb

julia> volumem(MoistAir, 20.0u"°C", WetBulb, 17.0u"°C", 93u"kPa", u"inch^3/kmol")
1.598733210336603e6 in^3 kmol^-1

julia> enthalpy(MoistAir, 20.0u"°C", WetBulb, 17.0u"°C", 93u"kPa")
50667.43014746832 kg^-1 J

julia> enthalpym(MoistAir, 20.0u"°C", WetBulb, 17.0u"°C", 93u"kPa")
1439.6551689935861 J mol^-1

julia> compressfactor(MoistAir, -90.0u"°C", RelHum, 0.01, 4.5u"MPa")
0.8552758629097985

julia> wetbulb(MoistAir, 20.0u"°C", WetBulb, 17.0u"°C", 93u"kPa", u"°C")
17.0 °C

julia> dewpoint(MoistAir, 20.0u"°C", WetBulb, 17.0u"°C", 93u"kPa", u"°C")
15.475836053510477 °C

julia> humrat(MoistAir, 20.0u"°C", WetBulb, 17.0u"°C", 93u"kPa")
0.012032930694441925

julia> relhum(MoistAir, 20.0u"°C", WetBulb, 17.0u"°C", 93u"kPa")
0.7517801524436909

julia> spechum(MoistAir, 20.0u"°C", WetBulb, 17.0u"°C", 93u"kPa")
0.011889860823189923

```

"""
function dewpoint(::Type{MoistAir}, Tk, ::Type{T}, y, P) where {T<:PsychroProperty}
    @assert 173.1 < Tk < 473.2 "Out of range error: Temperature should be between 173.15 K and 473.15 K!"
    @assert 0 <= P < 5e6 "Out of range error: Pressure should be below 5×10⁶ Pa"

    if T==DewPoint
        return y
    end

    xv = molarfrac(Tk, T, y, P)
    D = calcdewpoint(Tk, P, xv)
    return D
end

function relhum(::Type{MoistAir}, Tk, ::Type{T}, y, P) where {T<:PsychroProperty}
    @assert 173.1 < Tk < 473.2 "Out of range error: Temperature should be between 173.15 K and 473.15 K!"
    @assert 0 <= P < 5e6 "Out of range error: Pressure should be below 5×10⁶ Pa"

    if T==RelHum
        return y
    end

    xv = molarfrac(Tk, T, y, P)
    xv * P / (efactor(Tk, P) * Pws(Tk))
end
@doc (@doc dewpoint) relhum

function wetbulb(::Type{MoistAir}, Tk, ::Type{T}, y, P) where {T<:PsychroProperty}
    @assert 173.1 < Tk < 473.2 "Out of range error: Temperature should be between 173.15 K and 473.15 K!"
    @assert 0 <= P < 5e6 "Out of range error: Pressure should be below 5×10⁶ Pa"

    if T==WetBulb
        return y
    end
    xv = molarfrac(Tk, T, y, P)

    B = calcwetbulb(Tk, P, xv)
    return B

end
@doc (@doc dewpoint) wetbulb

humrat(xv) = xv / (1-xv) * (Mv/Ma)
function humrat(::Type{MoistAir}, Tk, ::Type{T}, y, P) where {T<:PsychroProperty}
    @assert 173.1 < Tk < 473.2 "Out of range error: Temperature should be between 173.15 K and 473.15 K!"
    @assert 0 <= P < 5e6 "Out of range error: Pressure should be below 5×10⁶ Pa"

    if T==HumRat
        return y
    end

    xv = molarfrac(Tk, T, y, P)
    return humrat(xv)
end
@doc (@doc dewpoint) humrat

function molarfrac(::Type{MoistAir}, Tk, ::Type{T}, y, P) where {T<:PsychroProperty}
    @assert 173.1 < Tk < 473.2 "Out of range error: Temperature should be between 173.15 K and 473.15 K!"
    @assert 0 <= P < 5e6 "Out of range error: Pressure should be below 5×10⁶ Pa"

    if T==MolarFrac
        return y
    end

    xv = molarfrac(Tk, T, y, P)
    return xv
end
@doc (@doc dewpoint) molarfrac

function spechum(::Type{MoistAir}, Tk, ::Type{T}, y, P) where {T<:PsychroProperty}
    @assert 173.1 < Tk < 473.2 "Out of range error: Temperature should be between 173.15 K and 473.15 K!"
    @assert 0 <= P < 5e6 "Out of range error: Pressure should be below 5×10⁶ Pa"

    if T==SpecHum
        return y
    end
    xv = molarfrac(Tk, T, y, P)
    return xv*Mv / ( (1-xv)*Ma + xv*Mv )

end
@doc (@doc dewpoint) spechum


@testset "base.jl" begin
    @testset "PointSpace" begin
        @test @capture_out(print(PointSpace())) == @capture_err(print(stderr, PointSpace()))
        @test iszero(PointSpace()())
        @test iszero(rand(PointSpace()))
        @test all(iszero, rand(PointSpace(), 100))
        @test dim(PointSpace()) == 0
        @test codim(PointSpace()) == 1

        @test offsetframe(PointSpace())[1] == PointSpace()()
        @test isempty(offsetframe(PointSpace())[2])
        r = rand(5)
        @test offsetframe(PointSpace())[2] *ₚ r == r
        @test dimincrease(PointSpace()) == 0
    end

    @testset "Sphere" begin
        for n in 1:5
            sphere = Sphere(n)
            @test @capture_out(print(sphere)) == @capture_err(print(stderr, sphere))
            @test norm(sphere(rand(n)...)) ≈ 1
            @test norm(rand(sphere)) ≈ 1
            @test all(norm.(rand(sphere, 100)) .≈ 1)
            @test dim(sphere) == n
            @test codim(sphere) == 1
            if n == 1
                @test dimincrease(sphere) == 1
                of = offsetframe(sphere, 0.00)
                @test of[1] ≈ [1, 0]
                @test of[2] ≈ [-1 0; 0 0; 0 1]
                of = offsetframe(sphere, 0.25)
                @test of[1] ≈ [0, 1]
                @test of[2] ≈ [0 0; -1 0; 0 1]
            else
                @test dimincrease(sphere) == n+1
                _, f = offsetframe(sphere, rand(n)...)
                r = rand(5)
                @test f *ₚ r == vcat(zeros(n+1), r)
            end
        end
    end

    @testset "Ball" begin
        for n in 1:5
            ball = Ball(n)
            @test @capture_out(print(ball)) == @capture_err(print(stderr, ball))
            @test norm(ball(rand(n)...)) ≤ 1
            @test norm(rand(ball)) ≤ 1
            @test all(norm.(rand(ball, 100)) .≤ 1)
            @test dim(ball) == n
            @test codim(ball) == 0
            @test dimincrease(ball) == n
            r = rand(5)
            @test offsetframe(ball, rand(n)...)[2] *ₚ r == vcat(zeros(n), r)
        end
    end

    @testset "Cube" begin
        for n in 1:5
            cube = Cube(n)
            @test @capture_out(print(cube)) == @capture_err(print(stderr, cube))
            @test sum(cube(rand(n)...)) .≤ n
            @test sum(rand(cube)) ≤ n
            @test all(sum.(rand(cube, 100)) .≤ n)
            @test dim(cube) == n
            @test codim(cube) == 0
            @test dimincrease(cube) == n
            r = rand(5)
            @test offsetframe(cube, rand(n)...)[2] *ₚ r == vcat(zeros(n), r)
        end
    end

    @testset "ParametricManifold" begin
        circfun(x) = (cospi(2x), sinpi(2x))
        circ = ParametricManifold{1, 1}(circfun)
        @test @capture_out(print(circ)) == @capture_err(print(stderr, circ))
        @test norm(circ(rand())) ≈ 1
        @test norm(rand(circ)) ≈ 1
        @test all(norm.(rand(circ, 100)) .≈ 1)
        @test dim(circ) == 1
        @test codim(circ) == 1
        of = offsetframe(circ, 0.00)
        @test of[1] ≈ [1, 0]
        @test of[2] ≈ [-1 0; 0 0; 0 1]
        of = offsetframe(circ, 0.25)
        @test of[1] ≈ [0, 1]
        @test of[2] ≈ [0 0; -1 0; 0 1]
        @test dimincrease(circ) == 1
        r = rand(5)
        @test length(offsetframe(circ, rand())[2] *ₚ r) == 6
        @test circ ≅ Sphere(1)

        # TODO: others
    end
end


## Physics:
##
## We simulate 2d cavity flow for compressible fluid,
## more precisely, for an ideal gas at constant temperature.
## Therefore, from the equation of state, we know that
## P / ρ = 1 (if we choose the units such that the constants
## terms are reduced to 1).
##
## The equations used are the continuity and the momentum equations
##
## The velocity in the x-direction on the top is constant,
## there are walls on the bottom, left and right edges.
##
## →→→→→→→→
## |      |
## |      |
## --------
##
## Initial conditions:
## u = u0 = 0.05 at y = 2
## v⃗ = (u, v) = 0⃗ everywhere else
## ρ = p = p0 everywhere (Atmospheric pressure)
##
## Boundary conditions:
## u = 1 at y = 2
## ∂p / ∂y = 0 at y = 0
## ρ = p = p0 at y = 2
## ∂p / ∂x = 0 at x = 0, 2

using PyPlot

## Time steps
duration = 10.0
Δt = 0.001
nsteps = convert(Int64, duration / Δt)

## Space grid [0, 2] × [0, 2]
Δx = 0.05
Δy = Δx
nx = convert(Int64, 2.0 / Δx)
ny = convert(Int64, 2.0 / Δy)

## Viscocity
ν = 0.1

## Atmospheric pressure
p0 = 1.0

## Velocity at the top
u0 = 0.05

u = zeros(nx, ny)
v = zeros(nx, ny)
ρ = zeros(nx, ny)

ρ[:, :] .= p0
u[:, ny] .= u0

function update!(ρnext, unext, vnext, ρwork, uwork, vwork,
        Δt, Δx, Δy, u0, ν, nx, ny)
    for j = 2 : ny - 1
        for i = 2 : nx - 1
            ## Auxiliary variables to avoid typos when
            ## working with partial derivatives
            ##
            ## NW--N--NE
            ##     |
            ## W---C---E
            ##     |
            ## SW--S--SE

            ρc = ρwork[i, j]
            ρe = ρwork[i + 1, j]
            ρw = ρwork[i - 1, j]
            ρn = ρwork[i, j + 1]
            ρs = ρwork[i, j - 1]

            uc = uwork[i, j]
            ue = uwork[i + 1, j]
            uw = uwork[i - 1, j]

            vc = vwork[i, j]
            vn = vwork[i, j + 1]
            vs = vwork[i, j - 1]

            ## Continuity equation to calculate ρ
            ## ∂ρ / ∂t + div(ρv⃗) = 0
            ρnext[i, j] = ρc - Δt * (uc * (ρe - ρw) / (2.0 * Δx) +
                                     vc * (ρn - ρs) / (2.0 * Δy) +
                                     ρc * (ue - uw) / (2.0 * Δx) +
                                     ρc * (vn - vs) / (2.0 * Δy))

        end
    end

    ## Question about view: https://stackoverflow.com/questions/65979923/what-is-happening-behind-the-scenes-in-julias-view-function-a3-viewa

    ## Enforcing boundary conditions for ρ = P
    ρnext[1, :] = @view ρnext[2, :]
    ρnext[nx, :] = @view ρnext[nx - 1, :]

    ρnext[:,  1] = @view ρnext[:, 2]
    ρnext[:, ny] .= p0

    ## Here we use a central difference scheme to discretize the second
    ## order derivatives, so we iterate over the 'inner' grid
    for j = 2 : ny - 1
        for i = 2 : nx - 1
            ## Auxiliar variables
            ρc = ρwork[i, j]
            ρe = ρwork[i + 1, j]
            ρw = ρwork[i - 1, j]
            ρn = ρwork[i, j + 1]
            ρs = ρwork[i, j - 1]

            uc = uwork[i, j]
            ue = uwork[i + 1, j]
            uw = uwork[i - 1, j]
            un = uwork[i, j + 1]
            us = uwork[i, j - 1]

            vc = vwork[i, j]
            ve = vwork[i + 1, j]
            vw = vwork[i - 1, j]
            vn = vwork[i, j + 1]
            vs = vwork[i, j - 1]

            ## Momentum equation
            ## ∂v⃗ / ∂t + (v⃗ ⋅∇)v⃗ = - (1 / ρ) ∇P + ν∇²v⃗
            ## -> we later replace this by Darcy's law
            viscous_term_u = ν * ((uw - 2.0 * uc + ue) / Δx^2 +
                                  (un - 2.0 * uc + us) / Δy^2)

            viscous_term_v = ν * ((vw - 2.0 * vc + ve) / Δx^2 +
                                  (vn - 2.0 * vc + vs) / Δy^2)

            pressure_term_u = -(1 / ρc) * (ρe - ρw) / (2.0 * Δx)

            pressure_term_v = -(1 / ρc) * (ρn - ρs) / (2.0 * Δy)

            inertia_term_u = uc * (ue - uw) / (2.0 * Δx) +
                             vc * (un - us) / (2.0 * Δy)

                             inertia_term_v = uc * (ve - vw) / (2.0 * Δx) +
                             vc * (vn - vs) / (2.0 * Δy- vw) / (2.0 * Δx)

            unext[i, j] = uc + Δt * (-inertia_term_u + pressure_term_u +
                                     viscous_term_u)

            vnext[i, j] = vc + Δt * (-inertia_term_v + pressure_term_v +
                                     viscous_term_v)
        end
    end
end

function cavity_flow!(ρ, u, v, Δx, Δy, Δt, nsteps, nx, ny, u0, ν)
    ## Helper arrays to store values from previous iteration
    ρwork, uwork, vwork = ρ, u, v
    unext = copy(u)
    vnext = copy(v)
    ρnext = copy(ρ)

    for t = 1 : nsteps
        update!(ρnext, unext, vnext, ρwork, uwork, vwork,
                Δt, Δx, Δy, u0, ν, nx, ny)

        ## Swap next and current and continue iteration
        ρnext, ρwork = ρwork, ρnext
        unext, uwork = uwork, unext
        vnext, vwork = vwork, vnext
    end

    ## If in the end ρwork is not the original ρ,
    ## copy the most recent data into ρ, u, v
    if ρwork !== ρ
        ρ .= ρwork
        u .= uwork
        v .= vwork
    end

    return ρ, u, v
end

function plot(u, v, ρ)
    fig = PyPlot.figure(figsize=(10, 10))
    ax = PyPlot.axes()
    ax.set_xlabel("x")
    ax.set_ylabel("y")
    #legend(loc="upper right",fancybox="true")

    rgx = range(0, 2.0, length=nx)
    rgy = range(0, 2.0, length=ny)
    x = [rgx;]'
    y = [rgy;]'
    X = repeat([rgx;]', length(x))
    Y = repeat([rgy;],1, length(y))

    ## Velocity vector field
    ## scale = 100.0
    quiver(x, y, u', v')

    ## Streamplot of velocity vector field
    #streamplot(x, y, u', v')

    ## Density contour map
    pos = ax.contourf(X, Y, ρ', alpha=0.5, cmap=matplotlib.cm.viridis)
    fig.colorbar(pos, ax=ax)
    cp = contour(X, Y, ρ', cmap=matplotlib.cm.viridis)

    PyPlot.title("Cavity Flow for Ideal Gas")

    # PyPlot.svg(true)
    # savefig("plot.svg")

    savefig("img/ideal-gas.png")
    savefig("img/ideal-gas.svg")
end

## Run our solver
ρ, u, v = cavity_flow!(ρ, u, v, Δx, Δy, Δt, nsteps, nx, ny, u0, ν)

## Plotting
plot(u, v, ρ)


using GMHPhotoReceptorAnalysis

###Load and display the photon sequence
photonfilename = joinpath(splitdir(@__FILE__())[1],"../data/naturallight.jld")

photons1 = GMHPhotoReceptor.photonsequence(photonfilename)
current1 = GMHPhotoReceptor.lightinducedcurrent(photonfilename)

println("====================================")
println("Measurement data loaded successfully")
println("====================================")

###Settings for the model
numvilli1 = 30000

#specify the values that determine the priors on the parameters
latencylocation = (2.0,3.5) #uniform distribution with (low,high) values
latencyscale = (0.2,0.7) #uniform distribution with (low,high) values
refractorylocation = (4.0,6.0) #uniform distribution with (low,high) values
refractoryscale = (1.5,2.5) #uniform distribution with (low,high) values
bumpamplitude = (3.0,5.0) #uniform distribution with (low,high) values
bumpshape = (log(3.0),0.3) #lognormal distribution with (location,scale) values
bumpscale = (log(2.5),0.3) #lognormal distribution with (location,scale) values

#4-parameter model with stochastic latency and refractory parameters and fixed bump parameters
mpolicy4 = GeneralizedMetropolisHastings.policy(:photoreceptor;bump=:fixed)
params4 = GeneralizedMetropolisHastings.parameters(:photoreceptor,mpolicy4,
                     latencylocation,latencyscale,refractorylocation,refractoryscale)

#7-parameter model with latency, refractory and bump parameters
mpolicy7 = GeneralizedMetropolisHastings.policy(:photoreceptor;bump=:sample)
params7 = GeneralizedMetropolisHastings.parameters(:photoreceptor,mpolicy7,
                     latencylocation,latencyscale,refractorylocation,refractoryscale,
                     bumpamplitude,bumpshape,bumpscale)

variance1 = [3600.0]

println("=====================================")
println("Model parameters defined successfully")
println("=====================================")

###Create PhotoReceptor models
model4 = GeneralizedMetropolisHastings.model(:photoreceptor,params4,photons1,current1,variance1,numvilli1,mpolicy4)
model7 = GeneralizedMetropolisHastings.model(:photoreceptor,params7,photons1,current1,variance1,numvilli1,mpolicy7)

###Show the model
println("===========================")
println("Models defined successfully")
println("===========================")
show(model4)
show(model7)

###Plot evaluation data
#GMHPhotoReceptorAnalysis.analysis1(model4,100)
#GMHPhotoReceptorAnalysis.analysis2(model4,500)
#GMHPhotoReceptorAnalysis.analysis3(model4,200)
#GMHPhotoReceptorAnalysis.analysis4(model4,50,200)
GMHPhotoReceptorAnalysis.analysis5(model4,100,1e-3)

nothing


tobinary = Dict("0" => "0000", "1" => "0001", "2" => "0010", "3" => "0011", "4" => "0100", "5" => "0101", "6" => "0110", "7" => "0111", "8" => "1000", "9" => "1001", "A" => "1010", "B" => "1011", "C" => "1100", "D" => "1101", "E" => "1110", "F" => "1111")
version_sum = 0

function parseInput(filename)
    line = readlines(filename*".txt")
    binline = ""
    for c in line[1]
        b = tobinary[string(c)]
        binline = binline * b
    end
    return binline
end

function interpretHeader(binline, startidx)
    version = parse(Int, binline[startidx:startidx+2], base = 2)
    type = parse(Int, binline[startidx+3:startidx+5], base = 2)
    return version, type
end

function interpretLiteral(binline, startidx)
    #pkglength = 6
    fivebits = binline[startidx:startidx+4]
    number = ""
    stopdesignator = false
    while stopdesignator != true
        number = number * fivebits[2:5]
        #pkglength += 5
        if fivebits[1] == '0'
            stopdesignator = true
        end
        startidx += 5
        if startidx+4 < length(binline)
            fivebits = binline[startidx:startidx+4]
        end
    end
    number = parse(Int, number, base = 2)

    return number, startidx
end

function readInstructions(binline, idx)
    global version_sum
    packet = Dict()
    v, t = interpretHeader(binline, idx)
    version_sum += v
    packet["version"] = v
    packet["type"] = t
    idx = idx + 6
    if packet["type"] == 4
        packet["val"], idx = interpretLiteral(binline, idx)
    else
        packet["i"] = parse(Int, binline[idx], base = 2)
        idx += 1
        if packet["i"] == 0
            packet["length"] = parse(Int, binline[idx:idx+14], base = 2)
            idx = idx + 15
            p_end = idx + packet["length"]
            if haskey(packet, "subpackages")
                sp = sp = [packet["subpackages"]]
            else
                sp = []
            end
            while idx < p_end
                subpack, idx = readInstructions(binline, idx)
                sp = push!(sp, subpack)
                packet["subpackages"] = sp
            end
        else
            packet["sub_count"] = parse(Int, binline[idx:idx+10], base = 2)
            idx = idx + 11
            if haskey(packet, "subpackages")
                sp = sp = [packet["subpackages"]]
            else
                sp = []
            end
            for _ in 1:packet["sub_count"]
                subpack, idx = readInstructions(binline, idx)
                sp = push!(sp, subpack)
                packet["subpackages"] = sp
            end
        end
        subp_vals = [p["val"] for p in packet["subpackages"]]

        if packet["type"] == 0
            #sum
            packet["val"] = sum(subp_vals)
        elseif packet["type"] == 1
            #product
            packet["val"] = prod(subp_vals)
        elseif packet["type"] == 2
            #minimum
            packet["val"] = minimum(subp_vals)
        elseif packet["type"] == 3
            #maximum
            packet["val"] = maximum(subp_vals)
        elseif packet["type"] == 5
            #greater than
            if subp_vals[1] > subp_vals[2]
                packet["val"] = 1
            else
                packet["val"] = 0
            end
        elseif packet["type"] == 6
            #less than
            if subp_vals[1] < subp_vals[2]
                packet["val"] = 1
            else
                packet["val"] = 0
            end
        elseif packet["type"] == 7
            #equal
            if subp_vals[1] == subp_vals[2]
                packet["val"] = 1
            else
                packet["val"] = 0
            end
        end
    end

    return packet, idx
end

function solve(filename)
    bin = parseInput(filename)
    packets, _ = readInstructions(bin, 1)
    println(version_sum)
    #println(packets)
    println(packets["val"])
end

solve("literal")
version_sum = 0
 solve("op1")
version_sum = 0
solve("op2")
version_sum = 0
solve("input")