# 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 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


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


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


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


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


# 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, ϵ)


###
#   Set operations
###

# Envolpe/hull. Keeps it as a IntervalUnion
env(x :: IntervalU) = IntervalU([hull(x.v)])
hull(x :: IntervalU) = IntervalU([hull(x.v)])

# Computes the complement of a IntervalUnion
function complement(x :: IntervalU)

    v = sort(x.v)
    vLo = left.(v)
    vHi = right.(v)

    vLo[1] == -∞ ? popfirst!(vLo) : pushfirst!(vHi, -∞)
    vHi[end] == ∞ ? pop!(vHi) : push!(vLo, ∞)

    complements = interval.(vHi,vLo)

    return intervalU(complements)
end

complement(x :: Interval) = complement(intervalU(x))

# bisect x at α
function bisect( x :: IntervalU, α = 0)

    v = deepcopy(x.v)

    αIn = α .∈ v
    these = findall(αIn .== 1)

    if isempty(these); return x ; end

    this = popat!(v, these[1])

    β = (α - this.lo) /(this.hi - this.lo)

    bs = bisect(this, β)

    new = IntervalU(v ∪ bs[1] ∪ bs[2])
    new = remove_empties(new)
    sort!(new.v)
    return new

end

function intersect(x :: IntervalU, y :: IntervalU)
    intersects = [intersect(xv, yv) for xv in x.v, yv in y.v]
    return intervalU(intersects[:])
end

function intersect(x :: IntervalU, y :: Interval)
    intersects = [intersect(xv, y) for xv in x.v]
    return intervalU(intersects[:])
end

function intersect(x :: Interval, y :: IntervalU)
    intersects = [intersect(x, yv) for yv in y.v]
    return intervalU(intersects[:])
end

function setdiff(x :: IntervalU, y :: IntervalU)
    yc = complement(y)
    return intersect(x, yc)
end

\(x :: IntervalU, y :: IntervalU) = setdiff(x, y)
\(x :: Interval, y :: IntervalU) = setdiff(intervalU(x), y)
\(x :: IntervalU, y :: Interval) = setdiff(x, intervalU(y))

function ⊂(x :: IntervalU, y :: IntervalU)
    issubs = [xv .⊂ y.v for xv in x.v]
    return all(any.(issubs))
end

function ⊂(x :: Interval, y :: IntervalU)
    if length(y.v) == 1; return x ⊂ y; end
    issubs = x .⊆ y.v
    return any(issubs)
end

function ⊂( x::IntervalU, y :: Interval)
    issubs = x.v .⊂ y
    return all(issubs)
end

function ⊆(x :: IntervalU, y :: IntervalU)
    issubs = [xv .⊆ y.v for xv in x.v]
    return all(any.(issubs))
end

function ⊆(x :: Interval, y :: IntervalU)
    issubs = x .⊆ y.v
    return any(issubs)
end

function ⊆( x::IntervalU, y :: Interval)
    issubs = x.v .⊆ y
    return all(issubs)
end


## PART 1

println("Part 1");

# Define functions for this part
function f1(v̄)
    return v̄[:,1] ./ v̄[:,2];
end

function f2(v̄)
    return exp( v̄[:,3] - v̄[:,4] );
end

function f3(v̄)
    logs = Float64[];
    for vv in v̄[:,5]
        push!(logs,log(vv));
    end
    return ( v̄[:,1]./v̄[:,2] + v̄[:,3]./v̄[:,4] ) .* logs;
end

# Calculate f1, f2, and f3 for these means
println("RESUMING QUESTION 2");
println("\n\tCalculating functions of means f1v1, f2v1, ...");
f1N1 = f1(v̄1);
f2N1 = f2(v̄1);
f3N1 = f3(v̄1);

f1N2 = f1(v̄2);
f2N2 = f2(v̄2);
f3N2 = f3(v̄2);

# Find standard deviations
println("\n\tCalculating standard deviations of the functions");
σ̂truef1N1 = std(f1N1);
σ̂truef2N1 = std(f2N1);
σ̂truef3N1 = std(f3N1);

σ̂truef1N2 = std(f1N2);
σ̂truef2N2 = std(f2N2);
σ̂truef3N2 = std(f3N2);


function chebyshev_polynomial_derivative(order::S,x::T) where {T<:AbstractFloat,S<:Integer}

  polynomial    = Array{T}(undef,1,order+1)
  poly_deriv    = Array{T}(undef,1,order+1)
  polynomial[1] = one(T)
  poly_deriv[1] = zero(T)

  for i = 2:order+1
    if i == 2
      polynomial[i] = x
      poly_deriv[i] = one(T)
    else
      polynomial[i] = 2*x*polynomial[i-1]-polynomial[i-2]
      poly_deriv[i] = 2*polynomial[i-1]+2*x*poly_deriv[i-1]-poly_deriv[i-2]
    end
  end

  return poly_deriv

end

function chebyshev_polynomial_derivative(order::S,x::Array{T,1}) where {T<:AbstractFloat,S<:Integer}

  poly_deriv = Array{T,2}(undef,length(x),order+1)

  for i = 1:length(x)
    poly_deriv[i,:] = chebyshev_polynomial_derivative(order,x[i])
  end

  return poly_deriv

end


import RationalApproximations: aaa_v1, aaa_v2, locate_extrema_idx
import RationalApproximations: RationalFunc, equioscillation!, minimax
using PyPlot

f = exp
S = range(-1, 1, length=201)
F = f.(S)
tol = 1e-6
max_m = 5
r, err = aaa_v1(F, S, max_m, tol)
m = length(r)

figure(1)
supp_pt = r[m].supp_pt
resid = F - r[m].(S)
plot(S, resid, supp_pt, f.(supp_pt)-r[m].(supp_pt), "o")
legend((L"$f-r$", "support points"))
title("Result using aaa_v1 (m = $m)")
grid(true)
xylims = axis()

figure(2)
r, err = aaa_v2(F, S, max_m, tol)
m = length(r)
supp_pt = r[m].supp_pt
resid = F - r[m].(S)
plot(S, resid, supp_pt, f.(supp_pt)-r[m].(supp_pt), "o")
legend((L"$f-r$", "support points"))
title("Result using aaa_v2 (m = $m)")
grid(true)
axis(xylims)

idx = locate_extrema_idx(resid, m)
x = S[idx]
figure(3)
plot(S, resid, x, f.(x)-r[m].(x), "o")
legend((L"$f-r$", "extrema"))
title("Locate extrema")
grid(true)
axis(xylims)

clamp = :no
α, β, λ = equioscillation!(f, x, supp_pt, clamp)
new_r = RationalFunc(α, β, supp_pt)
figure(4)
plot(S, f.(S) - new_r.(S),
     [-1.0, 1.0, NaN, -1.0, 1.0], [λ, λ, NaN, -λ, -λ], ":k",
     x, f.(x)-new_r.(x), "o")
grid(true)
title("Equioscillation procedure")
axis(xylims)

max_iterations = 4
x = S[idx]
r, zmin, zmax = minimax(f, x, supp_pt, (-1.0,1.0), max_iterations, clamp)
m = length(r)
figure(5)
plot(S, f.(S) - r[m].(S), x, f.(x) - r[m].(x), "o")
grid(true)
title("Result after $m Remez iterations")
axis(xylims)


module BigCombinatorics

using Combinatorics

export Fibonacci
export Factorial, DoubleFactorial, FallingFactorial, RisingFactorial
export Binomial, Catalan
export Derangements, MultiChoose, Multinomial
export Bell, Stirling1, Stirling2
export IntPartitions, IntPartitionsDistinct
export Euler, PowerSum

_master_table = Dict{Function,Dict}()

function _save(f::Function, x, val::BigInt)
    d = _master_table[f]
    d[x] = val
    nothing
end

function _has(f::Function, x)::Bool
    d = _master_table[f]
    return haskey(d,x)
end

function _get(f::Function, x)::BigInt
    d = _master_table[f]
    return d[x]
end

function _make(f::Function, T::Type)
    _master_table[f] = Dict{T,BigInt}()
    nothing
end

"""
`BigCombinatorics.cache_report()` reports how many
entries are saved for each function in the `BigCombinatorics`
module.
"""
function cache_report()
    total = 0
    for func in keys(_master_table)
        parse_func = split(string(func),".")
        func_name = last(parse_func)
        sz = length(_master_table[func])
        total += sz
        println("$sz\t$func_name")
    end
    println()
    println("$total\tTotal entries")
    nothing
end

"""
`BigCombinatorics.cache_clear(f)` clears the cached values for
the function `f`. Returns `false` if `f` values are not cached.

`BigCombinatorics.cache_clear()` clears all cached values.
"""
function cache_clear(f::Function)::Bool
    if !haskey(_master_table,f)
        return false
    end
    tab = _master_table[f]
    for k in keys(tab)
        delete!(tab,k)
    end
    return true
end

function cache_clear()
    for f in keys(_master_table)
        cache_clear(f)
    end
    true
end

"""
`Fibonacci(n)` returns the `n`-th Fibonacci number.
We begin with `Fibonacci(0)==0` and `Fibonacci(1)==1`.
"""
function Fibonacci(n::Integer)::BigInt
  if n<0
    throw(DomainError(n,"argument must be nonngative"))
  end
  if n==0
    return big(0)
  end
  if n==1
    return big(1)
  end
  if _has(Fibonacci,n)
      return _get(Fibonacci,n)
  end
  val = Fibonacci(n-1)+Fibonacci(n-2)
  _save(Fibonacci,n,val)
  return val
end

_make(Fibonacci,Integer)

"""
`Factorial(n)` returns `n!` for nonnegative integers `n`.
`Factorial(n,k)` returns `n!/k!` (to be consistent with Julia's
`factorial`.) Requires `0 <= k <= n`.

See also `FallingFactorial` and `RisingFactorial`.
"""
function Factorial(n::Integer)::BigInt
    if n < 0
        throw(DomainError(n,"argument must be nonngative"))
    end
    if n <= 1
        return big(1)
    end
    if _has(Factorial,n)
        return _get(Factorial,n)
    end
    val = big(n) * Factorial(n-1)
    _save(Factorial,n,val)
    return val
end

function Factorial(n::Integer,k::Integer)::BigInt
    if k>n
        throw(DomainError((n,k),"$k cannot exceed $n"))
    end
    return div(Factorial(n),Factorial(k))
end
_make(Factorial,Integer)

"""
`FallingFactorial(n,k)` returns `n*(n-1)*(n-2)*...*(n-k+1)`
(with a total of `k` factors). Requires `n,k >= 0`.
If `k>n` then `0` is returned.
"""
function FallingFactorial(n::Integer, k::Integer)::BigInt
  if n<0 || k<0
    throw(DomainError((n,k)," arguments must be nonnegative"))
  end
  if k>n
    return big(0)
  end
  return Factorial(n,n-k)
end

"""
`RisingFactorial(n,k)` returns `n*(n+1)*(n+2)*...*(n+k-1)`
(with a total of `k` factors). Requires `n,k >= 0`.
"""
function RisingFactorial(n::Integer,k::Integer)::BigInt
  if n<0 || k<0
    throw(DomainError((n,k),"arguments must be nonnegative"))
  end
  if k==0
    return big(1)
  end
  if n==0
    return big(0)
  end

  return FallingFactorial(n+k-1,k)
end

"""
`DoubleFactorial(n)` returns `n!!`, i.e.,
`n*(n-2)*...` with `(-1)!! == 0!! == 1!! == 1`.
"""
function DoubleFactorial(n::Integer)::BigInt
  if n<-1
    throw(DomainError(n,"argument must be at least -1"))
  end
  if n<2
    return big(1)
  end
  if _has(DoubleFactorial,n)
      return _get(DoubleFactorial,n)
  end
  val = n * DoubleFactorial(n-2)
  _save(DoubleFactorial,n,val)
  return val
end
_make(DoubleFactorial,Integer)

"""
`Binomial(n,k)` returns the binomial coefficient `n`-choose-`k`.
This is the number of `k`-element subsets of an `n`-element set.
"""
Binomial(n::Integer, k::Integer) = binomial(big(n),big(k))::BigInt

"""
`Multinomial(vec)` returns the multinomial coefficient whose
top index is the sum of `vec` (an array of `Int`s) and whose
bottom indices are given by `vec`.

This may also be called with a common-separated list of arguments,
that is, either of `Multinomial([1,2,3])` or `Multinomial(1,2,3)`.
The result is `60` in both cases as these equal `6!/(1! 2! 3!)`.

**Warning**: This is not the same as `MultiChoose`.
"""
function Multinomial(v...)::BigInt
  nv = length(v)
  for i=1:nv
    typeof(v[i])<:Integer || throw(DomainError(v,"arguments must be integers"))
    v[i]>=0 || throw(DomainError(v,"arguments must be nonngative"))
  end
  vals = [t for t in v]
  return Multinomial(vals)
end

Multinomial() = big(1)::BigInt

function Multinomial(vals::Vector{T})::BigInt where T<:Integer
  if any([t<0 for t in vals])
    throw(DomainError(vals,"arguments must be nonnegative"))
  end

  nv = length(vals)
  n  = sum(vals)
  # base cases
  if nv<=1 || n==0
    return big(1)
  end
  # reduce
  return Binomial(n,vals[end]) * Multinomial(vals[1:nv-1])
end

"""
`MultiChoose(n,k)` returns the number of `k`-element
*multisets* that can be formed using the elements of an
`n`-element set.

**Warning**: This is not the same as `Multinomial`.
"""
function MultiChoose(n::Integer,k::Integer)::BigInt
  return Binomial(n+k-1,k)
end

"""
`Catalan(n)` returns the `n`-th Catalan number.
"""
function Catalan(n::Integer)::BigInt
  n >= 0 || throw(DomainError(n,"argument must be nonnegative"))
  return div(Binomial(2n,n),n+1)
end

"""
`Derangements(n)` returns the number of permutations of
an `n`-set that have no fixed point.
"""
function Derangements(n::Integer)::BigInt
  if n<0
    throw(DomainError(n,"argument must be nonnegative"))
  end
  if n==0
    return big(1)
  end
  if n==1
    return big(0)
  end
  if _has(Derangements,n)
      return _get(Derangements,n)
  end

  val = (n-1)*(Derangements(n-1)+Derangements(n-2))
  _save(Derangements,n,val)
  return val
end

_make(Derangements,Integer)

"""
`Bell(n)` gives the `n`-th Bell number, that is,
the number of partitions of an `n`-element set.
"""
function Bell(n::Integer)::BigInt
  if n<0
    throw(DomainError(n,"argument must be nonnegative"))
  end
  if n==1 || n==0
    return big(1)
  end
  if _has(Bell,n)
      return _get(Bell,n)
  end
  N1 = n-1
  result = big(0)
  for k=0:n-1
    result += Binomial(n-1,k) * Bell(k)
  end
  _save(Bell,n,result)
  return result
end
_make(Bell,Integer)

"""
`Stirling2(n,k)` gives the Stirling number of the second kind,
that is, the number of paritions of an `n`-set into `k`-parts."
"""
function Stirling2(n::Integer,k::Integer)::BigInt
  # special cases
  if k<0 || n<0
    throw(DomainError((n,k),"arguments must be nonnegative"))
  end

  if k>n
    return big(0)
  end

  if n==0  # and by logic, k==0
    return big(1)
  end

  if k==0
    return big(0)
  end

  if n==k
    return big(1)
  end
  # END OF SPECIAL CASES, invoke recursion
  if _has(Stirling2,(n,k))
      return _get(Stirling2,(n,k))
  end

  val = Stirling2(n-1,k-1) + Stirling2(n-1,k)*k
  _save(Stirling2,(n,k),val)
  return val
end
_make(Stirling2,Tuple{Integer,Integer})

"""
`Stirling1(n,k)` gives the (signed) Stirling number
of the first kind, that is, the coefficient of `x^k`
in the poynomial `x(x-1)(x-2)...(x-n+1)`.
"""
function Stirling1(n::Integer,k::Integer)::BigInt
  # special cases
  if k<0 || n<0
    throw(DomainError((n,k),"arguments must be nonnegative"))
  end

  if k>n
    return big(0)
  end

  if n==0  # and, by logic, k==0
    return big(1)
  end

  if k==0  # and, by logic, n>0
    return big(0)
  end

  if _has(Stirling1,(n,k))
      return _get(Stirling1,(n,k))
  end

  # end of special cases, invoke recursion

  val = Stirling1(n-1,k-1) - (n-1)*Stirling1(n-1,k)
  _save(Stirling1,(n,k),val)
  return val
end

_make(Stirling1,Tuple{Integer,Integer})

"""
`IntPartitions(n)` is the number of partitions of the integer `n`.

`IntPartitions(n,k)` is the number of partitions of the integer
`n` with exactly `k` (nonzero) parts.
"""
function IntPartitions(n::Integer,k::Integer)::BigInt
  if n<0 || k<0
    throw(DomainError((n,k),"arguments must be nonnegative"))
  end
  # lots of special cases
  if k>n
    return big(0)
  end
  if n==0
    return big(1)
  end
  if k==0
    return big(0)
  end
  if k==n || k==1
    return big(1)
  end
  if _has(IntPartitions,(n,k))
      return _get(IntPartitions,(n,k))
  end

  val =  sum([IntPartitions(n-k,i) for i=0:k])
  _save(IntPartitions,(n,k),val)
  return val
end

function IntPartitions(n::Integer)::BigInt
    if n<0
        throw(DomainError(n,"argument must be nonnegative"))
    end
    if _has(IntPartitions,n)
        return _get(IntPartitions,n)
    end
    val = sum([IntPartitions(n,k) for k=0:n])
    _save(IntPartitions,n,val)
    return val
end

_make(IntPartitions,Union{Tuple{Integer,Integer},Integer})

"""
`IntPartitionsDistinct(n,k)` is the number of partitions of
the integer `n` into exactly `k` *distinct* parts.

`IntPartitionsDistinct(n)` is the number of partitions of `n`
into *distinct* parts.
"""
function IntPartitionsDistinct(n::Integer,k::Integer)::BigInt
  if n<0 || k<0
    throw(DomainError((n,k),"arguments must be nonnegative"))
  end
  Ck2 = div(k*(k-1),2)
  if n < Ck2
    return big(0)
  end
  return IntPartitions(n-Ck2,k)
end

function IntPartitionsDistinct(n::Integer)::BigInt
  if n<0
    throw(DomainError(n,"argument must be nonngative"))
  end
  result = big(0)
  for k=1:n
    s = IntPartitionsDistinct(n,k)
    if s==0
      break
    end
    result += s
  end
  return result
end

"""
`Euler(n)` returns the `n`-th Euler number. Starting with `n=0`
this is the sequence
1, 0, -1, 0, 5, 0, -61, 0, 1385 and so on.
"""
function Euler(n::Integer)::BigInt
  n>=0 || throw(DomainError(n,"argument must be nonngative"))
  if n%2 == 1
    return big(0)
  end
  if n==0
    return big(1)
  end
  if _has(Euler,n)
      return _get(Euler,n)
  end

  last = div(n,2)-1
  val =  -sum([ Binomial(n,2k)*Euler(2k) for k=0:last])
  _save(Euler,n,val)
  return val
end
_make(Euler,Integer)

"""
`PowerSum(n,k)` returns the sum of the `k`-th powers of the
integers `1` through `n`, i.e.,
`1^k + 2^k + 3^k + ... + n^k`.
"""
function PowerSum(n::Integer, k::Integer)::BigInt
  (n>=0 && k>=0) || throw(DomainError((n,k),"arguments must be nonngative"))
  # Base and special cases
  if n==0
    return big(0)
  end
  if k==0
    return big(n)
  end
  if k==1
    return Binomial(n,2)
  end
  if _has(PowerSum,(n,k))
      return _get(PowerSum,(n,k))
  end

  val =  big(n)^k + PowerSum(n-1,k)
  _save(PowerSum,(n,k),val)
  return val
end
_make(PowerSum,Tuple{Integer,Integer})

include("eulerian.jl")

end  #end of module


# Undergraduate Student: Arturo Burgos
# Professor: João Rodrigo Andrade
# Federal University of Uberlândia - UFU, Fluid Mechanics Laboratory - MFLab, Block 5P, Uberlândia, MG, Brazil

# Fourth exercise: Solving a Linear System --> ax = b

using Printf # Just the same as import a module
using Statistics
using LinearAlgebra # Import this module to use the norm function
using CPUTime

n = 324
k = Int32(sqrt(324))

# Here I set the A matrix

a = rand([0,0],(n,n))

for i = 1:n
  for j = 1:n
    if i==j

      a[i,j] = -4

      elseif i == (j-3) || i == (j+3)

        a[i,j] = 1

      elseif (mod(i,3) != 0 && j == (i+1))

        a[i,j] = 1

      elseif (mod(i,3) != 1 && j == (i-1))

        a[i,j] = 1

      end
  end
end

# Here I set the b matrix

b = ones(n)

for i = 1:k
  if  i < k

    b[i] = -50

  else

    b[i] = -150

  end
end

for i = k+1:n-k
  if i%k != 0

    b[i] = 0

  else

    b[i] = -100

  end
end

for i = n-k+1:n
  if  i < n

    b[i] = -50

  else

    b[i] = -150

  end
end

println("\n")

println("The B matrix is: ")
println(b)

R = zeros(n)

function  linearsystem(a,b,x_k1)
  # Initial x_k and x_k1 value
  x_k = zeros(n)

  x_k1 = ones(n)

  # Here I set the tolerance

  e = 1e-9

  # Here I set the iterations
  ite = 0

  # Here I set the error based in the Infinite norm
  #erro = (x_k1 - x_k)/x_k1; # ---> Why the relative error has the same result?
  global erro = norm((x_k1 - x_k),Inf)

  while erro>e
    for i = 1:n

      global x_k1[i] = b[i]

      for j = 1:n
        if j != i

          global x_k1[i] =  x_k1[i] - a[i,j]*x_k[j]

        end
      end

      global x_k1[i] = x_k1[i]/a[i,i]

    end

    global erro = norm((x_k1 - x_k),Inf)
    global x_k[:] = x_k1[:]

    ite = ite + 1

  end

  return x_k1

end

R = linearsystem(a,b,R);
println("\n\nThe R Matrix is: \n\n",R)
println("\n\n")

@time @CPUtime linearsystem(a,b,R)


function variable_degrees{T<:Real}(num_variables::Integer, poly_degrees::AbstractVector{T})
    max_poly_degree = maximum(poly_degrees)
    poly_degrees = Set(poly_degrees)
    all_variable_degrees = Vector{Int}[]
    var_degrees = zeros(Int, num_variables)
    total = 0
    while var_degrees[1] <= max_poly_degree
        if in(total, poly_degrees)
            push!(all_variable_degrees, copy(var_degrees))
        end
        var_degrees[end] += 1
        total += 1
        for j = length(var_degrees):-1:2
            if var_degrees[j] > max_poly_degree || total > max_poly_degree
                total -= var_degrees[j] - 1
                var_degrees[j] = 0
                var_degrees[j-1] += 1
            else
                break
            end
        end
    end
    return all_variable_degrees
end

function monomial(variable::Symbol, degree::Integer)
    MPoly{Float64}(OrderedDict([degree]=>1.0), [variable])
end

function chebyshev_polynomial_first_kind(variable::Symbol, degree::Integer)
    x = generator(variable)
    @assert degree >= 0
    if degree == 0
        return 1 + 0 * x
    elseif degree == 1
        return x
    else
        return (2x * chebyshev_polynomial_first_kind(variable, degree - 1)
                - chebyshev_polynomial_first_kind(variable, degree - 2))
    end
end

function polynomial_basis(variables, degrees=0:2, generator::Function=monomial)
    map(d -> prod(map(i -> generator(variables[i], d[i]),
                  1:length(variables))),
        variable_degrees(length(variables), degrees))
end

monomials(variables, degrees=0:2) = polynomial_basis(variables, degrees, monomial)
chebyshev_basis_first_kind(variables, degrees=0:2) = polynomial_basis(variables, degrees, chebyshev_polynomial_first_kind)


# -*- coding: utf-8 -*-
# $$
# \frac{d f}{dt} +  (v \frac{d f}{dx} - x \frac{d f}{dv}) = 0
# $$

# $$
# x \in [-\pi, \pi],\qquad y \in [-\pi, \pi] \qquad \mbox{ and } \qquad t \in [0, 200\pi]
# $$

using  FFTW
using  LinearAlgebra
using  Plots, ProgressMeter
using  BenchmarkTools

# ## Mesh parameters (matlab code)
#
# ```m
# Nx=128;Ny=256;
# xmin=-pi; xmax=pi;
#
# dx=(xmax-xmin)/Nx;
# x=xmin:dx:xmax-dx;
#
# ymin=-pi; ymax=pi;
# dy=(ymax-ymin)/Ny;
# y=ymin:dy:ymax-dy;
# ```

# ## Julia type for mesh information
#
# ```jl
# struct Mesh
#     nx   :: Int
#     ny   :: Int
#     xmin :: Float64
#     xmax :: Float64
#     ymin :: Float64
#     ymax :: Float64
#     dx   :: Float64
#     dy   :: Float64
#     x    :: Vector{Float64}
#     y    :: Vector{Float64}
# end
#
# mesh = Mesh( 128, 256, -π, π, -π, π, 2π/128, 2π/256, ...)
#
# ```
#

struct Mesh

    nx   :: Int
    ny   :: Int
    xmin :: Float64
    xmax :: Float64
    ymin :: Float64
    ymax :: Float64
    dx   :: Float64
    dy   :: Float64
    x    :: Vector{Float64}
    y    :: Vector{Float64}

    function Mesh( xmin, xmax, nx, ymin, ymax, ny)
        dx, dy = (xmax-xmin)/nx, (ymax-ymin)/ny
        x = range(xmin, stop=xmax, length=nx+1)[1:end-1]  ## we remove the end point
        y = range(ymin, stop=ymax, length=ny+1)[1:end-1]  ## periodic boundary condition
        new( nx, ny, xmin, xmax, ymin, ymax, dx, dy, x, y)
    end
end

mesh = Mesh(-π, π, 128, -π, π, 256)

@show mesh.xmin, mesh.xmax, mesh.nx, mesh.dx

# # Initialization of f : 2d array of double float
#
# ```m
# f=zeros(Nx,Ny);
# for i=1:Nx
#     xx=xmin+(i-1)*dx;
#     for j=1:Ny
#         yy=ymin+(j-1)*dy;
#         f(i,j)=exp(-(xx-1)*(xx-1)/0.1)*exp(-(yy-1)*(yy-1)/0.1);
#     end
# end
# ```

# ```jl
# f = zeros(Float64,(mesh.nx,mesh.ny))
#
# for (i, x) in enumerate(mesh.x), (j, y) in enumerate(mesh.y)
#
#     f[i,j] = exp(-(x-1)*(x-1)/0.1)*exp(-(y-1)*(y-1)/0.1)
#
# end
# ```

# ### Julia function to compute exact solution

function exact(tf, mesh)

    f = zeros(Float64,(mesh.nx, mesh.ny))
    for (i, x) in enumerate(mesh.x), (j, y) in enumerate(mesh.y)
        xn = cos(tf)*x - sin(tf)*y
        yn = sin(tf)*x + cos(tf)*y
        f[i,j] = exp(-(xn-1)*(xn-1)/0.1)*exp(-(yn-1)*(yn-1)/0.1)
    end

    f
end

f = exact(0.0, mesh)
surface(f)

# ## Create the gif to show what we are computing

function create_gif()
    x = y = range(-π, stop=π, length=40)
    n = 100

    prog = Progress(n,1) ## progress bar

    @gif for t in range(0, stop=2π, length=n)
        f(x,y) = exp(-((cos(t)*x-sin(t)*y)-1)^2/0.2)*exp(-((sin(t)*x+cos(t)*y)-1)^2/0.2)

        p = plot(x, y, f, st = [:surface])

        plot!(p[1])
        plot!(zlims=(-0.01,1.01))

        next!(prog) ## increment the progress bar
    end
end

create_gif();

# ![](tmp.gif)

# ## Advection and loop over time
# ```m
# tf=200*pi;Nt=1000;dt=tf/Nt;
# kx=2*pi/(xmax-xmin)*[0:Nx/2-1,Nx/2-Nx:Nx-1-Nx];
# ky=2*pi/(ymax-ymin)*[0:Ny/2-1,Ny/2-Ny:Ny-1-Ny];
#
# fnx=zeros(1,Nx);ffx=zeros(1,Nx);fny=zeros(1,Ny);ffy=zeros(1,Ny);
#
# for n=1:Nt
#     for i=1:Nx
#         xx=xmin+(i-1)*dx;
#         ffy=fft(f(i,:));
#         fny=real(ifft(exp(sqrt(-1)*xx*ky*tan(dt/2)).*ffy));
#         f(i,:)=fny;
#     end
#
#     for j=1:Ny
#         yy=ymin+(j-1)*dy;
#         ffx=fft(f(:,j));
#         fnx=real(ifft(exp(-sqrt(-1)*yy*kx*sin(dt)).*transpose(ffx)));
#         f(:,j)=fnx;
#     end
#
#     for i=1:Nx
#         xx=xmin+(i-1)*dx;
#         ffy=fft(f(i,:));
#         fny=real(ifft(exp(sqrt(-1)*xx*ky*tan(dt/2)).*ffy));
#         f(i,:)=fny;
#     end
# end
# ```

# ## Function to compute error
#
# ```m
#
# % compute errors in Linfty norm
# error1=max(max(f-f_exact))
#
# ```
#
# - In julia the max value of an array is `maximum`.

function error1(f, f_exact)
    maximum(abs.(f .- f_exact))
end
#----------------------------------------------------------------------------

# ## Naive translation of the matlab code

function naive_from_matlab(tf, nt, mesh::Mesh)

    dt = tf/nt

    kx = 2π/(mesh.xmax-mesh.xmin)*[0:mesh.nx÷2-1;mesh.nx÷2-mesh.nx:-1]
    ky = 2π/(mesh.ymax-mesh.ymin)*[0:mesh.ny÷2-1;mesh.ny÷2-mesh.ny:-1]

    f = exact(0.0, mesh)

    for n=1:nt

       for (i, x) in enumerate(mesh.x)
           f[i,:]=real(ifft(exp.(1im*x*ky*tan(dt/2)).*fft(f[i,:])))
       end

       for (j, y) in enumerate(mesh.y)
           f[:,j]=real(ifft(exp.(-1im*y*kx*sin(dt)).*fft(f[:,j])))
       end

       for (i, x) in enumerate(mesh.x)
           f[i,:]=real(ifft(exp.(1im*x*ky*tan(dt/2)).*fft(f[i,:])))
       end
   end

    f
end
#----------------------------------------------------------------------------

nt, tf = 1000, 200
println( " error = ", error1(naive_from_matlab(tf, nt, mesh), exact(tf, mesh)))
@btime naive_from_matlab(tf, nt, mesh);
#----------------------------------------------------------------------------

# ###  Vectorized version
#
# - We remove the for loops over direction x and y by creating the 2d arrays `exky` and `ekxy`.
# - We save cpu time by computing them before the loop over time

function vectorized(tf, nt, mesh::Mesh)

    dt = tf/nt

    kx = 2π/(mesh.xmax-mesh.xmin)*[0:mesh.nx÷2-1;mesh.nx÷2-mesh.nx:-1]
    ky = 2π/(mesh.ymax-mesh.ymin)*[0:mesh.ny÷2-1;mesh.ny÷2-mesh.ny:-1]

    f = exact(0.0, mesh)

    exky = exp.( 1im*tan(dt/2) .* mesh.x  .* ky')
    ekxy = exp.(-1im*sin(dt)   .* mesh.y' .* kx )

    for n = 1:nt
        f = real(ifft(exky .* fft(f, 2), 2))
        f = real(ifft(ekxy .* fft(f, 1), 1))
        f = real(ifft(exky .* fft(f, 2), 2))
    end

    f
end
#----------------------------------------------------------------------------

nt, tf = 1000, 200
println( " error = ", error1(vectorized(tf, nt, mesh), exact(tf, mesh)))
@btime vectorized(tf, nt, mesh);
#----------------------------------------------------------------------------

# ## Inplace computation
# - We remove the Float64-Complex128 conversion by allocating the distribution function `f` as a Complex array
# - Note that we need to use the inplace assignement operator ".="  to initialize the `f` array.
# - We use inplace computation for fft with the "bang" operator `!`

function inplace(tf, nt, mesh::Mesh)

    dt = tf/nt

    kx = 2π/(mesh.xmax-mesh.xmin)*[0:mesh.nx÷2-1;mesh.nx÷2-mesh.nx:-1]
    ky = 2π/(mesh.ymax-mesh.ymin)*[0:mesh.ny÷2-1;mesh.ny÷2-mesh.ny:-1]

    f  = zeros(Complex{Float64},(mesh.nx,mesh.ny))
    f .= exact(0.0, mesh)

    exky = exp.( 1im*tan(dt/2) .* mesh.x  .* ky')
    ekxy = exp.(-1im*sin(dt)   .* mesh.y' .* kx )

    for n = 1:nt
        fft!(f, 2)
        f .= exky .* f
        ifft!(f,2)
        fft!(f, 1)
        f .= ekxy .* f
        ifft!(f, 1)
        fft!(f, 2)
        f .= exky .* f
        ifft!(f,2)
    end

    real(f)
end
#----------------------------------------------------------------------------

nt, tf = 1000, 200
println( " error = ", error1(inplace(tf, nt, mesh), exact(tf, mesh)))
@btime inplace(tf, nt, mesh);
#----------------------------------------------------------------------------

# ### Use plans for fft

# - When you apply multiple fft on array with same shape and size, it is recommended to use fftw plan to improve computations.
# - Let's try to initialize our two fft along x and y with plans.

function with_fft_plans(tf, nt, mesh::Mesh)

    dt = tf/nt

    kx = 2π/(mesh.xmax-mesh.xmin)*[0:mesh.nx÷2-1;mesh.nx÷2-mesh.nx:-1]
    ky = 2π/(mesh.ymax-mesh.ymin)*[0:mesh.ny÷2-1;mesh.ny÷2-mesh.ny:-1]

    f  = zeros(Complex{Float64},(mesh.nx,mesh.ny))
    f .= exact(0.0, mesh)
    f̂  = similar(f)

    exky = exp.( 1im*tan(dt/2) .* mesh.x  .* ky')
    ekxy = exp.(-1im*sin(dt)   .* mesh.y' .* kx )

    Px = plan_fft(f, 1)
    Py = plan_fft(f, 2)

    for n = 1:nt

        f̂ .= Py * f
        f̂ .= f̂  .* exky
        f .= Py \ f̂

        f̂ .= Px * f
        f̂ .= f̂  .* ekxy
        f .= Px \ f̂

        f̂ .= Py * f
        f̂ .= f̂  .* exky
        f .= Py \ f̂

    end

    real(f)
end
#----------------------------------------------------------------------------

nt, tf = 1000, 200
println( " error = ", error1(with_fft_plans(tf, nt, mesh), exact(tf, mesh)))
@btime with_fft_plans(tf, nt, mesh);
#----------------------------------------------------------------------------

# ## Inplace computation and fft plans
#
# To apply fft plan to an array A, we use a preallocated output array Â by calling `mul!(Â, plan, A)`.
# The input array A must be a complex floating-point array like the output Â.
# The inverse-transform is computed inplace by applying `inv(P)` with `ldiv!(A, P, Â)`.

function with_fft_plans_inplace(tf, nt, mesh::Mesh)

    dt = tf/nt

    kx = 2π/(mesh.xmax-mesh.xmin)*[0:mesh.nx÷2-1;mesh.nx÷2-mesh.nx:-1]
    ky = 2π/(mesh.ymax-mesh.ymin)*[0:mesh.ny÷2-1;mesh.ny÷2-mesh.ny:-1]

    f  = zeros(Complex{Float64},(mesh.nx,mesh.ny))
    f .= exact(0.0, mesh)
    f̂  = similar(f)

    exky = exp.( 1im*tan(dt/2) .* mesh.x  .* ky')
    ekxy = exp.(-1im*sin(dt)   .* mesh.y' .* kx )

    Px = plan_fft(f, 1)
    Py = plan_fft(f, 2)

    for n = 1:nt

        mul!(f̂, Py, f)
        f̂ .= f̂ .* exky
        ldiv!(f, Py, f̂)

        mul!(f̂, Px, f)
        f̂ .= f̂ .* ekxy
        ldiv!(f, Px, f̂)

        mul!(f̂, Py, f)
        f̂ .= f̂ .* exky
        ldiv!(f, Py, f̂)

    end

    real(f)
end
#----------------------------------------------------------------------------

nt, tf = 1000, 200
println( " error = ", error1(with_fft_plans_inplace(tf, nt, mesh), exact(tf, mesh)))
@btime with_fft_plans_inplace(tf, nt, mesh);
#----------------------------------------------------------------------------

# ## Explicit transpose of `f`
#
# - Multidimensional arrays in Julia are stored in column-major order.
# - FFTs along y are slower than FFTs along x
# - We can speed-up the computation by allocating the transposed `f`
# and transpose f for each advection along y.

function with_fft_transposed(tf, nt, mesh::Mesh)

    dt = tf/nt

    kx = 2π/(mesh.xmax-mesh.xmin)*[0:mesh.nx÷2-1;mesh.nx÷2-mesh.nx:-1]
    ky = 2π/(mesh.ymax-mesh.ymin)*[0:mesh.ny÷2-1;mesh.ny÷2-mesh.ny:-1]

    f  = zeros(Complex{Float64},(mesh.nx,mesh.ny))
    f̂  = similar(f)
    fᵗ = zeros(Complex{Float64},(mesh.ny,mesh.nx))
    f̂ᵗ = similar(fᵗ)

    exky = exp.( 1im*tan(dt/2) .* mesh.x' .* ky )
    ekxy = exp.(-1im*sin(dt)   .* mesh.y' .* kx )

    FFTW.set_num_threads(4)
    Px = plan_fft(f,  1, flags=FFTW.PATIENT)
    Py = plan_fft(fᵗ, 1, flags=FFTW.PATIENT)

    f .= exact(0.0, mesh)

    for n = 1:nt
        transpose!(fᵗ,f)
        mul!(f̂ᵗ, Py, fᵗ)
        f̂ᵗ .= f̂ᵗ .* exky
        ldiv!(fᵗ, Py, f̂ᵗ)
        transpose!(f,fᵗ)

        mul!(f̂, Px, f)
        f̂ .= f̂ .* ekxy
        ldiv!(f, Px, f̂)

        transpose!(fᵗ,f)
        mul!(f̂ᵗ, Py, fᵗ)
        f̂ᵗ .= f̂ᵗ .* exky
        ldiv!(fᵗ, Py, f̂ᵗ)
        transpose!(f,fᵗ)
    end
    real(f)
end
#----------------------------------------------------------------------------

nt, tf = 1000, 200
println( " error = ", error1(with_fft_transposed(tf, nt, mesh), exact(tf, mesh)))
@btime with_fft_transposed(tf, nt, mesh);
#----------------------------------------------------------------------------

tf, nt = 400π, 1000
mesh = Mesh(-π, π, 512, -π, π, 256)

inplace_bench = @benchmark inplace(tf, nt, mesh)
vectorized_bench = @benchmark vectorized(tf, nt, mesh)
with_fft_plans_bench = @benchmark with_fft_plans(tf, nt, mesh)
with_fft_plans_inplace_bench = @benchmark with_fft_plans_inplace(tf, nt, mesh)
with_fft_transposed_bench = @benchmark with_fft_transposed(tf, nt, mesh)
#----------------------------------------------------------------------------

d = Dict()
d["vectorized"] = minimum(vectorized_bench.times) / 1e6
d["inplace"] = minimum(inplace_bench.times) / 1e6
d["with_fft_plans"] = minimum(with_fft_plans_bench.times) / 1e6
d["with_fft_plans_inplace"] = minimum(with_fft_plans_inplace_bench.times) / 1e6
d["with_fft_transposed"] = minimum(with_fft_transposed_bench.times) / 1e6;
#----------------------------------------------------------------------------

for (key, value) in sort(collect(d), by=last)
    println(rpad(key, 25, "."), lpad(round(value, digits=1), 6, "."))
end
#----------------------------------------------------------------------------

# ## Conclusion
# - Use pre-allocations of memory and inplace computation are very important
# - Try to always do computation on data contiguous in memory
# - In this notebook, use btime to not taking account of time consumed in
# compilation.


"""
celsius_to_fahrenheit(celsius, ndigits::Int = 2)

Converts celsius to fahrenheit and round to 2 decimal places

# Example

```julia
celsius_to_fahrenheit(273.354, 3) == 524.037  # returns true
celsius_to_fahrenheit(273.354, 0) == 524.0    # returns true
celsius_to_fahrenheit(-40.0) == -40.0         # returns true
celsius_to_fahrenheit(-20.0) == -4.0          # returns true
celsius_to_fahrenheit(0) == 32.0              # returns true
celsius_to_fahrenheit(20) == 68.0             # returns true
```
"""
function celsius_to_fahrenheit(celsius, ndigits::Int = 2)
    return round((float(celsius) * 1.8) + 32.0; digits = ndigits)
end

"""
function celsius_to_kelvin(celsius, ndigits::Int = 2)

Converts celsius to kelvin and round to 2 decimal places

# Example

```julia
celsius_to_kelvin(273.354, 3) == 546.504    # returns true
celsius_to_kelvin(273.354, 0) == 547.0      # returns true
celsius_to_kelvin(0.0) == 273.15            # returns true
celsius_to_kelvin(20.0) == 293.15           # returns true
```

"""
function celsius_to_kelvin(celsius, ndigits::Int = 2)
    return round(float(celsius) + 273.15; digits = ndigits)
end

"""
fahrenheit_to_celsius(fahrenheit, ndigits::Int = 2)

Converts fahrenheit to celsius and round to 2 decimal places

# Example

```julia
fahrenheit_to_celsius(273.354, 3) == 134.086 # returns true
fahrenheit_to_celsius(273.354, 0) == 134.0   # returns true
fahrenheit_to_celsius(0.0) == -17.78         # returns true
fahrenheit_to_celsius(20.0) == -6.67         # returns true
fahrenheit_to_celsius(40.0) == 4.44          # returns true
fahrenheit_to_celsius(60.0) == 15.56         # returns true
fahrenheit_to_celsius(80.0) == 26.67         # returns true
```
"""
function fahrenheit_to_celsius(fahrenheit, ndigits::Int = 2)
    return round((float(fahrenheit) - 32.0) * 5 / 9; digits = ndigits)
end

"""
fahrenheit_to_kelvin(fahrenheit, ndigits::Int = 2)

Converts fahrenheit to kelvin and round to 2 decimal places

# Example

```julia
fahrenheit_to_kelvin(273.354, 3) == 407.236 # returns true
fahrenheit_to_kelvin(273.354, 0) == 407.0   # returns true
fahrenheit_to_kelvin(0) == 255.37           # returns true
fahrenheit_to_kelvin(20.0) == 266.48        # returns true
fahrenheit_to_kelvin(40.0) == 277.59        # returns true
fahrenheit_to_kelvin(60.0) == 288.71        # returns true
fahrenheit_to_kelvin(80.0) == 299.82        # returns true
```
"""
function fahrenheit_to_kelvin(fahrenheit, ndigits::Int = 2)
    return round(
        ((float(fahrenheit) - 32.0) * 5 / 9) + 273.15;
        digits = ndigits,
    )
end

"""
function kelvin_to_celsius(kelvin, ndigits::Int = 2)

Converts kelvin to celsius and round to 2 decimal places

# Example

```julia
kelvin_to_celsius(273.354, 3) == 0.204   # returns true
kelvin_to_celsius(273.354, 0) == 0.0     # returns true
kelvin_to_celsius(273.15) == 0.0         # returns true
kelvin_to_celsius(300) == 26.85          # returns true
```
"""
function kelvin_to_celsius(kelvin, ndigits::Int = 2)
    return round(float(kelvin) - 273.15; digits = ndigits)
end

"""
function kelvin_to_fahrenheit(kelvin, ndigits::Int = 2)

Converts kelvin to fahrenheit and round to 2 decimal places

# Example

```julia
kelvin_to_fahrenheit(273.354, 3) == 32.367  # returns true
kelvin_to_fahrenheit(273.354, 0) == 32.0    # returns true
kelvin_to_fahrenheit(273.15) == 32.0        # returns true
kelvin_to_fahrenheit(300) == 80.33          # returns true
```
"""
function kelvin_to_fahrenheit(kelvin, ndigits::Int = 2)
    return round(((float(kelvin) - 273.15) * 1.8) + 32.0; digits = ndigits)
end


mutable struct GaussianFeature
    _means::AbstractArray{Float64,1} # the list of means
    _var::Float64 # the variance of gaussian
    _n_kernels::Int64 # cache the number of kernels
    function GaussianFeature(means::AbstractArray{Float64,1}, var::Float64)
        new(means, var, size(means)[1])
    end
end

function _gauss(mean::Float64, var::Float64, x::AbstractArray{Float64,1})
    return exp.(-(0.5 / var) .* (x .- mean) .^ 2)
end

function transform(feature::GaussianFeature, x::AbstractArray{Float64,1})
    # this feature transforms R^1 to R^1
    # x is an array of size n_samples
    # returns Phi = [phi_1(x),,,, phi_N(x)] where phi_i(x) is a gaussian centered around means[i]
    n_samples = size(x)[1]
    n_kernels = feature._n_kernels
    Phi = zeros(n_samples, n_kernels)
    for i = 1:n_kernels
        mean = feature._means[i]
        var = feature._var
        Phi[:, i] = _gauss(mean, var, x)
    end

    return Phi
end


using JuMP, GLPK

# wrap all of our code inside a function (for better performance)
function example_basic()

    # define model and optimizer
    model = Model(GLPK.Optimizer)

    # define variables
    @variable(model, 0 <= x <= 2)
    @variable(model, 0 <= y <= 30)

    # define objective function
    @objective(model, Max, 5x + 3y)

    # add additional constraints
    @constraint(model, 1x + 5y <= 3.0)

    # display the model
    print(model)

    # optimize the model
    JuMP.optimize!(model)

    # return and print objective function and optimal values of variables
    obj_value = JuMP.objective_value(model)
    x_value = JuMP.value(x)
    y_value = JuMP.value(y)
    println("Objective value: ", obj_value)
    println("x = ", x_value)
    println("y = ", y_value)
end

# call the function defined above
example_basic()

using JuMP, GLPK
function example_sudoku()

    # input the initial puzzle board (0s mean blanks)
    initial_grid = [
                    3 1 0 0 5 8 0 0 4;
                    0 0 9 3 2 0 0 0 0;
                    0 2 5 1 0 4 0 9 0;
                    0 0 0 0 0 0 3 8 9;
                    0 0 8 0 0 0 5 0 0;
                    5 4 6 0 0 0 0 0 0;
                    0 8 0 2 0 3 6 5 0;
                    0 0 0 0 7 1 4 0 0;
                    7 0 0 4 8 0 0 2 1
                    ]

    # use GLPK Optimizer
    model = Model(GLPK.Optimizer)

    # Set up the variables: each one can only take on binary values, so we add "Bin" to the end as a constraint
    @variable(model, x[1:9, 1:9, 1:9], Bin)

    # Add the constraints
    @constraints(model, begin
                     # Constraint 1 - Only one value appears in each cell
                     cell[i in 1:9, j in 1:9], sum(x[i, j, :]) == 1
                     # Constraint 2 - Each value appears in each row once only
                     row[i in 1:9, k in 1:9], sum(x[i, :, k]) == 1
                     # Constraint 3 - Each value appears in each column once only
                     col[j in 1:9, k in 1:9], sum(x[:, j, k]) == 1
                     # Constraint 4 - Each value appears in each 3x3 subgrid once only
                     subgrid[i=1:3:7, j=1:3:7, val=1:9], sum(x[i:i + 2, j:j + 2, val]) == 1
                 end)

    # Add additional constraints that reflect the starting point of the puzzle board
    # (i.e. don't attempt to update the numbers that were given as part of the puzzle)
    for row in 1:9, col in 1:9
        if initial_grid[row, col] != 0
            @constraint(model, x[row, col, initial_grid[row, col]] == 1)
        end
    end

    # Solve it
    JuMP.optimize!(model)

    term_status = JuMP.termination_status(model)
    primal_status = JuMP.primal_status(model)
    is_optimal = term_status == MOI.OPTIMAL

    # Check solution
    if is_optimal
        mip_solution = JuMP.value.(x)
        sol = zeros(Int, 9, 9)
        for row in 1:9, col in 1:9, val in 1:9
            if mip_solution[row, col, val] >= 0.9
                sol[row, col] = val
            end
        end
        return sol
    else
        error("The solver did not find an optimal solution.")
    end
end

function print_sudoku_solution(solution)
    println("Solution:")
    println("[-----------------------]")
    for row in 1:9
        print("[ ")
        for col in 1:9
            print(solution[row, col], " ")
            if col % 3 == 0 && col < 9
                print("| ")
            end
        end
        println("]")
        if row % 3 == 0
            println("[-----------------------]")
        end
    end
end

sol = example_sudoku()
print_sudoku_solution(sol)

using HTTP, JuliaDB, JuMP, GLPK

# first function: read in the player data from the class GitHub repository
function read_in_data(url)
    newtable  = csvread(IOBuffer(HTTP.get(url).body), skiplines_begin=0, header_exists=true)
    players   = newtable[1][2]
    salaries  = newtable[1][4]./1000000
    ppg       = newtable[1][11]
    return players,salaries,ppg
end

function SolveModel(players,salary,points)
    N = length(salary)

    m = Model(GLPK.Optimizer)

    # define the variables: they are 0 if the player did not make the team, 1 if the player did make the team
    @variable(m, picked[1:N], Bin)

    # categories:
    @objective(m, Max, sum( points[i] * picked[i] for i in 1:N))

    @constraints m begin
        # Constraint 1 - payroll <= 132.6m
        sum(salary[i] * picked[i] for i in 1:N) <= 132.6
        # Constraint 2 - must have exactly 15 players on roster
        sum(picked[i] for i in 1:N) == 15
    end

    # Solve it
    JuMP.optimize!(m);
    pck     = convert(BitArray,JuMP.value.(picked))
    lineup  = players[pck]
    points  = JuMP.objective_value(m)
    payroll = sum(salary[pck])
    return lineup,points,payroll
end

# call first function (to import data)
players,salaries,pts = read_in_data("https://raw.githubusercontent.com/tyleransom/DScourseS20/master/WebData/playerSalaryStats.csv")

# pass data into second function to get optimal lineup
lineup,total_points,payroll = SolveModel(players,salaries,pts)
println("team: ",lineup)
println("total points scored per game: ",total_points)
println("payroll: ",payroll)

# first function: read in the player data from the class GitHub repository
function read_in_data(url)
    newtable = csvread(IOBuffer(HTTP.get(url).body), skiplines_begin=0, header_exists=true)
    players  = newtable[1][2]
    salaries = newtable[1][4]./1000000
    mpg      = newtable[1][5]
    fgaG5    = 1.0.*((newtable[1][9]).>5)
    fga      = newtable[1][9]
    ppg      = newtable[1][11]
    return players,salaries,mpg,fgaG5,fga,ppg
end

function SolveModel(players,salary,minutes,field_goals_over5,field_goals,points)
    N = length(salary)

    m = Model(GLPK.Optimizer)

    # define the variables: they are 0 if the player did not make the team, 1 if the player did make the team
    @variable(m, picked[1:N], Bin)

    # categories:
    @objective(m, Max, sum( points[i] * picked[i] for i in 1:N))

    @constraints m begin
        # Constraint 1 - payroll <= 132.6m
        sum(salary[i] * picked[i] for i in 1:N) <= 132.6
        # Constraint 2 - must have exactly 15 players on roster
        sum(picked[i] for i in 1:N) == 15
        # Constraint 3 - total minutes must not exceed 240
        sum(minutes[i] * picked[i] for i in 1:N) <= 240
        # Constraint 4 - total shot attempts must be lower than 80
        sum(field_goals[i] * picked[i] for i in 1:N) <= 80
    end

    # Solve it
    JuMP.optimize!(m);
    pck      = convert(BitArray,JuMP.value.(picked))
    lineup   = players[pck]
    totmin   = sum(minutes[pck])
    payroll  = sum(salary[pck])
    totshots = sum(field_goals[pck])
    points   = JuMP.objective_value(m)
    return lineup,points,totmin,payroll,totshots
end

# call first function (to import data)
players,salaries,minutes,over5fg,fga,pts = read_in_data("https://raw.githubusercontent.com/tyleransom/DScourseS20/master/WebData/playerSalaryStats.csv")

# pass data into second function to get optimal lineup
lineup,total_points,total_minutes,payroll,totshots = SolveModel(players,salaries,minutes,over5fg,fga,pts)
println("team: ",lineup)
println("total points scored per game: ",total_points)
println("payroll: ",payroll)
println("total shots per game: ",totshots)
println("total minutes per game: ",total_minutes)

using HTTP, JuliaDB, JuMP, Ipopt
function import_auto(url)
    newtable  = csvread(IOBuffer(HTTP.get(url).body), skiplines_begin=0, header_exists=true)
    depvar    = log.(newtable[1][2]) # log price
    indepvars = cat(ones(size(depvar)),newtable[1][3],newtable[1][5],newtable[1][6]; dims=2) # constant, mpg, headroom, trunk
    return depvar,indepvars
end

Y,X = import_auto("https://tyleransom.github.io/teaching/MetricsLabs/auto.csv")

function jumpOLS(Y,X,startval=zeros(size(X,2),1))
    OLS = Model(Ipopt.Optimizer)

    # Declare the variables you are optimizing over
    @variable(OLS, b[i=1:size(X,2)], start = startval[i])

    # Write your objective function
    @NLobjective(OLS, Min, sum( (Y[i]-sum( X[i,k]*b[k] for k in 1:size(X,2) ))^2 for i in 1:size(X,1) ) )

    # Solve the objective function
    JuMP.optimize!(OLS)

    SSR = JuMP.objective_value(OLS)
    b_value = JuMP.value.(b)
    println("Objective value: ", SSR)
    println("beta hat = ", b_value)
    println("RMSE = ", sqrt(SSR/(size(X,1)-size(X,2))))
end

jumpOLS(Y,X)


using AlgebraicPetri
using OrdinaryDiffEq
using Plots
using Catlab.Meta
using JSON

import OrdinaryDiffEq: ODEProblem
ODEProblem(p::LabelledReactionNet, t) = ODEProblem(vectorfield(p), concentrations(p), t, rates(p))

# help capture JSON of defined functions
macro capture(funcname, exname, ex)
    quote
        $(esc(exname)) = $(repr(strip_lines(ex, recurse=true)))
        $(esc(funcname)) = $ex
    end
end

@capture γ γ_text 1/14
@capture β β_text t->begin
    policy_days = [20,60,120] .+ 17
    contact_rate = 0.05
    pol = findfirst(x->t<=x, policy_days) # array of days when policy changes
    growth_rate = pol == 1 ? 0.0 : (2^(1/((pol-1)*5)) - 1) # growth rate depending on policy
    return (growth_rate + γ) / 990 * (1-contact_rate) # calculate rate of infection
end

sir_cset= LabelledReactionNet{Function, Float64}((:S=>990, :I=>10, :R=>0), (:inf, β)=>((:S, :I)=>(:I,:I)), (:rec, t->γ)=>(:I=>:R))

Graph(sir_cset)

prob = ODEProblem(sir_cset, (17.0, 120.0))
sol = OrdinaryDiffEq.solve(prob,Tsit5())
plot(sol)

## Getting Sharable JSON
sir_cset_string = LabelledReactionNet{String, Int}((:S=>990, :I=>10, :R=>0), (:inf, β_text)=>((:S, :I)=>(:I,:I)), (:rec, γ_text)=>(:I=>:R))
JSON.print(sir_cset_string.tables, 2)


# Minimum variance analysis

export mva

"""
    mva(Bx, By, Bz; verbose=false)

Perform minimum variance analysis to vector components defined in orthogonal
coordinates `Bx`, `By` and `Bz`.
If λ₁ ≥ λ₂ ≥ λ₃ are 3 eigenvalues of the constructed matrix M, then a good
indicator of nice fitting LMN coordinate system should have λ₂/λ₃ > 5. Set
`verbose=true` to turn on the check.
"""
function mva(Bx, By, Bz; verbose=false)

   B̄1 = mean(Bx)
   B̄2 = mean(By)
   B̄3 = mean(Bz)
   B̄11= mean(Bx.*Bx) - B̄1*B̄1
   B̄22= mean(By.*By) - B̄2*B̄2
   B̄33= mean(Bz.*Bz) - B̄3*B̄3
   B̄12= mean(Bx.*By) - B̄1*B̄2
   B̄23= mean(By.*Bz) - B̄2*B̄3
   B̄31= mean(Bz.*Bx) - B̄3*B̄1
   # Construct the matrix
   M = [B̄11 B̄12 B̄31; B̄12 B̄22 B̄23; B̄31 B̄23 B̄33]

   # Compute the eigen values and ratios (descending order)
   F = eigen(M, sortby = x -> -abs(x))

   if verbose
      println("Eigenvalues:", F.values)
      println("Eigenvectors:")
      println(F.vectors[:,1])
      println(F.vectors[:,2])
      println(F.vectors[:,3])
      r = F.values[2] / F.values[3]
      println("Ratio of intermediate variance to minimum variance = ",
         round(r, digits=3))
      if r ≥ 5
         println("Seems to be a proper MVA attempt!")
      else
         @warn "Take the MVA result with a grain of salt!"
      end
   end
   F
end


# V-measure of contingency table
function _vmeasure(A::AbstractMatrix{<:Integer}; β::Real)
    (β >= 0) || throw(ArgumentError("β should be nonnegative"))

    N = sum(A)
    (N == 0.0) && return 0.0

    entA = entropy(A)
    entArows = entropy(sum(A, dims=2))
    entAcols = entropy(sum(A, dims=1))

    hck = (entA - entAcols)/N
    hkc = (entA - entArows)/N
    hc = entArows/N + log(N)
    hk = entAcols/N + log(N)

    # Homogeneity
    h = hc == 0.0 ? 1.0 : 1.0 - hck/hc
    # Completeness
    c = hk == 0.0 ? 1.0 : 1.0 - hkc/hk

    # V-measure
    V_β = (1 + β)*h*c/(β*h + c)
    return V_β
end

"""
    vmeasure(assign1, assign2; β = 1.0)

V-measure between two clustering assignments.

`assign1` and `assign2` can be either `ClusteringResult` objects or
assignments vectors (`AbstractVector{<:Integer}`).

The `β` parameter defines trade-off between _homogeneity_ and _completeness_:
 * if `β` is greater than 1, _completeness_ is weighted more strongly,
 * if `β` is less than 1, _homogeneity_ is weighted more strongly.

*Ref:* Andrew Rosenberg and Julia Hirschberg, 2007. "V-Measure: A conditional entropy-based external cluster evaluation measure"
"""
function vmeasure(assign1::Union{AbstractVector{<:Integer}, ClusteringResult},
                  assign2::Union{AbstractVector{<:Integer}, ClusteringResult};
                  β::Real = 1.0)
    _assign1 = isa(assign1, AbstractVector) ? assign1 : assignments(assign1)
    _assign2 = isa(assign2, AbstractVector) ? assign2 : assignments(assign2)
    return _vmeasure(counts(_assign1, _assign2,
                            (1:maximum(_assign1), 1:maximum(_assign2))), β=β)
end


#=
Given an array of numbers, find the maximum sum of any contiguous subarray of the array.

For example, given the array [34, -50, 42, 14, -5, 86], the maximum sum would be 137, since we would take elements 42, 14, -5, and 86.

Given the array [-5, -1, -8, -9], the maximum sum would be 0, since we would not take any elements.

Do this in O(N) time.
=#

function maximum_contiguous_sum(X::Array{Int,1})
    max_sum = 0
    current_subarray = []
    current_sum = 0
    for index in eachindex(X)
        if current_sum + X[index] > 0
            push!(current_subarray, X[index])
            current_sum = current_sum + X[index]
            if current_sum > max_sum
                max_sum = current_sum
            end
        else
            current_sum = 0
            current_subarray = []
        end
    end
    return max_sum
end