# Newton's method

x0 = 1  # The initial guess
f(x) = x^2 - 2  # The function whose root we are trying to find
fprime(x) = 2 * x  # The derivative of the function
tolerance = 10^(-7)  # 7 digit accuracy is desired
epsilon = 10^(-14)  # Do not divide by a number smaller than this
maxIterations = 20  # Do not allow the iterations to continue indefinitely
solutionFound = false  # Have not converged to a solution yet

for i = 1:maxIterations
  y = f(x0)
  yprime = fprime(x0)

  if abs(yprime) < epsilon  # Stop if the denominator is too small
    break
  end

  global x1 = x0 - y/yprime  # Do Newton's computation

  if abs(x1 - x0) <= tolerance  # Stop when the result is within the desired tolerance
    global solutionFound = true
    break
  end

  global x0 = x1  # Update x0 to start the process again
end

if solutionFound
  println("Solution: ", x1)  # x1 is a solution within tolerance and maximum number of iterations
else
  println("Did not converge")  # Newton's method did not converge
end

Code/Numerical Analysis & Machine Learning/Newton Method/NewtonMethod.jl


# Regression Metrics

"""
    mae(y_pred, y_true)

Mean Absolute Error. Calculated as `sum(|y_true .- y_pred|) / length(y_true)` based on provided `y_pred` and `y_true`.
"""
function mae(y_pred, y_true)
    @assert length(y_true) == length(y_pred)
    return sum(abs.(y_true .- y_pred)) / length(y_true)
end

"""
    mse(y_pred, y_true)

Mean Squared Error. Calculated as `sum((y_true .- y_pred).^2) / length(y_true)` based on provided `y_pred` and `y_true`.
"""
function mse(y_pred, y_true)
    @assert length(y_true) == length(y_pred)
    return sum((y_true .- y_pred).^2) / length(y_true)
end

"""
    male(y_pred, y_true)

Mean Absolute Logarithmic Error. Calculated as `sum(|log.(y_true) .- log.(y_pred)|) / length(y_true)` based on provided `y_pred` and `y_true`.
"""
function male(y_pred, y_true)
    @assert length(y_true) == length(y_pred)
    epsilon = eps(eltype(y_pred)(1.0))
    return sum(abs.(log.(y_true .+ epsilon) .- log.(y_pred .+ epsilon)))  / length(y_true)
end

"""
    msle(y_pred, y_true)

Mean Absolute Logarithmic Error.
Calculated as `sum((log.(y_true) .- log.(y_pred)).^2) / length(y_true)` based on provided `y_pred` and `y_true`.
"""
function msle(y_pred, y_true)
    @assert length(y_true) == length(y_pred)
    epsilon = eps(eltype(y_pred)(1.0))
    return sum((log.(y_true .+ epsilon) .- log.(y_pred .+ epsilon)).^2)  / length(y_true)
end

"""
    r2_score(y_pred, y_true)

Calculates the r2 (Coefficient of Determination) score for the provided `y_pred` and `y_true`.
Best possible score is 1.0 and it can be negative (because the model can be arbitrarily worse).
A constant model that always predicts the expected value of y, disregarding the input features, would get a r2_score of `0.0`.
"""
function r2_score(y_pred, y_true)
    @assert length(y_true) == length(y_pred)
    ss_res = sum((y_true .- y_pred).^2)
    mean = sum(y_true) / length(y_true)
    ss_total = sum((y_true .- mean).^2)
    return 1 - ss_res/(ss_total + eps(eltype(y_pred)))
end

"""
    adjusted_r2_score(y_pred, y_true, n)

Modified version of `r2_score` that has been adjusted for the number of predictors in the model. Here the argument `n` is for the number of predictors(or independent variables in X).

See also: [`r2_score`](@ref)
"""
function adjusted_r2_score(y_pred, y_true, n)		# n -> number of predictors(independent variables in X)
    @assert length(y_true) == length(y_pred)
    score = r2_score(y_pred, y_true)
    epsilon = eps(eltype(y_pred)(1.0))
    return 1 - ((1 - score) * (length(y_true) -1)) / abs(( length(y_true) - n - 1 + epsilon))
end


using NewtonsMethod
using Test

using LinearAlgebra, Statistics, Compat, ForwardDiff

@testset "NewtonsMethod.jl" begin

#This function has no root, it should return nothing
f1(x) = x^2+1
f1_prime(x) = 2*x

@test newtonroot(f1, f1_prime,x₀ = 0).root == nothing
@test newtonroot(f1, x₀ = 0).root == nothing

#Now, I will test some functions which I know the roots
f2(x) = (x - 1)^3
f2_prime(x) = 3*((x-1)^2)

f3(x) = 4*x+5
f3_prime(x) = 4

f4(x) = 5^x - 25
f4_prime(x) =  5^x * log(5)

@test newtonroot(f2, f2_prime,x₀ = 0).root ≈ 0.9999998643434097
@test newtonroot(f2, x₀ = 0).root ≈ 0.9999998643434097

@test newtonroot(f3, f3_prime,x₀ = 0).root ≈ -1.25
@test newtonroot(f3, x₀ = 0).root ≈ -1.25

@test newtonroot(f4, f4_prime,x₀ = 0).root ≈ 2
@test newtonroot(f4, x₀ = 0).root ≈ 2

 #Testing tolerance
 @test !(newtonroot(f2, f2_prime, x₀ = 0, tolerance = 1).root ≈ 0.9999998643434097)
 @test !(newtonroot(f2, x₀ = 0, tolerance = 1).root ≈ 0.9999998643434097)
 @test newtonroot(f2, f2_prime, x₀ = 0, tolerance = 1E-10).root ≈ 1 #with smaller tolerance, I can reach the "exact" number

 #Testing BigFloat
 @test newtonroot(f2, f2_prime, x₀ = BigFloat(0.0), tolerance = 1E-40).root ≈ 1
 @test newtonroot(f2, x₀ = BigFloat(0.0), tolerance = 1E-40).root ≈ 1

 #Testing if the maxiter works as intended
   #Function 2
   @test newtonroot(f2, f2_prime, x₀ = 0, maxiter = 10).root == nothing
  @test newtonroot(f2, x₀ = 0, maxiter = 10).root == nothing
  # Function 4
   @test newtonroot(f4, f4_prime, x₀ = 0, maxiter = 10).root == nothing
   @test newtonroot(f4,x₀ = 0, maxiter = 10).root == nothing

end


using LinearAlgebra
using Statistics
using StatsBase

function linear_regression(X, y)
    Q, R = modified_gram_schmidt(X)
    f = transpose(Q) * y
    betas = backsolve(R, f)
    return betas
end

# Helper Functions

function back_substitute(U, y)
    n, _ = size(U)
    x = zeros(n)
    for i in n:1
        x[i] = y[i]
        for j in (i+1): n
            x[i] = x[i] - U[i, j] * x[j]
        end
        x[i] = x[i] / U[i, i]
    end
    return x
end

function back_solve(A, b)
    Q,R = modified_gram_schmidt(A)
    y = tranpose(Q) * b
    x = back_substitute(R, y)
    return x
end

function classical_gram_schmidt(A)
    m, n = size(A)
    R = zeros(n, n)
    Q = zeros(m, n)
    for j in 1:n
        v_j = A[:, j]
        for i in 1:(j-1)
            R[i, j] = dot(Q[:, i], A[:, j])
            v_j = v_j - R[i, j] * Q[:, i]
        end
        R[j, j] = norm(v_j)
        Q[:, j] = v_j / R[j, j]
    end
    return Q, R
end

function modified_gram_schmidt(A)
    m, n = size(A)
    R = zeros(Float64, n, n)
    Q = zeros(Float64, m, n)

    V = zeros(Float64, n, n)
    for i in 1:n
        V[:, i] = A[:, i]
    end

    for i in 1:n
        R[i, i] = norm(V[:, i])
        Q[:, i] = V[:, i] / R[i, i]
        for j in (i+1):n
            R[i, j] = dot(Q[:, i], V[:, j])
            V[:, j] = V[:, j] - R[i, j] * Q[:, i]
        end
    end
    return Q, R
end

function check_stability(Q)
    m, n = size(Q)
    total_dot_error = 0
    for i in 1:m
        for j in 1:n
            if i != j
                total_dot_error += abs(dot(Q[:, i], Q[:, j]))
            end
        end
    end
    return total_dot_error
end

function boostrap_confint(X, y, bootstrap_samples)
    n, d = size(X)
    beta_arr = zeros(bootstrap_samples, d)

    for i in 1:bootstrap_samples
        sample_arr = sample(1:n,n)
        sampled_X = X[sample_arr, :]
        sampled_y = y[sample_arr]
        beta_arr[i, :] = linear_regression(sampled_X, sampled_y)
    end

    betas = linear_regression(X,y)
    confidence_intervals = zeros(n, 2)

    for i in 1:n
        beta_i = beta_arr[i, :]
        beta_std = std(beta_i)
        confidence_intervals[i, 1] = betas[i] - 1.96 * beta_std
        confidence_intervals[i, 2] = betas[i] + 1.96 * beta_std

    end
    return confidence_intervals
end

# Very Simple Stability Analysis

A = [1 2 3; 4 1 6; 7 8 1]
Q, R = classical_gram_schmidt(A)

classical_error = check_stability(Q)
Q, R = modified_gram_schmidt(A)
modified_error = check_stability(Q)

errror_ratios = classical_error / modified_error
println("classical error $classical_error")
println("modified error $modified_error")
println("Error ratios: $errror_ratios")


"""
    factorial_iterative(n)

Finds factorial of a number using Iterative method

# Example
```julia
factorial_iterative(5)      # returns 120
factorial_iterative(-1)     # returns error
```
# Reference
- factorial of a positive integer -- https://en.wikipedia.org/wiki/Factorial

Contributed By:- [Ashwani Rathee](https://github.com/ashwani-rathee) and [Rratic](https://github.com/Rratic)
"""
function factorial_iterative(n::N) where {N<:Integer}
    if n < 0
        throw(
            error(
                "factorial_iterative() only accepts non-negative integral values",
            ),
        )
    end
    if n == 0 || n == 1
        return one(BigInt)
    end
    return n * factorial_iterative(n - 1)
end

"""
    factorial_recursive(n)

Finds factorial of anumber using recursive method

# Example
```julia
factorial_recursive(5)      # returns 120
```
# Reference
- factorial of a positive integer -- https://en.wikipedia.org/wiki/Factorial

Contributed By:- [Ashwani Rathee](https://github.com/ashwani-rathee) and [Rratic](https://github.com/Rratic)
"""
function factorial_recursive(n::N)::BigInt where {N<:Integer}
    if n < 0
        throw(
            error(
                "factorial_iterative() only accepts non-negative integral values",
            ),
        )
    end
    if n == 0 || n == 1
        return one(BigInt)
        # keep same result type
    else
        factorial = one(BigInt)
        for i in 1:n
            factorial *= i
        end
        return factorial
    end
end


"""
    collatz_sequence(n)

Collatz conjecture: start with any positive integer n. The next term is
obtained as follows:
	If n term is even, the next term is: n / 2 .
	If n is odd, the next term is: 3 * n + 1.
The conjecture states the sequence will always reach 1 for any starting value n.
"""

function collatz_sequence(n)
	sequence = [n]
	while n != 1
		n = n % 2 == 1 ? 3 * n + 1 : div(n, 2)
		append!(sequence, n)
	end
	return sequence
end


using NewtonsMethod
using Test

@testset "NewtonsMethod.jl" begin
    # Write your own tests here.

# return back nothing if non-convergence: wrote in function

# test of non-convergence

f(x)=2+x^2
f_prime(x)=2*x
t1=newtonroot(f, f_prime; x_0=0, tolerance=1E-7, maxiter=1000)[1]
t2=newtonroot1(f; x_0=0, tolerance=1E-7, maxiter=1000)[1]
@test t1 == nothing
@test t2 == nothing

# root of a known function
f(x)=x^2-2x+1
f_prime(x)=2*x-2
t3=newtonroot(f, f_prime; x_0=0, tolerance=1E-7, maxiter=1000)[1]
@test t3 ≈ 1.0

# automatic differentiation
t4=newtonroot2(f; x_0=0, tolerance=1E-7, maxiter=1000)[1]
@test t4 ≈ 1.0

# finding those roots with a BigFloat
t5=newtonroot(f, f_prime; x_0=BigFloat(x0), tolerance=1E-7, maxiter=1000)[1]
@test t5 ≈ 1.0

# several of above tests

f(x)=x^2-2x
f_prime(x)=2*x-2

t3=newtonroot(f, f_prime; x_0=1.5, tolerance=1E-7, maxiter=1000)[1]
@test t3 ≈ 2.0
t4=newtonroot2(f; x_0=1.5, tolerance=1E-7, maxiter=1000)[1]
@test t4 ≈ 2.0
t5=newtonroot(f, f_prime; x_0=BigFloat(1.5), tolerance=1E-7, maxiter=1000)[1]
@test t5 ≈ 2.0

f(x)=x^2-3x+2
f_prime(x)=2*x-3

t3=newtonroot(f, f_prime; x_0=0, tolerance=1E-7, maxiter=1000)[1]
@test t3 ≈ 1.0
t4=newtonroot2(f; x_0=0, tolerance=1E-7, maxiter=1000)[1]
@test t4 ≈ 1.0
t5=newtonroot(f, f_prime; x_0=BigFloat(0), tolerance=1E-7, maxiter=1000)[1]
@test t5 ≈ 1.0

# ensure that the maxiter is working
f(x)=x^2-2x+1
f_prime(x)=2*x-2

t6=newtonroot(f, f_prime; x_0=0, tolerance=1E-7, maxiter=5)[1]
@test t6 == nothing

# ensure that tol is working
t7=newtonroot(f, f_prime; x_0=0, tolerance=1E-7, maxiter=1000)[2]
@test t7 <= 1E-7

end


"""
    simpsons_integration(f::Function, a::Real, b::Real, n::Int)

Simpson's rule uses a quadratic polynomial on each subinterval of a partition to approximate the function f(x) and to compute the definite integral.
This is an improvement over the trapezoid rule which approximates f(x) by a straight line on each subinterval of a partition.
For more details of the method, check the link in the reference.

# Arguments
- `f`: The function to integrate. (ar the moment only single variable is suported)
- `a`: Start of the integration limit.
- `b`: End of the integration limit.
- `n`: Number of points to sample. (as n increase, error decrease)

# Examples/Test
```julia
# aproximate pi with f(x) = 4 / (1 + x^2)
julia> simpsons_integration(x -> 4 / (1 + x^2), 0, 1, 100_000)
3.1415926535897936
julia> simpsons_integration(x -> 4 / (1 + x^2), 0, 1, 100_000) ≈ pi
true
```

# References:
- https://personal.math.ubc.ca/~pwalls/math-python/integration/simpsons-rule/

# Contributed By:- [AugustoCL](https://github.com/AugustoCL)
"""
function simpsons_integration(f::Function, a::Real, b::Real, n::Int)
    # width of the segments
    Δₓ = (b - a) / n

    # rules of the method (check link in references)
    a₁(i) = 2i - 2
    a₂(i) = 2i - 1

    # sum of the heights
    Σ = sum(1:n/2) do i
            f(a + a₁(i)*Δₓ) + 4f(a + a₂(i)*Δₓ) + f(a + 2i*Δₓ)
        end

    # approximate integral of f
    return (Δₓ / 3) * Σ
end


function calc_nchk(n::Int,k::Int)
    #=
    Calculate n choose k
    =#
    if n == 0 || k == 0
		return 1
	end

    #@myassert(n>=k)
    accum::BigInt = 1
    for i in 1:k
        accum = accum * (n-k+i) ÷ i
    end
    return accum
end

function build_nchk_table(N)
	binom_coeff = Array{BigInt,2}(undef,N+1,N+1)
	for i in 0:N
    		for j in i:N
        		binom_coeff[j+1,i+1] = calc_nchk(j,i)
    		end
    	end
	return binom_coeff
end


"""
    bab_sqrt(S::Real; tolerance = 1e-6, guess = nothing)

The Babylonian Method of calculating a square root is a simple iterative method to determine square roots. A full description of the algorithm can be found on [Wikipedia](https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method)

# Arguments:
- `S`: The number to calculate the square root for.

# Positional Arguments
- `tolerance`: How close the square of the square root needs to be from the input value. `abs(S - xn^2) < tolerance`
- `guess`: The initial value to use for `xn`

# Examples/Tests
```julia
julia> bab_sqrt(100)
10.000000000107445

julia> bab_sqrt(100, guess = 15)
10.000000000131072

julia> bab_sqrt(π, guess = 1)
1.7724538555800293

julia> bab_sqrt(π, guess = 1, tolerance = 2)
2.0707963267948966
```

# Algorithm:

```julia
while tolerance <= abs(xn^2 - S)
    xn = (1 / 2) * (xn + S / xn)
end
```

# References:
[Methods of computing square roots](https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method)

```

# Contributed by:- [Anson Biggs](https://ansonbiggs.com)
"""
function bab_sqrt(S::Real; tolerance = 1e-6, guess = nothing)

    # Can only calculate for positive numbers
    if S < 0
        throw(DomainError("Input must be greater than zero"))
    end

    xn = guess
    if xn === nothing
        xn = S / 2
    elseif xn < 0
        xn = abs(xn)
    end

    while tolerance <= abs(xn^2 - S)
        xn = (1 / 2) * (xn + S / xn)
    end
    return xn

end


"""
    trapezoid_integration(f::Function, a::Real, b::Real, n::Int)

The trapezoidal rule works by approximating the region under the graph of the function f(x) as a trapezoid and calculating its area.

# Arguments
- `f`: the function to integrate. (at the momment only single variable is suported)
- `a`: Start of the integration limit.
- `b`: End of the integration limit.
- `n`: Number of points to sample. (as n increase, error decrease)

# Examples/Test
```julia
julia> trapezoid_integration(x -> 4 / (1 + x^2), 0, 1, 100_000)
3.1415926535731526
julia> trapezoid_integration(x -> 4 / (1 + x^2), 0, 1, 100_000) ≈ pi
true
```
# References:
-https://personal.math.ubc.ca/~pwalls/math-python/integration/trapezoid-rule/
-https://en.wikipedia.org/wiki/Trapezoidal_rule

# Contributed By:- [AugustoCL](https://github.com/AugustoCL)
"""
function trapezoid_integration(f::Function, a::Real, b::Real, n::Int)
    # width of the trapezoids
    Δₓ = (b - a) / n

    # sum of the height of the trapezoids
    Σ = 0.5 * f(a) + sum(f(i) for i in (a+Δₓ):Δₓ:(b-Δₓ)) + 0.5 * f(b)

    # approximate integral of f
    return Δₓ * Σ
end


using Newtonsmethod
using Test
using LinearAlgebra

# Testing the first function (unit test)
f(x) = x^2-16.0
f′(x) = 2.0*x
@test newtonroot(f,f′;x₀=10.0)[1] ≈ 4.0 atol=0.00001

f(x) = log(x)-1.0
f′(x) = 1/x
@test newtonroot(f,f′;x₀=3.0)[1] ≈ 2.71828182 atol=0.00001

f(x) = exp(x) - 2.5
f′(x) = exp(x)
@test newtonroot(f,f′;x₀=10.0)[1] ≈ 0.9162907318741551 atol=0.00001

# Testing the second function
f(x) = x^2-16.0
@test newtonroot(f;x₀=10.0)[1] ≈ 4.0 atol=0.00001

f(x) = log(x)-1.0
@test newtonroot(f;x₀=3.0)[1] ≈ 2.71828182 atol=0.00001

f(x) = exp(x) - 2.5
@test newtonroot(f;x₀=10.0)[1] ≈ 0.9162907318741551 atol=0.00001

# Testing the BigFloat
f(x) = exp(x) - 2.5
@test newtonroot(f;x₀ = BigFloat(10.0), tolerance = BigFloat(1E-10), maxiter = BigFloat(1000))[1] ≈ 0.9162907318741551 atol=0.00001

# Testing non-convergence
f(x) = x^2+2.0
f′(x) = 2.0*x
@test newtonroot(f,f′;x₀=1.0) == nothing

# Testing maxiter
f(x) = exp(x) - 2.5
a=newtonroot(f;x₀=10.0)[1]
b=newtonroot(f;x₀=10.0,maxiter=5)
@testset "maxiter" begin
    @test a ≈ 0.9162907318741551 atol=0.00001
    @test b == nothing
end;

# Testing tolerance level
g(x) = exp(x) - 2.5
x₁ = newtonroot(g;x₀=10.0)[1]
x₂ = newtonroot(g;x₀=10.0,tolerance=1E-3)[1]
x₃ = newtonroot(g;x₀=10.0,tolerance=1)[1]
@testset "tolerance" begin
    @test norm(g(x₁)) < norm(g(x₂))
    @test norm(g(x₁)) < norm(g(x₃))
    @test norm(g(x₂)) < norm(g(x₃))
end;

# Give a fail test
# g(x) = exp(x) - 2.5
# @test newtonroot(g;x₀=1.0)[1] == 1.0


function ctrap(f::Function, a::Number, b::Number, n::Number)
  #= Numerical integration of functions using the trapezium rule
  Keyword Arguments:
    f -- Anonymous function to integrate
    a -- Lower limit of integration
    b -- Upper limit of integration
    n -- The number of sections to use in the calculation
  =#
  if n <= 0
    error("n must be a positive integer");
  end
  h = (b - a) / n;
  0.5 * h * (f(a) + 2*sum(f([a+h:h:b-h])) + f(b));
end

function richardson(f::Function, a::Number, b::Number, n::Number, n2::Number)
  #= Numerical integration of functions using Richardson's Method
  Keyword Arguments:
    f -- Anonymous function to integrate
    a -- Lower limit of integration
    b -- Upper limit of integration
    n -- Number of intervals to use for 1st estimate
    n2 -- Number of intervals to use for 2nd estimate.
  =#

  # Error checks
  if n <= 0 || n2 <= 0
    error("n and n2 must be positive integers.");
  end

  if n == n2
    error("n and n2 can't be equal.");
  end

  I1, I2 = ctrap(f, a, b, n), ctrap(f, a, b, n2);
  h1, h2 = (b - a) / n, (b - a) / n2;

  I2 + ((I1 - I2) / (1 - (h1 / h2)^2));
end

function simpsonInt(f::Function, x1::Number, x2::Number, x3::Number)
  #= Numerical integration of functions using Simpson's 1/3 Rule

  Keyword arguments:
    f -- Function to integrate
    x1 -- Lower limit of integration
    x2 -- Midpoint of integral limits
    x3 -- Upper limit of integration

  Returns:
    The results of ∫f between x1 and x3
  =#
  (x3 - x1) * (1//6) * (f(x1) + 4*f(x2) + f(x3));
end

function simpsonInt(f::Function, x1::Number, xn::Number, n::Int)
  #= Numerical integration using Simpson's Rule

  Will select the best method for integration depending on the number of data
  points provided. If n is odd, Simpson's Composite 1/3 rule will be used. If
  n is a multiple of 3, Simpson's composite 3/8 rule will be used.

  Keyword arguments
    f -- Function to integrate
    x1 -- Lower limit of integration.
    xn -- Upper limit of integration.
    n -- Number of data points to use

  Returns
    The results of ∫f between x1 and xn.
  =#
  if n <= 1
    error("Insufficient data points. Try increasing n.");
  elseif isodd(n)
    simpsonOneThirdComposite(f, x1, xn, n);
  elseif n%3 == 0
    simpsonThreeEigthComposite(f, x1, xn, n);
  else
    error("Unusable number of data points. Make sure n is ≥ 1, odd or a multiple of 3.");
  end
end

function simpsonOneThirdComposite(f::Function, x1::Number, xn::Number, n::Int)
  #= Numerical integration using Simpson's 1/3 Rule

  Keyword arguments:
    f -- Function to integrate
    x1 -- Lower limit of integration
    xn -- Upper limit of integration
    n -- Number of data points to use. **Must be odd**

  Returns:
    The results of ∫f between x1 and xn
  =#
  if n%2 == 0
    error("Must use an odd number of data points.");
  end
  h = (xn - x1) / (n - 1);
  (1//3) * h * (f(x1) + 4*(sum(f([x1+h:2h:xn-h]))) + 2*(sum(f([x1+2h:2h:xn-2h]))) + f(xn));
end

function simpsonThreeEigthComposite(f::Function, x1::Number, xn::Number, n::Int)
  #= Numerical integration using Simpson's 3/8 rule

  Keyword arguments:
    f -- Function to integrate
    x1 -- Lower limit of integration
    xn -- Upper limit of integration
    n -- Number of data points to use. **Must be a multiple of 3**.

  Returns:
    The result of ∫f between x1 and xn.
  =#
  if n%3 != 0
    error("Must use a multiple of three data points");
  end
  h = (xn - x1) / n;
  (3//8) * h * (f(x1) + 3*sum(f([x1+h:3h:xn-2h])) + 3*sum(f([x1+2h:3h:xn-h])) + 2*sum(f([x1+3h:3h:xn-3h])) + f(xn));
end


using Pkg
Pkg.activate(pwd())

# # Julia package
# ## Introduction

using DifferentialEquations

f(u,p,t) = 0.98*u

u0 = 1.0
tspan = (0.0, 1.0)

#+

prob = ODEProblem(f, u0, tspan)
sol = solve(prob)

#+

using Plots

plot(sol; label="")

#+

sol(0.8)

# ### Exercise:
#
# When calling the `solve` function, we can specify the interpolation way. Solve the ODE
# with linear interpolation (`dense=false`) and the Runge-Kutta method of the fourth order
# (`RK4()`). Plot the results and compare them with the default and original solutions.
#
# ---
# ### Solution:

sol2 = solve(prob, dense=false)
sol3 = solve(prob, RK4())

#+

ts = range(tspan...; length = 100)

plot(ts, t->exp(0.98*t), label="True solution", legend=:topleft);
plot!(ts, t->sol(t), label="Default");
plot!(ts, t->sol2(t), label="Linear");
plot!(ts, t->sol3(t), label="Runge-Kutta")

# ---
#
# ## Lorenz system

function lorenz(u, p, t)
    σ, ρ, β = p
    x_t = σ*(u[2]-u[1])
    y_t = u[1]*(ρ-u[3]) - u[2]
    z_t = u[1]*u[2] - β*u[3]
    return [x_t; y_t; z_t]
end

#+

u0 = [1.0; 0.0; 0.0]
p = [10; 28; 8/3]
tspan = (0.0, 100.0)

#+

prob = ODEProblem(lorenz, u0, tspan, p)
sol = solve(prob)

#+

plot(sol)

#+

plt1 = plot(sol, vars=(1,2,3), label="")

#+

plot(sol, vars=(1,2,3), denseplot=false; label="")

#+

traj = hcat(sol.u...)
plot(traj[1,:], traj[2,:], traj[3,:]; label="")

# ### Exercise:
#
# Use the `nextfloat` function to perturb the first parameter of `p` by the smallest
# possible value. Then solve the Lorenz system again and compare the results by plotting
# the two trajectories next to each other.
#
# ---
# ### Solution:

p0 = (nextfloat(p[1]), p[2:end]...)

#+

prob0 = ODEProblem(lorenz, u0, tspan, p0)
sol0 = solve(prob0)

#+

plt0 = plot(sol0, vars=(1,2,3), label="")
plot(plt1, plt0; layout=(1,2))

# ---

hcat(sol(tspan[2]), sol0(tspan[2]))


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

# 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 = jacobi(A,B,Xa);

julia> X
3-element Array{Float64,1}:
 1.9999999884128572
 3.9999999907302857
 3.0000000018539428

julia> flag
0

julia> err
3.777053140308286e-8

julia> M
18×5 Array{Float64,2}:
  0.0  1.0      2.0    2.0     NaN
  1.0  1.75     3.375  3.0       1.85826
  2.0  1.84375  3.875  3.025     0.509327
  3.0  1.9625   3.925  2.9625    0.143205
  ⋮
 14.0  2.0      4.0    3.0       1.00721e-6
 15.0  2.0      4.0    3.0       2.83194e-7
 16.0  2.0      4.0    3.0       1.37804e-7
 17.0  2.0      4.0    3.0       3.77705e-8
```
return solusi `X` dengan `flag` bernilai 0 jika metode jacobi berhasil menemukan
solusi dan gagal jika tidak 0. Serta, matriks `M` yang berisi catatan proses tiap
iterasi `[k, X', error]`
"""
function jacobi(A, B, Xawal)
    delta = 10^-7;
    maxi = 100;
    flag = 1;
    D = Diagonal(diag(A))
    R = A - D;
    X = Xawal[:];
    M = [0 X' NaN];
    for k = 1:maxi
        Xlama = X;
        X = inv(D)*(B - R*Xlama);
        err = norm(X-Xlama);
        M = [M; [k X' err] ];
        if err<delta || norm(B-A*X)<delta
            flag = 0;
            break
        end
    end
    err = M[end,end]
    return X, flag, err, M
end


#= Frobenius Dot product
   ---------------------

1st method of calculation

a) element-wise multiplication between 2 matrices (which have the same size)

b) Sum all elements
=#
Frb1(A,B) = reduce(+, (A .* B))

#= 2nd method
   ----------
a) Vectorise both matrices
b) Compute vector dot product

Vectorising a Matrix
--------------------

Create a vector from a matrix

e.g. [a c e; b d f]

goes to [a b c d e f]' (column-wise concatenation)
=#
function Frb2(A,B)
  va = A[:]
  vb = B[:]

  return va .* vb
end

#=
3rd way of Frobenius (most efficient)
-------------------------------------

take the trace of A' * B
=#
Frb3(A,B) = tr(A' * B)

#=
Frobenius Norm
--------------

Frobenius dot product of matrix with itself
=#

Fnorm(A) = sqrt(Frb3(A,A))

# Implementation in code
# ----------------------
using LinearAlgebra

m = 9; n = 4;

A = randn(m,n);
B = randn(m,n);

Av = A[:] # reshapes into a (mxn) x 1 column vector

Bv = B[:] # same for B

## Ways of calculating Frobenius Dot Product

# 1) Sum of Hadamard multiplication
Frb1(A,B) = reduce(+, (A .* B))

# 2) Dot product of vectorised matrices
Frb2(A,B) = A[:] .* B[:]

# 3) Trace of transposed A matrix multiplied by B matrix
Frb3(A,B) = tr(A' * B)

# Frobenius Norm aka Matrix Norm:
# 1) square root of trace of (A' * A)
Fnorm1(A) = sqrt(Frb3(A,A))

# 2) another way of doing Frobenius norm in Julia
Fnorm2(A) = norm(A, 2)

linear-algebra-code-challenges/Lecture 59 - Frobenius Dot Product.jl


#' ---
#' title: Integer Programming
#' ---

#' **Originally Contributed by**: Arpit Bhatia

#' While we already know how to set a variable as integer or binary in the `@variable` macro,
#' this tutorial covers other JuMP features for integer programming along with some modelling techniques.

using JuMP

#' ## Modelling Logical Conditions
#' Generally, in a mathematical programming problem, all constraints must hold.
#' However, we might want to have conditions where we have some logical conditions between constraints.
#' In such cases, we can use binary variables for modelling logical conditions between constraints.

#' ### Disjunctive Constraints (OR)
#' Suppose that we are given two constraints $a'x \geq b$ and $c' x \geq d$,
#' in which all components of $a$ and $c$ are non-negative.
#' We would like to model a requirement that at least one of the two constraints is satisfied.
#' For this, we defined a binary variable $y$ and impose the constraints:

#' $$
#' \begin{align*}
#' a' x \geq y b \\
#' c' x \geq (1 - y) d \\
#' y \in \{0,1\}
#' \end{align*}
#' $$

a = rand(1:100, 5, 5)
c = rand(1:100, 5, 5)
b = rand(1:100, 5)
d = rand(1:100, 5)

model = Model()
@variable(model, x[1:5])
@variable(model, y, Bin)
@constraint(model, a * x .>= y .* b)
@constraint(model, c * x .>= (1 - y) .* d);

#' ### Conditional Constraints ($\implies$)
#' Suppose we want to model that a certain linear inequality must be satisfied when some other event occurs.
#' i.e. for a binary variable $z$, we want to model the implication

#' $$
#' \begin{align*}
#' z = 1 \implies a^Tx\leq b
#' \end{align*}
#' $$

#' If we know in advance an upper bound $a^Tx\leq b$. Then we can write the above as a linear inequality

#' $$
#' \begin{align*}
#' a^Tx\leq b + M(1-z)
#' \end{align*}
#' $$

a = rand(1:100, 5, 5)
b = rand(1:100, 5)
m = rand(10000:11000, 5)

model = Model()
@variable(model, x[1:5])
@variable(model, z, Bin)
@constraint(model, a * x .<=  b .+ (m .* (1 - z)));
# If z was a regular Julia variable, we would not have had to use the vectorized dot operator

#' ### Boolean Operators on Binary Variables
#' The following table is useful when we want to model boolean operators in the form of
#' linear inequalities that can be given to a solver.

#' | Boolean Expression | Constraint                           |
#' |:----------         |                           ----------:|
#' | $z=x \lor y$       | $x \leq z,  y \leq z,  z \leq x+y$   |
#' | $z=x \land y$      | $x \geq z,  y \geq z,  z+1 \geq x+y$ |
#' | $z= \neg x$        | $z = 1 − x$                          |
#' | $x \implies y$     | $x \leq y$                           |
#' | $x \iff y$         | $x = y$                              |

#' ## Modelling Integer Variables

#' ### Integer Variables using Constraints
#' We can add binary and integer restrictions to the domain of each variable using the `@constraint` macro as well.

model = Model()

@variable(model, x)
@variable(model, y)
@constraint(model, x in MOI.ZeroOne())
@constraint(model, y in MOI.Integer());

#' ### Semi-Continuous Variables
#' A semi-continuous variable is a continuous variable
#' between bounds $[l,u]$ that also can assume the value zero. ie.
#' $$
#' x \in \{0\} \cup \{l,u\}
#' $$

l = 7.45
u = 22.22
@variable(model, a)
@constraint(model, a in MOI.Semicontinuous(l, u))

#' ### Semi-Integer Variables
#' A semi-integer variable is a variable which asummes integer values
#' between bounds $[l,u]$ and can also assume the value zero. ie.

#' $$
#' x \in \{0\} \cup (\{l,u\} \cap \mathbb{Z})
#' $$

l = 5
u = 34
@variable(model, b)
@constraint(model, b in MOI.Semiinteger(l, u))

#' Note that the bounds specified in `MOI.Semiinteger` must be integral otherwise it would throw an error.

#' ## Special Ordered Sets

#' ### Special Ordered Sets of type 1 (SOS1)
#' A Special Ordered Set of type 1 is a set of variables,
#' at most one of which can take a non-zero value, all others being at 0.
#' They most frequently apply where a set of variables are actually 0-1 variables:
#' in other words, we have to choose at most one from a set of possibilities.

@variable(model, u[1:3])
@constraint(model, u in MOI.SOS1([1.0, 2.0, 3.0]))

#' Note that we have to pass MOI.SOS1 a weight vector which is essentially an ordering on the variables.
#' If the decision variables are related and have a physical ordering, then the weight vector,
#' although not used directly in the constraint, can help the solver make a better decision in the solution process.

#' ### Special Ordered Sets of type 2 (SOS2)

#' A Special Ordered Set of type 2 is a set of non-negative variables,
#' of which at most two can be non-zero, and if two are non-zero these must be consecutive in their ordering.

@variable(model, v[1:3])
@constraint(model, v in MOI.SOS2([3.0, 1.0, 2.0]))

#' The ordering provided by the weight vector is more important in this case as
#' the variables need to be consecutive according to the ordering.
#' For example, in the above constraint, the possible pairs are:
#' * Consecutive
#'   * (`x[1]` and `x[3]`) as they correspond to 3 and 2 resp. and thus can be non-zero
#'   * (`x[2]` and `x[3]`) as they correspond to 1 and 2 resp. and thus can be non-zero
#' * Non-consecutive
#'   * (`x[1]` and `x[2]`) as they correspond to 3 and 1 resp. and thus cannot be non-zero


# Fourier Transform and visualization of which

using Plots
# idk why I just like to load plot function first
# - weired isn't it
gr()
# rotation factor

W_rotate(N::Int)=exp(-2*im*π/N)

W_rotate.(1:10)
plot(1:100,W_rotate.(1:100))#takes long if runned for the 1st time
plot(W_rotate.(1:100))#

#Amplitute can be calcd with abs(x::Complex)
abs(1+im)

# DFT and visualization

function dft(x::Array)
    n = length(x)
    r = Array{Any,1}(missing,n)
    #Note how sum is used to aggregate results after broadcasting
    for i in 1:n
        r[i]=sum(x.*exp(1).^(-(i-1)*collect(0:(n-1)).*(2*π*im/n)))
    end
    return(r)
end

x=1:.1:2π
y =sin.(x)
plot(x,y)
plot(1:length(x),dft(y))
plot(1:length(x),abs.(dft(y)))
plot(1:length(x),real.(dft(y)))
# FFT and visualization of FFT

function idft(x::Array)
    n = length(x)
    r = Array{Any,1}(missing,n)
    #Note how sum is used to aggregate results after broadcasting
    for i in 1:n
        r[i]=sum(x.*exp(1).^(+(i-1)*collect(0:(n-1)).*(2*π*im/n)))#change - to+
    end
    return(r)
end

#below inspired by
#https://www.mathworks.com/matlabcentral/answers/3187-fft-recursive-code-problem

function p2_fft_rec(f)
  n = length(f)
  if (n == 1)
    return(f)
  else
    a= p2_fft_rec(f[1:2:n])
    b= p2_fft_rec(f[2:2:n]).*exp(1).^(-2/n*π*im.*collect(0:1:(n/2)-1))
    return append!(a+b,a-b)
  end
end

function p2_fft(x)
  n = length(x)
  N = 2^(ceil(log2(n)))
  x = append!(x,repeat([0],Int64(N - n)))
  return(p2_fft_rec(x)[1:n])
end

#p2_fft(collect(1:4))
#inspired by
#https://in.mathworks.com/help/signal/ug/power-spectral-density-estimates-using-fft.html

function p2_psd_fft(x)
  N = length(x)
  xdft = p2_fft(x)[1:Int(N/2+1)]
  psdx = (1/N/N) * abs.(xdft).^2
  psdx[2:end] = 2*psdx[2:end]
  freq = 0:1:(N/2)
  return(Dict("freq"=>freq,"psdx"=>psdx,"psd_est"=>10*log10.(psdx)))
end


"""

                newtonroot(f[, f']; x0 = 0.8, tolerance = 1.e-13, maxiter = 1000)

This function computes the local root of a univariate smooth function `f(x)` using the Newton's Method. This method starts with a  `x_0` guess, a function `f(x)` and the first-derivative  `f'(x)`. The algorithm follows: `` x^{n+1} = x^n - f(x^n)/f'(x^n) `` until `` |x^{n+1} - x^n|`` is below the tolerance level.

...
# Arguments
- `f(x)::Function`: the function you are trying to solve.
- `f'(x)::Function`: the first derivative of the function you are trying to solve. If it is not given the program estimate `f'(x)` using ForwardDiff.
- `x0::Float64`: the initial value for the algorithm. The default value is 0.8.
- `tolerance::Float64` : the minimum tolerance for the algorithm to converge. The default value is 1.0e-13.
- `maxiter::Int64`: the maximum number of iterations allowed. The default value is 1000.
---
# Examples
```julia-repl
julia> f(x)  =  (x - 2)^2
f (generic function with 1 method)

julia> df(x) = 2(x - 2)
df (generic function with 1 method)

julia> newtonroot(f,df)
(value = 1.9999999999999318, normdiff = 6.816769371198461e-14, iter = 45)

julia> a = newtonroot(f)[1]
1.9999999999999318

julia> newtonroot(f)[1] ≈ 2
true
```
...

"""
function newtonroot(f, f_prime; x0=0.8, tolerance=1.0E-13, maxiter=1000)
    # Initial values
    v_old = x0
    normdiff = Inf
    iter = 1

    # Running the algorithm
    while (normdiff > tolerance)  && (iter < maxiter)
        if !(f_prime(v_old) == 0)
            v_new = v_old - (f(v_old) / f_prime(v_old))
            normdiff = norm(v_new - v_old)
            # replace and continue
            v_old = v_new
            iter = iter + 1
        else
            return(value = nothing, normdiff = nothing, iter = nothing)
        end
    end
    if (iter < maxiter)
        return(value = v_old, normdiff = normdiff, iter = iter)
    else
        return(value = nothing, normdiff = nothing, iter = nothing)
    end
end

function newtonroot(f; x0=0.8, tolerance=1.0E-13, maxiter=1000)
    # Initial values
    v_old = x0
    normdiff = Inf
    iter = 1

    # Taking the derivative
    D(f) = x -> ForwardDiff.derivative(f, x)
    f_prime = D(f)

    # Running the algorithm

    while (normdiff > tolerance)  && (iter < maxiter)
        if !(f_prime(v_old) == 0)
            v_new = v_old - (f(v_old) / f_prime(v_old))
            normdiff = norm(v_new - v_old)
            # replace and continue
            v_old = v_new
            iter = iter + 1
        else
            return(value = nothing, normdiff = nothing, iter = nothing)
        end
    end
    if (iter < maxiter)
        return(value = v_old, normdiff = normdiff, iter = iter)
    else
        return(value = nothing, normdiff = nothing, iter = nothing)
    end
end


# # Poisson Equation
#
# $$
# \frac{\partial^2 u}{\partial x^2} = b  \qquad x \in [0,1]
# $$
#
# $$
# u(0) = u(1) = 0, \qquad b = \sin(2\pi x)
# $$

using Plots, SparseArrays

Δx = 0.05
x = Δx:Δx:1-Δx ## Solve only interior points: the endpoints are set to zero.
n = length(x)
B = sin.(2π*x) * Δx^2

P = spdiagm( -1 =>    ones(Float64,n-1),
              0 => -2*ones(Float64,n),
              1 =>    ones(Float64,n-1))

u1 = P \ B

# ---

plot([0;x;1],[0;u1;0], label="computed")
scatter!([0;x;1],-sin.(2π*[0;x;1])/(4π^2),label="exact")
savefig("poisson1.png"); nothing #hide

# ![](poisson1.png)

# ---

# # DiffEqOperators.jl

using DiffEqOperators

Δx = 0.05
x = Δx:Δx:1-Δx ## Solve only interior points: the endpoints are set to zero.
n = length(x)
b = sin.(2π*x)

## Second order approximation to the second derivative
order = 2
deriv = 2

Δ = CenteredDifference{Float64}(deriv, order, Δx, n)
bc = Dirichlet0BC(Float64)

u2 = (Δ * bc) \ b

# ---

plot([0;x;1],[0;u2;0], label="computed")
scatter!([0;x;1],-sin.(2π*[0;x;1])/(4π^2),label="exact")
savefig("poisson2.png"); nothing #hide

# ![](poisson2.png)

# ---


# Test transforms.jl
using Test, LinearAlgebra

println("testing transforms")

tol = 1e-10

# rotx, roty, rotz, invrpy
roll = 0.3
pitch = -1.0
yaw = 0.8
T = rotz(yaw)*roty(pitch)*rotx(roll)

(rpy1, rpy2) = invrpy(T)

# Check 1st solution matches roll pitch and yaw
@test abs(rpy1[1]-yaw) < tol
@test abs(rpy1[2]-pitch) < tol
@test abs(rpy1[3]-roll) < tol

# Check 2nd solution produces the same transform
T2 = rotz(rpy2[1])*roty(rpy2[2])*rotx(rpy2[3])
@test maximum(abs.(T2-T)) < tol

# inveuler
phi = 0.5
theta = -0.2
psi = 0.1
T = rotz(phi) * roty(theta) * rotz(psi)
(euler1, euler2) = inveuler(T)

# Check both solutions produce the same transform
T1 = rotz(euler1[1])*roty(euler1[2])*rotz(euler1[3])
@test maximum(abs.(T1-T)) < tol

T2 = rotz(euler2[1])*roty(euler2[2])*rotz(euler2[3])
@test maximum(abs.(T2-T)) < tol

# invht
v = [1,-2,3]
T = rotz(yaw)*roty(pitch)*rotx(roll) * trans(v)

@test maximum(abs.(invht(T)*T - I)) < tol
@test maximum(abs.(trans(v) - trans(v[1],v[2],v[3]))) < tol

# angleaxis, angleaxis2matrix, matrix2angleaxis
# matrix2quaternion, quaternion2matrix
T1 = rotz(yaw)*roty(pitch)*rotx(roll)
ax = matrix2angleaxis(T1)
T2 = angleaxis2matrix(ax)
@test maximum(abs.(T2-T1)) < tol

theta = 0.35
axis = T1[1:3,1]
ax = angleaxis(theta, axis)
Q = quaternion(theta,axis)

T1 = quaternion2matrix(Q)
T2 = angleaxis2matrix(ax)
@test maximum(abs.(T2-T1)) < tol

Q2 = matrix2quaternion(T1)
@test maximum(abs.(Q-Q2)) < tol

# angleaxisrotate, quaternionrotate
ax = angleaxis(theta, [1,0,0])
Q = quaternion(theta, [1,0,0])
v = [3,-2,7,1]
vnew1 = quaternionrotate(Q,v)
vnew2 = angleaxisrotate(ax,v)
vnew3 = rotx(theta)*v

@test maximum(abs.(vnew3-vnew1)) < tol
@test maximum(abs.(vnew3-vnew2)) < tol

# quaternionconjugate, quaternionproduct,
# vector2quaternion


function compute_s(n)
    sn = 0
    for i = 1:n
        sn += 1/i
    end
    sn
end

compute_s(100)

for x = 1:2:20      # Steps of 2 - only odd values x
    print(x, " ")
end

for x = 1:0.2:5     # Non-integers are ok
    print(x, " ")
end

for x = 10:-1:-5    # Use a negative step to count down
    print(x, " ")
end

for x = 10:2:5      # No repetitions since start > stop and step is positive
    print(x, " ")
end

function my_factorial(n)
    y = 1
    for i = 2:n
        y *= i
    end
    y
end

my_factorial(5)

my_factorial(20)     # This is OK

my_factorial(30)     # Too large - overflow

my_factorial(Int128(30))

factorial(30)

function taylor_cos_bad(x,n)
    y = 0
    for k = 0:n
        y += (-1)^k / factorial(2k) * x^2k
    end
    y
end

# x = 0.25, excellent approximation of degree 4
println(taylor_cos_bad(0.25, 2))  # Taylor approximation
println(cos(0.25))                # true value

# x = 10, very poor approximation of degree 4
println(taylor_cos_bad(10, 2))  # Taylor approximation
println(cos(10))                # true value

# x = 10, try approximation of degree 30
println(taylor_cos_bad(10, 15)) # Taylor approximation
println(cos(10))                # true value

function taylor_cos(x,n)
    term = 1
    y = 1
    for k = 1:n
        term *= -x^2 / ((2k-1) * 2k)
        y += term
    end
    y
end

# x = 10, try approximation of degree 30
println(taylor_cos(10, 15)) # Taylor approximation
println(cos(10))            # true value

# x = 10, try approximation of degree 100
println(taylor_cos(10, 50)) # Taylor approximation
println(cos(10))            # true value

x = 10
y = 10

for i = 1:5
    z = i        # Local scope, only visible inside for-loop
    x = z        # Using x from parent scope
    local y = z  # Local scope, only visible inside for-loop (not using y from parent scope)
end

println(x)    #  = 5, since for loop modifies parent variable x
println(y)    #  = 10, since for loop uses local variable y
println(z)    # Error: z only defined in the local scope of the for-loop


### A Pluto.jl notebook ###
# v0.16.0

using Markdown
using InteractiveUtils

# ╔═╡ 9d2617c5-ebf2-46c8-910f-190ba1d160ac
using BenchmarkTools

# ╔═╡ 89196053-24db-4013-a1e3-3d3b22c4eedc
using Combinatorics

# ╔═╡ 27547863-22e1-466e-951e-2a5677bb9501
using IterTools

# ╔═╡ bade5453-0522-4cbc-8200-78ec160f1192
using ConstraintSolver, JuMP

# ╔═╡ 568f057a-126d-11ec-23cb-f98bea4a534f
md"# Project Euler 1

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.

"

# ╔═╡ daad198c-10c8-4e4c-93c4-d330ab22f3dd
naive1(x)=sum([i for i in 1:(x-1) if i%3==0 || i%5==0])

# ╔═╡ 2cca4797-d1ee-4c27-8243-736e3e468dd1
naive1(1000)

# ╔═╡ 6b82e993-2f3d-4d5c-8709-57050c7211de
sumRange(i,j) = (j-i+1)*(i+j)/2

# ╔═╡ 382a2b3a-4059-4cbe-8485-b0ecce99c451
sumRange(1,10)

# ╔═╡ e59e2718-acc6-4a98-8ec9-61cf0a375f90
md"We can also solve it with the inclusion-exclusion principle"

# ╔═╡ f7acd079-f79b-4967-a9ee-fc4965e2a06c
inclusionExclusionPrincipleProb1(n) = begin
	k=n-1
	sumRange(1,floor(k/3))*3+sumRange(1,floor(k/5))*5-sumRange(1,floor(k/15))*15
end

# ╔═╡ c118a05d-d1bb-4767-9f3a-0b5cefd69d47
inclusionExclusionPrincipleProb1(1000)

# ╔═╡ 078cb436-e238-4a69-bde6-156624db601b
@benchmark sum(1:1000000)

# ╔═╡ cea0b9f0-b908-4c33-8a5d-6a3019cfa47a
@benchmark sumRange(1,1000000)

# ╔═╡ 07d10397-f053-45e0-ab2b-6565c18db5a2
@benchmark inclusionExclusionPrincipleProb1(10000)

# ╔═╡ 464e87db-a12f-4e8b-a03a-ae1f943a42d0
@benchmark naive1(10000)

# ╔═╡ e323b98a-47a7-4182-9892-4d0b1891e295
md" so we can see we have about $(6.380*10^(-6)/(0.021*10^(-9))) speed-up"

# ╔═╡ 82baf161-7a98-4bb5-a928-b2273e4360cb
md"We can also do it for a general problem with the inclusion- exclusion principle as follows:
"

# ╔═╡ c7667675-8018-40f5-a1d9-8756f29466e0
prod([1,2,3])

# ╔═╡ ed4ded6b-925a-46e7-bcab-0cf0dc0fe80a
inclusionExclusionSumUpTo(n,multiples) = begin
	k=n-1
	res = 0
	for comb in combinations(multiples)
		pcomb=prod(comb)
		res += (-1)^(length(comb)-1)* sumRange(1,floor(k/pcomb))*pcomb
	end
	res
end

# ╔═╡ fa4d516c-74ea-44cb-bc18-e2b6cde80515
inclusionExclusionSumUpTo(1000,[3,5])

# ╔═╡ 120de079-e8e5-40b3-9582-c3741cf024cb
@benchmark inclusionExclusionSumUpTo(100000,[3,5,7,11,13])

# ╔═╡ 6ea414f8-d5bb-4432-9afb-6d2b2cfd1d01
md"We can see it's pretty fast..."

# ╔═╡ 6d793a48-b48e-4547-ac87-c23a1b03de9c
md" # Problem 2
Each new term in the Fibonacci sequence is generated by adding the previous two terms. By starting with 1 and 2, the first 10 terms will be:

1, 2, 3, 5, 8, 13, 21, 34, 55, 89, ...

By considering the terms in the Fibonacci sequence whose values do not exceed four million, find the sum of the even-valued terms.

"

# ╔═╡ e0470dad-e1ab-4603-85d0-e165f80c2463
md"Copy some fibonacci channel code from [this blog post about lazy sequences](https://www.oxinabox.net/2017/11/18/Lazy-Sequences-in-Julia.html)"

# ╔═╡ c6a079d7-2773-41ba-81c8-773efaabd010
function fib_ch(buff=1)
    prev=0
    cur=1
    Channel(ctype=Int, csize=buff) do c
        while(true)
            put!(c, cur)
            prev, cur = (cur, prev+cur) # nice little avoiding a temp here
        end
    end
end

# ╔═╡ 61d320a0-2f4f-4cf4-b800-20bb77560c17

# ╔═╡ 2eb91ffc-799d-496e-8b50-6be2d497beaa
sum([x for x in collect(IterTools.takewhile(x->x<=4_000_000,fib_ch(100))) if x%2==0])

# ╔═╡ 5e201d20-3600-4b21-a764-8dd1e89cdf66
md" It's also quite fast!"

# ╔═╡ 6678078e-f30c-4e5e-8de5-f697d6c241fd
eulerProblem2WithLazySeq(limit)=sum([x for x in collect(IterTools.takewhile(x->x<=limit,fib_ch(100))) if x%2==0])

# ╔═╡ c2c72029-ffd2-463c-8548-bbe306178bd1
eulerProblem2WithLazySeq(4_000_000)

# ╔═╡ 77f86b91-0ac4-4ba4-a687-0ea49a0ddd10
@benchmark eulerProblem2WithLazySeq(4_000_000)

# ╔═╡ 314415de-ac90-479a-8665-fa9e51323740
@benchmark eulerProblem2WithLazySeq(1_000_000_000_000)

# ╔═╡ cea3d8d2-70e4-49c7-9698-d4085976e924
md"Let's figure out also how to solve some recurrent equations and do it like that

$a_n=a_{n-1}+a_{n-2}$

The characteristic equation of the generating function is

$x^2-x-1==0$, with roots

$x_1 = (1+√5)/2, x_2=(1-√5)/2$

Then we can search for a solution of the form $dx_1^n+ex_2^n$,
since then
$a_n-a_{n-1}-a_{n-2} = (dx_1^n+ex_2^n)-(dx_1^{n-1}+ex_2^{n-1})-(...) = dx_1^{n-2}(...=0)-ex_2^{n-2}(...=0)$
and setting $a_0 = 0, a_1=1$ and solving for
$d,e$ we get eventually Binet's formula:

$ϕ = (\sqrt(5)+1)/2$
$a_n = (ϕ^n-((-ϕ)^{-n}))/√5$

"

# ╔═╡ fba39823-d264-4210-80c8-491eb19486d2
md"# Product 3

The prime factors of 13195 are 5, 7, 13 and 29.

What is the largest prime factor of the number 600851475143 ?

"

# ╔═╡ d6a95c3b-30c1-4004-884c-2acdf4a2ef7f
# let's implement erathostens' sieve
function get_primes(n::Int64)
	numbers = fill(true,n)
	numbers[1] = false

	for i = 2:isqrt(n)
		if numbers[i]
			for j = i:div(n,i)
				numbers[i*j] = false
			end
		end
	end
	primes = findall(numbers)
	return primes
end

# ╔═╡ 080bd517-473a-4c13-9969-a4775a2acc16

# ╔═╡ 4746f780-750d-4151-bdfb-9687456a36f5
function findFactors(n::Int64)
 all_primes = get_primes(isqrt(600851475143))
 res = []
 for prime in all_primes
		if mod(n,prime)==0
			push!(res,prime)
			while mod(n,prime)==0
				n= (n//prime)
			end
		end
	end
	res
end

# ╔═╡ f2279703-313c-4146-8e01-52c5fecf8cfd
findFactors(600851475143)

# ╔═╡ 69853164-2ca0-4cba-9aef-307f0766b273
md"
# Problem 4

 The largest palindrome made fromt he product of 2 2-digit numbers is 9009=91*99.
What's the largest palindorme made forom the product of two 3-digit numbers...

"

# ╔═╡ 3c93ec46-633e-4d97-aa53-6d16b7e85502
CS = ConstraintSolver

# ╔═╡ 1c57967f-f38f-41e2-bc7c-51d1dd681e0b
function CSPalindrome()
	mod = Model(CS.Optimizer)
	@variable(mod,0<=a[i=1:3]<=9,Int)
	@variable(mod,0<=b[i=1:3]<=9,Int)
	@variable(mod,0<=res[i=1:6]<=9,Int)
	@variable(mod,0<=carry[i=1:5]<=9,Int)
	@constraint(mod,three_digits_1,a[1]>=1)
	@constraint(mod,three_digits_2,b[1]>=1)
	@constraint(mod,first_digit,res[1]==b[1]*a[1])
	@objective(mod,Max,1_000_00*res[1]+10_000*res[2]+1000*res[3]+100*res[4]+10*res[5]+res[6])
	mod

end

# ╔═╡ b5da5b91-de7c-4b69-98f5-737a8a9ced34
begin
	md = CSPalindrome()
	optimize!(md)
end

# ╔═╡ e0d8e8d1-8e92-4ea3-9b38-bfb7378fe004
objective_value(md)

# ╔═╡ f6b8b94d-0176-4b19-9c81-2959cfff60e7
isPalindrome(k,l)= begin
 str1= string(k*l)
  str1==reverse(str1)
end

# ╔═╡ cc6dbc26-db1f-4534-95b9-7a801139e1d0
findPalindrome()=

	for i = 999:-1:100
	for j in i:-1:100
		if isPalindrome(i,j)
			return i,j
	end

	end
	end

# ╔═╡ c5d9bd7f-1a75-482f-a2aa-f653fdce083c
findPalindrome()

# ╔═╡ 00000000-0000-0000-0000-000000000001
PLUTO_PROJECT_TOML_CONTENTS = """
[deps]
BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf"
Combinatorics = "861a8166-3701-5b0c-9a16-15d98fcdc6aa"
ConstraintSolver = "e0e52ebd-5523-408d-9ca3-7641f1cd1405"
IterTools = "c8e1da08-722c-5040-9ed9-7db0dc04731e"
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"

[compat]
BenchmarkTools = "~1.1.4"
Combinatorics = "~1.0.2"
ConstraintSolver = "~0.7.0"
IterTools = "~1.3.0"
JuMP = "~0.21.10"
"""

# ╔═╡ 00000000-0000-0000-0000-000000000002
PLUTO_MANIFEST_TOML_CONTENTS = """
# This file is machine-generated - editing it directly is not advised

julia_version = "1.7.0-beta3.0"
manifest_format = "2.0"

[[deps.ArgTools]]
uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f"

[[deps.ArnoldiMethod]]
deps = ["LinearAlgebra", "Random", "StaticArrays"]
git-tree-sha1 = "f87e559f87a45bece9c9ed97458d3afe98b1ebb9"
uuid = "ec485272-7323-5ecc-a04f-4719b315124d"
version = "0.1.0"

[[deps.Arpack]]
deps = ["Arpack_jll", "Libdl", "LinearAlgebra"]
git-tree-sha1 = "2ff92b71ba1747c5fdd541f8fc87736d82f40ec9"
uuid = "7d9fca2a-8960-54d3-9f78-7d1dccf2cb97"
version = "0.4.0"

[[deps.Arpack_jll]]
deps = ["Libdl", "OpenBLAS_jll", "Pkg"]
git-tree-sha1 = "e214a9b9bd1b4e1b4f15b22c0994862b66af7ff7"
uuid = "68821587-b530-5797-8361-c406ea357684"
version = "3.5.0+3"

[[deps.Artifacts]]
uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33"

[[deps.Base64]]
uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"

[[deps.BenchmarkTools]]
deps = ["JSON", "Logging", "Printf", "Statistics", "UUIDs"]
git-tree-sha1 = "42ac5e523869a84eac9669eaceed9e4aa0e1587b"
uuid = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf"
version = "1.1.4"

[[deps.Bzip2_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"]
git-tree-sha1 = "19a35467a82e236ff51bc17a3a44b69ef35185a2"
uuid = "6e34b625-4abd-537c-b88f-471c36dfa7a0"
version = "1.0.8+0"

[[deps.Calculus]]
deps = ["LinearAlgebra"]
git-tree-sha1 = "f641eb0a4f00c343bbc32346e1217b86f3ce9dad"
uuid = "49dc2e85-a5d0-5ad3-a950-438e2897f1b9"
version = "0.5.1"

[[deps.ChainRulesCore]]
deps = ["Compat", "LinearAlgebra", "SparseArrays"]
git-tree-sha1 = "30ee06de5ff870b45c78f529a6b093b3323256a3"
uuid = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4"
version = "1.3.1"

[[deps.CodecBzip2]]
deps = ["Bzip2_jll", "Libdl", "TranscodingStreams"]
git-tree-sha1 = "2e62a725210ce3c3c2e1a3080190e7ca491f18d7"
uuid = "523fee87-0ab8-5b00-afb7-3ecf72e48cfd"
version = "0.7.2"

[[deps.CodecZlib]]
deps = ["TranscodingStreams", "Zlib_jll"]
git-tree-sha1 = "ded953804d019afa9a3f98981d99b33e3db7b6da"
uuid = "944b1d66-785c-5afd-91f1-9de20f533193"
version = "0.7.0"

[[deps.Combinatorics]]
git-tree-sha1 = "08c8b6831dc00bfea825826be0bc8336fc369860"
uuid = "861a8166-3701-5b0c-9a16-15d98fcdc6aa"
version = "1.0.2"

[[deps.CommonSubexpressions]]
deps = ["MacroTools", "Test"]
git-tree-sha1 = "7b8a93dba8af7e3b42fecabf646260105ac373f7"
uuid = "bbf7d656-a473-5ed7-a52c-81e309532950"
version = "0.3.0"

[[deps.Compat]]
deps = ["Base64", "Dates", "DelimitedFiles", "Distributed", "InteractiveUtils", "LibGit2", "Libdl", "LinearAlgebra", "Markdown", "Mmap", "Pkg", "Printf", "REPL", "Random", "SHA", "Serialization", "SharedArrays", "Sockets", "SparseArrays", "Statistics", "Test", "UUIDs", "Unicode"]
git-tree-sha1 = "6071cb87be6a444ac75fdbf51b8e7273808ce62f"
uuid = "34da2185-b29b-5c13-b0c7-acf172513d20"
version = "3.35.0"

[[deps.CompilerSupportLibraries_jll]]
deps = ["Artifacts", "Libdl"]
uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae"

[[deps.ConstraintProgrammingExtensions]]
deps = ["LinearAlgebra", "MathOptInterface", "Test"]
git-tree-sha1 = "989acd05e57112a5fd1bc8849ebc398a614aa010"
uuid = "b65d079e-ed98-51d9-b0db-edee61a5c5f8"
version = "0.3.2"

[[deps.ConstraintSolver]]
deps = ["ConstraintProgrammingExtensions", "DataStructures", "Formatting", "JSON", "JuMP", "LightGraphs", "MathOptInterface", "MatrixNetworks", "Random", "Statistics", "StatsBase", "StatsFuns"]
git-tree-sha1 = "47a4c07efa6890e7da286495e22742eb19f3eed9"
uuid = "e0e52ebd-5523-408d-9ca3-7641f1cd1405"
version = "0.7.0"

[[deps.DataAPI]]
git-tree-sha1 = "bec2532f8adb82005476c141ec23e921fc20971b"
uuid = "9a962f9c-6df0-11e9-0e5d-c546b8b5ee8a"
version = "1.8.0"

[[deps.DataStructures]]
deps = ["Compat", "InteractiveUtils", "OrderedCollections"]
git-tree-sha1 = "7d9d316f04214f7efdbb6398d545446e246eff02"
uuid = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
version = "0.18.10"

[[deps.Dates]]
deps = ["Printf"]
uuid = "ade2ca70-3891-5945-98fb-dc099432e06a"

[[deps.DelimitedFiles]]
deps = ["Mmap"]
uuid = "8bb1440f-4735-579b-a4ab-409b98df4dab"

[[deps.DiffResults]]
deps = ["StaticArrays"]
git-tree-sha1 = "c18e98cba888c6c25d1c3b048e4b3380ca956805"
uuid = "163ba53b-c6d8-5494-b064-1a9d43ac40c5"
version = "1.0.3"

[[deps.DiffRules]]
deps = ["NaNMath", "Random", "SpecialFunctions"]
git-tree-sha1 = "3ed8fa7178a10d1cd0f1ca524f249ba6937490c0"
uuid = "b552c78f-8df3-52c6-915a-8e097449b14b"
version = "1.3.0"

[[deps.Distributed]]
deps = ["Random", "Serialization", "Sockets"]
uuid = "8ba89e20-285c-5b6f-9357-94700520ee1b"

[[deps.DocStringExtensions]]
deps = ["LibGit2"]
git-tree-sha1 = "a32185f5428d3986f47c2ab78b1f216d5e6cc96f"
uuid = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae"
version = "0.8.5"

[[deps.Downloads]]
deps = ["ArgTools", "LibCURL", "NetworkOptions"]
uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6"

[[deps.Formatting]]
deps = ["Printf"]
git-tree-sha1 = "8339d61043228fdd3eb658d86c926cb282ae72a8"
uuid = "59287772-0a20-5a39-b81b-1366585eb4c0"
version = "0.4.2"

[[deps.ForwardDiff]]
deps = ["CommonSubexpressions", "DiffResults", "DiffRules", "LinearAlgebra", "NaNMath", "Printf", "Random", "SpecialFunctions", "StaticArrays"]
git-tree-sha1 = "b5e930ac60b613ef3406da6d4f42c35d8dc51419"
uuid = "f6369f11-7733-5829-9624-2563aa707210"
version = "0.10.19"

[[deps.HTTP]]
deps = ["Base64", "Dates", "IniFile", "Logging", "MbedTLS", "NetworkOptions", "Sockets", "URIs"]
git-tree-sha1 = "60ed5f1643927479f845b0135bb369b031b541fa"
uuid = "cd3eb016-35fb-5094-929b-558a96fad6f3"
version = "0.9.14"

[[deps.Inflate]]
git-tree-sha1 = "f5fc07d4e706b84f72d54eedcc1c13d92fb0871c"
uuid = "d25df0c9-e2be-5dd7-82c8-3ad0b3e990b9"
version = "0.1.2"

[[deps.IniFile]]
deps = ["Test"]
git-tree-sha1 = "098e4d2c533924c921f9f9847274f2ad89e018b8"
uuid = "83e8ac13-25f8-5344-8a64-a9f2b223428f"
version = "0.5.0"

[[deps.InteractiveUtils]]
deps = ["Markdown"]
uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240"

[[deps.IrrationalConstants]]
git-tree-sha1 = "f76424439413893a832026ca355fe273e93bce94"
uuid = "92d709cd-6900-40b7-9082-c6be49f344b6"
version = "0.1.0"

[[deps.IterTools]]
git-tree-sha1 = "05110a2ab1fc5f932622ffea2a003221f4782c18"
uuid = "c8e1da08-722c-5040-9ed9-7db0dc04731e"
version = "1.3.0"

[[deps.JLLWrappers]]
deps = ["Preferences"]
git-tree-sha1 = "642a199af8b68253517b80bd3bfd17eb4e84df6e"
uuid = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210"
version = "1.3.0"

[[deps.JSON]]
deps = ["Dates", "Mmap", "Parsers", "Unicode"]
git-tree-sha1 = "8076680b162ada2a031f707ac7b4953e30667a37"
uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
version = "0.21.2"

[[deps.JSONSchema]]
deps = ["HTTP", "JSON", "URIs"]
git-tree-sha1 = "2f49f7f86762a0fbbeef84912265a1ae61c4ef80"
uuid = "7d188eb4-7ad8-530c-ae41-71a32a6d4692"
version = "0.3.4"

[[deps.JuMP]]
deps = ["Calculus", "DataStructures", "ForwardDiff", "JSON", "LinearAlgebra", "MathOptInterface", "MutableArithmetics", "NaNMath", "Printf", "Random", "SparseArrays", "SpecialFunctions", "Statistics"]
git-tree-sha1 = "4358b7cbf2db36596bdbbe3becc6b9d87e4eb8f5"
uuid = "4076af6c-e467-56ae-b986-b466b2749572"
version = "0.21.10"

[[deps.KahanSummation]]
deps = ["Test"]
git-tree-sha1 = "1f01068b28d3ad83d4d1212a0ce8d7ecacb33482"
uuid = "8e2b3108-d4c1-50be-a7a2-16352aec75c3"
version = "0.1.0"

[[deps.LibCURL]]
deps = ["LibCURL_jll", "MozillaCACerts_jll"]
uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21"

[[deps.LibCURL_jll]]
deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll", "Zlib_jll", "nghttp2_jll"]
uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0"

[[deps.LibGit2]]
deps = ["Base64", "NetworkOptions", "Printf", "SHA"]
uuid = "76f85450-5226-5b5a-8eaa-529ad045b433"

[[deps.LibSSH2_jll]]
deps = ["Artifacts", "Libdl", "MbedTLS_jll"]
uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8"

[[deps.Libdl]]
uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb"

[[deps.LightGraphs]]
deps = ["ArnoldiMethod", "DataStructures", "Distributed", "Inflate", "LinearAlgebra", "Random", "SharedArrays", "SimpleTraits", "SparseArrays", "Statistics"]
git-tree-sha1 = "432428df5f360964040ed60418dd5601ecd240b6"
uuid = "093fc24a-ae57-5d10-9952-331d41423f4d"
version = "1.3.5"

[[deps.LinearAlgebra]]
deps = ["Libdl", "libblastrampoline_jll"]
uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"

[[deps.LogExpFunctions]]
deps = ["ChainRulesCore", "DocStringExtensions", "IrrationalConstants", "LinearAlgebra"]
git-tree-sha1 = "86197a8ecb06e222d66797b0c2d2f0cc7b69e42b"
uuid = "2ab3a3ac-af41-5b50-aa03-7779005ae688"
version = "0.3.2"

[[deps.Logging]]
uuid = "56ddb016-857b-54e1-b83d-db4d58db5568"

[[deps.MacroTools]]
deps = ["Markdown", "Random"]
git-tree-sha1 = "0fb723cd8c45858c22169b2e42269e53271a6df7"
uuid = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09"
version = "0.5.7"

[[deps.Markdown]]
deps = ["Base64"]
uuid = "d6f4376e-aef5-505a-96c1-9c027394607a"

[[deps.MathOptInterface]]
deps = ["BenchmarkTools", "CodecBzip2", "CodecZlib", "JSON", "JSONSchema", "LinearAlgebra", "MutableArithmetics", "OrderedCollections", "SparseArrays", "Test", "Unicode"]
git-tree-sha1 = "575644e3c05b258250bb599e57cf73bbf1062901"
uuid = "b8f27783-ece8-5eb3-8dc8-9495eed66fee"
version = "0.9.22"

[[deps.MatrixNetworks]]
deps = ["Arpack", "DataStructures", "DelimitedFiles", "IterTools", "KahanSummation", "LinearAlgebra", "Printf", "Random", "SparseArrays", "Statistics"]
git-tree-sha1 = "f6794d987d426bab570aabb2d03e2f40ca4fc438"
uuid = "4f449596-a032-5618-b826-5a251cb6dc11"
version = "1.0.2"

[[deps.MbedTLS]]
deps = ["Dates", "MbedTLS_jll", "Random", "Sockets"]
git-tree-sha1 = "1c38e51c3d08ef2278062ebceade0e46cefc96fe"
uuid = "739be429-bea8-5141-9913-cc70e7f3736d"
version = "1.0.3"

[[deps.MbedTLS_jll]]
deps = ["Artifacts", "Libdl"]
uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1"

[[deps.Missings]]
deps = ["DataAPI"]
git-tree-sha1 = "2ca267b08821e86c5ef4376cffed98a46c2cb205"
uuid = "e1d29d7a-bbdc-5cf2-9ac0-f12de2c33e28"
version = "1.0.1"

[[deps.Mmap]]
uuid = "a63ad114-7e13-5084-954f-fe012c677804"

[[deps.MozillaCACerts_jll]]
uuid = "14a3606d-f60d-562e-9121-12d972cd8159"

[[deps.MutableArithmetics]]
deps = ["LinearAlgebra", "SparseArrays", "Test"]
git-tree-sha1 = "3927848ccebcc165952dc0d9ac9aa274a87bfe01"
uuid = "d8a4904e-b15c-11e9-3269-09a3773c0cb0"
version = "0.2.20"

[[deps.NaNMath]]
git-tree-sha1 = "bfe47e760d60b82b66b61d2d44128b62e3a369fb"
uuid = "77ba4419-2d1f-58cd-9bb1-8ffee604a2e3"
version = "0.3.5"

[[deps.NetworkOptions]]
uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908"

[[deps.OpenBLAS_jll]]
deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"]
uuid = "4536629a-c528-5b80-bd46-f80d51c5b363"

[[deps.OpenSpecFun_jll]]
deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl", "Pkg"]
git-tree-sha1 = "13652491f6856acfd2db29360e1bbcd4565d04f1"
uuid = "efe28fd5-8261-553b-a9e1-b2916fc3738e"
version = "0.5.5+0"

[[deps.OrderedCollections]]
git-tree-sha1 = "85f8e6578bf1f9ee0d11e7bb1b1456435479d47c"
uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d"
version = "1.4.1"

[[deps.Parsers]]
deps = ["Dates"]
git-tree-sha1 = "438d35d2d95ae2c5e8780b330592b6de8494e779"
uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0"
version = "2.0.3"

[[deps.Pkg]]
deps = ["Artifacts", "Dates", "Downloads", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "Serialization", "TOML", "Tar", "UUIDs", "p7zip_jll"]
uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"

[[deps.Preferences]]
deps = ["TOML"]
git-tree-sha1 = "00cfd92944ca9c760982747e9a1d0d5d86ab1e5a"
uuid = "21216c6a-2e73-6563-6e65-726566657250"
version = "1.2.2"

[[deps.Printf]]
deps = ["Unicode"]
uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7"

[[deps.REPL]]
deps = ["InteractiveUtils", "Markdown", "Sockets", "Unicode"]
uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb"

[[deps.Random]]
deps = ["Serialization"]
uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"

[[deps.Reexport]]
git-tree-sha1 = "45e428421666073eab6f2da5c9d310d99bb12f9b"
uuid = "189a3867-3050-52da-a836-e630ba90ab69"
version = "1.2.2"

[[deps.Rmath]]
deps = ["Random", "Rmath_jll"]
git-tree-sha1 = "bf3188feca147ce108c76ad82c2792c57abe7b1f"
uuid = "79098fc4-a85e-5d69-aa6a-4863f24498fa"
version = "0.7.0"

[[deps.Rmath_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"]
git-tree-sha1 = "68db32dff12bb6127bac73c209881191bf0efbb7"
uuid = "f50d1b31-88e8-58de-be2c-1cc44531875f"
version = "0.3.0+0"

[[deps.SHA]]
uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce"

[[deps.Serialization]]
uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b"

[[deps.SharedArrays]]
deps = ["Distributed", "Mmap", "Random", "Serialization"]
uuid = "1a1011a3-84de-559e-8e89-a11a2f7dc383"

[[deps.SimpleTraits]]
deps = ["InteractiveUtils", "MacroTools"]
git-tree-sha1 = "5d7e3f4e11935503d3ecaf7186eac40602e7d231"
uuid = "699a6c99-e7fa-54fc-8d76-47d257e15c1d"
version = "0.9.4"

[[deps.Sockets]]
uuid = "6462fe0b-24de-5631-8697-dd941f90decc"

[[deps.SortingAlgorithms]]
deps = ["DataStructures"]
git-tree-sha1 = "b3363d7460f7d098ca0912c69b082f75625d7508"
uuid = "a2af1166-a08f-5f64-846c-94a0d3cef48c"
version = "1.0.1"

[[deps.SparseArrays]]
deps = ["LinearAlgebra", "Random"]
uuid = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"

[[deps.SpecialFunctions]]
deps = ["ChainRulesCore", "LogExpFunctions", "OpenSpecFun_jll"]
git-tree-sha1 = "a322a9493e49c5f3a10b50df3aedaf1cdb3244b7"
uuid = "276daf66-3868-5448-9aa4-cd146d93841b"
version = "1.6.1"

[[deps.StaticArrays]]
deps = ["LinearAlgebra", "Random", "Statistics"]
git-tree-sha1 = "3240808c6d463ac46f1c1cd7638375cd22abbccb"
uuid = "90137ffa-7385-5640-81b9-e52037218182"
version = "1.2.12"

[[deps.Statistics]]
deps = ["LinearAlgebra", "SparseArrays"]
uuid = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"

[[deps.StatsAPI]]
git-tree-sha1 = "1958272568dc176a1d881acb797beb909c785510"
uuid = "82ae8749-77ed-4fe6-ae5f-f523153014b0"
version = "1.0.0"

[[deps.StatsBase]]
deps = ["DataAPI", "DataStructures", "LinearAlgebra", "Missings", "Printf", "Random", "SortingAlgorithms", "SparseArrays", "Statistics", "StatsAPI"]
git-tree-sha1 = "8cbbc098554648c84f79a463c9ff0fd277144b6c"
uuid = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91"
version = "0.33.10"

[[deps.StatsFuns]]
deps = ["ChainRulesCore", "IrrationalConstants", "LogExpFunctions", "Reexport", "Rmath", "SpecialFunctions"]
git-tree-sha1 = "46d7ccc7104860c38b11966dd1f72ff042f382e4"
uuid = "4c63d2b9-4356-54db-8cca-17b64c39e42c"
version = "0.9.10"

[[deps.TOML]]
deps = ["Dates"]
uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76"

[[deps.Tar]]
deps = ["ArgTools", "SHA"]
uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e"

[[deps.Test]]
deps = ["InteractiveUtils", "Logging", "Random", "Serialization"]
uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[[deps.TranscodingStreams]]
deps = ["Random", "Test"]
git-tree-sha1 = "216b95ea110b5972db65aa90f88d8d89dcb8851c"
uuid = "3bb67fe8-82b1-5028-8e26-92a6c54297fa"
version = "0.9.6"

[[deps.URIs]]
git-tree-sha1 = "97bbe755a53fe859669cd907f2d96aee8d2c1355"
uuid = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4"
version = "1.3.0"

[[deps.UUIDs]]
deps = ["Random", "SHA"]
uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"

[[deps.Unicode]]
uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5"

[[deps.Zlib_jll]]
deps = ["Libdl"]
uuid = "83775a58-1f1d-513f-b197-d71354ab007a"

[[deps.libblastrampoline_jll]]
deps = ["Artifacts", "Libdl", "OpenBLAS_jll"]
uuid = "8e850b90-86db-534c-a0d3-1478176c7d93"

[[deps.nghttp2_jll]]
deps = ["Artifacts", "Libdl"]
uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d"

[[deps.p7zip_jll]]
deps = ["Artifacts", "Libdl"]
uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0"
"""

# ╔═╡ Cell order:
# ╠═568f057a-126d-11ec-23cb-f98bea4a534f
# ╠═daad198c-10c8-4e4c-93c4-d330ab22f3dd
# ╠═2cca4797-d1ee-4c27-8243-736e3e468dd1
# ╠═6b82e993-2f3d-4d5c-8709-57050c7211de
# ╠═382a2b3a-4059-4cbe-8485-b0ecce99c451
# ╠═e59e2718-acc6-4a98-8ec9-61cf0a375f90
# ╠═f7acd079-f79b-4967-a9ee-fc4965e2a06c
# ╠═c118a05d-d1bb-4767-9f3a-0b5cefd69d47
# ╠═9d2617c5-ebf2-46c8-910f-190ba1d160ac
# ╠═078cb436-e238-4a69-bde6-156624db601b
# ╠═cea0b9f0-b908-4c33-8a5d-6a3019cfa47a
# ╠═07d10397-f053-45e0-ab2b-6565c18db5a2
# ╠═464e87db-a12f-4e8b-a03a-ae1f943a42d0
# ╠═e323b98a-47a7-4182-9892-4d0b1891e295
# ╠═82baf161-7a98-4bb5-a928-b2273e4360cb
# ╠═89196053-24db-4013-a1e3-3d3b22c4eedc
# ╠═c7667675-8018-40f5-a1d9-8756f29466e0
# ╠═ed4ded6b-925a-46e7-bcab-0cf0dc0fe80a
# ╠═fa4d516c-74ea-44cb-bc18-e2b6cde80515
# ╠═120de079-e8e5-40b3-9582-c3741cf024cb
# ╠═6ea414f8-d5bb-4432-9afb-6d2b2cfd1d01
# ╠═6d793a48-b48e-4547-ac87-c23a1b03de9c
# ╠═e0470dad-e1ab-4603-85d0-e165f80c2463
# ╠═c6a079d7-2773-41ba-81c8-773efaabd010
# ╠═61d320a0-2f4f-4cf4-b800-20bb77560c17
# ╠═27547863-22e1-466e-951e-2a5677bb9501
# ╠═2eb91ffc-799d-496e-8b50-6be2d497beaa
# ╠═5e201d20-3600-4b21-a764-8dd1e89cdf66
# ╠═6678078e-f30c-4e5e-8de5-f697d6c241fd
# ╠═c2c72029-ffd2-463c-8548-bbe306178bd1
# ╠═77f86b91-0ac4-4ba4-a687-0ea49a0ddd10
# ╠═314415de-ac90-479a-8665-fa9e51323740
# ╠═cea3d8d2-70e4-49c7-9698-d4085976e924
# ╠═fba39823-d264-4210-80c8-491eb19486d2
# ╠═d6a95c3b-30c1-4004-884c-2acdf4a2ef7f
# ╠═080bd517-473a-4c13-9969-a4775a2acc16
# ╠═4746f780-750d-4151-bdfb-9687456a36f5
# ╠═f2279703-313c-4146-8e01-52c5fecf8cfd
# ╠═69853164-2ca0-4cba-9aef-307f0766b273
# ╠═bade5453-0522-4cbc-8200-78ec160f1192
# ╠═3c93ec46-633e-4d97-aa53-6d16b7e85502
# ╠═1c57967f-f38f-41e2-bc7c-51d1dd681e0b
# ╠═b5da5b91-de7c-4b69-98f5-737a8a9ced34
# ╠═e0d8e8d1-8e92-4ea3-9b38-bfb7378fe004
# ╠═f6b8b94d-0176-4b19-9c81-2959cfff60e7
# ╠═cc6dbc26-db1f-4534-95b9-7a801139e1d0
# ╠═c5d9bd7f-1a75-482f-a2aa-f653fdce083c
# ╟─00000000-0000-0000-0000-000000000001
# ╟─00000000-0000-0000-0000-000000000002


#=Given a posititve number n find all the numbers from 2 to n which
are prime and print them. Sieve of Eratosthenes is an optimal approach
to find the prime numbers.We get an array of length n and if the value
of array at index i is zero the number i is prime.=#

## Function

function primeSieve(n)
    a = zeros(Int64, n + 5)
    for i = 2:n
        if (a[i] == 0)
            for j = (i*i):i:n
                a[j] = 1
            end
        end
    end
    for i = 2:n
        if (a[i] == 0)
            print("$(i) ")
        end
    end
end

##Input

n = readline()
n = parse(Int64, n)

#Calling the function

primeSieve(n)

#=
Sample test case:
Input:
    n = 20
Output:
    2 3 5 7 11 13 17 19

Time complexity: O( N * log(log(N)) )
=#


#=
Matrix Norms
------------

Many different matrix norms with different numerical values for the same matrix

3 main types of Matrix Norms
----------------------------

1) Frobenius norm aka Euclidian Norm

Square all individual matrix elements,
add them together
take square root

=#
A = [1 2 3;4 5 6;7 8 9]
# naive implementation
frobnorm1(A) = sqrt(reduce(+, (map(x -> x^2, A))))

## (defn frobnorm [A]
##   (Math/sqrt (reduce + (map #(* % %) A))))

# more efficient ways using LinearAlgebra package

using LinearAlgebra

frobnorm2(A) = sqrt(tr(A' * A))

frobnorm3(A) = norm(A) # default in Julia
#=

2) Induced 2-norm - ascertains how much matrix A scales vector x

the magnitude of vector Ax (A matrix multiplied by vector x results in a vector),
divided by the magnitude of vector x

(where x is not a zero vector)

||Ax|| / ||x||
=#

twonorm(A,x) = norm(A * x) / norm(x)

normInd2(A) = sqrt(eigmax(A' * A))

# if A is a pure rotation matrix, then ||Ax|| would be equal to ||x||
# so the induced two-norm would be 1

#=
3) Schatten P-norm
Sum of the singluar values of a matrix

Singluar values is a set of scalar values associated with a matrix.

Singular Value Decomposition determines these values

=#

p = 1 # Schatten 1-norm
s = svdvals(A) # extracts singular values into a vector
normSchat = sum(s.^p)^(1/p)

p = 2 # same as Frobenius norm
normSchat = sum(s.^p)^(1/p)

#= Orthogonal Matrix
   -----------------

All the columns have a magnitude of 1, and each column is par-wise orthogonal to every other column

This can be a purely rotational matrix, so the induced 2-norm is 1

Can be extracted as the Q matrix of the pair from QR Decomposition
=#

Q, R = qr(randn(5,5))

# induced 2-norm = 1
ind2norm = sqrt(eigmax(Q' * Q))

linear-algebra-code-challenges/Lecture 60 - Matrix Norms.jl


# Entropy.jl

###### generalized entropy #####
"""
    gen_entropy(v, α)

    Compute the Generalized Entropy Index of a vector `v` at a specified parameter `α`.

# Examples
```julia
julia> using Inequality
julia> gen_entropy([8, 5, 1, 3, 5, 6, 7, 6, 3], 2)
0.09039256198347094
```
"""
gen_entropy(v::AbstractVector{<:Real}, α::Real)::Float64 = α == 1 ? theil(v) : (α == 0) ? (mld(v)) : (1/(α * (α - 1)) * sum(((v./Statistics.mean(v)).^α .- 1) * (1/length(v))) )

###### weighted generalized entropy #####
"""
    gen_entropy(v, w, α)

Compute the Generalized Entropy Index of a vector `v`, using weights given by a weight vector `w`
at a specified parameter `α`.

Weights must not be negative, missing or NaN. The weights and data vectors must have the same length.

# Examples
```julia
julia> using Inequality
julia> gen_entropy([8, 5, 1, 3, 5, 6, 7, 6, 3], collect(0.1:0.1:0.9), 2)
0.0709746654322026
```
"""
function gen_entropy(v::AbstractVector{<:Real},w::AbstractVector{<:Real}, α::Real)::Float64

    if α == 1
        return theil(v, w)
    elseif α == 0
        return mld(v, w)
    end

    checks_weights(v, w)

    return 1/(α * (α - 1)) * sum(((v./(sum(v .* w)/sum(w)) ) .^ α .-1) .* (w/sum(w)))

end

function gen_entropy(v::AbstractVector{<:Real},w::AbstractWeights, α::Real)::Float64

    if α == 1
        return theil(v, w)
    elseif α == 0
        return mld(v, w)
    end

    checks_weights(v, w)

    return 1/(α * (α - 1)) * sum(((v./(sum(v .* w)/w.sum) ) .^ α .-1) .* (w/w.sum))

end

"""
    wgen_entropy(v, w, α)

Compute the Generalized Entropy Index of `v` with weights `w` at a specified parameter `α`. See also [`gen_entropy`](gen_entropy)
"""
wgen_entropy(v::AbstractVector{<:Real}, w::AbstractVector{<:Real}, α::Real)::Float64 = gen_entropy(v,w,α)

wgen_entropy(v::AbstractVector{<:Real},  w::AbstractWeights, α::Real)::Float64 = gen_entropy(v,w,α)


using DifferentialEquations
using Plots; gr()

f(u,p,t) = p[1]*u
u0 = 0.5
#params = [3.0]
p = [3.0]
tspan = (0.0,1.0)
prob = ODEProblem(f,u0,tspan,p)

sol = solve(prob,reltol=1e-7,abstol=1e-7)

plot(sol)

plot(sol,linewidth=5,title="Exponential Growth",
     xaxis="Time",yaxis="Population size",label="Numerical solution")

plot!(sol.t,t->u0*exp(p[1]t),lw=3,ls=:dash,label="True Solution", legend =:bottomright)

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

f(u,p,t) = p[1]*u*(1-(u/p[2]))
#f(u,p,t) = p[1]*u*(1-u)
u0 = 0.005
#params = [5.0,1.0]
p = [1.2,1.0]
tspan = (0.0, 10.0)
#prob2 = ODEProblem(f, u0, tspan, params)
prob2 = ODEProblem(f, u0, tspan, p)

sol2 = solve(prob2,reltol=1e-8,abstol=1e-8)

plot(sol2)

plot(sol2,linewidth=5,title="Logistic Growth",
     xaxis="Time",yaxis="Population size",label="Numerical Solution", legend=:bottomright)

plot!(sol2.t,t->((p[2])*(u0*exp(p[1]t)))/(p[2]+(u0*(exp(p[1]t)-1))),lw=3,ls=:dash,label="True Solution")


using NewtonsMethod
using Test
using LinearAlgebra, Statistics, Compat, ForwardDiff

@testset "NewtonsMethod.jl" begin

    f(x) = (x-1.0)^3
    f′(x) = 3.0*(x-1.0)^2
    x_0 = 0.1
    @test newtonroot(f, f′, x_0 = x_0).root ≈ 0.9999998168636032
    @test newtonroot(f, x_0 = x_0).root ≈ 0.9999998168636032
    @test newtonroot(f, f′, x_0 = BigFloat(x_0), tolerance = 1E-80).root ≈ BigFloat(1.0)
    @test newtonroot(f, x_0 = BigFloat(x_0), tolerance = 1E-80).root ≈ BigFloat(1.0)

    #maxiter
    @test newtonroot(f, f′, x_0 = x_0, maxiter = 5).root == nothing
    @test newtonroot(f, x_0 = x_0, maxiter = 5).root == nothing
    @test newtonroot(f, f′, x_0 = BigFloat(x_0), tolerance = 1E-80, maxiter = 10).root == nothing
    @test newtonroot(f, x_0 = BigFloat(x_0), tolerance = 1E-80, maxiter = 10).root == nothing

    g(x) = log(x)
    g′(x) = 1.0/x
    x_0 = 2.0
    @test newtonroot(g, g′, x_0 = x_0).root ≈ 1.0
    @test newtonroot(g, x_0 = x_0).root ≈ 1.0
    @test newtonroot(g, g′, x_0 = BigFloat(x_0), tolerance = 1E-50).root ≈ BigFloat(1.0)
    @test newtonroot(g, x_0 = BigFloat(x_0), tolerance = 1E-50).root ≈ BigFloat(1.0)

    @test !(newtonroot(g, g′, x_0 = x_0, tolerance = 1E-2).root ≈ 1.0)
    @test !(newtonroot(g, x_0 = x_0, tolerance = 1E-2).root ≈ 1.0)

    #non-convergence

    h(x) = x^2 + 1.0
    h′(x) = 2x
    x_0 = 10.0
    @test newtonroot(h, h′, x_0 = x_0).root == nothing
    @test newtonroot(h, x_0 = x_0).root == nothing
    @test newtonroot(h, h′, x_0 = BigFloat(x_0), tolerance = 1E-100).root == nothing
    @test newtonroot(h, x_0 = BigFloat(x_0), tolerance = 1E-100).root == nothing

end


"""
# Interpolation Search
Used for sorted array which is uniformly distributed.
Binary Search always goes to the middle element to check. On the other hand, interpolation search may go to different locations according to the value of the key being searched. For example, if the value of the key is closer to the last element, interpolation search is likely to start search toward the end side.
The idea of formula is to return higher value of pos when element to be searched is closer to arr[hi]. And smaller value when closer to arr[lo]
pos = lo + [ (x-arr[lo])*(hi-lo) / (arr[hi]-arr[Lo]) ]
- arr[] ==> Array where elements need to be searched
- x     ==> Element to be searched
- lo    ==> Starting index in arr[]
- hi    ==> Ending index in arr[]
## Derivation =>
Let's assume that the elements of the array are linearly distributed.
General equation of line : y = m*x + c.
y is the value in the array and x is its index.
Now putting value of lo,hi and x in the equation
- arr[hi] = m*hi+c ----(1)
- arr[lo] = m*lo+c ----(2)
- x = m*pos + c     ----(3)
m = (arr[hi] - arr[lo] )/ (hi - lo)
subtracting eqxn (2) from (3)
- x - arr[lo] = m * (pos - lo)
- lo + (x - arr[lo])/m = pos
- pos = lo + (x - arr[lo]) *(hi - lo)/(arr[hi] - arr[lo])
## Algorithm:
Rest of the Interpolation algorithm is the same except the above partition logic.
- Step1: In a loop, calculate the value of “pos” using the probe position formula.
- Step2: If it is a match, return the index of the item, and exit.
- Step3: If the item is less than arr[pos], calculate the probe position of the left sub-array. Otherwise calculate the same in the right sub-array.
- Step4: Repeat until a match is found or the sub-array reduces to zero.

"""

"""
	 interpolation_search(arr::AbstractArray{T,1}, l::T, r::T, x::T) where {T <: Real}

Interpolation Search in 1-D array
Time Complexity: O(log2(log2 n))
"""
function interpolation_search(arr::AbstractArray{T,1}, l::T, r::T, x::T) where {T <: Real}

    if (r >= l && x >= arr[l] && x <= arr[r])

        mid = Int(ceil(l + (((x - arr[l]) * (r - l)) / (arr[r] - arr[l]))))

        if (arr[mid] == x)
            return mid

        elseif (arr[mid] > x)
            interpolation_search(arr, l, mid - 1, x)

        else
            interpolation_search(arr, mid + 1, r, x)

        end

    else
        return -1

    end

end


"""
    riemann_integration(f::Function, a::Real, b::Real, n::Int, approx::Symbol = :midpoint)

a Riemann sum is a certain kind of approximation of an integral by a finite sum.
The sum is calculated by partitioning the region into shapes (rectangles, trapezoids, parabolas, or cubics) that together form a region that is similar to the region being measured, then calculating the area for each of these shapes, and finally adding all of these small areas together.

Because the region filled by the small shapes is usually not exactly the same shape as the region being measured, the Riemann sum will differ from the area being measured.
This error can be reduced by dividing up the region more finely, using smaller and smaller shapes.
As the shapes get smaller and smaller, the sum approaches the Riemann integral.

# Arguments
- `f`: the function to integrate. (at the momment only single variable is suported)
- `a`: Start of the integration limit.
- `b`: End of the integration limit.
- `n`: Number of points to sample. (as n increase, error decrease)
- `approx`: Indicate the method of approximation (midpoint, left or right)

# Examples
```julia
julia> riemann_integration(x -> x, 1, 3, 1_000, :midpoint)  # 4.0
julia> riemann_integration(x -> x, 1, 3, 1_000, :left)      # 3.997997997997998
julia> riemann_integration(x -> x, 1, 3, 1_000, :right)     # 4.002002002002002
julia> riemann_integration(x -> 3*x^2, 0, 1, 100000)        # integrate a polynomial
0.9999999999750021
julia> riemann_integration(x -> sin(x), 0, pi, 1000)          # integrate the sin function
2.0000008241146774
```

# Refereces
- https://www.khanacademy.org/math/ap-calculus-ab/ab-integration-new/ab-6-2/a/riemann-sums-review
- https://math.libretexts.org/Courses/Mount_Royal_University/MATH_2200%3A_Calculus_for_Scientists_II/2%3A_Techniques_of_Integration/2.5%3A_Numerical_Integration_-_Midpoint%2C_Trapezoid%2C_Simpson's_rule
- https://abel.math.harvard.edu/~knill/teaching/math1a_2011/handouts/40-numerical.pdf
- https://en.wikipedia.org/wiki/Riemann_integral

Contributed By:- [AugustoCL](https://github.com/AugustoCL)
"""
function riemann_integration(f::Function, a::Real, b::Real, n::Int, approx::Symbol = :midpoint)
    # width of the rectangles
    Δₓ = (b - a) / n

    # methods of approximation (:midpoint, :left, :right)
    if approx == :midpoint
        sum_range = (a + Δₓ/2):Δₓ:(b - Δₓ/2)
    elseif approx == :left
        sum_range = a:Δₓ:(b - Δₓ)
    elseif approx == :right
        sum_range = (a + Δₓ):Δₓ:b
    else
        throw(ArgumentError("The symbol :$approx is not a valid argument. Insert :midpoint, :left or :right"))
    end

    # sum of the height of the rectangles
    Σ = 0.0
    for i in 2:length(sum_range)
        a  = sum_range[i-1]
        b  = sum_range[i]
        xᵢ = a + (b-a)*rand()   # draw a uniform(a,b) for each subinterval [a,b]
        Σ += f(xᵢ)
    end

    # approximate integral of f
    return Δₓ * Σ
end


"""Julia program to find the n'th number in the tribonacci series
Tribonacci series is a generalization of the Fibonacci sequence, in which the current term
is the sum of the previous three
"""

function find_tribonacci(n)
    dp = zeros(Int, n + 1)
    dp[1] = 0
    dp[2] = 0
    dp[3] = 1

    for i in 4:n
        dp[i] = dp[i-1] + dp[i-2] + dp[i-3]
    end

    return dp[n]
end

print("Enter the value of n?, where you need the n'th number in the tribonacci sequence. ")
n = readline()
n = parse(Int, n)
if (n <= 0)
    println("The given value of n is invalid.")
    exit()
end
res = find_tribonacci(n)
println("The $n'th term in the tribonacci series is $res.")

"""
Time Complexity - O(n)
Space Complexity - O(n)

SAMPLE INPUT AND OUTPUT

SAMPLE I
Enter the value of n?, where you need the n'th number in the tribonacci sequence. 12
The 12'th term in the tribonacci series is 149.

SAMPLE II
Enter the value of n?, where you need the n'th number in the tribonacci sequence. 1254
The 1254'th term in the tribonacci series is 4020147461713125140.
"""


# Run this cell to load the graphics packages

using Plots; gr()
using Interact

# ------------------------------------------------------------------------------------------
# ## 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:
# ------------------------------------------------------------------------------------------

σ(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:
# ------------------------------------------------------------------------------------------

f(x, w) = σ(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$. However, `Interact.jl` then
# allows us to modify interactively the value of $w$, and plot the new function that comes
# out:
# ------------------------------------------------------------------------------------------

@manipulate for w in -2:0.01:2

    plot(x->f(x, w), -5, 5, ylims=(0,1), label="sigmoid")
    plot!(x->(x>0), -5,5, label="Square Wave")
end

# ------------------------------------------------------------------------------------------
# #### Exercise 1
#
# Try writing your own function that takes a parameter. Start by copying and executing
#
# ```julia
# 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$$
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# #### 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
# @manipulate for a in 0:0.01:10
#     plot(x, square.(x), label="x^2")
#     plot!(x, square_and_scale.(x, a), ls=:dash, label="ax^2")
# end

# ------------------------------------------------------------------------------------------
# ## 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**: Move the slider until the graph of the function hits the data point. Which value
# of $w$ does that correspond to?
# ------------------------------------------------------------------------------------------

x0, y0 = 2, 0.8

@manipulate for w in -2:0.01:2
    plot(x->f(x, w), -5, 5, ylims=(0, 1), label="f")
    scatter!([x0], [y0], label="data")
end

# ------------------------------------------------------------------------------------------
# ## 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.
#
# (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)`.
#
# (c) Make the plot interactive.
#
# (d) Add as the plot title the value of the loss function for the current value of $w$.
#
# (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^*)$?
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# ## 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$.
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# ### Features of the loss function
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# 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.
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# ### 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.
# ------------------------------------------------------------------------------------------

.nbexports/introductory-tutorials/broader-topics-and-ecosystem/intro-to-ml/06. Tools - Adding a function parameter.jl


# function to determine whether an x, y point is in the unit circle
function in_circle(x_pos::Float64, y_pos::Float64)

    # Setting radius to 1 for unit circle
    radius = 1
    return x_pos^2 + y_pos^2 < radius^2
end

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

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

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

    # This is using a quarter of the unit sphere in a 1x1 box.
    # The formula is pi = (box_length^2 / radius^2) * (pi_count / n), but we
    #     are only using the upper quadrant and the unit circle, so we can use
    #     4*pi_count/n instead
    pi_estimate = 4*pi_count/n
    println("The pi estimate is: ", pi_estimate)
    println("Percent error is: ", signif(100 * abs(pi_estimate - pi) / pi, 3), " %")
end

monte_carlo(10000000)


using Symata, Test

println()
@sym begin
  xi = [1, 3, 6]
  yi = [1, 5, 10]
  L0(x_) := Simplify( Expand( ((x-xi[2])*(x-xi[3]))/((xi[1]-xi[2])*(xi[1]-xi[3])) ) )
  L1(x_) := Simplify( Expand( ((x-xi[1])*(x-xi[3]))/((xi[2]-xi[1])*(xi[2]-xi[3])) ) )
  L2(x_) := Simplify( Expand( ((x-xi[1])*(x-xi[2]))/((xi[3]-xi[1])*(xi[3]-xi[2])) ) )
  Q(x_) := Simplify(yi[1]*L0(x) + yi[2]*L1(x) + yi[3]*L2(x));
  SetJ(q, ToString(Q(x)))
end

@sym Print("Q(x) = ", Q(x))
println()
@sym Println("Q(4.5) = ", Q(4.5))

println()
q = @sym ToString(Q(x));
q |> display
println()

@eval f1(x) = $(Meta.parse(q))

@test round(f1(4.5), digits=5) == 7.65

#=
using GR

xint = 1:0.1:6 # FloatRange
p = plot(xint, f1.(xint),"g", [4.5], [f1(4.5)], "o", title="Interpolated curve")
=#


"""
    partitions_recursive(n)

Finds partitions of an integer using recursion.

A partition of a positive integer n, also called an integer partition,
is a way of writing n as a sum of positive integers.

There are 7 partitions of 5:
5
4 + 1
3 + 2
3 + 1 + 1
2 + 2 + 1
2 + 1 + 1 + 1
1 + 1 + 1 + 1 + 1

Partitions of n is equal to the sum of partitions of n with k parts.

``P \\left ( n \\right ) = \\sum_{k=1}^{n} P_{k} \\left ( n \\right )``

Partitions of n with k parts is the sum of
partitions of n-1 with k-1 parts and,
partitions of n-k with k parts.

``P_{k}\\left ( n \\right ) =
P_{k-1}\\left ( n - 1 \\right ) + P_{k}\\left ( n - k \\right )``

# Example
```julia
partitions_recursive(0)      # returns 1
partitions_recursive(1)      # returns 1
partitions_recursive(10)     # returns 42
partitions_recursive(-1)     # returns DomainError
```
# References
- Partitions of a positive integer -- https://en.wikipedia.org/wiki/Partition_function_(number_theory)
- Partitions of a positive integer -- https://www.whitman.edu/mathematics/cgt_online/book/section03.03.html

# Contributor
- [Vaishakh C R](https://github.com/Whiteshark-314)
"""
function partitions_recursive(n::N)::BigInt where {N<:Integer}
    n < 0 && throw(
        DomainError(
            "partitions_recursive() only accepts positive integral values",
        ),
    )
    if n == 0
        return one(BigInt)
    end
    k = collect(1:n)
    return sum(partitions_k_parts.(n, k))
end

function partitions_k_parts(n, k)
    n < 0 ||
        k < 0 && throw(
            DomainError(
                "partitions_k_parts() only accepts positive integral values",
            ),
        )
    if (n == 0 && k == 0) || (n > 0 && k == 1) || n == k
        return one(BigInt)
    elseif k == 0 || k > n
        return zero(BigInt)
    end
    return partitions_k_parts(n - 1, k - 1) + partitions_k_parts(n - k, k)
end


using LinearAlgebra
using SparseArrays
using Plots
using ForwardDiff

uexact(x) = log(2+sin(pi*x))
dudx_exact(x) = ForwardDiff.derivative(uexact,x)
f(x) = dudx_exact(x)-ForwardDiff.derivative(dudx_exact,x) # du/dx - d^2u/dx^2 = f(x)
α = uexact(-1)
β = uexact(1)

function solve(m,f,α,β)
    x = LinRange(-1,1,m+2) # add 2 endpoints
    xint = x[2:end-1]
    h = x[2]-x[1]
    A = (1/h^2) * spdiagm(0=>2*ones(m),-1=>-ones(m-1),1=>-ones(m-1))
    Q = (1/(2*h)) * spdiagm(-1=>-ones(m-1),1=>ones(m-1))
    b = f.(xint)
    b[1] += α/h^2 + α/(2*h)
    b[end] += β/h^2 - β/(2*h)
    u = (Q+A)\b
    return u,x,xint,h
end

function solve(m,ϵ,f,α,β)
    x = LinRange(-1,1,m+2) # add 2 endpoints
    xint = x[2:end-1]
    h = x[2]-x[1]
    A = (1/h^2) * spdiagm(0=>2*ones(m),-1=>-ones(m-1),1=>-ones(m-1))
    Q = (1/(2*h)) * spdiagm(-1=>-ones(m-1),1=>ones(m-1))
    # Q = (1/(h)) * spdiagm(0=>-ones(m-1),1=>ones(m-1))
    b = f.(xint)
    b[1] += ϵ*α/h^2 + α/(2*h)
    b[end] += ϵ*β/h^2 - β/(2*h)
    u = (Q+ϵ*A)\b
    return u,x,xint,h
end

ϵ = .0001
u,x,xint,h = solve(100,ϵ,x->1,0,0)
plot(xint,u,leg=false)

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


module Gcd

export my_gcd

"""
   my_gcd(a, b)

Compute the greatest common divisor of a and b

# Arguments
- `a::Integer`: first argument, not required in real life documentation
- `b::Integer`: second argument, not required in real life documentation

Both arguments should be strictly positive, otherwise a `DomaiunErr` exception will be thrown.

# Examples

```julia-repl
julia> my_gcd(12, 15)
3
julia> my_gcd(13, 11)
1
```
"""
function my_gcd(a::Integer, b::Integer)
    a < 1 && throw(DomainError(a, "argument should be strictly positive"))
    b < 1 && throw(DomainError(b, "argument should be strictly positive"))
    while a != b
        if a > b
            a -= b
        else
            b -= a
        end
    end
    return a
end

end # module


"""
Julia program to implement Eulerian Number
Eulerian number is a number found in combinatorial mathematics. It is the number of permutations of the
numbers 1 to n in which exactly m elements are greater than previous element.
"""

function eulerian(n, m)
    # Build a Dynamic Programming table
    dp = zeros(Int, n + 1, m + 1)

    # Fill the table in Bottom-Up manner
    for i in 2:(n+1)
        for j in 1:(m+1)
            #Update the table only if i greater than j
            if(i > j)
                if(j == 1)
                    dp[i, j] = 1
                else
                    dp[i, j] = (((i - j) * dp[i-1, j-1]) + ((j) * dp[i-1, j]))
                end
            end
        end
    end
    return dp[n + 1,m + 1]
end

print("Enter the value of n? ")
n = readline()
n = parse(Int64, n)
print("Enter the value of m? ")
m = readline()
m = parse(Int64, m)
ans = eulerian(n, m)
print("The required eulerian number is ")
print(ans)

"""
Time Complexity - O(n * m)
Space Complexity - O(n * m)

SAMPLE INPUT AND OUTPUT

SAMPLE I
Enter the value of n? 3
Enter the value of m? 1
The required eulerian number is 4

SAMPLE II
Enter the value of n? 15
Enter the value of m? 6
The required eulerian number is 311387598411
"""