using Distributions

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

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

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

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


@testset "$(rpad("Lobatto-Legendre",80))" begin

    @test_throws ErrorException LobattoLegendreQuadrature(1)

    for s in 2:10
        @test LobattoLegendreQuadrature(s) ≈  LobattoLegendreQuadrature(s; fast=true)
        @test LobattoLegendreQuadrature(s) == LobattoLegendreQuadrature(Float64, s)

        @test sum(weights(LobattoLegendreQuadrature(s))) ≈ 1
    end

end


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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


"""
    Deterministic(value)

Create a deterministic distribution over only one value.

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

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


"""
    hadi1992_handle_singularity(S)

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

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

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

"""
    hadi1992(multivariateData)

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

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

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

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

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

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

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

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

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

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

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

    outlierset = setdiff(allindices, basic_subset_indices)

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


function subtract(d1::Dict, d2::Dict)
    res = Dict()
    for key in keys(d1)
        if key ∉ keys(d2)
            res[key] = nothing
        end
    end
    res
end

# using Sets instead
"""
    Subtract two sets.
    Result is a Set.
"""
function subtract(s1::Set, s2::Set)
    setdiff(s1, s2)
end

#   ---------- Duplicates ----------

# ... using dictionaries

function hasduplicates(t::Dict)
    d = Dict()
    for x in t
        if x ∈ d
            return true
        end
        d[x] = nothing
    end
    false
end

function usesonly(word, available)
    for letter in word
        if word ∉ available
            return false
        end
    end
    true
end

# ... using sets
function hasduplicates(t)
    length(Set(t)) < length(t)
end

function usesonly_set(word, available)
    Set(word) ⊆ Set(available)
end

# ---------- Math -----------

x = 0:0.1:2π

cos.(x) == 0.5*(ℯ.^(im*x) + ℯ.^(-im*x))

# ----------- Strings --------

function usesonly_regex(word::String, available::String)
    r = Regex("[^$(available)]")
    @show r;
    !occursin(r,word)
end

usesonly_regex("banana", "abn")
usesonly_regex("bananas", "abn")

match(r"[^abn]", "banana")

m = match(r"[^abn]", "bananas")

# ---------------- Matricies  -----------------

z = zeros(Float64, 2,3)

z[1,2] = 2

z[2,3] = 1
z

size(z)

# 1x3 row matrix
s = ones(String, 1, 3)
size(s)

# 3 x _ column matrix
s_col = ["", "", ""]
size(s_col)
s == s_col

# also a column matrix
s_col2 = ["";"";""]
size(s_col2)

s_col == s_col2

s == s_col2

a = [1 2 3; 4 5 6]

@show a
print(a)
size(a)

#slicing

u = z[:,2:end]

#broadcasting
@show im*u
size(im*u)
g = ℯ.^(im*u)
@show g
typeof(g)

typeof(u)

# ----------- Interfaces: p243 -----------------

#parametric type with no fields
struct Fibonacci{T<:Real} end

#outer constructor
Fibonacci(d::DataType) = d<:Real ? Fibonacci{d}() : error("Not Real type!")

# Iterator interface implementation: 2 parts:

# Part 1: called to initialize the iterator
Base.iterate(::Fibonacci{T}) where {T<:Real} = (zero(T), (one(T), one(T)))

# Part 2:
Base.iterate(::Fibonacci{T}, state::Tuple{T,T}) where {T<:Real} = (state[1], (state[2], state[1] + state[2]))

for e in Fibonacci(Int64)
    e > 100 && break
    print(e, " ")
end

#Equivalently:
f = Fibonacci(Int64)

#initialize state sequence
(a, (b,c)) = iterate(f)

#iterate ...
(a, (b,c)) = iterate(f, (b,c))

(a, (b,c)) = iterate(f, (b,c))

(a, (b,c)) = iterate(f, (b,c))

(a, (b,c)) = iterate(f, (b,c))

(a, (b,c)) = iterate(f, (b,c))

(a, (b,c)) = iterate(f, (b,c))

(a, (b,c)) = iterate(f, (b,c))

(a, (b,c)) = iterate(f, (b,c))

(a, (b,c)) = iterate(f, (b,c))

(a, (b,c)) = iterate(f, (b,c))

#= a for loop in julia:

for i in iter
    body
end

    equivalant to:

next = iterate(iter)
while next !== nothing
    (i, state) = next
    body
    next = iterate(iter, state)
end

=#


#This example has been adopted from https://github.com/IntelLabs/ParallelAccelerator.jl/blob/master/examples/black-scholes/black-scholes.jl

using ArrayFire

function blackscholes_serial(sptprice::AbstractArray{Float32},
                           strike::AbstractArray{Float32},
                           rate::AbstractArray{Float32},
                           volatility::AbstractArray{Float32},
                           time::AbstractArray{Float32})
    logterm = log10( sptprice ./ strike)
    powterm = .5f0 .* volatility .* volatility
    den = volatility .* sqrt(time)
    d1 = (((rate .+ powterm) .* time) .+ logterm) ./ den
    d2 = d1 .- den
    NofXd1 = cndf2(d1)
    NofXd2 = cndf2(d2)
    futureValue = strike .* exp(- rate .* time)
    c1 = futureValue .* NofXd2
    call_ = sptprice .* NofXd1 .- c1
    put  = call_ .- futureValue .+ sptprice
end

@inline function cndf2(in::AbstractArray{Float32})
    out = 0.5f0 .+ 0.5f0 .* erf(0.707106781f0 .* in)
    return out
end

function run(iterations)
    sptprice   = Float32[ 42.0 for i = 1:iterations ]
    initStrike = Float32[ 40.0 + (i / iterations) for i = 1:iterations ]
    rate       = Float32[ 0.5 for i = 1:iterations ]
    volatility = Float32[ 0.2 for i = 1:iterations ]
    time       = Float32[ 0.5 for i = 1:iterations ]

    sptprice_gpu = AFArray(sptprice)
    initStrike_gpu = AFArray(initStrike)
    rate_gpu = AFArray(rate)
    volatility_gpu = AFArray(volatility)
    time_gpu = AFArray(time)

    tic()
    put1 = blackscholes_serial(sptprice, initStrike, rate, volatility, time)
    t1 = toq()
    println("Serial checksum: ", sum(put1))
    tic()
    put2 = blackscholes_serial(sptprice_gpu, initStrike_gpu, rate_gpu, volatility_gpu, time_gpu)
    t2 = toq()
    println("Parallel checksum: ", sum(put2))
    return t1, t2
end

function driver()
    srand(0)
    tic()
    iterations = 10^7
    blackscholes_serial(Float32[], Float32[], Float32[], Float32[], Float32[])
    blackscholes_serial(AFArray(Float32[1., 2.]), AFArray(Float32[1., 2.]),
                        AFArray(Float32[1., 2.]), AFArray(Float32[1., 2.]), AFArray(Float32[1., 2.]))
    println("SELFPRIMED ", toq())
    tserial, tparallel = run(iterations)
    println("Time taken for CPU = $tserial")
    println("Time taken for GPU = $tparallel")
    println("Speedup = $(tserial / tparallel)")
    println("CPU rate = ", iterations / tserial, " opts/sec")
    println("GPU rate = ", iterations / tparallel, " opts/sec")
end
driver()


"""
```julia
rate(q::Integer, M::Integer, n::Integer) -> Real
```

Calculate the rate of a code.  That is, how efficient the code is.

Parameters:
  - `q::Integer`: the number of symbols in the code.
  - `M::Integer`: the size/number of elements in the code.
  - `n::Integer`: The word length.

Returns:
  - `Real`: Rate of the code.

---

### Examples

```julia
julia> rate(3, 5, 4) # the rate of the code which has 3 symbols, 5 words in the code, and word length of 4 (e.g., Σ = {A, B, C}, C = {ABBA,CABA,BBBB,CAAB,ACBB})
0.3662433801794817
```
"""
rate(q::T, M::T, n::T) where {T <: Integer} = log(q, M) / n

__spheres(q::T, n::T, r::T) where {T <: Integer} = sum(Integer[((big(q) - 1)^i) * binomial(big(n), big(i)) for i in 0:r])
__sphere_bound(round_func::Function, q::T, n::T, d::T) where {T <: Integer} = round_func((big(q)^n) / __spheres(q, n, d))

"""
```julia
sphere_covering_bound(q::Integer, n::Integer, d::Integer) -> Integer
```

Computes the sphere covering bound of a ``[n, d]_q``-code.

Parameters:
  - `q::Integer`: the number of symbols in the code.
  - `n::Integer`: the word length.
  - `d::Integer`: the distance of the code.

Returns:
  - `Integer`: the sphere covering bound.

---

### Examples

```julia
julia> sphere_covering_bound(5,7,3)
215
```
"""
sphere_covering_bound(q::T, n::T, d::T) where {T <: Integer} = __sphere_bound(ceil, q, n, d - 1)

"""
```julia
sphere_packing_bound(q::Integer, n::Integer, d::Integer) -> Integer
sphere_packing_bound(q::Integer, n::Integer, d::Integer, ::Rounding) -> Real
```

Computes the sphere packing bound of a ``[n, d]_q``-code.  The sphere packing bound is also known as the hamming bound.  You can use `hamming_bound` to compute the same thing.

Parameters:
  - `q::Integer`: the number of symbols in the code.
  - `n::Integer`: the word length.
  - `d::Integer`: the distance of the code.
  - `::Rounding`: use the argument `no_round` in this position to preserve the rounding of the code — which usually by default rounds down.

Returns:
  - `Integer`: the sphere packing bound.

---

### Examples

```julia
julia> sphere_packing_bound(5,7,3)
2693
```
"""
sphere_packing_bound(q::T, n::T, d::T) where T <: Integer =
	__sphere_bound(a -> floor(T, a), q, n, floor(T, (d - 1) / 2))
sphere_packing_bound(q::T, n::T, d::T, ::Rounding) where T <: Integer =
	__sphere_bound(identity, q, n, floor(T, (d - 1) / 2))
hamming_bound(q::T, n::T, d::T) where T <: Integer =
	sphere_packing_bound(q, n, d)
hamming_bound(q::T, n::T, d::T, ::Rounding) where T <: Integer =
	sphere_packing_bound(q, n, d, no_round)

"""
```julia
sphere_packing_bound(q::Integer, n::Integer, d::Integer) -> Real
```

Computes the Singleton bound of a ``[n, d]_q``-code.

Parameters:
  - `q::Integer`: the number of symbols in the code.
  - `n::Integer`: the word length.
  - `d::Integer`: the distance of the code.

Returns:
  - `Real`: the Singleton bound.  Can round down, as it is an equivalent to the Hamming bound in that it is an upper bound.
"""
# promote()
# _T = typeof(T)

singleton_bound(q::T, n::T, d::T) where T <: Integer =
	floor(T, float(big(q))^(big(n) - big(d) + 1))
singleton_bound(q::T, n::T, d::T, ::Rounding) where T <: Integer =
	float(big(q))^(big(n) - big(d) + 1)

gilbert_varshamov_bound(q::T, n::T, d::T) where T <: Integer =
	__sphere_bound(a -> floor(T, a), q, n, d - 1)
gilbert_varshamov_bound(q::T, n::T, d::T, ::Rounding) where T <: Integer =
	__sphere_bound(identity, q, n, d - 1)

function __plotkin_bound_core(round_func::Function, q::T, n::T, d::T) where T <: Integer
	if ! isequal(q, 2)
		throw(error("The Plotkin bound only works for the binary code."))
	end

	if iseven(d) && 2d > n
		return round_func((d) / (2d + 1 - n))
	elseif isodd(d) && 2d + 1 > n
		return round_func((d + 1) / (2d + 1 - n))
	elseif iseven(d)
		return T(4d)
		# return A_2(2d, d) ≤ 4d
	elseif isodd(d)
		return T(4d + 4)
		# return A_2(2d + 1, d) ≤ 4d + 4
	end
end

plotkin_bound(q::T, n::T, d::T) where T <: Integer =
	__plotkin_bound_core(a -> floor(T, a), q, n, d)
plotkin_bound(q::T, n::T, d::T, ::Rounding) where T <: Integer =
	__plotkin_bound_core(identity, q, n, d, no_round)

elias_bassalygo_bound(q::T, n::T, d::T) where T <: Integer =
elias_bassalygo_bound(q::T, n::T, d::T, ::Rounding) where T <: Integer =

function __johnson_bound_core(round_func::Function, q::T, n::T, d::T) where T <: Integer
	if isinteger((d - 1) / 2) # is odd
		t = T((d - 1) / 2)
		__sphere_bound(round_func, q, n, t) # if d = 2t + 1
	elseif isinteger(d / 2)
		t = T(d / 2)
		__sphere_bound(round_func, q, n, t)
	end
end

@doc raw"""
```julia
construct_ham_matrix(r::Int, q::Int) -> Matrix
```

Construct a Hamming parity-check matrix.

Parameters:
  - `r::Int`: number of rows of a parity check matrix.
  - `q:::Int`: The size of the alphabet of the code.

Returns:
  - `Matrix`: The Hamming matrix, denoted as ``\text{Ham}(r, q)``

---

### Examples

```julia
julia> construct_ham_matrix(3,2)
3×7 Array{Int64,2}:
 0  0  0  1  1  1  1
 0  1  1  0  0  1  1
 1  0  1  0  1  0  1
```
"""
function construct_ham_matrix(r::Int, q::Int)
    ncols = Int(floor((q^r - 1) / (q - 1)))
    M = Matrix{Int}(undef, r, ncols)

    for i in 1:ncols
        M[:, i] = reverse(digits(parse(Int, string(i, base = q)), pad = r), dims = 1)
    end

    return M
end

"""
```julia
isperfect(n::Int, k::Int, d::Int, q::Int) -> Bool
```

Checks if a code is perfect.  That is, checks if the number of words in the code is exactly the "Hamming bound", or the "Sphere Packing Bound".

Parameters:
  - `q:::Int`: The size of the alphabet of the code.
  - `n::Int`: The length of the words in the code (block length).
  - `d::Int`: The distance of the code.
  - `k::Int`: The dimension of the code.

Returns:
  - `Bool`: true or false

---

### Examples

```julia
julia> isperfect(11, 6, 5, 3)
true
```
"""
function isperfect(n::T, k::T, d::T, q::T) where T <: Int
	isprimepower(q) || throw(error("Cannot check if the code is perfect with q not a prime power."))
    M = q^k

    isequal(sphere_packing_bound(q, n, d), M) && return true
	return false
end

"""
```julia
ishammingbound(r::Int, q::Int) -> Bool
```

Checks if the code is a perfect code that is of the form of a generalised Hamming code.

Parameters:
  - `r::Int`: number of rows of a parity check matrix.
  - `q::Int`: The size of the alphabet of the code.

Returns:
  - `Bool`: true or false
"""
function ishammingperfect(r::Int, q::Int)
    n = 2^r - 1
    k = n - r
    M = q^k
    d = size(construct_ham_matrix(r, q), 1) # the number of rows of the hamming matrix (which is, by design, linearly independent)
    d = r
    # r is dim of dueal code; dim of code itself is block length minus r
    # println(n)
    # println((q^r - 1) / (q - 1))

    isequal(n, (q^r - 1) / (q - 1)) && \
		isequal(d, 3) && \
		isequal(M, q^(((q^r - 1) / (q - 1)) - r)) && \
        return true
    return false
end

"""
```julia
ishammingperfect(n::Int, k::Int, d::Int, q::Int) -> Bool
ishammingperfect(q::Int, n::Int, d::Int) -> Bool
```

Checks if the code is a perfect code that is of the form of a generalised Hamming code.

Parameters:
  - `q:::Int`: The size of the alphabet of the code.
  - `n::Int`: The length of the words in the code (block length).
  - `d::Int`: The distance of the code.
  - `k::Int`: The dimension of the code.

Returns:
  - `Bool`: true or false

---

### Examples

```julia
julia> isgolayperfect(11, 6, 5, 3) # this is one of golay's perfect codes
true
```
"""
function ishammingperfect(n::T, k::T, d::T, q::T) where T <: Int
    isprimepower(q) || return false

    M = q^k
    r = log(ℯ, ((n * log(ℯ, 1)) / (log(ℯ, 2))) + 1) / log(ℯ, 2)

    if isequal(n, (q^(r - 1)) / (q - 1)) && isequal(d, 3) && isequal(M, q^(((q^r - 1) / (q - 1)) - r))
        return true
    end

    return false
end
function ishammingperfect(q::Int, n::Int, d::Int)
	isprimepower(q) || return false # we are working in finite fields, so q must be a prime power
	d ≠ 3 && return false

	r = 1
	while ((q^r - 1) / (q - 1)) < n
		r = r + 1
	end

	return ifelse(isequal(((q^r - 1) / (q - 1)), n), true, false)
end

"""
```julia
isgolayperfect(n::Int, k::Int, d::Int, q::Int) -> Bool
```

Golay found two perfect codes.  `isgolayperfect` checks if a code of block length n, distance d, alphabet size q, and dimension k, is a perfect code as described by Golay.

Parameters:
  - `n::Int`: The block length of words in the code (e.g., word "abc" has block length 3).
  - `k::Int`: The dimension of the code.
  - `d::Int`: The distance of the code (i.e., the minimum distance between codewords in the code).
  - `q::Int`: An Int that is a prime power.  The modulus of the finite field.

Returns:
  - `Bool`: true or false.

---

### Examples

```julia
julia> isgolayperfect(11, 6, 5, 3) # this is one of golay's perfect codes
true
```
"""
function isgolayperfect(n::T, k::T, d::T, q::T) where T <: Int
	isprimepower(q) ||  false # we are working in finite fields, so q must be a prime power
    M = q^k
    (isequal(q, 2) && isequal(n, 23) && isequal(d, 7) && isequal(M, 2^12)) && return true
    (isequal(q, 3) && isequal(n, 11) && isequal(d, 5) && isequal(M, 3^6)) && return true
    return false
end


export HPolygon, add_point!, npoints, RandomHPolygon, sides
export polygon_check

"""
`HPolygon()` creates a new polygon (with no points).

`HPolygon(list)` creates a polygon whose points are specified in `list`.

See: `add_point!`
"""
struct HPolygon <: HObject
    plist::Array{HPoint,1}
    attr::Dict{Symbol,Any}
    function HPolygon()
        X = new(HPoint[],Dict{Symbol,Any}())
        set_color(X)
        set_thickness(X)
        set_line_style(X)
        return X
    end
end

"""
`add_point!(X::HPolygon, P::HPoint)` adds the point `P`
as the last point of the polygon `X`
"""
add_point!(X::HPolygon, P::HPoint) = push!(X.plist,P)

"""
`endpoints(X::HPolygon)` returns the list of vertices (in order)
of the polygon.
"""
endpoints(X::HPolygon) = deepcopy(X.plist)

"""
`sides(P:::HPolygon/HTriangle)` returns a list of the line segments
that are the sides of the polygon.
"""
function sides(X::HPolygon)::Array{HSegment,1}
    n = npoints(X)
    if n < 2
        return HSegment[]
    end

    result = Array{HSegment,1}(undef,n)
    for k=1:n-1
        a = X.plist[k]
        b = X.plist[k+1]
        result[k] = a+b
    end
    result[end] = X.plist[end] + X.plist[1]
    return result
end

sides(T::HTriangle) = sides(HPolygon(T))

"""
`npoints(X::HPolygon)` returns the number of points on the polygon.
"""
npoints(X::HPolygon) = length(X.plist)

function HPolygon(pts::Array{HPoint,1})
    X = HPolygon()
    for p in pts
        add_point!(X,p)
    end
    return X
end

HPolygon(pts...) = HPolygon(collect(pts))
HPolygon(X::HPolygon) = HPolygon(X.plist)  # copy constructor

"""
`polygon_check(X::HPolygon,quiet=true)` checks that the polygon is nondegenerate.
Possible degeneracies are:
+ Repeated vertices
+ Fewer than three distinct vertices
+ Angles that are either 0 degrees or 180 degrees
If `quiet` is `false`, then a reason for the failed check is printed.
"""
function polygon_check(X::HPolygon, quiet::Bool=true)::Bool

    # Check that the endpoints are all distinct
    n = npoints(X)

    C = HContainer(X.plist...)
    if length(C) != n
        quiet || println("The polygon has repeated vertices")
        return false
    end

    if length(C) < 3
        quiet || println("The polygon is degenerate (fewer than 3 distinct vertices)")
        return false
    end

    angs = angles(X)
    zero_angs = angs .< (THRESHOLD * eps(1.0))
    if any(zero_angs)
        quiet || println("The polygon has 0-degree angles")
        return false
    end

    big_angs = angs .> (pi - THRESHOLD*eps(1.0))
    if any(big_angs)
        quiet || println("The polygon has 180-degree angles")
        return false
    end

    return true
end

function HPolygon(T::HTriangle)
    a,b,c = endpoints(T)
    return HPolygon(a,b,c)
end

function HTriangle(X::HPolygon)
    @assert npoints(X)==3 "Can only convert a 3-point HPolygon into an HTriangle"
    return HTriangle(X.plist...)
end

"""
`RandomHPolygon(n::Int,simple::Bool=false)` create a new `HPolygon` with
`n` points chosen at random. With `simple` set to `true`, return a polygon
that does not self-intersect.
"""
function RandomHPolygon(n::Int, simple::Bool=false)
    @assert n>=0 "Number of vertices must be nonnegative"
    if simple && n>3
        P = RandomHPolygon(n)
        while !is_simple(P)
            P = RandomHPolygon(n)
        end
        return P
    end

    pts = [ RandomHPoint() for j=1:n ]
    return HPolygon(pts)
end

"""
`angles(P::HPolygon)` returns a list of the angles at the vertices of `P`.

+ The results are always in the interval `[0,pi]`.
+ The order of the angles is the order of the vertices in `P.plist`.
"""
function angles(P::HPolygon)::Array{Float64,1}
    n = npoints(P)
    result = zeros(Float64,n)
    if n < 3
        return result
    end

    # first angle
    result[1] = angle(P.plist[end],P.plist[1],P.plist[2])

    for j=2:n-1
        result[j] = angle(P.plist[j-1], P.plist[j], P.plist[j+1])
    end

    result[n] = angle(P.plist[n-1],P.plist[n],P.plist[1])

    return result
end

function perimeter(P::HPolygon)
    n = npoints(P)
    if n < 2
        return 0.0
    end
    result = dist(P.plist[1],P.plist[end])
    if n==2
        return 2*result
    end
    for j=1:n-1
        result += dist(P.plist[j],P.plist[j+1])
    end
    return result
end

function show(io::IO,X::HPolygon)
    print(io,"HPolygon with $(npoints(X)) points")
end

# require 0 <= k < n
"""
`_cycle(A,k)` returns a `k`-step shift of `A`. We require
`k` to be in the interval `[0,n-1]` where `n=length(A)`.
No checking is done.
"""
function _cycle(A::Array{T,1}, k::Int) where T
    n = length(A)
    B = Array{T,1}(undef,n)
    for t = 1:n-k
        @inbounds B[t] = A[t+k]
    end
    for t = 1:k
        @inbounds B[n-k+t] = A[t]
    end
    return B
end

"""
`_cyclic_equal(A,B)` checks if some cyclic shift of one list
equals the other.
"""
function _cyclic_equal(A::Array{S,1}, B::Array{T,1}) where {S,T}
    n = length(A)
    if length(B) != n
        return false
    end
    for s=0:n-1
        if A == _cycle(B,s)
            return true
        end
    end
    return false
end

(==)(X::HPolygon, Y::HPolygon) = _cyclic_equal(X.plist, Y.plist) ||
    _cyclic_equal(X.plist,reverse(Y.plist))

"""
`is_simple(X::HPolygon)` determines if the polygon edges do not self-intersect.
Be sure the polygon is legit using `polygon_check` first.
"""
function is_simple(X::HPolygon)
    n = npoints(X)
    if n <= 3
        return true   # triangles are fine
    end
    slist = sides(X)
    for i=1:n-2
        for j=i+2:n
            if i==1 && j==n
                continue
            end
            if meet_check(slist[i], slist[j])
                return false
            end
        end
    end
    return true
end


# # Example (Ch. 9 Bathe)

#md # [![](https://img.shields.io/badge/show-nbviewer-579ACA.svg)](@__NBVIEWER_ROOT_URL__/models/example_9_1_Bathe.ipynb)
#

# The example considered next can be found in Chapter 9 of [[BATHE]](@ref).

using StructuralDynamicsODESolvers, Plots

# ## Problem formulation
M = [2 0; 0 1.]
K = [6 -2; -2 4.]
C = zeros(2, 2)
f = [0.0, 10.0]
example_9_1_Bathe = SecondOrderAffineContinuousSystem(M, C, K, f)

NSTEPS = 500
tdom = range(0, NSTEPS * 0.1, length=NSTEPS + 1)
U₀, U₀′ = zeros(2), zeros(2)
prob = InitialValueProblem(example_9_1_Bathe, (U₀, U₀′))

# ## Analytic solution
A = [1/√3  (1/2)*√(2/3);
     1/√3      -√(2/3)]
x₁(t) = (5 / √3) * (1 - cos(t*√2))
x₂(t) = (2 * √(2/3)) * (-1 + cos(t*√5))
U(t) = A * [x₁(t), x₂(t)]

# ## Central difference

sol = solve(prob, CentralDifference(Δt=0.1); NSTEPS=NSTEPS) |> displacements
ind = 150:170
fig = plot(xlab="time", ylab="x1(t)", legend=:outertopright)
fig2 = plot(xlab="time", ylab="x1(t)", legend=:outertopright)
plot!(fig, tdom, [s[1] for s in sol], lab="Central difference")
plot!(fig2, tdom[ind], [s[1] for s in sol[ind]], lab="Central difference")

# ## Houbolt

sol = solve(prob, Houbolt(Δt=0.1); NSTEPS=NSTEPS) |> displacements
plot!(fig, tdom, [s[1] for s in sol], lab="Houbolt")
plot!(fig2, tdom[ind], [s[1] for s in sol[ind]], lab="Houbolt")

# ## Newmark

sol = solve(prob, Trapezoidal(Δt=0.1); NSTEPS=NSTEPS) |> displacements
plot!(fig, tdom, [s[1] for s in sol], lab="Newmark")
plot!(fig2, tdom[ind], [s[1] for s in sol[ind]], lab="Newmark")

# ## Bathe

sol = solve(prob, Bathe(Δt=0.1); NSTEPS=NSTEPS) |> displacements
plot!(fig, tdom, [s[1] for s in sol], lab="Bathe")
plot!(fig2, tdom[ind], [s[1] for s in sol[ind]], lab="Bathe")

# ## Analytic solution

tdom = range(0, NSTEPS*0.1, length=1000)
plot!(fig, tdom, [U(t)[1] for t in tdom], lab="Analytic")
plot!(fig2, tdom[299:330], [U(t)[1] for t in tdom[299:330]], lab="Analytic")


export i_mean_sum, i_var_mean_sum, i_normal_logpdf, i_cor_cov

"""
    i_mean_sum(out!, sum!, x)

get the `mean` and `sum` of `x`.
"""
@i function i_mean_sum(out!, sum!, x)
    for i=1:length(x)
        sum! += x[i]
    end
    out! += sum!/length(x)
end

"""
    i_var_mean_sum(var!, varsum!, mean!, sum!, sqv)

Compute the variance, the accumulated variance, mean and sum.
"""
@i function i_var_mean_sum(var!, varsum!, mean!, sum!, v::AbstractVector{T}) where T
    i_mean_sum(mean!, sum!, v)
    for i=1:length(v)
        v[i] -= mean!
        varsum! += v[i] ^ 2
        v[i] += mean!
    end
    var! += varsum! / (length(v)-1)
end

"""
    i_normal_logpdf(out, x, μ, σ)

get the pdf of `Normal(μ, σ)` at point `x`.
"""
@i function i_normal_logpdf(out, x::T, μ, σ) where T
    @zeros T anc1 anc2 anc3

    @routine begin
        anc1 += x
        anc1 -= μ
        anc2 += anc1 / σ  # (x- μ)/σ
        anc3 += anc2^2 # (x-μ)^2/σ^2
    end

    out -= anc3 * 0.5 # -(x-μ)^2/2σ^2
    out -= log(σ) # -(x-μ)^2/2σ^2 - log(σ)
    out -= log(2π)/2 # -(x-μ)^2/2σ^2 - log(σ) - log(2π)/2

    ~@routine
end

"""
     i_cor_cov(rho!,cov!,a,b)

get Pearson correlation and covariance of two vectors `a` and `b`

"""

@i function i_cor_cov(rho!::T,cov!::T,a::AbstractVector{T},b::AbstractVector{T}) where T
    @safe @assert length(a) == length(b)
    @routine  @invcheckoff begin
        @zeros T var1 varsum1 mean1 sum1 std1
        i_var_mean_sum(var1, varsum1, mean1, sum1, a)
        std1 += sqrt(var1)
        @zeros T var2 varsum2 mean2 sum2 std2
        i_var_mean_sum(var2, varsum2, mean2, sum2, b)
        std2 += sqrt(var2)
        @zeros T anc3 anc4 anc5 anc6 anc7
        @inbounds for i=1:length(b)
            a[i] -= mean1
            anc3 += a[i]
            b[i] -= mean2
            anc4 += b[i]
            anc5 += anc3*anc4
            anc3 -= a[i]
            anc4 -= b[i]
            a[i] += mean1
            b[i] += mean2
        end
        anc6+=std1*std2
        anc7+=anc6*(length(b)-1)
    end
        cov! += anc5/(length(b)-1)
        rho! += anc5/anc7
    ~@routine
end


# Represents an N-dimensional ellipsoid
struct Ellipsoid
    ctr::Vector{Float64}  # center coordinates
    cov::Array{Float64, 2}
    icov::Array{Float64, 2}  # inverse of cov
    vol::Float64
end

# Draw a random point from within a unit N-ball
function randnball(ndim)
    z = randn(ndim)
    r2 = 0.
    for i=1:ndim
        r2 += z[i]*z[i]
    end
    factor = rand()^(1. /ndim) / sqrt(r2)
    for i=1:ndim
        z[i] *= factor
    end
    return z
end

# proportionality constant depending on dimension
# for n even:      (2pi)^(n    /2) / (2 * 4 * ... * n)
# for n odd :  2 * (2pi)^((n-1)/2) / (1 * 3 * ... * n)
function nball_vol_factor(ndim::Int)
    if ndim % 2 == 0
        c = 1.
        for i=2:2:ndim
            c *= 2pi / i
        end
        return c
    else
        c = 2.
        for i = 3:2:ndim
            c *= 2pi / i
        end
        return c
    end
end

function ellipsoid_volume(scaled_cov::Matrix{Float64})
    ndim = size(scaled_cov, 1)
    return nball_vol_factor(ndim) * sqrt(det(scaled_cov))
end

# find the bounding ellipsoid of points x where
function bounding_ellipsoid(x::Matrix{Float64}, enlarge=1.0)

    ndim, npoints = size(x)

    ctr = mean(x, dims=2)[:, 1]
    delta = x .- ctr
    cov = unscaled_covzm(delta, 2)
    icov = inv(cov)

    # Calculate expansion factor necessary to bound each point.
    # This finds the maximum of (delta_i' * icov * delta_i)
    fmax = -Inf
    for k in 1:npoints
        f = 0.0
        for j=1:ndim
            for i=1:ndim
                f += icov[i, j] * delta[i, k] * delta[j, k]
            end
        end
        fmax = max(fmax, f)
    end

    fmax *= enlarge
    cov .*= fmax
    icov .*= 1. /fmax
    vol = ellipsoid_volume(cov)

    return Ellipsoid(ctr, cov, icov, vol)
end

function sample_ellipsoid(ell::Ellipsoid)
    ndim = length(ell.ctr)

    # Get scaled eigenvectors (in columns): vs[:,i] is the i-th eigenvector.
    f = eigen(ell.cov)
    v, w = f.vectors, f.values
    for j=1:ndim
        tmp = sqrt(abs(w[j]))
        for i=1:ndim
            v[i, j] *= tmp
        end
    end

    return v*randnball(ndim) + ell.ctr
end


module HermiteNormalForm

using LinearAlgebra
export hnf!, hnf, ishnf

"""
    ishnf(A)

Check if matrix `A` is in Hermite norm form.
"""
function ishnf(A)
    m, n = size(A)
    for j in 1:n
        for i in 1:min(m, j-1)
            A[i, j] == 0 || return false
        end
        if m >= j
            A[j, j] > 0 || return false
        end
    end
    for i in 1:min(m, n)
        p = A[i, i]
        for j in 1:min(n, i-1)
            0 <= A[i, j] < p || return false
        end
    end
    return true
end

"""
    H, U = hnf(A)

`U` is unimodular and `H` is the Hermite normal form of `A`. If `A` is singular,
then it returns `nothing`.
"""
hnf(A) = hnf!(copy(A))

function hnf!(A)
    m, n = size(A)
    T = eltype(A)
    U = Matrix{T}(I, n, n)
    zz = zero(T)
    for k in 1:m
        # Pivot: A[k, k] should not be 0
        if n >= k && A[k, k] == zz
            pivot = k
            for j in k+1:n
                if A[k, j] != zz
                    pivot = j
                end
            end
            pivot == k && return nothing # rank deficient
            for i in 1:m
                A[i, k], A[i, pivot] = A[i, pivot], A[i, k]
            end
            for i in 1:n
                U[i, k], U[i, pivot] = U[i, pivot], U[i, k]
            end
        end
        # [A11   0 ] k-1 rows
        # [A21  A22]
        # Zero out A[k, k+1:n] === A22[1, 2:end] by multiplying
        # [p  -A[k, j]/d]
        # [q   A[k, k]/d]
        for j in k+1:n
            Akk, Akj = A[k, k], A[k, j]
            d, p, q = gcdx(Akk, Akj)
            Akkd, Akjd = div(Akk, d), div(Akj, d)
            for i in 1:m
                Aik, Aij = A[i, k], A[i, j]
                A[i, k] = Aik * p + Aij * q
                A[i, j] = -Aik * Akjd + Aij * Akkd
            end
            for i in 1:n
                Uik, Uij = U[i, k], U[i, j]
                U[i, k] = Uik * p + Uij * q
                U[i, j] = -Uik * Akjd + Uij * Akkd
            end
        end
        n >= k || continue
        # Ensure the positivity of A[k, k]
        if A[k, k] < zz
            @. A[:, k] = -A[:, k]
            @. U[:, k] = -U[:, k]
        end
        # Minimize A[k, 1:k-1] === A21[1, :]
        for j in 1:k-1
            mul = fld(A[k, j], A[k, k])
            @. A[:, j] -= mul * A[:, k]
            @. U[:, j] -= mul * U[:, k]
        end
    end
    A, U
end

end


export spgdemo_ex, spgdemo_im
using JOLI

function spgdemo_ex()

    # Create random m-by-n encoding matric and sparse vector
    m = 50; n = 128; k = 14
    A,Rtmp = qr(randn(n,m))
    A = A'
    p = randperm(n)
    p = p[1:k]
    x0 = zeros(n,1)
    x0[p] = randn(k,1);

    b = A*x0
    mytau=convert(Float64,pi)

    # Optionally set the verbosity to level 1
    opts = spgOptions(verbosity = 1)

    x, r, g, info = spgl1(A, vec(b); tau = mytau, options = opts)

    # Calc the SNR of the recovery
    SNR = -20*log10(norm(x0-x)/norm(x0));

    return x, r, g, info, SNR

end

function spgdemo_im()

    # Create random m-by-n encoding matric and sparse vector
    m = 50; n = 128; k = 14
    A,Rtmp = qr(randn(n,m))
    A = A'
    p = randperm(n)
    p = p[1:k]
    x0 = zeros(n,1)
    x0[p] = randn(k,1);

    b = A*x0
    mytau=convert(Float64,pi)

    # LASSO
    sigma = []
    x0=[]

    # Optionally set the verbosity to level 1
    opts = spgOptions(verbosity = 1)

    A_im = joMatrix(A; name="A")
    x, r, g, info = spgl1(A_im, vec(b); tau = mytau, options = opts)

    # Calc the SNR of the recovery
    SNR = -20*log10(norm(x0-x)/norm(x0));

    return x, r, g, info, SNR
end


@testset "basic building blocks" begin
    mt = UltimateMortality([0.5,0.5])

    # whole life insurance
    ins = Insurance(
            SingleLife(mort = mt,issue_age = 0),
            Yields.Constant(0.05)
    )

    @test all(survival(SingleLife(mort = mt,issue_age = 0)) .== [1.0, 0.5,0.25])

    @test timepoints(ins) == [1.0,2.0]
    @test survival(ins) == [1.0,0.5]
    @test discount(ins) == [1.0 / 1.05, 1 / 1.05^2]
    @test benefit(ins) == [1.0,1.0]
    @test probability(ins) == [0.5,0.25]
    @test cashflows(ins) == [0.5,0.25]
    @test cashflows(ins) == benefit(ins) .* probability(ins)
    @test present_value(ins)  ≈ 0.5 / 1.05 + 0.5 * 0.5 / 1.05 ^ 2
    @test present_value(ins) ≈ LifeContingencies.A(ins.life,ins.int)

    # basic life contingency tests
    @test survival(LifeContingency(ins),0,1) ≈ 0.5
    @test survival(LifeContingency(ins),0,2) ≈ 0.25
    @test discount(LifeContingency(ins),0,1) ≈ 1/1.05
    @test discount(LifeContingency(ins),0,2) ≈ 1/1.05^2

    # term life insurance
    ins = Insurance(
            SingleLife(mort = mt,issue_age = 0),
            Yields.Constant(0.05),
            n=1
    )

    @test survival(ins) == [1.0]
    @test discount(ins) == [1.0 / 1.05]
    @test benefit(ins) == [1.0]
    @test probability(ins) == [0.5]
    @test cashflows(ins) == [0.5]
    @test cashflows(ins) == benefit(ins) .* probability(ins)
    @test timepoints(ins) == [1.0]
    @test present_value(ins)  ≈ 0.5 / 1.05

    # annuity due
    ins = AnnuityDue(
            SingleLife(mort = mt,issue_age = 0),
            Yields.Constant(0.05)
    )

    @test survival(ins) == [1.0,0.5,.25]
    @test discount(ins) == [1.0, 1 / 1.05^1, 1 / 1.05^2]
    @test benefit(ins) == [1.0,1.0,1.0]
    @test timepoints(ins) == [0.0,1.0,2.0]
    @test probability(ins) == [1.,0.5,0.25]
    @test cashflows(ins) == [1.0,0.5,0.25]
    @test cashflows(ins) == benefit(ins) .* probability(ins)
    @test present_value(ins)  ≈ 1 + 1 * .5 / 1.05 +  1 * .25 / 1.05 ^2

    ins = AnnuityDue(
        SingleLife(mort = mt,issue_age = 0),
        Yields.Constant(0.05),
        n = 2
)
    @test present_value(ins)  ≈ 1 + 1 * .5 / 1.05

    ins = AnnuityImmediate(
        SingleLife(mort = mt,issue_age = 0),
        Yields.Constant(0.05),
        n = 1
    )

    @test present_value(ins)  ≈ 1 * .5 * 1 / 1.05

    ins = AnnuityDue(
        SingleLife(mort = mt,issue_age = 0),
        Yields.Constant(0.05),
        n = 1,
        start_time=1
    )

    @test timepoints(ins)  == [1.0]
    @test probability(ins)  == [0.5]
    @test present_value(ins)  ≈ 1 * .5 * 1 / 1.05

end

@testset "one  year" begin
    ins = LifeContingency(
        SingleLife(
            mort = UltimateMortality([0.5]),
            issue_age = 0
        ),
        Yields.Constant(0.05)
    )

    @test survival(ins,1) ≈ 0.5
    @test survival(ins,0) ≈ 1.0
    @test survival(ins.life,0,0.5) ≈ 1 - 0.5 * 0.5
    @test survival(ins,0.5) ≈ 1 - 0.5 * 0.5

    @test omega(ins) ≈ 1
    @test present_value(AnnuityDue(ins)) ≈ 1 + 1 * .5 / 1.05
    @test present_value(AnnuityDue(ins,n=1)) ≈ 1
    @test present_value(AnnuityDue(ins,n=0)) == 0
    @test present_value(AnnuityImmediate(ins,n=1)) ≈ 1 * .5 / 1.05
    @test present_value(AnnuityImmediate(ins,n=0)) == 0

    @test present_value(Insurance(ins)) ≈ 0.5 / 1.05
    @test present_value(Insurance(ins,n=1)) ≈ 0.5 / 1.05
    @test present_value(Insurance(ins,n=0)) ≈ 0

    ins_jl = LifeContingency(
        JointLife((ins.life,ins.life),LastSurvivor(),Frasier()),
        Yields.Constant(0.05)
    )

    @test omega(ins_jl) ≈ 1
    @test present_value(AnnuityDue(ins_jl)) ≈ 1 + 1 * .75 / 1.05
    @test present_value(AnnuityDue(ins_jl,n=1)) ≈ 1
    @test present_value(AnnuityImmediate(ins_jl,n=1)) ≈ 1 * .75 / 1.05
    @test present_value(AnnuityDue(ins_jl,n=2)) ≈ 1 + 1 * .75 / 1.05
    @test present_value(AnnuityDue(ins_jl,n=2;certain=2)) ≈ 1 + 1 / 1.05
    @test present_value(AnnuityDue(ins_jl,n=0)) == 0

    @test survival(ins_jl,1) ≈ .5 + .5 - .5 * .5
    @test all(survival(JointLife((ins.life,ins.life),LastSurvivor(),Frasier())) .== [1.,.75])
    @test present_value(Insurance(ins_jl)) ≈ .25 / 1.05
    @test present_value(Insurance(ins_jl,n=1)) ≈ 0.25 / 1.05
    @test present_value(Insurance(ins_jl,n=0)) ≈ 0
end

@testset "two year no discount" begin
    ins = LifeContingency(
        SingleLife(
            mort = UltimateMortality([0.0,0.0]),
            issue_age = 0
        ),
        Yields.Constant(0.00)
    )

    @test omega(ins) ≈ 2
    @test present_value(AnnuityDue(ins)) ≈ 3
    @test present_value(AnnuityDue(ins;start_time=1)) ≈ 2
    @test present_value(AnnuityDue(ins,n=1)) ≈ 1
    @test present_value(AnnuityDue(ins,n=2)) ≈ 2
    @test_throws BoundsError present_value(AnnuityDue(ins,n=2;start_time=2))
    @test present_value(AnnuityDue(ins,n=3)) ≈ 3
    @test present_value(AnnuityDue(ins,n=0)) == 0
    @test present_value(AnnuityImmediate(ins,n=0)) == 0
    @test present_value(AnnuityImmediate(ins,n=1)) ≈ 1
    @test present_value(AnnuityImmediate(ins,n=1;start_time=1)) ≈ 1
    @test_throws BoundsError  present_value(AnnuityImmediate(ins;start_time=2))
    @test present_value(AnnuityImmediate(ins,n=2)) ≈ 2
    @test present_value(AnnuityImmediate(ins)) ≈ 2
    @test present_value(AnnuityImmediate(ins;certain=0)) ≈ 2
    @test present_value(AnnuityImmediate(ins;certain=2)) ≈ 2

    @test present_value(Insurance(ins)) ≈ 0
    @test present_value(Insurance(ins,n=1)) ≈ 0
    @test present_value(Insurance(ins,n=0)) ≈ 0

    ins_jl = LifeContingency(
        JointLife(
            lives = (ins.life,ins.life),
            contingency = LastSurvivor(),
            joint_assumption = Frasier()),
        Yields.Constant(0.00)
    )

    @test survival(ins_jl,0) ≈ 1.0
    @test survival(ins_jl,1) ≈ 1
    @test survival(ins_jl,2) ≈ 1

end

@testset "two year with interest" begin
    ins = LifeContingency(
        SingleLife(
            mort = UltimateMortality([0.5,0.5]),
            issue_age = 0
        ),
        Yields.Constant(0.05)
    )

    @test omega(ins) ≈ 2
    @test present_value(AnnuityDue(ins)) ≈ 1 + 1 * .5 * 1 / 1.05 +  1 * .25 / 1.05 ^2
    @test present_value(AnnuityDue(ins,n=1)) ≈ 1
    @test present_value(AnnuityDue(ins,n=2)) ≈ 1 + 1 * .5 * 1 / 1.05
    @test present_value(AnnuityDue(ins,n=3)) ≈ 1 + 1 * .5 * 1 / 1.05 +  1 * .25 / 1.05 ^2
    @test present_value(AnnuityDue(ins,n=0)) ≈ 0
    @test present_value(AnnuityImmediate(ins,n=0)) ≈ 0
    @test present_value(AnnuityImmediate(ins,n=1)) ≈ 1 * .5 * 1 / 1.05

    @test present_value(Insurance(ins)) ≈ 0.5 / 1.05 + 0.5 * 0.5 / 1.05 ^ 2
    @test present_value(Insurance(ins,n=1)) ≈ 0.5 / 1.05
    @test present_value(Insurance(ins,n=0)) ≈ 0

    ins_jl = LifeContingency(
        JointLife(
            lives = (ins.life,ins.life),
            contingency = LastSurvivor(),
            joint_assumption = Frasier()),
        Yields.Constant(0.05)
    )

    @test survival(ins_jl,0) ≈ 1.0
    @test survival(ins_jl,1) ≈ .5 + .5 - .5 * .5
    @test survival(ins_jl,2) ≈ (.25 + .25 - .25 * .25)

end

@testset "two years" begin
    ins = LifeContingency(
        SingleLife(
            mort = UltimateMortality([0.5,0.5]),
            issue_age = 0
        ),
        Yields.Constant(0.05)
    )

    @test omega(ins) ≈ 2
    @test present_value(AnnuityDue(ins,n=1)) ≈ 1
    @test present_value(AnnuityDue(ins,n=2)) ≈ 1 + 1 * .5 * 1 / 1.05
    @test present_value(AnnuityDue(ins,n=3)) ≈ 1 + 1 * .5 * 1 / 1.05 + 1 * .25 * 1 / 1.05 ^ 2
    @test present_value(AnnuityDue(ins,n=0)) == 0
    @test present_value(AnnuityImmediate(ins,n=0)) == 0
    @test present_value(AnnuityImmediate(ins,n=1)) ≈ 1 * .5 * 1 / 1.05

    @test present_value(Insurance(ins)) ≈ 0.5 / 1.05 + 0.5 * 0.5 / 1.05 ^ 2
    @test present_value(Insurance(ins,n=1)) ≈ 0.5 / 1.05
    @test present_value(Insurance(ins,n=0)) ≈ 0

    ins_jl = LifeContingency(
        JointLife(
            lives = (ins.life,ins.life),
            contingency = LastSurvivor(),
            joint_assumption = Frasier()),
        Yields.Constant(0.05)
    )

    @test omega(ins_jl) ≈ 2
    @test present_value(AnnuityDue(ins_jl,n=1)) ≈ 1
    @test present_value(AnnuityDue(ins_jl,n=2)) ≈ 1 + 1 * .75 /1.05
    @test present_value(AnnuityDue(ins_jl,n=3)) ≈ 1 + 1 * .75 /1.05 + 1 * survival(ins_jl,2) / 1.05 ^ 2
    @test present_value(AnnuityDue(ins_jl,n=0)) == 0
    @test present_value(AnnuityImmediate(ins_jl,n=0)) == 0
    @test present_value(AnnuityImmediate(ins_jl,n=1)) ≈ 1 * .75 /1.05

end

# assumes embedded 'testMort' table in combination with MortalityTables
t = UltimateMortality(maleMort)

@testset "demo mortality" begin
    i = Yields.Constant(0.05)
    ins = LifeContingency(SingleLife(mort = t, issue_age = 0), i)

    @test l(ins, 0) ≈ 1.0
    @test l(ins, 1) ≈ 0.993010000000000
    @test l(ins, 2) ≈ 0.992566124530000

    @test D(ins, 0) ≈ 1.0
    @test D(ins, 1) ≈ 0.945723809523809
    @test D(ins, 2) ≈ 0.900286734267574

    @test N(ins, 0) ≈ 20.113017073119200
    @test N(ins, 1) ≈ 19.113017073119200
    @test N(ins, 2) ≈ 18.167293263595400

    @test C(ins, 0) ≈ 0.006657142857142910
    @test C(ins, 1) ≈ 0.000402608136054441
    @test C(ins, 2) ≈ 0.000258082197156658

    @test M(ins, 0) ≈ 0.0422372822324182
    @test M(ins, 1) ≈ 0.0355801393752753
    @test M(ins, 2) ≈ 0.0351775312392208

    @test present_value(Insurance(ins   )) ≈ 0.04223728223
    @test present_value(Insurance(ins, n=0)) ≈ 0.0
    @test present_value(Insurance(ins, n=1)) ≈ 0.0066571428571429
    @test present_value(AnnuityDue(ins, n=0)) ≈ 0.0
    @test present_value(AnnuityDue(ins, n=1)) ≈ 1.0
    @test present_value(AnnuityDue(ins, n=2)) ≈ 1.0 + survival(ins,1) / 1.05
    @test present_value(AnnuityImmediate(ins, n=0)) ≈ 0.0
    @test present_value(AnnuityImmediate(ins, n=1)) ≈ survival(ins,1) / 1.05

    @test present_value(Insurance(ins, n=30)) ≈ 0.0137761089686975

    @test N(ins, 26) ≈ 5.156762988852310
    @test D(ins, 26) ≈ 0.275358702015970
    @test present_value(AnnuityDue(ins, n=26)) ≈ 14.9562540842669

end


using Test
using HaltonSequences

@testset "values" begin
    @test haltonvalue(1,2) ≈ 0.5
    @test haltonvalue(111111,2) ≈ 0.876182556152344
    @test haltonvalue(789,19) ≈ 0.534917626476163
    @test Halton{Rational}(17)[24] == 120//289
end

@testset "integrals" begin
    @test sum( Halton(5)[100:20100] )/20000 ≈ 0.5 atol=0.002
    p = HaltonPoint(4,length=32000)
    @test sum(p)/32000 ≈ [0.5,0.5,0.5,0.5] atol=0.001
    h = Halton{Float32}(29,start=1234,length=22000)
    @test sum( h.^2 )/22000 ≈ 1/3 atol=0.001
end


@testset "Set distances" begin
    @testset "$n-dimensional orthants" for n in 1:3:15
        v = rand(n)
        @test MOD.distance_to_set(MOD.DefaultDistance(), v, MOI.Reals(n)) ≈ 0 atol=eps(Float64)
        @test MOD.distance_to_set(MOD.DefaultDistance(), v, MOI.Zeros(n)) > 0
        @test MOD.distance_to_set(MOD.DefaultDistance(), v, MOI.Nonnegatives(n)) ≈ 0 atol=eps(Float64)
        @test MOD.distance_to_set(MOD.DefaultDistance(), -v, MOI.Nonpositives(n)) ≈ 0 atol=eps(Float64)
        @test MOD.distance_to_set(MOD.DefaultDistance(), v, MOI.Nonpositives(n)) ≈ MOD.distance_to_set(MOD.DefaultDistance(), -v, MOI.Nonnegatives(n)) > 0
    end

    @testset "Scalar comparisons" begin
        values = rand(10)
        for v in values
            @test MOD.distance_to_set(MOD.DefaultDistance(), v, MOI.EqualTo(v)) ≈ 0 atol=eps(Float64)
            @test MOD.distance_to_set(MOD.DefaultDistance(), -v, MOI.EqualTo(v)) ≈ MOD.distance_to_set(MOD.DefaultDistance(), v, MOI.EqualTo(-v)) ≈ 2v
            @test MOD.distance_to_set(MOD.DefaultDistance(), v, MOI.LessThan(v)) ≈ MOD.distance_to_set(MOD.DefaultDistance(), v, MOI.LessThan(v+1)) ≈ 0
            @test MOD.distance_to_set(MOD.DefaultDistance(), v, MOI.LessThan(0)) ≈ MOD.distance_to_set(MOD.DefaultDistance(), -v, MOI.GreaterThan(0)) ≈ v
            @test MOD.distance_to_set(MOD.DefaultDistance(), v, MOI.GreaterThan(v)) ≈ MOD.distance_to_set(MOD.DefaultDistance(), v+1, MOI.GreaterThan(v+1)) ≈ 0
            @test MOD.distance_to_set(MOD.DefaultDistance(), v, MOI.Interval(v,v)) ≈ MOD.distance_to_set(MOD.DefaultDistance(), v, MOI.Interval(-v,v)) ≈ 0
            @test MOD.distance_to_set(MOD.DefaultDistance(), v, MOI.Interval(-v, 0.0)) ≈ MOD.distance_to_set(MOD.DefaultDistance(), -v, MOI.Interval(0.0, v)) ≈ v
        end
    end
    @testset "$n-dimensional norm cones" for n in 2:5:15
        x = rand(n)
        tsum = sum(x)
        vsum = vcat(tsum, x)
        @test MOD.distance_to_set(MOD.DefaultDistance(), vsum, MOI.NormOneCone(n+1)) ≈ MOD.distance_to_set(MOD.DefaultDistance(), vsum, MOI.NormInfinityCone(n+1)) ≈ 0
        tmax = maximum(x)
        vmax = vcat(tmax, x)
        @test MOD.distance_to_set(MOD.DefaultDistance(), vmax, MOI.NormOneCone(n+1)) > 0
        @test MOD.distance_to_set(MOD.DefaultDistance(), vmax, MOI.NormInfinityCone(n+1)) ≈ 0 atol=eps(Float64)
        tmin = 0
        vmin = vcat(tmin, x)
        @test MOD.distance_to_set(MOD.DefaultDistance(), vmin, MOI.NormInfinityCone(n+1)) ≈ tmax
        @test MOD.distance_to_set(MOD.DefaultDistance(), vmin, MOI.NormOneCone(n+1)) ≈ tsum

        tvalid = sqrt(n) # upper bound on the norm2
        vok_soc = vcat(tvalid, x)
        @test MOD.distance_to_set(MOD.DefaultDistance(), vok_soc, MOI.SecondOrderCone(n+1)) ≈ 0 atol=eps(Float64)
        vko_soc = vcat(-1.5 * tvalid, x)
        @test MOD.distance_to_set(MOD.DefaultDistance(), vko_soc, MOI.SecondOrderCone(n+1)) ≈ sqrt(9/4 * n + LinearAlgebra.norm(x)^2)

        t_ko_rot = u_ko_rot = LinearAlgebra.norm2(x) / 2
        vko_roc = vcat(t_ko_rot, u_ko_rot, x)
        @test MOD.distance_to_set(MOD.DefaultDistance(), vko_roc, MOI.RotatedSecondOrderCone(n+2)) ≈ LinearAlgebra.dot(x,x) / 2
        vok_roc = vcat(t_ko_rot * 2, u_ko_rot, x)
        @test MOD.distance_to_set(MOD.DefaultDistance(), vok_roc, MOI.RotatedSecondOrderCone(n+2)) ≈ 0 atol=10eps(Float64)
    end

    @testset "Geometric Mean cone dimension $n" for n in 2:5:15
        x = rand(n)
        t = 0.5 * prod(x)^(inv(n))
        vok = vcat(t, x)
        @test MOD.distance_to_set(MOD.DefaultDistance(), vok, MOI.GeometricMeanCone(n+1)) ≈ 0 atol=eps(Float64)
        @test MOD.distance_to_set(MOD.DefaultDistance(), vcat(t / 2, x), MOI.GeometricMeanCone(n+1)) ≈ 0 atol=eps(Float64)
        # negative x always means positive distance
        @test MOD.distance_to_set(MOD.DefaultDistance(), vcat(t / 2, vcat(x, -1)), MOI.GeometricMeanCone(n+2)) > 0
        @test MOD.distance_to_set(MOD.DefaultDistance(), vcat(t / 2, -x), MOI.GeometricMeanCone(n+1)) > 0
    end

    @testset "Exponential and power cones" begin
        for _ in 1:30
            (x, y, z) = rand(3)
            y += 1 # ensure y > 0
            if y * exp(x/y) <= z
                @test MOD.distance_to_set(MOD.DefaultDistance(), [x, y, z], MOI.ExponentialCone()) ≈ 0 atol=eps(Float64)
                @test MOD.distance_to_set(MOD.DefaultDistance(), [x, -1, z], MOI.ExponentialCone()) ≈ 1 atol=eps(Float64)
            else
                @test MOD.distance_to_set(MOD.DefaultDistance(), [x, y, z], MOI.ExponentialCone()) ≈ y * exp(x/y) - z
            end
            (u, v, w) = randn(3)
            if u != 0.0 # just in case not to blow up
                if -u*exp(v/u) < ℯ * w && u < 0
                    @test MOD.distance_to_set(MOD.DefaultDistance(), [u, v, w], MOI.DualExponentialCone()) ≈ 0 atol=eps(Float64)
                elseif u < 0
                    @test MOD.distance_to_set(MOD.DefaultDistance(), [u, v, w], MOI.DualExponentialCone()) ≈ -u*exp(v/u) - ℯ * w
                end

            end
            (x, y) = randn(2)
            if x < 0 || y < 0
                for e in  (10 * rand(10) .- 5) # e in [-5, 5]
                    @test MOD.distance_to_set(MOD.DefaultDistance(), [x, y, 0.0], MOI.PowerCone(e)) > 0
                end
            else
                for e in  (10 * rand(10) .- 5) # e in [-5, 5]
                    r = x^e * y^(1-e)
                    for z in -r:-0.5:r
                        @test MOD.distance_to_set(MOD.DefaultDistance(), [x, y, z], MOI.PowerCone(e)) ≈ 0 atol=eps(Float64)
                    end
                    @test MOD.distance_to_set(MOD.DefaultDistance(), [x, y, 3r], MOI.PowerCone(e)) ≈ MOD.distance_to_set(MOD.DefaultDistance(), [x, y, -3r], MOI.PowerCone(e)) > 0
                end
            end

            (u, v, w) = 10 * rand(3)
            e = rand()
            if 0 < e < 1 # avoid exponents of negatives
                @test MOD.distance_to_set(MOD.DefaultDistance(), [u, v, 0.0], MOI.DualPowerCone(e)) ≈ 0 atol = 10eps(Float64)
                @test MOD.distance_to_set(MOD.DefaultDistance(), [u, v, u^e * v^(1-e) / (e^e * (1-e)^(1-e))], MOI.DualPowerCone(e)) ≈ 0 atol=100eps(Float64)
                @test MOD.distance_to_set(MOD.DefaultDistance(), [u, v, 1 + u^e * v^(1-e) / (e^e * (1-e)^(1-e))], MOI.DualPowerCone(e)) ≈ 1
            end
        end
    end
end

struct DummyDistance <: MOD.AbstractDistance end

MOD.distance_to_set(::DummyDistance, v, s) = MOD.distance_to_set(MOD.DefaultDistance(), v, s) / 2

@testset "Set non-default distance" begin
    for n in 1:3
        v = rand(n)
        for s in (MOI.Reals(n), MOI.Zeros(n), MOI.Nonnegatives(n), MOI.Nonpositives(n))
            @test MOD.distance_to_set(MOD.DefaultDistance(), v, MOI.Reals(n)) ≈ 2 * MOD.distance_to_set(DummyDistance(), v, MOI.Reals(n)) atol=eps(Float64)
            @test MOD.distance_to_set(MOD.DefaultDistance(), -v, MOI.Reals(n)) ≈ 2 * MOD.distance_to_set(DummyDistance(), -v, MOI.Reals(n)) atol=eps(Float64)
        end
    end
end

@testset "Distance is composition of projection" begin
    for n in (1, 2, 10)
        @testset "Second order cone $n" begin
            s = MOI.SecondOrderCone(n+1)
            for _ in 1:10
                x = randn(n)
                for t in (-1, 0, 0.5, LinearAlgebra.norm(x), 2LinearAlgebra.norm(x))
                    v = vcat(t, x)
                    dist = MOD.distance_to_set(MOD.DefaultDistance(), v, s)
                    dist_proj = LinearAlgebra.norm2(MOD.projection_on_set(MOD.DefaultDistance(), v, s) - v)
                    @test dist ≈ dist_proj atol=10e-5
                    if dist ≈ 0
                        @test MOD.distance_to_set(MOD.EpigraphViolationDistance(), v, s) ≈ 0
                    end
                end
            end
        end
        @testset "PSD cone $n" begin
            s = MOI.PositiveSemidefiniteConeTriangle(n)
            X = Matrix{Float64}(undef, n, n)
            Xm = Matrix{Float64}(undef, n, n)
            Xp = Matrix{Float64}(undef, n, n)
            for _ in 1:10
                X .= randn(n, n)
                X .= (X .+ X')
                v = MOD.vec_symm(X)
                vproj = MOD.projection_on_set(MOD.DefaultDistance(), v, s)
                dist = MOD.distance_to_set(MOD.DefaultDistance(), v, s)
                @test LinearAlgebra.norm(v - vproj) ≈ dist atol=10e-5
                dist1 = MOD.distance_to_set(MOD.NormedEpigraphDistance{1}(), v, s)
                @test LinearAlgebra.norm(v - vproj, 1) ≈ dist1 atol=10e-5
                λ = LinearAlgebra.eigvals(X)
                if λ[1] >= 0
                    Xm = X - (λ[1] + λ[end])/2 * LinearAlgebra.I
                    v = MOD.vec_symm(Xm)
                    vproj = MOD.projection_on_set(MOD.DefaultDistance(), v, s)
                    dist = MOD.distance_to_set(MOD.DefaultDistance(), v, s)
                    @test LinearAlgebra.norm(v - vproj) ≈ dist atol=10e-5
                elseif λ[end] <= 0
                    Xp = X + (λ[1] + λ[end])/2 * LinearAlgebra.I
                    v = MOD.vec_symm(Xp)
                    vproj = MOD.projection_on_set(MOD.DefaultDistance(), v, s)
                    dist = MOD.distance_to_set(MOD.DefaultDistance(), v, s)
                    @test LinearAlgebra.norm(v - vproj) ≈ dist atol=10e-5
                end
            end
        end
    end
end


# Hessian Eigenmaps (HLLE)
# ---------------------------
# Hessian eigenmaps: Locally linear embedding techniques for high-dimensional data,
# D. Donoho and C. Grimes, Proc Natl Acad Sci U S A. 2003 May 13; 100(10): 5591–5596

#### HLLE type
immutable HLLE{T <: Real} <: SpectralResult
    k::Int
    λ::AbstractVector{T}
    proj::Projection{T}

    HLLE{T}(k::Int, λ::AbstractVector{T}, proj::Projection{T}) = new(k, λ, proj)
end

## properties
outdim(M::HLLE) = size(M.proj, 1)
projection(M::HLLE) = M.proj

eigvals(M::HLLE) = M.λ
neighbors(M::HLLE) = M.k

## show & dump
function show(io::IO, M::HLLE)
    print(io, "Hessian Eigenmaps(outdim = $(outdim(M)), neighbors = $(neighbors(M)))")
end

function dump(io::IO, M::HLLE)
    show(io, M)
    println(io, "eigenvalues: ")
    Base.showarray(io, M.λ', header=false, repr=false)
    println(io)
    println(io, "projection:")
    Base.showarray(io, M.proj, header=false, repr=false)
end

## interface functions
function transform{T<:Real}(::Type{HLLE}, X::DenseMatrix{T}; d::Int=2, k::Int=12)
    n = size(X, 2)

    # Identify neighbors
    D, I = find_nn(X, k)

    # Obtain tangent coordinates and develop Hessian estimator
    hs = Int(d*(d+1)/2)
    W = spzeros(hs*n,n)
    for i=1:n
        # re-center points in neighborhood
        μ = mean(X[:,I[:,i]],2)
        N = X[:,I[:,i]] .- μ
        # calculate tangent coordinates
        #tc = svdfact(N')[:U][:,1:d]
        tc = svdfact(N)[:V][:,1:d]

        # Develop Hessian estimator
        Yi = [ones(k) tc zeros(k,hs)]
        for ii=1:d
            Yi[:,d+ii+1] = tc[:,ii].^2
        end
        yi = 2(1+d)
        for (ii,jj) in combinations(1:d,2)
            Yi[:, yi] = tc[:, ii] .* tc[:, jj]
            yi += 1
        end
        F = qrfact(Yi)
        H = full(F[:Q])[:,d+2:end]'
        W[(i-1)*hs+(1:hs),I[:,i]] = H
    end

    # decomposition
    λ, V = decompose(W'*W, d)
    return HLLE{T}(k, λ, V' .* sqrt(n))
end


######### CmdStan program example  ###########

using CmdStan, Test, Statistics

ProjDir = dirname(@__FILE__)
cd(ProjDir) do

  bernoullimodel = "
  data {
    int<lower=1> N;
    int<lower=0,upper=1> y[N];
  }
  parameters {
    real<lower=0,upper=1> theta;
  }
  model {
    theta ~ beta(1,1);
    y ~ bernoulli(theta);
  }
  "

  bernoullidata = Dict("N" => 1, "y" => [0])

  global stanmodel, rc, sim

  stanmodel = Stanmodel(num_samples=1200, thin=2, name="bernoulli",
    output_format=:array, model=bernoullimodel);

  rc, sim, cnames = stan(stanmodel, bernoullidata, ProjDir,
    diagnostics=false, CmdStanDir=CMDSTAN_HOME);

  if rc == 0
    println()
    println("Test round.(mean(theta), 1) ≈ 0.3 rtol=0.1")
    @test round.(mean(sim[:,8,:]), digits=1) ≈ 0.3 rtol=0.1
  end
end # cd


using SatisfiabilityInterface, Symbolics

"Constraints representing different colours for each edge of a graph"
different_colours(E, c) = [c[i] ≠ c[j] for (i, j) in E]

"k is the number of colours"
function graph_colouring_problem(V, E, k=3)

    colours = [:red, :green, :yellow, :blue, :black][1:k]
    # c = [Num(Variable(:c, i)) for i in 1:length(V)]   # colour variables

    @variables c[1:length(V)]

    constraints =
    [
        [c[i] ∈ colours for i in 1:length(V)]

        [c[i] ≠ c[j] for (i, j) in E]  #  different_colours(E, c)
    ]

    return DiscreteCSP(constraints)
end

# cs = [Num(Variable(:c, i)) for i in 1:length(V)]   # colour variables

# constraints = [
#     [c ∈ colours for c in cs]
#     different_colours(E, cs)
# ]

V = [1, 2, 3]  # vertices
E = [(1, 2), (2, 3)]  # edges

prob = graph_colouring_problem(V, E, 2)
status, results = solve(prob)

# final_colours = [second(results[k]) for k in prob.original_vars]

# # check that it satisfies the constraint:
# all(different_neighbours(E, final_colours))

## Ring graph

function ring_graph(n=11)
    V = 1:n
    E = [(i, mod1((i+1), n)) for i in 1:n]

    return V, E
end

V, E = ring_graph(11)

prob = graph_colouring_problem(V, E, 2)
status, results = solve(prob)

prob = graph_colouring_problem(V, E, 3)
status, results = solve(prob)
status==:sat

# final_colours = [results[k] for k in prob.variables]
# all(different_neighbours(E, final_colours))

k = 3
colours = [:red, :green, :yellow, :blue, :black][1:k]

c = [Num(Variable(:c, i)) for i in 1:length(V)]   # colour variables

constraints =
    [
        [c[i] ∈ colours for i in 1:length(V)]

        [c[i] ≠ c[j] for (i, j) in E]  #  different_colours(E, c)
    ]

constraints

prob = ConstraintSatisfactionProblem(constraints)
prob2 = DiscreteCSP(prob)

solve(prob2)

k = 3


cigar(x) = x[1]^2 + 1 * sum(abs2, view(x, 2:length(x)))   # 1e6

cigtab(x) = x[1]^2 + 1 * x[end]^2 + 1e4 * sum(abs2, view(x, 2:length(x)-1)) # 1e8

function griewank(x)
  n = length(x)
  1 + (1/4000)*sum(abs2, (x.+7)) - prod(cos.((x.+7) ./ sqrt.(1:n)))
end

function rosenbrock2d(x)
  return sum((x).*(x).*cos.((x)).*sin.((x.-5)))+200
end

function quartic(x)
  D = length(x)
  sum( (1:D) .* (x.-2).^4 )
end

function schwefel1_2(x)
  D = length(x)
  partsums = zeros(D)
  partsum = 0
  for i in 1:D
    partsum += x[i]-9
    partsums[i] = partsum
  end
  sum(abs2, partsums)
end

function ellipsoid(x)
    res = 0.0
    cumsum = 0.0
    for xx in x
        cumsum += xx-sqrt(2)
        res += cumsum^2
    end
    res
end

function elliptic(x)
  D = length(x)
  condition = 1e+3
  if D==1
      coefficients=1
  else
      coefficients = condition .^ range(0,stop=1,length=D)
  end
  sum(coefficients .* (x.+3/2).^2)
end

function rastrigin(x)
  D = length(x)
  10 * D + sum(abs2, (x.+0.7)) - 10 * sum(xx -> cos.(2π * xx), (x.+0.7))
end

function sphere(x)
  sum(abs2, (x.-1.3))
end

function sin_01_x_2(x)
  sum(sin.((x.+0.7))+0.01.*(x.+0.7).^2)
end

function x_i(x)
  ress=0
  for i=1:length(x)
    ress+=(x[i]-i-2.1)^2
  end
  return ress
end

function x_5_sq(x)
  sum((x.-5).^2).-5
end

function x_plus_y(x)
    ress=0
    for i=1:length(x)
      ress+=x[i]
    end
    return ress
end

#

function rand_poly(x)
    return sum(rand(length(x)).*x)
end

# rand_poly(rand(10))
# x=-10:0.001:10
# y=sin.(10rand()x).+(rand()/1000)x.^2
# plot(x,y)

# function limited_domain(x::Vector)::Float64
#   sum( sqrt.(1 ./ (x.+1) .+ (x.+1) ) )  ## min is at 1,1,1...
# end#function
# limited_domain(rand(9))

# x=-10:0.01:10

# y=rand()rastrigin.(x)-.010rand()elliptic.(x)
# scatter(x,y)

function alpine_1(x)
  return sum(abs.(x.*sin.(x)+0.1x))
end


# Example that computes the full inverse wavelet transform using all the
# scales from Jmin=1, to Jmax=log2(n)-1 the coarsest scale

#push path
push!(LOAD_PATH,pwd()*"/src") # for julia 0.4 you need to put your path
#import libraries
using FWT
using PyPlot
using Images

n = 1024;
name = "data_samples/ernst_reuter_haus.bmp";
f = load_image(name, n);
f = rescale(sum(f,3));
f = f[:,:,1];

# filter call
h=filt_gen(WT.db2)
# high pass filter
g = cat(1, 0, h[length(h):-1:2]) .* ( (-1).^(1:length(h)) )
g = cat(1, 0, h[length(h):-1:2]) .* ( (-1).^(1:length(h)) )

# First compute wavelet transform
# Perform of the wavelet transform
Jmax = round(Int64,log2(n))-1;
Jmin = 1;
fW = copy(f);
clf;
for j=Jmax:-1:Jmin
    A = fW[1:2^(j+1),1:2^(j+1)];
    for d=1:2
        Coarse = subsampling(cconvol(A,h,d),d);
        Detail = subsampling(cconvol(A,g,d),d);
        A = cat(d, Coarse, Detail );
    end
    fW[1:2^(j+1),1:2^(j+1)] = A;
    j1 = Jmax-j;
    if j1<4
        imageplot(A[1:2^j,2^j+1:2^(j+1)], "Horizontal, j=$j", 3,4, j1 + 1);
        imageplot(A[2^j+1:2^(j+1),1:2^j], "Vertical, j=$j", 3,4, j1 + 5);
        imageplot(A[2^j+1:2^(j+1),2^j+1:2^(j+1)], "Diagonal, j=$j", 3,4, j1 + 9);
    end
end

# Now the inverse

f1 = copy(fW);
clf;
for j=Jmin:Jmax
    A = f1[1:2^(j+1),1:2^(j+1)];
    for d=1:2
        if d==1
            Coarse = A[1:2^j,:];
            Detail = A[2^j+1:2^(j+1),:];
        else
            Coarse = A[:,1:2^j];
            Detail = A[:,2^j+1:2^(j+1)];
        end
        Coarse = cconvol(upsampling(Coarse,d),FWT.reverse(h),d);
        Detail = cconvol(upsampling(Detail,d),FWT.reverse(g),d);
        A = Coarse + Detail;
        j1 = Jmax-j;
        if j1>0 && j1<5
            imageplot(A, "Partial reconstruction, j=$j", 2,2,j1);
        end
    end
    f1[1:2^(j+1),1:2^(j+1)] = A;
end


### A Pluto.jl notebook ###
# v0.12.16

using Markdown
using InteractiveUtils

# ╔═╡ e077ab16-22e5-11eb-15ed-5f0654bf7328
using Pkg, DrWatson

# ╔═╡ e75a9a06-22e5-11eb-2e36-8d4f62b830ed
begin
	@quickactivate "StatisticsWithJuliaPlutoNotebooks"
	using Statistics, DataFrames, CSV, HypothesisTests
	using Random, Distributions, Plots
	Random.seed!(0)
end;

# ╔═╡ ca5fbdc8-22e5-11eb-0a60-cb0a127a3ca5
md"## Listing6.01"

# ╔═╡ 1aa41898-3afa-11eb-2f49-570853ce6435
begin
	data = CSV.read(datadir("machine1.csv"), DataFrame; header=false)[:, 1]
	xBar, n = mean(data), length(data)
	sig = 1.2
	alpha = 0.1
	z = quantile(Normal(),1-alpha/2)

	Text("Calculating formula: $((xBar - z*sig/sqrt(n), xBar + z*sig/sqrt(n)))")
end

# ╔═╡ 07d8c2c0-3b2e-11eb-344d-c14ab2a82d0d
Text("Using confint() function: $(confint(OneSampleZTest(xBar,sig,n),alpha))")

# ╔═╡ 475ff888-22e6-11eb-2354-09f8f40a8e12
md"## End of listing6.01"

# ╔═╡ Cell order:
# ╠═ca5fbdc8-22e5-11eb-0a60-cb0a127a3ca5
# ╠═e077ab16-22e5-11eb-15ed-5f0654bf7328
# ╠═e75a9a06-22e5-11eb-2e36-8d4f62b830ed
# ╠═1aa41898-3afa-11eb-2f49-570853ce6435
# ╠═07d8c2c0-3b2e-11eb-344d-c14ab2a82d0d
# ╟─475ff888-22e6-11eb-2354-09f8f40a8e12


#=
Catenary Problem

The problem is to find the shape of a hanging chain. It can be defined as the configuration that minimizes the potential energy.

Objective: linear
Constraints: convex quadratic
Feasible set: convex

This model finds the shape of a hanging chain
The solution is known to be y = cosh(a*x) + b
for appropriate a and b.

This problem is a port to Julia/JuMP of a model by Professor Robert Vanderbei
at Princeton University, originally written in AMPL.
Source: http://orfe.princeton.edu/~rvdb/ampl/nlmodels/
=#

using JuMP
using Ipopt  # Nonlinear solver
using Gadfly # Graphing support

N = 100   # number of chainlinks
L = 1     # difference in x-coords of endlinks
h = 2*L/N # length of each link

m = Model(solver=IpoptSolver())

@defVar(m, x[0:N])
@defVar(m, y[0:N])

# Minimize potential energy from center of mass for link
@setObjective(m, Min, sum{(y[j-1] + y[j])/2, j=1:N})

# Anchor ends
@addConstraint(m, x[0] == 0)
@addConstraint(m, y[0] == 0)
@addConstraint(m, x[N] == L)
@addConstraint(m, y[N] == 0)

# Set starting values
for j in 0:N
    setValue(x[j], 0)
    setValue(y[j], 0)
end

# Link together pieces
for j in 1:N
    @addNLConstraint(m,
        (x[j] - x[j-1])^2 + (y[j] - y[j-1])^2 <= h^2
    )
end

solve(m)

# Graph the data

x_clean = Float64[]
y_clean = Float64[]
for j in 0:N
    push!(x_clean, getValue(x)[j])
    push!(y_clean, getValue(y)[j])
end

catenary = plot(x=x_clean, y=y_clean, Coord.Cartesian(xmin=0, xmax=1,))
draw(SVG("catenary.svg", 6inch, 6inch), catenary)


function Δ(model::LogReturnComputationModel; multiplier::Float64 = 1.0)::DataFrame

    # get stuff from the computational model -
    raw_data = model.data
    from = model.from
    to = model.to
    map = model.map

    # ok, so need to filter the data (if we have a from and to date set)
    data = raw_data
    if (isnothing(to) == false && isnothing(from) == false)

        # get the date key from the map -
        date_key = map.first

        # filter to the specified date range -
        data = filter(date_key => d -> (d >= from && d <= to), raw_data)
    end

    # initialize -
    (number_of_rows, _) = size(data)
    return_table = DataFrame(date = Date[], P₁ = Float64[], P₂ = Float64[], Δ = Float64[], Δ₍μ₎ = Float64[], Z = Float64[])

    # main loop -
    for row_index ∈ 2:number_of_rows

        # grab the date -
        tmp_date = data[row_index, map.first]

        # grab the price data -
        yesterday_close_price = data[row_index-1, map.second]
        today_close_price = data[row_index, map.second]

        # compute the diff -
        δ_value = multiplier * log(today_close_price / yesterday_close_price)

        # push! -
        push!(return_table, (tmp_date, yesterday_close_price, today_close_price, δ_value, 0.0, 0.0))
    end

    # compute the mean -
    μ = mean(return_table[!, :Δ])

    # compute the std -
    σ = std(return_table[!, :Δ])

    # add values to the δ - μ col -
    (number_of_rows, _) = size(return_table)
    for row_index ∈ 1:number_of_rows
        value = return_table[row_index, :Δ] - μ
        return_table[row_index, :Δ₍μ₎] = value
    end

    # add the Z column -
    for row_index ∈ 1:number_of_rows
        value = (1 / σ) * (return_table[row_index, :Δ] - μ)
        return_table[row_index, :Z] = value
    end

    # return -
    return return_table
end

function Δ(model::LinearReturnComputationModel; multiplier::Float64 = 1.0)::DataFrame

    # get stuff from the computational model -
    raw_data = model.data
    from = model.from
    to = model.to
    map = model.map

    # ok, so need to filter the data (if we have a from and to date set)
    data = raw_data
    if (isnothing(to) == false && isnothing(from) == false)

        # get the date key from the map -
        date_key = map.first

        # filter to the specified date range -
        data = filter(date_key => d -> (d >= from && d <= to), raw_data)
    end

    # initialize -
    (number_of_rows, _) = size(data)
    return_table = DataFrame(date = Date[], P₁ = Float64[], P₂ = Float64[], Δ = Float64[], Δ₍μ₎ = Float64[], Z = Float64[])

    # main loop -
    for row_index ∈ 2:number_of_rows

        # grab the date -
        tmp_date = data[row_index, map.first]

        # grab the price data -
        yesterday_close_price = data[row_index-1, map.second]
        today_close_price = data[row_index, map.second]

        # compute the diff -
        δ_value = multiplier * ((today_close_price - yesterday_close_price) / (yesterday_close_price))

        # push! -
        push!(return_table, (tmp_date, yesterday_close_price, today_close_price, δ_value, 0.0, 0.0))
    end

    # compute the mean -
    μ = mean(return_table[!, :Δ])

    # compute the std -
    σ = std(return_table[!, :Δ])

    # add values to the δ - μ col -
    (number_of_rows, _) = size(return_table)
    for row_index ∈ 1:number_of_rows
        value = return_table[row_index, :Δ] - μ
        return_table[row_index, :Δ₍μ₎] = value
    end

    # add the Z column -
    for row_index ∈ 1:number_of_rows
        value = (1 / σ) * (return_table[row_index, :Δ] - μ)
        return_table[row_index, :Z] = value
    end

    # return -
    return return_table
end

function Δ(models::Array{T,1};
    multiplier::Float64 = 1.0)::Dict{String,DataFrame} where {T<:AbstractBaseCampComputation}

    # initialize -
    Δ_dictionary = Dict{String,DataFrame}()

    # compute the returns for each model in the array -
    for model ∈ models
        ticker = model.ticker
        Δ_dictionary[ticker] = Δ(model; multiplier = multiplier)
    end

    # return -
    return Δ_dictionary
end

function 𝒟(distribution::Type{T}, ticker_symbol_array::Array{String,1}, data::Dict{String, DataFrame};
    colkey::Symbol = :Δ)::ContinuousMultivariateDistribution where {T <: ContinuousMultivariateDistribution}

    # how many keys and rows do we have?
    number_of_ticker_symbols = length(ticker_symbol_array)
    number_of_rows = length(data[first(ticker_symbol_array)][!,colkey])

    # initialize -
    tmp_array = Array{Float64,2}(undef, number_of_rows, number_of_ticker_symbols)

    # build the data array -
    for (ticker_index, ticker_symbol) ∈ enumerate(ticker_symbol_array)

        # grab the data for this ticker -
        tmp_data_col = data[ticker_symbol][!,colkey]

        # copy into the tmp array -
        for row_index ∈ 1:number_of_rows
            tmp_array[row_index, ticker_index] = tmp_data_col[row_index]
        end
    end

    # fit the distribution -
    return fit(distribution, transpose(tmp_array))
end

function 𝒟(distribution::Type{T}, data::DataFrame;
    colkey::Symbol = :Δ)::UnivariateDistribution where {T<:ContinuousUnivariateDistribution}

    # get the array of data from the data frame -
    data_array = data[!, colkey]

    # do the fit -
    return fit(distribution, data_array)
end

function 𝒟(distribution::Type{T}, data::DataFrame, weights::Union{Nothing,Array{Float64,1}};
    colkey::Symbol = :Δ)::UnivariateDistribution where {T<:ContinuousUnivariateDistribution}

    # get the array of data from the data frame -
    data_array = data[!, colkey]

    # check: do we have a weight array?
    if (isnothing(weights) == false)

        # do the fit w/weights -
        return fit(distribution, data_array, weights)
    else
         # do the fit w/o weights -
        return fit(distribution, data_array)
    end
end

function 𝒟(distribution::Type{T}, data::Dict{String, DataFrame};
    colkey::Symbol = :Δ, weights::Union{Nothing, Dict{String, Union{Nothing,Array{Float64,1}}}} = nothing)::Dict{String, T} where {T<:ContinuousUnivariateDistribution}

    # initialize -
    distribution_dictionary = Dict{String, T}()

    # call the single 𝒟 -
    for (key,value) ∈ data

        # get the array of data from the data frame -
        data_array = value[!, colkey]

        # check: do we have weights?
        d = nothing
        if (isnothing(weights) == false &&
            isnothing(weights[key]) == false)

            # we have a weights -
            w = weights[key]

            # fit -
            d = fit(distribution, data_array, w);
        else
            # fit a distribution -
            d = fit(distribution, data_array);
        end

        # capture -
        distribution_dictionary[key] = d;
    end

    # return data -
    return distribution_dictionary
end

function 𝒫(compare::Function, samples::Array{Float64})::Float64

    # initialize -
    number_of_samples = length(samples)
    tmp_array = BitArray(undef, (number_of_samples, 1))

    # main -
    for sample_index ∈ 1:number_of_samples

        # get the sample price -
        sample_price = samples[sample_index]

        # check: which is larger, sample or target price?
        compare(sample_price) ? tmp_array[sample_index] = 1 : tmp_array[sample_index] = 0
    end

    # sum the tmp_array -
    number_of_larger_values = sum(tmp_array)

    # compute the probability -
    return (number_of_larger_values / number_of_samples)
end

function covariance(tickers::Array{String,1}, data::Dict{String,DataFrame};
    key::Symbol = :Δ)::Array{Float64,2}

    # build a return matrix -
    number_of_tickers = length(tickers)

    # get the first data table so we can get the number of rows -
    (number_of_rows, _) = size(data[first(tickers)])

    # initialize -
    price_return_array = Array{Float64,2}(undef, number_of_rows, number_of_tickers)

    # populate the price return array -
    for col_index ∈ 1:number_of_tickers

        # get the ticker -
        ticker_symbol = tickers[col_index]

        # get data -
        df = data[ticker_symbol]

        for row_index ∈ 1:number_of_rows
            price_return_array[row_index, col_index] = df[row_index, key]
        end
    end

    # compute the cov array -
    return Statistics.cov(price_return_array)
end

function β(tickers::Array{String,1}, data::Dict{String,DataFrame};
    key::Symbol = :Δ, base::String = "SPY")::Array{Float64,1}

    # initialize -
    number_of_tickers = length(tickers)
    β_array = Array{Float64,1}(undef, number_of_tickers)

    # get the return data for the base -
    base_return_array = data[base][!,key]
    var_base = var(base_return_array)

    # compute β -
    for ticker_index ∈ 1:number_of_tickers

        # what ticker?
        ticker_value = tickers[ticker_index]
        return_array_ticker = data[ticker_value][!,key]

        # compute the β_value -
        β_value = cov(return_array_ticker, base_return_array) * (1 / var_base)

        # capture -
        β_array[ticker_index] = β_value
    end

    # return -
    return β_array
end

function sample(model::T, number_of_steps::Int64;
    number_of_sample_paths = 100)::Array{Array{Float64,2},1} where {T<:ContinuousMultivariateDistribution}

    # initialize -
    number_of_steps = number_of_steps + 1
    number_of_dimensions = length(model)
    sample_return_data = Array{Array{Float64,2},1}(undef, number_of_steps)

    for time_step_index ∈ 1:number_of_steps

        # generate a random block -
        B = rand(model, number_of_sample_paths)

        # capture -
        sample_return_data[time_step_index] = B
    end

    # return -
    return sample_return_data
end

function sample(model::T, number_of_steps::Int64;
    number_of_sample_paths = 100, number_of_strata = 1)::Array{Float64,2} where {T<:ContinuousUnivariateDistribution}

    # initialize -
    number_of_steps = number_of_steps + 1
    sample_return_data = Array{Array{Float64,1},1}(undef, number_of_steps)

	# Let's use stratefied sampling to generate the return samples -
    for time_step_index ∈ 1:number_of_steps

		tmp_vector = Array{Float64,1}()

        # sample the strata ...
        for strata_index ∈ 1:number_of_strata

            # compute a number_of_sample_paths draws from tis strata?
            for _ ∈ 1:number_of_sample_paths

                # role a random number -
                r = rand()

                # compute V -
                V₁ = (strata_index - 1)/number_of_strata + r/number_of_strata
                V₂ = (strata_index - 1)/number_of_strata + (1 - r)/number_of_strata

                # compute the quantile for this V -
                q₁ = quantile(model, V₁)
                q₂ = quantile(model, V₂)

                # grab this value -
                push!(tmp_vector, q₁)
                push!(tmp_vector, q₂)
            end
        end

        sample_return_data[time_step_index] = tmp_vector
    end

    # crunch the data together -
    sample_return_array = transpose(hcat(sample_return_data...))

    # return -
    return sample_return_array
end

function sample(models::Dict{String,T}, number_of_steps::Int64;
    number_of_sample_paths = 100, number_of_strata = 1)::Dict{String,Array{Float64,2}} where {T<:ContinuousUnivariateDistribution}

    # initialize -
    sample_dictionary = Dict{String,Array{Float64,2}}()

    # compute -
    for (ticker_symbol, model) ∈ models

        # sample this model -
        tmp_array = sample(model, number_of_steps;
            number_of_sample_paths = number_of_sample_paths, number_of_strata = number_of_strata)

        # store the samples -
        sample_dictionary[ticker_symbol] = tmp_array
    end

    # return -
    return sample_dictionary
end


"""
# module Doolittle

# Examples

```jldoctest
julia>
```
"""
module Doolittle

using LinearAlgebra: I, kron, UpperTriangular

using APMAE4001.Matrix: issquare
using APMAE4001.LUFactorization.Substitution

export doolittle_lu,
    polysolve

function row_vector(n::Int, k::Int)
    v = zeros(n)
    v[k] = one(eltype(v))
    return v
end

"""
    elimination_factors(n::Int, k::Int, A::AbstractMatrix)

Generate a vector of factors to eliminate the `k`th column.
Combine this function with `row_vector` to to generate an general elementary elimination matrix.

# Arguments
- `n::Integer`: the size of the matrix `A`.
- `k::Integer`: the step count in the elimination process. It must satisfies `1 ≤ k ≤ n - 1`.
- `A::AbstractMatrix`: the matrix from `(k - 1)`th step of the elimination process.
"""
function elimination_factors(n::Int, k::Int, A::AbstractMatrix)
    v = zeros(promote_type(eltype(A), Float64), n)
    for j in (k + 1):n
        v[j] = A[j, k] / A[k, k]  # elimination factor
    end
    v
end

"""
    general_elementary_elimination_matrix(lk::AbstractVector, ek::AbstractVector)

Elimination matrix ``E_{k}`` do row operations to eliminate the `k`th column.
"""
general_elementary_elimination_matrix(lk::AbstractVector, ek::AbstractVector) = I - kron(lk, transpose(ek))  # `I` is the identity matrix.

"""
    doolittle_lu(A::AbstractMatrix; include_l::Bool=true, include_e::Bool=false)

Referenced from [Wikipedia "LU decomposition" page](https://en.wikipedia.org/wiki/LU_decomposition#Doolittle_algorithm).
"""
function doolittle_lu(A::AbstractMatrix; include_l::Bool = true, include_e::Bool = false)
    issquare(A) || throw(DimensionMismatch("The matrix `A` is not a square matrix!"))
    n = size(A, 1)

    intermediate_elementary_elimination_matrices = []
    for k in 1:(n - 1)
        Ek = general_elementary_elimination_matrix(elimination_factors(n, k, A), row_vector(n, k))
        pushfirst!(intermediate_elementary_elimination_matrices, Ek)  # Keep this order!
        A = Ek * A
    end
    # Now `A` is the U matrix.
    E = prod(*, intermediate_elementary_elimination_matrices)
    include_e && return E, A
    include_l && return inv(E), A  # L matrix is `inv(E)`.
    return A
end

function polysolve(A::AbstractMatrix, b::AbstractVector)
    E, U = doolittle_lu(A, include_e = true)
    substitution(UpperTriangular(U), E * b)
end

end


n = 7 # rand(1:7)
@test matrixdepot("sampling", n) == matrixdepot("sampling", Float64, n)

A = matrixdepot("sampling", n)
v = sort(eigvals(A))
@test v ≈ [0: n - 1;] atol=1e-10
println("'sampling' passed test...")


struct Margins
    # critical frequency
    ω_c::Float64
    # gain cross-over frequency
    ω_g::Float64
    # gain margin
    gain_margin::Float64
    # phase margin (radians)
    phase_margin::Float64
end

"""
    margins = gain_phase_margins(g_ol, ω_c_guess=0.001, ω_g_guess=0.001)

compute critical frequency (radians / time), gain crossover frequency (radians / time),
gain margin, and phase margin (radians) of a closed loop, given its
closed loop transfer function `g_ol::TransferFunction`.

if ω_c or ω_g is not found (i.e. if either are `NaN`), but the `bode_plot` clearly shows
a critical/gain crossover frequency, adjust `ω_c_guess` or `ω_g_guess` to find the root.

# Example
```
g_ol = 2 * exp(-s) / (5 * s + 1)
margins = gain_phase_margins(g_ol)
margins.ω_c # critical freq. (radians / time)
margins.ω_g # gain crossover freq. (radians / time)
margins.gain_margin # gain margin
margins.phase_margin # phase margin (radians)
```
"""
function gain_phase_margins(g_ol::TransferFunction; ω_c_guess::Float64=0.001, ω_g_guess::Float64=0.001)
    # critical frequency ∠ G(i ω_c) = -π
    ω_c = NaN
    try
        ω_c = fzero(ω -> angle(evaluate(g_ol, im * ω)) + π, ω_c_guess, atol=2*sqrt(eps()))
    catch da_error
        if isa(da_error, Roots.ConvergenceFailed)
            ω_c = NaN
        else
            println(da_error)
            error("something went wrong when computing ω_c")
        end
    end
    # check for spurious solution (often ω_c d.n.e.) TODO check issue on Roots.jl
    if ! isapprox(angle(evaluate(g_ol, im * ω_c)), -π, rtol=0.01)
        ω_c = NaN
    end

    # gain cross over frequency | G(i ω_g) | = 1
    ω_g = NaN
    try
        ω_g = fzero(ω -> abs(evaluate(g_ol, im * ω)) - 1.0, ω_g_guess, atol=2*sqrt(eps()))
    catch da_error
        if isa(da_error, Roots.ConvergenceFailed)
            ω_g = NaN
        else
            println(da_error)
            error("something went wrong when computing ω_g")
        end
    end
    # check for spurious solution (often ω_c d.n.e.) TODO check issue on Roots.jl
    if ! isapprox(abs(evaluate(g_ol, im * ω_g)), 1.0, rtol=0.01)
        ω_g = NaN
    end
    # gain margin = 1 / | G(i ω_c) |
    gm = 1 / abs(evaluate(g_ol, im * ω_c))
    # phase margin = ∠ G(i ω_g) + π
    pm = π + angle(evaluate(g_ol, im * ω_g))
    return Margins(ω_c, ω_g, gm, pm)
end


#=
We can determine how "out of order" an array A is by counting the number of inversions it has. Two elements A[i] and A[j] form an inversion if A[i] > A[j] but i < j. That is, a smaller element appears after a larger element.

Given an array, count the number of inversions it has. Do this faster than O(N^2) time.

You may assume each element in the array is distinct.

For example, a sorted list has zero inversions. The array [2, 4, 1, 3, 5] has three inversions: (2, 1), (4, 1), and (4, 3). The array [5, 4, 3, 2, 1] has ten inversions: every distinct pair forms an inversion.
=#

using Test
include("Solutions/problem44_count_inversions.jl")

@test inversions([2, 4, 1, 3, 5]) == 3
@test inversions([5, 4, 3, 2, 1]) == 10


"""
$(TYPEDEF)

A system of partial differential equations.

# Fields
$(FIELDS)

# Example

```julia
using ModelingToolkit

@parameters t x
@variables u(..)
Dxx = Differential(x)^2
Dtt = Differential(t)^2
Dt = Differential(t)

#2D PDE
C=1
eq  = Dtt(u(t,x)) ~ C^2*Dxx(u(t,x))

# Initial and boundary conditions
bcs = [u(t,0) ~ 0.,# for all t > 0
       u(t,1) ~ 0.,# for all t > 0
       u(0,x) ~ x*(1. - x), #for all 0 < x < 1
       Dt(u(0,x)) ~ 0. ] #for all  0 < x < 1]

# Space and time domains
domains = [t ∈ IntervalDomain(0.0,1.0),
           x ∈ IntervalDomain(0.0,1.0)]

pde_system = PDESystem(eq,bcs,domains,[t,x],[u])
```
"""
struct PDESystem <: ModelingToolkit.AbstractSystem
    "The equations which define the PDE"
    eqs
    "The boundary conditions"
    bcs
    "The domain for the independent variables."
    domain
    "The independent variables"
    indvars
    "The dependent variables"
    depvars
    "The parameters"
    ps
    """
    defaults: The default values to use when initial conditions and/or
    parameters are not supplied in `ODEProblem`.
    """
    defaults::Dict
    @add_kwonly function PDESystem(eqs, bcs, domain, indvars, depvars, ps = SciMLBase.NullParameters(), defaults = Dict())
        new(eqs, bcs, domain, indvars, depvars, ps, defaults)
    end
end

Base.getproperty(x::PDESystem, sym::Symbol) = getfield(x, sym)

Base.summary(prob::PDESystem) = string(nameof(typeof(prob)))
function Base.show(io::IO, ::MIME"text/plain", sys::PDESystem)
    println(io,summary(sys))
    println(io,"Equations: ", get_eqs(sys))
    println(io,"Boundary Conditions: ", get_bcs(sys))
    println(io,"Domain: ", get_domain(sys))
    println(io,"Dependent Variables: ", get_depvars(sys))
    println(io,"Independent Variables: ", get_indvars(sys))
    println(io,"Parameters: ", get_ps(sys))
    print(io,"Default Parameter Values", get_defaults(sys))
    return nothing
end


"""
    get_neg_binomial_prn(p, r, shape=1; seed=nothing)

Generate a random variable from a Negative Binomial(`p`, `r`) distribution. Optionally you can set a specific seed.

# Examples

```julia-repl
julia> get_neg_binomial_prn(.2, 2)
 16

julia> get_neg_binomial_prn(.2, 2, seed=42)
 6

```

# References

Walk, C. Handbook on statistical distributions for experimentalists. 2007.
"""
function get_neg_binomial_prn(p::Real, r::Int; seed::Union{Int, Nothing}=nothing)
    check_p(p)
    n = convert(Int, r/p * 10)
    U = bernoulli_rng(p, n, seed=seed)
    X = sum(cumsum(U, dims=1) .< r) + 1   # TODO CHECK THIS AGAINST ANOTHER METHOD (e.g. convolution)
    return X
end

"""
    neg_binomial_rng(p, r, shape=1; seed=nothing)

Generate a `shape` element array of random variables from a Negative Binomial(`p`, `r`) distribution. Optionally you can set a specific seed.

# Notes

The Negative Binomial distribution is given:

``f(x,p,r) = \\binom{x-1}{r-1} (1-p)^{x-r} p^r \\quad x = 0,1,\\dots, n``

# Examples

```julia-repl
julia> neg_binomial_rng(.5, 2)
1-element Vector{Float64}:
 3.0

julia> neg_binomial_rng(.5, 5, 5)
5-element Vector{Float64}:
  8.0
 10.0
  8.0
 13.0
 10.0

julia> neg_binomial_rng(.5, 2, (2,2))
2×2 Matrix{Float64}:
 3.0  4.0
 4.0  2.0

```

# References

Walk, C. Handbook on statistical distributions for experimentalists. 2007.
"""
function neg_binomial_rng(p::Real, r::Int, shape::Union{Int, Tuple{Vararg{Int}}}=1; seed::Union{Int, Nothing}=nothing)
    check_p(p)
    X = zeros(shape)
    X .= get_neg_binomial_prn.(p, r, seed=seed)
    return X
end

"""
    conv_neg_binomial_rng(p, r, shape=1; seed=nothing)

Generate a `shape` element array of random variables from a Negative Binomial(`p`, `r`) distribution. Optionally you can set a specific seed.

# Notes

The Negative Binomial distribution is given:

``f(x,p,r) = \\binom{x-1}{r-1} (1-p)^{x-r} p^r \\quad x = 0,1,\\dots, n``

Uses a convolution algorithm to generate random variables, which is slightly slower than [`neg_binomial_rng`](@ref).

# Examples

```julia-repl
julia> conv_neg_binomial_rng(.4, 5, 1)
1×1 Matrix{Int64}:
 8

julia> conv_neg_binomial_rng(.4, 5, 5)
5×1 Matrix{Int64}:
 11
 14
 8
 10
 13

julia> conv_neg_binomial_rng(.4, 5, (2,2))
2×2×1 Array{Int64, 3}:
[:, :, 1] =
 20  11
 7  10
```

# References

Law, A. Simulation modeling and analysis, 5th Ed. McGraw Hill Education, Tuscon, 2013.
"""
function conv_neg_binomial_rng(p::Real, r::Int, shape::Union{Int, Tuple{Vararg{Int}}}=1; seed::Union{Int, Nothing}=nothing)
    check_p(p)
    Y = geometric_rng(p, (shape..., r), seed=seed)
    X = sum(Y, dims=ndims(Y))
    return X
end


using EngEconomics, Roots

# Given
aInit = -200000
aAnnual = 30000
aScrap = 45000
aLife = 20

bInit = -180000
bAnnual = 24000
bScrap = 20000
bLife = 15

MARR = 0.09

# Find Annual Worth Analysis
aAW = aInit * capitalRecoveryFactor(MARR, aLife) + aAnnual + aScrap * sinkingFundFactor(MARR, aLife)

bAW = bInit * capitalRecoveryFactor(MARR, bLife) + bAnnual + bScrap * sinkingFundFactor(MARR, bLife)

if aAW > bAW
	println("A")
else
	println("B")
end


using ApproxFun

#The following solves the Airy ODE with dirichlet boundary conditions

x=Fun(identity,[-1000.,15.])   # Fun corresponding to multiplication by x, on [-100,15]
d=domain(x)
D=Derivative()             # The derivative operator
B=dirichlet()              # Dirichlet boundary conditions, [u(-100),u(15)]

#Construct operator

A=[B;D^2-x]                # This is dirichlet conditions and u'' - x u
b=[airyai(first(d)),0.]         # We want it to equal airyai(-100) at -100, and 0 at
                           # 10, with 0 rhs
#Solve ODE

u=A\b                      # u satisfies A*u = b, or in other words,
                           # B*u = [airyai(-100.),0.] and (D2 - x)*u = 0.

# Check the accuracy
norm(u - Fun(airyai,d))

## We now solve with Neumann conditions

B=neumann()
A=[B;D^2-x]
b=[airyaiprime(first(d)),0.,0.]

u=A\b

# Check the accuracy
norm(u - Fun(airyai,d))


function romberg(f, a, b, n)
	r = zeros(2, n)
	h = b - a
	r[1, 1] = (h/2) * (f(a) + f(b))
	for i ∈ 2:n
		tsum = 0
		for k ∈ 1:(2^(i - 2))
			tsum += f(a + (k - 0.5) * h)
		end
		r[2, 1] = (1/2)*(h*tsum + r[1, 1])
		for j ∈ 2:i
			r[2, j] = r[2, j - 1] + (r[2, j - 1] - r[1, j - 1])/(4^(j - 1) - 1)
		end
		h = h/2
		for j ∈ 1:i
			r[1, j] = r[2, j]
		end
	end
	return r[2, n]
end

f(x) = sin(x)
println(romberg(f, 0, 3, 20))


##
import LinearAlgebra as linalg
import Plots as plt

include("KalmanFilter.jl")
import .KalmanFilter as kf

##
function get_data()
    # Observations
    x = [4000.0 4260.0 4550.0 4860.0 5110.0] # position [m]
    v = [ 280.0  282.0  285.0  286.0  290.0] # velocity [m/s]
    # x = [4000.0 4260.0 4550.0 4860.0 5110.0 5220.0 5115.0 4995.0 4871.0] # position [m]
    # v = [ 280.0  282.0  285.0  286.0  290.0  295.0  292.0  287.0  284.0] # velocity [m/s]
    X = [x; v]

    # Initial values
    a = 2.0 # acceleration
    Δt = 1.0 # Time step
    ΔP = [20.0; 5.0] # Process errors in process covariance matrix
    Δ = [25.0; 6.0] # Observation errors
    return X, a, Δ, ΔP, Δt
end

## Define the state space model

function define_model(X, a, Δ, ΔP, Δt)
    m, n = size(X)
    Q = zeros(m, m) # Process noise covariance matrix
    F = [1.0 Δt; 0.0 1.0] # Transition matrix
    B = [Δt^2/2.0 Δt]' # Control-input matrix
    u = 2.0 # # Control vector, acceleration
    R = linalg.Diagonal([Δ[1]^2 0.0; 0.0 Δ[2]^2]) # Sensor noise covariance matrix
    H = kf.eye(m) # Measurement matrix
    w = zeros(m)
    v = zeros(m)
    # Initial predicted process covariance matrix
    P = linalg.Diagonal([ΔP[1]^2 ΔP[1]*ΔP[2]; ΔP[2]*ΔP[1] ΔP[2]^2])
    return P, Q, R, F, H, B, u, m, n
end

## Data input
function data_input(X, n)
    X = [ [X[1,i], X[2,i]] for i in 1:n ]
    Xp = [ X[1] ] # Initialize predicted state
    Xk = copy(Xp) # Initialized new state matrix
    return Xp, Xk, X
end

## Run Kalman Filter
function run_kf(X, P, F, H, Q, R, B, u; filter=kf.CholeskySqrt())
    n = size(X, 2)
    _, _, Xx = data_input(X, n)
    k = 1
    S = kf._innovation_covariance(P, H, R)
    K = kf._kalman_gain(P, H, S)
    skf = [ kf.LinearKF(Xx[k], Xx[k], P, K, S, Xx[1], filter) ]
    while k < n
        s = kf.linear_kalman(
            Xx[k+1], skf[k].xk, skf[k].P, F, H, Q, R;
            B=B, u=u, sqrt_filter=filter,
        )
        push!(skf, s)
        k += 1
    end
    return skf
end

## Smoothers
function run_smoothers_bts(Xk, F, Σ)
    k = length(Xk)
    sm = Vector{kf.RTSSmoother}(undef, k)
    sm[k] = kf.RTSSmoother(Xk[k], Σ[k])
    ksmoother = kf.RauchTungStriebel()
    while k > 1
        s = kf.kalman_smooth(ksmoother, sm[k].xk, Xk[k-1], Xk[k], Σ[k-1], Σ[k], F)
        k -= 1
        sm[k] = s
    end
    return sm
end

function run_smoothers_mbf(skf, F, H)
    m = size(skf[1].xk, 1)
    k = length(skf)
    sm = Vector{kf.MBFSmoother}(undef, k)
    ksmoother = kf.ModifiedBrysonFrazier()
    # Λ, λ = zeros(size(lsm.F)), zeros(size(lsm.F,1))
    SH = skf[k].S \ H'
    Λ, λ = SH*H, -SH*skf[k].y
    sm[k] = kf.MBFSmoother(skf[k].xk, skf[k].P, Λ, λ)
    while k > 1
        s = kf.kalman_smooth(
            ksmoother,
            skf[k-1].xk,
            skf[k-1].P,
            skf[k-1].K,
            F,
            H,
            sm[k].L,
            sm[k].l,
            skf[k-1].S,
            skf[k-1].y,
        )
        k -= 1
        sm[k] = s
    end
    return sm
end

## Plots
function plot_kf(Xp, Xk, Xx, Δ)
    # Xp, Xk, Xx = reduce(hcat, Xp), reduce(hcat, Xk), reduce(hcat, Xx)
    p1 = plt.plot(Xp[1,:], m=:o, label="Predicted", legend=:topleft, ylabel="Position [m]", xlabel="Time [s]")
    plt.plot!(p1, Xx[1,:], m=:s, label="Measured", ribbon=Δ[1])
    plt.plot!(p1, Xk[1,:], m=:p, label="KF")
    p2 = plt.plot(Xp[2,:], m=:o, label="Predicted", legend=:topleft, ylabel="Velocity [m/s]", xlabel="Time [s]")
    plt.plot!(p2, Xx[2,:], m=:s, label="Measured", ribbon=Δ[2])
    plt.plot!(p2, Xk[2,:], m=:p, label="KF")
    p = plt.plot(p1, p2, size=(700,350))
    return p
end

function plot_kf_smooth(Xp, Xk, Xx, Xks, Δ)
    # Xp, Xk = reduce(hcat, Xp), reduce(hcat, Xk)
    # Xx, Xks = reduce(hcat, Xx), reduce(hcat, Xks)
    p1 = plt.plot(Xp[1,:], m=:o, label="Predicted", legend=:topleft, ylabel="Position [m]", xlabel="Time [s]")
    plt.plot!(p1, Xx[1,:], m=:s, label="Measured", ribbon=Δ[1])
    plt.plot!(p1, Xk[1,:], m=:p, label="KF")
    plt.plot!(p1, Xks[1,:], m=:h, label="Smooth")
    p2 = plt.plot(Xp[2,:], m=:o, label="Predicted", legend=:topleft, ylabel="Velocity [m/s]", xlabel="Time [s]")
    plt.plot!(p2, Xx[2,:], m=:s, label="Measured", ribbon=Δ[2])
    plt.plot!(p2, Xk[2,:], m=:p, label="KF")
    plt.plot!(p2, Xks[2,:], m=:h, label="Smooth")
    p = plt.plot(p1, p2, size=(700,350))
    return p
end

## Run KFs
X, a, Δ, ΔP, Δt = get_data()
P, Q, R, F, H, B, u, m, n = define_model(X, a, Δ, ΔP, Δt)

## Simple Kalman filter
skf = run_kf(X, P, F, H, Q, R, B, u; filter=kf.CholeskyModSqrt())
Xp, Xk = kf.unpack(skf, [:xp, :xk])
p = plot_kf(Xp, Xk, X, Δ)

## Rauch Tung Striebel Smoother
Σ, Xk2m = kf.unpack_matrix(skf, [:P, :xk])
skfm = run_smoothers_bts(Xk2m, F, Σ)
Xk2m = kf.unpack(skfm, :xk)
p2m = plot_kf_smooth(Xp, Xk, X, Xk2m, Δ)

## Modified Bryson–Frazier Smoother
skfmb = run_smoothers_mbf(skf, F, H)
Xk2mb = kf.unpack(skfmb, :xk)
p2m = plot_kf_smooth(Xp, Xk, X, Xk2mb, Δ)