using StableRNGs
using Test
using Statistics
using BayesianLinearRegression

m = @model X begin
    β ~ Normal() |> iid(size(X,2))
    y ~ For(eachrow(X)) do x
        Normal(x' * β, 1)
    end
    return collect(y)
end

rng = StableRNG(42)
X = randn(rng, 20, 2)
truth = simulate(rng, m(X=X))
y = value(truth)
post = dynamicHMC(rng, m(X=X) |  (y=y,))
pred = predictive(m,:β)

lr = BayesianLinReg(X,y)
lr.updateNoise = false
lr.updatePrior = false
fit!(lr)

@testset "dynamicHMC" begin
    @test mean(v.β for v in post) ≈ lr.weights  rtol = 0.05
    @test_nowarn [rand(rng, pred(;X=X, p...)) for p in post];
    #particles(post)
end

m2 = @model X begin
    N = size(X,1)
    k = size(X,2)
    β ~ Normal() |> iid(k)
    yhat = X * β
    y ~ For(N) do j
            Normal(yhat[j], 1)
        end
end;

jointdist = m2(X=X)

@testset "logdensity, symlogdensity" begin
    # @test_nowarn symlogdensity(m2).evalf(3)
    @test logdensity_def(jointdist, (β = truth.trace.β, y=y)) ≈ -8.015157812948065
    # @test logdensity_def(jointdist, truth, codegen) ≈ -28.551921801470904
end


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

"""
    Vec{Dim,T}

A vector in `Dim`-dimensional space with coordinates of type `T`.

A vector can be obtained by subtracting two [`Point`](@ref) objects.

## Examples

```julia
A = Point(0.0, 0.0)
B = Point(1.0, 0.0)
v = B - A
```

### Notes

- A `Vec` is a `SVector` from StaticArrays.jl
- Type aliases are `Vec1`, `Vec2`, `Vec3`, `Vec1f`, `Vec2f`, `Vec3f`
"""
const Vec = SVector

# type aliases for convenience
const Vec1  = Vec{1,Float64}
const Vec2  = Vec{2,Float64}
const Vec3  = Vec{3,Float64}
const Vec1f = Vec{1,Float32}
const Vec2f = Vec{2,Float32}
const Vec3f = Vec{3,Float32}


# An implementation of CRAIG-MR for the solution of the
# (under/over-determined or square) linear system
#
#  Ax = b.
#
# The method seeks to solve the minimum-norm problem
#
#  min ‖x‖²  s.t. Ax = b,
#
# and is equivalent to applying the conjugate residual method
# to the linear system
#
#  AA'y = b.
#
# This method is equivalent to CRMR, and is described in
#
# M. Arioli and D. Orban, Iterative Methods for Symmetric
# Quasi-Definite Linear Systems, Part I: Theory.
# Cahier du GERAD G-2013-32, GERAD, Montreal QC, Canada, 2013.
#
# D. Orban, The Projected Golub-Kahan Process for Constrained
# Linear Least-Squares Problems. Cahier du GERAD G-2014-15,
# GERAD, Montreal QC, Canada, 2014.
#
# Dominique Orban, <dominique.orban@gerad.ca>
# Montreal, QC, May 2015.

export craigmr

"""Solve the consistent linear system

  Ax + √λs = b

using the CRAIG-MR method, where λ ≥ 0 is a regularization parameter.
This method is equivalent to applying the Conjugate Residuals method
to the normal equations of the second kind

  (AA' + λI) y = b

but is more stable. When λ = 0, this method solves the minimum-norm problem

  min ‖x‖₂  s.t.  x ∈ argmin ‖Ax - b‖₂.

When λ > 0, this method solves the problem

  min ‖(x,s)‖₂  s.t. Ax + √λs = b.

CRAIGMR produces monotonic residuals ‖r‖₂.
It is formally equivalent to CRMR, though can be slightly more accurate,
and intricate to implement. Both the x- and y-parts of the solution are
returned.
"""
function craigmr(A :: AbstractLinearOperator, b :: AbstractVector{T};
                 λ :: Float64=0.0, atol :: Float64=1.0e-8, rtol :: Float64=1.0e-6,
                 itmax :: Int=0, verbose :: Bool=false) where T <: Number

  m, n = size(A);
  size(b, 1) == m || error("Inconsistent problem size");
  verbose && @printf("CRAIG-MR: system of %d equations in %d variables\n", m, n);

  # Compute y such that AA'y = b. Then recover x = A'y.
  y = zeros(T, m);
  u = copy(b)
  β₁ = @knrm2(m, u)   # Marginally faster than norm(b);
  β₁ == 0.0 && return (zeros(T, n), y, SimpleStats(true, false, [0.0], [], "x = 0 is a zero-residual solution"));
  β = β₁;

  # Initialize Golub-Kahan process.
  # β₁ u₁ = b.
  @kscal!(m, 1.0/β₁, u)
  v = copy(A' * u);
  α = @knrm2(n, v)
  Anorm² = α * α;

  verbose && @printf("%5s  %7s  %7s  %7s  %7s  %8s  %8s  %7s\n",
                     "Aprod", "‖r‖", "‖A'r‖", "β", "α", "cos", "sin", "‖A‖²");
  verbose && @printf("%5d  %7.1e  %7.1e  %7.1e  %7.1e  %8.1e  %8.1e  %7.1e\n",
                     1, β₁, α, β₁, α, 0, 1, Anorm²);

  # A'b = 0 so x = 0 is a minimum least-squares solution
  α == 0.0 && return (zeros(T, n), y, SimpleStats(true, false, [β₁], [0.0], "x = 0 is a minimum least-squares solution"));
  @kscal!(n, 1.0/α, v)

  # Initialize other constants.
  ζbar = β₁;
  ρbar = α;
  θ = 0.0;
  rNorm = ζbar;
  rNorms = [rNorm];
  ArNorm = α;
  ArNorms = [ArNorm];

  ɛ_c = atol + rtol * rNorm;   # Stopping tolerance for consistent systems.
  ɛ_i = atol + rtol * ArNorm;  # Stopping tolerance for inconsistent systems.

  iter = 0;
  itmax == 0 && (itmax = m + n);

  wbar = copy(u);
  @kscal!(m, 1.0/α, wbar)
  w = zeros(T, m);

  status = "unknown";
  solved = rNorm <= ɛ_c
  inconsistent = (rNorm > 1.0e+2 * ɛ_c) & (ArNorm <= ɛ_i)
  tired  = iter >= itmax

  while ! (solved || inconsistent || tired)
    iter = iter + 1;

    # Generate next Golub-Kahan vectors.
    # 1. βu = Av - αu
    @kscal!(m, -α, u)
    @kaxpy!(m, 1.0, A * v, u)
    β = @knrm2(m, u)
    β != 0.0 && @kscal!(m, 1.0/β, u)
    Anorm² = Anorm² + β * β;  # = ‖B_{k-1}‖²

    # Continue QR factorization
    #
    # Q [ Lₖ  β₁ e₁ ] = [ Rₖ   zₖ  ] :
    #   [ β    0    ]   [ 0   ζbar ]
    #
    #       k  k+1    k    k+1      k  k+1
    # k   [ c   s ] [ ρbar    ] = [ ρ  θ⁺    ]
    # k+1 [ s  -c ] [ β    α⁺ ]   [    ρbar⁺ ]
    #
    # so that we obtain
    #
    # [ c  s ] [ ζbar ] = [ ζ     ]
    # [ s -c ] [  0   ]   [ ζbar⁺ ]
    (c, s, ρ) = sym_givens(ρbar, β);
    ζ = c * ζbar;
    ζbar = s * ζbar;
    rNorm = abs(ζbar);
    push!(rNorms, rNorm);

    @kscal!(m, -θ/ρ, w)
    @kaxpy!(m, 1.0/ρ, wbar, w)  # w = (wbar - θ * w) / ρ;
    @kaxpy!(m, ζ, w, y)         # y = y + ζ * w;

    # 2. αv = A'u - βv
    @kscal!(n, -β, v)
    @kaxpy!(n, 1.0, A' * u, v)
    α = @knrm2(n, v)
    Anorm² = Anorm² + α * α;  # = ‖Lₖ‖
    ArNorm = α * β * abs(ζ/ρ);
    push!(ArNorms, ArNorm);

    verbose && @printf("%5d  %7.1e  %7.1e  %7.1e  %7.1e  %8.1e  %8.1e  %7.1e\n",
                       1 + 2 * iter, rNorm, ArNorm, β, α, c, s, Anorm²);

    if α != 0.0
      @kscal!(n, 1.0/α, v)
      @kscal!(m, -β/α, wbar)
      @kaxpy!(m, 1.0/α, u, wbar)  # wbar = (u - beta * wbar) / alpha;
    end
    θ = s * α;
    ρbar = -c * α;

    solved = rNorm <= ɛ_c
    inconsistent = (rNorm > 1.0e+2 * ɛ_c) & (ArNorm <= ɛ_i)
    tired  = iter >= itmax
  end

  x = A' * y;

  status = tired ? "maximum number of iterations exceeded" : (solved ? "found approximate minimum-norm solution" : "found approximate minimum least-squares solution")
  stats = SimpleStats(solved, inconsistent, rNorms, ArNorms, status);
  return (x, y, stats)
end


d = 3    # number of dimensions
K = 50   # number of points along each dimension
N = 100000  # number of points at which to interpolate

A = rand([K for i = 1:d]...)    # filtered coefficients
B = rand(N,d)                    # points at which to evaluate the splines
max(B, minimum(A)+0.01)
min(B, maximum(A)-0.01)

println("Comparison (d=",d,", K=", K, ", N=", N,")")

# interpolations.jl
using Interpolations
itp = interpolate(A, BSpline(Quadratic(Reflect())), OnGrid())

# what is the fastest way to operate on a list of points ?
LB = Vector{Float64}[copy(slice(B,i,:)) for i=1:size(B,1)]

function evaluate(itp, LB)
    s = 0.0
    for i=1:size(LB,1)
        I = LB[i]
        s += itp[I[1],I[2],I[3]]
    end
    return s
end

total_2 = evaluate(itp, LB)

println("interpolations.jl (quadratic)")
@time total_2 = evaluate(itp, LB)
exit()

# splines.jl
using splines

# dummy boundaries
a = [0.0 for i=1:d]
b = [1.0 for i=1:d]
orders = Int64[K for i=1:d]
coeffs = filter_coeffs(a,b,orders,A)

# jit
println("splines.jl: ")
values_1 = eval_UC_spline(a,b,orders,coeffs,B);
tot = sum(values_1)
@time  values_1 = eval_UC_spline(a,b,orders,coeffs,B); tot = sum(values_1)
# not sure whether the summation is taken into accoun
println("splines.jl (with derivatives): ")
values_1, dvalues_1 = eval_UC_spline_G(a,b,orders,coeffs,B); tot = sum(values_1)

@time  values_1, dvalues_1 = eval_UC_spline_G(a,b,orders,coeffs,B);total_1 = sum(values_1)


module TestFactorizationMachines

using FactorizationMachines
using Base.Test

matrix_dim = 4
X = eye(matrix_dim)
y = [.1, .2, .3, .4]

info("Solving system of equations with identity matrix")
fm = train(sparse(X), y, model_params = Models.gauss(k0 = false, k1 = true, num_factors = 0))
@test fm.model.w0 == 0
@test size(fm.model.V) == (0, matrix_dim)
@test_approx_eq_eps 0.0 evaluate(Evaluators.rmse(), fm, sparse(X), y) 1e-1

info("Solving system of equations with identity matrix")
fm = train(sparse(X), y, model_params = Models.gauss(k0 = false, k1 = false))
@test fm.model.w0 == 0
@test fm.model.w == zeros(matrix_dim)
@test_approx_eq_eps 0.0 evaluate(Evaluators.rmse(), fm, sparse(X), y) 1e-1

end


using PyCall
using Makie

# Construct data
spt = pyimport_conda("scipy.spatial", "scipy")

# Generate random points
pnts = [Point(rand(2)...) for i in 1 : 100]

# Find the convex hull
hull = spt.ConvexHull(collect(hcat(collect.(pnts)...)') )
vtx =  [Point(val[1], val[2]) for val in eachrow(hull.points[hull.vertices .+ 1 , :])]

fig, ax, plt = scatter(pnts, color=:black, markersize=5)
lines!(vtx, linewidth=3)
lines!(vtx[[1, end]], linewidth=3)
fig


@testset "Check center and scaling methods" begin

    @testset "Check center" begin

        @test IndexFunArrays.get_offset((5,), CtrCorner) == (1.0,)
        @test IndexFunArrays.get_offset((4,), CtrCorner) == (1.0,)
        @test IndexFunArrays.get_offset((5, 2), CtrFT) == (3.0, 2.0)
        @test IndexFunArrays.get_offset((5, 1), CtrFT) == (3.0, 1.0)
        @test IndexFunArrays.get_offset((5, 2), CtrFFT) == (1.0, 1.0)

        @test IndexFunArrays.get_offset((5, 2), CtrMid) == (3.0, 1.5)
        @test IndexFunArrays.get_offset((5, 3), CtrMid) == (3.0, 2.0)

        @test IndexFunArrays.get_offset((5, 2), CtrRFT) == (1, 2)
        @test IndexFunArrays.get_offset((5, 3), CtrRFT) == (1, 2)

        @test IndexFunArrays.get_offset((5, 2), CtrRFFT) == (1, 1)
        @test IndexFunArrays.get_offset((5, 3), CtrRFFT) == (1, 1)

        @test IndexFunArrays.get_offset((1123, 3123, 2), 123.123) == (123.123, 123.123, 123.123)

        @test IndexFunArrays.get_offset((5, 4), CtrEnd) == (5.0, 4.0)
    end

    @testset "Check scaling" begin
        @test IndexFunArrays.get_scale((5, 4), ScaFT) === (0.2, 0.25)
        @test IndexFunArrays.get_scale((5, 4), ScaUnit) == (1, 1)
        @test IndexFunArrays.get_scale((6, 5), ScaNorm) == (0.2, 0.25)
        @test IndexFunArrays.get_scale((6, 5), ScaNorm) == (0.2, 0.25)
        @test IndexFunArrays.get_scale((6, 5), ScaMid) == (0.4, 0.5)
        @test IndexFunArrays.get_scale((5,3), (2,11)) == (2,11)

        @test IndexFunArrays.get_scale((12, 1, 2), 2.123f0) == (2.123f0, 2.123f0, 2.123f0)

    end
end


function permutest(disp ::Disp,n_perm = 10000)
    """
    This function permutes the residuals returned by 'dispersion' to provide global and pairwise
    P-values. Currently, no corrections are made for multiple testing. MultipleTesting.jl provides
    this functionality, if desired

    The function accepts:
        disp: The named tuple returned by 'dispersion'
        n_perm: the number of permutations

    This function returns a named tuple containing:
        P = Global P-value
        pairwise_P = pairwise P-value
        F = Global F-Statistic
        pairwise_F = pairwise F-statistics

    https://github.com/juliangehring/MultipleTesting.jl
    """
    # read in values from `disp`
    F = disp.F
    R = disp.residuals
    F_pairs = disp.pairwise_F
    group = disp.group
    levels = disp.levels
    level_names = string.(levels)
    inds = [group .== level for level in levels]
    n = length(levels)

    # pre-calculate values for ANOVA function
    r = vcat(R...)
    X = vcat(R...)
    N = length(X)
    nj = Tuple(length.(R))
    X̅ = mean(X)
    k = length(R)
    # generate empty containers for F values
    perm = Vector{Float64}(undef,n_perm)
    #rs = copy(R)#Vector{Vector}(undef,n)
    # run permutation
    @inbounds for p in 1:n_perm
        shuffle!(r)
        rs = [view(r,inds[i]) for i in 1:n]

        perm[p] =f(rs,N,nj,X̅,k)
    end
    ppairs =zeros(k,k)

    Threads.@threads for i in 1:k-1
        for j in i+1:k
            r_1 = r[group .== levels[i]]
            r_2 = r[group .== levels[j]]
            pstat =permutest(r_1,r_2, F_pairs[j,i],n_perm)
            ppairs[j,i] = pstat
        end
    end
    # calculate P-values and return P and F values
    P = sum(perm .> F)/n_perm
    P_pairs = NamedArray(ppairs,( level_names,level_names), ("group","group"))
    F_pairs = NamedArray(LowerTriangular(F_pairs),( level_names,level_names ), ("group","group"))

    return (P =P,pairwise_P = P_pairs,F = F, pairwise_F =F_pairs)
end

function permutest(r_1,r_2 ,F,n_perm = 1000)
    r = vcat(r_1,r_2)
    nj = (length(r_1),length(r_2))
    n = 2
    N = sum(nj)
    # pre-calculate values for ANOVA function
    X̅ = mean(r)
    k = 2
    inds = (1:nj[1],1:nj[2])
    # generate empty containers for F values
    perm = Vector{Float64}(undef,n_perm)
    # run permutation
    @inbounds for p in 1:n_perm
        shuffle!(r)
        rs = (view(r,inds[1]) ,view(r,inds[2]) )
        perm[p] =f(rs,N,nj,X̅,k)
    end
    P = sum(perm .> F)/n_perm
    return P
end


using Knet, ParticleSwarmOptimization
include("Args.jl")
include("Functions.jl")

function main()
    gpu(false)
    args = parse_commandline()
    println("Parsed args:")

    for (arg,val) in args
        println("  $arg  =>  $val")
    end

    problem = RegularProblem(eval(parse(args["f"])), args["dim"], args["l"], args["h"])
    gbest = pso(problem, args["num"], args["epoch"], args["phi1"], args["phi2"], args["w"], args["vmin"], args["vmax"])

    println("\nBest: $(gbest.pbest_fitness), $(gbest.x)")
end

main()


"""
`MaternKernel([ρ=1.0,[ν=1.0]])`
The matern kernel is an isotropic Mercer kernel given by the formula:
```
    κ(x,y) = 2^{1-ν}/Γ(ν)*(√(2ν)‖x-y‖)^ν K_ν(√(2ν)‖x-y‖)
```
For `ν=n+1/2, n=0,1,2,...` it can be simplified and you should instead use [`ExponentialKernel`](@ref) for `n=0`, [`Matern32Kernel`](@ref), for `n=1`, [`Matern52Kernel`](@ref) for `n=2` and [`SqExponentialKernel`](@ref) for `n=∞`.
"""
struct MaternKernel{T,Tr,Tν<:Real} <: Kernel{T,Tr}
    transform::Tr
    metric::Euclidean
    ν::Tν
    function MaternKernel{T,Tr,Tν}(transform::Tr,ν::Tν) where {T,Tr<:Transform,Tν<:Real}
        return new{T,Tr,Tν}(transform,Euclidean(),ν)
    end
end

function MaternKernel(ρ::T₁=1.0,ν::T₂=1.5) where {T₁<:Real,T₂<:Real}
    @check_args(MaternKernel, ν, ν > zero(T₂), "ν > 0")
    MaternKernel{T₁,ScaleTransform{Base.RefValue{T₁}},T₂}(ScaleTransform(ρ),ν)
end

function MaternKernel(ρ::A,ν::T=1.5) where {A<:AbstractVector{<:Real},T<:Real}
    @check_args(MaternKernel, ν, ν > zero(T), "ν > 0")
    MaternKernel{eltype(A),ScaleTransform{A},T}(ScaleTransform(ρ),ν)
end

function MaternKernel(t::Tr,ν::T=1.5) where {Tr<:Transform,T<:Real}
    @check_args(MaternKernel, ν, ν > zero(T), "ν > 0")
    MaternKernel{eltype(t),Tr,T}(t,ν)
end

params(k::MaternKernel) = (params(transform(k)),k.ν)

@inline kappa(κ::MaternKernel, d::Real) = iszero(d) ? one(d) : exp((1.0-κ.ν)*logtwo-lgamma(κ.ν) + κ.ν*log(sqrt(2κ.ν)*d)+log(besselk(κ.ν,sqrt(2κ.ν)*d)))

"""
`Matern32Kernel([ρ=1.0])`
The matern 3/2 kernel is an isotropic Mercer kernel given by the formula:
```
    κ(x,y) = (1+√(3)ρ‖x-y‖)exp(-√(3)ρ‖x-y‖)
```
"""
struct Matern32Kernel{T,Tr} <: Kernel{T,Tr}
    transform::Tr
    metric::Euclidean
    function Matern32Kernel{T,Tr}(transform::Tr) where {T,Tr<:Transform}
        return new{T,Tr}(transform,Euclidean())
    end
end

@inline kappa(κ::Matern32Kernel, d::T) where {T<:Real} = (1+sqrt(3)*d)*exp(-sqrt(3)*d)

"""
`Matern52Kernel([ρ=1.0])`
The matern 5/2 kernel is an isotropic Mercer kernel given by the formula:
```
    κ(x,y) = (1+√(5)ρ‖x-y‖ + 5ρ²‖x-y‖^2/3)exp(-√(5)ρ‖x-y‖)
```
"""
struct Matern52Kernel{T,Tr} <: Kernel{T,Tr}
    transform::Tr
    metric::Euclidean
    function Matern52Kernel{T,Tr}(transform::Tr) where {T,Tr<:Transform}
        return new{T,Tr}(transform,Euclidean())
    end
end

@inline kappa(κ::Matern52Kernel, d::Real) where {T} = (1+sqrt(5)*d+5*d^2/3)*exp(-sqrt(5)*d)


using Images, IJulia, Plots
gr() # FASTER!

#using PyPlot, PyCall,
# using Plots
#@pyimport matplotlib.cm as cm
#@pyimport matplotlib.animation as anim

function makeMask(r_out, r_in=0.0, AA=0.0; normalize=true)
    @assert(AA>=0.0, "Antialiasing border size cannot be negative!")
    mask = zeros(Float64, 2*r_out+1, 2*r_out+1)
    cen = (r_out+1, r_out+1)
    for x in -r_out:r_out
        for y in -r_out:r_out
            l = sqrt(x^2 + y^2)
            if r_in <= l < r_out + AA/2
                if l < r_out - AA/2
                    mask[x+cen[1], y+cen[2]] = 1.0
                else
                    mask[x+cen[1], y+cen[2]] = (r_out + AA/2 - l)/AA
                end
            end
        end
    end

    return normalize ? mask/sum(mask) : mask
end

"""
    makeInnerMask(r_in[, AA=0.0])

Create a matrix filled with a ring of non-zero floats.
The sum of matrix is normalized to be equal to 1.0.

# Arguments
* `r_in::Int`: the inner radius of the ring. Must be smaller than r_out.
* `AA::Float64`: Thickness of antialiasing. 0.0 means no antialiasing.
"""
function makeInnerMask(r_in, AA=0.0; verbose=false)::Array{Float64,2}
    mask = makeMask(r_in, 0.0, AA)
    if verbose; plt[:figure](); plt[:axis]("off"); plt[:imshow](mask) end
    return mask
end

"""
    makeOuterMask(r_out, r_in[, AA=false])

Create a matrix filled with a ring of non-zero floats.
The sum of matrix is normalized to be equal to 1.0.

# Arguments
* `r_out::Int64`: the outer radius of the ring. Must be greater than r_in.
* `r_in::Int64`: the inner radius of the ring. Must be smaller than r_out.
* `AA`: Thickness of antialiasing. 0.0 means no antialiasing.
"""
function makeOuterMask(r_out, r_in, AA=0.0; verbose=false, subtract=false)::Array{Float64,2}
    mask = nothing
    if subtract
        mask = makeOuterMasksubtracted(r_out, r_in, AA)
    else
        mask = makeMask(r_out, r_in, AA)
    end

    if verbose; plt[:figure](); plt[:axis]("off"); plt[:imshow](mask) end
    return mask
end

"""
In this version, the inner mask is subtracted. Using antialiasing, this will lead to an continuous transition from inner to outer.
For further information, see 'makeOuterMask'.
"""
function makeOuterMasksubtracted(r_out, r_in, AA=0.0; verbose=false)::Array{Float64,2}
    mask = makeMask(r_out, 0.0, AA; normalize=false)

    # make the hole of the ring
    #mask_sub = zeros(Float64, 2*r_out+1, 2*r_out+1)
    mask_in = makeMask(r_in, 0.0, AA, normalize=false)
    println(size(mask_in))
    println(size(mask))
    println(size(mask[r_out-r_in+1:r_out+r_in+1, r_out-r_in+1:r_out+r_in+1]))
    flush(stdout)
    mask[r_out-r_in+1:r_out+r_in+1,r_out-r_in+1:r_out+r_in+1] -= mask_in

    #normalize mask, so that sum(mask) == 1.0
    mask /= sum(mask)

    if verbose; plt[:figure](); plt[:axis]("off"); plt[:imshow](mask) end
    return mask
end

"""
Add a splat to the given grid. Can be a square or circle.
Note: In a stable world, splats should become rings.

# Arguments
* `grid`:
* `height`: grid height
* `width`: grid width
* `r_out`: outer radius of conv mask
* `at`   : position as 2D array. If 'nothing', a random position is chosen instead.
* `val`  : Value of splat. Must be within [0,1]
* `round`: if true, make round splats. Make squares otherwise
"""
function splat!(grid::Array{Float64,2}, r_out, at = nothing, val = nothing; round=true)
    width = size(grid)[1]
    height = size(grid)[2]

    if at == nothing
        x = floor(Int64, rand()*width)+1
        y = floor(Int64, rand()*height)+1
    else
        if (x < 0 || x > width || y < 0 || y > height)
            throw(ArgumentError("Given position is outside of the grid boundaries!"))
        end
        x = at[1]
        y = at[2]
    end

    if val == nothing
        c = max(0.5, rand()) # val ∈ [0.5, 1.0]
    else
        if (val < 0 | val > 1)
            throw(ArgumentError("val must be within [0,1]!"))
        end
        c = val
    end

    for dx in -r_out:r_out
        for dy in -r_out:r_out
            # make a splat around [x, y]
            ix = x+dx
            iy = y+dy
            if 1<=ix<=width && 1<=iy<=height && (!round || round && sqrt(dx^2+dy^2) <= r_out)
                grid[ix,iy] = c
            end
        end
    end

    return grid
end

"""
Return a grid initiated with splats.

# Arguments
* `height`: grid height
* `width`: grid width
* `r_out`: outer radius of conv mask
"""
function initGrid(height, width, r_out)::Array{Float64,2}
	grid = zeros(Float64, height,width)
	for t in 0:((width/r_out)*(height/r_out))
		splat!(grid, r_out)
	end
    return grid
end

function sigma1(x::Float64, a::Float64, alpha::Float64)::Float64
    return 1.0 / ( 1.0 + exp(-(x-a)*4.0/alpha));
end

"""
Corrected rules according to ref. implementation
"""
function sigma2(x::Float64, a::Float64, b::Float64)::Float64
    αn = 0.028
    return sigma1(x,a,αn) * ( 1.0 - sigma1(x,b,αn));
end

function sigmam(x::Float64, y::Float64, m::Float64)::Float64
    αm = 0.147
    return x * ( 1.0 - sigma1(m,0.5,αm)) + y*sigma1(m, 0.5, αm);
end

"""
Return the new filling based of the area of the `outer` and the `inner` ring.
"""
function discrete_step(outer::Float64, inner::Float64)::Float64
    # Static Rule Set 1
    # Tested with euler stepping
    # Result: results in static, non moving pattern for dt < 0.35
    # Result: for dt = 0.5, incremental filling can be observed
    #b1 = 0.278
    #b2 = 0.365
    #d1 = 0.267
    #d2 = 0.445
    b1 = 0.257
    b2 = 0.336
    d1 = 0.365
    d2 = 0.549

    return sigmam(sigma2(outer,b1,b2), sigma2(outer,d1,d2),inner);
end

function discrete_as_euler(outer::Float64, inner::Float64)::Float64
    return 2.0 * discrete_step(outer, inner) - 1.0;
end

function next_step_as_euler(f::Float64, outer::Float64, inner::Float64, dt::Float64)::Float64
    return max(0.0, min(1.0, f + dt * discrete_as_euler(outer,inner)));
end

"""
Create two grids. Either empty or randomized. Randomized version uses splats.
Return a tuple. The first entry is a new grid, the second is an uninitialized, 2d array of the same size (for swapping)

Splats will be of size `r_out`.
"""
function createGrid(width=100, height=100, splat=true; r_out=21)::Tuple{Array{Float64,2}, Array{Float64,2}}
    if splat
        return (initGrid(height, width, r_out), Array{Float64, 2}(undef, width, height))
    else
        return (zeros(Float64, width, height), Array{Float64,2}(undef, width, height))
    end
end

"""
Start the smoothlife simulation. All results are estimates. Smaller time steps and greater masks improve resolution
but increase computation time also.

#Arguments
* `runs`: The number of total iterations (convolutions)
* `grid_width`:
* `grid_height`:
* `r_in::Int64`: Size of the inner ring. Greater radius takes more computation time, but improves precision.
* `r_out::Int64`: Size of outer ring. Greater radius takes more computation time, but improves precision/stability. Must be greater than radius_inner. Use 3*radius_inner for best results.
* `dt`: delta time. How much the time passes between two calculations. The greater, the rougher the estimation.
* `sleepTime`: Number of seconds the cpu waits after one complete convolution
* `showOnConsole`: if true, output grid in a python frame. Updates the same frame (video functionality)
* `verbose`: additional information output (e.g. masks)
"""
function simulate(
        ;
        runs = 10,
        start_grid = nothing,
        grid_width = 512,
        grid_height = 512,
        r_in = 7,
        r_out = 3*r_in,
        dt = 0.05,
        sleepTime=0.1,
        save_all_steps = true,
        showOnConsole=true,
        verbose=false
    )::Dict{}

    if start_grid == nothing
        (curGrid, newGrid) = createGrid(grid_width, grid_height; r_out=r_out)
    else
        (curGrid, newGrid) = (start_grid, zeros(Float64, grid_width, grid_height))
    end

    if save_all_steps == true
        # prepare full array for storage
        all_steps = Array{Float64}(undef, grid_width, grid_height, runs)
    end

    # prevent Atom from capturing the plot
    # that will not work!
    #ENV["PLOTS_USE_ATOM_PLOTPANE"] = "false"

    mask_in = makeInnerMask(r_in, 1.0; verbose=verbose)
    mask_out = makeOuterMask(r_out, r_in, 1.0; subtract=true, verbose=verbose)

    #plt[:axis]("off")
    #im = plt[:imshow](curGrid, cmap=cm.Greys_r, vmin = 0.0, vmax = 1.0)

    for run in 1:runs
        # convolution -> get fillings
        convInner = imfilter(curGrid, mask_in, "circular")  # HACK: fft is faster for masks greater 20x20
        convOuter = imfilter(curGrid, mask_out, "circular")

        # calc new frame
        newGrid = zeros(Float64, grid_width, grid_height) # just for debugging.
        for x in 1:grid_width
            for y in 1:grid_height
                #newGrid[x,y] = smoothStep(curGrid[x,y], convOuter[x,y], convInner[x,y], dt)
                newGrid[x,y] = next_step_as_euler(curGrid[x,y], convOuter[x,y], convInner[x,y], dt)
            end
        end

        # swap
        curGrid = newGrid

        if save_all_steps
            all_steps[:,:,run] = curGrid
        end

        # more colorful version, but may need a gradient to control the fill scale
        grad = ColorGradient(:grays)
        # grad = ColorGradient(RGBA{Float64}[RGBA{Float64}(0.05,0.05,0.05,1.0), RGBA{Float64}(0.95,0.95,0.95,1.0)], [0.0, 1.0])
        heatmap(curGrid,
            c = grad, # seems to be the same as :grays, tbh.
            legend = nothing,
            xticks = nothing,
            yticks = nothing,
            aspect_ratio = 1.0)
        gui()
        sleep(sleepTime)
    end

    return Dict("last" => curGrid, "all" => all_steps);
end

function save_runs_as_video(all_steps, file_name, save_type = ".mp4")
    if !(save_type in (".gif", ".mp4", ".mov"))
        throw(ArgumentError("Invalid save type! Please use either '.gif', '.mp4' or '.mov'!"))
    end

    dim = size(res["all"])
    nruns = dim[3]
    width = dim[1]
    height = dim[2]
    anim = @animate for i in 1:nruns
        heatmap(res["all"][:,:,i],
            c = grad, # seems to be the same as :grays, tbh.
            legend = nothing,
            xticks = nothing,
            yticks = nothing,
            aspect_ratio = 1.0)
    end

    # this function will also produce other file types based on the file ending!
    gif(anim, file_name * save_type, fps = 15);
    return anim;
end


module MultiFieldFEOperatorsTests

using Gridap.Arrays
using Gridap.Algebra
using Gridap.Geometry
using Gridap.FESpaces
using Gridap.Fields
using Gridap.Integration
using Gridap.MultiField
using Gridap.CellData
using SparseArrays
using Test
using LinearAlgebra

using Gridap.MultiField

order = 2

domain = (0,1,0,1)
partition = (3,3)
model = CartesianDiscreteModel(domain,partition)

trian = get_triangulation(model)
degree = order
quad = CellQuadrature(trian,degree)

strian = SkeletonTriangulation(model)
sdegree = order
squad = CellQuadrature(strian,sdegree)

V = TestFESpace(model=model,order=order,reffe=:Lagrangian,conformity=:H1,valuetype=Float64)
Q = TestFESpace(model=model,order=order-1,reffe=:Lagrangian,conformity=:L2,valuetype=Float64)

U = TrialFESpace(V)
P = TrialFESpace(Q)

Y = MultiFieldFESpace([V,Q])
X = MultiFieldFESpace([U,P])

function a(x,y)
  u,p = x
  v,q = y
  v*u + v*p - q*p
end

function l(y)
  v,q = y
  v*4 + q
end

function a_Γ(x,y)
  u,p = x
  v,q = y
  jump(v)*mean(u) + jump(∇(q))⋅jump(∇(p)) - mean(v)*mean(p)
end

t_Ω = AffineFETerm(a,l,trian,quad)
t_Γ = LinearFETerm(a_Γ,strian,squad)

op = AffineFEOperator(X,Y,t_Ω,t_Γ)

op = AffineFEOperator(SparseMatrixCSR,X,Y,t_Ω,t_Γ)

op = FEOperator(X,Y,t_Ω,t_Γ)
xh = zero(X)
b = residual(op,xh)
A = jacobian(op,xh)
test_fe_operator(op,get_free_values(xh),b)

function r(x,y)
  u,p = x
  v,q = y
  v*(u*u) + v*p*u - q*p - v*4 + q
end

function j(x,dx,y)
  u,p = x
  du,dp = dx
  v,q = y
  2*v*u*du + v*dp*u + v*p*du - q*dp
end

t_Ω = FETerm(r,j,trian,quad)
t_Ω_auto = FETerm(r,trian,quad)

x = FEFunction(X,rand(num_free_dofs(X)))
dx = get_cell_basis(X)
y = get_cell_basis(Y)

cell_r = get_cell_residual(t_Ω,x,y)
cell_j = get_cell_jacobian(t_Ω,x,dx,y)

cell_r_auto = get_cell_residual(t_Ω_auto,x,y)
cell_j_auto = get_cell_jacobian(t_Ω_auto,x,dx,y)
test_array(cell_r_auto,cell_r,≈)
test_array(cell_j_auto,cell_j,≈)

op = FEOperator(X,Y,t_Ω_auto)
xh = zero(X)
b = residual(op,xh)
A = jacobian(op,xh)
test_fe_operator(op,get_free_values(xh),b)

end # module


import MLBase
import ROCAnalysis
import StatsBase

function _aurocc_trapz(
        ytrue::AbstractVector{<:Integer},
        yscore::AbstractVector{<:Real},
        )
    allfpr, alltpr, allthresholds = roccurve(
        ytrue,
        yscore,
        )
    #
    permutation = sortperm(allthresholds; rev=true)
    allfpr = allfpr[permutation]
    alltpr = alltpr[permutation]
    allthresholds = allthresholds[permutation]
    #
    x = allfpr
    y = alltpr
    aurocc_trapz_result = trapz(x, y)
    return aurocc_trapz_result
end

function _aurocc_verify(
        ytrue::AbstractVector{<:Integer},
        yscore::AbstractVector{<:Real},
        )
    targetlevel = 1
    nontargetlevel = 0
    targetscores = yscore[ytrue .== targetlevel]
    nontargetscores = yscore[ytrue .== nontargetlevel]
    r = ROCAnalysis.roc(targetscores, nontargetscores)
    complement_of_aurocc = ROCAnalysis.auc(r)
    aurocc_verify_result = 1 - complement_of_aurocc
    return aurocc_verify_result
end

"""
"""
function aurocc(
        ytrue::AbstractVector{<:Integer},
        yscore::AbstractVector{<:Real},
        )
    aurocc_trapz_value = _aurocc_trapz(
        ytrue,
        yscore,
        )
    aurocc_verify_value = _aurocc_verify(
        ytrue,
        yscore,
        )
    if !( isapprox(aurocc_trapz_value, aurocc_verify_value; atol=0.00000001) )
        error("Was not able to accurately compute the AUROCC.")
    end
    return aurocc_trapz_value
end


using Base.Test
using suduko
p1=[0,7,0,9,6,4,0,0,0]
p2=[0,3,0,1,0,0,0,4,6]
p3=[0,0,0,8,0,0,0,0,5]
p4=[0,2,0,0,0,0,0,0,3]
p5=[6,0,0,0,0,0,0,0,4]
p6=[4,0,0,0,0,0,0,9,0]
p7=[1,0,0,0,0,3,0,0,0]
p8=[3,9,0,0,0,5,0,1,0]
p9=[0,0,0,7,1,6,0,5,0]

tester=hvcat(9,p1,p2,p3,p4,p5,p6,p7,p8,p9)
print ("in tester")
print(suduko.notConsistent (tester));
print ("done")

p1=[7,7,7,9,6,4,0,0,0] #this is not consistent
p2=[7,3,0,1,0,0,0,4,6]
p3=[0,0,0,8,0,0,0,0,5]
p4=[0,2,0,0,0,0,0,0,3]
p5=[6,0,0,0,0,0,0,0,4]
p6=[4,0,0,0,0,0,0,9,0]
p7=[1,0,0,0,0,3,0,0,0]
p8=[3,9,0,0,0,5,0,1,0]
p9=[0,0,0,7,1,6,0,5,0]

tester=hvcat(9,p1,p2,p3,p4,p5,p6,p7,p8,p9)

println("in tester")
print (suduko.notConsistent(tester))
print ("done")


using Quaternions
using DualNumbers
using ForwardDiff
using LinearAlgebra
using Random
using Test

function fdquat_to_dualquat(q::Quaternion{<:ForwardDiff.Dual})
    q0 = quat(
        ForwardDiff.value(q.s),
        ForwardDiff.value(q.v1),
        ForwardDiff.value(q.v2),
        ForwardDiff.value(q.v3),
    )
    qe = quat(
        ForwardDiff.partials(q.s, 1),
        ForwardDiff.partials(q.v1, 1),
        ForwardDiff.partials(q.v2, 1),
        ForwardDiff.partials(q.v3, 1),
    )
    return dualquat(q0, qe)
end

function dualquat_to_fdquat(dq::DualQuaternion)
    return quat(
        ForwardDiff.Dual(dq.q0.s, dq.qe.s),
        ForwardDiff.Dual(dq.q0.v1, dq.qe.v1),
        ForwardDiff.Dual(dq.q0.v2, dq.qe.v2),
        ForwardDiff.Dual(dq.q0.v3, dq.qe.v3),
    )
end

@testset "DualQuaternion" begin
    @testset "type aliases" begin
        @test DualQuaternionF16 === DualQuaternion{Float16}
        @test DualQuaternionF32 === DualQuaternion{Float32}
        @test DualQuaternionF64 === DualQuaternion{Float64}
    end

    @testset "Constructors" begin
        @testset "from quaternions" begin
            q0 = Quaternion{Int}(1, 2, 3, 4, false)
            qe = QuaternionF32(5, 6, 7, 8, false)
            @test @inferred(DualQuaternion(q0, qe)) isa DualQuaternionF32
            @test DualQuaternion(q0, qe) === DualQuaternionF32(
                QuaternionF32(1, 2, 3, 4, false), QuaternionF32(5, 6, 7, 8, false), false
            )
            @test @inferred(DualQuaternionF64(q0, qe, false)) === DualQuaternionF64(
                QuaternionF64(1, 2, 3, 4, false), QuaternionF64(5, 6, 7, 8, false), false
            )
            @test DualQuaternionF64(q0, qe, true) === DualQuaternionF64(
                QuaternionF64(1, 2, 3, 4, false), QuaternionF64(5, 6, 7, 8, false), true
            )

            qnorm = Quaternion(0//1, 1//1, 0//1, 0//1, true)
            @test @inferred(DualQuaternion(q0)) isa DualQuaternion{Int}
            @test DualQuaternion(qnorm) === DualQuaternion(qnorm, zero(qnorm), true)
            @test @inferred(DualQuaternionF64(q0)) ===
                DualQuaternionF64(QuaternionF64(q0), zero(QuaternionF64), false)
        end
        @testset "from dual" begin
            coef = (dual(1, 2), dual(3.0, 4.0), dual(5.0f0, 6.0f0), dual(7//1, 8//1))
            @testset for T in (Float32, Float64, Int), norm in (true, false)
                dq = @inferred DualQuaternion{T}(coef..., norm)
                @test dq isa DualQuaternion{T}
                @test dq.norm === norm
                @test dq === DualQuaternion{T}(convert.(Dual{T}, coef)..., norm)
                @test dq == DualQuaternion(
                    Quaternion(DualNumbers.value.(coef)...),
                    Quaternion(DualNumbers.epsilon.(coef)...),
                )
                dq2 = @inferred DualQuaternion(convert.(Dual{T}, coef)..., norm)
                @test DualQuaternion(convert.(Dual{T}, coef)..., norm) === dq
                @test DualQuaternion(coef[1]) ==
                    DualQuaternion(coef[1], fill(zero(coef[1]), 3)...)
                @test DualQuaternion{T}(coef[1]) ==
                    DualQuaternion(convert(Dual{T}, coef[1]), fill(zero(Dual{T}), 3)...)
                if !norm
                    @test DualQuaternion(convert.(Dual{T}, coef)...) === dq
                end
            end
        end
        @testset "from real" begin
            @testset for x in (-1//1, 1.0, 2.0), T in (Float32, Float64, Int, Rational{Int})
                coef = (Quaternion{T}(x, 0, 0, 0, isone(abs(x))), zero(Quaternion{T}))
                @test @inferred(DualQuaternion{T}(x)) ===
                    DualQuaternion{T}(coef..., isone(abs(x)))
                @test @inferred(DualQuaternion(T(x))) ===
                    DualQuaternion{T}(coef..., isone(abs(x)))
            end
        end
        @testset "from dual quaternion" begin
            dq = DualQuaternion(
                QuaternionF32(1, 2, 3, 4, false), QuaternionF32(4, 5, 6, 7, false), false
            )
            @test @inferred(DualQuaternion(dq)) === dq
            @test @inferred(DualQuaternionF64(dq)) ===
                DualQuaternionF64(dq.q0, dq.qe, false)
        end
        @testset "from vector" begin
            v = randn(3)
            @test @inferred(DualQuaternion(v)) ===
                DualQuaternion(Quaternion(zero(v)), Quaternion(v))
        end
    end

    @testset "==" begin
        @test DualQuaternion(Quaternion(1, 2, 3, 4), Quaternion(5, 6, 7, 8)) ==
            DualQuaternion(Quaternion(1.0, 2, 3, 4), Quaternion(5, 6, 7, 8))
        @test DualQuaternion(Quaternion(1, 2, 3, 4), Quaternion(5, 6, 7, 8)) !=
            DualQuaternion(Quaternion(1.0, 2, 3, 4), Quaternion(1, 2, 3, 4))
    end

    @testset "convert" begin
        @test convert(DualQuaternion{Float64}, 1) === DualQuaternion(1.0)
        @test convert(DualQuaternion{Float64}, DualNumbers.Dual(1, 2)) ===
            DualQuaternion(Quaternion(1.0), Quaternion(2.0))
        @test convert(DualQuaternion{Float64}, Quaternion(1, 2, 3, 4)) ===
            DualQuaternion(Quaternion(1.0, 2.0, 3.0, 4.0))
        @test convert(
            DualQuaternion{Float64},
            DualQuaternion(Quaternion(1, 2, 3, 4), Quaternion(5, 6, 7, 8)),
        ) === DualQuaternion(Quaternion(1.0, 2.0, 3.0, 4.0), Quaternion(5.0, 6.0, 7.0, 8.0))
    end

    @testset "promote" begin
        @test promote(DualQuaternion(1.0), 1.0) ===
            (DualQuaternion(1.0), DualQuaternion(1.0))
        @test promote(DualQuaternion(1.0f0), 2.0) ===
            (DualQuaternion(1.0), DualQuaternion(2.0))
        @test promote(DualQuaternion(1.0f0), dual(1, 2)) ===
            (DualQuaternion(1.0f0), DualQuaternion(dual(1.0f0, 2.0f0)))
        @test promote(DualQuaternion(1.0f0), Quaternion(3//1)) ===
            (DualQuaternion(1.0f0), DualQuaternion(3.0f0))
        @test promote(DualQuaternion(1.0f0), DualQuaternion(2.0)) ===
            (DualQuaternion(1.0), DualQuaternion(2.0))

        @test Quaternion(1, 2, 3, 4) == DualQuaternion(Quaternion(1, 2, 3, 4))
        @test Quaternion(1, 2, 3, 4) !=
            DualQuaternion(Quaternion(1, 2, 3, 4), Quaternion(5, 6, 7, 8))
        @test DualQuaternion(1) == 1.0
    end

    @testset "shorthands" begin
        @test dualquat(Quaternion(1, 0, 0, 0)) == Quaternion(1, 0, 0, 0)
        @test dualquat(Quaternion(1, 2, 3, 4)) == Quaternion(1, 2, 3, 4)
        @test dualquat(Quaternion(1, 0, 0, 0)) === DualQuaternion(Quaternion(1, 0, 0, 0)) # checking the .norm field in particular
        @test dualquat(Quaternion(1, 2, 3, 4)) === DualQuaternion(Quaternion(1, 2, 3, 4))
        @test dualquat(1) === DualQuaternion(1)
        @test dualquat(Dual(1, 2)) === DualQuaternion(Dual(1, 2))
        @test dualquat(Dual(1, 2), Dual(0), Dual(0), Dual(0)) ===
            DualQuaternion(Dual(1, 2), Dual(0), Dual(0), Dual(0))
        @test dualquat(Quaternion(1, 2, 3, 4), Quaternion(5, 6, 7, 8)) ==
            DualQuaternion(Quaternion(1, 2, 3, 4), Quaternion(5, 6, 7, 8))
        @test dualquat(Quaternion(1, 0, 0, 0), Quaternion(0)).norm == false
        @test dualquat(Quaternion(1, 0, 0, 0), Quaternion(0), false).norm == false # respect the .norm input (even if wrong)
        @test dualquat(Quaternion(1, 2, 3, 4), Quaternion(0), true).norm == true # respect the .norm input (even if wrong)
        @test dualquat(Dual(2, 0), Dual(0), Dual(0), Dual(0), true).norm == true # respect the .norm input (even if wrong)
        @test dualquat(Dual(1, 0), Dual(0), Dual(0), Dual(0), false).norm == false # respect the .norm input (even if wrong)
    end

    @testset "random generation" begin
        @testset "dualquatrand" begin
            dq = dualquatrand()
            @test dq isa DualQuaternionF64
            @test !dq.norm
        end

        @testset "ndualquatrand" begin
            dq = ndualquatrand()
            @test dq isa DualQuaternionF64
            @test dq.norm
        end

        @testset "rand($H)" for H in (DualQuaternionF32, DualQuaternionF64)
            rng = Random.MersenneTwister(42)
            dq = rand(rng, H)
            @test dq isa H
            @test !dq.norm

            dqs = rand(rng, H, 1000)
            @test eltype(dqs) === H
            @test length(dqs) == 1000
            xs = map(dqs) do dq
                return [
                    real(dq.q0)
                    imag_part(dq.q0)...
                    real(dq.qe)
                    imag_part(dq.qe)...
                ]
            end
            xs_mean = sum(xs) / length(xs)
            xs_var = sum(x -> abs2.(x .- xs_mean), xs) / (length(xs) - 1)
            @test all(isapprox.(xs_mean, 0.5; atol=0.1))
            @test all(isapprox.(xs_var, 1 / 12; atol=0.01))
        end
    end

    @testset "basic" begin
        q = rand(DualQuaternionF64)
        qnorm = normalize(q)
        @test_throws MethodError imag(q)
        @test_throws MethodError Quaternions.imag(q)
        @test_throws MethodError imag_part(q)
        @test conj(q) === dualquat(conj(q.q0), conj(q.qe), q.norm)
        @test conj(qnorm) === dualquat(conj(qnorm.q0), conj(qnorm.qe), qnorm.norm)
        @test conj(conj(q)) === q
        @test conj(conj(qnorm)) === qnorm
        @test float(dualquat(dual.(1:4, 5:8)...)) === dualquat(dual.(1.0:4.0, 5.0:8.0)...)
        @test dconj(q) === dualquat(q.q0, -q.qe, q.norm)
        @test dconj(qnorm) === dualquat(qnorm.q0, -qnorm.qe, qnorm.norm)
        @test Quaternions.Q0(q) === q.q0
        @test Quaternions.Qe(q) === q.qe
    end

    @testset "algebraic properties" begin
        @testset "addition/subtraction" begin
            for _ in 1:100
                dq1, dq2 = rand(DualQuaternionF64, 2)
                @test (dq1 + dq2).q0 ≈ dq1.q0 + dq2.q0
                @test (dq1 + dq2).qe ≈ dq1.qe + dq2.qe
                @test (dq1 - dq2).q0 ≈ dq1.q0 - dq2.q0
                @test (dq1 - dq2).qe ≈ dq1.qe - dq2.qe
                @test (+dq1).q0 === dq1.q0
                @test (+dq1).qe === dq1.qe
                @test (-dq1).q0 === -dq1.q0
                @test (-dq1).qe === -dq1.qe
            end
        end
        @testset "division" begin
            for _ in 1:100
                dq = rand(DualQuaternionF64)
                dq2 = dq / dq
                @test dq2.q0 ≈ 1
                @test dq2.qe ≈ zero(dq2.qe) atol = 1e-6
                dq2 = dq \ dq
                @test dq2.q0 ≈ 1
                @test dq2.qe ≈ zero(dq2.qe) atol = 1e-6
                s = randn()
                @test (dq / s).q0 ≈ dq.q0 / s
                @test (dq / s).qe ≈ dq.qe / s
                d = dual(randn(2)...)
                @test (dq / d).q0 ≈ (dq / dualquat(d)).q0
                @test (dq / d).qe ≈ (dq / dualquat(d)).qe
            end
        end
        @testset "multiplication is associative" begin
            for _ in 1:100
                dq1, dq2, dq3 = rand(DualQuaternionF64, 3)
                dq4 = (dq1 * dq2) * dq3
                dq5 = dq1 * (dq2 * dq3)
                @test dq4.q0 ≈ dq5.q0
                @test dq4.qe ≈ dq5.qe
            end
        end
        @testset "multiplication by dual" begin
            for _ in 1:100
                dq = rand(DualQuaternionF64)
                d = dual(randn(2)...)
                @test (dq * d).q0 ≈ (dq * dualquat(d)).q0
                @test (dq * d).qe ≈ (dq * dualquat(d)).qe
                @test (d * dq).q0 ≈ (dualquat(d) * dq).q0
                @test (d * dq).qe ≈ (dualquat(d) * dq).qe
            end
        end
    end

    @testset "iszero" begin
        @test iszero(dualquat(0))
        @test !iszero(dualquat(1))
        @test !iszero(dualquat(quat(0, 1, 0, 0)))
        @test !iszero(dualquat(quat(0, 0, 1, 0)))
        @test !iszero(dualquat(quat(0, 0, 0, 1)))
        @test !iszero(dualquat(quat(0), quat(1)))
        @test !iszero(dualquat(quat(0), quat(0, 1, 0, 0)))
        @test !iszero(dualquat(quat(0), quat(0, 0, 1, 0)))
        @test !iszero(dualquat(quat(0), quat(0, 0, 0, 1)))
    end

    @testset "isone" begin
        @test isone(dualquat(1))
        @test !isone(dualquat(-1))
        @test !isone(dualquat(quat(0, 1, 0, 0)))
        @test !isone(dualquat(quat(1, 1, 0, 0)))
        @test !isone(dualquat(quat(1, 0, 1, 0)))
        @test !isone(dualquat(quat(1, 0, 0, 1)))
        @test !isone(dualquat(quat(0), quat(1)))
        @test !isone(dualquat(quat(0), quat(0, 1, 0, 0)))
        @test !isone(dualquat(quat(0), quat(0, 0, 1, 0)))
        @test !isone(dualquat(quat(0), quat(0, 0, 0, 1)))
    end

    @testset "^" begin
        @testset "^(::DualQuaternion, ::Real)" begin
            for _ in 1:100
                dq = rand(DualQuaternionF64)
                @test dq^2 === dq * dq
                @test dq^1 === dq
                @test_broken (dq^2.0).q0 ≈ (dq * dq).q0
                @test_broken (dq^2.0).qe ≈ (dq * dq).qe
                @test_broken (dq^1.0).q0 ≈ dq.q0
                @test_broken (dq^1.0).qe ≈ dq.qe
                @test_broken (dq^-1.0).q0 ≈ inv(dq).q0
                @test_broken (dq^-1.0).qe ≈ inv(dq).qe
                for p in (1.3, 7.8, 1.3f0, 7.8f0)
                    @test (dq^p).q0 ≈ exp(p * log(dq)).q0
                    @test (dq^p).qe ≈ exp(p * log(dq)).qe
                end
            end
        end
        @testset "^(::DualQuaternion, ::DualQuaternion)" begin
            for _ in 1:100
                q, p = randn(QuaternionF64, 2)
                @test q^p ≈ exp(p * log(q))
            end
        end
    end

    @testset "non-analytic functions" begin
        unary_funs = [conj, abs, abs2, norm, sign]
        # since every dual quaternion is conjugate to a dual complex number,
        # one can establish correctness as follows:
        @testset for fun in unary_funs
            for _ in 1:100
                dq1, dq2 = rand(DualQuaternionF64, 2)
                c = dual(randn(ComplexF64), randn(ComplexF64))
                dq = dualquat(reim(c)..., reim(zero(c))...)
                p = dualquat(@inferred(fun(dq)))
                @test p.q0 ≈ DualNumbers.value(fun(c))
                @test p.qe ≈ DualNumbers.epsilon(fun(c))
                p2 = dq2 * fun(dq1) * inv(dq2)
                p3 = dualquat(fun(dq2 * dq1 * inv(dq2)))
                @test p2.q0 ≈ p3.q0
                @test p2.qe ≈ p3.qe
            end
        end
    end

    @testset "analytic functions" begin
        unary_funs = [sqrt, inv, exp, log]
        # since every dual quaternion is conjugate to a dual complex number,
        # one can establish correctness as follows:
        @testset for fun in unary_funs
            (fun === log || fun === sqrt) && continue  # log is currently broken
            for _ in 1:100
                dq1, dq2 = rand(DualQuaternionF64, 2)
                c = dual(randn(ComplexF64), randn(ComplexF64))
                dq = dualquat(reim(c)..., reim(zero(c))...)
                p = dualquat(@inferred(fun(dq)))
                @test p.q0 ≈ DualNumbers.value(fun(c))
                @test p.qe ≈ DualNumbers.epsilon(fun(c))
                p2 = dq2 * fun(dq1) * inv(dq2)
                p3 = dualquat(fun(dq2 * dq1 * inv(dq2)))
                @test p2.q0 ≈ p3.q0
                @test p2.qe ≈ p3.qe
            end
        end

        @testset "identities" begin
            for _ in 1:100
                dq = rand(DualQuaternionF64)
                @test (inv(dq) * dq).q0 ≈ one(dq.q0)
                @test (inv(dq) * dq).qe ≈ zero(dq.qe) atol = 1e-6
                @test (dq * inv(dq)).q0 ≈ one(dq.q0)
                @test (dq * inv(dq)).qe ≈ zero(dq.qe) atol = 1e-6
                @test_broken (sqrt(dq) * sqrt(dq)).q0 ≈ dq.q0
                @test_broken (sqrt(dq) * sqrt(dq)).qe ≈ dq.qe
                @test_broken exp(log(dq)).q0 ≈ dq.q0
                @test_broken exp(log(dq)).qe ≈ dq.qe
                @test exp(zero(dq)).q0 ≈ one(dq.q0)
                @test exp(zero(dq)).qe ≈ zero(dq.qe) atol = 1e-6
                @test log(one(dq)).q0 ≈ zero(dq.q0) atol = 1e-6
                @test log(one(dq)).qe ≈ zero(dq.qe) atol = 1e-6
            end
            @test_broken log(zero(DualQuaternionF64)) === dualquat(-Inf)
        end
    end

    @testset "normalize" begin
        for _ in 1:100
            dq = rand(DualQuaternionF64)
            q = dualquat_to_fdquat(dq)
            dqnorm = normalize(dq)
            qnorm = normalize(q)
            dqnorm2 = fdquat_to_dualquat(qnorm)
            @test dqnorm.q0 ≈ dqnorm2.q0
            @test dqnorm.qe ≈ dqnorm2.qe
            @test normalize(dqnorm) === dqnorm
            @test iszero(normalize(zero(dq)))
        end
    end

    @testset "normalizea" begin
        for _ in 1:100
            dq = rand(DualQuaternionF64)
            q = dualquat_to_fdquat(dq)
            dqnorm, dqa = normalizea(dq)
            qnorm, qa = normalizea(q)
            dqnorm2 = fdquat_to_dualquat(qnorm)
            @test dqnorm.q0 ≈ dqnorm2.q0
            @test dqnorm.qe ≈ dqnorm2.qe
            @test DualNumbers.value(dqa) ≈ ForwardDiff.value(qa)
            @test DualNumbers.epsilon(dqa) ≈ ForwardDiff.partials(qa, 1)
        end
    end

    @testset "angle/axis/angleaxis" begin
        dq1 = rand(DualQuaternionF64)
        dq2 = dq1 + (1 - dq1.q0.s)
        for dq in (dq1, dq2)
            t1, p1 = angleaxis(dq)
            t2, v = angleaxis(dualquat_to_fdquat(dq))
            p2 = fdquat_to_dualquat(quat(v))
            @test DualNumbers.value(t1) ≈ ForwardDiff.value(t2)
            @test_broken DualNumbers.epsilon(t1) ≈ ForwardDiff.partials(t2, 1)
            @test p2.q0 ≈ p1.q0
            @test_broken p2.qe ≈ p1.qe
            t3 = angle(dq)
            @test DualNumbers.value(t3) ≈ DualNumbers.value(t1)
            @test DualNumbers.epsilon(t3) ≈ DualNumbers.epsilon(t1)
            p3 = axis(dq)
            @test p3.q0 ≈ p1.q0
            @test p3.qe ≈ p1.qe
        end
    end
end


deal(a::Array{Int,1}) = reverse(a)

cut(a::Array{Int,1}, n::Int) = circshift(a, -n)

function deal(a, n)
    o = order(length(a), n)
    return [a[o[i]+1] for i=1:length(a)]
end

# Much trickier algorithm than it sounds in the question prompt.
# I haven't found a closed-form solution to this.
function order(len, stride)
    o = fill(-1, len)
    pos = -stride
    for i=0:len-1
        pos = (pos + stride) % len
        while o[pos + 1] >= 0
            pos = (pos + 1) % len
        end
        o[pos + 1] = i
    end
    return o
end

const deal_regex = r"deal with increment ([-]?\d+)"
const cut_regex = r"cut ([-]?\d+)"

function shuffle(a::Array{Int,1}, filename::String)
    commands = readlines(filename)
    for command in commands
        deal_match = match(deal_regex, command)
        cut_match = match(cut_regex, command)
        if deal_match !== nothing
            a = deal(a, parse(Int, deal_match[1]))
        elseif cut_match !== nothing
            a = cut(a, parse(Int, cut_match[1]))
        elseif command == "deal into new stack"
            a = deal(a)
        else
            println("Unmatched command: $(command)")
        end
    end
    return a
end

for example in ["example1.txt", "example2.txt", "example3.txt", "example4.txt"]
    println("$(example): $(shuffle(collect(0:9), example))")
end

print("Day 22 Part 1: ")
shuffled = shuffle(collect(0:10006), "input.txt")
println(findfirst(==(2019), shuffled) - 1)
# not 8192

# For part 2 I think it is safe to assume (from previous AoC adventures) that
# the shuffle attracts, but I have no idea how to get to it.
# It cannot be a coincidence that both the factory order and the shuffle count are prime.
# 119315717514047 / 101741582076661 is about 1.1727330662515558

# sum(map(l -> parse(Int,match(r"cut (-?\d+)", l)[1]), filter(l -> match(r"cut", l) != nothing, readlines("input.txt"))))
# -14903

# sum(map(l -> parse(Int,match(r"deal with increment (-?\d+)", l)[1]), filter(l -> match(r"eal with", l) != nothing, readlines("input.txt"))))
# 1628


## R code 6.3
m6.1 <- quap(
    alist(
        height ~ dnorm( mu , sigma ) ,
        mu <- a + bl*leg_left + br*leg_right ,
        a ~ dnorm( 10 , 100 ) ,
        bl ~ dnorm( 2 , 10 ) ,
        br ~ dnorm( 2 , 10 ) ,
        sigma ~ dexp( 1 )
    ) , data=d )

## R code 6.7
m6.2 <- quap(
    alist(
        height ~ dnorm( mu , sigma ) ,
        mu <- a + bl*leg_left,
        a ~ dnorm( 10 , 100 ) ,
        bl ~ dnorm( 2 , 10 ) ,
        sigma ~ dexp( 1 )
    ) , data=d )

## R code 6.9
# kcal.per.g regressed on perc.fat
m6.3 <- quap(
    alist(
        K ~ dnorm( mu , sigma ) ,
        mu <- a + bF*F ,
        a ~ dnorm( 0 , 0.2 ) ,
        bF ~ dnorm( 0 , 0.5 ) ,
        sigma ~ dexp( 1 )
    ) , data=d )

# kcal.per.g regressed on perc.lactose
m6.4 <- quap(
    alist(
        K ~ dnorm( mu , sigma ) ,
        mu <- a + bL*L ,
        a ~ dnorm( 0 , 0.2 ) ,
        bL ~ dnorm( 0 , 0.5 ) ,
        sigma ~ dexp( 1 )
    ) , data=d )

## R code 6.10
m6.5 <- quap(
    alist(
        K ~ dnorm( mu , sigma ) ,
        mu <- a + bF*F + bL*L ,
        a ~ dnorm( 0 , 0.2 ) ,
        bF ~ dnorm( 0 , 0.5 ) ,
        bL ~ dnorm( 0 , 0.5 ) ,
        sigma ~ dexp( 1 )
    ) ,
    data=d )

## R code 6.15
m6.6 <- quap(
    alist(
        h1 ~ dnorm( mu , sigma ),
        mu <- h0*p,
        p ~ dlnorm( 0 , 0.25 ),
        sigma ~ dexp( 1 )
    ), data=d )

## R code 6.16
m6.7 <- quap(
    alist(
        h1 ~ dnorm( mu , sigma ),
        mu <- h0 * p,
        p <- a + bt*treatment + bf*fungus,
        a ~ dlnorm( 0 , 0.2 ) ,
        bt ~ dnorm( 0 , 0.5 ),
        bf ~ dnorm( 0 , 0.5 ),
        sigma ~ dexp( 1 )
    ), data=d )

## R code 6.17
m6.8 <- quap(
    alist(
        h1 ~ dnorm( mu , sigma ),
        mu <- h0 * p,
        p <- a + bt*treatment,
        a ~ dlnorm( 0 , 0.2 ),
        bt ~ dnorm( 0 , 0.5 ),
        sigma ~ dexp( 1 )
    ), data=d )

## R code 6.23
d2$mid <- d2$married + 1
m6.9 <- quap(
    alist(
        happiness ~ dnorm( mu , sigma ),
        mu <- a[mid] + bA*A,
        a[mid] ~ dnorm( 0 , 1 ),
        bA ~ dnorm( 0 , 2 ),
        sigma ~ dexp(1)
    ) , data=d2 )
precis(m6.9,depth=2)

## R code 6.24
m6.10 <- quap(
    alist(
        happiness ~ dnorm( mu , sigma ),
        mu <- a + bA*A,
        a ~ dnorm( 0 , 1 ),
        bA ~ dnorm( 0 , 2 ),
        sigma ~ dexp(1)
    ) , data=d2 )

## R code 6.27
m6.11 <- quap(
    alist(
        C ~ dnorm( mu , sigma ),
        mu <- a + b_PC*P + b_GC*G,
        a ~ dnorm( 0 , 1 ),
        c(b_PC,b_GC) ~ dnorm( 0 , 1 ),
        sigma ~ dexp( 1 )
    ), data=d )

## R code 6.28
m6.12 <- quap(
    alist(
        C ~ dnorm( mu , sigma ),
        mu <- a + b_PC*P + b_GC*G + b_U*U,
        a ~ dnorm( 0 , 1 ),
        c(b_PC,b_GC,b_U) ~ dnorm( 0 , 1 ),
        sigma ~ dexp( 1 )
    ), data=d )

## R code 6.29
library(dagitty)
dag_6.1 <- dagitty( "dag {
    U [unobserved]
    X -> Y
    X <- U <- A -> C -> Y
    U -> B <- C
}")
adjustmentSets( dag_6.1 , exposure="X" , outcome="Y" )

## R code 6.30
library(dagitty)
dag_6.2 <- dagitty( "dag {
    A -> D
    A -> M -> D
    A <- S -> M
    S -> W -> D
}")
adjustmentSets( dag_6.2 , exposure="W" , outcome="D" )


module RI

include(joinpath(@__DIR__, "EiFunction.jl"))
import .EiFunction: expint

export lorentz_lorenz,
	   bruggeman,
       looyenga,
       maxwell,
       monecke,
       gedf,
       gem,
	   sellmeier,
	   cauchy_urbach,
	   drude_lorentz,
	   tauc_lorentz,
	   forouhi_bloomer,
	   lorentz_plasmon,
	   cody_lorentz,
       InfoF

function Info()
    tmp1 = "\n " *
	"\n " *
    "\n Available indices of refraction models:" *
	"\n " *
    "\n     bruggeman(p, n; df=1/3) -> EMA" *
    "\n     looyenga(p, n; df=1/3) -> EMA" *
    "\n     lorentz_lorenz(p, n) -> EMA" *
    "\n     maxwell(p, n; df=1/3) -> EMA" *
    "\n     monecke(p, n; dfm=:spheres, c=0.0) -> EMA" *
    "\n     gedf(x, n) -> EMA" *
    "\n     gem(x, n) -> EMA" *
    "\n     sellmeier(x, λ)" *
    "\n     cauchy_urbach(x, λ)" *
	"\n     drude_lorentz(x, ħω)" *
	"\n     tauc_lorentz(x, ħω)" *
	"\n     forouhi_bloomer(x, ħω)" *
	"\n     lorentz_plasmon(x, ħω)" *
	"\n     cody_lorentz(x, ħω; cme=:dipole)" *
	"\n " *
	"\n     To use any of these functions type: ?RI.function" *
	"\n " *
	"\n 	EMA: Effective medium approximation." *
	"\n "
	return println(tmp1)
end

## Function that checks the input for some EMA.
function _checkInputs_np(p, n)
	length(p) == size(n,2) || throw("The number of proportion values should be
    equal to the number of columns of n: (length(p) == size(n,2).")
    # sum(p) == 1.0 || throw("The sum over the p array should be equal to 1.")
end

function _checkInputs_ndf(n, df)
	length(df) == size(n,2) || throw("The length of depolarisation factors should be
    equal to the number of columns of n: (length(df) == size(n,2).")
end

"""

    Returns the effective index of refraction of a mixture of two liquids.

        neff = RI.lorentz_lorenz(p, n)

			p: proportion (real number, 0<p<1) of material n[:,1].
			n: column-array with the complex indices of refraction of the materials in the
 		       mixture. Each column represents the index of refraction of a material for a
 			   range of wavelengths.

            n: effective refractive index

		Source: https://en.wikipedia.org/wiki/Clausius–Mossotti_relation
				Thin Solid Films 519 (2011) 2994-2997

"""
function lorentz_lorenz(
    p::T0, n::Array{T1,2},
) where {T0<:Real, T1<:ComplexF64}
	ϵ = n.^2
	ϵeff = @. @views (2.0*p*ϵ[:,1] + ϵ[:,1]*ϵ[:,2] - 2.0*p*ϵ[:,2] + 2.0*ϵ[:,2])/(ϵ[:,1] + p*(ϵ[:,2] - ϵ[:,1]) + 2.0)
	return sqrt.(ϵeff)
end

"""

    Calculates the complex refractive index of a porous material using Bruggeman
    model, for nonmagnetic materials.

	Supports two components due to its complexity for a general solution,
    with different types of inclusions' geometries. To input the geometrical
    inclusions choosing the optional parameter, the depolarisation factor, df.
    By default takes df = 1/3.

        neff = RI.bruggeman(p, n; df=1/3)

			p: proportion parameter (real number, 0<p<1), with the proportion of material 1.
			n: column-array with the complex indices of refraction of the materials in the
			   mixture. The first column. n[:,1] has proportion p (inclusions), and n[:,2]
			   proportion 1-p (host material). Each column represents the index of refraction of
			   a material for a range of wavelengths.

               df: depolarisation factor (0.00001<=df<=0.99999, cannot be exactly zero or 1)
			   	  = 1/3 for spheres, default
				  = 1/2 for cylinders
				  = 0.99999 (close to one) for laminar structures with the fields directed parallel
				    to the direction along which layers alternate (stratification direction)
				  = 1/3<=df<=1 for prolate spheroidal shapes
                  = -1 for laminar structures for the fields directed perpendicularly
				   to the direction along which layers alternate (stratification direction)
            neff: effective index of refraction

    Source: Crystallography Reports, 2007, Vol. 52, No. 4, pp. 672-685
			Choy 2016, Effective medium theory: principles and applications
			Physical Review B, Vol 61, No 15, 2000-I, p 10437

"""
function bruggeman(
	p::T0, n::Array{T1,2};
	df=1.0/3.0,
) where {T0<:Real, T1<:ComplexF64}
    L = isequal(df, -1) ? :perpendicular : float(df)
    ϵeff = _bruggeman(float.([p, 1.0-p]), n.^2, L)
    return sqrt.(ϵeff)
end

function _bruggeman(
	p::Array{T0,1}, ϵ::Array{T1,2}, df::T0,
) where {T0<:Float64, T1<:ComplexF64}
	β = @. @views ϵ[:,1]*(p[1]/df - 1.0) + ϵ[:,2]*(p[2]/df - 1.0)
	ϵeff = @. @views 0.5*(β + sqrt(β^2 + 4*ϵ[:,1]*ϵ[:,2]*(1/df - 1.0)))/(1.0/df - 1.0)
	return ϵeff
end

function _bruggeman(
	p::Array{T0,1}, ϵ::Array{T1,2}, df::T2,
) where {T0<:Float64, T1<:ComplexF64, T2<:Symbol}
	ϵeff = @. @views ϵ[:,1]*p[1] + ϵ[:,2]*p[2]
	return ϵeff
end

"""

    Calculates the complex refractive index of the porous material using
    Looyenga-Landau-Lifshitz model, for nonmagnetic and isotropic materials.
    (See also Lichtenecker mixing formula: Stenzel, Optical characterization of
    thin solid films - 2018).

	Supports arbitrary number of components and the inclusions can be set
    through the polarisation factor df.

       neff = RI.looyenga(p, n; df=1.0/3.0)

	   	   p: array with proportion parameters (real number, 0<p<1). p[i] is the proportion of
		      material i respect to the total.
	   	   n: column-array with the complex indices of refraction of the materials in the
		      mixture. The first column. n[:,1] has proportion p[1], n[:,2] proportion p[2]
			  and so on. Each column represents the index of refraction of a material for a
			  range of wavelengths.
			  df: depolarization factors
   		   		 = 1/3 for spheres (default)
				 = 1/2 for cylinders
				 = 1 for laminar structures with the fields directed parallel to the direction
				   along which layers alternate (stratification direction)
				 = -1 for laminar structures with the fields directed perpendicular to the
				   direction along which layers alternate (stratification direction)
   			  The df entered as single values represent the same factors for all components, i.e.,
              there is only one geometrical type of inclusions for all of them.

           neff: effective index of refraction

    Sources: Physical Review B, Vol 61, No 15, 2000-I, p 10437
			 IEEE Transactions on Geoscience and Remote Sensing, Vol. 38, 2000.

"""
function looyenga(
	p::Array{T0,N0}, n::Array{T1,2};
	df=1.0/3.0,
) where {T0<:Real, N0, T1<:ComplexF64}
    p = float.(vec(p))
    _checkInputs_np(p, n)
    ϵeff = _looyenga(p, n.^2, float.(df))
    return sqrt.(ϵeff)
end

function _looyenga(
	p::Vector{T0}, ϵ::Array{T1,2}, df::T0,
) where {T0<:Float64, T1<:ComplexF64}
	ϵeff = zeros(ComplexF64, size(ϵ,1))
	for i in collect(1:size(ϵ,2))
		@. @views ϵeff += ϵ[:,i]^(1.0 - 2.0*df)*p[i]
	end
	@. ϵeff = ϵeff^(1.0/(1.0 - 2.0*df))
	return ϵeff
end

function _looyenga(
	p::Vector{T0}, ϵ::Array{T1,2}, df::Array{Array{T0,1},1},
) where {T0<:Float64, T1<:ComplexF64}
	_checkInputs_ndf(ϵ, df)
	ϵeff = zeros(ComplexF64, size(ϵ,1))
	for i in collect(1:size(ϵ,2)), j in collect(1:length(df))
        @. @views ϵeff += ϵ[:,i]^(1.0 - 2.0*df[i][j])*p[i]/length(df[i])
	end
	@. ϵeff = ϵeff^(1.0/(1.0 - 2.0*df))
	return ϵeff
end

"""

	Calculates the complex refractive index of a porous material using
    Maxwell-Garnett model, for nonmagnetic and isotropic materials.

		neff = RI.maxwell(p, n; df=1.0/3.0)

			p: array with proportion parameters (real number, 0<p<1). p[i] is the proportion of
			   material i respect to the total.
			n: column-array with the complex indices of refraction of the materials in the
			   mixture. The first column. n[:,1] has proportion p[1], and n[:,2] proportion p[2]
			   and so on. NOTICE that the host material is taken as the last column in the array
			   n. Each column represents the index of refraction of a material for a range of
			   wavelengths.
			   df: depolarization factors
				  = 1/3 for spheres (default)
				  = 1/2 for cylinders
				  = 1 for laminar structures with the fields directed parallel to the direction
					along which layers alternate (stratification direction)
				  = -1 for laminar structures with the fields directed perpendicular to the
					direction along which layers alternate (stratification direction)
			   The df factors are the same for all components, i.e., there is only one
			   geometrical type of inclusions for all of them.

			neff: effective index of refraction

	Source: IEEE Transactions on Geoscience and Remote Sensing, Vol. 38, 2000.
			Choy 2016, Effective medium theory: principles and applications
			Physical Review B, Vol 61, No 15, 2000-I, p 10437
			Crystallography Reports, 2007, Vol. 52, No. 4, pp. 672-685

"""
function maxwell(
	p::Array{T0,N0}, n::Array{T1,2};
	df::T0=1.0/3.0,
) where {T0<:Real, N0, T1<:ComplexF64}
	_checkInputs_np(p, n)
	ϵ = n.^2
	ϵeff = zeros(ComplexF64, size(n,1))
	for i in collect(1:size(n,2))
		@. @views ϵeff += p[i]*(ϵ[:,i] - ϵ[:,end])/(ϵ[:,end] + (ϵ[:,i] - ϵ[:,end])*df)
	end
	@. @views ϵeff = ϵ[:,end]*(ϵeff*(1.0 - df) + 1.0)/(1.0 - ϵeff*df)
    return sqrt.(ϵeff)
end

"""

    Calculates the complex refractive index of the porous material using
    Monecke model, for nonmagnetic and isotropic materials.

	Supports two components with different types of inclusions' geometries. To input the
	geometrical inclusions choosing the optional parameter dfm = :spheres or :cylinders.
	The last one are considered to be aligned infinite cylinders whose axes are perpendicular
	to the electric field. By default takes :spheres.

        neff = RI.monecke(p, n; dfm=:spheres, c=0.0)

			p: proportion parameter (real number, 0<p<1), with the proportion of n[:,1].
			n: column-array with the complex indices of refraction of the materials in the
			   mixture. The first column. n[:,1] has proportion p (inclusions), and n[:,2]
			   proportion 1-p (host material). Each column represents the index of refraction
			   of a material for a range of wavelengths
			   dfm: inclusions geometry
                   :spheres, default
                   :cylinders, aligned infinite cylinders whose axes are perpendicular to
				   the electric field
			   c: parameter to define the concentration of the host see source 2, to be used
			   	  with the :cylinders geometry, is = 0.0 by default

			neff: effective index of refraction

    Source: Physical Review B, Vol 61, No 15, 2000-I, p 10437
			Physical Review B, Vol 55, No 11, 1997-I, p 6739

"""
function monecke(
	p::T0, n::Array{T1,2};
	dfm::T2=:spheres, c::T0=0.0,
) where {T0<:Real, T1<:ComplexF64, T2<:Symbol}
    x = float.([p c])
    ϵ = n.^2
    if isequal(dfm, :spheres)
        ϵeff = _monecke_spheres(x, ϵ)
    elseif isequal(dfm, :cylinders)
        ϵeff = _monecke_cylinders(x, ϵ)
    else
        error("The accepted df values are: :spheres or :cylinders.")
    end
    return sqrt.(ϵeff)
end

function _monecke_spheres(
	x::Array{T0,2}, ϵ::Array{T1,2},
) where {T0<:Float64, T1<:ComplexF64}
	p = x[1]
	β = @. @views (ϵ[:,2] - ϵ[:,1])/(2.0*ϵ[:,1] + ϵ[:,2])
	ϵeff = @. @views (ϵ[:,1]*β + 4.0*(1.0 - p)*ϵ[:,1] + 2.0*(1.0 - p)^2*(ϵ[:,2] - ϵ[:,1]))/(β + 1.0 - p)
	return ϵeff
end

function _monecke_cylinders(
	x::Array{T0,2}, ϵ::Array{T1,2},
) where {T0<:Float64, T1<:ComplexF64}
	p, c = x # Physical Review B, Vol 55, No 11, 1997-I, p 6739
	s = @. @views ϵ[:,1]/(ϵ[:,1] - ϵ[:,2])
	s0 = @. 0.5 - (1.0 - p)*0.5 + 1.5*(1.0 - p)^2 - (1.0 - p)^3
	η = @. 0.5*s0*p*(1.0 - p)
	ϵeff = @. @views ϵ[:,1]*(1.0 - (c - η)/s - η/(s - s0))
	return ϵeff
end

"""

    Calculates the complex refractive index of a porous material using a general
    dielectric model, for nonmagnetic and isotropic materials. It considers
    n[:,1] hosted in another material n[:,2].

        neff = RI.gedf(x, n)

        	x: [p, β]
        		p: porosity parameter (real number, 0<p<1), proportion of n[:,1] over total
	            β: either 1 or 3
            n: column-array with the complex indices of refraction of the materials in the
			   mixture. The first column. n[:,1] has proportion p (inclusions), and n[:,2]
			   proportion 1-p (host material). Each column represents the index of refraction
			   of a material for a range of wavelengths.

            neff: effective refractive index

    Source: Appl. Phys. Lett. 108, 102902 (2016); doi: 10.1063/1.4943639.

"""
function gedf(x::AbstractArray{S}, n::Array{T,2}) where {S<:Float64, T<:ComplexF64}
	p, β = x
    ϵ = n.^2
	ϵeff = @. @views ϵ[:,2]*(ϵ[:,1] + (β*ϵ[:,2]) + (β*p*ϵ[:,1]) - (β*p*ϵ[:,2])) / (ϵ[:,1] + (β*ϵ[:,2]) - (p*ϵ[:,1]) + (p*ϵ[:,2]))
    return sqrt.(ϵeff)
end

"""

    Here we used equation (3) and the parameters ϕ, ϕc and tp as fitted. For a
    more detailed analysis and relation to the depolarization factors see the
    reference. It considers n[:,1] hosted in another material n[:,2].

        neff = RI.gem(x, n)

        	x: [ϕ, ϕc, tp]
        		ϕ: porosity parameter (real number, 0<ϕ<1), proportion of n1 over total
        		ϕc: critical volume fraction at which the high-index phase first percolates
        		tp: exponent related both to ϕc and to the shape of the grains
			n: column-array with the complex indices of refraction of the materials in the
			   mixture. The first column. n[:,1] has proportion p (inclusions), and n[:,2]
			   proportion 1-p (host material). Each column represents the index of refraction
			   of a material for a range of wavelengths.

            neff: effective refractive index

    Source: Carbon 40 (2002) 2801–2815.

"""
function gem(x::AbstractArray{S}, n::Array{T,2}) where {S<:Float64, T<:ComplexF64}
	ϕ, ϕc, tp = x
    α = (1-ϕc)/ϕc
    β = 1/tp
	ϵ = (n.^2).^β
	ϵeff = @. @views ((α*ϵ[:,1] - ϕ*ϵ[:,1] - α*ϕ*ϵ[:,1] - ϵ[:,2] + ϕ*ϵ[:,2] + α*ϕ*ϵ[:,2] + sqrt(4.0*α*ϵ[:,1]*ϵ[:,2] + ((α*(ϕ-1) + ϕ)*ϵ[:,1] + ϵ[:,2] - (1.0 + α)*ϕ*ϵ[:,2])^2))/(2.0*α))^tp
    return sqrt.(ϵeff)
end

"""

    Returns the index of refraction using the Sellmeier equation.

        n = RI.sellmeier(x, λ)

            x: Array containing the six values for the Sellmeier equation
               as described in https://en.wikipedia.org/wiki/Sellmeier_equation.
               (x = [B1, B2, B3, C1, C2, C3])
            λ: range of wavelengths in μm

            n: complex index of refraction

    Source: https://en.wikipedia.org/wiki/Sellmeier_equation

	Notice λ in μm.

"""
function sellmeier(x::AbstractArray{T}, λ::AbstractArray{T}) where {T<:Float64}
    λ = λ.^2
	n = @. sqrt(complex(1.0 + (x[1]*λ/(λ - x[4])) + (x[2]*λ/(λ - x[5])) + (x[3]*λ/(λ - x[6]))))
	return n
end

"""

    Returns the index of refraction using the Cauchy-Urbach model:
	n = Aₙ + Bₙ*1e4/λ^2 + Cₙ*1e9/λ^4 + im*α₀*exp((1240.0/λ - E₀)/Eᵤ).

        n = RI.cauchy_urbach(x, λ)

            x: Array containing the six values used in the model.
               (x = [
			   		 Aₙ, Bₙ, Cₙ, # parameters of the real part of the index of refraction
					 α₀, E₀, Eᵤ, # parameters of the imag part of the index of refraction
				]
			   )
            λ: range of wavelengths in nm

            n: complex index of refraction

    Source: E. Krous. Characterization of scandium oxide thin films for use in interference
	        coatings for high-power lasers operating in the near-infrared. Master’s thesis,
			Colorado State University, 2010. Page 60.

"""
function cauchy_urbach(x::AbstractArray{T}, λ::AbstractArray{T}) where {T<:Float64}
	Aₙ, Bₙ, Cₙ, α₀, E₀, Eᵤ = x
	# n = @. Aₙ + Bₙ*1e4/λ^2 + Cₙ*1e9/λ^4 + im*Aₖ*exp(Bₖ + (1240.0/λ/Cₖ))
	# n = @. Aₙ + Bₙ*1e4/λ^2 + Cₙ*1e9/λ^4 + im*Aₖ*exp(Bₖ*(1.0/λ - 1.0/Cₖ))
	n = @. Aₙ + Bₙ*1e4/λ^2 + Cₙ*1e9/λ^4 + im*α₀*exp((1240.0/λ - E₀)/Eᵤ)
	return n
end

"""

    Returns the index of refraction using the Drude-Lorentz oscillator model.

        n = RI.drude_lorentz(x, ħω)

            x: Array of array containing the values used in the model.
               (x = [
                      [ϵinf], # dielectric function at infinite frequency (offset)
					  [f1, ħωp1, ħω01, Γ1], # first oscillator
                      [f2, ħωp2, ħω02, Γ2], # second oscillator
                      [...],  # kth oscillator
                ])
            ħω: range of energies (1240/λ(nm))

            n: complex index of refraction

    Sources: F. Wooten, Optical properties of solids, Academic Press (1972)
			 http://www.horiba.com/fileadmin/uploads/Scientific/Downloads/OpticalSchool_CN/TN/ellipsometer/Lorentz_Dispersion_Model.pdf
             Stenzel, Optical characterization of thin solid films - 2018

"""
function drude_lorentz(
    x::Array{Array{T0,1},1}, ħω::AbstractArray{T1},
) where {T0<:Real, T1<:Real}
    ϵ = zeros(ComplexF64, length(vec(ħω)))
	ϵinf = x[1][1]
    for i = 2:length(x)
        f, ħωₚ, ħω₀, Γ = x[i]
        @. ϵ += (f*ħωₚ^2/(ħω₀^2 - ħω^2 - im*Γ*ħω))
    end
	@. ϵ += ϵinf
	return sqrt.(ϵ)
end

"""

    Returns the index of refraction using the Tauc-Lorentz dispersion model.

        n = RI.tauc_lorentz(x, ħω)

            x: Array of array containing the values used in the model.
               (x = [
			   		  [ϵinf, Eg],
                      [A1, E01, Γ1], # first oscillator
                      [A2, E02, Γ2], # second oscillator
                      [...],  # kth oscillator
                ])
               where A, E0 and Γ are in eV units.
            ħω: range of energies (1240/λ(nm))

            n: complex index of refraction

    Sources: Appl. Phys. Lett. 69 (3), 1996
    		 Appl. Phys. Lett. 69 (14), 1996
             J. Phys.: Condens. Matter 20 015216, 2008 (doi.org/10.1088/0953-8984/20/01/015216)

"""
function tauc_lorentz(
    x::Array{Array{T0,1},1}, ħω::AbstractArray{T1},
) where {T0<:Real, T1<:Real}
	ϵ = zeros(ComplexF64, length(vec(ħω)))
	ϵinf, Eg = x[1]
	for i = 2:length(x)
		A, E₀, C = x[i]
		ħω2 = ħω.^2
		E02 = E₀^2
		Eg2 = Eg^2
		C2 = C^2
		γ2 = sqrt(complex(E02 - C2/2))^2
		α = sqrt(complex(4.0*E02 - C2))
		ζ4 = @. (ħω2 - γ2)^2 + α^2*C2/4
		aL = @. (Eg2 - E02)*ħω2 + Eg2*C2 - E02*(E02 + 3.0*Eg2)
		aA = @. (ħω2 - E02)*(E02 + Eg2) + Eg2*C2
		# ϵf1 = @. A*C*aL/2.0/π/ζ4/α/E₀*log((E02 + Eg2 + α*Eg)/(E02 + Eg2 - α*Eg)) -
		# 		 A*aA/π/ζ4/E₀*(π - atan((2.0*Eg + α)/C) + atan((α - 2.0*Eg)/C)) +
		# 		 2.0*A*E₀*Eg/π/ζ4/α*(ħω2 - γ2)*(π + 2.0*atan(2.0/α/C*(γ2 - Eg2))) -
		# 		 A*E₀*C*(ħω2 + Eg2)/π/ζ4/ħω*log(abs(ħω - Eg)/(ħω + Eg)) +
		# 		 2.0*A*E₀*C*Eg/π/ζ4*log((abs(ħω - Eg)*(ħω + Eg))/sqrt((E02 - Eg2)^2 + Eg2*C2))
		ϵf1 = @. A*C*aL/2.0/π/ζ4/α/E₀*log(complex((E02 + Eg2 + α*Eg)/(E02 + Eg2 - α*Eg))) -
 				 A*aA/π/ζ4/E₀*(π - atan((2.0*Eg + α)/C) + atan((α - 2.0*Eg)/C)) +
 				 2.0*A*E₀*Eg/π/ζ4/α*(ħω2 - γ2)*(π + 2.0*atan(2.0/α/C*(γ2 - Eg2))) -
 				 A*E₀*C*(ħω2 + Eg2)/π/ζ4/ħω*log(abs(ħω - Eg)/(ħω + Eg)) +
 				 2.0*A*E₀*C*Eg/π/ζ4*log(abs(ħω - Eg)*(ħω + Eg)/sqrt(complex((E02 - Eg2)^2 + Eg2*C2)))
		ϵf2 = @. (A*E₀*C*(ħω - Eg)^2)/((ħω2 - E02)^2 + C2*ħω2)/ħω*(ħω > Eg)
		@. ϵ += ϵf1 + im*ϵf2
	end
	@. ϵ += ϵinf
	return sqrt.(ϵ)
end

"""

    Returns the index of refraction using the Forouhi-Bloomer model.
    Example marerials: Ta2O5, HfO2, Si3N4, Al2O3, a-Si, AlN.

        n = RI.forouhi_bloomer(x, ħω)

		x: Array of array containing the values used in the model.
		   (x = [
		   		  [ninf, Egap],
				  [A1, B1, C1], # first oscillator
				  [A2, B2, C2], # second oscillator
				  [...],  # kth oscillator
			])
            where B and Egap are in eV units, and C in eV^2.
            ħω: range of energies (1240/λ(nm))

            n: complex index of refraction

    Source: Physical Review B, 34, 7018 (1986)
			Physical Review B, 38, 1865 (1988)

"""
function forouhi_bloomer(
    x::Array{Array{T0,1},1}, ħω::AbstractArray{T1},
) where {T0<:Real, T1<:Real}
	N = zeros(ComplexF64, length(vec(ħω)))
	ninf, Egap = x[1]
	for i = 2:length(x)
		A, B, C = x[i]
		Q = real(0.5*sqrt(complex(4*C - B^2)))
		B₀ = A/Q*(-(0.5*B^2) + (Egap*B) - Egap^2 + C)
		C₀ = A/Q*(0.5*B*(Egap^2 + C) - (2.0*Egap*C))
		tmp1 = @. ħω^2 - B*ħω + C
		n = @. (B₀*ħω + C₀)/tmp1
		k = @. (A*(ħω - Egap)^2)/tmp1
		@. N += n + im*k
	end
	@. N += ninf
	return N
end

"""

    Returns the index of refraction using the Lorentz oscillator with plasmon damping.

        n = RI.lorentz_plasmon(x, ħω)

			x: Array of array containing the values used in the model.
		   		(x = [
						[ϵinf, ħωp, Γ],
				  		[ħωL1, ħωT1, γ1], # first oscillator
						[ħωL2, ħωT2, γ2], # second oscillator
						[...],  # kth oscillator
				])
               where all the parameters are in eV units, and ϵinf non-dimensional.
            ħω: range of energies (1240./λ(nm))

            n: complex index of refraction

    Sources: Physical Review B, 61, 10437 (2000-I)
			 J. Appl. Phys. 116, 233105 (2014)

"""
function lorentz_plasmon(
	x::Array{Array{T0,1},1}, ħω::AbstractArray{T1},
) where {T0<:Real, T1<:Real}
	ϵ = zeros(ComplexF64, length(vec(ħω)))
	ϵinf, ħωp, Γ = x[1]
	for i = 2:length(x)
		ħωL, ħωT, γ = x[i]
		@. ϵ += (ħω^2 - ħωL^2 + im*γ*ħω)/(ħω^2 - ħωT^2 + im*γ*ħω)
	end
	@. ϵ -= ħωp^2/(ħω^2 + im*Γ*ħω)
	@. ϵ *= ϵinf
	return sqrt.(ϵ)
end

"""

    Returns the index of refraction using the Cody-Lorentz dispersion model.

        n = RI.cody_lorentz(x, ħω; cme=:dipole)

            x: Array of array containing the values used in the model.
               (x = [
			   		  [ϵinf, Eg, Et, Ep, Eu],
                      [A1, E01, Γ1], # first oscillator
                      [A2, E02, Γ2], # second oscillator
                      [...],  # kth oscillator
                ])
               where all the parameters are in eV units.
            ħω: range of energies [eV] (1240./λ(nm))
			cme: There are two versions of Cody-Lorentz as defined by Ferlauto et al.
			 	depending on the constant matrix element (cme): :dipole (default) or
				:momentum. If you choose cme=:dipole, then the G function is calculated
				based on the Cody constant dipole matrix element. If you set cme=:momentum
				then G is calculated based on the Tauc theory with constant momentum matrix
				element.

            n: complex index of refraction

    Sources: Ferlauto et al. J. Appl. Phys., Vol. 92, No. 5, 2002
			 N. Malkova et al. Thin Solid Films 595 (2015) 32-35

	Notes:

	1. This function was not tested much since there is a scarce of biliography with data.

	2. This function uses the expint function (exponential integral function) from the
	   Bridge.jl package developed by @stevenjg (and modified by @mschauer) since it is not
	   yet included in the SpecialFunctions.jl module.
	   For more, check:
	   	https://github.com/stevengj/18S096-iap17/blob/master/pset3/pset3-solutions.ipynb
	   	https://github.com/JuliaMath/SpecialFunctions.jl/issues/19

"""
# 1. The parameter Eu is calculated as described in Malkova et al., and the real part of the
# dielectric function is assumed to be the same as described in Ferlauto, considering that Eu is a contant.
function cody_lorentz(
    x::Array{Array{T0,1},1}, ħω::AbstractArray{T1};
	cme::T2=:dipole,
) where {T0<:Real, T1<:Real, T2<:Symbol}
	isequal(cme, :dipole) && return _cody_lorentz_dipole(float.(x), vec(float.(ħω)))
	isequal(cme, :momentum) && return _cody_lorentz_momentum(float.(x), vec(float.(ħω)))
end

# Lorentz oscillator function _L and variable band edge function _G
_G(ħω::T1, Eg::T1) where {T1<:Float64} = ((ħω - Eg)/ħω)^2
_G(ħω::T1, Eg::T1, Ep::T1) where {T1<:Float64} = (ħω - Eg)^2/((ħω - Eg)^2 + Ep^2)
_L(A::T1, E0::T1, Γ::T1, ħω::T1) where {T1<:Float64} = A*E0*Γ*ħω/((ħω^2 - E0^2)^2 + (Γ*ħω)^2)
_Ld(E0::T1, Γ::T1, ħω::T1) where {T1<:Float64} = (ħω^2 - E0^2)^2 + (Γ*ħω)^2

# Determination of IcL assuming constant dipole matrix element
function _cody_lorentz_dipole(x::Array{Array{T1,1},1}, ħω::Array{T1,1}) where {T1<:Float64}
	ϵinf, Eg, Et, Ep, Eu = x[1]
	G = _G.(ħω, Eg, Ep)
	ϵ = zeros(ComplexF64, length(vec(ħω)))
	Iu = similar(ϵ)
	for i = 2:length(x)
		A, E0, Γ = x[i]
		# Eg<=Et<=E0 || throw("The Cody-Lorentz model requires that Eg<=Et<=E0.")
		D = (E0^2 - Et^4)/((E0^2 - Et^2)^2 + (Γ*Et)^2) + Ep^2*Et/(Et - Eg)/((Et - Eg)^2 + Ep^2)
		Eu = 0.5*Et/D
		L = _L.(A, E0, Γ, ħω)
		E1 = Et*_G.(Et, Eg, Ep)*_L.(A, E0, Γ, Et)
		_urbach_tail!.(Iu, E1, ħω, Eu, Et)
		Icl = _matrix_element_dipole.(A, E0, Eg, Et, Eu, Ep, Γ, ħω, G)
		ϵf1 = @. real(Iu) + real(Icl)
		ϵf2 = @. G*L*(ħω>Et) + E1*exp((ħω - Et)/Eu)/ħω*(ħω<=Et)
		@. ϵf2 *= ϵf2>0.0
		@. ϵ += ϵf1 + im*ϵf2
	end
	@. ϵ += ϵinf
	return sqrt.(ϵ)
end

function _matrix_element_dipole(
	A::T1, E0::T1, Eg::T1, Et::T1, Eu::T1, Ep::T1, Γ::T1, ħω::T1, G::T1,
) where {T1<:Float64}
	I0c = (π/2.0 - atan((Et - Ep)/Ep))/Ep
	c0c = 0.5*ħω*G/_Ld(E0,Γ,ħω)
	d0c = - 0.5*ħω*(ħω + Eg)^2/((ħω + Eg)^2 + Ep^2)/_Ld(E0,Γ,ħω)
	Fsq = Ep^2 + Eg^2
	ζ = E0^2 - Γ^2/2.0
	χ = sqrt(complex(4.0*E0^2 - Γ^2))
	Ksq = 2.0*Fsq + 2.0*ζ - 4.0*Eg^2
	Y4 = E0^4 + Fsq*(Ksq - Fsq) - 4.0*Eg^2*Ksq
	b0c = Y4*Fsq*(_Ld(E0,Γ,ħω)*((c0c - d0c)/ħω + 2.0*Eg*Ksq/Y4*(c0c + d0c)) - 1.0)/
		  ((Ksq - Fsq)*Fsq*Y4 + E0^4*Y4 + 4.0*Eg^2*Fsq*Ksq^2)
	b1c = (2.0*Eg*Ksq*b0c - _Ld(E0,Γ,ħω)*(c0c + d0c))/Y4
	a3c = - (b1c + c0c + d0c)
	a2c = - (b0c + 2.0*Eg*b1c + ħω*(c0c - d0c))
	a1c = - (2.0*Eg*b0c - (Ksq - Fsq)*b1c + (ħω^2 - 2.0*ζ)*(c0c + d0c))
	a0c = 1.0 + (Ksq - Fsq)*b0c + 2.0*Eg*Ksq*b1c - ħω*(ħω^2 - 2.0*ζ)*(c0c - d0c)
	_Icl = 2.0*A*E0*Γ/π*(b1c*(Eg*I0c - sqrt(complex(log((Et - Eg)^2 + Ep^2)))) + b0c*I0c)
	I1t = 0.5/χ/Γ*(π - 2.0*atan(2.0*(Et^2 - ζ)/χ/Γ))
	I0at = 0.5/Γ*(π - atan((2.0*Et + χ)/Γ) + atan((-2.0*Et + χ)/Γ))
	I0bt = 0.25/χ*log((Et^2 + E0^2 + χ*Et)/(Et^2 + E0^2 - χ*Et))
	Itl = 2.0*A*E0*Γ/π*(a3c*(ζ*I1t - log(_Ld(E0,Γ,Et))^(0.25)) +
		  a2c*(I0at + I0bt) + a1c*I1t + a0c*((I0at - I0bt)/E0^2) -
		  c0c*log(abs(ħω - Et)) - d0c*log(ħω + Et))
	Icl = _Icl + Itl
	return Icl
end

# Determination of ItL assuming constant momentum matrix element
function _cody_lorentz_momentum(x::Array{Array{T1,1},1}, ħω::Array{T1,1}) where {T1<:Float64}
	ϵinf, Eg, Et, Ep, Eu = x[1]
	G = _G.(ħω, Eg)
	ϵ = zeros(ComplexF64, length(vec(ħω)))
	Iu = similar(ϵ)
	for i = 2:length(x)
		A, E0, Γ = x[i]
		# Eg<=Et<=E0 || throw("The Cody-Lorentz model requires that Eg<=Et<=E0.")
		D = (E0^2 - Et^4)/((E0^2 - Et^2)^2 + (Γ*Et)^2) + Ep^2*Et/(Et - Eg)/((Et - Eg)^2 + Ep^2)
		Eu = 0.5*Et/D
		L = _L.(A, E0, Γ, ħω)
		E1 = Et*_G.(Et, Eg)*_L.(A, E0, Γ, Et)
		_urbach_tail!.(Iu, E1, ħω, Eu, Et)
		Itl = _matrix_element_momentum.(A, E0, Eg, Et, Eu, Γ, ħω, G)
		ϵf1 = @. real(Iu) + real(Itl)
		ϵf2 = @. G*L*(ħω>Et) + E1*exp((ħω - Et)/Eu)/ħω*(ħω<=Et)
		@. ϵf2 *= ϵf2>0.0
		@. ϵ += ϵf1 + im*ϵf2
	end
	@. ϵ += ϵinf
	return sqrt.(ϵ)
end

function _matrix_element_momentum(
	A::T1, E0::T1, Eg::T1, Et::T1, Eu::T1, Γ::T1, ħω::T1, G::T1,
) where {T1<:Float64}
	ζ = E0^2 - Γ^2/2.0 # I define ζ as ζ squared by removing the sqrt
	χ = sqrt(complex(4.0*E0^2 - Γ^2))
	c0t = 0.5*ħω*G/_Ld(E0,Γ,ħω)
	d0t = - 0.5*(ħω + Eg)^2/ħω/_Ld(E0,Γ,ħω)
	a3t = - (c0t + d0t)
	a2t = - ħω*(c0t - d0t)
	a0t = 1.0 + (ħω^2 - 2.0*ζ)*a2t
	a1t = (ħω^2 - 2.0*ζ)*a3t
	I1t = 0.5/χ/Γ*(π - 2.0*atan(2.0*(Et^2 - ζ)/χ/Γ))
	I0at = 0.5/Γ*(π - atan((2.0*Et + χ)/Γ) + atan((-2.0*Et + χ)/Γ))
	I0bt = 0.25/χ*log((Et^2 + E0^2 + χ*Et)/(Et^2 + E0^2 - χ*Et))
	Itl = 2.0*A*E0*Γ/π*(a3t*(ζ*I1t - log(_Ld(E0,Γ,Et))^(0.25)) +
		  a2t*(I0at + I0bt) + a1t*I1t + a0t*((I0at - I0bt)/E0^2) -
		  c0t*log(abs(ħω - Et)) - d0t*log(ħω + Et))
	return Itl
end

function _urbach_tail!(
		Iu::T0, E1::T1, ħω::T1, Eu::T1, Et::T1,
) where {T0<:ComplexF64, T1<:Float64}
	Iu = E1/π/ħω*(
			exp((ħω - Et)/Eu)*(expint(complex((Et - ħω)/Eu)) - expint(complex(-ħω/Eu))) -
			exp(-(ħω + Et)/Eu)*(expint(complex((Et + ħω)/Eu)) - expint(complex(ħω/Eu)))
	)
	return nothing
end

end # module RI


#=
Copyright (c) 2018-2022 Chris Coey, Lea Kapelevich, and contributors

This Julia package Hypatia.jl is released under the MIT license; see LICENSE
file in the root directory or at https://github.com/chriscoey/Hypatia.jl

compute the capacity of a classical-quantum channel
adapted from https://github.com/hfawzi/cvxquad/blob/master/examples/cq_channel_capacity.m
and listing 1 in "Efficient optimization of the quantum relative entropy" by H. Fawzi and O. Fawzi
=#

struct ClassicalQuantum{T <: Real} <: ExampleInstanceJuMP{T}
    n::Int
    complex::Bool
    use_EF::Bool
end

function build(inst::ClassicalQuantum{T}) where {T <: Float64}
    n = inst.n
    @assert !(inst.complex && inst.use_EF)
    rt2 = sqrt(T(2))
    R = (inst.complex ? Complex{T} : T)
    function hermtr1()
        ρ = randn(R, n, n)
        ρ = ρ * ρ'
        ρ ./= tr(ρ)
        return Hermitian(ρ)
    end
    ρs = [hermtr1() for _ in 1:n]
    Hs = [dot(ρ, log(ρ)) for ρ in ρs]

    model = JuMP.Model()
    JuMP.@variable(model, prob[1:n] >= 0)
    JuMP.@constraint(model, sum(prob) == 1)
    JuMP.@variable(model, epi)
    JuMP.@objective(model, Max, -epi + dot(prob, Hs))

    entr = zeros(JuMP.AffExpr, Cones.svec_length(R, n))
    ρ_vec = zeros(T, length(entr))
    for (ρ, p) in zip(ρs, prob)
        Cones.smat_to_svec!(ρ_vec, ρ, rt2)
        entr += p * ρ_vec
    end

    aff = vcat(epi, 1, entr)
    if inst.use_EF
        add_spectral(MatNegEntropyEigOrd(), n, aff, model)
    else
        JuMP.@constraint(model, aff in Hypatia.EpiPerSepSpectralCone{Float64}(
            Cones.NegEntropySSF(), Cones.MatrixCSqr{T, R}, n))
    end

    # save for use in tests
    model.ext[:epi] = epi
    model.ext[:entr] = entr

    return model
end

function test_extra(inst::ClassicalQuantum{T}, model::JuMP.Model) where T
    stat = JuMP.termination_status(model)
    @test stat == MOI.OPTIMAL
    (stat == MOI.OPTIMAL) || return

    # check constraint
    tol = eps(T)^0.2
    epi_opt = JuMP.value(model.ext[:epi])
    entr_opt = JuMP.value.(model.ext[:entr])
    Entr_opt = zeros(inst.complex ? Complex{T} : T, inst.n, inst.n)
    Cones.svec_to_smat!(Entr_opt, entr_opt, sqrt(T(2)))
    λ = eigvals(Hermitian(Entr_opt, :U))
    @test minimum(λ) >= -tol
    qe_result = get_val(pos_only(λ), MatNegEntropy())
    @test epi_opt ≈ qe_result atol=tol rtol=tol
    return
end


### ----------------------------------------------------------------------------
### ---- Basic testing for the Shifted Characteristic Polynomial Evaluation:
### ----------------------------------------------------------------------------

include("dynamic_symtridiagonal.jl")

using LinearAlgebra, Logging, Plots

"""
    Basic test on the characteristic polynomial for the tridiagonal Matrix.
"""
function Test1(n=50)
    mainDiag = rand(n)
    subDiag = rand(n - 1)
    dynamicT = DynamicSymTridiagonal(mainDiag[1])
    T = GetT(dynamicT)
    for Idx in 1:n - 1
        dynamicT(mainDiag[Idx + 1], subDiag[Idx])
    end
    @info "Characteristic poly without shift evaluated to be: "
    println(CharPolyShifted(dynamicT, 0))
    println(det(T))
    @info "Plot out the characteric polynomial over the min, max eigenvalue. "
    minEigen = eigmin(T); maxEigen = eigmax(T)
    grid = LinRange(minEigen, maxEigen, 1024)
    p = grid .|> (x) -> CharPolyShifted(dynamicT, x)
    p2 = grid .|> (x) -> det(T - x*I)
    fig = plot(grid, sign.(p).*log10.(abs.(p) .+ 1))
    display(plot!(fig, grid, sign.(p2).*log10.(abs.(p2) .+ 1)))

return dynamicT end

# Test1();

function Test2(n = 200)
    mainDiag = rand(n)
    subDiag = rand(n - 1)
    dynamicT = DynamicSymTridiagonal(mainDiag[1])
    T = GetT(dynamicT)
    for Idx in 1:n - 1
        dynamicT(mainDiag[Idx + 1], subDiag[Idx])
    end
    # try locating one of the inner eigenvalue of matrix T.
    @info "Trying out the EigenValueLocate function. "
    eigenVals = eigvals(T)
    sort!(eigenVals)
    P(x) = CharPolyShifted(dynamicT, x)
    locatedEig = EigenValueLocate(
        dynamicT,
        (eigenVals[end - 1] + eigenVals[end])/2,
        Inf)
    println("locatedEig: $locatedEig")
    println("Expected: $(eigenVals[end])")
    error = abs(locatedEig - eigenVals[end])
    println("error: $(error)")
    println("This is the lsit of all eigenvalues: ")
    display(eigenVals)
    println("The corresonding eigenvector is: ")
    LocatedEigenVec = eigvecs(T, [locatedEig])
    if error > 1e-10
        @warn "Error is bigger than expected. "
        # might over probe and cause big problem.
    end

return T, locatedEig, LocatedEigenVec end

# T, locatedEig, locatedEigenVec = Test2()
# display(plot(locatedEigenVec))

function Test3(n=2048)
    mainDiag = -2*ones(n)
    subDiag = ones(n - 1)
    dynamicT = DynamicSymTridiagonal(mainDiag[1])
    ChangeVelocityTol!(dynamicT, 1e-4)
    referenceT = SymTridiagonal(mainDiag, subDiag)
    for Idx in 1:n÷16
        dynamicT(mainDiag[Idx + 1], subDiag[Idx])
        EigenvaluesUpdate(dynamicT)
        if dynamicT.converged_this_step |> length >= 1
            println("step $(Idx) has: $(dynamicT.converged_this_step) converged. ")
        end
    end
    @info "Ritz System: "
    display(dynamicT.thetas)
    display(dynamicT.converged)
    println("Converged $(CountConverged(dynamicT))")
    # Plot the converged that are identified and the correct eigenvalues,
    # curious how good cauchy interlace is.

return end

Test3()


using Test
using ManifoldDiffEq
using Manifolds
using OrdinaryDiffEq: OrdinaryDiffEq, alg_order
using LinearAlgebra
using DiffEqBase

function test_solver_frozen(manifold_to_alg; expected_order = nothing, adaptive = false)
    expected_order !== nothing && @testset "alg_order" begin
        alg = manifold_to_alg(Sphere(2))
        @test alg_order(alg) == expected_order
    end

    @testset "Sphere" begin
        M = Sphere(2)
        A = ManifoldDiffEq.FrozenManifoldDiffEqOperator{Float64}() do u, p, t
            return cross(u, [1.0, 0.0, 0.0])
        end
        u0 = [0.0, 1.0, 0.0]
        alg = manifold_to_alg(M)
        prob = ManifoldDiffEq.ManifoldODEProblem(A, u0, (0, 2.0), M)
        sol1 = if adaptive
            solve(prob, alg)
        else
            solve(prob, alg, dt = 1 / 8)
        end

        @test sol1(0.0) ≈ u0
        @test is_point(M, sol1(1.0))
    end

    @testset "Product manifold" begin
        M = ProductManifold(Sphere(2), Euclidean(3))
        A = ManifoldDiffEq.FrozenManifoldDiffEqOperator{Float64}() do u, p, t
            return ProductRepr(cross(u.parts[1], [1.0, 0.0, 0.0]), u.parts[2])
        end
        u0 = ProductRepr([0.0, 1.0, 0.0], [1.0, 0.0, 0.0])
        alg = manifold_to_alg(M)
        prob = ManifoldDiffEq.ManifoldODEProblem(A, u0, (0, 2.0), M)
        sol1 = if adaptive
            solve(prob, alg)
        else
            solve(prob, alg, dt = 1 / 8)
        end

        @test isapprox(M, sol1(0.0), u0)
        @test is_point(M, sol1(1.0))
    end

end

function test_solver_lie(manifold_to_alg; expected_order = nothing)
    expected_order !== nothing && @testset "alg_order" begin
        action = RotationAction(Euclidean(3), SpecialOrthogonal(3))
        alg = manifold_to_alg(Sphere(2), action)
        @test alg_order(alg) == expected_order
    end

    @testset "Sphere" begin
        M = Sphere(2)
        action = RotationAction(Euclidean(3), SpecialOrthogonal(3))

        A = ManifoldDiffEq.LieManifoldDiffEqOperator{Float64}() do u, p, t
            return hat(SpecialOrthogonal(3), Matrix(I(3)), cross(u, [1.0, 0.0, 0.0]))
        end
        u0 = [0.0, 1.0, 0.0]
        alg = manifold_to_alg(M, action)
        prob = ManifoldDiffEq.ManifoldODEProblem(A, u0, (0, 2.0), M)
        sol1 = solve(prob, alg, dt = 1 / 8)

        @test sol1(0.0) ≈ u0
        @test is_point(M, sol1(1.0))
    end

end

# constructing tableaus for comparison with DiffEq on the Euclidean space
function constructME(T::Type = Float64)
    A = fill(0, 1, 1)
    c = [0]
    α = [1]
    A = map(T, A)
    α = map(T, α)
    c = map(T, c)
    return (DiffEqBase.ExplicitRKTableau(A, c, α, 1))
end

function constructCG2(T::Type = Float64)
    A = [
        0 0
        1//2 0
    ]
    c = [0, 1 // 2]
    α = [0, 1]
    A = map(T, A)
    α = map(T, α)
    c = map(T, c)
    return (DiffEqBase.ExplicitRKTableau(A, c, α, 2))
end

function constructCG3(T::Type = Float64)
    A = [
        0 0 0
        3//4 0 0
        119//216 17/108 0
    ]
    c = [0, 3 // 4, 17 // 24]
    α = [13 // 51, -2 // 3, 24 // 17]
    A = map(T, A)
    α = map(T, α)
    c = map(T, c)
    return (DiffEqBase.ExplicitRKTableau(A, c, α, 3))
end

function constructCG4a(T::Type = Float64)
    c2 = 0.8177227988124852
    c3 = 0.3859740639032449
    c4 = 0.3242290522866937
    c5 = 0.8768903263420429
    a21 = 0.8177227988124852
    a31 = 0.3199876375476427
    a41 = 0.9214417194464946
    a51 = 0.3552358559023322
    a32 = 0.0659864263556022
    a42 = 0.4997857776773573
    a52 = 0.2390958372307326
    a43 = -1.0969984448371582
    a53 = 1.3918565724203246
    a54 = -1.1092979392113465
    b1 = 0.1370831520630755
    b2 = -0.0183698531564020
    b3 = 0.7397813985370780
    b4 = -0.1907142565505889
    b5 = 0.3322195591068374

    A = [
        0 0 0 0 0
        a21 0 0 0 0
        a31 a32 0 0 0
        a41 a42 a43 0 0
        a51 a52 a53 a54 0
    ]
    c = [0, c2, c3, c4, c5]
    α = [b1, b2, b3, b4, b5]
    A = map(T, A)
    α = map(T, α)
    c = map(T, c)
    return (DiffEqBase.ExplicitRKTableau(A, c, α, 4))
end

function constructRKMK4(T::Type = Float64)
    A = [
        0 0 0 0
        1//2 0 0 0
        0 1//2 0 0
        0 0 1 0
    ]
    c = [0, 1 // 2, 1 // 2, 1]
    α = [1 // 6, 1 // 3, 1 // 3, 1 // 6]
    A = map(T, A)
    α = map(T, α)
    c = map(T, c)
    return (DiffEqBase.ExplicitRKTableau(A, c, α, 3))
end

function compare_with_diffeq_frozen(manifold_to_alg, tableau)
    M = Euclidean(2)
    # damped harmonic oscillator d^2u/dt^2 + c du/dt + ku = 0
    k = -0.25
    c = 1
    f(u, p, t) = [-c * u[1] - k * u[2], u[1]]
    A = ManifoldDiffEq.FrozenManifoldDiffEqOperator{Float64}(f)
    u0 = [-1.0, 1.0]
    alg = manifold_to_alg(M)
    tspan = (0, 2.0)
    dt = 1 / 8
    prob_frozen = ManifoldDiffEq.ManifoldODEProblem(A, u0, tspan, M)
    sol_frozen = solve(prob_frozen, alg, dt = dt)

    alg_diffeq = OrdinaryDiffEq.ExplicitRK(tableau)
    prob_diffeq = ODEProblem(A, u0, tspan)
    sol_diffeq = solve(prob_diffeq, alg_diffeq; dt = dt, adaptive = false)

    @test isapprox(sol_frozen.u, sol_diffeq.u)
end

function compare_with_diffeq_lie(manifold_to_alg, tableau)
    M = Euclidean(2)
    # damped harmonic oscillator d^2u/dt^2 + c du/dt + ku = 0
    k = -0.25
    c = 1
    f(u, p, t) = [-c * u[1] - k * u[2], u[1]]
    action = TranslationAction(Euclidean(2), TranslationGroup(2))

    A = ManifoldDiffEq.LieManifoldDiffEqOperator{Float64}(f)
    u0 = [-1.0, 1.0]
    alg = manifold_to_alg(M, action)
    tspan = (0, 2.0)
    dt = 1 / 8
    prob_lie = ManifoldDiffEq.ManifoldODEProblem(A, u0, tspan, M)
    sol_lie = solve(prob_lie, alg, dt = dt)

    alg_diffeq = OrdinaryDiffEq.ExplicitRK(tableau)
    prob_diffeq = ODEProblem(A, u0, tspan)
    sol_diffeq = solve(prob_diffeq, alg_diffeq; dt = dt, adaptive = false)

    @test isapprox(sol_lie.u, sol_diffeq.u)
end

@testset "ManifoldDiffEq" begin
    manifold_to_alg_me = M -> ManifoldDiffEq.ManifoldEuler(M, ExponentialRetraction())
    test_solver_frozen(manifold_to_alg_me; expected_order = 1)
    compare_with_diffeq_frozen(manifold_to_alg_me, constructME())

    manifold_to_alg_cg2 = M -> ManifoldDiffEq.CG2(M, ExponentialRetraction())
    test_solver_frozen(manifold_to_alg_cg2; expected_order = 2)
    compare_with_diffeq_frozen(manifold_to_alg_cg2, constructCG2())

    manifold_to_alg_cg23 = M -> ManifoldDiffEq.CG2_3(M, ExponentialRetraction())
    test_solver_frozen(manifold_to_alg_cg23; expected_order = 2, adaptive = true)

    manifold_to_alg_cg3 = M -> ManifoldDiffEq.CG3(M, ExponentialRetraction())
    test_solver_frozen(manifold_to_alg_cg3; expected_order = 3)
    compare_with_diffeq_frozen(manifold_to_alg_cg3, constructCG3())

    manifold_to_alg_cg4a = M -> ManifoldDiffEq.CG4a(M, ExponentialRetraction())
    test_solver_frozen(manifold_to_alg_cg4a; expected_order = 4)
    compare_with_diffeq_frozen(manifold_to_alg_cg4a, constructCG4a())

    manifold_to_alg_lie_euler =
        (M, action) -> ManifoldDiffEq.ManifoldLieEuler(M, ExponentialRetraction(), action)
    test_solver_lie(manifold_to_alg_lie_euler; expected_order = 1)
    compare_with_diffeq_lie(manifold_to_alg_lie_euler, constructME())

    manifold_to_alg_rkmk4 =
        (M, action) -> ManifoldDiffEq.RKMK4(M, ExponentialRetraction(), action)
    test_solver_lie(manifold_to_alg_rkmk4; expected_order = 4)
    compare_with_diffeq_lie(manifold_to_alg_rkmk4, constructRKMK4())
end


function onedBilinear( kernel, ϕ, test, wg )
#function onedBilinear( kernel:: Array(Float64,1),
#                       ϕ     :: Array(Float64,2),
#                       test  :: Array(Float64,2),
#                       wg    :: Array(Float64,1) )
#= """
  onedBilinear - routine to compute (kernel*ϕ,test) = \int{ kernel*ϕ*test }
                 returns a matrix,
                 rows correspond to different test functions,
                 columns correspond to different local unknowns (in ϕ)

  Author: Jeff Borggaard, Virginia Tech
          part of FEMfunctions.jl

  Licensing:
     This code is distributed under the MIT license.

  Usage:
  ```julia
    M = onedBilinear(kernel, ϕ, test, wg)
  ```

  Arguments:
  - `kernel`: Kernel function in the integral evaluated at the Gauss points
  - `ϕ`: Matrix of element test functions evaluated at the Gauss points
           (dim: n_gauss, n_dof)
  - `test`: Matrix of test functions evaluated at the Gauss points
            (dim: n_gauss, n_test)
  - `wg`: Column vector of Gauss weights

  Output arguments:
  - `M`: the current element's contribution to the system matrix
""" =#

  wk = wg.*kernel

  M = test'*(LinearAlgebra.Diagonal(wk)*ϕ)

  return M

end