function lgwt(N0::Integer,a::Real=-1,b::Real=1)
    # This script is for computing definite integrals using Legendre-Gauss
    # Quadrature. Computes the Legendre-Gauss nodes and weights on an interval
    # [a,b] with truncation order N
    #
    # Suppose you have a continuous function f(x) which is defined on [a,b]
    # which you can evaluate at any x in [a,b]. Simply evaluate it at all of
    # the values contained in the x vector to obtain a vector f. Then compute
    # the definite integral using sum(f.*w);
    #
    # Written by Greg von Winckel - 02/25/2004
    # First adapted to Julia by Tyler Ransom on 08-04-2015
    # Updated on 08-17-2020

    N  = N0-1
    N1 = N+1
    N2 = N+2

    xu = range(-1,stop=1,length=N1)

    # Initial guess
    y = cos.((2*(0:N) .+ 1)*pi/(2*N .+ 2))  .+  ( 0.27/N1 ) .* sin.( pi .* xu .* N/N2 )

    # Legendre-Gauss Vandermonde Matrix
    L  = zeros(N1,N2)

    # Derivative of LGVM
    Lp = zeros(N1,N2)

    # Compute the zeros of the N+1 Legendre Polynomial
    # using the recursion relation and the Newton-Raphson method

    y0 = 2

    vareps = 2e-52
    # Iterate until new points are uniformly within epsilon of old points

    i = 0
    tracker=0
    it_max=10
    while (norm(y.-y0,Inf)>vareps && tracker<=it_max)
        d=norm(y.-y0,Inf)

        L[:,1]  .= 1
        Lp[:,1] .= 0

        L[:,2] .= y

        for k=2:N1
            L[:,k+1] = ( (2*k-1)*y .* L[:,k] .- (k-1)*L[:,k-1] )/k
        end

        Lp = (N2)*( L[:,N1] .- y .* L[:,N2] )./(1 .- y.^2)
        y0 = y
        y  = y0 - L[:,N2]./Lp
        if norm(y.-y0,Inf)==d
            tracker+=1
        end
        i+=1

    end

    # Linear map from[-1,1] to [a,b]
    x = (a.*(1 .- y) .+ b .* (1 .+ y))./2

    # Compute the weights
    w=(b-a)./((1 .- y.^2).*Lp.^2)*(N2/N1)^2

    return x,w
end

#--------------------------------------------------

# Dr. Ransom Function for mlogit from PS3 solution
function mlogit_with_Z(theta, X, Z, y)

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

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

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

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

        return loglike
    end


### A Pluto.jl notebook ###
# v0.11.14

using Markdown
using InteractiveUtils

# ╔═╡ 4133e4a2-ed4d-11ea-11cd-0da48c09c2e8
using Pkg, DrWatson

# ╔═╡ 54bd3938-ed4d-11ea-29e4-c1c7020c1400
begin
	@quickactivate "SR2TuringPluto"
	using StatisticalRethinking
end

# ╔═╡ 0c5fe59e-ed4f-11ea-35a8-45c3aff83362
md"## Broadcasting.jl"

# ╔═╡ 54ccfc74-ed4d-11ea-366d-b738d54ff79a
p_grid = range(0, 1, length = 5)

# ╔═╡ 54d9bce8-ed4d-11ea-279e-3bc3bf465e28
prior = ones(5)

# ╔═╡ 54da91d4-ed4d-11ea-04f5-e9a4f8c659a3
likelihood = pdf.(Binomial.(9, p_grid), 6)

# ╔═╡ 54e45ff4-ed4d-11ea-0288-e3475c709007
posterior = likelihood .* prior

# ╔═╡ 54e8f906-ed4d-11ea-2fee-316c2df1d8f6
posterior ./= sum(posterior)

# ╔═╡ 54f1592c-ed4d-11ea-0210-0be7741d2604
md"##### Broadcasting is another important difference between Julia and R.

You don't have automatic
broadcasting (making functions work the same whether you run it on a single
number or on an array of numbers). However, fixing this is very easy,
you only have to annotate your function call with a dot:
  `f(single_number)` -> `f.(array_of_numbers)`.

That means the translation of the line `likelihood = pdf.(Binomial.(9, p_grid), 6)` goes something like:
1. First or every value in `p_grid` make a binomial distribution with that p value.
2. For every distribution then take the pdf of 6.

The same in the next line, this is elementwise multiplication.

For more infos read [this](https://julialang.org/blog/2017/01/moredots)."

# ╔═╡ e822d97e-ed4f-11ea-2c18-17ecfc81fdbd
md"## End of broadcasting.jl"

# ╔═╡ Cell order:
# ╟─0c5fe59e-ed4f-11ea-35a8-45c3aff83362
# ╠═4133e4a2-ed4d-11ea-11cd-0da48c09c2e8
# ╠═54bd3938-ed4d-11ea-29e4-c1c7020c1400
# ╠═54ccfc74-ed4d-11ea-366d-b738d54ff79a
# ╠═54d9bce8-ed4d-11ea-279e-3bc3bf465e28
# ╠═54da91d4-ed4d-11ea-04f5-e9a4f8c659a3
# ╠═54e45ff4-ed4d-11ea-0288-e3475c709007
# ╠═54e8f906-ed4d-11ea-2fee-316c2df1d8f6
# ╟─54f1592c-ed4d-11ea-0210-0be7741d2604
# ╟─e822d97e-ed4f-11ea-2c18-17ecfc81fdbd


"""
    advection_pde!(duhat,uhat,p,t)

RHS for the advection equation ``u_t = - u_x`` for numerical integration in Fourier space.

"""
function advection_pde!(duhat,uhat,p,t)
    N, k, dL, nu, alpha = p;
    uhat[Int(N/2)+1] = 0; # Set the most negative mode to zero to prevent an asymmetry
    duhat .= -alpha*im*k.*uhat;
end

"""
    advection_diffusion_pde!(duhat,uhat,p,t)

RHS for the advection-diffusion equation \$u_t = - u_x + ν u_{xx}\$ for numerical integration in Fourier space where ν is the viscosity.

"""
function advection_diffusion_pde!(duhat,uhat,p,t)
    N, k, dL, nu, alpha = p;
    uhat[Int(N/2)+1] = 0; # Set the most negative mode to zero to prevent an asymmetry
    duhat .= -alpha*im*k.*uhat + nu*(im*k).^2 .*uhat;
end

"""
    viscous_burgers_pde!(duhat,uhat,p,t)

RHS for the viscous Burgers equation \$u_t = - u u_x + ν u_{xx}\$ for numerical integration in Fourier space where ν is the viscosity.

"""
function viscous_burgers_pde!(duhat,uhat,p,t)
    N, k, dL, nu = p;
    alpha = 1.0;
    uhat[Int(N/2)+1] = 0.0; # Set the most negative mode to zero to prevent an asymmetry
    uhat_nonlinear = quadratic_nonlinear(uhat,N,dL,alpha);
    duhat .= -nu*k.^2 .*uhat .- uhat_nonlinear;
end

"""
    inviscid_burgers_pde!(duhat,uhat,p,t)

RHS for the inviscid Burgers equation \$u_t = - u u_x\$ for numerical integration in Fourier space.

"""
function inviscid_burgers_pde!(duhat,uhat,p,t)
    N, k, dL, nu = p;
    alpha = 1.0;
    uhat[Int(N/2)+1] = 0; # Set the most negative mode to zero to prevent an asymmetry
    uhat_nonlinear = quadratic_nonlinear(uhat,N,dL,alpha);
    duhat .= -uhat_nonlinear;
end

"""
    quadratic_nonlinear!(uhat,N,dL,alpha)

Compute the convolution sum \$\\frac{ik}{2}\\sum_{p+q=k} u_p u_q\$ resulting from the quadratic nonlinearity of Burgers equation \$u u_x\$ in Fourier space. Convolution sum is padded with the 3/2 rule for dealiasing.

"""
function quadratic_nonlinear(uhat,N,dL,alpha)
    M = Int((3/2)*N); # For dealiasing -> For MZ models must be 3*N to account for unresolved modes + dealiasing
    k_M = reduce(vcat,(2*π/dL)*[0:M/2-1 -M/2:-1]); # Wavenumbers for convolution sum with padding
    u_M = zeros(Complex{Float64},Int(M));
    u_M[1:Int(N/2)] = uhat[1:Int(N/2)];
    u_M[end-Int(N/2)+2:end] = uhat[Int(N/2)+2:end];
    u_M_sum = fft_norm(ifft_norm(u_M).*ifft_norm(u_M));
    u_M_convolution = alpha*im.*k_M/2.0 .*u_M_sum;
    uhat_nonlinear  = zeros(Complex{Float64},Int(N)); # Extract N modes from convolution
    uhat_nonlinear[1:Int(N/2)] = u_M_convolution[1:Int(N/2)];
    uhat_nonlinear[end-Int(N/2)+2:end] = u_M_convolution[end-Int(N/2)+2:end];
    return uhat_nonlinear
end

"""
    generate_fourier_solution(L1,L2,tspan,N,initial_condition,pde_function;dt=1e-3,nu=0.1,rtol=1e-10,atol=1e-14)

Generate the solution for a given `pde_function` and `initial_condition` on a periodic domain using a `N` mode Fourier expansion.

"""
function generate_fourier_solution(L1,L2,tspan,N,initial_condition,pde_function;dt=1e-3,nu=0.1,rtol=1e-10,atol=1e-14,alpha=1.0)
    # Transform random initial condition to Fourier domain
    uhat0 = fft_norm(initial_condition);
    dL = abs(L2-L1);

    # Generate Fourier Galerkin solution for N
    k = reduce(vcat,(2*π/dL)*[0:N/2-1 -N/2:-1]);
    p = [N,k,dL,nu,alpha]
    t_length = Int(round(tspan[2]/dt)+1);

    # Solve the system of ODEs in Fourier domain
    prob = ODEProblem(pde_function,uhat0,tspan,p);
    sol = solve(prob,DP5(),reltol=rtol,abstol=atol,saveat = dt)

    u_sol = zeros(t_length,N);
    for j in 1:size(sol.t,1) # Reshape output and plot
        u_sol[j,:] = real.(ifft_norm(sol.u[j]));
    end
    return u_sol, sol.u, k
end

"""
    central_difference(u_j,u_jpos,u_jneg,mu)

Compute the second order central difference for the viscous term of the viscous Burgers equation \$u_t = - u u_x + ν u_{xx}\$. `mu` is equal to \$ \\frac{\\nu}{\\Delta x^2}\$.

"""
function central_difference(u_j,u_jpos,u_jneg,mu)
    ux = zeros(size(u_j,2));
    ux = mu.*(u_jpos-2*u_j+u_jneg);
    return ux
end

"""
    minmod(x,y)

"""
function minmod(x,y)
    return sign(x)*max(0,min(abs(x),y*sign(x)));
end

"""
    ub(ulv,urv)

"""
function ub(ulv,urv)
    if ulv != urv
        return ((1/2)*urv^2 - (1/2)*ulv^2) / (urv - ulv); # ̄u_{j+/-1/2}
    else
        return ulv
    end
end

"""
    fl(ulv,urv,ubv)

"""
function fl(ulv,urv,ubv)
    return (1/2).*((1/2).*ulv.^2 .+ (1/2).*urv.^2 .- abs.(ubv).*(urv .- ulv)); # f_{j+/-1/2}
# fln(ulv,urv,ubv) = (1/2)*(flux.(urv) - flux.(ulv) - abs.(ubv).*(ulv - urv)); # f_{j+/-1/2}
end

"""
    ulpl(ujp,uj,ujn,kappa,omega)

"""
function ulpl(ujp,uj,ujn,kappa,omega)
    return uj .+ ((1-kappa)/4).*minmod.((uj .- ujn),omega.*(ujp .- uj)) .+ ((1+kappa)/4).*minmod.((ujp .- uj),omega.*(uj .- ujn)); # u_{j+1/2}^L
end

"""
    urpl(ujpp,ujp,uj,kappa,omega)

"""
function urpl(ujpp,ujp,uj,kappa,omega)
    return ujp  .- ((1+kappa)/4).*minmod.((ujp .- uj),omega.*(ujpp .- ujp)) .- ((1-kappa)/4).*minmod.((ujpp .- ujp),omega.*(ujp .- uj)); # u_{j+1/2}^R
end

"""
    ulnl(uj,ujn,ujnn,kappa,omega)

"""
function ulnl(uj,ujn,ujnn,kappa,omega)
    return ujn .+ ((1-kappa)/4).*minmod.((ujn .- ujnn),omega.*(uj .- ujn)) .+ ((1+kappa)/4).*minmod.((uj .- ujn),omega.*(ujn .- ujnn)); # u_{j-1/2}^L
end

"""
    urnl(ujp,uj,ujn,kappa,omega)

"""
function urnl(ujp,uj,ujn,kappa,omega)
    return uj .- ((1+kappa)/4).*minmod.((uj .- ujn),omega.*(ujp .- uj)) .- ((1-kappa)/4).*minmod.((ujp .- uj),omega.*(uj .- ujn)); # u_{j-1/2}^R
end

"""
    muscl_minmod_RHS!(du,u,p,t)

"""
function muscl_minmod_RHS!(du,u,p,t)
    dx, kappa, omega, nu = p;

    ulpvp = zeros(size(u,1));
    urpvp = zeros(size(u,1));
    ulnvp = zeros(size(u,1));
    urnvp = zeros(size(u,1));

    # ulp(ujp,uj,ujn,omega)
    ulpvp[2:end-1] .= ulpl(u[3:end],u[2:end-1],u[1:end-2],kappa,omega);
    ulpvp[1] = ulpl(u[2],u[1],u[end],kappa,omega);
    ulpvp[end] = ulpl(u[1],u[end],u[end-1],kappa,omega);

    # urp(ujpp,ujp,uj,omega)
    urpvp[1:end-2] .= urpl(u[3:end],u[2:end-1],u[1:end-2],kappa,omega);
    urpvp[end-1] = urpl(u[1],u[end],u[end-1],kappa,omega);
    urpvp[end] = urpl(u[2],u[1],u[end],kappa,omega);

    ubpp = ub.(ulpvp,urpvp);
    fpp = fl(ulpvp,urpvp,ubpp);

    # uln(uj,ujn,ujnn,omega)
    ulnvp[3:end] .= ulnl(u[3:end],u[2:end-1],u[1:end-2],kappa,omega);
    ulnvp[2] = ulnl(u[2],u[1],u[end],kappa,omega);
    ulnvp[1] = ulnl(u[1],u[end],u[end-1],kappa,omega);

    # urn(ujp,uj,ujn,omega)
    urnvp[2:end-1] .= urnl(u[3:end],u[2:end-1],u[1:end-2],kappa,omega);
    urnvp[1] = urnl(u[2],u[1],u[end],kappa,omega);
    urnvp[end] = urnl(u[1],u[end],u[end-1],kappa,omega);

    ubnp = ub.(ulnvp,urnvp);
    fnp = fl(ulnvp,urnvp,ubnp);

    du .= -(1/dx).*(fpp .- fnp);
end

"""
    muscl_minmod_viscous_RHS!(du,u,p,t)

"""
function muscl_minmod_viscous_RHS!(du,u,p,t)
    dx, kappa, omega, nu = p;

    mu = nu/(dx)^2;

    ulpvp = zeros(size(u,1));
    urpvp = zeros(size(u,1));
    ulnvp = zeros(size(u,1));
    urnvp = zeros(size(u,1));
    ux_viscous = zeros(size(u,1));

    # ulp(ujp,uj,ujn,omega)
    ulpvp[2:end-1] .= ulpl(u[3:end],u[2:end-1],u[1:end-2],kappa,omega);
    ulpvp[1] = ulpl(u[2],u[1],u[end],kappa,omega);
    ulpvp[end] = ulpl(u[1],u[end],u[end-1],kappa,omega);

    # urp(ujpp,ujp,uj,omega)
    urpvp[1:end-2] .= urpl(u[3:end],u[2:end-1],u[1:end-2],kappa,omega);
    urpvp[end-1] = urpl(u[1],u[end],u[end-1],kappa,omega);
    urpvp[end] = urpl(u[2],u[1],u[end],kappa,omega);

    ubpp = ub.(ulpvp,urpvp);
    fpp = fl(ulpvp,urpvp,ubpp);

    # uln(uj,ujn,ujnn,omega)
    ulnvp[3:end] .= ulnl(u[3:end],u[2:end-1],u[1:end-2],kappa,omega);
    ulnvp[2] = ulnl(u[2],u[1],u[end],kappa,omega);
    ulnvp[1] = ulnl(u[1],u[end],u[end-1],kappa,omega);

    # urn(ujp,uj,ujn,omega)
    urnvp[2:end-1] .= urnl(u[3:end],u[2:end-1],u[1:end-2],kappa,omega);
    urnvp[1] = urnl(u[2],u[1],u[end],kappa,omega);
    urnvp[end] = urnl(u[1],u[end],u[end-1],kappa,omega);

    ux_viscous[1] = central_difference(u[1],u[2],u[end],mu);
    ux_viscous[2:end-1] = central_difference(u[2:end-1],u[3:end],u[1:end-2],mu);
    ux_viscous[end] = central_difference(u[end],u[1],u[end-1],mu);

    ubnp = ub.(ulnvp,urnvp);
    fnp = fl(ulnvp,urnvp,ubnp);

    du .= -(1/dx).*(fpp .- fnp) .+ ux_viscous;
end

"""
    generate_muscl_minmod_solution(L1,L2,t_end,N,u0,pde_function_handle;dt=1e-4,kappa=-1)

"""
function generate_muscl_minmod_solution(L1,L2,t_end,N,u0,pde_function_handle;dt=1e-4,kappa=-1,nu=0.1)

    omega = ((3-kappa)/(1-kappa))

    dL = abs(L2-L1);
    # Set up periodic domain
    j = reduce(vcat,[0:1:N-1]);
    x = (dL.*j)./N;
    dx = x[2]-x[1];
    t = reduce(vcat,[0:dt:t_end]);

    p = [dx,kappa,omega,nu]
    t_span = (0,t_end);

    prob = ODEProblem(pde_function_handle,u0,t_span,p);
    sol = solve(prob,BS3(),reltol=1e-6,abstol=1e-8,saveat = dt);

    u_sol = zeros(size(sol.t,1),size(x,1));
    for i in 1:size(sol.t,1)
        u_sol[i,:] = sol.u[i];
    end

    return u_sol

end

"""
    generate_muscl_reduced(L1,L2,t_end,dt,M,N,ic_func,pde_function_handle;kappa=-1,nu=0.1)

"""
# function generate_muscl_reduced(L1,L2,t_end,dt,M,N,ic_func,pde_function_handle;kappa=-1,nu=0.1)
function generate_muscl_reduced(L1,L2,t_end,dt,M,N,ic,pde_function_handle;kappa=-1,nu=0.1)
    dL = abs(L2 - L1);
    j = reduce(vcat,[0:1:N-1]);
    x = (dL.*j)./N;
    j_full = reduce(vcat,[0:1:M-1]);
    x_full = (dL.*j_full)./M;
    # ic = ic_func.(x_full);
    u_full = generate_muscl_minmod_solution(L1,L2,t_end,M,ic,pde_function_handle;dt=dt,kappa=kappa,nu=nu)
    u_reduced = solution_spatial_sampling(x,x_full,u_full);
    return u_full, u_reduced
end

"""
    get_1D_energy_fft(u_solution)

Compute the energy in the Fourier domain using the scaling of \$ \\frac{1}{N} \$. Note: this does not include the 2π multiplier found in Parseval's identity for Fourier series and computes \$ \\frac{1}{2} \\sum \\vert \\hat{u}_k \\vert^2 \$.

"""
function get_1D_energy_fft(u_solution) # To Do: Add and extraction of specific mode step
    energy = zeros(size(u_solution,1))
    for i in 1:size(u_solution,1)
        u_hat = fft_norm(u_solution[i,:]);
        energy[i] = (1/2)*real((u_hat'*u_hat));
    end
    return energy
end

"""
    get_1D_energy_custom(basis,u_solution,L1,L2,weights;multiplier=1/(4*pi))

Compute the energy in the custom basis domain: \$ \\frac{1}{2} \\sum \\vert \\a_k \\vert^2 \$. Multiplier defaults to \$ \\frac{1}{4\\pi} \$ to match the Fourier calculation.

"""
function get_1D_energy_custom(basis,u_solution,nodes,weights;multiplier=1/(4*pi))
    energy = zeros(size(u_solution,1));
    for i in 1:size(u_solution,1)
        a_hat = expansion_coefficients(basis,u_solution[i,:],nodes,weights);
        energy[i] = get_1D_energy_custom_coefficients(a_hat,zeta=multiplier);
    end
    return energy
end

"""
    get_1D_energy_custom_coefficients(u_coefficients;zeta=1/(4*pi))

Compute the energy in the custom basis domain: \$ \\zeta \\sum \\vert \\a_k \\vert^2 \$. \$ \\zeta \$ defaults to \$ \\frac{1}{4\\pi} \$ to match the Fourier calculation.

"""
function get_1D_energy_custom_coefficients(u_coefficients;zeta=1/(4*pi))
        # energy[i] = (1/2)*(a_hat'*a_hat);
        energy = zeta*(u_coefficients'*u_coefficients);
    return energy
end

"""
    mode_extractor(uhat,N)

"""
function mode_extractor(uhat,N)
    uhat_N = zeros(typeof(uhat[1]),N);
    uhat_N[1:Int(N/2)] = uhat[1:Int(N/2)];
    uhat_N[end-Int(N/2)+2:end] = uhat[end-Int(N/2)+2:end];
    return uhat_N
end

"""
    get_1D_energy_upwind(u_solution_full,u_solution,N)

"""
function get_1D_energy_upwind(u_solution_full,u_solution,N)
    energy = zeros(size(u_solution,1))
    for i in 1:size(u_solution,1)
        u_upwind_fft = mode_extractor(fft_norm(u_solution_full[i,:]),N);
        energy[i] = (1/2)*real(u_upwind_fft'*u_upwind_fft);
    end
    return energy
end

"""
    spectral_approximation_fourier(x_locations,k,coefficients)

"""
function spectral_approximation_fourier(x_locations,k,coefficients)
    approximation = zeros(size(x_locations,1));
    for i = 1:length(k)
        approximation += coefficients[i]*exp.(im*k[i]*(x_locations));
    end
    return approximation
end


h₁(i::Int) = max(6 - abs(i-7), 0)
h₂(i::Int) = h₁(i - 8)
h₃(i::Int) = h₁(i - 4)

"""
  triangular_test_function(c1::Int, c2::Int, c3::Int, L::Int=32)

Generates a set of triangluar test functions with 3 classes.
"""
function triangular_test_functions(c1::Int, c2::Int, c3::Int; L::Int=32, shuffle::Bool=false)
  @assert c1 >= 0
  @assert c2 >= 0
  @assert c3 >= 0

  u = rand(Uniform(0,1),1)[1]
  ϵ = rand(Normal(0,1),(L,c1+c2+c3))

  y = string.(vcat(ones(c1), ones(c2) .+ 1, ones(c3) .+ 2))

  H₁ = Array{Float64,2}(undef,L,c1)
  H₂ = Array{Float64,2}(undef,L,c2)
  H₃ = Array{Float64,2}(undef,L,c3)
  for i in 1:L
    H₁[i,:] .= u * h₁(i) + (1 - u) * h₂(i)
    H₂[i,:] .= u * h₁(i) + (1 - u) * h₃(i)
    H₃[i,:] .= u * h₂(i) + (1 - u) * h₃(i)
  end

  H = hcat(H₁, H₂, H₃) + ϵ

  if shuffle
    idx = [1:(c1+c2+c3)...]
    shuffle!(idx)
    return H[:,idx], y[idx]
  end

  return H, y
end


#=
The prime factors of 13195 are 5, 7, 13 and 29.

What is the largest prime factor of the number 600851475143 ?
=#

using Primes
@time println(maximum(keys(factor(600851475143))))


module AnalyticalPlaneSolver

struct PlaneSolver
    L::Any
    W::Any
    R::Any
    lambda1::Any
    mu1::Any
    lambda2::Any
    mu2::Any
    theta0::Any
    C1::Any
    C2::Any
    D::Any
    uI::Any
    function PlaneSolver(L, W, R, lambda1, mu1, lambda2, mu2, theta0)
        C1 = solver_coefficient(lambda1, mu1)
        C2 = solver_coefficient(lambda2, mu2)
        D = 2 * lambda1 * mu1 / (lambda1 + 2mu1)

        uI = interface_displacement(L, R, C1, C2, D, theta0)
        new(L, W, R, lambda1, mu1, lambda2, mu2, theta0, C1, C2, D, uI)
    end
end

function solver_coefficient(lambda, mu)
    C = 4 * mu * (lambda + mu) / (lambda + 2mu)
    return C
end

function interface_displacement(L, R, C1, C2, D, theta0)
    uI = -(C1 + D) / (C2 / R + C1 / (L - R)) * theta0 / 3
    return uI
end

function parent_displacement_field(solver::PlaneSolver,x)
    uI = solver.uI
    R = solver.R
    lambda = solver.lambda2
    mu = solver.mu2

    ux = uI/R*x[1]
    uy = -lambda/(lambda+2mu)*uI/R*x[2]
    return [ux,uy]
end

function product_displacement_field(solver::PlaneSolver,x)
    uI = solver.uI
    L = solver.L
    R = solver.R
    lambda = solver.lambda1
    mu = solver.mu1
    t0 = solver.theta0

    ux = ((L-R)-(x[1]-R))/(L-R)*uI
    uy = (lambda/(lambda+2mu)*uI/(L-R) + (3lambda+2mu)/(lambda+2mu)*t0/3)*x[2]

    return [ux,uy]
end

function displacement_field(solver::PlaneSolver,x)
    R = solver.R
    if x[1] < R
        return parent_displacement_field(solver,x)
    else
        return product_displacement_field(solver,x)
    end
end

end


function simulate_exp_gpreal(t,alpha,beta,ndev)
#
# Function for simulating a GP based upon an exponential correlation
# matrix using an analytic form for the Cholesky decomposition and
# a computation in O(N) operations.  The autocorrelation function is:
#
#    K_ij = alpha * exp(-beta*(|t_i-t_j|))
#
# Requirements:
#  - alpha should be a real positive number.
#  - The times t *must* be sorted in order from least to greatest.
#  - beta may be complex
#  - ndev: normal deviates drawn from N(0,1) with the same length as time vector t.
# Output:
#  - data is a GP drawn from this correlation function with length nt
#
nt = length(t)
data = zeros(eltype(beta),nt)
data[nt] = sqrt(alpha)*ndev[nt]
gamma = zero(eltype(beta))
for i=nt-1:-1:1
  gamma = exp(-beta*(t[i+1]-t[i]))
#  println(i," ",abs(sqrt(1.0-gamma^2)))
  data[i] = sqrt(1.0-gamma^2)*sqrt(alpha)*ndev[i]+gamma*data[i+1]
end
return data
end


function CostGradient(X::Array,y::Vector;λ=0, Norm=false)

   #Size of the X Parameters
    m=size(X,1)
    n=size(X,2)
    #if Normalize

    if Norm==true
       μ=mean(X,dims=1)
       σ=std(X,dims=1)
       X=(X.-μ)./σ
    else
        μ=zeros(1,n)
        σ=ones(1,n)
    end

    #Add vector of ones to X parameters
    X=[ones(m) X]

    CostFunction=function (θ)
                    se=(X*θ).-y
                    J=(0.5./m).*((se'*se)+λ*(θ[2:end]'*θ[2:end]))
                    return J
                end
    θr=zeros(n+1)
    Gradient=function (θ)
             θr.=θ
             θr[1]=0
             g=(1/m).*(X'*((X*θ).-y)) .+ ((λ./m).*θr)
        return g
            end

    return CostFunction, Gradient, μ, σ
end


using AQFED.Math
const SqrtEpsilon = sqrt(eps())
# totalVariance is vol^2*τ
export blackScholesFormula, blackScholesVega

function blackScholesFormula(isCall::Bool, strike::Number, spot::Number, totalVariance::Number, driftDf::Number, discountDf::Number)
    sign = 1
    if !isCall
        sign = -1
    end
    forward = spot / driftDf
    if totalVariance < eps()
        price = discountDf * max(sign * (forward - strike), 0)
        return price
    elseif spot < eps()
        if isCall
            return 0
        else
            return discountDf * strike
        end
    elseif strike < eps()
        if isCall
            return discountDf * forward
        else
            return 0
        end
    else
        sqrtVar = sqrt(totalVariance)
        d1 = log(forward / strike)/sqrtVar + sqrtVar/2
        d2 = d1 - sqrtVar
        nd1 = normcdf(sign * d1)
        nd2 = normcdf(sign * d2)
        price = sign * discountDf * (forward * nd1 - strike * nd2)
        return price
    end
end

function blackScholesVega(strike::Number,
	spot::Number,
	variance::Number,
	driftDf::Number,
	discountDf::Number,
	tte::Number)::Number
	forward = spot / driftDf
	sqrtVar = sqrt(variance)
	d1 = 1.0/sqrtVar*log(forward/strike) + 0.5*sqrtVar
	nd1 = normpdf(d1)
	vega = discountDf * forward * nd1 * sqrt(tte)
	return vega
end


##
##  s p e c i a l . j l  Special Functions
##

# Lambert W function, inverse of x -> x exp(x)
# its derivative is 1 / (1+zw) / exp(zw) where zw = lambertW(z)
#
function lambertW(x::Real)
    if x <  -1.0/exp(1.0); return NaN, NaN;  end
    if x == -1.0/exp(1.0); return -Inf, Inf; end

    local w0::Real = 1.0, w1::Real, w2::Real
    w1 = w0 - (w0 * exp(w0) - x)/((w0 + 1.0) * exp(w0) -
        (w0 + 2.0) * (w0 * exp(w0) - x)/(2.0 * w0 + 2.0))

    while abs(w1 - w0) > 1e-15
        w0 = w1
        w1 = w0 - (w0 * exp(w0) - x)/((w0 + 1.0) * exp(w0) -
            (w0 + 2.0) * (w0 * exp(w0) - x)/(2.0 * w0 + 2.0))
    end

    w2 = 1.0 / (1 + w1) / exp(w1)

    return w1, w2
end


export Random, random
"""
    random(x;args)

Randomly generate a vector of size `x`, based on provided (optional arguments) :
- `L` is the typical wavelength (default is `L=1`),
- `s` is the (real) Sobolev index regularity (default is `s=∞`, smooth data),
- `λ` is the length of spatial localization (default is `λ=∞`, no localization),
- `a` is the amplitude of the returned vector (default is `a=1`).

The vector is generated through randomly chosen Fourier coefficients,
multiplied with weigth `w=10^(-|k|L/(2π))` if `s=∞`, or `w=1/(1+9(|k|L/(2π))^(s+1/2))` otherwise.
If `λ≠∞`, the function in spatial variables is multiplied by `exp(-|x/λ|^2)`,
and in any case normalized to have maximum absolute value 1.

"""
function random( x;L=1,s=Inf,λ=Inf,a=1 )

    k = Mesh( x ).k
    if s == Inf
        w = 10 .^(-abs.(k*L/(2*π)))
    else
        w = 1 ./( 1 .+ 9*abs.(k*L/(2*π)).^(s+1/2) )
    end
    if λ == Inf
        φ = zero(x).+1
    else
        φ = exp.(-abs.((x/λ).^2))
    end

    θ = 2*π*rand(Float64,length(x))
    r = rand(Float64,length(x))
    Fourier = r.*exp.(-1im.*θ).*w
    Physic = real.(ifft(Fourier)).*φ
    return Physic./(maximum(Physic)-minimum(Physic))*a
end

"""
    Random(param;args)

Randomly generated initial data, based on provided (optional arguments) :
- `L` is the typical wavelength (default is `L=1`),
- `s` is the (real) Sobolev index regularity (default is `s=∞`),
- `λ` is the length of spatial localization (default is `λ=∞`, no localization),
- `a` is the couple of amplitudes of the surface deformation, and velocity (default is `a=(1,1)`).

Return an initial data `init::InitialData`,  to be used in initial-value problems `Problem(model, init, param)`,
with `init.η` and `init.v` two samples of `random(x;L,s,λ)`,
where `x` is the set of collocation points generated by `Mesh(param)`.

"""
struct Random <: InitialData

    η
    v
    label :: String
    info  :: String

    function Random(param;L=1,s=Inf,λ=Inf,a=(1,1))
        mesh=Mesh(param);x=mesh.x;
        η = random( x; L=L,s=s,λ=λ,a=a[1] )
        v = random( x; L=L,s=s,λ=λ,a=a[2] )
        init=Init(x,η,v)
        if s == Inf s="∞" end
        if λ == Inf λ="∞" end
        label = "randomly generated"
        info = "Randomly generated initial data with \n\
        ├───typical wavelength: L = $L,\n\
        ├─────regularity index: s = $s,\n\
        ├─spatial localization: λ = $λ,\n\
        ├────surface amplitude: $(a[1]),\n\
        └───velocity amplitude: $(a[2])."
    	new( init.η,init.v,label,info )

    end

end


using BenchmarkTools
using LinearAlgebra

function progonka!(u,a,b,c,f)
    N = size(f,1)
    α=similar(f)
    β=similar(f)
    α[2] = -c[1]/b[1]
    β[2] = f[1]/b[1]
    @inbounds @fastmath for i in 2:N-1 #Forward Sweep
        α[i+1]=-c[i]/(a[i-1]*α[i]+b[i])
        β[i+1]=(f[i]-a[i-1]*β[i])/(a[i-1]*α[i]+b[i])
    end
    u[N]=(f[N]-a[N-1]*β[N])/(a[N-1]*α[N]+b[N])
    @inbounds @fastmath for i in N-1:-1:1 #Backward Sweep
        u[i]=α[i+1]*u[i+1]+β[i+1]
    end
    u
end

progonka(a,b,c,f)=progonka!(similar(f),a,b,c,f)

function tprogonka()
    N=1000
    a=-rand(N-1)
    c=-rand(N-1)
    b=2.0.+rand(N)
    f=rand(N)

    A=Tridiagonal(a,b,c)
    utri=A\f
    upro=progonka(a,b,c,f)
    @assert utri≈upro
    @btime utri=$A\$f
    @btime upro=progonka($a,$b,$c,$f)
    nothing
end


# v0.6.0

function magicsquareodd(base::Int)
    if base & 1 == 0 || base < 3; error("base must be odd and >3") end

    square = fill(0, base, base)
    r, number = 1, 1
    size = base * base

    c = div(base, 2) + 1
    while number ≤ size
        square[r, c] = number
        fr = r == 1 ? base : r - 1
        fc = c == base ? 1 : c + 1
        if square[fr, fc] != 0
            fr = r == base ? 1 : r + 1
            fc = c
        end
        r, c = fr, fc
        number += 1
    end

    return square
end

for n in 3:2:7
    println("Magic square with size $n - magic constant = ", div(n ^ 3 + n, 2))
    println("----------------------------------------------------")
    square = magicsquareodd(n)
    for i in 1:n
        println(square[i, :])
    end
    println()
end


import MathematicalSets.AbstractSet

# ===========================================================================
# This is a collection of concrete subtypes of `AbstractSet` for the purposes
# of testing and code coverage.
#
# For use cases of `MathematicalSets` in external libraries, see the section
# `Usage` in the documentation.
# ===========================================================================

"""
    DummySet <: MathematicalSets.AbstractSet

The dummy set is a set with no fields. It is used to cover the `MathematicalSets`
functions.
"""
struct DummySet <: MathematicalSets.AbstractSet end

"""
    Singleton{T<:AbstractVector} <: AbstractSet

A singleton is a set with one element.

### Fields

- `element` -- element in the singleton
"""
struct Singleton{T<:AbstractVector} <: MathematicalSets.AbstractSet
    element::T
end

MathematicalSets.space_dimension(s::Singleton) = length(s.element)

# the dimension of a point is conventionally 0
MathematicalSets.dimension(s::Singleton) = 0

# the following metric properties for a singleton are usually zero, although
# depending on the applicaton one could be interested to have e.g. a Dirac δ
# if commented, defaults to returning `nothing` in `MathematicalSets`:
MathematicalSets.volume(s::Singleton) = 0.
MathematicalSets.surface_area(s::Singleton) = 0.

"""
    Ball{T} <: AbstractSet

Euclidean ``n``-dimensional ball in ``\\mathbb{R}^n``.

### Fields

- `center` -- vector that represents the center of the ball
- `radius` -- scalar that represents the radius of the ball
"""
struct Ball{T, VT <: AbstractVector{T}} <: MathematicalSets.AbstractSet
    center::VT
    radius::T
end
radius(S::Ball) = S.radius
center(S::Ball) = S.center

MathematicalSets.space_dimension(S::Ball) = length(S.center)
MathematicalSets.dimension(S::Ball) = space_dimension(S)

# convenience constructor: if only the radius and dimension are supplied,
# the ball is centered in the origin
Ball(R::T, n::Int) where T = Ball(zeros(T, n), R)

using SpecialFunctions # needed for the gamma function

function MathematicalSets.volume(S::Ball)
    n, R = dimension(S), radius(S)
    return π^(n/2) * R^n / gamma(n/2 + 1)
end

function MathematicalSets.surface_area(S::Ball)
    n, R = dimension(S), radius(S)
    return π^(n/2) * n * R^(n-1) / gamma(n/2 + 1)
end


# =========================================================================== #
# Types
# =========================================================================== #

abstract type DistributionType
end

struct UniformDistribution{T} <: DistributionType
    a::T
    b::T
end
function UniformDistribution(a::T, b::T) where {T}
    a = min(a, b)
    b = max(a, b)
    UniformDistribution{T}(a, b)
end
function UniformDistribution() where {T}
    UniformDistribution{T}(0.0, 1.0)
end

struct NormalDistribution{T} <: DistributionType
    mu::T
    sigma::T
end
function NormalDistribution(mu::T, sigma::T) where {T}
    if sigma < 0
        error("sigma must be positive")
    end
    NormalDistribution{T}(mu, sigma)
end
function NormalDistribution(;mu = 0.0, sigma = 1.0)
    NormalDistribution(mu, sigma)
end

struct CauchyDistribution{T} <: DistributionType
    mu::T
    c::T
end
function CauchyDistribution(mu::T, c::T) where {T}
    CauchyDistribution{T}(mu, c)
end
function CauchyDistribution(;mu = 0.0, c = 1.0)
    CauchyDistribution(mu, c)
end

struct MultiNormalDistribution{T} <: DistributionType
    mu::Vector{T}
    sigma::Matrix{T}
    L::LowerTriangular{T, Matrix{T}}
    inv_sigma::Matrix{T}
end
function MultiNormalDistribution(mu::Vector{T}, sigma::Matrix{T}) where {T}
    n_dim = length(mu)
    sz = size(sigma)
    if sz[1] != sz[2]
        error("Non-square sigma matrix")
    end
    if sz[1] != n_dim
        error("dimension mismatch between mu and sigma")
    end

    fact = cholesky(sigma)

    id = Matrix{T}( one(T) * I, n_dim, n_dim )
    inv_sigma = fact.L \ ( fact.U \ id )
    MultiNormalDistribution{T}(mu, sigma, fact.L, inv_sigma)
end


"""
    TDist(ν)

The *Students T distribution* with `ν` degrees of freedom has probability density function

```math
f(x; d) = \\frac{1}{\\sqrt{d} B(1/2, d/2)}
\\left( 1 + \\frac{x^2}{d} \\right)^{-\\frac{d + 1}{2}}
```

```julia
TDist(d)      # t-distribution with d degrees of freedom

params(d)     # Get the parameters, i.e. (d,)
dof(d)        # Get the degrees of freedom, i.e. d
```

External links

[Student's T distribution on Wikipedia](https://en.wikipedia.org/wiki/Student%27s_t-distribution)

"""
struct TDist{T<:Real} <: ContinuousUnivariateDistribution
    ν::T
    TDist{T}(ν::T) where {T <: Real} = new{T}(ν)
end

function TDist(ν::T) where {T <: Real}
    @check_args(TDist, ν > zero(ν))
    return TDist{T}(ν)
end

TDist(ν::T, ::NoArgCheck) where {T<:Real} = TDist{T}(ν)
TDist(ν::Integer) = TDist(float(ν))

@distr_support TDist -Inf Inf

#### Conversions
convert(::Type{TDist{T}}, ν::Real) where {T<:Real} = TDist(T(ν))
convert(::Type{TDist{T}}, d::TDist{S}) where {T<:Real, S<:Real} = TDist(T(d.ν), NoArgCheck())

#### Parameters

dof(d::TDist) = d.ν
params(d::TDist) = (d.ν,)
@inline partype(d::TDist{T}) where {T<:Real} = T

#### Statistics

mean(d::TDist{T}) where {T<:Real} = d.ν > 1 ? zero(T) : T(NaN)
median(d::TDist{T}) where {T<:Real} = zero(T)
mode(d::TDist{T}) where {T<:Real} = zero(T)

function var(d::TDist{T}) where T<:Real
    ν = d.ν
    isinf(ν) && return one(T)
    ν > 2 ? ν / (ν - 2) :
    ν > 1 ? T(Inf) : T(NaN)
end

skewness(d::TDist{T}) where {T<:Real} = d.ν > 3 ? zero(T) : T(NaN)

function kurtosis(d::TDist{T}) where T<:Real
    ν = d.ν
    ν > 4 ? 6 / (ν - 4) :
    ν > 2 ? T(Inf) : T(NaN)
end

function entropy(d::TDist{T}) where T <: Real
    isinf(d.ν) && return entropy( Normal(zero(T), one(T)) )
    h = d.ν/2
    h1 = h + 1//2
    h1 * (digamma(h1) - digamma(h)) + log(d.ν)/2 + lbeta(h, 1//2)
end

#### Evaluation & Sampling

@_delegate_statsfuns TDist tdist ν

rand(rng::AbstractRNG, d::TDist) = randn(rng) / ( isinf(d.ν) ? 1 : sqrt(rand(rng, Chisq(d.ν))/d.ν) )

function cf(d::TDist{T}, t::Real) where T <: Real
    isinf(d.ν) && return cf(Normal(zero(T), one(T)), t)
    t == 0 && return complex(1)
    h = d.ν/2
    q = d.ν/4
    complex(2(q*t^2)^q * besselk(h, sqrt(d.ν) * abs(t)) / gamma(h))
end

gradlogpdf(d::TDist{T}, x::Real) where {T<:Real} = isinf(d.ν) ? gradlogpdf(Normal(zero(T), one(T)), x) : -((d.ν + 1) * x) / (x^2 + d.ν)


doc"""
    rsi(ta, n=14; wilder=false)

Relative Strength Index

```math
    RSI = \frac{EMA(Up, n)}{EMA(Up, n) + EMA(Dn, n)}
```
"""
function rsi(ta::TimeArray, n::Int=14; wilder::Bool=false)
    ret = diff(ta.values)
    ups = zeros(size(ta.values, 1) - 1, size(ta.values, 2))
    dns = zeros(size(ta.values, 1) - 1, size(ta.values, 2))

    @inbounds for i in 1:size(ta.values, 1) - 1
        for j in 1:size(ta.values, 2)
            if ret[i, j] >= 0
                ups[i, j] += ret[i, j]
            else
                dns[i, j] += ret[i, j]
            end
        end
    end

    upsema = ema(ups, n, wilder=wilder)
    dnsema = abs.(ema(dns, n, wilder=wilder))
    rs     = upsema ./ dnsema

    res  = @. 100 - (100 / (1 + rs))

    cname   = String[]
    cols    = colnames(ta)
    for c in 1:length(cols)
        push!(cname, string(cols[c], "_rsi_", n))
    end

    TimeArray(ta.timestamp[n+1:end], res, cname, ta.meta)
end

doc"""
    cci(ohlc, ma=20, c=0.015)

Commodity Channel Index

```math
    CCI = \frac{P_{typical} - SMA(P_{typical})}{c \times \sigma(P_{typical})}
```

**Reference**

- https://en.wikipedia.org/wiki/Commodity_channel_index

"""
function cci(ohlc::TimeArray, ma::Int=20, c::AbstractFloat=0.015)
    pt = typical(ohlc)
    cci = safediv.((pt .- sma(pt, ma)), moving(mean_abs_dev, pt, ma)) ./ c
    rename(cci, "cci")
end

doc"""
    chaikinoscillator(ohlcv, fast=3, slow=10; h="High", l="Low", c="Close")

**Chaikin Oscillator**

Developed by Marc Chaikin

**Formula**

```math
    Chaikin\ OSC = EMA(ADL, fast) - EMA(ADL, slow)
```

where the [`adl`](@ref) is the Accumulation/Distribution Line.

**Reference**

- [StockCharts]
  (http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:chaikin_oscillator)
"""
function chaikinoscillator(ohlcv::TimeArray, fast::Integer=3, slow::Integer=10;
                           h="High", l="Low", c="Close")
    _adl = adl(ohlcv, h=h, l=l, c=c)
    rename(ema(_adl, fast) .- ema(_adl, slow), ["chaikinoscillator"])
end

doc"""
    macd(ta, fast=12, slow=26, signal=9; wilder=false)

Moving Average Convergence / Divergence

```math
    \begin{align*}
        MACD Bar & = DIF - DEM \\
        DIF & = EMA(P_{close}, fast) - EMA(P_{close}, slow) \\
        DEM & = EMA(DIF, 9) \tag{signal}
    \end{align*}
```

**Return**:

`TimeArray` with 3 columns `["macd", "dif", "signal"]`.

If the input is a multi-column `TimeArray`, the new column names will be
`["A_macd", "B_macd", "A_dif", "B_dif", "A_signal", "B_signal"]`.

"""
function macd(ta::TimeArray{T,N},
              fast::Int=12, slow::Int=26, signal::Int=9;
              wilder::Bool=false) where {T,N}
    dif = ema(ta, fast, wilder=wilder) .- ema(ta, slow, wilder=wilder)
    sig = ema(dif, signal, wilder=wilder)
    osc = dif .- sig

    cols = ["macd", "dif", "signal"]
    new_cols = (N > 1) ? gen_colnames(ta.colnames, cols) : cols

    merge(merge(osc, dif), sig, colnames=new_cols)
end

doc"""
    roc(ta, n)

**Rate of Change**

**Formula**:

```math
    roc = \frac{close_{t} - close_{t-n}}{close_{t-n}}
```

**Reference**:

- [Wikipedia](https://en.wikipedia.org/wiki/Momentum_(technical_analysis))

"""
function roc(ta::TimeArray, n::Integer)
    prev = lag(ta, n)
    rename((ta .- prev) ./ prev, ["$c\_roc_$n" for c ∈ ta.colnames])
end

doc"""
    aroon(ohlc, n=25; h="High", l="Low")

**Aroon Oscillator**

**Formula**

```math
    \begin{align*}
        up   & = \frac{\mathop{argmax}(High_{t-n} \dots High_t)}{n} \times 100 \\
        down & = \frac{\mathop{argmin}(Low_{t-n} \dots Low_t)}{n} \times 100 \\
        osc  & = up - down
    \end{align*}
```

**Reference**

- [StockCharts]
  (http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:aroon_oscillator)
"""
function aroon(ohlc::TimeArray, n::Integer=25; h="High", l="Low")
    up = rename(moving(indmax, ohlc[h], n) ./ n .* 100, "up")
    dn = rename(moving(indmin, ohlc[l], n) ./ n .* 100, "dn")
    osc = rename(up .- dn, "osc")

    merge(merge(up, dn), osc)
end

doc"""
    adx(ohlc, n=14; h="High", l="Low", c="Close")

**Average Directional Movement Index**

Developed by J. Welles Wilder.
This Implementation follows StockCharts.

**Reference**

- [wikipedia]
  (https://en.wikipedia.org/wiki/Average_directional_movement_index)

- [StockCharts]
  (http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:average_directional_index_adx)
"""
function adx(ohlc::TimeArray, n::Integer=14; h="High", l="Low", c="Close")
    dm = relu(merge(ohlc[h] .- lag(ohlc[h]), lag(ohlc[l]) .- ohlc[l]))
    dm.values[findmin(dm.values, 2)[2]] = 0

    dm = wilder_smooth(dm, n)

    tr = wilder_smooth(truerange(ohlc, h=h, l=l, c=c), n)

    di = (dm ./ tr) .* 100
    di = rename(di, ["+di", "-di"])

    dx = @. abs((di["+di"] - di["-di"]) / (di["+di"] + di["-di"])) * 100
    adx = wilder_smooth(dx, n, dx=true)

    rename(merge(adx, merge(dx, di)), ["adx", "dx", "+di", "-di"])
end

doc"""
    stochasticoscillator(ohlc, n=14, fast_d=3, slow_d=3; h="High", l="Low", c="Close")

**Stochastic Oscillator**

A.k.a *%K%D*, or *KD*

**Parameter**

- `n`: period of fast(raw) `%K`

- `fast_d`: MA period of fast `%D`

- `slow_d`: MA period of slow `%D`

**Formula**

```math
    \begin{align*}
        fast\ \%K & = \frac{Close_t - \max(High_{t-n}, \dots, High_t)}
            {\max(High_{t-n}, \dots, High_t) - \min(Low_{t-n}, \dots, Low_t)}
            \times 100 \\
        fast\ \%D & = SMA(fast\ \%K) \\
        slow\ \%D & = SMA(fast\ \%D)
    \end{align*}
```

**Reference**

- [Wikipedia]
  (https://en.wikipedia.org/wiki/Stochastic_oscillator)

- [FMLabs]
  (http://www.fmlabs.com/reference/default.htm?url=StochasticOscillator.htm)
"""
function stochasticoscillator(ohlc::TimeArray, n::Integer=14, fast_d::Integer=3,
                              slow_d::Integer=3; h="High", l="Low", c="Close")
    high = moving(maximum, ohlc[h], n)
    low = moving(minimum, ohlc[l], n)
    fast_k = rename((ohlc[c] .- low) ./ (high .- low) .* 100, "fast_k")
    fast_d = rename(sma(fast_k, fast_d), "fast_d")
    slow_d = rename(sma(fast_d, slow_d), "slow_d")
    merge(merge(fast_k, fast_d), slow_d)
end


module MyInterpolations
export my_lin_interp
function my_lin_interp(grid,vals)
    function func(x::Real)
        if x <= grid[1]
            return (vals[2] - vals[1])/(grid[2] - grid[1])*(x - grid[1]) + vals[1]
        elseif x >= grid[end]
            return (vals[end] - vals[end-1])/(grid[end] - grid[end-1])*(x - grid[end]) + vals[end]
        else
            index = searchsortedlast(grid,x)
            return (vals[index+1] - vals[index])/(grid[index+1] - grid[index])*(x - grid[index]) + vals[index]
        end
    end
end
end


using sparsePolyChaos
using PolyChaos

###
### Multivariate sparse PCE
###
maxDeg = 10
interactions = 3

model(x,y) = (x^4 - 6 * x^2  + 3) + (x^2 - 1) * (y^2 - 1) + (y^3 - 3 * y) # He_4(x) + He_2(x) * He_2(y) + He_3(y)
# model(x,y) = (x^2 - 1) * (y^2 - 1) # He2(x) * He2(y)
# model(x,y) = (x^2 - 1) # He2(x)

### Setup and compute PCE coefficients of x ###
k = 2
op = GaussOrthoPoly(maxDeg)
mop = MultiOrthoPoly([op for i in 1:k], maxDeg)
L = dim(mop)

pce, Ap, p, R², Q² = sparsePCE(mop, model; pMax = maxDeg, jMax = interactions)

println("Basis polynomials: ", Ap)
println("PCE coefficients: ", pce)
println("Max degree:: ", p)
println("R² error: ", R²)
println("Q² error: ", Q²)


#############################################################################
# JuMP
# An algebraic modelling langauge for Julia
# See http://github.com/JuliaOpt/JuMP.jl
#############################################################################
# corr_sdp.jl
#
# Given three random variables A,B,C and given bounds on two of the three
# correlation coefficients:
# -0.2 <= ρ_AB <= -0.1
#  0.4 <= ρ_BC <=  0.5
# We can use the following property of the correlations:
# [  1    ρ_AB  ρ_AC ]
# [ ρ_AB   1    ρ_BC ]  ≽ 0
# [ ρ_AC  ρ_BC   1   ]
# To determine bounds on ρ_AC by solving a SDP
#############################################################################

using JuMP

m = Model()

@variable(m, X[1:3,1:3], SDP)

# Diagonal is 1s
@constraint(m, X[1,1] == 1)
@constraint(m, X[2,2] == 1)
@constraint(m, X[3,3] == 1)

# Bounds on the known correlations
@constraint(m, X[1,2] >= -0.2)
@constraint(m, X[1,2] <= -0.1)
@constraint(m, X[2,3] >=  0.4)
@constraint(m, X[2,3] <=  0.5)

# Find upper bound
@objective(m, Max, X[1,3])
solve(m)
println("Maximum value is ", getvalue(X)[1,3])
@assert +0.8719 <= getvalue(X)[1,3] <= +0.8720

# Find lower bound
@objective(m, Min, X[1,3])
solve(m)
println("Minimum value is ", getvalue(X)[1,3])
@assert -0.9779 >= getvalue(X)[1,3] >= -0.9799


module moo_functions

function quickavg(xs::Vector{Float64}; first::Int64 = 1, last::Int64 = length(xs))::Float64
    out = 0.0
    for k in first:last
        out += xs[k]
    end
    return out/(last - first + 1)
end

function zdt1(x::Vector{Float64})::Tuple{Float64, Float64}
    g  = 1.0 + 9.0*quickavg(x, first = 2)
    f1 = x[1]
    f2 = g * (1.0 - sqrt(f1/g))
    return (f1, f2)
end

function zdt2(x::Vector{Float64})::Tuple{Float64, Float64}
    g  = 1.0 + 9.0*quickavg(x, first = 2)
    f1 = x[1]
    f2 = g * (1.0 - (f1/g)^2)
    return (f1, f2)
end

function zdt3(x::Vector{Float64})::Tuple{Float64, Float64}
    g  = 1.0 + 9.0*quickavg(x, first = 2)
    f1 = x[1]
    f2 = g * (1.0 - sqrt(f1/g) - f1/g * sin(10*pi*f1))
    return (f1, f2)
end

function zdt4(x::Vector{Float64})::Tuple{Float64, Float64}
    g  = 1.0 + 10*(length(x)-1)
    for k in 2:length(x)
        g += x[k]^2 - 10*cos(4*pi*x[k])
    end
    f1 = x[1]
    f2 = g * (1.0 - sqrt(f1/g))
    return (f1, f2)
end

function zdt6(x::Vector{Float64})::Tuple{Float64, Float64}
    g  = 1.0 + 9.0*quickavg(x, first = 1)^0.25
    f1 = 1.0 - exp(-4.0*x[1])*sin(6*pi*x[1])^6
    f2 = g*(1.0 - (f1/g)^2)
    return (f1, f2)
end

function zdt()
    return Dict{Int8, Function}(
        1 => zdt1,
        2 => zdt2,
        3 => zdt3,
        4 => zdt4,
        6 => zdt6
    )
end

function opt_hvs()
    return Dict{Int8, Float64}(
        1 => 120.0 + 2/3,
        2 => 120.0 + 1/3,
        3 => 128.77811613069076060,
        4 => 120.0 + 2/3,
        6 => 117.51857519692037009,
    )
end

end


### A Pluto.jl notebook ###
# v0.14.5

using Markdown
using InteractiveUtils

# ╔═╡ c930a0fa-4ef8-4963-b4b2-0d576ecdde7e
using Pkg, DrWatson

# ╔═╡ 919b41e5-7e43-45e0-8676-373b77ac6208
begin
	@quickactivate "NumericalMethodsforEngineers"
	using Roots, Plots
	using Symbolics, SymbolicUtils, Latexify, LaTeXStrings, ModelingToolkit
	using NumericalMethodsforEngineers, Test
end

# ╔═╡ c34bdc36-5013-4096-9cb2-581ad9e02239
md" ## f-03-01.jl"

# ╔═╡ 0b1514c5-eded-4f60-ac31-6f27a4deff1d
begin
	@variables f x
	f = exp(x) - x^4
	L(f)
end

# ╔═╡ 7c4288f0-38d9-4c39-b6b1-789aba5465b6
f_j = build_function(f, [x], expression=Val{false});

# ╔═╡ c7c87623-9c41-4a68-8673-47a71c67f083
begin
	y = -10.0:0.1:10.0
	plot(y, f_j.(y), leg=false)
	hline!([0.0])
	vline!([fzeros(f_j, [-1, 2])])
end

# ╔═╡ 2708dfb1-09dd-458c-8b5f-2e2ec3d800c4
begin
	D = Differential(x)
	fp_s = expand_derivatives(D(f))
	fp_j = build_function(fp_s, [x], expression=Val{false})
	L(fp_s)
end

# ╔═╡ 1dd78a44-b910-4bb2-8af0-9a1ab3ee4a51
Roots.newton(f_j, fp_j, 8)        # 8.6131694564414

# ╔═╡ 8054c3fd-3ecb-4c2a-b606-7fd518d750a3
begin
	fpp_s = expand_derivatives(D(D(f)))
	fpp_j = build_function(fpp_s, [x], expression=Val{false})
	L(fpp_s)
end

# ╔═╡ 7e2f4bd6-4f9a-4a72-8e70-e5fc56f806ee
halley(f_j, fp_j, fpp_j, 8)

# ╔═╡ 3e022154-6933-4ba7-8cf9-2d48d3cbb27f
secant_method(f_j, [8, 8.5])

# ╔═╡ 26cd9127-ae9e-4f09-97e4-d10f514e2164
md" ## End of f-03-01.jl"

# ╔═╡ Cell order:
# ╟─c34bdc36-5013-4096-9cb2-581ad9e02239
# ╠═c930a0fa-4ef8-4963-b4b2-0d576ecdde7e
# ╠═919b41e5-7e43-45e0-8676-373b77ac6208
# ╠═0b1514c5-eded-4f60-ac31-6f27a4deff1d
# ╠═7c4288f0-38d9-4c39-b6b1-789aba5465b6
# ╠═c7c87623-9c41-4a68-8673-47a71c67f083
# ╠═2708dfb1-09dd-458c-8b5f-2e2ec3d800c4
# ╠═1dd78a44-b910-4bb2-8af0-9a1ab3ee4a51
# ╠═8054c3fd-3ecb-4c2a-b606-7fd518d750a3
# ╠═7e2f4bd6-4f9a-4a72-8e70-e5fc56f806ee
# ╠═3e022154-6933-4ba7-8cf9-2d48d3cbb27f
# ╟─26cd9127-ae9e-4f09-97e4-d10f514e2164


module UnionFindAlgo

export UnionFind, union!, find, isconnected

"""
Union find with the *Weighted quick union* (WQU)
algorithm.

- `id`: the parent node's id
- `sz`: size of the tree
"""
mutable struct UnionFind
    id::Vector{Int}
    sz::Vector{Int}
    ncomp::Int
    UnionFind(n::Int) = new(collect(1:n), fill(1, n), n)
end

function UnionFind(data::NamedTuple)
    uf = UnionFind(data.nv)
    edges = data.edges
    for (p, q) in edges
        union!(uf, p, q)
    end
    uf
end

function union!(uf, p, q)
    i = find(uf, p)
    j = find(uf, q)
    i == j && return
    # weighted quick-union: always connect the root
    # of smaller tree to the root of the larger tree
    if (uf.sz[i] < uf.sz[j])
        uf.id[i] = j
        uf.sz[j] += uf.sz[i]
    else
        uf.id[j] = i
        uf.sz[i] += uf.sz[j]
    end
    uf.ncomp -= 1
    return nothing
end

"""
    find(uf, p)

Find the root of p. The component in WQUPC is defined by
the root of each node.

In theory path compression can help further flatten
the tree. There are two ways to conduct path compression:

1. find the root for `p` and then assign all nodes on the path
   from `p` to root to the newly found root. This requires running
   the `find` twice.
2. Directly change parent to grandparent, this will halve the
   height in the subtree. In code: `uf.id[i] = uf.id[uf.id[i]]`

In practice, the performance gain from path compression is not
significant. On `largeUF.txt`(V=1e6, E=2e6) approach (2) gives
about 1% improvement. The difference might be larger for larger
dataset.
"""
function find(uf, p)
    i = uf.id[p]
    while uf.id[i] != i
        # path compression: set parent to grandparent
        # this trick halves the subtree each time
        uf.id[i] = uf.id[uf.id[i]]
        # go to the parent node
        i = uf.id[i]
    end
    i
end

isconnected(uf, p, q) = find(uf, p) == find(uf, q)

end


module NewtonsMethod

using ForwardDiff, LinearAlgebra

export newtonroot

function iterativesolver(f, x₀, checkconvergence, updatex)
    xold = x₀.*10
    fold = 10*f(x₀)
    xnew = x₀
    fnew = f(xnew)
    iter = 1
    while !checkconvergence(xold, xnew, fold, fnew, iter)
        xold = xnew
        xnew = updatex(f, xold)
        fold = fnew
        fnew = f(xnew)
        iter += 1
    end
    return(value = xnew, iter=iter)
end

function newtonroot(f, f′; x₀, tol=1e-7, maxiter=1000)
    res = iterativesolver(f,
                          x₀,
                          (xold, xnew, fold, fnew, iter) -> norm(xnew - xold) < 1e-4 || iter > maxiter,
                          (f, x) -> x - f(x) / f′(x))
    if res.iter <= maxiter
        return res
    else
        return (value = nothing, iter=res.iter)
    end
end

function newtonroot(f; x₀, tol=1e-7, maxiter=1000)
    return newtonroot(f, x -> ForwardDiff.derivative(f, x); x₀, tol, maxiter)
end

end


"""
    center(A)

Centers columns of a 2d array

# Arguments

- A = 2d array of floats

# Value

2d array of floats

"""
function center(A::AbstractArray{Float64,2})

    # Row means of A
    m = mean(A, dims=1)
    # Initialize centered matrix
    W = zeros(size(A))

    # Subtract column means
    W .= A .- m

    return W
end

"""
    cov_est(resid)

Estimates error variance and its variance/covariance

# Arguments

- resid = 2d array of floats consisting of the residuals

# Value

Tuple
- est: 2d array of floats; estimate
- varest: 2d array of floats; variance/covariance estimate

2d array of floats

"""
function cov_est(resid::AbstractArray{Float64,2})

    # Dimensions of residuals
    n = size(resid, 1)
    p = size(resid, 2)

    # Centered residual matrix
    W = center(resid)

    # Allocate space for the estimates and their variances
    est = zeros(p, p)
    varEst = zeros(p, p)

    # Loop through the possible entries
    for i = 1:p
        for j = i:p
            # Multiply the ith and jth columns
            ww = W[:,i].*W[:,j]

            if i==j # Diagonal elements
                est[i,i] = (n/(n-1)) * mean(ww)
                varEst[i,i] = (n/(n-1)^2) * var(ww)
            else # Non-diagonal elements
                est[i,j] = est[j,i] = (n/(n-1)) * mean(ww)
                varEst[i,j] = varEst[j,i] = (n/(n-1)^2) * var(ww)
            end

        end
    end

    return est, varEst
end

"""
    shrink_sigma(resid, targetType)

Estimates variance of errors and the shrinkage coefficient

# Arguments

- resid = 2d array of floats consisting of the residuals
- targetType = string indicating the target type toward which to shrink the
  variance. Acceptable inputs are "A", "B", "C", and "D".
    - "A": Target is identity matrix
    - "B": Target is diagonal matrix with constant diagonal
    - "C": Target is has same diagonal element, and same off-diagonal element
    - "D": Target is diagonal matrix with unequal entries

# Value

Tuple
- sigma: 2d array of floats; shrunk estimated variance of errors
- lambda: floating scalar; estimated shrinkage coefficient
  (0 = no shrinkage, 1 = complete shrinkage)

# Reference

Ledoit, O., & Wolf, M. (2003). Improved estimation of the covariance matrix
    of stock returns with an application to portfolio selection. Journal of
    empirical finance, 10(5), 603-621.

"""
function shrink_sigma(resid::AbstractArray{Float64,2}, targetType::String)

    # Dimensions of resid
    (n, p) = size(resid)

    # Estimates and the variance of the error variance
    (est, varEst) = cov_est(resid)

    if targetType=="A"  # Shrink to identity
        # Create identity target matrix
        T = Matrix{Float64}(I, p, p)
        # Estimate optimal lambda
        lambda = sum(varEst) / sum((est-T).^2)

    elseif targetType=="B"  # Shrink to common variance
        # Create target matrix
        T = Matrix{Float64}(I, p, p) * mean(diag(est))
        # Estimate optimal lambda
        lambda = sum(varEst) / sum((est-T).^2)

    elseif targetType=="C"  # Shrink to equal variance and covariance
        v = mean(diag(est))
        c = (sum(est) - sum(diag(est))) / (n*(n-1))
        # Create target matrix
        T = fill(c,(p,p)) + (v-c) * Matrix{Float64}(I, p, p)
        # Estimate optimal lambda
        lambda = sum(varEst) / sum((est-T).^2)

    elseif targetType=="D"  # Shrink to zero correlation
        v = diag(est)
        # Create target matrix
        T = diagm(0 => v)
        # Estimate optimal lambda
        lambda = (sum(varEst) - sum(diag(varEst))) /
                 (sum(est.^2) - sum(diag(est).^2))
    end

    return lambda*T + (1-lambda)*est, lambda
end


"""
Computes mean resultant vector length for circular data.

	1. α: sample of angles in radians
    - w: number of incidences in case of binned angle data
    - d: spacing of bin centers for binned data, used to correct for bias in estimation of r, in radians
    - dims: compute along this dimension(default=1)

	return: mean resultant length
"""
function circ_r(α; w = ones(size(α)), d = 0, dims = 1)
  # compute weighted sum of cos and sin of angles
  r = sum(w .* cis.(α); dims)

  # obtain length
  r = abs.(r) ./ sum(w; dims)

  # for data with known spacing, apply correction factor to correct for bias in the estimation of r (see Zar, p. 601, equ. 26.16)
  if d != 0
    c = d / 2 / sin(d / 2)
    r *= c
  end
  length(r) == 1 ? r[1] : r
end

"""
Computes the mean direction for circular data.

	1. α: sample of angles in radians
    - w: weightings in case of binned angle data
    - dims: compute along this dimension(default=1)

	return:
	- μ: mean direction
	- ul: upper 95% confidence limit
	- ll: lower 95% confidence limit
"""
function circ_mean(α; w = ones(size(α)), dims = 1)
  # compute weighted sum of cos and sin of angles
  r = sum(w .* cis.(α); dims)

  # obtain mean by
  μ = angle.(r)
  μ = length(μ) == 1 ? μ[1] : μ

  # confidence limits
  t = circ_confmean(α; xi = 0.05, w, d = 0, dims)
  ul = μ .+ t
  ll = μ .- t

  return (; μ, ul, ll)
end

"""
Transforms p-axial data to a common scale.

  1. α: sample of angles in radians
  2. p: number of modes(default=1)

  return: transformed data
"""
circ_axial(α, p = 1) = mod2pi.(α * p)

"""
Computes the median direction for circular data.

  1. α: sample of angles in radians

  return: median direction
"""
function circ_median(α::AbstractVector)
  α = mod2pi.(α)
  n = length(α)

  dd = circ_dist2(α)
  m1 = dropdims(count(>=(0), dd; dims = 1), dims = 1)
  m2 = dropdims(count(<=(0), dd; dims = 1), dims = 1)

  dm = abs.(m1 .- m2)
  m = minimum(dm)
  md = circ_mean(α[dm.==m]).μ
  αμ = circ_mean(α).μ
  if abs(circ_dist(αμ, md)) > abs(circ_dist(αμ, md + π))
    md = mod2pi(md + π)
  end
  return md
end

"""
Computes circular standard deviation for circular data (equ. 26.20, Zar).

	1. α: sample of angles in radians
   - w: weightings in case of binned angle data
   - d: spacing of bin centers for binned data, used to correct for bias in estimation of r, in radians
   - dims: compute along this dimension(default=1)

	return:
  - s: angular deviation
  - s0: circular standard deviation
"""
function circ_std(α; w = ones(size(α)), d = 0, dims = 1)
  # compute mean resultant vector length
  r = circ_r(α; w, d, dims)

  s = sqrt.(2 .* (1 .- r))      # 26.20
  s0 = sqrt.(-2 .* log.(r))   # 26.21
  return (; s, s0)
end

"""
Computes circular variance for circular data (equ. 26.17/18, Zar).

	1. α: sample of angles in radians
  - w: number of incidences in case of binned angle data
  - d: spacing of bin centers for binned data, used to correct for bias in estimation of r, in radians
	- dims: compute along this dimension(default=1)

	return:
	- S: circular variance 1-r
	- s: angular variance 2(1-r)
"""
function circ_var(α; w = ones(size(α)), d = 0, dims = 1)
  # compute mean resultant vector length
  r = circ_r(α; w, d, dims)

  # apply transformation to var
  S = 1 .- r
  s = 2 * S
  return (; S, s)
end

"""
Computes the confidence limits on the mean for circular data.

	1. α: sample of angles in radians
   - xi: (1-xi) confidence limits are computed, default=0.05
   - w: number of incidences in case of binned angle data
   - d: spacing of bin centers for binned data, used to correct for bias in estimation of r, in radians
   - dims: compute along this dimension(default=1)

	return: mean ± d yields upper/lower (1-xi)% confidence limit
"""
function circ_confmean(α; xi = 0.05, w = ones(size(α)), d = 0, dims = 1)
  # compute ingredients for conf. lim.
  r = circ_r(α; w, d, dims)
  n = sum(w; dims)
  R = n .* r
  c2 = quantile(Chisq(1), 1 - xi)

  # check for resultant vector length and select appropriate formula
  t = zeros(size(r))

  for i = 1:length(r)
    if r[i] < 0.9 && r[i] > sqrt(c2 / 2 / n[i])
      t[i] = sqrt((2 * n[i] * (2 * R[i]^2 - n[i] * c2)) / (4 * n[i] - c2))  # equ. 26.24
    elseif r[i] >= 0.9
      t[i] = sqrt(n[i]^2 - (n[i]^2 - R[i]^2) * exp(c2 / n[i]))      # equ. 26.25
    else
      t[i] = NaN
      @warn "Requirements for confidence levels not met."
    end
  end

  # apply final transform
  t = acos.(clamp.(t ./ R, -1, 1))
  t = length(t) == 1 ? t[1] : t
end

"""
Calculates a measure of angular skewness.

	1. α: sample of angles in radians
    - w: weightings in case of binned angle data
    - dims: compute along this dimension(default=1)

	return:
	- b: skewness (from Pewsey)
	- b0: alternative skewness measure (from Fisher)
"""
function circ_skewness(α; w = ones(size(α)), dims = 1)
  # compute neccessary values
  R = circ_r(α; w, d = 0, dims)
  θ = circ_mean(α; w, dims).μ
  _, ρ₂, μ₂ = circ_moment(α; w, p = 2, cent = false, dims)

  # compute skewness
  if ndims(θ) > 0
    θ₂ = repeat(θ, outer = Int.(size(α) ./ size(θ)))
  else
    θ₂ = θ
  end
  b = sum(w .* sin.(2 * circ_dist(α, θ₂)); dims) ./ sum(w; dims)
  b0 = ρ₂ .* sin.(circ_dist(μ₂, 2θ)) ./ (1 .- R) .^ (3 / 2)   # (formula 2.29)
  b = length(b) == 1 ? b[1] : b
  b0 = length(b0) == 1 ? b0[1] : b0
  return (; b, b0)
end

"""
Calculates a measure of angular kurtosis.

  1. α: sample of angles in radians
    - w: weightings in case of binned angle data
    - dims: compute along this dimension(default=1)

  return:
  - k: kurtosis (from Pewsey)
  - k0: kurtosis (from Fisher)
"""
function circ_kurtosis(α; w = ones(size(α)), dims = 1)
  # compute mean direction
  R = circ_r(α; w, d = 0, dims)
  θ = circ_mean(α; w, dims).μ
  _, ρ₂, _ = circ_moment(α; w, p = 2, cent = true, dims)
  _, _, μ₂ = circ_moment(α; w, p = 2, cent = false, dims)

  # compute skewness
  if ndims(θ) > 0
    θ₂ = repeat(θ, outer = Int.(size(α) ./ size(θ)))
  else
    θ₂ = θ
  end
  k = sum(w .* cos.(2 * circ_dist(α, θ₂)); dims) ./ sum(w; dims)
  k0 = (ρ₂ .* cos.(circ_dist(μ₂, 2θ)) .- R .^ 4) ./ (1 .- R) .^ 2    # (formula 2.30)
  k = length(k) == 1 ? k[1] : k
  k0 = length(k0) == 1 ? k0[1] : k0
  return (; k, k0)
end

"""
Calculates the complex p-th centred or non-centred moment of the angular data in angle.

  1. α: sample of angles in radians
   - w: number of incidences in case of binned angle data
   - p: p-th moment to be computed, default=1
   - cent: if true, central moments are computed, default = false
   - dims: compute along this dimension(default=1)

  return:
  - mp: complex p-th moment
  - ρp: magnitude of the p-th moment
  - μp: angle of th p-th moment
"""
function circ_moment(α; w = ones(size(α)), p = 1, cent = false, dims = 1)
  if cent
    θ = circ_mean(α; w, dims).μ
    if ndims(θ) > 0
      v = Int.(size(α) ./ size(θ))
      θ = repeat(θ, outer = v)
    end
    α = circ_dist(α, θ)
  end

  n = size(α, dims)
  c̄ = sum(cos.(p * α) .* w; dims) / n
  s̄ = sum(sin.(p * α) .* w; dims) / n
  mp = c̄ .+ im * s̄
  ρp = abs.(mp)
  μp = angle.(mp)
  mp = length(mp) == 1 ? mp[1] : mp
  ρp = length(ρp) == 1 ? ρp[1] : ρp
  μp = length(μp) == 1 ? μp[1] : μp
  return (; mp, ρp, μp)
end

"""
Pairwise difference α-β around the circle computed efficiently.

  1. α: sample of linear random variable
  2. β: sample of linear random variable or one single angle

  return: matrix with differences
"""
circ_dist(α, β) = angle.(cis.(α) ./ cis.(β))

"""
All pairwise difference α-β around the circle computed efficiently.

  1. α: sample of linear random variable
  2. β: sample of linear random variable

  return: matrix with pairwise differences
"""
circ_dist2(α) = circ_dist2(α, α)
circ_dist2(α, β) = angle.(cis.(α) ./ cis.(β'))

"""
Computes descriptive statistics for circular data.

  1. α: sample of angles in radians
    - w: weightings in case of binned angle data
    - d: spacing of bin centers for binned data, used to correct for bias in estimation of r, in radians

  return: descriptive statistics
"""
function circ_stats(α::AbstractVector; w = ones(size(α)), d = 0)
  # mean
  mean = circ_mean(α; w).μ
  # median
  median = circ_median(α)
  # variance
  var = circ_var(α; w, d)
  # standard deviation
  std, std0 = circ_std(α; w, d)
  # skewness
  skewness, skewness0 = circ_skewness(α; w)
  # kurtosis
  kurtosis, kurtosis0 = circ_kurtosis(α; w)

  return (; mean, median, var,std,std0, skewness,skewness0,kurtosis,kurtosis0)
end

"""
Computes Rayleigh test for non-uniformity of circular data.

	H0: the population is uniformly distributed around the circle
  HA: the populatoin is not distributed uniformly around the circle
	Assumption: the distribution has maximally one mode and the data is
							sampled from a von Mises distribution!

	1. α: sample of angles in radians
    - w: number of incidences in case of binned angle data
    - d: spacing of bin centers for binned data, used to correct for bias in estimation of r, in radians

	return:
	- p: p-value of Rayleigh's test
	- z: value of the z-statistic
"""
function circ_rtest(α; w = ones(size(α)), d = 0)
  r = circ_r(α; w, d)
  n = sum(w)

  # compute Rayleigh's R (equ. 27.1)
  R = n * r

  # compute Rayleigh's z (equ. 27.2)
  z = R^2 / n

  # compute p value using approxation in Zar, p. 617
  p = exp(sqrt(1 + 4n + 4(n^2 - R^2)) - (1 + 2n))
  return (; p, z)
end

"""
Computes Omnibus or Hodges-Ajne test for non-uniformity of circular data.

	H0: the population is uniformly distributed around the circle
	HA: the population is not distributed uniformly around the circle
	Alternative to the Rayleigh and Rao's test. Works well for unimodal,
	bimodal or multimodal data. If requirements of the Rayleigh test are met, the latter is more powerful.

	1. α: sample of angles in radians
    - sz: step size for evaluating distribution, default 1 degree
    - w: number of incidences in case of binned angle data

	return:
	- p: p-value
	- m: minimum number of samples falling in one half of the circle
"""
function circ_otest(α; sz = deg2rad(1), w = ones(size(α)))
  α = mod2pi.(α)
  n = sum(w)
  dg = 0:sz:π

  m1 = zeros(size(dg))
  m2 = zeros(size(dg))
  for i = 1:length(dg)
    m1[i] = sum((α .> dg[i]) .& (α .< π + dg[i]) .* w)
    m2[i] = n - m1[i]
  end
  m = min(minimum(m1), minimum(m2))

  if n > 50
    # approximation by Ajne (1968)
    A = π * sqrt(n) / 2 / (n - 2m)
    p = sqrt(2π) / A * exp(-π^2 / 8 / A^2)
  else
    # exact formula by Hodges (1955)
    # p = 2^(1-n) * (n-2m) * nchoosek(n,m)  # revised below for numerical stability
    p = exp((1 - n) * log(2) + log(n - 2m) + loggamma(n + 1) - loggamma(m + 1) - loggamma(n - m + 1))
  end
  return (; p, m)
end

"""
Calculates Rao's spacing test by comparing distances between points on a circle to those expected from a uniform distribution.
  H0: Data is distributed uniformly around the circle.
  H1: Data is not uniformly distributed around the circle.

  Alternative to the Rayleigh test and the Omnibus test. Less powerful
  than the Rayleigh test when the distribution is unimodal on a global scale but uniform locally.

  Due to the complexity of the distributioin of the test statistic, we resort to the tables published by
  Russell, Gerald S. and Levitin, Daniel J.(1995) An expanded table of probability values for rao's spacing test, Communications in Statistics - Simulation and Computation
  Therefore the reported p-value is the smallest α level at which the test would still be significant.
  If the test is not significant at the α=0.1 level, we return the critical value for α = 0.05 and p = 0.5.

  1. α: sample of angles in radians

  return:
  - p: smallest p-value at which test would be significant
  - u: computed value of the test-statistic u
  - UC: critical value of the test statistic at sig-level
"""
function circ_raotest(α)
  # for the purpose of the test, convert to angles
  α = sort(rad2deg.(α))
  n = length(α)

  # compute test statistic
  u = 0
  λ = 360 / n
  for j = 1:n-1
    ti = α[j+1] - α[j]
    u += abs(ti - λ)
  end

  tn = 360 - α[n] + α[1]
  u = 0.5(u + abs(tn - λ))

  getVal = (N, u) -> begin
      # Table II from Russel and Levitin, 1995
      α = [0.001, 0.01, 0.05, 0.10]
      table = [
        4 247.32 221.14 186.45 168.02
        5 245.19 211.93 183.44 168.66
        6 236.81 206.79 180.65 166.30
        7 229.46 202.55 177.83 165.05
        8 224.41 198.46 175.68 163.56
        9 219.52 195.27 173.68 162.36
        10 215.44 192.37 171.98 161.23
        11 211.87 189.88 170.45 160.24
        12 208.69 187.66 169.09 159.33
        13 205.87 185.68 167.87 158.50
        14 203.33 183.90 166.76 157.75
        15 201.04 182.28 165.75 157.06
        16 198.96 180.81 164.83 156.43
        17 197.05 179.46 163.98 155.84
        18 195.29 178.22 163.20 155.29
        19 193.67 177.08 162.47 154.78
        20 192.17 176.01 161.79 154.31
        21 190.78 175.02 161.16 153.86
        22 189.47 174.10 160.56 153.44
        23 188.25 173.23 160.01 153.05
        24 187.11 172.41 159.48 152.68
        25 186.03 171.64 158.99 152.32
        26 185.01 170.92 158.52 151.99
        27 184.05 170.23 158.07 151.67
        28 183.14 169.58 157.65 151.37
        29 182.28 168.96 157.25 151.08
        30 181.45 168.38 156.87 150.80
        35 177.88 165.81 155.19 149.59
        40 174.99 163.73 153.82 148.60
        45 172.58 162.00 152.68 147.76
        50 170.54 160.53 151.70 147.05
        75 163.60 155.49 148.34 144.56
        100 159.45 152.46 146.29 143.03
        150 154.51 148.84 143.83 141.18
        200 151.56 146.67 142.35 140.06
        300 148.06 144.09 140.57 138.71
        400 145.96 142.54 139.50 137.89
        500 144.54 141.48 138.77 137.33
        600 143.48 140.70 138.23 136.91
        700 142.66 140.09 137.80 136.59
        800 142.00 139.60 137.46 136.33
        900 141.45 139.19 137.18 136.11
        1000 140.99 138.84 136.94 135.92]

      ridx = findfirst(table[:, 1] .>= N)
      cidx = findfirst(table[ridx, 2:end] .< u)

      if isnothing(cidx)
        UC = table[ridx, end-1]
        p = 0.5
      else
        UC = table[ridx, cidx+1]
        p = α[cidx]
      end
      return p, UC
    end

  # get critical value from table
  p, UC = getVal(n, u)

  return (; p, u, UC)
end

"""
Computes V test for non-uniformity of circular data with a specified mean direction.

  H0: the population is uniformly distributed around the circle
  HA: the population is not distributed uniformly around the circle but has a mean of `m`.

  !!!note
    Not rejecting H0 may mean that the population is uniformly distributed around the circle OR
    that it has a mode but that this mode is not centered at `m`.

  The V test has more power than the Rayleigh test and is preferred if there is reason to believe in a specific mean direction.

  1. α: sample of angles in radians
  - m: suspected mean direction, default=0
  - w: number of incidences in case of binned angle data
  - d: spacing of bin centers for binned data, used to correct for bias in estimation of r, in radians

  return:
  - p: p-value of V test
  - v: value of the V statistic
"""
function circ_vtest(α; m = 0, w = ones(size(α)), d = 0)
  # compute some ingredients
  r = circ_r(α; w, d)
  μ, = circ_mean(α; w)
  n = sum(w)

  # compute Rayleigh's R (equ. 27.1)
  R = n * r

  # compute the V statistic (equ. 27.5)
  v = R * cos(μ - m)

  # compute u (equ. 27.6)
  u = v * sqrt(2 / n)

  # compute p-value from one tailed normal approximation
  p = 1 - cdf(Normal(), u)

  return (; p, v)
end

"""
Tests for significance of the median.

  H0: the population has median angle `md`
  HA: the population has not median angle `md`

  1. α: sample of angles in radians
  - md: median to test, default=0

  return: p-value
"""
function circ_medtest(α; md = 0)
  n = length(α)

  # compute deviations from median
  d = circ_dist(α, md)

  n1 = sum(d .< 0)
  n2 = sum(d .> 0)

  # compute p-value with binomial test
  sum(pdf.(Binomial(n, 0.5), [0:min(n1, n2); max(n1, n2):n]))
end

"""
One-Sample test for the mean angle.
  H0: the population has mean `m`.
  HA: the population has not mean `m`.

  !!!note: This is the equvivalent to a one-sample t-test with specified mean direction.

  1. α: sample of angles in radians
  - m: assumed mean direction, default=0
  - w: number of incidences in case of binned angle data
  - d: spacing of bin centers for binned data, used to correct for bias in estimation of r, in radians
  - xi: alpha level of the test

  return:
  - h: false if H0 can not be rejected, true otherwise
  - μ: mean
  - ul: upper (1-xi) confidence level
  - ll: lower (1-xi) confidence level
"""
function circ_mtest(α; xi = 0.05, m = 0, w = ones(size(α)), d = 0)
  # compute ingredients
  μ, = circ_mean(α; w)
  t = circ_confmean(α; xi, w, d)
  ul = μ + t
  ll = μ - t

  # compute test via confidence limits (example 27.3)
  h = abs(circ_dist2(m, μ)) > t

  return (; h, μ, ul, ll)
end

"""
Parametric Watson-Williams multi-sample test for equal means. Can be used as a one-way ANOVA test for circular data.

  H0: the s populations have equal means
  HA: the s populations have unequal means

  !!! note
  Use with binned data is only advisable if binning is finer than 10 deg.
  In this case, α is assumed to correspond to bin centers.

  The Watson-Williams two-sample test assumes underlying von-Mises distributrions.
  All groups are assumed to have a common concentration parameter k.

  1. α: angles in radians
  2. idx: indicates which population the respective angle in α comes from, 1:s
    - w: number of incidences in case of binned angle data

  return:
  - p: p-value of the Watson-Williams multi-sample test. Discard H0 if p is small.
  - F: F statistics
"""
function circ_wwtest(α, idx; w = ones(size(α)))
  # number of groups
  u = unique(idx)
  s = length(u)

  # number of samples
  n = sum(w)

  # compute relevant quantitites
  pn = zeros(s)
  pr = zeros(s)
  for t = 1:s
    pidx = idx .== u[t]
    pn[t] = sum(pidx .* w)
    pr[t] = circ_r(α[pidx]; w = w[pidx])
  end

  r = circ_r(α; w)
  rw = sum(pn .* pr) / n

  # make sure assumptions are satisfied
  checkAssumption =
    (rw, n) -> begin
      if n >= 11 && rw < 0.45
        @warn "Test not applicable. Average resultant vector length < 0.45."
      elseif n < 11 && n >= 7 && rw < 0.5
        @warn "Test not applicable. Average number of samples per population 6 < x < 11 and average resultant vector length < 0.5."
      elseif n >= 5 && n < 7 && rw < 0.55
        @warn "Test not applicable. Average number of samples per population 4 < x < 7 and average resultant vector length < 0.55."
      elseif n < 5
        @warn "Test not applicable. Average number of samples per population < 5."
      end
    end
  checkAssumption(rw, mean(pn))

  # test statistic
  kk = circ_kappa(rw)
  β = 1 + 3 / (8 * kk)    # correction factor
  A = sum(pr .* pn) - r * n
  B = n - sum(pr .* pn)

  F = β * (n - s) * A / (s - 1) / B
  # p = 1 - fcdf(F, s - 1, n - s)
  p = 1 - cdf(FDist(s - 1, n - s), F)

  # fprintf('\nANALYSIS OF VARIANCE TABLE (WATSON-WILLIAMS TEST)\n\n');
  # fprintf('%s\t\t\t\t%s\t%s\t\t%s\t\t%s\t\t\t%s\n', ' ' ,'d.f.', 'SS', 'MS', 'F', 'P-Value');
  # fprintf('--------------------------------------------------------------------\n');
  # fprintf('%s\t\t\t%u\t\t%.2f\t%.2f\t%.2f\t\t%.4f\n', 'Columns', s-1 , A, A/(s-1), F, pval);
  # fprintf('%s\t\t%u\t\t%.2f\t%.2f\n', 'Residual ', n-s, B, B/(n-s));
  # fprintf('--------------------------------------------------------------------\n');
  # fprintf('%s\t\t%u\t\t%.2f', 'Total   ',n-1,A+B);
  # fprintf('\n\n')

  # table = ["Source",'d.f.','SS','MS','F','P-Value';
  #          'Columns', s-1 , A, A/(s-1), F, pval;
  #          'Residual ', n-s, B, B/(n-s), [], [];
  #          'Total',n-1,A+B,[],[],[]]

  return (; p, F)
end

"""
Parametric two-way ANOVA for circular data with interations.

  !!!note
  The test assumes underlying von-Mises distributrions.
  All groups are assumed to have a common concentration parameter k, between 0 and 2.

  1. α: angles in radians
  2. idp: indicates the level of factor 1 (1:p)
  3. idq: indicates the level of factor 2 (1:q)
    - inter: whether to include effect of interaction
    - fn: string array containing names of the factors

  return:
  - p: pvalues for factors and interaction
  - s: statistic of each p value
"""
function circ_hktest(α, idp, idq; inter = true, fn = ['A', 'B'])
  # number of groups for every factor
  pu = unique(idp)
  p = length(pu)
  qu = unique(idq)
  q = length(qu)

  # number of samples
  n = length(α)

  # compute important sums for the test statistics
  cn = zeros(p, q)
  cr = zeros(p, q)
  pm = zeros(p)
  pr = zeros(p)
  pn = zeros(p)
  qm = zeros(q)
  qr = zeros(q)
  qn = zeros(q)
  for pp = 1:p
    p_id = idp .== pu[pp] # indices of factor1 = pp
    for qq = 1:q
      q_id = idq .== qu[qq] # indices of factor2 = qq
      idx = p_id .& q_id
      cn[pp, qq] = sum(idx)     # number of items in cell
      cr[pp, qq] = cn[pp, qq] * circ_r(α[idx]) # R of cell
    end
    # R and mean angle for factor 1
    pr[pp] = sum(p_id) * circ_r(α[p_id])
    pm[pp] = circ_mean(α[p_id]).μ
    pn[pp] = sum(p_id)
  end

  # R and mean angle for factor 2
  for qq = 1:q
    q_id = idq .== qu[qq]
    qr[qq] = sum(q_id) * circ_r(α[q_id])
    qm[qq] = circ_mean(α[q_id]).μ
    qn[qq] = sum(q_id)
  end

  # R and mean angle for whole sample (total)
  tr = n * circ_r(α)

  # estimate kappa
  kk = circ_kappa(tr / n)

  # different formulas for different width of the distribution
  if kk > 2
    # large kappa

    # effect of factor 1
    eff_1 = sum(pr .^ 2 ./ dropdims(sum(cn, dims = 2), dims = 2)) - tr .^ 2 / n
    df_1 = p - 1
    ms_1 = eff_1 / df_1

    # effect of factor 2
    eff_2 = sum(qr .^ 2 ./ dropdims(sum(cn, dims = 1), dims = 1)) - tr .^ 2 / n
    df_2 = q - 1
    ms_2 = eff_2 / df_2

    # total effect
    eff_t = n - tr .^ 2 / n
    df_t = n - 1

    m = mean(cn)

    if inter
      # correction factor for improved F statistic
      β = 1 / (1 - 1 / (5kk) - 1 / (10kk^2))

      # residual effects
      eff_r = n - sum(cr .^ 2 ./ cn)
      df_r = p * q * (m - 1)
      ms_r = eff_r / df_r

      # interaction effects
      eff_i =
        sum(cr .^ 2 ./ cn) - sum(qr .^ 2 ./ qn) - sum(pr .^ 2 ./ pn) +
        tr .^ 2 / n
      df_i = (p - 1) * (q - 1)
      ms_i = eff_i / df_i

      # interaction test statistic
      FI = ms_i / ms_r
      pI = 1 - cdf(FDist(df_i, df_r), FI)

    else
      # residual effect
      eff_r = n - sum(qr .^ 2 ./ qn) - sum(pr .^ 2 ./ pn) + tr .^ 2 / n
      df_r = (p - 1) * (q - 1)
      ms_r = eff_r / df_r

      # interaction effects
      eff_i = []
      df_i = []
      ms_i = []

      # interaction test statistic
      FI = []
      pI = NaN
      β = 1
    end

    # compute all test statistics as F = β * MS(A) / MS(R)
    F1 = β * ms_1 / ms_r
    p1 = 1 - cdf(FDist(df_1, df_r), F1)

    F2 = β * ms_2 / ms_r
    p2 = 1 - cdf(FDist(df_2, df_r), F2)
    s = [F1, F2, FI]
  else
    # small kappa

    # correction factor
    rr = besseli(1, kk) / besseli(0, kk)
    f = 2 / (1 - rr^2)

    chi1 = f * (sum(pr .^ 2 ./ pn) - tr .^ 2 / n)
    df_1 = 2 * (p - 1)
    p1 = 1 - cdf(Chisq(df_1), chi1)

    chi2 = f * (sum(qr .^ 2 ./ qn) - tr .^ 2 / n)
    df_2 = 2 * (q - 1)
    p2 = 1 - cdf(Chisq(df_2), chi2)

    chiI =
      f * (
        sum(cr .^ 2 ./ cn) - sum(pr .^ 2 ./ pn) - sum(qr .^ 2 ./ qn) +
        tr .^ 2 / n
      )
    df_i = (p - 1) * (q - 1)
    pI = 1 - cdf(Chisq(df_i), chiI)
    s = [chi1, chi2, chiI]
  end
  p = [p1, p2, pI]
  #
  # if kk>2
  #
  #   fprintf('\nANALYSIS OF VARIANCE TABLE (HIGH KAPPA MODE)\n\n');
  #
  #   fprintf('%s\t\t\t\t%s\t%s\t\t%s\t\t%s\t\t\t%s\n', ' ' ,'d.f.', 'SS', 'MS', 'F', 'P-Value');
  #   fprintf('--------------------------------------------------------------------\n');
  #   fprintf('%s\t\t\t\t%u\t\t%.2f\t%.2f\t%.2f\t\t%.4f\n', fn{1}, df_1 , eff_1, ms_1, F1, p1);
  #   fprintf('%s\t\t\t\t%u\t\t%.2f\t%.2f\t%.2f\t\t%.4f\n', fn{2}, df_2 , eff_2, ms_2, F2, p2);
  #   if (inter)
  #       fprintf('%s\t\t%u\t\t%.2f\t%.2f\t%.2f\t\t%.4f\n', 'Interaction', df_i , eff_i, ms_i, FI, pI);
  #   end
  #   fprintf('%s\t\t%u\t\t%.2f\t%.2f\n', 'Residual ', df_r, eff_r, ms_r);
  #   fprintf('--------------------------------------------------------------------\n');
  #   fprintf('%s\t\t%u\t\t%.2f', 'Total   ',df_t,eff_t);
  #   fprintf('\n\n')
  # else
  #   fprintf('\nANALYSIS OF VARIANCE TABLE (LOW KAPPA MODE)\n\n');
  #
  #   fprintf('%s\t\t\t\t%s\t%s\t\t\t%s\n', ' ' ,'d.f.', 'CHI2', 'P-Value');
  #   fprintf('--------------------------------------------------------------------\n');
  #   fprintf('%s\t\t\t\t%u\t\t%.2f\t\t\t%.4f\n', fn{1}, df_1 , chi1, p1);
  #   fprintf('%s\t\t\t\t%u\t\t%.2f\t\t\t%.4f\n', fn{2}, df_2 , chi2, p2);
  #   if (inter)
  #       fprintf('%s\t\t%u\t\t%.2f\t\t\t%.4f\n', 'Interaction', df_i , chiI, pI);
  #   end
  #   fprintf('--------------------------------------------------------------------\n');
  #   fprintf('\n\n')
  #
  # end
  return (; p, s)
end

"""
A parametric two-sample test to determine whether two concentration parameters are different.

  H0: The two concentration parameters are equal.
  HA: The two concentration parameters are different.

  Assumptions: both samples are drawn from von Mises type distributions and their joint resultant vector length should be > .7

  1. α₁: sample of angles in radians
  2. α₂: sample of angles in radians

  return:
  - p: p-value that samples have different concentrations
  - f: f-statistic calculated
"""
function circ_ktest(α₁, α₂)
  n1 = length(α₁)
  n2 = length(α₂)

  R1 = n1 * circ_r(α₁)
  R2 = n2 * circ_r(α₂)

  # make sure that r̄ > .7
  r̄ = (R1 + R2) / (n1 + n2)

  if r̄ < 0.7
    @warn "Resultant vector length should be > 0.7"
  end

  # calculate test statistic
  f = ((n2 - 1) * (n1 - R1)) / ((n1 - 1) * (n2 - R2))
  if f > 1
    p = 2 * (1 - cdf(FDist(n1, n2), f))
  else
    f = 1 / f
    p = 2 * (1 - cdf(FDist(n2, n1), f))
  end
  return (; p, f)
end

"""
Tests for symmetry about the median.
  H0: the population is symmetrical around the median
  HA: the population is not symmetrical around the median

  1. α: sample of angles in radians

  return: p-value
"""
function circ_symtest(α)
  # compute median
  md = circ_median(α)

  # compute deviations from median
  d = circ_dist(α, md)

  # compute wilcoxon sign rank test
  pvalue(SignedRankTest(d))
end

const kuipertable= [5	1.45800000000000	1.56500000000000	1.68200000000000	1.76300000000000	1.83800000000000	1.92000000000000	1.97000000000000;
                    6	1.47100000000000	1.58200000000000	1.71100000000000	1.79300000000000	1.86700000000000	1.95700000000000	2.02000000000000;
                    7	1.48300000000000	1.59800000000000	1.72700000000000	1.81400000000000	1.89400000000000	1.98700000000000	2.05100000000000;
                    8	1.49300000000000	1.60800000000000	1.74100000000000	1.83000000000000	1.91100000000000	2.00900000000000	2.07700000000000;
                    9	1.50000000000000	1.61800000000000	1.75200000000000	1.84300000000000	1.92600000000000	2.02700000000000	2.09700000000000;
                    10	1.50700000000000	1.62500000000000	1.76100000000000	1.85400000000000	1.93800000000000	2.04100000000000	2.11300000000000;
                    11	1.51300000000000	1.63100000000000	1.76900000000000	1.86200000000000	1.94800000000000	2.05300000000000	2.12600000000000;
                    12	1.51700000000000	1.63700000000000	1.77600000000000	1.87000000000000	1.95700000000000	2.06200000000000	2.13700000000000;
                    13	1.52200000000000	1.64200000000000	1.78200000000000	1.87600000000000	1.96400000000000	2.07100000000000	2.15400000000000;
                    14	1.52500000000000	1.64600000000000	1.78700000000000	1.88200000000000	1.97000000000000	2.07800000000000	2.15400000000000;
                    15	1.52900000000000	1.65000000000000	1.79100000000000	1.88700000000000	1.97600000000000	2.08500000000000	2.16100000000000;
                    16	1.53200000000000	1.65300000000000	1.79500000000000	1.89200000000000	1.98100000000000	2.09000000000000	2.16800000000000;
                    17	1.53400000000000	1.65700000000000	1.79900000000000	1.89600000000000	1.98600000000000	2.09600000000000	2.17300000000000;
                    18	1.53700000000000	1.65900000000000	1.80200000000000	1.89900000000000	1.99000000000000	2.10000000000000	2.17800000000000;
                    19	1.53900000000000	1.66200000000000	1.80500000000000	1.90300000000000	1.99300000000000	2.10400000000000	2.18300000000000;
                    20	1.54100000000000	1.66400000000000	1.80800000000000	1.90600000000000	1.99700000000000	2.10800000000000	2.18700000000000;
                    21	1.54300000000000	1.66700000000000	1.81000000000000	1.90800000000000	2	                2.11200000000000	2.19100000000000;
                    22	1.54500000000000	1.66900000000000	1.81300000000000	1.91100000000000	2.00300000000000	2.11500000000000	2.19400000000000;
                    23	1.54700000000000	1.67000000000000	1.81500000000000	1.91300000000000	2.00500000000000	2.11800000000000	2.19800000000000;
                    24	1.54900000000000	1.67200000000000	1.81700000000000	1.91600000000000	2.00800000000000	2.12100000000000	2.20100000000000;
                    25	1.55000000000000	1.67400000000000	1.81900000000000	1.91800000000000	2.01000000000000	2.12300000000000	2.20300000000000;
                    30	1.55600000000000	1.68100000000000	1.82600000000000	1.92600000000000	2.01900000000000	2.13400000000000	2.21500000000000;
                    35	1.56100000000000	1.68600000000000	1.83200000000000	1.93300000000000	2.02600000000000	2.14100000000000	2.22300000000000;
                    40	1.56500000000000	1.69000000000000	1.83700000000000	1.93800000000000	2.03200000000000	2.14800000000000	2.23000000000000;
                    45	1.56800000000000	1.69400000000000	1.84100000000000	1.94200000000000	2.03600000000000	2.15200000000000	2.23500000000000;
                    50	1.57100000000000	1.69700000000000	1.84400000000000	1.94600000000000	2.04000000000000	2.15700000000000	2.23900000000000;
                    100	1.58800000000000	1.71400000000000	1.86200000000000	1.96500000000000	2.06000000000000	2.17800000000000	2.26200000000000;
                    200	1.60000000000000	1.72600000000000	1.87600000000000	1.97900000000000	2.07500000000000	2.19400000000000	2.27900000000000;
                    500	1.61000000000000	1.73700000000000	1.88700000000000	1.99000000000000	2.08700000000000	2.20700000000000	2.29200000000000;
                    501	1.62000000000000	1.74700000000000	1.89800000000000	2.00100000000000	2.09800000000000	2.21800000000000	2.30300000000000]

"""
The Kuiper two-sample test tests whether the two samples differ significantly.
The difference can be in any property, such as mean location and dispersion.
It is a circular analogue of the Kolmogorov-Smirnov test.

  H0: The two distributions are identical.
  HA: The two distributions are different.

  1. α₁: sample of angles in radians
  2. α₂: sample of angles in radians
    - n: resolution at which the cdf are evaluated

  return:
  - p: p-value; the smallest of .10, .05, .02, .01, .005, .002, .001,
        for which the test statistic is still higher than the respective critical value.
        this is due to the use of tabulated values. if p>.1, pval is set to 1.
  - k: test statistic
  - K: critical value
"""
function circ_kuipertest(α₁, α₂; n = 100)
  n = length(α₁)
  m = length(α₂)

  # create cdfs of both samples
  ϕ₁, cdf1 = circ_samplecdf(α₁; n)
  _, cdf2 = circ_samplecdf(α₂; n)

  # maximal difference between sample cdfs
  dplus, gdpi = findmax([0; cdf1 .- cdf2])
  dminus, gdmi = findmax([0; cdf2 .- cdf1])

  # calculate k-statistic
  k = n * m * (dplus + dminus)

  # find p-value
  p, K = kuiperlookup(min(n, m), k / sqrt(n * m * (n + m)))
  K *= sqrt(n * m * (n + m))

  return (; p, k, K)
end

function kuiperlookup(n, k)
  α = [0.10, 0.05, 0.02, 0.01, 0.005, 0.002, 0.001]
  nn = kuipertable[:, 1]

  # find correct row of the table
  row = findfirst(n.==nn)
  if isnothing(row)
    # find closest value if no entry is present
    row = length(nn) - sum(n .< nn)
    if row == 0
      error("n too small.")
    else
      @warn "n=$n not found in table, using closest n=$(nn[row]) present."
    end
  end

  # find minimal p-value and test-statistic
  idx = findlast(kuipertable[row, 2:end] .< k)
  if isnothing(idx)
    p = 1
    K = NaN
  else
    p = α[idx]
    K = kuipertable[row, idx+1]
  end

  return (; p, K)
end

"""
Circular correlation coefficient for two circular random variables.

  1. α₁: sample of angles in radians
  2. α₂: sample of angles in radians

  return:
  - ρ: correlation coefficient
  - p: p-value
"""
function circ_corrcc(α₁, α₂)
  # compute mean directions
  n = length(α₁)
  ᾱ₁ = circ_mean(α₁).μ
  ᾱ₂ = circ_mean(α₂).μ

  # compute correlation coeffcient from p. 176
  num = sum(sin.(α₁ .- ᾱ₁) .* sin.(α₂ .- ᾱ₂))
  den = sqrt(sum(sin.(α₁ .- ᾱ₁) .^ 2) .* sum(sin.(α₂ .- ᾱ₂) .^ 2))
  ρ = num / den

  # compute pvalue
  l20 = mean(sin.(α₁ .- ᾱ₁) .^ 2)
  l02 = mean(sin.(α₂ .- ᾱ₂) .^ 2)
  l22 = mean((sin.(α₁ .- ᾱ₁) .^ 2) .* (sin.(α₂ .- ᾱ₂) .^ 2))

  ts = sqrt((n * l20 * l02) / l22) * ρ
  p = 2 * (1 - cdf(Normal(), abs(ts)))
  return (; ρ, p)
end

"""
Correlation coefficient between one circular and one linear random variable.

  1. α: sample of angles in radians
  2. x: sample of linear random variable

  return:
  - ρ: correlation coefficient
  - p: p-value
"""
function circ_corrcl(α, x)
  n = length(α)

  # compute correlation coefficent for sin and cos independently
  rxs = cor(x, sin.(α))
  rxc = cor(x, cos.(α))
  rcs = cor(sin.(α), cos.(α))

  # compute angular-linear correlation (equ. 27.47)
  ρ = sqrt((rxc^2 + rxs^2 - 2 * rxc * rxs * rcs) / (1 - rcs^2))

  # compute pvalue
  p = 1 - cdf(Chisq(2), n * ρ^2)
  return (; ρ, p)
end

"""
Computes an approximation to the ML estimate of the concentration parameter kappa of the von Mises distribution.

  1. α: angles in radians OR α is length resultant
    - w: number of incidences in case of binned angle data

  return: estimated value of kappa
"""
function circ_kappa(α; w = ones(size(α)))
  N = length(α)

  if N > 1
    R = circ_r(α; w)
  else
    R = α
  end

  if R < 0.53
    κ = 2R + R^3 + 5R^5 / 6
  elseif R >= 0.53 && R < 0.85
    κ = -0.4 + 1.39R + 0.43 / (1 - R)
  else
    κ = 1 / (R^3 - 4R^2 + 3R)
  end

  if N < 15 && N > 1
    if κ < 2
      κ = max(κ - 2 * (N * κ)^-1, 0)
    else
      κ = (N - 1)^3 * κ / (N^3 + N)
    end
  end
  κ
end

"""
Performs a simple agglomerative clustering of angular data.

1. α: sample of angles in radians
  - k: number of clusters desired, default=2

return:
- cid: cluster id for each entry of α
- α: sorted angles, matched with cid
- μ: mean direction of angles in each cluster
"""
function circ_clust(α; k = 2)

  n = length(α)
  n < k && error("Not enough data for clusters.")

  # prepare data
  cid = 1:n

  # main clustering loop
  num_unique = length(unique(cid))

  while (num_unique > k)

    # find centroid means...

    # calculate the means for each putative cluster
    μ = fill(NaN, n)
    for j = 1:n
      if sum(cid .== j) > 0
        μ[j], = circ_mean(α(cid .== j))
      end
    end

    # find distance between centroids...
    μdist = abs.(circ_dist2(μ))

    # find closest pair of clusters/datapoints
    mindist = minimum(μdist[tril(ones(size(μdist), size(μdist)), -1)==1])
    row, col = findall(μdist .== mindist)

    # update cluster id's
    cid[cid.==maximum(row)] = minimum(col)

    # update stop criteria
    num_unique = length(unique(cid))

  end

  # renumber cluster ids (so cids [1 3 7 10] => [1 2 3 4])
  cid2 = deepcopy(cid)
  uniquecids = unique(cid)
  for j = 1:length(uniquecids)
    cid[cid2.==uniquecids[j]] = j
  end

  # compute final cluster means
  μ = fill(NaN, num_unique)
  for j = 1:num_unique
    if sum(cid .== j) > 0
      μ[j], = circ_mean(α[cid.==j]')
    end
  end

  return (; cid, α, μ)
end

"""
Helper function for circ_kuipertest. Evaluates CDF of sample.

  1. α: sample of angles in radians
    - n: resolution at which the cdf are evaluated

  return:
  - ϕ: angles at which CDF are evaluated
  - c: CDF values at ϕ
"""
function circ_samplecdf(α; n = 100)
  ϕ = range(0, 2π, length = n + 1)

  # ensure all points in α are on interval [0, 2pi)
  α = sort(mod2pi.(α))

  dp = 1 / length(α) # incremental change in probability
  c = cumsum(map((l, r) -> dp * sum(l .<= α .< r), ϕ[1:end-1], ϕ[2:end]))
  ϕ = ϕ[1:end-1]

  return (; ϕ, c)
end


# # [Basic Epidemiology Models](@id epidemiology_example)
#
#md # [![](https://img.shields.io/badge/show-nbviewer-579ACA.svg)](@__NBVIEWER_ROOT_URL__/examples/covid/epidemiology.ipynb)

using AlgebraicPetri.Epidemiology

using Petri
using StochasticDiffEq
using Plots

using Catlab.Theories
using Catlab.CategoricalAlgebra.ShapeDiagrams
using Catlab.Graphics

display_wd(ex) = to_graphviz(ex, orientation=LeftToRight, labels=true);

# #### SIR Model:

# define model

sir = transmission ⋅ recovery

# get resulting petri net and visualize model

p_sir = decoration(F_epi(sir));
display_wd(sir)
#-
Graph(p_sir)

# define initial states and transition rates, then
# create, solve, and visualize ODE problem

u0 = [10.0, 1, 0];
p = [0.4, 0.4];

prob,cb = SDEProblem(p_sir,u0,(0.0,7.5),p);
sol = solve(prob,SRA1(),callback=cb)

plot(sol)

# #### SEIR Model:

# define model

sei = exposure ⋅ (illness ⊗ id(I)) ⋅ ∇(I)

seir = sei ⋅ recovery

# get resulting petri net and visualize model

p_seir = decoration(F_epi(seir));

display_wd(seir)
#-
Graph(p_seir)

# define initial states and transition rates, then
# create, solve, and visualize ODE problem

u0 = [10.0, 1, 0, 0];
p = [0.9, 0.2, 0.5];

prob,cb = SDEProblem(p_seir,u0,(0.0,15.0),p);
sol = solve(prob,SRA1(),callback=cb)

plot(sol)

# #### SEIRD Model:

# define model

seird = sei ⋅ Δ(I) ⋅ (death ⊗ recovery)

# get resulting petri net and visualize model

p_seird = decoration(F_epi(seird));

display_wd(seird)
#-
Graph(p_seird)

# define initial states and transition rates, then
# create, solve, and visualize ODE problem

u0 = [10.0, 1, 0, 0, 0];
p = [0.9, 0.2, 0.5, 0.1];

prob,cb = SDEProblem(p_seird,u0,(0.0,15.0),p);
sol = solve(prob,SRA1(),callback=cb)

plot(sol)


using Distributions

"""
    bubbleKernel(x::Float64, r::Float64)::Float64

Return 1.0 if dist <= r, otherwise 0.0.
"""
function bubbleKernel(x::Float64, r::Float64)::Float64
    return x <= r ? 1.0 : 0.0
end

"""
    gaussianKernel(x::Float64, r::Float64)::Float64

Return Gaussian(x) for μ=0.0 and σ = r/3.
(a value of σ = r/3 makes the training results comparable between different kernels
for same values of r).
"""
function gaussianKernel(x::Float64, r::Float64)::Float64
    return Distributions.pdf.(Distributions.Normal(0.0,r/3), x)
end


"""
    differentiate(f::Expr, x::Symbol)

Differentiate f with respect to x.
"""
function differentiate(f::Expr, x::Symbol)
    @assert f.head == :call
    return differentiate(Val{f.args[1]}, f, x)
end

"""
    differentiate(::Number, ::Symbol)

Differentiate a constant.
"""
function differentiate(::Number, ::Symbol)
    return 0
end

"""
    differentiate(f::Symbol, x::Symbol)

Differentiate a symbol.
"""
function differentiate(f::Symbol, x::Symbol)
    return f == x ? 1 : 0
end

"""
    differentiate(Val{:*}, f::Expr, x::Symbol)

Differentiate (fg)' using chain rule: ```(fgh)' = f'gh + fg'h + fgh'```.
"""
function differentiate(::Type{Val{:*}}, f::Expr, x::Symbol)
    op = first(f.args)
    @assert op == :*
    res_args = Any[:+]
    for i in 2:length(f.args)
        new_args = copy(f.args)
        new_args[i] = differentiate(f.args[i], x)
        push!(res_args, Expr(:call, new_args...))
    end
    return Expr(:call, res_args...)
end

"""
    differentiate(Val{:^}, f::Expr, x::Symbol)

Differentiate d/dx f^a = a * f ^ (a - 1) * diff(f, x)
"""
function differentiate(::Type{Val{:^}}, ex::Expr, x::Symbol)
    op, f, a = ex.args
    @assert op == :^
    df = differentiate(f, x)
    return :($a * $f ^ ($a - 1) * $df)
end

"""
    differentiate(Val{:/}, ex::Expr, x::Symbol)

Differentiate d/dx f/g.
"""
function differentiate(::Type{Val{:/}}, ex::Expr, x::Symbol)
    op, g, h = ex.args
    @assert op == :/
    dg = differentiate(g, x)
    dh = differentiate(h, x)
    return :(($dg * $h + $g * $dh) / $h^2)
end

"""
    differentiate(Val{:+}, ex::Expr, x::Symbol)

Differentiate d/dx (f + g)
"""
function differentiate(::Type{Val{:+}}, ex::Expr, x::Symbol)
    op = first(ex.args)
    @assert op == :+
    args = differentiate.(ex.args[2:end], x)
    return Expr(:call, :+, args...)
end

"""
    differentiate(Val{:-}, ex::Expr, x::Symbol)

Differentiate d/dx (f - g)
"""
function differentiate(::Type{Val{:-}}, ex::Expr, x::Symbol)
    op = first(ex.args)
    @assert op == :-
    args = differentiate.(ex.args[2:end], x)
    return Expr(:call, :-, args...)
end


#=
  From https://mhtess.github.io/bdappl/chapters/04-hypothesisTesting.html
  WebPPL model:
  """
  The research assistants

  Recall our example from the previous chapter about two research assistants
  who returned wildly different results. We had the intuition that they were
  doing something different from one another. Is there a way to quantify our
  degree of belief in that proposition?

  We can take a model comparison approach, comparing a model where we posit a
  single latent parameter (research assistants are doing / measuring the same
  thing) vs. a model where we posit different parameters for the different
  research assistants. The latter model has more flexibility, but it also probably
  predicts the observed data better.
  """

  Note: The WebPPL model use AIS() for this comparison.
  Here we use SMC's logevidence.
  PG() and IS() also has chns.logevidence, but HM() does not.

=#
using Turing
include("jl_utils.jl")

@model function model_comparison_simple(k1=0,n1=10,k2=10,n=10)
    p1 ~ Uniform(0,1)
    k1 ~ Binomial(n1,p1)
    p2 = p1
    k2 ~ Binomial(n2,p2)
end

#
# "Complex" model with separate p for Binomial
#
@model function model_comparison_complex(k1=0,n1=10,k2=10,n=10)
    p1 ~ Uniform(0,1)
    k1 ~ Binomial(n1,p1)
    p2 ~ Uniform(0,1)
    k2 ~ Binomial(n2,p2)
end

model = model_comparison_simple()
# chns = sample(model, Prior(), 10_000)
# chns = sample(model, MH(), 10_000)
# chns = sample(model, PG(15), 10_000)
# chns = sample(model, IS(), 10_000)
chns = sample(model, SMC(), 10_000)
# chns = sample(model, SMC(), MCMCThreads(), 10_000, 4)
# display(chns)
display(chns)
ll1 = chns.logevidence

model = model_comparison_complex()
# chns = sample(model, Prior(), 10_000)
# chns = sample(model, MH(), 10_000)
# chns = sample(model, PG(15), 10_000)
# chns = sample(model, IS(), 10_000)
chns = sample(model, SMC(), 10_000)
# chns = sample(model, SMC(), MCMCThreads(), 10_000, 4)
# display(chns)
display(chns)
ll2 = chns.logevidence

println("\nlog evidence model 1: ", ll1)
println("log evidence model 2: ", ll2)
println("diff exp(ll2-ll1): ", exp(ll2-ll1))


"""
    Deterministic(value)

Create a deterministic distribution over only one value.

This is intended to be used when a distribution is required, but the outcome is deterministic. It is equivalent to a Kronecker Delta distribution.
"""
struct Deterministic{T}
    val::T
end

rand(rng::AbstractRNG, d::Deterministic) = d.val
rand(d::Deterministic) = d.val
support(d::Deterministic) = (d.val,)
sampletype(::Type{Deterministic{T}}) where T = T
Random.gentype(::Type{Deterministic{T}}) where T = T
pdf(d::Deterministic, x) = convert(Float64, x == d.val)
mode(d::Deterministic) = d.val
mean(d::Deterministic{N}) where N<:Number = d.val / 1 # / 1 is to make this return a similar type to Statistics.mean
mean(d::Deterministic) = d.val # so that division need not be implemented for the value type


"""
    hadi1992_handle_singularity(S)

Perform the sub-algorithm of handling singularity defined in Hadi (1992).

# Arguments
- `S::Array{Float64, 2}`: A covariance matrix.

# Reference
Hadi, Ali S. "Identifying multiple outliers in multivariate data."
Journal of the Royal Statistical Society: Series B (Methodological) 54.3 (1992): 761-771.
 """
function hadi1992_handle_singularity(S::Array{Float64,2})::Array{Float64,2}
    p, _ = size(S)
    eigen_structure = eigen(S)
    values = eigen_structure.values
    vectors = eigen_structure.vectors
    lambda_s = find_minimum_nonzero(values)
    W = zeros(Float64, p, p)
    for i in 1:p
        @inbounds W[i, i] = 1 / max(values[i], lambda_s)
    end
    newS = vectors * W * vectors
    return newS
end

"""
    hadi1992(multivariateData)

Perform Hadi (1992) algorithm for a given multivariate data.

# Arguments
-`multivariate::Array{Float64, 2}`: Multivariate data.

# Examples
```julia-repl
julia> multidata = hcat(hbk.x1, hbk.x2, hbk.x3);

julia> hadi1992(multidata)
Dict{Any,Any} with 3 entries:
  "outliers"              => [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
  "criticial.chi.squared" => 7.81473
  "rth.robust.distance"   => 5.04541
```

# Reference
Hadi, Ali S. "Identifying multiple outliers in multivariate data."
Journal of the Royal Statistical Society: Series B (Methodological) 54.3 (1992): 761-771.
"""
function hadi1992(multivariateData::Array{Float64,2}; alpha=0.05)
    n, p = size(multivariateData)
    h = Int(floor((n + p + 1.0) / 2.0))
    chi_50_quantile = quantile(Chisq(p), 0.50)
    critical_quantile = quantile(Chisq(p), 1 - alpha)
    allindices = collect(1:n)

    # Step 0
    meds = coordinatwisemedians(multivariateData)
    Sm = (1.0 / (n - 1.0)) * (multivariateData .- meds')' * (multivariateData .- meds')
    mah0 = diag(mahalanobisSquaredMatrix(multivariateData, meanvector=meds, covmatrix=Sm))
    ordering_indices_mah0 = sortperm(mah0)
    best_indices_mah0 = ordering_indices_mah0[1:h]
    starting_data = multivariateData[best_indices_mah0, :]

    Cv = coordinatwisemedians(starting_data)
    Sv = (1.0 / (h - 1.0)) * (starting_data .- Cv')' * (starting_data .- Cv')
    mah1 = diag(mahalanobisSquaredMatrix(multivariateData, meanvector=Cv, covmatrix=Sv))
    ordering_indices_mah1 = sortperm(mah1)

    r = p + 1
    basic_subset_indices = []
    basic_subset = []
    sorted_mah1 = []

    while r < n
        cnpr = 1 + (r / (n - p))^2.0
        basic_subset_indices = ordering_indices_mah1[1:r]
        basic_subset = multivariateData[basic_subset_indices, :]
        Cb = applyColumns(mean, basic_subset)
        Sb = cov(basic_subset)

        r += 1
        cfactor = cnpr * sqrt(sort(mah1)[h]) / chi_50_quantile
        if det(cfactor * Sb) == 0
            @info "singular Sb case"
            newSb = hadi1992_handle_singularity(cfactor * Sb)
            mah1 = diag(mahalanobisSquaredMatrix(multivariateData, meanvector=Cb, covmatrix=newSb))
            ordering_indices_mah1 = sortperm(mah1)
            basic_subset_indices = ordering_indices_mah1[1:r]
        else
            mah1 = diag(mahalanobisSquaredMatrix(multivariateData, meanvector=Cb, covmatrix=(cfactor * Sb)))
            ordering_indices_mah1 = sortperm(mah1)
            basic_subset_indices = ordering_indices_mah1[1:r]
        end

        sorted_mah1 = sort(mah1)
        if sorted_mah1[r] >= critical_quantile
            break
        end
    end

    outlierset = setdiff(allindices, basic_subset_indices)

    result = Dict()
    result["outliers"] = sort(outlierset)
    result["criticial.chi.squared"] = critical_quantile
    result["rth.robust.distance"] = sorted_mah1[r - 1]
    return result
end