# Problem from https://exercism.io/my/tracks/julia#exercise-binary-search
# Based on my published solution https://exercism.io/tracks/julia/exercises/binary-search/solutions/5dcd7f36433245a28484889c42a957c0

# Problem Instructions:

"""
    binary_search(list, query; rev=false, lt=<, by=identity)

Implement a binary search algorithm.
Searching a sorted collection is a common task. A dictionary is a sorted list of word definitions. Given a word, one can find its definition. A telephone book is a sorted list of people's names, addresses, and telephone numbers. Knowing someone's name allows one to quickly find their telephone number and address.

If the list to be searched contains more than a few items (a dozen, say) a binary search will require far fewer comparisons than a linear search, but it imposes the requirement that the list be sorted.

In computer science, a binary search or half-interval search algorithm finds the position of a specified input value (the search "key") within an array sorted by key value.

In each step, the algorithm compares the search key value with the key value of the middle element of the array.

If the keys match, then a matching element has been found and the range of indices that equal the search key value are returned.

Otherwise, if the search key is less than the middle element's key, then the algorithm repeats its action on the sub-array to the left of the middle element or, if the search key is greater, on the sub-array to the right.

If the remaining array to be searched is empty, then the key cannot be found in the array and a special "not found" indication is returned. Search methods in Julia typically return an empty range located at the insertion point in this case.

A binary search halves the number of items to check with each iteration, so locating an item (or determining its absence) takes logarithmic time. A binary search is a dichotomic divide and conquer search algorithm.

Bonus task:
Implement keyword arguments by, lt and rev so that by specifies a transformation applied to all elements of the list, lt specifies a comparison and rev specifies if the list is ordered in reverse.

Contributed By:- [Soc Virnyl Estela](https://github.com/uncomfyhalomacro)
"""
function binary_search(list, query; rev = false, lt = <, by = identity)
    if issorted(list) || issorted(list; rev = true)
        low = !rev ? 1 : length(list)
        high = !rev ? length(list) : 1
        middle(l, h) = round(Int, (l + h) // 2)
        query = by(query)

        while !rev ? low <= high : high <= low
            mid = middle(low, high)
            by(list[mid]) === query && return mid:mid
            if lt(by(list[mid]), query)
                low = !rev ? mid + 1 : mid - 1
            else
                high = !rev ? mid - 1 : mid + 1
            end
        end
        return !rev ? (low:high) : (high:low)

    else
        throw(error("List not sorted, unable to search value"))
    end
end

# See Issue https://github.com/TheAlgorithms/Julia/issues/34
"""
    binary_search(arr::AbstractArray{T,1}, l::T, r::T, x::T) where {T<:Real}

The implementation of this binary Search is recursive and requires O(Log n) space. With iterative Binary Search, we need only O(1) space. Useful for the implementation of `exponential_search`.

Contributed By:- [Ash](https://github.com/ashwani-rathee)
"""
function binary_search(
    arr::AbstractArray{T,1},
    l::T,
    r::T,
    x::T,
) where {T<:Real}
    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


using RiskAdjustedLinearizations, FastGaussQuadrature, Test

@testset "Gauss-Hermite Quadrature for Expectations of Functions of Independent Normally Distributed Random Variables/Vectors" begin
    f(x) = x  # calculate the expected value
    g(x) = 1. # calculate the probability

    ϵᵢ, wᵢ = RiskAdjustedLinearizations.standard_normal_gausshermite(3)
    true_eps, true_wts = gausshermite(3)
    @test ϵᵢ == true_eps .* sqrt(2.)
    @test wᵢ == true_wts ./ sqrt(π)

    mean51 = gausshermite_expectation(f, 5., 1., 5)
    mean01 = gausshermite_expectation(f, 0., 1., 5)
    mean53 = gausshermite_expectation(f, 5., 3., 5)
    mean03 = gausshermite_expectation(f, 0., 3., 5)
    prob   = gausshermite_expectation(g, -5., .1)

    @test mean51 ≈ 5.
    @test mean53 ≈ 5.
    @test isapprox(mean01, 0., atol = 1e-14)
    @test isapprox(mean03, 0., atol = 1e-14)
    @test prob ≈ 1.

    h1(x) = x[1]
    h2(x) = x[2]

    prob1 = gausshermite_expectation(g, [.5, 5.], [1., 1.], 5)
    mean11 = gausshermite_expectation(h1, [.5, 5.], [1., 1.], 5)
    mean21 = gausshermite_expectation(h2, [.5, 5.], [1., 1.], 5)
    prob2 = gausshermite_expectation(g, [5., -1.], [1., 1.], (5, 5))
    mean12 = gausshermite_expectation(h1, [.5, 5.], [1., 1.], (5, 5))
    mean22 = gausshermite_expectation(h2, [.5, 5.], [1., 1.], (5, 5))
    prob3 = gausshermite_expectation(g, [5., -1., 2.], [1., 1., 2.], [5, 5, 3])
    mean13 = gausshermite_expectation(h1, [.5, 5., 2.], [1., 1., 1.], [5, 5, 3])
    mean23 = gausshermite_expectation(h2, [.5, 5., 2.], [1., 1., 1.], [5, 5, 3])

    @test prob1 ≈ prob2 ≈ prob3 ≈ 1.
    @test mean11 ≈ mean12 ≈ mean13 ≈ .5
    @test mean21 ≈ mean22 ≈ mean23 ≈ 5.
end

nothing


# ## Standard line colors

#md # ![FILE_NAME.png](images/FILE_NAME.png)

## ref https://jp.mathworks.com/matlabcentral/fileexchange/35229-matlab-plot-gallery-standard-line-colors?focused=6792895&tab=example

using SpecialFunctions

x = 0:0.2:10;
y0 = besselj.(0,x);
y1 = besselj.(1,x);
y2 = besselj.(2,x);
y3 = besselj.(3,x);
y4 = besselj.(4,x);
y5 = besselj.(5,x);
y6 = besselj.(6,x);

plot(x, y0, c=:red)
plot!(x, y1, c=:green)
plot!(x, y2, c=:blue)
plot!(x, y3, c=:cyan)
plot!(x, y4, c=:magenta)
plot!(x, y5, c=:yellow)
plot!(x, y6, c=:black)


export gauss_quad_linear,gauss_points
function gauss_quad_linear(N,a,b)
    N=N-1;
    N1=N+1;
    N2=N+2;
    xu=linspace(-1,1,N1)';
    # Initial guess
    y=cos.((2*(0:N)'+1)*pi/(2*N+2))+(0.27/N1)*sin.(pi*xu*N/N2);
    # Legendre-Gauss Vandermonde Matrix
    L=zeros(N1,N2);
    # Derivative of LGVM
    Lp=zeros(N1,N2);
    # Compute the zeros of the N+1 Legendre Polynomial
    # using the recursion relation and the Newton-Raphson method
    y0=2;
    # Iterate until new points are uniformly within epsilon of old points
    while (maximum(abs.(y-y0))>eps(1.0))
        L[:,1]=1;
        Lp[:,1]=0;
        L[:,2]=y;
        #Lp[:,2]=1;
        for k=2:N1
            L[:,k+1]=( (2*k-1)*y'.*L[:,k]-(k-1)*L[:,k-1] )/k;
        end
        Lp=(N2)*( L[:,N1]-y'.*L[:,N2] )./(1-y.^2)';
        y0=y;
        y=y0'-L[:,N2]./Lp;
        y=y'
    end
    y=y'
    # Linear map from[-1,1] to [a,b]
    x=(a*(1-y)+b*(1+y))/2;
    # Compute the weights
    w=(b-a)./((1-y.^2).*Lp.^2)*(N2/N1)^2;
    return x, w
end

function gauss_points(N)
    x,w = gauss_quad_linear(N,-1,1)
    gpt = zeros(N*N,2)
    gpw = zeros(N*N,1)
    for i = 1:N
        u = x[i]
        wu = w[i]
        for j = 1:N
            v = x[j]
            wv = w[j]
            gpt[(i-1)*N + j,1] = u
            gpt[(i-1)*N + j,2] = v
            gpw[(i-1)*N + j] = wu * wv
        end
    end
    return gpt,gpw
end


##
##  d e r i v a t i v e . j l  Numerical Derivatives
##

#-- Numerical gradient of multivariate function ------------------------------
#
function fd_gradient{T<:Real}(f::Function, x0::Array{T,1}; h::Real = 0.0)
    local heps::Real = h
    if heps == 0.0; heps = eps()^(1.0/3.0); end
    local n::Int = length(x0)
    if length(f(x0)) > 1
        error("Function 'f' must be a scalar function.")
    end

    local hh = zeros(n), gr = zeros(n)
    for i = 1:n
        hh[i] = heps
        gr[i] = ((f(x0 + hh) - f(x0 - hh)) / (2.0*heps))
        hh[i] = 0.0
    end
    return gr
end

function fd_gradient(f::Function, x0::Real; h::Real = 0.0)
    local heps::Real = h
    if heps == 0.0; heps = eps()^(1.0/3.0); end
    if length(f(x0)) > 1
        error("Function 'f' must be a scalar function.")
    end
    return (f(x0 + heps) - f(x0 - heps)) / (2.0*heps)
end

function fd_jacobian{T<:Real}(f::Function, x0::Array{T,1}; h::Real = 0.0)
    local heps::Real = h
    if heps == 0.0; heps = eps()^(1.0/3.0); end
	local n = length(x0), m = length(f(x0))
	local hh = zeros(n)
	jacob = zeros(m, n)
	for i = 1:n
		hh[i] = heps
		jacob[:, i] = (f(x0 + hh) - f(x0 - hh)) / (2.0*heps)
		hh[i] = 0.0
	end
	return jacob
end

function fd_hessian{T<:Real}(f::Function, x0::Array{T,1}; h::Real = 0.0)
    local heps::Real = h
    if heps == 0.0; heps = eps()^(1.0/4.0); end
    local n::Int = length(x0)
    if length(f(x0)) != 1
        error("Function 'f' must a scalar function of n variables.")
    end

    if n == 1
        H = (f(x0.-heps) - 2.0*f(x0) + f(x0.+heps)) / heps^2

    else
        H = zeros(n, n)
        hh = diagm(heps*ones(n))
        for i = 1:(n-1)
            hi = hh[:, i]
            H[i, i] = (f(x0-hi) - 2.0*f(x0) + f(x0+hi)) / heps^2
            for j = (i+1):n
                hj = hh[:, j]
                H[i, j] = (f(x0+hi+hj) - f(x0+hi-hj) - f(x0-hi+hj) + f(x0-hi-hj)) / (4.0*heps^2)
                H[j, i] = H[i, j]
            end
        end
        hi = hh[:, n]
        H[n, n] = (f(x0-hi) - 2.0*f(x0) + f(x0+hi)) / heps^2
    end

    return H
end

function fd_laplacian{T<:Real}(f::Function, x0::Array{T,1}; h::Real = 0.0)
    local heps::Real = h
    if heps == 0.0; heps = eps()^(1.0/4.0); end
    local n::Int = length(x0)
    local hh::Real = zeros(n)

    L = 0.0
    for i = 1:n
        hh[i] = heps
        L += (f(x0+hh) + f(x0-hh) - 2.0*f(x0)) / heps^2
        hh[i] = 0.0
    end

    return L
end

#-- Central difference combined with Richardson approximation ----------------
#
function numderiv(f::Function, x0::Real; n::Int = 16, h::Real = 0.1)

    local epsilon = eps(), err = Inf
    local hstep::Real = h

    j::Int = 1
    D = zeros(n, n)
    D[1, 1] = (f(x0+hstep) - f(x0-hstep))/(2*hstep)

    while j < n
        hstep = hstep / 2.0
        D[j+1, 1] = (f(x0+hstep) - f(x0-hstep)) / (2*hstep)
        for k = 1:j
            D[j+1,k+1] = D[j+1,k] + (D[j+1,k] - D[j,k]) / (4^k - 1)
        end

        err_new = 2 * abs(D[j+1,j+1] - D[j,j]) /
                  (abs(D[j+1,j+1]) + abs(D[j,j]) + epsilon)

        if err_new >= err; break; end
        err = err_new
        j += 1
    end

    return D[j,j]
end

#-- Complex-step derivative approach -----------------------------------------
#
function complex_step(f::Function, x0::Real; h::Real = 1e-20)
    # Check whether function accepts and returns complex values
    if !isa(f(x0), Real)
        error("Function 'f' must take on a real(!) value at 'x0'.")
    end

    fx0hi = try
        f(x0 + h*1im)
    catch err
        error("Function 'f' does not appear to accept complex arguments.")
    end

    # f(x0) must be real, fx0hi complex
    if !isa(fx0hi, Complex)
        error("'f(x0 + h*1im)' must be complex for 'complex-step' to work.")
    end

    return imag(fx0hi) / h
end


using Test, Plots, Parameters, LaTeXStrings
# transform to cartesian => polar coordinates for angular integrals
# https://www.ncbi.nlm.nih.gov/pmc/articles/PMC2858241/

Nx = 200
xm = 10.
x = LinRange(-xm,xm,Nx); y = x

# create some test data
psi = @. 10*exp(-.1*(x^2+y'^2)) + 15*exp(-(x^2+(y-5)'^2)) # + randn()+im*randn()
heatmap(x,y,abs2.(psi),transpose=true)
xlabel!(L"x");ylabel!(L"y")

abstract type Basis end
struct Oscillator <: Basis
    n::Int64
    ω::Float64
end

Oscillator(n::Int64) = Oscillator(n,1.0)
index(b::Oscillator) = 1:b.n
spectrum(b::Oscillator) = (0:b.n-1)*b.ω
qnumbers(b::Oscillator) = 0:b.n-1

function hermite(x,n)
    @assert n >= 1
    h1 = exp(-x^2 / 2) * (1 / π)^(1 / 4)
    n == 1 && (return h1)
    h2 = sqrt(2) * x * h1
    n == 2 && (return h2)
    h3 = 0.0
    for j ∈ 3:n
        h3 = sqrt(2/(j - 1)) * x * h2 - sqrt((j-2)/(j-1)) * h1
        h1 = h2; h2 = h3
    end
    n > 2 && (return h3)
end
hermite(x,n,ω) = hermite(√ω * x,n) * ω ^ ( 1 / 4 )

function (b::Oscillator)(x::Array{Float64,1})
    @unpack ω,n = b
    T = x * ones(1, n) |> zero
    T[:,1] = @. hermite(x,1,ω)
    n >= 2 && (T[:,2] = @. sqrt(2 * ω) * x * T[:,1])
    for j ∈ index(b)[3:end]
        m = qnumbers(b)[j]
        T[:,j] = @. sqrt(2 / m) * (√ω * x) * T[:,j-1] - sqrt((m-1) / m) * T[:,j-2]
    end
    return T
end

(b::Oscillator)(x::LinRange{Float64}) = b(x |> collect)
(b::Oscillator)(x::Float64,n::Int64) = hermite(x,n,b.ω)

function hermite_polar(x,n,ω)
    T = zeros(size(x)...,n)
    T[:,:,1] = @. hermite(x,1,ω)
    n >= 2 && (T[:,:,2] = @. sqrt(2 * ω) * x * T[:,:,1])
    for j in 3:n
        T[:,:,j] = @. sqrt(2 / (j-1)) * (√ω * x) * T[:,:,j-1] - sqrt((j-2) / (j-1)) * T[:,:,j-2]
    end
    return T
end
hermite_polar(x,n) = hermite_polar(x,n,1.)

# test that we can create the basis, or any one of oscillator modes
N = 30
Nv = 1:N
b1 = Oscillator(N,2.)
@time b1.(x,Nv')
plot(x,hermite.(x,Nv'),legend=false,grid=false)
n = 30
b1 = Oscillator(n)
@time b1(x)
plot(x,b1(x),legend=false,grid=false)

# broadcast over any input parameters
plot(x,hermite.(x,2,[1. .5 .2 .1]),legend=false,grid=false)

# filtering cartesian data
function filterH(psi,x,N)
    H = Oscillator(N)(x)
    T = inv(H'*H)
    F̂ = T*H'*psi*H*T
    psiF = zero(psi)
        for m = 1:N, n = 1:(N+1-m)
            @. psiF += H[:,m]*F̂[m,n]*H[:,n]'
        end
    return psiF
end

# filter data by projecting onto n oscillator modes
n = 30
@time psiF = filterH(psi,x,n)
heatmap(x,y,abs2.(psiF),transpose=true)
xlabel!(L"x");ylabel!(L"y")

# coversion to polar coords
function slowpolar(psi,x,N)
    H = Oscillator(N)(x)
    T = inv(H'*H)
    F̂ = T*H'*psi*H*T

    Nx = length(x)
    θ = LinRange(0,2*pi,2*Nx)
    r = LinRange(0,last(x),Nx/2 |> Int)'

    psiP = zero(θ*r)
    for m = 1:N, n = 1:(N + 1 - m)
        @. psiP += hermite(r*cos(θ),m) * F̂[m,n] * hermite(r*sin(θ),n)
    end
    return psiP
end

function init_polar(x,N)
    θ = LinRange(0,2*pi,2*Nx)
    r = LinRange(0,last(x),Nx/2 |> Int)'
    hx = hermite_polar((@. r*cos(θ)),N)
    hy = hermite_polar((@. r*sin(θ)),N)
    return hx,hy
end

@time hx,hy = init_polar(x,n)

function polar(psi,x,hx,hy)
    N = size(hx)[3]
    H = Oscillator(N)(x)
    T = inv(H'*H)
    F̂ = T*H'*psi*H*T

    Nx = length(x)
    θ = LinRange(0,2*pi,2*Nx)
    r = LinRange(0,last(x),Nx/2 |> Int)'

    psiFP = zero(θ*r)
    for m = 1:N, n = 1:(N + 1 - m)
        Hx = @view hx[:,:,m]
        Hy = @view hy[:,:,n]
        @. psiFP += Hx * F̂[m,n] * Hy
    end
    return psiFP
end

# convert to polar coords
r = LinRange(0,last(x),Nx/2 |> Int)'
θ = LinRange(0,2*pi,2*Nx)

@time psiP = slowpolar(psi,x,30)
@time psifp = polar(psi,x,hx,hy)

# plot in polar coordinates
heatmap(θ,r',abs2.(psiP),transpose=true)
xlabel!(L"\theta");ylabel!(L"r")

heatmap(θ,r',abs2.(psifp),transpose=true)
xlabel!(L"\theta");ylabel!(L"r")

dθ = θ[2]-θ[1]
dr = r[2]-r[1]
dx = x[2]-x[1]
psiPth = sum(psiP,dims=1)*dθ
plot(r',psiPth'.*r')
xlabel!(L"r")

# check total integral preserved (< 1%)
sum(psiPth.*r)*dr
sum(psiF)*dx^2

# ===================================
# ==== Now a more realistic example for trapped BEC
using FourierGPE, VortexDistributions
gr(titlefontsize=12,size=(500,300),transpose=true,colorbar=false)

# ==== Initialize simulation
L = (25.0,25.0)
N = (256,256)
sim = Sim(L,N)
@unpack_Sim sim

# ==== set simulation parameters
μ = 25.0

# Time dependent potential function (here trivial t dep)
import FourierGPE.V
V(x,y,t) = 0.5*(x^2 + y^2)
ψ0(x,y,μ,g) = sqrt(μ/g)*sqrt(max(1.0-V(x,y,0.0)/μ,0.0)+im*0.0)

# ==== make initial state
x,y = X
ψi = ψ0.(x,y',μ,g)
ϕi = kspace(ψi,sim)
@pack_Sim! sim

# ==== evolve
@time sol = runsim(sim)

# speed regression after here?
# ==== ground state
ϕg = sol[end]
ψg = xspace(ϕg,sim)
showpsi(x,y,ψg)

# ==== set simulation parameters
γ = 0.0
t = LinRange(ti,tf,Nt)
ϕi = kspace(ψg,sim)
# reltol = 1e-7
# alg = DP5()

# TF parameters
R(w) = sqrt(2*μ/w^2)
R(1)
rv = 3.
healinglength(x,y,μ,g) = 1/sqrt(g*abs2(ψ0(x,y,μ,g)))
ξ0 = healinglength.(0.,0.,μ,g)
ξ = healinglength(rv,0.,μ,g)

# vcore = Exact(VortexDistributions.ψi,ξ)
vcore = Exact(ξ)
pv = PointVortex(rv,0.,1)
vi = ScalarVortex(vcore,pv)
psi = Torus(copy(ψg),x,y)
vortex!(psi,vi)
showpsi(x,y,psi.ψ)
ψi .= psi.ψ
ϕi = kspace(ψi,sim)

# ===== compare with Fetter JLTP 2010
ξ = 1/sqrt(μ)
Rtf = R(1)
Ωm = 3*log(Rtf/ξ/sqrt(2))/2/Rtf^2
Ωv = Ωm/(1-rv^2/Rtf^2)

# or a precession period
Tv = 2*pi/Ωv

ti = 0.; tf = Tv
t = LinRange(ti,tf,Nt)

@pack_Sim! sim

# ==== evolve
solv = runsim(sim)

# ===================================
ϕf = solv[end]
ψf = xspace(ϕf,sim)
showpsi(x,y,ψf)

anim = @animate for i in eachindex(t)
    ψ = xspace(solv[i],sim)
    showpsi(x,y,ψ)
end

gif(anim,"./examples/vortexprecession.gif",fps=25)


# # Implementations and proof of concept
#
# In this examples I want to show some of the implementations and how they are used
# as a proof of concept. This means that we will use the "low-level API", i.e. calling
# the methods directly instead of the `optimize` interface.
#
# Further, we wish to show that the implementations can at least solve some of the
# most common benchmark optimization problems.
#
# Before we start, I will define a seed and an RNG to enable reproducibility of the
# results presented here.

using Random

RANDOM_SEED = 458012;
rng = MersenneTwister(RANDOM_SEED);

#
# ## Nonlinear ``d``-dimensional global optimization problem
#
# Using `BioMetaheuristics.jl` is fairly straightforward, we will start by defining
# an d-dimensional nonlinear function to minimize,
# in this case we will use a popular function, the
# [Griewank function](http://mathworld.wolfram.com/GriewankFunction.html)
# defined as
#
# ```math
# f(\mathbf{x}) = \sum_{i=1}^d \frac{x_i^2}{4000} - \prod_{i=1}^d \cos{\left(
# \frac{x_i}{\sqrt{i}}\right)} + 1
# ```
#
# where ``d`` is the dimension of the problem. It's mostly evaluated within the
# boundaries ``-100 \leq x_i \leq 100``, and it has a **minimum** at ``\mathbf
# {x^*} = (0, \cdots, 0)``, and it evaluates to ``f(\mathbf{x^*}) = 0``.
#
# We define the function in `Julia` like this

function griewank(x)
    first_term = sum(x.^2) / 4000
    ## This variable will hold the result of the product,
    ## the second term in the function definition from above
    second_term = 1.0
    for (idx, val) in enumerate(x)
        second_term *= cos(val / sqrt(idx))
    end

    return first_term - second_term + 1.0
end

# Now, we wish to find the minimum of this function, and fortunately we know the
# true value so we can compare it later, we can use some of the implementations
# from `BioMetaheuristics.jl`, for example, [`PSO`](@ref).
# In this script we have chosen 30 particles within the population, `d` is equal
# to 20, next we define the boundaries and finally we declare that the algorithm
# will run for 20000 maximum iterations until it stops, having _converged_.

using BioMetaheuristics

val = PSO(
    griewank,
    Population(35, 10, -600.0, 600.0, rng),
    20_000,
    rng
)
println(val)

# Within a certain tolerance of about ``\epsilon = 1 \times 10^{-6}`` we have found
# the _global_ minimum of the function. We can actually check the value with the
# evaluation, notice that it actually returns `0`, as expected.

griewank(val.x)

# ## Nonlinear 2-dimensional global optimization problem
#
# Let us now tackle one of the most common optimization problems, which is
# finding the minimum of the [Rosenbrock function](https://en.wikipedia.org/wiki/Rosenbrock_function)
# which is a non-convex function, meaning that is does not have just one minimum
# or stationary point, it has several, so it is a difficult problem for classical
# optimization algorithms. In this example we will try to solve it using the
# [`SimulatedAnnealing`](@ref) implementation from `BioMetaheuristics.jl`.
#
# First, we define the Rosenbrock function in `Julia`

rosenbrock2d(x) =  (1.0 - x[1])^2 + 100.0 * (x[2] - x[1]^2)^2;

# We will apply the _Simulated Annealing_ algorithm to find the global optimum
val = SimulatedAnnealing(
    rosenbrock2d, -5.0, 5.0, 2, rng; low_temp=10_000
)
println(val)

# Again, within a certain tolerance we find the expected result which is
#
# ```math
# \mathbf{x}^{*} = (1, 1)
# ```
#
# and if we account for rounding errors and floating-point arithmetic, we
# can safely take this result as the best.


using Random: AbstractRNG
using Bijectors
import Bijectors: logabsdetjac, bijector
using Distributions: MultivariateDistribution, Continuous
import Distributions: _rand!, _logpdf
using StatsFuns

"""
    Kipping13()

A non-informative prior for two-parameter limb-darkening coefficients using *triangular sampling* ([Kipping 2013](https://ui.adsabs.harvard.edu/abs/2013MNRAS.435.2152K/)).

# Examples

```jldoctest
julia> using Random; rng = Random.seed!(10);

julia> rand(rng, Kipping13())
2-element Vector{Float64}:
 0.24716310305467298
 0.08836997249298882

julia> rand(rng, Kipping13(), 5)
2×5 Matrix{Float64}:
 0.0664907  0.124817   1.00732    0.806902  0.74165
 0.520411   0.222718  -0.389412  -0.314755  0.0768429
```

# References

> [Kipping (2013)](https://ui.adsabs.harvard.edu/abs/2013MNRAS.435.2152K/)
>
>   "Efficient, uninformative sampling of limb darkening coefficients for two-parameter laws"
"""
struct Kipping13 <: MultivariateDistribution{Continuous} end

Base.length(::Kipping13) = 2

function _rand!(rng::AbstractRNG, ::Kipping13, x::AbstractVector{T}) where T
    q1, q2 = rand(rng, T, 2)
    sqrtq1 = sqrt(q1)
    twoq2 = 2 * q2
    x[begin] = sqrtq1 * twoq2
    x[end] = sqrtq1 * (1 - twoq2)
    return x
end

_logpdf(::Kipping13, x::AbstractArray{T}) where {T} = zero(T)

struct Kipping13Transform <: Bijector{1} end

function (::Kipping13Transform)(x::AbstractVector)
    usum = sum(x)
    q = [usum^2, 0.5 * first(x) / usum]
    return @. log(q) - log(1 - q)
end

function (::Inverse{<:Kipping13Transform})(y::AbstractVector)
    tmp = map(logistic, y)
    sqrtq1 = sqrt(first(tmp))
    twoq2 = 2 * last(tmp)
    tmp[begin] = sqrtq1 * twoq2
    tmp[end] = sqrtq1 * (1 - twoq2)
    return tmp
end

logabsdetjac(::Kipping13Transform, y::Number) = -2 * softplus(-y) - y
logabsdetjac(k::Kipping13Transform, y) = sum((yi) -> logabsdetjac(k, yi), y)
bijector(::Kipping13) = Kipping13Transform()


# From algorithm 3.1

function bracket_minimum(f, x=0, s=1e-2, k=2.0)

    a, ya = x, f(x)
    b, yb = a + s, f(a + s)
    if yb > ya
        a, b = b, a
        ya, yb = yb, ya
        s = -s
    end
    while true
        c, yc = b + s, f(b + s)
        if yc > yb
            return (min(a,c), max(a,c))
        end
        a, ya, b, yb = b, yb, c, yc
        s *= k
    end
end

function test_f(x)
     return x^3 + 2 * x^2 + x + 3
end

@time a, b = bracket_minimum(test_f)


# # Munk Gyre analytical solution equations
# things between #### blocks are the code for checking exact solution in DGmodel test

using Pkg
Pkg.add("Latexify")
using Latexify
Pkg.add("Plots")
using Plots
Pkg.add("PyPlot")
pyplot()

# Problem parameters
# These will come from DG model/problem settings
###
τ₀=0.1      # Stress
ρ=1000      # density
f=1.e-4     # Coriolis
β=1.e-11    # Rate of change of Coriolis
H=1000      # Depth
L_x=1000.e3 # Zonal extent
L_y=1000.e3 # Meridional extent
Ah=1000.    # Viscosity
g=10.       # Gravity

δ_m=(Ah/β)^(1/3)
####

# Set up Munk formula per https://mitgcm.readthedocs.io/en/latest/examples/examples.html#model-solution
# Build up using expressions to catch typos
t1=:( cos((3^0.5*x)/(2*δ_m))+(1)/(3^0.5)*sin((3^0.5*x)/(2*δ_m)) )
display(latexify(t1))
t2=:( 1-exp((-x)/(2*δ_m) ) * $t1 )
display(latexify(t2))
t3=:( π*sin(π*y/L_y) )
display(latexify(t3))
t4=:( τ₀/(ρ*g*H)*(f/β)*(1-x/L_x) )
display(latexify(t4))
tfull=:( ($t4) * ($t3) * ($t2) )
display(latexify(tfull))
display(tfull)

# Actual function (a copy paste from display(tfull) line)
#### This is formula needed for checking numerical against exact
ηfun(x,y)=(((τ₀ / (ρ * g * H)) * (f / β) * (1 - x / L_x)) * (π * sin((π * y) / L_y)) * (1 - exp(-x / (2δ_m)) * (cos((3 ^ 0.5 * x) / (2δ_m)) + (1 / 3 ^ 0.5) * sin((3 ^ 0.5 * x) / (2δ_m)))))
####

# Now make a plot to check
xvals=collect(0:L_x/100:L_x)
yvals=collect(0:L_y/100:L_y)
nx=length(xvals)
ny=length(yvals)
η=zeros(nx,ny)
for j=1:ny
 for i=1:nx
  η[i,j]=ηfun(xvals[i],yvals[j])
 end
end

p2 = contour(xvals, yvals, η')
plot(p2)


function swiss_roll(n, σ = 0.1)
    # x = rand(n) .* (3.0 * π) .+ (1.5 * π)
    # y = rand(n) .* (30.0 * π)

    ### 1
    # f = permutedims([(x, y) -> x .* cos.(x),
    #                  (x, y) -> y,
    #                  (x, y) -> x .* sin.(x) ])
    # broadcast((f, x, y) -> f(x, y), f, x, y)

    ### 2
    # f = ((x, y) -> x .* cos.(x),
    #      (x, y) -> y,
    #      (x, y) -> x .* sin.(x) )
    # for i in axis(res, 1), j in axis(res, 2)
    #     res[i, j] = ff[j](x[i], y[i])
    # end
    # return res

    ### 3
    # res = Array{Float64, 2}(undef, n, 3)
    # f1(x, y) = x .* cos.(x)
    # f2(x, y) = y
    # f3(x, y) = x .* sin.(x)
    # res[:, 1] .= f1.(x, y) .+ randn(n) .* σ
    # res[:, 2] .= f2.(x, y) .+ randn(n) .* σ
    # res[:, 3] .= f3.(x, y) .+ randn(n) .* σ
    # return res

    ### 4
    # hcat(x .* cos.(x), y, x .* sin.(x));

    ### 5
    # f(x, y) = (x * cos(x), y, x * sin(x))
    # permutedims(reshape(reinterpret(Float64, f.(x, y)), (3, n)))

    xx() = rand() .* (3.0 * π) .+ (1.5 * π)
    yy() = rand() .* (30.0 * π)

    permutedims(reshape(reinterpret(Float64, [
        begin
            x = xx()
            y = yy()
            (x * cos(x), y, x * sin(x))
        end for i in 1:n
    ]), (3, n)))
end

function circles(n, σ = 0.1, factor = 0.8)
    res = Array{Float64, 2}(undef, n, 2)

    classes = rand(Bool, n)
    ff = classes .* factor
    res[:, 1] .= cos.(range(0, stop = 2 * π, length = n)) .* ff
    res[:, 2] .= sin.(range(0, stop = 2 * π, length = n)) .* ff

    res .+= randn(size(res)...) .* σ
    res, classes
end


function luhntest(x::Integer)
    d = reverse(digits(x))
    s = sum(d[1:2:end])
    s += sum(sum.(digits.(2d[2:2:end])))
    return s % 10 == 0
end

for card in [49927398716, 49927398717, 1234567812345678, 1234567812345670]
    println(luhntest(card) ? "PASS " : "FAIL ", card)
end


module HWfuncapp

using FastGaussQuadrature # to get chebyshevnodes

# you dont' have to use PyPlot. I did much of it in PyPlot, hence
# you will see me often qualify code with `PyPlot.plot` or so.
using PyPlot
import ApproXD: getBasis, BSpline
using Distributions
using ApproxFun
using Plots

ChebyT(x,deg) = cos(acos(x)*deg)
unitmap(x,lb,ub) = 2 .* (x .- lb) ./ (ub .- lb) .- 1	#[a,b] -> [-1,1]

function q1(n)
	f(x) = x .+ 2x.^2 - exp.(-x)

	# without using PyPlot, just erase the `PyPlot.` part
	PyPlot.savefig(joinpath(dirname(@__FILE__),"..","q1.png"))
	return Dict(:error=>maximum(abs,err))
end

function q2(b::Number)
	@assert b > 0
	# use ApproxFun.jl to do the same:

	Plots.savefig(p,joinpath(dirname(@__FILE__),"..","q2.png"))
end

function q3(b::Number)

	# p is your plot
	return (p,integral)
end

# optinal
function q4()

	return fig
end

# I found having those useful for q5
mutable struct ChebyType
	f::Function # fuction to approximate
	nodes::Union{Vector,LinRange} # evaluation points
	basis::Matrix # basis evaluated at nodes
	coefs::Vector # estimated coefficients

	deg::Int 	# degree of chebypolynomial
	lb::Float64 # bounds
	ub::Float64

	# constructor
	function ChebyType(_nodes::Union{Vector,LinRange},_deg,_lb,_ub,_f::Function)
		n = length(_nodes)
		y = _f(_nodes)
		_basis = Float64[ChebyT(unitmap(_nodes[i],_lb,_ub),j) for i=1:n,j=0:_deg]
		_coefs = _basis \ y  # type `?\` to find out more about the backslash operator. depending the args given, it performs a different operation
		# create a ChebyComparer with those values
		new(_f,_nodes,_basis,_coefs,_deg,_lb,_ub)
	end
end

# function to predict points using info stored in ChebyType
function predict(Ch::ChebyType,x_new)

	true_new = Ch.f(x_new)
	basis_new = Float64[ChebyT(unitmap(x_new[i],Ch.lb,Ch.ub),j) for i=1:length(x_new),j=0:Ch.deg]
	basis_nodes = Float64[ChebyT(unitmap(Ch.nodes[i],Ch.lb,Ch.ub),j) for i=1:length(Ch.nodes),j=0:Ch.deg]
	preds = basis_new * Ch.coefs
	preds_nodes = basis_nodes * Ch.coefs

	return Dict("x"=> x_new,"truth"=>true_new, "preds"=>preds, "preds_nodes" => preds_nodes)
end

function q5(deg=(5,9,15),lb=-1.0,ub=1.0)

	runge(x) = 1.0 ./ (1 .+ 25 .* x.^2)

	PyPlot.savefig(joinpath(dirname(@__FILE__),"..","q5.png"))

end

function q6()

	# compare 2 knot vectors with runge's function

	PyPlot.savefig(joinpath(dirname(@__FILE__),"..","q6.png"))

end

function q7()

	PyPlot.savefig(joinpath(dirname(@__FILE__),"..","q7.png"))
end

	# function to run all questions
function runall()
	@info("running all questions of HW-funcapprox:")
	q1(15)
	q2(3)
	q3(10)
	q4()
	q5()
	q6()
	q7()
end

end # module


import Pkg; Pkg.add(Pkg.PackageSpec(url="https://github.com/JuliaComputing/JuliaAcademyData.jl"))
using JuliaAcademyData; activate("Foundations of machine learning")

# ## What is learning?
#
# Computers read data, as we saw in notebooks 1 and 2. We can then build functions that model that data to make decisions, as we saw in notebooks 3 and 5.
#
# But how do you make sure that the model actually fits the data well? In the last notebook, we saw that we can fiddle with the parameters of our function defining the model to reduce the loss function. However, we don't want to have to pick the model parameters ourselves. Choosing parameters ourselves works *well enough* when we have a simple model and only a few data points, but can quickly become extremely complex for more detailed models and larger data sets.
#
# Instead, we want our machine to *learn* the parameters that fit the model to our data, without needing us to fiddle with the parameters ourselves. In this notebook, we'll talk about the "learning" in machine learning.

#-

# ### Motivation: Fitting parameters by hand
#
# Let's go back to our example of fitting parameters from notebook 3. Recall that we looked at whether the amount of green in the pictures could distinguish between an apple and a banana, and used a sigmoid function to model our choice of "apple or banana" using the amount of green in an image.

using Plots; gr()
using Images, Statistics

σ(x,w,b) = 1 / (1 + exp(-w*x+b))

apple =  load(datapath("data/10_100.jpg"))
banana = load(datapath("data/104_100.jpg"))
apple_green_amount =  mean(Float64.(green.(apple)))
banana_green_amount = mean(Float64.(green.(banana)));

w = 10.0 # Try manipulating w between 0 and 30 to see how the plot changes
b = 15.0 # Try manipulating b bewteen 0 and 30

plot(x->σ(x,w,b), 0, 1, label="Model", legend = :topleft, lw=3)
scatter!([apple_green_amount],  [0.0], label="Apple")
scatter!([banana_green_amount], [1.0], label="Banana")

# Intuitively, how did you tweak the sliders so that way the model sends apples to 0 and bananas to 1? Most likely, you did the following:
#
# #### Move the sliders a bit, see whether the curve moves in the right direction, and if it did, keep doing it.
#
# For a machine, "learning" is that same process, translated into math!

#-

# ## "Learning by nudging": The process of descent
#
# Let's start to formalize this idea. In order to push the curve in the "right direction", we need some measurement of "how right" and "how wrong" the model is. When we translate the idea of a "right direction" into math, we end up with a **loss function**, `L(w, b)`, as we saw in notebook 5. We say that the loss function is lowest when the model `σ(x, w, b)` performs the best.
#
# Now we want to create a loss function that is the lowest when the apple is at `0` and the banana is at `1`. If the data (the amount of green) for our apple is $x_1$, then our model will output $σ(x_1,w, b)$ for our apple. So, we want the difference $0 - σ(x_1, w, b)$ to be small. Similarly, if our data for our banana (the banana's amount of green) is $x_2$, we want the difference $1 - σ(x_2, w, b)$ to be small.
#
# To create our loss function, let's add together the squares of the difference of the model's output from the desired output for the apple and the banana. We get
#
# $$ L(w,b) = (0 - σ(x_1, w, b))^2 + (1 - σ(x_2, w, b))^2. $$
#
# $L(w, b)$ is lowest when it outputs `0` for the apple and `1` for the banana, and thus the cost is lowest when the model "is correct".
#
# We can visualize this function by plotting it in 3D with the `surface` function or in 2D with contour lines

## plotly() # The plotly backend is nice for 3d surface plots
gr() # The GR backend is good for faster interactive plots

#-

L(w, b) = (0 - σ(apple_green_amount,w,b))^2 + (1 - σ(banana_green_amount,w,b))^2

w_range = 10:0.1:13
b_range = 0:1:20

L_values = [L(w,b) for b in b_range, w in w_range]

w = 11.5 # Try manipulating w with values from w_range (between 10 and 13)
b = 10 # Try manipulating b with values from b_range (between 0 and 20)
##     p1 = surface(w_range, b_range, L_values, xlabel="w", ylabel="b", cam=(70,40), cbar=false, leg=false)
##     scatter!(p1, [w], [b], [L(w,b)+1e-2], markersize=5, color = :blue)
p1 = contour(w_range, b_range, L_values, levels=0.05:0.1:1, xlabel="w", ylabel="b", cam=(70,40), cbar=false, leg=false)
scatter!(p1, [w], [b], markersize=5, color = :blue)

p2 = plot(x->σ(x,w,b), 0, 1, label="Model", legend = :topleft, lw=3)
scatter!(p2, [apple_green_amount],  [0.0], label="Apple", markersize=10)
scatter!(p2, [banana_green_amount], [1.0], label="Banana", markersize=10, xlim=(0,1), ylim=(0,1))
plot(p1, p2, layout=(2,1))

# The blue ball on the 3D plot shows the current parameter choices, plotted as `(w,b)`. Shown below the 3D plot is a 2D plot of the corresponding model with those parameters. Notice that as the blue ball rolls down the hill, the model becomes a better fit. Our loss function gives us a mathematical notion of a "hill", and the process of "learning by nudging" is simply rolling the ball down that hill.

#-

# To do this mathematically, we need to know which direction is "downhill". Recall from calculus that the derivative of `L` with respect to `b` tells you how `L` changes when `b` changes. Thus to roll downhill, we should go in the direction where the derivative is negative (the function goes down) for each parameter. This direction is the negative of what's called the **gradient**, $\nabla L$. This means that the "learn by nudging method" can be rephrased in mathematical terms as:
#
# 1. Calculate the gradient
# 2. Move a little bit in the direction of the negative gradient
# 3. Repeat
#
# This process of rolling the ball in the direction of the negative gradient is called **gradient descent**; written mathematically, it is
#
# $$p_{n+1} = p_n - \eta \nabla L(p_n).$$
#
# Here, $p_n$ represents the vector of current parameters $(w, b)$; $\nabla L(p_n)$ is the gradient of the loss function, given those parameters. We start from $p_n$ and change it by $\eta \nabla L(p_n)$, where $\eta$ is a small step size that determines how far we move the parameters in the direction of the negative gradient; notice that if you step too far, you'll overshoot the minimum!. The result is $p_{n+1}$, the new vector of parameters.
#
# [Picture of Gradient Descent Vectors]
#
# If we repeat this process, then we will end up at parameters where the model correctly labels apples as `0` and bananas as `1`. When this happens, the model has learned from the data and can then read pictures and tell you whether they are apples or bananas!

#-

# #### Exercise 1
#
# Use the following terms to fill in the sentences below. Terms may be used more than once or not at all:
# > gradient, loss function, derivative, gradient descent, learning.
#
# * We can think of a _(A)_ as a 1D version of a _(B)_.
# * We can visualize a _(C)_ as a hill.
# * In the explanation above, rolling downhill is called _(D)_ and means traveling along the _(E)_.
# * To quantify the correctness of a model we use a _(F)_.
# * When our program can minimize a _(G)_ on its own, we say it is _(H)_.
#
#

#
# A)

# B)

# C)

# D)

# E)

# F)

# G)

# H)

#-

# #### Solution
#
# A) derivative

# B) gradient

# C) loss function

# D) gradient descent

# E) gradient

# F) loss function

# G) loss function

# H) learning

Courses/Foundations of machine learning/0900.What-is-learning.jl


function pagerank(
    g::AbstractGraph{U},
    α=0.85,
    n=100::Integer,
    ϵ=1.0e-6
    ) where U <: Integer

    # indegree(g, v) is estimated run-time to iterate over inneighbors(g, v)
    partitions = LightGraphs.optimal_contiguous_partition(indegree(g), nthreads(), nv(g))

    α_div_outdegree = Vector{Float64}(undef,nv(g))
    dangling_nodes = Vector{U}()
    @inbounds for v in vertices(g)
        if outdegree(g, v) == 0
            push!(dangling_nodes, v)
        end
        α_div_outdegree[v] = (α/outdegree(g, v))
    end

    nvg = Int(nv(g))
    # solution vector and temporary vector
    x = fill(1.0 / nvg, nvg)
    xlast = copy(x)
    @inbounds for _ in 1:n
        dangling_sum = 0.0
        for v in dangling_nodes
            dangling_sum += x[v]
        end
        # flow from teleprotation
        y = (1 - α + α * dangling_sum) * (1.0 / nvg)
        xlast .= y
        # flow from edges
        let x = x
            @threads for v_set in partitions
                for v in v_set
                    for u in inneighbors(g, v)
                        xlast[v] += (x[u] * α_div_outdegree[u])
                    end
                end
            end
        end

        # l1 change in solution convergence criterion
        err = 0.0
        for v in vertices(g)
            err += abs(xlast[v] - x[v])
            x[v] = xlast[v]
        end
        if (err < nvg * ϵ)
            return x
        end
    end
    error("Pagerank did not converge after $n iterations.") # TODO 0.7: change to InexactError with appropriate msg.
end


# Exploration of Boos Stanski Chapter 1 - consulting example

# You are trying to model maximum flooding level Q
# while A is floodplane
# and k and η are parameters to be estimated

# literature suggests over 100 years the maximum level is:
Q(k, A, η) = k * A^(η - 1)

# Take the log to linearlize
logQ(k, A, η) = log(k) + (η - 1)*log(A)

# Verify
round(logQ(5, 100, .5), digits=5) == round(log(Q(5, 100, .5)), digits=5)

# The problem is that we don't actually see any observation which spans 100 years

# Example is of 140 gauging stations - 140 areas
# Stations have between 6 and 83 years of observations.

# The expected value E for subset of observations X₁ which is smaller than subset
# X₂ which is smaller than the full set X₁₀₀ is going to have the following property:
# E(max(X₁)) < E(max(X₂)) < E(max(X₁₀₀))

# What we would like to know is max(X₁₀₀)

# But the maximum of any of our observed values will be biased downwards since we
# never observe 100 years.

# Authors suggest using an estimate of the the joint likelihood that any observation
# will be below the median using the following identity allowing unobserved and observed
# X_i through X_100 to be ordered
# P(X_100 ≤ t) = P(X_1 ≤, ... X_100 ≤ t) = Π P(X_i < t) = [F(t)]^100
# by iid P(X_100 ≤ t) => [F(t)]^100

# P(X_100 ≤ t) = [F(t)]^100 = 1/2
# → P(X_100 ≤ t) = F(t₀) = (1/2)^.01 → t₀ = .993

# Using Extreme value function
# P(X_i <= t) = exp(-exp(-(t-μ)/σ))

Q993(μ, σ) = σ*(-log(-log(.993))) + μ

# The Authors suggest that we can estimate μ and σ then plug them into the function
# to get the solution.

# lets try it

using Random, Distributions

waterlevel = rand(Poisson(50), 100)
observe = rand(Binomial(1, .25), 100)

μhat = mean(waterlevel[observe .== 1])
σhat = var(waterlevel[observe .== 1])^.5

Q993(μhat, σhat)
maximum(waterlevel)


# D(u)_function.jl

"""
Implementation of FHIP's susceptibility formula D(u) for the Optical Absorption of Polarons.

See FHIP 1962, equation (35c):
https://link.aps.org/doi/10.1103/PhysRev.127.1004.
"""

"""
ℜD(x::Float64, y::Float64, v::Float64, w::Float64, β::Float64)

    Calculate the real part of D(u) (equation (35c) in FHIP) for a complex argument u = x + iy. v and w are the variational Polaron parameters that minimise the free energy, for the supplied α Frohlich coupling. β is the reduced thermodynamical beta = ħω/kT with k being Boltzman's constant.
"""
function ℜD(x, y, v, w, β)
    x = BigFloat(x)
    y = BigFloat(y)
    β = BigFloat(β)
    v = BigFloat(v)
    w = BigFloat(w)

    R = (v^2 - w^2) / (w^2 * v)
    a_squared = β^2 / 4 + R * β * coth(β * v / 2)
    b = R * β / sinh(β * v / 2)

    w^2 * (a_squared - β^2 / 4 - b * cos(v * x) * cosh(v * (y - β / 2)) + x^2 + y * (β - y)) / (β * v^2)
end

"""
ℑD(x::Float64, y::Float64, v::Float64, w::Float64, β::Float64)

    Calculate the imaginary part of D(u) (equation (35c) in FHIP) for a complex argument u = x + iy. v and w are the variational Polaron parameters that minimise the free energy, for the supplied α Frohlich coupling. β is the reduced thermodynamical beta = ħω/kT with k being Boltzman's constant.
"""
function ℑD(x, y, v, w, β)
    x = BigFloat(x)
    y = BigFloat(y)
    β = BigFloat(β)
    v = BigFloat(v)
    w = BigFloat(w)

    R = (v^2 - w^2) / (w^2 * v)
    a_squared = β^2 / 4 + R * β * coth(β * v / 2)
    b = R * β / sinh(β * v / 2)

    w^2 * (b * sin(v * x) * sinh(v * (y - β / 2)) + 2 * x * (y - β / 2)) / (β * v^2)
end

# Create a complex number from the real and imaginary parts of D(u).

D(x, y, v, w, β) = ℜD(x, y, v, w, β) + 1im * ℑD(x, y, v, w, β)


abstract type actFun end

export actFun

export probToValue

### sigmoid

abstract type σ <: actFun end

"""
    return the Sigmoid output
    inputs must be matices
"""
σ(X, W, B) = 1 ./ (1 .+ exp.(.-(W * X .+ B)))
σ(Z) = 1 ./ (1 .+ exp.(.-Z))

export σ

"""
    return the derivative of Sigmoid function
"""
dσ(Z) = σ(Z) .* (1 .- σ(Z))

export dσ

@doc raw"""
    function probToValue(
        actFun::Type{σ},
        probs::AbstractArray{T,N},
        labels::Aa = nothing;
        evalConst = 0.5,
    ) where {Aa<:Union{<:AbstractArray,Nothing},T,N}

Convert the probabilities return out of sigmoid function to Bool value (i.e. 0,1) values based on comparing on a threshold value `evalConst`

# Return

- `Ŷ_bool` := Boolean valuse of the probabilites

- `acc` := Accuracy when `labels` provided
"""
function probToValue(
    actFun::Type{σ},
    probs::AbstractArray{T,N},
    labels::Aa = nothing;
    evalConst = 0.5,
) where {Aa<:Union{<:AbstractArray,Nothing},T,N}

    s = size{probs}
    Ŷ_bool = probs .> T(evalConst)
    acc = nothing
    if labels isa AbstractArray
        acc = sum(Ŷ_bool .== labels) / (s[end-1] * s[end])
        # println("Accuracy = $acc")
    end
    return Ŷ_bool, acc
end #predictpredict(probs::AbstractArray{T, 2},

### relu

abstract type relu <: actFun end

"""
    return the ReLU output
"""
function relu(Z::AbstractArray{T,N}) where {T,N}
    max.(zero(T), Z)
end #function relu(Z::AbstractArray{T,N}) where {T,N}

export relu

"""
    return the derivative of ReLU function
"""
function drelu(Z::AbstractArray{T,N}) where {T,N}
    return T.(Z .> zero(T))
end #function drelu(z::AbstractArray{T,N}) where {T,N}

export drelu

### softmax

abstract type softmaxFamily <: actFun end

abstract type softmax <: softmaxFamily end

"""
    compute the softmax function

"""
function softmax(Ŷ::AbstractArray{T,N}) where {T,N}
    Ŷ_exp = exp.(Ŷ)
    sumofexp = sum(Ŷ_exp, dims = N - 1)
    return Ŷ_exp ./ sumofexp
end #softmax

function dsoftmax(Ŷ, dim = 1)
    sŶ = softmax(Ŷ)
    T = eltype(Ŷ)
    softMat = AbstractArray{T,3}(undef, 0, 0, 0)
    sSize = size(Ŷ)[dim]
    for c in eachcol(sŶ)
        tmpMat = zeros(T, sSize, sSize)
        for i = 1:length(c)
            for j = 1:length(c)
                if i == j
                    tmpMat[i, j] = c[i] * (1 - c[j])
                else
                    tmpMat[i, j] = -c[i] * c[j]
                end
            end
        end
        softMat = cat(softMat, tmpMat, dims = 3)
    end

    return softMat
end #dsoftmax

export softmax, dsoftmax

@doc raw"""
    function probToValue(
        actFun::Type{S},
        probs::AbstractArray{T,N};
        labels = nothing,
    ) where {T,N,S<:softmaxFamily}

convert the probabilites out of `softmax` or softmax-like functions into `Bool` values, where the max value gets 1 and the other get zeros

# Return

- `Ŷ_bool` := Boolean valuse of the probabilites

- `acc` := Accuracy when `labels` provided
"""
function probToValue(
    actFun::Type{S},
    probs::AbstractArray{T,N};
    labels = nothing,
) where {T,N,S<:softmaxFamily}

    maximums = maximum(probs, dims = N - 1)
    Ŷ_bool = probs .== maximums
    acc = nothing
    if labels isa AbstractArray
        acc = 0
        bool_labels = Bool.(labels)
        ax = axes(bool_labels)[1:end-1]
        endax = axes(bool_labels)[end]
        trueFalse = Array{Bool,1}(undef, length(endax))
        @simd for i in endax
            lab = view(bool_labels, ax..., i)
            pred = view(Ŷ_bool, ax..., i)
            trueFalse[i] = (lab == pred)
        end
        acc = mean(trueFalse)
        # println("Accuracy = $acc")
    end

    return Ŷ_bool, acc
end #predictpredict(probs::AbstractArray{T, 2}, :softmax)

### tanh

abstract type tanh <: actFun end

Base.tanh(Z::AbstractArray{T,N}) where {T,N} = Base.tanh.(Z)

tanh(Z::AbstractArray{T,N}) where {T,N} = Base.tanh.(Z)

dtanh(Z::AbstractArray{T,N}) where {T,N} = 1 .- (Base.tanh.(Z)) .^ 2

export dtanh, tanh

### noAct

abstract type noAct <: actFun end

function noAct(Z)
    return Z
end

function dnoAct(Z)
    return ones(eltype(Z), size(Z)...)
end

export noAct, dnoAct


"""
    horner(coefficients, x)

Evaluate the given polynomial at position x using the Horner scheme.

```math
p(x) = \\sum_{n=0}^N c_n x^n
```
"""
function horner(coefficients::Vector{T}, x::Number) where T<:Number
    bn = coefficients[end]
    for n=length(coefficients)-1:-1:1
        bn = coefficients[n] + bn*x
    end
    bn
end

module hermite

"""
    a_nk(N)

Calculate the all coefficients for all Hermite polynomials up to order N.

```math
H_n(x) = \\sum_{k=0}^n a_{n,k} x^k
```

Returns a vector of length N+1 where the n-th entry contains all coefficients
for the n-th Hermite polynomial.
"""
function a(N::Int)
    a = Vector{Vector{Int}}(undef, N+1)
    a[1] = [1]
    a[2] = [0,2]
    am = a[2]
    for n=2:N
        an = zeros(Int, n+1)
        a[n+1] = an
        if iseven(n)
            an[1] = -am[2]
        end
        an[n+1] = 2*am[n]
        if iseven(n)
            for k=3:2:n-1
                an[k] = 2*am[k-1] - am[k+1]*k
            end
        else
            for k=2:2:n-1
                an[k] = 2*am[k-1] - am[k+1]*k
            end
        end
        am = an
    end
    a
end

"""
    A_nk(N)

Calculate the all scaled coefficients for all Hermite polynomials up to order N.

The scaled coefficients `A` are connected to the unscaled coefficients `a` by
the relation ``A_{n,k} = \\frac{a_{n,k}}{\\sqrt{2^n n!}}``

Returns a vector of length N+1 where the n-th entry contains all scaled
coefficients for the n-th Hermite polynomial.
"""
function A(N)
    A = Vector{Vector{Float64}}(undef, N+1)
    A[1] = [1.]
    A[2] = [0., sqrt(2)]
    Am = A[2]
    for n=2:N
        An = zeros(Float64, n+1)
        A[n+1] = An
        if iseven(n)
            An[1] = -Am[2]/sqrt(2*n)
        end
        An[n+1] = Am[n]*sqrt(2/n)
        if iseven(n)
            for k=3:2:n-1
                An[k] = Am[k-1]*sqrt(2/n) - Am[k+1]*k/sqrt(2*n)
            end
        else
            for k=2:2:n-1
                An[k] = Am[k-1]*sqrt(2/n) - Am[k+1]*k/sqrt(2*n)
            end
        end
        Am = An
    end
    A
end

end # module hermite


"""
    metnum.jl

Modul ini berisi metode untuk menyelesaikan

1. Akar pertaksamaan tak-linear: `bisection`, `regulaFalsi`, `fixpoint`, `newtonRaphson`, `secant`
2. SPL langsung: `backsub`, `elimGaussNonPivoting`, `elimGaussWithPivoting`, `LUtanpaP`, `LUdenganP`
3. SPL iteratif: `jacobi`, `gaussSeidel`, `rekons` ,`conGrad`
4. Interpolas: `lagrange`, `newton`
5. Regresi: `reglin`, `regpower`, `regpoly`
6. Turunan: `bedaPusat`, `richardson`
7. Integral: `komptrap`, `kompsimp`, `rekursif`, `romberg`, `adaptif`
8. PDB dengan MNA: `euler`, `heun`, `taylor`, `rungekutta`, `rungekuttasistem`, `rkf45`
9. PDB dengan MNB: `linearshooting`, `findiff`
10. PDP: `gelombang`, `panas`, `dirichlet`
"""
module metnum

using LinearAlgebra
using Statistics
using Dates
using IJulia

export regpoly, regpower, regulaFalsi, rekons, rekursif, richardson, rkf45
export romberg, rungekutta, rungekuttasistem, secant
export taylor, adaptif, backsub, bedaPusat, bisection
export conGrad, dirichlet, elimGaussNonPivoting, elimGaussWithPivoting
export euler, findiff, fixpoint, gaussSeidel, gelombang, heun, jacobi
export kompsimp, komptrap, lagrange, linearshooting, LUdenganP, LUtanpaP
export newton, newtonRaphson, panas, reglin, mulaipraktikum

include("regpoly.jl")
include("regpower.jl")
include("regulaFalsi.jl")
include("rekons.jl")
include("rekursif.jl")
include("richardson.jl")
include("rkf45.jl")
include("romberg.jl")
include("rungekutta.jl")
include("rungekuttasistem.jl")
include("secant.jl")
include("taylor.jl")
include("adaptif.jl")
include("backsub.jl")
include("bedaPusat.jl")
include("bisection.jl")
include("conGrad.jl")
include("dirichlet.jl")
include("elimGaussNonPivoting.jl")
include("elimGaussWithPivoting.jl")
include("euler.jl")
include("findiff.jl")
include("fixpoint.jl")
include("gaussSeidel.jl")
include("gelombang.jl")
include("heun.jl")
include("jacobi.jl")
include("kompsimp.jl")
include("komptrap.jl")
include("lagrange.jl")
include("linearshooting.jl")
include("LUdenganP.jl")
include("LUtanpaP.jl")
include("newton.jl")
include("newtonRaphson.jl")
include("panas.jl")
include("reglin.jl")
include("mulaipraktikum.jl")

end