#Running mean
"""
    running_mean!(m, x)

Compute (in-place) the mean, `m`, of a set of vectors, `x`.
* `x` must be a matrix of Floats, with each column of `x` corresponding to a vector.
* `m` must be a vector of Floats of appropriate length (equal to the number of rows in `x`).
"""
function running_mean!(m::AbstractVector{T}, x::AbstractMatrix{T}) where T<:AbstractFloat
    k = zero(T)
    fill!(m,zero(T))
    @inbounds for j ∈ axes(x,2)
        k = 1/j
        @turbo for i ∈ axes(x,1)
            temp = (x[i,j] - m[i])
            m[i] += temp*k
        end
    end
end

"""
    running_mean(x)

Compute the mean of a set of vectors `x`.
* `x` must be a matrix of Floats, with each column of `x` corresponding to a vector.
"""
function running_mean(x::AbstractMatrix{T}) where T<:AbstractFloat
    m = zeros(T,size(x,1))
    running_mean!(m, x)
    return m
end

#Update running mean with a nth data point where n is the running index
"""
    update_running_mean!(m, x, n)

Update the mean, `m`, of some set of `n-1` vectors to be the mean of the set of `n` vectors where `x` is the ``n^th`` vector.
* `m` and `x` must be vectors of equal length.
* Typically used when the mean of a part of a dataset has been previously computed and needs to be updated with a new data point (vector).
"""
function update_running_mean!(mean::AbstractVector{T}, data::AbstractVector{T}, n::Int) where T<:AbstractFloat
    k = 1/n
    @inbounds @turbo for i ∈ eachindex(mean)
        temp = (data[i] - mean[i])*k
        mean[i] += temp
    end
end

#Running mean and variance
"""
    running_meanvar!(m, v, x)

Compute (in-place) the mean, `m`, and variance along each dimension, `v`, of a set of vectors, `x`.
* `x` must be a matrix of Floats, with each column of `x` corresponding to a vector.
* `m` must be a vector of Floats of appropriate length (equal to the number of rows in `x`).
* `v` must be a vector of Floats of appropriate length (equal to the number of rows in `x`).
"""
function running_meanvar!(m::AbstractVector{T}, v::AbstractVector{T}, x::AbstractMatrix{T}) where T<:AbstractFloat
    k = zero(T)
    fill!(m,zero(T))
    fill!(v,zero(T))
    @inbounds for j ∈ axes(x,2)
        k = 1/j
        @inbounds for i ∈ axes(x,1)
            temp = (x[i,j] - m[i])
            m[i] += temp*k
            v[i] += temp*(x[i,j] - m[i])
        end
    end
    N = 1/(size(x,2)-1)
    @turbo for i ∈ eachindex(v)
        v[i] *= N
    end
end

"""
    running_meanvar(x)

Compute the mean and variance along each dimension of a set of vectors, `x`.
* `x` must be a matrix of Floats, with each column of `x` corresponding to a vector.
"""
function running_meanvar(x::AbstractMatrix{T}) where T<:AbstractFloat
    m = zeros(T,size(x,1))
    v = zeros(T,size(x,1))
    running_meanvar!(m,v,x)
    return m, v
end

#Update running mean and variance with a nth data point where n is the running index
"""
    update_running_meanvar!(m, v, x, n)

Update the mean, `m`, and variance along each dimension, `v`, of some set of `n-1` vectors to be the mean and variance of the set of `n` vectors where `x` is the ``n^th`` vector (for `n>=3`).
* `m`, `v` and `x` must be vectors of equal length.
* Typically used when the mean and variance of a part of a dataset has been previously computed and needs to be updated with a new data point (vector).

!!! note
    `n` must be 3 or greater.
    [`running_meanvar()`](@ref) or [`running_meanvar!()`](@ref) can be used to obtain an initial estimate of `m` and `v` from at least two vectors.
"""
function update_running_meanvar!(m::AbstractVector{T}, v::AbstractVector{T}, x::AbstractVector{T}, n::Int) where T<:AbstractFloat
    n >=3 || error("n must be greater than 2. Use running_meanvar() to obtain an initial estimate of mean and variance from at least two vectors before using update_running_meanvar() to update the estimates.")
    k = 1/n
    k1 = 1/(n-1)
    @inbounds for i ∈ eachindex(m,v,x)
        temp = (x[i] - m[i])
        m[i] += temp*k
        v[i] *= (n-2)
        v[i] += temp*(x[i] - m[i])
        v[i] *= k1
    end
end

#running mean and covariance
"""
    running_meancov!(m, C, x)

Compute (in-place) the mean, `m`, and covariance matrix, `C`, of a set of vectors, `x`.
* `x` must be a matrix of Floats, with each column of `x` corresponding to a vector.
* `m` must be a vector of Floats of appropriate length (equal to the number of rows in `x`).
* `C` must be a square matrix of Floats of appropriate size (side equal to the number of rows in `x`).
"""
function running_meancov!(m::AbstractVector{T}, C::AbstractMatrix{T}, x::AbstractMatrix{T}) where T<:AbstractFloat
    k = zero(T)
    temp = Vector{T}(undef,length(m))
    fill!(m,zero(T))
    fill!(C,zero(T))
    @inbounds for n ∈ axes(x,2)
        k = 1/n
        @turbo for i ∈ axes(x,1)
            temp[i] = (x[i,n] - m[i])
            m[i] += temp[i]*k
        end
        @turbo for i ∈ axes(x,1), j ∈ axes(x,1)
            C[j,i] += (x[j,n] - m[j])*temp[i]
        end
    end
    N = 1/(size(x,2)-1)
    @turbo for i ∈ eachindex(C)
        C[i] *= N
    end
    C += C'
    C /= 2
end

"""
    running_meancov(x)

Compute the mean and covariance matrix a set of vectors, `x`.
* `x` must be a matrix of Floats, with each column of `x` corresponding to a vector.
"""
function running_meancov(x::AbstractMatrix{T}) where T<:AbstractFloat
    m = Vector{T}(undef,size(x,1))
    C = Matrix{T}(undef,size(x,1),size(x,1))
    running_meancov!(m,C,x)
    return m, C
end

#Update running mean and covariance with a nth data point where n is the running index
"""
    update_running_meancov!(m, C, x, n)

Update the mean, `m`, and covariance, `C`, of some set of `n-1` vectors to be the mean and variance of the set of `n` vectors where `x` is the ``n^th`` vector (for `n>=3`).
* `m` and `x` must be vectors of equal length.
* `C` must be a square matrix of side equal to the length of `x`.
* Typically used when the mean and covariance of a part of a dataset has been previously computed and needs to be updated with a new data point (vector).

!!! note
    `n` must be 3 or greater.
    [`running_meancov()`](@ref) or [`running_meanvcov!()`](@ref) can be used to obtain an initial estimate of `m` and `v` from at least two vectors.
"""
function update_running_meancov!(m::AbstractVector{T}, C::AbstractMatrix{T}, x::AbstractVector{T}, n::Int) where T<:AbstractFloat
    n >=3 || error("n must be greater than 2. Use running_meanvar() to obtain an initial estimate of mean and variance from at least two vectors before using update_running_meanvar() to update the estimates.")
    k = 1/n
    k1 = 1/(n-1)
    temp = Vector{T}(undef,length(m))
    @turbo for i ∈ eachindex(x)
        temp[i] = (x[i] - m[i])
        m[i] += temp[i]*k
    end
    @turbo for i ∈ eachindex(x), j ∈ eachindex(x)
        C[j,i] *= (n-2)
        C[j,i] += (x[j] - m[j])*temp[i]
        C[j,i] *= k1
    end
    C += C'
    C /= 2
end


"""
    newtonraphson(f::Function, x0::Number, fprime::Function, args::Tuple(); tol=1e-8, maxiter=50, eps0=1e-10)

Find solution of f(x) = 0 (x is scalar) using Newton-Raphson iterations.

This code is from https://mmas.github.io/newton-julia. There was no license

"""
function newtonraphson(f::Function, x0::Number, fprime::Function, args::Tuple=();
                tol::AbstractFloat=1e-8, maxiter::Integer=50, eps0::AbstractFloat=1e-10)
    for _ in 1:maxiter
        yprime = fprime(x0, args...)
        if abs(yprime) < eps0
            @warn "First derivative is zero"
            return x0
        end
        y = f(x0, args...)
        x1 = x0 - y/yprime
        if abs(x1-x0) < tol
            return x1
        end
        x0 = x1
    end
    error("Max iteration exceeded")
end

"""
    newtonraphson(f,x0,jac,args; tol=1e-8, maxiter=50, eps0=1e-10)

NR solution for multidimensional problem f(x)=0. Requires jacobian.

"""
function newtonraphson(f::Function, x0::AbstractVector, jac::Function, args::Tuple=(); tol::AbstractFloat=1e-8, maxiter::Integer=50, eps0::AbstractFloat=1e-10)
    xc = copy(x0)
    for _ in 1:maxiter
        J = jac(xc, args...)
        if cond(J) > 1/eps0
            @warn "Jacobian is ill-conditioned"
            return xc
        end
        y = f(xc, args...)
        x1 = xc - J\y
        if maximum(abs.(x1-xc)) < tol
            return x1
        end
        xc = x1
    end
    error("Max iteration exceeded")
end

"""
    newtonraphson(f,x0,args; tol=1e-8, maxiter=50, eps0=1e-10)

    Method without explicit jacobian, using automatic differentiation.
"""
function newtonraphson(f::Function, x0::AbstractVector, args::Tuple=(); tol::AbstractFloat=1e-8, maxiter::Integer=50, eps0::AbstractFloat=1e-10)
    xc = copy(x0)
    for _ in 1:maxiter
        J = jacobian(x->f(x,args...), xc)
        if cond(J) > 1/eps0
            @warn "Jacobian is ill-conditioned"
            return xc
        end
        y = f(xc, args...)
        x1 = xc - J\y
        if maximum(abs.(x1-xc)) < tol
            return x1
        end
        xc = x1
    end
    error("Max iteration exceeded")
end

"""
    quasinewton(dobs::AbstractVector, g::Function, mprior::AbstractVector, jac::Function, CMi=0.0I, CDi=I, args::Tuple=();
                step=1,tol=1e-8,maxiter=50,eps0=1e-10)

Solution of inverse problem dobs = g(m) using Quasi-Newton method. Direct implementation of Tarantola 2005, p. 69.

Return mean solution mpost and posterior covariance CMpost.
"""
function quasinewton(dobs::AbstractVector, g::Function,
                     mprior::AbstractVector, jac::Function,
                     CMi=0.0I, CDi=I, args::Tuple=();
                     step::AbstractFloat=1.0, tol::AbstractFloat=1e-8,
                     maxiter::Integer=50, eps0::AbstractFloat=1e-10)

    m = copy(mprior)

    for _ in 1:maxiter
        G = jac(m, args...)
        A = G'*CDi*G + CMi
        if cond(A)>1/eps0
            @warn "Operator is ill conditioned. Consider Smoothing."
            return m, inv(A)
        end
        d = g(m, args...)
        b = G'*CDi*(d - dobs) + CMi*(m-mprior)
        dm = -step*(A\b)
        m = m+dm
        if maximum(abs.(dm)) < tol
            CMpost = inv(A)
            return m, CMpost
        end
    end
    error("Max iteration exceeded")

end

"""
    quasinewton(dobs, g, mprior, CMi=0.0I, CDi=I, args=();
                step=1,tol=1e-8,maxiter=50,eps0=1e-10)

Method without direct specification of jacobian, computed using forwarddiff.
"""
function quasinewton(dobs::AbstractVector, g::Function,
                     mprior::AbstractVector,
                     CMi=0.0I, CDi=I, args::Tuple=();
                     step::AbstractFloat=1.0, tol::AbstractFloat=1e-8,
                     maxiter::Integer=50, eps0::AbstractFloat=1e-10)

    m = copy(mprior)

    for _ in 1:maxiter
        G = jacobian(x-> g(x,args...), m)
        A = G'*CDi*G + CMi
        if cond(A)>1/eps0
            @warn "Operator is ill conditioned. Consider Smoothing."
            return m, inv(A)
        end
        d = g(m, args...)
        b = G'*CDi*(d - dobs) + CMi*(m-mprior)
        dm = -step*(A\b)
        m = m+dm
        if maximum(abs.(dm)) < tol
            CMpost = inv(A)
            return m, CMpost
        end
    end
    error("Max iteration exceeded")

end

"""
    correlationmatrix(CM)

Compute correlation matrix from covariance matrix.
"""
function correlationmatrix(CM)
    c = copy(CM)
    for b in 1:size(CM,2), a in 1:size(CM,1)
        c[a,b] /= sqrt(CM[a,a]*CM[b,b])
    end
    return c
end


"""

    E1func(x)

Compute the exponential integral function ``E_{1}(x)`` for a real number `x`. This function
returns zero if `x` is larger than 700, or `Inf + pi*1im` if `x` is smaller than -700. (This
definition also defines the branch taken in the complex plane for ``x < 0``.) Note that even
though ``E_{1}(x)`` is real for ``x > 0``, the result of `E1func(x)` is cast as a complex
type, based on the type of `x`, for consistency.

This is a reimplementation of code at
`https://github.com/mitmath/18S096/blob/iap2017/pset3/pset3-solutions.ipynb` by Steven G.
Johnson, based on a combination of Taylor series and continued fraction expansions.

See also: [`E1taylor`](@ref), [`E1cf`](@ref)

"""
function E1func(x::AbstractFloat)

    if (x < -700*one(x))
        return Inf + pi*1im;
    elseif (x > 700*one(x))
        return zero(complex(x));
    else

        currfunc = E1taylor;
        if (x < -30*one(x) || x > 3*one(x))
            currfunc = E1cf;
        end
        mymaxorder = round(Int, 2 + 23/(1 + ((log10(abs(x)) - 0.4)/1.0)^2)); # useful empirical rule of thumb for largest order needed for positive x
        if (x < zero(x))
            mymaxorder = round(Int, 1 + 70/(1 + (log10(abs(x)) - 1.5)^2)); # useful empirical rule of thumb for largest order needed for negative x
        end
        myE1 = currfunc(x, mymaxorder);
        if (x < zero(x))
            return (complex(myE1) + pi*1im);
        end
        return complex(myE1);

    end

end

E1func(x::Real) = E1func(float(x));

"""

    E1taylor(x, maxorder)

Compute the exponential integral function ``E_{1}(x)`` for a real number `x`, using the
Taylor series expansion up to a maximum order `maxorder`. This function works best for
``-30 \\leq x \\leq 3``.

This is a reimplementation of code at
`https://github.com/mitmath/18S096/blob/iap2017/pset3/pset3-solutions.ipynb` by Steven G.
Johnson.

See also: [`E1func`](@ref), [`E1cf`](@ref)

"""
function E1taylor(x::AbstractFloat, maxorder::Integer)

    E1 = -1*MathConstants.eulergamma - log(abs(x));
    currterm = -1*x;
    E1 = E1 - currterm;

    for nn=2:maxorder

        currterm = -1*x*(nn-1)/(nn^2) * currterm;
        E1 = E1 - currterm;

    end

    return E1;

end

E1taylor(x::Real, maxorder::Integer) = E1taylor(float(x), maxorder);

"""

    E1cf(x, maxorder)

Compute the exponential integral function ``E_{1}(x)`` for a real number `x`, using the
continued fraction expansion up to a maximum order `maxorder`. This function works best for
``x < -30`` or ``x > 3``.

This is a reimplementation of code at
`https://github.com/mitmath/18S096/blob/iap2017/pset3/pset3-solutions.ipynb` by Steven G.
Johnson.

See also: [`E1func`](@ref), [`E1taylor`](@ref)

"""
function E1cf(x::AbstractFloat, maxorder::Integer)

    currcf = x;
    for nn=maxorder:-1:1
        currcf = x + (1+nn)/currcf;
        currcf = 1 + nn/currcf;
    end
    return (exp(-1*x)/(x + 1/currcf));

end

E1cf(x::Real, maxorder::Integer) = E1cf(float(x), maxorder);


using NumericalMethodsforEngineers

A = Float64[16 4 8;4 5 -4;8 -4 22]
b = Float64[4, 2, 5]
n = size(A, 1)
d = zeros(n)

ldlt!(A, d)
lower = zeros(3, 3)
for i in 1:n
  for j in 1:i
    lower[i, j] = A[i, j] / d[j]
  end
end

println("\nLower Triangular Factors: \n $lower")
println("\nDiagonal Terms: \n $d")

ldlfor!(A, b)
for i in 1:n
  A[i, :] = A[i, :] / d[i]
end

subbac!(A, b)
println("\nSolution Vector: \n $b")


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

using Markdown
using InteractiveUtils

# ╔═╡ 330763c4-f1e2-11ea-071a-b13a3bea37e9
md"# Arrays: Slices and views"

# ╔═╡ 50e2aaa2-f1e2-11ea-37b2-9d23357ab558
md"Since arrays are a very common way of storing data (but by no means the only way!), it is important to know how to manipulate them."

# ╔═╡ 614126a8-f1e2-11ea-08ef-3d787d155416
md"Suppose we start with a vector (1-dimensional array) of all ones:"

# ╔═╡ 6d6e5194-f1e2-11ea-116c-c191e52dd2af
v = ones(Int, 10)

# ╔═╡ 79332324-f1e2-11ea-3c82-39cad3d2391c
md"""We can **index** into the array to extract a certain element using square brackets ("`[`" and "`]`"):"""

# ╔═╡ 8b0fca3e-f1e2-11ea-2b70-011aaeeb94cc
v[3]

# ╔═╡ 8cbf61b6-f1e2-11ea-3b49-cb59792e1fc1
md"And we can modify that element using the same syntax:"

# ╔═╡ a87a0238-f1e2-11ea-272d-0d8cd4795df6
v[3] = 2

# ╔═╡ abbf2a72-f1e2-11ea-3e76-2bbe6e068c59
v

# ╔═╡ b0bafcae-f1e2-11ea-1c5a-a98c25cf6bd4
md"If we want to extract a subset of the array, we can do so by indexing using a **range**:"

# ╔═╡ c8ec70ac-f1e2-11ea-30d6-7fc4e1f3cf63
v[3:5]

# ╔═╡ d6903d5e-f1e2-11ea-1525-35f7f6aa2f66
md"We call this a **slice** of the array."

# ╔═╡ 7c447b7a-f1e3-11ea-0ef2-c996690ae871
md"## Modifying a slice: Broadcasting"

# ╔═╡ cd2c8f1a-f1e2-11ea-1449-73d40f742c60
md"How can we *modify* this slice, say to replace them all by the value 4? Intuitively you might expect to be able to write"

# ╔═╡ edec3322-f1e2-11ea-32b8-ebd1445b18e2
v[3:5] = 4

# ╔═╡ 0ed6e116-f1e3-11ea-38ed-693262492cf2
md"""
But Julia does *not* allow this. The problem is that the two objects on the two sides of the assignment are different types: an array on the left, and a number on the right. What we want to do is modify the array *element by element*; for this we need to **broadcast** the assignment using "`.`":
"""

# ╔═╡ 446a93ba-f1e3-11ea-2957-e9c3bb7b66cd
v[3:5] .= 4

# ╔═╡ 4974c36c-f1e3-11ea-3543-63fce86af871
md"Let's check if this modified the original array:"

# ╔═╡ 86be1e4e-f1e3-11ea-1212-6949dd55b8e8
v

# ╔═╡ 887640e0-f1e3-11ea-2d35-a9a9d1ae1897
md"Success!"

# ╔═╡ 8d6fdcfa-f1e3-11ea-2f50-9783f4cd6339
md"Following the above idea, if we provide a new array of the correct length, we can also modify the array:"

# ╔═╡ 9b504332-f1e3-11ea-35a2-8d33db468a83
v[3:5] = [5, 6, 7]

# ╔═╡ a1685124-f1e3-11ea-396c-cd12b8cdfdbd
v

# ╔═╡ b6e74bf4-f1e3-11ea-1eab-7db865915fe8
md"## Array slices"

# ╔═╡ c1562b4e-f1e3-11ea-2b73-0d18fc361323
md"Now suppose we give a name to that array slice:"

# ╔═╡ d69da0d8-f1e3-11ea-092b-7b7e9fd262ce
w = v[3:5]

# ╔═╡ afbeb970-f1e3-11ea-3658-c973f6808f6d
md"An important question is the following:

> *What happens if we modify `w`*?

Take a moment to think about this before carrying on. What do you expect to happen? What are the possibilities?
"

# ╔═╡ fc774f0c-f1e3-11ea-0e5a-3141ec04348d
md"(Seriously, think about it!)"

# ╔═╡ ffa05138-f1e3-11ea-280e-bf7da83de269
md"Of course, after thinking about it we should ask Julia what actually happens!"

# ╔═╡ 12dbdf38-f1e4-11ea-0449-b39ed2f5f12d
md"Here's the current situation:"

# ╔═╡ 1123aade-f1e4-11ea-3cce-4534d7a72a60
w

# ╔═╡ 19ca7c32-f1e4-11ea-39a5-3dcf97e6fbcf
md"Now let's change the first element of `w`:"

# ╔═╡ 21a48678-f1e4-11ea-1143-c709ee295e0d
w[1] = 8

# ╔═╡ 25abc85a-f1e4-11ea-156b-7f15f0474e3b
w

# ╔═╡ 2765efc0-f1e4-11ea-1324-ede5df51f047
md"As expected, we modified `w`'s first element. But what happened to `v`?"

# ╔═╡ 3eb46116-f1e4-11ea-29e9-c74e8478d803
v

# ╔═╡ 4084ffca-f1e4-11ea-2d5d-abb022cd41cc
md"We see that *`v` did not get modified!* This is surprising to many people.

What is going on?"

# ╔═╡ 59e92208-f1e4-11ea-30a0-4d6d44bb9ae6
md"What is happening is that the slice syntax `v[3:5]` actually creates a *copy* of the data in a new place in memory. Then `w` is a label, or pointer, for that new piece of memory. So modifying it does not touch the original values at all!

We should be aware of this whenever we are tempted to modify slices."

# ╔═╡ aa48f298-f1e4-11ea-19ef-f120e959faa4
md"## Array views"

# ╔═╡ addcc056-f1e4-11ea-0090-eb82f5545c37
md"Of course, there are times when we do *not* want to make a copy. We might want to give a name to a slice of an array and manipulate the *original* array using the new name.

To do so we need to make a **view** of the array. This is literally like a window that allows us to view (read and write) to the *same data*.

To do this we use the `view` function:
"

# ╔═╡ f5cc5e80-f1e4-11ea-027c-9bef1852423d
z = view(v, 3:5)

# ╔═╡ 218aa662-f1e5-11ea-1516-15ffd9c9ba08
md"Now what happens if we modify `z` element by element using broadcasting?"

# ╔═╡ 2b955ddc-f1e5-11ea-1ca1-e91c81100478
z .= 9

# ╔═╡ 3a957e84-f1e5-11ea-2131-21fdd5d8c5d5
z

# ╔═╡ 3c96558c-f1e5-11ea-2109-79717714c977
md"`z` changed as we expected. But what about the original vector `v`?"

# ╔═╡ 4ed5d79a-f1e5-11ea-1515-3be1b3fa9a2b
v

# ╔═╡ 50259ee6-f1e5-11ea-317b-11a542b6cd9e
md"`v` also changed! This confirms that `z` is a window onto the same underlying data stored inside the block of memory labelled with the name `v`."

# ╔═╡ ff65101a-f1e4-11ea-1b51-f5b09ae1f0eb
md"Let's compare the types of `w` and `z`:"

# ╔═╡ fbd656aa-f1e4-11ea-2de2-61abc56ea71d
typeof(w), typeof(z)

# ╔═╡ 0ab7d32e-f1e5-11ea-048b-a74d7515b0c5
md"We see that they are of similar, but different types. `w` looks like other arrays we have seen, but `z` is a new type, called a `SubArray`."

# ╔═╡ 781e93da-f1e5-11ea-3d3f-57842f69c032
md"## Nicer syntax for views"

# ╔═╡ 83c8711a-f1e5-11ea-30c4-87104a7c1c2a
md"Now we know how to produce views / `SubArray`s. But to do so we had to move away from the nice, compact array slicing syntax, from `v[3:5]` to `view(v, 3:5)`. Could there be a way to combine the two?"

# ╔═╡ fb50b922-f1e5-11ea-0049-773084392b80
md"In fact there is: Julia provides a **macro** `@view` that converts the slicing syntax into a view:"

# ╔═╡ 0d25ffae-f1e6-11ea-05c4-49b25532ae49
z2 = @view v[3:5]

# ╔═╡ 27c47f2a-f1e6-11ea-3aa2-bb05fcd0e566
typeof(z2)

# ╔═╡ 2998132a-f1e6-11ea-3ccc-7951c392e9f3
z2 .= 10

# ╔═╡ 2c010994-f1e6-11ea-0022-79d21e0764b8
v

# ╔═╡ 2d6f84e2-f1e6-11ea-2683-8be3ea0d39ac
md"What is going on here? The macro `@view` literally takes the piece of Julia code `v[3:5]` and *replaces* it with the new piece of code `view(v, 3:5)`. Please see the separate video on macros for more detail!"

# ╔═╡ 4ff57544-f1e6-11ea-0274-15cc7917c3e3
md"Finally, there is also a macro `@views`, which replaces *all* slices by views in an entire expression."

# ╔═╡ ba6e586c-f1e8-11ea-1aba-ab1ba56ace6d
md"## Matrices: slices and views"

# ╔═╡ f0753bec-f1e8-11ea-0776-99b556737a05
md"Slices and views work the same way for matrices. For example, let's start with a matrix:"

# ╔═╡ cdf780b6-f1e8-11ea-2c90-bdae4d1dacaf
M = [10i + j for i in 0:5, j in 1:4]

# ╔═╡ 3464d66e-f1e9-11ea-0e53-e99f5c962594
md"We can make slices and views of matrices using extensions of the syntax we saw above for vectors:"

# ╔═╡ 4f3451e0-f1e9-11ea-07b4-456c5a6fa4c5
M[3:5, 1:2]

# ╔═╡ 5cb68536-f1e9-11ea-272e-179f8dfe8acc
view(M, 3:5, 1:2)

# ╔═╡ 615a74ee-f1e9-11ea-1f71-3be78c5545b2
@view M[3:5, 1:2]

# ╔═╡ 670bbc0e-f1e9-11ea-01a1-93cb78e8cb90
md"You should check that slices and views have the same behaviour that we described above."

# ╔═╡ 94c85c38-f1e9-11ea-3ab8-976569b36e23
md"## Reshaping matrices"

# ╔═╡ eb96a16a-f1e8-11ea-0398-1715aac5fb2d
md"There's something else we would like to be able to do with matrices: **reshape** them. We can do so using the `reshape` function:"

# ╔═╡ 24cefdea-f1e9-11ea-3c37-c3626e555e4e
M2 = reshape(M, 3, 8)

# ╔═╡ c0dca20e-5d6f-11eb-16dc-efa62e027511
typeof(M2)

# ╔═╡ c9e36c90-5d6f-11eb-0b8e-6538834cc678
M2[1, 2] = 42

# ╔═╡ dec30d50-5d6f-11eb-2b8a-6d3bb29422e4
M

# ╔═╡ e40f19c0-5d6f-11eb-3989-490c5629d307
M2[2, 1] = 42

# ╔═╡ 0a056c60-5d70-11eb-11fc-85569c278285
M

# ╔═╡ cae01608-f1e9-11ea-0878-a7b281aa6113
md"(Note the order in which the elements were taken.)"

# ╔═╡ 2b3cbb88-f1e9-11ea-16f1-436de44841de
md"Given the discussion so far, you are hopefully asking yourself: Is this a copy or a view? We'll leave it to you to find out!"

# ╔═╡ c36d3770-f1e9-11ea-2fa5-5ddb60c2adce
md"Similarly, we might want to turn the matrix into a vector. For this we can use the `vec` function:"

# ╔═╡ ecb7395a-f1e9-11ea-2346-abbd7f1bd73e
vv = vec(M)

# ╔═╡ 663c99e0-5d70-11eb-3e33-0fa3b4241e9e
vv[7] = 42

# ╔═╡ 785615be-5d70-11eb-1089-236000da5068
M

# ╔═╡ f0eb9eaa-f1e9-11ea-1bd5-3f47ff24cbf6
md"Again you should check if this is a copy or a view. The order of the elements here is showing us the **storage order** of a matrix, i.e. in which order in the (linear) memory inside the computer the elements of the 2D matrix are being stored."

# ╔═╡ 648b17b6-f1e6-11ea-3351-23b46a2e265d
md"## Summary"

# ╔═╡ 689e4c60-f1e6-11ea-3782-43decd275b5f
md"In summary, we need to be aware of when we want to copy data from an array, and when we actually want to avoid copying and make views instead. A slice like `v[1:3]` makes a copy *unless* there's an `@view` or `@views` macro telling you otherwise.

For other operations like `reshape` and `vec` it's also critical to know if they make copies or views.
"

# ╔═╡ Cell order:
# ╟─330763c4-f1e2-11ea-071a-b13a3bea37e9
# ╟─50e2aaa2-f1e2-11ea-37b2-9d23357ab558
# ╟─614126a8-f1e2-11ea-08ef-3d787d155416
# ╠═6d6e5194-f1e2-11ea-116c-c191e52dd2af
# ╟─79332324-f1e2-11ea-3c82-39cad3d2391c
# ╠═8b0fca3e-f1e2-11ea-2b70-011aaeeb94cc
# ╟─8cbf61b6-f1e2-11ea-3b49-cb59792e1fc1
# ╠═a87a0238-f1e2-11ea-272d-0d8cd4795df6
# ╠═abbf2a72-f1e2-11ea-3e76-2bbe6e068c59
# ╟─b0bafcae-f1e2-11ea-1c5a-a98c25cf6bd4
# ╠═c8ec70ac-f1e2-11ea-30d6-7fc4e1f3cf63
# ╟─d6903d5e-f1e2-11ea-1525-35f7f6aa2f66
# ╟─7c447b7a-f1e3-11ea-0ef2-c996690ae871
# ╟─cd2c8f1a-f1e2-11ea-1449-73d40f742c60
# ╠═edec3322-f1e2-11ea-32b8-ebd1445b18e2
# ╟─0ed6e116-f1e3-11ea-38ed-693262492cf2
# ╠═446a93ba-f1e3-11ea-2957-e9c3bb7b66cd
# ╟─4974c36c-f1e3-11ea-3543-63fce86af871
# ╠═86be1e4e-f1e3-11ea-1212-6949dd55b8e8
# ╟─887640e0-f1e3-11ea-2d35-a9a9d1ae1897
# ╟─8d6fdcfa-f1e3-11ea-2f50-9783f4cd6339
# ╠═9b504332-f1e3-11ea-35a2-8d33db468a83
# ╠═a1685124-f1e3-11ea-396c-cd12b8cdfdbd
# ╟─b6e74bf4-f1e3-11ea-1eab-7db865915fe8
# ╟─c1562b4e-f1e3-11ea-2b73-0d18fc361323
# ╠═d69da0d8-f1e3-11ea-092b-7b7e9fd262ce
# ╟─afbeb970-f1e3-11ea-3658-c973f6808f6d
# ╟─fc774f0c-f1e3-11ea-0e5a-3141ec04348d
# ╟─ffa05138-f1e3-11ea-280e-bf7da83de269
# ╟─12dbdf38-f1e4-11ea-0449-b39ed2f5f12d
# ╠═1123aade-f1e4-11ea-3cce-4534d7a72a60
# ╟─19ca7c32-f1e4-11ea-39a5-3dcf97e6fbcf
# ╠═21a48678-f1e4-11ea-1143-c709ee295e0d
# ╠═25abc85a-f1e4-11ea-156b-7f15f0474e3b
# ╟─2765efc0-f1e4-11ea-1324-ede5df51f047
# ╠═3eb46116-f1e4-11ea-29e9-c74e8478d803
# ╟─4084ffca-f1e4-11ea-2d5d-abb022cd41cc
# ╟─59e92208-f1e4-11ea-30a0-4d6d44bb9ae6
# ╟─aa48f298-f1e4-11ea-19ef-f120e959faa4
# ╟─addcc056-f1e4-11ea-0090-eb82f5545c37
# ╠═f5cc5e80-f1e4-11ea-027c-9bef1852423d
# ╟─218aa662-f1e5-11ea-1516-15ffd9c9ba08
# ╠═2b955ddc-f1e5-11ea-1ca1-e91c81100478
# ╠═3a957e84-f1e5-11ea-2131-21fdd5d8c5d5
# ╟─3c96558c-f1e5-11ea-2109-79717714c977
# ╠═4ed5d79a-f1e5-11ea-1515-3be1b3fa9a2b
# ╟─50259ee6-f1e5-11ea-317b-11a542b6cd9e
# ╟─ff65101a-f1e4-11ea-1b51-f5b09ae1f0eb
# ╠═fbd656aa-f1e4-11ea-2de2-61abc56ea71d
# ╟─0ab7d32e-f1e5-11ea-048b-a74d7515b0c5
# ╟─781e93da-f1e5-11ea-3d3f-57842f69c032
# ╟─83c8711a-f1e5-11ea-30c4-87104a7c1c2a
# ╟─fb50b922-f1e5-11ea-0049-773084392b80
# ╠═0d25ffae-f1e6-11ea-05c4-49b25532ae49
# ╠═27c47f2a-f1e6-11ea-3aa2-bb05fcd0e566
# ╠═2998132a-f1e6-11ea-3ccc-7951c392e9f3
# ╠═2c010994-f1e6-11ea-0022-79d21e0764b8
# ╟─2d6f84e2-f1e6-11ea-2683-8be3ea0d39ac
# ╟─4ff57544-f1e6-11ea-0274-15cc7917c3e3
# ╟─ba6e586c-f1e8-11ea-1aba-ab1ba56ace6d
# ╟─f0753bec-f1e8-11ea-0776-99b556737a05
# ╠═cdf780b6-f1e8-11ea-2c90-bdae4d1dacaf
# ╟─3464d66e-f1e9-11ea-0e53-e99f5c962594
# ╠═4f3451e0-f1e9-11ea-07b4-456c5a6fa4c5
# ╠═5cb68536-f1e9-11ea-272e-179f8dfe8acc
# ╠═615a74ee-f1e9-11ea-1f71-3be78c5545b2
# ╟─670bbc0e-f1e9-11ea-01a1-93cb78e8cb90
# ╟─94c85c38-f1e9-11ea-3ab8-976569b36e23
# ╟─eb96a16a-f1e8-11ea-0398-1715aac5fb2d
# ╠═24cefdea-f1e9-11ea-3c37-c3626e555e4e
# ╠═c0dca20e-5d6f-11eb-16dc-efa62e027511
# ╠═c9e36c90-5d6f-11eb-0b8e-6538834cc678
# ╠═dec30d50-5d6f-11eb-2b8a-6d3bb29422e4
# ╠═e40f19c0-5d6f-11eb-3989-490c5629d307
# ╠═0a056c60-5d70-11eb-11fc-85569c278285
# ╟─cae01608-f1e9-11ea-0878-a7b281aa6113
# ╟─2b3cbb88-f1e9-11ea-16f1-436de44841de
# ╟─c36d3770-f1e9-11ea-2fa5-5ddb60c2adce
# ╠═ecb7395a-f1e9-11ea-2346-abbd7f1bd73e
# ╠═663c99e0-5d70-11eb-3e33-0fa3b4241e9e
# ╠═785615be-5d70-11eb-1089-236000da5068
# ╟─f0eb9eaa-f1e9-11ea-1bd5-3f47ff24cbf6
# ╟─648b17b6-f1e6-11ea-3351-23b46a2e265d
# ╟─689e4c60-f1e6-11ea-3782-43decd275b5f


# Multinomial theorem
# https://en.wikipedia.org/wiki/Multinomial_theorem

export multiexponents

struct MultiExponents{T}
    c::T
    nterms::Int
end

# Standard stars and bars:
# https://en.wikipedia.org/wiki/Stars_and_bars_(combinatorics)
function Base.iterate(m::MultiExponents, s = nothing)
    next = s === nothing ? iterate(m.c) : iterate(m.c, s)
    next === nothing && return
    stars, ss = next

    # stars minus their consecutive
    # position becomes their index
    result = zeros(Int, m.nterms)
    for (i, s) in enumerate(stars)
        result[s-i+1] += 1
    end

    result, ss
end

Base.length(m::MultiExponents) = length(m.c)

"""
    multiexponents(m, n)

Returns the exponents in the multinomial expansion (x₁ + x₂ + ... + xₘ)ⁿ.

For example, the expansion (x₁ + x₂ + x₃)² = x₁² + x₁x₂ + x₁x₃ + ...
has the exponents:

    julia> collect(multiexponents(3, 2))

    6-element Array{Any,1}:
     [2, 0, 0]
     [1, 1, 0]
     [1, 0, 1]
     [0, 2, 0]
     [0, 1, 1]
     [0, 0, 2]
"""
function multiexponents(m, n)
    # number of stars and bars = m+n-1
    c = combinations(1:m+n-1, n)

    MultiExponents(c, m)
end


using LightGraphs, SimpleWeightedGraphs, LinearAlgebra
"""
    SunFlowerGraph(; N = 400)

SUNFLOWERGRAPH construct a simple weighted sunflower graph with N vertices. Edge weights are the reciprocal of Euclidean distances.

# Input Arguments
- `N::Int64`: default is 400, the number of vertices. Requires N > 26.

# Output Argument
- `G::SimpleWeightedGraph{Int64,Float64}`: a simple weighted graph of the sunflower.
- `L::Matrix{Float64}`: the weighted unnormalized graph Laplacian matrix.
- `X::Matrix{Float64}`: a matrix whose i-th row represent the 2D coordinates of the i-th node.

"""
function SunFlowerGraph(; N = 400)
    c=1.0/N; θ=(sqrt(5.0)-1)*π;
    X = zeros(N,2); for k=1:N X[k,:]=c*(k-1)*[cos((k-1)*θ) sin((k-1)*θ)]; end

    G = SimpleWeightedGraph(N)
    for k = 2:8
        G.weights[1,k] = 1/norm(X[1,:] - X[k,:])
        G.weights[k,1] = 1/norm(X[1,:] - X[k,:])
    end

    for k = 1:N
        if k+8 <= N
            G.weights[k,k+8] = 1/norm(X[k,:] - X[k+8,:])
            G.weights[k+8,k] = 1/norm(X[k,:] - X[k+8,:])
        end
        if k+13 <= N
            G.weights[k,k+13] = 1/norm(X[k,:] - X[k+13,:])
            G.weights[k+13,k] = 1/norm(X[k,:] - X[k+13,:])
        end
    end
    W = weights(G) #weighted adjacency_matrix
    Lw = Diagonal(sum(W, dims = 2)[:]) - W #weighted laplacian_matrix
    return G, Lw, X
end


function tau(n::Integer)::Integer
    ans = 0
    for i = 1:n
        if n % i == 0
            ans = 1 + ans
        end
    end
    return ans
end

τf = tau

function sigma(n::Integer)::Integer
    ans = 0
    for i = 1:n
        if n % i == 0
            ans = i + ans
        end
    end
    return ans
end

σf = sigma

function s(n::Integer)::Integer
    return sigma(n) - n
end

# Adding cache to this function can make thing easier
function getMappingFromSFunction(n::Integer)::Array{Bool}
    sn = s(n)
    mappingSofNProperty = [sn < n, sn == n, sn > n]
    return mappingSofNProperty
end

function isDeficient(n::Integer)::Bool
    return getMappingFromSFunction(n)[1]
end

function isPerfect(n::Integer)::Bool
    return getMappingFromSFunction(n)[2]
end

function isAbundant(n::Integer)::Bool
    return getMappingFromSFunction(n)[3]
end

# Including N
function pic(n::Integer)::Integer
    numberOfPrimesTillN = 0
    for i = 2:n
        isPrime = true
        for j = 1:i
            if (i % j == 0 && i != j) || j == 1
                isPrime = false
                break
            end
        end
        if isPrime
            numberOfPrimesTillN += 1
        end
    end
    return numberOfPrimesTillN
end

# Not Including N
function pi(n::Integer)::Integer
    numberOfPrimesTillN = 0
    for i = 2:n-1
        isPrime = true
        for j = 2:i
            if (i % j == 0 && i != j)
                isPrime = false
                break
            end
        end
        if isPrime && (i != n - 1)
            numberOfPrimesTillN += 1
        end
    end
    return numberOfPrimesTillN
end

πf = pi
πfc = pic

function 𝞋(n::Integer)::Float64
    value = 0.0
    for i = 2:n
        isPrime = true
        for j = 2:i
            if (i % j == 0 && i != j)
                isPrime = false
                break
            end
        end
        if isPrime && (i != n - 1)
            value += log(ℯ, p)
        end
    end
    return value
end

chebyshev_theta = 𝞋

function ψ(n::Integer)::Float64
    value = 0.00
    for p = 2:n
        isPrime = true
        for j = 2:p
            if (p % j == 0 && p != j)
                isPrime = false
                break
            end
        end
        if isPrime
            pk = p^1
            sum = 0.0
            while pk <= n
                sum += log(ℯ, p)
                pk *= p
                println("ln $p")
            end
            value += sum
        end
    end
    return value
end

chebyshev_psi = ψ


"""
    lagrange_polynomials(nodes::Array{Float64, 1}, evalPt::Float64)

Evaluate the lagrange interpolating polynomials defied by `nodes` at the point `evalPt`.
"""
function lagrange_polynomials(nodes::Array{Float64,1}, evalPt::Float64)
    order = length(nodes)
    poly_coefs = zeros(order)
    if order > 1
        for n in 1:order
            notn = [1:n-1;n+1:order]
            poly_coefs[n] = prod(evalPt.-nodes[notn])./prod(nodes[n].-nodes[notn])
        end
    else
        poly_coefs = [1.0]
    end
    return poly_coefs
end

"""
    gauss_lobatto_points(a::Float64, b::Float64, n_evals::Int)

Return array containing `n_evals` gauss lobatto points in the interval `[a,b]`.
"""
function gauss_lobatto_points(a::Float64,b::Float64,n_evals::Int)
    (b<a)&&throw(DomainError("must have a<b"))
    (n_evals<1)&&throw(DomainError("n_evals must be > 0"))
    if n_evals > 1
        # the GL nodes
        # nodes in [-1,1]
        nodes = Jacobi.zglj(n_evals, 0, 0)
        # shift nodes to [a,b]
        nodes *= 0.5*(b-a)
        nodes .+= 0.5*(a+b)
    else
        nodes = [0.5*(a+b)]
    end
    return nodes
end

"""
    gauss_lobatto_weights(a::Float64, b::Float64, n_evals::Int)

Return array containing `n_evals` weights of the polynomials associated with the
gauss lobatto points in the interval `[a,b]`. i.e. the itegrals over the interval `[a,b]`
of the lagrange polynomials defined by the nodes given by `nodes = gauss_lobatto_points(a,b,n_evals)`
"""
function gauss_lobatto_weights(a::Float64,b::Float64,n_evals::Int)
    (b<a)&&throw(DomainError("must have a<b"))
    (n_evals<1)&&throw(DomainError("n_evals must be > 0"))
    if n_evals > 1
        nodes = Jacobi.zglj(n_evals, 0, 0)
        weights = Jacobi.wglj(nodes,0,0)*(b-a)/2.0
    else
        weights = [(b-a)]
    end
    return weights
end

"""
    lagrange_interpolation(fun::Function, a::Float64, b::Float64, n_evals::Int)

Return a polynomial approximation to `fun` of order `n_evals-1` on the interval `[a,b]` using
the `gauss_lobatto_points` as the nodes of the lagrange polynomials.
"""
function lagrange_interpolation(fun::Function,a::Float64,b::Float64,n_evals::Int)
    nodes = gauss_lobatto_points(a,b,n_evals)

    fun_vals = fun.(nodes)
    interpolant(x) = LinearAlgebra.dot(fun_vals,lagrange_polynomials(nodes,x))
    return interpolant
end

"""
    gauss_lobatto_quadrature(fun::Function, a::Float64, b::Float64, n_evals::Int)

Compute a quadrature approximation of `fun` on the interval `[a,b]` with `n_evals` function
evaluations using the `gauss_lobatto_points`
"""
function gauss_lobatto_quadrature(fun::Function,a::Float64,b::Float64,n_evals::Int)
    nodes = gauss_lobatto_points(a,b,n_evals)
    weights = gauss_lobatto_weights(a,b,n_evals)
    fun_vals = fun.(nodes)
    quad = LinearAlgebra.dot(fun_vals,weights)

    return quad
end
"""
    gauss_lobatto_quadrature(fun::Function, nodes::Array{Float64,1})

Compute a quadrature approximation of `fun` on the interval `nodes[1]` to `nodes[end]`
evaluating the function `fun` at `nodes`
"""
function gauss_lobatto_quadrature(fun::Function,nodes::Array{Float64,1})
    weights = gauss_lobatto_weights(a,b,length(nodes))
    fun_vals = fun.(nodes)
    quad = LinearAlgebra.dot(fun_vals,weights)

    return quad
end

"""
Construct a generalised vandermonde matrix.

    vandermonde( nBases::Int)

Note: requires Jacobi package Pkg.add("Jacobi")

# Arguments
- `nBases::Int`: the degree of the basis

# Output
- a tuple with keys
    - `:V::Array{Float64,2}`: where `:V[:,i]` contains the values of the `i`th
        legendre polynomial evaluate at the GLL nodes.
    - `:inv`: the inverse of :V
    - `:D::Array{Float64,2}`: where `V.D[:,i]` contains the values of the derivative
        of the `i`th legendre polynomial evaluate at the GLL nodes.
"""
function vandermonde(nBases::Int)
    if nBases > 1
        z = Jacobi.zglj(nBases, 0, 0) # the LGL nodes
    elseif nBases == 1
        z = 0.0
    end
    V = zeros(Float64, nBases, nBases)
    DV = zeros(Float64, nBases, nBases)
    if nBases > 1
        for j = 1:nBases
            # compute the polynomials at gauss-labotto quadrature points
            V[:, j] = Jacobi.legendre.(z, j - 1) .* sqrt((2 * (j - 1) + 1) / 2)
            DV[:, j] = Jacobi.dlegendre.(z, j - 1) .* sqrt((2 * (j - 1) + 1) / 2)
        end
        # Compute the Gauss-Lobatto weights for numerical quadrature
        w =
            2.0 ./ (
                nBases *
                (nBases - 1) *
                Jacobi.legendre.(Jacobi.zglj(nBases, 0, 0), nBases - 1) .^ 2
            )
    elseif nBases == 1
        V .= [1.0/sqrt(2)]
        DV .= [0.0]
        w = [2.0]
    end
    return (V = V, inv = inv(V), D = DV, w = w)
end

function legendre_to_lagrange(coeffs::Vector{Float64})
    order = length(coeffs)
    V = vandermonde(order)
    return V.V*coeffs
end

function lagrange_to_legendre(coeffs::Vector{Float64})
    order = length(coeffs)
    V = vandermonde(order)
    return V.inv*coeffs
end

function legendre_to_lagrange(coeffs::Array{Float64,3})
    order = size(coeffs,1)
    V = vandermonde(order)
    for  i in 1:size(coeffs,3)
        coeffs[:,:,i] = V.V*coeffs[:,:,i]
    end
    return coeffs
end

function lagrange_to_legendre(coeffs::Array{Float64,3})
    order = size(coeffs,1)
    V = vandermonde(order)
    for  i in 1:size(coeffs,3)
        coeffs[:,:,i] = V.inv*coeffs[:,:,i]
    end
    return coeffs
end


doc"""
apply Hodrick-Prescott filter to `AbstractVector`.

##### Arguments
- `y::AbstractVector` : data to be detrended
- `λ::Real` : penalty on variation in trend

##### Returns
- `y_cyclical::Vector`: cyclical component
- `y_trend::Vector`: trend component
"""
function hp_filter(y::AbstractVector{T}, λ::Real) where T <: Real
    y = Vector(y)
    N = length(y)
    H = spdiagm(-2 => fill(λ, N-2),
                -1 => vcat(-2λ, fill(-4λ, N - 3), -2λ),
                 0 => vcat(1 + λ, 1 + 5λ, fill(1 + 6λ, N-4),
                           1 + 5λ, 1 + λ),
                 1 => vcat(-2λ, fill(-4λ, N - 3), -2λ),
                 2 => fill(λ, N-2))
    y_trend = H \ y
    y_cyclical = y - y_trend
    return y_cyclical, y_trend
end

doc"""
This function applies "Hamilton filter" to `AbstractVector`.

http://econweb.ucsd.edu/~jhamilto/hp.pdf

##### Arguments
- `y::AbstractVector` : data to be filtered
- `h::Integer` : Time horizon that we are likely to predict incorrectly.
                 Original paper recommends 2 for annual data, 8 for quarterly data,
                 24 for monthly data.
- `p::Integer` : Number of lags in regression. Must be greater than `h`.
Note: For seasonal data, it's desirable for `p` and `h` to be integer multiples
      of the number of obsevations in a year.
      e.g. For quarterly data, `h = 8` and `p = 4` are recommended.
##### Returns
- `y_cycle::Vector` : cyclical component
- `y_trend::Vector` : trend component
"""
function hamilton_filter(y::AbstractVector, h::Integer, p::Integer)
    y = Vector(y)
    T = length(y)
    y_cycle = fill(NaN, T)

    # construct X matrix of lags
    X = ones(T-p-h+1)
    for j = 1:p
        X = hcat(X, y[p-j+1:T-h-j+1])
    end

    # do OLS regression
    b = (X'*X)\(X'*y[p+h:T])
    y_cycle[p+h:T] = y[p+h:T] - X*b
    y_trend = vcat(fill(NaN, p+h-1), X*b)
    return y_cycle, y_trend
end

doc"""
This function applies "Hamilton filter" to `<:AbstractVector`
under random walk assumption.

http://econweb.ucsd.edu/~jhamilto/hp.pdf

##### Arguments
- `y::AbstractVector` : data to be filtered
- `h::Integer` : Time horizon that we are likely to predict incorrectly.
                 Original paper recommends 2 for annual data, 8 for quarterly data,
                 24 for monthly data.
Note: For seasonal data, it's desirable for `h` to be an integer multiple
      of the number of obsevations in a year.
      e.g. For quarterly data, `h = 8` is recommended.
##### Returns
- `y_cycle::Vector` : cyclical component
- `y_trend::Vector` : trend component
"""
function hamilton_filter(y::AbstractVector, h::Integer)
    y = Vector(y)
    T = length(y)
    y_cycle = fill(NaN, T)
    y_cycle[h+1:T] = y[h+1:T] - y[1:T-h]
    y_trend = y - y_cycle
    return y_cycle, y_trend
end


# 2^15 = 32768 and the sum of its digits is 3 + 2 + 7 + 6 + 8 = 26.
#
# What is the sum of the digits of the number 2^1000?

using ProjectEulerSolutions

# Simple with Julia's built-in BigInt and the digits function.
function p016solution(mantissa::Integer=2, exponent::Integer=3)::Integer
    return sum(digits(BigInt(mantissa) ^ exponent))
end

p016 = Problems.Problem(p016solution)

Problems.benchmark(p016, 2, 1000)


######### function for a sigmoid curve
### dose input
# x is a vector of concentrations
### parameters for a sigmoid curve
# slope
# upper asymptote (fixed)
# lower asymptote (varying)
# inflection point (xmid)

function log_logistic(upper = 1.0; dose, slope::Float64, lower::Float64, xmid::Float64)
    num = upper - lower
    denom = 1 .+ ((dose ./ xmid) .^ slope)
    return lower .+ num ./ denom
end

doses = [0:1:100.0;]

# example to test
responses = log_logistic(dose = doses, slope = 5.0, lower = 0.0, xmid = 50.0)
plot(doses, responses, ylim = 1.0, label = "b=5, c=0, d=1, e=50")

### Vary slope ###
plt1 = plot([NaN], [NaN], label = "", title = "Varying slopes", xlabel = "dose", ylabel = "response")

# range of slope values
slopes = [4.0, 8.0, 16.0, 32.0, 64.0]
for sl in slopes
    responses = log_logistic(dose = doses, slope = sl, lower = 0.0, xmid = 50.0)
    plot!(doses, responses, label = "$sl")
end
plt1

### Vary asymptote
plt2 = plot([NaN], [NaN], label = "", title = "Varying asymptotes"
, xlabel = "dose", ylabel = "response")

# range of asymptote values
asymptotes = [0.0, 0.1, 0.2, 0.3, 0.4]
for as in asymptotes
    responses = log_logistic(dose = doses, slope = 8.0, lower = as, xmid = 50.0)
    plot!(doses, responses, label = "$as")
end
plt2

### Vary inflection point
plt3 = plot([NaN], [NaN], label = "", title = "Varying inflection points"
, xlabel = "dose", ylabel = "response")

# range of xmid values
inflections = [20.0, 30.0, 40.0, 50.0, 60.0]
for infl in inflections
    responses = log_logistic(dose = doses, slope = 8.0, lower = 0.0, xmid = infl)
    plot!(doses, responses, label = "$infl")
end
plt3

plot(plt1, plt2, plt3)

### loop for high low combos
plt4 = plot([NaN], [NaN], label = "", title = "High/low parameters")

slopes2 = [5.0, 50.0]
asymps2 = [0.0, 0.5]
inflex2 = [10.0, 50.0]

for b in slopes2
    for d in asymps2
        for e in inflex2
            responses = log_logistic(dose = doses, slope = b, lower = d, xmid = e)
            plot!(doses, responses, label = "b = $b, d = $d, e = $e")
        end
    end
end
plt4

# function Hill_equation(; dose::Vector{Float64}, slope::Float64, asym::Float64, xmid::Float64)
#     xb = dose .^ (-slope)
#     num = (1-asym) .* xb
#     denom = xb .+ (xmid .^ (-slope))
#     sigmoid = num ./ denom
#     return sigmoid
# end

# responses = Hill_equation(x = doses, slope = 10.0, asym = 1.0, xmid = 30.0)
# plot(doses, responses, ylim = 1.0)


using ForwardDiff, Roots, Plots, LinearAlgebra, Optim

function backtracking_line_search(f, ∇f, x, d, α; p=0.5, β=0.1)
    y, g = f(x), ∇f(x)
    aux = 0
    while f(x + α*d) > y + β*α*(g'⋅d)
        α *= p
        aux = α
    end
    α
    return aux
end

f(x) = ((x[1]-1.0)^2) + (0.1*(x[2]-4)^2) + sin(5*pi*x[1])+sin(5*pi*x[2])
grad_f(x) = ForwardDiff.gradient(f,x)

# Ponto inicial
xk = [-1.0,1.0]

#direcao de busca
dk = -grad_f(xk)/norm(grad_f(xk))
beta = 0.1
alpha_f = 1.0

phi(alpha) = f(xk+alpha*dk)
d_phi(alpha) = grad_f(xk+alpha*dk)'dk
l_alpha(alpha) = phi(0)+alpha*beta*d_phi(0)

#condicao de Armijo
armj = []
for alpha = 0:0.01:alpha_f
    if phi(alpha) <= l_alpha(alpha)
        push!(armj,alpha)
    end
end

#Armijo com Backtracking
armjbt = backtracking_line_search(f,grad_f,xk,dk,alpha_f)

plot(phi,0,alpha_f,lw=2,lab="phi")
plot!(l_alpha,0,alpha_f,lw=2,lab="L_phi",c=:black)
scatter!(armj,phi.(armj),lab="Armijo",ms=3,c=:red)
scatter!([armjbt],[phi(armjbt)],lab="Armijo - BT",c=:yellow)
plot!(legend=:outerbottomright)
plot!(title="beta = 0.1",xlabel = "alpha", ylabel = "phi(alpha), l_alpha(alpha")


function ffdiff(f::Function, x1::Number, h::Number)
  # Estimates the derivative of a function using a forward finite difference
  # approximation
  (f(x1 + h) - f(x1)) / (x1+h - x1);
end

function bfdiff(f::Function, x1::Number, h::Number)
  # Estimates the derivative of a function using a backwards finite difference
  # approximation
  (f(x1) - f(x1-h)) / (x1 + h - x1);
end

function cfdiff(f::Function, x1::Number, h::Number)
  # Estimates the derivative of a function using a centred finite difference
  # approximation
  (f(x1 + h)  - f(x1 - h)) / 2(x1 + h - x1);
end


@doc raw"""
    SIR(du,u,p,t)

Define classic SIR(suspected-infected-recovered) model.

Parameters: (birth rate, natural death rate, disaese induced death rate, population, infection rate, recovery rate)

```math
\begin{aligned}
& \frac{\rm{d}S}{\rm{dt}} = \Lambda -\beta S I/N - d S,\\
&\frac{\rm{d}I}{\rm{dt}} = \beta  S I/N - \gamma I - d I - \alpha I,\\
&\frac{\rm{d}R}{\rm{dt}} = \gamma I - d R,\\
\end{aligned}
```
"""
function SIRbasic(du, u, p, t)
    Λ, d, α, N, β, γ = p
    pop = (Λ > 0 & d > 0) ? Λ / d : N
    S, I, R = u
    du[1] = Λ - β * S * I / pop - d * S
    du[2] = β * S * I / pop - γ * I - d * I - α * I
    du[3] = γ * I - d * R
end

@doc raw"""
    SEIR(du,u,p,t)

Define classic SEIR(suspected-exposed-infected-recovered) model.

Parameters: (birth rate, natural death rate, disaese induced death rate, population, infection rate, recovery rate, incubation period,
exposed decreasing infection ratio)

```math
\begin{aligned}
& \frac{\rm{d}S}{\rm{dt}} = \Lambda -\beta S (I+k_{E}E)/N - d S,\\
&\frac{\rm{d}E}{\rm{dt}} = \beta  S (I+k_{E}E)/N - \sigma E -d E,\\
&\frac{\rm{d}I}{\rm{dt}} =\sigma E - \gamma I - d I - \alpha I,\\
&\frac{\rm{d}R}{\rm{dt}} = \gamma I - d R,\\
\end{aligned}
```
"""
function SEIRbasic(du, u, p, t)
    Λ, d, α, N, β, γ, σ, ke = p
    pop = (Λ > 0 & d > 0) ? Λ / d : N
    S, E, I, R = u
    du[1] = Λ - β * S * (I + ke * E) / pop - d * S
    du[2] = β * S * (I + ke * E) / pop - σ * E - d * E
    du[3] = σ * E - γ * I - d * I - α * I
    du[4] = γ * I - d * R
end

@doc raw"""
    SEIAR(du,u,p,t)

Define classic SEIR(suspected-exposed-infected-asymptomatic-recovered) model.

Parameters: (birth rate, natural death rate, disaese induced death rate, population, infection rate, recovery rate, incubation period,
exposed decreasing infection ratio, asymptomatic infection decreasing infection ratio, asymptomatic rate,recovery rate of asymptomatic)

```math
\begin{aligned}
& \frac{\rm{d}S}{\rm{dt}} = \Lambda -\beta S (I+k_{E}E+k_{A}A)/N - d S,\\
&\frac{\rm{d}E}{\rm{dt}} = \beta  S (I+k_{E}E++k_{A}A)/N - \sigma E -d E,\\
&\frac{\rm{d}I}{\rm{dt}} =(1-\rho)\sigma E - \gamma I - d I - \alpha I,\\
&\frac{\rm{d}A}{\rm{dt}} =\rho\sigma E - \gamma_{A} A - d A,\\
&\frac{\rm{d}R}{\rm{dt}} = \gamma I +\gamma_{A} A - d R,\\
\end{aligned}
```
"""
function SEIARbasic(du, u, p, t)
    Λ, d, α, N, β, γ, σ, ke, ka, ρ, γA= p
    pop = (Λ > 0 & d > 0) ? Λ / d : N
    S, E, I, A, R = u
    du[1] = Λ - β * S * (I + ke * E + ka * A) / pop - d * S
    du[2] = β * S * (I + ke * E + ka * A) / pop - σ * E - d * E
    du[3] = (1-ρ)*σ * E - γ * I - d * I - α * I
    du[3] = ρ * σ * E - γA * A - d * A
    du[4] = γ * I +  γA * A - d * R
end

@doc raw"""
    SIS(du,u,p,t)

Define classic SIR(suspected-infected-suspected) model.

Parameters: (population, infection rate, recovery rate)

```math
\begin{aligned}
& \frac{\rm{d}S}{\rm{dt}} = -\beta S I/N + \gamma I,\\
&\frac{\rm{d}I}{\rm{dt}} = \beta  S I/N - \gamma I,\\
\end{aligned}
```
"""
function SISbasic(du, u, p, t)
    pop, β, γ = p
    S, I = u
    du[1] = - β * S * I / pop + γ * I
    du[2] = β * S * I / pop - γ * I
end


module PhaseSpaces

export Q_of_θφ,Q_of_θϕ,P_of_θφ,P_of_θϕ,θ_of_QP,φ_of_QP,ϕ_of_QP,jz,jx,jy,arc_between_θφ,arc_between_θϕ,arc_between_QP
    """
    ```julia
    function Q_of_θϕ(θ,ϕ)
    ```

    Returns
    ```math
        Q=\\sqrt{2 (1 - \\cos(\\theta))}\\cos(\\phi).
    ```

    Also [`Q_of_θφ(θ,φ)`](@ref Q_of_θφ).
    """
    Q_of_θϕ(θ::Real,ϕ::Real)=sqrt(2*(1-cos(θ)))*cos(ϕ)

    """
    See [`Q_of_θϕ`](@ref).
    """
    Q_of_θφ(θ::Real,φ::Real)=Q_of_θϕ(θ,φ)

    """
    ```julia
    function P_of_θϕ(θ,ϕ)
    ```

    Returns
    ```math
        P=- \\sqrt{2 (1 - \\cos(\\theta))}\\sin(\\phi).
    ```
    Also [`P_of_θφ(θ,φ)`](@ref P_of_θφ).
    """
    P_of_θϕ(θ::Real,ϕ::Real)=-sqrt(2*(1-cos(θ)))*sin(ϕ)

    """
    See [`P_of_θϕ`](@ref).
    """
    P_of_θφ(θ::Real,φ::Real)=P_of_θϕ(θ,φ)
    """
    ```julia
    function θ_of_QP(Q,P)
    ```
    Returns
    ```math
        \\theta =- \\arccos\\left(1 - \\frac{Q^2 + P^2}{2}\\right).
    ```
    """
    θ_of_QP(Q::Real,P::Real)=acos(1-(Q^2+P^2)/2)
    """
    ```julia
    function ϕ_of_QP(Q,P)
    ```

    Returns
    ```math
        \\phi = \\text{arctan2}(-P,Q)\\in [0,2 \\pi ],
    ```
    where ``\\text{arctan2}`` is the [2-argument arctangent](https://en.wikipedia.org/wiki/Atan2).

    Also [`φ_of_QP(Q,P)`](@ref φ_of_QP).
    """
    ϕ_of_QP(Q::Real,P::Real)=mod(atan(-P,Q),2*pi)

    """
    See [`ϕ_of_QP`](@ref).
    """
    φ_of_QP(Q::Real,P::Real)=ϕ_of_QP(Q,P)

    """
    ```julia
    function jz(Q,P)
    ```
    Returns
    ```math
        j_z = -  \\cos(\\theta(Q,P)).
    ```
    where ``\\text{arctan2}`` is the [2-argument arctangent](https://en.wikipedia.org/wiki/Atan2).
    """
    jz(Q::Real,P::Real)=-cos(θ_of_QP(Q,P))
    """
    ```julia
    function jx(Q,P)
    ```
    Returns
    ```math
        j_x = \\sin(\\theta(Q,P))\\cos(\\phi(Q,P)).
    ```
    """
    jx(Q::Real,P::Real)=sin(θ_of_QP(Q,P))*cos(φ_of_QP(Q,P))
    """
    ```julia
    function jy(Q,P)
    ```
    Returns
    ```math
        j_y = \\sin(\\theta(Q,P))\\sin(\\phi(Q,P)).
    ```
    """
    jy(Q::Real,P::Real)=sin(θ_of_QP(Q,P))*sin(φ_of_QP(Q,P))
    """
    ```julia
    function arc_between_θϕ(θ1,φ1,θ2,φ2)
    ```
    Returns
    ```math
        \\Theta = \\arccos(\\cos(\\theta_1)\\cos(\\theta_2)+ \\sin(\\theta_1)\\sin(\\theta_2)\\cos(\\phi_1 - \\phi_2)).
    ```
    Also [`arc_between_θφ(θ1,φ1,θ2,φ2)`](@ref arc_between_θφ).

    """
    arc_between_θϕ(θ1::Real,ϕ1::Real,θ2::Real,ϕ2::Real)=acos(clamp(cos(θ1)*cos(θ2)+ sin(θ1)*sin(θ2)*cos(ϕ1 - ϕ2),-1,1))

    """
    See [`arc_between_θϕ`](@ref).
    """
    arc_between_θφ(θ1::Real,φ1::Real,θ2::Real,φ2::Real)=arc_between_θϕ(θ1,φ1,θ2,φ2)
    """
    ```julia
    function arc_between_QP(Q1,P1,Q2,P2)
    ```
    Returns
    ```julia
        arc_between_θϕ(θ_of_QP(Q1,P1),ϕ_of_QP(Q1,P1),θ_of_QP(Q2,P2),ϕ_of_QP(Q2,P2)).
    ```
    """
    arc_between_QP(Q1::Real,P1::Real,Q2::Real,P2::Real)=arc_between_θϕ(θ_of_QP(Q1,P1),ϕ_of_QP(Q1,P1),θ_of_QP(Q2,P2),ϕ_of_QP(Q2,P2))
end


"""
    findiff(p,q,r,a,b,alpha,beta,M)
berisi program untuk mencari solusi persamaan differensial
`x''=f(t,x,x')` dengan masalah nilai batas `x(a)=alpha` dan `x(b)=beta` pada interval `[a,b]`.
Program ini secara default berisi 7 masukan, yaitu fungsi `p(t)`, `q(t)` dan `r(t)` dari persamaan
`x''=p(t)x'+q(t)x+r(t)`, titik ujung interval `[a,b]`, nilai batas `[alpha,beta]`, dan
jumlah sub-interval `M`.
# Example
```jl
julia> p(t)= 2*t/(1+t^2);
       q(t)= -2/(1+t^2);
       r(t)= 1 + 0*t;

julia> solusi = findiff(p,q,r,0,4,1.25,-0.95,40)
41×2 Array{Float64,2}:
 0.0   1.25
 0.1   1.29077
 0.2   1.31665
 0.3   1.32791
 ⋮
 3.7  -1.03501
 3.8  -1.01924
 3.9  -0.991063
 4.0  -0.95
```
return solusi masalah nilai batas `sol`.
"""
function findiff(p,q,r,a,b,alpha,beta,M)
  h = (b-a)/M;
  T = a:h:b;
  T = T[2:end-1];
  #% Bangun Matriks B
  B = -h^2*r.(T);
  B[1] = B[1] + (1+h/2*p(T[1]))*alpha;
  B[end] = B[end] + (1-h/2*p(T[end]))*beta;
  # Bangun Matriks A - Bagian Diagonal
  Ad = 2 .+h^2*q.(T);
  #% Bangun Matriks A - Bagian Bawah Diagonal
  Tbawah = T[2:end];
  Abawah = -1 .-h/2*p.(Tbawah);
  #% Bangun Matriks A - Bagian Atas Diagonal
  Tatas = T[1:end-1];
  Aatas = -1 .+h/2*p.(Tatas);
  A = Tridiagonal(Abawah,Ad,Aatas)
  #% Selesaikan AX=B
  X = A\B;
  T = [a; T; b];
  X = [alpha; X ;beta];
  solusi = [T X];
end


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

using Markdown
using InteractiveUtils

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

# ╔═╡ e75a9a06-22e5-11eb-2e36-8d4f62b830ed
begin
	@quickactivate "StatisticsWithJuliaPlutoNotebooks"
	using Random, Distributions, Plots, LaTeXStrings
	using SpecialFunctions, Roots
	Random.seed!(0)
end

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

# ╔═╡ fa68607e-22e5-11eb-0558-c9a4d9f77426
eq(alpha, xb, xbl) = log(alpha) - digamma(alpha) - log(xb) + xbl

# ╔═╡ 06e24b20-3a9d-11eb-3557-97a1adc9f46e
begin
	actualAlpha, actualLambda = 2, 3
	gammaDist = Gamma(actualAlpha,1/actualLambda)
end

# ╔═╡ 06e27366-3a9d-11eb-211e-df469e196f9d
function mle(sample)
    alpha  = find_zero( (a)->eq(a,mean(sample),mean(log.(sample))), 1)
    lambda = alpha/mean(sample)
    return [alpha,lambda]
end

# ╔═╡ 7f6ea3be-3a9e-11eb-1287-4f1bc371585a
alpha

# ╔═╡ 06e2dc20-3a9d-11eb-2aed-6745e3221343
begin
	N = 10^4
	mles10   = [mle(rand(gammaDist,10)) for _ in 1:N]
	mles100  = [mle(rand(gammaDist,100)) for _ in 1:N]
	mles1000 = [mle(rand(gammaDist,1000)) for _ in 1:N]
end

# ╔═╡ 06ef2bc2-3a9d-11eb-3563-8b81e34087da
begin
	scatter(first.(mles10), last.(mles10),
		c=:blue, ms=1, msw=0, label="n = 10")
	scatter!(first.(mles100), last.(mles100),
		c=:red, ms=1, msw=0, label="n = 100")
	scatter!(first.(mles1000), last.(mles1000),
		c=:green, ms=1, msw=0, label="n = 1000",
		xlims=(0,6), ylims=(0,8), xlabel=L"\alpha", ylabel=L"\lambda")
end

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

# ╔═╡ Cell order:
# ╟─ca5fbdc8-22e5-11eb-0a60-cb0a127a3ca5
# ╠═e077ab16-22e5-11eb-15ed-5f0654bf7328
# ╠═e75a9a06-22e5-11eb-2e36-8d4f62b830ed
# ╠═fa68607e-22e5-11eb-0558-c9a4d9f77426
# ╠═06e24b20-3a9d-11eb-3557-97a1adc9f46e
# ╠═06e27366-3a9d-11eb-211e-df469e196f9d
# ╠═7f6ea3be-3a9e-11eb-1287-4f1bc371585a
# ╠═06e2dc20-3a9d-11eb-2aed-6745e3221343
# ╠═06ef2bc2-3a9d-11eb-3563-8b81e34087da
# ╟─475ff888-22e6-11eb-2354-09f8f40a8e12


# Low Rank Singular Value Decomposition
"""
    get_approximate_basis(A, l::Int; niter::Int = 2, M = nothing) -> Q
Return Matrix ``Q`` with ``l`` orthonormal columns such that ``Q Q^H A`` approximates ``A``. If ``M`` is specified, then ``Q`` is such that ``Q Q^H (A - M)`` approximates ``A - M``.
"""

struct Normal_QR{QT, RT}
    Q::QT
    R::RT
    function Normal_QR(Q::QT, R::RT) where {QT<:AbstractMatrix, RT<:AbstractMatrix}
        size(Q, 2) == size(R, 1) || throw(DimensionMismatch("$(size(Q)), $(length(R)) not compatible"))
        new{QT, RT}(Q, R)
    end
end

function private_qr(A::AbstractMatrix{T}) where T
	res = LinearAlgebra.qr(A)
	Normal_QR(Matrix(res.Q), res.R)
end

function get_approximate_basis(
    A::AbstractSparseMatrix{T}, l::Int, niter::Int = 2, M::Union{AbstractMatrix{T}, Nothing} = nothing) where T
    m, n = size(A)
    Ω = rand(T, (n, l))
    if M === nothing
        F_j = private_qr(A * Ω)
        for j = 1:niter
            F_H_j = private_qr(A' * Matrix(F_j.Q))
            F_j = qr(A * Matrix(F_H_j.Q))
        end
    else
        F_j = qr(A * Ω .- M * Ω)
        for j = 1:niter
            F_H_j = private_qr(A' * Matrix(F_j.Q) - M' * sum(Matrix(F_j.Q), dims=1))
            F_j = private_qr(A * Matrix(F_H_j.Q) .- M * Matrix(F_H_j.Q))
        end
    end
    Matrix(F_j.Q)
end

"""
    low_rank_svd(A, l::Int; niter::Int = 2, M = nothing) -> U, S, Vt
Return the singular value decomposition of a sparse matrix ``A`` with estimated rank ``l`` such that ``A ≈ U diag(S) Vt``. In case row vector ``M`` is given, then SVD is computed for the matrix ``A - M``.
"""

function low_rank_svd(A::AbstractSparseMatrix{T}, l::Int, niter::Int = 2, M::Union{AbstractMatrix{T}, Nothing} = nothing) where T

    if M === nothing
        Q = get_approximate_basis(A, l, niter, M)
        B = Q' * A
    else
        size(M, 2) == size(A, 2) || throw(DimensionMismatch())
        Q = get_approximate_basis(A, l, niter, M)
        B = Q' * A - sum(Q', dims=2) * M
    end

    dense_svd = svd(B)
    U = Q * dense_svd.U
    return U, dense_svd.S, dense_svd.Vt
end


# Julia translation of http://nbviewer.jupyter.org/github/barbagroup/AeroPython/blob/master/lessons/01_Lesson01_sourceSink.ipynb
# Lession 2 Source and Sink in a Freestream

using PyPlot
using Distributions

close("all")
meshgrid(x,y) = (repmat(x',length(y),1),repmat(y,1,length(x)))

N = 200                                # number of points in each direction
x_start, x_end = -4.0, 4.0            # boundaries in the x-direction
y_start, y_end = -2.0, 2.0            # boundaries in the y-direction
x = linspace(x_start, x_end, N)    # creates a 1D-array with the x-coordinates
y = linspace(y_start, y_end, N)    # creates a 1D-array with the y-coordinates

X,Y=meshgrid(x,y)

u_inf = 1.0

# computes the freestream velocity field
u_freestream = u_inf * ones((N, N))
v_freestream = zeros((N, N))

# computes the stream-function
psi_freestream = u_inf * Y

function get_velocity(strength, xs, ys, X, Y):
    """Returns the velocity field generated by a source/sink.

    Arguments
    ---------
    strength -- strength of the source/sink.
    xs, ys -- coordinates of the source/sink.
    X, Y -- mesh grid.
    """
    u = strength/(2*pi).*(X-xs)./((X-xs).^2+(Y-ys).^2)
    v = strength/(2*pi).*(Y-ys)./((X-xs).^2+(Y-ys).^2)

    return u, v
  end

function get_stream_function(strength, xs, ys, X, Y):
      """Returns the stream-function generated by a source/sink.

      Arguments
      ---------
      strength -- strength of the source/sink.
      xs, ys -- coordinates of the source/sink.
      X, Y -- mesh grid.
      """
      psi = strength/(2*pi).*atan2((Y-ys), (X-xs))

      return psi
    end

strength_source = 5.0            # strength of the source
x_source, y_source = -1.0, 0.0   # location of the source

# computes the velocity field
u_source, v_source = get_velocity(strength_source, x_source, y_source, X, Y)

# computes the stream-function
psi_source = get_stream_function(strength_source, x_source, y_source, X, Y)

  # superposition of the source on the freestream
  u = u_freestream + u_source
  v = v_freestream + v_source
  psi = psi_freestream + psi_source

  # plots the streamlines

  size = 10
  PyPlot.figure(figsize=(size, (y_end-y_start)/(x_end-x_start)*size))
  PyPlot.grid(true)
  PyPlot.xlabel("x", fontsize=16)
  PyPlot.ylabel("y", fontsize=16)
  PyPlot.xlim(x_start, x_end)
  PyPlot.ylim(y_start, y_end)
  PyPlot.streamplot(X, Y, u, v, density=2, linewidth=1, arrowsize=1, arrowstyle="->")
  PyPlot.scatter(x_source, y_source, color="#CD2305", s=80, marker="o")

  # calculates the stagnation point
  x_stagnation = x_source - strength_source/(2*pi*u_inf)
  y_stagnation = y_source

  # adding the stagnation point to the figure
  PyPlot.scatter(x_stagnation, y_stagnation, color="g", s=80, marker="o")

  # adds the dividing line to the figure
  PyPlot.contour(X, Y, psi,
              levels=[-strength_source/2, +strength_source/2],
              colors="#CD2305", linewidths=2, linestyles="solid");

strength_sink = -5.0        # strength of the sink
x_sink, y_sink = 1.0, 0.0   # location of the sink

# computes the velocity field on the mesh grid
u_sink, v_sink = get_velocity(strength_sink, x_sink, y_sink, X, Y)

# computes the stream-function on the grid mesh
psi_sink = get_stream_function(strength_sink, x_sink, y_sink, X, Y)

# superposition of a source and a sink on the freestream
u = u_freestream + u_source + u_sink
v = v_freestream + v_source + v_sink
psi = psi_freestream + psi_source + psi_sink

# plots the streamlines
size = 10
PyPlot.figure(figsize=(size, (y_end-y_start)/(x_end-x_start)*size))
PyPlot.xlabel("x", fontsize=16)
PyPlot.ylabel("y", fontsize=16)
PyPlot.xlim(x_start, x_end)
PyPlot.ylim(y_start, y_end)
PyPlot.streamplot(X, Y, u, v, density=2, linewidth=1, arrowsize=1, arrowstyle="->")
PyPlot.scatter([x_source, x_sink], [y_source, y_sink], color="#CD2305", s=80, marker="o")
PyPlot.contour(X, Y, psi, levels=[0.], colors="#CD2305", linewidths=2, linestyles="solid");

# computes the pressure coefficient field
cp = 1.0 - (u.^2+v.^2)./u_inf^2

# plots the pressure coefficient field
size = 10
PyPlot.figure(figsize=(1.1*size, (y_end-y_start)/(x_end-x_start)*size))
PyPlot.xlabel("x", fontsize=16)
PyPlot.ylabel("y", fontsize=16)
PyPlot.xlim(x_start, x_end)
PyPlot.ylim(y_start, y_end)
contf = PyPlot.contourf(X, Y, cp, levels=linspace(-2.0, 1.0, 100), extend="both")
cbar = PyPlot.colorbar(contf)
cbar[:set_label](L"$C_p$", fontsize=16)
cbar[:set_ticks]([-2.0, -1.0, 0.0, 1.0])
PyPlot.scatter([x_source, x_sink], [y_source, y_sink], color="#CD2305", s=80, marker="o")
PyPlot.contour(X, Y, psi, levels=[0.], colors="#CD2305", linewidths=2, linestyles="solid");


using LinearAlgebra: eigen, cholesky, diag, diagm, UpperTriangular, LowerTriangular
using Statistics: mean

"""
  Functions for mvrnorm, covriance to correlation conversion (cov2cor)
  and vice versa cor2cov.
"""
function generateRandomMatrix(p::Int64, n::Int64)
  x = rand(n, p)
  return x' * x
end

"""
  Function to convert covariance to correlation matrix
"""
function cov2cor(E::Array{T, 2}) where {T <: AbstractFloat}
  p = size(E)[1]
  d::Array{T, 1} = diag(E)
  d .^= -0.5
  C = (d * d') .* E
  C = 0.5.*(UpperTriangular(C) + LowerTriangular(C)')
  C = C + C'
  for i in 1:p
    C[i, i] = T(1)
  end
  return C
end

"""
  Function to convert correlation to covariance matrix
  d is the variance vector of the covariance matrix
"""
function cor2cov(C::Array{T, 2}, d::Array{T, 1}) where {T <: AbstractFloat}
  p = size(C)[1]
  d1 = d .^0.5
  E = (d1 * d1') .* C
  E = 0.5.*(UpperTriangular(E) + LowerTriangular(E)')
  E = E + E'
  for i in 1:p
    E[i, i] /= 2
  end
  return E
end

"""
  Function that carries samples from a multivariate normal
  distribution when supplied with the number of samples (n),
  the mean vector (mu), and the covariance matrix (sigma).

  Example:
  mvrnorm(100, zeros(10), cov2cor(generateRandomMatrix(10, 10000)))
"""
function mvrnorm(n::Int64, mu::Array{T, 1}, sigma::Array{T, 2}) where {T <: AbstractFloat}
  p = size(sigma)[1]
  A = cholesky(sigma).L
  output = zeros(T, (n, p))
  for i = 1:n
    output[i, :] = mu + A * randn(p)
  end
  return output
end

#=======================================================================================================#
"""
  Random Number Generator for Beta (and Uniform) Distribution
"""
abstract type AbstractSampleDistribution end
struct BetaSampleDistribution{T} <: AbstractSampleDistribution
  alpha::T
  beta::T
  function BetaSampleDistribution(alpha::T, beta::T) where {T <: AbstractFloat}
    return new{T}(alpha, beta)
  end
  function BetaSampleDistribution(alpha::T, beta::T) where {T <: Integer}
    return BetaSampleDistribution(Float64(alpha), Float64(beta))
  end
end

function calcSample(ualpha::T, vbeta::T) where {T <: AbstractFloat}
  return ualpha/(ualpha + vbeta)
end
function condition(ualpha::T, vbeta::T) where {T <: AbstractFloat}
  return (ualpha + vbeta) > T(1)
end

"""
  Version 1 of the random sample from beta distribution
  Reference: C. P. Robert, G. Casella,
            Monte Carlo Statistical Methods, Example 2.11 p44
"""
function sample(distrib::BetaSampleDistribution{T}, shape::Tuple{Vararg{Int64}}) where {T <: AbstractFloat}
  n = prod(shape)
  U = rand(T, n); V = rand(T, n)
  Y::Array{T} = zeros(T, shape)
  ialpha = 1/distrib.alpha; ibeta = 1/distrib.beta
  for i in 1:n
    u::T = U[i]; v::T = V[i]
    ualpha = u .^ialpha; vbeta = v .^ibeta;
    while condition(ualpha, vbeta)
      u = rand(T, 1)[1]; v = rand(T, 1)[1]
      ualpha = u .^ialpha; vbeta = v .^ibeta;
    end
    Y[i] = calcSample(ualpha, vbeta)
  end
  return Y
end

function sample(distrib::BetaSampleDistribution{T}, shape::Int64) where {T <: AbstractFloat}
  return sample(distrib, (shape,))
end

#=======================================================================================================#

struct UniformSampleDistribution{T} <: AbstractSampleDistribution
  min::T
  max::T
  function UniformSampleDistribution(min::T, max::T) where {T <: AbstractFloat}
    @assert(min < max, "Minimum value is not less than maximum value")
    return new{T}(min, max)
  end
  function UniformSampleDistribution(min::T, max::T) where {T <: Integer}
    return UniformSampleDistribution(Float64(min), Float64(max))
  end
end

function sample(distrib::UniformSampleDistribution{T}, shape::Tuple{Vararg{Int64}}) where {T <: AbstractFloat}
  rsample::Array{T} = rand(T, shape)
  return (rsample .* (distrib.max - distrib.min)) .+ distrib.min
end
function sample(distrib::UniformSampleDistribution{T}, shape::Int64) where {T <: AbstractFloat}
  return sample(distrib, (shape,))
end

function Base.min(x::Array{T}) where {T <: AbstractFloat}
  n = length(x)
  @assert(n > 0, "Length of array is zero.")
  if n == 1
    return x[1]
  end
  ret::T = x[1]
  for i in 2:n
    ret = ret > x[i] ? x[i] : ret
  end
  return ret
end

function Base.max(x::Array{T}) where {T <: AbstractFloat}
  n = length(x)
  @assert(n > 0, "Length of array is zero.")
  if n == 1
    return x[1]
  end
  ret::T = x[1]
  for i in 2:n
    ret = ret < x[i] ? x[i] : ret
  end
  return ret
end

function Base.range(x::Array{T}) where {T <: AbstractFloat}
  return [min(x), max(x)]
end

#=======================================================================================================#

"""
  Identity Matrix
"""
function I(::Type{T}, p::Int64) where {T <: Number}
  x = zeros(T, (p, p))
  for i in 1:p
    x[i, i] = T(1)
  end
  return x
end
"""
  Convenience overload
"""
function I(p::Int64)
  return I(Float64, p)
end

"""
  Types for generating random correlation matrices
"""
abstract type AbstractRandomCorrelationMatrix end

struct BetaGenerator <: AbstractRandomCorrelationMatrix end
struct OnionGenerator <: AbstractRandomCorrelationMatrix end
struct UniformGenerator <: AbstractRandomCorrelationMatrix end
struct VineGenerator <: AbstractRandomCorrelationMatrix end

"""
  Vine method for generating a random correlation matrix
  Source:
    https://stats.stackexchange.com/questions/2746/how-to-efficiently-generate-random-positive-semidefinite-correlation-matrices/125017#125017

    Check it with to make sure the solutions are correct:
    Journal of Multivariate Analysis 100 (2009) 1989–2001
    Generating random correlation matrices based on vines and extended
    onion method. Daniel Lewandowski(a)*, Dorota Kurowicka (a),
    Harry Joe (b).

  randomCorrelationMatrix(VineGenerator(), 10, 2.0)
"""
function randomCorrelationMatrix(::VineGenerator, d::Int64, eta::T) where {T <: AbstractFloat}

  beta = eta + (d - 1)/2
  P = zeros(T, (d, d))
  S = I(T, d)

  for k in 1:(d - 1)
    beta = beta - (1/2)
    distrib = BetaSampleDistribution(beta, beta)

    for i in (k + 1):d
      P[k, i] = sample(distrib, 1)[1]
      P[k, i] = (P[k, i] - 0.5)*2
      p = P[k, i]

      for l in (k - 1):-1:1
        p = p * sqrt((1 - P[l, i]^2)*(1 - P[l, k]^2)) + P[l, i]*P[l, k]
      end

      S[k, i] = p
      S[i, k] = p
    end
  end

  return S
end

"""
  Random correlation matrix using the Onion Generator
"""
function randomCorrelationMatrix(::OnionGenerator, d::Int64, eta::T) where {T <: AbstractFloat}
  beta::T = eta + (d - 2)/2
  distrib = BetaSampleDistribution(beta, beta)
  u = sample(distrib, 1)[1]
  r = I(T, 2)
  r[1, 2] = r[2, 1] = 2*u - 1
  for k in 2:(d - 1)
    beta -= T(1/2)
    distrib = BetaSampleDistribution(T(k/2), beta)
    y = sample(distrib, 1)[1]
    U = rand(T, k)
    w = sqrt(y) .* U

    ev = eigen(r)
    A = ev.vectors * diagm(abs.(ev.values).^(0.5)) * ev.vectors'
    z = A * w
    r = [r z; z' T(1)]
  end
  return r
end

"""
  Random correlation matrix by sampling from Beta Distribution
  # Example:
  randomCorrelationMatrix(BetaGenerator(), 10, (1.0, 1.0))
"""
function randomCorrelationMatrix(::BetaGenerator, d::Int64, (alpha, beta)::Tuple{T, T}) where {T <: AbstractFloat}
  distrib = BetaSampleDistribution(alpha, beta)
  r = sample(distrib, (d, d))
  # Change range to (-1, 1)
  r .= (r .* 2) .+ T(-1)
  # Symmetry
  r .= T(0.5) .* (r + r')
  for i in 1:d
    r[i, i] = T(1)
  end
  ev = eigen(r)
  r = ev.vectors * diagm(sort(abs.(ev.values))) * ev.vectors'
  maxR = max(r)
  r ./= maxR
  r .= T(0.5) .* (r + r')
  for i in 1:d
    r[i, i] = T(1)
  end
  return r
end

"""
  Random correlation matrix by sampling from Uniform Distribution
  # Example:
  randomCorrelationMatrix(Float64, UniformGenerator(), 10)
"""
function randomCorrelationMatrix(::Type{T}, ::UniformGenerator, d::Int64) where {T <: AbstractFloat}
  distrib = UniformSampleDistribution(T(-1), T(1))
  r = sample(distrib, (d, d))
  r .= T(0.5) .* (r + r')
  for i in 1:d
    r[i, i] = T(1)
  end
  ev = eigen(r)
  r = ev.vectors * diagm(sort(abs.(ev.values))) * ev.vectors'
  maxR = max(abs.(r)...)
  r ./= maxR
  r .= T(0.5) .* (r + r')
  for i in 1:d
    r[i, i] = T(1)
  end
  return r
end

#=======================================================================================================#

"""
  Function to simulate X and eta, p = number of parameters
  (including intercept), n = number of samples.

  # Example:
    using Random: seed!
    seed!(0)
    simulateData(Float64, 10, 1000)
"""
function simulateData(::Type{T}, p::Int64, n::Int64, delta::T = T(0)) where {T <: AbstractFloat}

  corr = randomCorrelationMatrix(T, UniformGenerator(), p)
  mu = zeros(T, p)
  X = mvrnorm(n, mu, corr)

  #= The intercept =#
  X[:, 1] .= T(1)

  b = zeros(T, p)
  idist = UniformSampleDistribution(T(0), T(0.3))
  b[1] = sample(idist, 1)[1]
  if length(b) > 1
    distrib = UniformSampleDistribution(T(-0.1), T(0.1))
    b[2:p] = sample(distrib, p - 1)
  end

  eta = X*b
  sd = 0.5*abs(mean(eta))
  eta = delta .+ eta .+ sd .* randn(n)

  return (X = X, eta = eta)
end


function LGnodes(alpha::Real, N::Int)
    # generate nodes of Laguerre-Gauss quadrature nodes.
    alpha > -1 || error("alpha <= -1 not allowed.")
    j = 1:N |> collect
    b = j .* (j .+ alpha)
    # +- diagonal
    dl = -sqrt.(b)
    j = 0:N |> collect
    # main diagonal
    d = 2.0 * j .+ alpha .+ 1
    A = SymTridiagonal(d, dl)
    return eigvals(A)
end

function LGRnodes(N::Int)
    # Laguerre-Gauss-Radau quadrature nodes.
    xi = LGnodes(1.0, N - 1)
    x = [0; xi]
    return x
end

function LGR(N::Int)
    # get nodes and weights for LGR.
    x = LGRnodes(N)
    LN = eval_laguerre_function.(N, x)
    # equation 7.37 in Shen, Tang & Wang.
    w = 1 ./ ( (N + 1) * LN.^2)
    return x, w
end

function eval_laguerre_function(n::Int, x::Real)
    # evaluate Laguerre function using the recurrence relation
    L0 = exp(-x / 2)
    L1 = (1 - x) * exp(-x / 2)
    if n == 0
        return L0
    end
    if n == 1
        return L1
    end
    # n >= 2 case
    Lnew = 0.0
    for i = 2:n
        Lnew = (2 - 1 / i - x / i) * L1 - (1 - 1 / i) * L0
        L0 = L1
        L1 = Lnew
    end
    return Lnew
end

function eval_LaguerreFunction(n::Int, x::Real)
    # method in Shen, SIAM J NUMER ANAL, Vol 38, No 4, pp. 1113-1133
    # STABLE AND EFFICIENT SPECTRAL METHODS IN UNBOUNDED
    # DOMAINS USING LAGUERRE FUNCTIONS
    signL0 = 1
    # TODO: need to indentify the error in the equations cited.
    # d0 = 1
    # signL1 = sign(1 - x)
    # d1 = abs(1 - x) * exp(-x / 2)
    # if n == 0
    #     return exp(-x / 2)
    # end
    # if n == 1
    #     return signL1 * d1
    # end
    # for i = 2:n
    #     # common term
    #     t = (2 * i - 1 - x) / i * signL1 * d1 * (d1 / d0)^(i - 2) - (1 - 1 / i) * signL0
    #     signLi = sign(t)
    #     di = d0^(1. - 2 / i) * abs(t)^(1.0 / n)
    #     # update old variables.
    #     signL0 = signL1
    #     signL1 = signLi
    #     d0 = d1
    #     d1  = di
    #     if i == n
    #         return signLi * di^n
    #     end
    # end

end

function LGRquad(f::Function, N::Int)
    # numerical integration using Laguerre-Gauss-Radau quadrature.
    # from 0 to infinity.
    x, w = LGR(N)
    return sum(f.(x) .* w)
end

function laguerre_transform(f::Function, N::Int)
    # interpolation: represent f in a series expansion
    # f = sum_{n=0}^N a_n Lnhat(x)
    # where Lnhat is the Laguerre function.
    # an = \sum_{j=0}^N f(x_j)Lnh(x_j)what_j
    x, w = LGR(N)
    an = zeros(N + 1)
    for i = 1:N + 1
        Ln = eval_laguerre_function.(i - 1, x)
        an[i] = sum(f.(x) .* Ln .* w)
    end
    return an
end

function inverse_laguerre_transform(an::AbstractVector, x::AbstractArray)
    # take an array of coefficients and reconstruct the function.
    N = length(an) - 1
    # f = sum_{n=0}^N an Lnh(x)
    f = zeros(size(x))

    for (a, n) in zip(an, 0:N)
        Lnh = eval_laguerre_function.(n, x)
        f += a * Lnh
    end
    return f
end


export rayleigh_criterion

"""
    rayleigh_criterion(focal_length, diameter; λ=500e-9)

Calculates the resolution of a microscope according to Rayleigh.
`focal_length` is the focal length of the lens.
`λ` is the wavelength and `diameter` is the diameter of the aperture of the lens.
See [Wikipedia](https://en.wikipedia.org/wiki/Angular_resolution#The_Rayleigh_criterion).

 # Examples
```julia-repl
julia> rayleigh_criterion(100, 200)
3.05e-7

julia> rayleigh_criterion(100, 200, λ=100e-9)
6.099999999999999e-8
```
"""
function rayleigh_criterion(focal_length, diameter; λ=500e-9)
    return rayleigh_criterion(calc_NA(focal_length, diameter), λ=λ)
end

"""
    rayleigh_criterion(NA; λ=500e-9)

Calculates the resolution of a microscope according to Rayleigh.
`λ` is the wavelength and `NA` the numerical aperture of the system.
See [Wikipedia](https://en.wikipedia.org/wiki/Angular_resolution#The_Rayleigh_criterion).

 # Examples
```julia-repl
julia> rayleigh_criterion(1.0)
3.05e-7

julia> rayleigh_criterion(1.0, λ=100e-9)
6.099999999999999e-8
```
"""
function rayleigh_criterion(NA; λ=500e-9)
    return 0.61 * λ / NA
end


using PyCall
using PyPlot
using SpecialFunctions
# uncomment for non-jupyter use:
#pygui(true)

### FUNCTIONS, used for several subtasks:
# %%

# the Bernoulli PDF:
function bernoulli_pdf(θ, y)
    if 0 <= θ <= 1 && (y==0 || y==1)    # Only admissible values
        return θ^y * (1-θ)^(1-y)
    else
        return 0
    end
end

# Likelihood fct of a (series of) coin tosses with bias:
function likelihood_fct(θ,y)
    # Make array s.t. multiple theta values can be input:
    likelihood = Array{Float64}(undef,length(θ))
    # Do that for every theta value requested:
    for ii in 1:length(θ)
        # Elementwise application of the beronoulli_pdf over outcome vector y, then MULTIPLY together:
        likelihood[ii] = prod(bernoulli_pdf.(θ[ii], y))
    end
    return likelihood
end

#%%

# Log-Bernoulli PDF:
function log_bernoulli_pdf(θ,y)
    if 0 < θ < 1 && (y==0 || y==1)    # Only admissible values
        return y * log(θ) + (1-y) * log(1-θ)
    else
        return -Inf
    end
end

# Log-Likelihood fct of a (series of) coin tosses with bias:
function log_likelihood_fct(θ, y)
    # Make array s.t. multiple theta values can be input:
    likelihood = Array{Float64}(undef,length(θ))
    # Do that for every theta value requested:
    for ii in 1:length(θ)
        # Elementwise application of the log_beronoulli_pdf over outcome vector y, then SUM together:
        likelihood[ii] = sum(log_bernoulli_pdf.(θ[ii],y))
    end
    return likelihood
end

#%%

### Choose and run the subtask:

###########
### Task 2
# Given the parameter, plot outcome probabilities
#%%
θ = 0.5
outcomes = [0, 1]

figure()
bar(outcomes, bernoulli_pdf.(θ,outcomes))
xticks([0, 1])
xlabel("y")
ylabel("θ")
plt.show()

#%%

θ = 0.25

figure()
bar(outcomes, bernoulli_pdf.(θ,outcomes))
xticks([0, 1])
xlabel("y")
ylabel("θ")
plt.show()

#%%

###########
### Task 3
# Given the outcome, plot parameter likelihoods
#%%
θ = 0:0.01:1;
outcome = 1;

figure()
plot(likelihood_fct.(θ,outcome))
plt.show()

#%%
outcome = 0

figure()
plot(likelihood_fct.(θ,outcome))
plt.show()
#%%

###########
### Task 4
# Evaluate likelihood fct after n flips

#%%
θ = 0.5

# a)
println("a) Regular Likelihood:")

for i in 1:3
    n = 10 * 100^(i-1)

    outcome = (rand(n) .>= 0.5) * 1

    print("L(y_1,y_2,...,y_$n | θ = $θ) = ")
    println(likelihood_fct(θ,outcome))
end

println("\nWith rising n, the likelihood -> 0")

#%%
# b) and c)
println("\nb) log-likelihoods, l, and L = exp(l):")

for i in 1:4
    n = 10 * 100^(i-1)

    outcome = (rand(n) .>= 0.5) * 1
    l = log_likelihood_fct(θ,outcome)
    L = exp.(l)

    print("l(y_1,y_2,...,y_$n | θ = $θ) = ")
    println(l)
    print("L(y_1,y_2,...,y_$n | θ = $θ) = ")
    println(L)
end

println("\nYes, n > 100000 could be evaluated without over-/underflow in logarithmic space.")

#%%
# d)
println("\nd) Plotting the likelihood function for a series")
θ = 0:0.01:1;

outcomeSeries = [[1], [1 1], [1 1 0 1]]

for thisOutcome in outcomeSeries
    figure()
    subplot(1,2,1)
    plot(θ,likelihood_fct(θ,thisOutcome),label="L($thisOutcome|θ)")
    grid()
    legend()
    subplot(1,2,2)
    plot(θ,log_likelihood_fct(θ,thisOutcome),label="l($thisOutcome|θ)")
    grid()
    legend()
    plt.show()
end
#%%

###########
### Task 5
# Implement the beta function and create plots

### i)
# Logarithmic version of the beta-distribution
function Log_Beta(θ,a::Any,b::Any)
    #using SpecialFunctions
    if 0 < θ < 1
        # Numerator
        num = log(θ)*(a-1) + log(1-θ)*(b-1)
        # The denominator (Beta-function) can be integrated or made up of Gamma-functions
        # Since we only treat integer values here, Γ(a) = (a-1)!
        #den = log(factorial(a-1) * factorial(b-1) / factorial(a+b-1))
        den = log(SpecialFunctions.beta(a,b))
        return num - den
    else
        return -Inf
    end
end

#%%
### ii) sub-tasks 1-3
function Log_posterior(θ,a,b,y)
    heads = sum(y)
    tails = length(y) - heads
    posterior = Array{Float64}(undef,length(θ))
    # Log addition, the brackets are for converting into a single float:
    for ii in 1:length(θ)
        posterior[ii] = (Log_Beta(θ[ii],a,b) .+ log_likelihood_fct(θ[ii], y) .-
                    log(SpecialFunctions.beta(a+heads,b+tails)))[1];
                    #log(factorial(a+heads-1) * factorial(b+tails-1) / factorial(a+heads+b+tails-1)))[1];
    end
    return posterior
end

#%%

println("\nd) Plotting the likelihood function for a series")
θ = 0:0.001:1;

#%%
# Flat prior:
a = 1;
b = 1;

figure()
subplot(1,2,1)
plot(θ,exp.(Log_Beta.(θ,a,b)), label="beta(θ,$a,$b)")
grid()
legend()
subplot(1,2,2)
plot(θ,Log_Beta.(θ,a,b), label="log(beta(θ,$a,$b))")
grid()
legend()
plt.show()

# %%
y = [1]
a = 1
b = 1

figure()
subplot(1,2,1)
plot(θ,exp.(Log_posterior(θ,a,b,y)), label="posterior for $y")
grid()
legend()
subplot(1,2,2)
plot(θ,Log_posterior(θ,a,b,y), label="log-posterior for $y")
grid()
legend()
plt.show()

# Alternatively directly by Beta:
heads = sum(y)
tails = length(y) - heads

figure()
subplot(1,2,1)
plot(θ,exp.(Log_Beta.(θ,a+heads,b+tails)), label="beta(θ,$(a+heads),$(b+tails))")
grid()
legend()
subplot(1,2,2)
plot(θ,Log_Beta.(θ,a+heads,b+tails), label="beta(θ,$(a+heads),$(b+tails))")
grid()
legend()
plt.show()

# %%
y = [1 1]
a = 1
b = 1

figure()
subplot(1,2,1)
plot(θ,exp.(Log_posterior(θ,a,b,y)), label="posterior for $y")
grid()
legend()
subplot(1,2,2)
plot(θ,Log_posterior(θ,a,b,y), label="log-posterior for $y")
grid()
legend()
plt.show()

# Alternatively directly by Beta:
heads = sum(y)
tails = length(y) - heads

figure()
subplot(1,2,1)
plot(θ,exp.(Log_Beta.(θ,a+heads,b+tails)), label="beta(θ,$(a+heads),$(b+tails))")
grid()
legend()
subplot(1,2,2)
plot(θ,Log_Beta.(θ,a+heads,b+tails), label="beta(θ,$(a+heads),$(b+tails))")
grid()
legend()
plt.show()

# %%
y = [1 1 0 1]
a = 1
b = 1

figure()
subplot(1,2,1)
plot(θ,exp.(Log_posterior(θ,a,b,y)), label="posterior for $y")
grid()
legend()
subplot(1,2,2)
plot(θ,Log_posterior(θ,a,b,y), label="log-posterior for $y")
grid()
legend()
plt.show()

# Alternatively directly by Beta:
heads = sum(y)
tails = length(y) - heads

figure()
subplot(1,2,1)
plot(θ,exp.(Log_Beta.(θ,a+heads,b+tails)), label="beta(θ,$(a+heads),$(b+tails))")
grid()
legend()
subplot(1,2,2)
plot(θ,Log_Beta.(θ,a+heads,b+tails), label="beta(θ,$(a+heads),$(b+tails))")
grid()
legend()
plt.show()

#%%

println("The differences are a scaling in the non-log curves and thus
a shifting in the log-curves.
The differences arise because before we only used the LIKELIHOOD function
before, but now we use a normalised POSTERIOR.")

# %%
### iii) More informative prior and reproduce Fig 6.4

# First Column
θ = 0:.01:1
a = 100
b = 100

y = [ones(1,17) zeros(1,3)]

subplot(3,1,1)
plot(θ,exp.(Log_Beta.(θ,a,b)))
grid()
subplot(3,1,2)
plot(θ,exp.(log_likelihood_fct(θ,y)))
grid()
subplot(3,1,3)
plot(θ,exp.(Log_posterior(θ,a,b,y)))
grid()
plt.show()
#%%

# Second Column
θ = 0:.01:1
a = 18.25
b = 6.75

y = [ones(1,17) zeros(1,3)]

subplot(3,1,1)
plot(θ,exp.(Log_Beta.(θ,a,b)))
grid()
subplot(3,1,2)
plot(θ,exp.(log_likelihood_fct(θ,y)))
grid()
subplot(3,1,3)
plot(θ,exp.(Log_posterior(θ,a,b,y)))
grid()
plt.show()

#%%

# Third Column
θ = 0:.001:1
a = 1
b = 1

y = [ones(1,17) zeros(1,3)]

subplot(3,1,1)
plot(θ,exp.(Log_Beta.(θ,a,b)))
grid()
subplot(3,1,2)
plot(θ,exp.(log_likelihood_fct(θ,y)))
grid()
subplot(3,1,3)
plot(θ,exp.(Log_posterior(θ,a,b,y)))
grid()
plt.show()


# # Wasserstein non-negative matrix factorisation
#
# In this example, we implement Wasserstein non-negative matrix factorisation (W-NMF)
# following the paper [^RCP16] by Rolet et al.
#
# [^RCP16]: Rolet, Antoine, Marco Cuturi, and Gabriel Peyré. ["Fast dictionary learning with a smoothed Wasserstein loss."](https://marcocuturi.net/Papers/rolet16fast.pdf) Artificial Intelligence and Statistics. PMLR, 2016.
#
# ## Introduction
#
# Matrix decomposition is a classical problem in machine learning. Given a $m \times n$ matrix $X$ representing $n$ repeated $m$-dimensional observations, one may seek matrices $D, \Lambda$ of appropriate dimensions such that
# ```math
# X \approx D \Lambda.
# ```
# For a target rank $k < \min \{ m, n \}$, i.e. $D \in \mathbb{R}^{m \times k}, \Lambda \in \mathbb{R}^{k \times n}$, this problem can be thought of as seeking a low-dimensional linear representation $D \Lambda$ of the potentially high-dimensional dataset $X$.
#
# Lee and Seung [^LS99] observed that the data matrix $X$ is non-negative in many practical applications, and that naturally one may want the factor matrices $D, \Lambda$ to be also non-negative.
#
# [^LS99]: Lee, Daniel D., and H. Sebastian Seung. "Learning the parts of objects by non-negative matrix factorization." Nature 401.6755 (1999): 788-791.
#
# For a given $m \times n$ matrix $X \ge 0$, finding the factor matrices $D \in \mathbb{R}^{m \times k}, \Lambda \in \mathbb{R}^{k \times n}$ such that $X \approx D \Lambda$ with $D \ge 0, \Lambda \ge 0$ is known as the rank-$k$ non-negative matrix factorization (NMF) problem. Typically, such an approximate representation is sought by solving a minimisation problem
# ```math
# \min_{D \in \mathbb{R}^{m \times k}, \Lambda \in \mathbb{R}^{k \times n}, D \ge 0, \Lambda \ge 0} \Phi(X, D \Lambda),
# ```
# where $(X, Y) \mapsto \Psi(X, Y)$ is a loss function defined on matrices. Commonly used choices of $\Phi$ include the squared Frobenius loss $\Phi(X, Y) = \| X - Y \|_F^2$ and Kullback-Leibler divergence $\Phi(X, Y) = \sum_{ij} X_{ij} \log(X_{ij}/Y_{ij})$.
#
# Both these loss functions (and many other common choices) decompose elementwise in their arguments, that is, they can be written as $\Phi(X, Y) = \sum_{ij} f(X_{ij}, Y_{ij})$ for some function $f$ acting on scalars.
#
# Rolet et al. note that pointwise loss functions cannot account for spatial correlations in datasets with underlying geometry, and propose to use entropy-regularised optimal transport as a loss function that is sensitive to the spatial arrangement of the data.
# They argue that for datasets such as images, optimal transport is a more natural choice of loss function, and that it achieves superior performance.
#
# In particular, suppose that the columns of $X$ encode image data, and that $C \in \mathbb{R}^{m \times m}$ encodes the squared Euclidean distances on the imaging domain. The problem that Rolet et al. pose is
# ```math
# \min_{D \in \mathbb{R}^{m \times k}_{\ge 0}, \Lambda \in \mathbb{R}^{k \times n}_{\ge 0}} \sum_{i = 1}^{n} \operatorname{OT}_{\varepsilon}(X_i, (D\Lambda)_i) + \rho_1 E(D) + \rho_2 E(\Lambda),
# ```
# where $\operatorname{OT}_{\varepsilon}(\alpha, \beta)$ is the entropy-regularised optimal transport loss between two probability distributions $\alpha, \beta$ for a cost $C$, and $E$ is an entropy barrier function (a smooth approximation to the non-negativity constraint),
# ```math
# E(A) = \sum_{ij} (A_{ij} \log(A_{ij}) - 1).
# ```
# The parameters $\rho_1, \rho_2$ control how "sharp" the non-negativity constraints are. As $\rho_1, \rho_2 \to 0$, the smoothed constraint approaches the hard non-negativity constraint. Finally, $\varepsilon$ controls the regularisation level for the optimal transport loss.
#
# This example shows how this method can be implemented using functions from the `Dual` submodule of OptimalTransport.jl.
#
# ## Set up prerequisites
#
# Load packages that will be required later.
#
using OptimalTransport
import OptimalTransport.Dual: Dual
using MLDatasets: MLDatasets
using StatsBase
using Plots;
default(; palette=:Set1_3)
using LogExpFunctions
using NNlib: NNlib
using LinearAlgebra
using Distances
using Base.Iterators
using NMF
using Optim
#
# Define `simplex_norm!`, which normalises `x` to sum to `1` along `dims`.
#
function simplex_norm!(x; dims=1)
    return x .= x ./ sum(x; dims=dims)
end
#
# ## Implementation
#
# We now implement Wasserstein-NMF. Rolet et al. split the original non-convex problem into a pair of convex problems, one for $D$ and one for $\Lambda$.
# ```math
# \begin{aligned}
# &\min_{D \in \mathbb{R}^{m \times k}} \sum_{i = 1}^{n} \operatorname{OT}_{\varepsilon}(X_i, (D\Lambda)_i) + \rho_2 E(D), \\
# &\min_{\Lambda \in \mathbb{R}^{k \times n}} \sum_{i = 1}^{n} \operatorname{OT}_{\varepsilon}(X_i, (D\Lambda)_i) + \rho_1 E(\Lambda).
# \end{aligned}
# ```
# For each of these problems, the dual problem can be derived (see Section 3.3 of Rolet et al. for details on how this is done). These turn out to be
# ```math
# \begin{aligned}
# &\min_{G \in \mathbb{R}^{m \times n}} \sum_{i = 1}^n \operatorname{OT}_{\varepsilon}^*(X_i, G_i) + \rho_2 \sum_{i = 1}^{n} E^*(-(G \Lambda^\top)_i / \rho_2) \\
# &\min_{G \in \mathbb{R}^{m \times n}} \sum_{i = 1}^n \operatorname{OT}_{\varepsilon}^*(X_i, G_i) + \rho_1 \sum_{i = 1}^{n} E^*(-(D^\top G)_i / \rho_1).
# \end{aligned}
# ```
# The semi-dual of entropy-regularised optimal transport loss, $\operatorname{OT}_{\varepsilon}$, is implemented as `Dual.ot_entropic_semidual` and its gradient can be computed by `Dual.ot_entropic_semidual_grad`. $E^*$ turns out to be `logsumexp`, for which we implement a wrapper function:
#
function E_star(x; dims=1)
    return logsumexp(x; dims=dims)
end;
#
# The gradient of `logsumexp` is `softmax`, so we define also its gradient:
#
function E_star_grad(x; dims=1)
    return NNlib.softmax(x; dims=1)
end;
#
# Thus, for each problem we may define the dual objective and its gradient. We note that `ot_entropic_semidual(_grad)` automatically broadcasts along columns of its input. There is therefore no need to make multiple calls to the function, thus allowing for more efficient evaluation.
#
function dual_obj_weights(X, K, ε, D, G, ρ1)
    return sum(Dual.ot_entropic_semidual(X, G, ε, K)) + ρ1 * sum(E_star(-D' * G / ρ1))
end
function dual_obj_weights_grad!(∇, X, K, ε, D, G, ρ1)
    return ∇ .= Dual.ot_entropic_semidual_grad(X, G, ε, K) - D * E_star_grad(-D' * G / ρ1)
end
function dual_obj_dict(X, K, ε, Λ, G, ρ2)
    return sum(Dual.ot_entropic_semidual(X, G, ε, K)) + ρ2 * sum(E_star(-G * Λ' / ρ2))
end
function dual_obj_dict_grad!(∇, X, K, ε, Λ, G, ρ2)
    return ∇ .= Dual.ot_entropic_semidual_grad(X, G, ε, K) - E_star_grad(-G * Λ' / ρ2) * Λ
end;
#
# The only remaining part of Wasserstein-NMF to implement is the conversion at optimality from the dual variable $G$ to the primal variables $D, \Lambda$. From the results of Theorems 3 and 4 in Rolet et al., we have for $\Lambda$:
# ```math
# \Lambda_i = \operatorname{softmax}((-D^\top G)_i / \rho_1),
# ```
# and we have for $D$:
# ```math
# D_i = \operatorname{softmax}((-G \Lambda^\top)_i / \rho_2).
# ```
#
function getprimal_weights(D, G, ρ1)
    return NNlib.softmax(-D' * G / ρ1; dims=1)
end
function getprimal_dict(Λ, G, ρ2)
    return NNlib.softmax(-G * Λ' / ρ2; dims=1)
end;
#
# We can now implement functions `solve_weights` and `solve_dict` that solve the respective dual problems for the next iterates of `Λ` and `D`.
#
function solve_weights(X, K, ε, D, ρ1; alg, options)
    opt = optimize(
        g -> dual_obj_weights(X, K, ε, D, g, ρ1),
        (∇, g) -> dual_obj_weights_grad!(∇, X, K, ε, D, g, ρ1),
        zero.(X),
        alg,
        options,
    )
    return getprimal_weights(D, Optim.minimizer(opt), ρ1)
end
function solve_dict(X, K, ε, Λ, ρ2; alg, options)
    opt = optimize(
        g -> dual_obj_dict(X, K, ε, Λ, g, ρ2),
        (∇, g) -> dual_obj_dict_grad!(∇, X, K, ε, Λ, g, ρ2),
        zero.(X),
        alg,
        options,
    )
    return getprimal_dict(Λ, Optim.minimizer(opt), ρ2)
end;

#
# ## Example: noisy univariate Gaussians
#
# We set up each observation as a mixture of 3 Gaussians with means sampled from `N(6, σ), N(0, σ), N(-6, σ)` respectively, and mixture weights sampled uniformly from `[0, 1]`. The resulting mixture model is normalised to sum to 1 on the discrete domain `coord`.
#
f(x, μ, σ) = exp.(-(x .- μ) .^ 2)
coord = range(-12, 12; length=100)
N = 100
σ = 1
X = hcat(
    [
        rand() * f(coord, σ * randn() + 6, 1) +
        rand() * f(coord, σ * randn(), 1) +
        rand() * f(coord, σ * randn() - 6, 1) for _ in 1:N
    ]...,
)
X = simplex_norm!(X);
#
# We visualise the  observations.
#
plot(coord, X; alpha=0.1, color=:blue, title="Input data", legend=nothing)
#
# We can apply NMF with a squared Frobenius loss using the NMF.jl package. We seek `k = 3` components.  This performs poorly, since the pointwise nature of the loss function cannot handle the translational noise in the data.
#
k = 3
result = nnmf(X, k; alg=:multmse)
plot(coord, result.W; title="NMF with Frobenius loss", palette=:Set1_3)
#
# We can now set up a cost matrix corresponding to the domain `coord`.
#
C = pairwise(SqEuclidean(), coord)
C = C / mean(C);
# Specify parameters
ε = 0.025
ρ1, ρ2 = (5e-2, 5e-2);
# Compute Gibbs kernel
K = exp.(-C / ε);
#
# Now we use a random initialisation, where columns of `D` and `Λ` are normalised to sum to 1.
#
D = rand(size(X, 1), k) # dictionary
simplex_norm!(D; dims=1) # norm columnwise
Λ = rand(k, size(X, 2)) # weights
simplex_norm!(Λ; dims=1); # norm rowwise
#
# We now run 10 iterations of Wasserstein-NMF.
#
n_iter = 10
for iter in 1:n_iter
    @info "Wasserstein-NMF: iteration $iter"
    D .= solve_dict(
        X,
        K,
        ε,
        Λ,
        ρ2;
        alg=LBFGS(),
        options=Optim.Options(;
            iterations=250, g_tol=1e-4, show_trace=false, show_every=10
        ),
    )
    Λ .= solve_weights(
        X,
        K,
        ε,
        D,
        ρ1;
        alg=LBFGS(),
        options=Optim.Options(;
            iterations=250, g_tol=1e-4, show_trace=false, show_every=10
        ),
    )
end
#
# We observe that Wasserstein-NMF learns atoms (columns of $D$) corresponding to the three Gaussians used to generate the input data.
#
plot(coord, D; title="NMF with Wasserstein loss", palette=:Set1_3)

# ## Example: image data (MNIST)
# Here we will download MNIST dataset using MLDatasets.jl and downscale each image to 14x14 to allow for faster runtime (since we are running on CPU).
#
sizex, sizey = 28, 28
factor = 2 # downscale factor
Σ = hcat([sum(I(sizex)[:, i:(i + factor - 1)]; dims=2) for i in 1:factor:sizex]...)
sizex, sizey = sizex ÷ factor, sizey ÷ factor
N = 256
x, y = MLDatasets.MNIST.traindata(Float64, sample(1:60_000, N; replace=false))
x = permutedims(x, (2, 1, 3))
x = cat([Σ' * x[:, :, i] * Σ for i in 1:N]...; dims=3)
X = simplex_norm!(reshape(x, (sizex * sizey, :)));
#
# The columns of `X` now correspond to images "flattened" as vectors. We can preview a few images.
#
M = 25
plot(
    [
        heatmap(
            reshape(X[:, i], sizex, sizey);
            legend=:none,
            axis=nothing,
            showaxis=false,
            aspect_ratio=:equal,
            c=:Blues,
            yflip=true,
        ) for i in 1:M
    ]...;
    layout=(5, M ÷ 5),
    plot_title="Input images",
)

# Now we can set up `coord`, cost matrix `C`, and specify parameters.
#
coord = reshape(collect(product(1:sizex, 1:sizey)), :)
C = pairwise(SqEuclidean(), coord)
C = C / mean(C);
ε = 0.0025
ρ1, ρ2 = (5e-3, 5e-3);
# We compute the Gibbs kernel from `C`:
K = exp.(-C / ε);
#
# Let us aim to learn `k = 25` atoms.
k = 25;
#
# Initialise again randomly
#
D = rand(size(X, 1), k) # dictionary
simplex_norm!(D; dims=1) # norm columnwise
Λ = rand(k, size(X, 2)) # weights
simplex_norm!(Λ; dims=1); # norm rowwise
#
# We now run 15 iterations of Wasserstein-NMF.
#
n_iter = 15
for iter in 1:n_iter
    @info "Wasserstein-NMF: iteration $iter"
    D .= solve_dict(
        X,
        K,
        ε,
        Λ,
        ρ2;
        alg=LBFGS(),
        options=Optim.Options(;
            iterations=250, g_tol=1e-4, show_trace=false, show_every=10
        ),
    )
    Λ .= solve_weights(
        X,
        K,
        ε,
        D,
        ρ1;
        alg=LBFGS(),
        options=Optim.Options(;
            iterations=250, g_tol=1e-4, show_trace=false, show_every=10
        ),
    )
end
#
# We can inspect the atoms learned (columns of `D`):
#

plot(
    [
        heatmap(
            reshape(D[:, i], sizex, sizey);
            legend=:none,
            axis=nothing,
            showaxis=false,
            aspect_ratio=:equal,
            c=:Blues,
            yflip=true,
        ) for i in 1:k
    ]...;
    layout=(5, k ÷ 5),
    plot_title="Learned atoms",
)

#
# Finally, we can look at the images constructed by the low-rank approximation `DΛ` and compare to the original images that we previewed earlier.
#
X_hat = D * Λ
plot(
    [
        heatmap(
            reshape(X_hat[:, i], sizex, sizey);
            legend=:none,
            axis=nothing,
            showaxis=false,
            aspect_ratio=:equal,
            c=:Blues,
            yflip=true,
        ) for i in 1:M
    ]...;
    layout=(5, M ÷ 5),
    plot_title="Low rank approximation",
)


using JuMP
using GLPK
using GraphPlot
using LightGraphs
using Colors
using LinearAlgebra

G = [
    0 1 0 0 0 0;
    1 0 1 1 0 0;
    0 1 0 0 1 1;
    0 1 0 0 1 0;
    0 0 1 1 0 0;
    0 0 1 0 0 0;
]

g = SimpleGraph(G)

gplot(g)

vertex_cover = Model(GLPK.Optimizer)

@variable(vertex_cover, y[1:nv(g)], Bin)
@constraint(vertex_cover, [i = 1:nv(g), j = 1:nv(g); G[i,j] == 1], y[i] + y[j] >= 1)
@objective(vertex_cover, Min, sum(y))

optimize!(vertex_cover)
@show value.(y);

membership = round.(Int, value.(y)) # Change to Int
membership = membership + ones(Int, nv(g)) # Make the color groups one indexed
nodecolor = [colorant"red", colorant"blue"] # Blue to represent vertices in the cover
nodefillc = nodecolor[membership]
gplot(g, nodefillc = nodefillc)

G = [
    0 1 0 0 0 0 0 0 0 1 0 ;
    1 0 1 0 0 0 0 0 0 0 1;
    0 1 0 1 0 1 0 0 0 0 0;
    0 0 1 0 1 0 0 0 0 0 0;
    0 0 0 1 0 1 0 0 0 0 0;
    0 0 1 0 1 0 1 0 0 0 0;
    0 0 0 0 0 1 0 1 0 0 0;
    0 0 0 0 0 0 1 0 1 0 1;
    0 0 0 0 0 0 0 1 0 1 1;
    1 0 0 0 0 0 0 0 1 0 1;
    0 1 0 0 0 0 0 1 1 1 0;
]

g = SimpleGraph(G)

gplot(g)

dominating_set = Model(GLPK.Optimizer)

@variable(dominating_set, x[1:nv(g)], Bin)
@constraint(dominating_set, [i = 1:nv(g)], dot(G[i,:], x) >= 1)
@objective(dominating_set, Min, sum(x))

optimize!(dominating_set)
@show value.(x);

membership = convert(Array{Int},value.(x)) # Change to Int
membership = membership + ones(Int, nv(g)) # Make the color groups one indexed
nodecolor = [colorant"red", colorant"blue"] # Blue to represent vertices in the set
nodefillc = nodecolor[membership]
gplot(g, nodefillc = nodefillc)

G = [
    0 0 0 0 1 0 0 0;
    0 0 0 0 0 1 0 0;
    0 0 0 0 0 0 1 0;
    0 0 0 0 0 0 0 1;
    1 0 0 0 0 1 0 1;
    0 1 0 0 1 0 1 0;
    0 0 1 0 0 1 0 1;
    0 0 0 1 1 0 1 0;
]

g = SimpleGraph(G)

gplot(g)

matching = Model(GLPK.Optimizer)

@variable(matching, m[i = 1:nv(g), j = 1:nv(g)], Bin)
@constraint(matching, [i = 1:nv(g)], sum(m[i,:]) <= 1)
@constraint(matching, [i = 1:nv(g), j = 1:nv(g); G[i,j] == 0], m[i,j] == 0)
@constraint(matching, [i = 1:nv(g), j = 1:nv(g)], m[i,j] == m[j,i])
@objective(matching, Max, sum(m))

optimize!(matching)
@show value.(m);

G = [
    0 1 0 0 1 1 0 0 0 0;
    1 0 1 0 0 0 1 0 0 0;
    0 1 0 1 0 0 0 1 0 0;
    0 0 1 0 1 0 0 0 1 0;
    1 0 0 1 0 0 0 0 0 1;
    1 0 0 0 0 0 1 0 0 1;
    0 1 0 0 0 1 0 1 0 0;
    0 0 1 0 0 0 1 0 1 0;
    0 0 0 1 0 0 0 1 0 1;
    0 0 0 0 1 1 0 0 1 0;
]

g = SimpleGraph(G)

gplot(g)

k = nv(g)

k_colouring = Model(GLPK.Optimizer)

@variable(k_colouring, z[1:k], Bin)
@variable(k_colouring, c[1:nv(g),1:k], Bin)
@constraint(k_colouring, [i = 1:nv(g)], sum(c[i,:]) == 1)
@constraint(k_colouring, [i = 1:nv(g), j = 1:nv(g), l = 1:k; G[i,j] == 1], c[i,l] + c[j,l] <= 1)
@constraint(k_colouring, [i = 1:nv(g), l = 1:k], c[i,l] <= z[l])

@objective(k_colouring, Min, sum(z))

optimize!(k_colouring)
@show value.(z);
@show value.(c);

c = value.(c)
membership = zeros(nv(g))
for i in 1:nv(g)
    for j in 1:k
        if c[i,j] == 1
            membership[i] = j
            break
        end
    end
end
membership = convert(Array{Int},membership)

nodecolor = distinguishable_colors(nv(g), colorant"green")
nodefillc = nodecolor[membership]
gplot(g, nodefillc = nodefillc)


using OrdinaryDiffEq
using LabelledArrays

function f(out,du,u,p,t)
  out.x = - 0.04u.x              + 1e4*u.y*u.z - du.x
  out.y = + 0.04u.x - 3e7*u.y^2 - 1e4*u.y*u.z - du.y
  out.z = u.x + u.y + u.z - 1.0
end

u₀ = LVector(x=1.0, y=0.0, z=0.0)
du₀ = LVector(x=-0.04, y=0.04, z=0.0)
tspan = (0.0,100000.0)

differential_vars = LVector(x=true, y=true, z=false)
prob = DAEProblem(f,du₀,u₀,tspan,differential_vars=differential_vars)

sol = solve(prob, DImplicitEuler())

function f1(du,u,p,t)
   du.x .= -1 .* u.x .* u.y .* p[1]
   du.y .= -1 .* u.y .* p[2]
end
const n = 1000
u_0 = @LArray  fill(1000.0,2*n) (x = (1:n),y = (n+1:2*n))
p = [0.1,0.1]
prob1 = ODEProblem(f1,u_0,(0,100.0),p)
sol = solve(prob1, Rodas5())
sol = solve(prob1, Rodas5(autodiff=false))


"""

```
sphereradiusfromshellvolume(volume,step)
```

Computes the radius that corresponds to a spherical shell of
a given volume.

"""
function sphereradiusfromshellvolume(volume,step)

  fourthirdsofpi = 4*pi/3
  if 3*step*volume - pi*step^4 <= 0.
    return 0.
  end
  rmin = (sqrt(3*pi)*sqrt(3*step*volume-pi*step^4)-3*pi*step^2)/(6*pi*step)
  return ( 0.5*( volume/fourthirdsofpi + 2*rmin^3 ) )^(1/3)

end