################################
# BIBLIOTECA JULIA CalcNumCTEC #
################################

using LinearAlgebra
using Printf
using Base

function Derivada_MDFCentrada(f::Function, x₀::Number, i::Int; h::Number=10^-4, n::Int=9)
    x = x₀-((n-1)/2)*h:h:x₀+((n-1)/2)*h

    A = zeros(n,n)
    for i in range(1,stop=n)
        for j in range(1,stop=n)
            A[i,j] = x[i]^(n-j)
        end
    end

    coefs = A \ f.(x)

    function dif_f_interp(x_in)
        res = 0
        for k in 1:n-i
            res += coefs[k] * ((factorial(n-k))/(factorial(n-k-i))) * x_in^(n-k-i)
        end
        return res
    end

    return dif_f_interp(x₀)
end

# Cálculo de zeros de funções ------------------------------------------------------------------------------

function Zeros_Bissecao(f::Function, a::Number, b::Number;
    tol::Number=10^-12, klim::Number=10^6)
    if f(a) * f(b) < 0
        k = 0
        x = (a + b) / 2

        while abs(f(x)) > tol
            if k == klim
                @assert false "ERRO: O método não convergiu para $klim iterações..."
            else
                k += 1
            end

            x = (a + b) / 2
            if f(x) * f(a) < 0
                b = x
            else
                a = x
            end
        end

        return x
    else
        @assert false "\n\nERRO NA FUNÇÃO ZeroBissecao:\nOs valores da função nos extremos do intervalo dado\nprecisam ter valores de sinais opostos entre si\npara que a convergência do método seja garantida..."
    end
end

function Zeros_Cordas(f::Function, a::Number, b::Number;
    tol::Float64=10^-12, klim::Int64=10^6)
    if f(a) * f(b) < 0
        k = 0
        x = (a * f(b) - b * f(a)) / (f(b) - f(a))

        while abs(f(x)) > tol
            if k == klim
                @assert false "ERRO: O método não convergiu para $klim iterações..."
            else
                k += 1
            end

            x = (a + b) / 2
            if f(x) * f(a) < 0
                b = x
            else
                a = x
            end
        end

        return x
    else
        @assert false "ERRO NA FUNÇÃO ZeroCordas:\nOs valores da função nos extremos do intervalo dado",
        "\nprecisam ter valores de sinais opostos entre si",
        "\npara que a convergência do método seja garantida..."
    end
end

function Zeros_NR(f::Function, x::Number;
    tol::Float64=10^-12, klim::Int64=10^6)
    k = 0
    while abs(f(x)) > tol
        if k == klim
            @assert false "ERRO NA FUNÇÃO ZeroNR:\nO método não convergiu para $klim iterações..."
        else
            k += 1
        end

        if abs(Derivada_MDFCentrada(f, x, 1, n=5)) < tol
            @assert false "ERRO NA FUNÇÃO ZeroNR:\nO ponto x=$x possui derivada nula..."
        end
        x = x - (f(x) / Derivada_MDFCentrada(f, x, 1, n=5))
    end

    return x
end

# Resolução de sistemas de equações lineares --------------------------------------------------------------

function SEL_EliminGauss(A::Matrix, b::Vector; tol::Float64=10^-12)
    (m,n) = size(A)

    if m==n && m == length(b)
        if abs(det(A)) < tol
            @assert false "ERRO na função EliminGauss:\nA matriz é singular..."
        else
            function test(M)
                aux = 0
                for j in range(1, stop = n)
                    for i in range(1, stop = n)
                        if i > j
                            aux += abs(M[i,j])
                        end
                    end
                end
                return aux
            end

            AG = copy(A)
            bG = copy(b)

            for j in 1:n
                i = j
                while A[j,j] == 0
                    i+=1

                    aux_A = AG[j,:]
                    AG[j,:] = AG[i,:]
                    AG[i,:] = aux_A

                    aux_b = b[j]
                    bG[j] = bG[i]
                    bG[i] = aux_b
                    if (i,j) == (m,n)
                        @assert false "ERRO na função EliminGauss:\nA matriz é singular..."
                    end
                end
            end

            i = 1
            j = 1
            k = 0
            while test(AG) > tol
                k += 1
                if i == n
                    j += 1
                    i = j
                end
                i += 1

                bG[i] -= bG[j] * (AG[i,j] / AG[j,j])
                AG[i,:] -= AG[j,:] * (AG[i,j] / AG[j,j])
            end

            x = zeros(n)
            for k in n:-1:1
                x[k] = (bG[k] - dot(AG[k,:], x)) / AG[k,k]
            end

            return x
        end
    else
        @assert false "ERRO NA FUNÇÃO ElimGauss:\nInconsistência nas dimensões da matriz e do vetor..."
    end
end

function SEL_CritConverg(A::Matrix, modo::Char)
    (m,n) = size(A)

    if m == n
        if uppercase(modo) == 'L'
            beta=zeros(n)
            for i in range(1, stop = n)
                for j in range(1, stop = n)
                    beta[i] += abs(A[i,j])
                end
                beta[i] /= abs(A[i,i])
            end
            return max(beta...)
        elseif uppercase(modo) == 'C'
            beta=zeros(n)
            for j in range(1, stop = n)
                for i in range(1, stop = n)
                    beta[j] += abs(A[i,j])
                end
                beta[j] /= abs(A[j,j])
            end
            return max(beta...)
        elseif uppercase(modo) == 'S'
            beta = ones(n)
            for i in range(1, stop = n)
                for j in range(1, stop = n)
                    beta[i] += abs(A[i,j]) * beta[j]
                end
                beta[i] -= 1
                beta[i] /= abs(A[i,i])
            end
            return max(beta...)
        end
    else
        @assert false "\n\nERRO NA FUNÇÃO CritConverg:\nA matriz dada não é quadrada..."
    end
end

function SEL_GaussJacobi(A::Matrix, b::Vector; x_in::Vector=zeros(length(b)),
    tol::Float64=1e-12, klim::Int=10^6)
    # Rseolve o sistema linear A*x=b através de iterações pelo Método da Gauss-Jacobi.
    # Tolerância padrão: 10^(-12)
    # Métrica de erro: eₐ = ||x_out - x_in|| / ||x_out||
    # Retorno da (x_out, err)

    (m,n) = size(A)

    if m==n && m == length(b)
        if abs(det(A)) < tol
            @assert false "ERRO na função GaussJacobi:\nA matriz é singular..."
        else
            for j in 1:n
                i = j
                while A[j,j] == 0
                    i+=1

                    aux_A = A[j,:]
                    A[j,:] = A[i,:]
                    A[i,:] = aux_A

                    aux_b = b[j]
                    b[j] = b[i]
                    b[i] = aux_b
                    if (i,j) == (m,n)
                        @assert false "ERRO na função EliminGauss:\nA matriz é singular..."
                    end
                end
            end

            function err(x1, x2)
                return norm(x2 - x1) / norm(x2)
            end

            k = 1
            x_out = zeros(n)
            for i in range(1, stop = n)
                x_out[i] = (b[i] - dot(A[i,1:i-1], x_in[1:i-1]) - dot(A[i,i+1:n], x_in[i+1:n])) / A[i,i]
            end

            while err(x_in, x_out) > tol
                if k == klim
                    @assert false "ERRO NA FUNÇÃO GaussJacobi:\nO método não convergiu para $klim iterações\ne tolerância de $tol..."
                else
                    k += 1
                end

                x_in = copy(x_out)

                for i in range(1, stop = n)
                    x_out[i] = (b[i] - dot(A[i,1:i-1], x_in[1:i-1]) - dot(A[i,i+1:n], x_in[i+1:n])) / A[i,i]
                end
            end

            return x_out
        end
    else
        @assert false "ERRO NA FUNÇÃO GaussJacobi:\nInconsistência nas dimensões da matriz e do vetor..."
    end
end

function SEL_GaussSeidel(A::Matrix, b::Vector; x_in::Vector=zeros(length(b)),
    tol::Float64=1e-12, klim::Int=10^6)
    # Rseolve o sistema linear A*x=b através de iterações pelo Método da Gauss-Seidel.
    # Tolerância padrão: 10^(-12)
    # Métrica de erro: eₐ = ||x_out - x_in|| / ||x_out||
    # Retorno da (x_out, err)

    (m,n) = size(A)

    if m==n && m == length(b)
        if abs(det(A)) < tol
            @assert false "\n\nERRO na função GaussJacobi:\nA matriz é singular...\n\n"
        else
            for j in 1:n
                i = j
                while A[j,j] == 0
                    i+=1

                    aux_A = A[j,:]
                    A[j,:] = A[i,:]
                    A[i,:] = aux_A

                    aux_b = b[j]
                    b[j] = b[i]
                    b[i] = aux_b
                    if (i,j) == (m,n)
                        @assert false "ERRO na função GaussSeidel:\nA matriz é singular..."
                    end
                end
            end

            function err(x1, x2)
                return norm(x2 - x1) / norm(x2)
            end

            k = 1
            x_out = zeros(n)
            for i in range(1, stop = n)
                x_out[i] = (b[i] - dot(A[i,1:i-1], x_out[1:i-1]) - dot(A[i,i+1:end], x_in[i+1:end])) / A[i,i]
            end

            while err(x_in, x_out) > tol
                if k == klim
                    @assert false "ERRO NA FUNÇÃO GaussSeidel:\nO método não convergiu para $klim iterações\ne tolerância de $tol..."
                else
                    k += 1
                end

                x_in = copy(x_out)

                for i in range(1, stop = n)
                    x_out[i] = (b[i] - dot(A[i,1:i-1], x_out[1:i-1]) - dot(A[i,i+1:end], x_in[i+1:end])) / A[i,i]
                end
            end

            return x_out
        end
    else
        printstyled("ERRO NA FUNÇÃO GaussJacobi:\nInconsistência nas dimensões da matriz e do vetor...\n\n", color=:bold)
    end
end

# Resolução de sistemas de equações não lineares ---------------------------------------------------------

function SENL_NRSecante(F::Function, x_in::Vector;
    tol::Float64=1e-6, klim::Int64=10^3)
    # Rseolve o sistema não linear F(x)=0 através de iterações pelo Método de Newton-Raphson Secante.
    # Ou seja, a Jacobiana é calculada de maneira aproximada em função da tolerância.
    # Tolerância padrão: 10^(-12)

    function J_aprox(x, δ=tol)
        n = length(x)
        Jap = zeros(n,n)

        for j in range(1, stop = n)
            δx = zeros(n)
            δx[j] = δ * x[j]
            for i in range(1, stop = n)
                Jap[i,j] = (F(x + δx)[i] - F(x)[i]) / δx[j]
            end
        end

        return Jap
    end

    if length(F(x_in)) == length(x_in)
        err(x_in, x_out) = norm(x_out - x_in) / norm(x_out)

        k = 1
        x_out = x_in - J_aprox(x_in) \ F(x_in)

        while err(x_in, x_out) > tol
            if k==klim
                @assert false "\n\nERRO na função NR_Secante:\nO método não convergiu para $klim iterações\ne tolerância de $tol...\n\n"
            else
                k+=1
            end

            x_in = copy(x_out)
            x_out = x_in - J_aprox(x_in) \ F(x_in)
        end

        return x_out
    else
        printstyled("\n\nERRO na função NR_Secante:\nOs vetores possuem dimensões diferentes...\n\n", color=:bold)
        return nothing
    end
end

# Interpolação e ajuste ------------------------------------------------------------------------------

function Interpolacao_Polinomial(x::Vector, y::Vector)
    # Calcula e retorna o polinômio interpolador para os dados de entrada via polinômios de Lagrange.
    # O grau do polinômio é determinado pelas dimensões dos vetores x e y.

    n = Int(length(x))

    if n==length(y)
        function f_interpoladora(x_in)
            res = 0
            for i in range(1, stop=n)
                aux = 1
                for j in range(1, stop=n)
                    if i != j
                        aux *= (x_in - x[j])/(x[i] - x[j])
                    end
                end
                res += y[i] * aux
            end
            return res
        end

        return f_interpoladora
    else
        @assert false "ERRO NA FUNÇÃO Interpolacao_Polinomial:\nOs vetores de entrada possuem dimensões diferentes..."
    end
end

function Interpolacao_Polinomial(f::Function, x::Vector)
    # Calcula e retorna o polinômio interpolador para os dados de entrada via polinômios de Lagrange.
    # O grau do polinômio é determinado pela dimensão do vetor x.

    n = Int(length(x))

    function f_interpoladora(x_in)
        res = 0
        for i in range(1, stop=n)
            aux = 1
            for j in range(1, stop=n)
                if i != j
                    aux *= (x_in - x[j])/(x[i] - x[j])
                end
            end
            res += f(x[i]) * aux
        end
        return res
    end

    return f_interpoladora
end

function Interpolacao_Bilinear(x::Vector, y::Vector, z::Vector)
    if length(x) == length(y) == length(z) == 4
        M = [ones(n) x y x.*y]
        b = M \ z
        g(x,y) = dot(b,[1 x y x*y])
        return g
    else
        @assert false "\n\nERRO NA FUNÇÃO Interpolacao_Bilinear:\nOs vetores de entrada precisam ter dimensões de 4 elementos..."
    end
end

function Ajuste_MinimosQuadrados(x::Vector, y::Vector; grau::Int64=1)
    # Calcula e retorna o polinômio ajustado para os dados de entrada via Método dos Mínimos Quadrados.
    # O grau padrão do polinômio é 1.
    # Retorno da função: (polinomio, r²)
    if length(x) != length(y)
        @assert false "ERRO NA FUNÇÃO MinimosQuadrados:\nOs vetores de entrada possuem dimensões diferentes..."
    else
        A = zeros(grau+1, grau+1)
        for i in 0:grau
            for j in 0:grau
                A[i+1,j+1] = sum(x.^(i+j))
            end
        end

        b = zeros(grau+1)
        for i in 0:grau
            b[i+1] = sum(y.*(x.^i))
        end

        coefs = A \ b
    end

    function f_ap(x_in)
        xᵏ = []
        for i in 0:grau
            push!(xᵏ, x_in^i)
        end
        return dot(coefs, xᵏ)
    end

    Sr = sum((y - f_ap.(x)).^2)
    ym = sum(y)/length(y)
    St = sum((y .- ym).^2)
    r² = 1 - (Sr/St)

    return f_ap, r²
end

# Integração Numérica ------------------------------------------------------------------

# OBS: COMPLETAR O ERRO COM A DIFERENCIAÇÃO NUMÉRICA

function Integral_Trapezio(f::Function, x::Tuple; tol::Float64 = 1e-12)
    if length(x) == 2
        a = x[1]
        b = x[2]
        c = (a+b)/2

        T(a,b) = (b-a) * (f(a) + f(b)) / 2

        if T(a,b) - T(a,c) - T(c,b) < tol
            return T(a,b)
        else
            return Integral_Trapezio(f, (a,c), tol=tol) + Integral_Trapezio(f, (c,b),tol=tol)
        end
    else
        @assert false "ERRO NA FUNÇÃO Intergral_Trapezio:\nO intervalo precisa ter apenas 2 valores..."
    end
end

function Integral_Trapezio(x::Vector, y::Vector)
    n = length(x)

    if n == length(y)
        I = 0
        for i in 1:n-1
            I += ((x[i+1]-x[i])/2)*(y[i] + y[i+1])
        end

        #err = abs(-(b-a)^4 * (IntegSimpson(dif_f4, a, b, n=10^3))[1] / (180*n^4))

        return I#, err
    else
        @assert false "ERRO NA FUNÇÃO Integral_Trapezio:\nOs vetores possuem dimensões diferentes..."
    end
end

function IntegralDupla_Trapezio(f::Function, x::Tuple, y::Tuple; tol::Float64=1e-12)
    if length(x) == length(y) == 2
        x₀, x₁ = x
        y₀, y₁ = y
        xₘ = (x₀ + x₁) / 2
        yₘ = (y₀ + y₁) / 2

        function T(x,y)
            return Integral_Trapezio([y...], [Integral_Trapezio([x...], f.([x...], [y[1],y[1]])),Integral_Trapezio([x...], f.([x...], [y[2],y[2]]))])
        end

        if T(x,y) - T((x₀,xₘ),y) - T((xₘ,x₁),y) < tol
            if T(x,y) - T(x,(y₀,yₘ)) - T(x,(yₘ,y₁)) < tol
                println("teste1")
                return T(x,y)
            else
                println("teste2")
                return IntegralDupla_Trapezio(f, x, (y₀,yₘ), tol=tol) + IntegralDupla_Trapezio(f, x, (yₘ,y₁), tol=tol)
            end
        elseif T(x,y) - T(x,(y₀,yₘ)) - T(x,(yₘ,y₁)) < tol
            println("teste3")
            return IntegralDupla_Trapezio(f, (x₀,xₘ), y, tol=tol) + IntegralDupla_Trapezio(f, (xₘ,x₁), y, tol=tol)
        else
            println("teste4")
            return IntegralDupla_Trapezio(f, (x₀,xₘ), (y₀,yₘ), tol=tol) + IntegralDupla_Trapezio(f, (xₘ,x₁), (y₀,yₘ), tol=tol) + IntegralDupla_Trapezio(f, (x₀,xₘ), y, tol=tol) + IntegralDupla_Trapezio(f, (xₘ,x₁), y, tol=tol)
        end
    else
        @assert false "ERRO NA FUNÇÃO IntegralDupla_Trapezio:\nOs intervalos precisam ter apenas 2 valores..."
    end
end

function Integral_Simpson(f::Function, x::Tuple; tol::Float64 = 1e-12)
    if length(x) == 2
        a = x[1]
        b = x[2]
        c = (a+b)/2

        S(a,b) = (b-a) * (f(a) + 4*f((a+b)/2) + f(b)) / 6

        if S(a,b) - S(a,c) - S(c,b) < tol
            return S(a,b)
        else
            return Integral_Simpson(f, (a,c), tol=tol) + Integral_Simpson(f, (c,b), tol=tol)
        end
    else
        @assert false "ERRO NA FUNÇÃO Integral_Simpson:\nO intervalo precisa ter apenas 2 valores..."
    end
end

function Integral_Simpson(x::Vector, y::Vector)
    n = length(x)

    if n == length(y)
        if n%2==1
            I = 0
            for i in 1:2:n-2
                I += ((x[i+2]-x[i])/6)*(y[i] + 4*y[i+1] + y[i+2])
            end

            return I
        else
            @assert false "ERRO NA FUNÇÃO Integral_Simpson:\nO número de elementos dos vetores deve ser ímpar..."
        end
    else
        @assert false "ERRO NA FUNÇÃO IntegSimpson:\nOs vetores possuem dimensões diferentes..."
    end
end

# function IntegralDupla_Simpson(f::Function, x1::Number, x2::Number,
#     y1::Number, y2::Number; nx::Number=(b-a)*10^3, ny::Number=(d-c)*10^3)

#     hx = (x2-x1)/2nx
#     hy = (y2-y1)/2ny

#     M = zeros(2nx+1, 2ny+1)
#     for (j,y) in enumerate(y1:hy:y2)
#         for (i,x) in enumerate(x1:hx:x2)
#             M[i,j] = f(x,y)
#         end
#     end

#     V = zeros(2ny+1)
#     for k in 1:2ny+1
#         V[k] += Integral_Simpson(Vector(x1:hx:x2), M[:,k])
#     end

#     return Integral_Simpson(Vector(y1:hy:y2), V)
# end

# function IntegralDupla_Simpson(f::Function, x₀::Number, x₁::Number, y₀::Number, y₁::Number; tol::Float64=1e-12)
#     S(a,b) = (b-a) * (f(a) + 4*f((a+b)/2) + f(b)) / 6

#     xₘ = (x₀ + x₁) / 2
#     yₘ = (y₀ + y₁) / 2

#     if

#     else

#     end
# end

function IntegralTripla_Simpson(f::Function, x1::Number, x2::Number, y1::Number, y2::Number,
    z1::Number, z2::Number; nx::Number=(x2-x1)*100, ny::Number=(y2-y1)*100, nz::Number=(z2-z1)*100)

    hx = (x2-x1)/2nx
    hy = (y2-y1)/2ny
    hz = (z2-z1)/2nz

    I = []

    M = Matrix{Number}(undef, 2ny+1, 2nz+1)
    for (k,z) in enumerate(z1:hz:z2)
        for (j,y) in enumerate(y1:hy:y2)
            for (i,x) in enumerate(x1:hx:x2)
                M[i,j] = f(x,y,z)
            end
        end
        # println(M)
        V = zeros(2nx+1)
        for k in 1:2nx+1
            V[k] = Integral_Simpson(Vector(x1:hx:x2), M[:,k])
        end
        # println(V)
        push!(I, Integral_Simpson(Vector(y1:hy:y2), V))
    end
    res = Integral_Simpson(Vector(z1:hz:z2), I)
    return res

    # M = Matrix{Number}(undef, 2nx+1, 2ny+1)
    # for (j,y) in enumerate(y1:hy:y2)
    #     for (i,x) in enumerate(x1:hx:x2)
    #         M[i,j] = Integral_Simpson(Vector(x1:hx:x2), S[i,j,:])
    #     end
    # end

    # V = zeros(2ny+1)
    # for k in 1:2ny+1
    #     V[k] = Integral_Simpson(Vector(x1:hx:x2), M[:,k])
    # end

    # return Integral_Simpson(Vector(y1:hy:y2), V)
end

function IntegralTripla_Trapezio(f::Function, x1::Number, x2::Number, y1::Number, y2::Number,
    z1::Number, z2::Number; nx::Int=10^3, ny::Int=10^3, nz::Int=10^3)

    hx = (x2-x1)/nx
    hy = (y2-y1)/ny
    hz = (z2-z1)/nz

    I = []

    M = Matrix{Number}(undef, ny+1, nz+1)
    for (k,z) in enumerate(z1:hz:z2)
        for (j,y) in enumerate(y1:hy:y2)
            for (i,x) in enumerate(x1:hx:x2)
                M[i,j] = f(x,y,z)
            end
        end
        # println(M)
        V = zeros(nx+1)
        for k in 1:nx+1
            V[k] = Integral_Trapezio(Vector(x1:hx:x2), M[:,k])
        end
        # println(V)
        push!(I, Integral_Trapezio(Vector(y1:hy:y2), V))
    end
    return Integral_Trapezio(Vector(z1:hz:z2), I)

    # M = Matrix{Number}(undef, 2nx+1, 2ny+1)
    # for (j,y) in enumerate(y1:hy:y2)
    #     for (i,x) in enumerate(x1:hx:x2)
    #         M[i,j] = Integral_Simpson(Vector(x1:hx:x2), S[i,j,:])
    #     end
    # end

    # V = zeros(2ny+1)
    # for k in 1:2ny+1
    #     V[k] = Integral_Simpson(Vector(x1:hx:x2), M[:,k])
    # end

    # return Integral_Simpson(Vector(y1:hy:y2), V)
end

# Geometria -----------------------------------------------------------------------------------

struct Vertex2d
    x::Number
    y::Number

    function Vertex2d(x::Number, y::Number)

    end

    function Base.print(v::Vertex2d)
        print("(", v.x, ", ", v.y, ")")
    end
    function Base.println(v::Vertex2d)
        println("(", v.x, ", ", v.y, ")")
    end
end

struct Vertex3d
    x::Number
    y::Number
    z::Number

    function Vertex3d(v::Vertex2d,z::Number)
        return Vertex3d(v.x, v.y, z)
    end

    function Base.print(v::Vertex3d)
        print("(", v.x, ", ", v.y, ", ", v.z, ")")
    end
    function Base.println(v::Vertex3d)
        println("(", v.x, ", ", v.y, ", ", v.z, ")")
    end
end

"CalcNumCTEC incluída com sucesso!"


```Write a function that takes a list of numbers, a starting base b1 and a target
base b2 and interprets the list as a number in base b1 and converts it into a number
in base b2 (in the form of a list-of-digits). So for example [2,1,0] in base 3 gets
converted to base 10 as [2,1].```

list = [2,1,0]

function base_3_10(x::Vector{Int64})
    num = zeros(Int64, size(x,1))
    for (i,j) in enumerate(x)
        num[i] += x[i] * abs(3 ^ i-1)
    end
    return sum(num)
end

println(" $(base_3_10(list)) base 10")


using JLD2, Random, LinearAlgebra, Statistics, CSV, DataFrames, FreqTables, Latexify, LaTeXStrings

######### Question 1 ################

# 1-Initializing variables and practice with basic matrix operations

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

#Create the following four matrices of random numbers, setting the seed to ’1234’ .
#Name the matrices and set the dimensions as noted

# create 10x7 distruburted U[-5,10]
Random.seed!(1234)
A = rand(10,7)  .* (10 - (-5)) .- 5

# create 10x7 distruburted random numbers distributed N (−2, 15) [st dev is 15]
Random.seed!(1234)
B = (-2) .+ (randn(10,7) * 15)

# create C 5×7 - the first 5 rows and first 5 columns of A and the last two columns and first 5 rows of B
sub_A = A[1:5,1:5]; sub_B = B[1:5,6:7]
C = hcat(sub_A, sub_B)

### D 10×7 - where Di,j = Ai,j if Ai,j ≤ 0, or 0 otherwise
D = similar(A)

for i in 1:10, j in 1:7
    if A[i,j] <= 0
        D[i,j]=A[i,j]
    else
        D[i,j] = 0
    end
end

display(D)

# or D = A .* (A .≤ 0)

## Use a built-in Julia function to list the number of elements of A

for i in 1:10, j in 1:7
    println(A[i,j])
end

## Use a series of built-in Julia functions to list the number of unique elements of D

vector_D = D[:]; unique_D = unique(vector_D);
println(unique_D)
print(length(unique_D))

## Using the reshape() function, create a new matrix called E which is the ‘vec’ operator applied to B. Can you find an easier way to accomplish this?

E = reshape(B,:,1)

# easier way
# E  = B[:]

## Create a new array called F which is 3-dimensional and contains A in the first column of the third dimension and B in the second column of the third dimension

F = cat(A, B, dims=3)
# size(F)

## Use the permutedims() function to twist F so that it is now F2×10×7 instead of F10×7×2. Save this new matrix as F.

# permutedims(F, [2, 10, 7])
F = reshape(F,(2,10,7))

## Create a matrix G which is equal to B ⊗ C (the Kronecker product of B and C). What happens when you try C ⊗ F ?

G = kron(B,C);

# kron(C, F)  # Doesn't work because they are of different # dimensions

## Save the matrices A, B, C, D, E, F and G as a .jld file named matrixpractice.

matrixpractice = Dict("A"=> A, "B"=> B, "C"=> C, "D"=> D, "E"=> E, "F"=> F, "G"=> G)
@save("matrixpractice.jld", matrixpractice)

## Save only the matrices A, B, C, and D as a .jld file called firstmatrix.

firstmatrix = Dict("A"=> A, "B"=> B, "C"=> C, "D"=> D)
@save("firstmatrix.jld", firstmatrix)

## Export C as a .csv file called Cmatrix. You will first need to transform C into a DataFrame.

typeof(C)
Cmatrix = convert(DataFrame, C)
CSV.write("Cmatirx.csv", Cmatrix)

## Export D as a tab-delimited .dat file called Dmatrix. You will first need to transform D into a DataFrame.

typeof(D)
Dmatrix = convert(DataFrame, D)
@save("Dmatrix.dat", Dmatrix, delim='\t')

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

# Wrap a function definition around all of the code for question 1.
# Call the function q1(). The function should have 0 inputs and should output the arrays
# A, B, C and D. At the very bottom of your script you should add the code A,B,C,D = q1().

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

function q1()
    # 1.a
    Random.seed!(1234);

    A = rand(10,7)  .* (10 - (-5)) .- 5;

    B = (-2) .+ (randn(10,7) * 15);

    C = zeros(5, 7);

    sub_A = A[1:5,1:5]; sub_B = B[1:5,6:7];
    C = hcat(sub_A, sub_B);

    D = A .* (A .≤ 0);

    D = similar(A);
    for i in 1:10, j in 1:7
        if A[i,j] <= 0
            D[i,j]=A[i,j]
        else
            D[i,j] = 0
        end
    end

    for i in 1:10, j in 1:7
        println(A[i,j])
    end

    vector_D = D[:]; unique_D = unique(vector_D);
    print(length(unique_D))

    E = reshape(B,:,1);

    F = cat(A, B, dims=3);
    F = reshape(F,(2,10,7));

    G = kron(B,C);

    matrixpractice = Dict("A"=> A, "B"=> B, "C"=> C, "D"=> D, "E"=> E, "F"=> F, "G"=> G);
    @save("matrixpractice.jld", matrixpractice)

    Cmatrix = convert(DataFrame, C);
    CSV.write("Cmatirx.csv", Cmatrix)

    Dmatrix = convert(DataFrame, D);
    @save("Dmatrix.dat", Dmatrix, delim='\t')

    return A, B, C, D
end

A,B,C,D = q1()

############## Question 2 ###############

# Practice with loops and comprehensions

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

# Write a loop or use a comprehension that computes the element-by-element product of A and B.
# Name the new matrix AB. Create a matrix called AB2 that accomplishes this ask without a loop or comprehension.

AB = zeros(size(A)...);

for i = 1:size(A)[1], j = 1:size(A)[2]
    AB[i, j] = A[i, j] * B[i, j];
end

AB2 = A .* B;

# Write a loop that creates a column vector called Cprime which contains only
#the elements of C that are between -5 and 5 (inclusive). Create a vector called
# Cprime2 which does this calculation without a loop.

Cprime = Vector{Float64}();
for item in eachrow(C), i in item
    if   -5 ≤  i ≤ 5
        append!(Cprime,i)
    end
end

Cprime2 = C[-5 .≤ C .≤ 5];

# Using loops or comprehensions, create a 3-dimensional array called X that is of
# dimen- sionN×K×TwhereN=15,169,K=6,andT=5. Forallt,thecolumnsofX should be (in order)

N = 15_169;
K = 6;
T = 5;
X = zeros(N, K, T);
binord(;n=20, p=0.6) = sum(rand(n) .≤ p);

for t in 1:T
    X[:, 1, t] .= 1;
    X[:, 2, t] = rand(N, 1) .≤ 0.75 * (6 - t) / 5;
    X[:, 3, t] = randn(N, 1) .* 5(t - 1) .+ (15 + t - 1);
    X[:, 4, t] = randn(N, 1) .* (1 / ℯ) .+ π * (6 - t) / 3;
    X[:, 5, t] = [binord() for _ in 1:N];
    X[:, 6, t] = [binord(p=0.5) for _ in 1:N];
end

# Use comprehensions to create a matrix β which is K × T and whose elements evolve across time in the following fashion:

β = zeros(K, T);
β[1, :] = [1 + 0.25(t - 1) for t in 1:T];
β[2, :] = [log(t) for t in 1:T];
β[3, :] = [-sqrt(t) for t in 1:T];
β[4, :] = [exp(t) - exp(t + 1) for t in 1:T];
β[5, :] = [t for t in 1:T];
β[6, :] = [t / 3 for t in 1:T];

# Use comprehensions to create a matrix Y which is N × T defined by Yt = Xt βt + εt , iid

ε = Random.randn(N, T) .* 0.36;
Y = [sum(X[n, :, t] .* β[:, t]) + ε[n, t] for n in 1:N, t in 1:T];

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

# Wrap a function definition around all of the code for question 2.
# Call the function q2(). The function should have take as inputs the arrays A, B and C.
# It should return nothing. At the very bottom of your script you should add the code q2(A,B,C,D).
#Make sure q2() gets called after q1()!

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

function q2(A, B, C, D)

    AB = zeros(size(A)...);

    for i = 1:size(A)[1], j = 1:size(A)[2]
        AB[i, j] = A[i, j] * B[i, j];
    end

    AB2 = A .* B;

    Cprime = Vector{Float64}();

    for item in eachrow(C), i in item
        if   -5 ≤  i ≤ 5
            append!(Cprime,i)
        end
    end

    Cprime2 = C[-5 .≤ C .≤ 5];

    N = 15_169;
    K = 6;
    T = 5;

    X = zeros(N, K, T);
    Random.seed!(1234)
    binord(;n=20, p=0.6) = sum(rand(n) .≤ p);

    for t in 1:T
        X[:, 1, t] .= 1;
        X[:, 2, t] = rand(N, 1) .≤ 0.75 * (6 - t) / 5;
        X[:, 3, t] = randn(N, 1) .* 5(t - 1) .+ (15 + t - 1);
        X[:, 4, t] = randn(N, 1) .* (1 / ℯ) .+ π * (6 - t) / 3;
        X[:, 5, t] = [binord() for _ in 1:N];
        X[:, 6, t] = [binord(p=0.5) for _ in 1:N];
    end

    β = zeros(K, T);
    β[1, :] = [1 + 0.25(t - 1) for t in 1:T];
    β[2, :] = [log(t) for t in 1:T];
    β[3, :] = [-sqrt(t) for t in 1:T];
    β[4, :] = [exp(t) - exp(t + 1) for t in 1:T];
    β[5, :] = [t for t in 1:T];
    β[6, :] = [t / 3 for t in 1:T];

    ε = randn(N, T) .* 0.36;
    Y = [sum(X[n, :, t] .* β[:, t]) + ε[n, t] for n in 1:N, t in 1:T];

    return nothing
end

q2(A, B, C, D)

############## Question 3 ###############

# Reading in Data and calculating summary statistics

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

# Clear the workspace and import the file nlsw88.csv into Julia as a DataFrame.

data = CSV.read("/Users/firatmelihyilmaz/fall-2020/ProblemSets/PS1-julia-intro/nlsw88.csv")
describe(data)
@save("nlsw88.jld", data)

#  What percentage of the sample has never been married? What percentage are college graduates?

ptc_never_married = (sum(data["never_married"])/(sum(data["never_married"]) + sum(data["married"]))) * 100;
println("Percentage percentage of the sample has never been married: $ptc_never_married %")

# Use the tabulate command to report what percentage of the sample is in each race category

races = prop(freqtable(data["race"]));
# (prop ∘ freqtable)(data["race"])

# Use the describe() function to create a matrix called summarystats which lists the mean,
# median, standard deviation, min, max, number of unique elements, and in- terquartile range
# (75th percentile minus 25th percentile) of the data frame. How many grade observations are missing?

summarystats = describe(data);

# Show the joint distribution of industry and occupation using a cross-tabulation.

joint_table = freqtable(data, :industry, :occupation);

# Wrap a function definition around all of the code for question 3.
# Call the function q3(). The function should have no inputs and no outputs.
# At the very bottom of your script you should add the code q3().

function q3()
    data = CSV.read("/Users/firatmelihyilmaz/fall-2020/ProblemSets/PS1-julia-intro/nlsw88.csv");
    @save("nlsw88.jld", data)
    ptc_never_married = (sum(data["never_married"])/(sum(data["never_married"]) + sum(data["married"]))) * 100;
    races = prop(freqtable(data["race"]));
    summarystats = describe(data);
end

############## Question 4 ###############

# Practice with functions

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

# Load firstmatrix.jld

@load("firstmatrix.jld")

# Write a function called matrixops that takes as inputs the matrices A and B
# from question (a) of problem 1 and has three outputs: (i) the element-by-element product of the inputs,
# (ii) the product A′B, and (iii) the sum of all the elements of A + B.

function matrixops(A, B)
    i = A .+ B
    ii = A' * B
    iii = sum(A + B)

    return i, ii, iii
end

matrixops(A,B);

function matrixops(A, B)
    """
    Takes  matrix A and B as inputs and returns following outputs:

    i = A .+ B
    ii = A' * B
    iii = sum(A + B)
    """
    i = A .+ B
    ii = A' * B
    iii = sum(A + B)

    return i, ii, iii
end

function matrixops(A, B)
    """
    Takes  matrix A and B as inputs and returns following outputs:

    i = A .+ B
    ii = A' * B
    iii = sum(A + B)
    """
    if size(A) != size(B)
        throw(DimensionMismatch("inputs must have the same size."))
    i = A .+ B
    ii = A' * B
    iii = sum(A + B)

    return i, ii, iii
end


#=
Please see
https://computationalmindset.com/en/neural-networks/ordinary-differential-equation-solvers.html#sys1
for details
=#

#=Original problem
x′ + x − y = 0
y′ − 4x + y = 0
x(0)=2
y(0)=0
=#

#=Esplicit form of equations
x′ = y - x
y′ = 4x - y
=#

#=Analytical solution
x(t) = e^t + e^−3t
y(t) = 2e^t − 2e^−3t
=#

using DifferentialEquations
using Plots

function ode_fn(du,u,p,t)
    x, y = u
    du[1] = y - x
    du[2] = 4.0 * x - y
end

an_sol_x(t) = exp(t) + exp(-3.0 * t)
an_sol_y(t) = 2.0 * exp(t) - 2.0 * exp(-3.0 * t)

t_begin=0.0
t_end=5.0
tspan = (t_begin,t_end)
x_init=2.0
y_init=0.0

prob = ODEProblem(ode_fn, [x_init, y_init], tspan)
num_sol = solve(prob, Tsit5(), reltol=1e-8, abstol=1e-8)
x_num_sol = [u[1] for u in num_sol.u]
y_num_sol = [u[2] for u in num_sol.u]

plot(num_sol.t, an_sol_x.(num_sol.t),
    linewidth=2, ls=:dash,
    title="System of 2 ODEs 1st order IVP solved by D.E. package",
    xaxis="t",
    label="analytical x",
    legend=true)
plot!(num_sol.t, an_sol_y.(num_sol.t),
    linewidth=2, ls=:dash,
    label="analytical y",
    legend=true)
plot!(num_sol.t, x_num_sol,
    linewidth=1,
    label="numerical x")
plot!(num_sol.t, y_num_sol,
    linewidth=1,
    label="numerical y")


# This file is a part of AstroLib.jl. License is MIT "Expat".
# Copyright (C) 2016 Mosè Giordano.

function _sphdist(long1::T, lat1::T, long2::T, lat2::T,
                  degrees::Bool) where {T<:AbstractFloat}
    # Convert both points to rectangular coordinates.
    rxy, z1 = polrec(1,   lat1,  degrees=degrees)
    x1, y1  = polrec(rxy, long1, degrees=degrees)
    rxy, z2 = polrec(1,   lat2,  degrees=degrees)
    x2, y2  = polrec(rxy, long2, degrees=degrees)
    # Compute vector dot product for both points.
    cs = x1*x2 + y1*y2 + z1*z2
    # Compute the vector cross product for both points.
    xc = y1*z2 - z1*y2
    yc = z1*x2 - x1*z2
    zc = x1*y2 - y1*x2
    sn = norm((xc, yc, zc))
    # Convert to polar coordinates.
    radius, angle = recpol(cs, sn)
    return degrees ? rad2deg(angle) : angle
end

"""
    sphdist(long1, lat1, long2, lat2[, degrees=true]) -> angular_distance

### Purpose ###

Angular distance between points on a sphere.

### Arguments ###

* `long1`:  longitude of point 1
* `lat1`: latitude of point 1
* `long2`: longitude of point 2
* `lat2`: latitude of point 2
* `degrees` (optional boolean keyword): if `true`, all angles, including the
  output distance, are assumed to be in degrees, otherwise they are all in
  radians.  It defaults to `false`.

### Output ###

Angular distance on a sphere between points 1 and 2, as an `AbstractFloat`.  It
is expressed in radians unless `degrees` keyword is set to `true`.

### Example ###

```jldoctest
julia> using AstroLib

julia> sphdist(120, -43, 175, +22)
1.5904422616007134
```

### Notes ###

* `gcirc` function is similar to `sphdist`, but may be more suitable for
  astronomical applications.

Code of this function is based on IDL Astronomy User's Library.
"""
sphdist(long1::Real, lat1::Real,
        long2::Real, lat2::Real; degrees::Bool=false) =
            _sphdist(promote(float(long1), float(lat1),
                             float(long2), float(lat2))... , degrees)


"""
Functions for area of various geometric shapes

Contributed By:- [Ashwani Rathee](https://github.com/ashwani-rathee)
"""

"""
    surfarea_cube(side)

Finds surface area of a cube

# Example

```julia
surfarea_cube(1)  # returns 6
surfarea_cube(3)  # returns 54
surfarea_cube(-1) # returns DomainError
```
"""
function surfarea_cube(side)
    if side < 0
        throw(DomainError(side, "surfarea_cube() only works for non-negative values"))
    end
    return 6(side^2)
end

"""
    surfarea_sphere(side)

Finds surface area of a sphere

# Example

```julia
surfarea_sphere(5)  # returns 314.1592653589793
surfarea_sphere(1)  # returns 12.566370614359172
surfarea_sphere(-1) # returns DomainError
```
"""
function surfarea_sphere(radius)
    if radius < 0
        throw(DomainError(radius, "surfarea_sphere() only works for non-negative values"))
    end
    return 4pi*(radius^2)
end

"""
    area_rectangle(length, width)

Finds area of the rectangle

# Example

```julia
area_rectangle(10,20) # returns 200
area_rectangle(-1,-2) # returns DomainError
area_rectangle(1,-2)  # returns DomainError
area_rectangle(-1,2)  # returns DomainError
```
"""
function area_rectangle(length, width)
    if length < 0 || width < 0
        throw(DomainError("area_rectangle() only works for non-negative values"))
    end
    return length * width
end

"""
    area_square(side)

Finds area of the area_square

# Example

```julia
area_square(10) # returns 100
area_square(-1) # returns DomainError
```
"""
function area_square(side)
    if side < 0
        throw(DomainError("area_square() only works for non-negative values"))
    end
    return side^2
end

"""
    area_triangle(base, height)

Finds area of the right angled triangle with base height

# Example

```julia
area_triangle(10,10) # returns 50.0
area_triangle(-1,-2) # returns DomainError
area_triangle(1,-2)  # returns DomainError
area_triangle(-1,2)  # returns DomainError
```
"""
function area_triangle(base, height)
    if base < 0 || height < 0
        throw(DomainError("area_triangle() only accepts non-negative values"))
    end
    return (base * height) / 2
end

"""
    area_heron_triangle(side1, side2, side3)

Finds area of a triangle using heron's formula

# Example

```julia
area_heron_triangle(5,12,13) # returns 30.0
area_heron_triangle(-1,-2,1) # returns DomainError
area_heron_triangle(1,-2,1)  # returns DomainError
area_heron_triangle(-1,2,1)  # returns DomainError
```
"""
function area_heron_triangle(side1, side2, side3)
    if side1 < 0 || side2 < 0 || side3 < 0
        throw(DomainError("area_triangle_three_sides() only accepts non-negative values"))
    elseif side1 + side2 < side3 || side1 + side3 < side2 || side2 + side3 < side1
        throw(DomainError("Given three sides do not form a triangle"))
    end
    semi_perimeter = (side1 + side2 + side3) / 2
    area = sqrt(
        semi_perimeter
        * (semi_perimeter - side1)
        * (semi_perimeter - side2)
        * (semi_perimeter - side3)
    )
    return area
end

"""
    area_parallelogram(base, height)

Finds area of the parallelogram

# Example

```julia
area_parallelogram(10,20) # returns 200
area_parallelogram(-1,-2) # returns DomainError
area_parallelogram(1,-2)  # returns DomainError
area_parallelogram(-1,2)  # returns DomainError
```
"""
function area_parallelogram(base, height)

    if base < 0 || height < 0
        throw(DomainError("area_parallelogram() only accepts non-negative values"))
    end
    return base * height
end

"""
area_trapezium(base1,base2,height)

Finds area of the trapizium

# Example

```julia
area_trapezium(10, 20, 30)  # returns 450.0
area_trapezium(-1, -2, -3)  # returns DomainError
area_trapezium(-1, 2, 3)    # returns DomainError
area_trapezium(1, -2, 3)    # returns DomainError
area_trapezium(1, 2, -3)    # returns DomainError
area_trapezium(-1, -2, 3)   # returns DomainError
area_trapezium(1, -2, -3)   # returns DomainError
area_trapezium(-1, 2, -3)   # returns DomainError
```
"""
function area_trapezium(base1,base2,height)
    if base1 < 0 || base2 < 0 || height < 0
        throw(DomainError("area_trapezium() only accepts non-negative values"))
    end
    return 1 / 2 * (base1 + base2) * height
end

"""
    area_circle(radius)

Finds area of the circle

# Example

```julia
area_circle(20) # returns 1256.6370614359173
area_circle(-1) # returns DomainError
```
"""
function area_circle(radius)
    if radius < 0
        throw(DomainError("area_circle() only accepts non-negative values"))
    end
    return pi * radius ^ 2
end

"""
    area_ellipse(radius_x, radius_y)

Finds area of the ellipse

# Example

```julia
area_ellipse(10, 10) # returns 314.1592653589793
area_ellipse(10, 20) # returns 628.3185307179587
area_ellipse(1, -2) # returns DomainError
area_ellipse(-1, 2) # returns DomainError
area_ellipse(-1, -2) # returns DomainError
```
"""
function area_ellipse(radius_x, radius_y)
    if radius_x < 0 || radius_y < 0
        throw(DomainError("area_ellipse() only accepts non-negative values"))
    end
    return pi * radius_x * radius_y
end

"""
    area_rhombus(diagonal_1, diagonal_2)

Finds area of the rhombus

# Example

```julia
area_rhombus(10, 20) # returns 100.0
area_rhombus(-1,-2) # returns DomainError
area_rhombus(1,-2)  # returns DomainError
area_rhombus(-1,2)  # returns DomainError
```
"""
function area_rhombus(diagonal_1, diagonal_2)
    if diagonal_1 < 0 || diagonal_2 < 0
        throw(DomainError("area_rhombus() only accepts non-negative values"))
    end
    return 1 / 2 * diagonal_1 * diagonal_2
end


module GeneralTridiagonal

export factorize!, solve!

"""
    factorize!(β, α, γ, δ, p)

Computes the LU factorization with partial pivoting for an `n×n` tridiagonal
matrix `A` with subdiagonal `β`, main diagonal `α` and superdiagonal `γ`.  Thus,
the vector `α` has length `n`, whereas `β` and `γ` each have length `n-1`.
For example, if `n=5` then the matrix is given by

          α_1  γ_1
          β_1  α_2  γ_2
    A =        β_2  α_3  γ_3
                    β_3  α_4  γ_4
                         β_4  α_5

The vector `δ` must have length `n-2` to hold the second superdiagonal of
the upper triangular factor, arising from the fill-in caused by pivoting.
The boolean vector `p` has length `n-1` and records the sequence of pivot
choices: `p[j]` is `true` if rows `j` and `j+1` were swapped at the `j`th
step.

The factorization is computed in place, so `β` is overwritten with the
nonzero subdiagonal entries of `L`, and `α`, `γ` and `δ` hold the nonzero
entries of `U`.
"""
function factorize!(β::Vector{T}, α::Vector{T}, γ::Vector{T}, δ::Vector{T},
                    p::Vector{Bool}) where T <: Number
    n = length(α)
    if ! ( length(β) == n-1 == length(γ) && length(δ) == n-2 )
        throw(BoundsError("Array sizes to not match"))
    end
    for j = 1:n-2
        δ[j] = zero(T)
        if abs(α[j]) < abs(β[j])
            p[j] = true
            α[j], β[j] = β[j], α[j]
            γ[j], α[j+1] = α[j+1], γ[j]
            δ[j], γ[j+1] = γ[j+1], δ[j]
        else
            p[j] = false
        end
        β[j] = β[j] / α[j]
        α[j+1] -= β[j] * γ[j]
        if p[j]
            γ[j+1] = -β[j] * δ[j]
        end
    end
    if abs(α[n-1]) < abs(β[n-1])
        p[n-1] = true
        α[n-1], β[n-1] = β[n-1], α[n-1]
        γ[n-1], α[n]   = α[n], γ[n-1]
    else
        p[n-1] = false
    end
    β[n-1] = β[n-1] / α[n-1]
    α[n] -= β[n-1] * γ[n-1]
end

"""
    solve!(b, β, α, γ, δ, p)

Solves the tridiagonal linear system `Ax = b` using the LU factorization
computed by `factorize`.
"""
function solve!(b::Vector{T}, β::Vector{T}, α::Vector{T}, γ::Vector{T},
                δ::Vector{T}, p::Vector{Bool}) where T <: Number
    n = length(α)
    if ! ( length(β) == n-1 == length(γ) && length(δ) == n-2 )
        throw(BoundsError("Array sizes to not match"))
    end
    for j = 1:n-1
        if p[j]
            b[j], b[j+1] = b[j+1], b[j]
        end
        b[j+1] -= β[j] * b[j]
    end
    b[n] /= α[n]
    b[n-1] = ( b[n-1] - γ[n-1] * b[n] ) / α[n-1]
    for j = n-2:-1:1
        b[j] = ( b[j] - γ[j] * b[j+1] - δ[j] * b[j+2] ) / α[j]
    end
end

end # module


# function to calculate the volume of a sphere
function sphere_vol(r)
    # julia allows Unicode names (in UTF-8 encoding)
    # so either "pi" or the symbol π can be used
    return 4 / 3 * pi * r^3
end

# functions can also be defined more succinctly
quadratic(a, sqr_term, b) = (-b + sqr_term) / 2a

# calculates x for 0 = a*x^2+b*x+c, arguments types can be defined in function definitions
function quadratic2(a::Float64, b::Float64, c::Float64)
    # unlike other languages 2a is equivalent to 2*a
    # a^2 is used instead of a**2 or pow(a,2)
    sqr_term = sqrt(b^2 - 4a * c)
    r1 = quadratic(a, sqr_term, b)
    r2 = quadratic(a, -sqr_term, b)
    # multiple values can be returned from a function using tuples
    # if the return keyword is omitted, the last term is returned
    r1, r2
end

unit_volume = sphere_vol(1)


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

"Sum the squares of the first `n` positive integers"
function sum_of_squares(n)
  return sum(map(x -> x * x, 1:n))
end

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


#!/usr/bin/env julia

export pe8

module PE8

const data=parse.(Int,collect(
    "73167176531330624919225119674426574742355349194934"*
    "96983520312774506326239578318016984801869478851843"*
    "85861560789112949495459501737958331952853208805511"*
    "12540698747158523863050715693290963295227443043557"*
    "66896648950445244523161731856403098711121722383113"*
    "62229893423380308135336276614282806444486645238749"*
    "30358907296290491560440772390713810515859307960866"*
    "70172427121883998797908792274921901699720888093776"*
    "65727333001053367881220235421809751254540594752243"*
    "52584907711670556013604839586446706324415722155397"*
    "53697817977846174064955149290862569321978468622482"*
    "83972241375657056057490261407972968652414535100474"*
    "82166370484403199890008895243450658541227588666881"*
    "16427171479924442928230863465674813919123162824586"*
    "17866458359124566529476545682848912883142607690042"*
    "24219022671055626321111109370544217506941658960408"*
    "07198403850962455444362981230987879927244284909188"*
    "84580156166097919133875499200524063689912560717606"*
    "05886116467109405077541002256983155200055935729725"*
    "71636269561882670428252483600823257530420752963450"
));

end

pe8(data::Vector{<:Integer}=PE8.data,n::Integer=13)=maximum(prod(data[i:i+n-1]) for i in 1:(length(data)-(n-1)))

if !haskey(ENV,"PROJECT_EULER_WITHOUT_RESULT")
    pe8()|>print
end


using StatsBase
using Base.Test

##### Location

## geomean

@test_approx_eq geomean([1, 2, 3]) (6.0)^(1/3)
@test_approx_eq geomean(1:3) (6.0)^(1/3)
@test_approx_eq geomean([2, 8]) 4.0
@test_approx_eq geomean([4, 1, 1/32]) 0.5
@test geomean([1, 0, 2]) == 0.0

## harmmean

@test_approx_eq harmmean([1, 2, 3]) 3 / (1 + 1/2 + 1/3)
@test_approx_eq harmmean(1:3) 3 / (1 + 1/2 + 1/3)
@test_approx_eq harmmean([1, 2, 4]) 12 / 7

## trimmean

@test_approx_eq trimmean([-100, 2, 3, 7, 200], 0.0) 22.4
@test_approx_eq trimmean([-100, 2, 3, 7, 200], 0.4) 4.0
@test_approx_eq trimmean([-100, 2, 3, 7, 200], 0.8) 3.0

## mode & modes

@test mode([1, 2, 3, 3, 2, 2, 1], 1:3) == 2
@test modes([1, 2, 3, 3, 2, 2, 1], 1:3) == [2]
@test modes([1, 3, 2, 3, 3, 2, 2, 1], 1:3) == [2, 3]

@test mode([1, 2, 3, 3, 2, 2, 1]) == 2
@test modes([1, 2, 3, 3, 2, 2, 1]) == [2]
@test sort(modes([1, 3, 2, 3, 3, 2, 2, 1])) == [2, 3]

## zscores

@test zscore([-3:3], 1.5, 0.5) == [-9.0:2.0:3.0]

a = [3 4 5 6; 7 8 1 2; 6 9 3 0]
z1 = [4. 6. 8. 10.; 5. 6. -1. 0.; 1.5 3.0 0.0 -1.5]
z2 = [8. 2. 3. 1.; 24. 10. -1. -1.; 20. 12. 1. -2.]

@test_approx_eq zscore(a, [1, 2, 3], [0.5, 1.0, 2.0]) z1
@test_approx_eq zscore(a, [1 3 2 4], [0.25 0.5 1.0 2.0]) z2

@test zscore!(float64([-3:3]), 1.5, 0.5) == [-9.0:2.0:3.0]
@test_approx_eq zscore!(float64(a), [1, 2, 3], [0.5, 1.0, 2.0]) z1
@test_approx_eq zscore!(float64(a), [1 3 2 4], [0.25 0.5 1.0 2.0]) z2

@test zscore!(zeros(7), [-3:3], 1.5, 0.5) == [-9.0:2.0:3.0]
@test_approx_eq zscore!(zeros(size(a)), a, [1, 2, 3], [0.5, 1.0, 2.0]) z1
@test_approx_eq zscore!(zeros(size(a)), a, [1 3 2 4], [0.25 0.5 1.0 2.0]) z2

@test_approx_eq zscore(a) zscore(a, mean(a), std(a))
@test_approx_eq zscore(a, 1) zscore(a, mean(a,1), std(a,1))
@test_approx_eq zscore(a, 2) zscore(a, mean(a,2), std(a,2))

###### quantile & friends

@test_approx_eq quantile(1:5) [1:5]
@test_approx_eq nquantile(1:5, 2) [1, 3, 5]
@test_approx_eq nquantile(1:5, 4) [1:5]

@test_approx_eq percentile([1:5], 25) 2.0
@test_approx_eq percentile([1:5], [25, 50, 75]) [2.0, 3.0, 4.0]

##### Dispersion

@test_approx_eq variation([1:5]) 0.527046276694730

@test_approx_eq sem([1:5]) 0.707106781186548

@test_approx_eq mad([1:5], 3) 1.4826
@test_approx_eq mad([1:5]) 1.4826
@test_approx_eq mad(1:5) 1.4826
@test_approx_eq mad([1:5], constant=1.0) 1.0
@test_approx_eq mad([1:5], 3, constant=1.0) 1.0

@test_approx_eq iqr(1:5) 2.0

##### entropy

@test_approx_eq entropy([0.5, 0.5]) 0.6931471805599453
@test_approx_eq entropy([0.2, 0.3, 0.5]) 1.0296530140645737

@test_approx_eq crossentropy([0.2, 0.3, 0.5], [0.3, 0.4, 0.3]) 1.1176681825904018

@test_approx_eq kldivergence([0.2, 0.3, 0.5], [0.3, 0.4, 0.3]) 0.08801516852582819

##### summarystats

s = summarystats(1:5)
@test isa(s, StatsBase.SummaryStats)
@test s.min == 1.0
@test s.max == 5.0
@test_approx_eq s.mean 3.0
@test_approx_eq s.median 3.0
@test_approx_eq s.q25 2.0
@test_approx_eq s.q75 4.0


"""
    newtonRaphson(f,df,p0)

adalah fungsi untuk menyelesaikan `f(x)=0` menggunakan metode newton. Metode ini
memerlukan turunan dari fungsi `f(x)` yaitu `df` dan nilai tebakan awal `p0`.

# Examples
```jl
julia> f(x) = x^3-3*x+2;

julia> df(x) = 3*x^2-3;

julia> pk,flag,M = newtonRaphson(f,df,-2.4);

julia> pk
-2.0

julia> flag
0

julia> M
6×3 Array{Float64,2}:
 0.0  -2.4      NaN
 1.0  -2.07619    0.32381
 2.0  -2.0036     0.0725945
 3.0  -2.00001    0.00358742
 4.0  -2.0        8.58992e-6
 5.0  -2.0        4.91913e-11
```
return solusi `p0` dengan `flag` bernilai 0 jika metode bisection berhasil menemukan
solusi dan gagal jika tidak 0. Serta, matriks `M` yang berisi catatan proses tiap
iterasi `[k, pk, error]`
"""
function newtonRaphson(f,df,p0)
    # Definisikan nilai toleransi, maksimum iterasi dan tebakan awal
    delta = 10^-7;
    maxi = 100;
    pk = p0
    M = [0 pk NaN];
    # Mulai langkah iterasi
    flag = 1;
    for k = 2:maxi
        # Rumus metode Newton-Raphson
        pk1 = pk
        pk = pk1 - f(pk1)/df(pk1);
        # Hitung nilai galat mutlak dan relatif
        err = abs(pk-pk1);
        rel = 2*err/(abs(pk)+eps());
        M = [M; [k-1 pk err]]
        # Kriteria penghentian iterasi jika galat memenuhi toleransi.
        if err<delta || rel<delta
            flag = 0;
            break
        end
    end
    return pk, flag, M
end


# Statistics-related math functions

# scalar functions

xlogx(x::FloatingPoint) = x > zero(x) ? x * log(x) : zero(x)
xlogx(x::Real) = xlogx(float(x))

xlogy{T<:FloatingPoint}(x::T, y::T) = x > zero(T) ? x * log(y) : zero(x)
xlogy{T<:Real}(x::T, y::T) = xlogy(float(x), float(y))
xlogy(x::Real, y::Real) = xlogy(promote(x, y)...)

logistic(x::FloatingPoint) = one(x) / (one(x) + exp(-x))
logistic(x::Real) = logistic(float(x))

logit(x::FloatingPoint) = log(x / (one(x) - x))
logit(x::Real) = logit(float(x))

softplus(x::FloatingPoint) = x <= 0 ? log1p(exp(x)) : x + log1p(exp(-x))
softplus(x::Real) = softplus(float(x))

invsoftplus(x::FloatingPoint) = log(exp(x) - one(x))
invsoftplus(x::Real) = invsoftplus(float(x))

@vectorize_1arg Real xlogx
@vectorize_2arg Real xlogy
@vectorize_1arg Real logistic
@vectorize_1arg Real logit
@vectorize_1arg Real softplus
@vectorize_1arg Real invsoftplus

## logsumexp

logsumexp{T<:FloatingPoint}(x::T, y::T) = x > y ? x + log1p(exp(y - x)) : y + log1p(exp(x - y))
logsumexp{T<:Real}(x::T, y::T) = logsumexp(float(x), float(y))
logsumexp(x::Real, y::Real) = logsumexp(promote(x, y)...)

function logsumexp{T<:Real}(x::AbstractArray{T})
    isempty(x) && return -Inf
    u = maximum(x)
    s = 0.
    for i = 1:length(x)
    	@inbounds s += exp(x[i] - u)
    end
    log(s) + u
end

## softmax

function softmax!{T<:Real}(r::AbstractArray, x::AbstractArray{T})
	n = length(x)
	length(r) == n || throw(DimensionMismatch("Inconsistent array lengths."))
	u = maximum(x)
	s = 0.
	for i = 1:n
		@inbounds s += (r[i] = exp(x[i] - u))
	end
	for i = 1:n
		r[i] /= s
	end
	r
end

softmax!{T<:FloatingPoint}(x::AbstractArray{T}) = softmax!(x, x)
softmax{T<:Real}(x::AbstractArray{T}) = softmax!(Array(Float64, size(x)), x)


using EngEconomics, Roots, Plots, ColorSchemes
plotly()

# Given
# - Finedetail = A, Simplicity = B
strA = "Finedetail"
strB = "Simplicity"
lifeA = 7
lifeB = 10
PA = 200000 # Initial Cost of A - [$]
PB = 350000 # Initial Cost of B - [$]
MA = 10000 # Maintenance Cost of A - [$/yr]
UMA = 0.05 # Maintenance Cost of A - [$/units]
MB = 20000 # Maintenance Cost of B - [$/yr]
UMB = 0.01 # Maintenance Cost of B - [$/units]
LA = 1.25 # Labor Cost of A - [$/unit]
LB = 0.50 # Labor Cost of B - [$/unit]
OA = 6500 # "Other" Costs of A - [$/yr]
UOA = 0.95 # "Other" Costs of A - [$/unit]
OB = 15500 # "Other" Costs of B - [$/yr]
UOB = 0.55 # "Other" Costs of B - [$/unit]
FA = 5000 # Salvage Value of A - [$]
FB = 20000 # Salvage Value of B - [$]
MARR = 0.15

# Find
# (a.) Who is the preferred supplier if sales are 30,000 units per year? Use an
#      annual worth comparison.
unitRate = 30000 # [units/yr]
EUAWA = -PA * capitalRecoveryFactor(MARR, lifeA) + -MA + -UMA * unitRate + -LA * unitRate + -OA + -UOA * unitRate + FA * sinkingFundFactor(MARR, lifeA)
EUAWB = -PB * capitalRecoveryFactor(MARR, lifeB) + -MB + -UMB * unitRate + -LB * unitRate + -OB + -UOB * unitRate + FB * sinkingFundFactor(MARR, lifeB)

if EUAWA > EUAWB
	println(strA, " is the preferred supplier if unit sales are ", unitRate, " because its EUAW exceeds that of ", strB)
else
	println(strB, " is the preferred supplier if unit sales are ", unitRate, " because its EUAW exceeds that of ", strA)
end

# (b.) Who is the preferred supplier if sales are 200,000 units per year. Use
#      annual worth comparison
unitRate = 200000 # [units/yr]
EUAWA = -PA * capitalRecoveryFactor(MARR, lifeA) + -MA + -UMA * unitRate + -LA * unitRate + -OA + -UOA * unitRate + FA * sinkingFundFactor(MARR, lifeA)
EUAWB = -PB * capitalRecoveryFactor(MARR, lifeB) + -MB + -UMB * unitRate + -LB * unitRate + -OB + -UOB * unitRate + FB * sinkingFundFactor(MARR, lifeB)

if EUAWA > EUAWB
	println(strA, " is the preferred supplier if unit sales are ", unitRate, " because its EUAW exceeds that of ", strB)
else
	println(strB, " is the preferred supplier if unit sales are ", unitRate, " because its EUAW exceeds that of ", strA)
end

# (c.) How sensitive is the choice of supplier to sales level? Experiment with
#      sales levels between 30,000 and 200,000 units per year. At what sales
#      level will the costs of the two melters be equal?
unitRateVector = collect(30000:1000:200000)
EUAWA = -PA * capitalRecoveryFactor(MARR, lifeA) .+ -MA .+ -UMA * unitRateVector .+ -LA * unitRateVector .+ -OA .+ -UOA * unitRateVector .+ FA * sinkingFundFactor(MARR, lifeA)
EUAWB = -PB * capitalRecoveryFactor(MARR, lifeB) .+ -MB .+ -UMB * unitRateVector .+ -LB * unitRateVector .+ -OB .+ -UOB * unitRateVector .+ FB * sinkingFundFactor(MARR, lifeB)

plot(unitRateVector, EUAWA, xlabel="Units", ylabel="Annual Worth (\$)", label=strA,
	title="Sensitivity of Alternatives to Unit Sales", legend=:outerbottomright)
plot!(unitRateVector, EUAWB, label=strB)

EUAWA_func(u) = -PA * capitalRecoveryFactor(MARR, lifeA) .+ -MA .+ -UMA * u .+ -LA * u .+ -OA .+ -UOA * u .+ FA * sinkingFundFactor(MARR, lifeA)
EUAWB_func(u) = -PB * capitalRecoveryFactor(MARR, lifeB) .+ -MB .+ -UMB * u .+ -LB * u .+ -OB .+ -UOB * u .+ FB * sinkingFundFactor(MARR, lifeB)
f(u) = EUAWA_func(u) - EUAWB_func(u)
unitBreakEven = find_zero(f, 34000)

println("The annual worths of both alternatives are sensitive to the unit sales rate.")
println("The breakeven unit sales rate is: ", unitBreakEven)
println("From the plot, we understand that if sales are below the breakeven unit sales rate, then Finedetail is preferred. However, if the unit sales rate exceeds the breakeven unit sales rate, then simplicity is preferred")


function linsolve_cg( LF::LF3dGrid, b::Array{Float64,1};
                      x0 = nothing,
                      NiterMax = 1000, TOL=5.e-10,
                      convmsg=true, showprogress=false )
  #
  Npoints = size(b)[1]
  if x0 == nothing
    x = zeros(Float64, Npoints)
  else
    x = copy(x0)
  end
  #
  r = zeros( Float64, Npoints )
  p = zeros( Float64, Npoints )
  #
  L_x = apply_Laplacian( LF, x )
  for ip = 1 : Npoints
    r[ip] = b[ip] - L_x[ip]
    p[ip] = r[ip]
  end

  rsold = dot( r, r )
  for iter = 1 : NiterMax
    #
    L_x = apply_Laplacian( LF, p )
    #
    α = rsold/dot( p, L_x )
    #
    x[:] = x[:] + α * p[:]  # FIXME use x[:] to force x to be copied, not referenced
    r[:] = r[:] - α * L_x[:]
    #
    rsnew = dot( r, r )
    # deltars = rsold - rsnew
    if showprogress
      @printf("%8d %20.10f\n", iter, sqrt(rsnew))
    end
    #
    if sqrt(rsnew) < TOL
      if convmsg
        @printf("#Convergence achieved in linsolve_cg: %8d iterations.\n", iter)
      end
      break
    end
    #
    p = r + (rsnew/rsold) * p
    #
    rsold = rsnew
  end
  #
  return x
  #
end


export soundspeed, halfwavelength, wavenumber
export flutelength, toneholelength, closedholecorrection

"""
  𝑐 = soundspeed(ϑ=25.0)

Calculate the speed of sound in air of temperature ϑ in m/s
"""
function soundspeed(ϑ=25.0)
  𝛾 = 1.400            # heat capacity ratio of air
  𝑅 = 8.31446261815324 # molar gas constant (J/mol/K)
  𝑀 = 0.028965369      # mean molar mass of air (kg/mol)
  𝑐 = √(𝛾*𝑅/𝑀 *273.15)*√(1.0 + ϑ/273.15)
  return 𝑐
end

"""
  𝜔 = angularfreq(𝑓=440.0)

calculate angular frequency of 𝑓 in rad/s
"""
function angularfreq(𝑓=440.0)
  𝜔 = 2π*𝑓
  return 𝜔
end

"""
  𝑘 = wavenumber(𝑓=440.0; 𝜗=25.0)

calculate wavenumber of given frequency 𝑓 in air of temperature 𝜗 in m⁻¹
"""
function wavenumber(𝑓=440.0; ϑ=25.0)
  𝑐 = soundspeed(ϑ)
  𝜔 = angularfreq(𝑓)
  𝑘 = 𝜔/𝑐
  return 𝑘
end

"""
  𝜆 = halfwavelength(𝑓=440.0; ϑ=25.0)

calculate half-wavelength of given frequency 𝑓 in air of temperature ϑ
"""
function halfwavelength(𝑓=440.0; ϑ=25.0)
  𝑐 = soundspeed(ϑ)
  𝜆 = 𝑐/2𝑓
  return 1000𝜆 # (to mm)
end

"""
  ℓₜ = flutelength(𝑓=440.0; ϑ=25.0, ⌀=19.0, ℎ=3.5, 𝛥ℓₑ=57.0, 𝛥ℓᵥ=0.0)

Calculate flute length from embouchure-hole to open-end
  for fundamental frequency 𝑓, temperature ϑ, open-end bore diameter ⌀,
  wall thickness ℎ, embouchure correction 𝛥ℓₑ and closed-hole correction 𝛥ℓᵥ
"""
function flutelength(𝑓=440.0; ϑ=25.0, ⌀=19.0, ℎ=3.5, 𝛥ℓₑ=57.0, 𝛥ℓᵥ=0.0)
  𝜆 = halfwavelength(𝑓; ϑ=ϑ)
  ⌀₊ = ⌀ + 2ℎ
  ϖ = ⌀/⌀₊
  𝜍 = 0.6133⌀/2
  𝜚 = 0.8216⌀/2
  𝛥ℓₜ = 𝜚 + ϖ * (𝜍-𝜚) + 0.057ϖ * (1-ϖ ^5)
  ℓₜ = 𝜆 - 𝛥ℓₑ - 𝛥ℓᵥ - 𝛥ℓₜ
  return ℓₜ
end

"""
  ℓₕ = toneholelength(𝑓=440.0, 𝑓ₜ=415.305, 𝑑=9.0; ϑ=25.0, ⌀=19.0, ℎ=3.5, 𝛥ℓₑ=57.0, 𝛥ℓᵥ=0.0)

Calculate distance from embouchure hole center to tone hole center
  for open frequency 𝑓, closed frequency 𝑓ₜ, tone-hole diameter 𝑑,
  temperature ϑ, tone-hole bore diameter ⌀, tone-hole height ℎ,
  embouchure correction 𝛥ℓₑ and closed-hole correction 𝛥ℓᵥ
"""
function toneholelength(𝑓=440.0, 𝑓ₜ=415.305, 𝑑=9.0; ϑ=25.0, ⌀=19.0, ℎ=3.5, 𝛥ℓₑ=57.0, 𝛥ℓᵥ=0.0)
  𝜆 = halfwavelength(𝑓; ϑ=ϑ)
  𝑔 = 𝑓/𝑓ₜ - 1
  𝜙 = (ℎ+𝑑) * (⌀/𝑑)^2 - 0.45⌀
  𝑧 = 𝑔/2 * √(1 + 4𝜙/(𝑔*𝜆)) - 𝑔/2
  𝛥ℓₕ = 𝑧 * 𝜆
  ℓₕ = 𝜆 - 𝛥ℓₑ - 𝛥ℓᵥ - 𝛥ℓₕ
  return ℓₕ
end

"""
  𝛥𝜆ₕ = closedholecorrection(𝑓=440.0, 𝑓ₜ=415.305, 𝑑=9.0; ϑ=25.0, ⌀=19.0, ℎ=3.5, 𝛥ℓₑ=57.0, 𝛥ℓᵥ=0.0)

Calculate correction due to closed hole
  for open frequency 𝑓, closed frequency 𝑓ₜ, tone-hole diameter 𝑑,
  temperature ϑ, tone-hole bore diameter ⌀, tone-hole height ℎ,
  embouchure correction 𝛥ℓₑ and closed-hole correction 𝛥ℓᵥ
"""
function closedholecorrection(𝑓=440.0, 𝑓ₜ=415.305, 𝑑=9.0; ϑ=25.0, ⌀=19.0, ℎ=3.5, 𝛥ℓₑ=57.0, 𝛥ℓᵥ=0.0)
  𝑘 = wavenumber(𝑓; ϑ=ϑ)
  𝜆ₜ= halfwavelength(𝑓ₜ; ϑ=ϑ)
  ℓₕ = toneholelength(𝑓, 𝑓ₜ, 𝑑; ϑ=ϑ, ⌀=⌀, ℎ=ℎ, 𝛥ℓₑ=𝛥ℓₑ, 𝛥ℓᵥ=𝛥ℓᵥ)
  𝜙 = 𝑑^2*ℎ / ⌀^2
  ϵ = 2/π * atan(2𝑑/13ℎ)
  𝛿ₕ = 𝜆ₜ - 𝛥ℓₑ - ℓₕ
  𝛥𝜆ₕ = (sin(𝑘*𝛿ₕ)^2 - ϵ*cos(𝑘*𝛿ₕ)^2) * 𝜙
  return 𝛥𝜆ₕ
end


function identical_matrix(n::Integer)
    ret = zeros(n, n)
    for itr1 in 1 : n
        ret[itr1, itr1] = 1
    end
    return ret
end

function inner_product(mat::Matrix, a::Integer, b::Integer)
    # if $a$ or $b$ are out of bound, return $0$
    if a > size(mat, 2) || b > size(mat, 2)
        return 0
    end
    # initialize the result
    ret = 0
    # calculate the inner (dot) product of column $a$ and $b$
    for itr1 in 1 : size(mat, 1)
        ret += mat[itr1, a] * mat[itr1, b]
    end
    return ret
end

function column_elimination!(mat::Matrix, a::Integer, b::Integer, k::Number, row::Integer = 1)
    for itr1 in row : size(mat, 1)
        mat[itr1, b] += mat[itr1, a] * k
    end
    return mat
end

function mgs(mat::Matrix)
    # make a copy of the matrix as $Q$
    Q = float(copy(mat))
    # it's assumed that the matrix has $m$ rows and $n$ columns
    m, n = size(mat)
    # make a identical matrix as $R$
    R = identical_matrix(n)
    for itr1 in 1 : n
        # calculate the norm of the column vector
        norm = sqrt(inner_product(Q, itr1, itr1))
        # if the column is $0$, just skip it
        norm == 0 && continue
        # normalize the column vector
        for itr2 in 1 : m
            Q[itr2, itr1] /= norm
        end
        # multiply the $itr1$th row with the norm
        for itr2 in itr1 : n
            R[itr1, itr2] *= norm
        end
        # substract the projections of the current vector
        # from the remaining vectors
        for itr2 in (itr1 + 1) : n
            scaler = inner_product(Q, itr1, itr2)
            column_elimination!(Q, itr1, itr2, -scaler)
            # add the corresponding vectors to some rows
            for itr3 in itr2 : n
                R[itr1, itr3] += R[itr2, itr3] * scaler
            end
        end
    end
    return Q, R
end


"""
ksintegrate: integrate kuramoto-sivashinsky equation (Julia)
       u_t = -u*u_x - u_xx - u_xxxx, domain x in [0,Lx], periodic BCs

 inputs
          u = initial condition (vector of u(x) values on uniform gridpoints))
         Lx = domain length
         dt = time step
         Nt = number of integration timesteps
      nsave = save every nsave-th time step

 outputs

          u = final state, vector of u(x, Nt*dt) at uniform x gridpoints

This implementation has two improvements over ksintegrateNaive.jl. It uses
   (1) in-place FFTs
   (2) loop fusion: Julia can translate arithmetic vector expressions in dot syntax
       to single for loop over the components, which should be much faster than
       constructing a temporary vector for each operation in the vector expression.
"""
function ksintegrateInplace(u, Lx, dt, Nt)
    u = (1+0im)*u                       # force u to be complex
    Nx = length(u)                      # number of gridpoints
    kx = vcat(0:Nx/2-1, 0:0, -Nx/2+1:-1)# integer wave #s,  exp(2*pi*i*kx*x/L)
    alpha = 2*pi*kx/Lx                  # real wavenumbers, exp(i*alpha*x)
    D = 1im*alpha                       # spectral D = d/dx operator
    L = alpha.^2 - alpha.^4             # spectral L = -D^2 - D^4 operator
    G = -0.5*D                          # spectral -1/2 D operator, to eval -u u_x = 1/2 d/dx u^2

    # convenience variables
    dt2  = dt/2
    dt32 = 3*dt/2
    A_inv = (ones(Nx) - dt2*L).^(-1)
    B     =  ones(Nx) + dt2*L

    # compute FFTW plans
    FFT! = plan_fft!(u, flags=FFTW.ESTIMATE)
    IFFT! = plan_ifft!(u, flags=FFTW.ESTIMATE)

    # compute nonlinear term Nn == -u u_x and Nuprev (Nu at prev timestep)
    Nn  = G.*fft(u.^2)  # Nn == -1/2 d/dx (u^2) = -u u_x
    Nn1 = copy(Nn)      # use Nn1 = Nn at first time step
    FFT!*u

    # timestepping loop
    for n = 0:Nt

        Nn1 .= Nn   # shift nonlinear term in time
        Nn .= u        # put u into N in prep for comp of nonlineat

        IFFT!*Nn       # transform Nn to gridpt values, in place
        Nn .= Nn.*Nn   # collocation calculation of u^2
        FFT!*Nn        # transform Nn back to spectral coeffs, in place

        Nn .= G.*Nn

        # loop fusion! Julia translates the folling line of code to a single for loop.
        u .= A_inv .*(B .* u .+ dt32.*Nn .- dt2.*Nn1)
    end

    IFFT!*u
    real(u)
end


using LinearAlgebra
using Plots
using IterativeSolvers

function func(y,t,ϵ)
    return -(1/ϵ)*(y-sin(t));
end

function actual_solution(t,ϵ)
    return (sin(t) - ϵ*cos(t))/(1+ϵ^2) + (1 + ϵ/(1+ϵ^2))*exp(-t/ϵ);
end

function compute_solution(t,ϵ)
    n = length(t);
    y = zeros(n);
    for i in 1:n
       y[i] = actual_solution(t[i],ϵ);
    end
    return y
end

function explicit_euler(t,y0,ϵ)
    n = length(t);
    y = zeros(n);
    y[1] = y0;
    dt = t[2]-t[1];
    for i in 2:n
       y[i] = y[i-1] + dt*func(y[i-1],t[i-1],ϵ);
    end
    return y;
end

function sor_implicit_euler(t,y0,ϵ)
    n = length(t);
    y = zeros(n);
    y[1] = y0;
    dt = t[2]-t[1];
    res = 100;
    τ = 1e-6;
    iter = 1e6;
    κ2 = (dt/ϵ);
    κ1 = 1/(1 + κ2);
    κ3 = 1/κ1;
    it = 0;
    while (it<iter && res>τ)
        for i in 2:n
           y[i] = κ1*(y[i-1] + κ2*sin(t[i]));
        end
        for i in 2:n
           res+= κ2*sin(t[i]) + y[i-1] - κ3*y[i];
        end
        res = sqrt(abs(res/n^2));
        it += 1;
    end
    return y;
end

function sor_trapezoidal_method(t,y0,ϵ)
    n = length(t);
    y = zeros(n);
    y[1] = y0;
    dt = t[2]-t[1];
    res = 100;
    τ = 1e-6;
    iter = 1e6;
    κ1 = (dt/(2*ϵ));
    κ2 = 1 - κ1;
    κ3 = 1 + κ1;
    κ4 = 1/κ3;
    it = 0;
    while (it<iter && res>τ)
        for i in 2:n
            y[i] = κ4*(κ1*(sin(t[i]) + sin(t[i-1])) + κ2*y[i-1]);
        end
        for i in 2:n
            res += κ1*(sin(t[i]) + sin(t[i-1])) + κ2*y[i-1] - κ3*y[i];
        end
        res = sqrt(abs(res/n^2));
        it += 1;
    end
    return y;
end

function norm(y1,y2)
    n = length(y1);
    norm = 0;
    for i = 1:n
        norm += abs(y1[i]-y2[i]);
    end
    norm = sqrt(norm/n*n);
    println("The norm is ",norm)
end

x0 = 0;
xl = pi;
N = 99;
y0 = 1;
x = LinRange(x0,xl,N)

ϵ = 1;
y_a = compute_solution(x,ϵ);
y = sor_trapezoidal_method(x,y0,ϵ);
plot(x,y)
p1 = plot!(x,y_a,title="Symplectic ϵ=1")
norm(y,y_a);

ϵ = 0.1;
y_a = compute_solution(x,ϵ);
y = sor_trapezoidal_method(x,y0,ϵ);
plot(x,y)
p2 = plot!(x,y_a,title="Symplectic  ϵ=0.1")
norm(y,y_a);

ϵ = 0.01;
y_a = compute_solution(x,ϵ);
y = sor_trapezoidal_method(x,y0,ϵ);
plot(x,y)
p3 = plot!(x,y_a,title="Symplectic  ϵ=0.01")
norm(y,y_a);

ϵ = 0.001;
y_a = compute_solution(x,ϵ);
y = sor_trapezoidal_method(x,y0,ϵ);
plot(x,y)
p4 = plot!(x,y_a,title="Symplectic  ϵ=0.001")
norm(y,y_a);

plot(p1,p2,p3,p4,layout=(2,2),legend=false)

x0 = 0;
xl = pi;
N = 99;
y0 = 1;
x = LinRange(x0,xl,N)

ϵ = 1;
y_a = compute_solution(x,ϵ);
y = explicit_euler(x,y0,ϵ);
plot(x,y)
p1 = plot!(x,y_a,title="Explcit Euler ϵ=1")
norm(y,y_a);

ϵ = .1;
y_a = compute_solution(x,ϵ);
y = explicit_euler(x,y0,ϵ);
plot(x,y)
p2 = plot!(x,y_a,title="Explcit Euler ϵ=0.1")
norm(y,y_a);

ϵ = .01;
y_a = compute_solution(x,ϵ);
y = explicit_euler(x,y0,ϵ);
plot(x,y)
p3 = plot!(x,y_a,title="Explcit Euler ϵ=0.01")
norm(y,y_a);

ϵ = .001;
y_a = compute_solution(x,ϵ);
y = explicit_euler(x,y0,ϵ);
plot(x,y)
p4 = plot!(x,y_a,title="Explcit Euler ϵ=0.001")
norm(y,y_a);

plot(p1,p2,p3,p4,layout=(2,2),legend=false)

x0 = 0;
xl = pi;
N = 99;
y0 = 1;
x = LinRange(x0,xl,N);

ϵ = 1;
y_a = compute_solution(x,ϵ);
y = sor_implicit_euler(x,y0,ϵ);
plot(x,y)
p1 = plot!(x,y_a,title="Implicit Euler ϵ=1")
norm(y,y_a);

ϵ = 0.1;
y_a = compute_solution(x,ϵ);
y = sor_implicit_euler(x,y0,ϵ);
plot(x,y)
p2 = plot!(x,y_a,title="Implicit Euler ϵ=0.1")
norm(y,y_a);

ϵ = 0.01;
y_a = compute_solution(x,ϵ);
y = sor_implicit_euler(x,y0,ϵ);
plot(x,y)
p3 = plot!(x,y_a,title="Implicit Euler ϵ=0.01")
norm(y,y_a);

ϵ = 0.001;
y_a = compute_solution(x,ϵ);
y = sor_implicit_euler(x,y0,ϵ);
plot(x,y)
p4 = plot!(x,y_a,title="Implicit Euler ϵ=0.001")
norm(y,y_a);

plot(p1,p2,p3,p4,layout=(2,2),legend=false)


@doc """
      Fourth-Order Runge-Kutta Method
      Supported by:
      -PhD Kelvyn B. Sánchez kelvyn.baruc@gmail.com
 """ ->

function rk4(f, tspan, y0; h = 1)
tint = floor((tspan[end] - tspan[1])/h) + 1
      t = range(tspan[1],tspan[end],length=Int(tint))

#t = linspace(tspan[1], tspan[end],tint)
y = zeros(Int(tint), length(y0))
y[1,:] = y0

      try
           error = f(t[1],y0)

            for i=1:1:(length(t) - 1)
                  ymid = zeros(1,Int(length(y0)))
                  k1 = zeros(1,Int(length(y0)))
                  k2 = zeros(1,Int(length(y0)))
                  k3 = zeros(1,Int(length(y0)))
                  k4 = zeros(1,Int(length(y0)))

                  for j=1:length(y0)
                        k1[1,j] = f(t[i],y[i,:])[j]
                        ymid[1,j] = y[i,j] + k1[1,j]*(h/2)
                  end

                  for j=1:length(y0)
                        k2[1,j] = f(t[i] + (1/2)*h,ymid)[j]
                        ymid[1,j] = y[i,j] + k2[1,j]*(h/2)
                  end

                  for j=1:length(y0)
                        k3[1,j] = f(t[i] + (1/2)*h,ymid)[j]
                        ymid[1,j] = y[i,j] + k3[1,j]*h
                  end

                  for j=1:length(y0)
                        k4[1,j] = f(t[i] + h,ymid)[j]
                  end

                  for j=1:length(y0)
                        y[i+1,j] = y[i,j] + (1/6)*(k1[1,j] + 2*(k2[1,j] + k3[1,j]) + k4[1,j])*h
                  end
            end

            return t, y
      catch
           throw(ErrorException("The number of initial values does not match the number of equations"))
      end

 end


# multivariate gamma function
# Chikuse, 2003 Eq. 1.5.7
mvgamma(m, a) = π^(m * (m - 1)//4) * prod(i -> gamma(a - (i - 1)//2), 1:m)

# tr(A'B)
tr_At_B(A, B) = sum((Ai, Bi) -> dot(Ai, Bi), zip(eachcol(A), eachcol(B)))

# hypergeometric functions with matrix arguments
pFq((), (), x) = exp(tr(x)) # ₀F₀
# TODO: Implement ₀F₁
# see P Koev, A Edelman. The efficient evaluation of the hypergeometric function of a
# matrix argument. Math. Comp. 75 (2006), 833-846
# pFq(a::NTuple{p}, b::NTuple{q}, x)


#=

   From https://math.stackexchange.com/questions/513250/conditional-probability-question-with-cards-where-the-other-side-color-is-to-be
   """
   A box contains three cards. One card is red on both sides, one card is green on both sides,
   and one card is red on one side and green on the other. One card is selected from the
   box at random, and the color on one side is observed. If this side is green, what is
   the probability that the other side of the card is also green?

   ... the answer to this question is 2/3.
   """

   Give that the color of card is green, then the probability
   of the different cards are:

   Distributions of variable selected_card
   all_green  =>   67030  (0.670300)
   red_green  =>   32970  (0.329700)

   I.e. the probability that it's the all green card is 2/3.

   Cf ~/webppl/card_problem.wppl

=#

using Turing, StatsPlots, DataFrames
include("jl_utils.jl")

@model function card_problem()
    red = 1
    green = 2

    all_red = 1
    all_green = 2
    red_green = 3

    #  What card did we select?
    selected_card ~ Categorical(simplex([1/3,1/3,1/3])) # [all_red,all_green,red_green]

    #  What is the color of the card (one side) that we see?
    card ~
        selected_card == all_red   ?  Dirac(red) :
        selected_card == all_green ?  Dirac(green) :
        # [red,green]
        selected_card == red_green ?  Categorical([1/2,1/2]) : Dirac(0)

    # Alternative:
    # if selected_card == all_red
    #     card ~  Dirac(red)
    # elseif selected_card == all_green
    #     card ~ Dirac(green)
    # else
    #     card ~ Categorical([1/2,1/2])
    # end

    #  The color of the card we see is green
    true ~ Dirac(card == green)

end

model = card_problem()

num_chains = 4

# chns = sample(model, Prior(), 10_000)
chns = sample(model, MH(), 100_000)
# chns = sample(model, PG(15), 10_000)
# chns = sample(model, SMC(10_000), 10_000)
# chns = sample(model, IS(), 10_000)

# chns = sample(model, NUTS(), 10_000)
# chns = sample(model, HMC(0.1,10), 10_000)

display(chns)
# display(plot(chns))

show_var_dist_pct(chns, :selected_card,["all_red","all_green","red_green"])


# Descriptive Statistics

#############################
#
#   Location
#
#############################

# Geometric mean
"""
    geomean(a)

Return the geometric mean of a real-valued array.
"""
function geomean(a::RealArray)
    s = 0.0
    n = length(a)
    for i = 1 : n
        @inbounds s += log(a[i])
    end
    return exp(s / n)
end

# Harmonic mean
"""
    harmmean(a)

Return the harmonic mean of a real-valued array.
"""
function harmmean(a::RealArray)
    s = 0.0
    n = length(a)
    for i in 1 : n
        @inbounds s += inv(a[i])
    end
    return n / s
end

# Generalized mean
"""
    genmean(a, p)

Return the generalized/power mean with exponent `p` of a real-valued array,
i.e. ``\left( \frac{1}{n} \sum_{i=1}^n a_i^p \right)^{\frac{1}{p}}``, where `n = length(a)`.
It is taken to be the geometric mean when `p == 0`.
"""
function genmean(a::RealArray, p::Real)
    if p == 0
        return geomean(a)
    end
    s = 0.0
    n = length(a)
    for x in a
        #= At least one of `x` or `p` must not be an int to avoid domain errors when `p` is a negative int.
        We choose `x` in order to exploit exponentiation by squaring when `p` is an int. =#
        @inbounds s += convert(Float64, x)^p
    end
    return (s/n)^(1/p)
end

# Trimmed mean
"""
    trimmean(x, p)

Compute the trimmed mean of `x`, i.e. the mean after removing a
proportion `p` of its highest- and lowest-valued elements.
"""
function trimmean(x::RealArray, p::Real)
    n = length(x)
    n > 0 || error("x can not be empty.")
    0 <= p < 1 || error("p must be non-negative and less than 1.")
    rn = min(round(Int, n * p), n-1)

    sx = sort(x)
    nl = rn >> 1
    nh = (rn - nl)
    s = 0.0
    for i = (1+nl) : (n-nh)
        @inbounds s += sx[i]
    end
    return s / (n - rn)
end

# compute mode, given the range of integer values
"""
    mode(a, [r])

Return the first mode (most common number) of an array, optionally
over a specified range `r`.
"""
function mode{T<:Integer}(a::AbstractArray{T}, r::UnitRange{T})
    isempty(a) && error("mode: input array cannot be empty.")
    len = length(a)
    r0 = r[1]
    r1 = r[end]
    cnts = zeros(Int, length(r))
    mc = 0    # maximum count
    mv = r0   # a value corresponding to maximum count
    for i = 1:len
        @inbounds x = a[i]
        if r0 <= x <= r1
            @inbounds c = (cnts[x - r0 + 1] += 1)
            if c > mc
                mc = c
                mv = x
            end
        end
    end
    return mv
end

"""
    modes(a, [r])::Vector

Return all modes (most common numbers) of an array, optionally over a
specified range `r`.
"""
function modes{T<:Integer}(a::AbstractArray{T}, r::UnitRange{T})
    r0 = r[1]
    r1 = r[end]
    n = length(r)
    cnts = zeros(Int, n)
    # find the maximum count
    mc = 0
    for i = 1:length(a)
        @inbounds x = a[i]
        if r0 <= x <= r1
            @inbounds c = (cnts[x - r0 + 1] += 1)
            if c > mc
                mc = c
            end
        end
    end
    # find all values corresponding to maximum count
    ms = T[]
    for i = 1:n
        @inbounds if cnts[i] == mc
            push!(ms, r[i])
        end
    end
    return ms
end

# compute mode over arbitrary array
function mode{T}(a::AbstractArray{T})
    isempty(a) && error("mode: input array cannot be empty.")
    cnts = Dict{T,Int}()
    # first element
    mc = 1
    mv = a[1]
    cnts[mv] = 1
    # find the mode along with table construction
    for i = 2 : length(a)
        @inbounds x = a[i]
        if haskey(cnts, x)
            c = (cnts[x] += 1)
            if c > mc
                mc = c
                mv = x
            end
        else
            cnts[x] = 1
            # in this case: c = 1, and thus c > mc won't happen
        end
    end
    return mv
end

function modes{T}(a::AbstractArray{T})
    isempty(a) && error("modes: input array cannot be empty.")
    cnts = Dict{T,Int}()
    # first element
    mc = 1
    cnts[a[1]] = 1
    # find the mode along with table construction
    for i = 2 : length(a)
        @inbounds x = a[i]
        if haskey(cnts, x)
            c = (cnts[x] += 1)
            if c > mc
                mc = c
            end
        else
            cnts[x] = 1
            # in this case: c = 1, and thus c > mc won't happen
        end
    end
    # find values corresponding to maximum counts
    ms = T[]
    for (x, c) in cnts
        if c == mc
            push!(ms, x)
        end
    end
    return ms
end

#############################
#
#   quantile and friends
#
#############################

"""
    percentile(v, p)

Return the `p`th percentile of a real-valued array `v`.
"""
percentile{T<:Real}(v::AbstractArray{T}, p) = quantile(v, p * 0.01)

quantile{T<:Real}(v::AbstractArray{T}) = quantile(v, [.0, .25, .5, .75, 1.0])

"""
    nquantile(v, n)

Return the n-quantiles of a real-valued array, i.e. the values which
partition `v` into `n` subsets of nearly equal size.
"""
nquantile{T<:Real}(v::AbstractArray{T}, n::Integer) = quantile(v, (0:n)/n)

#############################
#
#   Dispersion
#
#############################

# span, i.e. the range minimum(x):maximum(x)
"""
    span(x)

Return the span of an integer array, i.e. the range `minimum(x):maximum(x)`.
"""
span{T<:Integer}(x::AbstractArray{T}) = ((a, b) = extrema(x); a:b)

# Variation coefficient: std / mean
"""
    variation(x, m=mean(x))

Return the coefficient of variation of an array `x`, optionally specifying
a precomputed mean `m`. The coefficient of variation is the ratio of the
standard deviation to the mean.
"""
variation{T<:Real}(x::AbstractArray{T}, m::Real) = stdm(x, m) / m
variation{T<:Real}(x::AbstractArray{T}) = variation(x, mean(x))

# Standard error of the mean: std(a) / sqrt(len)
"""
    sem(a)

Return the standard error of the mean of `a`, i.e. `sqrt(var(a) / length(a))`.
"""
sem{T<:Real}(a::AbstractArray{T}) = sqrt(var(a) / length(a))

# Median absolute deviation
"""
    mad(v)

Compute the median absolute deviation of `v`.
"""
function mad{T<:Real}(v::AbstractArray{T})
    isempty(v) && throw(ArgumentError("mad is not defined for empty arrays"))

    S = promote_type(T, typeof(middle(first(v))))

    mad!(LinAlg.copy_oftype(v, S))
end

"""
    StatsBase.mad!(v, center=median!(v); constant=k)

Compute the maximum absolute deviation (MAD) of `v` about a precomputed center
`center`, overwriting `v` in the process. Using the MAD as a consistent estimator
of the standard deviation requires a scaling factor that depends on the underlying
distribution. For normally distributed data, `k` is chosen as
`1 / quantile(Normal(), 3/4) ≈ 1.4826`, which is used as the default here.
"""
function mad!{T<:Real}(v::AbstractArray{T}, center::Real=median!(v);
                       constant::Real = 1 / (-sqrt(2 * one(T)) * erfcinv(3 * one(T) / 2)))
    for i in 1:length(v)
        @inbounds v[i] = abs(v[i]-center)
    end
    constant * median!(v)
end

# Interquartile range
"""
    iqr(v)

Compute the interquartile range (IQR) of an array, i.e. the 75th percentile
minus the 25th percentile.
"""
iqr{T<:Real}(v::AbstractArray{T}) = (q = quantile(v, [.25, .75]); q[2] - q[1])

#############################
#
#   Z-scores
#
#############################

function _zscore!(Z::AbstractArray, X::AbstractArray, μ::Real, σ::Real)
    # Z and X are assumed to have the same size
    iσ = inv(σ)
    if μ == zero(μ)
        for i = 1 : length(X)
            @inbounds Z[i] = X[i] * iσ
        end
    else
        for i = 1 : length(X)
            @inbounds Z[i] = (X[i] - μ) * iσ
        end
    end
    return Z
end

@ngenerate N typeof(Z) function _zscore!{S,T,N}(Z::AbstractArray{S,N}, X::AbstractArray{T,N}, μ::AbstractArray, σ::AbstractArray)
    # Z and X are assumed to have the same size
    # μ and σ are assumed to have the same size, that is compatible with size(X)
    siz1 = size(X, 1)
    @nextract N ud d->size(μ, d)
    if size(μ, 1) == 1 && siz1 > 1
        @nloops N i d->(d>1 ? (1:size(X,d)) : (1:1)) d->(j_d = ud_d ==1 ? 1 : i_d) begin
            v = (@nref N μ j)
            c = inv(@nref N σ j)
            for i_1 = 1:siz1
                (@nref N Z i) = ((@nref N X i) - v) * c
            end
        end
    else
        @nloops N i X d->(j_d = ud_d ==1 ? 1 : i_d) begin
            (@nref N Z i) = ((@nref N X i) - (@nref N μ j)) / (@nref N σ j)
        end
    end
    return Z
end

function _zscore_chksize(X::AbstractArray, μ::AbstractArray, σ::AbstractArray)
    size(μ) == size(σ) || throw(DimensionMismatch("μ and σ should have the same size."))
    for i=1:ndims(X)
        dμ_i = size(μ,i)
        (dμ_i == 1 || dμ_i == size(X,i)) || throw(DimensionMismatch("X and μ have incompatible sizes."))
    end
end

"""
    zscore!([Z], X, μ, σ)

Compute the z-scores of an array `X` with mean `μ` and standard deviation `σ`.
z-scores are the signed number of standard deviations above the mean that an
observation lies. If a destination array `Z` is provided, the scores are stored
in `Z`, otherwise `X` is overwritten.
"""
function zscore!{ZT<:AbstractFloat,T<:Real}(Z::AbstractArray{ZT}, X::AbstractArray{T}, μ::Real, σ::Real)
    size(Z) == size(X) || throw(DimensionMismatch("Z and X must have the same size."))
    _zscore!(Z, X, μ, σ)
end

function zscore!{ZT<:AbstractFloat,T<:Real,U<:Real,S<:Real}(Z::AbstractArray{ZT}, X::AbstractArray{T},
                                                            μ::AbstractArray{U}, σ::AbstractArray{S})
    size(Z) == size(X) || throw(DimensionMismatch("Z and X must have the same size."))
    _zscore_chksize(X, μ, σ)
    _zscore!(Z, X, μ, σ)
end

zscore!{T<:AbstractFloat}(X::AbstractArray{T}, μ::Real, σ::Real) = _zscore!(X, X, μ, σ)

zscore!{T<:AbstractFloat,U<:Real,S<:Real}(X::AbstractArray{T}, μ::AbstractArray{U}, σ::AbstractArray{S}) =
    (_zscore_chksize(X, μ, σ); _zscore!(X, X, μ, σ))

"""
    zscore(X, [μ, σ])

Compute the z-scores of `X`, optionally specifying a precomputed mean `μ` and
standard deviation `σ`. z-scores are the signed number of standard deviations
above the mean that an observation lies. The mean and standard deviation can be
real numbers or arrays of real numbers.
"""
function zscore{T<:Real}(X::AbstractArray{T}, μ::Real, σ::Real)
    ZT = typeof((zero(T) - zero(μ)) / one(σ))
    _zscore!(Array(ZT, size(X)), X, μ, σ)
end

function zscore{T<:Real,U<:Real,S<:Real}(X::AbstractArray{T}, μ::AbstractArray{U}, σ::AbstractArray{S})
    _zscore_chksize(X, μ, σ)
    ZT = typeof((zero(T) - zero(U)) / one(S))
    _zscore!(Array(ZT, size(X)), X, μ, σ)
end

zscore{T<:Real}(X::AbstractArray{T}) = ((μ, σ) = mean_and_std(X); zscore(X, μ, σ))
zscore{T<:Real}(X::AbstractArray{T}, dim::Int) = ((μ, σ) = mean_and_std(X, dim); zscore(X, μ, σ))

#############################
#
#   entropy and friends
#
#############################

"""
    entropy(p, [b])

Compute the entropy of an array `p`, optionally specifying a real number
`b` such that the entropy is scaled by `1/log(b)`.
"""
function entropy{T<:Real}(p::AbstractArray{T})
    s = zero(T)
    z = zero(T)
    for i = 1:length(p)
        @inbounds pi = p[i]
        if pi > z
            s += pi * log(pi)
        end
    end
    return -s
end

function entropy{T<:Real}(p::AbstractArray{T}, b::Real)
    return entropy(p) / log(b)
end

"""
    renyientropy(p, α)

Compute the Rényi (generalized) entropy of order `α` of an array `p`.
"""
function renyientropy{T<:Real, U<:Real}(p::AbstractArray{T}, α::U)
    α < 0 && throw(ArgumentError("Order of Rényi entropy not legal, $(α) < 0."))

    s = zero(T)
    z = zero(T)
    scale = sum(p)

    if α ≈ 0
        for i = 1:length(p)
            @inbounds pi = p[i]
            if pi > z
                s += 1
            end
        end
        s = log(s / scale)
    elseif α ≈ 1
        for i = 1:length(p)
            @inbounds pi = p[i]
            if pi > z
                s -= pi * log(pi)
            end
        end
        s = s / scale
    elseif (isinf(α))
        s = -log(maximum(p))
    else # a normal Rényi entropy
        for i = 1:length(p)
            @inbounds pi = p[i]
            if pi > z
                s += pi ^ α
            end
        end
        s = log(s / scale) / (1 - α)
    end
    return s
end

"""
    crossentropy(p, q, [b])

Compute the cross entropy between `p` and `q`, optionally specifying a real
number `b` such that the result is scaled by `1/log(b)`.
"""
function crossentropy{T<:Real}(p::AbstractArray{T}, q::AbstractArray{T})
    length(p) == length(q) || throw(DimensionMismatch("Inconsistent array length."))
    s = 0.
    z = zero(T)
    for i = 1:length(p)
        @inbounds pi = p[i]
        @inbounds qi = q[i]
        if pi > z
            s += pi * log(qi)
        end
    end
    return -s
end

function crossentropy{T<:Real}(p::AbstractArray{T}, q::AbstractArray{T}, b::Real)
    return crossentropy(p,q) / log(b)
end

"""
    kldivergence(p, q, [b])

Compute the Kullback-Leibler divergence of `q` from `p`, optionally specifying
a real number `b` such that the divergence is scaled by `1/log(b)`.
"""
function kldivergence{T<:Real}(p::AbstractArray{T}, q::AbstractArray{T})
    length(p) == length(q) || throw(DimensionMismatch("Inconsistent array length."))
    s = 0.
    z = zero(T)
    for i = 1:length(p)
        @inbounds pi = p[i]
        @inbounds qi = q[i]
        if pi > z
            s += pi * log(pi / qi)
        end
    end
    return s
end

function kldivergence{T<:Real}(p::AbstractArray{T}, q::AbstractArray{T}, b::Real)
    return kldivergence(p,q) / log(b)
end

#############################
#
#   summary
#
#############################

immutable SummaryStats{T<:AbstractFloat}
    mean::T
    min::T
    q25::T
    median::T
    q75::T
    max::T
end

"""
    summarystats(a)

Compute summary statistics for a real-valued array `a`. Returns a
`SummaryStats` object containing the mean, minimum, 25th percentile,
median, 75th percentile, and maxmimum.
"""
function summarystats{T<:Real}(a::AbstractArray{T})
    m = mean(a)
    qs = quantile(a, [0.00, 0.25, 0.50, 0.75, 1.00])
    R = typeof(convert(AbstractFloat, zero(T)))
    SummaryStats{R}(
        convert(R, m),
        convert(R, qs[1]),
        convert(R, qs[2]),
        convert(R, qs[3]),
        convert(R, qs[4]),
        convert(R, qs[5]))
end

function Base.show(io::IO, ss::SummaryStats)
    println(io, "Summary Stats:")
    @printf(io, "Mean:         %.6f\n", ss.mean)
    @printf(io, "Minimum:      %.6f\n", ss.min)
    @printf(io, "1st Quartile: %.6f\n", ss.q25)
    @printf(io, "Median:       %.6f\n", ss.median)
    @printf(io, "3rd Quartile: %.6f\n", ss.q75)
    @printf(io, "Maximum:      %.6f\n", ss.max)
end

"""
    describe(a)

Pretty-print the summary statistics provided by `summarystats`:
the mean, minimum, 25th percentile, median, 75th percentile, and
maximum.
"""
describe{T<:Real}(a::AbstractArray{T}) = show(summarystats(a))


using Distributions, Plots, LaTeXStrings; pyplot()

function statPair(dist,n)
    sample = rand(dist,n)
    [mean(sample),var(sample)]
end

stdUni = Uniform(-sqrt(3),sqrt(3))
n, N = 3, 10^5

dataUni     = [statPair(stdUni,n) for _ in 1:N]
dataUniInd  = [[mean(rand(stdUni,n)),var(rand(stdUni,n))] for _ in 1:N]
dataNorm    = [statPair(Normal(),n) for _ in 1:N]
dataNormInd = [[mean(rand(Normal(),n)),var(rand(Normal(),n))] for _ in 1:N]

p1 = scatter(first.(dataUni), last.(dataUni),
	c=:blue, ms=1, msw=0, label="Same group")
p1 = scatter!(first.(dataUniInd), last.(dataUniInd),
	c=:red, ms=0.8, msw=0, label="Separate group",
	xlabel=L"\overline{X}", ylabel=L"S^2")

p2 = scatter(first.(dataNorm), last.(dataNorm),
	c=:blue, ms=1, msw=0, label="Same group")
p2 = scatter!(first.(dataNormInd), last.(dataNormInd),
	c=:red, ms=0.8, msw=0, label="Separate group",
	xlabel=L"\overline{X}", ylabel=L"$S^2$")

plot(p1, p2, ylims=(0,5), size=(800, 400))


# prime fields =========================================================================== #
"""
    is_prime(n)

Return `true` if `n` is a prime number.
"""
Base.@pure function is_prime(n::Int)
    if iseven(n) || n < 2
        return n == 2
    else
        p = 3
        q = n ÷ p
        while p ≤ q
            iszero(n % p) && return false
            p += 2
            q = n ÷ p
        end
        return true
    end
end
# The following causes an error if you try to use anything other than Int as a modulus.
is_prime(::Any) = false

"""
    mod_prime(i, ::Val{M})

Like `mod`, but with prime `M`.
"""
function mod_prime(i, ::Val{M}) where {M}
    is_prime(M) || throw(DomainError(M, "modulus must be a prime number"))
    i = i % M
    return i + ifelse(signbit(i), M, 0)
end
mod_prime(i, ::Val{2}) = i & 1

"""
    Mod{M} <: Integer

`Mod{M}` is the default field used by Ripserer. It is a representation of a finite field
``\\mathbb{Z}_M``, integers modulo small, prime `M`. Supports field arithmetic and can be
converted to integer with `Int`.

Its values are not comparable on purpose.

# Example

```jldoctest
julia> Mod{3}(5)
2 mod 3

julia> Mod{3}(5) + 1
0 mod 3

```
"""
struct Mod{M} <: Integer
    value::Int

    # Check mod allows construction when you know you don't need to mod the number.
    function Mod{M}(value::Integer, check_mod=true) where {M}
        if check_mod
            return new{M}(mod_prime(value, Val(M)))
        else
            return new{M}(value)
        end
    end
end
Mod{M}(i::Mod{M}) where {M} = i

Base.Int(i::Mod) = i.value

Base.show(io::IO, i::Mod{M}) where {M} = print(io, Int(i), " mod ", M)

for op in (:+, :-, :*)
    @eval (Base.$op)(i::Mod{M}, j::Mod{M}) where {M} = Mod{M}($op(Int(i), Int(j)))
end

Base.:/(i::Mod{M}, j::Mod{M}) where {M} = i * inv(j)
Base.:-(i::Mod{M}) where {M} = Mod{M}(M - Int(i), false)
Base.zero(::Type{Mod{M}}) where {M} = Mod{M}(0, false)
Base.one(::Type{Mod{M}}) where {M} = Mod{M}(1, false)
Base.sign(i::M) where {M<:Mod} = ifelse(iszero(i), zero(M), one(M))

Base.promote_rule(::Type{Mod{M}}, ::Type{<:Integer}) where {M} = Mod{M}

Base.inv(i::Mod{M}) where {M} = Mod{M}(invmod(Int(i), M), false)


using CSV, Distributions, HypothesisTests

data1 = CSV.read("../data/machine1.csv", header=false)[:,1]
data2 = CSV.read("../data/machine2.csv", header=false)[:,1]
xBar1, s1, n1 = mean(data1), std(data1), length(data1)
xBar2, s2, n2 = mean(data2), std(data2), length(data2)
delta0 = 0

v = ( s1^2/n1 + s2^2/n2 )^2 / ( (s1^2/n1)^2/(n1-1) + (s2^2/n2)^2/(n2-1)  )
testStatistic = ( xBar1-xBar2 - delta0 )  / sqrt( s1^2/n1 + s2^2/n2 )
pVal = 2*ccdf(TDist(v), abs(testStatistic))

println("Manually calculated degrees of freedom, v: ", v)
println("Manually calculated test statistic: ", testStatistic)
println("Manually calculated p-value: ", pVal, "\n")
println(UnequalVarianceTTest(data1, data2, delta0))


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

# # Modeling data 2

#-

# ## Building a model
#
# Recall that in notebook 3, we saw that we could use a mathematical function to classify an image as an apple or a banana, based on the average amount of green in an image:
#
#
#
#
#

#-

# A common function for performing this kind of **classification** is the sigmoid that we saw in the last notebook, and that we will now extend by adding two **parameters**, $w$ and $b$:
#
# $$\sigma(x; w, b) := \frac{1}{1 + \exp(-wx + b)}$$
#
# $$ x = \mathrm{data} $$
#
# \begin{align}
# \sigma(x;w,b) &\approx 0 \implies \mathrm{apple} \\
# \sigma(x;w,b) &\approx 1 \implies \mathrm{banana}
# \end{align}

#-

# In our mathematical notation above, the `;` in the function differentiates between the **data** and the **parameters**. `x` is the data and is determined from the image. The parameters, `w` and `b`, are numbers which we choose to make our function match the results it should be modeling.
#
# Note that in the code below, we don't distinguish between data and parameters - both are just inputs to our function, σ!

using Images, Statistics

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)))

println("Average green for apple = $apple_green_amount")
println("Average green for banana = $banana_green_amount")

#-

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

# What we want is that when we give σ as input the average green for the apple, roughly `x = 0.3385`, it should return as output something close to 0, meaning "apple". And when we give σ the input `x = 0.8808`, it should output something close to 1, meaning "banana".
#
# By changing the parameters of the function, we can change the shape of the function, and hence make it represent, or **fit**, the data better!

#-

# ## Data fitting by varying parameters

#-

# We can understand how our choice of `w` and `b` affects our model by seeing how our values for `w` and `b` change the plot of the $\sigma$ function.

using Plots; gr()   # GR works better for interactive manipulations

# Run the code in the next cell. You should see two "sliders" appear, one for `w` and one for `b`.
#
# **Game**:
# Change w and b around until the blue curve, labeled "model", which is the graph of the `\sigma` function, passes through *both* of the data points at the same time.

w = 10.0 # try manipulating w between -10 and 30
b = 10.0 # try manipulating b between 0 and 20

plot(x -> σ(x, w, b), xlim=(-0,1), ylim=(-0.1,1.1), label="model", legend=:topleft, lw=3)

scatter!([apple_green_amount],  [0.0], label="apple", ms=5)   # marker size = 5
scatter!([banana_green_amount], [1.0], label="banana", ms=5)

# Notice that the two parameters do two very different things. The **weight**, `w`, determines *how fast* the transition between 0 and 1 occurs. It encodes how trustworthy we think our data  actually is, and in what range we should be putting points between 0 and 1 and thus calling them "unsure". The **bias**, `b`, encodes *where* on the $x$-axis the switch should take place. It can be seen as shifting the function left-right. We'll come to understand these *parameters* more in notebook 6.
#
# Here are some parameter choices that work well:

w = 25.58; b = 15.6

plot(x -> σ(x, w, b), xlim=(0,1), ylim=(-0.1,1.1), label="model", legend=:topleft, lw=3)

scatter!([apple_green_amount], [0.0], label="apple")
scatter!([banana_green_amount],[1.0], label="banana")

# (Note that in this problem there are many combinations of `w` and `b` that fit the data well.)

#-

# Once we have a model, we have a computational representation for how to choose between "apple" and "banana". So let's pull in some new images and see what our model says about them!

apple2 = load(datapath("data/107_100.jpg"))

#-

green_amount = mean(Float64.(green.(apple2)))
@show green_amount

scatter!([green_amount], [0.0], label="new apple")

# Our model successfully says that our new image is an apple! Pat yourself on the back: you've actually just trained your first neural network!

#-

# #### Exercise 1
#
# Load the image of a banana in `data/8_100.jpg` as `mybanana`. Edit the code below to calculate the amount of green in `mybanana` and to overlay data for this image with the existing model and data points.

#-

# #### Solution

#-

# To get the desired overlay, the code we need is

mybanana = load(datapath("data/8_100.jpg"))
mybanana_green_amount = mean(Float64.(green.(banana)))
scatter!([mybanana_green_amount], [1.0], label="my banana")

# ## Closing remarks: bigger models, more data, more accuracy
#
# That last apple should start making you think: not all apples are red; some are yellow. "Redness" is one attribute of being an apple, but isn't the whole thing. What we need to do is incorporate more ideas into our model by allowing more inputs. However, more inputs would mean more parameters to play with. Also, we would like to have the computer start "learning" on its own, instead of modifying the parameters ourselves until we think it "looks right". How do we take the next step?
#
# The first thing to think about is, if you wanted to incorporate more data into the model, how would you change the sigmoid function? Play around with some ideas. But also, start thinking about how you chose parameters. What process did you do to finally end up at good parameters? These two problems (working with models with more data and automatically choosing parameters) are the last remaining step to understanding deep learning.

Courses/Foundations of machine learning/0500.Building-models.jl