### A Pluto.jl notebook ###
# v0.14.5

using Markdown
using InteractiveUtils

# ╔═╡ cea30482-e74a-41b0-bdc5-17c2a1c5cf91
md" ## LagrangeInterpolantGenerator.jl"

# ╔═╡ 97e64f8b-18a2-42dd-8872-2acd7983fe09
md" ##### See `https://discourse.julialang.org/t/lagrange-interpolation-function-generator` and the comments on that post. See also the BaricentricInterpolation.jl example (j-05-01b.jl)"

# ╔═╡ 19161060-226e-4469-b13c-ca41a8eace3f
function LagrangeInterpolantGenerator(xvals, yvals)
	function LagrangeInterpolant(x)
		numvalstoevaluate = length(x)
        numvalstoevaluate == 1 ? output = 0 : output = zeros(numvalstoevaluate)
        for k = 1:numvalstoevaluate
            N = length(xvals)
            LagrangePolynomials = ones(N)
            for i in 1:N
				# Surprisingly, this works even in the i=1 and i=N cases.
                for j in [1:i-1;i+1:N]
                    LagrangePolynomials[i] = LagrangePolynomials[i].*(x[k] -
						xvals[j])./(xvals[i]-xvals[j])
                end
            end
            numvalstoevaluate == 1 ? output = sum(LagrangePolynomials.*yvals) :
				output[k] = sum(LagrangePolynomials.*yvals)
        end
        return output
    end
    return LagrangeInterpolant
end

# ╔═╡ 3ce58d80-543c-42b0-9830-12ffaee6e844
begin
	x = [1.0, 3.0, 6.0, 5.0]
	y = [1.0, 5.0, 10.0, 9.0]
	xi = [2.0, 4.5]
end

# ╔═╡ e4e3e4de-2dbe-4a0d-9589-fb8456781a73
interpolantfunc = LagrangeInterpolantGenerator(x,y);

# ╔═╡ 748369b7-3df8-4bed-a125-fa812a92ed1c
interpolantfunc.(xi)

# ╔═╡ ccf3b050-7cac-41c4-baf4-c6e535590742
md" ## End of LagrageInterpolantGenerator.jl"

# ╔═╡ Cell order:
# ╟─cea30482-e74a-41b0-bdc5-17c2a1c5cf91
# ╠═97e64f8b-18a2-42dd-8872-2acd7983fe09
# ╠═19161060-226e-4469-b13c-ca41a8eace3f
# ╠═3ce58d80-543c-42b0-9830-12ffaee6e844
# ╠═e4e3e4de-2dbe-4a0d-9589-fb8456781a73
# ╠═748369b7-3df8-4bed-a125-fa812a92ed1c
# ╟─ccf3b050-7cac-41c4-baf4-c6e535590742


"""
    gaussSeidel(A,B,Xawal)
adalah fungsi yang digunakan untuk mencari solusi SPL `Ax=B` secara iteratif dengan
nilai tebakan awal `Xawal` menggunakan metode Gauss-Seidel.

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

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

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

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

julia> X
3-element Array{Float64,1}:
 1.99999999743314
 3.999999998137355
 2.999999999345785

julia> flag
0

julia> err
2.1378379298357413e-8

julia> M
11×5 Array{Float64,2}:
  0.0  1.0      2.0      2.0      NaN
  1.0  1.75     3.75     2.95       2.12779
  2.0  1.95     3.96875  2.98625    0.298606
  3.0  1.99562  3.99609  2.99903    0.0547054
  ⋮
  7.0  2.0      4.0      3.0        1.21519e-5
  8.0  2.0      4.0      3.0        1.34052e-6
  9.0  2.0      4.0      3.0        1.85294e-7
 10.0  2.0      4.0      3.0        2.13784e-8
```
return solusi `X` dengan `flag` bernilai 0 jika metode Gauss-Seidel berhasil menemukan
solusi dan gagal jika tidak 0. Serta, matriks `M` yang berisi catatan proses tiap
iterasi `[k, X', error]`
"""
function gaussSeidel(A,B,Xawal)
    delta = 10^-7;
    maxi = 100;
    flag = 1;
    D = Diagonal(diag(A))
    R = tril(A);
    U = triu(A,1);
    X = Xawal[:];
    M = [0 X' NaN];
    for k = 1:maxi
        Xlama = X;
        X = inv(R)*(B - U*Xlama);
        err = norm(X-Xlama);
        M = [M; [k X' err] ];
        if err<delta
            flag = 0;
            break
        end
    end
    err = M[end,end]
    return X, flag, err, M
end


using Random, Distributions, KernelDensity, Plots, LaTeXStrings; pyplot()
Random.seed!(1)

function pval(mu0,mu,sig,n)
    sample = rand(Normal(mu,sig),n)
    xBar   = mean(sample)
    s      = std(sample)
    tStat  = (xBar-mu0) / (s/sqrt(n))
    ccdf(TDist(n-1), tStat)
end

mu0, mu1A, mu1B  = 20, 22, 24
sig, n, N = 7, 5, 10^6

pValsH0 = [pval(mu0,mu0,sig,n) for _ in 1:N]
pValsH1A = [pval(mu0,mu1A,sig,n) for _ in 1:N]
pValsH1B = [pval(mu0,mu1B,sig,n) for _ in 1:N]

alpha = 0.05
estPwr(pVals) = sum(pVals .< alpha)/N

println("Power under H0:  ", estPwr(pValsH0))
println("Power under H1A: ", estPwr(pValsH1A))
println("Power under H1B (mu's farther apart): ", estPwr(pValsH1B))

stephist(pValsH0, bins=100,
	normed=true, c=:blue, label="Under H0")
stephist!(pValsH1A, bins=100,
	normed=true, c=:red, label="Under H1A")
stephist!(pValsH1B, bins=100,
	normed=true, c=:green, label="Under H1B",
    xlims=(0,1), ylims=(0,6), xlabel = "p-value", ylabel = "Density")

plot!([alpha,alpha],[0,6],
	c=:black, ls=:dash, label=L"\alpha")


function euclid_mod(a::Int64, b::Int64)
    a = abs(a)
    b = abs(b)

    while(b != 0)
        b,a = a%b,b
    end

    return a
end

function euclid_sub(a::Int64, b::Int64)
    a = abs(a)
    b = abs(b)

    while (a != b)
        if (a > b)
            a -= b
        else
            b -= a
        end
    end

    return a
end

function main()
    check1 = euclid_mod(64 * 67, 64 * 81);
    check2 = euclid_sub(128 * 12, 128 * 77);

    println("Modulus-based euclidean algorithm result: $(check1)")
    println("subtraction-based euclidean algorithm result: $(check2)")

end

main()


"""
    richardson(f, a; delta = 1e-9)
adalah fungsi yang digunakan untuk mencari nilai turunan `f` pada titik `a`
menggunakan formula ekstrapolasi Richardson. Secara default, digunakan nilai
toleransi `delta=1e-9`

# Examples
```jl
julia> sol,flag,err,D = richardson(sin,pi/3);

julia> sol
0.49999999999998856

julia> flag
0

julia> err
3.323475383787411e-10

julia> D
5×5 Array{Float64,2}:
 0.420735  0.0       0.0       0.0  0.0
 0.479426  0.498989  0.0       0.0  0.0
 0.494808  0.499935  0.499998  0.0  0.0
 0.498699  0.499996  0.5       0.5  0.0
 0.499675  0.5       0.5       0.5  0.5

julia> sol,flag,err,D = richardson(sin,pi/3,delta=1e-12);

julia> sol
0.5000000000000013

julia> flag
0

julia> err
1.27675647831893e-14

julia> D
6×6 Array{Float64,2}:
 0.420735  0.0       0.0       0.0  0.0  0.0
 0.479426  0.498989  0.0       0.0  0.0  0.0
 0.494808  0.499935  0.499998  0.0  0.0  0.0
 0.498699  0.499996  0.5       0.5  0.0  0.0
 0.499675  0.5       0.5       0.5  0.5  0.0
 0.499919  0.5       0.5       0.5  0.5  0.5
```
return solusi `sol` dengan `flag` bernilai 0 jika toleransi terpenuhi, `flag`
bernilai 1 jika maksimum iterasi tercapai. Serta, error `err` dan matriks `D`
yang berisi tabel ekstrapolasi Richardson.
"""
function richardson(f, a; delta = 1e-9)
  maxi = 50;
  flag = 1;
  h = 1;
  D = (f(a+h)-f(a-h))/(2*h);
  err = NaN;
  for j=1:maxi
    h = h/2;
    D = [D zeros(size(D,1),1);
		(f(a.+h).-f(a.-h))./(2*h) zeros(1, size(D,1))];
    for k = 1:j
      D[j+1,k+1] = D[j+1,k] + (D[j+1,k]-D[j,k])/(4^k-1);
    end
    err = abs(D[j+1,j+1]-D[j,j]);
    if err<delta
      flag=0;
      break
    end
  end
  sol = D[end,end];
  return sol, flag, err, D
end


"""
    perfect_number(number)

Checks if a number is a perfect_number number or not

# Details

perfect_number number is a positive integer that is equal to the sum of
its positive divisors, excluding the number itself.

For example : 6 is perfect_number number

Divisors of 6 => [1,2,3]

Sum of divisors => 1+2+3 = 6

6 == sum(divisors) # which is true

# Example

```julia
perfect_number(27)     # returns false
perfect_number(28)     # returns true
perfect_number(496)    # returns true
perfect_number(8128)   # returns true
perfect_number(123)    # returns false
```

Contributed By:- [Ashwani Rathee](https://github.com/ashwani-rathee)
"""
function perfect_number(number)
    divisors = []
    for i = 1:(number÷2+1)
        if number % i == 0
            push!(divisors, i)
        end
    end
    return sum(divisors) == number
end


"""
newton(f,dfdx,x1)

Use Newton's method to find a root of `f` starting from `x1`, where
`dfdx` is the derivative of `f`. Returns a vector of root estimates.
"""
function newton(f,dfdx,x1)
    # Operating parameters.
    funtol = 100*eps();  xtol = 100*eps();  maxiter = 40;

    x = [x1]
    y = f(x1)
    dx = Inf   # for initial pass below
    k = 1

    while (abs(dx) > xtol) && (abs(y) > funtol) && (k < maxiter)
        dydx = dfdx(x[k])
        dx = -y/dydx            # Newton step
        push!(x,x[k]+dx)        # append new estimate

        k = k+1
        y = f(x[k])
    end

    if k==maxiter
        @warn "Maximum number of iterations reached."
    end

    return x
end

"""
secant(f,x1,x2)

Use the secant method to find a root of `f` starting from `x1` and
`x2`. Returns a vector of root estimates.
"""
function secant(f,x1,x2)
    # Operating parameters.
    funtol = 100*eps();  xtol = 100*eps();  maxiter = 40;

    x = [x1,x2]
    y1 = f(x1); y2 = 100;
    dx = Inf   # for initial pass below
    k = 2

    while (abs(dx) > xtol) && (abs(y2) > funtol) && (k < maxiter)
        y2 = f(x[k])
        dx = -y2 * (x[k]-x[k-1]) / (y2-y1)   # secant step
        push!(x,x[k]+dx)        # append new estimate

        k = k+1
        y1 = y2    # current f-value becomes the old one next time
    end

    if k==maxiter
        @warn "Maximum number of iterations reached."
    end

    return x
end

"""
newtonsys(f,jac,x1)

Use Newton's method to find a root of a system of equations,
starting from `x1`. The functions `f` and `jac should return the
residual vector and the Jacobian matrix, respectively. Returns
history of root estimates as a vector of vectors.
"""
function newtonsys(f,jac,x1)
    # Operating parameters.
    funtol = 1000*eps();  xtol = 1000*eps();  maxiter = 40;

    x = [float(x1)]
    y,J = f(x1),jac(x1)
    dx = Inf   # for initial pass below
    k = 1

    while (norm(dx) > xtol) && (norm(y) > funtol) && (k < maxiter)
        dx = -(J\y)             # Newton step
        push!(x,x[k] + dx)    # append to history
        k += 1
        y,J = f(x[k]),jac(x[k])
    end

    if k==maxiter
        @warn "Maximum number of iterations reached."
    end

    return x
end

"""
fdjac(f,x0,y0)

Compute a finite-difference approximation of the Jacobian matrix for
`f` at `x0`, where `y0`=`f(x0)` is given.
"""
function fdjac(f,x0,y0)

delta = sqrt(eps())   # FD step size
m,n = length(y0),length(x0)
if n==1
    J = (f(x0+delta) - y0) / delta
else
    J = zeros(m,n)
    In = I(n)
    for j = 1:n
        J[:,j] = (f(x0 + delta*In[:,j]) - y0) / delta
    end
end

return J
end

"""
levenberg(f,x1,tol)

Use Levenberg's quasi-Newton iteration to find a root of the system
`f`, starting from `x1`, with `tol` as the stopping tolerance in
both step size and residual norm. Returns root estimates as a
matrix, one estimate per column.
"""
function levenberg(f,x1,tol=1e-12)

# Operating parameters.
ftol = tol;  xtol = tol;  maxiter = 40;

x = zeros(length(x1),maxiter)
x = [float(x1)]
fk = f(x1)
k = 1;  s = Inf;
Ak = fdjac(f,x1,fk)   # start with FD Jacobian
jac_is_new = true

lambda = 10;
while (norm(s) > xtol) && (norm(fk) > ftol) && (k < maxiter)
    # Compute the proposed step.
    B = Ak'*Ak + lambda*I
    z = Ak'*fk
    s = -(B\z)

    xnew = x[k] + s
    fnew = f(xnew)

    # Do we accept the result?
    if norm(fnew) < norm(fk)    # accept
        y = fnew - fk
        push!(x,xnew)
        fk = fnew
        k += 1

        lambda = lambda/10   # get closer to Newton
        # Broyden update of the Jacobian.
        Ak = Ak + (y-Ak*s)*(s'/(s'*s))
        jac_is_new = false
    else                       # don't accept
        # Get closer to steepest descent.
        lambda = 4lambda
        # Re-initialize the Jacobian if it's out of date.
        if !jac_is_new
            Ak = fdjac(f,x[k],fk)
            jac_is_new = true
        end
    end
end

if (norm(fk) > 1e-3)
    @warn "Iteration did not find a root."
end

return x
end


using InstantiateFromURL
# optionally add arguments to force installation: instantiate = true, precompile = true
github_project("QuantEcon/quantecon-notebooks-julia", version = "0.8.0")

using LinearAlgebra, Statistics
using QuantEcon, Plots
gr(fmt=:png);

# Exercise 1
ϕ_0, ϕ_1, ϕ_2 = 1.1, 0.8, -0.8

A = [1.0 0.0 0.0; ϕ_0 ϕ_1 ϕ_2; 0.0 1.0 0.0]
C = zeros(3,1)
G = [0 1 0]
μ_0 =  ones(3)

lss = LSS(A,C,G; mu_0 = μ_0)

x,y = simulate(lss, 50)
plot(dropdims(y, dims = 1), color = :blue)
plot!(xlabel="time", ylabel = "y_t", legend = :none)

# Exercise 2

using Random
Random.seed!(42) # For deterministic results.

ϕ1, ϕ2, ϕ3, ϕ4 = 0.5, -0.2, 0, 0.5
σ = 0.2

A = [ϕ1 ϕ2 ϕ3 ϕ4; 1.0 0.0 0.0 0.0; 0.0 1.0 0.0 0.0; 0.0 0.0 1.0 0.0]
C = [σ; 0.0; 0.0; 0.0]
G = [1.0 0.0 0.0 0.0]
μ_0 =  ones(4)

lss = LSS(A,C,G; mu_0 = μ_0)

x,y = simulate(lss, 200)
plot(dropdims(y, dims = 1), color = :blue)
plot!(xlabel="time", ylabel = "y_t", legend = :none)

# Exercise 3
ϕ1, ϕ2, ϕ3, ϕ4 = 0.5, -0.2, 0, 0.5
σ = 0.1

A = [ϕ1 ϕ2 ϕ3 ϕ4; 1.0 0.0 0.0 0.0; 0.0 1.0 0.0 0.0; 0.0 0.0 1.0 0.0]
C = [σ; 0.0; 0.0; 0.0]
G = [1.0 0.0 0.0 0.0]
μ_0 = ones(4)

I = 20
T = 50

ar = LSS(A, C, G; mu_0 = μ_0)

ensemble_mean = zeros(T)
ys = []
for ii in 1:I
        x,y = simulate(ar, T)
        y = dropdims(y, dims=1)
        push!(ys,y)
        ensemble_mean .+= y
end
ensemble_mean = ensemble_mean./I

plot(ensemble_mean, label = "y_t_bar", linewidth = 2)
plot!(ys, linewidth = 0.8, alpha = 0.2, label = "", color =:blue)

ymin, ymax = -0.5, 1.15
m = moment_sequence(ar)
pop_means = zeros(0)
for (i, t) ∈ enumerate(m)
    (μ_x, μ_y, Σ_x, Σ_y) = t
    push!(pop_means, μ_y[1])
end
plot!(pop_means, color = :green, linewidth = 2, label = "G mu_t")
plot!(ylims=(ymin, ymax), xlabel = "time", ylabel = "y_t", legendfont = font(12))


using JuMP
using HiGHS
using SCS
using LinearAlgebra

using Graphs
using GenericTensorNetworks
using Random
#g = smallgraph(:petersen)
g = random_diagonal_coupled_graph(10, 10, 0.8)

# MIT course: http://people.csail.mit.edu/moitra/854.html
function maxcut_ilp(g::SimpleGraph; optimizer=HiGHS.Optimizer)
    ws = ones(Int,ne(g))
    edgs = collect(edges(g))
    model = Model(optimizer)
    @variable model xs[1:nv(g)] Bin
    @variable model zs[1:ne(g)] Bin
    @constraint model [i=1:ne(g)] zs[i] <= xs[edgs[i].src] + xs[edgs[i].dst]
    @constraint model [i=1:ne(g)] zs[i] <= 2 - xs[edgs[i].src] - xs[edgs[i].dst]
    @objective model Max ws' * zs
    optimize!(model)
    return objective_value(model), value.(xs)
end

# see also: https://jump.dev/JuMP.jl/stable/tutorials/conic/max_cut_sdp/
function maxcut_relaxed_sdp(g::SimpleGraph; optimizer=SCS.Optimizer)
    L = laplacian_matrix(g)
    model = Model(optimizer)
    # Start with X as the identity matrix to avoid numerical issues.
    @variable model X[i=1:nv(g), j=1:nv(g)] PSD start=(i==j ? 1.0 : 0.0)
    @constraint model diag(X) .== 1
    @objective model Max 0.25 * dot(L, X)
    optimize!(model)
    @assert termination_status(model) == MOI.OPTIMAL
    return objective_value(model), value.(X)
end

function goemans_williamson_postprocess(X)
    num_vertex = size(X, 1)
    # small positive number to avoid zero
    V = cholesky(X + 1e-3 * I).U
    # Generate random vector on unit sphere.
    r = rand(size(V, 1))
    r /= LinearAlgebra.norm(r)
    # Iterate over vertices, and assign each vertex to a side of cut.
    cut = ones(num_vertex)
    for i in 1:num_vertex
        if LinearAlgebra.dot(r, V[:, i]) <= 0
            cut[i] = -1
        end
    end
    return cut
end

function goemans_williamson(g::SimpleGraph)
    loss, X = maxcut_relaxed_sdp(g)
    @info "Got loss = $(loss)"
    return goemans_williamson_postprocess(X)
end

@time solve(MaxCut(g; optimizer=TreeSA(ntrials=3, βs=0.1:0.1:30, niters=10, sc_target=25)), SizeMax())
#function addvar!(model::Model, ::Type{T}, name::String; lower_bound::Union{T,Nothing}=nothing, upper_bound::Union{T,Nothing}=nothing, fixed_value::Union{T,Nothing}=nothing, start) where T
#end
#JuMP.add_variable(model, JuMP.build_variable(error, JuMP.VariableInfo(true, 0, false, NaN, false, NaN, false, NaN, false, true)), "x")
#model[:x] = var"#25###291"


using Jacobi, Plots
gr(size=(700,700))

ProjDir = dirname(@__FILE__)
cd(ProjDir) #do

  if !isdefined(Main, :lgt)
    function lgt(Q, nx, lb, ub)
      z = zglj(Q)
      x = collect(linspace(lb, ub, nx))
      y = zeros(nx, Q)
      for k = 1:Q, i=1:nx
        y[i,k]=lagrange(k,x[i],z)
      end
      (x, y)
    end
  end

  Q = 5
  nx = 201
  lb = -1
  ub = 1
  (x, y) = lgt(Q, nx, lb, ub)

  p = plot()
  for k = 1:Q
    plot!(x, y[:, k], label="Q = $k")
  end

  plot(p)
  savefig("lagrange_n201.png")
  gui()

  Q = 5
  nx = 3
  lb = -1
  ub = 1
  (x, y) = lgt(Q, nx, lb, ub)
  x |> display
  println()
  y |> display
  println()

  p = plot()
  for k = 1:Q
    plot!(x, y[:, k], label="Q = $k")
  end

  plot(p)
  savefig("lagrange_n3.png")
  gui()

#end


"""
   `ecc_anom_init_guess_danby(mean_anomaly, eccentricity)`

Returns initial guess for the eccentric anomaly for use by itterative solvers of Kepler's equation for bound orbits.

Based on "The Solution of Kepler's Equations - Part Three"
Danby, J. M. A. (1987) Journal: Celestial Mechanics, Volume 40, Issue 3-4, pp. 303-312 (1987CeMec..40..303D)
"""
function ecc_anom_init_guess_danby(M::Real, ecc::Real)
	@assert -2π<= M <= 2π
	@assert 0 <= ecc <= 1.0
    if  M < zero(M)
		M += 2π
	end
    E = (M<π) ? M + 0.85*ecc : M - 0.85*ecc
end;

"""
   `update_ecc_anom_laguerre(eccentric_anomaly_guess, mean_anomaly, eccentricity)`

Update the current guess for solution to Kepler's equation

Based on "An Improved Algorithm due to Laguerre for the Solution of Kepler's Equation"
   Conway, B. A.  (1986) Celestial Mechanics, Volume 39, Issue 2, pp.199-211 (1986CeMec..39..199C)
"""
function update_ecc_anom_laguerre(E::Real, M::Real, ecc::Real)
  (es, ec) = ecc .* sincos(E)
  F = (E-es)-M
  Fp = one(M)-ec
  Fpp = es
  n = 5
  root = sqrt(abs((n-1)*((n-1)*Fp*Fp-n*F*Fpp)))
  denom = Fp>zero(E) ? Fp+root : Fp-root
  return E-n*F/denom
end;

"""
   `calc_ecc_anom( mean_anomaly, eccentricity )`
   `calc_ecc_anom( param::Vector )``
	Estimates eccentric anomaly for given 'mean_anomaly' and 'eccentricity'.
If passed a parameter vector, param[1] = mean_anomaly and param[2] = eccentricity.
Optional parameter `tol` specifies tolerance (default 1e-8)
"""
function calc_ecc_anom end
function calc_ecc_anom(mean_anom::Real, ecc::Real; tol::Real = 1.0e-8)
	@assert 0 <= ecc <= 1.0
	@assert 1e-16 <= tol < 1
  	M = rem2pi(mean_anom,RoundNearest)
    E = ecc_anom_init_guess_danby(M,ecc)
	local E_old
    max_its_laguerre = 200
    for i in 1:max_its_laguerre
       E_old = E
       E = update_ecc_anom_laguerre(E_old, M, ecc)
       if abs(E-E_old) < tol break end
    end
    return E
end

function calc_ecc_anom(param::Vector; tol::Real = 1.0e-8)
	@assert length(param) == 2
	calc_ecc_anom(param[1], param[2], tol=tol)
end


#=
The area of a circle is defined as πr^2. Estimate π to 3 decimal places using a Monte Carlo method.

Hint: The basic equation of a circle is x2 + y2 = r2
=#
using Test
include("Solutions/problem14_estimate_pi.jl")

@test abs(estimate_pi() - pi) <= 0.001
@test abs(estimate_pi(number_of_decimal = 5) - pi) <= 0.00001


"""
    gauss_jordan(A::AbstractMatrix{T}) where T<:Number

Gaussian elimination, also known as row reduction, is an algorithm for solving systems of linear equations.
It consists of a sequence of operations performed on the corresponding matrix of coefficients.
This method can also be used to compute the rank of a matrix, the determinant of a square matrix, and the inverse of an invertible matrix.
https://en.wikipedia.org/wiki/Gaussian_elimination

# Examples/Tests
```julia
julia> M1 = [1 2 3; 4 5 6];
julia> M2 = [1 2 3; 4 8 12];

julia> @test gauss_jordan(M1) == [1 0 -1; 0 1 2]        # Test Passed
julia> @test_throws AssertionError gauss_jordan(M2)     # Test Passed - Thrown: AssertionError
```

# Contributed By:- [AugustoCL](https://github.com/AugustoCL)
"""
function gauss_jordan(A::AbstractMatrix{T}) where {T<:Number}

    # check if matrix is singular
    m, n = size(A)
    if m == n
        @assert det(A) ≠ 0.0 "Must insert a non-singular matrix"
    else
        @assert det(A[:, 1:end-1]) ≠ 0.0 "Must insert a non-singular matrix or a system matrix [A b]"
    end

    # execute the gauss-jordan elimination
    for i ∈ axes(A, 1)

        if A[i, i] == 0.0
            for n ∈ (i + 1):m                           # iterate in lines below to check if could be swap
                if A[n, i] ≠ 0.0                        # condition to swap row
                    L = copy(A[i, :])                   # copy line to swap
                    A[i, :] = A[n, :]                   # swap occur
                    A[n, :] = L
                    break
                end
            end
        end

        @. A[i, :] = A[i, :] ./ A[i, i]                  # divide pivot line by pivot element

        for j ∈ axes(A, 1)                               # iterate each line for each pivot column, except pivot line
            if j ≠ i                                     # jump pivot line
                @. A[j, :] = A[j, :] - A[i, :]*A[j, i]   # apply gauss jordan in each line
            end
        end
    end

    return A
end

# using multiple dispatch to avoid InexactError with Integers
gauss_jordan(A::AbstractMatrix{T}) where {T<:Integer} = gauss_jordan(convert(Matrix{Float64}, A))


"""
    tern2cart(a, b, c)

Takes a point in the ternary coordinate system and returns the cartesian co-ordinate of this point.
The ternary axes are all between 0 and 1 and the cartesian co-ordinates form an equilateral triangle at (0, 0), (1, 0), (1/2, √3/2)
"""
tern2cart(a, b, c) = (1 / 2 * (2b + c) / (a + b + c), √3 / 2 * (c / (a + b + c)))
tern2cart(vec) = tern2cart(vec[1], vec[2], vec[3])

"""
    cart2tern(x, y)

Takes a point in the cartesian coordinate system and returns the ternary co-ordinate of this point.
"""
function cart2tern(x, y)
    c = (2 * y) / √3
    b = x - c / 2
    a = 1 - b - c
    return (a, b, c)
end
cart2tern(array) = cart2tern(array[1], array[2])


"Square the sum of the numbers up to the given number" # These descriptions were the wrong way round in the skeleton
# I knew GCSE maths would come in handy one day
# a = 1 + 2 + ... + n
# b = n + ... + 2 + 1
# a+b = n(n+1)
# => sum = n(n+1) ÷ 2 # even * odd = even
function square_of_sum(n::Integer)
#   These two lines are just as fast as each other: it turns out that Julia knows GCSE maths too
#   (see sum(::Abstractrange{<:Real}) in base/range.jl)
#   (n*(n+1) ÷ 2) ^ 2
    sum(Base.OneTo(n))^2
end

# a = 1^2 + 2^2 + ... + n
# b = n^2 + (n-1)^2 + ...
#
# Telescoping series trick:
# sum(k^2 - (k-1)^2) = n^2
# k^2 - (k^2 - 2k +1) = 2k - 1
# => sum(k^2 - (k-1)^2) = sum(2k - 1) = 2*sum(k) - n = n^2 => sum(k) = (n^2 + n) / 2
#
# We want sum([telescoping thing]) ∝ (sum(k^2) + tractable stuff)
# => try sum(k^3 - (k-1)^3) = n^3
# = sum(k^3 - (k-1)(k^2 - 2k +1)) = sum(k^3 - (k^3 -3k^2 +3k -1)) = sum(3k^2 - 3k +1) = 3*sum(k^2) - 3*sum(k) + n = n^3
# => sum(k^2) = 1/3 * (n^3 - n + 3(n(n+1))/2) = 1/3 n^3 + 1/2 n^2 + 1/6 n = n/6 * (2n^2 + 3n + 1) = n/6 * (2n + 1)(n + 1)
#
# Slightly faster way of getting this:
# ```
# using SymPy
# @vars(k,n)
# summation(k^2,(k,(1,n)))
# ```
#
# Can turn it into a function with summation(...).evalf() but it uses Python so it's slow - better off copying it out manually
"Sum the squares of the numbers up to the given number" # These descriptions were the wrong way round in the skeleton
function sum_of_squares(n::Integer)
    n*(2n+1)*(n+1) ÷ 6
end

"Subtract sum of squares from square of sums"
function difference(n::Integer)
    square_of_sum(n) - sum_of_squares(n)
end


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

function bisect_left(A, p, r, v)
    i = p
    if p < r
       q = floor(Int, (p+r)/2)
       if v <= A[q]
           i = bisect_left(A, p, q, v)
       else
           i = bisect_left(A, q+1, r, v)
       end
    end
    return i
end

function bisect_right(A, p, r, v)
    i = p
    if p < r
       q = floor(Int, (p+r)/2)
       if v < A[q]
           i = bisect_right(A, p, q, v)
       else
           i = bisect_right(A, q+1, r, v)
       end
    end
    return i
end

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

function bisect(A, p, r, v)
    while (p <= r)
        q = floor(Int, (p+r/2))
        if (v == A[q]) return q
        elseif (v < A[q]) r = q-1
        else p = q+1
        end
    end
    return nothing
end

function qsort!(A, lo, hi) # lo - low, hi - high
    i, j = lo, hi
    while i < hi # iterate from "low" to "high" in relevant partition
        pivot = A[(lo+hi)>>>1] # logical shift (low+high) bits right to divide them by 2
        while i <= j
            while A[i] < pivot; i = i+1; end # find index less than pivot
            while A[j] > pivot; j = j-1; end # find index bigger than pivot
            if i <= j
                A[i], A[j] = A[j], A[i] # change places of the lesser and bigger element
                i, j = i+1, j-1 # next index towards middle
            end
        end
        if lo < j; qsort!(A,lo,j); end # call qsort on bottom partition
        lo, j = i, hi # start over again with the top partition
    end
    return A
end

function algdat_sort!(A) # quicksort
    p = 1
    r = length(A)
    qsort!(A, p, r)
end

function find_median(A, lower, upper)
    lo = bisect_left(A, 1, length(A)+1, lower) # lower index of interval
    hi = bisect_right(A, 1, length(A)+1, upper) - 1 # upper index of interval (-1 to return the actual last idx containing this value, not to the next)

    ctr = (lo+hi)/2 # float or integer if size of array is even or odd
    ctrLeft = (floor(Int, ctr))

    if (ctrLeft == ctr) # if array is odd, ctr index is an integer and its value the median (comparing floats)
        return A[Int(ctr)]
    else # else find median of the values to the left and right of the floating idx value
        return (A[ctrLeft] + A[ctrLeft+1]) / 2
    end
end


using Random, Distributions

function normal_approximation_to_binomial(n, p)
    mu = n * p
    sigma = sqrt(n * (1-p) * p)
    return mu, sigma
end

function normal_cdf(lo, mu=0, sigma=1)
   return cdf(Normal(mu, sigma), lo)
end

# function normal_probability_below(lo, mu=0, sigma=1)
#   return normal_cdf(lo, mu, sigma)
# end

function normal_probability_above(lo, mu=0, sigma=1)
   return 1 - normal_cdf(lo, mu, sigma)
end

function normal_probability_between(lo, hi, mu=0, sigma=1)
   return normal_cdf(hi,mu, sigma) - normal_cdf(lo, mu, sigma)
end

function normal_probability_outside(lo, hi, mu=0, sigma=1)
    return 1 - normal_probability_between(lo, hi, mu, sigma)
end

@assert normal_cdf(0, 0 ,1)==0.5

function normal_upper_bound(probability, mu=0, sigma=1)
   return quantile(Normal(mu, sigma), probability)
end

function normal_lower_bound(probability, mu=0, sigma=1)
   return quantile(Normal(mu, sigma), 1 - probability)
end

function normal_two_sided_bounds(probability, mu=0, sigma=1)
   tail_probability = (1 - probability)/2

   upper_bound = normal_lower_bound(tail_probability, mu, sigma)

   lower_bound = normal_upper_bound(tail_probability, mu, sigma)

   return  lower_bound, upper_bound
end

mu_0, sigma_0 = normal_approximation_to_binomial(1000, 0.5)

@assert mu_0 == 500
@assert 15.8 < sigma_0 < 15.9
# 95% bounds based on assumption p is 0.5
lo, hi = normal_two_sided_bounds(0.95, mu_0, sigma_0)

# actual mu and sigma based on p = 0.55
mu_1, sigma_1 = normal_approximation_to_binomial(1000, 0.55)

# a type 2 error means we fail to reject the null hypothesis
# which will happen when X is still in our original interval
type_2_probability = normal_probability_between(lo, hi, mu_1, sigma_1)
power = 1 - type_2_probability      # 0.887
@assert 0.886 < power < 0.888

function two_sided_p_value(x, mu = 0, sigma = 1)
    """
    How likely are we to see a value at least as extreme as x (in either
    direction) if our values are from a N(mu, sigma)?
    """
   if (x >= mu)
        # x is greater than the mean, so the tail is everything greater than x
        return 2 * normal_probability_above(x, mu, sigma)
   else
        # x is less than the mean, so the tail is everything less than x
        return 2 * normal_probability_below(x, mu, sigma)
   end
end

two_sided_p_value(529.5, mu_0, sigma_0)   # 0.062

# TODO: Add julia code Monte-Carlo simulation

two_sided_p_value(531.5, mu_0, sigma_0)   # 0.0463

# TODO: Two sided tests

p_hat = 525 / 1000
mu = p_hat
sigma = sqrt(p_hat * (1 - p_hat) / 1000)   # 0.0158

normal_two_sided_bounds(0.95, mu, sigma)        # [0.4940, 0.5560]

p_hat = 540 / 1000
mu = p_hat
sigma = sqrt(p_hat * (1 - p_hat) / 1000) # 0.0158
normal_two_sided_bounds(0.95, mu, sigma) # [0.5091, 0.5709]


"""
`hilbert(n)` creates an `n`-by-`n` Hilbert matrix.

## Example
```
julia> hilbert(4)
4×4 Array{Rational{Int64},2}:
 1//1  1//2  1//3  1//4
 1//2  1//3  1//4  1//5
 1//3  1//4  1//5  1//6
 1//4  1//5  1//6  1//7
```
"""
function hilbert(n::Int)
    A = zeros(Rational{Int}, n, n)
    for i = 1:n
        for j = 1:n
            A[i, j] = 1 // (i + j - 1)
        end
    end
    return A
end


# function to determine whether an x, y point is in the unit circle
function in_circle(x_pos::Float64, y_pos::Float64, radius::Float64)
    if (x_pos^2 + y_pos^2 < radius^2)
        return true
    else
        return false
    end
end

# function to integrate a unit circle to find pi via monte_carlo
function monte_carlo(n::Int64, radius::Float64)

    pi_count = 0
    for i = 1:n
        point_x = rand()
        point_y = rand()

        if (in_circle(point_x, point_y, radius))
            pi_count += 1
        end
    end

    pi_estimate = 4*pi_count/(n*radius^2)
    println("Percent error is: ", signif(100*(pi - pi_estimate)/pi, 3), " %")
end

monte_carlo(10000000, 0.5)


export differentiate

#################################################################
#
# differentiate()
#   based on John's differentiate and this code, I think by Miles Lubin:
#     https://github.com/IainNZ/NLTester/blob/master/julia/nlp.jl#L74
#
#################################################################

differentiate(ex::SymbolicVariable, wrt::SymbolicVariable) = (ex == wrt) ? 1 : 0

differentiate(ex::Number, wrt::SymbolicVariable) = 0

function differentiate(ex::Expr,wrt)
    if ex.head != :call
        error("Unrecognized expression $ex")
    end
    simplify(differentiate(SymbolParameter(ex.args[1]), ex.args[2:end], wrt))
end

differentiate{T}(x::SymbolParameter{T}, args, wrt) = error("Derivative of function " * string(T) * " not supported")

# The Power Rule:
function differentiate(::SymbolParameter{:^}, args, wrt)
    x = args[1]
    y = args[2]
    xp = differentiate(x, wrt)
    yp = differentiate(y, wrt)
    if xp == 0 && yp == 0
        return 0
    elseif yp == 0
        return :( $y * $xp * ($x ^ ($y - 1)) )
    else
        return :( $x ^ $y * ($xp * $y / $x + $yp * log($x)) )
    end
end

function differentiate(::SymbolParameter{:+}, args, wrt)
    termdiffs = Any[:+]
    for y in args
        x = differentiate(y, wrt)
        if x != 0
            push!(termdiffs, x)
        end
    end
    if (length(termdiffs) == 1)
        return 0
    elseif (length(termdiffs) == 2)
        return termdiffs[2]
    else
        return Expr(:call, termdiffs...)
    end
end

function differentiate(::SymbolParameter{:-}, args, wrt)
    termdiffs = Any[:-]
    # first term is special, can't be dropped
    term1 = differentiate(args[1], wrt)
    push!(termdiffs, term1)
    for y in args[2:end]
        x = differentiate(y, wrt)
        if x != 0
            push!(termdiffs, x)
        end
    end
    if term1 != 0 && length(termdiffs) == 2 && length(args) >= 2
        # if all of the terms but the first disappeared, we just return the first
        return term1
    elseif (term1 == 0 && length(termdiffs) == 2)
        return 0
    else
        return Expr(:call, termdiffs...)
    end
end

# The Product Rule
# d/dx (f * g) = (d/dx f) * g + f * (d/dx g)
# d/dx (f * g * h) = (d/dx f) * g * h + f * (d/dx g) * h + ...
function differentiate(::SymbolParameter{:*}, args, wrt)
    n = length(args)
    res_args = Array(Any, n)
    for i in 1:n
       new_args = Array(Any, n)
       for j in 1:n
           if j == i
               new_args[j] = differentiate(args[j], wrt)
           else
               new_args[j] = args[j]
           end
       end
       res_args[i] = Expr(:call, :*, new_args...)
    end
    return Expr(:call, :+, res_args...)
end

# The Quotient Rule
# d/dx (f / g) = ((d/dx f) * g - f * (d/dx g)) / g^2
function differentiate(::SymbolParameter{:/}, args, wrt)
    x = args[1]
    y = args[2]
    xp = differentiate(x, wrt)
    yp = differentiate(y, wrt)
    if xp == 0 && yp == 0
        return 0
    elseif xp == 0
        return :( -$yp * $x / $y^2 )
    elseif yp == 0
        return :( $xp / $y )
    else
        return :( ($xp * $y - $x * $yp) / $y^2 )
    end
end

symbolic_derivative_1arg_list = [
    ( :sqrt,        :(  1 / 2 / sqrt(x)                         ))
    ( :cbrt,        :(  1 / 3 / cbrt(x)^2                       ))
    ( :abs2,        :(  1 * 2 * x                               ))
    ( :inv,         :( -1 * abs2(inv(x))                        ))
    ( :log,         :(  1 / x                                   ))
    ( :log10,       :(  1 / x / log(10)                         ))
    ( :log2,        :(  1 / x / log(2)                          ))
    ( :log1p,       :(  1 / (x + 1)                             ))
    ( :exp,         :(  exp(x)                                  ))
    ( :exp2,        :(  log(2) * exp2(x)                        ))
    ( :expm1,       :(  exp(x)                                  ))
    ( :sin,         :(  cos(x)                                  ))
    ( :cos,         :( -sin(x)                                  ))
    ( :tan,         :(  (1 + tan(x)^2)                          ))
    ( :sec,         :(  sec(x) * tan(x)                         ))
    ( :csc,         :( -csc(x) * cot(x)                         ))
    ( :cot,         :( -(1 + cot(x)^2)                          ))
    ( :sind,        :(  pi / 180 * cosd(x)                      ))
    ( :cosd,        :( -pi / 180 * sind(x)                      ))
    ( :tand,        :(  pi / 180 * (1 + tand(x)^2)              ))
    ( :secd,        :(  pi / 180 * secd(x) * tand(x)            ))
    ( :cscd,        :( -pi / 180 * cscd(x) * cotd(x)            ))
    ( :cotd,        :( -pi / 180 * (1 + cotd(x)^2)              ))
    ( :asin,        :(  1 / sqrt(1 - x^2)                       ))
    ( :acos,        :( -1 / sqrt(1 - x^2)                       ))
    ( :atan,        :(  1 / (1 + x^2)                           ))
    ( :asec,        :(  1 / abs(x) / sqrt(x^2 - 1)              ))
    ( :acsc,        :( -1 / abs(x) / sqrt(x^2 - 1)              ))
    ( :acot,        :( -1 / (1 + x^2)                           ))
    ( :asind,       :(  180 / pi / sqrt(1 - x^2)                ))
    ( :acosd,       :( -180 / pi / sqrt(1 - x^2)                ))
    ( :atand,       :(  180 / pi / (1 + x^2)                    ))
    ( :asecd,       :(  180 / pi / abs(x) / sqrt(x^2 - 1)       ))
    ( :acscd,       :( -180 / pi / abs(x) / sqrt(x^2 - 1)       ))
    ( :acotd,       :( -180 / pi / (1 + x^2)                    ))
    ( :sinh,        :(  cosh(x)                                 ))
    ( :cosh,        :(  sinh(x)                                 ))
    ( :tanh,        :(  sech(x)^2                               ))
    ( :sech,        :( -tanh(x) * sech(x)                       ))
    ( :csch,        :( -coth(x) * csch(x)                       ))
    ( :coth,        :( -csch(x)^2                               ))
    ( :asinh,       :(  1 / sqrt(x^2 + 1)                       ))
    ( :acosh,       :(  1 / sqrt(x^2 - 1)                       ))
    ( :atanh,       :(  1 / (1 - x^2)                           ))
    ( :asech,       :( -1 / x / sqrt(1 - x^2)                   ))
    ( :acsch,       :( -1 / abs(x) / sqrt(1 + x^2)              ))
    ( :acoth,       :(  1 / (1 - x^2)                           ))
    ( :deg2rad,     :(  pi / 180                                ))
    ( :rad2deg,     :(  180 / pi                                ))
    ( :erf,         :(  2 * exp(-x*x) / sqrt(pi)                ))
    ( :erfinv,      :(  0.5 * sqrt(pi) * exp(erfinv(x) * erfinv(x))  ))
    ( :erfc,        :( -2 * exp(-x*x) / sqrt(pi)                ))
    ( :erfcinv,     :( -0.5 * sqrt(pi) * exp(erfcinv(x) * erfcinv(x))  ))
    ( :erfi,        :(  2 * exp(x*x) / sqrt(pi)                 ))
    ( :gamma,       :(  digamma(x) * gamma(x)                   ))
    ( :lgamma,      :(  digamma(x)                              ))
    ( :digamma,     :(  trigamma(x)                             ))
    ( :invdigamma,  :(  inv(trigamma(invdigamma(x)))            ))
    ( :trigamma,    :(  polygamma(2, x)                         ))
    ( :airy,        :(  airyprime(x)                            ))  # note: only covers the 1-arg version
    ( :airyprime,   :(  x * airy(x)                             ))
    ( :airyai,      :(  airyaiprime(x)                          ))
    ( :airybi,      :(  airybiprime(x)                          ))
    ( :airyaiprime, :(  x * airyai(x)                           ))
    ( :airybiprime, :(  x * airybi(x)                           ))
    ( :besselj0,    :( -besselj1(x)                             ))
    ( :besselj1,    :(  (besselj0(x) - besselj(2, x)) / 2       ))
    ( :bessely0,    :( -bessely1(x)                             ))
    ( :bessely1,    :(  (bessely0(x) - bessely(2, x)) / 2       ))
    ( :erfcx,       :(  (2 * x * erfcx(x) - 2 / sqrt(pi))       ))
    ( :dawson,      :(  (1 - 2x * dawson(x))                    ))

]

# This is the public interface for accessing the list of symbolic
# derivatives. The format is a list of (Symbol,Expr) tuples
# (:f, deriv_expr), where deriv_expr is a symbolic
# expression for the first derivative of the function f.
# The symbol :x is used within deriv_expr for the point at
# which the derivative should be evaluated.
symbolic_derivatives_1arg() = symbolic_derivative_1arg_list
export symbolic_derivatives_1arg

# deprecated: for backward compatibility with packages that used
# this unexported interface.
derivative_rules = Array(@Compat.compat(Tuple{Symbol,Expr}),0)
for (s,ex) in symbolic_derivative_1arg_list
    push!(derivative_rules, (s, :(xp*$ex)))
end

for (funsym, exp) in symbolic_derivative_1arg_list
    @eval function differentiate(::SymbolParameter{$(Meta.quot(funsym))}, args, wrt)
        x = args[1]
        xp = differentiate(x, wrt)
        if xp != 0
            return @sexpr(xp*$exp)
        else
            return 0
        end
    end
end

derivative_rules_bessel = [
    ( :besselj,    :(    (besselj(nu - 1, x) - besselj(nu + 1, x)) / 2   ))
    ( :besseli,    :(    (besseli(nu - 1, x) + besseli(nu + 1, x)) / 2   ))
    ( :bessely,    :(    (bessely(nu - 1, x) - bessely(nu + 1, x)) / 2   ))
    ( :besselk,    :( -1 * (besselk(nu - 1, x) + besselk(nu + 1, x)) / 2   ))
    ( :hankelh1,   :(    (hankelh1(nu - 1, x) - hankelh1(nu + 1, x)) / 2 ))
    ( :hankelh2,   :(    (hankelh2(nu - 1, x) - hankelh2(nu + 1, x)) / 2 ))
]

# This is the public interface for accessing the list of symbolic
# derivatives. The format is a list of (Symbol,Expr) tuples
# (:f, deriv_expr), where deriv_expr is a symbolic
# expression for the first derivative of the function f with respect to x.
# The symbol :nu and :x are used within deriv_expr
# :nu specifies the first parameter of the bessel
# function (usually written n or alpha)
# :x gives the point at which the derivative should be evaluated.
symbolic_derivative_bessel_list() = derivative_rules_bessel
export symbolic_derivative_bessel_list

# 2-argument bessel functions
for (funsym, exp) in derivative_rules_bessel
    @eval function differentiate(::SymbolParameter{$(Meta.quot(funsym))}, args, wrt)
        nu = args[1]
        x = args[2]
        xp = differentiate(x, wrt)
        if xp != 0
            return @sexpr(xp*$exp)
        else
            return 0
        end
    end
end

### Other functions from julia/base/math.jl we might want to define
### derivatives for. Some have two arguments.

## atan2
## hypot
## beta, lbeta, eta, zeta, digamma

## Differentiate for piecewise functions defined using ifelse
function differentiate(::SymbolParameter{:ifelse}, args, wrt)
    :(ifelse($(args[1]), $(differentiate(args[2],wrt)),$(differentiate(args[3],wrt))))
end

function differentiate(ex::Expr, targets::Vector{Symbol})
    n = length(targets)
    exprs = Array(Any, n)
    for i in 1:n
        exprs[i] = differentiate(ex, targets[i])
    end
    return exprs
end

differentiate(ex::Expr) = differentiate(ex, :x)
differentiate(s::Compat.AbstractString, target...) = differentiate(parse(s), target...)
differentiate(s::Compat.AbstractString, target::Compat.AbstractString) = differentiate(parse(s), symbol(target))
differentiate{T <: Compat.AbstractString}(s::Compat.AbstractString, targets::Vector{T}) = differentiate(parse(s), map(symbol, targets))


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

using Markdown
using InteractiveUtils

# ╔═╡ 938f32d0-4dc2-11eb-29e1-19b4e28bf3c8
using BenchmarkTools, Random, DataFrames, Plots

# ╔═╡ 12341f32-4dbf-11eb-1753-6fac96fd666a
md"""
# Ancient Algorithms

The concept of algorithm has existed since ancient ages. Babylonian (c. 2500 BC) and Egyptian (c. 1550 BC) used arithmetic algorithms for example division. Greek mathematicians used the sieve of Eratosthenes for finding prime numbers, and the Euclidean algorithm for finding the greatest common divisor of two numbers.

The word algorithm itself was derived from the name of the 9th-century Persian mathematician Muḥammad ibn Mūsā al-Khwārizmī (c. 780–850) , whose name (Khwarazm) was Latinized as Algoritmi.
"""

# ╔═╡ 10ef1d92-4dc0-11eb-3524-bd5cb089a31b
md"""
## The Peasant Multiplication

Origin: Russian and Egyptian

Idea: To multiply two positive integers a and b,
- we can instead compute (a/2)*(2b) if a is even and
- ((a − 1)/2) * (2b) + b if a is odd.

**Algorithm Analysis:**
- a is reduced by at least a factor of 2, at the cost of one halving, one doubling, and possibly one addition.
- If we assume that halving, doubling, and addition are all constant-time operations, then the total computational complexity of the algorithm is proportional to the number of binary digits of a.
- Converges to correct answer
"""

# ╔═╡ 09744cea-4dc0-11eb-11b0-c7c6b42683bc
html"""
<iframe width="560" height="315" src="https://www.youtube.com/embed/HJ_PP5rqLg0" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
"""

# ╔═╡ aae2f0aa-4dbf-11eb-3f50-39e262cc1845
function peasantprod(a::T, b::T) where T <: Integer
    c = zero(T)
    while a > one(T)
        if isodd(a)
            c = c + b
        end
        a = a >> 1 # divide a by 2
        b = b << 1 # multiply b by 2
    end
    return c + b
end

# ╔═╡ 81d2d826-4dc0-11eb-2efb-9f5ef5161edd
peasantprod(10, 33)

# ╔═╡ 87f00406-4dc0-11eb-29b9-6b5c19d03b53
md"""
## Babylonian Method for finding square root

Still used by many calculators.

Idea:

$$x \cdot \frac{a}{x} = a = \sqrt{a} \sqrt{a}$$

- If $x < \sqrt{a}$, then $\frac{a}{x} > \sqrt{a}$
- If $x > \sqrt{a}$, then $\frac{a}{x} < \sqrt{a}$

Take averge of the two

$X_{n+1} = \frac{X_n + \frac{a}{X_n}}{2} = \frac{X_n^2 + a}{2 X_n}$

"""

# ╔═╡ a78d5570-4dc0-11eb-37a8-adc45a4964ee
function babylonian(a::T, tol::T) where T <: Real
    x = one(T)  # start x at 1
    while abs(x^2 - a) > tol  # convergence test
        x = (x + c / x) / 2
    end
    return x
end

# ╔═╡ c0c78d52-4dc1-11eb-325d-81452aa6d218

# ╔═╡ 0dd9ea7c-4dc2-11eb-1b99-f7b120c21a70
md"""
## [Sieve of Eratosthenes](https://en.wikipedia.org/wiki/Sieve_of_Eratosthenes) for finding primes

Iteratively mark of all composites of the identified prime.

![Sieve of Eratosthenes](https://upload.wikimedia.org/wikipedia/commons/b/b9/Sieve_of_Eratosthenes_animation.gif)

"""

# ╔═╡ 371f8004-4dc2-11eb-340e-c367fc0d718d
begin
	function es(n::Int)
	    isprime = trues(n)  # n-element vector of true-s
	    isprime[1] = false  # 1 is not a prime
	    for i in 2:isqrt(n)  # loop integers less or equal than sqrt(n)
	        if isprime[i]  # conditional evaluation
	            for j in i^2:i:n  # sequence with step i
	                isprime[j] = false
	                end
	            end
	        end
	    return filter(x -> isprime[x], 1:n)# filter using an anonymous function
	    end

	println(es(100))# print all primes less or equal than 100
	@time length(es(10^6))# check function execution time and memory usage
end

# ╔═╡ 5a9e6414-4dc2-11eb-2ed3-8f6abe2b7847
md"""
## Greatest Common Divisor (GCD)

For integers a and b (likely to be very large positive integers), GCD(a,b) is the largest integer d that divides both a and b. GCD is very important in number theory such as factorization, prime numbers and hence cryptography.
"""

# ╔═╡ 61a2ce56-4dc2-11eb-3b58-df1a7f2bf4d6
begin
	"""GCD using the definition of GCD"""
	function GCDnaive(a,b)
	    largest = 0
	    for d in 1:min(a,b)   # runtime ~min(a,b)
	        if a%d==0 && b%d==0
	            largest = d
	        end
	    end
	    return largest
	end

	@assert GCDnaive(8,12)==4
	@benchmark GCDnaive(3918848,1653264)
end

# ╔═╡ c3fdeae2-4dc2-11eb-0d57-598c117205c7
md"""
### Euclidean GCD

**Lemma:**
> Let $a'$ be the remainder when a is divided by b, then $$gcd(a,b) = gcd(a',b)= gcd(b,a')$$

**Proof**
- when a is divided by b, $a = b*q + a'$
- now d divides a and b if and only if it divides $a'$ and $b$

Each step reduces the size of the numbers by about a factor of 2. Takes about log(ab) steps
"""

# ╔═╡ e0401ae0-4dc2-11eb-2ba6-9d18e1915fa1
begin
	function GCDEuclid(a,b)
	    if b == 0
	        return a
	    else
	        return GCDEuclid(b,a%b)
	    end
	end

	# onliner
	GCD(a,b) = b==0 ? a : GCD(b , a%b)

	@benchmark GCDEuclid(3918848,1653264)
end

# ╔═╡ f40e1e6e-4dc2-11eb-2c04-d7020e727f7f
md"""
## Fibonacci Numbers

Developed by to study Rabbit Population after multiple genertions

$$F_n = \begin{cases} 0, \qquad n=0, \\ 1, \qquad n=1, \\ F_{n+1} + F_{n+2}, \quad n>1 \end{cases}$$

This has a rapid growth. $F_n \geq 2^{n/2} \text{ for } n \geq 6$

"""

# ╔═╡ 02317fca-4dc3-11eb-34c4-a5e009b09775
begin
	"""Fibbonaci Implementation directly from definition"""
	function fibbo_naive(n::Integer)
	    n >= 0 || throw(DomainError(n, "Domain: n ≥ 0"))
	    if n <= 1
	        return n
	    else
	        return fibbo_naive(n-1) + fibbo_naive(n-2)
	    end

	end

	# Do not try huge numbers here
	@assert fibbo_naive(1) == 1
	@assert fibbo_naive(7) == 13
end

# ╔═╡ 1b865a74-4dc3-11eb-25ac-d56e25e7b4b6
md"""
If T(n) is the Lines of Code executed by the function on input n. If you step through the debugger you can calculate the number of lines  for the implementation.

$$T(n) = \begin{cases} 2, \qquad n \leq 1, \\ F_{n+1} + F_{n+2} + 3, \quad n >1 \end{cases}$$
$$\therefore T(n) \geq F_n$$

$T(100) \approx 1.77 * 10^{21} \approx 56000$ years at 1GHz

Why is this so slow?
![Fibbonacci call stack](https://idawud.tech/assets/img/support/fibonacci_1.png)
For evaluating any number same number has to be evaluated multiple times eg f(3), f(2) has been repeatedly evaluated so many times, this increases exponentially when n is very large.

When we calculate manually we don't redo such calculation but rather use only the last two numbers.
"""

# ╔═╡ 0eea1918-4dc3-11eb-1484-b7f0db8d3a9a
begin
	function fibbo_efficient(n::Integer)
	    n >= 0 || throw(DomainError(n, "Domain: n ≥ 0"))
	    n <= 1 && return n

	    Fn = zeros(Integer, n + 1)
	    Fn[1:2] = [0, 1]  # you can only store two numbers

	    for i = 3:n+1
	        Fn[i] = Fn[i-1] + Fn[i-2]
	    end

	    return Fn[end]
	end

	@assert fibbo_efficient(7) == 13

	@benchmark fibbo_efficient(50)
end

# ╔═╡ b98372a2-4dc3-11eb-2956-69554b9ff7b8
md"""
$$T(n) = \begin{cases} 2, \qquad n \leq 1, \\ 2n + 3, \quad n >1 \end{cases}$$

$T(100) \approx 202$ Incredibly fast compared to above
"""

# ╔═╡ Cell order:
# ╟─12341f32-4dbf-11eb-1753-6fac96fd666a
# ╠═938f32d0-4dc2-11eb-29e1-19b4e28bf3c8
# ╟─10ef1d92-4dc0-11eb-3524-bd5cb089a31b
# ╟─09744cea-4dc0-11eb-11b0-c7c6b42683bc
# ╠═aae2f0aa-4dbf-11eb-3f50-39e262cc1845
# ╠═81d2d826-4dc0-11eb-2efb-9f5ef5161edd
# ╟─87f00406-4dc0-11eb-29b9-6b5c19d03b53
# ╠═a78d5570-4dc0-11eb-37a8-adc45a4964ee
# ╠═c0c78d52-4dc1-11eb-325d-81452aa6d218
# ╠═0dd9ea7c-4dc2-11eb-1b99-f7b120c21a70
# ╠═371f8004-4dc2-11eb-340e-c367fc0d718d
# ╟─5a9e6414-4dc2-11eb-2ed3-8f6abe2b7847
# ╠═61a2ce56-4dc2-11eb-3b58-df1a7f2bf4d6
# ╟─c3fdeae2-4dc2-11eb-0d57-598c117205c7
# ╠═e0401ae0-4dc2-11eb-2ba6-9d18e1915fa1
# ╟─f40e1e6e-4dc2-11eb-2c04-d7020e727f7f
# ╠═02317fca-4dc3-11eb-34c4-a5e009b09775
# ╟─1b865a74-4dc3-11eb-25ac-d56e25e7b4b6
# ╠═0eea1918-4dc3-11eb-1484-b7f0db8d3a9a
# ╟─b98372a2-4dc3-11eb-2956-69554b9ff7b8


using Pkg
Pkg.activate(pwd())

# # Arithmetic operators

1 + 2
2*3
4/3

#+

x = 1;
y = 3;

(x + 2)/(y - 1) - 4*(x - 2)^2

#+

2(3 + 4) # equivalent to 2*(3 + 4)

# ### Exercise:
# Determine the value and type of $y$ given by the following expression
# $$
# y = \frac{(x + 2)^2 - 4}{(x - 2)^{p - 2}},
# $$
# where $x = 4$ and $p = 5$.
#
# ---
# ### Solution:

# ---
#
# ## Promotion system

x = 1.0 # Float64
y = 2 # Int64

xp, yp = promote(x, y)

typeof(xp)
typeof(yp)

#+

promote(1, 2f0, true, 4.5, Int32(1))
promote_type(Float64, Int64, Bool, Int32)

#+

x = 1 # Int64
y = 2f0 # Float32
z = x + y

typeof(z)

# ### Exercise:
# All of these values represent number ``1``. Determine the smallest type which can
# represent them.

x = 1
y = 1f0
z = true
w = Int32(1)

# ---
# ### Solution:

# ---
#
# ## Updating operators

x = 1
x += 3 # x = x + 3
x *= 4 # x = x * 4
x /= 2 # x = x / 2
x \= 16 # x = x \ 16 = 16 / x

# ### Exercise:
# Compute the value of $y$ given by the following expression
# $$
# y = \frac{(x + 4)^{\frac{3}{2}}}{(x + 1)^{p - 1}},
# $$
# where $x = 5$ and $p = 3$. Then multiply the result by $8$, add $3$, divide by $3$, and
# subtract $1$. What are all the intermediate results and the final result?
#
# ---
# ### Solution:

# ---
#
# ## Numeric comparison

1 == 1
1 == 1.0

#+

-1 <= 1
-1 ≥ 1

#+

3 > 2 > 1
3 > 2 & 2 > 1

#+

1 < 2 <= 2 < 3 == 3 > 2 >= 1 == 1 < 3 != 5

#+

NaN == NaN
NaN != NaN
NaN < NaN

#+

isequal(NaN, NaN)
!isequal(NaN, NaN)

#+

!true
!false

# ## Rounding functions

x = 3141.5926

round(x)
floor(x)
ceil(x)

#+

round(Int64, x)
floor(Int32, x)
ceil(Int16, x)

#+

round(x; digits = 3)
round(x; sigdigits = 3)

# ### Exercise:
# Use rounding functions to solve the following tasks:
# - Round `1252.1518` to the nearest larger integer and convert the resulting value to
#   `Int64`.
# - Round `1252.1518` to the nearest smaller integer and convert the resulting value to
#   `Int16`.
# - Round `1252.1518` to `2` digits after the decimal point.
# - Round `1252.1518` to `3` significant digits.
#
# ---
# ### Solution:

# ---
#
# ## Numerical Conversions

convert(Float32, 1.234)
Float32(1.234)

#+

convert(Int64, 1.0)
Int64(1.0)

#+

convert(Int64, 1.234)
Int64(1.234)

# ### Exercise:
# Use the proper numeric conversion to get the correct result (not approximate) of summing
# the following two numbers
#
# ```julia
# x = 1//3
# y = 0.5
# ```
#
# **Hint:** rational numbers can be summed without approximation.
#
# ---
# ### Solution:


# column
unit_cost_c = [6 4 3 2 1]
# row
amount_r = [2, 3, 4, 6, 12]

# row * column adds up to a sum
unit_cost_c * amount_r

using LinearAlgebra
# define as row
unit_cost = [6, 4, 3, 2, 1]
# diagnol matrix times a vector gives a vector
Diagonal(unit_cost) * amount_r

#this obviously gives same answer as row 7 since transpose makes it a column
transpose(unit_cost) * amount_r

# matrix with different orders in eacch column
amounts =  [6 1 10;
            4 1 10;
            3 1 10;
            2 1 10;
            1 1 10]

# get the summed cost for each column order
transpose(unit_cost) * amounts

# first row calculates sum based on unit costs (as before)
# second row calculates average number of units (divide by 5 and sum)
operations =   [6.0 4.0 3.0 2.0 1.0;
                0.2 0.2 0.2 0.2 0.2]
operations * amounts

# identity operation
Diagonal([1,1,1]) * [8,12,3]
# I does it better
I*[8,12,3]
I*[8,12,3,5]

# reverse operation - since this is part of base not sure why Engheim doesnt mention simple operator
R =  [0 0 1;
      0 1 0;
      1 0 0]

R * [8, 12, 3]
reverse([8, 12, 3])
# can also reverse a row
reverse([8 12 3])
# you can use multiplication on the matrix
2I*[8, 12, 3]
# more examples
z = [1 2 3;
     4 5 6;
     7 8 9]

I*z
2*z
reverse(z)
2*reverse(z)
# be careful with inverse
#inv(z) # commented out see below
# this gives you a singular exception. If you take the determinant you see it is ) which explains why
det(z)
# a simple change solves the problem
zf = [1 2 3;
      4 5 6;
      7 8 10]
det(zf)
inv(zf)
# rounding error for floating point doesnt create perfect identity
If = zf * inv(zf)
# but its close enough
I ≈ If  # yields true

# Back to Engheim
A =   [1  1 -2;
       1 -1 -1;
       1  1  3]

B = [3, 0, 12]

X = inv(A)*B

A*X
B
A*X ≈ B  # yields true

# concatenation
C = [1 3 5]
D = [7 9 11]
E = [1, 3, 5]
F = [7, 9, 11]
G = [1 3 5; 2 4 6]
H = [7 9 11; 8 10 12]

# horizontal - dims indiccate which dimention
hcat(C, D)
cat(C, D, dims=2)
hcat(E, F)
cat(E, F, dims=2)
hcat(G, H)
cat(G, H, dims=2)
hcat(G, 2G, 3G, 4G)

# vertical
vcat(C, D)
cat(C, D, dims=1)
vcat(E, F)
cat(E, F, dims=1)
vcat(G, H)
cat(G, H, dims=1)
vcat(C, D, C, D)

# creating matrices
zeros(Int8, 3, 4)
ones(Int8, 3, 4)
rand(Int8, 3, 4)
zeros(Float64, 3, 4)
ones(Float64, 3, 4)
rand(Float64, 3, 4)
fill(12, 3, 4)  #Int64
fill(12.0, 3, 4) #Float64
fill(1//2, 3, 4) #Rational{Int64}
fill(0x4, 3, 4) #Int8

# in the example on page 249 for vector addition this is what you are really seeing:
u = [4,2]  # from the origin [0,0]
v = [-1,1] # from the starting point [4,2]
u + v
# yields:
# 2-element Vector{Int64}:
# 3
# 3
# scalar addition needs to be done per element
u .+ 1
# yields:
# 2-element Vector{Int64}:
# 5
# 3
# assignment of vectors
w = v
w == v # true
w[1] = 1 # change w
w == v # true
v[2] = 2 # change v
w == v # true
# element wise assignment has different result in that w is NOT pointing to unit
# w .= u is equivalent to w[1] = 4 and w[2] = 2 which obviously has no impact on u
w .= u
w == v # true
w == u # true temporarily
v == u # true temporarily
# but
w[1] = 1 # change w
w == v # true
w == u # false
v == u # false
# subtraction
v = [-1, 1]
# next two give same result. This also shows multiplying vector by scaler works as expected
u + (-v)
u - v
# vector rotation
x = [3, 4]
xm = 3*[1, 0] + 4*[0, 1]
x == xm # true
# rotate x 90 degrees
xmr = 3*[0, 1] + 4*[-1, 0]
# do it as matrix multiplication - NB: Engheim got the matrix wrong
# [0,1] and [-1, 0] are columns not rows so the matrix looks like A below
A = [ 0 -1;
      1 0]
# a vector is a column not a row
# multiply each row in the vector x times a column in A and add the two vectors together (as in xmr)
# 3 is first row times [0, 1] + 4 is second row * [-1,0] giving vector (column) [-4, 3]
A*x == xmr # true

#rotation(degrees)
#Create a rotation matrix for rotating a 2D vector `deg` degrees.

function rotation(deg::Real)
  rad = deg2rad(deg)
  cosθ = cos(rad)
  sinθ = sin(rad)

  [cosθ -sinθ;
   sinθ  cosθ]
end

# create a Matrix for rotating a vector 90 degrees
M = rotation(90)

# create a saaple [x,y] vector and rotate 90 degrees
v = [3, 4]
M*v
v2 = [3, 3]
M*v2
v3 = [3, 0]
M*v3
v4 = [0, 3]
M*v4

# # Dot Product
# # important you add "using LinearAlgebra" before running the following
# a = [2, 3]
# b = [3, 1]

# # to get the dot product character type "a \cdot[tab] b" (i.e. thit the tab key)
# a ⋅ b
# transpose(a)*b
# dot(a, b)

# # Work equations
# r = [3, 1]
# # the norm is the sqrt of r ⋅ r which gives the length of the vector r
# # u is a unit vector with same direction as r (divide r by its lenght
# # to get a vector lenght 1 with same direction as r)
# u = r / norm(r)
# # in the graph in the book Engheim shifts teh vector from the 0origin
# F = [2, 3]
# # the work
# F ⋅ u
# # from this equality “𝐅 ⋅ 𝐮 = |𝐅| cos θ " we can calulate the angle θ
# θ = acos(F ⋅ u/norm(F))
# # check the equality
# F ⋅ u == norm(F) * cos(θ)
# # substitute back the defintion of u
# F ⋅ (r/norm(r)) == norm(F) * cos(θ)
# # multiply norm(r) to both sides of the equation (there are rounding errors)
# F ⋅ r ≈ norm(F) * norm(r) * cos(θ)
# # Cross product
# u = [6, 0, 0]
# v = [3, 4, 0]
# # ⨯ is \times
# w = u ⨯ v


using StatsFuns
using Base.Test
using Compat

# xlogx & xlogy

println("\ttesting xlogx & xlogy ...")

@test xlogx(0) === 0.0
@test_approx_eq xlogx(2) 2.0 * log(2.0)

@test xlogy(0, 1) === 0.0
@test_approx_eq xlogy(2, 3) 2.0 * log(3.0)

# logistic & logit

println("\ttesting logistic & logit ...")

@test_approx_eq logistic(2) 1.0 / (1.0 + exp(-2.0))
@test_approx_eq logit(0.5) 0.0
@test_approx_eq logit(logistic(2)) 2.0

# log1psq

println("\ttesting log1psq ...")

@test_approx_eq log1psq(0.0) 0.0
@test_approx_eq log1psq(1.0) log1p(1.0)
@test_approx_eq log1psq(2.0) log1p(4.0)

# log1pexp, log1mexp, log2mexp & logexpm1

println("\ttesting log1pexp ...")

@test_approx_eq log1pexp(2.0) log(1.0 + exp(2.0))
@test_approx_eq log1pexp(-2.0) log(1.0 + exp(-2.0))
@test_approx_eq log1pexp(10000) 10000.0
@test_approx_eq log1pexp(-10000) 0.0

println("\ttesting log1mexp ...")

@test_approx_eq log1mexp(-1.0) log1p(- exp(-1.0))
@test_approx_eq log1mexp(-10.0) log1p(- exp(-10.0))

println("\ttesting log2mexp ...")

@test_approx_eq log2mexp(0.0) 0.0
@test_approx_eq log2mexp(-1.0) log(2.0 - exp(-1.0))

println("\ttesting logexpm1 ...")

@test_approx_eq logexpm1(2.0) log(exp(2.0) - 1.0)
@test_approx_eq logexpm1(log1pexp(2.0)) 2.0
@test_approx_eq logexpm1(log1pexp(-2.0)) -2.0

# log1pmx

println("\ttesting log1pmx ...")

@test_approx_eq log1pmx(0.0) 0.0
@test_approx_eq log1pmx(1.0) log(2.0) - 1.0
@test_approx_eq log1pmx(2.0) log(3.0) - 2.0

println("\ttesting logmxp1 ...")

@test_approx_eq logmxp1(1.0) 0.0
@test_approx_eq logmxp1(2.0) log(2.0) - 1.0
@test_approx_eq logmxp1(3.0) log(3.0) - 2.0

# logsumexp

println("\ttesting logsumexp ...")

@test_approx_eq logsumexp(2.0, 3.0) log(exp(2.0) + exp(3.0))
@test_approx_eq logsumexp(10002, 10003) 10000 + logsumexp(2.0, 3.0)

@test_approx_eq logsumexp([1.0, 2.0, 3.0]) 3.40760596444438
@test_approx_eq logsumexp([1.0, 2.0, 3.0] .+ 1000.) 1003.40760596444438

# softmax

println("\ttesting softmax ...")

x = [1.0, 2.0, 3.0]
r = @compat exp.(x) ./ sum(exp.(x))
@test_approx_eq softmax([1.0, 2.0, 3.0]) r
softmax!(x)
@test_approx_eq x r


# Julian program to Find the n'th Catalan number
# To know more about Catalan Numbers Visit Here : https://en.wikipedia.org/wiki/Catalan_number

function find_catalan(n)
    if (n == 0 || n == 1)
        return 1
    end

    # DP Table
    cat = [0 for i in 1:(n + 1)]

    # Initialize the first to values
    cat[1] = 1
    cat[2] = 1
    for i in 3:(n + 1)
        for j in 1:(i - 1)
            cat[i] = cat[i] +  (cat[j] * cat[i - j])
        end
    end
    return cat[n + 1]
end

print("Enter the number: ")
num = readline()
num = parse(Int, num)
catalan_number = find_catalan(abs(num))
println("The $num'th catalan number is $catalan_number.")

"""
Time Complexity: O(num^2), where 'num' is the given number
Space Complexity: O(num)

SAMPLE INPUT AND OUTPUT

SAMPLE 1
Enter the number: 12
The 12'th catalan number is 208012.

SAMPLE 2
Enter the number: 2
The 2'th catalan number is 2.

"""


"""
    cosine_similarity(u::AbstractVector, v::AbstractVector) -> Float

Compute the cosine similarity of two vectors.

# Examples

```jldoctest
julia> cosine_similarity([1, 0], [1, 0])
1.0

julia> round(cosine_similarity([1,1], [1,0]), digits=3)
0.707

julia> cosine_similarity([1, 0], [0, 1])
0.0

julia> cosine_similarity([-1, 0], [1, 0])
-1.0

julia> round(cosine_similarity([1,0,0], [1,1,1]), digits=3)
0.577
```

"""
function cosine_similarity(u::AbstractVector, v::AbstractVector)
    u ⋅ v / (norm(u) * norm(v))
end

"""
    angle_between(u::AbstractVector, v::AbstractVector) -> Float

Compute the angle between two vectors. The angle is returned in radians.

# Examples

```jldoctest
julia> angle_between([1, 0], [1, 0])
0.0

julia> round(angle_between([1,1], [1,0]), digits=3)
0.785

julia> round(angle_between([1, 0], [0, 1]), digits=3)
1.571

julia> round(angle_between([-1, 0], [1, 0]), digits=3)
3.142

julia> round(angle_between([1,0,0], [1,1,1]), digits=3)
0.955
```

"""
function angle_between(u::AbstractVector, v::AbstractVector)
    acos(cosine_similarity(u, v))
end

"""
    distance(point_a::AbstractVector, point_b::AbstractVector) -> Float

Compute the distance between two points.

# Examples

```jldoctest
julia> distance([0, 0], [0, 0])
0.0

julia> distance([1, 0], [0, 0])
1.0

julia> round(distance([1, 1], [2, 2]), digits=3)
1.414

julia> round(distance([0, 0, 0], [-1, -1, -1]), digits=3)
1.732
```

"""
function distance(point_a::AbstractVector, point_b::AbstractVector)
    return norm(Vector(point_a, point_b))
end

"""
    distance(point_a::AbstractVector, line::AbstractLine) -> Float

Compute the distance from a point to a line.

This is the distance from the point to its projection on the line.

# Examples

```jldoctest
julia> distance([0, 0], Line([0, 0], [1, 0]))
0.0

julia> round(distance([1, 0], Line([0, 0], [1, 1])), digits=3)
0.707

julia> round(distance([1, 2, 3], Line([-1, 3, 2], [7, 4, 2])), digits=3)
1.978
```

"""
function distance(point::AbstractVector, line::AbstractLine)
    point_projected = project(point, line)
    return distance(point, point_projected)
end


using ApproxFun, Plots

# In this example, we numerically compute with Galerkin orthogonal polynomials.
# If H is a separable Hilbert space and B contains ν linear functional constraints,
# then the Galerkin orthogonal polynomials are those orthogonal polynomials with
# respect to H_B := \{ u \in H : Bu = 0 \}.
# For example, if H = L^2([-1,1]) and we wish to enforce that u(-1) = u'(-1) =
# u'(1) = 0, then the 2-normalized orthonormal polynomials may be created by
# taking the QR factorization of the connection coefficients between the
# normalized Legendre polynomials \sqrt{n+1/2}P_n(x) and a degree-graded
# polynomial basis that satisfies the constraints.

N = 501

NS = NormalizedLegendre()
B = [Evaluation(NS, -1, 0); Evaluation(NS, -1, 1); Evaluation(NS, 1, 1)]
ν = size(B, 1)
QS = QuotientSpace(B)
A = Conversion(QS, NS)
Q, R = qr(A[1:N+ν,1:N])

p = Vector{Fun}(undef, N)
for n = 1:N
    en = [zeros(n-1);1.0;zeros(N+ν-n)]
    p[n] = Conversion(NS, Legendre())*Fun(NS, (Q*en)[1:n+ν])
    pad!(p[n], 3n+50)
end

# The mass matrix with entries M_{i,j} = \int_{-1}^1 p_i(x) p_j(x) dx,
# is numerically the identity, as we expect.

M = [innerproduct(p[i], p[j]) for i = 1:N, j = 1:N]
@show norm(M-I) ≤ 4*norm(M)*eps()
@show opnorm(M-I) ≤ 4*sqrt(N)*opnorm(M)*eps()

# Although the polynomials now satisfy deg(p_n) = n+ν instead of deg(p_n) = n,
# the index appears to determine the number of roots of the Galerkin orthogonal
# polynomials. In particular, p_n has exactly n roots in (-1,1).

pl = plot(p[1]; legend = false)
for n in 2:10
    plot!(p[n])
end
plot(pl)

# This need not be the case! If the second constraint is modified to impose
# u'(0) = 0, then the odd-index polynomials have one more root than their index
# and the even-index polynomials have one fewer.

B = [Evaluation(NS, -1, 0); Evaluation(NS, 0, 1); Evaluation(NS, 1, 1)]
QS = QuotientSpace(B)
A = Conversion(QS, NS)
Q, R = qr(A[1:N+ν,1:N])

q = Vector{Fun}(undef, N)
for n = 1:N
    en = [zeros(n-1);1.0;zeros(N+ν-n)]
    q[n] = Conversion(NS, Legendre())*Fun(NS, (Q*en)[1:n+ν])
    pad!(q[n], 3n+50)
end

pl = plot(q[1]; legend = false)
for n in 2:10
    plot!(q[n])
end
plot(pl)

# More information on Galerkin orthogonal polynomials is available in
#
#   P. W. Livermore. Galerkin orthogonal polynomials, J. Comp. Phys., 229:2046–2060, 2010.
#
# The banded QR factorization of the connection coefficients is a linear
# complexity algorithm to represent the Galerkin orthogonal polynomials in H_B
# in terms of the polynomial basis for H. This is described in
#
#   J. L. Aurentz and R. M. Slevinsky. On symmetrizing the ultraspherical spectral method for self-adjoint problems, arXiv:1903.08538, 2019.
#


"""Julia program to check if a number is a Perfect number or not.
    Perfect Number is a number if it is equal to the sum of its proper divisors excluding the number itself."""

"""Iterate from 1 till the square root of the given number.
    Identify the divisors and take thier sum. And then check
    If the sum is equal to given number"""
function perfect_number(num)
    # variable to store the sum of the proper divisors, initialized to one
    sum = 1
    # Index to iterate from 1 till the square root
    div = 2
    while((div * div) <= num)
        # Check if the current 'div' number is a divisor or not.
        if(num % div == 0)
            sum = sum + div + num ÷ div
        end
        div = div + 1
    end
    if(sum == num && num != 1)
        return true
    else
        return false
    end
end

print("Enter the number: ")
num = readline()
num = parse(Int, num)
res = perfect_number(num)
if res
    println("The given number $num is a Perfect Number.")
else
    println("The given number $num is not a Perfect Number.")
end

"""
Time Complexity: O(n^(0.5)), where 'n' is the given number
Space Complexity: O(1)

SAMPLE INPUT AND OUTPUT

SAMPLE 1
Enter the number: 8128
The given number 8128 is a Perfect Number.

SAMPLE 2
Enter the number: 8129
The given number 8129 is not a Perfect Number.
"""


@testset "Matrix" begin

    @testset "Matrix: Determinant" begin
        M1 = [1 0; 2 2]
        M2 = rand(3,3)
        M3 = rand(4,4)

        @test determinant(M1) == det(M1)
        @test round(determinant(M2),digits = 4) == round(det(M2),digits = 4)
        @test round(determinant(M3),digits = 4) == round(det(M3),digits = 4)
    end

    @testset "Matrix: LU Decompose" begin
        mat = [
            2  -1 -2;
            -4   6  3;
            -4  -2  8
        ]

        L = [
            1  0 0;
            -2  1 0;
            -2 -1 1
        ]
        U = [
            2 -1 -2;
            0  4 -1;
            0  0  3
        ]

	    @test lu_decompose(mat) == (L,U)
    end

	@testset "Matrix: rotation matrix" begin
		theta = pi/6
		a = [1;0]
		b = [0;1]
		@test rotation_matrix(theta)*a == [cos(theta);sin(theta)]
		@test rotation_matrix(theta)*b == [-sin(theta);cos(theta)]
	end
end


## Exercise 7-4
## The mathematician Srinivasa Ramanujan found an infinite series that can be used to generate a numerical approximation of 1/π:

## 1/π = 2√2 /9801 ∑_{k=0}^{∞} ((4k)!(1103+26390k))/(k!)^4 * 396^{4k}

## Write a function called estimatepi that uses this formula to compute and return an estimate of π. It should use a while loop to compute terms of the summation until the last term is smaller than 1e-15 (which is Julia notation for 10^{−15}). You can check the result by comparing it to π.
println("Ans: ")

function last_part(k::Int)
    numerator = factorial(4k) * (1103 + 26390 * k)
    denominator = factorial(k)^4 * 396^(4k)

    return numerator/denominator
end

function estimatepi()
    k = 0
    sum = 0.0

    while true
        last_term = last_part(k)

        if last_term < 1e-15
            break
        end

        k += 1
        sum += last_term
    end

    one_over_pi = ((2 * √2) / 9801) * sum

    return 1 / one_over_pi
end

println(estimatepi())

println("End.")


"""
    sum_gp(first_term, ratio, num_terms)

Finds sum of n terms in a geometric progression

# Input parameters

- first_term : first term of the series
- raio      : common ratio between consecutive terms -> a2/a1 or a3/a2 or a4/a3
- num_terms  : number of terms in the series till which we count sum

# Example

```julia
sum_gp(1, 2, 10)    # 1023.0
sum_gp(1, 10, 5)    # 11111.0
sum_gp(0, 2, 10)    # 0.0
sum_gp(1, 0, 10)    # 1.0
sum_gp(1, 2, 0)     # -0.0
sum_gp(-1, 2, 10)   # -1023.0
sum_gp(1, -2, 10)   # -341.0
```

Contributed By:- [Ashwani Rathee](https://github.com/ashwani-rathee)
"""
function sum_gp(first_term, ratio, num_terms)
    # case where ratio is 1
    if ratio == 1
        return num_terms * first_term
    end
    # ormula for finding sum of n terms of a geometric progression
    return (first_term / (1 - ratio)) * (1 - ratio^num_terms)
end


using NewtonsMethod
using Test

#Test method 1
f(x)=x^2-4.0
f′(x)=2x
@test newtonroot(f,f′,x₀=0.2)[1]≈2.0 atol=0.000001

f(x)=log(x)-20
f′(x)=1/x
@test newtonroot(f,f′,x₀=0.2)[1]≈4.851651954097909e8 atol=0.000001

f(x)=3x^2-5x+1
f′(x)=6x-5
@test newtonroot(f,f′,x₀=0.2)[1]≈0.23240824122077 atol=0.000001

f(x)=-10x^3-5x^2+20
f′(x)=-20x^2-10x
@test newtonroot(f,f′,x₀=0.2)[1]≈1.11338619006481 atol=0.000001

#Test method 2
f(x)=x^2-4.0
@test newtonroot(f,x₀=0.2)[1]≈2.0 atol=0.000001

f(x)=log(x)-20
@test newtonroot(f,x₀=0.2)[1]≈4.851651954097909e8 atol=0.000001

f(x)=3x^2-5x+1
@test newtonroot(f,x₀=0.2)[1]≈0.23240824122077 atol=0.000001

f(x)=-10x^3-5x^2+20
@test newtonroot(f,x₀=0.2)[1]≈1.11338619006481 atol=0.000001

#Test Big Float (Not really sure what to do here. What I did is compare the root with a BigFloat)
f(x)=3x^2-5x+1
a=BigFloat(0.23240824122077)
@testset "BigFloat" begin
  @test newtonroot(f,x₀=0.2)[1]≈0.23240824122077 atol=0.000001
  @test newtonroot(f,x₀=0.2)[1]≈a atol=0.000001
 end;

#(I also set all parameters to be BigFloat)
f(x)=3x^2-5x+1
@test newtonroot(f,x₀= BigFloat(0.2),tolerance = BigFloat(1E-7), maxiter = BigFloat(1000))[1]≈0.23240824122077 atol=0.000001

#Test non-convergence (return nothing)
f(x)=2+x^2
@test newtonroot(f,x₀=0.2)==nothing

#Test maxiter (first should return the value, second should return nothing)
f(x)=log(x)-20
a=newtonroot(f,x₀=0.2)[1] #Algorithm needs 17 iterations in this case
b=newtonroot(f,x₀=0.2,maxiter=5)
@testset "maxiter" begin
  @test a≈4.851651954097909e8 atol=0.000001
  @test b==nothing
 end;

#Test tolerance (As tolerance parameter increases, the code is less accurate)
f(x)=3x^2-5x+1
a=newtonroot(f,x₀=1)[1]
b=newtonroot(f,x₀=1, tolerance=0.0005)[1]
c=newtonroot(f,x₀=1, tolerance=0.05)[1]
@test f(a)<f(b)<f(c)


using Plots

function bracket_minimum(f, x=0; s=1e-2, k=2.0)
    a, ya = x, f(x)
    b, yb = a + s, f(a + s)
    if yb > ya
        a, b = b, a
        ya, yb = yb, ya
        s = -s
    end
    while true
        c, yc = b + s, f(b + s)
        if yc > yb
            return a < c ? (a, c) : (c, a)
        end
        a, ya, b, yb = b, yb, c, yc
        s *= k
    end
end

φ = MathConstants.φ
function golden_section_search(f, a, b, n)
    ρ = φ-1
    d = ρ * b + (1 - ρ)*a
    yd = f(d)
    for i = 1 : n-1
        c = ρ*a + (1 - ρ)*b
        yc = f(c)
        if yc < yd
            b, d, yd = d, c, yc
        else
            a, b = b, c
        end
    end
    return a < b ? (a, b) : (b, a)
end

f = x->sin(10x)+cos(3x)
x0 = 3.4
#busca do intervalo
a,c = bracket_minimum(f,x0)

#numero de iteracoes
n=(c-a)/(10^-3*log(φ))

am,bm = golden_section_search(f,a,c,n)
(am,f(am))
am,bm

plot(f,0.,4.,lw=2,draw_arrow="true")
scatter!([a,c],[f(a),f(c)])
scatter!([am,bm],[f(am),f(bm)])


function getPolynomial(X, Y)
    n = length(Y)
    p = string("P = x -> ", Y[1])
    for i::Int in 2:n
        P = string(p, " + ", Y[i])
        for j in 1:i-1
            p = string(p, "*(x-", X[j], ")")
        end
    end
    return eval(Meta.parse(p))
end

function getCoefficients(X, Y)

    n = length(X)

    for i::Int in 2:n
        for j::Int in 2:i
            Y[i, j] = (Y[i, j-1] - Y[i-1, j-1])/(X[i] - X[i-j+1])
        end
    end

    F = []

    for i in 1:n
        push!(F, Y[i, i])
    end

    return F
end

"""
    divid(X, Y, cff)

Computes interpolating polynomial passing through the points whoose coordinates are provided in `X[]` and `Y[]`
using Newton's divided difference method. Returns `::Array{Float64}` if `cff = true` (default), or the interpolating polynomial P if `cff = false`.

    divid(X::Array{Float64}, Y::Array{Float64}, cff::Bool=true)

### OUTPUT:
  * `F` - coefficiants of the interpolating polynomial of the type `Array{Float64}`,
  * `P` - interpolating polynomial.
"""
function divid(X::Array{Float64}, Y::Array{Float64}, cff::Bool=true)
    if cff
        return getCoefficients(X, Y)
    else
        return getPolynomial(X, Y)
    end
end


#HW2 Problem 4
#Minhao Yan my497, Wentong Chen wc422

#1. Recast the problem as an unconstrained utility maximization problem
#Plug the budget constraint back to the utility function
#u(c1) = (κ*c1^((η-1)/η)+(1-κ)*((w-p1*c1)/p2)^((η-1)/η))^(η/(η-1))

#2. Solve this utility maximization problem
Pkg.add("Optim")
using Optim

function utility_maximizer(κ, η, p1, p2, w, initial_guess, solver)
    u(c1)=-((κ*c1^((η-1)/η)+(1-κ)*((w-p1*c1)/p2)^((η-1)/η)))^(η/(η-1))
    results = optimize(u,initial_guess)
    c1_min=Optim.minimizer(results)[1]
    c2_min= (w-p1*c1_min)/p2
    u_max=-Optim.minimum(results)
    println("c1= $c1_min")
    println("c2= $c2_min")
    println("u_max= $u_max")
    return [c1_min c2_min u_max]
    return results
end

#3.Solve analytically
κ=.5
η=3
w=50
p1=2
p2=3
initial_guess=0.0
Result_ana = utility_maximizer(κ, η, p1, p2, w, initial_guess, Newton())

#Solve numerically
u1(c1)=κ*c1^((η-1)/η)+(1-κ)*((w-p1*c1)/p2)^((η-1)/η)
function g!(storage, x)
    storage[1]=-(η/(η-1))*u1(x[1])^(1/(η-1))*(κ*((η-1)/η)*x[1]^(-1/η)+(1-κ)*((η-1)/η)*((w-p1*x[1])/p2)^(-1/η)*(-p1/p2))
    storage[2] = 0.0
end
function utility_maximizer_num(κ, η, p1, p2, w, initial_guess, solver)
    x=initial_guess
    u(c1)=0.0*x[2]-((κ*c1^((η-1)/η)+(1-κ)*((w-p1*c1)/p2)^((η-1)/η)))^(η/(η-1))
    results = optimize(u, g!, initial_guess)
    c1_min=Optim.minimizer(results)[1]
    c2_min= (w-p1*c1_min)/p2
    u_max=-Optim.minimum(results)
    println("c1= $c1_min")
    println("c2= $c2_min")
    println("u_max= $u_max")
    return [c1_min c2_min u_max]
    return results
end

κ=.5
η=3
w=50
p1=2
p2=3
initial_guess=0.0
Return_num = utility_maximizer_num(κ, η, p1, p2, w, initial_guess, Newton())


function cosine_similarity(set1::Union{Vector, Array}, set2::Union{Vector, Array})

    @assert length(set1) == length(set2) "Sets don't have the same length."

    num = zero(eltype(set1))
    for i in 1:length(set1)
        num += set1[i] * set2[i]
    end
    norm_set1 = zero(eltype(set1))
    norm_set2 = zero(eltype(set2))
    for i in 1:length(set1)
        norm_set1 += set1[i]^2
        norm_set2 += set2[i]^2
    end
    norm_set1, norm_set2 = sqrt(norm_set1), sqrt(norm_set2)
    dem = norm_set1 * norm_set2

    if dem == 0
        throw(ArgumentError("denominador igual a 0"))
    end
    return num/dem
end

function cosine_similarity_2(set1, set2)
    return dot(set1, set2) / (norm(set1) * norm(set2))
end


"Fits a straight line through a set of points, `y = a₁ + a₂ * x`"
linear_fit(x, y) = hcat(fill!(similar(x),1), x) \ y

"Fits a log function through a set of points: `y = a₁+ a₂*log(x)`"
log_fit(x, y) = linear_fit(log.(x), y)

"Fits a power law through a set of points: `y = a₁*x^a₂`"
function power_fit(x, y)
    fit = linear_fit(log.(x), log.(y))
    [exp(fit[1]), fit[2]]
end

"Fits an `exp` through a set of points: `y = a₁*exp(a₂*x)`"
function exp_fit(x, y)
    fit = linear_fit(x, log.(y))
    [exp(fit[1]), fit[2]]
end

"""
Fits a polynomial of degree `n` through a set of points.

Simple algorithm, doesn't use orthogonal polynomials or any such thing
and therefore unconditioned matrices are possible. Use it only for low
degree polynomials.

This function returns a the coefficients of the polynomial.
"""
function poly_fit(x, y, n)

    nx = length(x)
    A = zeros(eltype(x), nx, n+1)
    A[:,1] .= 1.0
    for i in 1:n
        for k in 1:nx
            A[k,i+1] = A[k,i] * x[k]
        end
    end
    A\y
end

"""
High Level interface for fitting straight lines
"""
struct LinearFit{T<:Number} <: LeastSquares
    coefs::Array{T,1}
    LinearFit{T}(coefs) where {T<:Number} = new(copy(coefs))
end
LinearFit(x::AbstractVector{T}, y::AbstractVector{T}) where {T<:Number} = LinearFit{T}(linear_fit(x, y))

"High Level interface for fitting log laws"
struct LogFit{T<:Number} <: LeastSquares
    coefs::Array{T,1}
    LogFit{T}(coefs) where {T<:Number} = new(copy(coefs))
end
LogFit(x::AbstractVector{T}, y::AbstractVector{T}) where {T<:Number} = LogFit{T}(log_fit(x, y))

"High Level interface for fitting power laws"
struct PowerFit{T<:Number} <: LeastSquares
    coefs::Array{T,1}
    PowerFit{T}(coefs) where {T<:Number} = new(copy(coefs))
end
PowerFit(x::AbstractVector{T}, y::AbstractVector{T}) where {T<:Number} = PowerFit{T}(power_fit(x, y))

"High Level interface for fitting exp laws"
struct ExpFit{T<:Number} <: LeastSquares
    coefs::Array{T,1}
    ExpFit{T}(coefs) where {T<:Number} = new(copy(coefs))
end
ExpFit(x::AbstractVector{T}, y::AbstractVector{T}) where {T<:Number} = ExpFit{T}(exp_fit(x, y))

"""
# Generic interface for curve fitting.

The same function `curve_fit` can be used to fit the data depending on fit type,
shich is specified in the first parameter. This function returns an object that
can be used to estimate the value of the fitting model using function `apply_fit`.

## A few examples:

 * `f = curve_fit(LinearFit, x, y)`
 * `f = curve_fit(Poly, x, y, n)`
"""
curve_fit(::Type{T}, x, y) where {T<:LeastSquares} = T(x, y)
curve_fit(::Type{T}, x, y, args...) where {T<:LeastSquares} = T(x, y, args...)
curve_fit(::Type{Poly}, x, y, n=1) = Poly(poly_fit(x, y, n))

(f::LinearFit)(x) = f.coefs[1] + f.coefs[2] *x
(f::PowerFit)(x) = f.coefs[1] * x ^ f.coefs[2]
(f::LogFit)(x) = f.coefs[1] + f.coefs[2] * log(x)
(f::ExpFit)(x) = f.coefs[1] * exp(f.coefs[2] * x)


using NewtonsMethod
using Test

#Test method 1
f(x)=x^2-4.0
f′(x)=2x
@test newtonroot(f,f′,x₀=0.2)[1]≈2.0

f(x)=log(x)-20
f′(x)=1/x
@test newtonroot(f,f′,x₀=0.2)[1]≈4.851651954097909e8

f(x)=3x^2-5x+1
f′(x)=6x-5
@test newtonroot(f,f′,x₀=0.2)[1]≈2.32408120756001784480129727405038536389084291259026547774006206530426035584922e-01

f(x)=-10x^3-5x^2+20
f′(x)=-20x^2-10x
@test newtonroot(f,f′,x₀=0.2)[1]≈1.113386205687796447877673190361708447749219094960359587626412934254462034979434

#Test method 2
f(x)=x^2-4.0
@test newtonroot(f,x₀=0.2)[1]≈2.0

f(x)=log(x)-20
@test newtonroot(f,x₀=0.2)[1]≈4.851651954097909e8

f(x)=3x^2-5x+1
@test newtonroot(f,x₀=0.2)[1]≈2.32408120756001784480129727405038536389084291259026547774006206530426035584922e-01

f(x)=-10x^3-5x^2+20
@test newtonroot(f,x₀=0.2)[1]≈1.113386205687796447877673190361708447749219094960359587626412934254462034979434

#Test Big Float (Not really sure what to do here)
#(I set all parameters to be BigFloat)
f(x)=3x^2-5x+1
@test newtonroot(f,x₀= BigFloat(0.2),tolerance = BigFloat(1E-100), maxiter = BigFloat(1000))[1]  ≈  2.32408120756001784480129727405038536389084291259026547774006206530426035584922e-01

#Test non-convergence (return nothing)
f(x)=2+x^2
@test newtonroot(f,x₀=0.2)==nothing

#Test maxiter (first should return the value, second should return nothing)
f(x)=log(x)-20
a=newtonroot(f,x₀=0.2)[1] #Algorithm needs 17 iterations in this case
b=newtonroot(f,x₀=0.2,maxiter=5)
@testset "maxiter" begin
  @test a≈4.851651954097909e8
  @test b==nothing
 end;

#Test tolerance (As tolerance parameter increases, the code is less accurate)
f(x)=3x^2-5x+1
a=newtonroot(f,x₀=1)[1]
b=newtonroot(f,x₀=1, tolerance=0.0005)[1]
c=newtonroot(f,x₀=1, tolerance=0.05)[1]
@test f(a)<f(b)<f(c)


# # Basic linear algebra in Julia
# Author: Andreas Noack Jensen (MIT) (http://www.econ.ku.dk/phdstudent/noack/)
# (with edits from Jane Herriman)

#-

# First let's define a random matrix

A = rand(1:4,3,3)

# Define a vector of ones

x = fill(1.0, (3,)) # = fill(1.0, 3)

# Notice that $A$ has type Array{Int64,2} but $x$ has type Array{Float64,1}. Julia defines the aliases Vector{Type}=Array{Type,1} and Matrix{Type}=Array{Type,2}.
#
# Many of the basic operations are the same as in other languages
# #### Multiplication

b = A*x

# #### Transposition
# As in other languages `A'` is the conjugate transpose, or adjoint

A'

# and we can get the transpose with

transpose(A)

# #### Transposed multiplication
# Julia allows us to write this without *

A'A

# #### Solving linear systems
# The problem $Ax=b$ for ***square*** $A$ is solved by the \ function.

A\b

# `A\b` gives us the *least squares solution* if we have an overdetermined linear system (a "tall" matrix)

Atall = rand(3, 2)

#-

Atall\b

# and the *minimum norm least squares solution* if we have a rank-deficient least squares problem

v = rand(3)
rankdef = hcat(v, v)

#-

rankdef\b

# Julia also gives us the minimum norm solution when we have an underdetermined solution (a "short" matrix)

bshort = rand(2)
Ashort = rand(2, 3)

#-

Ashort\bshort

# # The LinearAlgebra library
#
# While much of linear algebra is available in Julia by default (as shown above), there's a standard library named `LinearAlgebra` that brings in many more relevant names and functions. In particular, it provides factorizations and some structured matrix types.  As with all packages, you can bring these additional features into your session with a `using LinearAlgebra`.

#-

# ### Exercises
#
# #### 10.1
# Take the inner product (or "dot" product) of a vector `v` with itself and assign it to variable `dot_v`.

v = [1,2,3]

#-

@assert dot_v == 14

# #### 10.2
# Take the outer product of a vector v with itself and assign it to variable `cross_v`

@assert cross_v == [0, 0, 0]


using Distributions
using LinearAlgebra
using BenchmarkTools

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

function find_Q(A, l)
    #=
    Given an m × n matrix A, and an integer l, compute an m × l orthonormal
    matrix Q whose range approximates the range of A.
    =#
    m, n = size(A)
    Ω = rand(Normal(), n, l)
    Y = A * Ω
    Q, R = qr(Y)
    return Q
end

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

function randomized_SVD(A, k)
    #=
    Given an m × n matrix A, a target number k of singular vectors, and an
    exponent q (say q = 1 or q = 2), this procedure computes an approximate
    rank-2k factorization UΣVt, where U and V are orthonormal and Σ is
    nonnegative and diagonal.
    =#
    Q = find_Q(A, 2*k)
    B = Q' * A
    S, Σ, Vt = svd(B)
    U = Q * S
    return U, Σ, Vt
end

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

m = 2000
n = 20000
k = 10
# Construct low-rank matrix
A = rand(m, k) * rand(k, n)
println("Rank of A: ", rank(A))
println("Size of A: ", size(A))

println("Throwaway test:")
@time svd(A)
@time randomized_SVD(A, k)
println("Actual test:")
@benchmark svd(A)
@benchmark randomized_SVD(A, k)

println("Completed")

# https://stackoverflow.com/questions/53162206/why-does-my-randomized-svd-implementation-use-so-much-memory


@doc raw"""
    lentz_thompson(b₀, a, b)

Lentz–Thompson algorithm for the forward evaluation of continued
fractions:

```math
f_n = b_0 + \frac{a_1}{b_1+}\frac{a_2}{b_2+}...\frac{a_{n-1}}{b_{n-1}+}\frac{a_n}{b_n}
```

As described in

- Barnett, A. R. (1996). The Calculation of Spherical Bessel and
  Coulomb Functions. In (Eds.), Computational Atomic Physics
  (pp. 181–202). Springer Berlin Heidelberg.

"""
function lentz_thompson(b₀::T, a, b;
                        max_iter=20000,
                        ϵ = eps(T), tol=100eps(T),
                        verbosity=0) where T
    val_or_ϵ(x) = abs(x) < ϵ ? ϵ : x

    f = val_or_ϵ(b₀)
    C = f
    D = zero(T)
    δ = zero(T)

    fmt = if verbosity>1
        printfmtln("{1:>4s} {2:>10s} {3:>10s} {4:>10s} {5:>10s} {6:>10s} {7:>10s} {8:>10s}",
                   "n", "a", "b", "C", "D", "|Δ-1|", "f", "δf")
        FormatExpr("{1:>4d} {2:10.3e} {3:10.3e} {4:10.3e} {5:10.3e} {6:10.3e} {7:10.3e} {8:10.3e}")
    else
        nothing
    end

    s = true

    for n = 1:max_iter
        aₙ = a(n)
        bₙ = b(n)
        C = val_or_ϵ(bₙ + aₙ/C)
        D = inv(val_or_ϵ(bₙ + aₙ*D))
        if isreal(D) && D < 0
            # See section 3.2 of
            #
            # - Barnett, A., Feng, D., Steed, J., & Goldfarb, L. (1974). Coulomb
            #   wave functions for all real $\eta$ and ρ. Computer Physics
            #   Communications, 8(5),
            #   377–395. http://dx.doi.org/10.1016/0010-4655(74)90013-7
            s = !s
        end
        Δ = C*D
        fn = f*Δ
        δ = abs(Δ-1)
        verbosity>1 && printfmtln(fmt, n, aₙ, bₙ, C, D, δ, fn, fn-f)
        f = fn
        δ < tol && return f,n,δ,s,true
    end

    verbosity > 0 && @warn "Lentz–Thompson did not converge in $(max_iter) iterations, |Δ-1| = $(δ)"
    f,max_iter,δ,s,false
end


"""Julia program to implement Fibonacci Search algorithm.
Fibonacci Search is a Divide and Conquer strategy based algorithm that works for sorted arrays
Fibonacci Search avoids CPU costly division operation and uses addition and subtraction opertations
This algorithm uses Fibonacci series as an underlying principle, where every element of the series is defined by:

    fib(n) = fib(n-1) + fib(n-2), where fib(0) = 0, fib(1) = 1
"""

function fibonacci_search(arr, n, ele)

    # We start the series from 1, 1 as the Julia language is 1-based indexing
    fibn2 = 1
    fibn1 = 1
    fibn = fibn1 + fibn2
    while (fibn < n)
        fibn2 = fibn1
        fibn1 = fibn
        fibn = fibn1 + fibn2
    end

    # To denote the eliminted range from left.
    offset = 0

    while (fibn > 1)
        i = min(offset + fibn2, n - 1)

        """Compares the element in i'th index with ele and decide the next fibonacci element. If the element doesn't
            turns out to be greater than or less than the element at i'th index then we find the required element"""
        if (arr[i] < ele)
            fibn = fibn1;
            fibn1 = fibn2;
            fibn2 = fibn - fibn1;
            offset = i;
        elseif (arr[i] > ele)
            fibn = fibn2;
            fibn1 = fibn1 - fibn2;
            fibn2 = fibn - fibn1;
        else
            return true
        end
        if (arr[offset + 1] == ele)
            return true
        end
    end
    return false
end

print("How many numbers are present in the array? ")
n = readline()
n = parse(Int, n)
if (n <= 0)
    println("Array is Empty!!!")
    exit()
end
arr = Int[]
print("Enter the numbers: ")
arr = [parse(Int, num) for num in split(readline())]
print("Which number do you want to search in the array? ")
ele = readline()
ele = parse(Int, ele)
# Sort the array in ascending order
arr = sort(arr)
res = fibonacci_search(arr, n, ele)
if (res == 0)
    print("The number $ele is not present in the array")
else
    print("The number $ele is present in the array.")
end

"""
Time Complexity - O(log(n)), where 'n' is the size of the array
Space Complexity - O(n)

SAMPLE INPUT AND OUTPUT

SAMPLE I

How many numbers are present in the array? 5
Enter the numbers: 1 2 3 4 5
Which number do you want to search in the array? 6
The number 6 is not present in the array

SAMPLE II

How many numbers are present in the array? 3
Enter the numbers: 3 1 2
Which number do you want to search in the array? 2
The number 2 is present in the array.

"""


#=
Created on 09/01/2021 15:00:09
Last update: -

@author: Michiel Stock
michielfmstock@gmail.com

Implement the information entropy:
$$
H(p) = -\sum_i p_i \log p_i\,,
$$

Take caution that, by definition,$0 \log 0$ is evaluated as 0 (i.e., its limit value).

Have an optional flag to specify the bag of log, default is base 2, making the output in bits.

Make a plot of the entropy of a Bernouilli-distributed variable for varying probability of success
$p$, i.e., the entropy of probability vector $[p, 1-p]$ for $p\in [0, 1]$.
=#

using Plots

entropy(p; base=2) = p .|> (pᵢ -> pᵢ > 0.0 ? -pᵢ * log(pᵢ) : 0.0) |> sum |> (H -> H / log(base))

entropy([0, 1, 0])  # 0.0
entropy([0.5, 0.5])  # 1.0
entropy([0.5, 0.5], base=10)  # 0.30102999566398114

plot(p->entropy((p, 1-p)), 0, 1, xlabel="p", label="entropy")


using FiniteDiff, LinearAlgebra, SparseArrays, Test, StaticArrays

function f(x)
  xm1 = [0;x[1:end-1]]
  xp1 = [x[2:end];0]
  xm1 - 2x + xp1
end

function second_derivative_stencil(N)
  A = zeros(N,N)
  for i in 1:N, j in 1:N
      (j-i==-1 || j-i==1) && (A[i,j]=1)
      j-i==0 && (A[i,j]=-2)
  end
  A
end

x = @SVector ones(30)
J = FiniteDiff.finite_difference_jacobian(f,x, Val{:forward}, eltype(x))
@test J ≈ second_derivative_stencil(30)
_J = sparse(J)
J = FiniteDiff.finite_difference_jacobian(f,x, Val{:central}, eltype(x))
@test J ≈ second_derivative_stencil(30)

J = FiniteDiff.finite_difference_jacobian(f,x, Val{:complex}, eltype(x))
@test J ≈ second_derivative_stencil(30)

#1x1 SVector test
x = SVector{1}([1.])
f(x) = x
J = FiniteDiff.finite_difference_jacobian(f, x, Val{:forward}, eltype(x))
@test J ≈ SMatrix{1,1}([1.])
J = FiniteDiff.finite_difference_jacobian(f, x, Val{:central}, eltype(x))
@test J ≈ SMatrix{1,1}([1.])
J = FiniteDiff.finite_difference_jacobian(f, x, Val{:complex}, eltype(x))
@test J ≈ SMatrix{1,1}([1.])


function chebyshev_points(::Type{T}, s::Integer, ::Val{1}) where {T}
    shift_nodes(T[ @big sin( π*(s-2i+1) / (2s) ) for i in s:-1:1 ])
end

function chebyshev_points(::Type{T}, s::Integer, ::Val{2}) where {T}
    shift_nodes(T[ @big cos( π * (i-1) / (s-1) ) for i in s:-1:1 ])
end

"""
Gauss-Chebyshev quadrature.
"""
function GaussChebyshevQuadrature(::Type{T}, s::Integer) where {T}
    local tj::BigFloat = 0
    local th::BigFloat = 0

    c = chebyshev_points(BigFloat, s, Val(1))
    b = zero(c)
    for i in eachindex(b)
        tj = 0
        th = @big π * (2i-1) / (2s)
        for j in 1:div(s,2)
            tj += @big cos(2j*th) / (4j^2 - 1)
        end
        b[i] = @big (1 - 2tj) / s
    end

    QuadratureRule(2s, c, b, T)
end

GaussChebyshevQuadrature(s) = GaussChebyshevQuadrature(Float64, s)

"""
Lobatto-Chebyshev quadrature.
"""
function LobattoChebyshevQuadrature(::Type{T}, s::Integer) where {T}
    if s == 1
        throw(ErrorException("Lobatto-Chebyshev quadrature is not defined for one stage."))
    end

    local tj::BigFloat = 0
    local th::BigFloat = 0

    c = chebyshev_points(BigFloat, s, Val(2))
    b = zero(c)
    for i in eachindex(b)
        tj = 0
        th = @big π * i / (s+1)
        for j in 1:div(s+1,2)
            tj += @big sin((2j-1) * th) / (2j - 1)
        end
        b[i] = @big (2tj * sin(th)) / (s+1)
    end

    QuadratureRule(2s, c, b, T)
end

LobattoChebyshevQuadrature(s) = LobattoChebyshevQuadrature(Float64, s)

ChebyshevQuadrature(::Type{T}, s::Integer, ::Val{1}) where {T} = GaussChebyshevQuadrature(T,s)
ChebyshevQuadrature(::Type{T}, s::Integer, ::Val{2}) where {T} = LobattoChebyshevQuadrature(T,s)

ChebyshevQuadrature(s, kind) = ChebyshevQuadrature(Float64, s, Val(kind))


using LinearAlgebra
using Test

# 1
# a
A = [
   3 -1 -1  0 -1
  -1  3  0 -1 -1
  -1  0  3 -1 -1
   0 -1 -1  3 -1
  -1 -1 -1 -1  4
]

AᵀA = A' * A

# b
@test tr(AᵀA) == 68

@test rank(AᵀA) == 4

values, = eigen(AᵀA)

@test round(sum(values)) == 68

@test round(reduce(*, values)) == 0

# c
A₁ = [
   1 -1 0 0 0
  -1  1 0 0 0
   0  0 0 0 0
   0  0 0 0 0
   0  0 0 0 0
]

A₁ᵀA₁ = A₁' * A₁

@test A₁ᵀA₁ == [
  2  -2  0  0  0
 -2   2  0  0  0
  0   0  0  0  0
  0   0  0  0  0
  0   0  0  0  0
]

#d
b = ones(8, 1)

@test_throws SingularException inv(A)


module number

function gcd(a::T, b::T) where {T <: Integer}
    a, b = (a < b) ? (b, a) : (a, b)
    while b != 0
        c = b
        d = mod(a, b)
        a = c
        b = d
    end
    return a
end

function lcm(a, b)
    (a, b) = (a < b) ? (a, b) : (b, a)
    d = gcd(b, a)
    return div(b, d) * a
end

function sieve_of_eratosthenes(n)
    a = trues(n)
    max_n = ceil(Int, sqrt(Float64(n)))

    for ii = 2:max_n
        if a[ii]
            jj = ii^2
            while jj <= n
                a[jj] = false
                jj += ii
            end
        end
    end
    a[1] = false # for 1
    pr = findall(a)
    return pr
end

end

#=
module ntest
using ..number

a = 30 # 2 * 3 * 5
b = 21 # 3 * 7

d = number.gcd(a, b) # 3
l = number.lcm(a, b) # 7 * 2 * 5 * 3

a = 28 # 2*2 * 7
b = 21 # 3 * 7

d = number.gcd(a, b) # 3
l = number.lcm(a, b) # 2 * 2 * 3 * 7

pr = number.sieve_of_eratosthenes(1000)

end
=#


using Distributions, Plots, LaTeXStrings, Random; pyplot()

function tStat(mu0,mu,sig,n)
    sample = rand(Normal(mu,sig),n)
    xBar   = mean(sample)
    s      = std(sample)
    (xBar-mu0) / (s/sqrt(n))
end

function powerEstimate(mu0,mu1,sig,n,alpha,N)
    Random.seed!(0)
    sampleH1 = [tStat(mu0,mu1,sig,n) for _ in 1:N]
    critVal  = quantile(TDist(n-1),1-alpha)
    sum(sampleH1 .> critVal)/N
end

mu0 = 20
sig = 5
alpha = 0.05
N = 10^4
rangeMu1 = 16:0.1:30
nList = [5,10,20,30]

powerCurves = [powerEstimate.(mu0,rangeMu1,sig,n,alpha,N) for n in nList]

plot(rangeMu1,powerCurves[1],c=:blue, label="n = $(nList[1])")
plot!(rangeMu1,powerCurves[2],c=:red, label="n = $(nList[2])")
plot!(rangeMu1,powerCurves[3],c=:green, label="n = $(nList[3])")
plot!(rangeMu1,powerCurves[4],c=:purple, label="n = $(nList[4])",
	xlabel= L"\mu", ylabel="Power",
	xlims=(minimum(rangeMu1) ,maximum(rangeMu1)), ylims=(0,1))


export permanent, veroneselift

using Combinatorics

function permanent(A, perms)
    n = LinearAlgebra.checksquare(A)
    sum(σ -> prod(i -> A[i,σ[i]], 1:n), perms)
end

function μ(combi)
    prod = 1
    last = 0
    cur = 1
    for i in combi
        if i == last
            cur += 1
        else
            prod *= factorial(cur)
            last = i
            cur = 1
        end
    end
    prod *= factorial(cur)
    return prod
end

"""
   veroneselift_explicit(A::AbstractMatrix, d::Integer)

Computes the Veronese lift of degree `d` of `A` using the explicit formula
```math
(A^{[d]})_{\\alpha\\beta} = \\frac{\\text{per}\\,A(\\alpha, \\beta)}{\\sqrt{\\mu(\\alpha)\\mu(\\beta)}}
```
given in equation (8) of [PJ08]. Note that this is a lot slower than
[`veroneselift`](@ref) and is implemented mainly for educational purpose.

* [PJ08] P. Parrilo and A. Jadbabaie.
*Approximation of the joint spectral radius using sum of squares*.
Linear Algebra and its Applications, Elsevier, **2008**, 428, 2385-2402
"""
function veroneselift_explicit(A::AbstractMatrix, d::Integer)
    d > 0 || throw(ArgumentError("`d` should be a positive integer, got $d"))

    n = LinearAlgebra.checksquare(A)
    N = binomial(n+d-1, d)

    Ad = Matrix{Float64}(undef, N, N)

    # Computing permutations is the most expensive part so we precompute it
    perms = collect(permutations(1:d))
    for (i,α) in enumerate(with_replacement_combinations(1:n, d))
        for (j,β) in enumerate(with_replacement_combinations(1:n, d))
            Ad[i, j] = permanent(A[α, β], perms) / sqrt(μ(α) * μ(β))
        end
    end

    return Ad
end

function transform_combination(α, m)
    β = zeros(Int, m)
    for i in α
        β[i] += 1
    end
    return β
end

"""
   veroneselift(A::AbstractMatrix, d::Integer)

Computes the Veronese lift of degree `d` of `A` using `DynamicPolynomials`.
"""
function veroneselift(A::AbstractMatrix, d::Integer)
    d > 0 || throw(ArgumentError("`d` should be a positive integer, got $d"))
    m, n = size(A)
    @polyvar x[1:n]
    Ax = A * x
    M = binomial(m + d - 1, d)
    N = binomial(n + d - 1, d)
    Ad = Matrix{Float64}(undef, M, N)
    df = factorial(d)
    scaling(m) = sqrt(df / prod(factorial, exponents(m)))
    X = monomials(x, d)
    # Scaling for the scaled monomial basis
    col_scaling = Float64[scaling(m) for m in X]
    if m == n
        Y = X
        row_scaling = col_scaling
    else
        # Only exponents matter
        @polyvar y[1:m]
        Y = monomials(y, d)
        row_scaling = Float64[scaling(m) for m in Y]
    end
    for (i, mono) in enumerate(Y)
        α = exponents(mono)
        Axα = prod(i -> Ax[i]^α[i], 1:m)
        Ad[i, :] = MP.coefficients(Axα, X) .* row_scaling[i] ./ col_scaling
    end
    return Ad
end


module MonteCarlo

export mcintegral

using Statistics

"""
Integrate a 1D real function using Monte Carlo integration with importance sampling

Does uniform sampling by default

# Arguments
- `func`: the function to be integrated
- `lo`: lower bound of the integral
- `up`: upper bound of the integral
- `samples`: the number of random samples used for integration
- `impfunc`: The importance sampling function.
    Constant function `x -> 1` by default (for uniform sampling).
- `impint`: the integral of the importance function in the integration interval. If equal to
    zero, this function will calculate the integral itself. `up - lo` by default
    (for uniform sampling).
- `distribution`: Applies this transformation function to the random numbers generated
    in the range [0, 1] to get the random samples used for integration. This must be
    a distribution that gives a probability density function proportional to impfunc
    to get the correct results. By default, samples are uniformly generated in the range
    [lo, up].

# Returns

The calculated integral and the standard error
"""
function mcintegral(func::Function, lo::Real, up::Real; samples::Integer = 10000,
        impfunc::Function = x -> 1, impint::Real = up - lo,
        distribution::Function = x -> muladd(x, up - lo, lo))
    if impint == 0
        impint = (up - lo) * mean(impfunc.(muladd.(rand(samples), up - lo, lo)))
    end

    intfunc(x) = func(x) / impfunc(x)
    samplevalues = intfunc.(distribution.(rand(samples)))
    avg = mean(samplevalues)

    integral = impint * avg
    error = impint / √samples * std(samplevalues, mean=avg)

    return integral, error
end

"""
n-dimensional Monte Carlo integration with importance sampling

Does uniform sampling by default

# Arguments
- `func`: the function to be integrated
- `lo`: vector of lower bounds of the integral
- `up`: vector of upper bounds of the integral
- `samples`: the number of random samples used for integration
- `impfunc`: The importance sampling function.
    Constant function `(x...) -> 1` by default (for uniform sampling).
- `impint`: the integral of the importance function in the integration interval. If equal to
    zero, this function will calculate the integral itself. `up - lo` by default
    (for uniform sampling).
- `distribution`: Applies this transformation function to the random collection of numbers
    generated in the range [0, 1] to get the random samples used for integration. This must
    be a distribution that gives a probability density function proportional to impfunc
    to get the correct results. By default, samples are uniformly generated in the range
    [lo, up].

Please note that `length(lo)` must equal `length(up)` and all functions should also take
this many arguments as their input.

# Returns

The calculated integral and the standard error
"""
function mcintegral(func::Function, lo::Vector{<:Real}, up::Vector{<:Real};
        samples::Integer = 10000,
        impfunc::Function = (x...) -> 1, impint::Real = reduce(.*, up .- lo),
        distribution::Function = (x...) -> muladd.(x, up .- lo, lo))
    ndims = length(up)
    if impint == 0
        rndnums = rand(ndims, samples)
        samplepoints = [muladd.(x, up .- lo, lo) for x in eachcol(rndnums)]
        impint = (up - lo) * mean(impfunc(x...) for x in samplepoints)
    end

    rndnums = rand(ndims, samples)
    samplepoints = [distribution(x...) for x in eachcol(rndnums)]

    intfunc(x...) = func(x...) / impfunc(x...)
    samplevalues = [intfunc(x...) for x in samplepoints]
    avg = mean(samplevalues)

    integral = impint * avg
    error = impint / √samples * std(samplevalues, mean=avg)

    return integral, error
end

end