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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


using Symbolics, ModelingToolkit, Plots, DifferentialEquations
using BenchmarkTools

# define variables
@variables x y

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

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

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

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

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

# direct simplification is performed
2x-x # x

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

ex + ex
ex / ex
ex^2/ex

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

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

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

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

# automatically perform correct algebra
z^2

real(z^2)

z'z # i.e. z_conj * z

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

xs[1]

sum(xs)  # lazy representation of sum operation

# eagerly evaluate summation
sum(collect(xs))

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

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

rosenbrock(xs)

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

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

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

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

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

# plot the sparsity matrix
spy(hes_sparsity)

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

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

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

foop(aa) ≈ out

using ForwardDiff

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


using LinearAlgebra
using SparseArrays
using Plots
using ForwardDiff

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

julia> flag
0

julia> err
7.477270454885512e-8

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


# --- User parameters

n = 100
num_samples = 10000

# --- Imports

using BenchmarkTools
using LinearAlgebra
using Statistics

# --- Functions

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

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

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

    Q, R
end;

# --- Test qr_mgs()

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

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

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

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

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

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

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

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

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

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

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

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

# --- Orthogonality Error

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

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

# --- Determinant Error

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

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

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

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

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

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


"""
[Ceva's theorem](https://en.wikipedia.org/wiki/Ceva%27s_theorem) is a theorem about triangles in
plane geometry. Given a triangle `ABC`, let the lines `AO`, `BO` and `CO` be drawn from the vertices
to a common point `O` (not on one of the sides of `ABC`), to meet opposite sides at `D`, `E` and `F`
respectively.  (The segments `AD`, `BE`, and `CF` are known as
[cevians](https://en.wikipedia.org/wiki/Cevian).) Then, using signed lengths of segments,
```math
BD \\times CE \\times AF = DC \\times EA \\times FB.
```
"""
module Ceva

using Plots
using SymPy
using PlaneGeometry
using PlaneGeometry.GeoPlots

export
    ceva_proof, ceva_draw, ceva_rand, ceva, ceva_check

"""
    ceva_draw(📐, O)

Verify Ceva's Theorem for the triangle `📐`.
"""
function ceva_draw(tri, O)

    plist, elist = ceva(tri, O)
    A, B, C, D, E, F = plist
    push!(plist, O)

    plt = plot(tri, fill=(0, :green), aspect_ratio=:equal, fillalpha= 0.2, leg=false)

    plt = scatter!(shape(plist),
            leg=false,
            aspect_ratio=:equal,
            color=:red,
            series_annotations = text.(['A':'F'..., 'O'], :bottom))

    AD, BE, CF = [Edge(epair...) for epair in zip([A, B, C], [D, E, F])]

    for e in [AD, BE, CF]
        plot!(e, color=:orange, leg=false)
    end

    plt, ceva_check(elist...)
end

"""
    ceva_rand()

Verify Ceva's Theorem for a random triangle.
"""
function ceva_rand()
    pts = rand(0:1//10:1,6);
    A, B, C = [Point(pts[i], pts[i+3]) for i in 1:3]

    # Choose O within ABC so the picture looks nicer
    ptx = [pt.x for pt in [A, B, C]]
    pty = [pt.y for pt in [A, B, C]]
    xmin, xmax = N.([f(ptx...) for f in [min, max]])
    ymin, ymax = N.([f(pty...) for f in [min, max]])

    tri = Triangle(A, B, C)
    AB, BC, CA = edges(tri)

    # Use rejections sampling until the point is in ABC
    while true
        x = rand(xmin:1//100:xmax)
        y = rand(ymin:1//100:ymax)
        O = Point(x, y)

        plist, elist = ceva(tri, O)

        # We are unlucky
        if plist == nothing
            continue
        end

        BD, DC, CE, EA, AF, FB = elist

        if BD > BC || DC > BC || CE > CA || EA > CA || AF > AB || FB > AB
            # This mean O is not in ABC
            continue
        end

        return ceva_draw(Triangle(A, B, C), O)
    end
end

"""
    ceva_proof()

Prove Ceva's Theorem.
"""
function ceva_proof()
    @vars by cx positive=true
    @vars cy ox oy

    A = Point(0, 0); B = Point(0, by); C = Point(cx, cy); O = Point(ox, oy)
    tri = Triangle(A, B, C)

    plist, elist = ceva(tri, O)
    ceva_check(elist...)
end

"""
    ceva(Triangle(A, B, C), O)

Returns `([A, B, C, D, E, F], [BD, DC, CE, EA, AF, FB])`.
"""
function ceva(tri, O)
    A, B, C = vertices(tri)
    AB, BC, CA = edges(tri)
    AO, BO, CO = [Edge(pt, O) for pt in vertices(tri)]
    D, E, F = [concurrent(epair...) for epair in zip([BC, CA, AB], [AO, BO, CO])]

    # This mean no such points can be found. (This is possible)
    if D == nothing || E == nothing || F == nothing
        return nothing, nothing
    end

    elist = [Edge(pts...) for pts in zip([B, D, C, E, A, F], [D, C, E, A, F, B])]
    ([A, B, C, D, E, F], elist)
end

"""
    ceva_check(BD, DC, CE, EA, AF, FB)

Check if the line segments satisfies Ceva's theorem.
"""
function ceva_check(BD, DC, CE, EA, AF, FB)
    eq = Eq((BD*CE*AF)^2, (DC*EA*FB)^2)
    N(simplify(eq))
end

end


### A Pluto.jl notebook ###
# v0.14.8

using Markdown
using InteractiveUtils

# This Pluto notebook uses @bind for interactivity. When running this notebook outside of Pluto, the following 'mock version' of @bind gives bound variables a default value (instead of an error).
macro bind(def, element)
    quote
        local el = $(esc(element))
        global $(esc(def)) = Core.applicable(Base.get, el) ? Base.get(el) : missing
        el
    end
end

# ╔═╡ e6db292c-d2be-11eb-1c25-05ed50a9256c
using FourierTools, TestImages, ImageShow, Plots, Colors, FFTW, Noise, PlutoUI

# ╔═╡ 4958079f-da85-4357-94df-b122cf01c958
FFTW.set_num_threads(12)

# ╔═╡ 2a47b7a9-7767-44a7-8dda-480ba1b41f57
md"$\mathcal{F}[U](\mathbf k) = \frac{1}{\sqrt{2\pi}} \int_{-\infty}^{\infty} U(\mathbf r) \cdot \exp[-i  (\mathbf x \cdot \mathbf k)] \, \mathrm d \mathbf x$

$\mathcal{F}^{-1}[\tilde U](\mathbf x) = \frac{1}{\sqrt{2\pi}} \int_{-\infty}^{\infty} U(\mathbf r) \cdot \exp[i  (\mathbf x \cdot \mathbf k)] \, \mathrm d \mathbf k$
";

# ╔═╡ b71a5e39-0805-4e3f-aa83-c3972f6c54af
begin
	img = Float32.(testimage("fabio_gray"));
	img_1D = img[:, 200]
end;

# ╔═╡ c39440fc-17e6-440c-9e10-7b2c9a7e9331
md"### FourierTools.jl | Working with the Frequency Space

* **Felix Wechsler**: Leibniz Institute of Photonic Technology in Jena, Germany
* **Rainer Heintzmann**: Leibniz Institute of Photonic Technology in Jena, Germany
"

# ╔═╡ 6e445cd5-6e51-4a8f-ad5c-2636b3da68f4
md"## FourierTools.jl

* A package built on FFTW.jl
* Provides several helper functions
* implements algorithms (like interpolation) based on FFTs
"

# ╔═╡ e28dc936-04da-48cb-b324-73632d008290
md"
### Definition of Fourier Transform

Complex valued sum

$\mathcal F[x_k] = X_k = \sum_{n=0}^{N-1} x_n \cdot \exp\left[-\frac{i 2 \pi}{N} k n\right] =$

$=\sum_{n=0}^{N-1} x_n \cdot \left[ \cos\left(\frac{2 \pi}{N} k n\right) - i \cdot \sin\left(\frac{2 \pi}{N} k n\right)\right]$

Inverse transform

$\mathcal F^{-1}[X_k] = x_k = \frac{1}{N} \sum_{k=0}^{N-1} X_k \cdot \exp\left[\frac{i 2 \pi}{N} k n\right]$

* it decomposes a signal into harmonic basis functions
* the signal is represented in the _Fourier_ or _Frequency_ space

### Fast Fourier Transform - FFT
* naive sum calculation takes $\mathcal O(N^2)$ operations for a 1D dataset with $N$ points
* FFT algorithms evaluates this with $\mathcal O(N \cdot \log(N))$ operations
* In Julia available _FFTW.jl_
"

# ╔═╡ e8f1e951-26dd-4c0e-83f9-4a2c60b0ebed
md" ## FFT of an dataset with FFTW.jl"

# ╔═╡ 3adab086-0c77-40e5-990a-6b89b183cc4d
begin
	x = range(0, 50, length=999)
	f(x) = (0.1 * sin(17.3 + x) + 0.5 * cos(1.2*x) + 0.3 * sin(5*x)^2 + 0.05 * sin(13.1*x) + sin(27.2 * x)) * exp(-x/20)
	data = add_gauss(f.(x), 0.1)
end;

# ╔═╡ 850869e6-32da-48cf-b2a0-081cc9f8e524
plot(x, data, title="function to transform")

# ╔═╡ d6f9bd7a-ddcb-40b5-8509-f4dac3f8bd5d
begin
	plot(real(fft(data)), title="FFT of the function", xlabel="frequency", label="real part")
	plot!(imag(fft(data)), label="imaginary part")
end

# ╔═╡ b4b57f32-e405-4463-b0fa-625e9d54df34
md"
* Data: $[x_1, x_2, ..., x_N]$
* FFT of Data: $[f_0, f_1, ..., f_{N/2}, f_{-N/2}, f_{-N/2+1}, ...,f_{-1}]$
* (for real data negative and positive frequencies are the complex conjugate)
"

# ╔═╡ f0746bc6-f50a-49f1-96d2-0379498a0168
begin
	plot(fftshift(fftfreq(size(data, 1))), real(ffts(data)), xlabel="frequency", label="real part", title="FFT of the dataset but shifted")
	plot!(fftshift(fftfreq(size(data, 1))), imag(ffts(data)), label="imaginary part")
end

# ╔═╡ 979d641e-83ad-4484-bc28-912947711936
fftshift(fft(data)) == ffts(data)

# ╔═╡ 0ba41c40-21a3-4e26-99fe-cacd76d47a67
typeof(ffts(data))

# ╔═╡ cc160200-5911-4c80-b651-a54e1608f186
begin
	N_low = 64
	x_min = 0.0
	x_max = 8*2π

	xs_low = range(x_min, x_max, length=N_low+1)[1:N_low]
	xs_high = range(x_min, x_max, length=5000)[1:end-1]
	f2(x) = sin(0.5*x) + cos(x) + cos(2 * x) + sin(0.25*x)
	arr_low = f2.(xs_low)
	arr_high = f2.(xs_high)
end;

# ╔═╡ 7acf4c37-397d-4c1f-8735-022888390cbe
md"## Different FFT methods
* `fft` interpretes the data as top-left centered

"

# ╔═╡ b15df267-6606-4583-8ce9-3dddf1ecf3f9
fft([1.0,0,0,0,0])

# ╔═╡ 76bb0c74-1a52-47ff-adf2-862cfea15da5
fft([0,0, 1.0 ,0,0]) # single delta peak

# ╔═╡ ec839c01-a62f-46eb-8e25-15d04a07a79f
ft([0,0, 1.0 ,0,0])

# which is equivalent to fftshift(fft(ifftshift([0,0, 1.0 ,0,0])))

# ╔═╡ c7f0e20e-7c80-4a47-abdf-800f26ca999a
ffts([0,0, 1.0 ,0,0])

# which is equivalent to fftshift(fft([0,0, 1.0 ,0,0]))

# ╔═╡ a725ae98-e2cc-439c-9cc1-692972ac98e7
md"## Calculate sinc interpolation

* Fourier coefficients represent coefficients of Fourier series
* Inverse transform is essentially a re-evaluation of a Fourier Series
* Evaluating this series at more samples points results in an interpolated signal
"

# ╔═╡ d5717c9c-2c16-47da-8533-a28acf189e55
begin
	N = 1000
	xs_interp = range(x_min, x_max, length=N+1)[1:N]
	arr_interp = resample(arr_low, N)
end;

# ╔═╡ 3d8f11a3-4ebf-4d36-a227-b2cafa13e1d6
begin
	scatter(xs_low, arr_low, legend=:bottomleft, markersize=2, label="Low sampling")
	plot!(xs_interp, arr_interp, label="FFT based sinc interpolation", linestyle=:dash)
	plot!(xs_high, arr_high, linestyle=:dashdotdot, label="High sampling")
end

# ╔═╡ 551f179b-46ad-425f-9dd6-d8f6469a09cf
[Gray.(img) Gray.(resample(img, (256, 712)))]

# ╔═╡ a5977c5c-dbcd-402d-b7a3-56614968ccb7
begin
	N_ds = 32
	xs_ds = range(x_min, x_max, length=N_ds+1)[1:N_ds]
	arr_ds = real(resample(arr_high, (N_ds,)))
end;

# ╔═╡ 7f3be670-cfe3-4e60-b38f-ebacb4eaf8d3
md"## Fourier Shift Theorem

$\mathcal{F}[U(x+ Δx)](\mathbf k) = \mathcal{F}[U(x)](\mathbf k) \exp(i k \Delta x)$

$\Longrightarrow$
$U(x+ Δx) = \mathcal{F}^{-1}\bigg[\mathcal{F}[U(x)](\mathbf k) \exp(i k \Delta x)\bigg](x)$

* which allows for efficient sub-pixel shifting
"

# ╔═╡ 1ab61f99-799f-4c34-b0d1-205c01c20dbc
@bind offset Slider(-10:0.01:10, show_value=true)

# ╔═╡ 55667950-4542-4505-9d21-e44ea8b6074d
begin
	f3(x) = cos(4π * x / 30)
	x1 = 1:30
	x2 = x1 .+ 3
	y1 = f3.(x1)
	y2 = f3.(x2)
	y3 = shift(y2, tuple(offset))
end;

# ╔═╡ 8fc68dfc-0fb7-4a5a-bef1-ae0b33ddf4f6
shift(y2, (offset,))

# ╔═╡ cfb02910-fb2d-4de0-bc81-8ecea50e5cb5
begin
	plot(y1, label="Original signal")
	plot!(y2, label="Shifted signal")
	plot!(y3, label="Fourier shifted with $offset")
end

# ╔═╡ 105344de-8617-47fd-aeed-84102f19eaeb
md"## Based on Shifting $\Longrightarrow$ Rotation

"

# ╔═╡ 344f9221-4213-4540-bb40-8a2de2d9b48c
begin
	z = zeros(Float32, round.(Ref(Int), 1.5 .* size(img)))
	FourierTools.center_set!(z, img)
end;

# ╔═╡ 27a7bcae-e50f-4225-bd7f-45934ec55be3
@bind ϕ Slider(-180:1:180, show_value=true)

# ╔═╡ ac331005-48ca-4be8-b0a8-85f6f740f09a
FourierTools.rotate(z, ϕ);

# ╔═╡ bf2c54f1-8110-4e25-b89c-7276cf55f42a
Gray.(FourierTools.rotate(z, ϕ))

# ╔═╡ a5ea8226-ff14-432c-974e-68e1a79155cf
Gray.(FourierTools.rotate(img, ϕ))

# ╔═╡ e1600b0a-b8ef-49b8-8ed0-f9c4ae0c436b

# ╔═╡ 7ff8de2d-85fd-402a-88eb-16427e1e0b47
md"## Convolution
Efficient FFT convolutions for both real and complex functions are included as well
"

# ╔═╡ 03effebd-e918-4847-b13a-7d41d4f159e7
md"Blurring strength"

# ╔═╡ e9862df6-8d1a-4c5e-88ab-1da28c2652af
@bind pow Slider(1:0.01:6, show_value=true)

# ╔═╡ bf631ded-b54c-4768-9a8d-825ab7612695
begin
	x3 = fftpos(1, size(img, 1))
	kernel = exp.(.- 10.0 .^pow .* (x3 .^2 .+ x3'.^2))
	kernel ./= sum(kernel)
end;

# ╔═╡ f4870819-9db6-4bdc-8c8d-141c8fa67461
img_blurry = conv_psf(img, kernel);

# ╔═╡ 9d22e799-fe80-47ef-a326-1f5f1ccc07f8
[Gray.(img) Gray.(img_blurry)]

# ╔═╡ c530d7ea-60bd-4458-b3ca-80b509fb4f33
md"# Conclusion

* FourierTools.jl provides convenient wrappers to the FFTW library
* offers efficient algorithms like sinc interpolation based on FFTs
* deals with the intricacies of the correct handling of even/odd sized arrays in Fourier space
"

# ╔═╡ Cell order:
# ╠═e6db292c-d2be-11eb-1c25-05ed50a9256c
# ╠═4958079f-da85-4357-94df-b122cf01c958
# ╟─2a47b7a9-7767-44a7-8dda-480ba1b41f57
# ╟─b71a5e39-0805-4e3f-aa83-c3972f6c54af
# ╟─c39440fc-17e6-440c-9e10-7b2c9a7e9331
# ╟─6e445cd5-6e51-4a8f-ad5c-2636b3da68f4
# ╟─e28dc936-04da-48cb-b324-73632d008290
# ╟─e8f1e951-26dd-4c0e-83f9-4a2c60b0ebed
# ╟─3adab086-0c77-40e5-990a-6b89b183cc4d
# ╟─850869e6-32da-48cf-b2a0-081cc9f8e524
# ╟─d6f9bd7a-ddcb-40b5-8509-f4dac3f8bd5d
# ╟─b4b57f32-e405-4463-b0fa-625e9d54df34
# ╟─f0746bc6-f50a-49f1-96d2-0379498a0168
# ╠═979d641e-83ad-4484-bc28-912947711936
# ╠═0ba41c40-21a3-4e26-99fe-cacd76d47a67
# ╟─cc160200-5911-4c80-b651-a54e1608f186
# ╟─7acf4c37-397d-4c1f-8735-022888390cbe
# ╠═b15df267-6606-4583-8ce9-3dddf1ecf3f9
# ╠═76bb0c74-1a52-47ff-adf2-862cfea15da5
# ╠═ec839c01-a62f-46eb-8e25-15d04a07a79f
# ╠═c7f0e20e-7c80-4a47-abdf-800f26ca999a
# ╟─a725ae98-e2cc-439c-9cc1-692972ac98e7
# ╟─d5717c9c-2c16-47da-8533-a28acf189e55
# ╟─3d8f11a3-4ebf-4d36-a227-b2cafa13e1d6
# ╠═551f179b-46ad-425f-9dd6-d8f6469a09cf
# ╟─a5977c5c-dbcd-402d-b7a3-56614968ccb7
# ╟─7f3be670-cfe3-4e60-b38f-ebacb4eaf8d3
# ╟─1ab61f99-799f-4c34-b0d1-205c01c20dbc
# ╠═8fc68dfc-0fb7-4a5a-bef1-ae0b33ddf4f6
# ╟─55667950-4542-4505-9d21-e44ea8b6074d
# ╟─cfb02910-fb2d-4de0-bc81-8ecea50e5cb5
# ╟─105344de-8617-47fd-aeed-84102f19eaeb
# ╟─344f9221-4213-4540-bb40-8a2de2d9b48c
# ╟─27a7bcae-e50f-4225-bd7f-45934ec55be3
# ╠═ac331005-48ca-4be8-b0a8-85f6f740f09a
# ╟─bf2c54f1-8110-4e25-b89c-7276cf55f42a
# ╟─a5ea8226-ff14-432c-974e-68e1a79155cf
# ╠═e1600b0a-b8ef-49b8-8ed0-f9c4ae0c436b
# ╟─7ff8de2d-85fd-402a-88eb-16427e1e0b47
# ╟─03effebd-e918-4847-b13a-7d41d4f159e7
# ╟─e9862df6-8d1a-4c5e-88ab-1da28c2652af
# ╠═f4870819-9db6-4bdc-8c8d-141c8fa67461
# ╠═9d22e799-fe80-47ef-a326-1f5f1ccc07f8
# ╟─bf631ded-b54c-4768-9a8d-825ab7612695
# ╟─c530d7ea-60bd-4458-b3ca-80b509fb4f33


#=
P36 (**) Determine the prime factors of a given positive integer (2).
Construct a list containing the prime factors and their multiplicity.
Example:
* (prime-factors-mult 315)
((3 2) (5 1) (7 1))
Hint: The problem is similar to problem P13.
=#

SMALLEST_PRIME = 2

# From P35
function next_prime(n)
    # Brute-force incremental check
    isprime(n) ? n : next_prime(n + 1)
end

# From P35
function _prime_factors(n, x, pf)
    if isprime(n)
        vcat(pf, n)
    else
        if mod(n, x) == 0
            _prime_factors(div(n, x), SMALLEST_PRIME, vcat(pf, x))
        else
            _prime_factors(n, next_prime(x + 1), pf)
        end
    end
end

# From P35
function prime_factors(n)
    # Starting with the smallest prime, 2
    isprime(n) ? n : _prime_factors(n, SMALLEST_PRIME, [])
end

function _prime_factors_multi(pf, n, count, counters)
    if isempty(pf)
        vcat(counters, Vector{}[[n, count]])
    else
        if pf[1] == n
            _prime_factors_multi(pf[2:end], n, count + 1, counters)
        else
            new_counters = vcat(counters, Vector{}[[n, count]])
            _prime_factors_multi(pf[2:end], pf[1], 1, new_counters)
        end
    end
end

function prime_factors_multi(n)
    pf = prime_factors(n)
    _prime_factors_multi(pf[2:end], pf[1], 1, Vector{}[])
end

@assert prime_factors_multi(315) == Vector{}[[3, 2], [5, 1], [7, 1]]

println("Tests passed: JL-36.jl")


"""
    interpolation_search(array::Array{Int64,1}, target::Int64)

The Interpolation search algorithm (ISA) finds a target-position in a sorted array by using
a numerical procedure. The sorting procedure uses a linear fitting for finding the target
position of the remaining search space in the array in more detail. The array's target
position is calculated by the straight slope between the lowest and largest boundary of the
remaining array and the lowest array position itself during each optimization cycle. If the
target-position cannot be found, the array-space will be shrink for the lower or higher
boundary region based on a comparison. For more information see: [https://en.wikipedia.org/wiki/Interpolation_search](https://en.wikipedia.org/wiki/Interpolation_search)

# Arguments
- `array::Array{Int64,1}`: Sorted array of integers
- `target::Int64`: Target-value to find the position

# Examples
```julia-repl
julia> import ClassicAlgorithmsCollections
julia> arr = [10,11, 12, 14, 16, 18, 19, 20, 21, 22, 23, 24, 33, 35, 42, 47]
julia> target = 12
julia> ClassicAlgorithmsCollections.interpolation_search(arr, target)
3
```
"""
function interpolation_search(array::Array{Int64,1}, target::Int64)
    # Find indexs of two low and high array-boundary
    low = 1
    high = length(array)

    # Since array is sorted, an element present in array must be in range defined by boundary
    while low <= high && target >= array[low] && target <= array[high]

        # Check if array contains only one element
        if low == high
            if array[low] == target
                return low
            end
            return nothing
        end

        # Linear fitting of the new position pos and transforming to an integer value.
        pos =
            low + floor(
                Int64,
                ((float(high - low) / (array[high] - array[low])) * (target - array[low])),
            )

        #  Check if target is now found
        if array[pos] == target
            return pos
        end

        # If x is larger, x is in upper part
        if array[pos] < target
            low = pos + 1
            # Else x has to be smaller, x is in lower part
        else
            high = pos - 1
        end
    end
    # If target is not in the list
    return nothing
end


"""Julia program to implement Binary Search algorithm.
Binary Search is a Divide and Conquer strategy based algorithm, where the entire array is divided into two halfs and the
required element is searched for in one of these sections as per the conditions. It is done on sorted arrays.
"""

function binary_search(arr, n, ele)
    low = 1
    high = n
    while high >= low
        mid = low + (high - low) ÷ 2
        # If the mid element is the required element, return that index
        if(arr[mid] == ele)
            return mid
        # ele is greater than mid element then ele would be present in first half of the array
        elseif (arr[mid] > ele)
            high = mid - 1
        #Else if  ele is smaller than mid element then ele would be present in last half of the array
        else
            low = mid + 1
        end
    end
    # If the element is not found return 0
    return 0
end

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

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

SAMPLE INPUT AND OUTPUT

SAMPLE I

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

SAMPLE II

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

"""


@test [combinations([])...] == []
@test [combinations(['a', 'b', 'c'])...] == Any[['a'],['b'],['c'],['a','b'],['a','c'],['b','c'],['a','b','c']]

@test [combinations("abc",3)...] == Any[['a','b','c']]
@test [combinations("abc",2)...] == Any[['a','b'],['a','c'],['b','c']]
@test [combinations("abc",1)...] == Any[['a'],['b'],['c']]
@test [combinations("abc",0)...] == Any[[]]
@test [combinations("abc",-1)...] == []

@test filter(x->iseven(x[1]),[combinations([1,2,3],2)...]) == Any[[2,3]]

# multiset_combinations
@test [multiset_combinations("aabc", 5)...] == Any[]
@test [multiset_combinations("aabc", 2)...] == Any[['a','a'],['a','b'],['a','c'],['b','c']]
@test [multiset_combinations("aabc", 1)...] == Any[['a'],['b'],['c']]
@test [multiset_combinations("aabc", 0)...] == Any[Char[]]
@test [multiset_combinations("aabc", -1)...] == Any[]
@test [multiset_combinations("", 1)...] == Any[]
@test [multiset_combinations("", 0)...] == Any[Char[]]
@test [multiset_combinations("", -1)...] == Any[]

# with_replacement_combinations
@test [with_replacement_combinations("abc", 2)...] == Any[['a','a'],['a','b'],['a','c'],
                                                              ['b','b'],['b','c'],['c','c']]
@test [with_replacement_combinations("abc", 1)...] == Any[['a'],['b'],['c']]
@test [with_replacement_combinations("abc", 0)...] == Any[Char[]]
@test [with_replacement_combinations("abc", -1)...] == Any[]
@test [with_replacement_combinations("", 1)...] == Any[]
@test [with_replacement_combinations("", 0)...] == Any[Char[]]
@test [with_replacement_combinations("", -1)...] == Any[]

#cool-lex iterator
@test_throws DomainError [CoolLexCombinations(-1, 1)...]
@test_throws DomainError [CoolLexCombinations(5, 0)...]
@test [CoolLexCombinations(4,2)...] == Vector[[1,2], [2,3], [1,3], [2,4], [3,4], [1,4]]
@test isa(iterate(CoolLexCombinations(1000, 20))[2], Combinatorics.CoolLexIterState{BigInt})

# Power set
@test collect(powerset([])) == Any[[]]
@test collect(powerset(['a', 'b', 'c'])) == Any[[],['a'],['b'],['c'],['a','b'],['a','c'],['b','c'],['a','b','c']]
@test collect(powerset(['a', 'b', 'c'], 1)) == Any[['a'],['b'],['c'],['a','b'],['a','c'],['b','c'],['a','b','c']]
@test collect(powerset(['a', 'b', 'c'], 1, 2)) == Any[['a'],['b'],['c'],['a','b'],['a','c'],['b','c']]


#****************************************************************#
# VeysiADN 4 Apr 2021
# Advanced Robotics Homework 3 Codes
#****************************************************************#
using LinearAlgebra
#****************************************************************#
# Returns 3 x 3 rotation matrix representing
# rotation about unit vector w by angle theta.
# theta should be in radian
function Rodrigues(w,theta)
    if( length(w) != 3)
        println("!Warning! : W matrix should have 3 elements.")
        return
    end
    if(size(w)==(1,3))
        w=w';
    end
    w_hat = [0      -w[3]    w[2];
             w[3]     0     -w[1];
            -w[2]    w[1]      0];
    rotation_matrix = cos(theta)*Matrix(1I,3,3) + sin(theta)*w_hat+(1-cos(theta))*w*w';
    return rotation_matrix;
end
#****************************************************************#
# Builds rigid transformation matrix T from given
#rotation matrix R and translation t
function RpToTrans(R,t)
    if( size(R)!=(3,3) )
        println("!Warning! : Rotation matrix should be 3x3.")
        return
    end
    if( length(t) != 3 )
        println("!Warning! : Translation matrix should have 3 elements.")
        return
    end
    if(size(t)==(1,3))
        t=t';
    end
    R_B_T = hcat(R,t) ;
    R_B_T = vcat(R_B_T,[0 0 0 1]) ;
    return R_B_T
end
#****************************************************************#
#Extracts rotation matrix R and translation t from given /
# rigid transformation matrix T
function TransToRp(T)
    if( size(T)!=(4,4) )
        println("!Warning! : Transformation matrix should be 4x4.")
        return
    end
    R = T[1:3,1:3]
    t = T[1:3,4]
    return R,t;
end
#****************************************************************#
# Inverts rigid transformation matrix T
function TransInv(T)
    if( size(T)!=(4,4) )
        println("!Warning! : W matrix should be 4x4.")
        return
    end
    if( det(T)==0 )
        println("!Warning! : Inverse doesn't exist (Determinant is equal to zero).")
        return
    end
    R,t = TransToRp(T);
    r_inv =hcat(R',-(R'*t))
    r_inv = vcat(r_inv,[0 0 0 1]);
    return r_inv;
end
#****************************************************************#


module NewtonsMethod

using LinearAlgebra, Statistics, Compat, ForwardDiff

function newtonroot(f, f_prime; x_0, tolerance = 1E-7, maxiter = 1000)
    x_old = x_0
    error = Inf
    iter = 1
    while error > tolerance && iter <= maxiter
        if f_prime(x_old) ≈ 0.0
            return (root = nothing, error = nothing, iter = nothing)
        end
        x_new = x_old - f(x_old)/f_prime(x_old)
        error = norm(x_new - x_old)
        x_old = x_new
        iter = iter + 1
    end
    if iter == maxiter+1
        return (root = nothing, error = nothing, iter = nothing)
    else
        return (root = x_old, error = error, iter = iter)
    end
end

# Applying auto-differentiation
D(f) = x -> ForwardDiff.derivative(f, x)

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

# Export function
export newtonroot

end


using Pkg
Pkg.activate(pwd())

# # Useful packages
# ## Statistics

using Statistics

x = rand(10);

mean(x)
var(x)
std(x)

# ## LinearAlgebra

using LinearAlgebra

A = [-4.0 -17.0; 2.0 2.0]

det(A)
inv(A)
norm(A)
eigvals(A)
eigvecs(A)

#+

D = Diagonal([1,2,3])

#+

D + I
D - I

# ## Random

using Random
using Random: seed!

seed!(1234);
rand(2)

#+

seed!(1234);
rand(2)

#+

randperm(4)

#+

v = [1,2,3,4]
shuffle(v)


# What is REPL - Read/Evaluate/Print/Loop
# Reads what you type;
# Evaluates it;
# Prints out the return value; then
# Loops back and does it all over again.

# Julia: Start REPL
# The Julia extension provides a Julia REPL inside VS Code. You can start this REPL with the Julia: Start REPL command.
# There are four commands that you can use to run code from your editor in the Julia REPL:
# Julia: Execute Code (Ctrl+Enter)
# Julia: Execute Code Block (Alt+Enter)
# Julia: Execute Code Cell (Shift+Enter)
# Julia: Execute File

##############################################################################
################################  basics  ####################################
##############################################################################

4+2
4+2; # If you don't want to see the result of the expression printed, use a semicolon at the end of the expression

ans # the value of the last expression you typed on the REPL, it's stored within the variable ans

name = readline(stdin) # receiving input from user
println("you name is ", name) # showing output to user

?exit # retuns help documentation on the exit function. Functions are typed without parenthesis
apropos("natural log") # returns functions whose description contains the string "natural log"

; # if you type ; you immidiately switch to shell mode
; ls # in shell mode you can type shell commands

# Julia and mathematics
1_000_000 / 1_000 # you can use _ as a number separator
planck_length = 1.61619997e-34 # to use scientific notation, just type "e" (or "E") and don't add any spaces

*(2,3,4) # does the same as 2 * 3 * 4

pi # some mathematical constants are inbuilt
Base.MathConstants.golden # golden ratio
Base.MathConstants.e # exponential

2 + 3 - 4 * 5 / 6 % 7 # All the usual operators are available
((2 + 3) - ((4 * 5) / 6) % 7) # the precedence of the operators is
:(2 + 3 - 4 * 5 / 6 % 7) # If you want to check the precedence of operators, enclose the expression in :( and )

x=2
2x + 4 # Multiplication is usually written *, but this can be omitted when multiplying a variable by a number literal

2^3 # power
10%4 # remainder

666//444 # To make rational numbers, use two slashes (//)

4 / 2
2 \ 4 # There's also reverse division "\", so that x/y = y\x

# The standard arithmetic operators also have special updating versions, which you can use to update variables quickly:
x=5
x+=11
x-=3
*=
/=
\=
%=
^=

# Element-wise operatorsfor arrays
[2,4] .* [10, 20]

3/2 # returns 1.5, which is floating point ans
div(3,2) # returns 1, which is integer ans

# For big calculations
2^64 # oops
big(2)^64 # better
2^big(64) # equally better

x=10 # variable
x==10 # To test equality, you should use the == operator or isequal() function.

a, b = 5, 3 # can also assign multiple variables at the same time

Julia replaces the \sqrt with a square root symbol
Some other examples:
\pi 	π
\Gamma 	Γ
\mercury 	☿
\degree 	°
\cdot 	⋅
\in 	∈

# Maths functions
sin(pi / 2)
log(12)
hypot(3, 4)
# norm() function (after loading via "using LinearAlgebra") returns the "p"-norm of a vector or the operator norm of a matrix
divrem(13, 3) # returns the division and the remainder

# Random numbers
rand() # gets one random Float64 between 0 and 1
rand(2, 2) # an array of Float64s with dimensions 2, 2
rand(Bool, (2,2)) # rand(type, 2, 2) – an array of values of this type with dims 2, 2
rand(0:10, 6) # rand(range, dims) – array of numbers in a range (including both ends) with specified dimensions

# Random numbers in a distribution
randn(100) # randn() gives you one random number in a normal distribution with mean 0 and standard deviation 1. randn(n) gives you n such numbers

# Installing a package
import Pkg
Pkg.add("Plots")

# you can plot this
using Plots; gr()
histogram(randn(500), nbins=50)

# Seeding the random number generator
using Random
Random.seed!(10);
rand(0:10, 6)

# Simple keyboard input
function areaofcircle()
    print("What's the radius?")
    r = parse(Float64, readline(stdin))
    print("a circle with radius $r has an area of:")
    println(π * r^2)
end
# call the function
areaofcircle()

##############################################################################
################################  arrays / tuples   ##########################
##############################################################################

# Arrays can be used for storing lists, vectors, tables, and matrices.
# An array is an ordered collection of elements.
# It's often indicated with square brackets and comma-separated items.
# You can create arrays that are full or empty, and
# arrays that hold values of different types or restricted to values of a specific type.
# Julia arrays are 'column-major'. This means that you read down the columns

# Creating arrays by initializing
arr_Int64 = [1, 2, 3, 4, 5]
arr_Float64 = [1, 2, 3, 4.0, 5]
arr_String = ["this", "is", "an", "array", "of", "strings"]
arr_Function = [sin, cos, tan]
arr_Any = [1, "2", 3.0, sin, pi] # It's possible to create arrays with elements of different types

# Creating uninitialized arrays using Array{type}(dims)
# random-looking numbers are a reminder that you've created an uninitialized array but haven't filled it with any sensible information
array = Array{Int64}(undef, 5)
array3 = Array{Int64}(undef, 2, 2, 2) # array of 3 dimentions

# Creating empty arrays
b = Int64[]
b = String[]
b = Float64[]

# Creating 2-d arrays
arr_2d = [1 2 3 4] # If you leave out the commas when defining an array, you can create 2D arrays quickly. Here's a single row, multi-column array:
arr_2d = [1 2 3 4 ; 5 6 7 8] # you can add another row using ;

# Row and column vectors
col_vector = [1, 2, 3, 4, 5]
row_vector = [1 2 3 4 5]

[[1, 2, 3], [4, 5, 6]] # 2 rows x 3 cols
[[1, 2, 3] [4, 5, 6]]  # 3 rows x 2 cols

# Constructor functions
Vector(undef, 5) # aliases for uninitialized one dimensional arrays
Matrix(undef, 5, 5) # aliases for uninitialized two dimensional arrays; matrix is filled column by column

# Creating arrays using range objects
a = 1:10 # creates a range variable with 10 elements from 1 to 10
collect(a) # collect displays a range variable
[a...] # instead of collect, you could use the ellipsis (...) operator (three periods) after the last element
for n in 1:10 print(n," ") end # can be used to run a for loop
collect(1:2:10) # start:step:stop
collect(4:-1:1) # To go down instead of up, you have to use a negative step value

# More range objects
range(1, length=12, stop=100) # Julia calculates the missing pieces for you by combining the values for the keywords step(), length(), and stop()
exp10.(range(2.0, stop=3.0, length=5)) # For logarithmic ranges (sometimes called 'log space'), you can use simple range objects and then broadcast the exp10 function (10^x) to every element of the range
step(range(1, length=10, stop=100)) # Use step() on a range object to find out what the step size is

# Chaining functions
1:10 |> collect

# Using comprehensions and generators to create arrays
[n^2 for n in 1:5] # a 1-d array
[r * c for r in 1:5, c in 1:5] # a 2-d array
[i^2 for i=1:10  if i != 5] # You can add an if test at the end to filter (keep) values that pass a test
# Generator functions
collect(x^2 for x in 1:10)
collect(x^2 for x in 1:10 if x != 1)
# The advantage of generator expressions is that they generate values when needed, rather than build an array to hold them first.

# create arrays with specific contents
zeros(2, 3) # zeros(Int64, 3, 5)
ones(2, 3)
rand(2, 3) # rand(1:6, 3, 3)
# randn(m, n) creates an m-row by n-column matrix full of normally-distributed random numbers with mean 0 and standard deviation 1
trues(3, 4) # falses()
fill(42, 9) # to create an array with a specific value, i.e. an array of repeating duplicates
fill("hi", 2, 2)
# Fill an existing array
a = zeros(10)
fill!(a, 42) # the exclamation mark (!) or "bang" is to warn you that you're about to change the contents of an existing array (a useful indication that's adopted throughout Julia).

reshape([1, 2, 3, 4, 5, 6, 7, 8], 2, 4) # create a simple array and then change its shape
reshape(range(0, stop=100, length=30), 10, 3) # result is a 10 by 3 array featuring evenly-spaced numbers between 0 and 100.
repeat([1, 2, 3], 2, 3) # repeat(A, n, m), the source array is repeated by n times in the first dimension (rows), and m times in the second (columns)
repeat([1, 2], inner = [2, 3])
repeat([1, 2], outer = [2, 3])
repeat([1 2; 3 4], inner=(2, 1), outer=(1, 3))

# Array Constructor - Each element is set to 'undefined' — #undef
# You can use Vector as an alias for Array
Array{Int}[] # empty array
Array{Int64}(undef, 6) # 1-d array
Array[[1, 2], [3,4]] # 2-d array
Array[1:3, 4:6] # 2-d array
Array{String}(undef, 2, 3, 4) # 3-d array

# ADD ELEMENTS
a = Array[[1, 2], [3,4]]
push!(a, [5,6]) # The push!() function pushes another item onto the back of an array
pushfirst!(a, 0) # To add an item at the front
splice() # To insert an element into an array at a given index
splice!(a, 4:5, 4:6) # insert, at position 4:5, the range of numbers 4:6
L = ['a','b','f']; splice!(L, 3:2, ['c','d','e']) # insert c, d, e between b and f

# REMOVE ELEMENTS
splice!(a,5); # If you don't supply a replacement, you can also use splice!() can remove elements and move the rest of them along
pop!(a) # To remove the last item
popfirst!(a)

b = similar(a) # Copying array dimensions using similar(). array dimensions are copied, but the values aren't

# Accessing arrays
# 1-d
a[5] # 5th element
a[end] # last element
a[end-1] # second last element
# 2-d
a = [[1, 2] [3,4]]
a[2,2] # element at row-2 x col-2
a[:,2] # all elements of col-2
getindex(a, 2,2) # same as a[2,2]

# Elementwise and vectorized operations
a / 100 # every element of the new array is the original divided by 100. These operations operate elementwise

n1 = 1:6;
n2 = 2:7;
n1 .* n2; # if two arrays are to be multiplied then we just add a . before the mathematical operator to signify elementwise
# the first element of the result is what you get by multiplying the first elements of the two arrays, and so on

# How function works on individual variables
f(a, b) = a * b
a=10;b=20;print(f(a,b))

# How function can be applied elementwise to arrays
n1 = 1:6;
n2 = 2:7;
print(f.(n1, n2))

max(), min() # return the largest value from arguments supplied to it
maximum(), minimum() # return the largest value from an array

a[a .== 0] .= 11; # test each value for being equal to 0, then set only those elements to 11

# Finding items in arrays
a = 1:10
3 in a
# findall(), findfirst(), findnext(), findprev() and findlast() — that you can use to get the index or indices of array cells that match a specific value, or pass a test
# findmax() finds the maximum element and returns it and its index in a tuple
# argmax() to return just the index
# Functions such as sum(), prod(), mean(), middle(), do what you would expect

# sum(), mean(), and prod() also let you supply functions: the function is applied to each element and then the results are summed/mean-ed/prod-ded
sum(sqrt, 1:10)  # the sum of the square roots of the first 10 integers

# Finding out about an array
ndims(a) # returns the number of dimensions, i.e. 1 for a vector, 2 for a table, and so on
size(a) # returns the row and column count of the array, in the form of a tuple
length(a) # tells you how many elements the array contains
count(!iszero, a) # tells you how many times a particular value occurs. !iszero helps to count how many non-zero items are there

1:10 |> diff # returns array of length-1, with difference between consecutive elements

union() # builds a new array that's the union or combination of two or more arrays. The operation removes duplicates, and the result contains a single version of each element:
odds = collect(1:2:10); evens = collect(2:2:10); union(odds, evens)

intersect() # returns a new array that's the intersection of two or more arrays. The result contains one occurrence of each element, but only if it occurs in every array:
intersect(1:10, 5:15)

setdiff() # finds the difference between two arrays, i.e. the elements that are in the first array but not the second:
setdiff(1:15, 5:20)

filter(isodd, 1:10) # filter() finds and keeps elements if they pass a test. Here, we're using the isodd() function
count(isodd, 1:100) # counts the number of elements that satisfy the condition
any(isodd, 1:100) # tells you whether any of the elements satisfy the condition
all(isodd, filter(isodd, 1:100)) # tells you if all of the elements satisfy the condition

Pkg.add("Combinatorics") # (do this just once)
using Combinatorics
collect(combinations(a, 3)) # combinations() finds all the possible combinations of elements in an array: you can specify how many elements in each combination
length(permutations(a)) # permutations() generates all permutations. There are a lot — in practice you probably won't need to use collect() to collect the items into an array

# Additional functions
resize!() # change the length of a Vector
append!() # push a second collection at the back of the first one
prepend!() # insert elements at the beginning of the first Vector
empty!(a) # remove all elements
rotr90(a) # make a copy of an array rotated 90 degrees clockwise

# MATRIX operations
A * B # multiply (*), assuming the dimensions are compatible, so m1 * m2 is possible if last(size(m1)) == first(size(m2))
A / B # division
A \ B # back division
A + 1 # you can add, subtract, multiply, and divide a matrix and a scalar

# Joining arrays and matrices
# hcat() keeps the first dimension and extends (joins) in the second, vcat() keeps the second dimension and extends the first
# example - if A and B are two 3x4 arrays
hcat(A, B) # makes a new array that still has 3 rows, but extends/joins the columns to make 8 in total
vcat(A, B) # makes a new array that keeps the 4 columns, but extends to 6 rows

vec() # flattens a matrix into a vector, turning it into a (what some call a 'column') vector

##############################################################################
################################  types  #####################################
##############################################################################

Data elements come in different shapes and sizes, which are called types.
Types are organized in a tree
supertype(Number) # returns the node above
subtypes(Number) # returns the nodes below
sizeof() # tells you how many bytes an item of this type occupies
# If you want to know how big a number you can fit into a particular type
typemax(Int64)
typemin(Int32)

# There are over 340 types in the base Julia system. You can investigate the type hierarchy with the following function:
 function showtypetree(T, level=0)
     println("\t" ^ level, T)
     for t in subtypes(T)
         showtypetree(t, level+1)
     end
 end
 showtypetree(Number)

#this function gets a number, and returns the same number plus one
function plus_one(n::Number) # uses the :: syntax, which means "is of type"
   return n + 1
end

# Concrete and abstract types
# The types that can have subtypes (e.g. Any, Number) are called abstract types
# The types that can have instances are called concrete types. These types cannot have any subtypes.

# Concrete types can be primitive (or basic), and complex (or composite)
primitive types:
    the basic integer and float types (signed and unsigned): Int8, UInt8, Int16, UInt16, Int32, UInt32, Int64, UInt64, Int128, UInt128, Float16, Float32, and Float64
    more advanced numeric types: BigFloat, BigInt
    Boolean and character types: Bool and Char
    Text string types: String
composite type is Rational, used to represent fractions. It is composed of two pieces, a numerator and a denominator, both integers (of type Int)

# Creating your own types
# abstract type
abstract type MyAbstractType end # By default, the type you create is a direct subtype of Any
abstract type MyAbstractType2 <: Number end # the new abstract type is a subtype of Number
# concrete type
# define the data type
mutable struct student <: Any
   name
   age::Int
end
# initialize a variable of that data type
x=student("vivek", 30)
# use the variable
x.name
x.age

##############################################################################
################################  flow control  ##############################
##############################################################################

# ternary and compound expressions
x = 1
x > 3 ? "yes" : "no"

# Boolean switching expressions
isodd(1000003) && @warn("That's odd!")
isodd(1000004) || @warn("That's odd!")

# if elseif else end - conditional evaluation
name = "Julia"
if name == "Julia"
   println("I like Julia")
elseif name == "Python"
   println("I like Python.")
   println("But I prefer Julia.")
else
   println("I don't know what I like")
end

# for end - iterative evaluation
# use the global keyword to define a variable that outlasts the loop
for i in 1:10
    z = i
    println("z is $z")
end

# Some sample for loop statements for different data types
for color in ["red", "green", "blue"] # an array
for letter in "julia" # a string
for element in (1, 2, 4, 8, 16, 32) # a tuple
for i in Dict("A"=>1, "B"=>2) # a dictionary
for i in Set(["a", "e", "a", "e", "i", "o", "i", "o", "u"])

# continue to skip the rest of the code inside the loop and start the loop again with the next value.

# Nested for Loops
for x in 1:10
    for y in 1:10
        @show (x, y) # @show macro prints out the names of things and their values.
        if y % 3 == 0
           break
        end
    end
 end

# list comprehensions
[i^2 for i in 1:10]
[(r,c) for r in 1:5, c in 1:2] # two iterators in a comprehension
# Generator expressions - generator expressions can be used to produce values from iterating a variable
sum(x^2 for x in 1:10)
# Enumerating arrays
m = rand(0:9, 3, 3)
[i for i in enumerate(m)]
# Zipping arrays
for i in zip(0:10, 100:110, 200:210)
    println(i)
end
# Iterable objects
ro = 0:2:100
[i for i in ro]

# while end - iterative conditional evaluation
x=0
while x < 4
    println(x)
    global x += 1
end

x=0
while true
    println(x)
    x += 1
    x >= 4 && break # breaks out of the loop
end

# try catch error throw exception handling
try
    <statement-that-might-cause-an-error>;
catch e # error gets caught if it happens
    println("caught an error: $e") # show the error if you want to
end
println("but we can continue with execution...")

try
    a=10 # no error
catch e
    print(e)
end

try
    la-la-la # undefined variable error
catch e
    print(e)
end

# do blocks

##############################################################################
################################  functions  #################################
##############################################################################

# A function is a collected group of instructions that can return one or more values, possibly based on the input arguments.

# Single expression functions
f(x) = x * x
g(x, y) = sqrt(x^2 + y^2)

# Functions with multiple expressions
function say_hello(name)
    println("hello ", name)
end
say_hello("vivek")

# return value
function add_numbers(a,b)
    return a+b
end
add_numbers(2,3)

# return multiple values
function add_multiply_numbers(a, b=10) # we can supply default values as well
    return(a+b, a*b)
end
add_multiply_numbers(2,3)
add_multiply_numbers(2)

# supplying variable number of arguments
function show_args(args...)
    for arg in args
        println(arg," ")
    end
end
show_args(10,20,25,35,50)

# Anonymous functions — functions with no name
map((x,y,z) -> x + y + z, [1,2,3], [4, 5, 6], [7, 8, 9])

# Map - If you already have a function and an array, you can call the function for each element of the array by using map()
a=1:10;
map(sin, a) # map() returns a new array but if you call map!() , you modify the contents of the original array

# The map() function collects the results of some function working on each and every element of an iterable object, such as an array of numbers.
map(+, 1:10)

# The reduce() function does a similar job, but after every element has been seen and processed by the function, only one is left. The function should take two arguments and return one.
reduce(+, 1:10)

##############################################################################
################################  dict and sets  #############################
##############################################################################

dict = Dict("a" => 1, "b" => 2, "c" => 3)
dict = Dict{String,Integer}("a"=>1, "b" => 2) # If you know the types of the keys and values in advance, you can specify them after the Dict keyword, in curly braces
# looking things up
dict["a"]
values(dict) # to retrieve all values
keys(dict) # to retrieve all keys
# these can be useful for iterating
for k in keys(dict)
for (key, value) in dict

merge(d1, d2) # merge() function which can merge two dictionaries
findmin(d1) # find the minimum value in a dictionary, and return the value, and its key
filter((k, v) -> k == 1, d1)

# sort dict - you can use the SortedDict data type from the DataStructures.jl package
Pkg.add("DataStructures")
import DataStructures
dict = DataStructures.SortedDict("b" => 2, "c" => 3, "d" => 4, "e" => 5, "f" => 6)

# Sets - A set is a collection of elements, just like an array or dictionary, with no duplicated elements.
colors = Set{String}(["red","green","blue","yellow"])
push!(colors, "black")  # You can use push!() to add elements to a set

union(colors, rainbow) # The union of two sets is the set of everything that is in one or the other sets
intersect(colors, rainbow) # The intersection of two sets is the set that contains every element that belongs to both sets
setdiff(colors, rainbow) # The difference between two sets is the set of elements that are in the first set, but not in the second

##############################################################################
################################  str and char  ##############################
##############################################################################

"this is a string"

# double quotes and dollar signs need to be preceded (escaped) with a backslash
"""this is "a" string with double quotes""" # triple double quotes can be used to store strings with double quotes in them

# special strings
r" " indicates a regular expression
v" " indicates a version string
b" " indicates a byte literal
raw" " indicates a raw string that doesn't do interpolation

# string interpolation - use the results of Julia expressions inside strings.
x = 42
"The value of x is $(x)." # "The value of x is 42."

# Substrings - To extract a smaller string from a string, use getindex(s, range) or s[range] syntax
str ="a load of characters"
str[3:6] # "load"
str[3:end-6] # "load of char"

isascii(str) # functions tests whether a string is ASCII or contains Unicode characters

length(str) # to find the length of a string
lastindex(str) # to find index of last char of string

for char in s  # iterate through a string
    print(char, "_")
end

# Get index of all char in a string
for i in eachindex(str)
    @show su[i]
end

# Splitting and joining strings
"s" * "t"  # can stick strings together (a process often called concatenation) using the multiply (*) operator
string("s", "t")

"s" ^ 18  # can 'multiply' strings, you can also raise them to a power

split("You know my methods, Watson.") # by default splits on space
split("You know my methods, Watson.", 'W') # splits on the char W
# If you want to split a string into separate single-character strings, use the empty string ("")

split("You know my methods, Watson.", r"a|e|i|o|u", false) # splits string on the char that matches any of the vowels
# false makes sure that empty strings are not returned

join(split(s, r"a|e|i|o|u", false), "aiou") # You can join the elements of a split string in array form using join()

# Converting between numbers and strings
a = BigInt(2)^200
a=string(a) # convert number to string
parse(BigInt, a) # convert strings to numbers

# Finding and replacing things inside strings
s = "My dear Frodo";
in('M', s) # true
occursin("Fro", s) # true
findfirst("My", s) # 1:2
replace(s, "Frodo" => "Frodo Baggins")
uppercase(s)

# There are lots of functions for testing and changing strings:
    length(str) length of string
    sizeof(str) length/size
    startswith(strA, strB) does strA start with strB?
    endswith(strA, strB) does strA end with strB?
    occursin(strA, strB) does strA occur in strB?
    all(isletter, str) is str entirely letters?
    all(isnumeric, str) is str entirely number characters?
    isascii(str) is str ASCII?
    all(iscntrl, str) is str entirely control characters?
    all(isdigit, str) is str 0-9?
    all(ispunct, str) does str consist of punctuation?
    all(isspace, str) is str whitespace characters?
    all(isuppercase, str) is str uppercase?
    all(islowercase, str) is str entirely lowercase?
    all(isxdigit, str) is str entirely hexadecimal digits?
    uppercase(str) return a copy of str converted to uppercase
    lowercase(str) return a copy of str converted to lowercase
    titlecase(str) return copy of str with the first character of each word converted to uppercase
    uppercasefirst(str) return copy of str with first character converted to uppercase
    lowercasefirst(str) return copy of str with first character converted to lowercase
    chop(str) return a copy with the last character removed
    chomp(str) return a copy with the last character removed only if it's a newline

##############################################################################
################################  text files  ################################
##############################################################################

# A simple application of a dictionary is to count how many times each word appears in a piece of text.
f = open("sherlock-holmes-canon.txt")
wordlist = String[]
for line in eachline(f)
    words = split(line, r"\W")
    map(w -> push!(wordlist, lowercase(w)), words)
end
filter!(!isempty, wordlist)
close(f)
wordcounts = Dict{String,Int64}() # To store the words and the word counts, we'll create a dictionary
# To build the dictionary, loop through the list of words, and use get() to look up the current tally, if any.
# If the word has already been seen, the count can be increased.
# If the word hasn't been seen before, the fall-back third argument of get() ensures that the absence doesn't cause an error, and 1 is stored instead
for word in wordlist
    wordcounts[word]=get(wordcounts, word, 0) + 1
end
wordcounts["watson"] # Now you can look up words in the wordcounts dictionary and find out how many times they appear
# Dictionaries aren't sorted, but you can use the collect() and keys() functions on the dictionary to collect the keys and then sort them.
# In a loop you can work through the dictionary in alphabetical order
for i in sort(collect(keys(wordcounts)))
    println("$i, $(wordcounts[i])")
end
# But how do you find out the most common words?
# One way is to use collect() to convert the dictionary to an array of tuples, and then to sort the array by looking at the last value of each tuple
sort(collect(wordcounts), by = tuple -> last(tuple), rev=true)
# To see only the top 20 words
sort(collect(wordcounts), by = tuple -> last(tuple), rev=true)[1:20]
# In a similar way, you can use the filter() function to find, for example, all words that start with "k" and occur less than four times
filter(tuple -> startswith(first(tuple), "k") && last(tuple) < 4, collect(wordcounts))

# Read a text files
f = "/tmp/adventures-of-sherlock-holmes.txt"
text = read(f, String);

# Lets see how to read write in an organized way
f = open("sherlock-holmes.txt") # To read text from a file, first obtain a file handle:
close(f) # When you've finished with the file, you should close the connection

# If you use the following then you dont need to close
# The open file is automatically closed when this block finishes
open("sherlock-holmes.txt") do file
    # do stuff with the open file
end

# These functions will be useful for working with filenames:
    cd(path) changes the current directory
    pwd() get the current working directory
    readdir(path) returns a lists of the contents of a named directory, or the current directory,
    abspath(path) adds the current directory's path to a filename to make an absolute pathname
    joinpath(str, str, ...) assembles a pathname from pieces
    isdir(path) tells you whether the path is a directory
    splitdir(path) - split a path into a tuple of the directory name and file name.
    splitdrive(path) - on Windows, split a path into the drive letter part and the path part. On Unix systems, the first component is always the empty string.
    splitext(path) - if the last component of a path contains a dot, split the path into everything before the dot and everything including and after the dot. Otherwise, return a tuple of the argument unmodified and the empty string.
    expanduser(path) - replace a tilde character at the start of a path with the current user's home directory.
    normpath(path) - normalize a path, removing "." and ".." entries.
    realpath(path) - canonicalize a path by expanding symbolic links and removing "." and ".." entries.
    homedir() - current user's home directory.
    dirname(path) - get the directory part of a path.
    basename(path)- get the file name part of a path.

##############################################################################
################################  date / time  ###############################
##############################################################################

# There are three main datatypes available:
    A Dates.Time object represents a precise moment of time in a day. It doesn't say anything about the day of the week, or the year, though. It's accurate to a nanosecond.
    A Dates.Date object represents just a date: no time zones, no daylight saving issues, etc... It's accurate to, well, a day.
    A Dates.DateTime object is a combination of a date and a time of day, and so it specifies an exact moment in time. It's accurate to a millisecond or so.

using Dates
import Dates # on of these is ok

Dates.Time(Dates.now()) # a Dates.Time object
Dates.Date(1997,3,15)   # a Dates.Date object
Dates.DateTime(1918,11,11,11,11,11) # a Dates.DateTime object
Dates.today() # returns a Date object for the current date
Dates.now() # function returns a DateTime object for the current instant in time
Dates.now(Dates.UTC)

Dates.year
Dates.month
Dates.day
Dates.minute
Dates.hour
Dates.second

Dates.dayofweek
Dates.dayname
Dates.yearmonth
Dates.yearmonthday
Dates.isleapyear
Dates.daysofweekinmonth
Dates.monthname
Dates.monthday
Dates.dayofweekofmonth

Dates.today() - Dates.Date(1997,3,15) # returns difference in number of days
Dates.today() - Dates.Month(6) # date 6 months ago

# Date formatting
Dates.Date("Fri, 15 Jun 2018", "e, d u y")

y  Year digit eg yyyy => 2015, yy => 15
m  Month digit eg m => 3 or 03
u  Month name eg Jan
U  Month name eg January
e  Day of week eg Tue
E  Day of week eg Tuesday
d  Day eg 3 or 03
H  Hour digit eg HH => 00
M  Minute digit eg MM => 00
S  Second digit eg S => 00
s  Millisecond digit eg .000

# UNIX TIME
time() # function, used without arguments, returns the Unix time value of the current second
Libc.strftime(time()) # ("string format time") function, which lives in the Libc module, converts a number of seconds in Unix time to a more readable form

strptime() # function takes a format string and a date string, and returns a TmStruct expression. This can then be converted to a Unix time value by passing it to time()
time(Libc.strptime("%Y-%m-%d","2014-10-1"))
Libc.strftime(time(Libc.strptime("%Y-%m-%d","2014-10-1")))

unix2datetime() # function, which converts a Unix time value to a date/time object
Dates.unix2datetime(time())

# @elapsed macro returns the number of seconds an expression took to evaluate
function test(n)
    for i in 1:n
        x = sin(rand())
    end
end
@elapsed test(100000000)

##############################################################################
################################  plotting  ##################################
##############################################################################

##############################################################################
################################  metaprogramming  ###########################
##############################################################################

##############################################################################
################################  modules / packages  ########################
##############################################################################

##############################################################################
################################  dataframes  ################################
##############################################################################

import Pkg

Pkg.add("CSV")
Pkg.add("DataFrames") # Julia DataFrames package is an alternative to Python's Pandas package

Pkg.add("DataFramesMeta") # Julia-only packages Query.jl and DataFramesMeta.jl can also be used with DataFrames
Pkg.add("Query")

Pkg.add("Pandas") # but can be used with Pandas using the Pandas.jl wrapper package

Pkg.add("RDatasets") # contains a number of famous datasets

# Loading data into DataFrames
# option 1 - copy/paste data
import DataFrames
anscombe = DataFrame( # copy/paste several rows of data, convert them to an array,
 [10  10  10  8   8.04   9.14  7.46   6.58;
  8   8   8   8   6.95   8.14  6.77   5.76;
  13  13  13  8   7.58   8.74  12.74  7.71;
  9   9   9   8   8.81   8.77  7.11   8.84;
  11  11  11  8   8.33   9.26  7.81   8.47;
  14  14  14  8   9.96   8.1   8.84   7.04;
  6   6   6   8   7.24   6.13  6.08   5.25;
  4   4   4   19  4.26   3.1   5.39   12.5;
  12  12  12  8   10.84  9.13  8.15   5.56;
  7   7   7   8   4.82   7.26  6.42   7.91;
  5   5   5   8   5.68   4.74  5.73   6.89], :auto);
rename!(anscombe, [Symbol.(:X, 1:4); Symbol.(:Y, 1:4)]) # rename the column names

# option 2 - collected datasets from the RDatasets package
import RDatasets
anscombe = dataset("datasets","anscombe")

# option 3 - Empty DataFrames
df = DataFrame(A = 1:6, B = 100:105) # by providing the information about rows, and column names, in arrays

df = DataFrame(Name=String[],  # create a completely empty DataFrame by supplying the column names and define their types
    Width=Float64[],
    Height=Float64[],
    Mass=Float64[],
    Volume=Float64[])

df = vcat(df, DataFrame(Name="Test", Width=1.0, Height=10.0, Mass=3.0, Volume=5.0)) # Add rows to a dataframe

# Basics
typeof(anscombe)

https://en.wikibooks.org/wiki/Introducing_Julia/DataFrames