### A Pluto.jl notebook ###
# v0.12.18

using Markdown
using InteractiveUtils

# ╔═╡ ceff6126-51fc-11eb-3cf5-dd30d637e267
begin
	using Dates
	using MortalityTables
	using DifferentialEquations
	using Plots
	using ActuaryUtilities
end

# ╔═╡ cd5e7080-5348-11eb-2c22-455a3556cab0
md"
# Universal Life Policy Account Mechanics as a Differential Equation
"

# ╔═╡ de0ace1a-5348-11eb-35c9-5f0ff4009d97
md"""
## Introduction

This demonstrates an example of defining an universal life policy value roll like a discrete differential equation.

It then uses the [SciML DifferentialEquations](https://sciml.ai/) package to "solve" the policy projection given a single point, but also to see how the policy projection behaves under different premium and interest rate conditions./
"""

# ╔═╡ ef26ffb6-5348-11eb-04a8-c5cbfdcab72a
md"""
Let's use the 2001 CSO table as the basis for cost of insurance charges:
"""

# ╔═╡ 45dab59c-528d-11eb-357d-af3664466910
begin
	tables = MortalityTables.tables()
	cso = tables["2001 CSO Super Preferred Select and Ultimate - Male Nonsmoker, ANB"]
end

# ╔═╡ ffe9b6e0-5348-11eb-2d38-a5613eb24383
md"""
Next, policy mechanics are coded. It's essentially a discrete differential equation, so it leverages [`DifferentialEquations.jl`](https://diffeq.sciml.ai/dev/)

The projection is coded in the [Discrete DifferentialEquation format](https://diffeq.sciml.ai/dev/types/discrete_types/):

``u_{n+1} = f(u,p,t_{n+1})``

In the code below, this translates to:

- `u` is the *state* of the system. We  will track three variables to represent the `state`:
   - `state[1]` is the account value
   - `state[2]` is the premium paid
   - `state[3]` is the policy duration
- `p` are the parameters of the system.
- `t` is the time, which will represent days since policy issuance
"""

# ╔═╡ da199644-51fc-11eb-0aab-dd68b1fcddc6
function policy_projection(state,p,t)
    # grab the state from the inputs
    av = state[1]

    # calculated variables
    cur_date = p.issue_date + Day(t)
    dur = duration(p.issue_date,cur_date)
    att_age = p.issue_age + dur - 1

	# lapse if AV <= 0
    lapsed = (av <= 0.0 ) & (t > 1)

    if !lapsed

        monthly_coi_rate = (1 - (1-p.mort_assump[att_age]) ^ (1/12))

		## Periodic Policy elements

        # annual events
        if Dates.monthday(cur_date) == Dates.monthday(p.issue_date) ||
			cur_date ==p.issue_date + Day(1) # OR first issue date

            premium = p.annual_prem
        else
            premium = 0.0
        end

        # monthly_events
        if Dates.day(cur_date) == Dates.day(p.issue_date)
            coi = max((p.face - av) * monthly_coi_rate,0.0)
        else
            coi = 0.0
        end

        # daily events
        int(av) = av * ((1 + p.int_rate) ^ (1 / 360) - 1.0)

        # av
        new_av = max(0.0,av - coi + premium + int(av-coi))

				# new state
		return [new_av, premium, dur] # AV, Prem, Dur

    else
		# new state
		return [0.0, 0.0, dur] # AV, Prem, Dur

    end

end

# ╔═╡ 8a227236-5349-11eb-353d-1d3f94a968d0
md"The following function will create a named tuple of parameters given a varying `prem` (premium) and `int` (credit rate)."

# ╔═╡ 956811e4-5349-11eb-2b34-b5441def32a1
params(prem,int) = (
    int_rate = int,
    issue_date = Date(2010,1,1),
    face = 1e6,
    issue_age = 25,
    mort_assump = tables["2001 CSO Super Preferred Select and Ultimate - Male Nonsmoker, ANB"].ultimate,
    projection_years = 75,
    annual_prem = prem,
)

# ╔═╡ 9ee35710-5349-11eb-0d45-23bc292d8a3d
md"""
## Runing the system

This results in the following plot. The tracked output variables u1 and u2 represent the two vars that we tracked above: account value and cumulative premium.
"""

# ╔═╡ b65ef28c-5349-11eb-1baa-6575ad7fb946
begin
	p = params(
	        8000.0, # 8,000 annual premium
	        0.08    # 8% interest
	        )

	# calculate the number of days to project
	projection_end_date = p.issue_date + Year(p.projection_years)
	days_to_project = Dates.value(projection_end_date - p.issue_date)

	# the [0.0,..] are the initial conditions for the tracked variables
	 prob = DiscreteProblem(policy_projection,[0.0,0.0,0],(0,days_to_project),p)
	 proj = solve(prob,FunctionMap())

	 plot(proj)
end

# ╔═╡ c4637664-5349-11eb-0e81-59e39974f02e
md"""
## Moving up the ladder of abstraction

An excellent way to understand the behavior of a model is to [move up the ladder of abstraction](http://worrydream.com/LadderOfAbstraction/). Below, we will see what happens to the projection at varying levels of credit rates and annual premiums.
"""

# ╔═╡ 02cc352a-5409-11eb-0fda-336d5fa42fa9
begin
	prem_range = 1000.0:100.0:9000.0
	int_range = 0.02:0.0025:0.08

	function ending_av(ann_prem,int,days_to_project)
	    p = params(ann_prem,int)
	    prob = DiscreteProblem(policy_projection,[0.0,0.0,0],(0,days_to_project),p)
	    proj = solve(prob,FunctionMap())
	    end_av = proj[end][1]
	    if end_av == 0.0
	        lapse_time = findfirst(isequal(0.0),proj[1,2:end])
	    else
	        lapse_time = length(proj)
	    end
	    duration = proj[3,lapse_time]
	    end_age = p.issue_age + duration - 1.0

	    return end_av,end_age

	end

	end_age = zeros(length(prem_range),length(int_range))
	end_av = zeros(length(prem_range),length(int_range))

	# loop through each projection we did and fill our ranges with the ending AV and ending age
	for (i,vp) in enumerate(prem_range)
	    for (j,vi) in enumerate(int_range)
	        (end_av[i,j],end_age[i,j]) = ending_av(vp,vi,days_to_project)
	    end
	end
end

# ╔═╡ 280d25a6-5409-11eb-17d9-674f218f4351
begin
	using ColorSchemes # for Turbo colors, which emphasize readability

	viz = plot(layout=2) # side by side plot

	#
	contour!(viz[1],int_range,
	    prem_range,
	    end_av ./ 1e6, # scale to millions for readability

	    contour_labels=true,
	    c=cgrad(ColorSchemes.turbo.colors),
	    fill=true,
	    title="AV at age 100 (\$M)",
	    ylabel="Annual Premium (\$)",

	)

	contour!(viz[2],int_range,
	    prem_range,
	    end_age,

	    contour_labels=true,
	    c=cgrad(ColorSchemes.turbo.colors),
	    fill=true,
	    yaxis=false,
	    title="Age at Lapse",
	)

	annotate!(viz[2],0.055,7000,Plots.text("Doesn't lapse \nbefore age 100", 8, :white, :center))

end

# ╔═╡ 21e53f60-5409-11eb-291d-d903da8e16b4
md"
Now let's plot the result. Not surprising, interest has a huge effect on the policy projection. Premium is also a major influence.

One thing that's remarkable is how going from 2000 premium to just ~2200 of premium results in about a $5m difference at 8% interest. The power of compound interest!
"

# ╔═╡ 684f7efc-5409-11eb-0757-19b40c5d418e
md"
## Conclusion

This shows how universal life mechanics are a dynamic system. the growth/decay is governed by two competing feedback loops:

- Growth: the force of interest lets the balance grow exponentially over long periods of time

- Decay: low balances increase the net amount at risk and the resulting COI charges.

"

# ╔═╡ 7e80faf0-5409-11eb-33f6-4f6be6fea835
md"
## Endnotes

This is not meant to represent any particular insurance product, nor fully replicate typical account mechanics.
"

# ╔═╡ Cell order:
# ╟─cd5e7080-5348-11eb-2c22-455a3556cab0
# ╟─de0ace1a-5348-11eb-35c9-5f0ff4009d97
# ╠═ceff6126-51fc-11eb-3cf5-dd30d637e267
# ╟─ef26ffb6-5348-11eb-04a8-c5cbfdcab72a
# ╠═45dab59c-528d-11eb-357d-af3664466910
# ╟─ffe9b6e0-5348-11eb-2d38-a5613eb24383
# ╠═da199644-51fc-11eb-0aab-dd68b1fcddc6
# ╟─8a227236-5349-11eb-353d-1d3f94a968d0
# ╠═956811e4-5349-11eb-2b34-b5441def32a1
# ╟─9ee35710-5349-11eb-0d45-23bc292d8a3d
# ╠═b65ef28c-5349-11eb-1baa-6575ad7fb946
# ╟─c4637664-5349-11eb-0e81-59e39974f02e
# ╠═02cc352a-5409-11eb-0fda-336d5fa42fa9
# ╟─21e53f60-5409-11eb-291d-d903da8e16b4
# ╠═280d25a6-5409-11eb-17d9-674f218f4351
# ╟─684f7efc-5409-11eb-0757-19b40c5d418e
# ╟─7e80faf0-5409-11eb-33f6-4f6be6fea835


# ---
# title: 1137. N-th Tribonacci Number
# id: problem1137
# author: zhwang
# date: 2022-02-10
# difficulty: Easy
# categories: Recursion
# link: <https://leetcode.com/problems/n-th-tribonacci-number/description/>
# hidden: true
# ---
#
# The Tribonacci sequence Tn is defined as follows:
#
# T0 = 0, T1 = 1, T2 = 1, and Tn+3 = Tn \+ Tn+1 \+ Tn+2 for n >= 0.
#
# Given `n`, return the value of Tn.
#
#
#
# **Example 1:**
#
#
#
#     Input: n = 4
#     Output: 4
#     Explanation:
#     T_3 = 0 + 1 + 1 = 2
#     T_4 = 1 + 1 + 2 = 4
#
#
# **Example 2:**
#
#
#
#     Input: n = 25
#     Output: 1389537
#
#
#
#
# **Constraints:**
#
#   * `0 <= n <= 37`
#   * The answer is guaranteed to fit within a 32-bit integer, ie. `answer <= 2^31 - 1`.
#
#
## @lc code=start
using LeetCode

function tribonacci_1137(n::Int)::Int
    n <= 1 && return n
    a, b, c = 0, 1, 1
    for _ in 1:(n - 2)
        a, b, c = b, c, a + b + c
    end
    c
end
## @lc code=end

## @lc test=start
@testset "1137.n-th-tribonacci-number.jl" begin
    @test tribonacci_1137(0) == 0
    @test tribonacci_1137(3) == 2
    @test tribonacci_1137(4) == 4
    @test tribonacci_1137(37) == 2082876103
end
## @lc test=end
# link: [solution to 1137](https://leetcode-cn.com/problems/n-th-tribonacci-number/solution/1137-di-n-ge-tai-bo-na-qi-shu-by-rex-rs-ilwr/)
## @lc info=start

## @lc info=end


using NumericalMethodsforEngineers

ab = SymmetricBandedMatrix(2, Float64[0 0 16; 0 4 5; 8 -4 22])
bb = SymmetricBandedMatrix(3, Float64[0 0 0 16; 0 0 4 2; 0 8 -4 22; 13 5 9 1])
cb = SymmetricBandedMatrix(4, Float64[0 0 0 0 1; 0 0 0 2 3; 0 0 4 5 6; 0 7 8 9 10; 11 12 13 14 15])
db = SymmetricBandedMatrix(2, Float64[0 0 16; 0 4 5; 8 -4 22; 1 2 3; 0 7 6 ; 11 0 15])
eb = SymmetricBandedMatrix(3, Float32[0 0 0 16; 0 0 4 5; 0 8 -4 22; 1 2 3 4; 0 0 7 6 ; 0 11 0 15; 0 0 0 20])
ebi = SymmetricBandedMatrix(3, [0 0 0 16; 0 0 4 5; 0 8 -4 22; 1 2 3 4; 0 0 7 6; 0 11 0 15; 0 0 0 20])
ebf = SymmetricBandedMatrix(3, [0 0 0 16+0.1im; 0 0 4 5; 0 8 -4+9im 22; 1 2 3 4; 0 0 7 6 ; 0 11 0 15; 0 0 0 20])
ebif = SymmetricBandedMatrix(3, [0 0 0 16; 0 0 4+im 5; 0 8 -4 22; 1 2 3 4; 0 0 7 6; 0 11 0 15; 0 0 0 20])
ebif32 = SymmetricBandedMatrix(3, Complex{Float32}[0 0 0 16; 0 0 4+im 5; 0 8 -4 22; 1 2 3 4; 0 0 7 6; 0 11 0 15; 0 0 0 20])

a = full(ab)
res = tosymmetricbandedmatrix(2, a)
@test res == ab

b = full(bb)
res = tosymmetricbandedmatrix(3, b,)
@test res == bb

c = full(cb)
res = tosymmetricbandedmatrix(4, c)
@test res == cb

d = full(db)
res = tosymmetricbandedmatrix(2, d)
@test res == db

e = full(eb)
res = tosymmetricbandedmatrix(3, e)
@test res == eb
res2 = tosymmetricbandedmatrix(e)
@test res2 == res

ei = full(ebi)
res = tosymmetricbandedmatrix(3, ei)
@test res == ebi
res2 = tosymmetricbandedmatrix(ei)
@test res2 == res

ef = full(ebf)
res = tosymmetricbandedmatrix(3, ef)
@test res == ebf

eif = full(ebif)
res = tosymmetricbandedmatrix(3, eif)
@test res == ebif

eif32 = full(ebif32)
res = tosymmetricbandedmatrix(3, eif32)
@test res == ebif32

res2 = copy(res)
@test res == res2
@test res !== res2

res2.bmat[4, 3] = 8.0+9im
@test res != res2
@test res !== res2

res3 = res2
@test res3 == res2
@test res3 === res2

f=sprand(6, 6, 0.3)
ff = Matrix(f)
ffs=ff.+ff'
bm = tosymmetricbandedmatrix(ffs)
@test full(bm) == ffs

ms = sprand(5, 5, 0.1)
# Make sure it is symmetric
sms = issymmetric(ms) ? ms : ms .+ ms'
bms = issymmetric(sms) ? convert(SymmetricBandedMatrix, sms) : convert(SymmetricBandedMatrix, round(sms, 8))
@test full(bms) == sms


using Cubature
using Distributions

"""
Demo of robust Bayesian analysis, from Berger 1984
"""
function robustPriorDemo()
    x = 5
    obsVar = 1

    # First let us verify that the Gaussian prior indeed satisfies the prior quartiles
    priorMu = 0
    priorVar = 2.19

    nprior = Normal(priorMu, sqrt(priorVar))
    @assert isapprox(cdf(nprior, -1), .25; atol=.001)
    @assert isapprox(cdf(nprior, 0) - cdf(nprior, -1), .25; atol=.001)
    @assert isapprox(cdf(nprior, 1) - cdf(nprior, 0), .25; atol=.001)
    @assert isapprox(1-cdf(nprior, 1), .25; atol=.001)
    @assert isapprox(median(nprior), 0; atol=.001)

    # Now compute posterior mean using Gaussian prior
    postVar = 1/(1/obsVar + 1/priorVar)
    postMeanNormal = postVar*(priorMu/priorVar + x/obsVar)
    @assert isapprox(postMeanNormal, 3.43; atol=.01)

    # Now let us do the same thing for the Cauchy
    cprior = Cauchy(0, 1)
    @assert isapprox(cdf(cprior, -1), .25; atol=.001)
    @assert isapprox(cdf(cprior, 0) - cdf(cprior, -1), .25; atol=.001)
    @assert isapprox(cdf(cprior, 1) - cdf(cprior, 0), .25; atol=.001)
    @assert isapprox(1-cdf(cprior, 1), .25; atol=.001)
    @assert isapprox(median(cprior), 0; atol=.001)

    # Now let us compute posterior mean using Cauchy

    lik(θ) = pdf(Normal(θ, sqrt(obsVar)), x)
    prior(θ) = pdf(cprior, θ)
    post(θ) = lik(θ).*prior(θ)

    ∞=10

    Z, err = hquadrature(post, -∞, ∞; reltol=1e-4)

    f(θ) = θ.*post(θ)/Z

    postMeanCauchy, err = hquadrature(f, -∞, ∞; reltol=1e-4)
    @assert isapprox(postMeanCauchy, 4.56; atol=.01)

    (postMeanNormal, postMeanCauchy)
end


function nlp_cvx_107_012(
    optimizer,
    objective_tol,
    primal_tol,
    dual_tol,
    termination_target = TERMINATION_TARGET_LOCAL,
    primal_target = PRIMAL_TARGET_LOCAL,
)
    model = Model(optimizer)

    @variable(model, x, start = 1.5)
    @variable(model, y, start = 0.5)

    @objective(model, Min, (x - 1.0)^2 + (y - 1.0)^2)
    @NLconstraint(model, x^2 + y^2 <= 1)

    optimize!(model)

    check_status(model, FEASIBLE_PROBLEM, termination_target, primal_target)
    check_objective(model, 0.17157287363083387, tol = objective_tol)
    return check_solution([x, y], [1 / sqrt(2), 1 / sqrt(2)], tol = primal_tol)
end


function solve_poisson_pcg( Lmat::SparseMatrixCSC{Float64,Int64},
                            prec::SparseMatrixCSC{Float64,Int64},
                            rho::Array{Float64,1}, NiterMax::Int64;
                            verbose=false, TOL=5.e-10 )
  #
  Npoints = size(rho)[1]
  phi = zeros( Float64, Npoints ) # XXX or use some starting guess
  #
  r = zeros( Float64, Npoints )
  p = zeros( Float64, Npoints )
  z = zeros( Float64, Npoints )
  #
  nabla2_phi = Lmat*phi
  r = rho - nabla2_phi
  z = apply_prec_ilu0( prec, r)
  p = copy(z)

  rsold = dot( r, z )

  for iter = 1 : NiterMax
    #
    nabla2_phi = Lmat*p
    #
    alpha = rsold/dot( p, nabla2_phi )
    #
    phi = phi + alpha * p
    r = r - alpha * nabla2_phi
    z = apply_prec_ilu0( prec, r)
    #
    rsnew = dot( z, r )
    deltars = rsold - rsnew
    #println(typeof(rsnew))
    #println(rsnew)
    if verbose
      @printf("%8d %18.10e\n", iter, sqrt(abs(rsnew)))
    end
    #
    if sqrt(abs(rsnew)) < TOL
      if verbose
        @printf("Convergence achieved in solve_poison_pcg: N, iter: %d %d\n", Npoints, iter)
      end
      break
    end
    #
    p = z + (rsnew/rsold) * p
    #
    rsold = rsnew
  end
  #
  return phi
  #
end # of function


function besselj(m, x)
    sn = 0
    an = 1/factorial(m)
    k = 0

    while abs(an) > eps()
        sn += an
        an = -an * x * x/4/(k+1)/(k+m+1)
        k += 1
    end

    sn *= (x/2)^m
    return sn
end

function besselj_plots()
    x = 0:0.02:20
    y1, y2, y3, y4, y5, y6 =[], [], [], [], [], []

    for i in x
        push!(y1, besselj(0, i))
        push!(y2, besselj(1, i))
        push!(y3, besselj(2, i))
        push!(y4, besselj(3, i))
        push!(y5, besselj(4, i))
        push!(y6, besselj(5, i))
    end

    p = plot(x, y1)
    plot!(p, x, y2)
    plot!(p, x, y3)
    plot!(p, x, y4)
    plot!(p, x, y5)
    plot!(p, x, y6)
    display(p)
end


using Test

function naive!(C, A, B)
    row, col  = size(C)
    colA = size(A)[2]
    for i in 1:row
        for j in 1:col
            for k in 1:colA
                C[i, j] = C[i, j] + A[i, k] * B[k, j]
            end
        end
    end
    C
end

"""
inner-product-form
"""
function ipform!(C, A, B)
    row, col  = size(C)
    colA = size(A)[2]
    for i in 1:row
        for j in 1:col
            dataC = 0.0
            for k in 1:colA
                dataC = dataC + A[i, k] * B[k, j]
            end
            C[i, j] = dataC
        end
    end
end

"""
outer-product-form
"""
function opform!(C, A, B)
    row, col  = size(C)
    colA = size(A)[2]
    for k in 1:colA
        for j in 1:col
            dataB = B[k, j]
            for i in 1:row
                C[i, j] = C[i, j] + A[i, k] * dataB
            end
        end
    end
end

"""
middle-product-form
"""
function mpform!(C, A, B)
    row, col  = size(C)
    colA = size(A)[2]
    for j in 1:col
        for k in 1:colA
            dataB = B[k, j]
            for i in 1:row
                C[i, j] = C[i, j] + A[i, k] * dataB
            end
        end
    end
end

function main(N)
    A = reshape(collect(1:1.0:N^2), N, N)
    B = reshape(collect(1:1.0:N^2), N, N)
    C = zeros(N, N)
    @time naive!(C, A, B)
    # copy result
    refC = copy(C)

    C = zeros(N, N)
    @time ipform!(C, A, B)
    @test C ≈ refC

    C = zeros(N, N)
    @time opform!(C, A, B)
    @test C ≈ refC

    C = zeros(N, N)
    @time mpform!(C, A, B)
    @test C ≈ refC
end


module  DifferentiableFunctionsTests

using Test
using ProximalBase
using Random
using LinearAlgebra

Random.seed!(1)

@testset "L2Loss" begin

  x = randn(5)
  y = randn(5)
  f1 = L2Loss()
  f2 = L2Loss(y)

  @test value(f1, x) ≈ sum(abs2,x)/2.
  @test value(f2, x) ≈ sum(abs2,x-y)/2.

  hat_x = similar(x)
  @test value_and_gradient!(f1, hat_x, x) ≈ sum(abs2,x)/2.
  @test hat_x ≈ x
  hat_x = similar(x)
  @test value_and_gradient!(f2, hat_x, x) ≈ sum(abs2,x-y)/2.
  @test hat_x ≈ x-y

end

@testset "Quadratic Function" begin

  A = randn(10, 10)
  A = A + A'
  b = randn(10)
  c = 1.

  x = randn(10)

  q1 = QuadraticFunction(A)
  q2 = QuadraticFunction(A, b)
  q3 = QuadraticFunction(A, b, c)

  @test value(q1, x) ≈ dot(x, A*x)/2.
  @test value(q2, x) ≈ dot(x, A*x)/2. + dot(x, b)
  @test value(q3, x) ≈ dot(x, A*x)/2. + dot(x, b) + c

  hat_x = similar(x)
  @test value_and_gradient!(q1, hat_x, x) ≈ dot(x, A*x)/2.
  @test hat_x ≈ A*x

  @test value_and_gradient!(q2, hat_x, x) ≈ dot(x, A*x)/2. + dot(x, b)
  @test hat_x ≈ A*x + b

  @test value_and_gradient!(q3, hat_x, x) ≈ dot(x, A*x)/2. + dot(x, b) + c
  @test hat_x ≈ A*x + b
end

@testset "Least Squares Loss" begin

  y = randn(10)
  X = randn(10, 5)
  b = rand(5)
  f = LeastSquaresLoss(y, X)

  @test value(f, b) ≈ norm(y-X*b)^2 / 2.
  grad_out = zeros(5)
  @test value_and_gradient!(f, grad_out, b) ≈ norm(y-X*b)^2 / 2.
  @test grad_out ≈ -X'*(y - X*b)

end

end # module


"""
    dct2!(a, fft_type::FFTType=Radix2Type())

Discrete cosine transform of type II

Y = DCT2(X) returns the discrete cosine transform of X,
based on the staggered-grid definition
           N
   Y(k) =  ∑  X(j) cos (π*(k-1)*(j-1/2)/N)
          j=1
The vector Y is the same size as X and contains the
discrete cosine transform coefficients.

Y = DCT2(X,N) pads or truncates the vector X to length N
before transforming.

If X is a matrix, the DCT2 operation is applied to each
column. This transform can be inverted using IDCT2.

Verified correct within machine precision for 2D array input 2/28/2021

Traditional dct2:
          N-1
   Y(k) =  ∑  X(j) cos (π*(j+1/2)*k/N)
          j=0

"""
function dct2!(x, fft_type=Radix2Type())
    x .= dct2(x, fft_type)
end
function dct2(a, fft_type::FFTType=Radix2Type())
    FT = eltype(a)
    if min(size(a)...)==1
        if size(a,2)>1
            do_trans = true;
        else
            do_trans = false;
        end
        a = a[:];
    else
        do_trans = false;
    end
    n = size(a,1);
    m = size(a,2);
    # Pad or truncate a if necessary
    if size(a,1)<n
      aa = zeros(n,m);
      aa[1:size(a,1),:] = a;
    else
      aa = a[1:n,:];
    end
    y = zeros(2*n, m);
    y[1:n,:] .= aa;
    i = Complex(0,1);
    e = 0.5 * exp.(-i*FT(0.5) * π * (0:n-1) ./ n);
    y[n+1:2*n,:] .= reverse(aa[1:n,:]; dims=1);
    yy = fft(y, 1);
    b = zeros(n,m);
    for l = 1:m
        b[1:n,l] .= real.(yy[1:n,l] .* e);
    end
    if isreal(a)
        b = real.(b);
    end
    if do_trans
        b = b'
    end
    return b
end


"""
# Divide-and-conquer algorithms

- Multiplication
- Recurrence relations
    - Master theorem
    - Binary search
- Mergesort
    - An n log n lower bound for sorting
- Medians
    - A randomized divide-and-conquer algorithm for selection
    - Efficiency analysis
    - The Unix sort command
- Matrix multiplication
- The fast Fourier transform
    - Why multiply polynomials?
    - An alternative representation of polynomials
    - Evaluation by divide-and-conquer
    - Interpolation
    - A matrix reformulation
    - Interpolation resolved
    - Inversion formula
    - A closer look at the fast Fourier transform
    - The definitive FFT algorithm
    - The fast Fourier transform unraveled
    - The slow spread of a fast algorithm
"""
module Chapter2

export
    karatsuba,
    karatsuba2,
    mergesort,
    iterativemergesort,
    polynomialmultiplication

"""
    karatsuba(x, y)

Using the Karatsuba algorithm, compute the product of two integers `x` and `y`.

# Examples

```jldoctest
julia> karatsuba(2412, 3231)
7793172
```
"""
function karatsuba(x::Int, y::Int)
    if ndigits(x) == 1 || ndigits(x) == 1
        return x * y
    end

    n = max(ndigits(x), ndigits(y))
    m = n ÷ 2

    xL, xR = x ÷ 10^m, x % 10^m
    yL, yR = y ÷ 10^m, y % 10^m

    P1 = karatsuba(xL, yL)
    P2 = karatsuba(xR, yR)
    P3 = karatsuba((xL + xR), (yL + yR))

    return (P1 * 10^(2m)) + ((P3 - P1 - P2) * 10^m) + P2
end

"""
    karatsuba2(x, y)

Using the Karatsuba algorithm, compute the product of two integers `x` and `y`.

# Examples

```jldoctest
julia> karatsuba(2412, 3231)
7793172
```
"""
function karatsuba2(x::Int, y::Int)
    if ndigits(x, base=2) == 1 || ndigits(y, base=2) == 1
        return x * y
    end

    n = max(ndigits(x, base=2), ndigits(y, base=2))
    m = n ÷ 2

    xL, xR = x ÷ 2^m, x % 2^m
    yL, yR = y ÷ 2^m, y % 2^m

    P1 = karatsuba(xL, yL)
    P2 = karatsuba(xR, yR)
    P3 = karatsuba((xL + xR), (yL + yR))

    return (P1 * 2^n) + ((P3 - P1 - P2) * 2^m) + P2
end

"""
    mergesort(a)

Sort an array of numbers `a` using the merge sort algorithm.

# Examples

```jldoctest
julia> mergesort([10, 2, 5, 3, 7, 13, 1, 6])
8-element Vector{Int64}:
  1
  2
  3
  5
  6
  7
 10
 13

julia> mergesort([1, 4.54312, 3.4321, π])
4-element Vector{Float64}:
 1.0
 3.141592653589793
 3.4321
 4.54312
```
"""
function mergesort(a::AbstractVector)
    if length(a) > 1
        m = length(a) ÷ 2
        return merge(mergesort(a[1:m]), mergesort(a[(m + 1):end]))
    else
        return a
    end
end

function merge(x::AbstractVector, y::AbstractVector)
    if length(x) == 0
        return y
    end
    if length(y) == 0
        return x
    end
    if x[1] <= y[1]
        return pushfirst!(merge(x[2:end], y), x[1])
    else
        return pushfirst!(merge(x, y[2:end]), y[1])
    end
end

function iterativemergesort(a::AbstractVector)
    Q = []
    for i ∈ 1:length(a)
        push!(Q, [a[i]])
    end
    while length(Q) > 1
        push!(Q, merge(eject(Q), eject(Q)))
    end
    return eject(Q)
end

function polynomialmultiplication(A::AbstractVector{T}, B::AbstractVector{T}) where {T <: Real}
    product = zeros(length(A) + length(B))

    for i ∈ 1:length(A)
        for j ∈ 1:length(B)
            product[i + j] += A[i] * B[j]
        end
    end

    return product
end

# TODO: FFT
# function FFT(A, ω)
#
# end

end # module


"""
    maxwell_boltzmann(f)

Maxwell Boltzmann entropy
"""
maxwell_boltzmann(f) = f * log(f) - f

"""
    maxwell_boltzmann_prime(x)

Prim of Maxwell Boltzmann entropy
"""
maxwell_boltzmann_prime(x) = log(x)

"""
    maxwell_boltzmann_dual(f)

Dual of Maxwell Boltzmann entropy
"""
maxwell_boltzmann_dual(f) = exp(f)

"""
    maxwell_boltzmann_dual_prime(f)

Dual prim of Maxwell Boltzmann entropy
"""
maxwell_boltzmann_dual_prime(f) = exp(f)

"""
    kinetic_entropy(α, m, weights)

Reconstruct mathematical entropy from Legendre dual
"""
function kinetic_entropy(α::AbstractArray, m::AbstractArray, weights::AbstractVector)
    B = KitBase.maxwell_boltzmann_dual_prime.(α' * m)[:]
    return sum(maxwell_boltzmann.(B) .* weights)
end


@testset "factorial_ij" begin
    @test all(
        QSB.factorial_div(i, j) == factorial(big(i)) / factorial(big(j))
        for i in 0:40, j in 0:40
    )
end

@testset "z" begin
    x_range = -10:0.01:10
    p_range = -6:0.01:6
    @test all(
        QSB.z(x, p) == sqrt(2.)*(x + p*im)
        for x in x_range, p in p_range
    )

    xs = collect(x_range)
    ps = collect(p_range)
    @test QSB.z(xs, ps) == sqrt(2.).*(xs .+ ps' .* im)
end

@testset "gaussian_function" begin
    tol = 1e-14
    x_range = -10:0.01:10
    p_range = -6:0.01:6
    @test all(
        isapprox(QSB.gaussian_function(x, p), exp(-(x^2 + p^2)) / π, atol=tol)
        for x in x_range, p in p_range
    )

    xs = collect(x_range)
    ps = collect(p_range)
    @test isapprox(
        QSB.gaussian_function(xs, ps),
        exp.(-(xs.^2 .+ (ps').^2)) ./ π,
        atol=tol
    )
end

@testset "(-1)^i" begin
    @test all(
        QSB.neg_one_to_power_of(i) == (-1)^i
        for i in 1:50
    )
end

@testset "coefficient_of_wave_function" begin
    tol = 1e-14
    @test all(
        isapprox(
            QSB.coefficient_of_wave_function(m, n),
            ((n ≥ m) ? (-1)^(m-1) : (-1)^(n-1)) *
            sqrt(factorial(big(min(m, n))) / factorial(big(max(m, n)))),
            atol=tol
        )
        for m in 1:70, n in 1:70
    )
end

@testset "z_to_power" begin
    tol = 1e-14

    m_range = n_range = 1:70
    x_range = -1:0.1:1
    p_range = -0.6:0.1:0.6
    @test all(
        QSB.z_to_power(m, n, x, p) == (
            (n >= m) ? sqrt(2.)*(x - p*im) : sqrt(2.)*(x + p*im)
        )^(max(m, n) - min(m, n))
        for m in m_range, n in n_range, x in x_range, p in p_range
    )

    xs = collect(x_range)
    ps = collect(p_range)
    @test all(
        QSB.z_to_power(m, n, xs, ps) == QSB.z_to_power.(m, n, xs, ps')
        for m in m_range, n in n_range
    )
end

@testset "laguerre" begin
    m_range = n_range = 1:70
    x_range = -1:0.1:1
    p_range = -0.6:0.1:0.6

    result = Bool[]
    # n >= m
    for m in m_range, n in m:n_range.stop, x in x_range, p in p_range
        push!(
            result,
            abs(laguerre(m, n, x, p) - laguerre(m-1, n-m, abs2(sqrt(2.)*(x+p*im)))) < 1e-7
        )
    end
    # n < m
    for n in n_range, m in n:m_range.stop, x in x_range, p in p_range
        push!(
            result,
            abs(laguerre(m, n, x, p) - laguerre(n-1, m-n, abs2(sqrt(2.)*(x+p*im)))) < 1e-7
        )
    end
    @test all(result)

    xs = collect(x_range)
    ps = collect(p_range)
    @test all(
        laguerre(m, n, xs, ps) == laguerre.(m, n, xs, ps')
        for m in m_range, n in n_range
    )
end


# Example 0: Solve 1D Burgers Equation
# u(x,t)_t+(0.5*u²(x,t))_{x}=0
# u(0,x) = f0(x)

using ConservationLawsDiffEq
using OrdinaryDiffEq
using LinearAlgebra

const CFL = 0.5
# First define the problem data (Jacobian is optional but useful)
Jf(u) = u           #Jacobian
f(u) = u^2/2        #Flux function
f0(x) = sin(2*π*x)  #Initial data distribution

# Now discretizate the domain
mesh = Uniform1DFVMesh(10, [0.0, 1.0])

# Now get a explicit semidiscretization (discrete in space) du_h(t)/dt = f_h(u_h(t))
f_h = getSemiDiscretization(f,LaxFriedrichsScheme(),mesh,[Periodic()]; Df = Jf, use_threads = false,numvars = 1)

#Compute discrete initial data
u0 = getInitialState(mesh,f0,use_threads = true)

#Setup ODE problem for a time interval = [0.0,1.0]
ode_prob = ODEProblem(f_h,u0,(0.0,1.0))

#Setup callback in order to fix CFL constant value
cb = getCFLCallback(f_h, CFL)

#Estimate an initial dt
dt = update_dt!(u0, f_h, CFL)

#Solve problem using OrdinaryDiffEq
sol = solve(ode_prob,SSPRK22(); dt = dt, callback = cb)

#Plot solution
#Wrap solution so we can plot using dispatch
u_h = fv_solution(sol, mesh)

using Plots
plot(u_h,tidx = 1,lab="uo",line=(:dot,2)) #Plot inital data
plot!(u_h,lab="LF")                       #Plot LaxFriedrichsScheme solution


function calcSim(duals, dist, S)
    if dist == "L1"
        # Measures L1 distance - Minkowski with p=1
        # https://juliahub.com/docs/LSHFunctions/nLuMy/0.1.2/similarities/lp_distance/
        Sim = collect(ℓ1(duals[i,:],duals[j,:]) for i in S, j in S)
    elseif dist == "L2"
        # Measures L2 distance - Minkowski with p=1
        # https://juliahub.com/docs/LSHFunctions/nLuMy/0.1.2/similarities/lp_distance/
        Sim = collect(ℓ2(duals[i,:],duals[j,:]) for i in S, j in S)
    elseif dist == "jaccard"
        # Measures Jaccard similarity
        # https://juliahub.com/docs/LSHFunctions/nLuMy/0.1.2/similarities/jaccard/
        Sim = collect(LSHFunctions.jaccard(duals[i,:],duals[j,:]) for i in S, j in S)
    elseif dist in ["wcityblock" "weuclidean" "whamming" "wsqeuclidean"]
        # Weighted metrics with equal weights
        W = ones(length(T))
        Sim = collect(Distances.eval(Symbol(dist))(duals[i,:],duals[j,:],W)+0. for i in S, j in S)
    elseif dist in ["wcityblockvar" "weuclideanvar" "whammingvar" "wsqeuclideanvar"]
        # Weighted metrics with weights proportional to variance
        dist = dist[1:end-3] # Renaming to use proper function
        W = var(duals, dims=1)
        Sim = collect(Distances.eval(Symbol(dist))(duals[i,:],duals[j,:],W)+0. for i in S, j in S)
    elseif dist in ["wcityblockinv" "weuclideaninv" "whamminginv" "wsqeuclideaninv"]
        dist = dist[1:end-3] # Renaming to use proper function
        # Weighted metrics with weights inverse to variance
        VAR = var(duals, dims=1)
        W = 1  .- VAR./maximum(VAR)
        Sim = collect(Distances.eval(Symbol(dist))(duals[i,:],duals[j,:],W)+0. for i in S, j in S)
    else
        Sim = collect(Distances.eval(Symbol(dist))(duals[i,:],duals[j,:])+0. for i in S, j in S)
    end
    return Sim
end


using MomentClosure
using MomentClosure: expected_coeff
using ModelingToolkit: value, simplify
using SymbolicUtils: expand
using Test

@parameters b
b = b.val
@test expected_coeff(2, 3) == 8
@test isequal(expected_coeff(b, 3), b*(1 + (6b) + (6(b^2))))
expr = ((b*((1 + b)^-1)) + ((b^6)*((1 + b)^-6)) + (57(b^2)*((1 + b)^-2)) + (302(b^3)*((1 + b)^-3)) + (302(b^4)*((1 + b)^-4)) + (57(b^5)*((1 + b)^-5)))*((1 + b)^6)
@test isequal(expected_coeff(b, 6), expr)

@parameters t, c₁, c₂, c₃, c₄, Ω
@variables X(t), Y(t)

S_mat = [ 1 -1  1 -1;
         -1  1  0  0]
a = [c₁*X*Y*(X-1)/Ω^2, c₂*X, c₃*Ω, c₄*X]
rn = ReactionSystemMod(t, [X, Y], [c₁, c₂, c₃, c₄, Ω], a, S_mat)

sys = generate_central_moment_eqs(rn, 2, 4)
expr1 = sys.odes.eqs[2].rhs
@test isequal(MomentClosure.Differential(t)(sys.μ[1,0]), sys.odes.eqs[1].lhs)
μ = sys.μ
M = sys.M
# simplify(value()) is needed due to diffs between ModelingToolkit Num type and SymbolicUtils types
expr2 = c₂*μ[1,0] + c₁*M[1,1]*(Ω^-2) + c₁*μ[0,1]*μ[1,0]*Ω^-2 - c₁*M[2,1]*Ω^-2 -
        2*c₁*M[1,1]*μ[1,0]*Ω^-2 - c₁*M[2,0]*μ[0,1]*Ω^-2 - c₁*μ[0,1]*Ω^-2*μ[1,0]^2
expr2 = simplify(value.(expr2))
@test isequal(expand(expr1), expr2)

sys = generate_central_moment_eqs(rn, 2)
expr1 = sys.odes.eqs[2].rhs
@test isequal(MomentClosure.Differential(t)(sys.μ[1,0]), sys.odes.eqs[1].lhs)
@test isequal(expand(expr1), expr2)

sys = generate_raw_moment_eqs(rn, 2)
μ = sys.μ
expr1 = sys.odes.eqs[4].rhs
expr2 = c₂*μ[2,0] + c₁*μ[1,1]/Ω^2 - c₁*μ[3,1]/Ω^2 -c₂*(μ[1,0] + μ[1,1]) +
        c₃*μ[0,1]*Ω - c₄*μ[1,1] - c₁*μ[1,2]/Ω^2 + c₁*μ[2,2]/Ω^2
expr2 = simplify(value.(expr2))
@test isequal(expand(expr1), expand(expr2))


function hilb(n::Int)
# Function generates the Hilbert matrix  A of size n,
#  A (i, j) = 1 / (i + j - 1)
# Inputs:
#	n: size of matrix A, n>=1
#
#
# Usage: hilb(10)
#
# Pawel Zielinski
        if n < 1
         error("size n should be >= 1")
        end
        return [1 / (i + j - 1) for i in 1:n, j in 1:n]
end


using GridCreation

@testset "zernike zero separation" begin
    for i in 1:100
        sep = zernike_zero_separation(i)
        @test 1/sep ≈ Float64(i)^2
    end
end

@testset "zernike fringe and nm round trip" begin
    for i in 1:100
        n, m = zernike_fringe_to_nm(i)
        j = zernike_nm_to_fringe(n,m)
        @test j == i
    end
end

@testset "zernike ANSI round trips" begin
    for i in 1:100
        n, m = zernike_ansi_j_to_nm(i)
        j = zernike_nm_to_ansi_j(n,m)
        @test j == i
    end
end

@testset "zernike gives correct values" begin
    for i in 1:100
        # a visual test proves the correctness of the zernike function in
        # general.  The values could only really be tested in a regression
        # fashion, generally, which is undesireable and assumes another
        # implementation is de-facto correct.

        # the test here, therefore, verifies only the analytic value of one
        # at the edge for the maxima of the azumithal term.
        n, m = zernike_noll_to_nm(i)
        out = zernike(n, m, [1.], [0.], norm=false)
        @test (out ≈ [1.]) || (out ≈ [0.])
        out = zernike(n, m, [1.], [0], norm=true)
        norm = [zernike_norm(n,m)]
        @test (out ≈ norm) || (out ≈ [0.])
    end
end

@testset "zernike series contains proper modes" begin
    x, y = mkCartVecs(1/8, 16)
    r, t = cartVecsToPolarGrid(x,y)
    modes = 1:11
    nms = zernike_noll_to_nm.(modes)
    out = zernike_series(nms, r, t)
    for mode in modes
        n, m = zernike_noll_to_nm(mode)
        truth = zernike(n, m, r, t)
        @test out[:, :, mode] ≈ truth
    end
end

@testset "zernike sum contains proper modes" begin
    x, y = mkCartVecs(1/8, 16)
    r, t = cartVecsToPolarGrid(x,y)
    nms = zernike_noll_to_nm.(1:11)
    weights = zeros(11)
    weights[5]=1.
    out = zernike_sum(nms, weights, r, t)
    truth = zernike(2, -2, r, t)
    @test out ≈ truth
end


using ChaosTools
using Test
using Distances: Cityblock, Euclidean

test_value = (val, vmin, vmax) -> @test vmin <= val <= vmax

println("\nTesting nonlinear timeseries analysis...")
@testset "Henon reconstruct" begin
    ds = Systems.henon()
    data = trajectory(ds, 100000)
    x = data[:, 1] # some "recorded" timeseries

    @testset "Dimension" begin
        τ = 1; D = 2
        R = reconstruct(x, D-1, τ)
        D2 = information_dim(R)
        test_value(D2, 1.1, 1.3)
    end
    @testset "Numerical Lyapunov" begin
        ks = 1:20
        @testset "meth = $meth" for meth in
            [FixedMassNeighborhood(1), FixedMassNeighborhood(4),
            FixedSizeNeighborhood(0.01)]
            @testset "distance = $di" for di in [Euclidean(), Cityblock()]
                for D in [2, 4]
                    R = reconstruct(x, D-1, 1)
                    E = numericallyapunov(R, ks,
                    refstates = 1:1000, distance=di, ntype=meth)
                    λ = linear_region(ks, E)[2]
                    test_value(λ, 0.3, 0.5)
                end
            end
        end
    end
end
@testset "Multidim Multitime R" begin
    ds = Systems.towel()
    data = trajectory(ds, 10000)

    taus = [2 3; 4 6; 6 8]
    data2 = data[:, 1:2]
    R = reconstruct(data2, 3, taus)

    ks = 1:20
    E = numericallyapunov(R, ks,
    refstates = 1:1000, distance=Cityblock(), ntype=FixedMassNeighborhood(1))
    λ = linear_region(ks, E)[2]
    test_value(λ, 0.3, 0.6)
end

@testset "Broomhead-King" begin
    using Random
    Random.seed!(1234);
    ds = Systems.gissinger(ones(3)) # standard initial condition

    data = trajectory(ds, 1000.0, dt = 0.05)
    x = data[1:end-1, 1] # "exactly" 20000 points
    s = x .+ 0.01rand(length(x))

    Ux, Σx = broomhead_king(x, 40)
    Us, Σs = broomhead_king(s, 40)
    R = reconstruct(x, 1, 30)

    for j in 10:40
        @test Σx[j] < Σs[j]
    end

    newcoords = Dataset(Us[:, 1], Us[:, 2])
    newcoordsclean = Dataset(Ux[:, 1], Ux[:, 2])

    Dnew = information_dim(newcoords)
    DR = information_dim(R)
    DC = information_dim(newcoordsclean)
    @test abs(Dnew - 0.1 - DR) < 0.2 # subtract 0.1 for "added dimensionality"
    @test abs(DC - DR) < 0.2
end


function bracket_minimum(f, x=0; s=1e-2, k=2.0)
    a, ya = x, f(x)
    b, yb = a + s, f(a + s)
    if yb > ya
        a, b = b, a
        ya, yb = yb, ya
        s = -s
    end
    while true
        c, yc = b + s, f(b + s)
        if yc > yb
            return a < c ? (a,b, c) : (c,b, a)
        end
        a, ya, b, yb = b, yb, c, yc
        s *= k
    end
end

function quadratic_fit_search(f, a, b, c, n)
    ya, yb, yc = f(a), f(b), f(c)
    for i in 1:n-3
        x = 0.5*(ya*(b^2-c^2)+yb*(c^2-a^2)+yc*(a^2-b^2)) /
            (ya*(b-c) +yb*(c-a) +yc*(a-b))
        yx = f(x)
        if x > b
            if yx > yb
                c, yc = x, yx
            else
                a, ya, b, yb = b, yb, x, yx
            end
        elseif x < b
            if yx > yb
                a, ya = x, yx
            else
                c, yc, b, yb = b, yb, x, yx
            end
        end
    end
    return (a, b, c)
end

φ = MathConstants.φ
function golden_section_search(f, a, b, n)
    ρ = φ-1
    d = ρ * b + (1 - ρ)*a
    yd = f(d)
    for i = 1 : n-1
        c = ρ*a + (1 - ρ)*b
        yc = f(c)
        if yc < yd
            b, d, yd = d, c, yc
        else
            a, b = b, c
        end
    end
    return a < b ? (a, b) : (b, a)
end

function line_search(f, x, d)
    objective = α -> f(x + α*d)
    a, b,c = bracket_minimum(objective)

    #numero de iteracoes
    n=(c-a)/(10^-3*log(φ))

    #pelo metodo golden section search
    α,beta = golden_section_search(objective, a, c,n)

    #pelo metodo quadratic fit search
    #α,beta,gamma = quadratic_fit_search(objective, a, b, c,n)
    return (x + α*d,α)
end

#funcao multivariavel
f(x) = sin(x[1]*x[2])+exp(x[2]+x[3])-x[3]

#ponto inicial
x0 = [1,2,3]

#direcao de busca
d = [0,-1,-1]

(x,α) = line_search(f, x0, d)
println("x* = ", x)


"""Squared deviations from the array mean."""
function SqDeviation(X::Array{T,1}) where {T<:AbstractFloat}

    s = zero(T)
    μ = mean(X)

    @simd for xi in X
        s += (xi - μ)^2
    end

    return s
end

"""Linear deviations from the array mean - i.e. L1-Norm."""
function LinDeviation(X::Array{T,1}) where {T<:AbstractFloat}

    s = zero(T)
    μ = mean(X)

    @simd for xi in X
        s += abs(xi - μ)
    end

    return s
end

"""Squared deviations from the class means."""
function SDCM(X::Array{T,1},breaks::Array{Int,1}) where {T<:AbstractFloat}

    n = length(breaks)-1  # number of classes (aka intervals)

    @boundscheck breaks[end] == length(X)+1 || throw(BoundsError())
    @boundscheck breaks[1] == 1 || throw(BoundsError())

    s = zero(T)

    for iclass in 1:n
        s += SqDeviation(ClassValues(X,breaks,iclass))
    end

    return s
end

"""Goodness of variance fit for data array X (assumed to be sorted) and classes determined by break indices."""
function GVF(X::Array{T,1},breaks::Array{Int,1}) where {T<:AbstractFloat}
    sdam = SqDeviation(X)       # squared deviation from array mean
    sdcm = SDCM(X,breaks)       # sum  of squared deviations from class mean for each class
    return 1 - sdcm/sdam
end

"""Average rounding error for data array X (assumed to be sorted) and classes determind by break indices."""
function ARE(X::Array{T,1},breaks::Array{Int,1}) where {T<:AbstractFloat}
    s = zero(T)
    ndata = length(X)
    for iclass in 1:(length(breaks)-1)
        s += LinDeviation(ClassValues(X,breaks,iclass))
    end
    return s/ndata
end


using Test
using RiemannComplexNumbers

a = 2+3IM
b = 4-5IM

@test a+b == 6-2im
@test a/0 == ComplexInf
@test isinf(a/0)
@test Complex(a)==a
@test hash(a) == hash(Complex(a))
@test isnan((a-a)/(b-b))
@test Inf + IM == Inf + 2*im
@test ComplexInf * ComplexInf == ComplexInf
@test isnan(ComplexInf + ComplexInf)
@test isnan(0*ComplexInf)

@test 1/a == inv(a)
@test a' == conj(a)

@test one(RC) == 1
@test zero(RC) == 0

z = RC(2+3im)
@test z*z == -5 + 12im
@test z/z == 1
@test z^3 == z*z*z
@test z^(-1) == 1/z
@test sqrt(RC(-1)) == IM
@test exp(2+3im) == exp(2+3IM)
@test angle(IM) == pi/2
z = 3 - 4IM
@test abs(z*z) == 25
@test abs(z*z) == z'*z

@test abs(exp(log(-1+0IM)) + 1) < 1e-10
@test abs(log(exp(-1+2IM)) - (-1+2IM)) < 1e-10

a = RC(3.0 + 0.0im)
b = a'
@test a==b
@test !isequal(a,b)

@test ComplexNaN != NaN
@test isequal(ComplexNaN,NaN)


using JuMP
using Ipopt

include("../OptMaps.jl")
using .OptMaps

function solve_model(solver, model_log, solver_options=Dict())
    model = Model(solver)

    for (k, v) in solver_options
        set_optimizer_attribute(model, k, v)
    end

    n_variables = 10
    n_constraints = 1

    @variable(model, -3.0 <= x[1:n_variables] <= 5.0)
    @NLconstraint(model, sum((x[i]^2 for i in 1:n_variables)) >= 5.0)

    constraints_f = []
    push!(constraints_f, (v) -> sum(v.^2) >= 5.0)

    @NLobjective(model, Min, sum(x[i]^2 for i in 1:n_variables))
    obj_f(v) = sum(v.^2)

    starting_point = zeros(Float64, n_variables) .+ 3.0
    solution_point = zeros(Float64, n_variables)

    ###############################################################
    #save model textual description
    open(model_log, "w") do f
        print(f, model)

        ###############################################################
        #solve model
        elapsed_time = @elapsed optimize!(model)

        println(f, "-----------------------------------------------")
        println(f, "Elapsed time: $elapsed_time")
        println(f, "-----------------------------------------------")
        println(f, "Status:    $(termination_status(model))")
        println(f, "Objective: $(objective_value(model))")
        println(f, "-----------------------------------------------")
        println(f, "Variables:")

        for i in 1:n_variables
            println(f, "$(x[i]): $(value(x[i]))")
            solution_point[i] = value(x[i])
        end
    end

    variable_names = vcat(["x[$(string(i))]" for i in 1:n_variables])
    constraint_names = ["c1"]

    obj_val = objective_value(model)

    return obj_f, constraints_f, variable_names, constraint_names, starting_point, solution_point, obj_val
end

optimizers = [Ipopt.Optimizer]
opt_names = ["ipopt"]

problems = Dict()

for (opt, opt_name) in zip(optimizers, opt_names)
    objective_f, constraints_f, variable_names, constraint_names, starting_point, solution_point, obj_val = solve_model(opt, "sphere_$(opt_name).log")
    problems[opt_name] = (objective_f, constraints_f, variable_names, constraint_names, starting_point, solution_point, obj_val)
end

#All functions are equal, get first to initialize OptMap
objective_f, constraints_f, variable_names, constraint_names, _, _, obj_val = problems[opt_names[1]]
variable_domains = convert(Array{Tuple{Float64,Float64},1}, [])

for i in 1:length(variable_names)
    push!(variable_domains, (-4.0,5.0))
end

O = OptMap(variable_names, variable_domains, objective_f; constraint_names=constraint_names, constraints_f=constraints_f, verbose=true)
create_map!(O)

for opt_name in opt_names
    _, _, _, _, starting_point, solution_point, _ = problems[opt_name]

    path_to_solution = zeros(Float64, length(variable_domains), 2)
    path_to_solution[:,1] = starting_point
    path_to_solution[:,2] = solution_point

    draw_solution!(O, path_to_solution; line_width=3.0)
    save_all(O, "sphere_$(opt_name)", ".")
end


module ForwardAutodiff

import Base: exp, sin, cos, tan, +, -, *, /, sqrt, convert, promote_rule, zero

export D

struct Infinitesimal{T <: Number} <: Number
    x::T
    dx::T
end

function convert(::Type{Infinitesimal{T}}, x::T) where {T}
   Infinitesimal(x, zero(x))
end
function convert(::Type{Infinitesimal{T}}, x::Infinitesimal{S}) where {S, T}
   Infinitesimal(T(x.x), T(x.dx))
end
# This is needed according to an error before
function convert(::Type{Infinitesimal{T}}, x::T) where {T <: Number}
    Infinitesimal(x, zero(x))
end
function convert(::Type{Infinitesimal{T}}, x::S) where {T, S <: Number}
    x_as_T = convert(T, x)
    Infinitesimal(x_as_T, zero(x_as_T))
end

function zero(x::Infinitesimal{T}) where T
    Infinitesimal(zero(x.x), zero(x.dx))
end

function promote_rule(::Type{Infinitesimal{T}}, ::Type{Infinitesimal{S}}) where {T,S}
    Infinitesimal{promote_type(T,S)}
end
function promote_rule(::Type{Infinitesimal{T}}, ::Type{S}) where {T, S <: Number}
    Infinitesimal{promote_type(T,S)}
end
function promote_rule(::Type{T}, ::Type{Infinitesimal{S}}) where {T <: Number, S}
    Infinitesimal{promote_type(T,S)}
end
function promote_rule(::Type{S}, ::Type{Infinitesimal{T}}) where {S <: AbstractIrrational, T}
    Infinitesimal{promote_type(S,T)}
end

function extract_derivative(xdx::Infinitesimal)
    return xdx.dx
end
function extract_derivative(xs::Array)
    [extract_derivative(x) for x in xs]
end
function extract_derivative(xs::Tuple)
    convert(Tuple, [extract_derivative(x) for x in xs])
end

"""    D([i], f)

Differential operator; with an optional integer, specifies a partial derivative
with respect to argment `i` (from 1).

Returns a function that computes the derivative of `f`.  NOTE: at the moment,
nested derivatives won't work: `D(D(f))` will not compute a second derivative.
"""
function D(f)
    function df(x)
        xdx = Infinitesimal(x, one(x))
        result = f(xdx)
        return extract_derivative(result)
    end
    df
end
function D(i::Integer, f)
    function df(xs...)
        xarr = [(j != i ? x : Infinitesimal(x, one(x))) for (j,x) in enumerate(xs)]
        result = f(xarr...)
        return extract_derivative(result)
    end
    df
end

function +(x::Infinitesimal, y::Infinitesimal)
    Infinitesimal(x.x+y.x, x.dx+y.dx)
end
function -(x::Infinitesimal, y::Infinitesimal)
    Infinitesimal(x.x-y.x, x.dx-y.dx)
end
function -(x::Infinitesimal)
    Infinitesimal(-x.x, -x.dx)
end
function *(x::Infinitesimal, y::Infinitesimal)
    Infinitesimal(x.x*y.x, x.x*y.dx + x.dx*y.x)
end
function /(x::Infinitesimal, y::Infinitesimal)
    Infinitesimal(x.x/y.x, x.dx/y.x - x.x*y.dx/(y.x*y.x))
end

function sqrt(x::Infinitesimal)
    Infinitesimal(sqrt(x.x), x.dx/(2*sqrt(x.x)))
end

function exp(x::Infinitesimal)
    return Infinitesimal(exp(x.x), exp(x.x)*x.dx)
end
function sin(x::Infinitesimal)
    return Infinitesimal(sin(x.x), cos(x.x)*x.dx)
end
function cos(x::Infinitesimal)
    return Infinitesimal(cos(x.x), -sin(x.x)*x.dx)
end
function tan(x::Infinitesimal)
    c = cos(x.x)
    return Infinitesimal(tan(x.x), x.dx/(c*c))
end

end # module


"""
    estimateKernelSize{T <: Real}(X::Array{T, 2}; sampleSize = 100)

    Estimate kernel size by taking median distance between points.
"""
function estimateKernelSize(X::Array{T, 2}; sampleSize = 100) where T <: Real

	M = size(X)[1]

	# set kernel size to median distance between points
	if M > sampleSize
		Xmed = X[shuffle(collect(1:M))[1:sampleSize],:]
		S = sampleSize
	else
		Xmed = X
		S = M
	end

	dists = pairwise(SqEuclidean(), Xmed, Xmed, dims=1)
    sig = sqrt(0.5 * median(dists))

    return sig
end

"""
    rbfDotProduct(X::Array{T, 2}, X::Array{T, 2}, kernelSize::Float64)

"""
function rbfDotProduct(X::Array{T, 2}, Y::Array{T, 2}, kernelSize) where T <: Real

    H = zeros(size(X,1), size(Y,1))
    @assert size(X,2) == size(Y,2)

    for i in 1:size(X,1)
        for j in 1:size(Y,1)
            @inbounds begin
                H[i,j] = first( (X[i,:].-Y[j,:]) * (X[i,:].-Y[j,:])' )
            end
        end
    end

    return exp.(-H/2/kernelSize^2)
end

eye(n) = Matrix(I, n, n)


using MonteCarloX
using Random
using Plots

""" Self-exciting process. Rate jumps up by x_increase on event and subsequently
decays to the baserate with timeconstant tau."""
function hawkes_process(tau::Float64 = 1.0, baserate::Float64 = 0.2, x_increase::Float64 = 0.001;
    plotflag::Bool = false, nSamples::Int = 30, rng::AbstractRNG = MersenneTwister(1000))

    alg = InhomogeneousPoissonPiecewiseDecreasing()

    decay(t, t0, x0) = x0 * exp(-(t - t0) / tau)
    Lambda(t, t0, x0) = baserate + decay(t, t0, x0)

    samples = zeros(nSamples)

    t0 = 0.0
    x0 = 0.0
    for i in 1:nSamples
        t_previous = t0
        t0 += next_time(alg, rng, dt->Lambda(dt, 0.0, x0))
        # The current exponential decays starting value x0 is
        # the decay up to now (t0) plus the increase
        x0 = decay(t0, t_previous, x0) + x_increase
        samples[i] = t0
    end

    if plotflag
        # For plotting we are inefficient; (t > t0) means only causal influence
        ratefun(t) = sum([(t > t0) * decay(t, t0, x_increase) for t0 in samples])
        maxT = samples[end] + 4 * tau
        plotrange = range(0.0, stop = maxT, length = 2000)
        plot(plotrange, map(ratefun, plotrange), grid = false)
        display(vline!(samples)) # plot lines when events happen
    end

    return samples
end


T = 250
σ = 0.2
t = range(0, 20, length = T)
s = sin.(t)
y = s + randn(T) * σ

h = (t[end] - t[1])/T
H = [1.0, 0.0, 0.0]
A = [1.0 h   0.5*h^2;
     0.0 1.0 h;
     0.0 0.0 1.0]

m = Model(y, H, A, Matrix(0.01*I, 3, 3), σ^2, [0.0, 0.0, 0.0], Matrix(1000.0I, 3, 3))
kf = kalmanfilter(m)
ks = kalmansmoother(m, kf.priors, kf.posteriors)

using Plots

scatter(y, label = "Measurements", legend = :bottom, ylim = [-3, 2],
        markerstrokewidth = 0, markercolor = "black", foreground_color_legend = nothing)
plot!(s, label = "Noiseless signal", linewidth = 3, color = "grey")
plot!(kf.priors.μ, label = "Prediction", linewidth = 2, color = "blue")
plot!(kf.posteriors.μ, label = "Filter", linewidth = 2, color = "red")
plot!(ks.μ, label = "Smoother", linewidth = 2, color = "green")

scatter(y, label = "Measurements", legend = :bottom, ylim = [-3, 2],
        markerstrokewidth = 0, markercolor = "black", foreground_color_legend = nothing)
plot!(s, label = "Noiseless signal", linewidth = 3, color = "grey")
plot!(ks.x, label = ["Position" "Velocity" "Acceleration"], linewidth = 2, color = ["blue" "red" "green"])


using DelimitedFiles
using FFTW

function create_sine_wave(freq, res, filename)
    a = [sin(freq*2*pi*i/res) for i = 1:res]
    writedlm(filename, a)
end

function create_sine_fft(freq, res, filename)
    a = [sin(freq*2*pi*i/res) for i = 1:res]
    writedlm(filename, abs.(fft(a)))
end

function create_3_sine_waves(freq1, freq2, freq3, res, filename)
    a = [sin(freq1*2*pi*i/res) for i = 1:res]
    a += [sin(freq2*2*pi*i/res) for i = 1:res]
    a += [sin(freq3*2*pi*i/res) for i = 1:res]
    writedlm(filename, a)
end

function create_3_sine_fft(freq1, freq2, freq3, res, filename)
    a = [sin(freq1*2*pi*i/res) for i = 1:res]
    a += [sin(freq2*2*pi*i/res) for i = 1:res]
    a += [sin(freq3*2*pi*i/res) for i = 1:res]
    writedlm(filename, abs.(fft(a)))
end


function nlp_cvx_102_010(
    optimizer,
    objective_tol,
    primal_tol,
    dual_tol,
    termination_target = TERMINATION_TARGET_LOCAL,
    primal_target = PRIMAL_TARGET_LOCAL,
)
    # Test Goals:
    # - linear and nonlinear constraints
    # Variants
    #   010 - constraint intersection
    #   011 - one binding constraint (linear)
    #   012 - one binding constraint (nonlienar)
    #   013 - one binding constraint (quadratic objective)
    #   014 - no binding constraints (quadratic objective)

    model = Model(optimizer)

    @variable(model, x)
    @variable(model, y)

    @objective(model, Min, -x)
    @NLconstraint(model, x^2 + y^2 <= 1.0)
    @constraint(model, x + y >= 1.2)

    optimize!(model)

    check_status(model, FEASIBLE_PROBLEM, termination_target, primal_target)
    check_objective(model, -0.974165743715913, tol = objective_tol)
    return check_solution(
        [x, y],
        [0.974165743715913, 0.2258342542139504],
        tol = primal_tol,
    )
end


using LsqFit
using Statistics
using StatsBase

"""
fit_straight_line(tdata::Vector{Float64}, vdata::Vector{Float64}; pa0=[0.0, 0.5], i0=1)

# Fields

`tdata`: time-like (x) data
`vdata`` : voltage-like (y) data
`pa0`: initial values of parameters
`i0` : index of first point in array to be used in the fit

# Returns
- coefficient of the field (a vector)
- standard errors of coefficients (a vector)
- yfit = fit(tdata), that is the prediction of the values corresponding to each tdata
  for the fit parameters

"""
function fit_straight_line(tdata::Vector{Float64}, vdata::Vector{Float64}; pa0=[0.0, 0.5], i0=1)
	tfun(t, a, b) = a + b * t
	pfun(t, p) = p[1] .+ p[2] .* t
	il = length(tdata)
	fit = curve_fit(pfun, tdata[i0:il], vdata[i0:il], pa0)
	coef(fit), stderror(fit), tfun.(tdata, coef(fit)...)
end

"""
fit_expo(tdata::Vector{Float64}, vdata::Vector{Float64}; pa0=[0.0, 0.5], i0=1)

# Fields

`tdata`: time-like (x) data
`vdata`` : voltage-like (y) data
`pa0`: initial values of parameters
`i0` : index of first point in array to be used in the fit

# Returns
- coefficient of the field (a vector)
- standard errors of coefficients (a vector)
- yfit = fit(tdata), that is the prediction of the values corresponding to each tdata
  for the fit parameters

"""
function fit_expo(tdata::Vector{Float64}, vdata::Vector{Float64}; pa0=[0.0, 0.5], i0=1)
	tfun(t, N, λ) = N*exp(-t/λ)
	pfun(t, p) = p[1] * exp.(-t/p[2])
	il = length(tdata)
	fit = curve_fit(pfun, tdata[i0:il], vdata[i0:il], pa0)
	coef(fit), stderror(fit), tfun.(tdata, coef(fit)...)
end

"""
    fit_time_histo(htime::Histogram; pa0=[100.0, 0.5], i0=1)

# Fields

	`htime` : An histogram of times
	`pa0`   : initial value of fit parameters
	`i0`    : first bin to fit

# Returns

# Returns
- coefficient of the field (a vector)
- standard errors of coefficients (a vector)
- yfit = fit(tdata), that is the prediction of the values corresponding to each tdata
  for the fit parameters

"""
function fit_time_histo(htime::Histogram; pa0=[100.0, 0.5], i0=1)
	tdata = centers(htime)
	vdata = htime.weights
    fit_expo(tdata, vdata; pa0=pa0, i0=i0)
end


@doc raw"""
    LogKernel([α [,γ]])

The Log Kernel is a negative definite kernel given by:

```math
\kappa(\mathbf{x},\mathbf{y}) =
\log \left(1 + \alpha\|\mathbf{x} - \mathbf{y} \|^{2\gamma}\right)
\qquad \alpha > 0, \; \gamma \in (0,1]
```
"""
struct LogKernel{T<:AbstractFloat} <: NegativeDefiniteKernel{T}
    alpha::HyperParameter{T}
    gamma::HyperParameter{T}
    LogKernel{T}(α::Real, γ::Real) where {T<:AbstractFloat} = new{T}(
        HyperParameter(convert(T,α), interval(OpenBound(zero(T)), nothing)),
        HyperParameter(convert(T,γ), interval(OpenBound(zero(T)), ClosedBound(one(T))))
    )
end
function LogKernel(α::T1 = 1.0, γ::T2 = one(T1)) where {T1<:Real,T2<:Real}
    LogKernel{floattype(T1,T2)}(α, γ)
end

@inline logkernel(z::T, α::T, γ::T) where {T<:AbstractFloat} = log(α*z^γ+1)

@inline basefunction(::LogKernel) = SquaredEuclidean()
@inline function kappa(κ::LogKernel{T}, z::T) where {T}
    logkernel(z, getvalue(κ.alpha), getvalue(κ.gamma))
end


defaulttol(a) = eps(real(float(one(eltype(a)))))^(1/2)

"""
 Take the L2 Tikhonov regularised inverse of a matrix `m`.

 The regularisation parameter is the larger of `delta` (the optional argument that defaults
 to zero) and square root of machine epsilon. The inverse is done using an SVD.
 """
 function reginv(a::AbstractMatrix, δ = defaulttol(a))
     U, S, V = svd(a)
     Sinv = inv.(hypot.(S, δ))
     return  V * Diagonal(Sinv) * U'
 end

 function posreginv(a::AbstractMatrix, δ = defaulttol(a))
     # assumes a is positive definite (or close to)
     D, V = eigen(Hermitian(a))
     Dinv = inv.(hypot.(D, δ))
     return  V * Diagonal(Dinv) * V'
 end

 # computes solution C of A * C + C * A = B for A hermitian
  function symm_sylvester_reg(A::AbstractMatrix, B::AbstractMatrix, δ = zero(eltype(A)))
      n = LinearAlgebra.checksquare(A)
      Λ, U = eigen(Hermitian(A))
      UdCU = U' * B * U
      for j = 1:n
          for i = 1:n
              UdCU[i,j] /= sqrt((Λ[i] + Λ[j])^2 + δ^2)
          end
      end
      C = U*UdCU*U'
      return C
  end


using IntelVectorMath, SpecialFunctions
using AcuteBenchmark

cd(@__DIR__)
rootPath = pwd()
################################################################
# Dimensions
oneArgDims = [10 20 50 100 200 500 700 1000 5000 10000]

twoArgDims = [10 20 50 100 200 500 700 1000 5000 10000; # arg1
              10 20 50 100 200 500 700 1000 5000 10000] # arg2

################################################################
## Real Functions

typesReal=[Float32, Float64]

configsRealBase = FunbArray([

    # oneArgDims
    Funb(acos, [(-1, 1)], typesReal, oneArgDims),
    Funb(asin, [(-1, 1)], typesReal, oneArgDims),
    Funb(atan, [(-50, 50)], typesReal, oneArgDims),
    Funb(cos, [(-1000, 1000)], typesReal, oneArgDims),
    Funb(sin, [(-1000, 1000)], typesReal, oneArgDims),
    Funb(tan, [(-1000, 1000)], typesReal, oneArgDims),
    Funb(acosh, [(1, 1000)], typesReal, oneArgDims),
    Funb(asinh, [(-1000, 1000)], typesReal, oneArgDims),
    Funb(atanh, [(-1, 1)], typesReal, oneArgDims),
    Funb(cosh, [(0, 89.415985f0)], typesReal, oneArgDims),
    Funb(sinh, [(-89.415985f0, 89.415985f0)], typesReal, oneArgDims),
    Funb(tanh, [(-8.66434f0, 8.66434f0)], typesReal, oneArgDims),
    Funb(cbrt, [(-1000, 1000)], typesReal, oneArgDims),
    Funb(sqrt, [(0, 1000)], typesReal, oneArgDims),
    Funb(exp, [(-88.72284f0, 88.72284f0)], typesReal, oneArgDims),
    Funb(expm1, [(-88.72284f0, 88.72284f0)], typesReal, oneArgDims),
    Funb(log, [(0, 1000)], typesReal, oneArgDims),
    # Funb(log10, [(0, 1000)], typesReal, oneArgDims), # faulty
    Funb(abs, [(-1000, 1000)], typesReal, oneArgDims),
    Funb(abs2, [(-1000, 1000)], typesReal, oneArgDims),
    Funb(ceil, [(-1000, 1000)], typesReal, oneArgDims),
    Funb(floor, [(-1000, 1000)], typesReal, oneArgDims),
    Funb(round, [(-1000, 1000)], typesReal, oneArgDims),
    Funb(trunc, [(-1000, 1000)], typesReal, oneArgDims),
    Funb(cis, [(-1000, 1000)], typesReal, oneArgDims),
    Funb(SpecialFunctions.erf, [(-3.8325067f0, 3.8325067f0)], typesReal, oneArgDims),
    Funb(SpecialFunctions.erfc, [(-3.7439213f0, 10.019834f0)], typesReal, oneArgDims),
    Funb(SpecialFunctions.erfinv, [(-1, 1)], typesReal, oneArgDims),
    Funb(SpecialFunctions.erfcinv, [(0, 2)], typesReal, oneArgDims),
    # Funb(SpecialFunctions.lgamma, [(0, 1000)], typesReal, oneArgDims),
    Funb(SpecialFunctions.gamma, [(0, 36)], typesReal, oneArgDims),

    # twoArgDims
    Funb(atan, [(-1, 1), (-1, 1)], typesReal, twoArgDims),
    Funb(hypot, [(-1000, 1000), (-1000, 1000)], typesReal, twoArgDims),
    # Funb(/, [(-1000, 1000), (-1000, 1000)], typesReal, twoArgDims),
    # Funb(^, [(0, 100), (-5, 20)], typesReal, twoArgDims),
])

configsRealIVM = FunbArray([

    # oneArgDims
    Funb(IVM.acos, [(-1, 1)], typesReal, oneArgDims),
    Funb(IVM.asin, [(-1, 1)], typesReal, oneArgDims),
    Funb(IVM.atan, [(-50, 50)], typesReal, oneArgDims),
    Funb(IVM.cos, [(-1000, 1000)], typesReal, oneArgDims),
    Funb(IVM.sin, [(-1000, 1000)], typesReal, oneArgDims),
    Funb(IVM.tan, [(-1000, 1000)], typesReal, oneArgDims),
    Funb(IVM.acosh, [(1, 1000)], typesReal, oneArgDims),
    Funb(IVM.asinh, [(-1000, 1000)], typesReal, oneArgDims),
    Funb(IVM.atanh, [(-1, 1)], typesReal, oneArgDims),
    Funb(IVM.cosh, [(0, 89.415985f0)], typesReal, oneArgDims),
    Funb(IVM.sinh, [(-89.415985f0, 89.415985f0)], typesReal, oneArgDims),
    Funb(IVM.tanh, [(-8.66434f0, 8.66434f0)], typesReal, oneArgDims),
    Funb(IVM.cbrt, [(-1000, 1000)], typesReal, oneArgDims),
    Funb(IVM.sqrt, [(0, 1000)], typesReal, oneArgDims),
    Funb(IVM.exp, [(-88.72284f0, 88.72284f0)], typesReal, oneArgDims),
    Funb(IVM.expm1, [(-88.72284f0, 88.72284f0)], typesReal, oneArgDims),
    Funb(IVM.log, [(0, 1000)], typesReal, oneArgDims),
    # Funb(IVM.log10, [(0, 1000)], typesReal, oneArgDims), # faulty
    Funb(IVM.abs, [(-1000, 1000)], typesReal, oneArgDims),
    Funb(IVM.abs2, [(-1000, 1000)], typesReal, oneArgDims),
    Funb(IVM.ceil, [(-1000, 1000)], typesReal, oneArgDims),
    Funb(IVM.floor, [(-1000, 1000)], typesReal, oneArgDims),
    Funb(IVM.round, [(-1000, 1000)], typesReal, oneArgDims),
    Funb(IVM.trunc, [(-1000, 1000)], typesReal, oneArgDims),
    Funb(IVM.cis, [(-1000, 1000)], typesReal, oneArgDims),
    Funb(IVM.erf, [(-3.8325067f0, 3.8325067f0)], typesReal, oneArgDims),
    Funb(IVM.erfc, [(-3.7439213f0, 10.019834f0)], typesReal, oneArgDims),
    Funb(IVM.erfinv, [(-1, 1)], typesReal, oneArgDims),
    Funb(IVM.erfcinv, [(0, 2)], typesReal, oneArgDims),
    # Funb(IVM.lgamma, [(0, 1000)], typesReal, oneArgDims), # faulty
    Funb(IVM.gamma, [(0, 36)], typesReal, oneArgDims),

    # twoArgDims
    Funb(IVM.atan, [(-1, 1), (-1, 1)], typesReal, twoArgDims),
    Funb(IVM.hypot, [(-1000, 1000), (-1000, 1000)], typesReal, twoArgDims),
    # Funb(IVM.(/), [(-1000, 1000), (-1000, 1000)], typesReal, twoArgDims),
    # Funb(IVM.(^), [(0, 100), (-5, 20)], typesReal, twoArgDims),
])

################################################################
# Complex Functions

typesComplex=[Complex{Float32}, Complex{Float64}]

configsComplexBase = FunbArray([
    # oneArgDims
    Funb(acos, [(-1, 1)], typesComplex, oneArgDims),
    Funb(asin, [(-1, 1)], typesComplex, oneArgDims),
    # Funb(atan, [(-50, 50)], typesComplex, oneArgDims),
    # Funb(cos, [(-10, 10)], typesComplex, oneArgDims),
    # Funb(sin, [(-10, 10)], typesComplex, oneArgDims),
    # Funb(tan, [(-10, 10)], typesComplex, oneArgDims),
    Funb(acosh, [(1, 1000)], typesComplex, oneArgDims),
    Funb(asinh, [(-1000, 1000)], typesComplex, oneArgDims),
    # Funb(atanh, [(-1, 1)], typesComplex, oneArgDims),
    # Funb(cosh, [(0, 89.415985f0)], typesComplex, oneArgDims),
    # Funb(sinh, [(-89.415985f0, 89.415985f0)], typesComplex, oneArgDims),
    # Funb(tanh, [(-8.66434f0, 8.66434f0)], typesComplex, oneArgDims),
    Funb(sqrt, [(0, 1000)], typesComplex, oneArgDims),
    Funb(exp, [(-88.72284f0, 88.72284f0)], typesComplex, oneArgDims),
    Funb(log, [(0, 1000)], typesComplex, oneArgDims),
    # Funb(log10, [(0, 1000)], typesComplex, oneArgDims),
    Funb(abs, [(-1000, 1000)], typesComplex, oneArgDims),
    Funb(angle, [(-1000, 1000)], typesComplex, oneArgDims),
    Funb(conj, [(-1000, 1000)], typesComplex, oneArgDims),
    # Funb(cis, [(-1000, 1000)], typesComplex, oneArgDims),

    # twoArgDims
    # Funb(/, [(-1000, 1000), (-1000, 1000)], typesComplex, twoArgDims),
    # Funb(^, [(0, 100), (-2, 10)], typesComplex, twoArgDims),

    # missing?
    ## oneArgDims

    # Funb(cbrt, [(-1000, 1000)], typesComplex, oneArgDims),
    # Funb(expm1, [(-88.72284f0, 88.72284f0)], typesComplex, oneArgDims),
    # Funb(abs2, [(-1000, 1000)], typesComplex, oneArgDims),
    # Funb(ceil, [(-1000, 1000)], typesComplex, oneArgDims),
    # Funb(SpecialFunctions.erf, [(-3.8325067f0, 3.8325067f0)], typesComplex, oneArgDims),
    # Funb(SpecialFunctions.erfc, [(-3.7439213f0, 10.019834f0)], typesComplex, oneArgDims),
    # Funb(SpecialFunctions.erfinv, [(-1, 1)], typesComplex, oneArgDims),
    # Funb(SpecialFunctions.erfcinv, [(0, 2)], typesComplex, oneArgDims),
    # Funb(SpecialFunctions.lgamma, [(0, 1000)], typesComplex, oneArgDims),
    # Funb(SpecialFunctions.gamma, [(0, 36)], typesComplex, oneArgDims),
    # Funb(floor, [(-1000, 1000)], typesComplex, oneArgDims),
    # Funb(round, [(-1000, 1000)], typesComplex, oneArgDims),
    # Funb(trunc, [(-1000, 1000)], typesComplex, oneArgDims),

    ## twoArgDims
    # Funb(atan, [(-1, 1), (-1, 1)], typesComplex, twoArgDims),
    # Funb(hypot, [(-1000, 1000), (-1000, 1000)], typesComplex, twoArgDims),
])

configsComplexIVM = FunbArray([
    # oneArgDims
    Funb(IVM.acos, [(-1, 1)], typesComplex, oneArgDims),
    Funb(IVM.asin, [(-1, 1)], typesComplex, oneArgDims),
    # Funb(IVM.atan, [(-50, 50)], typesComplex, oneArgDims),
    # Funb(IVM.cos, [(-10, 10)], typesComplex, oneArgDims),
    # Funb(IVM.sin, [(-10, 10)], typesComplex, oneArgDims),
    # Funb(IVM.tan, [(-10, 10)], typesComplex, oneArgDims),
    Funb(IVM.acosh, [(1, 1000)], typesComplex, oneArgDims),
    Funb(IVM.asinh, [(-1000, 1000)], typesComplex, oneArgDims),
    # Funb(IVM.atanh, [(-1, 1)], typesComplex, oneArgDims),
    # Funb(IVM.cosh, [(0, 89.415985f0)], typesComplex, oneArgDims),
    # Funb(IVM.sinh, [(-89.415985f0, 89.415985f0)], typesComplex, oneArgDims),
    # Funb(IVM.tanh, [(-8.66434f0, 8.66434f0)], typesComplex, oneArgDims),
    Funb(IVM.sqrt, [(0, 1000)], typesComplex, oneArgDims),
    Funb(IVM.exp, [(-88.72284f0, 88.72284f0)], typesComplex, oneArgDims),
    Funb(IVM.log, [(0, 1000)], typesComplex, oneArgDims),
    # Funb(IVM.log10, [(0, 1000)], typesComplex, oneArgDims),
    Funb(IVM.abs, [(-1000, 1000)], typesComplex, oneArgDims),
    Funb(IVM.angle, [(-1000, 1000)], typesComplex, oneArgDims),
    Funb(IVM.conj, [(-1000, 1000)], typesComplex, oneArgDims),
    # Funb(IVM.cis, [(-1000, 1000)], typesComplex, oneArgDims),

    # twoArgDims
    # Funb(IVM.(/), [(-1000, 1000), (-1000, 1000)], typesComplex, twoArgDims),
    # Funb(IVM.(^), [(0, 100), (-2, 10)], typesComplex, twoArgDims),

    # missing?
    ## oneArgDims

    # Funb(IVM.cbrt, [(-1000, 1000)], typesComplex, oneArgDims),
    # Funb(IVM.expm1, [(-88.72284f0, 88.72284f0)], typesComplex, oneArgDims),
    # Funb(IVM.abs2, [(-1000, 1000)], typesComplex, oneArgDims),
    # Funb(IVM.ceil, [(-1000, 1000)], typesComplex, oneArgDims),
    # Funb(IVM.erf, [(-3.8325067f0, 3.8325067f0)], typesComplex, oneArgDims),
    # Funb(IVM.erfc, [(-3.7439213f0, 10.019834f0)], typesComplex, oneArgDims),
    # Funb(IVM.erfinv, [(-1, 1)], typesComplex, oneArgDims),
    # Funb(IVM.erfcinv, [(0, 2)], typesComplex, oneArgDims),
    # Funb(IVM.lgamma, [(0, 1000)], typesComplex, oneArgDims),
    # Funb(IVM.gamma, [(0, 36)], typesComplex, oneArgDims),
    # Funb(IVM.floor, [(-1000, 1000)], typesComplex, oneArgDims),
    # Funb(IVM.round, [(-1000, 1000)], typesComplex, oneArgDims),
    # Funb(IVM.trunc, [(-1000, 1000)], typesComplex, oneArgDims),

    ## twoArgDims
    # Funb(IVM.atan, [(-1, 1), (-1, 1)], typesComplex, twoArgDims),
    # Funb(IVM.hypot, [(-1000, 1000), (-1000, 1000)], typesComplex, twoArgDims),
])

################################################################
# Performing Benchmarks
# Real
println("\nBenchmarking Base Real Functions\n")
benchmark!(configsRealBase)

println("\nBenchmarking IntelVectorMath Real Functions\n")
benchmark!(configsRealIVM)

cd(joinpath(rootPath,"Real"))

bar(configsRealBase => configsRealIVM, uniqueType = true, dimAnnotation = false, uniqueDim = true, configName =  "Base" => "IntelVectorMath")

dimplot([configsRealBase,configsRealIVM],["Base", "IntelVectorMath"])
################################################################
# Complex
println("\nBenchmarking Base Complex Functions\n")
benchmark!(configsComplexBase)

println("\nBenchmarking IntelVectorMath Complex Functions\n")
benchmark!(configsComplexIVM)

cd(joinpath(rootPath,"Complex"))

bar(configsComplexBase => configsComplexIVM, uniqueType = true, dimAnnotation = false, uniqueDim = true, configName = "Base" => "IntelVectorMath")

dimplot([configsComplexBase,configsComplexIVM],["Base", "IntelVectorMath"])


doc"""
    Pareto(α,θ)

The *Pareto distribution* with shape `α` and scale `θ` has probability density function

$f(x; \alpha, \theta) = \frac{\alpha \theta^\alpha}{x^{\alpha + 1}}, \quad x \ge \theta$

```julia
Pareto()            # Pareto distribution with unit shape and unit scale, i.e. Pareto(1.0, 1.0)
Pareto(a)           # Pareto distribution with shape a and unit scale, i.e. Pareto(a, 1.0)
Pareto(a, b)        # Pareto distribution with shape a and scale b

params(d)        # Get the parameters, i.e. (a, b)
shape(d)         # Get the shape parameter, i.e. a
scale(d)         # Get the scale parameter, i.e. b
```

External links
 * [Pareto distribution on Wikipedia](http://en.wikipedia.org/wiki/Pareto_distribution)

"""
immutable Pareto <: ContinuousUnivariateDistribution
    α::Float64
    θ::Float64

    function Pareto(α::Real, θ::Real)
        @check_args(Pareto, α > zero(α) && θ > zero(θ))
        new(α, θ)
    end
    Pareto(α::Real) = Pareto(α, 1.0)
    Pareto() = new(1.0, 1.0)
end

@distr_support Pareto d.θ Inf

#### Parameters

shape(d::Pareto) = d.α
scale(d::Pareto) = d.θ

params(d::Pareto) = (d.α, d.θ)

#### Statistics

mean(d::Pareto) = ((α, θ) = params(d); α > 1.0 ? α * θ / (α - 1.0) : Inf)
median(d::Pareto) = ((α, θ) = params(d); θ * 2.0 ^ (1.0 / α))
mode(d::Pareto) = d.θ

function var(d::Pareto)
    (α, θ) = params(d)
    α > 2.0 ? (θ^2 * α) / ((α - 1.0)^2 * (α - 2.0)) : Inf
end

function skewness(d::Pareto)
    α = shape(d)
    α > 3.0 ? ((2.0 * (1.0 + α)) / (α - 3.0)) * sqrt((α - 2.0) / α) : NaN
end

function kurtosis(d::Pareto)
    α = shape(d)
    α > 4.0 ? (6.0 * (α^3 + α^2 - 6.0 * α - 2.0)) / (α * (α - 3.0) * (α - 4.0)) : NaN
end

entropy(d::Pareto) = ((α, θ) = params(d); log(θ / α) + 1.0 / α + 1.0)

#### Evaluation

function pdf(d::Pareto, x::Float64)
    (α, θ) = params(d)
    x >= θ ? α * (θ / x)^α * (1.0 / x) : 0.0
end

function logpdf(d::Pareto, x::Float64)
    (α, θ) = params(d)
    x >= θ ? log(α) + α * log(θ) - (α + 1.0) * log(x) : -Inf
end

function ccdf(d::Pareto, x::Float64)
    (α, θ) = params(d)
    x >= θ ? (θ / x)^α : 1.0
end

cdf(d::Pareto, x::Float64) = 1.0 - ccdf(d, x)

function logccdf(d::Pareto, x::Float64)
    (α, θ) = params(d)
    x >= θ ? α * log(θ / x) : 0.0
end

logcdf(d::Pareto, x::Float64) = log1p(-ccdf(d, x))

cquantile(d::Pareto, p::Float64) = d.θ / p^(1.0 / d.α)
quantile(d::Pareto, p::Float64) = cquantile(d, 1.0 - p)

#### Sampling

rand(d::Pareto) = d.θ * exp(randexp() / d.α)

## Fitting

function fit_mle{T <: Real}(::Type{Pareto}, x::AbstractArray{T})
    # Based on
    # https://en.wikipedia.org/wiki/Pareto_distribution#Parameter_estimation

    θ = minimum(x)

    n = length(x)
    lθ = log(θ)
    temp1 = zero(T)
    for i=1:n
        temp1 += log(x[i]) - lθ
    end
    α = n/temp1

    return Pareto(α, θ)
end


# p39.m - eigenmodes of biharmonic on a square with clamped BCs
#         (compare p38.m)

# Construct spectral approximation to biharmonic operator:
N = 17; (D,x) = cheb(N); D2 = D^2; D2 = D2[2:N,2:N];
S = diagm([0; 1 ./(1-x[2:N].^2); 0]);
D4 = (diagm(1-x.^2)*D^4 - 8*diagm(x)*D^3 - 12*D^2)*S;
D4 = D4[2:N,2:N]; I = eye(N-1);
L = kron(I,D4) + kron(D4,I) + 2*kron(D2,I)*kron(I,D2);

# Find and plot 25 eigenmodes:
(Lam,V) = eig(-L); Lam = -real(Lam);
ii = sortperm(Lam)[1:25]; Lam = Lam[ii]; V = real(V[:,ii]);
Lam = sqrt.(Lam/Lam[1]);
y = x; xxx = yyy = -1:.01:1;
sq = [1+1im,-1+1im,-1-1im,1-1im,1+1im]; clf();
for i = 1:25
    uu = zeros(N+1,N+1); uu[2:N,2:N] = reshape(V[:,i],N-1,N-1);
    subplot(5,5,i); plot(real(sq),imag(sq));
    s = interpolate((x[end:-1:1],y[end:-1:1]),reduce(flipdim,uu,1:2),Gridded(Linear()));
    contour(xxx,yyy,s[xxx,yyy],levels=[0],color="k"); axis("square");
    axis(1.25*[-1,1,-1,1]); axis("off");
    text(-.3,1.15,"$(signif(Lam[i],5))",fontsize=7);
end


"""
    euclsq(X, Y)
Squared Euclidean distances
between the rows of `X` and `Y`.
* `X` : Data.
* `Y` : Data.

When `X` (n, p) and `Y` (m, p), it returns an object (n, m) with:
* i, j = distance between row i of `X` and row j of `Y`.
"""
function euclsq(X, Y)
    X = ensure_mat(X)
    Y = ensure_mat(Y)
    Distances.pairwise(SqEuclidean(), X', Y', dims = 2)
end

"""
    mahsq(X, Y)
    mahsq(X, Y, Sinv)
Squared Mahalanobis distances
between the rows of `X` and `Y`.
* `X` : Data.
* `Y` : Data.
* `Sinv` : Inverse of a covariance matrix S.
    If not given, this is the uncorrected covariance matrix of `X`.

For `X` (n, p) and `Y` (m, p), it returns an object (n, m) with:
* i, j = distance between row i of `X` and row j of `Y`.
"""
function mahsq(X, Y)
    X = ensure_mat(X)
    Y = ensure_mat(Y)
    S = Statistics.cov(X, corrected = false)
    LinearAlgebra.inv!(cholesky!(Hermitian(S)))
    Distances.pairwise(SqMahalanobis(S), X', Y', dims = 2)
end

function mahsq(X, Y, Sinv)
    X = ensure_mat(X)
    Y = ensure_mat(Y)
    Sinv = ensure_mat(Sinv)
    Distances.pairwise(SqMahalanobis(Sinv), X', Y', dims = 2)
end

"""
    mahsqchol(X, Y)
    mahsqchol(X, Y, Uinv)
Compute the squared Mahalanobis distances (with a Cholesky factorization)
between the observations (rows) of `X` and `Y`.
* `X` : Data.
* `Y` : Data.
* `Uinv` : Inverse of the upper matrix of a Cholesky factorization
    of a covariance matrix S.
    If not given, the factorization is done on S, the uncorrected covariance matrix of `X`.

When `X` (n, p) and `Y` (m, p), it returns an object (n, m) with:
* i, j = distance between row i of `X` and row j of `Y`.
"""
function mahsqchol(X, Y)
    X = ensure_mat(X)
    Y = ensure_mat(Y)
    p = size(X, 2)
    S = Statistics.cov(X, corrected = false)
    if p == 1
        Uinv = inv(sqrt(S))
    else
        Uinv = LinearAlgebra.inv!(cholesky!(Hermitian(S)).U)
    end
    zX = X * Uinv
    zY = Y * Uinv
    euclsq(zX, zY)
end

function mahsqchol(X, Y, Uinv)
    X = ensure_mat(X)
    Y = ensure_mat(Y)
    Uinv = ensure_mat(Uinv)
    zX = X * Uinv
    zY = Y * Uinv
    euclsq(zX, zY)
end


using Jecco
using LinearAlgebra
using SparseArrays
using Plots

# returns A_ij = x_i A_ij (no sum in i). note that A itself is also changed.
# this is equivalent to the operation A = x .* A, but it's much more efficient
function mul_col!(x::Vector, A::SparseMatrixCSC)
    @assert size(x)[1] == size(A)[1]

    rows = rowvals(A)
    vals = nonzeros(A)
    @inbounds for idx in eachindex(vals)
        row = rows[idx]
        vals[idx] *= x[row]
    end
    A
end

#=
build operator

   aa Dxx + bb Dx + cc

by overwriting Dxx with the returned value. the construction here is more
efficient than directly summing the sparse arrays. note that this construction
only works because we know that the returned array has the *same* sparsity
pattern as Dxx! we couldn't, for instance, mutate Dx instead since the Dx array
has zeros along the diagonal (which would be skipped in the construction below).
=#
function build_operator!(Dxx::SparseMatrixCSC, Dx::SparseMatrixCSC,
                         aa::Vector, bb::Vector, cc::Vector)
    mul_col!(aa, Dxx)
    mul_col!(bb, Dx)
    ccId = Diagonal(cc)

    rows = rowvals(Dxx)
    vals = nonzeros(Dxx)
    m, n = size(Dxx)

    # cf. https://docs.julialang.org/en/v1/stdlib/SparseArrays/#SparseArrays.nzrange
    @inbounds for j = 1:n
        @inbounds for i in nzrange(Dxx, j)
            row = rows[i]
            vals[i] += Dx[row,j] + ccId[row,j]
        end
    end
    Dxx
end

source(x) = exp(-x^2) * (-2 + 3*x^2)

x_min   = -5.0
x_max   = 5.0
x_nodes = 256

xcoord  = Cartesian{1}("x", x_min, x_max, x_nodes, endpoint=false)

hx = Jecco.delta(xcoord)
Nx = xcoord.nodes
x  = xcoord[:]

f_exact = exp.(-x.^2)

ord = 4

Dx_op  = CenteredDiff{1}(1, ord, hx, Nx)
Dxx_op = CenteredDiff{1}(2, ord, hx, Nx)
Dx     = SparseMatrixCSC(Dx_op)
Dxx    = SparseMatrixCSC(Dxx_op)

aa = ones(Nx)
bb = copy(x)
cc = x.^2

A_mat  = copy(Dxx)
Dx_tmp = copy(Dx)

# build operator A = Dxx + x Dx + x^2
build_operator!(A_mat, Dx_tmp, aa, bb, cc)

b_vec = source.(x)

A_fact = factorize(A_mat)
sol    = A_fact \ b_vec

plot(x, f_exact)
scatter!(x, sol)


"""
    LeviCivita(T::Type{<:Number} = Sym)

Builds an Array{T,3} of Levi-Civita Symbol `ϵᵢⱼₖ = (i-j) (j-k) (k-i) / 2`

# Examples
```julia
julia> ε = LeviCivita(Sym)
3×3×3 Array{Sym, 3}:
[:, :, 1] =
 0   0  0
 0   0  1
 0  -1  0

[:, :, 2] =
 0  0  -1
 0  0   0
 1  0   0

[:, :, 3] =
  0  1  0
 -1  0  0
  0  0  0
```
"""
LeviCivita(T::Type{<:Number} = Sym) = [T(T((i - j) * (j - k) * (k - i)) / T(2)) for i = 1:3, j = 1:3, k = 1:3]

"""
    𝐞(i::Integer, dim::Int = 3, T::Type{<:Number} = Sym)

Vector of the canonical basis

# Examples
```julia
julia> 𝐞(1)
Tens{1, 3, Sym, Sym, Vec{3, Sym}, CanonicalBasis{3, Sym}}
# data: 3-element Vec{3, Sym}:
 1
 0
 0
# var: (:cont,)
# basis: 3×3 Tensor{2, 3, Sym, 9}:
 1  0  0
 0  1  0
 0  0  1
```
"""
𝐞(::Val{i}, ::Val{dim} = Val(3), ::Val{T} = Val(Sym)) where {i, dim, T<:Number} =
    Tens(Vec{dim}(j -> j == i ? one(T) : zero(T)))

"""
    init_cartesian(coords = symbols("x y z", real = true))

Returns the coordinates, unit vectors and basis of the cartesian basis

# Examples
```julia
julia> coords, vectors, ℬ = init_cartesian() ; x, y, z = coords ; 𝐞₁, 𝐞₂, 𝐞₃ = vectors ;
```
"""
init_cartesian(coords = symbols("x y z", real = true)) = Tuple(coords),
ntuple(i -> 𝐞(Val(i), Val(length(coords)), Val(eltype(coords))), length(coords)),
CanonicalBasis{length(coords),eltype(coords)}()

init_cartesian(::Val{3}) = init_cartesian(symbols("x y z", real = true))
init_cartesian(::Val{2}) = init_cartesian(symbols("x y", real = true))
init_cartesian(dim::Integer) = init_cartesian(Val(dim))

"""
    𝐞ᵖ(i::Integer, θ::T = zero(Sym); canonical = false)

Vector of the polar basis

# Examples
```julia
julia> θ = symbols("θ", real = true) ;

julia> 𝐞ᵖ(1, θ)
Tens{1, 2, Sym, Sym, Vec{2, Sym}, RotatedBasis{2, Sym}}
# data: 2-element Vec{2, Sym}:
 1
 0
# var: (:cont,)
# basis: 2×2 Tensor{2, 2, Sym, 4}:
 cos(θ)  -sin(θ)
 sin(θ)   cos(θ)
```
"""
𝐞ᵖ(::Val{1}, θ::T = zero(Sym); canonical = false) where {T<:Number} =
    canonical ? Tens(Vec{2}([cos(θ), sin(θ)])) :
    Tens(Vec{2}([one(T), zero(T)]), Basis(θ))
𝐞ᵖ(::Val{2}, θ::T = zero(Sym); canonical = false) where {T<:Number} =
    canonical ? Tens(Vec{2}([-sin(θ), cos(θ)])) :
    Tens(Vec{2}([zero(T), one(T)]), Basis(θ))

"""
    init_polar(coords = (symbols("r", positive = true), symbols("θ", real = true)); canonical = false)

Returns the coordinates, base vectors and basis of the polar basis

# Examples
```julia
julia> coords, vectors, ℬᵖ = init_polar() ; r, θ = coords ; 𝐞ʳ, 𝐞ᶿ = vectors ;
```
"""
init_polar(
    coords = (symbols("r", positive = true), symbols("θ", real = true));
    canonical = false,
) = Tuple(coords),
ntuple(i -> 𝐞ᵖ(Val(i), coords[2]; canonical = canonical), 2),
Basis(coords[2])

"""
    𝐞ᶜ(i::Integer, θ::T = zero(Sym); canonical = false)

Vector of the cylindrical basis

# Examples
```julia
julia> θ = symbols("θ", real = true) ;

julia> 𝐞ᶜ(1, θ)
Tens{1, 3, Sym, Sym, Vec{3, Sym}, RotatedBasis{3, Sym}}
# data: 3-element Vec{3, Sym}:
 1
 0
 0
# var: (:cont,)
# basis: 3×3 Tensor{2, 3, Sym, 9}:
 cos(θ)  -sin(θ)  0
 sin(θ)   cos(θ)  0
      0        0  1
```
"""
𝐞ᶜ(::Val{1}, θ::T = zero(Sym); canonical = false) where {T<:Number} =
    canonical ? Tens(Vec{3}([cos(θ), sin(θ), zero(T)])) :
    Tens(Vec{3}([one(T), zero(T), zero(T)]), CylindricalBasis(θ))
𝐞ᶜ(::Val{2}, θ::T = zero(Sym); canonical = false) where {T<:Number} =
    canonical ? Tens(Vec{3}([-sin(θ), cos(θ), zero(T)])) :
    Tens(Vec{3}([zero(T), one(T), zero(T)]), CylindricalBasis(θ))
𝐞ᶜ(::Val{3}, θ::T = zero(Sym); canonical = false) where {T<:Number} =
    canonical ? Tens(Vec{3}([zero(T), zero(T), one(T)])) :
    Tens(Vec{3}([zero(T), zero(T), one(T)]), CylindricalBasis(θ))

"""
    init_cylindrical(coords = (symbols("r", positive = true), symbols("θ", real = true), symbols("z", real = true)); canonical = false)

Returns the coordinates, base vectors and basis of the cylindrical basis

# Examples
```julia
julia> coords, vectors, ℬᶜ = init_cylindrical() ; r, θ, z = coords ; 𝐞ʳ, 𝐞ᶿ, 𝐞ᶻ = vectors ;
```
"""
init_cylindrical(
    coords = (
        symbols("r", positive = true),
        symbols("θ", real = true),
        symbols("z", real = true),
    );
    canonical = false,
) = Tuple(coords),
ntuple(i -> 𝐞ᶜ(Val(i), coords[2]; canonical = canonical), 3),
CylindricalBasis(coords[2])

"""
    𝐞ˢ(i::Integer, θ::T = zero(Sym), ϕ::T = zero(Sym), ψ::T = zero(Sym); canonical = false)

Vector of the basis rotated with the 3 Euler angles `θ, ϕ, ψ` (spherical if `ψ=0`)

# Examples
```julia
julia> θ, ϕ, ψ = symbols("θ, ϕ, ψ", real = true) ;

Tens{1, 3, Sym, Sym, Vec{3, Sym}, RotatedBasis{3, Sym}}
# data: 3-element Vec{3, Sym}:
 1
 0
 0
# var: (:cont,)
# basis: 3×3 Tensor{2, 3, Sym, 9}:
 -sin(ψ)⋅sin(ϕ) + cos(θ)⋅cos(ψ)⋅cos(ϕ)  -sin(ψ)⋅cos(θ)⋅cos(ϕ) - sin(ϕ)⋅cos(ψ)  sin(θ)⋅cos(ϕ)
  sin(ψ)⋅cos(ϕ) + sin(ϕ)⋅cos(θ)⋅cos(ψ)  -sin(ψ)⋅sin(ϕ)⋅cos(θ) + cos(ψ)⋅cos(ϕ)  sin(θ)⋅sin(ϕ)
                        -sin(θ)⋅cos(ψ)                          sin(θ)⋅sin(ψ)         cos(θ)
```
"""
function 𝐞ˢ(
    ::Val{1},
    θ::T1 = 0,
    ϕ::T2 = 0,
    ψ::T3 = 0;
    canonical = false,
) where {T1<:Number,T2<:Number,T3<:Number}
    if canonical
        return Tens(
            Vec{3}([
                -sin(ψ) * sin(ϕ) + cos(θ) * cos(ψ) * cos(ϕ),
                sin(ψ) * cos(ϕ) + sin(ϕ) * cos(θ) * cos(ψ),
                -sin(θ) * cos(ψ),
            ]),
        )
    else
        T = promote_type(T1, T2, T3)
        return Tens(Vec{3}([one(T), zero(T), zero(T)]), Basis(θ, ϕ, ψ))
    end
end
function 𝐞ˢ(
    ::Val{2},
    θ::T1 = 0,
    ϕ::T2 = 0,
    ψ::T3 = 0;
    canonical = false,
) where {T1<:Number,T2<:Number,T3<:Number}
    if canonical
        return Tens(
            Vec{3}([
                -sin(ψ) * cos(θ) * cos(ϕ) - sin(ϕ) * cos(ψ),
                -sin(ψ) * sin(ϕ) * cos(θ) + cos(ψ) * cos(ϕ),
                sin(θ) * sin(ψ),
            ]),
        )
    else
        T = promote_type(T1, T2, T3)
        return Tens(Vec{3}([zero(T), one(T), zero(T)]), Basis(θ, ϕ, ψ))
    end
end
function 𝐞ˢ(
    ::Val{3},
    θ::T1 = 0,
    ϕ::T2 = 0,
    ψ::T3 = 0;
    canonical = false,
) where {T1<:Number,T2<:Number,T3<:Number}
    if canonical
        return Tens(Vec{3}([sin(θ) * cos(ϕ), sin(θ) * sin(ϕ), cos(θ)]))
    else
        T = promote_type(T1, T2, T3)
        return Tens(Vec{3}([zero(T), zero(T), one(T)]), Basis(θ, ϕ, ψ))
    end
end

for eb in (:𝐞, :𝐞ᵖ, :𝐞ᶜ, :𝐞ˢ)
    @eval $eb(i::Integer, args...; kwargs...) = $eb(Val(i), args...; kwargs...)
end

"""
    init_spherical(coords = (symbols("θ", real = true), symbols("ϕ", real = true), symbols("r", positive = true)); canonical = false)

Returns the coordinates, base vectors and basis of the spherical basis.
Take care that the order of the 3 vectors is `𝐞ᶿ, 𝐞ᵠ, 𝐞ʳ` so that
the basis coincides with the canonical one when the angles are null and in consistency
the coordinates are ordered as `θ, ϕ, r`.

# Examples
```julia
julia> coords, vectors, ℬˢ = init_spherical() ; θ, ϕ, r = coords ; 𝐞ᶿ, 𝐞ᵠ, 𝐞ʳ  = vectors ;
```
"""
init_spherical(
    coords = (
        symbols("θ", real = true),
        symbols("ϕ", real = true),
        symbols("r", positive = true),
    );
    canonical = false,
) = Tuple(coords),
ntuple(i -> 𝐞ˢ(Val(i), coords[1:2]...; canonical = canonical), 3),
SphericalBasis(coords[1:2]...)

"""
    init_rotated(coords = symbols("θ ϕ ψ", real = true); canonical = false)

Returns the angles, base vectors and basis of the rotated basis.
Note that here the coordinates are angles and do not represent a valid parametrization of `ℝ³`

# Examples
```julia
julia> angles, vectors, ℬʳ = init_rotated() ; θ, ϕ, ψ = angles ; 𝐞ᶿ, 𝐞ᵠ, 𝐞ʳ = vectors ;
```
"""
init_rotated(angles = symbols("θ ϕ ψ", real = true); canonical = false) = Tuple(angles),
ntuple(i -> 𝐞ˢ(Val(i), angles...; canonical = canonical), 3),
Basis(angles...)

"""
    rot3(θ, ϕ = 0, ψ = 0)

Returns a rotation matrix with respect to the 3 Euler angles `θ, ϕ, ψ`

# Examples
```julia
julia> cθ, cϕ, cψ, sθ, sϕ, sψ = symbols("cθ cϕ cψ sθ sϕ sψ", real = true) ;

julia> d = Dict(cos(θ) => cθ, cos(ϕ) => cϕ, cos(ψ) => cψ, sin(θ) => sθ, sin(ϕ) => sϕ, sin(ψ) => sψ) ;

julia> subs.(rot3(θ, ϕ, ψ),d...)
3×3 StaticArrays.SMatrix{3, 3, Sym, 9} with indices SOneTo(3)×SOneTo(3):
 cθ⋅cψ⋅cϕ - sψ⋅sϕ  -cθ⋅cϕ⋅sψ - cψ⋅sϕ  cϕ⋅sθ
 cθ⋅cψ⋅sϕ + cϕ⋅sψ  -cθ⋅sψ⋅sϕ + cψ⋅cϕ  sθ⋅sϕ
           -cψ⋅sθ              sθ⋅sψ     cθ
```
"""
rot3(θ, ϕ = 0, ψ = 0) = RotZYZ(ϕ, θ, ψ)

"""
    rot2(θ)

Returns a 2D rotation matrix with respect to the angle `θ`

# Examples
```julia
julia> rot2(θ)
2×2 Tensor{2, 2, Sym, 4}:
 cos(θ)  -sin(θ)
 sin(θ)   cos(θ)
```
"""
rot2(θ) = Tensor{2,2}((cos(θ), sin(θ), -sin(θ), cos(θ)))

"""
    rot6(θ, ϕ = 0, ψ = 0)

Returns a rotation matrix with respect to the 3 Euler angles `θ, ϕ, ψ`

# Examples
```julia
julia> cθ, cϕ, cψ, sθ, sϕ, sψ = symbols("cθ cϕ cψ sθ sϕ sψ", real = true) ;

julia> d = Dict(cos(θ) => cθ, cos(ϕ) => cϕ, cos(ψ) => cψ, sin(θ) => sθ, sin(ϕ) => sϕ, sin(ψ) => sψ) ;

julia> R = Tens(subs.(rot3(θ, ϕ, ψ),d...))
Tens.TensCanonical{2, 3, Sym, Tensor{2, 3, Sym, 9}}
# data: 3×3 Tensor{2, 3, Sym, 9}:
 cθ⋅cψ⋅cϕ - sψ⋅sϕ  -cθ⋅cϕ⋅sψ - cψ⋅sϕ  cϕ⋅sθ
 cθ⋅cψ⋅sϕ + cϕ⋅sψ  -cθ⋅sψ⋅sϕ + cψ⋅cϕ  sθ⋅sϕ
           -cψ⋅sθ              sθ⋅sψ     cθ
# var: (:cont, :cont)
# basis: 3×3 Tens.LazyIdentity{3, Sym}:
 1  0  0
 0  1  0
 0  0  1

julia> RR = R ⊠ˢ R
Tens.TensCanonical{4, 3, Sym, SymmetricTensor{4, 3, Sym, 36}}
# data: 6×6 Matrix{Sym}:
                          (cθ*cψ*cϕ - sψ*sϕ)^2                            (-cθ*cϕ*sψ - cψ*sϕ)^2           cϕ^2*sθ^2                      √2⋅cϕ⋅sθ⋅(-cθ⋅cϕ⋅sψ - cψ⋅sϕ)                     √2⋅cϕ⋅sθ⋅(cθ⋅cψ⋅cϕ - sψ⋅sϕ)                                   √2⋅(cθ⋅cψ⋅cϕ - sψ⋅sϕ)⋅(-cθ⋅cϕ⋅sψ - cψ⋅sϕ)
                          (cθ*cψ*sϕ + cϕ*sψ)^2                            (-cθ*sψ*sϕ + cψ*cϕ)^2           sθ^2*sϕ^2                      √2⋅sθ⋅sϕ⋅(-cθ⋅sψ⋅sϕ + cψ⋅cϕ)                     √2⋅sθ⋅sϕ⋅(cθ⋅cψ⋅sϕ + cϕ⋅sψ)                                   √2⋅(cθ⋅cψ⋅sϕ + cϕ⋅sψ)⋅(-cθ⋅sψ⋅sϕ + cψ⋅cϕ)
                                     cψ^2*sθ^2                                        sθ^2*sψ^2                cθ^2                                       √2⋅cθ⋅sθ⋅sψ                                    -√2⋅cθ⋅cψ⋅sθ                                                              -sqrt(2)*cψ*sθ^2*sψ
             -√2⋅cψ⋅sθ⋅(cθ⋅cψ⋅sϕ + cϕ⋅sψ)                √2⋅sθ⋅sψ⋅(-cθ⋅sψ⋅sϕ + cψ⋅cϕ)    √2⋅cθ⋅sθ⋅sϕ                    cθ*(-cθ*sψ*sϕ + cψ*cϕ) + sθ^2*sψ*sϕ                   cθ*(cθ*cψ*sϕ + cϕ*sψ) - cψ*sθ^2*sϕ                            -cψ⋅sθ⋅(-cθ⋅sψ⋅sϕ + cψ⋅cϕ) + sθ⋅sψ⋅(cθ⋅cψ⋅sϕ + cϕ⋅sψ)
             -√2⋅cψ⋅sθ⋅(cθ⋅cψ⋅cϕ - sψ⋅sϕ)                √2⋅sθ⋅sψ⋅(-cθ⋅cϕ⋅sψ - cψ⋅sϕ)    √2⋅cθ⋅cϕ⋅sθ                    cθ*(-cθ*cϕ*sψ - cψ*sϕ) + cϕ*sθ^2*sψ                   cθ*(cθ*cψ*cϕ - sψ*sϕ) - cψ*cϕ*sθ^2                            -cψ⋅sθ⋅(-cθ⋅cϕ⋅sψ - cψ⋅sϕ) + sθ⋅sψ⋅(cθ⋅cψ⋅cϕ - sψ⋅sϕ)
 √2⋅(cθ⋅cψ⋅cϕ - sψ⋅sϕ)⋅(cθ⋅cψ⋅sϕ + cϕ⋅sψ)  √2⋅(-cθ⋅cϕ⋅sψ - cψ⋅sϕ)⋅(-cθ⋅sψ⋅sϕ + cψ⋅cϕ)  sqrt(2)*cϕ*sθ^2*sϕ  cϕ⋅sθ⋅(-cθ⋅sψ⋅sϕ + cψ⋅cϕ) + sθ⋅sϕ⋅(-cθ⋅cϕ⋅sψ - cψ⋅sϕ)  cϕ⋅sθ⋅(cθ⋅cψ⋅sϕ + cϕ⋅sψ) + sθ⋅sϕ⋅(cθ⋅cψ⋅cϕ - sψ⋅sϕ)  (cθ*cψ*cϕ - sψ*sϕ)*(-cθ*sψ*sϕ + cψ*cϕ) + (cθ*cψ*sϕ + cϕ*sψ)*(-cθ*cϕ*sψ - cψ*sϕ)
# var: (:cont, :cont, :cont, :cont)
# basis: 3×3 Tens.LazyIdentity{3, Sym}:
 1  0  0
 0  1  0
 0  0  1

julia> R6 = invKM(subs.(KM(rot6(θ, ϕ, ψ)),d...))
Tens.TensCanonical{4, 3, Sym, SymmetricTensor{4, 3, Sym, 36}}
# data: 6×6 Matrix{Sym}:
                          (cθ*cψ*cϕ - sψ*sϕ)^2                            (-cθ*cϕ*sψ - cψ*sϕ)^2           cϕ^2*sθ^2                      √2⋅cϕ⋅sθ⋅(-cθ⋅cϕ⋅sψ - cψ⋅sϕ)                     √2⋅cϕ⋅sθ⋅(cθ⋅cψ⋅cϕ - sψ⋅sϕ)                                   √2⋅(cθ⋅cψ⋅cϕ - sψ⋅sϕ)⋅(-cθ⋅cϕ⋅sψ - cψ⋅sϕ)
                          (cθ*cψ*sϕ + cϕ*sψ)^2                            (-cθ*sψ*sϕ + cψ*cϕ)^2           sθ^2*sϕ^2                      √2⋅sθ⋅sϕ⋅(-cθ⋅sψ⋅sϕ + cψ⋅cϕ)                     √2⋅sθ⋅sϕ⋅(cθ⋅cψ⋅sϕ + cϕ⋅sψ)                                   √2⋅(cθ⋅cψ⋅sϕ + cϕ⋅sψ)⋅(-cθ⋅sψ⋅sϕ + cψ⋅cϕ)
                                     cψ^2*sθ^2                                        sθ^2*sψ^2                cθ^2                                       √2⋅cθ⋅sθ⋅sψ                                    -√2⋅cθ⋅cψ⋅sθ                                                              -sqrt(2)*cψ*sθ^2*sψ
             -√2⋅cψ⋅sθ⋅(cθ⋅cψ⋅sϕ + cϕ⋅sψ)                √2⋅sθ⋅sψ⋅(-cθ⋅sψ⋅sϕ + cψ⋅cϕ)    √2⋅cθ⋅sθ⋅sϕ                    cθ*(-cθ*sψ*sϕ + cψ*cϕ) + sθ^2*sψ*sϕ                   cθ*(cθ*cψ*sϕ + cϕ*sψ) - cψ*sθ^2*sϕ                            -cψ⋅sθ⋅(-cθ⋅sψ⋅sϕ + cψ⋅cϕ) + sθ⋅sψ⋅(cθ⋅cψ⋅sϕ + cϕ⋅sψ)
             -√2⋅cψ⋅sθ⋅(cθ⋅cψ⋅cϕ - sψ⋅sϕ)                √2⋅sθ⋅sψ⋅(-cθ⋅cϕ⋅sψ - cψ⋅sϕ)    √2⋅cθ⋅cϕ⋅sθ                    cθ*(-cθ*cϕ*sψ - cψ*sϕ) + cϕ*sθ^2*sψ                   cθ*(cθ*cψ*cϕ - sψ*sϕ) - cψ*cϕ*sθ^2                            -cψ⋅sθ⋅(-cθ⋅cϕ⋅sψ - cψ⋅sϕ) + sθ⋅sψ⋅(cθ⋅cψ⋅cϕ - sψ⋅sϕ)
 √2⋅(cθ⋅cψ⋅cϕ - sψ⋅sϕ)⋅(cθ⋅cψ⋅sϕ + cϕ⋅sψ)  √2⋅(-cθ⋅cϕ⋅sψ - cψ⋅sϕ)⋅(-cθ⋅sψ⋅sϕ + cψ⋅cϕ)  sqrt(2)*cde Liv Lehn ϕ*sθ^2*sϕ  cϕ⋅sθ⋅(-cθ⋅sψ⋅sϕ + cψ⋅cϕ) + sθ⋅sϕ⋅(-cθ⋅cϕ⋅sψ - cψ⋅sϕ)  cϕ⋅sθ⋅(cθ⋅cψ⋅sϕ + cϕ⋅sψ) + sθ⋅sϕ⋅(cθ⋅cψ⋅cϕ - sψ⋅sϕ)  (cθ*cψ*cϕ - sψ*sϕ)*(-cθ*sψ*sϕ + cψ*cϕ) + (cθ*cψ*sϕ + cϕ*sψ)*(-cθ*cϕ*sψ - cψ*sϕ)
# var: (:cont, :cont, :cont, :cont)
# basis: 3×3 Tens.LazyIdentity{3, Sym}:
 1  0  0
 0  1  0
 0  0  1

julia> R6 == RR
true
```
"""
function rot6(θ, ϕ = 0, ψ = 0)
    R = TensCanonical(rot3(θ, ϕ, ψ))
    return sboxtimes(R, R)
end

export LeviCivita
export 𝐞, 𝐞ᵖ, 𝐞ᶜ, 𝐞ˢ
export init_cartesian, init_polar, init_cylindrical, init_spherical, init_rotated
export rot2, rot3, rot6


module EqualizerFilters

import DSP.Biquad
import DSP.SecondOrderSections
using DSP

include("equalizerAPOstrings.jl")
include("DSPjlBiquads.jl")

export LP, HP, BP, NO, AP, PK, LS, HS, Biquad, eqAPOstring, SecondOrderSections

#default sampling rate
fs = 48e3

"""
    EqualizerFilters.sampling_rate(custom_sampling_rate)

Set the sampling rate for calculation of the biquads in Hz.

Defaults to 48 kHz.
"""
function sampling_rate(custom_sampling_rate)
    global fs = custom_sampling_rate
end

"""
    EqualizerFilters.sampling_rate()

Return the current sampling rate for biquad calculation in Hz.

Defaults to 48 kHz.
"""
function sampling_rate()
    fs
end

"""
    biquadPrecalculations(dbGain, freq, srate, Q)

Returns `(A, sn, cs, alpha, beta)` that have to be calculated for all
filter types.

The parameter A is not needed for gain-less filters.
The calling functions simply pass 0 dB Gain and do not use the returned parameter
`A`.
"""
function biquadPrecalculations(dbGain, freq, srate, Q)
    A = 10^(dbGain/40)
    omega = 2pi * freq / srate
    sn = sin(omega)
    cs = cos(omega)
    alpha = sn / (2 * Q)
    beta = 2 * sqrt(A) * alpha
    return A, sn, cs, alpha, beta
end

"""
    LP(f, Q, fs=sampling_rate)

Return lowpass biquad at frequency `f` in Hz with quality factor `Q`.

This second order lowpass falls of with 12 dB/oct.

If not given in the arguments, the sampling rate defaults to 48 kHz or can be
set with [`EqualizerFilters.sampling_rate`](@ref).
"""
function LP(f, Q, fs=fs)
    A, sn, cs, alpha, beta = biquadPrecalculations(0, f, fs, Q)
    b0 = (1 - cs) / 2
    b1 = 1 - cs
    b2 = (1 - cs) / 2
    a0 = 1 + alpha
    a1 = -2 * cs
    a2 = 1 - alpha
    biquadCoefs = [b0, b1, b2, a1, a2]./a0
    return Biquad(biquadCoefs...)
end

"""
    HP(f, Q, fs=sampling_rate)

Return highpass biquad at frequency `f` in Hz with quality factor `Q`.

This second order highpass falls of with 12 dB/oct.

If not given in the arguments, the sampling rate defaults to 48 kHz or can be
set with [`EqualizerFilters.sampling_rate`](@ref).
"""
function HP(f, Q, fs=fs)
    A, sn, cs, alpha, beta = biquadPrecalculations(0, f, fs, Q)
    b0 = (1 + cs) / 2;
    b1 = -(1 + cs);
    b2 = (1 + cs) / 2;
    a0 = 1 + alpha;
    a1 = -2 * cs;
    a2 = 1 - alpha;
    biquadCoefs = [b0, b1, b2, a1, a2]./a0
    return Biquad(biquadCoefs...)
end

"""
    BP(f, Q, fs=sampling_rate)

Return bandpass biquad at frequency `f` in Hz with quality factor `Q`.

If not given in the arguments, the sampling rate defaults to 48 kHz or can be
set with [`EqualizerFilters.sampling_rate`](@ref).
"""
function BP(f, Q, fs=fs)
    A, sn, cs, alpha, beta = biquadPrecalculations(0, f, fs, Q)
    b0 = alpha;
    b1 = 0;
    b2 = -alpha;
    a0 = 1 + alpha;
    a1 = -2 * cs;
    a2 = 1 - alpha;
    biquadCoefs = [b0, b1, b2, a1, a2]./a0
    return Biquad(biquadCoefs...)
end

"""
    NO(f, Q, fs=sampling_rate)

Return notch biquad at frequency `f` in Hz with quality factor `Q`.

If not given in the arguments, the sampling rate defaults to 48 kHz or can be
set with [`EqualizerFilters.sampling_rate`](@ref).
"""
function NO(f, Q, fs=fs)
    A, sn, cs, alpha, beta = biquadPrecalculations(0, f, fs, Q)
    b0 = 1;
    b1 = -2 * cs;
    b2 = 1;
    a0 = 1 + alpha;
    a1 = -2 * cs;
    a2 = 1 - alpha;
    biquadCoefs = [b0, b1, b2, a1, a2]./a0
    return Biquad(biquadCoefs...)
end

"""
    AP(f, Q, fs=sampling_rate)

Return allpass biquad at frequency `f` in Hz with quality factor `Q`.

This second order allpass rotates the phase by a total of ``2π``.

If not given in the arguments, the sampling rate defaults to 48 kHz or can be
set with [`EqualizerFilters.sampling_rate`](@ref).
"""
function AP(f, Q, fs=fs)
    A, sn, cs, alpha, beta = biquadPrecalculations(0, f, fs, Q)
    b0 = 1 - alpha;
    b1 = -2 * cs;
    b2 = 1 + alpha;
    a0 = 1 + alpha;
    a1 = -2 * cs;
    a2 = 1 - alpha;
    biquadCoefs = [b0, b1, b2, a1, a2]./a0
    return Biquad(biquadCoefs...)
end

"""
    PK(f, dbGain, Q, fs=sampling_rate)

Return peaking biquad with Gain `dbGain` in dB at frequency `f` in Hz
with quality factor `Q`.

In this implementation the Gain at 0 Hz and at the Nyquist frequency will
always be 0 dB. For an Orfanidis-like peak EQ with analog-inspired
Nyquist-gain see the package LakeBiquads.jl.

If not given in the arguments, the sampling rate defaults to 48 kHz or can be
set with [`EqualizerFilters.sampling_rate`](@ref).
"""
function PK(f, dbGain, Q, fs=fs)
    A, sn, cs, alpha, beta = biquadPrecalculations(dbGain, f, fs, Q)
    b0 = 1 + (alpha * A);
    b1 = -2 * cs;
    b2 = 1 - (alpha * A);
    a0 = 1 + (alpha / A);
    a1 = -2 * cs;
    a2 = 1 - (alpha / A);
    biquadCoefs = [b0, b1, b2, a1, a2]./a0
    return Biquad(biquadCoefs...)
end

"""
    LS(f, dbGain, Q, fs=sampling_rate)

Return low shelf biquad with Gain `dbGain` in dB, center frequency `f` in Hz and
quality factor `Q`.

If not given in the arguments, the sampling rate defaults to 48 kHz or can be
set with [`EqualizerFilters.sampling_rate`](@ref).
"""
function LS(f, dbGain, Q, fs=fs)
    A, sn, cs, alpha, beta = biquadPrecalculations(dbGain, f, fs, Q)
    b0 = A * ((A + 1) - (A - 1) * cs + beta);
    b1 = 2 * A * ((A - 1) - (A + 1) * cs);
    b2 = A * ((A + 1) - (A - 1) * cs - beta);
    a0 = (A + 1) + (A - 1) * cs + beta;
    a1 = -2 * ((A - 1) + (A + 1) * cs);
    a2 = (A + 1) + (A - 1) * cs - beta;
    biquadCoefs = [b0, b1, b2, a1, a2]./a0
    return Biquad(biquadCoefs...)
end

"""
    HS(f, dbGain, Q, fs=sampling_rate)

Return high shelf biquad with Gain `dbGain` in dB, center frequency `f` in Hz and
quality factor `Q`.

If not given in the arguments, the sampling rate defaults to 48 kHz or can be
set with [`EqualizerFilters.sampling_rate`](@ref).
"""
function HS(f, dbGain, Q, fs=fs)
    A, sn, cs, alpha, beta = biquadPrecalculations(dbGain, f, fs, Q)
    b0 = A * ((A + 1) + (A - 1) * cs + beta);
    b1 = -2 * A * ((A - 1) + (A + 1) * cs);
    b2 = A * ((A + 1) + (A - 1) * cs - beta);
    a0 = (A + 1) - (A - 1) * cs + beta;
    a1 = 2 * ((A - 1) - (A + 1) * cs);
    a2 = (A + 1) - (A - 1) * cs - beta;
    biquadCoefs = [b0, b1, b2, a1, a2]./a0
    return Biquad(biquadCoefs...)
end

end # module


function ACQR_bench(bench,nt,seed=1)
	#
	# benchmark models to test the functions
	#

	# random seed for the simulations
	srand(seed)

	if bench==1
		#
		A = 0.6
		C = 1
		Q = 2
		R = 4

		v = sqrt(R)*randn(nt)
		w = sqrt(Q)*randn(nt)

		x = zeros(nt)
		for t in 1:nt-1
			x[t+1] = A*x[t] + w[t]
		end
		y = C*x + v

		salida = y,A,C,Q,R

	elseif bench==2
		#
		# Based on:
		# "Subspace Identification for Linear Systems"
		# P. Van Overschee and B. De Moor, page 81
		#
		A = [0.6 0.6 0.;-0.6 0.6 0.;0. 0. 0.4]
		C = [0.7831 0.5351 0.9701]
		R = 6.0
		Q = [3 0.5 0.2;0.5 2 0.3;0.2 0.3 1]
		U = chol(Q)

		v = sqrt(R)*randn(1,nt)
		ns = size(A,1)
		w = U'*randn(ns,nt)

 		x = zeros(ns,nt)
		x[:,1] = w[:,1]
		for t in 1:nt-1
			x[:,t+1] = A*x[:,t] + w[:,t]
		end
		y = C*x + v

		salida = y,A,C,Q,R

	end

	return salida

end


# Atkinson.jl

###### atkinson #####
"""
    atkinson(v, ϵ)

Compute the Atkinson Index of a vector `v` at a specified inequality
aversion parameter `ϵ`.

# Examples
```julia
julia> using Inequality
julia> atkinson([8, 5, 1, 3, 5, 6, 7, 6, 3], 1.2)
0.1631765870035865
```
"""
function atkinson(v::AbstractVector{<:Real}, ϵ::Real)::Float64

    ϵ >= 0 ? nothing : throw(ArgumentError("`ϵ` must be larger or equal than 0"))

    if ϵ == 1
        v = v[v .!= 0]
        return 1 - (exp.(mean(log.(v)))/mean(v))
    elseif ϵ < 1
        return 1 - mean( (v./mean(v)).^(1-ϵ) ).^(1/(1-ϵ))
    else
        v = v[v .!= 0]
        return 1 - mean( (v./mean(v)).^(1-ϵ) ).^(1/(1-ϵ))
    end
end

###### weighted atkinson #####
"""
    atkinson(v, w, ϵ)

Compute the weighted Atkinson Index of a vector `v` at a specified inequality
aversion parameter `ϵ`, using weights given by a weight vector `w`.

Weights must not be negative, missing or NaN. The weights and data vectors must have the same length.

# Examples
```julia
julia> using Inequality
julia> atkinson([8, 5, 1, 3], [0.1,0.5,0.3,0.8], 1.2)
0.1681319821792493
```
"""
function atkinson(v::AbstractVector{<:Real}, w::AbstractVector{<:Real}, ϵ::Real)::Float64

    ϵ >= 0 ? nothing : throw(ArgumentError("`ϵ` must be larger or equal than 0"))
    checks_weights(v, w)

    v = v/Statistics.mean(v)
    w = w/sum(w)

    if ϵ == 1
        w = w[v .!= 0]
        v = v[v .!= 0]

        return 1 - (prod(exp.(w .* log.(v)))/sum(v .* w) )
    elseif ϵ < 1
        return 1-(sum(((v/sum(v.* w)).^(1-ϵ)).*w)).^(1/(1-ϵ))
    else
        w = w[v .!= 0]
        v = v[v .!= 0]

        return 1-(sum(((v/sum(v.* w)).^(1-ϵ)).*w)).^(1/(1-ϵ))
    end
end

function atkinson(v::AbstractVector{<:Real}, w::AbstractWeights, ϵ::Real)::Float64

    ϵ >= 0 ? nothing : throw(ArgumentError("`ϵ` must be larger or equal than 0"))
    checks_weights(v, w)

    v = v/Statistics.mean(v)
    w = w/w.sum

    if ϵ == 1
        w = w[v .!= 0]
        v = v[v .!= 0]

        return 1 - (prod(exp.(w .* log.(v)))/sum(v .* w) )
    elseif ϵ < 1
        return 1-(sum(((v/sum(v.* w)).^(1-ϵ)).*w)).^(1/(1-ϵ))
    else
        w = w[v .!= 0]
        v = v[v .!= 0]

        return 1-(sum(((v/sum(v.* w)).^(1-ϵ)).*w)).^(1/(1-ϵ))
    end
end

"""
    watkinson(v, w, p)

Compute the atkinson index of `v` with weights `w` and inequality aversion parameter 'ϵ'. See also [`atkinson`](@atkinson)
"""
watkinson(v::AbstractVector{<:Real}, w::AbstractVector{<:Real}, ϵ::Real) = atkinson(v, w, ϵ)

watkinson(v::AbstractVector{<:Real}, w::AbstractWeights, ϵ::Real) = atkinson(v, w, ϵ)