"""
    check_interval(f, a::Arf, b::Arf; check_unique = true)

Check if the function `f` has a zero on the interval `[a, b]`.

Returns `(maybe, unique)`. If `maybe` is `false` then `f` is proved to
not have a zero on the interval, if it's `true` then `f` might or
might not have a zero on the interval. If `unique` is `true` then `f`
is guaranteed to have exactly one zero on the interval, if it's
`false` then no information is given.

If `check_unique = false` then don't check for uniqueness, `unique`
will always be `false` in this case.

The function `f` should satisfy the same properties as for
[`isolate_roots`](@ref).

FUTURE WORK:
- Take values on endpoints as arguments to avoid having to compute
  them multiple times.

"""
function check_interval(f, a::Arf, b::Arf; check_unique::Bool = true)
    x = Arb((a, b))
    maybe = Arblib.contains_zero(f(x))

    maybe || return false, false

    check_unique || return true, false

    a_sign = Arblib.sgn_nonzero(f(Arb(a)))
    b_sign = Arblib.sgn_nonzero(f(Arb(b)))

    if a_sign * b_sign < 0
        df = f(ArbSeries((x, 1)))[1]
        return true, !Arblib.contains_zero(df)
    else
        return true, false
    end
end

function check_interval(p::ArbPoly, a::Arf, b::Arf; check_unique::Bool = true)
    x = Arb((a, b))

    check_unique || return Arblib.contains_zero(p(x)), false

    px, dpx = Arblib.evaluate2(p, x)

    Arblib.contains_zero(px) || return false, false

    a_sign = Arblib.sgn_nonzero(p(Arb(a)))
    b_sign = Arblib.sgn_nonzero(p(Arb(b)))

    if a_sign * b_sign < 0
        unique = !Arblib.contains_zero(dpx)
        return true, unique
    elseif a_sign * b_sign > 0
        maybe = Arblib.contains_zero(dpx)
        return maybe, false
    else
        return true, false
    end
end

"""
    isolate_roots(f, a::Arf, b::Arf; depth = 10, check_unique = true, verbose = false)

Isolate the roots of `f` on the interval `[a, b]`.

Returns a tuple `(found, flags)` where `found` is a vector of
subintervals of `[a, b]` given by pairs of `Arf` and `flags` is a
vector of booleans. The output has the following properties:
- The function has no roots on interval outside of the subintervals in
  `found`.
- Subintervals are sorted in increasing order (with no overlap except
  possibly starting and ending with the same point).
- Subintervals with a flag `true` has exactly one (simple) root.
- Subintervals with a flag `false` may or may not contain roots.

If all flags are true then all roots of the function on interval have
been isolated. If there are output subintervals on which the existence
or nonexistence of roots could not be determined, the user may attempt
further searches on those subintervals (possibly with increased
precision and/or increased bounds for the breaking criteria). Note
that roots of multiplicity higher than one and roots located exactly
at endpoints cannot be isolated by the algorithm.

This method closely mimics the function `arb_calc_isolate_roots` in
Arb, both in behaviour and in implementation.

The function `f` should support evaluation on both `Arb` and
`ArbSeries` and should return an enclosure of the result in both
cases. The evaluation on `ArbSeries` is done to compute the derivative
to check uniqueness, this can be skipped by setting `check_unique =
false`, see below. Alternatively `f` can be an `ArbPoly`, in which
case some optimizations are done.

If `check_unique = false` then don't check if roots are unique. In
this case `flags` will always be false and it will simply continue to
split the subintervals until it reaches the maximum depth. The only
benefit to this is that the derivative of `f` doesn't have to be
computed so it can be used when the function of interest doesn't
support evaluating derivatives.

If `verbose = true` then output information after each iteration on
the number of found roots and the number of remaining intervals that
needs to be split further.

FUTURE WORK:
- Parallelize.
- Compute values on endpoints of subintervals once to not have to
  compute them multiple times.
- Should it use `BitVector` or `Vector{Bool}` for flags?
- The current implementation does a breadth-first-search. Consider
  rewriting it to do a depth-first-search instead to reduce memory
  usage in case of many splittings. This might however make it harder
  to parallelize.
"""
function isolate_roots(
    f,
    a::Arf,
    b::Arf;
    depth::Integer = 10,
    check_unique::Bool = true,
    verbose = false,
)
    check_interval(a, b)

    if a == b
        fa = f(Arb(a))
        if Arblib.contains_zero(fa)
            return [(a, b)], BitVector((iszero(fa),))
        else
            return Vector{NTuple{2,Arf}}(), BitVector()
        end
    end

    intervals = [(a, b)]

    found = Vector{NTuple{2,Arf}}()
    flags = BitVector()

    iterations = 0
    while !isempty(intervals) && iterations < depth
        iterations += 1

        next_intervals = empty(intervals)

        for interval in intervals
            maybe, unique = check_interval(f, interval...; check_unique)

            if unique
                push!(found, interval)
                push!(flags, true)
            elseif maybe
                if iterations < depth
                    push!(next_intervals, bisect_interval(interval...)...)
                else
                    # If we are on the last iteration don't split the interval
                    push!(next_intervals, interval)
                end
            end
        end

        intervals = next_intervals

        verbose &&
            iterations < depth &&
            @info "isolate_roots iteration: $(lpad(iterations, 2)), " *
                  "found: $(lpad(length(found), 2)), " *
                  "remaining intervals: $(length(intervals) ÷ 2)"
    end

    verbose &&
        iterations == depth &&
        @info "isolate_roots iteration: $(lpad(iterations, 2)), " *
              "found: $(lpad(length(found), 2)), " *
              "remaining intervals: $(length(intervals))"

    append!(found, intervals)
    append!(flags, zeros(Bool, length(intervals)))

    p = sortperm(found, by = interval -> interval[1])

    return found[p], flags[p]
end


@testset "RationalPoly" begin
    #@test RationalPoly{true, Int, Int}(1) == 1
    #@test typeof(RationalPoly{false, Int, Int}(1)) == RationalPoly{false, Int, Int}
    #@inferred RationalPoly{true, Int, Int}(1)
    Mod.@polyvar x
    @test 1 / x isa RationalPoly
    @test 1.0 / x isa RationalPoly
    @inferred 1 / x
    @inferred 1.0 / x
    @test eltype([2.0x / x^2, (x+x) / (1 + 2x^2)]) <: RationalPoly
    @test 2 * (1/x * (1-x)) + (1/x * x) * (1/x^2 * x^2) - (1-x)/x == (1-x)/x + 1
    @test (1/x + 1/x) / 2 == ((1 / (x^2 - 1) + (x+1)) - (x+1)) * ((x^2 - 1) / x)
    #@test typeof(zero(1/x)) == Term{true, Int}
    @test iszero(zero(1/x))
    @test zero(1/x) == 0
    @test one(1/x) == 1
    #@test typeof(zero(RationalPoly{true, Float64, Int})) == Polynomial{true, Float64}
    #@test iszero(zero(RationalPoly{true, Float64, Int}))
    #@test typeof(x / 2) == Term{true, Float64}
    #@test typeof((x + x^2) / 3.0) == Polynomial{true, Float64}
    @test nothing != x / (x^2 + 1)
    @test (x^2 + 1) / (2x) != nothing
    @test Dict{Int,Int}() != x / (x^2 + 1)
    @test (x^2 + 1) / (2x) != Dict{Int,Int}()
    @test numerator(x / x^2) == x
    @test denominator(x / x^2) == x^2
    @test inv(x / x^2) == x
    @test x / x^2 == inv(x)
    @test isone(((x+1) / (x-1)) / ((x+1) / (x-1)))
    @test ((x+1)^2 / (x-1)) / ((x+1) / (x-1)) == x+1
    poly = (x+1)/(x+2.0)
    RType = typeof(poly)
    @test RType(true) == one(RType)
    @test RType(false) == zero(RType)
    @test one(RType) isa RType
    @test zero(RType) isa RType
    @test one(poly) isa RType
    @test zero(poly) isa RType
end


include("helpers.jl")

function laff_scal(alpha::T, x::T) where T <: Matrix{Number}
    # Como alpha é um escalar
    # apesar de estar representado como uma matriz
    # pegamos apenas o primeiro, e único, elemento da matriz
    x .* alpha[1]

end

## Testar ##
x = Matrix([1,2,3], 1, 3)
alpha = Matrix([0.5], 1, 1)

laff_scal(alpha, x)


# # 2D linear diffusion Julia MPI solver
using Plots, Printf, MAT
import MPI

# enable plotting by default
if !@isdefined do_save; do_save = true end

# MPI functions
@views function update_halo(A, neighbors_x, neighbors_y, comm)
    # Send to / receive from neighbor 1 in dimension x ("left neighbor")
    if neighbors_x[1] != MPI.MPI_PROC_NULL
        sendbuf = A[2,:]
        recvbuf = zeros(size(A[1,:]))
        MPI.Send(sendbuf,  neighbors_x[1], 0, comm)
        MPI.Recv!(recvbuf, neighbors_x[1], 1, comm)
        A[1,:] = recvbuf
    end
    # Send to / receive from neighbor 2 in dimension x("right neighbor")
    if neighbors_x[2] != MPI.MPI_PROC_NULL
        sendbuf = A[end-1,:]
        recvbuf = zeros(size(A[end,:]))
        MPI.Send(sendbuf,  neighbors_x[2], 1, comm)
        MPI.Recv!(recvbuf, neighbors_x[2], 0, comm)
        A[end,:] = recvbuf
    end
    # Send to / receive from neighbor 1 in dimension y ("bottom neighbor")
    if neighbors_y[1] != MPI.MPI_PROC_NULL
        sendbuf = A[:,2]
        recvbuf = zeros(size(A[:,1]))
        MPI.Send(sendbuf,  neighbors_y[1], 2, comm)
        MPI.Recv!(recvbuf, neighbors_y[1], 3, comm)
        A[:,1] = recvbuf
    end
    # Send to / receive from neighbor 2 in dimension y ("top neighbor")
    if neighbors_y[2] != MPI.MPI_PROC_NULL
        sendbuf = A[:,end-1]
        recvbuf = zeros(size(A[:,end]))
        MPI.Send(sendbuf,  neighbors_y[2], 3, comm)
        MPI.Recv!(recvbuf, neighbors_y[2], 2, comm)
        A[:,end] = recvbuf
    end
    return
end

@views function diffusion_2D_mpi(; do_save=false)
    # MPI
    MPI.Init()
    dims        = [0,0]
    comm        = MPI.COMM_WORLD
    nprocs      = MPI.Comm_size(comm)
    MPI.Dims_create!(nprocs, dims)
    comm_cart   = MPI.Cart_create(comm, dims, [0,0], 1)
    me          = MPI.Comm_rank(comm_cart)
    coords      = MPI.Cart_coords(comm_cart)
    neighbors_x = MPI.Cart_shift(comm_cart, 0, 1)
    neighbors_y = MPI.Cart_shift(comm_cart, 1, 1)
    if (me==0) println("nprocs=$(nprocs), dims[1]=$(dims[1]), dims[2]=$(dims[2])") end
    # Physics
    lx, ly     = 10.0, 10.0
    λ          = 1.0
    nt         = 100
    # Numerics
    nx, ny     = 32, 32                             # local number of grid points
    nx_g, ny_g = dims[1]*(nx-2)+2, dims[2]*(ny-2)+2 # global number of grid points
    # Derived numerics
    dx, dy     = lx/nx_g, ly/ny_g                   # global
    dt         = min(dx,dy)^2/λ/4.1
    # Array allocation
    qHx        = zeros(nx-1,ny-2)
    qHy        = zeros(nx-2,ny-1)
    # Initial condition
    x0, y0     = coords[1]*(nx-2)*dx, coords[2]*(ny-2)*dy
    xc         = [x0 + ix*dx - dx/2 - 0.5*lx  for ix=1:nx]
    yc         = [y0 + iy*dy - dy/2 - 0.5*ly  for iy=1:ny]
    H          = exp.(.-xc.^2 .-yc'.^2)
    t_tic = 0.0
    # Time loop
    for it = 1:nt
        if (it==11) t_tic = Base.time() end
        qHx .= .-λ*diff(H[:,2:end-1], dims=1)/dx
        qHy .= .-λ*diff(H[2:end-1,:], dims=2)/dy
        H[2:end-1,2:end-1] .= H[2:end-1,2:end-1] .- dt*(diff(qHx, dims=1)/dx .+ diff(qHy, dims=2)/dy)
        update_halo(H, neighbors_x, neighbors_y, comm_cart)
    end
    t_toc = (Base.time()-t_tic)
    if (me==0) @printf("Time = %1.4e s, T_eff = %1.2f GB/s \n", t_toc, round((2/1e9*nx*ny*sizeof(lx))/(t_toc/(nt-10)), sigdigits=2)) end
    # Save to visualise
    if do_save file = matopen("$(@__DIR__)/H_$(me).mat", "w"); write(file, "H", Array(H)); close(file) end
    MPI.Finalize()
    return
end

diffusion_2D_mpi(; do_save=do_save)


# Michelle Medina
# July 12, 2020
# Utilities for Nodal / Hierarchical Bases

export calcNodal, getMaximum, evaluate_upto

function calcNodal(u::Function, x::Array{T,1})::NodalBasis{T} where {T}
	coefficients = zeros(length(x))
 	for index in CartesianIndices(x)
 		coefficients[index] = u(x[index])
 	end
 	return NodalBasis(coefficients)
end

function getMaximum(values::HierarchicalBasis{T})::Array{T,1} where {T}
	M = []
	for l in 1:maxlevel(values)
		absolute = abs.(values.levels[l].coefficients)
		max = maximum(absolute)
		append!(M,max)
	end
	return M
end

function evaluate_upto(H::HierarchicalBasis{T}, level::Int, x::Float64)::T where {T}
	value = T(0)
	for l in 0:level
		for j in 0:2^l
			value = value + H[l, j]*basis(l, j, x)
		end
	end
	return value
end

function evaluate_upto(H::HierarchicalBasis{T}, level::Int, x::Array{T,1})::Array{T,1} where {T}
	A = zeros(size(x))
	for index in CartesianIndices(x)
		A[index] = evaluate_upto(H, level, x[index])
	end
	return A
end


using Test, YAAD, YAAD.TestUtils

include("math.jl")
include("linalg.jl")
include("cat.jl")
include("array.jl")


onethird(::Type{Float64}) = 0.3333333333333333
onethird(::Type{Double64}) = Double64((0.3333333333333333, 1.850371707708594e-17))

function Base.sqrt(x::Double64)
    x0 = sqrt(Hi(x))
    x1 = (x0 + x/x0) * 0.5
    x2 = (x1 + x/x1) * 0.5
    return x2
end

@inline function fastsqrt(x::Double64)
    x0 = sqrt(Hi(x))
    x1 = (x0 + x/x0) * 0.5
    return x1
end

function Base.cbrt(x::Double64)
    x0 = Double64(cbrt(Hi(x)))
    a =  fastsqrt( ((4*x/x0) - x0^2) / 3)
    x1 = (x0 + a)/2
    return x1
end

function fastcbrt(x::Double64)
    x0 = cbrt(Hi(x))
    x1 = (2*x0 + x/(x0*x0)) / 3.0
    x2 = (2*x1 + x/(x1*x1)) / 3.0
    return x2
end


using Plots

function f(z, C)
    return z * z + C
end

function convergence_judgment(C, inital_z)
    z = inital_z
    i::UInt8 = 0
    while i < 100
        z = f(z, C)
        if abs2(z) > 4
            return i
        end
        i += 1
    end
    return i
end

function generate_grid(N, inital_z)
    grid = Array{UInt8}(undef, N, N)
    points = range(-2, 2, length = N)
    n_pts = length(points)
    Threads.@threads for i in 1:n_pts
        for j in 1:n_pts
            ReC = points[i]
            ImC = points[j]
            C = ComplexF64(ReC, ImC)
            grid[j,i] = convergence_judgment(C, inital_z)
        end
    end
    return grid
end

function main(N)
    inital_z = 0.0
    @time grid = generate_grid(N, inital_z)
    heatmap(1:N, 1:N, grid, aspect_ratio = 1)
end

main(5000)


# Meep Materials Library
# https://github.com/NanoComp/meep/blob/master/python/materials.py
# default unit length is 1 um
um_scale = 1.0

# conversion factor for eV to 1/um [=1/hc]
eV_um_scale = um_scale / 1.23984193

#------------------------------------------------------------------
# crystalline silicon (c-Si) from A. Deinega et al., J. Optical Society of America A, Vol. 28, No. 5, pp. 770-77 (2011)
# based on experimental data for intrinsic silicon at T=300K from M.A. Green and M. Keevers, Progress in Photovoltaics, Vol. 3, pp. 189-92 (1995)
# wavelength range: 0.4 - 1.0 um

cSi_range = FreqRange(min = um_scale, max = um_scale / 0.4)

cSi_frq1 = 3.64 / um_scale
cSi_gam1 = 0
cSi_sig1 = 8
cSi_frq2 = 2.76 / um_scale
cSi_gam2 = 2 * 0.063 / um_scale
cSi_sig2 = 2.85
cSi_frq3 = 1.73 / um_scale
cSi_gam3 = 2 * 2.5 / um_scale
cSi_sig3 = -0.107

cSi_susc = [LorentzianSusceptibility(frequency = cSi_frq1, gamma = cSi_gam1, sigma = cSi_sig1),
    LorentzianSusceptibility(frequency = cSi_frq2, gamma = cSi_gam2, sigma = cSi_sig2),
    LorentzianSusceptibility(frequency = cSi_frq3, gamma = cSi_gam3, sigma = cSi_sig3)]

cSi = Medium(epsilon_model = DispersionModel(inf = 1.0, susceptibilities = cSi_susc, valid_freq_range = cSi_range))

#------------------------------------------------------------------
# amorphous silicon (a-Si) from Horiba Technical Note 08: Lorentz Dispersion Model
# ref: http://www.horiba.com/fileadmin/uploads/Scientific/Downloads/OpticalSchool_CN/TN/ellipsometer/Lorentz_Dispersion_Model.pdf
# wavelength range: 0.21 - 0.83 um

aSi_range = FreqRange(min = um_scale / 0.83, max = um_scale / 0.21)

aSi_frq1 = 1 / (0.315481407124682 * um_scale)
aSi_gam1 = 1 / (0.645751005208333 * um_scale)
aSi_sig1 = 14.571

aSi_susc = [LorentzianSusceptibility(frequency = aSi_frq1, gamma = aSi_gam1, sigma = aSi_sig1)]

aSi = Medium(epsilon_model = DispersionModel(inf = 3.109, susceptibilities = aSi_susc, valid_freq_range = aSi_range))

#------------------------------------------------------------------
# hydrogenated amorphous silicon (a-Si:H) from Horiba Technical Note 08: Lorentz Dispersion Model
# ref: http://www.horiba.com/fileadmin/uploads/Scientific/Downloads/OpticalSchool_CN/TN/ellipsometer/Lorentz_Dispersion_Model.pdf
# wavelength range: 0.21 - 0.83 um

aSi_H_range = FreqRange(min = um_scale / 0.83, max = um_scale / 0.21)

aSi_H_frq1 = 1 / (0.334189199460916 * um_scale)
aSi_H_gam1 = 1 / (0.579365387850467 * um_scale)
aSi_H_sig1 = 12.31

aSi_H_susc = [LorentzianSusceptibility(frequency = aSi_H_frq1, gamma = aSi_H_gam1, sigma = aSi_H_sig1)]

aSi_H = Medium(epsilon_model = DispersionModel(inf = 3.22, susceptibilities = aSi_H_susc, valid_freq_range = aSi_H_range))

#------------------------------------------------------------------
# indium tin oxide (ITO) from Horiba Technical Note 08: Lorentz Dispersion Model
# ref: http://www.horiba.com/fileadmin/uploads/Scientific/Downloads/OpticalSchool_CN/TN/ellipsometer/Lorentz_Dispersion_Model.pdf
# wavelength range: 0.21 - 0.83 um

ITO_range = FreqRange(min = um_scale / 0.83, max = um_scale / 0.21)

ITO_frq1 = 1 / (0.182329695588235 * um_scale)
ITO_gam1 = 1 / (1.94637665620094 * um_scale)
ITO_sig1 = 2.5

ITO_susc = [LorentzianSusceptibility(frequency = ITO_frq1, gamma = ITO_gam1, sigma = ITO_sig1)]

ITO = Medium(epsilon_model = DispersionModel(inf = 1.0, susceptibilities = ITO_susc, valid_freq_range = ITO_range))

#------------------------------------------------------------------
# alumina (Al2O3) from Horiba Technical Note 08: Lorentz Dispersion Model
# ref: http://www.horiba.com/fileadmin/uploads/Scientific/Downloads/OpticalSchool_CN/TN/ellipsometer/Lorentz_Dispersion_Model.pdf
# wavelength range: 0.21 - 2.07 um

Al2O3_range = FreqRange(min = um_scale / 2.07, max = um_scale / 0.21)

Al2O3_frq1 = 1 / (0.101476668030774 * um_scale)
Al2O3_gam1 = 0
Al2O3_sig1 = 1.52

Al2O3_susc = [LorentzianSusceptibility(frequency = Al2O3_frq1, gamma = Al2O3_gam1, sigma = Al2O3_sig1)]

Al2O3 = Medium(epsilon_model = DispersionModel(inf = 1.0, susceptibilities = Al2O3_susc, valid_freq_range = Al2O3_range))

#------------------------------------------------------------------
# aluminum nitride (AlN) from Horiba Technical Note 08: Lorentz Dispersion Model
# ref: http://www.horiba.com/fileadmin/uploads/Scientific/Downloads/OpticalSchool_CN/TN/ellipsometer/Lorentz_Dispersion_Model.pdf
# wavelength range: 0.26 - 1.65 um

AlN_range = FreqRange(min = um_scale / 1.65, max = um_scale / 0.26)

AlN_frq1 = 1 / (0.139058089950651 * um_scale)
AlN_gam1 = 0
AlN_sig1 = 3.306

AlN_susc = [LorentzianSusceptibility(frequency = AlN_frq1, gamma = AlN_gam1, sigma = AlN_sig1)]

AlN = Medium(epsilon_model = DispersionModel(inf = 1.0, susceptibilities = AlN_susc, valid_freq_range = AlN_range))

#------------------------------------------------------------------
# aluminum arsenide (AlAs) from R.E. Fern and A. Onton, J. Applied Physics, Vol. 42, pp. 3499-500, 1971
# ref: https://refractiveindex.info/?shelf=main&book=AlAs&page=Fern
# wavelength range: 0.56 - 2.2 um

AlAs_range = FreqRange(min = um_scale / 2.2, max = um_scale / 0.56)

AlAs_frq1 = 1 / (0.2822 * um_scale)
AlAs_gam1 = 0
AlAs_sig1 = 6.0840
AlAs_frq2 = 1 / (27.62 * um_scale)
AlAs_gam2 = 0
AlAs_sig2 = 1.900

AlAs_susc = [LorentzianSusceptibility(frequency = AlAs_frq1, gamma = AlAs_gam1, sigma = AlAs_sig1),
    LorentzianSusceptibility(frequency = AlAs_frq2, gamma = AlAs_gam2, sigma = AlAs_sig2)]

AlAs = Medium(epsilon_model = DispersionModel(inf = 2.0792, susceptibilities = AlAs_susc, valid_freq_range = AlAs_range))

#------------------------------------------------------------------
# borosilicate glass (BK7) from SCHOTT Zemax catalog 2017-01-20b
# ref: https://refractiveindex.info/?shelf=glass&book=BK7&page=SCHOTT
# wavelength range: 0.3 - 2.5 um

BK7_range = FreqRange(min = um_scale / 2.5, max = um_scale / 0.3)

BK7_frq1 = 1 / (0.07746417668832478 * um_scale)
BK7_gam1 = 0
BK7_sig1 = 1.03961212
BK7_frq2 = 1 / (0.14148467902921502 * um_scale)
BK7_gam2 = 0
BK7_sig2 = 0.231792344
BK7_frq3 = 1 / (10.176475470417055 * um_scale)
BK7_gam3 = 0
BK7_sig3 = 1.01046945

BK7_susc = [LorentzianSusceptibility(frequency = BK7_frq1, gamma = BK7_gam1, sigma = BK7_sig1),
    LorentzianSusceptibility(frequency = BK7_frq2, gamma = BK7_gam2, sigma = BK7_sig2),
    LorentzianSusceptibility(frequency = BK7_frq3, gamma = BK7_gam3, sigma = BK7_sig3)]

BK7 = Medium(epsilon_model = DispersionModel(inf = 1.0, susceptibilities = BK7_susc, valid_freq_range = BK7_range))

#------------------------------------------------------------------
# fused quartz (silica) from I.H. Malitson, J. Optical Society of America, Vol. 55, pp. 1205-9, 1965
# ref: https://refractiveindex.info/?shelf=glass&book=fused_silica&page=Malitson
# wavelength range: 0.21 - 6.7 um

fused_quartz_range = FreqRange(min = um_scale / 6.7, max = um_scale / 0.21)

fused_quartz_frq1 = 1 / (0.0684043 * um_scale)
fused_quartz_gam1 = 0
fused_quartz_sig1 = 0.696166300
fused_quartz_frq2 = 1 / (0.1162414 * um_scale)
fused_quartz_gam2 = 0
fused_quartz_sig2 = 0.407942600
fused_quartz_frq3 = 1 / (9.896161 * um_scale)
fused_quartz_gam3 = 0
fused_quartz_sig3 = 0.897479400

fused_quartz_susc = [LorentzianSusceptibility(frequency = fused_quartz_frq1, gamma = fused_quartz_gam1, sigma = fused_quartz_sig1),
    LorentzianSusceptibility(frequency = fused_quartz_frq2, gamma = fused_quartz_gam2, sigma = fused_quartz_sig2),
    LorentzianSusceptibility(frequency = fused_quartz_frq3, gamma = fused_quartz_gam3, sigma = fused_quartz_sig3)]

fused_quartz = Medium(epsilon_model = DispersionModel(inf = 1.0, susceptibilities = fused_quartz_susc, valid_freq_range = fused_quartz_range))

#------------------------------------------------------------------
# gallium arsenide (GaAs) from T. Skauli et al., J. Applied Physics, Vol. 94, pp. 6447-55, 2003
# ref: https://refractiveindex.info/?shelf=main&book=GaAs&page=Skauli
# wavelength range: 0.97 - 17 um

GaAs_range = FreqRange(min = um_scale / 17, max = um_scale / 0.97)

GaAs_frq1 = 1 / (0.4431307 * um_scale)
GaAs_gam1 = 0
GaAs_sig1 = 5.466742
GaAs_frq2 = 1 / (0.8746453 * um_scale)
GaAs_gam2 = 0
GaAs_sig2 = 0.02429960
GaAs_frq3 = 1 / (36.9166 * um_scale)
GaAs_gam3 = 0
GaAs_sig3 = 1.957522

GaAs_susc = [LorentzianSusceptibility(frequency = GaAs_frq1, gamma = GaAs_gam1, sigma = GaAs_sig1),
    LorentzianSusceptibility(frequency = GaAs_frq2, gamma = GaAs_gam2, sigma = GaAs_sig2),
    LorentzianSusceptibility(frequency = GaAs_frq3, gamma = GaAs_gam3, sigma = GaAs_sig3)]

GaAs = Medium(epsilon_model = DispersionModel(inf = 5.372514, susceptibilities = GaAs_susc, valid_freq_range = GaAs_range))

#------------------------------------------------------------------
# silicon nitride (Si3N4) from H. R. Philipp, J. Electrochemical Society 120, 295-300, 1973
# ref: https://refractiveindex.info/?shelf=main&book=Si3N4&page=Philipp
# wavelength range: 0.207 - 1.24 um

Si3N4_VISNIR_range = FreqRange(min = um_scale / 1.24, max = um_scale / 0.207)

Si3N4_VISNIR_frq1 = 1 / (0.13967 * um_scale)
Si3N4_VISNIR_gam1 = 0
Si3N4_VISNIR_sig1 = 2.8939

Si3N4_VISNIR_susc = [LorentzianSusceptibility(frequency = Si3N4_VISNIR_frq1, gamma = Si3N4_VISNIR_gam1, sigma = Si3N4_VISNIR_sig1)]

Si3N4_VISNIR = Medium(epsilon_model = DispersionModel(inf = 1.0, susceptibilities = Si3N4_VISNIR_susc, valid_freq_range = Si3N4_VISNIR_range))

#------------------------------------------------------------------
# silicon nitride (Si3N4) from K. Luke, et. al., Optics Letters, Vol. 40, pp. 4823-26, 2015
# ref: https://refractiveindex.info/?shelf=main&book=Si3N4&page=Luke
# wavelength range: 0.310 - 5.504 um

Si3N4_NIR_range = FreqRange(min = um_scale / 5.504, max = um_scale / 0.310)

Si3N4_NIR_frq1 = 1 / (0.1353406 * um_scale)
Si3N4_NIR_gam1 = 0
Si3N4_NIR_sig1 = 3.0249
Si3N4_NIR_frq2 = 1 / (1239.842 * um_scale)
Si3N4_NIR_gam2 = 0
Si3N4_NIR_sig2 = 40314

Si3N4_NIR_susc = [LorentzianSusceptibility(frequency = Si3N4_NIR_frq1, gamma = Si3N4_NIR_gam1, sigma = Si3N4_NIR_sig1),
    LorentzianSusceptibility(frequency = Si3N4_NIR_frq2, gamma = Si3N4_NIR_gam2, sigma = Si3N4_NIR_sig2)]

Si3N4_NIR = Medium(epsilon_model = DispersionModel(inf = 1.0, susceptibilities = Si3N4_NIR_susc, valid_freq_range = Si3N4_NIR_range))

#------------------------------------------------------------------
# elemental metals from A.D. Rakic et al., Applied Optics, Vol. 37, No. 22, pp. 5271-83 (1998)
# wavelength range: 0.2 - 12.4 um

metal_range = FreqRange(min = um_scale / 12.398, max = um_scale / 0.24797)

# silver (Ag)

Ag_plasma_frq = 9.01 * eV_um_scale
Ag_f0 = 0.845
Ag_frq0 = 1e-10
Ag_gam0 = 0.048 * eV_um_scale
Ag_sig0 = Ag_f0 * Ag_plasma_frq^2 / Ag_frq0^2
Ag_f1 = 0.065
Ag_frq1 = 0.816 * eV_um_scale      # 1.519 um
Ag_gam1 = 3.886 * eV_um_scale
Ag_sig1 = Ag_f1 * Ag_plasma_frq^2 / Ag_frq1^2
Ag_f2 = 0.124
Ag_frq2 = 4.481 * eV_um_scale      # 0.273 um
Ag_gam2 = 0.452 * eV_um_scale
Ag_sig2 = Ag_f2 * Ag_plasma_frq^2 / Ag_frq2^2
Ag_f3 = 0.011
Ag_frq3 = 8.185 * eV_um_scale      # 0.152 um
Ag_gam3 = 0.065 * eV_um_scale
Ag_sig3 = Ag_f3 * Ag_plasma_frq^2 / Ag_frq3^2
Ag_f4 = 0.840
Ag_frq4 = 9.083 * eV_um_scale      # 0.137 um
Ag_gam4 = 0.916 * eV_um_scale
Ag_sig4 = Ag_f4 * Ag_plasma_frq^2 / Ag_frq4^2
Ag_f5 = 5.646
Ag_frq5 = 20.29 * eV_um_scale      # 0.061 um
Ag_gam5 = 2.419 * eV_um_scale
Ag_sig5 = Ag_f5 * Ag_plasma_frq^2 / Ag_frq5^2

Ag_susc = [DrudeSusceptibility(frequency = Ag_frq0, gamma = Ag_gam0, sigma = Ag_sig0),
    LorentzianSusceptibility(frequency = Ag_frq1, gamma = Ag_gam1, sigma = Ag_sig1),
    LorentzianSusceptibility(frequency = Ag_frq2, gamma = Ag_gam2, sigma = Ag_sig2),
    LorentzianSusceptibility(frequency = Ag_frq3, gamma = Ag_gam3, sigma = Ag_sig3),
    LorentzianSusceptibility(frequency = Ag_frq4, gamma = Ag_gam4, sigma = Ag_sig4),
    LorentzianSusceptibility(frequency = Ag_frq5, gamma = Ag_gam5, sigma = Ag_sig5)]

Ag = Medium(epsilon_model = DispersionModel(inf = 1.0, susceptibilities = Ag_susc, valid_freq_range = metal_range))

#------------------------------------------------------------------
# gold (Au)

metal_range = FreqRange(min = um_scale / 6.1992, max = um_scale / 0.24797)

Au_plasma_frq = 9.03 * eV_um_scale
Au_f0 = 0.760
Au_frq0 = 1e-10
Au_gam0 = 0.053 * eV_um_scale
Au_sig0 = Au_f0 * Au_plasma_frq^2 / Au_frq0^2
Au_f1 = 0.024
Au_frq1 = 0.415 * eV_um_scale      # 2.988 um
Au_gam1 = 0.241 * eV_um_scale
Au_sig1 = Au_f1 * Au_plasma_frq^2 / Au_frq1^2
Au_f2 = 0.010
Au_frq2 = 0.830 * eV_um_scale      # 1.494 um
Au_gam2 = 0.345 * eV_um_scale
Au_sig2 = Au_f2 * Au_plasma_frq^2 / Au_frq2^2
Au_f3 = 0.071
Au_frq3 = 2.969 * eV_um_scale      # 0.418 um
Au_gam3 = 0.870 * eV_um_scale
Au_sig3 = Au_f3 * Au_plasma_frq^2 / Au_frq3^2
Au_f4 = 0.601
Au_frq4 = 4.304 * eV_um_scale      # 0.288 um
Au_gam4 = 2.494 * eV_um_scale
Au_sig4 = Au_f4 * Au_plasma_frq^2 / Au_frq4^2
Au_f5 = 4.384
Au_frq5 = 13.32 * eV_um_scale      # 0.093 um
Au_gam5 = 2.214 * eV_um_scale
Au_sig5 = Au_f5 * Au_plasma_frq^2 / Au_frq5^2

Au_susc = [DrudeSusceptibility(frequency = Au_frq0, gamma = Au_gam0, sigma = Au_sig0),
    LorentzianSusceptibility(frequency = Au_frq1, gamma = Au_gam1, sigma = Au_sig1),
    LorentzianSusceptibility(frequency = Au_frq2, gamma = Au_gam2, sigma = Au_sig2),
    LorentzianSusceptibility(frequency = Au_frq3, gamma = Au_gam3, sigma = Au_sig3),
    LorentzianSusceptibility(frequency = Au_frq4, gamma = Au_gam4, sigma = Au_sig4),
    LorentzianSusceptibility(frequency = Au_frq5, gamma = Au_gam5, sigma = Au_sig5)]

Au = Medium(epsilon_model = DispersionModel(inf = 1.0, susceptibilities = Au_susc, valid_freq_range = metal_range))

#------------------------------------------------------------------
# copper (Cu)

metal_range = FreqRange(min = um_scale / 12.398, max = um_scale / 0.20664)

Cu_plasma_frq = 10.83 * eV_um_scale
Cu_f0 = 0.575
Cu_frq0 = 1e-10
Cu_gam0 = 0.030 * eV_um_scale
Cu_sig0 = Cu_f0 * Cu_plasma_frq^2 / Cu_frq0^2
Cu_f1 = 0.061
Cu_frq1 = 0.291 * eV_um_scale      # 4.261 um
Cu_gam1 = 0.378 * eV_um_scale
Cu_sig1 = Cu_f1 * Cu_plasma_frq^2 / Cu_frq1^2
Cu_f2 = 0.104
Cu_frq2 = 2.957 * eV_um_scale      # 0.419 um
Cu_gam2 = 1.056 * eV_um_scale
Cu_sig2 = Cu_f2 * Cu_plasma_frq^2 / Cu_frq2^2
Cu_f3 = 0.723
Cu_frq3 = 5.300 * eV_um_scale      # 0.234 um
Cu_gam3 = 3.213 * eV_um_scale
Cu_sig3 = Cu_f3 * Cu_plasma_frq^2 / Cu_frq3^2
Cu_f4 = 0.638
Cu_frq4 = 11.18 * eV_um_scale      # 0.111 um
Cu_gam4 = 4.305 * eV_um_scale
Cu_sig4 = Cu_f4 * Cu_plasma_frq^2 / Cu_frq4^2

Cu_susc = [DrudeSusceptibility(frequency = Cu_frq0, gamma = Cu_gam0, sigma = Cu_sig0),
    LorentzianSusceptibility(frequency = Cu_frq1, gamma = Cu_gam1, sigma = Cu_sig1),
    LorentzianSusceptibility(frequency = Cu_frq2, gamma = Cu_gam2, sigma = Cu_sig2),
    LorentzianSusceptibility(frequency = Cu_frq3, gamma = Cu_gam3, sigma = Cu_sig3),
    LorentzianSusceptibility(frequency = Cu_frq4, gamma = Cu_gam4, sigma = Cu_sig4)]

Cu = Medium(epsilon_model = DispersionModel(inf = 1.0, susceptibilities = Cu_susc, valid_freq_range = metal_range))

#------------------------------------------------------------------
# aluminum (Al)

Al_plasma_frq = 14.98 * eV_um_scale
Al_f0 = 0.523
Al_frq0 = 1e-10
Al_gam0 = 0.047 * eV_um_scale
Al_sig0 = Al_f0 * Al_plasma_frq^2 / Al_frq0^2
Al_f1 = 0.227
Al_frq1 = 0.162 * eV_um_scale      # 7.654 um
Al_gam1 = 0.333 * eV_um_scale
Al_sig1 = Al_f1 * Al_plasma_frq^2 / Al_frq1^2
Al_f2 = 0.050
Al_frq2 = 1.544 * eV_um_scale      # 0.803 um
Al_gam2 = 0.312 * eV_um_scale
Al_sig2 = Al_f2 * Al_plasma_frq^2 / Al_frq2^2
Al_f3 = 0.166
Al_frq3 = 1.808 * eV_um_scale      # 0.686 um
Al_gam3 = 1.351 * eV_um_scale
Al_sig3 = Al_f3 * Al_plasma_frq^2 / Al_frq3^2
Al_f4 = 0.030
Al_frq4 = 3.473 * eV_um_scale      # 0.357 um
Al_gam4 = 3.382 * eV_um_scale
Al_sig4 = Al_f4 * Al_plasma_frq^2 / Al_frq4^2

Al_susc = [DrudeSusceptibility(frequency = Al_frq0, gamma = Al_gam0, sigma = Al_sig0),
    LorentzianSusceptibility(frequency = Al_frq1, gamma = Al_gam1, sigma = Al_sig1),
    LorentzianSusceptibility(frequency = Al_frq2, gamma = Al_gam2, sigma = Al_sig2),
    LorentzianSusceptibility(frequency = Al_frq3, gamma = Al_gam3, sigma = Al_sig3),
    LorentzianSusceptibility(frequency = Al_frq4, gamma = Al_gam4, sigma = Al_sig4)]

Al = Medium(epsilon_model = DispersionModel(inf = 1.0, susceptibilities = Al_susc, valid_freq_range = metal_range))

#------------------------------------------------------------------
# beryllium (Be)

Be_plasma_frq = 18.51 * eV_um_scale
Be_f0 = 0.084
Be_frq0 = 1e-10
Be_gam0 = 0.035 * eV_um_scale
Be_sig0 = Be_f0 * Be_plasma_frq^2 / Be_frq0^2
Be_f1 = 0.031
Be_frq1 = 0.100 * eV_um_scale     # 12.398 um
Be_gam1 = 1.664 * eV_um_scale
Be_sig1 = Be_f1 * Be_plasma_frq^2 / Be_frq1^2
Be_f2 = 0.140
Be_frq2 = 1.032 * eV_um_scale      # 1.201 um
Be_gam2 = 3.395 * eV_um_scale
Be_sig2 = Be_f2 * Be_plasma_frq^2 / Be_frq2^2
Be_f3 = 0.530
Be_frq3 = 3.183 * eV_um_scale      # 0.390 um
Be_gam3 = 4.454 * eV_um_scale
Be_sig3 = Be_f3 * Be_plasma_frq^2 / Be_frq3^2
Be_f4 = 0.130
Be_frq4 = 4.604 * eV_um_scale      # 0.269 um
Be_gam4 = 1.802 * eV_um_scale
Be_sig4 = Be_f4 * Be_plasma_frq^2 / Be_frq4^2

Be_susc = [DrudeSusceptibility(frequency = Be_frq0, gamma = Be_gam0, sigma = Be_sig0),
    LorentzianSusceptibility(frequency = Be_frq1, gamma = Be_gam1, sigma = Be_sig1),
    LorentzianSusceptibility(frequency = Be_frq2, gamma = Be_gam2, sigma = Be_sig2),
    LorentzianSusceptibility(frequency = Be_frq3, gamma = Be_gam3, sigma = Be_sig3),
    LorentzianSusceptibility(frequency = Be_frq4, gamma = Be_gam4, sigma = Be_sig4)]

Be = Medium(epsilon_model = DispersionModel(inf = 1.0, susceptibilities = Be_susc, valid_freq_range = metal_range))

#------------------------------------------------------------------
# chromium (Cr)

Cr_plasma_frq = 10.75 * eV_um_scale
Cr_f0 = 0.168
Cr_frq0 = 1e-10
Cr_gam0 = 0.047 * eV_um_scale
Cr_sig0 = Cr_f0 * Cr_plasma_frq^2 / Cr_frq0^2
Cr_f1 = 0.151
Cr_frq1 = 0.121 * eV_um_scale     # 10.247 um
Cr_gam1 = 3.175 * eV_um_scale
Cr_sig1 = Cr_f1 * Cr_plasma_frq^2 / Cr_frq1^2
Cr_f2 = 0.150
Cr_frq2 = 0.543 * eV_um_scale      # 2.283 um
Cr_gam2 = 1.305 * eV_um_scale
Cr_sig2 = Cr_f2 * Cr_plasma_frq^2 / Cr_frq2^2
Cr_f3 = 1.149
Cr_frq3 = 1.970 * eV_um_scale      # 0.629 um
Cr_gam3 = 2.676 * eV_um_scale
Cr_sig3 = Cr_f3 * Cr_plasma_frq^2 / Cr_frq3^2
Cr_f4 = 0.825
Cr_frq4 = 8.775 * eV_um_scale      # 0.141 um
Cr_gam4 = 1.335 * eV_um_scale
Cr_sig4 = Cr_f4 * Cr_plasma_frq^2 / Cr_frq4^2

Cr_susc = [DrudeSusceptibility(frequency = Cr_frq0, gamma = Cr_gam0, sigma = Cr_sig0),
    LorentzianSusceptibility(frequency = Cr_frq1, gamma = Cr_gam1, sigma = Cr_sig1),
    LorentzianSusceptibility(frequency = Cr_frq2, gamma = Cr_gam2, sigma = Cr_sig2),
    LorentzianSusceptibility(frequency = Cr_frq3, gamma = Cr_gam3, sigma = Cr_sig3),
    LorentzianSusceptibility(frequency = Cr_frq4, gamma = Cr_gam4, sigma = Cr_sig4)]

Cr = Medium(epsilon_model = DispersionModel(inf = 1.0, susceptibilities = Cr_susc, valid_freq_range = metal_range))

#------------------------------------------------------------------
# nickel (Ni)

Ni_plasma_frq = 15.92 * eV_um_scale
Ni_f0 = 0.096
Ni_frq0 = 1e-10
Ni_gam0 = 0.048 * eV_um_scale
Ni_sig0 = Ni_f0 * Ni_plasma_frq^2 / Ni_frq0^2
Ni_f1 = 0.100
Ni_frq1 = 0.174 * eV_um_scale      # 7.126 um
Ni_gam1 = 4.511 * eV_um_scale
Ni_sig1 = Ni_f1 * Ni_plasma_frq^2 / Ni_frq1^2
Ni_f2 = 0.135
Ni_frq2 = 0.582 * eV_um_scale      # 2.130 um
Ni_gam2 = 1.334 * eV_um_scale
Ni_sig2 = Ni_f2 * Ni_plasma_frq^2 / Ni_frq2^2
Ni_f3 = 0.106
Ni_frq3 = 1.597 * eV_um_scale      # 0.776 um
Ni_gam3 = 2.178 * eV_um_scale
Ni_sig3 = Ni_f3 * Ni_plasma_frq^2 / Ni_frq3^2
Ni_f4 = 0.729
Ni_frq4 = 6.089 * eV_um_scale      # 0.204 um
Ni_gam4 = 6.292 * eV_um_scale
Ni_sig4 = Ni_f4 * Ni_plasma_frq^2 / Ni_frq4^2

Ni_susc = [DrudeSusceptibility(frequency = Ni_frq0, gamma = Ni_gam0, sigma = Ni_sig0),
    LorentzianSusceptibility(frequency = Ni_frq1, gamma = Ni_gam1, sigma = Ni_sig1),
    LorentzianSusceptibility(frequency = Ni_frq2, gamma = Ni_gam2, sigma = Ni_sig2),
    LorentzianSusceptibility(frequency = Ni_frq3, gamma = Ni_gam3, sigma = Ni_sig3),
    LorentzianSusceptibility(frequency = Ni_frq4, gamma = Ni_gam4, sigma = Ni_sig4)]

Ni = Medium(epsilon_model = DispersionModel(inf = 1.0, susceptibilities = Ni_susc, valid_freq_range = metal_range))

#------------------------------------------------------------------
# palladium (Pd)

Pd_plasma_frq = 9.72 * eV_um_scale
Pd_f0 = 0.330
Pd_frq0 = 1e-10
Pd_gam0 = 0.008 * eV_um_scale
Pd_sig0 = Pd_f0 * Pd_plasma_frq^2 / Pd_frq0^2
Pd_f1 = 0.649
Pd_frq1 = 0.336 * eV_um_scale      # 3.690 um
Pd_gam1 = 2.950 * eV_um_scale
Pd_sig1 = Pd_f1 * Pd_plasma_frq^2 / Pd_frq1^2
Pd_f2 = 0.121
Pd_frq2 = 0.501 * eV_um_scale      # 2.475 um
Pd_gam2 = 0.555 * eV_um_scale
Pd_sig2 = Pd_f2 * Pd_plasma_frq^2 / Pd_frq2^2
Pd_f3 = 0.638
Pd_frq3 = 1.659 * eV_um_scale      # 0.747 um
Pd_gam3 = 4.621 * eV_um_scale
Pd_sig3 = Pd_f3 * Pd_plasma_frq^2 / Pd_frq3^2
Pd_f4 = 0.453
Pd_frq4 = 5.715 * eV_um_scale      # 0.217 um
Pd_gam4 = 3.236 * eV_um_scale
Pd_sig4 = Pd_f4 * Pd_plasma_frq^2 / Pd_frq4^2

Pd_susc = [DrudeSusceptibility(frequency = Pd_frq0, gamma = Pd_gam0, sigma = Pd_sig0),
    LorentzianSusceptibility(frequency = Pd_frq1, gamma = Pd_gam1, sigma = Pd_sig1),
    LorentzianSusceptibility(frequency = Pd_frq2, gamma = Pd_gam2, sigma = Pd_sig2),
    LorentzianSusceptibility(frequency = Pd_frq3, gamma = Pd_gam3, sigma = Pd_sig3),
    LorentzianSusceptibility(frequency = Pd_frq4, gamma = Pd_gam4, sigma = Pd_sig4)]

Pd = Medium(epsilon_model = DispersionModel(inf = 1.0, susceptibilities = Pd_susc, valid_freq_range = metal_range))

#------------------------------------------------------------------
# platinum (Pt)

Pt_plasma_frq = 9.59 * eV_um_scale
Pt_f0 = 0.333
Pt_frq0 = 1e-10
Pt_gam0 = 0.080 * eV_um_scale
Pt_sig0 = Pt_f0 * Pt_plasma_frq^2 / Pt_frq0^2
Pt_f1 = 0.191
Pt_frq1 = 0.780 * eV_um_scale      # 1.590 um
Pt_gam1 = 0.517 * eV_um_scale
Pt_sig1 = Pt_f1 * Pt_plasma_frq^2 / Pt_frq1^2
Pt_f2 = 0.659
Pt_frq2 = 1.314 * eV_um_scale      # 0.944 um
Pt_gam2 = 1.838 * eV_um_scale
Pt_sig2 = Pt_f2 * Pt_plasma_frq^2 / Pt_frq2^2
Pt_f3 = 0.547
Pt_frq3 = 3.141 * eV_um_scale      # 0.395 um
Pt_gam3 = 3.668 * eV_um_scale
Pt_sig3 = Pt_f3 * Pt_plasma_frq^2 / Pt_frq3^2
Pt_f4 = 3.576
Pt_frq4 = 9.249 * eV_um_scale      # 0.134 um
Pt_gam4 = 8.517 * eV_um_scale
Pt_sig4 = Pt_f4 * Pt_plasma_frq^2 / Pt_frq4^2

Pt_susc = [DrudeSusceptibility(frequency = Pt_frq0, gamma = Pt_gam0, sigma = Pt_sig0),
    LorentzianSusceptibility(frequency = Pt_frq1, gamma = Pt_gam1, sigma = Pt_sig1),
    LorentzianSusceptibility(frequency = Pt_frq2, gamma = Pt_gam2, sigma = Pt_sig2),
    LorentzianSusceptibility(frequency = Pt_frq3, gamma = Pt_gam3, sigma = Pt_sig3),
    LorentzianSusceptibility(frequency = Pt_frq4, gamma = Pt_gam4, sigma = Pt_sig4)]

Pt = Medium(epsilon_model = DispersionModel(inf = 1.0, susceptibilities = Pt_susc, valid_freq_range = metal_range))

#------------------------------------------------------------------
# titanium (Ti)

Ti_plasma_frq = 7.29 * eV_um_scale
Ti_f0 = 0.148
Ti_frq0 = 1e-10
Ti_gam0 = 0.082 * eV_um_scale
Ti_sig0 = Ti_f0 * Ti_plasma_frq^2 / Ti_frq0^2
Ti_f1 = 0.899
Ti_frq1 = 0.777 * eV_um_scale      # 1.596 um
Ti_gam1 = 2.276 * eV_um_scale
Ti_sig1 = Ti_f1 * Ti_plasma_frq^2 / Ti_frq1^2
Ti_f2 = 0.393
Ti_frq2 = 1.545 * eV_um_scale      # 0.802 um
Ti_gam2 = 2.518 * eV_um_scale
Ti_sig2 = Ti_f2 * Ti_plasma_frq^2 / Ti_frq2^2
Ti_f3 = 0.187
Ti_frq3 = 2.509 * eV_um_scale      # 0.494 um
Ti_gam3 = 1.663 * eV_um_scale
Ti_sig3 = Ti_f3 * Ti_plasma_frq^2 / Ti_frq3^2
Ti_f4 = 0.001
Ti_frq4 = 19.43 * eV_um_scale      # 0.064 um
Ti_gam4 = 1.762 * eV_um_scale
Ti_sig4 = Ti_f4 * Ti_plasma_frq^2 / Ti_frq4^2

Ti_susc = [DrudeSusceptibility(frequency = Ti_frq0, gamma = Ti_gam0, sigma = Ti_sig0),
    LorentzianSusceptibility(frequency = Ti_frq1, gamma = Ti_gam1, sigma = Ti_sig1),
    LorentzianSusceptibility(frequency = Ti_frq2, gamma = Ti_gam2, sigma = Ti_sig2),
    LorentzianSusceptibility(frequency = Ti_frq3, gamma = Ti_gam3, sigma = Ti_sig3),
    LorentzianSusceptibility(frequency = Ti_frq4, gamma = Ti_gam4, sigma = Ti_sig4)]

Ti = Medium(epsilon_model = DispersionModel(inf = 1.0, susceptibilities = Ti_susc, valid_freq_range = metal_range))

#------------------------------------------------------------------
# tungsten (W)

W_plasma_frq = 13.22 * eV_um_scale
W_f0 = 0.206
W_frq0 = 1e-10
W_gam0 = 0.064 * eV_um_scale
W_sig0 = W_f0 * W_plasma_frq^2 / W_frq0^2
W_f1 = 0.054
W_frq1 = 1.004 * eV_um_scale      # 1.235 um
W_gam1 = 0.530 * eV_um_scale
W_sig1 = W_f1 * W_plasma_frq^2 / W_frq1^2
W_f2 = 0.166
W_frq2 = 1.917 * eV_um_scale      # 0.647 um
W_gam2 = 1.281 * eV_um_scale
W_sig2 = W_f2 * W_plasma_frq^2 / W_frq2^2
W_f3 = 0.706
W_frq3 = 3.580 * eV_um_scale      # 0.346 um
W_gam3 = 3.332 * eV_um_scale
W_sig3 = W_f3 * W_plasma_frq^2 / W_frq3^2
W_f4 = 2.590
W_frq4 = 7.498 * eV_um_scale      # 0.165 um
W_gam4 = 5.836 * eV_um_scale
W_sig4 = W_f4 * W_plasma_frq^2 / W_frq4^2

W_susc = [DrudeSusceptibility(frequency = W_frq0, gamma = W_gam0, sigma = W_sig0),
    LorentzianSusceptibility(frequency = W_frq1, gamma = W_gam1, sigma = W_sig1),
    LorentzianSusceptibility(frequency = W_frq2, gamma = W_gam2, sigma = W_sig2),
    LorentzianSusceptibility(frequency = W_frq3, gamma = W_gam3, sigma = W_sig3),
    LorentzianSusceptibility(frequency = W_frq4, gamma = W_gam4, sigma = W_sig4)]

W = Medium(epsilon_model = DispersionModel(inf = 1.0, susceptibilities = W_susc, valid_freq_range = metal_range))

#------------------------------------------------------------------
# metals from D. Barchiesi and T. Grosges, J. Nanophotonics, Vol. 8, 08996, 2015
# wavelength range: 0.4 - 0.8 um

metal_visible_range = FreqRange(min = um_scale / 0.8, max = um_scale / 0.4)

# gold (Au)
# fit to P.B. Johnson and R.W. Christy, Physical Review B, Vol. 6, pp. 4370-9, 1972

Au_JC_visible_frq0 = 1 / (0.139779231751333 * um_scale)
Au_JC_visible_gam0 = 1 / (26.1269913352870 * um_scale)
Au_JC_visible_sig0 = 1

Au_JC_visible_frq1 = 1 / (0.404064525036786 * um_scale)
Au_JC_visible_gam1 = 1 / (1.12834046202759 * um_scale)
Au_JC_visible_sig1 = 2.07118534879440

Au_JC_visible_susc = [DrudeSusceptibility(frequency = Au_JC_visible_frq0, gamma = Au_JC_visible_gam0, sigma = Au_JC_visible_sig0),
    LorentzianSusceptibility(frequency = Au_JC_visible_frq1, gamma = Au_JC_visible_gam1, sigma = Au_JC_visible_sig1)]

Au_JC_visible = Medium(epsilon_model = DispersionModel(inf = 6.1599, susceptibilities = Au_JC_visible_susc, valid_freq_range = metal_visible_range))

#------------------------------------------------------------------
# gold (Au)
# fit to E.D. Palik, Handbook of Optical Constants, Academic Press, 1985

Au_visible_frq0 = 1 / (0.0473629248511456 * um_scale)
Au_visible_gam0 = 1 / (0.255476199605166 * um_scale)
Au_visible_sig0 = 1

Au_visible_frq1 = 1 / (0.800619321082804 * um_scale)
Au_visible_gam1 = 1 / (0.381870287531951 * um_scale)
Au_visible_sig1 = -169.060953137985

Au_visible_susc = [DrudeSusceptibility(frequency = Au_visible_frq0, gamma = Au_visible_gam0, sigma = Au_visible_sig0),
    LorentzianSusceptibility(frequency = Au_visible_frq1, gamma = Au_visible_gam1, sigma = Au_visible_sig1)]

Au_visible = Medium(epsilon_model = DispersionModel(inf = 0.6888, susceptibilities = Au_visible_susc, valid_freq_range = metal_visible_range))

#------------------------------------------------------------------
## WARNING: unstable; field divergence may occur

# silver (Au)
# fit to E.D. Palik, Handbook of Optical Constants, Academic Press, 1985

Ag_visible_frq0 = 1 / (0.142050162130618 * um_scale)
Ag_visible_gam0 = 1 / (18.0357292925015 * um_scale)
Ag_visible_sig0 = 1

Ag_visible_frq1 = 1 / (0.115692151792108 * um_scale)
Ag_visible_gam1 = 1 / (0.257794324096575 * um_scale)
Ag_visible_sig1 = 3.74465275944019

Ag_visible_susc = [DrudeSusceptibility(frequency = Ag_visible_frq0, gamma = Ag_visible_gam0, sigma = Ag_visible_sig0),
    LorentzianSusceptibility(frequency = Ag_visible_frq1, gamma = Ag_visible_gam1, sigma = Ag_visible_sig1)]

Ag_visible = Medium(epsilon_model = DispersionModel(inf = 0.0067526, susceptibilities = Ag_visible_susc, valid_freq_range = metal_visible_range))

#------------------------------------------------------------------
## WARNING: unstable; field divergence may occur

# aluminum (Al)
# fit to E.D. Palik, Handbook of Optical Constants, Academic Press, 1985

Al_visible_frq0 = 1 / (0.0625841659042985 * um_scale)
Al_visible_gam0 = 1 / (0.606007002962666 * um_scale)
Al_visible_sig0 = 1

Al_visible_frq1 = 1 / (0.528191199577075 * um_scale)
Al_visible_gam1 = 1 / (0.291862527666814 * um_scale)
Al_visible_sig1 = -44.4456675577921

Al_visible_susc = [DrudeSusceptibility(frequency = Al_visible_frq0, gamma = Al_visible_gam0, sigma = Al_visible_sig0),
    LorentzianSusceptibility(frequency = Al_visible_frq1, gamma = Al_visible_gam1, sigma = Al_visible_sig1)]

Al_visible = Medium(epsilon_model = DispersionModel(inf = 0.13313, susceptibilities = Al_visible_susc, valid_freq_range = metal_visible_range))

#------------------------------------------------------------------
# chroimium (Cr)
# fit to E.D. Palik, Handbook of Optical Constants, Academic Press, 1985

Cr_visible_frq0 = 1 / (0.118410119507342 * um_scale)
Cr_visible_gam0 = 1 / (0.628596264869804 * um_scale)
Cr_visible_sig0 = 1

Cr_visible_frq1 = 1 / (0.565709598452496 * um_scale)
Cr_visible_gam1 = 1 / (0.731117670900812 * um_scale)
Cr_visible_sig1 = 13.2912419951294

Cr_visible_susc = [DrudeSusceptibility(frequency = Cr_visible_frq0, gamma = Cr_visible_gam0, sigma = Cr_visible_sig0),
    LorentzianSusceptibility(frequency = Cr_visible_frq1, gamma = Cr_visible_gam1, sigma = Cr_visible_sig1)]

Cr_visible = Medium(epsilon_model = DispersionModel(inf = 2.7767, susceptibilities = Cr_visible_susc, valid_freq_range = metal_visible_range))

#------------------------------------------------------------------
## WARNING: unstable; field divergence may occur

# titanium (Ti)
# fit to E.D. Palik, Handbook of Optical Constants, Academic Press, 1985

Ti_visible_frq0 = 1 / (0.101331651921602 * um_scale)
Ti_visible_gam0 = 1 / (0.365743382258719 * um_scale)
Ti_visible_sig0 = 1

Ti_visible_frq1 = 1 / (4.56839173979216e-09 * um_scale)
Ti_visible_gam1 = 1 / (5.86441957443603e-10 * um_scale)
Ti_visible_sig1 = 54742662.1963414

Ti_visible_susc = [DrudeSusceptibility(frequency = Ti_visible_frq0, gamma = Ti_visible_gam0, sigma = Ti_visible_sig0),
    LorentzianSusceptibility(frequency = Ti_visible_frq1, gamma = Ti_visible_gam1, sigma = Ti_visible_sig1)]

Ti_visible = Medium(epsilon_model = DispersionModel(inf = -5.4742e7, susceptibilities = Ti_visible_susc, valid_freq_range = metal_visible_range))

#------------------------------------------------------------------
# aluminum (Al) from Horiba Technical Note 09: Drude Dispersion Model
# ref: http://www.horiba.com/fileadmin/uploads/Scientific/Downloads/OpticalSchool_CN/TN/ellipsometer/Drude_Dispersion_Model.pdf
# wavelength range: 0.19 - 0.83 um

Al_drude_range = FreqRange(min = um_scale / 0.83, max = um_scale / 0.19)

Al_drude_frq = 1 / (0.0789607648707171 * um_scale)
Al_drude_gam = 1 / (1.78138208333333 * um_scale)
Al_drude_sig = 1

Al_drude_susc = [DrudeSusceptibility(frequency = Al_drude_frq, gamma = Al_drude_gam, sigma = Al_drude_sig)]

Al_drude = Medium(epsilon_model = DispersionModel(inf = 1.0, susceptibilities = Al_drude_susc, valid_freq_range = Al_drude_range))

#------------------------------------------------------------------
# cobalt (Co) from Horiba Technical Note 09: Drude Dispersion Model
# ref: http://www.horiba.com/fileadmin/uploads/Scientific/Downloads/OpticalSchool_CN/TN/ellipsometer/Drude_Dispersion_Model.pdf
# wavelength range: 0.26 - 1.65 um

Co_range = FreqRange(min = um_scale / 1.65, max = um_scale / 0.26)

Co_frq = 1 / (0.0789607648707171 * um_scale)
Co_gam = 1 / (0.213802712536644 * um_scale)
Co_sig = 1

Co_susc = [DrudeSusceptibility(frequency = Co_frq, gamma = Co_gam, sigma = Co_sig)]

Co = Medium(epsilon_model = DispersionModel(inf = 3.694, susceptibilities = Co_susc, valid_freq_range = Co_range))

#------------------------------------------------------------------
## WARNING: unstable; field divergence may occur

# molybdenum (Mo) from Horiba Technical Note 09: Drude Dispersion Model
# ref: http://www.horiba.com/fileadmin/uploads/Scientific/Downloads/OpticalSchool_CN/TN/ellipsometer/Drude_Dispersion_Model.pdf
# wavelength range: 0.25 - 0.83 um

Mo_range = FreqRange(min = um_scale / 0.83, max = um_scale / 0.25)

Mo_frq = 1 / (0.0620790071099539 * um_scale)
Mo_gam = 1 / (0.148359690080172 * um_scale)
Mo_sig = 1

Mo_susc = [DrudeSusceptibility(frequency = Mo_frq, gamma = Mo_gam, sigma = Mo_sig)]

Mo = Medium(epsilon_model = DispersionModel(inf = -1.366, susceptibilities = Mo_susc, valid_freq_range = Mo_range))

#------------------------------------------------------------------
# nickel chrome (NiCr) from Horiba Technical Note 09: Drude Dispersion Model
# ref: http://www.horiba.com/fileadmin/uploads/Scientific/Downloads/OpticalSchool_CN/TN/ellipsometer/Drude_Dispersion_Model.pdf
# wavelength range: 0.25 - 0.83 um

NiCr_range = FreqRange(min = um_scale / 0.83, max = um_scale / 0.25)

NiCr_frq = 1 / (0.0868845080588648 * um_scale)
NiCr_gam = 1 / (0.308418390547264 * um_scale)
NiCr_sig = 1

NiCr_susc = [DrudeSusceptibility(frequency = NiCr_frq, gamma = NiCr_gam, sigma = NiCr_sig)]

NiCr = Medium(epsilon_model = DispersionModel(inf = 1.0, susceptibilities = NiCr_susc, valid_freq_range = NiCr_range))

#------------------------------------------------------------------
# nickel iron (NiFe) from Horiba Technical Note 09: Drude Dispersion Model
# ref: http://www.horiba.com/fileadmin/uploads/Scientific/Downloads/OpticalSchool_CN/TN/ellipsometer/Drude_Dispersion_Model.pdf
# wavelength range: 0.25 - 0.83 um

NiFe_range = FreqRange(min = um_scale / 0.83, max = um_scale / 0.25)

NiFe_frq = 1 / (0.0838297450980392 * um_scale)
NiFe_gam = 1 / (0.259381156903766 * um_scale)
NiFe_sig = 1

NiFe_susc = [DrudeSusceptibility(frequency = NiFe_frq, gamma = NiFe_gam, sigma = NiFe_sig)]

NiFe = Medium(epsilon_model = DispersionModel(inf = 1.0, susceptibilities = NiFe_susc, valid_freq_range = NiFe_range))

#------------------------------------------------------------------
# titanium (Ti) from Horiba Technical Note 09: Drude Dispersion Model
# ref: http://www.horiba.com/fileadmin/uploads/Scientific/Downloads/OpticalSchool_CN/TN/ellipsometer/Drude_Dispersion_Model.pdf
# wavelength range: 0.21 - 1.24 um

Ti_drude_range = FreqRange(min = um_scale / 1.24, max = um_scale / 0.21)

Ti_drude_frq = 1 / (0.113746966055046 * um_scale)
Ti_drude_gam = 1 / (0.490056098814229 * um_scale)
Ti_drude_sig = 1

Ti_drude_susc = [DrudeSusceptibility(frequency = Ti_drude_frq, gamma = Ti_drude_gam, sigma = Ti_drude_sig)]

Ti_drude = Medium(epsilon_model = DispersionModel(inf = 1.0, susceptibilities = Ti_drude_susc, valid_freq_range = Ti_drude_range))

#------------------------------------------------------------------
# silicon nitride (SiN), non-stoichiometric, from Horiba Technical Note 08: Lorentz Dispersion Model
# ref: http://www.horiba.com/fileadmin/uploads/Scientific/Downloads/OpticalSchool_CN/TN/ellipsometer/Lorentz_Dispersion_Model.pdf
# wavelength range: 0.21 - 2.07 um

SiN_range = FreqRange(min = um_scale / 2.07, max = um_scale / 0.21)

SiN_frq1 = 1 / (0.190891752117013 * um_scale)
SiN_gam1 = 1 / (3.11518072864322 * um_scale)
SiN_sig1 = 1.2650

SiN_susc = [LorentzianSusceptibility(frequency = SiN_frq1, gamma = SiN_gam1, sigma = SiN_sig1)]

SiN = Medium(epsilon_model = DispersionModel(inf = 2.320, susceptibilities = SiN_susc, valid_freq_range = SiN_range))

#------------------------------------------------------------------
# silicon nitride (Si3N4), stoichiometric, from Horiba Technical Note 08: Lorentz Dispersion Model
# ref: http://www.horiba.com/fileadmin/uploads/Scientific/Downloads/OpticalSchool_CN/TN/ellipsometer/Lorentz_Dispersion_Model.pdf
# wavelength range: 0.23 - 0.83 um

Si3N4_range = FreqRange(min = um_scale / 0.83, max = um_scale / 0.23)

Si3N4_frq1 = 1 / (0.389153148148148 * um_scale)
Si3N4_gam1 = 1 / (0.693811936205932 * um_scale)
Si3N4_sig1 = 4.377

Si3N4_susc = [LorentzianSusceptibility(frequency = Si3N4_frq1, gamma = Si3N4_gam1, sigma = Si3N4_sig1)]

Si3N4 = Medium(epsilon_model = DispersionModel(inf = 1.0, susceptibilities = Si3N4_susc, valid_freq_range = Si3N4_range))

#------------------------------------------------------------------
# silicon dioxide (SiO2) from Horiba Technical Note 08: Lorentz Dispersion Model
# ref: http://www.horiba.com/fileadmin/uploads/Scientific/Downloads/OpticalSchool_CN/TN/ellipsometer/Lorentz_Dispersion_Model.pdf
# wavelength range: 0.25 - 1.77 um

SiO2_range = FreqRange(min = um_scale / 1.77, max = um_scale / 0.25)

SiO2_frq1 = 1 / (0.103320160833333 * um_scale)
SiO2_gam1 = 1 / (12.3984193000000 * um_scale)
SiO2_sig1 = 1.12

SiO2_susc = [LorentzianSusceptibility(frequency = SiO2_frq1, gamma = SiO2_gam1, sigma = SiO2_sig1)]

SiO2 = Medium(epsilon_model = DispersionModel(inf = 1.0, susceptibilities = SiO2_susc, valid_freq_range = SiO2_range))

#------------------------------------------------------------------
# indium phosphide (InP) from Handbook of Optics, 2nd edition, Vol. 2, McGraw-Hill, 1994
# ref: https://refractiveindex.info/?shelf=main&book=InP&page=Pettit
# wavelength range: 0.95 - 10 um

InP_range = FreqRange(min = um_scale / 10, max = um_scale / 0.95)

InP_frq1 = 1 / (0.6263 * um_scale)
InP_gam1 = 0
InP_sig1 = 2.316
InP_frq2 = 1 / (32.935 * um_scale)
InP_gam2 = 0
InP_sig2 = 2.765

InP_susc = [LorentzianSusceptibility(frequency = InP_frq1, gamma = InP_gam1, sigma = InP_sig1),
    LorentzianSusceptibility(frequency = InP_frq2, gamma = InP_gam2, sigma = InP_sig2)]

InP = Medium(epsilon_model = DispersionModel(inf = 7.255, susceptibilities = InP_susc, valid_freq_range = InP_range))

#------------------------------------------------------------------
# germanium (Ge) from N. P. Barnes and M. S. Piltch, J. Optical Society America, Vol. 69, pp. 178-180 (1979)
# ref: https://refractiveindex.info/?shelf=main&book=Ge&page=Icenogle
# wavelength range: 2.5 - 12 um

Ge_range = FreqRange(min = um_scale / 12, max = um_scale / 2.5)

Ge_frq1 = 1 / (0.6641159 * um_scale)
Ge_gam1 = 0
Ge_sig1 = 6.7288
Ge_frq2 = 1 / (62.210127 * um_scale)
Ge_gam2 = 0
Ge_sig2 = 0.21307

Ge_susc = [LorentzianSusceptibility(frequency = Ge_frq1, gamma = Ge_gam1, sigma = Ge_sig1),
    LorentzianSusceptibility(frequency = Ge_frq2, gamma = Ge_gam2, sigma = Ge_sig2)]

Ge = Medium(epsilon_model = DispersionModel(inf = 9.28156, susceptibilities = Ge_susc, valid_freq_range = Ge_range))

#------------------------------------------------------------------
# silicon (Si) from C. D. Salzberg and J. J. Villa, , J. Optical Society America, Vol. 47, pp. 244-246 (1957)
# ref: https://refractiveindex.info/?shelf=main&book=Si&page=Salzberg
# wavelength range: 1.36 - 11 um

Si_range = FreqRange(min = um_scale / 11, max = um_scale / 1.36)

Si_frq1 = 1 / (0.301516485 * um_scale)
Si_gam1 = 0
Si_sig1 = 10.6684293
Si_frq2 = 1 / (1.13475115 * um_scale)
Si_gam2 = 0
Si_sig2 = 0.0030434748
Si_frq3 = 1 / (1104 * um_scale)
Si_gam3 = 0
Si_sig3 = 1.54133408

Si_susc = [LorentzianSusceptibility(frequency = Si_frq1, gamma = Si_gam1, sigma = Si_sig1),
    LorentzianSusceptibility(frequency = Si_frq2, gamma = Si_gam2, sigma = Si_sig2),
    LorentzianSusceptibility(frequency = Si_frq3, gamma = Si_gam3, sigma = Si_sig3)]

Si = Medium(epsilon_model = DispersionModel(inf = 1.0, susceptibilities = Si_susc, valid_freq_range = Si_range))

#------------------------------------------------------------------
# poly(methyl methacrylate) (PMMA) from N. Sultanova et al., Acta Physica Polonica A, Vol. 116, pp. 585-7 (2009)
# ref: https://refractiveindex.info/?shelf=organic&book=poly%28methyl_methacrylate%29&page=Sultanova
# wavelength range: 0.437 - 1.052 um

PMMA_range = FreqRange(min = um_scale / 1.052, max = um_scale / 0.437)

PMMA_frq1 = 1 / (0.106362587407415 * um_scale)
PMMA_gam1 = 0
PMMA_sig1 = 1.1819

PMMA_susc = [LorentzianSusceptibility(frequency = PMMA_frq1, gamma = PMMA_gam1, sigma = PMMA_sig1)]

PMMA = Medium(epsilon_model = DispersionModel(inf = 1.0, susceptibilities = PMMA_susc, valid_freq_range = PMMA_range))

#------------------------------------------------------------------
# polycarbonate (PC) from N. Sultanova et al., Acta Physica Polonica A, Vol. 116, pp. 585-7 (2009)
# ref: https://refractiveindex.info/?shelf=organic&book=polycarbonate&page=Sultanova
# wavelength range: 0.437 - 1.052 um

PC_range = FreqRange(min = um_scale / 1.052, max = um_scale / 0.437)

PC_frq1 = 1 / (0.145958898324152 * um_scale)
PC_gam1 = 0
PC_sig1 = 1.4182

PC_susc = [LorentzianSusceptibility(frequency = PC_frq1, gamma = PC_gam1, sigma = PC_sig1)]

PC = Medium(epsilon_model = DispersionModel(inf = 1.0, susceptibilities = PC_susc, valid_freq_range = PC_range))

#------------------------------------------------------------------
# polystyrene (PS) from N. Sultanova et al., Acta Physica Polonica A, Vol. 116, pp. 585-7 (2009)
# ref: https://refractiveindex.info/?shelf=organic&book=polystyren&page=Sultanova
# wavelength range: 0.437 - 1.052 um

PS_range = FreqRange(min = um_scale / 1.052, max = um_scale / 0.437)

PS_frq1 = 1 / (0.142182980697410 * um_scale)
PS_gam1 = 0
PS_sig1 = 1.4435

PS_susc = [LorentzianSusceptibility(frequency = PS_frq1, gamma = PS_gam1, sigma = PS_sig1)]

PS = Medium(epsilon_model = DispersionModel(inf = 1.0, susceptibilities = PS_susc, valid_freq_range = PS_range))

#------------------------------------------------------------------
# cellulose (CLS) from N. Sultanova et al., Acta Physica Polonica A, Vol. 116, pp. 585-7 (2009)
# ref: https://refractiveindex.info/?shelf=organic&book=cellulose&page=Sultanova
# wavelength range: 0.437 - 1.052 um

CLS_range = FreqRange(min = um_scale / 1.052, max = um_scale / 0.437)

CLS_frq1 = 1 / (0.105294824184287 * um_scale)
CLS_gam1 = 0
CLS_sig1 = 1.124

CLS_susc = [LorentzianSusceptibility(frequency = CLS_frq1, gamma = CLS_gam1, sigma = CLS_sig1)]

CLS = Medium(epsilon_model = DispersionModel(inf = 1.0, susceptibilities = CLS_susc, valid_freq_range = CLS_range))

#------------------------------------------------------------------
# barium borate (BaB2O4), beta phase, from G. Tamosauskas et al., Optical Materials Express, Vol. 8, pp. 1410-18 (2018)
# ref: https://refractiveindex.info/?shelf=main&book=BaB2O4&page=Tamosauskas-o
# ref: https://refractiveindex.info/?shelf=main&book=BaB2O4&page=Tamosauskas-e
# wavelength range: 0.188 - 5.2 um

## NOTE: ordinary (o) axes in X and Y, extraordinary (e) axis in Z

BaB2O4_range = FreqRange(min = um_scale / 5.2, max = um_scale / 0.188)

BaB2O4_frq1 = 1 / (0.06265780079128216 * um_scale)
BaB2O4_gam1 = 0
BaB2O4_sig1 = 0.90291
BaB2O4_frq2 = 1 / (0.13706202975295528 * um_scale)
BaB2O4_gam2 = 0
BaB2O4_sig2 = 0.83155
BaB2O4_frq3 = 1 / (7.746612162745725 * um_scale)
BaB2O4_gam3 = 0
BaB2O4_sig3 = 0.76536

BaB2O4_susc_o = [LorentzianSusceptibility(frequency = BaB2O4_frq1, gamma = BaB2O4_gam1, sigma_diag = BaB2O4_sig1 * [1, 1, 0]),
    LorentzianSusceptibility(frequency = BaB2O4_frq2, gamma = BaB2O4_gam2, sigma_diag = BaB2O4_sig2 * [1, 1, 0]),
    LorentzianSusceptibility(frequency = BaB2O4_frq3, gamma = BaB2O4_gam3, sigma_diag = BaB2O4_sig3 * [1, 1, 0])]

BaB2O4_frq1 = 1 / (0.0845103543951864 * um_scale)
BaB2O4_gam1 = 0
BaB2O4_sig1 = 1.151075
BaB2O4_frq2 = 1 / (0.15029970059850417 * um_scale)
BaB2O4_gam2 = 0
BaB2O4_sig2 = 0.21803
BaB2O4_frq3 = 1 / (16.217274740226856 * um_scale)
BaB2O4_gam3 = 0
BaB2O4_sig3 = 0.656

BaB2O4_susc_e = [
    LorentzianSusceptibility(frequency = BaB2O4_frq1, gamma = BaB2O4_gam1, sigma_diag = BaB2O4_sig1 * [0, 0, 1]),
    LorentzianSusceptibility(frequency = BaB2O4_frq2, gamma = BaB2O4_gam2, sigma_diag = BaB2O4_sig2 * [0, 0, 1]),
    LorentzianSusceptibility(frequency = BaB2O4_frq3, gamma = BaB2O4_gam3, sigma_diag = BaB2O4_sig3 * [0, 0, 1])
]

BaB2O4 = Medium(epsilon_model = DispersionModel(inf = 1.0, susceptibilities = [BaB2O4_susc_o; BaB2O4_susc_e], valid_freq_range = BaB2O4_range))

#------------------------------------------------------------------
# lithium niobate (LiNbO3) from D.E. Zelmon et al., J. Optical Society of America B, Vol. 14, pp. 3319-22 (1997)
# ref: https://refractiveindex.info/?shelf=main&book=LiNbO3&page=Zelmon-o
# ref: https://refractiveindex.info/?shelf=main&book=LiNbO3&page=Zelmon-e
# wavelength range: 0.4 - 5.0 um

## NOTE: ordinary (o) axes in X and Y, extraordinary (e) axis in Z

LiNbO3_range = FreqRange(min = um_scale / 5.0, max = um_scale / 0.4)

LiNbO3_frq1 = 1 / (0.13281566172707193 * um_scale)
LiNbO3_gam1 = 0
LiNbO3_sig1 = 2.6734
LiNbO3_frq2 = 1 / (0.24318717071424636 * um_scale)
LiNbO3_gam2 = 0
LiNbO3_sig2 = 1.2290
LiNbO3_frq3 = 1 / (21.78531615561271 * um_scale)
LiNbO3_gam3 = 0
LiNbO3_sig3 = 12.614

LiNbO3_susc_o = [LorentzianSusceptibility(frequency = LiNbO3_frq1, gamma = LiNbO3_gam1, sigma_diag = LiNbO3_sig1 * [1, 1, 0]),
    LorentzianSusceptibility(frequency = LiNbO3_frq2, gamma = LiNbO3_gam2, sigma_diag = LiNbO3_sig2 * [1, 1, 0]),
    LorentzianSusceptibility(frequency = LiNbO3_frq3, gamma = LiNbO3_gam3, sigma_diag = LiNbO3_sig3 * [1, 1, 0])]

LiNbO3_frq1 = 1 / (0.14307340773183533 * um_scale)
LiNbO3_gam1 = 0
LiNbO3_sig1 = 2.9804
LiNbO3_frq2 = 1 / (0.2580697580112788 * um_scale)
LiNbO3_gam2 = 0
LiNbO3_sig2 = 0.5981
LiNbO3_frq3 = 1 / (20.39803912144498 * um_scale)
LiNbO3_gam3 = 0
LiNbO3_sig3 = 8.9543

LiNbO3_susc_e = [LorentzianSusceptibility(frequency = LiNbO3_frq1, gamma = LiNbO3_gam1, sigma_diag = LiNbO3_sig1 * [0, 0, 1]),
    LorentzianSusceptibility(frequency = LiNbO3_frq2, gamma = LiNbO3_gam2, sigma_diag = LiNbO3_sig2 * [0, 0, 1]),
    LorentzianSusceptibility(frequency = LiNbO3_frq3, gamma = LiNbO3_gam3, sigma_diag = LiNbO3_sig3 * [0, 0, 1])]

LiNbO3 = Medium(epsilon_model = DispersionModel(inf = 1.0, susceptibilities = [LiNbO3_susc_o; LiNbO3_susc_e], valid_freq_range = LiNbO3_range))

#------------------------------------------------------------------
# calcium tungstate (CaWO4) from W.L. Bond, J. Applied Physics, Vol. 36, pp. 1674-77 (1965)
# ref: https://refractiveindex.info/?shelf=main&book=CaWO4&page=Bond-o
# ref: https://refractiveindex.info/?shelf=main&book=CaWO4&page=Bond-e
# wavelength range: 0.45 - 4.0 um

## NOTE: ordinary (o) axes in X and Y, extraordinary (e) axis in Z

CaWO4_range = FreqRange(min = um_scale / 4.0, max = um_scale / 0.45)

CaWO4_frq1 = 1 / (0.1347 * um_scale)
CaWO4_gam1 = 0
CaWO4_sig1 = 2.5493
CaWO4_frq2 = 1 / (10.815 * um_scale)
CaWO4_gam2 = 0
CaWO4_sig2 = 0.9200

CaWO4_susc_o = [LorentzianSusceptibility(frequency = CaWO4_frq1, gamma = CaWO4_gam1, sigma_diag = CaWO4_sig1 * [1, 1, 0]),
    LorentzianSusceptibility(frequency = CaWO4_frq2, gamma = CaWO4_gam2, sigma_diag = CaWO4_sig2 * [1, 1, 0])]

CaWO4_frq1 = 1 / (0.1379 * um_scale)
CaWO4_gam1 = 0
CaWO4_sig1 = 2.6041
CaWO4_frq2 = 1 / (21.371 * um_scale)
CaWO4_gam2 = 0
CaWO4_sig2 = 4.1237

CaWO4_susc_e = [LorentzianSusceptibility(frequency = CaWO4_frq1, gamma = CaWO4_gam1, sigma_diag = CaWO4_sig1 * [0, 0, 1]),
    LorentzianSusceptibility(frequency = CaWO4_frq2, gamma = CaWO4_gam2, sigma_diag = CaWO4_sig2 * [0, 0, 1])]

CaWO4 = Medium(epsilon_model = DispersionModel(inf = 1.0, susceptibilities = [CaWO4_susc_o; CaWO4_susc_e], valid_freq_range = CaWO4_range))

#------------------------------------------------------------------
# calcium carbonate (CaCO3) from G. Ghosh, Optics Communication, Vol. 163, pp. 95-102 (1999)
# ref: https://refractiveindex.info/?shelf=main&book=CaCO3&page=Ghosh-o
# ref: https://refractiveindex.info/?shelf=main&book=CaCO3&page=Ghosh-e
# wavelength range: 0.204 - 2.172 um

## NOTE: ordinary (o) axes in X and Y, extraordinary (e) axis in Z

CaCO3_range = FreqRange(min = um_scale / 2.172, max = um_scale / 0.204)

CaCO3_frq1 = 1 / (0.13940057496294625 * um_scale)
CaCO3_gam1 = 0
CaCO3_sig1 = 0.96464345
CaCO3_frq2 = 1 / (10.954451150103322 * um_scale)
CaCO3_gam2 = 0
CaCO3_sig2 = 1.82831454

CaCO3_susc_o = [LorentzianSusceptibility(frequency = CaCO3_frq1, gamma = CaCO3_gam1, sigma_diag = CaCO3_sig1 * [1, 1, 0]),
    LorentzianSusceptibility(frequency = CaCO3_frq2, gamma = CaCO3_gam2, sigma_diag = CaCO3_sig2 * [1, 1, 0])]

CaCO3_frq1 = 1 / (0.1032906302623815 * um_scale)
CaCO3_gam1 = 0
CaCO3_sig1 = 0.82427830
CaCO3_frq2 = 1 / (10.954451150103322 * um_scale)
CaCO3_gam2 = 0
CaCO3_sig2 = 0.14429128

CaCO3_susc_e = [LorentzianSusceptibility(frequency = CaCO3_frq1, gamma = CaCO3_gam1, sigma_diag = CaCO3_sig1 * [0, 0, 1]),
    LorentzianSusceptibility(frequency = CaCO3_frq2, gamma = CaCO3_gam2, sigma_diag = CaCO3_sig2 * [0, 0, 1])]

CaCO3 = Medium(epsilon_model = DispersionModel(inf_diag = [1.73358749, 1.73358749, 1.35859695], susceptibilities = [CaCO3_susc_o; CaCO3_susc_e], valid_freq_range = CaCO3_range))

#------------------------------------------------------------------
# silicon dioxide (SiO2) from G. Ghosh, Optics Communication, Vol. 163, pp. 95-102 (1999)
# ref: https://refractiveindex.info/?shelf=main&book=SiO2&page=Ghosh-o
# ref: https://refractiveindex.info/?shelf=main&book=SiO2&page=Ghosh-e
# wavelength range: 0.198 - 2.0531 um

## NOTE: ordinary (o) axes in X and Y, extraordinary (e) axis in Z

SiO2_range = FreqRange(min = um_scale / 2.0531, max = um_scale / 0.198)

SiO2_frq1 = 1 / (0.10029257051247614 * um_scale)
SiO2_gam1 = 0
SiO2_sig1 = 1.07044083
SiO2_frq2 = 1 / (10 * um_scale)
SiO2_gam2 = 0
SiO2_sig2 = 1.10202242

SiO2_susc_o = [LorentzianSusceptibility(frequency = SiO2_frq1, gamma = SiO2_gam1, sigma_diag = SiO2_sig1 * [1, 1, 0]),
    LorentzianSusceptibility(frequency = SiO2_frq2, gamma = SiO2_gam2, sigma_diag = SiO2_sig2 * [1, 1, 0])]

SiO2_frq1 = 1 / (0.10104546699382412 * um_scale)
SiO2_gam1 = 0
SiO2_sig1 = 1.09509924
SiO2_frq2 = 1 / (10 * um_scale)
SiO2_gam2 = 0
SiO2_sig2 = 1.15662475

SiO2_susc_e = [LorentzianSusceptibility(frequency = SiO2_frq1, gamma = SiO2_gam1, sigma_diag = SiO2_sig1 * [0, 0, 1]),
    LorentzianSusceptibility(frequency = SiO2_frq2, gamma = SiO2_gam2, sigma_diag = SiO2_sig2 * [0, 0, 1])]

SiO2_aniso = Medium(epsilon_model = DispersionModel(inf_diag = [1.28604141, 1.28604141, 1.28851804], susceptibilities = [SiO2_susc_o; SiO2_susc_e], valid_freq_range = SiO2_range))

#------------------------------------------------------------------
# gallium nitride (GaN), alpha phase (wurtzite), from A.S. Barker Jr. and M. Ilegems, Physical Review B, Vol. 7, pp. 743-50 (1973)
# ref: https://refractiveindex.info/?shelf=main&book=GaN&page=Barker-o
# ref: https://refractiveindex.info/?shelf=main&book=GaN&page=Barker-e
# wavelength range: 0.35 - 10 um

## NOTE: ordinary (o) axes in X and Y, extraordinary (e) axis in Z

GaN_range = FreqRange(min = um_scale / 10.0, max = um_scale / 0.35)

GaN_frq1 = 1 / (0.256 * um_scale)
GaN_gam1 = 0
GaN_sig1 = 1.75
GaN_frq2 = 1 / (17.86 * um_scale)
GaN_gam2 = 0
GaN_sig2 = 4.1

GaN_susc_o = [LorentzianSusceptibility(frequency = GaN_frq1, gamma = GaN_gam1, sigma_diag = GaN_sig1 * [1, 1, 0]),
    LorentzianSusceptibility(frequency = GaN_frq2, gamma = GaN_gam2, sigma_diag = GaN_sig2 * [1, 1, 0])]

GaN_frq1 = 1 / (18.76 * um_scale)
GaN_gam1 = 0
GaN_sig1 = 5.08

GaN_susc_e = [LorentzianSusceptibility(frequency = GaN_frq1, gamma = GaN_gam1, sigma_diag = GaN_sig1 * [0, 0, 1])]

GaN = Medium(epsilon_model = DispersionModel(inf_diag = [3.6, 3.6, 5.35], susceptibilities = [GaN_susc_o; GaN_susc_e], valid_freq_range = GaN_range))

#------------------------------------------------------------------
# aluminum nitride (AlN) from J. Pastrnak and L. Roskovcova, Physica Status Solidi, Vol. 14, K5-8 (1966)
# ref: https://refractiveindex.info/?shelf=main&book=AlN&page=Pastrnak-o
# ref: https://refractiveindex.info/?shelf=main&book=AlN&page=Pastrnak-e
# wavelength range: 0.22 - 5 um

## NOTE: ordinary (o) axes in X and Y, extraordinary (e) axis in Z

AlN_range = FreqRange(min = um_scale / 5.0, max = um_scale / 0.22)

AlN_frq1 = 1 / (0.1715 * um_scale)
AlN_gam1 = 0
AlN_sig1 = 1.3786
AlN_frq2 = 1 / (15.03 * um_scale)
AlN_gam2 = 0
AlN_sig2 = 3.861

AlN_susc_o = [
    LorentzianSusceptibility(frequency = AlN_frq1, gamma = AlN_gam1, sigma_diag = AlN_sig1 * [1, 1, 0]),
    LorentzianSusceptibility(frequency = AlN_frq2, gamma = AlN_gam2, sigma_diag = AlN_sig2 * [1, 1, 0])]

AlN_frq1 = 1 / (0.1746 * um_scale)
AlN_gam1 = 0
AlN_sig1 = 1.6173
AlN_frq2 = 1 / (15.03 * um_scale)
AlN_gam2 = 0
AlN_sig2 = 4.139

AlN_susc_e = [LorentzianSusceptibility(frequency = AlN_frq1, gamma = AlN_gam1, sigma_diag = AlN_sig1 * [0, 0, 1]),
    LorentzianSusceptibility(frequency = AlN_frq2, gamma = AlN_gam2, sigma_diag = AlN_sig2 * [0, 0, 1])]

AlN_aniso = Medium(epsilon_model = DispersionModel(inf_diag = [3.1399, 3.1399, 3.0729], susceptibilities = [AlN_susc_o; AlN_susc_e], valid_freq_range = AlN_range))

#------------------------------------------------------------------
# alumina/sapphire (Al2O3) from I.H. Malitson and M.J. Dodge, J. Optical Society of America, Vol. 62, pp. 1405 (1972)
# ref: https://refractiveindex.info/?shelf=main&book=Al2O3&page=Malitson-o
# ref: https://refractiveindex.info/?shelf=main&book=Al2O3&page=Malitson-e
# wavelength range: 0.2 - 5 um

## NOTE: ordinary (o) axes in X and Y, extraordinary (e) axis in Z

Al2O3_range = FreqRange(min = um_scale / 5.0, max = um_scale / 0.2)

Al2O3_frq1 = 1 / (0.0726631 * um_scale)
Al2O3_gam1 = 0
Al2O3_sig1 = 1.4313493
Al2O3_frq2 = 1 / (0.1193242 * um_scale)
Al2O3_gam2 = 0
Al2O3_sig2 = 0.65054713
Al2O3_frq3 = 1 / (18.02825 * um_scale)
Al2O3_gam3 = 0
Al2O3_sig3 = 5.3414021

Al2O3_susc_o = [LorentzianSusceptibility(frequency = Al2O3_frq1, gamma = Al2O3_gam1, sigma_diag = Al2O3_sig1 * [1, 1, 0]),
    LorentzianSusceptibility(frequency = Al2O3_frq2, gamma = Al2O3_gam2, sigma_diag = Al2O3_sig2 * [1, 1, 0]),
    LorentzianSusceptibility(frequency = Al2O3_frq3, gamma = Al2O3_gam3, sigma_diag = Al2O3_sig3 * [1, 1, 0])]

Al2O3_frq1 = 1 / (0.0740288 * um_scale)
Al2O3_gam1 = 0
Al2O3_sig1 = 1.5039759
Al2O3_frq2 = 1 / (0.1216529 * um_scale)
Al2O3_gam2 = 0
Al2O3_sig2 = 0.55069141
Al2O3_frq3 = 1 / (20.072248 * um_scale)
Al2O3_gam3 = 0
Al2O3_sig3 = 6.5927379

Al2O3_susc_e = [
    LorentzianSusceptibility(frequency = Al2O3_frq1, gamma = Al2O3_gam1, sigma_diag = Al2O3_sig1 * [0, 0, 1]),
    LorentzianSusceptibility(frequency = Al2O3_frq2, gamma = Al2O3_gam2, sigma_diag = Al2O3_sig2 * [0, 0, 1]),
    LorentzianSusceptibility(frequency = Al2O3_frq3, gamma = Al2O3_gam3, sigma_diag = Al2O3_sig3 * [0, 0, 1])
]

Al2O3_aniso = Medium(epsilon_model = DispersionModel(inf = 1, susceptibilities = [Al2O3_susc_o; Al2O3_susc_e], valid_freq_range = Al2O3_range))

#------------------------------------------------------------------
# yttrium oxide (Y2O3) from Y. Nigara, Japanese J. of Applied Physics, Vol. 7, pp. 404-8 (1968)
# ref: https://refractiveindex.info/?shelf=main&book=Y2O3&page=Nigara
# wavelength range: 0.25 - 9.6 um

Y2O3_range = FreqRange(min = um_scale / 9.6, max = um_scale / 0.25)

Y2O3_frq1 = 1 / (0.1387 * um_scale)
Y2O3_gam1 = 0
Y2O3_sig1 = 2.578
Y2O3_frq2 = 1 / (22.936 * um_scale)
Y2O3_gam2 = 0
Y2O3_sig2 = 3.935

Y2O3_susc = [LorentzianSusceptibility(frequency = Y2O3_frq1, gamma = Y2O3_gam1, sigma = Y2O3_sig1),
    LorentzianSusceptibility(frequency = Y2O3_frq2, gamma = Y2O3_gam2, sigma = Y2O3_sig2)]

Y2O3 = Medium(epsilon_model = DispersionModel(inf = 1.0, susceptibilities = Y2O3_susc, valid_freq_range = Y2O3_range))

#------------------------------------------------------------------
# undoped yttrium aluminum garnet (YAG) from D.E. Zelmon et al., Applied Optics, Vol. 37, 4933-5 (1998)
# ref: https://refractiveindex.info/?shelf=main&book=Y3Al5O12&page=Zelmon
# wavelength range: 0.4 - 5.0 um

YAG_range = FreqRange(min = um_scale / 5.0, max = um_scale / 0.4)

YAG_frq1 = 1 / (0.1088577052853862 * um_scale)
YAG_gam1 = 0
YAG_sig1 = 2.28200
YAG_frq2 = 1 / (16.814695953242804 * um_scale)
YAG_gam2 = 0
YAG_sig2 = 3.27644

YAG_susc = [LorentzianSusceptibility(frequency = YAG_frq1, gamma = YAG_gam1, sigma = YAG_sig1),
    LorentzianSusceptibility(frequency = YAG_frq2, gamma = YAG_gam2, sigma = YAG_sig2)]

YAG = Medium(epsilon_model = DispersionModel(inf = 1.0, susceptibilities = YAG_susc, valid_freq_range = YAG_range))

#------------------------------------------------------------------
# cadmium telluride (CdTe) from D.T.F. Marple, J. Applied Physics, Vol. 35, pp. 539-42 (1964)
# ref: https://refractiveindex.info/?shelf=main&book=CdTe&page=Marple
# wavelength range: 0.86 - 2.5 um

CdTe_range = FreqRange(min = um_scale / 2.5, max = um_scale / 0.86)

CdTe_frq1 = 1 / (0.6049793384901669 * um_scale)
CdTe_gam1 = 0
CdTe_sig1 = 1.53

CdTe_susc = [LorentzianSusceptibility(frequency = CdTe_frq1, gamma = CdTe_gam1, sigma = CdTe_sig1)]

CdTe = Medium(epsilon_model = DispersionModel(inf = 5.68, susceptibilities = CdTe_susc, valid_freq_range = CdTe_range))


include("euler/euler.jl")
using .Calculus: champernowne_digits
using BenchmarkTools
BenchmarkTools.DEFAULT_PARAMETERS.samples = 100

function compute(n::Int)::Int
    result = 1
    for i ∈ 0:n
        j, k = 10 ^ i, 0
        while champernowne_digits(k + 1) < j
            k += 1
        end
        k += 1
        remainder = j - champernowne_digits(k - 1)
        number = 10 ^ (k - 1) + remainder ÷ k - 1
        if remainder % k == 0
            result *= parse(Int, string(number)[end])
        else
            result *= parse(Int, string(number + 1)[remainder % k])
        end
    end
    return result
end

compute(6)

@benchmark compute(6)


include("common.jl")

example = """
0,9 -> 5,9
8,0 -> 0,8
9,4 -> 3,4
2,2 -> 2,1
7,0 -> 7,4
6,4 -> 2,0
0,9 -> 2,9
3,4 -> 1,4
0,0 -> 8,8
5,5 -> 8,2
"""

struct Line
    start_x::Int64
    start_y::Int64
    end_x::Int64
    end_y::Int64
end

function Base.parse(::Type{Line}, line::AbstractString)
    l_start, l_end = split(line, " -> ")
    start_x, start_y = split(l_start, ",")
    end_x, end_y = split(l_end, ",")
    return Line(map(x -> parse(Int64, x), (start_x, start_y, end_x, end_y))...)
end

function Base.parse(::Type{Vector{Line}}, input::AbstractString)
    lines = split(input, "\n")
    result = Line[]
    for line in lines
        isempty(line) && continue
        push!(result, parse(Line, line))
    end
    return result
end

horizontal(line::Line) = line.start_y == line.end_y
vertical(line::Line) = line.start_x == line.end_x

rectilinear(line::Line) = horizontal(line) || vertical(line)

function range_index_x(line::Line)
    ix1 = line.start_x + 1
    ix2 = line.end_x + 1
    return ix1 < ix2 ? (ix1:ix2) : (ix1:-1:ix2)
end

function range_index_y(line::Line)
    iy1 = line.start_y + 1
    iy2 = line.end_y + 1
    return iy1 < iy2 ? (iy1:iy2) : (iy1:-1:iy2)
end

function make_grid(lines::AbstractVector{Line})
    max_x = 0
    max_y = 0
    for line in lines
        max_x = max(max_x, line.start_x)
        max_x = max(max_x, line.end_x)
        max_y = max(max_y, line.start_y)
        max_y = max(max_y, line.end_y)
    end
    # Note that we have a zero-based indexing in terms of Line, so add one.
    return zeros(Int64, (max_x + 1, max_y + 1))
end

function populate_rectilinear!(grid::Matrix{Int64}, lines::AbstractVector{Line})
    for line in lines
        if horizontal(line)
            i_y = line.start_y + 1
            @inbounds for i_x in range_index_x(line)
                grid[i_x, i_y] += 1
            end
        elseif vertical(line)
            i_x = line.start_x + 1
            @inbounds for i_y in range_index_y(line)
                grid[i_x, i_y] += 1
            end
        end
        # Line gets skipped if it isn't rectilinear.
    end
    return grid
end

function populate_rectilinear(lines::AbstractVector{Line})
    return populate_rectilinear!(make_grid(lines), lines)
end

function get_ans(input::AbstractString)
    lines = parse(Vector{Line}, input)
    grid = populate_rectilinear(lines)
    return sum(grid .> 1)
end

get_ans(example)
@assert get_ans(example) == 5
get_ans(load_input(5))

# Part 2

function populate_all!(grid::Matrix{Int64}, lines::AbstractVector{Line})
    for line in lines
        if horizontal(line)
            i_y = line.start_y + 1
            @inbounds for i_x in range_index_x(line)
                grid[i_x, i_y] += 1
            end
        elseif vertical(line)
            i_x = line.start_x + 1
            @inbounds for i_y in range_index_y(line)
                grid[i_x, i_y] += 1
            end
        else
            # It should be the case that the line as at 45 degrees...
            Ix = range_index_x(line)
            Iy = range_index_y(line)
            @assert length(Ix) == length(Iy)
            for (i_x, i_y) in zip(Ix, Iy)
                grid[i_x, i_y] += 1
            end
        end
    end
    return grid
end

populate_all(lines) = populate_all!(make_grid(lines), lines)

function get_ans2(input::AbstractString)
    lines = parse(Vector{Line}, input)
    grid = populate_all(lines)
    return sum(grid .> 1)
end

@assert get_ans2(example) == 12
get_ans2(load_input(5))


using ProbNumDiffEq
using Test
using LinearAlgebra

using DiffEqProblemLibrary.ODEProblemLibrary: importodeproblems;
importodeproblems();
import DiffEqProblemLibrary.ODEProblemLibrary: prob_ode_lotkavoltera

h = rand()
σ = rand()

@testset "Test vanilla (ie. non-preconditioned) IBM (d=2,q=2)" begin
    d, q = 2, 2

    A!, Q! = ProbNumDiffEq.vanilla_ibm(d, q)
    Ah = diagm(0 => ones(d * (q + 1)))
    Qh = zeros(d * (q + 1), d * (q + 1))
    A!(Ah, h)
    Q!(Qh, h, σ^2)

    AH_22_IBM = [
        1 h h^2/2 0 0 0
        0 1 h 0 0 0
        0 0 1 0 0 0
        0 0 0 1 h h^2/2
        0 0 0 0 1 h
        0 0 0 0 0 1
    ]
    @test AH_22_IBM ≈ Ah

    QH_22_IBM =
        σ^2 .* [
            h^5/20 h^4/8 h^3/6 0 0 0
            h^4/8 h^3/3 h^2/2 0 0 0
            h^3/6 h^2/2 h 0 0 0
            0 0 0 h^5/20 h^4/8 h^3/6
            0 0 0 h^4/8 h^3/3 h^2/2
            0 0 0 h^3/6 h^2/2 h
        ]
    @test QH_22_IBM ≈ Qh
end

@testset "Test IBM with preconditioning (d=1,q=2)" begin
    d, q = 2, 2

    A, Q = ProbNumDiffEq.ibm(d, q)
    Qh = Q * σ^2

    AH_22_PRE = [
        1 2 1 0 0 0
        0 1 1 0 0 0
        0 0 1 0 0 0
        0 0 0 1 2 1
        0 0 0 0 1 1
        0 0 0 0 0 1
    ]

    QH_22_PRE =
        σ^2 * [
            1/5 1/4 1/3 0 0 0
            1/4 1/3 1/2 0 0 0
            1/3 1/2 1/1 0 0 0
            0 0 0 1/5 1/4 1/3
            0 0 0 1/4 1/3 1/2
            0 0 0 1/3 1/2 1/1
        ]

    @test AH_22_PRE ≈ A
    @test QH_22_PRE ≈ Qh
end

@testset "Verify correct prior dim" begin
    prob = prob_ode_lotkavoltera
    d = length(prob.u0)
    for q in 1:5
        integ = init(prob, EK0(order=q), initialize_derivatives=false)
        @test length(integ.cache.x.μ) == d * (q + 1)
        sol = solve!(integ)
        @test length(integ.cache.x.μ) == d * (q + 1)
        @test length(sol.x_filt[end].μ) == d * (q + 1)
    end
end


type AnnealingEpsilonGreedy <: BanditAlgorithm
  counts::Vector{Int64}
  values::Vector{Float64}
end

function AnnealingEpsilonGreedy(n_arms::Int64)
  AnnealingEpsilonGreedy(zeros(Int64, n_arms), zeros(n_arms))
end

function initialize(algo::AnnealingEpsilonGreedy, n_arms::Int64)
  algo.counts = zeros(Int64, n_arms)
  algo.values = zeros(n_arms)
end

function initialize(algo::AnnealingEpsilonGreedy, arms::Vector)
  initialize(algo, length(arms))
end

function select_arm(algo::AnnealingEpsilonGreedy)
  t = sum(algo.counts) + 1
  epsilon = 1 / log(t + eps(1.0))

  if rand() > epsilon
    return ind_max(algo.values)
  else
    return randi(length(algo.values))
  end
end

function update(algo::AnnealingEpsilonGreedy, chosen_arm::Int64, reward::Real)
  algo.counts[chosen_arm] += 1
  n = algo.counts[chosen_arm]

  value = algo.values[chosen_arm]
  algo.values[chosen_arm] = ((n - 1) / n) * value + (1 / n) * reward
end


using Pkg
Pkg.activate(@__DIR__)
using Soss
using ZigZagBoomerang

m = @model x begin
    α ~ SpikeMixture(Normal(), 0.3) # 0.2*Normal() + 0.8*Dirac(0)
    β ~ SpikeMixture(Normal(), 0.3)
    yhat = α .+ β .* x
    y ~ For(eachindex(x)) do j
        Normal(yhat[j], 2.0)
    end
end

vars = (α=1, β=2)

x = randn(20);
obs = -0.1 .+ 2x + 1randn(20);
T = 200.0
posterior = m(x=x) | (y=obs,)

bm_ = Soss.basemeasure(posterior, (β=0.0, α=0.0));
bm = getindex.(Ref(bm_.data), (keys(vars)));
@show kappa.(bm)

include("sparsezigzag.jl")
trace, final, (num, acc) = @time sparse_zigzag(posterior, T, c=50)

ts, xs = ZigZagBoomerang.sep(discretize(trace, 0.1))

xs = xform(posterior).(xs)

using Plots
p = plot(ts, getindex.(xs, 1))
plot!(ts, getindex.(xs, 2), color=:red)
savefig("tracesticky.png")
p2 = plot(first.(xs), last.(xs))
savefig("phasesticky.png")
p


#################### Heidelberger and Welch Diagnostic ####################

function heideldiag(x::Vector{T}; alpha::Real=0.05, eps::Real=0.1,
                             etype=:imse, start::Integer=1, args...) where {T<:Real}
  n = length(x)
  delta = trunc(Int, 0.10 * n)
  y = x[trunc(Int, n / 2):end]
  S0 = length(y) * mcse(y, etype; args...)^2
  i, pvalue, converged, ybar = 1, 1.0, false, NaN
  while i < n / 2
    y = x[i:end]
    m = length(y)
    ybar = mean(y)
    B = cumsum(y) - ybar * collect(1:m)
    Bsq = (B .* B) ./ (m * S0)
    I = sum(Bsq) / m
    pvalue = 1.0 - pcramer(I)
    converged = pvalue > alpha
    if converged
      break
    end
    i += delta
  end
  halfwidth = sqrt(2.0) * erfinv(1.0 - alpha) * mcse(y, etype; args...)
  passed = halfwidth / abs(ybar) <= eps
  [i + start - 2, converged, round(pvalue, digits = 4), ybar, halfwidth, passed]
end

function heideldiag(c::AbstractChains; alpha::Real=0.05, eps::Real=0.1,
                    etype=:imse, args...)
  _, p, m = size(c.value)
  vals = Array{Float64}(undef, p, 6, m)
  for j in 1:p, k in 1:m
    vals[j, :, k] = heideldiag(c.value[:, j, k], alpha=alpha, eps=eps,
                               etype=etype, start=first(c.range); args...)
  end
  hdr = header(c) * "\nHeidelberger and Welch Diagnostic:\n" *
        "Target Halfwidth Ratio = $eps\nAlpha = $alpha\n"
  ChainSummary(vals, c.names, ["Burn-in", "Stationarity", "p-value", "Mean",
                               "Halfwidth", "Test"], hdr)
end


# This file is a part of MGVI.jl, licensed under the MIT License (MIT).

using MGVI

using LinearAlgebra
using Random
using SparseArrays
using ValueShapes

Test.@testset "test_fisher_values" begin

    include("information_utils.jl")

    Random.seed!(42)
    epsilon = 5E-2
    num_runs = 100000

    function test_univariate(dist, params)
        model = p -> dist(p...)
        res = MGVI.fisher_information(model(params))
        truth = fisher_information_mc(model, params, num_runs)
        Test.@test norm((Matrix(res) - truth)) / norm(truth) < epsilon
    end

    test_univariate(Normal, [0.1, 0.2])
    test_univariate(Exponential, [0.3])
    test_univariate(Poisson, [5.75])

    function test_mvnormal(dim)
        dim = 1
        cov = I*5 + Symmetric(rand(dim, dim))
        mean = rand(dim)
        params = vcat(mean, cov[:])
        model = p -> MvNormal(p[1:dim], reshape(p[dim+1:end], dim, dim))
        res = MGVI.fisher_information(model(params))
        explicit = explicit_mv_normal_fi(cov)
        truth = fisher_information_mc(model, params, num_runs)
        Test.@test norm((Matrix(res) - truth)) / norm(truth) < epsilon
        Test.@test norm((Matrix(res) - explicit)) / norm(explicit) < epsilon
    end

    test_mvnormal(1)  # test mvnormal 1d
    test_mvnormal(2)  # test mvnormal 2d
    test_mvnormal(3)  # test mvnormal 3d

end

Test.@testset "test_fisher_information_combinations" begin

    epsilon = 1E-5

    # test Product(Univariates)
    μ1, σ1 = 0.1, 0.2
    μ2, σ2 = 0.1, 0.3
    dists = [Normal(μ1, σ1), Normal(μ2, σ2)]
    res = MGVI.fisher_information(Product(dists))
    truth = blockdiag(MGVI.fisher_information.(dists)...)
    Test.@test norm(Matrix(res) - Matrix(truth)) < epsilon

    # test NamedTupleDist
    dists = NamedTupleDist(a=Normal(0.1, 0.2),
                           b=Product([Normal(0.1, 0.2), Normal(0.3, 0.1)]),
                           c=MvNormal([0.2, 0.3], [2. 0.1; 0.1 4.5]))
    res = MGVI.fisher_information(dists)
    truth = blockdiag((MGVI.without_chol ∘ MGVI.fisher_information).(values(dists))...)
    Test.@test norm(Matrix(res) - Matrix(truth)) < epsilon

end


import Base: muladd, fma

function fma(xhi::T, xlo::T, yhi::T, ylo::T, zhi::T, zlo::T) where {T<:AbstractFloat}
   chi, c1 = two_prod(xhi, yhi)
   t0 = xlo * ylo
   t1 = fma(xhi, ylo, t0)
   c2 = fma(xlo, yhi, t1)
   c3 = c1 + c2
   dhi, dlo = two_hilo_sum(chi, c3)

   shi, slo = two_sum(zhi, dhi)
   thi, tlo = two_sum(zlo, dlo)
   c = slo + thi
   vhi, vlo = two_hilo_sum(shi, c)
   w = tlo + vlo
   hi, lo = two_hilo_sum(vhi, w)
   return DoubleFloat{T}(hi, lo)
end

@inline function fma(x::DoubleFloat{T}, y::DoubleFloat{T}, z::DoubleFloat{T}) where {T<:AbstractFloat}
   return fma(x.hi, x.lo, y.hi, y.lo, z.hi, z.lo)
end

function fma(x::T, y::T, zhi::T, zlo::T) where {T<:AbstractFloat}
   chi, c1 = two_prod(x, y)
   shi, slo = two_sum(zhi, chi)
   thi, tlo = two_sum(zlo, c1)
   c = slo + thi
   vhi, vlo = two_hilo_sum(shi, c)
   w = tlo + vlo
   hi, lo = two_hilo_sum(vhi, w)
   return DoubleFloat{T}(hi, lo)
end

@inline function fma(x::T, y::T, z::DoubleFloat{T}) where {T<:AbstractFloat}
   return fma(x, y, z.hi, z.lo)
end

function fma(xhi::T, xlo::T, yhi::T, ylo::T, z::T) where {T<:AbstractFloat}
   chi, c1 = two_prod(xhi, yhi)
   t0 = xlo * ylo
   t1 = fma(xhi, ylo, t0)
   c2 = fma(xlo, yhi, t1)
   c3 = c1 + c2
   dhi, dlo = two_hilo_sum(chi, c3)
   shi, slo = two_sum(z, dhi)
   c = slo + dlo
   hi, lo = two_hilo_sum(shi, c)
   return DoubleFloat{T}(hi, lo)
end

@inline function fma(x::DoubleFloat{T}, y::DoubleFloat{T}, z::T) where {T<:AbstractFloat}
   return fma(x.hi, x.lo, y.hi, y.lo, z)
end

@inline function fma(x::DoubleFloat{T}, y::DoubleFloat{T}, z::Tuple{T,T}) where {T<:AbstractFloat}
   return fma(x.hi, x.lo, y.hi, y.lo, z[1], z[2])
end
@inline function fma(x::DoubleFloat{T}, y::Tuple{T,T}, z::DoubleFloat{T}) where {T<:AbstractFloat}
   return fma(x.hi, x.lo, y[1], y[2], z.hi, z.lo)
end
@inline function fma(x::Tuple{T,T}, y::DoubleFloat{T}, z::DoubleFloat{T}) where {T<:AbstractFloat}
   return fma(x[1], x[2], y.hi, y.lo, z.hi, z.lo)
end
@inline function fma(x::DoubleFloat{T}, y::Tuple{T,T}, z::Tuple{T,T}) where {T<:AbstractFloat}
   return fma(x.hi, x.lo, y[1], y[2], z[1], z[2])
end
@inline function fma(x::Tuple{T,T}, y::Tuple{T,T}, z::DoubleFloat{T}) where {T<:AbstractFloat}
   return fma(x[1], x[2], y[1], y[2], z.hi, z.lo)
end
@inline function fma(x::Tuple{T,T}, y::DoubleFloat{T}, z::Tuple{T,T}) where {T<:AbstractFloat}
   return fma(x[1], x[2], y.hi, y.lo, z[1], z[2])
end
@inline function fma(x::Tuple{T,T}, y::Tuple{T,T}, z::Tuple{T,T}) where {T<:AbstractFloat}
   return fma(x[1], x[2], y[1], y[2], z[1], z[2])
end

function muladd(xhi::T, xlo::T, yhi::T, ylo::T, zhi::T, zlo::T) where {T<:AbstractFloat}
   chi, c1 = two_prod(xhi, yhi)
   t0 = xlo * ylo
   c2 = fma(xlo, yhi, t0)
   c3 = c1 + c2
   dhi, dlo = two_hilo_sum(chi, c3)

   shi, slo = two_sum(zhi, dhi)
   thi, tlo = two_sum(zlo, dlo)
   c = slo + thi
   vhi, vlo = two_hilo_sum(shi, c)
   w = tlo + vlo
   hi, lo = two_hilo_sum(vhi, w)
   return DoubleFloat{T}(hi, lo)
end

@inline function muladd(x::DoubleFloat{T}, y::DoubleFloat{T}, z::DoubleFloat{T}) where {T<:AbstractFloat}
   return muladd(x.hi, x.lo, y.hi, y.lo, z.hi, z.lo)
end

function muladd(x::T, y::T, zhi::T, zlo::T) where {T<:AbstractFloat}
   chi, clo = two_prod(x, y)
   shi, slo = two_sum(zhi, chi)
   thi, tlo = two_sum(zlo, clo)
   c = slo + thi
   vhi, vlo = two_hilo_sum(shi, c)
   w = tlo + vlo
   hi, lo = two_hilo_sum(vhi, w)
   return DoubleFloat{T}(hi, lo)
end

@inline function muladd(x::T, y::T, z::DoubleFloat{T}) where {T<:AbstractFloat}
   return muladd(x, y, z.hi, z.lo)
end

function muladd(xhi::T, xlo::T, yhi::T, ylo::T, z::T) where {T<:AbstractFloat}
   chi, c1 = two_prod(xhi, yhi)
   t0 = xlo * ylo
   c2 = fma(xlo, yhi, t0)
   c3 = c1 + c2
   dhi, dlo = two_hilo_sum(chi, c3)

   shi, slo = two_sum(z, dhi)
   c = slo + dlo
   hi, lo = two_hilo_sum(shi, c)
   return DoubleFloat{T}(hi, lo)
end

@inline function muladd(x::DoubleFloat{T}, y::DoubleFloat{T}, z::T) where {T<:AbstractFloat}
   return muladd(x.hi, x.lo, y.hi, y.lo, z)
end

@inline function muladd(x::DoubleFloat{T}, y::DoubleFloat{T}, z::Tuple{T,T}) where {T<:AbstractFloat}
   return muladd(x.hi, x.lo, y.hi, y.lo, z[1], z[2])
end
@inline function muladd(x::DoubleFloat{T}, y::Tuple{T,T}, z::DoubleFloat{T}) where {T<:AbstractFloat}
   return muladd(x.hi, x.lo, y[1], y[2], z.hi, z.lo)
end
@inline function muladd(x::Tuple{T,T}, y::DoubleFloat{T}, z::DoubleFloat{T}) where {T<:AbstractFloat}
   return muladd(x[1], x[2], y.hi, y.lo, z.hi, z.lo)
end
@inline function muladd(x::DoubleFloat{T}, y::Tuple{T,T}, z::Tuple{T,T}) where {T<:AbstractFloat}
   return muladd(x.hi, x.lo, y[1], y[2], z[1], z[2])
end
@inline function muladd(x::Tuple{T,T}, y::Tuple{T,T}, z::DoubleFloat{T}) where {T<:AbstractFloat}
   return muladd(x[1], x[2], y[1], y[2], z.hi, z.lo)
end
@inline function muladd(x::Tuple{T,T}, y::DoubleFloat{T}, z::Tuple{T,T}) where {T<:AbstractFloat}
   return muladd(x[1], x[2], y.hi, y.lo, z[1], z[2])
end
@inline function muladd(x::Tuple{T,T}, y::Tuple{T,T}, z::Tuple{T,T}) where {T<:AbstractFloat}
   return muladd(x[1], x[2], y[1], y[2], z[1], z[2])
end


function getTableauHSPARKSymmetricProjection(name, q::CoefficientsRK{T}, p::CoefficientsRK{T}, d=[]; R∞=1) where {T}

    @assert q.s == p.s

    o = min(q.o, p.o)

    a_q = q.a
    a_p = p.a

    α_q = zeros(T, q.s, 2)
    α_q[:,1] .= 0.5

    α_p = zeros(T, p.s, 2)
    α_p[:,1] .= 0.5

    a_q̃ = Array(transpose(hcat(zero(q.b), q.b)))
    a_p̃ = Array(transpose(hcat(zero(p.b), p.b)))

    α_q̃ = [[0.0  0.0]
           [0.5  R∞*0.5]]
    α_p̃ = [[0.0  0.0]
           [0.5  R∞*0.5]]

    b_q = q.b
    b_p = p.b
    β_q = [0.5, R∞*0.5]
    β_p = [0.5, R∞*0.5]

    c_q = q.c
    c_p = p.c
    c_λ = [ 0.0, 1.0]
    d_λ = [ 0.5, 0.5]

    ω_λ  = zeros(T, 1, 3)
    ω_λ .= [0.5 R∞*0.5 0.0]

    δ_λ  = zeros(T, 1, 2)
    δ_λ .= [-1.0 +1.0]

    if length(d) == 0
        return TableauHSPARKprimary(name, o,
                            a_q, a_p, α_q, α_p,
                            a_q̃, a_p̃, α_q̃, α_p̃,
                            b_q, b_p, β_q, β_p,
                            c_q, c_p, c_λ, d_λ,
                            ω_λ, δ_λ)
    else
        @assert length(d) == q.s == p.s

        return TableauHSPARKprimary(name, o,
                            a_q, a_p, α_q, α_p,
                            a_q̃, a_p̃, α_q̃, α_p̃,
                            b_q, b_p, β_q, β_p,
                            c_q, c_p, c_λ, d_λ,
                            ω_λ, δ_λ, d)
    end

end

"Tableau for Gauss-Lobatto IIIA-IIIB method with two stages and symmetric projection."
function getTableauHSPARKLobIIIAIIIB2pSymmetric()
    d = [+1.0, -1.0]
    getTableauHSPARKSymmetricProjection(:HSPARKLobIIIAIIIB2, getCoefficientsLobIIIA2(), getCoefficientsLobIIIB2(), d; R∞=-1)
end

"Tableau for Gauss-Lobatto IIIA-IIIB method with three stages and symmetric projection."
function getTableauHSPARKLobIIIAIIIB3pSymmetric()
    d = [+0.5, -1.0, +0.5]
    getTableauHSPARKSymmetricProjection(:HSPARKLobIIIAIIIB3, getCoefficientsLobIIIA3(), getCoefficientsLobIIIB3(), d; R∞=+1)
end

"Tableau for Gauss-Legendre method with s stages and symplectic projection."
function getTableauHSPARKGLRKpSymmetric(s)
    glrk = getCoefficientsGLRK(s)
    getTableauHSPARKSymmetricProjection(Symbol("HSPARKGLRK", s), glrk, glrk; R∞=(-1)^s)
end


using DifferentialEquations
using DataInterpolations

function munge(A::Vector{Vector{Float64}})
    l1 = length(A)
    l2 = length(A[1])
    M = zeros(l2,l1)
    for  i = 1:l1
        M[:,i] = A[i]
    end
    M
end

function lorenz!(du,u,p,t)
    du[1] = p[1]*(u[2]-u[1])
    du[2] = u[1]*(p[2]-u[3]) - u[2]
    du[3] = u[1]*u[2] - p[3]*u[3]
end

u₀ = [1,0,0]
p = [10,12,8/3] # Change d_lorenz
tspan = (0,10)
t = 0:0.01:10
prob = ODEProblem(lorenz!,u₀,tspan,p)
sol = solve(prob,Tsit5(),saveat=t)

U = munge(sol.u)
T = sol.t

interpq = QuadraticInterpolation(U,T)
DataInterpolations.derivative(interpq,1)

DataInterpolations.derivative(interpq,10)

interpl = LagrangeInterpolation(U,T)
DataInterpolations.derivative(interpl,1)

DataInterpolations.derivative(interpl,10)


using Unitful, UnitfulAstro

# Given
sellingPrice = 167 # Dollars
materialsAndParts = 25 # Dollars per unit
directLabor = 2 * 20
fixedCost = 1400000

# Find
# a.) overhead expenses are charged at
overheadExpenses = 0.8 * directLabor
totalManufacturingCost = materialsAndParts + directLabor + overheadExpenses

# b.) Breakeven
m = sellingPrice - totalManufacturingCost
b = -fixedCost
x = -b / m

# c.) Profit/unit
units = 30000
y = m * units + b
profitPerUnit = y / units

# d.)
x = 15000 # when y = 0, what should
P = -b / x + totalManufacturingCost


function roots(R)

    @info "Finding the roots..."

    # Compact notation for the possible parities
    ϵ = [1 -1]
    # Maximum value for x≡k⋅R=E⋅R/ħ with Emax=Λ, which will appear as argument for the Bessel function
    xmax = Λ * R / ħ

    # Empty vector for roots and their corresponding quantum numbers l and ϵ
    xroot = Float64[]
    lroot = Int64[]
    ϵroot = Int64[]

    # Routine for finding roots, performed until no roots are found two consecutive times
    stillsearch = true
    θ = false
    li = 0
    while stillsearch
        for ϵi in ϵ
            # Boundary condition
            boundary(x) = sphericalbessel(x, li) - ϵi * sphericalbessel(x, li + ϵi)
            if (li!=0) || (ϵi!=-1)
                # Function for finding the root of the boundary condition in the interval [0,xmax]
                root = find_zeros(boundary, zero(xmax), xmax)
                @debug l, root, θ
                if isempty(root)
                    if θ
                        stillsearch=false
                    else
                        θ = true
                    end
                else
                    roots = [r for r in root]
                    append!(xroot, roots)
                    # Notice that, for a given li and ϵi, multiple roots may be found
                    append!(lroot, fill(li, length(roots)))
                    append!(ϵroot, fill(ϵi, length(roots)))
                end
            end
        end
        li = li+1
    end

    # Number of roots
    κ = length(xroot)

    @info "Done!"
    @debug "Roots found at: " xroot
    @debug "Corresponding l: " lroot
    @debug "Corresponding ϵ: " ϵroot

    return xroot, lroot, ϵroot, κ

end