module IZNeuron
export IZStates, IZParams, evolve

struct IZStates{T <: AbstractFloat}
    v::T
    u::T
end

struct IZParams{T <: AbstractFloat}
    a::T
    b::T
    c::T
    d::T
    bc::T
    lazy_threshold::T

    IZParams(a, b, c, d, lazy_threshold) = new{T}(a, b, c, d, lazy_threshold, b * c)
end

function evolve(t, dt, states::IZStates, params::IZParams, inject_fn, update, fire_fn, put_task)

    i_syn, delta_v = inject_fn()
    new_v = states.v + dt * (0.04 * states.v^2 + 5 * states.v + 140 - states.u + i_syn) + delta_v
    new_u = states.u + dt * params.a * (params.b * states.v - states.u)

    if new_v >= 30.0
        new_v = params.c
        new_u += params.d
        fire_fn(t, dt)
    end

    update(IZStates(new_v, new_u))

    if abs(new_v - params.c) > params.lazy_threshold || abs(new_u - params.bc) > params.lazy_threshold
        put_task(t + dt)
    end

end

end # Module end


"""
    svmtrain(svm::LSSVC, x::AbstractMatrix, y::AbstractVector) -> Tuple

Solves a Least Squares Support Vector **classification** problem using the Conjugate
Gradient method. In particular, it uses the Lanczos process due to the fact that the
matrices are symmetric.

# Arguments
- `svm::LSSVC`: The Support Vector Machine that contains the hyperparameters, as well as the kernel to be used.
- `x::AbstractMatrix`: The data matrix with the features. It is expected that this array is already standardized, i.e. the mean for each feature is zero and its standard deviation is one.
- `y::AbstractVector`: A vector that contains the classes. It is expected that there are only two classes, -1 and 1.

# Returns
- `Tuple`: A tuple containing `x`, `y` and the following two elements:
- `b`: Contains the bias for the decision function.
- `α`: Contains the weights for the decision function.
"""
function svmtrain(svm::LSSVC, x::AbstractMatrix, y::AbstractVector)
    n = size(y, 1)
    # Specify the keyword arguments
    kwargs = _kwargs2dict(svm)
    # We build the kernel matrix and the omega matrix
    kern_mat = _build_kernel_matrix(x; kwargs...)
    Ω = (y .* y') .* kern_mat
    H = Ω + I / svm.γ

    # * Start solving the subproblems
    # First, solve for eta
    (η, stats) = cg_lanczos(H, y)
    # Then, solve for nu
    (ν, stats) = cg_lanczos(H, ones(n))

    # We then compute s
    s = dot(y, η)

    # Finally, we solve the problem for alpha and b
    b = dot(η, ones(n)) / s
    α = ν .- (η * b)

    return (x, y, α, b)
end

"""
    svmpredict(svm::LSSVC, fits, xnew::AbstractMatrix) -> AbstractArray

Uses the information obtained from `svmtrain` such as the bias and weights to construct a decision function and predict new class values. For the _classification_ problem only.

# Arguments
- `svm::LSSVC`: The Support Vector Machine that contains the hyperparameters, as well as the kernel to be used.
- `fits`: It can be any container data structure but it must have four elements: `x`, the data matrix; `y`, the labels vector; `α`, the weights; and `b`, the bias.
- `xnew::AbstractMatrix`: The data matrix that contains the new instances to be predicted.

# Returns
- `Array`: The labels corresponding to the prediction to each of the instances in `xnew`.
"""
function svmpredict(svm::LSSVC, fits, xnew::AbstractMatrix)
    x, y, α, b = fits
    kwargs = _kwargs2dict(svm)
    @assert size(x, 1) == size(xnew, 1)
    kern_mat = _build_kernel_matrix(x, xnew; kwargs...)
    result = sum(@. kern_mat * y * α; dims=1) .+ b
    # We need to remove the trailing dimension
    result = reshape(result, size(result, 2))

    return sign.(result)
end

function svmtrain_mc(svm::LSSVC, x, y, nclass)
    num_classifiers = nclass * (nclass - 1) / 2
    # Create a collection to store the learned parameters
    class_parameters = Vector{Tuple}(undef, Int(num_classifiers))
    # Create a collection to store the class pairs
    class_pairs = similar(class_parameters)
    c_idx = 1 # For keeping track of the classifiers

    for idx = 1:nclass - 1
        # Get the elements and indices for the first class
        a_class, a_idxs = _find_and_copy(idx, y)
        a_class .= 1.0 # The first class is encoded as 1.0
        for jdx = (idx + 1):nclass
            # Get the elements and indices for the second class
            b_class, b_idxs = _find_and_copy(jdx, y)
            b_class .= -1.0 # The second class is encoded as -1.0

            # Train a binary classification problem with the first and second classes
            all_indxs = vcat(a_idxs, b_idxs) # Join all indices
            all_classes = vcat(a_class, b_class) # Join both classes

            # Always copy the model and use views for the samples
            samples = @view(x[:, all_indxs])
            # Solve the binary classification problem between classes idx and jdx
            new_svm = deepcopy(svm)
            fits = svmtrain(new_svm, samples, all_classes)

            # We save the parameters of that classifier
            class_parameters[c_idx] = fits
            # We save the class pairs for decoding them later
            class_pairs[c_idx] = (idx, jdx)
            c_idx += 1
        end
    end

    return (deepcopy(svm), class_parameters, class_pairs)
end

function svmpredict_mc(fits, Xnew::AbstractMatrix)
    # Extract the model, parameters and class codes
    svm, params, pairs = fits
    nclass = length(pairs) # The third element are the class pairs
    # This will hold all the predictions for the classifiers
    pooled_predictions = Matrix{eltype(Xnew)}(undef, nclass, size(Xnew, 2))

    for (idx, p, s) in zip(1:nclass, params, pairs)
        # For prediction, we just need the fitted parameters
        prediction = svmpredict(svm, p, Xnew)

        # Now that we have predictions, decode them using the class pairs
        broadcast!(x -> x == 1.0 ? s[1] : s[2], prediction, prediction)

        # Save the decoded predictions to our pooling collection
        pooled_predictions[idx, :] = prediction
    end

    # Apply the voting scheme
    results = _predictions_by_votes(pooled_predictions)

    return results
end

"""
    svmtrain(svm::LSSVR, x::AbstractMatrix, y::AbstractVector) -> Tuple

Solves a Least Squares Support Vector **regression** problem using the Conjugate Gradient
method. In particular, it uses the Lanczos process due to the fact that the matrices are
symmetric.

# Arguments
- `svm::LSSVR`: The Support Vector Machine that contains the hyperparameters, as well as the kernel to be used.
- `x::AbstractMatrix`: The data matrix with the features. It is expected that this array is already standardized, i.e. the mean for each feature is zero and its standard deviation is one.
- `y::AbstractVector`: A vector that contains the continuous value of the function estimation. The elements can be any subtype of `Real`.

# Returns
- `Tuple`: A tuple containing `x` and the following two elements:
- `b`: Contains the bias for the decision function.
- `α`: Contains the weights for the decision function.
"""
function svmtrain(svm::LSSVR, x::AbstractMatrix, y::AbstractVector)
    n = size(y, 1)
    # Specify the keyword arguments
    kwargs = _kwargs2dict(svm)
    # We build the kernel matrix and the omega matrix
    kern_mat = _build_kernel_matrix(x; kwargs...)
    H = kern_mat + I / svm.γ

    # * Start solving the subproblems
    # First, solve for eta
    (η, stats) = cg_lanczos(H, ones(n))
    # Then, solve for nu
    (ν, stats) = cg_lanczos(H, y)

    # We then compute s
    s = dot(ones(n), η)

    # Finally, we solve the problem for alpha and b
    b = dot(η, y) / s
    α = ν .- (η * b)

    return (x, α, b)
end

"""
    svmpredict(svm::LSSVR, fits, xnew::AbstractMatrix) -> AbstractArray

Uses the information obtained from `svmtrain` such as the bias and weights to construct a
decision function and predict the new values of the function. For the _regression_
problem only.

# Arguments
- `svm::LSSVR`: The Support Vector Machine that contains the hyperparameters, as well as the kernel to be used.
- `fits`: It can be any container data structure but it must have four elements: `x`, the data matrix; `y`, the labels vector; `α`, the weights; and `b`, the bias.
- `xnew::AbstractMatrix`: The data matrix that contains the new instances to be predicted.

# Returns
- `Array`: The labels corresponding to the prediction to each of the instances in `xnew`.
"""
function svmpredict(svm::LSSVR, fits, xnew::AbstractMatrix)
    x, α, b = fits
    @assert size(x, 1) == size(xnew, 1)

    kwargs = _kwargs2dict(svm)
    kern_mat = _build_kernel_matrix(x, xnew; kwargs...)
    result = sum(kern_mat .* α; dims=1) .+ b

    # We need to remove the trailing dimension
    result = reshape(result, size(result, 2))

    return result
end


push!(LOAD_PATH, "Modules")
using LinearAlgebra
using Colors
using Revise
using JuliRay
using Statistics
RealVector=JuliRay.RealVector
RealMatrix=JuliRay.RealMatrix
Base.@irrational ° 0.0174532925199432957692369076848861271344 (big(pi)/big(180))
φ=MathConstants.φ
include("Polyhedra.jl")

##
ε=10^(-1.2)

function f(x)
    ifelse(x>0,exp(-1/x),0)
end
function g(x)
    f(2x/√3)/(f(2x/√3)+f(2/√3-2x/√3))
end
function Smooth(a,b,x)
    g((x-a)/(b-a))
end

function ℝ³⭢S³(p::Array{T,1}) where T<:Real
    if length(p)≠3
        error("No Point on ℝ³")
    end
    return [2p[1]/(1+p[1]^2+p[2]^2+p[3]^2),2p[2]/(1+p[1]^2+p[2]^2+p[3]^2),2p[3]/(1+p[1]^2+p[2]^2+p[3]^2),(-1+p[1]^2+p[2]^2+p[3]^2)/(1+p[1]^2+p[2]^2+p[3]^2)]
end
function ℝ³⭢S³(p::Array{T,1},θ::Real) where T<:Real
    if length(p)≠3
        error("No Point on ℝ³")
    elseif θ<ε
        return [p[1:3]...,0.0]
    else
        c=[0,0,0,cot(θ)]
        r=1/sin(θ)
        P=p*cot((π/2-θ)/2+π/4)
        return ℝ³⭢S³(P)*r+c
    end
end
function S³⭢ℝ³(q::Array{T,1}) where T<:Real
    if length(q)≠4
        error("No Point on S³")
    elseif !(norm(q) ≈ 1.0)
        print(norm(q))
        error("No Point on unit S³, radius: $(norm(q))")
    end
    return [q[1]/(1-q[4]),q[2]/(1-q[4]),q[3]/(1-q[4])]
end
function S³⭢ℝ³(q::Array{T,1},θ::Real) where T<:Real
    if length(q)≠4
        error("No Point on S³")
    elseif θ<ε
        return q[1:3]
    else
        c=[0,0,0,cot(θ)]
        r=1/sin(θ)
        return S³⭢ℝ³((q-c)/r)*tan((π/2-θ)/2+π/4)
    end
end
function ℝ⁴⭢S³(p::Array{T,1},θ::Real) where T<:Real
    if θ<ε
        return [p[1:3]...,0.0]
    else
        R=1/sin(θ)
        O=[0,0,0,cot(θ)]
        return O+R*normalize(p-O)
    end
end

function NormalVector(p₁::RealVector,p₂::RealVector,p₃::RealVector,p₄::RealVector)
    A=hcat(p₁-p₄,p₂-p₄,p₃-p₄)
    return normalize([(-1)^i*det(A[deleteat!(collect(1:4),i),:]) for i ∈ 1:4])
end

function Mirror(q::RealVector,p₁::RealVector,p₂::RealVector,p₃::RealVector)
    𝒏=JuliRay.NormalVector(p₁,p₂,p₃)
    return q-2*dot(𝒏,q-p₁)*𝒏
end
function Mirror(q::RealVector,p₁::RealVector,p₂::RealVector,p₃::RealVector,p₄::RealVector)
    𝒏=JuliRay.NormalVector(p₁,p₂,p₃,p₄)
    return q-2*dot(𝒏,q-p₁)*𝒏
end
function Mirror(q::RealVector,p₁::RealVector,p₂::RealVector,p₃::RealVector,p₄::RealVector, θ)
    𝒏=NormalVector(p₁,p₂,p₃,p₄)
    return ℝ⁴⭢S³(q-2*dot(𝒏,q-p₁)*𝒏,θ)
end

function PickFace(cell::CELL, v::RealVector,POINTS³)
    if norm(v) ≈ 0
        error("vector v must be non-zero")
    end
    faces=vertices.(cell)
    fpts=(i->POINTS³[i]).(faces)
    return cell[findmax([dot(v,+(pts...)) for pts ∈ fpts])[2]]
end

function NewCell(cell::CELL,v::RealVector,θ,POINTS³, POINTS⁴)
    n=length(POINTS³)
    c=copy(cell)
    face=PickFace(cell,v,POINTS³)
    IND_face=vertices(face)
    IND_cell=Int[]
    for f ∈ c for e ∈ f for v ∈ e push!(IND_cell,v) end end end
    IND_cell=union(IND_cell)
    IND_cell2=[
        if i ∈ IND_face
            i
        else
            push!(POINTS³,Mirror(POINTS³[i],POINTS³[IND_face[1]],POINTS³[IND_face[2]],POINTS³[IND_face[3]]));
            if θ<ε
                push!(POINTS⁴,[POINTS³[end]...,0]);
            else
                O=[0,0,0,cot(θ)]
                push!(POINTS⁴,Mirror(POINTS⁴[i],O,POINTS⁴[IND_face[1]],POINTS⁴[IND_face[2]],POINTS⁴[IND_face[3]],θ));
            end
            n=n+1
        end
        for i ∈ IND_cell]
    return [[[IND_cell2[findfirst(w->w==v,IND_cell)] for v ∈ e] for e ∈ f] for f ∈ c]
end

function SphericalSphere(v,r::Real,θ::Real) where T<:RealVector
    V=S³⭢ℝ³(v,θ)
    return Sphere(V,r)
end

function SphericalCylinder(v₁,v₂,r::Real,θ::Real) where T<:RealVector
    w₁=ℝ⁴⭢S³((v₁+v₂)/2,θ)
    V₁=S³⭢ℝ³(v₁,θ)
    V₂=S³⭢ℝ³(v₂,θ)
    W₁=S³⭢ℝ³(w₁,θ)
    return Arc(V₁,W₁,V₂,r)
end

function strictplane(ps::Array{Array{Float64,1}})
    m = length(ps) # m-gon
    c = mean(ps) # center
    δs = [ps[mod(i+2,m)+1] - ps[i] for i in 1:m] # edge
    qs = [ps[i]-c for i in 1:m]
    ns = [cross(qs[i],δs[i]) for i in 1:m] # normal
    n = normalize(mean(ns))
    ps′ = [qs[i]-dot(qs[i],n)*n+c for i in 1:m]
    return ps′
end

function SphericalPolygon(v::Array{T,1},θ::Real) where T<:RealVector
    n=length(v)
    u=ℝ⁴⭢S³(+(v...)/n,θ)
    v₁=v[1]
    v₂=v[2]
    v₃=v[3]
    V=(q->S³⭢ℝ³(q,θ)).(v)
    U=S³⭢ℝ³(u,θ)
    w=[ℝ⁴⭢S³((v[i]+v[mod(i,length(v))+1])/2,θ) for i ∈ 1:length(v)]
    W=(q->S³⭢ℝ³(q,θ)).(w)
    if θ<ε
    # if θ<10^(-3)
        return Polygon(V)
    elseif rank(hcat(V...),atol=1.0e-4)==2
    # elseif rank(hcat(V...),atol=1.0e-12)==2
        m=4
        vw=copy(v)
        # m-gon
        for _ ∈ 1:m
            l=length(vw)
            vw=[ℝ⁴⭢S³((vw[(i+1)÷2]+vw[mod(i÷2,l)+1])/2,θ) for i ∈ 1:2l]
        end
        VW=(q->S³⭢ℝ³(q,θ)).(vw)
        # return rgbftColor(csgUnion(Polygon(strictplane(VW))),RGB(1,0,0),FT(0.3,0.3))
        return csgUnion(Polygon(strictplane(VW)))
    else
        O=JuliRay.Circumcenter(U,V[1],V[2],V[3])
        sphere=Sphere(O,norm(U-O))
        N=JuliRay.NormalVector(V[1],W[1],V[2]);
        direction=sign(dot(N,O-U))

        cylinders=csgIntersection(
                (V₁=V[i];
                V₂=V[mod(i,n)+1];
                V₃=V[mod(i+1,n)+1];
                C=JuliRay.Circumcenter(V₁,W[i],V₂);
                N=JuliRay.NormalVector(V₁,W[i],V₂);
                direction=sign(dot(N,U-W[i]));
                cylinder=Cylinder(C,C+2*direction*norm(U-O)*N,norm(U-O)))
                for i ∈ 1:n)
        return csgClip(sphere,cylinders)
    end
end

function approxin(α,A)
    for a in A
        if norm(a - α) < 0.05
            return true
        end
    end
    return false
end

function Cells2Object(cells::Array{CELL,1},θ,POINTS⁴;rᵥ=0.05,rₑ=0.025,color=RGB(1,1,1))
    cs=copy(cells)
    # POINTS⁴=copy(POINTS⁴)
    fs=vcat(cs...)
    nf = length(fs)

    Cs = [S³⭢ℝ³(ℝ⁴⭢S³(mean([POINTS⁴[i] for i ∈ vertices(f)]),θ),θ) for f in fs]

    # get unique center points
    index_double_unique = []
    Cs_double = []
    Cs_dummy = []
    for i in 1:nf
        if approxin(Cs[i], Cs_dummy)
            push!(Cs_double,Cs[i])
            push!(index_double_unique,i)
        end
        push!(Cs_dummy,Cs[i])
    end

    # get double indices
    index_double = []
    for i in 1:nf
        if approxin(Cs[i], Cs_double)
            push!(index_double,i)
        end
    end

    # get single indices
    index_single = setdiff(1:nf, index_double)

    fs_double = fs[index_double_unique]
    fs_single = fs[index_single]

    # fs_b
    es=unique(vcat(fs...))
    vs=unique(vcat(es...))
    V=rgbColor(csgUnion([SphericalSphere(POINTS⁴[v],rᵥ,θ) for v ∈ vs]),RGB(0.1,0.1,0.1))
    E=rgbColor(csgUnion([SphericalCylinder(POINTS⁴[e[1]],POINTS⁴[e[2]],rₑ,θ) for e ∈ es]),RGB(0.2,0.2,0.2))
    # F=rgbftColor(csgUnion([SphericalPolygon([POINTS⁴[i] for i ∈ vertices(f)],θ) for f ∈ fs]),color,FT(0,0))
    # F=rgbftColor(csgUnion([SphericalPolygon([POINTS⁴[i] for i ∈ vertices(f)],θ) for f ∈ fs]),color,FT(0.4,0.2))
    F_double=rgbftColor2(csgUnion([SphericalPolygon([POINTS⁴[i] for i ∈ vertices(f)],θ) for f ∈ fs_double]),color,FT(0.6,0.3))
    F_single=rgbftColor(csgUnion([SphericalPolygon([POINTS⁴[i] for i ∈ vertices(f)],θ) for f ∈ fs_single]),color,FT(0.6,0.3))
    return csgUnion(V,E,F_double, F_single)
end

##%

function C₈(θ)
    h=1/2
    r=√(1-h^2)

    R=1/sin(θ)
    O=[0,0,0,cot(θ)]
    N=O+[0,0,0,R]
    H=O-[0,0,0,√(R^2-r^2)]

    POINTS, c₁=F₆
    POINTS=r*copy(POINTS)
    POINTS³=copy(POINTS)
    POINTS⁴=[[𝒑...,0]+H for 𝒑 ∈ POINTS]
    c₂=NewCell(c₁,[-1,0,0],θ,POINTS³, POINTS⁴)
    c₃=NewCell(c₂,[-1,0,0],θ,POINTS³, POINTS⁴)
    c₄=NewCell(c₁,[1,0,0],θ,POINTS³, POINTS⁴)
    c₅=NewCell(c₁,[0,0,-1],θ,POINTS³, POINTS⁴)
    c₆=NewCell(c₅,[0,-1,0],θ,POINTS³, POINTS⁴)
    c₇=NewCell(c₆,[0,-1,0],θ,POINTS³, POINTS⁴)
    c₈=NewCell(c₅,[0,1,0],θ,POINTS³, POINTS⁴)

    cells=[c₁,c₂,c₃,c₄,c₅,c₆,c₇,c₈]
    return cells, POINTS³, POINTS⁴
end

##%
M=120
for i ∈ 0:2M-1
    θ=π/2*(Smooth(0,1,i/M)-Smooth(1,2,i/M))
    cells, POINTS³, POINTS⁴=C₈(θ)
    render(Cells2Object(cells,θ,POINTS⁴,color=RGB(1.0,0.5,0.1),rₑ=0.012,rᵥ=0.03),camera=LngLatCamera(lng=180°+360°*i/M,lat=25°,pers=0.2,zoom=0.15,width=1200,height=900),name="8-Cell",index=i+1)
end

##%

function C₁₆(θ)
    h=1/2
    r=√(1-h^2)

    R=1/sin(θ)
    O=[0,0,0,cot(θ)]
    N=O+[0,0,0,R]
    H=O-[0,0,0,√(R^2-r^2)]

    POINTS, c₁=F₄
    POINTS=r*copy(POINTS)
    POINTS³=copy(POINTS)
    POINTS⁴=[[𝒑...,0]+H for 𝒑 ∈ POINTS]
    V=[1,-2,0]
    c₂=NewCell(c₁,V,θ,POINTS³, POINTS⁴)
    c₃=NewCell(c₂,V,θ,POINTS³, POINTS⁴)
    c₄=NewCell(c₃,V,θ,POINTS³, POINTS⁴)
    c₅=NewCell(c₁,-V,θ,POINTS³, POINTS⁴)
    c₆=NewCell(c₅,-V,θ,POINTS³, POINTS⁴)
    c₇=NewCell(c₆,-V,θ,POINTS³, POINTS⁴)
    c₈=NewCell(c₇,-V,θ,POINTS³, POINTS⁴)
    c₉=NewCell(c₁,[-2,-1,0],θ,POINTS³, POINTS⁴)
    W=[-1,-0.5,1]
    c₁₀=NewCell(c₉,W,θ,POINTS³, POINTS⁴)
    c₁₁=NewCell(c₁₀,W,θ,POINTS³, POINTS⁴)
    c₁₂=NewCell(c₁₁,W,θ,POINTS³, POINTS⁴)
    c₁₃=NewCell(c₉,-W,θ,POINTS³, POINTS⁴)
    c₁₄=NewCell(c₁₃,-W,θ,POINTS³, POINTS⁴)
    c₁₅=NewCell(c₁₄,-W,θ,POINTS³, POINTS⁴)
    c₁₆=NewCell(c₁₅,-W,θ,POINTS³, POINTS⁴)

    cells=[c₁,c₂,c₃,c₄,c₅,c₆,c₇,c₈,c₉,c₁₀,c₁₁,c₁₂,c₁₃,c₁₄,c₁₅,c₁₆]
    return cells, POINTS³, POINTS⁴
end

##%

function C₂₄(θ)
    h=1/√2
    r=√(1-h^2)

    R=1/sin(θ)
    O=[0,0,0,cot(θ)]
    N=O+[0,0,0,R]
    H=O-[0,0,0,√(R^2-r^2)]

    POINTS, c₁=F₈
    POINTS=r*copy(POINTS)
    POINTS³=copy(POINTS)
    POINTS⁴=[[𝒑...,0]+H for 𝒑 ∈ POINTS]
    c₂=NewCell(c₁,[1,1,1],θ,POINTS³, POINTS⁴)
    c₃=NewCell(c₂,[1,1,1],θ,POINTS³, POINTS⁴)
    c₄=NewCell(c₃,[1,1,1],θ,POINTS³, POINTS⁴)
    c₅=NewCell(c₁,-[1,1,1],θ,POINTS³, POINTS⁴)
    c₆=NewCell(c₅,-[1,1,1],θ,POINTS³, POINTS⁴)
    c₇=NewCell(c₁,[1,-1,-1],θ,POINTS³, POINTS⁴)
    c₈=NewCell(c₁,[-1,1,-1],θ,POINTS³, POINTS⁴)
    c₉=NewCell(c₁,[-1,-1,1],θ,POINTS³, POINTS⁴)
    c₁₀=NewCell(c₂,[-1,0,0],θ,POINTS³, POINTS⁴)
    c₁₁=NewCell(c₂,[0,-1,0],θ,POINTS³, POINTS⁴)
    c₁₂=NewCell(c₂,[0,0,-1],θ,POINTS³, POINTS⁴)
    c₁₃=NewCell(c₃,[1,-1,-1],θ,POINTS³, POINTS⁴)
    c₁₄=NewCell(c₃,[-1,1,-1],θ,POINTS³, POINTS⁴)
    c₁₅=NewCell(c₃,[-1,-1,1],θ,POINTS³, POINTS⁴)
    c₁₆=NewCell(c₄,[-1,0,0],θ,POINTS³, POINTS⁴)
    c₁₇=NewCell(c₄,[0,-1,0],θ,POINTS³, POINTS⁴)
    c₁₈=NewCell(c₄,[0,0,-1],θ,POINTS³, POINTS⁴)
    c₁₉=NewCell(c₆,[1,-1,-1],θ,POINTS³, POINTS⁴)
    c₂₀=NewCell(c₆,[-1,1,-1],θ,POINTS³, POINTS⁴)
    c₂₁=NewCell(c₆,[-1,-1,1],θ,POINTS³, POINTS⁴)
    c₂₂=NewCell(c₅,[-1,0,0],θ,POINTS³, POINTS⁴)
    c₂₃=NewCell(c₅,[0,-1,0],θ,POINTS³, POINTS⁴)
    c₂₄=NewCell(c₅,[0,0,-1],θ,POINTS³, POINTS⁴)

    cells=[c₁,c₂,c₃,c₄,c₅,c₆,c₇,c₈,c₉,c₁₀,c₁₁,c₁₂,c₁₃,c₁₄,c₁₅,c₁₆,c₁₇,c₁₈,c₁₉,c₂₀,c₂₁,c₂₂,c₂₃,c₂₄]
    return cells, POINTS³, POINTS⁴
end

##%

function C₁₂₀(θ)
    h=norm([φ^3,0,0,0])/norm([φ^3,1,1,1])
    r=√(1-h^2)

    R=1/sin(θ)
    O=[0,0,0,cot(θ)]
    N=O+[0,0,0,R]
    H=O-[0,0,0,√(R^2-r^2)]

    POINTS, c₁=F₁₂
    POINTS=r*copy(POINTS)
    POINTS³=copy(POINTS)
    POINTS⁴=[[𝒑...,0]+H for 𝒑 ∈ POINTS]
    Va,_=F₂₀
    Vb=[v-2*Va[1]*dot(Va[1],v) for v ∈ Va]
    c₂=NewCell(c₁,Va[1],θ,POINTS³, POINTS⁴)
    c₃=NewCell(c₂,Va[1],θ,POINTS³, POINTS⁴)
    c₄=NewCell(c₃,Va[1],θ,POINTS³, POINTS⁴)
    c₅=NewCell(c₄,Va[1],θ,POINTS³, POINTS⁴)
    c₆=NewCell(c₅,Va[1],θ,POINTS³, POINTS⁴)
    c₇=NewCell(c₁,-Va[1],θ,POINTS³, POINTS⁴)
    c₈=NewCell(c₇,-Va[1],θ,POINTS³, POINTS⁴)
    c₉=NewCell(c₈,-Va[1],θ,POINTS³, POINTS⁴)
    c₁₀=NewCell(c₉,-Va[1],θ,POINTS³, POINTS⁴)

    cells=[c₁,c₂,c₃,c₄,c₅,c₆,c₇,c₈,c₉,c₁₀]

    push!(cells,NewCell(cells[1],Va[3],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[1],Va[7],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[1],Va[8],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[1],Va[10],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[1],Va[12],θ,POINTS³, POINTS⁴))

    push!(cells,NewCell(cells[2],Vb[2],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[2],Vb[5],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[2],Vb[6],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[2],Vb[9],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[2],Vb[11],θ,POINTS³, POINTS⁴))

    push!(cells,NewCell(cells[3],Va[3],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[3],Va[7],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[3],Va[8],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[3],Va[10],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[3],Va[12],θ,POINTS³, POINTS⁴))

    push!(cells,NewCell(cells[4],Vb[2],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[4],Vb[5],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[4],Vb[6],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[4],Vb[9],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[4],Vb[11],θ,POINTS³, POINTS⁴))

    push!(cells,NewCell(cells[5],Va[3],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[5],Va[7],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[5],Va[8],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[5],Va[10],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[5],Va[12],θ,POINTS³, POINTS⁴))

    push!(cells,NewCell(cells[6],Vb[2],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[6],Vb[5],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[6],Vb[6],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[6],Vb[9],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[6],Vb[11],θ,POINTS³, POINTS⁴))

    push!(cells,NewCell(cells[7],Vb[2],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[7],Vb[5],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[7],Vb[6],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[7],Vb[9],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[7],Vb[11],θ,POINTS³, POINTS⁴))

    push!(cells,NewCell(cells[8],Va[3],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[8],Va[7],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[8],Va[8],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[8],Va[10],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[8],Va[12],θ,POINTS³, POINTS⁴))

    push!(cells,NewCell(cells[9],Vb[2],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[9],Vb[5],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[9],Vb[6],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[9],Vb[9],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[9],Vb[11],θ,POINTS³, POINTS⁴))

    push!(cells,NewCell(cells[10],Va[3],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[10],Va[7],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[10],Va[8],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[10],Va[10],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[10],Va[12],θ,POINTS³, POINTS⁴))

    push!(cells,NewCell(cells[18],Va[12],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[end],Va[5],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[end],-Va[11],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[end],-Va[11],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[end],-Va[11],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[end],-Va[11],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[end-4],Va[11],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[end],Va[11],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[end],Va[11],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[end],Va[11],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[end],Va[11],θ,POINTS³, POINTS⁴))

    push!(cells,NewCell(cells[17+45],Va[2],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[17+45],Va[4],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[17+45],Va[12],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[17+45],Vb[12],θ,POINTS³, POINTS⁴))

    push!(cells,NewCell(cells[18+45],Va[2],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[18+45],Va[12],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[18+45],Va[1],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[18+45],Va[6],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[18+45],Va[8],θ,POINTS³, POINTS⁴))

    push!(cells,NewCell(cells[19+45],[-1,-2,3],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[19+45],[1,-1,3],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[19+45],Vb[12],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[19+45],Vb[2],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[19+45],[-1,-1,-1],θ,POINTS³, POINTS⁴))

    push!(cells,NewCell(cells[20+45],Va[2],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[20+45],Va[12],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[20+45],Va[1],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[20+45],Va[6],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[20+45],Va[8],θ,POINTS³, POINTS⁴))

    push!(cells,NewCell(cells[21+45],[-1,-2,3],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[21+45],[1,-1,3],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[21+45],Vb[12],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[21+45],Vb[2],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[21+45],[-1,-1,-1],θ,POINTS³, POINTS⁴))

    push!(cells,NewCell(cells[22+45],Va[2],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[22+45],Va[12],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[22+45],Va[1],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[22+45],Va[6],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[22+45],Va[8],θ,POINTS³, POINTS⁴))

    push!(cells,NewCell(cells[23+45],[-1,-2,3],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[23+45],[1,-1,3],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[23+45],[2,-2,-1],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[23+45],Vb[2],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[23+45],[-1,-1,-1],θ,POINTS³, POINTS⁴))

    push!(cells,NewCell(cells[24+45],Va[2],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[24+45],Va[12],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[24+45],Va[1],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[24+45],Va[6],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[24+45],Va[8],θ,POINTS³, POINTS⁴))

    push!(cells,NewCell(cells[25+45],[-1,-2,3],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[25+45],[1,-1,3],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[25+45],[2,-2,-1],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[25+45],Vb[2],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[25+45],[-1,-1,-1],θ,POINTS³, POINTS⁴))

    push!(cells,NewCell(cells[26+45],Va[2],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[26+45],Va[12],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[26+45],Va[1],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[26+45],Va[6],θ,POINTS³, POINTS⁴))
    push!(cells,NewCell(cells[26+45],Va[8],θ,POINTS³, POINTS⁴))

    return cells, POINTS³, POINTS⁴
end

##
M=240
for i ∈ 0:2M-1
    θ=π/2*(Smooth(0,1,i/M)-Smooth(1,2,i/M))
    cells, POINTS³, POINTS⁴=C₁₆(θ)
    render(Cells2Object(cells,θ,POINTS⁴,color=RGB(0.1,1.0,0.1),rₑ=0.012,rᵥ=0.03),camera=LngLatCamera(lng=180°+360°*i/M,lat=25°,pers=0.2,zoom=0.15,width=1200,height=900),name="16-Cell",index=i+1)
end

##

M=240
for i ∈ 0:2M-1
    θ=π/2*(Smooth(0,1,i/M)-Smooth(1,2,i/M))
    cells, POINTS³, POINTS⁴=C₈(θ)
    render(Cells2Object(cells,θ,POINTS⁴,color=RGB(1.0,0.5,0.1),rₑ=0.012,rᵥ=0.03),camera=LngLatCamera(lng=180°+360°*i/M,lat=25°,pers=0.2,zoom=0.15,width=1200,height=900),name="8-Cell",index=i+1)
end

##

M=240
for i ∈ 0:2M-1
    θ=π/2*(Smooth(0,1,i/M)-Smooth(1,2,i/M))
    cells, POINTS³, POINTS⁴=C₂₄(θ)
    render(Cells2Object(cells,θ,POINTS⁴,color=RGB(1.0,0.1,1.0),rₑ=0.012,rᵥ=0.03),camera=LngLatCamera(lng=180°+360°*i/M,lat=25°,pers=0.2,zoom=0.12,width=1200,height=900),name="24-Cell",index=i+1)
end

##

M=240
for i ∈ 0:2M-1
    θ=π/2*(Smooth(0,1,i/M)-Smooth(1,2,i/M))
    cells, POINTS³, POINTS⁴=C₁₂₀(θ)
    try
        render(Cells2Object(cells,θ,POINTS⁴,color=RGB(0.1,0.1,1.0),rₑ=0.012,rᵥ=0.03),camera=LngLatCamera(lng=180°+360°*i/M,lat=25°,pers=0.2,zoom=0.12,width=1200,height=900),name="120-Cell",index=i+1)
    catch
    end
end

dropped = Int[]
M=150
for i ∈ 0:2M-1
    θ=π/2*(Smooth(0,1,i/M)-Smooth(1,2,i/M))
    cells, POINTS³, POINTS⁴=C₁₂₀(θ)
    try
        render(Cells2Object(cells,θ,POINTS⁴,color=RGB(0.1,0.3,1.0),rₑ=0.012,rᵥ=0.03),camera=LngLatCamera(lng=180°+180°*i/M,lat=25°,pers=0.2,zoom=0.075,width=500,height=500),name="120e-Cell",index=i+1)
    catch
        push!(dropped,i)
    end
end

dropped

for i ∈ 0:39
    θ=π/2*(Smooth(0,1,i/M)-Smooth(1,2,i/M))
    cells, POINTS³, POINTS⁴=C₁₂₀(θ)
    try
        # render(Cells2Object(cells,θ,POINTS⁴,color=RGB(0.1,0.3,1.0),rₑ=0.012,rᵥ=0.03),camera=LngLatCamera(lng=180°+360°*i/M,lat=25°,pers=0.2,zoom=0.075,width=500,height=500),name="120a-Cell",index=i+1)
        render(Cells2Object(cells,θ,POINTS⁴,color=RGB(0.1,0.3,1.0),rₑ=0.012,rᵥ=0.03),camera=LngLatCamera(lng=180°,lat=25°,pers=0.2,zoom=0.075,width=500,height=500),name="120b-Cell",index=i+1)
    catch
    end
end

try
    [1][3]
catch
end

## %
M=120
for i ∈ 0:2M-1
    θ=π/2*(Smooth(0,1,i/M)-Smooth(1,2,i/M))
    cells, POINTS³, POINTS⁴=C₈(θ)
    render(Cells2Object(cells,θ,POINTS⁴,color=RGB(0.2,1,1),rₑ=0.012,rᵥ=0.03),camera=LngLatCamera(lng=180°+360°*i/M,lat=25°,pers=0.2,zoom=0.15,width=1200,height=900),name="8-Cell",index=i+1)
end

M=120
for i ∈ 80
    θ=π/2*(Smooth(0,1,i/M)-Smooth(1,2,i/M))
    cells, POINTS³, POINTS⁴=C₁₆(θ)
    render(Cells2Object(cells,θ,POINTS⁴,color=RGB(0.2,1,1)),camera=LngLatCamera(lng=180°+360°*i/M,lat=25°,pers=0.2,zoom=0.12,width=600,height=450),name="16-Cell",index=i+1)
end

M=120
for i ∈ 50
    θ=π/2*(Smooth(0,1,i/M)-Smooth(1,2,i/M))
    cells, POINTS³, POINTS⁴=C₂₄(θ)
    render(Cells2Object(cells,θ,POINTS⁴,color=RGB(0.2,1,1)),camera=LngLatCamera(lng=180°+360°*i/M,lat=25°,pers=0.2,zoom=0.12,width=600,height=450),name="24-Cell",index=i+1)
end

M=120
i=24
θ=π/2*(Smooth(0,1,i/M)-Smooth(1,2,i/M))
cells, POINTS³, POINTS⁴=C₁₂₀(θ)
render(Cells2Object(cells,θ,POINTS⁴,color=RGB(0.2,1,1)),camera=LngLatCamera(lng=180°+360°*i/M,lat=25°,pers=0.2,zoom=0.12,width=600,height=450),name="120-CellB",index=i+1)

M=120
for i ∈ 0:2M
    θ=π/2*(Smooth(0,1,i/M)-Smooth(1,2,i/M))
    cells, POINTS³, POINTS⁴=C₁₂₀(θ)
    try
        render(Cells2Object(cells,θ,POINTS⁴,color=RGB(0.2,1,1)),camera=LngLatCamera(lng=180°+360°*i/M,lat=25°,pers=0.2,zoom=0.07,width=500,height=500),name="120-CellD",index=i+1)
    catch
        println(i)
    end
end

3


const me = 0.511 # MeV
const ec = 0.302811
const αem = ec^2/(4pi)

#################################################
# VECTOR SELF-ENERGY IN DILUTE NON-RELATIVISTIC GAS

# Plasma dispersion relation Z(xi) in a form valid for real arguments
function Zi(x)
	sqrt(pi)*exp(-x^2)
end

function Zr(x)
	-2*dawson(x)
end

# Real part of longitudinal photon polarization tensor in dilute, non-relativistic gas.
function ΠLr_dnr(q0,q,m,Z,n,T)
	s = sqrt(T/m)
	xi = q0/(sqrt(2)*q*s)
	qp = (q0^2 - q^2) / (2*q0*m)
	(
	 ((ec^2 * Z^2 * n) / (sqrt(2)*q*s)) *
	 (Zr(xi*(1 + qp)) - Zr(xi*(1 - qp)))
	 )
end

# Real part of transverse photon polarization tensor in dilute, non-relativistic gas.
function ΠTr_dnr(q0,q,m,Z,n,T)
	wp2 = ec^2 * Z^2 * n / m
	s = sqrt(T/m)
	xi = q0/(sqrt(2)*q*s)
	Q2 = q0^2 - q^2
	qp = Q2 / (2*q0*m)
	(
	 wp2*(1 - (s^2 - Q2/(4*m^2))*(m/q0)*xi*
		  (Zr(xi*(1 + qp)) - Zr(xi*(1 - qp))))
	 )
end

# Imaginary part of longitudinal photon polarization tensor in dilute, non-relativistic gas
function ΠLi_dnr(q0,q,m,Z,n,T)
	s = sqrt(T/m)
	xi = q0/(sqrt(2)*q*s)
	qp = (q0^2 - q^2) / (2*q0*m)
	(
	 ((ec^2 * Z^2 * n) / (sqrt(2)*q*s)) *
	 (Zi(xi*(1 + qp)) - Zi(xi*(1 - qp)))
	 )
end

# Imaginary part of transverse photon polarization tensor in dilute, non-relativistic gas
function ΠTi_dnr(q0,q,m,Z,n,T)
	wp2 = ec^2 * Z^2 * n / m
	s = sqrt(T/m)
	xi = q0/(sqrt(2)*q*s)
	Q2 = q0^2 - q^2
	qp = Q2 / (2*q0*m)
	(
	 -wp2*((s^2 - Q2/(4*m^2))*(m/q0)*xi*
		  (Zi(xi*(1 + qp)) - Zi(xi*(1 - qp))))
	 )
end

#################################################
# VECTOR SELF-ENERGY IN DEGENERATE FERMION GAS

# Imaginary part of longitudinal photon polarization tensor for degenerate electron gas.
function ΠLi_e_degen(q0,q,mue;mf=me)
	Q2 = q0^2 - q^2
	pre = -(ec^2/(4pi)) * (Q2/q^3)
	ep = max(mue,mf + q0,0.5*(q0 + q*sqrt(1 - 4*mf^2/Q2)))
	if ep > mue + q0
		0.
	else
		pre * (-((2*ep^3)/3) + (2*mue^3)/3 + ep^2*q0 + mue^2*q0 - q0^3/3
		 - (ep*Q2)/2 + (mue*Q2)/2 + (q0*Q2)/2)
	end
end

# Imaginary part of transverse photon polarization tensor for degenerate electron gas.
function ΠTi_e_degen(q0,q,mue;mf=me)
	Q2 = q0^2 - q^2
	pre = -(ec^2/(4pi)) * (1/q)
	ep = max(mue,mf + q0,0.5*(q0 + q*sqrt(1 - 4*mf^2/Q2)))
	if ep > mue + q0
		0.
	else
		(pre/12)*(-12*mf^2*(-ep + mue + q0) + 4*(-ep^3 + (mue + q0)^3) -
 (4*q0^2*(-ep^3 + (mue + q0)^3))/q^2 - 6*(-ep + mue + q0)*Q2 +
 (6*q0*(-ep^2 + (mue + q0)^2)*Q2)/q^2 - (3*(-ep + mue + q0)*Q2^2)/q^2)
	end
end

# Real part of longitudinal photon polarization tensor for degenerate electron gas.
# For internal integral definitions, see below.
function ΠLr_e_degen(q0,q,mue;mf=me)
	Q2 = q0^2 - q^2

	pf = sqrt(mue^2 - mf^2)

	ik = q*(-(mue*pf) + mf^2*log((mue + pf)/mf))

	i0m = integ0m(q0,q,mue,pf,mf,Q2)
	i1m = integ1m(q0,q,mue,pf,mf,Q2)
	i2m = integ2m(q0,q,mue,pf,mf,Q2)

	i0p = integ0p(q0,q,mue,pf,mf,Q2)
	i1p = integ1p(q0,q,mue,pf,mf,Q2)
	i2p = integ2p(q0,q,mue,pf,mf,Q2)

	int = (ik - (Q2/4)*i0m - i2m + q0*i1m
		   + i2p + (Q2/4)*i0p + q0*i1p)
	(Q2/q^3)*(ec^2/(2*pi^2))*int
end

# Real part of transverse photon polarization tensor for degenerate electron gas.
# For internal integral definitions, see below.
function ΠTr_e_degen(q0,q,mue;mf=me)
	Q2 = q0^2 - q^2

	pf = sqrt(mue^2 - mf^2)

	ik = -(1 + Q2/(2*q^2))*q*(-(mue*pf) + mf^2*log((mue + pf)/mf))

	i0m = integ0m(q0,q,mue,pf,mf,Q2)
	i1m = integ1m(q0,q,mue,pf,mf,Q2)
	i2m = integ2m(q0,q,mue,pf,mf,Q2)

	i0p = integ0p(q0,q,mue,pf,mf,Q2)
	i1p = integ1p(q0,q,mue,pf,mf,Q2)
	i2p = integ2p(q0,q,mue,pf,mf,Q2)

	int = (ik + (Q2/4 + Q2^2/(8*q^2) + mf^2/2)*(i0m - i0p)
		   + (Q2/(2*q^2))*(i2m-i2p) - q0*Q2*(i1m + i1p)/(2*q^2))
	(ec^2/(2*pi^2))*int/q
end

# Helper functions and internal integrals for real parts of polarization tensors in degenerate electron gas.

function logabs(x)
	log(abs(x))
end

function arcTanh(x)
	0.5*log(abs((1 + x)/(1 - x)))
end

function integ0m(q0,q,mue,pf,mf,Q2)
	QQ2 = q^2 + q0^2
	C1 = sqrt(Q2*(Q2 - 4*mf^2))
	((2*q*logabs((mue + pf)/mf) +
	 2*mue*logabs((-2*pf*q - 2*mue*q0 + Q2)/(2*pf*q - 2*mue*q0 + Q2)) +
	 ((4*mf^2*q*q0 - 2*q*q0*Q2 + C1*QQ2)*(logabs(mf) + logabs(-(C1*q) - 2*mue*Q2 + q0*Q2)))/
	 sqrt(Q2*(-2*q0*(C1*q + 2*mf^2*q0) + Q2*QQ2)) +
	 ((2*q*q0*(-2*mf^2 + Q2) + C1*QQ2)*(-logabs(mf) - logabs(C1*q - 2*mue*Q2 + q0*Q2)))/
	 sqrt(Q2*(2*C1*q*q0 - 4*mf^2*q0^2 + Q2*QQ2)) -
	 ((4*mf^2*q*q0 - 2*q*q0*Q2 + C1*QQ2)*logabs(C1*mue*q + 2*mf^2*Q2 - mue*q0*Q2 +
											 pf*sqrt(Q2*(-2*C1*q*q0 - 4*mf^2*q0^2 + Q2*QQ2))))/
	 sqrt(Q2*(-2*C1*q*q0 - 4*mf^2*q0^2 + Q2*QQ2)) +
	 ((-4*mf^2*q*q0 + 2*q*q0*Q2 + C1*QQ2)*logabs(C1*mue*q - 2*mf^2*Q2 + mue*q0*Q2 -
											  pf*sqrt(Q2*(2*C1*q*q0 - 4*mf^2*q0^2 + Q2*QQ2))))/
	 sqrt(Q2*(2*C1*q*q0 - 4*mf^2*q0^2 + Q2*QQ2)))/2)
end

function integ0p(q0,q,mue,pf,mf,Q2)
	QQ2 = q^2 + q0^2
	C1 = sqrt(Q2*(Q2 - 4*mf^2))

	((-2*q*logabs((mue + pf)/mf) +
  2*mue*logabs((2*pf*q + 2*mue*q0 + Q2)/(-2*pf*q + 2*mue*q0 + Q2)) +
  ((4*mf^2*q*q0 - 2*q*q0*Q2 - C1*QQ2)*(logabs(mf) + logabs(-(C1*q) - (2*mue + q0)*Q2)))/
   sqrt(Q2*(2*C1*q*q0 - 4*mf^2*q0^2 + Q2*QQ2)) -
  ((4*mf^2*q*q0 - 2*q*q0*Q2 + C1*QQ2)*(-logabs(mf) - logabs(C1*q - (2*mue + q0)*Q2)))/
   sqrt(Q2*(-2*q0*(C1*q + 2*mf^2*q0) + Q2*QQ2)) -
  ((4*mf^2*q*q0 - 2*q*q0*Q2 + C1*QQ2)*logabs(C1*mue*q - 2*mf^2*Q2 - mue*q0*Q2 -
      pf*sqrt(Q2*(-2*C1*q*q0 - 4*mf^2*q0^2 + Q2*QQ2))))/
   sqrt(Q2*(-2*C1*q*q0 - 4*mf^2*q0^2 + Q2*QQ2)) +
  ((-4*mf^2*q*q0 + 2*q*q0*Q2 + C1*QQ2)*logabs(C1*mue*q + 2*mf^2*Q2 + mue*q0*Q2 +
      pf*sqrt(Q2*(2*C1*q*q0 - 4*mf^2*q0^2 + Q2*QQ2))))/
  sqrt(Q2*(2*C1*q*q0 - 4*mf^2*q0^2 + Q2*QQ2)))/2)
end

function integ1m(q0,q,mue,pf,mf,Q2)
	QQ2 = q^2 + q0^2
	C1 = sqrt(Q2*(Q2 - 4*mf^2))
	((pf*q + (q*q0*(-2*mf^2 + Q2)*arcTanh(mue/pf))/Q2 -
		((8*q0*Q2*(-2*mf^2 + Q2)*(4*mf^2*q^2 + Q2^2) + (4*C1*q - 4*q0*Q2)*
		  (4*mf^2*(q^4 + q^2*q0^2 - 2*q0^4) + (q^2 + 3*q0^2)*Q2^2))*
		 sqrt(Q2*(-2*C1*q*q0 - 4*mf^2*q0^2 + Q2*QQ2))*
		 arcTanh((-(C1*mue*q) - 2*mf^2*Q2 + mue*q0*Q2)/(sqrt(-mf^2 + mue^2)*
														sqrt(Q2*(-2*C1*q*q0 - 4*mf^2*q0^2 + Q2*QQ2)))))/
		(C1*Q2*(-64*mf^2*Q2^2 + (4*C1*q - 4*q0*Q2)^2)) -
		((16*mf^4*q0*(q^3 - q*q0^2)^2 + Q2^3*(C1*(q^3 + 3*q*q0^2) + q0*(3*q^2 + q0^2)*Q2) -
		  4*mf^2*Q2^2*(C1*(q^3 + 2*q*q0^2) + q0*(4*q^2 + q0^2)*Q2))*
		 arcTanh((C1*mue*q - 2*mf^2*Q2 + mue*q0*Q2)/(sqrt(-mf^2 + mue^2)*
													 sqrt(Q2*(2*C1*q*q0 - 4*mf^2*q0^2 + Q2*QQ2)))))/
		(4*C1*Q2^2*sqrt(Q2*(2*C1*q*q0 - 4*mf^2*q0^2 + Q2*QQ2))) +
		mue^2*logabs((-2*pf*q - 2*mue*q0 + Q2)/(2*pf*q - 2*mue*q0 + Q2)))/2)
end

function integ1p(q0,q,mue,pf,mf,Q2)
	QQ2 = q^2 + q0^2
	C1 = sqrt(Q2*(Q2 - 4*mf^2))
	((-(pf*q) + (q*q0*(-2*mf^2 + Q2)*arcTanh(mue/pf))/Q2 +
  ((8*q0*Q2*(-2*mf^2 + Q2)*(4*mf^2*q^2 + Q2^2) - (-4*C1*q + 4*q0*Q2)*
      (4*mf^2*(q^4 + q^2*q0^2 - 2*q0^4) + (q^2 + 3*q0^2)*Q2^2))*
    sqrt(Q2*(-2*C1*q*q0 - 4*mf^2*q0^2 + Q2*QQ2))*
    arcTanh((C1*mue*q - (2*mf^2 + mue*q0)*Q2)/
      (pf*sqrt(Q2*(-2*C1*q*q0 - 4*mf^2*q0^2 + Q2*QQ2)))))/
   (C1*Q2*(-64*mf^2*Q2^2 + (4*C1*q - 4*q0*Q2)^2)) +
  ((16*mf^4*q0*(q^3 - q*q0^2)^2 + Q2^3*(C1*(q^3 + 3*q*q0^2) + q0*(3*q^2 + q0^2)*Q2) -
     4*mf^2*Q2^2*(C1*(q^3 + 2*q*q0^2) + q0*(4*q^2 + q0^2)*Q2))*
    arcTanh((-2*mf^2*Q2 - mue*(C1*q + q0*Q2))/
      (pf*sqrt(Q2*(2*C1*q*q0 - 4*mf^2*q0^2 + Q2*QQ2)))))/
   (4*C1*Q2^2*sqrt(Q2*(2*C1*q*q0 - 4*mf^2*q0^2 + Q2*QQ2))) +
  mue^2*logabs((2*pf*q + 2*mue*q0 + Q2)/(-2*pf*q + 2*mue*q0 + Q2)))/2)
end

function integ2m(q0,q,mue,pf,mf,Q2)
	QQ2 = q^2 + q0^2
	C1 = sqrt(Q2*(Q2 - 4*mf^2))
	((2*mue*pf*q + (4*pf*q*q0*(-2*mf^2 + Q2))/Q2 +
  2*mf^2*q*arcTanh(mue/pf) +
  (q*(4*mf^2*(q^4 + q^2*q0^2 - 2*q0^4) + (q^2 + 3*q0^2)*Q2^2)*
    arcTanh(mue/pf))/Q2^2 -
  (8*sqrt(Q2*(-2*C1*q*q0 - 4*mf^2*q0^2 + Q2*QQ2))*
    (Q2^2*(4*mf^2*q^2 + Q2^2)*(4*mf^2*(q^4 + q^2*q0^2 - 2*q0^4) +
       (q^2 + 3*q0^2)*Q2^2) + q0*(4*C1*q - 4*q0*Q2)*(4*mf^4*(q^3 - q*q0^2)^2 +
       mf^2*(5*q^4 - 2*q^2*q0^2 - 3*q0^4)*Q2^2 + Q2^4*QQ2))*
    arcTanh((-(C1*mue*q) - 2*mf^2*Q2 + mue*q0*Q2)/(sqrt(-mf^2 + mue^2)*
       sqrt(Q2*(-2*C1*q*q0 - 4*mf^2*q0^2 + Q2*QQ2)))))/
   (C1*Q2^3*(-64*mf^2*Q2^2 + (4*C1*q - 4*q0*Q2)^2)) +
  (sqrt(Q2*(2*C1*q*q0 - 4*mf^2*q0^2 + Q2*QQ2))*
    (Q2^2*(4*mf^2*q^2 + Q2^2)*(4*mf^2*(q^4 + q^2*q0^2 - 2*q0^4) +
       (q^2 + 3*q0^2)*Q2^2) - 4*q0*(C1*q + q0*Q2)*(4*mf^4*(q^3 - q*q0^2)^2 +
       mf^2*(5*q^4 - 2*q^2*q0^2 - 3*q0^4)*Q2^2 + Q2^4*QQ2))*
    arcTanh((C1*mue*q - 2*mf^2*Q2 + mue*q0*Q2)/(sqrt(-mf^2 + mue^2)*
       sqrt(Q2*(2*C1*q*q0 - 4*mf^2*q0^2 + Q2*QQ2)))))/
   (2*C1*Q2^3*(-4*mf^2*Q2^2 + (C1*q + q0*Q2)^2)) +
   4*mue^3*logabs((-2*pf*q - 2*mue*q0 + Q2)/(2*pf*q - 2*mue*q0 + Q2)))/12)
end

function integ2p(q0,q,mue,pf,mf,Q2)
	QQ2 = q^2 + q0^2
	C1 = sqrt(Q2*(Q2 - 4*mf^2))
	((-2*mue*pf*q + (4*pf*q*q0*(-2*mf^2 + Q2))/Q2 - 2*mf^2*q*arcTanh(mue/pf) -
  (q*(4*mf^2*(q^4 + q^2*q0^2 - 2*q0^4) + (q^2 + 3*q0^2)*Q2^2)*arcTanh(mue/pf))/Q2^2 -
  (sqrt(Q2*(-2*C1*q*q0 - 4*mf^2*q0^2 + Q2*QQ2))*
    (8*Q2^2*(4*mf^2*q^2 + Q2^2)*(4*mf^2*(q^4 + q^2*q0^2 - 2*q0^4) +
       (q^2 + 3*q0^2)*Q2^2) - 8*q0*(-4*C1*q + 4*q0*Q2)*(4*mf^4*(q^3 - q*q0^2)^2 +
       mf^2*(5*q^4 - 2*q^2*q0^2 - 3*q0^4)*Q2^2 + Q2^4*QQ2))*
    arcTanh((C1*mue*q - (2*mf^2 + mue*q0)*Q2)/
      (pf*sqrt(Q2*(-2*C1*q*q0 - 4*mf^2*q0^2 + Q2*QQ2)))))/
   (C1*Q2^3*(-64*mf^2*Q2^2 + (4*C1*q - 4*q0*Q2)^2)) +
  (sqrt(Q2*(2*C1*q*q0 - 4*mf^2*q0^2 + Q2*QQ2))*
    (8*Q2^2*(4*mf^2*q^2 + Q2^2)*(4*mf^2*(q^4 + q^2*q0^2 - 2*q0^4) +
       (q^2 + 3*q0^2)*Q2^2) - 32*q0*(C1*q + q0*Q2)*(4*mf^4*(q^3 - q*q0^2)^2 +
       mf^2*(5*q^4 - 2*q^2*q0^2 - 3*q0^4)*Q2^2 + Q2^4*QQ2))*
    arcTanh((-2*mf^2*Q2 - mue*(C1*q + q0*Q2))/
      (pf*sqrt(Q2*(2*C1*q*q0 - 4*mf^2*q0^2 + Q2*QQ2)))))/
   (16*C1*Q2^3*(-4*mf^2*Q2^2 + (C1*q + q0*Q2)^2)) +
  4*mue^3*logabs((2*pf*q + 2*mue*q0 + Q2)/(-2*pf*q + 2*mue*q0 + Q2)))/12)
end


export
ruleSPBetaOutNMM,
ruleSPBetaOutMCNMM,
ruleSPBetaMNM,
ruleSPBetaMMN,
ruleVBBetaOut,
ruleVBBetaA,
ruleVBBetaB

ruleSPBetaOutNMM(msg_out::Nothing, msg_a::Message{PointMass, Univariate}, msg_b::Message{PointMass, Univariate}) = Message(Univariate, Beta, a=deepcopy(msg_a.dist.params[:m]), b=deepcopy(msg_b.dist.params[:m]))

function ruleSPBetaOutMCNMM(msg_out::Nothing, msg_a::Message{F1, Univariate}, msg_b::Message{F2, Univariate}) where {F1<:FactorNode, F2<:FactorNode}
    n_samples = default_n_samples
    samples_a = sample(msg_a.dist,n_samples)
    samples_b = sample(msg_b.dist,n_samples)
    s_list = betainvcdf.(samples_a, samples_b, rand(n_samples))
    w_list = ones(n_samples)/n_samples
    Message(Univariate,SampleList,s=s_list,w=w_list)
end

# 1000 (default_n_samples) samples is chosen arbitrarily. Better to be defined by the user.
function ruleSPBetaMNM(msg_out::Message{F1,Univariate}, msg_a::Nothing, msg_b::Message{F2,Univariate}) where {F1<:FactorNode, F2<:FactorNode}
    n_samples = default_n_samples
    samples_out = sample(msg_out.dist,n_samples)
    samples_b = sample(msg_b.dist,n_samples)
    logp(x,a,b) = (a-1)*log(x) + (b-1)*log(1.0-x) - loggamma(a) - loggamma(b) + loggamma(a+b)
    logp(a) = sum(logp.(samples_out,a,samples_b))/n_samples
    Message(Univariate, Function, log_pdf = logp)
end

function ruleSPBetaMMN(msg_out::Message{F1,Univariate}, msg_a::Message{F2,Univariate}, msg_b::Nothing) where {F1<:FactorNode, F2<:FactorNode}
    n_samples = default_n_samples
    samples_out = sample(msg_out.dist,n_samples)
    samples_a = sample(msg_a.dist,n_samples)
    logp(x,a,b) = (a-1)*log(x) + (b-1)*log(1.0-x) - loggamma(a) - loggamma(b) + loggamma(a+b)
    logp(b) = sum(logp.(samples_out,samples_a,b))/n_samples
    Message(Univariate, Function, log_pdf = logp)
end

ruleVBBetaOut(marg_out::Any, dist_a::ProbabilityDistribution{Univariate}, dist_b::ProbabilityDistribution{Univariate}) = Message(Univariate, Beta, a=unsafeMean(dist_a), b=unsafeMean(dist_b))

function ruleVBBetaA( dist_out::ProbabilityDistribution{Univariate},
    dist_a::Any, dist_b::ProbabilityDistribution{Univariate})

    logGamAsumB(a) = sum(loggamma.(a.+sample(dist_b,default_n_samples)))/default_n_samples
    logp(a) = (a-1)*unsafeLogMean(dist_out) + logGamAsumB(a) - loggamma(a)

    Message(Univariate, Function, log_pdf = logp)
end

function ruleVBBetaB( dist_out::ProbabilityDistribution{Univariate},
    dist_a::ProbabilityDistribution{Univariate}, dist_b::Any)

    logGamAsumB(b) = sum(loggamma.(b.+sample(dist_a,default_n_samples)))/default_n_samples
    logp(b) = (b-1)*unsafeMirroredLogMean(dist_out) + logGamAsumB(b) - loggamma(b)

    Message(Univariate, Function, log_pdf = logp)
end


# -*- coding: utf-8 -*-
#
# Calculate the percentile of the gradient image, which
# used as the 'kappa' parameter of the anisotropic diffusion.
#
# Credits:
# [1] KAZE: nldiffusion_functions.cpp / compute_k_percentile()
#
# Aaron LI
# 2015/07/20
#

include("scharr.jl");

function calc_k_percentile(img, percent=0.7, nbins=300)
    rows, cols = size(img)
    # derivatives of the image
    img_gx = scharr(img, 1, 0)
    img_gy = scharr(img, 0, 1)
    img_modg = sqrt(img_gx.^2 + img_gy.^2)
    # histogram
    hmax = maximum(img_modg)
    hist_e, hist_counts = hist(reshape(img_modg, length(img_modg)), nbins)
    hist_cum = cumsum(hist_counts)
    # find the percent of the histogram percentile
    npoints = sum(img_modg .> 0.0)
    nthreshold = npoints * percent
    k = sum(hist_cum .<= nthreshold)
    kperc = (k == length(hist_cum)) ? 0.03 : (hmax * k / nbins)
    return kperc
end


using CompScienceMeshes
using BEAST

Γ1 = readmesh(joinpath(dirname(pathof(BEAST)),"../examples/sphere2.in"))
Γ2 = CompScienceMeshes.translate(Γ1, [10,0,0])

SL = Maxwell3D.singlelayer(wavenumber=1.0)
X1 = raviartthomas(Γ1)
X2 = raviartthomas(Γ2)

x1 = refspace(X1)
x2 = refspace(X2)

function BEAST.quaddata(op::typeof(SL), tref::typeof(x1), bref::typeof(x2),
    tels, bels, qs::BEAST.DoubleNumQStrat)

    qs = BEAST.DoubleNumWiltonSauterQStrat(qs.outer_rule, qs.inner_rule, 1, 1, 1, 1, 1, 1)
    BEAST.quaddata(op, tref, bref, tels, bels, qs)
end

function BEAST.quadrule(op::typeof(SL), tref::typeof(x1), bref::typeof(x2),
    i ,τ, j, σ, qd, qs::BEAST.DoubleNumQStrat)

    return BEAST.DoubleQuadRule(
        qd.tpoints[1,i],
        qd.bpoints[1,j])
end

BEAST.quadinfo(SL,X1,X2)
BEAST.quadinfo(SL,X1,X2, quadstrat=BEAST.DoubleNumQStrat(2,3))

@time Z1 = assemble(SL,X1,X2);
@time Z2 = assemble(SL,X1,X2,quadstrat=BEAST.DoubleNumQStrat(2,3));

ba1 = blockassembler(SL,X1,X2)
ba2 = blockassembler(SL,X1,X2,quadstrat=BEAST.DoubleNumQStrat(2,3))

T = scalartype(SL,X1,X2)
n1 = numfunctions(X1)
n2 = numfunctions(X2)

W1 = zeros(T,n1,n2);
W2 = zeros(T,n1,n2);

idcs1 = collect(1:n1)
idcs2 = collect(1:n2)
@time ba1(idcs1,idcs2, (v,m,n)->(W1[m,n]+=v))
@time ba2(idcs1,idcs2, (v,m,n)->(W2[m,n]+=v));


# Author: Gabrio Rizzuti, rizzuti.gabrio@gatech.edu
# Date: February, 2020
# Copyright: Georgia Institute of Technology, 2020
#
# Test forward modeling

using LinearAlgebra, PyPlot
push!(LOAD_PATH, "./src/"); using WaveOpsAVP

# Model
nz = 256
dz = R(5)
m_geom = ModelGeom(nz, dz)
nb = 8
m = R(1/2000^2)*ones(R, nz, nb)
m[102:end, :] .= R(1/2000^2)
# m[52:end, 2] .= R(1/2100^2)
# m[152:end, 3] .= R(1/2200^2)
M = Model(m_geom, m)

# Data parameters
nt = 1000
dt = R(0.001)
θ = range(R(0), stop = R(sqrt(3)/2), length = 10)
p = sin.(θ)*sqrt(max(m...))
dat_pars = DataParams(nt, dt, p)

# Source parameters
wav = cfreq_wavelet(50, nt, dt, (R(5), R(10), R(30), R(60)))
src_pars = SrcParams(wav, dt)

# ABC parameters
abc_size = (20, 40)
abc_fact = R(0.0001)
abc_geom = ABC_Geom(nz, abc_size, abc_fact)

# Modeling operator
acc_mod = 4
F = seismod_fun(dat_pars, src_pars; abc_geom = abc_geom, acc = acc_mod)

# Modeling
@time dat = F.eval(M; store_extracomp = true);

# # Plot
# figure(); imshow(dat.d[:, :, 1], aspect = "auto")


using PEG

function do_op(x)
  local v
  local op_v_pairs
  v, op_v_pairs = x
  for op_v ∈ op_v_pairs
    op, v2 = op_v
    v = eval(Symbol(op))(v, v2)
  end
  v
end

@rule num = r"\d+"w |> x->parse(Int, x) , r"\("p & add & r"\)"p |> x->x[2]
@rule mul = num & (r"[/*]"p & num)[*] |> do_op
@rule add = mul & (r"[+-]"p & mul)[*] |> do_op

using Test
@test add("2 + 3 * 5") == (17, "")
@test add("2 * 3 + 5") == (11, "")
@test add("(2 + 3) * 5") == (25, "")

@test try
  parse_whole(add, "1 + 2 ^ 3")
catch x
  split(x.msg, "\n")[1]
end == "On line 1, at column 7 (byte 7):"


using QuantLib

generate_flatforward_ts{C <: QuantLib.Time.BusinessCalendar}(cal::C, settlementDate::Date, flatRate::Quote) =
                        FlatForwardTermStructure(settlementDate, cal, flatRate, QuantLib.Time.Actual365())

function main()
  cal = QuantLib.Time.TargetCalendar()
  todays_date = Date(2007, 5, 15)
  settlementDate = todays_date
  set_eval_date!(settings, todays_date)

  flatRate = Quote(0.01)

  tsCurve = generate_flatforward_ts(cal, settlementDate, flatRate)

  recoveryRate = 0.5
  quoteSpreads = [0.0150, 0.0150, 0.0150, 0.0150]
  tenors = [Dates.Month(3), Dates.Month(6), Dates.Year(1), Dates.Year(2)]

  maturities = [QuantLib.Time.adjust(cal, QuantLib.Following(), todays_date + ten) for ten in tenors]

  insts = SpreadCDSHelper[SpreadCDSHelper(Quote(quoteSpreads[i]), tenors[i], 0, cal, QuantLib.Time.Quarterly(), QuantLib.Time.Following(), QuantLib.Time.DateGenerationTwentieth(),
            QuantLib.Time.Actual365(), recoveryRate, tsCurve) for i in eachindex(tenors)]

  hazardRateStructure = PiecewiseDefaultCurve(todays_date, insts, QuantLib.Time.Actual365(), QuantLib.Math.BackwardFlatInterpolation(), HazardRate(), 1.0e-12)

  hr_curve_data = nodes(hazardRateStructure)

  println(@sprintf("1Y Survival Probability: %.6f %%", survival_probability(hazardRateStructure, todays_date + Dates.Year(1)) * 100.0))
  println(@sprintf("2Y Survival Probability: %.6f %%", survival_probability(hazardRateStructure, todays_date + Dates.Year(2)) * 100.0))
end


using Test
using MIPVerify
using MIPVerify: UnrestrictedPerturbationFamily, BlurringPerturbationFamily, LInfNormBoundedPerturbationFamily
@isdefined(TestHelpers) || include("../../../TestHelpers.jl")

@timed_testset "conv+fc+softmax.jl" begin
    ### Parameters for neural net
    batch = 1
    c1_in_height = 6
    c1_in_width = 6
    p1_stride_height = 2
    p1_stride_width = 2
    p1_strides = (1, p1_stride_height, p1_stride_width, 1)
    p1_height = round(Int, c1_in_height/p1_stride_height, RoundUp)
    p1_width = round(Int, c1_in_width/p1_stride_width, RoundUp)
    c1_in_channels = 1
    c1_filter_height = 2
    c1_filter_width = 2
    c1_out_channels = 2

    l1_height = 4
    l1_width = p1_height*p1_width*c1_out_channels

    l2_height = 3
    l2_width = l1_height

    ### Choosing data to be used
    input = gen_array(
        (batch, c1_in_height, c1_in_width, c1_in_channels),
        0, 1
    )

    c1_kernel = gen_array(
        (c1_filter_height, c1_filter_width, c1_in_channels, c1_out_channels),
        -1, 1
    )
    c1_bias = gen_array((c1_out_channels,), -1, 1)

    l1_kernel = gen_array((l1_width, l1_height), -1, 1)
    l1_bias = gen_array((l1_height,), -1, 1)

    l2_kernel = gen_array((l2_width, l2_height), -1, 1)
    l2_bias = gen_array((l2_height,), -1, 1)

    nn = Sequential(
        [
            Conv2d(c1_kernel, c1_bias), MaxPool(p1_strides), ReLU(),
            Flatten(4),
            Linear(l1_kernel, l1_bias), ReLU(),
            Linear(l2_kernel, l2_bias)
        ],
        "tests.integration.generated_weights.conv+fc+softmax"
    )

    @timed_testset "BlurringPerturbationFamily" begin
        pp_blur = BlurringPerturbationFamily((5, 5))
        # TODO (vtjeng): Add example where blurring perturbation generates non-NaN results
        test_cases = [
            ((2, pp_blur, 1, 0), 6.959316),
            ((3, pp_blur, 1, 0), NaN),
        ]

        TestHelpers.batch_test_adversarial_example(nn, input, test_cases)
    end

    @timed_testset "UnrestrictedPerturbationFamily" begin
        pp_unrestricted = UnrestrictedPerturbationFamily()
        @timed_testset "Minimizing l1 norm" begin
            test_cases = [
                ((1, pp_unrestricted, 1, 0), 0),
                ((2, pp_unrestricted, 1, 0), 0.9187638),
            ]

            TestHelpers.batch_test_adversarial_example(nn, input, test_cases)
        end

        @timed_testset "Minimizing lInf norm" begin
            test_cases = [
                ((1, pp_unrestricted, Inf, 0), 0),
                ((2, pp_unrestricted, Inf, 0), 0.06688736),
                ((3, pp_unrestricted, Inf, 0), 0.4270584),
            ]

            TestHelpers.batch_test_adversarial_example(nn, input, test_cases)
        end

        @timed_testset "Increasing margin increases required distance" begin
            test_cases = [
                ((2, pp_unrestricted, 1, 0.1), 0.98717927),
            ]

            TestHelpers.batch_test_adversarial_example(nn, input, test_cases)
        end

        @timed_testset "With multiple target labels specified, minimum target label found" begin
            test_cases = [
                (([2, 3], pp_unrestricted, Inf, 0), 0.06688736),
            ]

            TestHelpers.batch_test_adversarial_example(nn, input, test_cases)
        end
    end

    @timed_testset "LInfNormBoundedPerturbationFamily" begin
        test_cases = [
            ((2, LInfNormBoundedPerturbationFamily(0.06), Inf, 0), NaN),  # restricting maximum perturbation to below minimum distance causes optimization problem to be infeasible
            ((2, LInfNormBoundedPerturbationFamily(0.07), Inf, 0), 0.06688736),  # restricting maximum perturbation to above minimum distance does not affect optimal value of problem
        ]

        TestHelpers.batch_test_adversarial_example(nn, input, test_cases)
    end
end


using CharFuncPricing, Test, Printf
using StatsBase

@testset "HestonLong" begin
τ=10.0
forward= 10000.0
params=HestonParams(1.0, 2.0, 0.0025, 0.5, 0.1)
strikes = [100.0001, 101.0, 110.0, 200.0, 1000.0, 10000.0]
cf = DefaultCharFunc(params)
quadratureGL = ModlobQuadrature(1e-13)
pricers = [ALCharFuncPricer(cf, quadratureGL), makeCosCharFuncPricer(cf, τ, 200, 8),
 makeCosCharFuncPricer(cf, τ, 8, tol=1e-8), FlinnCharFuncPricer(cf, τ, tTol = 1e-10, qTol=1e-10),
 FlinnCharFuncPricer(cf, τ, tTol = 1e-40, qTol=1e-10), AdaptiveFlinnCharFuncPricer(cf, τ, qTol=1e-8), ALCharFuncPricer(cf, n=200)]
pricerNames = ["Reference", "Cos (M=200)", "Cos-Adaptive", "Flinn-Truncated (1e-8)", "Flinn-Truncated (1e-40)", "Flinn-Transformed", "Andersen-Lake (n=200)"]
isCall = false
refPrices = map(x-> priceEuropean(pricers[1], isCall, x, forward, τ, 1.0),strikes)
for price in refPrices
    @printf("& %.8e", price)
end
println()
for (pricer,name) in zip(pricers,pricerNames)
    prices = map(x -> priceEuropean(pricer, isCall, x, forward, τ, 1.0), strikes)
    print(name," ")
    for (price,refPrice) in zip(prices,refPrices)
        @printf("& %.2e", price - refPrice)
    end
    println("\\\\")
end
prices = map(x -> priceEuropean(pricers[4], isCall, x, forward, τ, 1.0), strikes)
#@test isapprox(prices[1]-refPrices[1],1.97, atol=1e-2) #bad price for Flinn
@test isapprox(refPrices[1],prices[1], atol=1e-7)
prices = map(x -> priceEuropean(pricers[6], isCall, x, forward, τ, 1.0), strikes)
@test isapprox(prices[1], refPrices[1], atol=1e-7)
prices = map(x -> priceEuropean(pricers[7], isCall, x, forward, τ, 1.0), strikes)
@test isapprox(prices[1], refPrices[1], atol=1e-15)

τ=10.0
forward = 100.0
params = HestonParams(0.0001, 0.1, 0.25, 0.95, 3.0)
strike = 100.0001
cf = DefaultCharFunc(params)
pricers = [ALCharFuncPricer(cf, quadratureGL),  makeCosCharFuncPricer(cf, τ, 8),
makeCosCharFuncPricer(cf, τ, 200, 8), makeCosCharFuncPricer(cf, τ, 1000, 8),
 FlinnCharFuncPricer(cf, τ, tTol = 1e-10, qTol=1e-10),ALCharFuncPricer(cf, n=200)]
 pricerNames = ["Reference",  "Cos-Adaptive","Cos (M=200)","Cos (M=1000)",
  "Flinn-Truncated (1e-10)",  "Andersen-Lake (n=200)"]

isCall = false
refPrices = map(x-> priceEuropean(pricers[1], isCall, x, forward, τ, 1.0),strikes)
for price in refPrices
    @printf("& %.8e", price)
end
println()
for (pricer,name) in zip(pricers,pricerNames)
    prices = map(x -> priceEuropean(pricer, isCall, x, forward, τ, 1.0), strikes)
    print(name," ")
    for (price,refPrice) in zip(prices,refPrices)
        @printf("& %.2e", price - refPrice)
    end
    println("\\\\")
end
prices = map(x -> priceEuropean(pricers[4], isCall, x, forward, τ, 1.0), strikes)
@test isapprox(prices[1]-refPrices[1],0.27, atol=1e-2) #bad price for Cos
prices = map(x -> priceEuropean(pricers[2], isCall, x, forward, τ, 1.0), strikes)
@test isapprox(prices[1], refPrices[1], atol=1e-4) # bad accuracy for adaptive Cos
prices = map(x -> priceEuropean(pricers[5], isCall, x, forward, τ, 1.0), strikes)
@test isapprox(prices[1], refPrices[1], atol=1e-7)

#10.0 0.0001 0.01 1.0 -0.95 3.0 10000.0 9900.0 9882.675792888303
params = HestonParams(0.0001, 0.01, 1.0, -0.95, 3.0)
cf = DefaultCharFunc(params)
pricers=[ALCharFuncPricer(cf, quadratureGL), makeCosCharFuncPricer(cf, τ, 8),
 FlinnCharFuncPricer(cf, τ, tTol = 1e-10, qTol=1e-10), ALCharFuncPricer(cf, n=200)]
 pricerNames=["Reference", "Cos-Adaptive 8", "Flinn Trunc 1e-10", "AndersenLake"]
isCall = false
refPrices = map(x-> priceEuropean(pricers[1], isCall, x, forward, τ, 1.0),strikes)
for price in refPrices
    @printf("& %.8e", price)
end
println()
for (pricer,name) in zip(pricers,pricerNames)
    prices = map(x -> priceEuropean(pricer, isCall, x, forward, τ, 1.0), strikes)
    print(name," ")
    for (price,refPrice) in zip(prices,refPrices)
        @printf("& %.2e", price - refPrice)
    end
    println("\\\\")
end
prices = map(x -> priceEuropean(pricers[2], isCall, x, forward, τ, 1.0), strikes)
@test isless(1e-4, abs(prices[1]-refPrices[1])) #bad price for Cos
prices = map(x -> priceEuropean(pricers[3], isCall, x, forward, τ, 1.0), strikes)
@test isapprox(prices[1], refPrices[1], atol=1e-7)
prices = map(x -> priceEuropean(pricers[4], isCall, x, forward, τ, 1.0), strikes)
@test isapprox(prices[1], refPrices[1], atol=1e-8)

τ=10.0
strike=101.0
params=HestonParams{Float64}(0.0001, 0.01, 0.0001, 0.95, 3.0)
cf = DefaultCharFunc(params)
pricers = [ALCharFuncPricer(cf, quadratureGL), makeCosCharFuncPricer(cf, τ, 200, 8), makeCosCharFuncPricer(cf, τ, 8, tol=1e-8),
FlinnCharFuncPricer(cf, τ, tTol = 1e-10, qTol=1e-10), FlinnCharFuncPricer(cf, τ, tTol = 1e-40, qTol=1e-10),
 AdaptiveFlinnCharFuncPricer(cf, τ, qTol=1e-8), ALCharFuncPricer(cf, n=200)]
pricerNames = ["Reference", "Cos (M=200)", "Cos-Adaptive", "Flinn-Truncated (1e-8)", "Flinn-Truncated (1e-40)", "Flinn-Transformed", "Andersen-Lake (n=200)"]
isCall = false
#bad price for adapt-cos vs cos 8
refPrices = map(x-> priceEuropean(pricers[1], isCall, x, forward, τ, 1.0),strikes)
for price in refPrices
    @printf("& %.8e", price)
end
println()
for (pricer,name) in zip(pricers,pricerNames)
    prices = map(x -> priceEuropean(pricer, isCall, x, forward, τ, 1.0), strikes)
    print(name," ")
    for (price,refPrice) in zip(prices,refPrices)
        @printf("& %.2e", price - refPrice)
    end
    println("\\\\")
    # println(strike," & ",priceC," & ",priceC2," & ",priceF," & ",priceF2, " & ",priceA," \\\\")
end
prices = map(x -> priceEuropean(pricers[2], isCall, x, forward, τ, 1.0), strikes)
@test isless(6.28, abs(prices[1]-refPrices[1])) #bad price for Cos
prices = map(x -> priceEuropean(pricers[3], isCall, x, forward, τ, 1.0), strikes)
@test isapprox(prices[1], refPrices[1], atol=1e-4)
prices = map(x -> priceEuropean(pricers[4], isCall, x, forward, τ, 1.0), strikes)
#@test isless(1.86, abs(prices[1]-refPrices[1])) #bad price for Flinn-truncated
@test isapprox(prices[1], refPrices[1], atol=1e-7)
prices = map(x -> priceEuropean(pricers[6], isCall, x, forward, τ, 1.0), strikes)
@test isapprox(prices[1], refPrices[1], atol=1e-7)
prices = map(x -> priceEuropean(pricers[7], isCall, x, forward, τ, 1.0), strikes)
@test isapprox(prices[1], refPrices[1], atol=1e-8)

τ=10.0
strike=101.0
params= HestonParams{Float64}(1.0, 0.01, 0.25, 0.95, 3.0)
cf = DefaultCharFunc(params)
pricers= [ ALCharFuncPricer(cf, quadratureGL),  makeCosCharFuncPricer(cf, τ, 8),
FlinnCharFuncPricer(HestonCVCharFunc(cf), τ, tTol = 1e-10, qTol=1e-10), ALCharFuncPricer(cf, n=200)]
pricerNames=["Reference","Cos Adapt","Flinn trunc", "AL"]
isCall = false
refPrices = map(x-> priceEuropean(pricers[1], isCall, x, forward, τ, 1.0),strikes)
for price in refPrices
    @printf("& %.8e", price)
end
println()
for (pricer,name) in zip(pricers,pricerNames)
    prices = map(x -> priceEuropean(pricer, isCall, x, forward, τ, 1.0), strikes)
    print(name," ")
    for (price,refPrice) in zip(prices,refPrices)
        @printf("& %.2e", price - refPrice)
    end
    println("\\\\")
    # println(strike," & ",priceC," & ",priceC2," & ",priceF," & ",priceF2, " & ",priceA," \\\\")
end
prices = map(x -> priceEuropean(pricers[3], isCall, x, forward, τ, 1.0), strikes)
@test isapprox(prices[1], refPrices[1], atol=1e-8)
prices = map(x -> priceEuropean(pricers[4], isCall, x, forward, τ, 1.0), strikes)
@test isapprox(prices[1], refPrices[1], atol=1e-8)

#(30.0, 100.0001, HestonParams{Float64}(1.0, 0.1, 1.0, 0.95, 0.1))
τ=30.0
params =  HestonParams{Float64}(1.0, 0.1, 1.0, 0.95, 0.1)
cf = DefaultCharFunc(params)
pricers= [ ALCharFuncPricer(cf, quadratureGL),  makeCosCharFuncPricer(cf, τ, 8),
FlinnCharFuncPricer(HestonCVCharFunc(cf), τ, tTol = 1e-10, qTol=1e-10), ALCharFuncPricer(cf, n=200)]
pricerNames=["Reference","Cos Adapt","Flinn trunc", "AL"]
isCall = false
refPrices = map(x-> priceEuropean(pricers[1], isCall, x, forward, τ, 1.0),strikes)
for price in refPrices
    @printf("& %.8e", price)
end
println()
for (pricer,name) in zip(pricers,pricerNames)
    prices = map(x -> priceEuropean(pricer, isCall, x, forward, τ, 1.0), strikes)
    print(name," ")
    for (price,refPrice) in zip(prices,refPrices)
        @printf("& %.2e", price - refPrice)
    end
    println("\\\\")
    # println(strike," & ",priceC," & ",priceC2," & ",priceF," & ",priceF2, " & ",priceA," \\\\")
end
prices = map(x -> priceEuropean(pricers[3], isCall, x, forward, τ, 1.0), strikes)
@test isapprox(prices[1], refPrices[1], atol=1e-8)
prices = map(x -> priceEuropean(pricers[4], isCall, x, forward, τ, 1.0), strikes)
@test isapprox(prices[1], refPrices[1], atol=1e-8)

τ=0.0025
params = HestonParams{Float64}(0.0001, 0.01, 0.0001, -0.95, 3.0)
cf = DefaultCharFunc(params)
pricers= [ ALCharFuncPricer(cf, quadratureGL),  makeCosCharFuncPricer(cf, τ, 8),
FlinnCharFuncPricer(HestonCVCharFunc(cf), τ, tTol = 1e-10, qTol=1e-10), ALCharFuncPricer(cf, n=10000)]
pricerNames=["Reference","Cos Adapt","Flinn trunc", "AL"]
refPrices = map(x-> priceEuropean(pricers[1], isCall, x, forward, τ, 1.0),strikes)
for price in refPrices
    @printf("& %.8e", price)
end
println()
for (pricer,name) in zip(pricers,pricerNames)
    prices = map(x -> priceEuropean(pricer, isCall, x, forward, τ, 1.0), strikes)
    print(name," ")
    for (price,refPrice) in zip(prices,refPrices)
        @printf("& %.2e", price - refPrice)
    end
    println("\\\\")
    # println(strike," & ",priceC," & ",priceC2," & ",priceF," & ",priceF2, " & ",priceA," \\\\")
end
prices = map(x -> priceEuropean(pricers[3], isCall, x, forward, τ, 1.0), strikes)
@test isapprox(prices[1], refPrices[1], atol=1e-10)
prices = map(x -> priceEuropean(pricers[4], isCall, x, forward, τ, 1.0), strikes)
@test isapprox(prices[1], refPrices[1], atol=1e-15)

#flinn breaks strongly with or without cv. Also due to bad truncatino range (better if we use short mat criteria as well)
τ=0.5
strike = 100.0
params = HestonParams{Float64}(0.0025, 2.0, 0.25, 0.5, 0.0001)
cf = DefaultCharFunc(params)
pricers= [ ALCharFuncPricer(cf, quadratureGL),  makeCosCharFuncPricer(cf, τ, 8),
FlinnCharFuncPricer(HestonCVCharFunc(cf), τ, tTol = 1e-10, qTol=1e-10), ALCharFuncPricer(cf, n=10000)]
pricerNames=["Reference","Cos Adapt","Flinn trunc", "AL"]
refPrices = map(x-> priceEuropean(pricers[1], isCall, x, forward, τ, 1.0),strikes)
for price in refPrices
    @printf("& %.8e", price)
end
println()
for (pricer,name) in zip(pricers,pricerNames)
    prices = map(x -> priceEuropean(pricer, isCall, x, forward, τ, 1.0), strikes)
    print(name," ")
    for (price,refPrice) in zip(prices,refPrices)
        @printf("& %.2e", price - refPrice)
    end
    println("\\\\")
    # println(strike," & ",priceC," & ",priceC2," & ",priceF," & ",priceF2, " & ",priceA," \\\\")
end
prices = map(x -> priceEuropean(pricers[3], isCall, x, forward, τ, 1.0), strikes)
@test isapprox(prices[1], refPrices[1], atol=1e-7)
prices = map(x -> priceEuropean(pricers[4], isCall, x, forward, τ, 1.0), strikes)
@test isapprox(prices[1], refPrices[1], atol=1e-13)

#DE adaptive largest error abs
τ= 0.1
strike= 101.0
forward=100.0
params = HestonParams{Float64}(0.0001, 2.0, 0.04, -0.5, 3.0)
cf = DefaultCharFunc(params)
pricers= [ ALCharFuncPricer(cf, quadratureGL),  makeCosCharFuncPricer(cf, τ, 8),
FlinnCharFuncPricer(HestonCVCharFunc(cf), τ, tTol = 1e-10, qTol=1e-10), ALCharFuncPricer(cf, n=10000)]
pricerNames=["Reference","Cos Adapt","Flinn trunc", "AL"]
refPrices = map(x-> priceEuropean(pricers[1], isCall, x, forward, τ, 1.0),strikes)
for price in refPrices
    @printf("& %.8e", price)
end
println()
for (pricer,name) in zip(pricers,pricerNames)
    prices = map(x -> priceEuropean(pricer, isCall, x, forward, τ, 1.0), strikes)
    print(name," ")
    for (price,refPrice) in zip(prices,refPrices)
        @printf("& %.2e", price - refPrice)
    end
    println("\\\\")
    # println(strike," & ",priceC," & ",priceC2," & ",priceF," & ",priceF2, " & ",priceA," \\\\")
end
prices = map(x -> priceEuropean(pricers[3], isCall, x, forward, τ, 1.0), strikes)
@test isapprox(prices[1], refPrices[1], atol=1e-9)
prices = map(x -> priceEuropean(pricers[4], isCall, x, forward, τ, 1.0), strikes)
@test isapprox(prices[1], refPrices[1], atol=1e-14)

#Interestingly, AL-10000 is likley wrong as AL-0 with tol=1e-20 = TFLinn price, strike = 101 & 200.
τ=30.0
forward = 100.0
params = HestonParams{Float64}(0.0025, 0.1, 0.0001, 0.1, 3.0)
cf = DefaultCharFunc(params)
pricers= [ ALCharFuncPricer(cf, quadratureGL),  makeCosCharFuncPricer(cf, τ, 8),
FlinnCharFuncPricer(HestonCVCharFunc(cf), τ, tTol = 1e-10, qTol=1e-10), ALCharFuncPricer(cf, n=10000)]
pricerNames=["Reference","Cos Adapt","Flinn trunc", "AL"]
refPrices = map(x-> priceEuropean(pricers[1], isCall, x, forward, τ, 1.0),strikes)
for price in refPrices
    @printf("& %.8e", price)
end
println()
for (pricer,name) in zip(pricers,pricerNames)
    prices = map(x -> priceEuropean(pricer, isCall, x, forward, τ, 1.0), strikes)
    print(name," ")
    for (price,refPrice) in zip(prices,refPrices)
        @printf("& %.2e", price - refPrice)
    end
    println("\\\\")
    # println(strike," & ",priceC," & ",priceC2," & ",priceF," & ",priceF2, " & ",priceA," \\\\")
end
prices = map(x -> priceEuropean(pricers[3], isCall, x, forward, τ, 1.0), strikes)
@test isapprox(prices[1], refPrices[1], atol=1e-8)
prices = map(x -> priceEuropean(pricers[4], isCall, x, forward, τ, 1.0), strikes)
@test isapprox(prices[1], refPrices[1], atol=1e-12)

#Largest error on AL-1000 vs 2000
τ=10.0
forward = 100.0
params = HestonParams{Float64}(0.0001, 0.01, 0.0001, 0.95, 1.0)
#(10.0, 100.0001, HestonParams{Float64}(0.0001, 0.01, 0.0001, 0.95, 1.0))
cf = DefaultCharFunc(params)
pricers = [ALCharFuncPricer(cf, quadratureGL), makeCosCharFuncPricer(cf, τ, 200, 8),
makeCosCharFuncPricer(cf, τ, 8, tol=1e-8), FlinnCharFuncPricer(cf, τ, tTol = 1e-10, qTol=1e-10),
 FlinnCharFuncPricer(cf, τ, tTol = 1e-40, qTol=1e-10), AdaptiveFlinnCharFuncPricer(cf, τ, qTol=1e-8), ALCharFuncPricer(cf, n=200)]
pricerNames = ["Reference", "Cos (M=200)", "Cos-Adaptive", "Flinn-Truncated (1e-8)", "Flinn-Truncated (1e-40)", "Flinn-Transformed", "Andersen-Lake (n=200)"]
isCall = false
#bad price for adapt-cos vs cos 8
refPrices = map(x-> priceEuropean(pricers[1], isCall, x, forward, τ, 1.0),strikes)
for price in refPrices
    @printf("& %.8e", price)
end
println()
for (pricer,name) in zip(pricers,pricerNames)
    prices = map(x -> priceEuropean(pricer, isCall, x, forward, τ, 1.0), strikes)
    print(name," ")
    for (price,refPrice) in zip(prices,refPrices)
        @printf("& %.2e", price - refPrice)
    end
    println("\\\\")
    # println(strike," & ",priceC," & ",priceC2," & ",priceF," & ",priceF2, " & ",priceA," \\\\")
end
prices = map(x -> priceEuropean(pricers[2], isCall, x, forward, τ, 1.0), strikes)
@test isless(0.95, abs(prices[1]-refPrices[1])) #bad price for Cos
prices = map(x -> priceEuropean(pricers[3], isCall, x, forward, τ, 1.0), strikes)
@test isless(2e-4, abs(prices[1]-refPrices[1])) #bad price for Cos
prices = map(x -> priceEuropean(pricers[4], isCall, x, forward, τ, 1.0), strikes)
#@test isless(5e-2, abs(prices[1]-refPrices[1])) #bad price for Flinn
@test isapprox(prices[1], refPrices[1], atol=1e-7)
prices = map(x -> priceEuropean(pricers[5], isCall, x, forward, τ, 1.0), strikes)
#@test isless(5e-2, abs(prices[1]-refPrices[1])) #bad price for Flinn
@test isapprox(prices[1], refPrices[1], atol=1e-7)
prices = map(x -> priceEuropean(pricers[6], isCall, x, forward, τ, 1.0), strikes)
@test isapprox(prices[1], refPrices[1], atol=1e-7)
prices = map(x -> priceEuropean(pricers[7], isCall, x, forward, τ, 1.0), strikes)
@test isapprox(prices[1], refPrices[1], atol=1e-8)

#largest error n=200
τ=2.0
strike = 100.0
forward = 200.0
forwards = [100.0,100.0001, 101.0, 110.0, 200.0, 1000.0, 10000.0]
params = HestonParams{Float64}(0.0001, 2.0, 0.0001, 0.95, 1.0)
cf = DefaultCharFunc(params)
pricers = [ALCharFuncPricer(cf, quadratureGL), makeCosCharFuncPricer(cf, τ, 200, 8),
 makeCosCharFuncPricer(cf, τ, 8, tol=1e-8), FlinnCharFuncPricer(cf, τ, tTol = 1e-10, qTol=1e-10),
  FlinnCharFuncPricer(cf, τ, tTol = 1e-40, qTol=1e-10), AdaptiveFlinnCharFuncPricer(cf, τ, qTol=1e-8), ALCharFuncPricer(cf, n=200)]
pricerNames = ["Reference", "Cos (M=200)", "Cos-Adaptive", "Flinn-Truncated (1e-8)", "Flinn-Truncated (1e-40)", "Flinn-Transformed", "Andersen-Lake (n=200)"]
isCall = false
#bad price for adapt-cos vs cos 8
refPrices = map(x-> priceEuropean(pricers[1], isCall, strike, x, τ, 1.0),forwards)
for price in refPrices
    @printf("& %.8e", price)
end
println()
for (pricer,name) in zip(pricers,pricerNames)
    prices = map(x -> priceEuropean(pricer, isCall, strike, x, τ, 1.0), forwards)
    print(name," ")
    for (price,refPrice) in zip(prices,refPrices)
        @printf("& %.2e", price - refPrice)
    end
    println("\\\\")
end
prices = map(x -> priceEuropean(pricers[7], isCall, strike, x, τ, 1.0), forwards)
@test isapprox(prices[5], refPrices[5], atol=1.2e-2)
@test isless(0.01, prices[5]/ refPrices[5])

end

@testset "HestonFlinn" begin
    v0 = 0.0718
    ρ = -0.352
    σ = 0.582
    θ = 0.0762
    κ = 1.542
    params = HestonParams(v0, κ, θ, ρ, σ)

    tol = 1e-8
    τ = 0.1
    forward = 6317.80
    strike = 6317.80
    cf = DefaultCharFunc(params)
    refPricer = makeCosCharFuncPricer(cf, τ, 2048 * 4, 32)

    pricer = FlinnCharFuncPricer(cf, τ, qTol = tol, tTol = tol)
    refPrice = priceEuropean(refPricer, false, strike, forward, τ, 1.0)
    price = priceEuropean(pricer, false, strike, forward, τ, 1.0)
    @test isapprox(refPrice, price, atol = 5e-4)
#    @test isapprox(209.820637, price, atol = 1e-5)

    ccf = HestonCVCharFunc(cf)
    pricer = FlinnCharFuncPricer(ccf, τ, qTol = tol, tTol = tol)
    price = priceEuropean(pricer, false, strike, forward, τ, 1.0)
    @test isapprox(refPrice, price, atol = 1e-6)

    #Make sure BigFloat works
    params = HestonParams(BigFloat("0.0718"), BigFloat("1.542"), BigFloat("0.0762"),BigFloat("-0.352"),BigFloat("0.582"))
    cf = DefaultCharFunc{HestonParams{BigFloat},Complex{BigFloat}}(params)
    τ = BigFloat("0.1")
    strike = BigFloat("6317.80")
    forward = BigFloat("6317.80")
    pricer = AdaptiveFlinnCharFuncPricer(cf, τ, qTol = BigFloat(1e-24))
    priceB = priceEuropean(pricer, false, strike, forward, τ, BigFloat(1.0))
    @test isapprox(refPrice, Float64(priceB), atol = 1e-12)
    pricer = FlinnCharFuncPricer(cf, τ, qTol = BigFloat(1e-24), tTol = BigFloat(1e-24))
    priceC = priceEuropean(pricer, false, strike, forward, τ, BigFloat(1.0))
    @test isapprox(priceB, priceC, atol=BigFloat(1e-18))
end

@testset "HestonLogContinuity" begin
    v0 = 0.08
    ρ = -0.8
    σ = 0.25
    θ = 0.1
    κ = 3.0
    #params from Cui et al for Heston.
    params = HestonParams(v0, κ, θ, ρ, σ)
    #with the original formula from SZ, there is a discontinuity in the log.
    cf = DefaultCharFunc(params)
    sz = CharFuncPricing.evaluateLogCharFuncCui(cf, 2.0 + 0.0im, 15.0)
    lk = CharFuncPricing.evaluateLogCharFunc(cf, 2.0 + 0.0im, 15.0)
    @test isapprox(lk, sz, atol = 1e-13)
    for u = 0.1:0.1:4.0
        lk = CharFuncPricing.evaluateLogCharFunc(cf, u + 0.0im, 15.0)
        sz = CharFuncPricing.evaluateLogCharFuncCui(cf, u + 0.0im, 15.0)
        @test isapprox(lk, sz, atol = 1e-13)
    end
    v0 = 0.010201
    ρ = -0.7
    σ = 0.61
    κ = 6.21
    θ = 0.019
    α = 3.35861
    params = HestonParams{Float64}(sqrt(v0), κ, sqrt(θ), ρ, σ)
    cf = DefaultCharFunc(params)
    sz = CharFuncPricing.evaluateLogCharFunc(cf, 10.0 - (α + 1) * 1im, 10.0)
    lk = CharFuncPricing.evaluateLogCharFuncCui(cf, 10.0 - (α + 1) * 1im, 10.0)
    @test isapprox(lk, sz, atol = 1e-13)
end

@testset "LF2y" begin
    r = 0.0
    q = 0.0
    κ = 1.0
    θ = 0.1
    σ = 1.0
    ρ = -0.9
    v0 = 0.1
    τs = [2.0/52, 2.0/12, 2.0]
    spot = 1.0

    τ = τs[3]
    spot *= exp((r - q) * τ)
    df = exp(-r * τ)
    params = HestonParams{Float64}(v0, κ, θ, ρ, σ)
    cf = DefaultCharFunc(params)
    m = 200
    l = 8
    refPricer = makeCosCharFuncPricer(cf, τ, 2048 * 8, 32)
    pricer = makeCosCharFuncPricer(cf, τ, m, l)
    refPrices = Vector{Float64}(undef, 0)
    prices = Vector{Float64}(undef, 0)
    for strike = 0.4:0.012:1.6
        refPrice = priceEuropean(refPricer, false, strike, spot,τ, df)
        price = priceEuropean(pricer, false, strike, spot, τ, df)
        push!(refPrices, refPrice)
        push!(prices, price)
    end
    rmse = rmsd(prices, refPrices)
    println("RMSE ", rmse)
    @test isless(rmse, 1e-4)
    @test isless(1e-5, rmse)

    for τ in τs
        local refPrices = zeros(length(collect(0.4:0.012:1.6)))
        local prices =  zeros(length(collect(0.4:0.012:1.6)))
        measuredTime = @elapsed begin
        pricer = makeCosCharFuncPricer(cf, τ, 2048 * 8, 32)
        for (i, strike) in enumerate(0.4:0.012:1.6)
        price = priceEuropean(pricer, false, strike, spot,  τ, df)
        refPrices[i] = price
    end
    end
    println("Ref ", measuredTime)

    measuredTime = @elapsed begin
        pricer = makeCosCharFuncPricer(cf, τ, 200,8)
        for (i, strike) in enumerate(0.4:0.012:1.6)
        price = priceEuropean(pricer, false, strike, spot, τ, df)
        prices[i] = price
    end
    end
    rmse = rmsd(prices, refPrices)
    println("Cos ", rmse, " ",measuredTime)

    measuredTime = @elapsed begin
        pricer = FlinnCharFuncPricer(cf, τ)
        for (i, strike) in enumerate(0.4:0.012:1.6)
        price = priceEuropean(pricer, false, strike, spot,  τ, df)
        prices[i] = price
    end
    end
    rmse = rmsd(prices, refPrices)
    println("Truncated Flinn ", rmse, " ",measuredTime)

    measuredTime = @elapsed begin
        pricer = AdaptiveFlinnCharFuncPricer(cf, τ)
        for (i, strike) in enumerate(0.4:0.012:1.6)
        price = priceEuropean(pricer, false, strike, spot, τ, df)
        prices[i] = price
    end
    end
    rmse = rmsd(prices, refPrices)
    println("Transformed Flinn ", rmse, " ",measuredTime)

quadrature = TanhSinhQuadrature(200, eps())
    measuredTime = @elapsed begin
        pricer = ALCharFuncPricer(cf, quadrature)
        for (i, strike) in enumerate(0.4:0.012:1.6)
        price = priceEuropean(pricer, false, strike, spot, τ, df)
        prices[i] = price
        end
    end
    rmse = rmsd(prices, refPrices)
    println("Andersen-Lake ", rmse, " ",measuredTime)

    println(τ)
end
    κ = 1.417
    θ = 0.082
    σ = 0.591
    ρ = -0.384
    v0 = 0.073
end
#
# @testset "LFShortA" begin
#     r = 0.0
#     q = 0.0
#     κ = 0.254
#     θ = 0.320
#     σ = 0.344
#     ρ = -0.557
#     v0 = 0.826
#     τ = 0.0182
#     spot = 1000.0
#
#     spot *= exp((r - q) * τ)
#     df = exp(-r * τ)
#     params = HestonParams(v0, κ, θ, ρ, σ)
#     m = 200
#     l = 8
#     refPricer = makeCosCharFuncPricer(
#         Complex,
#         Float64,
#
#         params,
#         τ,
#         2048*4,
#         32,
#     )
#     pricer =
#         makeCosCharFuncPricer(Complex, Float64,  params, τ, m, l)
#     strike = 1400.0
#         refPrice = priceEuropean(refPricer, true, strike, spot, df)
#         price = priceEuropean(pricer, true, strike, spot, df)
#         println(
#             strike,
#             " ",
#             price,
#             " ",
#             refPrice,
#             " ",
#             price - refPrice,
#             " ",
#             price / refPrice - 1,
#         )
#
#     cinf = (params.v0+params.κ*params.θ*τ)/params.σ *sqrt(1-params.ρ^2)
#     #exp(-cinf*umax) = eps
#     #2*exp(-cinf*mmax*pi/(b-a))/(mmax*pi) = eps => exp(cinf*mmax*pi/(b-a))*mmax*pi/(b-a)*cinf = 2/eps/(b-a)*cinf => cinf*mmax*pi/(b-a) = lambertW( 2/eps/(b-a)*cinf)
#     eps = 1e-8
#     umax = -log(eps)/cinf
#     mmax = ceil(Int,umax*(pricer.b-pricer.a)/pi)+1
#     mmax = ceil(Int, sqrt(   lambertW(2*v0*τ/(eps*eps*(pricer.b-pricer.a)))/(v0*τ*pi)*(pricer.b-pricer.a)) )
#     mmax = max(32, ceil(Int, lambertW(2*cinf/(eps*(pricer.b-pricer.a)))/(cinf*pi)*(pricer.b-pricer.a)))
#     pricer =
#         makeCosCharFuncPricer(Complex, Float64,  params, τ, mmax, l)
#             price = priceEuropean(pricer, true, strike, spot, df)
#             println(
#                 strike,
#                 " ",
#                 price,
#                 " ",
#                 refPrice,
#                 " ",
#                 price - refPrice,
#                 " ",
#                 price / refPrice - 1,
#             )
# end
#
#
# @testset "LF1Y" begin
#     r = 0.0
#     q = 0.0
#     κ = 0.1
#     θ = 0.01
#     σ = 2.0
#     ρ = 0.5
#     v0 = 0.0225
#     τ = 1.0
#     spot = 1000000.0
#
#     spot *= exp((r - q) * τ)
#     df = exp(-r * τ)
#     params = HestonParams(v0, κ, θ, ρ, σ)
#     m = 200
#     l = 10
#     refPricer = makeCosCharFuncPricer(
#         Complex,
#         Float64,
#
#         params,
#         τ,
#         2048*4,
#         32,
#     )
#     pricer =
#         makeCosCharFuncPricer(Complex, Float64,  params, τ, m, l)
#     strike = spot*0.25
#         refPrice = priceEuropean(refPricer, false, strike, spot, df)
#         price = priceEuropean(pricer, false, strike, spot, df)
#         println(
#             strike,
#             " ",
#             price,
#             " ",
#             refPrice,
#             " ",
#             price - refPrice,
#             " ",
#             price / refPrice - 1,
#         )
#
#     cinf = (params.v0+params.κ*params.θ*τ)/params.σ *sqrt(1-params.ρ^2)
#     #exp(-cinf*umax) = eps
#     #2*exp(-cinf*mmax*pi/(b-a))/(mmax*pi) = eps => exp(cinf*mmax*pi/(b-a))*mmax*pi/(b-a)*cinf = 2/eps/(b-a)*cinf => cinf*mmax*pi/(b-a) = lambertW( 2/eps/(b-a)*cinf)
#     eps = 1e-10
#     umax = -log(eps)/cinf
#     mmax = ceil(Int,umax*(pricer.b-pricer.a)/pi)+1
#     mmax = ceil(Int, sqrt(   lambertW(2*v0*τ/(eps*eps*(pricer.b-pricer.a)))/(v0*τ*pi)*(pricer.b-pricer.a)) )
#     mmax = max(32, ceil(Int, lambertW(2*cinf/(eps*(pricer.b-pricer.a)))/(cinf*pi)*(pricer.b-pricer.a)))
#     pricer =
#         makeCosCharFuncPricer(Complex, Float64,  params, τ, mmax, l)
#             price = priceEuropean(pricer, false, strike, spot, df)
#             println(
#                 strike,
#                 " ",
#                 price,
#                 " ",
#                 refPrice,
#                 " ",
#                 price - refPrice,
#                 " ",
#                 price / refPrice - 1,
#             )
# end
@testset "LFShort" begin
    r = 0.0
    q = 0.0
    κ = 1.0
    θ = 0.1
    σ = 1.0
    ρ = -0.9
    v0 = 0.1
    τ = 2.0 / 365
    spot = 1.0

    spot *= exp((r - q) * τ)
    df = exp(-r * τ)
    params = HestonParams{Float64}(v0, κ, θ, ρ, σ)
    cf = DefaultCharFunc(params)
    m = 256
    l = 12
    refPricer = makeCosCharFuncPricer(cf, τ, 1024, 32)
    pricer = makeCosCharFuncPricer(cf, τ, m, l)
    for strike = 1.0:0.025:1.5
        refPrice = priceEuropean(refPricer, true, strike, spot,  τ,df)
        price = priceEuropean(pricer, true, strike, spot, τ, df)
        println(
            strike,
            " ",
            price,
            " ",
            refPrice,
            " ",
            price - refPrice,
            " ",
            price / refPrice - 1,
        )
        @test isapprox(refPrice, price, atol = 1e-15)
    end
end

#Testing https://financepress.com/2019/02/15/heston-model-reference-prices/
@testset "AlanFloat64Set" begin
    r = 0.01
    q = 0.02
    κ = 4.0
    θ = 0.25
    σ = 1.0
    ρ = -0.5
    v0 = 0.04
    τ = 1.0
    spot = 100.0
    strikes = [80.0, 90.0, 100.0, 110.0, 120.0]
    alanPuts = [
        7.958878113256768285213263077598987193482161301733,
        12.017966707346304987709573290236471654992071308187,
        17.055270961270109413522653999411000974895436309183,
        23.017825898442800538908781834822560777763225722188,
        29.811026202682471843340682293165857439167301370697,
    ]
    alanCalls = [
        26.774758743998854221382195325726949201687074848341,
        20.933349000596710388139445766564068085476194042256,
        16.070154917028834278213466703938231827658768230714,
        12.132211516709844867860534767549426052805766831181,
        9.024913483457835636553375454092357136489051667150,
    ]
    spot *= exp((r - q) * τ)
    df = exp(-r * τ)
    params = HestonParams{Float64}(v0, κ, θ, ρ, σ)
    cf = DefaultCharFunc(params)
    l = 32
    m = 1024
    pricer = makeCosCharFuncPricer(cf, τ, m, l)
    for (strike, refCall, refPut) in zip(strikes, alanCalls, alanPuts)
        price = priceEuropean(pricer, false, strike, spot, τ, df)
        println(Float64(strike), " P ", price, " ", price - refPut)
        @test isapprox(Float64(price - refPut), 0, atol = 1e-13)
        price = priceEuropean(pricer, true, strike, spot,  τ,df)
        println(Float64(strike), " C ", price, " ", price - refCall)
        @test isapprox(Float64(price - refCall), 0, atol = 1e-13)
    end
end
@testset "AlanJoshiFloat64Set" begin
    r = 0.01
    q = 0.02
    κ = 4.0
    θ = 0.25
    σ = 1.0
    ρ = -0.5
    v0 = 0.01
    τ = 0.01
    spot = 100.0
    strikes = [90.0, 95.0, 100.0, 105.0, 110.0]
    alanPuts = [
        4.5183603586861772614990106188215872180542e-8,
        0.000461954855653851579672612557018857858641926937,
        0.477781171629504680023239655436072890669645669297,
        5.009501052563650299130635110520904481889436667608,
        10.008998550115123724684210555728039829315964456261,
    ]
    alanCalls = [
        9.989001595065276544935948045293485530832966049263,
        4.989963479738160122154264702582719627807098780529,
        0.467782671512844263098248405184095087949465507760,
        2.527447823194706060519991248106500619490942e-6,
        1.29932760052624920704881258510264466e-13,
    ]
    spot *= exp((r - q) * τ)
    df = exp(-r * τ)
    params = HestonParams{Float64}(v0, κ, θ, ρ, σ)
    cf = DefaultCharFunc(params)
    l = 32
    m = 1024
    pricer = makeCosCharFuncPricer(cf, τ, m, l)
    for (strike, refCall, refPut) in zip(strikes, alanCalls, alanPuts)
        price = priceEuropean(pricer, false, strike, spot, τ, df)
        println(Float64(strike), " P ", price, " ", price - refPut)
        @test isapprox(Float64(price - refPut), 0, atol = 2e-14)
        price = priceEuropean(pricer, true, strike, spot, τ,  df)
        println(Float64(strike), " C ", price, " ", price - refCall)
        @test isapprox(Float64(price - refCall), 0, atol = 2e-14)
    end
end


using BenchmarkTools
BenchmarkTools.DEFAULT_PARAMETERS.samples = 100

compute(n::Int)::Int = binomial(2n, n)

compute(2)

compute(20)

@benchmark compute(20)