# calculateposterior.jl

"""
    calculatedependentfaberschauderfunctions(higestlevel::Int64)

Internal function, not exported.

Calculates which Faber-Schauder functions (ψ_i,ψ_j) have essential overlapping
support, i ≤ j. All other combinations (ψ_i, ψ_j), i ≤ j, have essential
nonoverlapping support and hence int_0^T ψ_i(X_t)ψ_j(X_t)dt=0, which we use in
calculategirsanovmatrix.

Returns a triple (lengthvectors, rowindices, columnindices) where
```julia
lengthvectors == length(rowindices) == length(columnindices),
```
and for i in 1:lengthvectors (ψ_rowindices[i],ψ_columnindices[i]) have
essentially overlapping support. For all i, rowindices[i] ≤ columnindices[i].
"""
function calculatedependentfaberschauderfunctions(higestlevel::Int64)
    lengthvectors = (higestlevel+1)*2^(higestlevel+1)+1 # Correct.
    rowindices = Vector{Int}(undef, lengthvectors)
    columnindices = Vector{Int}(undef, lengthvectors)
    # Ψ_1 is dependent with all basis functions, including itself.
    rowindices[1:2^(higestlevel+1)] .= 1
    columnindices[1:2^(higestlevel+1)] = 1:2^(higestlevel+1)
    index = 2^(higestlevel+1) + 1 # point where we are in constructing rowindices and columnindices
    # psi_{j,k}=psi_{2^j+k} is dependent with
    # psi_{j+d,(k-1)2^d+1},...,psi_{j+d, k2^d}, d\ge0 (which includes itself).
    for jone in 0:higestlevel
        twotothepowerjone = 2^jone
        for kone in 1:2^jone
            ione = twotothepowerjone+kone # other index system
            for  jtwo in jone:higestlevel
                d = jtwo - jone
                twotothepowerjtwo = 2^jtwo
                for ktwo in (kone-1)*2^d+1:kone*2^d
                    rowindices[index] = ione
                    columnindices[index] = twotothepowerjtwo+ktwo
                    index += 1
                end
            end
        end
    end
    @assert lengthvectors + 1 == index
    return (lengthvectors, rowindices, columnindices)
end

# calculategirsanovmatrixelement methods:

"""
    calculategirsanovmatrixelement(ψ1Xt, ψ2Xt, σ, Δt)
    calculategirsanovmatrixelement(samplevalueindices1, samplevalueindices2, ψ1Xt, ψ2Xt, σ, Δt)

Internal function, not exported!

Where ψ1Xt, ψ2Xt are arrays, and σ and Δt are either arrays or numbers.

Let ψ_a and ψ_b denote two basis elements.
calculategirsanovmatrixelement calculates the (a,b) element of the Girsanov
matrix defined by int_0^T ψ_a(X_t)ψ_b(X_t)/(σ^2(X_t)) dt, where ψ_a is given
by ψ1Xt (already evaluated in ψ_a) and ψ_b by ψ2Xt (already evaluated in ψ_b).

samplevalueindices1[i] == false should correspond to Ψ1Xt[i] == 0.0.
Similar for samplevalueindices2 and Ψ2Xt.
"""
calculategirsanovmatrixelement(ψ1Xt, ψ2Xt, σXt, Δt) = sum(Δt .* ψ1Xt .* ψ2Xt ./ (σXt.^2))
#test written
calculategirsanovmatrixelement(ψ1Xt, ψ2Xt, σXt, Δt::Number) = Δt * sum(ψ1Xt .* ψ2Xt ./ (σXt.^2))
#test written
calculategirsanovmatrixelement(ψ1Xt, ψ2Xt, σXt::Number, Δt) = sum(Δt .* ψ1Xt .* ψ2Xt) / (σXt^2)
#test written
calculategirsanovmatrixelement(ψ1Xt, ψ2Xt, σXt::Number, Δt::Number) = Δt * sum(ψ1Xt .* ψ2Xt) / (σXt^2)
#test written
function calculategirsanovmatrixelement(samplevalueindices1, samplevalueindices2, ψ1Xt, ψ2Xt, σXt, Δt)
    samplevalueindices = samplevalueindices1 .& samplevalueindices2
    filteredψ1Xt = ψ1Xt[samplevalueindices]
    filteredψ2Xt = ψ2Xt[samplevalueindices]
    filteredσXt = σXt[samplevalueindices]
    filteredΔt = Δt[samplevalueindices]
    return sum(filteredΔt .* filteredψ1Xt .* filteredψ2Xt ./ (filteredσXt.^2))
end
function calculategirsanovmatrixelement(samplevalueindices1, samplevalueindices2, ψ1Xt, ψ2Xt, σXt, Δt::Number)
    samplevalueindices = samplevalueindices1 .& samplevalueindices2
    filteredψ1Xt = ψ1Xt[samplevalueindices]
    filteredψ2Xt = ψ2Xt[samplevalueindices]
    filteredσXt = σXt[samplevalueindices]
    return Δt * sum(filteredψ1Xt .* filteredψ2Xt ./
        (filteredσXt.^2))
end
function calculategirsanovmatrixelement(samplevalueindices1, samplevalueindices2, ψ1Xt, ψ2Xt, σXt::Number, Δt)
    samplevalueindices = samplevalueindices1 .& samplevalueindices2
    filteredΔt = Δt[samplevalueindices]
    filteredψ1Xt = ψ1Xt[samplevalueindices]
    filteredψ2Xt = ψ2Xt[samplevalueindices]
    return sum(filteredΔt .* filteredψ1Xt .* filteredψ2Xt) / (σXt^2)
end
function calculategirsanovmatrixelement(samplevalueindices1, samplevalueindices2, ψ1Xt, ψ2Xt, σXt::Number, Δt::Number)
    samplevalueindices = samplevalueindices1 .& samplevalueindices2
    filteredψ1Xt = ψ1Xt[samplevalueindices]
    filteredψ2Xt = ψ2Xt[samplevalueindices]
    return Δt * sum(filteredψ1Xt .* filteredψ2Xt) / (σXt^2)
end

"""
    calculateΔt(timeinterval::AbstractRange) = step(timeinterval)
    calculateΔt(timeinterval) = timeinterval[2:end] - timeinterval[1:end-1]

Internal function, not exported!

Calculates Δt. It returns a number when timeinterval is a range object
and an array of increments of the timeinterval vector otherwise.
"""
calculateΔt(timeinterval::AbstractRange) = step(timeinterval)
calculateΔt(timeinterval) = timeinterval[2:end] - timeinterval[1:end-1]
# Test written.

"""
    calculategirsanovmatrix(sizesquarematrix, timeinterval, ψXt, σXt)
    calculategirsanovmatrix(Π, samplevalueindices, timeinterval, ψXt, σXt)

Internal function, not exported!

Calculates the Girsanov matrix. In the case of a FaberSchauderExpansionWithGaussianCoefficients-object
it will make use of the sparsity structure of the Faber-Schauder basis.
"""
function calculategirsanovmatrix(sizesquarematrix, timeinterval, ψXt, σXt)
    girsanovmatrix = Matrix{Float64}(undef, sizesquarematrix, sizesquarematrix)
    Δt = calculateΔt(timeinterval)
    for k in 1:sizesquarematrix
        for l in k:sizesquarematrix
            girsanovmatrix[k,l] = calculategirsanovmatrixelement(
                ψXt[k], ψXt[l], σXt, Δt)
        end
    end
    return Symmetric(girsanovmatrix)
end
function calculategirsanovmatrix(Π, samplevalueindices, timeinterval, ψXt, σXt)
    lengthvectors, rowindices, columnindices = calculatedependentfaberschauderfunctions(Π.higestlevel)
    d = length(Π)
    V = Vector{Float64}(undef, lengthvectors)
    Δt = calculateΔt(timeinterval)
    for i in 1:lengthvectors
        V[i] = calculategirsanovmatrixelement(samplevalueindices[rowindices[i]],
            samplevalueindices[columnindices[i]], ψXt[rowindices[i]], ψXt[columnindices[i]], σXt, Δt)
    end
    return Symmetric(sparse(rowindices, columnindices, V, d, d))
end

"""
    calculategirsanovvectorelement(ΔXt, ψXt, σXt)
    calculategirsanovvectorelement(ΔXt, ψXt, σXt::Number)

    calculategirsanovvectorelement(samplevalueindices, ΔXt, ψXt, σ::Number)

Internal function, used by calculategirsanovvector, not exported!

Calculates the kth Girsanov vector element int_0^T ψ_k(X_t)/(σ^2(X_t)) dX_t
with the Euler-Maruyama method where ψ_k is the kth basis function, and T is the end time.
"""
calculategirsanovvectorelement(ΔXt, ψXt, σXt) = sum(ΔXt .* ψXt ./ (σXt.^2))
#Test written.
calculategirsanovvectorelement(ΔXt, ψXt, σXt::Number) = sum(ΔXt .* ψXt) / (σXt^2)
#Test written.
function calculategirsanovvectorelement(samplevalueindices, ΔXt, ψXt, σXt)
    filteredΔXt = ΔXt[samplevalueindices]
    filterψXt = ψXt[samplevalueindices]
    filteredσXt = σXt[samplevalueindices]
    return sum(filteredΔXt .* filterψXt ./ (filteredσXt.^2))
end
#Test written.
function calculategirsanovvectorelement(samplevalueindices, ΔXt, ψXt, σXt::Number)
    filteredΔXt = ΔXt[samplevalueindices]
    filteredψXt = ψXt[samplevalueindices]
    return sum(filteredΔXt .* filteredψXt) / (σXt^2)
end
#Test written.
"""
    calculategirsanovvector(lengthvector, samplevalues, ψXt, σXt)
    calculategirsanovvector(lengthvector, samplevalueindices, samplevalues, ψXt, σXt)

Internal function, not exported!

Calculates the Girsanov vector.
"""
function calculategirsanovvector(lengthvector, samplevalues, ψXt, σXt)
    ΔXt = samplevalues[2:end] - samplevalues[1:end-1]
    return [calculategirsanovvectorelement(ΔXt, ψXt[k], σXt) for k in
        1:lengthvector]
end
# Test written.
function calculategirsanovvector(lengthvector, samplevalueindices, samplevalues, ψXt, σXt)
    ΔXt = samplevalues[2:end] - samplevalues[1:end-1]
    return [calculategirsanovvectorelement(samplevalueindices[k], ΔXt, ψXt[k],
        σXt) for k in 1:lengthvector]
end
# Test written.

"""
    calculateσXt(σ::Number, v) = σ
    calculateσXt(σ, v) = σ.(v)

Internal function, not exported!.

Returns σ when it is a number, and if σ is a function, it evaluates v in σ.
"""
calculateσXt(σ::Number, v) = σ
calculateσXt(σ, v) = σ.(v)
#Tests written.

"""
    calculateposterior(Π, X, model::SDEModel)
    calculateposterior(Π, X, σ)
    calculateposterior(Π::FaberSchauderExpansionWithGaussianCoefficients, X, σ)

Calculates the posterior distribution Π(⋅∣X) and returns a FaberSchauderExpansionWithGaussianCoefficients
object when Π is a FaberSchauderExpansionWithGaussianCoefficients-object. Otherwise, it returns a GaussianProcess-object.
Uses model to determine σ. When Π is a FaberSchauderExpansionWithGaussianCoefficients-object, then calculateposterior
makes use of the sparsity structure of the Faber-Schauder basis.

# Examples

##Example with Faber-Schauder expansion.
```julia
model = SDEModel(1.0, 0.0, 10000.0, 0.1)
sde = SDE(x->sinpi(2*x), model)
X = rand(sde)
Π = FaberSchauderExpansionWithGaussianCoefficients([2.0^j for j in 0:5])
postΠ = calculateposterior(Π, X, model)
```
##Example with Fourier expansion.
```julia
using Distributions
α = 0.5
model = SDEModel(1.0, 0.0, 10000.0, 0.1)
sde = SDE(x->sinpi(2*x), model)
X = rand(sde)
distribution = MvNormalCanon([k^(α+0.5) for k in 1:50])
Π = GaussianProcess([fourier(k) for k in 1:50], distribution)
postΠ = calculateposterior(Π, X, model)
```
"""
calculateposterior(Π, X, model::SDEModel) = calculateposterior(Π, X, model.σ)
calculateposterior(Π::FaberSchauderExpansionWithGaussianCoefficients, X, model::SDEModel) = calculateposterior(Π, X, model.σ)
function calculateposterior(Π, X, σ)
    σXt = calculateσXt(σ, X.samplevalues[1:end-1])
    ψXt = [f.(X.samplevalues[1:end-1]) for f in Π.basis]
    lengthΠ = length(Π)
    precisionprior = invcov(Π.distribution)
    girsanovvector = calculategirsanovvector(lengthΠ, X.samplevalues, ψXt, σXt)
    girsanovmatrix = calculategirsanovmatrix(lengthΠ, X.timeinterval, ψXt, σXt)
    precisionmatrixposterior = girsanovmatrix + precisionprior
    potentialposterior = girsanovvector + precisionprior * mean(Π.distribution)
    posteriordistributiononcoefficients = MvNormalCanon(potentialposterior, precisionmatrixposterior)
    return GaussianProcess(Π.basis, posteriordistributiononcoefficients)
end
function calculateposterior(Π::FaberSchauderExpansionWithGaussianCoefficients, X, σ)
    samplevaluesmod1 = mod.(X.samplevalues[1:end-1], 1.0)
    σXt = calculateσXt(σ, X.samplevalues[1:end-1])
    ψXt = [ψ.(X.samplevalues[1:end-1]) for ψ in Π.basis]
    # You only need to use the values which are in the support of the function.
    samplevalueindices = [Π.leftboundssupport[i] .≤
        samplevaluesmod1 .≤ Π.rightboundssupport[i] for i in 1:length(Π)]
    girsanovvector = calculategirsanovvector(length(Π), samplevalueindices,
        X.samplevalues, ψXt, σXt)
    girsanovmatrix = calculategirsanovmatrix(Π, samplevalueindices,
        X.timeinterval, ψXt, σXt)
    precisionprior = invcov(Π.distribution)
    precisionmatrixposterior = girsanovmatrix + precisionprior
    potentialposterior = girsanovvector + precisionprior * mean(Π.distribution)
    posteriordistributiononcoefficients = MvNormalCanon(potentialposterior, precisionmatrixposterior)
    return FaberSchauderExpansionWithGaussianCoefficients(Π.higestlevel, posteriordistributiononcoefficients)
end

function calculateposteriorcoefficients(d::GaussianVector,
        X::SamplePath, model::SDEModel)::GaussianProcess
    σXt = calculateσXt(model.σ, X.samplevalues[1:end-1])
    ψXt = [f.(X.samplevalues[1:end-1]) for f in Π.basis]
    lengthΠ = length(d)
    covariancematrixprior = Matrix(cov(Π.distribution))
    precisionprior = inv(covariancematrixprior)
    girsanovvector = calculategirsanovvector(lengthΠ, X.samplevalues, ψXt, σXt)
    girsanovmatrix = calculategirsanovmatrix(lengthΠ, X.timeinterval, ψXt, σXt)
    precisionmatrixposterior = girsanovmatrix + precisionprior
    potentialposterior = girsanovvector + precisionprior * mean(Π.distribution)
    meanposterior =  covariancematrixprior * potentialposterior
    posteriordistribution = GaussianVector(meanposterior,
        precisionmatrixposterior^(-0.5))
    return GaussianProcess(Π.basis, posteriordistribution)
end

function GibbsIter(siteration, A, B, meanGPPost, precGPPost, numberofbasisfunctions, X, ψXt, σXt)
    meanGPPost /= sqrt(siteration)
    precGPPost *= sqrt(siteration)
    girsanovvector = calculategirsanovvector(numberofbasisfunctions, X.samplevalues, ψXt, σXt)
    girsanovmatrix = calculategirsanovmatrix(numberofbasisfunctions, X.timeinterval, ψXt, σXt)
    precGPPost += Matrix(girsanovmatrix)
    # Lijn hieronder moet de rekenfout hersteld worden.
    potentialGPPost = girsanovmatrix + meanGPPost
    meanGPPost = precGPPost \ potentialGPPost
    distGPPost = GaussianVector(meanGPPost, sqrt(inv(precGPPost)))
    ϴ = rand(distGPPost)
    siteration = rand(Gamma(A+numberofbasisfunctions/2, B + 0.5 * (ϴ - meanGPPost)' *
        potentialGPPost * (ϴ - meanGPPost)))
    return (siteration, meanGPPost, precGPPost, ϴ)
end

function GibbsSampler(Π::S, A::Float64, B::Float64, X::SamplePath, model::SDEModel,
        numSamples::Int, S0 = 1.0, burnin::Int = 100, intercept::Int = 1) where {S<:AbstractGaussianProcess}
    meanGPPost = mean(Π.distribution)
    precGPPost = inv(Matrix(cov(Π.distribution)))
    numberofbasisfunctions = convert(Float64, length(Π))

    σXt = calculateσXt(model.σ, X.samplevalues[1:end-1])
    ψXt = [f.(X.samplevalues[1:end-1]) for f in Π.basis]

    siteration = S0

    for i in 1:burnin
        siteration, meanGPPost, precGPPost = GibbsIter(siteration, A, B, meanGPPost,
            precGPPost, numberofbasisfunctions, X, ψXt, σXt)
    end

    samplePostS = Array{Float64}(undef, numSamples)
    samplePostϴ = Array{Float64}(undef, numSamples, numberofbasisfunctions)
    for i in 1:numSamples
        for j in 1:(intercept+1)
            siteration, meanGPPost, precGPPost, ϴ = GibbsIter(siteration, A, B,
                meanGPPost, precGPPost, numberofbasisfunctions, X, ψXt, σXt)
        end
        samplePostS[i], samplePostϴ[i,:] = siteration, ϴ
    end
    return (samplePostS, samplePostϴ)
end


#' # Representing data in a computer
#' The core of data science and machine learning is **data**: we are interested in extracting knowledge from data.
#'
#' But how exactly do computers represent data? Let's find out exactly what an "artificial intelligence" has at its disposal to learn from.
import JuliaAcademyData
JAD = JuliaAcademyData

#' ## Data is represented as arrays
#'
#' Let's take a look at some fruit. Using the `Images.jl` library, we can load in some images:
import Images

#' Load apple
apple = Images.load("data/10_100.jpg")
#' Load banana
banana = Images.load("data/104_100.jpg")

#' Here we have images of apples and bananas. We would eventually like to build a program that can automatically distinguish between the two. However, the computer doesn't "see" an apple or a banana; instead, it just sees numbers.
#'
#' An image is encoded in something called an **array**, which is like a container that has boxes or slots for individual pieces of data:
#'
#' An array is a bunch of numbers in connected boxes; the figure above shows a 1-dimensional array. Our images are instead 2-dimensional arrays, or matrices, of numbers, arranged something like this:
#'
#'
#' ![array](data/array2d_s.png)
#'

typeof(apple)
#'
size(apple)
#' We can grab the datum stored in the box at row `i` and column `j` by *indexing* using square brackets: `[i, j]`. For example, let's get the pixel (piece of the image) in box $(40, 60)$, i.e. in the 40th row and 60th column of the image:
apple
#'
dump(apple[40,60])
#'
x = apple[40,60]
#'
apple[18:20,29:31]

#' We see that Julia displays a coloured box! Julia, via the `Colors.jl` package, is clever enough t'o display colours in a way that is useful to us humans!
#'
#' So, in fact, an image is a 2D array, in which each element of the array is an object (a collection of numbers) describing a coloured pixel.

#' ## Colors as numbers
#'
#' How, then, are these colors actually stored? Computers store colors in RGB format, that is they s'tore a value between 0 and 1 for each of three "channels": red, green, and blue. Here, 0 means n'one of that color and 1 means the brightest form of that color. The overall color is a c'ombination of those three colors.
#'
#' For example, we can pull out the `red` value using the function `red` applied to the color. Since internally the actual value is stored in a special format, we choose to convert it to a standard floating-point number using the `Float64` function:
Float64(Images.red(x))
#'

import Statistics
STAT = Statistics

funcs = [Images.red, Images.green, Images.blue]
imgs = [apple, banana]
A = [ STAT.mean(float.( f.(img) )) for f = funcs, img = imgs ]
#'

import Plots
import Plotly
#Plots.gr()
Plots.plotly()
v = float.(Images.green.(apple[:]))
pt = Plots.histogram(v, color="red", label="apple", normalize=true, nbins=25)
v = float.(Images.green.(banana[:]))
Plots.histogram!(pt, v, color="yellow", label="banana", normalize=true, nbins=25)
#'
pixel = apple[40,60]

red = Float64(Images.red(pixel))
green = Float64(Images.green(pixel))
blue = Float64(Images.blue(pixel))
println("The RGB values are ($red, $green, $blue)")

pixel = apple[1,1]

red = Float64(Images.red(pixel))
green = Float64(Images.green(pixel))
blue = Float64(Images.blue(pixel))
println("The RGB values are ($red, $green, $blue)")

R_of_apple = Float64.(Images.red.(apple))
STAT.mean(R_of_apple)
#'

R_of_banana = Float64.(Images.red.(banana))
STAT.mean(R_of_banana)
#'

G_of_banana = Float64.(Images.green.(banana))
STAT.mean(G_of_banana)

Ans = Plots.histogram(R_of_apple[:],color=:red,label="redness in the apple")

#' Julia's [mathematical standard library](https://docs.julialang.org/en/stable/stdlib/math/#Mathematics-1) has many mathematical functions built in. One of them is the `mean` function, which computes the average value. If we apply this to our apple:

STAT.mean(R_of_apple)

#' ## A quick riddle
#' Here's a quick riddle. Let's check the average value of red in the image of the banana.
R_of_banana = Float64.(Images.red.(banana))
STAT.mean(R_of_banana)
#' Oh no, that's more red than our apple? This isn't a mistake and is actually true! Before you move onto the next exercise, examine the images of the apple and the banana very carefully and see if you can explain why this is expected.

#' #### Exercise 1
#'
#' What is the average value of blue in the banana?
B_of_banana = Float64.(Images.blue.(banana))
STAT.mean(B_of_banana)

#' #### Exercise 2
#'
#' Does the banana have more blue or more green?
G_of_banana = Float64.(Images.green.(banana))
STAT.mean(G_of_banana)
#' which gives approximately `0.88`. The banana has more green on average.


abstract type ParallelType end
struct Parallel{T1<:AbstractVector, T2<:AbstractVector, T3 <: Symbol, T4<: Real} <: ParallelType
    real::T1
    simulated::T1
    simulated_bounds::T2
    resampled::T1
    resampled_bounds::T2
    iter::T4
    reduction_method::T3
    correlation_method::T3
end

struct ParallelSets{T <: ParallelType}
    FA::T
    PCA::T
end

"""
    parallel(data, niter, f = fa)
Parallel Analysis.

# Arguments
- `data`
- `niter` is a size of simulation that generate (normal) random data matrix, calculate R, correlation matrix and its eigen values.
- `f` Function for dimension reduction. Default is `fa`.

# Values
- `real` Eigen values from read data.
- `simulated`, `resampled` Mean of eigen values from simulated data.
- `simulated_bound`, `resampled_bounds` 2.5th and 97.5th perceintile rank values of simulated eigen values.
- `iter` Number of iterations (of the simulation).
- `reduncion_method`
- `correlation_method`

# Example
```julia
julia> using ParallelAnalysis, Random, StatsPlots
julia> begin Random.seed!(1234)
           a = rand(Uniform(0.5, 2.0), 30)
           b = [sort(rand(Uniform(-3, 3), 4); rev = false) for i in 1:30]
           θ = rand(Normal(0, 1), 3000)
           resp = generate_response(θ, a, b)
       end;

julia> par_fit1 = parallel(resp, 10, x -> fa(x; cor_method = :Polychoric))
Progress: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| Time: 0:00:02
Parallel Analysis:
    Suggested that the number of factors is 1 (based on resampling)
    Suggested that the number of componets is 1 (based on resampling)

julia> plot(par_fit1) # visualize a result of parallel analysis

julia> plot(par_fit1, markershape = :none) # Don't show noisy makers.

```

"""
function parallel(data, niter, f = fa)
    n, j = size(data)
    fit = f(data)
    # println("Analyze read data.")
    if fit.cor == :Pearson
        V = cor(Matrix(data))
    elseif fit.cor == :Polychoric
        V = polycor(data)
    end
    eig_real_pca = sort!(eigvals(Symmetric(V)); rev = true)
    V[diagind(V)] = communalities(fit)
    eig_real_fa = sort!(eigvals(Symmetric(V)); rev = true)

    # println("Prepare matrix.")
    eig_sim_fa = Matrix{eltype(eig_real_fa)}(undef, niter, length(eig_real_fa))
    eig_sim_pca = Matrix{eltype(eig_real_pca)}(undef, niter, length(eig_real_pca))
    rsm_sim_fa = similar(eig_sim_fa)
    rsm_sim_pca = similar(eig_sim_pca)

    prg = Progress(niter)
    # println("Start simulation!")
    Threads.@threads for i in 1:niter

        W = random_sample(data)
        Z = randn(n, j)

        # resample data
        rsm_fit = f(W)
        M = rsm_fit.mat
        rsm_sim_pca[i, :] = sort!(eigvals(Symmetric(M)); rev = true)
        M[diagind(M)] = communalities(rsm_fit)
        rsm_sim_fa[i, :] = sort!(eigvals(Symmetric(M)); rev = true)

        # simulated data
        sim_fit = fa_pearson(Z)
        R = sim_fit.mat
        eig_sim_pca[i, :] = sort!(eigvals(Symmetric(R)); rev = true)
        R[diagind(R)] = communalities(sim_fit)
        eig_sim_fa[i, :] = sort!(eigvals(Symmetric(R)); rev = true)

        next!(prg)
    end

    # summarize
    eig_sim_fa_mean = mean(eig_sim_fa, dims = 1)[:]
    eig_sim_fa_bounds = map(x -> quantile(x, [0.025, 0.975]), eachcol(eig_sim_fa))
    rsm_sim_fa_mean = mean(rsm_sim_fa, dims = 1)[:]
    rsm_sim_fa_bounds = map(x -> quantile(x, [0.025, 0.975]), eachcol(rsm_sim_fa))

    eig_sim_pca_mean = mean(eig_sim_pca, dims = 1)[:]
    eig_sim_pca_bounds = map(x -> quantile(x, [0.025, 0.975]), eachcol(eig_sim_pca))
    rsm_sim_pca_mean = mean(rsm_sim_pca, dims = 1)[:]
    rsm_sim_pca_bounds = map(x -> quantile(x, [0.025, 0.975]), eachcol(rsm_sim_pca))

    ps = ParallelSets(
        Parallel(eig_real_fa, eig_sim_fa_mean, eig_sim_fa_bounds, rsm_sim_fa_mean, rsm_sim_fa_bounds, niter, :FA, Symbol(fit.cor)),
        Parallel(eig_real_pca, eig_sim_pca_mean, eig_sim_pca_bounds, rsm_sim_pca_mean, rsm_sim_pca_bounds, niter, :PCA, Symbol(fit.cor))
    )

    return ps
end

function findnfactors(x, y)
    for i in axes(x, 1)
        comp = x[i:end] .> y[i:end]
        findfirst(comp) != 1 && return i - 1
    end
    return 0
end

function Base.show(io::IO, x::Parallel)
    println(io, "Parallel Analysis: \nDimension reduction: $(x.reduction_method)")
    println(io, "Correlation method: $(x.correlation_method)")
    println(io, "Simulation size: $(x.iter)")
    println(io, "Suggested the number of factors is $(findnfactors(x.real, x.resampled)) (based on resampling)")
end

function Base.show(io::IO, x::ParallelSets)
    println(io, "Parallel Analysis:")
    println(io, "   Suggested that the number of factors is $(findnfactors(x.FA.real, x.FA.resampled)) (based on resampling)")
    println(io, "   Suggested that the number of componets is $(findnfactors(x.PCA.real, x.PCA.resampled)) (based on resampling)")
end

@recipe function f(pa::Parallel)

    @series begin
        # simulated mean
        label --> "$(pa.reduction_method) - Simulated mean"
        linecolor --> :red
        fillcolor --> :red
        linestyle --> :dash
        bds = hcat(pa.simulated_bounds...)
        l = bds[1, :] .- pa.simulated
        u = pa.simulated .- bds[2, :]
        ribbon := (l, u)
        pa.simulated
    end

    @series begin
        # simulated mean
        label --> "$(pa.reduction_method) - Resampled mean"
        linecolor --> :blue
        fillcolor --> :blue
        linestyle --> :dash
        bds = hcat(pa.resampled_bounds...)
        l = bds[1, :] .- pa.resampled
        u = pa.resampled .- bds[2, :]
        ribbon := (l, u)
        pa.resampled
    end

    @series begin
        label := ""
        linecolor --> :black
        linestyle --> :dot
        seriestype := :hline
        [1]
    end

    label --> "$(pa.reduction_method) - Real data"
    linecolor --> :black
    yguide --> "Eigen values"
    xguide --> "The number of components"
    title --> "Parallel Analysis over $(pa.iter) simulation."
    ylims --> (-Inf, Inf)
    pa.real
end

@recipe function f(x::ParallelSets)
    @series begin
        markershape --> :dtriangle
        markeralpha --> 0.5
        markercolor --> :white
        x.PCA
    end
    @series begin
        markershape --> :utriangle
        markeralpha --> 0.5
        markercolor --> :white
        x.FA
    end
    nothing
end


using LinearAlgebra
using PyAMG
using SparseArrays
using Test

L1d(N) = spdiagm( -1 => -ones(N-1), 0 => 2*ones(N), 1 => -ones(N-1) ) * N^2
function L2d(N)
  L1 = L1d(N)
  ID = sparse(1.0I, N, N)
  A = kron(ID, L1) + kron(L1, ID)
  b = ones(size(A,1))
  return A, b
end
function L3d(N)
  L1 = L1d(N)
  ID = sparse(1.0I, N, N)
  A = kron(L1, ID, ID) + kron(ID, L1, ID) + kron(ID, ID, L1)
  b = ones(size(A,1))
  return A, b
end

println("=================================================")
println("Test 1: Julia `\\` vs PyAMG Blackbox `solve` (2D) Laplacian")
A, b = L2d(100)
print(" \\ : ")
@time x1 = A \ b
print("PyAMG-Warmup: ")
@time x2 = PyAMG.solve(A, b; tol=1e-12, verb=false);
print("PyAMG: ")
@time x3 = PyAMG.solve(A, b; tol=1e-12, verb=false)
println("|x_\\ - x_amg|_∞ = ", norm(x3 - x1, Inf))
println("|A x_amg - b|_∞ = ", norm(A * x2 - b, Inf))
@test norm(x3 - x1, Inf) < 1e-10

println("=================================================")
println("Test 2: Julia `\\` vs PyAMG Blackbox `solve` (3D)")
A, b = L3d(30)
print(" \\ : ")
@time x1 = A \ b
print("PyAMG-Warmup: ")
@time x2 = PyAMG.solve(A, b; tol=1e-12, verb=false)
print("PyAMG: ")
@time x3 = PyAMG.solve(A, b; tol=1e-12, verb=false)
println("|x_\\ - x_amg|_∞ = ", norm(x3-x1, Inf))
println("|A x_amg - b|_∞ = ", norm(A*x2 - b, Inf))
@test norm(x3-x1, Inf) < 1e-9

println("=================================================")
println("Test 3: RugeStubenSolver (3D)")
println("        50 x 50 x 50 grid = 125k dofs ")
A, b = L3d(50)
print("Create solver: ")
@time amg = RugeStubenSolver(A)
print("First solve: ")
@time x1 = solve(amg, b)
print("Second solve: (tol 1e-9)")
@time x2 = solve(amg, b; tol=1e-9)
print("Third solve: (tol 1e-6)")
@time x3 = solve(amg, b; tol=1e-6)
println("|A x_amg - b|_∞ = ", norm(A * x3 - b, Inf))
@test norm(A * x2 - b, Inf) < 1e-8

println("=================================================")
println("Test 4: \\, ldiv, and kwargs")
A, b = L2d(50)
amg = RugeStubenSolver(A)
x_solve = solve(amg, b, tol=1e-6, cycle="V", accel="cg")
set_kwargs!(amg, tol=1e-6, cycle="V", accel="cg")
x_bs = amg \ b
x_ldiv = LinearAlgebra.ldiv!(similar(b), amg, b)
@test x_solve == x_bs
@test x_solve == x_ldiv

println("=================================================")
println("Test 5: AMG as a preconditioner")
println("        100 x 100 Dirichlet problem, TOL = 1e-4")
println("        PyAMG vs CG vs PCG  (using IterativeSolvers)")
A, b = L2d(100)
amg = RugeStubenSolver(A)
M = aspreconditioner(amg)
using IterativeSolvers
TOL = 1e-4
println("Plain CG:")
@time x_cg = IterativeSolvers.cg(A, b; tol=TOL)
@time x_cg = IterativeSolvers.cg(A, b; tol=TOL)
println("PyAMG-preconditionerd CG:  (see `aspreconditioner`)")
@time x_pcg = IterativeSolvers.cg(A, b; Pl=M, tol=TOL)
@time x_pcg = IterativeSolvers.cg(A, b; Pl=M, tol=TOL)
println("PyAMG solver")
@time x_pyamg = PyAMG.solve(amg, b; tol=TOL*1e-2, accel="cg")
@time x_pyamg = PyAMG.solve(amg, b; tol=TOL*1e-2, accel="cg")
x = A \ b
println("|x_cg-x| = ", norm(x_cg - x), " \n",
         "|x_pcg-x| = ", norm(x_pcg - x), "\n",
         "|x_pyamg-x| = ", norm(x_pyamg - x) )
@test norm(x_cg - x) < 1e-5
@test norm(x_pcg - x) < 1e-5
@test norm(x_pyamg - x) < 1e-5


using Statistics

struct GaussNum{T<:Real} <: Real
    μ::T
    σ::T
end
Statistics.mean(x::GaussNum) = x.μ
Statistics.var(x::GaussNum) = x.σ^2
Statistics.std(x::GaussNum) = x.σ
GaussNum(x,y) = GaussNum(promote(x,y)...)
±(x,y) = GaussNum(x,y)
Base.convert(::Type{T}, x::T) where T<:GaussNum = x
Base.convert(::Type{GaussNum{T}}, x::Number) where T = GaussNum(x,zero(T))
Base.promote_rule(::Type{GaussNum{T}}, ::Type{S}) where {T,S} = GaussNum{T}
Base.promote_rule(::Type{GaussNum{T}}, ::Type{GaussNum{T}}) where T = GaussNum{T}

isuncertain(x::GaussNum) = x.σ!=0
isuncertain(x::Number) = false

# TODO: add AD/arithmetic funcs

using Plots
@recipe function plot(ts::AbstractVector, xs::AbstractVector{<:GaussNum})
    # you can set a default value for an attribute with `-->`
    # and force an argument with `:=`
    μs = [x.μ for x in xs]
    σs = [x.σ for x in xs]
    @series begin
        :seriestype := :path
        # ignore series in legend and color cycling
        primary := false
        linecolor := nothing
        fillcolor := :gray
        fillalpha := 0.5
        fillrange := μs .- σs
        # ensure no markers are shown for the error band
        markershape := :none
        # return series data
        ts, μs .+ σs
    end
    ts, μs
end


"""
SkewNormal(ξ, ω, α)
    The *skew normal distribution* is a continuous probability distribution
    that generalises the normal distribution to allow for non-zero skewness.
External links
* [Skew normal distribution on Wikipedia](https://en.wikipedia.org/wiki/Skew_normal_distribution)
* [Discourse](https://discourse.julialang.org/t/skew-normal-distribution/21549/7)
* [SkewDist.jl](https://github.com/STOR-i/SkewDist.jl)
"""
struct SkewNormal{T<:Real} <: ContinuousUnivariateDistribution
    ξ::T
    ω::T
    α::T
    SkewNormal{T}(ξ::T, ω::T, α::T) where {T} = new{T}(ξ, ω, α)
end

function SkewNormal(ξ::T, ω::T, α::T; check_args::Bool=true) where {T <: Real}
    @check_args SkewNormal (ω, ω > zero(ω))
    return SkewNormal{T}(ξ, ω, α)
end

SkewNormal(ξ::Real, ω::Real, α::Real; check_args::Bool=true) = SkewNormal(promote(ξ, ω, α)...; check_args=check_args)
SkewNormal(ξ::Integer, ω::Integer, α::Integer; check_args::Bool=true) = SkewNormal(float(ξ), float(ω), float(α); check_args=check_args)
SkewNormal(α::Real=0.0) = SkewNormal(zero(α), one(α), α; check_args=false)

@distr_support SkewNormal -Inf Inf

#### Conversions
convert(::Type{SkewNormal{T}}, ξ::S, ω::S, α::S) where {T <: Real, S <: Real} = SkewNormal(T(ξ), T(ω), T(α))
convert(::Type{SkewNormal{T}}, d::SkewNormal{S}) where {T <: Real, S <: Real} = SkewNormal(T(d.ξ), T(d.ω), T(d.α), check_args=false)

#### Parameters
params(d::SkewNormal) = (d.ξ, d.ω, d.α)
@inline partype(d::SkewNormal{T}) where {T<:Real} = T

#### Statistics
delta(d::SkewNormal) = d.α / √(1 + d.α^2)
mean_z(d::SkewNormal) = √(2/π) * delta(d)
std_z(d::SkewNormal) = 1 - (2/π) * delta(d)^2

mean(d::SkewNormal) = d.ξ + d.ω * mean_z(d)
var(d::SkewNormal) = abs2(d.ω) * (1 - mean_z(d)^2)
std(d::SkewNormal) = √var(d)
skewness(d::SkewNormal) = ((4 - π)/2) * (mean_z(d)^3/(1 - mean_z(d)^2)^(3/2))
kurtosis(d::SkewNormal) = 2 * (π-3) * ((delta(d) * sqrt(2/π))^4/(1-2 * (delta(d)^2)/π)^2)

# no analytic expression for max m_0(d) but accurate numerical approximation
m_0(d::SkewNormal) = mean_z(d) - (skewness(d) * std_z(d))/2 - (sign(d.α)/2) * exp(-2π/abs(d.α))
mode(d::SkewNormal) = d.ξ + d.ω * m_0(d)

#### Evalution
pdf(d::SkewNormal, x::Real) = (2/d.ω) * normpdf((x-d.ξ)/d.ω) * normcdf(d.α * (x-d.ξ)/d.ω)
logpdf(d::SkewNormal, x::Real) = log(2) - log(d.ω) + normlogpdf((x-d.ξ) / d.ω) + normlogcdf(d.α * (x-d.ξ) / d.ω)
#cdf requires Owen's T function.
#cdf/quantile etc

mgf(d::SkewNormal, t::Real) = 2 * exp(d.ξ * t + (d.ω^2 * t^2)/2 ) * normcdf(d.ω * delta(d) * t)

cf(d::SkewNormal, t::Real) = exp(im * t * d.ξ - (d.ω^2 * t^2)/2) * (1 + im * erfi((d.ω * delta(d) * t)/(sqrt(2))) )

#### Sampling
function rand(rng::AbstractRNG, d::SkewNormal)
    u0 = randn(rng)
    v = randn(rng)
    δ = delta(d)
    u1 = δ * u0 + √(1-δ^2) * v
    return d.ξ + d.ω * sign(u0) * u1
end

## Fitting  # to be added see: https://github.com/STOR-i/SkewDist.jl/issues/3


infile = "./src/Day10/input.txt"
input = parse.(Int,readlines(infile))

s = sort(input)
push!(s, maximum(s) + 3)
pushfirst!(s, 0)
d = diff(s)
ones = count(i -> i == 1,d)
threes =  count(i -> i == 3,d)
 ones*threes

test1 = sort([16, 10, 15, 5, 1, 11, 7, 19, 6, 12, 4 ])
pushfirst!(test1, 0)
diffs = diff(test1)

# from a particular index, count all the ways to get to the end
function countways!(pathdict, adapters, idx)
    if haskey(pathdict, idx)
        return pathdict[idx]
    end
    if idx == length(adapters)
        return 1
    end
    count = 0
    # can hop by size 1?
    if adapters[idx+1] - adapters[idx] <= 3
        # then this hop is valid, and need number of subsequent hops
        count += countways!(pathdict, adapters, idx + 1)
    end
    # can hop by 2 positions?
    if idx+2 <= length(adapters) && adapters[idx + 2] - adapters[idx] <= 3
        # then this hop is valid, and need number of subsequent hops
        count += countways!(pathdict, adapters, idx + 2)
    end
    # can hop by 3  positions?
    if idx+3 <= length(adapters) && adapters[idx + 3] - adapters[idx] <= 3
        # then this hop is valid, and need number of subsequent hops
        count += countways!(pathdict, adapters, idx + 3)
    end
    pathdict[idx] = count
end

pathdict = Dict{Int,Int}()
countways!(pathdict, adapters, 1)


for n in [1, 3, 4, 5, 6]
    M = matrixdepot("magic", n)
    @test sum(M, dims=1) == sum(M, dims=2)'
    k = rand(1:n)
    @test sum(M, dims=1)[k] == sum(M, dims=2)'[k]
    # diagnoal == antidiagnoal
    p = [n:-1:1;]
    @test sum(diag(M)) == sum(diag(M[:,p]))
end
@test size(matrixdepot("magic", 2)) == (2, 2)

println("'magic' passed test...")


using Test

using Dash, DashHtmlComponents, DashCoreComponents, DashBootstrapComponents
using PlotlyJS
using DataFrames

using Classic
#cc = Classic

function run_astar()
    maze = cc.new_maze(10, 10, (1,1), (10,10), 0.2)

    # create functions for bfs()
    is_goal(pt)     = cc.is_goal(maze.goal, pt)
    next_points(pt) = cc.next_points(maze.grid, pt)
    heuristic(pt)   = cc.manhattan_distance(pt, maze.goal)

    node = cc.a_star((1,1), is_goal, next_points, heuristic)

    if isnothing(node)
        println("Path not found")
        display(maze.grid) # print the unsovled maze
    else
        cc.mark_path!(maze.grid, node, start=maze.start, goal=maze.goal)
        display(maze.grid) # print the maze and the path
    end
end

function run_bfs()
    maze = cc.new_maze(10, 10, (1,1), (10,10), 0.2)

    # create functions for bfs()
    is_goal(pt)     = cc.is_goal(maze.goal, pt)
    next_points(pt) = cc.next_points(maze.grid, pt)

    node = cc.bfs((1,1), is_goal, next_points)

    if isnothing(node)
        println("Path not found")
        display(maze.grid) # print the unsovled maze
    else
        cc.mark_path!(maze.grid, node, start=maze.start, goal=maze.goal)
        display(maze.grid) # print the maze and the path
    end
end

function run_cell()
    x = cc.EMPTY
    y = cc.START

    println("x = $(string(x)), y = $(string(y))")
end

"""
    run_dfs()

Use depth-first search to solve a maze.
"""
function run_dfs()
    maze = cc.new_maze() #new_maze(10,10, [1,1], [10,10], 0.2)
    #@show maze
    #display(maze.grid)

    #node = ch2.Node()
    #locations = ch2.successors(maze.grid, (2,2))
    #@show locations

    # pass this to dfs(..., successors)
    successors(here) = cc.successors(maze.grid, here)
    # pass this to dfs(..., goal_test, ...)
    is_goal(here) = cc.is_goal(maze.goal, here)

    node = cc.dfs((1,1), is_goal, successors)
    if isnothing(node)
        println("Path not found")
        display(maze.grid)
    else
        cc.mark_path!(maze.grid, node, start=maze.start, goal=maze.goal)
        #@show maze
        display(maze.grid)
    end
    #@show node
end

function run_bfs_gui()
    maze = new_maze(10, 10, (1,1), (10,10), 0.2)

    # create functions for bfs()
    is_goal(pt)     = Classic.is_goal(maze.goal, pt)
    next_points(pt) = Classic.next_points(maze.grid, pt)

    node = bfs((1,1), is_goal, next_points)

    if isnothing(node)
        println("Path not found")
        display(maze.grid) # print the unsovled maze
    else
        mark_path!(maze.grid, node, start=maze.start, goal=maze.goal)
        display(maze.grid) # print the maze and the path
    end

    app = dash(external_stylesheets=[dbc_themes.SPACELAB])

    navbar = dbc_navbarsimple([
        dbc_dropdownmenu([
            dbc_dropdownmenuitem("BFS", href="#bfs", external_link=true),
            # dbc_dropdownmenuitem("", href="", external_link=true),
        ],
        in_navbar=true, label="Section", caret=true, direction="left"),
    ],
    sticky="top", expand=true, brand="WDVG", brand_href="https://www.wdvgco.com",
    )
    content =[
        dbc_container([
            dbc_badge("Line: $(@__LINE__)", color="info", className="ml-1"),
            dcc_graph(
                figure = Plot(bfs_heatmap(maze)...),
                config = Dict(),
            )
        ], className="p-3 my-2 border rounded"),
    ]
    pushfirst!(content, navbar)
    app.layout = dbc_container(content)

    run_server(
        app,
        "0.0.0.0",
        8055,
        debug=true, # enables hot reload and more
    )
end

function bfs_heatmap(maze)
    z = convert(Matrix{Int64}, maze.grid)
    # z =  [[1, 20, 30], [20, 1, 60], [30, 60, 1]]
    # z = hcat(z...)

    # Normalize color scale
    # Details at
    # https://community.plotly.com/t/colors-for-discrete-ranges-in-heatmaps/7780/10
    colorscale = [
        [0, "rgb(250,250,250)"],
        [1/4, "rgb(0,0,0)"],
        [2/4, "rgb(250, 75, 0)"],
        [3/4, "rgb(250, 75, 0)"],
        [4/4, "rgb(0, 250, 100)"],
    ]

    traces = Vector{AbstractTrace}([
        heatmap(
            z = z,
            zmin = 0,
            zmax = 4,
            colorscale = colorscale,
            showscale = false,
        )
    ])
    layout = Layout(
        title = "Solve maze with BFS",
        xaxis = Dict(
            :ticks => "",
            :showticklabels => false,
        ),
        yaxis = Dict(
            :ticks => "",
            :showticklabels => false,
        ),
        annotations = [],
    )
    annotations = layout["annotations"] # shortcut
    annotation = Dict(
        :x => 0,
        :y => 0,
        :text => "", #"S",
        #:showtext => false,
        :showarrow => false,
    )
    push!(annotations, annotation)
    annotation = Dict(
        :x => 9,
        :y => 9,
        :text => "", #"G",
        :showarrow => false,
    )
    push!(annotations, annotation)

    return traces, layout
end

#run_astar()
#run_cell()
#run_dfs()
#run_bfs()

run_bfs_gui()

nothing

#A = test_manhattan_distance()
#A # Test.Pass or not


"""
Script for simulating a delta robot when accounting for the Base and platform radii
    - Lagrangian Formulation
        - position and velocity initial conditions
        - robot parameters (lengths and masses)
        - time span to simulate over
    - Hamiltonian Formulation
        - position and velocity (velocities are internally converted to momentas)
        - robot parameters (lengths and masses)
        - time span to simulate over
"""
using Revise
using Plots
using DifferentialEquations
using LinearAlgebra
using Sundials
using DASSL
using DeltaRobot

# Set initial conditions
u0, du0, diff_vars = delta_robot_ics(
    [pi/4; 3*pi/4; 0.0; pi/4; 3*pi/4; 0.0; pi/4; 3*pi/4; 0.0],
    [0.0; 0.0; 0.0; 0.0; 0.0; 0.0; 0.0; 0.0; 0.0],
    [0.0; 0.0; 0.0; 0.0; 0.0; 0.0; 0.0; 0.0; 0.0]
)
@assert length(u0) == length(du0) # make sure position and velocity initial condition vectors have same dimensionality
@assert length(u0) == length(diff_vars) # make sure position and vector denoting which equations are differential equations have the same dimensionality
@assert u0[10:18] == du0[1:9] # make sure initial conditions are consistent
delta_robot_params = DeltaRobotParams(2.5, 3.0, 4.0, 0.312, 0.410, 9.8, 0.114, 0.051, 0.1, 0.1, 0.1, 100.0, 5.0); # Actual robot parameters with random d-params
tspan = (0.0, 20.0) # time span to simulate the system over
dt = 0.05 # time step

# Setup & solve the DAE under Lagrangian formalism
L_prob = DAEProblem(DeltaRobot.L_delta_robot_base!, du0, u0, tspan, delta_robot_params, differential_vars=diff_vars) # statement of the DAE problem
L_sol = solve(L_prob, IDA(), saveat=dt) # try :BCG

# Animate the solution to the differential equation
anim_L = DeltaRobot.animate_robot(L_sol, delta_robot_params)
gif(anim_L, "robot_animation.gif", fps= 1 / dt)

# Second set of initial conditions
phi0 = deg2rad.([20., 20., 40.0])
x0 = DeltaRobot.for_kin(phi0, [3*pi/4, 0.0, 3*pi/4, 0.0, 3*pi/4, 0.0], delta_robot_params)
u0, du0, diff_vars = delta_robot_ics(x0[1], zeros(9), zeros(9))
@assert length(u0) == length(du0) # make sure position and velocity initial condition vectors have same dimensionality
@assert length(u0) == length(diff_vars) # make sure position and vector denoting which equations are differential equations have the same dimensionality
@assert u0[10:18] == du0[1:9] # make sure initial conditions are consistent
DeltaRobot.holonomic_constraint(x0[1], delta_robot_params)

# Setup & solve the DAE under Lagrangian formalism
L_prob = DAEProblem(DeltaRobot.L_delta_robot_base!, du0, u0, tspan, delta_robot_params, differential_vars=diff_vars) # statement of the DAE problem
L_sol = solve(L_prob, IDA(), saveat=dt)

# Animate the solution to the differential equation
anim_L = DeltaRobot.animate_robot(L_sol, delta_robot_params)
gif(anim_L, "demo.gif", fps=1/dt)

# Gravity compensation
phi0 = deg2rad.([20., 20., 20.0])
x0 = DeltaRobot.for_kin(phi0, [3*pi/4, 0.0, 3*pi/4, 0.0, 3*pi/4, 0.0], delta_robot_params)
u0, du0, diff_vars = delta_robot_ics(x0[1], zeros(9), zeros(9))
@assert length(u0) == length(du0) # make sure position and velocity initial condition vectors have same dimensionality
@assert length(u0) == length(diff_vars) # make sure position and vector denoting which equations are differential equations have the same dimensionality
@assert u0[10:18] == du0[1:9] # make sure initial conditions are consistent
DeltaRobot.holonomic_constraint(x0[1], delta_robot_params)
grav_comp_prob = DAEProblem(DeltaRobot.gravity_compensated_model!, du0, u0, tspan, delta_robot_params, differential_vars=diff_vars) # statement of the DAE problem
grav_comp_sol = solve(grav_comp_prob,  IDA(linear_solver=:BCG), saveat=dt, reltol=1e-6,abstol=1e-6)

# Animate the solution to the differential equation
grav_comp_anim = DeltaRobot.animate_robot(grav_comp_sol, delta_robot_params)
gif(grav_comp_anim, fps=1/dt)

phi0 = deg2rad.([80., 80.0, 40.0])
x0 = DeltaRobot.for_kin(phi0, [3*pi/4, 0.0, 3*pi/4, 0.0, 3*pi/4, 0.0], delta_robot_params)
u0, du0, diff_vars = delta_robot_ics(x0[1], zeros(9), zeros(9))
@assert length(u0) == length(du0) # make sure position and velocity initial condition vectors have same dimensionality
@assert length(u0) == length(diff_vars) # make sure position and vector denoting which equations are differential equations have the same dimensionality
@assert u0[10:18] == du0[1:9] # make sure initial conditions are consistent
DeltaRobot.holonomic_constraint(x0[1], delta_robot_params)
grav_comp_prob = DAEProblem(DeltaRobot.gravity_compensated_model!, du0, u0, tspan, delta_robot_params, differential_vars=diff_vars) # statement of the DAE problem
grav_comp_sol = solve(grav_comp_prob,  IDA(linear_solver=:BCG), saveat=dt, reltol=1e-6,abstol=1e-6)

# Animate the solution to the differential equation
grav_comp_anim = DeltaRobot.animate_robot(grav_comp_sol, delta_robot_params)
gif(grav_comp_anim, fps=1/dt)

prob = DAEProblem(DeltaRobot.L_delta_robot_base_pdff!, du0, u0, tspan, delta_robot_params, differential_vars=diff_vars)
sol = solve(prob, IDA(linear_solver=:BCG), saveat=dt, reltol=1e-6, abstol=1e-6)
anim = DeltaRobot.animate_robot(sol, delta_robot_params)
gif(anim, fps=1/dt)

# # PD 1
# PD_prob = DAEProblem(DeltaRobot.gravity_compensated_model_PD!, du0, u0, tspan, delta_robot_params, differential_vars=diff_vars)
# # IDA(), IDA(linear_solver=:BCG), DABDF2(), DImplicitEuler(), dassl()
# PD_sol = solve(PD_prob, IDA(linear_solver=:GMRES), saveat=dt, reltol=1e-2*2, abstol=1e-2*2)
# PD_anim = DeltaRobot.animate_robot(PD_sol, delta_robot_params)
# gif(PD_anim, fps=1/dt)
#
# # PD
# phi0 = deg2rad.([0., 0., 0.])
# x0 = DeltaRobot.for_kin(phi0, [3*pi/4, 0.0, 3*pi/4, 0.0, 3*pi/4, 0.0], delta_robot_params)
# u0, du0, diff_vars = delta_robot_ics(x0[1], zeros(9), zeros(9))
# @assert length(u0) == length(du0) # make sure position and velocity initial condition vectors have same dimensionality
# @assert length(u0) == length(diff_vars) # make sure position and vector denoting which equations are differential equations have the same dimensionality
# @assert u0[10:18] == du0[1:9] # make sure initial conditions are consistent
# DeltaRobot.holonomic_constraint(x0[1], delta_robot_params)
# PD_prob = DAEProblem(DeltaRobot.gravity_compensated_model_PD!, du0, u0, tspan, delta_robot_params, differential_vars=diff_vars)
# # IDA(), IDA(linear_solver=:BCG), DABDF2(), DImplicitEuler(), dassl()
# PD_sol = solve(PD_prob,  IDA(linear_solver=:BCG), saveat=dt, reltol=1e-4, abstol=1e-4)
# PD_anim = DeltaRobot.animate_robot(PD_sol, delta_robot_params)
# gif(PD_anim, fps=1/dt)


using WAVEFORM, LinearAlgebra

# Partitioning
n = 96
partition_size = 48
overlap = 0

y = partition(n, partition_size, overlap)

@test size(y) == (partition_size, Int(n / partition_size))

# Grid interpolator
n_fine = (100, 100)
n_coarse = (50, 50)
T = Float64
(f2c,c2f) = fine2coarse(n_fine,n_coarse,T)

x = randn(100, 100)
y = randn(50, 50)

@test dot(vec(y), f2c*vec(x)) - dot(vec(x), f2c'*vec(y)) < 1e-9
@test dot(vec(y), c2f'*vec(x)) - dot(vec(x), c2f*vec(y)) < 1e-9


struct VTMResult
    fit::Any
    data::VirusLoadData
    names::Vector
    p0::Vector
    u0::Vector
end

ViralTargetODE = @ode_def begin
    dT = -β*T*V
    dI₁ = β*T*V - k*I₁
    dI₂ = k*I₁ - δ*I₂/(K + I₂)
    dV = p*I₂ - c*V
end β k δ K p c

u0default = [1e+7, 75.0, 0.0, 1e-12]
lbdefault = [0.0, 0.0, 0.0, 0.0, 0.0]
ubdefault = [Inf, Inf, Inf, Inf, Inf]

function LogViralTargetModel(t, p, data, u0)
    tspan = (0.0, maximum(data.t))
    pars = (β = p[1],
            k = 4.0,
            δ = p[2],
            K = p[3],
            p = p[4],
            c = p[5])
    prob = ODEProblem(ViralTargetODE, u0, tspan, pars)
    sol = solve(prob, Tsit5(), dtmax=1e-2)
    log10.(sol(t)[end, :])
end

function LogEmpiricalViralTargetModel(t, p, data, u0)
    θ = minimum(data.v)
    LogV = LogViralTargetModel(t, p, data, u0)
    max.(LogV, θ)
end

function fitVTM(data::VirusLoadData, p0::Vector; lb=lbdefault, ub=ubdefault, u0=u0default)
    model(t, p) = LogEmpiricalViralTargetModel(t, p, data, u0)
    fit = curve_fit(model, data.t, data.v, p0, lower=lb, upper=ub)
    names = ["β", "δ", "K", "p", "c"]
    VTMResult(fit, data, names, p0, u0)
end

@recipe function f(result::VTMResult; empirical=false, p0=false)
    tmin, tmax = 0.0, maximum(result.data.t)
    vmin, vmax = extrema(result.data.v)
    param = p0 ? result.fit.param : result.p0
    tt = Vector(range(tmin, tmax, step=1e-2))
    x := tt
    if empirical
        y := LogEmpiricalViralTargetModel(tt, param, result.data, result.u0)
    else
        y := LogViralTargetModel(tt, param, result.data, result.u0)
    end
    linewidth --> 4
    linestyle --> :dash
    xaxis --> ("Time (days)", (tmin, tmax))
    yaxis --> (L"\log\,V(t)", (vmin-1, vmax+1))
    grid --> :none
    label --> "Viral target model"
    ()
end

function Base.summary(result::VTMResult)
    println(@sprintf "RSS = %.5e (convergence: %s)" sum(abs2, result.fit.resid) result.fit.converged)
    CI = try
        confidence_interval(result.fit, 0.05)
    catch
        false
    end
    for i in 1:length(result.fit.param)
        name = result.names[i]
        val = result.fit.param[i]
        p0 = result.p0[i]
        if CI == false
            println(@sprintf "  %s = %.3e (initial=%.3e)" name val p0)
        else
            CIl = CI[i][1]
            CIr = CI[i][2]
            println(@sprintf "  %s = %.3e (CI=(%.3e, %.3e), initial=%.3e)" name val CIl CIr p0)
        end
    end
end

du[1]  = dAngI = PRA - (c_ACE + c_NEP)*AngI - ln(2.)/h_AngI*AngI
du[2]  = dAngII = c_ACE*AngI - k_ACE2*ACE2m*AngII - (c_AT1R + c_AT2R)*AngII - ln(2.)*AngII/h_AngII
du[3]  = dAT1AngII = c_AT1R*AngII - ln(2.)*AT1RAngII/h_AT1R
du[4]  = dAT2AngII = c_AT2R*AngII - ln(2.)*AT2RAngII/h_AT2R
du[5]  = dAng17 = c_NEP*AngI + k_ACE2*ACE2m*AngII - ln(2.)*Ang17/h_Ang17
du[6]  = dACE2m = S_ACE2m - (c_ADAM17 + c_AT1R*AT1RAngII + c_Cov19*P_plasma/(K_mACE2i + P_plasma + c_ACE2i*ACE2p))*ACE2m - ln(2.)*ACE2m/h_ACE2
du[7]  = dACE2p = c_ADAM17*ACE2m - ln(2.)*ACE2p/h_ACE2
du[8]  = dACE2i = (c_AT1R*AT1RAngII + c_Cov19*P_plasma/(K_mACE2i + P_plasma + c_ACE2i*ACE2p))*ACE2m - ln(2.)*ACE2i/h_ACE2
du[9]  = dP_plasma = k_burst*P - k_ACE2P*ACE2p*P_plasma     - c_Cov19*P_plasma*ACE2m/(K_mACE2i + P_plasma _ c_ACE2i*ACE2p)
du[10] = dP = k_Cov19*c_Cov19*P_plasma*ACE2m/(K_mACE2i + P_plasma _ c_ACE2i*ACE2p) + k_pg*P*(1 - P/P_∞) - k_pm*s_m*P/(μ_m + k_mp*P) - k_pn*f(N) - k_burst*P
du[11] = dN = s_nr*R/(μ_nr + R) + k_AT1R*AT1RAngII - μ_n*N
du[12] = dD = k_dn*f_s(f(N)) - μ_d*D
du[13] = dC_A = s_c + k_cn*Q/(1 + Q) + k_ACE2p*ACE2p + k_Ang17*Ang17 + k_AT2R*AT2RAngII - μ_c*C_A


### Singletons ###
abstract type ASetConstant <: AConstantSymbol end

# abstract type AΩ <: ASetConstant end
# Ω = Term(Symbol("Ω"), AΩ)

# ∅ = Term(Symbol("∅"), ASetConstant)
@new_constant ASetConstant AΩ Ω Sets
@new_constant ASetConstant A∅ ∅ Sets
@new_constant ASetConstant AStatements Statements Sets
@new_constant ASetConstant ASets Sets Sets

### Functions ###
abstract type ASetFunction <: AFunctionSymbol end

@new_function(ASetFunction, ASetUnion, SetUnion, (Sets, Sets), Sets, 2, ∪, false)
@new_function(ASetFunction, ASetIntersection, SetIntersection, (Sets, Sets), Sets, 2, ∩, false)
@new_function(ASetFunction, ASetDifference, SetDifference, (Sets, Sets), Sets, 2, -, false)

@new_function(ASetFunction, AConditionSet, ConditionSet, (Variable, Statements), Sets, 2)
ConditionSet(x::Term, condition::Term) = Term(:ConditionSet, ConditionSet, (x, condition))  # {x | condition}
Base.show(io::IO, self::Term{<:AConditionSet}) = print(io, "$(Meta.parse("$(data(self))($(args(self)[1]), $(args(self)[2]))"))")


using NetworkEpidemics
using LightGraphs
using Plots
using Random
using Statistics
using LinearAlgebra
using GraphRecipes

cd("metaplex-networks/old")

include("networks/empirical_networks.jl")

Random.seed!(2021);

N = 1000
M = 20

h = contiguous_usa()
h = london_transport()
h = erdos_renyi(M, 0.3)

M = nv(h)
N = M*1000
graphplot(h)

begin
    p = sortperm(degree(h))
    eig = eigen(Array(normalized_laplacian(h)))
    V = Array{Float64,2}(undef, nv(h), nv(h))
    for i in 1:nv(h)
        V[:,i] .= eig.vectors[p,i]
    end
    # for each node, find the eigenvector whose entry for that node is highest in magnitude
    eigenmode = Vector{Int}(undef, nv(h))
    for i in 1:nv(h)
        eigenmode[i] = findmax(abs.(eig.vectors[i,:]))[2]
    end
end

histogram(degree(h), label="")
scatter!(degree_histogram(h), label="")

scatter(eig.values, label="")

heatmap(V', color=cgrad(:balance))

bar(eigenmode, label="")

β = 0.39
γ = 0.4
D = 0.1

critical_k = (N / M) * γ / β

ks = fill(50, M)
ks_ = copy(ks)
ks_[1] = 400

gs = [random_regular_graph(N, k) for k in ks]
mpx = HeterogeneousMetaplex(gs, h, D, SIS(β,γ))

mp = HeterogeneousMetapopulation(h, N, ks, D, SIS(β,γ))
mp_ = HeterogeneousMetapopulation(h, N, ks_, D, SIS(β,γ))

x0_i = fill(1, N)
x0_i[1:10] .= 2

x0_μ = [div(i-1,div(N,M))+1 for i in 1:N]
x0_μ = [mod(i,M)+1 for i in 1:N]

x0 = Array{Int,2}(undef, M, 2)
x0[2:M,:] .= [1000 0]
x0[1,:] .= [980, 20]

tmax = 100.0
nmax = 1000000
nsims = 100
nbins = 200

#ts_mpx, u_mpx = gillespie(mpx, [x0_i,x0_μ], nmax=nmax, tmax=tmax)
ts_mp, u_mp = gillespie(mp, x0, nmax=nmax, tmax=tmax)

u_mp

#plot(ts_mpx, u_mpx[2], label="")
plot(ts_mp, u_mp[2], label="")

#ts_av_mpx, u_av_mpx = average(mpx, [x0_i, x0_μ], nmax=nmax, tmax=tmax, nbins=nbins, nsims=nsims)
#ts_mf_mpx, u_mf_mpx = meanfield(mpx, [x0_i,x0_μ], tmax=tmax)

ts_av_mp, u_av_mp = average(mp, x0, nmax=nmax, tmax=tmax, nbins=nbins, nsims=nsims, progressbar=true)
ts_mf_mp, u_mf_mp = meanfield(mp, x0, tmax=200.0)
ts_mf_mp_, u_mf_mp_ = meanfield(mp_, x0, tmax=200.0)
#plot(ts_av_mpx, sum(u_av_mpx[2], dims=2)/N, label="Average", ylims=(0.0,1.0))
#plot!(ts_mf_mpx, sum(u_mf_mpx[2], dims=2)/N,  label="Meanfield")

plot(ts_av_mp, sum(u_av_mp[2], dims=2)/N, label="Average")
plot!(ts_mf_mp, sum(u_mf_mp[2], dims=2)/N, label="Meanfield")

plot(ts_mf_mp, sum(u_mf_mp[2], dims=2)/N, label="Meanfield", legend=:topleft)
plot!(ts_mf_mp_, sum(u_mf_mp_[2], dims=2)/N, label="Meanfield", legend=:topleft)

plot(ts_av_mp, u_av_mp[2], label="")
plot(ts_mf_mp, u_mf_mp[2], label="")

#plot(ts_av_mpx, u_av_mpx[2], label="")
#plot(ts_mf_mpx, u_mf_mpx[2], label="")

v = inv(eig.vectors)
ψ_mf_mp = copy(u_mf_mp[2])
for i in 1:size(u_mf_mp)[1]
   ψ_mf_mp[i,:] .= v * u_mf_mp[2][i,:]
end
plot(ts_mf_mp, ψ_mf_mp, label="")

plot(ts_mf_mp, (eig.vectors * u_mf_mp[2]')', label="")


using Crystallography: Lattice
using LinearAlgebra: I

export distortby, strainstate

# See https://link.springer.com/content/pdf/10.1007%2F978-3-7091-0382-1_7.pdf and https://doi.org/10.2138/am-1997-1-207
distortby(lattice::Lattice, strain::TensorStrain) =
    Lattice((I + strain.data) * lattice.data)
distortby(lattice::Lattice, strain::EngineeringStrain) =
    distortby(lattice, TensorStrain(strain))

strainstate(old::Lattice, new::Lattice) = TensorStrain(new.data / old.data - I)


using DelayEmbeddings, ChaosTools, OrdinaryDiffEq
using Test

println("\nTesting R-param. estimation...")

"""
    saturation_point(x, y; threshold = 0.01, dxi::Int = 1, tol = 0.2)
Decompose the curve `y(x)` into linear regions using `linear_regions(x, y; dxi, tol)`
and then attempt to find a saturation point where the the first slope
of the linear regions become `< threshold`.

Return the `x` value of the saturation point.
"""
function saturation_point(Ds, E1s; threshold = 0.01, kwargs...)
    lrs, slops = ChaosTools.linear_regions(Ds, E1s; kwargs...)
    i = findfirst(x -> x < threshold, slops)
    return i == 0 ? Ds[end] : Ds[lrs[i]]
end

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

@testset "Estimate Dimension" begin
    s_sin = sin.(0:0.1:1000)
    τ = 15
    Ds = 1:5
    E1s = DelayEmbeddings.estimate_dimension(s_sin, τ, Ds)
    E2s = DelayEmbeddings.stochastic_indicator(s_sin, τ, Ds)
    @test saturation_point(Ds,E1s; threshold=0.01) == 1

    # @test minimum(E2s) < 0.1 # THIS TEST FAILS

    ds = Systems.roessler();τ=15; dt=0.1
    data = trajectory(ds,1000;dt=dt,diffeq...)
    s_roessler = data[:,1]
    Ds = 1:5
    E1s = DelayEmbeddings.estimate_dimension(s_roessler, τ, Ds)
    @test saturation_point(Ds,E1s; threshold=0.1) ∈ [2, 3]
    E2s = DelayEmbeddings.stochastic_indicator(s_roessler, τ, Ds)
    @test minimum(E2s) < 0.3

    E1s = DelayEmbeddings.estimate_dimension(s_roessler, τ, Ds;metric = Chebyshev())
    @test saturation_point(Ds,E1s; threshold=0.1) ∈ [2, 3]

    ds = Systems.lorenz();τ=5; dt=0.01
    data = trajectory(ds,500;dt=dt,diffeq...)
    s_lorenz = data[:,1]
    Ds = 1:5
    E1s = DelayEmbeddings.estimate_dimension(s_lorenz, τ, Ds)
    E2s = DelayEmbeddings.stochastic_indicator(s_lorenz, τ, Ds)
    @test saturation_point(Ds,E1s; threshold=0.1) ∈ [2, 3]

    # @test minimum(E2s) < 0.1 # THIS TEST FAILS

    #Test against random signal
    E2s = DelayEmbeddings.stochastic_indicator(rand(100000), 1, 1:5)
    @test minimum(E2s) > 0.9

    # Test `fnn` method

    τ = 15
    Ds = 1:5
    number_fnn = DelayEmbeddings.estimate_dimension(s_sin, τ, Ds, "fnn")
    @test findfirst(number_fnn .≈ 0.0) == 1

    τ = 15
    Ds = 1:5
    number_fnn = DelayEmbeddings.estimate_dimension(s_roessler, τ, Ds, "fnn"; rtol=15)
    @test findfirst(number_fnn .≈ 0.0) ∈ [2, 3]

    τ = 1
    Ds = 1:5
    number_fnn = DelayEmbeddings.estimate_dimension(s_lorenz, τ, Ds, "fnn"; atol=1, rtol=3.0)
    @test findfirst(number_fnn .≈ 0.0) ∈ [2, 3]

    # Test `f1nn` method

    τ = 15
    Ds = 1:5
    ffnn_ratio = DelayEmbeddings.estimate_dimension(s_sin, τ, Ds, "f1nn")
    @test saturation_point(Ds,(1 .- ffnn_ratio); threshold=0.1) == 1

    τ = 15
    Ds = 1:5
    ffnn_ratio = DelayEmbeddings.estimate_dimension(s_roessler, τ, Ds, "f1nn")
    @test saturation_point(Ds,(1 .- ffnn_ratio); threshold=0.1) ∈ [2, 3]
    ffnn_ratio = DelayEmbeddings.estimate_dimension(s_roessler, τ, Ds, "f1nn";metric=Chebyshev())
    @test saturation_point(Ds,(1 .- ffnn_ratio); threshold=0.1) ∈ [2, 3]

    τ = 15
    Ds = 1:5
    ffnn_ratio = DelayEmbeddings.estimate_dimension(s_lorenz, τ, Ds, "f1nn")
    @test saturation_point(Ds,(1 .- ffnn_ratio); threshold=0.1) ∈ [2, 3]

end


using MatrixCoalescent
using Test

@testset "Rational" begin
    n=3
    mc = MatCoal(Rational{Int}, n)
    @test mc.nLineages == 3
    @test dim(mc) == n-1
    @test mc.beta[1] == 1//1
    @test mc.beta[2] == 3//1

    @test mc.rbeta[1] == 1//1
    @test mc.rbeta[2] == 1//3

    @test mc.gmat[1,1] == 3//2
    @test mc.gmat[1,2] == -3//2
    @test mc.gmat[2,1] == 0//1
    @test mc.gmat[2,2] == 1//1

    @test mc.hmat[1,1] == -3//2
    @test mc.hmat[1,2] == 1//2
    @test mc.hmat[2,1] == 0//1
    @test mc.hmat[2,2] == -1//3
end

@testset "Float64" begin
    n=3
    mc = MatCoal(Float64, n)
    @test mc.nLineages == 3
    @test dim(mc) == n-1
    @test mc.beta[1] == 1.0
    @test mc.beta[2] == 3.0

    @test mc.rbeta[1] == 1.0
    @test mc.rbeta[2] == 1.0/3.0

    @test mc.gmat[1,1] == 1.5
    @test mc.gmat[1,2] == -3.0/2.0
    @test mc.gmat[2,1] == 0.0/1.0
    @test mc.gmat[2,2] == 1.0

    @test mc.hmat[1,1] == -1.5
    @test mc.hmat[1,2] == 0.5
    @test mc.hmat[2,1] == 0.0
    @test mc.hmat[2,2] == -1.0/3.0

    eig = Vector{Float64}(undef, n-1)
    v = 1.0
    eigenvals!(eig, v, mc)

    @test eig[1] == exp(-1)
    @test eig[2] == exp(-3)

    pr = Vector{Float64}(undef, n-1)
    project!(pr, eig, mc)

    @test pr[1] ≈ 1.5 * (exp(-1) - exp(-3))
    @test pr[2] ≈ exp(-3)

    elen = Vector{Float64}(undef, n-1)
    interval_lengths!(elen, eig, mc)

    elen[1] ≈ 1.0 - 1.5*exp(-1) + 0.5*exp(-3)
    elen[2] ≈ 1.0/3.0 - exp(-3)/3.0
end

"""
For a given number of lineages (nLin) and across a range of time values,
calculate the projected probability vector (using project!) and the
expected interval lengths (using interval_lengths!) using 64-bit
floats and also using 256-bit floats. Return the largest absolute
difference between the low-precision and the high-precision versions
of this calculation.
"""
function estimate_error(nLin::Int)
    mcl = MatCoal(Float64, nLin)  # low precision
    mch = MatCoal(BigFloat, nLin) # high precision

    el = Vector{Float64}(undef, nLin-1)
    eh = Vector{BigFloat}(undef, nLin-1)
    xl = Vector{Float64}(undef, nLin-1)
    xh = Vector{BigFloat}(undef, nLin-1)

    err = 0.0

    for v in 0.0 : 0.5 : 9.5
        eigenvals!(el, v, mcl)
        eigenvals!(eh, BigFloat(v), mch)
        project!(xl, el, mcl)
        project!(xh, eh, mch)
        for (l,h) in zip(xl, xh)
            err = max(err, abs(l - Float64(h)))
        end
        interval_lengths!(xl, el, mcl)
        interval_lengths!(xh, eh, mch)
        for (l,h) in zip(xl, xh)
            err = max(err, abs(l - Float64(h)))
        end
    end
    return err
end

@testset "Accuracy" begin
    @test estimate_error(2)  < eps(Float64)
    @test estimate_error(4)  < eps(Float64)
    @test estimate_error(8)  < 4e-15
    @test estimate_error(16) < 3.5e-13
    @test estimate_error(32) < 4e-8
    @test estimate_error(34) < 6e-8
    @test estimate_error(35) < 3e-7
    @test_throws OverflowError estimate_error(36)
end


"""
    EdgeGradient

This type is used to generate an edge gradient landscape, where values change
as a bilinear function of the *x* and *y* coordinates. The direction is
expressed as a floating point value, which will be in *[0,360]*. The inner
constructor takes the mod of the value passed and 360, so that a value that is
out of the correct interval will be corrected.
"""
struct EdgeGradient <: NeutralLandscapeMaker
    direction::Float64
    EdgeGradient(x::T) where {T <: Real} = new(mod(x, 360.0))
end

EdgeGradient() = EdgeGradient(360rand())

function _landscape!(mat, alg::EdgeGradient) where {IT <: Integer}
    _landscape!(mat, PlanarGradient(alg.direction))
    mat .= -2.0abs.(0.5 .- mat) .+ 1.0
end


include("Misc/acb_root_ctx.jl")
include("Misc/Infinity.jl")
include("Misc/MSet.jl")
include("Misc/fmpr.jl")
include("Misc/FactoredElem.jl")
include("Misc/FactoredElemIntegers.jl")
include("Misc/Integer.jl")
include("Misc/Primes.jl")
include("Misc/coprime.jl")
include("Misc/Magma.jl")
include("Misc/Matrix.jl")
include("Misc/CRT.jl")  # for crt_env and modular_env
include("Misc/Poly.jl")
include("Misc/nmod_poly.jl")
include("Misc/MPoly.jl")
include("Misc/MPolyFactor.jl")
include("Misc/Residue.jl")
include("Misc/ResidueRingPoly.jl")
include("Misc/FiniteField.jl")
include("Misc/Arb.jl")
include("Misc/psibound.jl")
include("Misc/UnitsModM.jl")
include("Misc/toMagma.jl")
include("Misc/toNemo.jl")
include("Misc/Places.jl")
include("Misc/RatRecon.jl")
include("Misc/HNFstorjohann.jl")
include("Misc/RealField.jl")
include("Misc/serialize.jl")
include("Misc/Series.jl")
include("Misc/Screen.jl")
include("Misc/IJulia.jl")
include("Misc/Localization.jl")
include("Misc/OrdLocalization.jl")
include("Misc/jordan.jl")
include("Misc/PseudoPolynomial.jl")
include("Misc/UnsafeRational.jl")
include("Misc/FactoredElemWrapped.jl")
include("Misc/Fields.jl")
include("Misc/Cartesian.jl")
#include("Misc/RelFiniteField.jl")
#include("Misc/Plesken.jl")
#include("Misc/cohomology.jl")


export Integral

"""
A one-form is a map `V → K`, i.e., it is a generic function of one argument.

A one-form is applied to an argument using the `F(f)` syntax, which itself
invokes `form(F::OneForm, f)`. The `form` function can be specialized for
concrete types of `F` and `f`.

The main use cases in `DomainIntegrals.jl` are a lazy `Integral` object and a
quadratic form that derives from a bilinear form.
"""
abstract type OneForm end

(F::OneForm)(f) = form(F, f)

"`Integral` is a functional that represents integration of a function."
abstract type AbstractIntegral <: OneForm end

domain(F::AbstractIntegral) = F.domain
measure(F::AbstractIntegral) = F.measure
properties(F::AbstractIntegral) = F.properties

form(F::AbstractIntegral, f) = integral(f, domain(F), measure(F), properties(F)...)

struct Integral <: AbstractIntegral
    integrand
    domain
    measure
    properties
end

Integral(f, args...) = Integral(f, process_arguments(args...)...)

Integral(integrand, domain::Domain, measure::Measure, properties::Property...) =
    Integral(integrand, domain, measure, properties)

"""
A two-form is a map `V x W → K`, i.e., it is a generic function of two arguments.

The two-form is applied to two arguments using the `F(f,g)` syntax, which itself
invokes `form(F::TwoForm, f, g)`. The `form` function can be specialized for
concrete types of `F`, `f` and `g`.
"""
abstract type TwoForm end

(F::TwoForm)(f, g) = form(F, f, g)

"A bilinear form is linear in its two arguments."
abstract type BilinearForm <: TwoForm end

"A sesquilinear form is linear in the first and conjugate linear in the second argument."
abstract type SesquilinearForm <: TwoForm end

isbilinear(F::BilinearForm) = true
isbilinear(F::SesquilinearForm) = false

issesquilinear(F::BilinearForm) = false
issesquilinear(F::SesquilinearForm) = true

"""
Supertype of quadratic forms that derive from a two-form.

If `F` is a TwoForm, then we call `F(f,f)` the associated quadratic form.
"""
abstract type AbstractQuadraticForm <: OneForm end

# for convenience, assume the underlying form is stored in the field F
superform(F::AbstractQuadraticForm) = F.F

form(F::AbstractQuadraticForm, f) = quadraticform(superform(F), f)
quadraticform(F, f) = form(F, f, f)

"A quadratic form derived from a stored two-form."
struct QuadraticForm{FORM} <: AbstractQuadraticForm
    F   ::  FORM
end

"""
Apply a two-form to the given arguments.

`form(F::TwoForm, f, g)`

By default, `form` invokes `form1` which in turn invokes `form2`. The `form`
function itself can be specialized on the type of `F` without ambiguity.
Similarly, `form1` and `form2` can be specialized without ambiguity on the types
of `f` and `g` respectively.

Each of these functions can be specialized on all three types involved, as long
as the main type is at least as specific as any other specialization.
"""
form(F::TwoForm, f, g) = form1(F, f, g)

"Helper function for `form(F, f, g)` that can be specialized on the type of `f`."
form1(F, f, g) = form2(F, f, g)

"Helper function for `form(F, f, g)` that can be specialized on the type of `g`."
form2(F, f, g) = default_form(F, f, g)

functionproduct(f, g) = x -> f(x)*g(x)
functionconjugateproduct(f, g) = x -> f(x)*conj(g(x))

# By default we assume that the form is induced by a measure.
default_form(F::BilinearForm, f, g) =
    integral(functionproduct(f,g), measure(F))
default_form(F::SesquilinearForm, f, g) =
    integral(functionconjugateproduct(f,g), measure(F))

##################
## Inner products
##################

"An inner-product is a positive definite sesquilinear form."
abstract type InnerProduct <: SesquilinearForm end

# For convenience, assume the underlying form is stored in the field F.
superform(F::InnerProduct) = F.F

ishermitian(F::InnerProduct) = true
ispositivesemidefinite(F::InnerProduct) = true
ispositivedefinite(F::InnerProduct) = true

# an inner product is typically implemented in terms of another form
form(F::InnerProduct, f, g) = form(superform(F), f, g)

# An example concrete type follows. Other concrete types may be defined that
# return a form on-the-fly by defining `superform`, for example.

"A generic inner product that stores a positive definite Hermitian sesquilinear form."
struct GenericInnerProduct{FORM} <: InnerProduct
    F   ::  FORM
end

#########################
## Measure induced forms
#########################

"""
A bilinear form induced by a measure.

It is the case that `F(f,g)` equals `integral(x -> f(x)*g(x), measure(F))`.
"""
abstract type InducedBilinearForm <: BilinearForm end

issymmetric(F::InducedBilinearForm) = true
isconjugatesymmetric(F::InducedBilinearForm) = false
ishermitian(F::InducedBilinearForm) = false

"""
A sesquilinear form induced by a measure.

It is the case that `F(f,g)` equals `integral(x -> f(x)*conj(g(x)), measure(F))`.
"""
abstract type InducedSesquilinearForm <: SesquilinearForm end

issymmetric(F::InducedSesquilinearForm) = false
isconjugatesymmetric(F::InducedSesquilinearForm) = true
ishermitian(F::InducedSesquilinearForm) = isreal(measure(F))
ispositivesemidefinite(F::InducedSesquilinearForm) = ishermitian(F)

# Two concrete types follow. Other concrete types may be defined that do not
# store a measure, but merely return it on-the-fly by implementing `measure(F)`.

"A bilinear form induced by a generic measure."
struct MeasureBilinearForm{M} <: InducedBilinearForm
    measure     ::  M
end

"A sesquilinear form induced by a generic measure."
struct MeasureSesquilinearForm{M} <: InducedSesquilinearForm
    measure     ::  M
end


# This file is part of IntegerSequences.
# Copyright Peter Luschny. License is MIT.

(@__DIR__) ∉ LOAD_PATH && push!(LOAD_PATH, (@__DIR__))

module LandauConstants

using Nemo, NumberTheory, GeneralizedBinomial, Products
export V000120, V102376, V011371, V120738, V060818, V124399, V056982
export V001511, V046161, V001316, V163590, V327492, V005187, V001790
export V277233, V327495, V327493, V327491, V007814, V001803, V327494
export V006519, V000265, Bin, BitCount, EvenPart, OddPart

"""

* V277233(n) / 4^V005187(n) are the Landau constants. These constants are defined as G(n)`` = ∑_{j=0..n} g(j)^2`` with the normalized central binomial
```
    g(n) = (2n)! / (2^n n!)^2 = V001790(n) / V046161(n).
```
* V327495(n) / 4^V327492(n) are the rationals considered here. These numbers are defined as H(n)`` = ∑_{j=0..n} h(j)^2`` with the normalized swinging factorial
```
    h(n) = n! / (2^n ⌊n/2⌋!)^2 = V163590(n) / V327493(n).
```
* In particular, this means that we have the pure integer representations
```
    V277233(n) = ∑{k=0..n}(V001790(k) 2^{V005187(n) - V005187(k)})^2;
    V327495(n) = ∑{k=0..n}(V163590(k) 2^{V327492(n) - V327492(k)})^2.
```

* V000120, V102376, V011371, V120738, V060818, V124399, V056982, V001511, V046161, V001316, V163590, V327492, V005187, V001790, V277233, V327495, V327493, V327491, V007814, V001803, V327494, V006519, V000265, Bin, BitCount, EvenPart, OddPart
"""
const ModuleLandauConstants = ""

"""

Return the 2-adic valuation of ``n``.
"""
Bin(n::Int) = n == 0 ? 0 : valuation(n, 2)

"""

Return the even part of ``n``.
"""
EvenPart(n::Int) = 1 << Bin(n)

"""

Return the even part of ``n``.
"""
V006519(n::Int) = EvenPart(n)

"""

Return the odd part of ``n``.
"""
OddPart(n::Int) = n >> Bin(n)

"""

Return the odd part of ``n``.
"""
V000265(n::Int) = OddPart(n)

"""

Return the odd part of the swinging factorial ``n! / ⌊n/2⌋!^2``.
"""
function V163590(n::Int)
    (r = get(CacheV163590, n, 0)) > 0 && return r
    h = V163590(n - 1) << Bin(n)
    CacheV163590[n] = isEven(n) ? div(h, n) : h * n
end

const CacheV163590 = Dict{Int,Int}(0 => 1)

"""

Return the odd part of the swinging factorial of ``2n+1``.
"""
V001803(n) = V163590(2n+1)
#V001803(n) = sum(<<(V001790(k), V005187(n) - V005187(k)) for k ∈ 0:n)

"""

Return the odd part of the swinging factorial of ``2n``.
"""
V001790(n::Int) = V163590(2n)
# V001790(n) = Binomial(2n, n) >> BitCount(2n)

"""

Return valuation ``(n, 2)`` if ``4`` divides ``n`` else ``1 + (n mod 2)``.
"""
V327491(n::Int) = Divides(4, n) ? Bin(n) : mod(n, 2) + 1

"""

Return ``2n - bc(n) + mod(n, 2)`` where bc(n) is BitCount(n).
"""
V327492(n::Int) = 2n - BitCount(n) + mod(n, 2)

"""

Return ``2^{2n - bc(n) + mod(n, 2)}`` where bc(n) is BitCount(n).
"""
V327493(n::Int) = 2^(2n - BitCount(n) + mod(n, 2))
#V327493(n::Int) = 2^V327492(n)

"""

Return the 2-adic valuation of ``n``.
"""
V007814(n::Int) = Bin(n)

"""

Return the 2-adic valuation of ``2n``.
"""
V001511(n::Int) = Bin(2n)

"""

Return the number of ``1``'s in binary expansion of ``n``.
"""
BitCount(n::Int) = count_ones(n)
#sum(digits(n, base = 2))

"""

Return the number of ``1``'s in binary expansion of ``n``.
"""
V000120(n::Int) = BitCount(n)

"""

Return 2^BitCount(n). Should be called Glaisher's sequence since James Glaisher showed that odd binomial coefficients are counted by this sequence.
"""
V001316(n::Int) = 2^BitCount(n)

"""

Return 4^BitCount(n).
"""
V102376(n::Int) = 4^BitCount(n)

"""

Return ``n`` - BitCount(n).
"""
V011371(n::Int) = n - BitCount(n)

"""

Return ``2n`` - BitCount(n).
"""
V005187(n::Int) = 2n - BitCount(n)

"""

Return ``4n`` - BitCount(n).
"""
V120738(n::Int) = 4n - BitCount(n)

"""

Return ``2^{n - bc(n)}`` where bc(n) is BitCount(n).
"""
V060818(n::Int) = 2^(n - BitCount(n))

"""

Return ``2^{2n - bc(n)}`` where bc(n) is BitCount(n).
"""
V046161(n::Int) = 2^(2n - BitCount(n))
# V046161(n) = 2^V005187(n)

"""

Return ``4^{n - bc(n)}`` where bc(n) is BitCount(n).
"""
V124399(n::Int) = 4^(n - BitCount(n))

"""

Return ``4^{2n - bc(n)}`` where bc(n) is BitCount(n).
"""
V056982(n::Int) = 4^(2n - BitCount(n))
# V056982(n) = 4^V005187(n)

"""

Return the numerator of ``∑_{j=0..n} j!/(2^j ⌊j/2⌋!)^2``.
"""
V327494(n) = sum(<<(V163590(k), V327492(n) - V327492(k)) for k ∈ 0:n)

"""

Return the numerators of the Landau constants.
"""
V277233(n) = sum(<<(V001790(k), V005187(n) - V005187(k))^2 for k ∈ 0:n)

"""

Return the numerators of the swinging Landau constants.
"""
V327495(n) = sum(<<(V163590(k), V327492(n) - V327492(k))^2 for k ∈ 0:n)

#START-TEST-########################################################

using Test

function test()
    @testset "Landau" begin
        @test V006519(0) == 1
        @test V000265(0) == 0
        @test V327494(0) == 1
        @test V001803(0) == 1
        @test V007814(0) == 0
        @test V327491(0) == 0
        @test V327493(0) == 1
        @test V000120(0) == 0
        @test V102376(0) == 1
        @test V011371(0) == 0
        @test V120738(0) == 0
        @test V060818(0) == 1
        @test V124399(0) == 1
        @test V056982(0) == 1
        @test V001511(0) == 0
        @test V046161(0) == 1
        @test V001316(0) == 1
        @test V163590(0) == 1
        @test V327492(0) == 0
        @test V005187(0) == 0
        @test V001790(0) == 1
        @test V277233(0) == 1
        @test V327495(0) == 1

        @test V006519(10) == 2
        @test V000265(10) == 5
        @test V327494(10) == 403341
        @test V001803(10) == 969969
        @test V007814(10) == 1
        @test V327491(10) == 1
        @test V327493(10) == 262144
        @test V000120(10) == 2
        @test V102376(10) == 16
        @test V011371(10) == 8
        @test V120738(10) == 38
        @test V060818(10) == 256
        @test V124399(10) == 65536
        @test V056982(10) == 68719476736
        @test V001511(10) == 2
        @test V046161(10) == 262144
        @test V001316(10) == 4
        @test V001511(10) == 2
        @test V163590(10) == 63
        @test V327492(10) == 18
        @test V005187(10) == 18
        @test V001790(10) == 46189
        @test V277233(10) == 125233642041
        @test V327495(10) == 74796032037
    end
end

function demo() end

function perf()
    "V001511" |> println
    @time for n ∈ 0:100 V001511(n) end
    "V163590" |> println
    @time for n ∈ 0:100 V163590(n) end
    "V327492" |> println
    @time for n ∈ 0:100 V327492(n) end
    "V005187" |> println
    @time for n ∈ 0:100 V005187(n) end
    "V001790" |> println
    @time for n ∈ 0:100 V001790(n) end
    "V277233" |> println
    @time for n ∈ 0:100 V277233(n) end
    "V327495" |> println
    @time for n ∈ 0:100 V327495(n) end
end

function main()
    test()
    demo()
    perf()
end

main()

end # module


include("euler/euler.jl")
using .BigIntegers: BigInteger

function compute(d::Int, n::Int)::Int
    numerator, denominator = BigInteger(1), BigInteger(1)
    a, b = d, d - 1
    result = 0
    for _ ∈ 1:n
        numerator, denominator = numerator + a * denominator, numerator + b * denominator
        if length(numerator) ≠ length(denominator) result += 1 end
    end
    return result
end


## Julia program for Grouped Multinomial Data (Ex. 7.2.3)
## author: weiya <szcfweiya@gmail.com>
## date: 2018-08-26

# call gamma function
#using SpecialFunctions

# sample from Dirichlet distributions
using Distributions

function gmulti(T, x, a, b, alpha1 = 0.5, alpha2 = 0.5, alpha3 = 0.5)
    z = ones(T+1, size(x, 1)-1) # initial z satisfy `z <= x`
    mu = ones(T+1)
    eta = ones(T+1)
    for t = 1:T
        # sample from g_1(theta | y)
        dir = Dirichlet([z[t, 1] + z[t, 2] + alpha1, z[t, 3] + z[t, 4] + alpha2, x[5] + alpha3])
        sample = rand(dir, 1)
        mu[t+1] = sample[1]
        eta[t+1] = sample[2]
        # sample from g_2(z | x, theta)
        for i = 1:2
            bi = Binomial(x[i], a[i]*mu[t+1]/(a[i]*mu[t+1]+b[i]))
            z[t+1, i] = rand(bi, 1)[1]
        end
        for i = 3:4
            bi = Binomial(x[i], a[i]*eta[t+1]/(a[i]*eta[t+1]+b[i]))
            z[t+1, i] = rand(bi, 1)[1]
        end
    end
    return mu, eta
end

# example

## data
a = [0.06, 0.14, 0.11, 0.09];
b = [0.17, 0.24, 0.19, 0.20];
x = [9, 15, 12, 7, 8];

gmulti(100, x, a, b)


using Test

include("../src/matrixcore.jl")
using .matrixcore

@testset "test_matrixcore" begin

    @testset "StoMtoS" begin
        # Running random tests to check that MtoS and StoM are each others inverse
        for _ in 1:100
            S = rand(ComplexF64, 2, 2)
            @test S ≈ (StoM ∘ MtoS)(S)
        end
    end

    @testset "test_cascades" begin
        # Getting random transmittance and reflectances, testing two mirror systems
        # Saleh & Teich 3.ed eq.7.1-8
        for _ in 1:100
            t12 = rand(ComplexF64)
            t21 = rand(ComplexF64)

            r12 = rand(ComplexF64)
            r21 = rand(ComplexF64)

            t23 = rand(ComplexF64)
            t32 = rand(ComplexF64)

            r23 = rand(ComplexF64)
            r32 = rand(ComplexF64)

            ϕ = rand() * 2π

            mirror1 = [
                t12 r21;
                r12 t21
            ]
            air = [ℯ^(-ϕ * im) 0; 0 ℯ^(-ϕ * im)]
            mirror2 = [
                t23 r32;
                r23 t32
            ]

            machine = CascadeScattering([mirror1, air, mirror2])

            @test machine[1, 1] ≈ (t12 * t23 * ℯ^(-ϕ * im)) / (1 - (r21 * r23 * ℯ^(-ϕ * 2im)))
            @test machine[2, 1] ≈ r12 + (t12 * t21 * r23 * ℯ^(-ϕ * 2im)) / (1 - (r21 * r23 * ℯ^(-ϕ * 2im)))
        end
    end

end


function solve_rosenbrock()
    function get_rosenbrock()
        function eval_obj(x::AbstractArray{T}) where T
            f = (1 - x[1])^2 + 100*(x[2] - x[1]^2)^2

            f
        end

        function eval_cons(x::AbstractArray{T}) where T
            g = vcat(-5. .- x, x .- 5.)

            g
        end

        return generate_autodiff_solver_fn(eval_obj,eval_cons,[],1:4)
    end

    solver_fn = get_rosenbrock()

    x0 = [0.0; 0.0]
    options = Dict{String, Any}()
    options["Derivative option"] = 1
    options["Verify level"] = 1
    xopt, info = Bilevel.snopt(solver_fn, 0, 4, x0, options)

    function get_barnesgrad()
        a1 = 75.196
        a3 = 0.12694
        a5 = 1.0345e-5
        a7 = 0.030234
        a9 = 3.5256e-5
        a11 = 0.25645
        a13 = 1.3514e-5
        a15 = -5.2375e-6
        a17 = 7.0e-10
        a19 = -1.6638e-6
        a21 = 0.0005
        a2 = -3.8112
        a4 = -2.0567e-3
        a6 = -6.8306
        a8 = -1.28134e-3
        a10 = -2.266e-7
        a12 = -3.4604e-3
        a14 = -28.106
        a16 = -6.3e-8
        a18 = 3.4054e-4
        a20 = -2.8673

        function eval_obj(x::AbstractArray{T}) where T
            x1 = x[1]
            x2 = x[2]
            y1 = x1*x2
            y2 = y1*x1
            y3 = x2^2
            y4 = x1^2

            f = a1 + a2*x1 + a3*y4 + a4*y4*x1 + a5*y4^2 +
                a6*x2 + a7*y1 + a8*x1*y1 + a9*y1*y4 + a10*y2*y4 +
                a11*y3 + a12*x2*y3 + a13*y3^2 + a14/(x2+1) +
                a15*y3*y4 + a16*y1*y4*x2 + a17*y1*y3*y4 + a18*x1*y3 +
                a19*y1*y3 + a20*exp(a21*y1)

            f
        end

        function eval_cons(x::AbstractArray{T}) where T
            x1 = x[1]
            x2 = x[2]
            y1 = x1*x2
            y2 = y1*x1
            y3 = x2^2
            y4 = x1^2

            g = zeros(T, 3)
            g[1] = 1 - y1/700.0
            g[2] = y4/25.0^2 - x2/5.0
            g[3] = (x1/500.0- 0.11) - (x2/50.0-1)^2

            g
        end

        fres = DiffResults.HessianResult(zeros(2))
        fcfg = ForwardDiff.HessianConfig(eval_obj, fres, zeros(2))
        gres = DiffResults.JacobianResult(zeros(3), zeros(2))
        gcfg = ForwardDiff.JacobianConfig(eval_cons, zeros(2))

        return Bilevel.generate_autodiff_solver_fn(eval_obj,fres,fcfg,eval_cons,gres,gcfg,[],1:3)
    end

    solver_fn = get_barnesgrad()

    x0 = [10., 10.]
    options = Dict{String, Any}()
    options["Derivative option"] = 1
    options["Verify level"] = 1
    xopt, info = Bilevel.snopt(solver_fn, 0, 3, x0, options)

    xopt
end


@doc raw"""
    SoftMaxLikelihood(num_class::Int) -> MultiClassLikelihood

## Arguments
- `num_class::Int` : Total number of classes

    SoftMaxLikelihood(labels::AbstractVector) -> MultiClassLikelihood

## Arguments
- `labels::AbstractVector` : List of classes labels

Multiclass likelihood with [Softmax transformation](https://en.wikipedia.org/wiki/Softmax_function):

```math
p(y=i|\{f_k\}_{k=1}^K) = \frac{\exp(f_i)}{\sum_{k=1}^K\exp(f_k)}
```

There is no possible augmentation for this likelihood
"""
SoftMaxLikelihood(x) = MultiClassLikelihood(SoftMaxLink(), x)

implemented(::MultiClassLikelihood{<:SoftMaxLink}, ::MCIntegrationVI) = true

Base.show(io::IO, ::SoftMaxLink) = print(io, "SoftMax Link")

function grad_samples!(
    model::AbstractGPModel{T,<:MultiClassLikelihood{<:SoftMaxLink}},
    samples::AbstractMatrix{T},
    opt_state,
    y,
    index,
) where {T}
    grad_μ = zeros(T, n_latent(model))
    grad_Σ = zeros(T, n_latent(model))
    num_sample = size(samples, 1)
    samples .= mapslices(StatsFuns.softmax, samples; dims=2)
    @inbounds for i in 1:num_sample
        s = samples[i, y][1]
        @views g_μ = grad_softmax(samples[i, :], y) / s
        grad_μ += g_μ
        @views h = diaghessian_softmax(samples[i, :], y) / s
        grad_Σ += h - abs2.(g_μ)
    end
    for k in 1:n_latent(model)
        opt_state[k].ν[index] = -grad_μ[k] / num_sample
        opt_state[k].λ[index] = grad_Σ[k] / num_sample
    end
end

function log_like_samples(
    ::AbstractGPModel{T,<:MultiClassLikelihood{<:SoftMaxLink}},
    samples::AbstractMatrix,
    y::BitVector,
) where {T}
    num_sample = size(samples, 1)
    return mapslices(logsumexp, samples; dims=2) / num_sample
end

function grad_softmax(s::AbstractVector{<:Real}, y)
    return (y - s) * s[y][1]
end

function diaghessian_softmax(s::AbstractVector{<:Real}, y)
    return s[y][1] * (abs2.(y - s) - s .* (1 .- s))
end

function hessian_softmax(s::AbstractVector{T}, y) where {T}
    m = length(s)
    i = findfirst(y)
    hessian = zeros(T, m, m)
    for j in 1:m
        for k in 1:m
            hessian[j, k] =
                s[i] * ((δ(i, k) - s[k]) * (δ(i, j) - s[j]) - s[j] * (δ(j, k) - s[k]))
        end
    end
    return hessian
end


len(n) = length(string(n))

c = 0
n = 1
while true
  global c, n
  if len(BigInt(9^n)) < n
    break
  end
  for i = 0:9
    maybe = BigInt(i^n)
    if len(maybe) == n
      println(i, "^", n, "=", maybe)
      c += 1
    end
  end
  n += 1
end
println(c)


using ThreeBodyDecay
using Plots

H1(two_τ,two_j,two_L) = CG_doublearg(two_L,0,two_j,two_τ,1,two_τ)
H2(two_μ,two_λ,two_j,two_s,two_l) = CG_doublearg(2,two_μ,1,         -two_λ,two_s,two_μ-two_λ)*
                              CG_doublearg(two_l,0,two_s,two_μ-two_λ,two_j,two_μ-two_λ)
#
I(cosθ,two_j,two_s,two_l,two_L) = sum(abs2,
    H1(two_τ,two_j,two_L)*
    wignerd_doublearg(two_j,two_τ,two_μ-two_λ,cosθ)*
    H2(two_μ,two_λ,two_j,two_s,two_l) for two_μ=-2:2:2, two_λ=-1:2:1, two_τ=(-two_j):2:two_j)

let jp = (3,"-")
    ( :LS=>possibleLS((1,"+"),jp,(0,"-")),
      :ls=>possibleLS(jp,(2,"-"),(1,"+")) )
end

let
    two_j,two_s,two_l,two_L = 3,3,2,2
    plot(cosθ->I(cosθ,two_j,two_s,two_l,two_L),-1,1)
end
############################################################

const Lb2JpK = let mLb = 5.62, mJψ = 3.09, mp=0.938, mK = 0.49367
    ThreeBodySystem(mJψ,mp,mK,mLb)
end

function plot_dalitz_with_projections(f=(σ3,σ1)->1.0)
    # layout = @layout [a{0.8h}; grid(1,2)]
    layout = @layout [a{0.65w,0.7h} b; c _]
    plot(layout=layout, size=(800,600), link=:both)
    #
    σ1v = LinRange(Lb2JpK.mthsq[1],Lb2JpK.sthsq[1], 202)
    σ3v = LinRange(Lb2JpK.mthsq[3],Lb2JpK.sthsq[3], 200)
    cal = [Kibble31(σ3,σ1,Lb2JpK) < 0 ? f(σ3,σ1) : NaN for σ3 in σ3v, σ1 in σ1v]
    heatmap!(σ1v, σ3v, cal, c=:viridis, colorbar=false, sp=1,
        ylab="m[J/psi p] (GeV^2)", xlab="m[p K] (GeV^2)")
    #
    calz = map(z->isnan(z) ? 0.0 : z, cal)
    plot!(sum(calz, dims=2)[:,1], σ3v, lab="", xaxis=false, l=(2,:black), sp=2)
    plot!(σ1v, sum(calz, dims=1)[1,:], lab="", yaxis=false, l=(2,:black), sp=3)
end

############################################################

function A(σ3,σ1, CS, Cs)
    σ = [σ1, 0.0, σ3]
    return sum(c*amp(σ[ch[1]], ch[2]) for (ch,c) in zip(CS,Cs))
end

I(σ3,σ1, CS, Cs) = abs2(A(σ3,σ1, CS, Cs))

Λ1405  = BreitWigner(1.405,   0.090)
Λ1520  = BreitWigner(1.5195,  0.0156)
Λ1690  = BreitWigner(1.685,   0.050)
Λ1810  = BreitWigner(1.80,    0.090)

model = [(1,Λ1405),
 (1,Λ1520),
 (1,BreitWigner(1.6,0.2)),
 (1,Λ1690),
 (1,Λ1810),
 (3,BreitWigner(4.45,0.06))]

σ3v, σ1v = flatDalitzPlotSample31(tbs; Nev=1000)

I(σ3v[1],σ1v[1],
    model,
    [0.9, 1.0, 0.7, 0.2, 0.3, 0.2im])

# let
#     plot_dalitz_with_projections((σ3,σ1)->I(σ3,σ1,
#         model,
#         [0.9, 1.0, 0.7, 0.2, 0.3, 0.2im]))
# end


# ------------------------------------------------------------------- #
# Epstein Zin
# ------------------------------------------------------------------- #
immutable EpsteinZinAgent <: AbstractConsumer
    ρ::Float64
    α::Float64
    β::Float64
end

_unpack(ez::EpsteinZinAgent) = (ez.ρ, ez.α, ez.β)

is_recursive(a::EpsteinZinAgent) = a.ρ == a.α ? false : true

utility(a::EpsteinZinAgent, flow, continuation) =
    ((1 - a.β).*flow.^a.ρ + a.β * continuation.^a.ρ).^(1./a.ρ)

# ------------------------------------------------------------------- #
# Power Utility
# ------------------------------------------------------------------- #
immutable CRRAAgent <: AbstractConsumer
    γ::Float64
    β::Float64
end

typealias PowerUtilityAgent CRRAAgent

is_recursive(::CRRAAgent) = false
utility(a::CRRAAgent, c) = c.^(1-a.γ)./(1 - a.γ)


center = 5
sigma = 2

mf = GaussianMF(center, sigma)

# Evaluation tests
@assert Fuzzy.eval(mf, center + sigma) == exp(-0.5)
@assert Fuzzy.eval(mf, center) == 1
@assert Fuzzy.eval(mf, center + sigma) == Fuzzy.eval(mf, center - sigma)

# Mean finding tests
@assert Fuzzy.mean_at(mf, 1) == Fuzzy.mean_at(mf, 0.6) == Fuzzy.mean_at(mf, 0.3) == Fuzzy.mean_at(mf, 0) == center


## Generic and Fallbacks
"""
    discount(yc, to)
    discount(yc, from,to)

The discount factor for the yield curve `yc` for times `from` through `to`.
"""
discount(yc::YieldCurve, time) = exp(-yc.zero(time) * time)
discount(yc::AbstractYield, from, to) = discount(yc, to) / discount(yc, from)

"""
    forward(yc, from, to, CompoundingFrequency=Periodic(1))

The forward `Rate` implied by the yield curve `yc` between times `from` and `to`.
"""
function forward(yc::AbstractYield, from, to)
    return forward(yc, from, to, Periodic(1))
end

function forward(yc::AbstractYield, from, to, cf::CompoundingFrequency)
    r = Periodic((accumulation(yc, to) / accumulation(yc, from))^(1 / (to - from)) - 1, 1)
    return convert(cf, r)
end

function forward(yc::AbstractYield, from)
    to = from + 1
    return forward(yc, from, to)
end

"""
    accumulation(yc, from, to)

The accumulation factor for the yield curve `yc` for times `from` through `to`.
"""
function accumulation(yc::AbstractYield, time)
    return 1 ./ discount(yc, time)
end

function accumulation(yc::AbstractYield, from, to)
    return 1 ./ discount(yc, from, to)
end


function calc_area_ngon(a,b)
if length(a) == length(b) && length(a) >= 2
    A = 0;
    for i = 1:length(a)-1
        A = A + (a[i]*b[i+1]-b[i]*a[i+1])/2;
    end
    return A
end
end


"""
    int_F²(f)

Compute the time profile contribution to the ``b``-value given the time profile `f`. The
``b``-value is given by

```julia
b = γ^2 * g^2 * int_F²(f)
```

where `γ` is the gyromagnetic ratio of the water proton and `g` is the gradient amplitude.
"""
int_F²(f::TimeProfile{T}) where {T} = quadgk(s -> integral(f, s)^2, zero(T), echotime(f))
int_F²(f::PGSE) = f.δ^2 * (f.Δ - f.δ / 3)
int_F²(f::DoublePGSE) = 2f.δ^2 * (f.Δ - f.δ / 3)
int_F²(f::CosOGSE) = f.δ^3 / (2π * f.nperiod)^2
int_F²(f::SinOGSE) = 3f.δ^3 / (2π * f.nperiod)^2


@doc raw"""
    DAR(y::Vector{Fl}, lags::Int) where Fl

A Dynamic Autorregressive model is defined by:
```math
\begin{gather*}
    \begin{aligned}
        y_{t} &= \phi_0 + \sum_{i=1}^{lags} \phi_{i, t} y_{t - i} + \varepsilon_{t}  \quad \varepsilon_{t} \sim \mathcal{N}(0, \sigma^2_{\varepsilon})\\
        \phi_{i, t+1} &= \phi_{i, t} + \eta{i, t}  \quad \eta{i, t} \sim \mathcal{N}(0, \sigma^2_{i, \eta})\\
    \end{aligned}
\end{gather*}
```

!!!! note System matrices
    When building the system matrices we get rid of the first `lags` observations
    in order to build all system matrices respecting the correspondent timestamps

!!! warning Forecasting
    The dynamic autorregressive model does not have the [`forecast`](@ref) method implemented yet.
    If you wish to perform forecasts with this model please open an issue.

# Example
```jldoctest
julia> model = DAR(randn(100), 2)
DAR
```
"""
mutable struct DAR <: StateSpaceModel
    lags::Int
    first_observations::Vector{<:Real}
    hyperparameters::HyperParameters
    system::LinearUnivariateTimeVariant
    results::Results

    function DAR(y::Vector{Fl}, lags::Int) where Fl

        X = lagmat(y, lags)
        num_observations = size(X, 1)
        first_observations = y[1:lags]

        # Define system matrices
        Z = [X[t, :] for t in 1:num_observations]
        T = [Matrix{Fl}(I, lags, lags) for _ in 1:num_observations]
        R = [Matrix{Fl}(I, lags, lags) for _ in 1:num_observations]
        d = [zero(Fl) for _ in 1:num_observations]
        c = [zeros(lags) for _ in 1:num_observations]
        H = [one(Fl) for _ in 1:num_observations]
        Q = [Matrix{Fl}(I, lags, lags) for _ in 1:num_observations]

        system = LinearUnivariateTimeVariant{Fl}(y[lags+1:end], Z, T, R, d, c, H, Q)

        # Define hyperparameters names
        names = [["sigma2_ε", "intercept"];["sigma2_ar_$i" for i in 1:lags]]
        hyperparameters = HyperParameters{Fl}(names)

        return new(lags, first_observations, hyperparameters, system, Results{Fl}())
    end
end

# Obligatory methods
function default_filter(model::DAR)
    Fl = typeof_model_elements(model)
    a1 = zeros(Fl, num_states(model))
    # P1 is identity on sigma \eta and 0 otherwise.
    P1 = Matrix{Fl}(I, num_states(model), num_states(model))
    steadystate_tol = Fl(1e-5)
    return UnivariateKalmanFilter(a1, P1, 0, steadystate_tol)
end

function get_sigma_ar_name(model::DAR, i::Int)
    return model.hyperparameters.names[i + 2]
end

function initial_hyperparameters!(model::DAR)
    Fl = typeof_model_elements(model)
    observed_variance = var(model.system.y[findall(!isnan, model.system.y)])
    observed_mean = mean(model.system.y[findall(!isnan, model.system.y)])
    initial_hyperparameters = Dict{String,Fl}(
        "sigma2_ε" => observed_variance, "intercept" => observed_mean
    )
    for i in axes(model.system.T[1], 2)
        initial_hyperparameters[get_sigma_ar_name(model, i)] = one(Fl)
    end
    set_initial_hyperparameters!(model, initial_hyperparameters)
    return model
end

function constrain_hyperparameters!(model::DAR)
    for i in axes(model.system.T[1], 2)
        constrain_variance!(model, get_sigma_ar_name(model, i))
    end
    constrain_variance!(model, "sigma2_ε")
    constrain_identity!(model, "intercept")
    return model
end

function unconstrain_hyperparameters!(model::DAR)
    for i in axes(model.system.T[1], 2)
        unconstrain_variance!(model, get_sigma_ar_name(model, i))
    end
    unconstrain_variance!(model, "sigma2_ε")
    unconstrain_identity!(model, "intercept")
    return model
end

function fill_model_system!(model::DAR)
    H = get_constrained_value(model, "sigma2_ε")
    fill_H_in_time(model, H)
    intercept = get_constrained_value(model, "intercept")
    for t in 1:length(model.system.Q)
        fill!(model.system.d, intercept)
        for i in axes(model.system.Q[1], 1)
            model.system.Q[t][i, i] = get_constrained_value(model, get_sigma_ar_name(model, i))
        end
    end
    return model
end

function fill_H_in_time(model::DAR, H::Fl) where Fl
    return fill_system_matrice_with_value_in_time(model.system.H, H)
end

function reinstantiate(model::DAR, y::Vector{Fl}) where Fl
    return DAR(y, model.lags)
end

# This model needs a custom made forecasting routine
function forecast(
    model::DAR, steps_ahead::Int; filter::KalmanFilter=default_filter(model)
)
    return error("Forecasting is currently nor implemented for the DAR model.")
end


#=
distributions.jl

25/08/2015
Adham Beyki, odinay@gmail.com

TODO
 1. revise pdf and logpdf of Dirichlet, they are in fact expectations at the moment
 2. add check_args to inputs
=#

abstract Distribution

####################################
############ Gaussian1D ############
####################################
immutable Gaussian1D <: Distribution
    mu::Float64
    vv::Float64

    Gaussian1D(mu::Float64, vv::Float64) = new(mu, vv)
end
Gaussian1D(mu::Real, vv::Real) = Gaussian1D(convert(Float64, mu), convert(Float64, vv))

Base.deepcopy(me::Gaussian1D)  = Gaussian1D(me.mu, me.vv)

# displaying the Gaussian1D distribution
function Base.show(io::IO, me::Gaussian1D)
    println(io, "Gaussian1D distribution")
    println(io, "mean: mu=", me.mu, ", variance: vv=", me.vv)
end

# sampling
sample(me::Gaussian1D)    = me.mu + sqrt(me.vv)*randn()
sample(me::Gaussian1D, n) = me.mu + sqrt(me.vv)*randn(n)

# pdf
pdf(me::Gaussian1D, x::Float64)    = exp(-(x-me.mu)^2/(2*me.vv)) / (sqrt(2*pi*me.vv))
logpdf(me::Gaussian1D, x::Float64) = log(pdf(me, x))

##############################
######### Dirichlet ##########
##############################
type Dirichlet <: Distribution
    alpha::Vector{Float64}

    Dirichlet(alpha::Vector{Float64}) = new(alpha)
end

Dirichlet(alpha::Vector{Int})    = Dirichlet(convert(Vector{Float64}, alpha))
Dirichlet(alpha::Real, dim::Int) = Dirichlet(fill(alpha/dim, dim))
Base.deepcopy(me::Dirichlet)     = Dirichlet(deepcopy(me.alpha))

# displaying the Dirichlet distribution
function Base.show(io::IO, me::Dirichlet)
    println(io, "Dirichlet distribution")
    println(io, "cardinality = ", length(me.alpha))
    println(io, "alpha       = ", me.alpha)
end

Base.mean(me::Dirichlet) = me.alpha / sum(me.alpha)

pdf(me::Dirichlet, x::Int)    = mean(me)[x]
logpdf(me::Dirichlet, x::Int) = log(pdf(me, x))