export minkowski_distance, euclidian_distance, manhattan_distance, chebyshev_distance, braycurtis_distance, canberra_distance, cityblock_distance, mahalanobis_distance,
correlation, cosine_distance, cosine_similarity

using LinearAlgebra
using Statistics: mean

#TODO
# scikitlearn-like kernels ?

# Distance methods

"""
```minkowski_distance(u,v; keywords)```
```minkowski_distance(x::Tuple; keywords)```

Return the Minkowski distance between the given points both of which are given with vector-like structures.

## Arguments
- `u` : first point
- `v` : second point
- `x::Tuple` : tuple as follows: (u,v)

## Keywords
- `p::Int = 2` : p value
- `w::Int` : weights that must be of the same length with u,v
- `rooted = false` : denotes whether or not to execute the final rooting operation
"""
function minkowski_distance(u,v; p = 2, w = nothing, rooted = false)
    _validate_distance_input(u, v, w; p = p, p_is_used = true)
    val = abs.(u .- v)
    val = w != nothing ? val .* w : val
    if p == 1
        val = sum(val)
    elseif p == Inf
        val = maximum(val)
    else
        val = sum(val .^ p)
    end
    return rooted ? val .^ (1 / p) : val
end

minkowski_distance(x::Tuple; p = 2, w = nothing) = minkowski_distance(x...; p = p, w = w)

"""
```euclidian_distance(u,v; keywords)```
```euclidian_distance(x::Tuple; keywords)```

Return the Euclidian distance between the given points both of which are given with vector-like structures.

## Arguments
- `u` : first point
- `v` : second point
- `x::Tuple` : tuple as follows: (u,v)

## Keywords
- `w::Int` : weights that must be of the same length with u,v
- `squared = false` : denotes whether or not to execute the final squaring operation
"""
euclidian_distance(u,v; w = nothing, squared = false) = minkowski_distance(u,v; w = w, p = 2, rooted = squared)
euclidian_distance(x::Tuple;w = nothing, squared = false) = euclidian_distance(x...; w = w, rooted = squared)

"""
```manhattan_distance(u,v; keywords)```
```manhattan_distance(x::Tuple; keywords)```

Return the Manhattan distance between the given points both of which are given with vector-like structures.

## Arguments
- `u` : first point
- `v` : second point
- `x::Tuple` : tuple as follows: (u,v)

## Keywords
- `w::Int` : weights that must be of the same length with u,v
"""
manhattan_distance(u,v; w = nothing) = minkowski_distance(u,v; w = w, p = 1)
manhattan_distance(x::Tuple;w = nothing) = manhattan_distance(x...; w = w)

"""
```chebyshev_distance(u,v; keywords)```
```chebyshev_distance(x::Tuple; keywords)```

Return the Chebyshev distance between the given points both of which are given with vector-like structures.

## Arguments
- `u` : first point
- `v` : second point
- `x::Tuple` : tuple as follows: (u,v)

## Keywords
- `w::Int` : weights that must be of the same length with u,v
"""
chebyshev_distance(u,v; w = nothing) = minkowski_distance(u,v; w = w, p = Inf, rooted=false)
chebyshev_distance(x::Tuple;w = nothing) = chebyshev_distance(x...; w = w)

"""
```braycurtis_distance(u,v; keywords)```
```braycurtis_distance(x::Tuple; keywords)```

Return the Braycurtis distance between the given points both of which are given with vector-like structures.

## Arguments
- `u` : first point
- `v` : second point
- `x::Tuple` : tuple as follows: (u,v)

## Keywords
- `w::Int` : weights that must be of the same length with u,v
"""
function braycurtis_distance(u,v; w = nothing)
    _validate_distance_input(u, v, w)
    difference = abs.(u .- v)
    summation = abs.(u .+ v)
    if w != nothing
        difference = w .* difference
        summation = w .* summation
    end
    sum(difference) / sum(summation)
end

braycurtis_distance(x::Tuple; w = nothing) = braycurtis_distance(x...; w = w)

"""
```canberra_distance(u,v; keywords)```
```canberra_distance(x::Tuple; keywords)```

Return the Canberra distance between the given points both of which are given with vector-like structures.

## Arguments
- `u` : first point
- `v` : second point
- `x::Tuple` : tuple as follows: (u,v)

## Keywords
- `w::Int` : weights that must be of the same length with u,v
"""
function canberra_distance(u,v; w = nothing)
    _validate_distance_input(u, v, w)
    difference = abs.(u .- v)
    result = difference / (abs.(u) .+ abs.(v))
    if w != nothing
        result = result .* w
    end
    return  sum(result[result .!== Inf])
end
canberra_distance(x::Tuple; w = nothing) = canberra_distance(x...; w = w)

"""
```cityblock_distance(u,v; keywords)```
```cityblock_distance(x::Tuple; keywords)```

Return the Cityblock distance between the given points both of which are given with vector-like structures.

## Arguments
- `u` : first point
- `v` : second point
- `x::Tuple` : tuple as follows: (u,v)

## Keywords
- `w::Int` : weights that must be of the same length with u,v
"""
function cityblock_distance(u,v; w = nothing)
    _validate_distance_input(u,v,w)
    result = abs.(u .- v)
    if w != nothing
        result = result .* w
    end
    return sum(result)
end
cityblock_distance(x; w = nothing) = cityblock_distance(x...; w = w)

"""
```mahalanobis_distance(u,v; keywords)```
```mahalanobis_distance(x::Tuple; keywords)```

Return the Cityblock distance between the given points both of which are given with vector-like structures.

## Arguments
- `u` : first point
- `v` : second point
- `x::Tuple` : tuple as follows: (u,v)
- `Vinv::Matrix` : The inverse of the covariance matrix.

"""
function mahalanobis_distance(u,v, Vinv)
    _validate_distance_input(u,v,nothing)
    delta = u .- v
    return sqrt.(dot(dot(delta, convert_1d(Vinv)), delta))
end

mahalanobis_distance(x::Tuple, Vinv) = mahalanobis_distance(x..., Vinv)

##

"""
```correlation(u,v; keywords)```
```correlation(x::Tuple; keywords)```

Return the Cityblock distance between the given points both of which are given with vector-like structures.

## Arguments
- `u` : first point
- `v` : second point
- `x::Tuple` : tuple as follows: (u,v)

## Keywords
- `w::Int` : weights that must be of the same length with u,v
- `centered=true` : If true, u and v will be centered.
"""
function correlation(u,v; w = nothing, centered = true)
    _validate_distance_input(u,v,w)
    w = w != nothing ? w : ones(length(u))
    _u = convert_1d(u)
    _v = convert_1d(v)
    if centered
        umu = mean(_u .* w)
        vmu = mean(_v .* w)
        _u = _u .- umu
        _v = _v .- vmu
    end
    uv = mean(_u .* _v .* w)
    uu = mean( (_u .^ 2) .* w )
    vv = mean( (_v .^ 2) .* w  )
    return abs(1 - uv / sqrt(uu*vv))
end

cosine_distance(u,v;w=nothing) = correlation(u,v; w = w)
cosine_similarity(u,v; w = nothing) = -cosine_distance(u,v; w = w) + 1


"""
A 2D Rotation matrix is a mtrix that rotates a vector in a 2D real space by an angle theta.
For more info: https://en.wikipedia.org/wiki/Rotation_matrix

This function takes the angle `theta` in radians as input and returns a 2D Matrix which will rotate the the vector by angle `theta`.
"""
function rotation_matrix(θ)
	rot_mat = Matrix{Float64}(undef,2,2)
	rot_mat[1,1]  = cos(θ)
	rot_mat[1,2] = -sin(θ)
	rot_mat[2,1] = sin(θ)
	rot_mat[2,2] = cos(θ)

	return rot_mat
end


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

#=Original problem
x′′ + x′ + 2x = 0
x(0)=1
x′(0)=0
=#

#=Esplicit form of equation
x′′ = -x′ - 2x
=#

#=Analytical solution
x(t) = e^(-t/2) (cos(√7 t / 2) + sin(√7 t / 2) / √7)
=#

using DifferentialEquations
using Plots

function ode_fn(dx,x,p,t)
    -dx -2.0 * x
end

an_sol(t) = exp(-t/2.0) *
            (cos(sqrt(7.0) * t / 2.0) + sin(sqrt(7.0) * t / 2.0)/sqrt(7.0))

t_begin=0.0
t_end=12.0
tspan = (t_begin,t_end)
x_init=1.0
dxdt_init=0.0

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

plot(num_sol.t, an_sol.(num_sol.t),
    linewidth=2, ls=:dash,
    title="ODE 2nd order IVP solved by D.E. package",
    xaxis="t", yaxis="x",
    label="analytical",
    legend=true)
plot!(num_sol.t, x_num_sol,
    linewidth=1,
    label="numerical")


## The birthday paradox in theory and in practice.

## Let's simulate the classic birthday paradox.
# In this simulation, we will draw birthdays at random for a group
# of N people, and see if there are any two birthdays that
# are the same.

"""
N is the number of people

This function returns true if two people had the same birthday.
"""
function simple_birthday_simulation(N::Int)
    birthdays = rand(1:365, N)
    # so each birthday is an integer from 1 to 365.
    # we need to check if all of the birthdays are unique
    return !allunique(birthdays) # ! reverses true -> false, false -> true
end

simple_birthday_simulation(20)

## Let's look at what happens as we vary the number of people.
ntrials = 100
prob_dup_birthday = zeros(0) # this is an empty array
for people=5:40
    numdup = 0
    for t=1:ntrials
        numdup += simple_birthday_simulation(people)
    end
    push!(prob_dup_birthday, numdup/ntrials)
end
# make a quick people - by - prob table
display([5:40 prob_dup_birthday])
# but also make a plot
using Plots
plot(5:40, prob_dup_birthday)
xlabel!("People")
ylabel!("Prob. of shared birthday")

## Some ideas to improve
# IDEA -- add confidence intervals
# IDEA -- add "exact" curve based on prob.

## Let's use real data.
using DelimitedFiles
birthdays = readdlm("1-julia-intro/birthday.txt")
plot(birthdays[:,2])

## First step, let's simulate a random birthday from data.
using StatsBase
bday_weights = fweights(Int.(birthdays[:,2]))
sample(bday_weights)

## Let's do the same demo
sample_bdays = [sample(fweights([2,4,3,1])) for i=1:1000]
sum(sample_bdays .== 4)

## Let's implement the sampling ourselves too...
"""
N is the number of people
data is an array of length number of days with the number of people
that were born on that day.

Internally, what this does is convert data to a cum. prob. distribution
and then we use a random [0,1] sample to find where in the CDF we fall.

The idea of how this works is:
give probs [0.2 0.4 0.3 0.1] (i.e. we have 4 birthdays)
then we are going to make an array
[0.2, 0.6, 0.9, 1.0]
and then we are going to guess a random [0,1] variable
and see which bin it falls into.
if the rv. is less than 0.2, we will call it birthday 1
if the rv. is less between 0.2, 0.6, we will call it birthday 2
if the rv. is less between 0.6, 0.9, we will call it birthday 3
if the rv. is less between 0.9, 1, we will call it birthday 4
but this will work for even larger lists too.
"""
function sample_birthdays(N::Int, data)
    probs = data/sum(data)
    cumdist = cumsum(probs)
    bdays = zeros(Int, N)
    for i=1:N
        rv = rand()
        for j=1:length(cumdist)
            if rv <= cumdist[j]
                bdays[i] = j
                break # need to stop when we find the birthday
            end
        end
    end
    return bdays
end
bdays = sample_birthdays(1000, [2,4,3,1])
# Let's check their frequency
sum(bdays .== 3) # count the number of "2" birthdays in our test
## IDEA - use binary search to find the right birthday faster.

## Let's simulate the data-based shared birthday probability
function data_birthday_simulation(N::Int, data)
    birthdays = sample_birthdays(N, data)
    # so each birthday is an integer from 1 to 365.
    # we need to check if all of the birthdays are unique
    return !allunique(birthdays) # ! reverses true -> false, false -> true
end

data_birthday_simulation(20, Int.(birthdays[:,2]))

ntrials = 100
prob_dup_birthday = zeros(0) # this is an empty array
for people=5:40
    numdup = 0
    for t=1:ntrials
        numdup += data_birthday_simulation(people, Int.(birthdays[:,2]))
    end
    push!(prob_dup_birthday, numdup/ntrials)
end
# make a quick people - by - prob table
display([5:40 prob_dup_birthday])
# but also make a plot
using Plots
plot(5:40, prob_dup_birthday)
xlabel!("People")
ylabel!("Prob. of shared birthday based on data")

## TODO Do a test to show that this curve is higher than the uniform
# case.

## Use Julia's sampler
function data_birthday_simulation_julia(N::Int, data)
    w = fweights(data)
    birthdays = [sample(w) for i=1:N]
    # so each birthday is an integer from 1 to 365.
    # we need to check if all of the birthdays are unique
    return !allunique(birthdays) # ! reverses true -> false, false -> true
end
ntrials = 100
prob_dup_birthday = zeros(0) # this is an empty array
for people=5:40
    numdup = 0
    for t=1:ntrials
        numdup += data_birthday_simulation_julia(people, Int.(birthdays[:,2]))
    end
    push!(prob_dup_birthday, numdup/ntrials)
end
# make a quick people - by - prob table
display([5:40 prob_dup_birthday])
# but also make a plot
using Plots
plot(5:40, prob_dup_birthday)
xlabel!("People")
ylabel!("Prob. of shared birthday based on data")


"""
    lis_length(str_1::String, str_2::String)

The longest increasing subsequence (LIS) algorithm is dedicated to finding the longest
increasing subsequence for a given array.  This subsequence does not have to be contiguous
or unique.

    1. Figure out the number of possible different subsequences of a string with length `m` and `n`.
    2. Recontracted `for-loops` for defining the outer layer of the storing matrix `C`.
    3. Checking if `str_1[i] == str_2[j]` and updating the count in  matrix `C`.
    4.Return the latest element of `C[m,n]`.

For more information see: [https://en.wikipedia.org/wiki/Longest_increasing_subsequence](https://en.wikipedia.org/wiki/Longest_increasing_subsequence)

# Arguments
- `array::Array{Int64,1}`: array of integers unsortetd or sorted

# Examples
```julia-repl
julia> import ClassicAlgorithmsCollections
julia> arr = [10, 22, 9, 33, 21, 50, 41, 60]
julia> ClassicAlgorithmsCollections.lis_length(arr)
4
```
"""
function lis_length(array::Array{Int64,1})
    n = length(array)
    list = fill(1, n)
    for i in 2:n
        for j in 1:i
            if array[i] > array[j] && list[i] < list[j] + 1
                list[i] = list[j] + 1
            end
        end
    end

    result = 0
    for i in 1:n
        if list[i] > result
            result = list[i]
        end
    end
    return result
end


"""
Sieve of Eratosthenes is an algorithm for finding all the primes upto a limit `n`.

Reference:
-https://en.wikipedia.org/wiki/Sieve_of_Eratosthenes
"""
function eratosthenes(n)
    primes = fill(true,n)
    primes[1] = false
    for p = 2:n
        primes[p] || continue
        for i = 2:div(n,p)
            primes[p*i] = false
        end
    end
    findall(primes)
end


## Before we got started
# I added the Images package via
# typing "]" then "add Images"
# alternatively, we could use
# using Pkg; Pkg.add("Images")

## Let's try and make Wikipedia's Julia set figure.
using Images
using Plots

## It says the seed coordinates are:
seeds = (-0.512511498387847167, 0.521295573094847167)

## Let's work with the Julia set.
# What we need to do is to check if, when starting
# from the complex number z=0, and iterating the map
# f(z) = z^2 + c
# for a large number of times (say 256)
# if the value of abs(f(z)) ever goes above 2,
# then we stop when that happens and return the value.
c = 0.25
# if we start at z = 0, then we test if z=0 is in the set.
z = 0.0+0.0im # this is how to make an imaginary number
f(z) = z^2 + c
@show f(z)
@show f(f(z))
@show f(f(f(z)))

##
zi = z
for i=1:10
    global zi
    zi = f(zi)
    @show zi
end
##
"""
`julia_check` is a subroutine useful for working with
the Julia set. We are going take as input two values:
z0 and c. The goal is to test whether or not the value
z0 seems to be in the Julia set for
    f(z) = z^2 + c
by starting from z0 = the input z0 and iterating
z1 = f(z0)
z2 = f(z1)
...
and so on.
Now, if at any point, we find abs(z) >= 2, then we know
that the point is NOT in the Julia set. In which case,
we return the iteration number where we detect this.
We run this for maxiter steps.
"""
function julia_check(z0, c; maxiter::Int=256)
    z = z0
    for i=1:maxiter
        z = z^2 + c
        if abs(z) >= 2
            return i
        end
    end
    return maxiter
end
julia_check(-1, 0.25)

## Let's look at starting positions z0 that range
## from [-1.75 to 1.75] in the real component
## and [-1.75 to 1.75] in the complex component
npts = 10
Z = zeros(npoints,npoints) # this will represent our Images
real_values = range(start=-1.75,stop=1.75,length=npts)
complex_values = range(start=-1.75,stop=1.75,length=npts)
for i=1:npts
    for j=1:npts
        z0 = real_values[i] + complex_values[j]*1.0im
        Z[j,i] = julia_check(z0, 0.25)
    end
end
heatmap(Z)

## The fix!
npts = 10
Z = zeros(npts,npts) # this will represent our Images
real_values = range(-1.75,stop=1.75,length=npts)
complex_values = range(-1.75,stop=1.75,length=npts)
for i=1:npts
    for j=1:npts
        z0 = real_values[i] + complex_values[j]*1.0im
        Z[j,i] = julia_check(z0, 0.25)
    end
end
heatmap(Z)

## Let's turn this into a function!
function julia_set(real_values, complex_values)
    Z = zeros(length(complex_values), length(real_values))
    for i=1:length(real_values)
        for j=1:length(complex_values)
            z0 = real_values[i] + complex_values[j]*1.0im
            Z[j,i] = julia_check(z0, 0.25)
        end
    end
    return Z
end
npts = 500
real_values = range(-1.75,stop=1.75,length=npts)
complex_values = range(-1.75,stop=1.75,length=npts)
heatmap(julia_set(real_values, complex_values))
##
function julia_set(real_values, complex_values; c=0.25)
    Z = zeros(length(complex_values), length(real_values))
    for i=1:length(real_values)
        for j=1:length(complex_values)
            z0 = real_values[i] + complex_values[j]*1.0im
            Z[j,i] = julia_check(z0, c)
        end
    end
    return Z
end
npts = 500
real_values = range(-1.75,stop=1.75,length=npts)
complex_values = range(-1.75,stop=1.75,length=npts)
heatmap(julia_set(real_values, complex_values; c = seeds[1] + seeds[2]*1.0im))
## Let's increase the resolution.
npts = 1000
real_values = range(-1.75,stop=1.75,length=npts)
complex_values = range(-1.75,stop=1.75,length=npts)
Z = julia_set(real_values, complex_values; c = seeds[1] + seeds[2]*1.0im)
## And then try and make our plot.
heatmap(log10.(Z))
## TODO or IDEA - Play around the with the colors to see if you find something you like better
## Show this like an image.
image = Gray.(Z/maximum(Z))
image

## to save this picture, we can use
using FileIO
using ImageMagick
save("juliaset.png", Gray.(Z/maximum(Z)))


##########################################################
#FEniCS tutorial demo program: Poisson equation with Dirichlet conditions.
#Test problem is chosen to give an exact solution at all nodes of the mesh.
#  -Laplace(u) = f    in the unit square
#            u = u_D  on the boundary
#  u_D = 1 + x^2 + 2y^2
#    f = -6
###########################################################

module ft01

using FenicsPy

# Create mesh and define function space
mesh = UnitSquareMesh(8, 8)
V = FunctionSpace(mesh, "P", 1)

# Define boundary condition
u_D = Expression("1 + x[0]*x[0] + 2*x[1]*x[1]", degree=2)

bc = DirichletBC(V, u_D, "on_boundary")

# Define variational problem
u = TrialFunction(V)
v = TestFunction(V)
f = Constant(-6.0)
a = dot(grad(u), grad(v))*dx
L = f*v*dx

# Compute solution
u = FeFunction(V)
solve(a == L, u, bc)

# Save solution to file in VTK format
vtkfile = File("poisson/solution.pvd")
vtkfile << u

# Compute error in L2 norm
error_L2 = errornorm(u_D, u, "L2")

# Compute maximum error at vertices
vertex_values_u_D = u_D.compute_vertex_values(mesh)
vertex_values_u = u.compute_vertex_values(mesh)
error_max = max(abs.(vertex_values_u_D - vertex_values_u)...)

# Print errors
println("error_L2  =", error_L2)
println("error_max =", error_max)

# Plot solution and mesh
#plot(u)
#plot(mesh)

end # module ft01


using FractalTools
using Plots

function example_attractor_of_ifs(N_iter)
    # An example of one dimensional ifs
    A1 = reshape([1/2],1,1)
    b1 = [0]
    A2 = reshape([1/2],1,1)
    b2 = [1/2]

    w1 = Transformation(A1,b1)
    w2 = Transformation(A2,b2)

    w = [w1, w2]
    ifs = IFS(w)
    initset = [[0.1]]
    atr = attractor(ifs, initset; alg=RandAlg(), numiter=N_iter,allocated=true)
    attrset = vcat(atr.set...)
    t = ones(length(attrset))
    return atr, attrset, t
end

atr, attrset, t = example_attractor_of_ifs(10)
p1 = scatter(t, attrset, title="Attractor of the IFS", label="Attractor Points", markersize = 4)

atr, attrset, t = example_attractor_of_ifs(100)
p2 = scatter(t, attrset, title="Attractor of the IFS", label="Attractor Points", markersize = 4)

atr, attrset, t = example_attractor_of_ifs(10000)
p3 = scatter(t, attrset, title="Attractor of the IFS", label="Attractor Points", markersize = 4)

plot(p1, p2, p3, layout=(1,3))
xlabel!("x")
ylabel!("z")
xlims!((0.9,1.1))
# savefig("myplot.png")

# An example of atr with Channel
atr = attractor(ifs1, initset; alg=RandAlg(), numiter=100, allocated=false)
collect(atr.set)
take!(atr.generator)
# An example of atr with randalg_sequential_generator
# generator = randalg_sequential_generator(ifs1.ws, ifs1.probs)
# take!(generator)

# An example of two dimensional ifs
A = [1/2 0;0 1/2]
b1 = [0; 0]
b2 = [0; 1/2]
b3 = [1/2; 1/2]
b4 = [1/2; 0]

w1 = Transformation(A,b1)
w2 = Transformation(A,b2)
w3 = Transformation(A,b3)
w4 = Transformation(A,b4)

w = [w1, w2, w3, w4]
ifs2 = IFS(w)
ifs2.ws
ifs2.probs

# An example of different probabilities

p = [0.2, 0.4, 0.1, 0.3]

ifs3 = IFS(w,p)
ifs3.ws
ifs3.probs


@testset "l2inner" begin
    ndim = 10
    a = rand(Float,ndim)
    b = rand(Float,ndim)
    ca = rand(Complex{Float},ndim)
    cb = rand(Complex{Float},ndim)
    @test a⋅b ≈ VectorDataUtils.l2inner(a,b)
    @test a⋅cb ≈ VectorDataUtils.l2inner(a,cb)
    @test ca⋅b ≈ VectorDataUtils.l2inner(ca,b)
    @test ca⋅cb ≈ VectorDataUtils.l2inner(ca,cb)
    @test a⋅a ≈ VectorDataUtils.l2inner(a)
    @test a⋅a ≈ VectorDataUtils.sql2norm(a)
    @test sqrt(a⋅a) ≈ VectorDataUtils.l2norm(a)
end

@testset "l2dist" begin
    ndim = 10
    a = rand(Float,ndim)
    b = rand(Float,ndim)
    @test (a-b)⋅(a-b) ≈ VectorDataUtils.sql2dist(a,b)
    @test sqrt(sum(abs2,a-b)) ≈ VectorDataUtils.l2dist(a,b)
end

@testset "lpnorms" begin
    ndim = 10
    a = rand(Float,ndim)
    @test sum(abs.(a)) ≈ VectorDataUtils.l1norm(a)
end

@testset "lpdist" begin
    ndim = 10
    a = rand(Float,ndim)
    b = rand(Float,ndim)
    @test sum(abs.(a-b)) ≈ VectorDataUtils.l1dist(a,b)
end


# Root finder functions for 1d func
# Newton's Method, Linear Approximation
# Note that you can also find root by applying optimizer on an Error function
# ϵ(x,...)

# Newton's Method
# f1 is δf╱δx
function solve_Newton(f,f1,par;nlim = 1e4,ϵ=1e-3)
    for i in 1:nlim
        par1 = par - f(par)/f1(par)
        if ϵ>abs(par1 -par)
            return(par1)
        end
        par=par1
    end
    println("Warning! unconverged after" * string(nlim) *"Iterations.")
    return(par1)
end

# Linear Approximation
function solve_Linear_Approx(f,par1,par2;nlim = 1e4,ϵ=1e-3)
    #must follow :x1 * x2<0
    #             par1<par2
    for i in 1:nlim
        x1 = f(par1)
        x2 = f(par2)
        global par3 = par1+(par2 - par1)*abs(x1/(x1-x2))
        if abs(par3-par1)<ϵ
            if abs(par3 - par2)<ϵ
                return par3
            end
        else
            if (par3*par1)>0
                par1=par3
            else (par3*par2)>0
                par2=par3
            end
        end
    end
    println("Warning! unconverged after"*string(nlim)*"iterations.")
    return par3
end

#----------------------------------------------------------------
using Plots
x = 1:.1:4
y = sin.(x)
plot(x,y)
plot!(x,zeros(length(x)))

f(x) = sin(x)
f1(x) = cos(x)

solve_Newton(f,f1,3)

solve_Linear_Approx(f,3,4)
#----------------------------------------------------------------


"""
    ceil_val(x)

Finds the ceiling of x as an Integer

# Example
```julia
ceil_val(1.3)   # 2.0
ceil_val(2.0)   # returns 2.0
ceil_val(-1.5)  #returns -1.0
````

# Reference
- https://en.wikipedia.org/wiki/Floor_and_ceiling_functions

Contributed By:- [Ashwani Rathee](https://github.com/ashwani-rathee)
"""
function ceil_val(x)
    return x -trunc(x) <= 0 ? trunc(x) : trunc(x)+1
end

"""
    floor_val(x)

Finds the floor of x as an Integer

# Example
```julia
floor_val(1.3)  # 1
floor_val(2.0)  # returns 2.0
floor_val(-1.7) # returns -2.0
````

# Reference
- https://en.wikipedia.org/wiki/Floor_and_ceiling_functions

Contributed By:- [Ashwani Rathee](https://github.com/ashwani-rathee)
"""
function floor_val(x)
    return x -trunc(x) >= 0 ? trunc(x) : trunc(x)-1
end


# Number of iterations Euclidean Algorithm needs for gcd(a, b)
function gcd_iters(a, b)
    iters = 0
    while b != 0
        iters += 1
        a, b = b, a % b
    end
    return iters
end

# Number of bits needed to represent some non-negative n
function num_bits(n)
    ct = 1
    n >>= 1
    while n != 0
        n >>= 1
        ct += 1
    end
    return ct
end

# Test num_bits
num_bits_tests = [(0, 1), (1, 1), (2, 2), (3, 2), (4, 3), (5, 3), (6, 3), (7, 3), (8, 4)]
for (num, ans) in num_bits_tests
    @assert num_bits(num) == ans
end

small_numbers = collect(0:2^12-1)
fibs = [0, 1] # Fibonacci numbers
for i in 1:90
    push!(fibs, fibs[length(fibs) - 1] + fibs[length(fibs)])
end
two_powers = [2 ^ x for x in 1 : 60]
mersenne_numbers = [x - 1 for x in two_powers]

operands_of_interest = Set(vcat(small_numbers, fibs, two_powers, mersenne_numbers))

# Verify that for all operands of interest, num iters is <= 1 + num_bits(y) ÷ log2_of_φ (note floor divison)
log2_of_φ = log2(MathConstants.golden)
for x in operands_of_interest, y in operands_of_interest
# for x in 0:2^16-1, y in 0:2^16-1
    iters = gcd_iters(x, y)
    if 1 + num_bits(y) ÷ log2_of_φ < iters
        println("gcd($(x), $(y)) takes $(iters) iters (max bits: $(max_bits), ratio: $(iters / max_bits))")
    end
    @assert iters <= 1 + num_bits(y) ÷ log2_of_φ
end
println("Done")

#==
Operands where (# of iterations)/(max_bits) = 1.5 (none have a higher ratio, up through 14 bits)
Note that all are sequential fibonacci #s
gcd(2, 3) takes 3 iters (max bits: 2)
gcd(8, 13) takes 6 iters (max bits: 4)
gcd(34, 55) takes 9 iters (max bits: 6)
gcd(144, 233) takes 12 iters (max bits: 8)
gcd(610, 987) takes 15 iters (max bits: 10)

https://stackoverflow.com/questions/9060816/running-time-of-euclids-gcd-algorithm
y = second arg
b = # of bits in second arg
bound is:
1 + log(φ, y)
1 + log(φ, 2^b - 1)
1 + log(2, 2^b - 1)/log(2, φ)
1 + b/log(2, φ)

experimentally verified for all numbers representable in 16 bits

==#


struct BracketingInterval
    x_lo
    x_mid
    x_hi
    f_lo
    f_mid
    f_hi
    iter
    success::Bool
end

function Base.show(io::IO, bracket::BracketingInterval)
    if bracket.success
        print("Successful seach")
    else
        print("Failed seach")
    end
    print(" with $(bracket.iter)-iterations\n")
    println("\tx\tf(x)")
    println( "lo:\t$(bracket.x_lo)\t$(bracket.f_lo)" )
    println( "mid:\t$(bracket.x_mid)\t$(bracket.f_mid)" )
    println( "hi:\t$(bracket.x_hi)\t$(bracket.f_hi)" )
end

function bracket_min(
    fcn,
    x_lo,
    x_hi;
    step = abs(x_hi-x_lo) * 1e-2,
    step_factor = 1.61,
    max_iter = 1000)

    f_x_lo = fcn(x_lo)
    f_x_hi = fcn(x_hi)

    if f_x_lo < f_x_hi
        # swap the orders
        x_lo, x_hi = x_hi, x_lo
        f_x_lo, f_x_hi = f_x_hi, f_x_lo
        step = -step
    end

    iter = 1
    while iter <= max_iter
        x_next = x_hi + step
        f_next = fcn(x_next)
        if f_next > f_x_hi
            # bracketed interval found
            return BracketingInterval(x_lo, x_hi, x_next, f_x_lo, f_x_hi, f_next, iter, true)
        end

        x_lo, x_hi = x_hi, x_next
        f_x_lo, f_x_hi = f_x_hi, f_next

        step *= step_factor
        iter += 1
    end
    println("Failed to find a bracketing interval in the prescribed number of iterations")
    return BracketingInterval(x_lo, x_hi, x_next, f_x_lo, f_x_hi, f_next, iter, false)

end

struct GoldenSectionSearchStatistics
    x_sol
    y_sol
    iter
end

function golden_section_search(fcn, x_lo, x_hi; max_iter = 100, x_tol = 1e-10)

    rho = 0.5 * (sqrt(5)-1)
    if x_lo > x_hi
        # swap
        x_lo, x_hi = x_hi, x_lo
    end
    a = x_lo
    b = x_hi

    c_fcn(lo, hi) = rho * lo + (1 - rho) * hi
    d_fcn(lo, hi) = (1 - rho) * lo + rho * hi

    y_a = fcn(a)
    y_b = fcn(b)

    c = c_fcn(a, b)
    d = d_fcn(a, b)

    y_c = fcn(c)
    y_d = fcn(d)

    x_sol = NaN
    y_sol = NaN
    iter = 0
    for outer iter = 1:max_iter

        if y_c < y_d
            # choose (a, c, d)
            a, d, b = a, c, d
            y_a, y_d, y_b = y_a, y_c, y_d
            c = c_fcn(a, b)
            y_c = fcn(c)
        else
            # choose (c, d, b)
            a, c, b = c, d, b
            y_a, y_c, y_b = y_c, y_d, y_b
            d = d_fcn(a, b)
            y_d = fcn(d)
        end

        if abs(b - a) <= x_tol
            (x_sol, y_sol) = (y_c < y_d) ? (c, y_c) : (d, y_d)
            break
        end

    end
    if iter == max_iter
        (x_sol, y_sol) = (y_c < y_d) ? (c, y_c) : (d, y_d)
    end

    return GoldenSectionSearchStatistics(x_sol, y_sol, iter)

end


# p12.jl - accuracy of Chebyshev spectral differentiation
#         (compare p7.jl)

# Compute derivatives for various values of N:
Nmax = 50; allN = 6:2:Nmax; E = zeros(4,Nmax);
for N = 1:Nmax
    (D,x) = cheb(N);
    v = @. abs(x)^3; vprime = @. 3*x*abs(x);   # 3rd deriv in BV
    E[1,N] = norm(D*v-vprime,Inf);
    v = @. exp(-x^(-2)); vprime = @. 2*v/x^3;  # C-infinity
    E[2,N] = norm(D*v-vprime,Inf);
    v = @. 1/(1+x^2); vprime = @. -2*x*v^2;    # analytic in [-1,1]
    E[3,N] = norm(D*v-vprime,Inf);
    v = x.^10; vprime = 10*x.^9;               # polynomial
    E[4,N] = norm(D*v-vprime,Inf);
end

# Plot results:
titles = [L"|x|^3",L"\exp(-x^2)",L"1/(1+x^2)",L"x^{10}"];  clf();
for iplot = 1:4
    subplot(2,2,iplot);
    semilogy(1:Nmax,E[iplot,:],".-",markersize=6);
    axis([0,Nmax,1e-16,1e3]); grid(true);
    xticks(0:10:Nmax); yticks(10.0.^(-15:5:0));
    title(titles[iplot]);
    iplot > 2 ? xlabel("N") : nothing;
    iplot%2 > 0? ylabel("error") : nothing;
end


#' ## Run this cell to load the graphics packages
#+ results="hidden"
using Plots
#gr()
#plotly()
pyplot()
#+

#' ## Adding a function parameter
#' In the last notebook, we saw an example of adding **parameters** to
#' functions. These are values that control the behavior of a function.
#' Let's look at that in some more detail.
#' Let's go back to our original version of the σ function:
#+ results="hidden"
sigma(x) = 1/(1+exp(-x))
#+
#' Instead of working with a single function, we can work with a whole class
#' (set) of functions that look similar but differ in the value of a
#' **parameter**. Let's make a new function that uses the previous $\sigma$
#' function, but also has a parameter, $w$. Mathematically, we could write

#' $$f_w(x) = f(x; w) = \sigma(w \, x).$$

#' (Here, $w$ and $x$ are multiplied in the argument of $\sigma$; we could
#' write $w \times x$ or $w * x$, or even $w \cdot x$, but usually the symbols
#' are not written.)

#' Mathematically speaking, we can think of $f_w$ as a different function for
#' each different value of the parameter $w$.

#' In Julia, we write this as follows:
#+ results="hidden"
f(x, w) = sigma(w * x)
#+
#' Note that Julia just treats parameters as additional *arguments* to the
#' function; the function `f` has two arguments, the value of `x` and the
#' value of `w` that we want to use.

#' We can now investigate the effect of $w$ interactively. To do so, we need
#' a way of writing in Julia "the function of one variable $x$ that we obtain
#' when we fix the value of $w$". We write this as an "anonymous function", as
#' we saw in the notebook on functions:
#'
#'     x -> f(x, w)
#'
#' We can read this as "the function that maps $x$ to the value of $f(x, w)$,
#' for a value of $w$ that was previously given".

#' Now we are ready to draw the function. For each plot, we *fix* a value of
#' the parameter $w$ and draw the resulting function as a function of $x$.
#' We can interactively modify the value of $w$, and plot the new function that
#' comes out:
#' ## Try manipulating `w` between -2 and 2
w = -1
plot(x->f(x, w), -5, 5, ylims=(0,1), label="sigmoid")
plot!(x->(x>0), -5,5, label="Square Wave")

#' #### Exercise 1
#'
#' Try writing your own function that takes a parameter. Start by copying and
#' executing
#'
square(x) = x^2
#'
#' Then use `square` to declare a new function `square_and_scale` that takes
#' two inputs, `a` and `x` such that
#'
#' $$\mathrm{square\_and\_scale}(x; a) := a \cdot x^2$$

#' ####
function square_and_scale(x, a)
    return a*x^2
end

#' We can write some tests as follows, using the approximate equality operator,
# `≈`, written as `\approx<TAB>`:
square_and_scale(1, 1) ≈ 1
square_and_scale(1, 2) ≈ 2
square_and_scale(2, 1) ≈ 4
square_and_scale(2, 2) ≈ 8

# #### Exercise 2

#' Once you have declared `square_and_scale`, uncomment the code below and see
#' how the parameter `a` scales the function `square` :

x = -10:10
a = 5.0 # Try manipulating a here

plot(x, square.(x), label="x^2")
plot!(x, square_and_scale.(x, a), ls=:dash, label="ax^2")

#' ## Fitting a function to data
#' As we saw in the previous notebook, what we would like to do is use the
#' fact that we now have a parameter in our function in order to do something
#' useful! Namely, we want to model data with it.

#' So suppose that we are given a single data point $(x_0, y_0) = (2, 0.8)$.
#' We can try to "fit" the function $f_w$ by adjusting the parameter $w$ until
#' the function passes through the data.
#'
#' **Game**: Change `w` until the graph of the function hits the data point.
#' Which value of $w$ does that correspond to?
x0, y0 = 2, 0.8

#' f(x, w) = sigma(w * x)
w = 0.7 # try mainipulatoing `w` between -2 and 2
plot(x->f(x, w), -5, 5, ylims=(0, 1), label="f")
scatter!([x0], [y0], label="data")
#' ## Quantifying how far we are from the goal: the *loss function*
#' We can see visually when the graph of the function passes through the data
#' point. But the goal is to be able to automate this process so that the
#' computer can do it unaided.
#'
#' So we will need a more precise way of deciding and quantifying
#' (i.e. measuring with a number) *how far away we are from the goal*;
#' here, the goal means hitting the data point with the function.

#' #### Exercise 3
#' Can you think of a way of measuring how far away our function is from
#' the data point?

#' ### Defining the loss function
#' We need to measure how far away the curve is from the data point when we
#' choose a particular value of $w$.
#' One way to do this is by finding the vertical distance $d$ from the curve
#' to the data point.
#'
#' Instead of just taking the distance, it is common to take the *square* of
#' the distance, $d^2$.
#'
#' Since we are taking the vertical distance, we need the distance at the given
#' value of $x_0$ where the data point lies. For a given value of the parameter
#' $w$, the height of the point on the curve with that value of $x_0$ is
#' $f(x_0, w)$.
#'
#' So we take
#'
#' $$d := y_0 - f(x_0, w)$$
#'
#' and
#'
#' $$d^2 = [y_0 - f(x_0, w)]^2.$$
#'
#' This is our measure of distance. It will change when $w$ changes -- in other
#' words, it is itself a *function of $w$*; we will denote this function by
#' $L(w)$, and call it the **loss function**:
#'
#' $$L(w) := [y_0 - f(x_0, w)]^2.$$
#'
#' So the goal is to find the value $w^*$ of $w$ where the loss function is *least*; in other words, we need to *minimize* the loss function!
#'
#' (Another name for a loss function is a *cost function*.)

#' #### Exercise 4
#'
#' (a) Define the loss function `L(w)` in Julia.
#+ results="hidden"
x0, y0 = 2, 0.8
L(w) = (y0 - f(x0,w))^2
#+

#' (b) Draw the data point and the function `x -> f(x, w)`.
#' Also draw a vertical line from the data point to the function `x -> f(x, w)`.
w = 0.5
pl = plot(x->f(x,w), -5, 5, ylims=(0,1), label="f_w", lw=3)
scatter!(pl, [x0], [y0], label="data")
plot!([x0, x0], [y0, f(x0, w)], label="loss", legend=(0.2,0.9))
#' (c) Make the plot interactive.
#'
#' (d) Add as the plot title the value of the loss function for the current value
#' of $w$.
title!("L(w) = $(round(L(w); sigdigits = 5))")
#'
#' (e) Use the slider to find the value $w^*$ of $w$ for which the loss function
#' reaches its minimum value. What is $w^*$? What is the value of the loss
#' function there, $L(w^*)$?
w = 0.7
pl = plot(x->f(x,w), -5, 5, ylims=(0,1), label="f_w", lw=3)
scatter!(pl, [x0], [y0], label="data")
plot!([x0, x0], [y0, f(x0, w)], label="loss", legend=(0.1,0.9))
title!("L(w) = $(round(L(w); sigdigits = 5))")

#' ## What does the loss function look like?
#' The loss function $L(w)$ tells us how far away the function $f_w$ is from
#' the data when the parameter value is $w$, represented visually as the
#' vertical line in the previous plot. When the data are fixed, this is a
#' function only of the parameter $w$. What does this function look like as
#' a function of $w$? Let's draw it!

#' #### Exercise 5
#' Draw the function $L(w)$ as a function of $w$.

#' #### Solution
pl = plot(L, -2, 2, xlabel="w", ylabel="L(w)", ylims=(0, 0.7), label="L", legend=(0.8,0.9))
#' This graph quantifies how far we are from the data point for a given value
#' of $w$.
#' What features can we see from the graph?
#'
#' Firstly, we see that $L(w)$ is always bigger than $0$, for any value of $w$.
#' This is because we want $L$ to be some kind of measure of *distance*, and
#' distances cannot be negative.
#'
#' Secondly, we see that there is a special value $w^*$ of $w$ where the
#' function $L$ reaches its minimum value. In this particular case, it actually
#' reaches all the way down to $0$!
#' This means that the original function $f$ (the one we manipulated above)
#' passes exactly through the data point $(x_0, y_0)$.

#' #### Exercise 6
#' Draw a zoomed-in version of the graph to find the place $w^*$ where the
#' function hits $0$ more precisely.
pl = plot(L, 0.5, 1, xlabel="w", ylabel="L(w)", ylims=(0, 0.01), label="L", legend=(0.8,0.9))

#' ### A different way of defining the loss function
#' **Why did we use such a complicated function $L$ with those squares inside?**
#' We could instead just have used the absolute distance, instead of the
#' distance squared, using the *absolute value* function, written mathematically
#' as $| \cdot |$, and in Julia as `abs`.

#' #### Exercise 7
#'
#' Define a new loss function, `L_abs`, using the absolute value, and see what
#' it looks like.

#' #### Solution
L_abs(w) = abs(y0 - f(x0,w))

pl = plot(L_abs, 0.5, 1, xlabel="w", ylabel="L_abs(w)", ylims=(0, 0.01), label="L", legend=(0.8,0.9))

#' Now we see why it was previously generally preferred to use squares: using
#' the absolute value gives a cost function that is *not smooth*. This makes it
#' difficult to use methods from calculus to find the minimum. Nonetheless,
#' using non-smooth functions is very common in machine learning nowadays.


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

# Example
```jl
julia> A = [2  3  3
        3  5  4
        3  4  6];

julia> B = [3,4,6];

julia> X,flag,err,M = conGrad(A,B,[0 0 0.9]);

julia> X
3-element Array{Float64,1}:
  2.380760976839332e-6
 -1.220333023839802e-6
  0.9999996142293243

julia> flag
0

julia> err
8.019009497939746e-8

julia> M
229×5 Array{Float64,2}:
   0.0  0.0          0.0          0.9       NaN
   1.0  0.0266764    0.0355685    0.953353    0.0694497
   2.0  0.0154769    0.000266424  0.975767    0.0432903
   3.0  0.0193622    0.00858077   0.976843    0.00924021
   ⋮
 225.0  5.10651e-6  -1.83459e-6   0.999999    1.32762e-6
 226.0  2.29335e-6  -1.29137e-6   1.0         3.08384e-6
 227.0  2.4522e-6   -1.23209e-6   1.0         1.73709e-7
 228.0  2.38076e-6  -1.22033e-6   1.0         8.01901e-8
```
return solusi `X` dengan `flag` bernilai 0 jika metode conjugate gradient
berhasil menemukan solusi dan gagal jika tidak 0.
Serta, matriks `M` yang berisi catatan proses tiap iterasi `[k, X', error]`
"""
function conGrad(A,B,Xawal)
    if ~(isposdef(A) && issymmetric(A))
        error("matriks A harus simetrik definit positif")
    end
    delta = 10^-7;
    maxi = 1000;
    flag = 1;
    X = Xawal[:];
    r = B-A*X
    d = r
    M = [0 X' NaN];
    for k = 1:maxi
        Xlama = X
        rlama = r
        dlama = d
        X = Xlama + ((rlama'rlama)/(dlama'A*dlama))*dlama
        r = rlama - ((rlama'rlama)/(dlama'A*dlama))*A*dlama
        d = r - ((r'r)/(rlama'rlama))*dlama
        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


function solve_euler(timestep::Float64, n::Int64)
    euler_result = Vector{Float64}(undef, n)

    # Setting the initial condition
    euler_result[1] = 1;
    for i = 2:length(euler_result)
        euler_result[i] = euler_result[i-1] - 3.0*euler_result[i-1]*timestep
    end
    return euler_result
end

function check_result(euler_result::Vector{Float64}, threshold::Float64,
                      timestep::Float64)
    is_approx = true

    for i = 1:length(euler_result)
        time = (i - 1)*timestep
        solution = exp(-3*time);
        if (abs(euler_result[i] - solution) > threshold)
            println(euler_result[i], solution)
            is_approx = false
        end
    end

    return is_approx
end

function main()
    timestep = 0.01
    n = 100
    threshold = 0.01

    euler_result = solve_euler(timestep,n)
    is_approx = check_result(euler_result, threshold, timestep)

    println(is_approx)
end

main()


using SymPy

# National Income Model

eq1 =  SymFunction("eq1")
eq2 =  SymFunction("eq2")

Y, C, b, I₀, G₀, a = symbols("Y C b I₀ G₀, a")

eq1 = Eq(Y - C, I₀ + G₀)
eq2 = Eq(-b*Y + C, a)

display(eq1)
display(eq2)

row1 = [1 -1 I₀ + G₀]
row2 = [-b 1 a]

system  = sympy.Matrix((row1, row2))

display(system)

solution = linsolve(system, (Y, C))
display(solution)

# Demand and Supply Model

eq1 = SymFunction("eq1")
eq2 = SymFunction("eq2")
eq3 = SymFunction("eq3")

Qᵈ, Qˢ, a, b, P, f, Y, c, d, g, Pᵢ = symbols("Qᵈ Qˢ a b P f Y c d g Pᵢ")

eq1 = Eq(Qᵈ + b*P, a + f*Y)
eq2 = Eq(Qˢ + d*s, c - g*Pᵢ)
eq3 = Eq(Qᵈ - Qˢ)

row1 = [1 0 b a+f*Y]
row2 = [0 1 -d c-g*Pᵢ]
row3 = [1 -1 0 0]

system = sympy.Matrix(((1, 0, b, a+f*Y), (0, 1, -d, c-g*Pᵢ), (1, -1, 0, 0)))

solution = linsolve(system, (Qᵈ, Qˢ, P))

display(solution)

sympy.init_printing(use_latex = "mathjax")


## Exercise 5-7
## https://benlauwens.github.io/ThinkJulia.jl/latest/images/fig52.svg
## Figure 8. A Koch curve

## The Koch curve is a fractal that looks something like A Koch curve. To draw a Koch curve with length x, all you have to do is

## 1. Draw a Koch curve with length x/3.
## 2. Turn left 60 degrees.
## 3. Draw a Koch curve with length x/3.
## 4. Turn right 120 degrees.
## 5. Draw a Koch curve with length x/3.
## 6. Turn left 60 degrees.
## 7. Draw a Koch curve with length x/3.

## The exception is if x is less than 3: in that case, you can just draw a straight line with length x.

using ThinkJulia

## 1. Write a function called koch that takes a turtle and a length as parameters, and that uses the turtle to draw a Koch curve with the given length.
println("Ans 1: ")

function koch(turtle::Turtle, length)
    if length < 3
        forward(turtle, length)
        return
    end

    koch(turtle, length/3)
    turn(turtle, 60)
    koch(turtle, length/3)
    turn(turtle, -120)
    koch(turtle, length/3)
    turn(turtle, 60)
    koch(turtle, length/3)
end

turtle = Turtle()
# @svg begin
#     koch(turtle, 100)
# end

## 2. Write a function called snowflake that draws three Koch curves to make the outline of a snowflake.
println("Ans 2: ")

function snowflake(turtle, length)
    koch(turtle, length)
    turn(turtle, -60)
    koch(turtle, length)
    turn(turtle, -60)
    koch(turtle, length)
    turn(turtle, -60)
    koch(turtle, length)
    turn(turtle, -60)
    koch(turtle, length)
    turn(turtle, -60)
    koch(turtle, length)
    turn(turtle, -60)
end

# @svg begin
#     snowflake(turtle, 100)
# end

## 3. The Koch curve can be generalized in several ways. See https://en.wikipedia.org/wiki/Koch_snowflake for examples and implement your favorite.
println("Ans 3: ")

function koch_square(turtle::Turtle, length)
    if length < 3
        forward(turtle, length)
        return
    end

    koch_square(turtle, length/3)
    turn(turtle, 90)
    koch_square(turtle, length/3)
    turn(turtle, -90)
    koch_square(turtle, length/3)
    turn(turtle, -90)
    koch_square(turtle, length/3)
    turn(turtle, 90)
    koch_square(turtle, length/3)
    turn(turtle, 90)
end

@svg begin
    koch_square(turtle, 60)
end

println("End.")


#=
The sequence of triangle numbers is generated by adding the natural numbers. So the 7th triangle number would be 1 + 2 + 3 + 4 + 5 + 6 + 7 = 28. The first ten terms would be:

1, 3, 6, 10, 15, 21, 28, 36, 45, 55, ...

Let us list the factors of the first seven triangle numbers:

 1: 1
 3: 1,3
 6: 1,2,3,6
10: 1,2,5,10
15: 1,3,5,15
21: 1,3,7,21
28: 1,2,4,7,14,28
We can see that 28 is the first triangle number to have over five divisors.

What is the value of the first triangle number to have over five hundred divisors?
=#

function num_divisors(n)
  count,i = 2,2 # accounts for 'n' and '1'
  while (i^2 <= n)
    if n%i==0
      count += 2
    end
    i += 1
  end
  count
end

function triangle_number(n)
  return convert(Int64,(1/2)*n*(n+1))
end

function calc()
  i = 2
  while num_divisors(triangle_number(i)) < 500
    i += 1
  end
  triangle_number(i)
end
@time println(calc())


#=
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
=#
function estimate_pi(;number_of_decimal::Int64 = 3)
    in = 0
    out = 0
    pi_estimated = 0.0
    n_points = 0
    while round(pi_estimated; digits = number_of_decimal) != round(pi; digits = number_of_decimal)
        x, y = rand(2)
        if (x^2 + y^2) <= 1
            in = in + 1
        end
        n_points = n_points + 1
        pi_estimated = in/n_points * 4
    end
    return pi_estimated
end


# This script solves a Fredholm equation of the first type

println('\n', " "^4, "> Loading the packages...")

using LinearAlgebra # Norm
using Optim # Optimization
using Plots # Plotting
using Printf # Formatted printing
using QuadGK # Gauss–Kronrod integration

# Use the GR backend for plots
gr()

# Change the default font for plots
default(fontfamily="Computer Modern", dpi=300, legend=:outerright)

println(" "^4, "> Computing the solution (Test)...")

# Define the kernel (Test)
K(x, s) = exp(s * x)

# Define the right part of the equation (Test)
u(x) = (exp(x + 1) - 1) / (x + 1)

# Set the integration intervals
a = 0
b = 1
c = 0
d = 1

# Set the number of nodes
n = 100

# Calculate the step
h = (b - a) / n

# Calculate the nodes for the s argument
s = [ a + 0.5 * h + (i - 1) * h for i in 1:n ]

# Set the initial value of the regularization parameter
α = 0.001

# Prepare a range of nodes for the residual calculation
xr = range(c, d; length=1000)

# Compute the residual of the solution with the specified number of nodes and the regularization parameter
function calculate(n::Int, α::Float64)::Tuple{Vector{Float64},Float64}
    # Calculate the step
    h = (b - a) / n

    # Calculate the nodes for the s argument
    s = [ a + 0.5 * h + (i - 1) * h for i in 1:n ]

    # Calculate the nodes for the t argument
    t = copy(s)

    # Compute the g vector
    g = [ quadgk(x -> K(x, s[i]) * u(x), c, d; rtol=1e-8)[1] for i in 1:n ]

    # Prepare a matrix for the computation
    Bα = Matrix{Float64}(undef, n, n)

    # Compute the Bα matrix
    for i in 1:n, j in 1:n
        Bα[i, j] = quadgk(x -> K(x, s[i]) * K(x, t[j]), c, d; rtol=1e-8)[1] * h + (i == j ? α : 0)
    end

    # Compute the solution
    z = Symmetric(Bα) \ g

    # Calculate the residual
    r = norm([ sum(K.(x, s) .* z .* h) for x in xr ] .- u.(xr))

    return z, r
end

# Compute the residual of the solution with the specified regularization parameter (optimization only)
function residual(θ::Vector{Float64})::Float64
    return calculate(100, θ[1])[2]
end

# Optimize over the regularization parameter
res = Optim.optimize(
    residual,
    [α,],
    LBFGS(),
    Optim.Options(
        show_trace=false,
        extended_trace=true,
        store_trace=true,
    );
    inplace=false,
)

# Save the trace and the results
open("test_optimization.trace", "w") do io
    println(io, res.trace)
    println(
        io,
        " * Parameters:\n",
        " "^4, "α = $(res.minimum)\n",
    )
    show(io, res)
end

# Print the residual
println(
    '\n',
    " "^6, "Regularization parameter: ", res.minimizer[1], '\n',
    " "^6, "Residual: ", res.minimum, '\n',
)

println(" "^4, "> Plotting the solution (Test)...")

# Recalculate the solution
z = calculate(n, res.minimizer[1])

# Plot the solution
p = plot(s, z; label="Приближенное решение", xlabel="s", ylabel="z(s)");

# Add the solution to the plot
plot!(p, x -> exp(x), a, b; label="Точное решение");

# Save the figure
savefig(p, "$(@__DIR__)/../plots/test.pdf")

println('\n', " "^6, "* The figure `test.pdf` is saved. *", '\n')

println(" "^4, "> Plotting the residuals against the number of nodes (Test)...")

# Define the `n`s to plot against
ns = [ i for i in 10:300 ]

# Calculate the residuals for these `n`s
rs = @. getindex(calculate(ns, res.minimizer[1]), 2)

# Make a plot of the residual against `n`
p = plot(ns, rs; label="", xlabel="n", ylabel="r");

# Save the figure
savefig(p, "$(@__DIR__)/../plots/test_residual.pdf")

println('\n', " "^6, "* The figure `test_residual.pdf` is saved. *", '\n')

println(" "^4, "> Computing the solution (Assignment)...")

# Define the kernel
K(x, s) = 1 / (1 + x + s)

# Define the right part of the equation
u(x) = 1 / sqrt(2 - x) * (log((2 + x) / (1 + x)) +
       2 * log((sqrt(3) + sqrt(2 - x)) / (2 + sqrt(2 - x))))

# Compute the g vector
g = [ quadgk(x -> K(x, s[i]) * u(x), c, d; rtol=1e-8)[1] for i in 1:n ]

# Optimize over the regularization parameter
res = Optim.optimize(
    residual,
    [α,],
    LBFGS(),
    Optim.Options(
        show_trace=false,
        extended_trace=true,
        store_trace=true,
    );
    inplace=false,
)

# Save the trace and the results
open("assignment_optimization.trace", "w") do io
    println(io, res.trace)
    println(
        io,
        " * Parameters:\n",
        " "^4, "α = $(res.minimum)\n",
    )
    show(io, res)
end

# Print the residual
println(
    '\n',
    " "^6, "Regularization parameter: ", res.minimizer[1], '\n',
    " "^6, "Residual: ", res.minimum, '\n',
)

println(" "^4, "> Plotting the solution (Assignment)...")

# Recalculate the solution
z = calculate(n, res.minimizer[1])

# Plot the solution
p = plot(s, z; label="", xlabel="s", ylabel="z(s)");

# Save the figure
savefig(p, "$(@__DIR__)/../plots/assignment.pdf")

println('\n', " "^6, "* The figure `assignment.pdf` is saved. *", '\n')

println(" "^4, "> Plotting the residuals against the number of nodes (Assignment)...")

# Define the `n`s to plot against
ns = [ i for i in 10:300 ]

# Calculate the residuals for these `n`s
rs = @. getindex(calculate(ns, res.minimizer[1]), 2)

# Make a plot of the residual against `n`
p = plot(ns, rs; label="", xlabel="n", ylabel="r");

# Save the figure
savefig(p, "$(@__DIR__)/../plots/assignment_residual.pdf")

println('\n', " "^6, "* The figure `assignment_residual.pdf` is saved. *", '\n')

println(" "^4, "> Calculating the solutions for tables (Assignment)...")

# Define the header
header = "α = 1e-4" * " "^7 * "1e-5" * " "^11 * "1e-6" * " "^11 * "1e-7" *
         " "^11 * "1e-8" * " "^11 * "1e-9" * " "^11 * "1e-10" * " "^10 * "1e-11" *
         " "^10 * "1e-12" * " "^10 * "1e-13" * " "^10 * "1e-14" * " "^10 * "1e-15"

# Calculate the solutions for different numbers of nodes and regularization parameters
for n in [100, 200, 300]
    step = Int(n * 0.05)
    data = Matrix{Float64}(undef, 20, 12)
    format = Printf.Format("%.8e "^11 * "%.8e\n")
    open("$(@__DIR__)/../tables/$(n).dat", "w") do io
        println(io, header)
        for (i, α) in enumerate([1e-4, 1e-5, 1e-6, 1e-7, 1e-8, 1e-9, 1e-10, 1e-11, 1e-12, 1e-13, 1e-14, 1e-15])
            sol, _ = calculate(n, α)
            data[:, i] = sol[1:step:end]
        end
        for i in 1:20
            Printf.format(io, format, data[i, :]...)
        end
    end
end

println('\n', " "^6, "* Tables `100.dat`, `200.dat`, and `300.dat` are saved. *", '\n')


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

_trueanom{T<:AbstractFloat}(E::T, e::T) =
    if e < 0
        # We don't need to check e > 1, because a DomainError is raised anyway.
        error("eccentricity must be in the range [0, 1]")
    else
        2.0*atan(sqrt((1.0 + e)/(1.0 - e))*tan(E/2.0))
    end

"""
    trueanom(E, e) -> true anomaly

### Purpose ###

Calculate true anomaly for a particle in elliptic orbit with eccentric anomaly
\$E\$ and eccentricity \$e\$.

### Explanation ###

In the two-body problem, once that the [Kepler's
equation](https://en.wikipedia.org/wiki/Kepler%27s_equation) is solved and
\$E(t)\$ is determined, the polar coordinates \$(r(t), \\theta(t))\$ of the body
at time \$t\$ in the elliptic orbit are given by

\$\$ \\theta(t) = 2\\arctan \\left(\\sqrt{\\frac{1 + e}{1 - e}} \\tan\\frac{E(t)}{2} \\right)\$\$
\$\$ r(t) = \\frac{a(1 - e^{2})}{1 + e\\cos(\\theta(t) - \\theta_{0})}\$\$

in which \$a\$ is the semi-major axis of the orbit, and \$\\theta_0\$ the value
of angular coordinate at time \$t = t_{0}\$.

### Arguments ###

* `E`: eccentric anomaly.  This can be either a scalar or an array
* `e`: eccentricity, in the elliptic motion regime (\$0 \\leq e \\leq 1\$)

### Output ###

The true anomaly.  If an array of eccentric anomalies is provided in input, an
array of the same length as `E` is returned.

### Example ###

Plot the true anomaly as a function of mean anomaly for eccentricity \$e = 0\$,
\$0.5\$, \$0.9\$.  Use [PyPlot.jl](https://github.com/stevengj/PyPlot.jl) for
plotting.

``` julia
using PyPlot
M=linspace(0, 2pi, 1001)[1:end-1];
for ecc in (0, 0.5, 0.9)
    E = kepler_solver(M, ecc);
    plot(M, cirrange(trueanom(E, ecc), 2pi))
end
```

### Notes ###

The eccentric anomaly can be calculated with `kepler_solver` function.
"""
trueanom(E::Real, e::Real) = _trueanom(promote(float(E), float(e))...)

function trueanom{R1<:Real,R2<:Real}(E::AbstractArray{R1}, e::R2)
    typeν = promote_type(float(R1), float(R2))
    ν = similar(E, typeν)
    for i in eachindex(E)
        ν[i] = trueanom(E[i], e)
    end
    return ν
end


# Imports
using Random, Statistics, Plots, Printf
Random.seed!(430);

# Part (a)
N = 10000;
Xₙ = randn(2, 1);
μₓ = [0.5; 0.3];
Cₓ = [1 0.6; 0.6 sqrt(2)];

# 2-dimensional Gaussian vector
X = Cₓ * Xₙ + μₓ;
println("Answer of part (a)")
println("2-dimensional Gaussian vector:")
println(X)

# Part (b)
# Generate 10000 vector with related parameters
function generate(matrix, μₓ, n)
    samples = zeros(N, 2)
    for i in 1:n
        X = randn(2, 1);
        X = sqrt(matrix) * X + μₓ;
        samples[i, :] = X;
    end
    return samples
end

# Plot generated samples
samples = generate(Cₓ, μₓ, N)
X₁, X₂ = samples[:, 1], samples[:, 2];
display(plot(X₁, X₂, seriestype = :scatter, title = "Scatter Plot"))

# Part (c)
#∑ = zeros(2,2)
∑ = []
for i in 1:N
    #∑ += samples[i, :]*samples[i, :]'
    push!(∑, samples[i, :]*samples[i, :]')
end
#Sₓ = ∑/N
Sₓ = sum(∑) / N;
println("\nAnswer of part (c) for $N samples: ")
println(Sₓ)

# When we increase "N", convergence will be [1.25 0.75; 0.75 1.504] matrix
println("Convergence limit matrix:")
convergence = Cₓ + μₓ*μₓ'


# # [Ecosystem Models](@id ecosystem_example)
#
#md # [![](https://img.shields.io/badge/show-nbviewer-579ACA.svg)](@__NBVIEWER_ROOT_URL__/examples/Ecosystem.ipynb)

using AlgebraicDynamics
using AlgebraicDynamics.DWDDynam
using AlgebraicDynamics.UWDDynam

using Catlab.CategoricalAlgebra
using Catlab.WiringDiagrams
using Catlab.Graphics
using Catlab.Programs

using OrdinaryDiffEq
using Plots, Plots.PlotMeasures

# ## Land Ecosystem

# ### Rabbits and foxes

# A standard Lotka Volterra predator-prey model is the composition of three primitive resource sharers:

# 1. a model of rabbit growth: this resource sharer has dynamics $\dot r(t) = \alpha r(t)$ and one port which exposes the rabbit population.
# 2. a model of rabbit/fox predation: this resource sharer has dynamics $$\dot r(t) = -\beta r(t) f(t), \dot f(t) = \gamma r(t)f(t)$$ and two ports which expose the rabbit and fox populations respectively.
# 3. a model of fox population decline: this resource sharer has dynamics $\dot f(t) = -\delta f(t)$ and one port which exposes the fox population.

# However, there are not two independent rabbit populations -- one that grows and one that gets eaten by foxes. Likewise, there are not two independent fox populations -- one that declines and one that feasts on rabbits. To capture these interactions between the trio of resource sharers, we compose them by identifying the exposed rabbit populations and identifying the exposed fox populations.
# The syntax for this undirected composition is defined by an undirected wiring diagram.

## Define the primitive systems
dotr(u,p,t) = p[1]*u
dotrf(u,p,t) = [-p[2]*u[1]*u[2], p[3]*u[1]*u[2]]
dotf(u,p,t) = -p[4]*u

rabbit_growth = ContinuousResourceSharer{Float64}(1, dotr)
rabbitfox_predation = ContinuousResourceSharer{Float64}(2, dotrf)
fox_decline = ContinuousResourceSharer{Float64}(1, dotf)

## Define the composition pattern
rabbitfox_pattern = @relation (rabbits, foxes) begin
    rabbit_growth(rabbits)
    rabbitfox_predation(rabbits,foxes)
    fox_decline(foxes)
end

## Compose
rabbitfox_system = oapply(rabbitfox_pattern, [rabbit_growth, rabbitfox_predation, fox_decline])

# Previously, when we derived the Lotka-Volterra model via [undirected composition](https://algebraicjulia.github.io/AlgebraicDynamics.jl/dev/examples/Lotka-Volterra/#Undirected-composition), we by-hand defined the undirected wiring diagram that implements the composition pattern. In contrast, here we implement the same composition pattern as before but this time using the [`@relation` macro](https://algebraicjulia.github.io/Catlab.jl/stable/apis/programs/#Catlab.Programs.RelationalPrograms.@relation-Tuple). This strategy simplifies the definition and explicitly names the boxes and variables. We  visualize the composition pattern below.

to_graphviz(rabbitfox_pattern, box_labels = :name, junction_labels = :variable, edge_attrs=Dict(:len => ".75"))

# We can now construct an `ODEProblem` from the resource sharer `rabbitfox_system` and plot the solution.

α, β, γ, δ = 0.3, 0.015, 0.015, 0.7
params = [α, β, γ, δ]

u0 = [10.0, 100.0]
tspan = (0.0, 100.0)

prob = ODEProblem(rabbitfox_system, u0, tspan, params)
sol = solve(prob, Tsit5())

plot(sol, lw=2, title = "Lotka-Volterra Predator-Prey Model", bottom_margin=10mm, left_margin=10mm, label=["rabbits" "foxes"])
xlabel!("Time")
ylabel!("Population size")

# ### Rabbits, foxes, and hawks
# Suppose we now have a three species ecosystem containing rabbits, foxes, and hawks. Foxes and hawks both prey upon rabbits but do not interact with each other. This ecosystem consists of five primitive systems which share variables.
# 1. rabbit growth:  $\dot r(t) = \alpha r(t)$
# 2. rabbit/fox predation:  $\dot r(t) = -\beta r(t) f(t), \dot f(t) = \delta r(t)f(t)$
# 3. fox decline:  $\dot f(t) = -\gamma f(t)$
# 4. rabbit/hawk predation: $\dot r(t) = -\beta' r(t)h(t), \dot h(t) = \delta' r(t)h(t)$
# 5. hawk decline:  $\dot h(t) = -\gamma' h(t)$

# This means the desired composition pattern has five boxes and many ports and wires to keep track of. Instead of implementing this composition pattern by hand, we construct it as a pushout.

## Define the composition pattern for rabbit growth
rabbit_pattern = @relation (rabbits,) -> rabbit_growth(rabbits)

## Define the composition pattern for the rabbit/hawk Lotka Volterra model
rabbithawk_pattern = @relation (rabbits, hawks) begin
    rabbit_growth(rabbits)
    rabbit_hawk_predation(rabbits,hawks)
    hawk_decline(hawks)
end

## Define transformations between the composition patterns
rabbitfox_transform  = ACSetTransformation((Box=[1], Junction=[1], Port=[1], OuterPort=[1]), rabbit_pattern, rabbitfox_pattern)
rabbithawk_transform = ACSetTransformation((Box=[1], Junction=[1], Port=[1], OuterPort=[1]), rabbit_pattern, rabbithawk_pattern)

## Take the pushout to define the composition pattern for the rabbit, fox, hawk system
rabbitfoxhawk_pattern = ob(pushout(rabbitfox_transform, rabbithawk_transform))

## Visualize the compsition pattern
to_graphviz(rabbitfoxhawk_pattern, box_labels = :name, junction_labels = :variable, edge_attrs=Dict(:len => ".9"))

#-
## Define the additional primitive systems
dotrh(x, p, t) = [-p[5]*x[1]*x[2], p[6]*x[1]*x[2]]
doth(x, p, t)  = -p[7]*x

rabbithawk_predation = ContinuousResourceSharer{Float64}(2, dotrh)
hawk_decline         = ContinuousResourceSharer{Float64}(1, doth)

## Compose
land_system = oapply(rabbitfoxhawk_pattern,
                        [rabbit_growth, rabbitfox_predation, fox_decline,
                         rabbithawk_predation, hawk_decline])

## Solve and plot
β′, γ′, δ′ = .01, .01, .5
params = vcat(params, [β′, γ′, δ′])

u0 = [10.0, 100.0, 50.0]
tspan = (0.0, 100.0)

prob = ODEProblem(land_system, u0, tspan, params)
sol = solve(prob, Tsit5())

plot(sol, lw=2, title = "Land Ecosystem", bottom_margin=10mm, left_margin=10mm, label=["rabbits" "foxes" "hawks"])
xlabel!("Time")
ylabel!("Population size")

# Unfortunately, the hawks are going extinct in this model. We'll have to give hawks something else to eat!
#-
# ## Ocean Ecosystem

# Consider a ocean ecosystem containing three species —- little fish, big fish, and sharks -— with two predation interactions —- sharks eat big fish and big fish eat little fish.

# This ecosystem can be modeled as the composition of 3 machines:
# 1. Evolution of the little fish population:  this machine has one exogenous variable which represents a population of predators $h(t)$ that hunt little fish. This machine has one output which emits the little fish population. The dynamics of this machine is the driven ODE $$\dot f(t) = \alpha f(t) - \beta f(t)h(t)$$
# 2. Evolution of the big fish population:  this machine has two exogenous variables which represent a population of prey $e(t)$ that are eaten by big fish and a population of predators $h(t)$ which hunt big fish. This machine has one output which emits the big fish population. The dynamics of this machine is the drive ODE $$\dot F(t) = \gamma F(t)e(t) - \delta F(t) - \beta'F(t)h(t)$$
# 3. Evolution of the shark population:  this machine has one exogenous variable which represents a population of prey $e(t)$ that are eaten by sharks. This machine has one output which emits the shark population. The dynamics of this machine is the driven ODE $$\dot s(t) = \gamma's(t)e(t) - \delta's(t)$$

## Define the primitive systems
dotfish(f, x, p, t) = [p[1]*f[1] - p[2]*x[1]*f[1]]
dotFISH(F, x, p, t) = [p[3]*x[1]*F[1] - p[4]*F[1] - p[5]*x[2]*F[1]]
dotsharks(s, x, p, t) = [p[6]*s[1]*x[1]-p[7]*s[1]]

fish   = ContinuousMachine{Float64}(1,1,1, dotfish,   f->f)
FISH   = ContinuousMachine{Float64}(2,1,1, dotFISH,   F->F)
sharks = ContinuousMachine{Float64}(1,1,1, dotsharks, s->s)

# We compose these machines by (1) sending the output of the big fish machine as the input to both the little fish and shark machines and (2) sending the output of the little fish and shark machines as the inputs to the big fish machine.
# The syntax for this directed composition is given by a directed wiring diagram.

## Define the composition pattern
ocean_pattern = WiringDiagram([], [])
fish_box = add_box!(ocean_pattern, Box(:fish, [:pop], [:pop]))
Fish_box = add_box!(ocean_pattern, Box(:Fish, [:pop, :pop], [:pop]))
shark_box = add_box!(ocean_pattern, Box(:shark, [:pop], [:pop]))

add_wires!(ocean_pattern, Pair[
    (fish_box, 1)  => (Fish_box, 1),
    (shark_box, 1) => (Fish_box, 2),
    (Fish_box, 1)  => (fish_box, 1),
    (Fish_box, 1)  => (shark_box, 1)
])

## Visualize the composition pattern
to_graphviz(ocean_pattern, orientation=TopToBottom)

#-

## Compose
ocean_system = oapply(ocean_pattern, [fish, FISH, sharks])

## Solve and plot
α, β, γ, δ, β′, γ′, δ′ = 0.35, 0.015, 0.015, 0.7, 0.017, 0.017, 0.35
params = [α, β, γ, δ, β′, γ′, δ′]

u0 = [100.0, 10, 2.0]
tspan = (0.0, 100.0)

prob = ODEProblem(ocean_system, u0, tspan, params)
sol = solve(prob, FRK65(0))

plot(sol, lw=2, title = "Ocean Ecosystem", bottom_margin=10mm, left_margin=10mm, label=["little fish" "big fish" "sharks"])
xlabel!("Time")
ylabel!("Population size")

# ## Total ecosystem
# ### Another layer of composition

# We will introduce a final predation interaction -- hawks eat little fish --  which will combine the land and ocean ecosystems.

# There will be 16 parameters in to the total ecosystem.
# - parameters 1-7 will determine the land ecosystem
# - parameters 8 and 9 will determine the hawk/little fish predation. Parameter 8 gives the rate of hawk growth and parameter 8 gives the rate of little fish decline.
# - parameter 10-16 will determine the ocean ecosystem.

# The composition will be as resource shareres so the first thing we will do is use the dynamics of the machine `ocean_system` to define the dynamics of a resource sharer. We will also define a resource sharer that models hawk/little fish predation.

## Define the additional primitive systems
ocean_system_rs = ContinuousResourceSharer{Float64}(3, (u,p,t)->eval_dynamics(ocean_system, u, [], p[10:16]))

dothf(u,p,t) = [p[8]*u[1]*u[2], -p[9]*u[1]*u[2]]
fishhawk_predation = ContinuousResourceSharer{Float64}(2, dothf)

## Define the composition pattern
eco_pattern = @relation () where (rabbits, foxes, hawks, littlefish, BigFish, sharks)  begin
    turf(rabbits,foxes,hawks)
    air(hawks, littlefish)
    surf(littlefish, BigFish, sharks)
end

## Visualize the composition pattern
to_graphviz(eco_pattern, box_labels = :name, junction_labels = :variable, edge_attrs=Dict(:len => ".75"))

#-
## Compose
ecosystem=oapply(eco_pattern, [land_system, fishhawk_predation, ocean_system_rs])

# We can now plot the evolution of the total ecosystem.

## Solve and plot
u0 = [100.0, 50.0, 20.0, 100, 10, 2.0]
tspan = (0.0, 100.0)

params = [0.3, 0.015, 0.015, 0.7, .01, .01, .5,
          0.001, 0.003,
          0.35, 0.015, 0.015, 0.7, 0.017, 0.017, 0.35]

prob = ODEProblem(ecosystem, u0, tspan, params)
sol = solve(prob, Tsit5())
plot(sol, lw=2, bottom_margin=10mm, left_margin=10mm, label = ["rabbits" "foxes" "hawks" "little fish" "big fish" "sharks"])

# Let's zoom in on a narrower time-window.
tspan = (0.0, 30.0)

prob = ODEProblem(ecosystem, u0, tspan, params)
sol = solve(prob, Tsit5())
plot(sol, lw=2, bottom_margin=10mm, left_margin=10mm, label = ["rabbits" "foxes" "hawks" "little fish" "big fish" "sharks"])

# As a sanity check we can define the rates for the hawk/little fish predation to be 0. This decouples the land and ocean ecosystems. As expected, the plot shows the original evolution of the land ecosystem overlayed with the original evolution of the ocean ecosystem. This shows that they two ecosystems now evolve independently.

tspan = (0.0, 100.0)
params = [0.3, 0.015, 0.015, 0.7, .01, .01, .5,
          0, 0,
          0.35, 0.015, 0.015, 0.7, 0.017, 0.017, 0.35]
prob = ODEProblem(ecosystem, u0, tspan, params)
sol = solve(prob, Tsit5())
plot(sol, lw=2, bottom_margin=10mm, left_margin=10mm, label = ["rabbits" "foxes" "hawks" "little fish" "big fish" "sharks"])


##############   EXAMPLE-3     ##############
# Correlated errors in X and Y

using LinearFitXYerrors
using Plots; gr()

# INPUT DATA:
# Mahon, K. [1996] The New “York” Regression: Application of an Improved Statistical Method to Geochemistry. International Geology Review, 38(4), pp.293–303

#  Table 3
#      X        Y
M = [0.037   0.0080
     0.035   0.0084
     0.032   0.0100
     0.040   0.0085
     0.013   0.0270
     0.038   0.0071
     0.042   0.0043
     0.030   0.0160]

X = M[:,1]
Y = M[:,2]
σX = 0.03*X     # standard deviation of errors in X
σY = 0.10*Y     # standard deviation of errors in Y
r1 = 0  # correlation coefficient between errors
r2 = 0.7071

# COMPUTE and PLOT:

# st = linearfitxy(X, Y; isplot=true, ratio=:auto);  # if assuming no errors
# stxy1 = linearfitxy(X, Y; σX=σX, σY=σY, r=r1, isplot=true, ratio=:auto);

stxy2 = linearfitxy(X, Y; σX=σX, σY=σY, r=r2, isplot=true, ratio=:auto);

savefig("Example3_LinearFitXYerrors.png")
########################################


using LinearAlgebra
using SparseArrays
using Plots

"This routine solves the periodic advection equation using finite volumes."

m = 200 # number of points

# define spatial grid
xv = LinRange(-1,1,m+2)
Δx = xv[2]-xv[1]
x = xv[1:end-1] .+ Δx/2 # m+1 cell centers

a = 1 # advection speed
Δt = .9*Δx / a # timestep
T = 2.0 # final time

# initial condition and forcing
u0(x) = exp(-25*x^2)
u = u0.(x)
f(uL,uR) = uL

# indexing for periodic boundaries
index_left = [m+1; 1:m+1]
index_right = [1:m+1; 1]
fv_flux = zeros(m+2) # storage at interfaces

Nsteps = ceil(Int,T/Δt)
Δt = T / Nsteps
interval = 10
plot()
unorm = zeros(Nsteps)
@gif for k = 1:Nsteps
    for i = 1:m+2 # loop over cell interfaces
        left = index_left[i]
        right = index_left[i]
        fv_flux[i] = f(u[left],u[right])
    end
    for i = 1:m+1
        u[i] = u[i] - a*Δt/Δx * (fv_flux[i+1]-fv_flux[i])
    end

    if k % interval==0
        plot(x,u,linewidth=2,legend=false,title="Solution at time $(k*Δt)",ylims=(-1.5,1.5))
        println("on timestep $k out of $Nsteps.")
    end
end every interval
# plot(x,u,linewiΔth=2,legend=false,title="Solution at final time $(T)",ylims=(-1.5,1.5))


"""
    SEq1

Solutions of Equations in One Variable
here are functions to find root:
1. Bisection
2. Fixed-Point Iteration
3. Newton Method
4. The Secant Method
5. The False Position Method
6.  Müller’s Method
"""
module SEq1
using ForwardDiff

"""
    Bisection(f::Function, a::Real, b::Real; TOL::Float64=0.0000001, N::Int=40, output_seq::Bool=false)

Use Bisection To find a solution to.

``f(x) = 0``

You should use the function, and a interval ``[a, b]``, tolerance TOL, maximum number of iterations N.

Then you will get approximate solution ``p`` or can't find in maximum N and return -1.

you can set output_seq=true to output sequence.
"""
@inline function Bisection(f::Function, a::Real, b::Real; TOL::Float64=0.0000001, N::Int=40, output_seq::Bool=false)
    i = 1
    fa = f(a)
    seq = []
    if f(a)*f(b) > 0
        return -1
    else
        while i <= N
            p = a + (b - a)/2
            fp = f(p)
            push!(seq, [p, f(p)])
            if fp == 0 || (b - a)/2 < TOL
                if output_seq
                    return seq
                else
                    return p
                end
                break
            end
            i += 1
            if fa*fp > 0
                a = p
                fa = fp
            else
                b = p
            end
        end
        return -1
    end
end

"""
    fixed_point(f::Function, p₀::Real; TOL::Float64=0.0000001, N::Int=30, output_seq::Bool=false)

Use fixed point Iteration to find root.

The number p is a **fixed point** for a given function g if ``g(p) = p``.

input a function, initial p₀, optional Args: tolerancs TOL, maximum number of iterations N, output_seq.

Output a root p or sequence.
"""
@inline function fixed_point(f::Function, p₀::Real; TOL::Float64=0.0000001, N::Int=30, output_seq::Bool=false)
    i = 1
    seq = []
    while i <= N
        p = f(p₀)
        push!(seq, p₀)
        if abs(p - p₀) < TOL
            if output_seq
                return seq
            else
                return p
            end
            break
        end
        i += 1
        p₀ = p
    end
    return -1
end

"""
    Newton(f::Function, p₀::Real; TOL::Float64=0.0000001, N::Int=20, output_seq=false)

Use Newton method to find root.

you should use a function and initial p₀, then you can set Args or use default.

"""
@inline function Newton(f::Function, p₀::Real; TOL::Float64=0.0000001, N::Int=20, output_seq::Bool=false)
    i = 1
    seq = []
    f1(x) = ForwardDiff.derivative(x -> f(x), x)
    while i <= N
        push!(seq, p₀)
        p = p₀ - f(p₀)/f1(p₀)
        if abs(p - p₀) < TOL
            if output_seq
                return seq
            else
                return p
            end
            break
        end
        p₀ = p
        i += 1
    end
    return -1
end

"""
    Secant(f::Function, p₀::Real, )

Use the Secant Method to find root
A function and p₀, p₁(p₀<p₁), then you can set Args or use default.
"""
@inline function Secant(f::Function, p₀::Real, p₁::Real; TOL::Float64=0.0000001, N::Int=30, output_seq::Bool=false)
    i = 2
    seq = []
    a₀ = f(p₀)
    a₁ = f(p₁)
    push!(seq, p₀)
    push!(seq, p₁)
    while i <= N
        p = p₁ - a₁*(p₁ - p₀)/(a₁ - a₀)
        push!(seq, p)
        if abs(p - p₁) < TOL
            if output_seq
                return seq
            else
                return p
            end
            break
        end
        i += 1
        p₀ = p₁
        a₀ = a₁
        p₁ = p
        a₁ = f(p)
    end
end

"""
    FalsePos(f::Function, p₀::Real, p₁::Real; TOL::Float64=0.00000001, N::Int=40, output_seq::Bool=false)

Use the false position method to find root.
a function and interval ``[a, b]``, then you can set Args or use default.
"""
@inline function FalsePos(f::Function, p₀::Real, p₁::Real; TOL::Float64=0.00000001, N::Int=40, output_seq::Bool=false)
    i = 2
    seq = []
    q₀ = f(p₀)
    q₁ = f(p₁)
    push!(seq, p₀)
    push!(seq, p₁)
    while i <= N
        p = p₁ - q₁*(p₁ - p₀)/(q₁ - q₀)
        push!(seq, p)
        if abs(p - p₁) < TOL
            if output_seq
                return seq
            else
                return p
            end
            break
        end
        i += 1
        q = f(p)
        if q*q₁ < 0
            p₀ = p₁
            q₀ = q₁
        end
        p₁ = p
        q₁ = q
    end
    return -1
end

"""
    ModifiedNewton(f::Function, p₀::Real; TOL::Float64=0.00000001, N::Int=20, output_seq::Bool=false)

Newton and ModifiedNewton, Both methods are rapidly convergent to the actual zero.
"""
@inline function ModifiedNewton(f::Function, p₀::Real; TOL::Float64=0.00000001, N::Int=20, output_seq::Bool=false)
    i = 1
    seq = []
    push!(seq, p₀)
    f1(x) = ForwardDiff.derivative(x -> f(x), x)
    f11(x) = ForwardDiff.derivative(x -> f1(x), x)
    while i <= N
        p = p₀ - f(p₀)*f1(p₀)/((f1(p₀))^2 - f(p₀)*f11(p₀))
        push!(seq, p)
        if abs(p - p₀) < TOL
            if output_seq
                return seq
            else
                return p
            end
            break
        end
        i += 1
        p₀ = p
    end
    return -1
end

"""
    Muller(f::Function, p₀::Real, p₁::Real, p₂::Real; TOL::Float64=0.00000001, N::Int=20, output_seq::Bool=false)
"""
@inline function Muller(f::Function, p₀::Real, p₁::Real, p₂::Real; TOL::Float64=0.00000001, N::Int=20, output_seq::Bool=false)
    h₁ = p₁ - p₀
    h₂ = p₂ - p₁
    δ₁ = (f(p₁) - f(p₀))/h₁
    δ₂ = (f(p₂) - f(p₁))/h₂
    d = (δ₂ - δ₁)/(h₁ + h₂)
    i = 3
    seq = []
    while i <= N
        b = δ₂ + h₂*d
        D = sqrt(complex(b^2 - 4*f(p₂)*d))
        if abs(b - D) < abs(b + D)
            E = b + D
        else
            E = b - D
        end
        h = -2 * f(p₂)/E
        p = p₂ + h
        push!(seq, p)
        if abs(h) < TOL
            if output_seq
                return seq
            else
                return p
            end
            break
        end
        p₀ = p₁
        p₁ = p₂
        p₂ = p
        h₁ = p₁ - p₀
        h₂ = p₂ - p₁
        δ₁ = (f(p₁) - f(p₀))/h₁
        δ₂ = (f(p₂) - f(p₁))/h₂
        d = (δ₂ - δ₁)/(h₁ + h₂)
        i += 1
    end
    return -1
end

end


using Test
import LinearAlgebra: dot
using RationalSimplex

@testset "RationalSimplex" begin

@testset "Test standard form" begin
      # Test case 1
      # min -10x - 12y - 12z
      # st     x +  2y +  2z <= 20
      #       2x +   y +  2z <= 20
      #       2x +  2y +   z <= 20
      #       x, y, z >= 0
      @testset "Test case 1 (optimal)" begin
            c = [-10//1, -12//1, -12//1, 0//1, 0//1, 0//1]
            A = [  1//1    2//1    2//1  1//1  0//1  0//1;
                   2//1    1//1    2//1  0//1  1//1  0//1;
                   2//1    2//1    1//1  0//1  0//1  1//1]
            b = [20//1, 20//1, 20//1]
            status, x = simplex(c, A, b)
            @test status == :Optimal
            @test x[1] == 4//1
            @test x[2] == 4//1
            @test x[3] == 4//1
      end

      # Test case 2
      # min -90x - y
      # st    2x + y <= 40
      #      20x + y <= 100
      #       2x     <= 3
      #        x,  y >= 0
      @testset "Test case 2 (optimal)" begin
            c = [-90//1, -1//1, 0//1, 0//1, 0//1]
            A = [  2//1   1//1  1//1  0//1  0//1;
                  20//1   1//1  0//1  1//1  0//1;
                   2//1   0//1  0//1  0//1  1//1]
            b = [ 40//1, 100//1, 3//1]
            status, x = simplex(c, A, b)
            @test status == :Optimal
            @test x[1] == 3//2
            @test x[2] == 37//1
            @test x[3] == 0//1
            @test x[4] == 33//1
            @test x[5] == 0//1
      end

      # Test case 3
      # min x
      # st  x >= 3
      #     x <= 2
      @testset "Test case 3 (infeasible)" begin
            c = [1//1, 0//1, 0//1]
            A = [1//1 -1//1  0//1;
                 1//1  0//1  1//1]
            b = [3//1, 2//1]
            status, x = simplex(c, A, b)
            @test status == :Infeasible
      end

      # Test case 4
      # min -x
      # st  x >= 2
      @testset "Test Case 4 (unbounded)" begin
            c = [-1//1, 0//1]
            A = [ 1//1 -1//1;]
            b = [ 2//1]
            status, x = simplex(c, A, b)
            @test status == :Unbounded
      end

      # Check simplex detects nonneg rhs
      @testset "Check for nonnegative RHS" begin
            c = [-1//1, 0//1]
            A = [ 1//1 -1//1;]
            b = [-2//1]
            @test_throws AssertionError simplex(c, A, b)
      end
end

@testset "Test general form" begin
      # Test case 1
      # max  10x + 12y + 12z
      # st     x +  2y +  2z <= 20
      #       2x +   y +  2z <= 20
      #       2x +  2y +   z <= 20
      #       x, y, z >= 0
      @testset "Test case 1 (optimal)" begin
            c = [ 10//1,  12//1,  12//1]
            A = [  1//1    2//1    2//1;
                   2//1    1//1    2//1;
                   2//1    2//1    1//1]
            b = [20//1, 20//1, 20//1]
            status, x = simplex(c, :Max, A, b, ['<','<','<'])
            @test status == :Optimal
            @test x[1] == 4//1
            @test x[2] == 4//1
            @test x[3] == 4//1
      end

      # Test case 2
      # max  90x + y
      # st    2x + y <= 40
      #      20x + y <= 100
      #       2x     <= 3
      #        x,  y >= 0
      @testset "Test case 2 (optimal)" begin
            c = [ 90//1,  1//1]
            A = [  2//1   1//1;
                  20//1   1//1;
                   2//1   0//1]
            b = [ 40//1, 100//1, 3//1]
            status, x = simplex(c, :Max, A, b, ['<','<','<'])
            @test status == :Optimal
            @test x[1] == 3//2
            @test x[2] == 37//1
      end

      # Test case 3
      # min x
      # st  x >= 3
      #     x <= 2
      @testset "Test case 3 (infeasible)" begin
            c = [1//1, 0//1]
            A = [1//1 0//1;
                 1//1 0//1]
            b = [3//1, 2//1]
            status, x = simplex(c, :Min, A, b, ['>','<'])
            @test status == :Infeasible
      end

      # Test case 4
      # min x
      # st   x >=  3
      #     -x >= -2
      @testset "Test case 4 (infeasible)" begin
            c = [1//1, 0//1]
            A = [ 1//1 0//1;
                 -1//1 0//1]
            b = [3//1, -2//1]
            status, x = simplex(c, :Min, A, b, ['>','>'])
            @test status == :Infeasible
      end
end

@testset "Kantorovich distances" begin
      # Problem description from
      # http://stla.github.io/stlapblog/posts/KantorovichWithJulia.html
      c = [0//1, 1//1, 1//1, 1//1, 0//1, 1//1, 1//1, 1//1, 0//1]
      b = [1//7, 2//7, 4//7, 1//4, 1//4, 1//2]
      M = [1//1 1//1 1//1 0//1 0//1 0//1 0//1 0//1 0//1;
           0//1 0//1 0//1 1//1 1//1 1//1 0//1 0//1 0//1;
           0//1 0//1 0//1 0//1 0//1 0//1 1//1 1//1 1//1;
           1//1 0//1 0//1 1//1 0//1 0//1 1//1 0//1 0//1;
           0//1 1//1 0//1 0//1 1//1 0//1 0//1 1//1 0//1;
           0//1 0//1 1//1 0//1 0//1 1//1 0//1 0//1 1//1]

      status, x = simplex(c, :Min, M, b, ['=','=','=','=','=','='])
      @test dot(c,x) == 3//28
end

end  # RationalSimplex testset.


using Pkg
Pkg.activate(pwd())

# # Logistic regression
# ## Loading and preparing data

using StatsPlots
using RDatasets

iris = dataset("datasets", "iris")

# ### Exercise:
# Create the `iris_reduced` dataframe in the following way:
# - Label "setosa" will be deleted.
# - Label "versicolor" will be the negative class.
# - Label "virginica" will be the positive class.
# - Add the `intercept` column with ones as entries.
# For the features, consider only petal length and petal width.
#
# **Hint**: Use the `Query` package or do it manually via the `!insertcols` function.
#
# ---
# ### Solution:

# ---

X = Matrix(iris_reduced[:, 1:3])
y = iris_reduced.label

# ### Exercise:
# Since X has two features (columns), it is simple to visualize. Use scatter plot to show
# the data. Use different colours for different classes. Try to produce a nice graph by
# including names of classes and axis labels (petal length and petal width).
#
# ---
# ### Solution:

# ---
#
# ## Training the classifier
# ### Exercise:
# Write a function log_reg which takes as an input the dataset, the labels and the initial
# point. It should use Newton's method to find the optimal weights ``w``. Print the results
# when started from zero.
#
# It would be possible to use the code optim(f, g, x, s::Step) from the previous lecture
# and define only the step function s for the Newton's method. However, sometimes it may be
# better to write simple functions separately instead of using more complex machinery.
#
# ---
# ### Solution:

# ---
#
# ## Analyzing the solution

using LinearAlgebra

separ(x::Real, w) = (-w[3]-w[1]*x)/w[2]

xlims = extrema(iris_reduced.PetalLength) .+ [-0.1, 0.1]
ylims = extrema(iris_reduced.PetalWidth) .+ [-0.1, 0.1]

@df iris_reduced scatter(
    :PetalLength,
    :PetalWidth;
    group = :Species,
    xlabel = "Petal length",
    ylabel = "Petal width",
    legend = :topleft,
    xlims,
    ylims,
)

plot!(xlims, x -> separ(x,w); label = "Separation", line = (:black,3))

#+

y_hat = σ.(X*w)
grad = X'*(y_hat.-y) / size(X,1)
norm(grad)

# ### Exercise:
# Compute how many samples were correctly and incorrectly classified.
#
# ---
# ### Solution:


# Helpful packages for working with images and factorizations
# using Pkg; Pkg.add("Images")
# using Pkg; Pkg.add("ImageMagick") # And this allows us to load JPEG-encoded images
using Images, LinearAlgebra, Interact

# ------------------------------------------------------------------------------------------
# ### Using a SVD to compress an image
#
# In this exercise, we'll use a singular value decomposition (SVD) to compress an image --
# so that we can store an image without keeping around "unnecessary" information.
#
# To start, let's define a singular value decomposition. In a SVD, we take a matrix $A$ and
# factorize it so that
#
# $$A = USV^T$$
#
# where matrices $U$ and $V$ are unitary and hold our singular vectors. Matrix $S$ is
# diagonal and stores our singular values in decreasing order from top/left to bottom/right.
#
# In Julia, our images are stored as arrays, so we can think of `yellowbanana` as a matrix
# ------------------------------------------------------------------------------------------

file = download("https://uploads6.wikiart.org/images/salvador-dali/the-persistence-of-memory-1931.jpg!Large.jpg")

img = load(file)

size(img)

img[24,24] # Each element in the array is a color

dump(img[24,24])

# ------------------------------------------------------------------------------------------
# We can extract each "channel" of red, green, and blue and view each independently:
# ------------------------------------------------------------------------------------------

channels = Float64.(channelview(img))
Gray.(channels[1, :, :])

# ------------------------------------------------------------------------------------------
# That means we can take the SVD of this image. So, we can store this picture of a banana as
# sets of singular vectors and singular values.
#
# **The reason this is important** is that we'll find that we do **not** need to keep track
# of *all* the singular vectors and *all* the singular values to store an image that still
# looks like a banana! This means we can choose to keep only the important information,
# throw away the rest, and thereby "compress" the image.
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# If we don't throw away any data, we get back what we started with:
# ------------------------------------------------------------------------------------------

U, S, V = svd(channels[1,:,:])
Gray.(U * Diagonal(S) * V')

# ------------------------------------------------------------------------------------------
# But of course we're not doing any compression here — the sizes of U, S, and V are bigger
# than our original matrix! This is like the opposite of compression.  The key is that the
# values are stored in decreasing order so we can start throwing things away.
# ------------------------------------------------------------------------------------------

sum(length.((U, S, V)))

length(img)

Gray.(U[:, 1:25] * Diagonal(S[1:25]) * V[:, 1:25]')

sum(length.((U[:, 1:25], S[1:25], V[:, 1:25])))/length(img)

# ------------------------------------------------------------------------------------------
# Of course this is just one channel of the image. Let's put it all back together and see
# how we can compress the different channels to find an acceptable compression level.
# ------------------------------------------------------------------------------------------

function rank_approx(M, k)
    U, S, V = svd(M)

    M = U[:, 1:k] * Diagonal(S[1:k]) * V[:, 1:k]'

    M = min.(max.(M, 0.0), 1.)
end

n = 100
@manipulate for k1 in 1:n, k2 in 1:n, k3 in 1:n
    colorview(  RGB,
                rank_approx(channels[1,:,:], k1),
                rank_approx(channels[2,:,:], k2),
                rank_approx(channels[3,:,:], k3)
    )
end

# ------------------------------------------------------------------------------------------
# **So how can we use a SVD to determine what information in an image is really important?**
#
# The singular values tell us!
#
# If we have matrices $U$, $S$, and $V$ from our image, we can rebuild that image with the
# matrix product $USV^T$.
#
# Taking this matrix product is the same as adding together the outer products of each
# corresponding pair of vectors from $U$ and $V$, scaled by a singular value ($\sigma$) from
# $S$. In other words, for a (100 x 100) pixel image,
#
# $$A_{image} = USV^T = \sum_{i = 1}^{100} \sigma_i \mathbf{u_i}\mathbf{v_i'} $$
#
# Every outer product $u_i * v_i'$ creates a (100 x 100) matrix. Here we're summing together
# one hundred (100 x 100) matrices in order to create the original matrix $A_{image}$. The
# matrices at the beginning of the series -- those that are scaled by **large** singular
# values -- will be **much** more important in recreating the original matrix $A_{image}$.
#
# This means we can approximate $A_{image}$ as
#
# $$A_{image} \approx \sum_{i = 1}^{n} \sigma_i \mathbf{u_i}\mathbf{v_i'}$$
#
# where $n < 100$.
#
#
# #### Exercise
#
# Write a function called `compress_image`. Its input arguments should be an image and the
# factor by which you want to compress the image. A compressed grayscale image should
# display when `compress_image` is called.
#
# For example,
#
# ```julia
# compress_image("images/104_100.jpg", 33)
# ```
#
# will return a compressed image of a grayscale banana built using 3 singular values. (This
# image has 100 singular values, so use `fld(100, 33)` to determine how many singular values
# to keep. `fld` performs "floor" division.)
#
# *Hints*:
#
# * Perform the SVD on the `channelview` of a grayscale image.
# * In an empty input cell, execute `?svd` to find a function that wil perform an SVD for
# you.
# ------------------------------------------------------------------------------------------

.nbexports/introductory-tutorials/intro-to-julia/long-version/050 Compressing an Image.jl


#function to calculate volume
function sphere_vol(r)

    return 4/3*pi*r^3
end

quadratic(a, sqr_term, b) = (-b + sqr_term) / 2a

#quadratic funtion calculation

function quadratic2(a::Float64, b::Float64, c::Float64)

    sqr_term = sqrt(b^2-4a*c)
    r1 = quadratic(a, sqr_term, b)
    r2 = quadratic(a, -sqr_term, b)

    r1, r2
end

vol = sphere_vol(3)

@printf "volume = %0.3f\n" vol
#prints volume = 113.097

quad1, quad2 = quadratic2(2.0, -2.0, -12.0)
println("result 1: ", quad1)
#> result 1: 3.0
println("result 2: ", quad2)
#> result 2: -2.0


function isPrime(n::Int)
	for i in 2:ceil(sqrt(n))
		if n%i==0 && n!=i return false end
	end
	return true
end

function nextPrime!(l)
	x = l[end]+2
	while !isPrime(x)
		x += 2
	end
	push!(l, x)
end

l = [2,3,5,7,11,13]
while length(l) < 10001
	nextPrime!(l)
end
println(length(l))
println(l[end])


# Julia program to find the sum of divisors of a number.

function sum_of_divisors(num)
    i = 1
    sum = 0
    while (i <= sqrt(num))
        if num % i == 0 && i == sqrt(num)
            sum = sum + i
        elseif num % i == 0
            sum = sum + i
            sum = sum + (num ÷ i)
        end
        i = i + 1
    end
    return sum
end

print("Enter the number: ")
num = readline()
num = parse(Int, num)
sum = sum_of_divisors(num)
print("The Sum of the Divisors of the given number is $sum.")

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

SAMPLE INPUT AND OUTPUT

SAMPLE I

Enter the number: 256
The Sum of the Divisors of the given number is 511.

SAMPLE II

Enter the number: 5687
The Sum of the Divisors of the given number is 6384.
"""


# Formulas used for Chapter 2 exercises
yearly_compounded_interest(i, r, y) = (i * (1 + (r / 100)))^y

find_effective_rate(i, n) = (1 + i / n)^n - 1

"internal rate of return"
function irr(func1, func2, init, stop)
    sol = Float64[]
    λₖ = func1(init)
    λₖ₊₁ = λₖ - func1(λₖ) / func2(λₖ)
    push!(sol, float(λₖ))
    if stop == 0
        return [λₖ]
    end
    push!(sol, float(λₖ₊₁))
    if stop == 1
        return [λₖ, λₖ₊₁]
    end
    for k in range(2; stop=stop)
        λₖ = λₖ₊₁
        λₖ₊₁ = λₖ - func1(λₖ) / func2(λₖ)
        push!(sol, float(λₖ₊₁))
    end
    return sol
end

"net present value"
npv(inv, val, rate, year) = -inv + val / (1 + rate)^year

"annual equivalent rate"
aer(r, n) = (1 + r / n)^n - 1

"continuously compounded interest"
cci(r) = exp(r) - 1

"present value"
function pv(p, r, n)
    interest = 0.0
    for i in range(0, n - 1; step=1)
        interest += (1 + r)^-i
    end
    return p * interest
end

# From Lecture Notes

"present value"
function present_value(s, r)
    return mapreduce((k, x) -> x / (1 + r)^k, +, 0:(length(s) - 1), s)
end


"""If we list all the natural numbers below 10 that are multiples of 3 or 5, we get 3, 5, 6 and 9. The sum of these multiples is 23.
Find the sum of all the multiples of 3 or 5 below 1000."""

function sum_multiples_3_5(n::Int)
    multiples3or5 = union(3:3:n-1 , 5:5:n-1)
    return sum(multiples3or5)
end

@assert sum_multiples_3_5(10) ==23

@show sum_multiples_3_5(1000)

# Oneliner and faster
@show sum([i for i in 1:999 if i%3==0 || i%5 ==0]);


function getGrid(minongrid, maxongrid, GridPoints, method)

#NB: This code borrows some code from Chris Carroll of John's Hopkins University
#His website is here http://www.econ2.jhu.edu/people/ccarroll/ and his
#notes on solving dynamic models are very good - they can be found
#here: http://www.econ2.jhu.edu/people/ccarroll/SolvingMicroDSOPs.pdf

#% ------------------------------------------------------------------------
# Explanation

# We need to a grid from a to b. A basic approach could involve spacing out
# the point equally. The following line of code would achieve this:
#     grid= linspace(a, b, GridPoints);

# If we want to space them out so that the growth rate of the distance
# between spaces is equal we achieve this by spacing out the logs of grid
# equally. We could achieve this by the following line of code:
#     grid= exp(linspace(log(a), log(b), GridPoints));

# This is problematic if a<=0. The approach taken here is to get a grid
# from log(1) to log(b - a + 1), exponentiate and then subrtact 1
# so that we have a grid from 0 to b-a. If we have add a to each point we
# get the desired result - a log-spaced grid from a to b

span = maxongrid - minongrid;     # b - a

if method=="equalsteps"
    grid= linspace(min, span, GridPoints);
elseif method== "logsteps"
  loggrid = linspace(log(1), log(1+span), GridPoints);
  grid = exp(loggrid)-1;
elseif method=="3logsteps"
  loggrid = linspace(log(1+log(1+log(1))), log(1+log(1+log(1+span))), GridPoints);
  grid = exp(exp(exp(loggrid)-1)-1)-1;
elseif method=="5logsteps"
  loggrid = linspace(log(1+log(1+log(1+log(1+log(1))))), log(1+log(1+log(1+log(1+log(1+span))))), GridPoints);
  grid = exp(exp(exp(exp(exp(loggrid)-1)-1)-1)-1)-1;
elseif method=="10logsteps"
  loggrid = linspace(log(1+log(1+log(1+log(1+log(1+log(1+log(1+log(1+log(1+log(1)))))))))), log(1+log(1+log(1+log(1+log(1+log(1+log(1+log(1+log(1+log(1+span)))))))))), GridPoints);
  grid = exp(exp(exp(exp(exp(exp(exp(exp(exp(exp(loggrid)-1)-1)-1)-1)-1)-1)-1)-1)-1)-1;
else
    error("Error in getgrid. You have entered an invalid method for choosing the distance between grid points. Method must be one of equalsteps, logsteps, 3logsteps, 5logsteps or 10logsteps.");
end

grid = grid + minongrid*ones(Float64,GridPoints,1);

return grid;

end


import LinearAlgebra
import SparseArrays
#using Images
import Images
import MAT

LA = LinearAlgebra
SA = SparseArrays
Img = Images

# Matrix
dim = 4
A = rand(dim,dim)
AT = A'
A2 = A*AT

@show A[11] == A[1,2]
b = rand(dim)
# Solve for x where Ax = b
x = A\b
@show LA.norm(A*x - b)

@show typeof(A)
@show typeof(b)
@show typeof(rand(1,dim))
@show typeof(AT)

#A = [1 2;3 4]
sizeof(A) # => byte count
sizeof(AT)
B = copy(AT)
sizeof(B)

# \(x, y) => x^(-1) * y
#x = \(A,b) # => A^(-1)*b
@show \(3,6) # => 1/3 * 6
@show 3\6    # => 1/3 * 6

1//3 + 1//2

#A = sparse([1,2,4], [1,1,1], [1.0,1.0,1.0], 4, 2)
#F = qr(A)

luA = LA.lu(A)
# LU = PA
LA.norm(luA.L*luA.U - luA.P*A)

# QR = A
qrA = LA.qr(A)
Q = qrA.Q
R = qrA.R
LA.norm(Q*R - A)

# Skip Cholesky factorization

# Skip Sparse Linear Algebra

# Images as matrices
X1 = Images.load("data/khiam-small.jpg")
# Gray scale
Xgray = Images.Gray.(X1)
# Extract RGB values
R = map(i -> X1[i].r, 1:length(X1))    # => Vector
R2 = Float64.(reshape(R, size(X1)...)) # (80089,) => (283,283)
G = map(i -> X1[i].g, 1:length(X1))
G2 = Float64.(reshape(G, size(X1)...)) # (80089,) => (283,283)
B = map(i -> X1[i].b, 1:length(X1))
B2 = Float64.(reshape(B, size(X1)...))

Z = zeros(size(G2)...)
Images.RGB.(Z,G2,Z)

Xgrayvalues = Float64.(Xgray)
# SVD: A = U*Sigma*V'
usv =LA.svd(Xgrayvalues)
# usv.U, S, V, Vt
U = usv.U
S = usv.S
V = usv.V
Vt = usv.Vt

LA.norm(U*LA.diagm(S)*Vt - Xgrayvalues)

# Use the top 4 singular vectors/values to form a new image
i = 1
u1 = usv.U[:,i]
v1 = usv.V[:,i]
img1 = usv.S[i]*u1*v1'

i = 2
u1 = usv.U[:,i]
v1 = usv.V[:,i]
img1 += usv.S[i]*u1*v1'

i = 3
u1 = usv.U[:,i]
v1 = usv.V[:,i]
img1 += usv.S[i]*u1*v1'

i = 4
u1 = usv.U[:,i]
v1 = usv.V[:,i]
img1 += usv.S[i]*u1*v1'

Images.Gray.(img1)

i = 1:100
u1 = usv.U[:,i]
s1 = SA.spdiagm(0=>usv.S[i])
v1 = usv.V[:,i]
img1 = u1 * s1 * v1'
Img.Gray.(img1)
# Close but not the same
LA.norm(Xgrayvalues - img1)

# Face recognition
M = MAT.matread("data/face_recog_qr.mat")
# V2 contains 490 faces
# each column is a face and need to reshape into 192,168
V2 = M["V2"]
q = reshape(V2[:,200], 192, 168)
Img.Gray.(q)
b = q[:]

A = V2[:,2:end]
x = A\b
Img.Gray.(reshape(A*x, 192,168))
LA.norm(A*x - b)

# Skip


begin
# Jump Search

# Julia code to jump search x in arr[].
# If x is present then return its location,
# otherwise return -1
import Base:length,show,AbstractArray
arr = [ 0, 1, 1, 2, 3, 5, 8, 13, 21,
    34, 55, 89, 144, 233, 377, 610 ]
x = 610
n = length(arr)
function jumpsearch(arr::AbstractArray{},n,x)

    # Finding block size to be jumped
    step = sqrt(n)

    # Finding the block where element is
    # present (if it is present)
    prev = 1
    while arr[round(Int,floor(min(step, n)))] < x
        prev = step
        step += sqrt(n)
        if prev >= n
            return -1
		end
	end
    # Doing a linear search for x in
    # block beginning with prev.
    while arr[round(Int,floor(prev))] < x
        prev += 1

        # If we reached next block or end
        # of array, element is not present.
        if prev == min(step, n)
            return -1
		end
	end
    # If element is found
    if arr[round(Int,floor(prev))] == x
        return prev
	end
    return -1
end
global result=-1

result=jumpsearch(arr,n, x)

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