"""
Computes Jacobi polynomial and derivatives of Jacobi polynomials.

This function computes the jacobi polynomial of degree `n` with weights
`a` and `b` at point x (\$P_n^{a,b}(x)\$). There are several variants of the function
with default values:

 * `jacobi(x, n)` (`a` and `b` are both zero)
 * `jacobi(x, n, a)` (`b` is zero)
 * `jacobi(x, n, a, b) where `x` is an array
 * `jacobi!(x, n, a, b, y)` modifying array version of the function

The derivative of Jacobi polynomials (\$\\frac{dP_n^{a,b}(x)}{dx}\$)are computed with functions that add a `d` in front of its name:

 * `djacobi`
 * `djacobi!`

### Examples
```julia
using Jacobi

x = 0.3
a = 0.2
b = 0.1
m = 5
y = jacobi(x, m, a, b)
x1 = linspace(-1,1,21)
y1 = jacobi(x1, m, a, b)
jacobi!(x1, m, a, b, y1)

dy = djacobi(x, m, a, b)
dy1 = djacobi(x1, m, a, b)
djacobi!(x1, m, a, b, dy1)

```

"""
function jacobi(x, n, a, b)
    ox = one(x)
    zx = zero(x)
    if n==0
        return ox
    elseif n==1
        return ox/2 * (a - b + (a + b + 2)*x)
    end

    p0 = ox
    p1 = ox/2 * (a - b + (a + b + 2)*x)
    p2 = zx;

    for i = 1:(n-1)
	a1 = 2*(i+1)*(i+a+b+1)*(2*i+a+b);
	a2 = (2*i+a+b+1)*(a*a-b*b);
	a3 = (2*i+a+b)*(2*i+a+b+1)*(2*i+a+b+2);
	a4 = 2*(i+a)*(i+b)*(2*i+a+b+2);
	p2 = ox/a1*( (a2 + a3*x)*p1 - a4*p0);

        p0 = p1
        p1 = p2
    end

    return p2
end
jacobi(x, n) = jacobi(x, n, zero(x), zero(x))
jacobi(x, n, a) = jacobi(x, n, a, zero(x))

djacobi(x, n, a, b) =  one(x)/2 * (a + b + n + 1) * jacobi(x, n-1, a+1, b+1)

djacobi(x, n) = djacobi(x, n, zero(x), zero(x))
djacobi(x, n, a) = djacobi(x, n, a, zero(x))

eps1(::Type{T}) where {T<:AbstractFloat} = eps(T)
eps1(::Type{Complex{T}}) where {T<:AbstractFloat} = eps(T)

"""
Compute the zeros of Jacobi polynomials

This function computes the zeros of Jacobi polynomials:

 * \$P_m^{a,b}(x) = 0 \$

The `jacobi_zeros!` is the modifying version and the memory where the zeros
will be stored are preallocated. The non-modifying version, `jacobi_zeros`
allocates a the memory and calls the modifying version. The function `legendre_zeros`
compute the zeros of Legendre polynomials (`a = b = 0`)

### Examples
```
using Jacobi
z = jacobi_zeros(7, 0.3, 0.2)
```
"""
function jacobi_zeros!(m, alpha, beta, x::AbstractArray{T}) where {T<:Number}

    o = one(T)
    z = zero(T)

    a = convert(T,alpha)
    b = convert(T,beta)

    MAXITER = 500
    EPS::T = 100 * eps1(T)
    local i; local k; local iter=0

    for k = 1:m
        # Initial guess.
        r = -cos( (2k-o)/(2*m) * pi)
        if (k > 1)
            r = (r + x[k-1]) / 2
        end
        iter = 0
        while(true)
            s = z
            for i = 1:(k-1)
                s += o/(r - x[i])
            end

            poly = jacobi(r, m, a, b)
            delta = -poly / (djacobi(r, m, a, b) - poly*s)

            r += delta
            iter += 1

            if iter > MAXITER
                throw("Program did not converge")
            end

            if abs(delta) < abs(EPS)
                break
            end
        end
        x[k] = r
    end

    return x

end

function jacobi_zeros(m, a, b, ::Type{T}=Float64) where {T<:Number}
    jacobi_zeros!(m, a, b, zeros(T,m))
end

jacobi_zeros(m) = jacobi_zeros(m, 0.0, 0.0)
jacobi_zeros(m, a) = jacobi_zeros(m, a, zero(a))

@doc (@doc jacobi) djacobi
@doc (@doc jacobi) djacobi!
@doc (@doc jacobi) jacobi!
@doc (@doc jacobi_zeros!) jacobi_zeros


"""
Monte carlo integration is a very easy and scalable way to do multidimentional integrals.
However, only single variable integrals are considered.

This function takes three outputs:
	- `f`: the function to integrate. (at the momment only single variable is suported)
	- `N`: Number of points to sample. For most simple functions, 1000 to 10,000 should be okay.
	- `span`: a tuple (start, end) for the integration limits

Examples:
```julia
julia> monte_carlo_integration(x->3*x^2,100000,(0,1)) # integrate a polynomial
1.0000037602209

julia> monte_carlo_integration(x->sin(x),1000,(0,pi)) # integrate the sin function
2.0018927826323756

```
	Refereces:
-https://www.scratchapixel.com/lessons/mathematics-physics-for-computer-graphics/monte-carlo-methods-in-practice/monte-carlo-integration
-https://kingaa.github.io/sbied/pfilter/monteCarlo.html

Contributed By-: [Ved Mahajan](https://github.com/Ved-Mahajan)
"""
function monte_carlo_integration(f,N,span)
	x_start,x_end = span
	rand_array = rand(x_start:0.0001:x_end,N)
	return sum(f.(rand_array))*(x_end - x_start)/float(N)
end


using Plots, LaTeXStrings, ProgressBars
cd(dirname(@__FILE__))
function LogisticMap(x₀::Vector{Float64} , r::Float64, n::Real)
    x = x₀
    for i in 1:n
    x = 4 .* r .* x .* (1 .- x)
    end
    return x
end

####Part A####
r_s=collect(range(0.0, 1.0, step=10^(-4)))
x₀_s = rand(100)
x_s=zeros(length(r_s), length(x₀_s))

for (i,r) in enumerate(r_s)
    x_s[i,:]=LogisticMap(x₀_s, r, 10^4 )
end

scatter(r_s, x_s, legend=false, ms=0.5, color=:black, dpi=400, xlabel=L"r", ylabel=L"x", title=L"Logistic\ Map\ :\ x_n_+_1\ =\ 4rx_n(1-x_n)")
savefig("../../computational_physics/PSet9/Figs/Bifurcation.pdf")

####Part B####
####Zoomed Plot####
r_zoom= collect(range(0.70, 1.0, step=10^(-4)))
x₀_zoom = rand(range(0.70, 1.0, length=100), 100)
x_zoom=zeros(length(r_zoom), length(x₀_zoom))
for (i,r) in enumerate(r_zoom)
    x_zoom[i,:]=LogisticMap(x₀_zoom, r, 10^4 )
end
scatter(r_zoom, x_zoom, legend=false, ms=0.5, color=:black, dpi=400, xlabel=L"r", ylabel=L"x",
    title=L"Logistic\ Map\ :\ x_n_+_1\ =\ 4rx_n(1-x_n),\ 0.7<r<1.0")
savefig("../../computational_physics/PSet9/Figs/Zoomed_Bifurcation.pdf")
####Part C####
####Feigenbaum constants#####

#### σ ####
rb= collect(range(0.85, 0.99, step=10^(-4)))
x₀=rand(range(0.88, 0.99, length=256), 256)
x=zeros(length(rb), length(x₀))
for (i,r) in ProgressBar(enumerate(rb))
    x[i,:]=LogisticMap(x₀, r, 10^4 )
end
x
dₙ₋₂_index = findfirst(i->length(unique(round.(x[i, :], digits=9))) == 4, 1:1401)
dₙ₋₁_index = findfirst(i->length(unique(round.(x[i, :], digits=9))) == 8, 1:1401)
dₙ_index = findfirst(i->length(unique(round.(x[i, :], digits=9))) == 16, 1:1401)
δ=(rb[dₙ₋₁_index]-rb[dₙ₋₂_index])/(rb[dₙ_index]-rb[dₙ₋₁_index])
# σ ∼ 5.0999
#### α ####
dₙ₊₁_index = findfirst(i->length(unique(round.(x[i, :], digits=9))) == 32, 1:1401)
rₙ₋₂_index = findfirst(i-> 1/2 ∈ unique(ceil.(x[i, :], digits=3)), dₙ₋₂_index:dₙ₋₁_index) + dₙ₋₂_index
rₙ₋₁_index = findfirst(i-> 1/2 ∈ unique(ceil.(x[i, :], digits=3)),dₙ₋₁_index:dₙ_index) + dₙ₋₁_index
rₙ_index = findfirst(i-> 1/2 ∈ unique(ceil.(x[i, :], digits=3)), dₙ_index:dₙ₊₁_index) + dₙ_index
xsₙ₋₂ = sort(unique(round.(x[rₙ₋₂_index, :], digits=3)))
xsₙ₋₁ = sort(unique(round.(x[rₙ₋₁_index, :], digits=3)))
xsₙ = sort(unique(round.(x[rₙ_index, :], digits=3)))
α = (xsₙ₋₁[2] - xsₙ₋₁[1] - (xsₙ₋₂[4]- xsₙ₋₂[3] ))/( xsₙ[2] - xsₙ[1] - (xsₙ₋₁[2] - xsₙ₋₁[1]))


"""
    fivePointStencil(f,x,n::Integer,h)

Evaluates function f and its derivatives up to order n ∈ [0,4] at x:
``f(x),f'(x),...,f^{(n)}(x)``
The result is an array of length n+1.
Derivatives are numerically computed using the 5-point stencil method
with h≠0 being the grid spacing:
[https://en.wikipedia.org/wiki/Five-point_stencil](https://en.wikipedia.org/wiki/Five-point_stencil)

"""
function fivePointStencil(f,x,n::Integer,h)
    if ((n<0) || (n>4))
        error("In fivePointStencil: Invalid order $n")
    end
    if (h==0.0)
        error("In fivePointStencil: Invalid grid spacing $h")
    end
    fm2 = f(x-2h)
    fm1 = f(x-h)
    fn0 = f(x)
    fp1 = f(x+h)
    fp2 = f(x+2h)

    res = Vector{typeof(fn0)}(undef,n+1)

    res[1] = fn0

    (n==0) && return res

    res[2] = (-fp2+8fp1-8fm1+fm2)/(12*h)

    (n==1) && return res

    h2 = h * h
    res[3] = (-fp2+16fp1-30fn0+16fm1-fm2)/(12*h2)

    (n==2) && return res

    h3 = h2 * h
    res[4] = (fp2-2fp1+2fm1-fm2)/(2*h3)

    (n==3) && return res

    h4 = h3 * h
    res[5] = (fp2-4fp1+6fn0-4fm1+fm2)/h4

    return res
end


"""
    NCalculus

Numerical Differentiation and Integration Module.
"""
module NCalculus

const GaussianRoots = [0.5773502692,-0.5773502692,
        0.7745966692,0.0,-0.7745966692,
        0.8611363116,0.3399810436,-0.3399810436,-0.8611363116,
        0.9061798459,0.5384693101,0.0,-0.5384693101,-0.9061798459]
const GaussianCoef = [1.0,1.0,
    0.5555555556,0.8888888889,0.5555555556,
    0.3478548451,0.6521451549,0.6521451549,0.3478548451,
    0.2369268850,0.4786286705,0.5688888889,0.4786286705,0.2369268850]

"""
    ThreePoint(f::Function, x₀::Real, h::Real; method::String="Endpoint")

input a function `f`, x₀ and h.Args is method (default: "Endpoint", if you set method is't "Endpoint", it will
return MidPoint ans.)
"""
@inline function ThreePoint(f::Function, x₀::Real, h::Real; method::String="Endpoint")
    if method == "Endpoint"
        return 1/h*(-3/2*f(x₀) + 2*f(x₀+h)- 1/2*f(x₀+2*h))
    else
        return 1/(2*h) * (f(x₀+h) - f(x₀- h))
    end
end

"""
    FivePoint(f::Function, x₀::Real, h::Real; method::String="Endpoint")

input a function `f`, x₀ and h.Args is method (default: "Endpoint", if you set method is't "Endpoint", it will
return MidPoint ans.)
"""
@inline function FivePoint(f::Function, x₀::Real, h::Real; method::String="Endpoint")
    if method == "Endpoint"
        return 1/(12*h)*(-25*f(x₀)+48f(x₀ +h)-36f(x₀+2h)+16f(x₀+3h)-3f(x₀+4h))
    else
        return 1/(12*h) * (f(x₀ - 2h) - 8f(x₀ -h)+8f(x₀+h)-f(x₀ + 2h))
    end
end

"""
    Trapezoidal(f::function, a::Real, b::Real)

input a function `f`, and ``x in [a, b]`` calculate ∫f(x)dx.
"""
@inline function Trapezoidal(f::Function, a::Real, b::Real)
    return (b-a)/2 * (f(a) + f(b))
end

"""
    Simpson(f::Function, a::Real, b::Real)

input a function `f`, and ``x in [a, b]`` calculate ∫f(x)dx.
"""
@inline function Simpson(f::Function, a::Real, b::Real)
    return (b-a)/6 * (f(a) + 4*f(a+(b-a)/2) + f(b))
end

"""
    Newton_Cotes(f::Function, a::Real, b::Real; n::Int=0, method::String="Midpoint")

input a function `f`, and ``x in [a, b]``` calculate ``int_a^b f(x)dx``. args ``n∈[1,4]``
"""
@inline function Newton_cotes(f::Function, a::Real, b::Real; n::Int=0, method::String="Midpoint")
    if method == "Open"
        o1 = (b-a)/2 * (f(a) + f(b))
        o2 = (b-a)/6 * (f(a) + 4*f(a+(b-a)/2) + f(b))
        o3 = 3*(b-a)/24 * (f(a) + 3*f(a+(b-a)/3) + 3*f(a+2*(b-a)/3) + f(b))
        o4 = 2*(b-a)/180 * (7*f(a) + 32*f(a+(b-a)/4) + 12*f(a+(b-a)/2) + 32*f(a + 3*(b-a)/4) + 7*f(b))
        o = [o1, o2, o3, o4]
        if 0<n≤4
            return o[n]
        else
            return o
        end
    else
        c1 = 2 * (b-a)/2 * f((b+a)/2)
        c2 = 3/2 * (b-a)/3 * (f((b-a)/3) + f(2*(b-a)/3))
        c3 = 4/3 * (b-a)/4 * (2*f((b-a)/4) - f((b-a)/2) + 2*f(3*(b-a)/4))
        c4 = 5/24 * (b-a)/5 * (11*f((b-a)/5)+f(2*(b-a)/5)+f(3*(b-a)/5)+11f(4*(b-a)/5))
        c = [c1, c2, c3, c4]
        if 0<n≤4
            return c[n]
        else
            return c
        end
    end
end

"""
    Romberg(f::Function, a::Real, b::Real, n::Int; seq_tab::Bool=false)
input a function `f`, and ``x in [a, b]``, set `n`.if Args `seq_tab=true`, will return a table, else return a value.
"""
@inline function Romberg(f::Function, a::Real, b::Real, n::Int; seq_tab::Bool=false)
    h = (b-a)
    result = zeros(n,n)
    result[1,1] = (f(a)+f(b))*h/2
    for i = 2:n
        w = f(a)+f(b)
        for j = 1:(2^(i-1)-1)
            w = w + 2*f(a+j*(h/2^(i-1)))
        end
        result[i,1] = w*(h/2^i)
    end
    for i = 2:n
        for j = i:n
            result[j,i] = result[j,i-1]+(1/(4^(i-1)-1))*(result[j,i-1]-result[j-1,i-1])
        end
    end
    if seq_tab
        return result
    else
        return result[n,n]
    end
end

"""
    Gaussian_Quad(f::Function; n::Int=3, a::Real=-1, b::Real=1)

Gaussian Quadrature must set `n<6`,default calculate `int f(x)dx` in `[-1,1]`. you can set interval `[a, b]` to change default interval.
"""
@inline function Gaussian_Quad(f::Function;n::Int=3,a::Real=-1, b::Real=1)
    #Gaussian Args
    #Root = [0.5773502692,-0.5773502692,
    #    0.7745966692,0.0,-0.7745966692,
    #    0.8611363116,0.3399810436,-0.3399810436,-0.8611363116,
    #    0.9061798459,0.5384693101,0.0,-0.5384693101,-0.9061798459]
    #Coef = [1.0,1.0,
    #    0.5555555556,0.8888888889,0.5555555556,
    #    0.3478548451,0.6521451549,0.6521451549,0.3478548451,
    #    0.2369268850,0.4786286705,0.5688888889,0.4786286705,0.2369268850]

    if a == -1 && b == 1
        return GaussianCoef[sum(1:n-1):sum(1:n-1)+n-1]' * f.(GaussianRoots[sum(1:n-1):sum(1:n-1)+n-1])

    else
        return GaussianCoef[sum(1:n-1):sum(1:n-1)+n-1]' * (b-a)/2 * f.((b-a)/2 .* GaussianRoots[sum(1:n-1):sum(1:n-1)+n-1] .+ (b+a)/2)
    end
end

"""
    MutipleIntegralSimple(f::Function, interval_x::Tuple, interval_y::Tuple)

It will apply the **Composite Trapezoidal rule** to approximate the integral.
"""
@inline function MutipleIntegralSimple(f::Function, interval_x::Tuple, interval_y::Tuple)
    a, b = interval_x
    c, d = interval_y

    return (b-a)*(d-c)/16 * (f(a,c) + f(a,d) +f(b,c) + f(b,d)
    + 2*(f((a+b)/2,c) + f((a+b)/2, d) + f(a, (c+d)/2) + f(b, (c+d)/2))
    + 4*f((a+b)/2, (c+d)/2))
end

"""
    SimpsonDoubleIntegral(f::Function, interval_x::Tuple, y_up::Function, y_down::Function; n::Int =6, m::Int=6)

``f  = f(x, y), x ∈ interval_x, y = yup(x) - ydown(x)``, Args m and n is Separation distance.
"""
@inline function SimpsonDoubleIntegral(f::Function, interval_x::Tuple, y_up::Function, y_down::Function; n::Int =6, m::Int=6)
    a, b = interval_x
    h = (b - a) / n
    J₁, J₂, J₃ = zeros(3)
    for i  = 0:n
        x = a + i * h
        HX = (y_up(x) - y_down(x)) / m
        K₁ = f(x, y_down(x)) + f(x, y_up(x))
        K₂, K₃ = 0, 0
        for j = 1:m-1
            y = y_down(x) + j*HX
            Q = f(x, y)
            if iseven(j)
                K₂+= Q
            else
                K₃+= Q
            end
        end
        L = (K₁ + 2*K₂ + 4*K₃)*HX/3
        if i == 0 || i == n
            J₁ = J₁ + L
        elseif iseven(i)
            J₂+= L
        else
            J₃+= L
        end
    end
    return h*(J₁+2J₂+4J₃)/3
end

"""
     GaussianDoubleIntegral(f::Function, interval_x::Tuple, y_up::Function, y_down::Function, m::Int=5, n::Int=5)

``f  = f(x, y), x ∈ interval_x, y = yup(x) - ydown(x)``, Args m and n is Separation distance.
"""
@inline function GaussianDoubleIntegral(f::Function, interval_x::Tuple, y_up::Function, y_down::Function; m::Int=5, n::Int=5)
    m, n = min(5, m), min(5, n)
    a, b = interval_x
    h₁ = (b - a) / 2
    h₂ = (b + a) / 2
    J = 0
    for i in 1:m
        JX = 0
        x = h₁ * GaussianRoots[sum(1:m-1)+i-1] + h₂
        d₁ = y_up(x)
        c₁ = y_down(x)
        k₁ = (d₁ - c₁)/2
        k₂ = (d₁ + c₁)/2
        for j = 1:n
            y = k₁ * GaussianRoots[sum(1:n-1)+j-1] + k₂
            Q = f(x, y)
            JX += GaussianCoef[sum(1:n-1)+j-1] * Q
        end
        J += GaussianCoef[sum(1:m-1)+i-1] * k₁ * JX
    end
    return h₁*J
end

end


# code to calculate the first derivative of a function using finite differences
# function to be evaluated is a sin function
#Initial Parameters
using Statistics
xmax=10.0
nx=200
dx=xmax/(nx-1)
x=LinRange(0,xmax,nx)
#Initialization of the sin function
l=20*dx #wavelength
k=2*pi/l
f=sin.(k.*x)
using PyPlot
figure(1)
plot(x,f)
xlabel("X")
ylabel("Amplitude")
title("Sin function")
#Initialize the analytical and numerical derivatives
ader=zeros(nx)
nder=zeros(nx)
#Numerical derivative
for i in 2:nx-1
    nder[i]=(f[i+1]-f[i-1])/(2*dx)
end
#Analytical derivative
ader=k*cos.(k.*x)
ader[1]=0
ader[nx]=0
#Calculate RMS error
t1=(ader.-nder)
t2=t1.^2
t3=mean(t2)
rms=sqrt.(t3)
difference=ader-nder
figure(2)
plot(x,ader)
plot(x,nder)
plot(x,difference)
xlabel("X")
ylabel("Amplitude")
title("Derivatives rms error: $rms")
gcf()


### A Pluto.jl notebook ###
# v0.12.10

using Markdown
using InteractiveUtils

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

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

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

# ╔═╡ fa68607e-22e5-11eb-0558-c9a4d9f77426
begin
	n = 2000
	data = rand(Normal(),n)
	l, m = minimum(data), maximum(data)

	delta = 0.3;
	bins = [(x,x+delta) for x in l:delta:m-delta]
	if last(bins)[2] < m
		push!(bins,(last(bins)[2],m))
	end
	L = length(bins)
end

# ╔═╡ 77671026-24f7-11eb-13a3-4b72e9a23228
inBin(x,j) = first(bins[j]) <= x && x < last(bins[j])

# ╔═╡ 7767403c-24f7-11eb-2b4e-a53365e0cc7e
sizeBin(j) = last(bins[j]) - first(bins[j])

# ╔═╡ 7767cd10-24f7-11eb-0c8c-21a0e6b3a7c8
f(j) = sum([inBin(x,j)  for x in data])/n

# ╔═╡ 777462b4-24f7-11eb-2d4d-49436588ef26
h(x) = sum([f(j)/sizeBin(j) * inBin(x,j) for j in 1:L])

# ╔═╡ 7774fe3e-24f7-11eb-0810-390ca9b1c641
begin
	xGrid = -4:0.01:4
	histogram(data,normed=true, bins=L,
		label="Built-in histogram",
		c=:blue, la=0, alpha=0.6)
	plot!(xGrid,h.(xGrid), lw=3, c=:red, label="Manual histogram",
		xlabel="x",ylabel="Frequency")
	plot!(xGrid,pdf.(Normal(),xGrid),label="True PDF",
		lw=3, c=:green, xlims=(-4,4), ylims=(0,0.5))
end

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

# ╔═╡ Cell order:
# ╟─ca5fbdc8-22e5-11eb-0a60-cb0a127a3ca5
# ╠═e077ab16-22e5-11eb-15ed-5f0654bf7328
# ╠═e75a9a06-22e5-11eb-2e36-8d4f62b830ed
# ╠═fa68607e-22e5-11eb-0558-c9a4d9f77426
# ╠═77671026-24f7-11eb-13a3-4b72e9a23228
# ╠═7767403c-24f7-11eb-2b4e-a53365e0cc7e
# ╠═7767cd10-24f7-11eb-0c8c-21a0e6b3a7c8
# ╠═777462b4-24f7-11eb-2d4d-49436588ef26
# ╠═7774fe3e-24f7-11eb-0810-390ca9b1c641
# ╟─475ff888-22e6-11eb-2354-09f8f40a8e12


"""
    secant(x0, x1, f, e, n)

Compute the root of the given function using Secant method, an Extension of Newton's method.
Insted of using a tangent line, which requres knoledge of the function's derivative, Secant method uses two values of
the initial approximation `x0` and `x1` to find secant line and uses the intersection of the secant line and x-axis
to find the next approximation.

    secant(x0::Float64, x1::Float64, f::Function, e::Float64, n::Integer)

### OUTPUT:
  * `p` - approximation of the root of the function `f` (Float64) or,
  * `-1` - if `n` the maximum number of iterations was exceeded.
"""
function secant(x0::Float64, x1::Float64, f::Function, e::Float64, n::Integer)

    f0 = f(x0)
    f1 = f(x1)

    for i in 1:n

        x = x1 - f1*(x1-x0)/(f1 - f0)
        fx = f(x)
        x0 = x1
        x1 = x
        f0 = f1
        f1 = fx

        if(abs(x1 - x0) < e)
            return x1
        end
    end

    return -1
end


"""
# Exponential Search
It works in O(Log n) time
Exponential search involves two steps:
- Find range where element is present
- Do Binary Search in above found range.
### Time Complexity :
O(Log n)
### Auxiliary Space :
The above implementation of Binary Search is recursive and requires O(Log n) space. With iterative Binary Search, we need only O(1) space.
Applications of Exponential Search:
Exponential Binary Search is particularly useful for unbounded searches, where size of array is infinite. Please refer Unbounded Binary Search for an example.
It works better than Binary Search for bounded arrays, and also when the element to be searched is closer to the first element.
"""

"""
	Binary Search
"""
function binary_search(arr::AbstractArray{T,1}, l::T, r::T, x::T) where T <: Real
	n = size(arr)[1]
	if(r>=l)
		mid = Int(ceil(l+(r-l)/2));
		println(mid)
		if(arr[mid] == x)
			return "Element present at index $mid"
		elseif(arr[mid] > x)
			binary_search(arr, l, mid-1, x)
		else
			binary_search(arr, mid+1, r, x)
		end
	else
		return "Element not present in array"
	end
end

"""
	 exponential_search(arr::AbstractArray{T,1}, x::T) where {T <: Real}

Exponential Search in 1-D array
Time Complexity:  O(Log n)
"""
function exponential_search(arr::AbstractArray{T,1}, x::T) where {T <: Real}
	n = size(arr)[1]
	if(arr[1] == x)
		return "Elemenet present at index 1"
	end

	i = 1
	while( i<n && arr[i]<=x)
		i = i * 2
	end
	return binary_search( arr, Int(ceil(i / 2)), min(i, n), x)
end

# Arguments
arr = [1, 2, 3, 4, 13, 15, 20];
x = 4;
n = size(arr)[1]
l = 1;
r = n;

exponential_search(arr, x)


#-----------------------------------------------------------
# Woodbury inequality for efficient inverse calculation
#-----------------------------------------------------------

"""
    woodbury_A_plus_UCV(; A⁻¹=A⁻¹, U=U, C⁻¹=C⁻¹, V=V)

Efficient computation of `(A + UCV)⁻¹`` when `A⁻¹`` is available (and `C` can be easily inverted).
It does so by applying the Woodbury identity is used:

# Examples
```julia-repl
julia> using LinearAlgebra
julia> A = randn(5,5); A = A*A'; U = randn(5,3); C = Diagonal(rand(3)); V = randn(3,5);
julia> A⁻¹ = A\\I # assume inverse is available
julia> B1 = inv(A + U*C*V) # regular way of computing inverse
julia> B2 = woodbury_A_plus_UCV(; A⁻¹=A⁻¹, U=U, C⁻¹=C\\I, V=V)
julia> maximum(abs.(B1.-B2))<1e-10 # verify that they are numerically equal, should return true
```
"""
woodbury_A_plus_UCV(; A⁻¹=A⁻¹, U=U, C⁻¹=C⁻¹, V=V) =  A⁻¹ - A⁻¹*U*((C⁻¹ + V*A⁻¹*U)\V)*A⁻¹

#-----------------------------------------------------------
# Make matrix symmetric by adding its transpose
#-----------------------------------------------------------

function makematrixsymmetric!(A)
    I,J = size(A)
    for i in 1:I
        for j in i+1:J
            @inbounds tmp = (A[i,j]+A[j,i])/2
            @inbounds A[i,j] = A[j,i] = tmp
        end
    end
    nothing
end

function makematrixsymmetric(A)
    (A+A')/2
end

#-----------------------------------------------------------
# Add a small constant, i.e. jitter to the diagonal
#-----------------------------------------------------------

function addjitter!(A, JITTER = 1e-8)

    I, J = size(A)

    if I !== J
        error("Matrix passed to  addjitter! must be square")
    end

    for i in 1:I
        @inbounds A[i,i] += JITTER
    end

    nothing
end

addjitter(A, JITTER = 1e-8) = A + JITTER*I

#-----------------------------------------------------------
# Find closest positive definite matrix
#-----------------------------------------------------------

"""
    nearestposdef(A; minimumeigenvalue = 1e-6)

Nearest positive definite matrix in Frobenious norm.
Also called the projection of the matrix onto the cone of positive definite matrices.
See: https://nhigham.com/2021/01/26/what-is-the-nearest-positive-semidefinite-matrix/
```
"""
function nearestposdef(A; minimumeigenvalue = 1e-6)

    local Evalues, Evector = eigen(A)

    local newA = Evector * Diagonal(max.(Evalues, minimumeigenvalue)) * Evector'

    makematrixsymmetric!(newA)

    newA

end


module InverseQuadraticInterpolation
export invquadinterp

function invquadinterp(f::Function, x0::Number, x1::Number, x2::Number,
                        args::Tuple=(); xtol::AbstractFloat=1e-5,
                        ytol=2eps(Float64), maxiter::Integer=50)
    y0 = f(x0, args...)
    y1 = f(x1, args...)
    y2 = f(x2, args...)
    for _ in 1:maxiter
       x = x0*y1*y2/((y0-y1)*(y0-y2)) +
           x1*y0*y2/((y1-y0)*(y1-y2)) +
           x2*y0*y1/((y2-y0)*(y2-y1))
        # x-tolerance
        if min(abs(x-x0), abs(x-x1), abs(x-x2)) < xtol
           return x
        end
        y = f(x, args...)
        # y-tolerance.
        if abs(y) < ytol
            return x
        end
        x0 = x1
        y0 = y1
        x1 = x2
        y1 = y2
        x2 = x
        y2 = y
    end
    error("Max iteration exceeded")
end

end


using Pkg
Pkg.add("QuadGK")

using QuadGK

func1(x) = exp(-x^2)
res, err = quadgk(func1, -Inf, Inf)

abs(res - sqrt(π)) / sqrt(π)

res, err = quadgk(func1, -Inf, Inf, rtol = 1e-15)

abs(res - sqrt(π)) / sqrt(π)

res, err = quadgk(func1, -Inf, Inf, order = 12)

abs(res - sqrt(π)) / sqrt(π)

sqrt(π)

#%%

res, err = quadgk(func1, BigFloat(-Inf), BigFloat(Inf), rtol = 1e-15, order=12)
abs(res - sqrt(BigFloat(π))) / sqrt(BigFloat(π))
#%%
func2(x, y, z) = x + y^3 + sin(z)

x = 5
z = 3
arg(y) = func2(x, y, z)

quadgk(arg, 1, 3)

quadgk(y -> func2(x, y, z), 1, 3)

res, err = let x=5; z=3
    arg(y) = func2(x, y, z)

    quadgk(arg, 1, 3)
end

#%%
func3(x, y) = x^2 * exp(y)

function test_int(x, ymin, ymax)
    arg(y) = func3(x, y)
    return quadgk(arg, ymin, ymax)[1]
end

test_int(3, 1, 5)


using LinearAlgebra

function cheb4c(N)

    # The function x,D4 = cheb4c(N) computes the fourth derivative
    # matrix D4 on Chebyshev interior points, incorporating the
    # clamped boundary conditions.
    # It is adapted from the function with the same name in dmsuite
    # developed for Matlab by JAC Weideman and SC Reddy.
    #
    # Adapted by M. Beneitez at KTH Mechanics.
    # beneitez@mech.kth.se
    #
    # Input:
    # N-2:  Order of the differentiation matrix.
    #
    # Output:
    # yF: Interior Chebyshev points
    # D4: Size [N-2,N-2] Fourth derivative matrix
    #

    eye = Matrix{Float64}(I, N-2, N-2);

    n1 = Int(floor(N/2-1)); n2  = Int(ceil(N/2-1));     # Indices used for flipping trick.

    k = collect(1:N-2);                        # Compute theta vector.
    th = k.*pi./(N-1)

    x = sin.(pi.*collect(N-3:-2:3-N)./(2*(N-1))); # Compute Chebyshev points.

    s = [sin.(th[1:n1]);reverse(sin.(th[1:n2]), dims = 1)]

    alpha = s.^4;
    beta1 = -4*s.^2 .*x./alpha;
    beta2 = 4*(3*x.^2 .-1)./alpha;
    beta3 = 24*x./alpha;
    beta4 = 24 ./alpha;
    B = [beta1'; beta2'; beta3'; beta4'];

    T = repeat(th/2,1,N-2);
    DX = 2*sin.(T'+T).*sin.(T'-T);          # Trigonometric identity.
    DX = [DX[1:n1,:]; -reverse(reverse(DX[1:n2,:], dims = 2), dims=1)];   # Flipping trick.
    DX[eye.==true] = ones(N-2,1);                      # Put 1's on the main diagonal of DX.

    ss = s.^2 .*(-1.0).^k;
    S  = repeat(ss, 1, N-2);
    C  = S./S';

    Z  = 1 ./DX;
    Z[eye.==true] = zeros(N-2,1);

    X  = Z';
    X  = X[X .!= 0.0];
    X  = reshape(X,N-3,N-2);

    Y  = ones(N-3,N-2);
    D  = Matrix{Float64}(I, N-2, N-2);
    DM = zeros(N-2,N-2,4);

    for ell = 1:4
        Y = cumsum([B[ell,:]'; ell.*Y[1:N-3,:].*X], dims = 1);
        D = ell.*Z.*(C.*repeat(diag(D), 1, N-2)-D);
        D[eye.==true] = Y[N-2,:];
        DM[:,:,ell] = D;
    end

    D4 = DM[:,:,4];

    return x,D4

end


"""

`MTH229`: helper functions for using `Julia` with MTH229

This module does two things:

* Install other useful packages with one command (`Roots`,  `ForwardDiff`, `QuadGK`, `SpecialFunctions`, ...) and re-exports their methods.

* Add a number of helper functions.

The helper functions include:

- `secant(f, a, b)`: return a function giving the secant line between ``(a,f(a))`` and ``(b,f(b))``.

- `tangent(f, c)`:  return a function giving the tangent line to ``f(x)`` at the point ``(c,f(c))``.

- `bisection(f, a, b)`: A simple implementation of the bisection
  method. The interval ``[a,b]`` should be a bracketing interval. This
  function makes an illustrative graphic. For real use of the bisection method, the `fzero(f,
  a, b)` function, from the `Roots` package, should be used.

- `'`: As in `f'`. Overloads `adjoint` allowing the derivative of   a function to be found as with math notation: `f'`.  The notation can be used for higher-order derivatives too: `f''`, `f'''`, ... This uses automatic differentiation from the `ForwardDiff` package.

- `plotif(f, g, a, b)`: Plot the function `f` over the interval `[a,b]` and color differently where ``g(x) > 0`` over ``[a,b]``. By passing in `f` for `g` shows where `f` is positive on `[a,b]`; passing in `f'` shows where `f` is increasing on `[a,b]`; and passing in `f''` shows where `f` is concave up on `[a,b]`.

- `sign_chart(f, a, b)`: shows a *signchart* of `f` by numerically identifying the zero crossings or infinities of `f` over `[a,b]` (assuming `f(a)` and `f(b)` are non zero, then checking the sign between these values. Calling `sign_chart(f', a, b)` is useful for the first-derivative test and `sign_chart(f'', a, b)` for the second-derivative test.

- `fisheye(f)` returns the composition `atan ∘ f ∘ tan`, which can be useful to find zeros of a function over the entire range of real numbers.

- `riemann(f, a, b, n; method="right")` An implementation of Riemann sums. The method can be "right" or "left" for Riemann sums, or "trapezoid" or "simpsons" for related approximations.

This package provides some plotting routines for `Plots`, `SimplePlots`, and `Makie`. For the latter two, some "recipes" for plotting functions and symbolic directions; and for all some convenience methods.
"""
module MTH229

# msg = """
# Loading the `MTH229` package.

# * Run the command `?MTH229` for a short description.

# """

# @info msg

if isdefined(Base, :Experimental) && isdefined(Base.Experimental, Symbol("@optlevel"))
    @eval Base.Experimental.@optlevel 1
end

using Reexport
@reexport using Roots
@reexport using SpecialFunctions
@reexport using SymPy
@reexport using QuadGK
@reexport using LinearAlgebra
@reexport using ForwardDiff
using PlotUtils

using Requires

function __init__()
     @require SimplePlots="307c2aad-90be-4152-b348-f51955fac6ce" include("simpleplots.jl")
     @require Plots="91a5bcdd-55d7-5caf-9e0b-520d859cae80" include("plots.jl")
     @require AbstractPlotting="537997a7-5e4e-5d89-9595-2241ea00577e" include("makie.jl")
 end

## start it up

"""
    mth229(dirnm)
Entry point to install projects and start notebook
"""
function mth229(dirnm=homedir())
    eval(:(using IJulia))
    eval(:(import ZipFile))

    if !isfile(joinpath(dirnm, "01-calculator.ipynb"))
        @warn "installing projects in $dirnm"
        zf = "https://www.github.com/mth229/229-projects/archive/master.zip"
        zarchive = ZipFile.Reader(download(zf))
        !isdir(dirnm) && mkdir(dirnm)
        cd(dirnm)

        @show zarchive.files
        for f in zarchive.files
            nm = basename(f.name)
            occursin("ipynb", nm) || continue
            @info "installing $nm"
            open(nm, "w") do io
                write(io, read(f, String))
            end
        end
    end

    Base.invokelatest(IJulia.notebook)
end
export mth229

###
export tangent, secant, D, grad, sign_chart, fisheye, rangeclamp
export lim
export bisection, newton
export riemann, fubini
export uvec, xs_ys, unzip, parametric_grid
export e

@info "This package defines `e = exp(1)`"
"e is not Base.MathConstants.ℯ, rather `exp(1)` so that it plays nicely with ForwardDiff"
e = exp(1)

"""
    rangeclamp(f, hi=20, lo=-hi)

Replace large values of `f` with `NaN`; useful for plotting with vertical asymptotes.
"""
rangeclamp(f, hi=20, lo=-hi; replacement=NaN) = x -> lo < f(x) < hi ? f(x) : replacement

# give a warning
@info "This package overloads the `'` operation for derivatives. This may cause issues with linear algebra usage"

 " f'(x) will find the derivative of `f` using Automatic Differentation from the `ForwardDiff` package "
Base.adjoint(f::Function) = x -> ForwardDiff.derivative(f, float(x))
function D(f, n::Int=1)
    n < 0 && throw(ArgumentError("n must be non-negative"))
    n == 0 && return f
    n == 1 && return x -> ForwardDiff.derivative(f, float(x))
    D(D(f), n-1)
end
grad(f) = (x, xs...) -> ForwardDiff.gradient(f, vcat(x, xs...))
# could aslo wrap as function ForwardDiff.jacobian, ForwardDiff.hessian

"""
   sign_chart(f, a, b; atol=1e-4)

Create a sign chart for `f` over `(a,b)`. Returns a tuple with an identified zero or vertical asymptote and the corresponding sign change. The tolerance is used to disambiguate numerically found values.

# Example

```
julia> sign_chart(x -> x/(x-1)^2, -5, 5)
2-element Vector{NamedTuple{(:∞0, :sign_change), Tuple{Float64, String}}}:
 (∞0 = 0.0, sign_change = "- → +")
 (∞0 = 1.0000000000000002, sign_change = "+ → +")
```

"""
function sign_chart(f, a, b; atol=1e-6)
    pm(x) = x < 0 ? "-" : x > 0 ? "+" : "0"
    summarize(f,cp,d) = (DNE_0_∞=cp, sign_change=pm(f(cp-d)) * " → " * pm(f(cp+d)))

    if Roots._is_f_approx_0(f(a),a, eps(), eps()) ||
        Roots._is_f_approx_0(f(b), b, eps(), eps())
        return "Sorry, the endpoints must not be zeros for the function"
    end

    zs = find_zeros(f, a, b)
    pts = vcat(a, zs, b)
    for (u,v) ∈ zip(pts[1:end-1], pts[2:end])
        zs′ = find_zeros(x -> 1/f(x), u, v)
        for z′ ∈ zs′
            flag = false
            for z ∈ zs
                if isapprox(z′, z, atol=atol)
                    flag = true
                    break
                end
            end
            !flag && push!(zs, z′)
        end
    end

    if isempty(zs)
	fc = f(a + (b-a)/2)
	return "No sign change, always " * (fc > 0 ? "positive" : iszero(fc) ? "zero" : "negative")
    end

    sort!(zs)
    m,M = extrema(zs)
    d = min((m-a)/2, (b-M)/2)
    if length(zs) > 1
        d′ = minimum(diff(zs))/2
        d = min(d, d′ )
    end
    summarize.(f, zs, d)
end

"""
Returns a function describing the tangent line to the graph of f at x=c.

Example. Where does the tangent line intersect the y axis?
```
f(x) = sin(x)
tl(x) = tangent(f, pi/4)(x)  # or tl = tangent(f, pi/3) to use a non-generic function
tl(0)
```

Uses the automatic derivative of `f` to find the slope of the tangent line at `x=c`.

"""
tangent(f,c) = x -> f(c) + f'(c) * (x-c)

"""
Returns a function describing the secant line to the graph of f at x=a and x=b.

Example. Where does the secant line intersect the y axis?
```
f(x) = sin(x)
a, b = pi/4, pi/3
sl(x) = secant(f, a, b)(x)  # or sl = sl(f, a, b) to use a non-generic function
sl(0)
```

"""
secant(f, a, b) = x -> f(a) + (f(b) - f(a)) / (b-a) * (x - a)

"""

`lim(f, c, n, dir="+")`: means to generate numeric table of values of `f` as `h` gets close to `c`.

Example:
```
f(x) = sin(x) / x
lim(f, 0)
```
"""
function lim(f::Function, c::Real; n::Int=6, dir="+")
	 hs = [(1/10)^i for i in 1:n] # close to 0
	 if dir == "+"
	   xs = c .+ hs
	 else
	   xs = c .- hs
	 end
	 ys = map(f, xs)
	 [xs ys]
end

"""

Simple implementation of the bisection method.

Example:

```julia
bisection(sin, 3, 4)
f(x) = x^5 - x^4 - x^3 - x^2 - x - 1
a = bisection(f, 1, 2)
f(a)
```

The display shows a simple graphic illustrating the method's division for the first few steps.

An easier-to-understand alternative to `Roots.find_zero(f, (a,b), Bisection())`.

"""
function bisection(f::Function, a, b)
    a,b = sort([a,b])

    if f(a) * f(b) > 0
        error("[a,b] is not a bracket. A bracket means f(a) and f(b) have different signs!")
    end

    M = a + (b-a) / 2

    i, j = 0, 64
    ss = fill("#", 65)
    ss[i+1]="a"; ss[j+1]="b"
    println("")
    println(join(ss))
    flag = true

    while a < M < b
        if flag && j-i == 1
            ss = fill(" ", 65)
            ss[j:(j+1)] .= "⋮"
            println(join(ss))
            println("")
            flag = false
        end

        if f(M) == 0.0
            println("... exact answer found ...")
	    break
        end
        ## update step
	if f(a) * f(M) < 0
	    a, b = a, M

            if flag
                j = div(i + j, 2)
            end

	else
	    a, b = M, b

            if flag
                i = div(i + j, 2)
            end

	end

        if flag
            ss = fill(".", 65)
            ss[i+1]="a"; ss[j+1]="b"; ss[(i+2):j] .= "#"
            println(join(ss))
        end

        M = a + (b-a) / 2
    end
    M
end

newton(f, fp, x0; kwargs...) = Roots.find_zero((f,fp), x0, Roots.Newton(); kwargs...)
newton(f, x0; kwargs...) = newton(f, f', x0; kwargs...)

"""
    fisheye(f)

Transform `f` defined on `(-∞, ∞)` to a new function whose domain is in `(-π/2, π/2)` and range is within `(-π/2, π/2)`. Useful for finding all zeros over the real line. For example

```
f(x) = 1 + 100x^2 - x^3
fzeros(f, -100, 100) # empty just misses the zero found with:
fzeros(fisheye(f), -pi/2, pi/2) .|> tan  # finds 100.19469143521222, not perfect but easy to get
```

By Gunter Fuchs.
"""
fisheye(f) = atan ∘ f ∘ tan

##
## --------------------------------------------------
##

"""
riemann: compute Riemann sum approximations to a definite integral. As well, implement trapezoid and Simpson's rule.

Example:
```
f(x) = exp(x^2)
riemann(f, 0, 1, 1000)   # default right-Riemann sums
riemann(f, 0, 1, 1000, method="left")       # left sums
riemann(f, 0, 1, 1000, method="trapezoid")  # use trapezoid rule
riemann(f, 0, 1, 1000, method="simpsons")   # use Simpson's rule
```

"""
function riemann(f::Function, a::Real, b::Real, n::Int; method="right")
  if method == "right"
     meth = (f,l,r) -> f(r) * (r-l)
  elseif method == "left"
     meth= (f,l,r) -> f(l) * (r-l)
  elseif method == "trapezoid"
     meth = (f,l,r) -> (1/2) * (f(l) + f(r)) * (r-l)
  elseif method == "simpsons"
     meth = (f,l,r) -> (1/6) * (f(l) + 4*(f((l+r)/2)) + f(r)) * (r-l)
  end

  xs = a .+ (0:n) * (b-a)/n
  as = [meth(f, l, r) for (l,r) in zip(xs[1:end-1], xs[2:end])]
  sum(as)
end

#######
## simplified multivariable integrals

# limits of integration
endpoints(ys,x) = ((f,x) -> isa(f, Function) ? f(x...) : f).(ys, Ref(x))
# avoid specialization in quadgk
struct FWrapper
    f
end
(F::FWrapper)(x) = F.f(x)

"""
fubini(f, dy, dx)
fubini(f, dz, dy, dx)

Computes numeric integral of `f` over region specified by `dz`, `dy`, `dx`. These are a tuple of values of numbers or univariate functions depending on the value of the term on the right (`dy` can depend on `dx`s value).

*Much* slower than `hcubature` from the `HCubature` package, as it refines flat areas too many times, allocates too much, etc. But does allow a more flexible specification of the region to integrate over, as `hcubature` requires box-like regions.

```
f(x,y,z) = x * y^2 * z^3
fubini(f, (0,1), (0,2), (0,3))  # int_0^3 int_0^2 int_0^1 f(x,y,z) dz dy dx
g(v) = f(v...)
hcubature(g, (0,0,0), (3,2,1))  # same. Not order switched

# triangular like region
fubini(f, (0, y->y), (0, x->x), (0,3))
```
"""
fubini(f, dx)     = quadgk(FWrapper(f), dx...)[1]
fubini(f, ys, xs) = fubini(x -> fubini(y -> f(x,y), endpoints(ys, x)), xs)
fubini(f, zs, ys, xs) = fubini(x ->
    fubini(y ->
        fubini(z -> f(x,y,z),
            endpoints(zs, (x,y))),
        endpoints(ys,x)),
    xs)

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

uvec(x) = x / norm(x)

"""
    `unzip(vs)`
    `unzip(v1, v2, ...)`
    `unzip(r::Function, a, b)`
    `unzip(r::Function, a, b, n)`

Take a vector of points described by vectors (as returned by, say
`r(t)=[sin(t),cos(t)], r.([1,2,3])`, and return a tuple of collected x
values, y values, and optionally z values.

If the argument is specified as a comma separated collection of vectors, then these are combined and passed along.

If the argument is a function and two end point, then the  points are chosen by `PlotUtils.adaptedgrid`.

If the argument is a function, two endpoints and a number of points, then  `n` points are chosen evenly spaced over `[a,b]`.

This is useful for plotting when the data is more conveniently
represented in terms of vectors, but the plotting interface requires the x and y values collected.

Examples:
```
using SimplePlots
r(t) = [sin(t), cos(t)]
rp(t) = [cos(t), -sin(t)]
plot(unzip(r, 0, 2pi)...)  # calls plot(xs, ys)

t0, t1 = pi/6, pi/4

p, v = r(t0), rp(t0)
plot!(unzip(p, p+v)...)  # connect p to p+v with line

p, v = r(t1), rp(t1)
quiver!(unzip([p])..., quiver=unzip([v]))
```

Based on `unzip` from the `Plots` package.
"""
unzip(vs) = Tuple([vs[j][i] for j in eachindex(vs)] for i in eachindex(vs[1]))
unzip(v,vs...) = unzip([v, vs...])
unzip(r::Function, a, b, n) = unzip(r.(range(a, stop=b, length=n)))
# return (xs, f.(xs)) or (f₁(xs), f₂(xs), ...)
function unzip(f::Function, a, b)
    n = length(f(a))
    if n == 1
        return PlotUtils.adapted_grid(f, (a,b))
    else
        xsys = [PlotUtils.adapted_grid(x->f(x)[i], (a,b)) for i ∈ 1:n]
        xs = sort(vcat([xsys[i][1] for i ∈ 1:n]...))
        return unzip(f.(xs))
    end
end

# unzip(a) = map(x -> getfield.(a, x), fieldnames(eltype(a)))

# alternate, should deprecate
xs_ys(vs) = (A=hcat(vs...); Tuple([A[i,:] for i in eachindex(vs[1])]))
xs_ys(v,vs...) = xs_ys([v, vs...])
xs_ys(r::Function, a, b, n=100) = xs_ys(r.(range(a, stop=b, length=n)))

"""
    parametric_grid(us, vs, r)

Create matrices for `xs`, `ys`, `zs` from `r(u,v) = [x(u,v), y(u,v), z(u,v)]`

Used to plot parametrically defined surfaces.
"""
function parametric_grid(us, vs, r)
    unzip(r.(us, vs'))
end

# for plotif. This identifies a vector of colors
function identify_colors(g, xs, colors=(:red, :blue, :black))
    F = (a,b) -> begin
        ga,gb=g(a),g(b)
        ga * gb < 0 && return nothing
        ga >= 0 && return true
        return false
    end
    find_colors(F, xs, colors)
end

# F(a,b) returns true, false, or nothing
function find_colors(F, xs, colors=(:red, :blue, :black))
    n = length(xs)
    cols = repeat([colors[1]], n)
    for i in 1:n-1
        a,b = xs[i], xs[i+1]
        val = F(a,b)
        if val == nothing
            cols[i] = colors[3]
        elseif val
            cols[i] = colors[1]
        else
            cols[i] = colors[2]
        end
    end
    cols[end] = cols[end-1]
    cols
end

end


include("distribution.jl")
include("const.jl")

export entropy

function entropy_ML(data::Vector{Int64}, base::Number)
  p = fe1p(data)
  return -sum([ p[x] > 0 ? (p[x] * log(base, p[x])) : 0 for x=1:size(p)[1]])
end

# implemented from [1] (see below)
function entropy_MLBC(data::Vector{Int64}, base::Number)
  p = fe1p(data)
  n = float(size(data)[1])
  S = float(size(p)[1])
  H = -sum([ p[x] > ϵ ? (p[x] * log(base, p[x])) : 0 for x=1:size(p)[1]])
  return H + (S-1) / (2.0 * n)
end

# implemented from [1] (see below)
function entropy_HT(data::Vector{Int64}, base::Number)
  p = fe1p(data)
  n = size(data)[1]
  return -sum([ p[x] > ϵ ? ((p[x] * log(base, p[x])) / (1.0 - ((1.0 - p[x])^n))) : 0 for x=1:size(p)[1]])
end

# implemented from [1] (see below)
function entropy_CS(data::Vector{Int64}, base::Number)
  m = maximum(v)
  n  = size(v)[1]
  c  = counts(v, 1:m)
  c = c ./ n
  # just to get rid of the numerical inaccuracies and make sure its a probability distribution
  s = sum(c)
  p = c ./ s
  C = 1.0 - float(sum(filter(x == 1, c))) / float(n)
  p = p .* C
  return -sum([ p[x] > ϵ ? ((p[x] * log(base, p[x])) / (1.0 - ((1.0 - p[x])^l))) : 0 for x=1:size(p)[1]])
end

function entropy(data::Vector{Int64}; base=2, mode="ML")
  modes  = ["ML", "Maximum Likelihood",
            "MLBC", "Maximum Likelihood with Bias Correction",
            "Horovitz-Thompson", "HT",
            "ChaoShen", "Chao-Shen", "CS"]
  umodes = map(x->uppercase(x), modes)
  known_mode = uppercase(mode) in umodes
  pmodes = map(x->", $x",modes)
  pmodes = foldl(*, pmodes[1][3:end], pmodes[2:end])
  @assert known_mode "Mode may be any of the following: [$pmodes]."

  if uppercase(mode) in umodes[1:2]
    return entropy_ML(data, base)
  elseif uppercase(mode) in umodes[3:4]
    return entropy_MLBC(data, base)
  elseif uppercase(mode) in umodes[5:6]
    return entropy_HT(data, base)
  elseif uppercase(mode) in umodes[7:9]
    return entropy_CS(data, base)
  end
  return nothing
end

# [1] A. Chao and T.-J. Shen. Nonparametric estimation of shannon’s index of diversity when there are unseen species in sample. Environmental and Ecological Statistics, 10(4):429–443, 2003.


# ------------------------------------------------------------------------------
# Geometric objects
# -------------------------------------------------------------------------------

"""
    squaredist(A, B)

Compute the square of the euclidean distance between `A` and `B`.

# Examples
```jldoctest
julia> squaredist(Point(0, 0), Point(2,2))
8
```
"""
function squaredist(A, B)
    d = (A.x-B.x)^2+(A.y-B.y)^2
    simplify(Sym(d))
end

"""
    distance(A, B)

Compute the euclidean distance between `A` and `B`, which is the "ordinary" straight-line distance
between two points.

# Examples
```jldoctest
julia> distance(Point(0, 0), Point(3,4))
5
```
"""
function distance(A, B)
    SymPy.sqrt(squaredist(A, B))
end

"""
    midpoint(A, B)

Find the midpoint between `A` and `B`.

# Examples

```jldoctest
julia> midpoint(Point(0, 0), Point(2,4))
Point(1, 2)
```
"""
function midpoint(A, B)
    x1 = (A.x + B.x)/2
    y1 = (A.y + B.y)/2
    Point(x1, y1)
end

"""
    concurrent(edgelist::Vector{Edge})

Find the point where all edges (lines) in `edgelist` intersect if such point exists. Return nothing otherwise.

# Examples

```jldoctest
julia> concurrent([Edge(Point(0,1), Point(1,1)), Edge(Point(0,1), Point(1,0))])
Point(0, 1)
```
"""
function concurrent(edgelist::Vector{Edge})
    enum = length(edgelist)
    if enum == 1
        throw(ArgumentError("At least two edges are needed. $enum is given."))
    end

    @vars x y

    eqs = Sym[]
    for e in edgelist
        # This should be 0
        eq = (x-e.src.x)*(y-e.dst.y) - (x-e.dst.x)*(y-e.src.y)
        push!(eqs, eq)
    end

    sol = solve(eqs, [x,y])

    if length(sol) == 0
        return nothing
    else
        return Point(simplify(sol[x]), simplify(sol[y]))
    end
end

"""
    concurrent(e1::Edge, e2::Edge)

Find the point the edges `e1` and `e2` intersect if such point exists. Return nothing otherwise.

# Examples

```jldoctest
julia> concurrent(Edge(Point(0,1), Point(1,1)), Edge(Point(0,1), Point(1,0)))
Point(0, 1)
```
"""
function concurrent(e1::Edge, e2::Edge)
    return concurrent([e1, e2])
end


module Generate

# using PyCall

# const map   = pyimport("mpl_toolkits.basemap")
# const Globe = map.Basemap()

"""
    sphere(N; R=1)

Generate a spherical point cloud of `N` points with extent of radius `R`.
"""
function sphere(N; R=1)
    θ = π  .* rand(Float64, N)
    ϕ = 2π .* rand(Float64, N)

    return R.*hcat(sin.(θ).*cos.(ϕ), sin.(θ).*sin.(ϕ), cos.(θ))', hcat(θ, ϕ)'
end

function spherical_distance(x; R=1)
    n̂ = x / R
    D = zeros(size(x,2),size(x,2))

    norm² = sum(n̂[i,:].^2 for i ∈ 1:size(x,1))
    cosΔψ = sum(n̂[i,:]' .* n̂[i,:] for i ∈ 1:size(x,1))
    sinΔψ = [norm(cross(n̂[:,i], n̂[:,i])) for i ∈ 1:size(x,2), j ∈ 1:size(x,2)]

    return R*atan.(sinΔψ, cosΔψ)
end

#=
"""
    globe(N)

Generate a spherical point cloud of `N` points where all points are guaranteed to be sampled from land-masses from Earth.
Uses mpl.basemap Python library.
"""
function globe(N)
    Θ = Array{Float64}(undef, N)
    Φ = Array{Float64}(undef, N)
    for n in 1:N
    @label sample
        θ = π*rand(1)[1]
        ϕ = 2π*rand(1)[1]

        x, y = Globe(180*(ϕ.-π)/π, 180*(π/2 .- θ)/π)

        !Globe.is_land(x,y) && @goto sample

        Θ[n] = θ
        Φ[n] = ϕ
    end

    return hcat(sin.(Θ).*cos.(Φ), sin.(Θ).*sin.(Φ), cos.(Θ))', hcat(Θ, Φ)'
end
=#

"""
    swissroll(N; z₀=10, R=1/20)

Generate a point cloud of `N` distributed on a swiss roll manifold with unit radius and length ``\frac{z₀}{R}``.
"""
function swissroll(N; z₀=10, R=1/20)
    z = (z₀/R)*rand(Float64, N)
    ϕ = 1.5π .+ 3π .* rand(Float64, N)

    return hcat(ϕ .* cos.(ϕ), ϕ .* sin.(ϕ), z)' .* R, hcat(ϕ, z)'
end

"""
    torus(N; R=2, r=1)

Generate a point cloud of `N` distributed on a torus, sized inner `r` and outer radius `R` respectively.
"""
function torus(N; R=2, r=1)
    θ = 2π  .* rand(Float64, N)
    ϕ = 2π .* rand(Float64, N)

    return hcat((R .+ r*cos.(θ)) .* cos.(ϕ), (R .+ r*cos.(θ)).* sin.(ϕ), r*sin.(θ))', hcat(ϕ, θ)'
end

end


export DirectedPath, DirectedCycle, DirectedComplete
export RandomDigraph, RandomTournament, TorusDigraph

"""
`DirectedPath(n)` creates a directed cycles with vertices `1:n`.
"""
function DirectedPath(n::Int)
    if n < 1
        error("n must be positive")
    end
    G = IntDigraph(n)
    for u=1:(n-1)
        add!(G,u,u+1)
    end
    return G
end

"""
`DirectedCycle(n)` creates a directed cycles with vertices `1:n`.
"""
function DirectedCycle(n::Int)
    G = DirectedPath(n)
    add!(G,n,1)
    return G
end

# Create a complete digraph (all possible edges)
"""
`DirectedComplete(n)` creates a directed complete graph with
all possible edges (including a loop at each vertex). Use
`DirectedComplete(n,false)` to supress the creation of loops.
"""
function DirectedComplete(n::Int, with_loops::Bool=true)
    G = IntDigraph(n)
    if !with_loops
        forbid_loops!(G)
    end
    for u=1:n
        for v=1:n
            add!(G,u,v)
        end
    end
    return G
end

# Create a random digraph (Erdos-Renyi style)
"""
`RandomDigraph(n,p)` creates an Erdos-Renyi style random directed
graph with vertices `1:n` and edge probability `p` (equal to 0.5 by
default). The possible edges `(u,v)` and `(v,u)` are independent. No
loops are created. To also create loops (each with probability `p`)
use `RandomDigraph(n,p,true)`.
"""
function RandomDigraph(n::Int, p::Real=0.5, with_loops=false)
    G = IntDigraph(n)
    if !with_loops
        forbid_loops!(G)
    end
    for u=1:n
        for v=1:n
            if rand() < p
                add!(G,u,v)
            end
        end
    end
    return G
end

# Create a random tournament (no loops!)

"""
`RandomTournament(n)` creates a random tournament with vertex set
`1:n`.  This is equivalent to randomly assigning a direction to every
edge of a simple complete graph.
"""
function RandomTournament(n::Int)
    G = IntDigraph()
    for u=1:n-1
        for v=u+1:n
            if rand() < 0.5
                add!(G,u,v)
            else
                add!(G,v,u)
            end
        end
    end
    return G
end

"""
`all_tuples(alphabet, n)` creates an iterator that produces all
length-`n` tuples of distinct elements in `alphabet`.
"""
function all_tuples(alphabet, n::Int)
    elts = collect(distinct(alphabet))
    src  = [ elts for _=1:n ]
    its  = Base.Iterators.product(src...)
    return its
end

export ShiftDigraph

"""
`ShiftDigraph(alphabet,n)` creates a `SimpleDigraph` whose vertices
are all length-`n` tuples of the elements in `alphabet` (which can be
an array such as `[0,1]` or a string such as `"abc"`). An edge from
`u` to `v` corresponds to an element dropped from the first position
in `u` and another element added to the end yielding `v`. For example,
in `ShiftDigraph([0,1],5)` there are two edges leaving vertex
`(0,1,0,1,1)`; one goes to `(1,0,1,1,0)` and the other to
`(1,0,1,1,1)`.
"""
function ShiftDigraph(alphabet=[0,1], n::Int=3)
    elts = collect(distinct(alphabet))
    vertex_iter = all_tuples(alphabet, n)
    vlist = collect(vertex_iter)
    T = typeof(vlist[1])
    G = SimpleDigraph{T}()
    for v in vlist
        add!(G,v)
    end

    # create edges here

    for v in vlist
        head = collect(IterTools.drop(v,1))
        for c in elts
            w = tuple([head;c]...)
            add!(G,v,w)
        end
    end

    return G

end

"""
function for creating a Torus Graph
"""
function TorusDigraph(n::Int=4, m::Int=3)

  G = SimpleDigraph();

  #create vertices
  vlist = Tuple{Int,Int}[]
  for i = 1:m
    for j = 1:n
      push!(vlist,(i,j))
    end
  end

  #create edges
  for v in vlist
    if v[1] + 1 <= m
      w = (v[1]+1,v[2])
      add!(G,v,w)
    end
    if v[2] + 1 <= n
      w = (v[1],v[2]+1)
      add!(G,v,w)
    end
    if v[1] == m
      w = (1,v[2])
      add!(G,v,w)
    end
    if v[2] == n
      w = (v[1],1)
      add!(G,v,w)
    end
  end

  return G
end


#=
# Example: Plotting velocity profiles
In this notebook, we will discuss a useful way to visualize a flow in fluid
mechanics: the **velocity profile**.
=#
#-

# ### Set up the module
using ThermofluidQuantities
#-
using Plots

#=
To start, let's consider one of the most basic types of flow: the linear shear flow,
often called **Couette flow**. This type of flow is generated, for example, between
two parallel walls when one wall is moving and the other is stationary. In the figure
below, the top wall is moving with velocity $U$, the lower wall is stationary:

<img src="https://raw.githubusercontent.com/UCLAMAEThreads/MAE103/main/notebook/Couette.svg" alt="velocity profile" width="300" align="center"/>

Because of the **no-slip condition**, the fluid next to each wall moves with it. The
fluid next to the upper wall moves at velocity $U$, the fluid next to the lower wall
is at rest.

The velocity profile depicts $u(y)$, the horizontal component of velocity as a
function of vertical position, $y$. This increases linearly from the lower to the
upper wall. In fact, the function is just

$$u(y) = Uy/H$$

The arrows indicate the direction that the fluid is moving, and the lengths of
the arrows indicate the relative speed at that $y$ position.
=#
#-
#=
### Plotting velocity profiles
The arrows are helpful, but you can also plot a velocity profile without them.
For example, consider the following velocity:

$$u(y) = \frac{4U_c}{H^2} y (H - y)$$

The coefficient $U_c$ is a speed, and $H$ is the gap height.
=#
#=
Let's define a function that evaluates this velocity. Here, `y`, `Uc`, and `H`
are to be given as arguments to the function.
=#
u(y,Uc,H) = 4*Uc/H^2*y*(H-y)

#=
Suppose the gap height $H$ is 1 cm and the speed $U_c$ is 1 m/s. We will evaluate
this velocity at a range of locations between 0 and $H$:
=#
H = 1u"cm"  # 1 cm = 0.01 m
Uc = 1u"m/s"
y = range(0u"cm",H,length=101) # 101 points to evaluate at, just to make it look smooth.

#=
Now we evaluate the velocity function at the range of $y$ locations. (Remember that
the `.` vectorizes the evaluation of a function.)
=#
v = Velocity.(u.(y,Uc,H))

#=
Notice that $u$ is 0 at the beginning and end of the range. Let's plot it. But
let's plot it as a velocity profile, which means we make $u$ the 'x' axis and $y$
the 'y' axis.
=#

plot(v,y,xlim=(0u"m/s",2Uc),ylim=(0u"cm",H),
    legend=false,xlabel="u(y)",ylabel="y")
#=
The top and bottom of this plot suggest that these are stationary walls where the
flow is at rest. In fact, this is the velocity profile associated with pressure-driven
flow through the gap.
=#


using Turing, MCMCChains, Distributions, StatsPlots, CSV

@model die_model(observations) = begin
    # Set prior distribution for p (probability of rolling 1)
    p ~ Uniform(0,1)#Beta(5,1)

    # Probability of rolling one of 2 - 6
    q = (1-p)/5

    # Define how samples are obtained
    for i=1:length(observations)
        observations[i] ~ Categorical([p, q, q, q, q, q])
    end

    return p
end

# Define the true probabilities
true_p = 0.25
true_q = (1-true_p)/5

## Simulate observed data
using DataFrames
using CSV
data = rand(Categorical([true_p, true_q, true_q, true_q, true_q, true_q]), 100)
df = DataFrame(X = data)
CSV.write("pp_ex_1_data.csv", df)

# Load observed data
data = CSV.read("pp_ex_1_data.csv")[:,1]

# Use HMC sampler to obtain posterior samples
num_posterior_samples = 1000
ϵ = 0.05 # Leapfrog step size
τ = 10   # Number of leapfrog iterations

# Obtain posterior samples
chain = sample(die_model(data), HMC(ϵ, τ, :p), num_posterior_samples, progress=false)

# Extract summary of p parameter and plot histogram
p_summary = chain[:p]
plot(p_summary, seriestype = :histogram, bg =  RGB(247/255, 236/255, 226/255))

savefig("../images/pp_ex_1_hist.svg")

# Extract 95% interval
println(quantile(Array(p_summary), [0.025, 0.975]))


#Tutorial 2
#=
Daniel
=#
# ## is a break on the code

# control+c to stop the process
my_integer=5
my_float=5.
my_range=1:7
my_vector=[1, 3, 5]
my_string="hello"
my_character='o'
my_boolean=true

typeof(my_integer)
typeof(my_float)

typeof(
    my_character
)

# use \ to write simbols like alpha
α=5;
#show the command
@show α + my_integer
print("α = $α \n", "My integer is: $(my_integer)")

#println automatically saparated linea

#column vector
A = [1, 3, 5]
#row vector
A = [1 2 3]

ones(3)
zeros(5)
C=[1 3; 5 "test"]
C=[1 3; 5 9]

A = 1:9
# number with a distance o 0.5
B= 1:0.5:9
# secon element of break
B[3]

typeof(C)
eltype(C)
size(C)
length(C)
ndims(C)

#indexing
C[end]
C[1,2]
@show C
@show C[2,:] #show select row*

using LinearAlgebra

C*I
C^2
C.^2 #by element operation

sin.([1 2 3])

inv(C)
inv(C)*C # too costly
C\C # this notation is better

# to installa a package type ], add pkg name

using Random
rand(1)
rand(3)
randn(3)

Random.seed!(1234) #name of the seed
rand(1)
rand(1)

Random.seed!(1234)
rand(1)

#if/else statement
a=3
if a==3
    print("a is 3")
end

a=true
if a
    print("a is true")
end

a ? print("a is true") : nothing

if a
    print("a is true")
else
    print("a is no true")
end

b=true

if a | b
    print("a or b is true (or both)")
end

if a & b  # double & makes faster the process, just evaluate the firs statement.
    print("a and b are true")
end

#for loops
vals = ["a" "b" "c"]

for val in vals
    println(val)
end

#global
a = 3
for val in vals
    a = a + 1
    return a
end

for i in eachindex(vals)
    println(vals[i])
end

# index and value
for (index, value) in enumerate(vals)
    println("Element $index of vals is $value")
end

# Functions
function cobbdouglas(l, k, α, β = 1 - α; A=1)
    y=A*l^α*k^β
    return y
end

cobbdouglas(1, 3, 0.5)

@show output = cobbdouglas(1, 3, 0.5)

cobbdouglas.([1 2], 3, 0.5)

cobbdouglas(l, k, α) = A*l^α*k^(1-α)
cobbdouglas(1, 3, 0.5)


"Square the sum of the first `n` positive integers"
function square_of_sum(n)
    sum(1:n)^2
end

"Sum the squares of the first `n` positive integers"
function sum_of_squares(n)
    sum((1:n).^2)
end

"Subtract the sum of squares from square of the sum of the first `n` positive ints"
function difference(n)
    square_of_sum(n) - sum_of_squares(n)
end


"""
    LinearKernel(; c = 0.0)

The linear kernel is a Mercer kernel given by
```
    κ(x,y) = xᵀy + c
```
Where `c` is a real number
"""
struct LinearKernel{Tc<:Real} <: SimpleKernel
    c::Vector{Tc}
    function LinearKernel(;c::T=0.0) where {T}
        new{T}([c])
    end
end

kappa(κ::LinearKernel, xᵀy::Real) = xᵀy + first(κ.c)

metric(::LinearKernel) = DotProduct()

Base.show(io::IO, κ::LinearKernel) = print(io, "Linear Kernel (c = ", first(κ.c), ")")

"""
    PolynomialKernel(; d = 2.0, c = 0.0)

The polynomial kernel is a Mercer kernel given by
```
    κ(x,y) = (xᵀy + c)^d
```
Where `c` is a real number, and `d` is a shape parameter bigger than 1. For `d = 1` see [`LinearKernel`](@ref)
"""
struct PolynomialKernel{Td<:Real, Tc<:Real} <: SimpleKernel
    d::Vector{Td}
    c::Vector{Tc}
    function PolynomialKernel(; d::Td=2.0, c::Tc=0.0) where {Td<:Real, Tc<:Real}
        @check_args(PolynomialKernel, d, d >= one(Td), "d >= 1")
        return new{Td, Tc}([d], [c])
    end
end

kappa(κ::PolynomialKernel, xᵀy::Real) = (xᵀy + first(κ.c))^(first(κ.d))

metric(::PolynomialKernel) = DotProduct()

Base.show(io::IO, κ::PolynomialKernel) = print(io, "Polynomial Kernel (c = ", first(κ.c), ", d = ", first(κ.d), ")")


"""
    romberg(f, a, b; maxi = 10, delta = 10^-9)
berisi program untuk mencari integral numerik dari fungsi `f(x)` pada
interval `[a,b]`. Program ini secara default berisi 3 masukan, yaitu fungsi `f(x)`, dan titik
ujung interval `[a,b]`. Secara default memiliki nilai maksimum iterasi `maxi=10` dan
toleransi error sebesar `delta = 1e-9`
# Examples
```jl
julia> f(x) = (x^2+x+1)*cos(x)
f (generic function with 1 method)

julia> sol, flag, err, R = romberg(f, 0, pi/2);

julia> sol
2.0381974270672245

julia> flag
0

julia> err
1.213065203842234e-10

julia> R
6×6 Array{Float64,2}:
 0.785398  0.0      0.0     0.0     0.0     0.0
 1.72681   2.04062  0.0     0.0     0.0     0.0
 1.96053   2.03844  2.0383  0.0     0.0     0.0
 2.01879   2.03821  2.0382  2.0382  0.0     0.0
 2.03335   2.0382   2.0382  2.0382  2.0382  0.0
 2.03698   2.0382   2.0382  2.0382  2.0382  2.0382

 julia> sol, flag, err, R = romberg(f, 0, pi/2, maxi=20, delta=1e-14);

 julia> sol
 2.038197427067236

 julia> flag
 0

 julia> err
 0.0

 julia> R
 8×8 Array{Float64,2}:
  0.785398  0.0      0.0     0.0     0.0     0.0     0.0     0.0
  1.72681   2.04062  0.0     0.0     0.0     0.0     0.0     0.0
  1.96053   2.03844  2.0383  0.0     0.0     0.0     0.0     0.0
  2.01879   2.03821  2.0382  2.0382  0.0     0.0     0.0     0.0
  2.03335   2.0382   2.0382  2.0382  2.0382  0.0     0.0     0.0
  2.03698   2.0382   2.0382  2.0382  2.0382  2.0382  0.0     0.0
  2.03789   2.0382   2.0382  2.0382  2.0382  2.0382  2.0382  0.0
  2.03812   2.0382   2.0382  2.0382  2.0382  2.0382  2.0382  2.0382
```
return nilai solusi integral numerik `sol`, status solusi
`flag`, hampiran galat `err`, dan matriks `R` yaitu matriks iterasi Romberg.
"""
function romberg(f, a, b; maxi = 10, delta = 10^-9)
  flag = 1;
  M = 1;
  h = b-a;
  err = 1;
  R = h/2*(f(a)+f(b));
  for J = 1:maxi
    # Rekursif Trapesium
    h = h/2;
    M = 2*M;
    s = 0;
    for p = 1:M/2
      x = a+h*(2*p-1);
      s = s+f(x);
    end
    R = [R zeros(size(R,1),1) ; R[J,1]/2 + h*s zeros(1,size(R,1))]
    # Aturan Romberg
    for K=1:J
      R[J+1,K+1]=R[J+1,K]+(R[J+1,K]-R[J,K])/(4^K-1);
    end
    err = abs.(R[J,J]-R[J+1,J+1]);
    if err<delta; flag=0; break; end
  end
  sol = R[end,end];
  return sol, flag, err, R
end


#### Cauchy matrix

export Cauchy
"""
[`Cauchy` matrix](http://en.wikipedia.org/wiki/Cauchy_matrix)
```julia
Cauchy(x,y)[i,j]=1/(x[i]+y[j])
Cauchy(x)=Cauchy(x,x)
cauchy(k::Number)=Cauchy(collect(1:k))

julia> Cauchy([1,2,3],[3,4,5])
3x3 Cauchy{Int64}:
 0.25      0.2       0.166667
 0.2       0.166667  0.142857
 0.166667  0.142857  0.125

julia> Cauchy([1,2,3])
3x3 Cauchy{Int64}:
 0.5       0.333333  0.25
 0.333333  0.25      0.2
 0.25      0.2       0.166667

julia> Cauchy(pi)
3x3 Cauchy{Float64}:
 0.5       0.333333  0.25
 0.333333  0.25      0.2
 0.25      0.2       0.166667
```
"""
struct Cauchy{T} <: AbstractMatrix{T}
    x::Vector{T} #
    y::Vector{T} #
end # immutable

function Cauchy(k::Number)
         Cauchy(collect(1:k),collect(1:k))
end

function Cauchy(x::Vector)
         Cauchy(x,x)
end

# Define its size

size(A::Cauchy, dim::Integer) = dim==1 ? length(A.x) : dim==2 ? length(A.y) : throw(ArgumentError("Invalid dimension $dim"))
size(A::Cauchy)= length(A.x), length(A.y)

# Index into a Cauchy
function getindex(A::Cauchy,i::Integer,j::Integer)
    return 1.0/(A.x[i]+A.y[j])
end # getindex

# Dense version of Cauchy
Matrix(A::Cauchy) = [A[i,j] for i=1:size(A,1), j=1:size(A,2)]


# # SVM with L^1 regularization
## Generate data for SVM classifier with L1 regularization.
using Random
Random.seed!(3);
n = 20;
m = 1000;
TEST = m;
DENSITY = 0.2;
beta_true = randn(n,1);
idxs = randperm(n)[1:round(Int, (1-DENSITY)*n)];
beta_true[idxs] .= 0
offset = 0;
sigma = 45;
X = 5 * randn(m, n);
Y = sign.(X * beta_true .+ offset .+ sigma * randn(m,1));
X_test = 5 * randn(TEST, n);

#-

## Form SVM with L1 regularization problem.
using Convex, SCS, ECOS

beta = Variable(n);
v = Variable();
loss = sum(pos(1 - Y .* (X*beta - v)));
reg = norm(beta, 1);

## Compute a trade-off curve and record train and test error.
TRIALS = 100
train_error = zeros(TRIALS);
test_error = zeros(TRIALS);
lambda_vals = exp10.(range(-2, stop=0, length=TRIALS);)
beta_vals = zeros(length(beta), TRIALS);
for i = 1:TRIALS
    lambda = lambda_vals[i];
    problem = minimize(loss/m + lambda*reg);
    solve!(problem, ECOS.Optimizer(verbose=0));
    ## solve!(problem, SCS.Optimizer(verbose=0,linear_solver=SCS.Direct, eps=1e-3))
    train_error[i] = sum(float(sign.(X*beta_true .+ offset) .!= sign.(evaluate(X*beta - v))))/m;
    test_error[i] = sum(float(sign.(X_test*beta_true .+ offset) .!= sign.(evaluate(X_test*beta - v))))/TEST;
    beta_vals[:, i] =  evaluate(beta);
end

#-

# Plot the train and test error over the trade-off curve.
using Plots
plot(lambda_vals, train_error, label="Train error");
plot!(lambda_vals, test_error, label="Test error");
plot!(xscale=:log, yscale=:log, ylabel="errors", xlabel="lambda")

#-

# Plot the regularization path for beta.

plot()
for i = 1:n
    plot!(lambda_vals, vec(beta_vals[i,:]), label="beta$i")
end
plot!(xscale=:log, ylabel="betas", xlabel="lambda")


# Basic control flow
#
# # Fibonnacci numbers
#
# The Fibonnacci sequence is: 1, 1, 2, 3, 5, 8, 13..., where each subsequent
# number is the sum of the prior two numbers. Create a function that computes
# the N-th Fibonnacci number.
#   Options:
#       * Iterative: use a for loop and keep track of prior two values to add them
#           What happens if you ask for the 95th number? The 96th? 97th?
#       * Recursive: work backwards and add the prior two numbers together
#           How long does it take to get the 40th number? The 41st? 42nd?
#       * Matrix magic

# # Monte-carlo pi
#
# Create a function that estimates pi by repeatedly throwing a dart at a square
# board. It's easy to test if the particular dart landed inside the circle —
# just ask if x^2 + y^2 < 1! Then comparing the number inside vs. the total
# gives you the ratio of the circle's area to the square's area.

using Images
load(download("http://corysimon.github.io/images/julia/myplot.png"))

# Plot the error as the number of iterations increases

# # Packages
#
# Add the Primes package. What does it do? How do you compute the 100th prime?
# A very large prime number?
#
#

Courses/Introduction to Julia/Day One Exercises.jl


"""
    area(poly)

Calculate the signed area via the shoelace formula. If the points are ordered
counterclockwise, the result will be positive. If the points are ordered clockwise,
the result will be negative.
"""
function area(poly::T) where T
    a = zero(eltype(eltype(T)))
    @inbounds for i in 1:length(poly)-1
        p1 = poly[i]
        p2 = poly[i+1]
        a += p1[1]*p2[2]-p2[1]*p1[2]
    end
    return a/2
end

function centroid(poly::T) where T
    ZT = zero(eltype(eltype(T)))
    a = ZT
    c = zero(eltype(poly))

    @inbounds for i in 1:length(poly)-1
        p = poly[i]
        n = poly[i+1]
        k = p[1]*n[2]-n[1]*p[2]
        a += k
        c = c .+ (p.+n).*k
    end
    a /= 2
    return c./(6*a)
end


"""
    make_twospirals(;    n_samples::Int = 2000,
                             start_degrees::Int = 90,
                             total_degrees::Int = 570,
                             noise::Float64 = 0.2
Generate two spirals dataset. Return a Nx3 matrix, where each line contains the X,Y coordinates and the class of an instance.
# Arguments
- `n_samples::Int = 2000`: The total number of points generated.
- `start_degrees::Int = 90`: Determines how far from the origin the spirals start.
- `total_degrees::Int = 570`: Controls the lenght of the spirals.
- `noise::Float64 = 0.2`: Determines the noise in the dataset.
Reference: [link](https://la.mathworks.com/matlabcentral/fileexchange/41459-6-functions-for-generating-artificial-datasets)
"""
function make_twospirals(; n_samples::Int = 2000,
                               start_degrees::Int = 90,
                               total_degrees::Int = 570,
                               noise::Float64 = 0.2)
   start_degrees = deg2rad(start_degrees);

   N1 = floor(Int, n_samples / 2);
   N2 = n_samples - N1;

   n = start_degrees .+ sqrt.(rand(N1,1)) .* deg2rad(total_degrees);
   d1 = [-cos.(n).*n + rand(N1,1).*noise sin.(n).*n+rand(N1,1).*noise];

   n = start_degrees .+ sqrt.(rand(N2,1)) .* deg2rad(total_degrees);
   d2 = [cos.(n).*n+rand(N2,1)*noise -sin.(n).*n+rand(N2,1)*noise];

   features = [d1; d2]
   labels = [zeros(Int, N1); ones(Int, N2)]

   return convert(features, labels);
end

"""
      make_halfkernel(; n_samples::Int = 1000,
                               minx::Int = -20,
                               r1::Int = 20,
                               r2::Int = 35,
                               noise::Float64 = 4.0,
                               ratio::Float64 = 0.6)
Generates two half ellipses, one inside the other
# Arguments
- `n_samples::Int = 1000`: The total number of points generated
- `r1::Int = 20`:
- `r2::Int = 35`:
- `minx::Int = -20`:
- `noise::Float64 = 0.2`: Determines the noise in the dataset.
- `ratio::Float64 = 0.6)`:
Reference: [link](https://la.mathworks.com/matlabcentral/fileexchange/41459-6-functions-for-generating-artificial-datasets)
"""
function make_halfkernel(; n_samples::Int = 1000,
                               minx::Int = -20,
                               r1::Int = 20,
                               r2::Int = 35,
                               noise::Float64 = 4.0,
                               ratio::Float64 = 0.6)

   N = floor(Int, n_samples / 2)

   phi1 = rand(N, 1) * pi
   inner = [minx .+ r1 .* sin.(phi1) .- .5 .* noise .+ noise .* rand(N, 1) r1 .* ratio .* cos.(phi1) .- .5 .* noise .+ noise .* rand(N,1)]
   l1 = ones(Int, N)

   phi2 = rand(N,1) * pi
   outer = [minx .+ r2 .* sin.(phi2) .- .5 .* noise  .+ noise .* rand(N,1) r2 .* ratio .* cos.(phi2) .- .5 .* noise  .+ noise .* rand(N,1)]
   l2 = zeros(Int, N)

   features = [inner; outer]
   labels = [l1; l2]

   return convert(features, labels)
end

"""
      make_outlier(;n_samples::Int = 600,
                                 r::Int = 20,
                                 dist::Int = 30,
                                 outliers::Float64 = 0.04,
                                 noise::Float64 = 5.0)
Generates outlier dataset.
# Arguments
- `n_samples::600 = 1000`: The total number of points generated.
- `r::Int = 20`: Radius of lateral blobs.
- `dist::Int = 30`: Determine the distance between the labels.
- `noise::Float64 = 5.0`: Determines the noise in the dataset.
Reference: [link](https://la.mathworks.com/matlabcentral/fileexchange/41459-6-functions-for-generating-artificial-datasets)
"""
function make_outlier(;n_samples::Int = 600,
                           r::Int = 20,
                           dist::Int = 30,
                           outliers::Float64 = 0.04,
                           noise::Float64 = 5.0)

   n1 = round(Int, (n_samples * (0.5 - outliers)) )
   n2 = n1
   n3 = round(Int, n_samples * outliers)
   n4 = n_samples - n1 - n2 - n3

   phi1 = rand(n1, 1) * pi
   r1 = sqrt.(rand(n1, 1)) * r
   p1 = [ -dist .+ (r1 .* sin.(phi1)) r1 .* cos.(phi1)]
   l1 = zeros(Int, n1)

   phi2 = rand(n2, 1) * pi
   r2 = sqrt.(rand(n2, 1)) * r
   p2 = [ dist .- r2.*sin.(phi2) r2.*cos.(phi2)]
   l2 = 3 * ones(Int, n2)

   p3 = [ rand(n3, 1) * noise   dist .+ rand(n3, 1) * noise ]
   l3 = 2 * ones(Int, n3)

   p4 = [ rand(n4, 1) * noise -dist .+ rand(n4, 1) * noise ]
   l4 = ones(Int, n4)

   features = [p1; p2; p3; p4]
   labels = [l1; l2; l3; l4]

   return convert(features, labels)
end


begin
# Interpolation Search

# Julia code to interpolation search x in arr[].
# If x is present then return its location,
# otherwise return -1
import Base:length,show,AbstractArray
arr = [10, 12, 13, 16, 18, 19, 20, 21,22, 23, 24, 33, 35, 42, 47]
x = 18
n = length(arr)
function interpolationsearch(arr::AbstractArray{},n,x)
# Find indexs of two corners
    lo = 1
    hi = n

    # Since array is sorted, an element present
    # in array must be in range defined by corner
    while lo <= hi && x >= arr[lo] && x <= arr[hi]
        if lo == hi
            if arr[lo] == x
                return lo;
			end
            return -1;
		end
        # Probing the position with keeping
        # uniform distribution in mind.
        pos  = lo + round(Int,(float(hi - lo)) / ( arr[hi] - arr[lo])) * ( x - arr[lo])

        # Condition of target found
        if arr[pos] == x
            return pos
		end
        # If x is larger, x is in upper part
        if arr[pos] < x
            lo = pos + 1;
   # If x is smaller, x is in lower part
        else
            hi = pos - 1;
		end
	end
    return -1
end
global result=-1

result=interpolationsearch(arr, n, x)

println("Result after function execution:",result)
if result == -1
    println("Element:",x," is not present in array",arr)
else
    println("Element:",x," is present at index:", result," in array",arr)
end
end


using DataFrames, GLM, Statistics, LinearAlgebra, CSV
data = CSV.read("../data/L1L2data.csv")
xVals, yVals = data[:,1], data[:,2]
n = length(xVals)
A = [ones(n) xVals]

# Approach A
xBar, yBar = mean(xVals),mean(yVals)
sXX, sXY = ones(n)'*(xVals.-xBar).^2 , dot(xVals.-xBar,yVals.-yBar)
b1A = sXY/sXX
b0A = yBar - b1A*xBar

# Approach B
b1B = cor(xVals,yVals)*(std(yVals)/std(xVals))
b0B = yBar - b1B*xBar

# Approach C
b0C, b1C = A'A \ A'yVals

# Approach D
Adag = inv(A'*A)*A'
b0D, b1D = Adag*yVals

# Approach E
b0E, b1E = pinv(A)*yVals

# Approach F
b0F, b1F = A\yVals

# Approach G
F = qr(A)
Q, R = F.Q, F.R
b0G, b1G = (inv(R)*Q')*yVals

# Approach H
F = svd(A)
V, Sp, Us = F.V, Diagonal(1 ./ F.S), F.U'
b0H, b1H = (V*Sp*Us)*yVals

# Approach I
eta, eps = 0.002, 10^-6.
b, bPrev = [0,0], [1,1]
while norm(bPrev-b) >= eps
    global bPrev = b
    global b = b - eta*2*A'*(A*b - yVals)
end
b0I, b1I = b[1], b[2]

# Approach J
modelJ = lm(@formula(Y ~ X), data)
b0J, b1J = coef(modelJ)

# Approach K
modelK = glm(@formula(Y ~ X), data, Normal())
b0K, b1K = coef(modelK)
println(round.([b0A,b0B,b0C,b0D,b0E,b0F,b0G,b0H,b0I,b0J,b0K],digits=3))
println(round.([b1A,b1B,b1C,b1D,b1E,b1F,b1G,b1H,b1I,b1J,b1K],digits=3))


"""
    laguerre(x, p::Integer)

Evaluate the Laguerre polynomial of degree `p` at `x` using the three term recursion.
"""
function laguerre(x, p::Integer)
    T = typeof( 1-x )
    p₀ = one(T)
    p₁ = 1-x

    if p <= 0
        return p₀
    elseif p == 1
        return p₁
    end

    for n in 2:p
        p₀, p₁ = p₁, ( (2n-1-x)*p₁ - (n-1)*p₀ ) / n
    end

    p₁
end

"""
    laguerre(x, p::Integer, α)

Evaluate the generalised Laguerre polynomial with parameter `α` of degree `p`
at `x` using the three term recursion.
"""
function laguerre(x, p::Integer, α)
    T = typeof( α+1-x )
    p₀ = one(T)
    p₁ = α+1-x

    if p <= 0
        return p₀
    elseif p == 1
        return p₁
    end

    for n in 2:p
        p₀, p₁ = p₁, ( (α+2n-1-x)*p₁ - (α+n-1)*p₀ ) / n
    end

    p₁
end


"""
Verlet integration is an integration method used to integrate newtons - law of motion. It is frequently used to find trajectories in molecular dynamics simulation.
The function takes `four` inputs viz,
- `f` : the differential equation
- `x0` : the initial condition. This is a Vector with the first element as initial value for position (x_0) and the second initial condition for velocity (v_0)
- `tspan`: is the time span for integration. It is a tuple (initial time, final time)
This functionr returns a tuple (x,t):
- `x` is the solution
- `t` is the array containing the time points
Reference:
- https://www.algorithm-archive.org/contents/verlet_integration/verlet_integration.html
Contributed by: [Ved Mahajan](https://github.com/Ved-Mahajan)
"""
function verlet_integration(f, x0, tspan, Δt = 1.0e-3)
    s, e = tspan
    N = Int(floor((e - s) / Δt))
    x = Vector{Float64}(undef, N)
    t = collect(s:Δt:(e-Δt))
    x[1] = x0[1]
    x[2] = x0[1] + x0[2] * Δt + 0.5 * f(x0[1]) * (Δt)^2

    for i in 2:(N-1)
        x[i+1] = 2 * x[i] - x[i-1] + f(x[i]) * (Δt)^2
    end
    return x, t
end


module Newtonsmethod

using ForwardDiff, LinearAlgebra

function newtonroot(f, f′; x₀, tolerance = 1E-7, maxiter = 1000)
    # setup the algorithm
    x_old = x₀
    normdiff = Inf
    iter = 1
    while normdiff > tolerance && iter <= maxiter
        x_new = x_old - f(x_old)/f′(x_old)
        normdiff = norm(x_new - x_old)
        x_old = x_new
        iter = iter + 1
    end
    if iter < maxiter
        return (value = x_old, normdiff = normdiff, iter = iter)
    else
        return nothing
    end
end

D(f) = x -> ForwardDiff.derivative(f, x)

newtonroot(f; x₀, tolerance = 1E-7, maxiter = 1000) = newtonroot(f, D(f); x₀ = x₀, tolerance=tolerance, maxiter=maxiter)

export newtonroot

end # module


# transition band is cosine. This allows satisfies the power complementary condition.
# cos( (yp-y)/(yp-ys) * π/2 ) for a falling edge. yp is pass band, ys is stop band.
function cosinetransitionlowpassfunction(y::T, yp::T, ys::T)::T where T
    abs_y = abs(y)

    if abs_y <= yp
        return one(T)
    elseif yp < abs_y <= ys
        #return cos( (yp-y)/(ys-yp)*π/2 )
        return cos( (yp-abs_y)/(ys-yp)*π/2 )
    end

    return zero(T)
end

function cosinetransitionhighpassfunction(x::T, xp::T, xs::T)::T where T
    abs_x = abs(x)

    if abs_x <= xs
        return zero(T)
    elseif xs < abs_x <= xp
        #return cos( (xp-x)/(xp-xs)*π/2 )
        return cos( (xp-abs_x)/(xp-xs)*π/2 )
    end

    return one(T)
end

# rs is rising edge's stopband.
# rp is rising edge's passband.
# fp is falling edge's passband.
# fs is falling edge's stopband.
function cosinetransitionbandpassfunction(y::T, rs, rp, fp, fs)::T where T
    abs_y = abs(y)

    # rising edge.
    if abs_y <= rs
        return zero(T)
    elseif rs < abs_y <= rp
        #return cos( (rp-y)/(rp-rs)*π/2 )
        return cos( (rp-abs_y)/(rp-rs)*π/2 )
    end

    # falling edge.
    if rp <= abs_y <= fp
        return one(T)
    elseif fp < abs_y <= fs
        #return cos( (fp-y)/(fs-fp)*π/2 )
        return cos( (fp-abs_y)/(fs-fp)*π/2 )
    end

    return zero(T)
end


#############################
# Example of Horner's Method and
# finding roots of polynomials
# Created for the YouTube channel
# https://www.youtube.com/OscarVeliz
# @author Oscar Veliz
#############################

# Long Evaluation
function longway(a, x)
	result = 0
	for i in 1:length(a)
		result += a[i] * x^(i-1)
	end
	return result
end

# Iterative Horner Evaluation
function horner(a, x)
	result = a[end]
	for i in length(a)-1:-1:1
		result = a[i] + x * result
	end
	return result
end

# Recursive Horner Evaluation
function rHorner(a, x, i)
	if i == length(a)
		return a[i]
	else
		return a[i] + x * rHorner(a,x,i+1)
	end
end

# Synthetic Division
# Given array of coefficients and value x return quotient
function quotient(a, x)
	q = zeros(length(a))
	q[end] = a[end]
	for i in length(a)-1:-1:1
		q[i] = x * q[i+1]+a[i]
	end
	return view(q, 2:length(q))#ignore remainder
end

# Differentiate using Power Rule
function ddx(a)
	d = zeros(length(a)-1)
	for i in 1:length(d)
		d[i] = a[i+1]*i
	end
	return d
end

# Find derivative at a point using Horner
function hornerDeriv(a, x)
	q = quotient(a, x)
	return horner(q,x)
end

# Newton's Method strictly on Polynomials using Horner
function newtonHorner(a, x)
	i = 0
	px = horner(a,x)
	# Power Rule
	#q = ddx(a)
	#qx = horner(q,x)
	# Ruffini's Rule
	qx = hornerDeriv(a,x)
	while abs(px) > 0.0000001 && i < 1000
		if qx == 0 # avoid divide by zero
			return NaN
		end
		x = x - px / qx
		px = horner(a,x)
		#qx = horner(q,x) # Power Rule
		qx = hornerDeriv(a,x) # Ruffini's Rule
		i = i + 1
	end
	if i == 1000 # hit the iteration cap (divierging or stuck in loop)
		return NaN
	end
	r = round(x) # check if rounding will give you a better answer
	if(abs(horner(a,r)) < abs(horner(a,x)))
		x = r
	end
	return x
end

# Assumes that there are no imaginary roots
function findRoots(a)
	roots = zeros(length(a)-1)
	for r in 1:length(roots)
		printPoly(a)
		root = newtonHorner(a,0) # initial guess of zero
		println("r",r," = ",root)
		roots[r] = root
		a = quotient(a,root) # deflation
	end
	return roots
end

# Print in polynomial form instead of array of coefficients
function printPoly(a)
	for i in length(a):-1:1
		# remove ".0" at end
		ai = string(a[i])
		if length(ai) >= 2 && ai[end] == '0' && ai[end-1] == '.'
			ai = ai[1:end-2]
		end
		if i == 1 # no x term
			println(ai)
		elseif i == 2 # no exponent
			print(ai,"x + ")
		else
			print(ai,"x^",(i-1)," + ")
		end
	end
end

p = [-2310, 727, 382, -72, -8, 1]
println(sort(findRoots(p)))


# # Portfolio Optimisation
#
# We consider a single-period Markowitz portfolio optimisation example.
# Assume that we have a portfolio with $n$ assets at the beginning of time period $t$. Given some forecasts on risks and expected returns we try to find the optimal trade vector that rebalances the portfolio to achieve a good balance between expected risk (variance) $x^\top \Sigma x$ and returns $\mu^\top x$. In it's most simple form we want to solve:
# $$
# \begin{array}{ll} \text{maximize} &  \mu^\top x - \gamma (x^\top \Sigma x)\\
# \text{subject to} &  1^\top x = d + 1^\top x^0 \\
#                    &  x  \geq 0,
# \end{array}
# $$
# with variable $x \in \mathbf{R}^n$, $\mu$ forecasted (expected) returns, $\gamma > 0 $ risk aversion parameter. $x^0_i$ represents the initial investment in asset $i$ and $d$ represents the cash reserve. Consequently, the equality constraint tells us that the sum of the new allocation vector $x$ has to equal the initial allocation plus the cash reserve. Furthermore, the covariance matrix of our risk model is given by $\Sigma \in \mathbf{S}_+^n$. Here we assume a factor risk model, i.e. we can write $\Sigma$ as $\Sigma = D + F F^\top$ where $D$ is diagonal and the factor matrix $F$ has a lower rank $k < n$. This approach allows us to reduce the number of nonzeros in the problem. Furthermore, note that we don't consider shortselling in this example. Let's generate some problem data:
#-
using LinearAlgebra, SparseArrays, Random, COSMO, JuMP, Test

## generate the data
rng = Random.MersenneTwister(1)
k = 5; # number of factors
n = k * 10; # number of assets
D = spdiagm(0 => rand(rng, n) .* sqrt(k))
F = sprandn(rng, n, k, 0.5); # factor loading matrix
μ = (3 .+ 9. * rand(rng, n)) / 100. # expected returns between 3% - 12%
γ = 1.0; # risk aversion parameter
d = 1 # we are starting from all cash
x0 = zeros(n);

# We can now write the problem as a QP:
# $$
# \begin{array}{ll} \text{minimize} &  x^\top D x + y^\top y - \gamma^{-1} \mu^\top x \\
# \text{subject to} &  y = F^\top x \\
#                   &  1^\top x = d + 1^\top x^0 \\
#                    &  x  \geq 0.
# \end{array}
# $$
# Before considering other effects, let's create the model in JuMP and solve it using COSMO:
#-

model = JuMP.Model(with_optimizer(COSMO.Optimizer));
@variable(model, x[1:n]);
@variable(model, y[1:k]);
@objective(model, Min,  x' * D * x + y' * y - 1/γ * μ' * x);
@constraint(model, y .== F' * x);
@constraint(model, sum(x) == d + sum(x0));
@constraint(model, x .>= 0);
JuMP.optimize!(model)

# After solving the problem, we can calculate the expected return and risk $\sigma= \sqrt{x^{* \top} \Sigma x^*}$:
x_opt = JuMP.value.(x);
y_opt = JuMP.value.(y);
expected_return_basic = dot(μ, x_opt)

#-
expected_risk_basic = sqrt(dot(y_opt, y_opt))

# ## Using standard deviation in the model
# It is pointed out in \[1\] that above problem formulation can lead to numerical problems, e.g. if $\Sigma$ is not strictly positive semidefinite. Another option is to formulate the risk constraint in terms of the standard deviation $\|M^\top x \|$ where $M M^\top = D + F F^\top$ and bound it using a second-order cone constraint:
# $$
# \begin{array}{ll} \text{minimize} &  - \mu^\top x \\
# \text{subject to} &    \|M^\top x\| \leq \gamma \\
#                   &  1^\top x = d + 1^\top x^0 \\
#                    &  x  \geq 0.
# \end{array}
# $$

Mt = [D.^0.5; F']
model = JuMP.Model(with_optimizer(COSMO.Optimizer));
@variable(model, x[1:n]);
@objective(model, Min, - μ' * x);
@constraint(model,  [γ; Mt * x] in SecondOrderCone()); # ||M'x|| <= γ
@constraint(model, sum(x) == d + sum(x0));
@constraint(model, x .>= 0);
JuMP.optimize!(model)

# Note that the result is different from the example above because $\gamma$ scales the problem in a different way. Here it can be seen as an upper bound on the standard deviation of the portfolio.
x_opt = JuMP.value.(x);
expected_return = dot(μ, x_opt)
# Let us verify that the bound holds:
@test norm(Mt * x_opt) <= γ

# ## Pareto-optimal front
# The above portfolio optimisation approach yields the optimal expected return for a given level of risk. The result is obviously impacted by the risk aversion $\gamma$ parameter. To visualise the trade-off and present the investor with an efficient Pareto optimal portfolio for their risk appetite we can compute the optimal portfolio for many choices of $\gamma$ and plot the corresponding risk-return trade-off curve.

gammas = [ 0.001, 0.01, 0.1,  0.5,  1., 3., 10, 100, 1000]
risks = zeros(length(gammas))
returns = zeros(length(gammas))
model = JuMP.Model(with_optimizer(COSMO.Optimizer, verbose = false));
@variable(model, x[1:n]);
@variable(model, y[1:k]);
@objective(model, Min,  x' * D * x + y' * y - 1/γ * μ' * x);
@constraint(model, y .== F' * x);
@constraint(model, sum(x) == d + sum(x0));
@constraint(model, x .>= 0);

## solve the same problem for different values of γ
for (k, gamma) in enumerate(gammas)
    coeff = - 1/gamma * μ
    JuMP.set_objective_coefficient.(model, x, coeff)
    JuMP.optimize!(model)
    x_opt = JuMP.value.(x);
    y_opt = JuMP.value.(y);
    returns[k] = dot(μ, x_opt)
    risks[k] = sqrt(dot(y_opt, y_opt))
end
# We can now plot the risk-return trade-off curve:
using Plots
Plots.plot(risks, returns, xlabel = "Standard deviation (risk)", ylabel = "Expected return", title = "Risk-return trade-off for efficient portfolios", legend = false)

# ## Transaction costs
# In the model above we assume that trading the assets is free and does not impact the market. However, this is clearly not the case in reality. To make the example more realistic consider the following cost $c_j$ associated with the trade $δ_j = x_j - x_j^0$:
# $$
# c_j(\delta_j) = a_j |\delta_j| + b_j |\delta_j|^{3/2},
# $$
# where the first term models the bid-ask spread and broker fees for asset $j$. The second term models the impact on the market that our trade has. This is obviously only a factor if the volume of our trade is significant. The constant $b_j$ is a function of the total volume traded in the considered time periode and the price volatility of the asset and has to be estimated by the trader. To make this example simple we consider the same coefficients $a$ and $b$ for every asset. The $|\delta_j|^{3/2}$ term can be easily modeled using a power cone constraint $\mathcal{K}_{pow} = \{(x, y, z) \mid x^\alpha y^{(1-\alpha)} \geq |z|, x \geq 0, y \geq 0, 0 \leq \alpha \leq 1 \}$. In fact this can be used to model any market impact function with exponent greater than 1.
# We can write the total transaction cost $a^\top s + b^\top t$ where $s_j$ bounds the absolute value of $\delta_j$ and $t_{j}$ is used to bound the term $|x_j - x_j^0|^{3/2} \leq t_{j}$ using a power cone formulation: $(t_{j}, 1, x_j - x_j^0) \in \mathcal{K}_{pow}(2/3)$.

#-
a = 1e-3
b = 1e-1
γ = 1.0;
model = JuMP.Model(with_optimizer(COSMO.Optimizer, eps_abs = 1e-5, eps_rel = 1e-5));
@variable(model, x[1:n]);
@variable(model, y[1:k]);
@variable(model, t[1:n]);
@variable(model, s[1:n]);
@objective(model, Min, x' * D * x + y' * y - 1/γ * μ' * x);
@constraint(model, y .== F' * x);
@constraint(model, x .>= 0);

## transaction costs
@constraint(model, sum(x) + a * sum(s) + b * sum(t) == d + sum(x0) );
@constraint(model, [i = 1:n], x[i] - x0[i] <= s[i]); # model the absolute value with slack variable s
@constraint(model, [i = 1:n], x0[i] - x[i] <= s[i]);
@constraint(model, [i = 1:n], [t[i], 1, x[i] - x0[i]] in MOI.PowerCone(2/3));
JuMP.optimize!(model)
# Let's look at the expected return and the total transaction cost:

x_opt = JuMP.value.(x);
y_opt = JuMP.value.(y);
s_opt = JuMP.value.(s);
t_opt = JuMP.value.(t);
expected_return = dot(μ, x_opt)
#-
expected_risk = dot(y_opt, y_opt)
#-
transaction_cost = a * sum(s_opt) + b * sum( t_opt)

# ## References
# [1] [Mosek Case Studies](https://docs.mosek.com/9.2/pythonfusion/case-studies-portfolio.html)


using Symbolics, ModelingToolkit, Plots, DifferentialEquations
using BenchmarkTools

# define variables
@variables x y

# expressions are exactly as you would think
x^2+y^2

# common derivative operations
Symbolics.derivative(x^2 + y^2, x)
Symbolics.gradient(x^2 + y^2, [x, y])
Symbolics.jacobian([x^2 + y^2; y^2], [x, y])

# other operations  # i.e. replace x with y^2
substitute(sin(x)^2 + 2 + cos(x)^2, Dict(x=>y^2))

# substitute numerical value
substitute(sin(x)^2 + 2 + cos(x)^2, Dict(x=>1.0))

# simplify expression to 3 (the analytic value)
simplify(sin(x)^2 + 2 + cos(x)^2)

# direct simplification is performed
2x-x # x

ex = x^2 + y^2 + sin(x)

ex + ex
ex / ex
ex^2/ex

# compare terms
isequal(ex^2/ex, ex)

# what if we have stochasticity?
foo(x, y) = x * rand() + y
@register foo(x,y) # this tells symbolic not trace further into function
Symbolics.derivative(foo(hypot(x, y), y), x)
# this leaves stops the derivative at foo(x,y) and returns result with differential using the chain rule
# since we don't know how to take (d foo/ dx)

# figure out the type that x is representing
SymbolicUtils.symtype(Symbolics.value(x))

# tell symbolics that z is a complex number
@variables z::Complex

# automatically perform correct algebra
z^2

real(z^2)

z'z # i.e. z_conj * z

# xs is a symbolic vector of length 10
@variables xs[1:10]

xs[1]

sum(xs)  # lazy representation of sum operation

# eagerly evaluate summation
sum(collect(xs))

# Rosenbrock function
rosenbrock(xs) = sum(1:length(xs)-1) do i
    100*(xs[i+1] - xs[i]^2)^2 + (1-xs[i])^2
end

N = 100
xs = ones(N)  # location of minimum

rosenbrock(xs)

# check that it is a minimum via small perturbations to check concavity
rosenbrock(xs + 1e-6*rand(N))
rosenbrock(xs + 1e-2*rand(N))

# passing symbolic variables to julia functions produces symbolic functions
@variables xs[1:N]
rosenbrock(xs)

xs = collect(xs)  # expand into eager represenation
rxs = rosenbrock(xs)
# compute gradient of rosenbrock
grad = Symbolics.gradient(rxs, xs)
hess = Symbolics.jacobian(grad, xs)
# or
hess = Symbolics.hessian(rxs, xs) # Hessian is zeroes.
hess_sparse = Symbolics.sparsehessian(rxs, xs)  # we could also do sparse jacobian.

# check that it worked
size(hess_sparse)
hess_sparse[1,1]

# to get just the sparsity pattern we can do this:
hes_sparsity = Symbolics.hessian_sparsity(rxs, xs)
hes_sparsity[1,1]

# plot the sparsity matrix
spy(hes_sparsity)

@benchmark Symbolics.hessian($rxs, $xs)
@benchmark Symbolics.sparsehessian($rxs, $xs)

foop, fip = build_function(grad, xs, expression=false) # out of place, in place

aa = rand(N)
out = similar(aa) # create output vector
fip(out, aa) # apply in place function of gradients to aa
out

foop(aa) ≈ out

using ForwardDiff

# evaluate the Jacobian via forward-mode AD to check
ForwardDiff.gradient(rosenbrock, aa) ≈ out


using LinearAlgebra
using SparseArrays
using Plots
using ForwardDiff

"This code performs a convergence test for two finite difference methods applied to
Laplace's equation with variable diffusivity κ(x)."

# variable diffusivity
κ(x) = 1 + .5*sin(pi*x) # assumed κ(x) >= κ_min > 0
# κ(x) = Float64(1 + (x>0))

# method of manufactured solutions: define the exact solution, then figure out
# what f,α,β should be to make it the solution to your PDE
uexact(x) = log(2+sin(pi*x))
dudx_exact(x) = ForwardDiff.derivative(uexact,x)
f(x) = -ForwardDiff.derivative(x->κ(x)*dudx_exact(x),x) # -d/dx (κ(x)*du/dx) = f
α = uexact(-1)
β = uexact(1)

function solve(m,κ,f,α,β)
    x = LinRange(-1,1,m+2) # points x_0, x_1, ... x_m, x_m+1
    xint = x[2:end-1]
    h = x[2]-x[1]

    xmid = @. x[1:end-1] + h/2 # x_0 + h/2, x_1 + h/2, ..., x_m + h/2
    # (re-indexing using fractional indices) x_1/2, x_3/2, ..., x_i+1/2, ..., x_m+1/2
    k = κ.(xmid)

    #                        κ_{i-1/2}+κ_{i+1/2}      κ_{i+1/2}      κ_{i+1/2}
    A = (1/h^2) * spdiagm(0=>k[1:end-1]+k[2:end],-1=>-k[2:end-1],1=>-k[2:end-1])
    b = f.(xint)
    b[1] += k[1]*α/h^2
    b[end] += k[end]*β/h^2

    u = A\b
    return u,x,xint,h
end

m = 40
u,x,xint,h = solve(m,κ,f,α,β)
plot(xint,u,mark=:dot,markersize=2,label="FD solution")
plot!(x,uexact.(x),label="Exact solution")
plot!(leg=:bottomright)

# mvec = 2 .^ (2:9)
# hvec = zeros(length(mvec))
# err = zeros(length(mvec))
# for (i,m) in enumerate(mvec)
#     u,x,xint,h = solve(m,κ,f,α,β)
#     hvec[i] = h
#     err[i] = maximum(@. abs(uexact(xint) - u))
# end
# plot(hvec,err,marker=:dot,label="Max error")
# plot!(hvec,hvec.^2,linestyle=:dash,label="O(h^2)")
# plot!(xaxis=:log,yaxis=:log,legend=:bottomright)


"""
    rekons(A,B,Xawal)
adalah fungsi yang digunakan untuk mencari solusi SPL `Ax=B` secara iteratif dengan
nilai tebakan awal `Xawal` menggunakan metode rekonstruksi aljabar.

# Example
```jl
julia> A = [4 -1 1
            4 -8 1
           -2 1 5];

julia> B = [7;-21;15];

julia> Xa = [1,2,2];

julia> X,flag,err,M = rekons(A,B,Xa);

julia> X
3-element Array{Float64,1}:
 1.9999987931831584
 3.9999993707528194
 2.9999996431226994

julia> flag
0

julia> err
7.477270454885512e-8

julia> M
18×5 Array{Float64,2}:
  0.0  1.0       2.0      2.0      NaN
  1.0  0.677229  3.45151  2.58059    0.658544
  2.0  1.51207   3.75397  2.85403    0.0644652
  3.0  1.79762   3.89498  2.94005    0.0145717
  ⋮
 14.0  1.99998   3.99999  3.0        9.83924e-7
 15.0  1.99999   4.0      3.0        4.16762e-7
 16.0  2.0       4.0      3.0        1.76529e-7
 17.0  2.0       4.0      3.0        7.47727e-8
```
return solusi `X` dengan `flag` bernilai 0 jika metode rekonstruksi aljabar
berhasil menemukan solusi dan gagal jika tidak 0.
Serta, matriks `M` yang berisi catatan proses tiap iterasi `[k, X', error]`
"""
function rekons(A,B,Xawal)
    delta = 10^-7;
    maxi = 100;
    flag = 1;
    X = Xawal[:];
    Xlama = X;
    M = [0 X' NaN];
    for k = 1:maxi
        for i = 1:length(B)
            Xlama = X;
            X = Xlama + A[i,:]*(B[i]-A[i,:]'Xlama)/(A[i,:]'A[i,:])
        end
        err = norm(X-Xlama)
        M = [M; [k X' err] ];
        if err<delta
            flag = 0;
            break
        end
    end
    err = M[end,end]
    return X, flag, err, M
end


# --- User parameters

n = 100
num_samples = 10000

# --- Imports

using BenchmarkTools
using LinearAlgebra
using Statistics

# --- Functions

"""
    Compute QR factorization of `A` using the modified Gram-Schmidt algorithm.
"""
function qr_mgs(A::Matrix)
    # Initialize Q and R
    Q = copy(A)
    R = zero(A)

    for j in 1:size(Q, 2)
        norm = LinearAlgebra.norm(Q[:, j])
        if norm > 0
            Q[:, j] /= norm
            R[j, j] = norm
        else
            continue
        end

        column = Q[:, j]
        for j_inner in (j + 1):size(Q, 2)
            R[j, j_inner] = Q[:, j_inner] ⋅ column
            Q[:, j_inner] -= R[j, j_inner] * column
        end
    end

    Q, R
end;

# --- Test qr_mgs()

n = 10
A = randn(n, n)
Q, R = qr_mgs(A)

println("Relative error Q * R: ", opnorm(A - Q * R)/opnorm(A))
println("Absolute error Q * R: ", opnorm(A - Q * R))
println("opnorm(Q' * Q): ", opnorm(transpose(Q) * Q))

det_A = det(A)
det_Q = det(Q)
det_R = det(R)
println("det(A): ", det_A)
println("det(Q): ", det_Q)
println("det(R): ", det_R)
println("Relative error det(Q * R):", (det_A - det_Q*det_R) / det_A)
println("Absolute error det(Q * R):", det_A - det_Q*det_R)

# Initialize data vectors
mgs_orthogonality_errors = zeros(num_samples)
householder_orthogonality_errors = zeros(num_samples)

mgs_det_errors = zeros(num_samples)
householder_det_errors = zeros(num_samples)

# Collect data
for i in 1:num_samples
    # Generate random matrix
    A = randn(n, n)
    det_A = det(A)

    # Compute QR factorization using modified Gram-Schmidt algorithm
    Q_mgs, R_mgs = qr_mgs(A)

    mgs_orthogonality_errors[i] = opnorm(transpose(Q_mgs) * Q_mgs - LinearAlgebra.I)

    det_R = det(R_mgs)
    mgs_det_errors[i] = abs((abs(det_A) - abs(det_R)) / det_A)

    # Compute QR factorization using Householder triangularization
    F_householder= qr(A)
    Q_householder = F_householder.Q
    R_householder = F_householder.R

    householder_orthogonality_errors[i] =
        opnorm(transpose(Q_householder) * Q_householder - LinearAlgebra.I)

    det_R = det(R_householder)
    householder_det_errors[i] = abs((abs(det_A) - abs(det_R)) / det_A)
end

# --- Orthogonality Error

println("mean(mgs_orthogonality_errors): ", mean(mgs_orthogonality_errors))
println("std(mgs_orthogonality_errors): ", std(mgs_orthogonality_errors))

println("mean(householder_orthogonality_errors): ", mean(householder_orthogonality_errors))
println("std(householder_orthogonality_errors): ", std(householder_orthogonality_errors))

# --- Determinant Error

println("mean(mgs_det_errors): ", mean(mgs_det_errors))
println("std(mgs_det_errors): ", std(mgs_det_errors))

println("mean(householder_det_errors): ", mean(householder_det_errors))
println("std(householder_det_errors): ", std(householder_det_errors))

# --- Special cases that demonstrate large loss of orthogonality with MGS

A = [0.700000 0.70711; 0.70001 0.70711]
det_A = det(A)

# MGS
Q_mgs, R_mgs = qr_mgs(A)
println("MGS orthogonality error: ", opnorm(transpose(Q_mgs) * Q_mgs - LinearAlgebra.I))
println("MGS determinant error: ", abs((abs(det_A) - abs(det(R_mgs))) / det_A))

# Householder triangularization
F_householder = qr(A)
Q_householder = F_householder.Q
R_householder = F_householder.R
println("Householder triangularization orthogonality error: ",
        opnorm(transpose(Q_householder) * Q_householder - LinearAlgebra.I))
println("Householder triangularization determinant error: ",
        abs((abs(det_A) - abs(det(R_householder))) / det_A))